From 7e7a07afa908de45d5eff296dfcd1693cc5b5f1a Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 13 Nov 2025 01:52:09 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=EC=9B=B9=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index a24fcd40..b3597f6c 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,9 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + //Websocket + implementation 'org.springframework.boot:spring-boot-starter-websocket' + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' From 03a0529efae65984b5634ff09c0564ba38fa3921 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 13 Nov 2025 01:59:30 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EC=9B=B9=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../debatetimer/config/WebSocketConfig.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/debatetimer/config/WebSocketConfig.java diff --git a/src/main/java/com/debatetimer/config/WebSocketConfig.java b/src/main/java/com/debatetimer/config/WebSocketConfig.java new file mode 100644 index 00000000..a829e277 --- /dev/null +++ b/src/main/java/com/debatetimer/config/WebSocketConfig.java @@ -0,0 +1,25 @@ +package com.debatetimer.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/room"); + registry.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} From 30150ecc4fe6da095e6fc18d5d9ced70c6326d99 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 13 Nov 2025 02:25:05 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20=EA=B5=AC=EB=8F=85=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=A6=AC=EC=8A=A4=EB=84=88=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/sharing/RoomSubscribeListener.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java diff --git a/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java new file mode 100644 index 00000000..97ce7837 --- /dev/null +++ b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java @@ -0,0 +1,35 @@ +package com.debatetimer.event.sharing; + +import com.debatetimer.dto.sharing.request.ChairmanSharingRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.SessionSubscribeEvent; + +@Component +@RequiredArgsConstructor +public class RoomSubscribeListener { + + private static final String AUDIENCE_SUBSCRIBE_PREFIX = "/room/"; + private static final String CHAIRMAN_CHANNEL_PREFIX = "/chairman/"; + + private final SimpMessagingTemplate messagingTemplate; + + @EventListener + public void handleSubscribeEvent(SessionSubscribeEvent event) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage()); + + String destination = accessor.getDestination(); + if (destination == null) { + return; + } + + if (destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX)) { + long roomId = Long.parseLong(destination.replace(AUDIENCE_SUBSCRIBE_PREFIX, "")); + messagingTemplate.convertAndSend(CHAIRMAN_CHANNEL_PREFIX + roomId, new ChairmanSharingRequest(roomId)); + } + } +} + From 151afbbb91aa8b1a81f6f7974c141245f89b0df8 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 13 Nov 2025 02:25:27 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/sharing/SharingController.java | 22 +++++++++++++++++++ .../request/ChairmanSharingRequest.java | 7 ++++++ .../dto/sharing/request/SharingRequest.java | 9 ++++++++ .../dto/sharing/response/SharingResponse.java | 9 ++++++++ 4 files changed, 47 insertions(+) create mode 100644 src/main/java/com/debatetimer/controller/sharing/SharingController.java create mode 100644 src/main/java/com/debatetimer/dto/sharing/request/ChairmanSharingRequest.java create mode 100644 src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java create mode 100644 src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java diff --git a/src/main/java/com/debatetimer/controller/sharing/SharingController.java b/src/main/java/com/debatetimer/controller/sharing/SharingController.java new file mode 100644 index 00000000..e27e9ee1 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/sharing/SharingController.java @@ -0,0 +1,22 @@ +package com.debatetimer.controller.sharing; + +import com.debatetimer.dto.sharing.request.SharingRequest; +import com.debatetimer.dto.sharing.response.SharingResponse; +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.handler.annotation.SendTo; +import org.springframework.stereotype.Controller; + +@Controller +public class SharingController { + + @MessageMapping("/event/{roomId}") + @SendTo("/room/{roomId}") + public SharingResponse share( + @DestinationVariable(value = "roomId") long roomId, + @Payload SharingRequest request + ) { + return new SharingResponse(request.time()); + } +} diff --git a/src/main/java/com/debatetimer/dto/sharing/request/ChairmanSharingRequest.java b/src/main/java/com/debatetimer/dto/sharing/request/ChairmanSharingRequest.java new file mode 100644 index 00000000..ecc5134f --- /dev/null +++ b/src/main/java/com/debatetimer/dto/sharing/request/ChairmanSharingRequest.java @@ -0,0 +1,7 @@ +package com.debatetimer.dto.sharing.request; + +public record ChairmanSharingRequest( + long roomId +) { + +} diff --git a/src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java b/src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java new file mode 100644 index 00000000..b6063711 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java @@ -0,0 +1,9 @@ +package com.debatetimer.dto.sharing.request; + +import java.time.LocalDateTime; + +public record SharingRequest( + LocalDateTime time +) { + +} diff --git a/src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java b/src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java new file mode 100644 index 00000000..704384d1 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java @@ -0,0 +1,9 @@ +package com.debatetimer.dto.sharing.response; + +import java.time.LocalDateTime; + +public record SharingResponse( + LocalDateTime time +) { + +} From 15d9c2dcde6d19062143544059525ca71802a961 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 13 Nov 2025 02:46:25 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20member=20jwt=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sharing/WebSocketAuthMemberResolver.java | 41 +++++++++++++++++++ .../config/{ => sharing}/WebSocketConfig.java | 13 +++++- .../controller/sharing/SharingController.java | 3 ++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java rename src/main/java/com/debatetimer/config/{ => sharing}/WebSocketConfig.java (67%) diff --git a/src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java b/src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java new file mode 100644 index 00000000..de16ac9f --- /dev/null +++ b/src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java @@ -0,0 +1,41 @@ +package com.debatetimer.config.sharing; + +import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.controller.tool.jwt.AuthManager; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.service.auth.AuthService; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WebSocketAuthMemberResolver implements HandlerMethodArgumentResolver { + + private final AuthManager authManager; + private final AuthService authService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + String token = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + + if (token == null) { + throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER); + } + + String email = authManager.resolveAccessToken(token); + return authService.getMember(email); + } +} + diff --git a/src/main/java/com/debatetimer/config/WebSocketConfig.java b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java similarity index 67% rename from src/main/java/com/debatetimer/config/WebSocketConfig.java rename to src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java index a829e277..f4c37d31 100644 --- a/src/main/java/com/debatetimer/config/WebSocketConfig.java +++ b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java @@ -1,15 +1,26 @@ -package com.debatetimer.config; +package com.debatetimer.config.sharing; +import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration +@RequiredArgsConstructor @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + private final WebSocketAuthMemberResolver webSocketAuthMemberResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(webSocketAuthMemberResolver); + } + @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/room"); diff --git a/src/main/java/com/debatetimer/controller/sharing/SharingController.java b/src/main/java/com/debatetimer/controller/sharing/SharingController.java index e27e9ee1..edacf8e6 100644 --- a/src/main/java/com/debatetimer/controller/sharing/SharingController.java +++ b/src/main/java/com/debatetimer/controller/sharing/SharingController.java @@ -1,5 +1,7 @@ package com.debatetimer.controller.sharing; +import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.domain.member.Member; import com.debatetimer.dto.sharing.request.SharingRequest; import com.debatetimer.dto.sharing.response.SharingResponse; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -14,6 +16,7 @@ public class SharingController { @MessageMapping("/event/{roomId}") @SendTo("/room/{roomId}") public SharingResponse share( + @AuthMember Member member, @DestinationVariable(value = "roomId") long roomId, @Payload SharingRequest request ) { From 6cfc51cef381b75fa861337f6bf0bae7b6089c8c Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 13 Nov 2025 15:09:00 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/debatetimer/BaseStompTest.java | 76 +++++++++++++++++++ .../com/debatetimer/MessageFrameHandler.java | 31 ++++++++ .../sharing/SharingControllerTest.java | 39 ++++++++++ .../debatetimer/fixture/HeaderGenerator.java | 9 +++ 4 files changed, 155 insertions(+) create mode 100644 src/test/java/com/debatetimer/BaseStompTest.java create mode 100644 src/test/java/com/debatetimer/MessageFrameHandler.java create mode 100644 src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java diff --git a/src/test/java/com/debatetimer/BaseStompTest.java b/src/test/java/com/debatetimer/BaseStompTest.java new file mode 100644 index 00000000..698ade79 --- /dev/null +++ b/src/test/java/com/debatetimer/BaseStompTest.java @@ -0,0 +1,76 @@ +package com.debatetimer; + +import com.debatetimer.fixture.HeaderGenerator; +import com.debatetimer.fixture.entity.MemberGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; +import org.springframework.web.socket.sockjs.client.SockJsClient; +import org.springframework.web.socket.sockjs.client.Transport; +import org.springframework.web.socket.sockjs.client.WebSocketTransport; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class BaseStompTest { + + private static final String SOCKET_ENDPOINT = "/ws"; + + protected StompSession stompSession; + + @LocalServerPort + private int port; + + private final String url; + + private final WebSocketStompClient websocketClient; + + @Autowired + protected MemberGenerator memberGenerator; + + @Autowired + protected HeaderGenerator headerGenerator; + + public BaseStompTest() { + List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); + this.websocketClient = new WebSocketStompClient(new SockJsClient(transports)); + this.websocketClient.setMessageConverter(buildMessageConverter()); + this.url = "ws://localhost:"; + } + + private MessageConverter buildMessageConverter() { + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.findAndRegisterModules(); + converter.setObjectMapper(objectMapper); + return converter; + } + + @BeforeEach + public void connect() throws ExecutionException, InterruptedException, TimeoutException { + this.stompSession = this.websocketClient + .connectAsync(url + port + SOCKET_ENDPOINT, new StompSessionHandlerAdapter() { + }) + .get(3, TimeUnit.SECONDS); + } + + @AfterEach + public void disconnect() { + if (this.stompSession.isConnected()) { + this.stompSession.disconnect(); + } + } +} diff --git a/src/test/java/com/debatetimer/MessageFrameHandler.java b/src/test/java/com/debatetimer/MessageFrameHandler.java new file mode 100644 index 00000000..d0e60346 --- /dev/null +++ b/src/test/java/com/debatetimer/MessageFrameHandler.java @@ -0,0 +1,31 @@ +package com.debatetimer; + +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; + +public class MessageFrameHandler implements StompFrameHandler { + + private final CompletableFuture completableFuture = new CompletableFuture<>(); + private final Class tClass; + + public MessageFrameHandler(Class tClass) { + this.tClass = tClass; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + if (completableFuture.complete((T) payload)) { + } + } + + @Override + public Type getPayloadType(StompHeaders headers) { + return this.tClass; + } + + public CompletableFuture getCompletableFuture() { + return completableFuture; + } +} diff --git a/src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java b/src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java new file mode 100644 index 00000000..c1362bc5 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java @@ -0,0 +1,39 @@ +package com.debatetimer.controller.sharing; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.debatetimer.BaseStompTest; +import com.debatetimer.MessageFrameHandler; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.sharing.request.SharingRequest; +import com.debatetimer.dto.sharing.response.SharingResponse; +import java.time.LocalDateTime; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.simp.stomp.StompHeaders; + +class SharingControllerTest extends BaseStompTest { + + @Nested + class Share { + + @Test + void 사회자가_발생시킨_이벤트를_청중이_공유받는다() throws ExecutionException, InterruptedException, TimeoutException { + long roomId = 1L; + LocalDateTime time = LocalDateTime.now(); + MessageFrameHandler handler = new MessageFrameHandler<>(SharingResponse.class); + Member member = memberGenerator.generate("example@email.com"); + StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member); + stompSession.subscribe("/room/" + roomId, handler); //청중의 구독 + + stompSession.send(headers, new SharingRequest(time)); //사회자의 이벤트 발생 + + SharingResponse response = handler.getCompletableFuture() + .get(3L, TimeUnit.SECONDS); + assertThat(response.time()).isEqualTo(time); + } + } +} diff --git a/src/test/java/com/debatetimer/fixture/HeaderGenerator.java b/src/test/java/com/debatetimer/fixture/HeaderGenerator.java index a8a6b8bb..70362980 100644 --- a/src/test/java/com/debatetimer/fixture/HeaderGenerator.java +++ b/src/test/java/com/debatetimer/fixture/HeaderGenerator.java @@ -6,6 +6,7 @@ import io.restassured.http.Header; import io.restassured.http.Headers; import org.springframework.http.HttpHeaders; +import org.springframework.messaging.simp.stomp.StompHeaders; import org.springframework.stereotype.Component; @Component @@ -21,4 +22,12 @@ public Headers generateAccessTokenHeader(Member member) { String accessToken = jwtTokenProvider.createAccessToken(new MemberInfo(member)); return new Headers(new Header(HttpHeaders.AUTHORIZATION, accessToken)); } + + public StompHeaders generateAccessTokenHeader(String destination, Member member) { + String accessToken = jwtTokenProvider.createAccessToken(new MemberInfo(member)); + StompHeaders stompHeaders = new StompHeaders(); + stompHeaders.setDestination(destination); + stompHeaders.add(HttpHeaders.AUTHORIZATION, accessToken); + return stompHeaders; + } } From c86904f85128ac31db20144bc4d5168e60d45a32 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 13 Nov 2025 17:51:05 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20=EA=B5=AC=EB=8F=85=20=EC=8B=9C=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EB=B0=9C=EC=86=A1=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/sharing/WebSocketConfig.java | 2 +- .../event/sharing/RoomSubscribeListener.java | 6 +++- .../sharing/RoomSubscribeListenerTest.java | 33 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java diff --git a/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java index f4c37d31..a60474d2 100644 --- a/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java +++ b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java @@ -23,7 +23,7 @@ public void addArgumentResolvers(List resolvers) @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/room"); + registry.enableSimpleBroker("/room", "/chairman"); registry.setApplicationDestinationPrefixes("/app"); } diff --git a/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java index 97ce7837..143fda09 100644 --- a/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java +++ b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java @@ -2,12 +2,14 @@ import com.debatetimer.dto.sharing.request.ChairmanSharingRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionSubscribeEvent; +@Slf4j @Component @RequiredArgsConstructor public class RoomSubscribeListener { @@ -19,15 +21,17 @@ public class RoomSubscribeListener { @EventListener public void handleSubscribeEvent(SessionSubscribeEvent event) { + log.info("구독정보가 들어오긴 함"); StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage()); - String destination = accessor.getDestination(); + System.out.println("destination = " + destination); if (destination == null) { return; } if (destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX)) { long roomId = Long.parseLong(destination.replace(AUDIENCE_SUBSCRIBE_PREFIX, "")); + System.out.println("roomId = " + roomId); messagingTemplate.convertAndSend(CHAIRMAN_CHANNEL_PREFIX + roomId, new ChairmanSharingRequest(roomId)); } } diff --git a/src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java b/src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java new file mode 100644 index 00000000..b8e2bba7 --- /dev/null +++ b/src/test/java/com/debatetimer/event/sharing/RoomSubscribeListenerTest.java @@ -0,0 +1,33 @@ +package com.debatetimer.event.sharing; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.debatetimer.BaseStompTest; +import com.debatetimer.MessageFrameHandler; +import com.debatetimer.dto.sharing.request.ChairmanSharingRequest; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class RoomSubscribeListenerTest extends BaseStompTest { + + @Nested + class SubscribeListener { + + @Test + void 새로운_청중이_공유되면_사회자에게_정보공유_트리거를_발송한다() throws ExecutionException, InterruptedException, TimeoutException { + long roomId = 1L; + MessageFrameHandler handler = new MessageFrameHandler<>( + ChairmanSharingRequest.class); + stompSession.subscribe("/chairman/" + roomId, handler); + + stompSession.subscribe("/room/" + roomId, handler); + + ChairmanSharingRequest sharingRequest = handler.getCompletableFuture() + .get(3L, TimeUnit.SECONDS); + assertThat(sharingRequest).isNotNull(); + } + } +} From b1af2ee1872fd9362eaa53fca0cbd13d59b62e2c Mon Sep 17 00:00:00 2001 From: coli Date: Mon, 17 Nov 2025 18:47:26 +0900 Subject: [PATCH 08/14] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EA=B9=85=20=EC=A0=9C=EA=B1=B0=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/event/sharing/RoomSubscribeListener.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java index 143fda09..69f4c744 100644 --- a/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java +++ b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java @@ -21,17 +21,14 @@ public class RoomSubscribeListener { @EventListener public void handleSubscribeEvent(SessionSubscribeEvent event) { - log.info("구독정보가 들어오긴 함"); StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage()); String destination = accessor.getDestination(); - System.out.println("destination = " + destination); if (destination == null) { return; } if (destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX)) { long roomId = Long.parseLong(destination.replace(AUDIENCE_SUBSCRIBE_PREFIX, "")); - System.out.println("roomId = " + roomId); messagingTemplate.convertAndSend(CHAIRMAN_CHANNEL_PREFIX + roomId, new ChairmanSharingRequest(roomId)); } } From 5481b312e1c8465e739fb63f276f8888d9c147bd Mon Sep 17 00:00:00 2001 From: coli Date: Mon, 17 Nov 2025 18:52:56 +0900 Subject: [PATCH 09/14] =?UTF-8?q?chore:=20abstract=20=EC=BD=94=EB=93=9C=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 --- src/test/java/com/debatetimer/BaseStompTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/debatetimer/BaseStompTest.java b/src/test/java/com/debatetimer/BaseStompTest.java index 698ade79..4f0410c0 100644 --- a/src/test/java/com/debatetimer/BaseStompTest.java +++ b/src/test/java/com/debatetimer/BaseStompTest.java @@ -24,7 +24,7 @@ import org.springframework.web.socket.sockjs.client.WebSocketTransport; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class BaseStompTest { +public abstract class BaseStompTest { private static final String SOCKET_ENDPOINT = "/ws"; From efbecad6d2aaff23b89a0fb6fe3d15ad4d468d34 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 20 Nov 2025 18:30:10 +0900 Subject: [PATCH 10/14] =?UTF-8?q?style:=20=EA=B3=B5=EB=B0=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b3597f6c..06191f38 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' - //Websocket + // Websocket implementation 'org.springframework.boot:spring-boot-starter-websocket' // JWT From 93ac5af7ca0d9df2e1c0b6265fe481799df73586 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 20 Nov 2025 18:53:57 +0900 Subject: [PATCH 11/14] =?UTF-8?q?chore:=20CORS=20=EC=84=A4=EC=A0=95=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 --- .../com/debatetimer/config/CorsConfig.java | 27 ++++------------ .../debatetimer/config/CorsProperties.java | 31 +++++++++++++++++++ .../config/sharing/WebSocketConfig.java | 4 ++- ...onfigTest.java => CorsPropertiesTest.java} | 9 +++--- 4 files changed, 44 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/debatetimer/config/CorsProperties.java rename src/test/java/com/debatetimer/config/{CorsConfigTest.java => CorsPropertiesTest.java} (84%) diff --git a/src/main/java/com/debatetimer/config/CorsConfig.java b/src/main/java/com/debatetimer/config/CorsConfig.java index 216c15da..12511729 100644 --- a/src/main/java/com/debatetimer/config/CorsConfig.java +++ b/src/main/java/com/debatetimer/config/CorsConfig.java @@ -1,8 +1,7 @@ package com.debatetimer.config; -import com.debatetimer.exception.custom.DTInitializationException; -import com.debatetimer.exception.errorcode.InitializationErrorCode; -import org.springframework.beans.factory.annotation.Value; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -10,30 +9,16 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties(CorsProperties.class) public class CorsConfig implements WebMvcConfigurer { - private final String[] corsOrigin; - - public CorsConfig(@Value("${cors.origin}") String[] corsOrigin) { - validate(corsOrigin); - this.corsOrigin = corsOrigin; - } - - private void validate(String[] corsOrigin) { - if (corsOrigin == null || corsOrigin.length == 0) { - throw new DTInitializationException(InitializationErrorCode.CORS_ORIGIN_EMPTY); - } - for (String origin : corsOrigin) { - if (origin == null || origin.isBlank()) { - throw new DTInitializationException(InitializationErrorCode.CORS_ORIGIN_STRING_BLANK); - } - } - } + private final CorsProperties corsProperties; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOriginPatterns(corsOrigin) + .allowedOriginPatterns(corsProperties.getCorsOrigin()) .allowedMethods( HttpMethod.GET.name(), HttpMethod.POST.name(), diff --git a/src/main/java/com/debatetimer/config/CorsProperties.java b/src/main/java/com/debatetimer/config/CorsProperties.java new file mode 100644 index 00000000..8103553b --- /dev/null +++ b/src/main/java/com/debatetimer/config/CorsProperties.java @@ -0,0 +1,31 @@ +package com.debatetimer.config; + +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@Getter +@ConfigurationProperties(prefix = "cors.origin") +public class CorsProperties { + + private final String[] corsOrigin; + + //TODO 머지될 때 dev, prod secret 갱신 필요 + public CorsProperties(String[] corsOrigin) { + validate(corsOrigin); + this.corsOrigin = corsOrigin; + } + + private void validate(String[] corsOrigin) { + if (corsOrigin == null || corsOrigin.length == 0) { + throw new DTInitializationException(InitializationErrorCode.CORS_ORIGIN_EMPTY); + } + for (String origin : corsOrigin) { + if (origin == null || origin.isBlank()) { + throw new DTInitializationException(InitializationErrorCode.CORS_ORIGIN_STRING_BLANK); + } + } + } +} diff --git a/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java index a60474d2..b2ee31a6 100644 --- a/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java +++ b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java @@ -1,5 +1,6 @@ package com.debatetimer.config.sharing; +import com.debatetimer.config.CorsProperties; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -14,6 +15,7 @@ @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + private final CorsProperties corsProperties; private final WebSocketAuthMemberResolver webSocketAuthMemberResolver; @Override @@ -30,7 +32,7 @@ public void configureMessageBroker(MessageBrokerRegistry registry) { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") - .setAllowedOriginPatterns("*") + .setAllowedOriginPatterns(corsProperties.getCorsOrigin()) .withSockJS(); } } diff --git a/src/test/java/com/debatetimer/config/CorsConfigTest.java b/src/test/java/com/debatetimer/config/CorsPropertiesTest.java similarity index 84% rename from src/test/java/com/debatetimer/config/CorsConfigTest.java rename to src/test/java/com/debatetimer/config/CorsPropertiesTest.java index 0011729a..b4889839 100644 --- a/src/test/java/com/debatetimer/config/CorsConfigTest.java +++ b/src/test/java/com/debatetimer/config/CorsPropertiesTest.java @@ -9,21 +9,21 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; -class CorsConfigTest { +class CorsPropertiesTest { @Nested class Validate { @Test void 허용된_도메인이_null_일_경우_예외를_발생시칸다() { - assertThatThrownBy(() -> new CorsConfig(null)) + assertThatThrownBy(() -> new CorsProperties(null)) .isInstanceOf(DTInitializationException.class) .hasMessage(InitializationErrorCode.CORS_ORIGIN_EMPTY.getMessage()); } @Test void 허용된_도메인이_빈_배열일_경우_예외를_발생시칸다() { - assertThatThrownBy(() -> new CorsConfig(new String[0])) + assertThatThrownBy(() -> new CorsProperties(new String[0])) .isInstanceOf(DTInitializationException.class) .hasMessage(InitializationErrorCode.CORS_ORIGIN_EMPTY.getMessage()); } @@ -31,10 +31,9 @@ class Validate { @ParameterizedTest @NullAndEmptySource void 허용된_도메인_중에_빈_값이_있을_경우_예외를_발생시킨다(String empty) { - assertThatThrownBy(() -> new CorsConfig(new String[]{empty})) + assertThatThrownBy(() -> new CorsProperties(new String[]{empty})) .isInstanceOf(DTInitializationException.class) .hasMessage(InitializationErrorCode.CORS_ORIGIN_STRING_BLANK.getMessage()); - } } } From cebf6f54350488286a7b6ec53a9b44ccfe33e6af Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 20 Nov 2025 19:08:47 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=EB=90=9C?= =?UTF-8?q?=20roomId=20=EB=B0=9C=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/sharing/RoomSubscribeListener.java | 13 ++++++++++++- .../exception/errorcode/ClientErrorCode.java | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java index 69f4c744..f2e4b47a 100644 --- a/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java +++ b/src/main/java/com/debatetimer/event/sharing/RoomSubscribeListener.java @@ -1,6 +1,8 @@ package com.debatetimer.event.sharing; import com.debatetimer.dto.sharing.request.ChairmanSharingRequest; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -28,9 +30,18 @@ public void handleSubscribeEvent(SessionSubscribeEvent event) { } if (destination.startsWith(AUDIENCE_SUBSCRIBE_PREFIX)) { - long roomId = Long.parseLong(destination.replace(AUDIENCE_SUBSCRIBE_PREFIX, "")); + long roomId = parseRoomId(destination); messagingTemplate.convertAndSend(CHAIRMAN_CHANNEL_PREFIX + roomId, new ChairmanSharingRequest(roomId)); } } + + private long parseRoomId(String destination) { + try { + String parsedRoomId = destination.substring(AUDIENCE_SUBSCRIBE_PREFIX.length()); + return Long.parseLong(parsedRoomId); + } catch (NumberFormatException exception) { + throw new DTClientErrorException(ClientErrorCode.INVALID_ROOM_ID); + } + } } diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 695cbb0c..5bf7d05b 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -53,6 +53,8 @@ public enum ClientErrorCode implements ResponseErrorCode { ALREADY_DONE_POLL(HttpStatus.BAD_REQUEST, "이미 완료된 투표 입니다"), ALREADY_VOTED_PARTICIPANT(HttpStatus.BAD_REQUEST, "이미 참여한 투표자 입니다"), + INVALID_ROOM_ID(HttpStatus.BAD_REQUEST, "잘못된 roomId 값입니다"), + TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "토론 테이블을 찾을 수 없습니다."), NOT_TABLE_OWNER(HttpStatus.UNAUTHORIZED, "테이블을 소유한 회원이 아닙니다."), POLL_NOT_FOUND(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다."), From 3a429cdac189cb32971a463d3b9891fcc411eec2 Mon Sep 17 00:00:00 2001 From: coli Date: Thu, 20 Nov 2025 19:13:49 +0900 Subject: [PATCH 13/14] =?UTF-8?q?fix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20cors?= =?UTF-8?q?Origin=20prefix=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/debatetimer/controller/GlobalControllerTest.java | 2 +- src/test/resources/application.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/debatetimer/controller/GlobalControllerTest.java b/src/test/java/com/debatetimer/controller/GlobalControllerTest.java index 6c9c041f..4e4cace9 100644 --- a/src/test/java/com/debatetimer/controller/GlobalControllerTest.java +++ b/src/test/java/com/debatetimer/controller/GlobalControllerTest.java @@ -8,7 +8,7 @@ public class GlobalControllerTest extends BaseControllerTest { - @Value("${cors.origin}") + @Value("${cors.origin.cors-origin[0]}") private String corsOrigin; @Nested diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index b844ae6c..fa923caa 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -3,7 +3,9 @@ spring: active: test cors: - origin: http://test.debate-timer.com + origin: + cors-origin: + - http://test.debate-timer.com oauth: client_id: oauth_client_id From 0dc1af870dea0f793dc0135a26c4e64f05557ebf Mon Sep 17 00:00:00 2001 From: coli Date: Tue, 25 Nov 2025 19:43:59 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20cors?= =?UTF-8?q?Origin=20prefix=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 4 +++- src/main/resources/application-prod.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b60259e8..46ebc8ed 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -21,7 +21,9 @@ spring: baseline-version: 1 cors: - origin: ${secret.cors.origin} + origin: + cors-origin: + - ${secret.cors.origin} oauth: client_id: ${secret.oauth.client_id} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 208ac91b..cc4de59d 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -20,7 +20,9 @@ spring: baseline-version: 1 cors: - origin: ${secret.cors.origin} + origin: + cors-origin: + - ${secret.cors.origin} oauth: client_id: ${secret.oauth.client_id}