1+ package com .back .global .websocket .webrtc .controller ;
2+
3+ import com .back .global .security .user .CustomUserDetails ;
4+ import com .back .global .websocket .webrtc .dto .media .WebRTCMediaToggleRequest ;
5+ import com .back .global .websocket .webrtc .dto .media .WebRTCMediaStateResponse ;
6+ import com .back .global .websocket .webrtc .dto .signal .*;
7+ import com .back .global .websocket .webrtc .service .WebRTCSignalValidator ;
8+ import com .back .global .websocket .util .WebSocketErrorHelper ;
9+ import lombok .RequiredArgsConstructor ;
10+ import lombok .extern .slf4j .Slf4j ;
11+ import org .springframework .messaging .handler .annotation .MessageMapping ;
12+ import org .springframework .messaging .handler .annotation .Payload ;
13+ import org .springframework .messaging .simp .SimpMessageHeaderAccessor ;
14+ import org .springframework .messaging .simp .SimpMessagingTemplate ;
15+ import org .springframework .security .core .Authentication ;
16+ import org .springframework .stereotype .Controller ;
17+ import org .springframework .validation .annotation .Validated ;
18+
19+ import java .security .Principal ;
20+
21+ @ Controller
22+ @ RequiredArgsConstructor
23+ @ Slf4j
24+ public class WebRTCSignalingController {
25+
26+ private final SimpMessagingTemplate messagingTemplate ;
27+ private final WebSocketErrorHelper errorHelper ;
28+ private final WebRTCSignalValidator validator ;
29+
30+ // WebRTC Offer 메시지 처리
31+ @ MessageMapping ("/webrtc/offer" )
32+ public void handleOffer (@ Validated @ Payload WebRTCOfferRequest request ,
33+ SimpMessageHeaderAccessor headerAccessor ,
34+ Principal principal ) {
35+ try {
36+ // WebSocket에서 인증된 사용자 정보 추출
37+ CustomUserDetails userDetails = extractUserDetails (principal );
38+ if (userDetails == null ) {
39+ errorHelper .sendUnauthorizedError (headerAccessor .getSessionId ());
40+ return ;
41+ }
42+
43+ Long fromUserId = userDetails .getUserId ();
44+
45+ // 시그널 검증
46+ validator .validateSignal (request .roomId (), fromUserId , request .targetUserId ());
47+
48+ log .info ("WebRTC Offer received - Room: {}, From: {}, To: {}, MediaType: {}" ,
49+ request .roomId (), fromUserId , request .targetUserId (), request .mediaType ());
50+
51+ // Offer 메시지 생성
52+ WebRTCSignalResponse response = WebRTCSignalResponse .offerOrAnswer (
53+ WebRTCSignalType .OFFER ,
54+ fromUserId ,
55+ request .targetUserId (),
56+ request .roomId (),
57+ request .sdp (),
58+ request .mediaType ()
59+ );
60+
61+ // 방 전체에 브로드캐스트 (P2P Mesh 연결)
62+ messagingTemplate .convertAndSend (
63+ "/topic/room/" + request .roomId () + "/webrtc" ,
64+ response
65+ );
66+
67+ } catch (Exception e ) {
68+ log .error ("WebRTC Offer 처리 중 오류 발생 - roomId: {}" , request .roomId (), e );
69+ errorHelper .sendGenericErrorToUser (headerAccessor .getSessionId (), e , "Offer 전송 중 오류가 발생했습니다" );
70+ }
71+ }
72+
73+ // WebRTC Answer 메시지 처리
74+ @ MessageMapping ("/webrtc/answer" )
75+ public void handleAnswer (@ Validated @ Payload WebRTCAnswerRequest request ,
76+ SimpMessageHeaderAccessor headerAccessor ,
77+ Principal principal ) {
78+ try {
79+ // WebSocket에서 인증된 사용자 정보 추출
80+ CustomUserDetails userDetails = extractUserDetails (principal );
81+ if (userDetails == null ) {
82+ errorHelper .sendUnauthorizedError (headerAccessor .getSessionId ());
83+ return ;
84+ }
85+
86+ Long fromUserId = userDetails .getUserId ();
87+
88+ // 시그널 검증
89+ validator .validateSignal (request .roomId (), fromUserId , request .targetUserId ());
90+
91+ log .info ("WebRTC Answer received - Room: {}, From: {}, To: {}, MediaType: {}" ,
92+ request .roomId (), fromUserId , request .targetUserId (), request .mediaType ());
93+
94+ // Answer 메시지 생성
95+ WebRTCSignalResponse response = WebRTCSignalResponse .offerOrAnswer (
96+ WebRTCSignalType .ANSWER ,
97+ fromUserId ,
98+ request .targetUserId (),
99+ request .roomId (),
100+ request .sdp (),
101+ request .mediaType ()
102+ );
103+
104+ // 방 전체에 브로드캐스트 (P2P Mesh 연결)
105+ messagingTemplate .convertAndSend (
106+ "/topic/room/" + request .roomId () + "/webrtc" ,
107+ response
108+ );
109+
110+ } catch (Exception e ) {
111+ log .error ("WebRTC Answer 처리 중 오류 발생 - roomId: {}" , request .roomId (), e );
112+ errorHelper .sendGenericErrorToUser (headerAccessor .getSessionId (), e , "Answer 전송 중 오류가 발생했습니다" );
113+ }
114+ }
115+
116+ // ICE Candidate 메시지 처리
117+ @ MessageMapping ("/webrtc/ice-candidate" )
118+ public void handleIceCandidate (@ Validated @ Payload WebRTCIceCandidateRequest request ,
119+ SimpMessageHeaderAccessor headerAccessor ,
120+ Principal principal ) {
121+ try {
122+ // WebSocket에서 인증된 사용자 정보 추출
123+ CustomUserDetails userDetails = extractUserDetails (principal );
124+ if (userDetails == null ) {
125+ errorHelper .sendUnauthorizedError (headerAccessor .getSessionId ());
126+ return ;
127+ }
128+
129+ Long fromUserId = userDetails .getUserId ();
130+
131+ // 시그널 검증
132+ validator .validateSignal (request .roomId (), fromUserId , request .targetUserId ());
133+
134+ log .info ("ICE Candidate received - Room: {}, From: {}, To: {}" ,
135+ request .roomId (), fromUserId , request .targetUserId ());
136+
137+ // ICE Candidate 메시지 생성
138+ WebRTCSignalResponse response = WebRTCSignalResponse .iceCandidate (
139+ fromUserId ,
140+ request .targetUserId (),
141+ request .roomId (),
142+ request .candidate (),
143+ request .sdpMid (),
144+ request .sdpMLineIndex ()
145+ );
146+
147+ // 방 전체에 브로드캐스트 (P2P Mesh 연결)
148+ messagingTemplate .convertAndSend (
149+ "/topic/room/" + request .roomId () + "/webrtc" ,
150+ response
151+ );
152+
153+ } catch (Exception e ) {
154+ log .error ("ICE Candidate 처리 중 오류 발생 - roomId: {}" , request .roomId (), e );
155+ errorHelper .sendGenericErrorToUser (headerAccessor .getSessionId (), e , "ICE Candidate 전송 중 오류가 발생했습니다" );
156+ }
157+ }
158+
159+ // 미디어 상태 토글 처리
160+ @ MessageMapping ("/webrtc/media/toggle" )
161+ public void handleMediaToggle (@ Validated @ Payload WebRTCMediaToggleRequest request ,
162+ SimpMessageHeaderAccessor headerAccessor ,
163+ Principal principal ) {
164+ try {
165+ // 인증된 사용자 정보 추출
166+ CustomUserDetails userDetails = extractUserDetails (principal );
167+ if (userDetails == null ) {
168+ errorHelper .sendUnauthorizedError (headerAccessor .getSessionId ());
169+ return ;
170+ }
171+
172+ Long userId = userDetails .getUserId ();
173+ String nickname = userDetails .getUsername ();
174+
175+ // 미디어 상태 변경 검증
176+ validator .validateMediaStateChange (request .roomId (), userId );
177+
178+ log .info ("미디어 상태 변경 - Room: {}, User: {}, MediaType: {}, Enabled: {}" ,
179+ request .roomId (), userId , request .mediaType (), request .enabled ());
180+
181+ // 미디어 상태 응답 생성
182+ WebRTCMediaStateResponse response = WebRTCMediaStateResponse .of (
183+ userId ,
184+ nickname ,
185+ request .mediaType (),
186+ request .enabled ()
187+ );
188+
189+ // 방 전체에 브로드캐스트
190+ messagingTemplate .convertAndSend (
191+ "/topic/room/" + request .roomId () + "/media-status" ,
192+ response
193+ );
194+
195+ } catch (Exception e ) {
196+ log .error ("미디어 상태 변경 중 오류 발생 - roomId: {}" , request .roomId (), e );
197+ errorHelper .sendGenericErrorToUser (headerAccessor .getSessionId (), e , "미디어 상태 변경 중 오류가 발생했습니다" );
198+ }
199+ }
200+
201+ // Principal에서 CustomUserDetails 추출 헬퍼 메서드
202+ private CustomUserDetails extractUserDetails (Principal principal ) {
203+ if (principal instanceof Authentication auth ) {
204+ Object principalObj = auth .getPrincipal ();
205+ if (principalObj instanceof CustomUserDetails userDetails ) {
206+ return userDetails ;
207+ }
208+ }
209+ return null ;
210+ }
211+ }
0 commit comments