From 2377a424589fcf3d073513f1df1aef019023f12f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:28:57 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20controller=20=EA=B5=AC=ED=98=84=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/NotificationQueryController.java | 13 +++++++++++++ .../NotificationUncheckedExistsResponse.java | 9 +++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java 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 a8ace6dab..dec6a9e67 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 @@ -8,6 +8,8 @@ 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.adapter.in.web.response.NotificationUncheckedExistsResponse; +import konkuk.thip.notification.application.port.in.NotificationExistsUncheckedUseCase; 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; @@ -26,6 +28,7 @@ public class NotificationQueryController { private final NotificationShowEnableStateUseCase notificationShowEnableStateUseCase; private final NotificationShowUseCase notificationShowUseCase; + private final NotificationExistsUncheckedUseCase notificationExistsUncheckedUseCase; @Operation( summary = "사용자 푸시알림 수신여부 조회 (마이페이지 -> 알림설정)", @@ -56,4 +59,14 @@ public BaseResponse showNotifications( ) { return BaseResponse.ok(notificationShowUseCase.showNotifications(userId, cursor, NotificationType.from(type))); } + + @Operation( + summary = "유저의 안읽은 알림 존재 여부 확인", + description = "유저가 읽지 않은 알림이 존재하는지 여부를 확인합니다." + ) + @GetMapping("/notifications/exists-unchecked") + public BaseResponse existsUnchecked(@Parameter(hidden = true) @UserId final Long userId) { + return BaseResponse.ok(NotificationUncheckedExistsResponse.of( + notificationExistsUncheckedUseCase.existsUnchecked(userId))); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java new file mode 100644 index 000000000..f48dd55f5 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java @@ -0,0 +1,9 @@ +package konkuk.thip.notification.adapter.in.web.response; + +public record NotificationUncheckedExistsResponse( + boolean exists +) { + public static NotificationUncheckedExistsResponse of(boolean exists) { + return new NotificationUncheckedExistsResponse(exists); + } +} From d28b95e18f22898b882428e61eace1699fe6e3d7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:29:10 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20use=20case=20=EA=B5=AC=ED=98=84=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationExistsUncheckedUseCase.java | 6 ++++++ .../NotificationExistsUncheckedService.java | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java diff --git a/src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java new file mode 100644 index 000000000..3b34473df --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java @@ -0,0 +1,6 @@ +package konkuk.thip.notification.application.port.in; + +public interface NotificationExistsUncheckedUseCase { + + boolean existsUnchecked(Long userId); +} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java new file mode 100644 index 000000000..66783b6ea --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.notification.application.port.in.NotificationExistsUncheckedUseCase; +import konkuk.thip.notification.application.port.out.NotificationQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class NotificationExistsUncheckedService implements NotificationExistsUncheckedUseCase { + + private final NotificationQueryPort notificationQueryPort; + + @Override + @Transactional(readOnly = true) + public boolean existsUnchecked(Long userId) { + return notificationQueryPort.existsUnchecked(userId); + } +} From 2bc5f6e8fc48ca9bca4ffd6614159cffec1df8b4 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:29:39 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EC=98=81=EC=86=8D=EC=84=B1=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/NotificationQueryPersistenceAdapter.java | 5 +++++ .../application/port/out/NotificationQueryPort.java | 2 ++ 2 files changed, 7 insertions(+) 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 289428136..ab90570a3 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 @@ -40,6 +40,11 @@ public CursorBasedList findFeedAndRoomNotificationsByUserI )); } + @Override + public boolean existsUnchecked(Long userId) { + return notificationJpaRepository.existsByUserIdAndIsCheckedFalse(userId); + } + private CursorBasedList findNotificationsByPrimaryKeyCursor(Cursor cursor, PrimaryKeyNotificationQueryFunction queryFunction) { Long lastNotificationId = cursor.isFirstRequest() ? null : cursor.getLong(0); int pageSize = cursor.getPageSize(); 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 index e1af77781..c718817c6 100644 --- a/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java +++ b/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java @@ -11,4 +11,6 @@ public interface NotificationQueryPort { CursorBasedList findRoomNotificationsByUserId(Long userId, Cursor cursor); CursorBasedList findFeedAndRoomNotificationsByUserId(Long userId, Cursor cursor); + + boolean existsUnchecked(Long userId); } From c2dee7bd887cb86e82734c720e35c75ef5ab687d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:29:51 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20querydsl=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20(#31?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/NotificationQueryRepository.java | 2 ++ .../repository/NotificationQueryRepositoryImpl.java | 11 +++++++++++ 2 files changed, 13 insertions(+) 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 index ccf540952..836899999 100644 --- 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 @@ -11,4 +11,6 @@ public interface NotificationQueryRepository { List findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); List findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); + + boolean existsByUserIdAndIsCheckedFalse(Long userId); } 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 c0fd7b310..5cf78fa3a 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 @@ -47,6 +47,17 @@ public List findFeedAndRoomNotificationsOrderByCreatedAtDe return getNotificationQueryDtos(pageSize, notification, where); } + @Override + public boolean existsByUserIdAndIsCheckedFalse(Long userId) { + Integer result = queryFactory.selectOne() + .from(notification) + .where(notification.userJpaEntity.userId.eq(userId) + .and(notification.isChecked.eq(false))) + .fetchFirst(); + + return result != null; + } + private static BooleanExpression applyCursor(Long lastNotificationId, BooleanExpression where, QNotificationJpaEntity notification) { if (lastNotificationId != null) { where = where.and(notification.notificationId.lt(lastNotificationId)); From c6b5e7b861e8ca2b053c3a952164c9db6fe917b2 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:30:05 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[test]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationExistsUncheckedApiTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java diff --git a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java new file mode 100644 index 000000000..ad2ce70e1 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java @@ -0,0 +1,72 @@ +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.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 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; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 안읽은 알림 존재 여부 확인 api 통합 테스트") +class NotificationExistsUncheckedApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; + + @Test + @DisplayName("유저가 읽지 않은 알림이 있을 경우, true 를 반환한다.") + void notification_exists_unchecked_true() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + NotificationJpaEntity n1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림1", NotificationCategory.FEED)); + + //when + ResultActions result = mockMvc.perform(get("/notifications/exists-unchecked") + .requestAttr("userId", user.getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.exists").value(true)); + } + + @Test + @DisplayName("유저가 읽지 않은 알림이 없을 경우, false 를 반환한다.") + void notification_exists_unchecked_false() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + NotificationJpaEntity n1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림1", NotificationCategory.FEED)); + jdbcTemplate.update( + "UPDATE notifications SET is_checked = TRUE WHERE notification_id = ?", + n1.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications/exists-unchecked") + .requestAttr("userId", user.getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.exists").value(false)); + } +}