Skip to content

Ranking#69

Merged
Juhye0k merged 26 commits intodevfrom
ranking
Feb 18, 2026
Merged

Ranking#69
Juhye0k merged 26 commits intodevfrom
ranking

Conversation

@Juhye0k
Copy link
Copy Markdown
Contributor

@Juhye0k Juhye0k commented Feb 16, 2026

🚀 1. 개요

이번 PR은 Ranking 도메인 개선 + 최대 집중시간 자동 종료 + FCM 푸시 알림 도입 + 프로젝트 문서 체계화를 포함합니다.

  • 최대 집중시간(기본 3시간) 초과 시 세션 자동 종료 + FCM 알림 전송
  • FCM 토큰 등록/삭제 API 추가 및 전송 재시도(@retryable) 적용
  • 시즌 학과 랭킹 로직 리팩토링 (학과별 집계 응답 분리)
  • .ai/* 문서 + CLAUDE.md/AGENT.md온보딩/테스트/유즈케이스 문서 추가

📝 2. 주요 변경 사항

2-1. 최대 집중시간 자동 종료 (Study)

  • study.max-focus-hours 설정값(기본 3) 기반으로 STARTED 세션 중 오래된 세션 자동 종료
  • MaxFocusStudyScheduler가 주기적으로 StudySessionService.finishMaxFocusStudySession() 호출
  • 종료 시 endTime = startTime + maxFocusHours 로 고정(서버 시간 신뢰 원칙 유지)

2-2. FCM 도입 (Push Notification)

  • FCM 토큰을 User.fcmToken에 저장/삭제
  • 토큰 등록/삭제 API 추가
    • POST /api/v1/fcm/register
    • DELETE /api/v1/fcm/token
  • FcmService.sendMessage()재시도(3회, 지수 백오프) 적용
  • @Recover에서 최종 실패 시 BusinessException(FCM_SEND_FAILED) 처리
  • 회원 로그아웃/탈퇴 시 FCM 토큰 제거

2-3. 시즌 학과 랭킹 리팩토링 (Rank)

  • 기존 학과 시즌랭킹 -> 학과별로 필터링해서 데이터 제공
  • 수정본 -> 개별 학과만 가져오는 것이 아닌 전체 랭킹으로 제공

2-4. 문서 체계화 / 에이전트 가이드 추가

  • 아키텍처/테스트/유즈케이스/에이전트 가이드를 문서로 정리
  • 도메인별 CLAUDE.md 추가(Study/Rank/WiFi)

Summary by CodeRabbit

  • 새로운 기능

    • FCM 푸시 지원: 토큰 등록/제거 및 푸시 전송(재시도·복구 포함)
    • 최대 집중시간 초과 시 학습 세션 자동 종료 및 푸시 알림
    • 학과별 집계 랭킹 조회 개선 및 응답 형식 변경
  • 문서

    • 아키텍처·테스트 전략·사용 사례·AI 에이전트 가이드 등 주요 문서 추가
  • 테스트

    • FCM 목(Mock) 및 테스트 설정(테스트용 Firebase) 추가
  • 설정

    • Firebase 설정 및 최대 집중시간 기본값, CI 테스트 실행 환경 업데이트

@Juhye0k Juhye0k self-assigned this Feb 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 16, 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

FCM 통합(토큰 등록/삭제/전송, Firebase 설정, 예외 타입) 추가, 테스트용 Firebase 설정, 최대 집중시간 자동 종료 스케줄러·서비스·엔터티 변경, 부서 집계형 랭킹 응답 구조 도입, 광범위한 문서·설정 파일 추가입니다.

Changes

Cohort / File(s) Summary
문서화
\.ai/ARCHITECTURE.md, \.ai/TESTING.md, \.ai/USE-CASES.md, AGENT.md, CLAUDE.md, src/main/java/com/gpt/geumpumtabackend/rank/CLAUDE.md, src/main/java/com/gpt/geumpumtabackend/study/CLAUDE.md, src/main/java/com/gpt/geumpumtabackend/wifi/CLAUDE.md
아키텍처·테스트 전략·유스케이스·AI 에이전트 가이드·도메인별 설계 문서 등 다수 문서 추가/보강.
FCM 통합
build.gradle, src/main/java/com/gpt/geumpumtabackend/fcm/api/FcmApi.java, src/main/java/com/gpt/geumpumtabackend/fcm/controller/FcmController.java, src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmService.java, src/main/java/com/gpt/geumpumtabackend/fcm/dto/*, src/main/java/com/gpt/geumpumtabackend/global/config/fcm/*, src/main/java/com/gpt/geumpumtabackend/global/exception/ExceptionType.java
Firebase Admin 의존성 추가, FCM API/컨트롤러/서비스·DTO·FirebaseApp 설정, 전송 재시도/복구 로직 및 FCM 관련 예외 타입 추가.
최대 집중 시간 기능
src/main/java/com/gpt/geumpumtabackend/study/config/StudyProperties.java, src/main/java/com/gpt/geumpumtabackend/study/domain/StudySession.java, src/main/java/com/gpt/geumpumtabackend/study/scheduler/MaxFocusStudyScheduler.java, src/main/java/com/gpt/geumpumtabackend/study/service/StudySessionService.java, src/main/java/com/gpt/geumpumtabackend/study/repository/StudySessionRepository.java, src/main/resources/application.yml
설정 기반(maxFocusHours) 매초 검사 스케줄러 추가, 만료 세션 자동 종료(endMaxFocusStudySession) 로직, FCM 알림 통합 및 repository 조회 메서드 추가.
부서 집계형 랭킹
src/main/java/com/gpt/geumpumtabackend/rank/api/SeasonRankApi.java, src/main/java/com/gpt/geumpumtabackend/rank/controller/SeasonRankController.java, src/main/java/com/gpt/geumpumtabackend/rank/dto/response/SeasonDepartmentRankingResponse.java, src/main/java/com/gpt/geumpumtabackend/rank/service/SeasonRankService.java, src/main/java/com/gpt/geumpumtabackend/rank/repository/DepartmentRankingRepository.java, src/main/java/com/gpt/geumpumtabackend/rank/repository/SeasonRankingSnapshotRepository.java
부서 매개변수 제거, userId 기반 부서 집계 응답(SeasonDepartmentRankingResponse) 도입, 네이티브 쿼리/리포지토리 메서드 및 병합·정렬 로직 변경.
사용자 도메인 확장
src/main/java/com/gpt/geumpumtabackend/user/domain/User.java, src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java, src/test/.../UserServiceTest.java
User 엔터티에 fcmToken 필드 추가 및 update/clear 메서드, 로그아웃/탈퇴 시 FCM 토큰 제거 호출, 단위 테스트에 FcmService 목 추가.
테스트/테스트 설정
src/test/java/.../TestFcmConfig.java, src/test/java/.../UserServiceTest.java
테스트용 FirebaseApp 설정(TestFcmConfig) 추가 및 UserService 테스트에 FcmService 목 추가.
설정 파일 및 에이전트 정책
.claude/settings.json, src/main/resources/application.yml
AI 에이전트 권한 정책 파일 추가 및 study.max-focus-hours: 3 기본값 추가.
API 문서(스웨거) 설명 보강
src/main/java/com/gpt/geumpumtabackend/study/api/StudySessionApi.java
최대 집중 시간 자동 종료 동작과 FCM 알림(STUDY_SESSION_FORCE_ENDED) 관련 설명 보강(설명문만 변경).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    actor Client
    participant Controller as FcmController
    participant Service as FcmService
    participant Repo as UserRepository
    participant Firebase as Firebase Admin SDK
    end

    Client->>Controller: POST /api/v1/fcm/register (fcmToken)
    Controller->>Service: registerFcmToken(userId, token)
    Service->>Repo: findById(userId)
    Repo-->>Service: User
    Service->>Repo: update user.fcmToken
    Repo-->>Service: saved
    Service->>Firebase: (optional) validate/send
    Service-->>Controller: success
    Controller-->>Client: 200 OK

    Client->>Controller: DELETE /api/v1/fcm/token
    Controller->>Service: removeFcmToken(userId)
    Service->>Repo: findById(userId)
    Repo-->>Service: User
    Service->>Repo: clear user.fcmToken
    Repo-->>Service: saved
    Controller-->>Client: 200 OK
Loading
sequenceDiagram
    rect rgba(200,255,200,0.5)
    participant Scheduler as MaxFocusStudyScheduler
    participant StudyService as StudySessionService
    participant Repo as StudySessionRepository
    participant UserRepo as UserRepository
    participant FcmService as FcmService
    end

    Scheduler->>StudyService: finishMaxFocusStudySession()
    StudyService->>StudyService: cutoff = now - maxFocusHours
    StudyService->>Repo: findAllByStatusAndStartTimeBefore(STARTED, cutoff)
    Repo-->>StudyService: List<StudySession>

    loop expired sessions
        StudyService->>StudyService: session.endMaxFocusStudySession(start, maxHours)
        StudyService->>Repo: save(session)
        Repo-->>StudyService: saved
        StudyService->>UserRepo: findById(session.userId)
        UserRepo-->>StudyService: User
        StudyService->>FcmService: sendMaxFocusNotification(user, hours)
        FcmService-->>StudyService: success / error
    end

    StudyService-->>Scheduler: complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, documentation

Suggested reviewers

  • kon28289
  • patulus

"나는 토끼, 깡충 깡충 달려와
토큰을 남기고 알림을 띄워줄게요 🐇
집중 시간이 끝나면 살포시 알려주고
부서 순위는 모아 새로 정렬했어요
문서도 챙겨두고 조용히 떠납니다."

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목 'Ranking'은 시즌 학과 랭킹 리팩토링의 일부만 언급하며, 최대 집중시간 자동 종료, FCM 푸시 알림, 문서 체계화 등 주요 변경사항들을 포함하지 않음. PR 제목을 '최대 집중시간 자동 종료, FCM 알림, 시즌 랭킹 리팩토링' 또는 유사한 형태로 변경하여 주요 변경사항을 명확히 반영하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% 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
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고 개요, 주요 변경사항, 스크린샷 섹션을 포함하며 상세한 구현 내용을 제공합니다.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into dev

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ranking

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
Contributor

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

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/gpt/geumpumtabackend/user/service/UserService.java (1)

96-112: ⚠️ Potential issue | 🔴 Critical

Firebase 설정 누락으로 인한 fcmService NPE — 테스트 프로필에 Firebase 속성 필요

FcmConfigfirebase.serviceAccountPathfirebase.projectId 속성 없이는 FirebaseApp 초기화에 실패합니다. 현재 application-unit-test.ymlapplication-test.yml 모두에서 이 속성들이 정의되어 있지 않아서, 테스트 컨텍스트에서 FcmService 빈이 제대로 로드되지 않고 있습니다. 그 결과 logout()(line 101)과 withdrawUser()(line 110)에서 fcmService.removeFcmToken() 호출 시 NPE가 발생합니다.

해결 방안:

  1. FcmConfig@ConditionalOnProperty(prefix = "firebase", name = "serviceAccountPath")를 추가하여 Firebase 속성이 없는 경우 빈 초기화를 건너뛰거나
  2. 테스트 프로필(application-unit-test.yml, application-test.yml)에 dummy Firebase 속성을 추가하거나
  3. FcmService 호출을 try-catch로 감싸거나 Optional로 처리
🤖 Fix all issues with AI agents
In @.ai/USE-CASES.md:
- Line 3: The document header text "총 **42개** 유즈케이스." is inconsistent with the
summary table total ("요약 테이블 합계" shows 46); reconcile them by either updating
the header count to match the table (change 42 → 46) or correcting the
summary-table rows so their sum equals the header—ensure the chosen value is
correct, keep the bold formatting (e.g., **46개**) and update any other places in
the document that repeat the total.

In `@src/main/java/com/gpt/geumpumtabackend/fcm/api/FcmApi.java`:
- Around line 28-46: The API docs in FcmApi currently hardcode "3시간" and
maxFocusHours "3"; update the documentation text in the FcmApi class so it
references the configured value (study.max-focus-hours) or indicates it's a
default, and if possible interpolate the actual configured value retrieved via
the existing property (study.max-focus-hours) used by the application (or
mention the default constant used where messages are built, e.g., the method
that constructs the FCM payload/buildFcmPayload or sendStudySessionForceEnded).
Ensure the Notification body and the Data Message maxFocusHours field reflect
the configured property or state that the number is configurable/default.

In `@src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmService.java`:
- Around line 78-97: sendMaxFocusNotification is calling sendMessage directly
which bypasses Spring AOP proxies so `@Retryable` on sendMessage won't run and the
surrounding try-catch swallows retries; fix by invoking sendMessage through the
proxied FcmService bean instead of a direct this.call (inject FcmService into
itself or obtain the bean from ApplicationContext) OR move the FCM send logic
into a separate `@Component` (e.g., FcmSender) with the `@Retryable-annotated`
method and call that bean from sendMaxFocusNotification, and remove/adjust the
broad try-catch so retryable exceptions can propagate to the proxy.

In `@src/main/java/com/gpt/geumpumtabackend/rank/service/SeasonRankService.java`:
- Around line 235-265: The fallback myRanking rank is computed from
topRanks.size() which only includes entries with totalMillis > 0, causing
mismatches with the RANK() logic; update buildSeasonDepartmentRankingResponse to
compute the fallback rank using the full merged result size from the input
rankings list (i.e., use rankings.size() + 1) instead of topRanks.size() + 1 so
the fallback aligns with mergeAndRankDepartments and the original RANK semantics
when creating the new DepartmentRankingEntryResponse for myRanking.

In `@src/main/java/com/gpt/geumpumtabackend/study/CLAUDE.md`:
- Around line 42-49: The "최대 공부시간" section is incomplete; update the section to
explicitly state that after the server scheduler finds a session exceeding 3
hours and sets the end_time, it must send an FCM message to the client
instructing it to immediately stop the study session; the client should stop
timers, update the UI to show the session ended (e.g., show a
modal/notification), persist any local progress if needed, and call the existing
API to sync the final end_time/study duration with the server so records are
consistent. Ensure the text references the scheduler, end_time, and FCM so
implementers know which components (스케줄러, end_time, FCM) are involved and what
the client must do upon receiving the message.

In `@src/main/java/com/gpt/geumpumtabackend/study/domain/StudySession.java`:
- Around line 50-54: The endMaxFocusStudySession method currently takes a
LocalDateTime startTime parameter but computes endTime from that parameter while
computing totalMillis from the entity field this.startTime, risking
inconsistency; remove the startTime parameter from
StudySession.endMaxFocusStudySession and compute endTime as
this.startTime.plusHours(maxFocusTime), set status to StudyStatus.FINISHED and
totalMillis from Duration.between(this.startTime, this.endTime).toMillis(); then
update callers (e.g., StudySessionService) to call
endMaxFocusStudySession(maxFocusTime) with the session instance's startTime no
longer passed in.

In
`@src/main/java/com/gpt/geumpumtabackend/study/service/StudySessionService.java`:
- Around line 105-109: The repository method findAllByStatusAndStartTimeBefore
causes an N+1 when the service calls getUser() on each StudySession (used in
endMaxFocusStudySession); update the repository to eagerly fetch the User to
avoid per-row lazy loads by annotating findAllByStatusAndStartTimeBefore with
`@EntityGraph`(attributePaths = {"user"}) or replace it with an explicit `@Query`
using JOIN FETCH to load the user together; keep the method signature
(findAllByStatusAndStartTimeBefore) and ensure tests still pass after the
change.

In `@src/main/java/com/gpt/geumpumtabackend/user/domain/User.java`:
- Around line 63-64: The fcmToken field currently allows duplicate values which
can cause duplicate notifications; update the model or registration flow to
enforce uniqueness by either adding a unique constraint to the fcmToken column
(the fcmToken field in User class) or, in FcmService.registerFcmToken(), before
persisting a token, query for existing users with that token (add
UserRepository.findByFcmToken(String)) and clear or nullify their fcmToken, then
save the new owner’s token; ensure changes touch User.fcmToken,
FcmService.registerFcmToken(), and UserRepository (add findByFcmToken) so only
one user holds a given token.
🧹 Nitpick comments (8)
src/main/java/com/gpt/geumpumtabackend/user/domain/User.java (1)

102-108: updateFcmToken에 입력값 검증 부재

updateFcmToken(String fcmToken)null이나 빈 문자열도 허용합니다. clearFcmToken()이 별도로 존재하므로, updateFcmToken에서는 유효한 토큰만 받도록 방어 코드를 추가하면 의도치 않은 호출을 방지할 수 있습니다.

🛡️ 입력값 검증 추가 제안
 public void updateFcmToken(String fcmToken) {
+    if (fcmToken == null || fcmToken.isBlank()) {
+        throw new IllegalArgumentException("FCM token must not be null or blank");
+    }
     this.fcmToken = fcmToken;
 }
src/main/java/com/gpt/geumpumtabackend/study/config/StudyProperties.java (1)

8-16: maxFocusHours에 유효성 검증이 없습니다.

0 이하의 값이 설정되면 스케줄러의 세션 종료 로직에서 예기치 않은 동작이 발생할 수 있습니다. @Min(1) 등의 제약 조건 추가를 고려해 주세요.

또한, FcmProperties@EnableConfigurationProperties로 등록하는 반면, 이 클래스는 @Component로 등록하고 있어 프로젝트 내 패턴이 일관되지 않습니다.

제안
 package com.gpt.geumpumtabackend.study.config;
 
+import jakarta.validation.constraints.Min;
 import lombok.Getter;
 import lombok.Setter;
 import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
+import org.springframework.validation.annotation.Validated;
 
-@Component
 `@ConfigurationProperties`(prefix = "study")
 `@Getter`
 `@Setter`
+@Validated
 public class StudyProperties {
     /**
      * 최대 집중 공부 시간 (시간 단위)
      */
+    `@Min`(1)
     private int maxFocusHours = 3;
 }

@Component 대신 사용하는 쪽에서 @EnableConfigurationProperties(StudyProperties.class)로 등록하면 FcmProperties와 패턴이 일치합니다.

src/main/java/com/gpt/geumpumtabackend/global/config/fcm/FcmProperties.java (1)

7-13: 필수 속성에 @NotBlank 검증 추가를 권장합니다.

serviceAccountPathprojectId가 미설정되면 FcmConfig에서 NPE 또는 불분명한 오류가 발생할 수 있습니다. @Validated + @NotBlank를 추가하면 애플리케이션 시작 시 명확한 오류 메시지를 얻을 수 있습니다.

제안
+import jakarta.validation.constraints.NotBlank;
 import lombok.Getter;
 import lombok.Setter;
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
 
 `@Getter`
 `@Setter`
 `@ConfigurationProperties`(prefix = "firebase")
+@Validated
 public class FcmProperties {
+    `@NotBlank`
     private String serviceAccountPath;
+    `@NotBlank`
     private String projectId;
 }
src/main/java/com/gpt/geumpumtabackend/global/config/fcm/FcmConfig.java (1)

26-45: firebaseApp() Bean에서 IOException이 전파되면 FCM이 불필요한 환경(로컬 개발 등)에서도 앱 시작이 실패합니다.

서비스 계정 파일이 없는 환경에서 전체 애플리케이션이 기동되지 않을 수 있습니다. FCM이 선택적 기능이라면 @ConditionalOnProperty 또는 @Profile로 Bean 생성을 제어하는 것을 고려해 주세요.

예시: 조건부 Bean 등록
 `@Configuration`
 `@RequiredArgsConstructor`
 `@EnableConfigurationProperties`(FcmProperties.class)
+@ConditionalOnProperty(prefix = "firebase", name = "service-account-path")
 `@Slf4j`
 public class FcmConfig {
src/main/java/com/gpt/geumpumtabackend/rank/dto/response/SeasonDepartmentRankingResponse.java (1)

4-8: 사용되지 않는 import 존재

DepartmentRankingTemp (line 4)와 Collectors (line 8)가 import되어 있지만 이 파일에서 사용되지 않습니다.

🧹 제거 제안
-import com.gpt.geumpumtabackend.rank.dto.DepartmentRankingTemp;
-
 import java.time.LocalDate;
 import java.util.List;
-import java.util.stream.Collectors;
src/main/java/com/gpt/geumpumtabackend/study/service/StudySessionService.java (1)

100-119: 트랜잭션 내 FCM 호출로 인한 DB 커넥션 장시간 점유 위험

@Transactional 범위 안에서 FCM 알림 전송(재시도 3회, 지수 백오프)이 이루어지므로, 만료 세션이 많을 경우 트랜잭션과 DB 커넥션이 장시간 유지됩니다.

세션 종료(DB 변경)와 FCM 알림 전송을 분리하는 것을 권장합니다. 예를 들어:

  1. 트랜잭션 내에서 세션 종료만 처리하고 알림 대상 목록을 수집
  2. 트랜잭션 커밋 후 FCM 알림을 별도로 전송
♻️ 분리 제안
     `@Transactional`
     public void finishMaxFocusStudySession() {
         int maxFocusHours = studyProperties.getMaxFocusHours();
         LocalDateTime cutoffTime = LocalDateTime.now().minusHours(maxFocusHours);
 
         List<StudySession> expiredSessions = studySessionRepository.findAllByStatusAndStartTimeBefore(
                 StudyStatus.STARTED, cutoffTime
         );
         for (StudySession expiredSession : expiredSessions) {
             expiredSession.endMaxFocusStudySession(expiredSession.getStartTime(), maxFocusHours);
-
-            // FCM 알림 전송
-            try {
-                fcmService.sendMaxFocusNotification(expiredSession.getUser(), maxFocusHours);
-            } catch (Exception e) {
-                log.error("Failed to send FCM notification for session {}", expiredSession.getId(), e);
-                // 알림 실패해도 세션 종료는 계속 진행
-            }
         }
+        // 트랜잭션 커밋 후 알림 전송은 별도 메서드 또는 `@TransactionalEventListener로` 처리
+        for (StudySession expiredSession : expiredSessions) {
+            try {
+                fcmService.sendMaxFocusNotification(expiredSession.getUser(), maxFocusHours);
+            } catch (Exception e) {
+                log.error("Failed to send FCM notification for session {}", expiredSession.getId(), e);
+            }
+        }
     }

참고: 위 diff는 단순 분리 예시이며, 이상적으로는 @TransactionalEventListener@Async를 활용하여 트랜잭션 바깥에서 FCM 전송을 처리하는 것이 좋습니다.

src/main/java/com/gpt/geumpumtabackend/study/scheduler/MaxFocusStudyScheduler.java (1)

17-24: fixedRate = 1000(1초)은 3시간 초과 세션 종료 작업에 비해 지나치게 빈번합니다.

최대 집중 시간이 3시간인데, 1초마다 DB를 조회하면 불필요한 부하가 발생합니다. 30~60초 간격이면 충분합니다. 1초 단위의 정밀도가 필요한 유스케이스가 아닙니다.

♻️ 제안
-    `@Scheduled`(fixedRate = 1000)
+    `@Scheduled`(fixedRate = 60000)
src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmService.java (1)

24-25: 클래스 레벨 @Transactional(readOnly = true)sendMessage에도 적용됩니다.

sendMessage는 DB 작업이 없는 FCM 전송 메서드인데, 불필요하게 읽기 전용 트랜잭션이 열립니다. 외부 API 호출 중 트랜잭션을 유지하는 것은 커넥션 풀 점유 시간을 늘릴 수 있습니다. sendMessagesendMaxFocusNotification@Transactional(propagation = Propagation.NOT_SUPPORTED)를 고려하거나, 클래스 레벨 트랜잭션 전략을 재검토해 주세요.

Comment on lines +63 to +64
@Column(length = 255)
private String fcmToken;
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for updateFcmToken and fcmToken usage in service layer
rg -n -C5 'updateFcmToken|fcmToken' --type=java -g '!**/domain/User.java'

Repository: Geumpumta/backend

Length of output: 4081


🏁 Script executed:

#!/bin/bash
# Get complete FcmService content
cat -n src/main/java/com/gpt/geumpumtabackend/fcm/service/FcmService.java

Repository: Geumpumta/backend

Length of output: 4422


🏁 Script executed:

#!/bin/bash
# Check UserRepository for custom methods related to fcmToken
rg -n 'findBy|existsBy' --type=java src/main/java/com/gpt/geumpumtabackend/user/repository/ -A2

Repository: Geumpumta/backend

Length of output: 1470


🏁 Script executed:

#!/bin/bash
# Check User.updateFcmToken method implementation
rg -n -A5 'updateFcmToken|clearFcmToken' --type=java src/main/java/com/gpt/geumpumtabackend/user/domain/User.java

Repository: Geumpumta/backend

Length of output: 255


FCM 토큰에 중복 할당 가능 - 중복 알림 발생 위험

fcmToken 필드에 unique 제약 조건이 없고, FcmService.registerFcmToken()에서 동일 토큰을 가진 다른 사용자의 토큰을 제거하는 로직이 없습니다. 한 기기에서 여러 계정으로 로그인하면 동일 토큰이 여러 사용자에게 등록되어 중복 알림이 발생할 수 있습니다.

토큰 등록 시 다음 중 하나를 수행해야 합니다:

  • 데이터베이스 제약 조건: fcmToken 컬럼에 unique 제약 추가
  • 애플리케이션 로직: 토큰 등록 전 기존 동일 토큰을 가진 다른 사용자의 토큰 제거
  • 저장소 메서드: UserRepository.findByFcmToken() 추가 후 registerFcmToken()에서 활용
🤖 Prompt for AI Agents
In `@src/main/java/com/gpt/geumpumtabackend/user/domain/User.java` around lines 63
- 64, The fcmToken field currently allows duplicate values which can cause
duplicate notifications; update the model or registration flow to enforce
uniqueness by either adding a unique constraint to the fcmToken column (the
fcmToken field in User class) or, in FcmService.registerFcmToken(), before
persisting a token, query for existing users with that token (add
UserRepository.findByFcmToken(String)) and clear or nullify their fcmToken, then
save the new owner’s token; ensure changes touch User.fcmToken,
FcmService.registerFcmToken(), and UserRepository (add findByFcmToken) so only
one user holds a given token.

Copy link
Copy Markdown
Contributor

@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

🤖 Fix all issues with AI agents
In
`@src/test/java/com/gpt/geumpumtabackend/unit/user/service/UserServiceTest.java`:
- Around line 51-52: The logout and withdraw unit tests are missing verification
that FcmService.removeFcmToken(userId) is called; update the tests in
UserServiceTest to add verify(fcmService).removeFcmToken(userId) in the logout
test (alongside verify(refreshTokenRepository).deleteByUserId(userId)) and in
the withdrawUser test (alongside
verify(refreshTokenRepository).deleteByUserId(userId) and
verify(userRepository).deleteById(userId)) so both UserService.logout() and
UserService.withdrawUser() behavior is asserted.
🧹 Nitpick comments (2)
src/test/java/com/gpt/geumpumtabackend/unit/user/service/UserServiceTest.java (2)

288-301: 로그아웃 테스트에서 FCM 토큰 삭제 검증이 누락되었습니다.

UserService.logout()이 FCM 토큰을 제거하도록 변경되었다면, 이 테스트에서도 해당 동작을 검증해야 합니다.

🧪 FCM 토큰 삭제 검증 추가 제안
         // Then
         verify(refreshTokenRepository).deleteByUserId(userId);
+        verify(fcmService).removeFcmToken(userId);

321-335: 회원 탈퇴 테스트에서도 FCM 토큰 삭제 검증이 누락되었습니다.

로그아웃과 동일하게, 회원 탈퇴 시에도 FCM 토큰 삭제가 호출되는지 검증이 필요합니다.

🧪 FCM 토큰 삭제 검증 추가 제안
         // Then
         verify(refreshTokenRepository).deleteByUserId(userId);
         verify(userRepository).deleteById(userId);
+        verify(fcmService).removeFcmToken(userId);

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
src/main/java/com/gpt/geumpumtabackend/global/config/fcm/FcmConfig.java (1)

14-14: 사소한 import 정리.

org.springframework.context.annotation.Profile import가 다른 org.springframework.context.annotation.* import들(Line 9-10)과 분리되어 있습니다.

♻️ import 그룹 정리 제안
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.ResourceLoader;
-
-import org.springframework.context.annotation.Profile;

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
build.gradle (1)

75-77: TestContainers 버전 업그레이드는 적절해 보입니다.

세 아티팩트 모두 동일한 1.20.4 버전으로 일관되게 업데이트되었습니다. 다만 BOM(Bill of Materials)을 사용하면 버전을 한 곳에서 관리할 수 있어 향후 업그레이드 시 누락 위험을 줄일 수 있습니다.

♻️ TestContainers BOM 사용 제안
+    // TestContainers BOM
+    testImplementation platform('org.testcontainers:testcontainers-bom:1.20.4')
+    testImplementation 'org.testcontainers:junit-jupiter'
+    testImplementation 'org.testcontainers:mysql'
+    testImplementation 'org.testcontainers:testcontainers'
-    testImplementation 'org.testcontainers:junit-jupiter:1.20.4'
-    testImplementation 'org.testcontainers:mysql:1.20.4'
-    testImplementation 'org.testcontainers:testcontainers:1.20.4'

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (2)
.github/workflows/dev-ci.yml (2)

22-22: test job은 ubuntu-22.04로 고정했지만, docker job(Line 81)은 여전히 ubuntu-latest를 사용하고 있습니다.

Testcontainers 안정성을 위해 러너 버전을 고정한 의도는 이해되지만, 두 job 간 러너 버전이 다르면 향후 ubuntu-latest가 24.04로 올라갈 때 빌드 환경 차이가 생길 수 있습니다. 의도적인 선택인지 확인해 주세요.


56-59: TESTCONTAINERS_RYUK_DISABLED: true — CI 환경에서는 허용 가능하나 주의가 필요합니다.

GitHub Actions 러너는 일회성이므로 Ryuk 비활성화가 실질적 문제를 일으키지 않지만, 이 설정이 로컬 개발 가이드로 전파되면 컨테이너 누수가 발생할 수 있습니다. CI 전용임을 명시하는 주석을 추가하면 좋겠습니다.

또한 DOCKER_HOSTTESTCONTAINERS_DOCKER_SOCKET_OVERRIDE는 GitHub-hosted Linux 러너의 기본값과 동일하므로, Testcontainers가 소켓을 못 찾는 특정 이슈를 해결하기 위한 것이 아니라면 불필요할 수 있습니다.

@Juhye0k Juhye0k requested a review from kon28289 February 16, 2026 15:17
@Juhye0k Juhye0k requested a review from kon28289 February 18, 2026 07:25
Copy link
Copy Markdown
Contributor

@kon28289 kon28289 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!!

@Juhye0k Juhye0k merged commit 417a831 into dev Feb 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants