Skip to content

feat: 멀티디바이스 푸시 알림 지원을 위한 API 스펙 변경 반영#221

Merged
seoyoon513 merged 4 commits intodevelopfrom
BOOK-430-feature/#220
Nov 8, 2025
Merged

feat: 멀티디바이스 푸시 알림 지원을 위한 API 스펙 변경 반영#221
seoyoon513 merged 4 commits intodevelopfrom
BOOK-430-feature/#220

Conversation

@seoyoon513
Copy link
Copy Markdown
Contributor

@seoyoon513 seoyoon513 commented Nov 8, 2025

🔗 관련 이슈

📙 작업 설명

🧪 테스트 내역

  • 주요 기능 정상 동작 확인
  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

💬 추가 설명 or 리뷰 포인트

  • 현재 서버에서 로그아웃 or refreshToken 만료로 인한 세션 만료 회원에 대해서 fcm 토큰 처리를 따로 하고 있는게 없다는 답변 받았습니다! 현재 푸시 알림은 회원 대상으로 발송하고 있기 때문에 로그아웃 시 클라에서 토큰을 만료처리 해야될 것 같아요. 방법 3가지를 고민하고 있는데,
    지훈님 의견이 궁급함니다

1. AuthRepository 내부에서 처리

override suspend fun logout() = runSuspendCatching {
    service.logout()
    clearTokens()
    deleteFcmToken()
}

override suspend fun withdraw() = runSuspendCatching {
    service.withdraw()
    clearTokens()
    clearNotificationDataStore()
}
  • 구현이 단순하나 NotificationDatasource, FireMessaging과 같은 Auth와 무관한 의존성 생기며 관련 없는 책임까지 떠안음

2. Presenter에서 logout(), withdraw() 호출 시점에서 처리

  • Repository 책임(auth/user)이 명확히 분리되지만 추상화가 안되는 문제가 있음 (Presenter에서 너무 많은 조합을 담당)

3. UseCase 도입

class LogoutUseCase @Inject constructor(
    private val authRepository: AuthRepository,
    private val userRepository: UserRepository,
) {
    suspend operator fun invoke(): Result<Unit> = runSuspendCatching {
        authRepository.logout()
        userRepository.deleteFcmToken()
    }
}
  • AuthRepository, UserRepository가 각자의 책임만 유지, Presenter에서 간결한 형태로 사용 가능
  • 도메인 레이어 구축 비용 발생 및 복잡도 증가

=> 일단 토끼가 제안한 2번 방식으로 진행하려고 합니다

Summary by CodeRabbit

  • 새로운 기능

    • 기기 등록/업서트 흐름이 개선되어 기기 식별 정보가 함께 전송됩니다.
    • 사용자 저장소에 알림 관련 데이터를 초기화하는 기능(resetNotificationData)이 추가되었습니다.
  • 버그 수정 / 개선

    • 로그아웃 및 회원 탈퇴 시 원격 알림 토큰과 로컬 알림 상태를 안전하게 삭제하도록 동작이 개선되었습니다.
    • 설정 화면의 로그아웃/탈퇴 처리에 알림 초기화 호출이 추가되었습니다.

- FID(Firebase Installation ID)를 기기 ID로 사용하여 FCM 토큰과 함께 서버에 등록하도록 `upsertDevice` API를 호출하는 로직 추가
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 8, 2025

Walkthrough

Firebase Installations를 주입해 장치 ID를 획득하고, FCM 토큰 흐름을 장치 등록/업서트(DeviceRegistrationRequest → upsertDevice)로 전환했으며 관련 인터페이스·데이터 모델·저장소 구현과 설정 화면 호출을 업데이트합니다.

Changes

Cohort / File(s) Change Summary
Firebase DI
core/data/impl/.../di/FirebaseModule.kt
provideFirebaseInstallation(): FirebaseInstallations 제공자 추가 (Firebase.installations)
데이터 모델
core/network/.../request/DeviceRegistrationRequest.kt
FcmTokenRequestDeviceRegistrationRequest로 이름 변경; deviceId: String 필드(@SerialName("deviceId")) 추가
네트워크 서비스
core/network/.../service/ReedService.kt
updateFcmToken(...)upsertDevice(...)로 메서드명·요청 타입 변경; 엔드포인트 /api/v1/users/me/fcm-token/api/v1/users/me/devices
UserRepository API
core/data/api/.../repository/UserRepository.kt
suspend fun resetNotificationData() 인터페이스에 추가
저장소 구현
core/data/impl/.../repository/DefaultUserRepository.kt
FirebaseInstallations 의존성 주입 추가; getDeviceId(), registerDevice(fcmToken), deleteRemoteFcmToken(), clearNotificationDataStore(), resetNotificationData() 등 장치/토큰 관리 로직 추가 및 updateFcmToken 제거/대체
인증 저장소 정리
core/data/impl/.../repository/DefaultAuthRepository.kt
NotificationDataSource 의존성 및 관련 호출 제거
설정 화면 변경
feature/settings/.../SettingsPresenter.kt
UserRepository 주입 추가; 로그아웃·탈퇴 성공 시 userRepository.resetNotificationData() 호출 추가

Sequence Diagram(s)

sequenceDiagram
    participant App as 앱
    participant Repo as DefaultUserRepository
    participant Firebase as FirebaseInstallations
    participant Service as ReedService
    participant Backend as 백엔드

    rect rgb(240,255,240)
    note over App,Repo: 신규 흐름 — 장치 등록/업서트
    App->>Repo: syncFcmToken() / update flow
    Repo->>Firebase: id() (getDeviceId)
    Firebase-->>Repo: deviceId
    Repo->>Service: upsertDevice(DeviceRegistrationRequest{deviceId, fcmToken})
    Service->>Backend: POST /api/v1/users/me/devices
    Backend-->>Service: UserProfileResponse
    Service-->>Repo: 응답
    Repo-->>App: 완료
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 주의 깊게 볼 파일/영역:
    • DefaultUserRepository.getDeviceId() 예외 처리 및 재전파 정책
    • registerDevice() / upsertDevice 요청/응답 직렬화(필드 이름 일치) 및 오류 처리 흐름
    • resetNotificationData()가 원격 삭제와 로컬 정리 순서를 안전하게 수행하는지
    • DI 변경 (FirebaseModule)이 빌드/테스트 환경에서 올바르게 주입되는지
    • SettingsPresenter에 추가된 호출로 인한 동작 순서(로그/내비게이션) 영향

Poem

🐰 깡충깡충 나는 토끼, 장치마다 이름 달아요
아이디 하나 받아서, 토큰과 함께 전해요
숲 속 모든 기기들에 알림 씨앗 뿌리며
멀티디바이스 춤추는 밤, 기쁨을 속삭여요 🎋

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 멀티디바이스 푸시 알림 지원을 위한 API 스펙 변경 반영이라는 변경 사항의 핵심을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 변경된 API 스펙 반영 요구사항이 충족되었으며, FirebaseInstallations 통합, DeviceRegistrationRequest로의 API 변경, resetNotificationData() 구현 등 모든 코드 수준의 목표가 달성되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 멀티디바이스 푸시 알림 지원을 위한 API 스펙 변경 범위 내에 있으며, 로그아웃 시 FCM 토큰 정리 처리도 명시된 요구사항의 일부입니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-430-feature/#220

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

🧹 Nitpick comments (1)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)

93-105: 디바이스 등록 플로우가 올바르게 구현되었습니다.

Firebase Installation ID를 사용한 디바이스 등록 로직이 정확하며, 에러 처리도 적절합니다. runSuspendCatching으로 감싸진 호출 체인에서 예외가 올바르게 전파됩니다.

선택 사항: 디바이스 등록 성공 로깅 추가

관찰성(observability) 향상을 위해 registerDevice 메서드에 성공 로그를 추가하는 것을 고려해보세요:

 private suspend fun registerDevice(fcmToken: String) {
     val deviceId = getDeviceId()
     service.upsertDevice(DeviceRegistrationRequest(deviceId, fcmToken))
+    Logger.d("Device registered successfully - deviceId: ${deviceId.take(8)}...")
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c3b9e7 and 402b547.

📒 Files selected for processing (4)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/FirebaseModule.kt (2 hunks)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (4 hunks)
  • core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/DeviceRegistrationRequest.kt (1 hunks)
  • core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seoyoon513
Repo: YAPP-Github/Reed-Android PR: 46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함
🧬 Code graph analysis (1)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (3)
core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt (1)
  • setFcmToken (7-7)
core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt (1)
  • setFcmToken (24-28)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt (1)
  • runSuspendCatching (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Compose Stability Check
  • GitHub Check: ci-build
🔇 Additional comments (4)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/FirebaseModule.kt (1)

38-40: Firebase Installations 프로바이더 추가가 적절합니다.

멀티디바이스 지원을 위한 Firebase Installation ID 획득에 필요한 의존성이 올바르게 제공되고 있습니다. 기존 Firebase 프로바이더 패턴과 일관성 있게 구현되었습니다.

core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/DeviceRegistrationRequest.kt (1)

7-11: 디바이스 등록 요청 모델이 올바르게 업데이트되었습니다.

클래스명 변경(FcmTokenRequestDeviceRegistrationRequest)과 deviceId 필드 추가가 멀티디바이스 푸시 알림 지원을 위한 API 스펙 변경을 정확히 반영하고 있습니다.

core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt (1)

56-57: API 엔드포인트가 새로운 스펙에 맞게 올바르게 변경되었습니다.

메서드명과 엔드포인트가 디바이스 기반 등록/업데이트 방식으로 적절히 변경되었으며, upsert 네이밍이 생성 또는 업데이트 의미를 명확히 전달합니다.

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)

41-57: 토큰 동기화 플로우가 새로운 디바이스 등록 방식으로 올바르게 업데이트되었습니다.

두 가지 syncFcmToken 메서드 모두 registerDevice를 호출하도록 변경되어 멀티디바이스 지원이 정상적으로 적용되었습니다.

로그아웃 시 FCM 토큰 삭제 처리 확인

PR 설명에서 언급하신 것처럼, 서버가 세션 만료 시 FCM 토큰을 별도 처리하지 않으므로 클라이언트에서 로그아웃 시 토큰 삭제가 필요합니다. 현재 코드베이스에서 로그아웃/탈퇴 구현 위치를 확인하여 어느 접근 방식이 가장 적절한지 검토하시는 것을 권장합니다.

제안하신 3가지 접근 방식 중:

  1. AuthRepository 내부 처리: 구현은 간단하지만 책임 분리 측면에서 고려 필요
  2. Presenter에서 처리: 책임 분리는 되지만 Presenter의 조합 책임 증가
  3. UseCase 도입: 가장 명확한 책임 분리이나 도메인 레이어 구축 비용 발생

현재 로그아웃/탈퇴 구현 위치를 파악하는 데 도움이 필요하시면 코드베이스를 검색해드릴 수 있습니다.

@seoyoon513
Copy link
Copy Markdown
Contributor Author

seoyoon513 commented Nov 8, 2025

@coderabbitai 위에서 제시한 3가지 방법 중 너의 의견은? (현재 구조를 고려해서 고민해줘)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 8, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 프로젝트 구조 파악 - 도메인 레이어 및 UseCase 존재 여부 확인
echo "=== Project structure ==="
fd -t d -d 2 . | head -20

echo -e "\n=== Looking for UseCase files ==="
fd -e kt -i usecase | head -20

echo -e "\n=== Looking for domain layer ==="
fd -t d domain | head -10

echo -e "\n=== Looking for AuthRepository ==="
fd -e kt -i authrepository

echo -e "\n=== Looking for Presenter files ==="
fd -e kt -i presenter | head -15

Length of output: 2024


🏁 Script executed:

#!/bin/bash
# AuthRepository와 DefaultAuthRepository 내용 확인
echo "=== AuthRepository interface ==="
cat core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt

echo -e "\n=== DefaultAuthRepository implementation ==="
cat core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt

echo -e "\n=== UserRepository interface ==="
fd -e kt -i userrepository --exec cat {}

echo -e "\n=== Settings Presenter (logout/withdraw 처리 예상) ==="
cat feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt

Length of output: 17799


🏁 Script executed:

#!/bin/bash
# ReedService에서 device 관련 API 확인
echo "=== ReedService - checking for device deletion API ==="
cat core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt | grep -A 3 -B 3 -i "device\|fcm"

Length of output: 868


@seoyoon513 현재 프로젝트 구조를 분석한 결과, 방법 2 (Presenter에서 처리) 를 추천합니다.

근거

현재 프로젝트 구조 분석

  • 도메인 레이어(UseCase)가 존재하지 않음
  • feature(Presenter) → core/data(Repository) 구조
  • logout/withdraw는 현재 SettingsPresenter 한 곳에서만 호출됨

각 방법에 대한 평가

방법 1: AuthRepository 내부 처리

  • ❌ Repository 간 의존성 생성 (AuthRepository → UserRepository)
  • ❌ 순환 의존성 위험
  • ❌ AuthRepository가 FCM 토큰 관리 책임까지 가지게 됨 (SRP 위반)
  • ✅ 모든 호출처에서 자동 처리되므로 누락 위험 없음

방법 2: Presenter에서 처리

  • ✅ Repository 간 의존성 없음, 책임 분리 명확
  • ✅ 현재 구조(도메인 레이어 없는 MVI)와 일관적
  • logout/withdraw 호출이 한 곳에만 있어 관리 용이
  • ⚠️ 추후 다른 Presenter에서도 호출 시 실수 가능성 (현재는 해당 없음)

방법 4: UseCase 도입

  • ❌ 전체 아키텍처 변경 필요 (도메인 레이어 구축)
  • ❌ 현재 요구사항 대비 과도한 엔지니어링
  • ✅ 장기적으로 가장 확장 가능한 구조

구현 예시 (방법 2)

// UserRepository에 메서드 추가
interface UserRepository {
    suspend fun deleteFcmToken(): Result<Unit>
}

// DefaultUserRepository 구현
override suspend fun deleteFcmToken() = runSuspendCatching {
    firebaseMessaging.deleteToken().await()
    notificationDataSource.setFcmToken("")
}

// SettingsPresenter에서 호출
fun logout() {
    scope.launch {
        try {
            isLoading = true
            userRepository.deleteFcmToken() // FCM 토큰 먼저 삭제
            authRepository.logout()
                .onSuccess {
                    analyticsHelper.logEvent(SETTINGS_LOGOUT_COMPLETE)
                    navigator.resetRoot(LoginScreen())
                }
                // ... error handling
        } finally {
            isLoading = false
        }
    }
}

추후 고려사항

만약 향후 다른 곳(예: 토큰 만료 시 자동 로그아웃)에서도 logout을 호출해야 한다면, 그때 다음 중 하나를 선택할 수 있습니다:

  1. 헬퍼 함수로 추출 (suspend fun performLogout())
  2. UseCase 레이어 도입 고려

현재로서는 방법 2가 가장 현실적이고 구조적으로 적절합니다.


🧠 Learnings used
Learnt from: seoyoon513
Repo: YAPP-Github/Reed-Android PR: 46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함

@YAPP-Github YAPP-Github deleted a comment from coderabbitai Bot Nov 8, 2025
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: 0

Caution

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

⚠️ Outside diff range comments (2)
feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt (2)

61-89: 로그아웃 플로우에서 불일치 상태 발생 가능성

현재 구현에서 authRepository.logout()이 성공한 후 resetNotificationData()가 실패하면, 사용자는 서버에서는 로그아웃되었지만 로그인 화면으로 이동하지 않고 설정 화면에 남게 됩니다. 이는 불일치 상태를 만들 수 있습니다.

다음 중 하나의 해결 방법을 권장합니다:

방법 1 (권장): Best-effort 토큰 정리

                     authRepository.logout()
                         .onSuccess {
-                            userRepository.resetNotificationData()
+                            runCatching { userRepository.resetNotificationData() }
+                                .onFailure { Logger.e("Failed to reset notification data: ${it.message}") }
                             analyticsHelper.logEvent(SETTINGS_LOGOUT_COMPLETE)
                             navigator.resetRoot(LoginScreen())
                         }

방법 2: 별도 try-catch로 처리

                     authRepository.logout()
                         .onSuccess {
+                            try {
+                                userRepository.resetNotificationData()
+                            } catch (e: Exception) {
+                                Logger.e("Failed to reset notification data: ${e.message}")
+                            }
                             analyticsHelper.logEvent(SETTINGS_LOGOUT_COMPLETE)
                             navigator.resetRoot(LoginScreen())
                         }

PR 설명에 따르면 서버는 로그아웃 시 FCM 토큰을 별도 처리하지 않으므로, 토큰 정리 실패가 치명적이지는 않습니다. 하지만 사용자는 서버 로그아웃 성공 시 항상 로그인 화면으로 이동해야 합니다.


91-119: 탈퇴 플로우에도 동일한 이슈 적용

logout() 메서드와 동일한 불일치 상태 문제가 withdraw() 메서드에도 존재합니다. 위의 로그아웃 플로우 리뷰 코멘트에서 제안한 동일한 수정을 적용해주세요.

                     authRepository.withdraw()
                         .onSuccess {
-                            userRepository.resetNotificationData()
+                            runCatching { userRepository.resetNotificationData() }
+                                .onFailure { Logger.e("Failed to reset notification data: ${it.message}") }
                             analyticsHelper.logEvent(SETTINGS_WITHDRAWAL_COMPLETE)
                             navigator.resetRoot(LoginScreen())
                         }
🧹 Nitpick comments (2)
core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt (1)

31-31: 인터페이스 추가 확인 완료

메서드 시그니처와 위치가 적절합니다. 알림 관련 메서드들과 함께 그룹화되어 있어 가독성이 좋습니다.

향후 유지보수성을 위해 KDoc 추가를 고려해보세요:

+    /**
+     * 알림 관련 데이터를 초기화합니다.
+     * 원격 FCM 토큰 삭제 및 로컬 알림 데이터 스토어를 클리어합니다.
+     */
     suspend fun resetNotificationData()
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)

74-77: 알림 데이터 초기화 구현 검토

현재 구현은 적절하지만, 예외 처리 전략을 명확히 하는 것이 좋습니다.

deleteRemoteFcmToken()은 실패를 로그만 남기고 무시하지만, clearNotificationDataStore()는 예외를 전파할 수 있습니다. 일관성을 위해 다음과 같이 수정하는 것을 권장합니다:

     override suspend fun resetNotificationData() {
         deleteRemoteFcmToken()
-        clearNotificationDataStore()
+        try {
+            clearNotificationDataStore()
+        } catch (e: Exception) {
+            Logger.e("Failed to clear notification data store: ${e.message}")
+        }
     }

이렇게 하면 resetNotificationData()가 항상 성공하며(never throws), 상위 호출자(SettingsPresenter)에서 예외 처리를 걱정할 필요가 없습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 402b547 and 61072db.

📒 Files selected for processing (4)
  • core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt (1 hunks)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt (0 hunks)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (5 hunks)
  • feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt (4 hunks)
💤 Files with no reviewable changes (1)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: seoyoon513
Repo: YAPP-Github/Reed-Android PR: 46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함
📚 Learning: 2025-08-28T12:25:54.058Z
Learnt from: easyhooon
Repo: YAPP-Github/Reed-Android PR: 174
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt:128-133
Timestamp: 2025-08-28T12:25:54.058Z
Learning: In BookSearchPresenter.kt, when a guest user tries to register a book and is redirected to login, the bottom sheet (isBookRegisterBottomSheetVisible) and selection state (selectedBookIsbn, selectedBookStatus) are intentionally kept open/preserved so that when the user returns from login, they can continue from where they left off without losing context.

Applied to files:

  • feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt
🧬 Code graph analysis (1)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (3)
core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt (2)
  • setFcmToken (7-7)
  • clearNotificationDataStore (15-15)
core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt (2)
  • setFcmToken (24-28)
  • clearNotificationDataStore (54-58)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt (1)
  • runSuspendCatching (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci-build
  • GitHub Check: Compose Stability Check
🔇 Additional comments (7)
feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt (1)

13-13: 의존성 주입 추가 확인 완료

UserRepository 의존성이 기존 패턴과 일관되게 추가되었습니다.

Also applies to: 38-38

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (6)

3-3: 의존성 추가 확인 완료

FirebaseInstallations 의존성과 DeviceRegistrationRequest 사용이 적절하게 추가되었습니다.

Also applies to: 10-10, 25-25


41-57: FCM 토큰 동기화 로직 업데이트 확인 완료

updateFcmToken에서 registerDevice로의 마이그레이션이 깔끔하게 처리되었습니다. runSuspendCatching으로 예외 처리가 적절하게 되어 있습니다.


98-105: 디바이스 ID 조회 로직 확인 완료

Firebase Installations ID를 가져오는 로직이 적절하게 구현되었습니다. 에러 로깅 후 재throw하는 패턴이 getRemoteFcmToken()과 일관성 있게 적용되었습니다.


107-110: 디바이스 등록 로직 확인 완료

DeviceRegistrationRequest를 사용한 디바이스 등록 구현이 깔끔합니다. 호출자의 runSuspendCatching에서 예외 처리가 되므로 적절합니다.


112-118: 원격 FCM 토큰 삭제 로직 확인 완료

Best-effort 방식으로 원격 토큰 삭제를 시도하되, 실패해도 로컬 데이터 정리를 계속 진행하는 전략이 적절합니다. PR 설명에서 언급된 "서버는 로그아웃 시 FCM 토큰을 별도 처리하지 않음"과 일치하는 구현입니다.


120-122: 로컬 알림 데이터 클리어 로직 확인 완료

DataSource로의 단순 위임이 적절합니다. 개선 제안은 resetNotificationData() 리뷰 코멘트를 참고하세요.

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

🧹 Nitpick comments (1)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)

111-114: 디바이스 등록 로직 구현 확인

Device ID 획득 후 서버에 디바이스를 등록하는 로직이 올바르게 구현되었습니다. 에러 처리는 호출자(syncFcmToken)의 runSuspendCatching에서 처리되므로 현재 구조가 적절합니다.

디버깅 편의를 위해 서비스 호출 실패 시 로깅을 추가하는 것을 고려할 수 있습니다:

private suspend fun registerDevice(fcmToken: String) {
    val deviceId = getDeviceId()
    try {
        service.upsertDevice(DeviceRegistrationRequest(deviceId, fcmToken))
    } catch (e: Exception) {
        Logger.e("Failed to register device: ${e.message}")
        throw e
    }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61072db and 962f1c4.

📒 Files selected for processing (2)
  • core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt (1 hunks)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seoyoon513
Repo: YAPP-Github/Reed-Android PR: 46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함
🧬 Code graph analysis (1)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (3)
core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt (2)
  • setFcmToken (7-7)
  • clearNotificationDataStore (15-15)
core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt (2)
  • setFcmToken (24-28)
  • clearNotificationDataStore (54-58)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt (1)
  • runSuspendCatching (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Compose Stability Check
  • GitHub Check: ci-build
🔇 Additional comments (5)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (4)

3-3: 의존성 추가 확인 완료

Firebase Installations 의존성이 올바르게 추가되었으며, 생성자 주입 패턴을 정확히 따르고 있습니다. DeviceRegistrationRequest로의 이름 변경도 멀티디바이스 지원 목적에 부합합니다.

Also applies to: 10-10, 25-25


41-57: FCM 토큰 동기화 로직 업데이트 확인

updateFcmToken에서 registerDevice로의 변경이 올바르게 적용되었습니다. 기존의 runSuspendCatching 래퍼를 통한 에러 처리도 유지되어 안정적입니다.


102-109: Device ID 획득 로직 구현 확인

Firebase Installation ID(FID)를 올바르게 획득하고 있으며, 에러 처리도 기존 패턴(getRemoteFcmToken)과 일관되게 구현되었습니다. 멀티디바이스 지원의 핵심 변경사항이 정확히 반영되었습니다.


116-122: 정리 헬퍼 메서드 구현 확인

FCM 토큰 삭제 및 로컬 데이터 정리를 위한 헬퍼 메서드들이 명확하게 분리되어 구현되었습니다. 단일 책임 원칙을 잘 따르고 있으며, 에러 처리는 호출자에서 수행되는 구조가 적절합니다.

core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt (1)

33-33: 검증 결과: 의도적인 설계로 보이지만 일관성 문제 유지

원본 리뷰 의견의 우려는 부분적으로 타당합니다. 다음 사항이 확인되었습니다:

  1. 일관성 부족 - syncFcmToken(), updateNotificationSettings() 등 다른 저장소 메서드들은 Result<Unit> 또는 Result<T>를 반환하지만, resetNotificationData()는 반환값이 없습니다.

  2. 의도적인 설계 - 구현부에서 모든 예외를 catch하여 로그만 기록하고 예외를 전파하지 않습니다. 이는 "최선형 노력" 정리 작업 패턴입니다.

  3. 호출 방식 - SettingsPresenter에서 로그아웃/탈퇴 성공 후 호출되며, 반환값이 무시되고 실패해도 메인 플로우를 차단하지 않습니다.

관찰사항: 현재 구조는 작동하지만, 다음 중 하나를 고려하세요:

  • 정리 작업 실패를 사용자에게 보고해야 한다면 Result<Unit> 반환 추가
  • 현재 "최선형 노력" 방식이 의도라면, 그 의도를 코드 주석으로 명시

Comment on lines +78 to +85
override suspend fun resetNotificationData() {
try {
deleteRemoteFcmToken()
clearNotificationDataStore()
} catch (e: Exception) {
Logger.e("Failed to reset notification data: ${e.message}")
}
}
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 | 🟠 Major

에러 처리 일관성 및 부분 실패 처리 검토 필요

다음 사항들을 확인해 주세요:

  1. 에러 처리 불일치: 이 메서드는 예외를 로깅만 하고 삼키는 반면, getRemoteFcmToken(), getDeviceId() 등 다른 메서드들은 예외를 다시 던집니다.

  2. 부분 실패 케이스: deleteRemoteFcmToken()은 성공했지만 clearNotificationDataStore()가 실패하거나 그 반대의 경우, 부분적인 정리만 수행됩니다.

현재 구현이 "최선의 노력(best effort)" 정리를 의도한 것이라면:

  • 주석으로 명확히 문서화하거나
  • 두 작업을 독립적으로 try-catch하여 하나가 실패해도 다른 하나는 시도하도록 개선할 수 있습니다.

다음과 같이 개선할 수 있습니다:

 override suspend fun resetNotificationData() {
-        try {
-            deleteRemoteFcmToken()
-            clearNotificationDataStore()
-        } catch (e: Exception) {
-            Logger.e("Failed to reset notification data: ${e.message}")
-        }
+        // 각 정리 작업을 독립적으로 시도하여 하나가 실패해도 다른 작업은 수행
+        runCatching {
+            deleteRemoteFcmToken()
+        }.onFailure { e ->
+            Logger.e("Failed to delete remote FCM token: ${e.message}")
+        }
+        
+        runCatching {
+            clearNotificationDataStore()
+        }.onFailure { e ->
+            Logger.e("Failed to clear notification datastore: ${e.message}")
+        }
     }
🤖 Prompt for AI Agents
In
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt
around lines 78 to 85, the resetNotificationData method swallows exceptions and
performs two operations in one try block which leads to inconsistent error
handling compared to other methods and allows partial failures; either (A) make
this method consistent by rethrowing exceptions after logging so callers can
handle failures, or (B) implement best-effort semantics explicitly: add a brief
comment stating "best-effort cleanup", wrap deleteRemoteFcmToken() and
clearNotificationDataStore() in separate try-catch blocks that log each failure
(including exception details) but ensure the other operation still runs, and do
not swallow errors silently (consider returning a boolean or result object if
callers need status).

@easyhooon
Copy link
Copy Markdown
Contributor

저는 앱 재설치시에도 그 값이 변하지않은 SSAID를 Devide 고유 식별 ID로 주로 사용했었는데, FID도 생각해보니 괜찮은것 같군요(앱 재설치시지 fcmToken 역시 새로 발급되므로 1:1 매핑 구조, 자동으로 다른 디바이스로 인식)

다만, 서버에서 한 사용자에 대한 등록 기기가 계속 증가하게 되는 구조이므로(앱 재설치마다, FID 변경 -> 다른 기기라고 판단)
서버에서 FCM 토큰 만료를 감지하여(NotRegistered, InvalidRegistration) 해당 FID를 삭제하는 로직이 있는지 확인 후 없으면 추가해주면 좋을 것 같습니다.

@easyhooon
Copy link
Copy Markdown
Contributor

easyhooon commented Nov 8, 2025

방법 2: Presenter에서 처리 ✨
✅ Repository 간 의존성 없음, 책임 분리 명확
✅ 현재 구조(도메인 레이어 없는 MVI)와 일관적
✅ logout/withdraw 호출이 한 곳에만 있어 관리 용이
⚠️ 추후 다른 Presenter에서도 호출 시 실수 가능성 (현재는 해당 없음)

@seoyoon513 2,3 번 방식 모두 괜찮다고 생각합니다.

그런데 현재 refreshToken 까지 만료된 경우, logout API를 호출하고 있진 않군요. 서버에서 accessToken, refreshToken에 대한 블랙리스트 처리를 하고 있다면, 서버 정책에 따라 필요할것으로도 보여지는데 확인을 해봐야겠네요

만약에 logout API를 호출해야한다면 그에 따라 모든 presenter에서 반복적으로 호출될 필요가 있어, UseCase로 따로 빼는게 더 나을듯 합니다.

@seoyoon513
Copy link
Copy Markdown
Contributor Author

그런데 현재 refreshToken 까지 만료된 경우, logout API를 호출하고 있진 않군요. 서버에서 accessToken, refreshToken에 대한 블랙리스트 처리를 하고 있다면, 서버 정책에 따라 필요할것으로도 보여지는데 확인을 해봐야겠네요

만약에 logout API를 호출해야한다면 그에 따라 모든 presenter에서 반복적으로 호출될 필요가 있어, UseCase로 따로 빼는게 더 나을듯 합니다.

그런데 logout api 자체가 auth required인데, refreshToken까지 만료된 경우면 401에러가 발생하지 않을까요?

@easyhooon
Copy link
Copy Markdown
Contributor

그런데 logout api 자체가 auth required인데, refreshToken까지 만료된 경우면 401에러가 발생하지 않을까요?

그르네여 그땐 logout API 호출이 의미가 없군여. 지금대로 하면 될 것 같슴다

@seoyoon513
Copy link
Copy Markdown
Contributor Author

다만, 서버에서 한 사용자에 대한 등록 기기가 계속 증가하게 되는 구조이므로(앱 재설치마다, FID 변경 -> 다른 기기라고 판단) 서버에서 FCM 토큰 만료를 감지하여(NotRegistered, InvalidRegistration) 해당 FID를 삭제하는 로직이 있는지 확인 후 없으면 추가해주면 좋을 것 같습니다.

서버에서 어떤 방식으로 upsert를 진행하는지에 따라 달라지겠군요. 해당 부분은 서버에 노티가 필요하겠네요

Copy link
Copy Markdown
Contributor

@easyhooon easyhooon left a comment

Choose a reason for hiding this comment

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

iOS에서는 DeviceId를 어떤 것을 사용하는지 확인해보고, FID처럼 재설치시 갱신되는 것을 사용한다면 위에 코멘트에 언급했던 서버 로직 요청 해두면 될 것 같슴다. 수고하셨습니다~

@easyhooon
Copy link
Copy Markdown
Contributor

easyhooon commented Nov 8, 2025

서버에서 어떤 방식으로 upsert를 진행하는지에 따라 달라지겠군요. 해당 부분은 서버에 노티가 필요하겠네요

정확힌 upsert 라기 보단 사후적인 delete 라고 해야하나
서버는 기존 FID 기반 deviceID가 이젠 사용되지 않는다는 것을 알 수 없기 때문에, 해당 deviceID와 바인딩된 fcmToken으로 fcm를 쏴보고, 이에 따른 에러 발생을 통해, deviceID 검증을 할 수 있을 것으로 보여요. 만료된 fcmToken을 가지고 있는 deviceId가 계속 쌓이지않도록 제거가 되어야 할 것 같슴다.

@seoyoon513 seoyoon513 merged commit 56bc9d2 into develop Nov 8, 2025
4 checks passed
@seoyoon513 seoyoon513 deleted the BOOK-430-feature/#220 branch November 8, 2025 15:37
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.

[BOOK-430/feat] 멀티디바이스 푸시 알림 지원을 위한 API 스펙 변경 반영

2 participants