Skip to content

feat: 인앱 알림 기능 추가#436

Merged
JanooGwan merged 10 commits intodevelopfrom
feat/cam-235
Mar 25, 2026
Merged

feat: 인앱 알림 기능 추가#436
JanooGwan merged 10 commits intodevelopfrom
feat/cam-235

Conversation

@JanooGwan
Copy link
Contributor

@JanooGwan JanooGwan commented Mar 23, 2026

🔍 개요

푸시 알림만 존재하던 기존 구조에 인앱 알림 기능을 추가합니다.
알림을 DB에 저장하여 앱 내에서 알림 목록 조회, 읽음 처리 등을 지원합니다.


🚀 주요 변경 내용

DB

  • notification_inbox 테이블 추가 (V59 Flyway)
    • user_id, type, title, body, path, is_read, created_at, updated_at
    • user_id, (user_id, is_read) 인덱스 추가

모델 / Enum

  • NotificationInboxType enum 추가 (CLUB_APPLICATION_SUBMITTED, CLUB_APPLICATION_APPROVED, CLUB_APPLICATION_REJECTED)
  • NotificationInbox 엔티티 추가

서비스

  • NotificationInboxService 추가
    • 인앱 알림 저장 (save)
    • 알림 목록 페이징 조회 (getMyInboxes)
    • 미읽음 개수 조회 (getUnreadCount)
    • 단건 읽음 처리 (markAsRead)
    • 전체 읽음 처리 (markAllAsRead)
  • NotificationService 수정: 동아리 지원/승인/거절 알림 발송 시 인앱 알림도 함께 저장

API

Method URL 설명
GET /notifications/inbox 인앱 알림 목록 페이징 조회
GET /notifications/inbox/unread-count 미읽음 알림 개수
PATCH /notifications/inbox/{id}/read 단건 읽음 처리
PATCH /notifications/inbox/read-all 전체 읽음 처리

에러 코드

  • NOT_FOUND_NOTIFICATION_INBOX 추가

💬 참고 사항

  • 인앱 알림 저장 실패는 예외를 catch하여 push 전송에 영향을 주지 않도록 처리
  • 현재 동아리 지원 관련 3가지 타입만 지원 (채팅 알림은 인앱 저장 미포함)
  • 알림 목록은 최신순 정렬, 페이지당 20개

✅ Checklist (완료 조건)

  • 코드 스타일 가이드 준수
  • 테스트 코드 포함됨
  • Reviewers / Assignees / Labels 지정 완료
  • 보안 및 민감 정보 검증 (API 키, 환경 변수, 개인정보 등)

@linear
Copy link

linear bot commented Mar 23, 2026

@coderabbitai
Copy link

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

사용자 알림 수신함 기능이 추가되었습니다. JPA 엔티티·리포지토리·서비스·컨트롤러·DTO, DB 마이그레이션 및 관련 통합 테스트가 도입되었고 NotificationService에 수신함 저장 호출이 통합되었습니다.

Changes

Cohort / File(s) Summary
엔티티 & 열거형
src/main/java/gg/agit/konect/domain/notification/model/NotificationInbox.java, src/main/java/gg/agit/konect/domain/notification/enums/NotificationInboxType.java
NotificationInbox JPA 엔티티(팩토리 메서드, markAsRead 포함) 및 알림 타입 enum 추가.
리포지토리
src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
사용자별 페이징 조회(생성시간+id 내림차순), 미확인 카운트, id+user 조회, 일괄 읽음 업데이트 JPQL(@Modifying) 및 getByIdAndUserId 기본 구현(없으면 NOT_FOUND 예외) 추가.
서비스
src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java, src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java
NotificationInboxService 추가(저장(REQUIRES_NEW), 페이징 조회, 미확인 카운트, 단건/전체 읽음). NotificationService에서 클럽 신청 관련 알림 전 푸시 전송 전에 inbox 저장 호출 삽입.
컨트롤러 & API
src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java, src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxController.java
/notifications/inbox API 인터페이스 및 구현 추가: 목록 조회, unread-count, 단건 읽음, 전체 읽음 엔드포인트(유효성/태깅 포함).
DTOs
src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxResponse.java, src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java, src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxUnreadCountResponse.java
단건/페이지 응답 및 미확인 카운트 레코드 DTO와 정적 팩토리 메서드 추가.
글로벌 코드 & 마이그레이션
src/main/java/gg/agit/konect/global/code/ApiResponseCode.java, src/main/resources/db/migration/V59__add_notification_inbox_table.sql, src/main/resources/db/migration/V60__fix_notification_inbox_table.sql, src/main/resources/db/migration/V61__update_notification_inbox_index.sql
NOT_FOUND_NOTIFICATION_INBOX 응답 코드 추가. notification_inbox 테이블 생성, 타임스탬프/인덱스 수정 및 복합 인덱스(V61) 추가 마이그레이션.
이벤트·적용 흐름 변경
src/main/java/gg/agit/konect/domain/club/event/ClubApplicationSubmittedEvent.java, src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java, src/main/java/gg/agit/konect/domain/notification/listener/ClubApplicationNotificationListener.java
ClubApplicationSubmittedEvent 수신자 타입을 단일 ID→ID 목록으로 변경. applyClub에서 매니저 집합에 이벤트를 발행하도록 변경하고, 리스너는 각 수신자에 대해 개별 알림 전송 호출하도록 수정.
테스트
src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java
인티그레이션 테스트 추가: 목록 조회(정렬/페이징/소유권), unread-count, 단건/전체 읽음 흐름 및 404 검증.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as NotificationInboxController
    participant Service as NotificationInboxService
    participant Repo as NotificationInboxRepository
    participant DB as Database

    Client->>Controller: GET /notifications/inbox?page=1
    Controller->>Service: getMyInboxes(userId, page)
    Service->>Repo: findAllByUserIdOrderByCreatedAtDescIdDesc(userId, pageable)
    Repo->>DB: SELECT ... FROM notification_inbox WHERE user_id=? ORDER BY created_at DESC, id DESC
    DB-->>Repo: Page<NotificationInbox>
    Repo-->>Service: Page<NotificationInbox>
    Service->>Service: NotificationInboxesResponse.from(page)
    Service-->>Controller: NotificationInboxesResponse
    Controller-->>Client: 200 OK
Loading
sequenceDiagram
    participant Client
    participant Controller as NotificationInboxController
    participant Service as NotificationInboxService
    participant Repo as NotificationInboxRepository
    participant DB as Database

    Client->>Controller: PATCH /notifications/inbox/{id}/read
    Controller->>Service: markAsRead(userId, notificationId)
    Service->>Repo: getByIdAndUserId(notificationId, userId)
    Repo->>DB: SELECT * FROM notification_inbox WHERE id=? AND user_id=?
    DB-->>Repo: NotificationInbox
    Repo-->>Service: NotificationInbox
    Service->>Service: inbox.markAsRead()
    Service->>DB: UPDATE notification_inbox SET is_read=true,...
    DB-->>Service: OK
    Service-->>Controller: void
    Controller-->>Client: 200 OK
Loading
sequenceDiagram
    participant EventProducer
    participant NotificationService
    participant InboxService as NotificationInboxService
    participant Repo as NotificationInboxRepository
    participant DB as Database
    participant PushSender

    EventProducer->>NotificationService: sendClubApplicationSubmittedNotification(...)
    NotificationService->>InboxService: save(receiverId, CLUB_APPLICATION_SUBMITTED, title, body, path)
    InboxService->>Repo: save(NotificationInbox)
    Repo->>DB: INSERT INTO notification_inbox ...
    DB-->>Repo: OK
    NotificationService->>PushSender: sendNotification(receiverId, title, body, path)
    PushSender-->>NotificationService: OK
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 깡총, 알림함이 도착했네, 토끼가 기뻐 뛰네
쌓인 소식 하나씩, 읽음 표시로 반짝반짝
DB에 저장하고 푸시도 보냈지, 순서도 반듯하게
개발자의 당근 한 조각, 코드에 숨은 소확행 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 주요 변경 사항을 명확하게 설명하고 있습니다. 인앱 알림 기능 추가가 이 PR의 핵심 목표이며, 제목이 이를 정확하게 반영하고 있습니다.
Description check ✅ Passed 설명이 변경 사항과 직접적으로 관련이 있습니다. DB 테이블 추가, 모델/Enum, 서비스, API 엔드포인트 등 모든 주요 변경 사항을 자세히 설명하고 있습니다.

✏️ 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/cam-235

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.

@JanooGwan JanooGwan self-assigned this Mar 23, 2026
@JanooGwan JanooGwan added the 기능 새로운 기능을 개발합니다. label Mar 23, 2026
@JanooGwan JanooGwan changed the title [CAM-235] 인앱 알림 기능 추가 feat: 인앱 알림 기능 추가 Mar 23, 2026
@JanooGwan JanooGwan requested a review from Copilot March 23, 2026 05:49
Copy link

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java`:
- Around line 22-25: page 파라미터에 대한 입력 검증이 빠져 음수 값으로 요청 시 PageRequest.of(...)에서
IllegalArgumentException(500)이 발생하므로, 인터페이스 메서드
NotificationInboxApi.getMyInboxes의 page 파라미터에
javax.validation.constraints.Min(value = 0) 애노테이션을 추가하고, 해당 구현 클래스
NotificationInboxController에 Spring의 `@Validated를` 붙여 요청 바인딩 시 검증이 트리거되도록 수정하세요;
또한 필요한 경우 import를 추가하고 기존 전역/컨트롤러 예외처리 핸들러가 검증 실패를 400으로 매핑하도록 확인하세요.

In
`@src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java`:
- Around line 25-27: Update the `@Modifying` annotation on markAllAsReadByUserId
in NotificationInboxRepository to include clearAutomatically = true so the
persistence context is automatically cleared after the bulk UPDATE of
NotificationInbox entities; this prevents stale in-memory entities in the same
transaction after the bulk operation.

In `@src/main/resources/db/migration/V59__add_notification_inbox_table.sql`:
- Around line 13-14: Add a composite index to avoid filesort when querying by
user and ordering by created_at: create the index named
idx_notification_inbox_user_id_created_at on (user_id, created_at) in the
migration V59__add_notification_inbox_table.sql so that queries for
/notifications/inbox filtering by user_id and ordering by created_at use the
index; ensure the index name matches idx_notification_inbox_user_id_created_at
and that it is added alongside the existing single-column indexes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 01227445-8ef2-4518-963d-cd872c349ce1

📥 Commits

Reviewing files that changed from the base of the PR and between 6eff288 and 3588f64.

📒 Files selected for processing (12)
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxController.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxResponse.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxUnreadCountResponse.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java
  • src/main/java/gg/agit/konect/domain/notification/enums/NotificationInboxType.java
  • src/main/java/gg/agit/konect/domain/notification/model/NotificationInbox.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java
  • src/main/java/gg/agit/konect/global/code/ApiResponseCode.java
  • src/main/resources/db/migration/V59__add_notification_inbox_table.sql
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/global/code/ApiResponseCode.java
  • src/main/java/gg/agit/konect/domain/notification/enums/NotificationInboxType.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxUnreadCountResponse.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxResponse.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxController.java
  • src/main/java/gg/agit/konect/domain/notification/model/NotificationInbox.java
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/java/gg/agit/konect/global/code/ApiResponseCode.java
  • src/main/java/gg/agit/konect/domain/notification/enums/NotificationInboxType.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxUnreadCountResponse.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxResponse.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java
  • src/main/resources/db/migration/V59__add_notification_inbox_table.sql
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxController.java
  • src/main/java/gg/agit/konect/domain/notification/model/NotificationInbox.java
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java
src/main/resources/db/migration/**/*.sql

⚙️ CodeRabbit configuration file

src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:

  • 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
  • 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
  • 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.

Files:

  • src/main/resources/db/migration/V59__add_notification_inbox_table.sql
🔇 Additional comments (4)
src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxController.java (1)

12-40: LGTM!

컨트롤러가 서비스 레이어에 깔끔하게 위임하는 표준 패턴을 따르고 있습니다.

src/main/java/gg/agit/konect/domain/notification/model/NotificationInbox.java (1)

23-83: LGTM!

엔티티 설계가 프로젝트 컨벤션을 잘 따르고 있습니다. @ManyToOne(fetch = LAZY) 사용과 @Enumerated(STRING)으로 enum ordinal 변경에 안전합니다.

src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java (1)

18-62: LGTM!

서비스 레이어가 트랜잭션 경계를 적절히 설정하고 있습니다. save() 메서드의 예외 처리는 푸시 알림 전송에 영향을 주지 않도록 의도된 설계로 이해됩니다.

src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java (1)

17-33: LGTM (나머지 부분)

파생 쿼리 메서드들과 getByIdAndUserId 기본 메서드의 예외 처리 패턴이 프로젝트 컨벤션에 맞게 잘 구현되어 있습니다.

Comment on lines +13 to +14
INDEX idx_notification_inbox_user_id (user_id),
INDEX idx_notification_inbox_user_id_is_read (user_id, is_read),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

[LEVEL: medium] 목록 조회 정렬용 복합 인덱스가 없어 성능 저하가 발생할 수 있습니다.
현재 인덱스 구성으로는 user_id 조건 + created_at 최신순 페이지 조회에서 filesort가 발생할 수 있습니다.
알림 데이터가 누적되면 /notifications/inbox 응답 시간이 점진적으로 증가해 체감 지연이 커질 수 있습니다.
As per coding guidelines '보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.', INDEX idx_notification_inbox_user_id_created_at (user_id, created_at)를 추가해 조회 경로를 고정해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/db/migration/V59__add_notification_inbox_table.sql` around
lines 13 - 14, Add a composite index to avoid filesort when querying by user and
ordering by created_at: create the index named
idx_notification_inbox_user_id_created_at on (user_id, created_at) in the
migration V59__add_notification_inbox_table.sql so that queries for
/notifications/inbox filtering by user_id and ordering by created_at use the
index; ensure the index name matches idx_notification_inbox_user_id_created_at
and that it is added alongside the existing single-column indexes.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

푸시 알림 중심이던 기존 알림 구조에 DB 기반 인앱 알림(inbox) 을 도입해, 앱 내에서 알림 목록 조회/미읽음 개수/읽음 처리를 가능하게 하는 변경입니다. 동아리 지원(제출/승인/거절) 푸시 발송 시 인앱 알림도 함께 저장하도록 연동합니다.

Changes:

  • notification_inbox 테이블(Flyway V59) 및 인앱 알림 도메인(엔티티/enum/repository/service) 추가
  • 인앱 알림 API(목록/미읽음 개수/단건 읽음/전체 읽음) 신규 추가
  • 동아리 지원 관련 푸시 전송 시 인앱 알림 저장 로직을 NotificationService에 연동, 에러코드 추가

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/main/resources/db/migration/V59__add_notification_inbox_table.sql 인앱 알림 테이블/인덱스/외래키 추가
src/main/java/gg/agit/konect/global/code/ApiResponseCode.java 인앱 알림 조회 실패용 에러 코드 추가
src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java 동아리 지원/승인/거절 푸시 발송 시 inbox 저장 연동
src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java inbox 저장/조회/읽음 처리 비즈니스 로직 추가
src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java inbox 조회/카운트/읽음 처리용 쿼리 추가
src/main/java/gg/agit/konect/domain/notification/model/NotificationInbox.java inbox 엔티티 추가 (BaseEntity 상속)
src/main/java/gg/agit/konect/domain/notification/enums/NotificationInboxType.java inbox 타입 enum 추가
src/main/java/gg/agit/konect/domain/notification/dto/*.java inbox 응답 DTO 추가
src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java inbox API 스펙 정의
src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxController.java inbox API 구현 컨트롤러 추가

Comment on lines +29 to +37
@Transactional
public void save(Integer userId, NotificationInboxType type, String title, String body, String path) {
try {
User user = userRepository.getById(userId);
NotificationInbox inbox = NotificationInbox.of(user, type, title, body, path);
notificationInboxRepository.save(inbox);
} catch (Exception e) {
log.error("Failed to save notification inbox: userId={}, type={}", userId, type, e);
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

save() 메서드가 @transactional이라 실제 커밋은 메서드 반환 이후에 수행됩니다. 따라서 try/catch로 본문 예외는 잡더라도 커밋 시점(예: 제약조건 위반, 커넥션 문제 등)에 발생하는 예외는 이 블록에서 잡히지 않아, 호출한 푸시 전송 로직까지 전파될 수 있습니다. 인앱 알림 저장 실패가 푸시에 영향을 주지 않게 하려면 (1) 이 메서드의 @transactional을 제거해 repository.save의 트랜잭션 경계(커밋 포함)를 try/catch로 감싸거나, (2) TransactionTemplate 등으로 커밋까지 포함해 예외를 처리하거나, (3) 호출부에서 save 호출을 별도 try/catch로 감싸는 방식으로 구조를 조정해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +24

@Tag(name = "(Normal) Notification: 알림", description = "알림 API")
@RequestMapping("/notifications/inbox")
public interface NotificationInboxApi {

@Operation(summary = "인앱 알림 목록을 조회한다.")
@GetMapping
ResponseEntity<NotificationInboxesResponse> getMyInboxes(
@UserId Integer userId,
@RequestParam(defaultValue = "0") int page
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

현재 page 파라미터가 0부터 시작(defaultValue="0")인데, 코드베이스의 다른 페이지네이션 API들은 page=1부터 시작하고(@min(1)) 서비스에서 PageRequest.of(page - 1, ...)로 변환하고 있습니다(예: NoticeApi, ChatApi). 클라이언트/응답 currentPage 기준 혼선을 막기 위해 여기에서도 page를 1-based로 통일하고, @Min 검증을 추가해 주세요(검증을 쓰려면 컨트롤러에 @validated도 필요합니다).

Suggested change
@Tag(name = "(Normal) Notification: 알림", description = "알림 API")
@RequestMapping("/notifications/inbox")
public interface NotificationInboxApi {
@Operation(summary = "인앱 알림 목록을 조회한다.")
@GetMapping
ResponseEntity<NotificationInboxesResponse> getMyInboxes(
@UserId Integer userId,
@RequestParam(defaultValue = "0") int page
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
@Tag(name = "(Normal) Notification: 알림", description = "알림 API")
@RequestMapping("/notifications/inbox")
@Validated
public interface NotificationInboxApi {
@Operation(summary = "인앱 알림 목록을 조회한다.")
@GetMapping
ResponseEntity<NotificationInboxesResponse> getMyInboxes(
@UserId Integer userId,
@RequestParam(defaultValue = "1") @Min(1) int page

Copilot uses AI. Check for mistakes.
}

public NotificationInboxesResponse getMyInboxes(Integer userId, int page) {
PageRequest pageable = PageRequest.of(page, DEFAULT_PAGE_SIZE);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

PageRequest.of(page, ...)로 0-based 페이지를 그대로 사용하고 있고, 응답 DTO도 page.getNumber()(0-based)를 그대로 내려줍니다. 다른 API들이 1-based page를 사용하므로, API 설계 일관성을 위해 page 입력/응답을 1-based로 맞추고 여기서는 PageRequest.of(page - 1, ...) 형태로 변환하는 쪽이 안전합니다.

Suggested change
PageRequest pageable = PageRequest.of(page, DEFAULT_PAGE_SIZE);
PageRequest pageable = PageRequest.of(Math.max(0, page - 1), DEFAULT_PAGE_SIZE);

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +26
@Modifying
@Query("UPDATE NotificationInbox n SET n.isRead = true WHERE n.user.id = :userId AND n.isRead = false")
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

bulk update(markAllAsReadByUserId)는 엔티티 리스너(Auditing) 기반 updatedAt 갱신이 동작하지 않아 updated_at이 변경되지 않습니다. 또한 영속성 컨텍스트에 이미 로딩된 NotificationInbox가 있으면 상태가 stale해질 수 있습니다. @Modifying(clearAutomatically = true, flushAutomatically = true) 적용과 함께 JPQL에서 updatedAt도 함께 갱신하도록 수정(예: n.updatedAt = CURRENT_TIMESTAMP)하는 방식을 고려해 주세요.

Suggested change
@Modifying
@Query("UPDATE NotificationInbox n SET n.isRead = true WHERE n.user.id = :userId AND n.isRead = false")
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE NotificationInbox n SET n.isRead = true, n.updatedAt = CURRENT_TIMESTAMP " +
"WHERE n.user.id = :userId AND n.isRead = false")

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +11
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

migration에서 created_at/updated_at을 DATETIME(6)로 두고 기본값/ON UPDATE가 없어, BaseEntity(@column columnDefinition="TIMESTAMP") 및 기존 테이블들(TIMESTAMP DEFAULT CURRENT_TIMESTAMP ...)과 스키마 타입/동작이 불일치합니다. Hibernate validate 사용 시 실패할 수 있고, DB에서 updated_at 자동 갱신도 되지 않습니다. TIMESTAMP(6) + DEFAULT CURRENT_TIMESTAMP / ON UPDATE CURRENT_TIMESTAMP 등 기존 패턴과 맞추는 쪽으로 수정해 주세요.

Suggested change
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),

Copilot uses AI. Check for mistakes.
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
INDEX idx_notification_inbox_user_id (user_id),
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

목록 조회 쿼리가 WHERE user_id = ? ORDER BY created_at DESC 형태가 될 텐데, 현재 인덱스는 (user_id)만 있어 정렬 시 filesort가 발생할 수 있습니다. 페이징 성능을 위해 (user_id, created_at DESC) 복합 인덱스 추가를 고려해 주세요(기존 user_id 단일 인덱스는 대체 가능).

Suggested change
INDEX idx_notification_inbox_user_id (user_id),
INDEX idx_notification_inbox_user_id_created_at (user_id, created_at DESC),

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +23
public static NotificationInboxesResponse from(Page<NotificationInbox> page) {
return new NotificationInboxesResponse(
page.getContent().stream().map(NotificationInboxResponse::from).toList(),
page.getNumber(),
page.getTotalPages(),
page.getTotalElements(),
page.hasNext()
);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

currentPagepage.getNumber()로 내려주면 0-based 값이 반환됩니다. 다른 페이지네이션 응답들은 1-based로 내려주는 패턴이 있어(예: ChatMessagePageResponse에서 messages.getNumber()+1), page 정책을 1-based로 통일한다면 여기 응답도 1-based로 보정해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
public interface NotificationInboxRepository extends JpaRepository<NotificationInbox, Integer> {

Page<NotificationInbox> findAllByUserIdOrderByCreatedAtDesc(Integer userId, Pageable pageable);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

이 프로젝트의 대부분 Repository들은 org.springframework.data.repository.Repository를 확장하면서 필요한 메서드만 노출하는 패턴을 사용합니다(예: NotificationDeviceTokenRepository, AdvertisementRepository 등). 여기서는 JpaRepository의 추가 기능을 사용하지 않으므로, 일관성과 불필요한 API 노출을 줄이기 위해 동일한 패턴으로 맞추는 것을 검토해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +39
@RestController
@RequiredArgsConstructor
@RequestMapping("/notifications/inbox")
public class NotificationInboxController implements NotificationInboxApi {

private final NotificationInboxService notificationInboxService;

@Override
public ResponseEntity<NotificationInboxesResponse> getMyInboxes(Integer userId, int page) {
return ResponseEntity.ok(notificationInboxService.getMyInboxes(userId, page));
}

@Override
public ResponseEntity<NotificationInboxUnreadCountResponse> getUnreadCount(Integer userId) {
return ResponseEntity.ok(notificationInboxService.getUnreadCount(userId));
}

@Override
public ResponseEntity<Void> markAsRead(Integer userId, Integer notificationId) {
notificationInboxService.markAsRead(userId, notificationId);
return ResponseEntity.ok().build();
}

@Override
public ResponseEntity<Void> markAllAsRead(Integer userId) {
notificationInboxService.markAllAsRead(userId);
return ResponseEntity.ok().build();
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

notification 도메인에 이미 통합 테스트(NotificationApiTest)가 있고, 이번 PR은 새로운 인앱 알림 조회/읽음 처리 API를 추가합니다. 회귀를 막기 위해 /notifications/inbox, /unread-count, 단건/전체 읽음 처리에 대한 통합 테스트(정렬/페이징, 사용자 소유권 검증, 읽음 처리 후 unreadCount 변화 등)를 추가해 주세요.

Copilot uses AI. Check for mistakes.
Copy link

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java`:
- Around line 27-30: The JPQL update in
NotificationInboxRepository.markAllAsReadByUserId redundantly sets n.updatedAt =
CURRENT_TIMESTAMP while the DB already has ON UPDATE CURRENT_TIMESTAMP (V60
migration); remove the updatedAt assignment from the `@Query` so it only updates
n.isRead, i.e. change the Query in NotificationInboxRepository (method
markAllAsReadByUserId) to "UPDATE NotificationInbox n SET n.isRead = true WHERE
n.user.id = :userId AND n.isRead = false" and keep the `@Modifying` annotations
as-is so the DB trigger manages updatedAt.

In
`@src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java`:
- Around line 29-37: The save method in NotificationInboxService is currently
under a class-level `@Transactional`(readOnly = true) which prevents DB writes;
annotate the save(Integer userId, NotificationInboxType type, String title,
String body, String path) method with `@Transactional` (no readOnly) to allow
commits, and stop swallowing errors — either remove the broad try/catch or
rethrow the exception after logging so transaction rollback can occur; refer to
NotificationInboxService.save, the class-level `@Transactional`(readOnly = true),
userRepository.getById, and notificationInboxRepository.save when making the
change.

In `@src/main/resources/db/migration/V60__fix_notification_inbox_table.sql`:
- Around line 5-6: The migration adds an index with "created_at DESC"
(idx_notification_inbox_user_id_created_at via ALTER TABLE notification_inbox)
which requires MySQL 8.0+; update project documentation and build configuration
to state the minimum DB version is MySQL 8.0 (e.g., add a note to the
README/operational docs and set the minimum MySQL version in build.gradle or a
setup checklist) so consumers know DESC indexes are supported and the migration
behaves as intended.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49b97aa6-1c09-4f4e-af24-3eafb0cb3840

📥 Commits

Reviewing files that changed from the base of the PR and between 3588f64 and 792855c.

📒 Files selected for processing (5)
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/resources/db/migration/V60__fix_notification_inbox_table.sql
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/main/resources/db/migration/**/*.sql

⚙️ CodeRabbit configuration file

src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:

  • 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
  • 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
  • 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.

Files:

  • src/main/resources/db/migration/V60__fix_notification_inbox_table.sql
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/resources/db/migration/V60__fix_notification_inbox_table.sql
  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java
🔇 Additional comments (3)
src/main/java/gg/agit/konect/domain/notification/dto/NotificationInboxesResponse.java (1)

9-24: LGTM!

페이지네이션 DTO 구조가 적절하며, 1-based 페이지 번호 변환이 API 스펙과 일관성 있게 구현되었습니다.

src/main/java/gg/agit/konect/domain/notification/controller/NotificationInboxApi.java (1)

18-28: LGTM!

@Validated@Min(1) 검증이 적용되어 이전 리뷰에서 지적된 음수 페이지 요청 문제가 해결되었습니다.

src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java (1)

39-60: LGTM!

읽기 전용 조회 메서드와 쓰기 트랜잭션이 필요한 메서드에 적절한 @Transactional 설정이 되어 있습니다. 1-based 페이지 변환 로직도 API 스펙과 일관성 있게 구현되었습니다.

Comment on lines +5 to +6
ALTER TABLE notification_inbox
ADD INDEX idx_notification_inbox_user_id_created_at (user_id, created_at DESC);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

MySQL descending index support version

💡 Result:

MySQL supports descending indexes starting from version 8.0. In prior versions (such as 5.7 and earlier), the DESC keyword in index definitions was parsed but ignored, with all indexes stored in ascending order. Descending indexes allow InnoDB to store key values in descending order, enabling more efficient forward scans for DESC ORDER BY clauses and supporting mixed ASC/DESC multi-column indexes to avoid filesorts. This feature was introduced in MySQL 8.0 labs releases around 2016 and is fully supported in the stable MySQL 8.0 series (GA from 8.0.11 in April 2018). It is implemented via WL#1074 and documented in the official MySQL 8.0+ Reference Manual. Key details: - Supported only for InnoDB BTREE indexes. - Not supported for FULLTEXT, SPATIAL, or HASH indexes. - Syntax: CREATE INDEX idx_name ON tbl (col1 ASC, col2 DESC); As of 2026-03-23, this remains supported in MySQL 8.4 and later.

Citations:


🏁 Script executed:

# First, find and read the full migration file
find . -name "V60__fix_notification_inbox_table.sql" -type f 2>/dev/null | head -5

Repository: BCSDLab/KONECT_BACK_END

Length of output: 138


🏁 Script executed:

# Search for MySQL version configuration/documentation in the project
fd -t f "(pom.xml|build.gradle|application.properties|application.yml|README.md|docker-compose.yml)" | head -20 | xargs grep -l "mysql\|MySQL\|version" 2>/dev/null | head -10

Repository: BCSDLab/KONECT_BACK_END

Length of output: 79


🏁 Script executed:

# Check if there are other migration files to understand the MySQL version context
ls -la src/main/resources/db/migration/*.sql 2>/dev/null | tail -20

Repository: BCSDLab/KONECT_BACK_END

Length of output: 2517


🏁 Script executed:

# Read the full V60 migration file
cat -n src/main/resources/db/migration/V60__fix_notification_inbox_table.sql

Repository: BCSDLab/KONECT_BACK_END

Length of output: 533


🏁 Script executed:

# Check build.gradle for MySQL/database version configuration
cat build.gradle | grep -A 20 -B 5 "mysql\|MySQL\|database" -i

Repository: BCSDLab/KONECT_BACK_END

Length of output: 1862


🏁 Script executed:

# Also check if there's a docker-compose or other config specifying MySQL version
find . -name "docker-compose*" -o -name "docker-compose.yml" 2>/dev/null | xargs cat 2>/dev/null | head -50

Repository: BCSDLab/KONECT_BACK_END

Length of output: 1476


🏁 Script executed:

# Search for MySQL version specification in docker-compose files more thoroughly
find . -name "docker-compose*" -type f 2>/dev/null | xargs ls -la

Repository: BCSDLab/KONECT_BACK_END

Length of output: 147


🏁 Script executed:

# Check application.properties or application.yml files
find . -name "application*.properties" -o -name "application*.yml" 2>/dev/null | xargs cat 2>/dev/null | head -100

Repository: BCSDLab/KONECT_BACK_END

Length of output: 2338


🏁 Script executed:

# Check README or docs for MySQL version requirements
find . -name "README*" -o -name "SETUP*" -o -name "*.md" 2>/dev/null | head -10 | xargs cat 2>/dev/null | grep -i "mysql\|version\|requirement" -A 3 -B 3

Repository: BCSDLab/KONECT_BACK_END

Length of output: 49


🏁 Script executed:

# Check the previous migration V59 to understand the context
cat -n src/main/resources/db/migration/V59__add_notification_inbox_table.sql

Repository: BCSDLab/KONECT_BACK_END

Length of output: 820


[LEVEL: 낮음] MySQL 8.0+ 버전 요구사항을 문서화하세요.

DESC 인덱스는 MySQL 8.0 이상에서만 지원됩니다. MySQL 5.7 이하에서는 DESC 키워드가 무시되어 인덱스가 ASC로 생성되므로, ORDER BY created_at DESC 쿼리에서 역순 스캔의 성능 저하가 발생합니다. 현재 프로젝트의 MySQL 버전 요구사항이 명시되어 있지 않으므로, 문서 또는 build.gradle에 최소 MySQL 8.0 이상을 명시해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/db/migration/V60__fix_notification_inbox_table.sql` around
lines 5 - 6, The migration adds an index with "created_at DESC"
(idx_notification_inbox_user_id_created_at via ALTER TABLE notification_inbox)
which requires MySQL 8.0+; update project documentation and build configuration
to state the minimum DB version is MySQL 8.0 (e.g., add a note to the
README/operational docs and set the minimum MySQL version in build.gradle or a
setup checklist) so consumers know DESC indexes are supported and the migration
behaves as intended.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines +19 to +38
@Override
public ResponseEntity<NotificationInboxesResponse> getMyInboxes(Integer userId, int page) {
return ResponseEntity.ok(notificationInboxService.getMyInboxes(userId, page));
}

@Override
public ResponseEntity<NotificationInboxUnreadCountResponse> getUnreadCount(Integer userId) {
return ResponseEntity.ok(notificationInboxService.getUnreadCount(userId));
}

@Override
public ResponseEntity<Void> markAsRead(Integer userId, Integer notificationId) {
notificationInboxService.markAsRead(userId, notificationId);
return ResponseEntity.ok().build();
}

@Override
public ResponseEntity<Void> markAllAsRead(Integer userId) {
notificationInboxService.markAllAsRead(userId);
return ResponseEntity.ok().build();
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

새로 추가된 인앱 알림 API(목록/미읽음 개수/단건 읽음/전체 읽음)와 서비스 로직에 대한 테스트가 현재 PR에 포함되어 있지 않습니다. 기존에 NotificationApiTest 등 알림 도메인 통합 테스트가 있는 만큼, 최소한 목록 페이징(1-based page), 소유자 검증(다른 유저 id로 markAsRead 시 404), markAllAsRead 동작을 커버하는 테스트를 추가하는 게 안전합니다.

Copilot uses AI. Check for mistakes.
Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java`:
- Around line 47-64: getMyInboxesSuccess relies on millisecond precision
timestamps so two createInbox(user, ...) calls can yield identical createdAt and
cause flaky ordering; make the test deterministic by inserting a short pause or
setting distinct timestamps between the two creations (e.g., call
Thread.sleep(1) or enhance the createInbox test helper to accept and set
createdAt) before clearPersistenceContext(), so the assertions on
notifications[0]/[1] are stable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 90d8b0d4-8a31-441c-a74e-af5b0352547d

📥 Commits

Reviewing files that changed from the base of the PR and between 792855c and 517e638.

📒 Files selected for processing (3)
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
🔇 Additional comments (4)
src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java (1)

17-35: LGTM!

리포지토리 구현이 적절합니다. @Modifying(clearAutomatically = true, flushAutomatically = true) 설정으로 벌크 업데이트 후 영속성 컨텍스트 관리가 올바르게 처리되고, getByIdAndUserId 기본 메서드로 not-found 예외 처리가 일관되게 구현되어 있습니다.

src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java (2)

29-38: LGTM!

@Transactional 어노테이션이 추가되어 쓰기 트랜잭션이 올바르게 설정되었습니다. 예외를 catch하여 로깅만 하는 패턴은 PR 목적에 명시된 대로 푸시 알림 전송에 영향을 주지 않기 위한 의도적인 설계입니다.


40-61: LGTM!

읽기 전용 메서드(getMyInboxes, getUnreadCount)는 클래스 레벨 readOnly = true 설정을 따르고, 쓰기 메서드(markAsRead, markAllAsRead)는 개별 @Transactional로 올바르게 트랜잭션 경계가 설정되어 있습니다.

src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java (1)

107-224: LGTM!

미읽음 개수 조회, 단건 읽음 처리, 전체 읽음 처리 테스트가 잘 구성되어 있습니다. 특히 다른 사용자의 알림에 대한 접근 제어(404 반환)와 전체 읽음 처리 시 다른 사용자의 알림이 영향받지 않는 것을 검증하는 테스트가 포함되어 있어 보안 시나리오도 커버됩니다.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comment on lines 315 to 319
String body = applicantName + "님이 동아리 가입을 신청했어요.";
String path = "mypage/manager/" + clubId + "/applications/" + applicationId;
notificationInboxService.save(
receiverId, NotificationInboxType.CLUB_APPLICATION_SUBMITTED, clubName, body, path);
sendNotification(receiverId, clubName, body, path);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

NotificationInbox 저장 실패가 push 전송에 영향을 주지 않게 하려면, 현재처럼 NotificationInboxService 내부에서 try/catch 하는 방식만으로는 부족할 수 있습니다. @Transactional 메서드의 커밋/flush 단계에서 예외가 발생하면(save 호출 시점이 아니라 메서드 종료 시점) 이 catch로 잡히지 않고 전파되어 sendNotification까지 도달하지 못할 수 있습니다. NotificationService에서 notificationInboxService.save 호출을 별도 try/catch로 감싸거나, save 구현을 TransactionTemplate/REQUIRES_NEW + 명시적 flush 등으로 커밋 예외까지 이 메서드 내부에서 처리되도록 변경해 주세요.

Copilot uses AI. Check for mistakes.

NotificationInbox save(NotificationInbox notificationInbox);

Page<NotificationInbox> findAllByUserIdOrderByCreatedAtDesc(Integer userId, Pageable pageable);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

최신순 정렬을 createdAt DESC만으로 하면, created_at이 초 단위(TIMESTAMP)로 저장되는 환경에서는 같은 초에 생성된 알림들의 순서가 비결정적일 수 있습니다(페이징 시 중복/누락처럼 보이는 현상도 가능). 정렬에 id DESC 같은 타이브레이커를 추가해서 항상 결정적인 순서를 보장해 주세요.

Suggested change
Page<NotificationInbox> findAllByUserIdOrderByCreatedAtDesc(Integer userId, Pageable pageable);
Page<NotificationInbox> findAllByUserIdOrderByCreatedAtDescIdDesc(Integer userId, Pageable pageable);

Copilot uses AI. Check for mistakes.
MODIFY COLUMN updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

ALTER TABLE notification_inbox
ADD INDEX idx_notification_inbox_user_id_created_at (user_id, created_at DESC);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

알림 목록을 (created_at DESC, id DESC)처럼 결정적으로 정렬하려면, 인덱스도 (user_id, created_at, id) 형태로 맞춰주는 편이 페이징 성능/정렬 비용 측면에서 유리합니다. 현재 (user_id, created_at DESC)만으로는 created_at이 동일한 행들에 대해 추가 정렬이 필요할 수 있으니, 정렬 기준을 확정한 뒤 인덱스 컬럼 구성도 함께 조정해 주세요.

Suggested change
ADD INDEX idx_notification_inbox_user_id_created_at (user_id, created_at DESC);
ADD INDEX idx_notification_inbox_user_id_created_at (user_id, created_at DESC, id DESC);

Copilot uses AI. Check for mistakes.
Copy link

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java`:
- Around line 41-46: The getMyInboxes method in NotificationInboxService
currently calls PageRequest.of(page - 1, ...) without guarding against page <=
0; add a defensive check at the start of NotificationInboxService.getMyInboxes
to validate the page parameter (e.g., if page == null or page < 1) and either
normalize it to 1 or throw a clear IllegalArgumentException; update usage around
PageRequest.of(...) so it always receives a non-negative page index and include
a brief error message referencing the page parameter when throwing.
- Around line 30-39: The save() method in NotificationInboxService currently
swallows all exceptions, so add a metric counter to track failures: inject a
MeterRegistry (or a Counter) into NotificationInboxService, create a Counter
named e.g. "inbox_save_failure_total" (optionally tagged by type or result), and
increment that counter inside the catch block before logging; keep the existing
log but ensure the counter is incremented for every exception thrown by
userRepository.getById(...) or notificationInboxRepository.save(...). Use the
class name NotificationInboxService and method save(Integer userId,
NotificationInboxType type, String title, String body, String path) to locate
where to add the metric and update the constructor to initialize the Counter via
MeterRegistry.counter(...).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba00585c-4ae8-40f6-ab1a-2db6be1a75fb

📥 Commits

Reviewing files that changed from the base of the PR and between 517e638 and 5946916.

📒 Files selected for processing (4)
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/main/resources/db/migration/V61__update_notification_inbox_index.sql
  • src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/main/resources/db/migration/**/*.sql

⚙️ CodeRabbit configuration file

src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:

  • 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
  • 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
  • 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.

Files:

  • src/main/resources/db/migration/V61__update_notification_inbox_index.sql
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/resources/db/migration/V61__update_notification_inbox_index.sql
  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
🔇 Additional comments (3)
src/main/java/gg/agit/konect/domain/notification/repository/NotificationInboxRepository.java (1)

17-35: [LEVEL: low] LGTM - Repository 구현이 적절합니다.

  • @Modifying(clearAutomatically = true, flushAutomatically = true) 설정으로 벌크 업데이트 후 영속성 컨텍스트 정리가 올바르게 처리됩니다.
  • 쿼리 메서드 네이밍이 인덱스 (user_id, created_at DESC, id DESC)와 정확히 일치하여 인덱스 스캔이 효율적으로 동작합니다.
  • getByIdAndUserId default 메서드로 예외 처리 로직이 잘 캡슐화되어 있습니다.
src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java (1)

24-226: [LEVEL: low] LGTM - 테스트 커버리지가 적절합니다.

  • 정렬 순서 검증에서 Thread.sleep(1)로 타임스탬프 정밀도 문제를 해결했습니다.
  • 사용자 격리, 빈 목록, 잘못된 페이지 요청, 다른 유저 알림 접근 시도 등 주요 경계 케이스가 포함되어 있습니다.
  • markAllAsRead 후 다른 유저의 미읽음 개수가 영향받지 않음을 검증하여 격리성을 확인합니다.
src/main/resources/db/migration/V61__update_notification_inbox_index.sql (1)

1-2: [LEVEL: critical] MySQL는 DROP INDEX IF EXISTS 구문을 지원하지 않음

검색 결과, MySQL은 어떤 버전에서도(2026년 기준 MySQL 8.4 포함) DROP INDEX IF EXISTS 구문을 지원하지 않습니다. 이 구문은 MariaDB에서만 지원되며, MySQL 공식 문서에도 DROP INDEX 및 ALTER TABLE에서 IF EXISTS 절이 없습니다. 현재 마이그레이션 파일은 MySQL 표준에 맞게 작성되었습니다.

기존 인덱스 존재 여부가 보장되지 않는 환경이라면, INFORMATION_SCHEMA를 조회하거나 애플리케이션 레벨에서 예외 처리하는 방식을 검토하세요. 다만 이전 마이그레이션(V59 또는 그 이전)에서 이 인덱스가 생성되었다면 현재 마이그레이션은 정상 작동합니다.

			> Likely an incorrect or invalid review comment.

Comment on lines +30 to +39
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Integer userId, NotificationInboxType type, String title, String body, String path) {
try {
User user = userRepository.getById(userId);
NotificationInbox inbox = NotificationInbox.of(user, type, title, body, path);
notificationInboxRepository.save(inbox);
} catch (Exception e) {
log.error("Failed to save notification inbox: userId={}, type={}", userId, type, e);
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

[LEVEL: medium] save() 예외 무시로 인한 디버깅 어려움 및 데이터 정합성 추적 불가

의도적으로 푸시 알림 전송에 영향을 주지 않기 위해 예외를 로깅만 하고 있으나, NOT_FOUND_USER 등 비즈니스 로직 오류까지 무시됩니다. 운영 환경에서 inbox 저장 실패율이 높아져도 모니터링하기 어렵습니다. 최소한 메트릭 카운터(예: inbox_save_failure_total)를 추가하여 실패율을 추적할 수 있도록 권장합니다.

♻️ 메트릭 추가 예시
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;

 `@Slf4j`
 `@Service`
 `@RequiredArgsConstructor`
 `@Transactional`(readOnly = true)
 public class NotificationInboxService {

+    private final MeterRegistry meterRegistry;
+
     `@Transactional`(propagation = Propagation.REQUIRES_NEW)
     public void save(Integer userId, NotificationInboxType type, String title, String body, String path) {
         try {
             User user = userRepository.getById(userId);
             NotificationInbox inbox = NotificationInbox.of(user, type, title, body, path);
             notificationInboxRepository.save(inbox);
         } catch (Exception e) {
             log.error("Failed to save notification inbox: userId={}, type={}", userId, type, e);
+            Counter.builder("notification.inbox.save.failure")
+                .tag("type", type.name())
+                .register(meterRegistry)
+                .increment();
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java`
around lines 30 - 39, The save() method in NotificationInboxService currently
swallows all exceptions, so add a metric counter to track failures: inject a
MeterRegistry (or a Counter) into NotificationInboxService, create a Counter
named e.g. "inbox_save_failure_total" (optionally tagged by type or result), and
increment that counter inside the catch block before logging; keep the existing
log but ensure the counter is incremented for every exception thrown by
userRepository.getById(...) or notificationInboxRepository.save(...). Use the
class name NotificationInboxService and method save(Integer userId,
NotificationInboxType type, String title, String body, String path) to locate
where to add the metric and update the constructor to initialize the Counter via
MeterRegistry.counter(...).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment on lines +1 to +5
ALTER TABLE notification_inbox
DROP INDEX idx_notification_inbox_user_id_created_at;

ALTER TABLE notification_inbox
ADD INDEX idx_notification_inbox_user_id_created_at_id (user_id, created_at DESC, id DESC);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

인덱스 변경을 위해 DROP/ADD를 별도 마이그레이션으로 두면 배포 시점에 테이블 락/인덱스 재빌드 비용이 추가로 발생할 수 있습니다. 아직 초기 도입 단계라면 V59(또는 하나의 마이그레이션)에서 최종 인덱스(user_id, created_at, id)를 바로 생성하도록 정리하는 편이 운영 부담이 적습니다.

Copilot uses AI. Check for mistakes.
void getMyInboxesSuccess() throws Exception {
// given
createInbox(user, NotificationInboxType.CLUB_APPLICATION_APPROVED, "동아리 승인");
Thread.sleep(1);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

정렬 테스트를 위해 Thread.sleep(1)을 사용하면 환경/타이밍에 따라 불필요한 지연이나 플래키를 유발할 수 있습니다. 현재 조회 정렬이 createdAt DESC 이후 id DESC로 tie-break 되므로, sleep 없이도 생성 순서를 보장하려면 id 기반(또는 createdAt을 고정 세팅하는 방식)으로 검증하는 편이 안정적입니다.

Suggested change
Thread.sleep(1);

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +11
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

notification_inbox 생성 마이그레이션(V59)에서 created_at/updated_at이 DATETIME(6)이고 기본값/ON UPDATE 설정이 없습니다. 이 저장소의 다른 테이블들은 보통 TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP 패턴을 사용하고(BaseEntity도 TIMESTAMP로 매핑), 이후 V60/V61에서 다시 수정하고 있어 마이그레이션이 불필요하게 분절됩니다. 최종 스키마(타입/기본값/인덱스) 형태로 V59를 정리하고, V60/V61은 제거/재작성하는 쪽이 안전합니다.

Suggested change
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +9

ALTER TABLE notification_inbox
ADD INDEX idx_notification_inbox_user_id_created_at (user_id, created_at DESC);

ALTER TABLE notification_inbox
DROP INDEX idx_notification_inbox_user_id;
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

V60에서 created_at/updated_at 타입/기본값을 변경하고, 인덱스를 추가/삭제한 뒤 V61에서 다시 같은 인덱스를 drop/add 하고 있습니다. Flyway 마이그레이션은 누적되며 되돌리기 어렵기 때문에, 아직 릴리즈 전이라면 한 번에 최종 형태로 만드는 것이 유지보수/운영 리스크를 줄입니다(특히 인덱스 drop/add는 배포 시 락 영향도 큽니다).

Suggested change
ALTER TABLE notification_inbox
ADD INDEX idx_notification_inbox_user_id_created_at (user_id, created_at DESC);
ALTER TABLE notification_inbox
DROP INDEX idx_notification_inbox_user_id;

Copilot uses AI. Check for mistakes.
coderabbitai[bot]
coderabbitai bot previously requested changes Mar 24, 2026
Copy link

@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

♻️ Duplicate comments (1)
src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java (1)

32-38: ⚠️ Potential issue | 🟠 Major

[LEVEL: medium] save()가 예외를 포괄적으로 무시해 실패 추적이 어렵습니다.
문제: save()에서 Exception을 통째로 catch하고 로그만 남겨 저장 실패를 모두 삼키고 있습니다.
영향: FK 제약 위반이나 DB 일시 오류가 반복돼도 푸시는 정상 동작하므로 인앱 알림 누락이 누적되고 운영에서 실패율을 늦게 인지할 수 있습니다.
제안: DataAccessException 등 예상 가능한 예외를 분리 처리하고 실패 메트릭(예: notification.inbox.save.failure)을 함께 기록해 알림함 저장 실패를 모니터링 가능하게 해주세요; As per coding guidelines "보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java`
around lines 32 - 38, The current broad try/catch around userRepository.getById,
NotificationInbox.of and notificationInboxRepository.save swallows all
Exceptions; change it to explicitly catch DataAccessException (or the Spring
data exception type you use) around notificationInboxRepository.save, log the
error with full context, increment a failure metric (e.g.
"notification.inbox.save.failure") and rethrow or propagate a runtime exception
so callers can react; for any other unexpected Exception you can log and rethrow
as well. Use the specific symbols userRepository.getById, NotificationInbox.of,
notificationInboxRepository.save and replace the single catch(Exception e) with
targeted catches that record the metric and do not silently swallow the failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/gg/agit/konect/domain/club/event/ClubApplicationSubmittedEvent.java`:
- Around line 12-19: Wrap the incoming receiverIds in an immutable copy to
guarantee event immutability: in the factory method
ClubApplicationSubmittedEvent.of(...) replace direct use of the passed List with
List.copyOf(receiverIds) (or otherwise ensure the constructor/field stores
List.copyOf(receiverIds)) so the event holds an unmodifiable snapshot of the
receiver IDs.

---

Duplicate comments:
In
`@src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java`:
- Around line 32-38: The current broad try/catch around userRepository.getById,
NotificationInbox.of and notificationInboxRepository.save swallows all
Exceptions; change it to explicitly catch DataAccessException (or the Spring
data exception type you use) around notificationInboxRepository.save, log the
error with full context, increment a failure metric (e.g.
"notification.inbox.save.failure") and rethrow or propagate a runtime exception
so callers can react; for any other unexpected Exception you can log and rethrow
as well. Use the specific symbols userRepository.getById, NotificationInbox.of,
notificationInboxRepository.save and replace the single catch(Exception e) with
targeted catches that record the metric and do not silently swallow the failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6f14fe25-8a81-4070-8b73-375c880a3ec8

📥 Commits

Reviewing files that changed from the base of the PR and between 5946916 and 620db54.

📒 Files selected for processing (6)
  • src/main/java/gg/agit/konect/domain/club/event/ClubApplicationSubmittedEvent.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/notification/listener/ClubApplicationNotificationListener.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/domain/notification/listener/ClubApplicationNotificationListener.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java
  • src/main/java/gg/agit/konect/domain/club/event/ClubApplicationSubmittedEvent.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/java/gg/agit/konect/domain/notification/listener/ClubApplicationNotificationListener.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java
  • src/main/java/gg/agit/konect/domain/club/event/ClubApplicationSubmittedEvent.java
  • src/main/java/gg/agit/konect/domain/notification/service/NotificationInboxService.java
  • src/test/java/gg/agit/konect/integration/domain/notification/NotificationInboxApiTest.java
🔇 Additional comments (3)
src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java (1)

69-80: LGTM!

새로운 쿼리 메서드가 기존 패턴과 일관되게 잘 구현되어 있습니다. JOIN FETCH로 N+1 문제를 방지하고, deletedAt IS NULL 조건으로 soft-delete 필터링이 적절히 적용되었습니다.

src/main/java/gg/agit/konect/domain/notification/listener/ClubApplicationNotificationListener.java (1)

28-39: LGTM!

@Async가 적용된 메서드를 호출하므로 각 수신자별로 독립된 비동기 태스크가 생성되어, 한 수신자에 대한 알림 실패가 다른 수신자에게 영향을 주지 않습니다. 트랜잭션 커밋 후(AFTER_COMMIT) 이벤트가 처리되어 데이터 정합성도 보장됩니다.

src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java (1)

304-318: LGTM!

운영진 전체(PRESIDENT, VICE_PRESIDENT, MANAGER)에게 알림을 발송하도록 변경되었습니다. JOIN FETCH로 N+1이 방지되고, managerIds.isEmpty() 체크로 운영진이 없는 동아리에 대한 불필요한 이벤트 발행이 방지됩니다.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Comment on lines +12 to +16
@RestController
@RequiredArgsConstructor
@RequestMapping("/notifications/inbox")
public class NotificationInboxController implements NotificationInboxApi {

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

NotificationInboxController에 @validated가 없어, NotificationInboxApi의 @min(1) 같은 파라미터 검증이 Spring MVC에서 적용되지 않을 수 있습니다(다른 컨트롤러들은 클래스에 @validated를 선언하는 패턴입니다. 예: NoticeController). 현재는 서비스에서 IllegalArgumentException을 던져 400이 나올 수 있지만, 에러 코드/필드 에러 형태가 다른 API들과 달라질 수 있으니 컨트롤러에 @validated를 추가하고(page 검증은 컨트롤러 validation에 위임) 서비스의 수동 page 체크는 제거/정리하는 쪽이 일관적입니다.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Comment on lines +21 to +29
public SseEmitter subscribe(Integer userId) {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);

emitter.onCompletion(() -> emitters.remove(userId));
emitter.onTimeout(() -> emitters.remove(userId));
emitter.onError(e -> emitters.remove(userId));

emitters.put(userId, emitter);

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

SSE 구독 시 동일 userId로 재구독하면 기존 SseEmitter가 Map에서 덮어써지는데, 기존 emitter의 onCompletion/onTimeout/onError 콜백이 이후에 실행되면 emitters.remove(userId)가 새 emitter까지 제거해 현재 연결이 끊길 수 있습니다(또한 기존 emitter는 complete되지 않아 리소스가 남을 수 있습니다). 기존 emitter를 교체할 때는 이전 emitter를 명시적으로 종료하고, 콜백에서는 emitters.remove(userId, emitter)처럼 현재 콜백의 emitter와 동일할 때만 제거하도록 처리하거나 userId당 emitter를 여러 개 관리하는 구조로 바꾸는 게 안전합니다.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Comment on lines +49 to +53
emitter.send(SseEmitter.event().name("notification").data(notification));
} catch (IOException e) {
log.warn("SSE send failed: userId={}", userId, e);
emitters.remove(userId);
emitter.completeWithError(e);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

SSE 전송 실패 시 emitters.remove(userId)로 제거하면, 동시에 다른 구독이 새 emitter로 교체된 경우 새 emitter까지 제거될 수 있습니다(레이스 컨디션). remove(userId, emitter)처럼 현재 send에 사용한 emitter와 매칭될 때만 제거하도록 바꾸는 편이 안전합니다.

Copilot uses AI. Check for mistakes.
@JanooGwan JanooGwan merged commit 23852c0 into develop Mar 25, 2026
3 checks passed
@JanooGwan JanooGwan deleted the feat/cam-235 branch March 25, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

기능 새로운 기능을 개발합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants