From 18cc91cbf79f0f1548ee2a9a13f5dc381d536a11 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:50:20 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20=EA=B8=B0=EB=B3=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/ProjectDescriptionHelpers/Project+Settings.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tuist/ProjectDescriptionHelpers/Project+Settings.swift b/Tuist/ProjectDescriptionHelpers/Project+Settings.swift index c56f2cbb..48bae5f2 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Settings.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Settings.swift @@ -16,7 +16,6 @@ public extension Settings { "CURRENT_PROJECT_VERSION": "1", "INFOPLIST_KEY_CFBundleShortVersionString": "$(MARKETING_VERSION)", "INFOPLIST_KEY_CFBundleVersion": "$(CURRENT_PROJECT_VERSION)", - "SWIFT_STRICT_CONCURRENCY": "complete", "SWIFT_VERSION": "5.0", "TARGETED_DEVICE_FAMILY": "1,2", ] From d61f063f2b5d7289a95badb546cf648c44a8d6da Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:04:21 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20AsyncStream=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=8A=A4=ED=8A=B8=EB=A6=BC=EC=9D=84=20Combine?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/TodoMutationEventBusImpl.swift | 30 ++++------------- .../Repository/TodoRepositoryImpl.swift | 6 ++-- .../TodoMutationEventBusImplTests.swift | 19 ++++++----- .../Repository/TodoRepositoryImplTests.swift | 14 ++++---- .../Protocol/TodoMutationEventBus.swift | 8 +++-- .../Home/Home/HomeViewCoordinator.swift | 33 +++++++++---------- 6 files changed, 46 insertions(+), 64 deletions(-) diff --git a/Application/DevLogData/Sources/Repository/TodoMutationEventBusImpl.swift b/Application/DevLogData/Sources/Repository/TodoMutationEventBusImpl.swift index c0f4e9ce..ddd60a8d 100644 --- a/Application/DevLogData/Sources/Repository/TodoMutationEventBusImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoMutationEventBusImpl.swift @@ -5,33 +5,17 @@ // Created by opfic on 6/6/26. // -import Foundation +import Combine import DevLogDomain -actor TodoMutationEventBusImpl: TodoMutationEventBus { - private var continuations = [UUID: AsyncStream.Continuation]() +final class TodoMutationEventBusImpl: TodoMutationEventBus { + private let subject = PassthroughSubject() - func publish(_ event: TodoMutationEvent) async { - continuations.values.forEach { $0.yield(event) } + func publish(_ event: TodoMutationEvent) { + subject.send(event) } - func events() -> AsyncStream { - let id = UUID() - let (stream, continuation) = AsyncStream.makeStream(of: TodoMutationEvent.self) - - continuations[id] = continuation - continuation.onTermination = { [weak self] _ in - Task { - await self?.removeContinuation(id: id) - } - } - - return stream - } -} - -private extension TodoMutationEventBusImpl { - func removeContinuation(id: UUID) { - continuations[id] = nil + func observe() -> AnyPublisher { + subject.eraseToAnyPublisher() } } diff --git a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift index d8e5472b..3ea1f376 100644 --- a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift @@ -110,7 +110,7 @@ final class TodoRepositoryImpl: TodoRepository { func upsertTodo(_ todo: Todo) async throws { let todoRequest = TodoRequest.fromDomain(todo) try await upsertTodo(todoRequest) - await todoMutationEventBus.publish(.updated(todo.id)) + todoMutationEventBus.publish(.updated(todo.id)) } func upsertTodo(_ todoDraft: TodoDraft) async throws { @@ -131,7 +131,7 @@ final class TodoRepositoryImpl: TodoRepository { do { try await todoService.deleteTodo(todoId: todoId) widgetSyncEventBus.publish(.syncRequested) - await todoMutationEventBus.publish(.deleted(todoId)) + todoMutationEventBus.publish(.deleted(todoId)) } catch { throw error.toDomain() } @@ -141,7 +141,7 @@ final class TodoRepositoryImpl: TodoRepository { do { try await todoService.undoDeleteTodo(todoId: todoId) widgetSyncEventBus.publish(.syncRequested) - await todoMutationEventBus.publish(.restored(todoId)) + todoMutationEventBus.publish(.restored(todoId)) } catch { throw error.toDomain() } diff --git a/Application/DevLogData/Tests/Repository/TodoMutationEventBusImplTests.swift b/Application/DevLogData/Tests/Repository/TodoMutationEventBusImplTests.swift index 2b21d9f1..2c078e12 100644 --- a/Application/DevLogData/Tests/Repository/TodoMutationEventBusImplTests.swift +++ b/Application/DevLogData/Tests/Repository/TodoMutationEventBusImplTests.swift @@ -5,23 +5,24 @@ // Created by opfic on 6/6/26. // +import Combine import Testing import DevLogDomain @testable import DevLogData struct TodoMutationEventBusImplTests { @Test("TodoMutationEventBus는 발행된 이벤트를 관찰자에게 전달한다") - func todoMutationEventBus는_발행된_이벤트를_관찰자에게_전달한다() async { + func todoMutationEventBus는_발행된_이벤트를_관찰자에게_전달한다() { let bus = TodoMutationEventBusImpl() - let events = await bus.events() - let task = Task { - var iterator = events.makeAsyncIterator() - return await iterator.next() - } + var events = [TodoMutationEvent]() + var cancellables = Set() - await bus.publish(.updated("todo-id")) + bus.observe() + .sink { events.append($0) } + .store(in: &cancellables) - let event = await task.value - #expect(event == .updated("todo-id")) + bus.publish(.updated("todo-id")) + + #expect(events == [.updated("todo-id")]) } } diff --git a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift index 7f72e996..8b9de1b3 100644 --- a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift @@ -25,7 +25,7 @@ struct TodoRepositoryImplTests { let events = fixture.widgetSyncEventBus.events #expect(events == [.syncRequested, .syncRequested, .syncRequested]) - let mutationEvents = await fixture.todoMutationEventBus.publishedEvents() + let mutationEvents = fixture.todoMutationEventBus.publishedEvents() #expect(mutationEvents == [.updated(todo.id), .deleted(todo.id), .restored(todo.id)]) } @@ -58,7 +58,7 @@ struct TodoRepositoryImplTests { let syncEvents = fixture.widgetSyncEventBus.events #expect(syncEvents.isEmpty) - let mutationEvents = await fixture.todoMutationEventBus.publishedEvents() + let mutationEvents = fixture.todoMutationEventBus.publishedEvents() #expect(mutationEvents.isEmpty) } @@ -169,10 +169,10 @@ private final class WidgetSyncEventBusSpy: WidgetSyncEventBus { } } -private actor TodoMutationEventBusSpy: TodoMutationEventBus { +private final class TodoMutationEventBusSpy: TodoMutationEventBus { private var capturedEvents = [TodoMutationEvent]() - func publish(_ event: TodoMutationEvent) async { + func publish(_ event: TodoMutationEvent) { capturedEvents.append(event) } @@ -180,10 +180,8 @@ private actor TodoMutationEventBusSpy: TodoMutationEventBus { capturedEvents } - func events() async -> AsyncStream { - AsyncStream { continuation in - continuation.finish() - } + func observe() -> AnyPublisher { + Empty().eraseToAnyPublisher() } } diff --git a/Application/DevLogDomain/Sources/Protocol/TodoMutationEventBus.swift b/Application/DevLogDomain/Sources/Protocol/TodoMutationEventBus.swift index 466b9562..ff3559e5 100644 --- a/Application/DevLogDomain/Sources/Protocol/TodoMutationEventBus.swift +++ b/Application/DevLogDomain/Sources/Protocol/TodoMutationEventBus.swift @@ -5,7 +5,9 @@ // Created by opfic on 6/6/26. // -public protocol TodoMutationEventBus: Sendable { - func publish(_ event: TodoMutationEvent) async - func events() async -> AsyncStream +import Combine + +public protocol TodoMutationEventBus { + func publish(_ event: TodoMutationEvent) + func observe() -> AnyPublisher } diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index f5ef30d4..8cdb3a89 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -13,17 +13,15 @@ import DevLogDomain @MainActor @Observable final class HomeViewCoordinator { - private enum AsyncStreamTaskID { - case todoMutationEvent - } - let viewModel: HomeViewModel let router = NavigationRouter() private let container: DIContainer @ObservationIgnored - private var cancellable: AnyCancellable? + private var cancellables = Set() + @ObservationIgnored + private var isTodoMutationEventBound = false @ObservationIgnored - private var streamTasks = [AsyncStreamTaskID: Task]() + private var isWindowEventBound = false init(container: DIContainer) { self.container = container @@ -40,10 +38,6 @@ final class HomeViewCoordinator { ) } - deinit { - streamTasks.values.forEach { $0.cancel() } - } - func fetchData() { viewModel.send(.fetchData) } @@ -53,30 +47,33 @@ final class HomeViewCoordinator { } func bindTodoMutationEvent() { - guard streamTasks[.todoMutationEvent] == nil else { return } + guard isTodoMutationEventBound == false else { return } + isTodoMutationEventBound = true let bus = container.resolve(TodoMutationEventBus.self) - streamTasks[.todoMutationEvent] = Task { [weak self] in - let events = await bus.events() - for await event in events { - guard let self else { break } + bus.observe() + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + guard let self else { return } switch event { case .updated, .deleted, .restored: self.refreshRecentTodos() } } - } + .store(in: &cancellables) } func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) { - guard cancellable == nil else { return } + guard isWindowEventBound == false else { return } + isWindowEventBound = true - cancellable = windowEvent.submits + windowEvent.submits .sink { [weak self] submit in guard case .create(let value) = submit, value.matchesCreate(source: .home) else { return } self?.viewModel.send(.fetchData) } + .store(in: &cancellables) } func makeTodoManageViewModel() -> TodoManageViewModel { From 092b53810c160d3d652f1f248fa1c0770c05d47f Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:19:41 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20Sendable=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogDomain/Sources/Entity/AuthProvider.swift | 2 +- .../Sources/Entity/TodoMutationEvent.swift | 2 +- .../Sources/Persistence/WebPageImageStoreImpl.swift | 2 +- .../Sources/Login/LoginFeature.swift | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Application/DevLogDomain/Sources/Entity/AuthProvider.swift b/Application/DevLogDomain/Sources/Entity/AuthProvider.swift index ae30ca08..d739738b 100644 --- a/Application/DevLogDomain/Sources/Entity/AuthProvider.swift +++ b/Application/DevLogDomain/Sources/Entity/AuthProvider.swift @@ -7,7 +7,7 @@ import Foundation -public enum AuthProvider: String, CaseIterable, Sendable { +public enum AuthProvider: String, CaseIterable { case apple = "apple.com" case google = "google.com" case github = "github.com" diff --git a/Application/DevLogDomain/Sources/Entity/TodoMutationEvent.swift b/Application/DevLogDomain/Sources/Entity/TodoMutationEvent.swift index 78e10a08..be6fa990 100644 --- a/Application/DevLogDomain/Sources/Entity/TodoMutationEvent.swift +++ b/Application/DevLogDomain/Sources/Entity/TodoMutationEvent.swift @@ -5,7 +5,7 @@ // Created by opfic on 6/6/26. // -public enum TodoMutationEvent: Equatable, Sendable { +public enum TodoMutationEvent: Equatable { case updated(String) case deleted(String) case restored(String) diff --git a/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift b/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift index 8d6adb8e..a73fcdf4 100644 --- a/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift +++ b/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift @@ -51,7 +51,7 @@ final class WebPageImageStoreImpl: WebPageImageStore { } private extension WebPageImageStoreImpl { - func perform(_ operation: @escaping @Sendable () throws -> T) async throws -> T { + func perform(_ operation: @escaping () throws -> T) async throws -> T { try await withCheckedThrowingContinuation { continuation in queue.async { do { diff --git a/Application/DevLogPresentation/Sources/Login/LoginFeature.swift b/Application/DevLogPresentation/Sources/Login/LoginFeature.swift index 476ed292..d6f69006 100644 --- a/Application/DevLogPresentation/Sources/Login/LoginFeature.swift +++ b/Application/DevLogPresentation/Sources/Login/LoginFeature.swift @@ -20,7 +20,7 @@ struct LoginFeature { var alertMessage = "" } - enum Action: Sendable { + enum Action { case setAlert(Bool, AlertType? = nil) case tapSignInButton(AuthProvider) case signInSucceeded @@ -28,7 +28,7 @@ struct LoginFeature { case signInCancelled } - enum AlertType: Equatable, Sendable { + enum AlertType: Equatable { case emailUnavailable case error } @@ -65,10 +65,10 @@ struct LoginFeature { } } -struct SignInUseCaseDependency: Sendable { - var execute: @Sendable (AuthProvider) async throws -> Void +struct SignInUseCaseDependency { + var execute: (AuthProvider) async throws -> Void - init(execute: @escaping @Sendable (AuthProvider) async throws -> Void) { + init(execute: @escaping (AuthProvider) async throws -> Void) { self.execute = execute } } From 3bcfdde45d9fda7a281ff131218a0c3ad431c29a Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:34:27 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20Sendable=20=EA=B2=BD=EA=B3=A0?= =?UTF-8?q?=EB=A5=BC=20=ED=95=B4=EA=B2=B0=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=9E=94=EC=97=AC=20=EA=B5=AC=ED=98=84=EB=AC=BC=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 --- .../Sources/Common/FirebaseDependency.swift | 47 ----------- .../Sources/Service/AuthServiceImpl.swift | 83 +++++-------------- .../AppleAuthenticationServiceImpl.swift | 78 ++++++----------- .../GithubAuthenticationServiceImpl.swift | 15 ++-- .../GoogleAuthenticationServiceImpl.swift | 4 +- .../Sources/Service/UserServiceImpl.swift | 2 +- .../Widget/UserDefaultsDependency.swift | 32 ------- .../WidgetSnapshotPreferenceStoreImpl.swift | 4 +- .../Common/UserDefaultsDependency.swift | 28 ------- .../Common/WidgetSharedDefaultsStore.swift | 4 +- 10 files changed, 64 insertions(+), 233 deletions(-) delete mode 100644 Application/DevLogInfra/Sources/Common/FirebaseDependency.swift delete mode 100644 Application/DevLogPersistence/Sources/Widget/UserDefaultsDependency.swift delete mode 100644 Widget/DevLogWidgetCore/Sources/Common/UserDefaultsDependency.swift diff --git a/Application/DevLogInfra/Sources/Common/FirebaseDependency.swift b/Application/DevLogInfra/Sources/Common/FirebaseDependency.swift deleted file mode 100644 index d34077da..00000000 --- a/Application/DevLogInfra/Sources/Common/FirebaseDependency.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// FirebaseDependency.swift -// DevLogInfra -// -// Created by opfic on 6/5/26. -// - -import FirebaseAuth -import FirebaseFirestore -import FirebaseFunctions -import FirebaseMessaging - -struct FirebaseDependency { - private let value: Value - - init(value: Value) { - self.value = value - } -} - -extension FirebaseDependency where Value == Firestore { - func document(_ path: String) -> DocumentReference { - value.document(path) - } -} - -extension FirebaseDependency where Value == Functions { - func httpsCallable(_ name: some RawRepresentable) -> HTTPSCallable { - value.httpsCallable(name) - } -} - -extension FirebaseDependency where Value == Messaging { - func token() async throws -> String { - try await value.token() - } - - func deleteToken() async throws { - try await value.deleteToken() - } -} - -extension FirebaseDependency where Value == AuthStateDidChangeListenerHandle { - func removeAuthStateDidChangeListener() { - Auth.auth().removeStateDidChangeListener(value) - } -} diff --git a/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift b/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift index c5587dc2..c8f97147 100644 --- a/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift @@ -13,10 +13,12 @@ import DevLogCore import DevLogData final class AuthServiceImpl: AuthService { - private let store = FirebaseDependency(value: Firestore.firestore()) - private let messaging = FirebaseDependency(value: Messaging.messaging()) + private let store = Firestore.firestore() + private let messaging = Messaging.messaging() private let logger = Logger(category: "AuthServiceImpl") - private let authStatePublisher: AuthStatePublisher + private let subject = CurrentValueSubject(Auth.auth().currentUser != nil) + private var handler: AuthStateDidChangeListenerHandle? + private var isCompletingSignIn = false var uid: String? { Auth.auth().currentUser?.uid @@ -35,26 +37,36 @@ final class AuthServiceImpl: AuthService { } init() { - authStatePublisher = AuthStatePublisher(logger: logger) + handler = Auth.auth().addStateDidChangeListener { [weak self] _, user in + self?.handleAuthStateChange(user) + } + } + + deinit { + guard let handler else { return } + Auth.auth().removeStateDidChangeListener(handler) } func observeSignedIn() -> AnyPublisher { - authStatePublisher.observeSignedIn() + subject.eraseToAnyPublisher() } func beginSignIn() { logger.info("Beginning sign-in bootstrap") - authStatePublisher.beginSignIn() + isCompletingSignIn = true + subject.send(false) } func completeSignIn() { logger.info("Completing sign-in bootstrap") - authStatePublisher.completeSignIn() + isCompletingSignIn = false + subject.send(Auth.auth().currentUser != nil) } func cancelSignIn() { logger.info("Cancelling sign-in bootstrap") - authStatePublisher.cancelSignIn() + isCompletingSignIn = false + subject.send(Auth.auth().currentUser != nil) } func getProviderID() async throws -> String? { @@ -114,59 +126,8 @@ final class AuthServiceImpl: AuthService { } -private final class AuthStatePublisher { - private let logger: Logger - private let subject: CurrentValueSubject - private let lock = NSLock() - private var handler: FirebaseDependency? - private var isCompletingSignIn = false - - init(logger: Logger) { - self.logger = logger - self.subject = CurrentValueSubject(Auth.auth().currentUser != nil) - self.handler = FirebaseDependency( - value: Auth.auth().addStateDidChangeListener { [weak self] _, user in - self?.handleAuthStateChange(user) - } - ) - } - - deinit { - guard let handler else { return } - handler.removeAuthStateDidChangeListener() - } - - func observeSignedIn() -> AnyPublisher { - lock.lock() - defer { lock.unlock() } - return subject.eraseToAnyPublisher() - } - - func beginSignIn() { - lock.lock() - isCompletingSignIn = true - subject.send(false) - lock.unlock() - } - - func completeSignIn() { - lock.lock() - isCompletingSignIn = false - subject.send(Auth.auth().currentUser != nil) - lock.unlock() - } - - func cancelSignIn() { - lock.lock() - isCompletingSignIn = false - subject.send(Auth.auth().currentUser != nil) - lock.unlock() - } - - private func handleAuthStateChange(_ user: User?) { - lock.lock() - defer { lock.unlock() } - +private extension AuthServiceImpl { + func handleAuthStateChange(_ user: User?) { let signedIn = user != nil logger.info("Firebase auth state changed. signedIn: \(signedIn)") diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift index ab354471..2a7e7270 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift @@ -23,11 +23,13 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { case revokeAppleAccessToken } - private let session = AppleSignInSession() - private let store = FirebaseDependency(value: Firestore.firestore()) - private let functions = FirebaseDependency(value: Functions.functions(region: "asia-northeast3")) - private let messaging = FirebaseDependency(value: Messaging.messaging()) + private var appleSignInDelegate: AppleSignInDelegate? + private var appleSignInContinuation: CheckedContinuation? + private let store = Firestore.firestore() + private let functions = Functions.functions(region: "asia-northeast3") + private let messaging = Messaging.messaging() private var user: User? { Auth.auth().currentUser } + private let providerID = AuthProviderID.apple private let logger = Logger(category: "AppleAuthService") func signIn() async throws -> AuthDataResponse { @@ -79,9 +81,9 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { try await changeRequest.commitChanges() // FirebaseAuth 계정에 Apple ID 연결 - if !result.user.providerData.contains(where: { $0.providerID == AuthProviderID.apple.rawValue }) { + if !result.user.providerData.contains(where: { $0.providerID == providerID.rawValue }) { let appleCredential = OAuthProvider.credential( - providerID: AuthProviderID.apple, + providerID: providerID, idToken: idTokenString, rawNonce: nonce ) @@ -149,7 +151,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { } let appleCredential = OAuthProvider.credential( - providerID: AuthProviderID.apple, + providerID: providerID, idToken: idTokenString, rawNonce: nonce ) @@ -185,7 +187,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { } logger.info("Starting Firebase Apple provider unlink. uid: \(uid)") - _ = try await user?.unlink(fromProvider: AuthProviderID.apple.rawValue) + _ = try await user?.unlink(fromProvider: providerID.rawValue) } catch { logger.error("Failed to unlink Apple account", error: error) throw error @@ -195,7 +197,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { // Apple 인증 메서드 @MainActor func authenticateWithAppleAsync() async throws -> AppleAuthResponse { - guard session.canStartSignIn else { + guard appleSignInDelegate == nil, appleSignInContinuation == nil else { throw SocialLoginError.authenticationAlreadyInProgress } @@ -211,11 +213,13 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { let controller = ASAuthorizationController(authorizationRequests: [request]) let authorization = try await withCheckedThrowingContinuation { continuation in - let delegate = session.start(continuation: continuation) { [weak self] result in + let delegate = AppleSignInDelegate { [weak self] result in self?.completeAppleSignIn(with: result) } - controller.delegate = delegate - controller.presentationContextProvider = delegate + self.appleSignInDelegate = delegate + self.appleSignInContinuation = continuation + controller.delegate = self.appleSignInDelegate + controller.presentationContextProvider = self.appleSignInDelegate controller.performRequests() } @@ -237,7 +241,17 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { @MainActor private func completeAppleSignIn(with result: Result) { - session.complete(with: result) + guard let continuation = appleSignInContinuation else { return } + + appleSignInContinuation = nil + appleSignInDelegate = nil + + switch result { + case .success(let authorization): + continuation.resume(returning: authorization) + case .failure(let error): + continuation.resume(throwing: error) + } } // Apple CustomToken 발급 메서드 @@ -299,41 +313,3 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { _ = try await revokeFunction.call(["token": token]) } } - -private final class AppleSignInSession { - @MainActor - private var delegate: AppleSignInDelegate? - @MainActor - private var continuation: CheckedContinuation? - - @MainActor - var canStartSignIn: Bool { - delegate == nil && continuation == nil - } - - @MainActor - func start( - continuation: CheckedContinuation, - completion: @escaping @MainActor (Result) -> Void - ) -> AppleSignInDelegate { - let delegate = AppleSignInDelegate(finish: completion) - self.delegate = delegate - self.continuation = continuation - return delegate - } - - @MainActor - func complete(with result: Result) { - guard let continuation else { return } - - self.continuation = nil - self.delegate = nil - - switch result { - case .success(let authorization): - continuation.resume(returning: authorization) - case .failure(let error): - continuation.resume(throwing: error) - } - } -} diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift index de71578c..f0482b80 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift @@ -26,10 +26,11 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { static let acceptHeader = "application/vnd.github.v3+json" } - private let store = FirebaseDependency(value: Firestore.firestore()) - private let functions = FirebaseDependency(value: Functions.functions(region: "asia-northeast3")) - private let messaging = FirebaseDependency(value: Messaging.messaging()) + private let store = Firestore.firestore() + private let functions = Functions.functions(region: "asia-northeast3") + private let messaging = Messaging.messaging() private var user: User? { Auth.auth().currentUser } + private let providerID = AuthProviderID.gitHub private let provider = TopViewControllerProvider() private let logger = Logger(category: "GithubAuthService") private let gitHubApiClient = NXAPIClient( @@ -66,8 +67,8 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { } // 5. GitHub 계정과 Firebase Auth 계정 연결 - if !result.user.providerData.contains(where: { $0.providerID == "github.com" }) { - let credential = OAuthProvider.credential(providerID: AuthProviderID.gitHub, accessToken: accessToken) + if !result.user.providerData.contains(where: { $0.providerID == providerID.rawValue }) { + let credential = OAuthProvider.credential(providerID: providerID, accessToken: accessToken) try await result.user.link(with: credential) } @@ -136,7 +137,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { try await tokensRef.setData(["githubAccessToken": accessToken], merge: true) - let credential = OAuthProvider.credential(providerID: AuthProviderID.gitHub, accessToken: accessToken) + let credential = OAuthProvider.credential(providerID: providerID, accessToken: accessToken) try await user?.link(with: credential) logger.info("Successfully linked GitHub account") @@ -160,7 +161,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { try await tokensRef.updateData(["githubAccessToken": FieldValue.delete()]) logger.info("Starting Firebase GitHub provider unlink. uid: \(uid)") - _ = try await user?.unlink(fromProvider: AuthProviderID.gitHub.rawValue) + _ = try await user?.unlink(fromProvider: providerID.rawValue) } catch { logger.error("Failed to unlink GitHub account", error: error) throw error diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift index 5be6c436..b575c04c 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift @@ -14,8 +14,8 @@ import DevLogCore import DevLogData final class GoogleAuthenticationServiceImpl: AuthenticationService { - private let store = FirebaseDependency(value: Firestore.firestore()) - private let messaging = FirebaseDependency(value: Messaging.messaging()) + private let store = Firestore.firestore() + private let messaging = Messaging.messaging() private var user: User? { Auth.auth().currentUser } private let provider = TopViewControllerProvider() private let logger = Logger(category: "GoogleAuthService") diff --git a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift index f112c62e..cdedbb61 100644 --- a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift @@ -11,7 +11,7 @@ import DevLogCore import DevLogData final class UserServiceImpl: UserService { - private let store = FirebaseDependency(value: Firestore.firestore()) + private let store = Firestore.firestore() private let logger = Logger(category: "UserServiceImpl") // 유저를 Firestore에 저장 및 업데이트 diff --git a/Application/DevLogPersistence/Sources/Widget/UserDefaultsDependency.swift b/Application/DevLogPersistence/Sources/Widget/UserDefaultsDependency.swift deleted file mode 100644 index e74cae86..00000000 --- a/Application/DevLogPersistence/Sources/Widget/UserDefaultsDependency.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// UserDefaultsDependency.swift -// DevLogPersistence -// -// Created by opfic on 6/5/26. -// - -import Foundation - -struct UserDefaultsDependency { - private let value: UserDefaults - - init(value: UserDefaults) { - self.value = value - } - - func stringArray(forKey key: String) -> [String]? { - value.stringArray(forKey: key) - } - - func string(forKey key: String) -> String? { - value.string(forKey: key) - } - - func set(_ value: Any?, forKey key: String) { - self.value.set(value, forKey: key) - } - - func removeObject(forKey key: String) { - value.removeObject(forKey: key) - } -} diff --git a/Application/DevLogPersistence/Sources/Widget/WidgetSnapshotPreferenceStoreImpl.swift b/Application/DevLogPersistence/Sources/Widget/WidgetSnapshotPreferenceStoreImpl.swift index 82a8cc1a..e17594b9 100644 --- a/Application/DevLogPersistence/Sources/Widget/WidgetSnapshotPreferenceStoreImpl.swift +++ b/Application/DevLogPersistence/Sources/Widget/WidgetSnapshotPreferenceStoreImpl.swift @@ -16,10 +16,10 @@ final class WidgetSnapshotPreferenceStoreImpl: WidgetSnapshotPreferenceStore { case todayFocusVisibility = "Today.focusVisibility" } - private let userDefaults: UserDefaultsDependency + private let userDefaults: UserDefaults init(userDefaults: UserDefaults = .standard) { - self.userDefaults = UserDefaultsDependency(value: userDefaults) + self.userDefaults = userDefaults } func heatmapActivityTypes() -> [String] { diff --git a/Widget/DevLogWidgetCore/Sources/Common/UserDefaultsDependency.swift b/Widget/DevLogWidgetCore/Sources/Common/UserDefaultsDependency.swift deleted file mode 100644 index 76b83f1d..00000000 --- a/Widget/DevLogWidgetCore/Sources/Common/UserDefaultsDependency.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// UserDefaultsDependency.swift -// DevLogWidgetCore -// -// Created by opfic on 6/5/26. -// - -import Foundation - -struct UserDefaultsDependency { - private let value: UserDefaults - - init(value: UserDefaults) { - self.value = value - } - - func data(forKey key: String) -> Data? { - value.data(forKey: key) - } - - func set(_ value: Any?, forKey key: String) { - self.value.set(value, forKey: key) - } - - func removeObject(forKey key: String) { - value.removeObject(forKey: key) - } -} diff --git a/Widget/DevLogWidgetCore/Sources/Common/WidgetSharedDefaultsStore.swift b/Widget/DevLogWidgetCore/Sources/Common/WidgetSharedDefaultsStore.swift index 9d1ee58d..72b63765 100644 --- a/Widget/DevLogWidgetCore/Sources/Common/WidgetSharedDefaultsStore.swift +++ b/Widget/DevLogWidgetCore/Sources/Common/WidgetSharedDefaultsStore.swift @@ -8,10 +8,10 @@ import Foundation public final class WidgetSharedDefaultsStore { - private let userDefaults: UserDefaultsDependency + private let userDefaults: UserDefaults public init(userDefaults: UserDefaults = UserDefaults(suiteName: WidgetAppGroup.identifier) ?? .standard) { - self.userDefaults = UserDefaultsDependency(value: userDefaults) + self.userDefaults = userDefaults } func data(forKey key: String) -> Data? { From bb12799d4eafdbaf766c7b943481a60e9a2c4bcf Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:47:20 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=EB=AC=B4=EB=B6=84=EB=B3=84?= =?UTF-8?q?=ED=95=9C=20Sendable=20=EC=B1=84=ED=83=9D=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Persistence/WebPageImageStoreImpl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift b/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift index a73fcdf4..8d6adb8e 100644 --- a/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift +++ b/Application/DevLogPersistence/Sources/Persistence/WebPageImageStoreImpl.swift @@ -51,7 +51,7 @@ final class WebPageImageStoreImpl: WebPageImageStore { } private extension WebPageImageStoreImpl { - func perform(_ operation: @escaping () throws -> T) async throws -> T { + func perform(_ operation: @escaping @Sendable () throws -> T) async throws -> T { try await withCheckedThrowingContinuation { continuation in queue.async { do {