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); + } +} 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/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)); 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/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); } 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); + } +} 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)); + } +}