From 3fde8d624d547549daf3a7264a41d77f0f033dd1 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:44:29 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20TodoDraft=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=AA=A8=EB=8D=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Mapper/TodoMapping.swift | 46 ++++++++++++----- .../Repository/TodoRepositoryImpl.swift | 13 ++++- .../Widget/WidgetSyncEventHandlerTests.swift | 4 ++ .../DevLogDomain.xcodeproj/project.pbxproj | 4 ++ .../DevLogDomain/Sources/Entity/Todo.swift | 4 +- .../Sources/Entity/TodoDraft.swift | 51 +++++++++++++++++++ .../Sources/Protocol/TodoRepository.swift | 1 + .../Todo/Upsert/UpsertTodoUseCase.swift | 1 + .../Todo/Upsert/UpsertTodoUseCaseImpl.swift | 4 ++ .../Profile/HeatmapActivityItem.swift | 3 +- .../Structure/Todo/RecentTodoItem.swift | 3 +- .../Structure/Todo/TodayTodoItem.swift | 3 +- .../Sources/Structure/Todo/TodoListItem.swift | 3 +- .../Tests/Support/TestSupport.swift | 5 ++ 14 files changed, 119 insertions(+), 26 deletions(-) create mode 100644 Application/DevLogDomain/Sources/Entity/TodoDraft.swift diff --git a/Application/DevLogData/Sources/Mapper/TodoMapping.swift b/Application/DevLogData/Sources/Mapper/TodoMapping.swift index 31e23f7a..fa6373c6 100644 --- a/Application/DevLogData/Sources/Mapper/TodoMapping.swift +++ b/Application/DevLogData/Sources/Mapper/TodoMapping.swift @@ -9,21 +9,39 @@ import DevLogCore import DevLogDomain public extension TodoRequest { - static func fromDomain(_ entity: Todo) -> Self { + static func fromDomain(_ todo: Todo) -> Self { + TodoRequest( + id: todo.id, + isPinned: todo.isPinned, + isCompleted: todo.isCompleted, + isChecked: todo.isChecked, + title: todo.title, + content: todo.content, + createdAt: todo.createdAt, + updatedAt: todo.updatedAt, + completedAt: todo.completedAt, + deletedAt: todo.deletedAt, + dueDate: todo.dueDate, + tags: todo.tags, + category: todo.category.storageValue + ) + } + + static func fromDomain(_ todoDraft: TodoDraft) -> Self { TodoRequest( - id: entity.id, - isPinned: entity.isPinned, - isCompleted: entity.isCompleted, - isChecked: entity.isChecked, - title: entity.title, - content: entity.content, - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, - completedAt: entity.completedAt, - deletedAt: entity.deletedAt, - dueDate: entity.dueDate, - tags: entity.tags, - category: entity.category.storageValue + id: todoDraft.id, + isPinned: todoDraft.isPinned, + isCompleted: todoDraft.isCompleted, + isChecked: todoDraft.isChecked, + title: todoDraft.title, + content: todoDraft.content, + createdAt: todoDraft.createdAt, + updatedAt: todoDraft.updatedAt, + completedAt: todoDraft.completedAt, + deletedAt: nil, + dueDate: todoDraft.dueDate, + tags: todoDraft.tags, + category: todoDraft.category.storageValue ) } } diff --git a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift index fc85fa79..3f010106 100644 --- a/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift +++ b/Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift @@ -105,9 +105,18 @@ final class TodoRepositoryImpl: TodoRepository { } func upsertTodo(_ todo: Todo) async throws { - let request = TodoRequest.fromDomain(todo) + let todoRequest = TodoRequest.fromDomain(todo) + try await upsertTodo(todoRequest) + } + + func upsertTodo(_ todoDraft: TodoDraft) async throws { + let todoRequest = TodoRequest.fromDomain(todoDraft) + try await upsertTodo(todoRequest) + } + + private func upsertTodo(_ todoRequest: TodoRequest) async throws { do { - try await todoService.upsertTodo(request: request) + try await todoService.upsertTodo(request: todoRequest) widgetSyncEventBus.publish(.syncRequested) } catch { throw error.toDomain() diff --git a/Application/DevLogData/Tests/Widget/WidgetSyncEventHandlerTests.swift b/Application/DevLogData/Tests/Widget/WidgetSyncEventHandlerTests.swift index 7171bce4..3a716d6e 100644 --- a/Application/DevLogData/Tests/Widget/WidgetSyncEventHandlerTests.swift +++ b/Application/DevLogData/Tests/Widget/WidgetSyncEventHandlerTests.swift @@ -232,6 +232,10 @@ private actor WidgetSyncTodoRepositorySpy: TodoRepository { throw WidgetSyncTodoRepositorySpyError.unexpectedCall } + func upsertTodo(_ todoDraft: TodoDraft) async throws { + throw WidgetSyncTodoRepositorySpyError.unexpectedCall + } + func deleteTodo(_ todoId: String) async throws { throw WidgetSyncTodoRepositorySpyError.unexpectedCall } diff --git a/Application/DevLogDomain/DevLogDomain.xcodeproj/project.pbxproj b/Application/DevLogDomain/DevLogDomain.xcodeproj/project.pbxproj index 077ad93b..eaa35fb0 100644 --- a/Application/DevLogDomain/DevLogDomain.xcodeproj/project.pbxproj +++ b/Application/DevLogDomain/DevLogDomain.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 5F978D87E2642CFA203A01EB /* FetchTodayDisplayOptionsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9EFA52FBE798894A263625 /* FetchTodayDisplayOptionsUseCase.swift */; }; 60B31EFA1482AD2F1A83B13F /* FetchPushNotificationsUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AAB919C7D411DB4D9168030 /* FetchPushNotificationsUseCaseImpl.swift */; }; 6437BF9BCF3C6702C627B7AC /* FetchHeatmapActivityTypesUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C878DA61089719667B00EE1 /* FetchHeatmapActivityTypesUseCaseImpl.swift */; }; + 64F70B07260067A13C7F0FE0 /* TodoDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D715313032680FBCAEC3272 /* TodoDraft.swift */; }; 6669BB8F446C8A59B4EDEB82 /* ObserveUnreadPushCountUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BC92678E2D3F131E95AA44 /* ObserveUnreadPushCountUseCase.swift */; }; 6994DB7AC479B55D96759204 /* SystemTodoCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC9E59B49502EBE884E79F7 /* SystemTodoCategory.swift */; }; 6A1A2DDE4A21808768208B29 /* UpdatePushSettingsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43411396CEB5611FD6DC065 /* UpdatePushSettingsUseCase.swift */; }; @@ -167,6 +168,7 @@ 0919CFDC9E74DC60E2EA82DA /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; 0C2E5255AF2FDDCCF84C8B1E /* UpdatePushSettingsUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePushSettingsUseCaseImpl.swift; sourceTree = ""; }; 0C449AA008FB11BE56D5339B /* UpdateRecentSearchQueriesUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRecentSearchQueriesUseCase.swift; sourceTree = ""; }; + 0D715313032680FBCAEC3272 /* TodoDraft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoDraft.swift; sourceTree = ""; }; 0DE4C7B87FC4BDDA0F36164F /* TodoCategoryPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoCategoryPreference.swift; sourceTree = ""; }; 0DE6079177C9C1BEB7729105 /* ObserveNetworkConnectivityUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserveNetworkConnectivityUseCaseImpl.swift; sourceTree = ""; }; 0FFA6212304F79947234F6B6 /* FetchTodoCategoryPreferencesUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTodoCategoryPreferencesUseCaseImpl.swift; sourceTree = ""; }; @@ -460,6 +462,7 @@ 8FB3EADEF22E74D670F9AC07 /* TodoCategory.swift */, 0DE4C7B87FC4BDDA0F36164F /* TodoCategoryPreference.swift */, D568A3D0C748AC2EBA756981 /* TodoCursor.swift */, + 0D715313032680FBCAEC3272 /* TodoDraft.swift */, 9215705C81F1AC8DFF028D5B /* TodoPage.swift */, ABA9FA543E3197EF5DF55ECB /* TodoReference.swift */, 79E7E7D9AB8EC8B90D6DF0BD /* UserProfile.swift */, @@ -912,6 +915,7 @@ A9B574CED797235C974185A2 /* TodoCategory.swift in Sources */, EDA93E3E36530BA704D41A68 /* TodoCategoryPreference.swift in Sources */, C3253442982CCFCC7736197E /* TodoCursor.swift in Sources */, + 64F70B07260067A13C7F0FE0 /* TodoDraft.swift in Sources */, C8E9E1FB4F3F5B7851612EB2 /* TodoPage.swift in Sources */, 41E798191E4C30558452302F /* TodoReference.swift in Sources */, F7063A1AB3A294E8BB2E585A /* UserProfile.swift in Sources */, diff --git a/Application/DevLogDomain/Sources/Entity/Todo.swift b/Application/DevLogDomain/Sources/Entity/Todo.swift index c2dd1953..604a0859 100644 --- a/Application/DevLogDomain/Sources/Entity/Todo.swift +++ b/Application/DevLogDomain/Sources/Entity/Todo.swift @@ -12,7 +12,7 @@ public struct Todo: Hashable { public var isPinned: Bool // 해당 할 일이 상단에 고정되어 있는지 여부 public var isCompleted: Bool // 해당 할 일의 완료 여부 public var isChecked: Bool // 해당 할 일의 체크 여부 - public var number: Int? // 사용자에게 노출되는 Todo 번호 + public var number: Int // 사용자에게 노출되는 Todo 번호 public var title: String // 할 일의 제목 public var content: String // 할 일의 설명 public var createdAt: Date // 할 일 생성 날짜 @@ -28,7 +28,7 @@ public struct Todo: Hashable { isPinned: Bool, isCompleted: Bool, isChecked: Bool, - number: Int?, + number: Int, title: String, content: String, createdAt: Date, diff --git a/Application/DevLogDomain/Sources/Entity/TodoDraft.swift b/Application/DevLogDomain/Sources/Entity/TodoDraft.swift new file mode 100644 index 00000000..3fd5d09a --- /dev/null +++ b/Application/DevLogDomain/Sources/Entity/TodoDraft.swift @@ -0,0 +1,51 @@ +// +// TodoDraft.swift +// DevLogDomain +// +// Created by opfic on 6/2/26. +// + +import Foundation + +public struct TodoDraft: Hashable { + public var id: String + public var isPinned: Bool + public var isCompleted: Bool + public var isChecked: Bool + public var title: String + public var content: String + public var createdAt: Date + public var updatedAt: Date + public var completedAt: Date? + public var dueDate: Date? + public var tags: [String] + public var category: TodoCategory + + public init( + id: String, + isPinned: Bool, + isCompleted: Bool, + isChecked: Bool, + title: String, + content: String, + createdAt: Date, + updatedAt: Date, + completedAt: Date?, + dueDate: Date?, + tags: [String], + category: TodoCategory + ) { + self.id = id + self.isPinned = isPinned + self.isCompleted = isCompleted + self.isChecked = isChecked + self.title = title + self.content = content + self.createdAt = createdAt + self.updatedAt = updatedAt + self.completedAt = completedAt + self.dueDate = dueDate + self.tags = tags + self.category = category + } +} diff --git a/Application/DevLogDomain/Sources/Protocol/TodoRepository.swift b/Application/DevLogDomain/Sources/Protocol/TodoRepository.swift index 7aa97459..0edd543f 100644 --- a/Application/DevLogDomain/Sources/Protocol/TodoRepository.swift +++ b/Application/DevLogDomain/Sources/Protocol/TodoRepository.swift @@ -13,6 +13,7 @@ public protocol TodoRepository { func fetchTodo(_ todoId: String) async throws -> Todo func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] func upsertTodo(_ todo: Todo) async throws + func upsertTodo(_ todoDraft: TodoDraft) async throws func deleteTodo(_ todoId: String) async throws func undoDeleteTodo(_ todoId: String) async throws } diff --git a/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCase.swift b/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCase.swift index 39592559..122701ed 100644 --- a/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCase.swift +++ b/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCase.swift @@ -7,4 +7,5 @@ public protocol UpsertTodoUseCase { func execute(_ todo: Todo) async throws + func execute(_ todoDraft: TodoDraft) async throws } diff --git a/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCaseImpl.swift b/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCaseImpl.swift index fb4f5df0..43e6228f 100644 --- a/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCaseImpl.swift +++ b/Application/DevLogDomain/Sources/UseCase/Todo/Upsert/UpsertTodoUseCaseImpl.swift @@ -15,4 +15,8 @@ public final class UpsertTodoUseCaseImpl: UpsertTodoUseCase { public func execute(_ todo: Todo) async throws { try await repository.upsertTodo(todo) } + + public func execute(_ todoDraft: TodoDraft) async throws { + try await repository.upsertTodo(todoDraft) + } } diff --git a/Application/DevLogPresentation/Sources/Structure/Profile/HeatmapActivityItem.swift b/Application/DevLogPresentation/Sources/Structure/Profile/HeatmapActivityItem.swift index 9e847742..e06065fc 100644 --- a/Application/DevLogPresentation/Sources/Structure/Profile/HeatmapActivityItem.swift +++ b/Application/DevLogPresentation/Sources/Structure/Profile/HeatmapActivityItem.swift @@ -29,10 +29,9 @@ public struct HeatmapActivityItem: Identifiable, Hashable, Comparable { } init?(todo: Todo, activityKinds: [ActivityKind]) { - guard let number = todo.number else { return nil } self.todoId = todo.id self.title = todo.title - self.number = number + self.number = todo.number self.category = todo.category self.activityKinds = activityKinds self.isDeleted = todo.deletedAt != nil diff --git a/Application/DevLogPresentation/Sources/Structure/Todo/RecentTodoItem.swift b/Application/DevLogPresentation/Sources/Structure/Todo/RecentTodoItem.swift index 8c41997f..53d87191 100644 --- a/Application/DevLogPresentation/Sources/Structure/Todo/RecentTodoItem.swift +++ b/Application/DevLogPresentation/Sources/Structure/Todo/RecentTodoItem.swift @@ -18,9 +18,8 @@ public struct RecentTodoItem: Identifiable, Hashable { public var category: TodoCategory init?(from todo: Todo) { - guard let number = todo.number else { return nil } self.id = todo.id - self.number = number + self.number = todo.number self.title = todo.title self.isPinned = todo.isPinned self.updatedAt = todo.updatedAt diff --git a/Application/DevLogPresentation/Sources/Structure/Todo/TodayTodoItem.swift b/Application/DevLogPresentation/Sources/Structure/Todo/TodayTodoItem.swift index 3aa309c9..b56a2429 100644 --- a/Application/DevLogPresentation/Sources/Structure/Todo/TodayTodoItem.swift +++ b/Application/DevLogPresentation/Sources/Structure/Todo/TodayTodoItem.swift @@ -19,9 +19,8 @@ public struct TodayTodoItem: Identifiable, Hashable { public let category: TodoCategory init?(from todo: Todo) { - guard let number = todo.number else { return nil } self.id = todo.id - self.number = number + self.number = todo.number self.title = todo.title self.tags = todo.tags self.isPinned = todo.isPinned diff --git a/Application/DevLogPresentation/Sources/Structure/Todo/TodoListItem.swift b/Application/DevLogPresentation/Sources/Structure/Todo/TodoListItem.swift index 128b7b1d..ca32936f 100644 --- a/Application/DevLogPresentation/Sources/Structure/Todo/TodoListItem.swift +++ b/Application/DevLogPresentation/Sources/Structure/Todo/TodoListItem.swift @@ -20,9 +20,8 @@ public struct TodoListItem: Identifiable, Hashable { public let updatedAt: Date init?(from todo: Todo) { - guard let number = todo.number else { return nil } self.id = todo.id - self.number = number + self.number = todo.number self.title = todo.title self.tags = todo.tags self.isPinned = todo.isPinned diff --git a/Application/DevLogPresentation/Tests/Support/TestSupport.swift b/Application/DevLogPresentation/Tests/Support/TestSupport.swift index de9e6274..970ab039 100644 --- a/Application/DevLogPresentation/Tests/Support/TestSupport.swift +++ b/Application/DevLogPresentation/Tests/Support/TestSupport.swift @@ -132,10 +132,15 @@ final class UndoDeleteWebPageUseCaseSpy: UndoDeleteWebPageUseCase { final class UpsertTodoUseCaseSpy: UpsertTodoUseCase { private(set) var todos: [Todo] = [] + private(set) var todoDrafts: [TodoDraft] = [] func execute(_ todo: Todo) async throws { todos.append(todo) } + + func execute(_ todoDraft: TodoDraft) async throws { + todoDrafts.append(todoDraft) + } } final class FetchTodosUseCaseSpy: FetchTodosUseCase { From 5d877bac8b76a368aee193309b5de9926025623d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:44:47 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20Todo=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=A0=80=EC=9E=A5=20=ED=9D=90=EB=A6=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Home/HomeViewCoordinator.swift | 5 +- .../Sources/Home/TodoDetailView.swift | 6 +- .../Sources/Home/TodoEditorView.swift | 3 +- .../Sources/Home/TodoEditorViewModel.swift | 70 ++++++++++++++----- .../Sources/Home/TodoEditorWindowEvent.swift | 8 ++- .../Sources/Home/TodoEditorWindowValue.swift | 9 ++- .../Sources/Home/TodoEditorWindowView.swift | 13 ++-- .../Sources/Home/TodoListView.swift | 2 +- .../Sources/Home/TodoWindowCoordinator.swift | 19 ++--- 9 files changed, 91 insertions(+), 44 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index fbbc9410..303baba1 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -43,7 +43,8 @@ final class HomeViewCoordinator { cancellable = windowEvent.submits .sink { [weak self] submit in - guard submit.value.matchesCreate(source: .home) else { return } + guard case .create(let value) = submit, + value.matchesCreate(source: .home) else { return } self?.viewModel.send(.fetchData) } } @@ -59,7 +60,7 @@ final class HomeViewCoordinator { fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), - onUpsertSuccess: { [weak self] _ in + onCreateSuccess: { [weak self] in self?.viewModel.send(.setPresentation(.todoEditor, false)) self?.viewModel.send(.fetchData) } diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift index 87bf57d4..80fcb9f9 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift @@ -18,12 +18,12 @@ struct TodoDetailView: View { var body: some View { ZStack { Color(.systemGroupedBackground).ignoresSafeArea() - if let todo = viewModel.state.todo, let number = todo.number { + if let todo = viewModel.state.todo { TodoDetailContentView( title: todo.title, content: todo.content, referenceItems: viewModel.state.referenceItems, - number: number, + number: todo.number, onOpenTodoID: { viewModel.send(.setSelectedTodoId(TodoIdItem(id: $0))) } ) } else if viewModel.state.isLoading { @@ -69,7 +69,7 @@ struct TodoDetailView: View { fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - onUpsertSuccess: { todo in + onUpdateSuccess: { todo in viewModel.send(.setShowEditor(false)) viewModel.send(.setTodo(todo)) } diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift index 2e2277a0..2a569439 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift @@ -208,8 +208,7 @@ struct TodoEditorView: View { } private func submit() { - let todo = viewModel.makeTodo() - viewModel.send(.upsertTodo(todo)) + viewModel.send(.upsertTodo) } private func close() { diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift index 82fac208..64521460 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift @@ -91,13 +91,14 @@ final class TodoEditorViewModel: Store { case setTitle(String) case setCategories([TodoCategoryItem]) case setReferenceItems([Int: TodoReferenceItem]) - case upsertTodo(Todo) + case upsertTodo } enum SideEffect { case fetchCategories case resolveMarkdown(String) - case upsertTodo(Todo) + case createTodo(TodoDraft) + case updateTodo(Todo) } private(set) var state = State() @@ -106,7 +107,8 @@ final class TodoEditorViewModel: Store { private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase private let upsertTodoUseCase: UpsertTodoUseCase private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? - private let onUpsertSuccess: ((Todo) -> Void)? + private let onCreateSuccess: (() -> Void)? + private let onUpdateSuccess: ((Todo) -> Void)? private let id: String private let isCompleted: Bool private let isChecked: Bool @@ -142,13 +144,14 @@ final class TodoEditorViewModel: Store { fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, upsertTodoUseCase: UpsertTodoUseCase, trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? = nil, - onUpsertSuccess: ((Todo) -> Void)? = nil + onCreateSuccess: (() -> Void)? = nil ) { self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase self.upsertTodoUseCase = upsertTodoUseCase self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase - self.onUpsertSuccess = onUpsertSuccess + self.onCreateSuccess = onCreateSuccess + self.onUpdateSuccess = nil self.id = UUID().uuidString self.isCompleted = false self.isChecked = false @@ -167,13 +170,14 @@ final class TodoEditorViewModel: Store { fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, upsertTodoUseCase: UpsertTodoUseCase, trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? = nil, - onUpsertSuccess: ((Todo) -> Void)? = nil + onUpdateSuccess: ((Todo) -> Void)? = nil ) { self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase self.upsertTodoUseCase = upsertTodoUseCase self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase - self.onUpsertSuccess = onUpsertSuccess + self.onCreateSuccess = nil + self.onUpdateSuccess = onUpdateSuccess self.id = todo.id self.isCompleted = todo.isCompleted self.isChecked = todo.isChecked @@ -243,8 +247,12 @@ final class TodoEditorViewModel: Store { state.categories = categories case .setReferenceItems(let items): state.referenceItems = items - case .upsertTodo(let todo): - effects = [.upsertTodo(todo)] + case .upsertTodo: + if originalDraft == nil { + effects = [.createTodo(makeTodoDraft())] + } else if let todo = makeTodo() { + effects = [.updateTodo(todo)] + } } if self.state != state { self.state = state } @@ -276,16 +284,25 @@ final class TodoEditorViewModel: Store { send(.setReferenceItems(referenceItems)) } - case .upsertTodo(let todo): + case .createTodo(let todoDraft): + send(.setLoading(true)) + Task { + do { + defer { send(.setLoading(false)) } + try await upsertTodoUseCase.execute(todoDraft) + trackAnalyticsEventUseCase?.execute(.todoCreate) + onCreateSuccess?() + } catch { + send(.setAlert(true)) + } + } + case .updateTodo(let todo): send(.setLoading(true)) Task { do { defer { send(.setLoading(false)) } try await upsertTodoUseCase.execute(todo) - if originalDraft == nil { - trackAnalyticsEventUseCase?.execute(.todoCreate) - } - onUpsertSuccess?(todo) + onUpdateSuccess?(todo) } catch { send(.setAlert(true)) } @@ -321,17 +338,36 @@ extension TodoEditorViewModel { state.showAlert = isPresented } - func makeTodo() -> Todo { + private func makeTodoDraft() -> TodoDraft { + let date = Date() + return TodoDraft( + id: self.id, + isPinned: state.isPinned, + isCompleted: state.isCompleted, + isChecked: self.isChecked, + title: state.title, + content: state.content, + createdAt: date, + updatedAt: date, + completedAt: state.completedAt, + dueDate: state.dueDate, + tags: state.tags.map { $0 }, + category: state.category.category + ) + } + + private func makeTodo() -> Todo? { + guard let number, let createdAt else { return nil } let date = Date() return Todo( id: self.id, isPinned: state.isPinned, isCompleted: state.isCompleted, isChecked: self.isChecked, - number: self.number, + number: number, title: state.title, content: state.content, - createdAt: self.createdAt ?? date, + createdAt: createdAt, updatedAt: date, completedAt: state.completedAt, deletedAt: self.deletedAt, diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift index c76f9750..d32d4fa1 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift @@ -17,10 +17,14 @@ public final class TodoEditorWindowEvent { public init() { } - func submit( + func submitCreate(value: TodoEditorWindowValue) { + subject.send(.create(value)) + } + + func submitUpdate( value: TodoEditorWindowValue, todo: Todo ) { - subject.send(TodoEditorWindowSubmit(value: value, todo: todo)) + subject.send(.update(value, todo)) } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift index ec27f458..6b4552f5 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift @@ -77,7 +77,7 @@ public struct TodoEditorWindowTodo: Codable, Hashable { private let isPinned: Bool private let isCompleted: Bool private let isChecked: Bool - private let number: Int? + private let number: Int private let title: String private let content: String private let createdAt: Date @@ -136,10 +136,9 @@ public struct TodoEditorWindowTodo: Codable, Hashable { } } -struct TodoEditorWindowSubmit: Equatable { - let id = UUID() - let value: TodoEditorWindowValue - let todo: Todo +enum TodoEditorWindowSubmit: Equatable { + case create(TodoEditorWindowValue) + case update(TodoEditorWindowValue, Todo) } extension TodoEditorWindowValue { diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift index fa95eac0..1f489fab 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift @@ -34,7 +34,7 @@ public struct TodoEditorWindowView: View { fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), - onUpsertSuccess: upsert + onCreateSuccess: create ), onClose: closeWindow ) @@ -45,7 +45,7 @@ public struct TodoEditorWindowView: View { fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - onUpsertSuccess: upsert + onUpdateSuccess: update ), onClose: closeWindow ) @@ -56,8 +56,13 @@ public struct TodoEditorWindowView: View { } } - private func upsert(_ todo: Todo) { - windowEvent.submit(value: value, todo: todo) + private func create() { + windowEvent.submitCreate(value: value) + closeWindow() + } + + private func update(_ todo: Todo) { + windowEvent.submitUpdate(value: value, todo: todo) closeWindow() } diff --git a/Application/DevLogPresentation/Sources/Home/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/TodoListView.swift index 669795ce..8a71efe5 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListView.swift @@ -87,7 +87,7 @@ struct TodoListView: View { fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), - onUpsertSuccess: { _ in + onCreateSuccess: { viewModel.send(.setShowEditor(false)) viewModel.send(.refresh) } diff --git a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift index 525daf60..0dacaca3 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift @@ -74,14 +74,17 @@ final class TodoWindowCoordinator { } private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit) { - if let listViewModel, - submit.value.matchesCreate(category: listViewModel.category, source: .list) { - listViewModel.send(.refresh) - } - - if let detailViewModel, - submit.value.matchesEdit(todoId: detailViewModel.todoId) { - detailViewModel.send(.setTodo(submit.todo)) + switch submit { + case .create(let value): + if let listViewModel, + value.matchesCreate(category: listViewModel.category, source: .list) { + listViewModel.send(.refresh) + } + case .update(let value, let todo): + if let detailViewModel, + value.matchesEdit(todoId: detailViewModel.todoId) { + detailViewModel.send(.setTodo(todo)) + } } } } From 714c7ca2d4a2fca1af3ce2724228983fd75d6bc0 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:02:45 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20TodoDraft=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EA=B0=90=EC=A7=80=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Entity/TodoDraft.swift | 30 ++++++++++++- .../Sources/Home/TodoEditorViewModel.swift | 42 ++----------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/Application/DevLogDomain/Sources/Entity/TodoDraft.swift b/Application/DevLogDomain/Sources/Entity/TodoDraft.swift index 3fd5d09a..2d3c54c8 100644 --- a/Application/DevLogDomain/Sources/Entity/TodoDraft.swift +++ b/Application/DevLogDomain/Sources/Entity/TodoDraft.swift @@ -7,7 +7,7 @@ import Foundation -public struct TodoDraft: Hashable { +public struct TodoDraft: Equatable { public var id: String public var isPinned: Bool public var isCompleted: Bool @@ -48,4 +48,32 @@ public struct TodoDraft: Hashable { self.tags = tags self.category = category } + + public init(todo: Todo) { + self.id = todo.id + self.isPinned = todo.isPinned + self.isCompleted = todo.isCompleted + self.isChecked = todo.isChecked + self.title = todo.title + self.content = todo.content + self.createdAt = todo.createdAt + self.updatedAt = todo.updatedAt + self.completedAt = todo.completedAt + self.dueDate = todo.dueDate + self.tags = todo.tags + self.category = todo.category + } + + public static func == (lhs: TodoDraft, rhs: TodoDraft) -> Bool { + lhs.id == rhs.id && + lhs.isPinned == rhs.isPinned && + lhs.isCompleted == rhs.isCompleted && + lhs.isChecked == rhs.isChecked && + lhs.title == rhs.title && + lhs.content == rhs.content && + lhs.completedAt == rhs.completedAt && + lhs.dueDate == rhs.dueDate && + lhs.tags == rhs.tags && + lhs.category == rhs.category + } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift index 64521460..8bcd4669 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift @@ -11,39 +11,6 @@ import DevLogDomain @Observable final class TodoEditorViewModel: Store { - private struct Draft: Equatable { - let isCompleted: Bool - let completedAt: Date? - let isPinned: Bool - let title: String - let content: String - let dueDate: Date? - let tags: [String] - let category: TodoCategory - - init(todo: Todo) { - self.isCompleted = todo.isCompleted - self.completedAt = todo.completedAt - self.isPinned = todo.isPinned - self.title = todo.title - self.content = todo.content - self.dueDate = todo.dueDate - self.tags = todo.tags - self.category = todo.category - } - - init(state: State) { - self.isCompleted = state.isCompleted - self.completedAt = state.completedAt - self.isPinned = state.isPinned - self.title = state.title - self.content = state.content - self.dueDate = state.dueDate - self.tags = Array(state.tags) - self.category = state.category.category - } - } - struct State: Equatable { var isCompleted: Bool = false var completedAt: Date? @@ -110,12 +77,11 @@ final class TodoEditorViewModel: Store { private let onCreateSuccess: (() -> Void)? private let onUpdateSuccess: ((Todo) -> Void)? private let id: String - private let isCompleted: Bool private let isChecked: Bool private let number: Int? private let createdAt: Date? private let deletedAt: Date? - private let originalDraft: Draft? + private let originalDraft: TodoDraft? var navigationTitle: String { if originalDraft == nil { @@ -130,7 +96,7 @@ final class TodoEditorViewModel: Store { var hasChanges: Bool { guard let originalDraft else { return true } - return originalDraft != Draft(state: state) + return originalDraft != makeTodoDraft() } var isReadyToSubmit: Bool { @@ -153,7 +119,6 @@ final class TodoEditorViewModel: Store { self.onCreateSuccess = onCreateSuccess self.onUpdateSuccess = nil self.id = UUID().uuidString - self.isCompleted = false self.isChecked = false self.number = nil self.createdAt = nil @@ -179,12 +144,11 @@ final class TodoEditorViewModel: Store { self.onCreateSuccess = nil self.onUpdateSuccess = onUpdateSuccess self.id = todo.id - self.isCompleted = todo.isCompleted self.isChecked = todo.isChecked self.number = todo.number self.createdAt = todo.createdAt self.deletedAt = todo.deletedAt - self.originalDraft = Draft(todo: todo) + self.originalDraft = TodoDraft(todo: todo) state.isCompleted = todo.isCompleted state.completedAt = todo.completedAt state.isPinned = todo.isPinned From 900c942807c96920e49485df5a3e88fa948305cc Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:25:53 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20Todo=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=20=EB=B3=80=ED=99=98=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogPresentation/Sources/Home/TodoEditorViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift index 8bcd4669..55089578 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift @@ -315,7 +315,7 @@ extension TodoEditorViewModel { updatedAt: date, completedAt: state.completedAt, dueDate: state.dueDate, - tags: state.tags.map { $0 }, + tags: Array(state.tags), category: state.category.category ) } @@ -336,7 +336,7 @@ extension TodoEditorViewModel { completedAt: state.completedAt, deletedAt: self.deletedAt, dueDate: state.dueDate, - tags: state.tags.map { $0 }, + tags: Array(state.tags), category: state.category.category ) }