Skip to content

[SOU-647] FCM 토큰 API 연동#75

Open
gnoes-ios wants to merge 6 commits into
developfrom
feat/SOU-647/fcm-token-api
Open

[SOU-647] FCM 토큰 API 연동#75
gnoes-ios wants to merge 6 commits into
developfrom
feat/SOU-647/fcm-token-api

Conversation

@gnoes-ios

@gnoes-ios gnoes-ios commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

📌 변경 요약

SOU-647 FCM 토큰 API 연동 Story입니다.

로그인 사용자의 FCM 토큰을 서버에 등록·갱신하고, 로그아웃 시 디바이스 토큰을 비활성화하는 Domain/Data/App 연결을 추가했습니다.

📌 변경 내용

  • FCM 토큰 등록/비활성화 Domain 모델, Repository, UseCase를 추가했습니다.
  • Data 레이어에 POST /api/users/me/fcm-tokens, DELETE /api/users/me/fcm-tokens?deviceId= Endpoint, DTO, RemoteDataSource, Repository 구현을 추가했습니다.
  • 설치별 deviceId를 Keychain에 저장하고 FCM 등록 요청 메타데이터에 연결했습니다.
  • App 레이어에서 현재 FCM 토큰을 인증 상태에 맞춰 동기화하고, 앱 활성화 시 재동기화 경로를 추가했습니다.
  • 로그아웃 시 FCM 토큰 비활성화를 먼저 시도하고, 실패해도 기존 auth logout은 진행하도록 했습니다.
  • FCM UseCase와 LogoutUseCase 단위 테스트를 추가했습니다.
  • 모듈 테스트 검증이 tuist test에서 누락되지 않도록 VERIFY_TEST_SCHEME 지정 시 xcodebuild test 경로를 사용하게 검증 스크립트를 보강했습니다.

📌 기타 참고 사항

검증:

  • VERIFY_TEST_SCHEME="Domain" docs/harness/scripts/verify.sh story
  • SwiftFormat lint 통과
  • SwiftLint strict 통과
  • Tuist generate 통과
  • 수집-Debug build 통과
  • DomainTests: 86 tests, 0 failures

develop 변경 확인:

  • base commit: e7210e9
  • base_commit..origin/develop 변경 파일: 없음
  • base_commit..HEAD 변경 파일: 40개
  • 겹치는 파일: 없음

관련 문서:


Note

Medium Risk
Touches authenticated API calls, logout ordering, and app-wide DI/bootstrap; failures are mostly logged or swallowed on deactivate, so server/device token state could drift without blocking logout.

Overview
로그인 사용자 FCM 토큰을 서버에 등록·갱신하고, 로그아웃 시 디바이스 토큰 비활성화까지 Domain → Data → App으로 연결합니다. 기존 DeferredPushTokenSyncer(로그만)는 제거되고, 인증 상태·Keychain deviceId·기기 메타데이터를 포함해 POST/DELETE /api/users/me/fcm-tokens를 호출하는 경로로 바뀝니다.

Domain에 FCMRegistration, register/deactivate UseCase, FCMRepository가 추가되고 로그아웃 UseCase는 FCM 비활성화를 먼저 시도하되(try?) 실패해도 auth logout은 그대로 진행합니다. App은 AppContainer 부트스트랩으로 FCM·DefaultPushTokenSyncer를 묶고, 토큰 스트림 구독과 씬 활성화 시 재동기화를 넣습니다.

검증 하네스는 모듈 스킴(VERIFY_TEST_SCHEME)일 때 **tuist test 대신 xcodebuild test**를 쓰도록 verify.sh와 관련 문서·스킬을 맞춥니다. Domain FCM·로그아웃 단위 테스트가 추가됩니다.

Reviewed by Cursor Bugbot for commit 0e87720. Configure here.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

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

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 714ac56a-0ff1-4a89-8d53-b2e998b99f39

📥 Commits

Reviewing files that changed from the base of the PR and between e7210e9 and 0e87720.

📒 Files selected for processing (40)
  • .codex/skills/souzip-plan/SKILL.md
  • .codex/skills/souzip-verify/SKILL.md
  • Projects/App/Sources/AppDelegate.swift
  • Projects/App/Sources/Factory/AppContainer.swift
  • Projects/App/Sources/Factory/AppFactory.swift
  • Projects/App/Sources/Push/DefaultPushTokenSyncer.swift
  • Projects/App/Sources/Push/DeviceInfoProvider.swift
  • Projects/App/Sources/Push/PushTokenSyncer.swift
  • Projects/App/Sources/SceneDelegate.swift
  • Projects/Core/Storage/Sources/Device/DeviceIdProviding.swift
  • Projects/Core/Storage/Sources/Device/DeviceIdStore.swift
  • Projects/Data/Sources/FCM/DTO/FCMRequest.swift
  • Projects/Data/Sources/FCM/DTO/FCMRequestMapper.swift
  • Projects/Data/Sources/FCM/DataSource/FCMRemoteDataSource.swift
  • Projects/Data/Sources/FCM/Endpoint/FCMEndpoint.swift
  • Projects/Data/Sources/FCM/Repository/DefaultFCMRepository.swift
  • Projects/Data/Sources/Util/Factory/DataFactory.swift
  • Projects/Domain/Sources/Auth/UseCase/LogoutUseCase.swift
  • Projects/Domain/Sources/FCM/Error/FCMError.swift
  • Projects/Domain/Sources/FCM/Model/FCMDeviceType.swift
  • Projects/Domain/Sources/FCM/Model/FCMRegistration.swift
  • Projects/Domain/Sources/FCM/Repository/FCMRepository.swift
  • Projects/Domain/Sources/FCM/UseCase/DeactivateFCMTokenUseCase.swift
  • Projects/Domain/Sources/FCM/UseCase/RegisterFCMTokenUseCase.swift
  • Projects/Domain/Sources/Util/Factory/DataFactory.swift
  • Projects/Domain/Sources/Util/Factory/DomainFactory+Auth.swift
  • Projects/Domain/Sources/Util/Factory/DomainFactory+FCM.swift
  • Projects/Domain/Sources/Util/Factory/DomainFactory.swift
  • Projects/Domain/Tests/Auth/UseCase/LogoutUseCaseTests.swift
  • Projects/Domain/Tests/Discovery/Mocks/MockDiscoveryRepository.swift
  • Projects/Domain/Tests/Discovery/UseCase/LoadAIRecommendationsForCategoryUseCaseTests.swift
  • Projects/Domain/Tests/Discovery/UseCase/LoadAIRecommendationsForUploadUseCaseTests.swift
  • Projects/Domain/Tests/FCM/Mocks/MockFCMRepository.swift
  • Projects/Domain/Tests/FCM/UseCase/DeactivateFCMTokenUseCaseTests.swift
  • Projects/Domain/Tests/FCM/UseCase/RegisterFCMTokenUseCaseTests.swift
  • Projects/Shared/Utils/Sources/Key/KeychainKey.swift
  • docs/harness/scripts/verify.sh
  • docs/harness/templates/plan/plan-template.md
  • docs/harness/verification.md
  • docs/harness/workflow/operating-sequence.md

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

❤️ Share

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

@gnoes-ios gnoes-ios added the feat label Jun 14, 2026
@gnoes-ios gnoes-ios self-assigned this Jun 14, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0e87720. Configure here.

}

private func registerIfNeeded(fcmToken: String) async {
guard await checkFullAuthentication.execute() else { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No FCM sync after login

Medium Severity

FCM server registration runs only when the token stream emits or on sceneDidBecomeActive. If the FCM token is already known while the user is logged out, registerIfNeeded returns early and nothing retries after login or auto-login until the app backgrounds or the token changes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0e87720. Configure here.

func makePushTokenSyncer() -> PushTokenSyncing {
let deviceIdStore = DeviceIdStore(
keychain: keychainFactory.makeKeychainStorage()
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Split device ID stores desync

Medium Severity

Registration reads deviceId from a DeviceIdStore created in AppFactory, while logout deactivation uses a separate cached store in DataFactory. Two actors can each mint a UUID before either writes Keychain, so register and deactivate may use different IDs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0e87720. Configure here.

}

public func execute() async throws {
try? await deactivateFCMToken.execute()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Logout races FCM registration

Medium Severity

LogoutUseCase deactivates the FCM token then clears auth, but an in-flight registerFCMToken started earlier can still complete with a valid bearer token and re-register the device after deactivation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0e87720. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant