From 096b9d3ca244b10f860cd770638f4b618d0e297f Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 10 May 2026 21:00:19 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20pr=20=ED=85=9C=ED=94=8C=EB=A6=BF?= =?UTF-8?q?=EC=97=90=20=EC=9E=91=EC=97=85=20=EC=9D=98=EB=8F=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pull_request_template.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cca6ad61..c418bf6b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,10 +3,12 @@ - closed #이슈번호 +## 🎯 의도 + ## 📝 작업 내용 ### 📌 요약 ### 🔍 상세 -## 📸 영상 / 이미지 (Optional) \ No newline at end of file +## 📸 영상 / 이미지 (Optional) From 451341e0afab4f27fb9e3f8423a5226443f17271 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 10 May 2026 21:20:33 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EB=B7=B0=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=97=AD=ED=95=A0=EC=9D=84=20=EB=B7=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=BD=94=EB=94=94=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Main/MainView.swift | 33 ++------------- DevLog/UI/Main/MainViewCoordinator.swift | 51 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/DevLog/UI/Main/MainView.swift b/DevLog/UI/Main/MainView.swift index fb461ef1..7701fa90 100644 --- a/DevLog/UI/Main/MainView.swift +++ b/DevLog/UI/Main/MainView.swift @@ -13,13 +13,11 @@ struct MainView: View { @State private var homeViewCoordinator: HomeViewCoordinator @State private var todayViewCoordinator: TodayViewCoordinator @Binding var selectedTab: MainTab - private let container: DIContainer init( container: DIContainer, selectedTab: Binding ) { - self.container = container self._coordinator = State(initialValue: MainViewCoordinator(container: container)) self._homeViewCoordinator = State(initialValue: HomeViewCoordinator(container: container)) self._todayViewCoordinator = State(initialValue: TodayViewCoordinator(container: container)) @@ -117,7 +115,7 @@ struct MainView: View { Group { if let todoId = coordinator.todoIdToPresent?.id { TodoDetailView( - viewModel: makeTodoDetailViewModel( + viewModel: coordinator.todoDetailViewModel( todoId: todoId, showEditButton: false ) @@ -235,11 +233,11 @@ struct MainView: View { switch homeRoute { case .category(let item): TodoListView( - viewModel: makeTodoListViewModel(category: item.todoCategory) + viewModel: coordinator.todoListViewModel(category: item.todoCategory) ) .id(item.id) case .todo(let item): - TodoDetailView(viewModel: makeTodoDetailViewModel(todoId: item.id)) + TodoDetailView(viewModel: coordinator.todoDetailViewModel(todoId: item.id)) .id(item.id) case .webPage(let item): WebView(url: item.url) @@ -302,7 +300,7 @@ struct MainView: View { private func todayDestinationView(_ todayRoute: TodayRoute) -> some View { switch todayRoute { case .todo(let item): - TodoDetailView(viewModel: makeTodoDetailViewModel(todoId: item.id)) + TodoDetailView(viewModel: coordinator.todoDetailViewModel(todoId: item.id)) .id(item.id) } } @@ -378,29 +376,6 @@ private extension MainView { ) } - func makeTodoListViewModel(category: TodoCategory) -> TodoListViewModel { - TodoListViewModel( - 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), - category: category - ) - } - - func makeTodoDetailViewModel( - todoId: String, - showEditButton: Bool = true - ) -> TodoDetailViewModel { - TodoDetailViewModel( - fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), - todoId: todoId, - showEditButton: showEditButton - ) - } } private enum MainTabSplitStyle { diff --git a/DevLog/UI/Main/MainViewCoordinator.swift b/DevLog/UI/Main/MainViewCoordinator.swift index 3b2ebb10..9e653d0b 100644 --- a/DevLog/UI/Main/MainViewCoordinator.swift +++ b/DevLog/UI/Main/MainViewCoordinator.swift @@ -14,8 +14,14 @@ final class MainViewCoordinator { let pushNotificationListViewModel: PushNotificationListViewModel let profileViewModel: ProfileViewModel var todoIdToPresent: TodoIdItem? + private let diContainer: DIContainer + @ObservationIgnored + private var todoListViewModels = [TodoCategory: TodoListViewModel]() + @ObservationIgnored + private var todoDetailViewModels = [TodoDetailViewModelKey: TodoDetailViewModel]() init(container: DIContainer) { + self.diContainer = container self.mainViewModel = MainViewModel( unreadPushCountUseCase: container.resolve(ObserveUnreadPushCountUseCase.self) ) @@ -36,4 +42,49 @@ final class MainViewCoordinator { updateHeatmapActivityTypesUseCase: container.resolve(UpdateHeatmapActivityTypesUseCase.self) ) } + + func todoListViewModel(category: TodoCategory) -> TodoListViewModel { + if let todoListViewModel = todoListViewModels[category] { + return todoListViewModel + } + + let todoListViewModel = 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), + category: category + ) + todoListViewModels[category] = todoListViewModel + return todoListViewModel + } + + func todoDetailViewModel( + todoId: String, + showEditButton: Bool = true + ) -> TodoDetailViewModel { + let todoDetailViewModelKey = TodoDetailViewModelKey( + todoId: todoId, + showEditButton: showEditButton + ) + if let todoDetailViewModel = todoDetailViewModels[todoDetailViewModelKey] { + return todoDetailViewModel + } + + let todoDetailViewModel = TodoDetailViewModel( + fetchTodoUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), + fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), + upsertUseCase: diContainer.resolve(UpsertTodoUseCase.self), + todoId: todoId, + showEditButton: showEditButton + ) + todoDetailViewModels[todoDetailViewModelKey] = todoDetailViewModel + return todoDetailViewModel + } + + private struct TodoDetailViewModelKey: Hashable { + let todoId: String + let showEditButton: Bool + } } From 8a12019d19024e279164cb40f9ccb505eb360c1b Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 10 May 2026 23:48:56 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20MainViewCoordinator=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=ED=99=94=EB=A9=B4=20ViewModel=20=EB=B3=B4=EA=B4=80?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=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 MainViewCoordinator가 생성하는 하위 화면 ViewModel을 단일 인스턴스 기준으로 보관하도록 변경 - TodoListViewModel은 현재 category와 일치할 때 기존 인스턴스 재사용 - TodoDetailViewModel은 현재 todoId와 showEditButton 조건이 일치할 때 기존 인스턴스 재사용 - category별 dictionary와 todoId별 dictionary 제거 - ViewModel이 가진 생성 기준 값을 재사용 조건으로 활용하여 코디네이터의 별도 key 상태 제거 - compact/regular 전환으로 같은 하위 화면이 재생성될 때 기존 ViewModel 인스턴스를 유지하는 구조 --- .../ViewModel/TodoDetailViewModel.swift | 2 +- .../ViewModel/TodoListViewModel.swift | 4 ++++ DevLog/UI/Main/MainViewCoordinator.swift | 24 +++++++------------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift b/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift index 6ceb674f..c3c4e9eb 100644 --- a/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoDetailViewModel.swift @@ -40,11 +40,11 @@ final class TodoDetailViewModel: Store { } private(set) var state: State = .init() + let todoId: String let showEditButton: Bool private let fetchTodoUseCase: FetchTodoByIdUseCase private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase private let upsertUseCase: UpsertTodoUseCase - private let todoId: String private let loadingState = LoadingState() init( diff --git a/DevLog/Presentation/ViewModel/TodoListViewModel.swift b/DevLog/Presentation/ViewModel/TodoListViewModel.swift index 9700fc49..40726987 100644 --- a/DevLog/Presentation/ViewModel/TodoListViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoListViewModel.swift @@ -114,6 +114,10 @@ final class TodoListViewModel: Store { let searchResultsLimit = 5 + var category: TodoCategory { + state.category + } + var appliedFilterCount: Int { var count = 0 if state.query.sortTarget != .createdAt { count += 1 } diff --git a/DevLog/UI/Main/MainViewCoordinator.swift b/DevLog/UI/Main/MainViewCoordinator.swift index 9e653d0b..8e89fe9e 100644 --- a/DevLog/UI/Main/MainViewCoordinator.swift +++ b/DevLog/UI/Main/MainViewCoordinator.swift @@ -16,9 +16,9 @@ final class MainViewCoordinator { var todoIdToPresent: TodoIdItem? private let diContainer: DIContainer @ObservationIgnored - private var todoListViewModels = [TodoCategory: TodoListViewModel]() + private var todoListViewModel: TodoListViewModel? @ObservationIgnored - private var todoDetailViewModels = [TodoDetailViewModelKey: TodoDetailViewModel]() + private var todoDetailViewModel: TodoDetailViewModel? init(container: DIContainer) { self.diContainer = container @@ -44,7 +44,8 @@ final class MainViewCoordinator { } func todoListViewModel(category: TodoCategory) -> TodoListViewModel { - if let todoListViewModel = todoListViewModels[category] { + if let todoListViewModel, + todoListViewModel.category == category { return todoListViewModel } @@ -56,7 +57,7 @@ final class MainViewCoordinator { undoDeleteTodoUseCase: diContainer.resolve(UndoDeleteTodoUseCase.self), category: category ) - todoListViewModels[category] = todoListViewModel + self.todoListViewModel = todoListViewModel return todoListViewModel } @@ -64,11 +65,9 @@ final class MainViewCoordinator { todoId: String, showEditButton: Bool = true ) -> TodoDetailViewModel { - let todoDetailViewModelKey = TodoDetailViewModelKey( - todoId: todoId, - showEditButton: showEditButton - ) - if let todoDetailViewModel = todoDetailViewModels[todoDetailViewModelKey] { + if let todoDetailViewModel, + todoDetailViewModel.todoId == todoId, + todoDetailViewModel.showEditButton == showEditButton { return todoDetailViewModel } @@ -79,12 +78,7 @@ final class MainViewCoordinator { todoId: todoId, showEditButton: showEditButton ) - todoDetailViewModels[todoDetailViewModelKey] = todoDetailViewModel + self.todoDetailViewModel = todoDetailViewModel return todoDetailViewModel } - - private struct TodoDetailViewModelKey: Hashable { - let todoId: String - let showEditButton: Bool - } } From bba0dbfffd7bd854a76123b7aa72b42329874583 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 10 May 2026 23:54:56 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20category=EB=A5=BC=20=EC=83=81?= =?UTF-8?q?=ED=83=9C(State)=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/ViewModel/TodoListViewModel.swift | 12 ++++-------- DevLog/UI/Home/TodoListView.swift | 10 +++++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoListViewModel.swift b/DevLog/Presentation/ViewModel/TodoListViewModel.swift index 40726987..86643ab6 100644 --- a/DevLog/Presentation/ViewModel/TodoListViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoListViewModel.swift @@ -13,7 +13,6 @@ final class TodoListViewModel: Store { var todos: [TodoListItem] = [] var searchText: String = "" var searchResults: [TodoListItem] = [] - let category: TodoCategory var showEditor: Bool = false var showAlert: Bool = false var alertTitle: String = "" @@ -81,6 +80,7 @@ final class TodoListViewModel: Store { case request } + let category: TodoCategory private(set) var state: State private let fetchTodosUseCase: FetchTodosUseCase private let fetchTodoByIdUseCase: FetchTodoByIdUseCase @@ -106,18 +106,14 @@ final class TodoListViewModel: Store { self.upsertTodoUseCase = upsertTodoUseCase self.deleteTodoUseCase = deleteTodoUseCase self.undoDeleteTodoUseCase = undoDeleteTodoUseCase + self.category = category self.state = State( - category: category, query: TodoQuery(category: category) ) } let searchResultsLimit = 5 - var category: TodoCategory { - state.category - } - var appliedFilterCount: Int { var count = 0 if state.query.sortTarget != .createdAt { count += 1 } @@ -195,7 +191,7 @@ final class TodoListViewModel: Store { self.endLoading(.immediate) } } - let query = TodoQuery(category: state.category, keyword: keyword) + let query = TodoQuery(category: category, keyword: keyword) let page = try await fetchTodosUseCase.execute(query, cursor: nil) if Task.isCancelled { return } send(.fetchSearchResults(page.items.compactMap { TodoListItem(from: $0) })) @@ -311,7 +307,7 @@ private extension TodoListViewModel { self.nextCursor = nil return [.fetch] case .resetFilters: - state.query = TodoQuery(category: state.category) + state.query = TodoQuery(category: category) self.nextCursor = nil return [.fetch] case .setIsSearching(let value): diff --git a/DevLog/UI/Home/TodoListView.swift b/DevLog/UI/Home/TodoListView.swift index 9dfcb9d8..9a99bf51 100644 --- a/DevLog/UI/Home/TodoListView.swift +++ b/DevLog/UI/Home/TodoListView.swift @@ -45,7 +45,7 @@ struct TodoListView: View { prompt: Text( String.localizedStringWithFormat( String(localized: "todo_list_search_prompt_format"), - TodoCategoryItem(from: viewModel.state.category).localizedName + TodoCategoryItem(from: viewModel.category).localizedName ) ) ) @@ -71,14 +71,14 @@ struct TodoListView: View { ) { Label(viewModel.state.toastMessage, systemImage: "arrow.uturn.left") } - .navigationTitle(TodoCategoryItem(from: viewModel.state.category).localizedName) + .navigationTitle(TodoCategoryItem(from: viewModel.category).localizedName) .fullScreenCover(isPresented: Binding( get: { viewModel.state.showEditor }, set: { viewModel.send(.setShowEditor($0)) } )) { TodoEditorView( viewModel: TodoEditorViewModel( - category: viewModel.state.category, + category: viewModel.category, fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) ), @@ -222,7 +222,7 @@ struct TodoListView: View { prompt: Text( String.localizedStringWithFormat( String(localized: "todo_list_search_prompt_format"), - TodoCategoryItem(from: viewModel.state.category).localizedName + TodoCategoryItem(from: viewModel.category).localizedName ) ) ) @@ -240,7 +240,7 @@ struct TodoListView: View { Text( String.localizedStringWithFormat( String(localized: "todo_list_search_instruction_format"), - TodoCategoryItem(from: viewModel.state.category).localizedName + TodoCategoryItem(from: viewModel.category).localizedName ) ) .foregroundStyle(Color.gray)