diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b768b1..b0e3ad3 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: diff --git a/DevLog/App/Assembler/AppLayerAssembler.swift b/DevLog/App/Assembler/AppLayerAssembler.swift new file mode 100644 index 0000000..8533e68 --- /dev/null +++ b/DevLog/App/Assembler/AppLayerAssembler.swift @@ -0,0 +1,21 @@ +// +// AppLayerAssembler.swift +// DevLog +// +// Created by opfic on 3/19/26. +// + +final class AppLayerAssembler: Assembler { + func assemble(_ container: any DIContainer) { + container.register(FCMTokenSyncHandler.self) { + FCMTokenSyncHandler( + repository: container.resolve(UserDataRepository.self) + ) + } + container.register(UserTimeZoneSyncHandler.self) { + UserTimeZoneSyncHandler( + repository: container.resolve(UserDataRepository.self) + ) + } + } +} diff --git a/DevLog/App/Assembler/Assembler.swift b/DevLog/App/Assembler/Assembler.swift index 5c65b40..ca5f99f 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 7a4e033..b2cdb7f 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,7 +26,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { FirebaseApp.configure() - + _ = container.resolve(FCMTokenSyncHandler.self) + _ = container.resolve(UserTimeZoneSyncHandler.self) + // 알림 권한 요청 UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in @@ -41,7 +43,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { } // 앱이 온그라운드로 되었을 때, 로그인 세션이 존재한다면 현재 유저의 timeZone 저장 - updateUserTimeZone() + NotificationCenter.default.post(name: .didRequestUserTimeZoneSync, object: nil) // Firebase Messaging 설정 Messaging.messaging().delegate = self @@ -78,21 +80,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { ) { if let fcmToken = fcmToken { logger.info("FCM token: \(fcmToken)") - } - } -} - -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) - } + NotificationCenter.default.post( + name: .didRefreshFCMToken, + object: nil, + userInfo: ["fcmToken": fcmToken] + ) } } } diff --git a/DevLog/App/Handler/FCMTokenSyncHandler.swift b/DevLog/App/Handler/FCMTokenSyncHandler.swift new file mode 100644 index 0000000..c3218c5 --- /dev/null +++ b/DevLog/App/Handler/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 repository: UserDataRepository + private let logger = Logger(category: "FCMTokenSyncHandler") + private var cancellables = Set() + + init( + repository: UserDataRepository, + notificationCenter: NotificationCenter = .default + ) { + self.repository = repository + + notificationCenter.publisher(for: .didRefreshFCMToken) + .compactMap { $0.userInfo?["fcmToken"] as? String } + .sink { [weak self] fcmToken in + Task { + do { + try await self?.repository.updateFCMToken(fcmToken) + } catch { + self?.logger.error("Failed to sync refreshed FCM token", error: error) + } + } + } + .store(in: &cancellables) + } +} diff --git a/DevLog/App/Handler/UserTimeZoneSyncHandler.swift b/DevLog/App/Handler/UserTimeZoneSyncHandler.swift new file mode 100644 index 0000000..2e15f8b --- /dev/null +++ b/DevLog/App/Handler/UserTimeZoneSyncHandler.swift @@ -0,0 +1,35 @@ +// +// UserTimeZoneSyncHandler.swift +// DevLog +// +// Created by opfic on 3/19/26. +// + +import Combine +import UIKit + +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) + .merge(with: notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification)) + .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 new file mode 100644 index 0000000..41edfbc --- /dev/null +++ b/DevLog/App/Notification/NotificationName+.swift @@ -0,0 +1,13 @@ +// +// NotificationName+.swift +// DevLog +// +// Created by opfic on 3/19/26. +// + +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 0004d7e..baf7365 100644 --- a/DevLog/Data/Repository/UserDataRepositoryImpl.swift +++ b/DevLog/Data/Repository/UserDataRepositoryImpl.swift @@ -19,6 +19,14 @@ 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) + } + + func updateUserTimeZone() async throws { + try await userService.updateUserTimeZone() } } diff --git a/DevLog/Domain/Protocol/UserDataRepository.swift b/DevLog/Domain/Protocol/UserDataRepository.swift index 020520d..f0612a4 100644 --- a/DevLog/Domain/Protocol/UserDataRepository.swift +++ b/DevLog/Domain/Protocol/UserDataRepository.swift @@ -8,4 +8,6 @@ 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 d62134b..0409310 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 { @@ -157,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 + } + } }