From 543820fe3542b7dad456d78725fceebcde1ae882 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:17:49 +0900 Subject: [PATCH 01/36] =?UTF-8?q?[chore]=20notifications=20table=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20sql=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20(#3?= =?UTF-8?q?08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migration/V250921__Add_notification_redirect_spec.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/main/resources/db/migration/V250921__Add_notification_redirect_spec.sql diff --git a/src/main/resources/db/migration/V250921__Add_notification_redirect_spec.sql b/src/main/resources/db/migration/V250921__Add_notification_redirect_spec.sql new file mode 100644 index 000000000..5a4d469ea --- /dev/null +++ b/src/main/resources/db/migration/V250921__Add_notification_redirect_spec.sql @@ -0,0 +1,3 @@ +ALTER TABLE `notifications` + ADD COLUMN `redirect_spec` TEXT NULL + COMMENT 'NotificationRedirectSpec을 json 형식의 TEXT 로 저장'; \ No newline at end of file From c9165c1d1d60ed0bbae012bf447ea8a4e8c9668b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:19:11 +0900 Subject: [PATCH 02/36] =?UTF-8?q?[feat]=20NotificationJpaEntity=20?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80=ED=95=A0=20data=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=84=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../value/NotificationRedirectSpec.java | 12 ++++++ .../NotificationRedirectSpecConverter.java | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpec.java create mode 100644 src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java diff --git a/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpec.java b/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpec.java new file mode 100644 index 000000000..0c899719a --- /dev/null +++ b/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpec.java @@ -0,0 +1,12 @@ +package konkuk.thip.notification.domain.value; + +import java.util.Map; + +public record NotificationRedirectSpec( + MessageRoute route, // FE 이동 목적지 + Map params // 목적지로 이동 시 필요한 파라미터들 +) { + public static NotificationRedirectSpec none() { + return new NotificationRedirectSpec(MessageRoute.NONE, Map.of()); + } +} diff --git a/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java b/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java new file mode 100644 index 000000000..59f203628 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java @@ -0,0 +1,38 @@ +package konkuk.thip.notification.domain.value; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import konkuk.thip.common.exception.InvalidStateException; + +import java.io.IOException; + +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED; +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED; + +@Converter +public class NotificationRedirectSpecConverter implements AttributeConverter { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(NotificationRedirectSpec attribute) { + if (attribute == null) return null; + try { + return objectMapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + throw new InvalidStateException(NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED); + } + } + + @Override + public NotificationRedirectSpec convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.isBlank()) return NotificationRedirectSpec.none(); + try { + return objectMapper.readValue(dbData, NotificationRedirectSpec.class); + } catch (IOException e) { + throw new InvalidStateException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED); + } + } +} From f51edccc4c78c341b347014d71da7e973649ba7c Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:20:25 +0900 Subject: [PATCH 03/36] =?UTF-8?q?[refactor]=20=EC=88=98=EC=A0=95=EB=90=9C?= =?UTF-8?q?=20notification=20=EA=B5=AC=EC=A1=B0=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20jpa,=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EB=B0=8F=20mapper=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/jpa/NotificationJpaEntity.java | 13 +++++++++- .../out/mapper/NotificationMapper.java | 2 ++ .../notification/domain/Notification.java | 25 ++++++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java index 3e8a1f549..d46ca8c08 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java @@ -2,7 +2,10 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; +import konkuk.thip.notification.domain.Notification; import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.notification.domain.value.NotificationRedirectSpecConverter; +import konkuk.thip.notification.domain.value.NotificationRedirectSpec; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; @@ -34,4 +37,12 @@ public class NotificationJpaEntity extends BaseJpaEntity { @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; -} \ No newline at end of file + + @Convert(converter = NotificationRedirectSpecConverter.class) + @Column(name = "redirect_spec", columnDefinition = "TEXT") // nullable + private NotificationRedirectSpec redirectSpec; + + public void updateFrom(Notification notification) { + this.isChecked = notification.isChecked(); // 현재는 isChecked만 업데이트 가능 + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java b/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java index dd65e5db3..69da81947 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java @@ -15,6 +15,7 @@ public NotificationJpaEntity toJpaEntity(Notification notification, UserJpaEntit .isChecked(notification.isChecked()) .notificationCategory(notification.getNotificationCategory()) .userJpaEntity(userJpaEntity) + .redirectSpec(notification.getRedirectSpec()) .build(); } @@ -26,6 +27,7 @@ public Notification toDomainEntity(NotificationJpaEntity notificationJpaEntity) .isChecked(notificationJpaEntity.isChecked()) .notificationCategory(notificationJpaEntity.getNotificationCategory()) .targetUserId(notificationJpaEntity.getUserJpaEntity().getUserId()) + .redirectSpec(notificationJpaEntity.getRedirectSpec()) .createdAt(notificationJpaEntity.getCreatedAt()) .modifiedAt(notificationJpaEntity.getModifiedAt()) .status(notificationJpaEntity.getStatus()) diff --git a/src/main/java/konkuk/thip/notification/domain/Notification.java b/src/main/java/konkuk/thip/notification/domain/Notification.java index 412342eaf..2622abb84 100644 --- a/src/main/java/konkuk/thip/notification/domain/Notification.java +++ b/src/main/java/konkuk/thip/notification/domain/Notification.java @@ -1,10 +1,15 @@ package konkuk.thip.notification.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.notification.domain.value.NotificationRedirectSpec; import lombok.Getter; import lombok.experimental.SuperBuilder; +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_ACCESS_FORBIDDEN; +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_ALREADY_CHECKED; + @Getter @SuperBuilder public class Notification extends BaseDomainEntity { @@ -21,13 +26,31 @@ public class Notification extends BaseDomainEntity { private Long targetUserId; - public static Notification withoutId (String title, String content, NotificationCategory notificationCategory, Long targetUserId) { + private NotificationRedirectSpec redirectSpec; + + public static Notification withoutId(String title, String content, NotificationCategory notificationCategory, Long targetUserId, + NotificationRedirectSpec redirectSpec) { return Notification.builder() .title(title) .content(content) .isChecked(false) .notificationCategory(notificationCategory) .targetUserId(targetUserId) + .redirectSpec(redirectSpec) .build(); } + + public void validateOwner(Long userId) { + if (!targetUserId.equals(userId)) { + throw new InvalidStateException(NOTIFICATION_ACCESS_FORBIDDEN); + } + } + + public void markToChecked() { + if (isChecked) { + throw new InvalidStateException(NOTIFICATION_ALREADY_CHECKED); + } + + this.isChecked = true; + } } From 75d058a9f4a89c443ea94eb8372294ca2a5167bf Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:23:17 +0900 Subject: [PATCH 04/36] =?UTF-8?q?[rename]=20=ED=94=BC=EB=93=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20=EC=B2=98=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/feed/application/service/FeedCreateService.java | 2 +- .../application/port/in/FeedNotificationOrchestrator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java index 4d61ce21d..b439dfcb5 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java @@ -71,7 +71,7 @@ private void sendNotifications(FeedCreateCommand command, Long savedFeedId) { List targetUsers = userQueryPort.getAllFollowersByUserId(command.userId()); User actorUser = userCommandPort.findById(command.userId()); for (User targetUser : targetUsers) { - feedNotificationOrchestrator.notifyFolloweeNewPost(targetUser.getId(), actorUser.getId(), actorUser.getNickname(), savedFeedId); + feedNotificationOrchestrator.notifyFolloweeNewFeed(targetUser.getId(), actorUser.getId(), actorUser.getNickname(), savedFeedId); } } diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FeedNotificationOrchestrator.java b/src/main/java/konkuk/thip/notification/application/port/in/FeedNotificationOrchestrator.java index f58a8b845..222dfc34f 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/FeedNotificationOrchestrator.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/FeedNotificationOrchestrator.java @@ -14,7 +14,7 @@ public interface FeedNotificationOrchestrator { void notifyFeedReplied(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); - void notifyFolloweeNewPost(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); + void notifyFolloweeNewFeed(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); void notifyFeedLiked(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); From 100eb32d234b0dd8f989cf34a46c5755bde2afcb Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:25:02 +0900 Subject: [PATCH 05/36] =?UTF-8?q?[refactor]=20fcm=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=9C=20?= =?UTF-8?q?=EC=8B=9C=EC=97=90=20notificationId=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=8B=A4=EB=A5=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=82=AD=EC=A0=9C=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리다이렉트를 위한 데이터들은 fcm 푸시알림에 포함시키는게 아니라, 다른 api 를 통해 응답하도록 수정 --- .../adapter/out/event/dto/FeedEvents.java | 34 ++++++-------- .../adapter/out/event/dto/RoomEvents.java | 44 ++++++++----------- .../service/EventCommandInvoker.java | 2 +- 3 files changed, 33 insertions(+), 47 deletions(-) diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/dto/FeedEvents.java b/src/main/java/konkuk/thip/message/adapter/out/event/dto/FeedEvents.java index 922721d64..a2fe8a85f 100644 --- a/src/main/java/konkuk/thip/message/adapter/out/event/dto/FeedEvents.java +++ b/src/main/java/konkuk/thip/message/adapter/out/event/dto/FeedEvents.java @@ -1,4 +1,3 @@ -// message/adapter/out/event/dto/FeedEvents.java package konkuk.thip.message.adapter.out.event.dto; import lombok.Builder; @@ -8,41 +7,36 @@ public class FeedEvents { // 누군가 나를 팔로우하는 경우 @Builder public record FollowerEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 누군가 내 피드에 댓글을 다는 경우 @Builder public record FeedCommentedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 누군가 내 댓글에 대댓글을 다는 경우 @Builder public record FeedCommentRepliedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + String title, String content, Long notificationId, + Long targetUserId) {} - // 내가 팔로우하는 사람이 새 글을 올리는 경우 + // 내가 팔로우하는 사람이 새 피드를 올리는 경우 @Builder - public record FolloweeNewPostEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + public record FolloweeNewFeedEvent( + String title, String content, Long notificationId, + Long targetUserId) {} // 내 피드가 좋아요를 받는 경우 @Builder public record FeedLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내 피드 댓글이 좋아요를 받는 경우 @Builder public record FeedCommentLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + String title, String content, Long notificationId, + Long targetUserId) {} } diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/dto/RoomEvents.java b/src/main/java/konkuk/thip/message/adapter/out/event/dto/RoomEvents.java index e96c8c098..00880f84a 100644 --- a/src/main/java/konkuk/thip/message/adapter/out/event/dto/RoomEvents.java +++ b/src/main/java/konkuk/thip/message/adapter/out/event/dto/RoomEvents.java @@ -1,4 +1,3 @@ -// message/adapter/out/event/dto/RoomEvents.java package konkuk.thip.message.adapter.out.event.dto; import lombok.Builder; @@ -9,61 +8,54 @@ public class RoomEvents { // 내 모임방 기록/투표에 댓글이 달린 경우 @Builder public record RoomPostCommentedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 참여한 모임방에 새로운 투표가 시작된 경우 @Builder public record RoomVoteStartedEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle, - Integer page, Long postId) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 참여한 모임방에 새로운 기록이 작성된 경우 @Builder public record RoomRecordCreatedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, String roomTitle, Integer page, Long postId) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 참여한 모임방이 조기 종료된 경우 (호스트가 모집 마감 버튼 누른 경우) @Builder public record RoomRecruitClosedEarlyEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 참여한 모임방 활동이 시작된 경우 (방이 시작 기간이 되어 자동으로 시작된 경우) @Builder public record RoomActivityStartedEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 방장일 때, 새로운 사용자가 모임방 참여를 한 경우 @Builder public record RoomJoinRequestedToOwnerEvent( - String title, String content, - Long ownerUserId, Long roomId, String roomTitle, - Long applicantUserId, String applicantUsername) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 참여한 모임방의 나의 댓글이 좋아요를 받는 경우 @Builder public record RoomCommentLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 참여한 모임방의 나의 기록이 좋아요를 받는 경우 @Builder public record RoomPostLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} + String title, String content, Long notificationId, + Long targetUserId) {} // 내가 참여한 모임방의 나의 댓글에 대댓글이 달린 경우 @Builder public record RoomPostCommentRepliedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} + String title, String content, Long notificationId, + Long targetUserId) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java b/src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java index c104814a3..07f1b3603 100644 --- a/src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java +++ b/src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java @@ -3,5 +3,5 @@ @FunctionalInterface public interface EventCommandInvoker { - void publish(String title, String content); + void publish(String title, String content, Long notificationId); } From c352794118dc05ac81368bf6392af453d4b52074 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:29:45 +0900 Subject: [PATCH 06/36] =?UTF-8?q?[refactor]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=20=EC=B2=98=EB=A6=AC=20executor=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 DB 저장 시에 RedirectSpec 또한 전달받아 저장하도록 수정 - 저장한 알림의 notificationId 값을 이벤트 퍼블리시 과정에 포함해야하므로, EventCommandInvoker 에게 전달하도록 수정 --- .../service/NotificationSyncExecutor.java | 26 ++++++++++++++----- .../service/NotificationSyncExecutorTest.java | 5 +++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java index e5bf17417..61ea31ae2 100644 --- a/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java @@ -5,6 +5,7 @@ import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.domain.Notification; import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.notification.domain.value.NotificationRedirectSpec; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,32 +16,45 @@ public class NotificationSyncExecutor { private final NotificationCommandPort notificationCommandPort; + /** + * NotificationRedirectSpec 미지정 시 + */ public void execute( NotificationTemplate template, T args, Long targetUserId, EventCommandInvoker invoker + ) { + execute(template, args, targetUserId, NotificationRedirectSpec.none(), invoker); + } + + public void execute( + NotificationTemplate template, + T args, + Long targetUserId, + NotificationRedirectSpec notificationRedirectSpec, + EventCommandInvoker invoker ) { String title = template.title(args); String content = template.content(args); NotificationCategory notificationCategory = template.notificationCategory(args); // 1. DB 저장 - saveNotification(title, content, notificationCategory, targetUserId); + Long notificationId = saveNotification(title, content, notificationCategory, targetUserId, notificationRedirectSpec); // 2. 이벤트 퍼블리시 try { - invoker.publish(title, content); + invoker.publish(title, content, notificationId); } catch (Exception e) { // 이벤트 발행 실패 시, DB에 저장된 알림을 롤백하지는 않음 // -> 알림 저장은 비즈니스 트랜잭션과 동일한 경계 내에서 수행되므로, 알림 저장은 유지 // -> 푸시 알림 이벤트 발행이 실패한 경우, 일단 로깅만 추가 - log.error("푸시 알림 이벤트 퍼블리시 실패 targetUserId = {}, title = {}", targetUserId, title, e); + log.error("푸시 알림 이벤트 퍼블리시 실패 targetUserId = {}, title = {}, notificationId = {}", targetUserId, title, notificationId, e); } } - private void saveNotification(String title, String content, NotificationCategory category, Long targetUserId) { - Notification notification = Notification.withoutId(title, content, category, targetUserId); - notificationCommandPort.save(notification); + private Long saveNotification(String title, String content, NotificationCategory category, Long targetUserId, NotificationRedirectSpec redirectSpec) { + Notification notification = Notification.withoutId(title, content, category, targetUserId, redirectSpec); + return notificationCommandPort.save(notification); } } diff --git a/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java index 8db11367a..4f312bce6 100644 --- a/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java @@ -20,6 +20,8 @@ class NotificationSyncExecutorTest { void execute_publish_failure_does_not_throw() { // given NotificationCommandPort commandPort = mock(NotificationCommandPort.class); + when(commandPort.save(any(Notification.class))).thenReturn(42L); // save 시 생성된 notificationId 를 리턴하도록 스텁 + NotificationSyncExecutor executor = new NotificationSyncExecutor(commandPort); // 간단한 템플릿 스텁 (title/content 고정) @@ -33,7 +35,7 @@ void execute_publish_failure_does_not_throw() { }; // publish 호출 시 강제로 예외를 던지는 invoker - EventCommandInvoker invoker = (title, content) -> { + EventCommandInvoker invoker = (title, content, notificationId) -> { throw new RuntimeException("강제 퍼블리시 실패"); }; @@ -51,5 +53,6 @@ void execute_publish_failure_does_not_throw() { assertThat(saved.getTitle()).isEqualTo("테스트제목"); assertThat(saved.getContent()).isEqualTo("테스트내용"); assertThat(saved.getTargetUserId()).isEqualTo(123L); + assertThat(saved.getNotificationCategory()).isEqualTo(NotificationCategory.FEED); } } From 22ecd8264cfeec8032d7c339121e33bbc73973b7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:31:28 +0900 Subject: [PATCH 07/36] =?UTF-8?q?[refactor]=20FeedNotificationOrchestrator?= =?UTF-8?q?SyncImpl=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림에 함께 저장해야할 redirectSpec 구성 - 이 값을 포함해서 NotificationSyncExecutor 호출하도록 수정 --- .../FeedNotificationOrchestratorSyncImpl.java | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java b/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java index 1e3db8503..294410a0d 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java +++ b/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java @@ -4,10 +4,14 @@ import konkuk.thip.message.application.port.out.FeedEventCommandPort; import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; import konkuk.thip.notification.application.service.template.feed.*; +import konkuk.thip.notification.domain.value.MessageRoute; +import konkuk.thip.notification.domain.value.NotificationRedirectSpec; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.util.Map; + @HelperService @RequiredArgsConstructor public class FeedNotificationOrchestratorSyncImpl implements FeedNotificationOrchestrator { @@ -27,12 +31,19 @@ public class FeedNotificationOrchestratorSyncImpl implements FeedNotificationOrc @Transactional(propagation = Propagation.MANDATORY) public void notifyFollowed(Long targetUserId, Long actorUserId, String actorUsername) { var args = new FollowedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.FEED_USER, + Map.of("userId", actorUserId) + ); + notificationSyncExecutor.execute( FollowedTemplate.INSTANCE, args, targetUserId, - (title, content) -> feedEventCommandPort.publishFollowEvent( - title, content, targetUserId, actorUserId, actorUsername + redirectSpec, + (title, content, notificationId) -> feedEventCommandPort.publishFollowEvent( + title, content, notificationId, targetUserId ) ); } @@ -41,12 +52,19 @@ public void notifyFollowed(Long targetUserId, Long actorUserId, String actorUser @Transactional(propagation = Propagation.MANDATORY) public void notifyFeedCommented(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { var args = new FeedCommentedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.FEED_DETAIL, + Map.of("feedId", feedId) + ); + notificationSyncExecutor.execute( FeedCommentedTemplate.INSTANCE, args, targetUserId, - (title, content) -> feedEventCommandPort.publishFeedCommentedEvent( - title, content, targetUserId, actorUserId, actorUsername, feedId + redirectSpec, + (title, content, notificationId) -> feedEventCommandPort.publishFeedCommentedEvent( + title, content, notificationId, targetUserId ) ); } @@ -55,26 +73,40 @@ public void notifyFeedCommented(Long targetUserId, Long actorUserId, String acto @Transactional(propagation = Propagation.MANDATORY) public void notifyFeedReplied(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { var args = new FeedRepliedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.FEED_DETAIL, + Map.of("feedId", feedId) + ); + notificationSyncExecutor.execute( FeedRepliedTemplate.INSTANCE, args, targetUserId, - (title, content) -> feedEventCommandPort.publishFeedRepliedEvent( - title, content, targetUserId, actorUserId, actorUsername, feedId + redirectSpec, + (title, content, notificationId) -> feedEventCommandPort.publishFeedRepliedEvent( + title, content, notificationId, targetUserId ) ); } @Override @Transactional(propagation = Propagation.MANDATORY) - public void notifyFolloweeNewPost(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { + public void notifyFolloweeNewFeed(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { var args = new FolloweeNewPostTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.FEED_DETAIL, + Map.of("feedId", feedId) + ); + notificationSyncExecutor.execute( FolloweeNewPostTemplate.INSTANCE, args, targetUserId, - (title, content) -> feedEventCommandPort.publishFolloweeNewPostEvent( - title, content, targetUserId, actorUserId, actorUsername, feedId + redirectSpec, + (title, content, notificationId) -> feedEventCommandPort.publishFolloweeNewFeedEvent( + title, content, notificationId, targetUserId ) ); } @@ -83,12 +115,19 @@ public void notifyFolloweeNewPost(Long targetUserId, Long actorUserId, String ac @Transactional(propagation = Propagation.MANDATORY) public void notifyFeedLiked(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { var args = new FeedLikedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.FEED_DETAIL, + Map.of("feedId", feedId) + ); + notificationSyncExecutor.execute( FeedLikedTemplate.INSTANCE, args, targetUserId, - (title, content) -> feedEventCommandPort.publishFeedLikedEvent( - title, content, targetUserId, actorUserId, actorUsername, feedId + redirectSpec, + (title, content, notificationId) -> feedEventCommandPort.publishFeedLikedEvent( + title, content, notificationId, targetUserId ) ); } @@ -97,12 +136,19 @@ public void notifyFeedLiked(Long targetUserId, Long actorUserId, String actorUse @Transactional(propagation = Propagation.MANDATORY) public void notifyFeedCommentLiked(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { var args = new FeedCommentLikedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.FEED_DETAIL, + Map.of("feedId", feedId) + ); + notificationSyncExecutor.execute( FeedCommentLikedTemplate.INSTANCE, args, targetUserId, - (title, content) -> feedEventCommandPort.publishFeedCommentLikedEvent( - title, content, targetUserId, actorUserId, actorUsername, feedId + redirectSpec, + (title, content, notificationId) -> feedEventCommandPort.publishFeedCommentLikedEvent( + title, content, notificationId, targetUserId ) ); } From 3480f7354a269411a4429d6ad172460bc1df8fa8 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:32:04 +0900 Subject: [PATCH 08/36] =?UTF-8?q?[refactor]=20FeedNotificationOrchestrator?= =?UTF-8?q?SyncImpl=20=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeedNotificationOrchestratorSyncImplTest.java | 6 +++--- ...dNotificationOrchestratorSyncImplUnitTest.java | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java index 6bfa02b5b..36c728f49 100644 --- a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java @@ -106,14 +106,14 @@ void notifyFeedCommented_afterCommit_listenerInvoked_andNotificationPersisted() ArgumentCaptor.forClass(FeedEvents.FeedCommentedEvent.class); verify(feedNotificationDispatchUseCase).handleFeedCommented(captor.capture()); + NotificationJpaEntity saved = notificationJpaRepository.findAll().get(0); + FeedEvents.FeedCommentedEvent event = captor.getValue(); assertThat(event).isNotNull(); assertThat(event.title()).isNotBlank(); assertThat(event.content()).contains(actorUsername); assertThat(event.targetUserId()).isEqualTo(targetUserId); - assertThat(event.actorUserId()).isEqualTo(actorUserId); - assertThat(event.actorUsername()).isEqualTo(actorUsername); - assertThat(event.feedId()).isEqualTo(feedId); + assertThat(event.notificationId()).isEqualTo(saved.getNotificationId()); // 퍼블리시되는 이벤트에는 저장된 notificationId 값이 포함됨 } @Test diff --git a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java index 3972f9803..636df7797 100644 --- a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java @@ -9,6 +9,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -35,17 +37,18 @@ void notify_feed_commented_test() { // then: NotificationSyncExecutor 가 올바르게 호출되었는지 검증 ArgumentCaptor invokerCaptor = ArgumentCaptor.forClass(EventCommandInvoker.class); verify(notificationSyncExecutor).execute( - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.eq(targetUserId), - invokerCaptor.capture() + any(), // template + any(), // args + eq(targetUserId), // targetUserId + any(), // redirectSpec + invokerCaptor.capture() // invoker ); // then: invoker 가 EventCommandPort 메서드를 올바르게 호출하는지 검증 EventCommandInvoker invoker = invokerCaptor.getValue(); - invoker.publish("title", "content"); + invoker.publish("title", "content", 123L); verify(feedEventCommandPort).publishFeedCommentedEvent( - "title", "content", targetUserId, actorUserId, actorUsername, feedId + "title", "content", 123L, targetUserId ); } } From 698c5c70cf4e70c4fa3b87e448223c4d5fe76150 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:32:49 +0900 Subject: [PATCH 09/36] =?UTF-8?q?[refactor]=20RoomNotificationOrchestrator?= =?UTF-8?q?SyncImpl=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마찬가지로 redirectSpec 구성 & 이 값을 포함해서 NotificationSyncExecutor 를 호출하도록 수정 --- .../RoomNotificationOrchestratorSyncImpl.java | 133 +++++++++++++++--- 1 file changed, 115 insertions(+), 18 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java b/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java index 9162c65d8..6792f5ba6 100644 --- a/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java +++ b/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java @@ -4,10 +4,14 @@ import konkuk.thip.message.application.port.out.RoomEventCommandPort; import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.notification.application.service.template.room.*; +import konkuk.thip.notification.domain.value.MessageRoute; +import konkuk.thip.notification.domain.value.NotificationRedirectSpec; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.util.Map; + @HelperService @RequiredArgsConstructor public class RoomNotificationOrchestratorSyncImpl implements RoomNotificationOrchestrator { @@ -28,12 +32,24 @@ public class RoomNotificationOrchestratorSyncImpl implements RoomNotificationOrc public void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { var args = new RoomPostCommentedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_POST_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", postType + ) + ); + notificationSyncExecutor.execute( RoomPostCommentedTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomPostCommentedEvent( - title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomPostCommentedEvent( + title, content, notificationId, targetUserId ) ); } @@ -42,12 +58,24 @@ public void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomVoteStarted(Long targetUserId, Long roomId, String roomTitle, Integer page, Long postId) { var args = new RoomVoteStartedTemplate.Args(roomTitle); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_VOTE_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", "VOTE" + ) + ); + notificationSyncExecutor.execute( RoomVoteStartedTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomVoteStartedEvent( - title, content, targetUserId, roomId, roomTitle, page, postId + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomVoteStartedEvent( + title, content, notificationId, targetUserId ) ); } @@ -57,12 +85,24 @@ public void notifyRoomVoteStarted(Long targetUserId, Long roomId, String roomTit public void notifyRoomRecordCreated(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, String roomTitle, Integer page, Long postId) { var args = new RoomRecordCreatedTemplate.Args(roomTitle, actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_RECORD_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", "RECORD" + ) + ); + notificationSyncExecutor.execute( RoomRecordCreatedTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomRecordCreatedEvent( - title, content, targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomRecordCreatedEvent( + title, content, notificationId, targetUserId ) ); } @@ -71,12 +111,19 @@ public void notifyRoomRecordCreated(Long targetUserId, Long actorUserId, String @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomRecruitClosedEarly(Long targetUserId, Long roomId, String roomTitle) { var args = new RoomRecruitClosedEarlyTemplate.Args(roomTitle); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_MAIN, + Map.of("roomId", roomId) + ); + notificationSyncExecutor.execute( RoomRecruitClosedEarlyTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomRecruitClosedEarlyEvent( - title, content, targetUserId, roomId, roomTitle + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomRecruitClosedEarlyEvent( + title, content, notificationId, targetUserId ) ); } @@ -85,12 +132,19 @@ public void notifyRoomRecruitClosedEarly(Long targetUserId, Long roomId, String @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomActivityStarted(Long targetUserId, Long roomId, String roomTitle) { var args = new RoomActivityStartedTemplate.Args(roomTitle); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_MAIN, + Map.of("roomId", roomId) + ); + notificationSyncExecutor.execute( RoomActivityStartedTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomActivityStartedEvent( - title, content, targetUserId, roomId, roomTitle + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomActivityStartedEvent( + title, content, notificationId, targetUserId ) ); } @@ -99,12 +153,19 @@ public void notifyRoomActivityStarted(Long targetUserId, Long roomId, String roo @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomJoinToHost(Long hostUserId, Long roomId, String roomTitle, Long actorUserId, String actorUsername) { var args = new RoomJoinToHostTemplate.Args(roomTitle, actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_DETAIL, + Map.of("roomId", roomId) + ); + notificationSyncExecutor.execute( RoomJoinToHostTemplate.INSTANCE, args, hostUserId, - (title, content) -> roomEventCommandPort.publishRoomJoinEventToHost( - title, content, hostUserId, roomId, roomTitle, actorUserId, actorUsername + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomJoinEventToHost( + title, content, notificationId, hostUserId ) ); } @@ -114,12 +175,24 @@ public void notifyRoomJoinToHost(Long hostUserId, Long roomId, String roomTitle, public void notifyRoomCommentLiked(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { var args = new RoomCommentLikedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_POST_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", postType + ) + ); + notificationSyncExecutor.execute( RoomCommentLikedTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomCommentLikedEvent( - title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomCommentLikedEvent( + title, content, notificationId, targetUserId ) ); } @@ -129,12 +202,24 @@ public void notifyRoomCommentLiked(Long targetUserId, Long actorUserId, String a public void notifyRoomPostLiked(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { var args = new RoomPostLikedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_POST_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", postType + ) + ); + notificationSyncExecutor.execute( RoomPostLikedTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomPostLikedEvent( - title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomPostLikedEvent( + title, content, notificationId, targetUserId ) ); } @@ -144,12 +229,24 @@ public void notifyRoomPostLiked(Long targetUserId, Long actorUserId, String acto public void notifyRoomPostCommentReplied(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { var args = new RoomPostCommentRepliedTemplate.Args(actorUsername); + + NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( + MessageRoute.ROOM_POST_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", postType + ) + ); + notificationSyncExecutor.execute( RoomPostCommentRepliedTemplate.INSTANCE, args, targetUserId, - (title, content) -> roomEventCommandPort.publishRoomPostCommentRepliedEvent( - title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + redirectSpec, + (title, content, notificationId) -> roomEventCommandPort.publishRoomPostCommentRepliedEvent( + title, content, notificationId, targetUserId ) ); } From cd275df963257bb90dc0cdda80d96d9d594ff318 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:33:03 +0900 Subject: [PATCH 10/36] =?UTF-8?q?[refactor]=20RoomNotificationOrchestrator?= =?UTF-8?q?SyncImpl=20=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomNotificationOrchestratorSyncImplTest.java | 9 +++------ .../RoomNotificationOrchestratorSyncImplUnitTest.java | 10 +++++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java index 65cd80564..bcdcd80e4 100644 --- a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java @@ -126,17 +126,14 @@ void roomPostCommented_afterCommit_listenerInvoked_andNotificationCommitted() { ArgumentCaptor.forClass(RoomEvents.RoomPostCommentedEvent.class); verify(roomNotificationDispatchUseCase).handleRoomPostCommented(captor.capture()); + NotificationJpaEntity saved = notificationJpaRepository.findAll().get(0); + RoomEvents.RoomPostCommentedEvent event = captor.getValue(); assertThat(event).isNotNull(); assertThat(event.title()).isNotBlank(); assertThat(event.content()).contains(actorUsername); assertThat(event.targetUserId()).isEqualTo(targetUserId); - assertThat(event.actorUserId()).isEqualTo(actorUserId); - assertThat(event.actorUsername()).isEqualTo(actorUsername); - assertThat(event.roomId()).isEqualTo(roomId); - assertThat(event.page()).isEqualTo(page); - assertThat(event.postId()).isEqualTo(postId); - assertThat(event.postType()).isEqualTo(postType); + assertThat(event.notificationId()).isEqualTo(saved.getNotificationId()); } @Test diff --git a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java index 93e15df72..c5fd1eec5 100644 --- a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java @@ -37,14 +37,18 @@ void notify_room_post_commented() { // then: NotificationSyncExecutor 가 올바르게 호출되었는지 검증 ArgumentCaptor invokerCaptor = ArgumentCaptor.forClass(EventCommandInvoker.class); verify(notificationSyncExecutor).execute( - any(), any(), eq(targetUserId), invokerCaptor.capture() + any(), // template + any(), // args + eq(targetUserId), // targetUserId + any(), // redirectSpec + invokerCaptor.capture() // invoker ); // then: invoker 가 EventCommandPort 메서드를 올바르게 호출하는지 검증 EventCommandInvoker invoker = invokerCaptor.getValue(); - invoker.publish("title", "content"); + invoker.publish("title", "content", 123L); verify(roomEventCommandPort).publishRoomPostCommentedEvent( - "title", "content", targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + "title", "content", 123L, targetUserId ); } } From 692952205f5049f4231f344afc09b53ed8d581d7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:34:18 +0900 Subject: [PATCH 11/36] =?UTF-8?q?[refactor]=20fcm=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 퍼블리시하는 이벤트의 구성 수정에 따른 기존 코드 수정 --- .../out/event/FeedEventPublisherAdapter.java | 58 ++++------- .../out/event/RoomEventPublisherAdapter.java | 97 ++++++------------- .../port/out/FeedEventCommandPort.java | 31 +++--- .../port/out/RoomEventCommandPort.java | 43 ++++---- 4 files changed, 81 insertions(+), 148 deletions(-) diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/FeedEventPublisherAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/event/FeedEventPublisherAdapter.java index f210e480b..59084d828 100644 --- a/src/main/java/konkuk/thip/message/adapter/out/event/FeedEventPublisherAdapter.java +++ b/src/main/java/konkuk/thip/message/adapter/out/event/FeedEventPublisherAdapter.java @@ -14,89 +14,73 @@ public class FeedEventPublisherAdapter implements FeedEventCommandPort { @Override public void publishFollowEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(FeedEvents.FollowerEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) .build()); } @Override public void publishFeedCommentedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(FeedEvents.FeedCommentedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .feedId(feedId) .build()); } @Override public void publishFeedRepliedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(FeedEvents.FeedCommentRepliedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .feedId(feedId) .build()); } @Override - public void publishFolloweeNewPostEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { - publisher.publishEvent(FeedEvents.FolloweeNewPostEvent.builder() + public void publishFolloweeNewFeedEvent( + String title, String content, Long notificationId, + Long targetUserId) { + publisher.publishEvent(FeedEvents.FolloweeNewFeedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .feedId(feedId) .build()); } @Override public void publishFeedLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(FeedEvents.FeedLikedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .feedId(feedId) .build()); } @Override public void publishFeedCommentLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(FeedEvents.FeedCommentLikedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .feedId(feedId) .build()); } -} \ No newline at end of file +} diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/RoomEventPublisherAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/event/RoomEventPublisherAdapter.java index 873b42131..f63d58c9e 100644 --- a/src/main/java/konkuk/thip/message/adapter/out/event/RoomEventPublisherAdapter.java +++ b/src/main/java/konkuk/thip/message/adapter/out/event/RoomEventPublisherAdapter.java @@ -14,148 +14,109 @@ public class RoomEventPublisherAdapter implements RoomEventCommandPort { @Override public void publishRoomPostCommentedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomPostCommentedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .roomId(roomId) - .page(page) - .postId(postId) - .postType(postType) .build()); } @Override public void publishRoomVoteStartedEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle, - Integer page, Long postId) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomVoteStartedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .roomId(roomId) - .roomTitle(roomTitle) - .page(page) - .postId(postId) .build()); } @Override public void publishRoomRecordCreatedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, String roomTitle, Integer page, Long postId) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomRecordCreatedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .roomId(roomId) - .roomTitle(roomTitle) - .page(page) - .postId(postId) .build()); } @Override public void publishRoomRecruitClosedEarlyEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomRecruitClosedEarlyEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .roomId(roomId) - .roomTitle(roomTitle) .build()); } @Override public void publishRoomActivityStartedEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomActivityStartedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .roomId(roomId) - .roomTitle(roomTitle) .build()); } @Override public void publishRoomJoinEventToHost( - String title, String content, - Long hostUserId, Long roomId, String roomTitle, - Long actorUserId, String actorUsername) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomJoinRequestedToOwnerEvent.builder() .title(title) .content(content) - .ownerUserId(hostUserId) - .roomId(roomId) - .roomTitle(roomTitle) - .applicantUserId(actorUserId) - .applicantUsername(actorUsername) + .notificationId(notificationId) + .targetUserId(targetUserId) .build()); } @Override public void publishRoomCommentLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomCommentLikedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .roomId(roomId) - .page(page) - .postId(postId) - .postType(postType) .build()); } @Override public void publishRoomPostLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomPostLikedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .roomId(roomId) - .page(page) - .postId(postId) - .postType(postType) .build()); } @Override public void publishRoomPostCommentRepliedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { + String title, String content, Long notificationId, + Long targetUserId) { publisher.publishEvent(RoomEvents.RoomPostCommentRepliedEvent.builder() .title(title) .content(content) + .notificationId(notificationId) .targetUserId(targetUserId) - .actorUserId(actorUserId) - .actorUsername(actorUsername) - .roomId(roomId) - .page(page) - .postId(postId) - .postType(postType) .build()); } -} \ No newline at end of file +} diff --git a/src/main/java/konkuk/thip/message/application/port/out/FeedEventCommandPort.java b/src/main/java/konkuk/thip/message/application/port/out/FeedEventCommandPort.java index aa9b0433f..0fbe78c6b 100644 --- a/src/main/java/konkuk/thip/message/application/port/out/FeedEventCommandPort.java +++ b/src/main/java/konkuk/thip/message/application/port/out/FeedEventCommandPort.java @@ -4,36 +4,31 @@ public interface FeedEventCommandPort { // 누군가 나를 팔로우하는 경우 void publishFollowEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername); + String title, String content, Long notificationId, + Long targetUserId); // 누군가 내 피드에 댓글을 다는 경우 void publishFeedCommentedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + String title, String content, Long notificationId, + Long targetUserId); // 누군가 내 댓글에 대댓글을 다는 경우 void publishFeedRepliedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + String title, String content, Long notificationId, + Long targetUserId); // 내가 팔로우하는 사람이 새 글을 올리는 경우 - void publishFolloweeNewPostEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + void publishFolloweeNewFeedEvent( + String title, String content, Long notificationId, + Long targetUserId); // 내 피드가 좋아요를 받는 경우 void publishFeedLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + String title, String content, Long notificationId, + Long targetUserId); // 내 피드 댓글이 좋아요를 받는 경우 void publishFeedCommentLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + String title, String content, Long notificationId, + Long targetUserId); } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/application/port/out/RoomEventCommandPort.java b/src/main/java/konkuk/thip/message/application/port/out/RoomEventCommandPort.java index a52c7c990..c4d6d1527 100644 --- a/src/main/java/konkuk/thip/message/application/port/out/RoomEventCommandPort.java +++ b/src/main/java/konkuk/thip/message/application/port/out/RoomEventCommandPort.java @@ -4,53 +4,46 @@ public interface RoomEventCommandPort { // 내 모임방 기록/투표에 댓글이 달린 경우 void publishRoomPostCommentedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + String title, String content, Long notificationId, + Long targetUserId); // 내가 참여한 모임방에 새로운 투표가 시작된 경우 void publishRoomVoteStartedEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle, - Integer page, Long postId); + String title, String content, Long notificationId, + Long targetUserId); // 내가 참여한 모임방에 새로운 기록이 작성된 경우 void publishRoomRecordCreatedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, String roomTitle, Integer page, Long postId); + String title, String content, Long notificationId, + Long targetUserId); // 내가 참여한 모임방이 조기 종료된 경우 (호스트가 모집 마감 버튼 누른 경우) void publishRoomRecruitClosedEarlyEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle); + String title, String content, Long notificationId, + Long targetUserId); // 내가 참여한 모임방 활동이 시작된 경우 (방이 시작 기간이 되어 자동으로 시작된 경우) void publishRoomActivityStartedEvent( - String title, String content, - Long targetUserId, Long roomId, String roomTitle); + String title, String content, Long notificationId, + Long targetUserId); // 내가 방장일 때, 새로운 사용자가 모임방 참여를 한 경우 void publishRoomJoinEventToHost( - String title, String content, - Long hostUserId, Long roomId, String roomTitle, - Long actorUserId, String actorUsername); + String title, String content, Long notificationId, + Long targetUserId); // 내가 참여한 모임방의 나의 댓글이 좋아요를 받는 경우 void publishRoomCommentLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + String title, String content, Long notificationId, + Long targetUserId); // 내가 참여한 모임방 안의 나의 기록/투표가 좋아요를 받는 경우 void publishRoomPostLikedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + String title, String content, Long notificationId, + Long targetUserId); // 내가 참여한 모임방의 나의 댓글에 대댓글이 달린 경우 void publishRoomPostCommentRepliedEvent( - String title, String content, - Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + String title, String content, Long notificationId, + Long targetUserId); } \ No newline at end of file From b8b456cc97d909dc719e94ced6021b77a94c24cb Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:35:20 +0900 Subject: [PATCH 12/36] =?UTF-8?q?[refactor]=20fcm=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=9C=84=ED=95=9C=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=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리스닝하는 이벤트의 구성 수정에 따른 기존 코드 수정 --- .../in/event/MessageFeedEventListener.java | 4 +- .../in/FeedNotificationDispatchUseCase.java | 2 +- .../FeedNotificationDispatchService.java | 81 ++----- .../RoomNotificationDispatchService.java | 216 +++--------------- 4 files changed, 61 insertions(+), 242 deletions(-) diff --git a/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java b/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java index e8e97855e..7c1c2e385 100644 --- a/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java +++ b/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java @@ -34,8 +34,8 @@ public void onFeedCommentReplied(FeedEvents.FeedCommentRepliedEvent e) { @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void onFolloweeNewPost(FeedEvents.FolloweeNewPostEvent e) { - feedUseCase.handleFolloweeNewPost(e); + public void onFolloweeNewPost(FeedEvents.FolloweeNewFeedEvent e) { + feedUseCase.handleFolloweeNewFeed(e); } @Async diff --git a/src/main/java/konkuk/thip/message/application/port/in/FeedNotificationDispatchUseCase.java b/src/main/java/konkuk/thip/message/application/port/in/FeedNotificationDispatchUseCase.java index 66eaee3e8..9a0e294ce 100644 --- a/src/main/java/konkuk/thip/message/application/port/in/FeedNotificationDispatchUseCase.java +++ b/src/main/java/konkuk/thip/message/application/port/in/FeedNotificationDispatchUseCase.java @@ -9,7 +9,7 @@ public interface FeedNotificationDispatchUseCase { void handleFeedCommentReplied(FeedEvents.FeedCommentRepliedEvent e); - void handleFolloweeNewPost(FeedEvents.FolloweeNewPostEvent e); + void handleFolloweeNewFeed(FeedEvents.FolloweeNewFeedEvent e); void handleFeedLiked(FeedEvents.FeedLikedEvent e); diff --git a/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java b/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java index 1941205dd..a029d11b3 100644 --- a/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java +++ b/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java @@ -6,7 +6,6 @@ import konkuk.thip.message.application.port.out.FirebaseMessagingPort; import konkuk.thip.message.adapter.out.event.dto.FeedEvents; import konkuk.thip.notification.domain.value.NotificationCategory; -import konkuk.thip.message.domain.MessageRoute; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; import lombok.RequiredArgsConstructor; @@ -26,96 +25,66 @@ public class FeedNotificationDispatchService implements FeedNotificationDispatch @Override public void handleFollower(final FeedEvents.FollowerEvent event) { - Notification n = buildNotification(event.title(), event.content()); - - List tokens = fcmTokenPersistencePort.findEnabledByUserId(event.targetUserId()); - - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), n, - MessageRoute.FEED_USER, - "userId", String.valueOf(event.actorUserId())); - - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } - firebasePort.sendBatch(msgs, tk, dev); + Notification n = buildFcmNotification(event.title(), event.content()); + push(event.targetUserId(), n, event.notificationId()); } @Override public void handleFeedCommented(final FeedEvents.FeedCommentedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushFeedDetail(event.targetUserId(), notification, event.feedId()); + Notification n = buildFcmNotification(event.title(), event.content()); + push(event.targetUserId(), n, event.notificationId()); } @Override public void handleFeedCommentReplied(final FeedEvents.FeedCommentRepliedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushFeedDetail(event.targetUserId(), notification, event.feedId()); + Notification n = buildFcmNotification(event.title(), event.content()); + push(event.targetUserId(), n, event.notificationId()); } @Override - public void handleFolloweeNewPost(final FeedEvents.FolloweeNewPostEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushFeedDetail(event.targetUserId(), notification, event.feedId()); + public void handleFolloweeNewFeed(final FeedEvents.FolloweeNewFeedEvent event) { + Notification n = buildFcmNotification(event.title(), event.content()); + push(event.targetUserId(), n, event.notificationId()); } @Override public void handleFeedLiked(final FeedEvents.FeedLikedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushFeedDetail(event.targetUserId(), notification, event.feedId()); + Notification n = buildFcmNotification(event.title(), event.content()); + push(event.targetUserId(), n, event.notificationId()); } @Override public void handleFeedCommentLiked(final FeedEvents.FeedCommentLikedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushFeedDetail(event.targetUserId(), notification, event.feedId()); + Notification n = buildFcmNotification(event.title(), event.content()); + push(event.targetUserId(), n, event.notificationId()); } - private void pushFeedDetail(Long userId, Notification notification, Long feedId) { + private void push(Long userId, Notification n, Long notificationId) { List tokens = fcmTokenPersistencePort.findEnabledByUserId(userId); - if (tokens.isEmpty()) return; List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); List dev = new ArrayList<>(tokens.size()); for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.FEED_DETAIL, - "feedId", String.valueOf(feedId)); + Message m = Message.builder() + .setToken(t.getFcmToken()) + .setNotification(n) + .putData("category", NotificationCategory.FEED.getDisplay()) + .putData("action", "OPEN_NOTIFICATION") // FE는 이 액션으로 알림 상세/라우팅을 BE api 요청으로 처리 + .putData("notificationId", String.valueOf(notificationId)) + .build(); msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); } + firebasePort.sendBatch(msgs, tk, dev); } - private Notification buildNotification(final String title, final String body) { + private Notification buildFcmNotification(final String title, final String body) { return Notification.builder().setTitle(title).setBody(body).build(); } - - private Message buildMessage(final String token, final Notification n, - final MessageRoute route, - final String... kv) { - Message.Builder b = Message.builder() - .setToken(token) - .setNotification(n) - .putData("category", NotificationCategory.FEED.getDisplay()) - .putData("action", "OPEN_ROUTE") - .putData("route", route.getCode()); - for (int i = 0; i + 1 < kv.length; i += 2) b.putData(kv[i], kv[i + 1]); - return b.build(); - } -} \ No newline at end of file +} diff --git a/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java b/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java index e705697a2..97abbdd4a 100644 --- a/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java +++ b/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java @@ -6,7 +6,6 @@ import konkuk.thip.message.application.port.out.FirebaseMessagingPort; import konkuk.thip.message.adapter.out.event.dto.RoomEvents; import konkuk.thip.notification.domain.value.NotificationCategory; -import konkuk.thip.message.domain.MessageRoute; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; import lombok.RequiredArgsConstructor; @@ -25,179 +24,55 @@ public class RoomNotificationDispatchService implements RoomNotificationDispatch private final FirebaseMessagingPort firebasePort; @Override - public void handleRoomPostCommented(final RoomEvents.RoomPostCommentedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_POST_DETAIL, - "roomId", String.valueOf(event.roomId()), - "page", String.valueOf(event.page()), - "type", "group", - "postId", String.valueOf(event.postId()), - "postType", String.valueOf(event.postType())); - - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } - firebasePort.sendBatch(msgs, tk, dev); + public void handleRoomPostCommented(final RoomEvents.RoomPostCommentedEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomVoteStarted(final RoomEvents.RoomVoteStartedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_VOTE_DETAIL, - "roomId", String.valueOf(event.roomId()), - "page", String.valueOf(event.page()), - "type", "group", - "postId", String.valueOf(event.postId()), - "postType", "VOTE"); - - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } - firebasePort.sendBatch(msgs, tk, dev); + public void handleRoomVoteStarted(final RoomEvents.RoomVoteStartedEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomRecordCreated(final RoomEvents.RoomRecordCreatedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_RECORD_DETAIL, - "roomId", String.valueOf(event.roomId()), - "page", String.valueOf(event.page()), - "type", "group", - "postId", String.valueOf(event.postId()), - "postType", "RECORD"); - - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } - firebasePort.sendBatch(msgs, tk, dev); + public void handleRoomRecordCreated(final RoomEvents.RoomRecordCreatedEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomRecruitClosedEarly(final RoomEvents.RoomRecruitClosedEarlyEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushRoomMain(event.targetUserId(), event.roomId(), notification); + public void handleRoomRecruitClosedEarly(final RoomEvents.RoomRecruitClosedEarlyEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomActivityStarted(final RoomEvents.RoomActivityStartedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushRoomMain(event.targetUserId(), event.roomId(), notification); + public void handleRoomActivityStarted(final RoomEvents.RoomActivityStartedEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomJoinRequestedToOwner(final RoomEvents.RoomJoinRequestedToOwnerEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - pushRoomDetail(event.ownerUserId(), event.roomId(), notification); + public void handleRoomJoinRequestedToOwner(final RoomEvents.RoomJoinRequestedToOwnerEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomCommentLiked(final RoomEvents.RoomCommentLikedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_POST_DETAIL, - "roomId", String.valueOf(event.roomId()), - "page", String.valueOf(event.page()), - "type", "group", - "postId", String.valueOf(event.postId()), - "postType", String.valueOf(event.postType())); - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } - firebasePort.sendBatch(msgs, tk, dev); + public void handleRoomCommentLiked(final RoomEvents.RoomCommentLikedEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomPostLiked(final RoomEvents.RoomPostLikedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_POST_DETAIL, - "roomId", String.valueOf(event.roomId()), - "page", String.valueOf(event.page()), - "type", "group", - "postId", String.valueOf(event.postId()), - "postType", String.valueOf(event.postType())); - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } - firebasePort.sendBatch(msgs, tk, dev); + public void handleRoomPostLiked(final RoomEvents.RoomPostLikedEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } @Override - public void handleRoomPostCommentReplied(RoomEvents.RoomPostCommentRepliedEvent event) { - Notification notification = buildNotification(event.title(), event.content()); - - List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_POST_DETAIL, - "roomId", String.valueOf(event.roomId()), - "page", String.valueOf(event.page()), - "type", "group", - "postId", String.valueOf(event.postId()), - "postType", String.valueOf(event.postType())); - - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } - firebasePort.sendBatch(msgs, tk, dev); + public void handleRoomPostCommentReplied(final RoomEvents.RoomPostCommentRepliedEvent e) { + push(e.targetUserId(), e.title(), e.content(), e.notificationId()); } // ===== helpers ===== + private void push(Long userId, String title, String content, Long notificationId) { + Notification notification = buildNotification(title, content); - private void pushRoomMain(Long targetUserId, Long roomId, Notification notification) { - List tokens = fcmTokenQueryPort.findEnabledByUserId(targetUserId); - + List tokens = fcmTokenQueryPort.findEnabledByUserId(userId); if (tokens.isEmpty()) return; List msgs = new ArrayList<>(tokens.size()); @@ -205,48 +80,23 @@ private void pushRoomMain(Long targetUserId, Long roomId, Notification notificat List dev = new ArrayList<>(tokens.size()); for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_MAIN, - "roomId", String.valueOf(roomId)); - + Message m = Message.builder() + .setToken(t.getFcmToken()) + .setNotification(notification) + .putData("category", NotificationCategory.ROOM.getDisplay()) + .putData("action", "OPEN_NOTIFICATION") // FE는 이 액션으로 알림 상세/라우팅을 BE api 요청으로 처리 + .putData("notificationId", String.valueOf(notificationId)) + .build(); msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); } - firebasePort.sendBatch(msgs, tk, dev); - } - - private void pushRoomDetail(Long targetUserId, Long roomId, Notification notification) { - List tokens = fcmTokenQueryPort.findEnabledByUserId(targetUserId); - - if (tokens.isEmpty()) return; - - List msgs = new ArrayList<>(tokens.size()); - List tk = new ArrayList<>(tokens.size()); - List dev = new ArrayList<>(tokens.size()); - for (FcmToken t : tokens) { - Message m = buildMessage(t.getFcmToken(), notification, - MessageRoute.ROOM_DETAIL, - "roomId", String.valueOf(roomId)); - - msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); - } firebasePort.sendBatch(msgs, tk, dev); } private Notification buildNotification(final String title, final String body) { - return Notification.builder().setTitle(title).setBody(body).build(); - } - - private Message buildMessage(final String token, final Notification n, - final MessageRoute route, - final String... kv) { - var b = Message.builder() - .setToken(token) - .setNotification(n) - .putData("category", NotificationCategory.ROOM.getDisplay()) - .putData("action", "OPEN_ROUTE") - .putData("route", route.getCode()); - for (int i = 0; i + 1 < kv.length; i += 2) b.putData(kv[i], kv[i + 1]); - return b.build(); + return Notification.builder() + .setTitle(title) + .setBody(body) + .build(); } -} \ No newline at end of file +} From 9b22d5a993d92079eee1e7bd86554eb070fadc68 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:35:48 +0900 Subject: [PATCH 13/36] =?UTF-8?q?[refactor]=20fcm=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=9C=84=ED=95=9C=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=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/event/MessageFeedEventListenerTest.java | 8 ++++++-- .../adapter/in/event/MessageRoomEventListenerTest.java | 8 +++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java b/src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java index bbd7ee37e..f9bd8e039 100644 --- a/src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java +++ b/src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java @@ -33,7 +33,11 @@ class MessageFeedEventListenerTest { void follower_isHandled_afterCommit() { // given var e = FeedEvents.FollowerEvent.builder() - .targetUserId(1L).actorUserId(2L).actorUsername("bob").build(); + .title("title") + .content("content") + .notificationId(1L) + .targetUserId(1L) + .build(); // when: 트랜잭션 안에서 이벤트 발행 publisher.publishEvent(e); @@ -45,4 +49,4 @@ void follower_isHandled_afterCommit() { // then verify(feedUseCase, times(1)).handleFollower(Mockito.eq(e)); } -} \ No newline at end of file +} diff --git a/src/test/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListenerTest.java b/src/test/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListenerTest.java index 25d89a434..b980fe5ec 100644 --- a/src/test/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListenerTest.java +++ b/src/test/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListenerTest.java @@ -31,8 +31,10 @@ class MessageRoomEventListenerTest { @DisplayName("RoomPostCommentedEvent 발행 → 커밋 시 이벤트 리스너가 useCase.handleRoomPostCommented 호출") void roomPostCommented_isHandled_afterCommit() { var e = RoomEvents.RoomPostCommentedEvent.builder() - .targetUserId(10L).actorUserId(20L).actorUsername("alice") - .roomId(100L).page(12).postId(999L).postType("RECORD") + .title("title") + .content("content") + .notificationId(1L) + .targetUserId(10L) .build(); publisher.publishEvent(e); @@ -42,4 +44,4 @@ void roomPostCommented_isHandled_afterCommit() { verify(roomUseCase, times(1)).handleRoomPostCommented(Mockito.eq(e)); } -} \ No newline at end of file +} From d957988cef13278bb11ef62237526a2d2a4aec2b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:37:22 +0900 Subject: [PATCH 14/36] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20controller=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/NotificationCommandController.java | 16 ++++++++++++++++ .../NotificationMarkToCheckedRequest.java | 12 ++++++++++++ .../NotificationMarkToCheckedResponse.java | 11 +++++++++++ 3 files changed, 39 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/adapter/in/web/request/NotificationMarkToCheckedRequest.java create mode 100644 src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java index f9f572237..30eaa2f7d 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java @@ -10,7 +10,10 @@ import konkuk.thip.notification.adapter.in.web.request.FcmTokenDeleteRequest; import konkuk.thip.notification.adapter.in.web.request.FcmTokenEnableStateChangeRequest; import konkuk.thip.notification.adapter.in.web.request.FcmTokenRegisterRequest; +import konkuk.thip.notification.adapter.in.web.request.NotificationMarkToCheckedRequest; import konkuk.thip.notification.adapter.in.web.response.FcmTokenEnableStateChangeResponse; +import konkuk.thip.notification.adapter.in.web.response.NotificationMarkToCheckedResponse; +import konkuk.thip.notification.application.port.in.NotificationMarkUseCase; import konkuk.thip.notification.application.port.in.fcm.FcmDeleteUseCase; import konkuk.thip.notification.application.port.in.fcm.FcmEnableStateChangeUseCase; import konkuk.thip.notification.application.port.in.fcm.FcmRegisterUseCase; @@ -27,6 +30,7 @@ public class NotificationCommandController { private final FcmRegisterUseCase fcmRegisterUseCase; private final FcmEnableStateChangeUseCase fcmEnableStateChangeUseCase; private final FcmDeleteUseCase fcmDeleteUseCase; + private final NotificationMarkUseCase notificationMarkUseCase; @Operation(summary = "FCM 토큰 등록", description = "사용자의 FCM 토큰을 서버에 등록합니다. 기존 토큰이 있다면 deviceId 기준으로 토큰을 갱신합니다.") @PostMapping("/notifications/fcm-tokens") @@ -60,4 +64,16 @@ public BaseResponse deleteFcmToken( fcmDeleteUseCase.deleteToken(request.toCommand(userId)); return BaseResponse.ok(null); } + + @Operation( + summary = "유저의 특정 알림 읽음 처리", + description = "유저가 특정 알림을 읽음 처리합니다. 읽음 처리 후, 해당 알림의 페이지로 리다이렉트를 위한 데이터를 응답합니다." + ) + @ExceptionDescription(NOTIFICATION_MARK_TO_CHECKED) + @PostMapping("/notifications/check") + public BaseResponse markNotificationToChecked( + @RequestBody @Valid NotificationMarkToCheckedRequest request, + @Parameter(hidden = true) @UserId final Long userId) { + return BaseResponse.ok(notificationMarkUseCase.markToChecked(request.notificationId(), userId)); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/NotificationMarkToCheckedRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/NotificationMarkToCheckedRequest.java new file mode 100644 index 000000000..8b7ad1fe2 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/NotificationMarkToCheckedRequest.java @@ -0,0 +1,12 @@ +package konkuk.thip.notification.adapter.in.web.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "알림 읽음 처리 요청 DTO") +public record NotificationMarkToCheckedRequest( + + @NotNull + @Schema(description = "읽음 처리할 알림 ID", example = "1") + Long notificationId +) { } diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java new file mode 100644 index 000000000..85cf6f546 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java @@ -0,0 +1,11 @@ +package konkuk.thip.notification.adapter.in.web.response; + +import konkuk.thip.notification.domain.value.MessageRoute; + +import java.util.Map; + +public record NotificationMarkToCheckedResponse( + MessageRoute route, + Map params +) { +} From b04aebb908774aa7070764975de8592aaf52a2f3 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:37:44 +0900 Subject: [PATCH 15/36] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20use=20case=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/NotificationMarkUseCase.java | 8 +++++ .../service/NotificationMarkService.java | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java diff --git a/src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java new file mode 100644 index 000000000..62abd35d1 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.notification.application.port.in; + +import konkuk.thip.notification.adapter.in.web.response.NotificationMarkToCheckedResponse; + +public interface NotificationMarkUseCase { + + NotificationMarkToCheckedResponse markToChecked(Long notificationId, Long userId); +} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java new file mode 100644 index 000000000..e4d523861 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java @@ -0,0 +1,34 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.notification.adapter.in.web.response.NotificationMarkToCheckedResponse; +import konkuk.thip.notification.application.port.in.NotificationMarkUseCase; +import konkuk.thip.notification.application.port.out.NotificationCommandPort; +import konkuk.thip.notification.domain.Notification; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class NotificationMarkService implements NotificationMarkUseCase { + + private final NotificationCommandPort notificationCommandPort; + + @Override + @Transactional + public NotificationMarkToCheckedResponse markToChecked(Long notificationId, Long userId) { + // 1. 알림 존재 여부 확인 + Notification notification = notificationCommandPort.getByIdOrThrow(notificationId); + notification.validateOwner(userId); + + // 2. 알림 읽음 처리 + notification.markToChecked(); + notificationCommandPort.update(notification); + + // 3. 읽음 처리된 알림의 redirectSpec 반환 (for FE 알림 리다이렉트 동작) + return new NotificationMarkToCheckedResponse( + notification.getRedirectSpec().route(), + notification.getRedirectSpec().params() + ); + } +} From 7359995a122f86c648c8f0ec6106adb2997d8287 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:38:17 +0900 Subject: [PATCH 16/36] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20=EC=98=81=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...NotificationCommandPersistenceAdapter.java | 22 +++++++++++++++++-- .../port/out/NotificationCommandPort.java | 17 ++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationCommandPersistenceAdapter.java index 0227e28d9..5099912cd 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationCommandPersistenceAdapter.java @@ -2,6 +2,7 @@ import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; import konkuk.thip.notification.adapter.out.mapper.NotificationMapper; import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; import konkuk.thip.notification.application.port.out.NotificationCommandPort; @@ -11,6 +12,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository @RequiredArgsConstructor public class NotificationCommandPersistenceAdapter implements NotificationCommandPort { @@ -21,11 +24,26 @@ public class NotificationCommandPersistenceAdapter implements NotificationComman private final NotificationMapper notificationMapper; @Override - public void save(Notification notification) { + public Long save(Notification notification) { UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(notification.getTargetUserId()).orElseThrow( () -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND) ); - notificationJpaRepository.save(notificationMapper.toJpaEntity(notification, userJpaEntity)); + return notificationJpaRepository.save(notificationMapper.toJpaEntity(notification, userJpaEntity)).getNotificationId(); + } + + @Override + public Optional findById(Long id) { + return notificationJpaRepository.findById(id) + .map(notificationMapper::toDomainEntity); + } + + @Override + public void update(Notification notification) { + NotificationJpaEntity notificationJpaEntity = notificationJpaRepository.findById(notification.getId()).orElseThrow( + () -> new EntityNotFoundException(ErrorCode.NOTIFICATION_NOT_FOUND) + ); + + notificationJpaEntity.updateFrom(notification); } } diff --git a/src/main/java/konkuk/thip/notification/application/port/out/NotificationCommandPort.java b/src/main/java/konkuk/thip/notification/application/port/out/NotificationCommandPort.java index e9a122db3..f256b3329 100644 --- a/src/main/java/konkuk/thip/notification/application/port/out/NotificationCommandPort.java +++ b/src/main/java/konkuk/thip/notification/application/port/out/NotificationCommandPort.java @@ -1,9 +1,22 @@ package konkuk.thip.notification.application.port.out; - +import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.notification.domain.Notification; +import java.util.Optional; + +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_NOT_FOUND; + public interface NotificationCommandPort { - void save(Notification notification); + Long save(Notification notification); + + Optional findById(Long id); + + default Notification getByIdOrThrow(Long id) { + return findById(id) + .orElseThrow(() -> new EntityNotFoundException(NOTIFICATION_NOT_FOUND)); + } + + void update(Notification notification); } From dc5c3b1be47833be6d9afa62a2ec65686ec7210a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:39:42 +0900 Subject: [PATCH 17/36] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20error=20code=20=EC=B6=94=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/common/exception/code/ErrorCode.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index e9d19073d..fd4f22393 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -229,6 +229,17 @@ public enum ErrorCode implements ResponseCode { * 205000 : notification error */ INVALID_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, 205000, "유효하지 않은 알림 타입입니다."), + NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 205001, "알림 리다이렉트 데이터 직렬화에 실패했습니다."), + NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 205002, "알림 리다이렉트 데이터 역직렬화에 실패했습니다."), + NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, 205003, "존재하지 않는 NOTIFICATION 입니다."), + NOTIFICATION_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 205004, "알림 접근 권한이 없습니다."), + NOTIFICATION_ALREADY_CHECKED(HttpStatus.BAD_REQUEST, 205005, "이미 읽음 처리된 알림입니다."), + + + /** + * 300000 : util error + */ + INVALID_FE_PLATFORM(HttpStatus.BAD_REQUEST, 300000, "유효하지 않은 FE 플랫폼입니다."), ; From d81ed044a9745f5c542aff125e9df003ca568c2a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:40:22 +0900 Subject: [PATCH 18/36] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20ResponseDescription=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/swagger/SwaggerResponseDescription.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 81c2e1f04..37ac64015 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -358,7 +358,8 @@ public enum SwaggerResponseDescription { // Notiification FCM_TOKEN_REGISTER(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, - FCM_TOKEN_NOT_FOUND + FCM_TOKEN_NOT_FOUND, + INVALID_FE_PLATFORM ))), FCM_TOKEN_ENABLE_STATE_CHANGE(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, @@ -376,6 +377,11 @@ public enum SwaggerResponseDescription { NOTIFICATION_SHOW(new LinkedHashSet<>(Set.of( INVALID_NOTIFICATION_TYPE ))), + NOTIFICATION_MARK_TO_CHECKED(new LinkedHashSet<>(Set.of( + NOTIFICATION_NOT_FOUND, + NOTIFICATION_ACCESS_FORBIDDEN, + NOTIFICATION_ALREADY_CHECKED + ))), ; private final Set errorCodeList; From 792edd2481990d5747b010aacda5c684ff35353a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:40:44 +0900 Subject: [PATCH 19/36] =?UTF-8?q?[test]=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/util/TestEntityFactory.java | 24 +++- .../web/NotificationMarkToCheckedApiTest.java | 123 ++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 86a2f75f1..4490cf101 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -10,7 +10,9 @@ import konkuk.thip.feed.domain.value.TagList; import konkuk.thip.feed.domain.value.ContentList; import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; +import konkuk.thip.notification.domain.value.MessageRoute; import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.notification.domain.value.NotificationRedirectSpec; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; import konkuk.thip.post.domain.PostType; @@ -28,9 +30,7 @@ import konkuk.thip.user.domain.value.Alias; import java.time.LocalDate; -import java.util.Collections; -import java.util.List; -import java.util.UUID; +import java.util.*; public class TestEntityFactory { @@ -382,4 +382,22 @@ public static NotificationJpaEntity createNotification(UserJpaEntity user, Strin .userJpaEntity(user) .build(); } + + /** + * redirectSpec 데이터도 함께 저장하는 팩토리 메서드 + */ + public static NotificationJpaEntity createNotification(UserJpaEntity user, String title, NotificationCategory category, NotificationRedirectSpec redirectSpec) { + return NotificationJpaEntity.builder() + .title(title) + .content("알림 내용") + .isChecked(false) + .notificationCategory(category) + .userJpaEntity(user) + .redirectSpec(redirectSpec) + .build(); + } + + public static NotificationRedirectSpec createNotificationRedirectSpec(MessageRoute route, Map params) { + return new NotificationRedirectSpec(route, params); + } } diff --git a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java new file mode 100644 index 000000000..65ddfcb83 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java @@ -0,0 +1,123 @@ +package konkuk.thip.notification.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; +import konkuk.thip.notification.domain.value.MessageRoute; +import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.notification.domain.value.NotificationRedirectSpec; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_ALREADY_CHECKED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 알림 읽음 처리 api 통합 테스트") +class NotificationMarkToCheckedApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; + + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; + @Autowired private EntityManager em; + + @Test + @DisplayName("본인의 알림을 읽음 처리할 경우, 해당 알림의 isChecked가 true로 변경되고, 알림의 리다이렉트를 위한 데이터가 반환된다.") + void mark_notification_to_checked_success() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationRedirectSpec redirectSpec = TestEntityFactory.createNotificationRedirectSpec( + MessageRoute.FEED_USER, Map.of("userId", 123L) // 특정 유저의 피드 페이지로 이동 + ); + + NotificationJpaEntity notificationJpaEntity = notificationJpaRepository.save( + TestEntityFactory.createNotification(user, "피드알림", NotificationCategory.FEED, redirectSpec)); + + // when & then + String body = objectMapper.writeValueAsString(Map.of("notificationId", notificationJpaEntity.getNotificationId())); + mockMvc.perform(post("/notifications/check") + .contentType(APPLICATION_JSON) + .content(body) + .requestAttr("userId", user.getUserId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.route").value(MessageRoute.FEED_USER.name())) + .andExpect(jsonPath("$.data.params.userId").value(123)); + + // DB 반영 확인 + NotificationJpaEntity reloaded = notificationJpaRepository.findById(notificationJpaEntity.getNotificationId()).orElseThrow(); + assertThat(reloaded.isChecked()).isTrue(); + } + + @Test + @DisplayName("다른 사용자의 알림을 읽음 처리하려고 하면, 403(FORBIDDEN) 에러를 반환한다") + void mark_notification_to_checked_forbidden_when_not_owner() throws Exception { + // given: 알림 주인(owner)과 다른 사용자(stranger) + UserJpaEntity owner = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + UserJpaEntity stranger = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity notification = notificationJpaRepository.save( + TestEntityFactory.createNotification(owner, "남의 알림", NotificationCategory.FEED)); + + // when & then: 남의 알림을 stranger가 읽음 처리 시도 → 403 + String body = objectMapper.writeValueAsString(Map.of("notificationId", notification.getNotificationId())); + mockMvc.perform(post("/notifications/check") + .contentType(APPLICATION_JSON) + .content(body) + .requestAttr("userId", stranger.getUserId())) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("이미 읽음 처리된 알림을 다시 읽음 처리하면, 400 에러를 반환한다.") + void mark_notification_to_checked_already_checked() throws Exception { + // given: owner의 알림을 미리 is_checked=true 상태로 만들어 둔다 + UserJpaEntity owner = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity notification = notificationJpaRepository.save( + TestEntityFactory.createNotification(owner, "이미 읽은 알림", NotificationCategory.FEED)); + + // is_checked=true 로 강제 세팅 + jdbcTemplate.update( + "UPDATE notifications SET is_checked = TRUE WHERE notification_id = ?", + notification.getNotificationId() + ); + em.flush(); + em.clear(); // 영속성 컨텍스트 초기화(DB에 직접 반영한 엔티티 변경사항을 반영하기 위해) + + // when & then: 다시 읽음 처리 시도 → 400 + String body = objectMapper.writeValueAsString(Map.of("notificationId", notification.getNotificationId())); + mockMvc.perform(post("/notifications/check") + .contentType(APPLICATION_JSON) + .content(body) + .requestAttr("userId", owner.getUserId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(NOTIFICATION_ALREADY_CHECKED.getCode())) + .andExpect(jsonPath("$.message", containsString(NOTIFICATION_ALREADY_CHECKED.getMessage()))); + } +} From c1ef1b6fc62a9a7e6ad39473085f561e04d44413 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:41:00 +0900 Subject: [PATCH 20/36] =?UTF-8?q?[test]=20Notification=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/domain/NotificationTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/domain/NotificationTest.java diff --git a/src/test/java/konkuk/thip/notification/domain/NotificationTest.java b/src/test/java/konkuk/thip/notification/domain/NotificationTest.java new file mode 100644 index 000000000..2a9c84044 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/domain/NotificationTest.java @@ -0,0 +1,53 @@ +package konkuk.thip.notification.domain; + +import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.notification.domain.value.NotificationCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_ACCESS_FORBIDDEN; +import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_ALREADY_CHECKED; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("[단위] Notification 단위 테스트") +class NotificationTest { + + @Test + @DisplayName("validateOwner(): 알림의 소유자가 아니면 InvalidStateException 을 던진다.") + void validate_owner_other_user() throws Exception { + //given + Long ownerId = 1L; + Long otherId = 2L; + Notification notification = createNotification(ownerId); + + //when //then + assertThatThrownBy(() -> notification.validateOwner(otherId)) + .isInstanceOf(InvalidStateException.class) + .hasMessage(NOTIFICATION_ACCESS_FORBIDDEN.getMessage()); + + } + + @Test + @DisplayName("markToChecked(): 이미 읽음 처리된 알림에 대해 다시 읽음 처리하려고 하면, InvalidStateException 을 던진다.") + void mark_to_checked_already_checked() throws Exception { + //given + Notification notification = createNotification(1L); + notification.markToChecked(); // 이미 읽음 처리 + + //when //then + assertThatThrownBy(notification::markToChecked) + .isInstanceOf(InvalidStateException.class) + .hasMessage(NOTIFICATION_ALREADY_CHECKED.getMessage()); + } + + private Notification createNotification(Long targetUserId) { + return Notification.builder() + .title("title") + .content("content") + .isChecked(false) + .notificationCategory(NotificationCategory.FEED) + .targetUserId(targetUserId) + .redirectSpec(null) + .build(); + } +} From 91f312adab561133033150e4c67b46f7c0d7c233 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:42:26 +0900 Subject: [PATCH 21/36] =?UTF-8?q?[move]=20PlatformType=20enum=20=EC=97=90?= =?UTF-8?q?=20=EC=A0=95=EC=A0=81=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notifications 패키지 -> common/util 패키지로 이동 --- .../konkuk/thip/common/util/PlatformType.java | 25 +++++++++++++++++++ .../domain/value/PlatformType.java | 5 ---- 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 src/main/java/konkuk/thip/common/util/PlatformType.java delete mode 100644 src/main/java/konkuk/thip/notification/domain/value/PlatformType.java diff --git a/src/main/java/konkuk/thip/common/util/PlatformType.java b/src/main/java/konkuk/thip/common/util/PlatformType.java new file mode 100644 index 000000000..3adbc6190 --- /dev/null +++ b/src/main/java/konkuk/thip/common/util/PlatformType.java @@ -0,0 +1,25 @@ +package konkuk.thip.common.util; + +import konkuk.thip.common.exception.InvalidStateException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import static konkuk.thip.common.exception.code.ErrorCode.INVALID_FE_PLATFORM; + +@Getter +@RequiredArgsConstructor +public enum PlatformType { + ANDROID("ANDROID"), + WEB("WEB"); + + private final String value; + + public static PlatformType from(String value) { + for (PlatformType type : PlatformType.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new InvalidStateException(INVALID_FE_PLATFORM); + } +} diff --git a/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java b/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java deleted file mode 100644 index 5ae99334f..000000000 --- a/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java +++ /dev/null @@ -1,5 +0,0 @@ -package konkuk.thip.notification.domain.value; - -public enum PlatformType { - ANDROID, WEB -} From af90d6b6791d7ad018c5999a7de1f82fbc3491d1 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:43:53 +0900 Subject: [PATCH 22/36] =?UTF-8?q?[refactor]=20=EA=B8=B0=EC=A1=B4=20fcm=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=93=B1=EB=A1=9D=20api=20=EC=9D=98=20req?= =?UTF-8?q?uest=20body=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EA=B0=95=ED=99=94=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlatformType 의 정적 팩토리 메서드를 활용하여 지원하는 FE 플랫폼인지 확인하도록 코드 수정 --- .../adapter/in/web/request/FcmTokenRegisterRequest.java | 6 +++--- .../application/port/in/dto/FcmTokenRegisterCommand.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java index 69b516421..4c11a9886 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java @@ -3,8 +3,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import konkuk.thip.notification.domain.value.PlatformType; import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; +import konkuk.thip.common.util.PlatformType; @Schema(description = "FCM 토큰 등록 요청 DTO") public record FcmTokenRegisterRequest( @@ -18,13 +18,13 @@ public record FcmTokenRegisterRequest( @NotNull @Schema(description = "플랫폼 타입 (ANDROID 또는 WEB)", example = "ANDROID") - PlatformType platformType + String platformType ) { public FcmTokenRegisterCommand toCommand(Long userId) { return FcmTokenRegisterCommand.builder() .deviceId(this.deviceId) .fcmToken(this.fcmToken) - .platformType(this.platformType) + .platformType(PlatformType.from(this.platformType)) .userId(userId) .build(); } diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java index d25e8be28..0ee73d6d5 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java @@ -1,6 +1,6 @@ package konkuk.thip.notification.application.port.in.dto; -import konkuk.thip.notification.domain.value.PlatformType; +import konkuk.thip.common.util.PlatformType; import lombok.Builder; @Builder From 4c2778a4ad4d779fa3002e470bca3a12d3128914 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:44:20 +0900 Subject: [PATCH 23/36] =?UTF-8?q?[refactor]=20PlatformType=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20import=20=EB=AC=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java | 2 +- src/main/java/konkuk/thip/notification/domain/FcmToken.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java b/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java index 25ea5fc29..b67b963eb 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java @@ -3,7 +3,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; import konkuk.thip.notification.domain.FcmToken; -import konkuk.thip.notification.domain.value.PlatformType; +import konkuk.thip.common.util.PlatformType; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; diff --git a/src/main/java/konkuk/thip/notification/domain/FcmToken.java b/src/main/java/konkuk/thip/notification/domain/FcmToken.java index 58bc299db..1aceac41b 100644 --- a/src/main/java/konkuk/thip/notification/domain/FcmToken.java +++ b/src/main/java/konkuk/thip/notification/domain/FcmToken.java @@ -3,7 +3,7 @@ import konkuk.thip.common.entity.BaseDomainEntity; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.exception.code.ErrorCode; -import konkuk.thip.notification.domain.value.PlatformType; +import konkuk.thip.common.util.PlatformType; import lombok.Getter; import lombok.experimental.SuperBuilder; From 1f061a3eec5ef7a121fe13118d630e3e946baa90 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 22 Sep 2025 17:45:15 +0900 Subject: [PATCH 24/36] =?UTF-8?q?[refactor]=20MessageRoute=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 필요없는 String code value 삭제 - message/domain 패키지 -> notification/domain/value 패키지로 이동 --- .../thip/message/domain/MessageRoute.java | 21 ------------------ .../domain/value/MessageRoute.java | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 src/main/java/konkuk/thip/message/domain/MessageRoute.java create mode 100644 src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java diff --git a/src/main/java/konkuk/thip/message/domain/MessageRoute.java b/src/main/java/konkuk/thip/message/domain/MessageRoute.java deleted file mode 100644 index d664ecc8f..000000000 --- a/src/main/java/konkuk/thip/message/domain/MessageRoute.java +++ /dev/null @@ -1,21 +0,0 @@ -package konkuk.thip.message.domain; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum MessageRoute { - // FEED - FEED_USER("FEED_USER"), // 자신을 팔로우한 사용자의 피드 목록으로 화면 이동 - FEED_DETAIL("FEED_DETAIL"), // 특정 피드 상세 화면으로 이동 - - // ROOM - ROOM_MAIN("ROOM_MAIN"), // 특정 모임방 메인 화면으로 이동 - ROOM_DETAIL("ROOM_DETAIL"), // 특정 모임 상세정보 화면으로 이동 - ROOM_POST_DETAIL("ROOM_POST_DETAIL"), // 특정 모임 게시글 상세 화면으로 이동 -> PostType으로 투표인지 기록인지 판단 - ROOM_RECORD_DETAIL("ROOM_RECORD_DETAIL"), // 특정 모임 기록 상세 화면으로 이동 (기록장 조회 - 페이지 필터 걸린채로) - ROOM_VOTE_DETAIL("ROOM_VOTE_DETAIL"); // 특정 모임 투표 상세 화면으로 이동 (투표 조회 - 페이지 필터 걸린채로) - - private final String code; -} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java b/src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java new file mode 100644 index 000000000..e2ec24ce7 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java @@ -0,0 +1,22 @@ +package konkuk.thip.notification.domain.value; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MessageRoute { + // NONE + NONE, // 알림 클릭 시 이동하지 않음 + + // FEED + FEED_USER, // 자신을 팔로우한 사용자의 피드 목록으로 화면 이동 + FEED_DETAIL, // 특정 피드 상세 화면으로 이동 + + // ROOM + ROOM_MAIN, // 특정 모임방 메인 화면으로 이동 + ROOM_DETAIL, // 특정 모임 상세정보 화면으로 이동 + ROOM_POST_DETAIL, // 특정 모임 게시글 상세 화면으로 이동 -> PostType으로 투표인지 기록인지 판단 + ROOM_RECORD_DETAIL, // 특정 모임 기록 상세 화면으로 이동 (기록장 조회 - 페이지 필터 걸린채로) + ROOM_VOTE_DETAIL; // 특정 모임 투표 상세 화면으로 이동 (투표 조회 - 페이지 필터 걸린채로) +} From 7459b5f0f5099ae14d26ef3b6a44e029cce6280c Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 23 Sep 2025 18:01:50 +0900 Subject: [PATCH 25/36] =?UTF-8?q?[rename]=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FolloweeNewPost -> FolloweeNewFeed 로 네이밍 수정 --- .../message/adapter/in/event/MessageFeedEventListener.java | 2 +- .../service/FeedNotificationOrchestratorSyncImpl.java | 4 ++-- ...loweeNewPostTemplate.java => FolloweeNewFeedTemplate.java} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/konkuk/thip/notification/application/service/template/feed/{FolloweeNewPostTemplate.java => FolloweeNewFeedTemplate.java} (87%) diff --git a/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java b/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java index 7c1c2e385..10f9a7427 100644 --- a/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java +++ b/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java @@ -34,7 +34,7 @@ public void onFeedCommentReplied(FeedEvents.FeedCommentRepliedEvent e) { @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void onFolloweeNewPost(FeedEvents.FolloweeNewFeedEvent e) { + public void onFolloweeNewFeed(FeedEvents.FolloweeNewFeedEvent e) { feedUseCase.handleFolloweeNewFeed(e); } diff --git a/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java b/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java index 294410a0d..561340273 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java +++ b/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java @@ -93,7 +93,7 @@ public void notifyFeedReplied(Long targetUserId, Long actorUserId, String actorU @Override @Transactional(propagation = Propagation.MANDATORY) public void notifyFolloweeNewFeed(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { - var args = new FolloweeNewPostTemplate.Args(actorUsername); + var args = new FolloweeNewFeedTemplate.Args(actorUsername); NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( MessageRoute.FEED_DETAIL, @@ -101,7 +101,7 @@ public void notifyFolloweeNewFeed(Long targetUserId, Long actorUserId, String ac ); notificationSyncExecutor.execute( - FolloweeNewPostTemplate.INSTANCE, + FolloweeNewFeedTemplate.INSTANCE, args, targetUserId, redirectSpec, diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewFeedTemplate.java similarity index 87% rename from src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java rename to src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewFeedTemplate.java index 61c4130d1..730138de1 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewFeedTemplate.java @@ -3,7 +3,7 @@ import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.domain.value.NotificationCategory; -public enum FolloweeNewPostTemplate implements NotificationTemplate { +public enum FolloweeNewFeedTemplate implements NotificationTemplate { INSTANCE; @Override From 48394c003d5140e9b75293b3e2c351870fa9474f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 23 Sep 2025 20:10:19 +0900 Subject: [PATCH 26/36] =?UTF-8?q?[refactor]=20request=20dto=20bean=20valid?= =?UTF-8?q?ation=20error=20message=20=EC=B6=94=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/request/FcmTokenDeleteRequest.java | 2 +- .../in/web/request/FcmTokenEnableStateChangeRequest.java | 4 ++-- .../adapter/in/web/request/FcmTokenRegisterRequest.java | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenDeleteRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenDeleteRequest.java index a18419a16..f300b1e9f 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenDeleteRequest.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenDeleteRequest.java @@ -6,7 +6,7 @@ @Schema(description = "푸시 알림 설정 삭제 요청 DTO") public record FcmTokenDeleteRequest( - @NotBlank + @NotBlank(message = "디바이스 ID는 필수입니다.") @Schema(description = "디바이스 고유 ID", example = "device12345") String deviceId ) { diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenEnableStateChangeRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenEnableStateChangeRequest.java index 767032e35..975179820 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenEnableStateChangeRequest.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenEnableStateChangeRequest.java @@ -7,11 +7,11 @@ @Schema(description = "푸시 알림 설정 변경 요청 DTO") public record FcmTokenEnableStateChangeRequest( - @NotNull + @NotNull(message = "푸시 알림 수신 여부는 필수입니다.") @Schema(description = "푸시 알림 수신 여부", example = "true") Boolean enable, - @NotBlank + @NotBlank(message = "디바이스 ID는 필수입니다.") @Schema(description = "디바이스 고유 ID", example = "device12345") String deviceId ) { diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java index 4c11a9886..4e0cbcbaa 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java @@ -2,21 +2,20 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; import konkuk.thip.common.util.PlatformType; @Schema(description = "FCM 토큰 등록 요청 DTO") public record FcmTokenRegisterRequest( - @NotBlank + @NotBlank(message = "디바이스 ID는 필수입니다.") @Schema(description = "디바이스 고유 ID", example = "device12345") String deviceId, - @NotBlank + @NotBlank(message = "FCM 토큰은 필수입니다.") @Schema(description = "FCM 토큰", example = "fcm_token_example_123456") String fcmToken, - @NotNull + @NotBlank(message = "플랫폼 타입은 필수입니다.") @Schema(description = "플랫폼 타입 (ANDROID 또는 WEB)", example = "ANDROID") String platformType ) { From 8d4ba693e5a8048035e3e90383301466d26fef3f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 23 Sep 2025 22:01:48 +0900 Subject: [PATCH 27/36] =?UTF-8?q?[move]=20PlatformType=20enum=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notification 패키지 하위로 이동 - 프로젝트 전체의 util 적인 성질보다는 notification 패키지 하위에서 사용되는 value 로 생각하는 것으로 결정됨 --- .../adapter/in/web/request/FcmTokenRegisterRequest.java | 2 +- .../thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java | 2 +- .../application/port/in/dto/FcmTokenRegisterCommand.java | 2 +- src/main/java/konkuk/thip/notification/domain/FcmToken.java | 2 +- .../util => notification/domain/value}/PlatformType.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/konkuk/thip/{common/util => notification/domain/value}/PlatformType.java (92%) diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java index 4e0cbcbaa..c59ee0f56 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; -import konkuk.thip.common.util.PlatformType; +import konkuk.thip.notification.domain.value.PlatformType; @Schema(description = "FCM 토큰 등록 요청 DTO") public record FcmTokenRegisterRequest( diff --git a/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java b/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java index b67b963eb..25ea5fc29 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java @@ -3,7 +3,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; import konkuk.thip.notification.domain.FcmToken; -import konkuk.thip.common.util.PlatformType; +import konkuk.thip.notification.domain.value.PlatformType; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java index 0ee73d6d5..d25e8be28 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java @@ -1,6 +1,6 @@ package konkuk.thip.notification.application.port.in.dto; -import konkuk.thip.common.util.PlatformType; +import konkuk.thip.notification.domain.value.PlatformType; import lombok.Builder; @Builder diff --git a/src/main/java/konkuk/thip/notification/domain/FcmToken.java b/src/main/java/konkuk/thip/notification/domain/FcmToken.java index 1aceac41b..58bc299db 100644 --- a/src/main/java/konkuk/thip/notification/domain/FcmToken.java +++ b/src/main/java/konkuk/thip/notification/domain/FcmToken.java @@ -3,7 +3,7 @@ import konkuk.thip.common.entity.BaseDomainEntity; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.exception.code.ErrorCode; -import konkuk.thip.common.util.PlatformType; +import konkuk.thip.notification.domain.value.PlatformType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/src/main/java/konkuk/thip/common/util/PlatformType.java b/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java similarity index 92% rename from src/main/java/konkuk/thip/common/util/PlatformType.java rename to src/main/java/konkuk/thip/notification/domain/value/PlatformType.java index 3adbc6190..c97995ab0 100644 --- a/src/main/java/konkuk/thip/common/util/PlatformType.java +++ b/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.util; +package konkuk.thip.notification.domain.value; import konkuk.thip.common.exception.InvalidStateException; import lombok.Getter; From 57f9970abd8d152ee0ae8ad4b31504442fd1d661 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 23 Sep 2025 22:10:47 +0900 Subject: [PATCH 28/36] =?UTF-8?q?[docs]=20api=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/NotificationCommandController.java | 2 +- .../in/web/response/NotificationMarkToCheckedResponse.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java index 30eaa2f7d..a2a7ae231 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java @@ -67,7 +67,7 @@ public BaseResponse deleteFcmToken( @Operation( summary = "유저의 특정 알림 읽음 처리", - description = "유저가 특정 알림을 읽음 처리합니다. 읽음 처리 후, 해당 알림의 페이지로 리다이렉트를 위한 데이터를 응답합니다." + description = "유저가 특정 알림을 읽음 처리합니다 (푸시알림, 알림센터의 알림 모두 포함). 읽음 처리 후, 해당 알림의 페이지로 리다이렉트를 위한 데이터를 응답합니다." ) @ExceptionDescription(NOTIFICATION_MARK_TO_CHECKED) @PostMapping("/notifications/check") diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java index 85cf6f546..571f4926e 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationMarkToCheckedResponse.java @@ -1,11 +1,16 @@ package konkuk.thip.notification.adapter.in.web.response; +import io.swagger.v3.oas.annotations.media.Schema; import konkuk.thip.notification.domain.value.MessageRoute; import java.util.Map; +@Schema(description = "알림 읽음 처리 응답 DTO") public record NotificationMarkToCheckedResponse( + @Schema(description = "'알림 리다이렉트 목적지' 에 해당하는 enum 값 입니다.", example = "POST_DETAIL -> 게시글 상세 페이지로 이동한다는 의미") MessageRoute route, + + @Schema(description = "'알림 리다이렉트 목적지' 로 이동할 때 필요한 파라미터들 입니다.", example = "{\"postId\": 123}") Map params ) { } From 6cb40fe65c9beb9c538899815e3d5c0f3c5fac3b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 24 Sep 2025 18:01:01 +0900 Subject: [PATCH 29/36] =?UTF-8?q?[fix]=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 읽음 처리된 알림에 대한 요청을 받더라도 정상 응답 하도록 수정 --- .../swagger/SwaggerResponseDescription.java | 3 +-- .../in/web/NotificationCommandController.java | 3 ++- .../service/NotificationMarkService.java | 11 ++++++++--- .../in/web/NotificationMarkToCheckedApiTest.java | 16 +++++++++------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 37ac64015..648c7d004 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -379,8 +379,7 @@ public enum SwaggerResponseDescription { ))), NOTIFICATION_MARK_TO_CHECKED(new LinkedHashSet<>(Set.of( NOTIFICATION_NOT_FOUND, - NOTIFICATION_ACCESS_FORBIDDEN, - NOTIFICATION_ALREADY_CHECKED + NOTIFICATION_ACCESS_FORBIDDEN ))), ; diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java index a2a7ae231..145706b58 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java @@ -67,7 +67,8 @@ public BaseResponse deleteFcmToken( @Operation( summary = "유저의 특정 알림 읽음 처리", - description = "유저가 특정 알림을 읽음 처리합니다 (푸시알림, 알림센터의 알림 모두 포함). 읽음 처리 후, 해당 알림의 페이지로 리다이렉트를 위한 데이터를 응답합니다." + description = "유저가 특정 알림을 읽음 처리합니다 (푸시알림, 알림센터의 알림 모두 포함). 읽음 처리 후, 해당 알림의 페이지로 리다이렉트를 위한 데이터를 응답합니다. " + + "이미 읽음처리가 된 알림에 대해서는 리다이렉트를 위한 데이터만 응답합니다." ) @ExceptionDescription(NOTIFICATION_MARK_TO_CHECKED) @PostMapping("/notifications/check") diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java index e4d523861..ddff7d7b4 100644 --- a/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java @@ -1,5 +1,6 @@ package konkuk.thip.notification.application.service; +import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.notification.adapter.in.web.response.NotificationMarkToCheckedResponse; import konkuk.thip.notification.application.port.in.NotificationMarkUseCase; import konkuk.thip.notification.application.port.out.NotificationCommandPort; @@ -22,9 +23,13 @@ public NotificationMarkToCheckedResponse markToChecked(Long notificationId, Long notification.validateOwner(userId); // 2. 알림 읽음 처리 - notification.markToChecked(); - notificationCommandPort.update(notification); - + try { + notification.markToChecked(); + notificationCommandPort.update(notification); + } catch (InvalidStateException e) { + // 이미 알림 읽음 처리된 경우 -> 무시 + } + // 3. 읽음 처리된 알림의 redirectSpec 반환 (for FE 알림 리다이렉트 동작) return new NotificationMarkToCheckedResponse( notification.getRedirectSpec().route(), diff --git a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java index 65ddfcb83..c09ddcf6d 100644 --- a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java +++ b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkToCheckedApiTest.java @@ -23,9 +23,7 @@ import java.util.Map; -import static konkuk.thip.common.exception.code.ErrorCode.NOTIFICATION_ALREADY_CHECKED; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -94,13 +92,17 @@ void mark_notification_to_checked_forbidden_when_not_owner() throws Exception { } @Test - @DisplayName("이미 읽음 처리된 알림을 다시 읽음 처리하면, 400 에러를 반환한다.") + @DisplayName("이미 읽음 처리된 알림을 다시 읽음 처리하더라도, 에러를 반환하지 않고 알림의 리다이렉트를 위한 데이터를 반환한다.") void mark_notification_to_checked_already_checked() throws Exception { // given: owner의 알림을 미리 is_checked=true 상태로 만들어 둔다 UserJpaEntity owner = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + NotificationRedirectSpec redirectSpec = TestEntityFactory.createNotificationRedirectSpec( + MessageRoute.FEED_USER, Map.of("userId", 123L) // 특정 유저의 피드 페이지로 이동 + ); + NotificationJpaEntity notification = notificationJpaRepository.save( - TestEntityFactory.createNotification(owner, "이미 읽은 알림", NotificationCategory.FEED)); + TestEntityFactory.createNotification(owner, "이미 읽은 알림", NotificationCategory.FEED, redirectSpec)); // is_checked=true 로 강제 세팅 jdbcTemplate.update( @@ -116,8 +118,8 @@ void mark_notification_to_checked_already_checked() throws Exception { .contentType(APPLICATION_JSON) .content(body) .requestAttr("userId", owner.getUserId())) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NOTIFICATION_ALREADY_CHECKED.getCode())) - .andExpect(jsonPath("$.message", containsString(NOTIFICATION_ALREADY_CHECKED.getMessage()))); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.route").value(MessageRoute.FEED_USER.name())) + .andExpect(jsonPath("$.data.params.userId").value(123)); } } From 47426c46655b0212599b78fc79d3b62f07d5307b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 25 Sep 2025 11:09:15 +0900 Subject: [PATCH 30/36] =?UTF-8?q?[refactor]=20RoomNotificationOrchestrator?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88?= =?UTF-8?q?=EC=B2=98=20=EC=88=98=EC=A0=95=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - postType 인자의 타입을 String -> PostType enum 으로 수정 --- .../port/in/RoomNotificationOrchestrator.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/application/port/in/RoomNotificationOrchestrator.java b/src/main/java/konkuk/thip/notification/application/port/in/RoomNotificationOrchestrator.java index 436f64e8d..b2cdd09fd 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/RoomNotificationOrchestrator.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/RoomNotificationOrchestrator.java @@ -1,5 +1,7 @@ package konkuk.thip.notification.application.port.in; +import konkuk.thip.post.domain.PostType; + public interface RoomNotificationOrchestrator { /** @@ -9,7 +11,7 @@ public interface RoomNotificationOrchestrator { // ===== Room 영역 ===== void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + Long roomId, Integer page, Long postId, PostType postType); void notifyRoomVoteStarted(Long targetUserId, Long roomId, String roomTitle, Integer page, Long postId); @@ -24,11 +26,11 @@ void notifyRoomJoinToHost(Long hostUserId, Long roomId, String roomTitle, Long actorUserId, String actorUsername); void notifyRoomCommentLiked(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + Long roomId, Integer page, Long postId, PostType postType); void notifyRoomPostLiked(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + Long roomId, Integer page, Long postId, PostType postType); void notifyRoomPostCommentReplied(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + Long roomId, Integer page, Long postId, PostType postType); } From a7f7262526c5090fd1c76e7dbeefbdb740270877 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 25 Sep 2025 11:19:38 +0900 Subject: [PATCH 31/36] =?UTF-8?q?[refactor]=20RoomNotificationOrchestrator?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88?= =?UTF-8?q?=EC=B2=98=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PostType enum 타입인 인자로 메서드 호출하도록 수정 --- .../service/CommentCreateService.java | 34 ++++++++++++------- .../service/CommentLikeService.java | 20 +++++++---- .../application/service/PostLikeService.java | 20 ++++++----- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java b/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java index af9a0d330..5fa211bdc 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java @@ -93,24 +93,34 @@ public CommentCreateResponse createComment(CommentCreateCommand command) { private void sendNotificationsToPostWriter(PostQueryDto postQueryDto, User actorUser) { if (postQueryDto.creatorId().equals(actorUser.getId())) return; // 자신이 작성한 게시글 제외 - if (postQueryDto.postType().equals(FEED.getType())) { - // 피드 댓글 알림 이벤트 발행 - feedNotificationOrchestrator.notifyFeedCommented(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); - } else if (postQueryDto.postType().equals(RECORD.getType()) || postQueryDto.postType().equals(VOTE.getType())) { - // 모임방 게시글 댓글 알림 이벤트 발행 - roomNotificationOrchestrator.notifyRoomPostCommented(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + PostType postType = PostType.from(postQueryDto.postType()); + switch (postType) { + case FEED -> // 피드 댓글 알림 이벤트 발행 + feedNotificationOrchestrator.notifyFeedCommented( + postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId() + ); + case RECORD, VOTE -> // 모임방 게시글 댓글 알림 이벤트 발행 + roomNotificationOrchestrator.notifyRoomPostCommented( + postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), + postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postType + ); } } private void sendNotificationsToParentCommentWriter(PostQueryDto postQueryDto, CommentQueryDto parentCommentDto, User actorUser) { if (parentCommentDto.creatorId().equals(actorUser.getId())) return; // 자신이 작성한 댓글 제외 - if (postQueryDto.postType().equals(FEED.getType())) { - // 피드 답글 알림 이벤트 발행 - feedNotificationOrchestrator.notifyFeedReplied(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); - } else if (postQueryDto.postType().equals(RECORD.getType()) || postQueryDto.postType().equals(VOTE.getType())) { - // 모임방 게시글 답글 알림 이벤트 발행 - roomNotificationOrchestrator.notifyRoomPostCommentReplied(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + PostType postType = PostType.from(postQueryDto.postType()); + switch (postType) { + case FEED -> // 피드 답글 알림 이벤트 발행 + feedNotificationOrchestrator.notifyFeedReplied( + parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId() + ); + case RECORD, VOTE -> // 모임방 게시글 답글 알림 이벤트 발행 + roomNotificationOrchestrator.notifyRoomPostCommentReplied( + parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), + postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postType + ); } } diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java b/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java index 40b90d99d..475132a6d 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java @@ -71,13 +71,19 @@ private void sendNotifications(CommentIsLikeCommand command, Comment comment) { if (command.userId().equals(comment.getCreatorId())) return; // 자신의 댓글에 좋아요 누르는 경우 제외 User actorUser = userCommandPort.findById(command.userId()); - // 좋아요 푸쉬알림 전송 - if (comment.getPostType() == PostType.FEED) { - feedNotificationOrchestrator.notifyFeedCommentLiked(comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), comment.getTargetPostId()); - } - if (comment.getPostType() == PostType.RECORD || comment.getPostType() == PostType.VOTE) { - PostQueryDto postQueryDto = postHandler.getPostQueryDto(comment.getPostType(), comment.getTargetPostId()); - roomNotificationOrchestrator.notifyRoomCommentLiked(comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + PostType postType = comment.getPostType(); + + switch (postType) { + case FEED -> + feedNotificationOrchestrator.notifyFeedCommentLiked( + comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(),comment.getTargetPostId() + ); + case RECORD, VOTE -> { + PostQueryDto postQueryDto = postHandler.getPostQueryDto(comment.getPostType(), comment.getTargetPostId()); + roomNotificationOrchestrator.notifyRoomCommentLiked( + comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postType + ); + } } } } diff --git a/src/main/java/konkuk/thip/post/application/service/PostLikeService.java b/src/main/java/konkuk/thip/post/application/service/PostLikeService.java index 4d0004ab3..bf9a2cf1b 100644 --- a/src/main/java/konkuk/thip/post/application/service/PostLikeService.java +++ b/src/main/java/konkuk/thip/post/application/service/PostLikeService.java @@ -11,7 +11,6 @@ import konkuk.thip.post.application.port.out.PostLikeCommandPort; import konkuk.thip.post.application.port.out.PostLikeQueryPort; import konkuk.thip.post.application.service.validator.PostLikeAuthorizationValidator; -import konkuk.thip.post.domain.PostType; import konkuk.thip.post.domain.service.PostCountService; import konkuk.thip.user.application.port.out.UserCommandPort; import konkuk.thip.user.domain.User; @@ -69,15 +68,20 @@ public PostIsLikeResult changeLikeStatusPost(PostIsLikeCommand command) { private void sendNotifications(PostIsLikeCommand command) { PostQueryDto postQueryDto = postHandler.getPostQueryDto(command.postType(), command.postId()); - if(command.userId().equals(postQueryDto.creatorId())) return; // 자신의 게시글에 좋아요 누르는 경우 제외 + if (command.userId().equals(postQueryDto.creatorId())) return; // 자신의 게시글에 좋아요 누르는 경우 제외 User actorUser = userCommandPort.findById(command.userId()); - // 좋아요 푸쉬알림 전송 - if (command.postType() == PostType.FEED) { - feedNotificationOrchestrator.notifyFeedLiked(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); - } - if (command.postType() == PostType.RECORD || command.postType() == PostType.VOTE) { - roomNotificationOrchestrator.notifyRoomPostLiked(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + + switch (command.postType()) { + case FEED -> + feedNotificationOrchestrator.notifyFeedLiked( + postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId() + ); + case RECORD, VOTE -> + roomNotificationOrchestrator.notifyRoomPostLiked( + postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), command.postType() + ); + } } } From 6c5a62a15ba565a45a3562199108fe5553607a64 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 25 Sep 2025 11:21:12 +0900 Subject: [PATCH 32/36] =?UTF-8?q?[refactor]=20RoomNotificationOrchestrator?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모임방 게시글 상세화면에 해당하는 MessageRoute 통일 - FE 요청으로 댓글 모달창 오픈 여부를 나타내는 "openComments" 키 값을 RedirectSpec에 추가 --- .../RoomNotificationOrchestratorSyncImpl.java | 95 ++++++++----------- .../domain/value/MessageRoute.java | 4 +- 2 files changed, 38 insertions(+), 61 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java b/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java index 6792f5ba6..974e8231a 100644 --- a/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java +++ b/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java @@ -6,6 +6,7 @@ import konkuk.thip.notification.application.service.template.room.*; import konkuk.thip.notification.domain.value.MessageRoute; import konkuk.thip.notification.domain.value.NotificationRedirectSpec; +import konkuk.thip.post.domain.PostType; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -30,18 +31,10 @@ public class RoomNotificationOrchestratorSyncImpl implements RoomNotificationOrc @Override @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + Long roomId, Integer page, Long postId, PostType postType) { var args = new RoomPostCommentedTemplate.Args(actorUsername); - NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( - MessageRoute.ROOM_POST_DETAIL, - Map.of( - "roomId", roomId, - "page", page, - "postId", postId, - "postType", postType - ) - ); + NotificationRedirectSpec redirectSpec = createRoomPostWithCommentsRedirectSpec(roomId, page, postId, postType); notificationSyncExecutor.execute( RoomPostCommentedTemplate.INSTANCE, @@ -59,15 +52,7 @@ public void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String public void notifyRoomVoteStarted(Long targetUserId, Long roomId, String roomTitle, Integer page, Long postId) { var args = new RoomVoteStartedTemplate.Args(roomTitle); - NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( - MessageRoute.ROOM_VOTE_DETAIL, - Map.of( - "roomId", roomId, - "page", page, - "postId", postId, - "postType", "VOTE" - ) - ); + NotificationRedirectSpec redirectSpec = createRoomPostRedirectSpec(roomId, page, postId, PostType.VOTE); notificationSyncExecutor.execute( RoomVoteStartedTemplate.INSTANCE, @@ -86,15 +71,7 @@ public void notifyRoomRecordCreated(Long targetUserId, Long actorUserId, String Long roomId, String roomTitle, Integer page, Long postId) { var args = new RoomRecordCreatedTemplate.Args(roomTitle, actorUsername); - NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( - MessageRoute.ROOM_RECORD_DETAIL, - Map.of( - "roomId", roomId, - "page", page, - "postId", postId, - "postType", "RECORD" - ) - ); + NotificationRedirectSpec redirectSpec = createRoomPostRedirectSpec(roomId, page, postId, PostType.RECORD); notificationSyncExecutor.execute( RoomRecordCreatedTemplate.INSTANCE, @@ -173,18 +150,10 @@ public void notifyRoomJoinToHost(Long hostUserId, Long roomId, String roomTitle, @Override @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomCommentLiked(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + Long roomId, Integer page, Long postId, PostType postType) { var args = new RoomCommentLikedTemplate.Args(actorUsername); - NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( - MessageRoute.ROOM_POST_DETAIL, - Map.of( - "roomId", roomId, - "page", page, - "postId", postId, - "postType", postType - ) - ); + NotificationRedirectSpec redirectSpec = createRoomPostWithCommentsRedirectSpec(roomId, page, postId, postType); notificationSyncExecutor.execute( RoomCommentLikedTemplate.INSTANCE, @@ -200,18 +169,10 @@ public void notifyRoomCommentLiked(Long targetUserId, Long actorUserId, String a @Override @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomPostLiked(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + Long roomId, Integer page, Long postId, PostType postType) { var args = new RoomPostLikedTemplate.Args(actorUsername); - NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( - MessageRoute.ROOM_POST_DETAIL, - Map.of( - "roomId", roomId, - "page", page, - "postId", postId, - "postType", postType - ) - ); + NotificationRedirectSpec redirectSpec = createRoomPostRedirectSpec(roomId, page, postId, postType); notificationSyncExecutor.execute( RoomPostLikedTemplate.INSTANCE, @@ -227,18 +188,10 @@ public void notifyRoomPostLiked(Long targetUserId, Long actorUserId, String acto @Override @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomPostCommentReplied(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + Long roomId, Integer page, Long postId, PostType postType) { var args = new RoomPostCommentRepliedTemplate.Args(actorUsername); - NotificationRedirectSpec redirectSpec = new NotificationRedirectSpec( - MessageRoute.ROOM_POST_DETAIL, - Map.of( - "roomId", roomId, - "page", page, - "postId", postId, - "postType", postType - ) - ); + NotificationRedirectSpec redirectSpec = createRoomPostWithCommentsRedirectSpec(roomId, page, postId, postType); notificationSyncExecutor.execute( RoomPostCommentRepliedTemplate.INSTANCE, @@ -250,4 +203,30 @@ public void notifyRoomPostCommentReplied(Long targetUserId, Long actorUserId, St ) ); } + + private NotificationRedirectSpec createRoomPostWithCommentsRedirectSpec(Long roomId, Integer page, Long postId, PostType postType) { + return new NotificationRedirectSpec( + MessageRoute.ROOM_POST_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", postType, + "openComments", true + ) + ); + } + + private NotificationRedirectSpec createRoomPostRedirectSpec(Long roomId, Integer page, Long postId, PostType postType) { + return new NotificationRedirectSpec( + MessageRoute.ROOM_POST_DETAIL, + Map.of( + "roomId", roomId, + "page", page, + "postId", postId, + "postType", postType, + "openComments", false + ) + ); + } } diff --git a/src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java b/src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java index e2ec24ce7..84d1927ab 100644 --- a/src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java +++ b/src/main/java/konkuk/thip/notification/domain/value/MessageRoute.java @@ -16,7 +16,5 @@ public enum MessageRoute { // ROOM ROOM_MAIN, // 특정 모임방 메인 화면으로 이동 ROOM_DETAIL, // 특정 모임 상세정보 화면으로 이동 - ROOM_POST_DETAIL, // 특정 모임 게시글 상세 화면으로 이동 -> PostType으로 투표인지 기록인지 판단 - ROOM_RECORD_DETAIL, // 특정 모임 기록 상세 화면으로 이동 (기록장 조회 - 페이지 필터 걸린채로) - ROOM_VOTE_DETAIL; // 특정 모임 투표 상세 화면으로 이동 (투표 조회 - 페이지 필터 걸린채로) + ROOM_POST_DETAIL, // 특정 모임 게시글 상세 화면으로 이동 -> PostType으로 투표(VOTE)인지 기록(RECORD)인지 판단 } From c9972769b492264a73ca11a1bb4bb72cc7e23672 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 25 Sep 2025 11:21:44 +0900 Subject: [PATCH 33/36] =?UTF-8?q?[refactor]=20RoomNotificationOrchestrator?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88?= =?UTF-8?q?=EC=B2=98=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomNotificationOrchestratorSyncImplTest.java | 9 +++++---- .../RoomNotificationOrchestratorSyncImplUnitTest.java | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java index bcdcd80e4..d4f8ef788 100644 --- a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java @@ -6,6 +6,7 @@ import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; +import konkuk.thip.post.domain.PostType; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; @@ -63,7 +64,7 @@ void mandatory_without_transaction_throws() { Long roomId = 11L; Integer page = 1; Long postId = 22L; - String postType = "RECORD"; + PostType postType = PostType.RECORD; // when & then assertThatThrownBy(() -> @@ -83,7 +84,7 @@ void mandatory_with_transaction_succeeds_and_persists() { Long roomId = 12L; Integer page = 3; Long postId = 33L; - String postType = "RECORD"; + PostType postType = PostType.RECORD; // when orchestrator.notifyRoomPostCommented( @@ -110,7 +111,7 @@ void roomPostCommented_afterCommit_listenerInvoked_andNotificationCommitted() { Long roomId = 1001L; Integer page = 7; Long postId = 5001L; - String postType = "RECORD"; + PostType postType = PostType.RECORD; // when (트랜잭션 안) orchestrator.notifyRoomPostCommented( @@ -146,7 +147,7 @@ void roomPostCommented_rollback_listenerNotInvoked_andNotificationNotCommitted() Long roomId = 1002L; Integer page = 2; Long postId = 5002L; - String postType = "RECORD"; + PostType postType = PostType.RECORD; // when orchestrator.notifyRoomPostCommented( diff --git a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java index c5fd1eec5..906281b04 100644 --- a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java @@ -1,6 +1,7 @@ package konkuk.thip.notification.application.service; import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.post.domain.PostType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,7 +30,7 @@ void notify_room_post_commented() { Long targetUserId = 10L; Long actorUserId = 20L; String actorUsername = "alice"; - Long roomId = 1L; int page = 2; Long postId = 3L; String postType = "RECORD"; + Long roomId = 1L; int page = 2; Long postId = 3L; PostType postType = PostType.RECORD; // when sut.notifyRoomPostCommented(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); From f33f4c03b87daae8060c90c0a3b156487b849535 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 25 Sep 2025 11:22:01 +0900 Subject: [PATCH 34/36] =?UTF-8?q?[refactor]=20=EA=B8=B0=EB=A1=9D=EC=9E=A5?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EA=B8=B0=EB=B3=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=ED=81=AC=EA=B8=B0=2020=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roompost/application/service/RoomPostSearchService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java b/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java index 2caf740a0..6056fba75 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java @@ -48,7 +48,7 @@ public class RoomPostSearchService implements RoomPostSearchUseCase { private final RoomPostAccessValidator roomPostAccessValidator; private final RoomPostQueryMapper roomPostQueryMapper; - private static final int DEFAULT_PAGE_SIZE = 10; + private static final int DEFAULT_PAGE_SIZE = 20; @Override @Transactional(readOnly = true) From a9145c72630dcee7116ecd9fb702c79584f4266f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 25 Sep 2025 11:22:24 +0900 Subject: [PATCH 35/36] =?UTF-8?q?[refactor]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=20=EB=AC=B8=20=EC=82=AD=EC=A0=9C=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/PostLikeCommandPersistenceAdapter.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java index a7f8e8305..a62911350 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java @@ -2,14 +2,11 @@ import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; -import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.post.domain.PostType; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.mapper.PostLikeMapper; import konkuk.thip.post.application.port.out.PostLikeCommandPort; -import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; -import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -17,11 +14,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import static konkuk.thip.common.exception.code.ErrorCode.*; import static konkuk.thip.common.exception.code.ErrorCode.RECORD_NOT_FOUND; From 7b50425f8f611a07a75d2b416b04fdc888a1731a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 25 Sep 2025 16:52:26 +0900 Subject: [PATCH 36/36] =?UTF-8?q?[chore]=20=ED=94=84=EB=A1=9C=EB=A9=94?= =?UTF-8?q?=ED=85=8C=EC=9A=B0=EC=8A=A4=20=EC=9D=98=EC=A1=B4=EC=84=B1=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 --- build.gradle | 1 + .../konkuk/thip/common/security/constant/SecurityWhitelist.java | 1 + 2 files changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 74aa09e47..4f43d1200 100644 --- a/build.gradle +++ b/build.gradle @@ -81,6 +81,7 @@ dependencies { // Spring Boot Actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' // AOP implementation 'org.springframework.boot:spring-boot-starter-aop' diff --git a/src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java b/src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java index 35d417829..9a4d8247c 100644 --- a/src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java +++ b/src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java @@ -17,6 +17,7 @@ public enum SecurityWhitelist { OAUTH2_AUTHORIZATION("/oauth2/authorization/**"), LOGIN_OAUTH2_CODE("/login/oauth2/code/**"), ACTUATOR_HEALTH("/actuator/health"), + ACTUATOR_PROMETHEUS("/actuator/prometheus"), AUTH_USERS("/auth/users"), AUTH_TOKEN("/auth/token"), API_TEST("/api/test/**"),