From 4ab07c68a37b39f2e3a5275be52033106fd701a2 Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 09:33:08 +0900 Subject: [PATCH 1/9] =?UTF-8?q?Fix:=20=EC=8A=A4=ED=84=B0=EB=94=94=EB=A3=B8?= =?UTF-8?q?=20=ED=95=98=ED=8A=B8=EB=B9=84=ED=8A=B8=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=8B=9C=EA=B0=84=EC=9D=84=20=EA=B3=A0?= =?UTF-8?q?=EB=A0=A4=ED=95=B4=EC=84=9C=20=EC=84=B8=EC=85=98=20TTL=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/websocket/service/WebSocketSessionManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java b/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java index 27c1d44d..3e770c04 100644 --- a/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java +++ b/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java @@ -28,8 +28,8 @@ public class WebSocketSessionManager { private static final String SESSION_USER_KEY = "ws:session:{}"; private static final String ROOM_USERS_KEY = "ws:room:{}:users"; - // TTL 설정 (10분) - private static final int SESSION_TTL_MINUTES = 10; + // TTL 설정 + private static final int SESSION_TTL_MINUTES = 6; // 사용자 세션 추가 (연결 시 호출) public void addSession(Long userId, String sessionId) { From 4768b6d93257e6fe0dc5b4585089817a08008dc8 Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 10:10:44 +0900 Subject: [PATCH 2/9] =?UTF-8?q?Feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=86=B5=EC=8B=A0=EC=9D=84=20=EC=9C=84=ED=95=9C=20WebRTC=20?= =?UTF-8?q?=EC=8B=9C=EA=B7=B8=EB=84=90=EB=A7=81=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websocket/dto/WebRTCAnswerRequest.java | 11 ++++ .../dto/WebRTCIceCandidateRequest.java | 12 ++++ .../global/websocket/dto/WebRTCMediaType.java | 7 ++ .../websocket/dto/WebRTCOfferRequest.java | 11 ++++ .../websocket/dto/WebRTCSignalResponse.java | 64 +++++++++++++++++++ .../websocket/dto/WebRTCSignalType.java | 7 ++ 6 files changed, 112 insertions(+) create mode 100644 src/main/java/com/back/global/websocket/dto/WebRTCAnswerRequest.java create mode 100644 src/main/java/com/back/global/websocket/dto/WebRTCIceCandidateRequest.java create mode 100644 src/main/java/com/back/global/websocket/dto/WebRTCMediaType.java create mode 100644 src/main/java/com/back/global/websocket/dto/WebRTCOfferRequest.java create mode 100644 src/main/java/com/back/global/websocket/dto/WebRTCSignalResponse.java create mode 100644 src/main/java/com/back/global/websocket/dto/WebRTCSignalType.java diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCAnswerRequest.java b/src/main/java/com/back/global/websocket/dto/WebRTCAnswerRequest.java new file mode 100644 index 00000000..a232f120 --- /dev/null +++ b/src/main/java/com/back/global/websocket/dto/WebRTCAnswerRequest.java @@ -0,0 +1,11 @@ +package com.back.global.websocket.dto; + +import jakarta.validation.constraints.NotNull; + +public record WebRTCAnswerRequest( + @NotNull Long roomId, + @NotNull Long targetUserId, + @NotNull String sdp, + @NotNull WebRTCMediaType mediaType +) { +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCIceCandidateRequest.java b/src/main/java/com/back/global/websocket/dto/WebRTCIceCandidateRequest.java new file mode 100644 index 00000000..f83792e5 --- /dev/null +++ b/src/main/java/com/back/global/websocket/dto/WebRTCIceCandidateRequest.java @@ -0,0 +1,12 @@ +package com.back.global.websocket.dto; + +import jakarta.validation.constraints.NotNull; + +public record WebRTCIceCandidateRequest( + @NotNull Long roomId, + @NotNull Long targetUserId, + @NotNull String candidate, + @NotNull String sdpMid, + @NotNull Integer sdpMLineIndex +) { +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCMediaType.java b/src/main/java/com/back/global/websocket/dto/WebRTCMediaType.java new file mode 100644 index 00000000..483d4f86 --- /dev/null +++ b/src/main/java/com/back/global/websocket/dto/WebRTCMediaType.java @@ -0,0 +1,7 @@ +package com.back.global.websocket.dto; + +public enum WebRTCMediaType { + AUDIO, + VIDEO, + SCREEN +} diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCOfferRequest.java b/src/main/java/com/back/global/websocket/dto/WebRTCOfferRequest.java new file mode 100644 index 00000000..f7b1561d --- /dev/null +++ b/src/main/java/com/back/global/websocket/dto/WebRTCOfferRequest.java @@ -0,0 +1,11 @@ +package com.back.global.websocket.dto; + +import jakarta.validation.constraints.NotNull; + +public record WebRTCOfferRequest( + @NotNull Long roomId, + @NotNull Long targetUserId, + @NotNull String sdp, + @NotNull WebRTCMediaType mediaType +) { +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCSignalResponse.java b/src/main/java/com/back/global/websocket/dto/WebRTCSignalResponse.java new file mode 100644 index 00000000..e2f3b4a3 --- /dev/null +++ b/src/main/java/com/back/global/websocket/dto/WebRTCSignalResponse.java @@ -0,0 +1,64 @@ +package com.back.global.websocket.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; + +public record WebRTCSignalResponse( + WebRTCSignalType type, + Long fromUserId, + Long targetUserId, + Long roomId, + String sdp, + WebRTCMediaType mediaType, + String candidate, + String sdpMid, + Integer sdpMLineIndex, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + LocalDateTime timestamp +) { + // Offer, Answer용 생성자 + public static WebRTCSignalResponse offerOrAnswer( + WebRTCSignalType type, + Long fromUserId, + Long targetUserId, + Long roomId, + String sdp, + WebRTCMediaType mediaType + ) { + return new WebRTCSignalResponse( + type, + fromUserId, + targetUserId, + roomId, + sdp, + mediaType, + null, + null, + null, + LocalDateTime.now() + ); + } + + // ICE Candidate용 생성자 + public static WebRTCSignalResponse iceCandidate( + Long fromUserId, + Long targetUserId, + Long roomId, + String candidate, + String sdpMid, + Integer sdpMLineIndex + ) { + return new WebRTCSignalResponse( + WebRTCSignalType.ICE_CANDIDATE, + fromUserId, + targetUserId, + roomId, + null, + null, + candidate, + sdpMid, + sdpMLineIndex, + LocalDateTime.now() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCSignalType.java b/src/main/java/com/back/global/websocket/dto/WebRTCSignalType.java new file mode 100644 index 00000000..6f61795c --- /dev/null +++ b/src/main/java/com/back/global/websocket/dto/WebRTCSignalType.java @@ -0,0 +1,7 @@ +package com.back.global.websocket.dto; + +public enum WebRTCSignalType { + OFFER, + ANSWER, + ICE_CANDIDATE +} From 174e18f82ce9e9d1306e7c76068fc7caa5df0260 Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 10:34:12 +0900 Subject: [PATCH 3/9] =?UTF-8?q?Feat:=20=EA=B8=B0=EB=B3=B8=20WebRTCSignalin?= =?UTF-8?q?gController=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WebRTCSignalingController.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 src/main/java/com/back/global/websocket/controller/WebRTCSignalingController.java diff --git a/src/main/java/com/back/global/websocket/controller/WebRTCSignalingController.java b/src/main/java/com/back/global/websocket/controller/WebRTCSignalingController.java new file mode 100644 index 00000000..364ddb9a --- /dev/null +++ b/src/main/java/com/back/global/websocket/controller/WebRTCSignalingController.java @@ -0,0 +1,170 @@ +package com.back.global.websocket.controller; + +import com.back.global.security.user.CustomUserDetails; +import com.back.global.websocket.dto.*; +import com.back.global.websocket.util.WebSocketErrorHelper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; + +import java.security.Principal; + +@Controller +@RequiredArgsConstructor +@Slf4j +public class WebRTCSignalingController { + + private final SimpMessagingTemplate messagingTemplate; + private final WebSocketErrorHelper errorHelper; + + /** + * WebRTC Offer 메시지 처리 + * P2P Mesh 방식: 방 전체에 브로드캐스트하여 모든 참가자가 수신 + * 클라이언트가 /app/webrtc/offer로 전송 + */ + @MessageMapping("/webrtc/offer") + public void handleOffer(@Validated @Payload WebRTCOfferRequest request, + SimpMessageHeaderAccessor headerAccessor, + Principal principal) { + try { + // WebSocket에서 인증된 사용자 정보 추출 + CustomUserDetails userDetails = extractUserDetails(principal); + if (userDetails == null) { + errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); + return; + } + + Long fromUserId = userDetails.getUserId(); + + log.info("WebRTC Offer received - Room: {}, From: {}, To: {}, MediaType: {}", + request.roomId(), fromUserId, request.targetUserId(), request.mediaType()); + + // Offer 메시지 생성 + WebRTCSignalResponse response = WebRTCSignalResponse.offerOrAnswer( + WebRTCSignalType.OFFER, + fromUserId, + request.targetUserId(), + request.roomId(), + request.sdp(), + request.mediaType() + ); + + // 방 전체에 브로드캐스트 (P2P Mesh 연결) + messagingTemplate.convertAndSend( + "/topic/room/" + request.roomId() + "/webrtc", + response + ); + + } catch (Exception e) { + log.error("WebRTC Offer 처리 중 오류 발생 - roomId: {}", request.roomId(), e); + errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "Offer 전송 중 오류가 발생했습니다"); + } + } + + /** + * WebRTC Answer 메시지 처리 + * P2P Mesh 방식: 방 전체에 브로드캐스트하여 모든 참가자가 수신 + * 클라이언트가 /app/webrtc/answer로 전송 + */ + @MessageMapping("/webrtc/answer") + public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, + SimpMessageHeaderAccessor headerAccessor, + Principal principal) { + try { + // WebSocket에서 인증된 사용자 정보 추출 + CustomUserDetails userDetails = extractUserDetails(principal); + if (userDetails == null) { + errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); + return; + } + + Long fromUserId = userDetails.getUserId(); + + log.info("WebRTC Answer received - Room: {}, From: {}, To: {}, MediaType: {}", + request.roomId(), fromUserId, request.targetUserId(), request.mediaType()); + + // Answer 메시지 생성 + WebRTCSignalResponse response = WebRTCSignalResponse.offerOrAnswer( + WebRTCSignalType.ANSWER, + fromUserId, + request.targetUserId(), + request.roomId(), + request.sdp(), + request.mediaType() + ); + + // 방 전체에 브로드캐스트 + messagingTemplate.convertAndSend( + "/topic/room/" + request.roomId() + "/webrtc", + response + ); + + } catch (Exception e) { + log.error("WebRTC Answer 처리 중 오류 발생 - roomId: {}", request.roomId(), e); + errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "Answer 전송 중 오류가 발생했습니다"); + } + } + + /** + * ICE Candidate 메시지 처리 + * P2P Mesh 방식: 방 전체에 브로드캐스트하여 모든 참가자가 수신 + * 클라이언트가 /app/webrtc/ice-candidate로 전송 + */ + @MessageMapping("/webrtc/ice-candidate") + public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest request, + SimpMessageHeaderAccessor headerAccessor, + Principal principal) { + try { + // WebSocket에서 인증된 사용자 정보 추출 + CustomUserDetails userDetails = extractUserDetails(principal); + if (userDetails == null) { + errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); + return; + } + + Long fromUserId = userDetails.getUserId(); + + log.info("ICE Candidate received - Room: {}, From: {}, To: {}", + request.roomId(), fromUserId, request.targetUserId()); + + // ICE Candidate 메시지 생성 + WebRTCSignalResponse response = WebRTCSignalResponse.iceCandidate( + fromUserId, + request.targetUserId(), + request.roomId(), + request.candidate(), + request.sdpMid(), + request.sdpMLineIndex() + ); + + // 방 전체에 브로드캐스트 + messagingTemplate.convertAndSend( + "/topic/room/" + request.roomId() + "/webrtc", + response + ); + + } catch (Exception e) { + log.error("ICE Candidate 처리 중 오류 발생 - roomId: {}", request.roomId(), e); + errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "ICE Candidate 전송 중 오류가 발생했습니다"); + } + } + + /** + * WebSocket Principal에서 CustomUserDetails 추출 + */ + private CustomUserDetails extractUserDetails(Principal principal) { + if (principal instanceof Authentication auth) { + Object principalObj = auth.getPrincipal(); + if (principalObj instanceof CustomUserDetails userDetails) { + return userDetails; + } + } + return null; + } +} \ No newline at end of file From 4b9775d2ccfe2bb3e9660192ad3c2f7890887e9a Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 10:37:34 +0900 Subject: [PATCH 4/9] =?UTF-8?q?Refactor:=20webrtc=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => webrtc}/controller/WebRTCSignalingController.java | 4 ++-- .../websocket/{ => webrtc}/dto/WebRTCAnswerRequest.java | 2 +- .../websocket/{ => webrtc}/dto/WebRTCIceCandidateRequest.java | 2 +- .../global/websocket/{ => webrtc}/dto/WebRTCMediaType.java | 2 +- .../global/websocket/{ => webrtc}/dto/WebRTCOfferRequest.java | 2 +- .../websocket/{ => webrtc}/dto/WebRTCSignalResponse.java | 2 +- .../global/websocket/{ => webrtc}/dto/WebRTCSignalType.java | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename src/main/java/com/back/global/websocket/{ => webrtc}/controller/WebRTCSignalingController.java (98%) rename src/main/java/com/back/global/websocket/{ => webrtc}/dto/WebRTCAnswerRequest.java (83%) rename src/main/java/com/back/global/websocket/{ => webrtc}/dto/WebRTCIceCandidateRequest.java (85%) rename src/main/java/com/back/global/websocket/{ => webrtc}/dto/WebRTCMediaType.java (58%) rename src/main/java/com/back/global/websocket/{ => webrtc}/dto/WebRTCOfferRequest.java (83%) rename src/main/java/com/back/global/websocket/{ => webrtc}/dto/WebRTCSignalResponse.java (97%) rename src/main/java/com/back/global/websocket/{ => webrtc}/dto/WebRTCSignalType.java (61%) diff --git a/src/main/java/com/back/global/websocket/controller/WebRTCSignalingController.java b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java similarity index 98% rename from src/main/java/com/back/global/websocket/controller/WebRTCSignalingController.java rename to src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java index 364ddb9a..bdf8131f 100644 --- a/src/main/java/com/back/global/websocket/controller/WebRTCSignalingController.java +++ b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java @@ -1,8 +1,8 @@ -package com.back.global.websocket.controller; +package com.back.global.websocket.webrtc.controller; import com.back.global.security.user.CustomUserDetails; -import com.back.global.websocket.dto.*; import com.back.global.websocket.util.WebSocketErrorHelper; +import com.back.global.websocket.webrtc.dto.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCAnswerRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCAnswerRequest.java similarity index 83% rename from src/main/java/com/back/global/websocket/dto/WebRTCAnswerRequest.java rename to src/main/java/com/back/global/websocket/webrtc/dto/WebRTCAnswerRequest.java index a232f120..63229d7e 100644 --- a/src/main/java/com/back/global/websocket/dto/WebRTCAnswerRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCAnswerRequest.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.dto; +package com.back.global.websocket.webrtc.dto; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCIceCandidateRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCIceCandidateRequest.java similarity index 85% rename from src/main/java/com/back/global/websocket/dto/WebRTCIceCandidateRequest.java rename to src/main/java/com/back/global/websocket/webrtc/dto/WebRTCIceCandidateRequest.java index f83792e5..cd9a26fe 100644 --- a/src/main/java/com/back/global/websocket/dto/WebRTCIceCandidateRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCIceCandidateRequest.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.dto; +package com.back.global.websocket.webrtc.dto; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCMediaType.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaType.java similarity index 58% rename from src/main/java/com/back/global/websocket/dto/WebRTCMediaType.java rename to src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaType.java index 483d4f86..c5828c7e 100644 --- a/src/main/java/com/back/global/websocket/dto/WebRTCMediaType.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaType.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.dto; +package com.back.global.websocket.webrtc.dto; public enum WebRTCMediaType { AUDIO, diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCOfferRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCOfferRequest.java similarity index 83% rename from src/main/java/com/back/global/websocket/dto/WebRTCOfferRequest.java rename to src/main/java/com/back/global/websocket/webrtc/dto/WebRTCOfferRequest.java index f7b1561d..8d13faeb 100644 --- a/src/main/java/com/back/global/websocket/dto/WebRTCOfferRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCOfferRequest.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.dto; +package com.back.global.websocket.webrtc.dto; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCSignalResponse.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalResponse.java similarity index 97% rename from src/main/java/com/back/global/websocket/dto/WebRTCSignalResponse.java rename to src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalResponse.java index e2f3b4a3..ec11f40b 100644 --- a/src/main/java/com/back/global/websocket/dto/WebRTCSignalResponse.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalResponse.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.dto; +package com.back.global.websocket.webrtc.dto; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; diff --git a/src/main/java/com/back/global/websocket/dto/WebRTCSignalType.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalType.java similarity index 61% rename from src/main/java/com/back/global/websocket/dto/WebRTCSignalType.java rename to src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalType.java index 6f61795c..f138e4da 100644 --- a/src/main/java/com/back/global/websocket/dto/WebRTCSignalType.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalType.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.dto; +package com.back.global.websocket.webrtc.dto; public enum WebRTCSignalType { OFFER, From 2a4da2c184cdb7d77b59c004d3d294f1f20cabdd Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 11:13:16 +0900 Subject: [PATCH 5/9] =?UTF-8?q?Feat:=20WebRTC=20=EB=AF=B8=EB=94=94?= =?UTF-8?q?=EC=96=B4=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WebRTCSignalingController.java | 63 +++++++++++++------ .../webrtc/dto/WebRTCMediaStateResponse.java | 28 +++++++++ .../webrtc/dto/WebRTCMediaToggleRequest.java | 9 +++ 3 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaStateResponse.java create mode 100644 src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaToggleRequest.java diff --git a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java index bdf8131f..622a9bc9 100644 --- a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java +++ b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java @@ -5,6 +5,7 @@ import com.back.global.websocket.webrtc.dto.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -23,11 +24,7 @@ public class WebRTCSignalingController { private final SimpMessagingTemplate messagingTemplate; private final WebSocketErrorHelper errorHelper; - /** - * WebRTC Offer 메시지 처리 - * P2P Mesh 방식: 방 전체에 브로드캐스트하여 모든 참가자가 수신 - * 클라이언트가 /app/webrtc/offer로 전송 - */ + // WebRTC Offer 메시지 처리 @MessageMapping("/webrtc/offer") public void handleOffer(@Validated @Payload WebRTCOfferRequest request, SimpMessageHeaderAccessor headerAccessor, @@ -67,11 +64,7 @@ public void handleOffer(@Validated @Payload WebRTCOfferRequest request, } } - /** - * WebRTC Answer 메시지 처리 - * P2P Mesh 방식: 방 전체에 브로드캐스트하여 모든 참가자가 수신 - * 클라이언트가 /app/webrtc/answer로 전송 - */ + // WebRTC Answer 메시지 처리 @MessageMapping("/webrtc/answer") public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, SimpMessageHeaderAccessor headerAccessor, @@ -111,11 +104,7 @@ public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, } } - /** - * ICE Candidate 메시지 처리 - * P2P Mesh 방식: 방 전체에 브로드캐스트하여 모든 참가자가 수신 - * 클라이언트가 /app/webrtc/ice-candidate로 전송 - */ + // ICE Candidate 메시지 처리 @MessageMapping("/webrtc/ice-candidate") public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest request, SimpMessageHeaderAccessor headerAccessor, @@ -155,9 +144,47 @@ public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest req } } - /** - * WebSocket Principal에서 CustomUserDetails 추출 - */ + // 미디어 상태 토글 처리 (오디오/비디오/화면공유 on/off) + @MessageMapping("/webrtc/media/toggle") + public void handleMediaToggle(@Validated @Payload WebRTCMediaToggleRequest request, + @DestinationVariable Long roomId, + SimpMessageHeaderAccessor headerAccessor, + Principal principal) { + try { + // 인증된 사용자 정보 추출 + CustomUserDetails userDetails = extractUserDetails(principal); + if (userDetails == null) { + errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); + return; + } + + Long userId = userDetails.getUserId(); + String nickname = userDetails.getUsername(); + + log.info("미디어 상태 변경 - Room: {}, User: {}, MediaType: {}, Enabled: {}", + roomId, userId, request.mediaType(), request.enabled()); + + // 미디어 상태 응답 생성 + WebRTCMediaStateResponse response = WebRTCMediaStateResponse.of( + userId, + nickname, + request.mediaType(), + request.enabled() + ); + + // 방 전체에 브로드캐스트 + messagingTemplate.convertAndSend( + "/topic/room/" + roomId + "/media-status", + response + ); + + } catch (Exception e) { + log.error("미디어 상태 변경 중 오류 발생 - roomId: {}", roomId, e); + errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "미디어 상태 변경 중 오류가 발생했습니다"); + } + } + + // Principal에서 CustomUserDetails 추출 헬퍼 메서드 private CustomUserDetails extractUserDetails(Principal principal) { if (principal instanceof Authentication auth) { Object principalObj = auth.getPrincipal(); diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaStateResponse.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaStateResponse.java new file mode 100644 index 00000000..f5959a33 --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaStateResponse.java @@ -0,0 +1,28 @@ +package com.back.global.websocket.webrtc.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; + +public record WebRTCMediaStateResponse( + Long userId, + String nickname, + WebRTCMediaType mediaType, + Boolean enabled, + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + LocalDateTime timestamp +) { + public static WebRTCMediaStateResponse of( + Long userId, + String nickname, + WebRTCMediaType mediaType, + Boolean enabled + ) { + return new WebRTCMediaStateResponse( + userId, + nickname, + mediaType, + enabled, + LocalDateTime.now() + ); + } +} diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaToggleRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaToggleRequest.java new file mode 100644 index 00000000..3d6a2cb3 --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaToggleRequest.java @@ -0,0 +1,9 @@ +package com.back.global.websocket.webrtc.dto; + +import jakarta.validation.constraints.NotNull; + +public record WebRTCMediaToggleRequest( + @NotNull WebRTCMediaType mediaType, + @NotNull Boolean enabled +) { +} \ No newline at end of file From 7ec2db6451dba73cd13fdb6b743a5690499ce9ad Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 11:25:17 +0900 Subject: [PATCH 6/9] =?UTF-8?q?Feat:=20dto=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20/=20ICE=20=EC=84=9C=EB=B2=84=20REST=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WebRTCApiController.java | 61 +++++++++++++++++++ .../controller/WebRTCSignalingController.java | 4 +- .../websocket/webrtc/dto/ice/IceServer.java | 20 ++++++ .../webrtc/dto/ice/IceServerConfig.java | 17 ++++++ .../{ => media}/WebRTCMediaStateResponse.java | 2 +- .../{ => media}/WebRTCMediaToggleRequest.java | 2 +- .../dto/{ => media}/WebRTCMediaType.java | 2 +- .../dto/{ => signal}/WebRTCAnswerRequest.java | 3 +- .../WebRTCIceCandidateRequest.java | 2 +- .../dto/{ => signal}/WebRTCOfferRequest.java | 3 +- .../{ => signal}/WebRTCSignalResponse.java | 3 +- .../dto/{ => signal}/WebRTCSignalType.java | 2 +- 12 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/back/global/websocket/webrtc/controller/WebRTCApiController.java create mode 100644 src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServer.java create mode 100644 src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServerConfig.java rename src/main/java/com/back/global/websocket/webrtc/dto/{ => media}/WebRTCMediaStateResponse.java (93%) rename src/main/java/com/back/global/websocket/webrtc/dto/{ => media}/WebRTCMediaToggleRequest.java (76%) rename src/main/java/com/back/global/websocket/webrtc/dto/{ => media}/WebRTCMediaType.java (55%) rename src/main/java/com/back/global/websocket/webrtc/dto/{ => signal}/WebRTCAnswerRequest.java (65%) rename src/main/java/com/back/global/websocket/webrtc/dto/{ => signal}/WebRTCIceCandidateRequest.java (83%) rename src/main/java/com/back/global/websocket/webrtc/dto/{ => signal}/WebRTCOfferRequest.java (65%) rename src/main/java/com/back/global/websocket/webrtc/dto/{ => signal}/WebRTCSignalResponse.java (93%) rename src/main/java/com/back/global/websocket/webrtc/dto/{ => signal}/WebRTCSignalType.java (58%) diff --git a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCApiController.java b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCApiController.java new file mode 100644 index 00000000..ed085425 --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCApiController.java @@ -0,0 +1,61 @@ +package com.back.global.websocket.webrtc.controller; + +import com.back.global.common.dto.RsData; +import com.back.global.websocket.webrtc.dto.ice.IceServerConfig; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/api/webrtc") +@RequiredArgsConstructor +@Tag(name = "WebRTC API", description = "WebRTC 시그널링 및 ICE 서버 관련 REST API") +public class WebRTCApiController { + + // ICE 서버 설정 조회 + @GetMapping("/ice-servers") + @Operation( + summary = "ICE 서버 설정 조회", + description = "WebRTC 연결에 필요한 STUN/TURN 서버 정보를 조회합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + public ResponseEntity> getIceServers( + @Parameter(description = "사용자 ID (선택)") @RequestParam(required = false) Long userId, + @Parameter(description = "방 ID (선택)") @RequestParam(required = false) Long roomId) { + + log.info("ICE 서버 설정 요청 - userId: {}, roomId: {}", userId, roomId); + + // 기본 Google STUN 서버 사용 + IceServerConfig config = IceServerConfig.withDefaultStunServers(); + + log.info("ICE 서버 설정 제공 완료 - STUN 서버 {}개", config.iceServers().size()); + + return ResponseEntity + .status(HttpStatus.OK) + .body(RsData.success("ICE 서버 설정 조회 성공", config)); + } + + // WebRTC 서비스 상태 확인 + @GetMapping("/health") + @Operation( + summary = "WebRTC 서비스 상태 확인", + description = "WebRTC 시그널링 서버의 상태를 확인합니다." + ) + @ApiResponse(responseCode = "200", description = "정상 작동 중") + public ResponseEntity> healthCheck() { + return ResponseEntity + .status(HttpStatus.OK) + .body(RsData.success("WebRTC 서비스 정상 작동 중")); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java index 622a9bc9..c714c1dc 100644 --- a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java +++ b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java @@ -2,7 +2,9 @@ import com.back.global.security.user.CustomUserDetails; import com.back.global.websocket.util.WebSocketErrorHelper; -import com.back.global.websocket.webrtc.dto.*; +import com.back.global.websocket.webrtc.dto.media.WebRTCMediaStateResponse; +import com.back.global.websocket.webrtc.dto.media.WebRTCMediaToggleRequest; +import com.back.global.websocket.webrtc.dto.signal.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.DestinationVariable; diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServer.java b/src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServer.java new file mode 100644 index 00000000..633aa927 --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServer.java @@ -0,0 +1,20 @@ +package com.back.global.websocket.webrtc.dto.ice; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record IceServer( + String urls, + String username, + String credential +) { + // STUN 서버 (인증 불필요) + public static IceServer stun(String url) { + return new IceServer(url, null, null); + } + + // TURN 서버 (인증 필요) + public static IceServer turn(String url, String username, String credential) { + return new IceServer(url, username, credential); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServerConfig.java b/src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServerConfig.java new file mode 100644 index 00000000..460c30be --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/dto/ice/IceServerConfig.java @@ -0,0 +1,17 @@ +package com.back.global.websocket.webrtc.dto.ice; + +import java.util.List; + +public record IceServerConfig( + List iceServers +) { + public static IceServerConfig withDefaultStunServers() { + return new IceServerConfig( + List.of( + IceServer.stun("stun:stun.l.google.com:19302"), + IceServer.stun("stun:stun1.l.google.com:19302"), + IceServer.stun("stun:stun2.l.google.com:19302") + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaStateResponse.java b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaStateResponse.java similarity index 93% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaStateResponse.java rename to src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaStateResponse.java index f5959a33..b945059c 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaStateResponse.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaStateResponse.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.media; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaToggleRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaToggleRequest.java similarity index 76% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaToggleRequest.java rename to src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaToggleRequest.java index 3d6a2cb3..f52569af 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaToggleRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaToggleRequest.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.media; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaType.java b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaType.java similarity index 55% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaType.java rename to src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaType.java index c5828c7e..340f77f9 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCMediaType.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaType.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.media; public enum WebRTCMediaType { AUDIO, diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCAnswerRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCAnswerRequest.java similarity index 65% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCAnswerRequest.java rename to src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCAnswerRequest.java index 63229d7e..ca3d7cfe 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCAnswerRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCAnswerRequest.java @@ -1,5 +1,6 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.signal; +import com.back.global.websocket.webrtc.dto.media.WebRTCMediaType; import jakarta.validation.constraints.NotNull; public record WebRTCAnswerRequest( diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCIceCandidateRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCIceCandidateRequest.java similarity index 83% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCIceCandidateRequest.java rename to src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCIceCandidateRequest.java index cd9a26fe..2649ea46 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCIceCandidateRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCIceCandidateRequest.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.signal; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCOfferRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCOfferRequest.java similarity index 65% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCOfferRequest.java rename to src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCOfferRequest.java index 8d13faeb..93fe4108 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCOfferRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCOfferRequest.java @@ -1,5 +1,6 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.signal; +import com.back.global.websocket.webrtc.dto.media.WebRTCMediaType; import jakarta.validation.constraints.NotNull; public record WebRTCOfferRequest( diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalResponse.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalResponse.java similarity index 93% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalResponse.java rename to src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalResponse.java index ec11f40b..59f8d2b4 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalResponse.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalResponse.java @@ -1,5 +1,6 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.signal; +import com.back.global.websocket.webrtc.dto.media.WebRTCMediaType; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalType.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalType.java similarity index 58% rename from src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalType.java rename to src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalType.java index f138e4da..c95e33b3 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCSignalType.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalType.java @@ -1,4 +1,4 @@ -package com.back.global.websocket.webrtc.dto; +package com.back.global.websocket.webrtc.dto.signal; public enum WebRTCSignalType { OFFER, From 081b103d33eff14bf30a5964a022c042e71c4088 Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 11:37:21 +0900 Subject: [PATCH 7/9] =?UTF-8?q?Feat:=20WebRTC=20=EC=8B=9C=EA=B7=B8?= =?UTF-8?q?=EB=84=90=EB=A7=81=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WebRTCSignalingController.java | 32 +++++++--- .../dto/media/WebRTCMediaToggleRequest.java | 1 + .../webrtc/service/WebRTCSignalValidator.java | 62 +++++++++++++++++++ 3 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/back/global/websocket/webrtc/service/WebRTCSignalValidator.java diff --git a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java index c714c1dc..55f82a22 100644 --- a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java +++ b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java @@ -1,13 +1,13 @@ package com.back.global.websocket.webrtc.controller; import com.back.global.security.user.CustomUserDetails; -import com.back.global.websocket.util.WebSocketErrorHelper; -import com.back.global.websocket.webrtc.dto.media.WebRTCMediaStateResponse; import com.back.global.websocket.webrtc.dto.media.WebRTCMediaToggleRequest; +import com.back.global.websocket.webrtc.dto.media.WebRTCMediaStateResponse; import com.back.global.websocket.webrtc.dto.signal.*; +import com.back.global.websocket.webrtc.service.WebRTCSignalValidator; +import com.back.global.websocket.util.WebSocketErrorHelper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -25,6 +25,7 @@ public class WebRTCSignalingController { private final SimpMessagingTemplate messagingTemplate; private final WebSocketErrorHelper errorHelper; + private final WebRTCSignalValidator validator; // WebRTC Offer 메시지 처리 @MessageMapping("/webrtc/offer") @@ -41,6 +42,9 @@ public void handleOffer(@Validated @Payload WebRTCOfferRequest request, Long fromUserId = userDetails.getUserId(); + // 시그널 검증 + validator.validateSignal(request.roomId(), fromUserId, request.targetUserId()); + log.info("WebRTC Offer received - Room: {}, From: {}, To: {}, MediaType: {}", request.roomId(), fromUserId, request.targetUserId(), request.mediaType()); @@ -81,6 +85,9 @@ public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, Long fromUserId = userDetails.getUserId(); + // 시그널 검증 + validator.validateSignal(request.roomId(), fromUserId, request.targetUserId()); + log.info("WebRTC Answer received - Room: {}, From: {}, To: {}, MediaType: {}", request.roomId(), fromUserId, request.targetUserId(), request.mediaType()); @@ -94,7 +101,7 @@ public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, request.mediaType() ); - // 방 전체에 브로드캐스트 + // 방 전체에 브로드캐스트 (P2P Mesh 연결) messagingTemplate.convertAndSend( "/topic/room/" + request.roomId() + "/webrtc", response @@ -121,6 +128,9 @@ public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest req Long fromUserId = userDetails.getUserId(); + // 시그널 검증 + validator.validateSignal(request.roomId(), fromUserId, request.targetUserId()); + log.info("ICE Candidate received - Room: {}, From: {}, To: {}", request.roomId(), fromUserId, request.targetUserId()); @@ -134,7 +144,7 @@ public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest req request.sdpMLineIndex() ); - // 방 전체에 브로드캐스트 + // 방 전체에 브로드캐스트 (P2P Mesh 연결) messagingTemplate.convertAndSend( "/topic/room/" + request.roomId() + "/webrtc", response @@ -146,10 +156,9 @@ public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest req } } - // 미디어 상태 토글 처리 (오디오/비디오/화면공유 on/off) + // 미디어 상태 토글 처리 @MessageMapping("/webrtc/media/toggle") public void handleMediaToggle(@Validated @Payload WebRTCMediaToggleRequest request, - @DestinationVariable Long roomId, SimpMessageHeaderAccessor headerAccessor, Principal principal) { try { @@ -163,8 +172,11 @@ public void handleMediaToggle(@Validated @Payload WebRTCMediaToggleRequest reque Long userId = userDetails.getUserId(); String nickname = userDetails.getUsername(); + // 미디어 상태 변경 검증 + validator.validateMediaStateChange(request.roomId(), userId); + log.info("미디어 상태 변경 - Room: {}, User: {}, MediaType: {}, Enabled: {}", - roomId, userId, request.mediaType(), request.enabled()); + request.roomId(), userId, request.mediaType(), request.enabled()); // 미디어 상태 응답 생성 WebRTCMediaStateResponse response = WebRTCMediaStateResponse.of( @@ -176,12 +188,12 @@ public void handleMediaToggle(@Validated @Payload WebRTCMediaToggleRequest reque // 방 전체에 브로드캐스트 messagingTemplate.convertAndSend( - "/topic/room/" + roomId + "/media-status", + "/topic/room/" + request.roomId() + "/media-status", response ); } catch (Exception e) { - log.error("미디어 상태 변경 중 오류 발생 - roomId: {}", roomId, e); + log.error("미디어 상태 변경 중 오류 발생 - roomId: {}", request.roomId(), e); errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "미디어 상태 변경 중 오류가 발생했습니다"); } } diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaToggleRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaToggleRequest.java index f52569af..d564cc60 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaToggleRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/media/WebRTCMediaToggleRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.NotNull; public record WebRTCMediaToggleRequest( + @NotNull Long roomId, @NotNull WebRTCMediaType mediaType, @NotNull Boolean enabled ) { diff --git a/src/main/java/com/back/global/websocket/webrtc/service/WebRTCSignalValidator.java b/src/main/java/com/back/global/websocket/webrtc/service/WebRTCSignalValidator.java new file mode 100644 index 00000000..6a8dfd96 --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/service/WebRTCSignalValidator.java @@ -0,0 +1,62 @@ +package com.back.global.websocket.webrtc.service; + +import com.back.domain.studyroom.entity.RoomMember; +import com.back.domain.studyroom.repository.RoomMemberRepository; +import com.back.global.exception.CustomException; +import com.back.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * WebRTC 시그널링 메시지 검증 + * - 같은 방에 있는지 확인 + * - 자기 자신에게 보내는지 확인 + * - 온라인 상태인지 확인 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class WebRTCSignalValidator { + + private final RoomMemberRepository roomMemberRepository; + + // WebRTC 시그널 검증 + public void validateSignal(Long roomId, Long fromUserId, Long targetUserId) { + // 1. 자기 자신에게 보내는지 확인 + if (fromUserId.equals(targetUserId)) { + log.warn("자기 자신에게 시그널 전송 시도 - userId: {}", fromUserId); + throw new CustomException(ErrorCode.BAD_REQUEST); + } + + // 2. 발신자가 방에 속해있는지 확인 + Optional fromMember = roomMemberRepository.findByRoomIdAndUserId(roomId, fromUserId); + if (fromMember.isEmpty() || !fromMember.get().isOnline()) { + log.warn("방에 속하지 않은 사용자의 시그널 전송 시도 - roomId: {}, userId: {}", roomId, fromUserId); + throw new CustomException(ErrorCode.NOT_ROOM_MEMBER); + } + + // 3. 수신자가 같은 방에 속해있는지 확인 + Optional targetMember = roomMemberRepository.findByRoomIdAndUserId(roomId, targetUserId); + if (targetMember.isEmpty() || !targetMember.get().isOnline()) { + log.warn("수신자가 방에 없거나 오프라인 상태 - roomId: {}, targetUserId: {}", roomId, targetUserId); + throw new CustomException(ErrorCode.NOT_ROOM_MEMBER); + } + + log.debug("WebRTC 시그널 검증 통과 - roomId: {}, from: {}, to: {}", roomId, fromUserId, targetUserId); + } + + // 미디어 상태 변경 검증 + public void validateMediaStateChange(Long roomId, Long userId) { + Optional member = roomMemberRepository.findByRoomIdAndUserId(roomId, userId); + + if (member.isEmpty() || !member.get().isOnline()) { + log.warn("방에 속하지 않은 사용자의 미디어 상태 변경 시도 - roomId: {}, userId: {}", roomId, userId); + throw new CustomException(ErrorCode.NOT_ROOM_MEMBER); + } + + log.debug("미디어 상태 변경 검증 통과 - roomId: {}, userId: {}", roomId, userId); + } +} \ No newline at end of file From 6d724a10069a56d71223cd59e926692633a42368 Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 11:43:23 +0900 Subject: [PATCH 8/9] =?UTF-8?q?Refactor:=20WebSocket=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomChatWebSocketController.java | 19 +++++----------- .../websocket/util/WebSocketAuthHelper.java | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/back/global/websocket/util/WebSocketAuthHelper.java diff --git a/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java b/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java index 4501e5e3..2c6f830b 100644 --- a/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java +++ b/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java @@ -7,6 +7,7 @@ import com.back.global.exception.CustomException; import com.back.global.security.user.CustomUserDetails; import com.back.domain.chat.room.service.RoomChatService; +import com.back.global.websocket.util.WebSocketAuthHelper; import com.back.global.websocket.util.WebSocketErrorHelper; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -29,6 +30,7 @@ public class RoomChatWebSocketController { private final RoomChatService roomChatService; private final SimpMessagingTemplate messagingTemplate; + private final WebSocketAuthHelper authHelper; private final WebSocketErrorHelper errorHelper; /** @@ -43,7 +45,8 @@ public void handleRoomChat(@DestinationVariable Long roomId, try { // WebSocket에서 인증된 사용자 정보 추출 - CustomUserDetails userDetails = extractUserDetails(principal); + CustomUserDetails userDetails = authHelper.extractUserDetails(principal); + if (userDetails == null) { errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); return; @@ -101,7 +104,8 @@ public void clearRoomChat(@DestinationVariable Long roomId, log.info("WebSocket 채팅 일괄 삭제 요청 - roomId: {}", roomId); // 사용자 인증 확인 - CustomUserDetails userDetails = extractUserDetails(principal); + CustomUserDetails userDetails = authHelper.extractUserDetails(principal); + if (userDetails == null) { errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); return; @@ -148,15 +152,4 @@ public void clearRoomChat(@DestinationVariable Long roomId, } } - // WebSocket Principal에서 CustomUserDetails 추출 - private CustomUserDetails extractUserDetails(Principal principal) { - if (principal instanceof Authentication auth) { - Object principalObj = auth.getPrincipal(); - if (principalObj instanceof CustomUserDetails userDetails) { - return userDetails; - } - } - return null; - } - } \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/util/WebSocketAuthHelper.java b/src/main/java/com/back/global/websocket/util/WebSocketAuthHelper.java new file mode 100644 index 00000000..a609dc9d --- /dev/null +++ b/src/main/java/com/back/global/websocket/util/WebSocketAuthHelper.java @@ -0,0 +1,22 @@ +package com.back.global.websocket.util; + +import com.back.global.security.user.CustomUserDetails; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.security.Principal; + +@Component +public class WebSocketAuthHelper { + + // WebSocket에서 인증된 사용자 정보 추출 + public static CustomUserDetails extractUserDetails(Principal principal) { + if (principal instanceof Authentication auth) { + Object principalObj = auth.getPrincipal(); + if (principalObj instanceof CustomUserDetails userDetails) { + return userDetails; + } + } + return null; + } +} \ No newline at end of file From 71a94bb5d706acea0643d1e7df9788b4dacfbb4f Mon Sep 17 00:00:00 2001 From: jueunk617 Date: Tue, 30 Sep 2025 12:12:54 +0900 Subject: [PATCH 9/9] =?UTF-8?q?Fix:=20WebSocketSessionManagerTest=20TTL=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/WebSocketSessionManagerTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java b/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java index 10f7aad0..297a2e2e 100644 --- a/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java +++ b/src/test/java/com/back/global/websocket/service/WebSocketSessionManagerTest.java @@ -59,7 +59,7 @@ void t1() { ); // then - verify(valueOperations, times(2)).set(anyString(), any(), eq(Duration.ofMinutes(10))); + verify(valueOperations, times(2)).set(anyString(), any(), eq(Duration.ofMinutes(6))); } @Test @@ -81,7 +81,7 @@ void t2() { // then verify(redisTemplate, atLeastOnce()).delete(anyString()); // 기존 세션 삭제 - verify(valueOperations, times(2)).set(anyString(), any(), eq(Duration.ofMinutes(10))); // 새 세션 등록 + verify(valueOperations, times(2)).set(anyString(), any(), eq(Duration.ofMinutes(6))); // 새 세션 등록 } @Test @@ -191,7 +191,7 @@ void t9() { ); // then - verify(valueOperations).set(eq("ws:user:123"), any(WebSocketSessionInfo.class), eq(Duration.ofMinutes(10))); + verify(valueOperations).set(eq("ws:user:123"), any(WebSocketSessionInfo.class), eq(Duration.ofMinutes(6))); } @Test @@ -229,8 +229,8 @@ void t11() { // then verify(setOperations).add("ws:room:456:users", TEST_USER_ID); - verify(redisTemplate).expire("ws:room:456:users", Duration.ofMinutes(10)); - verify(valueOperations).set(eq("ws:user:123"), any(WebSocketSessionInfo.class), eq(Duration.ofMinutes(10))); + verify(redisTemplate).expire("ws:room:456:users", Duration.ofMinutes(6)); + verify(valueOperations).set(eq("ws:user:123"), any(WebSocketSessionInfo.class), eq(Duration.ofMinutes(6))); } @Test @@ -280,7 +280,7 @@ void t13() { // then verify(setOperations).remove("ws:room:456:users", TEST_USER_ID); - verify(valueOperations).set(eq("ws:user:123"), any(WebSocketSessionInfo.class), eq(Duration.ofMinutes(10))); + verify(valueOperations).set(eq("ws:user:123"), any(WebSocketSessionInfo.class), eq(Duration.ofMinutes(6))); } @Test