From a7515c3de7f6c1027950d80cac8c0548ace9815a Mon Sep 17 00:00:00 2001 From: Jae-HyeokKim Date: Tue, 26 Aug 2025 15:44:50 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=20=EB=A1=9C=EC=A7=81=20=EB=94=94=EB=B2=84=EA=B9=85=20?= =?UTF-8?q?(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/bus/ChatRedisPublisher.java | 26 +++++++-------- .../domain/chat/bus/ChatRedisSubscriber.java | 16 ++------- .../controller/ChatMessagingController.java | 29 +++++----------- .../ChatNotificationController.java | 2 +- .../domain/chat/entity/ChatReadState.java | 14 ++++++-- .../Impl/ChatNotificationServiceImpl.java | 2 +- .../domain/draft/dto/DraftRequest.java | 4 +++ .../domain/player/entity/Player.java | 33 +++++++++++++++---- .../global/configuration/OpenApiConfig.java | 25 +++++++------- .../global/configuration/RedisConfig.java | 1 - .../configuration/UnifiedWebSocketConfig.java | 10 ------ .../global/redis/RedisSubscriber.java | 3 +- 12 files changed, 80 insertions(+), 85 deletions(-) diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisPublisher.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisPublisher.java index 0a714fe..97c6ab1 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisPublisher.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisPublisher.java @@ -1,35 +1,31 @@ package likelion.mlb.backendProject.domain.chat.bus; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class ChatRedisPublisher { + private final StringRedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper; - // JVM 런타임 식별자를 사용하여 노드 구분 (재시작 시마다 변경됨) - private final String nodeId = System.getProperty("node.id", - java.lang.management.ManagementFactory.getRuntimeMXBean().getName()); - - public void publishToRoom(String roomId, Map payload) throws JsonProcessingException { - System.out.println("roomId = "+roomId); - System.out.println("payload = "+payload); + private final String nodeId = java.util.UUID.randomUUID().toString(); - //payload.put("_src", nodeId()); // 루프 방지 태그 + public void publishToRoom(UUID roomId, Map payload) { + try { + payload.put("_src", nodeId); // 루프 방지 태그 String json = objectMapper.writeValueAsString(payload); - System.out.println("publishToRoom = "+json); - - stringRedisTemplate.convertAndSend(roomId, json); - + stringRedisTemplate.convertAndSend(ChatChannels.roomChannel(roomId), json); + } catch (Exception ignore) { + } } - public String nodeId() { return nodeId; } + public String nodeId() { + return nodeId; + } } \ No newline at end of file diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisSubscriber.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisSubscriber.java index 2a27c95..26b9c86 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisSubscriber.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/bus/ChatRedisSubscriber.java @@ -25,21 +25,11 @@ public void onMessage(Message message, byte[] pattern) { @SuppressWarnings("unchecked") Map payload = objectMapper.readValue(body, Map.class); - // ✅ 내 노드에서 보낸 메시지는 이미 전달했으므로 Redis를 통한 재전달 방지 - // (ChatRedisPublisher에서 nodeId를 _src로 설정) - Object srcNodeId = payload.get("_src"); - if (srcNodeId != null) { - // 현재 노드의 ID와 비교 (ChatRedisPublisher의 nodeId와 비교하려면 주입받아야 함) - // 여기서는 간단히 현재 JVM의 식별자로 비교 - String currentNodeId = System.getProperty("node.id", - java.lang.management.ManagementFactory.getRuntimeMXBean().getName()); - if (currentNodeId.equals(srcNodeId)) { - return; // 내가 보낸 메시지는 중복 전송 방지 - } - } + // ❌ 내 노드에서 보낸 것도 브로커로 전달해야 함 (중복루프 없음) + // if (publisher.nodeId().equals(payload.get("_src"))) return; String topic = ChatChannels.toTopic(channel); - // ✅ STOMP 브로커로 전송 (다른 노드에서 온 메시지만) + // ✅ STOMP 브로커로 전송 messagingTemplate.convertAndSend(topic, payload); } catch (Exception e) { diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatMessagingController.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatMessagingController.java index e2b270c..c8cd91c 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatMessagingController.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatMessagingController.java @@ -1,7 +1,6 @@ package likelion.mlb.backendProject.domain.chat.controller; -import com.fasterxml.jackson.core.JsonProcessingException; import java.security.Principal; import java.util.Map; import java.util.UUID; @@ -16,7 +15,6 @@ import likelion.mlb.backendProject.domain.chat.entity.ChatMessage; import likelion.mlb.backendProject.domain.chat.service.ChatMessageService; import likelion.mlb.backendProject.global.security.dto.CustomUserDetails; -import org.springframework.transaction.annotation.Transactional; @Controller @RequiredArgsConstructor @@ -28,10 +26,10 @@ public class ChatMessagingController { private final ChatRedisPublisher chatRedisPublisher; - @Transactional - @MessageMapping("/chat/send") - public void send(ChatSendRequest req, - Principal principal) throws JsonProcessingException { + @MessageMapping("/chat/{roomId}/send") + public void send(@DestinationVariable UUID roomId, + ChatSendRequest req, + Principal principal) { UUID userId = null; if (principal instanceof Authentication auth @@ -40,20 +38,17 @@ public void send(ChatSendRequest req, } // ✅ 방 멤버인지 권한 체크 (아니면 바로 거절) - if (userId == null || !membershipRepository.isMember(req.getRoomId(), userId)) { + if (userId == null || !membershipRepository.isMember(roomId, userId)) { throw new org.springframework.messaging.MessagingException("Not a member of this chat room"); // 또는 그냥 return; // 조용히 무시하고 싶으면 } - - System.out.println("------------/chat/{roomId}/send 시작 "); - // 안전장치: 메시지의 roomId는 URL의 roomId로 강제 - ChatMessage saved = chatMessageService.saveUserMessage(req.getRoomId(), userId, req.getContent()); + ChatMessage saved = chatMessageService.saveUserMessage(roomId, userId, req.getContent()); Map payload = Map.of( "id", saved.getId().toString(), - "chatRoomId", req.getRoomId().toString(), + "chatRoomId", roomId.toString(), "type", saved.getMessageType().name(), "content", saved.getContent(), "userId", userId != null ? userId.toString() : null, @@ -61,14 +56,6 @@ public void send(ChatSendRequest req, ); - //System.out.println("------------받은메세지 payload"+payload.get(0).toString()); - - System.out.println("------------받은메세지"+req.getContent()); - - // ✅ 즉시 현재 노드의 클라이언트에게 전달 - messagingTemplate.convertAndSend("/topic/chat/" + req.getRoomId(), payload); - - // ✅ 다른 노드를 위해 Redis로도 전달 - chatRedisPublisher.publishToRoom("chat." + req.getRoomId(), payload); + chatRedisPublisher.publishToRoom(roomId, new java.util.HashMap<>(payload)); } } diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatNotificationController.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatNotificationController.java index eaa9bec..d8cc8c4 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatNotificationController.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/controller/ChatNotificationController.java @@ -50,7 +50,7 @@ public ResponseEntity> roomAlert( "createdAt", saved.getCreatedAt().toString() ); messagingTemplate.convertAndSend("/topic/chat/" + roomId, payload); - //chatRedisPublisher.publishToRoom(roomId, new java.util.HashMap<>(payload)); + chatRedisPublisher.publishToRoom(roomId, new java.util.HashMap<>(payload)); return ResponseEntity.ok(Map.of("ok", true, "id", saved.getId().toString())); } diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/entity/ChatReadState.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/entity/ChatReadState.java index b9fa383..40d2476 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/entity/ChatReadState.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/entity/ChatReadState.java @@ -8,7 +8,11 @@ @Entity @Table(name = "chat_read_state", uniqueConstraints = @UniqueConstraint(columnNames = {"chat_room_id", "user_id"})) -@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder public class ChatReadState { @Id @@ -29,8 +33,12 @@ public class ChatReadState { @PrePersist void prePersist() { - if (id == null) id = UUID.randomUUID(); - if (lastReadAt == null) lastReadAt = Instant.EPOCH; // 처음엔 1970-01-01 + if (id == null) { + id = UUID.randomUUID(); + } + if (lastReadAt == null) { + lastReadAt = Instant.EPOCH; // 처음엔 1970-01-01 + } } public void mark(UUID messageId, Instant when) { diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/service/Impl/ChatNotificationServiceImpl.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/service/Impl/ChatNotificationServiceImpl.java index 3bc2dfd..3a3832a 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/service/Impl/ChatNotificationServiceImpl.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/chat/service/Impl/ChatNotificationServiceImpl.java @@ -85,7 +85,7 @@ protected void doDispatch(UUID playerId, "createdAt", saved.getCreatedAt().toString() ); messagingTemplate.convertAndSend("/topic/chat/" + roomId, payload); - // chatRedisPublisher.publishToRoom(roomId, new java.util.HashMap<>(payload)); + chatRedisPublisher.publishToRoom(roomId, new java.util.HashMap<>(payload)); } } diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/draft/dto/DraftRequest.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/draft/dto/DraftRequest.java index 3c8c892..86e38fc 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/draft/dto/DraftRequest.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/draft/dto/DraftRequest.java @@ -1,6 +1,7 @@ package likelion.mlb.backendProject.domain.draft.dto; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -10,12 +11,15 @@ @Data @NoArgsConstructor @AllArgsConstructor +@Builder public class DraftRequest { private UUID draftId; // 드래프트 방 pk값 private UUID participantId; // 드래프트 참가자 pk값 + private String userName; // 참가자 이름 + private UUID playerId; // player pk값 private String playerWebName; // player 영어 이름 diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/domain/player/entity/Player.java b/backendProject/src/main/java/likelion/mlb/backendProject/domain/player/entity/Player.java index 224cb6d..0e8919b 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/domain/player/entity/Player.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/domain/player/entity/Player.java @@ -1,6 +1,8 @@ package likelion.mlb.backendProject.domain.player.entity; import jakarta.persistence.*; +import java.util.stream.Collectors; +import likelion.mlb.backendProject.domain.draft.dto.DraftRequest; import likelion.mlb.backendProject.domain.player.cache.dto.PlayerDto; import likelion.mlb.backendProject.domain.team.entity.Team; import likelion.mlb.backendProject.global.jpa.entity.BaseTime; @@ -14,7 +16,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; @Entity @Table(name = "player") @@ -114,8 +115,8 @@ public static Player playerBuilder(FplElement element, Map typeMap, Map teamMap) { - String picUri = "https://resources.premierleague.com/premierleague/photos/players/250x250/p" - + element.getCode() + ".png"; + String picUri = "https://resources.premierleague.com/premierleague25/photos/players/110x140/" + + element.getCode() + ".png"; return Player.builder() .code(element.getCode()) .fplId(element.getFplId()) @@ -137,10 +138,9 @@ public static Player playerBuilder(FplElement element, public void updatePlayer(FplElement element, Map typeMap, Map teamMap) { - String picUri = "https://resources.premierleague.com/premierleague/photos/players/250x250/p" - + element.getCode() + ".png"; + String picUri = "https://resources.premierleague.com/premierleague25/photos/players/110x140/" + + element.getCode() + ".png"; this.pic = picUri; - this.code = element.getCode(); this.status = element.getStatus(); this.fplId = element.getFplId(); this.news = element.getNews(); @@ -175,5 +175,26 @@ public static List toDtoList(List players) { ).collect(Collectors.toList()); } + /** + * Player 엔티티 → DraftRequest 변환 + */ + public static DraftRequest toDraftRequest(Player player) { + return DraftRequest.builder() + .playerId(player.getId()) + .playerWebName(player.getWebName()) + .playerKrName(player.getKrName()) + .playerPic(player.getPic()) + + // team관련 설정 + .teamName(player.getTeam().getName()) + .teamKrName(player.getTeam().getKrName()) + + // 포지션(elementType) 관련 설정 + .elementTypeId(player.getElementType().getId()) + .elementTypePluralName(player.getElementType().getPluralName()) + .elementTypeKrName(player.getElementType().getKrName()) + .build(); + } + } diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/OpenApiConfig.java b/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/OpenApiConfig.java index 316195a..4bd2387 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/OpenApiConfig.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/OpenApiConfig.java @@ -14,23 +14,24 @@ public class OpenApiConfig { @Bean public OpenAPI openAPI() { Info info = new Info() - .title("MLB Project API") - .version("v1.0.0") - .description("API documentation for the MLB project."); + .title("MLB Project API") + .version("v1.0.0") + .description("API documentation for the MLB project."); // Security Scheme for JWT String jwtSchemeName = "jwtAuth"; SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); Components components = new Components() - .addSecuritySchemes(jwtSchemeName, new SecurityScheme() - .name(jwtSchemeName) - .type(SecurityScheme.Type.HTTP) // HTTP 방식 - .scheme("bearer") - .bearerFormat("JWT")); + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) // HTTP 방식 + .scheme("bearer") + .bearerFormat("JWT")); return new OpenAPI() - .info(info) - .addSecurityItem(securityRequirement) - .components(components); + .info(info) + .addSecurityItem(securityRequirement) + .components(components); } -} + +} \ No newline at end of file diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/RedisConfig.java b/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/RedisConfig.java index 77b0781..0a958d0 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/RedisConfig.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/RedisConfig.java @@ -62,7 +62,6 @@ public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnecti RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(new MessageListenerAdapter(redisSubscriber), new PatternTopic("draft.*")); - container.addMessageListener(new MessageListenerAdapter(redisSubscriber), new PatternTopic("chat/*")); return container; } } diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/UnifiedWebSocketConfig.java b/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/UnifiedWebSocketConfig.java index 1c3420a..1329b9f 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/UnifiedWebSocketConfig.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/global/configuration/UnifiedWebSocketConfig.java @@ -86,17 +86,7 @@ protected Principal determineUser(ServerHttpRequest request, WebSocketHandler ws @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); - - // TaskScheduler 생성 및 초기화 -// org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler scheduler = -// new org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler(); -// scheduler.setPoolSize(1); -// scheduler.setThreadNamePrefix("websocket-heartbeat-"); -// scheduler.initialize(); - registry.enableSimpleBroker("/topic", "/queue"); -// .setTaskScheduler(scheduler) -// .setHeartbeatValue(new long[]{10000, 10000}); // 하트비트 간격 조정 registry.setUserDestinationPrefix("/user"); } diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/global/redis/RedisSubscriber.java b/backendProject/src/main/java/likelion/mlb/backendProject/global/redis/RedisSubscriber.java index 143e601..eee4ff7 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/global/redis/RedisSubscriber.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/global/redis/RedisSubscriber.java @@ -27,8 +27,7 @@ public void onMessage(Message message, byte[] pattern) { String msgBody = new String(message.getBody()); DraftRequest draftRequest = objectMapper.readValue(msgBody, DraftRequest.class); -// simpMessagingTemplate.convertAndSend("/topic/draft." + draftRequest.getDraftId(), draftRequest); - simpMessagingTemplate.convertAndSend("/chat/" + draftRequest.getDraftId(), draftRequest); + simpMessagingTemplate.convertAndSend("/topic/draft." + draftRequest.getDraftId(), draftRequest); } catch (Exception e) { }