Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public ResponseEntity<RsData<Void>> toggleSetting(

settingService.toggleSetting(currentUser.getUserId(), type);

return ResponseEntity.ok(RsData.success(null));
return ResponseEntity.ok(RsData.success("알림 설정 토글 성공"));
}

@PutMapping("/all")
Expand All @@ -49,6 +49,6 @@ public ResponseEntity<RsData<Void>> toggleAllSettings(

settingService.toggleAllSettings(currentUser.getUserId(), enable);

return ResponseEntity.ok(RsData.success(null));
return ResponseEntity.ok(RsData.success("알림 설정 전체 변경 성공"));
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/back/domain/studyroom/entity/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public class Room extends BaseEntity {
private boolean allowAudio;
private boolean allowScreenShare;

@Builder.Default
@Column(nullable = false)
private Integer currentParticipants = 0;

// 방 상태
@Builder.Default
@Enumerated(EnumType.STRING)
Expand Down Expand Up @@ -161,6 +165,7 @@ public static Room create(String title, String description, boolean isPrivate,
room.allowAudio = useWebRTC; // WebRTC 사용 여부에 따라 설정
room.allowScreenShare = useWebRTC; // WebRTC 사용 여부에 따라 설정
room.status = RoomStatus.WAITING; // 생성 시 대기 상태
room.currentParticipants = 0;
room.createdBy = creator;
room.theme = theme;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ private WebSocketConstants() {
public static final String ROOM_USERS_KEY_PREFIX = "ws:room:";
public static final String ROOM_USERS_KEY_SUFFIX = ":users";

/**
* 전체 온라인 사용자 수 저장 Key
* - 패턴: ws:online_users:count
* - 값: Long (카운트)
*/
public static final String ONLINE_USER_COUNT_KEY = "ws:online_users:count";

// ===== Key 빌더 헬퍼 메서드 =====

public static String buildUserSessionKey(Long userId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.back.global.websocket.controller;

import com.back.global.exception.CustomException;
import com.back.global.websocket.dto.HeartbeatMessage;
import com.back.global.security.user.CustomUserDetails;
import com.back.global.websocket.service.WebSocketSessionManager;
import com.back.global.websocket.util.WebSocketErrorHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;

import java.security.Principal;

@Slf4j
@Controller
@RequiredArgsConstructor
Expand All @@ -22,106 +24,41 @@ public class WebSocketMessageController {

// Heartbeat 처리
@MessageMapping("/heartbeat")
public void handleHeartbeat(@Payload HeartbeatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
try {
if (message.userId() != null) {
// TTL 10분으로 연장
sessionManager.updateLastActivity(message.userId());
log.debug("Heartbeat 처리 완료 - 사용자: {}", message.userId());
} else {
log.warn("유효하지 않은 Heartbeat 메시지 수신: userId가 null");
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
}
} catch (CustomException e) {
log.error("Heartbeat 처리 실패: {}", e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
} catch (Exception e) {
log.error("Heartbeat 처리 중 예상치 못한 오류", e);
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "Heartbeat 처리 중 오류가 발생했습니다");
public void handleHeartbeat(Principal principal, SimpMessageHeaderAccessor headerAccessor) {
if (principal instanceof Authentication auth && auth.getPrincipal() instanceof CustomUserDetails userDetails) {
Long userId = userDetails.getUserId();
sessionManager.updateLastActivity(userId);
log.debug("Heartbeat 처리 완료 - 사용자: {}", userId);
} else {
log.warn("인증되지 않은 Heartbeat 요청: {}", headerAccessor.getSessionId());
errorHelper.sendUnauthorizedError(headerAccessor.getSessionId());
}
}

/**
* 방 입장 처리
*
* @deprecated 이 STOMP 엔드포인트는 REST API로 대체되었습니다.
* 대신 POST /api/rooms/{roomId}/join을 사용하세요.
*
* 참고: REST API 호출 시 자동으로 Redis에 입장 처리되며 WebSocket 알림도 전송됩니다.
* 이 엔드포인트는 하위 호환성을 위해 유지되지만 사용을 권장하지 않습니다.
*/
@Deprecated
@MessageMapping("/rooms/{roomId}/join")
public void handleJoinRoom(@DestinationVariable Long roomId,
@Payload HeartbeatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
try {
if (message.userId() != null) {
sessionManager.joinRoom(message.userId(), roomId);
log.info("STOMP 방 입장 처리 완료 - 사용자: {}, 방: {}", message.userId(), roomId);
} else {
log.warn("유효하지 않은 방 입장 요청: userId가 null");
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
}
} catch (CustomException e) {
log.error("방 입장 처리 실패 - 방: {}, 에러: {}", roomId, e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
} catch (Exception e) {
log.error("방 입장 처리 중 예상치 못한 오류 - 방: {}", roomId, e);
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "방 입장 중 오류가 발생했습니다");
// 사용자 활동 신호 처리
@MessageMapping("/activity")
public void handleActivity(Principal principal, SimpMessageHeaderAccessor headerAccessor) {
if (principal instanceof Authentication auth && auth.getPrincipal() instanceof CustomUserDetails userDetails) {
Long userId = userDetails.getUserId();
sessionManager.updateLastActivity(userId);
log.debug("사용자 활동 신호 처리 완료 - 사용자: {}", userId);
} else {
log.warn("유효하지 않은 활동 신호: 인증 정보 없음");
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
}
}

/**
* 방 퇴장 처리
*
* @deprecated 이 STOMP 엔드포인트는 REST API로 대체되었습니다.
* 대신 POST /api/rooms/{roomId}/leave를 사용하세요.
*
* 참고: REST API 호출 시 자동으로 Redis에서 퇴장 처리되며 WebSocket 알림도 전송됩니다.
* 이 엔드포인트는 하위 호환성을 위해 유지되지만 사용을 권장하지 않습니다.
*/
@Deprecated
@MessageMapping("/rooms/{roomId}/leave")
public void handleLeaveRoom(@DestinationVariable Long roomId,
@Payload HeartbeatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
try {
if (message.userId() != null) {
sessionManager.leaveRoom(message.userId(), roomId);
log.info("STOMP 방 퇴장 처리 완료 - 사용자: {}, 방: {}", message.userId(), roomId);
} else {
log.warn("유효하지 않은 방 퇴장 요청: userId가 null");
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
}
} catch (CustomException e) {
log.error("방 퇴장 처리 실패 - 방: {}, 에러: {}", roomId, e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
} catch (Exception e) {
log.error("방 퇴장 처리 중 예상치 못한 오류 - 방: {}", roomId, e);
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "방 퇴장 중 오류가 발생했습니다");
}
// WebSocket 메시지 처리 중 발생하는 CustomException 처리
@MessageExceptionHandler(CustomException.class)
public void handleCustomException(CustomException e, SimpMessageHeaderAccessor headerAccessor) {
log.error("WebSocket 처리 중 CustomException 발생: {}", e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
}

// 사용자 활동 신호 처리
@MessageMapping("/activity")
public void handleActivity(@Payload HeartbeatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
try {
if (message.userId() != null) {
sessionManager.updateLastActivity(message.userId());
log.debug("사용자 활동 신호 처리 완료 - 사용자: {}", message.userId());
} else {
log.warn("유효하지 않은 활동 신호: userId가 null");
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
}
} catch (CustomException e) {
log.error("활동 신호 처리 실패: {}", e.getMessage());
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
} catch (Exception e) {
log.error("활동 신호 처리 중 예상치 못한 오류", e);
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "활동 신호 처리 중 오류가 발생했습니다");
}
// 예상치 못한 모든 Exception 처리
@MessageExceptionHandler(Exception.class)
public void handleGeneralException(Exception e, SimpMessageHeaderAccessor headerAccessor) {
log.error("WebSocket 처리 중 예상치 못한 오류 발생", e);
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "요청 처리 중 서버 오류가 발생했습니다.");
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public void registerSession(Long userId, String sessionId) {
redisSessionStore.saveUserSession(userId, newSession);
redisSessionStore.saveSessionUserMapping(sessionId, userId);

redisSessionStore.incrementOnlineUserCount();

log.info("WebSocket 세션 등록 완료 - 사용자: {}, 세션: {}", userId, sessionId);
}

Expand All @@ -44,6 +46,9 @@ public void terminateSession(String sessionId) {
if (userId != null) {
redisSessionStore.deleteUserSession(userId);
redisSessionStore.deleteSessionUserMapping(sessionId);

redisSessionStore.decrementOnlineUserCount();

log.info("WebSocket 세션 종료 완료 - 세션: {}, 사용자: {}", sessionId, userId);
} else {
log.warn("종료할 세션을 찾을 수 없음 - 세션: {}", sessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,35 @@ public long getRoomUserCount(Long roomId) {

public long getTotalOnlineUserCount() {
try {
Set<String> userKeys = redisTemplate.keys(WebSocketConstants.buildUserSessionKeyPattern());
return userKeys != null ? userKeys.size() : 0;
// 카운터 키에서 직접 값을 가져옴
Object count = redisTemplate.opsForValue().get(WebSocketConstants.ONLINE_USER_COUNT_KEY);
if (count instanceof Number) {
return ((Number) count).longValue();
}
return 0L;
} catch (Exception e) {
log.error("전체 온라인 사용자 수 조회 실패", e);
return 0;
return 0; // 에러 발생 시 0 반환
}
}

public void incrementOnlineUserCount() {
try {
redisTemplate.opsForValue().increment(WebSocketConstants.ONLINE_USER_COUNT_KEY);
} catch (Exception e) {
log.error("온라인 사용자 수 증가 실패", e);
}
}

public void decrementOnlineUserCount() {
try {
// 카운터가 0보다 작아지지 않도록 방지
Long currentValue = redisTemplate.opsForValue().decrement(WebSocketConstants.ONLINE_USER_COUNT_KEY);
if (currentValue != null && currentValue < 0) {
redisTemplate.opsForValue().set(WebSocketConstants.ONLINE_USER_COUNT_KEY, 0L);
}
} catch (Exception e) {
log.error("온라인 사용자 수 감소 실패", e);
}
}

Expand Down
Loading