From d1e4cc411b7a4726c4f8850b405cd1eb75910f87 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 04:08:30 +0900 Subject: [PATCH 01/31] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20DB=20=EC=A0=80=EC=9E=A5=20+=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=8D=BC=EB=B8=94=EB=A6=AC?= =?UTF-8?q?=EC=8B=9C=20=EB=A5=BC=20=EB=8B=B4=EB=8B=B9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/FeedNotificationOrchestrator.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/port/in/FeedNotificationOrchestrator.java 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 new file mode 100644 index 000000000..f58a8b845 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/FeedNotificationOrchestrator.java @@ -0,0 +1,22 @@ +package konkuk.thip.notification.application.port.in; + +public interface FeedNotificationOrchestrator { + + /** + * 비즈니스 로직 이후, NotificationOrchestrator 를 호출하여 알림 관련 로직 실행 + * -> DB에 notification data save + 푸시알림 + */ + + // ===== Feed 영역 ===== + void notifyFollowed(Long targetUserId, Long actorUserId, String actorUsername); + + void notifyFeedCommented(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); + + void notifyFeedReplied(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); + + void notifyFolloweeNewPost(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); + + void notifyFeedLiked(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); + + void notifyFeedCommentLiked(Long targetUserId, Long actorUserId, String actorUsername, Long feedId); +} From c92686330b2700e3cfe3705c587fd95057bef98f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 04:08:56 +0900 Subject: [PATCH 02/31] =?UTF-8?q?[feat]=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20DB=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20+=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=8D=BC?= =?UTF-8?q?=EB=B8=94=EB=A6=AC=EC=8B=9C=20=EB=A5=BC=20=EB=8B=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/RoomNotificationOrchestrator.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/port/in/RoomNotificationOrchestrator.java 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 new file mode 100644 index 000000000..436f64e8d --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/RoomNotificationOrchestrator.java @@ -0,0 +1,34 @@ +package konkuk.thip.notification.application.port.in; + +public interface RoomNotificationOrchestrator { + + /** + * 비즈니스 로직 이후, NotificationOrchestrator 를 호출하여 알림 관련 로직 실행 + * -> DB에 notification data save + 푸시알림 + */ + + // ===== Room 영역 ===== + void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); + + void notifyRoomVoteStarted(Long targetUserId, Long roomId, String roomTitle, Integer page, Long postId); + + void notifyRoomRecordCreated(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId); + + void notifyRoomRecruitClosedEarly(Long targetUserId, Long roomId, String roomTitle); + + void notifyRoomActivityStarted(Long targetUserId, Long roomId, String roomTitle); + + 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); + + void notifyRoomPostLiked(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); + + void notifyRoomPostCommentReplied(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); +} From a5f53b2b8c86872ada123750907fed79a028dfdc Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:45:17 +0900 Subject: [PATCH 03/31] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20DB=20=EC=A0=80=EC=9E=A5=20+=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=8D=BC=EB=B8=94=EB=A6=AC?= =?UTF-8?q?=EC=8B=9C=20=EB=A5=BC=20=EB=8B=B4=EB=8B=B9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=B6=94=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notification DB 저장은 상위 트랜잭션 커밋시에 동기적으로 수행 - 상위 트랜잭션이 있을 경우에만 정상 동작하도록 Propagation.MANDATORY 제한 설정 - 푸시알림을 위한 이벤트 퍼블리시는 비동기적으로 수행 - 이벤트 리스너가 트랜잭션 커밋시에 이벤트를 받아서 fcm 서버로 푸시알림 보내는 구조는 유지 --- .../FeedNotificationOrchestratorSyncImpl.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java diff --git a/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java b/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java new file mode 100644 index 000000000..fc17bac01 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java @@ -0,0 +1,118 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.common.annotation.application.HelperService; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; +import konkuk.thip.notification.application.port.out.NotificationCommandPort; +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.application.service.template.feed.*; +import konkuk.thip.notification.domain.Notification; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@HelperService +@RequiredArgsConstructor +public class FeedNotificationOrchestratorSyncImpl implements FeedNotificationOrchestrator { + + /** + * 정책: + * 1) 알림(Notification) DB 저장은 비즈니스 트랜잭션과 동일한 경계 내에서 "동기"로 수행한다. + * -> 비즈니스 로직에서 시작한 상위 트랜잭션에 DB notification 저장이 포함되어야 하므로, Propagation.MANDATORY 강제 + * 2) 푸시 알림은 AFTER_COMMIT 리스너에서 "비동기"로 발송한다. + */ + + private final NotificationCommandPort notificationCommandPort; + private final FeedEventCommandPort feedEventCommandPort; + + // ========================= 공통 헬퍼 ========================= + private void notifyWithTemplate( + NotificationTemplate template, + T args, + Long targetUserId, + Runnable eventPublisher + ) { + String title = template.title(args); + String content = template.content(args); + saveNotification(title, content, targetUserId); + eventPublisher.run(); + } + + private void saveNotification(String title, String content, Long targetUserId) { + Notification notification = Notification.withoutId(title, content, targetUserId); + notificationCommandPort.save(notification); + } + + // ========================= Feed 영역 ========================= + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyFollowed(Long targetUserId, Long actorUserId, String actorUsername) { + var args = new FollowedTemplate.Args(actorUsername); + notifyWithTemplate( + FollowedTemplate.INSTANCE, + args, + targetUserId, + () -> feedEventCommandPort.publishFollowEvent(targetUserId, actorUserId, actorUsername) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyFeedCommented(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { + var args = new FeedCommentedTemplate.Args(actorUsername); + notifyWithTemplate( + FeedCommentedTemplate.INSTANCE, + args, + targetUserId, + () -> feedEventCommandPort.publishFeedCommentedEvent(targetUserId, actorUserId, actorUsername, feedId) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyFeedReplied(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { + var args = new FeedRepliedTemplate.Args(actorUsername); + notifyWithTemplate( + FeedRepliedTemplate.INSTANCE, + args, + targetUserId, + () -> feedEventCommandPort.publishFeedRepliedEvent(targetUserId, actorUserId, actorUsername, feedId) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyFolloweeNewPost(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { + var args = new FolloweeNewPostTemplate.Args(actorUsername); + notifyWithTemplate( + FolloweeNewPostTemplate.INSTANCE, + args, + targetUserId, + () -> feedEventCommandPort.publishFolloweeNewPostEvent(targetUserId, actorUserId, actorUsername, feedId) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyFeedLiked(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { + var args = new FeedLikedTemplate.Args(actorUsername); + notifyWithTemplate( + FeedLikedTemplate.INSTANCE, + args, + targetUserId, + () -> feedEventCommandPort.publishFeedLikedEvent(targetUserId, actorUserId, actorUsername, feedId) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyFeedCommentLiked(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { + var args = new FeedCommentLikedTemplate.Args(actorUsername); + notifyWithTemplate( + FeedCommentLikedTemplate.INSTANCE, + args, + targetUserId, + () -> feedEventCommandPort.publishFeedCommentLikedEvent(targetUserId, actorUserId, actorUsername, feedId) + ); + } +} From 1dd9ac3581ad32c5574cab8fb0220cd359b9524b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:46:17 +0900 Subject: [PATCH 04/31] =?UTF-8?q?[feat]=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=95=8C=EB=A6=BC=20DB=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20+=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=8D=BC?= =?UTF-8?q?=EB=B8=94=EB=A6=AC=EC=8B=9C=20=EB=A5=BC=20=EB=8B=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notification DB 저장은 상위 트랜잭션 커밋시에 동기적으로 수행 - 상위 트랜잭션이 있을 경우에만 정상 동작하도록 Propagation.MANDATORY 제한 설정 - 푸시알림을 위한 이벤트 퍼블리시는 비동기적으로 수행 - 이벤트 리스너가 트랜잭션 커밋시에 이벤트를 받아서 fcm 서버로 푸시알림 보내는 구조는 유지 --- .../RoomNotificationOrchestratorSyncImpl.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java diff --git a/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java b/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java new file mode 100644 index 000000000..151fc209d --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java @@ -0,0 +1,159 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.common.annotation.application.HelperService; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; +import konkuk.thip.notification.application.port.out.NotificationCommandPort; +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.application.service.template.room.*; +import konkuk.thip.notification.domain.Notification; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@HelperService +@RequiredArgsConstructor +public class RoomNotificationOrchestratorSyncImpl implements RoomNotificationOrchestrator { + + /** + * 정책: + * 1) 알림(Notification) DB 저장은 비즈니스 트랜잭션과 동일한 경계 내에서 "동기"로 수행한다. + * -> 비즈니스 로직에서 시작한 상위 트랜잭션에 DB notification 저장이 포함되어야 하므로, Propagation.MANDATORY 강제 + * 2) 푸시 알림은 AFTER_COMMIT 리스너에서 "비동기"로 발송한다. + */ + + private final NotificationCommandPort notificationCommandPort; + private final RoomEventCommandPort roomEventCommandPort; + + // ========================= 공통 헬퍼 ========================= + private void notifyWithTemplate( + NotificationTemplate template, + T args, + Long targetUserId, + Runnable eventPublisher + ) { + String title = template.title(args); + String content = template.content(args); + saveNotification(title, content, targetUserId); + eventPublisher.run(); + } + + private void saveNotification(String title, String content, Long targetUserId) { + Notification notification = Notification.withoutId(title, content, targetUserId); + notificationCommandPort.save(notification); + } + + // ========================= Room 영역 ========================= + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { + var args = new RoomPostCommentedTemplate.Args(actorUsername); + notifyWithTemplate( + RoomPostCommentedTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomPostCommentedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomVoteStarted(Long targetUserId, Long roomId, String roomTitle, Integer page, Long postId) { + var args = new RoomVoteStartedTemplate.Args(roomTitle); + notifyWithTemplate( + RoomVoteStartedTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomVoteStartedEvent(targetUserId, roomId, roomTitle, page, postId) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomRecordCreated(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId) { + var args = new RoomRecordCreatedTemplate.Args(roomTitle, actorUsername); + notifyWithTemplate( + RoomRecordCreatedTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomRecordCreatedEvent(targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomRecruitClosedEarly(Long targetUserId, Long roomId, String roomTitle) { + var args = new RoomRecruitClosedEarlyTemplate.Args(roomTitle); + notifyWithTemplate( + RoomRecruitClosedEarlyTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomRecruitClosedEarlyEvent(targetUserId, roomId, roomTitle) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomActivityStarted(Long targetUserId, Long roomId, String roomTitle) { + var args = new RoomActivityStartedTemplate.Args(roomTitle); + notifyWithTemplate( + RoomActivityStartedTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomActivityStartedEvent(targetUserId, roomId, roomTitle) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomJoinToHost(Long hostUserId, Long roomId, String roomTitle, Long actorUserId, String actorUsername) { + var args = new RoomJoinToHostTemplate.Args(roomTitle, actorUsername); + notifyWithTemplate( + RoomJoinToHostTemplate.INSTANCE, + args, + hostUserId, + () -> roomEventCommandPort.publishRoomJoinEventToHost(hostUserId, roomId, roomTitle, actorUserId, actorUsername) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomCommentLiked(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { + var args = new RoomCommentLikedTemplate.Args(actorUsername); + notifyWithTemplate( + RoomCommentLikedTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomCommentLikedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomPostLiked(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { + var args = new RoomPostLikedTemplate.Args(actorUsername); + notifyWithTemplate( + RoomPostLikedTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomPostLikedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + ); + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void notifyRoomPostCommentReplied(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { + var args = new RoomPostCommentRepliedTemplate.Args(actorUsername); + notifyWithTemplate( + RoomPostCommentRepliedTemplate.INSTANCE, + args, + targetUserId, + () -> roomEventCommandPort.publishRoomPostCommentRepliedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + ); + } +} From fb6158618df0bb7f1d171bb4d56a4f2b134ac8ed Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:48:12 +0900 Subject: [PATCH 05/31] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=A0=20title,=20con?= =?UTF-8?q?tent=20=EC=9D=98=20=EC=83=9D=EC=84=B1=EC=9D=84=20=EB=8B=B4?= =?UTF-8?q?=EB=8B=B9=ED=95=98=EB=8A=94=20template=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/template/NotificationTemplate.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java diff --git a/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java new file mode 100644 index 000000000..8d8e22f95 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java @@ -0,0 +1,8 @@ +package konkuk.thip.notification.application.service.template; + +public interface NotificationTemplate { + + String title(T args); + + String content(T args); +} From 4d1d3162ebd3c8c12bbbe16272d9eef20daa8abc Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:49:49 +0900 Subject: [PATCH 06/31] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=95=8C=EB=A6=BC=EC=9D=98=20title,=20content=20?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20template=20e?= =?UTF-8?q?num=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feed/FeedCommentLikedTemplate.java | 20 +++++++++++++++++ .../template/feed/FeedCommentedTemplate.java | 20 +++++++++++++++++ .../template/feed/FeedLikedTemplate.java | 20 +++++++++++++++++ .../template/feed/FeedRepliedTemplate.java | 20 +++++++++++++++++ .../template/feed/FollowedTemplate.java | 22 +++++++++++++++++++ .../feed/FolloweeNewPostTemplate.java | 20 +++++++++++++++++ 6 files changed, 122 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java new file mode 100644 index 000000000..4c2358115 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.feed; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum FeedCommentLikedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.FEED.prefixedTitle("좋아요 알림"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java new file mode 100644 index 000000000..e58391abe --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.feed; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum FeedCommentedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.FEED.prefixedTitle("새로운 댓글이 달렸어요"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 글에 댓글을 달았어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java new file mode 100644 index 000000000..15b608c3a --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.feed; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum FeedLikedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.FEED.prefixedTitle("내 글을 좋아합니다"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 글에 좋아요를 눌렀어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java new file mode 100644 index 000000000..37a8de487 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.feed; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum FeedRepliedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.FEED.prefixedTitle("새로운 답글이 달렸어요"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 댓글에 답글을 달았어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java new file mode 100644 index 000000000..f9e8003f4 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java @@ -0,0 +1,22 @@ +package konkuk.thip.notification.application.service.template.feed; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; +import lombok.Getter; + +@Getter +public enum FollowedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.FEED.prefixedTitle("팔로워 알림"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 나를 띱했어요!"; + } + + public record Args(String actorUsername) {} +} 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/FolloweeNewPostTemplate.java new file mode 100644 index 000000000..af18247fa --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.feed; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum FolloweeNewPostTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.FEED.prefixedTitle("새 글 알림"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 새로운 글을 작성했어요!"; + } + + public record Args(String actorUsername) {} +} From cd10cdb2478cf4adb78692fd1b811889a0d67b42 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:50:10 +0900 Subject: [PATCH 07/31] =?UTF-8?q?[feat]=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=95=8C=EB=A6=BC=EC=9D=98=20title,=20con?= =?UTF-8?q?tent=20=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20templ?= =?UTF-8?q?ate=20enum=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room/RoomActivityStartedTemplate.java | 20 +++++++++++++++++++ .../room/RoomCommentLikedTemplate.java | 20 +++++++++++++++++++ .../template/room/RoomJoinToHostTemplate.java | 20 +++++++++++++++++++ .../room/RoomPostCommentRepliedTemplate.java | 20 +++++++++++++++++++ .../room/RoomPostCommentedTemplate.java | 20 +++++++++++++++++++ .../template/room/RoomPostLikedTemplate.java | 20 +++++++++++++++++++ .../room/RoomRecordCreatedTemplate.java | 20 +++++++++++++++++++ .../room/RoomRecruitClosedEarlyTemplate.java | 20 +++++++++++++++++++ .../room/RoomVoteStartedTemplate.java | 20 +++++++++++++++++++ 9 files changed, 180 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java new file mode 100644 index 000000000..4ea611682 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomActivityStartedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle(args.roomTitle); + } + + @Override + public String content(Args args) { + return "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"; + } + + public record Args(String roomTitle) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java new file mode 100644 index 000000000..09e09c86c --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomCommentLikedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle("내 댓글을 좋아합니다"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java new file mode 100644 index 000000000..5ab0096b1 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomJoinToHostTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle(args.roomTitle); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 모임에 참여했어요!"; + } + + public record Args(String roomTitle, String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java new file mode 100644 index 000000000..860c30c9c --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomPostCommentRepliedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle("새로운 답글이 달렸어요"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 댓글에 답글을 달았어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java new file mode 100644 index 000000000..c10510b20 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomPostCommentedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle("새로운 댓글이 달렸어요"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 독서기록에 댓글을 달았어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java new file mode 100644 index 000000000..37f3133b7 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomPostLikedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle("좋아요 알림"); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 내 독서기록에 좋아요를 눌렀어요!"; + } + + public record Args(String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java new file mode 100644 index 000000000..a1180550e --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomRecordCreatedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle(args.roomTitle); + } + + @Override + public String content(Args args) { + return "@" + args.actorUsername() + " 님이 새로운 독서 기록을 작성했어요!"; + } + + public record Args(String roomTitle, String actorUsername) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java new file mode 100644 index 000000000..600f3c778 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomRecruitClosedEarlyTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle(args.roomTitle); + } + + @Override + public String content(Args args) { + return "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"; + } + + public record Args(String roomTitle) {} +} diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java new file mode 100644 index 000000000..2384361eb --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service.template.room; + +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.value.NotificationCategory; + +public enum RoomVoteStartedTemplate implements NotificationTemplate { + INSTANCE; + + @Override + public String title(Args args) { + return NotificationCategory.ROOM.prefixedTitle(args.roomTitle); + } + + @Override + public String content(Args args) { + return "새로운 투표가 시작되었어요!"; + } + + public record Args(String roomTitle) {} +} From ec18391608b4d15467ad5aa75b2c72fa8b67d922 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:51:09 +0900 Subject: [PATCH 08/31] =?UTF-8?q?[feat]=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=82=B4?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EC=A0=95=EC=A0=81=20=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/notification/domain/Notification.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/konkuk/thip/notification/domain/Notification.java b/src/main/java/konkuk/thip/notification/domain/Notification.java index 5438446f0..6c35bd815 100644 --- a/src/main/java/konkuk/thip/notification/domain/Notification.java +++ b/src/main/java/konkuk/thip/notification/domain/Notification.java @@ -17,4 +17,13 @@ public class Notification extends BaseDomainEntity { private boolean isChecked; private Long targetUserId; + + public static Notification withoutId (String title, String content, Long targetUserId) { + return Notification.builder() + .title(title) + .content(content) + .isChecked(false) + .targetUserId(targetUserId) + .build(); + } } From 28d1bb07f17099aa407f5257ddcc93a3ef02e27c Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:52:18 +0900 Subject: [PATCH 09/31] =?UTF-8?q?[feat]=20notification=20DB=20save=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationCommandPersistenceAdapter.java | 15 +++++++++++++++ .../port/out/NotificationCommandPort.java | 3 +++ 2 files changed, 18 insertions(+) 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 568af5196..0227e28d9 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 @@ -1,8 +1,13 @@ package konkuk.thip.notification.adapter.out.persistence; +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.common.exception.code.ErrorCode; 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; +import konkuk.thip.notification.domain.Notification; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -11,6 +16,16 @@ public class NotificationCommandPersistenceAdapter implements NotificationCommandPort { private final NotificationJpaRepository notificationJpaRepository; + private final UserJpaRepository userJpaRepository; + private final NotificationMapper notificationMapper; + @Override + public void save(Notification notification) { + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(notification.getTargetUserId()).orElseThrow( + () -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND) + ); + + notificationJpaRepository.save(notificationMapper.toJpaEntity(notification, userJpaEntity)); + } } 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 e335879cb..e9a122db3 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,6 +1,9 @@ package konkuk.thip.notification.application.port.out; +import konkuk.thip.notification.domain.Notification; + public interface NotificationCommandPort { + void save(Notification notification); } From 7a62726306e5c25327b32614f4e49ad374a61cf8 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:54:17 +0900 Subject: [PATCH 10/31] =?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=20event=20dto?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모든 이벤트에 title, content 추가 - 이벤트 리스너는 이벤트에 포함된 raw data 로 title, content 를 구성하는게 아니라, 이벤트에 포함된 title, content 정보를 그대로 사용하도록 하기 위해 이벤트 구조 수정 --- .../adapter/out/event/dto/FeedEvents.java | 36 ++++++++----- .../adapter/out/event/dto/RoomEvents.java | 52 +++++++++++++------ 2 files changed, 59 insertions(+), 29 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 176f1f801..922721d64 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 @@ -7,30 +7,42 @@ public class FeedEvents { // 누군가 나를 팔로우하는 경우 @Builder - public record FollowerEvent(Long targetUserId, Long actorUserId, String actorUsername) {} + public record FollowerEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername) {} // 누군가 내 피드에 댓글을 다는 경우 @Builder - public record FeedCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + public record FeedCommentedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} // 누군가 내 댓글에 대댓글을 다는 경우 @Builder - public record FeedCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + public record FeedCommentRepliedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} // 내가 팔로우하는 사람이 새 글을 올리는 경우 @Builder - public record FolloweeNewPostEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + public record FolloweeNewPostEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} // 내 피드가 좋아요를 받는 경우 @Builder - public record FeedLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} + public record FeedLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} // 내 피드 댓글이 좋아요를 받는 경우 @Builder - public record FeedCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) {} -} \ No newline at end of file + public record FeedCommentLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} +} 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 bdc076f93..e96c8c098 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 @@ -8,44 +8,62 @@ public class RoomEvents { // 댓글 대상이 "기록/투표" 모두 가능하므로 통합 스키마 사용 // 내 모임방 기록/투표에 댓글이 달린 경우 @Builder - public record RoomPostCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} + public record RoomPostCommentedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} // 내가 참여한 모임방에 새로운 투표가 시작된 경우 @Builder - public record RoomVoteStartedEvent(Long targetUserId, Long roomId, String roomTitle, - Integer page, Long postId) {} + public record RoomVoteStartedEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle, + Integer page, Long postId) {} // 내가 참여한 모임방에 새로운 기록이 작성된 경우 @Builder - public record RoomRecordCreatedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, String roomTitle, Integer page, Long postId) {} + public record RoomRecordCreatedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId) {} // 내가 참여한 모임방이 조기 종료된 경우 (호스트가 모집 마감 버튼 누른 경우) @Builder - public record RoomRecruitClosedEarlyEvent(Long targetUserId, Long roomId, String roomTitle) {} + public record RoomRecruitClosedEarlyEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle) {} // 내가 참여한 모임방 활동이 시작된 경우 (방이 시작 기간이 되어 자동으로 시작된 경우) @Builder - public record RoomActivityStartedEvent(Long targetUserId, Long roomId, String roomTitle) {} + public record RoomActivityStartedEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle) {} // 내가 방장일 때, 새로운 사용자가 모임방 참여를 한 경우 @Builder - public record RoomJoinRequestedToOwnerEvent(Long ownerUserId, Long roomId, String roomTitle, - Long applicantUserId, String applicantUsername) {} + public record RoomJoinRequestedToOwnerEvent( + String title, String content, + Long ownerUserId, Long roomId, String roomTitle, + Long applicantUserId, String applicantUsername) {} // 내가 참여한 모임방의 나의 댓글이 좋아요를 받는 경우 @Builder - public record RoomCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} + public record RoomCommentLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} // 내가 참여한 모임방의 나의 기록이 좋아요를 받는 경우 @Builder - public record RoomPostLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} + public record RoomPostLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} // 내가 참여한 모임방의 나의 댓글에 대댓글이 달린 경우 @Builder - public record RoomPostCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) {} -} \ No newline at end of file + public record RoomPostCommentRepliedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} +} From 47215b31cb8ee325bbb07ca34a9211bd77c0eab7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:55:49 +0900 Subject: [PATCH 11/31] =?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=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이벤트에 포함된 title, content 정보를 받아서 바로 사용하도록 수정 - title, content 의 생성 책임은 template enum 만 담당 --- .../FeedNotificationDispatchService.java | 22 ++++----- .../RoomNotificationDispatchService.java | 47 ++++++++----------- 2 files changed, 27 insertions(+), 42 deletions(-) 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 27ecdcd7b..1941205dd 100644 --- a/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java +++ b/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java @@ -5,7 +5,7 @@ import konkuk.thip.message.application.port.in.FeedNotificationDispatchUseCase; import konkuk.thip.message.application.port.out.FirebaseMessagingPort; import konkuk.thip.message.adapter.out.event.dto.FeedEvents; -import konkuk.thip.message.domain.NotificationCategory; +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; @@ -26,8 +26,7 @@ public class FeedNotificationDispatchService implements FeedNotificationDispatch @Override public void handleFollower(final FeedEvents.FollowerEvent event) { - Notification n = buildNotification("팔로워 알림", - "@" + event.actorUsername() + " 님이 나를 띱했어요!"); + Notification n = buildNotification(event.title(), event.content()); List tokens = fcmTokenPersistencePort.findEnabledByUserId(event.targetUserId()); @@ -49,40 +48,35 @@ public void handleFollower(final FeedEvents.FollowerEvent event) { @Override public void handleFeedCommented(final FeedEvents.FeedCommentedEvent event) { - Notification notification = buildNotification("새로운 댓글이 달렸어요", - "@" +event.actorUsername() + " 님이 내 글에 댓글을 달았어요!"); + Notification notification = buildNotification(event.title(), event.content()); pushFeedDetail(event.targetUserId(), notification, event.feedId()); } @Override public void handleFeedCommentReplied(final FeedEvents.FeedCommentRepliedEvent event) { - Notification notification = buildNotification("새로운 답글이 달렸어요", - "@" + event.actorUsername() + " 님이 내 댓글에 답글을 달았어요!"); + Notification notification = buildNotification(event.title(), event.content()); pushFeedDetail(event.targetUserId(), notification, event.feedId()); } @Override public void handleFolloweeNewPost(final FeedEvents.FolloweeNewPostEvent event) { - Notification notification = buildNotification("새 글 알림", - "@" + event.actorUsername() + " 님이 새로운 글을 작성했어요!"); + Notification notification = buildNotification(event.title(), event.content()); pushFeedDetail(event.targetUserId(), notification, event.feedId()); } @Override public void handleFeedLiked(final FeedEvents.FeedLikedEvent event) { - Notification notification = buildNotification("내 글을 좋아합니다", - "@" + event.actorUsername() + " 님이 내 글에 좋아요를 눌렀어요!"); + Notification notification = buildNotification(event.title(), event.content()); pushFeedDetail(event.targetUserId(), notification, event.feedId()); } @Override public void handleFeedCommentLiked(final FeedEvents.FeedCommentLikedEvent event) { - Notification notification = buildNotification("좋아요 알림", - "@" + event.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"); + Notification notification = buildNotification(event.title(), event.content()); pushFeedDetail(event.targetUserId(), notification, event.feedId()); } @@ -109,7 +103,7 @@ private void pushFeedDetail(Long userId, Notification notification, Long feedId) } private Notification buildNotification(final String title, final String body) { - return Notification.builder().setTitle(NotificationCategory.FEED.prefixedTitle(title)).setBody(body).build(); + return Notification.builder().setTitle(title).setBody(body).build(); } private Message buildMessage(final String token, final Notification n, 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 93acf0b81..e705697a2 100644 --- a/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java +++ b/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java @@ -5,7 +5,7 @@ import konkuk.thip.message.application.port.in.RoomNotificationDispatchUseCase; import konkuk.thip.message.application.port.out.FirebaseMessagingPort; import konkuk.thip.message.adapter.out.event.dto.RoomEvents; -import konkuk.thip.message.domain.NotificationCategory; +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; @@ -26,8 +26,7 @@ public class RoomNotificationDispatchService implements RoomNotificationDispatch @Override public void handleRoomPostCommented(final RoomEvents.RoomPostCommentedEvent event) { - Notification notification = buildNotification("새로운 댓글이 달렸어요", - "@" + event.actorUsername() + " 님이 내 독서기록에 댓글을 달았어요!"); + Notification notification = buildNotification(event.title(), event.content()); List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); if (tokens.isEmpty()) return; @@ -52,8 +51,7 @@ public void handleRoomPostCommented(final RoomEvents.RoomPostCommentedEvent even @Override public void handleRoomVoteStarted(final RoomEvents.RoomVoteStartedEvent event) { - Notification notification = buildNotification(event.roomTitle(), - "새로운 투표가 시작되었어요!"); + Notification notification = buildNotification(event.title(), event.content()); List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); if (tokens.isEmpty()) return; @@ -78,8 +76,7 @@ public void handleRoomVoteStarted(final RoomEvents.RoomVoteStartedEvent event) { @Override public void handleRoomRecordCreated(final RoomEvents.RoomRecordCreatedEvent event) { - Notification notification = buildNotification(event.roomTitle(), - "@" + event.actorUsername() + " 님이 새로운 독서 기록을 작성했어요!"); + Notification notification = buildNotification(event.title(), event.content()); List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); if (tokens.isEmpty()) return; @@ -104,32 +101,28 @@ public void handleRoomRecordCreated(final RoomEvents.RoomRecordCreatedEvent even @Override public void handleRoomRecruitClosedEarly(final RoomEvents.RoomRecruitClosedEarlyEvent event) { - Notification n = buildNotification(event.roomTitle(), - "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"); + Notification notification = buildNotification(event.title(), event.content()); - pushRoomMain(event.targetUserId(), event.roomId(), n); + pushRoomMain(event.targetUserId(), event.roomId(), notification); } @Override public void handleRoomActivityStarted(final RoomEvents.RoomActivityStartedEvent event) { - Notification notification = buildNotification(event.roomTitle(), - "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"); + Notification notification = buildNotification(event.title(), event.content()); pushRoomMain(event.targetUserId(), event.roomId(), notification); } @Override public void handleRoomJoinRequestedToOwner(final RoomEvents.RoomJoinRequestedToOwnerEvent event) { - Notification n = buildNotification(event.roomTitle(), - "@" + event.applicantUsername() + " 님이 모임에 참여했어요!"); + Notification notification = buildNotification(event.title(), event.content()); - pushRoomDetail(event.ownerUserId(), event.roomId(), n); + pushRoomDetail(event.ownerUserId(), event.roomId(), notification); } @Override public void handleRoomCommentLiked(final RoomEvents.RoomCommentLikedEvent event) { - Notification notification = buildNotification("내 댓글을 좋아합니다", - "@" + event.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"); + Notification notification = buildNotification(event.title(), event.content()); List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); if (tokens.isEmpty()) return; @@ -153,8 +146,7 @@ public void handleRoomCommentLiked(final RoomEvents.RoomCommentLikedEvent event) @Override public void handleRoomPostLiked(final RoomEvents.RoomPostLikedEvent event) { - Notification notification = buildNotification("좋아요 알림", - "@" + event.actorUsername() + " 님이 내 독서기록에 좋아요를 눌렀어요!"); + Notification notification = buildNotification(event.title(), event.content()); List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); if (tokens.isEmpty()) return; @@ -177,11 +169,10 @@ public void handleRoomPostLiked(final RoomEvents.RoomPostLikedEvent event) { } @Override - public void handleRoomPostCommentReplied(RoomEvents.RoomPostCommentRepliedEvent e) { - Notification notification = buildNotification("새로운 댓글이 달렸어요", - "@" + e.actorUsername() + " 님이 내 댓글에 대댓글을 달았어요!"); + public void handleRoomPostCommentReplied(RoomEvents.RoomPostCommentRepliedEvent event) { + Notification notification = buildNotification(event.title(), event.content()); - List tokens = fcmTokenQueryPort.findEnabledByUserId(e.targetUserId()); + List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); if (tokens.isEmpty()) return; List msgs = new ArrayList<>(tokens.size()); @@ -191,11 +182,11 @@ public void handleRoomPostCommentReplied(RoomEvents.RoomPostCommentRepliedEvent for (FcmToken t : tokens) { Message m = buildMessage(t.getFcmToken(), notification, MessageRoute.ROOM_POST_DETAIL, - "roomId", String.valueOf(e.roomId()), - "page", String.valueOf(e.page()), + "roomId", String.valueOf(event.roomId()), + "page", String.valueOf(event.page()), "type", "group", - "postId", String.valueOf(e.postId()), - "postType", String.valueOf(e.postType())); + "postId", String.valueOf(event.postId()), + "postType", String.valueOf(event.postType())); msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); } @@ -243,7 +234,7 @@ private void pushRoomDetail(Long targetUserId, Long roomId, Notification notific } private Notification buildNotification(final String title, final String body) { - return Notification.builder().setTitle(NotificationCategory.ROOM.prefixedTitle(title)).setBody(body).build(); + return Notification.builder().setTitle(title).setBody(body).build(); } private Message buildMessage(final String token, final Notification n, From 9f28bd6a463ba5df5eebb4cb7ead950083138271 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 11:58:26 +0900 Subject: [PATCH 12/31] =?UTF-8?q?[refactor]=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=EC=83=9D=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20=EB=8B=B4=EB=8B=B9=ED=95=98=EB=8A=94=20ser?= =?UTF-8?q?vice=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NotificationOrchestrator 를 호출하도록 수정 - NotificationOrchestrator 내부에서 알림센터 저장 + 이벤트 퍼블리시 수행 --- .../service/CommentCreateService.java | 17 ++++++++--------- .../application/service/CommentLikeService.java | 12 ++++++------ .../application/service/FeedCreateService.java | 6 +++--- .../application/service/PostLikeService.java | 12 ++++++------ .../application/service/RoomJoinService.java | 10 +++++----- .../service/RoomRecruitCloseService.java | 6 +++--- .../service/RoomStateChangeService.java | 6 +++--- .../service/RecordCreateService.java | 6 +++--- .../application/service/VoteCreateService.java | 6 +++--- .../service/following/UserFollowService.java | 6 +++--- 10 files changed, 43 insertions(+), 44 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 b959bbfb9..af9a0d330 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java @@ -11,8 +11,8 @@ import konkuk.thip.comment.application.service.validator.CommentAuthorizationValidator; import konkuk.thip.comment.domain.Comment; import konkuk.thip.common.exception.InvalidStateException; -import konkuk.thip.message.application.port.out.FeedEventCommandPort; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.post.application.port.out.dto.PostQueryDto; import konkuk.thip.post.domain.CountUpdatable; import konkuk.thip.post.application.service.handler.PostHandler; @@ -26,7 +26,6 @@ import static konkuk.thip.common.exception.code.ErrorCode.INVALID_COMMENT_CREATE; import static konkuk.thip.post.domain.PostType.*; - @Service @RequiredArgsConstructor public class CommentCreateService implements CommentCreateUseCase { @@ -40,8 +39,8 @@ public class CommentCreateService implements CommentCreateUseCase { private final PostHandler postHandler; private final CommentAuthorizationValidator commentAuthorizationValidator; - private final FeedEventCommandPort feedEventCommandPort; - private final RoomEventCommandPort roomEventCommandPort; + private final FeedNotificationOrchestrator feedNotificationOrchestrator; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; @Override @Transactional @@ -96,10 +95,10 @@ private void sendNotificationsToPostWriter(PostQueryDto postQueryDto, User actor if (postQueryDto.postType().equals(FEED.getType())) { // 피드 댓글 알림 이벤트 발행 - feedEventCommandPort.publishFeedCommentedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); + feedNotificationOrchestrator.notifyFeedCommented(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); } else if (postQueryDto.postType().equals(RECORD.getType()) || postQueryDto.postType().equals(VOTE.getType())) { // 모임방 게시글 댓글 알림 이벤트 발행 - roomEventCommandPort.publishRoomPostCommentedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + roomNotificationOrchestrator.notifyRoomPostCommented(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); } } @@ -108,10 +107,10 @@ private void sendNotificationsToParentCommentWriter(PostQueryDto postQueryDto, C if (postQueryDto.postType().equals(FEED.getType())) { // 피드 답글 알림 이벤트 발행 - feedEventCommandPort.publishFeedRepliedEvent(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); + feedNotificationOrchestrator.notifyFeedReplied(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); } else if (postQueryDto.postType().equals(RECORD.getType()) || postQueryDto.postType().equals(VOTE.getType())) { // 모임방 게시글 답글 알림 이벤트 발행 - roomEventCommandPort.publishRoomPostCommentRepliedEvent(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + roomNotificationOrchestrator.notifyRoomPostCommentReplied(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.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 4fc0c4725..40b90d99d 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java @@ -8,8 +8,8 @@ import konkuk.thip.comment.application.port.out.CommentLikeQueryPort; import konkuk.thip.comment.application.service.validator.CommentAuthorizationValidator; import konkuk.thip.comment.domain.Comment; -import konkuk.thip.message.application.port.out.FeedEventCommandPort; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.post.application.port.out.dto.PostQueryDto; import konkuk.thip.post.application.service.handler.PostHandler; import konkuk.thip.post.domain.CountUpdatable; @@ -32,8 +32,8 @@ public class CommentLikeService implements CommentLikeUseCase { private final PostHandler postHandler; private final CommentAuthorizationValidator commentAuthorizationValidator; - private final FeedEventCommandPort feedEventCommandPort; - private final RoomEventCommandPort roomEventCommandPort; + private final FeedNotificationOrchestrator feedNotificationOrchestrator; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; @Override @Transactional @@ -73,11 +73,11 @@ private void sendNotifications(CommentIsLikeCommand command, Comment comment) { User actorUser = userCommandPort.findById(command.userId()); // 좋아요 푸쉬알림 전송 if (comment.getPostType() == PostType.FEED) { - feedEventCommandPort.publishFeedCommentLikedEvent(comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), comment.getTargetPostId()); + 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()); - roomEventCommandPort.publishRoomCommentLikedEvent(comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + roomNotificationOrchestrator.notifyRoomCommentLiked(comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); } } } 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 a1aea4ac4..4d61ce21d 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java @@ -12,7 +12,7 @@ import konkuk.thip.feed.domain.value.ContentList; import konkuk.thip.feed.domain.value.Tag; import konkuk.thip.feed.domain.value.TagList; -import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; import konkuk.thip.user.application.port.out.UserCommandPort; import konkuk.thip.user.application.port.out.UserQueryPort; import konkuk.thip.user.domain.User; @@ -33,7 +33,7 @@ public class FeedCreateService implements FeedCreateUseCase { private final UserCommandPort userCommandPort; private final ImageUrlValidationService imageUrlValidationService; - private final FeedEventCommandPort feedEventCommandPort; + private final FeedNotificationOrchestrator feedNotificationOrchestrator; @Override @Transactional @@ -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) { - feedEventCommandPort.publishFolloweeNewPostEvent(targetUser.getId(), actorUser.getId(), actorUser.getNickname(), savedFeedId); + feedNotificationOrchestrator.notifyFolloweeNewPost(targetUser.getId(), actorUser.getId(), actorUser.getNickname(), savedFeedId); } } 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 4ab27d818..4d0004ab3 100644 --- a/src/main/java/konkuk/thip/post/application/service/PostLikeService.java +++ b/src/main/java/konkuk/thip/post/application/service/PostLikeService.java @@ -1,7 +1,7 @@ package konkuk.thip.post.application.service; -import konkuk.thip.message.application.port.out.FeedEventCommandPort; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.post.application.port.out.dto.PostQueryDto; import konkuk.thip.post.application.service.handler.PostHandler; import konkuk.thip.post.domain.CountUpdatable; @@ -31,8 +31,8 @@ public class PostLikeService implements PostLikeUseCase { private final PostCountService postCountService; private final PostLikeAuthorizationValidator postLikeAuthorizationValidator; - private final FeedEventCommandPort feedEventCommandPort; - private final RoomEventCommandPort roomEventCommandPort; + private final FeedNotificationOrchestrator feedNotificationOrchestrator; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; @Override @Transactional @@ -74,10 +74,10 @@ private void sendNotifications(PostIsLikeCommand command) { User actorUser = userCommandPort.findById(command.userId()); // 좋아요 푸쉬알림 전송 if (command.postType() == PostType.FEED) { - feedEventCommandPort.publishFeedLikedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); + feedNotificationOrchestrator.notifyFeedLiked(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); } if (command.postType() == PostType.RECORD || command.postType() == PostType.VOTE) { - roomEventCommandPort.publishRoomPostLikedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + roomNotificationOrchestrator.notifyRoomPostLiked(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); } } } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java b/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java index 3f4167821..409887131 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java @@ -2,7 +2,7 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.room.application.port.in.RoomJoinUseCase; import konkuk.thip.room.application.port.in.dto.RoomJoinCommand; import konkuk.thip.room.application.port.in.dto.RoomJoinResult; @@ -27,7 +27,7 @@ public class RoomJoinService implements RoomJoinUseCase { private final RoomParticipantCommandPort roomParticipantCommandPort; private final UserCommandPort userCommandPort; - private final RoomEventCommandPort roomEventCommandPort; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; @Override @Transactional @@ -45,7 +45,7 @@ public RoomJoinResult changeJoinState(RoomJoinCommand roomJoinCommand) { // 방 참여 상태 변경 요청에 따라 분기 처리 switch (type) { case JOIN -> handleJoin(roomJoinCommand, roomParticipantOptional, room); - case CANCEL -> handleCancel(roomJoinCommand, roomParticipantOptional, roomParticipantOptional, room); + case CANCEL -> handleCancel(roomJoinCommand, roomParticipantOptional, room); } // 방의 상태 업데이트 @@ -62,10 +62,10 @@ public RoomJoinResult changeJoinState(RoomJoinCommand roomJoinCommand) { private void sendNotifications(RoomJoinCommand roomJoinCommand, Room room) { RoomParticipant targetUser = roomParticipantCommandPort.findHostByRoomId(room.getId()); User actorUser = userCommandPort.findById(roomJoinCommand.userId()); - roomEventCommandPort.publishRoomJoinEventToHost(targetUser.getUserId(), room.getId(), room.getTitle(), actorUser.getId(), actorUser.getNickname()); + roomNotificationOrchestrator.notifyRoomJoinToHost(targetUser.getUserId(), room.getId(), room.getTitle(), actorUser.getId(), actorUser.getNickname()); } - private void handleCancel(RoomJoinCommand roomJoinCommand, Optional participantOptional, Optional roomParticipantOptional, Room room) { + private void handleCancel(RoomJoinCommand roomJoinCommand, Optional participantOptional, Room room) { // 참여하지 않은 상태 RoomParticipant participant = participantOptional.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_PARTICIPATED_CANNOT_CANCEL) diff --git a/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java b/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java index 0b8140a9a..6073a0cf7 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java @@ -2,7 +2,7 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.room.application.port.in.RoomRecruitCloseUseCase; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; @@ -21,7 +21,7 @@ public class RoomRecruitCloseService implements RoomRecruitCloseUseCase { private final RoomParticipantCommandPort roomParticipantCommandPort; private final RoomCommandPort roomCommandPort; - private final RoomEventCommandPort roomEventCommandPort; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; @Override @Transactional @@ -51,7 +51,7 @@ private void sendNotifications(Long roomId, Room room) { List actorUsers = roomParticipantCommandPort.findAllByRoomId(roomId); for (RoomParticipant participant : actorUsers) { if(participant.isHost()) continue; // 호스트는 제외 - roomEventCommandPort.publishRoomRecruitClosedEarlyEvent(participant.getUserId(), roomId, room.getTitle()); + roomNotificationOrchestrator.notifyRoomRecruitClosedEarly(participant.getUserId(), roomId, room.getTitle()); } } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java b/src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java index 7bb9604ff..b371df2ea 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java @@ -1,6 +1,6 @@ package konkuk.thip.room.application.service; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.room.application.port.in.RoomStateChangeUseCase; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; @@ -22,7 +22,7 @@ public class RoomStateChangeService implements RoomStateChangeUseCase { private final RoomCommandPort roomCommandPort; private final RoomParticipantCommandPort roomParticipantCommandPort; - private final RoomEventCommandPort roomEventCommandPort; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; /** * end_date < 오늘 => EXPIRED @@ -54,7 +54,7 @@ private void sendNotifications() { for (Room room : targetRooms) { List targetUsers = roomParticipantCommandPort.findAllByRoomId(room.getId()); for (RoomParticipant participant : targetUsers) { - roomEventCommandPort.publishRoomActivityStartedEvent(participant.getUserId(), room.getId(), room.getTitle()); + roomNotificationOrchestrator.notifyRoomActivityStarted(participant.getUserId(), room.getId(), room.getTitle()); } } } diff --git a/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java b/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java index 7e075f9cf..6dc6d8cb8 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java @@ -3,7 +3,7 @@ import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.roompost.application.port.in.RecordCreateUseCase; import konkuk.thip.roompost.application.port.in.dto.record.RecordCreateCommand; import konkuk.thip.roompost.application.port.in.dto.record.RecordCreateResult; @@ -38,7 +38,7 @@ public class RecordCreateService implements RecordCreateUseCase { private final RoomParticipantValidator roomParticipantValidator; private final RoomProgressManager roomProgressManager; - private final RoomEventCommandPort roomEventCommandPort; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; @Override @Transactional @@ -82,7 +82,7 @@ private void sendNotifications(RecordCreateCommand command, Room room, Record re List targetUsers = roomParticipantCommandPort.findAllByRoomId(command.roomId()); for (RoomParticipant targetUser : targetUsers) { if (targetUser.getUserId().equals(command.userId())) continue; // 본인 제외 - roomEventCommandPort.publishRoomRecordCreatedEvent(targetUser.getUserId(), actorUser.getId(), actorUser.getNickname(), room.getId(), room.getTitle(), record.getPage(), newRecordId); + roomNotificationOrchestrator.notifyRoomRecordCreated(targetUser.getUserId(), actorUser.getId(), actorUser.getNickname(), room.getId(), room.getTitle(), record.getPage(), newRecordId); } } diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java index ce7cf3788..a91888206 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java @@ -2,7 +2,7 @@ import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; @@ -35,7 +35,7 @@ public class VoteCreateService implements VoteCreateUseCase { private final RoomProgressManager roomProgressManager; - private final RoomEventCommandPort roomEventCommandPort; + private final RoomNotificationOrchestrator roomNotificationOrchestrator; @Transactional @Override @@ -81,7 +81,7 @@ private void sendNotifications(VoteCreateCommand command, Room room, Vote vote, List targetUsers = roomParticipantCommandPort.findAllByRoomId(command.roomId()); for (RoomParticipant targetUser : targetUsers) { if (targetUser.getUserId().equals(command.userId())) continue; // 본인 제외 - roomEventCommandPort.publishRoomVoteStartedEvent(targetUser.getUserId(), room.getId(), room.getTitle(), vote.getPage(), newVoteId); + roomNotificationOrchestrator.notifyRoomVoteStarted(targetUser.getUserId(), room.getId(), room.getTitle(), vote.getPage(), newVoteId); } } diff --git a/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java b/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java index 24e9eeb4c..c0316e510 100644 --- a/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java +++ b/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java @@ -1,7 +1,7 @@ package konkuk.thip.user.application.service.following; import konkuk.thip.common.exception.BusinessException; -import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; import konkuk.thip.user.application.port.in.UserFollowUsecase; import konkuk.thip.user.application.port.in.dto.UserFollowCommand; import konkuk.thip.user.application.port.out.FollowingCommandPort; @@ -23,7 +23,7 @@ public class UserFollowService implements UserFollowUsecase { private final FollowingCommandPort followingCommandPort; private final UserCommandPort userCommandPort; - private final FeedEventCommandPort feedEventCommandPort; + private final FeedNotificationOrchestrator feedNotificationOrchestrator; @Override @Transactional @@ -55,7 +55,7 @@ public Boolean changeFollowingState(UserFollowCommand followCommand) { private void sendNotifications(Long userId, Long targetUserId) { User actorUser = userCommandPort.findById(userId); - feedEventCommandPort.publishFollowEvent(targetUserId, actorUser.getId(), actorUser.getNickname()); + feedNotificationOrchestrator.notifyFollowed(targetUserId, actorUser.getId(), actorUser.getNickname()); } private void validateParams(Long userId, Long targetUserId) { From 3f0918e13e9185ac20bbf1b706d6ca35a08c1d40 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:00:24 +0900 Subject: [PATCH 13/31] =?UTF-8?q?[refactor]=20=EA=B8=B0=EC=A1=B4=20service?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20EventCommandPort=20=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=93=A4=20=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NotificationOrchestrator 구현체를 의존하도록 수정 --- .../room/application/service/RoomJoinServiceTest.java | 8 ++++---- .../application/service/UserFollowServiceTest.java | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java b/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java index 383d0933d..a7ad258f6 100644 --- a/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java +++ b/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java @@ -2,7 +2,7 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; -import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.service.RoomNotificationOrchestratorSyncImpl; import konkuk.thip.room.application.port.in.dto.RoomJoinCommand; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; @@ -32,7 +32,7 @@ class RoomJoinServiceTest { private RoomParticipantCommandPort roomParticipantCommandPort; private RoomJoinService roomJoinService; private UserCommandPort userCommandPort; - private RoomEventCommandPort roomEventCommandPort; + private RoomNotificationOrchestratorSyncImpl roomNotificationOrchestratorSyncImpl; private final Long ROOM_ID = 1L; private final Long USER_ID = 2L; @@ -46,13 +46,13 @@ void setUp() { roomCommandPort = mock(RoomCommandPort.class); roomParticipantCommandPort = mock(RoomParticipantCommandPort.class); userCommandPort = mock(UserCommandPort.class); - roomEventCommandPort = mock(RoomEventCommandPort.class); + roomNotificationOrchestratorSyncImpl = mock(RoomNotificationOrchestratorSyncImpl.class); roomJoinService = new RoomJoinService( roomCommandPort, roomParticipantCommandPort, userCommandPort, - roomEventCommandPort + roomNotificationOrchestratorSyncImpl ); } diff --git a/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java b/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java index 84471a5d9..050b5c692 100644 --- a/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java +++ b/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java @@ -1,7 +1,7 @@ package konkuk.thip.user.application.service; import konkuk.thip.common.exception.BusinessException; -import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.notification.application.service.FeedNotificationOrchestratorSyncImpl; import konkuk.thip.user.application.port.in.dto.UserFollowCommand; import konkuk.thip.user.application.port.out.FollowingCommandPort; import konkuk.thip.user.application.port.out.UserCommandPort; @@ -29,14 +29,14 @@ class UserFollowServiceTest { private UserCommandPort userCommandPort; private UserFollowService userFollowService; - private FeedEventCommandPort feedEventCommandPort; + private FeedNotificationOrchestratorSyncImpl feedNotificationOrchestratorSyncImpl; @BeforeEach void setUp() { followingCommandPort = mock(FollowingCommandPort.class); userCommandPort = mock(UserCommandPort.class); - feedEventCommandPort = mock(FeedEventCommandPort.class); - userFollowService = new UserFollowService(followingCommandPort, userCommandPort, feedEventCommandPort); + feedNotificationOrchestratorSyncImpl = mock(FeedNotificationOrchestratorSyncImpl.class); + userFollowService = new UserFollowService(followingCommandPort, userCommandPort, feedNotificationOrchestratorSyncImpl); } @Nested @@ -157,4 +157,4 @@ private User createUserWithFollowerCount(int count) { .alias(null) .build(); } -} \ No newline at end of file +} From 4b2d4ffa23adc56ef81c0f7c39f134a2e9c911c6 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:01:15 +0900 Subject: [PATCH 14/31] =?UTF-8?q?[refactor]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20teardown=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 클래스 레벨에 트랜잭션이 걸려있으므로 teardown 메서드 삭제 --- .../thip/room/adapter/in/web/RoomCloseJoinApiTest.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java index 2a399c451..42161bbb1 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java @@ -13,7 +13,6 @@ import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -96,14 +95,6 @@ void setup() { roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, member, RoomParticipantRole.MEMBER, 0.0)); } - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("모집 마감 성공 - 방 시작일이 오늘로 바뀜") void closeRoomRecruit_success() throws Exception { From 4b34ea3f82d32aa459c6e75eb0d2ef69ea342bd6 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:02:45 +0900 Subject: [PATCH 15/31] =?UTF-8?q?[refactor]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=BC=ED=84=B0=20=EC=A0=80=EC=9E=A5=EA=B3=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=EC=9E=88=EB=8A=94=20=EA=B8=B0=EC=A1=B4=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=93=A4=EC=9D=98=20teardown=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - userJpaRepository 의 delete 전에 NotificationJpaRepository의 delete 를 선행하도록 코드 수정 --- .../thip/room/adapter/in/web/RoomJoinApiTest.java | 9 ++++++--- .../roompost/adapter/in/web/VoteCreateApiTest.java | 5 +++++ .../application/service/VoteCreateServiceTest.java | 5 +++++ .../thip/user/adapter/in/web/UserFollowApiTest.java | 12 ++++++------ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java index f8e187a2e..ce1c0ff9d 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java @@ -4,6 +4,7 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; import konkuk.thip.room.domain.value.RoomParticipantRole; @@ -48,6 +49,7 @@ class RoomJoinApiTest { @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; @Autowired private BookJpaRepository bookJpaRepository; @Autowired private UserJpaRepository userJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; private RoomJpaEntity room; private UserJpaEntity host; @@ -110,9 +112,10 @@ private void createUsers(Alias alias) { @AfterEach void tearDown() { roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + notificationJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); } @Test diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java index 5cc332584..3275f9b77 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java @@ -5,6 +5,7 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; @@ -66,6 +67,9 @@ class VoteCreateApiTest { @Autowired private VoteItemJpaRepository voteItemJpaRepository; + @Autowired + private NotificationJpaRepository notificationJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; @@ -76,6 +80,7 @@ void tearDown() { roomParticipantJpaRepository.deleteAllInBatch(); roomJpaRepository.deleteAllInBatch(); bookJpaRepository.deleteAllInBatch(); + notificationJpaRepository.deleteAllInBatch(); userJpaRepository.deleteAllInBatch(); } diff --git a/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java b/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java index dbb3603fd..2491a9723 100644 --- a/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java +++ b/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java @@ -3,6 +3,7 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; import konkuk.thip.room.domain.value.RoomParticipantRole; @@ -55,6 +56,9 @@ class VoteCreateServiceTest { @Autowired private VoteCreateService voteCreateService; + @Autowired + private NotificationJpaRepository notificationJpaRepository; + @AfterEach void tearDown() { voteItemJpaRepository.deleteAllInBatch(); @@ -62,6 +66,7 @@ void tearDown() { roomParticipantJpaRepository.deleteAllInBatch(); roomJpaRepository.deleteAllInBatch(); bookJpaRepository.deleteAllInBatch(); + notificationJpaRepository.deleteAllInBatch(); userJpaRepository.deleteAllInBatch(); } diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java index 77814ec56..0344345fa 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java @@ -1,6 +1,7 @@ package konkuk.thip.user.adapter.in.web; import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.domain.value.UserRole; @@ -34,16 +35,15 @@ class UserFollowApiTest { @Autowired private MockMvc mockMvc; - @Autowired - private UserJpaRepository userJpaRepository; - - @Autowired - private FollowingJpaRepository followingJpaRepository; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private FollowingJpaRepository followingJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; @AfterEach void tearDown() { followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAll(); + notificationJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); } @Test From 90b8495d1fe39615a83c41a23ea80cb47474e255 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:03:44 +0900 Subject: [PATCH 16/31] =?UTF-8?q?[rename]=20=EA=B8=B0=EB=A1=9D=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20api=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...RecordCreateControllerTest.java => RecordCreateApiTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/java/konkuk/thip/roompost/adapter/in/web/{RecordCreateControllerTest.java => RecordCreateApiTest.java} (99%) diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateControllerTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateApiTest.java similarity index 99% rename from src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateControllerTest.java rename to src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateApiTest.java index dd779b8e0..fd50178e8 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateControllerTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateApiTest.java @@ -42,7 +42,7 @@ @Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] RecordCommandController 테스트") -class RecordCreateControllerTest { +class RecordCreateApiTest { @Autowired MockMvc mockMvc; From bb8d52bd4f6648f937ed4b60e3a10bcd54298ac7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:04:43 +0900 Subject: [PATCH 17/31] =?UTF-8?q?[move]=20NotificationCategory=20enum?= =?UTF-8?q?=EC=9D=98=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notification 패키지에서 알림센터의 title, content 생성을 담당하므로 notification/domain 하위로 이동 --- .../domain/value}/NotificationCategory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/konkuk/thip/{message/domain => notification/domain/value}/NotificationCategory.java (86%) diff --git a/src/main/java/konkuk/thip/message/domain/NotificationCategory.java b/src/main/java/konkuk/thip/notification/domain/value/NotificationCategory.java similarity index 86% rename from src/main/java/konkuk/thip/message/domain/NotificationCategory.java rename to src/main/java/konkuk/thip/notification/domain/value/NotificationCategory.java index 3280395a2..54408bc75 100644 --- a/src/main/java/konkuk/thip/message/domain/NotificationCategory.java +++ b/src/main/java/konkuk/thip/notification/domain/value/NotificationCategory.java @@ -1,4 +1,4 @@ -package konkuk.thip.message.domain; +package konkuk.thip.notification.domain.value; import lombok.Getter; import lombok.RequiredArgsConstructor; From 597e8cffd8b9d405abd57d13ff9c11e146353b02 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:05:58 +0900 Subject: [PATCH 18/31] =?UTF-8?q?[test]=20=ED=94=BC=EB=93=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=ED=97=AC=ED=8D=BC=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EC=9D=98=20=EB=8B=A8=EC=9C=84/=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...dNotificationOrchestratorSyncImplTest.java | 77 ++++++++ ...ificationOrchestratorSyncImplUnitTest.java | 175 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java create mode 100644 src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java diff --git a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java new file mode 100644 index 000000000..ba94a1361 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java @@ -0,0 +1,77 @@ +package konkuk.thip.notification.application.service; + +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.application.port.in.FeedNotificationOrchestrator; +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.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("[통합] 피드 알림 (동기화 방식) 헬퍼 서비스 통합 테스트") +class FeedNotificationOrchestratorSyncImplTest { + + @Autowired FeedNotificationOrchestrator orchestrator; // 프록시를 타기 위해 인터페이스 타입 주입 + @Autowired NotificationJpaRepository notificationJpaRepository; + @Autowired UserJpaRepository userJpaRepository; + + private Long targetUserId; + + @BeforeEach + void setUp() { + // Notification 저장 시 FK 검사 통과를 위해 대상 유저 하나 만들어 둠 + UserJpaEntity target = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "노성준")); + targetUserId = target.getUserId(); + } + + @AfterEach + void tearDown() { + // 롤백이 아닌 경우를 대비한 안전 정리 + notificationJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("상위 트랜잭션 없이 호출하면 IllegalTransactionStateException 발생 (MANDATORY)") + void mandatory_without_transaction_throws() { + // when & then + assertThatThrownBy(() -> + orchestrator.notifyFeedCommented( + targetUserId, /*actor*/ 999L, "alice", /*feedId*/ 123L + ) + ).isInstanceOf(IllegalTransactionStateException.class); + } + + @Test + @Transactional + @DisplayName("상위 트랜잭션 안에서 호출하면 정상 동작하고, Notification이 저장된다") + void mandatory_with_transaction_succeeds_and_persists() { + // when + orchestrator.notifyFeedCommented( + targetUserId, /*actor*/ 1000L, "bob", /*feedId*/ 777L + ); + + // then (같은 트랜잭션 안에서 즉시 조회 가능) + var all = notificationJpaRepository.findAll(); + assertThat(all).hasSize(1); + + NotificationJpaEntity saved = all.get(0); + assertThat(saved.getUserJpaEntity().getUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains("bob"); + } +} diff --git a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java new file mode 100644 index 000000000..40717b15b --- /dev/null +++ b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java @@ -0,0 +1,175 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.notification.application.port.out.NotificationCommandPort; +import konkuk.thip.notification.domain.Notification; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[단위] 피드 알림 (동기화 방식) 헬퍼 서비스 단위 테스트") +class FeedNotificationOrchestratorSyncImplUnitTest { + + @Mock NotificationCommandPort notificationCommandPort; + @Mock FeedEventCommandPort feedEventCommandPort; + + @InjectMocks FeedNotificationOrchestratorSyncImpl sut; + + @Test + @DisplayName("피드 댓글 알림: DB 저장 + 이벤트 퍼블리시") + void notify_feed_commented_test() { + // given + Long targetUserId = 10L; + Long actorUserId = 20L; + String actorUsername = "alice"; + Long feedId = 99L; + + // when + sut.notifyFeedCommented(targetUserId, actorUserId, actorUsername, feedId); + + // then 1) DB 저장 포트 호출 값 검증 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + // then 2) 이벤트 퍼블리시 포트 호출 검증 + verify(feedEventCommandPort) + .publishFeedCommentedEvent(targetUserId, actorUserId, actorUsername, feedId); + } + + @Test + @DisplayName("팔로우 알림: DB 저장 + 이벤트 퍼블리시") + void notify_followed_test() { + // given + Long targetUserId = 11L; + Long actorUserId = 21L; + String actorUsername = "bob"; + + // when + sut.notifyFollowed(targetUserId, actorUserId, actorUsername); + + // then: DB 저장값 검증 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + // then: 이벤트 퍼블리시 검증 + verify(feedEventCommandPort) + .publishFollowEvent(targetUserId, actorUserId, actorUsername); + } + + @Test + @DisplayName("피드 대댓글 알림: DB 저장 + 이벤트 퍼블리시") + void notify_feed_replied_test() { + // given + Long targetUserId = 12L; + Long actorUserId = 22L; + String actorUsername = "carol"; + Long feedId = 100L; + + // when + sut.notifyFeedReplied(targetUserId, actorUserId, actorUsername, feedId); + + // then: DB 저장값 검증 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + // then: 이벤트 퍼블리시 검증 + verify(feedEventCommandPort) + .publishFeedRepliedEvent(targetUserId, actorUserId, actorUsername, feedId); + } + + @Test + @DisplayName("팔로우한 사람의 새 글 알림: DB 저장 + 이벤트 퍼블리시") + void notify_followee_new_post_test() { + // given + Long targetUserId = 13L; + Long actorUserId = 23L; + String actorUsername = "dave"; + Long feedId = 101L; + + // when + sut.notifyFolloweeNewPost(targetUserId, actorUserId, actorUsername, feedId); + + // then: DB 저장값 검증 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + // then: 이벤트 퍼블리시 검증 + verify(feedEventCommandPort) + .publishFolloweeNewPostEvent(targetUserId, actorUserId, actorUsername, feedId); + } + + @Test + @DisplayName("피드 좋아요 알림: DB 저장 + 이벤트 퍼블리시") + void notify_feed_liked_test() { + // given + Long targetUserId = 14L; + Long actorUserId = 24L; + String actorUsername = "eve"; + Long feedId = 102L; + + // when + sut.notifyFeedLiked(targetUserId, actorUserId, actorUsername, feedId); + + // then: DB 저장값 검증 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + // then: 이벤트 퍼블리시 검증 + verify(feedEventCommandPort) + .publishFeedLikedEvent(targetUserId, actorUserId, actorUsername, feedId); + } + + @Test + @DisplayName("피드 댓글 좋아요 알림: DB 저장 + 이벤트 퍼블리시") + void notify_feed_comment_liked_test() { + // given + Long targetUserId = 15L; + Long actorUserId = 25L; + String actorUsername = "frank"; + Long feedId = 103L; + + // when + sut.notifyFeedCommentLiked(targetUserId, actorUserId, actorUsername, feedId); + + // then: DB 저장값 검증 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + // then: 이벤트 퍼블리시 검증 + verify(feedEventCommandPort) + .publishFeedCommentLikedEvent(targetUserId, actorUserId, actorUsername, feedId); + } +} From f5ceff77dca72907a4a79f951379dd64df1eda1e Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:06:06 +0900 Subject: [PATCH 19/31] =?UTF-8?q?[test]=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=ED=97=AC=ED=8D=BC=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=9D=98=20=EB=8B=A8=EC=9C=84/=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(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...mNotificationOrchestratorSyncImplTest.java | 92 +++++++ ...ificationOrchestratorSyncImplUnitTest.java | 247 ++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java create mode 100644 src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java diff --git a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java new file mode 100644 index 000000000..48fe3a9c5 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java @@ -0,0 +1,92 @@ +package konkuk.thip.notification.application.service; + +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.application.port.in.RoomNotificationOrchestrator; +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.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("[통합] 모임방 알림 (동기화 방식) 헬퍼 서비스 통합 테스트") +class RoomNotificationOrchestratorSyncImplTest { + + @Autowired RoomNotificationOrchestrator orchestrator; // 반드시 인터페이스 타입으로 주입(트랜잭션 프록시 적용) + @Autowired NotificationJpaRepository notificationJpaRepository; + @Autowired UserJpaRepository userJpaRepository; + + private Long targetUserId; + + @BeforeEach + void setUp() { + // Notification FK를 만족시키기 위한 대상 사용자 준비 + UserJpaEntity target = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "노성준")); + targetUserId = target.getUserId(); + } + + @AfterEach + void tearDown() { + notificationJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("상위 트랜잭션 없이 호출하면 IllegalTransactionStateException 발생 (MANDATORY)") + void mandatory_without_transaction_throws() { + // given + Long actorUserId = 200L; + String actorUsername = "carol"; + Long roomId = 11L; + Integer page = 1; + Long postId = 22L; + String postType = "RECORD"; + + // when & then + assertThatThrownBy(() -> + orchestrator.notifyRoomPostCommented( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ) + ).isInstanceOf(IllegalTransactionStateException.class); + } + + @Test + @Transactional // 상위 트랜잭션 존재 + @DisplayName("상위 트랜잭션 안에서 호출하면 정상 동작하고, Notification이 저장된다") + void mandatory_with_transaction_succeeds_and_persists() { + // given + Long actorUserId = 201L; + String actorUsername = "dave"; + Long roomId = 12L; + Integer page = 3; + Long postId = 33L; + String postType = "RECORD"; + + // when + orchestrator.notifyRoomPostCommented( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ); + + // then + var all = notificationJpaRepository.findAll(); + assertThat(all).hasSize(1); + + NotificationJpaEntity saved = all.get(0); + assertThat(saved.getUserJpaEntity().getUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + } +} diff --git a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java new file mode 100644 index 000000000..af063cc90 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java @@ -0,0 +1,247 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.notification.application.port.out.NotificationCommandPort; +import konkuk.thip.notification.domain.Notification; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[단위] 모임방 알림 (동기화 방식) 헬퍼 서비스 단위 테스트") +class RoomNotificationOrchestratorSyncImplUnitTest { + + @Mock NotificationCommandPort notificationCommandPort; + @Mock RoomEventCommandPort roomEventCommandPort; + + @InjectMocks RoomNotificationOrchestratorSyncImpl sut; + + @Test + @DisplayName("모임방 게시글에 댓글: DB 저장 + 이벤트 퍼블리시") + void notify_room_post_commented() { + // given + Long targetUserId = 10L; + Long actorUserId = 20L; + String actorUsername = "alice"; + Long roomId = 1L; int page = 2; Long postId = 3L; String postType = "RECORD"; + + // when + sut.notifyRoomPostCommented(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); + + // then 1) DB 저장값 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + // then 2) 이벤트 퍼블리시 + verify(roomEventCommandPort).publishRoomPostCommentedEvent( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ); + } + + @Test + @DisplayName("모임방 투표 시작: DB 저장 + 이벤트 퍼블리시") + void notify_room_vote_started() { + // given + Long targetUserId = 11L; + Long roomId = 101L; String roomTitle = "독서방"; int page = 1; Long postId = 999L; + + // when + sut.notifyRoomVoteStarted(targetUserId, roomId, roomTitle, page, postId); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).contains(roomTitle); + assertThat(saved.getContent()).isNotBlank(); + + verify(roomEventCommandPort).publishRoomVoteStartedEvent( + targetUserId, roomId, roomTitle, page, postId + ); + } + + @Test + @DisplayName("모임방 기록 작성: DB 저장 + 이벤트 퍼블리시") + void notify_room_record_created() { + // given + Long targetUserId = 12L; + Long actorUserId = 22L; + String actorUsername = "bob"; + Long roomId = 201L; String roomTitle = "역사방"; int page = 3; Long postId = 1001L; + + // when + sut.notifyRoomRecordCreated(targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + verify(roomEventCommandPort).publishRoomRecordCreatedEvent( + targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId + ); + } + + @Test + @DisplayName("모집 조기 마감: DB 저장 + 이벤트 퍼블리시") + void notify_room_recruit_closed_early() { + // given + Long targetUserId = 13L; + Long roomId = 301L; String roomTitle = "과학방"; + + // when + sut.notifyRoomRecruitClosedEarly(targetUserId, roomId, roomTitle); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).contains(roomTitle); + assertThat(saved.getContent()).isNotBlank(); + + verify(roomEventCommandPort).publishRoomRecruitClosedEarlyEvent( + targetUserId, roomId, roomTitle + ); + } + + @Test + @DisplayName("모임 활동 시작: DB 저장 + 이벤트 퍼블리시") + void notify_room_activity_started() { + // given + Long targetUserId = 14L; + Long roomId = 401L; String roomTitle = "문학방"; + + // when + sut.notifyRoomActivityStarted(targetUserId, roomId, roomTitle); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).contains(roomTitle); + assertThat(saved.getContent()).isNotBlank(); + + verify(roomEventCommandPort).publishRoomActivityStartedEvent( + targetUserId, roomId, roomTitle + ); + } + + @Test + @DisplayName("호스트에게 참여 알림: DB 저장 + 이벤트 퍼블리시") + void notify_room_join_to_host() { + // given + Long hostUserId = 15L; + Long actorUserId = 25L; + String actorUsername = "carol"; + Long roomId = 501L; String roomTitle = "미술방"; + + // when + sut.notifyRoomJoinToHost(hostUserId, roomId, roomTitle, actorUserId, actorUsername); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(hostUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + verify(roomEventCommandPort).publishRoomJoinEventToHost( + hostUserId, roomId, roomTitle, actorUserId, actorUsername + ); + } + + @Test + @DisplayName("모임 댓글 좋아요: DB 저장 + 이벤트 퍼블리시") + void notify_room_comment_liked() { + // given + Long targetUserId = 16L; + Long actorUserId = 26L; + String actorUsername = "dave"; + Long roomId = 601L; int page = 9; Long postId = 777L; String postType = "RECORD"; + + // when + sut.notifyRoomCommentLiked(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + verify(roomEventCommandPort).publishRoomCommentLikedEvent( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ); + } + + @Test + @DisplayName("모임 게시글 좋아요: DB 저장 + 이벤트 퍼블리시") + void notify_room_post_liked() { + // given + Long targetUserId = 17L; + Long actorUserId = 27L; + String actorUsername = "erin"; + Long roomId = 701L; int page = 5; Long postId = 888L; String postType = "RECORD"; + + // when + sut.notifyRoomPostLiked(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + verify(roomEventCommandPort).publishRoomPostLikedEvent( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ); + } + + @Test + @DisplayName("모임 대댓글: DB 저장 + 이벤트 퍼블리시") + void notify_room_post_comment_replied() { + // given + Long targetUserId = 18L; + Long actorUserId = 28L; + String actorUsername = "frank"; + Long roomId = 801L; int page = 6; Long postId = 999L; String postType = "RECORD"; + + // when + sut.notifyRoomPostCommentReplied(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(notificationCommandPort).save(captor.capture()); + + Notification saved = captor.getValue(); + assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); + assertThat(saved.getTitle()).isNotBlank(); + assertThat(saved.getContent()).contains(actorUsername); + + verify(roomEventCommandPort).publishRoomPostCommentRepliedEvent( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ); + } +} From 48bacfb1ea9a34c38b472e750d637da574d6c111 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:06:18 +0900 Subject: [PATCH 20/31] =?UTF-8?q?[remove]=20dummy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationQueryPersistenceAdapter.java | 3 +-- .../application/port/in/DummyUseCase.java | 5 ----- .../application/port/out/NotificationQueryPort.java | 5 ----- .../application/service/NotificationService.java | 11 ----------- 4 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 src/main/java/konkuk/thip/notification/application/port/in/DummyUseCase.java delete mode 100644 src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java delete mode 100644 src/main/java/konkuk/thip/notification/application/service/NotificationService.java diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java index 1b6163c39..469395bc3 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java @@ -2,13 +2,12 @@ import konkuk.thip.notification.adapter.out.mapper.NotificationMapper; import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; -import konkuk.thip.notification.application.port.out.NotificationQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @Repository @RequiredArgsConstructor -public class NotificationQueryPersistenceAdapter implements NotificationQueryPort { +public class NotificationQueryPersistenceAdapter { private final NotificationJpaRepository jpaRepository; private final NotificationMapper notificationMapper; diff --git a/src/main/java/konkuk/thip/notification/application/port/in/DummyUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/DummyUseCase.java deleted file mode 100644 index 08474ce86..000000000 --- a/src/main/java/konkuk/thip/notification/application/port/in/DummyUseCase.java +++ /dev/null @@ -1,5 +0,0 @@ -package konkuk.thip.notification.application.port.in; - -public interface DummyUseCase { - -} diff --git a/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java b/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java deleted file mode 100644 index ac161135f..000000000 --- a/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java +++ /dev/null @@ -1,5 +0,0 @@ -package konkuk.thip.notification.application.port.out; - -public interface NotificationQueryPort { - -} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationService.java deleted file mode 100644 index 064c0a7ec..000000000 --- a/src/main/java/konkuk/thip/notification/application/service/NotificationService.java +++ /dev/null @@ -1,11 +0,0 @@ -package konkuk.thip.notification.application.service; - -import konkuk.thip.notification.application.port.in.DummyUseCase; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class NotificationService implements DummyUseCase { - -} From 983abafd127a5b8b0a0dd1e63564b07b197d1809 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:07:23 +0900 Subject: [PATCH 21/31] =?UTF-8?q?[fix]=20FirebaseAdapter=EC=9D=98=20profil?= =?UTF-8?q?e=20=EC=84=A4=EC=A0=95=20=EB=AC=B8=EB=B2=95=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/message/adapter/out/firebase/FirebaseAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java index f8587d743..f907ceb6a 100644 --- a/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java +++ b/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java @@ -13,7 +13,7 @@ @Slf4j @Component -@Profile({"!test", "!local"}) +@Profile("!test & !local") @RequiredArgsConstructor public class FirebaseAdapter implements FirebaseMessagingPort { From 05256bb88ad412c36ca8ed83d18471830f8b3f77 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 12:19:51 +0900 Subject: [PATCH 22/31] =?UTF-8?q?[refactor]=20SecurityConfig,=20JwtAuthent?= =?UTF-8?q?icationFilter=20=EC=97=90=EC=84=9C=20=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20whitelist=20=EB=A5=BC=20=EB=B0=94=EB=9D=BC?= =?UTF-8?q?=EB=B3=B4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일단 whitelist enum 객체를 도입 - 필요하다면 yml 에 명시하고, 환경변수로 주입받아도 될 듯 --- .../security/constant/SecurityWhitelist.java | 39 +++++++++++++++++++ .../filter/JwtAuthenticationFilter.java | 15 ++----- .../konkuk/thip/config/SecurityConfig.java | 14 +------ 3 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java diff --git a/src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java b/src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java new file mode 100644 index 000000000..35d417829 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java @@ -0,0 +1,39 @@ +package konkuk.thip.common.security.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.List; + +@RequiredArgsConstructor +@Getter +public enum SecurityWhitelist { + + SWAGGER_UI("/swagger-ui/**"), + API_DOCS("/api-docs/**"), + SWAGGER_UI_HTML("/swagger-ui.html"), + V3_API_DOCS("/v3/api-docs/**"), + OAUTH2_AUTHORIZATION("/oauth2/authorization/**"), + LOGIN_OAUTH2_CODE("/login/oauth2/code/**"), + ACTUATOR_HEALTH("/actuator/health"), + AUTH_USERS("/auth/users"), + AUTH_TOKEN("/auth/token"), + API_TEST("/api/test/**"), + AUTH_EXCHANGE_TEMP_TOKEN("/auth/exchange-temp-token"), + AUTH_SET_COOKIE("/auth/set-cookie"); + + private final String pattern; + + // SecurityConfig 용 전체 리스트 + public static String[] patterns() { + return Arrays.stream(values()) + .map(SecurityWhitelist::getPattern) + .toArray(String[]::new); + } + + // JwtAuthenticationFilter.shouldNotFilter() 용 편의 메서드 + public static List patternsList() { + return Arrays.asList(patterns()); + } +} diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java index 47cabf33b..38916bf03 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.security.constant.SecurityWhitelist; import konkuk.thip.common.security.oauth2.CustomOAuth2User; import konkuk.thip.common.security.oauth2.LoginUser; import konkuk.thip.common.security.util.JwtUtil; @@ -98,18 +99,8 @@ protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); // 화이트리스트 경로에 대해서는 JWT 필터 제외 - return path.startsWith("/swagger-ui") - || path.startsWith("/v3/api-docs") - || path.startsWith("/api-docs") - || path.startsWith("/actuator/health") - || path.startsWith("/oauth2/authorization") - || path.startsWith("/login/oauth2/code") - || path.startsWith("/auth/users") - || path.equals("/auth/token") - -// || path.equals("/auth/set-cookie") -// || path.equals("/auth/exchange-temp-token") - ; + return SecurityWhitelist.patternsList().stream() + .anyMatch(path::startsWith); } } diff --git a/src/main/java/konkuk/thip/config/SecurityConfig.java b/src/main/java/konkuk/thip/config/SecurityConfig.java index fa4cbbe98..9ee8472fe 100644 --- a/src/main/java/konkuk/thip/config/SecurityConfig.java +++ b/src/main/java/konkuk/thip/config/SecurityConfig.java @@ -1,5 +1,6 @@ package konkuk.thip.config; +import konkuk.thip.common.security.constant.SecurityWhitelist; import konkuk.thip.common.security.filter.JwtAuthenticationEntryPoint; import konkuk.thip.common.security.filter.JwtAuthenticationFilter; import konkuk.thip.common.security.oauth2.CustomOAuth2UserService; @@ -48,17 +49,6 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final CustomSuccessHandler customSuccessHandler; - private static final String[] WHITELIST = { - "/swagger-ui/**", "/api-docs/**", "/swagger-ui.html", - "/v3/api-docs/**","/oauth2/authorization/**", - "/login/oauth2/code/**", "/actuator/health", - "/auth/users", "/auth/token", - - "/api/test/**", // for test - - "/auth/exchange-temp-token", "/auth/set-cookie", // deprecated - }; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -75,7 +65,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .successHandler(customSuccessHandler) // OAuth2 로그인 성공 시 처리 ) .authorizeHttpRequests(auth -> auth - .requestMatchers(WHITELIST).permitAll() + .requestMatchers(SecurityWhitelist.patterns()).permitAll() .anyRequest().authenticated() ) .exceptionHandling(handler -> handler.authenticationEntryPoint(jwtAuthenticationEntryPoint)) From 3f9abb955d5f71599da97fd788be8989909eeeaa Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 16:48:59 +0900 Subject: [PATCH 23/31] =?UTF-8?q?[refactor]=20EventCommandPort=20=EC=9D=98?= =?UTF-8?q?=20=EB=AA=A8=EB=93=A0=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8B=9C?= =?UTF-8?q?=EA=B7=B8=EB=8B=88=EC=B2=98=EC=97=90=20title,=20content=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#2?= =?UTF-8?q?96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/out/FeedEventCommandPort.java | 34 +++++++++---- .../port/out/RoomEventCommandPort.java | 50 +++++++++++++------ 2 files changed, 57 insertions(+), 27 deletions(-) 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 0fbc5631d..aa9b0433f 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 @@ -3,25 +3,37 @@ public interface FeedEventCommandPort { // 누군가 나를 팔로우하는 경우 - void publishFollowEvent(Long targetUserId, Long actorUserId, String actorUsername); + void publishFollowEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername); // 누군가 내 피드에 댓글을 다는 경우 - void publishFeedCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + void publishFeedCommentedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); // 누군가 내 댓글에 대댓글을 다는 경우 - void publishFeedRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + void publishFeedRepliedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); // 내가 팔로우하는 사람이 새 글을 올리는 경우 - void publishFolloweeNewPostEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + void publishFolloweeNewPostEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); // 내 피드가 좋아요를 받는 경우 - void publishFeedLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + void publishFeedLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); // 내 피드 댓글이 좋아요를 받는 경우 - void publishFeedCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId); + void publishFeedCommentLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); } \ 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 b840969a1..a52c7c990 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 @@ -3,36 +3,54 @@ public interface RoomEventCommandPort { // 내 모임방 기록/투표에 댓글이 달린 경우 - void publishRoomPostCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + void publishRoomPostCommentedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); // 내가 참여한 모임방에 새로운 투표가 시작된 경우 - void publishRoomVoteStartedEvent(Long targetUserId, Long roomId, String roomTitle, - Integer page, Long postId); + void publishRoomVoteStartedEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle, + Integer page, Long postId); // 내가 참여한 모임방에 새로운 기록이 작성된 경우 - void publishRoomRecordCreatedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, String roomTitle, Integer page, Long postId); + void publishRoomRecordCreatedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId); // 내가 참여한 모임방이 조기 종료된 경우 (호스트가 모집 마감 버튼 누른 경우) - void publishRoomRecruitClosedEarlyEvent(Long targetUserId, Long roomId, String roomTitle); + void publishRoomRecruitClosedEarlyEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle); // 내가 참여한 모임방 활동이 시작된 경우 (방이 시작 기간이 되어 자동으로 시작된 경우) - void publishRoomActivityStartedEvent(Long targetUserId, Long roomId, String roomTitle); + void publishRoomActivityStartedEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle); // 내가 방장일 때, 새로운 사용자가 모임방 참여를 한 경우 - void publishRoomJoinEventToHost(Long hostUserId, Long roomId, String roomTitle, - Long actorUserId, String actorUsername); + void publishRoomJoinEventToHost( + String title, String content, + Long hostUserId, Long roomId, String roomTitle, + Long actorUserId, String actorUsername); // 내가 참여한 모임방의 나의 댓글이 좋아요를 받는 경우 - void publishRoomCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + void publishRoomCommentLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); // 내가 참여한 모임방 안의 나의 기록/투표가 좋아요를 받는 경우 - void publishRoomPostLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + void publishRoomPostLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); // 내가 참여한 모임방의 나의 댓글에 대댓글이 달린 경우 - void publishRoomPostCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType); + void publishRoomPostCommentRepliedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); } \ No newline at end of file From b07e0a54c960225f77a94daacc7054f9c686259d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 16:49:29 +0900 Subject: [PATCH 24/31] =?UTF-8?q?[refactor]=20EventCommandPort=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=B2=B4=EC=9D=98=20=EB=AA=A8=EB=93=A0=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EC=97=90=20title,=20content=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/event/FeedEventPublisherAdapter.java | 46 +++++++++---- .../out/event/RoomEventPublisherAdapter.java | 66 ++++++++++++++----- 2 files changed, 86 insertions(+), 26 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 b826b181e..f210e480b 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 @@ -13,8 +13,12 @@ public class FeedEventPublisherAdapter implements FeedEventCommandPort { private final ApplicationEventPublisher publisher; @Override - public void publishFollowEvent(Long targetUserId, Long actorUserId, String actorUsername) { + public void publishFollowEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername) { publisher.publishEvent(FeedEvents.FollowerEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -22,9 +26,13 @@ public void publishFollowEvent(Long targetUserId, Long actorUserId, String actor } @Override - public void publishFeedCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + public void publishFeedCommentedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { publisher.publishEvent(FeedEvents.FeedCommentedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -33,9 +41,13 @@ public void publishFeedCommentedEvent(Long targetUserId, Long actorUserId, Strin } @Override - public void publishFeedRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + public void publishFeedRepliedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { publisher.publishEvent(FeedEvents.FeedCommentRepliedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -44,9 +56,13 @@ public void publishFeedRepliedEvent(Long targetUserId, Long actorUserId, String } @Override - public void publishFolloweeNewPostEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + public void publishFolloweeNewPostEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { publisher.publishEvent(FeedEvents.FolloweeNewPostEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -55,9 +71,13 @@ public void publishFolloweeNewPostEvent(Long targetUserId, Long actorUserId, Str } @Override - public void publishFeedLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + public void publishFeedLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { publisher.publishEvent(FeedEvents.FeedLikedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -66,9 +86,13 @@ public void publishFeedLikedEvent(Long targetUserId, Long actorUserId, String ac } @Override - public void publishFeedCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long feedId) { + public void publishFeedCommentLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { publisher.publishEvent(FeedEvents.FeedCommentLikedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) 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 c8cf0c5d7..873b42131 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 @@ -13,9 +13,13 @@ public class RoomEventPublisherAdapter implements RoomEventCommandPort { private final ApplicationEventPublisher publisher; @Override - public void publishRoomPostCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + public void publishRoomPostCommentedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { publisher.publishEvent(RoomEvents.RoomPostCommentedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -27,9 +31,13 @@ public void publishRoomPostCommentedEvent(Long targetUserId, Long actorUserId, S } @Override - public void publishRoomVoteStartedEvent(Long targetUserId, Long roomId, String roomTitle, - Integer page, Long postId) { + public void publishRoomVoteStartedEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle, + Integer page, Long postId) { publisher.publishEvent(RoomEvents.RoomVoteStartedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .roomId(roomId) .roomTitle(roomTitle) @@ -39,9 +47,13 @@ public void publishRoomVoteStartedEvent(Long targetUserId, Long roomId, String r } @Override - public void publishRoomRecordCreatedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, String roomTitle, Integer page, Long postId) { + public void publishRoomRecordCreatedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId) { publisher.publishEvent(RoomEvents.RoomRecordCreatedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -53,8 +65,12 @@ public void publishRoomRecordCreatedEvent(Long targetUserId, Long actorUserId, S } @Override - public void publishRoomRecruitClosedEarlyEvent(Long targetUserId, Long roomId, String roomTitle) { + public void publishRoomRecruitClosedEarlyEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle) { publisher.publishEvent(RoomEvents.RoomRecruitClosedEarlyEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .roomId(roomId) .roomTitle(roomTitle) @@ -62,8 +78,12 @@ public void publishRoomRecruitClosedEarlyEvent(Long targetUserId, Long roomId, S } @Override - public void publishRoomActivityStartedEvent(Long targetUserId, Long roomId, String roomTitle) { + public void publishRoomActivityStartedEvent( + String title, String content, + Long targetUserId, Long roomId, String roomTitle) { publisher.publishEvent(RoomEvents.RoomActivityStartedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .roomId(roomId) .roomTitle(roomTitle) @@ -71,9 +91,13 @@ public void publishRoomActivityStartedEvent(Long targetUserId, Long roomId, Stri } @Override - public void publishRoomJoinEventToHost(Long hostUserId, Long roomId, String roomTitle, - Long actorUserId, String actorUsername) { + public void publishRoomJoinEventToHost( + String title, String content, + Long hostUserId, Long roomId, String roomTitle, + Long actorUserId, String actorUsername) { publisher.publishEvent(RoomEvents.RoomJoinRequestedToOwnerEvent.builder() + .title(title) + .content(content) .ownerUserId(hostUserId) .roomId(roomId) .roomTitle(roomTitle) @@ -83,9 +107,13 @@ public void publishRoomJoinEventToHost(Long hostUserId, Long roomId, String room } @Override - public void publishRoomCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + public void publishRoomCommentLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { publisher.publishEvent(RoomEvents.RoomCommentLikedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -97,9 +125,13 @@ public void publishRoomCommentLikedEvent(Long targetUserId, Long actorUserId, St } @Override - public void publishRoomPostLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, - Long roomId, Integer page, Long postId, String postType) { + public void publishRoomPostLikedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { publisher.publishEvent(RoomEvents.RoomPostLikedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) @@ -111,8 +143,12 @@ public void publishRoomPostLikedEvent(Long targetUserId, Long actorUserId, Strin } @Override - public void publishRoomPostCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { + public void publishRoomPostCommentRepliedEvent( + String title, String content, + Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { publisher.publishEvent(RoomEvents.RoomPostCommentRepliedEvent.builder() + .title(title) + .content(content) .targetUserId(targetUserId) .actorUserId(actorUserId) .actorUsername(actorUsername) From bed7c40f161530eb9cb0274383021ef771b9a768 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 16:51:17 +0900 Subject: [PATCH 25/31] =?UTF-8?q?[feat]=20=EA=B3=B5=ED=86=B5=20=ED=97=AC?= =?UTF-8?q?=ED=8D=BC=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=ED=98=95=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EB=8F=84=EC=9E=85=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/EventCommandInvoker.java | 7 ++++ .../service/NotificationSyncExecutor.java | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java diff --git a/src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java b/src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java new file mode 100644 index 000000000..c104814a3 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/EventCommandInvoker.java @@ -0,0 +1,7 @@ +package konkuk.thip.notification.application.service; + +@FunctionalInterface +public interface EventCommandInvoker { + + void publish(String title, String content); +} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java new file mode 100644 index 000000000..5bb6203ae --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java @@ -0,0 +1,35 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.common.annotation.application.HelperService; +import konkuk.thip.notification.application.port.out.NotificationCommandPort; +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.Notification; +import lombok.RequiredArgsConstructor; + +@HelperService +@RequiredArgsConstructor +public class NotificationSyncExecutor { + + private final NotificationCommandPort notificationCommandPort; + + public void execute( + NotificationTemplate template, + T args, + Long targetUserId, + EventCommandInvoker invoker + ) { + String title = template.title(args); + String content = template.content(args); + + // 1. DB 저장 + saveNotification(title, content, targetUserId); + + // 2. 이벤트 퍼블리시 + invoker.publish(title, content); + } + + private void saveNotification(String title, String content, Long targetUserId) { + Notification notification = Notification.withoutId(title, content, targetUserId); + notificationCommandPort.save(notification); + } +} From 64728a7b75c3b98a764b60d28e847e536403957e Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 16:52:22 +0900 Subject: [PATCH 26/31] =?UTF-8?q?[refactor]=20=EA=B8=B0=EC=A1=B4=20Notific?= =?UTF-8?q?ationOrchestrator=20=EA=B5=AC=ED=98=84=EC=B2=B4=EA=B0=80=20?= =?UTF-8?q?=ED=97=AC=ED=8D=BC=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A5=BC=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=A8=EC=9C=BC=EB=A1=9C=EC=8D=A8=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=88=98?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeedNotificationOrchestratorSyncImpl.java | 59 ++++++-------- .../RoomNotificationOrchestratorSyncImpl.java | 77 +++++++++---------- 2 files changed, 62 insertions(+), 74 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 fc17bac01..1e3db8503 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java +++ b/src/main/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImpl.java @@ -3,10 +3,7 @@ import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.message.application.port.out.FeedEventCommandPort; import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; -import konkuk.thip.notification.application.port.out.NotificationCommandPort; -import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.application.service.template.feed.*; -import konkuk.thip.notification.domain.Notification; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -22,37 +19,21 @@ public class FeedNotificationOrchestratorSyncImpl implements FeedNotificationOrc * 2) 푸시 알림은 AFTER_COMMIT 리스너에서 "비동기"로 발송한다. */ - private final NotificationCommandPort notificationCommandPort; + private final NotificationSyncExecutor notificationSyncExecutor; private final FeedEventCommandPort feedEventCommandPort; - // ========================= 공통 헬퍼 ========================= - private void notifyWithTemplate( - NotificationTemplate template, - T args, - Long targetUserId, - Runnable eventPublisher - ) { - String title = template.title(args); - String content = template.content(args); - saveNotification(title, content, targetUserId); - eventPublisher.run(); - } - - private void saveNotification(String title, String content, Long targetUserId) { - Notification notification = Notification.withoutId(title, content, targetUserId); - notificationCommandPort.save(notification); - } - // ========================= Feed 영역 ========================= @Override @Transactional(propagation = Propagation.MANDATORY) public void notifyFollowed(Long targetUserId, Long actorUserId, String actorUsername) { var args = new FollowedTemplate.Args(actorUsername); - notifyWithTemplate( + notificationSyncExecutor.execute( FollowedTemplate.INSTANCE, args, targetUserId, - () -> feedEventCommandPort.publishFollowEvent(targetUserId, actorUserId, actorUsername) + (title, content) -> feedEventCommandPort.publishFollowEvent( + title, content, targetUserId, actorUserId, actorUsername + ) ); } @@ -60,11 +41,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( FeedCommentedTemplate.INSTANCE, args, targetUserId, - () -> feedEventCommandPort.publishFeedCommentedEvent(targetUserId, actorUserId, actorUsername, feedId) + (title, content) -> feedEventCommandPort.publishFeedCommentedEvent( + title, content, targetUserId, actorUserId, actorUsername, feedId + ) ); } @@ -72,11 +55,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( FeedRepliedTemplate.INSTANCE, args, targetUserId, - () -> feedEventCommandPort.publishFeedRepliedEvent(targetUserId, actorUserId, actorUsername, feedId) + (title, content) -> feedEventCommandPort.publishFeedRepliedEvent( + title, content, targetUserId, actorUserId, actorUsername, feedId + ) ); } @@ -84,11 +69,13 @@ public void notifyFeedReplied(Long targetUserId, Long actorUserId, String actorU @Transactional(propagation = Propagation.MANDATORY) public void notifyFolloweeNewPost(Long targetUserId, Long actorUserId, String actorUsername, Long feedId) { var args = new FolloweeNewPostTemplate.Args(actorUsername); - notifyWithTemplate( + notificationSyncExecutor.execute( FolloweeNewPostTemplate.INSTANCE, args, targetUserId, - () -> feedEventCommandPort.publishFolloweeNewPostEvent(targetUserId, actorUserId, actorUsername, feedId) + (title, content) -> feedEventCommandPort.publishFolloweeNewPostEvent( + title, content, targetUserId, actorUserId, actorUsername, feedId + ) ); } @@ -96,11 +83,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( FeedLikedTemplate.INSTANCE, args, targetUserId, - () -> feedEventCommandPort.publishFeedLikedEvent(targetUserId, actorUserId, actorUsername, feedId) + (title, content) -> feedEventCommandPort.publishFeedLikedEvent( + title, content, targetUserId, actorUserId, actorUsername, feedId + ) ); } @@ -108,11 +97,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( FeedCommentLikedTemplate.INSTANCE, args, targetUserId, - () -> feedEventCommandPort.publishFeedCommentLikedEvent(targetUserId, actorUserId, actorUsername, feedId) + (title, content) -> feedEventCommandPort.publishFeedCommentLikedEvent( + title, content, targetUserId, actorUserId, actorUsername, feedId + ) ); } } 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 151fc209d..9162c65d8 100644 --- a/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java +++ b/src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java @@ -3,10 +3,7 @@ import konkuk.thip.common.annotation.application.HelperService; import konkuk.thip.message.application.port.out.RoomEventCommandPort; import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; -import konkuk.thip.notification.application.port.out.NotificationCommandPort; -import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.application.service.template.room.*; -import konkuk.thip.notification.domain.Notification; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -22,38 +19,22 @@ public class RoomNotificationOrchestratorSyncImpl implements RoomNotificationOrc * 2) 푸시 알림은 AFTER_COMMIT 리스너에서 "비동기"로 발송한다. */ - private final NotificationCommandPort notificationCommandPort; + private final NotificationSyncExecutor notificationSyncExecutor; private final RoomEventCommandPort roomEventCommandPort; - // ========================= 공통 헬퍼 ========================= - private void notifyWithTemplate( - NotificationTemplate template, - T args, - Long targetUserId, - Runnable eventPublisher - ) { - String title = template.title(args); - String content = template.content(args); - saveNotification(title, content, targetUserId); - eventPublisher.run(); - } - - private void saveNotification(String title, String content, Long targetUserId) { - Notification notification = Notification.withoutId(title, content, targetUserId); - notificationCommandPort.save(notification); - } - // ========================= Room 영역 ========================= @Override @Transactional(propagation = Propagation.MANDATORY) public void notifyRoomPostCommented(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { var args = new RoomPostCommentedTemplate.Args(actorUsername); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomPostCommentedTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomPostCommentedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + (title, content) -> roomEventCommandPort.publishRoomPostCommentedEvent( + title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ) ); } @@ -61,11 +42,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomVoteStartedTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomVoteStartedEvent(targetUserId, roomId, roomTitle, page, postId) + (title, content) -> roomEventCommandPort.publishRoomVoteStartedEvent( + title, content, targetUserId, roomId, roomTitle, page, postId + ) ); } @@ -74,11 +57,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomRecordCreatedTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomRecordCreatedEvent(targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId) + (title, content) -> roomEventCommandPort.publishRoomRecordCreatedEvent( + title, content, targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId + ) ); } @@ -86,11 +71,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomRecruitClosedEarlyTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomRecruitClosedEarlyEvent(targetUserId, roomId, roomTitle) + (title, content) -> roomEventCommandPort.publishRoomRecruitClosedEarlyEvent( + title, content, targetUserId, roomId, roomTitle + ) ); } @@ -98,11 +85,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomActivityStartedTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomActivityStartedEvent(targetUserId, roomId, roomTitle) + (title, content) -> roomEventCommandPort.publishRoomActivityStartedEvent( + title, content, targetUserId, roomId, roomTitle + ) ); } @@ -110,11 +99,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomJoinToHostTemplate.INSTANCE, args, hostUserId, - () -> roomEventCommandPort.publishRoomJoinEventToHost(hostUserId, roomId, roomTitle, actorUserId, actorUsername) + (title, content) -> roomEventCommandPort.publishRoomJoinEventToHost( + title, content, hostUserId, roomId, roomTitle, actorUserId, actorUsername + ) ); } @@ -123,11 +114,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomCommentLikedTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomCommentLikedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + (title, content) -> roomEventCommandPort.publishRoomCommentLikedEvent( + title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ) ); } @@ -136,11 +129,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomPostLikedTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomPostLikedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + (title, content) -> roomEventCommandPort.publishRoomPostLikedEvent( + title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ) ); } @@ -149,11 +144,13 @@ 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); - notifyWithTemplate( + notificationSyncExecutor.execute( RoomPostCommentRepliedTemplate.INSTANCE, args, targetUserId, - () -> roomEventCommandPort.publishRoomPostCommentRepliedEvent(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType) + (title, content) -> roomEventCommandPort.publishRoomPostCommentRepliedEvent( + title, content, targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ) ); } } From 30bc7c97fa8531da4257c6c8d5016d688ab6caf1 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 16:52:50 +0900 Subject: [PATCH 27/31] =?UTF-8?q?[refactor]=20=EA=B8=B0=EC=A1=B4=20Notific?= =?UTF-8?q?ationOrchestrator=20=EA=B5=AC=ED=98=84=EC=B2=B4=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=88=98=EC=A0=95=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ificationOrchestratorSyncImplUnitTest.java | 158 ++----------- ...ificationOrchestratorSyncImplUnitTest.java | 223 +----------------- 2 files changed, 30 insertions(+), 351 deletions(-) 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 40717b15b..3972f9803 100644 --- a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplUnitTest.java @@ -1,8 +1,6 @@ package konkuk.thip.notification.application.service; import konkuk.thip.message.application.port.out.FeedEventCommandPort; -import konkuk.thip.notification.application.port.out.NotificationCommandPort; -import konkuk.thip.notification.domain.Notification; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,20 +9,19 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @DisplayName("[단위] 피드 알림 (동기화 방식) 헬퍼 서비스 단위 테스트") class FeedNotificationOrchestratorSyncImplUnitTest { - @Mock NotificationCommandPort notificationCommandPort; + @Mock NotificationSyncExecutor notificationSyncExecutor; @Mock FeedEventCommandPort feedEventCommandPort; @InjectMocks FeedNotificationOrchestratorSyncImpl sut; @Test - @DisplayName("피드 댓글 알림: DB 저장 + 이벤트 퍼블리시") + @DisplayName("피드 댓글 알림: NotificationSyncExecutor 실행 (= DB notification 저장 + 이벤트 퍼블리시)") void notify_feed_commented_test() { // given Long targetUserId = 10L; @@ -35,141 +32,20 @@ void notify_feed_commented_test() { // when sut.notifyFeedCommented(targetUserId, actorUserId, actorUsername, feedId); - // then 1) DB 저장 포트 호출 값 검증 - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - // then 2) 이벤트 퍼블리시 포트 호출 검증 - verify(feedEventCommandPort) - .publishFeedCommentedEvent(targetUserId, actorUserId, actorUsername, feedId); - } - - @Test - @DisplayName("팔로우 알림: DB 저장 + 이벤트 퍼블리시") - void notify_followed_test() { - // given - Long targetUserId = 11L; - Long actorUserId = 21L; - String actorUsername = "bob"; - - // when - sut.notifyFollowed(targetUserId, actorUserId, actorUsername); - - // then: DB 저장값 검증 - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - // then: 이벤트 퍼블리시 검증 - verify(feedEventCommandPort) - .publishFollowEvent(targetUserId, actorUserId, actorUsername); - } - - @Test - @DisplayName("피드 대댓글 알림: DB 저장 + 이벤트 퍼블리시") - void notify_feed_replied_test() { - // given - Long targetUserId = 12L; - Long actorUserId = 22L; - String actorUsername = "carol"; - Long feedId = 100L; - - // when - sut.notifyFeedReplied(targetUserId, actorUserId, actorUsername, feedId); - - // then: DB 저장값 검증 - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - // then: 이벤트 퍼블리시 검증 - verify(feedEventCommandPort) - .publishFeedRepliedEvent(targetUserId, actorUserId, actorUsername, feedId); - } - - @Test - @DisplayName("팔로우한 사람의 새 글 알림: DB 저장 + 이벤트 퍼블리시") - void notify_followee_new_post_test() { - // given - Long targetUserId = 13L; - Long actorUserId = 23L; - String actorUsername = "dave"; - Long feedId = 101L; - - // when - sut.notifyFolloweeNewPost(targetUserId, actorUserId, actorUsername, feedId); - - // then: DB 저장값 검증 - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - // then: 이벤트 퍼블리시 검증 - verify(feedEventCommandPort) - .publishFolloweeNewPostEvent(targetUserId, actorUserId, actorUsername, feedId); - } - - @Test - @DisplayName("피드 좋아요 알림: DB 저장 + 이벤트 퍼블리시") - void notify_feed_liked_test() { - // given - Long targetUserId = 14L; - Long actorUserId = 24L; - String actorUsername = "eve"; - Long feedId = 102L; - - // when - sut.notifyFeedLiked(targetUserId, actorUserId, actorUsername, feedId); - - // then: DB 저장값 검증 - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - // then: 이벤트 퍼블리시 검증 - verify(feedEventCommandPort) - .publishFeedLikedEvent(targetUserId, actorUserId, actorUsername, feedId); - } - - @Test - @DisplayName("피드 댓글 좋아요 알림: DB 저장 + 이벤트 퍼블리시") - void notify_feed_comment_liked_test() { - // given - Long targetUserId = 15L; - Long actorUserId = 25L; - String actorUsername = "frank"; - Long feedId = 103L; - - // when - sut.notifyFeedCommentLiked(targetUserId, actorUserId, actorUsername, feedId); - - // then: DB 저장값 검증 - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - // then: 이벤트 퍼블리시 검증 - verify(feedEventCommandPort) - .publishFeedCommentLikedEvent(targetUserId, actorUserId, actorUsername, feedId); + // 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() + ); + + // then: invoker 가 EventCommandPort 메서드를 올바르게 호출하는지 검증 + EventCommandInvoker invoker = invokerCaptor.getValue(); + invoker.publish("title", "content"); + verify(feedEventCommandPort).publishFeedCommentedEvent( + "title", "content", targetUserId, actorUserId, actorUsername, feedId + ); } } 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 af063cc90..93e15df72 100644 --- a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplUnitTest.java @@ -1,8 +1,6 @@ package konkuk.thip.notification.application.service; import konkuk.thip.message.application.port.out.RoomEventCommandPort; -import konkuk.thip.notification.application.port.out.NotificationCommandPort; -import konkuk.thip.notification.domain.Notification; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,20 +9,21 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @DisplayName("[단위] 모임방 알림 (동기화 방식) 헬퍼 서비스 단위 테스트") class RoomNotificationOrchestratorSyncImplUnitTest { - @Mock NotificationCommandPort notificationCommandPort; + @Mock NotificationSyncExecutor notificationSyncExecutor; @Mock RoomEventCommandPort roomEventCommandPort; @InjectMocks RoomNotificationOrchestratorSyncImpl sut; @Test - @DisplayName("모임방 게시글에 댓글: DB 저장 + 이벤트 퍼블리시") + @DisplayName("모임방 게시글에 댓글: NotificationSyncExecutor 실행 (= DB notification 저장 + 이벤트 퍼블리시)") void notify_room_post_commented() { // given Long targetUserId = 10L; @@ -35,213 +34,17 @@ void notify_room_post_commented() { // when sut.notifyRoomPostCommented(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); - // then 1) DB 저장값 - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - // then 2) 이벤트 퍼블리시 - verify(roomEventCommandPort).publishRoomPostCommentedEvent( - targetUserId, actorUserId, actorUsername, roomId, page, postId, postType - ); - } - - @Test - @DisplayName("모임방 투표 시작: DB 저장 + 이벤트 퍼블리시") - void notify_room_vote_started() { - // given - Long targetUserId = 11L; - Long roomId = 101L; String roomTitle = "독서방"; int page = 1; Long postId = 999L; - - // when - sut.notifyRoomVoteStarted(targetUserId, roomId, roomTitle, page, postId); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).contains(roomTitle); - assertThat(saved.getContent()).isNotBlank(); - - verify(roomEventCommandPort).publishRoomVoteStartedEvent( - targetUserId, roomId, roomTitle, page, postId - ); - } - - @Test - @DisplayName("모임방 기록 작성: DB 저장 + 이벤트 퍼블리시") - void notify_room_record_created() { - // given - Long targetUserId = 12L; - Long actorUserId = 22L; - String actorUsername = "bob"; - Long roomId = 201L; String roomTitle = "역사방"; int page = 3; Long postId = 1001L; - - // when - sut.notifyRoomRecordCreated(targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - verify(roomEventCommandPort).publishRoomRecordCreatedEvent( - targetUserId, actorUserId, actorUsername, roomId, roomTitle, page, postId + // then: NotificationSyncExecutor 가 올바르게 호출되었는지 검증 + ArgumentCaptor invokerCaptor = ArgumentCaptor.forClass(EventCommandInvoker.class); + verify(notificationSyncExecutor).execute( + any(), any(), eq(targetUserId), invokerCaptor.capture() ); - } - - @Test - @DisplayName("모집 조기 마감: DB 저장 + 이벤트 퍼블리시") - void notify_room_recruit_closed_early() { - // given - Long targetUserId = 13L; - Long roomId = 301L; String roomTitle = "과학방"; - // when - sut.notifyRoomRecruitClosedEarly(targetUserId, roomId, roomTitle); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).contains(roomTitle); - assertThat(saved.getContent()).isNotBlank(); - - verify(roomEventCommandPort).publishRoomRecruitClosedEarlyEvent( - targetUserId, roomId, roomTitle - ); - } - - @Test - @DisplayName("모임 활동 시작: DB 저장 + 이벤트 퍼블리시") - void notify_room_activity_started() { - // given - Long targetUserId = 14L; - Long roomId = 401L; String roomTitle = "문학방"; - - // when - sut.notifyRoomActivityStarted(targetUserId, roomId, roomTitle); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).contains(roomTitle); - assertThat(saved.getContent()).isNotBlank(); - - verify(roomEventCommandPort).publishRoomActivityStartedEvent( - targetUserId, roomId, roomTitle - ); - } - - @Test - @DisplayName("호스트에게 참여 알림: DB 저장 + 이벤트 퍼블리시") - void notify_room_join_to_host() { - // given - Long hostUserId = 15L; - Long actorUserId = 25L; - String actorUsername = "carol"; - Long roomId = 501L; String roomTitle = "미술방"; - - // when - sut.notifyRoomJoinToHost(hostUserId, roomId, roomTitle, actorUserId, actorUsername); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(hostUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - verify(roomEventCommandPort).publishRoomJoinEventToHost( - hostUserId, roomId, roomTitle, actorUserId, actorUsername - ); - } - - @Test - @DisplayName("모임 댓글 좋아요: DB 저장 + 이벤트 퍼블리시") - void notify_room_comment_liked() { - // given - Long targetUserId = 16L; - Long actorUserId = 26L; - String actorUsername = "dave"; - Long roomId = 601L; int page = 9; Long postId = 777L; String postType = "RECORD"; - - // when - sut.notifyRoomCommentLiked(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - verify(roomEventCommandPort).publishRoomCommentLikedEvent( - targetUserId, actorUserId, actorUsername, roomId, page, postId, postType - ); - } - - @Test - @DisplayName("모임 게시글 좋아요: DB 저장 + 이벤트 퍼블리시") - void notify_room_post_liked() { - // given - Long targetUserId = 17L; - Long actorUserId = 27L; - String actorUsername = "erin"; - Long roomId = 701L; int page = 5; Long postId = 888L; String postType = "RECORD"; - - // when - sut.notifyRoomPostLiked(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - verify(roomEventCommandPort).publishRoomPostLikedEvent( - targetUserId, actorUserId, actorUsername, roomId, page, postId, postType - ); - } - - @Test - @DisplayName("모임 대댓글: DB 저장 + 이벤트 퍼블리시") - void notify_room_post_comment_replied() { - // given - Long targetUserId = 18L; - Long actorUserId = 28L; - String actorUsername = "frank"; - Long roomId = 801L; int page = 6; Long postId = 999L; String postType = "RECORD"; - - // when - sut.notifyRoomPostCommentReplied(targetUserId, actorUserId, actorUsername, roomId, page, postId, postType); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); - verify(notificationCommandPort).save(captor.capture()); - - Notification saved = captor.getValue(); - assertThat(saved.getTargetUserId()).isEqualTo(targetUserId); - assertThat(saved.getTitle()).isNotBlank(); - assertThat(saved.getContent()).contains(actorUsername); - - verify(roomEventCommandPort).publishRoomPostCommentRepliedEvent( - targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + // then: invoker 가 EventCommandPort 메서드를 올바르게 호출하는지 검증 + EventCommandInvoker invoker = invokerCaptor.getValue(); + invoker.publish("title", "content"); + verify(roomEventCommandPort).publishRoomPostCommentedEvent( + "title", "content", targetUserId, actorUserId, actorUsername, roomId, page, postId, postType ); } } From a2102fa8293c8a5699f0f893a24478a26720a436 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 16:53:24 +0900 Subject: [PATCH 28/31] =?UTF-8?q?[refactor]=20=EA=B8=B0=EC=A1=B4=20Notific?= =?UTF-8?q?ationOrchestrator=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=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(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이벤트 리스너가 이벤트를 잘 수신하는지 검증하는 테스트 메서드 추가 --- ...dNotificationOrchestratorSyncImplTest.java | 62 +++++++++++++++ ...mNotificationOrchestratorSyncImplTest.java | 75 +++++++++++++++++++ 2 files changed, 137 insertions(+) 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 ba94a1361..6bfa02b5b 100644 --- a/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/FeedNotificationOrchestratorSyncImplTest.java @@ -1,6 +1,8 @@ package konkuk.thip.notification.application.service; import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.message.adapter.out.event.dto.FeedEvents; +import konkuk.thip.message.application.port.in.FeedNotificationDispatchUseCase; import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; import konkuk.thip.notification.application.port.in.FeedNotificationOrchestrator; @@ -11,14 +13,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.transaction.TestTransaction; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @SpringBootTest @ActiveProfiles("test") @@ -29,6 +37,8 @@ class FeedNotificationOrchestratorSyncImplTest { @Autowired NotificationJpaRepository notificationJpaRepository; @Autowired UserJpaRepository userJpaRepository; + @MockitoBean FeedNotificationDispatchUseCase feedNotificationDispatchUseCase; + private Long targetUserId; @BeforeEach @@ -74,4 +84,56 @@ void mandatory_with_transaction_succeeds_and_persists() { assertThat(saved.getTitle()).isNotBlank(); assertThat(saved.getContent()).contains("bob"); } + + @Test + @Transactional + @DisplayName("커밋 시: AFTER_COMMIT 리스너가 handleFeedCommented 호출 & Notification 커밋됨") + void notifyFeedCommented_afterCommit_listenerInvoked_andNotificationPersisted() { + // given + Long actorUserId = 200L; + String actorUsername = "alice"; + Long feedId = 999L; + + // when (트랜잭션 안) + orchestrator.notifyFeedCommented(targetUserId, actorUserId, actorUsername, feedId); + + // 실제 커밋 트리거 + TestTransaction.flagForCommit(); + TestTransaction.end(); // 여기서 @TransactionalEventListener(AFTER_COMMIT) 실행됨 (테스트 프로필은 동기 실행) + + // then : 리스너에 전달되는 DTO 필드 검증 + ArgumentCaptor captor = + ArgumentCaptor.forClass(FeedEvents.FeedCommentedEvent.class); + verify(feedNotificationDispatchUseCase).handleFeedCommented(captor.capture()); + + 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); + } + + @Test + @Transactional + @DisplayName("롤백 시: AFTER_COMMIT 리스너는 호출되지 않고, Notification도 저장되지 않음") + void notifyFeedCommented_rollback_listenerNotInvoked_andNotificationNotPersisted() { + // given + Long actorUserId = 201L; + String actorUsername = "bob"; + Long feedId = 1000L; + + // when (트랜잭션 안) + orchestrator.notifyFeedCommented(targetUserId, actorUserId, actorUsername, feedId); + + // 롤백 트리거 + TestTransaction.flagForRollback(); + TestTransaction.end(); // 커밋이 아니므로 AFTER_COMMIT 리스너는 실행되지 않음 + + // then + verify(feedNotificationDispatchUseCase, times(0)).handleFeedCommented(any()); + assertThat(notificationJpaRepository.findAll()).isEmpty(); + } } 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 48fe3a9c5..65cd80564 100644 --- a/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImplTest.java @@ -1,6 +1,8 @@ package konkuk.thip.notification.application.service; import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.message.adapter.out.event.dto.RoomEvents; +import konkuk.thip.message.application.port.in.RoomNotificationDispatchUseCase; 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; @@ -11,14 +13,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.transaction.TestTransaction; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @SpringBootTest @ActiveProfiles("test") @@ -29,6 +37,8 @@ class RoomNotificationOrchestratorSyncImplTest { @Autowired NotificationJpaRepository notificationJpaRepository; @Autowired UserJpaRepository userJpaRepository; + @MockitoBean RoomNotificationDispatchUseCase roomNotificationDispatchUseCase; + private Long targetUserId; @BeforeEach @@ -89,4 +99,69 @@ void mandatory_with_transaction_succeeds_and_persists() { assertThat(saved.getTitle()).isNotBlank(); assertThat(saved.getContent()).contains(actorUsername); } + + @Test + @Transactional + @DisplayName("커밋 시: AFTER_COMMIT 리스너가 handleRoomPostCommented 호출 & Notification 커밋됨") + void roomPostCommented_afterCommit_listenerInvoked_andNotificationCommitted() { + // given + Long actorUserId = 301L; + String actorUsername = "alice"; + Long roomId = 1001L; + Integer page = 7; + Long postId = 5001L; + String postType = "RECORD"; + + // when (트랜잭션 안) + orchestrator.notifyRoomPostCommented( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ); + + // 실제 커밋 트리거 → AFTER_COMMIT 리스너 실행 (test 프로필은 @Async 동기화) + TestTransaction.flagForCommit(); + TestTransaction.end(); + + // then : 리스너에 전달되는 DTO 필드 검증 + ArgumentCaptor captor = + ArgumentCaptor.forClass(RoomEvents.RoomPostCommentedEvent.class); + verify(roomNotificationDispatchUseCase).handleRoomPostCommented(captor.capture()); + + 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); + } + + @Test + @Transactional + @DisplayName("롤백 시: AFTER_COMMIT 리스너는 호출되지 않고, Notification도 저장되지 않음") + void roomPostCommented_rollback_listenerNotInvoked_andNotificationNotCommitted() { + // given + Long actorUserId = 302L; + String actorUsername = "bob"; + Long roomId = 1002L; + Integer page = 2; + Long postId = 5002L; + String postType = "RECORD"; + + // when + orchestrator.notifyRoomPostCommented( + targetUserId, actorUserId, actorUsername, roomId, page, postId, postType + ); + + // 롤백 트리거 → AFTER_COMMIT 미실행 + TestTransaction.flagForRollback(); + TestTransaction.end(); + + // then + verify(roomNotificationDispatchUseCase, times(0)).handleRoomPostCommented(any()); + assertThat(notificationJpaRepository.findAll()).isEmpty(); + } } From af159e1a4101f005ed68c197bcbf53d3f614f763 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 13 Sep 2025 17:02:31 +0900 Subject: [PATCH 29/31] =?UTF-8?q?[feat]=20JwtAuthenticationFilter=20?= =?UTF-8?q?=ED=99=94=EC=9D=B4=ED=8A=B8=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=A7=A4=EC=B9=AD=EC=9D=84=20PathPattern?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - '/swagger-ui/**' 와 같은 Ant 스타일 패턴의 화이트리스트 경로를 정확히 처리하기 위해 기존 startsWith 방식 대신 PathPattern 기반 비교 방식으로 변경 - AntPathMatcher 는 deprecated 기능이므로 JwtAuthenticationFilter 내에서 PathPatternParser 를 활용해 직접 매칭 로직 구현 --- .../filter/JwtAuthenticationFilter.java | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java index 38916bf03..51070cf47 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package konkuk.thip.common.security.filter; +import jakarta.annotation.PostConstruct; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -12,13 +13,17 @@ import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.PathContainer; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; import java.io.IOException; +import java.util.List; import static konkuk.thip.common.exception.code.ErrorCode.*; import static konkuk.thip.common.security.constant.AuthParameters.*; @@ -31,6 +36,31 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserTokenBlacklistQueryPort userTokenBlacklistQueryPort; + private List whitelistPatterns; + private final PathPatternParser pathPatternParser = new PathPatternParser(); + + @PostConstruct + void initWhitelistPatterns() { + this.whitelistPatterns = SecurityWhitelist.patternsList().stream() + .map(pathPatternParser::parse) // 애플리케이션 시작 시 1회 컴파일 + .toList(); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 컨텍스트 패스 고려한 실제 경로(= path) 추출 + String requestUri = request.getRequestURI(); + String contextPath = request.getContextPath(); + String path = (contextPath != null && !contextPath.isEmpty() && requestUri.startsWith(contextPath)) + ? requestUri.substring(contextPath.length()) + : requestUri; + + PathContainer container = PathContainer.parsePath(path); + + // PathPattern으로 세그먼트 기반 매칭 + return whitelistPatterns.stream().anyMatch(p -> p.matches(container)); + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @@ -93,14 +123,4 @@ private String extractToken(HttpServletRequest request) { log.info("토큰이 없습니다."); return null; } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String path = request.getRequestURI(); - - // 화이트리스트 경로에 대해서는 JWT 필터 제외 - return SecurityWhitelist.patternsList().stream() - .anyMatch(path::startsWith); - } - } From 4c68d4c612fbbe558c12e281536a7940402b6875 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 14 Sep 2025 01:49:30 +0900 Subject: [PATCH 30/31] =?UTF-8?q?[refactor]=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=8D=BC?= =?UTF-8?q?=EB=B8=94=EB=A6=AC=EC=8B=9C=20=EC=A4=91=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8D=94=EB=9D=BC?= =?UTF-8?q?=EB=8F=84=20=EC=9D=B4=EB=A5=BC=20=EC=99=B8=EB=B6=80=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=98=EC=A7=80=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20try?= =?UTF-8?q?-catch=20=EB=AC=B8=20=EB=8F=84=EC=9E=85=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationSyncExecutor.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 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 5bb6203ae..bff1959b9 100644 --- a/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java @@ -5,14 +5,16 @@ import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.domain.Notification; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @HelperService @RequiredArgsConstructor +@Slf4j public class NotificationSyncExecutor { private final NotificationCommandPort notificationCommandPort; - public void execute( + public void execute( NotificationTemplate template, T args, Long targetUserId, @@ -25,7 +27,14 @@ public void execute( saveNotification(title, content, targetUserId); // 2. 이벤트 퍼블리시 - invoker.publish(title, content); + try { + invoker.publish(title, content); + } catch (Exception e) { + // 이벤트 발행 실패 시, DB에 저장된 알림을 롤백하지는 않음 + // -> 알림 저장은 비즈니스 트랜잭션과 동일한 경계 내에서 수행되므로, 알림 저장은 유지 + // -> 푸시 알림 이벤트 발행이 실패한 경우, 일단 로깅만 추가 + log.error("푸시 알림 이벤트 퍼블리시 실패 targetUserId = {}, title = {}", targetUserId, title, e); + } } private void saveNotification(String title, String content, Long targetUserId) { From 6850a502c05f6faf7d777f304959eacffc37c949 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 14 Sep 2025 01:50:25 +0900 Subject: [PATCH 31/31] =?UTF-8?q?[test]=20NotificationExecutor=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=9E=91=EC=84=B1=20(#296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 푸시 알림 이벤트 퍼블리시 과정에서 예외가 발생할 경우, 이를 외부로 던지지 않음을 단위 테스트 코드로 검증 --- .../service/NotificationSyncExecutorTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java diff --git a/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java new file mode 100644 index 000000000..d988c278d --- /dev/null +++ b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java @@ -0,0 +1,52 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.notification.application.port.out.NotificationCommandPort; +import konkuk.thip.notification.application.service.template.NotificationTemplate; +import konkuk.thip.notification.domain.Notification; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.*; + +@DisplayName("[단위] NotificationSyncExecutor - 동기 알림 저장 및 이벤트 퍼블리시") +class NotificationSyncExecutorTest { + + @Test + @DisplayName("execute() 메서드 : 푸시 알림 이벤트 퍼블리시 과정에서 예외가 발생하더라도 예외를 외부로 던지지 않는다.") + void execute_publish_failure_does_not_throw() { + // given + NotificationCommandPort commandPort = mock(NotificationCommandPort.class); + NotificationSyncExecutor executor = new NotificationSyncExecutor(commandPort); + + // 간단한 템플릿 스텁 (title/content 고정) + NotificationTemplate template = new NotificationTemplate<>() { + @Override + public String title(String args) { return "테스트제목"; } + @Override + public String content(String args) { return "테스트내용"; } + }; + + // publish 호출 시 강제로 예외를 던지는 invoker + EventCommandInvoker invoker = (title, content) -> { + throw new RuntimeException("강제 퍼블리시 실패"); + }; + + // when & then + assertThatCode(() -> + executor.execute(template, "dummyArgs", 123L, invoker) + ).doesNotThrowAnyException(); + + // NotificationCommandPort은 정상적으로 호출되었는지 검증 + ArgumentCaptor captor = ArgumentCaptor.forClass(Notification.class); + verify(commandPort, times(1)).save(captor.capture()); + + Notification saved = captor.getValue(); + // 템플릿에서 설정한 title/content 값이 그대로 들어갔는지 확인 + assertThat(saved.getTitle()).isEqualTo("테스트제목"); + assertThat(saved.getContent()).isEqualTo("테스트내용"); + assertThat(saved.getTargetUserId()).isEqualTo(123L); + } +}