From 579c556868fe2e740a18a64d0d0196a745478647 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:49:08 +0900 Subject: [PATCH 01/19] =?UTF-8?q?refactor:=20preference=20->=20CategoryPre?= =?UTF-8?q?ference=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Protocol/TodoCategoryService.swift | 4 +- .../PushNotificationRepositoryImpl.swift | 6 ++- .../TodoCategoryRepositoryImpl.swift | 8 ++-- .../Repository/TodoRepositoryImpl.swift | 39 +++++++++++++------ .../Repository/TodoRepositoryImplTests.swift | 4 +- .../Protocol/TodoCategoryRepository.swift | 4 +- ...chTodoCategoryPreferencesUseCaseImpl.swift | 2 +- ...teTodoCategoryPreferencesUseCaseImpl.swift | 2 +- .../Service/TodoCategoryServiceImpl.swift | 4 +- 9 files changed, 45 insertions(+), 28 deletions(-) diff --git a/Application/DevLogData/Sources/Protocol/TodoCategoryService.swift b/Application/DevLogData/Sources/Protocol/TodoCategoryService.swift index 3ad7e980..466f91ed 100644 --- a/Application/DevLogData/Sources/Protocol/TodoCategoryService.swift +++ b/Application/DevLogData/Sources/Protocol/TodoCategoryService.swift @@ -8,6 +8,6 @@ import Foundation public protocol TodoCategoryService { - func fetchPreferences() async throws -> [TodoCategoryPreferenceResponse] - func updatePreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws + func fetchCategoryPreferences() async throws -> [TodoCategoryPreferenceResponse] + func updateCategoryPreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws } diff --git a/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift index 293f452a..97fa34e9 100644 --- a/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift @@ -59,7 +59,7 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { do { let cursorDTO = cursor.map { PushNotificationCursorDTO.fromDomain($0) } async let responseTask = pushNotificationService.requestNotifications(query, cursor: cursorDTO) - async let preferencesTask = todoCategoryService.fetchPreferences() + async let preferencesTask = todoCategoryService.fetchCategoryPreferences() let (response, preferenceResponses) = try await (responseTask, preferencesTask) return try resolvePage(from: response, with: preferenceResponses.toDomain()) @@ -91,7 +91,9 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { Task { do { - let preferences = try await self.todoCategoryService.fetchPreferences().toDomain() + let preferences = try await self.todoCategoryService + .fetchCategoryPreferences() + .toDomain() let page = try self.resolvePage(from: response, with: preferences) subject.send(page) } catch { diff --git a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift index b74b2c9b..945f7614 100644 --- a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift @@ -14,17 +14,17 @@ final class TodoCategoryRepositoryImpl: TodoCategoryRepository { self.todoCategoryService = todoCategoryService } - func fetchPreferences() async throws -> [TodoCategoryPreference] { + func fetchCategoryPreferences() async throws -> [TodoCategoryPreference] { do { - return try await todoCategoryService.fetchPreferences().toDomain() + return try await todoCategoryService.fetchCategoryPreferences().toDomain() } catch { throw error.toDomain() } } - func updatePreferences(_ preferences: [TodoCategoryPreference]) async throws { + func updateCategoryPreferences(_ preferences: [TodoCategoryPreference]) async throws { do { - try await todoCategoryService.updatePreferences( + try await todoCategoryService.updateCategoryPreferences( preferences.map(TodoCategoryPreferenceResponse.fromDomain) ) } catch { diff --git a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift index 3ea1f376..57381610 100644 --- a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift @@ -32,10 +32,15 @@ final class TodoRepositoryImpl: TodoRepository { do { async let response = todoService.fetchTodos(query, cursor: responseCursor) - async let preferences = todoCategoryService.fetchPreferences() - - let (todoResponse, todoPreferenceResponses) = try await (response, preferences) - let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses.toDomain().compactMap { preference in + async let preferences = todoCategoryService.fetchCategoryPreferences() + + let (todoResponse, todoPreferenceResponses) = try await ( + response, + preferences + ) + let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses + .toDomain() + .compactMap { preference in guard case .user(let category) = preference.category else { return nil } @@ -59,10 +64,15 @@ final class TodoRepositoryImpl: TodoRepository { func fetchTodo(_ todoId: String) async throws -> Todo { do { async let response = todoService.fetchTodo(todoId: todoId) - async let preferences = todoCategoryService.fetchPreferences() - - let (todoResponse, todoPreferenceResponses) = try await (response, preferences) - let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses.toDomain().compactMap { preference in + async let preferences = todoCategoryService.fetchCategoryPreferences() + + let (todoResponse, todoPreferenceResponses) = try await ( + response, + preferences + ) + let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses + .toDomain() + .compactMap { preference in guard case .user(let category) = preference.category else { return nil } @@ -79,10 +89,15 @@ final class TodoRepositoryImpl: TodoRepository { func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] { do { async let responseTask = todoService.fetchReferences(numbers) - async let preferencesTask = todoCategoryService.fetchPreferences() - - let (responses, preferenceResponses) = try await (responseTask, preferencesTask) - let userTodoCategories: [UserTodoCategory] = preferenceResponses.toDomain().compactMap { preference in + async let preferencesTask = todoCategoryService.fetchCategoryPreferences() + + let (responses, preferenceResponses) = try await ( + responseTask, + preferencesTask + ) + let userTodoCategories: [UserTodoCategory] = preferenceResponses + .toDomain() + .compactMap { preference in guard case .user(let category) = preference.category else { return nil } diff --git a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift index 8b9de1b3..ea48cebf 100644 --- a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift @@ -148,11 +148,11 @@ private actor TodoServiceSpy: TodoService { } private struct TodoCategoryServiceSpy: TodoCategoryService { - func fetchPreferences() async throws -> [TodoCategoryPreferenceResponse] { + func fetchCategoryPreferences() async throws -> [TodoCategoryPreferenceResponse] { [] } - func updatePreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws { + func updateCategoryPreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws { throw TodoRepositoryImplTestsError.unexpectedCall } } diff --git a/Application/DevLogDomain/Sources/Protocol/TodoCategoryRepository.swift b/Application/DevLogDomain/Sources/Protocol/TodoCategoryRepository.swift index 686932ca..8b2be92b 100644 --- a/Application/DevLogDomain/Sources/Protocol/TodoCategoryRepository.swift +++ b/Application/DevLogDomain/Sources/Protocol/TodoCategoryRepository.swift @@ -6,6 +6,6 @@ // public protocol TodoCategoryRepository { - func fetchPreferences() async throws -> [TodoCategoryPreference] - func updatePreferences(_ preferences: [TodoCategoryPreference]) async throws + func fetchCategoryPreferences() async throws -> [TodoCategoryPreference] + func updateCategoryPreferences(_ preferences: [TodoCategoryPreference]) async throws } diff --git a/Application/DevLogDomain/Sources/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCaseImpl.swift index f70faf26..7338fd51 100644 --- a/Application/DevLogDomain/Sources/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCaseImpl.swift +++ b/Application/DevLogDomain/Sources/UseCase/TodoCategory/Fetch/FetchTodoCategoryPreferencesUseCaseImpl.swift @@ -13,6 +13,6 @@ public final class FetchTodoCategoryPreferencesUseCaseImpl: FetchTodoCategoryPre } public func execute() async throws -> [TodoCategoryPreference] { - try await todoCategoryRepository.fetchPreferences() + try await todoCategoryRepository.fetchCategoryPreferences() } } diff --git a/Application/DevLogDomain/Sources/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCaseImpl.swift index a280c0fd..fe78098a 100644 --- a/Application/DevLogDomain/Sources/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCaseImpl.swift +++ b/Application/DevLogDomain/Sources/UseCase/TodoCategory/Update/UpdateTodoCategoryPreferencesUseCaseImpl.swift @@ -13,6 +13,6 @@ public final class UpdateTodoCategoryPreferencesUseCaseImpl: UpdateTodoCategoryP } public func execute(_ preferences: [TodoCategoryPreference]) async throws { - try await todoCategoryRepository.updatePreferences(preferences) + try await todoCategoryRepository.updateCategoryPreferences(preferences) } } diff --git a/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift index 7c4959cf..e058eb2b 100644 --- a/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift @@ -29,7 +29,7 @@ final class TodoCategoryServiceImpl: TodoCategoryService { private let store = Firestore.firestore() private let logger = Logger(category: "TodoCategoryServiceImpl") - func fetchPreferences() async throws -> [TodoCategoryPreferenceResponse] { + func fetchCategoryPreferences() async throws -> [TodoCategoryPreferenceResponse] { guard let uid = Auth.auth().currentUser?.uid else { logger.error("User not authenticated") throw DataLayerError.notAuthenticated @@ -61,7 +61,7 @@ final class TodoCategoryServiceImpl: TodoCategoryService { } } - func updatePreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws { + func updateCategoryPreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws { guard let uid = Auth.auth().currentUser?.uid else { logger.error("User not authenticated") throw DataLayerError.notAuthenticated From a1886a6715766c2f1a66b4ae5f27c1fd202fbfd3 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:57:24 +0900 Subject: [PATCH 02/19] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/Repository/TodoRepositoryImpl.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift index 57381610..2bdbef33 100644 --- a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift @@ -31,11 +31,11 @@ final class TodoRepositoryImpl: TodoRepository { let responseCursor = cursor.map { TodoCursorDTO.fromDomain($0) } do { - async let response = todoService.fetchTodos(query, cursor: responseCursor) + async let todos = todoService.fetchTodos(query, cursor: responseCursor) async let preferences = todoCategoryService.fetchCategoryPreferences() let (todoResponse, todoPreferenceResponses) = try await ( - response, + todos, preferences ) let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses From 45cb97172eaafb881909ac4f1028fca2bdfcdc42 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:22:19 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20store=EC=97=90=20json=ED=99=94=20?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20Codable=20?= =?UTF-8?q?=EC=B1=84=ED=83=9D=20=ED=83=80=EC=9E=85=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Protocol/UserDefaultsStore.swift | 3 +++ .../Persistence/UserDefaultsStoreImpl.swift | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Application/DevLogData/Sources/Protocol/UserDefaultsStore.swift b/Application/DevLogData/Sources/Protocol/UserDefaultsStore.swift index 15dd37f7..5369c52a 100644 --- a/Application/DevLogData/Sources/Protocol/UserDefaultsStore.swift +++ b/Application/DevLogData/Sources/Protocol/UserDefaultsStore.swift @@ -8,6 +8,9 @@ import Foundation public protocol UserDefaultsStore { + func value(forKey key: String) -> T? + func setValue(_ value: T?, forKey key: String) + func removeValues(withPrefix prefix: String) func string(forKey key: String) -> String? func setString(_ value: String?, forKey key: String) func stringArray(forKey key: String) -> [String] diff --git a/Application/DevLogPersistence/Sources/Persistence/UserDefaultsStoreImpl.swift b/Application/DevLogPersistence/Sources/Persistence/UserDefaultsStoreImpl.swift index 7775a701..21cd825c 100644 --- a/Application/DevLogPersistence/Sources/Persistence/UserDefaultsStoreImpl.swift +++ b/Application/DevLogPersistence/Sources/Persistence/UserDefaultsStoreImpl.swift @@ -19,6 +19,33 @@ final class UserDefaultsStoreImpl: UserDefaultsStore { userDefaults.string(forKey: key) } + func value(forKey key: String) -> T? { + let decoder = JSONDecoder() + guard let data = userDefaults.data(forKey: key) else { return nil } + guard let value = try? decoder.decode(T.self, from: data) else { + userDefaults.removeObject(forKey: key) + return nil + } + return value + } + + func setValue(_ value: T?, forKey key: String) { + let encoder = JSONEncoder() + guard let value else { + userDefaults.removeObject(forKey: key) + return + } + + guard let data = try? encoder.encode(value) else { return } + userDefaults.set(data, forKey: key) + } + + func removeValues(withPrefix prefix: String) { + userDefaults.dictionaryRepresentation().keys + .filter { $0.hasPrefix(prefix) } + .forEach(userDefaults.removeObject(forKey:)) + } + func setString(_ value: String?, forKey key: String) { userDefaults.set(value, forKey: key) } From ee18284165decc4ff7d7b77933ebdb716c0081aa Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:48:49 +0900 Subject: [PATCH 04/19] =?UTF-8?q?refactor:=20DTO=EC=97=90=20codable=20?= =?UTF-8?q?=EC=B1=84=ED=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DTO/TodoCategoryPreferenceResponse.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Application/DevLogData/Sources/DTO/TodoCategoryPreferenceResponse.swift b/Application/DevLogData/Sources/DTO/TodoCategoryPreferenceResponse.swift index 8bc875a3..1f0e7226 100644 --- a/Application/DevLogData/Sources/DTO/TodoCategoryPreferenceResponse.swift +++ b/Application/DevLogData/Sources/DTO/TodoCategoryPreferenceResponse.swift @@ -7,13 +7,13 @@ import Foundation -public struct TodoCategoryPreferenceResponse: Equatable { - public enum Category: Equatable { +public struct TodoCategoryPreferenceResponse: Equatable, Codable { + public enum Category: Equatable, Codable { case system(String) case user(UserCategory) } - public struct UserCategory: Equatable { + public struct UserCategory: Equatable, Codable { public let id: String public let name: String public let colorHex: String From e0749d56edb62d82221733bd4c1e6230a3d68c67 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:02:19 +0900 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20TodoCategoryPreference=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DataAssembler.swift | 4 +++- .../TodoCategoryRepositoryImpl.swift | 21 ++++++++++++++----- .../Repository/TodoRepositoryImpl.swift | 19 ++++++++++++++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift index bb64b47c..431d0c8a 100644 --- a/Application/DevLogData/Sources/DataAssembler.swift +++ b/Application/DevLogData/Sources/DataAssembler.swift @@ -40,6 +40,7 @@ public final class DataAssembler: Assembler { TodoRepositoryImpl( todoService: container.resolve(TodoService.self), todoCategoryService: container.resolve(TodoCategoryService.self), + store: container.resolve(UserDefaultsStore.self), widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self), todoMutationEventBus: container.resolve(TodoMutationEventBus.self) ) @@ -53,7 +54,8 @@ public final class DataAssembler: Assembler { container.register(TodoCategoryRepository.self) { TodoCategoryRepositoryImpl( - todoCategoryService: container.resolve(TodoCategoryService.self) + todoCategoryService: container.resolve(TodoCategoryService.self), + store: container.resolve(UserDefaultsStore.self) ) } diff --git a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift index 945f7614..39e66e44 100644 --- a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift @@ -8,15 +8,26 @@ import DevLogDomain final class TodoCategoryRepositoryImpl: TodoCategoryRepository { + private enum Key { + static let preferences = "TodoCategory.preferences" + } + private let todoCategoryService: TodoCategoryService + private let store: UserDefaultsStore - init(todoCategoryService: TodoCategoryService) { + init( + todoCategoryService: TodoCategoryService, + store: UserDefaultsStore + ) { self.todoCategoryService = todoCategoryService + self.store = store } func fetchCategoryPreferences() async throws -> [TodoCategoryPreference] { do { - return try await todoCategoryService.fetchCategoryPreferences().toDomain() + let responses = try await todoCategoryService.fetchCategoryPreferences() + store.setValue(responses, forKey: Key.preferences) + return responses.toDomain() } catch { throw error.toDomain() } @@ -24,9 +35,9 @@ final class TodoCategoryRepositoryImpl: TodoCategoryRepository { func updateCategoryPreferences(_ preferences: [TodoCategoryPreference]) async throws { do { - try await todoCategoryService.updateCategoryPreferences( - preferences.map(TodoCategoryPreferenceResponse.fromDomain) - ) + let responses = preferences.map(TodoCategoryPreferenceResponse.fromDomain) + try await todoCategoryService.updateCategoryPreferences(responses) + store.setValue(responses, forKey: Key.preferences) } catch { throw error.toDomain() } diff --git a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift index 2bdbef33..6008c1eb 100644 --- a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift @@ -10,19 +10,26 @@ import DevLogCore import DevLogDomain final class TodoRepositoryImpl: TodoRepository { + private enum Key { + static let preferences = "TodoCategory.preferences" + } + private let todoService: TodoService private let todoCategoryService: TodoCategoryService + private let store: UserDefaultsStore private let widgetSyncEventBus: WidgetSyncEventBus private let todoMutationEventBus: TodoMutationEventBus init( todoService: TodoService, todoCategoryService: TodoCategoryService, + store: UserDefaultsStore, widgetSyncEventBus: WidgetSyncEventBus, todoMutationEventBus: TodoMutationEventBus ) { self.todoService = todoService self.todoCategoryService = todoCategoryService + self.store = store self.widgetSyncEventBus = widgetSyncEventBus self.todoMutationEventBus = todoMutationEventBus } @@ -32,7 +39,7 @@ final class TodoRepositoryImpl: TodoRepository { do { async let todos = todoService.fetchTodos(query, cursor: responseCursor) - async let preferences = todoCategoryService.fetchCategoryPreferences() + async let preferences = todoCategoryPreferenceResponses() let (todoResponse, todoPreferenceResponses) = try await ( todos, @@ -164,6 +171,16 @@ final class TodoRepositoryImpl: TodoRepository { } private extension TodoRepositoryImpl { + func todoCategoryPreferenceResponses() async throws -> [TodoCategoryPreferenceResponse] { + if let preferences: [TodoCategoryPreferenceResponse] = store.value(forKey: Key.preferences) { + return preferences + } + + let preferences = try await todoCategoryService.fetchCategoryPreferences() + store.setValue(preferences, forKey: Key.preferences) + return preferences + } + func resolve( _ response: TodoResponse, userTodoCategories: [UserTodoCategory] From 9248dfc517cc54281004d17995b1c549311f811d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:15:54 +0900 Subject: [PATCH 06/19] =?UTF-8?q?refactor:=20=EC=83=9D=EC=84=B1=EC=9E=90?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Auth/Session/ObserveAuthSessionUseCaseImpl.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Application/DevLogDomain/Sources/UseCase/Auth/Session/ObserveAuthSessionUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/Auth/Session/ObserveAuthSessionUseCaseImpl.swift index 6dc3054f..68b19f98 100644 --- a/Application/DevLogDomain/Sources/UseCase/Auth/Session/ObserveAuthSessionUseCaseImpl.swift +++ b/Application/DevLogDomain/Sources/UseCase/Auth/Session/ObserveAuthSessionUseCaseImpl.swift @@ -10,11 +10,11 @@ import Combine public final class ObserveAuthSessionUseCaseImpl: ObserveAuthSessionUseCase { private let repository: AuthSessionRepository - public func observe() -> AnyPublisher { - repository.observeSignedIn() - } - init(_ repository: AuthSessionRepository) { self.repository = repository } + + public func observe() -> AnyPublisher { + repository.observeSignedIn() + } } From b479acd1593c9e5d4d9d3f7ff55499e34b69594e Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:04:33 +0900 Subject: [PATCH 07/19] =?UTF-8?q?fix:=20TodoCategoryPreference=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DataAssembler.swift | 4 +- .../AuthSessionRepositoryImpl.swift | 46 ++++- .../TodoCategoryRepositoryImpl.swift | 4 + .../Repository/TodoRepositoryImpl.swift | 4 +- .../AuthSessionRepositoryImplTests.swift | 181 ++++++++++++++++++ .../Repository/TodoRepositoryImplTests.swift | 43 +++++ .../UserPreferencesRepositoryImplTests.swift | 8 + 7 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift index 431d0c8a..796503de 100644 --- a/Application/DevLogData/Sources/DataAssembler.swift +++ b/Application/DevLogData/Sources/DataAssembler.swift @@ -61,7 +61,9 @@ public final class DataAssembler: Assembler { container.register(AuthSessionRepository.self) { AuthSessionRepositoryImpl( - authService: container.resolve(AuthService.self) + authService: container.resolve(AuthService.self), + todoCategoryService: container.resolve(TodoCategoryService.self), + store: container.resolve(UserDefaultsStore.self) ) } diff --git a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift index f9b154d2..7be56e42 100644 --- a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift @@ -9,13 +9,57 @@ import Combine import DevLogDomain final class AuthSessionRepositoryImpl: AuthSessionRepository { + private enum Key { + static let preferences = "TodoCategory.preferences" + } + private let authService: AuthService + private let todoCategoryService: TodoCategoryService + private let store: UserDefaultsStore - init(authService: AuthService) { + init( + authService: AuthService, + todoCategoryService: TodoCategoryService, + store: UserDefaultsStore + ) { self.authService = authService + self.todoCategoryService = todoCategoryService + self.store = store } func observeSignedIn() -> AnyPublisher { authService.observeSignedIn() + .map { [self] isSignedIn in + Future { promise in + Task { + if isSignedIn { + await cachePreferencesIfNeeded() + } else { + clearPreferencesCache() + } + promise(.success(isSignedIn)) + } + } + } + .switchToLatest() + .eraseToAnyPublisher() + } +} + +private extension AuthSessionRepositoryImpl { + func cachePreferencesIfNeeded() async { + if store.value(forKey: Key.preferences) as [TodoCategoryPreferenceResponse]? != nil { + return + } + + guard let preferences = try? await todoCategoryService.fetchCategoryPreferences() else { + return + } + + store.setValue(preferences, forKey: Key.preferences) + } + + func clearPreferencesCache() { + store.setValue(Optional<[TodoCategoryPreferenceResponse]>.none, forKey: Key.preferences) } } diff --git a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift index 39e66e44..ee152839 100644 --- a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift @@ -25,6 +25,10 @@ final class TodoCategoryRepositoryImpl: TodoCategoryRepository { func fetchCategoryPreferences() async throws -> [TodoCategoryPreference] { do { + if let preferences: [TodoCategoryPreferenceResponse] = store.value(forKey: Key.preferences) { + return preferences.toDomain() + } + let responses = try await todoCategoryService.fetchCategoryPreferences() store.setValue(responses, forKey: Key.preferences) return responses.toDomain() diff --git a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift index 6008c1eb..500610e4 100644 --- a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift @@ -71,7 +71,7 @@ final class TodoRepositoryImpl: TodoRepository { func fetchTodo(_ todoId: String) async throws -> Todo { do { async let response = todoService.fetchTodo(todoId: todoId) - async let preferences = todoCategoryService.fetchCategoryPreferences() + async let preferences = todoCategoryPreferenceResponses() let (todoResponse, todoPreferenceResponses) = try await ( response, @@ -96,7 +96,7 @@ final class TodoRepositoryImpl: TodoRepository { func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] { do { async let responseTask = todoService.fetchReferences(numbers) - async let preferencesTask = todoCategoryService.fetchCategoryPreferences() + async let preferencesTask = todoCategoryPreferenceResponses() let (responses, preferenceResponses) = try await ( responseTask, diff --git a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift new file mode 100644 index 00000000..95a35a6b --- /dev/null +++ b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift @@ -0,0 +1,181 @@ +// +// AuthSessionRepositoryImplTests.swift +// DevLogDataTests +// +// Created by opfic on 6/8/26. +// + +import Combine +import Foundation +import Testing +@testable @preconcurrency import DevLogData + +struct AuthSessionRepositoryImplTests { + private enum Key { + static let preferences = "TodoCategory.preferences" + } + + @Test("로그인 세션 true는 category preference 캐싱 후 방출한다") + func 로그인_세션_true는_category_preference_캐싱_후_방출한다() async throws { + let preference = makePreferenceResponse() + let authService = AuthSessionAuthServiceSpy() + let todoCategoryService = AuthSessionTodoCategoryServiceSpy(preferences: [preference]) + let store = AuthSessionUserDefaultsStoreSpy() + let repository = AuthSessionRepositoryImpl( + authService: authService, + todoCategoryService: todoCategoryService, + store: store + ) + let valueTask = Task { + for await value in repository.observeSignedIn().values where value { + return value + } + return false + } + + authService.send(true) + + #expect(await valueTask.value) + #expect(store.value(forKey: Key.preferences) == [preference]) + #expect(await todoCategoryService.fetchCategoryPreferencesCallCount() == 1) + } + + @Test("로그아웃 세션은 category preference 캐시를 제거한다") + func 로그아웃_세션은_category_preference_캐시를_제거한다() async throws { + let preference = makePreferenceResponse() + let authService = AuthSessionAuthServiceSpy(isSignedIn: true) + let todoCategoryService = AuthSessionTodoCategoryServiceSpy(preferences: [preference]) + let store = AuthSessionUserDefaultsStoreSpy() + store.setValue([preference], forKey: Key.preferences) + let repository = AuthSessionRepositoryImpl( + authService: authService, + todoCategoryService: todoCategoryService, + store: store + ) + let valueTask = Task { + for await value in repository.observeSignedIn().values where value == false { + return value + } + return true + } + + authService.send(false) + + #expect(await valueTask.value == false) + #expect(store.value(forKey: Key.preferences) == Optional<[TodoCategoryPreferenceResponse]>.none) + } + + private func makePreferenceResponse() -> TodoCategoryPreferenceResponse { + TodoCategoryPreferenceResponse( + category: .user( + TodoCategoryPreferenceResponse.UserCategory( + id: "user-category-id", + name: "User Category", + colorHex: "#FFFFFF" + ) + ), + isVisible: true + ) + } +} + +private final class AuthSessionAuthServiceSpy: AuthService { + private let subject: CurrentValueSubject + + var uid: String? { nil } + var providerIDs: [String] { [] } + var currentUserEmail: String? { nil } + var providerCount: Int { 0 } + + init(isSignedIn: Bool = false) { + self.subject = CurrentValueSubject(isSignedIn) + } + + func observeSignedIn() -> AnyPublisher { + subject.eraseToAnyPublisher() + } + + func beginSignIn() { } + func completeSignIn() { } + func cancelSignIn() { } + func getProviderID() async throws -> String? { nil } + func deleteCurrentUser() async throws { } + func clearCurrentSession() async throws { } + + func send(_ isSignedIn: Bool) { + subject.send(isSignedIn) + } +} + +private actor AuthSessionTodoCategoryServiceSpy: TodoCategoryService { + private let preferences: [TodoCategoryPreferenceResponse] + private let fetchDelay: Duration? + private var fetchCategoryPreferencesCount = 0 + + init( + preferences: [TodoCategoryPreferenceResponse], + fetchDelay: Duration? = nil + ) { + self.preferences = preferences + self.fetchDelay = fetchDelay + } + + func fetchCategoryPreferences() async throws -> [TodoCategoryPreferenceResponse] { + fetchCategoryPreferencesCount += 1 + if let fetchDelay { + try await Task.sleep(for: fetchDelay) + } + return preferences + } + + func updateCategoryPreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws { } + + func fetchCategoryPreferencesCallCount() -> Int { + fetchCategoryPreferencesCount + } +} + +private final class AuthSessionUserDefaultsStoreSpy: UserDefaultsStore { + private var values = [String: Data]() + + func value(forKey key: String) -> T? { + guard let data = values[key] else { + return nil + } + + return try? JSONDecoder().decode(T.self, from: data) + } + + func setValue(_ value: T?, forKey key: String) { + guard let value else { + values.removeValue(forKey: key) + return + } + + values[key] = try? JSONEncoder().encode(value) + } + + func removeValues(withPrefix prefix: String) { + values.keys + .filter { $0.hasPrefix(prefix) } + .forEach { values.removeValue(forKey: $0) } + } + + func string(forKey key: String) -> String? { + nil + } + + func setString(_ value: String?, forKey key: String) { } + + func stringArray(forKey key: String) -> [String] { + [] + } + + func setStringArray(_ value: [String], forKey key: String) { } + + func bool(forKey key: String) -> Bool { + false + } + + func setBool(_ value: Bool, forKey key: String) { } +} diff --git a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift index ea48cebf..f17f194b 100644 --- a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift @@ -65,11 +65,13 @@ struct TodoRepositoryImplTests { private func makeFixture() -> Fixture { let todoService = TodoServiceSpy() let todoCategoryService = TodoCategoryServiceSpy() + let store = TodoRepositoryUserDefaultsStoreSpy() let widgetSyncEventBus = WidgetSyncEventBusSpy() let todoMutationEventBus = TodoMutationEventBusSpy() let repository = TodoRepositoryImpl( todoService: todoService, todoCategoryService: todoCategoryService, + store: store, widgetSyncEventBus: widgetSyncEventBus, todoMutationEventBus: todoMutationEventBus ) @@ -157,6 +159,47 @@ private struct TodoCategoryServiceSpy: TodoCategoryService { } } +private final class TodoRepositoryUserDefaultsStoreSpy: UserDefaultsStore { + private var values = [String: Any]() + + func value(forKey key: String) -> T? { + values[key] as? T + } + + func setValue(_ value: T?, forKey key: String) { + guard let value else { + values.removeValue(forKey: key) + return + } + + values[key] = value + } + + func removeValues(withPrefix prefix: String) { + values.keys + .filter { $0.hasPrefix(prefix) } + .forEach { values.removeValue(forKey: $0) } + } + + func string(forKey key: String) -> String? { + nil + } + + func setString(_ value: String?, forKey key: String) { } + + func stringArray(forKey key: String) -> [String] { + [] + } + + func setStringArray(_ value: [String], forKey key: String) { } + + func bool(forKey key: String) -> Bool { + false + } + + func setBool(_ value: Bool, forKey key: String) { } +} + private final class WidgetSyncEventBusSpy: WidgetSyncEventBus { private(set) var events = [WidgetSyncEvent]() diff --git a/Application/DevLogData/Tests/Repository/UserPreferencesRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/UserPreferencesRepositoryImplTests.swift index da1a6532..65a0ea8b 100644 --- a/Application/DevLogData/Tests/Repository/UserPreferencesRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/UserPreferencesRepositoryImplTests.swift @@ -43,6 +43,14 @@ struct UserPreferencesRepositoryImplTests { } private final class UserDefaultsStoreSpy: UserDefaultsStore { + func value(forKey key: String) -> T? { + nil + } + + func setValue(_ value: T?, forKey key: String) { } + + func removeValues(withPrefix prefix: String) { } + func string(forKey key: String) -> String? { nil } From 8f3adf8933027faadbf9293c461e1b048f7d2e0b Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:30:00 +0900 Subject: [PATCH 08/19] =?UTF-8?q?refactor:=20swift=206=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Repository/AuthSessionRepositoryImpl.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift index 7be56e42..9913303a 100644 --- a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift @@ -33,9 +33,9 @@ final class AuthSessionRepositoryImpl: AuthSessionRepository { Future { promise in Task { if isSignedIn { - await cachePreferencesIfNeeded() + await self.cachePreferencesIfNeeded() } else { - clearPreferencesCache() + self.clearPreferencesCache() } promise(.success(isSignedIn)) } From ca68f88602af9036f48c63af3eac7b2bb75a587e Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:07:06 +0900 Subject: [PATCH 09/19] =?UTF-8?q?refactor:=20preconcurrency=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/Repository/AuthSessionRepositoryImplTests.swift | 2 +- .../DevLogData/Tests/Repository/TodoRepositoryImplTests.swift | 2 +- Application/DevLogPresentation/Sources/Login/LoginFeature.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift index 95a35a6b..f9fbf9cf 100644 --- a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift @@ -8,7 +8,7 @@ import Combine import Foundation import Testing -@testable @preconcurrency import DevLogData +@testable import DevLogData struct AuthSessionRepositoryImplTests { private enum Key { diff --git a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift index f17f194b..bfe041ba 100644 --- a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift @@ -10,7 +10,7 @@ import Foundation import Testing import DevLogCore import DevLogDomain -@testable @preconcurrency import DevLogData +@testable import DevLogData struct TodoRepositoryImplTests { @Test("Todo 변경 성공 시 위젯 동기화와 mutation 이벤트를 발행한다") diff --git a/Application/DevLogPresentation/Sources/Login/LoginFeature.swift b/Application/DevLogPresentation/Sources/Login/LoginFeature.swift index d6f69006..4ff8d5c5 100644 --- a/Application/DevLogPresentation/Sources/Login/LoginFeature.swift +++ b/Application/DevLogPresentation/Sources/Login/LoginFeature.swift @@ -6,7 +6,7 @@ // import ComposableArchitecture -@preconcurrency import DevLogDomain +import DevLogDomain import Foundation @Reducer From 30e794ef1ff80730d5a5acb59412e6a29e98c26d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:23:51 +0900 Subject: [PATCH 10/19] =?UTF-8?q?fix:=20Auth=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=ED=9B=84=20=EC=83=81=ED=83=9C=20=EC=A0=84?= =?UTF-8?q?=ED=8C=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DataAssembler.swift | 3 ++- .../Protocol/AuthSessionStateProvider.swift | 13 ++++++++++ .../AuthSessionRepositoryImpl.swift | 6 ++++- .../AuthSessionRepositoryImplTests.swift | 22 ++++++++++++++-- .../AuthSessionStateProviderImpl.swift | 25 +++++++++++++++++++ .../Sources/Widget/WidgetAssembler.swift | 4 +++ 6 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 Application/DevLogData/Sources/Protocol/AuthSessionStateProvider.swift create mode 100644 Application/DevLogWidget/Sources/Provider/AuthSessionStateProviderImpl.swift diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift index 796503de..a052bfb6 100644 --- a/Application/DevLogData/Sources/DataAssembler.swift +++ b/Application/DevLogData/Sources/DataAssembler.swift @@ -63,7 +63,8 @@ public final class DataAssembler: Assembler { AuthSessionRepositoryImpl( authService: container.resolve(AuthService.self), todoCategoryService: container.resolve(TodoCategoryService.self), - store: container.resolve(UserDefaultsStore.self) + store: container.resolve(UserDefaultsStore.self), + provider: container.resolve(AuthSessionStateProvider.self) ) } diff --git a/Application/DevLogData/Sources/Protocol/AuthSessionStateProvider.swift b/Application/DevLogData/Sources/Protocol/AuthSessionStateProvider.swift new file mode 100644 index 00000000..6a6128e5 --- /dev/null +++ b/Application/DevLogData/Sources/Protocol/AuthSessionStateProvider.swift @@ -0,0 +1,13 @@ +// +// AuthSessionStateProvider.swift +// DevLogData +// +// Created by opfic on 6/9/26. +// + +import Combine + +public protocol AuthSessionStateProvider { + func publish(_ isSignedIn: Bool) + func observeSignedIn() -> AnyPublisher +} diff --git a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift index 9913303a..a44d9477 100644 --- a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift @@ -16,15 +16,18 @@ final class AuthSessionRepositoryImpl: AuthSessionRepository { private let authService: AuthService private let todoCategoryService: TodoCategoryService private let store: UserDefaultsStore + private let provider: AuthSessionStateProvider init( authService: AuthService, todoCategoryService: TodoCategoryService, - store: UserDefaultsStore + store: UserDefaultsStore, + provider: AuthSessionStateProvider ) { self.authService = authService self.todoCategoryService = todoCategoryService self.store = store + self.provider = provider } func observeSignedIn() -> AnyPublisher { @@ -37,6 +40,7 @@ final class AuthSessionRepositoryImpl: AuthSessionRepository { } else { self.clearPreferencesCache() } + self.provider.publish(isSignedIn) promise(.success(isSignedIn)) } } diff --git a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift index f9fbf9cf..d7f67d07 100644 --- a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift @@ -21,10 +21,12 @@ struct AuthSessionRepositoryImplTests { let authService = AuthSessionAuthServiceSpy() let todoCategoryService = AuthSessionTodoCategoryServiceSpy(preferences: [preference]) let store = AuthSessionUserDefaultsStoreSpy() + let provider = AuthSessionStateProviderSpy() let repository = AuthSessionRepositoryImpl( authService: authService, todoCategoryService: todoCategoryService, - store: store + store: store, + provider: provider ) let valueTask = Task { for await value in repository.observeSignedIn().values where value { @@ -38,6 +40,7 @@ struct AuthSessionRepositoryImplTests { #expect(await valueTask.value) #expect(store.value(forKey: Key.preferences) == [preference]) #expect(await todoCategoryService.fetchCategoryPreferencesCallCount() == 1) + #expect(provider.events == [true]) } @Test("로그아웃 세션은 category preference 캐시를 제거한다") @@ -46,11 +49,13 @@ struct AuthSessionRepositoryImplTests { let authService = AuthSessionAuthServiceSpy(isSignedIn: true) let todoCategoryService = AuthSessionTodoCategoryServiceSpy(preferences: [preference]) let store = AuthSessionUserDefaultsStoreSpy() + let provider = AuthSessionStateProviderSpy() store.setValue([preference], forKey: Key.preferences) let repository = AuthSessionRepositoryImpl( authService: authService, todoCategoryService: todoCategoryService, - store: store + store: store, + provider: provider ) let valueTask = Task { for await value in repository.observeSignedIn().values where value == false { @@ -63,6 +68,7 @@ struct AuthSessionRepositoryImplTests { #expect(await valueTask.value == false) #expect(store.value(forKey: Key.preferences) == Optional<[TodoCategoryPreferenceResponse]>.none) + #expect(provider.events == [false]) } private func makePreferenceResponse() -> TodoCategoryPreferenceResponse { @@ -79,6 +85,18 @@ struct AuthSessionRepositoryImplTests { } } +private final class AuthSessionStateProviderSpy: AuthSessionStateProvider { + private(set) var events = [Bool]() + + func publish(_ isSignedIn: Bool) { + events.append(isSignedIn) + } + + func observeSignedIn() -> AnyPublisher { + Empty().eraseToAnyPublisher() + } +} + private final class AuthSessionAuthServiceSpy: AuthService { private let subject: CurrentValueSubject diff --git a/Application/DevLogWidget/Sources/Provider/AuthSessionStateProviderImpl.swift b/Application/DevLogWidget/Sources/Provider/AuthSessionStateProviderImpl.swift new file mode 100644 index 00000000..b0307bbe --- /dev/null +++ b/Application/DevLogWidget/Sources/Provider/AuthSessionStateProviderImpl.swift @@ -0,0 +1,25 @@ +// +// AuthSessionStateProviderImpl.swift +// DevLogWidget +// +// Created by opfic on 6/9/26. +// + +import Combine +import DevLogData + +public final class AuthSessionStateProviderImpl: AuthSessionStateProvider { + private let subject = CurrentValueSubject(nil) + + public init() { } + + public func publish(_ isSignedIn: Bool) { + subject.send(isSignedIn) + } + + public func observeSignedIn() -> AnyPublisher { + subject + .compactMap { $0 } + .eraseToAnyPublisher() + } +} diff --git a/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift b/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift index 6c09331d..f8ef5596 100644 --- a/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift +++ b/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift @@ -12,6 +12,10 @@ public final class WidgetAssembler: Assembler { public init() { } public func assemble(_ container: any DIContainer) { + container.register(AuthSessionStateProvider.self) { + AuthSessionStateProviderImpl() + } + container.register(WidgetSyncEventBus.self) { WidgetSyncEventBusImpl() } From d6ececb4e35505aa3c6739f40151f165d4c8e343 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:32:57 +0900 Subject: [PATCH 11/19] =?UTF-8?q?fix:=20=EC=9C=84=EC=A0=AF=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Handler/WidgetSessionSyncHandler.swift | 8 ++-- .../Sources/Widget/WidgetAssembler.swift | 2 +- .../WidgetSessionSyncHandlerTests.swift | 40 +++++++------------ 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/Application/DevLogWidget/Sources/Handler/WidgetSessionSyncHandler.swift b/Application/DevLogWidget/Sources/Handler/WidgetSessionSyncHandler.swift index 824e971b..bfc8beea 100644 --- a/Application/DevLogWidget/Sources/Handler/WidgetSessionSyncHandler.swift +++ b/Application/DevLogWidget/Sources/Handler/WidgetSessionSyncHandler.swift @@ -10,19 +10,19 @@ import Foundation import DevLogData public final class WidgetSessionSyncHandler { - private let authService: AuthService + private let provider: AuthSessionStateProvider private let widgetSyncEventBus: WidgetSyncEventBus private var hasRequestedWidgetSync = false private var cancellables = Set() public init( - authService: AuthService, + provider: AuthSessionStateProvider, widgetSyncEventBus: WidgetSyncEventBus ) { - self.authService = authService + self.provider = provider self.widgetSyncEventBus = widgetSyncEventBus - authService.observeSignedIn() + provider.observeSignedIn() .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] isSignedIn in diff --git a/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift b/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift index f8ef5596..61244806 100644 --- a/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift +++ b/Application/DevLogWidget/Sources/Widget/WidgetAssembler.swift @@ -28,7 +28,7 @@ public final class WidgetAssembler: Assembler { } container.register(WidgetSessionSyncHandler.self) { WidgetSessionSyncHandler( - authService: container.resolve(AuthService.self), + provider: container.resolve(AuthSessionStateProvider.self), widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self) ) } diff --git a/Application/DevLogWidget/Tests/Handler/WidgetSessionSyncHandlerTests.swift b/Application/DevLogWidget/Tests/Handler/WidgetSessionSyncHandlerTests.swift index af3b4f9a..45c647dc 100644 --- a/Application/DevLogWidget/Tests/Handler/WidgetSessionSyncHandlerTests.swift +++ b/Application/DevLogWidget/Tests/Handler/WidgetSessionSyncHandlerTests.swift @@ -14,15 +14,15 @@ import DevLogData struct WidgetSessionSyncHandlerTests { @Test("로그인 세션 true 첫 진입에서만 위젯 초기 동기화를 요청한다") func 로그인_세션_true_첫_진입에서만_위젯_초기_동기화를_요청한다() async { - let authServiceSpy = AuthServiceSpy() + let provider = AuthSessionStateProviderSpy() let widgetSyncEventBusSpy = WidgetSyncEventBusSpy() let widgetSessionSyncHandler = WidgetSessionSyncHandler( - authService: authServiceSpy, + provider: provider, widgetSyncEventBus: widgetSyncEventBusSpy ) - authServiceSpy.send(true) - authServiceSpy.send(true) + provider.publish(true) + provider.publish(true) await withCheckedContinuation { continuation in DispatchQueue.main.async { @@ -35,16 +35,16 @@ struct WidgetSessionSyncHandlerTests { @Test("로그아웃 이후 재로그인 시 위젯 초기 동기화를 다시 요청한다") func 로그아웃_이후_재로그인_시_위젯_초기_동기화를_다시_요청한다() async { - let authServiceSpy = AuthServiceSpy() + let provider = AuthSessionStateProviderSpy() let widgetSyncEventBusSpy = WidgetSyncEventBusSpy() let widgetSessionSyncHandler = WidgetSessionSyncHandler( - authService: authServiceSpy, + provider: provider, widgetSyncEventBus: widgetSyncEventBusSpy ) - authServiceSpy.send(true) - authServiceSpy.send(false) - authServiceSpy.send(true) + provider.publish(true) + provider.publish(false) + provider.publish(true) await withCheckedContinuation { continuation in DispatchQueue.main.async { @@ -56,27 +56,15 @@ struct WidgetSessionSyncHandlerTests { } } -private final class AuthServiceSpy: AuthService { - private let currentValueSubject = CurrentValueSubject(false) - - var uid: String? { nil } - var providerIDs: [String] { [] } - var currentUserEmail: String? { nil } - var providerCount: Int { 0 } +private final class AuthSessionStateProviderSpy: AuthSessionStateProvider { + private let subject = PassthroughSubject() func observeSignedIn() -> AnyPublisher { - currentValueSubject.eraseToAnyPublisher() + subject.eraseToAnyPublisher() } - func beginSignIn() { } - func completeSignIn() { } - func cancelSignIn() { } - func getProviderID() async throws -> String? { nil } - func deleteCurrentUser() async throws { } - func clearCurrentSession() async throws { } - - func send(_ isSignedIn: Bool) { - currentValueSubject.send(isSignedIn) + func publish(_ isSignedIn: Bool) { + subject.send(isSignedIn) } } From 2d436d2385f46ff0f6d3bd6ed741280a2080e281 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:51:50 +0900 Subject: [PATCH 12/19] =?UTF-8?q?refactor:=20=EC=9C=84=EC=A0=AF=20Todo=20s?= =?UTF-8?q?napshot=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DataAssembler.swift | 4 +- .../WidgetTodoSnapshotRepositoryImpl.swift | 69 ++++++++----- ...idgetTodoSnapshotRepositoryImplTests.swift | 96 +++++++++---------- 3 files changed, 91 insertions(+), 78 deletions(-) diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift index a052bfb6..9efc982b 100644 --- a/Application/DevLogData/Sources/DataAssembler.swift +++ b/Application/DevLogData/Sources/DataAssembler.swift @@ -47,9 +47,7 @@ public final class DataAssembler: Assembler { } container.register(WidgetTodoSnapshotRepository.self) { - WidgetTodoSnapshotRepositoryImpl( - repository: container.resolve(TodoRepository.self) - ) + WidgetTodoSnapshotRepositoryImpl(todoService: container.resolve(TodoService.self)) } container.register(TodoCategoryRepository.self) { diff --git a/Application/DevLogData/Sources/Repository/WidgetTodoSnapshotRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/WidgetTodoSnapshotRepositoryImpl.swift index b628c600..8df7be08 100644 --- a/Application/DevLogData/Sources/Repository/WidgetTodoSnapshotRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/WidgetTodoSnapshotRepositoryImpl.swift @@ -10,10 +10,10 @@ import DevLogCore import DevLogDomain final class WidgetTodoSnapshotRepositoryImpl: WidgetTodoSnapshotRepository { - private let repository: TodoRepository + private let todoService: TodoService - init(repository: TodoRepository) { - self.repository = repository + init(todoService: TodoService) { + self.todoService = todoService } func fetchTodayTodos( @@ -22,19 +22,21 @@ final class WidgetTodoSnapshotRepositoryImpl: WidgetTodoSnapshotRepository { sortOrder: TodoQuery.SortOrder, pageSize: Int ) async throws -> [WidgetTodoSnapshot] { - let todoPage = try await repository.fetchTodos( - TodoQuery( - completionFilter: .incomplete, - dueDateFilter: dueDateFilter, - sortTarget: sortTarget, - sortOrder: sortOrder, - pageSize: pageSize, - fetchAllPages: true - ), - cursor: nil + let query = TodoQuery( + completionFilter: .incomplete, + dueDateFilter: dueDateFilter, + sortTarget: sortTarget, + sortOrder: sortOrder, + pageSize: pageSize, + fetchAllPages: true ) - return todoPage.items.map(WidgetTodoSnapshot.fromDomain) + do { + let todoPage = try await todoService.fetchTodos(query, cursor: nil) + return todoPage.items.map(WidgetTodoSnapshot.fromResponse) + } catch { + throw error.toDomain() + } } func fetchHeatmapTodos( @@ -43,18 +45,35 @@ final class WidgetTodoSnapshotRepositoryImpl: WidgetTodoSnapshotRepository { nextQuarterStart: Date, pageSize: Int ) async throws -> [WidgetTodoSnapshot] { - let todoPage = try await repository.fetchTodos( - TodoQuery( - sortDateFrom: quarterStart, - sortDateTo: nextQuarterStart, - includesDeleted: true, - sortTarget: sortTarget, - pageSize: pageSize, - fetchAllPages: true - ), - cursor: nil + let query = TodoQuery( + sortDateFrom: quarterStart, + sortDateTo: nextQuarterStart, + includesDeleted: true, + sortTarget: sortTarget, + pageSize: pageSize, + fetchAllPages: true ) - return todoPage.items.map(WidgetTodoSnapshot.fromDomain) + do { + let todoPage = try await todoService.fetchTodos(query, cursor: nil) + return todoPage.items.map(WidgetTodoSnapshot.fromResponse) + } catch { + throw error.toDomain() + } + } +} + +private extension WidgetTodoSnapshot { + static func fromResponse(_ response: TodoResponse) -> Self { + WidgetTodoSnapshot( + id: response.id, + number: response.number, + title: response.title, + isPinned: response.isPinned, + createdAt: response.createdAt, + completedAt: response.completedAt, + deletedAt: response.deletedAt, + dueDate: response.dueDate + ) } } diff --git a/Application/DevLogData/Tests/Repository/WidgetTodoSnapshotRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/WidgetTodoSnapshotRepositoryImplTests.swift index 93f0922d..2efbb274 100644 --- a/Application/DevLogData/Tests/Repository/WidgetTodoSnapshotRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/WidgetTodoSnapshotRepositoryImplTests.swift @@ -12,14 +12,14 @@ import DevLogDomain @testable import DevLogData struct WidgetTodoSnapshotRepositoryImplTests { - @Test("Today 위젯 Todo 조회는 기존 TodoRepository query와 snapshot 매핑을 사용한다") - func today_위젯_todo_조회는_기존_todorepository_query와_snapshot_매핑을_사용한다() async throws { - let repositorySpy = TodoRepositorySpy() - let repository = WidgetTodoSnapshotRepositoryImpl(repository: repositorySpy) + @Test("Today 위젯 Todo 조회는 TodoService query와 snapshot 매핑을 사용한다") + func today_위젯_todo_조회는_todoservice_query와_snapshot_매핑을_사용한다() async throws { + let todoServiceSpy = WidgetTodoSnapshotTodoServiceSpy() + let repository = WidgetTodoSnapshotRepositoryImpl(todoService: todoServiceSpy) let now = Date(timeIntervalSince1970: 100) - let todo = makeTodo(id: "today", createdAt: now, dueDate: now) + let todo = makeTodoResponse(id: "today", createdAt: now, dueDate: now) - await repositorySpy.setTodos([todo], for: .dueDate) + await todoServiceSpy.setTodos([todo], for: .dueDate) let snapshots = try await repository.fetchTodayTodos( dueDateFilter: .withDueDate, @@ -27,7 +27,7 @@ struct WidgetTodoSnapshotRepositoryImplTests { sortOrder: .oldest, pageSize: 100 ) - let queries = await repositorySpy.calledQueries() + let queries = await todoServiceSpy.calledQueries() #expect(snapshots == [makeSnapshot(id: "today", createdAt: now, dueDate: now)]) #expect(queries == [ @@ -42,15 +42,15 @@ struct WidgetTodoSnapshotRepositoryImplTests { ]) } - @Test("Heatmap 위젯 Todo 조회는 기존 TodoRepository query와 snapshot 매핑을 사용한다") - func heatmap_위젯_todo_조회는_기존_todorepository_query와_snapshot_매핑을_사용한다() async throws { - let repositorySpy = TodoRepositorySpy() - let repository = WidgetTodoSnapshotRepositoryImpl(repository: repositorySpy) + @Test("Heatmap 위젯 Todo 조회는 TodoService query와 snapshot 매핑을 사용한다") + func heatmap_위젯_todo_조회는_todoservice_query와_snapshot_매핑을_사용한다() async throws { + let todoServiceSpy = WidgetTodoSnapshotTodoServiceSpy() + let repository = WidgetTodoSnapshotRepositoryImpl(todoService: todoServiceSpy) let quarterStart = Date(timeIntervalSince1970: 100) let nextQuarterStart = Date(timeIntervalSince1970: 200) - let todo = makeTodo(id: "created", createdAt: quarterStart) + let todo = makeTodoResponse(id: "created", createdAt: quarterStart) - await repositorySpy.setTodos([todo], for: .createdAt) + await todoServiceSpy.setTodos([todo], for: .createdAt) let snapshots = try await repository.fetchHeatmapTodos( sortTarget: .createdAt, @@ -58,7 +58,7 @@ struct WidgetTodoSnapshotRepositoryImplTests { nextQuarterStart: nextQuarterStart, pageSize: 100 ) - let queries = await repositorySpy.calledQueries() + let queries = await todoServiceSpy.calledQueries() #expect(snapshots == [makeSnapshot(id: "created", createdAt: quarterStart)]) #expect(queries == [ @@ -73,90 +73,86 @@ struct WidgetTodoSnapshotRepositoryImplTests { ]) } - private func makeTodo( + private func makeSnapshot( id: String, createdAt: Date, completedAt: Date? = nil, deletedAt: Date? = nil, dueDate: Date? = nil - ) -> Todo { - Todo( + ) -> WidgetTodoSnapshot { + WidgetTodoSnapshot( id: id, - isPinned: false, - isCompleted: completedAt != nil, - isChecked: false, number: 1, title: id, - content: "", + isPinned: false, createdAt: createdAt, - updatedAt: createdAt, completedAt: completedAt, deletedAt: deletedAt, - dueDate: dueDate, - tags: [], - category: .system(.feature) + dueDate: dueDate ) } - private func makeSnapshot( + private func makeTodoResponse( id: String, createdAt: Date, completedAt: Date? = nil, deletedAt: Date? = nil, dueDate: Date? = nil - ) -> WidgetTodoSnapshot { - WidgetTodoSnapshot( + ) -> TodoResponse { + TodoResponse( id: id, + isPinned: false, + isCompleted: completedAt != nil, + isChecked: false, number: 1, title: id, - isPinned: false, + content: "", createdAt: createdAt, + updatedAt: createdAt, completedAt: completedAt, deletedAt: deletedAt, - dueDate: dueDate + dueDate: dueDate, + tags: [], + category: .raw(SystemTodoCategory.feature.rawValue) ) } } -private actor TodoRepositorySpy: TodoRepository { +private actor WidgetTodoSnapshotTodoServiceSpy: TodoService { private var queries = [TodoQuery]() - private var todosBySortTarget = [TodoQuery.SortTarget: [Todo]]() + private var todosBySortTarget = [TodoQuery.SortTarget: [TodoResponse]]() - func setTodos(_ todos: [Todo], for sortTarget: TodoQuery.SortTarget) { + func setTodos(_ todos: [TodoResponse], for sortTarget: TodoQuery.SortTarget) { todosBySortTarget[sortTarget] = todos } - func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage { + func fetchTodos(_ query: TodoQuery, cursor: TodoCursorDTO?) async throws -> TodoPageResponse { queries.append(query) - return TodoPage( + return TodoPageResponse( items: todosBySortTarget[query.sortTarget] ?? [], nextCursor: nil ) } - func fetchTodo(_ todoId: String) async throws -> Todo { - throw TodoRepositorySpyError.unexpectedCall - } - - func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] { - throw TodoRepositorySpyError.unexpectedCall + func upsertTodo(request: TodoRequest) async throws { + throw WidgetTodoSnapshotTodoServiceSpyError.unexpectedCall } - func upsertTodo(_ todo: Todo) async throws { - throw TodoRepositorySpyError.unexpectedCall + func deleteTodo(todoId: String) async throws { + throw WidgetTodoSnapshotTodoServiceSpyError.unexpectedCall } - func upsertTodo(_ todoDraft: TodoDraft) async throws { - throw TodoRepositorySpyError.unexpectedCall + func undoDeleteTodo(todoId: String) async throws { + throw WidgetTodoSnapshotTodoServiceSpyError.unexpectedCall } - func deleteTodo(_ todoId: String) async throws { - throw TodoRepositorySpyError.unexpectedCall + func fetchTodo(todoId: String) async throws -> TodoResponse { + throw WidgetTodoSnapshotTodoServiceSpyError.unexpectedCall } - func undoDeleteTodo(_ todoId: String) async throws { - throw TodoRepositorySpyError.unexpectedCall + func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReferenceResponse] { + throw WidgetTodoSnapshotTodoServiceSpyError.unexpectedCall } func calledQueries() -> [TodoQuery] { @@ -164,6 +160,6 @@ private actor TodoRepositorySpy: TodoRepository { } } -private enum TodoRepositorySpyError: Error { +private enum WidgetTodoSnapshotTodoServiceSpyError: Error { case unexpectedCall } From 878c6b6311fd7be6509276f256bb0d7ceea4d997 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 02:02:32 +0900 Subject: [PATCH 13/19] =?UTF-8?q?refactor:=20PushNotificationRepositoryImp?= =?UTF-8?q?l=EC=97=90=EC=84=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=EA=B4=80=EB=A0=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DataAssembler.swift | 3 ++- .../PushNotificationRepositoryImpl.swift | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift index 9efc982b..7b61da84 100644 --- a/Application/DevLogData/Sources/DataAssembler.swift +++ b/Application/DevLogData/Sources/DataAssembler.swift @@ -103,7 +103,8 @@ public final class DataAssembler: Assembler { container.register(PushNotificationRepository.self) { PushNotificationRepositoryImpl( pushNotificationService: container.resolve(PushNotificationService.self), - todoCategoryService: container.resolve(TodoCategoryService.self) + todoCategoryService: container.resolve(TodoCategoryService.self), + store: container.resolve(UserDefaultsStore.self) ) } diff --git a/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift index 97fa34e9..77b95e3f 100644 --- a/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift @@ -11,15 +11,22 @@ import DevLogCore import DevLogDomain final class PushNotificationRepositoryImpl: PushNotificationRepository { + private enum Key { + static let preferences = "TodoCategory.preferences" + } + private let pushNotificationService: PushNotificationService private let todoCategoryService: TodoCategoryService + private let store: UserDefaultsStore init( pushNotificationService: PushNotificationService, - todoCategoryService: TodoCategoryService + todoCategoryService: TodoCategoryService, + store: UserDefaultsStore ) { self.pushNotificationService = pushNotificationService self.todoCategoryService = todoCategoryService + self.store = store } /// 푸시 알림 On/Off 설정 @@ -59,7 +66,7 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { do { let cursorDTO = cursor.map { PushNotificationCursorDTO.fromDomain($0) } async let responseTask = pushNotificationService.requestNotifications(query, cursor: cursorDTO) - async let preferencesTask = todoCategoryService.fetchCategoryPreferences() + async let preferencesTask = todoCategoryPreferenceResponses() let (response, preferenceResponses) = try await (responseTask, preferencesTask) return try resolvePage(from: response, with: preferenceResponses.toDomain()) @@ -91,8 +98,7 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { Task { do { - let preferences = try await self.todoCategoryService - .fetchCategoryPreferences() + let preferences = try await self.todoCategoryPreferenceResponses() .toDomain() let page = try self.resolvePage(from: response, with: preferences) subject.send(page) @@ -149,6 +155,16 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { } private extension PushNotificationRepositoryImpl { + func todoCategoryPreferenceResponses() async throws -> [TodoCategoryPreferenceResponse] { + if let preferences: [TodoCategoryPreferenceResponse] = store.value(forKey: Key.preferences) { + return preferences + } + + let preferences = try await todoCategoryService.fetchCategoryPreferences() + store.setValue(preferences, forKey: Key.preferences) + return preferences + } + func resolvePage( from response: PushNotificationPageResponse, with preferences: [TodoCategoryPreference] From df90832cec21701c683c195be2acd3a4ff095bd4 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 02:33:36 +0900 Subject: [PATCH 14/19] =?UTF-8?q?fix:=20RootViewModel=20=EA=B5=AC=EB=8F=85?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Root/RootViewModel.swift | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Root/RootViewModel.swift b/Application/DevLogPresentation/Sources/Root/RootViewModel.swift index 28bacbff..405f395e 100644 --- a/Application/DevLogPresentation/Sources/Root/RootViewModel.swift +++ b/Application/DevLogPresentation/Sources/Root/RootViewModel.swift @@ -32,11 +32,17 @@ final class RootViewModel: StorePattern { enum SideEffect { case clearApplicationBadgeCount + case observeNetworkConnectivity + case observeSession + case observeTheme case trackLoginScreen } private(set) var state: State private var cancellables = Set() + private var isObservingNetworkConnectivity = false + private var isObservingSession = false + private var isObservingTheme = false private let sessionUseCase: ObserveAuthSessionUseCase private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase private let systemThemeUseCase: ObserveSystemThemeUseCase @@ -53,10 +59,6 @@ final class RootViewModel: StorePattern { self.systemThemeUseCase = systemThemeUseCase self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase self.state = State() - - setupNetworkObserving() - setupSessionObserving() - setupThemeObserving() } func reduce(with action: Action) -> [SideEffect] { @@ -65,7 +67,12 @@ final class RootViewModel: StorePattern { switch action { case .onAppear: - effects = [.clearApplicationBadgeCount] + effects = [ + .clearApplicationBadgeCount, + .observeNetworkConnectivity, + .observeSession, + .observeTheme + ] case .setAlert(let isPresented): setAlert(&state, isPresented: isPresented) case .networkStatusChanged(let isConnected): @@ -91,6 +98,12 @@ final class RootViewModel: StorePattern { switch effect { case .clearApplicationBadgeCount: UNUserNotificationCenter.current().setBadgeCount(0) { _ in } + case .observeNetworkConnectivity: + setupNetworkObserving() + case .observeSession: + setupSessionObserving() + case .observeTheme: + setupThemeObserving() case .trackLoginScreen: trackAnalyticsEventUseCase.execute(.screenView("login")) } @@ -109,6 +122,9 @@ private extension RootViewModel { } func setupNetworkObserving() { + guard !isObservingNetworkConnectivity else { return } + isObservingNetworkConnectivity = true + networkConnectivityUseCase.observe() .receive(on: DispatchQueue.main) .sink { [weak self] isConnected in @@ -118,6 +134,9 @@ private extension RootViewModel { } func setupSessionObserving() { + guard !isObservingSession else { return } + isObservingSession = true + sessionUseCase.observe() .removeDuplicates() .receive(on: DispatchQueue.main) @@ -128,6 +147,9 @@ private extension RootViewModel { } func setupThemeObserving() { + guard !isObservingTheme else { return } + isObservingTheme = true + systemThemeUseCase.observe() .removeDuplicates() .receive(on: DispatchQueue.main) From c5fded3e467f7cc170ab042bce5c8d05a16148c5 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:47:25 +0900 Subject: [PATCH 15/19] =?UTF-8?q?fix:=20Auth=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=B0=A9=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Repository/AuthSessionRepositoryImpl.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift index a44d9477..afcdf1f0 100644 --- a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift @@ -32,6 +32,7 @@ final class AuthSessionRepositoryImpl: AuthSessionRepository { func observeSignedIn() -> AnyPublisher { authService.observeSignedIn() + .removeDuplicates() .map { [self] isSignedIn in Future { promise in Task { From c1fb4611e8aca351995e423e9687c73033d8e41d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:27:01 +0900 Subject: [PATCH 16/19] =?UTF-8?q?feat:=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=ED=94=84=EB=A1=9C=ED=86=A0=EC=BD=9C,=20?= =?UTF-8?q?=EC=8B=A4=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Protocol/MemoryCacheStore.swift | 13 +++++++ .../Sources/Cache/MemoryCacheStoreImpl.swift | 38 +++++++++++++++++++ .../Sources/PersistenceAssembler.swift | 4 ++ 3 files changed, 55 insertions(+) create mode 100644 Application/DevLogData/Sources/Protocol/MemoryCacheStore.swift create mode 100644 Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift diff --git a/Application/DevLogData/Sources/Protocol/MemoryCacheStore.swift b/Application/DevLogData/Sources/Protocol/MemoryCacheStore.swift new file mode 100644 index 00000000..06893155 --- /dev/null +++ b/Application/DevLogData/Sources/Protocol/MemoryCacheStore.swift @@ -0,0 +1,13 @@ +// +// MemoryCacheStore.swift +// DevLogData +// +// Created by opfic on 6/9/26. +// + +import Foundation + +public protocol MemoryCacheStore { + func value(forKey key: String) -> T? + func setValue(_ value: T?, forKey key: String) +} diff --git a/Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift b/Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift new file mode 100644 index 00000000..65cc1e0e --- /dev/null +++ b/Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift @@ -0,0 +1,38 @@ +// +// MemoryCacheStoreImpl.swift +// DevLogPersistence +// +// Created by opfic on 6/9/26. +// + +import Foundation +import DevLogData + +final class MemoryCacheStoreImpl: MemoryCacheStore { + private let queue = DispatchQueue( + label: "devlog.memory-cache-store", + qos: .utility + ) + private var values = [String: Data]() + + func value(forKey key: String) -> T? { + queue.sync { + let decoder = JSONDecoder() + guard let data = self.values[key] else { return nil } + return try? decoder.decode(T.self, from: data) + } + } + + func setValue(_ value: T?, forKey key: String) { + queue.sync { + let encoder = JSONEncoder() + guard let value else { + self.values.removeValue(forKey: key) + return + } + + guard let data = try? encoder.encode(value) else { return } + self.values[key] = data + } + } +} diff --git a/Application/DevLogPersistence/Sources/PersistenceAssembler.swift b/Application/DevLogPersistence/Sources/PersistenceAssembler.swift index 2e472d83..ef7fdb05 100644 --- a/Application/DevLogPersistence/Sources/PersistenceAssembler.swift +++ b/Application/DevLogPersistence/Sources/PersistenceAssembler.swift @@ -17,6 +17,10 @@ public final class PersistenceAssembler: Assembler { UserDefaultsStoreImpl() } + container.register(MemoryCacheStore.self) { + MemoryCacheStoreImpl() + } + container.register(ThemeStore.self) { ThemeStoreImpl() } From 0f8ab1e8759ec752e1e312e6e8e9856348c3bdbd Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:36:10 +0900 Subject: [PATCH 17/19] =?UTF-8?q?refactor:=20=EB=A9=94=EB=AA=A8=EB=A6=AC?= =?UTF-8?q?=20=EC=BA=90=EC=8B=B1=EC=9C=BC=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogData/Sources/DataAssembler.swift | 8 ++--- .../AuthSessionRepositoryImpl.swift | 4 +-- .../PushNotificationRepositoryImpl.swift | 4 +-- .../TodoCategoryRepositoryImpl.swift | 4 +-- .../Repository/TodoRepositoryImpl.swift | 4 +-- .../AuthSessionRepositoryImplTests.swift | 30 ++----------------- .../Repository/TodoRepositoryImplTests.swift | 28 ++--------------- 7 files changed, 17 insertions(+), 65 deletions(-) diff --git a/Application/DevLogData/Sources/DataAssembler.swift b/Application/DevLogData/Sources/DataAssembler.swift index 7b61da84..42c7b2bd 100644 --- a/Application/DevLogData/Sources/DataAssembler.swift +++ b/Application/DevLogData/Sources/DataAssembler.swift @@ -40,7 +40,7 @@ public final class DataAssembler: Assembler { TodoRepositoryImpl( todoService: container.resolve(TodoService.self), todoCategoryService: container.resolve(TodoCategoryService.self), - store: container.resolve(UserDefaultsStore.self), + store: container.resolve(MemoryCacheStore.self), widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self), todoMutationEventBus: container.resolve(TodoMutationEventBus.self) ) @@ -53,7 +53,7 @@ public final class DataAssembler: Assembler { container.register(TodoCategoryRepository.self) { TodoCategoryRepositoryImpl( todoCategoryService: container.resolve(TodoCategoryService.self), - store: container.resolve(UserDefaultsStore.self) + store: container.resolve(MemoryCacheStore.self) ) } @@ -61,7 +61,7 @@ public final class DataAssembler: Assembler { AuthSessionRepositoryImpl( authService: container.resolve(AuthService.self), todoCategoryService: container.resolve(TodoCategoryService.self), - store: container.resolve(UserDefaultsStore.self), + store: container.resolve(MemoryCacheStore.self), provider: container.resolve(AuthSessionStateProvider.self) ) } @@ -104,7 +104,7 @@ public final class DataAssembler: Assembler { PushNotificationRepositoryImpl( pushNotificationService: container.resolve(PushNotificationService.self), todoCategoryService: container.resolve(TodoCategoryService.self), - store: container.resolve(UserDefaultsStore.self) + store: container.resolve(MemoryCacheStore.self) ) } diff --git a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift index afcdf1f0..fd99cde2 100644 --- a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift @@ -15,13 +15,13 @@ final class AuthSessionRepositoryImpl: AuthSessionRepository { private let authService: AuthService private let todoCategoryService: TodoCategoryService - private let store: UserDefaultsStore + private let store: MemoryCacheStore private let provider: AuthSessionStateProvider init( authService: AuthService, todoCategoryService: TodoCategoryService, - store: UserDefaultsStore, + store: MemoryCacheStore, provider: AuthSessionStateProvider ) { self.authService = authService diff --git a/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift index 77b95e3f..b3ffa34e 100644 --- a/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift @@ -17,12 +17,12 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { private let pushNotificationService: PushNotificationService private let todoCategoryService: TodoCategoryService - private let store: UserDefaultsStore + private let store: MemoryCacheStore init( pushNotificationService: PushNotificationService, todoCategoryService: TodoCategoryService, - store: UserDefaultsStore + store: MemoryCacheStore ) { self.pushNotificationService = pushNotificationService self.todoCategoryService = todoCategoryService diff --git a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift index ee152839..77977b36 100644 --- a/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift @@ -13,11 +13,11 @@ final class TodoCategoryRepositoryImpl: TodoCategoryRepository { } private let todoCategoryService: TodoCategoryService - private let store: UserDefaultsStore + private let store: MemoryCacheStore init( todoCategoryService: TodoCategoryService, - store: UserDefaultsStore + store: MemoryCacheStore ) { self.todoCategoryService = todoCategoryService self.store = store diff --git a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift index 500610e4..475e3091 100644 --- a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift @@ -16,14 +16,14 @@ final class TodoRepositoryImpl: TodoRepository { private let todoService: TodoService private let todoCategoryService: TodoCategoryService - private let store: UserDefaultsStore + private let store: MemoryCacheStore private let widgetSyncEventBus: WidgetSyncEventBus private let todoMutationEventBus: TodoMutationEventBus init( todoService: TodoService, todoCategoryService: TodoCategoryService, - store: UserDefaultsStore, + store: MemoryCacheStore, widgetSyncEventBus: WidgetSyncEventBus, todoMutationEventBus: TodoMutationEventBus ) { diff --git a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift index d7f67d07..95c455fc 100644 --- a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift @@ -20,7 +20,7 @@ struct AuthSessionRepositoryImplTests { let preference = makePreferenceResponse() let authService = AuthSessionAuthServiceSpy() let todoCategoryService = AuthSessionTodoCategoryServiceSpy(preferences: [preference]) - let store = AuthSessionUserDefaultsStoreSpy() + let store = AuthSessionMemoryCacheStoreSpy() let provider = AuthSessionStateProviderSpy() let repository = AuthSessionRepositoryImpl( authService: authService, @@ -48,7 +48,7 @@ struct AuthSessionRepositoryImplTests { let preference = makePreferenceResponse() let authService = AuthSessionAuthServiceSpy(isSignedIn: true) let todoCategoryService = AuthSessionTodoCategoryServiceSpy(preferences: [preference]) - let store = AuthSessionUserDefaultsStoreSpy() + let store = AuthSessionMemoryCacheStoreSpy() let provider = AuthSessionStateProviderSpy() store.setValue([preference], forKey: Key.preferences) let repository = AuthSessionRepositoryImpl( @@ -153,7 +153,7 @@ private actor AuthSessionTodoCategoryServiceSpy: TodoCategoryService { } } -private final class AuthSessionUserDefaultsStoreSpy: UserDefaultsStore { +private final class AuthSessionMemoryCacheStoreSpy: MemoryCacheStore { private var values = [String: Data]() func value(forKey key: String) -> T? { @@ -172,28 +172,4 @@ private final class AuthSessionUserDefaultsStoreSpy: UserDefaultsStore { values[key] = try? JSONEncoder().encode(value) } - - func removeValues(withPrefix prefix: String) { - values.keys - .filter { $0.hasPrefix(prefix) } - .forEach { values.removeValue(forKey: $0) } - } - - func string(forKey key: String) -> String? { - nil - } - - func setString(_ value: String?, forKey key: String) { } - - func stringArray(forKey key: String) -> [String] { - [] - } - - func setStringArray(_ value: [String], forKey key: String) { } - - func bool(forKey key: String) -> Bool { - false - } - - func setBool(_ value: Bool, forKey key: String) { } } diff --git a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift index bfe041ba..64d45eb3 100644 --- a/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift @@ -65,7 +65,7 @@ struct TodoRepositoryImplTests { private func makeFixture() -> Fixture { let todoService = TodoServiceSpy() let todoCategoryService = TodoCategoryServiceSpy() - let store = TodoRepositoryUserDefaultsStoreSpy() + let store = TodoRepositoryMemoryCacheStoreSpy() let widgetSyncEventBus = WidgetSyncEventBusSpy() let todoMutationEventBus = TodoMutationEventBusSpy() let repository = TodoRepositoryImpl( @@ -159,7 +159,7 @@ private struct TodoCategoryServiceSpy: TodoCategoryService { } } -private final class TodoRepositoryUserDefaultsStoreSpy: UserDefaultsStore { +private final class TodoRepositoryMemoryCacheStoreSpy: MemoryCacheStore { private var values = [String: Any]() func value(forKey key: String) -> T? { @@ -174,30 +174,6 @@ private final class TodoRepositoryUserDefaultsStoreSpy: UserDefaultsStore { values[key] = value } - - func removeValues(withPrefix prefix: String) { - values.keys - .filter { $0.hasPrefix(prefix) } - .forEach { values.removeValue(forKey: $0) } - } - - func string(forKey key: String) -> String? { - nil - } - - func setString(_ value: String?, forKey key: String) { } - - func stringArray(forKey key: String) -> [String] { - [] - } - - func setStringArray(_ value: [String], forKey key: String) { } - - func bool(forKey key: String) -> Bool { - false - } - - func setBool(_ value: Bool, forKey key: String) { } } private final class WidgetSyncEventBusSpy: WidgetSyncEventBus { From b80bc9277edd77a06cf8dbdab23e0d7a28182e37 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:48:53 +0900 Subject: [PATCH 18/19] =?UTF-8?q?refactor:=20=EB=A9=94=EB=AA=A8=EB=A6=AC?= =?UTF-8?q?=20=EC=BA=90=EC=8B=9C=20=EC=A0=80=EC=9E=A5=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Cache/MemoryCacheStoreImpl.swift | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift b/Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift index 65cc1e0e..83a3342e 100644 --- a/Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift +++ b/Application/DevLogPersistence/Sources/Cache/MemoryCacheStoreImpl.swift @@ -9,30 +9,25 @@ import Foundation import DevLogData final class MemoryCacheStoreImpl: MemoryCacheStore { - private let queue = DispatchQueue( - label: "devlog.memory-cache-store", - qos: .utility - ) - private var values = [String: Data]() + private let lock = NSLock() + private var values = [String: Any]() func value(forKey key: String) -> T? { - queue.sync { - let decoder = JSONDecoder() - guard let data = self.values[key] else { return nil } - return try? decoder.decode(T.self, from: data) - } + lock.lock() + defer { lock.unlock() } + + return values[key] as? T } func setValue(_ value: T?, forKey key: String) { - queue.sync { - let encoder = JSONEncoder() - guard let value else { - self.values.removeValue(forKey: key) - return - } + lock.lock() + defer { lock.unlock() } - guard let data = try? encoder.encode(value) else { return } - self.values[key] = data + guard let value else { + values.removeValue(forKey: key) + return } + + values[key] = value } } From 3968bb1bca70c9fe21c4438cc0095624fa9ae3cf Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:57:53 +0900 Subject: [PATCH 19/19] =?UTF-8?q?fix:=20Auth=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EA=B4=80=EC=B0=B0=20=ED=9D=90=EB=A6=84=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthSessionRepositoryImpl.swift | 33 +++++++++++-------- .../AuthSessionRepositoryImplTests.swift | 8 +++-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift index fd99cde2..6971c204 100644 --- a/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift @@ -17,6 +17,7 @@ final class AuthSessionRepositoryImpl: AuthSessionRepository { private let todoCategoryService: TodoCategoryService private let store: MemoryCacheStore private let provider: AuthSessionStateProvider + private var cancellables = Set() init( authService: AuthService, @@ -28,30 +29,34 @@ final class AuthSessionRepositoryImpl: AuthSessionRepository { self.todoCategoryService = todoCategoryService self.store = store self.provider = provider + + setupObservation() } func observeSignedIn() -> AnyPublisher { + provider.observeSignedIn() + } +} + +private extension AuthSessionRepositoryImpl { + func setupObservation() { authService.observeSignedIn() .removeDuplicates() - .map { [self] isSignedIn in - Future { promise in - Task { - if isSignedIn { - await self.cachePreferencesIfNeeded() - } else { - self.clearPreferencesCache() - } - self.provider.publish(isSignedIn) - promise(.success(isSignedIn)) + .sink { [weak self] isSignedIn in + Task { [weak self] in + guard let self else { return } + + if isSignedIn { + await self.cachePreferencesIfNeeded() + } else { + self.clearPreferencesCache() } + self.provider.publish(isSignedIn) } } - .switchToLatest() - .eraseToAnyPublisher() + .store(in: &cancellables) } -} -private extension AuthSessionRepositoryImpl { func cachePreferencesIfNeeded() async { if store.value(forKey: Key.preferences) as [TodoCategoryPreferenceResponse]? != nil { return diff --git a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift index 95c455fc..2d0a3bb9 100644 --- a/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift +++ b/Application/DevLogData/Tests/Repository/AuthSessionRepositoryImplTests.swift @@ -40,7 +40,7 @@ struct AuthSessionRepositoryImplTests { #expect(await valueTask.value) #expect(store.value(forKey: Key.preferences) == [preference]) #expect(await todoCategoryService.fetchCategoryPreferencesCallCount() == 1) - #expect(provider.events == [true]) + #expect(provider.events.contains(true)) } @Test("로그아웃 세션은 category preference 캐시를 제거한다") @@ -68,7 +68,7 @@ struct AuthSessionRepositoryImplTests { #expect(await valueTask.value == false) #expect(store.value(forKey: Key.preferences) == Optional<[TodoCategoryPreferenceResponse]>.none) - #expect(provider.events == [false]) + #expect(provider.events.contains(false)) } private func makePreferenceResponse() -> TodoCategoryPreferenceResponse { @@ -86,14 +86,16 @@ struct AuthSessionRepositoryImplTests { } private final class AuthSessionStateProviderSpy: AuthSessionStateProvider { + private let subject = PassthroughSubject() private(set) var events = [Bool]() func publish(_ isSignedIn: Bool) { events.append(isSignedIn) + subject.send(isSignedIn) } func observeSignedIn() -> AnyPublisher { - Empty().eraseToAnyPublisher() + subject.eraseToAnyPublisher() } }