From b744eaf178efc5ad5b0d8a3561582a3743ed1c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 7 Aug 2026 22:37:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=AA=A8=EB=91=90?= =?UTF-8?q?=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 유저가 자신의 읽지 않은 모든 알림을 한 번에 읽음 처리할 수 있도록 POST /notifications/check-all 엔드포인트를 추가한다. Co-Authored-By: Claude Sonnet 5 --- .../swagger/SwaggerResponseDescription.java | 3 + .../in/web/NotificationCommandController.java | 12 ++++ ...NotificationCommandPersistenceAdapter.java | 5 ++ .../repository/NotificationJpaRepository.java | 8 +++ .../port/in/NotificationMarkUseCase.java | 2 + .../port/out/NotificationCommandPort.java | 2 + .../service/NotificationMarkService.java | 6 ++ .../NotificationMarkAllToCheckedApiTest.java | 70 +++++++++++++++++++ 8 files changed, 108 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkAllToCheckedApiTest.java diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index de9dde63c..4a76e2e27 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -393,6 +393,9 @@ public enum SwaggerResponseDescription { NOTIFICATION_NOT_FOUND, NOTIFICATION_ACCESS_FORBIDDEN ))), + NOTIFICATION_MARK_ALL_TO_CHECKED(new LinkedHashSet<>(Set.of( + + ))), ; private final Set errorCodeList; 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 145706b58..cdd1ec34a 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 @@ -77,4 +77,16 @@ public BaseResponse markNotificationToChecked @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(notificationMarkUseCase.markToChecked(request.notificationId(), userId)); } + + @Operation( + summary = "유저의 모든 알림 읽음 처리", + description = "유저의 읽지 않은 모든 알림을 읽음 처리합니다." + ) + @ExceptionDescription(NOTIFICATION_MARK_ALL_TO_CHECKED) + @PostMapping("/notifications/check-all") + public BaseResponse markAllNotificationsToChecked( + @Parameter(hidden = true) @UserId final Long userId) { + notificationMarkUseCase.markAllToChecked(userId); + return BaseResponse.ok(null); + } } 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 5099912cd..70ff92d6c 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 @@ -46,4 +46,9 @@ public void update(Notification notification) { notificationJpaEntity.updateFrom(notification); } + + @Override + public void markAllAsCheckedByUserId(Long userId) { + notificationJpaRepository.markAllAsCheckedByUserId(userId); + } } 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 5fc891d93..627fbebe1 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 @@ -2,6 +2,14 @@ import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface NotificationJpaRepository extends JpaRepository, NotificationQueryRepository { + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE NotificationJpaEntity n SET n.isChecked = true " + + "WHERE n.userJpaEntity.userId = :userId AND n.isChecked = false") + int markAllAsCheckedByUserId(@Param("userId") Long userId); } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java index 62abd35d1..eebabe516 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/NotificationMarkUseCase.java @@ -5,4 +5,6 @@ public interface NotificationMarkUseCase { NotificationMarkToCheckedResponse markToChecked(Long notificationId, Long userId); + + void markAllToChecked(Long userId); } 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 f256b3329..b70711626 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 @@ -19,4 +19,6 @@ default Notification getByIdOrThrow(Long id) { } void update(Notification notification); + + void markAllAsCheckedByUserId(Long userId); } diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java index ddff7d7b4..dc3f0d384 100644 --- a/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationMarkService.java @@ -36,4 +36,10 @@ public NotificationMarkToCheckedResponse markToChecked(Long notificationId, Long notification.getRedirectSpec().params() ); } + + @Override + @Transactional + public void markAllToChecked(Long userId) { + notificationCommandPort.markAllAsCheckedByUserId(userId); + } } diff --git a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkAllToCheckedApiTest.java b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkAllToCheckedApiTest.java new file mode 100644 index 000000000..03dd6f362 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationMarkAllToCheckedApiTest.java @@ -0,0 +1,70 @@ +package konkuk.thip.notification.adapter.in.web; + +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.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 알림 모두 읽음 처리 api 통합 테스트") +class NotificationMarkAllToCheckedApiTest { + + @Autowired private MockMvc mockMvc; + + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; + + @Test + @DisplayName("본인의 읽지 않은 모든 알림이 읽음 처리되고, 다른 유저의 알림은 영향받지 않는다.") + void mark_all_notifications_to_checked_success() throws Exception { + // given + UserJpaEntity owner = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + UserJpaEntity other = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity unchecked1 = notificationJpaRepository.save( + TestEntityFactory.createNotification(owner, "알림1", NotificationCategory.FEED)); + NotificationJpaEntity unchecked2 = notificationJpaRepository.save( + TestEntityFactory.createNotification(owner, "알림2", NotificationCategory.FEED)); + NotificationJpaEntity otherUserNotification = notificationJpaRepository.save( + TestEntityFactory.createNotification(other, "다른 유저 알림", NotificationCategory.FEED)); + + // when & then + mockMvc.perform(post("/notifications/check-all") + .requestAttr("userId", owner.getUserId())) + .andExpect(status().isOk()); + + assertThat(notificationJpaRepository.findById(unchecked1.getNotificationId()).orElseThrow().isChecked()).isTrue(); + assertThat(notificationJpaRepository.findById(unchecked2.getNotificationId()).orElseThrow().isChecked()).isTrue(); + assertThat(notificationJpaRepository.findById(otherUserNotification.getNotificationId()).orElseThrow().isChecked()).isFalse(); + } + + @Test + @DisplayName("읽지 않은 알림이 없는 경우에도 에러 없이 성공한다.") + void mark_all_notifications_to_checked_when_none_unchecked() throws Exception { + // given + UserJpaEntity owner = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + // when & then + mockMvc.perform(post("/notifications/check-all") + .requestAttr("userId", owner.getUserId())) + .andExpect(status().isOk()); + } +}