From 3c425013eb6eedffc7554ddefd7dd1cb0ff30185 Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 16 Mar 2026 12:22:58 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EB=B9=84?= =?UTF-8?q?=EC=8A=B7=ED=95=9C=20=EC=95=A1=EC=85=98=20=EC=A7=91=ED=95=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=9B=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/HomeViewModel.swift | 200 ++++++++++-------- DevLog/UI/Home/HomeView.swift | 16 +- 2 files changed, 114 insertions(+), 102 deletions(-) diff --git a/DevLog/Presentation/ViewModel/HomeViewModel.swift b/DevLog/Presentation/ViewModel/HomeViewModel.swift index b6868a8..c525bed 100644 --- a/DevLog/Presentation/ViewModel/HomeViewModel.swift +++ b/DevLog/Presentation/ViewModel/HomeViewModel.swift @@ -18,8 +18,6 @@ final class HomeViewModel: Store { var showSearchView: Bool = false var webPageURLInput: String = "https://" var selectedTodoKind: TodoKind? - var searchText: String = "" - var isSearching: Bool = false var reorderTodo: Bool = false var isRecentTodosLoading: Bool = false var isWebPageLoading: Bool = false @@ -34,28 +32,21 @@ final class HomeViewModel: Store { } enum Action { + case onAppear + case setPresentation(Presentation, Bool) + case setAlert(isPresented: Bool, type: AlertType? = nil) + case setToast(isPresented: Bool, type: ToastType? = nil) + case setLoading(LoadingTarget, Bool) case tapTodoKind(TodoKind) case orderTodoKindPreferences([TodoKindPreference]) - case setReorderTodo(Bool) - case setShowTodoEditor(Bool) - case setShowContentPicker(Bool) - case setShowSearchView(Bool) - case setAlert(isPresented: Bool, type: AlertType? = nil) - case onAppear - case updateWebPageURLInput(String) - case updateSearching(Bool) - case updateSearchText(String) case addTodo(Todo) + case updateRecentTodos([RecentTodoItem]) + case updateWebPageURLInput(String) case addWebPage case deleteWebPage(WebPageItem) case undoDeleteWebPage - case setToast(isPresented: Bool, type: ToastType? = nil) - case fetchRecentTodos([RecentTodoItem]) - case fetchWebPages([WebPageItem]) + case updateWebPages([WebPageItem]) case restoreWebPage(WebPageItem, Int) - case setRecentTodosLoading(Bool) - case setWebPageLoading(Bool) - case setAppending(Bool) } enum SideEffect { @@ -83,6 +74,19 @@ final class HomeViewModel: Store { case urlInputAlert } + enum Presentation { + case reorderTodo + case todoEditor + case contentPicker + case searchView + } + + enum LoadingTarget { + case recentTodos + case webPage + case overlay + } + private(set) var state = State() private let upsertTodoUseCase: UpsertTodoUseCase private let addWebPageUseCase: AddWebPageUseCase @@ -113,17 +117,12 @@ final class HomeViewModel: Store { var effects: [SideEffect] = [] switch action { - case .tapTodoKind, .orderTodoKindPreferences, .setReorderTodo, - .setShowTodoEditor, .setShowContentPicker, .setShowSearchView, - .updateWebPageURLInput, .setAlert, .deleteWebPage, - .undoDeleteWebPage, .setToast: - effects = reduceByUser(action, state: &state) - - case .onAppear, .updateSearching, .updateSearchText, .addTodo, .addWebPage: + case .onAppear, .setPresentation, .setAlert, .setToast, .tapTodoKind, + .orderTodoKindPreferences, .addTodo, .updateWebPageURLInput, + .addWebPage, .deleteWebPage, .undoDeleteWebPage: effects = reduceByView(action, state: &state) - case .fetchRecentTodos, .fetchWebPages, .restoreWebPage, .setRecentTodosLoading, - .setWebPageLoading, .setAppending: + case .setLoading, .updateRecentTodos, .updateWebPages, .restoreWebPage: effects = reduceByRun(action, state: &state) } @@ -136,14 +135,15 @@ final class HomeViewModel: Store { case .addTodo(let todo): Task { do { - send(.setAppending(true)) + defer { send(.setLoading(.overlay, false)) } + send(.setLoading(.overlay, true)) try await upsertTodoUseCase.execute(todo) let page = try await fetchRecentTodos() let items = page.items .filter { $0.createdAt != $0.updatedAt } .prefix(5) .map { RecentTodoItem(from: $0) } - send(.fetchRecentTodos(items)) + send(.updateRecentTodos(items)) } catch { send(.setAlert(isPresented: true, type: .error)) } @@ -151,14 +151,14 @@ final class HomeViewModel: Store { case .fetchRecentTodos: Task { do { - defer { send(.setRecentTodosLoading(false)) } - send(.setRecentTodosLoading(true)) + defer { send(.setLoading(.recentTodos, false)) } + send(.setLoading(.recentTodos, true)) let page = try await fetchRecentTodos() let items = page.items .filter { $0.createdAt != $0.updatedAt } .prefix(5) .map { RecentTodoItem(from: $0) } - send(.fetchRecentTodos(items)) + send(.updateRecentTodos(items)) } catch { send(.setAlert(isPresented: true, type: .error)) } @@ -166,11 +166,11 @@ final class HomeViewModel: Store { case .addWebPage(let urlString): Task { do { - defer { send(.setAppending(false)) } - send(.setAppending(true)) + defer { send(.setLoading(.overlay, false)) } + send(.setLoading(.overlay, true)) try await addWebPageUseCase.execute(urlString) let pages = try await fetchWebPagesUseCase.execute("") - send(.fetchWebPages(pages.map { WebPageItem(from: $0) })) + send(.updateWebPages(pages.map { WebPageItem(from: $0) })) } catch { send(.setAlert(isPresented: true, type: .error)) } @@ -178,8 +178,8 @@ final class HomeViewModel: Store { case .deleteWebPage(let page, let index): Task { do { - defer { send(.setWebPageLoading(false)) } - send(.setWebPageLoading(true)) + defer { send(.setLoading(.webPage, false)) } + send(.setLoading(.webPage, true)) try await deleteWebPageUseCase.execute(page.url.absoluteString) } catch { send(.restoreWebPage(page, index)) @@ -188,8 +188,8 @@ final class HomeViewModel: Store { } case .undoDeleteWebPage(let urlString): Task { - defer { send(.setWebPageLoading(false)) } - send(.setWebPageLoading(true)) + defer { send(.setLoading(.webPage, false)) } + send(.setLoading(.webPage, true)) var shouldPresentError = false @@ -201,7 +201,7 @@ final class HomeViewModel: Store { do { let pages = try await fetchWebPagesUseCase.execute("") - send(.fetchWebPages(pages.map { WebPageItem(from: $0) })) + send(.updateWebPages(pages.map { WebPageItem(from: $0) })) } catch { shouldPresentError = true } @@ -213,10 +213,10 @@ final class HomeViewModel: Store { case .fetchWebPages: Task { do { - defer { send(.setWebPageLoading(false)) } - send(.setWebPageLoading(true)) + defer { send(.setLoading(.webPage, false)) } + send(.setLoading(.webPage, true)) let pages = try await fetchWebPagesUseCase.execute("") - send(.fetchWebPages(pages.map { WebPageItem(from: $0) })) + send(.updateWebPages(pages.map { WebPageItem(from: $0) })) } catch { send(.setAlert(isPresented: true, type: .error)) } @@ -226,7 +226,7 @@ final class HomeViewModel: Store { try await Task.sleep(for: .seconds(0.1)) switch type { case .todoEditor: - send(.setShowTodoEditor(true)) + send(.setPresentation(.todoEditor, true)) case .urlInputAlert: send(.setAlert(isPresented: true, type: .webPageInput)) } @@ -237,63 +237,34 @@ final class HomeViewModel: Store { // MARK: - Reduce Methods private extension HomeViewModel { - func reduceByUser(_ action: Action, state: inout State) -> [SideEffect] { + // swiftlint:disable cyclomatic_complexity + func reduceByView(_ action: Action, state: inout State) -> [SideEffect] { switch action { - case .tapTodoKind(let kind): - state.selectedTodoKind = kind - state.showContentPicker = false - return [.showModalAfterDelay(.todoEditor)] - case .orderTodoKindPreferences(let preferences): - state.todoKindPreferences = preferences - case .setReorderTodo(let presented): - state.reorderTodo = presented - case .setShowTodoEditor(let presented): - state.showTodoEditor = presented - if !presented { state.selectedTodoKind = nil } - case .setShowContentPicker(let presented): - state.showContentPicker = presented - case .setShowSearchView(let presented): - state.showSearchView = presented - case .updateWebPageURLInput(let text): - state.webPageURLInput = text + case .onAppear: + return [.fetchRecentTodos, .fetchWebPages] + case .setPresentation(let presentation, let isPresented): + setPresentation(&state, presentation: presentation, isPresented: isPresented) case .setAlert(let presented, let type): if presented && type == .webPageInput && state.showContentPicker { state.showContentPicker = false return [.showModalAfterDelay(.urlInputAlert)] } setAlert(&state, isPresented: presented, type: type) - case .deleteWebPage(let page): - if let index = state.webPages.firstIndex(where: { $0.id == page.id }) { - deletedWebPageURLString = page.url.absoluteString - state.webPages.remove(at: index) - setToast(&state, isPresented: true, for: .deleteWebPage) - return [.deleteWebPage(page, index)] - } - case .undoDeleteWebPage: - guard let deletedWebPageURLString else { return [] } - self.deletedWebPageURLString = nil - return [.undoDeleteWebPage(deletedWebPageURLString)] case .setToast(let isPresented, let type): setToast(&state, isPresented: isPresented, for: type) if !isPresented { deletedWebPageURLString = nil } - default: - break - } - return [] - } - - func reduceByView(_ action: Action, state: inout State) -> [SideEffect] { - switch action { - case .onAppear: - return [.fetchRecentTodos, .fetchWebPages] - case .updateSearching(let isSearching): - state.isSearching = isSearching - case .updateSearchText(let text): - state.searchText = text + case .tapTodoKind(let kind): + state.selectedTodoKind = kind + state.showContentPicker = false + return [.showModalAfterDelay(.todoEditor)] + case .orderTodoKindPreferences(let preferences): + state.todoKindPreferences = preferences case .addTodo(let todo): return [.addTodo(todo)] + case .updateWebPageURLInput(let text): + state.webPageURLInput = text case .addWebPage: guard let normalizedURL = normalizedWebPageURL(state.webPageURLInput) else { setAlert(&state, isPresented: true, type: .invalidURL) @@ -301,17 +272,31 @@ private extension HomeViewModel { } setAlert(&state, isPresented: false, type: nil) return [.addWebPage(normalizedURL)] + case .deleteWebPage(let page): + if let index = state.webPages.firstIndex(where: { $0.id == page.id }) { + deletedWebPageURLString = page.url.absoluteString + state.webPages.remove(at: index) + setToast(&state, isPresented: true, for: .deleteWebPage) + return [.deleteWebPage(page, index)] + } + case .undoDeleteWebPage: + guard let deletedWebPageURLString else { return [] } + self.deletedWebPageURLString = nil + return [.undoDeleteWebPage(deletedWebPageURLString)] default: break } return [] } + // swiftlint:enable cyclomatic_complexity func reduceByRun(_ action: Action, state: inout State) -> [SideEffect] { switch action { - case .fetchRecentTodos(let todos): + case .setLoading(let loadingTarget, let isLoading): + setLoading(&state, loadingTarget: loadingTarget, isLoading: isLoading) + case .updateRecentTodos(let todos): state.recentTodos = todos - case .fetchWebPages(let pages): + case .updateWebPages(let pages): state.webPages = pages case .restoreWebPage(let page, let index): if state.webPages.contains(where: { $0.id == page.id }) { break } @@ -323,12 +308,6 @@ private extension HomeViewModel { if deletedWebPageURLString == page.url.absoluteString { deletedWebPageURLString = nil } - case .setRecentTodosLoading(let isLoading): - state.isRecentTodosLoading = isLoading - case .setWebPageLoading(let isLoading): - state.isWebPageLoading = isLoading - case .setAppending(let isLoading): - state.isAppending = isLoading default: break } @@ -338,6 +317,24 @@ private extension HomeViewModel { // MARK: - Helper Methods private extension HomeViewModel { + func setPresentation( + _ state: inout State, + presentation: Presentation, + isPresented: Bool + ) { + switch presentation { + case .reorderTodo: + state.reorderTodo = isPresented + case .todoEditor: + state.showTodoEditor = isPresented + if !isPresented { state.selectedTodoKind = nil } + case .contentPicker: + state.showContentPicker = isPresented + case .searchView: + state.showSearchView = isPresented + } + } + func setAlert( _ state: inout State, isPresented: Bool, @@ -377,6 +374,21 @@ private extension HomeViewModel { state.toastType = type } + func setLoading( + _ state: inout State, + loadingTarget: LoadingTarget, + isLoading: Bool + ) { + switch loadingTarget { + case .recentTodos: + state.isRecentTodosLoading = isLoading + case .webPage: + state.isWebPageLoading = isLoading + case .overlay: + state.isAppending = isLoading + } + } + func normalizedWebPageURL(_ input: String) -> String? { let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } diff --git a/DevLog/UI/Home/HomeView.swift b/DevLog/UI/Home/HomeView.swift index 0d1fdcf..f377ba8 100644 --- a/DevLog/UI/Home/HomeView.swift +++ b/DevLog/UI/Home/HomeView.swift @@ -56,12 +56,12 @@ struct HomeView: View { .toolbar { toolbar } .sheet(isPresented: Binding( get: { viewModel.state.reorderTodo }, - set: { viewModel.send(.setReorderTodo($0)) } + set: { viewModel.send(.setPresentation(.reorderTodo, $0)) } )) { TodoManageView( viewModel: TodoManageViewModel(viewModel.state.todoKindPreferences), onDismiss: { array in - viewModel.send(.setReorderTodo(false)) + viewModel.send(.setPresentation(.reorderTodo, false)) withAnimation { viewModel.send(.orderTodoKindPreferences(array)) } @@ -76,7 +76,7 @@ struct HomeView: View { } .fullScreenCover(isPresented: Binding( get: { viewModel.state.showTodoEditor }, - set: { viewModel.send(.setShowTodoEditor($0)) } + set: { viewModel.send(.setPresentation(.todoEditor, $0)) } )) { if let selectedKind = viewModel.state.selectedTodoKind { TodoEditorView( @@ -87,7 +87,7 @@ struct HomeView: View { } .fullScreenCover(isPresented: Binding( get: { viewModel.state.showSearchView }, - set: { viewModel.send(.setShowSearchView($0)) } + set: { viewModel.send(.setPresentation(.searchView, $0)) } )) { SearchView(viewModel: SearchViewModel( fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), @@ -177,7 +177,7 @@ struct HomeView: View { .bold() Spacer() Button(action: { - viewModel.send(.setReorderTodo(true)) + viewModel.send(.setPresentation(.reorderTodo, true)) }) { Image(systemName: "ellipsis") .font(.title2) @@ -251,7 +251,7 @@ struct HomeView: View { private var toolbar: some ToolbarContent { ToolbarItem(placement: .topBarTrailing) { Button { - viewModel.send(.setShowContentPicker(true)) + viewModel.send(.setPresentation(.contentPicker, true)) } label: { Image(systemName: "plus") } @@ -261,7 +261,7 @@ struct HomeView: View { } ToolbarItemGroup(placement: .topBarTrailing) { Button { - viewModel.send(.setShowSearchView(true)) + viewModel.send(.setPresentation(.searchView, true)) } label: { Image(systemName: "magnifyingglass") } @@ -327,7 +327,7 @@ struct HomeView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { Button { - viewModel.send(.setShowContentPicker(false)) + viewModel.send(.setPresentation(.contentPicker, false)) } label: { Image(systemName: "xmark") .bold()