Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughFCM 통합(토큰 등록/삭제/전송, Firebase 설정, 예외 타입) 추가, 테스트용 Firebase 설정, 최대 집중시간 자동 종료 스케줄러·서비스·엔터티 변경, 부서 집계형 랭킹 응답 구조 도입, 광범위한 문서·설정 파일 추가입니다. Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🔴 CriticalFirebase 설정 누락으로 인한
fcmServiceNPE — 테스트 프로필에 Firebase 속성 필요
FcmConfig는firebase.serviceAccountPath와firebase.projectId속성 없이는FirebaseApp초기화에 실패합니다. 현재application-unit-test.yml과application-test.yml모두에서 이 속성들이 정의되어 있지 않아서, 테스트 컨텍스트에서FcmService빈이 제대로 로드되지 않고 있습니다. 그 결과logout()(line 101)과withdrawUser()(line 110)에서fcmService.removeFcmToken()호출 시 NPE가 발생합니다.해결 방안:
FcmConfig에@ConditionalOnProperty(prefix = "firebase", name = "serviceAccountPath")를 추가하여 Firebase 속성이 없는 경우 빈 초기화를 건너뛰거나- 테스트 프로필(
application-unit-test.yml,application-test.yml)에 dummy Firebase 속성을 추가하거나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검증 추가를 권장합니다.
serviceAccountPath나projectId가 미설정되면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 알림 전송을 분리하는 것을 권장합니다. 예를 들어:
- 트랜잭션 내에서 세션 종료만 처리하고 알림 대상 목록을 수집
- 트랜잭션 커밋 후 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 호출 중 트랜잭션을 유지하는 것은 커넥션 풀 점유 시간을 늘릴 수 있습니다.sendMessage와sendMaxFocusNotification에@Transactional(propagation = Propagation.NOT_SUPPORTED)를 고려하거나, 클래스 레벨 트랜잭션 전략을 재검토해 주세요.
src/main/java/com/gpt/geumpumtabackend/rank/service/SeasonRankService.java
Show resolved
Hide resolved
src/main/java/com/gpt/geumpumtabackend/study/domain/StudySession.java
Outdated
Show resolved
Hide resolved
src/main/java/com/gpt/geumpumtabackend/study/service/StudySessionService.java
Outdated
Show resolved
Hide resolved
| @Column(length = 255) | ||
| private String fcmToken; |
There was a problem hiding this comment.
🧩 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.javaRepository: 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/ -A2Repository: 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.javaRepository: 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.
There was a problem hiding this comment.
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);
src/test/java/com/gpt/geumpumtabackend/unit/user/service/UserServiceTest.java
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/main/java/com/gpt/geumpumtabackend/global/config/fcm/FcmConfig.java (1)
14-14: 사소한 import 정리.
org.springframework.context.annotation.Profileimport가 다른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;
There was a problem hiding this comment.
🧹 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'
There was a problem hiding this comment.
🧹 Nitpick comments (2)
.github/workflows/dev-ci.yml (2)
22-22:testjob은ubuntu-22.04로 고정했지만,dockerjob(Line 81)은 여전히ubuntu-latest를 사용하고 있습니다.Testcontainers 안정성을 위해 러너 버전을 고정한 의도는 이해되지만, 두 job 간 러너 버전이 다르면 향후
ubuntu-latest가 24.04로 올라갈 때 빌드 환경 차이가 생길 수 있습니다. 의도적인 선택인지 확인해 주세요.
56-59:TESTCONTAINERS_RYUK_DISABLED: true— CI 환경에서는 허용 가능하나 주의가 필요합니다.GitHub Actions 러너는 일회성이므로 Ryuk 비활성화가 실질적 문제를 일으키지 않지만, 이 설정이 로컬 개발 가이드로 전파되면 컨테이너 누수가 발생할 수 있습니다. CI 전용임을 명시하는 주석을 추가하면 좋겠습니다.
또한
DOCKER_HOST와TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE는 GitHub-hosted Linux 러너의 기본값과 동일하므로, Testcontainers가 소켓을 못 찾는 특정 이슈를 해결하기 위한 것이 아니라면 불필요할 수 있습니다.
🚀 1. 개요
이번 PR은 Ranking 도메인 개선 + 최대 집중시간 자동 종료 + FCM 푸시 알림 도입 + 프로젝트 문서 체계화를 포함합니다.
.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)
User.fcmToken에 저장/삭제POST /api/v1/fcm/registerDELETE /api/v1/fcm/tokenFcmService.sendMessage()에 재시도(3회, 지수 백오프) 적용@Recover에서 최종 실패 시BusinessException(FCM_SEND_FAILED)처리2-3. 시즌 학과 랭킹 리팩토링 (Rank)
2-4. 문서 체계화 / 에이전트 가이드 추가
CLAUDE.md추가(Study/Rank/WiFi)Summary by CodeRabbit
새로운 기능
문서
테스트
설정