Skip to content

사전 푸시 알림 기능을 구현한다.#46

Merged
strongmhk merged 13 commits intodevelopfrom
feature/#39-fcm-push-notification
Aug 9, 2025
Merged

사전 푸시 알림 기능을 구현한다.#46
strongmhk merged 13 commits intodevelopfrom
feature/#39-fcm-push-notification

Conversation

@strongmhk
Copy link
Copy Markdown
Member

@strongmhk strongmhk commented Aug 9, 2025

📄 PR 요약

사전 푸시 알림 기능을 구현한다.

✍🏻 PR 상세

  1. 알람 울리기 1시간 전 사전 푸시 알림 구현
  • Firebase 설정, json 파일은 github secrets에 저장
  • 푸시 알림 수신 동의한 회원에 한해서 알람 울리기 1시간 전에 푸시 알림을 전송
  1. 토큰 재발급, 로그아웃 API 수정
  • request body에서 deviceId 삭제

👀 참고사항

✅ 체크리스트

  • PR 양식에 맞게 작성했습니다.
  • 모든 테스트가 통과했습니다.
  • 프로그램이 정상적으로 작동합니다.
  • 적절한 라벨을 설정했습니다.
  • 불필요한 코드를 제거했습니다.

🚪 연관된 이슈 번호

Closes #39

Summary by CodeRabbit

  • New Features

    • FCM 토큰 등록 및 관리 기능 추가로 알람 리마인더 푸시 알림 지원.
    • 예정된 알람에 대한 예약 푸시 알림 도입, 대량 알림 전송 및 유효하지 않은 토큰 자동 정리 기능 포함.
    • FCM 토큰 등록을 위한 API 엔드포인트 추가.
  • Improvements

    • JWT에 디바이스 ID 연동 및 로그아웃, 토큰 재발급 프로세스 간소화로 인증 및 토큰 관리 개선.
    • Redis 트랜잭션 지원 활성화 및 FCM 토큰 관리 안정성 향상.
  • Bug Fixes

    • 알람 발생 건당 리마인더 알림이 한 번만 전송되도록 보장.
  • Chores

    • Firebase 연동을 위한 의존성 및 설정 파일 업데이트.
    • 민감한 Firebase 키 파일을 제외하도록 .gitignore 확장.

@strongmhk strongmhk self-assigned this Aug 9, 2025
@strongmhk strongmhk added the ✨ feature New feature or request label Aug 9, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 9, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

이번 변경은 Firebase Cloud Messaging(FCM)을 활용한 사전 푸시 알림 기능을 구현한다. FCM 설정과 Redis 기반 FCM 토큰 관리, 예정된 알람을 조회해 대상자에게 알림을 발송하는 스케줄러 서비스를 추가했다. 인증 흐름 단순화 및 관련 DTO, 설정, 워크플로우 수정도 포함한다.

Changes

Cohort / File(s) Change Summary
CI/CD 및 빌드 설정
.github/workflows/dev-cd.yml, .github/workflows/dev-ci.yml, .gitignore, build.gradle
Firebase 설정용 whiplash-firebase-key.json 생성 및 무시 설정 추가, Firebase Admin SDK 의존성 추가.
알람 사전 알림 기능
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java, .../dto/etc/OccurrencePushInfo.java, .../dto/etc/PushTargetDto.java, .../mapper/AlarmMapper.java, .../domain/service/AlarmQueryService.java, .../domain/service/AlarmQueryServiceImpl.java, .../persistence/entity/AlarmOccurrenceEntity.java, .../persistence/repository/AlarmOccurrenceRepository.java
사전 알림 대상 조회, 알림 전송 스케줄러 및 관련 DTO, 엔티티, 저장소, 서비스 메서드 추가 및 수정. 알림 전송 상태 필드 추가.
Firebase 연동
src/main/java/akuma/whiplash/infrastructure/firebase/FirebaseConfig.java, .../FcmService.java
Firebase SDK 초기화 설정 추가 및 FCM 토큰 등록과 대량 푸시 알림 전송, 실패 토큰 제거 로직 포함 서비스 구현.
Redis 토큰 관리
src/main/java/akuma/whiplash/infrastructure/redis/RedisConfig.java, .../RedisService.java
Redis 트랜잭션 지원 활성화 및 멤버별, 디바이스별 FCM 토큰 원자적 등록, 조회, 삭제 기능 제공하는 Redis 서비스 추가.
인증 흐름 리팩터링
src/main/java/akuma/whiplash/domains/auth/application/dto/etc/MemberContext.java, .../dto/request/LogoutRequest.java (삭제), .../dto/request/ReissueRequest.java (삭제), .../dto/request/RegisterFcmTokenRequest.java, .../mapper/AuthMapper.java, .../usecase/AuthUseCase.java, .../domain/service/AuthCommandService.java, .../domain/service/AuthCommandServiceImpl.java, .../presentation/AuthController.java, .../global/config/security/jwt/JwtProvider.java, .../global/config/security/jwt/JwtUtils.java
deviceId 포함하도록 인증 컨텍스트 및 JWT 확장, 로그아웃 및 토큰 재발급 API 단순화, FCM 토큰 등록 API 추가, 불필요 DTO 삭제.
기타
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
알람 체크인 관련 TODO 주석 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as AlarmReminderScheduler
    participant AlarmQuery as AlarmQueryService
    participant Redis as RedisService
    participant FCM as FcmService

    Scheduler->>AlarmQuery: findPushTargetsByTimeRange(date, start, end)
    AlarmQuery-->>Scheduler: List<OccurrencePushInfo>
    loop For each OccurrencePushInfo
        Scheduler->>Redis: getFcmTokens(memberId)
        Redis-->>Scheduler: Set<String> tokens
    end
    Scheduler->>FCM: sendBulkNotification(List<PushTargetDto>)
    FCM-->>Scheduler: (Handles notification results, updates reminderSent)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
FCM 세팅 (#39)
스케줄링 로직 구현 (#39)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
인증 관련 DTO 및 엔드포인트 리팩터링 (예: LogoutRequest.java, ReissueRequest.java 삭제, AuthController.java, AuthUseCase.java 변경 등) #39 이슈의 사전 푸시 알림 기능과 직접 관련 없는 인증 흐름 개선 작업으로 보임.
MemberContext 및 JWT에 deviceId 추가 (MemberContext.java, JwtProvider.java, JwtUtils.java 등) 사전 알림 기능과는 별개로 인증 컨텍스트 확장 작업이며, #39 요구사항에는 포함되지 않음.

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60200e7 and 406e616.

📒 Files selected for processing (1)
  • .semgrep/semgrep.yml (1 hunks)
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#39-fcm-push-notification

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🧹 Nitpick comments (17)
.gitignore (1)

41-42: Good: service account key is ignored

Ignoring whiplash-firebase-key.json is correct and prevents accidental commits.

If the file always resides under resources, you can be stricter:

+src/main/resources/whiplash-firebase-key.json

This avoids ignoring similarly named files elsewhere by accident.

.github/workflows/dev-ci.yml (1)

43-45: Harden secret materialization and fix naming nit

  • Step name ends with a trailing comma; optional cleanup.
  • Use printf to avoid echo injecting an extra newline. Also set strict shell flags.

Apply:

-      - name: Generate env.properties,
-        run: |
-          mkdir -p src/main/resources
-          echo "${{ secrets.DEV_ENV_PROPERTIES }}" > src/main/resources/env.properties
-          echo "${{ secrets.GOOGLE_JSON }}" | base64 -d > src/main/resources/google.json
-          echo "${{ secrets.FIREBASE_KEY_JSON }}" | base64 -d > src/main/resources/whiplash-firebase-key.json
+      - name: Generate env.properties and keys
+        shell: bash
+        run: |
+          set -euo pipefail
+          mkdir -p src/main/resources
+          printf '%s' "${{ secrets.DEV_ENV_PROPERTIES }}" > src/main/resources/env.properties
+          printf '%s' "${{ secrets.GOOGLE_JSON }}" | base64 -d > src/main/resources/google.json
+          printf '%s' "${{ secrets.FIREBASE_KEY_JSON }}" | base64 -d > src/main/resources/whiplash-firebase-key.json

Confirm the runtime matches the filename your Firebase config loads (i.e., classpath:whiplash-firebase-key.json).

Also applies to: 49-49

src/main/java/akuma/whiplash/domains/auth/application/dto/request/RegisterFcmTokenRequest.java (1)

1-12: Solid DTO; consider tightening validation and trimming input

Current @NotBlank is good. Consider:

  • Trimming whitespace at binding time to avoid tokens with leading/trailing spaces.
  • Optionally add a conservative upper bound to prevent abuse (e.g., 1–4096).

Example:

@Schema(description = "FCM 토큰", example = "djkhsa01whjas")
@NotBlank
@Size(max = 4096)
String fcmToken;

If you want automatic trimming, we can add a Jackson deserializer or a ControllerAdvice binder. I can provide a snippet.

src/main/java/akuma/whiplash/domains/alarm/application/mapper/AlarmMapper.java (2)

63-64: Correctly initializes reminderSent to false

This aligns with the new reminder workflow and avoids null/indeterminate states.

To reduce duplication, consider defaulting this field at the entity level (field default or DB column default) so callers don’t need to set it explicitly everywhere.


77-78: Consistent initialization for date-specific occurrences

Good consistency with the first-occurrence mapping.

Add a unit test asserting reminderSent=false on creation for both paths to prevent regressions.

.github/workflows/dev-cd.yml (2)

43-43: Comment/file name mismatch

The comment says “firebase-key.json”, but the file written is “whiplash-firebase-key.json”. Align the comment to prevent confusion.

Apply:

-      # 6. env.properties, google.json, firebase-key.json 생성
+      # 6. env.properties, google.json, whiplash-firebase-key.json 생성

45-49: Harden secret materialization step (fail-fast and avoid spurious newlines)

  • Add “set -euo pipefail” to fail on decode/write errors.
  • Prefer printf over echo to avoid trailing newline surprises.

Apply:

       - name: Generate env.properties
         run: |
+          set -euo pipefail
           mkdir -p src/main/resources
-          echo "${{ secrets.DEV_ENV_PROPERTIES }}" > src/main/resources/env.properties
-          echo "${{ secrets.GOOGLE_JSON }}" | base64 -d > src/main/resources/google.json
-          echo "${{ secrets.FIREBASE_KEY_JSON }}" | base64 -d > src/main/resources/whiplash-firebase-key.json
+          printf '%s' "${{ secrets.DEV_ENV_PROPERTIES }}" > src/main/resources/env.properties
+          printf '%s' "${{ secrets.GOOGLE_JSON }}" | base64 -d > src/main/resources/google.json
+          printf '%s' "${{ secrets.FIREBASE_KEY_JSON }}" | base64 -d > src/main/resources/whiplash-firebase-key.json
src/main/java/akuma/whiplash/infrastructure/redis/RedisConfig.java (1)

42-42: Enabling transactions on the default RedisTemplate: confirm need and demarcation strategy

setEnableTransactionSupport(true) binds connections per thread and can degrade throughput if applied globally. If only specific ops (e.g., FCM token upsert/remove) need MULTI/EXEC:

  • Option A: Provide a dedicated tx-enabled template bean for those paths, leave the default template non-transactional.
  • Option B: Add RedisTransactionManager and use @transactional(transactionManager="redisTransactionManager") on the service, or use SessionCallback with multi/exec.

I can add a RedisTransactionManager bean and a dedicated tx template if you prefer.

Example (outside current lines):

@Bean
public org.springframework.data.redis.transaction.RedisTransactionManager redisTransactionManager(
    RedisConnectionFactory cf
) {
    return new org.springframework.data.redis.transaction.RedisTransactionManager(cf);
}

@Bean(name = "txRedisTemplate")
public RedisTemplate<String, String> txRedisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, String> t = new RedisTemplate<>();
    t.setKeySerializer(new StringRedisSerializer());
    t.setValueSerializer(new StringRedisSerializer());
    t.setConnectionFactory(cf);
    t.setEnableTransactionSupport(true);
    return t;
}
src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java (1)

3-10: Consider using a more explicit field name (e.g., fcmToken).

"token" is ambiguous; clarifying the domain improves readability.

src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java (1)

86-88: Idempotency under concurrent schedulers.

If multiple scheduler instances run, both can pick the same rows (reminder_sent=false) and send duplicates before flipping the flag. Use a distributed lock (e.g., ShedLock) or an atomic update pattern.

Option A: Use ShedLock on the scheduled task.
Option B: Update with conditional DML (WHERE reminder_sent=false) and check affected rows before sending.
Option C: Add @Version optimistic locking and perform flag flip inside a transaction prior to send, then send; on conflict, skip.

src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java (1)

11-11: Interface extension LGTM; document time-window assumptions.

Method naming and return type are clear. Document whether start/end can represent cross-day windows or if callers must split by date.

I can add a short Javadoc to clarify expected usage.

src/main/java/akuma/whiplash/global/config/security/jwt/JwtProvider.java (3)

44-56: Add issuedAt to access tokens for better traceability

Setting iat improves debuggability and aligns with JWT best practices.

Apply:

 return Jwts.builder()
     .claim(ROLE, role)
     .claim(TYPE, ACCESS)
     .claim(DEVICE_ID, deviceId)
     .setSubject(memberId.toString())
+    .setIssuedAt(new Date())
     .setExpiration(Date.from(accessDate))
     .signWith(key, SignatureAlgorithm.HS256)
     .compact();

72-76: Be explicit about Redis key prefix string; avoid relying on enum toString

Using TokenType enum directly relies on its toString; prefer name() for stability/clarity.

-        String redisKey = REFRESH + ":" + memberId + ":" + deviceId;
+        String redisKey = REFRESH.name() + ":" + memberId + ":" + deviceId;

Optionally, centralize the key format:

// outside range; for reference only
private static final String REFRESH_KEY_FMT = "REFRESH:%s:%s";
String redisKey = String.format(REFRESH_KEY_FMT, memberId, deviceId);

88-90: Prefer Instant.now() for expiration calculation (DST-safe and simpler)

Functionally equivalent but clearer and avoids any DST edge quirks.

Outside selected range example:

private Instant getExpiration(long expirationSeconds) {
    return Instant.now().plusSeconds(expirationSeconds);
}
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)

38-43: N × Redis round-trips

redisService.getFcmTokens is invoked per occurrence, resulting in one network call per member.
Fetch tokens in bulk or pipeline them to cut latency when thousands of alarms hit the same minute.

src/main/java/akuma/whiplash/infrastructure/redis/RedisService.java (1)

117-122: Very long keys

keyTokenToDevice embeds the raw FCM token.
At ~4 KB per token this may exceed Redis key length best-practice and hurts memory.
Consider hashing the token (SHA-256) before using it as part of the key.

src/main/java/akuma/whiplash/domains/auth/domain/service/AuthCommandServiceImpl.java (1)

63-66: Key scan on every login

redisRepository.getKeys("REFRESH:" + member.getId() + ":*") performs a wildcard scan per login.
For high-volume systems this becomes O(total keys).
Store the current deviceId in Redis and compare directly instead of scanning.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6aed11 and 44391d1.

📒 Files selected for processing (28)
  • .github/workflows/dev-cd.yml (1 hunks)
  • .github/workflows/dev-ci.yml (1 hunks)
  • .gitignore (1 hunks)
  • build.gradle (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/OccurrencePushInfo.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/application/mapper/AlarmMapper.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (3 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/auth/application/dto/etc/MemberContext.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/auth/application/dto/request/LogoutRequest.java (0 hunks)
  • src/main/java/akuma/whiplash/domains/auth/application/dto/request/RegisterFcmTokenRequest.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/auth/application/dto/request/ReissueRequest.java (0 hunks)
  • src/main/java/akuma/whiplash/domains/auth/application/mapper/AuthMapper.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/auth/application/usecase/AuthUseCase.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/auth/domain/service/AuthCommandService.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/auth/domain/service/AuthCommandServiceImpl.java (4 hunks)
  • src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java (3 hunks)
  • src/main/java/akuma/whiplash/global/config/security/jwt/JwtProvider.java (1 hunks)
  • src/main/java/akuma/whiplash/global/config/security/jwt/JwtUtils.java (3 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (1 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/FirebaseConfig.java (1 hunks)
  • src/main/java/akuma/whiplash/infrastructure/redis/RedisConfig.java (1 hunks)
  • src/main/java/akuma/whiplash/infrastructure/redis/RedisService.java (1 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/akuma/whiplash/domains/auth/application/dto/request/ReissueRequest.java
  • src/main/java/akuma/whiplash/domains/auth/application/dto/request/LogoutRequest.java
🧰 Additional context used
📓 Path-based instructions (4)
**/*.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/application/mapper/AlarmMapper.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
  • src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java
  • src/main/java/akuma/whiplash/global/config/security/jwt/JwtProvider.java
  • src/main/java/akuma/whiplash/infrastructure/redis/RedisConfig.java
  • src/main/java/akuma/whiplash/domains/auth/application/mapper/AuthMapper.java
  • src/main/java/akuma/whiplash/domains/auth/application/dto/etc/MemberContext.java
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/OccurrencePushInfo.java
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
  • src/main/java/akuma/whiplash/domains/auth/application/dto/request/RegisterFcmTokenRequest.java
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FirebaseConfig.java
  • src/main/java/akuma/whiplash/domains/auth/domain/service/AuthCommandService.java
  • src/main/java/akuma/whiplash/domains/auth/application/usecase/AuthUseCase.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
  • src/main/java/akuma/whiplash/global/config/security/jwt/JwtUtils.java
  • src/main/java/akuma/whiplash/infrastructure/redis/RedisService.java
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
  • src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java
  • src/main/java/akuma/whiplash/domains/auth/domain/service/AuthCommandServiceImpl.java
**/*{Entity,Dto}.java

📄 CodeRabbit Inference Engine (AGENT.md)

모든 PK는 alarmId, userId처럼 도메인명을 포함하여 명명합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
**/*Dto.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*Dto.java: 요청/응답 객체의 필드명은 DB 컬럼명을 기준으로 하며, 필요시 userNickname, adminNickname 등 식별자를 붙입니다.
리스트 응답 필드는 alarms, tickets처럼 복수형 도메인명으로 작성합니다.
Enum은 .name() 값 그대로, 날짜는 ISO_LOCAL_DATE 또는 ISO_LOCAL_DATE_TIME 형식으로 반환합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
**/*{Controller,Service,Repository}.java

📄 CodeRabbit Inference Engine (AGENT.md)

Controller/Service는 getUser, createUser, removeUser 등 동사 기반으로 명명하고, Repository는 findByUser, insertUser, deleteUser처럼 명확히 구분합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
  • src/main/java/akuma/whiplash/domains/auth/domain/service/AuthCommandService.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
  • src/main/java/akuma/whiplash/infrastructure/redis/RedisService.java
  • src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java
🧠 Learnings (2)
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*{Entity,Dto}.java : 모든 PK는 `alarmId`, `userId`처럼 도메인명을 포함하여 명명합니다.

Applied to files:

  • src/main/java/akuma/whiplash/domains/alarm/application/mapper/AlarmMapper.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*Dto.java : 리스트 응답 필드는 `alarms`, `tickets`처럼 복수형 도메인명으로 작성합니다.

Applied to files:

  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
🧬 Code Graph Analysis (2)
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java (2)
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmRepository.java (1)
  • AlarmRepository (10-23)
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOffLogRepository.java (1)
  • AlarmOffLogRepository (10-24)
src/main/java/akuma/whiplash/global/config/security/jwt/JwtUtils.java (2)
src/main/java/akuma/whiplash/domains/auth/application/mapper/AuthMapper.java (1)
  • AuthMapper (9-36)
src/main/java/akuma/whiplash/global/config/security/jwt/JwtAuthenticationFilter.java (2)
  • Override (33-66)
  • Component (22-93)
🔇 Additional comments (14)
build.gradle (1)

68-70: Verify Firebase Admin transitive dependencies in build.gradle

We couldn’t inspect transitive versions here due to an invalid JAVA_HOME. Please run these locally to catch any mismatches:

• File: build.gradle (lines 68–70)
• Commands to run in your Java-configured environment:

./gradlew dependencies --configuration runtimeClasspath | sed -n '/firebase-admin/,/---/p'
./gradlew dependencyInsight --configuration runtimeClasspath --dependency google-auth-library-oauth2-http
./gradlew dependencyInsight --configuration runtimeClasspath --dependency google-http-client-jackson2

If you find conflicting versions of google-auth-library-oauth2-http or google-http-client-jackson2, align them using dependencyManagement, an enforced platform, or explicit constraints.

src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (1)

61-64: Good addition; validate time-window semantics (midnight wrap) and nulls.

The delegation is clean. Ensure the caller and/or repository correctly handle ranges that cross midnight (e.g., 23:30–00:30) and that date/start/end are validated (non-null, start ≤ end if single-date query).

Would you confirm how AlarmReminderScheduler constructs the window when it crosses days? If needed, I can propose a small guard or documentation for this method.

src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/OccurrencePushInfo.java (1)

5-12: Record DTO looks good; verify Lombok builder for records.

The PK-style names comply with guidelines. If using Lombok < 1.18.20, @builder on records may not be fully supported.

If the project Lombok version is older, consider removing @builder or updating Lombok. I can adjust usages accordingly if needed.

src/main/java/akuma/whiplash/global/config/security/jwt/JwtProvider.java (2)

62-70: LGTM: Refresh token now carries deviceId claim

Embedding deviceId in the refresh token claim aligns with device-specific session management.


41-41: LGTM: Introduced DEVICE_ID claim key constant

Consistent claim key usage reduces typos and drift.

src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java (2)

3-3: LGTM: OccurrencePushInfo import

Import aligns with JPQL projection usage below.


7-7: LGTM: LocalTime import

Matches method parameters for time range filtering.

src/main/java/akuma/whiplash/domains/auth/domain/service/AuthCommandService.java (1)

11-12: LGTM: Simplified signatures using MemberContext

Reduces DTO churn and keeps deviceId routing consistent across logout/reissue.

src/main/java/akuma/whiplash/domains/auth/application/usecase/AuthUseCase.java (1)

23-29: LGTM: Use MemberContext for logout and reissue

Consistent with device-aware token handling introduced in JWT/impl.

src/main/java/akuma/whiplash/infrastructure/redis/RedisService.java (1)

38-70: Transactions require template config

upsertFcmToken relies on MULTI/EXEC.
Ensure RedisTemplate#setEnableTransactionSupport(true) is done in RedisConfig, otherwise the operations run outside a Tx and may leave keys inconsistent.

src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java (4)

7-7: Import looks correct and scoped to usage.

No issues; the DTO is used in the new endpoint.


46-48: Logout via MemberContext only—verify device scoping and null handling.

Confirm that MemberContext is always present on this path and contains a non-null deviceId so device-scoped logout (including FCM token cleanup) behaves as expected.


57-61: Reissue flow depends on @AuthenticationPrincipal—ensure refresh-token-based auth populates MemberContext.

The main use case for reissue is an expired access token. Verify your security chain builds MemberContext from a refresh token (e.g., cookie) for this endpoint, not from an access token, and that this path is permitted accordingly.


63-69: Unify to MemberContext and consider idempotent semantics.

  • The AI summary states use-case signatures rely solely on MemberContext. Align this call accordingly.
  • Registering an FCM token is idempotent; consider PUT for clearer semantics.

Controller changes within this hunk:

-    @PostMapping("/fcm-token")
+    @PutMapping("/fcm-token")
@@
-        authUseCase.registerFcmToken(memberContext.memberId(), memberContext.deviceId(), request.fcmToken());
+        authUseCase.registerFcmToken(memberContext, request.fcmToken());

Additionally add the import outside this hunk:

import org.springframework.web.bind.annotation.PutMapping;

Likely an incorrect or invalid review comment.

Comment on lines +3 to +10
public record PushTargetDto(
String token,
String address,
Long memberId,
Long occurrenceId
) {

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Mask FCM token in logs (record default toString leaks secrets).

Java records generate a toString including all fields. If any logging includes PushTargetDto, the raw FCM token will be exposed. Override toString to mask the token.

Apply this diff:

 public record PushTargetDto(
     String token,
     String address,
     Long memberId,
     Long occurrenceId
 ) {
-
-}
+    @Override
+    public String toString() {
+        String masked = token == null
+            ? "null"
+            : (token.length() <= 8 ? "****" : token.substring(0, 4) + "..." + token.substring(token.length() - 4));
+        return "PushTargetDto[token=" + masked
+            + ", address=" + address
+            + ", memberId=" + memberId
+            + ", occurrenceId=" + occurrenceId + "]";
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public record PushTargetDto(
String token,
String address,
Long memberId,
Long occurrenceId
) {
}
public record PushTargetDto(
String token,
String address,
Long memberId,
Long occurrenceId
) {
@Override
public String toString() {
String masked = token == null
? "null"
: (token.length() <= 8
? "****"
: token.substring(0, 4) + "..." + token.substring(token.length() - 4));
return "PushTargetDto[token=" + masked
+ ", address=" + address
+ ", memberId=" + memberId
+ ", occurrenceId=" + occurrenceId + "]";
}
}
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
around lines 3 to 10, the default toString method of the record exposes the raw
FCM token, risking secret leakage in logs. Override the toString method to
return a string representation that masks or obfuscates the token field while
including the other fields normally. This prevents sensitive token data from
appearing in logs.

private final RedisService redisService;
private final FcmService fcmService;

// @Scheduled(cron = "0 * * * * *") // 매 분마다 실행
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Scheduler is disabled

@Scheduled is commented out. If the job is meant to run in prod, keep the annotation and gate it with a Spring profile instead of commenting it out.

@Profile("prod")
@Scheduled(cron = "0 * * * * *")
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
at line 27, the @Scheduled annotation is commented out, disabling the scheduler.
To enable the scheduler only in production, uncomment the @Scheduled annotation
and add the @Profile("prod") annotation above the method or class. This will
ensure the scheduled job runs only when the 'prod' Spring profile is active.

}


// TODO: 사용자가 알람이 울리는 날 알람을 끄지 않아 그 다음날로 넘어간 경우에 어떻게 처리할건지
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Define and implement carry-over rules for missed-day check-in/off; align with alarmOff logic

Current TODO leaves ambiguity. Recommend:

  • Accept client time in check-in (signature like alarmOff) to avoid TZ skew.
  • Resolve target occurrence: if the most recent occurrence (yesterday/today) has rung and is not deactivated, and within a defined grace window, treat it as the check-in target; otherwise fallback to the next scheduled occurrence.
  • Reuse the “after ringing” logic from alarmOff to avoid divergence.

I can draft a helper like resolveCheckinTargetDate(alarm, clientNow) and update checkinAlarm accordingly. Want me to push a patch?

Would you like a patch that:

  • Updates checkinAlarm(Long memberId, Long alarmId, AlarmCheckinRequest request, LocalDateTime clientNow)
  • Introduces resolveCheckinTargetDate with parity to alarmOff?

🏁 Script executed:

#!/bin/bash
# Locate the alarmOff method signature and its implementation
rg "alarmOff\s*\(" -n src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java

# Locate the checkinAlarm method signature and its implementation
rg "checkinAlarm\s*\(" -n src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java

Length of output: 420


🏁 Script executed:

#!/bin/bash
# Display alarmOff method with context
rg "public AlarmOffResultResponse alarmOff" -n -A 30 src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java

# Display checkinAlarm method with context
rg "public void checkinAlarm" -n -A 30 src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java

Length of output: 3038


Align checkinAlarm with alarmOff: implement carry-over rules & clientNow parameter

The current TODO at line 212 highlights that past (missed) alarms aren’t handled—checkinAlarm always uses LocalDate.now() and DateUtil.getNextOccurrenceDate, so:

  • It risks client/server TZ skew (no clientNow), unlike alarmOff.
  • It ignores any prior-day occurrence that rang but wasn’t deactivated.
  • It diverges from alarmOff’s “after ringing” logic.

Recommended changes:

  • Update signature to
    public void checkinAlarm(Long memberId, Long alarmId, AlarmCheckinRequest request, LocalDateTime clientNow)
  • Introduce
    private LocalDate resolveCheckinTargetDate(AlarmEntity alarm, LocalDateTime clientNow)
    mirroring alarmOff’s occurrence resolution (most recent und eactivated within a grace window, else next scheduled).
  • In checkinAlarm, replace LocalDate today = LocalDate.now(); … getNextOccurrenceDate with the helper and include validClockSkew(clientNow, serverNow) if needed.
  • Reuse the post-ringing/deactivation logic from alarmOff to ensure consistency.

Let me know if you’d like me to draft this refactor patch.

🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
at line 199, update the checkinAlarm method to accept a LocalDateTime clientNow
parameter to handle client/server timezone differences. Create a private helper
method resolveCheckinTargetDate that determines the correct alarm occurrence
date based on clientNow, similar to alarmOff's logic for handling missed or
carried-over alarms. Replace the current usage of LocalDate.now() and
DateUtil.getNextOccurrenceDate in checkinAlarm with this helper method and
incorporate validClockSkew checks as done in alarmOff. Ensure the post-ringing
and deactivation logic is consistent between checkinAlarm and alarmOff to
properly handle alarms that were not turned off on the day they rang.

Comment on lines +73 to +75
@Column(name = "reminder_sent", nullable = false)
private boolean reminderSent;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DB migration required for non-null reminder_sent.

Adding a non-nullable column needs a migration that sets a default and backfills existing rows (e.g., default false). Otherwise deploys can fail.

Please ensure a migration like:

  • Flyway/Liquibase DDL: add column reminder_sent boolean not null default false
  • Backfill existing rows to false (if default not provided)

I can generate the migration script if you share the dialect.


🏁 Script executed:

#!/bin/bash
# Check for migrations touching 'reminder_sent'
rg -n --pretty 'reminder_sent'

Length of output: 176


Add DB migration for non-nullable reminder_sent column

Adding a non-nullable column without a default/backfill will cause deploy failures. Please create a migration that:

  • Uses your chosen tool (Flyway, Liquibase, etc.) to add the column with a default value:
    ALTER TABLE alarm_occurrence ADD COLUMN reminder_sent BOOLEAN NOT NULL DEFAULT FALSE;
  • Ensures existing rows are set to false (if your dialect doesn’t apply the default to backfill automatically):
    UPDATE alarm_occurrence SET reminder_sent = FALSE;

Let me know your SQL dialect or migration framework and I can provide a ready-to-use script.

🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java
around lines 73 to 75, you added a non-nullable boolean column reminderSent
without a default value or backfill, which will cause deployment failures. To
fix this, create a database migration using your migration tool (Flyway,
Liquibase, etc.) that adds the reminder_sent column with a default value of
FALSE and updates existing rows to set reminder_sent to FALSE. This ensures the
column is non-nullable without breaking existing data.

Comment on lines +73 to 86
public void validateRefreshTokenExists(String refreshTokenFromClient, Long memberId, String deviceId) {
String refreshTokenInRedis = redisRepository
.getValues(REFRESH + ":" + memberId + ":" + deviceId)
.orElseThrow(() -> ApplicationException.from(TOKEN_NOT_FOUND));

if (!redisToken.equals(refreshToken)) {
// 재사용/탈취 의심 → 정책에 따라 해당 키 삭제, 전체 디바이스 무효화도 가능
if (!refreshTokenInRedis.equals(refreshTokenFromClient)) {
throw ApplicationException.from(INVALID_TOKEN);
}
}

public void expireRefreshToken(Long memberId, String deviceId) {
redisRepository.deleteValues(REFRESH.toString() + ":" + memberId + ":" + deviceId);
}

private Claims parseClaims(String token) {
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));

return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
redisRepository.deleteValues(REFRESH + ":" + memberId + ":" + deviceId);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Two Redis abstractions in one class

JwtUtils still depends on RedisRepository while the new codebase standardises on RedisService.
Unify to a single abstraction to keep key-building rules consistent and avoid duplicate connection pools.

🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/global/config/security/jwt/JwtUtils.java between
lines 73 and 86, replace all usages of RedisRepository with RedisService to
unify the Redis abstraction. Update method calls accordingly to use
RedisService's API, ensuring key-building logic remains consistent and avoid
maintaining multiple Redis connections. Remove RedisRepository dependency and
inject RedisService instead.

Comment on lines +72 to +80
if (res.isSuccessful()) {
// 푸시 성공 시 occurrence 테이블에 전송 완료 처리
alarmOccurrenceRepository.findById(dto.occurrenceId())
.ifPresent(occ -> {
occ.updateReminderSent(true);
// @Transactional 사용하지 않기 때문에 더티 체킹 동작 X -> 반영을 위해 save 메서드 호출
alarmOccurrenceRepository.save(occ);
});
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

O(N × tokens) DB writes

Inside handleSendResult every successful token causes findById() + save().
For ≥500 tokens pointing to the same occurrence this spams the DB.

Gather unique occurrenceIds and update each once:

batch.stream()
     .filter(res::isSuccessful)
     .map(PushTargetDto::occurrenceId)
     .distinct()
     .forEach(id -> alarmOccurrenceRepository.updateReminderSent(id, true));
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java around
lines 72 to 80, the current code performs a findById() and save() for each
successful token, causing excessive database writes when many tokens share the
same occurrenceId. To fix this, collect all unique occurrenceIds from the
successful tokens in the batch, then update each occurrence once by calling a
repository method like updateReminderSent(id, true) for each distinct id,
avoiding repeated find and save operations.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44391d1 and 17d6e98.

📒 Files selected for processing (2)
  • .coderabbit.yaml (1 hunks)
  • .semgrep/semgrep.yml (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.

Applied to files:

  • .coderabbit.yaml
  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 사용되지 않는 변수나 주석은 제거를 제안합니다.

Applied to files:

  • .coderabbit.yaml
  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*{Controller,Service,Repository}.java : Controller/Service는 `getUser`, `createUser`, `removeUser` 등 동사 기반으로 명명하고, Repository는 `findByUser`, `insertUser`, `deleteUser`처럼 명확히 구분합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*{ErrorCode,Exception}.java : 허용된 HttpStatus만 사용합니다: 400, 401, 403, 404, 409

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*Test.java : 테스트 설명은 도메인 관점에서 작성되었는지 확인합니다.

Applied to files:

  • .semgrep/semgrep.yml
🪛 YAMLlint (1.37.1)
.coderabbit.yaml

[warning] 15-15: comment not indented like content

(comments-indentation)


[error] 83-83: too many spaces after colon

(colons)


[error] 83-83: too many spaces inside braces

(braces)


[error] 83-83: too many spaces after comma

(commas)


[error] 83-83: too many spaces inside braces

(braces)


[error] 84-84: too many spaces after colon

(colons)


[error] 84-84: too many spaces inside braces

(braces)


[error] 84-84: too many spaces inside braces

(braces)


[error] 85-85: too many spaces after colon

(colons)


[error] 85-85: too many spaces inside braces

(braces)


[error] 85-85: too many spaces inside braces

(braces)


[error] 86-86: too many spaces after colon

(colons)


[error] 86-86: too many spaces inside braces

(braces)


[error] 86-86: too many spaces inside braces

(braces)


[error] 87-87: too many spaces inside braces

(braces)


[error] 87-87: too many spaces inside braces

(braces)


[error] 88-88: too many spaces after colon

(colons)


[error] 88-88: too many spaces inside braces

(braces)


[error] 88-88: too many spaces inside braces

(braces)


[error] 89-89: too many spaces after colon

(colons)


[error] 89-89: too many spaces inside braces

(braces)


[error] 89-89: too many spaces inside braces

(braces)


[error] 90-90: too many spaces after colon

(colons)


[error] 90-90: too many spaces inside braces

(braces)


[error] 90-90: too many spaces inside braces

(braces)


[error] 91-91: too many spaces after colon

(colons)


[error] 91-91: too many spaces inside braces

(braces)


[error] 91-91: too many spaces inside braces

(braces)

.semgrep/semgrep.yml

[error] 10-10: trailing spaces

(trailing-spaces)


[warning] 77-77: comment not indented like content

(comments-indentation)

🔇 Additional comments (2)
.coderabbit.yaml (2)

2-11: 구성 전반은 목적에 부합합니다.

ko-KR 언어, 자동 리뷰, finishing_touches, 도구 활성화, semgrep 경로 연계 등은 설정 의도와 잘 맞습니다. yamllint 수정 후 그대로 유지하셔도 됩니다.

Also applies to: 75-81, 96-97


18-41: reviews.instructions는 CodeRabbit v2에서 공식 지원됩니다. 해당 스키마 필드는 전역 혹은 경로별 코드 리뷰 지침 설정에 문제가 없으니, 원래 코멘트는 무시하시기 바랍니다.

Likely an incorrect or invalid review comment.

- 자정 크로스란

예를 들어, now = 23:59라면
start = 00:58 (다음날 시간)
end = 01:00 (다음날 시간)
그런데 date는 여전히 “오늘”이라서 date=오늘 AND time BETWEEN 00:58~01:00 같은 조건은 절대 성립하지 않는다. 실제로는 “내일 00:58~01:00”을 찾아야 하기 때문
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (8)
.semgrep/semgrep.yml (1)

143-153: 시간 소스 차단 범위 확장 권장(LocalDate/ZonedDateTime/Instant).

테스트 안정성과 시간 주입 일관성을 위해 LocalDate.now(), ZonedDateTime.now(), Instant.now()도 함께 금지하는 규칙을 추가하세요.

   # 6) 시간 소스: LocalDateTime.now() 직접 사용 금지(Clock/DateUtil 사용 권장)
   - id: forbid-direct-now
     languages: [java]
     message: "LocalDateTime.now() 직접 호출을 지양하고 Clock/DateUtil을 통해 주입받아 사용하세요."
     severity: WARNING
     pattern: LocalDateTime.now(...)
     paths:
       include:
         - "domains/**/application/**"
         - "domains/**/domain/**"
         - "domains/**/presentation/**"
+
+  # 6-b) 추가 시간 소스도 동일하게 금지
+  - id: forbid-direct-now-others
+    languages: [java]
+    message: "시간 소스(LocalDate/ZonedDateTime/Instant).now() 직접 호출을 지양하고 Clock/DateUtil을 통해 주입받아 사용하세요."
+    severity: WARNING
+    patterns:
+      - pattern-either:
+          - pattern: LocalDate.now(...)
+          - pattern: ZonedDateTime.now(...)
+          - pattern: Instant.now(...)
+    paths:
+      include:
+        - "domains/**/application/**"
+        - "domains/**/domain/**"
+        - "domains/**/presentation/**"
.coderabbit.yaml (1)

15-16: 주석 들여쓰기 정렬(yamllint 경고 제거).

auto_review 블록 내부 주석 들여쓰기가 키 레벨과 맞지 않습니다. 아래처럼 들여쓰기를 맞추세요.

-#    ignore_title_keywords: ["WIP", "DO NOT MERGE"]
-#    base_branches: ["main", "develop"]
+    # ignore_title_keywords: ["WIP", "DO NOT MERGE"]
+    # base_branches: ["main", "develop"]
src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java (2)

7-7: DTO 유효성 검사 확인 (@notblank 권장)

RegisterFcmTokenRequest.fcmToken 필드에 Bean Validation(@notblank 등)이 선언되어 있는지 확인해 주세요. 컨트롤러는 현재 @Valid만 유지하고 별도 수동 검증은 생략된 상태이므로 DTO 단 검증이 필수입니다.


63-71: FCM 토큰 등록: 동의 조건 명시 및 보안 스키마 추가 제안

문서에 “푸시 수신에 동의한 사용자만 등록됩니다.”를 명시하고, 보안 스키마도 추가해 주세요. 이전 코멘트와 동일한 방향입니다.

적용 diff:

-    @CustomErrorCodes(commonErrorCodes = {BAD_REQUEST})
-    @Operation(summary = "FCM 토큰 등록", description = "FCM 토큰을 등록합니다.")
+    @CustomErrorCodes(commonErrorCodes = {BAD_REQUEST})
+    @Operation(
+        summary = "FCM 토큰 등록",
+        description = "푸시 수신에 동의한 사용자만 등록됩니다.",
+        security = { @SecurityRequirement(name = "bearerAuth") }
+    )

추가 import:

import io.swagger.v3.oas.annotations.security.SecurityRequirement;
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java (1)

199-199: checkinAlarm의 ‘전일 carry-over’ 규칙 미정의 — alarmOff와 동등 규칙 도입 제안

현재 TODO 상태이며, alarmOff에 존재하는 클라이언트 시간 검증/캐리오버 판별 로직이 checkinAlarm에는 없습니다. 동일한 사용자 경험을 위해 alarmOff의 규칙을 재사용하세요: clientNow 수신 → 시간 왜곡(validClockSkew) 검증 → “울린 후 미비활성화”면 다음 텀을 타겟으로 해석.

권장 변경 요약:

  • checkinAlarm(Long memberId, Long alarmId, AlarmCheckinRequest request, LocalDateTime clientNow)로 시그니처 확장
  • resolveCheckinTargetDate(alarm, clientNow) 헬퍼 도입(알람 울림 이후/동일 주 검증 포함)
  • DateUtil.getNextOccurrenceDate 사용 시 alarmOff와 동일한 분기 사용

예시(참고용, 전체 패치 제공 가능):

private LocalDate resolveCheckinTargetDate(AlarmEntity alarm, LocalDateTime clientNow) {
    LocalDate clientDate = clientNow.toLocalDate();
    Optional<AlarmOccurrenceEntity> today = alarmOccurrenceRepository.findByAlarmIdAndDate(alarm.getId(), clientDate);

    boolean afterRinging = today
        .filter(o -> o.isAlarmRinging() && o.getDeactivateType() == DeactivateType.NONE)
        .map(o -> clientNow.isAfter(o.getTime().atDate(clientDate)))
        .orElse(false);

    Set<DayOfWeek> repeats = alarm.getRepeatDays().stream().map(Weekday::getDayOfWeek).collect(Collectors.toSet());
    LocalDate searchStart = afterRinging ? clientDate.plusDays(1) : clientDate;
    return DateUtil.getNextOccurrenceDate(repeats, searchStart);
}

원하시면 전체 리팩터 패치를 드리겠습니다.

src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java (1)

1-13: 기본 toString이 FCM 토큰을 그대로 노출 — 마스킹 필요

Java record의 기본 toString이 token을 평문으로 출력합니다. 로그/예외에 노출될 경우 보안 이슈입니다. token 마스킹을 위해 toString을 오버라이드하세요. (주소 또한 로그 노출 최소화를 권장)

 public record PushTargetDto(
     String token,
     String address,
     Long memberId,
     Long occurrenceId
 ) {
-
-}
+    @Override
+    public String toString() {
+        String masked = token == null
+            ? "null"
+            : (token.length() <= 8 ? "****" : token.substring(0, 4) + "..." + token.substring(token.length() - 4));
+        String addrMasked = address == null ? "null" : address.replaceAll("(.{6}).+", "$1***");
+        return "PushTargetDto[token=" + masked
+            + ", address=" + addrMasked
+            + ", memberId=" + memberId
+            + ", occurrenceId=" + occurrenceId + "]";
+    }
+}
src/main/java/akuma/whiplash/infrastructure/firebase/FirebaseConfig.java (1)

17-34: InputStream 자원 미반환 & 탭/스페이스 혼용

serviceAccount 스트림을 닫지 않아 잠재적 자원 누수가 발생합니다.
이전 PR에서도 같은 지적이 있었으므로 빠른 반영이 필요합니다.

-    @PostConstruct
-    public void init(){
-        try{
-            InputStream serviceAccount = new ClassPathResource("whiplash-firebase-key.json").getInputStream();
+    @PostConstruct
+    public void init() {
+        try (InputStream serviceAccount =
+                 new ClassPathResource("whiplash-firebase-key.json").getInputStream()) {
             FirebaseOptions options = new FirebaseOptions.Builder()
                 .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                 .build();

또한 탭과 스페이스가 섞여 있어 코드 스타일이 흔들립니다. 프로젝트 규칙에 맞춰 통일해 주세요.

src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)

30-34: 자정 교차 시각창 여전히 불완전

now.plusMinutes(59/61) 계산은 날짜를 그대로 두므로 23:59 실행 시 “내일 00:58~01:00” 범위가 누락됩니다.
이전에도 동일 지적이 있었으니 서비스 계층에서 날짜 보정을 했는지 재확인 바랍니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17d6e98 and 60200e7.

📒 Files selected for processing (13)
  • .coderabbit.yaml (1 hunks)
  • .semgrep/semgrep.yml (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandService.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (3 hunks)
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java (3 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (1 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/FirebaseConfig.java (1 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/dto/FcmSendResult.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*{Controller,Service,Repository}.java

📄 CodeRabbit Inference Engine (AGENT.md)

Controller/Service는 getUser, createUser, removeUser 등 동사 기반으로 명명하고, Repository는 findByUser, insertUser, deleteUser처럼 명확히 구분합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandService.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
  • src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
**/*.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandService.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FirebaseConfig.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/dto/FcmSendResult.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
  • src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
**/*Dto.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*Dto.java: 요청/응답 객체의 필드명은 DB 컬럼명을 기준으로 하며, 필요시 userNickname, adminNickname 등 식별자를 붙입니다.
리스트 응답 필드는 alarms, tickets처럼 복수형 도메인명으로 작성합니다.
Enum은 .name() 값 그대로, 날짜는 ISO_LOCAL_DATE 또는 ISO_LOCAL_DATE_TIME 형식으로 반환합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
**/*{Entity,Dto}.java

📄 CodeRabbit Inference Engine (AGENT.md)

모든 PK는 alarmId, userId처럼 도메인명을 포함하여 명명합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
🧠 Learnings (14)
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*Dto.java : 리스트 응답 필드는 `alarms`, `tickets`처럼 복수형 도메인명으로 작성합니다.

Applied to files:

  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandService.java
  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
  • .semgrep/semgrep.yml
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*{Entity,Dto}.java : 모든 PK는 `alarmId`, `userId`처럼 도메인명을 포함하여 명명합니다.

Applied to files:

  • src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
  • .semgrep/semgrep.yml
  • src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java
  • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 사용되지 않는 변수나 주석은 제거를 제안합니다.

Applied to files:

  • .coderabbit.yaml
  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.

Applied to files:

  • .coderabbit.yaml
  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*{Controller,Service,Repository}.java : Controller/Service는 `getUser`, `createUser`, `removeUser` 등 동사 기반으로 명명하고, Repository는 `findByUser`, `insertUser`, `deleteUser`처럼 명확히 구분합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*{ErrorCode,Exception}.java : 허용된 HttpStatus만 사용합니다: 400, 401, 403, 404, 409

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*ErrorCode.java : 각 도메인에서 발생 가능한 에러는 `CustomErrorCodes`로 명시합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*ErrorCode.java : 에러 코드는 `Domain_x0n` 형태로 작성되었는지 확인합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*ErrorCode.java : message는 `~입니다.` 형태의 문장으로 작성되어야 합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*Test.java : 테스트명은 `DisplayName`을 사용하고, "음료를 추가하면 주문 목록에 담긴다"와 같이 문장형으로 작성합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*Test.java : 테스트 설명은 도메인 관점에서 작성되었는지 확인합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*Test.java : 테스트는 `Given-When-Then` 구조로 작성되었는지 확인합니다.

Applied to files:

  • .semgrep/semgrep.yml
📚 Learning: 2025-08-03T16:11:00.389Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: 각 디렉토리는 역할에 따라 적절히 분리되어야 합니다: application(DTO, Mapper, UseCase), domain(비즈니스 상수 및 서비스), persistence(entity, repository), presentation(controller)

Applied to files:

  • .semgrep/semgrep.yml
🧬 Code Graph Analysis (4)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandService.java (1)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmOccurrenceBatchService.java (1)
  • Slf4j (18-78)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (2)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)
  • Slf4j (18-74)
src/main/java/akuma/whiplash/infrastructure/redis/RedisService.java (1)
  • Service (13-123)
src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java (8)
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmInfoPreviewResponse.java (1)
  • Builder (8-54)
src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/AlarmOccurrenceCreateBatchResult.java (1)
  • Builder (5-11)
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/CreateAlarmOccurrenceResponse.java (1)
  • CreateAlarmOccurrenceResponse (5-10)
src/main/java/akuma/whiplash/domains/alarm/application/dto/request/AlarmRegisterRequest.java (1)
  • Schema (12-50)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberTermsModifyRequest.java (1)
  • MemberTermsModifyRequest (6-15)
src/main/java/akuma/whiplash/domains/auth/application/dto/response/TokenResponse.java (1)
  • TokenResponse (5-11)
src/main/java/akuma/whiplash/domains/place/application/dto/response/PlaceDetailResponse.java (1)
  • PlaceDetailResponse (5-9)
src/main/java/akuma/whiplash/domains/auth/application/dto/etc/MemberContext.java (1)
  • MemberContext (6-15)
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java (1)
src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java (1)
  • Entity (27-82)
🪛 YAMLlint (1.37.1)
.semgrep/semgrep.yml

[error] 53-53: duplication of key "pattern-not" in mapping

(key-duplicates)


[error] 54-54: duplication of key "pattern-not" in mapping

(key-duplicates)


[error] 55-55: duplication of key "pattern-not" in mapping

(key-duplicates)


[error] 56-56: duplication of key "pattern-not" in mapping

(key-duplicates)

🔇 Additional comments (8)
src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java (4)

46-48: MemberContext 기반으로 로그아웃 시그니처 단순화된 점 좋습니다.


46-48: 요청 본문 제거에 따른 클라이언트 영향도 점검

기존에 요청 본문을 보내던 클라이언트가 있다면 호환성 문제가 생길 수 있습니다. 앱/프론트 업데이트, API 문서(ChangeLog 포함), e2e 테스트 갱신 여부를 확인해 주세요.


57-61: 토큰 재발급 시그니처 간소화 👍

MemberContext만으로 재발급할 수 있게 정리된 점은 가독성과 유지보수에 이점이 있습니다.


57-61: /api/auth/reissue 경로에 Refresh 토큰 기반 MemberContext 주입 확인됨

  • JwtAuthenticationFilter.doFilterInternal에서 request URI에 /reissue가 포함된 경우 jwtUtils.validateToken(response, token, REFRESH)를 호출하도록 구현되어 있습니다.
  • JwtUtils.validateToken 내부에서 AuthMapper.mapToMemberContext를 통해 MemberContext를 생성하고 SecurityContext에 설정하므로, 액세스 토큰 만료 시에도 /api/auth/reissue 호출 시 @AuthenticationPrincipal MemberContext 주입이 정상 동작합니다.

따라서 추가 검증이나 수정이 필요하지 않습니다.

src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandService.java (1)

8-8: 인터페이스에 사전 알림 마킹 메서드 추가: 시그니처 적절. 계약/주석 보강 제안

Set 사용으로 중복 방지 의도가 분명하고 CommandService에 위치도 적절합니다. 다만 동작 계약을 명시해 두는 것이 유지보수에 유리합니다. 선택 사항으로 Javadoc 추가를 권장합니다.

다음과 같이 주석을 추가해 주세요:

-    void markReminderSent(Set<Long> occurrenceIds);
+    /**
+     * 사전 푸시 발송에 성공한 발생건(occurrence)들을 reminderSent=true로 마킹합니다.
+     * - occurrenceIds가 null 또는 비어 있으면 아무 작업도 수행하지 않습니다(no-op).
+     * - 중복 ID는 무시됩니다(Set 권장).
+     *
+     * @param occurrenceIds 발송 성공한 occurrence ID 집합
+     */
+    void markReminderSent(Set<Long> occurrenceIds);

Also applies to: 17-17

⛔ Skipped due to learnings
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 사용되지 않는 변수나 주석은 제거를 제안합니다.
src/main/java/akuma/whiplash/infrastructure/firebase/dto/FcmSendResult.java (1)

11-15: DTO 정의 이상 없음 – 승인

불변 필드와 Lombok @builder 조합이 적절하며, 직관적인 필드명으로 가독성이 좋습니다.
추가 의견 없습니다.

src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (1)

3-6: 사용되지 않는 import 제거

AlarmOccurrenceRepository는 본 파일에서 사용되지 않습니다. 불필요한 의존성은 삭제해 주세요.

⛔ Skipped due to learnings
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 사용되지 않는 변수나 주석은 제거를 제안합니다.
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.389Z
Learning: Applies to **/*.java : 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java (1)

118-124: 벌크 업데이트 쿼리 👍

markReminderSentIn 메서드가 중복 업데이트를 막도록 AND o.reminderSent = false 조건을 포함한 점이 좋습니다.

Comment on lines +65 to +70
// 4. 무효 토큰 정리(멤버 매핑 정보가 있으면 거기서 제거)
for (Map.Entry<Long, List<String>> e : result.getMemberToTokens().entrySet()) {
Long memberId = e.getKey();
for (String bad : result.getInvalidTokens()) {
redisService.removeInvalidToken(memberId, bad);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

무효 토큰 정리 로직 불완전

memberToTokens는 성공 토큰만 담고 있어, 실패만 발생한 사용자에 대해 토큰 제거가 이루어지지 않습니다.
또한 모든 member-loop 안에서 invalidTokens 전체를 반복하여 불필요한 N×M 호출이 발생합니다.

-for (Map.Entry<Long, List<String>> e : result.getMemberToTokens().entrySet()) {
-    Long memberId = e.getKey();
-    for (String bad : result.getInvalidTokens()) {
-        redisService.removeInvalidToken(memberId, bad);
-    }
-}
+result.getInvalidTokens().forEach(bad ->
+    redisService.removeInvalidTokenByToken(bad)   // 새로운 단일-토큰 API 권장
+);

또는 FcmService에서 member별 invalid token 맵을 반환하도록 API를 확장하세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
around lines 65 to 70, the current logic only removes invalid tokens for members
present in memberToTokens, missing users with only failed tokens, and causes
redundant calls by looping all invalid tokens inside each member loop. To fix
this, modify the logic to iterate over a map of members to their invalid tokens
directly, either by extending the FcmService API to return a member-specific
invalid token map or by restructuring the data so you remove invalid tokens per
member without nested loops over all invalid tokens.

Comment on lines +242 to +247
@Override
public void markReminderSent(Set<Long> occurrenceIds) {
if (occurrenceIds == null || occurrenceIds.isEmpty()) return;
// 벌크 업데이트가 가능하면 JPQL update 사용 권장 (락/동시성 고려)
alarmOccurrenceRepository.markReminderSentIn(occurrenceIds);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

벌크 업데이트 후 영속성 컨텍스트 동기화 보장 및 결과 확인 제안

입력 가드(no-op) 좋습니다. 다만 JPQL 벌크 업데이트 사용 시 1차 캐시와 불일치가 생길 수 있습니다. Repository 메서드에 @Modifying(clearAutomatically = true, flushAutomatically = true)를 지정해 캐시 정합성을 보장하세요. 또한 업데이트 건수를 반환해 모니터링/로깅에 활용하는 것도 추천합니다.

Repository 선언이 다음을 포함하는지 확인해 주세요. 없다면 추가를 권장합니다:

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update AlarmOccurrenceEntity o
   set o.reminderSent = true
 where o.id in :ids
   and o.reminderSent = false
""")
int markReminderSentIn(@Param("ids") Set<Long> ids);

검증 스크립트:


🏁 Script executed:

#!/bin/bash
rg -n "markReminderSentIn\s*\(" -A 6 -B 4

Length of output: 2201


벌크 업데이트 사용 시 1차 캐시 동기화 및 결과 집계 적용 요망

  • 대상:
    • src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.javamarkReminderSentIn 메서드
  • 수정 사항:
    1. @Modifying(clearAutomatically = true, flushAutomatically = true) 추가
    2. 반환 타입을 void에서 int로 변경 (업데이트 건수 반환)
  • (선택) 서비스에서 반환된 건수를 받아 로깅/모니터링에 활용
--- AlarmOccurrenceRepository.java
@@
-    @Query("""
-        update AlarmOccurrenceEntity o
-           set o.reminderSent = true
-         where o.id in :ids
-           and o.reminderSent = false
-    """)
-    void markReminderSentIn(@Param("ids") Set<Long> ids);
+    @Modifying(clearAutomatically = true, flushAutomatically = true)
+    @Query("""
+        update AlarmOccurrenceEntity o
+           set o.reminderSent = true
+         where o.id in :ids
+           and o.reminderSent = false
+    """)
+    int markReminderSentIn(@Param("ids") Set<Long> ids);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public void markReminderSent(Set<Long> occurrenceIds) {
if (occurrenceIds == null || occurrenceIds.isEmpty()) return;
// 벌크 업데이트가 가능하면 JPQL update 사용 권장 (락/동시성 고려)
alarmOccurrenceRepository.markReminderSentIn(occurrenceIds);
}
// In src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update AlarmOccurrenceEntity o
set o.reminderSent = true
where o.id in :ids
and o.reminderSent = false
""")
int markReminderSentIn(@Param("ids") Set<Long> ids);
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java,
update the markReminderSentIn method by adding @Modifying annotation with
clearAutomatically = true and flushAutomatically = true to ensure first-level
cache synchronization. Change its return type from void to int to return the
count of updated records. Then, in
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
around lines 242 to 247, modify markReminderSent to capture the returned update
count from markReminderSentIn and optionally log or monitor this count.

@@ -1,8 +1,11 @@
package akuma.whiplash.domains.alarm.domain.service;

import akuma.whiplash.domains.alarm.application.dto.etc.OccurrencePushInfo;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

사전 알림 대상 조회 메서드 추가: 경계/타임존/포함 규칙 명시 필요

메서드 목적과 시그니처가 명확합니다. 사용 측 혼동을 줄이기 위해 포함 범위와 전제 조건, 타임존 기준을 인터페이스에서 문서화해 두길 권장합니다.

-    List<OccurrencePushInfo> getPreNotificationTargets(LocalDateTime startInclusive, LocalDateTime endInclusive);
+    /**
+     * [startInclusive, endInclusive] 구간(양끝 포함)에 예정된 사전 알림 대상 발생건을 조회합니다.
+     * 전제:
+     * - startInclusive <= endInclusive
+     * - 서버 타임존(LocalDateTime) 기준으로 평가
+     * - 저장소 레이어에서 reminderSent=false 및 비활성화(OFF/CHECKIN) 아님을 필터링해야 함
+     */
+    List<OccurrencePushInfo> getPreNotificationTargets(LocalDateTime startInclusive, LocalDateTime endInclusive);

Also applies to: 5-5, 10-10

🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
at lines 3, 5, and 10, the new method for querying pre-notification targets
lacks clear documentation on its boundary conditions, timezone handling, and
inclusion rules. Add detailed JavaDoc comments to the method signature and
interface describing the method's purpose, the exact inclusion criteria, any
assumptions made, and the timezone context used for calculations to prevent user
confusion.

Comment on lines +58 to +86
@Override
public List<OccurrencePushInfo> getPreNotificationTargets(LocalDateTime startInclusive, LocalDateTime endInclusive) {
LocalDate startDate = startInclusive.toLocalDate();
LocalTime startTime = startInclusive.toLocalTime();
LocalDate endDate = endInclusive.toLocalDate();
LocalTime endTime = endInclusive.toLocalTime();

// startDate == endDate -> 검색 범위가 같은 날짜 안이면 단일 쿼리
if (startDate.equals(endDate)) {
return alarmOccurrenceRepository.findPreNotificationTargetsSameDay(
startDate, startTime, endTime, DeactivateType.NONE
);
}

// startDate < endDate -> 검색 범위가 다음 날로 넘어가면 두 구간 합집합
List<OccurrencePushInfo> part1 = alarmOccurrenceRepository.findPreNotificationTargetsFromTime(
startDate, startTime, DeactivateType.NONE
); // [startDate startTime ~ 23:59:59]
List<OccurrencePushInfo> part2 = alarmOccurrenceRepository.findPreNotificationTargetsUntilTime(
endDate, endTime, DeactivateType.NONE
);// [endDate 00:00:00 ~ endTime]

// 중복 제거(혹시 모를 중복 대비)
return Stream.concat(part1.stream(), part2.stream())
.collect(Collectors.collectingAndThen(
Collectors.toMap(OccurrencePushInfo::occurrenceId, x -> x, (a, b) -> a),
m -> new ArrayList<>(m.values())
));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

사전 알림 대상 조회: 입력 유효성 가드, 중복 제거 시 순서 보존, 저장소 필터 확인 필요

구간이 동일일 때 단일 쿼리, 날짜가 넘어갈 때 2분할 쿼리로 합치는 접근은 타당합니다. 다음 보완을 제안합니다.

  • start > end 가드 추가: 잘못된 입력 시 조용히 빈 결과를 돌리기보다 예외가 이해하기 쉽습니다.
  • 중복 제거 시 LinkedHashMap을 사용해 최초 조회 순서 보존(특히 part1→part2 순서 유지).
  • 전제 명시: 현재 구현은 start와 end의 일수 차가 최대 1일임을 가정. 스케줄러 계약으로 제한하거나, 다일수 윈도우 일반화를 원하면 루프 기반으로 확장 필요.
  • 저장소 레이어가 reminderSent=false 및 회원의 push 동의 여부(terms.pushNotificationPolicy=true)를 반드시 필터링하고 있는지 확인.

아래처럼 가드를 추가하고, 순서 보존을 위해 LinkedHashMap을 사용하세요.

     public List<OccurrencePushInfo> getPreNotificationTargets(LocalDateTime startInclusive, LocalDateTime endInclusive) {
+        if (endInclusive.isBefore(startInclusive)) {
+            throw new IllegalArgumentException("startInclusive must be <= endInclusive");
+        }
         LocalDate startDate = startInclusive.toLocalDate();
         LocalTime startTime = startInclusive.toLocalTime();
         LocalDate endDate   = endInclusive.toLocalDate();
         LocalTime endTime   = endInclusive.toLocalTime();
@@
-        return Stream.concat(part1.stream(), part2.stream())
-            .collect(Collectors.collectingAndThen(
-                Collectors.toMap(OccurrencePushInfo::occurrenceId, x -> x, (a, b) -> a),
-                m -> new ArrayList<>(m.values())
-            ));
+        return Stream.concat(part1.stream(), part2.stream())
+            .collect(Collectors.collectingAndThen(
+                Collectors.toMap(
+                    OccurrencePushInfo::occurrenceId,
+                    x -> x,
+                    (a, b) -> a,
+                    java.util.LinkedHashMap::new // 순서 보존
+                ),
+                m -> new ArrayList<>(m.values())
+            ));
     }

저장소 필터(미발송/동의) 검증 스크립트:


🏁 Script executed:

#!/bin/bash
rg -n "findPreNotificationTargets(SameDay|FromTime|UntilTime)" -A 8 -B 4
rg -n "reminderSent\s*=\s*false|pushNotificationPolicy" -A 3 -B 3

Length of output: 16963


getPreNotificationTargets: 입력 범위 검증 및 중복 제거 순서 보존

  • endInclusive < startInclusive인 잘못된 입력은 예외를 던져 조기 종료하도록 추가하세요.
  • 중복 제거 시 Collectors.toMap 기본 구현은 순서를 보장하지 않으므로 LinkedHashMap을 사용해 part1→part2 순서를 유지해야 합니다.
  • 저장소 레이어에는 이미 o.reminderSent = falsem.pushNotificationPolicy = true 필터가 적용되어 있어 추가 검증은 불필요합니다.

수정 예시 (src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java):

     public List<OccurrencePushInfo> getPreNotificationTargets(LocalDateTime startInclusive, LocalDateTime endInclusive) {
+        if (endInclusive.isBefore(startInclusive)) {
+            throw new IllegalArgumentException("startInclusive must be <= endInclusive");
+        }
         LocalDate startDate = startInclusive.toLocalDate();
         LocalTime startTime = startInclusive.toLocalTime();
         LocalDate endDate   = endInclusive.toLocalDate();
         LocalTime endTime   = endInclusive.toLocalTime();

         // ... (기존 로직)

-        return Stream.concat(part1.stream(), part2.stream())
-            .collect(Collectors.collectingAndThen(
-                Collectors.toMap(OccurrencePushInfo::occurrenceId, x -> x, (a, b) -> a),
-                m -> new ArrayList<>(m.values())
-            ));
+        return Stream.concat(part1.stream(), part2.stream())
+            .collect(Collectors.collectingAndThen(
+                Collectors.toMap(
+                    OccurrencePushInfo::occurrenceId,
+                    x -> x,
+                    (a, b) -> a,
+                    java.util.LinkedHashMap::new
+                ),
+                m -> new ArrayList<>(m.values())
+            ));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public List<OccurrencePushInfo> getPreNotificationTargets(LocalDateTime startInclusive, LocalDateTime endInclusive) {
LocalDate startDate = startInclusive.toLocalDate();
LocalTime startTime = startInclusive.toLocalTime();
LocalDate endDate = endInclusive.toLocalDate();
LocalTime endTime = endInclusive.toLocalTime();
// startDate == endDate -> 검색 범위가 같은 날짜 안이면 단일 쿼리
if (startDate.equals(endDate)) {
return alarmOccurrenceRepository.findPreNotificationTargetsSameDay(
startDate, startTime, endTime, DeactivateType.NONE
);
}
// startDate < endDate -> 검색 범위가 다음 날로 넘어가면 두 구간 합집합
List<OccurrencePushInfo> part1 = alarmOccurrenceRepository.findPreNotificationTargetsFromTime(
startDate, startTime, DeactivateType.NONE
); // [startDate startTime ~ 23:59:59]
List<OccurrencePushInfo> part2 = alarmOccurrenceRepository.findPreNotificationTargetsUntilTime(
endDate, endTime, DeactivateType.NONE
);// [endDate 00:00:00 ~ endTime]
// 중복 제거(혹시 모를 중복 대비)
return Stream.concat(part1.stream(), part2.stream())
.collect(Collectors.collectingAndThen(
Collectors.toMap(OccurrencePushInfo::occurrenceId, x -> x, (a, b) -> a),
m -> new ArrayList<>(m.values())
));
}
@Override
public List<OccurrencePushInfo> getPreNotificationTargets(LocalDateTime startInclusive, LocalDateTime endInclusive) {
if (endInclusive.isBefore(startInclusive)) {
throw new IllegalArgumentException("startInclusive must be <= endInclusive");
}
LocalDate startDate = startInclusive.toLocalDate();
LocalTime startTime = startInclusive.toLocalTime();
LocalDate endDate = endInclusive.toLocalDate();
LocalTime endTime = endInclusive.toLocalTime();
// startDate == endDate -> 검색 범위가 같은 날짜 안이면 단일 쿼리
if (startDate.equals(endDate)) {
return alarmOccurrenceRepository.findPreNotificationTargetsSameDay(
startDate, startTime, endTime, DeactivateType.NONE
);
}
// startDate < endDate -> 검색 범위가 다음 날로 넘어가면 두 구간 합집합
List<OccurrencePushInfo> part1 = alarmOccurrenceRepository.findPreNotificationTargetsFromTime(
startDate, startTime, DeactivateType.NONE
); // [startDate startTime ~ 23:59:59]
List<OccurrencePushInfo> part2 = alarmOccurrenceRepository.findPreNotificationTargetsUntilTime(
endDate, endTime, DeactivateType.NONE
);// [endDate 00:00:00 ~ endTime]
// 중복 제거(혹시 모를 중복 대비)
return Stream.concat(part1.stream(), part2.stream())
.collect(Collectors.collectingAndThen(
Collectors.toMap(
OccurrencePushInfo::occurrenceId,
x -> x,
(a, b) -> a,
java.util.LinkedHashMap::new
),
m -> new ArrayList<>(m.values())
));
}
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
around lines 58 to 86, add input validation to throw an exception if
endInclusive is before startInclusive to prevent invalid ranges. When removing
duplicates by collecting into a map, use a LinkedHashMap to preserve the order
of part1 followed by part2 streams. Do not add extra filters for reminderSent or
pushNotificationPolicy since these are already handled in the repository layer.

Comment on lines +65 to +66
// import 생략: 파라미터 바인딩이면 JPQL에 FQN 필요 없음
@Query("""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

불필요한 한글 주석 제거 권장

// import 생략: 주석은 코드 동작과 무관하며 오히려 가독성을 해칩니다. 제거해 주세요.

🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
at lines 65 to 66, remove the Korean comment "// import 생략: 파라미터 바인딩이면 JPQL에 FQN
필요 없음" as it is unnecessary and reduces code readability.

Comment on lines 43 to +46
@CustomErrorCodes(commonErrorCodes = {BAD_REQUEST})
@Operation(summary = "로그아웃", description = "리프레시 토큰을 삭제합니다.")
@PostMapping("/logout")
public ApplicationResponse<Void> logout(@AuthenticationPrincipal MemberContext memberContext, @RequestBody @Valid LogoutRequest request) {
authUseCase.logout(request, memberContext.memberId());
public ApplicationResponse<Void> logout(@AuthenticationPrincipal MemberContext memberContext) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Logout 엔드포인트 OpenAPI에 보안 스키마 명시 제안

인증이 필요한 엔드포인트임을 문서에 드러내기 위해 SecurityRequirement를 추가하는 것을 권장합니다.

적용 diff:

-    @Operation(summary = "로그아웃", description = "리프레시 토큰을 삭제합니다.")
+    @Operation(
+        summary = "로그아웃",
+        description = "리프레시 토큰을 삭제합니다.",
+        security = { @SecurityRequirement(name = "bearerAuth") }
+    )

추가 import:

import io.swagger.v3.oas.annotations.security.SecurityRequirement;
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java
around lines 43 to 46, the logout endpoint lacks an OpenAPI security schema
annotation to indicate it requires authentication. Fix this by importing
io.swagger.v3.oas.annotations.security.SecurityRequirement and adding the
@SecurityRequirement annotation to the logout method to specify the security
scheme used, ensuring the endpoint's authentication requirement is documented.

Comment on lines +63 to +71
@CustomErrorCodes(commonErrorCodes = {BAD_REQUEST})
@Operation(summary = "FCM 토큰 등록", description = "FCM 토큰을 등록합니다.")
@PostMapping("/fcm-token")
public ApplicationResponse<Void> registerFcmToken(
@AuthenticationPrincipal MemberContext memberContext,
@Valid @RequestBody RegisterFcmTokenRequest request) {
authUseCase.registerFcmToken(memberContext.memberId(), memberContext.deviceId(), request.fcmToken());
return ApplicationResponse.onSuccess();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DTO 유효성/동의 여부/디바이스 식별자 검증 확인 필요

  • RegisterFcmTokenRequest.fcmToken: @notblank 적용 여부 확인.
  • 동의한 사용자만 등록: UseCase 단에서 동의 상태를 검증하는지 확인.
  • memberContext.deviceId(): null 가능성 차단(토큰에 deviceId 클레임 포함 및 필수화) 여부 확인.

필요 시 컨트롤러 수동 체크가 아니라 DTO/도메인 정책에서 막는 것을 권장합니다.

확인을 위한 탐색 스크립트:


🏁 Script executed:

#!/bin/bash
# DTO 유효성 및 동의 검증/사용처 확인
rg -n --hidden "RegisterFcmTokenRequest" -A 8 -B 2
rg -n --hidden "@NotBlank\\s*\\(.*\\)\\s*String\\s+fcmToken|String\\s+fcmToken" -A 2 -B 2
rg -n --hidden "registerFcmToken\\(|PushConsent|NotificationConsent" -A 5 -B 3
rg -n --hidden "deviceId\\(" -A 2 -B 2

Length of output: 13002


🏁 Script executed:

#!/bin/bash
# Inspect MemberContext fields to verify deviceId nullability/validation
rg -n "record MemberContext" -A 5 -B 0 src/main/java/akuma/whiplash/domains/auth/application/dto/etc/MemberContext.java

Length of output: 259


🏁 Script executed:

#!/bin/bash
# Inspect SocialLoginRequest for deviceId validation
sed -n '1,20p' src/main/java/akuma/whiplash/domains/auth/application/dto/request/SocialLoginRequest.java

Length of output: 799


🏁 Script executed:

#!/bin/bash
# MemberContext 레코드의 전체 필드 확인
sed -n '1,200p' src/main/java/akuma/whiplash/domains/auth/application/dto/etc/MemberContext.java

Length of output: 396


FCM 등록 전 Push 수신 동의 검증 로직 추가 필요

  • RegisterFcmTokenRequest.fcmToken은 이미 @NotBlank로 검증되고 있습니다.
  • SocialLoginRequest에 @NotBlankdeviceId가 필수화되어 있어, 로그인 시 JWT에 deviceId가 포함되고 MemberContext.deviceId로 주입됩니다. 인증 필터 구현부에서 누락 없이 매핑되는지 확인하세요.
  • 하지만 FCM 토큰 등록(authUseCase.registerFcmToken) 전에 사용자의 Push 수신 동의 여부를 검증하는 로직이 없습니다.
    • UseCase 또는 Domain Policy 레이어에서 PushConsent(또는 유사 엔티티) 상태를 확인하도록 로직을 추가하세요.
    • 가능하면 컨트롤러가 아닌 도메인에서 정책 검증을 처리하는 것을 권장합니다.
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/domains/auth/presentation/AuthController.java
around lines 63 to 71, before calling authUseCase.registerFcmToken, add logic to
verify the user's Push consent status. This check should be implemented in the
UseCase or Domain Policy layer, not in the controller. Ensure the domain layer
queries the PushConsent entity or equivalent to confirm consent before
proceeding with FCM token registration. Also, verify that deviceId is correctly
injected into MemberContext from the authentication filter as expected.

Comment on lines +55 to +58
Set<Long> successOccurrenceIds = new HashSet<>();
List<String> invalidTokens = new ArrayList<>();
Map<Long, List<String>> memberToTokens = new HashMap<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

invalid token → member 매핑 정보 누락

memberToTokens에는 “성공한” 토큰만 저장됩니다.
Scheduler에서는 memberToTokens를 기준으로 모든 member에 대해 invalidTokens를 순회해 삭제하고 있어,
① 실패만 발생한 member의 토큰을 정리하지 못하고 ② 불필요한 Redis 호출이 반복됩니다.

해결 제안:

  1. 실패 토큰도 member별로 수집하여 반환
-Map<Long, List<String>> memberToTokens
+Map<Long, List<String>> memberToValidTokens,
+Map<Long, List<String>> memberToInvalidTokens
  1. 혹은 invalidTokensSet<String>으로만 넘기고, Scheduler에서
invalidTokens.forEach(tok -> redisService.removeInvalidTokenByToken(tok));

같이 단일 API로 처리하도록 리팩터링하세요.

🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java around
lines 55 to 58, the memberToTokens map only stores tokens that succeeded,
missing the mapping for invalid tokens. To fix this, modify the code to also
collect and map failed (invalid) tokens by member, so that the scheduler can
properly clean up tokens for all members without redundant Redis calls.
Alternatively, refactor to pass invalidTokens as a Set<String> only and handle
token removal individually in the scheduler via a single Redis API call per
token.

Comment on lines +118 to +125
private boolean isTokenInvalid(FirebaseMessagingException e) {
return List.of(
"registration-token-not-registered",
"invalid-argument",
"unregistered",
"messaging/invalid-registration-token"
).contains(e.getErrorCode());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

상수 리스트 매 호출 생성 – 미세 최적화

isTokenInvalid에서 List.of(...)가 매번 새로 만들어집니다. 고정 값이므로 private static final Set<String>으로 캐싱하면 GC 부하를 줄일 수 있습니다.

🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java around
lines 118 to 125, the method isTokenInvalid creates a new List of error codes on
every call, causing unnecessary object creation. To fix this, define a private
static final Set<String> containing these error codes as a constant at the class
level, and then check if the exception's error code is contained in this set
instead of creating the list each time.

@strongmhk strongmhk merged commit 9ffa999 into develop Aug 9, 2025
1 of 2 checks passed
@strongmhk strongmhk deleted the feature/#39-fcm-push-notification branch August 9, 2025 17:25
@coderabbitai coderabbitai bot mentioned this pull request Aug 11, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

사전 푸시 알림 기능을 구현한다.

1 participant