From 68b18b59ad5e0729ff58d83eb1f78a10df907441 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 12:02:03 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=EC=9D=98=20Todo=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20=EC=9C=A0?= =?UTF-8?q?=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fetch/Todo/FetchTodosByKindUseCase.swift | 11 +++++++++++ .../Todo/FetchTodosByKindUseCaseImpl.swift | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCase.swift create mode 100644 DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCaseImpl.swift diff --git a/DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCase.swift b/DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCase.swift new file mode 100644 index 0000000..f69c98c --- /dev/null +++ b/DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCase.swift @@ -0,0 +1,11 @@ +// +// FetchTodosByKindUseCase.swift +// DevLog +// +// Created by 최윤진 on 2/1/26. +// + +protocol FetchTodosByKindUseCase { + var repository: TodoRepository { get } + func execute(_ kind: TodoKind) async throws -> [Todo] +} diff --git a/DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCaseImpl.swift b/DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCaseImpl.swift new file mode 100644 index 0000000..0fd9555 --- /dev/null +++ b/DevLog/Domain/UseCase/UserData/Fetch/Todo/FetchTodosByKindUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// FetchTodosByKindUseCaseImpl.swift +// DevLog +// +// Created by 최윤진 on 2/1/26. +// + +final class FetchTodosByKindUseCaseImpl: FetchTodosByKindUseCase { + let repository: TodoRepository + + init(_ repository: TodoRepository) { + self.repository = repository + } + + func execute(_ kind: TodoKind) async throws -> [Todo] { + return try await repository.fetchTodos(kind) + } +} From 050eed35614584d1e0ba459e75b680457423cdd5 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 12:30:52 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A6=88=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/DomainAssembler.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DevLog/App/Assembler/DomainAssembler.swift b/DevLog/App/Assembler/DomainAssembler.swift index b8862fc..bc9f9c1 100644 --- a/DevLog/App/Assembler/DomainAssembler.swift +++ b/DevLog/App/Assembler/DomainAssembler.swift @@ -46,5 +46,9 @@ final class DomainAssembler: Assembler { container.register(UpdatePushSettingsUseCase.self) { UpdatePushSettingsUseCaseImpl(container.resolve(PushNotificationRepository.self)) } + + container.register(FetchTodosByKindUseCase.self) { + FetchTodosByKindUseCaseImpl(container.resolve(TodoRepository.self)) + } } } From 787a2ab8ea63728c8fa0ee967c650f2cb3d1a68e Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 12:41:06 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A6=88=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Presentation/ViewModel/TodoViewModel.swift | 12 +++++++++++- DevLog/UI/Home/HomeView.swift | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoViewModel.swift b/DevLog/Presentation/ViewModel/TodoViewModel.swift index 7548cea..f2b28b9 100644 --- a/DevLog/Presentation/ViewModel/TodoViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoViewModel.swift @@ -11,7 +11,7 @@ final class TodoViewModel: Store { struct State { var todos: [Todo] = [] var searchText: String = "" - var kind: TodoKind + let kind: TodoKind var showEditor: Bool = false var showToast: Bool = false var toastMessage: String = "" @@ -53,13 +53,16 @@ final class TodoViewModel: Store { case swipeTodo(Todo) } + private let fetchTodosByKindUseCase: FetchTodosByKindUseCase private let upsertTodoUseCase: UpsertTodoUseCase @Published private(set) var state: State init( + fetchTodosByKindUseCase: FetchTodosByKindUseCase, upsertTodoUseCase: UpsertTodoUseCase, kind: TodoKind ) { + self.fetchTodosByKindUseCase = fetchTodosByKindUseCase self.upsertTodoUseCase = upsertTodoUseCase self.state = State(kind: kind) } @@ -100,6 +103,13 @@ final class TodoViewModel: Store { switch effect { case .fetchTodos: break + Task { + do { + let todos = try await fetchTodosByKindUseCase.execute(state.kind) + send(.didFetchTodos(todos)) + } catch { + } + } case .upsertTodo(let todo): Task { try await upsertTodoUseCase.execute(todo) diff --git a/DevLog/UI/Home/HomeView.swift b/DevLog/UI/Home/HomeView.swift index 00b1ef0..8f35c29 100644 --- a/DevLog/UI/Home/HomeView.swift +++ b/DevLog/UI/Home/HomeView.swift @@ -121,7 +121,8 @@ struct HomeView: View { .navigationDestination(for: Path.self) { path in switch path { case .kind(let todoKind): - TodoView(viewModel: TodoViewModel( + TodoView(viewModel:TodoViewModel( + fetchTodosByKindUseCase: container.resolve(FetchTodosByKindUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), kind: todoKind )) From 943da9d5f6181e8e4e26eefe8f04cf6d8aa82053 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 12:54:27 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EB=B0=9B=EC=95=84=EC=98=AC=20?= =?UTF-8?q?=EC=8B=9C=20=EC=98=A4=EB=A5=98=EA=B0=80=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=A9=B4=20=EC=96=BC=EB=9F=BF=EB=A5=BC=20=EB=9D=84?= =?UTF-8?q?=EC=9A=B0=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/TodoViewModel.swift | 22 ++++++++++++++----- DevLog/Resource/Localizable.xcstrings | 3 +++ DevLog/UI/Home/TodoView.swift | 16 +++++++++++--- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoViewModel.swift b/DevLog/Presentation/ViewModel/TodoViewModel.swift index f2b28b9..975cd48 100644 --- a/DevLog/Presentation/ViewModel/TodoViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoViewModel.swift @@ -13,8 +13,8 @@ final class TodoViewModel: Store { var searchText: String = "" let kind: TodoKind var showEditor: Bool = false - var showToast: Bool = false - var toastMessage: String = "" + var showAlert: Bool = false + var alertMessage: String = "" var scope: TodoScope = .title var filterOption: FilterOption = .create var isLoading = false @@ -37,12 +37,13 @@ final class TodoViewModel: Store { // Binding case openEditor case closeEditor - case closeToast + case closeAlert case setScope(TodoScope) case setSearchText(String) // Call from run case didFetchTodos([Todo]) + case didShowAlert(String) case didTogglePinned(Todo) } @@ -83,14 +84,17 @@ final class TodoViewModel: Store { state.showEditor = true case .closeEditor: state.showEditor = false - case .closeToast: - state.showToast = false + case .closeAlert: + state.showAlert = false case .setScope(let scope): state.scope = scope case .setSearchText(let text): state.searchText = text case .didFetchTodos(let todos): state.todos = todos + case .didShowAlert(let message): + state.alertMessage = message + state.showAlert = true case .didTogglePinned(let todo): if let index = state.todos.firstIndex(where: { $0.id == todo.id }) { state.todos[index] = todo @@ -102,18 +106,24 @@ final class TodoViewModel: Store { func run(_ effect: SideEffect) { switch effect { case .fetchTodos: - break Task { do { let todos = try await fetchTodosByKindUseCase.execute(state.kind) send(.didFetchTodos(todos)) } catch { + send(.didShowAlert(error.localizedDescription)) } } case .upsertTodo(let todo): Task { try await upsertTodoUseCase.execute(todo) send(.refresh) + do { + try await upsertTodoUseCase.execute(todo) + send(.refresh) + } catch { + send(.didShowAlert(error.localizedDescription)) + } } case .togglePinned(let todo): break diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index bdf905b..cc5e3f8 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -277,6 +277,9 @@ }, "베타 테스트 참여" : { + }, + "불러오기 실패" : { + }, "사용자 설정" : { diff --git a/DevLog/UI/Home/TodoView.swift b/DevLog/UI/Home/TodoView.swift index d790919..83d926e 100644 --- a/DevLog/UI/Home/TodoView.swift +++ b/DevLog/UI/Home/TodoView.swift @@ -76,6 +76,18 @@ struct TodoView: View { } } } + .alert("불러오기 실패", isPresented: Binding( + get: { viewModel.state.showAlert }, + set: { _, _ in } + )) { + Button(role: .cancel, action: { + viewModel.send(.closeAlert) + }) { + Text("확인") + } + } message: { + Text(viewModel.state.alertMessage) + } .navigationTitle(viewModel.state.kind.localizedName) .navigationBarTitleDisplayMode(.large) .fullScreenCover(isPresented: Binding( @@ -181,9 +193,7 @@ struct TodoView: View { Text(scope.localizedName).tag(scope) } } - .task { - viewModel.send(.onAppear) - } + .task { viewModel.send(.onAppear) } .overlay { if viewModel.state.isLoading { LoadingView() From 350a1c1681a0aaab3ab4607680d36523c7833a54 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 12:54:57 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=8B=9C=20=EB=A1=9C=EB=94=A9=20=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Presentation/ViewModel/TodoViewModel.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoViewModel.swift b/DevLog/Presentation/ViewModel/TodoViewModel.swift index 975cd48..f60e833 100644 --- a/DevLog/Presentation/ViewModel/TodoViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoViewModel.swift @@ -43,6 +43,7 @@ final class TodoViewModel: Store { // Call from run case didFetchTodos([Todo]) + case didLoading(Bool) case didShowAlert(String) case didTogglePinned(Todo) } @@ -92,6 +93,8 @@ final class TodoViewModel: Store { state.searchText = text case .didFetchTodos(let todos): state.todos = todos + case .didLoading(let value): + state.isLoading = value case .didShowAlert(let message): state.alertMessage = message state.showAlert = true @@ -108,6 +111,8 @@ final class TodoViewModel: Store { case .fetchTodos: Task { do { + defer { send(.didLoading(false)) } + send(.didLoading(true)) let todos = try await fetchTodosByKindUseCase.execute(state.kind) send(.didFetchTodos(todos)) } catch { @@ -116,9 +121,9 @@ final class TodoViewModel: Store { } case .upsertTodo(let todo): Task { - try await upsertTodoUseCase.execute(todo) - send(.refresh) do { + defer { send(.didLoading(false)) } + send(.didLoading(true)) try await upsertTodoUseCase.execute(todo) send(.refresh) } catch { From 7a1855fcbc38fb7b7ac64267874e2e96aad73827 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 13:05:37 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20LoadingVIew=EB=A5=BC=20?= =?UTF-8?q?=EB=9D=84=EC=9A=B8=20=EC=8B=9C=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoView.swift | 111 +++++++++++++++++----------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/DevLog/UI/Home/TodoView.swift b/DevLog/UI/Home/TodoView.swift index 83d926e..825d2e6 100644 --- a/DevLog/UI/Home/TodoView.swift +++ b/DevLog/UI/Home/TodoView.swift @@ -12,66 +12,70 @@ struct TodoView: View { @EnvironmentObject var router: NavigationRouter var body: some View { - VStack { - if viewModel.state.todos.isEmpty { - VStack { - Spacer() - Text("작성된 내용이 없습니다.") - .foregroundStyle(Color.gray) - Spacer() - } - .frame(maxWidth: .infinity, alignment: .center) + ZStack { + if viewModel.state.isLoading { + LoadingView() } else { - List(viewModel.state.todos) { todo in - Button { - router.push(Path.detail(todo)) - } label: { - VStack(alignment: .leading, spacing: 5) { - HStack { - if todo.isPinned { - Image(systemName: "star.fill") + if viewModel.state.todos.isEmpty { + VStack { + Spacer() + Text("작성된 내용이 없습니다.") + .foregroundStyle(Color.gray) + Spacer() + } + .frame(maxWidth: .infinity, alignment: .center) + } else { + List(viewModel.state.todos) { todo in + Button { + router.push(Path.detail(todo)) + } label: { + VStack(alignment: .leading, spacing: 5) { + HStack { + if todo.isPinned { + Image(systemName: "star.fill") + .font(.headline) + .foregroundStyle(Color.orange) + } + Text(todo.title) .font(.headline) - .foregroundStyle(Color.orange) + .lineLimit(1) } - Text(todo.title) - .font(.headline) + Text(todo.content) + .font(.subheadline) + .foregroundStyle(Color.gray) .lineLimit(1) } - Text(todo.content) - .font(.subheadline) - .foregroundStyle(Color.gray) - .lineLimit(1) + .padding(.vertical, 5) } - .padding(.vertical, 5) - } - .swipeActions(edge: .leading) { - Button(action: { - viewModel.send(.tapTogglePinned(todo)) - }) { - Image(systemName: "star\(todo.isPinned ? ".slash" : ".fill")") - } - .tint(Color.orange) - } - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button(role: .destructive, action: { - viewModel.send(.swipeTodo(todo)) - }) { - Image(systemName: "trash") + .swipeActions(edge: .leading) { + Button(action: { + viewModel.send(.tapTogglePinned(todo)) + }) { + Image(systemName: "star\(todo.isPinned ? ".slash" : ".fill")") + } + .tint(Color.orange) } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive, action: { + viewModel.send(.swipeTodo(todo)) + }) { + Image(systemName: "trash") + } + } } - } - .listStyle(.plain) - .refreshable { - viewModel.send(.refresh) - } - .navigationDestination(for: Path.self) { path in - switch path { - case .detail(let todo): - TodoDetailView( - todo: todo, - onSubmit: { viewModel.send(.upsertTodo($0)) } - ) + .listStyle(.plain) + .refreshable { + viewModel.send(.refresh) + } + .navigationDestination(for: Path.self) { path in + switch path { + case .detail(let todo): + TodoDetailView( + todo: todo, + onSubmit: { viewModel.send(.upsertTodo($0)) } + ) + } } } } @@ -194,11 +198,6 @@ struct TodoView: View { } } .task { viewModel.send(.onAppear) } - .overlay { - if viewModel.state.isLoading { - LoadingView() - } - } } private enum Path: Hashable {