diff --git a/WhereAreYou/WhereAreYou.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WhereAreYou/WhereAreYou.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8b0a48f..65e9db0 100644 --- a/WhereAreYou/WhereAreYou.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WhereAreYou/WhereAreYou.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fe26f57aef57400a7e879fac321c410367e7b6e5a32d724723e25088c1ff4ee2", + "originHash" : "c1c6e189988c67057039e01b7485b3d037d9e66a68a7715356b0558a08a8feae", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902", - "version" : "11.3.1" + "revision" : "97f7d74dd0e8f3d0fe5fc75cc8957aa73bdc948a", + "version" : "11.3.2" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "346daa9f46316aa372b35b317e18224acc2e9063", - "version" : "12.18.0" + "revision" : "8c29ca981990a32e626c8617544fa65d22b1834f", + "version" : "12.19.2" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", "state" : { - "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55", - "version" : "3.6.1" + "revision" : "0208e2681ec81f8a9c81084696f60fcce26cb7ee", + "version" : "3.7.0" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5", - "version" : "12.18.0" + "revision" : "b8455dd2ff55cc269fda7a4f9eac6c1e3588c53f", + "version" : "12.19.2" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3", - "version" : "8.1.2" + "revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3", + "version" : "8.1.3" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/navermaps/SPM-NMapsMap", "state" : { - "revision" : "14e1cfa1e270458fc58b0cd8827c87b3cee905ab", - "version" : "3.23.3" + "revision" : "ffee72c8d3ddac7995671d36702069b8a68c485f", + "version" : "3.24.0" } } ], diff --git a/WhereAreYou/WhereAreYou/Core/AppDelegate.swift b/WhereAreYou/WhereAreYou/Core/AppDelegate.swift index 4687732..d69433c 100644 --- a/WhereAreYou/WhereAreYou/Core/AppDelegate.swift +++ b/WhereAreYou/WhereAreYou/Core/AppDelegate.swift @@ -19,8 +19,6 @@ import AppIntents @main class AppDelegate: UIResponder, UIApplicationDelegate { - private var latestFCMToken: String? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if let clientId = Bundle.main.object(forInfoDictionaryKey: "NMFClientId") as? String { NMFAuthManager.shared().ncpKeyId = clientId @@ -82,17 +80,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { /// APNs 서버에 디바이스 등록이 성공했을 때 시스템이 호출 func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken - if let token = latestFCMToken { - saveFCMToken(token) - } + // 이미 등록된 상태에서 불러도 대리자 didReceiveRegistration이 기존 설치 ID로 다시 호출된다. + // APNs 등록은 앱 실행·로그인 때마다 일어나므로, 여기서 등록을 요청해 그때마다 설치 ID를 저장한다 + Messaging.messaging().register { _ in } } - // MARK: - FCM 토큰 저장 + // MARK: - FCM 설치 ID 저장 - private func saveFCMToken(_ token: String) { - let fcmTokenService: FCMTokenService = DIContainer.shared.resolve() + private func saveFCMInstallationID(_ installationID: String) { + let fcmInstallationIDService: FCMInstallationIDService = DIContainer.shared.resolve() Task { - try? await fcmTokenService.save(token: token) + try? await fcmInstallationIDService.save(installationID: installationID) } } @@ -117,11 +115,10 @@ extension AppDelegate: UNUserNotificationCenterDelegate { extension AppDelegate: MessagingDelegate { - /// FCM 토큰이 새로 발급되거나 갱신될 때 호출 - func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { - guard let token = fcmToken else { return } - latestFCMToken = token - saveFCMToken(token) + /// FCM 등록이 생성·갱신되거나 register(completion:)을 직접 호출했을 때 호출 — 설치 ID를 서버 발송 대상으로 저장 + func messaging(_ messaging: Messaging, didReceiveRegistration installationId: String?) { + guard let installationID = installationId else { return } + saveFCMInstallationID(installationID) } } diff --git a/WhereAreYou/WhereAreYou/Core/Coordinator/Implementation/AppCoordinator.swift b/WhereAreYou/WhereAreYou/Core/Coordinator/Implementation/AppCoordinator.swift index 8b8bd83..aa4e3f4 100644 --- a/WhereAreYou/WhereAreYou/Core/Coordinator/Implementation/AppCoordinator.swift +++ b/WhereAreYou/WhereAreYou/Core/Coordinator/Implementation/AppCoordinator.swift @@ -14,14 +14,14 @@ final class AppCoordinator: Coordinator { private let window: UIWindow private let screenFactory: ScreenFactory private let sessionValidationService: SessionValidationService - private let fcmTokenService: FCMTokenService + private let fcmInstallationIDService: FCMInstallationIDService private var tabBarCoordinator: TabBarCoordinator? init(window: UIWindow, screenFactory: ScreenFactory = .shared) { self.window = window self.screenFactory = screenFactory self.sessionValidationService = screenFactory.container.resolve() - self.fcmTokenService = screenFactory.container.resolve() + self.fcmInstallationIDService = screenFactory.container.resolve() } func start() { @@ -124,7 +124,7 @@ final class AppCoordinator: Coordinator { UIApplication.shared.registerForRemoteNotifications() } case .denied: - self?.deleteFCMToken() + self?.deleteFCMInstallationID() default: self?.requestNotificationPermission() } @@ -140,9 +140,9 @@ final class AppCoordinator: Coordinator { } } - private func deleteFCMToken() { + private func deleteFCMInstallationID() { Task { - try? await fcmTokenService.deleteForCurrentUser() + try? await fcmInstallationIDService.deleteForCurrentUser() } } diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift index 3fa81a1..8a65832 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift @@ -23,10 +23,10 @@ extension DIContainer { )) // 푸시 알림 - register(FCMTokenRepository.self, instance: FirestoreFCMTokenRepository()) - register(FCMTokenService.self, instance: DefaultFCMTokenService( + register(FCMInstallationIDRepository.self, instance: FirestoreFCMInstallationIDRepository()) + register(FCMInstallationIDService.self, instance: DefaultFCMInstallationIDService( authRepository: resolve(AuthRepository.self), - fcmTokenRepository: resolve(FCMTokenRepository.self) + fcmInstallationIDRepository: resolve(FCMInstallationIDRepository.self) )) // 세션 검증 diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory+MyPage.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory+MyPage.swift index 417cd64..ea76619 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory+MyPage.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory+MyPage.swift @@ -17,7 +17,7 @@ extension ScreenFactory { ), signOutUseCase: SignOutUseCase( authRepository: container.resolve(AuthRepository.self), - fcmTokenRepository: container.resolve(FCMTokenRepository.self) + fcmInstallationIDRepository: container.resolve(FCMInstallationIDRepository.self) ) ) return MyPageViewController(viewModel: viewModel) diff --git a/WhereAreYou/WhereAreYou/Core/Info.plist b/WhereAreYou/WhereAreYou/Core/Info.plist index 69df8bb..68dd34a 100644 --- a/WhereAreYou/WhereAreYou/Core/Info.plist +++ b/WhereAreYou/WhereAreYou/Core/Info.plist @@ -36,6 +36,8 @@ + FirebaseMessagingInstallationIdEnabled + UIBackgroundModes remote-notification diff --git a/WhereAreYou/WhereAreYou/Data/FirestoreFCMTokenRepository.swift b/WhereAreYou/WhereAreYou/Data/FirestoreFCMInstallationIDRepository.swift similarity index 57% rename from WhereAreYou/WhereAreYou/Data/FirestoreFCMTokenRepository.swift rename to WhereAreYou/WhereAreYou/Data/FirestoreFCMInstallationIDRepository.swift index a6a8bfe..6f8afbd 100644 --- a/WhereAreYou/WhereAreYou/Data/FirestoreFCMTokenRepository.swift +++ b/WhereAreYou/WhereAreYou/Data/FirestoreFCMInstallationIDRepository.swift @@ -1,5 +1,5 @@ // -// FirestoreFCMTokenRepository.swift +// FirestoreFCMInstallationIDRepository.swift // WhereAreYou // // Created by 이상유 on 2026-09-19. @@ -8,15 +8,15 @@ import Foundation import FirebaseFirestore -/// FCMTokenRepository의 Firestore 구현체 -final class FirestoreFCMTokenRepository: FCMTokenRepository { +/// FCMInstallationIDRepository의 Firestore 구현체 +final class FirestoreFCMInstallationIDRepository: FCMInstallationIDRepository { private let db = Firestore.firestore() - func save(token: String, forUserID userID: String) async throws { + func save(installationID: String, forUserID userID: String) async throws { do { try await db.collection("users").document(userID) - .updateData(["fcmToken": token]) + .updateData(["fcmInstallationID": installationID]) } catch { throw FirebaseErrorMapper.map(error) } @@ -25,7 +25,7 @@ final class FirestoreFCMTokenRepository: FCMTokenRepository { func delete(forUserID userID: String) async throws { do { try await db.collection("users").document(userID) - .updateData(["fcmToken": FieldValue.delete()]) + .updateData(["fcmInstallationID": FieldValue.delete()]) } catch { throw FirebaseErrorMapper.map(error) } diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/FCMInstallationIDRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/FCMInstallationIDRepository.swift new file mode 100644 index 0000000..f52bc73 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Repository/FCMInstallationIDRepository.swift @@ -0,0 +1,18 @@ +// +// FCMInstallationIDRepository.swift +// WhereAreYou +// +// Created by 이상유 on 2026-09-19. +// + +import Foundation + +/// FCM 설치 ID 저장소 — Firestore 유저 문서의 fcmInstallationID 필드 관리. +/// 서버는 이 값을 FCM 발송 대상(fid)으로 쓴다 +protocol FCMInstallationIDRepository { + + func save(installationID: String, forUserID: String) async throws + + func delete(forUserID: String) async throws + +} diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/FCMTokenRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/FCMTokenRepository.swift deleted file mode 100644 index 6c8d104..0000000 --- a/WhereAreYou/WhereAreYou/Domain/Repository/FCMTokenRepository.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// FCMTokenRepository.swift -// WhereAreYou -// -// Created by 이상유 on 2026-09-19. -// - -import Foundation - -/// FCM 디바이스 토큰 저장소 — Firestore 유저 문서의 fcmToken 필드 관리 -protocol FCMTokenRepository { - - func save(token: String, forUserID: String) async throws - - func delete(forUserID: String) async throws - -} diff --git a/WhereAreYou/WhereAreYou/Domain/Service/DefaultFCMInstallationIDService.swift b/WhereAreYou/WhereAreYou/Domain/Service/DefaultFCMInstallationIDService.swift new file mode 100644 index 0000000..5977fed --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Service/DefaultFCMInstallationIDService.swift @@ -0,0 +1,31 @@ +// +// DefaultFCMInstallationIDService.swift +// WhereAreYou +// +// Created by 이상유 on 2026-09-28. +// + +import Foundation + +/// FCMInstallationIDService 구현 — 인증 저장소에서 현재 유저 ID를 얻어 FCM 설치 ID 저장소에 저장·삭제한다 +final class DefaultFCMInstallationIDService: FCMInstallationIDService { + + private let authRepository: AuthRepository + private let fcmInstallationIDRepository: FCMInstallationIDRepository + + init(authRepository: AuthRepository, fcmInstallationIDRepository: FCMInstallationIDRepository) { + self.authRepository = authRepository + self.fcmInstallationIDRepository = fcmInstallationIDRepository + } + + func save(installationID: String) async throws { + guard let userID = authRepository.currentUserID else { return } + try await fcmInstallationIDRepository.save(installationID: installationID, forUserID: userID) + } + + func deleteForCurrentUser() async throws { + guard let userID = authRepository.currentUserID else { return } + try await fcmInstallationIDRepository.delete(forUserID: userID) + } + +} diff --git a/WhereAreYou/WhereAreYou/Domain/Service/DefaultFCMTokenService.swift b/WhereAreYou/WhereAreYou/Domain/Service/DefaultFCMTokenService.swift deleted file mode 100644 index ead76b8..0000000 --- a/WhereAreYou/WhereAreYou/Domain/Service/DefaultFCMTokenService.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// DefaultFCMTokenService.swift -// WhereAreYou -// -// Created by 이상유 on 2026-09-28. -// - -import Foundation - -/// FCMTokenService 구현 — 인증 저장소에서 현재 유저 ID를 얻어 FCM 토큰 저장소에 저장·삭제한다 -final class DefaultFCMTokenService: FCMTokenService { - - private let authRepository: AuthRepository - private let fcmTokenRepository: FCMTokenRepository - - init(authRepository: AuthRepository, fcmTokenRepository: FCMTokenRepository) { - self.authRepository = authRepository - self.fcmTokenRepository = fcmTokenRepository - } - - func save(token: String) async throws { - guard let userID = authRepository.currentUserID else { return } - try await fcmTokenRepository.save(token: token, forUserID: userID) - } - - func deleteForCurrentUser() async throws { - guard let userID = authRepository.currentUserID else { return } - try await fcmTokenRepository.delete(forUserID: userID) - } - -} diff --git a/WhereAreYou/WhereAreYou/Domain/Service/FCMInstallationIDService.swift b/WhereAreYou/WhereAreYou/Domain/Service/FCMInstallationIDService.swift new file mode 100644 index 0000000..a19ac1a --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Service/FCMInstallationIDService.swift @@ -0,0 +1,17 @@ +// +// FCMInstallationIDService.swift +// WhereAreYou +// +// Created by 이상유 on 2026-09-20. +// + +import Foundation + +/// FCM 설치 ID 서비스 — 현재 로그인된 유저의 FCM 설치 ID(푸시 발송 대상) 저장·삭제 +protocol FCMInstallationIDService { + + func save(installationID: String) async throws + + func deleteForCurrentUser() async throws + +} diff --git a/WhereAreYou/WhereAreYou/Domain/Service/FCMTokenService.swift b/WhereAreYou/WhereAreYou/Domain/Service/FCMTokenService.swift deleted file mode 100644 index d3fffb9..0000000 --- a/WhereAreYou/WhereAreYou/Domain/Service/FCMTokenService.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// FCMTokenService.swift -// WhereAreYou -// -// Created by 이상유 on 2026-09-20. -// - -import Foundation - -/// FCM 토큰 서비스 — 현재 로그인된 유저의 FCM 디바이스 토큰 저장·삭제 -protocol FCMTokenService { - - func save(token: String) async throws - - func deleteForCurrentUser() async throws - -} diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/SignOutUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/SignOutUseCase.swift index abe84c1..9dc223a 100644 --- a/WhereAreYou/WhereAreYou/Domain/UseCase/SignOutUseCase.swift +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/SignOutUseCase.swift @@ -7,20 +7,20 @@ import Foundation -/// 로그아웃 — FCM 토큰 삭제 후 Firebase Auth 세션 해제 +/// 로그아웃 — FCM 설치 ID 삭제 후 Firebase Auth 세션 해제 final class SignOutUseCase { private let authRepository: AuthRepository - private let fcmTokenRepository: FCMTokenRepository + private let fcmInstallationIDRepository: FCMInstallationIDRepository - init(authRepository: AuthRepository, fcmTokenRepository: FCMTokenRepository) { + init(authRepository: AuthRepository, fcmInstallationIDRepository: FCMInstallationIDRepository) { self.authRepository = authRepository - self.fcmTokenRepository = fcmTokenRepository + self.fcmInstallationIDRepository = fcmInstallationIDRepository } func execute() async throws { if let userID = authRepository.currentUserID { - try? await fcmTokenRepository.delete(forUserID: userID) + try? await fcmInstallationIDRepository.delete(forUserID: userID) } try authRepository.signOut() }