Skip to content

Commit 7562c0d

Browse files
authored
Merge branch 'dev' into Refactor/167
2 parents 3c7056f + ee9cf87 commit 7562c0d

File tree

9 files changed

+154
-329
lines changed

9 files changed

+154
-329
lines changed

src/main/java/com/back/domain/notification/controller/NotificationSettingController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public ResponseEntity<RsData<Void>> toggleSetting(
3838

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

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

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

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

52-
return ResponseEntity.ok(RsData.success(null));
52+
return ResponseEntity.ok(RsData.success("알림 설정 전체 변경 성공"));
5353
}
5454
}

src/main/java/com/back/domain/studyroom/entity/Room.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ public class Room extends BaseEntity {
2929
private boolean allowAudio;
3030
private boolean allowScreenShare;
3131

32+
@Builder.Default
33+
@Column(nullable = false)
34+
private Integer currentParticipants = 0;
35+
3236
// 방 상태
3337
@Builder.Default
3438
@Enumerated(EnumType.STRING)
@@ -161,6 +165,7 @@ public static Room create(String title, String description, boolean isPrivate,
161165
room.allowAudio = useWebRTC; // WebRTC 사용 여부에 따라 설정
162166
room.allowScreenShare = useWebRTC; // WebRTC 사용 여부에 따라 설정
163167
room.status = RoomStatus.WAITING; // 생성 시 대기 상태
168+
room.currentParticipants = 0;
164169
room.createdBy = creator;
165170
room.theme = theme;
166171

src/main/java/com/back/global/websocket/config/WebSocketConstants.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ private WebSocketConstants() {
4646
public static final String ROOM_USERS_KEY_PREFIX = "ws:room:";
4747
public static final String ROOM_USERS_KEY_SUFFIX = ":users";
4848

49+
/**
50+
* 전체 온라인 사용자 수 저장 Key
51+
* - 패턴: ws:online_users:count
52+
* - 값: Long (카운트)
53+
*/
54+
public static final String ONLINE_USER_COUNT_KEY = "ws:online_users:count";
55+
4956
// ===== Key 빌더 헬퍼 메서드 =====
5057

5158
public static String buildUserSessionKey(Long userId) {
Lines changed: 33 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
package com.back.global.websocket.controller;
22

33
import com.back.global.exception.CustomException;
4-
import com.back.global.websocket.dto.HeartbeatMessage;
4+
import com.back.global.security.user.CustomUserDetails;
55
import com.back.global.websocket.service.WebSocketSessionManager;
66
import com.back.global.websocket.util.WebSocketErrorHelper;
77
import lombok.RequiredArgsConstructor;
88
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
910
import org.springframework.messaging.handler.annotation.MessageMapping;
10-
import org.springframework.messaging.handler.annotation.DestinationVariable;
11-
import org.springframework.messaging.handler.annotation.Payload;
1211
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
12+
import org.springframework.security.core.Authentication;
1313
import org.springframework.stereotype.Controller;
1414

15+
import java.security.Principal;
16+
1517
@Slf4j
1618
@Controller
1719
@RequiredArgsConstructor
@@ -22,106 +24,41 @@ public class WebSocketMessageController {
2224

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

45-
/**
46-
* 방 입장 처리
47-
*
48-
* @deprecated 이 STOMP 엔드포인트는 REST API로 대체되었습니다.
49-
* 대신 POST /api/rooms/{roomId}/join을 사용하세요.
50-
*
51-
* 참고: REST API 호출 시 자동으로 Redis에 입장 처리되며 WebSocket 알림도 전송됩니다.
52-
* 이 엔드포인트는 하위 호환성을 위해 유지되지만 사용을 권장하지 않습니다.
53-
*/
54-
@Deprecated
55-
@MessageMapping("/rooms/{roomId}/join")
56-
public void handleJoinRoom(@DestinationVariable Long roomId,
57-
@Payload HeartbeatMessage message,
58-
SimpMessageHeaderAccessor headerAccessor) {
59-
try {
60-
if (message.userId() != null) {
61-
sessionManager.joinRoom(message.userId(), roomId);
62-
log.info("STOMP 방 입장 처리 완료 - 사용자: {}, 방: {}", message.userId(), roomId);
63-
} else {
64-
log.warn("유효하지 않은 방 입장 요청: userId가 null");
65-
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
66-
}
67-
} catch (CustomException e) {
68-
log.error("방 입장 처리 실패 - 방: {}, 에러: {}", roomId, e.getMessage());
69-
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
70-
} catch (Exception e) {
71-
log.error("방 입장 처리 중 예상치 못한 오류 - 방: {}", roomId, e);
72-
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "방 입장 중 오류가 발생했습니다");
38+
// 사용자 활동 신호 처리
39+
@MessageMapping("/activity")
40+
public void handleActivity(Principal principal, SimpMessageHeaderAccessor headerAccessor) {
41+
if (principal instanceof Authentication auth && auth.getPrincipal() instanceof CustomUserDetails userDetails) {
42+
Long userId = userDetails.getUserId();
43+
sessionManager.updateLastActivity(userId);
44+
log.debug("사용자 활동 신호 처리 완료 - 사용자: {}", userId);
45+
} else {
46+
log.warn("유효하지 않은 활동 신호: 인증 정보 없음");
47+
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
7348
}
7449
}
7550

76-
/**
77-
* 방 퇴장 처리
78-
*
79-
* @deprecated 이 STOMP 엔드포인트는 REST API로 대체되었습니다.
80-
* 대신 POST /api/rooms/{roomId}/leave를 사용하세요.
81-
*
82-
* 참고: REST API 호출 시 자동으로 Redis에서 퇴장 처리되며 WebSocket 알림도 전송됩니다.
83-
* 이 엔드포인트는 하위 호환성을 위해 유지되지만 사용을 권장하지 않습니다.
84-
*/
85-
@Deprecated
86-
@MessageMapping("/rooms/{roomId}/leave")
87-
public void handleLeaveRoom(@DestinationVariable Long roomId,
88-
@Payload HeartbeatMessage message,
89-
SimpMessageHeaderAccessor headerAccessor) {
90-
try {
91-
if (message.userId() != null) {
92-
sessionManager.leaveRoom(message.userId(), roomId);
93-
log.info("STOMP 방 퇴장 처리 완료 - 사용자: {}, 방: {}", message.userId(), roomId);
94-
} else {
95-
log.warn("유효하지 않은 방 퇴장 요청: userId가 null");
96-
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
97-
}
98-
} catch (CustomException e) {
99-
log.error("방 퇴장 처리 실패 - 방: {}, 에러: {}", roomId, e.getMessage());
100-
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
101-
} catch (Exception e) {
102-
log.error("방 퇴장 처리 중 예상치 못한 오류 - 방: {}", roomId, e);
103-
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "방 퇴장 중 오류가 발생했습니다");
104-
}
51+
// WebSocket 메시지 처리 중 발생하는 CustomException 처리
52+
@MessageExceptionHandler(CustomException.class)
53+
public void handleCustomException(CustomException e, SimpMessageHeaderAccessor headerAccessor) {
54+
log.error("WebSocket 처리 중 CustomException 발생: {}", e.getMessage());
55+
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
10556
}
10657

107-
// 사용자 활동 신호 처리
108-
@MessageMapping("/activity")
109-
public void handleActivity(@Payload HeartbeatMessage message,
110-
SimpMessageHeaderAccessor headerAccessor) {
111-
try {
112-
if (message.userId() != null) {
113-
sessionManager.updateLastActivity(message.userId());
114-
log.debug("사용자 활동 신호 처리 완료 - 사용자: {}", message.userId());
115-
} else {
116-
log.warn("유효하지 않은 활동 신호: userId가 null");
117-
errorHelper.sendInvalidRequestError(headerAccessor.getSessionId(), "사용자 ID가 필요합니다");
118-
}
119-
} catch (CustomException e) {
120-
log.error("활동 신호 처리 실패: {}", e.getMessage());
121-
errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e);
122-
} catch (Exception e) {
123-
log.error("활동 신호 처리 중 예상치 못한 오류", e);
124-
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "활동 신호 처리 중 오류가 발생했습니다");
125-
}
58+
// 예상치 못한 모든 Exception 처리
59+
@MessageExceptionHandler(Exception.class)
60+
public void handleGeneralException(Exception e, SimpMessageHeaderAccessor headerAccessor) {
61+
log.error("WebSocket 처리 중 예상치 못한 오류 발생", e);
62+
errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "요청 처리 중 서버 오류가 발생했습니다.");
12663
}
12764
}

src/main/java/com/back/global/websocket/dto/HeartbeatMessage.java

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/main/java/com/back/global/websocket/service/UserSessionService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public void registerSession(Long userId, String sessionId) {
3434
redisSessionStore.saveUserSession(userId, newSession);
3535
redisSessionStore.saveSessionUserMapping(sessionId, userId);
3636

37+
redisSessionStore.incrementOnlineUserCount();
38+
3739
log.info("WebSocket 세션 등록 완료 - 사용자: {}, 세션: {}", userId, sessionId);
3840
}
3941

@@ -44,6 +46,9 @@ public void terminateSession(String sessionId) {
4446
if (userId != null) {
4547
redisSessionStore.deleteUserSession(userId);
4648
redisSessionStore.deleteSessionUserMapping(sessionId);
49+
50+
redisSessionStore.decrementOnlineUserCount();
51+
4752
log.info("WebSocket 세션 종료 완료 - 세션: {}, 사용자: {}", sessionId, userId);
4853
} else {
4954
log.warn("종료할 세션을 찾을 수 없음 - 세션: {}", sessionId);

src/main/java/com/back/global/websocket/store/RedisSessionStore.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,35 @@ public long getRoomUserCount(Long roomId) {
182182

183183
public long getTotalOnlineUserCount() {
184184
try {
185-
Set<String> userKeys = redisTemplate.keys(WebSocketConstants.buildUserSessionKeyPattern());
186-
return userKeys != null ? userKeys.size() : 0;
185+
// 카운터 키에서 직접 값을 가져옴
186+
Object count = redisTemplate.opsForValue().get(WebSocketConstants.ONLINE_USER_COUNT_KEY);
187+
if (count instanceof Number) {
188+
return ((Number) count).longValue();
189+
}
190+
return 0L;
187191
} catch (Exception e) {
188192
log.error("전체 온라인 사용자 수 조회 실패", e);
189-
return 0;
193+
return 0; // 에러 발생 시 0 반환
194+
}
195+
}
196+
197+
public void incrementOnlineUserCount() {
198+
try {
199+
redisTemplate.opsForValue().increment(WebSocketConstants.ONLINE_USER_COUNT_KEY);
200+
} catch (Exception e) {
201+
log.error("온라인 사용자 수 증가 실패", e);
202+
}
203+
}
204+
205+
public void decrementOnlineUserCount() {
206+
try {
207+
// 카운터가 0보다 작아지지 않도록 방지
208+
Long currentValue = redisTemplate.opsForValue().decrement(WebSocketConstants.ONLINE_USER_COUNT_KEY);
209+
if (currentValue != null && currentValue < 0) {
210+
redisTemplate.opsForValue().set(WebSocketConstants.ONLINE_USER_COUNT_KEY, 0L);
211+
}
212+
} catch (Exception e) {
213+
log.error("온라인 사용자 수 감소 실패", e);
190214
}
191215
}
192216

0 commit comments

Comments
 (0)