From d1e4cc411b7a4726c4f8850b405cd1eb75910f87 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 04:08:30 +0900 Subject: [PATCH 01/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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/89] =?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 e57dc01d463048f2aaee3ee53262eeda871c7f52 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 15:22:19 +0900 Subject: [PATCH 23/89] =?UTF-8?q?[refactor]=20=ED=8A=B9=EC=A0=95=20?= =?UTF-8?q?=EC=B1=85=EA=B3=BC=20=EA=B4=80=EB=A0=A8=EB=90=9C=20=EB=B0=A9=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=ED=95=84=ED=84=B0=EB=A7=81=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=EC=9D=84=20=EC=A0=95=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/application/service/BookRecruitingRoomsService.java | 4 +--- .../thip/book/application/service/BookSearchService.java | 5 ++--- .../out/persistence/repository/RoomJpaRepository.java | 5 ++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookRecruitingRoomsService.java b/src/main/java/konkuk/thip/book/application/service/BookRecruitingRoomsService.java index fc0fc5d00..21c439a64 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookRecruitingRoomsService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookRecruitingRoomsService.java @@ -11,8 +11,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; - @Service @RequiredArgsConstructor public class BookRecruitingRoomsService implements BookRecruitingRoomsUseCase { @@ -26,7 +24,7 @@ public class BookRecruitingRoomsService implements BookRecruitingRoomsUseCase { @Transactional(readOnly = true) public BookRecruitingRoomsResponse getRecruitingRoomsWithBook(String isbn, String cursorStr) { Integer totalRoomCount = (cursorStr == null || cursorStr.isBlank()) ? // 첫 요청 여부 판단 - roomQueryPort.countRecruitingRoomsByBookAndStartDateAfter(isbn, LocalDate.now()) : null; + roomQueryPort.countRecruitingRoomsByBookIsbn(isbn) : null; Cursor cursor = Cursor.from(cursorStr, DEFAULT_PAGE_SIZE); CursorBasedList roomDtos = roomQueryPort.findRoomsByIsbnOrderByDeadline(isbn, cursor); diff --git a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java index 3493e5d73..617a27d4d 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java @@ -113,9 +113,8 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { } private int getRecruitingRoomCount(Book book) { - //오늘 날짜 기준으로 방 활동 시작 기간이 이후인 방 찾기(모집중인 방) - LocalDate today = LocalDate.now(); - return roomQueryPort.countRecruitingRoomsByBookAndStartDateAfter(book.getIsbn(), today); + // 모집 중인 방 개수 + return roomQueryPort.countRecruitingRoomsByBookIsbn(book.getIsbn()); } private int getReadCount(Book book) { diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java index a41e7e811..905a89a69 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java @@ -6,7 +6,6 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -19,8 +18,8 @@ public interface RoomJpaRepository extends JpaRepository, R @Query("SELECT COUNT(r) FROM RoomJpaEntity r " + "WHERE r.bookJpaEntity.isbn = :isbn " + - "AND r.startDate > :currentDate") - int countActiveRoomsByBookIdAndStartDateAfter(@Param("isbn") String isbn, @Param("currentDate") LocalDate currentDate); + "AND r.roomStatus = 'RECRUITING'") + int countRecruitingRoomsByBookIsbn(@Param("isbn") String isbn); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" From edc58dd49f7b40747e76b2c2211e534a15750952 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 15:24:07 +0900 Subject: [PATCH 24/89] =?UTF-8?q?[fix]=20FIrebaseAdapter=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#289)?= 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 a41646a98b4f069c753d70071528e9b2639f6845 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 15:25:59 +0900 Subject: [PATCH 25/89] =?UTF-8?q?[refactor]=20=EB=B0=A9=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=82=B4=EB=B6=80=20=EB=B0=A9=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=A0=95=EC=A0=81=EC=9D=84=20=EB=B3=80=EA=B2=BD=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/RoomJoinService.java | 9 ++------- .../java/konkuk/thip/room/domain/Room.java | 20 ++++++++----------- 2 files changed, 10 insertions(+), 19 deletions(-) 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..02c0e7676 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java @@ -34,11 +34,11 @@ public class RoomJoinService implements RoomJoinUseCase { public RoomJoinResult changeJoinState(RoomJoinCommand roomJoinCommand) { RoomJoinType type = roomJoinCommand.type(); - // 방이 존재하지 않거나 만료된 경우 + // 방이 존재하지 않거나 모집기간이 만료된 경우 예외 처리 Room room = roomCommandPort.findById(roomJoinCommand.roomId()) .orElseThrow(() -> new BusinessException(ErrorCode.USER_CANNOT_JOIN_OR_CANCEL)); - validateRoom(room); + room.validateRoomRecruitExpired(); Optional roomParticipantOptional = roomParticipantCommandPort.findByUserIdAndRoomIdOptional(roomJoinCommand.userId(), roomJoinCommand.roomId()); @@ -93,11 +93,6 @@ private void handleJoin(RoomJoinCommand roomJoinCommand, Optional Date: Fri, 12 Sep 2025 15:26:44 +0900 Subject: [PATCH 26/89] =?UTF-8?q?[refactor]=20=EB=B0=A9=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EA=B8=B0=EA=B0=84=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=A1=B0=EA=B1=B4=20=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EC=A0=95=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#2?= =?UTF-8?q?89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomQueryPersistenceAdapter.java | 4 +- .../repository/RoomQueryRepositoryImpl.java | 49 ++++++++++--------- .../application/port/out/RoomQueryPort.java | 5 +- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java index 5fa848e00..237172e33 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java @@ -25,8 +25,8 @@ public class RoomQueryPersistenceAdapter implements RoomQueryPort { private final RoomJpaRepository roomJpaRepository; @Override - public int countRecruitingRoomsByBookAndStartDateAfter(String isbn, LocalDate currentDate) { - return roomJpaRepository.countActiveRoomsByBookIdAndStartDateAfter(isbn, currentDate); + public int countRecruitingRoomsByBookIsbn(String isbn) { + return roomJpaRepository.countRecruitingRoomsByBookIsbn(isbn); } @Override diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java index b708d4cc3..6aeacf18f 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java @@ -19,6 +19,7 @@ import konkuk.thip.room.application.port.out.dto.RoomParticipantQueryDto; import konkuk.thip.room.application.port.out.dto.RoomQueryDto; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -35,9 +36,9 @@ public class RoomQueryRepositoryImpl implements RoomQueryRepository { private final QRoomParticipantJpaEntity participant = QRoomParticipantJpaEntity.roomParticipantJpaEntity; /** 모집중 + ACTIVE 공통 where */ - private BooleanBuilder recruitingActiveWhere(LocalDate today) { + private BooleanBuilder recruitingActiveWhere() { BooleanBuilder where = new BooleanBuilder(); - where.and(room.startDate.after(today)); + where.and(room.roomStatus.eq(RoomStatus.RECRUITING)); return where; } @@ -91,10 +92,9 @@ private QRoomQueryDto projectionForRecruitingRoomSearch() { */ @Override public List findRecruitingRoomsOrderByStartDateAsc(String keyword, LocalDate lastStartDate, Long roomId, int pageSize) { - final LocalDate today = LocalDate.now(); DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate - BooleanBuilder where = recruitingActiveWhere(today); + BooleanBuilder where = recruitingActiveWhere(); applyKeyword(where, keyword); applyCursorStartDateAsc(where, cursorExpr, lastStartDate, roomId); @@ -110,10 +110,9 @@ public List findRecruitingRoomsOrderByStartDateAsc(String keyword, @Override public List findRecruitingRoomsWithCategoryOrderByStartDateAsc(String keyword, Category category, LocalDate lastStartDate, Long roomId, int pageSize) { - final LocalDate today = LocalDate.now(); DateExpression cursorExpr = room.startDate; - BooleanBuilder where = recruitingActiveWhere(today); + BooleanBuilder where = recruitingActiveWhere(); applyCategory(where, category); applyKeyword(where, keyword); applyCursorStartDateAsc(where, cursorExpr, lastStartDate, roomId); @@ -130,9 +129,7 @@ public List findRecruitingRoomsWithCategoryOrderByStartDateAsc(Str @Override public List findRecruitingRoomsOrderByMemberCountDesc(String keyword, Integer lastMemberCount, Long roomId, int pageSize) { - final LocalDate today = LocalDate.now(); - - BooleanBuilder where = recruitingActiveWhere(today); + BooleanBuilder where = recruitingActiveWhere(); applyKeyword(where, keyword); applyCursorMemberCountDesc(where, lastMemberCount, roomId); @@ -148,9 +145,7 @@ public List findRecruitingRoomsOrderByMemberCountDesc(String keywo @Override public List findRecruitingRoomsWithCategoryOrderByMemberCountDesc(String keyword, Category category, Integer lastMemberCount, Long roomId, int pageSize) { - final LocalDate today = LocalDate.now(); - - BooleanBuilder where = recruitingActiveWhere(today); + BooleanBuilder where = recruitingActiveWhere(); applyCategory(where, category); applyKeyword(where, keyword); applyCursorMemberCountDesc(where, lastMemberCount, roomId); @@ -174,7 +169,7 @@ public List findOtherRecruitingR .join(room.bookJpaEntity, book) .where( room.category.eq(category) - .and(room.startDate.after(LocalDate.now())) // 모집 마감 시각 > 현재 시각 + .and(room.roomStatus.eq(RoomStatus.RECRUITING)) // 모집 중인 방 .and(room.roomId.ne(roomId))// 현재 방 제외 .and(room.isPublic.isTrue()) // 공개방 만 ) @@ -207,8 +202,9 @@ public List findHomeJoinedRoomsByUserPercentage( // 활동 기간 중인 방만: startDate ≤ today ≤ endDate BooleanBuilder where = new BooleanBuilder(); where.and(participant.userJpaEntity.userId.eq(userId)); - where.and(room.startDate.loe(LocalDate.now())); - where.and(room.endDate.goe(LocalDate.now())); +// where.and(room.startDate.loe(LocalDate.now())); +// where.and(room.endDate.goe(LocalDate.now())); + where.and(room.roomStatus.eq(RoomStatus.IN_PROGRESS)); // 활동 기간 중인 방만: IN_PROGRESS 상태 // 커서 기반 추가 조건 if (userPercentageCursor != null && startDateCursor != null && roomIdCursor != null) { @@ -252,7 +248,8 @@ public List findRecruitingRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(room.startDate.after(today)); // 유저가 참여한 방 && 모집중인 방 +// .and(room.startDate.after(today)); // 유저가 참여한 방 && 모집중인 방 + .and(room.roomStatus.eq(RoomStatus.RECRUITING)); // 유저가 참여한 방 && 모집중인 방 DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -268,8 +265,9 @@ public List findPlayingRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(room.startDate.loe(today)) - .and(room.endDate.goe(today)); // 유저가 참여한 방 && 현재 진행중인 방 +// .and(room.startDate.loe(today)) +// .and(room.endDate.goe(today)); // 유저가 참여한 방 && 현재 진행중인 방 + .and(room.roomStatus.eq(RoomStatus.IN_PROGRESS)); // 유저가 참여한 방 && 현재 진행중인 방 DateExpression cursorExpr = room.endDate; // 커서 비교는 endDate(= 진행 마감일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -284,8 +282,10 @@ public List findPlayingAndRecruitingRoomsUserParticipated( Long userId, Integer priorityCursor, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { LocalDate today = LocalDate.now(); - BooleanExpression playing = room.startDate.loe(today).and(room.endDate.goe(today)); - BooleanExpression recruiting = room.startDate.after(today); +// BooleanExpression playing = room.startDate.loe(today).and(room.endDate.goe(today)); +// BooleanExpression recruiting = room.startDate.after(today); + BooleanExpression playing = room.roomStatus.eq(RoomStatus.IN_PROGRESS); + BooleanExpression recruiting = room.roomStatus.eq(RoomStatus.RECRUITING); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) .and(playing.or(recruiting)); // 유저가 참여한 방 && 현재 진행중인 방 + 모집중인 방 @@ -309,7 +309,8 @@ public List findExpiredRoomsUserParticipated( ) { LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(room.endDate.before(today)); // 유저가 참여한 방 && 만료된 방 +// .and(room.endDate.before(today)); // 유저가 참여한 방 && 만료된 방 + .and(room.roomStatus.eq(RoomStatus.EXPIRED)); // 유저가 참여한 방 && 만료된 방 DateExpression cursorExpr = room.endDate; OrderSpecifier[] orders = new OrderSpecifier[]{ @@ -361,7 +362,8 @@ public List findRoomsByCategoryOrderByMemberCount(Category categor public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalDate dateCursor, Long roomIdCursor, int pageSize) { DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) - .and(room.startDate.after(LocalDate.now())); // 모집 마감 시각 > 현재 시각 +// .and(room.startDate.after(LocalDate.now())); // 모집 마감 시각 > 현재 시각 + .and(room.roomStatus.eq(RoomStatus.RECRUITING)); // 모집중인 방 if (dateCursor != null && roomIdCursor != null) { // 첫 페이지가 아닌 경우 @@ -389,7 +391,8 @@ public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalD private BooleanExpression findDeadlinePopularRoomCondition(Category category, Long userId) { return room.category.eq(category) - .and(room.startDate.after(LocalDate.now())) // 모집 마감 시각 > 현재 시각 +// .and(room.startDate.after(LocalDate.now())) // 모집 마감 시각 > 현재 시각 + .and(room.roomStatus.eq(RoomStatus.RECRUITING)) // 모집중인 방 .and(room.isPublic.isTrue()) // 공개 방만 조회 .and(userJoinedRoom(userId).not()); // 유저가 참여하지 않은 방만 조회 } diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java index b28d75426..8c43d2034 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java @@ -5,15 +5,14 @@ import konkuk.thip.room.adapter.in.web.response.RoomRecruitingDetailViewResponse; import konkuk.thip.room.application.port.out.dto.RoomParticipantQueryDto; import konkuk.thip.room.application.port.out.dto.RoomQueryDto; -import konkuk.thip.room.domain.value.Category; import konkuk.thip.room.domain.Room; +import konkuk.thip.room.domain.value.Category; -import java.time.LocalDate; import java.util.List; public interface RoomQueryPort { - int countRecruitingRoomsByBookAndStartDateAfter(String isbn, LocalDate currentDate); + int countRecruitingRoomsByBookIsbn(String isbn); /** * 방 검색 From 231c256fd0e109e5aced11fcf688edad9971c908 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 15:27:43 +0900 Subject: [PATCH 27/89] =?UTF-8?q?[refactor]=20=EA=B8=B0=EA=B0=84=EC=9D=B4?= =?UTF-8?q?=20=EB=A7=8C=EB=A3=8C=EB=90=9C=20=EB=B0=A9=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=EB=93=A4=EC=9D=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A7=8C=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../policy/RoomPostCommentAccessPolicy.java | 3 +++ .../swagger/SwaggerResponseDescription.java | 22 ++++++++++++------- .../service/validator/RoomValidator.java | 19 ++++++++++++++++ .../service/AttendanceCheckCreateService.java | 7 ++++++ .../service/AttendanceCheckDeleteService.java | 7 ++++++ .../service/RecordDeleteService.java | 10 ++++++--- .../service/RoomPostUpdateService.java | 14 ++++++++++++ .../service/VoteCreateService.java | 1 + .../policy/RoomPostLikeAccessPolicy.java | 3 +++ 9 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java diff --git a/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java b/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java index fe9bdc0c1..01b4d45aa 100644 --- a/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java +++ b/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java @@ -2,6 +2,7 @@ import konkuk.thip.post.domain.CountUpdatable; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.application.service.validator.RoomValidator; import konkuk.thip.roompost.domain.RoomPost; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -11,11 +12,13 @@ public class RoomPostCommentAccessPolicy implements CommentAccessPolicy { private final RoomParticipantValidator roomParticipantValidator; + private final RoomValidator roomValidator; @Override public void validateCommentAccess(CountUpdatable post, Long userId) { RoomPost roomPost = (RoomPost) post; roomParticipantValidator.validateUserIsRoomMember(roomPost.getRoomId(), userId); + roomValidator.validateRoomExpired(roomPost.getRoomId()); } } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 703e7f4dc..e57e6c0da 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -147,7 +147,8 @@ public enum SwaggerResponseDescription { RECORD_DELETE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, RECORD_NOT_FOUND, - RECORD_ACCESS_FORBIDDEN + RECORD_ACCESS_FORBIDDEN, + ROOM_IS_EXPIRED ))), RECORD_PIN(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, @@ -158,7 +159,8 @@ public enum SwaggerResponseDescription { RECORD_UPDATE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, RECORD_NOT_FOUND, - RECORD_ACCESS_FORBIDDEN + RECORD_ACCESS_FORBIDDEN, + ROOM_IS_EXPIRED ))), // Vote @@ -175,7 +177,8 @@ public enum SwaggerResponseDescription { VOTE_ITEM_NOT_FOUND, VOTE_ITEM_ALREADY_VOTED, VOTE_ITEM_NOT_VOTED_CANNOT_CANCEL, - VOTE_ITEM_COUNT_CANNOT_BE_NEGATIVE + VOTE_ITEM_COUNT_CANNOT_BE_NEGATIVE, + ROOM_IS_EXPIRED ))), VOTE_DELETE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, @@ -251,8 +254,8 @@ public enum SwaggerResponseDescription { VOTE_NOT_FOUND, INVALID_COMMENT_CREATE, FEED_ACCESS_FORBIDDEN, - ROOM_ACCESS_FORBIDDEN - + ROOM_ACCESS_FORBIDDEN, + ROOM_IS_EXPIRED ))), CHANGE_COMMENT_LIKE_STATE(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, @@ -264,7 +267,8 @@ public enum SwaggerResponseDescription { COMMENT_NOT_LIKED_CANNOT_CANCEL, COMMENT_LIKE_COUNT_UNDERFLOW, FEED_ACCESS_FORBIDDEN, - ROOM_ACCESS_FORBIDDEN + ROOM_ACCESS_FORBIDDEN, + ROOM_IS_EXPIRED ))), COMMENT_DELETE(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, @@ -275,7 +279,8 @@ public enum SwaggerResponseDescription { COMMENT_DELETE_FORBIDDEN, COMMENT_COUNT_UNDERFLOW, FEED_ACCESS_FORBIDDEN, - ROOM_ACCESS_FORBIDDEN + ROOM_ACCESS_FORBIDDEN, + ROOM_IS_EXPIRED ))), // Book @@ -321,7 +326,8 @@ public enum SwaggerResponseDescription { ROOM_NOT_FOUND, USER_NOT_FOUND, ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED, - ATTENDANCE_CHECK_NOT_FOUND + ATTENDANCE_CHECK_NOT_FOUND, + ROOM_IS_EXPIRED ))), ATTENDANCE_CHECK_SHOW(new LinkedHashSet<>(Set.of( diff --git a/src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java b/src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java new file mode 100644 index 000000000..305d58473 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java @@ -0,0 +1,19 @@ +package konkuk.thip.room.application.service.validator; + +import konkuk.thip.common.annotation.application.HelperService; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; +import lombok.RequiredArgsConstructor; + +@HelperService +@RequiredArgsConstructor +public class RoomValidator { + + private final RoomCommandPort roomCommandPort; + + public void validateRoomExpired(Long roomId) { + Room room = roomCommandPort.getByIdOrThrow(roomId); + room.validateRoomExpired(); + } + +} diff --git a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java index 1fa93c9ca..62321747e 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java @@ -1,6 +1,8 @@ package konkuk.thip.roompost.application.service; import konkuk.thip.common.util.DateUtil; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.in.AttendanceCheckCreateUseCase; import konkuk.thip.roompost.application.port.in.dto.attendancecheck.AttendanceCheckCreateCommand; import konkuk.thip.roompost.application.port.in.dto.attendancecheck.AttendanceCheckCreateResult; @@ -22,6 +24,7 @@ public class AttendanceCheckCreateService implements AttendanceCheckCreateUseCas private final AttendanceCheckCommandPort attendanceCheckCommandPort; private final UserCommandPort userCommandPort; private final AttendanceCheckQueryPort attendanceCheckQueryPort; + private final RoomCommandPort roomCommandPort; @Transactional @Override @@ -32,6 +35,10 @@ public AttendanceCheckCreateResult create(AttendanceCheckCreateCommand command) // 1-1. 유저가 해당 방에 오늘의 한마디를 작성할 수 있는지 검증 roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.creatorId()); + // 1-2. 방이 만료되었는지 검증 + Room room = roomCommandPort.getByIdOrThrow(command.roomId()); + room.validateRoomExpired(); + // 2. 유저가 해당 방에서 오늘 이미 작성한 오늘의 한마디 개수 조회 int alreadyWrittenCountToday = attendanceCheckQueryPort.countAttendanceChecksOnTodayByUser(command.creatorId(), command.roomId()); diff --git a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java index a67707898..b4f55b53e 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java @@ -1,6 +1,8 @@ package konkuk.thip.roompost.application.service; +import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.in.AttendanceCheckDeleteUseCase; import konkuk.thip.roompost.application.port.out.AttendanceCheckCommandPort; import konkuk.thip.roompost.domain.AttendanceCheck; @@ -14,6 +16,7 @@ public class AttendanceCheckDeleteService implements AttendanceCheckDeleteUseCas private final AttendanceCheckCommandPort attendanceCheckCommandPort; private final RoomParticipantValidator roomParticipantValidator; + private final RoomCommandPort roomCommandPort; @Override @Transactional @@ -21,6 +24,10 @@ public Long delete(Long creatorId, Long roomId, Long attendanceCheckId) { // 1. creator 가 room participant인지 검증 roomParticipantValidator.validateUserIsRoomMember(roomId, creatorId); + // 1-1. 방이 만료되었는지 검증 + Room room = roomCommandPort.getByIdOrThrow(roomId); + room.validateRoomExpired(); + // 2. creator 겁증 AttendanceCheck attendanceCheck = attendanceCheckCommandPort.getByIdOrThrow(attendanceCheckId); attendanceCheck.validateCreator(creatorId); diff --git a/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java index f84c3f007..2f4215d2f 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java @@ -2,12 +2,13 @@ import konkuk.thip.comment.application.port.out.CommentCommandPort; import konkuk.thip.post.application.port.out.PostLikeCommandPort; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.in.RecordDeleteUseCase; import konkuk.thip.roompost.application.port.in.dto.record.RecordDeleteCommand; import konkuk.thip.roompost.application.port.out.RecordCommandPort; -import konkuk.thip.roompost.application.service.manager.RoomProgressManager; import konkuk.thip.roompost.domain.Record; -import konkuk.thip.room.application.service.validator.RoomParticipantValidator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,9 +20,9 @@ public class RecordDeleteService implements RecordDeleteUseCase { private final RecordCommandPort recordCommandPort; private final CommentCommandPort commentCommandPort; private final PostLikeCommandPort postLikeCommandPort; + private final RoomCommandPort roomCommandPort; private final RoomParticipantValidator roomParticipantValidator; - private final RoomProgressManager roomProgressManager; @Override @Transactional @@ -30,6 +31,9 @@ public Long deleteRecord(RecordDeleteCommand command) { // 1. 방 참여자 검증 roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId()); + Room room = roomCommandPort.getByIdOrThrow(command.roomId()); + room.validateRoomExpired(); + // 2. 기록 조회 및 검증 Record record = recordCommandPort.getByIdOrThrow(command.recordId()); // 2-1. 기록 삭제 권한 검증 diff --git a/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java b/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java index 8df64dab2..a4a9bfc4e 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java @@ -1,6 +1,8 @@ package konkuk.thip.roompost.application.service; +import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.in.RoomPostUpdateUseCase; import konkuk.thip.roompost.application.port.in.dto.record.RecordUpdateCommand; import konkuk.thip.roompost.application.port.in.dto.vote.VoteUpdateCommand; @@ -19,6 +21,7 @@ public class RoomPostUpdateService implements RoomPostUpdateUseCase { private final RoomParticipantValidator roomParticipantValidator; private final RecordCommandPort recordCommandPort; private final VoteCommandPort voteCommandPort; + private final RoomCommandPort roomCommandPort; @Override @Transactional @@ -26,6 +29,9 @@ public Long updateRecord(RecordUpdateCommand command) { // 1. 사용자가 방의 참가자인지 검증 roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId()); + // 1.1 방이 만료되었는지 검증 + validateRoom(command.roomId()); + // 2. Record 조회 Record record = recordCommandPort.getByIdOrThrow(command.postId()); @@ -43,6 +49,9 @@ public Long updateVote(VoteUpdateCommand command) { // 1. 사용자가 방의 참가자인지 검증 roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId()); + // 1.1 방이 만료되었는지 검증 + validateRoom(command.roomId()); + // 2. Vote 조회 Vote vote = voteCommandPort.getByIdOrThrow(command.postId()); @@ -53,4 +62,9 @@ public Long updateVote(VoteUpdateCommand command) { voteCommandPort.updateVote(vote); return command.roomId(); } + + private void validateRoom(Long command) { + Room room = roomCommandPort.getByIdOrThrow(command); + room.validateRoomExpired(); + } } 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..12991f796 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java @@ -55,6 +55,7 @@ public VoteCreateResult createVote(VoteCreateCommand command) { Room room = roomCommandPort.getByIdOrThrow(vote.getRoomId()); Book book = bookCommandPort.findById(room.getBookId()); validateVote(vote, book); + room.validateRoomExpired(); // 2. vote 저장 Long savedVoteId = voteCommandPort.saveVote(vote); diff --git a/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java b/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java index 5d75645aa..bc827de00 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java +++ b/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java @@ -3,6 +3,7 @@ import konkuk.thip.post.application.service.policy.PostLikeAccessPolicy; import konkuk.thip.post.domain.CountUpdatable; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.application.service.validator.RoomValidator; import konkuk.thip.roompost.domain.RoomPost; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -12,10 +13,12 @@ public class RoomPostLikeAccessPolicy implements PostLikeAccessPolicy { private final RoomParticipantValidator roomParticipantValidator; + private final RoomValidator roomValidator; @Override public void validatePostLikeAccess(CountUpdatable post, Long userId) { RoomPost roomPost = (RoomPost) post; roomParticipantValidator.validateUserIsRoomMember(roomPost.getRoomId(), userId); + roomValidator.validateRoomExpired(roomPost.getRoomId()); } } \ No newline at end of file From 1d8a507b2340ceb0f005e423ba3bbd38a0ed16d0 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 15:28:00 +0900 Subject: [PATCH 28/89] =?UTF-8?q?[refactor]=20RoomStatus=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room/adapter/out/jpa/RoomJpaEntity.java | 3 + .../in/web/BookDetailSearchApiTest.java | 2 + .../in/web/BookRecruitingRoomApiTest.java | 6 +- .../thip/common/util/TestEntityFactory.java | 7 +- .../adapter/in/web/RoomCloseJoinApiTest.java | 5 +- .../in/web/RoomGetDeadlinePopularApiTest.java | 8 +- .../in/web/RoomGetHomeJoinedRoomsApiTest.java | 21 ++- .../web/RoomRecruitingDetailViewApiTest.java | 57 ++++---- .../adapter/in/web/RoomSearchApiTest.java | 101 ++++++++------ .../adapter/in/web/RoomShowMineApiTest.java | 124 +++++++++--------- .../in/web/RoomVerifyPasswordApiTest.java | 2 + .../konkuk/thip/room/domain/RoomTest.java | 7 +- 12 files changed, 197 insertions(+), 146 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index 5865135fd..c5822ba89 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java @@ -100,4 +100,7 @@ public void updateIsPublic(boolean isPublic) { @VisibleForTesting public void updateRoomPercentage(double roomPercentage) {this.roomPercentage = roomPercentage;} + + @VisibleForTesting + public void updateRoomStatus(RoomStatus roomStatus) {this.roomStatus = roomStatus;} } diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java index 0da7110b4..28fecf312 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java @@ -14,6 +14,7 @@ import konkuk.thip.book.adapter.out.jpa.SavedBookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -155,6 +156,7 @@ void searchDetailBooks_NoRecruitingRooms_ReturnsZero() { .roomPercentage(0.0) .startDate(LocalDate.now().minusDays(10)) .endDate(LocalDate.now().minusDays(5)) + .roomStatus(RoomStatus.IN_PROGRESS) .recruitCount(10) .bookJpaEntity(book) .category(Category.LITERATURE) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java index 93e0f5145..560eed86c 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java @@ -8,6 +8,7 @@ import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -85,6 +86,7 @@ void getRecruitingRoomsByIsbn_success() throws Exception { .roomPercentage(0.0) .startDate(LocalDate.now().plusDays(1)) .endDate(LocalDate.now().plusDays(30)) + .roomStatus(RoomStatus.RECRUITING) .recruitCount(5) .bookJpaEntity(book) .category(category) @@ -97,6 +99,7 @@ void getRecruitingRoomsByIsbn_success() throws Exception { .roomPercentage(0.0) .startDate(LocalDate.now().minusDays(30)) .endDate(LocalDate.now().minusDays(1)) + .roomStatus(RoomStatus.IN_PROGRESS) .recruitCount(5) .bookJpaEntity(book) .category(category) @@ -139,7 +142,8 @@ void getRecruitingRoomsWithCursor_success() throws Exception { book, category, LocalDate.now().plusDays(i + 1), - LocalDate.now().plusDays(i + 2) + LocalDate.now().plusDays(i + 2), + RoomStatus.RECRUITING )); } diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 27fb9b62a..5f165f1af 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -18,6 +18,7 @@ import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.roompost.adapter.out.jpa.*; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; @@ -143,7 +144,7 @@ public static RoomJpaEntity createRoom(BookJpaEntity book, Category category) { .build(); } - public static RoomJpaEntity createCustomRoom(BookJpaEntity book, Category category, LocalDate startDate, LocalDate endDate) { + public static RoomJpaEntity createCustomRoom(BookJpaEntity book, Category category, LocalDate startDate, LocalDate endDate, RoomStatus roomStatus) { return RoomJpaEntity.builder() .title("방이름") .description("설명") @@ -153,10 +154,11 @@ public static RoomJpaEntity createCustomRoom(BookJpaEntity book, Category catego .recruitCount(3) .bookJpaEntity(book) .category(category) + .roomStatus(roomStatus) .build(); } - public static RoomJpaEntity createCustomRoom(BookJpaEntity book, Category category, String roomName, LocalDate startDate, LocalDate endDate) { + public static RoomJpaEntity createCustomRoom(BookJpaEntity book, Category category, String roomName, LocalDate startDate, LocalDate endDate, RoomStatus roomStatus) { return RoomJpaEntity.builder() .title(roomName) .description("설명") @@ -166,6 +168,7 @@ public static RoomJpaEntity createCustomRoom(BookJpaEntity book, Category catego .recruitCount(20) .bookJpaEntity(book) .category(category) + .roomStatus(roomStatus) .build(); } 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..12400aca5 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 @@ -9,6 +9,7 @@ import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -138,9 +139,9 @@ void closeRoomRecruit_fail_not_host() throws Exception { @DisplayName("이미 시작된 방은 모집 마감 요청 시 실패") void closeRoomRecruit_fail_already_started() throws Exception { // startDate를 오늘로 설정해서 이미 시작된 상태로 만듦 - Field field = RoomJpaEntity.class.getDeclaredField("startDate"); + Field field = RoomJpaEntity.class.getDeclaredField("roomStatus"); field.setAccessible(true); - field.set(room, LocalDate.now()); + field.set(room, RoomStatus.IN_PROGRESS); roomJpaRepository.save(room); mockMvc.perform(post("/rooms/" + room.getRoomId() + "/close") diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java index c3e1e72ad..d097c98b5 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java @@ -5,10 +5,11 @@ import konkuk.thip.common.util.DateUtil; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; @@ -74,6 +75,7 @@ void setUp() { RoomJpaEntity deadlineRoom = TestEntityFactory.createRoom(book, category); deadlineRoom.updateStartDate(today.plusDays(i + 1)); deadlineRoom.updateMemberCount(5); + deadlineRoom.updateRoomStatus(RoomStatus.RECRUITING); rooms.add(roomJpaRepository.save(deadlineRoom)); } @@ -81,6 +83,7 @@ void setUp() { RoomJpaEntity popularRoom = TestEntityFactory.createRoom(book, category); popularRoom.updateStartDate(today.plusDays(10 + i)); popularRoom.updateMemberCount(maxMemberCount - i); + popularRoom.updateRoomStatus(RoomStatus.RECRUITING); rooms.add(roomJpaRepository.save(popularRoom)); } @@ -88,17 +91,20 @@ void setUp() { RoomJpaEntity expiredRoom = TestEntityFactory.createRoom(book, category); expiredRoom.updateStartDate(today); expiredRoom.updateIsPublic(true); + expiredRoom.updateRoomStatus(RoomStatus.EXPIRED); roomJpaRepository.save(expiredRoom); // 비공개 방 (조건 불만족) privateRoom = TestEntityFactory.createRoom(book, category); privateRoom.updateStartDate(today.plusDays(2)); + privateRoom.updateRoomStatus(RoomStatus.RECRUITING); privateRoom.updateIsPublic(false); roomJpaRepository.save(privateRoom); // 내가 참여한 방 (조건 불만족) joinedRoom = TestEntityFactory.createRoom(book, category); joinedRoom.updateStartDate(today.plusDays(5)); + joinedRoom.updateRoomStatus(RoomStatus.RECRUITING); joinedRoom = roomJpaRepository.save(joinedRoom); participantJpaRepository.save( TestEntityFactory.createRoomParticipant(joinedRoom, currentUser, RoomParticipantRole.MEMBER, 0) diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java index 1e30aa466..3ac9526f8 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java @@ -8,6 +8,7 @@ import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.*; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; @@ -62,8 +63,13 @@ void setUp() { bookJpaRepository.save(book); category = TestEntityFactory.createLiteratureCategory(); - room1 = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); - room2 = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + room1 = TestEntityFactory.createRoom(book, category); + room1.updateRoomStatus(RoomStatus.IN_PROGRESS); + roomJpaRepository.save(room1); + + room2 = TestEntityFactory.createRoom(book, category); + room2.updateRoomStatus(RoomStatus.IN_PROGRESS); + roomJpaRepository.save(room2); // 1번방에 유저 1이 호스트, 유저2가 멤버 roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room1,user1, RoomParticipantRole.HOST, 80.0)); @@ -161,15 +167,15 @@ void getHomeJoinedRooms_sortByStartDateWhenUserPercentageEquals() throws Excepti // 방1: 시작일 오늘-2 RoomJpaEntity room1 = roomJpaRepository.save( - TestEntityFactory.createCustomRoom(book, category, LocalDate.now().minusDays(2), LocalDate.now().plusDays(10)) + TestEntityFactory.createCustomRoom(book, category, LocalDate.now().minusDays(2), LocalDate.now().plusDays(10), RoomStatus.IN_PROGRESS) ); // 방2: 시작일 오늘-1 RoomJpaEntity room2 = roomJpaRepository.save( - TestEntityFactory.createCustomRoom(book, category, LocalDate.now().minusDays(1), LocalDate.now().plusDays(8)) + TestEntityFactory.createCustomRoom(book, category, LocalDate.now().minusDays(1), LocalDate.now().plusDays(8), RoomStatus.IN_PROGRESS) ); // 방3: 시작일 오늘 RoomJpaEntity room3 = roomJpaRepository.save( - TestEntityFactory.createCustomRoom(book, category, LocalDate.now(), LocalDate.now().plusDays(9)) + TestEntityFactory.createCustomRoom(book, category, LocalDate.now(), LocalDate.now().plusDays(9), RoomStatus.IN_PROGRESS) ); // 모두 동일한 진행률(70%)로 참여 @@ -208,11 +214,11 @@ void getHomeJoinedRooms_excludeRecruitingRooms() throws Exception { // 모집중(시작일 미래) RoomJpaEntity recruitRoom = roomJpaRepository.save( - TestEntityFactory.createCustomRoom(book, category, LocalDate.now().plusDays(2), LocalDate.now().plusDays(5)) + TestEntityFactory.createCustomRoom(book, category, LocalDate.now().plusDays(2), LocalDate.now().plusDays(5), RoomStatus.RECRUITING) ); // 활동중(시작일 오늘-1, 종료일 오늘+2) RoomJpaEntity activeRoom = roomJpaRepository.save( - TestEntityFactory.createCustomRoom(book, category, LocalDate.now().minusDays(1), LocalDate.now().plusDays(2)) + TestEntityFactory.createCustomRoom(book, category, LocalDate.now().minusDays(1), LocalDate.now().plusDays(2), RoomStatus.IN_PROGRESS) ); roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(recruitRoom, newUser, RoomParticipantRole.MEMBER, 20.0)); @@ -267,6 +273,7 @@ void getHomeJoinedRooms_page_1() throws Exception { LocalDate end = LocalDate.now().plusDays(30); RoomJpaEntity room = saveScienceRoom("모집중인방-책-" + (i + 1), isbn, title, start, end, 10); + room.updateRoomStatus(RoomStatus.IN_PROGRESS); changeRoomMemberCount(room, 8); double userPercentage = 89.6 - i; // 진행률은 방번호가 작을수록 높음 diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java index 0e11daa9b..ee37cbf18 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java @@ -6,12 +6,13 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; -import konkuk.thip.user.adapter.out.jpa.*; +import konkuk.thip.room.domain.value.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomStatus; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; -import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.domain.value.Alias; import konkuk.thip.user.domain.value.UserRole; import org.junit.jupiter.api.AfterEach; @@ -53,7 +54,7 @@ void tearDown() { userJpaRepository.deleteAll(); } - private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { + private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount, RoomStatus roomStatus) { Alias alias = TestEntityFactory.createScienceAlias(); BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() @@ -79,10 +80,11 @@ private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String room .recruitCount(recruitCount) .bookJpaEntity(book) .category(category) + .roomStatus(roomStatus) .build()); } - private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { + private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount, RoomStatus roomStatus) { Alias alias = TestEntityFactory.createLiteratureAlias(); BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() @@ -108,6 +110,7 @@ private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String r .recruitCount(recruitCount) .bookJpaEntity(book) .category(category) + .roomStatus(roomStatus) .build()); } @@ -143,23 +146,23 @@ private void saveUsersToRoom(RoomJpaEntity roomJpaEntity, int count) { @DisplayName("모집중인 모임방 상세조회할 경우, 해당 모임방의 정보, 책 정보, 추천할 모임방의 정보를 반환한다.") void get_recruiting_room_detail() throws Exception { //given - RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING); saveUsersToRoom(targetRoom, 4); UserJpaEntity joiningUser = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1).getUserJpaEntity(); - RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "isbn2", "방이름입니다", LocalDate.now().plusDays(1), 10); + RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "isbn2", "방이름입니다", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING); saveUsersToRoom(science_room_2, 5); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "isbn3", "무슨방일까요??", LocalDate.now().plusDays(5), 8); + RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "isbn3", "무슨방일까요??", LocalDate.now().plusDays(5), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_3, 2); - RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "isbn4", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), 8); + RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "isbn4", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_4, 1); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "isbn5", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), 8); + RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "isbn5", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), 8, RoomStatus.RECRUITING); saveUsersToRoom(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "isbn6", "모집기한-지난-과학방", LocalDate.now().minusDays(1), 8); + RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "isbn6", "모집기한-지난-과학방", LocalDate.now().minusDays(1), 8, RoomStatus.IN_PROGRESS); saveUsersToRoom(recruit_expired_room_4, 6); //when @@ -191,7 +194,7 @@ void get_recruiting_room_detail() throws Exception { @DisplayName("모임방의 호스트가 조회할 경우, 유저가 해당 방의 호스트임을 응답값으로 보여준다. (나머지 응답값은 동일)") void get_recruiting_room_detail_host() throws Exception { //given - RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING); saveUsersToRoom(targetRoom, 4); RoomParticipantJpaEntity firstMember = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1); roomParticipantJpaRepository.delete(firstMember); @@ -201,19 +204,19 @@ void get_recruiting_room_detail_host() throws Exception { .roomParticipantRole(RoomParticipantRole.HOST) .build()); // firstMember 을 MEMBER -> HOST 로 수정 - RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "isbn2", "방이름입니다", LocalDate.now().plusDays(1), 10); + RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "isbn2", "방이름입니다", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING); saveUsersToRoom(science_room_2, 5); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "isbn3", "무슨방일까요??", LocalDate.now().plusDays(5), 8); + RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "isbn3", "무슨방일까요??", LocalDate.now().plusDays(5), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_3, 2); - RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "isbn4", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), 8); + RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "isbn4", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_4, 1); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "isbn5", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), 8); + RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "isbn5", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), 8, RoomStatus.RECRUITING); saveUsersToRoom(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "isbn6", "모집기한-지난-과학방", LocalDate.now().minusDays(1), 8); + RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "isbn6", "모집기한-지난-과학방", LocalDate.now().minusDays(1), 8, RoomStatus.IN_PROGRESS); saveUsersToRoom(recruit_expired_room_4, 6); //when @@ -244,26 +247,26 @@ void get_recruiting_room_detail_host() throws Exception { @DisplayName("추천하는 다른 모집중인 모임방이 많을 경우, 모집 기한 마감임박 순으로 최대 5개만 반환한다.") void get_recruiting_room_detail_too_many_recommend_rooms() throws Exception { //given - RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING); saveUsersToRoom(targetRoom, 4); UserJpaEntity joiningUser = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1).getUserJpaEntity(); - RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "isbn2", "방이름입니다", LocalDate.now().plusDays(1), 10); + RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "isbn2", "방이름입니다", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING); saveUsersToRoom(science_room_2, 5); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "isbn3", "무슨방일까요??", LocalDate.now().plusDays(5), 8); + RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "isbn3", "무슨방일까요??", LocalDate.now().plusDays(5), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_3, 2); - RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "isbn4", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), 8); + RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "isbn4", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_4, 1); - RoomJpaEntity science_room_5 = saveScienceRoom("과학-책", "isbn5", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), 8); + RoomJpaEntity science_room_5 = saveScienceRoom("과학-책", "isbn5", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_5, 1); - RoomJpaEntity science_room_6 = saveScienceRoom("과학-책", "isbn6", "과학-방-15일뒤-활동시작", LocalDate.now().plusDays(15), 8); + RoomJpaEntity science_room_6 = saveScienceRoom("과학-책", "isbn6", "과학-방-15일뒤-활동시작", LocalDate.now().plusDays(15), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_6, 1); - RoomJpaEntity science_room_7 = saveScienceRoom("과학-책", "isbn7", "과학-방-20일뒤-활동시작", LocalDate.now().plusDays(20), 8); + RoomJpaEntity science_room_7 = saveScienceRoom("과학-책", "isbn7", "과학-방-20일뒤-활동시작", LocalDate.now().plusDays(20), 8, RoomStatus.RECRUITING); saveUsersToRoom(science_room_7, 1); //when @@ -296,14 +299,14 @@ void get_recruiting_room_detail_too_many_recommend_rooms() throws Exception { @DisplayName("추천하는 다른 모집중인 모임방이 없을 경우, 해당 데이터를 빈 배열로 반환한다.") void get_recruiting_room_detail_no_recommend_rooms() throws Exception { //given - RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING); saveUsersToRoom(targetRoom, 4); UserJpaEntity joiningUser = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1).getUserJpaEntity(); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "isbn5", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), 8); + RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "isbn5", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), 8, RoomStatus.RECRUITING); saveUsersToRoom(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "isbn6", "모집기한-지난-과학방", LocalDate.now().minusDays(1), 8); + RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "isbn6", "모집기한-지난-과학방", LocalDate.now().minusDays(1), 8, RoomStatus.IN_PROGRESS); saveUsersToRoom(recruit_expired_room_4, 6); //when diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java index 70b16969d..0a45f740f 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java @@ -8,10 +8,11 @@ import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; -import konkuk.thip.user.adapter.out.jpa.*; +import konkuk.thip.room.domain.value.RoomStatus; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; -import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -56,18 +57,32 @@ void tearDown() { userJpaRepository.deleteAllInBatch(); } - private RoomJpaEntity saveScienceRoom(String bookTitle, String roomName, LocalDate startDate, LocalDate endDate) { + private RoomJpaEntity saveScienceRecruitingRoom(String bookTitle, String roomName, LocalDate startDate, LocalDate endDate) { + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle(bookTitle)); + + Category category = TestEntityFactory.createScienceCategory(); + return roomJpaRepository.save(TestEntityFactory.createCustomRoom(book, category, roomName, startDate, endDate, RoomStatus.RECRUITING)); + } + + private RoomJpaEntity saveScienceProgressRoom(String bookTitle, String roomName, LocalDate startDate, LocalDate endDate) { BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle(bookTitle)); Category category = TestEntityFactory.createScienceCategory(); - return roomJpaRepository.save(TestEntityFactory.createCustomRoom(book, category, roomName, startDate, endDate)); + return roomJpaRepository.save(TestEntityFactory.createCustomRoom(book, category, roomName, startDate, endDate, RoomStatus.IN_PROGRESS)); + } + + private RoomJpaEntity saveLiteratureRecruitingRoom(String bookTitle, String roomName, LocalDate startDate, LocalDate endDate) { + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle(bookTitle)); + + Category category = TestEntityFactory.createLiteratureCategory(); + return roomJpaRepository.save(TestEntityFactory.createCustomRoom(book, category, roomName, startDate, endDate, RoomStatus.RECRUITING)); } - private RoomJpaEntity saveLiteratureRoom(String bookTitle, String roomName, LocalDate startDate, LocalDate endDate) { + private RoomJpaEntity saveLiteratureProgressRoom(String bookTitle, String roomName, LocalDate startDate, LocalDate endDate) { BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle(bookTitle)); Category category = TestEntityFactory.createLiteratureCategory(); - return roomJpaRepository.save(TestEntityFactory.createCustomRoom(book, category, roomName, startDate, endDate)); + return roomJpaRepository.save(TestEntityFactory.createCustomRoom(book, category, roomName, startDate, endDate, RoomStatus.IN_PROGRESS)); } private void updateRoomMemberCount(RoomJpaEntity roomJpaEntity, int count) { @@ -81,22 +96,22 @@ private void updateRoomMemberCount(RoomJpaEntity roomJpaEntity, int count) { @DisplayName("keyword = [과학], 카테고리 선택 X, 정렬 = [마감임박순] 일 경우, 방이름 or 책제목에 '과학'이 포함된 모집중인 방 검색 결과가 마감임박순으로 반환된다.") void search_keyword_and_sort_deadline() throws Exception { //given - RoomJpaEntity science_room_1 = saveScienceRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_1, 4); - RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "방이름입니다", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_2 = saveScienceRecruitingRoom("과학-책", "방이름입니다", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_2, 5); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_3, 2); - RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_4 = saveScienceRecruitingRoom("과학-책", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_4, 1); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); updateRoomMemberCount(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(recruit_expired_room_4, 6); //when @@ -127,22 +142,22 @@ void search_keyword_and_sort_deadline() throws Exception { @DisplayName("keyword = [과학], 카테고리 선택 X, 정렬 = [인기순] 일 경우, 방이름 or 책제목에 '과학'이 포함된 모집중인 방 검색 결과가 인기순(= 현재까지 모집된 인원 많은 순)으로 반환된다.") void search_keyword_and_sort_member_count() throws Exception { //given - RoomJpaEntity science_room_1 = saveScienceRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_1, 4); - RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "방이름입니다", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_2 = saveScienceRecruitingRoom("과학-책", "방이름입니다", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_2, 5); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_3, 2); - RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_4 = saveScienceRecruitingRoom("과학-책", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_4, 1); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); updateRoomMemberCount(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(recruit_expired_room_4, 6); //when @@ -182,16 +197,16 @@ void search_keyword_and_sort_member_count() throws Exception { @DisplayName("keyword 입력 x, 카테고리 = [과학/IT], 정렬 = [마감임박순] 일 경우, [과학/IT] 카테고리에 속하는 방 검색 결과가 반환된다.") void search_category_and_sort_deadline() throws Exception { //given - RoomJpaEntity science_room_1 = saveScienceRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_1, 4); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_3, 2); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); updateRoomMemberCount(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(recruit_expired_room_4, 6); //when @@ -219,16 +234,16 @@ void search_category_and_sort_deadline() throws Exception { @DisplayName("keyword 입력 x, 카테고리 입력 x, 정렬 = [마감임박순] 일 경우, DB에 존재하는 전체 방 검색 결과가 반환된다.") void search_sort_deadline() throws Exception { //given - RoomJpaEntity science_room_1 = saveScienceRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_1, 4); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_3, 2); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); updateRoomMemberCount(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(recruit_expired_room_4, 6); //when @@ -256,16 +271,16 @@ void search_sort_deadline() throws Exception { @DisplayName("keyword=[과학], category=[과학/IT], 정렬=[마감임박순] 일 경우, keyword, category 조건을 모두 만족하는 방만 반환된다.") void search_keyword_and_category() throws Exception { // given - RoomJpaEntity science_room_1 = saveScienceRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_1, 4); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_3, 2); - RoomJpaEntity room_3 = saveLiteratureRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); updateRoomMemberCount(room_3, 6); - RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(recruit_expired_room_4, 6); // when @@ -296,7 +311,7 @@ void search_keyword_saved() throws Exception { // given Alias aliasJpa = TestEntityFactory.createScienceAlias(); UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(aliasJpa)); - RoomJpaEntity science_room_1 = saveScienceRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); updateRoomMemberCount(science_room_1, 4); // when @@ -327,18 +342,18 @@ void search_keyword_saved() throws Exception { @DisplayName("검색결과에 해당하는 방이 많은 경우, 정렬 조건 기준으로 페이징 처리한다.") void comment_show_all_page_test() throws Exception { //given - RoomJpaEntity science_room_1 = saveScienceRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_2 = saveScienceRoom("과학-책", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_3 = saveScienceRoom("과학-책", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_4 = saveScienceRoom("과학-책", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_5 = saveScienceRoom("과학-책", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_6 = saveScienceRoom("과학-책", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_7 = saveScienceRoom("과학-책", "과학-방-7일뒤-활동시작", LocalDate.now().plusDays(7), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_8 = saveScienceRoom("과학-책", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_9 = saveScienceRoom("과학-책", "과학-방-9일뒤-활동시작", LocalDate.now().plusDays(9), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_10 = saveScienceRoom("과학-책", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_11 = saveScienceRoom("과학-책", "과학-방-11일뒤-활동시작", LocalDate.now().plusDays(11), LocalDate.now().plusDays(30)); - RoomJpaEntity science_room_12 = saveScienceRoom("과학-책", "과학-방-12일뒤-활동시작", LocalDate.now().plusDays(12), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_2 = saveScienceRecruitingRoom("과학-책", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_4 = saveScienceRecruitingRoom("과학-책", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_5 = saveScienceRecruitingRoom("과학-책", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_6 = saveScienceRecruitingRoom("과학-책", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_7 = saveScienceRecruitingRoom("과학-책", "과학-방-7일뒤-활동시작", LocalDate.now().plusDays(7), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_8 = saveScienceRecruitingRoom("과학-책", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_9 = saveScienceRecruitingRoom("과학-책", "과학-방-9일뒤-활동시작", LocalDate.now().plusDays(9), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_10 = saveScienceRecruitingRoom("과학-책", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_11 = saveScienceRecruitingRoom("과학-책", "과학-방-11일뒤-활동시작", LocalDate.now().plusDays(11), LocalDate.now().plusDays(30)); + RoomJpaEntity science_room_12 = saveScienceRecruitingRoom("과학-책", "과학-방-12일뒤-활동시작", LocalDate.now().plusDays(12), LocalDate.now().plusDays(30)); //when //then MvcResult firstResult = mockMvc.perform(get("/rooms/search") diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java index 2493e5ce5..c75278393 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java @@ -10,6 +10,7 @@ import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; @@ -53,7 +54,7 @@ void tearDown() { userJpaRepository.deleteAllInBatch(); } - private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, LocalDate endDate, int recruitCount) { + private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, LocalDate endDate, int recruitCount, RoomStatus roomStatus) { Alias alias = TestEntityFactory.createScienceAlias(); BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() @@ -79,6 +80,7 @@ private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String room .recruitCount(recruitCount) .bookJpaEntity(book) .category(category) + .roomStatus(roomStatus) .build()); } @@ -105,16 +107,16 @@ private RoomParticipantJpaEntity saveSingleUserToRoom(RoomJpaEntity roomJpaEntit @DisplayName("type 으로 playing 을 받을 경우, 해당 유저가 참여중인 방 중 [현재 진행중인 모임방]의 정보를 [활동 마감일 임박순] 으로 반환한다.") void get_my_playing_rooms() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn2", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10); + RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn2", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playingRoom1, 6); - RoomJpaEntity playingRoom2 = saveScienceRoom("진행중인방-책-2", "isbn3", "과학-방-10일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(10), 10); + RoomJpaEntity playingRoom2 = saveScienceRoom("진행중인방-책-2", "isbn3", "과학-방-10일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(10), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playingRoom2, 3); - RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10); + RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10, RoomStatus.EXPIRED); changeRoomMemberCount(expiredRoom1, 7); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -144,16 +146,16 @@ void get_my_playing_rooms() throws Exception { @DisplayName("type 으로 recruiting 을 받을 경우, 해당 유저가 참여중인 방 중 [모집중인 모임방]의 정보를 [모집 마감일 임박순] 으로 반환한다.") void get_my_recruiting_rooms() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom2, 8); - RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10); + RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playingRoom1, 6); - RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10); + RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10, RoomStatus.EXPIRED); changeRoomMemberCount(expiredRoom1, 7); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -183,16 +185,16 @@ void get_my_recruiting_rooms() throws Exception { @DisplayName("type 이 주어지지 않을 경우, 해당 유저가 참여중인 방 중 [현재 진행중 + 모집중인 모임방]의 정보를 [현재 진행중 -> 모집중] 순 으로 반환한다.") void get_my_playing_and_recruiting_rooms() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom2, 8); - RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10); + RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playingRoom1, 6); - RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10); + RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10, RoomStatus.EXPIRED); changeRoomMemberCount(expiredRoom1, 7); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -223,16 +225,16 @@ void get_my_playing_and_recruiting_rooms() throws Exception { @DisplayName("type 으로 expired 을 받을 경우, 해당 유저가 참여중인 방 중 [만료된 모임방]의 정보를 [활동 마감일 최신순] 으로 반환한다.") void get_my_expired_rooms() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn2", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10); + RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn2", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playingRoom1, 6); - RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn3", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10); + RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn3", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10, RoomStatus.EXPIRED); changeRoomMemberCount(expiredRoom1, 7); - RoomJpaEntity expiredRoom2 = saveScienceRoom("만료된방-책-2", "isbn4", "과학-방-10일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(10), 10); + RoomJpaEntity expiredRoom2 = saveScienceRoom("만료된방-책-2", "isbn4", "과학-방-10일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(10), 10, RoomStatus.EXPIRED); changeRoomMemberCount(expiredRoom2, 1); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -262,16 +264,16 @@ void get_my_expired_rooms() throws Exception { @DisplayName("유효하지 않은 type 을 받을 경우, 400 error 를 반환한다.") void get_my_rooms_wrong_type() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom2, 8); - RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10); + RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playingRoom1, 6); - RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10); + RoomJpaEntity expiredRoom1 = saveScienceRoom("만료된방-책-1", "isbn4", "과학-방-5일전-활동마감", LocalDate.now().minusDays(30), LocalDate.now().minusDays(5), 10, RoomStatus.EXPIRED); changeRoomMemberCount(expiredRoom1, 7); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -298,40 +300,40 @@ void get_my_rooms_wrong_type() throws Exception { @DisplayName("한번에 최대 10개의 데이터만을 반환한다. 다음 페이지에 해당하는 데이터가 있을 경우, 다음 페이지의 cursor 값을 반환한다.") void get_my_rooms_page_1() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom2, 8); - RoomJpaEntity recruitingRoom3 = saveScienceRoom("모집중인방-책-3", "isbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom3 = saveScienceRoom("모집중인방-책-3", "isbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom3, 8); - RoomJpaEntity recruitingRoom4 = saveScienceRoom("모집중인방-책-4", "isbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom4 = saveScienceRoom("모집중인방-책-4", "isbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom4, 8); - RoomJpaEntity recruitingRoom5 = saveScienceRoom("모집중인방-책-5", "isbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom5 = saveScienceRoom("모집중인방-책-5", "isbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom5, 8); - RoomJpaEntity recruitingRoom6 = saveScienceRoom("모집중인방-책-6", "isbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom6 = saveScienceRoom("모집중인방-책-6", "isbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom6, 8); - RoomJpaEntity recruitingRoom7 = saveScienceRoom("모집중인방-책-7", "isbn7", "과학-방-7일뒤-활동시작", LocalDate.now().plusDays(7), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom7 = saveScienceRoom("모집중인방-책-7", "isbn7", "과학-방-7일뒤-활동시작", LocalDate.now().plusDays(7), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom7, 8); - RoomJpaEntity recruitingRoom8 = saveScienceRoom("모집중인방-책-8", "isbn8", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom8 = saveScienceRoom("모집중인방-책-8", "isbn8", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom8, 8); - RoomJpaEntity recruitingRoom9 = saveScienceRoom("모집중인방-책-9", "isbn9", "과학-방-9일뒤-활동시작", LocalDate.now().plusDays(9), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom9 = saveScienceRoom("모집중인방-책-9", "isbn9", "과학-방-9일뒤-활동시작", LocalDate.now().plusDays(9), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom9, 8); - RoomJpaEntity recruitingRoom10 = saveScienceRoom("모집중인방-책-10", "isbn10", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom10 = saveScienceRoom("모집중인방-책-10", "isbn10", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom10, 8); - RoomJpaEntity recruitingRoom11 = saveScienceRoom("모집중인방-책-11", "isbn11", "과학-방-11일뒤-활동시작", LocalDate.now().plusDays(11), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom11 = saveScienceRoom("모집중인방-책-11", "isbn11", "과학-방-11일뒤-활동시작", LocalDate.now().plusDays(11), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom11, 8); - RoomJpaEntity recruitingRoom12 = saveScienceRoom("모집중인방-책-12", "isbn12", "과학-방-12일뒤-활동시작", LocalDate.now().plusDays(12), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom12 = saveScienceRoom("모집중인방-책-12", "isbn12", "과학-방-12일뒤-활동시작", LocalDate.now().plusDays(12), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom12, 8); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -377,40 +379,40 @@ void get_my_rooms_page_1() throws Exception { @DisplayName("cursor 값을 기준으로 해당 페이지의 데이터를 반환한다.") void get_my_rooms_page_2() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom2, 8); - RoomJpaEntity recruitingRoom3 = saveScienceRoom("모집중인방-책-3", "isbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom3 = saveScienceRoom("모집중인방-책-3", "isbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom3, 8); - RoomJpaEntity recruitingRoom4 = saveScienceRoom("모집중인방-책-4", "isbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom4 = saveScienceRoom("모집중인방-책-4", "isbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom4, 8); - RoomJpaEntity recruitingRoom5 = saveScienceRoom("모집중인방-책-5", "isbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom5 = saveScienceRoom("모집중인방-책-5", "isbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom5, 8); - RoomJpaEntity recruitingRoom6 = saveScienceRoom("모집중인방-책-6", "isbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom6 = saveScienceRoom("모집중인방-책-6", "isbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom6, 8); - RoomJpaEntity recruitingRoom7 = saveScienceRoom("모집중인방-책-7", "isbn7", "과학-방-7일뒤-활동시작", LocalDate.now().plusDays(7), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom7 = saveScienceRoom("모집중인방-책-7", "isbn7", "과학-방-7일뒤-활동시작", LocalDate.now().plusDays(7), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom7, 8); - RoomJpaEntity recruitingRoom8 = saveScienceRoom("모집중인방-책-8", "isbn8", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom8 = saveScienceRoom("모집중인방-책-8", "isbn8", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom8, 8); - RoomJpaEntity recruitingRoom9 = saveScienceRoom("모집중인방-책-9", "isbn9", "과학-방-9일뒤-활동시작", LocalDate.now().plusDays(9), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom9 = saveScienceRoom("모집중인방-책-9", "isbn9", "과학-방-9일뒤-활동시작", LocalDate.now().plusDays(9), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom9, 8); - RoomJpaEntity recruitingRoom10 = saveScienceRoom("모집중인방-책-10", "isbn10", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom10 = saveScienceRoom("모집중인방-책-10", "isbn10", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom10, 8); - RoomJpaEntity recruitingRoom11 = saveScienceRoom("모집중인방-책-11", "isbn11", "과학-방-11일뒤-활동시작", LocalDate.now().plusDays(11), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom11 = saveScienceRoom("모집중인방-책-11", "isbn11", "과학-방-11일뒤-활동시작", LocalDate.now().plusDays(11), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom11, 8); - RoomJpaEntity recruitingRoom12 = saveScienceRoom("모집중인방-책-12", "isbn12", "과학-방-12일뒤-활동시작", LocalDate.now().plusDays(12), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom12 = saveScienceRoom("모집중인방-책-12", "isbn12", "과학-방-12일뒤-활동시작", LocalDate.now().plusDays(12), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom12, 8); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -450,16 +452,16 @@ void get_my_rooms_page_2() throws Exception { @DisplayName("유저가 나간 방(= 모집기간 중 참여 취소한 경우, 진행기간 중 방 나간 경우) 은 조회되지 않는다.") void get_my_rooms_about_exit_rooms() throws Exception { //given - RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruitingRoom1, 5); - RoomJpaEntity exitRecruitingRoom = saveScienceRoom("모집중인방-책-2", "isbn2", "참여신청하고-나간-모집중인-과학-방", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10); + RoomJpaEntity exitRecruitingRoom = saveScienceRoom("모집중인방-책-2", "isbn2", "참여신청하고-나간-모집중인-과학-방", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(exitRecruitingRoom, 8); - RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10); + RoomJpaEntity playingRoom1 = saveScienceRoom("진행중인방-책-1", "isbn3", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playingRoom1, 6); - RoomJpaEntity exitPlayingRoom = saveScienceRoom("진행중인방-책-2", "isbn4", "모임진행중-나간-진행중인-과학-방", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10); + RoomJpaEntity exitPlayingRoom = saveScienceRoom("진행중인방-책-2", "isbn4", "모임진행중-나간-진행중인-과학-방", LocalDate.now().minusDays(5), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(exitPlayingRoom, 6); Alias scienceAlias = TestEntityFactory.createScienceAlias(); @@ -497,31 +499,31 @@ void get_my_rooms_about_exit_rooms() throws Exception { void get_my_playing_and_recruiting_rooms_pagination() throws Exception { // given // 진행중인 방 6개 (endDate 임박 순서: +1d ~ +6d) - RoomJpaEntity playing1 = saveScienceRoom("진행중인방-책-P1", "pisbn1", "과학-방-1일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(1), 10); + RoomJpaEntity playing1 = saveScienceRoom("진행중인방-책-P1", "pisbn1", "과학-방-1일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(1), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playing1, 3); - RoomJpaEntity playing2 = saveScienceRoom("진행중인방-책-P2", "pisbn2", "과학-방-2일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(2), 10); + RoomJpaEntity playing2 = saveScienceRoom("진행중인방-책-P2", "pisbn2", "과학-방-2일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(2), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playing2, 4); - RoomJpaEntity playing3 = saveScienceRoom("진행중인방-책-P3", "pisbn3", "과학-방-3일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(3), 10); + RoomJpaEntity playing3 = saveScienceRoom("진행중인방-책-P3", "pisbn3", "과학-방-3일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(3), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playing3, 5); - RoomJpaEntity playing4 = saveScienceRoom("진행중인방-책-P4", "pisbn4", "과학-방-4일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(4), 10); + RoomJpaEntity playing4 = saveScienceRoom("진행중인방-책-P4", "pisbn4", "과학-방-4일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(4), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playing4, 6); - RoomJpaEntity playing5 = saveScienceRoom("진행중인방-책-P5", "pisbn5", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(5), 10); + RoomJpaEntity playing5 = saveScienceRoom("진행중인방-책-P5", "pisbn5", "과학-방-5일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(5), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playing5, 7); - RoomJpaEntity playing6 = saveScienceRoom("진행중인방-책-P6", "pisbn6", "과학-방-6일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(6), 10); + RoomJpaEntity playing6 = saveScienceRoom("진행중인방-책-P6", "pisbn6", "과학-방-6일뒤-활동마감", LocalDate.now().minusDays(10), LocalDate.now().plusDays(6), 10, RoomStatus.IN_PROGRESS); changeRoomMemberCount(playing6, 8); // 모집중인 방 6개 (startDate 임박 순서: +1d ~ +6d) - RoomJpaEntity recruiting1 = saveScienceRoom("모집중인방-책-R1", "risbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruiting1 = saveScienceRoom("모집중인방-책-R1", "risbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruiting1, 3); - RoomJpaEntity recruiting2 = saveScienceRoom("모집중인방-책-R2", "risbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruiting2 = saveScienceRoom("모집중인방-책-R2", "risbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruiting2, 4); - RoomJpaEntity recruiting3 = saveScienceRoom("모집중인방-책-R3", "risbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruiting3 = saveScienceRoom("모집중인방-책-R3", "risbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruiting3, 5); - RoomJpaEntity recruiting4 = saveScienceRoom("모집중인방-책-R4", "risbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruiting4 = saveScienceRoom("모집중인방-책-R4", "risbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruiting4, 6); - RoomJpaEntity recruiting5 = saveScienceRoom("모집중인방-책-R5", "risbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruiting5 = saveScienceRoom("모집중인방-책-R5", "risbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruiting5, 7); - RoomJpaEntity recruiting6 = saveScienceRoom("모집중인방-책-R6", "risbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10); + RoomJpaEntity recruiting6 = saveScienceRoom("모집중인방-책-R6", "risbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10, RoomStatus.RECRUITING); changeRoomMemberCount(recruiting6, 8); Alias alias = TestEntityFactory.createScienceAlias(); diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java index 5efa7075f..dde07a0b3 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java @@ -6,6 +6,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; @@ -162,6 +163,7 @@ void verifyRoomPassword_recruitmentPeriodExpired() throws Exception { .startDate(LocalDate.now()) // 오늘 시작이므로 모집기간은 어제까지 .endDate(LocalDate.now().plusDays(5)) .recruitCount(3) + .roomStatus(RoomStatus.IN_PROGRESS) // 이미 시작된 방 .build() ); Long expiredRoomId = expiredRoom.getRoomId(); diff --git a/src/test/java/konkuk/thip/room/domain/RoomTest.java b/src/test/java/konkuk/thip/room/domain/RoomTest.java index a8d051334..03f76c043 100644 --- a/src/test/java/konkuk/thip/room/domain/RoomTest.java +++ b/src/test/java/konkuk/thip/room/domain/RoomTest.java @@ -3,6 +3,7 @@ import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -187,7 +188,8 @@ void isRecruitmentPeriodExpired_expired() { "제목", "설명", false, "1234", start, start.plusDays(10), 5, 123L, validCategory ); - setField(room, "startDate", today); // 모집기간 만료 상태를 강제로 만든 후 검증 + setField(room, "startDate", today); + setField(room, "roomStatus", RoomStatus.IN_PROGRESS); // 모집기간 만료 상태를 강제로 만든 후 검증 assertTrue(room.isRecruitmentPeriodExpired()); } @@ -199,7 +201,8 @@ void verifyPassword_recruitmentPeriodExpired() { "제목", "설명", false, "1234", startExpired, END, 5, 123L, validCategory ); - setField(room, "startDate", today); // 모집기간 만료 상태를 강제로 만든 후 검증 + setField(room, "startDate", today); + setField(room, "roomStatus", RoomStatus.IN_PROGRESS); // 모집기간 만료 상태를 강제로 만든 후 검증 InvalidStateException ex = assertThrows(InvalidStateException.class, () -> room.verifyPassword("1234")); assertEquals(ErrorCode.ROOM_RECRUITMENT_PERIOD_EXPIRED, ex.getErrorCode()); From 3f9abb955d5f71599da97fd788be8989909eeeaa Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 12 Sep 2025 16:48:59 +0900 Subject: [PATCH 29/89] =?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 30/89] =?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 31/89] =?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 32/89] =?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 33/89] =?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 34/89] =?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 a2297a13729f0f00a625c5aaa633ed999051ba87 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 17:04:46 +0900 Subject: [PATCH 35/89] =?UTF-8?q?[refactor]=20=ED=88=AC=ED=91=9C=ED=95=98?= =?UTF-8?q?=EA=B8=B0,=20=ED=88=AC=ED=91=9C=EC=82=AD=EC=A0=9C=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20api=EC=97=90=EC=84=9C=20=EB=B0=A9=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EA=B2=80=EC=A6=9D=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/swagger/SwaggerResponseDescription.java | 9 ++++++--- .../roompost/application/service/VoteDeleteService.java | 7 +++++++ .../thip/roompost/application/service/VoteService.java | 7 +++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index e57e6c0da..225f0603b 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -183,12 +183,14 @@ public enum SwaggerResponseDescription { VOTE_DELETE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, VOTE_NOT_FOUND, - VOTE_ACCESS_FORBIDDEN + VOTE_ACCESS_FORBIDDEN, + ROOM_IS_EXPIRED ))), VOTE_UPDATE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, VOTE_NOT_FOUND, - VOTE_ACCESS_FORBIDDEN + VOTE_ACCESS_FORBIDDEN, + ROOM_IS_EXPIRED ))), @@ -337,7 +339,8 @@ public enum SwaggerResponseDescription { ATTENDANCE_CHECK_DELETE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, ATTENDANCE_CHECK_NOT_FOUND, - ATTENDANCE_CHECK_CAN_NOT_DELETE + ATTENDANCE_CHECK_CAN_NOT_DELETE, + ROOM_IS_EXPIRED ))), // Notiification diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java index 5fa410ef6..1d6b371b2 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java @@ -3,7 +3,9 @@ import jakarta.transaction.Transactional; import konkuk.thip.comment.application.port.out.CommentCommandPort; import konkuk.thip.post.application.port.out.PostLikeCommandPort; +import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.in.VoteDeleteUseCase; import konkuk.thip.roompost.application.port.in.dto.vote.VoteDeleteCommand; import konkuk.thip.roompost.application.port.out.VoteCommandPort; @@ -18,6 +20,7 @@ public class VoteDeleteService implements VoteDeleteUseCase { private final VoteCommandPort voteCommandPort; private final CommentCommandPort commentCommandPort; private final PostLikeCommandPort postLikeCommandPort; + private final RoomCommandPort roomCommandPort; private final RoomParticipantValidator roomParticipantValidator; @@ -28,6 +31,10 @@ public Long deleteVote(VoteDeleteCommand command) { // 1. 방 참여자 검증 roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId()); + // 1.1 방 존재 여부 및 만료 검증 + Room room = roomCommandPort.getByIdOrThrow(command.roomId()); + room.validateRoomExpired(); + // 2. 투표 조회 및 검증 Vote vote = voteCommandPort.getByIdOrThrow(command.voteId()); // 2-1. 투표 삭제 권한 검증 diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteService.java index 58487d2db..e0b6658e9 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteService.java @@ -2,7 +2,9 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.in.VoteUseCase; import konkuk.thip.roompost.application.port.in.dto.vote.VoteCommand; import konkuk.thip.roompost.application.port.in.dto.vote.VoteResult; @@ -25,6 +27,7 @@ public class VoteService implements VoteUseCase { private final VoteCommandPort voteCommandPort; private final VoteQueryPort voteQueryPort; private final RoomParticipantValidator roomParticipantValidator; + private final RoomCommandPort roomCommandPort; @Override @Transactional @@ -32,6 +35,10 @@ public VoteResult vote(VoteCommand command) { // 1. 방 참가자 검증 roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId()); + // 1.1 방 존재 여부 및 만료 검증 + Room room = roomCommandPort.getByIdOrThrow(command.roomId()); + room.validateRoomExpired(); + if (command.type()) { // 투표하기 voteOrUpdate(command.userId(), command.voteId(), command.voteItemId()); From 04145629482d94c47d9fdce319a5175216b3e1fa Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 17:15:48 +0900 Subject: [PATCH 36/89] =?UTF-8?q?[fix]=20=ED=88=AC=ED=91=9C=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20RoomCommandPort=20Mock=20=EC=B6=94=EA=B0=80=20(#295?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/roompost/application/service/VoteServiceTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java b/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java index 35d593186..240de2a07 100644 --- a/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java +++ b/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java @@ -3,8 +3,8 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; -import konkuk.thip.roompost.application.service.VoteService; import konkuk.thip.roompost.application.port.in.dto.vote.VoteCommand; import konkuk.thip.roompost.application.port.in.dto.vote.VoteResult; import konkuk.thip.roompost.application.port.out.VoteCommandPort; @@ -30,13 +30,15 @@ class VoteServiceTest { private VoteQueryPort voteQueryPort; private RoomParticipantValidator roomParticipantValidator; private VoteService voteService; + private RoomCommandPort roomCommandPort; @BeforeEach void setUp() { voteCommandPort = mock(VoteCommandPort.class); voteQueryPort = mock(VoteQueryPort.class); roomParticipantValidator = mock(RoomParticipantValidator.class); - voteService = new VoteService(voteCommandPort, voteQueryPort, roomParticipantValidator); + roomCommandPort = mock(RoomCommandPort.class); + voteService = new VoteService(voteCommandPort, voteQueryPort, roomParticipantValidator, roomCommandPort); } private void mockVoteQueryResult(Long voteId, Long voteItemId, String itemName, boolean isVoted, int count) { From a99e536ff100abfef810bfbb51586c2fc308c964 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 12 Sep 2025 17:23:05 +0900 Subject: [PATCH 37/89] =?UTF-8?q?[fix]=20=ED=88=AC=ED=91=9C=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20Room=20=EC=8A=A4=ED=85=81=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/roompost/application/service/VoteServiceTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java b/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java index 240de2a07..d8371a8b2 100644 --- a/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java +++ b/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java @@ -5,6 +5,7 @@ import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; +import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.in.dto.vote.VoteCommand; import konkuk.thip.roompost.application.port.in.dto.vote.VoteResult; import konkuk.thip.roompost.application.port.out.VoteCommandPort; @@ -38,6 +39,11 @@ void setUp() { voteQueryPort = mock(VoteQueryPort.class); roomParticipantValidator = mock(RoomParticipantValidator.class); roomCommandPort = mock(RoomCommandPort.class); + + Room mockRoom = mock(Room.class); + doNothing().when(mockRoom).validateRoomExpired(); + when(roomCommandPort.getByIdOrThrow(anyLong())).thenReturn(mockRoom); + voteService = new VoteService(voteCommandPort, voteQueryPort, roomParticipantValidator, roomCommandPort); } From af159e1a4101f005ed68c197bcbf53d3f614f763 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 13 Sep 2025 17:02:31 +0900 Subject: [PATCH 38/89] =?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 1f07fefcce96812580a649f62cad0472e02f9c0f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 13 Sep 2025 21:50:03 +0900 Subject: [PATCH 39/89] =?UTF-8?q?[refactor]=20RoomQueryDto=EC=97=90=20room?= =?UTF-8?q?Status=20=EC=B6=94=EA=B0=80=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RoomQueryRepositoryImpl.java | 18 ++++++++++++------ .../application/port/out/dto/RoomQueryDto.java | 14 +++++++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java index 6aeacf18f..3ff22a9e3 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java @@ -83,7 +83,8 @@ private QRoomQueryDto projectionForRecruitingRoomSearch() { room.recruitCount, room.memberCount, room.startDate, - room.isPublic + room.isPublic, + room.roomStatus ); } @@ -329,7 +330,8 @@ public List findRoomsByCategoryOrderByStartDateAsc(Category catego room.title, room.recruitCount, room.memberCount, - room.startDate + room.startDate, + room.roomStatus )) .from(room) .join(room.bookJpaEntity, book) @@ -348,7 +350,8 @@ public List findRoomsByCategoryOrderByMemberCount(Category categor room.title, room.recruitCount, room.memberCount, - room.startDate + room.startDate, + room.roomStatus )) .from(room) .join(room.bookJpaEntity, book) @@ -379,7 +382,8 @@ public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalD room.recruitCount, room.memberCount, cursorExpr, - room.isPublic + room.isPublic, + room.roomStatus )) .from(room) .join(room.bookJpaEntity, book) @@ -443,7 +447,8 @@ private List fetchMyRooms( room.memberCount, room.startDate, cursorExpr, // endDate 자리에 상황별 deadline 컬럼 전달 - room.isPublic + room.isPublic, + room.roomStatus )) .from(participant) .join(participant.roomJpaEntity, room) @@ -490,7 +495,8 @@ private List fetchMyRoomsWithPriority( room.memberCount, room.startDate, cursorExpr, // endDate 자리에 상황별 deadline 컬럼 전달 - room.isPublic + room.isPublic, + room.roomStatus )) .from(participant) .join(participant.roomJpaEntity, room) diff --git a/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java b/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java index ac466fd11..8a65e8f80 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java +++ b/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java @@ -1,6 +1,7 @@ package konkuk.thip.room.application.port.out.dto; import com.querydsl.core.annotations.QueryProjection; +import konkuk.thip.room.domain.value.RoomStatus; import lombok.Builder; import org.springframework.util.Assert; @@ -16,7 +17,8 @@ public record RoomQueryDto( Integer memberCount, LocalDate startDate, // 방 진행 시작일 LocalDate endDate, // 방 진행 마감일 or 방 모집 마감일 - Boolean isPublic // 공개방 여부 + Boolean isPublic, // 공개방 여부 + RoomStatus roomStatus ) { // 내가 참여한 모임방(모집중, 진행중, 모집+진행중, 완료된) 조회 시 활용 @QueryProjection @@ -36,9 +38,10 @@ public RoomQueryDto( String roomName, Integer recruitCount, Integer memberCount, - LocalDate endDate + LocalDate endDate, + RoomStatus roomStatus ) { - this(roomId, bookImageUrl, roomName, recruitCount, memberCount, null, endDate, null); + this(roomId, bookImageUrl, roomName, recruitCount, memberCount, null, endDate, null, roomStatus); } // 방 검색 시 활용 @@ -50,8 +53,9 @@ public RoomQueryDto( Integer recruitCount, Integer memberCount, LocalDate endDate, - Boolean isPublic + Boolean isPublic, + RoomStatus roomStatus ) { - this(roomId, bookImageUrl, roomName, recruitCount, memberCount, null, endDate, isPublic); + this(roomId, bookImageUrl, roomName, recruitCount, memberCount, null, endDate, isPublic, roomStatus); } } From a06af4770040ed41be2001d104b1d8174cf48804 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 13 Sep 2025 22:07:19 +0900 Subject: [PATCH 40/89] =?UTF-8?q?[refactor]=20=EC=A7=84=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=A9=EC=9D=B4=20=EC=95=84=EB=8B=8C=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EC=9D=98=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index f944caa6b..50f51b6d7 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -101,6 +101,7 @@ public enum ErrorCode implements ResponseCode { ROOM_MEMBER_COUNT_UNDERFLOW(HttpStatus.BAD_REQUEST, 100007, "방의 인원 수가 1 이하(방장 포함)입니다."), ROOM_IS_EXPIRED(HttpStatus.BAD_REQUEST, 100008, "방이 만료되었습니다."), ROOM_POST_TYPE_NOT_MATCH(HttpStatus.BAD_REQUEST, 100009, "일치하는 방 게시물 타입 이름이 없습니다. [RECORD, VOTE] 중 하나여야 합니다."), + ROOM_NOT_IN_PROGRESS(HttpStatus.BAD_REQUEST, 100010, "진행 중인 방이 아닙니다."), /** * 110000 : vote error From ce1956d7af8909420c66e94edf0d811a4d4dc99f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 13 Sep 2025 22:07:38 +0900 Subject: [PATCH 41/89] =?UTF-8?q?[refactor]=20=EC=A7=84=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=A9=EC=9D=B4=20=EC=95=84=EB=8B=8C=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0(=EB=AA=A8=EC=A7=91=20=EC=A4=91=EC=9D=B8=20=EB=B0=A9)?= =?UTF-8?q?=EC=9D=98=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/policy/RoomPostCommentAccessPolicy.java | 2 +- .../room/application/service/validator/RoomValidator.java | 4 ++-- src/main/java/konkuk/thip/room/domain/Room.java | 5 ++++- .../application/service/AttendanceCheckCreateService.java | 2 +- .../application/service/AttendanceCheckDeleteService.java | 2 +- .../roompost/application/service/RecordCreateService.java | 2 +- .../roompost/application/service/RecordDeleteService.java | 2 +- .../roompost/application/service/RoomPostUpdateService.java | 2 +- .../thip/roompost/application/service/VoteCreateService.java | 2 +- .../thip/roompost/application/service/VoteDeleteService.java | 2 +- .../thip/roompost/application/service/VoteService.java | 2 +- .../application/service/policy/RoomPostLikeAccessPolicy.java | 2 +- .../thip/roompost/application/service/VoteServiceTest.java | 2 +- 13 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java b/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java index 01b4d45aa..434448fa2 100644 --- a/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java +++ b/src/main/java/konkuk/thip/comment/application/service/policy/RoomPostCommentAccessPolicy.java @@ -18,7 +18,7 @@ public class RoomPostCommentAccessPolicy implements CommentAccessPolicy { public void validateCommentAccess(CountUpdatable post, Long userId) { RoomPost roomPost = (RoomPost) post; roomParticipantValidator.validateUserIsRoomMember(roomPost.getRoomId(), userId); - roomValidator.validateRoomExpired(roomPost.getRoomId()); + roomValidator.validateRoomInProgress(roomPost.getRoomId()); } } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java b/src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java index 305d58473..b5d725f3b 100644 --- a/src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java +++ b/src/main/java/konkuk/thip/room/application/service/validator/RoomValidator.java @@ -11,9 +11,9 @@ public class RoomValidator { private final RoomCommandPort roomCommandPort; - public void validateRoomExpired(Long roomId) { + public void validateRoomInProgress(Long roomId) { Room room = roomCommandPort.getByIdOrThrow(roomId); - room.validateRoomExpired(); + room.validateRoomInProgress(); } } diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index 7dc402a83..378d87830 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -177,10 +177,13 @@ public void startRoomProgress() { roomStatus = RoomStatus.IN_PROGRESS; } - public void validateRoomExpired() { + public void validateRoomInProgress() { if (this.roomStatus == RoomStatus.EXPIRED) { throw new InvalidStateException(ErrorCode.ROOM_IS_EXPIRED); } + if (this.roomStatus != RoomStatus.IN_PROGRESS) { + throw new InvalidStateException(ErrorCode.ROOM_NOT_IN_PROGRESS); + } } } diff --git a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java index 62321747e..f192a126b 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckCreateService.java @@ -37,7 +37,7 @@ public AttendanceCheckCreateResult create(AttendanceCheckCreateCommand command) // 1-2. 방이 만료되었는지 검증 Room room = roomCommandPort.getByIdOrThrow(command.roomId()); - room.validateRoomExpired(); + room.validateRoomInProgress(); // 2. 유저가 해당 방에서 오늘 이미 작성한 오늘의 한마디 개수 조회 int alreadyWrittenCountToday = attendanceCheckQueryPort.countAttendanceChecksOnTodayByUser(command.creatorId(), command.roomId()); diff --git a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java index b4f55b53e..5642cf80e 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckDeleteService.java @@ -26,7 +26,7 @@ public Long delete(Long creatorId, Long roomId, Long attendanceCheckId) { // 1-1. 방이 만료되었는지 검증 Room room = roomCommandPort.getByIdOrThrow(roomId); - room.validateRoomExpired(); + room.validateRoomInProgress(); // 2. creator 겁증 AttendanceCheck attendanceCheck = attendanceCheckCommandPort.getByIdOrThrow(attendanceCheckId); 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..607c73072 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java @@ -99,7 +99,7 @@ private void validateRoomParticipant(RoomParticipant roomParticipant, boolean is private void validateRoom(Room room) { // 방이 만료되었는지 검증 - room.validateRoomExpired(); + room.validateRoomInProgress(); } private void validateRecord(Record record, Book book) { diff --git a/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java index 2f4215d2f..c40070eb8 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java @@ -32,7 +32,7 @@ public Long deleteRecord(RecordDeleteCommand command) { roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId()); Room room = roomCommandPort.getByIdOrThrow(command.roomId()); - room.validateRoomExpired(); + room.validateRoomInProgress(); // 2. 기록 조회 및 검증 Record record = recordCommandPort.getByIdOrThrow(command.recordId()); diff --git a/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java b/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java index a4a9bfc4e..adbb1c38a 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RoomPostUpdateService.java @@ -65,6 +65,6 @@ public Long updateVote(VoteUpdateCommand command) { private void validateRoom(Long command) { Room room = roomCommandPort.getByIdOrThrow(command); - room.validateRoomExpired(); + room.validateRoomInProgress(); } } 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 12991f796..7e5ff4635 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java @@ -55,7 +55,7 @@ public VoteCreateResult createVote(VoteCreateCommand command) { Room room = roomCommandPort.getByIdOrThrow(vote.getRoomId()); Book book = bookCommandPort.findById(room.getBookId()); validateVote(vote, book); - room.validateRoomExpired(); + room.validateRoomInProgress(); // 2. vote 저장 Long savedVoteId = voteCommandPort.saveVote(vote); diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java index 1d6b371b2..35268d868 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java @@ -33,7 +33,7 @@ public Long deleteVote(VoteDeleteCommand command) { // 1.1 방 존재 여부 및 만료 검증 Room room = roomCommandPort.getByIdOrThrow(command.roomId()); - room.validateRoomExpired(); + room.validateRoomInProgress(); // 2. 투표 조회 및 검증 Vote vote = voteCommandPort.getByIdOrThrow(command.voteId()); diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteService.java index e0b6658e9..73603e3a1 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteService.java @@ -37,7 +37,7 @@ public VoteResult vote(VoteCommand command) { // 1.1 방 존재 여부 및 만료 검증 Room room = roomCommandPort.getByIdOrThrow(command.roomId()); - room.validateRoomExpired(); + room.validateRoomInProgress(); if (command.type()) { // 투표하기 diff --git a/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java b/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java index bc827de00..70c696865 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java +++ b/src/main/java/konkuk/thip/roompost/application/service/policy/RoomPostLikeAccessPolicy.java @@ -19,6 +19,6 @@ public class RoomPostLikeAccessPolicy implements PostLikeAccessPolicy { public void validatePostLikeAccess(CountUpdatable post, Long userId) { RoomPost roomPost = (RoomPost) post; roomParticipantValidator.validateUserIsRoomMember(roomPost.getRoomId(), userId); - roomValidator.validateRoomExpired(roomPost.getRoomId()); + roomValidator.validateRoomInProgress(roomPost.getRoomId()); } } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java b/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java index d8371a8b2..7f4b85eac 100644 --- a/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java +++ b/src/test/java/konkuk/thip/roompost/application/service/VoteServiceTest.java @@ -41,7 +41,7 @@ void setUp() { roomCommandPort = mock(RoomCommandPort.class); Room mockRoom = mock(Room.class); - doNothing().when(mockRoom).validateRoomExpired(); + doNothing().when(mockRoom).validateRoomInProgress(); when(roomCommandPort.getByIdOrThrow(anyLong())).thenReturn(mockRoom); voteService = new VoteService(voteCommandPort, voteQueryPort, roomParticipantValidator, roomCommandPort); From 5e43d565fa6e8e797e5e7a8b3019ed06c1cccef3 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 13 Sep 2025 22:07:48 +0900 Subject: [PATCH 42/89] =?UTF-8?q?[docs]=20=EC=97=90=EB=9F=AC=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=B6=94=EA=B0=80=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerResponseDescription.java | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 225f0603b..477fe9fc3 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -136,7 +136,8 @@ public enum SwaggerResponseDescription { BOOK_NOT_FOUND, ROOM_IS_EXPIRED, RECORD_CANNOT_BE_OVERVIEW, - INVALID_RECORD_PAGE_RANGE + INVALID_RECORD_PAGE_RANGE, + ROOM_NOT_IN_PROGRESS ))), RECORD_SEARCH(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, @@ -148,7 +149,8 @@ public enum SwaggerResponseDescription { ROOM_ACCESS_FORBIDDEN, RECORD_NOT_FOUND, RECORD_ACCESS_FORBIDDEN, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), RECORD_PIN(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, @@ -160,7 +162,8 @@ public enum SwaggerResponseDescription { ROOM_ACCESS_FORBIDDEN, RECORD_NOT_FOUND, RECORD_ACCESS_FORBIDDEN, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), // Vote @@ -170,7 +173,8 @@ public enum SwaggerResponseDescription { BOOK_NOT_FOUND, ROOM_IS_EXPIRED, VOTE_CANNOT_BE_OVERVIEW, - INVALID_VOTE_PAGE_RANGE + INVALID_VOTE_PAGE_RANGE, + ROOM_NOT_IN_PROGRESS ))), VOTE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, @@ -178,19 +182,22 @@ public enum SwaggerResponseDescription { VOTE_ITEM_ALREADY_VOTED, VOTE_ITEM_NOT_VOTED_CANNOT_CANCEL, VOTE_ITEM_COUNT_CANNOT_BE_NEGATIVE, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), VOTE_DELETE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, VOTE_NOT_FOUND, VOTE_ACCESS_FORBIDDEN, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), VOTE_UPDATE(new LinkedHashSet<>(Set.of( ROOM_ACCESS_FORBIDDEN, VOTE_NOT_FOUND, VOTE_ACCESS_FORBIDDEN, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), @@ -257,7 +264,8 @@ public enum SwaggerResponseDescription { INVALID_COMMENT_CREATE, FEED_ACCESS_FORBIDDEN, ROOM_ACCESS_FORBIDDEN, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), CHANGE_COMMENT_LIKE_STATE(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, @@ -270,7 +278,8 @@ public enum SwaggerResponseDescription { COMMENT_LIKE_COUNT_UNDERFLOW, FEED_ACCESS_FORBIDDEN, ROOM_ACCESS_FORBIDDEN, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), COMMENT_DELETE(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, @@ -282,7 +291,8 @@ public enum SwaggerResponseDescription { COMMENT_COUNT_UNDERFLOW, FEED_ACCESS_FORBIDDEN, ROOM_ACCESS_FORBIDDEN, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), // Book @@ -329,7 +339,8 @@ public enum SwaggerResponseDescription { USER_NOT_FOUND, ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED, ATTENDANCE_CHECK_NOT_FOUND, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), ATTENDANCE_CHECK_SHOW(new LinkedHashSet<>(Set.of( @@ -340,7 +351,8 @@ public enum SwaggerResponseDescription { ROOM_ACCESS_FORBIDDEN, ATTENDANCE_CHECK_NOT_FOUND, ATTENDANCE_CHECK_CAN_NOT_DELETE, - ROOM_IS_EXPIRED + ROOM_IS_EXPIRED, + ROOM_NOT_IN_PROGRESS ))), // Notiification From d87aa324e55447175a9b4e689b40e2ca18233a3a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 13 Sep 2025 22:25:23 +0900 Subject: [PATCH 43/89] =?UTF-8?q?[test]=20TestEntityFactory=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B0=A9=20=EC=83=9D=EC=84=B1=EC=8B=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=A7=84=ED=96=89=EC=A4=91=EC=9D=B8=20=EB=B0=A9?= =?UTF-8?q?=EC=9D=B4=20=EC=83=9D=EC=84=B1=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/konkuk/thip/common/util/TestEntityFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 5f165f1af..959aeb160 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -141,6 +141,7 @@ public static RoomJpaEntity createRoom(BookJpaEntity book, Category category) { .recruitCount(3) .bookJpaEntity(book) .category(category) + .roomStatus(RoomStatus.IN_PROGRESS) .build(); } From fbaef2e360a7d7b405e468a72097c423ca5350b7 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 13 Sep 2025 22:38:28 +0900 Subject: [PATCH 44/89] =?UTF-8?q?[chore]=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RoomQueryRepositoryImpl.java | 16 +--------------- src/main/java/konkuk/thip/room/domain/Room.java | 4 ---- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java index 3ff22a9e3..c791ed28e 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java @@ -200,11 +200,9 @@ public List findHomeJoinedRoomsByUserPercentage( // 검색 조건(where) 조립 // 유저가 참여한 방만: userId 조건 - // 활동 기간 중인 방만: startDate ≤ today ≤ endDate + // 활동 기간 중인 방만: IN_PROGRESS 상태 BooleanBuilder where = new BooleanBuilder(); where.and(participant.userJpaEntity.userId.eq(userId)); -// where.and(room.startDate.loe(LocalDate.now())); -// where.and(room.endDate.goe(LocalDate.now())); where.and(room.roomStatus.eq(RoomStatus.IN_PROGRESS)); // 활동 기간 중인 방만: IN_PROGRESS 상태 // 커서 기반 추가 조건 @@ -247,9 +245,7 @@ public List findHomeJoinedRoomsByUserPercentage( public List findRecruitingRoomsUserParticipated( Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { - LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) -// .and(room.startDate.after(today)); // 유저가 참여한 방 && 모집중인 방 .and(room.roomStatus.eq(RoomStatus.RECRUITING)); // 유저가 참여한 방 && 모집중인 방 DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) OrderSpecifier[] orders = new OrderSpecifier[]{ @@ -264,10 +260,7 @@ public List findRecruitingRoomsUserParticipated( public List findPlayingRoomsUserParticipated( Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { - LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) -// .and(room.startDate.loe(today)) -// .and(room.endDate.goe(today)); // 유저가 참여한 방 && 현재 진행중인 방 .and(room.roomStatus.eq(RoomStatus.IN_PROGRESS)); // 유저가 참여한 방 && 현재 진행중인 방 DateExpression cursorExpr = room.endDate; // 커서 비교는 endDate(= 진행 마감일) OrderSpecifier[] orders = new OrderSpecifier[]{ @@ -282,9 +275,6 @@ public List findPlayingRoomsUserParticipated( public List findPlayingAndRecruitingRoomsUserParticipated( Long userId, Integer priorityCursor, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { - LocalDate today = LocalDate.now(); -// BooleanExpression playing = room.startDate.loe(today).and(room.endDate.goe(today)); -// BooleanExpression recruiting = room.startDate.after(today); BooleanExpression playing = room.roomStatus.eq(RoomStatus.IN_PROGRESS); BooleanExpression recruiting = room.roomStatus.eq(RoomStatus.RECRUITING); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) @@ -308,9 +298,7 @@ public List findPlayingAndRecruitingRoomsUserParticipated( public List findExpiredRoomsUserParticipated( Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { - LocalDate today = LocalDate.now(); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) -// .and(room.endDate.before(today)); // 유저가 참여한 방 && 만료된 방 .and(room.roomStatus.eq(RoomStatus.EXPIRED)); // 유저가 참여한 방 && 만료된 방 DateExpression cursorExpr = room.endDate; @@ -365,7 +353,6 @@ public List findRoomsByCategoryOrderByMemberCount(Category categor public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalDate dateCursor, Long roomIdCursor, int pageSize) { DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) -// .and(room.startDate.after(LocalDate.now())); // 모집 마감 시각 > 현재 시각 .and(room.roomStatus.eq(RoomStatus.RECRUITING)); // 모집중인 방 @@ -395,7 +382,6 @@ public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalD private BooleanExpression findDeadlinePopularRoomCondition(Category category, Long userId) { return room.category.eq(category) -// .and(room.startDate.after(LocalDate.now())) // 모집 마감 시각 > 현재 시각 .and(room.roomStatus.eq(RoomStatus.RECRUITING)) // 모집중인 방 .and(room.isPublic.isTrue()) // 공개 방만 조회 .and(userJoinedRoom(userId).not()); // 유저가 참여하지 않은 방만 조회 diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index 378d87830..490856180 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -141,9 +141,6 @@ ROOM_RECRUITMENT_PERIOD_EXPIRED, new IllegalArgumentException(message) } public boolean isRecruitmentPeriodExpired() { -// LocalDate today = LocalDate.now(); -// // 모집 마감일: startDate.minusDays(1) -// return today.isAfter(this.startDate.minusDays(1)); return this.roomStatus != RoomStatus.RECRUITING; } @@ -171,7 +168,6 @@ private void checkCancelPossible() { public void startRoomProgress() { validateRoomRecruitExpired(); -// validateRoomExpired(); startDate = LocalDate.now(); roomStatus = RoomStatus.IN_PROGRESS; From f25d4f25bcaf893cdf9e8e8385373e08911d795c Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 13 Sep 2025 22:39:00 +0900 Subject: [PATCH 45/89] =?UTF-8?q?[refactor]=20validateRoom=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EC=B6=9C=20=ED=95=B4=EC=A0=9C=20?= =?UTF-8?q?(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roompost/application/service/RecordCreateService.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 607c73072..0ea804f90 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java @@ -61,7 +61,7 @@ public RecordCreateResult createRecord(RecordCreateCommand command) { Book book = bookCommandPort.findById(room.getBookId()); // 3. 유효성 검증 - validateRoom(room); + room.validateRoomInProgress(); // 방이 만료되었는지 검증 validateRoomParticipant(roomParticipant, command.isOverview()); validateRecord(record, book); @@ -97,11 +97,6 @@ private void validateRoomParticipant(RoomParticipant roomParticipant, boolean is } } - private void validateRoom(Room room) { - // 방이 만료되었는지 검증 - room.validateRoomInProgress(); - } - private void validateRecord(Record record, Book book) { // 페이지 유효성 검증 record.validatePage(book.getPageCount()); From a4819f3bf7f1c7ffac0e79878047012102a62c6e Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sun, 14 Sep 2025 00:30:10 +0900 Subject: [PATCH 46/89] =?UTF-8?q?[refactor]=20RoomStatus=20not=20null=20as?= =?UTF-8?q?sertion=20=EC=B6=94=EA=B0=80=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/room/application/port/out/dto/RoomQueryDto.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java b/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java index 8a65e8f80..6a268459b 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java +++ b/src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java @@ -29,6 +29,7 @@ public record RoomQueryDto( Assert.notNull(endDate, "endDate must not be null"); Assert.notNull(recruitCount, "recruitCount must not be null"); Assert.notNull(memberCount, "memberCount must not be null"); + Assert.notNull(roomStatus, "roomStatus must not be null"); } @QueryProjection From 4c68d4c612fbbe558c12e281536a7940402b6875 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 14 Sep 2025 01:49:30 +0900 Subject: [PATCH 47/89] =?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 48/89] =?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); + } +} From 654cab76de2863cb291b52f15a5b30d3d1b1dd85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:07:19 +0900 Subject: [PATCH 49/89] =?UTF-8?q?[refactor]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20tearDown()=EC=97=90=EC=84=9C=20@=20Tran?= =?UTF-8?q?sactional=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=95=88?= =?UTF-8?q?=EC=93=B0=EB=8A=94=20=ED=95=A8=EC=88=98,=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EA=B4=80=EA=B3=84,import=EB=AC=B8=20=EC=82=AD=EC=A0=9C(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/BookChangeSavedApiTest.java | 10 ++----- .../in/web/BookDetailSearchApiTest.java | 19 +++++------- .../in/web/BookGetSelectableListApiTest.java | 12 ++------ .../in/web/BookMostSearchedBooksApiTest.java | 8 ++--- .../adapter/in/web/BookSearchApiTest.java | 11 ++----- .../in/web/BookShowSavedListApiTest.java | 12 ++------ .../in/web/CommentCreateControllerTest.java | 2 -- .../adapter/in/web/CommentDeleteApiTest.java | 28 ++++++++--------- .../adapter/in/web/CommentShowAllApiTest.java | 11 ++----- .../CommentCommandPersistenceAdapterTest.java | 6 ---- .../thip/comment/domain/CommentTest.java | 16 ---------- .../common/persistence/StatusFilterTest.java | 9 +----- .../in/web/BasicFeedShowAllApiTest.java | 13 ++------ .../adapter/in/web/FeedShowMineApiTest.java | 25 ++-------------- .../adapter/in/web/FeedShowSingleApiTest.java | 18 ++--------- .../in/web/FeedShowSpecificUserApiTest.java | 16 ++-------- .../in/web/FeedShowUserInfoApiTest.java | 25 ++++++++++------ .../in/web/FeedUpdateControllerTest.java | 2 -- .../FollowingPriorityFeedShowAllApiTest.java | 13 ++------ .../konkuk/thip/feed/domain/FeedTest.java | 24 --------------- .../adapter/in/web/RoomCreateApiTest.java | 11 +------ .../in/web/RoomGetHomeJoinedRoomsApiTest.java | 21 +++++-------- .../in/web/RoomGetMemberListApiTest.java | 11 ++----- .../room/adapter/in/web/RoomJoinApiTest.java | 19 +++++------- .../web/RoomRecruitingDetailViewApiTest.java | 15 ++-------- .../adapter/in/web/RoomShowMineApiTest.java | 12 ++------ .../in/web/RoomVerifyPasswordApiTest.java | 9 ++---- .../in/web/AttendanceCheckCreateApiTest.java | 12 ++------ .../in/web/AttendanceCheckDeleteApiTest.java | 19 +++++------- .../in/web/AttendanceCheckShowApiTest.java | 12 ++------ .../adapter/in/web/VoteCreateApiTest.java | 18 ++--------- .../service/VoteCreateServiceTest.java | 26 ++-------------- .../adapter/in/web/UserDeleteApiTest.java | 30 ++++--------------- .../adapter/in/web/UserFollowApiTest.java | 12 ++------ .../in/web/UserIsFollowingApiTest.java | 9 ++---- .../UserShowFollowingsInFeedViewApiTest.java | 11 ++----- .../in/web/UserSignupControllerTest.java | 8 ++--- .../adapter/in/web/UserUpdateApiTest.java | 8 ++--- .../web/UserVerifyNicknameControllerTest.java | 8 ++--- 39 files changed, 119 insertions(+), 432 deletions(-) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java index fe5aa6855..45dd85b8f 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.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; @@ -24,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.Optional; @@ -36,6 +36,7 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") +@Transactional @DisplayName("[통합] 방 저장상태 변경 api 통합 테스트") class BookChangeSavedApiTest { @@ -90,13 +91,6 @@ void setUp() { } - @AfterEach - void tearDown() { - savedBookJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } - @Test @DisplayName("DB에 책이 존재하고 해당 책을 저장하려고 할때 [책 저장 성공]") void saveBook_success() throws Exception { diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java index 28fecf312..608f77ece 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java @@ -1,5 +1,6 @@ package konkuk.thip.book.adapter.in.web; +import jakarta.persistence.EntityManager; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.book.application.service.BookSearchService; @@ -19,7 +20,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; @@ -27,6 +27,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -35,6 +36,7 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") +@Transactional @DisplayName("[통합] 책 상세보기 api 통합 테스트") class BookDetailSearchApiTest { @@ -52,6 +54,8 @@ class BookDetailSearchApiTest { private FeedJpaRepository feedJpaRepository; @Autowired private SavedBookJpaRepository savedBookJpaRepository; + @Autowired + EntityManager em; @BeforeEach void setup() { @@ -90,6 +94,9 @@ void setup() { .category(category) .build()); + em.flush(); + em.clear(); + roomParticipantJpaRepository.save(RoomParticipantJpaEntity.builder() .currentPage(10) .userPercentage(10.0) @@ -112,16 +119,6 @@ void setup() { .build()); } - @AfterEach - void tearDown() { - savedBookJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("책 상세 검색 결과를 정상적으로 반환.") void searchDetailBooks_ReturnsCorrectResult() { diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java index 030332ce1..64694759a 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java @@ -15,7 +15,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.hamcrest.Matchers; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +24,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDate; @@ -39,6 +39,7 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") +@Transactional @DisplayName("[통합] 저장한 책 및 참여 중 책 리스트 조회 API 통합 테스트") class BookGetSelectableListApiTest { @@ -52,15 +53,6 @@ class BookGetSelectableListApiTest { @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - savedBookJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - private RoomJpaEntity saveScienceRoomWithBookIsbn(String isbn, String roomName, double roomPercentage) { BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() .title("책이름") diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java index a1cec3e9d..8069449ab 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java @@ -8,7 +8,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; @@ -21,6 +20,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -34,6 +34,7 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") +@Transactional @DisplayName("[통합] 가장 많이 검색된 책 조회 API 통합 테스트") class BookMostSearchedBooksApiTest { @@ -72,11 +73,6 @@ void setUp() { .build()); } - @AfterEach - void tearDown() { - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("어제 랭킹 Top 5를 정상적으로 조회한다") void getMostSearchedBooks_returnsRankList() throws Exception { diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java index a2f307559..f480dc4ad 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java @@ -10,7 +10,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; @@ -20,6 +19,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -32,7 +32,8 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -@DisplayName("[통합] 방 검색 api 통합 테스트") +@DisplayName("[통합] 책 검색 api 통합 테스트") +@Transactional class BookSearchApiTest { @Autowired @@ -70,12 +71,6 @@ void setUp() { testToken = jwtUtil.createAccessToken(user.getUserId()); } - @AfterEach - void tearDown() { - recentSearchJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("책 검색 API 정상 호출 - 키워드와 페이지 번호가 주어졌을 때") void searchBooks_success() throws Exception { diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java index f8a49f643..559bfd07d 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java @@ -11,7 +11,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.hamcrest.Matchers; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +20,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -34,6 +34,7 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") +@Transactional @DisplayName("[통합] 저장한 책 조회 API 통합 테스트") class BookShowSavedListApiTest { @@ -47,15 +48,6 @@ class BookShowSavedListApiTest { @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - savedBookJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } - @Test @DisplayName("저장된 책 조회 시 책 정보를 저장한 최신순으로 정렬해서 반환한다.") void getSavedBooks_success() throws Exception { diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java index 7f9346f20..4eb90f0d1 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; -import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; @@ -58,7 +57,6 @@ class CommentCreateControllerTest { @Autowired private FeedJpaRepository feedJpaRepository; @Autowired private VoteJpaRepository voteJpaRepository; @Autowired private RecordJpaRepository recordJpaRepository; - @Autowired private CommentJpaRepository commentJpaRepository; @Autowired private RoomJpaRepository roomJpaRepository; @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java index e0bdbd54a..b33bc4bd8 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java @@ -1,5 +1,6 @@ package konkuk.thip.comment.adapter.in.web; +import jakarta.persistence.EntityManager; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; @@ -20,7 +21,6 @@ import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; 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; @@ -29,6 +29,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import static konkuk.thip.common.entity.StatusType.INACTIVE; import static konkuk.thip.post.domain.PostType.FEED; @@ -38,6 +39,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 댓글 삭제 api 통합 테스트") class CommentDeleteApiTest { @@ -55,6 +57,8 @@ class CommentDeleteApiTest { @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; @Autowired private CommentLikeJpaRepository commentLikeJpaRepository; + @Autowired private EntityManager em; + private Alias alias; private UserJpaEntity user; private Category category; @@ -77,19 +81,6 @@ record = recordJpaRepository.save(TestEntityFactory.createRecord(user,room)); roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, user, RoomParticipantRole.HOST, 0.0)); } - @AfterEach - void tearDown() { - recordJpaRepository.deleteAllInBatch(); - voteJpaRepository.deleteAllInBatch(); - commentLikeJpaRepository.deleteAll(); - commentJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("루트댓글을 삭제하면 [soft delete 처리]된다") void deleteRootComment_success() throws Exception { @@ -105,6 +96,9 @@ void deleteRootComment_success() throws Exception { .requestAttr("userId", user.getUserId())) .andExpect(status().isOk()); + em.flush(); + em.clear(); + // then CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); @@ -126,6 +120,9 @@ void deleteReplyComment_success() throws Exception { .requestAttr("userId", user.getUserId())) .andExpect(status().isOk()); + em.flush(); + em.clear(); + // then CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(replyId).orElse(null); assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); @@ -149,6 +146,9 @@ void deleteComment_success() throws Exception { .requestAttr("userId", user.getUserId())) .andExpect(status().isOk()); + em.flush(); + em.clear(); + // then CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java index 9f0b13de4..89e52e4a6 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java @@ -23,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -37,6 +38,7 @@ @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 댓글 조회 api 통합 테스트") +@Transactional class CommentShowAllApiTest { @Autowired private MockMvc mockMvc; @@ -49,15 +51,6 @@ class CommentShowAllApiTest { private static final String FEED_POST_TYPE = PostType.FEED.getType(); - @AfterEach - void tearDown() { - commentLikeJpaRepository.deleteAllInBatch(); - commentJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("댓글 조회 요청에 대하여, 특정 게시글(= 피드, 기록, 투표)의 루트 댓글, 루트 댓글의 모든 자식 댓글의 데이터를 구분하여 반환한다.") void comment_show_all_test() throws Exception { diff --git a/src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java b/src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java index 875360766..3e7fdcf17 100644 --- a/src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java @@ -6,15 +6,12 @@ import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; import konkuk.thip.comment.adapter.out.mapper.CommentMapper; import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; -import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.entity.StatusType; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.config.TestQuerydslConfig; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.domain.PostType; -import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; -import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; @@ -37,12 +34,9 @@ class CommentCommandPersistenceAdapterTest { @Autowired CommentCommandPersistenceAdapter adapter; // repository 인터페이스가 아니므로 자동 스캔 X -> import 해줘야함 @Autowired CommentJpaRepository commentJpaRepository; - @Autowired CommentLikeJpaRepository commentLikeJpaRepository; @Autowired BookJpaRepository bookJpaRepository; @Autowired FeedJpaRepository feedJpaRepository; @Autowired UserJpaRepository userJpaRepository; - @Autowired RecordJpaRepository recordJpaRepository; - @Autowired VoteJpaRepository voteJpaRepository; @MockitoBean CommentMapper commentMapper; // Mock bean 으로 설정 diff --git a/src/test/java/konkuk/thip/comment/domain/CommentTest.java b/src/test/java/konkuk/thip/comment/domain/CommentTest.java index a33a2b877..c608f474e 100644 --- a/src/test/java/konkuk/thip/comment/domain/CommentTest.java +++ b/src/test/java/konkuk/thip/comment/domain/CommentTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import static konkuk.thip.common.entity.StatusType.ACTIVE; -import static konkuk.thip.common.entity.StatusType.INACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.*; import static konkuk.thip.post.domain.PostType.FEED; import static org.junit.jupiter.api.Assertions.*; @@ -32,21 +31,6 @@ private Comment createParentComment(Long postId) { .build(); } - - private Comment createInactiveComment(Long postId) { - return Comment.builder() - .id(124L) //ID 임의 주입 - .content(CONTENT) - .targetPostId(postId) - .creatorId(CREATOR_ID) - .postType(FEED) - .parentCommentId(null) - .reportCount(0) - .likeCount(0) - .status(INACTIVE) - .build(); - } - @Test @DisplayName("createComment: 일반 댓글 생성 시 parentId는 null이면 정상적으로 Comment가 생성된다.") void createRootComment_valid() { diff --git a/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java index 238491f34..8277ec669 100644 --- a/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java +++ b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java @@ -8,7 +8,6 @@ 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +23,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional public class StatusFilterTest { @Autowired private UserJpaRepository userJpaRepository; @@ -37,13 +37,6 @@ public class StatusFilterTest { @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - public void tearDown() { - savedBookJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - private void saveActiveUser(int count) { for (int i = 1; i <= count; i++) { UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "activeUser" + i); diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java index cd6c59e9c..b9760a385 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java @@ -13,7 +13,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +21,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -38,6 +38,7 @@ ) @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 피드 전체 조회(최신순 조회) api 통합 테스트") class BasicFeedShowAllApiTest { @Autowired @@ -64,16 +65,6 @@ class BasicFeedShowAllApiTest { @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - postLikeJpaRepository.deleteAllInBatch(); - savedFeedJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("피드 조회를 요청할 경우, [feedId, 작성자 닉네임, ,,] 의 피드 정보를 최신순으로 정렬해서 반환한다.") void feed_show_all_test() throws Exception { diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java index 7636df682..9bfb3623f 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java @@ -5,13 +5,9 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; -import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; -import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; -import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +16,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -35,6 +32,7 @@ @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 내 피드 조회 api 통합 테스트") +@Transactional class FeedShowMineApiTest { @Autowired @@ -46,31 +44,12 @@ class FeedShowMineApiTest { @Autowired private FeedJpaRepository feedJpaRepository; - @Autowired - private FollowingJpaRepository followingJpaRepository; - @Autowired private BookJpaRepository bookJpaRepository; - @Autowired - private SavedFeedJpaRepository savedFeedJpaRepository; - - @Autowired - private PostLikeJpaRepository postLikeJpaRepository; - @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - postLikeJpaRepository.deleteAllInBatch(); - savedFeedJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("내 피드 조회를 요청할 경우, [feedId, 작성일, 책정보, ,,] 의 피드 정보를 최신순으로 정렬해서 반환한다.") void feed_show_mine_test() throws Exception { diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java index 450798708..782eafc71 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java @@ -8,13 +8,10 @@ import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.feed.domain.value.Tag; -import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; -import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +19,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -35,6 +33,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 단일 피드 조회 api 통합 테스트") class FeedShowSingleApiTest { @@ -42,23 +41,10 @@ class FeedShowSingleApiTest { private MockMvc mockMvc; @Autowired private UserJpaRepository userJpaRepository; @Autowired private FeedJpaRepository feedJpaRepository; - @Autowired private FollowingJpaRepository followingJpaRepository; @Autowired private BookJpaRepository bookJpaRepository; @Autowired private SavedFeedJpaRepository savedFeedJpaRepository; - @Autowired private PostLikeJpaRepository postLikeJpaRepository; - private List tags; - @AfterEach - void tearDown() { - postLikeJpaRepository.deleteAllInBatch(); - savedFeedJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("단일 피드 조회를 요청할 경우, [피드 정보, 피드 작성자 정보, 피드와 연관된 태그들] 등의 정보를 반환한다.") void feed_show_single_test() throws Exception { diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java index 6701bfb82..726aa5c50 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java @@ -11,9 +11,7 @@ import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; -import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +20,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -36,6 +35,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 특정 유저 피드 조회 api 통합 테스트") class FeedShowSpecificUserApiTest { @@ -48,9 +48,6 @@ class FeedShowSpecificUserApiTest { @Autowired private FeedJpaRepository feedJpaRepository; - @Autowired - private FollowingJpaRepository followingJpaRepository; - @Autowired private BookJpaRepository bookJpaRepository; @@ -63,15 +60,6 @@ class FeedShowSpecificUserApiTest { @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - postLikeJpaRepository.deleteAllInBatch(); - savedFeedJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } @Test @DisplayName("특정 유저의 피드 조회를 요청할 경우, [feedId, 작성일, 책정보, ,,] 의 피드 정보를 최신순으로 정렬해서 반환한다.") diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowUserInfoApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowUserInfoApiTest.java index 13e017cf0..46999902d 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowUserInfoApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowUserInfoApiTest.java @@ -1,5 +1,6 @@ package konkuk.thip.feed.adapter.in.web; +import jakarta.persistence.EntityManager; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; @@ -9,7 +10,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,6 +18,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -31,6 +32,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 피드 화면에서의 유저 정보 조회 api 통합 테스트") class FeedShowUserInfoApiTest { @@ -40,14 +42,7 @@ class FeedShowUserInfoApiTest { @Autowired private FollowingJpaRepository followingJpaRepository; @Autowired private BookJpaRepository bookJpaRepository; @Autowired private JdbcTemplate jdbcTemplate; - - @AfterEach - void tearDown() { - feedJpaRepository.deleteAllInBatch(); - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } + @Autowired private EntityManager em; @Test @DisplayName("내 피드에서의 유저 정보를 조회할 경우, 내 개인 정보, 나의 팔로워 정보, 내가 작성한 모든 피드 개수 를 반환한다.") @@ -74,6 +69,9 @@ void feed_show_mine_info_test() throws Exception { "UPDATE users SET follower_count = ? WHERE user_id = ?", 2, me.getUserId()); // me 의 followerCount 값을 2로 update + em.flush(); + em.clear(); + // 피드 생성 및 생성일 직접 설정 BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); // 공통 Book feedJpaRepository.save(TestEntityFactory.createFeed(me, book, true)); // 공개글 @@ -144,6 +142,9 @@ void feed_show_mine_info_follower_many_test() throws Exception { "UPDATE users SET follower_count = ? WHERE user_id = ?", 7, me.getUserId()); // me 의 followerCount 값을 7로 update + em.flush(); + em.clear(); + //when //then mockMvc.perform(get("/feeds/mine/info") .requestAttr("userId", me.getUserId())) @@ -191,6 +192,9 @@ void feed_show_user_info_test() throws Exception { feedJpaRepository.save(TestEntityFactory.createFeed(anotherUser, book, false)); // 비공개글 feedJpaRepository.save(TestEntityFactory.createFeed(anotherUser, book, false)); // 비공개글 + em.flush(); + em.clear(); + //when //then mockMvc.perform(get("/feeds/users/{userId}/info", anotherUser.getUserId()) .requestAttr("userId", me.getUserId())) @@ -284,6 +288,9 @@ void feed_show_user_info_follower_many_test() throws Exception { "UPDATE users SET follower_count = ? WHERE user_id = ?", 7, anotherUser.getUserId()); // anotherUser 의 followerCount 값을 7로 update + em.flush(); + em.clear(); + //when //then mockMvc.perform(get("/feeds/users/{userId}/info", anotherUser.getUserId()) .requestAttr("userId", me.getUserId())) diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java index 97816072e..3fd5aa97e 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java @@ -5,7 +5,6 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; -import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; @@ -52,7 +51,6 @@ class FeedUpdateControllerTest { @BeforeEach void setUp() { - Alias alias = TestEntityFactory.createLiteratureAlias(); UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias)); BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java index 4c4e1b3fb..e489e42a4 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java @@ -13,7 +13,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +21,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -37,6 +37,7 @@ ) @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 피드 전체 조회(내가 팔로잉한 유저 우선순 조회) api 통합 테스트") class FollowingPriorityFeedShowAllApiTest { @@ -64,16 +65,6 @@ class FollowingPriorityFeedShowAllApiTest { @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - postLikeJpaRepository.deleteAllInBatch(); - savedFeedJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("피드 조회를 요청할 경우, [feedId, 작성자 닉네임, ,,] 의 피드 정보를 최신순으로 정렬해서 반환한다.") void feed_show_all_test() throws Exception { diff --git a/src/test/java/konkuk/thip/feed/domain/FeedTest.java b/src/test/java/konkuk/thip/feed/domain/FeedTest.java index 746e5be96..e4aa33d34 100644 --- a/src/test/java/konkuk/thip/feed/domain/FeedTest.java +++ b/src/test/java/konkuk/thip/feed/domain/FeedTest.java @@ -63,30 +63,6 @@ private Feed createPrivateFeed() { .build(); } -// @Test -// @DisplayName("validateTags: 태그가 5개 초과 시 InvalidStateException이 발생한다.") -// void validateTags_exceedsMax_throws() { -// List tags = List.of("a", "b", "c", "d", "e", "f"); -// -// InvalidStateException ex = assertThrows(InvalidStateException.class, -// () -> Feed.validateTags(tags)); -// -// assertEquals(INVALID_FEED_COMMAND, ex.getErrorCode()); -// assertTrue(ex.getCause().getMessage().contains("최대 5개")); -// } -// -// @Test -// @DisplayName("validateTags: 중복 태그 있을 경우 InvalidStateException이 발생한다.") -// void validateTags_withDuplicates_throws() { -// List tags = List.of("a", "b", "a"); -// -// InvalidStateException ex = assertThrows(InvalidStateException.class, -// () -> Feed.validateTags(tags)); -// -// assertEquals(INVALID_FEED_COMMAND, ex.getErrorCode()); -// assertTrue(ex.getCause().getMessage().contains("중복")); -// } - @Test @DisplayName("validateCreateComment: 공개 피드면 누구나 댓글을 작성 할 수 있다") void validateCreateComment_publicFeed_passes() { diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java index 829039b1b..78306cd96 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java @@ -40,6 +40,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 방 생성 api 통합 테스트") class RoomCreateApiTest { @@ -50,14 +51,6 @@ class RoomCreateApiTest { @Autowired private RoomJpaRepository roomJpaRepository; @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - private void saveUserAndLiteratureCategory() { Alias alias = TestEntityFactory.createLiteratureAlias(); @@ -68,8 +61,6 @@ private void saveUserAndLiteratureCategory() { .role(UserRole.USER) .alias(alias) .build()); - - Category category = TestEntityFactory.createLiteratureCategory(); } private void saveBookWithPageCount() { diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java index 3ac9526f8..acce4237f 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java @@ -14,7 +14,6 @@ import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.hamcrest.Matchers; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -24,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -36,6 +36,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 모임 홈 참여중인 내 모임방 조회 api 통합 테스트") class RoomGetHomeJoinedRoomsApiTest { @@ -79,14 +80,6 @@ void setUp() { roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room2,user1, RoomParticipantRole.HOST,60.0)); } - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } - private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, LocalDate endDate, int recruitCount) { BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() .title(bookTitle) @@ -205,8 +198,8 @@ void getHomeJoinedRooms_sortByStartDateWhenUserPercentageEquals() throws Excepti } @Test - @DisplayName("사용자가 참여중인 방 목록 중 모집중(시작 전)인 방은 참여중 목록에 포함되지 않는다.") - void getHomeJoinedRooms_excludeRecruitingRooms() throws Exception { + @DisplayName("사용자가 참여중인 방 목록 중 모집중(시작 전)인 방도 참여중 목록에 포함된다.") + void getHomeJoinedRooms_includeRecruitingRooms() throws Exception { // given Alias alias = TestEntityFactory.createLiteratureAlias(); @@ -231,11 +224,13 @@ void getHomeJoinedRooms_excludeRecruitingRooms() throws Exception { // then result.andExpect(status().isOk()) - .andExpect(jsonPath("$.data.roomList", hasSize(1))) + .andExpect(jsonPath("$.data.roomList", hasSize(2))) .andExpect(jsonPath("$.data.nextCursor").value(Matchers.nullValue())) .andExpect(jsonPath("$.data.isLast", is(true))) .andExpect(jsonPath("$.data.roomList[0].roomId", is(activeRoom.getRoomId().intValue()))) - .andExpect(jsonPath("$.data.roomList[0].userPercentage", is(50))); + .andExpect(jsonPath("$.data.roomList[0].userPercentage", is(50))) + .andExpect(jsonPath("$.data.roomList[1].roomId", is(recruitRoom.getRoomId().intValue()))) + .andExpect(jsonPath("$.data.roomList[1].userPercentage", is(-1))); } diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java index 480bba84d..5132ad08d 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java @@ -23,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -34,6 +35,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 독서 메이트(방 멤버) 조회 api 통합 테스트") class RoomGetMemberListApiTest { @@ -103,15 +105,6 @@ void setUp() { followingJpaRepository.save(TestEntityFactory.createFollowing(user3, user1)); } - @AfterEach - void tearDown() { - followingJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } - @Test @DisplayName("방 멤버 리스트(독서메이트)가 userId, nickname, imageUrl, alias, subscriberCount로 조회된다.") void getRoomMemberList_success() throws Exception { 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 ce1c0ff9d..cf6913a3c 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 @@ -1,10 +1,10 @@ package konkuk.thip.room.adapter.in.web; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; 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; @@ -15,7 +15,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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +24,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.HashMap; @@ -39,6 +39,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 방 참여/취소 API 통합 테스트") class RoomJoinApiTest { @@ -49,7 +50,7 @@ class RoomJoinApiTest { @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; @Autowired private BookJpaRepository bookJpaRepository; @Autowired private UserJpaRepository userJpaRepository; - @Autowired private NotificationJpaRepository notificationJpaRepository; + @Autowired private EntityManager em; private RoomJpaEntity room; private UserJpaEntity host; @@ -109,15 +110,6 @@ private void createUsers(Alias alias) { .build()); } - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - notificationJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("방 참여 성공 - 참여자 저장 및 인원수 증가 확인") void joinRoom_success() throws Exception { @@ -175,6 +167,9 @@ void cancelJoin_success() throws Exception { .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()); + em.flush(); + em.clear(); + // 참여자 삭제 확인 RoomParticipantJpaEntity member = roomParticipantJpaRepository.findById(memberParticipation.getRoomParticipantId()).orElse(null); assertThat(member.getStatus()).isEqualTo(INACTIVE); diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java index ee37cbf18..f2959bfe7 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java @@ -15,7 +15,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import konkuk.thip.user.domain.value.UserRole; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.List; @@ -36,6 +36,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 모집 중인 방 상세조회 api 통합 테스트") class RoomRecruitingDetailViewApiTest { @@ -46,17 +47,7 @@ class RoomRecruitingDetailViewApiTest { @Autowired private RoomJpaRepository roomJpaRepository; @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } - private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount, RoomStatus roomStatus) { - Alias alias = TestEntityFactory.createScienceAlias(); - BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() .title(bookTitle) .isbn(isbn) @@ -85,8 +76,6 @@ private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String room } private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount, RoomStatus roomStatus) { - Alias alias = TestEntityFactory.createLiteratureAlias(); - BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() .title(bookTitle) .isbn(isbn) diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java index c75278393..3bf245393 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java @@ -24,6 +24,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -35,6 +36,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 내 방 목록 조회 api 통합 테스트") class RoomShowMineApiTest { @@ -46,17 +48,7 @@ class RoomShowMineApiTest { @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, LocalDate endDate, int recruitCount, RoomStatus roomStatus) { - Alias alias = TestEntityFactory.createScienceAlias(); - BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() .title(bookTitle) .isbn(isbn) diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java index dde07a0b3..2c62ebd2e 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java @@ -24,6 +24,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -34,6 +35,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 비공개 방 비밀번호 입력 검증 api 통합 테스트") class RoomVerifyPasswordApiTest { @@ -100,13 +102,6 @@ void setUp() { publicRoomId = publicRoom.getRoomId(); } - @AfterEach - void tearDown() { - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } - @Test @DisplayName("모집기간이 만료되지 않은 비공개 방의 비밀번호 입력 검증에 [성공]한다") void verifyRoomPassword_success() throws Exception { diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java index a3117747d..8cd93fb60 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java @@ -14,7 +14,6 @@ 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.util.Map; @@ -37,6 +37,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 오늘의 한마디 생성 api 통합 테스트") class AttendanceCheckCreateApiTest { @@ -51,15 +52,6 @@ class AttendanceCheckCreateApiTest { @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; @Autowired private AttendanceCheckJpaRepository attendanceCheckJpaRepository; - @AfterEach - void tearDown() { - attendanceCheckJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("방의 참석자는 출석체크(= 오늘의 한마디) 를 작성할 수 있다.") void attendance_check_create_test() throws Exception { diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java index d9274100a..9777942a5 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java @@ -1,6 +1,6 @@ package konkuk.thip.roompost.adapter.in.web; -import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; @@ -14,7 +14,6 @@ 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.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,6 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import static konkuk.thip.common.entity.StatusType.INACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.ATTENDANCE_CHECK_CAN_NOT_DELETE; @@ -35,6 +35,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 오늘의 한마디 삭제 api 통합 테스트") class AttendanceCheckDeleteApiTest { @@ -42,21 +43,12 @@ class AttendanceCheckDeleteApiTest { @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; @Autowired private UserJpaRepository userJpaRepository; @Autowired private BookJpaRepository bookJpaRepository; @Autowired private RoomJpaRepository roomJpaRepository; @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; @Autowired private AttendanceCheckJpaRepository attendanceCheckJpaRepository; - - @AfterEach - void tearDown() { - attendanceCheckJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } + @Autowired private EntityManager em; @Test @DisplayName("오늘의 한마디 작성자는 본인이 작성한 오늘의 한마디를 삭제(= soft delete) 할 수 있다.") @@ -81,6 +73,9 @@ void attendance_check_delete_test() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.data.roomId", is(room.getRoomId().intValue()))); + em.flush(); + em.clear(); + AttendanceCheckJpaEntity deleted = attendanceCheckJpaRepository.findById(ac1.getAttendanceCheckId()).orElse(null); Assertions.assertNotNull(deleted); assertThat(deleted.getStatus()).isEqualTo(INACTIVE); diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java index 949369b07..a7e57badd 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java @@ -14,7 +14,6 @@ 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -36,6 +36,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 오늘의 한마디 조회 api 통합 테스트") class AttendanceCheckShowApiTest { @@ -50,15 +51,6 @@ class AttendanceCheckShowApiTest { @Autowired private AttendanceCheckJpaRepository attendanceCheckJpaRepository; @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - attendanceCheckJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("방의 출석체크(= 오늘의 한마디) 조회 요청하면, [오늘의 한마디 작성자 정보, 오늘의 한마디 정보] 등을 최신순으로 반환한다.") void attendance_check_show_test() throws Exception { 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 3275f9b77..9c1a2054b 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 @@ -19,17 +19,16 @@ import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteItemJpaRepository; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -41,6 +40,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@Transactional @DisplayName("[통합] 투표 생성 api 통합 테스트") class VoteCreateApiTest { @Autowired @@ -70,20 +70,6 @@ class VoteCreateApiTest { @Autowired private NotificationJpaRepository notificationJpaRepository; - @Autowired - private JdbcTemplate jdbcTemplate; - - @AfterEach - void tearDown() { - voteItemJpaRepository.deleteAllInBatch(); - voteJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - notificationJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - private void saveUserAndRoom() { Alias alias = TestEntityFactory.createScienceAlias(); UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias, "user")); 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 2491a9723..debdbc06e 100644 --- a/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java +++ b/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java @@ -3,26 +3,23 @@ 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; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; -import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteItemJpaRepository; -import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import konkuk.thip.roompost.application.port.in.dto.vote.VoteCreateCommand; 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -31,6 +28,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 투표 생성 service 통합 테스트") class VoteCreateServiceTest { @@ -47,29 +45,9 @@ class VoteCreateServiceTest { @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; - @Autowired - private VoteJpaRepository voteJpaRepository; - - @Autowired - private VoteItemJpaRepository voteItemJpaRepository; - @Autowired private VoteCreateService voteCreateService; - @Autowired - private NotificationJpaRepository notificationJpaRepository; - - @AfterEach - void tearDown() { - voteItemJpaRepository.deleteAllInBatch(); - voteJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - notificationJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("유저가 투표를 생성하면, 해당 유저의 [RoomParticipant의 currentPage, userPercentage]와 해당 방의 [Room의 roomPercentage] 값이 변경된다.") void vote_create_room_participant_and_room_percentage_update() throws Exception { diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java index 0a513adcd..016874078 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java @@ -34,7 +34,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -43,6 +42,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -61,6 +61,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc @DisplayName("[통합] 회원탈퇴 api 테스트") public class UserDeleteApiTest { @@ -86,28 +87,7 @@ public class UserDeleteApiTest { @Autowired private UserTokenBlacklistQueryPort userTokenBlacklistQueryPort; @Autowired private JwtUtil jwtUtil; - @Autowired private EntityManager entityManager; - - @AfterEach - void tearDown() { - followingJpaRepository.deleteAllInBatch(); - recentSearchJpaRepository.deleteAllInBatch(); - savedFeedJpaRepository.deleteAllInBatch(); - savedBookJpaRepository.deleteAllInBatch(); - attendanceCheckJpaRepository.deleteAllInBatch(); - voteParticipantJpaRepository.deleteAllInBatch(); - commentLikeJpaRepository.deleteAllInBatch(); - commentJpaRepository.deleteAllInBatch(); - postLikeJpaRepository.deleteAllInBatch(); - feedJpaRepository.deleteAllInBatch(); - recordJpaRepository.deleteAllInBatch(); - voteItemJpaRepository.deleteAllInBatch(); - voteJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } + @Autowired private EntityManager em; @Test @DisplayName("회원탈퇴 성공시 모든 연관 엔티티가 각 엔티티 삭제 전략에 맞게 삭제되고 탈퇴한 회원의 토큰이 블랙리스트에 등록된다.") @@ -260,6 +240,9 @@ void deleteUser_success() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); + em.flush(); + em.clear(); + // then: 1) 유저 팔로잉/팔로워 관계 삭제 // 탈퇴한 유저1의 팔로잉/팔로워 관계는 모두 삭제되어야하고, 관련 없는 유저3->유저2 팔로우관계만 남아있어야함 // 유저2의 팔로워 수가 1이어야함 @@ -390,7 +373,6 @@ void deleteUser_success() throws Exception { // 12) 유저 soft delete (status=INACTIVE) // 탈퇴한 유저의 oauth2Id는 deleted:로 시작해야함 - entityManager.clear(); UserJpaEntity deletedUser = userJpaRepository.findById(testUser1.getUserId()).orElse(null); assertThat(deletedUser.getStatus()).isEqualTo(INACTIVE); assertThat(deletedUser.getOauth2Id()).startsWith("deleted:"); 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 0344345fa..1f68b4c99 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,14 +1,12 @@ 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; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -17,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.Optional; @@ -27,6 +26,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest +@Transactional @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 팔로잉 상태 변경 API 통합 테스트") @@ -37,14 +37,6 @@ class UserFollowApiTest { @Autowired private UserJpaRepository userJpaRepository; @Autowired private FollowingJpaRepository followingJpaRepository; - @Autowired private NotificationJpaRepository notificationJpaRepository; - - @AfterEach - void tearDown() { - followingJpaRepository.deleteAllInBatch(); - notificationJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } @Test @DisplayName("팔로우 요청 후 언팔로우 요청 시 엔티티가 삭제되었는지 확인한다.") diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java index 118d5d2b0..40eb8722c 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java @@ -6,7 +6,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,6 +14,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -22,6 +22,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 팔로잉 여부 조회 API 통합 테스트") class UserIsFollowingApiTest { @@ -35,12 +36,6 @@ class UserIsFollowingApiTest { @Autowired private FollowingJpaRepository followingJpaRepository; - @AfterEach - void tearDown() { - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAll(); - } - @Test @DisplayName("팔로우 관계가 존재하면 true를 반환한다.") void isFollowing_true() throws Exception { diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java index 95531b453..13ebd6e35 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java @@ -10,7 +10,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +18,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -32,6 +32,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 피드 조회 화면에서, 내 띱 목록 조회 api 통합 테스트") class UserShowFollowingsInFeedViewApiTest { @@ -43,14 +44,6 @@ class UserShowFollowingsInFeedViewApiTest { @Autowired private BookJpaRepository bookJpaRepository; @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - feedJpaRepository.deleteAllInBatch(); - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("전체 피드 조회 화면에서, 내가 팔로잉 하는 사람들의 [userId, 닉네임, 프로필 이미지] 정보를 1.최근 공개 피드를 작성한 사람 -> 2.최근 팔로잉 맺은 사람 순으로 반환합니다.") void show_my_following_recent_writers_test() throws Exception { diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java index 77b38e3be..b2235efb4 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java @@ -8,7 +8,6 @@ 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,6 +17,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; import static konkuk.thip.common.exception.code.ErrorCode.AUTH_TOKEN_NOT_FOUND; @@ -30,6 +30,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc +@Transactional @DisplayName("[통합] 회원가입 api 테스트") class UserSignupControllerTest { @@ -45,11 +46,6 @@ class UserSignupControllerTest { @Autowired private JwtUtil jwtUtil; - @AfterEach - void tearDown() { - userJpaRepository.deleteAll(); - } - @Test @DisplayName("[칭호id, 닉네임] 정보를 바탕으로 회원가입을 진행한다.") void signup_success() throws Exception { diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java index 9a929dc52..4386ab7ee 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java @@ -7,7 +7,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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -26,6 +26,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 사용자 정보 수정 API 통합 테스트") class UserUpdateApiTest { @@ -39,11 +40,6 @@ class UserUpdateApiTest { @Autowired private UserJpaRepository userJpaRepository; - @AfterEach - void tearDown() { - userJpaRepository.deleteAll(); - } - @Test @DisplayName("사용자 닉네임과 별칭이 정상적으로 업데이트된다.") void updateUser_success() throws Exception { diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java index 99901b3d6..a6042d8f6 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java @@ -8,7 +8,6 @@ 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +18,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -32,6 +32,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 닉네임 중복 검증 api 테스트") class UserVerifyNicknameControllerTest { @@ -45,11 +46,6 @@ class UserVerifyNicknameControllerTest { @Autowired private UserJpaRepository userJpaRepository; @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - userJpaRepository.deleteAllInBatch(); - } - @Test @DisplayName("[닉네임]값이 unique 할 경우, true를 반환한다.") void verify_nickname_true() throws Exception { From 58d9d0fa9197c975320b7341c743dfec4286afc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:07:48 +0900 Subject: [PATCH 50/89] =?UTF-8?q?[refactor]=203.jwt=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EA=B8=B0=EA=B0=84=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=B6=94=EC=B6=9C=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/security/util/JwtUtil.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/util/JwtUtil.java b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java index df2073145..0aea6d718 100644 --- a/src/main/java/konkuk/thip/common/security/util/JwtUtil.java +++ b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java @@ -25,9 +25,10 @@ public class JwtUtil { private final SecretKey secretKey; - //todo 확정 후 환경변수로 변경 - private final long tokenExpiredMs = 2592000000L; // 30일 - private final long signupTokenExpiredMs = 2592000000L; // 30일 + @Value("${jwt.access-token-expiration}") + private long tokenExpiredMs; + @Value("${jwt.signup-token-expiration}") + private long signupTokenExpiredMs; public JwtUtil(@Value("${jwt.secret}") String secret) { secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); From 604523b6a2072b73614eea7652afb3487a8d35bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:11:23 +0900 Subject: [PATCH 51/89] =?UTF-8?q?[refactor]=201.=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=EB=90=9C=20=EB=B0=A9=20=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=EC=B6=94=EA=B0=80=20=20=EC=A7=84=ED=96=89=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=20=EB=B0=A9=20=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20=EC=99=80?= =?UTF-8?q?=20response=EA=B0=80=20=EA=B0=99=EA=B8=B0=EB=95=8C=EB=AC=B8?= =?UTF-8?q?=EC=97=90=20=EA=B8=B0=EC=A1=B4=EC=9D=98=20=EC=A7=84=ED=96=89?= =?UTF-8?q?=EC=A4=91=EC=9D=B8=20=EB=B0=A9=20=EC=83=81=EC=84=B8=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A5=BC=20=EB=A7=8C=EB=A3=8C=EB=90=9C=20=EB=B0=A9=20?= =?UTF-8?q?+=20=EC=A7=84=ED=96=89=EC=A4=91=EC=9D=B8=20=EB=B0=A9=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20=ED=95=98=EB=8A=94=20api?= =?UTF-8?q?=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=95=A9=EC=B9=A8=20=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/common/util/DateUtil.java | 23 +++++ .../RoomGetHomeJoinedListResponse.java | 7 +- ...omPlayingOrExpiredDetailViewResponse.java} | 2 +- .../mapper/RoomParticipantQueryMapper.java | 26 ++++- .../in/RoomShowPlayingDetailViewUseCase.java | 8 -- ...ShowPlayingOrExpiredDetailViewUseCase.java | 8 ++ .../port/out/dto/RoomParticipantQueryDto.java | 7 +- ...howPlayingOrExpiredDetailViewService.java} | 31 +++--- .../VoteQueryPersistenceAdapter.java | 4 +- .../repository/vote/VoteQueryRepository.java | 4 +- .../vote/VoteQueryRepositoryImpl.java | 10 +- .../application/port/out/VoteQueryPort.java | 4 +- ...oomPlayingOrExpiredDetailViewApiTest.java} | 95 ++++++++++++++----- 13 files changed, 160 insertions(+), 69 deletions(-) rename src/main/java/konkuk/thip/room/adapter/in/web/response/{RoomPlayingDetailViewResponse.java => RoomPlayingOrExpiredDetailViewResponse.java} (94%) delete mode 100644 src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java create mode 100644 src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingOrExpiredDetailViewUseCase.java rename src/main/java/konkuk/thip/room/application/service/{RoomShowPlayingDetailViewService.java => RoomShowPlayingOrExpiredDetailViewService.java} (62%) rename src/test/java/konkuk/thip/room/adapter/in/web/{RoomPlayingDetailViewApiTest.java => RoomPlayingOrExpiredDetailViewApiTest.java} (82%) diff --git a/src/main/java/konkuk/thip/common/util/DateUtil.java b/src/main/java/konkuk/thip/common/util/DateUtil.java index 7e4f6f373..414b0a05b 100644 --- a/src/main/java/konkuk/thip/common/util/DateUtil.java +++ b/src/main/java/konkuk/thip/common/util/DateUtil.java @@ -68,6 +68,29 @@ public static String RecruitingRoomFormatAfterTime(LocalDate date) { return "마감 임박"; } + public static String RecruitingRoomFormatAfterTimeSimple(LocalDate date) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime dateTime = date.atStartOfDay(); + Duration d = Duration.between(now, dateTime); + + if (d.isNegative() || d.isZero()) { + return "??"; + } + + long days = d.toDays(); + if (days > 0) { + return days + "일"; + } + + long hours = d.toHours(); + if (hours >= 1) { + return hours + "시간"; + } + + long minutes = d.toMinutes(); + return minutes + "분"; + } + public static String formatDate(LocalDate date) { return date.format(DATE_FORMATTER); diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java index c67c97888..516ac114e 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java @@ -1,5 +1,7 @@ package konkuk.thip.room.adapter.in.web.response; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; public record RoomGetHomeJoinedListResponse( @@ -13,7 +15,10 @@ public record JoinedRoomInfo( String bookImageUrl, String roomTitle, int memberCount, - int userPercentage + @Schema(description = "진행중인 방에서 유저의 방 진행도(ex. \"35\"), 모집중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.") + int userPercentage, + @Schema(description = "모집중인 방에서 방 모집 마감일까지 남은 시간 (ex. \"3일\"), 진행중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.") + String deadlineDate // 방 모집 마감일 (~일/시 형식) ) {} public static RoomGetHomeJoinedListResponse of(List roomList, String nickname, String nextCursor, boolean isLast){ diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingDetailViewResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingOrExpiredDetailViewResponse.java similarity index 94% rename from src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingDetailViewResponse.java rename to src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingOrExpiredDetailViewResponse.java index e68bc4010..aeff43eae 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingDetailViewResponse.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomPlayingOrExpiredDetailViewResponse.java @@ -5,7 +5,7 @@ import java.util.List; @Builder -public record RoomPlayingDetailViewResponse( +public record RoomPlayingOrExpiredDetailViewResponse( boolean isHost, Long roomId, String roomName, diff --git a/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java b/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java index 08fb5c95b..035059f37 100644 --- a/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java +++ b/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java @@ -4,11 +4,13 @@ import konkuk.thip.room.adapter.in.web.response.RoomGetHomeJoinedListResponse; import konkuk.thip.room.application.port.out.dto.RoomParticipantQueryDto; import org.mapstruct.Mapper; -import org.mapstruct.Mapping; import org.mapstruct.ReportingPolicy; import java.util.List; +import static konkuk.thip.room.domain.value.RoomStatus.IN_PROGRESS; +import static konkuk.thip.room.domain.value.RoomStatus.RECRUITING; + @Mapper( componentModel = "spring", imports = DateUtil.class, @@ -18,6 +20,24 @@ public interface RoomParticipantQueryMapper { List toHomeJoinedRoomResponse(List dtos); - @Mapping(target = "userPercentage", expression = "java(dto.userPercentage().intValue())") - RoomGetHomeJoinedListResponse.JoinedRoomInfo toJoinedRoomInfo(RoomParticipantQueryDto dto); + default RoomGetHomeJoinedListResponse.JoinedRoomInfo toJoinedRoomInfo(RoomParticipantQueryDto dto) { + int userPercentage = -1; + String deadlineDate = null; + + if (IN_PROGRESS.equals(dto.roomStatus())) { + userPercentage = dto.userPercentage().intValue(); + } else if (RECRUITING.equals(dto.roomStatus())) { + deadlineDate = DateUtil.RecruitingRoomFormatAfterTimeSimple(dto.startDate()); + } + + return new RoomGetHomeJoinedListResponse.JoinedRoomInfo( + dto.roomId(), + dto.bookImageUrl(), + dto.roomTitle(), + dto.memberCount(), + userPercentage, + deadlineDate + ); + } + } diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java deleted file mode 100644 index abc8f2624..000000000 --- a/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java +++ /dev/null @@ -1,8 +0,0 @@ -package konkuk.thip.room.application.port.in; - -import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; - -public interface RoomShowPlayingDetailViewUseCase { - - RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long roomId); -} diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingOrExpiredDetailViewUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingOrExpiredDetailViewUseCase.java new file mode 100644 index 000000000..bea7cd0b6 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingOrExpiredDetailViewUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.room.application.port.in; + +import konkuk.thip.room.adapter.in.web.response.RoomPlayingOrExpiredDetailViewResponse; + +public interface RoomShowPlayingOrExpiredDetailViewUseCase { + + RoomPlayingOrExpiredDetailViewResponse getPlayingOrExpiredRoomDetailView(Long userId, Long roomId); +} diff --git a/src/main/java/konkuk/thip/room/application/port/out/dto/RoomParticipantQueryDto.java b/src/main/java/konkuk/thip/room/application/port/out/dto/RoomParticipantQueryDto.java index 052fdea03..51a8048eb 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/dto/RoomParticipantQueryDto.java +++ b/src/main/java/konkuk/thip/room/application/port/out/dto/RoomParticipantQueryDto.java @@ -1,6 +1,7 @@ package konkuk.thip.room.application.port.out.dto; import com.querydsl.core.annotations.QueryProjection; +import konkuk.thip.room.domain.value.RoomStatus; import lombok.Builder; import org.springframework.util.Assert; @@ -13,8 +14,9 @@ public record RoomParticipantQueryDto( String roomTitle, Integer memberCount, Double userPercentage, - LocalDate startDate // 방 진행 시작일 -) { + LocalDate startDate, // 방 진행 시작일 + RoomStatus roomStatus + ) { @QueryProjection public RoomParticipantQueryDto { Assert.notNull(roomId, "roomId must not be null"); @@ -23,5 +25,6 @@ public record RoomParticipantQueryDto( Assert.notNull(memberCount, "memberCount must not be null"); Assert.notNull(userPercentage, "userPercentage must not be null"); Assert.notNull(startDate, "startDate must not be null"); + Assert.notNull(roomStatus, "roomStatus must not be null"); } } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java b/src/main/java/konkuk/thip/room/application/service/RoomShowPlayingOrExpiredDetailViewService.java similarity index 62% rename from src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java rename to src/main/java/konkuk/thip/room/application/service/RoomShowPlayingOrExpiredDetailViewService.java index fc3ba2f07..c0351ef55 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomShowPlayingOrExpiredDetailViewService.java @@ -3,14 +3,13 @@ import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.util.DateUtil; -import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; -import konkuk.thip.room.application.port.in.RoomShowPlayingDetailViewUseCase; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingOrExpiredDetailViewResponse; +import konkuk.thip.room.application.port.in.RoomShowPlayingOrExpiredDetailViewUseCase; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomQueryPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; import konkuk.thip.room.domain.Room; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; -import konkuk.thip.room.domain.RoomParticipants; import konkuk.thip.room.domain.RoomParticipant; import konkuk.thip.roompost.application.port.out.VoteQueryPort; import lombok.RequiredArgsConstructor; @@ -21,7 +20,7 @@ @Service @RequiredArgsConstructor -public class RoomShowPlayingDetailViewService implements RoomShowPlayingDetailViewUseCase { +public class RoomShowPlayingOrExpiredDetailViewService implements RoomShowPlayingOrExpiredDetailViewUseCase { private static final int TOP_PARTICIPATION_VOTES_COUNT = 3; @@ -35,7 +34,7 @@ public class RoomShowPlayingDetailViewService implements RoomShowPlayingDetailVi @Override @Transactional(readOnly = true) - public RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long roomId) { + public RoomPlayingOrExpiredDetailViewResponse getPlayingOrExpiredRoomDetailView(Long userId, Long roomId) { // 1. 해당 방의 참여자인지 조회 roomParticipantValidator.validateUserIsRoomMember(roomId, userId); @@ -44,21 +43,19 @@ public RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long Room room = roomCommandPort.getByIdOrThrow(roomId); Book book = bookCommandPort.findById(room.getBookId()); - // 2. Room과 연관된 UserRoom 조회, RoomParticipants 일급 컬렉션 생성 - // TODO. Room 도메인에 memberCount 값 추가된 후 리펙토링 - List findByRoomId = roomParticipantCommandPort.findAllByRoomId(roomId); - RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId); + // 2. Room과 연관된 RoomParticipant 조회 + RoomParticipant roomParticipant = roomParticipantCommandPort.getByUserIdAndRoomIdOrThrow(userId, roomId); // 3. 투표 참여율이 가장 높은 투표 조회 - List topParticipationVotes = voteQueryPort.findTopParticipationVotesByRoom(room, TOP_PARTICIPATION_VOTES_COUNT); + List topParticipationVotes = voteQueryPort.findTopParticipationVotesByRoom(room, TOP_PARTICIPATION_VOTES_COUNT); // 4. response 구성 - return buildResponse(userId, room, book, roomParticipants, topParticipationVotes); + return buildResponse(room, book, roomParticipant, topParticipationVotes); } - private RoomPlayingDetailViewResponse buildResponse(Long userId, Room room, Book book, RoomParticipants roomParticipants, List topParticipationVotes) { - return RoomPlayingDetailViewResponse.builder() - .isHost(roomParticipants.isHostOfRoom(userId)) + private RoomPlayingOrExpiredDetailViewResponse buildResponse(Room room, Book book, RoomParticipant roomParticipant, List topParticipationVotes) { + return RoomPlayingOrExpiredDetailViewResponse.builder() + .isHost(roomParticipant.isHost()) .roomId(room.getId()) .roomName(room.getTitle()) .roomImageUrl(room.getCategory().getImageUrl()) @@ -67,13 +64,13 @@ private RoomPlayingDetailViewResponse buildResponse(Long userId, Room room, Book .progressEndDate(DateUtil.formatDate(room.getEndDate())) .category(room.getCategory().getValue()) .roomDescription(room.getDescription()) - .memberCount(roomParticipants.calculateMemberCount()) + .memberCount(room.getMemberCount()) .recruitCount(room.getRecruitCount()) .isbn(book.getIsbn()) .bookTitle(book.getTitle()) .authorName(book.getAuthorName()) - .currentPage(roomParticipants.getCurrentPageOfUser(userId)) - .userPercentage((int) roomParticipants.getUserPercentageOfUser(userId)) + .currentPage(roomParticipant.getCurrentPage()) + .userPercentage((int) roomParticipant.getUserPercentage()) .currentVotes(topParticipationVotes) .categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory())) // TODO : 리펙토링 대상 .build(); diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java index ac86e9552..97ab89043 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java @@ -1,6 +1,6 @@ package konkuk.thip.roompost.adapter.out.persistence; -import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingOrExpiredDetailViewResponse; import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import konkuk.thip.roompost.application.port.out.VoteQueryPort; @@ -23,7 +23,7 @@ public class VoteQueryPersistenceAdapter implements VoteQueryPort { private final VoteJpaRepository voteJpaRepository; @Override - public List findTopParticipationVotesByRoom(Room room, int count) { + public List findTopParticipationVotesByRoom(Room room, int count) { return voteJpaRepository.findTopParticipationVotesByRoom(room.getId(), count); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepository.java index b659d7029..d7033890b 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepository.java @@ -1,6 +1,6 @@ package konkuk.thip.roompost.adapter.out.persistence.repository.vote; -import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingOrExpiredDetailViewResponse; import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import konkuk.thip.roompost.application.port.out.dto.VoteItemQueryDto; @@ -11,7 +11,7 @@ public interface VoteQueryRepository { List findVotesByRoom(Long roomId, String type, Integer pageStart, Integer pageEnd, Long userId); - List findTopParticipationVotesByRoom(Long roomId, int count); + List findTopParticipationVotesByRoom(Long roomId, int count); List mapVoteItemsByVoteIds(Set voteIds, Long userId); diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java index adbbeca2b..4071ba234 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java @@ -3,7 +3,7 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; -import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingOrExpiredDetailViewResponse; import konkuk.thip.roompost.adapter.out.jpa.QVoteItemJpaEntity; import konkuk.thip.roompost.adapter.out.jpa.QVoteJpaEntity; import konkuk.thip.roompost.adapter.out.jpa.QVoteParticipantJpaEntity; @@ -54,7 +54,7 @@ private BooleanExpression filterByType(String type, QVoteJpaEntity post, Long us } @Override - public List findTopParticipationVotesByRoom(Long roomId, int count) { + public List findTopParticipationVotesByRoom(Long roomId, int count) { // 1. Fetch top votes by total participation count List topVotes = jpaQueryFactory .select(vote) @@ -69,16 +69,16 @@ public List findTopParticipationVotes // 2. Map to DTOs including vote items return topVotes.stream() .map(vote -> { - List voteItems = jpaQueryFactory + List voteItems = jpaQueryFactory .select(voteItem) .from(voteItem) .where(voteItem.voteJpaEntity.eq(vote)) .orderBy(voteItem.count.desc()) .fetch() .stream() - .map(item -> new RoomPlayingDetailViewResponse.CurrentVote.VoteItem(item.getItemName())) + .map(item -> new RoomPlayingOrExpiredDetailViewResponse.CurrentVote.VoteItem(item.getItemName())) .toList(); - return new RoomPlayingDetailViewResponse.CurrentVote( + return new RoomPlayingOrExpiredDetailViewResponse.CurrentVote( vote.getContent(), vote.getPage(), vote.isOverview(), diff --git a/src/main/java/konkuk/thip/roompost/application/port/out/VoteQueryPort.java b/src/main/java/konkuk/thip/roompost/application/port/out/VoteQueryPort.java index 3bb1a416e..ee68f34ec 100644 --- a/src/main/java/konkuk/thip/roompost/application/port/out/VoteQueryPort.java +++ b/src/main/java/konkuk/thip/roompost/application/port/out/VoteQueryPort.java @@ -1,6 +1,6 @@ package konkuk.thip.roompost.application.port.out; -import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse; +import konkuk.thip.room.adapter.in.web.response.RoomPlayingOrExpiredDetailViewResponse; import konkuk.thip.room.domain.Room; import konkuk.thip.roompost.application.port.out.dto.VoteItemQueryDto; @@ -10,7 +10,7 @@ public interface VoteQueryPort { - List findTopParticipationVotesByRoom(Room room, int count); + List findTopParticipationVotesByRoom(Room room, int count); Map> findVoteItemsByVoteIds(Set voteIds, Long userId); diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java similarity index 82% rename from src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java rename to src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java index 90f6e4a24..86852ca31 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java @@ -10,6 +10,7 @@ import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -18,7 +19,6 @@ import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteItemJpaRepository; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import konkuk.thip.user.domain.value.Alias; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,12 +27,15 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.List; import java.util.stream.IntStream; import static konkuk.thip.common.exception.code.ErrorCode.ROOM_ACCESS_FORBIDDEN; +import static konkuk.thip.room.domain.value.RoomStatus.EXPIRED; +import static konkuk.thip.room.domain.value.RoomStatus.IN_PROGRESS; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -40,9 +43,10 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) -@DisplayName("[통합] 진행 중인 방 상세조회 api 통합 테스트") -class RoomPlayingDetailViewApiTest { +@DisplayName("[통합] 진행 중인/완료된 방 상세조회 api 통합 테스트") +class RoomPlayingOrExpiredDetailViewApiTest { @Autowired private MockMvc mockMvc; @Autowired private UserJpaRepository userJpaRepository; @@ -52,19 +56,8 @@ class RoomPlayingDetailViewApiTest { @Autowired private VoteJpaRepository voteJpaRepository; @Autowired private VoteItemJpaRepository voteItemJpaRepository; - @AfterEach - void tearDown() { - voteItemJpaRepository.deleteAll(); - voteJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); - } - - private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { - Alias alias = TestEntityFactory.createScienceAlias(); + private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, RoomStatus roomStatus) { BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() .title(bookTitle) .isbn(isbn) @@ -85,9 +78,11 @@ private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String room .roomPercentage(0.0) .startDate(startDate) .endDate(LocalDate.now().plusDays(30)) - .recruitCount(recruitCount) + .recruitCount(10) .bookJpaEntity(book) .category(category) + .memberCount(4) + .roomStatus(roomStatus) .build()); } @@ -145,11 +140,59 @@ private void createVoteToRoom(UserJpaEntity creator, RoomJpaEntity roomJpaEntity } } + @Test + @DisplayName("완료된 모임방 상세조회할 경우, [해당 모임방의 정보, 책 정보, 유저의 현재 활동 정보, 현재 진행중인 투표]를 반환한다.") + void get_expired_room_detail() throws Exception { + //given + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),EXPIRED); + saveUsersToRoom(room, 4); + RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()).get(0); + roomParticipantJpaRepository.delete(roomParticipantJpaEntity); + RoomParticipantJpaEntity joiningMember = roomParticipantJpaRepository.save(RoomParticipantJpaEntity.builder() + .userJpaEntity(roomParticipantJpaEntity.getUserJpaEntity()) + .roomJpaEntity(roomParticipantJpaEntity.getRoomJpaEntity()) + .roomParticipantRole(RoomParticipantRole.MEMBER) // Member + .currentPage(50) // 현재 member의 마지막 활동 page + .userPercentage(10.6) // 현재 member의 활동 percentage + .build()); + + createVoteToRoom(joiningMember.getUserJpaEntity(), room, 2); // 2개의 투표 생성 + + //when + ResultActions result = mockMvc.perform(get("/rooms/{roomId}", room.getRoomId()) + .requestAttr("userId", joiningMember.getUserJpaEntity().getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isHost", is(false))) + .andExpect(jsonPath("$.data.roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomImageUrl", is(Category.SCIENCE_IT.getImageUrl()))) // 방 대표 이미지 추가 + .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1))))) + .andExpect(jsonPath("$.data.memberCount", is(4))) + .andExpect(jsonPath("$.data.recruitCount", is(10))) + .andExpect(jsonPath("$.data.isbn", is("isbn1"))) + .andExpect(jsonPath("$.data.bookTitle", is("과학-책"))) + .andExpect(jsonPath("$.data.currentPage", is(50))) + .andExpect(jsonPath("$.data.userPercentage", is(10))) // 내림 + .andExpect(jsonPath("$.data.currentVotes", hasSize(2))) + /** + * currentVotes 검증 : 현재 모임방의 참여율이 높은 투표와 투표 항목들을 노출 + * <정렬 순서> : 투표 참여율 높은 순 (vote 2 -> vote 1 순) + */ + .andExpect(jsonPath("$.data.currentVotes[0].content", is("vote-content-2"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[0].itemName", is("item-2-1"))) + .andExpect(jsonPath("$.data.currentVotes[0].voteItems[1].itemName", is("item-2-2"))) + + .andExpect(jsonPath("$.data.currentVotes[1].content", is("vote-content-1"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[0].itemName", is("item-1-1"))) + .andExpect(jsonPath("$.data.currentVotes[1].voteItems[1].itemName", is("item-1-2"))); + } + @Test @DisplayName("진행중인 모임방 상세조회할 경우, [해당 모임방의 정보, 책 정보, 유저의 현재 활동 정보, 현재 진행중인 투표]를 반환한다.") void get_playing_room_detail() throws Exception { //given - RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),IN_PROGRESS); saveUsersToRoom(room, 4); RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()).get(0); roomParticipantJpaRepository.delete(roomParticipantJpaEntity); @@ -164,7 +207,7 @@ void get_playing_room_detail() throws Exception { createVoteToRoom(joiningMember.getUserJpaEntity(), room, 2); // 2개의 투표 생성 //when - ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + ResultActions result = mockMvc.perform(get("/rooms/{roomId}", room.getRoomId()) .requestAttr("userId", joiningMember.getUserJpaEntity().getUserId())); //then @@ -197,7 +240,7 @@ void get_playing_room_detail() throws Exception { @DisplayName("모임방의 호스트가 조회할 경우, 유저가 해당 방의 호스트임을 응답값으로 보여준다. (나머지 응답값은 동일)") void get_playing_room_detail_host() throws Exception { //given - RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),IN_PROGRESS); saveUsersToRoom(room, 4); RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()).get(0); roomParticipantJpaRepository.delete(roomParticipantJpaEntity); @@ -212,7 +255,7 @@ void get_playing_room_detail_host() throws Exception { createVoteToRoom(roomHost.getUserJpaEntity(), room, 2); // 2개의 투표 생성 //when - ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + ResultActions result = mockMvc.perform(get("/rooms/{roomId}", room.getRoomId()) .requestAttr("userId", roomHost.getUserJpaEntity().getUserId())); //then @@ -245,7 +288,7 @@ void get_playing_room_detail_host() throws Exception { @DisplayName("모임방에 속하지 않는 유저가 진행중인 모임방 상세조회를 요청한 경우, 400 error 발생한다.") void get_playing_room_detail_not_belong_to_room() throws Exception { //given - RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),IN_PROGRESS); saveUsersToRoom(room, 4); RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()).get(0); roomParticipantJpaRepository.delete(roomParticipantJpaEntity); @@ -260,7 +303,7 @@ void get_playing_room_detail_not_belong_to_room() throws Exception { createVoteToRoom(joiningMember.getUserJpaEntity(), room, 2); // 2개의 투표 생성 //when //then - mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + mockMvc.perform(get("/rooms/{roomId}", room.getRoomId()) .requestAttr("userId", 1000L)) // 방에 속하지 않는 유저 .andExpect(status().isForbidden()) .andExpect(jsonPath("$.code").value(ROOM_ACCESS_FORBIDDEN.getCode())) @@ -271,7 +314,7 @@ void get_playing_room_detail_not_belong_to_room() throws Exception { @DisplayName("모임방에서 진행중인 투표가 많을 경우, 참여율이 높은 순으로 최대 3개의 투표만 보여준다.") void get_playing_room_detail_too_many_votes() throws Exception { //given - RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),IN_PROGRESS); saveUsersToRoom(room, 4); RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()).get(0); roomParticipantJpaRepository.delete(roomParticipantJpaEntity); @@ -286,7 +329,7 @@ void get_playing_room_detail_too_many_votes() throws Exception { createVoteToRoom(joiningMember.getUserJpaEntity(), room, 6); // 6개의 투표 생성 //when - ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + ResultActions result = mockMvc.perform(get("/rooms/{roomId}", room.getRoomId()) .requestAttr("userId", joiningMember.getUserJpaEntity().getUserId())); //then @@ -323,7 +366,7 @@ void get_playing_room_detail_too_many_votes() throws Exception { @DisplayName("모임방에서 진행중인 투표가 없을 경우, 빈 리스트를 보여준다.") void get_playing_room_detail_no_votes() throws Exception { //given - RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),IN_PROGRESS); saveUsersToRoom(room, 4); RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()).get(0); roomParticipantJpaRepository.delete(roomParticipantJpaEntity); @@ -338,7 +381,7 @@ void get_playing_room_detail_no_votes() throws Exception { createVoteToRoom(joiningMember.getUserJpaEntity(), room, 0); // 투표 생성 X //when - ResultActions result = mockMvc.perform(get("/rooms/{roomId}/playing", room.getRoomId()) + ResultActions result = mockMvc.perform(get("/rooms/{roomId}", room.getRoomId()) .requestAttr("userId", joiningMember.getUserJpaEntity().getUserId())); //then From 23eb3d3537d9b125fa7af9b12242afb6ab0f24f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:13:27 +0900 Subject: [PATCH 52/89] =?UTF-8?q?[refactor]=205.=20=EC=B5=9C=EA=B7=BC?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EB=90=9C=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=AA=A8=EC=A7=91=20=EB=A7=88?= =?UTF-8?q?=EA=B0=90=20=EC=9E=85=EB=B0=95=ED=95=9C=20=EB=B0=A9/=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=20=EB=B0=A9/=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1=EB=90=9C=20=EB=B0=A9=20-->=20?= =?UTF-8?q?=EB=AA=A8=EC=A7=91=EC=A4=91=EC=9D=B8=EB=B0=A9/=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=EB=B0=A9=20=EC=A1=B0=ED=9A=8C=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/RoomQueryController.java | 36 ++++----- .../RoomGetDeadlinePopularRecentResponse.java | 25 +++++++ .../RoomGetDeadlinePopularResponse.java | 22 ------ .../RoomQueryPersistenceAdapter.java | 13 +++- .../repository/RoomQueryRepository.java | 6 +- .../repository/RoomQueryRepositoryImpl.java | 75 +++++++++++++------ .../application/mapper/RoomQueryMapper.java | 6 +- .../RoomGetDeadlinePopularRecentUseCase.java | 8 ++ .../in/RoomGetDeadlinePopularUseCase.java | 8 -- .../application/port/out/RoomQueryPort.java | 5 +- ... RoomGetDeadlinePopularRecentService.java} | 21 +++--- ... RoomGetDeadlinePopularRecentApiTest.java} | 38 ++-------- 12 files changed, 144 insertions(+), 119 deletions(-) create mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularRecentResponse.java delete mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularResponse.java create mode 100644 src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularRecentUseCase.java delete mode 100644 src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularUseCase.java rename src/main/java/konkuk/thip/room/application/service/{RoomGetDeadlinePopularService.java => RoomGetDeadlinePopularRecentService.java} (63%) rename src/test/java/konkuk/thip/room/adapter/in/web/{RoomGetDeadlinePopularApiTest.java => RoomGetDeadlinePopularRecentApiTest.java} (80%) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index 12c04b8ba..da9c13f2f 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -27,10 +27,10 @@ public class RoomQueryController { private final RoomVerifyPasswordUseCase roomVerifyPasswordUseCase; private final RoomShowRecruitingDetailViewUseCase roomShowRecruitingDetailViewUseCase; private final RoomGetMemberListUseCase roomGetMemberListUseCase; - private final RoomShowPlayingDetailViewUseCase roomShowPlayingDetailViewUseCase; + private final RoomShowPlayingOrExpiredDetailViewUseCase roomShowPlayingOrExpiredDetailViewUseCase; private final RoomShowMineUseCase roomShowMineUseCase; private final RoomGetBookPageUseCase roomGetBookPageUseCase; - private final RoomGetDeadlinePopularUseCase roomGetDeadlinePopularUsecase; + private final RoomGetDeadlinePopularRecentUseCase roomGetDeadlinePopularRecentUseCase; @Operation( summary = "모집중인 방 검색", @@ -41,6 +41,7 @@ public class RoomQueryController { public BaseResponse searchRecruitingRooms( @Parameter(description = "검색 키워드 (책 이름 or 방 이름)", example = "해리") @RequestParam(value = "keyword", required = false, defaultValue = "") final String keyword, @Parameter(description = "모임방 카테고리", example = "문학") @RequestParam(value = "category", required = false, defaultValue = "") final String category, + @Parameter(description = "전체검색여부 (전체 검색에 해당할때만 true로 보내주세요) ", example = "true") @RequestParam(value = "isAllCategory", required = false, defaultValue = "false") final boolean isAllCategory, @Parameter(description = "정렬 방식 (마감 임박 : deadline, 신청 인원 : memberCount)", example = "deadline") @RequestParam("sort") final String sort, @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized, @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @@ -48,7 +49,7 @@ public BaseResponse searchRecruitingRooms( @Parameter(hidden = true) @UserId final Long userId ) { return BaseResponse.ok(roomSearchUseCase.searchRecruitingRooms( - RoomSearchQuery.of(keyword, category, sort, isFinalized, cursor, userId) + RoomSearchQuery.of(keyword, category, sort, isFinalized, cursor, userId,isAllCategory) )); } @@ -79,7 +80,7 @@ public BaseResponse getRecruitingRoomDetailVie @Operation( summary = "[모임 홈] 참여중인 내 모임방 조회", - description = "사용자가 참여중인 모임방 목록을 조회합니다." + description = "사용자가 참여중인 (모집중/진행중인 방) 모임방 목록을 조회합니다." ) @ExceptionDescription(ROOM_GET_HOME_JOINED_LIST) @GetMapping("/rooms/home/joined") @@ -105,18 +106,18 @@ public BaseResponse getRoomMemberList( return BaseResponse.ok(roomGetMemberListUseCase.getRoomMemberList(userId, roomId)); } - // 진행중인 방 상세보기 + // 진행중인/완료된 방 상세보기 @Operation( - summary = "진행중인 방 상세보기", - description = "진행중인 방의 상세 정보를 조회합니다." + summary = "진행중인/완료된 방 상세보기", + description = "진행중인/완료된 방의 상세 정보를 조회합니다." ) - @ExceptionDescription(ROOM_PLAYING_DETAIL) - @GetMapping("/rooms/{roomId}/playing") - public BaseResponse getPlayingRoomDetailView( + @ExceptionDescription(ROOM_PLAYING_OR_EXPIRED_DETAIL) + @GetMapping("/rooms/{roomId}") + public BaseResponse getPlayingOrExpiredRoomDetailView( @Parameter(hidden = true) @UserId final Long userId, @PathVariable("roomId") final Long roomId ) { - return BaseResponse.ok(roomShowPlayingDetailViewUseCase.getPlayingRoomDetailView(userId, roomId)); + return BaseResponse.ok(roomShowPlayingOrExpiredDetailViewUseCase.getPlayingOrExpiredRoomDetailView(userId, roomId)); } // 내 모임방 리스트 조회 @@ -148,16 +149,15 @@ public BaseResponse getBookPage( } @Operation( - summary = "마감 임박 및 인기 방 조회", - description = "카테고리별로 마감 임박 방과 인기 방을 조회합니다." + summary = "마감 임박/인기 방/최근 생성된 방 조회", + description = "카테고리별로 마감 임박 방, 인기 방, 최근 생성된 방을 조회합니다." ) - @ExceptionDescription(ROOM_GET_DEADLINE_POPULAR) + @ExceptionDescription(ROOM_GET_DEADLINE_POPULAR_RECENT) @GetMapping("/rooms") - public BaseResponse getDeadlineAndPopularRoomList( + public BaseResponse getDeadlineAndPopularAndRecentRoomList( @Parameter(description = "카테고리 이름 (default : 문학)", example = "과학/IT") - @RequestParam(value = "category", defaultValue = "문학") final String category, - @Parameter(hidden = true) @UserId final Long userId + @RequestParam(value = "category", defaultValue = "문학") final String category ) { - return BaseResponse.ok(roomGetDeadlinePopularUsecase.getDeadlineAndPopularRoomList(category, userId)); + return BaseResponse.ok(roomGetDeadlinePopularRecentUseCase.getDeadlineAndPopularAndRecentRoomList(category)); } } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularRecentResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularRecentResponse.java new file mode 100644 index 000000000..372b990d2 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularRecentResponse.java @@ -0,0 +1,25 @@ +package konkuk.thip.room.adapter.in.web.response; + +import java.util.List; + +public record RoomGetDeadlinePopularRecentResponse( + List deadlineRoomList, + List popularRoomList, + List recentRoomList +) { + public record RoomGetDeadlinePopularRecentDto( + Long roomId, + String bookImageUrl, + String roomName, + int recruitCount, // 방 최대 인원 수 + int memberCount, + String deadlineDate + ) { + } + + public static RoomGetDeadlinePopularRecentResponse of(List deadlineRoomList, + List popularRoomList, + List recentRoomList) { + return new RoomGetDeadlinePopularRecentResponse(deadlineRoomList, popularRoomList, recentRoomList); + } +} diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularResponse.java deleted file mode 100644 index 21d75bd2d..000000000 --- a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package konkuk.thip.room.adapter.in.web.response; - -import java.util.List; - -public record RoomGetDeadlinePopularResponse( - List deadlineRoomList, - List popularRoomList -) { - public record RoomGetDeadlinePopularDto( - Long roomId, - String bookImageUrl, - String roomName, - int recruitCount, // 방 최대 인원 수 - int memberCount, - String deadlineDate - ) { - } - - public static RoomGetDeadlinePopularResponse of(List deadlineRoomList, List popularRoomList) { - return new RoomGetDeadlinePopularResponse(deadlineRoomList, popularRoomList); - } -} diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java index 237172e33..0847b867f 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java @@ -169,13 +169,18 @@ private CursorBasedList findRoomsByMemberCountCursor(Cursor cursor } @Override - public List findRoomsByCategoryOrderByDeadline(Category category, int limit, Long userId) { - return roomJpaRepository.findRoomsByCategoryOrderByStartDateAsc(category, limit, userId); + public List findRoomsByCategoryOrderByDeadline(Category category, int limit) { + return roomJpaRepository.findRoomsByCategoryOrderByStartDateAsc(category, limit); } @Override - public List findRoomsByCategoryOrderByPopular(Category category, int limit, Long userId) { - return roomJpaRepository.findRoomsByCategoryOrderByMemberCount(category, limit, userId); + public List findRoomsByCategoryOrderByPopular(Category category, int limit) { + return roomJpaRepository.findRoomsByCategoryOrderByMemberCount(category, limit); + } + + @Override + public List findRoomsByCategoryOrderByRecent(Category category, int limit) { + return roomJpaRepository.findRoomsByCategoryOrderByCreatedAtDesc(category, limit); } @Override diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java index 03af9f0b9..453d396ac 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java @@ -31,9 +31,11 @@ public interface RoomQueryRepository { List findExpiredRoomsUserParticipated(Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize); - List findRoomsByCategoryOrderByStartDateAsc(Category category, int limit, Long userId); + List findRoomsByCategoryOrderByStartDateAsc(Category category, int limit); - List findRoomsByCategoryOrderByMemberCount(Category category, int limit, Long userId); + List findRoomsByCategoryOrderByMemberCount(Category category, int limit); + + List findRoomsByCategoryOrderByCreatedAtDesc(Category category, int limit); List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalDate dateCursor, Long roomIdCursor, int pageSize); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java index c791ed28e..2e039bf51 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java @@ -24,8 +24,12 @@ import org.springframework.stereotype.Repository; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; +import static konkuk.thip.room.domain.value.RoomStatus.IN_PROGRESS; +import static konkuk.thip.room.domain.value.RoomStatus.RECRUITING; + @Repository @RequiredArgsConstructor public class RoomQueryRepositoryImpl implements RoomQueryRepository { @@ -38,7 +42,7 @@ public class RoomQueryRepositoryImpl implements RoomQueryRepository { /** 모집중 + ACTIVE 공통 where */ private BooleanBuilder recruitingActiveWhere() { BooleanBuilder where = new BooleanBuilder(); - where.and(room.roomStatus.eq(RoomStatus.RECRUITING)); + where.and(room.roomStatus.eq(RECRUITING)); return where; } @@ -170,7 +174,7 @@ public List findOtherRecruitingR .join(room.bookJpaEntity, book) .where( room.category.eq(category) - .and(room.roomStatus.eq(RoomStatus.RECRUITING)) // 모집 중인 방 + .and(room.roomStatus.eq(RECRUITING)) // 모집 중인 방 .and(room.roomId.ne(roomId))// 현재 방 제외 .and(room.isPublic.isTrue()) // 공개방 만 ) @@ -200,10 +204,12 @@ public List findHomeJoinedRoomsByUserPercentage( // 검색 조건(where) 조립 // 유저가 참여한 방만: userId 조건 - // 활동 기간 중인 방만: IN_PROGRESS 상태 + // 활동/모집 기간 중인 방만: IN_PROGRESS or RECRUITING BooleanBuilder where = new BooleanBuilder(); where.and(participant.userJpaEntity.userId.eq(userId)); - where.and(room.roomStatus.eq(RoomStatus.IN_PROGRESS)); // 활동 기간 중인 방만: IN_PROGRESS 상태 + where.and(room.roomStatus.eq(IN_PROGRESS) + .or(room.roomStatus.eq(RECRUITING))); // 활동: IN_PROGRESS 상태, 모집: RECRUITING + // 커서 기반 추가 조건 if (userPercentageCursor != null && startDateCursor != null && roomIdCursor != null) { @@ -218,6 +224,7 @@ public List findHomeJoinedRoomsByUserPercentage( ); } + //정렬 순서 : 진행중인 모임방 (진행도 높은 순 > 활동시작일 빠른 순 > 방 아이디 오름차순) > 참여한 모집중인 모임방 (활동 시작일 빠른 순 > 방 아이디 오름차순) return queryFactory .select(new QRoomParticipantQueryDto( room.roomId, @@ -225,16 +232,22 @@ public List findHomeJoinedRoomsByUserPercentage( room.title, room.memberCount, participant.userPercentage, - room.startDate + room.startDate, + room.roomStatus )) .from(participant) .join(participant.roomJpaEntity, room) .join(room.bookJpaEntity, book) .where(where) .orderBy( - participant.userPercentage.desc(), // 진행률 높은 순(내림차순) - room.startDate.asc(), // 진행률 같으면 활동 시작일 빠른 순 (오름차순) - room.roomId.asc() // 둘 다 같으면 방 아이디 작은 순 (오름차순) + // 1. 상태가 IN_PROGRESS이면 진행률 높은 순, 모집중이면 0(기본값)로 처리해 후순위로 빠지게 함 + new CaseBuilder() + .when(room.roomStatus.eq(IN_PROGRESS)) + .then(participant.userPercentage) + .otherwise((double) 0) + .desc(), + room.startDate.asc(), // 2. 활동 시작일 빠른 순 + room.roomId.asc() // 3. 방 아이디 작은 순(오름차순) ) .limit(pageSize + 1) .fetch(); @@ -246,7 +259,7 @@ public List findRecruitingRoomsUserParticipated( Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(room.roomStatus.eq(RoomStatus.RECRUITING)); // 유저가 참여한 방 && 모집중인 방 + .and(room.roomStatus.eq(RECRUITING)); // 유저가 참여한 방 && 모집중인 방 DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -261,7 +274,7 @@ public List findPlayingRoomsUserParticipated( Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(room.roomStatus.eq(RoomStatus.IN_PROGRESS)); // 유저가 참여한 방 && 현재 진행중인 방 + .and(room.roomStatus.eq(IN_PROGRESS)); // 유저가 참여한 방 && 현재 진행중인 방 DateExpression cursorExpr = room.endDate; // 커서 비교는 endDate(= 진행 마감일) OrderSpecifier[] orders = new OrderSpecifier[]{ cursorExpr.asc(), room.roomId.asc() @@ -275,8 +288,8 @@ public List findPlayingRoomsUserParticipated( public List findPlayingAndRecruitingRoomsUserParticipated( Long userId, Integer priorityCursor, LocalDate dateCursor, Long roomIdCursor, int pageSize ) { - BooleanExpression playing = room.roomStatus.eq(RoomStatus.IN_PROGRESS); - BooleanExpression recruiting = room.roomStatus.eq(RoomStatus.RECRUITING); + BooleanExpression playing = room.roomStatus.eq(IN_PROGRESS); + BooleanExpression recruiting = room.roomStatus.eq(RECRUITING); BooleanExpression base = participant.userJpaEntity.userId.eq(userId) .and(playing.or(recruiting)); // 유저가 참여한 방 && 현재 진행중인 방 + 모집중인 방 @@ -310,7 +323,7 @@ public List findExpiredRoomsUserParticipated( } @Override - public List findRoomsByCategoryOrderByStartDateAsc(Category category, int limit, Long userId) { + public List findRoomsByCategoryOrderByStartDateAsc(Category category, int limit) { return queryFactory .select(new QRoomQueryDto( room.roomId, @@ -323,14 +336,14 @@ public List findRoomsByCategoryOrderByStartDateAsc(Category catego )) .from(room) .join(room.bookJpaEntity, book) - .where(findDeadlinePopularRoomCondition(category, userId)) + .where(findDeadlinePopularRecentRoomCondition(category)) .orderBy(room.startDate.asc(), room.memberCount.desc(), room.roomId.asc()) .limit(limit) .fetch(); } @Override - public List findRoomsByCategoryOrderByMemberCount(Category category, int limit, Long userId) { + public List findRoomsByCategoryOrderByMemberCount(Category category, int limit) { return queryFactory .select(new QRoomQueryDto( room.roomId, @@ -343,17 +356,38 @@ public List findRoomsByCategoryOrderByMemberCount(Category categor )) .from(room) .join(room.bookJpaEntity, book) - .where(findDeadlinePopularRoomCondition(category, userId)) + .where(findDeadlinePopularRecentRoomCondition(category)) .orderBy(room.memberCount.desc(), room.startDate.asc(), room.roomId.asc()) .limit(limit) .fetch(); } + @Override + public List findRoomsByCategoryOrderByCreatedAtDesc(Category category, int limit) { + return queryFactory + .select(new QRoomQueryDto( + room.roomId, + book.imageUrl, + room.title, + room.recruitCount, + room.memberCount, + room.startDate, + room.roomStatus + )) + .from(room) + .join(room.bookJpaEntity, book) + .where(findDeadlinePopularRecentRoomCondition(category) + .and(room.createdAt.goe(LocalDateTime.now().minusHours(72)))) //생성된지 72시간 이내 + .orderBy(room.createdAt.desc(), room.roomId.desc()) + .limit(limit) + .fetch(); + } + @Override public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalDate dateCursor, Long roomIdCursor, int pageSize) { DateExpression cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) - .and(room.roomStatus.eq(RoomStatus.RECRUITING)); // 모집중인 방 + .and(room.roomStatus.eq(RECRUITING)); // 모집중인 방 if (dateCursor != null && roomIdCursor != null) { // 첫 페이지가 아닌 경우 @@ -380,11 +414,10 @@ public List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalD .fetch(); } - private BooleanExpression findDeadlinePopularRoomCondition(Category category, Long userId) { + private BooleanExpression findDeadlinePopularRecentRoomCondition(Category category) { return room.category.eq(category) - .and(room.roomStatus.eq(RoomStatus.RECRUITING)) // 모집중인 방 - .and(room.isPublic.isTrue()) // 공개 방만 조회 - .and(userJoinedRoom(userId).not()); // 유저가 참여하지 않은 방만 조회 + .and(room.roomStatus.eq(RECRUITING)) // 모집중인 방 + .and(room.isPublic.isTrue()); // 공개 방만 조회 } /** diff --git a/src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java b/src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java index ad930e20c..d7b3df6fc 100644 --- a/src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java +++ b/src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java @@ -1,7 +1,7 @@ package konkuk.thip.room.application.mapper; import konkuk.thip.common.util.DateUtil; -import konkuk.thip.room.adapter.in.web.response.RoomGetDeadlinePopularResponse; +import konkuk.thip.room.adapter.in.web.response.RoomGetDeadlinePopularRecentResponse; import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; import konkuk.thip.room.adapter.in.web.response.RoomShowMineResponse; import konkuk.thip.room.application.port.out.dto.RoomQueryDto; @@ -29,9 +29,9 @@ public interface RoomQueryMapper { target = "deadlineDate", expression = "java(DateUtil.formatAfterTime(dto.endDate()))" ) - RoomGetDeadlinePopularResponse.RoomGetDeadlinePopularDto toDeadlinePopularRoomDto(RoomQueryDto dto); + RoomGetDeadlinePopularRecentResponse.RoomGetDeadlinePopularRecentDto toDeadlinePopularRecentRoomDto(RoomQueryDto dto); - List toDeadlinePopularRoomDtoList(List roomQueryDtos); + List toDeadlinePopularRecentRoomDtoList(List roomQueryDtos); diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularRecentUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularRecentUseCase.java new file mode 100644 index 000000000..c5a22c283 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularRecentUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.room.application.port.in; + +import konkuk.thip.room.adapter.in.web.response.RoomGetDeadlinePopularRecentResponse; + +public interface RoomGetDeadlinePopularRecentUseCase { + + RoomGetDeadlinePopularRecentResponse getDeadlineAndPopularAndRecentRoomList(String category); +} diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularUseCase.java deleted file mode 100644 index ded5d0d12..000000000 --- a/src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularUseCase.java +++ /dev/null @@ -1,8 +0,0 @@ -package konkuk.thip.room.application.port.in; - -import konkuk.thip.room.adapter.in.web.response.RoomGetDeadlinePopularResponse; - -public interface RoomGetDeadlinePopularUseCase { - - RoomGetDeadlinePopularResponse getDeadlineAndPopularRoomList(String category, Long userId); -} diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java index 8c43d2034..c8dcf7b6f 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java @@ -36,10 +36,11 @@ public interface RoomQueryPort { CursorBasedList findRoomsByIsbnOrderByDeadline(String isbn, Cursor cursor); - List findRoomsByCategoryOrderByDeadline(Category category, int limit, Long userId); + List findRoomsByCategoryOrderByDeadline(Category category, int limit); - List findRoomsByCategoryOrderByPopular(Category category, int limit, Long userId); + List findRoomsByCategoryOrderByPopular(Category category, int limit); + List findRoomsByCategoryOrderByRecent(Category category, int limit); /** * 임시 메서드 * TODO 리펙토링 대상 diff --git a/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularService.java b/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java similarity index 63% rename from src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularService.java rename to src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java index ba6bad483..752bac44e 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java @@ -1,8 +1,8 @@ package konkuk.thip.room.application.service; -import konkuk.thip.room.adapter.in.web.response.RoomGetDeadlinePopularResponse; +import konkuk.thip.room.adapter.in.web.response.RoomGetDeadlinePopularRecentResponse; import konkuk.thip.room.application.mapper.RoomQueryMapper; -import konkuk.thip.room.application.port.in.RoomGetDeadlinePopularUseCase; +import konkuk.thip.room.application.port.in.RoomGetDeadlinePopularRecentUseCase; import konkuk.thip.room.application.port.out.RoomQueryPort; import konkuk.thip.room.domain.value.Category; import lombok.RequiredArgsConstructor; @@ -11,7 +11,7 @@ @Service @RequiredArgsConstructor -public class RoomGetDeadlinePopularService implements RoomGetDeadlinePopularUseCase { +public class RoomGetDeadlinePopularRecentService implements RoomGetDeadlinePopularRecentUseCase { private final RoomQueryPort roomQueryPort; private final RoomQueryMapper roomQueryMapper; @@ -20,14 +20,17 @@ public class RoomGetDeadlinePopularService implements RoomGetDeadlinePopularUseC @Override @Transactional(readOnly = true) - public RoomGetDeadlinePopularResponse getDeadlineAndPopularRoomList(String categoryStr, Long userId) { + public RoomGetDeadlinePopularRecentResponse getDeadlineAndPopularAndRecentRoomList(String categoryStr) { Category category = Category.from(categoryStr); - var deadlineRoomList = roomQueryMapper.toDeadlinePopularRoomDtoList( - roomQueryPort.findRoomsByCategoryOrderByDeadline(category, DEFAULT_LIMIT, userId)); - var popularRoomList = roomQueryMapper.toDeadlinePopularRoomDtoList( - roomQueryPort.findRoomsByCategoryOrderByPopular(category, DEFAULT_LIMIT, userId)); + var deadlineRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( + roomQueryPort.findRoomsByCategoryOrderByDeadline(category, DEFAULT_LIMIT)); + var popularRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( + roomQueryPort.findRoomsByCategoryOrderByPopular(category, DEFAULT_LIMIT)); + var recentRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( + roomQueryPort.findRoomsByCategoryOrderByRecent(category, DEFAULT_LIMIT)); - return RoomGetDeadlinePopularResponse.of(deadlineRoomList, popularRoomList); + return RoomGetDeadlinePopularRecentResponse.of(deadlineRoomList, popularRoomList,recentRoomList); } + } diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java similarity index 80% rename from src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java rename to src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java index d097c98b5..59025daac 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java @@ -34,20 +34,18 @@ /** * /rooms?category=문학 API 통합 테스트 - * - 마감 임박, 인기 방 조회 - * - 내가 참여한 방은 제외 + * - 마감 임박, 인기 방, 최근생성된 방 조회 * - 비공개 방은 제외 */ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) -@DisplayName("[통합] 마감 임박 및 인기 방 조회 API 통합 테스트") +@DisplayName("[통합] 마감 임박, 인기 방, 최근 생성된 방 조회 API 통합 테스트") @Transactional -class RoomGetDeadlinePopularApiTest { +class RoomGetDeadlinePopularRecentApiTest { @Autowired private MockMvc mockMvc; @Autowired private RoomJpaRepository roomJpaRepository; - @Autowired private RoomParticipantJpaRepository participantJpaRepository; @Autowired private UserJpaRepository userJpaRepository; @Autowired private BookJpaRepository bookJpaRepository; @@ -61,7 +59,6 @@ class RoomGetDeadlinePopularApiTest { private final LocalDate today = LocalDate.now(); private RoomJpaEntity privateRoom; - private RoomJpaEntity joinedRoom; @BeforeEach void setUp() { @@ -100,19 +97,10 @@ void setUp() { privateRoom.updateRoomStatus(RoomStatus.RECRUITING); privateRoom.updateIsPublic(false); roomJpaRepository.save(privateRoom); - - // 내가 참여한 방 (조건 불만족) - joinedRoom = TestEntityFactory.createRoom(book, category); - joinedRoom.updateStartDate(today.plusDays(5)); - joinedRoom.updateRoomStatus(RoomStatus.RECRUITING); - joinedRoom = roomJpaRepository.save(joinedRoom); - participantJpaRepository.save( - TestEntityFactory.createRoomParticipant(joinedRoom, currentUser, RoomParticipantRole.MEMBER, 0) - ); } @Test - @DisplayName("카테고리별로 마감 임박 방 4개와 인기 방 4개를 조회하고 조건과 정렬을 검증한다") + @DisplayName("카테고리별로 마감 임박 방 4개와 인기 방 4개 최근생성된 방 4개를 조회하고 조건과 정렬을 검증한다") void getDeadlineAndPopularRooms() throws Exception { mockMvc.perform( get("/rooms") @@ -128,20 +116,10 @@ void getDeadlineAndPopularRooms() throws Exception { .andExpect(jsonPath("$.data.deadlineRoomList[0].bookImageUrl").isString()) .andExpect(jsonPath("$.data.deadlineRoomList[0].roomName").isString()) .andExpect(jsonPath("$.data.deadlineRoomList[0].memberCount").isNumber()) - .andExpect(jsonPath("$.data.deadlineRoomList[0].deadlineDate").isString()); - } - - @Test - @DisplayName("내가 참여한 방은 조회 결과에 포함되지 않는다") - void joinedRoomIsExcluded() throws Exception { - mockMvc.perform( - get("/rooms") - .param("category", "문학") - .requestAttr("userId", currentUser.getUserId()) - ) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.deadlineRoomList[*].roomId", not(hasItem(joinedRoom.getRoomId().intValue())))) - .andExpect(jsonPath("$.data.popularRoomList[*].roomId", not(hasItem(joinedRoom.getRoomId().intValue())))); + .andExpect(jsonPath("$.data.deadlineRoomList[0].deadlineDate").isString()) + .andExpect(jsonPath("$.data.recentRoomList", hasSize(4))) + .andExpect(jsonPath("$.data.recentRoomList[0].roomId").value(rooms.get(9).getRoomId())) //최근생성된 방은 생성된 순으로 정렬 + .andExpect(jsonPath("$.data.recentRoomList[1].roomId").value(rooms.get(8).getRoomId())); } @Test From 22e5a808241ba30f949aabc92303f12e3248fa67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:13:56 +0900 Subject: [PATCH 53/89] =?UTF-8?q?[refactor]=206.=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EB=B0=A9=20=EA=B2=80=EC=83=89=EC=8B=9C=EC=97=90=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EB=B0=A9=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20=20?= =?UTF-8?q?(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/dto/RoomSearchQuery.java | 7 +- .../service/RoomSearchService.java | 71 ++++++-- .../adapter/in/web/RoomSearchApiTest.java | 151 ++++++++++++------ 3 files changed, 165 insertions(+), 64 deletions(-) diff --git a/src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java index 7de400e8b..cda18478e 100644 --- a/src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java +++ b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java @@ -6,10 +6,11 @@ public record RoomSearchQuery( String sortStr, boolean isFinalized, String cursorStr, - Long userId + Long userId, + boolean isAllCategory ) { public static RoomSearchQuery of (String keyword, String categoryStr, String sortStr, - boolean isFinalized, String cursorStr, Long userId) { - return new RoomSearchQuery(keyword, categoryStr, sortStr, isFinalized, cursorStr, userId); + boolean isFinalized, String cursorStr, Long userId, boolean isAllCategory) { + return new RoomSearchQuery(keyword, categoryStr, sortStr, isFinalized, cursorStr, userId, isAllCategory); } } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java index 849a38051..0fb7e22d1 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java @@ -1,5 +1,6 @@ package konkuk.thip.room.application.service; +import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.util.Cursor; import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.recentSearch.domain.value.RecentSearchType; @@ -16,6 +17,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; + @Service @RequiredArgsConstructor public class RoomSearchService implements RoomSearchUseCase { @@ -31,6 +34,7 @@ public class RoomSearchService implements RoomSearchUseCase { public RoomSearchResponse searchRecruitingRooms(RoomSearchQuery query) { // 1. validation RoomSearchSortParam sortParam = RoomSearchSortParam.from(query.sortStr()); + validateSearchParams(query.keyword(),query.isAllCategory(),query.categoryStr()); Category category = validateCategory(query.categoryStr()); // 2. Cursor 생성 @@ -51,23 +55,48 @@ public RoomSearchResponse searchRecruitingRooms(RoomSearchQuery query) { } private CursorBasedList executeRecruitingRoomSearch(RoomSearchQuery query, Category category, RoomSearchSortParam sortParam, Cursor cursor) { - CursorBasedList result = null; + boolean isAllCategory = query.isAllCategory(); + String keyword = query.keyword(); + boolean isKeywordEmpty = (keyword == null || keyword.trim().isEmpty()); + + // 빈 키워드이면서 isAllCategory가 true인 경우는 전체 조회를 위해 빈 문자열을 사용하고, 그렇지 않으면 그대로 keyword를 사용 + String effectiveKeyword = isKeywordEmpty && isAllCategory ? "" : keyword; + if (category == null) { - switch (sortParam) { - case DEADLINE: - return roomQueryPort.searchRecruitingRoomsByDeadline(query.keyword(), cursor); - case MEMBER_COUNT: - return roomQueryPort.searchRecruitingRoomsByMemberCount(query.keyword(), cursor); + // 전체 카테고리 중에서 + // 1) 전체검색(isAllCategory=true)이거나 + // 2) 키워드가 비어있지 않은 경우 + // 해당 조건 모두 포함해서 키워드 기반 검색 또는 전체 검색 수행 + if (isAllCategory || !isKeywordEmpty) { + switch (sortParam) { + case DEADLINE: + return roomQueryPort.searchRecruitingRoomsByDeadline(effectiveKeyword, cursor); + case MEMBER_COUNT: + return roomQueryPort.searchRecruitingRoomsByMemberCount(effectiveKeyword, cursor); + } } } else { - switch (sortParam) { - case DEADLINE: - return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(query.keyword(), category, cursor); - case MEMBER_COUNT: - return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(query.keyword(), category, cursor); + if (isAllCategory && isKeywordEmpty) { + // isAllCategory가 true이고, 키워드가 비어있으면 + // 특정 카테고리 내에서 '전체 조회'를 의미함 즉, 키워드 없이 카테고리 필터만 적용해서 전체 방 조회 + switch (sortParam) { + case DEADLINE: + return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor); + case MEMBER_COUNT: + return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor); + } + } else if (!isAllCategory) { + // isAllCategory가 false인 경우 (전체검색 아님) + // category가 존재하고 키워드는 있거나 빈 문자열이어도 키워드 기반 조회 수행 + switch (sortParam) { + case DEADLINE: + return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(effectiveKeyword, category, cursor); + case MEMBER_COUNT: + return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(effectiveKeyword, category, cursor); + } } } - return result; + return null; } private Category validateCategory(String categoryStr) { @@ -77,4 +106,22 @@ private Category validateCategory(String categoryStr) { return Category.from(categoryStr); } + + private void validateSearchParams(String keyword, boolean isAllCategory, String categoryStr) { + boolean isKeywordEmpty = (keyword == null || keyword.trim().isEmpty()); + boolean isCategoryEmpty = (categoryStr == null || categoryStr.trim().isEmpty()); + + // 키워드와 카테고리 둘 다 없을 때 isAllCategory가 true여야 함 + if (isKeywordEmpty && isCategoryEmpty && !isAllCategory) { + throw new BusinessException(API_INVALID_PARAM, + new IllegalArgumentException("검색어와 카테고리가 없을 경우, 전체 검색을 명시하는 isAllCategory=true 옵션이 필요합니다.")); + } + + // 기존 예외 : 키워드 있는데 isAllCategory=true 이면서 특정 카테고리 존재 불가 + if (isAllCategory && !isKeywordEmpty && !isCategoryEmpty) { + throw new BusinessException(API_INVALID_PARAM, + new IllegalArgumentException("키워드가 있을 때 특정 카테고리 검색과 전체검색(isAllCategory=true)을 동시에 사용할 수 없습니다.")); + } + } + } diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java index 0a45f740f..582e60a18 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java @@ -8,13 +8,11 @@ import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; -import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.room.domain.value.RoomStatus; 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +23,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -36,6 +35,7 @@ @SpringBootTest @ActiveProfiles("test") +@Transactional @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 방 검색 api 통합 테스트") class RoomSearchApiTest { @@ -44,19 +44,9 @@ class RoomSearchApiTest { @Autowired private UserJpaRepository userJpaRepository; @Autowired private BookJpaRepository bookJpaRepository; @Autowired private RoomJpaRepository roomJpaRepository; - @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; @Autowired private RecentSearchJpaRepository recentSearchJpaRepository; @Autowired private JdbcTemplate jdbcTemplate; - @AfterEach - void tearDown() { - recentSearchJpaRepository.deleteAllInBatch(); - roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAllInBatch(); - } - private RoomJpaEntity saveScienceRecruitingRoom(String bookTitle, String roomName, LocalDate startDate, LocalDate endDate) { BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle(bookTitle)); @@ -92,6 +82,106 @@ private void updateRoomMemberCount(RoomJpaEntity roomJpaEntity, int count) { ); } + + @Test + @DisplayName("isAllCategory=true, keyword 입력 x, 카테고리 선택 X -> 방 전체조회") + void search_allCategory_true_keyword_empty_category_null() throws Exception { + + //given + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + updateRoomMemberCount(science_room_1, 4); + + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + updateRoomMemberCount(science_room_3, 2); + + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + updateRoomMemberCount(room_3, 6); + + RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); + updateRoomMemberCount(recruit_expired_room_4, 6); + + // when + ResultActions result = mockMvc.perform(get("/rooms/search") + .requestAttr("userId", 1L) + .param("isAllCategory", "true") // 전체 카테고리 포함 + .param("sort", "deadline") + .param("isFinalized", "false")); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomList", hasSize(3))) // 결과 리스트 크기 확인 + .andExpect(jsonPath("$.data.isLast", is(true))) + /** + * roomList 검증 : 정렬 순서, 방 검색 결과 검증 + * <정렬 순서> + * 1. 정렬 조건 + * 2. 정렬 조건이 같을 경우, roomId 기준 오름차순 정렬 (무한 스크롤 시 누락 & 중복 되는 데이터 발생하지 않도록) + */ + .andExpect(jsonPath("$.data.roomList[0].roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomList[1].roomName", is("무슨방일까요??"))) + .andExpect(jsonPath("$.data.roomList[2].roomName", is("방제목에-과학-포함된-문학방"))); + } + + @Test + @DisplayName("isAllCategory=true, keyword = [과학], 카테고리 선택 X -> 전체 방중에서 키워드 검색") + void search_allCategory_true_keyword_nonEmpty_category_null() throws Exception { + // given + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + updateRoomMemberCount(science_room_1, 4); + RoomJpaEntity science_room_2 = saveScienceRecruitingRoom("과학-책", "방이름입니다", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30)); + updateRoomMemberCount(science_room_2, 5); + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + updateRoomMemberCount(room_3, 6); + + // when + ResultActions result = mockMvc.perform(get("/rooms/search") + .requestAttr("userId", 1L) + .param("keyword", "과학") // 실제 키워드 있음 + .param("isAllCategory", "true") + .param("sort", "deadline") + .param("isFinalized", "false")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomList", hasSize(3))) + .andExpect(jsonPath("$.data.roomList[0].roomName", containsString("과학"))) + .andExpect(jsonPath("$.data.roomList[1].roomName", containsString("방이름입니다"))) + .andExpect(jsonPath("$.data.roomList[2].roomName", containsString("과학"))) + .andExpect(jsonPath("$.data.isLast", is(true))); + } + + @Test + @DisplayName("isAllCategory=true, keyword 입력 x, 카테고리 = [과학/IT] -> [과학/IT] 카테고리 내 전체 조회") + void search_allCategory_true_keyword_empty_category_given() throws Exception { + // given + RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); + updateRoomMemberCount(science_room_1, 4); + + RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); + updateRoomMemberCount(science_room_3, 2); + + RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); + updateRoomMemberCount(room_3, 6); + + RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); + updateRoomMemberCount(recruit_expired_room_4, 6); + // when + ResultActions result = mockMvc.perform(get("/rooms/search") + .requestAttr("userId", 1L) + .param("category", Category.SCIENCE_IT.getValue()) // 과학 카테고리 설정 + .param("isAllCategory", "true") + .param("sort", "deadline") + .param("isFinalized", "false")); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomList", hasSize(2))) + .andExpect(jsonPath("$.data.roomList[0].roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomList[1].roomName", is("무슨방일까요??"))) + .andExpect(jsonPath("$.data.isLast", is(true))); + } + + @Test @DisplayName("keyword = [과학], 카테고리 선택 X, 정렬 = [마감임박순] 일 경우, 방이름 or 책제목에 '과학'이 포함된 모집중인 방 검색 결과가 마감임박순으로 반환된다.") void search_keyword_and_sort_deadline() throws Exception { @@ -230,43 +320,6 @@ void search_category_and_sort_deadline() throws Exception { .andExpect(jsonPath("$.data.roomList[1].roomName", is("무슨방일까요??"))); } - @Test - @DisplayName("keyword 입력 x, 카테고리 입력 x, 정렬 = [마감임박순] 일 경우, DB에 존재하는 전체 방 검색 결과가 반환된다.") - void search_sort_deadline() throws Exception { - //given - RoomJpaEntity science_room_1 = saveScienceRecruitingRoom("과학-책", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30)); - updateRoomMemberCount(science_room_1, 4); - - RoomJpaEntity science_room_3 = saveScienceRecruitingRoom("과학-책", "무슨방일까요??", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30)); - updateRoomMemberCount(science_room_3, 2); - - RoomJpaEntity room_3 = saveLiteratureRecruitingRoom("문학-책", "방제목에-과학-포함된-문학방", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30)); - updateRoomMemberCount(room_3, 6); - - RoomJpaEntity recruit_expired_room_4 = saveScienceProgressRoom("과학-책", "모집기한-지난-과학방", LocalDate.now().minusDays(1), LocalDate.now().plusDays(30)); - updateRoomMemberCount(recruit_expired_room_4, 6); - - //when - ResultActions result = mockMvc.perform(get("/rooms/search") - .requestAttr("userId", 1L) - .param("sort", "deadline") - .param("isFinalized", String.valueOf(false))); - - //then - result.andExpect(status().isOk()) - .andExpect(jsonPath("$.data.roomList", hasSize(3))) // 결과 리스트 크기 확인 - .andExpect(jsonPath("$.data.isLast", is(true))) - /** - * roomList 검증 : 정렬 순서, 방 검색 결과 검증 - * <정렬 순서> - * 1. 정렬 조건 - * 2. 정렬 조건이 같을 경우, roomId 기준 오름차순 정렬 (무한 스크롤 시 누락 & 중복 되는 데이터 발생하지 않도록) - */ - .andExpect(jsonPath("$.data.roomList[0].roomName", is("과학-방-1일뒤-활동시작"))) - .andExpect(jsonPath("$.data.roomList[1].roomName", is("무슨방일까요??"))) - .andExpect(jsonPath("$.data.roomList[2].roomName", is("방제목에-과학-포함된-문학방"))); - } - @Test @DisplayName("keyword=[과학], category=[과학/IT], 정렬=[마감임박순] 일 경우, keyword, category 조건을 모두 만족하는 방만 반환된다.") void search_keyword_and_category() throws Exception { From 948285f6b896059d3bcd6d2a3d8137616e6c5657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:14:10 +0900 Subject: [PATCH 54/89] =?UTF-8?q?[refactor]=207.=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EB=B0=A9=20=EB=B8=94=EB=9D=BC=EC=9D=B8=EB=93=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20-=20=EA=B8=80=EC=9E=90=20=EC=88=98=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1(=EB=9D=84=EC=96=B4=EC=93=B0=EA=B8=B0)?= =?UTF-8?q?=EC=9D=84=20=EC=9B=90=EB=B3=B8=20=EB=82=B4=EC=9A=A9=EA=B3=BC=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=ED=95=98=EA=B2=8C=20=EC=B9=98=ED=99=98=20=20?= =?UTF-8?q?(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validator/RoomPostAccessValidator.java | 28 +++++++++++++------ .../adapter/in/web/RoomPostSearchApiTest.java | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java b/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java index 705b8e8c8..5c2c80d5e 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java +++ b/src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java @@ -7,7 +7,7 @@ @HelperService public class RoomPostAccessValidator { - private static final String BLURRED_STRING = "여긴 못 지나가지롱~~"; + private static final String BLURRED_STRING = "태정태세문단세예성연중인명선광인효현숙경영정순헌철고순"; public void validateGroupRoomPostFilters(Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview, int bookPageSize, double currentPercentage) { if(!isPageFilter && !isOverview) { // 어떤 필터도 적용되지 않는 경우 @@ -52,22 +52,32 @@ public String createBlurredString(String contents) { return contents; } - int originalLength = contents.length(); int blurLen = BLURRED_STRING.length(); + StringBuilder sb = new StringBuilder(contents.length()); - // 필요한 전체 반복 횟수 계산 - int repeat = originalLength / blurLen; + // 블러 문자열 인덱스 + int blurIndex = 0; - StringBuilder sb = new StringBuilder(originalLength); + for (int i = 0; i < contents.length(); i++) { + char ch = contents.charAt(i); - // 몫 만큼 반복 - for (int i = 0; i < repeat + 1; i++) { - sb.append(BLURRED_STRING); + // 특수문자/공백일 경우 그대로 append + if (Character.isWhitespace(ch) || isSpecialCharacter(ch)) { + sb.append(ch); + } else { + // 나머지 문자들은 모두 치환 + sb.append(BLURRED_STRING.charAt(blurIndex)); + blurIndex = (blurIndex + 1) % blurLen; // 순환 + } } - return sb.toString(); } + private boolean isSpecialCharacter(char ch) { + // 아스키 문자 중 문자/숫자만 제외하고 모두 특수문자 처리 예시 + return !Character.isLetterOrDigit(ch); + } + public boolean isLocked(int currentPage, int bookPageSize) { return currentPage < bookPageSize; } diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java index 7eee4c414..d7e8571be 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java @@ -345,7 +345,7 @@ void searchGroupRecords_locked_content_blurred() throws Exception { String content = post.path("content").asText(); if (isLocked) { - assertThat(content).contains("여긴 못 지나가지롱~~"); + assertThat(content).contains("태정 태세문단세"); } } } From 3c89c83a53e574b5db44bf210ac56d33b577ce89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:14:18 +0900 Subject: [PATCH 55/89] =?UTF-8?q?[refactor]=20=EA=B4=80=EB=A0=A8=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/swagger/SwaggerResponseDescription.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 5d21b174e..223bdbef9 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -99,7 +99,7 @@ public enum SwaggerResponseDescription { ROOM_GET_MEMBER_LIST(new LinkedHashSet<>(Set.of( ROOM_NOT_FOUND ))), - ROOM_PLAYING_DETAIL(new LinkedHashSet<>(Set.of( + ROOM_PLAYING_OR_EXPIRED_DETAIL(new LinkedHashSet<>(Set.of( BOOK_NOT_FOUND, ROOM_NOT_FOUND, ROOM_ACCESS_FORBIDDEN @@ -109,7 +109,7 @@ public enum SwaggerResponseDescription { BOOK_NOT_FOUND, ROOM_ACCESS_FORBIDDEN ))), - ROOM_GET_DEADLINE_POPULAR(new LinkedHashSet<>(Set.of( + ROOM_GET_DEADLINE_POPULAR_RECENT(new LinkedHashSet<>(Set.of( CATEGORY_NOT_MATCH ))), CHANGE_ROOM_LIKE_STATE(new LinkedHashSet<>(Set.of( From 6eeed8a732d0a1809210c77987daf6e7bfd1272d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 00:17:21 +0900 Subject: [PATCH 56/89] =?UTF-8?q?[refactor]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20tearDown()=EC=97=90=EC=84=9C=20@=20Tran?= =?UTF-8?q?sactional=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=95=88?= =?UTF-8?q?=EC=93=B0=EB=8A=94=20=ED=95=A8=EC=88=98,=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EA=B4=80=EA=B3=84,import=EB=AC=B8=20=EC=82=AD=EC=A0=9C(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/comment/adapter/in/web/CommentShowAllApiTest.java | 1 - .../java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java | 1 - .../adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java | 2 -- .../thip/room/adapter/in/web/RoomGetMemberListApiTest.java | 1 - .../konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java | 1 - .../thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java | 1 - 6 files changed, 7 deletions(-) diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java index 89e52e4a6..4b7a7637a 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java @@ -13,7 +13,6 @@ 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java index 78306cd96..0abe8ac7f 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java @@ -14,7 +14,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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java index 59025daac..7cfaf062d 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java @@ -6,9 +6,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; -import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; -import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java index 5132ad08d..8de71496d 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java @@ -13,7 +13,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; 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; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java index 3bf245393..c700c04e0 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java @@ -14,7 +14,6 @@ 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.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java index 2c62ebd2e..9ea7461e1 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java @@ -12,7 +12,6 @@ import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; 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; From cf038d10cb1f529487567c638c47c08fc8b3dbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 02:45:43 +0900 Subject: [PATCH 57/89] =?UTF-8?q?[refactor]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/RoomPlayingOrExpiredDetailViewApiTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java index 86852ca31..4df3fb245 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java @@ -144,7 +144,7 @@ private void createVoteToRoom(UserJpaEntity creator, RoomJpaEntity roomJpaEntity @DisplayName("완료된 모임방 상세조회할 경우, [해당 모임방의 정보, 책 정보, 유저의 현재 활동 정보, 현재 진행중인 투표]를 반환한다.") void get_expired_room_detail() throws Exception { //given - RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),EXPIRED); + RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-지난달-활동시작", LocalDate.now().minusDays(31),EXPIRED); saveUsersToRoom(room, 4); RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()).get(0); roomParticipantJpaRepository.delete(roomParticipantJpaEntity); @@ -165,9 +165,9 @@ void get_expired_room_detail() throws Exception { //then result.andExpect(status().isOk()) .andExpect(jsonPath("$.data.isHost", is(false))) - .andExpect(jsonPath("$.data.roomName", is("과학-방-1일뒤-활동시작"))) + .andExpect(jsonPath("$.data.roomName", is("과학-방-지난달-활동시작"))) .andExpect(jsonPath("$.data.roomImageUrl", is(Category.SCIENCE_IT.getImageUrl()))) // 방 대표 이미지 추가 - .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1))))) + .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().minusDays(31))))) .andExpect(jsonPath("$.data.memberCount", is(4))) .andExpect(jsonPath("$.data.recruitCount", is(10))) .andExpect(jsonPath("$.data.isbn", is("isbn1"))) From ca8feae0be2a556f9629a214312e7c3b0835b4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 15 Sep 2025 20:03:39 +0900 Subject: [PATCH 58/89] =?UTF-8?q?[refactor]=20=EB=A6=AC=EB=B7=B0=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EC=88=98=EC=A0=95=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/exception/code/ErrorCode.java | 2 +- .../konkuk/thip/common/util/DateUtil.java | 4 +- .../RoomGetHomeJoinedListResponse.java | 6 +- .../RoomQueryPersistenceAdapter.java | 5 +- .../repository/RoomQueryRepository.java | 3 +- .../repository/RoomQueryRepositoryImpl.java | 4 +- .../mapper/RoomParticipantQueryMapper.java | 2 +- .../port/in/dto/RoomSearchMode.java | 47 ++++++++ .../application/port/out/RoomQueryPort.java | 3 +- .../RoomGetDeadlinePopularRecentService.java | 5 +- .../service/RoomSearchService.java | 103 +++++++++--------- .../RoomShowRecruitingDetailViewService.java | 2 +- 12 files changed, 121 insertions(+), 65 deletions(-) create mode 100644 src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchMode.java diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 50f51b6d7..0628d8a6d 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -99,7 +99,7 @@ public enum ErrorCode implements ResponseCode { INVALID_ROOM_SEARCH_SORT(HttpStatus.BAD_REQUEST, 100005, "방 검색 시 정렬 조건이 잘못되었습니다."), ROOM_MEMBER_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, 100006, "방의 최대 인원 수를 초과했습니다."), ROOM_MEMBER_COUNT_UNDERFLOW(HttpStatus.BAD_REQUEST, 100007, "방의 인원 수가 1 이하(방장 포함)입니다."), - ROOM_IS_EXPIRED(HttpStatus.BAD_REQUEST, 100008, "방이 만료되었습니다."), + ROOM_IS_EXPIRED(HttpStatus.BAD_REQUEST, 100008, " 완료된 모임방에서는 기존 기록에 대한 조회만 가능해요."), ROOM_POST_TYPE_NOT_MATCH(HttpStatus.BAD_REQUEST, 100009, "일치하는 방 게시물 타입 이름이 없습니다. [RECORD, VOTE] 중 하나여야 합니다."), ROOM_NOT_IN_PROGRESS(HttpStatus.BAD_REQUEST, 100010, "진행 중인 방이 아닙니다."), diff --git a/src/main/java/konkuk/thip/common/util/DateUtil.java b/src/main/java/konkuk/thip/common/util/DateUtil.java index 414b0a05b..b07f47d33 100644 --- a/src/main/java/konkuk/thip/common/util/DateUtil.java +++ b/src/main/java/konkuk/thip/common/util/DateUtil.java @@ -46,7 +46,7 @@ public static String formatAfterTime(LocalDate date) { return minutes + "분 뒤"; } - public static String RecruitingRoomFormatAfterTime(LocalDate date) { + public static String recruitingRoomFormatAfterTime(LocalDate date) { LocalDateTime now = LocalDateTime.now(); LocalDateTime dateTime = date.atStartOfDay(); Duration d = Duration.between(now, dateTime); @@ -68,7 +68,7 @@ public static String RecruitingRoomFormatAfterTime(LocalDate date) { return "마감 임박"; } - public static String RecruitingRoomFormatAfterTimeSimple(LocalDate date) { + public static String recruitingRoomFormatAfterTimeSimple(LocalDate date) { LocalDateTime now = LocalDateTime.now(); LocalDateTime dateTime = date.atStartOfDay(); Duration d = Duration.between(now, dateTime); diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java index 516ac114e..e68404d63 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java @@ -15,9 +15,11 @@ public record JoinedRoomInfo( String bookImageUrl, String roomTitle, int memberCount, - @Schema(description = "진행중인 방에서 유저의 방 진행도(ex. \"35\"), 모집중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.") + @Schema(description = "진행중인 방에서 유저의 방 진행도, 모집중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.", + example = "35") int userPercentage, - @Schema(description = "모집중인 방에서 방 모집 마감일까지 남은 시간 (ex. \"3일\"), 진행중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.") + @Schema(description = "모집중인 방에서 방 모집 마감일까지 남은 시간, 진행중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.", + example = "3일") String deadlineDate // 방 모집 마감일 (~일/시 형식) ) {} public static RoomGetHomeJoinedListResponse of(List roomList, diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java index 0847b867f..de59242cd 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Repository; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; @Repository @@ -179,8 +180,8 @@ public List findRoomsByCategoryOrderByPopular(Category category, i } @Override - public List findRoomsByCategoryOrderByRecent(Category category, int limit) { - return roomJpaRepository.findRoomsByCategoryOrderByCreatedAtDesc(category, limit); + public List findRoomsByCategoryOrderByRecent(Category category, LocalDateTime now, int limit) { + return roomJpaRepository.findRoomsByCategoryOrderByCreatedAtDesc(category, now, limit); } @Override diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java index 453d396ac..3a8893c07 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java @@ -5,6 +5,7 @@ import konkuk.thip.room.application.port.out.dto.RoomQueryDto; import konkuk.thip.room.domain.value.Category; +import java.time.LocalDateTime; import java.util.List; import java.time.LocalDate; @@ -35,7 +36,7 @@ public interface RoomQueryRepository { List findRoomsByCategoryOrderByMemberCount(Category category, int limit); - List findRoomsByCategoryOrderByCreatedAtDesc(Category category, int limit); + List findRoomsByCategoryOrderByCreatedAtDesc(Category category, LocalDateTime now, int limit); List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalDate dateCursor, Long roomIdCursor, int pageSize); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java index 2e039bf51..79b9f1287 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java @@ -363,7 +363,7 @@ public List findRoomsByCategoryOrderByMemberCount(Category categor } @Override - public List findRoomsByCategoryOrderByCreatedAtDesc(Category category, int limit) { + public List findRoomsByCategoryOrderByCreatedAtDesc(Category category, LocalDateTime now, int limit) { return queryFactory .select(new QRoomQueryDto( room.roomId, @@ -377,7 +377,7 @@ public List findRoomsByCategoryOrderByCreatedAtDesc(Category categ .from(room) .join(room.bookJpaEntity, book) .where(findDeadlinePopularRecentRoomCondition(category) - .and(room.createdAt.goe(LocalDateTime.now().minusHours(72)))) //생성된지 72시간 이내 + .and(room.createdAt.goe(now.minusHours(72)))) .orderBy(room.createdAt.desc(), room.roomId.desc()) .limit(limit) .fetch(); diff --git a/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java b/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java index 035059f37..7a9421d44 100644 --- a/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java +++ b/src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java @@ -27,7 +27,7 @@ default RoomGetHomeJoinedListResponse.JoinedRoomInfo toJoinedRoomInfo(RoomPartic if (IN_PROGRESS.equals(dto.roomStatus())) { userPercentage = dto.userPercentage().intValue(); } else if (RECRUITING.equals(dto.roomStatus())) { - deadlineDate = DateUtil.RecruitingRoomFormatAfterTimeSimple(dto.startDate()); + deadlineDate = DateUtil.recruitingRoomFormatAfterTimeSimple(dto.startDate()); } return new RoomGetHomeJoinedListResponse.JoinedRoomInfo( diff --git a/src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchMode.java b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchMode.java new file mode 100644 index 000000000..e66c5c9dd --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchMode.java @@ -0,0 +1,47 @@ +package konkuk.thip.room.application.port.in.dto; + +import konkuk.thip.room.domain.value.Category; + +public enum RoomSearchMode{ + GLOBAL_BY_KEYWORD_OR_ALL, // 카테고리 없음: 전체 또는 키워드 기반 + CATEGORY_ALL, // 카테고리 있음 + 전체조회 (키워드 비어있고 isAllCategory=true) + CATEGORY_BY_KEYWORD; // 카테고리 있음 + 키워드 기반(또는 전체가 아님) + + public static RoomSearchMode determineSearchMode(Category category, boolean isAllCategory, String keyword) { + final boolean isKeywordEmpty = isEmpty(keyword); + + if (category == null) { + // 카테고리 없음 → 전체 또는 키워드 기반 + // 유효성 검증에서 이미 조합을 보장하므로 그대로 처리 + return RoomSearchMode.GLOBAL_BY_KEYWORD_OR_ALL; + } + + // 카테고리 있음 + if (isAllCategory && isKeywordEmpty) { + return RoomSearchMode.CATEGORY_ALL; + } + return RoomSearchMode.CATEGORY_BY_KEYWORD; + } + + public static String resolveEffectiveKeyword(RoomSearchMode mode, String keyword) { + final boolean isKeywordEmpty = isEmpty(keyword); + return switch (mode) { + case GLOBAL_BY_KEYWORD_OR_ALL -> + // 전체(isAllCategory=true)이며 키워드 비어있을 수 있으므로 빈 문자열 허용 + isKeywordEmpty ? "" : keyword.trim(); + case CATEGORY_ALL -> + // 카테고리 전체 조회: 키워드 강제 빈 문자열 + ""; + case CATEGORY_BY_KEYWORD -> + // 키워드가 비어있을 수도 있지만, 의미적으로 "전체가 아님"이므로 + // 저장 계층에서 빈 문자열이면 전체와 동일해질 수 있음 → 그대로 전달(정책 유지) + isKeywordEmpty ? "" : keyword.trim(); + }; + } + + public static boolean isEmpty(String s) { + return s == null || s.trim().isEmpty(); + } +} + + diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java index c8dcf7b6f..c3c347f71 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java @@ -8,6 +8,7 @@ import konkuk.thip.room.domain.Room; import konkuk.thip.room.domain.value.Category; +import java.time.LocalDateTime; import java.util.List; public interface RoomQueryPort { @@ -40,7 +41,7 @@ public interface RoomQueryPort { List findRoomsByCategoryOrderByPopular(Category category, int limit); - List findRoomsByCategoryOrderByRecent(Category category, int limit); + List findRoomsByCategoryOrderByRecent(Category category, LocalDateTime now, int limit); /** * 임시 메서드 * TODO 리펙토링 대상 diff --git a/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java b/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java index 752bac44e..2e893d5bd 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java @@ -9,6 +9,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; + @Service @RequiredArgsConstructor public class RoomGetDeadlinePopularRecentService implements RoomGetDeadlinePopularRecentUseCase { @@ -22,13 +24,14 @@ public class RoomGetDeadlinePopularRecentService implements RoomGetDeadlinePopul @Transactional(readOnly = true) public RoomGetDeadlinePopularRecentResponse getDeadlineAndPopularAndRecentRoomList(String categoryStr) { Category category = Category.from(categoryStr); + LocalDateTime now = LocalDateTime.now(); var deadlineRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( roomQueryPort.findRoomsByCategoryOrderByDeadline(category, DEFAULT_LIMIT)); var popularRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( roomQueryPort.findRoomsByCategoryOrderByPopular(category, DEFAULT_LIMIT)); var recentRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( - roomQueryPort.findRoomsByCategoryOrderByRecent(category, DEFAULT_LIMIT)); + roomQueryPort.findRoomsByCategoryOrderByRecent(category, now, DEFAULT_LIMIT)); return RoomGetDeadlinePopularRecentResponse.of(deadlineRoomList, popularRoomList,recentRoomList); } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java index 0fb7e22d1..27bfa01ac 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java @@ -7,6 +7,7 @@ import konkuk.thip.recentSearch.application.service.manager.RecentSearchCreateManager; import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; import konkuk.thip.room.application.mapper.RoomQueryMapper; +import konkuk.thip.room.application.port.in.dto.RoomSearchMode; import konkuk.thip.room.application.port.in.dto.RoomSearchSortParam; import konkuk.thip.room.application.port.in.RoomSearchUseCase; import konkuk.thip.room.application.port.in.dto.RoomSearchQuery; @@ -30,23 +31,29 @@ public class RoomSearchService implements RoomSearchUseCase { private final RoomQueryMapper roomQueryMapper; @Override - @Transactional // <- 최근 검색 저장으로 인한 트랜잭션 + @Transactional public RoomSearchResponse searchRecruitingRooms(RoomSearchQuery query) { - // 1. validation + // 1) 파라미터 파싱/검증 RoomSearchSortParam sortParam = RoomSearchSortParam.from(query.sortStr()); - validateSearchParams(query.keyword(),query.isAllCategory(),query.categoryStr()); + validateSearchParams(query.keyword(), query.isAllCategory(), query.categoryStr()); Category category = validateCategory(query.categoryStr()); - // 2. Cursor 생성 + // 2) 검색 모드/키워드 결정 + RoomSearchMode mode = RoomSearchMode.determineSearchMode(category, query.isAllCategory(), query.keyword()); + String effectiveKeyword = RoomSearchMode.resolveEffectiveKeyword(mode, query.keyword()); + + // 3) 커서 생성 Cursor cursor = Cursor.from(query.cursorStr(), DEFAULT_PAGE_SIZE); - // 3. 방 검색 - CursorBasedList result = executeRecruitingRoomSearch(query, category, sortParam, cursor); + // 4) 실행 (정렬 기준별 단일 switch) + CursorBasedList result = executeSearchMode(mode, sortParam, effectiveKeyword, category, cursor); - // 4. 검색 완료일 경우, 최근 검색어 저장 - recentSearchCreateManager.saveRecentSearchByUser(query.userId(), query.keyword(), RecentSearchType.ROOM_SEARCH, query.isFinalized()); + // 5) 최근 검색어 저장 + recentSearchCreateManager.saveRecentSearchByUser( + query.userId(), query.keyword(), RecentSearchType.ROOM_SEARCH, query.isFinalized() + ); - // 5. response 구성 + // 6) 응답 매핑 return new RoomSearchResponse( roomQueryMapper.toRoomSearchResponse(result.contents()), result.nextCursor(), @@ -54,49 +61,43 @@ public RoomSearchResponse searchRecruitingRooms(RoomSearchQuery query) { ); } - private CursorBasedList executeRecruitingRoomSearch(RoomSearchQuery query, Category category, RoomSearchSortParam sortParam, Cursor cursor) { - boolean isAllCategory = query.isAllCategory(); - String keyword = query.keyword(); - boolean isKeywordEmpty = (keyword == null || keyword.trim().isEmpty()); + private CursorBasedList executeSearchMode( + RoomSearchMode mode, + RoomSearchSortParam sort, + String keyword, + Category category, + Cursor cursor + ) { + return switch (sort) { + case DEADLINE -> executeByDeadline(mode, keyword, category, cursor); + case MEMBER_COUNT -> executeByMemberCount(mode, keyword, category, cursor); + default -> throw new BusinessException( + API_INVALID_PARAM, + new IllegalArgumentException("지원하지 않는 정렬 기준입니다: " + sort) + ); + }; + } - // 빈 키워드이면서 isAllCategory가 true인 경우는 전체 조회를 위해 빈 문자열을 사용하고, 그렇지 않으면 그대로 keyword를 사용 - String effectiveKeyword = isKeywordEmpty && isAllCategory ? "" : keyword; - - if (category == null) { - // 전체 카테고리 중에서 - // 1) 전체검색(isAllCategory=true)이거나 - // 2) 키워드가 비어있지 않은 경우 - // 해당 조건 모두 포함해서 키워드 기반 검색 또는 전체 검색 수행 - if (isAllCategory || !isKeywordEmpty) { - switch (sortParam) { - case DEADLINE: - return roomQueryPort.searchRecruitingRoomsByDeadline(effectiveKeyword, cursor); - case MEMBER_COUNT: - return roomQueryPort.searchRecruitingRoomsByMemberCount(effectiveKeyword, cursor); - } - } - } else { - if (isAllCategory && isKeywordEmpty) { - // isAllCategory가 true이고, 키워드가 비어있으면 - // 특정 카테고리 내에서 '전체 조회'를 의미함 즉, 키워드 없이 카테고리 필터만 적용해서 전체 방 조회 - switch (sortParam) { - case DEADLINE: - return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor); - case MEMBER_COUNT: - return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor); - } - } else if (!isAllCategory) { - // isAllCategory가 false인 경우 (전체검색 아님) - // category가 존재하고 키워드는 있거나 빈 문자열이어도 키워드 기반 조회 수행 - switch (sortParam) { - case DEADLINE: - return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(effectiveKeyword, category, cursor); - case MEMBER_COUNT: - return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(effectiveKeyword, category, cursor); - } - } - } - return null; + private CursorBasedList executeByDeadline( + RoomSearchMode mode, String keyword, Category category, Cursor cursor + ) { + return switch (mode) { + case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByDeadline(keyword, cursor); + case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor); + case CATEGORY_BY_KEYWORD -> + roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(keyword, category, cursor); + }; + } + + private CursorBasedList executeByMemberCount( + RoomSearchMode mode, String keyword, Category category, Cursor cursor + ) { + return switch (mode) { + case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByMemberCount(keyword, cursor); + case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor); + case CATEGORY_BY_KEYWORD -> + roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(keyword, category, cursor); + }; } private Category validateCategory(String categoryStr) { diff --git a/src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java b/src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java index 5600c619b..2897ef2af 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java @@ -64,7 +64,7 @@ private RoomRecruitingDetailViewResponse buildResponse( .isPublic(room.isPublic()) .progressStartDate(DateUtil.formatDate(room.getStartDate())) .progressEndDate(DateUtil.formatDate(room.getEndDate())) - .recruitEndDate(DateUtil.RecruitingRoomFormatAfterTime(room.getStartDate())) + .recruitEndDate(DateUtil.recruitingRoomFormatAfterTime(room.getStartDate())) .category(room.getCategory().getValue()) .categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory())) // TODO : 리펙토링 대상 .roomDescription(room.getDescription()) From 4a376c831d2faec488e48aba13a6c0605939f553 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 16 Sep 2025 16:38:45 +0900 Subject: [PATCH 59/89] =?UTF-8?q?[chore]=20notifications=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?with=20flyway=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 카테고리 컬럼 추가 --- .../adapter/out/jpa/NotificationJpaEntity.java | 7 ++++++- .../V250916__Add_notification_category.sql | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V250916__Add_notification_category.sql diff --git a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java index b28d749e5..3e8a1f549 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; +import konkuk.thip.notification.domain.value.NotificationCategory; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; @@ -23,9 +24,13 @@ public class NotificationJpaEntity extends BaseJpaEntity { @Column(length = 200, nullable = false) private String content; - @Column(name = "is_checked",nullable = false) + @Column(name = "is_checked", nullable = false) private boolean isChecked; + @Enumerated(EnumType.STRING) + @Column(name = "notification_category", length = 16, nullable = false) + private NotificationCategory notificationCategory; + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; diff --git a/src/main/resources/db/migration/V250916__Add_notification_category.sql b/src/main/resources/db/migration/V250916__Add_notification_category.sql new file mode 100644 index 000000000..f96e0b8b2 --- /dev/null +++ b/src/main/resources/db/migration/V250916__Add_notification_category.sql @@ -0,0 +1,14 @@ +-- 1단계: NULL 허용으로 notification_category 컬럼 추가 +ALTER TABLE notifications + ADD COLUMN notification_category VARCHAR(16) + COMMENT '알림 카테고리: FEED or ROOM'; + +-- 2단계: 기존 데이터 update +UPDATE notifications +SET notification_category = 'FEED' +WHERE notification_category IS NULL; + +-- 3단계: NOT NULL 제약 추가 +ALTER TABLE notifications + MODIFY COLUMN notification_category VARCHAR(16) NOT NULL + COMMENT '알림 카테고리: FEED/ROOM'; \ No newline at end of file From 8b8085e3357c718bd6f49b1043d1b83bdaf38538 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 16 Sep 2025 16:56:06 +0900 Subject: [PATCH 60/89] =?UTF-8?q?[refactor]=20notification=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C,=20Template=20=EC=9C=BC=EB=A1=9C=EB=B6=80?= =?UTF-8?q?=ED=84=B0=20notification=20category=20=EA=B0=92=20=EB=B0=9B?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/NotificationSyncExecutor.java | 8 +++++--- .../service/template/NotificationTemplate.java | 4 ++++ .../konkuk/thip/notification/domain/Notification.java | 6 +++++- 3 files changed, 14 insertions(+), 4 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 bff1959b9..e5bf17417 100644 --- a/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java @@ -4,6 +4,7 @@ import konkuk.thip.notification.application.port.out.NotificationCommandPort; import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.domain.Notification; +import konkuk.thip.notification.domain.value.NotificationCategory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,9 +23,10 @@ public void execute( ) { String title = template.title(args); String content = template.content(args); + NotificationCategory notificationCategory = template.notificationCategory(args); // 1. DB 저장 - saveNotification(title, content, targetUserId); + saveNotification(title, content, notificationCategory, targetUserId); // 2. 이벤트 퍼블리시 try { @@ -37,8 +39,8 @@ public void execute( } } - private void saveNotification(String title, String content, Long targetUserId) { - Notification notification = Notification.withoutId(title, content, targetUserId); + private void saveNotification(String title, String content, NotificationCategory category, Long targetUserId) { + Notification notification = Notification.withoutId(title, content, category, targetUserId); notificationCommandPort.save(notification); } } 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 index 8d8e22f95..30165b1a9 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java @@ -1,8 +1,12 @@ package konkuk.thip.notification.application.service.template; +import konkuk.thip.notification.domain.value.NotificationCategory; + public interface NotificationTemplate { String title(T args); String content(T args); + + NotificationCategory notificationCategory(T args); } diff --git a/src/main/java/konkuk/thip/notification/domain/Notification.java b/src/main/java/konkuk/thip/notification/domain/Notification.java index 6c35bd815..412342eaf 100644 --- a/src/main/java/konkuk/thip/notification/domain/Notification.java +++ b/src/main/java/konkuk/thip/notification/domain/Notification.java @@ -1,6 +1,7 @@ package konkuk.thip.notification.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.notification.domain.value.NotificationCategory; import lombok.Getter; import lombok.experimental.SuperBuilder; @@ -16,13 +17,16 @@ public class Notification extends BaseDomainEntity { private boolean isChecked; + private NotificationCategory notificationCategory; + private Long targetUserId; - public static Notification withoutId (String title, String content, Long targetUserId) { + public static Notification withoutId (String title, String content, NotificationCategory notificationCategory, Long targetUserId) { return Notification.builder() .title(title) .content(content) .isChecked(false) + .notificationCategory(notificationCategory) .targetUserId(targetUserId) .build(); } From 474a23a617e5608a6a8697433176cfcbcbfdf09a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 16 Sep 2025 16:57:00 +0900 Subject: [PATCH 61/89] =?UTF-8?q?[refactor]=20NotificationTemplate=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 적절한 notification category 값을 직접 주입하도록 수정 --- .../service/template/feed/FeedCommentLikedTemplate.java | 5 +++++ .../service/template/feed/FeedCommentedTemplate.java | 5 +++++ .../application/service/template/feed/FeedLikedTemplate.java | 5 +++++ .../service/template/feed/FeedRepliedTemplate.java | 5 +++++ .../application/service/template/feed/FollowedTemplate.java | 5 +++++ .../service/template/feed/FolloweeNewPostTemplate.java | 5 +++++ .../service/template/room/RoomActivityStartedTemplate.java | 5 +++++ .../service/template/room/RoomCommentLikedTemplate.java | 5 +++++ .../service/template/room/RoomJoinToHostTemplate.java | 5 +++++ .../template/room/RoomPostCommentRepliedTemplate.java | 5 +++++ .../service/template/room/RoomPostCommentedTemplate.java | 5 +++++ .../service/template/room/RoomPostLikedTemplate.java | 5 +++++ .../service/template/room/RoomRecordCreatedTemplate.java | 5 +++++ .../template/room/RoomRecruitClosedEarlyTemplate.java | 5 +++++ .../service/template/room/RoomVoteStartedTemplate.java | 5 +++++ 15 files changed, 75 insertions(+) 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 index 4c2358115..a28825ff2 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + 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 index e58391abe..f3cf02dfd 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 글에 댓글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + 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 index 15b608c3a..bb52e6a68 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 글에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + 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 index 37a8de487..5a6c8c143 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 답글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + 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 index f9e8003f4..1fcdb2b8d 100644 --- 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 @@ -18,5 +18,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 나를 띱했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + 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 index af18247fa..61c4130d1 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 새로운 글을 작성했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + public record Args(String actorUsername) {} } 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 index 4ea611682..6d6cdb605 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index 09e09c86c..252b3795d 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index 5ab0096b1..049e33eeb 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 모임에 참여했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index 860c30c9c..4870d791b 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 답글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index c10510b20..53e475cd9 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 독서기록에 댓글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index 37f3133b7..3eebe7a05 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 독서기록에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index a1180550e..dd32368f8 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 새로운 독서 기록을 작성했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index 600f3c778..85951e30a 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + 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 index 2384361eb..837b3979f 100644 --- 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 @@ -16,5 +16,10 @@ public String content(Args args) { return "새로운 투표가 시작되었어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String roomTitle) {} } From 851d29f0b50e4c6b81afcf197b50aee1f94d3528 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 16 Sep 2025 16:57:32 +0900 Subject: [PATCH 62/89] =?UTF-8?q?[refactor]=20NotificationMapper=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jpa <-> domain 엔티티의 매핑과정에서 notification category 필드 값의 매핑 추가 --- .../notification/adapter/out/mapper/NotificationMapper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java b/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java index aff2462d0..dd65e5db3 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java @@ -13,6 +13,7 @@ public NotificationJpaEntity toJpaEntity(Notification notification, UserJpaEntit .title(notification.getTitle()) .content(notification.getContent()) .isChecked(notification.isChecked()) + .notificationCategory(notification.getNotificationCategory()) .userJpaEntity(userJpaEntity) .build(); } @@ -23,6 +24,7 @@ public Notification toDomainEntity(NotificationJpaEntity notificationJpaEntity) .title(notificationJpaEntity.getTitle()) .content(notificationJpaEntity.getContent()) .isChecked(notificationJpaEntity.isChecked()) + .notificationCategory(notificationJpaEntity.getNotificationCategory()) .targetUserId(notificationJpaEntity.getUserJpaEntity().getUserId()) .createdAt(notificationJpaEntity.getCreatedAt()) .modifiedAt(notificationJpaEntity.getModifiedAt()) From 22541904b0cd3ea050ac5cccae1dda768ae78901 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 16 Sep 2025 16:57:49 +0900 Subject: [PATCH 63/89] =?UTF-8?q?[refactor]=20=EA=B8=B0=EC=A1=B4=20notific?= =?UTF-8?q?ation=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/NotificationSyncExecutorTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java index d988c278d..8db11367a 100644 --- a/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java @@ -3,6 +3,7 @@ import konkuk.thip.notification.application.port.out.NotificationCommandPort; import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.domain.Notification; +import konkuk.thip.notification.domain.value.NotificationCategory; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -27,6 +28,8 @@ void execute_publish_failure_does_not_throw() { public String title(String args) { return "테스트제목"; } @Override public String content(String args) { return "테스트내용"; } + @Override + public NotificationCategory notificationCategory(String args) { return NotificationCategory.FEED; } }; // publish 호출 시 강제로 예외를 던지는 invoker From c987296e7e07712ca6e223eff8e865dae3be913f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 16 Sep 2025 21:56:04 +0900 Subject: [PATCH 64/89] =?UTF-8?q?[refactor]=20=EB=A6=AC=EB=B7=B0=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EC=88=98=EC=A0=95=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/RoomQueryPersistenceAdapter.java | 4 ++-- .../out/persistence/repository/RoomQueryRepository.java | 2 +- .../persistence/repository/RoomQueryRepositoryImpl.java | 8 +++++--- .../thip/room/application/port/out/RoomQueryPort.java | 2 +- .../service/RoomGetDeadlinePopularRecentService.java | 4 +++- .../thip/room/application/service/RoomSearchService.java | 8 ++------ 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java index de59242cd..b706a517b 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java @@ -180,8 +180,8 @@ public List findRoomsByCategoryOrderByPopular(Category category, i } @Override - public List findRoomsByCategoryOrderByRecent(Category category, LocalDateTime now, int limit) { - return roomJpaRepository.findRoomsByCategoryOrderByCreatedAtDesc(category, now, limit); + public List findRoomsByCategoryOrderByRecent(Category category, LocalDateTime createdAfter, int limit) { + return roomJpaRepository.findRoomsByCategoryOrderByCreatedAtDesc(category, createdAfter, limit); } @Override diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java index 3a8893c07..cd93198e8 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java @@ -36,7 +36,7 @@ public interface RoomQueryRepository { List findRoomsByCategoryOrderByMemberCount(Category category, int limit); - List findRoomsByCategoryOrderByCreatedAtDesc(Category category, LocalDateTime now, int limit); + List findRoomsByCategoryOrderByCreatedAtDesc(Category category, LocalDateTime createdAfter, int limit); List findRoomsByIsbnOrderByStartDateAsc(String isbn, LocalDate dateCursor, Long roomIdCursor, int pageSize); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java index 79b9f1287..3ae7fe5d3 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java @@ -363,7 +363,7 @@ public List findRoomsByCategoryOrderByMemberCount(Category categor } @Override - public List findRoomsByCategoryOrderByCreatedAtDesc(Category category, LocalDateTime now, int limit) { + public List findRoomsByCategoryOrderByCreatedAtDesc(Category category, LocalDateTime createdAfter, int limit) { return queryFactory .select(new QRoomQueryDto( room.roomId, @@ -376,8 +376,10 @@ public List findRoomsByCategoryOrderByCreatedAtDesc(Category categ )) .from(room) .join(room.bookJpaEntity, book) - .where(findDeadlinePopularRecentRoomCondition(category) - .and(room.createdAt.goe(now.minusHours(72)))) + .where( + findDeadlinePopularRecentRoomCondition(category) + .and(room.createdAt.goe(createdAfter)) + ) .orderBy(room.createdAt.desc(), room.roomId.desc()) .limit(limit) .fetch(); diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java index c3c347f71..d0961035f 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java @@ -41,7 +41,7 @@ public interface RoomQueryPort { List findRoomsByCategoryOrderByPopular(Category category, int limit); - List findRoomsByCategoryOrderByRecent(Category category, LocalDateTime now, int limit); + List findRoomsByCategoryOrderByRecent(Category category, LocalDateTime createdAfter, int limit); /** * 임시 메서드 * TODO 리펙토링 대상 diff --git a/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java b/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java index 2e893d5bd..2b7be4ae2 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java @@ -19,19 +19,21 @@ public class RoomGetDeadlinePopularRecentService implements RoomGetDeadlinePopul private final RoomQueryMapper roomQueryMapper; private static final int DEFAULT_LIMIT = 4; + public static final long RECENT_HOURS = 72; @Override @Transactional(readOnly = true) public RoomGetDeadlinePopularRecentResponse getDeadlineAndPopularAndRecentRoomList(String categoryStr) { Category category = Category.from(categoryStr); LocalDateTime now = LocalDateTime.now(); + LocalDateTime recentCutoff = now.minusHours(RECENT_HOURS); var deadlineRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( roomQueryPort.findRoomsByCategoryOrderByDeadline(category, DEFAULT_LIMIT)); var popularRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( roomQueryPort.findRoomsByCategoryOrderByPopular(category, DEFAULT_LIMIT)); var recentRoomList = roomQueryMapper.toDeadlinePopularRecentRoomDtoList( - roomQueryPort.findRoomsByCategoryOrderByRecent(category, now, DEFAULT_LIMIT)); + roomQueryPort.findRoomsByCategoryOrderByRecent(category, recentCutoff, DEFAULT_LIMIT)); return RoomGetDeadlinePopularRecentResponse.of(deadlineRoomList, popularRoomList,recentRoomList); } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java index 27bfa01ac..268251e62 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java @@ -83,9 +83,7 @@ private CursorBasedList executeByDeadline( ) { return switch (mode) { case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByDeadline(keyword, cursor); - case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor); - case CATEGORY_BY_KEYWORD -> - roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(keyword, category, cursor); + case CATEGORY_ALL, CATEGORY_BY_KEYWORD -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(keyword, category, cursor); }; } @@ -94,9 +92,7 @@ private CursorBasedList executeByMemberCount( ) { return switch (mode) { case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByMemberCount(keyword, cursor); - case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor); - case CATEGORY_BY_KEYWORD -> - roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(keyword, category, cursor); + case CATEGORY_ALL, CATEGORY_BY_KEYWORD -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(keyword, category, cursor); }; } From 0597611942e3daca82580ac8be6fb427b286474e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 16 Sep 2025 22:12:36 +0900 Subject: [PATCH 65/89] =?UTF-8?q?[refactor]=20=EA=B3=B5=EB=B0=B1=EC=9D=BC?= =?UTF-8?q?=20=EA=B2=BD=EC=9A=B0(=EC=A0=84=EC=B2=B4=EA=B2=80=EC=83=89)=20?= =?UTF-8?q?=EC=B5=9C=EA=B7=BC=EA=B2=80=EC=83=89=EC=96=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20x=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/RecentSearchCreateManager.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java b/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java index 111e3db1a..963d38632 100644 --- a/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java +++ b/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java @@ -15,15 +15,15 @@ public class RecentSearchCreateManager { private final RecentSearchQueryPort recentSearchQueryPort; public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) { - // 검색완료일 경우에 최근검색어 추가 - if (isFinalized) { - // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 - recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId, type) - .ifPresentOrElse( - recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신 - () -> recentSearchCommandPort.save(RecentSearch.withoutId(keyword, type, userId)) // 없으면 새로 저장 - ); - } + if (!isFinalized) return; // 검색완료일 경우에 최근검색어 추가 + if (keyword == null || keyword.trim().isEmpty()) return; + String normalized = keyword.trim(); + // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 + recentSearchQueryPort.findRecentSearchByKeywordAndUserId(normalized, userId, type) + .ifPresentOrElse( + recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신 + () -> recentSearchCommandPort.save(RecentSearch.withoutId(normalized, type, userId)) // 없으면 새로 저장 + ); } } From ebc410deb239bc2bdc93b16fada7e4c2a47b13c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 16 Sep 2025 23:06:23 +0900 Subject: [PATCH 66/89] =?UTF-8?q?[refactor]=20=EB=AA=A8=EC=9E=84=ED=99=88?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20response=20=EC=8A=A4?= =?UTF-8?q?=EC=9B=A8=EA=B1=B0=EA=B0=92=20=EB=AA=85=EC=8B=9C=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/response/RoomGetHomeJoinedListResponse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java index e68404d63..b76482e7e 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java @@ -15,10 +15,10 @@ public record JoinedRoomInfo( String bookImageUrl, String roomTitle, int memberCount, - @Schema(description = "진행중인 방에서 유저의 방 진행도, 모집중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.", + @Schema(description = "[진행중인 방]에서 유저의 방 진행도 --> 모집중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.", example = "35") int userPercentage, - @Schema(description = "모집중인 방에서 방 모집 마감일까지 남은 시간, 진행중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.", + @Schema(description = "[모집중인 방]에서 방 모집 마감일까지 남은 시간 --> 진행중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.", example = "3일") String deadlineDate // 방 모집 마감일 (~일/시 형식) ) {} From f616f0914dc6858021b29c31adefe07611663830 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:06:02 +0900 Subject: [PATCH 67/89] =?UTF-8?q?[move]=20notification=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=82=B4=EC=9D=98=20fcm=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EA=B4=80=EB=A0=A8=20java=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=93=A4=20=ED=8C=A8=ED=82=A4=EC=A7=95=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/NotificationCommandController.java | 6 +++--- .../application/port/in/{ => fcm}/FcmDeleteUseCase.java | 2 +- .../port/in/{ => fcm}/FcmEnableStateChangeUseCase.java | 2 +- .../application/port/in/{ => fcm}/FcmRegisterUseCase.java | 2 +- .../application/service/{ => fcm}/FcmDeleteService.java | 4 ++-- .../service/{ => fcm}/FcmEnableStateChangeService.java | 4 ++-- .../application/service/{ => fcm}/FcmRegisterService.java | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) rename src/main/java/konkuk/thip/notification/application/port/in/{ => fcm}/FcmDeleteUseCase.java (74%) rename src/main/java/konkuk/thip/notification/application/port/in/{ => fcm}/FcmEnableStateChangeUseCase.java (77%) rename src/main/java/konkuk/thip/notification/application/port/in/{ => fcm}/FcmRegisterUseCase.java (75%) rename src/main/java/konkuk/thip/notification/application/service/{ => fcm}/FcmDeleteService.java (86%) rename src/main/java/konkuk/thip/notification/application/service/{ => fcm}/FcmEnableStateChangeService.java (86%) rename src/main/java/konkuk/thip/notification/application/service/{ => fcm}/FcmRegisterService.java (92%) diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java index b6d3772f5..f9f572237 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java @@ -11,9 +11,9 @@ import konkuk.thip.notification.adapter.in.web.request.FcmTokenEnableStateChangeRequest; import konkuk.thip.notification.adapter.in.web.request.FcmTokenRegisterRequest; import konkuk.thip.notification.adapter.in.web.response.FcmTokenEnableStateChangeResponse; -import konkuk.thip.notification.application.port.in.FcmDeleteUseCase; -import konkuk.thip.notification.application.port.in.FcmEnableStateChangeUseCase; -import konkuk.thip.notification.application.port.in.FcmRegisterUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmDeleteUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmEnableStateChangeUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmRegisterUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmDeleteUseCase.java similarity index 74% rename from src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java rename to src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmDeleteUseCase.java index b6335003b..10f3e7e3b 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmDeleteUseCase.java @@ -1,4 +1,4 @@ -package konkuk.thip.notification.application.port.in; +package konkuk.thip.notification.application.port.in.fcm; import konkuk.thip.notification.application.port.in.dto.FcmTokenDeleteCommand; diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmEnableStateChangeUseCase.java similarity index 77% rename from src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java rename to src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmEnableStateChangeUseCase.java index 14ba4c395..ed19dee18 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmEnableStateChangeUseCase.java @@ -1,4 +1,4 @@ -package konkuk.thip.notification.application.port.in; +package konkuk.thip.notification.application.port.in.fcm; import konkuk.thip.notification.application.port.in.dto.FcmEnableStateChangeCommand; diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmRegisterUseCase.java similarity index 75% rename from src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java rename to src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmRegisterUseCase.java index ff9b096ed..870a4b529 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmRegisterUseCase.java @@ -1,4 +1,4 @@ -package konkuk.thip.notification.application.port.in; +package konkuk.thip.notification.application.port.in.fcm; import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmDeleteService.java similarity index 86% rename from src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java rename to src/main/java/konkuk/thip/notification/application/service/fcm/FcmDeleteService.java index b08f5ab45..b1de324ee 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java +++ b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmDeleteService.java @@ -1,6 +1,6 @@ -package konkuk.thip.notification.application.service; +package konkuk.thip.notification.application.service.fcm; -import konkuk.thip.notification.application.port.in.FcmDeleteUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmDeleteUseCase; import konkuk.thip.notification.application.port.in.dto.FcmTokenDeleteCommand; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmEnableStateChangeService.java similarity index 86% rename from src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java rename to src/main/java/konkuk/thip/notification/application/service/fcm/FcmEnableStateChangeService.java index abae5af41..710a6f4a7 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java +++ b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmEnableStateChangeService.java @@ -1,6 +1,6 @@ -package konkuk.thip.notification.application.service; +package konkuk.thip.notification.application.service.fcm; -import konkuk.thip.notification.application.port.in.FcmEnableStateChangeUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmEnableStateChangeUseCase; import konkuk.thip.notification.application.port.in.dto.FcmEnableStateChangeCommand; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmRegisterService.java similarity index 92% rename from src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java rename to src/main/java/konkuk/thip/notification/application/service/fcm/FcmRegisterService.java index 71f8835d2..48b56ba0d 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java +++ b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmRegisterService.java @@ -1,6 +1,6 @@ -package konkuk.thip.notification.application.service; +package konkuk.thip.notification.application.service.fcm; -import konkuk.thip.notification.application.port.in.FcmRegisterUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmRegisterUseCase; import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; From 16e57444e61e1deb2c8f8b97647f56b4dbe639f8 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:09:37 +0900 Subject: [PATCH 68/89] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20api=20controller=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerResponseDescription.java | 3 +++ .../in/web/NotificationQueryController.java | 21 +++++++++++++++++++ .../response/NotificationShowResponse.java | 18 ++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationShowResponse.java diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 5d21b174e..0f7de8076 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -373,6 +373,9 @@ public enum SwaggerResponseDescription { NOTIFICATION_GET_ENABLE_STATE(new LinkedHashSet<>(Set.of( FCM_TOKEN_NOT_FOUND ))), + NOTIFICATION_SHOW(new LinkedHashSet<>(Set.of( + INVALID_NOTIFICATION_TYPE + ))), ; private final Set errorCodeList; diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java index b6afd1e39..8b172c438 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java @@ -7,13 +7,17 @@ import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.notification.adapter.in.web.response.NotificationShowEnableStateResponse; +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; import konkuk.thip.notification.application.port.in.NotificationShowEnableStateUseCase; +import konkuk.thip.notification.application.port.in.NotificationShowUseCase; +import konkuk.thip.notification.application.port.in.dto.NotificationType; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import static konkuk.thip.common.swagger.SwaggerResponseDescription.NOTIFICATION_GET_ENABLE_STATE; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.NOTIFICATION_SHOW; @Tag(name = "Notification Query API", description = "알림 조회 관련 API") @RestController @@ -21,6 +25,7 @@ public class NotificationQueryController { private final NotificationShowEnableStateUseCase notificationShowEnableStateUseCase; + private final NotificationShowUseCase notificationShowUseCase; @Operation( summary = "사용자 푸시알림 수신여부 조회 (마이페이지 -> 알림설정)", @@ -35,4 +40,20 @@ public BaseResponse showNotificationEnableS return BaseResponse.ok( NotificationShowEnableStateResponse.of(notificationShowEnableStateUseCase.getNotificationShowEnableState(userId,deviceId))); } + + @Operation( + summary = "유저의 알림 조회", + description = "유저의 알림 목록을 조회합니다. 최신순으로 정렬합니다." + ) + @ExceptionDescription(NOTIFICATION_SHOW) + @GetMapping("/notifications") + public BaseResponse showNotifications( + @UserId final Long userId, + @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") + @RequestParam(value = "cursor", required = false) final String cursor, + @Parameter(description = "알림 타입. 해당 파라미터 값이 null인 경우에는 알림 타입을 구분하지 않고 조회합니다.", example = "feed or room") + @RequestParam(value = "type", required = false, defaultValue = "feedAndRoom") final String type + ) { + return BaseResponse.ok(notificationShowUseCase.showNotifications(userId, cursor, NotificationType.from(type))); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationShowResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationShowResponse.java new file mode 100644 index 000000000..d810e8dd1 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationShowResponse.java @@ -0,0 +1,18 @@ +package konkuk.thip.notification.adapter.in.web.response; + +import java.util.List; + +public record NotificationShowResponse( + List notifications, + String nextCursor, + boolean isLast +) { + public record NotificationOfUser( + Long notificationId, + String title, + String content, + boolean isChecked, + String notificationType, + String postDate + ) {} +} From dcf9d6fb3a32445a6b749b4f31f306b3ec761dfc Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:11:14 +0900 Subject: [PATCH 69/89] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20api=20use=20case=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/NotificationShowUseCase.java | 9 ++++ .../port/in/dto/NotificationType.java | 28 +++++++++++++ .../service/NotificationShowService.java | 42 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/port/in/NotificationShowUseCase.java create mode 100644 src/main/java/konkuk/thip/notification/application/port/in/dto/NotificationType.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/NotificationShowService.java diff --git a/src/main/java/konkuk/thip/notification/application/port/in/NotificationShowUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/NotificationShowUseCase.java new file mode 100644 index 000000000..4841480c3 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/NotificationShowUseCase.java @@ -0,0 +1,9 @@ +package konkuk.thip.notification.application.port.in; + +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; +import konkuk.thip.notification.application.port.in.dto.NotificationType; + +public interface NotificationShowUseCase { + + NotificationShowResponse showNotifications(Long userId, String cursorStr, NotificationType notificationType); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/NotificationType.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/NotificationType.java new file mode 100644 index 000000000..f825b16e6 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/dto/NotificationType.java @@ -0,0 +1,28 @@ +package konkuk.thip.notification.application.port.in.dto; + +import konkuk.thip.common.exception.InvalidStateException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static konkuk.thip.common.exception.code.ErrorCode.INVALID_NOTIFICATION_TYPE; + +@Getter +@RequiredArgsConstructor +public enum NotificationType { + FEED("feed"), + ROOM("room"), + FEED_AND_ROOM("feedAndRoom"); + + private final String type; + + public static NotificationType from(String type) { + return Arrays.stream(NotificationType.values()) + .filter(param -> param.getType().equals(type)) + .findFirst() + .orElseThrow( + () -> new InvalidStateException(INVALID_NOTIFICATION_TYPE) + ); + } +} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationShowService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationShowService.java new file mode 100644 index 000000000..27e98e14a --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationShowService.java @@ -0,0 +1,42 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; +import konkuk.thip.notification.application.mapper.NotificationQueryMapper; +import konkuk.thip.notification.application.port.in.NotificationShowUseCase; +import konkuk.thip.notification.application.port.in.dto.NotificationType; +import konkuk.thip.notification.application.port.out.NotificationQueryPort; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class NotificationShowService implements NotificationShowUseCase { + + private final static int PAGE_SIZE = 10; + + private final NotificationQueryPort notificationQueryPort; + private final NotificationQueryMapper notificationQueryMapper; + + @Override + @Transactional(readOnly = true) + public NotificationShowResponse showNotifications(Long userId, String cursorStr, NotificationType notificationType) { + // 1. Cursor 생성 + Cursor cursor = Cursor.from(cursorStr, PAGE_SIZE); + + // 2. 커서 기반 조회 + CursorBasedList result = switch (notificationType) { + case FEED -> notificationQueryPort.findFeedNotificationsByUserId(userId, cursor); + case ROOM -> notificationQueryPort.findRoomNotificationsByUserId(userId, cursor); + case FEED_AND_ROOM -> notificationQueryPort.findFeedAndRoomNotificationsByUserId(userId, cursor); + }; + + // 3. dto -> response 매핑 + var responses = notificationQueryMapper.toNotificationOfUsers(result.contents()); + + return new NotificationShowResponse(responses, result.nextCursor(), !result.hasNext()); + } +} From 981bf814065398fb591cb0741033dc662f422ce5 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:12:38 +0900 Subject: [PATCH 70/89] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20api=20=EC=98=81=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=96=B4=EB=8C=91=ED=84=B0=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationQueryPersistenceAdapter.java | 45 ++++++++++++++++++- .../PrimaryKeyNotificationQueryFunction.java | 11 +++++ .../port/out/NotificationQueryPort.java | 14 ++++++ .../port/out/dto/NotificationQueryDto.java | 28 ++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/main/java/konkuk/thip/notification/adapter/out/persistence/function/PrimaryKeyNotificationQueryFunction.java create mode 100644 src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java create mode 100644 src/main/java/konkuk/thip/notification/application/port/out/dto/NotificationQueryDto.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 469395bc3..289428136 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 @@ -1,15 +1,56 @@ package konkuk.thip.notification.adapter.out.persistence; +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.notification.adapter.out.mapper.NotificationMapper; +import konkuk.thip.notification.adapter.out.persistence.function.PrimaryKeyNotificationQueryFunction; import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; +import konkuk.thip.notification.application.port.out.NotificationQueryPort; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository @RequiredArgsConstructor -public class NotificationQueryPersistenceAdapter { +public class NotificationQueryPersistenceAdapter implements NotificationQueryPort { - private final NotificationJpaRepository jpaRepository; + private final NotificationJpaRepository notificationJpaRepository; private final NotificationMapper notificationMapper; + @Override + public CursorBasedList findFeedNotificationsByUserId(Long userId, Cursor cursor) { + return findNotificationsByPrimaryKeyCursor(cursor, ((lastNotificationId, pageSize) -> + notificationJpaRepository.findFeedNotificationsOrderByCreatedAtDesc(userId, lastNotificationId, pageSize) + )); + } + + @Override + public CursorBasedList findRoomNotificationsByUserId(Long userId, Cursor cursor) { + return findNotificationsByPrimaryKeyCursor(cursor, ((lastNotificationId, pageSize) -> + notificationJpaRepository.findRoomNotificationsOrderByCreatedAtDesc(userId, lastNotificationId, pageSize) + )); + } + + @Override + public CursorBasedList findFeedAndRoomNotificationsByUserId(Long userId, Cursor cursor) { + return findNotificationsByPrimaryKeyCursor(cursor, ((lastNotificationId, pageSize) -> + notificationJpaRepository.findFeedAndRoomNotificationsOrderByCreatedAtDesc(userId, lastNotificationId, pageSize) + )); + } + + private CursorBasedList findNotificationsByPrimaryKeyCursor(Cursor cursor, PrimaryKeyNotificationQueryFunction queryFunction) { + Long lastNotificationId = cursor.isFirstRequest() ? null : cursor.getLong(0); + int pageSize = cursor.getPageSize(); + + List dtos = queryFunction.apply(lastNotificationId, pageSize); + + return CursorBasedList.of(dtos, pageSize, dto -> { + Cursor nextCursor = new Cursor(List.of( + dto.notificationId().toString() + )); + return nextCursor.toEncodedString(); + }); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/function/PrimaryKeyNotificationQueryFunction.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/function/PrimaryKeyNotificationQueryFunction.java new file mode 100644 index 000000000..9783cdcee --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/function/PrimaryKeyNotificationQueryFunction.java @@ -0,0 +1,11 @@ +package konkuk.thip.notification.adapter.out.persistence.function; + +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; + +import java.util.List; + +@FunctionalInterface +public interface PrimaryKeyNotificationQueryFunction { + + List apply(Long lastNotificationId, int pageSize); +} 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 new file mode 100644 index 000000000..e1af77781 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java @@ -0,0 +1,14 @@ +package konkuk.thip.notification.application.port.out; + +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; + +public interface NotificationQueryPort { + + CursorBasedList findFeedNotificationsByUserId(Long userId, Cursor cursor); + + CursorBasedList findRoomNotificationsByUserId(Long userId, Cursor cursor); + + CursorBasedList findFeedAndRoomNotificationsByUserId(Long userId, Cursor cursor); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/out/dto/NotificationQueryDto.java b/src/main/java/konkuk/thip/notification/application/port/out/dto/NotificationQueryDto.java new file mode 100644 index 000000000..a08f14af0 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/out/dto/NotificationQueryDto.java @@ -0,0 +1,28 @@ +package konkuk.thip.notification.application.port.out.dto; + +import com.querydsl.core.annotations.QueryProjection; +import konkuk.thip.notification.domain.value.NotificationCategory; +import lombok.Builder; +import org.springframework.util.Assert; + +import java.time.LocalDateTime; + +@Builder +public record NotificationQueryDto( + Long notificationId, + String title, + String content, + boolean isChecked, + NotificationCategory notificationCategory, + LocalDateTime createdAt +) { + @QueryProjection + public NotificationQueryDto { + Assert.notNull(notificationId, "NotificationId must not be null"); + Assert.notNull(title, "Title must not be null"); + Assert.notNull(content, "Content must not be null"); + Assert.notNull(isChecked, "isChecked must not be null"); + Assert.notNull(notificationCategory, "NotificationCategory must not be null"); + Assert.notNull(createdAt, "CreatedAt must not be null"); + } +} From 450b5e30c80a771436767d55bdbf7310772d56e7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:13:39 +0900 Subject: [PATCH 71/89] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20api=20query=20dsl=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/NotificationJpaRepository.java | 2 +- .../NotificationQueryRepository.java | 14 ++++ .../NotificationQueryRepositoryImpl.java | 76 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java create mode 100644 src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java index c56b45e08..5fc891d93 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java @@ -3,5 +3,5 @@ import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface NotificationJpaRepository extends JpaRepository { +public interface NotificationJpaRepository extends JpaRepository, NotificationQueryRepository { } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java new file mode 100644 index 000000000..ccf540952 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java @@ -0,0 +1,14 @@ +package konkuk.thip.notification.adapter.out.persistence.repository; + +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; + +import java.util.List; + +public interface NotificationQueryRepository { + + List findFeedNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); + + List findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); + + List findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java new file mode 100644 index 000000000..395457ab5 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java @@ -0,0 +1,76 @@ +package konkuk.thip.notification.adapter.out.persistence.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import konkuk.thip.notification.adapter.out.jpa.QNotificationJpaEntity; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; +import konkuk.thip.notification.application.port.out.dto.QNotificationQueryDto; +import konkuk.thip.notification.domain.value.NotificationCategory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class NotificationQueryRepositoryImpl implements NotificationQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findFeedNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { + QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; + + var where = notification.userJpaEntity.userId.eq(userId) + .and(notification.notificationCategory.eq(NotificationCategory.FEED)); + + where = applyCursor(lastNotificationId, where, notification); + + return getNotificationQueryDtos(pageSize, notification, where); + } + + @Override + public List findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { + QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; + + var where = notification.userJpaEntity.userId.eq(userId) + .and(notification.notificationCategory.eq(NotificationCategory.ROOM)); + where = applyCursor(lastNotificationId, where, notification); + + return getNotificationQueryDtos(pageSize, notification, where); + } + + @Override + public List findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { + QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; + + var where = notification.userJpaEntity.userId.eq(userId) + .and(notification.notificationCategory.in(NotificationCategory.FEED, NotificationCategory.ROOM)); + where = applyCursor(lastNotificationId, where, notification); + + return getNotificationQueryDtos(pageSize, notification, where); + } + + private static BooleanExpression applyCursor(Long lastNotificationId, BooleanExpression where, QNotificationJpaEntity notification) { + if (lastNotificationId != null) { + where = where.and(notification.notificationId.lt(lastNotificationId)); + } + return where; + } + + private List getNotificationQueryDtos(int pageSize, QNotificationJpaEntity notification, BooleanExpression where) { + return queryFactory.select(new QNotificationQueryDto( + notification.notificationId, + notification.title, + notification.content, + notification.isChecked, + notification.notificationCategory, + notification.createdAt + )) + .from(notification) + .where(where) + .orderBy(notification.notificationId.desc()) // PK 기준 내림차순 (= 최신순) + .limit(pageSize + 1) + .fetch(); + } +} From 9f3d389eded3271656c9ddb54ea0d50dca96fe26 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:14:01 +0900 Subject: [PATCH 72/89] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20error=20c?= =?UTF-8?q?ode=20=EC=B6=94=EA=B0=80=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/exception/code/ErrorCode.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 50f51b6d7..95762e1c8 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -222,7 +222,13 @@ public enum ErrorCode implements ResponseCode { FCM_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, 200000, "존재하지 않는 FCM TOKEN 입니다."), FCM_TOKEN_ENABLED_STATE_ALREADY(HttpStatus.BAD_REQUEST, 200001, "요청한 상태로 이미 푸쉬 알림 여부가 설정되어 있습니다."), FCM_TOKEN_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 200002, "토큰을 소유하고 있는 계정이 아닙니다."), - FIREBASE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 200003, "FCM 푸쉬 알림 전송에 실패했습니다.") + FIREBASE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 200003, "FCM 푸쉬 알림 전송에 실패했습니다."), + + + /** + * 205000 : notification error + */ + INVALID_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, 205000, "유효하지 않은 알림 타입입니다."), ; From 63b93e803a52c07baa86ad581712774a68dea170 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:14:49 +0900 Subject: [PATCH 73/89] =?UTF-8?q?[feat]=20NotificationQueryMapper=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NotificationQueryDto <-> response 매퍼 추가 --- .../mapper/NotificationQueryMapper.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/mapper/NotificationQueryMapper.java diff --git a/src/main/java/konkuk/thip/notification/application/mapper/NotificationQueryMapper.java b/src/main/java/konkuk/thip/notification/application/mapper/NotificationQueryMapper.java new file mode 100644 index 000000000..1ccf2203e --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/mapper/NotificationQueryMapper.java @@ -0,0 +1,29 @@ +package konkuk.thip.notification.application.mapper; + +import konkuk.thip.common.util.DateUtil; +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +import java.util.List; + +@Mapper( + componentModel = "spring", + imports = DateUtil.class, + unmappedTargetPolicy = ReportingPolicy.IGNORE // 명시적으로 매핑하지 않은 필드를 무시하도록 설정 +) +public interface NotificationQueryMapper { + + // 단건 매핑 + @Mapping(target = "notificationType", + expression = "java(dto.notificationCategory().getDisplay())") + @Mapping(target = "postDate", + expression = "java(DateUtil.formatBeforeTime(dto.createdAt()))") + NotificationShowResponse.NotificationOfUser toNotificationOfUser(NotificationQueryDto dto); + + // 컬렉션 매핑 + List toNotificationOfUsers(List dtos); + +} From c53b2bc89cc1654e39f84a66fac775017e6c6abd Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:15:19 +0900 Subject: [PATCH 74/89] =?UTF-8?q?[test]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20api=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/util/TestEntityFactory.java | 11 + .../in/web/NotificationShowApiTest.java | 285 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/adapter/in/web/NotificationShowApiTest.java diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 959aeb160..86a2f75f1 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -9,6 +9,8 @@ import konkuk.thip.feed.domain.value.Tag; import konkuk.thip.feed.domain.value.TagList; import konkuk.thip.feed.domain.value.ContentList; +import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; +import konkuk.thip.notification.domain.value.NotificationCategory; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; import konkuk.thip.post.domain.PostType; @@ -371,4 +373,13 @@ public static RecentSearchJpaEntity createRecentSearch(UserJpaEntity userJpaEnti .build(); } + public static NotificationJpaEntity createNotification(UserJpaEntity user, String title, NotificationCategory category) { + return NotificationJpaEntity.builder() + .title(title) + .content("알림 내용") + .isChecked(false) + .notificationCategory(category) + .userJpaEntity(user) + .build(); + } } diff --git a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationShowApiTest.java b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationShowApiTest.java new file mode 100644 index 000000000..06ac5bc83 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationShowApiTest.java @@ -0,0 +1,285 @@ +package konkuk.thip.notification.adapter.in.web; + +import com.jayway.jsonpath.JsonPath; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; +import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.hamcrest.Matchers.*; + + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 알림센터 조회 api 통합 테스트") +@Transactional +class NotificationShowApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; + + @Test + @DisplayName("type == feed : 유저의 피드 알림을 최신순으로 반환한다.") + void show_feed_notifications() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity feedN_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_1", NotificationCategory.FEED)); + NotificationJpaEntity feedN_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_2", NotificationCategory.FEED)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), feedN_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), feedN_2.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "feed")); // type == feed + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(2))) + .andExpect(jsonPath("$.data.isLast").value(true)) + // 정렬 순서 : 최신순 (feedN_2 -> feedN_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(feedN_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("피드알림_2")) + .andExpect(jsonPath("$.data.notifications[0].notificationType").value(NotificationCategory.FEED.getDisplay())) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(feedN_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("피드알림_1")) + .andExpect(jsonPath("$.data.notifications[1].notificationType").value(NotificationCategory.FEED.getDisplay())); + } + + @Test + @DisplayName("type == room : 유저의 모임 알림을 최신순으로 반환한다.") + void show_room_notifications() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity roomN_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "모임알림_1", NotificationCategory.ROOM)); + NotificationJpaEntity roomN_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "모임알림_2", NotificationCategory.ROOM)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), roomN_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), roomN_2.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "room")); // type == room + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(2))) + .andExpect(jsonPath("$.data.isLast").value(true)) + // 정렬 순서 : 최신순 (roomN_2 -> roomN_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(roomN_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("모임알림_2")) + .andExpect(jsonPath("$.data.notifications[0].notificationType").value(NotificationCategory.ROOM.getDisplay())) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(roomN_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("모임알림_1")) + .andExpect(jsonPath("$.data.notifications[1].notificationType").value(NotificationCategory.ROOM.getDisplay())); + } + + @Test + @DisplayName("type == null : type이 null 일 경우, 유저의 피드 & 모임 알림을 최신순으로 반환한다.") + void show_feed_and_room_notifications() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity n_feed_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_피드_1", NotificationCategory.FEED)); + NotificationJpaEntity n_feed_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_피드_2", NotificationCategory.FEED)); + NotificationJpaEntity n_room_3 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_모임_3", NotificationCategory.ROOM)); + NotificationJpaEntity n_room_4 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_모임_4", NotificationCategory.ROOM)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), n_feed_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(20), n_feed_2.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), n_room_3.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(5), n_room_4.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId())); // type == null + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(4))) + .andExpect(jsonPath("$.data.isLast").value(true)) + // 정렬 순서 : 최신순 (n_room_4 -> n_room_3 -> n_feed_2 -> n_feed_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(n_room_4.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("알림_모임_4")) + .andExpect(jsonPath("$.data.notifications[0].notificationType").value(NotificationCategory.ROOM.getDisplay())) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(n_room_3.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("알림_모임_3")) + .andExpect(jsonPath("$.data.notifications[1].notificationType").value(NotificationCategory.ROOM.getDisplay())) + .andExpect(jsonPath("$.data.notifications[2].notificationId").value(n_feed_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[2].title").value("알림_피드_2")) + .andExpect(jsonPath("$.data.notifications[2].notificationType").value(NotificationCategory.FEED.getDisplay())) + .andExpect(jsonPath("$.data.notifications[3].notificationId").value(n_feed_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[3].title").value("알림_피드_1")) + .andExpect(jsonPath("$.data.notifications[3].notificationType").value(NotificationCategory.FEED.getDisplay())); + } + + @Test + @DisplayName("유저의 알림을 최신순으로 최대 10개 반환한다. 다음 페이지에 해당하는 데이터가 있을 경우, 다음 페이지의 cursor 값을 반환한다.") + void show_notifications_paging() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity feedN_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_1", NotificationCategory.FEED)); + NotificationJpaEntity feedN_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_2", NotificationCategory.FEED)); + NotificationJpaEntity feedN_3 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_3", NotificationCategory.FEED)); + NotificationJpaEntity feedN_4 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_4", NotificationCategory.FEED)); + NotificationJpaEntity feedN_5 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_5", NotificationCategory.FEED)); + NotificationJpaEntity feedN_6 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_6", NotificationCategory.FEED)); + NotificationJpaEntity feedN_7 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_7", NotificationCategory.FEED)); + NotificationJpaEntity feedN_8 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_8", NotificationCategory.FEED)); + NotificationJpaEntity feedN_9 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_9", NotificationCategory.FEED)); + NotificationJpaEntity feedN_10 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_10", NotificationCategory.FEED)); + NotificationJpaEntity feedN_11 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_11", NotificationCategory.FEED)); + NotificationJpaEntity feedN_12 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_12", NotificationCategory.FEED)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(50), feedN_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(45), feedN_2.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(40), feedN_3.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(35), feedN_4.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), feedN_5.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(25), feedN_6.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(20), feedN_7.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(15), feedN_8.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), feedN_9.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(5), feedN_10.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(3), feedN_11.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(1), feedN_12.getNotificationId() + ); + + //when //then + MvcResult firstResult = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "feed")) // type == feed + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(10))) // 10개 조회 + .andExpect(jsonPath("$.data.isLast").value(false)) // 다음 페이지 존재 + .andExpect(jsonPath("$.data.nextCursor").isNotEmpty()) // nextCursor 존재 + // 정렬 순서 : 최신순 (feedN_12 -> feedN_3) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(feedN_12.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("피드알림_12")) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(feedN_11.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("피드알림_11")) + .andExpect(jsonPath("$.data.notifications[2].notificationId").value(feedN_10.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[2].title").value("피드알림_10")) + .andExpect(jsonPath("$.data.notifications[3].notificationId").value(feedN_9.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[3].title").value("피드알림_9")) + .andExpect(jsonPath("$.data.notifications[4].notificationId").value(feedN_8.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[4].title").value("피드알림_8")) + .andExpect(jsonPath("$.data.notifications[5].notificationId").value(feedN_7.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[5].title").value("피드알림_7")) + .andExpect(jsonPath("$.data.notifications[6].notificationId").value(feedN_6.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[6].title").value("피드알림_6")) + .andExpect(jsonPath("$.data.notifications[7].notificationId").value(feedN_5.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[7].title").value("피드알림_5")) + .andExpect(jsonPath("$.data.notifications[8].notificationId").value(feedN_4.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[8].title").value("피드알림_4")) + .andExpect(jsonPath("$.data.notifications[9].notificationId").value(feedN_3.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[9].title").value("피드알림_3")) + .andReturn(); + + String responseBody = firstResult.getResponse().getContentAsString(); + String nextCursor = JsonPath.read(responseBody, "$.data.nextCursor"); + + mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "feed") // type == feed + .param("cursor", nextCursor)) // 두번째 페이지 조회 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(2))) // 2개 조회 + .andExpect(jsonPath("$.data.isLast").value(true)) // 마지막 페이지 + // 정렬 순서 : 최신순 (feedN_2 -> feedN_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(feedN_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("피드알림_2")) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(feedN_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("피드알림_1")); + } +} From d76c0b2da33d09df35f2bffbecede546fb2a7e9f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 17 Sep 2025 04:17:29 +0900 Subject: [PATCH 75/89] =?UTF-8?q?[refactor]=20=EB=82=B4=20=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=EB=B0=A9=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20use=20case=20=EC=9D=98=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B7=B8=EB=8B=88=EC=B2=98=20=EC=88=98=EC=A0=95=20(#3?= =?UTF-8?q?03)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application/port/in/dto 의 MyRoomType enum 을 메서드 파라미터로 설정함으로써 service 내부에서 MyRoomType 검증과정 삭제 --- .../konkuk/thip/room/adapter/in/web/RoomQueryController.java | 3 ++- .../thip/room/application/port/in/RoomShowMineUseCase.java | 3 ++- .../thip/room/application/service/RoomShowMineService.java | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index 12c04b8ba..c8ce51e78 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -10,6 +10,7 @@ import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; import konkuk.thip.room.adapter.in.web.response.*; import konkuk.thip.room.application.port.in.*; +import konkuk.thip.room.application.port.in.dto.MyRoomType; import konkuk.thip.room.application.port.in.dto.RoomGetHomeJoinedListQuery; import konkuk.thip.room.application.port.in.dto.RoomSearchQuery; import lombok.RequiredArgsConstructor; @@ -131,7 +132,7 @@ public BaseResponse getMyRooms( @RequestParam(value = "type", required = false, defaultValue = "playingAndRecruiting") final String type, @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(value = "cursor", required = false) final String cursor) { - return BaseResponse.ok(roomShowMineUseCase.getMyRooms(userId, type, cursor)); + return BaseResponse.ok(roomShowMineUseCase.getMyRooms(userId, MyRoomType.from(type), cursor)); } @Operation( diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java index 4a7bbffed..c8221d017 100644 --- a/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java @@ -1,8 +1,9 @@ package konkuk.thip.room.application.port.in; import konkuk.thip.room.adapter.in.web.response.RoomShowMineResponse; +import konkuk.thip.room.application.port.in.dto.MyRoomType; public interface RoomShowMineUseCase { - RoomShowMineResponse getMyRooms(Long userId, String type, String cursor); + RoomShowMineResponse getMyRooms(Long userId, MyRoomType myRoomType, String cursor); } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java b/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java index d53f30ec5..e99671939 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java @@ -25,12 +25,11 @@ public class RoomShowMineService implements RoomShowMineUseCase { @Override @Transactional(readOnly = true) - public RoomShowMineResponse getMyRooms(Long userId, String type, String cursor) { + public RoomShowMineResponse getMyRooms(Long userId, MyRoomType myRoomType, String cursor) { // 1. cursor 생성 Cursor nextCursor = Cursor.from(cursor, PAGE_SIZE); - // 2. type 검증 및 커서 기반 조회 - MyRoomType myRoomType = MyRoomType.from(type); + // 2. 커서 기반 조회 CursorBasedList result = switch (myRoomType) { case RECRUITING -> roomQueryPort .findRecruitingRoomsUserParticipated(userId, nextCursor); From 20fc36bf41680e172e34fd25433d03fff0a6a678 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 18 Sep 2025 01:39:46 +0900 Subject: [PATCH 76/89] =?UTF-8?q?[refactor]=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20api=20userId=20request=20param?= =?UTF-8?q?=20=EB=A5=BC=20swagger=20hidden=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/NotificationQueryController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java index 8b172c438..a8ace6dab 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java @@ -48,7 +48,7 @@ public BaseResponse showNotificationEnableS @ExceptionDescription(NOTIFICATION_SHOW) @GetMapping("/notifications") public BaseResponse showNotifications( - @UserId final Long userId, + @Parameter(hidden = true) @UserId final Long userId, @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(value = "cursor", required = false) final String cursor, @Parameter(description = "알림 타입. 해당 파라미터 값이 null인 경우에는 알림 타입을 구분하지 않고 조회합니다.", example = "feed or room") From a6884929f54d39c195f1a25beeaed6fb50704643 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 18 Sep 2025 01:40:12 +0900 Subject: [PATCH 77/89] =?UTF-8?q?[refactor]=20NotificationQueryRepositoryI?= =?UTF-8?q?mpl=20=EB=82=B4=EB=B6=80=EC=97=90=20Q=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=84=A0=EC=96=B8=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/NotificationQueryRepositoryImpl.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java index 395457ab5..c0fd7b310 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java @@ -17,10 +17,10 @@ public class NotificationQueryRepositoryImpl implements NotificationQueryReposit private final JPAQueryFactory queryFactory; + private final QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; + @Override public List findFeedNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { - QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; - var where = notification.userJpaEntity.userId.eq(userId) .and(notification.notificationCategory.eq(NotificationCategory.FEED)); @@ -31,8 +31,6 @@ public List findFeedNotificationsOrderByCreatedAtDesc(Long @Override public List findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { - QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; - var where = notification.userJpaEntity.userId.eq(userId) .and(notification.notificationCategory.eq(NotificationCategory.ROOM)); where = applyCursor(lastNotificationId, where, notification); @@ -42,8 +40,6 @@ public List findRoomNotificationsOrderByCreatedAtDesc(Long @Override public List findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { - QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; - var where = notification.userJpaEntity.userId.eq(userId) .and(notification.notificationCategory.in(NotificationCategory.FEED, NotificationCategory.ROOM)); where = applyCursor(lastNotificationId, where, notification); From 80bea58953dba8da3106f2de09863c52ecabb60f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:38:46 +0900 Subject: [PATCH 78/89] =?UTF-8?q?[refactor]=20argument=5Fresover=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20->=20resolver=EB=A1=9C=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthTokenArgumentResolver.java | 2 +- .../Oauth2IdArgumentResolver.java | 2 +- .../{argument_resolver => resolver}/UserIdArgumentResolver.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/konkuk/thip/common/security/{argument_resolver => resolver}/AuthTokenArgumentResolver.java (96%) rename src/main/java/konkuk/thip/common/security/{argument_resolver => resolver}/Oauth2IdArgumentResolver.java (96%) rename src/main/java/konkuk/thip/common/security/{argument_resolver => resolver}/UserIdArgumentResolver.java (96%) diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java b/src/main/java/konkuk/thip/common/security/resolver/AuthTokenArgumentResolver.java similarity index 96% rename from src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java rename to src/main/java/konkuk/thip/common/security/resolver/AuthTokenArgumentResolver.java index 672b12bf3..c8fa81803 100644 --- a/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/AuthTokenArgumentResolver.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.security.argument_resolver; +package konkuk.thip.common.security.resolver; import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.common.exception.AuthException; diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java b/src/main/java/konkuk/thip/common/security/resolver/Oauth2IdArgumentResolver.java similarity index 96% rename from src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java rename to src/main/java/konkuk/thip/common/security/resolver/Oauth2IdArgumentResolver.java index 34d590e14..00bd17d23 100644 --- a/src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/Oauth2IdArgumentResolver.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.security.argument_resolver; +package konkuk.thip.common.security.resolver; import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.common.exception.AuthException; diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java b/src/main/java/konkuk/thip/common/security/resolver/UserIdArgumentResolver.java similarity index 96% rename from src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java rename to src/main/java/konkuk/thip/common/security/resolver/UserIdArgumentResolver.java index f2b8a7d06..b82954ee9 100644 --- a/src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/UserIdArgumentResolver.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.security.argument_resolver; +package konkuk.thip.common.security.resolver; import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.common.exception.AuthException; From 2e3b64e42f19bc01da5be6549d16a7b6aaf30727 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:52:48 +0900 Subject: [PATCH 79/89] =?UTF-8?q?[refactor]=20redirect=5Furl=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=A1=B8=EB=B2=84=20=EA=B5=AC=ED=98=84=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomAuthorizationRequestResolver.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java diff --git a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java new file mode 100644 index 000000000..8e2974e64 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java @@ -0,0 +1,58 @@ +package konkuk.thip.common.security.resolver; + +import jakarta.servlet.http.HttpServletRequest; +import konkuk.thip.config.properties.WebDomainProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_SESSION_KEY; +import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_URL_KEY; + +@Slf4j +@RequiredArgsConstructor +public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + + private final OAuth2AuthorizationRequestResolver delegate; + private final WebDomainProperties webDomainProperties; + + public CustomAuthorizationRequestResolver(ClientRegistrationRepository repo, + String authorizationRequestBaseUri, + WebDomainProperties props) { + this.delegate = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri); + this.webDomainProperties = props; + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + OAuth2AuthorizationRequest base = delegate.resolve(request); + return customize(request, base); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + OAuth2AuthorizationRequest base = delegate.resolve(request, clientRegistrationId); + return customize(request, base); + } + + private OAuth2AuthorizationRequest customize(HttpServletRequest request, OAuth2AuthorizationRequest base) { + if (base == null) return null; + + String redirectUrl = request.getParameter(REDIRECT_URL_KEY.getValue()); + Map additional = new HashMap<>(base.getAdditionalParameters()); + if (StringUtils.hasText(redirectUrl) && webDomainProperties.isAllowed(redirectUrl)) { + request.getSession(true).setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUrl); + } + + return OAuth2AuthorizationRequest.from(base) + .additionalParameters(additional) + .build(); + } +} \ No newline at end of file From 9d428ff94c32a17611124bba03a045ad6eb9fb85 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:52:59 +0900 Subject: [PATCH 80/89] =?UTF-8?q?[refactor]=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20properties=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=84=A0=EC=96=B8=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../properties/WebDomainProperties.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/konkuk/thip/config/properties/WebDomainProperties.java diff --git a/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java new file mode 100644 index 000000000..cf45ec711 --- /dev/null +++ b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java @@ -0,0 +1,29 @@ +package konkuk.thip.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "server") +public class WebDomainProperties { + + private List webDomainUrls = new ArrayList<>(); + + public boolean isAllowed(String target) { + try { + URI uri = URI.create(target); + String origin = uri.getScheme() + "://" + uri.getHost() + (uri.getPort() > 0 ? ":" + uri.getPort() : ""); + return webDomainUrls.stream().anyMatch(o -> o.equalsIgnoreCase(origin)); + } catch (Exception e) { + return false; + } + } +} From fe121b3ef77be14266cddf0980ca99a68526cee5 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:53:25 +0900 Subject: [PATCH 81/89] =?UTF-8?q?[refactor]=20Security=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=97=90=EC=84=9C=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20resolver=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/config/SecurityConfig.java | 39 ++++++++++++------- .../java/konkuk/thip/config/WebMvcConfig.java | 6 +-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/main/java/konkuk/thip/config/SecurityConfig.java b/src/main/java/konkuk/thip/config/SecurityConfig.java index 9ee8472fe..4bfac31bc 100644 --- a/src/main/java/konkuk/thip/config/SecurityConfig.java +++ b/src/main/java/konkuk/thip/config/SecurityConfig.java @@ -5,6 +5,8 @@ import konkuk.thip.common.security.filter.JwtAuthenticationFilter; import konkuk.thip.common.security.oauth2.CustomOAuth2UserService; import konkuk.thip.common.security.oauth2.CustomSuccessHandler; +import konkuk.thip.common.security.resolver.CustomAuthorizationRequestResolver; +import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -16,6 +18,8 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -32,12 +36,6 @@ @RequiredArgsConstructor public class SecurityConfig { - @Value("${server.web-url}") - private String webUrl; - - @Value("${server.web-domain-url}") - private String webDomainUrl; - @Value("${server.https-url}") private String prodServerUrl; @@ -49,9 +47,22 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final CustomSuccessHandler customSuccessHandler; + private final ClientRegistrationRepository clientRegistrationRepository; + private final WebDomainProperties webDomainProperties; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // redirect_url(origin) → additionalParameters.return_to 저장 + var resolver = new CustomAuthorizationRequestResolver( + clientRegistrationRepository, + "/oauth2/authorization", + webDomainProperties + ); + + // 세션 저장소 + var authReqRepo = new HttpSessionOAuth2AuthorizationRequestRepository(); + http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) @@ -59,6 +70,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .httpBasic(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .oauth2Login((oauth2) -> oauth2 + .authorizationEndpoint(authorizationEndpointConfig -> authorizationEndpointConfig + .authorizationRequestResolver(resolver) + .authorizationRequestRepository(authReqRepo) + ) .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig .userService(customOAuth2UserService) ) @@ -89,13 +104,11 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of( - "http://localhost:5173", - webUrl, - webDomainUrl, - prodServerUrl, - devServerUrl - )); + + List allowedOrigins = webDomainProperties.getWebDomainUrls(); + allowedOrigins.addAll(List.of(prodServerUrl, devServerUrl)); + + config.setAllowedOrigins(allowedOrigins); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(Collections.singletonList("*")); config.setAllowCredentials(true); diff --git a/src/main/java/konkuk/thip/config/WebMvcConfig.java b/src/main/java/konkuk/thip/config/WebMvcConfig.java index 44a410d9e..b4b8a3256 100644 --- a/src/main/java/konkuk/thip/config/WebMvcConfig.java +++ b/src/main/java/konkuk/thip/config/WebMvcConfig.java @@ -1,8 +1,8 @@ package konkuk.thip.config; -import konkuk.thip.common.security.argument_resolver.AuthTokenArgumentResolver; -import konkuk.thip.common.security.argument_resolver.Oauth2IdArgumentResolver; -import konkuk.thip.common.security.argument_resolver.UserIdArgumentResolver; +import konkuk.thip.common.security.resolver.AuthTokenArgumentResolver; +import konkuk.thip.common.security.resolver.Oauth2IdArgumentResolver; +import konkuk.thip.common.security.resolver.UserIdArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; From c5b31fc895d63d35d991c47eae0233b9a76a3940 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 22:53:43 +0900 Subject: [PATCH 82/89] =?UTF-8?q?[refactor]=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B1=EA=B3=B5=EC=8B=9C=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=B4=EB=91=94=20red?= =?UTF-8?q?irect=5Furl=20=EB=B3=B5=EC=9B=90=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/exception/code/ErrorCode.java | 1 + .../security/constant/AuthParameters.java | 3 ++ .../security/oauth2/CustomSuccessHandler.java | 33 ++++++++++++++----- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 0628d8a6d..1a8ea98f7 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -27,6 +27,7 @@ public enum ErrorCode implements ResponseCode { JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."), AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."), + WEB_DOMAIN_ORIGIN_EMPTY(HttpStatus.INTERNAL_SERVER_ERROR, 50102, "허용된 웹 도메인 설정이 비어있습니다."), PERSISTENCE_TRANSACTION_REQUIRED(HttpStatus.INTERNAL_SERVER_ERROR, 50110, "@Transactional 컨텍스트가 필요합니다. 트랜잭션 범위 내에서만 사용할 수 있습니다."), diff --git a/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java index 688986eef..70c51d441 100644 --- a/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java +++ b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java @@ -19,6 +19,9 @@ public enum AuthParameters { COOKIE_ACCESS_TOKEN("access_token"), COOKIE_TEMP_TOKEN("temp_token"), + REDIRECT_URL_KEY("redirect_url"), + REDIRECT_SESSION_KEY("oauth2_return_to"), + ; private final String value; diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java index 74e2faf98..f91341f14 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java @@ -3,31 +3,32 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.time.Duration; +import java.util.List; +import java.util.Objects; import java.util.UUID; -import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_HOME_URL; -import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_SIGNUP_URL; +import static konkuk.thip.common.security.constant.AuthParameters.*; @Slf4j @Component @RequiredArgsConstructor public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private static final int COOKIE_MAX_AGE = 60 * 60 * 24; // 1일 private final LoginTokenStorage loginTokenStorage; - @Value("${server.web-redirect-url}") - private String webRedirectUrl; + private final WebDomainProperties webDomainProperties; private final JwtUtil jwtUtil; @@ -38,6 +39,22 @@ public void onAuthenticationSuccess( Authentication authentication ) throws IOException, ServletException { + // Resolver에서 세션에 저장한 origin을 복원 + String webRedirectDomain = null; + if (request.getSession(false) != null) { + webRedirectDomain = (String) request.getSession(false).getAttribute(REDIRECT_SESSION_KEY.getValue()); + request.getSession(false).removeAttribute(REDIRECT_SESSION_KEY.getValue()); // 사용했으면 제거(일회성) + } + + // 허용 오리진 검증 및 폴백 + if (!webDomainProperties.isAllowed(Objects.toString(webRedirectDomain, ""))) { + List origins = webDomainProperties.getWebDomainUrls(); + if (origins == null || origins.isEmpty()) { + throw new AuthException(ErrorCode.WEB_DOMAIN_ORIGIN_EMPTY); + } + webRedirectDomain = origins.get(0); + } + CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); LoginUser loginUser = oAuth2User.getLoginUser(); @@ -48,7 +65,7 @@ public void onAuthenticationSuccess( String loginTokenKey = UUID.randomUUID().toString(); loginTokenStorage.put(loginTokenKey, TokenType.TEMP, tempToken, Duration.ofMinutes(5)); // ttl 5분 - getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL.getValue() + "?loginTokenKey=" + loginTokenKey); + getRedirectStrategy().sendRedirect(request, response, webRedirectDomain + REDIRECT_SIGNUP_URL.getValue() + "?loginTokenKey=" + loginTokenKey); } else { // 기존 유저 - 로그인용 액세스 토큰 String accessToken = jwtUtil.createAccessToken(loginUser.userId()); @@ -56,7 +73,7 @@ public void onAuthenticationSuccess( String loginTokenKey = UUID.randomUUID().toString(); loginTokenStorage.put(loginTokenKey, TokenType.ACCESS, accessToken, Duration.ofMinutes(5)); // ttl 5분 - getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL.getValue() + "?loginTokenKey=" + loginTokenKey); + getRedirectStrategy().sendRedirect(request, response, webRedirectDomain + REDIRECT_HOME_URL.getValue() + "?loginTokenKey=" + loginTokenKey); } } } From 3f9698143f71ba45422b4e536664f9c74742dcb2 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 18 Sep 2025 23:24:37 +0900 Subject: [PATCH 83/89] =?UTF-8?q?[refactor]=20LoginTokenStorage=EB=A5=BC?= =?UTF-8?q?=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EA=B8=B0=EB=B0=98=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Redis=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/oauth2/CustomSuccessHandler.java | 1 + .../security/oauth2/LoginTokenStorage.java | 48 ------------------- .../security/oauth2/auth/AuthController.java | 14 +++--- .../tokenstorage/LoginTokenStorage.java | 19 ++++++++ .../tokenstorage/MemoryLoginTokenStorage.java | 44 +++++++++++++++++ .../tokenstorage/RedisLoginTokenStorage.java | 48 +++++++++++++++++++ 6 files changed, 119 insertions(+), 55 deletions(-) delete mode 100644 src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java create mode 100644 src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java create mode 100644 src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java create mode 100644 src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java index f91341f14..b0008a60e 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletResponse; import konkuk.thip.common.exception.AuthException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.security.oauth2.tokenstorage.LoginTokenStorage; import konkuk.thip.common.security.util.JwtUtil; import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java deleted file mode 100644 index fe664f313..000000000 --- a/src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java +++ /dev/null @@ -1,48 +0,0 @@ -package konkuk.thip.common.security.oauth2; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.time.Duration; -import java.time.Instant; -import java.util.concurrent.ConcurrentHashMap; - -@Component -@RequiredArgsConstructor -public class LoginTokenStorage { - - @Getter - public static final class Entry { - private final TokenType type; - private final String token; - private final long expireAtEpochMillis; - - private Entry(TokenType type, String token, long expireAtEpochMillis) { - this.type = type; - this.token = token; - this.expireAtEpochMillis = expireAtEpochMillis; - } - } - - private final ConcurrentHashMap store = new ConcurrentHashMap<>(); - - /** - * 토큰을 메모리에 저장 (TTL 적용) - */ - public void put(String key, TokenType type, String token, Duration ttl) { - long expiredAt = Instant.now().plus(ttl).toEpochMilli(); - store.put(key, new Entry(type, token, expiredAt)); - } - - /** - * 토큰을 일회성으로 조회 후 제거 (만료 시 null 반환) - */ - public Entry consume(String key) { - Entry entry = store.remove(key); - - if (entry == null) return null; - if (entry.expireAtEpochMillis < Instant.now().toEpochMilli()) return null; - return entry; - } -} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java b/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java index 05155cc80..3c0c43029 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java @@ -10,7 +10,7 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.common.security.annotation.Oauth2Id; -import konkuk.thip.common.security.oauth2.LoginTokenStorage; +import konkuk.thip.common.security.oauth2.tokenstorage.LoginTokenStorage; import konkuk.thip.common.security.oauth2.auth.dto.AuthSetCookieRequest; import konkuk.thip.common.security.oauth2.auth.dto.AuthSetCookieResponse; import konkuk.thip.common.security.oauth2.auth.dto.AuthTokenRequest; @@ -92,11 +92,11 @@ public BaseResponse getToken( String token; boolean isNewUser; - if (entry.getType() == ACCESS) { - token = entry.getToken(); + if (entry.type() == ACCESS) { + token = entry.token(); isNewUser = false; } else { - token = entry.getToken(); + token = entry.token(); isNewUser = true; } @@ -127,8 +127,8 @@ public BaseResponse setCookie( ResponseCookie cookie; String type; - if (entry.getType() == ACCESS) { - cookie = ResponseCookie.from(COOKIE_ACCESS_TOKEN.getValue(), entry.getToken()) + if (entry.type() == ACCESS) { + cookie = ResponseCookie.from(COOKIE_ACCESS_TOKEN.getValue(), entry.token()) .httpOnly(true) .secure(true) .sameSite("None") @@ -137,7 +137,7 @@ public BaseResponse setCookie( .build(); type = ACCESS.getValue(); } else { - cookie = ResponseCookie.from(COOKIE_TEMP_TOKEN.getValue(), entry.getToken()) + cookie = ResponseCookie.from(COOKIE_TEMP_TOKEN.getValue(), entry.token()) .httpOnly(true) .secure(true) .sameSite("None") diff --git a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java new file mode 100644 index 000000000..247ee1e31 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/LoginTokenStorage.java @@ -0,0 +1,19 @@ +package konkuk.thip.common.security.oauth2.tokenstorage; + +import konkuk.thip.common.security.oauth2.TokenType; + +import java.time.Duration; + +public interface LoginTokenStorage { + + void put(String key, TokenType type, String token, Duration ttl); + + /** + * 저장된 토큰을 1회용으로 소비 후 삭제한다. + * 존재하지 않으면 null 반환. + */ + Entry consume(String key); + + record Entry(TokenType type, String token) { + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java new file mode 100644 index 000000000..575987f82 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/MemoryLoginTokenStorage.java @@ -0,0 +1,44 @@ +package konkuk.thip.common.security.oauth2.tokenstorage; + +import konkuk.thip.common.security.oauth2.TokenType; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; + +@Profile("test") +@Component +@RequiredArgsConstructor +public class MemoryLoginTokenStorage implements LoginTokenStorage{ + private record InternalEntry(TokenType type, String token, long expireAtEpochMillis) { } + + private final ConcurrentHashMap store = new ConcurrentHashMap<>(); + + /** + * 토큰을 메모리에 저장 (TTL 적용) + */ + @Override + public void put(String key, TokenType type, String token, Duration ttl) { + long expiredAt = Instant.now().plus(ttl).toEpochMilli(); + store.put(key, new InternalEntry(type, token, expiredAt)); + } + + /** + * 토큰을 일회성으로 조회 후 제거 (만료 시 null 반환) + */ + @Override + public Entry consume(String key) { + InternalEntry entry = store.remove(key); + if (entry == null) return null; + + if (entry.expireAtEpochMillis() < Instant.now().toEpochMilli()) { + return null; // 만료 + } + + // 외부에는 최소 DTO만 반환 (내부 정보 캡슐화) + return new Entry(entry.type(), entry.token()); + } +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java new file mode 100644 index 000000000..689dd2133 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java @@ -0,0 +1,48 @@ +package konkuk.thip.common.security.oauth2.tokenstorage; + +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.security.oauth2.TokenType; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Profile({"!test"}) +@Component +@RequiredArgsConstructor +public class RedisLoginTokenStorage implements LoginTokenStorage { + + private final RedisTemplate redisTemplate; + + private static final String PREFIX = "auth:login-token:"; + + @Override + public void put(String key, TokenType type, String token, Duration ttl) { + String redisKey = toRedisKey(key); + Entry entry = new Entry(type, token); + + redisTemplate.opsForValue().set(redisKey, entry, ttl); + } + + @Override + public Entry consume(String key) { + String redisKey = toRedisKey(key); + Object value = redisTemplate.opsForValue().getAndDelete(redisKey); + if (value == null) { + return null; + } + + if (value instanceof Entry entry) { + return entry; + } + + throw new AuthException(ErrorCode.JSON_PROCESSING_ERROR); + } + + private String toRedisKey(String key) { + return PREFIX + key; + } +} \ No newline at end of file From 7accbaf3634ce066bf6f9b46f292e652acc3f538 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 19 Sep 2025 00:51:48 +0900 Subject: [PATCH 84/89] =?UTF-8?q?[refactor]=20Setter=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20=ED=9B=84=20=EB=B6=88=EB=B3=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/config/properties/WebDomainProperties.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java index cf45ec711..9a51754f9 100644 --- a/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java +++ b/src/main/java/konkuk/thip/config/properties/WebDomainProperties.java @@ -1,7 +1,6 @@ package konkuk.thip.config.properties; import lombok.Getter; -import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -10,12 +9,11 @@ import java.util.List; @Getter -@Setter @Component @ConfigurationProperties(prefix = "server") public class WebDomainProperties { - private List webDomainUrls = new ArrayList<>(); + private final List webDomainUrls = new ArrayList<>(); public boolean isAllowed(String target) { try { From ef217a47ab766556e2ee89f366b90297e36c861b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 19 Sep 2025 00:52:08 +0900 Subject: [PATCH 85/89] =?UTF-8?q?[refactor]=20Slf4j=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/resolver/CustomAuthorizationRequestResolver.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java index 8e2974e64..76f1f26b0 100644 --- a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java @@ -3,7 +3,6 @@ import jakarta.servlet.http.HttpServletRequest; import konkuk.thip.config.properties.WebDomainProperties; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; @@ -16,7 +15,6 @@ import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_SESSION_KEY; import static konkuk.thip.common.security.constant.AuthParameters.REDIRECT_URL_KEY; -@Slf4j @RequiredArgsConstructor public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { From ebbbf09a4b7eab4c282bd8a63b5f1b16af281f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 20 Sep 2025 22:40:48 +0900 Subject: [PATCH 86/89] =?UTF-8?q?[refactor]=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=EA=B2=80=EC=A6=9D=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EC=9D=80=20=EC=82=AC=EC=9A=A9=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/user/application/service/UserVerifyNicknameService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java b/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java index 3155f2a93..33da229c1 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java @@ -15,7 +15,6 @@ public class UserVerifyNicknameService implements UserVerifyNicknameUseCase { @Override @Transactional(readOnly = true) - @Unfiltered // soft delete 된 유저의 닉네임을 포함해서 중복 검증 public boolean isNicknameUnique(String nickname) { return !userQueryPort.existsByNickname(nickname); } From d300c6070022c5b5d820661d6ed89dd8c0b41c33 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sun, 21 Sep 2025 01:26:09 +0900 Subject: [PATCH 87/89] =?UTF-8?q?[refactor]=20=EC=BF=BC=EB=A6=AC=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=AF=B8=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=EC=8B=9C=20=EC=84=B8=EC=85=98=20=EC=A0=9C=EA=B1=B0=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/CustomAuthorizationRequestResolver.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java index 76f1f26b0..e202f22bf 100644 --- a/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java +++ b/src/main/java/konkuk/thip/common/security/resolver/CustomAuthorizationRequestResolver.java @@ -45,10 +45,12 @@ private OAuth2AuthorizationRequest customize(HttpServletRequest request, OAuth2A String redirectUrl = request.getParameter(REDIRECT_URL_KEY.getValue()); Map additional = new HashMap<>(base.getAdditionalParameters()); + var session = request.getSession(true); if (StringUtils.hasText(redirectUrl) && webDomainProperties.isAllowed(redirectUrl)) { - request.getSession(true).setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUrl); + session.setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUrl); + } else { + session.removeAttribute(REDIRECT_SESSION_KEY.getValue()); } - return OAuth2AuthorizationRequest.from(base) .additionalParameters(additional) .build(); From a78016821c42afa8d183c5e066aa7bd8c17eb39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sun, 21 Sep 2025 01:52:24 +0900 Subject: [PATCH 88/89] =?UTF-8?q?[refactor]=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=EA=B2=80=EC=A6=9D=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EC=9D=80=20=EC=82=AC=EC=9A=A9=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20-->=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/adapter/in/web/UserVerifyNicknameControllerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java index a6042d8f6..82e2d2f62 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java @@ -147,7 +147,7 @@ void nickname_too_long() throws Exception { } @Test - @DisplayName("회원 탈퇴한(= soft delete 처리된) 유저의 닉네임 정보를 포함해서 중복 검증을 수행한다.") + @DisplayName("회원 탈퇴한(= soft delete 처리된) 유저의 닉네임 정보를 포함하지않고 중복 검증을 수행한다.") void verify_nickname_with_soft_delete_users() throws Exception { //given UserJpaEntity deleteUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "노성준")); @@ -170,6 +170,6 @@ void verify_nickname_with_soft_delete_users() throws Exception { JsonNode jsonNode = objectMapper.readTree(json); boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean(); - assertThat(isVerified).isFalse(); // 닉네임 중복으로 인해 isVerified == false + assertThat(isVerified).isTrue(); } } From 2052fcf2ae65b74e197c39132e0ca9022b323978 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sun, 21 Sep 2025 02:38:29 +0900 Subject: [PATCH 89/89] =?UTF-8?q?[refactor]=20entrypoint=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=84=98=EC=96=B4=EC=98=A4=EB=8A=94=20=EC=98=88=EC=83=81?= =?UTF-8?q?=EC=B9=98=20=EB=AA=BB=ED=95=9C=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?AuthException=EC=9C=BC=EB=A1=9C=20=EA=B0=90=EC=8B=B8=EC=84=9C?= =?UTF-8?q?=20handler=EC=97=90=20=EC=A0=84=EB=8B=AC=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/exception/code/ErrorCode.java | 1 + .../filter/JwtAuthenticationEntryPoint.java | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 7b88726e2..9790bb1be 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -24,6 +24,7 @@ public enum ErrorCode implements ResponseCode { AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."), AUTH_INVALID_LOGIN_TOKEN_KEY(HttpStatus.UNAUTHORIZED, 40106, "유효하지 않은 로그인 토큰 키입니다."), AUTH_BLACKLIST_TOKEN(HttpStatus.UNAUTHORIZED, 40107, "블랙리스트에 등록된 토큰입니다."), + AUTH_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 40108, "인증 처리 중 서버 오류가 발생했습니다."), JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."), AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."), diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java index b52dfa329..0a2fe3c5f 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java @@ -3,6 +3,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.code.ErrorCode; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -15,16 +17,32 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private final HandlerExceptionResolver resolver; - public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver){ + public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") + HandlerExceptionResolver resolver) { this.resolver = resolver; } @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - Exception e = (Exception) request.getAttribute("exception"); - if(e == null){ - e = authException; + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + // 필터에서 set한 예외 우선 + Exception original = (Exception) request.getAttribute("exception"); + if (original == null) { + original = authException; + } + + Exception mapped = wrapAsAuthException(original); + + resolver.resolveException(request, response, null, mapped); + } + + // 모든 예외를 AuthException(401)으로 감싸는 메서드 + private Exception wrapAsAuthException(Exception e) { + if (e instanceof AuthException) { + return e; } - resolver.resolveException(request, response, null, e); + return new AuthException(ErrorCode.AUTH_INTERNAL_SERVER_ERROR, e); } -} +} \ No newline at end of file