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)) + } } } 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) + } +} diff --git a/DevLog/Presentation/ViewModel/TodoViewModel.swift b/DevLog/Presentation/ViewModel/TodoViewModel.swift index 7548cea..f60e833 100644 --- a/DevLog/Presentation/ViewModel/TodoViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoViewModel.swift @@ -11,10 +11,10 @@ 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 = "" + var showAlert: Bool = false + var alertMessage: String = "" var scope: TodoScope = .title var filterOption: FilterOption = .create var isLoading = false @@ -37,12 +37,14 @@ 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 didLoading(Bool) + case didShowAlert(String) case didTogglePinned(Todo) } @@ -53,13 +55,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) } @@ -80,14 +85,19 @@ 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 .didLoading(let value): + state.isLoading = value + 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 @@ -99,11 +109,26 @@ final class TodoViewModel: Store { func run(_ effect: SideEffect) { switch effect { case .fetchTodos: - break + Task { + do { + defer { send(.didLoading(false)) } + send(.didLoading(true)) + 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 { + defer { send(.didLoading(false)) } + send(.didLoading(true)) + 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/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 )) diff --git a/DevLog/UI/Home/TodoView.swift b/DevLog/UI/Home/TodoView.swift index d790919..825d2e6 100644 --- a/DevLog/UI/Home/TodoView.swift +++ b/DevLog/UI/Home/TodoView.swift @@ -12,70 +12,86 @@ 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)) } + ) + } } } } } + .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,14 +197,7 @@ struct TodoView: View { Text(scope.localizedName).tag(scope) } } - .task { - viewModel.send(.onAppear) - } - .overlay { - if viewModel.state.isLoading { - LoadingView() - } - } + .task { viewModel.send(.onAppear) } } private enum Path: Hashable {