From da1095bfe12f3c79a10470bcf6d380faa221f81f Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 19 Mar 2026 23:11:43 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20NotificationCenter=EC=9D=84=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20FCM=20=ED=86=A0=ED=81=B0=EC=9D=B4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EB=90=9C=20=EA=B2=83=EC=9D=84=20=EC=A0=84?= =?UTF-8?q?=ED=8C=8C=ED=95=98=EA=B3=A0,=20=EC=A0=84=ED=8C=8C=EB=90=9C=20?= =?UTF-8?q?=EA=B2=83=EC=9D=84=20=EB=B0=9B=EB=8A=94=20=EA=B2=83=EC=9D=84=20?= =?UTF-8?q?resolve=20=ED=95=98=EC=97=AC=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=ED=95=98=EB=93=9C=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/AppLayerAssembler.swift | 16 +++++++++ DevLog/App/Assembler/Assembler.swift | 3 +- DevLog/App/Delegate/AppDelegate.swift | 6 ++++ DevLog/App/FCMTokenSyncHandler.swift | 35 +++++++++++++++++++ .../App/Notification/NotificationName+.swift | 12 +++++++ .../Repository/UserDataRepositoryImpl.swift | 6 +++- .../Domain/Protocol/UserDataRepository.swift | 1 + DevLog/Infra/Service/UserService.swift | 7 +++- 8 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 DevLog/App/Assembler/AppLayerAssembler.swift create mode 100644 DevLog/App/FCMTokenSyncHandler.swift create mode 100644 DevLog/App/Notification/NotificationName+.swift diff --git a/DevLog/App/Assembler/AppLayerAssembler.swift b/DevLog/App/Assembler/AppLayerAssembler.swift new file mode 100644 index 00000000..5ac892fa --- /dev/null +++ b/DevLog/App/Assembler/AppLayerAssembler.swift @@ -0,0 +1,16 @@ +// +// AppLayerAssembler.swift +// DevLog +// +// Created by opfic on 3/19/26. +// + +final class AppLayerAssembler: Assembler { + func assemble(_ container: any DIContainer) { + container.register(FCMTokenSyncHandler.self) { + FCMTokenSyncHandler( + userDataRepository: container.resolve(UserDataRepository.self) + ) + } + } +} diff --git a/DevLog/App/Assembler/Assembler.swift b/DevLog/App/Assembler/Assembler.swift index 5c65b405..ca5f99fd 100644 --- a/DevLog/App/Assembler/Assembler.swift +++ b/DevLog/App/Assembler/Assembler.swift @@ -14,7 +14,8 @@ final class AppAssembler: Assembler { PersistenceAssembler(), InfraAssembler(), DataAssembler(), - DomainAssembler() + DomainAssembler(), + AppLayerAssembler() ] func assemble(_ container: any DIContainer) { diff --git a/DevLog/App/Delegate/AppDelegate.swift b/DevLog/App/Delegate/AppDelegate.swift index 7a4e0339..b863ef0c 100644 --- a/DevLog/App/Delegate/AppDelegate.swift +++ b/DevLog/App/Delegate/AppDelegate.swift @@ -26,6 +26,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { FirebaseApp.configure() + _ = AppDIContainer.shared.resolve(FCMTokenSyncHandler.self) // 알림 권한 요청 UNUserNotificationCenter.current().delegate = self @@ -78,6 +79,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { ) { if let fcmToken = fcmToken { logger.info("FCM token: \(fcmToken)") + NotificationCenter.default.post( + name: .didRefreshFCMToken, + object: nil, + userInfo: ["fcmToken": fcmToken] + ) } } } diff --git a/DevLog/App/FCMTokenSyncHandler.swift b/DevLog/App/FCMTokenSyncHandler.swift new file mode 100644 index 00000000..6d07215b --- /dev/null +++ b/DevLog/App/FCMTokenSyncHandler.swift @@ -0,0 +1,35 @@ +// +// FCMTokenSyncHandler.swift +// DevLog +// +// Created by opfic on 3/19/26. +// + +import Combine +import Foundation + +final class FCMTokenSyncHandler { + private let userDataRepository: UserDataRepository + private let logger = Logger(category: "FCMTokenSyncHandler") + private var cancellables = Set() + + init( + userDataRepository: UserDataRepository, + notificationCenter: NotificationCenter = .default + ) { + self.userDataRepository = userDataRepository + + notificationCenter.publisher(for: .didRefreshFCMToken) + .compactMap { $0.userInfo?["fcmToken"] as? String } + .sink { [weak self] fcmToken in + Task { + do { + try await self?.userDataRepository.updateFCMToken(fcmToken) + } catch { + self?.logger.error("Failed to sync refreshed FCM token", error: error) + } + } + } + .store(in: &cancellables) + } +} diff --git a/DevLog/App/Notification/NotificationName+.swift b/DevLog/App/Notification/NotificationName+.swift new file mode 100644 index 00000000..31e40921 --- /dev/null +++ b/DevLog/App/Notification/NotificationName+.swift @@ -0,0 +1,12 @@ +// +// NotificationName+.swift +// DevLog +// +// Created by opfic on 3/19/26. +// + +import Foundation + +extension Notification.Name { + static let didRefreshFCMToken = Notification.Name("didRefreshFCMToken") +} diff --git a/DevLog/Data/Repository/UserDataRepositoryImpl.swift b/DevLog/Data/Repository/UserDataRepositoryImpl.swift index 0004d7e7..4269ce68 100644 --- a/DevLog/Data/Repository/UserDataRepositoryImpl.swift +++ b/DevLog/Data/Repository/UserDataRepositoryImpl.swift @@ -19,6 +19,10 @@ final class UserDataRepositoryImpl: UserDataRepository { } func upsertStatusMessage(_ message: String) async throws { - try await self.userService.upsertStatusMessage(message) + try await userService.upsertStatusMessage(message) + } + + func updateFCMToken(_ fcmToken: String) async throws { + try await userService.updateFCMToken(fcmToken) } } diff --git a/DevLog/Domain/Protocol/UserDataRepository.swift b/DevLog/Domain/Protocol/UserDataRepository.swift index 020520df..5ad5c600 100644 --- a/DevLog/Domain/Protocol/UserDataRepository.swift +++ b/DevLog/Domain/Protocol/UserDataRepository.swift @@ -8,4 +8,5 @@ protocol UserDataRepository { func fetch() async throws -> UserProfile func upsertStatusMessage(_ message: String) async throws + func updateFCMToken(_ fcmToken: String) async throws } diff --git a/DevLog/Infra/Service/UserService.swift b/DevLog/Infra/Service/UserService.swift index d62134b2..c4e73a96 100644 --- a/DevLog/Infra/Service/UserService.swift +++ b/DevLog/Infra/Service/UserService.swift @@ -145,7 +145,12 @@ final class UserService { } } - func updateFCMToken(_ userId: String, fcmToken: String) async throws { + func updateFCMToken(_ fcmToken: String) async throws { + guard let userId = Auth.auth().currentUser?.uid else { + logger.info("Skipping FCM token update because no authenticated user exists") + return + } + logger.info("Updating FCM token for user: \(userId)") do { From d2a13c10cf2d7d0c48c73b0be0544a15d495f58c Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 19 Mar 2026 23:25:06 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EC=8B=9C=EA=B0=84=EB=8C=80?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=AD=EC=8B=9C=20=EB=8F=99=EC=9D=BC=ED=95=9C=20?= =?UTF-8?q?=ED=98=95=ED=83=9C=EB=A1=9C=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/AppLayerAssembler.swift | 7 +++- DevLog/App/Delegate/AppDelegate.swift | 24 +++---------- .../{ => Handler}/FCMTokenSyncHandler.swift | 8 ++--- .../App/Handler/UserTimeZoneSyncHandler.swift | 34 +++++++++++++++++++ .../App/Notification/NotificationName+.swift | 1 + .../Repository/UserDataRepositoryImpl.swift | 4 +++ .../Domain/Protocol/UserDataRepository.swift | 1 + DevLog/Infra/Service/UserService.swift | 21 ++++++++++++ 8 files changed, 76 insertions(+), 24 deletions(-) rename DevLog/App/{ => Handler}/FCMTokenSyncHandler.swift (76%) create mode 100644 DevLog/App/Handler/UserTimeZoneSyncHandler.swift diff --git a/DevLog/App/Assembler/AppLayerAssembler.swift b/DevLog/App/Assembler/AppLayerAssembler.swift index 5ac892fa..8533e68d 100644 --- a/DevLog/App/Assembler/AppLayerAssembler.swift +++ b/DevLog/App/Assembler/AppLayerAssembler.swift @@ -9,7 +9,12 @@ final class AppLayerAssembler: Assembler { func assemble(_ container: any DIContainer) { container.register(FCMTokenSyncHandler.self) { FCMTokenSyncHandler( - userDataRepository: container.resolve(UserDataRepository.self) + repository: container.resolve(UserDataRepository.self) + ) + } + container.register(UserTimeZoneSyncHandler.self) { + UserTimeZoneSyncHandler( + repository: container.resolve(UserDataRepository.self) ) } } diff --git a/DevLog/App/Delegate/AppDelegate.swift b/DevLog/App/Delegate/AppDelegate.swift index b863ef0c..b2cdb7fa 100644 --- a/DevLog/App/Delegate/AppDelegate.swift +++ b/DevLog/App/Delegate/AppDelegate.swift @@ -7,11 +7,11 @@ import UIKit import Firebase -import FirebaseAuth import GoogleSignIn class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { private let logger = Logger(category: "AppDelegate") + private let container = AppDIContainer.shared func application( _ app: UIApplication, @@ -26,8 +26,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { FirebaseApp.configure() - _ = AppDIContainer.shared.resolve(FCMTokenSyncHandler.self) - + _ = container.resolve(FCMTokenSyncHandler.self) + _ = container.resolve(UserTimeZoneSyncHandler.self) + // 알림 권한 요청 UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in @@ -42,7 +43,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { } // 앱이 온그라운드로 되었을 때, 로그인 세션이 존재한다면 현재 유저의 timeZone 저장 - updateUserTimeZone() + NotificationCenter.default.post(name: .didRequestUserTimeZoneSync, object: nil) // Firebase Messaging 설정 Messaging.messaging().delegate = self @@ -88,21 +89,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { } } -private extension AppDelegate { - func updateUserTimeZone() { - Task { - do { - guard let uid = Auth.auth().currentUser?.uid else { return } - let settingsRef = Firestore.firestore().document("users/\(uid)/userData/settings") - - try await settingsRef.setData(["timeZone": TimeZone.autoupdatingCurrent.identifier], merge: true) - } catch { - logger.error("Failed to update timeZone", error: error) - } - } - } -} - extension AppDelegate: UNUserNotificationCenterDelegate { // 앱이 포그라운드에 있을 때 알림 표시 func userNotificationCenter( diff --git a/DevLog/App/FCMTokenSyncHandler.swift b/DevLog/App/Handler/FCMTokenSyncHandler.swift similarity index 76% rename from DevLog/App/FCMTokenSyncHandler.swift rename to DevLog/App/Handler/FCMTokenSyncHandler.swift index 6d07215b..c3218c5c 100644 --- a/DevLog/App/FCMTokenSyncHandler.swift +++ b/DevLog/App/Handler/FCMTokenSyncHandler.swift @@ -9,22 +9,22 @@ import Combine import Foundation final class FCMTokenSyncHandler { - private let userDataRepository: UserDataRepository + private let repository: UserDataRepository private let logger = Logger(category: "FCMTokenSyncHandler") private var cancellables = Set() init( - userDataRepository: UserDataRepository, + repository: UserDataRepository, notificationCenter: NotificationCenter = .default ) { - self.userDataRepository = userDataRepository + self.repository = repository notificationCenter.publisher(for: .didRefreshFCMToken) .compactMap { $0.userInfo?["fcmToken"] as? String } .sink { [weak self] fcmToken in Task { do { - try await self?.userDataRepository.updateFCMToken(fcmToken) + try await self?.repository.updateFCMToken(fcmToken) } catch { self?.logger.error("Failed to sync refreshed FCM token", error: error) } diff --git a/DevLog/App/Handler/UserTimeZoneSyncHandler.swift b/DevLog/App/Handler/UserTimeZoneSyncHandler.swift new file mode 100644 index 00000000..f047f812 --- /dev/null +++ b/DevLog/App/Handler/UserTimeZoneSyncHandler.swift @@ -0,0 +1,34 @@ +// +// UserTimeZoneSyncHandler.swift +// DevLog +// +// Created by opfic on 3/19/26. +// + +import Combine +import Foundation + +final class UserTimeZoneSyncHandler { + private let repository: UserDataRepository + private let logger = Logger(category: "UserTimeZoneSyncHandler") + private var cancellables = Set() + + init( + repository: UserDataRepository, + notificationCenter: NotificationCenter = .default + ) { + self.repository = repository + + notificationCenter.publisher(for: .didRequestUserTimeZoneSync) + .sink { [weak self] _ in + Task { + do { + try await self?.repository.updateUserTimeZone() + } catch { + self?.logger.error("Failed to sync user timeZone", error: error) + } + } + } + .store(in: &cancellables) + } +} diff --git a/DevLog/App/Notification/NotificationName+.swift b/DevLog/App/Notification/NotificationName+.swift index 31e40921..41edfbc6 100644 --- a/DevLog/App/Notification/NotificationName+.swift +++ b/DevLog/App/Notification/NotificationName+.swift @@ -9,4 +9,5 @@ import Foundation extension Notification.Name { static let didRefreshFCMToken = Notification.Name("didRefreshFCMToken") + static let didRequestUserTimeZoneSync = Notification.Name("didRequestUserTimeZoneSync") } diff --git a/DevLog/Data/Repository/UserDataRepositoryImpl.swift b/DevLog/Data/Repository/UserDataRepositoryImpl.swift index 4269ce68..baf73654 100644 --- a/DevLog/Data/Repository/UserDataRepositoryImpl.swift +++ b/DevLog/Data/Repository/UserDataRepositoryImpl.swift @@ -25,4 +25,8 @@ final class UserDataRepositoryImpl: UserDataRepository { func updateFCMToken(_ fcmToken: String) async throws { try await userService.updateFCMToken(fcmToken) } + + func updateUserTimeZone() async throws { + try await userService.updateUserTimeZone() + } } diff --git a/DevLog/Domain/Protocol/UserDataRepository.swift b/DevLog/Domain/Protocol/UserDataRepository.swift index 5ad5c600..f0612a4a 100644 --- a/DevLog/Domain/Protocol/UserDataRepository.swift +++ b/DevLog/Domain/Protocol/UserDataRepository.swift @@ -9,4 +9,5 @@ protocol UserDataRepository { func fetch() async throws -> UserProfile func upsertStatusMessage(_ message: String) async throws func updateFCMToken(_ fcmToken: String) async throws + func updateUserTimeZone() async throws } diff --git a/DevLog/Infra/Service/UserService.swift b/DevLog/Infra/Service/UserService.swift index c4e73a96..0409310b 100644 --- a/DevLog/Infra/Service/UserService.swift +++ b/DevLog/Infra/Service/UserService.swift @@ -162,4 +162,25 @@ final class UserService { throw error } } + + func updateUserTimeZone() async throws { + guard let userId = Auth.auth().currentUser?.uid else { + logger.info("Skipping timeZone update because no authenticated user exists") + return + } + + logger.info("Updating timeZone for user: \(userId)") + + do { + let settingsRef = store.document("users/\(userId)/userData/settings") + try await settingsRef.setData( + ["timeZone": TimeZone.autoupdatingCurrent.identifier], + merge: true + ) + logger.info("Successfully updated timeZone") + } catch { + logger.error("Failed to update timeZone", error: error) + throw error + } + } } From 897b7f3e036c7d6b95adb5a5ed0ee3783f966ace Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 19 Mar 2026 23:29:58 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20qa-*=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EC=B0=BE=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b768b19..b0e3ad3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,36 +16,7 @@ permissions: checks: write jobs: - detect_qa_tag: - runs-on: macos-latest - outputs: - has_qa_tag: ${{ steps.detect.outputs.has_qa_tag }} - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Detect QA Tag on PR Head - id: detect - shell: bash - run: | - set -euo pipefail - PR_HEAD_SHA="${{ github.event.pull_request.head.sha }}" - MATCHED_TAG="$(git tag --points-at "$PR_HEAD_SHA" | grep -E '^qa(-local)?-' | head -n 1 || true)" - - if [ -n "$MATCHED_TAG" ]; then - echo "Found QA tag on PR head: $MATCHED_TAG" - echo "has_qa_tag=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "No QA tag found on PR head" - echo "has_qa_tag=false" >> "$GITHUB_OUTPUT" - build: - needs: detect_qa_tag - if: needs.detect_qa_tag.outputs.has_qa_tag != 'true' runs-on: macos-latest timeout-minutes: 30 steps: From dbdda5678ed30c50c726110227775dca65a59ae2 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 19 Mar 2026 23:44:25 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EC=95=B1=EC=9D=B4=20=EC=BC=9C?= =?UTF-8?q?=EC=A7=84=20=EC=9D=B4=ED=9B=84=20=EC=8B=9C=EA=B0=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EB=B3=80=EA=B2=BD=EB=90=98=EB=A9=B4=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Handler/UserTimeZoneSyncHandler.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DevLog/App/Handler/UserTimeZoneSyncHandler.swift b/DevLog/App/Handler/UserTimeZoneSyncHandler.swift index f047f812..2e15f8ba 100644 --- a/DevLog/App/Handler/UserTimeZoneSyncHandler.swift +++ b/DevLog/App/Handler/UserTimeZoneSyncHandler.swift @@ -6,7 +6,7 @@ // import Combine -import Foundation +import UIKit final class UserTimeZoneSyncHandler { private let repository: UserDataRepository @@ -20,6 +20,7 @@ final class UserTimeZoneSyncHandler { self.repository = repository notificationCenter.publisher(for: .didRequestUserTimeZoneSync) + .merge(with: notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification)) .sink { [weak self] _ in Task { do {