From e8bea23312ca9b6c9e622583a236ddb4a77865b4 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:54:14 +0900 Subject: [PATCH 01/11] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=EB=A5=BC=20?= =?UTF-8?q?=ED=8E=B8=ED=95=98=EA=B2=8C=20=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=201.2=20=EC=9C=BC=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Shared/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/Shared/Version.xcconfig b/Application/Shared/Version.xcconfig index 050e30a3..481a7a3a 100644 --- a/Application/Shared/Version.xcconfig +++ b/Application/Shared/Version.xcconfig @@ -1,2 +1,2 @@ -MARKETING_VERSION = 1.2.5 +MARKETING_VERSION = 1.2 IPHONEOS_DEPLOYMENT_TARGET = 17.0 From 15235b7e3969d168b6758f4a49dc748aa18b8e08 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 08:56:17 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix:=20fcmToken=EC=9D=B4=20=EC=97=86?= =?UTF-8?q?=EC=96=B4=EB=8F=84=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=84=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DTO/AuthDataResponse.swift | 4 ++-- .../Sources/Extension/FirebaseAuthUser+.swift | 2 +- .../SocialLogin/AppleAuthenticationServiceImpl.swift | 4 +--- .../GithubAuthenticationServiceImpl.swift | 3 --- .../GoogleAuthenticationServiceImpl.swift | 4 +--- .../Sources/Service/UserServiceImpl.swift | 12 ++++++++++-- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Application/DevLogData/Sources/DTO/AuthDataResponse.swift b/Application/DevLogData/Sources/DTO/AuthDataResponse.swift index 758a1ffc..3628fcff 100644 --- a/Application/DevLogData/Sources/DTO/AuthDataResponse.swift +++ b/Application/DevLogData/Sources/DTO/AuthDataResponse.swift @@ -14,7 +14,7 @@ public struct AuthDataResponse { public let email: String? public let providers: [String] public let providerID: String - public let fcmToken: String + public let fcmToken: String? public let accessToken: String? public init( @@ -23,7 +23,7 @@ public struct AuthDataResponse { email: String?, providers: [String], providerID: String, - fcmToken: String, + fcmToken: String? = nil, accessToken: String? = nil ) { self.uid = uid diff --git a/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift b/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift index af7a8e35..54c988b4 100644 --- a/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift +++ b/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift @@ -12,7 +12,7 @@ import DevLogData extension FirebaseAuth.User { func makeResponse( providerID: AuthProviderID, - fcmToken: String, + fcmToken: String? = nil, accessToken: String? = nil ) -> AuthDataResponse { return AuthDataResponse( diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift index 2a7e7270..914d1b6c 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift @@ -90,10 +90,8 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { try await result.user.link(with: appleCredential) } - let fcmToken = try await messaging.token() - logger.info("Successfully signed in with Apple") - return result.user.makeResponse(providerID: .apple, fcmToken: fcmToken) + return result.user.makeResponse(providerID: .apple) } catch { logger.error("Failed to sign in with Apple", error: error) throw error diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift index f0482b80..e3b0a847 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift @@ -72,12 +72,9 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { try await result.user.link(with: credential) } - let fcmToken = try await messaging.token() - logger.info("Successfully signed in with GitHub") return result.user.makeResponse( providerID: .gitHub, - fcmToken: fcmToken, accessToken: accessToken ) } catch { diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift index b575c04c..50d63525 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift @@ -51,10 +51,8 @@ final class GoogleAuthenticationServiceImpl: AuthenticationService { try await changeRequest.commitChanges() } - let fcmToken = try await messaging.token() - logger.info("Successfully signed in with Google") - return result.user.makeResponse(providerID: .google, fcmToken: fcmToken) + return result.user.makeResponse(providerID: .google) } catch { logger.error("Failed to sign in with Google", error: error) throw error diff --git a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift index cdedbb61..f2a1b508 100644 --- a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift @@ -56,7 +56,11 @@ final class UserServiceImpl: UserService { userField["createdAt"] = FieldValue.serverTimestamp() } - var settingField = ["fcmToken": response.fcmToken] + var settingField: [String: Any] = [:] + + if let fcmToken = response.fcmToken { + settingField["fcmToken"] = fcmToken + } // 깃헙 로그인 시 추가 정보 저장 if response.providerID == "github.com", let accessToken = response.accessToken { @@ -73,7 +77,11 @@ final class UserServiceImpl: UserService { merge: true ) async let infoUpdate: Void = infoRef.setData(userFieldSnapshot, merge: true) - async let tokensUpdate: Void = tokensRef.setData(settingFieldSnapshot, merge: true) + async let tokensUpdate: Void? = { + guard !settingFieldSnapshot.isEmpty else { return nil } + try await tokensRef.setData(settingFieldSnapshot, merge: true) + return nil + }() let settingsDocument = try await settingsRef.getDocument() var settingsField: [String: Any] = [ From 99da5e88f5488d3a0b4903bb7ed6e3a14fd25309 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:16:26 +0900 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20AppDelegate=EC=97=90=EC=84=9C=20FC?= =?UTF-8?q?M=20=EB=8F=99=EA=B8=B0=ED=99=94=20=ED=8A=B8=EB=A6=AC=EA=B1=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=84=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/App/Delegate/AppDelegate.swift | 31 ++++++++++++++++--- .../App/Notification/NotificationName+.swift | 3 ++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift index c65a68d2..b0a44f43 100644 --- a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift +++ b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift @@ -32,6 +32,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ = container.resolve(UserTimeZoneSyncHandler.self) _ = container.resolve(WidgetSyncEventHandler.self) _ = container.resolve(WidgetSessionSyncHandler.self) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleSceneDidActivate), + name: UIScene.didActivateNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleRemoteNotificationRegistrationRequest), + name: .didRequestRemoteNotificationRegistration, + object: nil + ) // 알림 권한 요청 UNUserNotificationCenter.current().delegate = self @@ -40,9 +52,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.logger.error("Notification authorization error", error: error) } else { self.logger.info("Notification permission granted: \(granted)") - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } + NotificationCenter.default.post(name: .didRequestFCMTokenSync, object: nil) } } @@ -63,13 +73,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + @objc private func handleSceneDidActivate() { + NotificationCenter.default.post(name: .didRequestFCMTokenSync, object: nil) + } + + @MainActor + @objc private func handleRemoteNotificationRegistrationRequest() { + UIApplication.shared.registerForRemoteNotifications() + } + // APNs 등록 성공 func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { logger.info("APNs token: \(deviceToken.map { String(format: "%02.2hhx", $0) }.joined())") - container.resolve(PushMessagingService.self).setAPNSToken(deviceToken) + NotificationCenter.default.post( + name: .didReceiveAPNSToken, + object: nil, + userInfo: ["deviceToken": deviceToken] + ) } // APNs 등록 실패 diff --git a/Application/DevLogApp/Sources/App/Notification/NotificationName+.swift b/Application/DevLogApp/Sources/App/Notification/NotificationName+.swift index 41edfbc6..84aaef68 100644 --- a/Application/DevLogApp/Sources/App/Notification/NotificationName+.swift +++ b/Application/DevLogApp/Sources/App/Notification/NotificationName+.swift @@ -9,5 +9,8 @@ import Foundation extension Notification.Name { static let didRefreshFCMToken = Notification.Name("didRefreshFCMToken") + static let didReceiveAPNSToken = Notification.Name("didReceiveAPNSToken") + static let didRequestFCMTokenSync = Notification.Name("didRequestFCMTokenSync") + static let didRequestRemoteNotificationRegistration = Notification.Name("didRequestRemoteNotificationRegistration") static let didRequestUserTimeZoneSync = Notification.Name("didRequestUserTimeZoneSync") } From 892a13bdee2319f857e5722db6a389ff7798e2a1 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:16:46 +0900 Subject: [PATCH 04/11] =?UTF-8?q?fix:=20FCMTokenSyncHandler=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=86=A0=ED=81=B0=20backfill=20=ED=9D=90=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Assembler/AppLayerAssembler.swift | 1 + .../App/Handler/FCMTokenSyncHandler.swift | 69 +++++++++++++++++-- .../Protocol/PushMessagingService.swift | 2 + .../Service/PushMessagingServiceImpl.swift | 32 +++++++++ 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift b/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift index 8dbc0bee..cd2ecbc2 100644 --- a/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift +++ b/Application/DevLogApp/Sources/App/Assembler/AppLayerAssembler.swift @@ -12,6 +12,7 @@ final class AppLayerAssembler: Assembler { func assemble(_ container: any DIContainer) { container.register(FCMTokenSyncHandler.self) { FCMTokenSyncHandler( + messagingService: container.resolve(PushMessagingService.self), userService: container.resolve(UserService.self) ) } diff --git a/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift b/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift index b631c8a1..d9d5fef2 100644 --- a/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift +++ b/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift @@ -11,27 +11,82 @@ import DevLogData import Foundation final class FCMTokenSyncHandler { + private let messagingService: PushMessagingService private let userService: UserService + private let notificationCenter: NotificationCenter private let logger = Logger(category: "FCMTokenSyncHandler") private var cancellables = Set() init( + messagingService: PushMessagingService, userService: UserService, notificationCenter: NotificationCenter = .default ) { + self.messagingService = messagingService self.userService = userService + self.notificationCenter = notificationCenter notificationCenter.publisher(for: .didRefreshFCMToken) .compactMap { $0.userInfo?["fcmToken"] as? String } .sink { [weak self] fcmToken in - Task { - do { - try await self?.userService.updateFCMToken(fcmToken) - } catch { - self?.logger.error("Failed to sync refreshed FCM token", error: error) - } - } + self?.syncFCMToken(fcmToken) + } + .store(in: &cancellables) + + notificationCenter.publisher(for: .didRequestFCMTokenSync) + .sink { [weak self] _ in + self?.requestFCMTokenSync() } .store(in: &cancellables) + + notificationCenter.publisher(for: .didReceiveAPNSToken) + .compactMap { $0.userInfo?["deviceToken"] as? Data } + .sink { [weak self] deviceToken in + self?.handleAPNSToken(deviceToken) + } + .store(in: &cancellables) + } +} + +private extension FCMTokenSyncHandler { + func requestFCMTokenSync() { + Task { [weak self] in + guard let self else { return } + guard await messagingService.isNotificationAuthorized() else { + return + } + notificationCenter.post(name: .didRequestRemoteNotificationRegistration, object: nil) + syncCurrentFCMToken() + } + } + + func handleAPNSToken(_ deviceToken: Data) { + messagingService.setAPNSToken(deviceToken) + syncCurrentFCMToken() + } + + func syncCurrentFCMToken() { + Task { [weak self] in + guard let self else { return } + + do { + guard let fcmToken = try await messagingService.fetchFCMToken() else { + return + } + try await userService.updateFCMToken(fcmToken) + } catch { + logger.error("Failed to sync current FCM token", error: error) + } + } + } + + func syncFCMToken(_ fcmToken: String) { + Task { [weak self] in + do { + try await self?.userService.updateFCMToken(fcmToken) + } catch { + self?.logger.error("Failed to sync refreshed FCM token", error: error) + } + } } } diff --git a/Application/DevLogData/Sources/Protocol/PushMessagingService.swift b/Application/DevLogData/Sources/Protocol/PushMessagingService.swift index d5f61f24..cdb9c0e0 100644 --- a/Application/DevLogData/Sources/Protocol/PushMessagingService.swift +++ b/Application/DevLogData/Sources/Protocol/PushMessagingService.swift @@ -10,6 +10,8 @@ import Foundation public protocol PushMessagingService: AnyObject { func setDelegate(_ delegate: PushMessagingServiceDelegate?) func setAPNSToken(_ deviceToken: Data) + func isNotificationAuthorized() async -> Bool + func fetchFCMToken() async throws -> String? } public protocol PushMessagingServiceDelegate: AnyObject { diff --git a/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift b/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift index 44a29d7d..0c1ff320 100644 --- a/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift @@ -8,6 +8,7 @@ import Foundation import DevLogData import FirebaseMessaging +import UserNotifications final class PushMessagingServiceImpl: NSObject, PushMessagingService { private weak var delegate: PushMessagingServiceDelegate? @@ -20,6 +21,30 @@ final class PushMessagingServiceImpl: NSObject, PushMessagingService { func setAPNSToken(_ deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken } + + func isNotificationAuthorized() async -> Bool { + let settings = await UNUserNotificationCenter.current().notificationSettings() + + switch settings.authorizationStatus { + case .authorized, .provisional, .ephemeral: + return true + case .denied, .notDetermined: + return false + @unknown default: + return false + } + } + + func fetchFCMToken() async throws -> String? { + do { + return try await Messaging.messaging().token() + } catch { + if error.isMissingAPNSTokenForFCMToken { + return nil + } + throw error + } + } } extension PushMessagingServiceImpl: MessagingDelegate { @@ -27,3 +52,10 @@ extension PushMessagingServiceImpl: MessagingDelegate { delegate?.pushMessagingService(self, didReceiveRegistrationToken: fcmToken) } } + +private extension Error { + var isMissingAPNSTokenForFCMToken: Bool { + let nsError = self as NSError + return nsError.domain == "com.google.fcm" && nsError.code == 505 + } +} From 99bbe58e8d8de5de48e65fe473c9d6f5e72e92a1 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:17:05 +0900 Subject: [PATCH 05/11] =?UTF-8?q?chore:=20FCMTokenSyncHandler=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FCMTokenSyncHandlerTests.swift | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 Application/DevLogApp/Tests/PushNotification/FCMTokenSyncHandlerTests.swift diff --git a/Application/DevLogApp/Tests/PushNotification/FCMTokenSyncHandlerTests.swift b/Application/DevLogApp/Tests/PushNotification/FCMTokenSyncHandlerTests.swift new file mode 100644 index 00000000..67be8fe6 --- /dev/null +++ b/Application/DevLogApp/Tests/PushNotification/FCMTokenSyncHandlerTests.swift @@ -0,0 +1,176 @@ +// +// FCMTokenSyncHandlerTests.swift +// DevLogAppTests +// +// Created by opfic on 6/13/26. +// + +import Foundation +import Testing +import DevLogData +@testable import DevLogApp + +struct FCMTokenSyncHandlerTests { + @Test("현재 FCM token 동기화 요청 시 token이 있으면 저장한다") + func 현재_FCM_token_동기화_요청_시_token이_있으면_저장한다() async throws { + let notificationCenter = NotificationCenter() + let registrationObserver = NotificationObserver( + notificationCenter: notificationCenter, + name: .didRequestRemoteNotificationRegistration + ) + let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token") + let userService = UserServiceSpy() + let handler = FCMTokenSyncHandler( + messagingService: messagingService, + userService: userService, + notificationCenter: notificationCenter + ) + + notificationCenter.post(name: .didRequestFCMTokenSync, object: nil) + + try await waitUntil { + await userService.updatedFCMTokens == ["current-token"] + } + #expect(registrationObserver.didReceiveNotification) + _ = handler + } + + @Test("현재 FCM token 동기화 요청 시 token이 없으면 저장하지 않는다") + func 현재_FCM_token_동기화_요청_시_token이_없으면_저장하지_않는다() async throws { + let notificationCenter = NotificationCenter() + let messagingService = PushMessagingServiceSpy(currentFCMToken: nil) + let userService = UserServiceSpy() + let handler = FCMTokenSyncHandler( + messagingService: messagingService, + userService: userService, + notificationCenter: notificationCenter + ) + + notificationCenter.post(name: .didRequestFCMTokenSync, object: nil) + + try await Task.sleep(for: .milliseconds(100)) + #expect(await userService.updatedFCMTokens.isEmpty) + _ = handler + } + + @Test("갱신된 FCM token 이벤트 수신 시 저장한다") + func 갱신된_FCM_token_이벤트_수신_시_저장한다() async throws { + let notificationCenter = NotificationCenter() + let messagingService = PushMessagingServiceSpy(currentFCMToken: nil) + let userService = UserServiceSpy() + let handler = FCMTokenSyncHandler( + messagingService: messagingService, + userService: userService, + notificationCenter: notificationCenter + ) + + notificationCenter.post( + name: .didRefreshFCMToken, + object: nil, + userInfo: ["fcmToken": "refreshed-token"] + ) + + try await waitUntil { + await userService.updatedFCMTokens == ["refreshed-token"] + } + _ = handler + } + + @Test("APNs token 이벤트 수신 시 APNs token을 적용하고 현재 FCM token을 저장한다") + func APNs_token_이벤트_수신_시_APNs_token을_적용하고_현재_FCM_token을_저장한다() async throws { + let notificationCenter = NotificationCenter() + let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token") + let userService = UserServiceSpy() + let handler = FCMTokenSyncHandler( + messagingService: messagingService, + userService: userService, + notificationCenter: notificationCenter + ) + let deviceToken = Data([0x01, 0x02, 0x03]) + + notificationCenter.post( + name: .didReceiveAPNSToken, + object: nil, + userInfo: ["deviceToken": deviceToken] + ) + + try await waitUntil { + await userService.updatedFCMTokens == ["current-token"] + } + #expect(messagingService.apnsTokens == [deviceToken]) + _ = handler + } +} + +private actor UserServiceSpy: UserService { + private(set) var updatedFCMTokens = [String]() + + func upsertUser(_ response: AuthDataResponse) async throws { } + func fetchUserProfile() async throws -> UserProfileResponse { fatalError() } + func upsertStatusMessage(_ message: String) async throws { } + + func updateFCMToken(_ fcmToken: String) async throws { + updatedFCMTokens.append(fcmToken) + } + + func updateUserTimeZone() async throws { } +} + +private final class PushMessagingServiceSpy: PushMessagingService { + private let currentFCMToken: String? + private(set) var apnsTokens = [Data]() + + init(currentFCMToken: String?) { + self.currentFCMToken = currentFCMToken + } + + func setDelegate(_ delegate: PushMessagingServiceDelegate?) { } + func setAPNSToken(_ deviceToken: Data) { + apnsTokens.append(deviceToken) + } + func isNotificationAuthorized() async -> Bool { true } + + func fetchFCMToken() async throws -> String? { + currentFCMToken + } +} + +private final class NotificationObserver { + private(set) var didReceiveNotification = false + private var token: NSObjectProtocol? + private let notificationCenter: NotificationCenter + + init(notificationCenter: NotificationCenter, name: Notification.Name) { + self.notificationCenter = notificationCenter + self.token = notificationCenter.addObserver( + forName: name, + object: nil, + queue: nil + ) { [weak self] _ in + self?.didReceiveNotification = true + } + } + + deinit { + if let token { + notificationCenter.removeObserver(token) + } + } +} + +private func waitUntil( + timeout: Duration = .seconds(1), + pollInterval: Duration = .milliseconds(10), + condition: @escaping @Sendable () async -> Bool +) async throws { + let deadline = ContinuousClock.now + timeout + + while ContinuousClock.now < deadline { + if await condition() { + return + } + try await Task.sleep(for: pollInterval) + } + + Issue.record("조건을 만족하지 못함") +} From 2bdb8c5c5bf06fccf0899ab67d1ec29b75926324 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:49:26 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift b/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift index 54c988b4..e3e7951a 100644 --- a/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift +++ b/Application/DevLogInfra/Sources/Extension/FirebaseAuthUser+.swift @@ -12,7 +12,6 @@ import DevLogData extension FirebaseAuth.User { func makeResponse( providerID: AuthProviderID, - fcmToken: String? = nil, accessToken: String? = nil ) -> AuthDataResponse { return AuthDataResponse( @@ -21,7 +20,6 @@ extension FirebaseAuth.User { email: self.email, providers: self.providerData.map { $0.providerID }, providerID: providerID.rawValue, - fcmToken: fcmToken, accessToken: accessToken ) } From 1bfbee3bdb8f682ce0684723fb31bfd0c9a7c01b Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:51:53 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20MainActor=20=EB=82=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift index b0a44f43..befab07d 100644 --- a/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift +++ b/Application/DevLogApp/Sources/App/Delegate/AppDelegate.swift @@ -77,9 +77,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { NotificationCenter.default.post(name: .didRequestFCMTokenSync, object: nil) } - @MainActor @objc private func handleRemoteNotificationRegistrationRequest() { - UIApplication.shared.registerForRemoteNotifications() + Task { @MainActor in + UIApplication.shared.registerForRemoteNotifications() + } } // APNs 등록 성공 From f5c77a646c5d904479591fd4103e7aff7f7e7302 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:57:44 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor:=20FCMTokenSyncHandler=EC=9D=98?= =?UTF-8?q?=20=EC=A4=91=EC=B2=A9=20Task=EB=A5=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Handler/FCMTokenSyncHandler.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift b/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift index d9d5fef2..557bb11d 100644 --- a/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift +++ b/Application/DevLogApp/Sources/App/Handler/FCMTokenSyncHandler.swift @@ -56,27 +56,25 @@ private extension FCMTokenSyncHandler { return } notificationCenter.post(name: .didRequestRemoteNotificationRegistration, object: nil) - syncCurrentFCMToken() + await syncCurrentFCMToken() } } func handleAPNSToken(_ deviceToken: Data) { messagingService.setAPNSToken(deviceToken) - syncCurrentFCMToken() - } - - func syncCurrentFCMToken() { Task { [weak self] in - guard let self else { return } + await self?.syncCurrentFCMToken() + } + } - do { - guard let fcmToken = try await messagingService.fetchFCMToken() else { - return - } - try await userService.updateFCMToken(fcmToken) - } catch { - logger.error("Failed to sync current FCM token", error: error) + func syncCurrentFCMToken() async { + do { + guard let fcmToken = try await messagingService.fetchFCMToken() else { + return } + try await userService.updateFCMToken(fcmToken) + } catch { + logger.error("Failed to sync current FCM token", error: error) } } From 8e0f25dce77793d851bb8eb503761c30f94d4a5b Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:58:26 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20nil=20=EB=B0=98=ED=99=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogInfra/Sources/Service/UserServiceImpl.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift index f2a1b508..eefdceca 100644 --- a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift @@ -77,10 +77,9 @@ final class UserServiceImpl: UserService { merge: true ) async let infoUpdate: Void = infoRef.setData(userFieldSnapshot, merge: true) - async let tokensUpdate: Void? = { - guard !settingFieldSnapshot.isEmpty else { return nil } + async let tokensUpdate: Void = { + guard !settingFieldSnapshot.isEmpty else { return } try await tokensRef.setData(settingFieldSnapshot, merge: true) - return nil }() let settingsDocument = try await settingsRef.getDocument() From 3707300731d0fd942eab5117c3fd9cc55f95bf65 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:45:18 +0900 Subject: [PATCH 10/11] =?UTF-8?q?chore:=201.2.5=20=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Shared/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/Shared/Version.xcconfig b/Application/Shared/Version.xcconfig index 481a7a3a..050e30a3 100644 --- a/Application/Shared/Version.xcconfig +++ b/Application/Shared/Version.xcconfig @@ -1,2 +1,2 @@ -MARKETING_VERSION = 1.2 +MARKETING_VERSION = 1.2.5 IPHONEOS_DEPLOYMENT_TARGET = 17.0 From 0e3a8a8c7adfb6738270f41b8531269178db5ebf Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:38:32 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore:=20=EB=A7=88=EC=9D=B4=EB=84=88=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20+=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Shared/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/Shared/Version.xcconfig b/Application/Shared/Version.xcconfig index 050e30a3..894ea785 100644 --- a/Application/Shared/Version.xcconfig +++ b/Application/Shared/Version.xcconfig @@ -1,2 +1,2 @@ -MARKETING_VERSION = 1.2.5 +MARKETING_VERSION = 1.2.6 IPHONEOS_DEPLOYMENT_TARGET = 17.0