From 94b4cd79390405df22ee9c971299a6a8a66379b6 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Wed, 21 Jan 2026 23:40:23 +0900 Subject: [PATCH 01/16] =?UTF-8?q?chore:=20FRONTEND=5FPROD=5FURL,=20BACKEND?= =?UTF-8?q?=5FURL,=20BACKEND=5FPROD=5FURL=20Value=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linkit/global/config/WebSocketConfig.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/swyp/linkit/global/config/WebSocketConfig.java b/src/main/java/org/swyp/linkit/global/config/WebSocketConfig.java index 37d98516..8d893762 100644 --- a/src/main/java/org/swyp/linkit/global/config/WebSocketConfig.java +++ b/src/main/java/org/swyp/linkit/global/config/WebSocketConfig.java @@ -1,6 +1,7 @@ package org.swyp.linkit.global.config; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; @@ -17,6 +18,15 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { private final WebSocketAuthInterceptor webSocketAuthInterceptor; private final StompErrorHandler stompErrorHandler; + @Value("${FRONTEND_URL}") + private String frontendUrl; + @Value("${FRONTEND_PROD_URL}") + private String frontendProdUrl; + @Value("${BACKEND_URL}") + private String backendUrl; + @Value("${BACKEND_PROD_URL}") + private String backendProdUrl; + @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 클라이언트가 구독할 prefix (서버 -> 클라이언트) @@ -34,10 +44,10 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { // WebSocket 연결 엔드포인트 (SockJS fallback 포함) registry.addEndpoint("/ws") .setAllowedOriginPatterns( - "{FRONTEND_URL}", // 로컬 개발 - "{FRONTEND_PROD_URL}", // Vercel 프리뷰/배포 - "{BACKEND_URL}", // 운영 도메인 - "{BACKEND_PROD_URL}" // 서브도메인 + frontendUrl, // 로컬 개발 + frontendProdUrl, // Vercel 프리뷰/배포 + backendUrl, // 운영 도메인 + backendProdUrl // 서브도메인 ) .withSockJS(); From 5efc1b131d9c0a156d365a4f8fb6c19a921ba7f3 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Sat, 4 Apr 2026 22:23:57 +0900 Subject: [PATCH 02/16] =?UTF-8?q?refactor=20:=20message=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp/linkit/domain/notification/dto/NotificationDto.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationDto.java b/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationDto.java index fce64e73..0ab0e311 100644 --- a/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationDto.java +++ b/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationDto.java @@ -21,8 +21,9 @@ public class NotificationDto { private Long refId; private boolean isRead; private LocalDateTime createdAt; + private String message; - public static NotificationDto from(Notification notification) { + public static NotificationDto from(Notification notification, String message) { return NotificationDto.builder() .id(notification.getId()) .receiverId(notification.getReceiver().getId()) @@ -31,6 +32,7 @@ public static NotificationDto from(Notification notification) { .refId(notification.getRefId()) .isRead(notification.isRead()) .createdAt(notification.getCreatedAt()) + .message(message) .build(); } } \ No newline at end of file From 3c715565d595ee4e0e66d8f42a4e3f4e686e1040 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Sat, 4 Apr 2026 22:24:08 +0900 Subject: [PATCH 03/16] =?UTF-8?q?refactor=20:=20REST=20API=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=97=90=20message=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationServiceImpl.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java index 60d5af67..58c94939 100644 --- a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java @@ -25,6 +25,9 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; @Slf4j @@ -70,7 +73,7 @@ public NotificationDto createNotification(Long receiverId, Long senderId, Notifi publishNotificationToRedis(savedNotification, senderNickname, message); log.info("알림 생성 및 발송: receiverId={}, type={}, refId={}", receiverId, type, refId); - return NotificationDto.from(savedNotification); + return NotificationDto.from(savedNotification, message); } @Override @@ -86,7 +89,7 @@ public NotificationDto createSystemNotification(Long receiverId, NotificationTyp publishNotificationToRedis(savedNotification, "시스템", message); log.info("시스템 알림 생성 및 발송: receiverId={}, type={}, refId={}", receiverId, type, refId); - return NotificationDto.from(savedNotification); + return NotificationDto.from(savedNotification, message); } // ===== 미읽음 개수 조회 ===== @@ -133,8 +136,22 @@ public NotificationListResponseDto getNotifications(Long userId) { combinedNotifications.addAll(unreadNotifications); combinedNotifications.addAll(readNotifications); + // sender nickname 배치 조회 + Set senderIds = combinedNotifications.stream() + .map(Notification::getSenderId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map nicknameMap = userRepository.findAllById(senderIds).stream() + .collect(Collectors.toMap(User::getId, User::getNickname)); + List notificationDtos = combinedNotifications.stream() - .map(NotificationDto::from) + .map(n -> { + String senderNickname = n.getSenderId() != null + ? nicknameMap.getOrDefault(n.getSenderId(), "시스템") + : "시스템"; + String message = generateNotificationMessage(n.getNotificationType(), senderNickname); + return NotificationDto.from(n, message); + }) .collect(Collectors.toList()); return NotificationListResponseDto.of(notificationDtos, unreadNotifications.size()); From c0c0345ba13375a42a485e1ddaf9d7a89176b910 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Sat, 4 Apr 2026 22:24:18 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor=20:=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=B0=9C=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/controller/ChatStompController.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java b/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java index 020d3393..45e1e48c 100644 --- a/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java +++ b/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java @@ -8,7 +8,10 @@ import org.springframework.stereotype.Controller; import org.swyp.linkit.domain.chat.dto.request.ChatSendRequestDto; import org.swyp.linkit.domain.chat.entity.ChatMessage; +import org.swyp.linkit.domain.chat.entity.ChatRoom; import org.swyp.linkit.domain.chat.service.ChatService; +import org.swyp.linkit.domain.notification.entity.NotificationType; +import org.swyp.linkit.domain.notification.service.NotificationService; import java.security.Principal; @@ -24,6 +27,7 @@ public class ChatStompController { private final ChatService chatService; + private final NotificationService notificationService; /** * 메시지 전송 @@ -45,6 +49,11 @@ public void send(@Payload ChatSendRequestDto dto, Principal principal) { dto.getMessageType(), dto.getImageUrl()); chatService.publishToRedis(saved); + + // 상대방에게 채팅 알림 발송 + ChatRoom room = saved.getChatRoom(); + Long receiverId = room.getMentorId().equals(senderId) ? room.getMenteeId() : room.getMentorId(); + notificationService.createNotification(receiverId, senderId, NotificationType.CHAT_MESSAGE, roomId); } /** From 761d298efae22eeb7ef5add7af86c3a985597c25 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Sat, 4 Apr 2026 23:50:03 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor=20:=20null=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationServiceImpl.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java index 58c94939..d768c488 100644 --- a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java @@ -24,6 +24,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -141,14 +142,15 @@ public NotificationListResponseDto getNotifications(Long userId) { .map(Notification::getSenderId) .filter(Objects::nonNull) .collect(Collectors.toSet()); - Map nicknameMap = userRepository.findAllById(senderIds).stream() - .collect(Collectors.toMap(User::getId, User::getNickname)); + Map nicknameMap = senderIds.isEmpty() + ? Collections.emptyMap() + : userRepository.findAllById(senderIds).stream() + .collect(Collectors.toMap(User::getId, + u -> u.getNickname() != null ? u.getNickname() : "시스템")); List notificationDtos = combinedNotifications.stream() .map(n -> { - String senderNickname = n.getSenderId() != null - ? nicknameMap.getOrDefault(n.getSenderId(), "시스템") - : "시스템"; + String senderNickname = nicknameMap.getOrDefault(n.getSenderId(), "시스템"); String message = generateNotificationMessage(n.getNotificationType(), senderNickname); return NotificationDto.from(n, message); }) From a310cb429c916e6f0c5c3ebf1453062323d765d8 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Sun, 5 Apr 2026 00:07:40 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor=20:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=B0=9C=EC=86=A1=20=EC=83=81=ED=83=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/controller/ChatStompController.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java b/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java index 45e1e48c..020d3393 100644 --- a/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java +++ b/src/main/java/org/swyp/linkit/domain/chat/controller/ChatStompController.java @@ -8,10 +8,7 @@ import org.springframework.stereotype.Controller; import org.swyp.linkit.domain.chat.dto.request.ChatSendRequestDto; import org.swyp.linkit.domain.chat.entity.ChatMessage; -import org.swyp.linkit.domain.chat.entity.ChatRoom; import org.swyp.linkit.domain.chat.service.ChatService; -import org.swyp.linkit.domain.notification.entity.NotificationType; -import org.swyp.linkit.domain.notification.service.NotificationService; import java.security.Principal; @@ -27,7 +24,6 @@ public class ChatStompController { private final ChatService chatService; - private final NotificationService notificationService; /** * 메시지 전송 @@ -49,11 +45,6 @@ public void send(@Payload ChatSendRequestDto dto, Principal principal) { dto.getMessageType(), dto.getImageUrl()); chatService.publishToRedis(saved); - - // 상대방에게 채팅 알림 발송 - ChatRoom room = saved.getChatRoom(); - Long receiverId = room.getMentorId().equals(senderId) ? room.getMenteeId() : room.getMentorId(); - notificationService.createNotification(receiverId, senderId, NotificationType.CHAT_MESSAGE, roomId); } /** From 720fefff123a5daf5c60b0fe575804451d6fdbf8 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Mon, 6 Apr 2026 21:43:44 +0900 Subject: [PATCH 07/16] =?UTF-8?q?fix=20:=20=20RedisConfig=EC=97=90=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=A6=AC=EC=8A=A4=EB=84=88=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/swyp/linkit/global/config/RedisConfig.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/swyp/linkit/global/config/RedisConfig.java b/src/main/java/org/swyp/linkit/global/config/RedisConfig.java index 04c3acbd..9f8e3f09 100644 --- a/src/main/java/org/swyp/linkit/global/config/RedisConfig.java +++ b/src/main/java/org/swyp/linkit/global/config/RedisConfig.java @@ -8,12 +8,14 @@ import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.swyp.linkit.domain.chat.redis.RedisChatSubscriber; +import org.swyp.linkit.domain.notification.redis.RedisNotificationSubscriber; @Profile("!test") @Configuration public class RedisConfig { private static final String CHAT_CHANNEL_PATTERN = "chat:room:*"; + private static final String NOTIFICATION_CHANNEL_PATTERN = "notification:user:*"; @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { @@ -23,11 +25,13 @@ public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connection @Bean public RedisMessageListenerContainer redisMessageListenerContainer( RedisConnectionFactory connectionFactory, - RedisChatSubscriber redisChatSubscriber) { + RedisChatSubscriber redisChatSubscriber, + RedisNotificationSubscriber redisNotificationSubscriber) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(redisChatSubscriber, new PatternTopic(CHAT_CHANNEL_PATTERN)); + container.addMessageListener(redisNotificationSubscriber, new PatternTopic(NOTIFICATION_CHANNEL_PATTERN)); return container; } } \ No newline at end of file From a3642c410bde8063b5834757d3ea502c1bbc14b1 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Mon, 6 Apr 2026 21:56:56 +0900 Subject: [PATCH 08/16] =?UTF-8?q?fix=20:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=20?= =?UTF-8?q?ChatMessage.content=20NPE=20=EB=B0=9C=EC=83=9D=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linkit/domain/chat/service/ChatRoomService.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/chat/service/ChatRoomService.java b/src/main/java/org/swyp/linkit/domain/chat/service/ChatRoomService.java index 1ba82847..0f5fcb49 100644 --- a/src/main/java/org/swyp/linkit/domain/chat/service/ChatRoomService.java +++ b/src/main/java/org/swyp/linkit/domain/chat/service/ChatRoomService.java @@ -9,6 +9,7 @@ import org.swyp.linkit.domain.chat.entity.ChatRoom; import org.swyp.linkit.domain.chat.entity.ChatRoomDelete; import org.swyp.linkit.domain.chat.entity.ChatRoomStatus; +import org.swyp.linkit.domain.chat.entity.MessageType; import org.swyp.linkit.domain.chat.repository.ChatMessageRepository; import org.swyp.linkit.domain.chat.repository.ChatRoomDeleteRepository; import org.swyp.linkit.domain.chat.repository.ChatRoomRepository; @@ -86,7 +87,7 @@ public List findRoomsByUserId(Long userId) { ChatMessage lastMessage = (ChatMessage) row[1]; // 마지막 메시지 내용 - String lastMessageContent = lastMessage != null ? lastMessage.getContent() : null; + String lastMessageContent = lastMessage != null ? resolveLastMessageContent(lastMessage) : null; // 상대방 정보 (연관관계로 바로 접근) boolean isMentor = room.getMentorId().equals(userId); @@ -120,7 +121,7 @@ public ChatRoomDto findDtoById(Long roomId, Long currentUserId) { String lastMessageContent = null; if (room.getLastMessageId() != null) { lastMessageContent = chatMessageRepository.findById(room.getLastMessageId()) - .map(ChatMessage::getContent) + .map(this::resolveLastMessageContent) .orElse(null); } @@ -180,6 +181,13 @@ public boolean isMentor(ChatRoom room, Long userId) { // === Private Helper Methods === + private String resolveLastMessageContent(ChatMessage message) { + if (message.getMessageType() == MessageType.IMAGE) { + return "[이미지]"; + } + return message.getContent(); + } + private User findUserById(Long userId) { return userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found: " + userId)); From 8c31e08330737865b7801bb124ebf28a2ec3c83c Mon Sep 17 00:00:00 2001 From: rorrxr Date: Mon, 6 Apr 2026 22:25:15 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor=20:=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EC=8B=9C=EC=8A=A4=ED=85=9C/=EC=95=8C?= =?UTF-8?q?=20=EC=88=98=20=EC=97=86=EC=9D=8C=20=EA=B5=AC=EB=B6=84=20(resol?= =?UTF-8?q?veNickname()=EC=9C=BC=EB=A1=9C=20null/blank=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EC=B2=98=EB=A6=AC,=20=EC=8B=9C=EC=8A=A4=ED=85=9C?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=EA=B3=BC=20=ED=83=88=ED=87=B4/=EB=AF=B8?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=9C=A0=EC=A0=80=20=EA=B5=AC=EB=B6=84,?= =?UTF-8?q?=20toMap=20merge=20function=20=EC=B6=94=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=82=A4=20=EC=98=88=EC=99=B8=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationServiceImpl.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java index 1a18694a..7963a0ff 100644 --- a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java @@ -69,7 +69,7 @@ public NotificationDto createNotification(Long receiverId, Long senderId, Notifi Notification savedNotification = notificationRepository.save(notification); // WebSocket 실시간 알림 발송 - String senderNickname = sender != null ? sender.getNickname() : "시스템"; + String senderNickname = resolveNickname(sender); String message = generateNotificationMessage(type, senderNickname); publishNotificationToRedis(savedNotification, senderNickname, message); @@ -154,8 +154,10 @@ public NotificationListResponseDto getNotifications(Long userId) { Map nicknameMap = senderIds.isEmpty() ? Collections.emptyMap() : userRepository.findAllById(senderIds).stream() - .collect(Collectors.toMap(User::getId, - u -> u.getNickname() != null ? u.getNickname() : "시스템")); + .collect(Collectors.toMap( + User::getId, + u -> hasValidNickname(u) ? u.getNickname() : "알 수 없음", + (existing, replacement) -> existing)); List notificationDtos = combinedNotifications.stream() .map(n -> { @@ -240,6 +242,18 @@ public int markAllAsRead(Long userId) { // ===== Private Methods ===== + /** + * sender가 null이면 "시스템", 존재하지만 닉네임이 없으면 "알 수 없음" + */ + private String resolveNickname(User sender) { + if (sender == null) return "시스템"; + return hasValidNickname(sender) ? sender.getNickname() : "알 수 없음"; + } + + private boolean hasValidNickname(User user) { + return user.getNickname() != null && !user.getNickname().isBlank(); + } + private User findUserById(Long userId) { return userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); From a0541722093e6f7ab9afc88fab02a93089c6778b Mon Sep 17 00:00:00 2001 From: rorrxr Date: Tue, 7 Apr 2026 23:30:38 +0900 Subject: [PATCH 10/16] =?UTF-8?q?fix=20:=20Redis=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B0=9B=EC=9D=80=20JSON=20=EC=97=AD=EC=A7=81=EB=A0=AC?= =?UTF-8?q?=ED=99=94=20=EC=98=A4=EB=A5=98=EB=A1=9C=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=AF=B8=EC=88=98=EC=8B=A0=20=EC=9D=B4=EC=8A=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linkit/domain/notification/dto/NotificationMessageDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationMessageDto.java b/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationMessageDto.java index 1a134104..55254614 100644 --- a/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationMessageDto.java +++ b/src/main/java/org/swyp/linkit/domain/notification/dto/NotificationMessageDto.java @@ -15,7 +15,7 @@ * WebSocket으로 전송되는 알림 메시지 DTO */ @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder(access = AccessLevel.PRIVATE) public class NotificationMessageDto { From ba2771a81bde86369bfc08e5234cbecbf79deeef Mon Sep 17 00:00:00 2001 From: rorrxr Date: Tue, 7 Apr 2026 23:31:47 +0900 Subject: [PATCH 11/16] =?UTF-8?q?fix=20:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20ObjectMapper=EB=A5=BC=20RequiredArgsConstructor?= =?UTF-8?q?=EB=A1=9C=20Spring=20Bean=20=EC=A3=BC=EC=9E=85=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/swyp/linkit/domain/chat/redis/RedisChatSubscriber.java | 2 +- .../java/org/swyp/linkit/domain/chat/service/ChatService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/chat/redis/RedisChatSubscriber.java b/src/main/java/org/swyp/linkit/domain/chat/redis/RedisChatSubscriber.java index c9467189..2721edfa 100644 --- a/src/main/java/org/swyp/linkit/domain/chat/redis/RedisChatSubscriber.java +++ b/src/main/java/org/swyp/linkit/domain/chat/redis/RedisChatSubscriber.java @@ -15,7 +15,7 @@ public class RedisChatSubscriber implements MessageListener { private final SimpMessagingTemplate messagingTemplate; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; @Override public void onMessage(Message message, byte[] pattern) { diff --git a/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java b/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java index 005fa918..8fe4506f 100644 --- a/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java +++ b/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java @@ -37,7 +37,7 @@ public class ChatService { private final UserRepository userRepository; private final StringRedisTemplate redisTemplate; private final NotificationService notificationService; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; private static final String CHAT_CHANNEL_PREFIX = "chat:room:"; From de0c2600aa0fadec50b82e5044c1aac838059a41 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Tue, 7 Apr 2026 23:32:25 +0900 Subject: [PATCH 12/16] =?UTF-8?q?fix=20:=20Redis=20publish=20=EC=8B=9C=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=EC=9D=B4=ED=9B=84=20=EC=8B=A4=ED=96=89=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=81=EC=9A=A9=20(=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=EC=9D=B4=20=EB=A1=A4=EB=B0=B1=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EC=97=90=EB=8F=84=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=B0=9C=ED=96=89=ED=95=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=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/NotificationServiceImpl.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java index 7963a0ff..50a93f1a 100644 --- a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java @@ -7,6 +7,8 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.swyp.linkit.domain.notification.dto.NotificationDto; import org.swyp.linkit.domain.notification.dto.NotificationMessageDto; import org.swyp.linkit.domain.notification.dto.response.ChatRoomUnreadCountResponseDto; @@ -260,18 +262,31 @@ private User findUserById(Long userId) { } /** - * Redis Pub/Sub을 통해 알림 발행 + * Redis Pub/Sub을 통해 알림 발행 (트랜잭션 커밋 이후 발행) */ private void publishNotificationToRedis(Notification notification, String senderNickname, String message) { NotificationMessageDto payload = NotificationMessageDto.from(notification, senderNickname, message); + String channel = NOTIFICATION_CHANNEL_PREFIX + notification.getReceiver().getId(); + + if (TransactionSynchronizationManager.isActualTransactionActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + doPublish(channel, payload, notification.getId()); + } + }); + } else { + doPublish(channel, payload, notification.getId()); + } + } + private void doPublish(String channel, NotificationMessageDto payload, Long notificationId) { try { String json = objectMapper.writeValueAsString(payload); - String channel = NOTIFICATION_CHANNEL_PREFIX + notification.getReceiver().getId(); redisTemplate.convertAndSend(channel, json); - log.info("Redis 알림 발행: channel={}, notificationId={}", channel, notification.getId()); + log.info("Redis 알림 발행: channel={}, notificationId={}", channel, notificationId); } catch (JsonProcessingException e) { - log.error("알림 직렬화 실패", e); + log.error("알림 직렬화 실패: notificationId={}", notificationId, e); } } @@ -283,7 +298,7 @@ private String generateNotificationMessage(NotificationType type, String senderN case REQUEST_RECEIVED -> senderNickname + "님이 스킬 교환을 요청했습니다."; case REQUEST_SENT -> senderNickname + "님에게 스킬 교환 요청을 보냈습니다."; case REQUEST_STATUS_CHANGED -> "스킬 교환 요청 상태가 변경되었습니다."; - case CHAT_MESSAGE -> senderNickname + "님에게 메시지 요청이 왔습니다."; + case CHAT_MESSAGE -> senderNickname + "님이 메시지를 보냈습니다."; }; } } \ No newline at end of file From e0373903cdc188a0120c9777885817d4685c7325 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Tue, 7 Apr 2026 23:42:32 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor=20:=20ErrorCode=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(N005,=20CH007)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/swyp/linkit/global/error/ErrorCode.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/swyp/linkit/global/error/ErrorCode.java b/src/main/java/org/swyp/linkit/global/error/ErrorCode.java index 5466542c..1bc6027d 100644 --- a/src/main/java/org/swyp/linkit/global/error/ErrorCode.java +++ b/src/main/java/org/swyp/linkit/global/error/ErrorCode.java @@ -135,6 +135,9 @@ public enum ErrorCode implements BaseErrorCode { @ExplainError("멘토와 멘티가 동일한 사용자인 경우 발생합니다.") CHAT_SAME_USER(HttpStatus.BAD_REQUEST, "CH006", "멘토와 멘티는 서로 다른 사용자여야 합니다."), + @ExplainError("Redis Pub/Sub을 통한 채팅 메시지 발행에 실패한 경우 발생합니다.") + CHAT_PUBLISH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "CH007", "채팅 메시지 발행에 실패했습니다."), + // 크레딧 @ExplainError("사용자의 크레딧 정보가 존재하지 않는 경우 발생합니다.") NOT_FOUND_CREDIT(HttpStatus.NOT_FOUND, "CR001", "크레딧 정보를 찾을 수 없습니다."), @@ -216,6 +219,9 @@ public enum ErrorCode implements BaseErrorCode { @ExplainError("지원하지 않는 알림 타입을 요청한 경우 발생합니다.") INVALID_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, "N004", "유효하지 않은 알림 타입입니다."), + @ExplainError("Redis Pub/Sub을 통한 알림 발행에 실패한 경우 발생합니다.") + NOTIFICATION_PUBLISH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "N005", "알림 발행에 실패했습니다."), + // 채팅 파일 업로드 @ExplainError("채팅 파일 크기가 10MB를 초과하는 경우 발생합니다.") CHAT_FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "CF001", "파일 용량은 최대 10MB까지 업로드할 수 있습니다."), From ffe4c6ac01a3968c97b1085891f33fcd77b6c46d Mon Sep 17 00:00:00 2001 From: rorrxr Date: Tue, 7 Apr 2026 23:44:20 +0900 Subject: [PATCH 14/16] =?UTF-8?q?feat=20:=20=EC=83=88=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20(N005=20:?= =?UTF-8?q?=20NotificationPublishFailedException,=20CH007=20:=20ChatPublis?= =?UTF-8?q?hFailedException)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp/linkit/domain/chat/service/ChatService.java | 11 +++++++---- .../notification/service/NotificationServiceImpl.java | 5 ++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java b/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java index 8fe4506f..4f1672b6 100644 --- a/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java +++ b/src/main/java/org/swyp/linkit/domain/chat/service/ChatService.java @@ -189,8 +189,9 @@ public void deleteMessages(Long roomId, Long userId, List messageIds) { * Redis Pub/Sub을 통해 메시지 발행 */ public void publishToRedis(ChatMessage message) { + Long roomId = message.getChatRoom().getId(); ChatPayloadResponseDto payload = ChatPayloadResponseDto.builder() - .roomId(message.getChatRoom().getId()) + .roomId(roomId) .messageId(message.getId()) .senderId(message.getSenderId()) .senderRole(message.getSenderRole().name()) @@ -203,11 +204,12 @@ public void publishToRedis(ChatMessage message) { try { String json = objectMapper.writeValueAsString(payload); - String channel = CHAT_CHANNEL_PREFIX + message.getChatRoom().getId(); + String channel = CHAT_CHANNEL_PREFIX + roomId; redisTemplate.convertAndSend(channel, json); log.info("Redis 메시지 발행: channel={}, messageId={}", channel, message.getId()); } catch (JsonProcessingException e) { - log.error("ChatPayload 직렬화 실패", e); + log.error("채팅 메시지 직렬화 실패: roomId={}, messageId={}", roomId, message.getId(), e); + throw new ChatPublishFailedException(roomId); } } @@ -228,7 +230,8 @@ public void publishReadEvent(Long roomId, Long userId, Long lastReadMessageId) { redisTemplate.convertAndSend(channel, json); log.info("읽음 이벤트 발행: channel={}, readerId={}", channel, userId); } catch (JsonProcessingException e) { - log.error("읽음 이벤트 직렬화 실패", e); + log.error("읽음 이벤트 직렬화 실패: roomId={}, userId={}", roomId, userId, e); + throw new ChatPublishFailedException(roomId); } } diff --git a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java index 50a93f1a..068d8a95 100644 --- a/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/org/swyp/linkit/domain/notification/service/NotificationServiceImpl.java @@ -22,6 +22,7 @@ import org.swyp.linkit.global.error.exception.NotificationAccessDeniedException; import org.swyp.linkit.global.error.exception.NotificationAlreadyReadException; import org.swyp.linkit.global.error.exception.NotificationNotFoundException; +import org.swyp.linkit.global.error.exception.NotificationPublishFailedException; import org.swyp.linkit.global.error.exception.UserNotFoundException; import java.time.LocalDateTime; @@ -286,7 +287,9 @@ private void doPublish(String channel, NotificationMessageDto payload, Long noti redisTemplate.convertAndSend(channel, json); log.info("Redis 알림 발행: channel={}, notificationId={}", channel, notificationId); } catch (JsonProcessingException e) { - log.error("알림 직렬화 실패: notificationId={}", notificationId, e); + // afterCommit() 내부에서는 예외를 throw해도 Spring이 억제하므로 로그로 대체 + NotificationPublishFailedException ex = new NotificationPublishFailedException(notificationId); + log.error("[{}] {}", ex.getErrorCode().getCode(), ex.getMessage(), e); } } From daf7fb9ba7fd72ec9570231c9e7ea1c3e0fd92c2 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Tue, 7 Apr 2026 23:44:47 +0900 Subject: [PATCH 15/16] =?UTF-8?q?refactor=20:=20=EC=8B=A0=EA=B7=9C=20excep?= =?UTF-8?q?tion=20Swagger=20Docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp/linkit/global/swagger/docs/ChatExceptionDocs.java | 7 +++++++ .../global/swagger/docs/NotificationExceptionDocs.java | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/org/swyp/linkit/global/swagger/docs/ChatExceptionDocs.java b/src/main/java/org/swyp/linkit/global/swagger/docs/ChatExceptionDocs.java index 75f175e8..ee577d1b 100644 --- a/src/main/java/org/swyp/linkit/global/swagger/docs/ChatExceptionDocs.java +++ b/src/main/java/org/swyp/linkit/global/swagger/docs/ChatExceptionDocs.java @@ -59,4 +59,11 @@ public BaseErrorCode getErrorCode() { return ErrorCode.CHAT_SAME_USER; } } + + public static class ChatPublishFailedException implements SwaggerExampleExceptions { + @Override + public BaseErrorCode getErrorCode() { + return ErrorCode.CHAT_PUBLISH_FAILED; + } + } } diff --git a/src/main/java/org/swyp/linkit/global/swagger/docs/NotificationExceptionDocs.java b/src/main/java/org/swyp/linkit/global/swagger/docs/NotificationExceptionDocs.java index 397782b7..63db9eb2 100644 --- a/src/main/java/org/swyp/linkit/global/swagger/docs/NotificationExceptionDocs.java +++ b/src/main/java/org/swyp/linkit/global/swagger/docs/NotificationExceptionDocs.java @@ -45,4 +45,11 @@ public BaseErrorCode getErrorCode() { return ErrorCode.INVALID_NOTIFICATION_TYPE; } } + + public static class NotificationPublishFailedException implements SwaggerExampleExceptions { + @Override + public BaseErrorCode getErrorCode() { + return ErrorCode.NOTIFICATION_PUBLISH_FAILED; + } + } } From c36084baa4c75a503a941e11de65caaf40cd9784 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Thu, 9 Apr 2026 00:37:59 +0900 Subject: [PATCH 16/16] =?UTF-8?q?fix=20:=20=EB=AF=B8=EC=BB=A4=EB=B0=8B?= =?UTF-8?q?=EB=90=9C=20=EC=98=88=EC=99=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20(NotificationPublishF?= =?UTF-8?q?ailedException,=20ChatPublishFailedException)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ChatPublishFailedException.java | 16 ++++++++++++++++ .../NotificationPublishFailedException.java | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/org/swyp/linkit/global/error/exception/ChatPublishFailedException.java create mode 100644 src/main/java/org/swyp/linkit/global/error/exception/NotificationPublishFailedException.java diff --git a/src/main/java/org/swyp/linkit/global/error/exception/ChatPublishFailedException.java b/src/main/java/org/swyp/linkit/global/error/exception/ChatPublishFailedException.java new file mode 100644 index 00000000..c5a8e993 --- /dev/null +++ b/src/main/java/org/swyp/linkit/global/error/exception/ChatPublishFailedException.java @@ -0,0 +1,16 @@ +package org.swyp.linkit.global.error.exception; + +import org.swyp.linkit.global.error.ErrorCode; +import org.swyp.linkit.global.error.exception.base.BusinessException; + +public class ChatPublishFailedException extends BusinessException { + + public ChatPublishFailedException() { + super(ErrorCode.CHAT_PUBLISH_FAILED); + } + + public ChatPublishFailedException(Long roomId) { + super(ErrorCode.CHAT_PUBLISH_FAILED, + "채팅 메시지 발행에 실패했습니다. roomId=" + roomId); + } +} diff --git a/src/main/java/org/swyp/linkit/global/error/exception/NotificationPublishFailedException.java b/src/main/java/org/swyp/linkit/global/error/exception/NotificationPublishFailedException.java new file mode 100644 index 00000000..49df93b1 --- /dev/null +++ b/src/main/java/org/swyp/linkit/global/error/exception/NotificationPublishFailedException.java @@ -0,0 +1,16 @@ +package org.swyp.linkit.global.error.exception; + +import org.swyp.linkit.global.error.ErrorCode; +import org.swyp.linkit.global.error.exception.base.BusinessException; + +public class NotificationPublishFailedException extends BusinessException { + + public NotificationPublishFailedException() { + super(ErrorCode.NOTIFICATION_PUBLISH_FAILED); + } + + public NotificationPublishFailedException(Long notificationId) { + super(ErrorCode.NOTIFICATION_PUBLISH_FAILED, + "알림 발행에 실패했습니다. notificationId=" + notificationId); + } +}