11package com .back .global .websocket .service ;
22
3+ import com .back .domain .user .common .entity .User ;
4+ import com .back .domain .user .common .repository .UserRepository ;
35import com .back .global .exception .CustomException ;
46import com .back .global .exception .ErrorCode ;
57import com .back .global .websocket .dto .WebSocketSessionInfo ;
8+ import com .back .global .websocket .event .SessionDisconnectedEvent ;
9+ import com .back .global .websocket .event .UserJoinedEvent ;
10+ import com .back .global .websocket .event .UserLeftEvent ;
611import com .back .global .websocket .store .RedisSessionStore ;
712import lombok .RequiredArgsConstructor ;
813import lombok .extern .slf4j .Slf4j ;
14+ import org .springframework .context .event .EventListener ;
15+ import org .springframework .messaging .simp .SimpMessagingTemplate ;
916import org .springframework .stereotype .Service ;
17+ import org .springframework .transaction .annotation .Transactional ;
1018
19+ import java .util .Map ;
1120import java .util .Set ;
1221
1322/**
2231public class RoomParticipantService {
2332
2433 private final RedisSessionStore redisSessionStore ;
34+ private final SimpMessagingTemplate messagingTemplate ;
35+ private final UserRepository userRepository ;
36+
37+ // 세션 종료 이벤트 리스너
38+ @ EventListener
39+ @ Transactional
40+ public void handleSessionDisconnected (SessionDisconnectedEvent event ) {
41+ Long userId = event .getUserId ();
42+ log .info ("[이벤트 수신] 세션 종료 이벤트 수신하여 퇴장 처리 시작 - 사용자: {}" , userId );
43+ exitAllRooms (userId );
44+ }
2545
2646 // 사용자 방 입장 (아바타 정보 포함)
2747 public void enterRoom (Long userId , Long roomId , Long avatarId ) {
@@ -32,7 +52,7 @@ public void enterRoom(Long userId, Long roomId, Long avatarId) {
3252 throw new CustomException (ErrorCode .WS_SESSION_NOT_FOUND );
3353 }
3454
35- if (sessionInfo .currentRoomId () != null ) {
55+ if (sessionInfo .currentRoomId () != null && ! sessionInfo . currentRoomId (). equals ( roomId ) ) {
3656 exitRoom (userId , sessionInfo .currentRoomId ());
3757 log .debug ("기존 방에서 퇴장 처리 완료 - 사용자: {}, 이전 방: {}" ,
3858 userId , sessionInfo .currentRoomId ());
@@ -46,6 +66,8 @@ public void enterRoom(Long userId, Long roomId, Long avatarId) {
4666 saveUserAvatar (roomId , userId , avatarId );
4767
4868 log .info ("방 입장 완료 - 사용자: {}, 방: {}, 아바타: {}" , userId , roomId , avatarId );
69+
70+ broadcastUserJoined (roomId , userId , avatarId );
4971 }
5072
5173 // 기존 메서드 호환성 유지 (아바타 없이 입장)
@@ -56,16 +78,25 @@ public void enterRoom(Long userId, Long roomId) {
5678 // 사용자 방 퇴장
5779 public void exitRoom (Long userId , Long roomId ) {
5880 WebSocketSessionInfo sessionInfo = redisSessionStore .getUserSession (userId );
59-
60- if (sessionInfo == null ) {
61- log .warn ("세션 정보가 없지만 방 퇴장 처리 계속 진행 - 사용자: {}, 방: {}" , userId , roomId );
62- } else {
81+ if (sessionInfo != null ) {
6382 WebSocketSessionInfo updatedSession = sessionInfo .withoutRoom ();
6483 redisSessionStore .saveUserSession (userId , updatedSession );
6584 }
66-
6785 redisSessionStore .removeUserFromRoom (roomId , userId );
6886 log .info ("방 퇴장 완료 - 사용자: {}, 방: {}" , userId , roomId );
87+ broadcastUserLeft (roomId , userId );
88+ }
89+
90+ public void exitAllRooms (Long userId ) {
91+ try {
92+ Long currentRoomId = getCurrentRoomId (userId );
93+ if (currentRoomId != null ) {
94+ exitRoom (userId , currentRoomId );
95+ log .info ("모든 방에서 퇴장 처리 완료 - 사용자: {}" , userId );
96+ }
97+ } catch (Exception e ) {
98+ log .error ("모든 방 퇴장 처리 실패 - 사용자: {}" , userId , e );
99+ }
69100 }
70101
71102 // 사용자의 현재 방 ID 조회
@@ -90,29 +121,13 @@ public boolean isUserInRoom(Long userId, Long roomId) {
90121 return currentRoomId != null && currentRoomId .equals (roomId );
91122 }
92123
93- // 모든 방에서 사용자 퇴장 처리 (세션 종료 시 사용)
94- public void exitAllRooms (Long userId ) {
95- try {
96- Long currentRoomId = getCurrentRoomId (userId );
97-
98- if (currentRoomId != null ) {
99- exitRoom (userId , currentRoomId );
100- log .info ("모든 방에서 퇴장 처리 완료 - 사용자: {}" , userId );
101- }
102-
103- } catch (Exception e ) {
104- log .error ("모든 방 퇴장 처리 실패 - 사용자: {}" , userId , e );
105- // 에러를 던지지 않고 로그만 남김 (세션 종료는 계속 진행되어야 함)
106- }
107- }
108-
109124 /**
110125 * 여러 방의 온라인 참가자 수를 일괄 조회
111126 * N+1 문제 해결을 위한 일괄 조회 메서드
112127 * @param roomIds 방 ID 목록
113128 * @return 방 ID → 참가자 수 맵
114129 */
115- public java . util . Map <Long , Long > getParticipantCounts (java .util .List <Long > roomIds ) {
130+ public Map <Long , Long > getParticipantCounts (java .util .List <Long > roomIds ) {
116131 return redisSessionStore .getRoomUserCounts (roomIds );
117132 }
118133
@@ -125,18 +140,12 @@ public java.util.Map<Long, Long> getParticipantCounts(java.util.List<Long> roomI
125140 * @param avatarId 아바타 ID
126141 */
127142 private void saveUserAvatar (Long roomId , Long userId , Long avatarId ) {
128- if (avatarId == null ) {
129- return ; // 아바타 정보가 없으면 저장하지 않음
130- }
131-
143+ if (avatarId == null ) return ;
132144 String avatarKey = buildAvatarKey (roomId , userId );
133- redisSessionStore .saveValue (avatarKey , avatarId .toString (),
134- java .time .Duration .ofMinutes (6 ));
135-
136- log .debug ("아바타 정보 저장 - RoomId: {}, UserId: {}, AvatarId: {}" ,
137- roomId , userId , avatarId );
145+ redisSessionStore .saveValue (avatarKey , avatarId .toString (), java .time .Duration .ofMinutes (6 ));
146+ log .debug ("아바타 정보 저장 - RoomId: {}, UserId: {}, AvatarId: {}" , roomId , userId , avatarId );
138147 }
139-
148+
140149 /**
141150 * 사용자의 아바타 ID 조회
142151 * @param roomId 방 ID
@@ -154,21 +163,19 @@ public Long getUserAvatar(Long roomId, Long userId) {
154163 try {
155164 return Long .parseLong (avatarIdStr );
156165 } catch (NumberFormatException e ) {
157- log .warn ("아바타 ID 파싱 실패 - RoomId: {}, UserId: {}, Value: {}" ,
158- roomId , userId , avatarIdStr );
166+ log .warn ("아바타 ID 파싱 실패 - RoomId: {}, UserId: {}, Value: {}" , roomId , userId , avatarIdStr );
159167 return null ;
160168 }
161169 }
162-
170+
163171 /**
164172 * 여러 사용자의 아바타 ID 일괄 조회 (N+1 방지)
165173 * @param roomId 방 ID
166174 * @param userIds 사용자 ID 목록
167175 * @return 사용자 ID → 아바타 ID 맵
168176 */
169- public java .util .Map <Long , Long > getUserAvatars (Long roomId , Set <Long > userIds ) {
170- java .util .Map <Long , Long > result = new java .util .HashMap <>();
171-
177+ public Map <Long , Long > getUserAvatars (Long roomId , Set <Long > userIds ) {
178+ Map <Long , Long > result = new java .util .HashMap <>();
172179 for (Long userId : userIds ) {
173180 Long avatarId = getUserAvatar (roomId , userId );
174181 if (avatarId != null ) {
@@ -194,15 +201,28 @@ private String buildAvatarKey(Long roomId, Long userId) {
194201 * @param avatarId 새 아바타 ID
195202 */
196203 public void updateUserAvatar (Long roomId , Long userId , Long avatarId ) {
197- if (avatarId == null ) {
204+ if (avatarId == null ) return ;
205+ String avatarKey = buildAvatarKey (roomId , userId );
206+ redisSessionStore .saveValue (avatarKey , avatarId .toString (), java .time .Duration .ofMinutes (6 ));
207+ log .info ("아바타 업데이트 (Redis) - RoomId: {}, UserId: {}, AvatarId: {}" , roomId , userId , avatarId );
208+ }
209+
210+ private void broadcastUserJoined (Long roomId , Long userId , Long avatarId ) {
211+ User user = userRepository .findById (userId ).orElse (null );
212+ if (user == null ) {
213+ log .error ("📢 [방송 실패] 사용자 정보를 찾을 수 없어 입장 알림을 보낼 수 없습니다. userId: {}" , userId );
198214 return ;
199215 }
200-
201- String avatarKey = buildAvatarKey (roomId , userId );
202- redisSessionStore .saveValue (avatarKey , avatarId .toString (),
203- java .time .Duration .ofMinutes (6 ));
204-
205- log .info ("아바타 업데이트 (Redis) - RoomId: {}, UserId: {}, AvatarId: {}" ,
206- roomId , userId , avatarId );
216+ UserJoinedEvent event = new UserJoinedEvent (user .getId (), user .getNickname (), user .getProfileImageUrl (), avatarId );
217+ String destination = "/topic/room/" + roomId + "/events" ;
218+ messagingTemplate .convertAndSend (destination , event );
219+ log .info ("📢 [방송] 사용자 입장 알림 - 방: {}, 사용자: {}" , roomId , userId );
220+ }
221+
222+ private void broadcastUserLeft (Long roomId , Long userId ) {
223+ UserLeftEvent event = new UserLeftEvent (userId );
224+ String destination = "/topic/room/" + roomId + "/events" ;
225+ messagingTemplate .convertAndSend (destination , event );
226+ log .info ("📢 [방송] 사용자 퇴장 알림 - 방: {}, 사용자: {}" , roomId , userId );
207227 }
208228}
0 commit comments