Skip to content

feat: 알림 API 구현#71

Merged
deli-minju merged 6 commits into
developfrom
feat/#70-notification-api
May 26, 2026
Merged

feat: 알림 API 구현#71
deli-minju merged 6 commits into
developfrom
feat/#70-notification-api

Conversation

@deli-minju
Copy link
Copy Markdown
Contributor

@deli-minju deli-minju commented May 26, 2026

📌 작업 요약

  • 요약:
    • React Native 앱에서 푸시 알림이 누락되거나 지연될 수 있는 상황을 고려해 서버 보관함 기반 알림 API를 구현
    • 알림 목록 조회, 미읽음 수 조회, 단건/일괄/전체 읽음 처리 API를 추가
    • 푸시 토큰 등록/갱신/삭제 API를 추가해 앱 재설치, 로그인, 토큰 refresh 상황에 대응
    • 중복 알림 방지를 위한 dedupeKey와 향후 FCM/Expo 발송 결과 추적을 위한 delivery log 구조를 추가
    • 알림 관련 Flyway migration, 성공/실패 코드, 통합 테스트를 추가
    • ErrorCode 동기화
      • NOTI4041 (HTTP 404 Not Found): notificationId에 해당하는 알림이 없거나 소유권이 일치하지 않음
    • 에러코드 스냅샷 탭(v10): error-code-v10(gid=1155198711)
    • 커밋 해시: 136501b
  • 관련 이슈: closes [FEAT] 알림 API 구현 #70

🌿 브랜치 정보

  • Source: feat/#70-notification-api
  • Target: develop (기본)

✅ 체크리스트

  • 브랜치 컨벤션 준수 (feat/refac/hotfix/chore/design/bugfix)
  • 커밋 컨벤션 준수 (feat/fix/refactor/docs/style/chore)
  • self-review 완료
  • 테스트 및 로컬 실행 확인 완료

🧪 테스트 결과

.\scripts\run-tests-with-postgres.ps1

Summary by CodeRabbit

  • New Features

    • 알림 시스템 API 추가: 알림 목록 조회(커서 기반 페이지네이션), 미읽음 개수 확인, 개별·일괄·전체 읽음 처리
    • 푸시 토큰 관리: iOS/Android/Expo 토큰 등록·갱신 및 삭제, 토큰 재등록 시 기존 레코드 복구/유지
  • Tests

    • 알림 및 푸시 토큰 흐름(조회·읽음 처리·등록·삭제·재등록·유효성) 통합 테스트 추가

Review Change Stack

@deli-minju deli-minju self-assigned this May 26, 2026
@deli-minju deli-minju added the feat 새로운 기능 추가 작업 label May 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@deli-minju, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 41 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d4689839-8023-4763-a034-348ebe82349f

📥 Commits

Reviewing files that changed from the base of the PR and between 6cda408 and 13e1b87.

📒 Files selected for processing (2)
  • .github/workflows/docker-be.yml
  • docs/error-code.md
📝 Walkthrough

Walkthrough

알림 API 기능을 완성하기 위한 타입 계약부터 REST 엔드포인트, 데이터베이스 마이그레이션, 통합 테스트까지 전체 스택을 추가했습니다. 단순 푸시 발송에 의존하지 않고 서버 기반 알림 보관함으로 누락된 알림 동기화를 지원합니다.

Changes

알림 API 구현

Layer / File(s) Summary
도메인 계약 및 타입 정의
src/main/java/com/gachi/be/domain/notification/dto/request/*, src/main/java/com/gachi/be/domain/notification/dto/response/*, src/main/java/com/gachi/be/domain/notification/entity/enums/*, src/main/java/com/gachi/be/domain/notification/service/NotificationCreateCommand.java, src/main/java/com/gachi/be/domain/notification/service/NotificationDeliveryResultCommand.java
요청(NotificationReadRequest, PushTokenRegisterRequest, PushTokenDeleteRequest), 응답(NotificationListResponse, NotificationResponse, NotificationReadResponse, PushTokenResponse, NotificationUnreadCountResponse) DTO와 NotificationType, PushPlatform, NotificationDeliveryStatus 열거형, 커맨드 객체를 정의합니다.
도메인 엔티티 및 생명주기
src/main/java/com/gachi/be/domain/notification/entity/Notification.java, src/main/java/com/gachi/be/domain/notification/entity/PushDeviceToken.java, src/main/java/com/gachi/be/domain/notification/entity/NotificationDeliveryLog.java
Notification, PushDeviceToken, NotificationDeliveryLog 엔티티를 JPA로 매핑하고, 생성자, 상태 변경 메서드(isRead, markRead, refresh, softDelete), JPA 라이프사이클 콜백으로 타임스탬프를 관리합니다.
데이터 접근 계층
src/main/java/com/gachi/be/domain/notification/repository/NotificationRepository.java, src/main/java/com/gachi/be/domain/notification/repository/PushDeviceTokenRepository.java, src/main/java/com/gachi/be/domain/notification/repository/NotificationDeliveryLogRepository.java
커서 기반 페이징(findInbox), dedupe_key 중복 검사, 미읽음 카운트, 일괄 읽음 처리(markAllReadByUserId), 토큰 해시 조회, 소프트 삭제 대응 조회 메서드를 제공합니다.
알림 및 푸시 토큰 비즈니스 로직
src/main/java/com/gachi/be/domain/notification/service/NotificationService.java
알림 조회(커서 페이징), 읽음 처리(단건/일괄), 미읽음 수 조회, 푸시 토큰 등록/삭제/갱신(SHA-256 해싱), 알림 생성(중복 제거, JSON 직렬화), 전달 이력 기록, 응답 변환, 페이로드 직렬화/역직렬화 및 문자열 정규화 유틸리티를 구현합니다.
REST API 엔드포인트
src/main/java/com/gachi/be/domain/notification/api/controller/NotificationController.java
/api/v1/notifications 기반의 알림 목록 조회, 미읽음 수 조회, 단건/일괄/전체 읽음 처리, 푸시 토큰 등록/삭제 엔드포인트를 제공하며, 모든 응답을 ApiResponse로 감싸고 인증 및 입력 검증을 적용합니다.
데이터베이스 스키마 및 응답 코드
src/main/resources/db/migration/V14__notification_api.sql, src/main/java/com/gachi/be/global/code/ErrorCode.java, src/main/java/com/gachi/be/global/code/SuccessCode.java
notifications, push_device_tokens, notification_delivery_logs 테이블을 생성하고 외래키 제약, 체크 제약, 유니크 인덱스(dedupe_key, 토큰 해시), 페이징 인덱스를 추가합니다. 에러 코드 NOTIFICATION_NOT_FOUND(NOTI4041)와 5개의 성공 코드를 정의합니다.
통합 테스트 및 검증
src/test/java/com/gachi/be/domain/notification/api/controller/NotificationControllerIntegrationTest.java
알림 인박스 조회(커서 페이징, 미읽음 필터, 읽음 동기화), 푸시 토큰 등록/삭제/재등록 흐름, 권한 검사(다른 사용자 알림 접근 거부)를 검증하는 통합 테스트를 포함합니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • Hminkyung
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive Java setup action 버전 업데이트와 메일 헬스체크 비활성화 설정은 배포/CI 개선사항이지만, PR의 핵심 알림 API 기능과는 직접 관련이 없습니다. CI/배포 설정 변경이 이슈 #70과의 연관성을 명확히 하거나, 별도 PR로 분리하는 것을 권장합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 메인 기능인 알림 API 구현을 명확하고 간결하게 표현하고 있습니다.
Linked Issues check ✅ Passed PR의 구현 내용이 이슈 #70의 모든 주요 요구 사항을 충족합니다. 알림 도메인 모델, API 엔드포인트(목록/미읽음/읽음 처리), 푸시 토큰 관리, 중복 방지(dedupeKey), 발송 결과 추적(delivery log), 에러 코드, 통합 테스트가 모두 구현되었습니다.
Description check ✅ Passed PR 설명서는 대부분의 필수 섹션을 포함하고 있으며, 작업 요약, 브랜치 정보, 체크리스트, 테스트 결과를 모두 기재했습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#70-notification-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/gachi/be/domain/notification/api/controller/NotificationController.java`:
- Around line 56-60: The controller exposes the size query param without a
Swagger default; update the NotificationController's size parameter to declare
the OpenAPI default so docs match runtime: add defaultValue="20" to the
`@RequestParam` on the Integer size parameter (keep the `@Min`(1)/@Max(100) and the
existing description and required=false as-is) — this aligns the controller spec
with NotificationService#normalizePageSize and the DEFAULT_PAGE_SIZE(20)
behavior.

In
`@src/main/java/com/gachi/be/domain/notification/dto/request/PushTokenRegisterRequest.java`:
- Line 11: The request indicates ambiguity about deviceId being optional and its
overwrite behavior: in PushTokenRegisterRequest (deviceId) either make it
required by adding `@NotBlank` to the field to force callers to always supply a
deviceId, or change the refresh(...) logic that sets PushDeviceToken.deviceId
(which currently uses normalizeOptional(request.deviceId())) to only overwrite
when the normalized value is non-null/non-empty so existing deviceId is
preserved; update the code paths that create/update tokens (registerPushToken,
deletePushToken, and the refresh method) to follow the chosen policy and ensure
normalizeOptional(request.deviceId()) is only used to assign when you intend to
replace the stored deviceId.

In `@src/main/java/com/gachi/be/domain/notification/entity/PushDeviceToken.java`:
- Around line 82-90: The refresh method updates token but not tokenHash, leaving
a stale hash; update refresh(PushPlatform platform, String token, String
deviceId, String appVersion) to also set tokenHash (either by recalculating from
the new token inside PushDeviceToken.refresh or by adding a tokenHash parameter
and assigning it) so that the field tokenHash is kept in sync whenever token is
updated; modify any callers if you choose the parameter approach and ensure
tokenHash is consistently used for token-based lookups.

In
`@src/main/java/com/gachi/be/domain/notification/service/NotificationService.java`:
- Around line 104-106: The code currently normalizes the push token but does not
validate emptiness, causing blank tokens to be hashed and stored/deleted; update
registerPushToken (and the corresponding unregister/delete method that also
calls normalizeRequired/sha256Hex) to trim and then reject empty tokens by
throwing a validation exception (e.g., IllegalArgumentException or your
service's ValidationException) with a clear message before computing
sha256Hex(token) or performing persistence/soft-delete so blank tokens cannot be
recorded or removed accidentally.

In
`@src/test/java/com/gachi/be/domain/notification/api/controller/NotificationControllerIntegrationTest.java`:
- Around line 40-60: Add integration tests to
NotificationControllerIntegrationTest to cover the suggested edge cases: create
tests (e.g., shouldReturnEmptyListWhenNoNotifications,
shouldHandleInvalidCursor, shouldIgnoreMarkReadForAlreadyReadNotification,
shouldValidateSizeBoundaries, shouldReturnEmptyWhenUnreadOnlyAndNone) that use
mockMvc (built in setUp), objectMapper, and jwtTokenProvider to call the
notification endpoints; seed/cleanup data via userRepository,
notificationRepository, and pushDeviceTokenRepository and assert proper HTTP
status and response bodies for empty lists, invalid cursor values, re-marking
read notifications, size values (0, 101, negative), and unreadOnly=true
returning empty results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4fcd890a-bb9d-4082-880f-7693a60a78b3

📥 Commits

Reviewing files that changed from the base of the PR and between d1d889a and 136501b.

📒 Files selected for processing (25)
  • src/main/java/com/gachi/be/domain/notification/api/controller/NotificationController.java
  • src/main/java/com/gachi/be/domain/notification/dto/request/NotificationReadRequest.java
  • src/main/java/com/gachi/be/domain/notification/dto/request/PushTokenDeleteRequest.java
  • src/main/java/com/gachi/be/domain/notification/dto/request/PushTokenRegisterRequest.java
  • src/main/java/com/gachi/be/domain/notification/dto/response/NotificationListResponse.java
  • src/main/java/com/gachi/be/domain/notification/dto/response/NotificationReadResponse.java
  • src/main/java/com/gachi/be/domain/notification/dto/response/NotificationResponse.java
  • src/main/java/com/gachi/be/domain/notification/dto/response/NotificationUnreadCountResponse.java
  • src/main/java/com/gachi/be/domain/notification/dto/response/PushTokenResponse.java
  • src/main/java/com/gachi/be/domain/notification/entity/Notification.java
  • src/main/java/com/gachi/be/domain/notification/entity/NotificationDeliveryLog.java
  • src/main/java/com/gachi/be/domain/notification/entity/PushDeviceToken.java
  • src/main/java/com/gachi/be/domain/notification/entity/enums/NotificationDeliveryStatus.java
  • src/main/java/com/gachi/be/domain/notification/entity/enums/NotificationType.java
  • src/main/java/com/gachi/be/domain/notification/entity/enums/PushPlatform.java
  • src/main/java/com/gachi/be/domain/notification/repository/NotificationDeliveryLogRepository.java
  • src/main/java/com/gachi/be/domain/notification/repository/NotificationRepository.java
  • src/main/java/com/gachi/be/domain/notification/repository/PushDeviceTokenRepository.java
  • src/main/java/com/gachi/be/domain/notification/service/NotificationCreateCommand.java
  • src/main/java/com/gachi/be/domain/notification/service/NotificationDeliveryResultCommand.java
  • src/main/java/com/gachi/be/domain/notification/service/NotificationService.java
  • src/main/java/com/gachi/be/global/code/ErrorCode.java
  • src/main/java/com/gachi/be/global/code/SuccessCode.java
  • src/main/resources/db/migration/V14__notification_api.sql
  • src/test/java/com/gachi/be/domain/notification/api/controller/NotificationControllerIntegrationTest.java

Comment thread src/main/java/com/gachi/be/domain/notification/entity/PushDeviceToken.java Outdated
Comment on lines +40 to +60
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class NotificationControllerIntegrationTest {
private static final AtomicInteger PHONE_SEQUENCE = new AtomicInteger(7000);

private final ObjectMapper objectMapper = new ObjectMapper();
private MockMvc mockMvc;

@Autowired private WebApplicationContext webApplicationContext;
@Autowired private UserRepository userRepository;
@Autowired private JwtTokenProvider jwtTokenProvider;
@Autowired private NotificationService notificationService;
@Autowired private NotificationRepository notificationRepository;
@Autowired private PushDeviceTokenRepository pushDeviceTokenRepository;

@BeforeEach
void setUp() {
mockMvc =
MockMvcBuilders.webAppContextSetup(webApplicationContext).apply(springSecurity()).build();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

추가 엣지 케이스 테스트를 고려해보세요.

현재 테스트는 주요 시나리오를 잘 커버하고 있습니다. 다음과 같은 엣지 케이스를 추가하면 더욱 견고해질 수 있습니다:

  • 빈 알림 목록 조회
  • 잘못된 cursor 값 처리
  • 이미 읽은 알림을 다시 읽기
  • size 경계값 (0, 101, 음수)
  • unreadOnly=true일 때 빈 결과

현재 구현도 충분히 좋지만, 프로덕션 안정성을 높이려면 이러한 케이스들을 추가로 검증하는 것이 도움이 될 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/gachi/be/domain/notification/api/controller/NotificationControllerIntegrationTest.java`
around lines 40 - 60, Add integration tests to
NotificationControllerIntegrationTest to cover the suggested edge cases: create
tests (e.g., shouldReturnEmptyListWhenNoNotifications,
shouldHandleInvalidCursor, shouldIgnoreMarkReadForAlreadyReadNotification,
shouldValidateSizeBoundaries, shouldReturnEmptyWhenUnreadOnlyAndNone) that use
mockMvc (built in setUp), objectMapper, and jwtTokenProvider to call the
notification endpoints; seed/cleanup data via userRepository,
notificationRepository, and pushDeviceTokenRepository and assert proper HTTP
status and response bodies for empty lists, invalid cursor values, re-marking
read notifications, size values (0, 101, negative), and unreadOnly=true
returning empty results.

@deli-minju deli-minju requested a review from Hminkyung May 26, 2026 13:12
Copy link
Copy Markdown
Collaborator

@Hminkyung Hminkyung left a comment

Choose a reason for hiding this comment

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

확인했씁니다!!!!! 고생하셨씁니다아아아아ㅏ

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/gachi/be/domain/notification/service/NotificationService.java (1)

110-131: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

토큰 refresh 시 기존 레코드가 갱신되지 않습니다.

현재 등록 경로는 userId + tokenHash로만 기존 토큰을 찾습니다. 푸시 토큰이 새 값으로 갱신되면 해시도 바뀌므로 기존 행을 절대 찾지 못하고 새 레코드를 추가하게 됩니다. 그 결과 이전 토큰이 enabled=true로 남아 이후 발송에서 중복 알림이나 불필요한 실패 로그를 만들 수 있습니다. deviceId 같은 안정적인 식별자로 기존 행을 먼저 찾아 refresh(...)하거나, 동일 기기의 이전 활성 토큰을 비활성화하는 경로가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/gachi/be/domain/notification/service/NotificationService.java`
around lines 110 - 131, The current lookup in NotificationService that uses
pushDeviceTokenRepository.findByUserIdAndTokenHash(...) fails to match when the
token (and its hash) changes; update the logic to first try finding an existing
PushDeviceToken by stable identifiers (e.g., userId + deviceId using
request.deviceId() via preserveExistingIfBlank/normalizeOptional) and call
existing.refresh(...) if found, otherwise fall back to the tokenHash lookup and
then to creation; additionally, ensure you deactivate or set enabled=false on
any other active tokens for the same userId+deviceId (use
pushDeviceTokenRepository to query and update other rows) so only the current
token remains active, while still using PushDeviceToken.builder() when creating
a truly new record.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/docker-be.yml:
- Line 43: 현재 워크플로에서 사용중인 actions/setup-java@v4 태그는 공급망 리스크가 있으니 태그 대신 해당 태그의 커밋
SHA c1e323688fd81a25caa38c78aa6df2d33d3e20d9으로 교체하세요; 구체적으로
`.github/workflows/docker-be.yml`의 uses: actions/setup-java@v4 항목을 SHA 핀으로 바꾸고,
동일한 파일의 uses: actions/checkout@v6 및 uses: gradle/actions/setup-gradle@v5 항목도 가능한
경우 각 태그의 대응 커밋 SHA로 핀하도록 업데이트하세요.

---

Outside diff comments:
In
`@src/main/java/com/gachi/be/domain/notification/service/NotificationService.java`:
- Around line 110-131: The current lookup in NotificationService that uses
pushDeviceTokenRepository.findByUserIdAndTokenHash(...) fails to match when the
token (and its hash) changes; update the logic to first try finding an existing
PushDeviceToken by stable identifiers (e.g., userId + deviceId using
request.deviceId() via preserveExistingIfBlank/normalizeOptional) and call
existing.refresh(...) if found, otherwise fall back to the tokenHash lookup and
then to creation; additionally, ensure you deactivate or set enabled=false on
any other active tokens for the same userId+deviceId (use
pushDeviceTokenRepository to query and update other rows) so only the current
token remains active, while still using PushDeviceToken.builder() when creating
a truly new record.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec77a4d0-a4a0-4432-8655-5628b1dad6ab

📥 Commits

Reviewing files that changed from the base of the PR and between 136501b and 6cda408.

📒 Files selected for processing (6)
  • .github/workflows/docker-be.yml
  • deploy/docker-compose.yml
  • src/main/java/com/gachi/be/domain/notification/api/controller/NotificationController.java
  • src/main/java/com/gachi/be/domain/notification/entity/PushDeviceToken.java
  • src/main/java/com/gachi/be/domain/notification/service/NotificationService.java
  • src/test/java/com/gachi/be/domain/notification/api/controller/NotificationControllerIntegrationTest.java

Comment thread .github/workflows/docker-be.yml Outdated
@deli-minju deli-minju merged commit b7575ef into develop May 26, 2026
3 checks passed
@deli-minju deli-minju deleted the feat/#70-notification-api branch May 26, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새로운 기능 추가 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 알림 API 구현

2 participants