Skip to content

Comments

FCM 수신 및 딥링크 처리 구현#96

Merged
dogmania merged 38 commits intodevelopfrom
feat/#92-fcm-service
Feb 24, 2026
Merged

FCM 수신 및 딥링크 처리 구현#96
dogmania merged 38 commits intodevelopfrom
feat/#92-fcm-service

Conversation

@dogmania
Copy link
Member

이슈 번호

작업내용

  • FCM 수신

결과물

2026-02-24.4.29.17.mov

리뷰어에게 추가로 요구하는 사항 (선택)

  • 우선 FCM 수신, 딥링크 파싱, 네비게이션 로직만 구현했습니다. 실제 푸시알림 수신하려면 토큰을 서버에 보내는 로직이 필요한데 PR이 너무 커질 거 같아서 따로 분리해서 작업할게요!
  • 영상은 파베 콘솔에서 테스트 메시지 전송이 잘 수신되는지 녹화한 거라 수신하는 로직에는 크게 문제가 없는 것 같아요
  • :core:navigation <-> :core:notification 두 모듈 사이의 의존을 분리하기 위해서 :core:navigation-contract를 만들어서 인터페이스에 의존하도록 설계했습니다.
  • 마찬가지로 :core:datastore <-> :core:notification 의존을 분리하기 위해서 :core:device-contract를 추가했습니다.
  • deviceId를 UUID 기반으로 DataStore에 저장하고 있는데 실제 안드로이드 디바이스 ID를 사용하지 않은 이유는 OS별로 일관된 정책을 만들기가 어려워서 UUID를 활용했습니다.
  • 알림의 종류가 많고 딥링크가 포함되어 있어서 알림 플로우에 관여하는 컴포넌트가 많습니다. 전체적인 구조랑 어떤 컴포넌트가 추가됐는지 참고 자료를 만들어 봤는데 도움이 됐으면 좋겠어요ㅎㅎ...
image
:app
	/service
		TwixFirebaseMessagingService.kt // FCM 수신을 감지
:core
	:notification
		/channel
			TwixNotificationChannel.kt // 기본 알림 채널을 보장
		/deeplink
			NotificationDeepLink.kt // 딥링크 종류를 sealed interface 기반으로 정의
			NotificationDeepLinkParser.kt // FCM에 포함된 deepLink 필드를 파싱해서 데이터 추출
		/routing
			NotificationRouter.kt // deepLink 필드 파싱 결과를 기반으로 네비게이션 수행 및 알림 읽음 처리
			NotificationLaunchDispatcher.kt // Service로부터 전달받은 알림 이벤트 Intent를 관리
		/token
			NotificationTokenRegistrar.kt // FCM 토큰을 저장/업데이트
		/model
			PushPayload.kt // FCM에서 추출할 데이터 모델
	
	:datastore
		/deviceid
			DeviceId.kt // 디바이스 아이디 모델
			DeviceIdProvider.kt // DataStore 기반 디바이스 아이디 공급자
			
	:device-contract // :core:notification, :core:datastore 의존 관계 분리를 위한 중간 interface
		IdProvider.kt // 구현체는 DeviceIdProvider
	
	:navigation-contract // :core:notification, :core:navigation 의존 관계 분리를 위한 중간 interface
		AppNavigator.kt // 구현체는 AppNavHost 내부에 존재
		NotificationDeepLinkHandler.kt // 구현체는 NotificationDeepLinkParser
		NotificationLaunchEventSource.kt // 구현체는 NotificationLaunchDispatcher

@dogmania dogmania requested a review from chanho0908 February 23, 2026 19:50
@dogmania dogmania self-assigned this Feb 23, 2026
@dogmania dogmania added the Feature Extra attention is needed label Feb 23, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9486a71 and 9c2ca7e.

📒 Files selected for processing (1)
  • core/navigation/build.gradle.kts
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/navigation/build.gradle.kts

📝 Walkthrough

Walkthrough

이 PR은 FCM 기반 푸시 알림 기능을 추가합니다. app 모듈에 Koin DI, 권한 요청 및 FCM 서비스 등록을 추가하고 TwixFirebaseMessagingService를 선언했습니다. core에 notification, navigation-contract, device-contract 모듈을 새로 도입해 알림 채널 관리자, 푸시 페이로드/딥링크 파서, 딥링크 라우터, 알림 발신 디스패처, 토큰 등록기, DeviceId DataStore 및 IdProvider 구현과 관련 DI 모듈들을 추가·연결했습니다. AppNavHost와 MainActivity에 알림 딥링크 처리 흐름이 통합되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.36% 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
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확히 반영합니다. FCM 수신과 딥링크 처리 구현이라는 핵심 내용이 간결하게 요약되어 있습니다.
Description check ✅ Passed 설명이 변경사항과 관련있으며, 구체적인 작업 내용, 아키텍처 설계, 그리고 구현 선택 이유를 상세히 설명합니다.
Linked Issues check ✅ Passed PR이 issue #92의 모든 주요 목표를 충족합니다. FCM 수신, 딥링크 파싱, 채널 관리, 토큰 등록, 라우팅 등 모든 요구사항이 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 issue #92의 FCM 수신 및 딥링크 처리 구현 범위 내에 있습니다. 추가적인 범위 외 작업이 발견되지 않습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#92-fcm-service

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

❤️ Share

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

Copy link

@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

🧹 Nitpick comments (5)
core/navigation-contract/build.gradle.kts (1)

10-10: 프로젝트 전체에서 사용하는 타입 세이프 접근자(projects.*)와 표기 방식이 다릅니다.

core/notification/build.gradle.kts, core/navigation/build.gradle.kts 등 이 PR의 다른 Gradle 파일들은 모두 projects.core.ui 방식을 사용하는데, 이 파일만 문자열 기반 project(":core:ui")를 사용하고 있어 일관성이 깨집니다.

♻️ 제안하는 수정
 dependencies {
-    implementation(project(":core:ui"))
+    implementation(projects.core.ui)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/navigation-contract/build.gradle.kts` at line 10, Replace the
string-based project reference implementation(project(":core:ui")) with the
type-safe Gradle projects accessor used across the repo; update the dependency
to use projects.core.ui so it matches the other build scripts (e.g.,
core/notification/build.gradle.kts) and preserves consistency in the notation.
core/navigation-contract/src/main/java/com/twix/navigation_contract/NotificationLaunchEventSource.kt (1)

14-14: consumePendingDeepLinkexpected 파라미터 의도를 문서화하면 좋을 것 같습니다.

현재 코드만 봐서는 expected가 "현재 값이 이 값과 일치할 때만 소비(compare-and-clear)" 하는 역할인지 단순히 소비할 딥링크를 명시하는 역할인지 파악하기 어렵습니다. 구현 코드를 별도로 확인해야 이해할 수 있어 계약(interface)으로서의 표현력이 떨어집니다.

KDoc을 추가해 동작을 명확히 전달하는 것을 권장합니다.

📝 KDoc 추가 예시
-    fun consumePendingDeepLink(expected: String? = null)
+    /**
+     * pendingDeepLink를 소비(null로 초기화)합니다.
+     * [expected]가 null이 아닌 경우, 현재 [pendingDeepLink] 값과 일치할 때만 소비합니다.
+     * 이를 통해 처리 중 다른 deepLink로 교체된 경우 의도치 않은 소비를 방지합니다.
+     */
+    fun consumePendingDeepLink(expected: String? = null)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/navigation-contract/src/main/java/com/twix/navigation_contract/NotificationLaunchEventSource.kt`
at line 14, 문제: consumePendingDeepLink의 expected 파라미터 의도가 인터페이스 문서에 명확히 드러나지
않습니다. 수정: NotificationLaunchEventSource의 consumePendingDeepLink(expected:
String? = null) 함수에 KDoc을 추가하여 expected가 "비교-후-소비(compare-and-clear)" 동작을 하는지(즉
현재 보관된 딥링크가 expected와 일치할 때만 소비하는지), expected가 null일 때는 모든 대기 딥링크를 소비하는지, 반환값(예:
소비된 딥링크 또는 null)과 동시성/스레드 안전성 관련 보장 여부를 간단히 기술하세요. 함수명 consumePendingDeepLink과
파라미터 expected를 명시하여 어떤 조건에서 소비가 발생하는지 예시 한 줄로 명시하면 충분합니다.
core/notification/src/main/java/com/twix/notification/routing/NotificationRouter.kt (1)

10-14: appScope 파라미터가 현재 사용되지 않습니다.

markAsReadBestEffort가 완전히 주석 처리된 상태이므로 appScope는 어디서도 참조되지 않습니다. 기능 구현 전까지는 YAGNI 원칙에 따라 제거하거나, 향후 구현 예정임을 명시하는 TODO를 추가하는 방향을 고려해보세요.

♻️ 수정 제안 (구현 예정 명시 방식)
 class NotificationRouter(
     private val parser: NotificationDeepLinkParser,
-//    private val notificationRepository: NotificationRepository,
-    private val appScope: CoroutineScope,
+    // TODO: 읽음 처리 구현 시 아래 파라미터 추가
+    // private val notificationRepository: NotificationRepository,
+    // private val appScope: CoroutineScope,
 ) : NotificationDeepLinkHandler {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/notification/src/main/java/com/twix/notification/routing/NotificationRouter.kt`
around lines 10 - 14, NotificationRouter currently accepts an unused constructor
parameter appScope because markAsReadBestEffort is fully commented out; either
remove appScope from NotificationRouter's constructor and any callers to
eliminate dead code, or if you plan to implement markAsReadBestEffort soon,
restore a short TODO comment above appScope and the commented method (or add a
KDoc `@todo`) clarifying intended future use so the unused parameter is
intentional; reference the NotificationRouter class, the appScope constructor
parameter, and the commented markAsReadBestEffort method when making the change.
core/notification/src/main/java/com/twix/notification/routing/NotificationLaunchDispatcher.kt (1)

42-52: shouldIgnoreDuplicate가 상태를 변경하는 부수 효과를 가지고 있으며, 매직 넘버 1500L이 사용됩니다.

shouldIgnore... 이라는 이름은 순수한 조건 검사(predicate)를 암시하지만, 내부에서 lastDispatchedDeepLinklastDispatchedAtMillis를 갱신합니다. Command-Query Separation 관점에서 이름이 의도를 완전히 반영하지 못합니다. 또한 1500L은 의미를 알기 어려운 매직 넘버입니다.

♻️ 수정 제안
+    companion object {
+        const val ACTION_NOTIFICATION_CLICK = "com.twix.notification.ACTION_CLICK"
+        const val EXTRA_DEEP_LINK = "extra_deep_link"
+        const val EXTRA_FROM_PUSH_CLICK = "extra_from_push_click"
+        private const val DUPLICATE_DISPATCH_THRESHOLD_MS = 1_500L
+    }
+
-    private fun shouldIgnoreDuplicate(deepLink: String): Boolean {
+    /**
+     * 중복 여부를 확인하고, 중복이 아닌 경우 마지막 디스패치 상태를 갱신합니다.
+     * `@return` 중복이면 true (무시해야 함), 새로운 이벤트면 false
+     */
+    private fun checkDuplicateAndRecord(deepLink: String): Boolean {
         val now = System.currentTimeMillis()
         val isDuplicate =
-            lastDispatchedDeepLink == deepLink && (now - lastDispatchedAtMillis) < 1500L
+            lastDispatchedDeepLink == deepLink &&
+                (now - lastDispatchedAtMillis) < DUPLICATE_DISPATCH_THRESHOLD_MS

         if (!isDuplicate) {
             lastDispatchedDeepLink = deepLink
             lastDispatchedAtMillis = now
         }
         return isDuplicate
     }

호출부도 함께 변경:

-        if (shouldIgnoreDuplicate(deepLink)) return
+        if (checkDuplicateAndRecord(deepLink)) return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/notification/src/main/java/com/twix/notification/routing/NotificationLaunchDispatcher.kt`
around lines 42 - 52, The method shouldIgnoreDuplicate has hidden side-effects
(it mutates lastDispatchedDeepLink and lastDispatchedAtMillis) and uses the
magic number 1500L; change this by extracting the duplication check into a pure
predicate and a separate mutating method (or rename to clearly indicate
mutation, e.g., isDuplicateAndUpdateLastDispatched) and replace 1500L with a
named constant (e.g., DUPLICATE_WINDOW_MS) so intent is clear; update all
callers to either call the pure predicate then call the mutator on
non-duplicate, or to use the new renamed method if mutation-first behavior is
intended, referencing shouldIgnoreDuplicate, lastDispatchedDeepLink,
lastDispatchedAtMillis, and the 1500L occurrence when locating the changes.
core/datastore/src/main/java/com/twix/datastore/deviceid/DeviceId.kt (1)

27-37: SerializationExceptiondefaultValue를 반환하면 데이터 손실이 무증상으로 발생할 수 있습니다.

현재 구현은 역직렬화 실패 시 예외를 무시하고 빈 DeviceId()를 반환합니다. 이렇게 되면 저장된 디바이스 ID가 오염된 경우에도 조용히 새 UUID로 교체되어 디버깅이 어렵습니다. Android 공식 문서의 예시에서는 SerializationExceptionCorruptionException으로 감싸서 throw하는 방식을 권장합니다.

또한, input.readBytes()IOException을 던질 수 있는데, 이를 처리하지 않으면 DataStore의 data Flow가 에러를 방출합니다. Android 공식 문서에서도 DataStore를 읽을 때 IOException을 별도로 처리하도록 안내하고 있습니다.

디바이스 ID는 자동 복구가 허용된다면 IOException도 함께 처리하는 방향을 고려해보세요.

♻️ 수정 제안 (두 가지 방식)

방식 A: 공식 문서 권장 방식 (CorruptionException 사용, IOException은 호출부에서 처리)

 override suspend fun readFrom(input: InputStream): DeviceId =
     try {
         withContext(Dispatchers.IO) {
             json.decodeFromString(
                 deserializer = DeviceId.serializer(),
                 string = input.readBytes().decodeToString(),
             )
         }
-    } catch (e: SerializationException) {
-        defaultValue
+    } catch (e: SerializationException) {
+        throw CorruptionException("DeviceId 데이터가 손상되었습니다.", e)
     }

방식 B: 자동 복구 허용 (DeviceId 특성상 재생성이 허용되는 경우)

-    } catch (e: SerializationException) {
-        defaultValue
+    } catch (e: IOException) {
+        defaultValue
+    } catch (e: SerializationException) {
+        defaultValue
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/datastore/src/main/java/com/twix/datastore/deviceid/DeviceId.kt` around
lines 27 - 37, In readFrom (DeviceId.readFrom) don’t silently swallow
deserialization failures: catch SerializationException and rethrow it wrapped in
an androidx.datastore.core.CorruptionException (preserving the original
exception as the cause) instead of returning defaultValue; also explicitly
handle IOExceptions from input.readBytes() — either rethrow the IOException so
DataStore can surface it (official recommended behavior) or, if DeviceId is
allowed to be auto-recovered, catch IOException and return defaultValue
(Document this choice), and ensure the json.decodeFromString call still happens
on Dispatchers.IO via withContext as currently implemented.
🤖 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/src/main/java/com/yapp/twix/service/TwixFirebaseMessagingService.kt`:
- Around line 84-105: In PushPayload.resolveStableNotificationId(), don’t
silently swallow exceptions when parsing deepLink; catch the exception and
record it (include the exception message/stack and the failing deepLink value)
via the app’s logging/metrics facility (e.g., Log/Timber or your telemetry
client) before returning null so you can trace bad deep links — update the
try/catch around deepLink.toUri() in resolveStableNotificationId to log the
caught Exception and the deepLink string (use unique identifiers deepLink and
fromDeepLink in the log) rather than discarding it.

In `@core/navigation-contract/build.gradle.kts`:
- Around line 9-11: The project declares an unnecessary dependency on :core:ui
in the dependencies block (implementation(project(":core:ui"))) even though
navigation-contract interfaces (AppNavigator, NotificationDeepLinkHandler,
NotificationLaunchEventSource) use only standard types; remove the
implementation(project(":core:ui")) entry from build.gradle.kts so the contract
module remains lightweight and does not transitively pull :core:ui into its
consumers.

In `@core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt`:
- Around line 92-95: The TODO in toStatisticsEndedGoals currently throws
NotImplementedError at runtime; replace the TODO with a safe fallback navigation
or guard: call ensureMainStack(navController) then perform a non-crashing action
(e.g., navigate to a safe default route such as the main statistics screen or
the home screen via navController.navigate(...) or simply return/no-op) and
optionally log the unexpected invocation; alternatively, block this deep link
upstream in the router if the route isn't supported. Ensure you update the
toStatisticsEndedGoals implementation (and keep ensureMainStack/navController
usage) so it never throws.

In `@core/notification/build.gradle.kts`:
- Line 5: Remove the google services Gradle plugin from the library module by
deleting the alias(libs.plugins.google.services) entry in core/notification's
build.gradle.kts; the google-services plugin must only be applied in the app
module (app/build.gradle.kts) so leave that file unchanged and ensure
notification module keeps only the Firebase dependency declarations (e.g.,
firebase-messaging BOM) without applying the plugin.

In
`@core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt`:
- Around line 55-57: In NotificationDeepLinkParser.kt update the catch block
that currently calls logger.e("notification deepLink 파싱에 실패했습니다.: $raw") so the
caught exception e is passed to the logger; change the logger invocation to the
project's standard form (e.g., logger.e(e) { "notification deepLink 파싱에 실패했습니다.:
$raw" } or logger.e("notification deepLink 파싱에 실패했습니다.: $raw", e)) so the stack
trace is recorded when parsing fails.

In
`@core/notification/src/main/java/com/twix/notification/routing/NotificationRouter.kt`:
- Around line 66-76: Uncomment and restore the best-effort background mark logic
inside markAsReadBestEffort, keeping the appScope.launch and try/catch
structure, but fix the logger call in the catch for Exception: replace the
invalid logger.w(it, "...") with the Kermit pattern logger.w(e) { "알림 읽음 처리 실패:
$notificationId" }, and ensure CancellationException is rethrown (catch (ce:
CancellationException) { throw ce }) while using the existing
notificationRepository.markNotification(notificationId).

In
`@core/notification/src/main/java/com/twix/notification/token/NotificationTokenRegistrar.kt`:
- Around line 53-70: The registerInternal function currently never sends the FCM
token because the call to notificationRepository.registerFcmToken is commented
out; restore the registration by uncommenting (or re-adding) the call inside the
AppResult.Success<User> branch using userResult.data.id, deviceId from
deviceIdProvider.getOrCreateDeviceId(), and the fcmToken parameter, and ensure
any exceptions are handled/logged; if you intentionally defer sending the token,
add a clear TODO with an issue link inside registerInternal referencing why
notificationRepository.registerFcmToken is disabled so it’s tracked.

---

Nitpick comments:
In `@core/datastore/src/main/java/com/twix/datastore/deviceid/DeviceId.kt`:
- Around line 27-37: In readFrom (DeviceId.readFrom) don’t silently swallow
deserialization failures: catch SerializationException and rethrow it wrapped in
an androidx.datastore.core.CorruptionException (preserving the original
exception as the cause) instead of returning defaultValue; also explicitly
handle IOExceptions from input.readBytes() — either rethrow the IOException so
DataStore can surface it (official recommended behavior) or, if DeviceId is
allowed to be auto-recovered, catch IOException and return defaultValue
(Document this choice), and ensure the json.decodeFromString call still happens
on Dispatchers.IO via withContext as currently implemented.

In `@core/navigation-contract/build.gradle.kts`:
- Line 10: Replace the string-based project reference
implementation(project(":core:ui")) with the type-safe Gradle projects accessor
used across the repo; update the dependency to use projects.core.ui so it
matches the other build scripts (e.g., core/notification/build.gradle.kts) and
preserves consistency in the notation.

In
`@core/navigation-contract/src/main/java/com/twix/navigation_contract/NotificationLaunchEventSource.kt`:
- Line 14: 문제: consumePendingDeepLink의 expected 파라미터 의도가 인터페이스 문서에 명확히 드러나지
않습니다. 수정: NotificationLaunchEventSource의 consumePendingDeepLink(expected:
String? = null) 함수에 KDoc을 추가하여 expected가 "비교-후-소비(compare-and-clear)" 동작을 하는지(즉
현재 보관된 딥링크가 expected와 일치할 때만 소비하는지), expected가 null일 때는 모든 대기 딥링크를 소비하는지, 반환값(예:
소비된 딥링크 또는 null)과 동시성/스레드 안전성 관련 보장 여부를 간단히 기술하세요. 함수명 consumePendingDeepLink과
파라미터 expected를 명시하여 어떤 조건에서 소비가 발생하는지 예시 한 줄로 명시하면 충분합니다.

In
`@core/notification/src/main/java/com/twix/notification/routing/NotificationLaunchDispatcher.kt`:
- Around line 42-52: The method shouldIgnoreDuplicate has hidden side-effects
(it mutates lastDispatchedDeepLink and lastDispatchedAtMillis) and uses the
magic number 1500L; change this by extracting the duplication check into a pure
predicate and a separate mutating method (or rename to clearly indicate
mutation, e.g., isDuplicateAndUpdateLastDispatched) and replace 1500L with a
named constant (e.g., DUPLICATE_WINDOW_MS) so intent is clear; update all
callers to either call the pure predicate then call the mutator on
non-duplicate, or to use the new renamed method if mutation-first behavior is
intended, referencing shouldIgnoreDuplicate, lastDispatchedDeepLink,
lastDispatchedAtMillis, and the 1500L occurrence when locating the changes.

In
`@core/notification/src/main/java/com/twix/notification/routing/NotificationRouter.kt`:
- Around line 10-14: NotificationRouter currently accepts an unused constructor
parameter appScope because markAsReadBestEffort is fully commented out; either
remove appScope from NotificationRouter's constructor and any callers to
eliminate dead code, or if you plan to implement markAsReadBestEffort soon,
restore a short TODO comment above appScope and the commented method (or add a
KDoc `@todo`) clarifying intended future use so the unused parameter is
intentional; reference the NotificationRouter class, the appScope constructor
parameter, and the commented markAsReadBestEffort method when making the change.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e2784d and f87f0ff.

📒 Files selected for processing (42)
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/yapp/twix/TwixApplication.kt
  • app/src/main/java/com/yapp/twix/di/CoroutineModule.kt
  • app/src/main/java/com/yapp/twix/di/InitKoin.kt
  • app/src/main/java/com/yapp/twix/main/MainActivity.kt
  • app/src/main/java/com/yapp/twix/service/TwixFirebaseMessagingService.kt
  • core/datastore/build.gradle.kts
  • core/datastore/src/main/java/com/twix/datastore/DataStore.kt
  • core/datastore/src/main/java/com/twix/datastore/deviceid/DeviceId.kt
  • core/datastore/src/main/java/com/twix/datastore/deviceid/DeviceIdProvider.kt
  • core/datastore/src/main/java/com/twix/datastore/di/DataStoreModule.kt
  • core/device-contract/.gitignore
  • core/device-contract/build.gradle.kts
  • core/device-contract/consumer-rules.pro
  • core/device-contract/proguard-rules.pro
  • core/device-contract/src/main/AndroidManifest.xml
  • core/device-contract/src/main/java/com/twix/device_contract/IdProvider.kt
  • core/navigation-contract/.gitignore
  • core/navigation-contract/build.gradle.kts
  • core/navigation-contract/consumer-rules.pro
  • core/navigation-contract/proguard-rules.pro
  • core/navigation-contract/src/main/AndroidManifest.xml
  • core/navigation-contract/src/main/java/com/twix/navigation_contract/AppNavigator.kt
  • core/navigation-contract/src/main/java/com/twix/navigation_contract/NotificationDeepLinkHandler.kt
  • core/navigation-contract/src/main/java/com/twix/navigation_contract/NotificationLaunchEventSource.kt
  • core/navigation/build.gradle.kts
  • core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt
  • core/notification/.gitignore
  • core/notification/build.gradle.kts
  • core/notification/consumer-rules.pro
  • core/notification/proguard-rules.pro
  • core/notification/src/main/AndroidManifest.xml
  • core/notification/src/main/java/com/twix/notification/channel/TwixNotificationChannelManager.kt
  • core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLink.kt
  • core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt
  • core/notification/src/main/java/com/twix/notification/di/NotificationModule.kt
  • core/notification/src/main/java/com/twix/notification/model/PushPayload.kt
  • core/notification/src/main/java/com/twix/notification/routing/NotificationLaunchDispatcher.kt
  • core/notification/src/main/java/com/twix/notification/routing/NotificationRouter.kt
  • core/notification/src/main/java/com/twix/notification/token/NotificationTokenRegistrar.kt
  • settings.gradle.kts

@dogmania dogmania changed the title FCM 수신 및 에러 처리 구현 FCM 수신 및 딥링크 처리 구현 Feb 24, 2026
Copy link

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

♻️ Duplicate comments (1)
core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt (1)

55-57: 이전 리뷰 반영 확인 ✅

이전 리뷰에서 지적된 예외 e가 로거에 전달되지 않는 문제가 정확히 수정되었습니다. logger.e(e) { "..." } 형태로 스택 트레이스를 포함하도록 개선된 점이 좋습니다.

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

In
`@core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt`
around lines 55 - 57, The previous issue (missing exception being passed to the
logger) has been fixed in NotificationDeepLinkParser by using logger.e(e) {
"notification deepLink 파싱에 실패했습니다.: $raw" }; no further code changes are
required—confirm the catch block in NotificationDeepLinkParser.kt still passes
the caught Exception `e` into `logger.e(e) { ... }` to ensure stack traces are
logged and keep the method returning null as currently implemented.
🧹 Nitpick comments (1)
core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt (1)

65-67: LocalDate::parse 예외 타입 불일치 — 선택적 개선 제안

getQueryParameter(name)null이면 IllegalArgumentException("유효하지 않은 Date param입니다.: $name")이 발생하지만, null이 아닌 잘못된 형식(예: "not-a-date")이 들어오면 LocalDate::parseDateTimeParseException을 던져 ?: throw 분기를 거치지 않습니다. 두 경우 모두 바깥의 catch (e: Exception)에서 잡히므로 동작은 정상이나, 어느 파라미터가 문제인지에 대한 메시지 일관성이 떨어집니다.

logger.e(e) { ... }로 스택 트레이스가 이미 기록되기 때문에 디버깅에 치명적이지는 않지만, 헬퍼 함수의 의도 — "항상 IllegalArgumentException으로 파라미터 이름을 포함한 메시지를 던진다" — 를 명확히 하려면 아래와 같이 개선할 수 있습니다.

♻️ 예외 타입 일관성 개선 제안
 private fun Uri.requireLocalDateQuery(name: String): LocalDate =
-    getQueryParameter(name)?.let(LocalDate::parse)
-        ?: throw IllegalArgumentException("유효하지 않은 Date param입니다.: $name")
+    getQueryParameter(name)
+        ?.let { runCatching { LocalDate.parse(it) }.getOrNull() }
+        ?: throw IllegalArgumentException("유효하지 않은 Date param입니다.: $name")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt`
around lines 65 - 67, The helper requireLocalDateQuery currently lets
LocalDate::parse throw DateTimeParseException for malformed values which breaks
consistency with the null branch that throws IllegalArgumentException; update
requireLocalDateQuery to catch parsing exceptions (DateTimeParseException or
Exception around LocalDate.parse) and rethrow an IllegalArgumentException that
includes the parameter name (and wrap the original exception as the cause) so
callers always receive an IllegalArgumentException with a clear message
identifying the bad param.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt`:
- Around line 55-57: The previous issue (missing exception being passed to the
logger) has been fixed in NotificationDeepLinkParser by using logger.e(e) {
"notification deepLink 파싱에 실패했습니다.: $raw" }; no further code changes are
required—confirm the catch block in NotificationDeepLinkParser.kt still passes
the caught Exception `e` into `logger.e(e) { ... }` to ensure stack traces are
logged and keep the method returning null as currently implemented.

---

Nitpick comments:
In
`@core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt`:
- Around line 65-67: The helper requireLocalDateQuery currently lets
LocalDate::parse throw DateTimeParseException for malformed values which breaks
consistency with the null branch that throws IllegalArgumentException; update
requireLocalDateQuery to catch parsing exceptions (DateTimeParseException or
Exception around LocalDate.parse) and rethrow an IllegalArgumentException that
includes the parameter name (and wrap the original exception as the cause) so
callers always receive an IllegalArgumentException with a clear message
identifying the bad param.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f87f0ff and 9486a71.

📒 Files selected for processing (4)
  • app/src/main/java/com/yapp/twix/service/TwixFirebaseMessagingService.kt
  • core/navigation-contract/build.gradle.kts
  • core/notification/build.gradle.kts
  • core/notification/src/main/java/com/twix/notification/deeplink/NotificationDeepLinkParser.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/yapp/twix/service/TwixFirebaseMessagingService.kt
  • core/navigation-contract/build.gradle.kts
  • core/notification/build.gradle.kts

Copy link
Member

@chanho0908 chanho0908 left a comment

Choose a reason for hiding this comment

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

고생했어 현수야 !
내가 FCM을 구현해본적이 없어서 많은 리뷰를 남기진 못했어 🙏
궁금한거 2개만 남겼으니 확인해줘 !

Comment on lines +13 to +18
val coroutineModule =
module {
single<CoroutineScope>(named("AppScope")) {
CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

일전에 DataStore 리뷰할 때 현수가 AuthTokenProvider에 코루틴을 전역으로 주입해서 사용하면
좋겠다고 의견을 줘서 app/di/AppModule 에 named만 지정하지 않은 코루틴 주입 객체가 있는데
혹시 이거랑 다른걸까 ?

context: Context,
) : IdProvider {
private val dataStore: DataStore<DeviceId> = context.deviceIdDataStore
private val mutex = Mutex()
Copy link
Member

Choose a reason for hiding this comment

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

mutex는 어떤 의도로 사용하게 된걸까 ?

@dogmania dogmania requested a review from chanho0908 February 24, 2026 14:03
@dogmania dogmania merged commit 6880da7 into develop Feb 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FCM 수신 및 에러 처리 구현

2 participants