diff --git a/src/main/java/ita/tinybite/domain/chat/service/ChatService.java b/src/main/java/ita/tinybite/domain/chat/service/ChatService.java index db890a1..3caa80e 100644 --- a/src/main/java/ita/tinybite/domain/chat/service/ChatService.java +++ b/src/main/java/ita/tinybite/domain/chat/service/ChatService.java @@ -4,7 +4,9 @@ import ita.tinybite.domain.chat.dto.res.ChatMessageResDto; import ita.tinybite.domain.chat.dto.res.ChatMessageSliceResDto; import ita.tinybite.domain.chat.entity.ChatMessage; +import ita.tinybite.domain.chat.entity.ChatRoom; import ita.tinybite.domain.chat.entity.ChatRoomMember; +import ita.tinybite.domain.chat.enums.ChatRoomType; import ita.tinybite.domain.chat.enums.MessageType; import ita.tinybite.domain.chat.repository.ChatMessageRepository; import ita.tinybite.domain.chat.repository.ChatRoomRepository; @@ -57,19 +59,38 @@ public void sendNotification(ChatMessage message, Long chatRoomId) { // 4. 해당 비구독자 유저들에게 메시지가 왔다는 알림 전송 String messageContent; - if(message.getMessageType().equals(MessageType.TEXT)) { - messageContent = message.getText(); - } else if (message.getMessageType().equals(MessageType.IMAGE)) { - messageContent = message.getImageUrl(); - } else if (message.getMessageType().equals(MessageType.SYSTEM)) { - messageContent = message.getSystemMessage(); - } else { - messageContent = ""; - } + ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow(); - unsubscribers.forEach(unsubscriber -> - notificationFacade.notifyNewChatMessage(unsubscriber.getUserId(), chatRoomId, unsubscriber.getNickname(), messageContent) - ); + // 1대1일 때 + if (chatRoom.getType().equals(ChatRoomType.ONE_TO_ONE)) { + switch (message.getMessageType()) { + case TEXT -> { + messageContent = message.getText(); + unsubscribers.forEach(unsubscriber -> + notificationFacade.notifyOneToOneChat(unsubscriber.getUserId(), chatRoomId, unsubscriber.getNickname(), messageContent) + ); + } + case IMAGE -> { + unsubscribers.forEach(unsubscriber -> + notificationFacade.notifyOneToOneImage(unsubscriber.getUserId(), chatRoomId, unsubscriber.getNickname()) + ); + } + } + } else { // 그룹일 때 + switch (message.getMessageType()) { + case TEXT -> { + messageContent = message.getText(); + unsubscribers.forEach(unsubscriber -> + notificationFacade.notifyGroupChat(unsubscriber.getUserId(), chatRoomId, chatRoom.getParty().getTitle(), message.getSenderName(), messageContent) + ); + } + case IMAGE -> { + unsubscribers.forEach(unsubscriber -> + notificationFacade.notifyGroupImage(unsubscriber.getUserId(), chatRoomId, chatRoom.getParty().getTitle(), message.getSenderName()) + ); + } + } + } } public ChatMessageSliceResDto getChatMessage(Long roomId, int page, int size) { diff --git a/src/main/java/ita/tinybite/domain/notification/service/ChatNotificationService.java b/src/main/java/ita/tinybite/domain/notification/service/ChatNotificationService.java index eea149b..8044397 100644 --- a/src/main/java/ita/tinybite/domain/notification/service/ChatNotificationService.java +++ b/src/main/java/ita/tinybite/domain/notification/service/ChatNotificationService.java @@ -19,6 +19,7 @@ @RequiredArgsConstructor @Slf4j public class ChatNotificationService { + private static final int MAX_CONTENT_LENGTH = 30; private final FcmNotificationSender fcmNotificationSender; private final FcmTokenService fcmTokenService; @@ -26,7 +27,77 @@ public class ChatNotificationService { private final NotificationLogService notificationLogService; private final NotificationTransactionHelper notificationTransactionHelper; + // 1:1 채팅 일반 메시지 @Transactional + public void sendOneToOneChatMessage(Long targetUserId, Long chatRoomId, String senderName, String content) { + String title = senderName; + String detail = truncateContent(content); + send(targetUserId, chatRoomId, title, detail, senderName); + } + + // 1:1 채팅 사진 전송 + @Transactional + public void sendOneToOneChatImage(Long targetUserId, Long chatRoomId, String senderName) { + String title = senderName; + String detail = "📷 사진을 보냈어요"; + send(targetUserId, chatRoomId, title, detail, senderName); + } + + // 단체 채팅 일반 메시지 + @Transactional + public void sendGroupChatMessage(Long targetUserId, Long chatRoomId, String partyTitle, String senderName, String content) { + String title = partyTitle; + String detail = senderName + ": " + truncateContent(content); + send(targetUserId, chatRoomId, title, detail, senderName); + } + + // 단체 채팅 사진 전송 + @Transactional + public void sendGroupChatImage(Long targetUserId, Long chatRoomId, String partyTitle, String senderName) { + String title = partyTitle; + String detail = senderName + ": 📷 사진을 보냈어요"; + send(targetUserId, chatRoomId, title, detail, senderName); + } + + @Transactional + public void sendUnreadReminderNotification(Long targetUserId, Long chatRoomId) { + String title = "🔔 놓친 메시지가 있어요!"; + String detail = "안 읽은 메시지가 있어요! 지금 확인해 보세요."; + notificationLogService.saveLog(targetUserId, NotificationType.CHAT_UNREAD_REMINDER.name(), title, detail); + + List tokens = fcmTokenService.getTokensAndLogIfEmpty(targetUserId); + if (tokens.isEmpty()) { + return; + } + + NotificationMulticastRequest request = + chatMessageManager.createUnreadReminderRequest(tokens, chatRoomId, title, detail); + + BatchResponse response = fcmNotificationSender.send(request); + notificationTransactionHelper.handleBatchResponse(response, tokens); + } + + /** + * 공통 전송 로직 + */ + private void send(Long targetUserId, Long chatRoomId, String title, String detail, String senderName) { + // 알림 로그 저장 + notificationLogService.saveLog(targetUserId, NotificationType.CHAT_NEW_MESSAGE.name(), title, detail); + + // 토큰 조회 + List tokens = fcmTokenService.getTokensAndLogIfEmpty(targetUserId); + if (tokens.isEmpty()) return; + + // FCM 요청 생성 + NotificationMulticastRequest request = + chatMessageManager.createNewChatMessageRequest(tokens, chatRoomId, title, senderName, detail); + + // 발송 및 후처리 + BatchResponse response = fcmNotificationSender.send(request); + notificationTransactionHelper.handleBatchResponse(response, tokens); + } + + /*@Transactional public void sendNewChatMessage( Long targetUserId, Long chatRoomId, @@ -52,23 +123,14 @@ public void sendNewChatMessage( BatchResponse response = fcmNotificationSender.send(request); notificationTransactionHelper.handleBatchResponse(response, tokens); - } + }*/ - @Transactional - public void sendUnreadReminderNotification(Long targetUserId, Long chatRoomId) { - String title = "🔔 놓친 메시지가 있어요!"; - String detail = "안 읽은 메시지가 있어요! 지금 확인해 보세요."; - notificationLogService.saveLog(targetUserId, NotificationType.CHAT_UNREAD_REMINDER.name(), title, detail); - - List tokens = fcmTokenService.getTokensAndLogIfEmpty(targetUserId); - if (tokens.isEmpty()) { - return; + // 텍스트 30자 제한 헬퍼 메서드 + private String truncateContent(String content) { + if (content == null) return ""; + if (content.length() > MAX_CONTENT_LENGTH) { + return content.substring(0, MAX_CONTENT_LENGTH) + "..."; } - - NotificationMulticastRequest request = - chatMessageManager.createUnreadReminderRequest(tokens, chatRoomId, title, detail); - - BatchResponse response = fcmNotificationSender.send(request); - notificationTransactionHelper.handleBatchResponse(response, tokens); + return content; } } diff --git a/src/main/java/ita/tinybite/domain/notification/service/facade/NotificationFacade.java b/src/main/java/ita/tinybite/domain/notification/service/facade/NotificationFacade.java index 261ff31..0399514 100644 --- a/src/main/java/ita/tinybite/domain/notification/service/facade/NotificationFacade.java +++ b/src/main/java/ita/tinybite/domain/notification/service/facade/NotificationFacade.java @@ -55,7 +55,6 @@ public void notifyApproval(Long targetUserId, Long partyId) { partyNotificationService.sendApprovalNotification(targetUserId, party.getTitle(), partyId); } - @Transactional public void notifyRejection(Long targetUserId, Long partyId) { Party party = partyRepository.findById(partyId) .orElseThrow(() -> new BusinessException(PartyErrorCode.PARTY_NOT_FOUND)); @@ -95,7 +94,7 @@ public void notifyMemberLeave(Long managerId, Long partyId, String leaverName) { partyNotificationService.sendMemberLeaveNotification(managerId, partyId, leaverName); } - @Transactional + /* public void notifyNewChatMessage( Long targetUserId, Long chatRoomId, @@ -103,9 +102,29 @@ public void notifyNewChatMessage( String messageContent ) { chatNotificationService.sendNewChatMessage(targetUserId, chatRoomId, senderName, messageContent); + }*/ + + // 1:1 채팅 + public void notifyOneToOneChat(Long targetUserId, Long chatRoomId, String senderName, String content) { + chatNotificationService.sendOneToOneChatMessage(targetUserId, chatRoomId, senderName, content); + } + + // 1:1 사진 + public void notifyOneToOneImage(Long targetUserId, Long chatRoomId, String senderName) { + chatNotificationService.sendOneToOneChatImage(targetUserId, chatRoomId, senderName); + } + + // 단체 채팅 + public void notifyGroupChat(Long targetUserId, Long chatRoomId, String partyTitle, String senderName, String content) { + chatNotificationService.sendGroupChatMessage(targetUserId, chatRoomId, partyTitle, senderName, content); + } + + // 단체 사진 + public void notifyGroupImage(Long targetUserId, Long chatRoomId, String partyTitle, String senderName) { + chatNotificationService.sendGroupChatImage(targetUserId, chatRoomId, partyTitle, senderName); } - // 스케줄러/채팅 서비스가 호출하며, 알림 도메인은 전송만 처리 + // 스케줄러/채팅 서비스가 호출하며, 알림 도메인은 전송만 처리(보류) @Transactional public void notifyUnreadReminder(Long targetUserId, Long chatRoomId) { chatNotificationService.sendUnreadReminderNotification(targetUserId, chatRoomId); diff --git a/src/main/java/ita/tinybite/domain/party/controller/PartyController.java b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java index 33e798e..c502c5f 100644 --- a/src/main/java/ita/tinybite/domain/party/controller/PartyController.java +++ b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java @@ -29,6 +29,8 @@ import java.util.List; +import static ita.tinybite.global.response.APIResponse.*; + @Tag(name = "파티 API", description = "파티 생성, 조회, 참여 관련 API") @RestController @RequestMapping("/api/parties") @@ -65,13 +67,10 @@ public class PartyController { ) }) @PostMapping("/{partyId}/join") - public ResponseEntity joinParty( + public APIResponse joinParty( @PathVariable Long partyId, @Parameter(hidden = true) @AuthenticationPrincipal Long userId) { - - partyService.joinParty(partyId, userId); - - return ResponseEntity.ok().build(); + return success(partyService.joinParty(partyId, userId)); } @Operation(summary = "참여 승인", description = "파티장이 참여를 승인하면 단체 채팅방에 자동 입장됩니다") @@ -496,7 +495,7 @@ public APIResponse getParty( @RequestParam(defaultValue = "20") int size ) { - return APIResponse.success(partySearchService.searchParty(q, category, page, size, userLat, userLon)); + return success(partySearchService.searchParty(q, category, page, size, userLat, userLon)); } @Operation( @@ -508,7 +507,7 @@ public APIResponse getParty( ) @GetMapping("/search/log") public APIResponse> getRecentLog() { - return APIResponse.success(partySearchService.getLog()); + return success(partySearchService.getLog()); } @Operation( @@ -521,7 +520,7 @@ public APIResponse> getRecentLog() { @DeleteMapping("/search/log/{keyword}") public APIResponse deleteRecentLog(@PathVariable String keyword) { partySearchService.deleteLog(keyword); - return APIResponse.success(); + return success(); } @Operation( @@ -533,6 +532,6 @@ public APIResponse deleteRecentLog(@PathVariable String keyword) { @DeleteMapping("/search/log") public APIResponse deleteRecentLogAll() { partySearchService.deleteAllLog(); - return APIResponse.success(); + return success(); } } \ No newline at end of file diff --git a/src/main/java/ita/tinybite/domain/party/entity/Party.java b/src/main/java/ita/tinybite/domain/party/entity/Party.java index 696178f..bee0908 100644 --- a/src/main/java/ita/tinybite/domain/party/entity/Party.java +++ b/src/main/java/ita/tinybite/domain/party/entity/Party.java @@ -34,6 +34,9 @@ public class Party { @Column(length = 500) private String thumbnailImage; // 섬네일 이미지 URL + @Column(length = 500) + private String thumbnailImageDetail; // 파티상세 썸네일 이미지 URL + @Column(length = 500) private List images; // 이미지 URL diff --git a/src/main/java/ita/tinybite/domain/party/service/PartyService.java b/src/main/java/ita/tinybite/domain/party/service/PartyService.java index a7cbc5c..63b0bc0 100644 --- a/src/main/java/ita/tinybite/domain/party/service/PartyService.java +++ b/src/main/java/ita/tinybite/domain/party/service/PartyService.java @@ -36,12 +36,18 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class PartyService { - @Value("${default.image.delivery}") + @Value("${default.image.thumbnail.delivery}") private String defaultDeliveryImage; - @Value("${default.image.grocery}") + @Value("${default.image.thumbnail.grocery}") private String defaultGroceryImage; - @Value("${default.image.household}") + @Value("${default.image.thumbnail.household}") private String defaultHouseholdImage; + @Value("${default.image.detail.delivery}") + private String defaultDeliveryDetailImage; + @Value("${default.image.detail.grocery}") + private String defaultGroceryDetailImage; + @Value("${default.image.detail.household}") + private String defaultHouseholdDetailImage; private final PartyRepository partyRepository; private final UserRepository userRepository; private final LocationService locationService; @@ -73,6 +79,7 @@ public Long createParty(Long userId, PartyCreateRequest request) { .build()) .images(getImagesIfPresent(request.getImages())) .thumbnailImage(getThumbnailIfPresent(request.getImages(), request.getCategory())) + .thumbnailImageDetail(getThumbnailDetailIfPresent(request.getImages(), request.getCategory())) .link(getLinkIfValid(request.getProductLink(), request.getCategory())) .description(getDescriptionIfPresent(request.getDescription())) .currentParticipants(1) @@ -233,7 +240,7 @@ public Long joinParty(Long partyId, Long userId) { partyId ); - return saved.getId(); + return oneToOneChatRoom.getId(); } private void validateProductLink(PartyCategory category, String productLink) { @@ -672,6 +679,18 @@ private String getThumbnailIfPresent(List images, PartyCategory category }; } + private String getThumbnailDetailIfPresent(List images, PartyCategory category) { + if (images != null && !images.isEmpty()) { + return images.get(0); + } + return switch (category) { + case DELIVERY -> defaultDeliveryDetailImage; + case GROCERY -> defaultGroceryDetailImage; + case HOUSEHOLD -> defaultHouseholdDetailImage; + default -> throw new IllegalArgumentException("존재하지 않는 카테고리입니다: " + category); + }; + } + private String getLinkIfValid(String link, PartyCategory category) { if (link != null && !link.isBlank()) { validateProductLink(category, link); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 229e9d7..a1a14d6 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -42,6 +42,11 @@ fcm: default: image: - delivery: ${DEFAULT_IMAGE_DELIVERY} - grocery: ${DEFAULT_IMAGE_GROCERY} - household: ${DEFAULT_IMAGE_HOUSEHOLD} \ No newline at end of file + thumbnail: + delivery: ${DEFAULT_IMAGE_DELIVERY} + grocery: ${DEFAULT_IMAGE_GROCERY} + household: ${DEFAULT_IMAGE_HOUSEHOLD} + detail: + delivery: ${DEFAULT_DETAIL_IMAGE_DELIVERY} + grocery: ${DEFAULT_DETAIL_IMAGE_GROCERY} + household: ${DEFAULT_DETAIL_IMAGE_HOUSEHOLD}