From 94b4cd79390405df22ee9c971299a6a8a66379b6 Mon Sep 17 00:00:00 2001 From: rorrxr Date: Wed, 21 Jan 2026 23:40:23 +0900 Subject: [PATCH 1/9] =?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 2/9] =?UTF-8?q?refactor=20:=20message=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../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 3/9] =?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 4/9] =?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 5/9] =?UTF-8?q?refactor=20:=20null=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=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 6/9] =?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 7/9] =?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 8/9] =?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 9/9] =?UTF-8?q?refactor=20:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20=EB=B0=8F=20=EC=8B=9C=EC=8A=A4=ED=85=9C/=EC=95=8C=20?= =?UTF-8?q?=EC=88=98=20=EC=97=86=EC=9D=8C=20=EA=B5=AC=EB=B6=84=20(resolveN?= =?UTF-8?q?ickname()=EC=9C=BC=EB=A1=9C=20null/blank=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC,=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=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);