diff --git a/Mark-In/Sources/Domain/Entities/Folder.swift b/Mark-In/Sources/Domain/Entities/Folder.swift index 9028315..56f8579 100644 --- a/Mark-In/Sources/Domain/Entities/Folder.swift +++ b/Mark-In/Sources/Domain/Entities/Folder.swift @@ -7,7 +7,7 @@ import Foundation -struct Folder { +struct Folder: Equatable, Hashable { var id: String var name: String var createdBy: Date diff --git a/Mark-In/Sources/Domain/UseCases/Implements/GenerateFolderUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/GenerateFolderUseCaseImpl.swift new file mode 100644 index 0000000..1299dfc --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Implements/GenerateFolderUseCaseImpl.swift @@ -0,0 +1,27 @@ +// +// GenerateFolderUseCaseImpl.swift +// Mark-In +// +// Created by 이정동 on 5/8/25. +// + +import Foundation + +struct GenerateFolderUseCaseImpl: GenerateFolderUseCase { + + private let folderRepository: FolderRepository + + init(folderRepository: FolderRepository) { + self.folderRepository = folderRepository + } + + func execute(name: String) async throws -> Folder { + let writeFolder = WriteFolder(name: name) + + // TODO: #29번 PR 머지 후 AuthManager를 통해 현재 로그인 유저 정보 가져옴 + let user = "123" + + let newFolder = try await folderRepository.create(userID: user, folder: writeFolder) + return newFolder + } +} diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateFolderUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateFolderUseCase.swift new file mode 100644 index 0000000..50def6e --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/GenerateFolderUseCase.swift @@ -0,0 +1,12 @@ +// +// GenerateFolderUseCase.swift +// Mark-In +// +// Created by 이정동 on 5/8/25. +// + +import Foundation + +protocol GenerateFolderUseCase { + func execute(name: String) async throws -> Folder +} diff --git a/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift new file mode 100644 index 0000000..fdc8f66 --- /dev/null +++ b/Mark-In/Sources/Feature/AddFolder/AddFolderView.swift @@ -0,0 +1,99 @@ +// +// AddFolderView.swift +// Mark-In +// +// Created by 이정동 on 5/8/25. +// + +import SwiftUI + +import DesignSystem + +struct AddFolderView: View { + @Environment(\.dismiss) private var dismiss + @State private var viewModel = AddFolderViewModel() + @State private var title: String = "" + + private var isSaving: Bool { + viewModel.state.isLoading + } + + let completion: (Folder) -> () + + var body: some View { + VStack(spacing: 0) { + Text("폴더를 추가:") + .frame(maxWidth: .infinity, alignment: .leading) + + 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: { + Text("취소") + .padding(.vertical, 4) + .padding(.horizontal, 14) + .foregroundStyle(.markBlack) + .background(.markWhite) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay { + RoundedRectangle(cornerRadius: 6) + .stroke(.markBlack10, lineWidth: 0.5) + } + } + .disabled(title.isEmpty || isSaving) + + Button { + viewModel.send(.didTapAddLinkButton(title: title)) + } label: { + Text("추가") + .padding(.vertical, 4) + .padding(.horizontal, 14) + .foregroundStyle(.markWhite) + .background(.markPoint) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + .disabled(title.isEmpty || isSaving) + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.top, 18) + .font(.pretendard(size: 14, weight: .medium)) + .buttonStyle(.plain) + } + .padding(20) + .frame(width: 400) + .onChange(of: viewModel.state.createdFolder) { + guard let folder = $1 else { return } + completion(folder) + dismiss() + } + .alert( + "폴더 생성에 실패했습니다.", + isPresented: .init( + get: { viewModel.state.isError }, + set: { viewModel.send(.updateErrorState($0)) } + ) + ) { + Button(role: .cancel) { + } label: { + Text("확인") + } + } + } +} + +#Preview { + AddFolderView() { + print($0) + } +} diff --git a/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift b/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift new file mode 100644 index 0000000..4908601 --- /dev/null +++ b/Mark-In/Sources/Feature/AddFolder/AddFolderViewModel.swift @@ -0,0 +1,76 @@ +// +// AddFolderViewModel.swift +// Mark-In +// +// Created by 이정동 on 5/8/25. +// + +import Foundation + +@Observable @MainActor +final class AddFolderViewModel: Reducer { + struct State { + var createdFolder: Folder? + var isLoading: Bool = false + var isError: Bool = false + } + + enum Action { + case didTapAddLinkButton(title: String) + case didCompleteSave(Folder) + case updateErrorState(Bool) + } + + private let generateFolderUseCase: GenerateFolderUseCase + + private(set) var state: State = .init() + + init() { + // TODO: DIContainer PR 머지 이후 DIContainer를 통해 의존성 주입 + self.generateFolderUseCase = GenerateFolderUseCaseImpl(folderRepository: FolderRepositoryImpl()) + } + + func send(_ action: Action) { + let effect = reduce(state: &state, action: action) + handleEffect(effect) + } + + func reduce(state: inout State, action: Action) -> Effect { + switch action { + case .didTapAddLinkButton(let title): + state.isLoading = true + + return .run { + do { + let result = try await self.generateFolderUseCase.execute(name: title) + return .didCompleteSave(result) + } catch { + return .updateErrorState(true) + } + } + + case .didCompleteSave(let folder): + state.isLoading = false + state.createdFolder = folder + return .none + + case .updateErrorState(let bool): + state.isLoading = false + state.isError = bool + 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/AddLink/AddLinkView.swift b/Mark-In/Sources/Feature/AddLink/AddLinkView.swift index 74634fd..121b29c 100644 --- a/Mark-In/Sources/Feature/AddLink/AddLinkView.swift +++ b/Mark-In/Sources/Feature/AddLink/AddLinkView.swift @@ -63,12 +63,12 @@ struct AddLinkView: View { Text("취소") .padding(.vertical, 4) .padding(.horizontal, 14) - .foregroundStyle(.black) - .background(.white) + .foregroundStyle(.markBlack) + .background(.markWhite) .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay { RoundedRectangle(cornerRadius: 6) - .stroke(.gray, lineWidth: 0.5) + .stroke(.markBlack10, lineWidth: 0.5) } } @@ -79,8 +79,8 @@ struct AddLinkView: View { Text("추가") .padding(.vertical, 4) .padding(.horizontal, 14) - .foregroundStyle(.white) - .background(.blue) + .foregroundStyle(.markWhite) + .background(.markPoint) .clipShape(RoundedRectangle(cornerRadius: 6)) } } diff --git a/Mark-In/Sources/Feature/Common/SidebarTab.swift b/Mark-In/Sources/Feature/Common/SidebarTab.swift index 3aa2c60..38efb35 100644 --- a/Mark-In/Sources/Feature/Common/SidebarTab.swift +++ b/Mark-In/Sources/Feature/Common/SidebarTab.swift @@ -11,7 +11,7 @@ enum SidebarTab: Hashable { case total case pin case nonRead - case folder(TestFolder) + case folder(Folder) var title: String { switch self { @@ -30,9 +30,4 @@ enum SidebarTab: Hashable { case .folder(_): "folder" } } - - var isFolder: Bool { - if case .folder(_) = self { true } - else { false } - } } diff --git a/Mark-In/Sources/Feature/Main/MainView.swift b/Mark-In/Sources/Feature/Main/MainView.swift index a6afaab..aec1496 100644 --- a/Mark-In/Sources/Feature/Main/MainView.swift +++ b/Mark-In/Sources/Feature/Main/MainView.swift @@ -12,7 +12,6 @@ import DesignSystem struct MainView: View { @State private var viewModel = MainViewModel() @State private var searchText: String = "" - @State private var isAddMode: Bool = false var body: some View { ZStack { @@ -43,8 +42,12 @@ struct MainView: View { .onAppear { viewModel.send(.onAppear) } - .sheet(isPresented: $isAddMode) { - AddLinkView() + .sheet(item: .init( + get: { viewModel.state.isPresentedSheet }, + set: { viewModel.send(.presentSheet($0)) + } + )) { type in + buildSheet(type) } } @@ -79,7 +82,6 @@ struct MainView: View { Spacer() Button { // TODO: 구현 예정 - isAddMode = true } label: { Image(systemName: "plus") } @@ -99,6 +101,18 @@ struct MainView: View { } } } + + @ViewBuilder + private func buildSheet(_ type: MainViewModel.SheetType) -> some View { + switch type { + case .addLink: + AddLinkView() + case .addFolder: + AddFolderView() { + viewModel.send(.didCreateFolder($0)) + } + } + } } #Preview { diff --git a/Mark-In/Sources/Feature/Main/MainViewModel.swift b/Mark-In/Sources/Feature/Main/MainViewModel.swift index 6aaed15..0d8d135 100644 --- a/Mark-In/Sources/Feature/Main/MainViewModel.swift +++ b/Mark-In/Sources/Feature/Main/MainViewModel.swift @@ -12,14 +12,21 @@ final class MainViewModel: Reducer { struct State { var isLoading: Bool = true - var tabs: [SidebarTab] = [.total, .pin, .nonRead] + var defaultTabs: [SidebarTab] = [.total, .pin, .nonRead] + var folderTabs: [SidebarTab] = [] var selectedTab: SidebarTab? = .total + + var isPresentedSheet: SheetType? } enum Action { case onAppear case refresh case changeTab(SidebarTab?) + + case presentSheet(SheetType?) + + case didCreateFolder(Folder) } private(set) var state: State = .init() @@ -38,13 +45,24 @@ final class MainViewModel: Reducer { } case .refresh: - (1...3).forEach { state.tabs.append(.folder(.init(id: "\($0)", name: "\($0)"))) } + // TODO: 실제 데이터 가져오는 작업 구현 필요 + (1...3).forEach { + state.folderTabs.append(.folder(.init(id: "\($0)", name: "\($0)", createdBy: .now))) + } state.isLoading = false return .none case .changeTab(let tab): state.selectedTab = tab return .none + + case .presentSheet(let sheetType): + state.isPresentedSheet = sheetType + return .none + + case .didCreateFolder(let folder): + state.folderTabs.append(.folder(folder)) + return .none } } @@ -61,8 +79,11 @@ final class MainViewModel: Reducer { } } -// TODO: 이후 제거 예정 -struct TestFolder: Hashable { - var id: String - var name: String +extension MainViewModel { + enum SheetType: Identifiable { + case addLink + case addFolder + + var id: String { String(describing: self) } + } } diff --git a/Mark-In/Sources/Feature/Main/SideBar.swift b/Mark-In/Sources/Feature/Main/SideBar.swift index 8d8ba3e..ce2b051 100644 --- a/Mark-In/Sources/Feature/Main/SideBar.swift +++ b/Mark-In/Sources/Feature/Main/SideBar.swift @@ -21,7 +21,7 @@ struct SideBar: View { ) { Section("기본") { ForEach( - viewModel.state.tabs.filter { !$0.isFolder }, + viewModel.state.defaultTabs, id: \.self ) { tab in NavigationLink(value: tab) { @@ -32,7 +32,7 @@ struct SideBar: View { Section("저장된 폴더") { ForEach( - viewModel.state.tabs.filter { $0.isFolder }, + viewModel.state.folderTabs, id: \.self ) { tab in NavigationLink(value: tab) { @@ -47,10 +47,10 @@ struct SideBar: View { Divider() .padding(.horizontal, 10) - .foregroundStyle(.blue) + .foregroundStyle(.markBlack20) Button(action: { - // TODO: 구현 예정 + viewModel.send(.presentSheet(.addFolder)) }, label: { Label("새로운 폴더 만들기", systemImage: "plus") .lineLimit(1)