11package cmf .commitField .global .websocket ;
22
3+ import cmf .commitField .domain .chat .chatMessage .controller .request .ChatMsgRequest ;
4+ import cmf .commitField .domain .chat .chatMessage .controller .response .ChatMsgResponse ;
5+ import cmf .commitField .domain .chat .chatMessage .service .ChatMessageService ;
6+ import cmf .commitField .domain .user .entity .User ;
7+ import cmf .commitField .domain .user .repository .UserRepository ;
38import cmf .commitField .global .error .ErrorCode ;
49import cmf .commitField .global .exception .CustomException ;
10+ import com .fasterxml .jackson .databind .JsonNode ;
11+ import com .fasterxml .jackson .databind .ObjectMapper ;
12+ import lombok .RequiredArgsConstructor ;
513import lombok .extern .slf4j .Slf4j ;
614import org .springframework .stereotype .Component ;
715import org .springframework .web .socket .*;
816
917import java .io .IOException ;
18+ import java .time .LocalDateTime ;
1019import java .util .*;
1120
1221@ Component
1322@ Slf4j
23+ @ RequiredArgsConstructor
1424public class ChatWebSocketHandler implements WebSocketHandler {
1525
1626 private final Map <Long , List <WebSocketSession >> chatRooms = new HashMap <>();
17- // 방의 키값
18-
27+ private final ObjectMapper objectMapper = new ObjectMapper ();
28+ private final ChatMessageService chatMessageService ;
29+ private final UserRepository userRepository ;
1930
2031 // 연결이 되었을 때
2132 @ Override
2233 public void afterConnectionEstablished (WebSocketSession session )
2334 throws Exception {
24- // list.add(session);
25- Long roomId = extractRoomId (session );
26- // roomId 가 없을 경우, session list (new ArrayList)
27- List <WebSocketSession > roomSessions = chatRooms .getOrDefault (roomId , new ArrayList <>());
28- // 세션 추가
29- roomSessions .add (session );
30- // 해당 방의 키값에 session list 추가
31- chatRooms .put (roomId , roomSessions );
32- log .info (session + "의 클라이언트 접속" );
35+ log .info ("클라이언트 접속: {}" , session .getId ());
36+
37+ // 연결 성공 메시지 전송
38+ Map <String , Object > connectMessage = new HashMap <>();
39+ connectMessage .put ("type" , "SYSTEM" );
40+ connectMessage .put ("message" , "채팅 서버에 연결되었습니다." );
41+ connectMessage .put ("timestamp" , LocalDateTime .now ().toString ());
42+
43+ try {
44+ session .sendMessage (new TextMessage (objectMapper .writeValueAsString (connectMessage )));
45+ } catch (Exception e ) {
46+ log .error ("연결 메시지 전송 실패: {}" , e .getMessage ());
47+ }
3348 }
3449
3550 // 클라이언트로부터 받은 메시지를 처리하는 로직
3651 @ Override
3752 public void handleMessage (WebSocketSession session , WebSocketMessage <?> message )
3853 throws Exception {
39- // 메시지 처리 로직
40- Long roomId = extractRoomId (session );
54+ String payload = message .getPayload ().toString ();
55+ log .info ("메시지 수신: {}" , payload );
56+
57+ try {
58+ JsonNode jsonNode = objectMapper .readTree (payload );
59+ String messageType = jsonNode .has ("type" ) ? jsonNode .get ("type" ).asText () : "UNKNOWN" ;
60+
61+ switch (messageType ) {
62+ case "SUBSCRIBE" :
63+ handleSubscribe (session , jsonNode );
64+ break ;
65+ case "UNSUBSCRIBE" :
66+ handleUnsubscribe (session , jsonNode );
67+ break ;
68+ case "CHAT" :
69+ handleChatMessage (session , jsonNode );
70+ break ;
71+ default :
72+ log .warn ("알 수 없는 메시지 타입: {}" , messageType );
73+ sendErrorMessage (session , "지원하지 않는 메시지 타입입니다: " + messageType );
74+ }
75+ } catch (Exception e ) {
76+ log .error ("메시지 처리 중 오류 발생: {}" , e .getMessage (), e );
77+ // 오류 메시지 전송
78+ sendErrorMessage (session , "메시지 처리 중 오류가 발생했습니다: " + e .getMessage ());
79+ }
80+ }
81+
82+ // 구독 메시지 처리
83+ private void handleSubscribe (WebSocketSession session , JsonNode jsonNode ) {
84+ try {
85+ if (!jsonNode .has ("roomId" )) {
86+ sendErrorMessage (session , "roomId 필드가 누락되었습니다." );
87+ return ;
88+ }
89+
90+ Long roomId = jsonNode .get ("roomId" ).asLong ();
91+ log .info ("채팅방 구독 요청: roomId={}, sessionId={}" , roomId , session .getId ());
92+
93+ // 해당 룸의 세션 목록에 추가
94+ List <WebSocketSession > roomSessions = chatRooms .getOrDefault (roomId , new ArrayList <>());
95+
96+ // 이미 등록된 세션인지 확인하여 중복 등록 방지
97+ boolean alreadyRegistered = roomSessions .stream ()
98+ .anyMatch (existingSession -> existingSession .getId ().equals (session .getId ()));
99+
100+ if (!alreadyRegistered ) {
101+ roomSessions .add (session );
102+ chatRooms .put (roomId , roomSessions );
103+ log .info ("클라이언트 세션 {}가 룸 {}에 구독됨" , session .getId (), roomId );
104+
105+ // 구독 확인 메시지 전송
106+ Map <String , Object > subscribeResponse = new HashMap <>();
107+ subscribeResponse .put ("type" , "SUBSCRIBE_ACK" );
108+ subscribeResponse .put ("roomId" , roomId );
109+ subscribeResponse .put ("timestamp" , LocalDateTime .now ().toString ());
110+ subscribeResponse .put ("message" , "채팅방에 연결되었습니다." );
111+
112+ session .sendMessage (new TextMessage (objectMapper .writeValueAsString (subscribeResponse )));
113+ } else {
114+ log .info ("이미 구독 중인 세션: sessionId={}, roomId={}" , session .getId (), roomId );
115+ }
116+ } catch (Exception e ) {
117+ log .error ("구독 처리 중 오류: {}" , e .getMessage (), e );
118+ try {
119+ sendErrorMessage (session , "구독 처리 중 오류: " + e .getMessage ());
120+ } catch (IOException ex ) {
121+ log .error ("오류 메시지 전송 실패: {}" , ex .getMessage ());
122+ }
123+ }
124+ }
125+
126+ // 구독 해제 메시지 처리
127+ private void handleUnsubscribe (WebSocketSession session , JsonNode jsonNode ) {
128+ try {
129+ if (!jsonNode .has ("roomId" )) {
130+ sendErrorMessage (session , "roomId 필드가 누락되었습니다." );
131+ return ;
132+ }
133+
134+ Long roomId = jsonNode .get ("roomId" ).asLong ();
135+
136+ List <WebSocketSession > roomSessions = chatRooms .get (roomId );
137+ if (roomSessions != null ) {
138+ roomSessions .removeIf (existingSession -> existingSession .getId ().equals (session .getId ()));
139+ log .info ("클라이언트 세션 {}가 룸 {}에서 구독 해제됨" , session .getId (), roomId );
140+
141+ // 구독 해제가 성공적으로 처리되었음을 알리는 메시지 전송
142+ Map <String , Object > unsubscribeResponse = new HashMap <>();
143+ unsubscribeResponse .put ("type" , "UNSUBSCRIBE_ACK" );
144+ unsubscribeResponse .put ("roomId" , roomId );
145+ unsubscribeResponse .put ("timestamp" , LocalDateTime .now ().toString ());
146+ unsubscribeResponse .put ("message" , "채팅방에서 연결이 해제되었습니다." );
147+
148+ session .sendMessage (new TextMessage (objectMapper .writeValueAsString (unsubscribeResponse )));
149+ } else {
150+ log .warn ("존재하지 않는 채팅방 구독 해제 시도: roomId={}" , roomId );
151+ sendErrorMessage (session , "존재하지 않는 채팅방입니다: " + roomId );
152+ }
153+ } catch (Exception e ) {
154+ log .error ("구독 해제 처리 중 오류: {}" , e .getMessage (), e );
155+ try {
156+ sendErrorMessage (session , "구독 해제 처리 중 오류: " + e .getMessage ());
157+ } catch (IOException ex ) {
158+ log .error ("오류 메시지 전송 실패: {}" , ex .getMessage ());
159+ }
160+ }
161+ }
162+
163+ // 채팅 메시지 처리
164+ private void handleChatMessage (WebSocketSession session , JsonNode jsonNode ) {
165+ try {
166+ // 필수 필드 검증
167+ if (!jsonNode .has ("roomId" ) || !jsonNode .has ("message" ) || !jsonNode .has ("userId" )) {
168+ sendErrorMessage (session , "필수 필드가 누락되었습니다. (roomId, message, userId 필요)" );
169+ return ;
170+ }
171+
172+ Long roomId = jsonNode .get ("roomId" ).asLong ();
173+ Long userId = jsonNode .get ("userId" ).asLong ();
174+ String message = jsonNode .get ("message" ).asText ();
175+
176+ if (message == null || message .trim ().isEmpty ()) {
177+ sendErrorMessage (session , "메시지 내용이 비어있습니다." );
178+ return ;
179+ }
180+
181+ log .info ("채팅 메시지: roomId={}, userId={}, message={}" , roomId , userId , message );
182+
183+ // 사용자 정보 검증
184+ User user = userRepository .findById (userId ).orElse (null );
185+ if (user == null ) {
186+ log .warn ("존재하지 않는 사용자: userId={}" , userId );
187+ sendErrorMessage (session , "존재하지 않는 사용자입니다." );
188+ return ;
189+ }
190+
191+ // 메시지 저장 및 처리
192+ try {
193+ ChatMsgRequest chatMsgRequest = new ChatMsgRequest (message );
194+ ChatMsgResponse response = chatMessageService .sendMessage (chatMsgRequest , userId , roomId );
195+
196+ // 메시지 포맷 변환하여 전송
197+ String messageJson = objectMapper .writeValueAsString (response );
198+
199+ // 해당 채팅방의 모든 세션에 메시지 브로드캐스트
200+ broadcastMessageToRoom (roomId , messageJson );
201+ } catch (Exception e ) {
202+ log .error ("메시지 저장 처리 중 오류: {}" , e .getMessage (), e );
203+ sendErrorMessage (session , "메시지 전송 중 오류가 발생했습니다: " + e .getMessage ());
204+ }
205+
206+ } catch (Exception e ) {
207+ log .error ("채팅 메시지 처리 중 오류: {}" , e .getMessage (), e );
208+ try {
209+ sendErrorMessage (session , "메시지 전송 중 오류가 발생했습니다: " + e .getMessage ());
210+ } catch (IOException ex ) {
211+ log .error ("오류 메시지 전송 실패: {}" , ex .getMessage ());
212+ }
213+ }
214+ }
215+
216+ // 특정 채팅방에 메시지 브로드캐스트
217+ private void broadcastMessageToRoom (Long roomId , String message ) {
41218 List <WebSocketSession > roomSessions = chatRooms .get (roomId );
42219 if (roomSessions != null ) {
43- String payload = message .getPayload ().toString ();
44- log .info ("전송 메시지: " + payload );
220+ List <WebSocketSession > failedSessions = new ArrayList <>();
45221
46- for (WebSocketSession msg : roomSessions ) {
222+ for (WebSocketSession session : roomSessions ) {
47223 try {
48- msg .sendMessage (message );
224+ if (session .isOpen ()) {
225+ session .sendMessage (new TextMessage (message ));
226+ } else {
227+ failedSessions .add (session );
228+ }
49229 } catch (IOException e ) {
50- throw new CustomException (ErrorCode .CHAT_ERROR );
230+ log .error ("메시지 브로드캐스트 중 오류: {}" , e .getMessage ());
231+ failedSessions .add (session );
51232 }
52233 }
234+
235+ // 실패한 세션 정리
236+ if (!failedSessions .isEmpty ()) {
237+ log .info ("닫힌 세션 정리: {} 개의 세션 제거" , failedSessions .size ());
238+ roomSessions .removeAll (failedSessions );
239+ }
53240 } else {
54- log .info ("해당 채팅방에 클라이언트가 없습니다." );
55- throw new CustomException (ErrorCode .NOT_EXIST_CLIENT );
241+ log .warn ("존재하지 않는 채팅방에 메시지 전송 시도: roomId={}" , roomId );
56242 }
57243 }
58244
59- //오류 처리 로직을 구현 (네트워크 오류, 프로토콜 오류, 처리 오류... 생각 중)
245+ // 오류 메시지 전송
246+ private void sendErrorMessage (WebSocketSession session , String errorMessage ) throws IOException {
247+ Map <String , Object > errorResponse = new HashMap <>();
248+ errorResponse .put ("type" , "ERROR" );
249+ errorResponse .put ("message" , errorMessage );
250+ errorResponse .put ("timestamp" , LocalDateTime .now ().toString ());
251+
252+ session .sendMessage (new TextMessage (objectMapper .writeValueAsString (errorResponse )));
253+ }
254+
255+ // 오류 처리 로직
60256 @ Override
61257 public void handleTransportError (WebSocketSession session , Throwable exception )
62258 throws Exception {
63- log .error (exception .getMessage ());
259+ log .error ("WebSocket 통신 오류: {}" , exception .getMessage (), exception );
64260 }
65261
66262 // 연결 종료되었을 때
67263 @ Override
68264 public void afterConnectionClosed (WebSocketSession session , CloseStatus closeStatus )
69265 throws Exception {
70- Long roomId = extractRoomId ( session ); // 클라이언트가 속한 채팅방 ID를 추출
266+ log . info ( "클라이언트 접속 해제: {}, 상태코드: {}" , session . getId (), closeStatus );
71267
72- List <WebSocketSession > roomSessions = chatRooms .get (roomId );
73- if (roomSessions != null ) {
74- roomSessions .remove (session );
268+ // 모든 채팅방에서 세션 제거
269+ for (Map .Entry <Long , List <WebSocketSession >> entry : chatRooms .entrySet ()) {
270+ Long roomId = entry .getKey ();
271+ List <WebSocketSession > roomSessions = entry .getValue ();
272+
273+ boolean removed = roomSessions .removeIf (existingSession ->
274+ existingSession .getId ().equals (session .getId ()));
275+
276+ if (removed ) {
277+ log .info ("세션이 채팅방 {}에서 제거됨: {}" , roomId , session .getId ());
278+ }
75279 }
76- log .info (session + "의 클라이언트 접속 해제" );
77280 }
78281
79- //부분 메시지를 지원하는지 여부를 반환 (아직까지는 필요 없으니 false)
80- //대용량(사진이나 동영상 등)이 필요한 경우에는 따로 구현할 필요가 있음.
282+ // 부분 메시지 지원 여부
81283 @ Override
82284 public boolean supportsPartialMessages () {
83285 return false ;
84286 }
85-
86- private Long extractRoomId (WebSocketSession session ) {
87- Long roomId = null ;
88- String uri = Objects .requireNonNull (session .getUri ()).toString ();
89- String [] uriParts = uri .split ("/" );
90- // EX_URL) /chat/room/{roomId} 일 때 roomId 추출
91- // 늘어난다면 수 변경해주면.. (일단 임시로 설정)
92- // if (uriParts.length >= 3 && uriParts[2].equals("room")) {
93- // roomId = Long.valueOf(uriParts[3]);
94- if (uriParts .length >= 4 && uriParts [2 ].equals ("msg" )) {
95- return Long .valueOf (uriParts [3 ]);
96- }
97- // /chat/room/join/{roomId}, /chat/room/out/{roomId}, /chat/room/delete/{roomId} 일 때 roomId 추출
98- if (uriParts .length >= 5 && uriParts [2 ].equals ("room" ) &&
99- (uriParts [3 ].equals ("join" ) || uriParts [3 ].equals ("out" ) || uriParts [3 ].equals ("delete" ))) {
100- roomId = Long .valueOf (uriParts [4 ]);
101- }
102- return roomId ;
103- }
104- //메세지 전송
105- public void sendMessage (String payload ) throws Exception {
106- for (List <WebSocketSession > sessions : chatRooms .values ()) {
107- for (WebSocketSession session : sessions ) {
108- TextMessage msg = new TextMessage (payload );
109- session .sendMessage (msg );
110- }
111- }
112- }
113287}
0 commit comments