-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 알림 기능 구현 #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 알림 기능 구현 #239
Conversation
WalkthroughAPNs/FCM 연동 및 알림 설정 기능이 추가되었습니다. FCM 토큰 저장·동기화, 관련 API/DTO/레포지토/스토어/DI/유스케이스, 알림 설정 UI·코디네이터·뷰모델, Firebase 설정·권한 처리, URLPresenting 리팩토링이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant App as AppDelegate
participant FCM as FirebaseMessaging
participant Store as KeychainPushTokenStore
participant Provider as KeychainPushTokenProvider
participant Repo as PushTokenRepository
participant UseCase as SyncFCMTokenUseCase
participant Notif as NotificationRepository
App->>FCM: didReceiveRegistrationToken(token)
FCM-->>App: token
App->>Store: save(fcmToken)
Store-->>App: completion
Note over App: 앱 시작 또는 로그인 후 FCM 동기화
App->>UseCase: execute()
UseCase->>Repo: isSyncNeeded()
Repo-->>UseCase: true/false
alt 동기화 필요
UseCase->>Repo: getFCMToken()
Repo-->>UseCase: fcmToken
UseCase->>Notif: upsertFCMToken(fcmToken)
Notif-->>UseCase: success
UseCase->>Repo: resetSyncNeeded()
Repo-->>UseCase: success
else 동기화 불필요
UseCase-->>App: completed
end
sequenceDiagram
participant User as User
participant VC as NotificationSettingsViewController
participant VM as NotificationSettingsViewModel
participant UseCase as UpdateNotificationSettingsUseCase
participant Repo as NotificationRepository
User->>VC: 화면 진입 (onAppear)
VC->>VM: onAppear()
VM->>UseCase: fetch current setting (side effect)
UseCase-->>VM: current value
VM-->>VC: update view state
User->>VC: 토글 변경 (isEnabled)
VC->>VM: notificationToggleTapped(isEnabled)
VM->>UseCase: execute(isEnabled)
UseCase->>Repo: upsertNotificationSettings(isEnabled)
Repo-->>UseCase: success/err
UseCase-->>VM: result
VM-->>VC: 업데이트된 상태 반영
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (2)📓 Common learnings📚 Learning: 2025-08-19T10:38:27.309ZApplied to files:
🔇 Additional comments (1)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/SupportingFiles/Booket/Info.plist (1)
5-8: 중복된 키를 제거하세요.
NSPhotoLibraryAddUsageDescription와NSPhotoLibraryUsageDescription키가 56-59번 라인에도 중복되어 있습니다. Plist 파일에서 중복 키는 정의되지 않은 동작을 유발하며, 일반적으로 마지막에 나타난 값만 사용됩니다.다음 diff를 적용하여 중복 키를 제거하세요:
- <key>NSPhotoLibraryAddUsageDescription</key> - <string>문장 카드를 이미지로 저장하기 위해 사진첩 접근 권한이 필요합니다.</string> - <key>NSPhotoLibraryUsageDescription</key> - <string>문장 카드를 이미지로 저장하기 위해 사진첩 접근 권한이 필요합니다.</string> <key>BASE_API_URL</key>
♻️ Duplicate comments (2)
src/SupportingFiles/Booket/Info.plist (2)
105-108: 중복된 키입니다.
ITSAppUsesNonExemptEncryption(42-43번 라인)와FirebaseAutomaticScreenReportingEnabled(40-41번 라인)가 이미 파일 상단에 정의되어 있습니다. 이 중복 항목들을 제거해야 합니다.다음 diff를 적용하여 중복 키를 제거하세요:
<key>UISupportedInterfaceOrientations</key> <array> <string>UIInterfaceOrientationPortrait</string> </array> - <key>ITSAppUsesNonExemptEncryption</key> - <false/> - <key>FirebaseAutomaticScreenReportingEnabled</key> - <false/> </dict> </plist>
56-59: 중복된 키입니다.이 키들은 5-8번 라인에 이미 정의되어 있습니다. 두 위치 중 하나를 선택하여 중복을 제거해야 합니다.
🧹 Nitpick comments (2)
src/Projects/BKDomain/Sources/Interface/Usecase/UpdateNotificationSettingsUseCase.swift (1)
6-9: 프로토콜 정의가 명확합니다.유스케이스 프로토콜이 간결하고 Combine 패턴을 잘 따르고 있습니다. 반환 타입
Bool이 서버 업데이트 성공 여부를 나타내는지, 아니면 업데이트된 설정 상태를 나타내는지 문서화하면 더욱 명확할 것 같습니다.src/Projects/Booket/Sources/AppDelegate.swift (1)
64-84: FCM 토큰 저장 로직을 DI를 통해 주입받는 것을 권장합니다.현재는
didReceiveRegistrationToken메서드 내에서KeychainKeyValueStorage와KeychainPushTokenStore를 직접 생성하고 있습니다. 이는 다음과 같은 문제를 야기할 수 있습니다:
- 구체적인 구현에 대한 강한 결합
- 테스트 시 어려움
- 토큰 갱신 시마다 새 인스턴스 생성
다음과 같은 개선을 고려해보세요:
옵션 1: AppDelegate에 PushTokenStore를 프로퍼티로 주입받기
private let pushTokenStore: PushTokenStore // 초기화 시점이나 적절한 위치에서 self.pushTokenStore = KeychainPushTokenStore(storage: KeychainKeyValueStorage())옵션 2: 싱글톤 패턴 사용 (이미 DI 컨테이너가 없다면)
func messaging( _ messaging: Messaging, didReceiveRegistrationToken fcmToken: String? ) { guard let token = fcmToken else { return } let pushTokenStore = DIContainer.shared.resolve(PushTokenStore.self) pushTokenStore.save(fcmToken: token) .sink(...) .store(in: &cancellables) }참고: 로깅 시
AppLogger.auth대신AppLogger.storage를 사용하는 것이 의미상 더 적절할 수 있습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (38)
src/Projects/BKData/Sources/API/UserAPI.swift(3 hunks)src/Projects/BKData/Sources/DTO/Request/NotificationStatusRequestDTO.swift(1 hunks)src/Projects/BKData/Sources/DTO/Request/UpsertFCMTokenRequestDTO.swift(1 hunks)src/Projects/BKData/Sources/DTO/Response/UserProfileResponseDTO.swift(1 hunks)src/Projects/BKData/Sources/DataAssembly.swift(1 hunks)src/Projects/BKData/Sources/Interface/Storage/PushTokenProvider.swift(1 hunks)src/Projects/BKData/Sources/Interface/Storage/PushTokenStore.swift(1 hunks)src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift(1 hunks)src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift(1 hunks)src/Projects/BKDomain/Sources/DomainAssembly.swift(1 hunks)src/Projects/BKDomain/Sources/Entity/UserProfile.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Repository/NotificationRepository.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Repository/PushTokenRepository.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Usecase/SyncFCMTokenUseCase.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Usecase/UpdateNotificationSettingsUseCase.swift(1 hunks)src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift(1 hunks)src/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swift(1 hunks)src/Projects/BKPresentation/Sources/AppCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/AuthFlow/Coordinator/TermsCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/Common/Coordinator/URLPresenting.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Coordinator/SettingCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/Coordinator/NotificationSettingsCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/ViewModel/SettingViewModel.swift(2 hunks)src/Projects/BKStorage/Sources/Constant/StorageKeys.swift(1 hunks)src/Projects/BKStorage/Sources/StorageAssembly.swift(1 hunks)src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenProvider.swift(1 hunks)src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift(1 hunks)src/Projects/Booket/Project.swift(2 hunks)src/Projects/Booket/Sources/AppDelegate.swift(4 hunks)src/Projects/Booket/Sources/SceneDelegate.swift(1 hunks)src/SupportingFiles/Booket/Booket.entitlements(1 hunks)src/SupportingFiles/Booket/Info.plist(4 hunks)src/Tuist/Package.swift(1 hunks)src/Tuist/ProjectDescriptionHelpers/TargetDependency+External.swift(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-21T19:57:02.989Z
Learnt from: doyeonk429
PR: YAPP-Github/Reed-iOS#190
File: src/Projects/BKPresentation/Sources/MainFlow/BookDetail/ViewModel/SentenceCardViewModel.swift:1-9
Timestamp: 2025-08-21T19:57:02.989Z
Learning: Reed-iOS 프로젝트에서 PreviewApp(src/Projects/BKDesign/PreviewApp/Info.plist)에는 Photo Library 권한 설명 키(NSPhotoLibraryAddUsageDescription, NSPhotoLibraryUsageDescription)를 추가할 필요가 없다. PreviewApp은 디자인 프리뷰 용도로만 사용되며 실제 사진 저장 기능은 메인 앱에서만 사용한다.
Applied to files:
src/SupportingFiles/Booket/Info.plist
🧬 Code graph analysis (20)
src/Projects/BKDomain/Sources/Interface/Repository/PushTokenRepository.swift (1)
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (4)
getFCMToken(19-21)isSyncNeeded(23-25)resetSyncNeeded(27-38)clearCache(40-42)
src/Projects/BKPresentation/Sources/AppCoordinator.swift (1)
src/Projects/BKCore/Sources/Logger/AppLogger.swift (2)
error(44-53)debug(31-42)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Coordinator/SettingCoordinator.swift (1)
src/Projects/BKPresentation/Sources/Common/Coordinator/Coordinator.swift (1)
addChildCoordinator(47-52)
src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (1)
src/Projects/BKData/Sources/Error/NetworkError.swift (1)
toDomainError(16-27)
src/Projects/BKDomain/Sources/Interface/Repository/NotificationRepository.swift (1)
src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (2)
upsertFCMToken(14-25)upsertNotificationSettings(27-40)
src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift (2)
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (2)
isSyncNeeded(23-25)getFCMToken(19-21)src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (1)
upsertFCMToken(14-25)
src/Projects/BKDomain/Sources/Interface/Usecase/SyncFCMTokenUseCase.swift (1)
src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift (1)
execute(19-39)
src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift (1)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Coordinator/SettingCoordinator.swift (1)
didTapNotificationSetting(27-34)
src/Projects/BKData/Sources/Interface/Storage/PushTokenProvider.swift (2)
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (1)
isSyncNeeded(23-25)src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenProvider.swift (1)
clearCache(44-47)
src/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swift (2)
src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift (1)
execute(19-39)src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (1)
upsertNotificationSettings(27-40)
src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenProvider.swift (3)
src/Projects/BKData/Sources/Repository/DefaultRecentSearchRepository.swift (1)
load(19-21)src/Projects/BKCore/Sources/Logger/AppLogger.swift (1)
error(44-53)src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (1)
isSyncNeeded(23-25)
src/Projects/BKDomain/Sources/Interface/Usecase/UpdateNotificationSettingsUseCase.swift (1)
src/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swift (1)
execute(16-31)
src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift (1)
src/Projects/BKData/Sources/Repository/DefaultRecentSearchRepository.swift (1)
load(19-21)
src/Projects/Booket/Sources/AppDelegate.swift (2)
src/Projects/BKCore/Sources/Tracking/GAManager.swift (1)
configureAnalytics(8-33)src/Projects/BKCore/Sources/Logger/AppLogger.swift (1)
error(44-53)
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (1)
src/Projects/BKCore/Sources/Logger/AppLogger.swift (1)
error(44-53)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift (3)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift (1)
send(43-47)src/Projects/BKPresentation/Sources/Common/Coordinator/URLPresenting.swift (1)
presentApp(19-29)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (1)
updateNotificationToggle(178-183)
src/Projects/BKData/Sources/Interface/Storage/PushTokenStore.swift (1)
src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift (3)
save(15-31)resetSyncNeeded(33-43)clear(45-56)
src/Projects/BKData/Sources/API/UserAPI.swift (1)
src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (2)
upsertFCMToken(14-25)upsertNotificationSettings(27-40)
src/Projects/Booket/Project.swift (1)
src/Tuist/ProjectDescriptionHelpers/TargetDependency+External.swift (1)
external(33-35)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (1)
src/Projects/BKDesign/Sources/Extensions/UIColor+.swift (3)
bkContentColor(52-63)bkBackgroundColor(38-50)bkBaseColor(91-102)
🪛 SwiftLint (0.57.0)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift
[Warning] 11-11: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
[Warning] 86-86: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (49)
src/SupportingFiles/Booket/Info.plist (2)
25-26: LGTM!Firebase 수동 통합을 위해
FirebaseAppDelegateProxyEnabled를 비활성화하는 것이 적절합니다.
87-90: LGTM!푸시 알림 백그라운드 처리를 위한
UIBackgroundModes설정이 올바르게 추가되었습니다.src/SupportingFiles/Booket/Booket.entitlements (1)
5-6: 추가 정보를 수집하기 위해 다른 쿼리를 실행하겠습니다.프로덕션 환경 설정에 대한 우려는 불필요합니다.
Xcode는 앱 서명 시 자동으로 entitlements 값을 재정의하며, 값은 provisioning profile에서 파생됩니다. 개발 프로필의 경우 development, 배포 프로필의 경우 production으로 설정됩니다. 또한 archive 중 entitlements가 자동으로 업데이트되며, 배포 프로세스 중에 entitlements는 코드 서명에 사용된 provisioning profile을 통해 설정됩니다.
배포 allowlist의 경우 aps-environment는 항상 production을 포함합니다. 따라서 현재 entitlements 파일에서
development로 설정한 것은 개발 빌드에 적합하며, App Store 제출 시 Xcode가 자동으로 production으로 재정의합니다.추가 구성이 필요하지 않습니다.
src/Tuist/Package.swift (1)
19-20: 변경사항이 올바르게 적용되었습니다.FirebaseMessaging이 다른 Firebase 의존성과 동일한 형식으로 staticLibrary로 추가되었습니다. 일관성 있는 구성입니다.
src/Tuist/ProjectDescriptionHelpers/TargetDependency+External.swift (1)
26-26: External 열거형에 FirebaseMessaging 케이스가 올바르게 추가되었습니다.다른 Firebase 의존성들과 함께 적절히 그룹화되어 있습니다.
src/Projects/Booket/Project.swift (2)
33-34: 디버그 타겟에 Firebase 의존성이 올바르게 추가되었습니다.FirebaseAnalytics와 FirebaseMessaging은 FCM 기능을 위해 함께 필요한 일반적인 구성입니다.
77-79: 릴리스 타겟에 Firebase 의존성이 일관되게 추가되었습니다.검증 결과, 모든 구성요소가 올바르게 설정되어 있습니다:
- Firebase iOS SDK 12.1.0: 알려진 보안 취약점 없음 ✅
- APNs 설정 (
src/SupportingFiles/Booket/Booket.entitlements):aps-environment설정 완료 ✅- Info.plist 구성:
UIBackgroundModes: remote-notification및FirebaseAppDelegateProxyEnabled: NO(수동 Firebase 초기화 설정) ✅디버그 타겟과 동일한 Firebase 구성을 유지하여 일관성이 확보되었고, 모든 지원 인프라가 적절히 구성되어 있습니다.
src/Projects/BKData/Sources/Interface/Storage/PushTokenProvider.swift (1)
3-7: LGTM! Provider 패턴이 잘 설계되었습니다.읽기 전용 토큰 제공자 프로토콜이 명확하게 정의되어 있습니다.
PushTokenStore와의 책임 분리(읽기/쓰기)가 잘 되어 있으며,clearCache()메서드로 캐시 무효화를 지원하는 것도 좋습니다.src/Projects/BKData/Sources/Interface/Storage/PushTokenStore.swift (1)
6-14: LGTM! Store 프로토콜 설계가 우수합니다.토큰 저장소 프로토콜이 Combine 패턴을 잘 활용하고 있으며, 각 메서드의 책임이 명확합니다. 구현체에서
save()시 토큰 변경 여부를 확인하여 동기화 플래그를 설정하는 로직도 효율적입니다.src/Projects/Booket/Sources/SceneDelegate.swift (1)
62-70: LGTM! 의존성 주입이 올바르게 구성되었습니다.
SyncFCMTokenUseCase가@Autowired를 통해 적절히 주입되고AppCoordinator에 전달되어 앱 시작 시 FCM 토큰 동기화가 가능하도록 설정되었습니다.src/Projects/BKData/Sources/DTO/Response/UserProfileResponseDTO.swift (1)
12-12: LGTM! DTO 필드 추가가 정확합니다.
notificationEnabled필드가 올바르게 추가되었고,toUserProfile()메서드에서 도메인 모델로의 매핑도 정확합니다.Also applies to: 20-21
src/Projects/BKPresentation/Sources/MainFlow/Setting/ViewModel/SettingViewModel.swift (1)
10-10: LGTM! 메뉴 항목 추가 및 접근 제어 로직이 적절합니다.알림 메뉴 항목이 올바르게 추가되었으며, 인증된 사용자(member)에게만 표시되도록 필터링 로직이 정확하게 구현되었습니다. 기존 패턴을 잘 따르고 있습니다.
Also applies to: 104-109
src/Projects/BKStorage/Sources/StorageAssembly.swift (1)
44-61: LGTM! DI 등록이 올바르게 구성되었습니다.
PushTokenProvider는 싱글톤으로,PushTokenStore는 기본 스코프로 등록되어 있으며, 이는 기존TokenProvider/TokenStore패턴과 일관성 있게 구현되었습니다. 캐싱 제공자는 싱글톤으로, 저장소는 연산별 인스턴스로 사용하는 것이 적절합니다.src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift (1)
72-73: LGTM! 알림 설정 화면 네비게이션이 올바르게 구현되었습니다.새로운 알림 메뉴 항목 선택 시 코디네이터를 통한 화면 전환 로직이 기존 패턴과 일관성 있게 구현되었습니다.
src/Projects/BKData/Sources/DTO/Request/UpsertFCMTokenRequestDTO.swift (1)
5-7: LGTM! 단순하고 명확한 DTO 구조입니다.FCM 토큰 업서트 요청을 위한 최소한의 구조로 적절하게 구현되었습니다. struct가
public으로 표시되지 않았지만, BKData 모듈 내부에서만 사용된다면 내부 가시성이 적합합니다.src/Projects/BKData/Sources/DTO/Request/NotificationStatusRequestDTO.swift (1)
5-7: LGTM! 알림 설정 상태를 표현하는 간결한 DTO입니다.Boolean 프로퍼티를 사용하여 알림 활성화 상태를 명확하게 전달하며,
Encodable준수를 통해 서버 요청 본문으로 직렬화할 수 있습니다.src/Projects/BKDomain/Sources/Entity/UserProfile.swift (1)
11-11: LGTM! 도메인 엔티티에 알림 상태가 올바르게 추가되었습니다.
notificationEnabled프로퍼티가 선언, 초기화 파라미터, 할당 전반에 걸쳐 일관되게 구현되었습니다. UserProfileResponseDTO와의 매핑도 PR 변경사항에 포함되어 있어 API-도메인 계층 간 동기화가 보장됩니다.Also applies to: 18-19, 26-26
src/Projects/BKPresentation/Sources/AuthFlow/Coordinator/TermsCoordinator.swift (1)
27-27: LGTM! 프로토콜 리팩토링이 적절하게 적용되었습니다.
WebPresenting에서URLPresenting으로의 전환은 프로젝트 전반에 걸친 리팩토링의 일부입니다.URLPresenting은 기존 웹 URL 처리에 더해 앱 스킴 URL 처리 기능(presentApp(url:))을 추가하여 URL 라우팅을 개선합니다.src/Projects/BKDomain/Sources/Interface/Usecase/SyncFCMTokenUseCase.swift (1)
6-10: LGTM! 명확한 문서화와 함께 잘 설계된 유즈케이스 인터페이스입니다.한글 주석이 유즈케이스의 목적과 동작 흐름(플래그 확인 → 필요시 서버 전송 → 성공시 플래그 리셋)을 명확하게 설명하고 있습니다. Combine의
AnyPublisher를 사용한 반응형 API는 프로젝트의 기존 패턴과 일관되며,DefaultSyncFCMTokenUseCase구현체가 설명된 로직을 올바르게 구현하고 있습니다.src/Projects/BKData/Sources/DataAssembly.swift (1)
133-149: LGTM! DI 컨테이너에 새 리포지토리가 올바르게 등록되었습니다.두 리포지토리 모두 기존 패턴을 따라 적절하게 등록되었습니다:
NotificationRepository: OAuthNetworkProvider를 사용하여 인증된 엔드포인트 호출PushTokenRepository:PushTokenProvider와PushTokenStore를 주입받아 토큰 관리기본 스코프(transient)가 사용되었는데, 이는 파일 내 대부분의 다른 리포지토리와 일관됩니다.
src/Projects/BKStorage/Sources/Constant/StorageKeys.swift (1)
3-7: LGTM! 스토리지 키가 적절하게 공개되었습니다.enum과 모든 키 상수를
public으로 표시하여 모듈 간 접근이 가능하도록 했습니다. FCM 기능을 위한 두 개의 새 키(fcmTokenKey,isSyncNeededKey)가 추가되었으며, 명명 규칙이 일관되고 명확합니다.src/Projects/BKPresentation/Sources/MainFlow/Setting/Coordinator/SettingCoordinator.swift (2)
26-35: LGTM! 알림 설정 화면으로의 네비게이션이 올바르게 구현되었습니다.
didTapNotificationSetting()메서드가 Coordinator 패턴을 적절히 따릅니다:
NotificationSettingsCoordinator생성 시 부모와 네비게이션 컨트롤러 전달addChildCoordinator로 계층 구조 설정start()호출로 새 플로우 시작
37-37: LGTM! 프로토콜 준수가 프로젝트 전반의 리팩토링과 일관되게 업데이트되었습니다.
WebPresenting에서URLPresenting으로의 전환이 다른 코디네이터들과 동일하게 적용되었습니다.src/Projects/BKDomain/Sources/Interface/Repository/NotificationRepository.swift (1)
5-13: 프로토콜 정의가 깔끔합니다.Repository 패턴을 잘 따르고 있으며, Combine을 활용한 비동기 작업 처리가 적절합니다. 메서드 시그니처도 명확하고 구현체(DefaultNotificationRepository)와 잘 연동되어 있습니다.
src/Projects/BKPresentation/Sources/AppCoordinator.swift (3)
21-21: 의존성 주입이 적절합니다.새로운 use case를 깔끔하게 추가했으며, 기존 코드 패턴을 잘 따르고 있습니다.
Also applies to: 29-30, 37-37
42-42: 앱 시작 시 FCM 토큰 동기화를 트리거하는 것이 적절합니다.
50-64: FCM 토큰 동기화 구현이 잘 되어 있습니다.에러 핸들링과 로깅이 적절하게 구현되어 있으며, Combine 패턴을 올바르게 사용하고 있습니다.
src/Projects/Booket/Sources/AppDelegate.swift (4)
5-6: 필요한 의존성이 적절하게 추가되었습니다.FCM 통합을 위한 import와 Combine 구독 관리를 위한 cancellables 선언이 적절합니다.
Also applies to: 8-9, 18-18
34-41: 알림 권한 요청 플로우가 적절합니다.권한 승인 후 원격 알림 등록을 수행하는 흐름이 올바르게 구현되어 있습니다. 메인 스레드에서
registerForRemoteNotifications를 호출하는 것도 적절합니다.
57-62: APNs 토큰을 Firebase Messaging에 전달하는 표준 패턴입니다.
86-92: Foreground 알림 표시 옵션이 적절합니다.앱이 포그라운드에 있을 때도 알림을 배너, 리스트, 사운드로 표시하는 것은 일반적인 UX 패턴입니다.
Also applies to: 95-95
src/Projects/BKDomain/Sources/Interface/Repository/PushTokenRepository.swift (1)
5-10: 프로토콜 설계가 명확하고 적절합니다.동기/비동기 메서드의 조합이 적절하며, 토큰 관리와 동기화 상태를 효과적으로 표현하고 있습니다.
src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenProvider.swift (3)
7-14: 의존성 주입과 캐시 구조가 적절합니다.KeyValueStorage를 주입받고 캐시 프로퍼티를 활용한 구조가 깔끔합니다.
16-28: FCM 토큰 로딩 로직이 잘 구현되어 있습니다.캐시 우선 접근 방식과 에러 핸들링이 적절합니다. 에러 발생 시 로깅 후 nil을 반환하는 것은 합리적인 fallback 전략입니다.
30-47: 동기화 플래그 로딩과 캐시 초기화가 일관되게 구현되었습니다.
isSyncNeeded프로퍼티가fcmToken과 동일한 패턴을 따르며,clearCache()메서드도 명확합니다.src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/Coordinator/NotificationSettingsCoordinator.swift (2)
27-27: URLPresenting 프로토콜 확장이 적절합니다.시스템 설정 열기 등의 URL 처리를 위한 기능을 깔끔하게 추가했습니다.
18-24: 이 리뷰 코멘트는 재검토가 필요합니다.
NotificationSettingsViewModel은 실제로 의존성을 제대로 주입받고 있습니다.@Autowired어노테이션을 통해authStateUseCase와updateNotificationSettingsUseCase가 자동으로 주입됩니다.코디네이터가
NotificationSettingsViewModel()을 매개변수 없이 생성하는 것은 이 아키텍처에서 의도된 정상적인 패턴입니다. DI 컨테이너가@Autowired프로퍼티를 자동으로 해결하므로, 추가 변경사항은 필요하지 않습니다.Likely an incorrect or invalid review comment.
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift (4)
7-26: ViewController 구조가 깔끔합니다.네비게이션 설정과 의존성 관리가 적절하며,
AnyViewBindableViewModel래퍼 패턴도 잘 활용하고 있습니다.
28-41: Lifecycle 관리가 적절합니다.설정 화면 진입 시 탭바를 숨기고, 나갈 때 다시 표시하는 것은 일반적인 UX 패턴입니다.
59-75: 상태 바인딩과 애니메이션 제어가 잘 구현되었습니다.초기 로드 시 애니메이션을 비활성화하여 부자연스러운 UI 전환을 방지하는 것은 좋은 UX 개선입니다.
53-57: iOS 버전 호환성 검증 완료 - 문제 없음프로젝트의 최소 배포 타겟이 iOS 16.0으로 설정되어 있습니다 (src/Tuist/ProjectDescriptionHelpers/Project+Templates.swift).
UIApplication.openNotificationSettingsURLString은 iOS 15.4+에서 지원되므로, 현재 코드는 완벽하게 호환됩니다. 추가 조치는 필요하지 않습니다.src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift (3)
7-17: Use case 구조가 깔끔합니다.의존성 주입이 명확하고, stateless한 use case를 struct로 구현한 것이 적절합니다.
19-30: 조기 반환 로직이 효율적입니다.동기화가 필요없거나 토큰이 없을 때 성공으로 반환하는 것은 합리적인 설계 결정입니다. 토큰이 없는 상황을 "동기화할 것이 없음"으로 처리하는 것이 적절합니다.
32-39: 토큰 동기화 플로우가 올바르게 구현되었습니다.
flatMap을 사용하여 토큰 업로드와 동기화 플래그 리셋을 순차적으로 실행하는 것이 적절하며, 에러 로깅도 잘 되어 있습니다.src/Projects/BKData/Sources/API/UserAPI.swift (5)
8-9: LGTM! 새로운 enum case가 올바르게 추가되었습니다.FCM 토큰과 알림 설정 업데이트를 위한 enum case가 기존 패턴을 따라 명확하게 정의되었습니다. 연관 값의 타입과 네이밍도 적절합니다.
23-26: LGTM! 경로 매핑이 올바르게 구현되었습니다.새로운 엔드포인트의 경로가 RESTful 컨벤션을 따르고 있으며, 기존 패턴과 일관성 있게 작성되었습니다.
34-34: LGTM! HTTP 메서드 매핑이 적절합니다.Upsert 작업에 PUT 메서드를 사용하는 것이 올바르며, 유사한 작업을 수행하는 케이스들을 함께 그룹화한 것이 깔끔합니다.
41-41: LGTM! 헤더 설정이 올바릅니다.JSON 요청 본문을 전송하는 케이스들에 대해 Content-Type 헤더가 적절하게 설정되었습니다.
54-57: LGTM! 요청 본문 매핑이 올바르게 구현되었습니다.각 케이스에 대한 DTO 생성이 기존 패턴을 따르고 있으며, 파라미터 매핑도 정확합니다. Repository 레이어에서 정상적으로 활용되고 있는 것을 확인했습니다.
...cts/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
Show resolved
Hide resolved
...entation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
일단 클라이언트 수준에서 바로 수정 가능한 것 위주로 체크완료했습니다. 알림 로직 자체는 설계랑 구현 다 잘하셨어요! 수고하셨습니다 :-)
...cts/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
Show resolved
Hide resolved
...cts/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
Show resolved
Hide resolved
YAPP-Github/Reed-Android#204 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift (1)
51-70: 접근 제어로 인한 컴파일 오류:private func가 다른 extension에서 보이지 않습니다
handleError(다른 extension)에서 아래 헬퍼들을 호출하지만, 이 헬퍼들이private extension내부에 다시private func로 선언되어 있어 같은 파일의 다른 extension에서 접근 불가합니다.fileprivate또는 기본 가시성(=private extension의 기본: fileprivate)을 사용해야 합니다.아래처럼 함수 선언의
private키워드만 제거해 주세요(또는fileprivate로 변경):-private extension ErrorHandleable where Self: Coordinator & AuthenticationRequiredNotifying { - private func presentAuthErrorAlert() { +private extension ErrorHandleable where Self: Coordinator & AuthenticationRequiredNotifying { + func presentAuthErrorAlert() { @@ - private func presentGuestAuthErrorAlert() { + func presentGuestAuthErrorAlert() { @@ - private func presentServerErrorAlert() { + func presentServerErrorAlert() { @@ - private func presentTimeoutAlert() { + func presentTimeoutAlert() {Also applies to: 72-94, 96-112, 114-130
♻️ Duplicate comments (2)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (2)
38-46: 이 코멘트는 이전 리뷰에서 이미 논의되었습니다 (doyeonk429의 코멘트 참조).
178-187: 이 코멘트는 이전 리뷰에서 이미 논의되었습니다 (doyeonk429의 코멘트 참조).
🧹 Nitpick comments (5)
src/SupportingFiles/Booket/Info.plist (1)
25-26: Boolean 타입 사용을 고려하세요.
FirebaseAppDelegateProxyEnabled값이 문자열"NO"로 설정되어 있습니다. 일반적으로 plist의 boolean 값은<false/>나<true/>를 사용합니다. 파일 내 다른 boolean 설정(40-43번 라인)과의 일관성을 위해 boolean 타입 사용을 고려하세요.<key>FirebaseAppDelegateProxyEnabled</key> - <string>NO</string> + <false/>src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift (1)
12-12: 불필요한 nil 초기화 제거 권장옵셔널 변수를
nil로 명시적으로 초기화하는 것은 중복입니다. Swift에서 옵셔널 변수는 기본적으로nil로 초기화됩니다.다음과 같이 수정하는 것을 권장합니다:
- var error: DomainError? = nil + var error: DomainError?src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift (1)
46-48: UI 프리젠테이션 메인 스레드 보장 제안
present(...)/dismiss(...)가 백그라운드에서 호출될 가능성이 있으면 크래시/워닝 유발합니다. 프리젠테이션 함수들에@MainActor를 붙이거나, 호출부를 메인 큐로 디스패치하는 것을 권장합니다.예시(간단 디스패치):
- topViewController?.present(dialogViewController, animated: true) + DispatchQueue.main.async { + self.topViewController?.present(dialogViewController, animated: true) + }혹은 프리젠트/디스미스 메서드 자체를
@MainActor로 표시하여 호출 경로 전반을 보장할 수도 있습니다. 업스트림에서 이미receive(on: RunLoop.main)등으로 보장 중인지 확인 부탁드립니다.Also applies to: 68-70, 92-94, 110-112, 128-130
src/Projects/BKDomain/Sources/Error/AuthError.swift (2)
17-21: 불필요한 바인딩 제거로 경고/가독성 개선
message를 바인딩하지만 사용하지 않아 경고 요인입니다. 바인딩을 제거해 간결하게 작성하세요.- case .sdkError(message: let message): - return .clientError - case .serverError(message: let message): - return .internalServerError + case .sdkError: + return .clientError + case .serverError: + return .internalServerError
21-27: 약관 미동의의 매핑 재검토(권장)
.termsNotAccepted를.unauthorized로 매핑하면 프레젠테이션 레이어에서 세션 만료/재로그인 흐름이 트리거됩니다. 약관 동의 유도 UX가 필요하다면 별도 도메인 에러(예:.termsNotAccepted또는.forbidden)를 두는 편이 명확합니다.
- 선택지 A: DomainError에 전용 케이스 추가 후 해당 케이스 처리(UI: 약관 동의 화면으로 전환).
- 선택지 B: 유지하되,
.unauthorized내에서 약관 미동의 사유 식별 가능하도록 컨텍스트를 보강.기존 러닝에 따르면
unauthorized는 세션 만료 흐름을 보장하기 위한 제약으로 쓰입니다. 의도된 동작인지 확인 부탁드립니다. Based on learnings
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/Projects/BKDomain/Sources/Error/AuthError.swift(1 hunks)src/Projects/BKDomain/Sources/Error/DomainError.swift(1 hunks)src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift(1 hunks)src/SupportingFiles/Booket/Info.plist(4 hunks)
🧰 Additional context used
🧠 Learnings (14)
📓 Common learnings
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:43-45
Timestamp: 2025-08-08T01:39:15.620Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성 개선 작업을 별도의 전용 PR이나 이슈에서 일괄 처리하는 것을 선호한다. 개별 기능 구현 PR에서는 접근성 관련 제안을 하지 않고, 접근성 전담 작업에서 한번에 처리하는 방식을 원한다.
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/MainFlow/Home/View/HomeViewController.swift:19-24
Timestamp: 2025-08-08T01:38:59.656Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성(accessibility) 관련 개선사항은 현재 작업 중인 PR에서 즉시 처리하지 않고, 접근성 전용 PR이나 이슈를 별도로 만들어 한번에 처리하는 것을 선호한다.
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 147
File: src/Projects/BKData/Sources/Service/AppleLoginDelegateProxy.swift:65-78
Timestamp: 2025-08-06T08:53:16.909Z
Learning: clxxrlove는 Reed-iOS 프로젝트의 Apple 로그인 구현에서 authorizationCode 강제 언래핑 이슈에 대해 현재 구현으로도 문제없다고 판단했다. 초기 구현 단계에서는 기본 기능 동작을 우선시하고 추후 개선할 예정이라는 기존 패턴과 일치한다.
📚 Learning: 2025-07-07T13:47:32.784Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 56
File: src/Projects/BKPresentation/Sources/AuthFlow/ViewModel/LoginViewModel.swift:109-111
Timestamp: 2025-07-07T13:47:32.784Z
Learning: The LoginViewModel's authenticateWithToken case in src/Projects/BKPresentation/Sources/AuthFlow/ViewModel/LoginViewModel.swift is test code and not important for implementation according to the user.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift
📚 Learning: 2025-08-08T11:20:33.637Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 160
File: src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift:9-20
Timestamp: 2025-08-08T11:20:33.637Z
Learning: Reed-iOS: ErrorHandleable.handleError는 API를 사용하는 View용이며, unauthorized 발생 시 세션 만료 흐름을 보장하기 위해 Self: Coordinator & SessionExpirationNotifying 제약을 의도적으로 요구한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swiftsrc/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift
📚 Learning: 2025-08-08T01:38:59.656Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/MainFlow/Home/View/HomeViewController.swift:19-24
Timestamp: 2025-08-08T01:38:59.656Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성(accessibility) 관련 개선사항은 현재 작업 중인 PR에서 즉시 처리하지 않고, 접근성 전용 PR이나 이슈를 별도로 만들어 한번에 처리하는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-08T01:39:15.620Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:43-45
Timestamp: 2025-08-08T01:39:15.620Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성 개선 작업을 별도의 전용 PR이나 이슈에서 일괄 처리하는 것을 선호한다. 개별 기능 구현 PR에서는 접근성 관련 제안을 하지 않고, 접근성 전담 작업에서 한번에 처리하는 방식을 원한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-06T08:53:16.909Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 147
File: src/Projects/BKData/Sources/Service/AppleLoginDelegateProxy.swift:65-78
Timestamp: 2025-08-06T08:53:16.909Z
Learning: clxxrlove는 Reed-iOS 프로젝트의 Apple 로그인 구현에서 authorizationCode 강제 언래핑 이슈에 대해 현재 구현으로도 문제없다고 판단했다. 초기 구현 단계에서는 기본 기능 동작을 우선시하고 추후 개선할 예정이라는 기존 패턴과 일치한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-12T01:44:56.649Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 169
File: src/Projects/BKPresentation/Sources/MainFlow/BookDetail/ViewModel/BookDetailViewModel.swift:153-164
Timestamp: 2025-08-12T01:44:56.649Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 복잡한 액션 구조 변경보다는 throttling과 같은 간단한 해결책을 선호한다. nextPage 중복 증가 버그 해결에 대해 Action에 page 정보를 추가하는 방법보다 ViewController 레벨의 throttling을 선호했다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-31T12:41:20.058Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 125
File: src/Projects/BKPresentation/Sources/MainFlow/Note/View/SentenceRegistrationView.swift:12-14
Timestamp: 2025-07-31T12:41:20.058Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 OCR 기능 구현 시 SentenceRegistrationEvent.ocrScanTapped enum을 미리 선언해두고, 나중에 SentenceRegistrationView에서 버튼으로 진입하는 플로우를 구현할 때 연결할 예정이라고 명시했다. UI와 기능 구현을 먼저 완성하고 화면 간 연결 로직은 후속 작업으로 계획하는 개발 방식을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-01T20:35:55.669Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 134
File: src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift:175-182
Timestamp: 2025-08-01T20:35:55.669Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 1차 API 연결과 같은 초기 구현 단계에서는 에러 핸들링보다 기본 기능 동작을 우선시한다. 에러 처리 개선은 향후 단계로 미루는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-12T02:18:00.565Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 169
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:51-52
Timestamp: 2025-08-12T02:18:00.565Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 로그인 버튼들의 텍스트 형식을 일관되게 "Provider로 로그인" 패턴으로 유지하는 것을 선호한다. 카카오 공식 브랜딩 가이드라인보다 UI 일관성을 우선시하는 디자인 결정을 한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-10T14:01:04.933Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 69
File: src/Projects/BKDesign/PreviewApp/Sources/View/CatalogViewController.swift:57-70
Timestamp: 2025-07-10T14:01:04.933Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 테스트/데모용 코드와 프로덕션 코드에 대해 다른 코드 품질 기준을 적용한다. 테스트용 뷰에서는 안전성 검증 등의 엄격한 코드 품질 요구사항을 적용하지 않는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-24T13:00:59.096Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 105
File: src/Projects/BKPresentation/Sources/TabBarFlow/BottomTabBarController.swift:24-30
Timestamp: 2025-07-24T13:00:59.096Z
Learning: doyeonk429는 BottomTabBarController에서 viewDidLayoutSubviews의 레이어 재생성 방식을 유지하기를 선호합니다. 탭바에서는 레이아웃 변경 케이스가 거의 없기 때문에 성능 최적화보다는 현재의 단순한 구현 방식이 더 적합하다고 판단합니다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-21T05:16:18.481Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 94
File: src/Projects/BKData/Sources/Repository/DefaultBookRepository.swift:15-40
Timestamp: 2025-07-21T05:16:18.481Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 기능 구현 시 우선순위를 명확하게 설정한다. 기본 기능 구현을 먼저 완료하고, 에러 처리나 안정성 개선은 이후 단계에서 진행하는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-21T19:57:02.989Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 190
File: src/Projects/BKPresentation/Sources/MainFlow/BookDetail/ViewModel/SentenceCardViewModel.swift:1-9
Timestamp: 2025-08-21T19:57:02.989Z
Learning: Reed-iOS 프로젝트에서 PreviewApp(src/Projects/BKDesign/PreviewApp/Info.plist)에는 Photo Library 권한 설명 키(NSPhotoLibraryAddUsageDescription, NSPhotoLibraryUsageDescription)를 추가할 필요가 없다. PreviewApp은 디자인 프리뷰 용도로만 사용되며 실제 사진 저장 기능은 메인 앱에서만 사용한다.
Applied to files:
src/SupportingFiles/Booket/Info.plist
🧬 Code graph analysis (3)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift (1)
src/Projects/BKDomain/Sources/Error/AuthError.swift (1)
toDomainError(15-28)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (2)
src/Projects/BKDesign/Sources/Extensions/UIColor+.swift (3)
bkContentColor(52-63)bkBackgroundColor(38-50)bkBaseColor(91-102)src/Projects/BKDesign/Sources/Extensions/UIView+.swift (1)
addSubviews(7-9)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift (3)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift (1)
send(45-49)src/Projects/BKPresentation/Sources/Common/Coordinator/URLPresenting.swift (1)
presentApp(19-29)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (2)
updateNotificationToggle(179-182)updatePermissionRequestVisibility(184-186)
🪛 SwiftLint (0.57.0)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift
[Warning] 12-12: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
⏰ 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). (1)
- GitHub Check: build
🔇 Additional comments (6)
src/SupportingFiles/Booket/Info.plist (2)
40-43: LGTM!Firebase 자동 화면 보고 비활성화와 암호화 규정 준수 설정이 올바르게 구성되었습니다. Boolean 타입도 정확하게 사용되었습니다.
87-90: LGTM!푸시 알림 기능 구현을 위해
UIBackgroundModes에remote-notification이 올바르게 추가되었습니다. 이는 APNs/FCM 통합에 필수적인 설정입니다.src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift (1)
8-98: 구현이 잘 되어 있습니다ViewController 구현이 깔끔하고 적절합니다:
- Coordinator 패턴을 통한 네비게이션 처리가 올바릅니다
- 앱 포그라운드 진입 시 권한 상태 재확인 로직이 적절합니다
isInitialLoad플래그를 사용하여 초기 로딩 시 불필요한 애니메이션을 방지한 것이 좋습니다- MVVM 패턴이 명확하게 구현되어 있습니다
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift (1)
87-109: 리뷰 의견이 잘못되었습니다. 에러 처리가 올바르게 구현되어 있습니다.검증 결과:
- Line 95:
authStateUseCase.execute()는AnyPublisher<UserProfile, AuthError>를 반환하므로error.toDomainError()로AuthError를DomainError로 변환하는 것이 올바릅니다.- Line 105:
updateNotificationSettingsUseCase.execute()는AnyPublisher<Bool, DomainError>를 반환하므로 에러가 이미DomainError타입입니다. 따라서 직접 전달하는 것이 올바릅니다.두 메서드는 서로 다른 에러 타입을 반환하도록 설계되었고, 각 계층에서 적절하게 처리되고 있습니다. 추가 수정이 필요하지 않습니다.
src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift (1)
18-19:.unknown추가 처리 LGTM알 수 없는 오류를 서버 오류 얼럿과 함께 처리하는 방향 적절합니다. 기존 흐름과도 자연스럽게 맞습니다.
src/Projects/BKDomain/Sources/Error/DomainError.swift (1)
10-10:unknown케이스 추가 LGTM도메인 계층에서 미분류 오류를 안전하게 전달할 수 있게 되어 일관성이 좋아졌습니다. 프레젠테이션 레이어(
ErrorHandleable)와도 매칭됩니다.
| extension NotificationSettingsView { | ||
| func updateNotificationToggle(isEnabled: Bool, animated: Bool = true) { | ||
| guard permissionToggle.isOn != isEnabled else { return } | ||
| permissionToggle.setOn(isEnabled, animated: false) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
animated 파라미터가 무시되고 있습니다
Line 179의 animated 파라미터가 전달되었음에도 Line 181에서 항상 animated: false로 하드코딩되어 있습니다. 이로 인해 호출자가 의도한 애니메이션 동작이 무시됩니다.
다음과 같이 수정하세요:
func updateNotificationToggle(isEnabled: Bool, animated: Bool = true) {
guard permissionToggle.isOn != isEnabled else { return }
- permissionToggle.setOn(isEnabled, animated: false)
+ permissionToggle.setOn(isEnabled, animated: animated)
}🤖 Prompt for AI Agents
In
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
around lines 178 to 182, the method updateNotificationToggle ignores the
animated parameter by hardcoding animated: false when calling
permissionToggle.setOn; change the call to use the passed-in animated parameter
(permissionToggle.setOn(isEnabled, animated: animated)) so the caller's
animation intent is respected.
There was a problem hiding this 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 (2)
src/Projects/BKPresentation/Sources/AppCoordinator.swift (2)
51-65: FCM 토큰 동기화 로직이 구현되었습니다.Use case를 통한 토큰 동기화 구현이 적절합니다. 에러 발생 시 로그만 남기고 있는데, 토큰 동기화는 나중에 재시도될 수 있으므로 현재 구현으로도 충분합니다.
참고: 토큰이 업데이트될 때마다 자동으로 재동기화되는 메커니즘이 있다면 더 견고할 것입니다.
223-235: 알림 권한 요청 로직이 구현되었으나 개선이 필요합니다.기본적인 권한 요청 흐름은 올바르게 구현되었습니다. 다음 개선사항을 고려해주세요:
토큰 동기화 누락:
registerForRemoteNotifications()호출 후 FCM 토큰이 생성/갱신될 때, 서버와 동기화하는 로직이 필요합니다. 현재는AppDelegate에서 토큰을 로컬에 저장만 하고 있습니다.권한 상태 변경 감지:
.notDetermined인 경우만 처리하고 있는데, 사용자가 설정에서 권한을 다시 활성화한 경우는 감지하지 못합니다.첫 번째 이슈 해결을 위한 제안:
private func requestNotificationPermissionIfNeeded() { UNUserNotificationCenter.current().getNotificationSettings { settings in guard settings.authorizationStatus == .notDetermined else { return } UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in guard granted else { return } DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() + // FCM 토큰이 생성되면 자동으로 서버와 동기화됨 + // 하지만 명시적으로 재시도를 트리거할 수도 있음 + self.upsertFCMTokenIfNeeded() } } } }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/Projects/BKPresentation/Sources/AppCoordinator.swift(4 hunks)src/Projects/Booket/Sources/AppDelegate.swift(4 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/MainFlow/Home/View/HomeViewController.swift:19-24
Timestamp: 2025-08-08T01:38:59.656Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성(accessibility) 관련 개선사항은 현재 작업 중인 PR에서 즉시 처리하지 않고, 접근성 전용 PR이나 이슈를 별도로 만들어 한번에 처리하는 것을 선호한다.
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:43-45
Timestamp: 2025-08-08T01:39:15.620Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성 개선 작업을 별도의 전용 PR이나 이슈에서 일괄 처리하는 것을 선호한다. 개별 기능 구현 PR에서는 접근성 관련 제안을 하지 않고, 접근성 전담 작업에서 한번에 처리하는 방식을 원한다.
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 147
File: src/Projects/BKData/Sources/Service/AppleLoginDelegateProxy.swift:65-78
Timestamp: 2025-08-06T08:53:16.909Z
Learning: clxxrlove는 Reed-iOS 프로젝트의 Apple 로그인 구현에서 authorizationCode 강제 언래핑 이슈에 대해 현재 구현으로도 문제없다고 판단했다. 초기 구현 단계에서는 기본 기능 동작을 우선시하고 추후 개선할 예정이라는 기존 패턴과 일치한다.
📚 Learning: 2025-07-11T12:12:48.911Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 79
File: src/Projects/BKPresentation/Sources/MainFlow/Setting/ViewModel/SettingViewModel.swift:78-85
Timestamp: 2025-07-11T12:12:48.911Z
Learning: AppVersionUseCase.execute() returns AnyPublisher<String, Never> where the Failure type is Never, making error handling unnecessary. The app version is retrieved from the main bundle which is always available, and the implementation already handles missing version by defaulting to "-".
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swift
📚 Learning: 2025-07-30T11:32:20.533Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 120
File: src/Projects/BKPresentation/Sources/AppCoordinator.swift:31-41
Timestamp: 2025-07-30T11:32:20.533Z
Learning: Reed-iOS 프로젝트의 OnboardingCheckUseCase.execute() 메서드는 AnyPublisher<Bool, Never>를 반환하므로 실패할 수 없도록 설계되었다. Never 타입은 에러가 발생하지 않음을 보장하므로 에러 처리 로직이 불필요하다.
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swift
📚 Learning: 2025-08-19T17:14:52.765Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 179
File: src/Projects/BKPresentation/Sources/AppCoordinator.swift:53-73
Timestamp: 2025-08-19T17:14:52.765Z
Learning: Reed-iOS 프로젝트에서는 Coordinator 패턴에서 자동 cleanup 메커니즘이 구현되어 있다. LoginViewController 등에서 coordinator?.popAndFinish() 호출 시 didFinish()가 자동으로 parentCoordinator?.removeChildCoordinator(self)를 호출하여 child coordinator를 제거한다. 따라서 수동으로 child coordinator 제거를 구현할 필요가 없다.
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swift
📚 Learning: 2025-08-21T19:57:02.989Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 190
File: src/Projects/BKPresentation/Sources/MainFlow/BookDetail/ViewModel/SentenceCardViewModel.swift:1-9
Timestamp: 2025-08-21T19:57:02.989Z
Learning: Reed-iOS 프로젝트에서 PreviewApp(src/Projects/BKDesign/PreviewApp/Info.plist)에는 Photo Library 권한 설명 키(NSPhotoLibraryAddUsageDescription, NSPhotoLibraryUsageDescription)를 추가할 필요가 없다. PreviewApp은 디자인 프리뷰 용도로만 사용되며 실제 사진 저장 기능은 메인 앱에서만 사용한다.
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swift
🧬 Code graph analysis (2)
src/Projects/Booket/Sources/AppDelegate.swift (3)
src/Projects/BKCore/Sources/Tracking/GAManager.swift (1)
configureAnalytics(8-33)src/Tuist/ProjectDescriptionHelpers/TargetDependency+Module.swift (1)
storage(26-28)src/Projects/BKCore/Sources/Logger/AppLogger.swift (1)
error(44-53)
src/Projects/BKPresentation/Sources/AppCoordinator.swift (1)
src/Projects/BKCore/Sources/Logger/AppLogger.swift (2)
error(44-53)debug(31-42)
⏰ 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). (1)
- GitHub Check: build
🔇 Additional comments (6)
src/Projects/Booket/Sources/AppDelegate.swift (3)
34-36: 알림 delegate 설정이 올바르게 구현되었습니다.Firebase 초기화 후 notification center와 messaging delegate를 설정하는 순서가 적절합니다.
51-56: APNs 토큰 전달 구현이 올바릅니다.디바이스 토큰을 Firebase Messaging에 전달하는 표준 구현입니다.
80-86: 포그라운드 알림 표시 구현이 적절합니다.배너, 리스트, 사운드 옵션으로 포그라운드 알림을 표시하는 구현이 올바릅니다.
src/Projects/BKPresentation/Sources/AppCoordinator.swift (3)
22-22: DI 패턴이 올바르게 적용되었습니다.
SyncFCMTokenUseCase가 initializer를 통해 주입되는 방식이 프로젝트의 기존 패턴과 일치합니다.Also applies to: 30-31, 38-38
124-129: 로그인 후 알림 권한 요청 타이밍이 적절합니다.로그인 완료 후 알림 권한을 요청하는 것은 좋은 UX 패턴입니다. 사용자가 인증 후에 권한 요청을 받는 것이 자연스럽습니다.
43-43: FCM 토큰 동기화 검증 완료 - 문제 없음
DefaultSyncFCMTokenUseCase의 구현을 확인한 결과,getFCMToken()이 nil을 반환하는 경우를 이미 올바르게 처리하고 있습니다:guard let fcmToken = pushTokenRepository.getFCMToken() else { return Just(()) .setFailureType(to: DomainError.self) .eraseToAnyPublisher() }토큰이 없으면 성공적으로 조용히 반환되므로
start()시점에 토큰이 아직 생성되지 않았더라도 문제없습니다.MessagingDelegate에서 나중에 토큰이 생성되면 다음 동기화 사이클에서 처리됩니다.추가 검증이나 수정이 필요하지 않습니다.
| func messaging( | ||
| _ messaging: Messaging, | ||
| didReceiveRegistrationToken fcmToken: String? | ||
| ) { | ||
| guard let token = fcmToken else { return } | ||
| let storage = KeychainKeyValueStorage() | ||
| let pushTokenStore = KeychainPushTokenStore(storage: storage) | ||
|
|
||
| pushTokenStore.save(fcmToken: token) | ||
| .sink( | ||
| receiveCompletion: { completion in | ||
| if case .failure(let error) = completion { | ||
| AppLogger.auth.error("Failed to save FCM token: \(error)") | ||
| } | ||
| }, | ||
| receiveValue: { _ in } | ||
| ) | ||
| .store(in: &cancellables) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DI 원칙 위반 및 스레드 안전성 문제가 있습니다.
이 메서드에서 KeychainKeyValueStorage와 KeychainPushTokenStore를 직접 생성하는 것은 프로젝트의 DI 패턴을 위반합니다. 또한 다음 문제들이 있습니다:
- DI 위반: Storage 인스턴스를 AppDelegate 초기화 시점에 주입받아 property로 보관해야 합니다.
- 스레드 안전성:
MessagingDelegate메서드는 백그라운드 스레드에서 호출될 수 있는데,cancellables에 동기화 없이 접근하고 있습니다. - 타이밍 이슈:
AppCoordinator.start()에서upsertFCMTokenIfNeeded()가 호출되는데, FCM 토큰이 아직 생성되지 않았을 수 있습니다. 토큰 저장 후 서버 동기화를 트리거하는 메커니즘이 필요합니다.
제안하는 수정 방향:
final class AppDelegate: UIResponder, UIApplicationDelegate {
private var cancellables = Set<AnyCancellable>()
+ private let pushTokenStore: PushTokenStore
+ private let syncQueue = DispatchQueue(label: "com.booket.tokenSync")
+
+ override init() {
+ let storage = KeychainKeyValueStorage()
+ self.pushTokenStore = KeychainPushTokenStore(storage: storage)
+ super.init()
+ }그리고 토큰 저장 로직을 수정:
func messaging(
_ messaging: Messaging,
didReceiveRegistrationToken fcmToken: String?
) {
guard let token = fcmToken else { return }
- let storage = KeychainKeyValueStorage()
- let pushTokenStore = KeychainPushTokenStore(storage: storage)
-
- pushTokenStore.save(fcmToken: token)
+ pushTokenStore.save(fcmToken: token)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
AppLogger.auth.error("Failed to save FCM token: \(error)")
}
},
- receiveValue: { _ in }
+ receiveValue: { _ in
+ // 토큰 저장 성공 후 서버 동기화 트리거
+ NotificationCenter.default.post(name: .fcmTokenUpdated, object: nil)
+ }
)
- .store(in: &cancellables)
+ .store(in: &syncQueue.sync { cancellables })
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/Projects/Booket/Sources/AppDelegate.swift around lines 60 to 78, the
messaging(_:didReceiveRegistrationToken:) currently violates DI by creating
KeychainKeyValueStorage/KeychainPushTokenStore inline, accesses the cancellables
set unsafely from a possible background thread, and doesn't signal the app to
sync the token with the server after save; fix by injecting a
KeychainKeyValueStorage (or KeychainPushTokenStore) into AppDelegate at init and
storing it as a property, use that injected store in this method instead of
creating new instances, ensure all mutations to cancellables occur on a
dedicated serial DispatchQueue (or dispatch to DispatchQueue.main) to guarantee
thread safety, and after the save publisher completes successfully emit a
notification or call a coordinator method to trigger
upsertFCMTokenIfNeeded()/server sync; also ensure cancellables is accessed only
via the same queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 스레드 안정성은 Combine이 이미 처리 중
- 타이밍 이슈에 대한 매커니즘은 이미 존재
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@clxxrlove, 확인했습니다!
- 스레드 안정성: Combine이 내부적으로 처리하고 있다는 점 이해했습니다.
- 타이밍 메커니즘:
KeychainPushTokenStore.save()에서 토큰 변경 시isSyncNeeded플래그를 설정하고,AppCoordinator.start()에서SyncFCMTokenUseCase를 통해 동기화하는 구조가 이미 구현되어 있네요.
현재 구현으로 진행하시면 될 것 같습니다. 👍
🧠 Learnings used
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:43-45
Timestamp: 2025-08-08T01:39:15.620Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성 개선 작업을 별도의 전용 PR이나 이슈에서 일괄 처리하는 것을 선호한다. 개별 기능 구현 PR에서는 접근성 관련 제안을 하지 않고, 접근성 전담 작업에서 한번에 처리하는 방식을 원한다.
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/MainFlow/Home/View/HomeViewController.swift:19-24
Timestamp: 2025-08-08T01:38:59.656Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성(accessibility) 관련 개선사항은 현재 작업 중인 PR에서 즉시 처리하지 않고, 접근성 전용 PR이나 이슈를 별도로 만들어 한번에 처리하는 것을 선호한다.
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 147
File: src/Projects/BKData/Sources/Service/AppleLoginDelegateProxy.swift:65-78
Timestamp: 2025-08-06T08:53:16.909Z
Learning: clxxrlove는 Reed-iOS 프로젝트의 Apple 로그인 구현에서 authorizationCode 강제 언래핑 이슈에 대해 현재 구현으로도 문제없다고 판단했다. 초기 구현 단계에서는 기본 기능 동작을 우선시하고 추후 개선할 예정이라는 기존 패턴과 일치한다.
There was a problem hiding this 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)
src/SupportingFiles/Booket/Info.plist (1)
40-43: Firebase 및 App Store 설정이 올바르게 추가되었습니다.
FirebaseAutomaticScreenReportingEnabled와ITSAppUsesNonExemptEncryption설정이 올바르게 추가되었으며, boolean 타입도 적절하게 사용되었습니다. 과거 리뷰에서 지적된 중복 키 문제도 해결되었습니다.선택적 개선사항: Firebase 관련 키들(25-26, 40-41번 라인)을 한 곳에 그룹화하면 가독성이 향상됩니다. 다만 plist의 기능적 동작에는 영향이 없으므로 필수는 아닙니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/SupportingFiles/Booket/Info.plist(3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:43-45
Timestamp: 2025-08-08T01:39:15.620Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성 개선 작업을 별도의 전용 PR이나 이슈에서 일괄 처리하는 것을 선호한다. 개별 기능 구현 PR에서는 접근성 관련 제안을 하지 않고, 접근성 전담 작업에서 한번에 처리하는 방식을 원한다.
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/MainFlow/Home/View/HomeViewController.swift:19-24
Timestamp: 2025-08-08T01:38:59.656Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성(accessibility) 관련 개선사항은 현재 작업 중인 PR에서 즉시 처리하지 않고, 접근성 전용 PR이나 이슈를 별도로 만들어 한번에 처리하는 것을 선호한다.
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 147
File: src/Projects/BKData/Sources/Service/AppleLoginDelegateProxy.swift:65-78
Timestamp: 2025-08-06T08:53:16.909Z
Learning: clxxrlove는 Reed-iOS 프로젝트의 Apple 로그인 구현에서 authorizationCode 강제 언래핑 이슈에 대해 현재 구현으로도 문제없다고 판단했다. 초기 구현 단계에서는 기본 기능 동작을 우선시하고 추후 개선할 예정이라는 기존 패턴과 일치한다.
📚 Learning: 2025-08-21T19:57:02.989Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 190
File: src/Projects/BKPresentation/Sources/MainFlow/BookDetail/ViewModel/SentenceCardViewModel.swift:1-9
Timestamp: 2025-08-21T19:57:02.989Z
Learning: Reed-iOS 프로젝트에서 PreviewApp(src/Projects/BKDesign/PreviewApp/Info.plist)에는 Photo Library 권한 설명 키(NSPhotoLibraryAddUsageDescription, NSPhotoLibraryUsageDescription)를 추가할 필요가 없다. PreviewApp은 디자인 프리뷰 용도로만 사용되며 실제 사진 저장 기능은 메인 앱에서만 사용한다.
Applied to files:
src/SupportingFiles/Booket/Info.plist
⏰ 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). (1)
- GitHub Check: build
🔇 Additional comments (1)
src/SupportingFiles/Booket/Info.plist (1)
83-86: 푸시 알림을 위한 백그라운드 모드가 올바르게 추가되었습니다.
UIBackgroundModes에remote-notification추가는 백그라운드에서 푸시 알림을 수신하기 위해 필수적인 설정입니다. 올바르게 구현되었습니다.
… multiple URL schemes
1165f3e to
151af29
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/SupportingFiles/Booket/Info.plist (1)
25-26: 타입 일관성을 위해 boolean 타입 사용을 권장합니다.
FirebaseAppDelegateProxyEnabled키가<string>NO</string>타입으로 설정되어 있습니다. 파일 내 다른 Firebase 키들(40-43번 라인)은<false/>를 사용하고 있어 일관성이 떨어집니다. Firebase 설정 키는 boolean 타입을 사용하는 것이 표준입니다.다음과 같이 수정하는 것을 권장합니다:
<key>FirebaseAppDelegateProxyEnabled</key> -<string>NO</string> +<false/>src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (1)
179-182: animated 파라미터가 무시되고 있습니다.Line 181에서
animated: false로 하드코딩되어 있어, 호출자가 전달한animated파라미터가 무시됩니다. 이전 리뷰에서도 지적된 사항인데 아직 수정되지 않은 것으로 보입니다.다음과 같이 수정하세요:
func updateNotificationToggle(isEnabled: Bool, animated: Bool = true) { guard permissionToggle.isOn != isEnabled else { return } - permissionToggle.setOn(isEnabled, animated: false) + permissionToggle.setOn(isEnabled, animated: animated) }
🧹 Nitpick comments (2)
src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift (1)
48-59: 부분 삭제 실패 시 불일치 상태를 고려하세요.Line 50에서 첫 번째 키 삭제는 성공하지만 Line 51에서 두 번째 키 삭제가 실패하면 부분적으로만 정리된 상태가 됩니다. clear 작업의 특성상 치명적이지는 않지만, 가능하면 두 삭제 작업을 원자적으로 처리하는 것을 고려해보세요.
src/Projects/BKPresentation/Sources/Common/Coordinator/URLPresenting.swift (1)
19-29: URL 스킴별 분기 처리가 잘 구현되었습니다.presentApp 메서드가 https URL은 웹으로, 다른 스킴은 앱으로 열도록 적절히 분기하고 있습니다. 다만, 선택적으로 URL 열기 실패 시 로깅을 추가하면 디버깅에 도움이 될 수 있습니다.
다음과 같이 완료 핸들러와 로깅을 추가하는 것을 고려해볼 수 있습니다:
func presentApp( url: URL? ) { guard let url else { return } if url.scheme?.lowercased() == "https" { presentWeb(url: url) } else if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) + UIApplication.shared.open(url) { success in + if !success { + Log.error("Failed to open URL: \(url)", logger: AppLogger.presentation) + } + } } }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (41)
src/Projects/BKData/Sources/API/UserAPI.swift(3 hunks)src/Projects/BKData/Sources/DTO/Request/NotificationStatusRequestDTO.swift(1 hunks)src/Projects/BKData/Sources/DTO/Request/UpsertFCMTokenRequestDTO.swift(1 hunks)src/Projects/BKData/Sources/DTO/Response/UserProfileResponseDTO.swift(1 hunks)src/Projects/BKData/Sources/DataAssembly.swift(1 hunks)src/Projects/BKData/Sources/Interface/Storage/PushTokenProvider.swift(1 hunks)src/Projects/BKData/Sources/Interface/Storage/PushTokenStore.swift(1 hunks)src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift(1 hunks)src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift(1 hunks)src/Projects/BKDomain/Sources/DomainAssembly.swift(1 hunks)src/Projects/BKDomain/Sources/Entity/UserProfile.swift(1 hunks)src/Projects/BKDomain/Sources/Error/AuthError.swift(1 hunks)src/Projects/BKDomain/Sources/Error/DomainError.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Repository/NotificationRepository.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Repository/PushTokenRepository.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Usecase/SyncFCMTokenUseCase.swift(1 hunks)src/Projects/BKDomain/Sources/Interface/Usecase/UpdateNotificationSettingsUseCase.swift(1 hunks)src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift(1 hunks)src/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swift(1 hunks)src/Projects/BKPresentation/Sources/AppCoordinator.swift(4 hunks)src/Projects/BKPresentation/Sources/AuthFlow/Coordinator/TermsCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift(1 hunks)src/Projects/BKPresentation/Sources/Common/Coordinator/URLPresenting.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Coordinator/SettingCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/Coordinator/NotificationSettingsCoordinator.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift(1 hunks)src/Projects/BKPresentation/Sources/MainFlow/Setting/ViewModel/SettingViewModel.swift(2 hunks)src/Projects/BKStorage/Sources/Constant/StorageKeys.swift(1 hunks)src/Projects/BKStorage/Sources/StorageAssembly.swift(1 hunks)src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenProvider.swift(1 hunks)src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift(1 hunks)src/Projects/Booket/Project.swift(2 hunks)src/Projects/Booket/Sources/AppDelegate.swift(4 hunks)src/Projects/Booket/Sources/SceneDelegate.swift(1 hunks)src/SupportingFiles/Booket/Booket.entitlements(1 hunks)src/SupportingFiles/Booket/Info.plist(3 hunks)src/Tuist/Package.swift(1 hunks)src/Tuist/ProjectDescriptionHelpers/TargetDependency+External.swift(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (18)
- src/Projects/BKData/Sources/DTO/Request/NotificationStatusRequestDTO.swift
- src/Projects/BKStorage/Sources/StorageAssembly.swift
- src/Projects/BKDomain/Sources/Interface/Repository/NotificationRepository.swift
- src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift
- src/Projects/BKStorage/Sources/Constant/StorageKeys.swift
- src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenProvider.swift
- src/Tuist/Package.swift
- src/Projects/BKData/Sources/Interface/Storage/PushTokenStore.swift
- src/Projects/Booket/Sources/AppDelegate.swift
- src/Projects/BKDomain/Sources/Entity/UserProfile.swift
- src/Projects/BKPresentation/Sources/AuthFlow/Coordinator/TermsCoordinator.swift
- src/Projects/BKPresentation/Sources/MainFlow/Setting/ViewModel/SettingViewModel.swift
- src/SupportingFiles/Booket/Booket.entitlements
- src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift
- src/Projects/BKDomain/Sources/DomainAssembly.swift
- src/Tuist/ProjectDescriptionHelpers/TargetDependency+External.swift
- src/Projects/BKData/Sources/Interface/Storage/PushTokenProvider.swift
- src/Projects/BKDomain/Sources/Error/AuthError.swift
🧰 Additional context used
🧠 Learnings (18)
📓 Common learnings
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:43-45
Timestamp: 2025-08-08T01:39:15.620Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성 개선 작업을 별도의 전용 PR이나 이슈에서 일괄 처리하는 것을 선호한다. 개별 기능 구현 PR에서는 접근성 관련 제안을 하지 않고, 접근성 전담 작업에서 한번에 처리하는 방식을 원한다.
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/MainFlow/Home/View/HomeViewController.swift:19-24
Timestamp: 2025-08-08T01:38:59.656Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성(accessibility) 관련 개선사항은 현재 작업 중인 PR에서 즉시 처리하지 않고, 접근성 전용 PR이나 이슈를 별도로 만들어 한번에 처리하는 것을 선호한다.
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 147
File: src/Projects/BKData/Sources/Service/AppleLoginDelegateProxy.swift:65-78
Timestamp: 2025-08-06T08:53:16.909Z
Learning: clxxrlove는 Reed-iOS 프로젝트의 Apple 로그인 구현에서 authorizationCode 강제 언래핑 이슈에 대해 현재 구현으로도 문제없다고 판단했다. 초기 구현 단계에서는 기본 기능 동작을 우선시하고 추후 개선할 예정이라는 기존 패턴과 일치한다.
📚 Learning: 2025-08-08T16:13:31.712Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 163
File: src/Projects/BKData/Sources/DTO/Request/UserBookRegisterRequestDTO.swift:7-9
Timestamp: 2025-08-08T16:13:31.712Z
Learning: Reed-iOS에서 서버 스펙 변경으로 DTO 필드명이 바뀔 때, doyeonk429는 코드 전반의 파라미터/시그니처 리네이밍까지 확장하지 않고, 실제 요청 직렬화 키가 맞는지(예: isbn13)만 보장하면 충분하다는 범위 최소화 방식을 선호한다. (PR: YAPP-Github/Reed-iOS#163, 파일: BKData/Sources/DTO/Request/UserBookRegisterRequestDTO.swift)
Applied to files:
src/Projects/BKData/Sources/DTO/Request/UpsertFCMTokenRequestDTO.swift
📚 Learning: 2025-07-11T12:12:48.911Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 79
File: src/Projects/BKPresentation/Sources/MainFlow/Setting/ViewModel/SettingViewModel.swift:78-85
Timestamp: 2025-07-11T12:12:48.911Z
Learning: AppVersionUseCase.execute() returns AnyPublisher<String, Never> where the Failure type is Never, making error handling unnecessary. The app version is retrieved from the main bundle which is always available, and the implementation already handles missing version by defaulting to "-".
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swiftsrc/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swift
📚 Learning: 2025-07-30T11:32:20.533Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 120
File: src/Projects/BKPresentation/Sources/AppCoordinator.swift:31-41
Timestamp: 2025-07-30T11:32:20.533Z
Learning: Reed-iOS 프로젝트의 OnboardingCheckUseCase.execute() 메서드는 AnyPublisher<Bool, Never>를 반환하므로 실패할 수 없도록 설계되었다. Never 타입은 에러가 발생하지 않음을 보장하므로 에러 처리 로직이 불필요하다.
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swiftsrc/Projects/BKDomain/Sources/Interface/Usecase/UpdateNotificationSettingsUseCase.swiftsrc/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swiftsrc/Projects/BKDomain/Sources/Interface/Usecase/SyncFCMTokenUseCase.swift
📚 Learning: 2025-08-19T17:14:52.765Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 179
File: src/Projects/BKPresentation/Sources/AppCoordinator.swift:53-73
Timestamp: 2025-08-19T17:14:52.765Z
Learning: Reed-iOS 프로젝트에서는 Coordinator 패턴에서 자동 cleanup 메커니즘이 구현되어 있다. LoginViewController 등에서 coordinator?.popAndFinish() 호출 시 didFinish()가 자동으로 parentCoordinator?.removeChildCoordinator(self)를 호출하여 child coordinator를 제거한다. 따라서 수동으로 child coordinator 제거를 구현할 필요가 없다.
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swift
📚 Learning: 2025-08-21T19:57:02.989Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 190
File: src/Projects/BKPresentation/Sources/MainFlow/BookDetail/ViewModel/SentenceCardViewModel.swift:1-9
Timestamp: 2025-08-21T19:57:02.989Z
Learning: Reed-iOS 프로젝트에서 PreviewApp(src/Projects/BKDesign/PreviewApp/Info.plist)에는 Photo Library 권한 설명 키(NSPhotoLibraryAddUsageDescription, NSPhotoLibraryUsageDescription)를 추가할 필요가 없다. PreviewApp은 디자인 프리뷰 용도로만 사용되며 실제 사진 저장 기능은 메인 앱에서만 사용한다.
Applied to files:
src/Projects/BKPresentation/Sources/AppCoordinator.swiftsrc/SupportingFiles/Booket/Info.plist
📚 Learning: 2025-08-08T01:38:59.656Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/MainFlow/Home/View/HomeViewController.swift:19-24
Timestamp: 2025-08-08T01:38:59.656Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성(accessibility) 관련 개선사항은 현재 작업 중인 PR에서 즉시 처리하지 않고, 접근성 전용 PR이나 이슈를 별도로 만들어 한번에 처리하는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-08T01:39:15.620Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 157
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:43-45
Timestamp: 2025-08-08T01:39:15.620Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 접근성 개선 작업을 별도의 전용 PR이나 이슈에서 일괄 처리하는 것을 선호한다. 개별 기능 구현 PR에서는 접근성 관련 제안을 하지 않고, 접근성 전담 작업에서 한번에 처리하는 방식을 원한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-06T08:53:16.909Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 147
File: src/Projects/BKData/Sources/Service/AppleLoginDelegateProxy.swift:65-78
Timestamp: 2025-08-06T08:53:16.909Z
Learning: clxxrlove는 Reed-iOS 프로젝트의 Apple 로그인 구현에서 authorizationCode 강제 언래핑 이슈에 대해 현재 구현으로도 문제없다고 판단했다. 초기 구현 단계에서는 기본 기능 동작을 우선시하고 추후 개선할 예정이라는 기존 패턴과 일치한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-12T01:44:56.649Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 169
File: src/Projects/BKPresentation/Sources/MainFlow/BookDetail/ViewModel/BookDetailViewModel.swift:153-164
Timestamp: 2025-08-12T01:44:56.649Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 복잡한 액션 구조 변경보다는 throttling과 같은 간단한 해결책을 선호한다. nextPage 중복 증가 버그 해결에 대해 Action에 page 정보를 추가하는 방법보다 ViewController 레벨의 throttling을 선호했다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-31T12:41:20.058Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 125
File: src/Projects/BKPresentation/Sources/MainFlow/Note/View/SentenceRegistrationView.swift:12-14
Timestamp: 2025-07-31T12:41:20.058Z
Learning: doyeonk429는 Reed-iOS 프로젝트에서 OCR 기능 구현 시 SentenceRegistrationEvent.ocrScanTapped enum을 미리 선언해두고, 나중에 SentenceRegistrationView에서 버튼으로 진입하는 플로우를 구현할 때 연결할 예정이라고 명시했다. UI와 기능 구현을 먼저 완성하고 화면 간 연결 로직은 후속 작업으로 계획하는 개발 방식을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-01T20:35:55.669Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 134
File: src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift:175-182
Timestamp: 2025-08-01T20:35:55.669Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 1차 API 연결과 같은 초기 구현 단계에서는 에러 핸들링보다 기본 기능 동작을 우선시한다. 에러 처리 개선은 향후 단계로 미루는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-08-12T02:18:00.565Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 169
File: src/Projects/BKPresentation/Sources/AuthFlow/View/LoginView.swift:51-52
Timestamp: 2025-08-12T02:18:00.565Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 로그인 버튼들의 텍스트 형식을 일관되게 "Provider로 로그인" 패턴으로 유지하는 것을 선호한다. 카카오 공식 브랜딩 가이드라인보다 UI 일관성을 우선시하는 디자인 결정을 한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-24T13:00:59.096Z
Learnt from: doyeonk429
Repo: YAPP-Github/Reed-iOS PR: 105
File: src/Projects/BKPresentation/Sources/TabBarFlow/BottomTabBarController.swift:24-30
Timestamp: 2025-07-24T13:00:59.096Z
Learning: doyeonk429는 BottomTabBarController에서 viewDidLayoutSubviews의 레이어 재생성 방식을 유지하기를 선호합니다. 탭바에서는 레이아웃 변경 케이스가 거의 없기 때문에 성능 최적화보다는 현재의 단순한 구현 방식이 더 적합하다고 판단합니다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-10T14:01:04.933Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 69
File: src/Projects/BKDesign/PreviewApp/Sources/View/CatalogViewController.swift:57-70
Timestamp: 2025-07-10T14:01:04.933Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 테스트/데모용 코드와 프로덕션 코드에 대해 다른 코드 품질 기준을 적용한다. 테스트용 뷰에서는 안전성 검증 등의 엄격한 코드 품질 요구사항을 적용하지 않는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-21T05:16:18.481Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 94
File: src/Projects/BKData/Sources/Repository/DefaultBookRepository.swift:15-40
Timestamp: 2025-07-21T05:16:18.481Z
Learning: clxxrlove는 Reed-iOS 프로젝트에서 기능 구현 시 우선순위를 명확하게 설정한다. 기본 기능 구현을 먼저 완료하고, 에러 처리나 안정성 개선은 이후 단계에서 진행하는 것을 선호한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift
📚 Learning: 2025-07-07T13:47:32.784Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 56
File: src/Projects/BKPresentation/Sources/AuthFlow/ViewModel/LoginViewModel.swift:109-111
Timestamp: 2025-07-07T13:47:32.784Z
Learning: The LoginViewModel's authenticateWithToken case in src/Projects/BKPresentation/Sources/AuthFlow/ViewModel/LoginViewModel.swift is test code and not important for implementation according to the user.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift
📚 Learning: 2025-08-08T11:20:33.637Z
Learnt from: clxxrlove
Repo: YAPP-Github/Reed-iOS PR: 160
File: src/Projects/BKPresentation/Sources/Common/Coordinator/ErrorHandleable.swift:9-20
Timestamp: 2025-08-08T11:20:33.637Z
Learning: Reed-iOS: ErrorHandleable.handleError는 API를 사용하는 View용이며, unauthorized 발생 시 세션 만료 흐름을 보장하기 위해 Self: Coordinator & SessionExpirationNotifying 제약을 의도적으로 요구한다.
Applied to files:
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift
🧬 Code graph analysis (16)
src/Projects/BKData/Sources/DataAssembly.swift (1)
src/Projects/BKCore/Sources/DiContainer/DIContainer.swift (1)
register(25-46)
src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift (3)
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (2)
isSyncNeeded(23-25)getFCMToken(19-21)src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (1)
upsertFCMToken(14-25)src/Projects/BKCore/Sources/Extension/Publisher+.swift (1)
debugError(8-25)
src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift (1)
src/Tuist/ProjectDescriptionHelpers/TargetDependency+Module.swift (1)
storage(26-28)
src/Projects/BKData/Sources/API/UserAPI.swift (1)
src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (2)
upsertFCMToken(14-25)upsertNotificationSettings(27-40)
src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift (1)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Coordinator/SettingCoordinator.swift (1)
didTapNotificationSetting(27-34)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift (3)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift (1)
send(45-49)src/Projects/BKPresentation/Sources/Common/Coordinator/URLPresenting.swift (1)
presentApp(19-29)src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (2)
updateNotificationToggle(179-182)updatePermissionRequestVisibility(184-186)
src/Projects/BKPresentation/Sources/AppCoordinator.swift (1)
src/Projects/BKCore/Sources/Logger/AppLogger.swift (2)
error(44-53)debug(31-42)
src/Projects/BKDomain/Sources/Interface/Usecase/UpdateNotificationSettingsUseCase.swift (1)
src/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swift (1)
execute(16-31)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Coordinator/SettingCoordinator.swift (1)
src/Projects/BKPresentation/Sources/Common/Coordinator/Coordinator.swift (1)
addChildCoordinator(47-52)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (2)
src/Projects/BKDesign/Sources/Extensions/UIColor+.swift (3)
bkContentColor(52-63)bkBackgroundColor(38-50)bkBaseColor(91-102)src/Projects/BKDesign/Sources/Extensions/UIView+.swift (1)
addSubviews(7-9)
src/Projects/BKDomain/Sources/Interface/Repository/PushTokenRepository.swift (1)
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (4)
getFCMToken(19-21)isSyncNeeded(23-25)resetSyncNeeded(27-38)clearCache(40-42)
src/Projects/BKDomain/Sources/UseCase/DefaultUpdateNotificationSettingsUseCase.swift (3)
src/Projects/BKData/Sources/Repository/DefaultNotificationRepository.swift (1)
upsertNotificationSettings(27-40)src/Projects/BKCore/Sources/Extension/Publisher+.swift (1)
debugError(8-25)src/Projects/BKCore/Sources/Logger/AppLogger.swift (1)
error(44-53)
src/Projects/BKDomain/Sources/Interface/Usecase/SyncFCMTokenUseCase.swift (1)
src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift (1)
execute(19-39)
src/Projects/Booket/Project.swift (1)
src/Tuist/ProjectDescriptionHelpers/TargetDependency+External.swift (1)
external(33-35)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift (1)
src/Projects/BKDomain/Sources/Error/AuthError.swift (1)
toDomainError(15-28)
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (1)
src/Projects/BKCore/Sources/Logger/AppLogger.swift (1)
error(44-53)
🪛 SwiftLint (0.57.0)
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/ViewModel/NotificationSettingsViewModel.swift
[Warning] 12-12: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
⏰ 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). (1)
- GitHub Check: build
🔇 Additional comments (30)
src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift (2)
6-14: LGTM! 싱글톤 패턴이 적절하게 구현되었습니다.AppDelegate에서 DI 없이 사용하기 위한 shared instance와 테스트/DI를 위한 public initializer를 모두 제공하여 유연성을 확보했습니다.
36-46: LGTM! 동기화 플래그 리셋 로직이 명확합니다.메서드가 단순하고 의도가 명확하게 구현되었습니다.
src/Projects/BKPresentation/Sources/MainFlow/Setting/View/SettingViewController.swift (1)
72-73: LGTM! 알림 설정 네비게이션이 기존 패턴과 일관되게 추가되었습니다.다른 메뉴 항목들과 동일한 방식으로 코디네이터를 통한 화면 전환을 구현했습니다.
src/Projects/BKData/Sources/DTO/Response/UserProfileResponseDTO.swift (1)
12-22: LGTM! DTO에 알림 설정 필드가 올바르게 추가되었습니다.서버 응답 스펙과 매핑이 일치하며, 도메인 모델로의 변환도 적절하게 구현되었습니다.
src/Projects/BKDomain/Sources/Error/DomainError.swift (1)
10-10: LGTM! unknown 케이스 추가가 적절합니다.예상하지 못한 에러를 매핑하기 위한 unknown 케이스는 에러 처리 패턴에서 일반적이며 유용합니다.
src/Projects/BKDomain/Sources/Interface/Usecase/UpdateNotificationSettingsUseCase.swift (1)
1-9: LGTM! UseCase 프로토콜이 명확하게 정의되었습니다.알림 설정 업데이트는 네트워크 오류나 인증 문제 등으로 실패할 수 있으므로, DomainError를 반환 타입에 포함한 것이 적절합니다. 메서드 시그니처도 간결하고 의도가 명확합니다.
src/Projects/Booket/Sources/SceneDelegate.swift (1)
62-70: LGTM! DI 패턴이 일관되게 적용되었습니다.새로운 SyncFCMTokenUseCase 의존성이 기존 코드 스타일과 동일한 방식으로 추가되었습니다.
src/Projects/BKData/Sources/DataAssembly.swift (1)
133-149: LGTM! 새로운 레포지토리들이 올바르게 DI에 등록되었습니다.NotificationRepository와 PushTokenRepository의 DI 등록이 기존 패턴과 일관되게 구현되었으며, 필요한 의존성들도 적절하게 주입됩니다. OAuth NetworkProvider를 사용하는 것도 인증이 필요한 알림 API 특성에 맞습니다.
src/Projects/BKData/Sources/DTO/Request/UpsertFCMTokenRequestDTO.swift (1)
5-7: 접근 제어자 패턴 분석 결과: 리뷰 지적 부정확코드베이스 검증 결과, 요청 DTO(Request)는 모두 명시적 접근 제어자 없이 내부 접근으로 설정되어 있습니다:
- 11개 Request DTO 전부
internal기본값 적용 (UpsertFCMTokenRequestDTO 포함)응답 DTO(Response)는 혼합 패턴입니다:
- 7개는
public명시 (UserProfileResponseDTO 등)- 4개는
internal기본값 (SearchBookResponseDTO, FetchRecordResponseDTO 등)결론: UpsertFCMTokenRequestDTO의
internal기본값은 Request DTO 전체의 일관된 패턴이며, 데이터 레이어 내부 전용 용도에 적합합니다. UserProfileResponseDTO(Response)와의 불일치는 의도된 아키텍처 차이입니다.Likely an incorrect or invalid review comment.
src/Projects/BKDomain/Sources/Interface/Repository/PushTokenRepository.swift (1)
5-10: 프로토콜 설계가 잘 되어 있습니다.PushTokenRepository 프로토콜이 명확하게 정의되어 있으며, FCM 토큰 관리를 위한 책임이 적절히 분리되어 있습니다. 토큰 조회, 동기화 상태 확인, 동기화 플래그 리셋, 캐시 초기화 메서드가 모두 포함되어 있어 완성도가 높습니다.
src/Projects/BKDomain/Sources/Interface/Usecase/SyncFCMTokenUseCase.swift (1)
6-10: Use case 프로토콜과 문서화가 잘 되어 있습니다.SyncFCMTokenUseCase 프로토콜이 명확하게 정의되어 있으며, 한글 주석으로 동작이 잘 설명되어 있습니다. isSyncNeeded 플래그 확인 → 서버 전송 → 플래그 리셋의 흐름이 명확히 문서화되어 있어 이해하기 쉽습니다.
src/Projects/Booket/Project.swift (2)
33-34: FirebaseMessaging 의존성이 올바르게 추가되었습니다.디버그 타겟에 FirebaseMessaging이 적절히 추가되어 APNs/FCM 통합을 지원합니다. 기존 Firebase 의존성 패턴을 일관되게 따르고 있습니다.
77-79: 릴리즈 타겟에도 FirebaseMessaging이 올바르게 추가되었습니다.릴리즈 타겟에도 일관되게 FirebaseMessaging 의존성이 추가되어 프로덕션 환경에서도 알림 기능이 정상 동작할 수 있습니다.
src/SupportingFiles/Booket/Info.plist (2)
40-43: Firebase 설정 키가 올바르게 추가되었습니다.
FirebaseAutomaticScreenReportingEnabled와ITSAppUsesNonExemptEncryption키가 적절한 boolean 타입으로 설정되어 있습니다. Firebase 통합 및 App Store 제출 요구사항을 충족합니다.
83-86: 백그라운드 알림 지원이 올바르게 추가되었습니다.
UIBackgroundModes에remote-notification이 추가되어 백그라운드에서 푸시 알림을 수신할 수 있습니다. APNs/FCM 통합에 필수적인 설정입니다.src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/Coordinator/NotificationSettingsCoordinator.swift (2)
5-25: 코디네이터 구현이 잘 되어 있습니다.NotificationSettingsCoordinator가 표준 Coordinator 패턴을 올바르게 따르고 있으며, 뷰 컨트롤러와 뷰모델 생성 및 네비게이션 스택 관리가 적절히 구현되어 있습니다.
27-27: URLPresenting 프로토콜 적용이 적절합니다.URLPresenting 프로토콜 확장을 통해 URL 기반 네비게이션을 지원하도록 구성되어 있습니다. 설정 화면에서 외부 링크나 시스템 설정 앱으로의 이동을 처리할 수 있습니다.
src/Projects/BKData/Sources/Repository/DefaultPushTokenRepository.swift (4)
7-17: 레포지토리 구조가 잘 설계되어 있습니다.DefaultPushTokenRepository가 PushTokenProvider와 PushTokenStore를 적절히 조합하여 관심사를 분리하고 있습니다. 의존성 주입을 통한 초기화가 명확합니다.
19-25: 토큰 조회 및 동기화 상태 확인이 올바르게 구현되었습니다.getFCMToken()과 isSyncNeeded() 메서드가 provider에 적절히 위임하고 있으며, nil 처리를 위한 기본값(false) 사용이 안전합니다.
27-38: 동기화 플래그 리셋 로직과 에러 처리가 잘 되어 있습니다.resetSyncNeeded() 메서드가 store 업데이트 후 provider 캐시를 정리하는 순서를 올바르게 구현하고 있으며, 에러 발생 시 로깅과 DomainError 매핑이 적절히 처리되고 있습니다.
40-42: 캐시 초기화가 간단하고 명확하게 구현되었습니다.clearCache() 메서드가 provider에 올바르게 위임하고 있습니다.
src/Projects/BKPresentation/Sources/Common/Coordinator/URLPresenting.swift (1)
6-17: 프로토콜 이름 변경과 메서드 단순화가 적절합니다.WebPresenting에서 URLPresenting으로 리네이밍하여 더 넓은 범위의 URL 처리를 나타내며, presentWeb 메서드에서 entersReaderIfAvailable 매개변수를 제거하여 인터페이스가 단순해졌습니다.
src/Projects/BKDomain/Sources/UseCase/DefaultSyncFCMTokenUseCase.swift (2)
7-17: Use case 구조가 명확하게 설계되었습니다.DefaultSyncFCMTokenUseCase가 필요한 레포지토리들을 의존성 주입을 통해 받아 관심사 분리를 잘 실현하고 있습니다.
19-39: FCM 토큰 동기화 로직이 견고하게 구현되었습니다.execute() 메서드가 다음을 잘 처리하고 있습니다:
- 동기화 불필요 시 조기 반환 (lines 20-24)
- FCM 토큰 없을 시 조기 반환 (lines 26-30)
- 토큰 업서트 후 동기화 플래그 리셋 (lines 32-38)
Combine 기반의 리액티브 프로그래밍 패턴을 올바르게 사용하고 있으며, debugError를 통한 에러 로깅도 적절합니다.
src/Projects/BKData/Sources/API/UserAPI.swift (1)
8-9: 잘 구현되었습니다!FCM 토큰 및 알림 설정 API 엔드포인트가 기존 패턴을 잘 따르고 있습니다. HTTP 메서드, 헤더, 요청 본문 매핑이 모두 일관되게 구현되어 있고, DefaultNotificationRepository와의 연동도 적절합니다.
Also applies to: 23-26, 34-34, 41-41, 54-57
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsView.swift (5)
7-78: UI 구조가 깔끔하게 설계되었습니다.뷰 계층 구조가 명확하고, 스택 뷰를 적절히 활용하여 레이아웃을 구성했습니다. 디자인 시스템(BKDesign)을 잘 활용하고 있으며, 클로저 기반 콜백 패턴도 적절합니다.
80-174: 레이아웃 및 설정이 잘 구현되었습니다.제약 조건을 활용한 두 가지 레이아웃 상태 전환 방식이 깔끔하고,
isAnimating플래그로 동시 애니메이션을 방지하는 것도 좋습니다. SnapKit을 활용한 레이아웃 설정과 제스처 인식기 구성도 적절합니다.
184-198: 액션 핸들러가 적절하게 구현되었습니다.퍼블릭 메서드와 액션 핸들러가 깔끔하게 구현되어 있습니다. 클로저 호출을 통한 이벤트 전달 방식이 적절합니다.
200-239: 애니메이션 로직이 잘 구현되었습니다.초기 로드 시 애니메이션 없이 즉시 표시하는 처리와
isAnimating플래그를 통한 동시 애니메이션 방지 로직이 적절합니다. 권한 요청 뷰의 표시/숨김 타이밍은 이전 논의를 통해 오버랩 방지를 위해 의도적으로 설계된 것으로 이해됩니다.
242-253: 레이아웃 상수가 잘 관리되고 있습니다.레이아웃 관련 상수를 enum으로 중앙화하고 디자인 시스템의 값을 일관되게 사용하는 것이 좋습니다.
| private func checkNotificationAuthorization() { | ||
| UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in | ||
| DispatchQueue.main.async { | ||
| let isAuthorized = settings.authorizationStatus == .authorized | ||
| self?.viewModel.send(.systemNotificationAuthorizationChecked(isAuthorized)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override func bindAction() { | ||
| viewModel.send(.onAppear) | ||
| } | ||
|
|
||
| override func bindState() { | ||
| viewModel.statePublisher | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { [weak self] state in | ||
| guard let self = self else { return } | ||
| // 초기 로드시에는 애니메이션 없이 즉시 설정 | ||
| let shouldAnimate = !self.isInitialLoad | ||
| self.contentView.updateNotificationToggle(isEnabled: state.notificationEnabled, animated: shouldAnimate) | ||
| self.contentView.updatePermissionRequestVisibility( | ||
| shouldShow: !state.systemNotificationAuthorized, | ||
| animated: shouldAnimate | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로비저널 권한도 허용 상태로 처리해야 합니다
authorizationStatus == .authorized만 체크하면 iOS가 부여한 .provisional(조용한 알림 허용)이나 App Clip에서 사용하는 .ephemeral 상태를 모두 “미허용”으로 잘못 분류합니다. 그 결과, 이미 기기에서 알림을 받아볼 수 있는 사용자에게 계속 권한 요청 UI를 띄우게 되고, 토글·동기화 로직도 실제 상태와 엇갈립니다. Apple 문서에서는 .provisional 역시 알림을 보낼 수 있는 허용 상태임을 명시합니다.(stackoverflow.com)
authorized, provisional, ephemeral을 모두 허용으로 간주하도록 분기 로직을 보완해주세요.
- let isAuthorized = settings.authorizationStatus == .authorized
+ let status = settings.authorizationStatus
+ let isAuthorized: Bool
+ if #available(iOS 14.0, *) {
+ isAuthorized = status == .authorized || status == .provisional || status == .ephemeral
+ } else {
+ isAuthorized = status == .authorized || status == .provisional
+ }🤖 Prompt for AI Agents
In
src/Projects/BKPresentation/Sources/MainFlow/Setting/Notification/View/NotificationSettingsViewController.swift
around lines 70 to 94, the notification authorization check only treats
settings.authorizationStatus == .authorized as allowed; update the logic to
treat .authorized, .provisional, and .ephemeral as granted so users with
provisional or ephemeral permissions aren’t shown the permission UI incorrectly
and toggles/sync remain consistent. Change the isAuthorized calculation to check
for those three statuses and then send systemNotificationAuthorizationChecked
with that result on the main queue.
| case .errorOccurred(let error): | ||
| newState.isLoading = false | ||
| newState.error = error | ||
| /// 에러 발생시 이전 상태로 롤백 | ||
| effects.append(.fetchNotificationSettings) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
무한 재시도 루프 방지 필요
.errorOccurred에서 항상 .fetchNotificationSettings를 다시 발생시키면, 권한 만료나 서버 오류처럼 지속적으로 실패하는 상황에서 사이드이펙트가 즉시 재호출되며 끝없이 반복됩니다. 이 루프는 UI를 멈추게 하고 백엔드에도 불필요한 트래픽을 유발합니다. 실패 시 즉시 재요청하지 말고 사용자의 추가 액션(오류 확인, 재시도 버튼 등)이나 특정 오류 타입(일시적 네트워크)에 한해 재시도를 트리거하도록 변경해주세요.
case .errorOccurred(let error):
newState.isLoading = false
newState.error = error
- /// 에러 발생시 이전 상태로 롤백
- effects.append(.fetchNotificationSettings)
+ /// 재시도는 별도 액션(사용자 트리거 등)에서 처리
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case .errorOccurred(let error): | |
| newState.isLoading = false | |
| newState.error = error | |
| /// 에러 발생시 이전 상태로 롤백 | |
| effects.append(.fetchNotificationSettings) | |
| case .errorOccurred(let error): | |
| newState.isLoading = false | |
| newState.error = error | |
| /// 재시도는 별도 액션(사용자 트리거 등)에서 처리 |
| public func save(fcmToken: String) -> AnyPublisher<Void, TokenError> { | ||
| do { | ||
| let existingToken: String? = try? storage.load(for: StorageKeys.fcmTokenKey) | ||
| let isTokenChanged = existingToken != fcmToken | ||
| try storage.save(fcmToken, for: StorageKeys.fcmTokenKey) | ||
| if isTokenChanged { | ||
| try storage.save(true, for: StorageKeys.isSyncNeededKey) | ||
| } | ||
|
|
||
| return Just(()) | ||
| .setFailureType(to: TokenError.self) | ||
| .eraseToAnyPublisher() | ||
| } catch { | ||
| return Fail(error: TokenError.saveFailed(underlying: error)) | ||
| .eraseToAnyPublisher() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
토큰 저장과 동기화 플래그 업데이트 간 일관성을 검토하세요.
현재 구현에서는 Line 22에서 토큰 저장이 성공한 후 Line 24에서 동기화 플래그 업데이트가 실패하면, 새 토큰은 저장되지만 서버 동기화가 트리거되지 않는 불일치 상태가 발생할 수 있습니다. 또한 여러 스레드에서 동시에 이 메서드를 호출하면 경쟁 상태(race condition)가 발생할 수 있습니다.
다음 스크립트로 동시 호출 패턴을 확인하세요:
🏁 Script executed:
#!/bin/bash
# Description: KeychainPushTokenStore.save 메서드의 동시 호출 패턴 확인
# save 메서드 호출 위치 검색
rg -n -A 5 -B 3 '\.save\(fcmToken:' --type swiftLength of output: 798
추가 검증을 위해 저장소 구현과 동기화 메커니즘을 확인하겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# KeychainPushTokenStore 클래스 정의 및 storage 구현 확인
rg -n -A 20 'class KeychainPushTokenStore' --type swift
Length of output: 0
🏁 Script executed:
#!/bin/bash
# storage 속성 타입과 구현 확인
rg -n -B 5 -A 10 'var storage' --type swift | head -100Length of output: 3548
🏁 Script executed:
#!/bin/bash
# 모든 save 메서드 호출 패턴 검색 (더 광범위)
rg -n 'storage\.save\(' --type swift -C 2Length of output: 4114
🏁 Script executed:
#!/bin/bash
# DispatchQueue, Lock, NSLock 등 동기화 메커니즘 검사
rg -n 'DispatchQueue|NSLock|Lock|@Synchronized' --type swift | grep -i 'push\|token\|keychain'Length of output: 0
🏁 Script executed:
#!/bin/bash
# TokenStorage 프로토콜/인터페이스 정의 확인
rg -n -A 15 'protocol.*TokenStorage\|class.*TokenStorage' --type swiftLength of output: 0
파일 내용을 직접 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
# KeychainPushTokenStore.swift 전체 파일 확인
cat src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swiftLength of output: 2125
🏁 Script executed:
#!/bin/bash
# KeyValueStorage 또는 storage 인터페이스 검색
find . -name "*.swift" -type f | xargs grep -l "protocol KeyValueStorage\|protocol.*Storage.*save\|protocol.*Storage.*load" | head -20Length of output: 69
🏁 Script executed:
#!/bin/bash
# storage 구현 관련 파일 찾기
fd -e swift | xargs rg -l 'Keychain.*Storage\|KeychainStorage' | head -20Length of output: 0
🏁 Script executed:
#!/bin/bash
# AppDelegate에서의 동시 호출 가능성 검토
cat src/Projects/Booket/Sources/AppDelegate.swift | grep -A 20 -B 10 'didReceiveRegistrationToken'Length of output: 1062
KeyValueStorage 프로토콜과 구현을 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
# KeyValueStorage 프로토콜 정의 확인
cat src/Projects/BKData/Sources/Interface/Storage/KeyValueStorage.swiftLength of output: 256
🏁 Script executed:
#!/bin/bash
# KeychainKeyValueStorage 구현 찾기
fd -e swift -type f | xargs grep -l 'class KeychainKeyValueStorage\|struct KeychainKeyValueStorage'Length of output: 170
🏁 Script executed:
#!/bin/bash
# storage.save 및 storage.load 메서드 서명 검색
rg -n -A 5 'func save\(|func load\(' src/Projects/BKData/Sources/Interface/Storage/KeyValueStorage.swiftLength of output: 0
KeychainKeyValueStorage 구현을 찾겠습니다.
🏁 Script executed:
#!/bin/bash
# KeychainKeyValueStorage 클래스 찾기
rg -n 'class KeychainKeyValueStorage|struct KeychainKeyValueStorage' --type swiftLength of output: 127
🏁 Script executed:
#!/bin/bash
# KeyValueStorage 구현체 파일 전체 검색
find . -name "*.swift" -type f | xargs grep -l 'KeychainKeyValueStorage'Length of output: 200
🏁 Script executed:
#!/bin/bash
# KeychainKeyValueStorage 전체 구현 확인
cat src/Projects/BKStorage/Sources/Storage/KeychainKeyValueStorage.swiftLength of output: 2691
Keychain 저장소의 원자성 부족과 동시 접근 문제를 해결하세요.
검증 결과, 다음 두 가지 실제 문제가 확인되었습니다:
-
불일치 상태: 22번 줄에서 토큰 저장 후 24번 줄에서 동기화 플래그 저장이 실패하면, 새 토큰은 저장되지만 서버 동기화가 트리거되지 않습니다.
-
경쟁 상태: KeychainKeyValueStorage의 save() 메서드가 존재 여부를 확인한 후 업데이트/추가하는 패턴(check-then-act)을 사용하므로, 동시 호출 시 두 작업 모두 항목이 없다고 판단하여 SecItemAdd를 실행할 수 있습니다. Firebase 토큰 갱신 중에 이런 동시 호출이 발생 가능합니다.
해결책:
- 두 save() 호출을 하나의 원자적 작업으로 통합하거나
- KeychainKeyValueStorage에 동기화 메커니즘(lock)을 추가하세요.
🤖 Prompt for AI Agents
In src/Projects/BKStorage/Sources/TokenStorage/KeychainPushTokenStore.swift
around lines 18 to 34, the current implementation performs two separate
storage.save calls which can leave the token and sync-flag inconsistent and is
vulnerable to race conditions from check-then-act in KeychainKeyValueStorage;
fix by making the two writes atomic — either add an atomic API on
KeychainKeyValueStorage (e.g., a single saveAll/transactionalWrite that persists
both the fcmToken and isSyncNeeded flag together) and call that from this
method, or acquire a storage-level lock/serial queue around the existing save
calls so they execute as one critical section; ensure any underlying errors are
propagated and return the same publisher behavior on success/failure.
doyeonk429
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| presentGuestAuthErrorAlert() | ||
| } | ||
| case .internalServerError, .clientError: | ||
| case .internalServerError, .clientError, .unknown: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2) 추후에 unknown은 따로 처리해야할 것 같네욥! 뭔가 서버 에러라기에는 애매한 지점이 있는 거 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인정합니다
🔗 관련 이슈
📘 작업 유형
📙 작업 내역
🧪 테스트 내역
🎨 스크린샷 또는 시연 영상 (선택)
ScreenRecording_10-29-2025.23-26-42_1.mov
💬 추가 설명 or 리뷰 포인트 (선택)
Summary by CodeRabbit
새로운 기능
개선