diff --git a/Mark-In/Sources/App/DIContainer.swift b/Mark-In/Sources/App/DIContainer.swift index c6de151..801a09d 100644 --- a/Mark-In/Sources/App/DIContainer.swift +++ b/Mark-In/Sources/App/DIContainer.swift @@ -52,5 +52,24 @@ extension DIContainer { register(folderRepository) register(linkRepository) + + /// UseCase + let fetchLinkListUseCase: FetchLinkListUseCase = FetchLinkListUseCaseImpl( + linkRepository: linkRepository + ) + let fetchFolderListUseCase: FetchFolderListUseCase = FetchFolderListUseCaseImpl( + folderRepository: folderRepository + ) + let generateLinkUseCase: GenerateLinkUseCase = GenerateLinkUseCaseImpl( + linkRepository: linkRepository + ) + let generateFolderUseCase: GenerateFolderUseCase = GenerateFolderUseCaseImpl( + folderRepository: folderRepository + ) + + register(fetchLinkListUseCase) + register(fetchFolderListUseCase) + register(generateLinkUseCase) + register(generateFolderUseCase) } } diff --git a/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift b/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift index cb60870..dfbe31f 100644 --- a/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift +++ b/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift @@ -59,7 +59,8 @@ struct FolderRepositoryImpl: FolderRepository { func update(userID: String, folder: Folder) async throws { /// 1. Folder 문서 참조 생성 - let path = FirebasePath.folders(userID: userID).path + "/\(folder.id)" + let folderID = folder.id ?? "" + let path = FirebasePath.folders(userID: userID).path + "/\(folderID)" let folderDocRef = db.document(path) /// 2. Entity를 DTO로 변환 @@ -84,7 +85,8 @@ struct FolderRepositoryImpl: FolderRepository { func delete(userID: String, folder: Folder) async throws { /// 1. Folder 문서 참조 생성 - let path = FirebasePath.folders(userID: userID).path + "/\(folder.id)" + let folderID = folder.id ?? "" + let path = FirebasePath.folders(userID: userID).path + "/\(folderID)" let folderDocRef = db.document(path) /// 2. Folder 삭제 diff --git a/Mark-In/Sources/Domain/Entities/Folder.swift b/Mark-In/Sources/Domain/Entities/Folder.swift index 56f8579..01a4cf3 100644 --- a/Mark-In/Sources/Domain/Entities/Folder.swift +++ b/Mark-In/Sources/Domain/Entities/Folder.swift @@ -8,7 +8,7 @@ import Foundation struct Folder: Equatable, Hashable { - var id: String + var id: String? var name: String var createdBy: Date } diff --git a/Mark-In/Sources/Domain/Entities/Link.swift b/Mark-In/Sources/Domain/Entities/Link.swift index deedb09..fd736d4 100644 --- a/Mark-In/Sources/Domain/Entities/Link.swift +++ b/Mark-In/Sources/Domain/Entities/Link.swift @@ -7,7 +7,7 @@ import Foundation -struct Link { +struct Link: Hashable { var id: String var url: String var title: String? diff --git a/Mark-In/Sources/Domain/UseCases/Implements/FetchFolderListUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/FetchFolderListUseCaseImpl.swift new file mode 100644 index 0000000..0346210 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Implements/FetchFolderListUseCaseImpl.swift @@ -0,0 +1,21 @@ +// +// FetchFolderListUseCaseImpl.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +struct FetchFolderListUseCaseImpl: FetchFolderListUseCase { + + private let folderRepository: FolderRepository + + init(folderRepository: FolderRepository) { + self.folderRepository = folderRepository + } + + func execute(userID: String) async throws -> [Folder] { + try await folderRepository.fetchAll(userID: userID) + } +} diff --git a/Mark-In/Sources/Domain/UseCases/Implements/FetchLinkListUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/FetchLinkListUseCaseImpl.swift new file mode 100644 index 0000000..fa994d8 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Implements/FetchLinkListUseCaseImpl.swift @@ -0,0 +1,21 @@ +// +// FetchLinkListUseCaseImpl.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +struct FetchLinkListUseCaseImpl: FetchLinkListUseCase { + + private let linkRepository: LinkRepository + + init(linkRepository: LinkRepository) { + self.linkRepository = linkRepository + } + + func execute(userID: String) async throws -> [Link] { + try await linkRepository.fetchAll(userID: userID) + } +} diff --git a/Mark-In/Sources/Domain/UseCases/Implements/GenerateFolderUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/GenerateFolderUseCaseImpl.swift index 1299dfc..96aba3f 100644 --- a/Mark-In/Sources/Domain/UseCases/Implements/GenerateFolderUseCaseImpl.swift +++ b/Mark-In/Sources/Domain/UseCases/Implements/GenerateFolderUseCaseImpl.swift @@ -15,8 +15,7 @@ struct GenerateFolderUseCaseImpl: GenerateFolderUseCase { self.folderRepository = folderRepository } - func execute(name: String) async throws -> Folder { - let writeFolder = WriteFolder(name: name) + func execute(writeFolder: WriteFolder) async throws -> Folder { // TODO: #29번 PR 머지 후 AuthManager를 통해 현재 로그인 유저 정보 가져옴 let user = "123" diff --git a/Mark-In/Sources/Domain/UseCases/Implements/GenerateLinkUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/GenerateLinkUseCaseImpl.swift new file mode 100644 index 0000000..fa6c1e4 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Implements/GenerateLinkUseCaseImpl.swift @@ -0,0 +1,26 @@ +// +// GenerateLinkUseCaseImpl.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +struct GenerateLinkUseCaseImpl: GenerateLinkUseCase { + + private let linkRepository: LinkRepository + + init(linkRepository: LinkRepository) { + self.linkRepository = linkRepository + } + + func execute(writeLink: WriteLink) async throws -> Link { + + // TODO: #29번 PR 머지 후 AuthManager를 통해 현재 로그인 유저 정보 가져옴 + let user = "testUser" + + let newLink = try await linkRepository.create(userID: user, link: writeLink) + return newLink + } +} diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/FetchFolderListUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/FetchFolderListUseCase.swift new file mode 100644 index 0000000..5104c8d --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/FetchFolderListUseCase.swift @@ -0,0 +1,12 @@ +// +// FetchFolderListUseCase.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +protocol FetchFolderListUseCase { + func execute(userID: String) async throws -> [Folder] +} diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/FetchLinkListUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/FetchLinkListUseCase.swift new file mode 100644 index 0000000..7ab0fd0 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/FetchLinkListUseCase.swift @@ -0,0 +1,12 @@ +// +// FetchLinkListUseCase.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +protocol FetchLinkListUseCase { + func execute(userID: String) async throws -> [Link] +} diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateFolderUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateFolderUseCase.swift index 50def6e..6b00edd 100644 --- a/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateFolderUseCase.swift +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateFolderUseCase.swift @@ -8,5 +8,5 @@ import Foundation protocol GenerateFolderUseCase { - func execute(name: String) async throws -> Folder + func execute(writeFolder: WriteFolder) async throws -> Folder } diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateLinkUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateLinkUseCase.swift new file mode 100644 index 0000000..d457584 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateLinkUseCase.swift @@ -0,0 +1,12 @@ +// +// GenerateLinkUseCase.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +protocol GenerateLinkUseCase { + func execute(writeLink: WriteLink) async throws -> Link +} diff --git a/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift index fdc8f66..e2147b5 100644 --- a/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift +++ b/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift @@ -54,7 +54,7 @@ struct AddFolderView: View { .disabled(title.isEmpty || isSaving) Button { - viewModel.send(.didTapAddLinkButton(title: title)) + viewModel.send(.didTapAddFolderButton(name: title)) } label: { Text("추가") .padding(.vertical, 4) diff --git a/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift index 4908601..aa4e795 100644 --- a/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift +++ b/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift @@ -16,7 +16,7 @@ final class AddFolderViewModel: Reducer { } enum Action { - case didTapAddLinkButton(title: String) + case didTapAddFolderButton(name: String) case didCompleteSave(Folder) case updateErrorState(Bool) } @@ -26,8 +26,7 @@ final class AddFolderViewModel: Reducer { private(set) var state: State = .init() init() { - // TODO: DIContainer PR 머지 이후 DIContainer를 통해 의존성 주입 - self.generateFolderUseCase = GenerateFolderUseCaseImpl(folderRepository: FolderRepositoryImpl()) + self.generateFolderUseCase = DIContainer.shared.resolve() } func send(_ action: Action) { @@ -37,12 +36,13 @@ final class AddFolderViewModel: Reducer { func reduce(state: inout State, action: Action) -> Effect { switch action { - case .didTapAddLinkButton(let title): + case .didTapAddFolderButton(let name): state.isLoading = true return .run { do { - let result = try await self.generateFolderUseCase.execute(name: title) + let writeFolder = WriteFolder(name: name) + let result = try await self.generateFolderUseCase.execute(writeFolder: writeFolder) return .didCompleteSave(result) } catch { return .updateErrorState(true) diff --git a/Mark-In/Sources/Feature/AddLink/AddLinkView.swift b/Mark-In/Sources/Feature/AddLink/AddLinkView.swift index 121b29c..36e3027 100644 --- a/Mark-In/Sources/Feature/AddLink/AddLinkView.swift +++ b/Mark-In/Sources/Feature/AddLink/AddLinkView.swift @@ -9,24 +9,29 @@ import SwiftUI import DesignSystem -// TODO: 후에 제거 예정 -private struct TestFolder1: Hashable { - var id: String - var name: String - - static let dummy: [Self] = [ - .init(id: "1", name: "Root"), - .init(id: "2", name: "Work"), - .init(id: "3", name: "Personal"), - ] -} - struct AddLinkView: View { @Environment(\.dismiss) var dismiss + @State private var viewModel = AddLinkViewModel() @State private var title: String = "" @State private var url: String = "" - @State private var currentFolder: TestFolder1 = TestFolder1.dummy[0] + @State private var currentFolder: Folder + + private let folders: [Folder] + private let completion: (Link) -> Void + + private var isSaving: Bool { + viewModel.state.isSaving + } + + init( + folders: [Folder], + completion: @escaping (Link) -> Void + ) { + self.folders = folders + self._currentFolder = State(initialValue: folders[0]) + self.completion = completion + } var body: some View { VStack { @@ -35,7 +40,7 @@ struct AddLinkView: View { VStack(spacing: 8) { Picker("", selection: $currentFolder) { - ForEach(TestFolder1.dummy, id: \.self) { folder in + ForEach(folders, id: \.self) { folder in Label(title: { Text(folder.name) }, icon: { @@ -47,16 +52,23 @@ struct AddLinkView: View { .pickerStyle(.menu) .labelsHidden() - TextField("", text: $title, prompt: Text("제목")) + TextField("", text: $url, prompt: Text("주소")) .textFieldStyle(.roundedBorder) - TextField("", text: $url, prompt: Text("주소")) + TextField("", text: $title, prompt: Text("제목(선택)")) .textFieldStyle(.roundedBorder) } .padding(.top, 14) + .disabled(isSaving) HStack { + if isSaving { + ProgressView() + .frame(width: 12, height: 12) + .scaleEffect(0.4, anchor: .center) + } + Button { dismiss() } label: { @@ -71,10 +83,15 @@ struct AddLinkView: View { .stroke(.markBlack10, lineWidth: 0.5) } } + .disabled(isSaving) Button { - // TODO: 링크 추가 로직 - dismiss() + let link = WriteLink( + url: url, + title: title, + folderID: currentFolder.id + ) + viewModel.send(.addLinkButtonTapped(link: link)) } label: { Text("추가") .padding(.vertical, 4) @@ -83,6 +100,7 @@ struct AddLinkView: View { .background(.markPoint) .clipShape(RoundedRectangle(cornerRadius: 6)) } + .disabled(url.isEmpty || isSaving) } .frame(maxWidth: .infinity, alignment: .trailing) .padding(.top, 18) @@ -91,9 +109,33 @@ struct AddLinkView: View { } .padding(20) .frame(width: 400) + .onChange(of: viewModel.state.createdLink) { + guard let link = $1 else { return } + completion(link) + dismiss() + } + .alert( + "링크 생성에 실패했습니다.", + isPresented: .init( + get: { viewModel.state.isError }, + set: { viewModel.send(.occurError($0)) } + ) + ) { + Button(role: .cancel) { + } label: { + Text("확인") + } + } } } #Preview { - AddLinkView() + AddLinkView( + folders: [ + .init(id: "", name: "기본폴더", createdBy: .now), + .init(id: "1", name: "폴더1", createdBy: .now), + ] + ) { + print($0) + } } diff --git a/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift new file mode 100644 index 0000000..06d25c3 --- /dev/null +++ b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift @@ -0,0 +1,71 @@ +// +// AddLinkViewModel.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +@Observable @MainActor +final class AddLinkViewModel: Reducer { + struct State { + var createdLink: Link? + var isSaving: Bool = false + var isError: Bool = false + } + + enum Action { + case addLinkButtonTapped(link: WriteLink) + case completeSave(Link) + case occurError(Bool) + } + + private let generateLinkUseCase: GenerateLinkUseCase + + private(set) var state: State = .init() + + init() { + self.generateLinkUseCase = DIContainer.shared.resolve() + } + + func send(_ action: Action) { + let effect = reduce(state: &state, action: action) + handleEffect(effect) + } + + func reduce(state: inout State, action: Action) -> Effect { + switch action { + case .addLinkButtonTapped(let writeLink): + state.isSaving = true + return .run { + do { + let newLink = try await self.generateLinkUseCase.execute(writeLink: writeLink) + return .completeSave(newLink) + } catch { + return .occurError(true) + } + } + case .completeSave(let link): + state.createdLink = link + state.isSaving = false + return .none + case .occurError(let bool): + state.isError = bool + state.isSaving = false + return .none + } + } + + private func handleEffect(_ effect: Effect) { + switch effect { + case .none: + break + case .run(let action): + Task.detached { [weak self] in + let newAction = await action() + await self?.send(newAction) + } + } + } +} diff --git a/Mark-In/Sources/Feature/Main/LinkListView.swift b/Mark-In/Sources/Feature/Main/LinkListView.swift index b26b1b8..15a316d 100644 --- a/Mark-In/Sources/Feature/Main/LinkListView.swift +++ b/Mark-In/Sources/Feature/Main/LinkListView.swift @@ -18,7 +18,23 @@ private enum ViewConstants { struct LinkListView: View { let viewModel: MainViewModel - + + private var links: [Link] { + let totalLinks = viewModel.state.links + let tab = viewModel.state.selectedTab ?? .total + + switch tab { + case .total: + return totalLinks + case .pin: + return totalLinks.filter { $0.isPinned } + case .nonRead: + return totalLinks.filter { $0.lastAccessedAt == nil } + case .folder(let folder): + return totalLinks.filter { $0.folderID == folder.id } + } + } + var body: some View { GeometryReader { geometry in let columns = getColumns(from: geometry.size.width) @@ -29,12 +45,8 @@ struct LinkListView: View { alignment: .leading, spacing: ViewConstants.spacing ) { - ForEach((0...19), id: \.self) { _ in - LinkCell() - .frame( - width: ViewConstants.cellWidth, - height: ViewConstants.cellHeight - ) + ForEach(links, id: \.self) { link in + LinkCell(link: link) } } .padding(20) @@ -44,7 +56,6 @@ struct LinkListView: View { private func getColumns(from width: Double) -> [GridItem] { let cellWidth = ViewConstants.cellWidth - let cellHeight = ViewConstants.cellHeight let spacing = ViewConstants.spacing let numberOfColumns = max( Int((width + spacing) / (cellWidth + spacing)), 1 @@ -64,36 +75,105 @@ struct LinkListView: View { private struct LinkCell: View { + let link: Link + var body: some View { ZStack(alignment: .bottom) { - Image(.sampleImage) - .resizable() - .aspectRatio(contentMode: .fit) + // TODO: Link 네이밍 충돌로, 이후 리팩토링 작업 후 적용 예정 +// Link(destination: URL(string: link.url)) { +// AsyncImage( +// url: URL(string: link.thumbnailUrl ?? "") +// ) { image in +// image +// .resizable() +// .aspectRatio(contentMode: .fill) +// } placeholder: { +// Rectangle() +// .fill(.markWhite70) +// } +// .frame(width: ViewConstants.cellWidth, height: ViewConstants.cellHeight) +// } + + AsyncImage( + url: URL(string: link.thumbnailUrl ?? "") + ) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + } + .frame( + width: ViewConstants.cellWidth, + height: ViewConstants.cellHeight + ) - VStack(alignment: .leading, spacing: 4) { - Text("맛집 링크 - 네이버 지도") - .font(.pretendard(size: 12, weight: .semiBold)) - Text("7일 전") - .font(.pretendard(size: 10, weight: .regular)) + VStack(alignment: .leading, spacing: 0) { + headerTitle + + bodyTitle + .padding(.top, 1) + + footerTitle + .padding(.top, 7) } .frame(maxWidth: .infinity, alignment: .leading) - .padding([.top, .horizontal], 10) - .padding(.bottom, 12) - .background(.green) + .padding(.horizontal, 10) + .padding([.top, .bottom], 12) + .background(.markWhite) } - .background(.yellow) .clipShape( RoundedRectangle(cornerRadius: 6) ) .overlay(content: { RoundedRectangle(cornerRadius: 6) - .stroke(lineWidth: 0.5) + .stroke(.markBlack20, lineWidth: 0.5) }) } + + private var headerTitle: some View { + HStack(alignment: .top, spacing: 3) { + if link.isPinned { + Image(systemName: "star.fill") + .resizable() + .foregroundStyle(.markPoint) + .frame(width: 12, height: 12) + } + + Text(link.title ?? "제목 없음") + .font(.pretendard(size: 12, weight: .semiBold)) + .foregroundStyle(.markBlack) + .lineLimit(1) + } + } + + private var bodyTitle: some View { + Text(link.url) + .font(.pretendard(size: 10, weight: .regular)) + .foregroundStyle(.markBlack40) + .lineLimit(1) + } + + private var footerTitle: some View { + HStack(alignment: .center, spacing: 4) { + if link.lastAccessedAt == nil { + Circle() + .fill(.markRed) + .frame(width: 5, height: 5) + } + + Text(link.createdBy.description) + .font(.pretendard(size: 10, weight: .regular)) + .foregroundStyle(.markBlack40) + .lineLimit(1) + } + } } #Preview { - LinkListView(viewModel: MainViewModel()) - .frame(width: 600, height: 600) + LinkCell(link: .init(id: "", url: "www.naver.com", isPinned: true, createdBy: .now)) + .frame(width: 210, height: 160) +// LinkListView(viewModel: MainViewModel()) +// .frame(width: 600, height: 600) } diff --git a/Mark-In/Sources/Feature/Main/MainView.swift b/Mark-In/Sources/Feature/Main/MainView.swift index aec1496..001f2a1 100644 --- a/Mark-In/Sources/Feature/Main/MainView.swift +++ b/Mark-In/Sources/Feature/Main/MainView.swift @@ -81,7 +81,7 @@ struct MainView: View { Spacer() Button { - // TODO: 구현 예정 + viewModel.send(.presentSheet(.addLink)) } label: { Image(systemName: "plus") } @@ -106,7 +106,15 @@ struct MainView: View { private func buildSheet(_ type: MainViewModel.SheetType) -> some View { switch type { case .addLink: - AddLinkView() + let folderTabs = viewModel.state.folderTabs + let folders = folderTabs + .compactMap { + if case let .folder(folder) = $0 { folder } + else { nil } + } + AddLinkView(folders: folders) { + viewModel.send(.didCreateLink($0)) + } case .addFolder: AddFolderView() { viewModel.send(.didCreateFolder($0)) diff --git a/Mark-In/Sources/Feature/Main/MainViewModel.swift b/Mark-In/Sources/Feature/Main/MainViewModel.swift index e0d9a5d..8cf7cf4 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -7,11 +7,13 @@ import Foundation -@Observable +@MainActor @Observable final class MainViewModel: Reducer { struct State { var isLoading: Bool = true + var links: [Link] = [] + var defaultTabs: [SidebarTab] = [.total, .pin, .nonRead] var folderTabs: [SidebarTab] = [] var selectedTab: SidebarTab? = .total @@ -21,16 +23,29 @@ final class MainViewModel: Reducer { enum Action { case onAppear - case refresh + case fetchSucceeded([Link], [Folder]) case changeTab(SidebarTab?) case presentSheet(SheetType?) + case didCreateLink(Link) case didCreateFolder(Folder) + + case occuredError + + case empty } + private let fetchLinkListUseCase: FetchLinkListUseCase + private let fetchFolderListUseCase: FetchFolderListUseCase + private(set) var state: State = .init() + init() { + self.fetchLinkListUseCase = DIContainer.shared.resolve() + self.fetchFolderListUseCase = DIContainer.shared.resolve() + } + func send(_ action: Action) { let effect = reduce(state: &state, action: action) handleEffect(effect) @@ -40,15 +55,28 @@ final class MainViewModel: Reducer { switch action { case .onAppear: return .run { - try? await Task.sleep(nanoseconds: 3_000_000_000) - return .refresh + do { + // TODO: 실제 로그인 유저 ID 전달 + async let links = await self.fetchLinkListUseCase.execute(userID: "testUser") + async let folders = await self.fetchFolderListUseCase.execute(userID: "testUser") + + return try await .fetchSucceeded(links, folders) + } catch { + return .occuredError + } } - case .refresh: - // TODO: 실제 데이터 가져오는 작업 구현 필요 - (1...3).forEach { - state.folderTabs.append(.folder(.init(id: "\($0)", name: "\($0)", createdBy: .now))) + case let .fetchSucceeded(links, folders): + + state.links = links + + state.folderTabs.append(.folder(.init(id: nil, name: "기본폴더", createdBy: .now))) + folders.forEach { + state.folderTabs.append( + .folder(.init(id: $0.id, name: $0.name, createdBy: $0.createdBy)) + ) } + state.isLoading = false return .none @@ -60,9 +88,20 @@ final class MainViewModel: Reducer { state.isPresentedSheet = sheetType return .none + case .didCreateLink(let link): + state.links.insert(link, at: 0) + return .none + case .didCreateFolder(let folder): state.folderTabs.append(.folder(folder)) return .none + + // TODO: 에러 처리 로직 추가 + case .occuredError: + return .none + + case .empty: + return .none } }