From 84366f2af053ebd81dd07449e6f0564fac70d200 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sun, 31 May 2026 22:25:12 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=EB=A7=A5=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=95=B1=EC=9D=84=20=EC=8B=A4=ED=96=89=ED=96=88=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=EA=B5=AC=EB=B6=84=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EB=B0=A9=EB=B2=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Extension/EnvironmentValues+.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift b/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift index fd30488e..7b8eb91a 100644 --- a/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift +++ b/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift @@ -21,6 +21,10 @@ extension EnvironmentValues { self[SceneHeightKey.self] } + var isiOSAppOnMac: Bool { + self[IOSAppOnMacKey.self] + } + private struct SafeAreaInsetsKey: EnvironmentKey { static var defaultValue: EdgeInsets { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, @@ -48,6 +52,12 @@ extension EnvironmentValues { return windowScene.screen.bounds.height } } + + private struct IOSAppOnMacKey: EnvironmentKey { + static var defaultValue: Bool { + ProcessInfo.processInfo.isiOSAppOnMac + } + } } extension UIEdgeInsets { From c080ca15ebc73ad62dc23e47825dc91908e28236 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sun, 31 May 2026 23:14:29 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20Mac=20Todo=20=ED=8E=B8=EC=A7=91?= =?UTF-8?q?=20=EC=9C=88=EB=8F=84=EC=9A=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogApp/Sources/App/DevLogApp.swift | 11 ++ .../Sources/Home/Home/HomeView.swift | 25 ++- .../Sources/Home/TodoDetailView.swift | 25 ++- .../Sources/Home/TodoEditorView.swift | 13 +- .../Sources/Home/TodoEditorWindowValue.swift | 164 ++++++++++++++++++ .../Sources/Home/TodoEditorWindowView.swift | 56 ++++++ .../Sources/Home/TodoListView.swift | 24 ++- 7 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift create mode 100644 Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift diff --git a/Application/DevLogApp/Sources/App/DevLogApp.swift b/Application/DevLogApp/Sources/App/DevLogApp.swift index 3737e02a..f9f06a47 100644 --- a/Application/DevLogApp/Sources/App/DevLogApp.swift +++ b/Application/DevLogApp/Sources/App/DevLogApp.swift @@ -38,5 +38,16 @@ struct DevLogApp: App { container.resolve(WidgetSyncEventBus.self).publish(.syncRequested) } } + WindowGroup(id: TodoEditorWindowValue.sceneId, for: TodoEditorWindowValue.self) { value in + if let value = value.wrappedValue { + TodoEditorWindowView(value: value) + .autocorrectionDisabled() + } else { + ContentUnavailableView( + String(localized: "todo_edit"), + systemImage: "square.and.pencil" + ) + } + } } } diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift index 3627f29d..6c9b402e 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift @@ -9,6 +9,8 @@ import SwiftUI import DevLogDomain struct HomeView: View { + @Environment(\.openWindow) private var openWindow + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) let coordinator: HomeViewCoordinator let isCompactLayout: Bool @@ -87,6 +89,9 @@ struct HomeView: View { LoadingView() } } + .onReceive(NotificationCenter.default.publisher(for: .todoEditorDidSubmit)) { notification in + handleTodoEditorSubmit(notification) + } } @ViewBuilder @@ -314,7 +319,7 @@ struct HomeView: View { ForEach(preferences, id: \.id) { item in Button { DispatchQueue.main.async { - coordinator.viewModel.send(.tapTodoCategory(item.category)) + openTodoEditor(for: item.category) } } label: { labelImage( @@ -384,6 +389,24 @@ struct HomeView: View { .contentShape(.rect) } + private func openTodoEditor(for todoCategory: TodoCategory) { + if isiOSAppOnMac { + coordinator.viewModel.send(.setPresentation(.contentPicker, false)) + openWindow( + id: TodoEditorWindowValue.sceneId, + value: TodoEditorWindowValue(todoCategory: todoCategory, source: .home) + ) + } else { + coordinator.viewModel.send(.tapTodoCategory(todoCategory)) + } + } + + private func handleTodoEditorSubmit(_ notification: Notification) { + guard let submit = notification.object as? TodoEditorWindowSubmit, + submit.value.matchesCreate(source: .home) else { return } + coordinator.viewModel.send(.addTodo(submit.todo)) + } + } enum HomeRoute: Hashable { diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift index 62bb6d64..542887e0 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift @@ -11,6 +11,8 @@ import DevLogDomain struct TodoDetailView: View { @Environment(\.diContainer) private var container: DIContainer + @Environment(\.openWindow) private var openWindow + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @State var viewModel: TodoDetailViewModel var body: some View { @@ -29,6 +31,9 @@ struct TodoDetailView: View { } } .onAppear { viewModel.send(.onAppear) } + .onReceive(NotificationCenter.default.publisher(for: .todoEditorDidSubmit)) { notification in + handleTodoEditorSubmit(notification) + } .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: Binding( get: { viewModel.state.showInfo }, @@ -90,7 +95,7 @@ struct TodoDetailView: View { } ToolbarItem(placement: .topBarTrailing) { Button { - viewModel.send(.setShowEditor(true)) + openTodoEditor() } label: { Text(String(localized: "todo_edit")) } @@ -98,6 +103,24 @@ struct TodoDetailView: View { } } + private func openTodoEditor() { + if isiOSAppOnMac { + guard let todo = viewModel.state.todo else { return } + openWindow( + id: TodoEditorWindowValue.sceneId, + value: TodoEditorWindowValue(todo: todo) + ) + } else { + viewModel.send(.setShowEditor(true)) + } + } + + private func handleTodoEditorSubmit(_ notification: Notification) { + guard let submit = notification.object as? TodoEditorWindowSubmit, + submit.value.matchesEdit(todoId: viewModel.todoId) else { return } + viewModel.send(.upsertTodo(submit.todo)) + } + @ViewBuilder private var sheetContent: some View { if let todo = viewModel.state.todo { diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift index b7b1e4fa..318217d0 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift @@ -18,6 +18,7 @@ struct TodoEditorView: View { @FocusState private var field: Field? private let calendar = Calendar.current var onSubmit: ((Todo) -> Void)? + var onClose: (() -> Void)? var body: some View { NavigationStack { @@ -74,7 +75,7 @@ struct TodoEditorView: View { .presentationDragIndicator(.visible) } .toolbar { - ToolbarLeadingButton { dismiss() } + ToolbarLeadingButton { close() } ToolbarItem(placement: .topBarTrailing) { Button { viewModel.send(.setShowInfo(true)) @@ -180,7 +181,15 @@ struct TodoEditorView: View { private func submit() { let todo = viewModel.makeTodo() onSubmit?(todo) - dismiss() + close() + } + + private func close() { + if let onClose { + onClose() + } else { + dismiss() + } } private func transitionToPreview() { diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift new file mode 100644 index 00000000..777ac38c --- /dev/null +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift @@ -0,0 +1,164 @@ +// +// TodoEditorWindowValue.swift +// DevLogPresentation +// +// Created by opfic on 5/31/26. +// + +import Foundation +import DevLogDomain + +public enum TodoEditorWindowValue: Codable, Hashable { + case create(TodoEditorWindowCategory, TodoEditorWindowSource) + case edit(TodoEditorWindowTodo) + + public static let sceneId = "todo-editor" + + public init( + todoCategory: TodoCategory, + source: TodoEditorWindowSource + ) { + self = .create(TodoEditorWindowCategory(todoCategory: todoCategory), source) + } + + init(todo: Todo) { + self = .edit(TodoEditorWindowTodo(todo: todo)) + } +} + +public enum TodoEditorWindowSource: String, Codable, Hashable { + case home + case list +} + +public struct TodoEditorWindowCategory: Codable, Hashable { + private enum Kind: String, Codable { + case system + case user + } + + private let kind: Kind + private let id: String + private let name: String + private let colorHex: String + + init(todoCategory: TodoCategory) { + switch todoCategory { + case .system(let systemTodoCategory): + self.kind = .system + self.id = systemTodoCategory.rawValue + self.name = "" + self.colorHex = "" + case .user(let userTodoCategory): + self.kind = .user + self.id = userTodoCategory.id + self.name = userTodoCategory.name + self.colorHex = userTodoCategory.colorHex + } + } + + var todoCategory: TodoCategory { + switch kind { + case .system: + let systemTodoCategory = SystemTodoCategory(rawValue: id) ?? .etc + return .system(systemTodoCategory) + case .user: + return .user(UserTodoCategory( + id: id, + name: name, + colorHex: colorHex + )) + } + } +} + +public struct TodoEditorWindowTodo: Codable, Hashable { + private let id: String + private let isPinned: Bool + private let isCompleted: Bool + private let isChecked: Bool + private let number: Int? + private let title: String + private let content: String + private let createdAt: Date + private let updatedAt: Date + private let completedAt: Date? + private let deletedAt: Date? + private let dueDate: Date? + private let tags: [String] + private let category: TodoEditorWindowCategory + + init(todo: Todo) { + self.id = todo.id + self.isPinned = todo.isPinned + self.isCompleted = todo.isCompleted + self.isChecked = todo.isChecked + self.number = todo.number + self.title = todo.title + self.content = todo.content + self.createdAt = todo.createdAt + self.updatedAt = todo.updatedAt + self.completedAt = todo.completedAt + self.deletedAt = todo.deletedAt + self.dueDate = todo.dueDate + self.tags = todo.tags + self.category = TodoEditorWindowCategory(todoCategory: todo.category) + } + + public static func == ( + lhs: TodoEditorWindowTodo, + rhs: TodoEditorWindowTodo + ) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + var todo: Todo { + Todo( + id: id, + isPinned: isPinned, + isCompleted: isCompleted, + isChecked: isChecked, + number: number, + title: title, + content: content, + createdAt: createdAt, + updatedAt: updatedAt, + completedAt: completedAt, + deletedAt: deletedAt, + dueDate: dueDate, + tags: tags, + category: category.todoCategory + ) + } +} + +struct TodoEditorWindowSubmit { + let value: TodoEditorWindowValue + let todo: Todo +} + +extension TodoEditorWindowValue { + func matchesCreate( + category: TodoCategory? = nil, + source: TodoEditorWindowSource + ) -> Bool { + guard case .create(let windowCategory, source) = self else { return false } + if let category { + return windowCategory.todoCategory == category + } + return true + } + + func matchesEdit(todoId: String) -> Bool { + guard case .edit(let windowTodo) = self else { return false } + return windowTodo.todo.id == todoId + } +} + +extension Notification.Name { + static let todoEditorDidSubmit = Notification.Name("DevLogPresentation.todoEditorDidSubmit") +} diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift new file mode 100644 index 00000000..e266fca1 --- /dev/null +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift @@ -0,0 +1,56 @@ +// +// TodoEditorWindowView.swift +// DevLogPresentation +// +// Created by opfic on 5/31/26. +// + +import SwiftUI +import DevLogCore +import DevLogDomain + +public struct TodoEditorWindowView: View { + @Environment(\.diContainer) private var container: DIContainer + @Environment(\.dismissWindow) private var dismissWindow + private let value: TodoEditorWindowValue + + public init(value: TodoEditorWindowValue) { + self.value = value + } + + public var body: some View { + switch value { + case .create(let windowCategory, _): + TodoEditorView( + viewModel: TodoEditorViewModel( + category: windowCategory.todoCategory, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) + ), + onSubmit: submit, + onClose: closeWindow + ) + case .edit(let windowTodo): + TodoEditorView( + viewModel: TodoEditorViewModel( + todo: windowTodo.todo, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) + ), + onSubmit: submit, + onClose: closeWindow + ) + } + } + + private func closeWindow() { + dismissWindow(id: TodoEditorWindowValue.sceneId, value: value) + } + + private func submit(_ todo: Todo) { + NotificationCenter.default.post( + name: .todoEditorDidSubmit, + object: TodoEditorWindowSubmit(value: value, todo: todo) + ) + } +} diff --git a/Application/DevLogPresentation/Sources/Home/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/TodoListView.swift index ec0a44ff..537a6ef0 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListView.swift @@ -13,6 +13,8 @@ struct TodoListView: View { @Environment(NavigationRouter.self) private var router @Environment(\.diContainer) var container: DIContainer @Environment(\.colorScheme) private var colorScheme + @Environment(\.openWindow) private var openWindow + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .body) private var headerHeight = 41 @State private var headerOffset: CGFloat = .zero @State private var isScrollTrackingEnabled = false @@ -90,7 +92,7 @@ struct TodoListView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { - viewModel.send(.setShowEditor(true)) + openTodoEditor() } label: { Image(systemName: "plus") } @@ -110,6 +112,9 @@ struct TodoListView: View { } .background(NavigationBarConfigurator()) .task { viewModel.send(.onAppear) } + .onReceive(NotificationCenter.default.publisher(for: .todoEditorDidSubmit)) { notification in + handleTodoEditorSubmit(notification) + } } @ViewBuilder @@ -230,6 +235,23 @@ struct TodoListView: View { ) } + private func openTodoEditor() { + if isiOSAppOnMac { + openWindow( + id: TodoEditorWindowValue.sceneId, + value: TodoEditorWindowValue(todoCategory: viewModel.category, source: .list) + ) + } else { + viewModel.send(.setShowEditor(true)) + } + } + + private func handleTodoEditorSubmit(_ notification: Notification) { + guard let submit = notification.object as? TodoEditorWindowSubmit, + submit.value.matchesCreate(category: viewModel.category, source: .list) else { return } + viewModel.send(.upsertTodo(submit.todo)) + } + @ViewBuilder private var searchResultsContent: some View { let searchResults = viewModel.state.searchResults.filter { !$0.isHidden } From 31167bca3d4edef79f2258d2c2ee9907fcd65717 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Sun, 31 May 2026 23:26:50 +0900 Subject: [PATCH 03/11] =?UTF-8?q?refactor:=20Todo=20=ED=8E=B8=EC=A7=91=20?= =?UTF-8?q?=EC=9C=88=EB=8F=84=EC=9A=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogApp/Sources/App/DevLogApp.swift | 3 +++ .../Sources/Home/Home/HomeView.swift | 9 +++---- .../Sources/Home/TodoDetailView.swift | 9 +++---- .../Sources/Home/TodoEditorWindowEvent.swift | 24 +++++++++++++++++++ .../Sources/Home/TodoEditorWindowValue.swift | 7 ++---- .../Sources/Home/TodoEditorWindowView.swift | 6 ++--- .../Sources/Home/TodoListView.swift | 9 +++---- 7 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift diff --git a/Application/DevLogApp/Sources/App/DevLogApp.swift b/Application/DevLogApp/Sources/App/DevLogApp.swift index f9f06a47..6a48dec8 100644 --- a/Application/DevLogApp/Sources/App/DevLogApp.swift +++ b/Application/DevLogApp/Sources/App/DevLogApp.swift @@ -16,6 +16,7 @@ struct DevLogApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate @Environment(\.diContainer) var container: DIContainer @Environment(\.scenePhase) var scenePhase + @State private var windowEvent = TodoEditorWindowEvent() init() { AppAssembler().assemble(AppDIContainer.shared) @@ -33,6 +34,7 @@ struct DevLogApp: App { clearPushNotificationRoute: { PushNotificationRoute.shared.clear() } ) .autocorrectionDisabled() + .environment(windowEvent) .onChange(of: scenePhase) { _, phase in guard phase == .background else { return } container.resolve(WidgetSyncEventBus.self).publish(.syncRequested) @@ -42,6 +44,7 @@ struct DevLogApp: App { if let value = value.wrappedValue { TodoEditorWindowView(value: value) .autocorrectionDisabled() + .environment(windowEvent) } else { ContentUnavailableView( String(localized: "todo_edit"), diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift index 6c9b402e..93cabcbd 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift @@ -9,6 +9,7 @@ import SwiftUI import DevLogDomain struct HomeView: View { + @Environment(TodoEditorWindowEvent.self) private var windowEvent @Environment(\.openWindow) private var openWindow @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) @@ -89,8 +90,8 @@ struct HomeView: View { LoadingView() } } - .onReceive(NotificationCenter.default.publisher(for: .todoEditorDidSubmit)) { notification in - handleTodoEditorSubmit(notification) + .onChange(of: windowEvent.submitted) { _, submitted in + handleTodoEditorSubmit(submitted) } } @@ -401,8 +402,8 @@ struct HomeView: View { } } - private func handleTodoEditorSubmit(_ notification: Notification) { - guard let submit = notification.object as? TodoEditorWindowSubmit, + private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { + guard let submit, submit.value.matchesCreate(source: .home) else { return } coordinator.viewModel.send(.addTodo(submit.todo)) } diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift index 542887e0..a2a3d05b 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift @@ -11,6 +11,7 @@ import DevLogDomain struct TodoDetailView: View { @Environment(\.diContainer) private var container: DIContainer + @Environment(TodoEditorWindowEvent.self) private var windowEvent @Environment(\.openWindow) private var openWindow @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @State var viewModel: TodoDetailViewModel @@ -31,8 +32,8 @@ struct TodoDetailView: View { } } .onAppear { viewModel.send(.onAppear) } - .onReceive(NotificationCenter.default.publisher(for: .todoEditorDidSubmit)) { notification in - handleTodoEditorSubmit(notification) + .onChange(of: windowEvent.submitted) { _, submitted in + handleTodoEditorSubmit(submitted) } .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: Binding( @@ -115,8 +116,8 @@ struct TodoDetailView: View { } } - private func handleTodoEditorSubmit(_ notification: Notification) { - guard let submit = notification.object as? TodoEditorWindowSubmit, + private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { + guard let submit, submit.value.matchesEdit(todoId: viewModel.todoId) else { return } viewModel.send(.upsertTodo(submit.todo)) } diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift new file mode 100644 index 00000000..0eb4e22c --- /dev/null +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift @@ -0,0 +1,24 @@ +// +// TodoEditorWindowEvent.swift +// DevLogPresentation +// +// Created by opfic on 5/31/26. +// + +import Foundation +import DevLogDomain + +@MainActor +@Observable +public final class TodoEditorWindowEvent { + var submitted: TodoEditorWindowSubmit? + + public init() { } + + func submit( + value: TodoEditorWindowValue, + todo: Todo + ) { + submitted = TodoEditorWindowSubmit(value: value, todo: todo) + } +} diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift index 777ac38c..3bfadee8 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift @@ -136,7 +136,8 @@ public struct TodoEditorWindowTodo: Codable, Hashable { } } -struct TodoEditorWindowSubmit { +struct TodoEditorWindowSubmit: Equatable { + let id = UUID() let value: TodoEditorWindowValue let todo: Todo } @@ -158,7 +159,3 @@ extension TodoEditorWindowValue { return windowTodo.todo.id == todoId } } - -extension Notification.Name { - static let todoEditorDidSubmit = Notification.Name("DevLogPresentation.todoEditorDidSubmit") -} diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift index e266fca1..ea61cb0e 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift @@ -12,6 +12,7 @@ import DevLogDomain public struct TodoEditorWindowView: View { @Environment(\.diContainer) private var container: DIContainer @Environment(\.dismissWindow) private var dismissWindow + @Environment(TodoEditorWindowEvent.self) private var windowEvent private let value: TodoEditorWindowValue public init(value: TodoEditorWindowValue) { @@ -48,9 +49,6 @@ public struct TodoEditorWindowView: View { } private func submit(_ todo: Todo) { - NotificationCenter.default.post( - name: .todoEditorDidSubmit, - object: TodoEditorWindowSubmit(value: value, todo: todo) - ) + windowEvent.submit(value: value, todo: todo) } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/TodoListView.swift index 537a6ef0..c977cf81 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListView.swift @@ -11,6 +11,7 @@ import DevLogDomain struct TodoListView: View { @Environment(NavigationRouter.self) private var router + @Environment(TodoEditorWindowEvent.self) private var windowEvent @Environment(\.diContainer) var container: DIContainer @Environment(\.colorScheme) private var colorScheme @Environment(\.openWindow) private var openWindow @@ -112,8 +113,8 @@ struct TodoListView: View { } .background(NavigationBarConfigurator()) .task { viewModel.send(.onAppear) } - .onReceive(NotificationCenter.default.publisher(for: .todoEditorDidSubmit)) { notification in - handleTodoEditorSubmit(notification) + .onChange(of: windowEvent.submitted) { _, submitted in + handleTodoEditorSubmit(submitted) } } @@ -246,8 +247,8 @@ struct TodoListView: View { } } - private func handleTodoEditorSubmit(_ notification: Notification) { - guard let submit = notification.object as? TodoEditorWindowSubmit, + private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { + guard let submit, submit.value.matchesCreate(category: viewModel.category, source: .list) else { return } viewModel.send(.upsertTodo(submit.todo)) } From 82dd39c18c4939364e9d76ca227bf65426c60da9 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:22:26 +0900 Subject: [PATCH 04/11] =?UTF-8?q?ui:=20=EC=83=88=EC=B0=BD=20/=20=ED=92=80?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=B2=84=EC=A0=84=20ui=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 --- .../Sources/Home/TodoEditorView.swift | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift index 318217d0..5de01eb3 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift @@ -15,6 +15,7 @@ struct TodoEditorView: View { @State var viewModel: TodoEditorViewModel @Environment(\.diContainer) private var container: DIContainer @Environment(\.dismiss) private var dismiss + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @FocusState private var field: Field? private let calendar = Calendar.current var onSubmit: ((Todo) -> Void)? @@ -24,7 +25,7 @@ struct TodoEditorView: View { NavigationStack { ScrollView { LazyVStack(spacing: 10) { - titleField + titleSection LazyVStack( alignment: .leading, spacing: 0, @@ -33,7 +34,10 @@ struct TodoEditorView: View { Section { tabView } header: { - tabViewSelector + if !isiOSAppOnMac { + tabPicker + .padding(.horizontal) + } } } } @@ -75,7 +79,9 @@ struct TodoEditorView: View { .presentationDragIndicator(.visible) } .toolbar { - ToolbarLeadingButton { close() } + if !isiOSAppOnMac { + ToolbarLeadingButton { close() } + } ToolbarItem(placement: .topBarTrailing) { Button { viewModel.send(.setShowInfo(true)) @@ -91,6 +97,22 @@ struct TodoEditorView: View { } } + @ViewBuilder + private var titleSection: some View { + Group { + if isiOSAppOnMac { + HStack(spacing: 12) { + titleField + tabPicker + .frame(width: 180) + } + } else { + titleField + } + } + .padding(.horizontal) + } + private var titleField: some View { TextField( "", @@ -103,10 +125,9 @@ struct TodoEditorView: View { .font(.title2) .frame(height: 30) .focused($field, equals: .title) - .padding(.horizontal) } - private var tabViewSelector: some View { + private var tabPicker: some View { Picker( "", selection: Binding( @@ -127,7 +148,6 @@ struct TodoEditorView: View { .tag(TodoEditorViewModel.Tag.preview) } .pickerStyle(.segmented) - .padding(.horizontal) } private var tabView: some View { From b71be42da2969cddea22ddd660c9b9416a9ad054 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:47:52 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20=EB=B2=84=ED=8A=BC=EC=9D=84=20?= =?UTF-8?q?=EC=A0=95=ED=99=95=ED=95=98=EA=B2=8C=20=ED=83=AD=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=95=98=EC=9D=84=20=EB=95=8C=20=ED=83=AD?= =?UTF-8?q?=20=EC=9D=B8=EC=8B=9D=EC=9D=B4=20=EC=95=88=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogPresentation/Sources/Home/Home/HomeView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift index 93cabcbd..2664dcbe 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift @@ -278,6 +278,7 @@ struct HomeView: View { } label: { RecentTodoRow(todo: item) .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(.rect) } .buttonStyle(.plain) } @@ -296,6 +297,7 @@ struct HomeView: View { } label: { WebItemRow(item: item, showsChevron: false) .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(.rect) } .buttonStyle(.plain) } From 886d825174f3f25aa590a861e5b94e2e26b53684 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:34:53 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20Todo=20=ED=8E=B8=EC=A7=91=20?= =?UTF-8?q?=EC=9C=88=EB=8F=84=EC=9A=B0=20source=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/TodoEditorWindowValue.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift index 3bfadee8..ec27f458 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift @@ -147,7 +147,8 @@ extension TodoEditorWindowValue { category: TodoCategory? = nil, source: TodoEditorWindowSource ) -> Bool { - guard case .create(let windowCategory, source) = self else { return false } + guard case .create(let windowCategory, let windowSource) = self, + windowSource == source else { return false } if let category { return windowCategory.todoCategory == category } From 958b19e11d71a3de9cb9438cd0b74c1228703e14 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:44:01 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20HomeCoordinator=20=EA=B2=BD?= =?UTF-8?q?=EB=9F=89=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Home/HomeViewCoordinator.swift | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index cfb78512..a1f7045c 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -14,33 +14,19 @@ import DevLogDomain final class HomeViewCoordinator { let viewModel: HomeViewModel let router = NavigationRouter() - private let fetchTodoCategoryPreferencesUseCase: FetchTodoCategoryPreferencesUseCase - private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase - private let fetchWebPagesUseCase: FetchWebPagesUseCase - private let fetchTodosUseCase: FetchTodosUseCase - private let fetchRecentSearchQueriesUseCase: FetchRecentSearchQueriesUseCase - private let updateRecentSearchQueriesUseCase: UpdateRecentSearchQueriesUseCase + private let diContainer: DIContainer init(container: DIContainer) { - let fetchTodoCategoryPreferencesUseCase = container.resolve(FetchTodoCategoryPreferencesUseCase.self) - let fetchWebPagesUseCase = container.resolve(FetchWebPagesUseCase.self) - let fetchTodosUseCase = container.resolve(FetchTodosUseCase.self) - - self.fetchTodoCategoryPreferencesUseCase = fetchTodoCategoryPreferencesUseCase - self.fetchReferenceItemsUseCase = container.resolve(FetchReferenceItemsUseCase.self) - self.fetchWebPagesUseCase = fetchWebPagesUseCase - self.fetchTodosUseCase = fetchTodosUseCase - self.fetchRecentSearchQueriesUseCase = container.resolve(FetchRecentSearchQueriesUseCase.self) - self.updateRecentSearchQueriesUseCase = container.resolve(UpdateRecentSearchQueriesUseCase.self) + self.diContainer = container self.viewModel = HomeViewModel( - fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self), addWebPageUseCase: container.resolve(AddWebPageUseCase.self), deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self), undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - fetchTodosUseCase: fetchTodosUseCase, - fetchWebPagesUseCase: fetchWebPagesUseCase, + fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), + fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self) ) @@ -57,17 +43,17 @@ final class HomeViewCoordinator { func makeTodoEditorViewModel(category: TodoCategory) -> TodoEditorViewModel { TodoEditorViewModel( category: category, - fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase, - fetchReferenceItemsUseCase: fetchReferenceItemsUseCase + fetchPreferencesUseCase: diContainer.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self) ) } func makeSearchViewModel() -> SearchViewModel { SearchViewModel( - fetchWebPagesUseCase: fetchWebPagesUseCase, - fetchTodosUseCase: fetchTodosUseCase, - fetchRecentSearchQueriesUseCase: fetchRecentSearchQueriesUseCase, - updateRecentSearchQueriesUseCase: updateRecentSearchQueriesUseCase + fetchWebPagesUseCase: diContainer.resolve(FetchWebPagesUseCase.self), + fetchTodosUseCase: diContainer.resolve(FetchTodosUseCase.self), + fetchRecentSearchQueriesUseCase: diContainer.resolve(FetchRecentSearchQueriesUseCase.self), + updateRecentSearchQueriesUseCase: diContainer.resolve(UpdateRecentSearchQueriesUseCase.self) ) } } From a01d0712be102e288ee4b5536ed468652013168c Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:10:34 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor:=20Todo=20=EC=B6=94=EA=B0=80=20/?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EC=B1=85=EC=9E=84=EC=9D=84=20=EC=97=90?= =?UTF-8?q?=EB=94=94=ED=84=B0=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/Home/HomeView.swift | 8 +-- .../Home/Home/HomeViewCoordinator.swift | 9 ++- .../Sources/Home/Home/HomeViewModel.swift | 26 +-------- .../Sources/Home/TodoDetailView.swift | 13 +++-- .../Sources/Home/TodoDetailViewModel.swift | 18 ------ .../Sources/Home/TodoEditorView.swift | 18 ++++-- .../Sources/Home/TodoEditorViewModel.swift | 56 ++++++++++++++++++- .../Sources/Home/TodoEditorWindowView.swift | 14 +++-- .../Sources/Home/TodoListView.swift | 13 +++-- .../Sources/Home/TodoListViewModel.swift | 18 +----- .../Sources/Home/TodoWindowCoordinator.swift | 1 - .../Profile/ProfileViewCoordinator.swift | 1 - .../PushNotificationListViewCoordinator.swift | 1 - .../Sources/Root/RootView.swift | 1 - .../Sources/Search/SearchView.swift | 1 - 15 files changed, 105 insertions(+), 93 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift index 2664dcbe..f46e705f 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift @@ -51,8 +51,7 @@ struct HomeView: View { )) { if let selectedCategory = coordinator.viewModel.state.selectedTodoCategory { TodoEditorView( - viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory), - onSubmit: { coordinator.viewModel.send(.addTodo($0)) } + viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory) ) } } @@ -405,9 +404,8 @@ struct HomeView: View { } private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { - guard let submit, - submit.value.matchesCreate(source: .home) else { return } - coordinator.viewModel.send(.addTodo(submit.todo)) + guard let submit, submit.value.matchesCreate(source: .home) else { return } + coordinator.viewModel.send(.fetchData) } } diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index a1f7045c..e38311db 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -24,7 +24,6 @@ final class HomeViewCoordinator { addWebPageUseCase: container.resolve(AddWebPageUseCase.self), deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self), undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self), - upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), @@ -44,7 +43,13 @@ final class HomeViewCoordinator { TodoEditorViewModel( category: category, fetchPreferencesUseCase: diContainer.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self) + fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: diContainer.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: diContainer.resolve(TrackAnalyticsEventUseCase.self), + onUpsertSuccess: { [weak self] _ in + self?.viewModel.send(.setPresentation(.todoEditor, false)) + self?.viewModel.send(.fetchData) + } ) } diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift index c492ae5d..1fa8b7ce 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift @@ -50,7 +50,6 @@ final class HomeViewModel: Store { case tapTodoCategory(TodoCategory) case orderTodoCategory([TodoCategoryItem]) case setTodoCategory([TodoCategoryItem]) - case addTodo(Todo) case updateRecentTodos([RecentTodoItem]) case updateWebPageURLInput(String) case addWebPage @@ -60,7 +59,6 @@ final class HomeViewModel: Store { } enum SideEffect { - case addTodo(Todo) case addWebPage(String) case deleteWebPage(WebPageItem) case undoDeleteWebPage(String) @@ -103,7 +101,6 @@ final class HomeViewModel: Store { private(set) var state = State() private let fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase private let updatePreferencesUseCase: UpdateTodoCategoryPreferencesUseCase - private let upsertTodoUseCase: UpsertTodoUseCase private let addWebPageUseCase: AddWebPageUseCase private let deleteWebPageUseCase: DeleteWebPageUseCase private let undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase @@ -121,7 +118,6 @@ final class HomeViewModel: Store { addWebPageUseCase: AddWebPageUseCase, deleteWebPageUseCase: DeleteWebPageUseCase, undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase, - upsertTodoUseCase: UpsertTodoUseCase, fetchTodosUseCase: FetchTodosUseCase, fetchWebPagesUseCase: FetchWebPagesUseCase, networkConnectivityUseCase: ObserveNetworkConnectivityUseCase, @@ -132,7 +128,6 @@ final class HomeViewModel: Store { self.addWebPageUseCase = addWebPageUseCase self.deleteWebPageUseCase = deleteWebPageUseCase self.undoDeleteWebPageUseCase = undoDeleteWebPageUseCase - self.upsertTodoUseCase = upsertTodoUseCase self.fetchTodosUseCase = fetchTodosUseCase self.fetchWebPagesUseCase = fetchWebPagesUseCase self.networkConnectivityUseCase = networkConnectivityUseCase @@ -149,7 +144,7 @@ final class HomeViewModel: Store { case .networkStatusChanged(let isConnected): state.isNetworkConnected = isConnected case .fetchData, .setPresentation, .setAlert, .setToast, .refreshWebPages, - .tapTodoCategory, .orderTodoCategory, .addTodo, .updateWebPageURLInput, + .tapTodoCategory, .orderTodoCategory, .updateWebPageURLInput, .addWebPage, .deleteWebPage, .undoDeleteWebPage: effects = reduceByView(action, state: &state) @@ -183,23 +178,6 @@ final class HomeViewModel: Store { send(.setAlert(isPresented: true, type: .error)) } } - case .addTodo(let todo): - beginLoading(for: .overlay, mode: .delayed) - Task { - do { - defer { endLoading(for: .overlay, mode: .delayed) } - try await upsertTodoUseCase.execute(todo) - trackAnalyticsEventUseCase.execute(.todoCreate) - let page = try await fetchRecentTodos() - let items = page.items - .filter { $0.createdAt != $0.updatedAt } - .prefix(5) - .compactMap { RecentTodoItem(from: $0) } - send(.updateRecentTodos(items)) - } catch { - send(.setAlert(isPresented: true, type: .error)) - } - } case .fetchRecentTodos: beginLoading(for: .recentTodos, mode: .immediate) Task { @@ -305,8 +283,6 @@ private extension HomeViewModel { state.preferences = preferences state.recentTodos = syncRecentTodos(state.recentTodos, preferences: preferences) return [.updateTodoCategoryPreferences(preferences)] - case .addTodo(let todo): - return [.addTodo(todo)] case .updateWebPageURLInput(let text): state.webPageURLInput = text case .addWebPage: diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift index a2a3d05b..911c140c 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift @@ -50,7 +50,6 @@ struct TodoDetailView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: item.id, showEditButton: false )) @@ -72,9 +71,13 @@ struct TodoDetailView: View { viewModel: TodoEditorViewModel( todo: todo, fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) - ), - onSubmit: { viewModel.send(.upsertTodo($0)) } + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + onUpsertSuccess: { todo in + viewModel.send(.setShowEditor(false)) + viewModel.send(.setTodo(todo)) + } + ) ) } } @@ -119,7 +122,7 @@ struct TodoDetailView: View { private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { guard let submit, submit.value.matchesEdit(todoId: viewModel.todoId) else { return } - viewModel.send(.upsertTodo(submit.todo)) + viewModel.send(.setTodo(submit.todo)) } @ViewBuilder diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift index d5d034cc..dc068268 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift @@ -31,13 +31,11 @@ final class TodoDetailViewModel: Store { case setTodo(Todo) case setReferenceItems([Int: TodoReferenceItem]) case setLoading(Bool) - case upsertTodo(Todo) } enum SideEffect { case fetchTodo case resolveMarkdown(String) - case upsertTodo(Todo) } private(set) var state: State = .init() @@ -45,19 +43,16 @@ final class TodoDetailViewModel: Store { let showEditButton: Bool private let fetchTodoUseCase: FetchTodoByIdUseCase private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase - private let upsertUseCase: UpsertTodoUseCase private let loadingState = LoadingState() init( fetchTodoUseCase: FetchTodoByIdUseCase, fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, - upsertUseCase: UpsertTodoUseCase, todoId: String, showEditButton: Bool = true ) { self.fetchTodoUseCase = fetchTodoUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase - self.upsertUseCase = upsertUseCase self.todoId = todoId self.showEditButton = showEditButton } @@ -85,8 +80,6 @@ final class TodoDetailViewModel: Store { state.referenceItems = items case .setLoading(let value): state.isLoading = value - case .upsertTodo(let todo): - effects = [.upsertTodo(todo)] } if self.state != state { self.state = state } @@ -122,17 +115,6 @@ final class TodoDetailViewModel: Store { send(.setReferenceItems(referenceItems)) } - case .upsertTodo(let todo): - beginLoading(.delayed) - Task { - do { - defer { endLoading(.delayed) } - try await upsertUseCase.execute(todo) - send(.setTodo(todo)) - } catch { - send(.setAlert(true)) - } - } } } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift index 5de01eb3..2d01eeae 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift @@ -18,7 +18,6 @@ struct TodoEditorView: View { @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @FocusState private var field: Field? private let calendar = Calendar.current - var onSubmit: ((Todo) -> Void)? var onClose: (() -> Void)? var body: some View { @@ -65,7 +64,6 @@ struct TodoEditorView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: item.id, showEditButton: false )) @@ -92,7 +90,18 @@ struct TodoEditorView: View { ToolbarTrailingButton { submit() } - .disabled(!viewModel.isReadyToSubmit) + .disabled(!viewModel.isReadyToSubmit || viewModel.state.isLoading) + } + .alert( + viewModel.state.alertTitle, + isPresented: Binding( + get: { viewModel.state.showAlert }, + set: { viewModel.send(.setAlert($0)) } + ) + ) { + Button(String(localized: "common_close"), role: .cancel) { } + } message: { + Text(viewModel.state.alertMessage) } } } @@ -200,8 +209,7 @@ struct TodoEditorView: View { private func submit() { let todo = viewModel.makeTodo() - onSubmit?(todo) - close() + viewModel.send(.upsertTodo(todo)) } private func close() { diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift index 691f1a85..82fac208 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift @@ -54,6 +54,10 @@ final class TodoEditorViewModel: Store { var referenceItems: [Int: TodoReferenceItem] = [:] var dueDate: Date? var showInfo: Bool = false + var showAlert: Bool = false + var alertTitle: String = "" + var alertMessage: String = "" + var isLoading: Bool = false var tags: OrderedSet = [] var tagText: String = "" var focusOnEditor: Bool = false @@ -77,6 +81,8 @@ final class TodoEditorViewModel: Store { case setCompleted(Bool) case setDueDate(Date?) case setCategory(TodoCategoryItem) + case setAlert(Bool) + case setLoading(Bool) case setPinned(Bool) case setShowInfo(Bool) case setSelectedTodoId(TodoIdItem?) @@ -85,17 +91,22 @@ final class TodoEditorViewModel: Store { case setTitle(String) case setCategories([TodoCategoryItem]) case setReferenceItems([Int: TodoReferenceItem]) + case upsertTodo(Todo) } enum SideEffect { case fetchCategories case resolveMarkdown(String) + case upsertTodo(Todo) } private(set) var state = State() private let calendar = Calendar.current private let fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase + private let upsertTodoUseCase: UpsertTodoUseCase + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? + private let onUpsertSuccess: ((Todo) -> Void)? private let id: String private let isCompleted: Bool private let isChecked: Bool @@ -128,10 +139,16 @@ final class TodoEditorViewModel: Store { init( category: TodoCategory, fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase, - fetchReferenceItemsUseCase: FetchReferenceItemsUseCase + fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, + upsertTodoUseCase: UpsertTodoUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? = nil, + onUpsertSuccess: ((Todo) -> Void)? = nil ) { self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase + self.upsertTodoUseCase = upsertTodoUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase + self.onUpsertSuccess = onUpsertSuccess self.id = UUID().uuidString self.isCompleted = false self.isChecked = false @@ -147,10 +164,16 @@ final class TodoEditorViewModel: Store { init( todo: Todo, fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase, - fetchReferenceItemsUseCase: FetchReferenceItemsUseCase + fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, + upsertTodoUseCase: UpsertTodoUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? = nil, + onUpsertSuccess: ((Todo) -> Void)? = nil ) { self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase + self.upsertTodoUseCase = upsertTodoUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase + self.onUpsertSuccess = onUpsertSuccess self.id = todo.id self.isCompleted = todo.isCompleted self.isChecked = todo.isChecked @@ -201,6 +224,10 @@ final class TodoEditorViewModel: Store { state.isCompleted = isCompleted case .setCategory(let todoCategoryItem): state.category = todoCategoryItem + case .setAlert(let isPresented): + setAlert(&state, isPresented: isPresented) + case .setLoading(let value): + state.isLoading = value case .setPinned(let isPinned): state.isPinned = isPinned case .setShowInfo(let isPresented): @@ -216,6 +243,8 @@ final class TodoEditorViewModel: Store { state.categories = categories case .setReferenceItems(let items): state.referenceItems = items + case .upsertTodo(let todo): + effects = [.upsertTodo(todo)] } if self.state != state { self.state = state } @@ -247,6 +276,20 @@ final class TodoEditorViewModel: Store { send(.setReferenceItems(referenceItems)) } + case .upsertTodo(let todo): + send(.setLoading(true)) + Task { + do { + defer { send(.setLoading(false)) } + try await upsertTodoUseCase.execute(todo) + if originalDraft == nil { + trackAnalyticsEventUseCase?.execute(.todoCreate) + } + onUpsertSuccess?(todo) + } catch { + send(.setAlert(true)) + } + } } } } @@ -269,6 +312,15 @@ extension TodoEditorViewModel { } } + private func setAlert( + _ state: inout State, + isPresented: Bool + ) { + state.alertTitle = String(localized: "common_error_title") + state.alertMessage = String(localized: "common_error_message") + state.showAlert = isPresented + } + func makeTodo() -> Todo { let date = Date() return Todo( diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift index ea61cb0e..13f6168d 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift @@ -26,9 +26,11 @@ public struct TodoEditorWindowView: View { viewModel: TodoEditorViewModel( category: windowCategory.todoCategory, fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), + onUpsertSuccess: upsert ), - onSubmit: submit, onClose: closeWindow ) case .edit(let windowTodo): @@ -36,9 +38,10 @@ public struct TodoEditorWindowView: View { viewModel: TodoEditorViewModel( todo: windowTodo.todo, fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + onUpsertSuccess: upsert ), - onSubmit: submit, onClose: closeWindow ) } @@ -48,7 +51,8 @@ public struct TodoEditorWindowView: View { dismissWindow(id: TodoEditorWindowValue.sceneId, value: value) } - private func submit(_ todo: Todo) { + private func upsert(_ todo: Todo) { windowEvent.submit(value: value, todo: todo) + closeWindow() } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/TodoListView.swift index c977cf81..15792c06 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListView.swift @@ -85,9 +85,14 @@ struct TodoListView: View { viewModel: TodoEditorViewModel( category: viewModel.category, fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) - ), - onSubmit: { viewModel.send(.upsertTodo($0)) } + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), + onUpsertSuccess: { _ in + viewModel.send(.setShowEditor(false)) + viewModel.send(.refresh) + } + ) ) } .toolbar { @@ -250,7 +255,7 @@ struct TodoListView: View { private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { guard let submit, submit.value.matchesCreate(category: viewModel.category, source: .list) else { return } - viewModel.send(.upsertTodo(submit.todo)) + viewModel.send(.refresh) } @ViewBuilder diff --git a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift index ebab4b66..9467e77b 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift @@ -50,7 +50,6 @@ final class TodoListViewModel: Store { case loadNextPage case setSearchText(String) case setToast(isPresented: Bool) - case upsertTodo(Todo) // Run case applySearchQuery(String) @@ -70,7 +69,6 @@ final class TodoListViewModel: Store { case fetch case loadNextPage case search(String) - case upsert(Todo) case delete(TodoListItem) case undoDelete(String) case toggleCompleted(TodoListItem) @@ -138,7 +136,7 @@ final class TodoListViewModel: Store { .setShowAllSearchResults, .tapToggleCompleted, .tapTogglePinned, .undoDelete: effects = reduceByUser(action, state: &state) - case .onAppear, .loadNextPage, .setSearchText, .setToast, .upsertTodo: + case .onAppear, .loadNextPage, .setSearchText, .setToast: effects = reduceByView(action, state: &state) case .applySearchQuery, .fetchSearchResults, .didToggleCompleted, .didTogglePinned, @@ -206,18 +204,6 @@ final class TodoListViewModel: Store { } } searchTasks[.request] = requestTask - case .upsert(let item): - beginLoading(.delayed) - Task { - do { - defer { endLoading(.delayed) } - try await upsertTodoUseCase.execute(item) - trackAnalyticsEventUseCase.execute(.todoCreate) - send(.refresh) - } catch { - send(.setAlert(true)) - } - } case .toggleCompleted(let item): beginLoading(.delayed) Task { @@ -369,8 +355,6 @@ private extension TodoListViewModel { state.searchResults.removeAll { $0.isHidden } self.undoTodoId = nil } - case .upsertTodo(let todo): - return [.upsert(todo)] default: break } diff --git a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift index bea4f7ce..911a0c8b 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift @@ -54,7 +54,6 @@ final class TodoWindowCoordinator { let detailViewModel = TodoDetailViewModel( fetchTodoUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: diContainer.resolve(UpsertTodoUseCase.self), todoId: todoId, showEditButton: showEditButton ) diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift b/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift index e1cefe4d..2a90f684 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift @@ -61,7 +61,6 @@ final class ProfileViewCoordinator { TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: todoId, showEditButton: false ) diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift index fe8f9b85..e507a9fc 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift @@ -44,7 +44,6 @@ final class PushNotificationListViewCoordinator { let todoDetailViewModel = TodoDetailViewModel( fetchTodoUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: diContainer.resolve(UpsertTodoUseCase.self), todoId: todoId, showEditButton: false ) diff --git a/Application/DevLogPresentation/Sources/Root/RootView.swift b/Application/DevLogPresentation/Sources/Root/RootView.swift index 0ba4bec5..27f188e1 100644 --- a/Application/DevLogPresentation/Sources/Root/RootView.swift +++ b/Application/DevLogPresentation/Sources/Root/RootView.swift @@ -91,7 +91,6 @@ public struct RootView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: todoId, showEditButton: false )) diff --git a/Application/DevLogPresentation/Sources/Search/SearchView.swift b/Application/DevLogPresentation/Sources/Search/SearchView.swift index 3d07a4a5..27616391 100644 --- a/Application/DevLogPresentation/Sources/Search/SearchView.swift +++ b/Application/DevLogPresentation/Sources/Search/SearchView.swift @@ -24,7 +24,6 @@ struct SearchView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: todoId )) case .web(let page): From eb19e386fa8755261a61009973099507b07a39fb Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:13:25 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20TodoEditor=EC=97=90=EC=84=9C=20Tod?= =?UTF-8?q?o=EB=A5=BC=20=EC=83=9D=EC=84=B1,=20=EC=88=98=EC=A0=95=ED=95=98?= =?UTF-8?q?=EB=A9=B4=EC=84=9C=20=EC=9C=88=EB=8F=84=EC=9A=B0=EA=B0=80=20?= =?UTF-8?q?=EB=8B=AB=ED=9E=88=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Home/TodoEditorWindowView.swift | 88 +++++++++++++------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift index 13f6168d..eef495e6 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift @@ -11,8 +11,8 @@ import DevLogDomain public struct TodoEditorWindowView: View { @Environment(\.diContainer) private var container: DIContainer - @Environment(\.dismissWindow) private var dismissWindow @Environment(TodoEditorWindowEvent.self) private var windowEvent + @State private var windowScene: UIWindowScene? private let value: TodoEditorWindowValue public init(value: TodoEditorWindowValue) { @@ -20,39 +20,69 @@ public struct TodoEditorWindowView: View { } public var body: some View { - switch value { - case .create(let windowCategory, _): - TodoEditorView( - viewModel: TodoEditorViewModel( - category: windowCategory.todoCategory, - fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), - onUpsertSuccess: upsert - ), - onClose: closeWindow - ) - case .edit(let windowTodo): - TodoEditorView( - viewModel: TodoEditorViewModel( - todo: windowTodo.todo, - fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - onUpsertSuccess: upsert - ), - onClose: closeWindow - ) + Group { + switch value { + case .create(let windowCategory, _): + TodoEditorView( + viewModel: TodoEditorViewModel( + category: windowCategory.todoCategory, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), + onUpsertSuccess: upsert + ), + onClose: closeWindow + ) + case .edit(let windowTodo): + TodoEditorView( + viewModel: TodoEditorViewModel( + todo: windowTodo.todo, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + onUpsertSuccess: upsert + ), + onClose: closeWindow + ) + } + } + .background { + WindowSceneReader { windowScene = $0 } } - } - - private func closeWindow() { - dismissWindow(id: TodoEditorWindowValue.sceneId, value: value) } private func upsert(_ todo: Todo) { windowEvent.submit(value: value, todo: todo) closeWindow() } + + private func closeWindow() { + guard let windowScene else { return } + UIApplication.shared.requestSceneSessionDestruction( + windowScene.session, + options: nil, + errorHandler: nil + ) + } +} + +private struct WindowSceneReader: UIViewRepresentable { + let onResolve: (UIWindowScene?) -> Void + + func makeUIView(context: Context) -> UIView { + let view = UIView() + resolve(from: view) + return view + } + + func updateUIView(_ view: UIView, context: Context) { + resolve(from: view) + } + + private func resolve(from view: UIView) { + DispatchQueue.main.async { + onResolve(view.window?.windowScene) + } + } } From 2a64384de825414446620ec9d923753e39545701 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:44:45 +0900 Subject: [PATCH 10/11] =?UTF-8?q?refactor:=20Todo=20=ED=8E=B8=EC=A7=91=20?= =?UTF-8?q?=EC=9C=88=EB=8F=84=EC=9A=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogApp/Sources/App/DevLogApp.swift | 8 ++++--- .../Sources/Home/Home/HomeView.swift | 9 ------- .../Home/Home/HomeViewCoordinator.swift | 13 ++++++++++ .../Sources/Home/TodoDetailView.swift | 10 -------- .../Sources/Home/TodoEditorWindowEvent.swift | 12 ++++++---- .../Sources/Home/TodoEditorWindowView.swift | 8 +++++-- .../Sources/Home/TodoListView.swift | 10 -------- .../Sources/Home/TodoWindowCoordinator.swift | 24 +++++++++++++++++++ .../Sources/Main/MainView.swift | 5 ++++ .../Sources/Root/RootView.swift | 4 ++++ 10 files changed, 64 insertions(+), 39 deletions(-) diff --git a/Application/DevLogApp/Sources/App/DevLogApp.swift b/Application/DevLogApp/Sources/App/DevLogApp.swift index 6a48dec8..1a2dca6c 100644 --- a/Application/DevLogApp/Sources/App/DevLogApp.swift +++ b/Application/DevLogApp/Sources/App/DevLogApp.swift @@ -30,11 +30,11 @@ struct DevLogApp: App { systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self), trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), widgetURLTab: { MainTab(widgetURL: $0) }, + windowEvent: windowEvent, pushNotificationTodoIdPublisher: PushNotificationRoute.shared.observe(), clearPushNotificationRoute: { PushNotificationRoute.shared.clear() } ) .autocorrectionDisabled() - .environment(windowEvent) .onChange(of: scenePhase) { _, phase in guard phase == .background else { return } container.resolve(WidgetSyncEventBus.self).publish(.syncRequested) @@ -42,9 +42,11 @@ struct DevLogApp: App { } WindowGroup(id: TodoEditorWindowValue.sceneId, for: TodoEditorWindowValue.self) { value in if let value = value.wrappedValue { - TodoEditorWindowView(value: value) + TodoEditorWindowView( + value: value, + windowEvent: windowEvent + ) .autocorrectionDisabled() - .environment(windowEvent) } else { ContentUnavailableView( String(localized: "todo_edit"), diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift index f46e705f..ee3040c0 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift @@ -9,7 +9,6 @@ import SwiftUI import DevLogDomain struct HomeView: View { - @Environment(TodoEditorWindowEvent.self) private var windowEvent @Environment(\.openWindow) private var openWindow @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) @@ -89,9 +88,6 @@ struct HomeView: View { LoadingView() } } - .onChange(of: windowEvent.submitted) { _, submitted in - handleTodoEditorSubmit(submitted) - } } @ViewBuilder @@ -403,11 +399,6 @@ struct HomeView: View { } } - private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { - guard let submit, submit.value.matchesCreate(source: .home) else { return } - coordinator.viewModel.send(.fetchData) - } - } enum HomeRoute: Hashable { diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index e38311db..24661568 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -5,6 +5,7 @@ // Created by opfic on 5/10/26. // +import Combine import Foundation import DevLogCore import DevLogDomain @@ -15,6 +16,8 @@ final class HomeViewCoordinator { let viewModel: HomeViewModel let router = NavigationRouter() private let diContainer: DIContainer + @ObservationIgnored + private var cancellable: AnyCancellable? init(container: DIContainer) { self.diContainer = container @@ -35,6 +38,16 @@ final class HomeViewCoordinator { viewModel.send(.fetchData) } + func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) { + guard cancellable == nil else { return } + + cancellable = windowEvent.submits + .sink { [weak self] submit in + guard submit.value.matchesCreate(source: .home) else { return } + self?.viewModel.send(.fetchData) + } + } + func makeTodoManageViewModel() -> TodoManageViewModel { TodoManageViewModel(viewModel.state.preferences) } diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift index 911c140c..93d9a62e 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift @@ -11,7 +11,6 @@ import DevLogDomain struct TodoDetailView: View { @Environment(\.diContainer) private var container: DIContainer - @Environment(TodoEditorWindowEvent.self) private var windowEvent @Environment(\.openWindow) private var openWindow @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @State var viewModel: TodoDetailViewModel @@ -32,9 +31,6 @@ struct TodoDetailView: View { } } .onAppear { viewModel.send(.onAppear) } - .onChange(of: windowEvent.submitted) { _, submitted in - handleTodoEditorSubmit(submitted) - } .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: Binding( get: { viewModel.state.showInfo }, @@ -119,12 +115,6 @@ struct TodoDetailView: View { } } - private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { - guard let submit, - submit.value.matchesEdit(todoId: viewModel.todoId) else { return } - viewModel.send(.setTodo(submit.todo)) - } - @ViewBuilder private var sheetContent: some View { if let todo = viewModel.state.todo { diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift index 0eb4e22c..c76f9750 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift @@ -5,13 +5,15 @@ // Created by opfic on 5/31/26. // -import Foundation +import Combine import DevLogDomain -@MainActor -@Observable public final class TodoEditorWindowEvent { - var submitted: TodoEditorWindowSubmit? + private let subject = PassthroughSubject() + + var submits: AnyPublisher { + subject.eraseToAnyPublisher() + } public init() { } @@ -19,6 +21,6 @@ public final class TodoEditorWindowEvent { value: TodoEditorWindowValue, todo: Todo ) { - submitted = TodoEditorWindowSubmit(value: value, todo: todo) + subject.send(TodoEditorWindowSubmit(value: value, todo: todo)) } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift index eef495e6..fa95eac0 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift @@ -11,12 +11,16 @@ import DevLogDomain public struct TodoEditorWindowView: View { @Environment(\.diContainer) private var container: DIContainer - @Environment(TodoEditorWindowEvent.self) private var windowEvent @State private var windowScene: UIWindowScene? private let value: TodoEditorWindowValue + private let windowEvent: TodoEditorWindowEvent - public init(value: TodoEditorWindowValue) { + public init( + value: TodoEditorWindowValue, + windowEvent: TodoEditorWindowEvent + ) { self.value = value + self.windowEvent = windowEvent } public var body: some View { diff --git a/Application/DevLogPresentation/Sources/Home/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/TodoListView.swift index 15792c06..a8c2ab11 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListView.swift @@ -11,7 +11,6 @@ import DevLogDomain struct TodoListView: View { @Environment(NavigationRouter.self) private var router - @Environment(TodoEditorWindowEvent.self) private var windowEvent @Environment(\.diContainer) var container: DIContainer @Environment(\.colorScheme) private var colorScheme @Environment(\.openWindow) private var openWindow @@ -118,9 +117,6 @@ struct TodoListView: View { } .background(NavigationBarConfigurator()) .task { viewModel.send(.onAppear) } - .onChange(of: windowEvent.submitted) { _, submitted in - handleTodoEditorSubmit(submitted) - } } @ViewBuilder @@ -252,12 +248,6 @@ struct TodoListView: View { } } - private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit?) { - guard let submit, - submit.value.matchesCreate(category: viewModel.category, source: .list) else { return } - viewModel.send(.refresh) - } - @ViewBuilder private var searchResultsContent: some View { let searchResults = viewModel.state.searchResults.filter { !$0.isHidden } diff --git a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift index 911a0c8b..df14bb82 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift @@ -5,6 +5,7 @@ // Created by opfic on 5/31/26. // +import Combine import Foundation import DevLogCore import DevLogDomain @@ -17,11 +18,22 @@ final class TodoWindowCoordinator { private var listViewModel: TodoListViewModel? @ObservationIgnored private var detailViewModel: TodoDetailViewModel? + @ObservationIgnored + private var cancellable: AnyCancellable? init(container: DIContainer) { self.diContainer = container } + func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) { + guard cancellable == nil else { return } + + cancellable = windowEvent.submits + .sink { [weak self] submit in + self?.handleTodoEditorSubmit(submit) + } + } + func makeListViewModel(category: TodoCategory) -> TodoListViewModel { if let listViewModel, listViewModel.category == category { @@ -60,4 +72,16 @@ final class TodoWindowCoordinator { self.detailViewModel = detailViewModel return detailViewModel } + + 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)) + } + } } diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index 355708af..fef09bd7 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -18,9 +18,11 @@ struct MainView: View { @State private var pushNotificationListViewCoordinator: PushNotificationListViewCoordinator @State private var profileViewCoordinator: ProfileViewCoordinator @Binding var selectedTab: MainTab? + private let windowEvent: TodoEditorWindowEvent init( container: DIContainer, + windowEvent: TodoEditorWindowEvent, selectedTab: Binding ) { self._coordinator = State(initialValue: MainViewCoordinator(container: container)) @@ -31,6 +33,7 @@ struct MainView: View { initialValue: PushNotificationListViewCoordinator(container: container) ) self._profileViewCoordinator = State(initialValue: ProfileViewCoordinator(container: container)) + self.windowEvent = windowEvent self._selectedTab = selectedTab } @@ -46,6 +49,8 @@ struct MainView: View { } .onAppear { coordinator.viewModel.send(.onAppear) + homeViewCoordinator.bindWindowEvent(windowEvent) + todoWindowCoordinator.bindWindowEvent(windowEvent) } .onChange(of: selectedTab, initial: true) { _, newValue in guard let newValue else { return } diff --git a/Application/DevLogPresentation/Sources/Root/RootView.swift b/Application/DevLogPresentation/Sources/Root/RootView.swift index 27f188e1..b74658b2 100644 --- a/Application/DevLogPresentation/Sources/Root/RootView.swift +++ b/Application/DevLogPresentation/Sources/Root/RootView.swift @@ -16,6 +16,7 @@ public struct RootView: View { @State private var selectedRoute: Route? @State private var selectedMainTab: MainTab? private let widgetURLTab: (URL) -> MainTab? + private let windowEvent: TodoEditorWindowEvent private let pushNotificationTodoIdPublisher: AnyPublisher private let clearPushNotificationRoute: () -> Void @@ -25,6 +26,7 @@ public struct RootView: View { systemThemeUseCase: ObserveSystemThemeUseCase, trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase, widgetURLTab: @escaping (URL) -> MainTab?, + windowEvent: TodoEditorWindowEvent, pushNotificationTodoIdPublisher: AnyPublisher, clearPushNotificationRoute: @escaping () -> Void ) { @@ -35,6 +37,7 @@ public struct RootView: View { trackAnalyticsEventUseCase: trackAnalyticsEventUseCase )) self.widgetURLTab = widgetURLTab + self.windowEvent = windowEvent self.pushNotificationTodoIdPublisher = pushNotificationTodoIdPublisher self.clearPushNotificationRoute = clearPushNotificationRoute } @@ -46,6 +49,7 @@ public struct RootView: View { if signIn { MainView( container: container, + windowEvent: windowEvent, selectedTab: $selectedMainTab ) } else { From d8a8ac5f9aa974d088f31761f28904b81508af27 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:09:25 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EB=B3=80=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Home/HomeViewCoordinator.swift | 20 +++++++++---------- .../Sources/Home/TodoWindowCoordinator.swift | 20 +++++++++---------- .../PushNotificationListViewCoordinator.swift | 8 ++++---- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index 24661568..fbbc9410 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -15,12 +15,12 @@ import DevLogDomain final class HomeViewCoordinator { let viewModel: HomeViewModel let router = NavigationRouter() - private let diContainer: DIContainer + private let container: DIContainer @ObservationIgnored private var cancellable: AnyCancellable? init(container: DIContainer) { - self.diContainer = container + self.container = container self.viewModel = HomeViewModel( fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self), @@ -55,10 +55,10 @@ final class HomeViewCoordinator { func makeTodoEditorViewModel(category: TodoCategory) -> TodoEditorViewModel { TodoEditorViewModel( category: category, - fetchPreferencesUseCase: diContainer.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), - upsertTodoUseCase: diContainer.resolve(UpsertTodoUseCase.self), - trackAnalyticsEventUseCase: diContainer.resolve(TrackAnalyticsEventUseCase.self), + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), onUpsertSuccess: { [weak self] _ in self?.viewModel.send(.setPresentation(.todoEditor, false)) self?.viewModel.send(.fetchData) @@ -68,10 +68,10 @@ final class HomeViewCoordinator { func makeSearchViewModel() -> SearchViewModel { SearchViewModel( - fetchWebPagesUseCase: diContainer.resolve(FetchWebPagesUseCase.self), - fetchTodosUseCase: diContainer.resolve(FetchTodosUseCase.self), - fetchRecentSearchQueriesUseCase: diContainer.resolve(FetchRecentSearchQueriesUseCase.self), - updateRecentSearchQueriesUseCase: diContainer.resolve(UpdateRecentSearchQueriesUseCase.self) + fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), + fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), + fetchRecentSearchQueriesUseCase: container.resolve(FetchRecentSearchQueriesUseCase.self), + updateRecentSearchQueriesUseCase: container.resolve(UpdateRecentSearchQueriesUseCase.self) ) } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift index df14bb82..525daf60 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift @@ -13,7 +13,7 @@ import DevLogDomain @MainActor @Observable final class TodoWindowCoordinator { - private let diContainer: DIContainer + private let container: DIContainer @ObservationIgnored private var listViewModel: TodoListViewModel? @ObservationIgnored @@ -22,7 +22,7 @@ final class TodoWindowCoordinator { private var cancellable: AnyCancellable? init(container: DIContainer) { - self.diContainer = container + self.container = container } func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) { @@ -41,12 +41,12 @@ final class TodoWindowCoordinator { } let listViewModel = TodoListViewModel( - fetchTodosUseCase: diContainer.resolve(FetchTodosUseCase.self), - fetchTodoByIdUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), - upsertTodoUseCase: diContainer.resolve(UpsertTodoUseCase.self), - deleteTodoUseCase: diContainer.resolve(DeleteTodoUseCase.self), - undoDeleteTodoUseCase: diContainer.resolve(UndoDeleteTodoUseCase.self), - trackAnalyticsEventUseCase: diContainer.resolve(TrackAnalyticsEventUseCase.self), + fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), + fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + deleteTodoUseCase: container.resolve(DeleteTodoUseCase.self), + undoDeleteTodoUseCase: container.resolve(UndoDeleteTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), category: category ) self.listViewModel = listViewModel @@ -64,8 +64,8 @@ final class TodoWindowCoordinator { } let detailViewModel = TodoDetailViewModel( - fetchTodoUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), + fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), todoId: todoId, showEditButton: showEditButton ) diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift index e507a9fc..23b9bee8 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift @@ -14,12 +14,12 @@ import DevLogDomain final class PushNotificationListViewCoordinator { let viewModel: PushNotificationListViewModel var todoIdToPresent: TodoIdItem? - private let diContainer: DIContainer + private let container: DIContainer @ObservationIgnored private var todoDetailViewModel: TodoDetailViewModel? init(container: DIContainer) { - self.diContainer = container + self.container = container self.viewModel = PushNotificationListViewModel( fetchUseCase: container.resolve(FetchPushNotificationsUseCase.self), deleteUseCase: container.resolve(DeletePushNotificationUseCase.self), @@ -42,8 +42,8 @@ final class PushNotificationListViewCoordinator { } let todoDetailViewModel = TodoDetailViewModel( - fetchTodoUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), + fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), todoId: todoId, showEditButton: false )