Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +28,7 @@ public class NotificationQueryController {

private final NotificationShowEnableStateUseCase notificationShowEnableStateUseCase;
private final NotificationShowUseCase notificationShowUseCase;
private final NotificationExistsUncheckedUseCase notificationExistsUncheckedUseCase;

@Operation(
summary = "사용자 푸시알림 수신여부 조회 (마이페이지 -> 알림설정)",
Expand Down Expand Up @@ -56,4 +59,14 @@ public BaseResponse<NotificationShowResponse> showNotifications(
) {
return BaseResponse.ok(notificationShowUseCase.showNotifications(userId, cursor, NotificationType.from(type)));
}

@Operation(
summary = "유저의 안읽은 알림 존재 여부 확인",
description = "유저가 읽지 않은 알림이 존재하는지 여부를 확인합니다."
)
@GetMapping("/notifications/exists-unchecked")
public BaseResponse<NotificationUncheckedExistsResponse> existsUnchecked(@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(NotificationUncheckedExistsResponse.of(
notificationExistsUncheckedUseCase.existsUnchecked(userId)));
}
Comment on lines +63 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Swagger 문서에 예외 처리 설명이 누락되었습니다.

Lines 38, 51에서 다른 엔드포인트들은 @ExceptionDescription 어노테이션으로 예외 케이스를 문서화하고 있으나, 이 엔드포인트에는 누락되었습니다. API 문서의 일관성을 위해 추가를 권장합니다.

다음과 같이 추가하세요:

     @Operation(
             summary = "유저의 안읽은 알림 존재 여부 확인",
             description = "유저가 읽지 않은 알림이 존재하는지 여부를 확인합니다."
     )
+    @ExceptionDescription(NOTIFICATION_EXISTS_UNCHECKED)
     @GetMapping("/notifications/exists-unchecked")
     public BaseResponse<NotificationUncheckedExistsResponse> existsUnchecked(@Parameter(hidden = true) @UserId final Long userId) {

그리고 SwaggerResponseDescription 클래스에 상수를 추가하세요:

public static final String NOTIFICATION_EXISTS_UNCHECKED = """
    ### 발생 가능한 예외
    - [인증/인가 관련 예외]
    """;

다음 스크립트로 SwaggerResponseDescription 클래스 위치 및 패턴을 확인하세요:


🏁 Script executed:

#!/bin/bash
# Description: SwaggerResponseDescription 클래스 확인 및 패턴 분석

# SwaggerResponseDescription 클래스 찾기
fd -e java SwaggerResponseDescription

# 기존 NOTIFICATION 관련 상수 패턴 확인
rg -nP 'NOTIFICATION_[A-Z_]+\s*=' --type=java -A 3

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Find usages of @ExceptionDescription in Java files
rg -n '@ExceptionDescription' -g '*.java'

# Display first 200 lines of SwaggerResponseDescription.java
sed -n '1,200p' src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java

# List all public static final String constants in SwaggerResponseDescription.java
rg -n 'public static final String' src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java

Length of output: 12502


🏁 Script executed:

rg -n 'NOTIFICATION_' src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java

Length of output: 312


existsUnchecked 엔드포인트에 @ExceptionDescription 및 SwaggerResponseDescription enum constant 추가

  • src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java 에서
    @GetMapping("/notifications/exists-unchecked") 바로 위에
    @ExceptionDescription(NOTIFICATION_EXISTS_UNCHECKED)
    추가
  • src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java 에서
    기존 알림 처리 enum(NOTIFICATION_MARK_TO_CHECKED) 다음에
    NOTIFICATION_EXISTS_UNCHECKED(new LinkedHashSet<>(Set.of(
        USER_NOT_FOUND
    ))),
    형태로 새 enum constant 등록 (실제 던질 수 있는 ErrorCode로 Set.of(...) 보강)
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java
around lines 63 to 71, add the
@ExceptionDescription(NOTIFICATION_EXISTS_UNCHECKED) annotation immediately
above the @GetMapping("/notifications/exists-unchecked") declaration so the
endpoint is annotated for documented exceptions; in
src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java,
directly after the existing NOTIFICATION_MARK_TO_CHECKED enum constant add a new
enum constant NOTIFICATION_EXISTS_UNCHECKED(...) that supplies a LinkedHashSet
containing the appropriate ErrorCode(s) that this endpoint can actually throw
(replace Set.of(...) with the real error codes, e.g., USER_NOT_FOUND if
applicable), ensuring imports and enum formatting follow the surrounding enum
structure.

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public CursorBasedList<NotificationQueryDto> findFeedAndRoomNotificationsByUserI
));
}

@Override
public boolean existsUnchecked(Long userId) {
return notificationJpaRepository.existsByUserIdAndIsCheckedFalse(userId);
}

private CursorBasedList<NotificationQueryDto> findNotificationsByPrimaryKeyCursor(Cursor cursor, PrimaryKeyNotificationQueryFunction queryFunction) {
Long lastNotificationId = cursor.isFirstRequest() ? null : cursor.getLong(0);
int pageSize = cursor.getPageSize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface NotificationQueryRepository {
List<NotificationQueryDto> findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize);

List<NotificationQueryDto> findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize);

boolean existsByUserIdAndIsCheckedFalse(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ public List<NotificationQueryDto> findFeedAndRoomNotificationsOrderByCreatedAtDe
return getNotificationQueryDtos(pageSize, notification, where);
}

@Override
public boolean existsByUserIdAndIsCheckedFalse(Long userId) {
Integer result = queryFactory.selectOne()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단순 궁금인데 간단한 쿼리같은데 JPQL대신 queryDsl을 사용한 이유가 따로 있을까요??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역시 예리하시네요.

첨부한 블로그 내용 확인해주시면 될 것 같습니다!!
https://mirrorofcode.tistory.com/362

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 jpql에서는 count 함수밖에 사용하지 못하는데 이는 모든 테이블을 스캔하는 것이고, exists 함수 또는 현재 성준님이 구현하신 것처럼 select 1 limit 1을 사용하면 특정 매치되는 row가 하나만 있으면 바로 스캔이 중단되어서 성능이 더 좋은 것 같네요! 꼼꼼하십니다 굿굿

.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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package konkuk.thip.notification.application.port.in;

public interface NotificationExistsUncheckedUseCase {

boolean existsUnchecked(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface NotificationQueryPort {
CursorBasedList<NotificationQueryDto> findRoomNotificationsByUserId(Long userId, Cursor cursor);

CursorBasedList<NotificationQueryDto> findFeedAndRoomNotificationsByUserId(Long userId, Cursor cursor);

boolean existsUnchecked(Long userId);
}
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

public boolean existsUnchecked(Long userId) {
return notificationQueryPort.existsUnchecked(userId);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}