Skip to content

[feat]: FCM api 연동#19

Open
Minsu-Lee wants to merge 28 commits intoDDD-Community:developfrom
Minsu-Lee:feature/fcm-push-notification
Open

[feat]: FCM api 연동#19
Minsu-Lee wants to merge 28 commits intoDDD-Community:developfrom
Minsu-Lee:feature/fcm-push-notification

Conversation

@Minsu-Lee
Copy link
Copy Markdown
Collaborator

🌁 작업 배경

FCM 푸시 알림 인프라 구성

Firebase Cloud Messaging(FCM)을 연동하여 서버 발송 푸시 알림을 수신하고, FCM 토큰을 서버와 동기화하는 기능을 추가합니다.

Clean Architecture 기반 토큰 동기화 설계

FCM 토큰의 생명주기(발급 → 로컬 저장 → 서버 등록 → 갱신)를 Clean Architecture 레이어에 맞게 분리하고, 미로그인 상태에서 발급된 토큰도 로그인 후 자동 동기화되도록 설계합니다.

✨ 주요 변경 사항

core:notification 모듈 신설

Firebase Messaging 관련 Android 컴포넌트(NeveraMessagingService, NotificationType)를 feature 모듈과 분리하여 독립 모듈로 구성합니다. 알림 채널 리소스(strings.xml) 및 ProGuard 규칙도 이 모듈에서 관리합니다.

FCM 토큰 동기화 파이프라인 구현

  • UpdateFcmTokenUseCase: 새 토큰 수신 시 로컬 저장 후 로그인 상태이면 서버 등록
  • SyncFcmTokenUseCase: 앱 실행/로그인 시점에 미동기화된 토큰을 서버에 등록
  • FcmTokenRepository / FcmTokenRepositoryImpl: 로컬 DataStore + 원격 API 통합
  • FcmTokenLocalDataSourceImpl: DataStore Preferences로 토큰과 sync 상태 원자적 관리

FcmTokenProvider 추상화로 CLA 준수

  • FcmTokenProvider 인터페이스를 domain 레이어에 정의
  • FirebaseFcmTokenProvider를 data 레이어에 구현 (Firebase 타임아웃 로직 포함)
  • Splash/Login ViewModel에서 Firebase SDK 직접 의존성 제거, feature 모듈 Firebase 의존성을 data 모듈로 통합

푸시 알림 수신 처리

  • NeveraMessagingService: FCM 메시지 수신 및 로컬 노티피케이션 표시
  • NotificationType: 알림 타입 분류 (DEFAULT / UNKNOWN)
  • 딥링크(nevera://) 기반 알림 클릭 이동 지원
  • NeveraApplication: 알림 채널 초기화

🔥 중점 리뷰 사항

FcmTokenProvider DI 구조

FcmTokenProvider 인터페이스를 domain에 두고 FirebaseFcmTokenProvider를 data에 위치시켰습니다. Firebase 타임아웃(5초) 로직도 data 레이어 구현체 안으로 이동하여 domain UseCase가 인프라 세부사항을 알지 않아도 되도록 처리했습니다.

미로그인 상태 토큰 처리 전략

로그인 전에 FCM 토큰이 발급되면 markTokenForSync로 로컬에만 저장해두고, 로그인 성공 후 SyncFcmTokenUseCase를 통해 서버에 등록합니다. DataStore의 atomic edit을 활용해 토큰과 sync 상태의 불일치를 방지합니다.

NeveraMessagingService의 NotificationType 처리

현재 서버에서 type, deepLink 필드가 전달되지 않아 임시로 DEFAULT로 하드코딩되어 있습니다. 서버 연동 완료 후 NotificationType.from() 파싱 결과를 사용하도록 변경 예정입니다.

📸 스크린샷(Optional)

UI 변경 없음

⚓️ References

- core:notification 모듈 신규 생성 (NeveraMessagingService, NotificationType)
- domain 레이어: FcmTokenRepository, SyncFcmTokenUseCase, UpdateFcmTokenUseCase
- data 레이어: NotificationApi, FcmTokenDataSource, FcmTokenRepositoryImpl
- app 모듈: 알림 채널 생성 및 AndroidManifest 서비스 등록
- SyncFcmTokenUseCase: GMS RPC 미준비 상태 대비 5초 withTimeoutOrNull 적용
로그인 성공 후 FCM 토큰을 서버에 동기화하도록 연동
실패 시에도 화면 이동은 계속 진행 (fail-safe)
- FcmTokenProvider 인터페이스를 domain 레이어에 정의
- FirebaseFcmTokenProvider를 data 레이어에 구현, Firebase 타임아웃 로직도 이 계층으로 이동
- SyncFcmTokenUseCase가 FcmTokenProvider를 생성자 주입으로 받아 람다 파라미터 제거
- LoginViewModel, SplashViewModel에서 Firebase SDK 직접 의존성 제거
- feature/login, feature/splash 모듈의 Firebase 의존성을 data 모듈로 통합
- FakeFcmTokenRepository를 testutil 패키지로 추출해 중복 제거 (SyncFcmTokenUseCaseTest, UpdateFcmTokenUseCaseTest 공유)
- SyncFcmTokenUseCaseTest에 FakeFcmTokenProvider 추가로 FcmTokenProvider DI 주입 테스트 반영
- FcmTokenErrorLogMessageTest에 CommonError 전체 variant 케이스 추가 (NetworkUnavailable, Timeout, Unauthorized, Unknown)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

FCM 푸시 지원을 추가하는 변경입니다. 새로운 core:notification 모듈과 관련 서비스/채널을 도입하고, 도메인·데이터·피처 레이어에서 FCM 토큰 저장·동기화 유스케이스·API·DI 바인딩 및 관련 테스트를 추가·연동했습니다.

Changes

Cohort / File(s) Summary
빌드 설정 & 버전 카탈로그
build.gradle.kts, gradle/libs.versions.toml, settings.gradle.kts, app/build.gradle.kts
Google Services 플러그인 alias 추가, Firebase BOM·Messaging 라이브러리 및 coroutines 항목 추가, :core:notification 모듈 포함 및 app 모듈에 :core:notification 구현 의존 추가.
앱 매니페스트·애플리케이션 초기화
app/src/main/AndroidManifest.xml, app/src/main/kotlin/com/anddd/nevera/NeveraApplication.kt
POST_NOTIFICATIONS 권한 추가, allowBackup false로 변경, MainActivityVIEW 딥링크 인텐트필터 추가. 애플리케이션 시작 시 NotificationChannelInitializer.initialize(this) 호출 추가.
Core Notification 모듈
core/notification/build.gradle.kts, core/notification/src/main/AndroidManifest.xml, core/notification/src/main/kotlin/.../NeveraMessagingService.kt, core/notification/src/main/kotlin/.../NotificationChannelInitializer.kt, core/notification/src/main/kotlin/.../NotificationType.kt, core/notification/src/main/res/values/strings.xml
라이브러리 모듈 생성. FCM 서비스(NeveraMessagingService) 등록 및 메시지 처리, 알림 채널 초기화 유틸, 알림 타입 enum, 문자열 리소스 추가.
데이터 계층 — API/모델/데이터소스/저장소/DI
data/src/main/kotlin/.../api/NotificationApi.kt, .../model/notification/*, .../datasource/*, .../repository/FcmTokenRepositoryImpl.kt, .../di/ApiModule.kt, .../di/DataSourceModule.kt, .../di/RepositoryModule.kt, .../mapper/error/FcmTokenErrorMapper.kt, data/build.gradle.kts
FCM 등록 REST API 인터페이스와 요청/응답 모델 추가. DataStore 기반 로컬 토큰 저장소, Firebase 토큰 제공자, 원격 등록 데이터소스 및 저장소 구현, DI 바인딩과 FCM 관련 에러 매핑 추가.
도메인 계층 — 인터페이스·모델·유스케이스·테스트유틸
domain/src/main/kotlin/.../repository/FcmTokenProvider.kt, .../repository/FcmTokenRepository.kt, .../model/notification/FcmTokenError*.kt, .../usecase/notification/SyncFcmTokenUseCase.kt, .../usecase/notification/UpdateFcmTokenUseCase.kt, domain/src/test/kotlin/.../FakeFcmTokenRepository.kt, domain/.../FakeTokenRepository.kt
FCM 토큰 제공자/저장소 인터페이스와 도메인 에러 타입 및 로그 메시지 유틸 추가. 토큰 동기화·업데이트 유스케이스 구현 및 테스트 더블 추가.
유스케이스 통합 — 피처 레이어 변경
feature/login/src/main/kotlin/.../LoginViewModel.kt, feature/splash/src/main/kotlin/.../SplashViewModel.kt, feature/splash/src/main/kotlin/.../SplashScreen.kt, feature/splash/build.gradle.kts
SyncFcmTokenUseCase 주입 및 로그인/스플래시 흐름에서 동기화 호출 추가. 스플래시에 런타임 알림 권한 요청 컴포저블 도입. BuildConfig/BuildFeatures 설정 변경.
도메인 기존 유스케이스 확장
domain/src/main/kotlin/.../usecase/auth/LogoutUseCase.kt, .../WithdrawUseCase.kt
로그아웃·회원탈퇴 성공 이후 FCM 데이터 정리(fcmTokenRepository.clearFcmData()) 호출 추가 및 예외 처리 확장.
테스트 추가
domain/src/test/kotlin/.../FcmTokenErrorLogMessageTest.kt, .../usecase/notification/SyncFcmTokenUseCaseTest.kt, .../UpdateFcmTokenUseCaseTest.kt
FCM 에러 메시지 변환 및 토큰 동기화·업데이트 유스케이스에 대한 단위 테스트 추가.
기타 경미한 변경
data/src/main/kotlin/.../repository/TokenRepositoryImpl.kt
생성자 끝에 트레일링 콤마 추가(문법/형식 변화).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션(작업 배경, 주요 변경사항, 중점 리뷰 사항, References)을 포함하고 있으며, 각 섹션이 충분히 상세하고 명확합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 요약합니다. FCM API 연동이라는 핵심 내용이 간결하고 구체적으로 표현되어 있습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Minsu-Lee Minsu-Lee added the 리뷰 중 리뷰 요청된 상태 label Apr 21, 2026
@Minsu-Lee Minsu-Lee self-assigned this Apr 21, 2026
@Minsu-Lee
Copy link
Copy Markdown
Collaborator Author

@coderabbitai
컴온 드루와

@Minsu-Lee Minsu-Lee requested a review from Jooman-Lee April 21, 2026 14:51
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

@Minsu-Lee 드루왔습니다! 🚀 FCM 푸시 알림 PR 리뷰 시작하겠습니다!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 25

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/AndroidManifest.xml (1)

5-5: ⚠️ Potential issue | 🟠 Major

Android 13+ 알림 권한 선언과 런타임 요청을 함께 추가해주세요.

이 PR에서 NeveraMessagingService가 로컬 알림을 표시하고 있는데, 앱이 targetSdk 36으로 설정되어 있기 때문에 Android 13(API 33) 이상에서는 POST_NOTIFICATIONS 권한의 선언과 런타임 요청이 없으면 알림이 사용자에게 표시되지 않을 수 있습니다.

app/src/main/AndroidManifest.xml에 다음 권한을 추가하고:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

그리고 앱 진입 플로우(예: MainActivity 또는 SplashScreen)에서 런타임 권한 요청 로직을 함께 구현해주면 좋겠습니다. 현재 코드에는 이러한 요청 경로가 없어서 Android 13 이상의 기기에서 신규 사용자들이 알림을 받지 못할 가능성이 있습니다.

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

In `@app/src/main/AndroidManifest.xml` at line 5, The manifest is missing the
POST_NOTIFICATIONS permission required on Android 13+ causing
NeveraMessagingService local notifications to be blocked; add the permission
declaration (<uses-permission
android:name="android.permission.POST_NOTIFICATIONS" />) to the manifest and
implement a runtime permission request flow in your app entry (e.g.,
MainActivity or SplashScreen) that checks
android.Manifest.permission.POST_NOTIFICATIONS, requests it via
ActivityCompat.requestPermissions (or the new Activity Result API), and handles
the grant/deny result so NeveraMessagingService can display notifications for
targetSdk 36+ devices.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/build.gradle.kts`:
- Line 6: Add a short developer note to the repo docs or README explaining that
the Google Services plugin (alias(libs.plugins.google.services) /
com.google.gms.google-services) is enabled but requires a Firebase config file
for local runs: instruct developers to export the Firebase project from the
console, download google-services.json and place it into the app/ directory (and
remind that google-services.json is intentionally gitignored), and optionally
note how CI runs without this file.

In `@app/src/main/kotlin/com/anddd/nevera/NeveraApplication.kt`:
- Around line 30-38: Move the notification channel initialization out of
NeveraApplication.createNotificationChannel() into a single responsibility entry
point inside the core:notification module (e.g., a
NotificationChannelInitializer with a public init(context: Context) method that
owns channel IDs/resources and creation logic), then replace the current
createNotificationChannel() call in Application.onCreate with a single call to
NotificationChannelInitializer.init(this); ensure the existing
NotificationChannel creation logic (including getString(...) IDs and
NotificationManager.createNotificationChannel) is relocated to that initializer
so multiple channels can be added centrally.
- Around line 50-52: The Intent creation chain in NeveraApplication using
Intent(this@NeveraApplication, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
}.let(::startActivity) is stylistically verbose for a single flag-setting
operation; simplify by passing the configured Intent directly into startActivity
(e.g., build the Intent with apply and call startActivity with it) to improve
readability while keeping the same behavior — update the code in
NeveraApplication where Intent(...).apply{...}.let(::startActivity) is used to
instead call startActivity with the Intent returned from apply.

In `@core/notification/build.gradle.kts`:
- Around line 21-25: The Gradle file sets Java compileOptions
(sourceCompatibility/targetCompatibility = JavaVersion.VERSION_17) but does not
set Kotlin's JVM target; add a matching Kotlin JVM target to avoid bytecode
mismatch. Update this module's build script to either add kotlinOptions {
jvmTarget = "17" } (or kotlin { jvmToolchain(17) } for a unified approach) so
Kotlin bytecode matches the Java compileOptions; apply the same chosen approach
across all modules for consistency. Ensure you reference the existing
compileOptions block when making the change.

In
`@core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt`:
- Around line 45-67: The current forced assignment "type =
NotificationType.DEFAULT" makes the NotificationType.UNKNOWN branch dead;
instead, stop parsing-then-overwriting by explicitly ignoring the incoming
server field and declaring the default type clearly: replace "var type =
NotificationType.from(remoteMessage.data[NOTIFICATION_TYPE])" and subsequent
overwrite with a no-op read like "val _ = remoteMessage.data[NOTIFICATION_TYPE]"
(or remove the parse entirely) and then "val type = NotificationType.DEFAULT";
keep the explicit DEFAULT used in showNotification, retain the UNKNOWN branch
for future use, and add a TODO comment referencing the issue number
(placeholder) so the temporary behavior is tracked and can be reverted later.
- Around line 30-40: The runCatching inside the serviceScope.launch around
updateFcmTokenUseCase(token) swallows CancellationException; replace runCatching
{ ... }.onSuccess{...}.onFailure{...} with a try { val result =
updateFcmTokenUseCase(token); result.logFcmSyncFailure(TAG, BuildConfig.DEBUG,
Log::w) } catch (e: CancellationException) { throw e } catch (e: Throwable) { if
(BuildConfig.DEBUG) Log.e(TAG, "FCM 토큰 업데이트 실패", e) } so cancellation is
rethrown and other errors are logged; apply the same change to the similar
pattern in LoginViewModel.syncFcmToken().
- Around line 48-85: The current DEFAULT_DEEP_LINK ("nevera://") can produce an
unresolvable Intent and lead to no-op or ActivityNotFoundException when creating
the Intent in showNotification; update the deepLink resolution in
onMessageReceived (the deepLink variable and the TODO logic) to use a safe
fallback (e.g., "nevera://home" or the app's launch Intent from
packageManager.getLaunchIntentForPackage(packageName)), then before creating the
Intent(Intent.ACTION_VIEW, deepLink.toUri()) check
Intent.resolveActivity(packageManager) and if it returns null, replace the
intent with the launch Intent (from getLaunchIntentForPackage) so
PendingIntent.getActivity(...) always receives a resolvable Intent; adjust
showNotification to accept and use that verified Intent (or handle the fallback
inside showNotification) and keep using type.ordinal as requestCode and
FLAG_IMMUTABLE|FLAG_UPDATE_CURRENT for the PendingIntent.
- Around line 87-100: The code uses applicationInfo.icon as the small
notification icon (in the NotificationCompat.Builder and setSmallIcon call),
which can render poorly; replace this with a silhouette-style notification
drawable (e.g., a new res/drawable/ic_stat_notification.xml or vector) and call
setSmallIcon(R.drawable.ic_stat_notification) instead; add the new drawable
resource following the Android "System decoration" template and ensure
NotificationCompat.Builder uses that resource for consistent notification
appearance across API 21+.

In `@data/build.gradle.kts`:
- Around line 49-52: 현재 각 모듈(data와 core/notification)에 중복으로 선언된
platform(libs.firebase.bom)과 implementation(libs.firebase.messaging)을 단일 지점으로
통합하세요: buildSrc 또는 build-logic의 convention plugin에 firebase-bom(platform(...))
선언을 옮기고 각 모듈의 build.gradle.kts(data와 core/notification)에서는 platform 선언을 제거하고 필요한
경우 implementation(libs.firebase.messaging)만 남겨 사용하도록 변경하며, 관련 코드 참조는
FirebaseFcmTokenProvider(데이터 모듈)와 FirebaseMessagingService(코어/notification 모듈)을
그대로 유지해 BOM 버전이 중앙에서 관리되도록 합니다.

In `@data/src/main/kotlin/com/anddd/nevera/data/di/DataSourceModule.kt`:
- Line 16: FcmTokenProvider is currently placed under
domain.usecase.notification but is a port/outbound gateway, so move the
interface (FcmTokenProvider) to a more appropriate package such as
domain.repository or domain.port/domain.gateway (choose one) and update all
references and imports (e.g., the binding in DataSourceModule.kt and the usages
alongside SyncFcmTokenUseCase and FcmTokenRepository); specifically relocate the
interface file, adjust package declaration, and update import statements where
FcmTokenProvider is referenced (including the imports around lines that
correspond to 51-53) so bindings and constructor injections compile against the
new package.

In
`@data/src/main/kotlin/com/anddd/nevera/data/mapper/error/FcmTokenErrorMapper.kt`:
- Around line 12-20: Move the hard-coded business error constant out of the
mapper and eliminate the duplicate fallback branch: create a shared constant
(e.g., MEMBER_NOT_FOUND) in a common location such as BusinessErrorCode (or
core:network) and replace the private const in FcmTokenErrorMapper with that
shared symbol; then simplify NetworkError.toFcmTokenError() by handling the
HttpError specific code (check for MEMBER_NOT_FOUND ->
FcmTokenError.MemberNotFound) and use a single default branch that returns
FcmTokenError.Common(toCommonError()) for all other cases to remove the
duplicated else branch.

In
`@data/src/main/kotlin/com/anddd/nevera/data/model/notification/RegisterFcmTokenRequest.kt`:
- Line 3: Add a Gson `@SerializedName` annotation to the token property in the
RegisterFcmTokenRequest data class to explicitly pin the JSON key (match the
pattern used in RegisterFcmTokenResponse); update the RegisterFcmTokenRequest
definition (class name: RegisterFcmTokenRequest, property: token) to include
`@SerializedName`("token") so serialization remains stable across obfuscation or
refactors.

In
`@data/src/main/kotlin/com/anddd/nevera/data/repository/FcmTokenRepositoryImpl.kt`:
- Around line 39-45: In registerFcmToken inside FcmTokenRepositoryImpl, change
the success lambda from onSuccess = { Unit } to the empty lambda onSuccess = { }
to more idiomatically indicate the success payload is ignored (the rest of the
call chain—apiCall and notificationDataSource.registerFcmToken—stays the same).

In
`@domain/src/main/kotlin/com/anddd/nevera/domain/model/notification/FcmTokenErrorLogMessage.kt`:
- Line 13: The ServerError branch in FcmTokenErrorLogMessage.kt currently
interpolates CommonError.ServerError.message which can be null; change that
interpolation in the is CommonError.ServerError case to use a safe fallback
(e.g. message ?: "원인 불명" or "unknown") so the log reads "FCM 동기화 실패: 서버 오류(원인
불명)" instead of "(null)"; update the is CommonError.ServerError -> "FCM 동기화 실패:
서버 오류(${commonError.message})" expression to use the safe-coalescing fallback.

In
`@domain/src/main/kotlin/com/anddd/nevera/domain/repository/FcmTokenRepository.kt`:
- Around line 6-13: The FcmTokenRepository interface exposes low-level
sync-state operations (saveFcmToken, setNeedsSync, markTokenForSync,
isSyncNeeded) which leaks implementation details; make the public API semantic
by keeping only high-level operations like markTokenForSync(token) and
registerFcmToken(token) on the interface, move or restrict saveFcmToken(token)
and setNeedsSync(...) to internal/implementation-only scope, consider changing
isSyncNeeded() from suspend fun to a Flow<Boolean> if consumers need to observe
changes, and rename the setNeedsSync parameter from value to needed or isNeeded
for clarity.

In
`@domain/src/main/kotlin/com/anddd/nevera/domain/usecase/notification/SyncFcmTokenUseCase.kt`:
- Around line 14-33: SyncFcmTokenUseCase.invoke may call
fcmTokenRepository.registerFcmToken even when the user is not logged in; update
the invoke implementation (in SyncFcmTokenUseCase) to first check
fcmTokenRepository.getAccessToken() and early-return Success if no access token,
and when you fetch a fallback token from fcmTokenProvider only call
fcmTokenRepository.markTokenForSync(fetched) and set needsSync=true but do not
call registerFcmToken unless getAccessToken() returns a non-null token; keep the
existing onSuccess { setNeedsSync(false) } behavior only tied to successful
registerFcmToken.

In
`@domain/src/main/kotlin/com/anddd/nevera/domain/usecase/notification/UpdateFcmTokenUseCase.kt`:
- Around line 15-25: The early-return in UpdateFcmTokenUseCase.suspend operator
fun invoke(newToken: String) that does "if (newToken == storedToken) return
Success" intentionally skips re-registration even when storedToken has needsSync
= true; add a brief comment above that conditional explaining the use-case only
handles token changes and that reconciliation of failed registrations
(needsSync) is handled by SyncFcmTokenUseCase during login/splash, referencing
methods like fcmTokenRepository.getFcmToken(),
fcmTokenRepository.markTokenForSync(), and SyncFcmTokenUseCase so future readers
understand this design choice.

In
`@domain/src/test/kotlin/com/anddd/nevera/domain/model/notification/FcmTokenErrorLogMessageTest.kt`:
- Line 7: Add tests in FcmTokenErrorLogMessageTest to cover the
logFcmSyncFailure() branch: invoke logFcmSyncFailure() with both isDebug=true
and isDebug=false and assert the logger receives the expected failure messages
for each mode, verify that when passing a successful result the success path (no
failure log) and original result return behavior are preserved, and include a
case asserting the function returns the original Result/response unchanged;
locate and exercise the logFcmSyncFailure extension function and any
helper/formatter used for message conversion to ensure both message formatting
and logging side-effects are tested.

In
`@domain/src/test/kotlin/com/anddd/nevera/domain/usecase/notification/SyncFcmTokenUseCaseTest.kt`:
- Around line 1-13: Remove the redundant import of FcmTokenProvider in
SyncFcmTokenUseCaseTest: since the file package is
com.anddd.nevera.domain.usecase.notification, the import line "import
com.anddd.nevera.domain.usecase.notification.FcmTokenProvider" is
unnecessary—delete that import to clean up the file while leaving all references
to FcmTokenProvider unchanged.
- Around line 73-101: Add two tests for SyncFcmTokenUseCase to cover provider
exceptions: (1) extend FakeFcmTokenProvider to throw a generic Exception when
invoked and assert that useCase() returns NeveraResult.Success(Unit) and
repository state (storedToken, markedTokens, registeredTokens, syncNeeded) is
unchanged; (2) extend FakeFcmTokenProvider to throw CancellationException and
assert that the CancellationException is propagated (i.e., the test expects the
exception to be thrown). Reference SyncFcmTokenUseCase and FakeFcmTokenProvider
and ensure behavior matches the runCatching { ... }.getOrNull() safety net and
the guideline about CancellationException propagation.

In
`@domain/src/test/kotlin/com/anddd/nevera/domain/usecase/notification/UpdateFcmTokenUseCaseTest.kt`:
- Around line 15-86: Add a regression test to assert that UpdateFcmTokenUseCase
propagates repository-thrown errors: create a test in UpdateFcmTokenUseCaseTest
that uses FakeFcmTokenRepository (or a variant) configured to throw a
CancellationException (and/or a generic Throwable) from its save/register method
and verify the exception is thrown by calling useCase("token"); also move/shared
test doubles like FakeTokenRepository and FakeFcmTokenRepository into
domain/src/test/.../testutil/ so other use-case tests can reuse them and reduce
duplication.

In
`@feature/login/src/main/kotlin/com/anddd/nevera/feature/login/main/LoginViewModel.kt`:
- Around line 104-114: The syncFcmToken() implementation currently swallows
CancellationException via runCatching; change it to rethrow
CancellationException so coroutine cancellation propagates: replace runCatching
{ ... }.onFailure { ... } with a try { val result = syncFcmTokenUseCase();
result.logFcmSyncFailure(...) } catch (t: Throwable) { if (t is
CancellationException) throw t; if (BuildConfig.DEBUG) Log.e(TAG, t.message, t)
} and then stop awaiting syncFcmToken() before MoveToHomeScreen—invoke it as a
fire-and-forget side effect using viewModelScope.launch { syncFcmToken() } (or
launch inside syncFcmToken) so FCM sync no longer blocks navigation; apply the
same CancellationException rethrow pattern to
NeveraMessagingService.onNewToken().

In
`@feature/splash/src/main/kotlin/com/anddd/nevera/feature/splash/main/SplashViewModel.kt`:
- Around line 37-39: In SplashViewModel the call to syncFcmToken() is blocking
the emission of SplashUiState.NavigateToHome because it’s awaited; change it to
a fire-and-forget background launch so navigation isn’t delayed: emit
_uiState.value = SplashUiState.NavigateToHome(accessToken) immediately, then
start token sync in a non-blocking coroutine (e.g.,
viewModelScope.launch(Dispatchers.IO) { syncFcmToken() } or dispatch to an
app-level/background worker) and ensure any exceptions are caught/logged inside
that coroutine; keep references to syncFcmToken, SplashViewModel, _uiState and
SplashUiState.NavigateToHome to locate the change.
- Around line 46-55: The onFailure handler inside syncFcmToken’s runCatching
currently swallows all throwables; update the onFailure lambda in syncFcmToken
(the runCatching { syncFcmTokenUseCase() } block) to detect if the throwable is
a CancellationException and rethrow it, otherwise proceed with the existing
debug Log.e handling—this preserves cooperative cancellation semantics without
changing the overall fire-and-forget behavior.

In `@gradle/libs.versions.toml`:
- Around line 28-30: Rename the kebab-case catalog keys to follow the existing
camelCase convention: change `firebase-bom` to `firebaseBom` and
`google-services` to `googleServices` in libs.versions.toml, and update any
build script or module references that use those catalog keys (for example
references to `libs.firebase-bom` / `libs.google-services`) to the new
`libs.firebaseBom` / `libs.googleServices` names so the catalog stays consistent
with `coreKtx`, `lifecycleRuntimeKtx`, etc.

---

Outside diff comments:
In `@app/src/main/AndroidManifest.xml`:
- Line 5: The manifest is missing the POST_NOTIFICATIONS permission required on
Android 13+ causing NeveraMessagingService local notifications to be blocked;
add the permission declaration (<uses-permission
android:name="android.permission.POST_NOTIFICATIONS" />) to the manifest and
implement a runtime permission request flow in your app entry (e.g.,
MainActivity or SplashScreen) that checks
android.Manifest.permission.POST_NOTIFICATIONS, requests it via
ActivityCompat.requestPermissions (or the new Activity Result API), and handles
the grant/deny result so NeveraMessagingService can display notifications for
targetSdk 36+ devices.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4f2199e5-6bd0-4d6f-927d-8ad659bdad00

📥 Commits

Reviewing files that changed from the base of the PR and between 827befc and 372cc5d.

📒 Files selected for processing (40)
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/kotlin/com/anddd/nevera/NeveraApplication.kt
  • build.gradle.kts
  • core/notification/build.gradle.kts
  • core/notification/consumer-rules.pro
  • core/notification/src/main/AndroidManifest.xml
  • core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt
  • core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NotificationType.kt
  • core/notification/src/main/res/values/strings.xml
  • data/build.gradle.kts
  • data/src/main/kotlin/com/anddd/nevera/data/api/NotificationApi.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSource.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSourceImpl.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FirebaseFcmTokenProvider.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/NotificationRemoteDataSource.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/NotificationRemoteDataSourceImpl.kt
  • data/src/main/kotlin/com/anddd/nevera/data/di/ApiModule.kt
  • data/src/main/kotlin/com/anddd/nevera/data/di/DataSourceModule.kt
  • data/src/main/kotlin/com/anddd/nevera/data/di/RepositoryModule.kt
  • data/src/main/kotlin/com/anddd/nevera/data/mapper/error/FcmTokenErrorMapper.kt
  • data/src/main/kotlin/com/anddd/nevera/data/model/notification/RegisterFcmTokenRequest.kt
  • data/src/main/kotlin/com/anddd/nevera/data/model/notification/RegisterFcmTokenResponse.kt
  • data/src/main/kotlin/com/anddd/nevera/data/repository/FcmTokenRepositoryImpl.kt
  • domain/build.gradle.kts
  • domain/src/main/kotlin/com/anddd/nevera/domain/model/notification/FcmTokenError.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/model/notification/FcmTokenErrorLogMessage.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/repository/FcmTokenRepository.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/notification/FcmTokenProvider.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/notification/SyncFcmTokenUseCase.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/notification/UpdateFcmTokenUseCase.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/model/notification/FcmTokenErrorLogMessageTest.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/testutil/FakeFcmTokenRepository.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/usecase/notification/SyncFcmTokenUseCaseTest.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/usecase/notification/UpdateFcmTokenUseCaseTest.kt
  • feature/login/src/main/kotlin/com/anddd/nevera/feature/login/main/LoginViewModel.kt
  • feature/splash/build.gradle.kts
  • feature/splash/src/main/kotlin/com/anddd/nevera/feature/splash/main/SplashViewModel.kt
  • gradle/libs.versions.toml
  • settings.gradle.kts

Comment thread app/build.gradle.kts
Comment thread app/src/main/kotlin/com/anddd/nevera/NeveraApplication.kt Outdated
Comment thread app/src/main/kotlin/com/anddd/nevera/NeveraApplication.kt
Comment thread core/notification/build.gradle.kts
Comment thread gradle/libs.versions.toml Outdated
usecase 패키지에 두었던 FcmTokenProvider 인터페이스를 repository 패키지로 이동하여
외부 시스템 접근 포트임을 명확히 표현한다.
채널 생성 책임을 core:notification 모듈로 이동하여 응집도를 높이고,
NeveraApplication은 단일 호출만 위임하도록 단순화한다.
ProGuard/R8 난독화 시 필드명 변경으로 인한 직렬화 키 불일치를 방지한다.
…xception 케이스 추가

UpdateFcmTokenUseCaseTest의 인라인 FakeTokenRepository를 공용 testutil로 추출하고,
repository CancellationException 전파 여부를 검증하는 테스트를 추가한다.
provider가 일반 예외를 던져도 Success로 조기 종료되는 보증 테스트와,
CancellationException이 runCatching을 통과해 전파되는지 검증하는 테스트를 추가한다.
setNeedsSync(Boolean)를 clearSyncNeeded()로 교체해 내부 상태 노출을 줄이고,
UpdateFcmTokenUseCase에서 isSyncNeeded 조건을 추가해 중복 등록을 방지한다.
NeveraMessagingService, LoginViewModel, SplashViewModel에서 runCatching이
CancellationException을 삼키는 문제를 수정한다.
로그아웃 후 동일 기기에서 재로그인하면 FCM 토큰이 남아있어
SyncFcmTokenUseCase가 조기 종료하며 서버 등록을 건너뛰는 버그 수정.
clearLoginInfo() 호출 시 FCM 토큰과 needsSync 상태를 함께 초기화한다.
TokenRepositoryImpl의 FCM 의존성을 제거하고,
LogoutUseCase/WithdrawUseCase에서 clearFcmData()를 명시적으로 호출하도록 변경.
로그아웃/탈퇴 시 정리되는 상태가 UseCase에 명확히 드러난다.
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
feature/login/src/main/kotlin/com/anddd/nevera/feature/login/main/LoginViewModel.kt (1)

64-103: 🧹 Nitpick | 🔵 Trivial

로그인 성공 → FCM 동기화 → 화면 이동 순서에 대한 가벼운 제안

loginWithEmail/loginWithGoogle 성공 시 syncFcmToken()await한 뒤 MoveToHomeScreen을 전송하고 있어, FCM 등록 네트워크 응답(+ 5초 타임아웃)이 홈 진입 지연으로 이어질 수 있습니다. SplashViewModelviewModelScope가 곧 cancel되는 특성 때문에 순차 실행이 필요하다는 점을 알고 있지만, LoginViewModelMoveToHomeScreen 이후에도 ViewModel이 살아 있을 수 있으니 여기서는 viewModelScope.launch { syncFcmToken() } 같은 fire-and-forget을 써도 안전합니다. 사용자 체감 로그인 속도가 신경 쓰이는 지점이라면 한 번 고려해 보시면 좋을 것 같습니다.

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

In
`@feature/login/src/main/kotlin/com/anddd/nevera/feature/login/main/LoginViewModel.kt`
around lines 64 - 103, The login success flow currently awaits syncFcmToken()
inside loginWithEmail and loginWithGoogle which can delay sending
MoveToHomeScreen; change to fire-and-forget by launching syncFcmToken in a
separate coroutine (e.g., viewModelScope.launch { syncFcmToken() }) so you
update _uiState to Success and send LoginSideEffect.MoveToHomeScreen
immediately, while still attempting FCM sync in background; update both
loginWithEmail and loginWithGoogle to perform background sync rather than
directly calling syncFcmToken().
♻️ Duplicate comments (2)
core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt (2)

94-97: ⚠️ Potential issue | 🟠 Major

알림 small icon은 전용 drawable로 분리하는 편이 좋겠습니다.

applicationInfo.icon은 보통 런처 아이콘이라 기기/OS별 알림 영역에서 기대와 다르게 보일 수 있습니다. Android 공식 예시도 setSmallIcon(R.drawable.notification_icon)처럼 알림 전용 리소스를 지정하는 형태를 사용하므로, ic_stat_notification 같은 단색/실루엣 drawable을 추가해 사용하는 방향을 제안드립니다. 참고: Android 알림 생성 가이드.

🎨 제안 패치 예시
-            .setSmallIcon(applicationInfo.icon)
+            .setSmallIcon(R.drawable.ic_stat_notification)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt`
around lines 94 - 97, Replace use of applicationInfo.icon for notifications with
a dedicated monochrome drawable resource: update the
notificationBuilder.setSmallIcon call in NeveraMessagingService (where
notificationBuilder.setContentTitle/ setContentText are used) to reference a
notification-specific drawable (e.g., R.drawable.ic_stat_notification). Add the
new drawable resource (single-color/silhouette) to the project's res/drawable
resources and ensure it follows notification small icon best practices so the
notification icon renders consistently across devices and Android versions.

50-87: ⚠️ Potential issue | 🟠 Major

기본 deep link는 해석 가능한 Intent로 폴백하는 편이 안전합니다.

DEFAULT_DEEP_LINK = "nevera://"는 scheme만 있는 URI라 앱의 intent-filter와 매칭되지 않을 수 있습니다. 서버가 deepLink를 아직 내려주지 않는 현재 상태에서는 알림 탭이 no-op으로 끝날 가능성이 있으니, nevera://home처럼 실제 랜딩 가능한 URI를 기본값으로 두거나 resolveActivity(packageManager) 실패 시 launcher intent로 폴백하는 방어 로직을 유지해 주세요.

🐛 제안 패치 예시
-        val intent = Intent(Intent.ACTION_VIEW, deepLink.toUri()).apply {
+        val deepLinkIntent = Intent(Intent.ACTION_VIEW, deepLink.toUri()).apply {
             addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
         }
+        val intent = deepLinkIntent.takeIf { it.resolveActivity(packageManager) != null }
+            ?: packageManager.getLaunchIntentForPackage(packageName)
+            ?: deepLinkIntent
@@
-        private const val DEFAULT_DEEP_LINK = "nevera://"
+        private const val DEFAULT_DEEP_LINK = "nevera://home"

Also applies to: 116-116

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

In
`@core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt`
around lines 50 - 87, DEFAULT_DEEP_LINK ("nevera://") can be an unusable URI;
update showNotification to ensure the deepLink resolves to an Activity: when
computing deepLink (or inside showNotification), if deepLink is null/empty or
Intent(Intent.ACTION_VIEW, deepLink.toUri()) cannot resolve via
packageManager.resolveActivity(...), fall back to a known landing URI such as
"nevera://home" or to the app launcher intent (create Intent(context,
packageManager.getLaunchIntentForPackage(packageName))). Use the notification
type.ordinal for PendingIntent requestCode as before and keep FLAG_IMMUTABLE |
FLAG_UPDATE_CURRENT.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt`:
- Around line 101-102: The notification code in NeveraMessagingService (usage of
NotificationManager and notificationManager.notify(...)) is missing Android 13+
POST_NOTIFICATIONS handling; add the permission to AndroidManifest
(<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />),
implement a runtime permission request flow in the app UI (e.g., onboarding or
app start) that requests Manifest.permission.POST_NOTIFICATIONS, and before
calling NotificationManager.notify(...) in NeveraMessagingService check
NotificationManagerCompat.areNotificationsEnabled() (and the runtime
permission/grant state) to avoid silent failures when permission is denied.
- Around line 31-42: onNewToken currently launches long-running work via
serviceScope.launch and may be killed before server sync completes; change
onNewToken to only perform the local atomic save (call markTokenForSync()/the
DataStore write that sets token and syncNeeded) and then enqueue a WorkManager
job (or JobScheduler) that runs updateFcmTokenUseCase (the same server-sync
logic) so server registration is retried reliably; ensure the Worker invokes
updateFcmTokenUseCase (and retains existing logFcmSyncFailure behavior) and that
SplashViewModel's SyncFcmTokenUseCase still covers app-start retries.

In
`@data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSource.kt`:
- Around line 3-10: Remove the redundant saveFcmToken(token: String) method from
the FcmTokenLocalDataSource interface and all its concrete implementations,
since markTokenForSync(token: String) already atomically stores the token and
sets sync-needed state; update implementors of FcmTokenLocalDataSource to delete
the saveFcmToken override and any callers referencing saveFcmToken, leaving
setNeedsSync and markTokenForSync intact (keep setNeedsSync because
FcmTokenRepositoryImpl.clearSyncNeeded() depends on it).

In
`@data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSourceImpl.kt`:
- Around line 16-20: FcmTokenLocalDataSourceImpl이 Context를 private val로 보관하고 있는데
이후에 사용되지 않으므로 생성자에서만 받도록 제거하고 클래스 프로퍼티로는 dataStore만 유지하세요: 생성자 시
ApplicationContext context 매개변수는 val/var로 저장하지 않고 바로 context.pushTokenDataStore를
이용해 private val dataStore를 초기화하도록 변경하고, 클래스 내 다른 참조가 없는지(예: context 사용 위치) 확인해
제거합니다(참조되는 식별자: FcmTokenLocalDataSourceImpl, dataStore, pushTokenDataStore,
context).

In
`@data/src/main/kotlin/com/anddd/nevera/data/datasource/FirebaseFcmTokenProvider.kt`:
- Around line 17-20: FcmTokenProvider.getToken currently uses withTimeoutOrNull
{ Firebase.messaging.token.await() } which only converts timeouts to null while
other exceptions (ApiException/IOException) propagate; add a clear KDoc on
FirebaseFcmTokenProvider.getToken stating whether failures should return null or
throw, and either (a) document that exceptions propagate so callers must handle
them (mention SyncFcmTokenUseCase currently wraps callers with runCatching and
rethrows CancellationException), or (b) change getToken implementation to absorb
non-cancellation exceptions and return null (rethrow CancellationException) to
fit a “failure => null” contract—update KDoc accordingly to reflect the chosen
behavior and keep withTimeoutOrNull usage and Firebase.messaging.token.await
references.

In
`@domain/src/main/kotlin/com/anddd/nevera/domain/repository/FcmTokenRepository.kt`:
- Line 9: Remove the dead commented declaration from the FcmTokenRepository
interface: delete the line "// suspend fun saveFcmToken(token: String)" so the
interface only contains active API surface; if you need to preserve historical
intent, record it in the commit message/PR description instead of leaving
commented code in FcmTokenRepository.

In
`@domain/src/main/kotlin/com/anddd/nevera/domain/usecase/auth/WithdrawUseCase.kt`:
- Around line 20-23: The onSuccess block in WithdrawUseCase (inside invoke())
currently calls fcmTokenRepository.clearFcmData() directly which can throw and
turn a successful server-side withdraw into an error; wrap the call to
fcmTokenRepository.clearFcmData() in a try/catch (or ensure the repository
implementation swallows/logs its own exceptions) so any exception during local
FCM cleanup is ignored and does not change the overall success result—keep
tokenRepository.clearLoginInfo() as-is and apply the same pattern in
LogoutUseCase.

In
`@domain/src/test/kotlin/com/anddd/nevera/domain/testutil/FakeTokenRepository.kt`:
- Around line 20-34: The fake currently only stores accessToken; update
FakeTokenRepository so setTokens(accessToken, refreshToken) assigns both
this.accessToken and this.refreshToken, and setLoginInfo(accessToken,
refreshToken, provider) assigns this.accessToken, this.refreshToken and
this.provider; ensure getProvider() returns the stored provider (and adjust any
existing internal fields names accessToken/refreshToken/provider accordingly) so
the in-memory fake fully reflects all supplied token/provider state.

---

Outside diff comments:
In
`@feature/login/src/main/kotlin/com/anddd/nevera/feature/login/main/LoginViewModel.kt`:
- Around line 64-103: The login success flow currently awaits syncFcmToken()
inside loginWithEmail and loginWithGoogle which can delay sending
MoveToHomeScreen; change to fire-and-forget by launching syncFcmToken in a
separate coroutine (e.g., viewModelScope.launch { syncFcmToken() }) so you
update _uiState to Success and send LoginSideEffect.MoveToHomeScreen
immediately, while still attempting FCM sync in background; update both
loginWithEmail and loginWithGoogle to perform background sync rather than
directly calling syncFcmToken().

---

Duplicate comments:
In
`@core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt`:
- Around line 94-97: Replace use of applicationInfo.icon for notifications with
a dedicated monochrome drawable resource: update the
notificationBuilder.setSmallIcon call in NeveraMessagingService (where
notificationBuilder.setContentTitle/ setContentText are used) to reference a
notification-specific drawable (e.g., R.drawable.ic_stat_notification). Add the
new drawable resource (single-color/silhouette) to the project's res/drawable
resources and ensure it follows notification small icon best practices so the
notification icon renders consistently across devices and Android versions.
- Around line 50-87: DEFAULT_DEEP_LINK ("nevera://") can be an unusable URI;
update showNotification to ensure the deepLink resolves to an Activity: when
computing deepLink (or inside showNotification), if deepLink is null/empty or
Intent(Intent.ACTION_VIEW, deepLink.toUri()) cannot resolve via
packageManager.resolveActivity(...), fall back to a known landing URI such as
"nevera://home" or to the app launcher intent (create Intent(context,
packageManager.getLaunchIntentForPackage(packageName))). Use the notification
type.ordinal for PendingIntent requestCode as before and keep FLAG_IMMUTABLE |
FLAG_UPDATE_CURRENT.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c5766c5-d279-4daf-9459-72db10cac65b

📥 Commits

Reviewing files that changed from the base of the PR and between 372cc5d and ad1cd8c.

📒 Files selected for processing (24)
  • app/src/main/kotlin/com/anddd/nevera/NeveraApplication.kt
  • core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt
  • core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NotificationChannelInitializer.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSource.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSourceImpl.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FirebaseFcmTokenProvider.kt
  • data/src/main/kotlin/com/anddd/nevera/data/di/DataSourceModule.kt
  • data/src/main/kotlin/com/anddd/nevera/data/mapper/error/FcmTokenErrorMapper.kt
  • data/src/main/kotlin/com/anddd/nevera/data/model/notification/RegisterFcmTokenRequest.kt
  • data/src/main/kotlin/com/anddd/nevera/data/repository/FcmTokenRepositoryImpl.kt
  • data/src/main/kotlin/com/anddd/nevera/data/repository/TokenRepositoryImpl.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/repository/FcmTokenProvider.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/repository/FcmTokenRepository.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/auth/LogoutUseCase.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/auth/WithdrawUseCase.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/notification/SyncFcmTokenUseCase.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/notification/UpdateFcmTokenUseCase.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/testutil/FakeFcmTokenRepository.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/testutil/FakeTokenRepository.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/usecase/notification/SyncFcmTokenUseCaseTest.kt
  • domain/src/test/kotlin/com/anddd/nevera/domain/usecase/notification/UpdateFcmTokenUseCaseTest.kt
  • feature/login/src/main/kotlin/com/anddd/nevera/feature/login/main/LoginViewModel.kt
  • feature/splash/src/main/kotlin/com/anddd/nevera/feature/splash/main/SplashViewModel.kt
  • gradle/libs.versions.toml

Comment thread domain/src/main/kotlin/com/anddd/nevera/domain/repository/FcmTokenRepository.kt Outdated
@Minsu-Lee
Copy link
Copy Markdown
Collaborator Author

@Jooman-Lee
푸시 토큰을 저장하는 두 UseCase는 하나로 통합해 처리할 수 있을 것 같습니다.
이번 PR이 종료된 이후 리팩토링을 진행한 뒤 다시 요청드릴 예정입니다.

현재 PR에서는 푸시 토큰 저장 로직의 대략적인 흐름만 확인해주시면 될 것 같습니다.

  • SyncFcmTokenUseCase (추후 제거 예정)
  • UpdateFcmTokenUseCase (추후 제거 예정)

또한 리팩토링 과정에서 현재처럼 scope 내에서 비동기로 실행하는 방식 대신,
core:notification 모듈에 CoroutineWorker로 정의해 호출하는 구조로 변경할 계획입니다.

@Minsu-Lee
Copy link
Copy Markdown
Collaborator Author

@Jooman-Lee
( 참고 ) 프로젝트 초기 구성 시, gitignore에 google-services.json이 명시되어 있어 레포에 포함이 안되고 있었나봅니다.
ci 빌드 실패 확인해서, google-services.json 포함되게 수정하였습니다.

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

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

Inline comments:
In
`@core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt`:
- Around line 52-58: The code creates a local variable type via
NotificationType.from(...) but immediately overwrites it with
NotificationType.DEFAULT, making the from(...) result and the UNKNOWN branch
effectively dead; update NeveraMessagingService.kt so the temporary DEFAULT
override is tracked and reversible: either (a) only set type =
NotificationType.DEFAULT when remoteMessage.data[NOTIFICATION_TYPE] is
null/blank (so NotificationType.from(...) is used when present), or (b) if you
must keep the unconditional override for now, replace the plain TODO with a
clear tracking comment that includes an issue number or owner (e.g., link to a
new issue or the responsible person) and mention deepLink behavior so reviewers
can revert the change when the server starts sending fields; reference the
variables and functions involved: type (local var), NotificationType.from(...),
DEFAULT, deepLink and the existing TODO line.
- Around line 94-114: Move the permission/state check to before you
allocate/build the notification and pending intent: call canNotify() immediately
when entering the notification routine (before creating
NotificationCompat.Builder and before PendingIntent.getActivity(...)) and return
early if false; then replace the direct NotificationManager usage
(getSystemService(...) as NotificationManager and
notificationManager.notify(...)) with
NotificationManagerCompat.from(this).notify(type.ordinal, notification) to keep
notification enablement checks aligned with areNotificationsEnabled.

In
`@data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSourceImpl.kt`:
- Around line 32-34: The clearFcmData() implementation currently calls
it.clear(), which wipes all preferences in pushTokenDataStore; change it to
explicitly remove only the FCM-related keys by calling it.remove(KEY_FCM_TOKEN)
and it.remove(KEY_NEEDS_SYNC) (referencing the existing KEY_FCM_TOKEN and
KEY_NEEDS_SYNC symbols) so only those two entries are deleted and future
unrelated keys in the DataStore are preserved.

In
`@domain/src/main/kotlin/com/anddd/nevera/domain/usecase/auth/LogoutUseCase.kt`:
- Line 19: The invoke signature for LogoutUseCase was changed to suspend
operator fun invoke(isDebug: Boolean) but callers still call logoutUseCase()
with no args (e.g., HomeViewModel’s logout invocation); fix by either adding a
default value to the use-case signature (e.g., suspend operator fun
invoke(isDebug: Boolean = false)) to preserve existing call sites, or update the
call site(s) to pass a boolean debug flag (e.g., logoutUseCase(isDebug =
<appropriateDebugFlag>)) so the LogoutUseCase.invoke parameter is satisfied.
- Around line 27-49: clearLoginInfo currently swallows all Throwables which can
hide failures to remove local auth state; change
clearLoginInfo(tokenRepository.clearLoginInfo) to let CancellationException
rethrow but propagate other exceptions to the caller (e.g., rethrow as-is or
wrap in a domain-specific exception) instead of only printing stack trace; keep
clearFcmData's more permissive catch but narrow it to expected exceptions if
possible; finally, ensure callers (e.g., HomeViewModel.logout) pass the isDebug
flag through to logoutUseCase so debug logging behavior is consistent.

In
`@feature/splash/src/main/kotlin/com/anddd/nevera/feature/splash/main/SplashScreen.kt`:
- Around line 41-64: NotificationPermissionRequester currently captures
onPermissionFlowCompleted directly in both rememberLauncherForActivityResult and
LaunchedEffect which can hold a stale reference if the parent callback changes;
wrap onPermissionFlowCompleted with rememberUpdatedState (e.g., val
latestOnPermission = rememberUpdatedState(onPermissionFlowCompleted)) and use
latestOnPermission.value when invoking from the launcher callback and inside
LaunchedEffect to ensure the most recent lambda is called; keep the rest of the
logic (notificationPermissionLauncher, permission check, conditional launch or
call) unchanged and continue to rely on the SplashViewModel re-entry guard to
avoid duplicate startAutoLogin calls.

In
`@feature/splash/src/main/kotlin/com/anddd/nevera/feature/splash/main/SplashViewModel.kt`:
- Around line 28-41: startAutoLogin can be re-entered causing concurrent
checkAutoLoginUseCase() and syncFcmToken() runs and duplicated _uiState emits;
add a reentrancy guard (e.g., an AtomicBoolean like isAutoLoginStarted or a
Mutex, or check if _uiState.value is SplashUiState.Loading) at the top of
startAutoLogin to return early when already running, ensure the guard is cleared
only after the coroutine work completes, and keep awaiting syncFcmToken() before
emitting SplashUiState.NavigateToHome to preserve the current cancellation/await
semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed5debaf-c277-4182-b15b-1dca8f0ae4b4

📥 Commits

Reviewing files that changed from the base of the PR and between ad1cd8c and ce09ee1.

📒 Files selected for processing (9)
  • app/src/main/AndroidManifest.xml
  • core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSource.kt
  • data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSourceImpl.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/repository/FcmTokenRepository.kt
  • domain/src/main/kotlin/com/anddd/nevera/domain/usecase/auth/LogoutUseCase.kt
  • feature/splash/build.gradle.kts
  • feature/splash/src/main/kotlin/com/anddd/nevera/feature/splash/main/SplashScreen.kt
  • feature/splash/src/main/kotlin/com/anddd/nevera/feature/splash/main/SplashViewModel.kt

@Minsu-Lee Minsu-Lee changed the title [feat]: fcm push notification [feat]: FCM api 연동 Apr 22, 2026
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