From 6aff78f7c6d6ddac6adb7e8cb801fd537b5a5f9f Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 13:49:25 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[#34]=20Link,=20Folder=20FetchUseCase=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FetchFolderListUseCaseImpl.swift | 21 +++++++++++++++++++ .../Implements/FetchLinkListUseCaseImpl.swift | 21 +++++++++++++++++++ .../Interfaces/FetchFolderListUseCase.swift | 12 +++++++++++ .../Interfaces/FetchLinkListUseCase.swift | 12 +++++++++++ 4 files changed, 66 insertions(+) create mode 100644 Mark-In/Sources/Domain/UseCases/Implements/FetchFolderListUseCaseImpl.swift create mode 100644 Mark-In/Sources/Domain/UseCases/Implements/FetchLinkListUseCaseImpl.swift create mode 100644 Mark-In/Sources/Domain/UseCases/Interfaces/FetchFolderListUseCase.swift create mode 100644 Mark-In/Sources/Domain/UseCases/Interfaces/FetchLinkListUseCase.swift 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/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] +} From 84c84cabbcf081e3e70dacbbcd78dfe0303924e4 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 14:35:00 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[#34]=20ViewModel=EC=97=90=20UseCase=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Feature/Main/MainViewModel.swift | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/Mark-In/Sources/Feature/Main/MainViewModel.swift b/Mark-In/Sources/Feature/Main/MainViewModel.swift index 0d8d135..b0be0cc 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -7,11 +7,17 @@ import Foundation +// TODO: DIContainer 도입 시 제거 +import LinkMetadataKit +import LinkMetadataKitInterface + @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 +27,29 @@ final class MainViewModel: Reducer { enum Action { case onAppear - case refresh + case fetchSucceeded([Link], [Folder]) case changeTab(SidebarTab?) case presentSheet(SheetType?) case didCreateFolder(Folder) + + case occuredError + + case empty } + private let fetchLinkListUseCase: FetchLinkListUseCase + private let fetchFolderListUseCase: FetchFolderListUseCase + private(set) var state: State = .init() + init() { + // TODO: DIContainer로 주입 + self.fetchLinkListUseCase = FetchLinkListUseCaseImpl(linkRepository: LinkRepositoryImpl(linkMetadataProvider: LinkMetadataProviderImpl())) + self.fetchFolderListUseCase = FetchFolderListUseCaseImpl(folderRepository: FolderRepositoryImpl()) + } + func send(_ action: Action) { let effect = reduce(state: &state, action: action) handleEffect(effect) @@ -40,15 +59,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: "", name: "기본폴더", createdBy: .now))) + folders.forEach { + state.folderTabs.append( + .folder(.init(id: $0.id, name: $0.name, createdBy: $0.createdBy)) + ) } + state.isLoading = false return .none @@ -63,6 +95,13 @@ final class MainViewModel: Reducer { case .didCreateFolder(let folder): state.folderTabs.append(.folder(folder)) return .none + + // TODO: 에러 처리 로직 추가 + case .occuredError: + return .none + + case .empty: + return .none } } From 6661ad364c50a1dd6b791ed9dad7b4402f0b30ff Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 14:36:26 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[#34]=20View=EC=97=90=20ViewModel=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/Domain/Entities/Link.swift | 2 +- .../Sources/Feature/Main/LinkListView.swift | 101 ++++++++++++++---- .../Sources/Feature/Main/MainViewModel.swift | 16 +++ 3 files changed, 96 insertions(+), 23 deletions(-) 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/Feature/Main/LinkListView.swift b/Mark-In/Sources/Feature/Main/LinkListView.swift index b26b1b8..a7d6c8b 100644 --- a/Mark-In/Sources/Feature/Main/LinkListView.swift +++ b/Mark-In/Sources/Feature/Main/LinkListView.swift @@ -18,6 +18,14 @@ private enum ViewConstants { struct LinkListView: View { let viewModel: MainViewModel + + private var links: [Link] { + viewModel.state.links + } + + private var currentTab: SidebarTab { + viewModel.state.selectedTab ?? .total + } var body: some View { GeometryReader { geometry in @@ -29,12 +37,11 @@ struct LinkListView: View { alignment: .leading, spacing: ViewConstants.spacing ) { - ForEach((0...19), id: \.self) { _ in - LinkCell() - .frame( - width: ViewConstants.cellWidth, - height: ViewConstants.cellHeight - ) + ForEach( + links.filter(tab: currentTab), + id: \.self + ) { link in + LinkCell(link: link) } } .padding(20) @@ -44,7 +51,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 +70,87 @@ struct LinkListView: View { private struct LinkCell: View { + let link: Link + var body: some View { ZStack(alignment: .bottom) { - Image(.sampleImage) - .resizable() - .aspectRatio(contentMode: .fit) + 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/MainViewModel.swift b/Mark-In/Sources/Feature/Main/MainViewModel.swift index b0be0cc..f1b19da 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -126,3 +126,19 @@ extension MainViewModel { var id: String { String(describing: self) } } } + +extension Array where Element == Link { + func filter(tab: SidebarTab) -> Self { + switch tab { + case .total: + return self + case .pin: + return self.filter { $0.isPinned == true } + case .nonRead: + return self.filter { $0.lastAccessedAt == nil } + case .folder(let folder): + let folderID = folder.id == "" ? nil : folder.id + return self.filter { $0.folderID == folderID } + } + } +} From ea94ae8cf23bfcbd1d17c1a141262f454366cdd9 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 15:17:21 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[#34]=20AddFolderViewModel=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/Feature/AddFolder/AddFolderView.swift | 2 +- Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift index d0968fc..83fab1b 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(.addLinkButtonTapped(title: title)) + viewModel.send(.addFolderButtonTapped(title: 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 fdacd9b..5f2be03 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 addLinkButtonTapped(title: String) + case addFolderButtonTapped(title: String) case completeSave(Folder) case occurError(Bool) } @@ -37,7 +37,7 @@ final class AddFolderViewModel: Reducer { func reduce(state: inout State, action: Action) -> Effect { switch action { - case .addLinkButtonTapped(let title): + case .addFolderButtonTapped(let title): state.isSaving = true return .run { From 493908e4c65c72f80d97fe3034280b8cf01e1483 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 15:17:47 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[#34]=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EB=9D=84=EC=9A=B0=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20Link=20View=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Feature/Main/LinkListView.swift | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Mark-In/Sources/Feature/Main/LinkListView.swift b/Mark-In/Sources/Feature/Main/LinkListView.swift index a7d6c8b..a61c4df 100644 --- a/Mark-In/Sources/Feature/Main/LinkListView.swift +++ b/Mark-In/Sources/Feature/Main/LinkListView.swift @@ -74,6 +74,21 @@ private struct LinkCell: View { var body: some View { ZStack(alignment: .bottom) { + // 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 @@ -83,7 +98,10 @@ private struct LinkCell: View { } placeholder: { Rectangle() } - .frame(width: ViewConstants.cellWidth, height: ViewConstants.cellHeight) + .frame( + width: ViewConstants.cellWidth, + height: ViewConstants.cellHeight + ) VStack(alignment: .leading, spacing: 0) { headerTitle From 8161acf4abc1e7cfe6f8e3c62f369d9e3bea244a Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 15:18:02 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[#34]=20GenerateLinkUseCase=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implements/GenerateLinkUseCaseImpl.swift | 26 +++++++++++++++++++ .../Interfaces/GenerateLinkUseCase.swift | 12 +++++++++ 2 files changed, 38 insertions(+) create mode 100644 Mark-In/Sources/Domain/UseCases/Implements/GenerateLinkUseCaseImpl.swift create mode 100644 Mark-In/Sources/Domain/UseCases/Interfaces/GenerateLinkUseCase.swift 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/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 +} From 0fb8cbc8cce2784da3d5ccea5bcedd80248c33f7 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 15:24:12 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[#34]=20AddLinkViewModel=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/AddLink/AddLinkViewModel.swift | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift diff --git a/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift new file mode 100644 index 0000000..72b8b8f --- /dev/null +++ b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift @@ -0,0 +1,76 @@ +// +// AddLinkViewModel.swift +// Mark-In +// +// Created by 이정동 on 5/11/25. +// + +import Foundation + +// TODO: DIContainer 적용 시 제거 +import LinkMetadataKit +import LinkMetadataKitInterface + +@Observable @MainActor +final class AddLinkViewModel: Reducer { + struct State { + var createdLink: Link? + var isSaving: Bool = false + var isError: Bool = false + } + + enum Action { + case addLinkButtonTapped(WriteLink) + case completeSave(Link) + case occurError(Bool) + } + + private let generateLinkUseCase: GenerateLinkUseCase + + private(set) var state: State = .init() + + init() { + // TODO: DIContainer 의존성 주입 + self.generateLinkUseCase = GenerateLinkUseCaseImpl(linkRepository: LinkRepositoryImpl(linkMetadataProvider: LinkMetadataProviderImpl())) + } + + 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) + } + } + } +} From 519cdc8f15f876eca6bc21e9dd4b252d6b360f48 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 15:51:18 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[#34]=20Folder=20ID=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Data/Repositories/FolderRepositoryImpl.swift | 6 ++++-- Mark-In/Sources/Domain/Entities/Folder.swift | 2 +- Mark-In/Sources/Feature/Main/MainViewModel.swift | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) 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/Feature/Main/MainViewModel.swift b/Mark-In/Sources/Feature/Main/MainViewModel.swift index f1b19da..249e070 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -74,7 +74,7 @@ final class MainViewModel: Reducer { state.links = links - state.folderTabs.append(.folder(.init(id: "", name: "기본폴더", createdBy: .now))) + 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)) @@ -137,8 +137,7 @@ extension Array where Element == Link { case .nonRead: return self.filter { $0.lastAccessedAt == nil } case .folder(let folder): - let folderID = folder.id == "" ? nil : folder.id - return self.filter { $0.folderID == folderID } + return self.filter { $0.folderID == folder.id } } } } From c8fb00e6fda75a4e2e443e989b4a5a41f35b0918 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 16:03:16 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[#34]=20View=EC=97=90=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Feature/AddLink/AddLinkView.swift | 80 ++++++++++++++----- Mark-In/Sources/Feature/Main/MainView.swift | 12 ++- .../Sources/Feature/Main/MainViewModel.swift | 5 ++ 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/Mark-In/Sources/Feature/AddLink/AddLinkView.swift b/Mark-In/Sources/Feature/AddLink/AddLinkView.swift index 121b29c..ae5309c 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)) } 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/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 249e070..e4658fe 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -32,6 +32,7 @@ final class MainViewModel: Reducer { case presentSheet(SheetType?) + case didCreateLink(Link) case didCreateFolder(Folder) case occuredError @@ -92,6 +93,10 @@ 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 From ac480de22ce2b7e0c104e3d983f39a599cb64782 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 16:09:34 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[#34]=20AddLinkViewModel=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/Feature/AddLink/AddLinkView.swift | 2 +- Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mark-In/Sources/Feature/AddLink/AddLinkView.swift b/Mark-In/Sources/Feature/AddLink/AddLinkView.swift index ae5309c..36e3027 100644 --- a/Mark-In/Sources/Feature/AddLink/AddLinkView.swift +++ b/Mark-In/Sources/Feature/AddLink/AddLinkView.swift @@ -91,7 +91,7 @@ struct AddLinkView: View { title: title, folderID: currentFolder.id ) - viewModel.send(.addLinkButtonTapped(link)) + viewModel.send(.addLinkButtonTapped(link: link)) } label: { Text("추가") .padding(.vertical, 4) diff --git a/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift index 72b8b8f..2e74642 100644 --- a/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift +++ b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift @@ -20,7 +20,7 @@ final class AddLinkViewModel: Reducer { } enum Action { - case addLinkButtonTapped(WriteLink) + case addLinkButtonTapped(link: WriteLink) case completeSave(Link) case occurError(Bool) } From 1a616fb2f9252dc7b968f1dcbd85bfd1811bc9bd Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 16:09:51 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[#34]=20AddFolder=20ViewModel=20=EB=B0=8F?= =?UTF-8?q?=20UseCase=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCases/Implements/GenerateFolderUseCaseImpl.swift | 3 +-- .../Domain/UseCases/Interfaces/GenerateFolderUseCase.swift | 2 +- Mark-In/Sources/Feature/AddFolder/AddFolderView.swift | 3 ++- Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) 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/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/Feature/AddFolder/AddFolderView.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift index 83fab1b..9aa440b 100644 --- a/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift +++ b/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift @@ -54,7 +54,8 @@ struct AddFolderView: View { .disabled(title.isEmpty || isSaving) Button { - viewModel.send(.addFolderButtonTapped(title: title)) + let folder = WriteFolder(name: title) + viewModel.send(.addFolderButtonTapped(folder: folder)) } label: { Text("추가") .padding(.vertical, 4) diff --git a/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift index 5f2be03..c9f176b 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 addFolderButtonTapped(title: String) + case addFolderButtonTapped(folder: WriteFolder) case completeSave(Folder) case occurError(Bool) } @@ -37,12 +37,12 @@ final class AddFolderViewModel: Reducer { func reduce(state: inout State, action: Action) -> Effect { switch action { - case .addFolderButtonTapped(let title): + case .addFolderButtonTapped(let writeFolder): state.isSaving = true return .run { do { - let result = try await self.generateFolderUseCase.execute(name: title) + let result = try await self.generateFolderUseCase.execute(writeFolder: writeFolder) return .completeSave(result) } catch { return .occurError(true) From 5e378823726c62718a81fafac5392707b552837f Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Sun, 11 May 2025 16:30:20 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[#34]=20Link=20Array=20Extension=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Feature/Main/LinkListView.swift | 23 +++++++++++-------- .../Sources/Feature/Main/MainViewModel.swift | 15 ------------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/Mark-In/Sources/Feature/Main/LinkListView.swift b/Mark-In/Sources/Feature/Main/LinkListView.swift index a61c4df..15a316d 100644 --- a/Mark-In/Sources/Feature/Main/LinkListView.swift +++ b/Mark-In/Sources/Feature/Main/LinkListView.swift @@ -20,13 +20,21 @@ struct LinkListView: View { let viewModel: MainViewModel private var links: [Link] { - viewModel.state.links + 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 } + } } - private var currentTab: SidebarTab { - viewModel.state.selectedTab ?? .total - } - var body: some View { GeometryReader { geometry in let columns = getColumns(from: geometry.size.width) @@ -37,10 +45,7 @@ struct LinkListView: View { alignment: .leading, spacing: ViewConstants.spacing ) { - ForEach( - links.filter(tab: currentTab), - id: \.self - ) { link in + ForEach(links, id: \.self) { link in LinkCell(link: link) } } diff --git a/Mark-In/Sources/Feature/Main/MainViewModel.swift b/Mark-In/Sources/Feature/Main/MainViewModel.swift index e4658fe..8a42abb 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -131,18 +131,3 @@ extension MainViewModel { var id: String { String(describing: self) } } } - -extension Array where Element == Link { - func filter(tab: SidebarTab) -> Self { - switch tab { - case .total: - return self - case .pin: - return self.filter { $0.isPinned == true } - case .nonRead: - return self.filter { $0.lastAccessedAt == nil } - case .folder(let folder): - return self.filter { $0.folderID == folder.id } - } - } -} From cde62f8b98d14e7736ac860aaa3b531da48f477b Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 14:02:45 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[#34]=20DIContainer=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/App/DIContainer.swift | 19 +++++++++++++++++++ .../AddFolder/AddFolderViewModel.swift | 3 +-- .../Feature/AddLink/AddLinkViewModel.swift | 7 +------ .../Sources/Feature/Main/MainViewModel.swift | 9 ++------- 4 files changed, 23 insertions(+), 15 deletions(-) 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/Feature/AddFolder/AddFolderViewModel.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift index 213f14d..aa4e795 100644 --- a/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift +++ b/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift @@ -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) { diff --git a/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift index 2e74642..06d25c3 100644 --- a/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift +++ b/Mark-In/Sources/Feature/AddLink/AddLinkViewModel.swift @@ -7,10 +7,6 @@ import Foundation -// TODO: DIContainer 적용 시 제거 -import LinkMetadataKit -import LinkMetadataKitInterface - @Observable @MainActor final class AddLinkViewModel: Reducer { struct State { @@ -30,8 +26,7 @@ final class AddLinkViewModel: Reducer { private(set) var state: State = .init() init() { - // TODO: DIContainer 의존성 주입 - self.generateLinkUseCase = GenerateLinkUseCaseImpl(linkRepository: LinkRepositoryImpl(linkMetadataProvider: LinkMetadataProviderImpl())) + self.generateLinkUseCase = DIContainer.shared.resolve() } func send(_ action: Action) { diff --git a/Mark-In/Sources/Feature/Main/MainViewModel.swift b/Mark-In/Sources/Feature/Main/MainViewModel.swift index 8a42abb..8cf7cf4 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -7,10 +7,6 @@ import Foundation -// TODO: DIContainer 도입 시 제거 -import LinkMetadataKit -import LinkMetadataKitInterface - @MainActor @Observable final class MainViewModel: Reducer { struct State { @@ -46,9 +42,8 @@ final class MainViewModel: Reducer { private(set) var state: State = .init() init() { - // TODO: DIContainer로 주입 - self.fetchLinkListUseCase = FetchLinkListUseCaseImpl(linkRepository: LinkRepositoryImpl(linkMetadataProvider: LinkMetadataProviderImpl())) - self.fetchFolderListUseCase = FetchFolderListUseCaseImpl(folderRepository: FolderRepositoryImpl()) + self.fetchLinkListUseCase = DIContainer.shared.resolve() + self.fetchFolderListUseCase = DIContainer.shared.resolve() } func send(_ action: Action) {