diff --git a/Projects/DataSource/Sources/Common/Extension/Encodable+.swift b/Projects/DataSource/Sources/Common/Extension/Encodable+.swift new file mode 100644 index 00000000..79eba303 --- /dev/null +++ b/Projects/DataSource/Sources/Common/Extension/Encodable+.swift @@ -0,0 +1,18 @@ +// +// Encodable+.swift +// DataSource +// +// Created by 이동현 on 8/3/25. +// + +import Foundation + +extension Encodable { + var dictionary: [String: Any] { + guard + let data = try? JSONEncoder().encode(self), + let dictionary = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + else { return [:] } + return dictionary + } +} diff --git a/Projects/DataSource/Sources/DTO/RoutineCreationDTO.swift b/Projects/DataSource/Sources/DTO/RoutineCreationDTO.swift new file mode 100644 index 00000000..a499368f --- /dev/null +++ b/Projects/DataSource/Sources/DTO/RoutineCreationDTO.swift @@ -0,0 +1,13 @@ +// +// RoutienCreationDTO.swift +// DataSource +// +// Created by 이동현 on 8/3/25. +// + +struct RoutineCreationDTO: Codable { + let routineName: String + let repeatDay: [String] + let executionTime: String + let subRoutineName: [String] +} diff --git a/Projects/DataSource/Sources/DTO/RoutineResponseDTO.swift b/Projects/DataSource/Sources/DTO/RoutineResponseDTO.swift index ab867295..3373662e 100644 --- a/Projects/DataSource/Sources/DTO/RoutineResponseDTO.swift +++ b/Projects/DataSource/Sources/DTO/RoutineResponseDTO.swift @@ -26,13 +26,15 @@ struct RoutineResponseDTO: Decodable { extension RoutineResponseDTO { func toRoutineEntity() -> RoutineEntity { + let sortedSubRoutinesDTO = subRoutineSearchResultDto.sorted { $0.sortOrder < $1.sortOrder } + return RoutineEntity( routineId: routineId, historySeq: historySeq, routineName: routineName, repeatDay: repeatDay, executionTime: executionTime, - subRoutineSearchResultDto: subRoutineSearchResultDto.map({ $0.toSubRoutineEntity() }), + subRoutineSearchResultDto: sortedSubRoutinesDTO.map({ $0.toSubRoutineEntity() }), modifiedYn: modifiedYn, routineCompletionId: routineCompletionId, completeYn: completeYn, diff --git a/Projects/DataSource/Sources/DTO/RoutineUpdateDTO.swift b/Projects/DataSource/Sources/DTO/RoutineUpdateDTO.swift new file mode 100644 index 00000000..52ded8a4 --- /dev/null +++ b/Projects/DataSource/Sources/DTO/RoutineUpdateDTO.swift @@ -0,0 +1,14 @@ +// +// RoutineUpdateDTO.swift +// DataSource +// +// Created by 이동현 on 8/3/25. +// + +struct RoutineUpdateDTO: Codable { + let routineId: String + let routineName: String + let repeatDay: [String] + let executionTime: String + let subRoutineInfos: [SubRoutineUpdateDTO] +} diff --git a/Projects/DataSource/Sources/DTO/SubRoutineUpdateDTO.swift b/Projects/DataSource/Sources/DTO/SubRoutineUpdateDTO.swift new file mode 100644 index 00000000..52aeddcc --- /dev/null +++ b/Projects/DataSource/Sources/DTO/SubRoutineUpdateDTO.swift @@ -0,0 +1,12 @@ +// +// SubRoutineUpdateDTO.swift +// DataSource +// +// Created by 이동현 on 8/3/25. +// + +struct SubRoutineUpdateDTO: Codable { + let subRoutineId: String? + let subRoutineName: String? + let sortOrder: Int? +} diff --git a/Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift b/Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift index 5896af7c..b6ddcd0b 100644 --- a/Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift +++ b/Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift @@ -6,7 +6,10 @@ // enum RoutineEndpoint { + case createRoutine(routine: RoutineCreationDTO) + case fetchRoutine(routineId: String) case fetchRoutines(startDate: String, endDate: String) + case updateRoutine(routine: RoutineUpdateDTO) } extension RoutineEndpoint: Endpoint { @@ -16,13 +19,21 @@ extension RoutineEndpoint: Endpoint { var path: String { switch self { - case .fetchRoutines: baseURL + case .fetchRoutine(let routineId): + "\(baseURL)/\(routineId)" + default: + baseURL } } var method: HTTPMethod { switch self { - case .fetchRoutines: .get + case .createRoutine: + .post + case .fetchRoutine, .fetchRoutines: + .get + case .updateRoutine: + .patch } } @@ -40,11 +51,20 @@ extension RoutineEndpoint: Endpoint { return [ "startDate": startDate, "endDate": endDate] + default: + return [:] } } var bodyParameters: [String : Any] { - return [:] + switch self { + case .createRoutine(let routine): + return routine.dictionary + case .updateRoutine(let routine): + return routine.dictionary + default: + return [:] + } } var isAuthorized: Bool { diff --git a/Projects/DataSource/Sources/Repository/RoutineRepository.swift b/Projects/DataSource/Sources/Repository/RoutineRepository.swift index 46d92e13..eb9320c8 100644 --- a/Projects/DataSource/Sources/Repository/RoutineRepository.swift +++ b/Projects/DataSource/Sources/Repository/RoutineRepository.swift @@ -10,6 +10,26 @@ import Domain final class RoutineRepository: RoutineRepositoryProtocol { private let networkService = NetworkService.shared + func createRoutine(routineSummary: RoutineSummaryEntity, subRoutineSummaries: [SubRoutineSummaryEntity]) async throws { + let subRoutineNames = subRoutineSummaries.compactMap { $0.subRoutineName } + + let routineCreationDTO = RoutineCreationDTO( + routineName: routineSummary.routineName, + repeatDay: routineSummary.repeatDay.map { $0.rawValue }, + executionTime: routineSummary.executionTime, + subRoutineName: subRoutineNames) + let endpoint = RoutineEndpoint.createRoutine(routine: routineCreationDTO) + + _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) + } + + func fetchRoutine(routineId: String) async throws -> RoutineEntity? { + let endpoint = RoutineEndpoint.fetchRoutine(routineId: routineId) + guard let response = try await networkService.request(endpoint: endpoint, type: RoutineResponseDTO.self) else { return nil } + + return response.toRoutineEntity() + } + func fetchRoutines(from startDate: String, to endDate: String) async throws -> [String: [RoutineEntity]] { let endpoint = RoutineEndpoint.fetchRoutines(startDate: startDate, endDate: endDate) guard let response = try await networkService.request(endpoint: endpoint, type: RoutineDictionaryDTO.self) @@ -21,4 +41,25 @@ final class RoutineRepository: RoutineRepositoryProtocol { } return result } + + func updateRoutine(routineSummary: RoutineSummaryEntity, subRoutineSummaries: [SubRoutineSummaryEntity]) async throws { + guard let routineId = routineSummary.routineId else { return } + + let subRoutineDTO = subRoutineSummaries.map { + SubRoutineUpdateDTO( + subRoutineId: $0.subRoutineId, + subRoutineName: $0.subRoutineName, + sortOrder: $0.sortOrder) + } + + let routineUpdateDTO = RoutineUpdateDTO( + routineId: routineId, + routineName: routineSummary.routineName, + repeatDay: routineSummary.repeatDay.map { $0.rawValue }, + executionTime: routineSummary.executionTime, + subRoutineInfos: subRoutineDTO) + let endpoint = RoutineEndpoint.updateRoutine(routine: routineUpdateDTO) + + _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) + } } diff --git a/Projects/Domain/Sources/Entity/Enum/WeekType.swift b/Projects/Domain/Sources/Entity/Enum/WeekType.swift new file mode 100644 index 00000000..822bfa68 --- /dev/null +++ b/Projects/Domain/Sources/Entity/Enum/WeekType.swift @@ -0,0 +1,16 @@ +// +// Week.swift +// Domain +// +// Created by 이동현 on 8/3/25. +// + +public enum WeekType: String, CaseIterable { + case monday = "MONDAY" + case tuesday = "TUESDAY" + case wednesday = "WEDNESDAY" + case thursday = "THURSDAY" + case friday = "FRIDAY" + case saturday = "SATURDAY" + case sunday = "SUNDAY" +} diff --git a/Projects/Domain/Sources/Entity/RoutineEntity.swift b/Projects/Domain/Sources/Entity/RoutineEntity.swift index 6b068df5..ceb7d8e5 100644 --- a/Projects/Domain/Sources/Entity/RoutineEntity.swift +++ b/Projects/Domain/Sources/Entity/RoutineEntity.swift @@ -6,10 +6,10 @@ // public struct RoutineEntity { - public let routineId: String + public let routineId: String? public let historySeq: Int public let routineName: String - public let repeatDay: [String] + public let repeatDay: [WeekType] public let executionTime: String public let subRoutineSearchResultDto: [SubRoutineEntity] public let modifiedYn: Bool @@ -18,7 +18,7 @@ public struct RoutineEntity { public let routineType: String public init( - routineId: String, + routineId: String?, historySeq: Int, routineName: String, repeatDay: [String]?, @@ -29,10 +29,12 @@ public struct RoutineEntity { completeYn: Bool, routineType: String ) { + let weekType: [WeekType] = repeatDay?.compactMap(WeekType.init(rawValue:)) ?? [] + self.routineId = routineId self.historySeq = historySeq self.routineName = routineName - self.repeatDay = repeatDay ?? [] + self.repeatDay = weekType self.executionTime = executionTime self.subRoutineSearchResultDto = subRoutineSearchResultDto self.modifiedYn = modifiedYn diff --git a/Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift b/Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift new file mode 100644 index 00000000..616f408b --- /dev/null +++ b/Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift @@ -0,0 +1,27 @@ +// +// RoutineSummaryEntity.swift +// Domain +// +// Created by 이동현 on 8/3/25. +// + +public struct RoutineSummaryEntity { + public let routineId: String? + public let routineName: String + public let repeatDay: [WeekType] + public let executionTime: String + + public init( + routineId: String?, + routineName: String, + repeatDay: [String]?, + executionTime: String + ) { + let weekType: [WeekType] = repeatDay?.compactMap(WeekType.init(rawValue:)) ?? [] + + self.routineId = routineId + self.routineName = routineName + self.repeatDay = weekType + self.executionTime = executionTime + } +} diff --git a/Projects/Domain/Sources/Entity/SubRoutineEntity.swift b/Projects/Domain/Sources/Entity/SubRoutineEntity.swift index 5b423d4b..091bf49e 100644 --- a/Projects/Domain/Sources/Entity/SubRoutineEntity.swift +++ b/Projects/Domain/Sources/Entity/SubRoutineEntity.swift @@ -6,7 +6,7 @@ // public struct SubRoutineEntity: Decodable { - public let subRoutineId: String + public let subRoutineId: String? public let historySeq: Int public let subRoutineName: String public let modifiedYn: Bool diff --git a/Projects/Domain/Sources/Entity/SubRoutineSummaryEntity.swift b/Projects/Domain/Sources/Entity/SubRoutineSummaryEntity.swift new file mode 100644 index 00000000..100d6ba5 --- /dev/null +++ b/Projects/Domain/Sources/Entity/SubRoutineSummaryEntity.swift @@ -0,0 +1,22 @@ +// +// SubRoutineSummaryEntity.swift +// Domain +// +// Created by 이동현 on 8/3/25. +// + +public struct SubRoutineSummaryEntity: Decodable, Hashable { + public let subRoutineId: String? + public let subRoutineName: String? + public let sortOrder: Int? + + public init( + subRoutineId: String?, + subRoutineName: String?, + sortOrder: Int? + ) { + self.subRoutineId = subRoutineId + self.subRoutineName = subRoutineName + self.sortOrder = sortOrder + } +} diff --git a/Projects/Domain/Sources/Entity/Untitled.swift b/Projects/Domain/Sources/Entity/Untitled.swift new file mode 100644 index 00000000..b0e96cdd --- /dev/null +++ b/Projects/Domain/Sources/Entity/Untitled.swift @@ -0,0 +1,7 @@ +// +// Untitled.swift +// Domain +// +// Created by 이동현 on 8/3/25. +// + diff --git a/Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift index ee13d44b..45562454 100644 --- a/Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift +++ b/Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift @@ -7,9 +7,27 @@ // 루틴 관련 로직(조회, 완료, 등록, 삭제 등)을 수행하는 Repository public protocol RoutineRepositoryProtocol { - /// 루틴을 조회합니다. (기간) + /// 루틴을 생성합니다. + /// - Parameters: + /// - routineSummary: 루틴 요약 정보 + /// - subRoutineSummaries: 서브 루틴 요약 정보 배열 + func createRoutine(routineSummary: RoutineSummaryEntity, subRoutineSummaries: [SubRoutineSummaryEntity]) async throws + + /// 루틴을 조회합니다. + /// - Parameter routineId: 조회할 루틴 id + /// - Returns: 조회된 루틴 + func fetchRoutine(routineId: String) async throws -> RoutineEntity? + + /// 루틴 목록을 조회합니다. (기간) /// - Parameters: /// - startDate: 조회 시작 날짜 /// - endDate: 조회 종료 날짜 func fetchRoutines(from startDate: String, to endDate: String) async throws -> [String: [RoutineEntity]] + + + /// 루틴을 수정합니다. + /// - Parameters: + /// - routineSummary: 루틴 요약 정보 + /// - subRoutineSummaries: 서브 루틴 요약 정보 배열 + func updateRoutine(routineSummary: RoutineSummaryEntity, subRoutineSummaries: [SubRoutineSummaryEntity]) async throws } diff --git a/Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift b/Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift index bb129a62..6d3e4b9a 100644 --- a/Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift +++ b/Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift @@ -8,5 +8,13 @@ import Foundation public protocol RoutineUseCaseProtocol { + func fetchRoutine(routineId: String) async throws -> RoutineEntity? + func fetchRoutines(startDate: Date, endDate: Date) async throws -> [String: [RoutineEntity]] + + func saveRoutine( + routineSummary: RoutineSummaryEntity, + subRoutineSummaries: [SubRoutineSummaryEntity], + deletedSubRoutineSummaries: [SubRoutineSummaryEntity] + ) async throws } diff --git a/Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift b/Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift index 58ae3e52..6496177e 100644 --- a/Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift @@ -15,11 +15,59 @@ public final class RoutineUseCase: RoutineUseCaseProtocol { self.routineRepository = routineRepository } + public func fetchRoutine(routineId: String) async throws -> RoutineEntity? { + let routineEnity = try await routineRepository.fetchRoutine(routineId: routineId) + return routineEnity + } + public func fetchRoutines(startDate: Date, endDate: Date) async throws -> [String: [RoutineEntity]] { let start = startDate.convertToString(dateType: .yearMonthDate) let end = endDate.convertToString(dateType: .yearMonthDate) - var routineEntities = try await routineRepository.fetchRoutines(from: start, to: end) + let routineEntities = try await routineRepository.fetchRoutines(from: start, to: end) return routineEntities } + + public func saveRoutine( + routineSummary: RoutineSummaryEntity, + subRoutineSummaries: [SubRoutineSummaryEntity], + deletedSubRoutineSummaries: [SubRoutineSummaryEntity] + ) async throws { + if routineSummary.routineId == nil { // 루틴 아이디가 있으면 수정, 없으면 생성 + try await createRoutine(routineSummary: routineSummary, subRoutinesSummaries: subRoutineSummaries) + } else { + try await updateRoutine( + routineSummary: routineSummary, + subRoutineSummaries: subRoutineSummaries, + deletedSubRoutineSummaries: deletedSubRoutineSummaries) + } + } + + private func createRoutine(routineSummary: RoutineSummaryEntity, subRoutinesSummaries: [SubRoutineSummaryEntity]) async throws { + try await routineRepository.createRoutine(routineSummary: routineSummary, subRoutineSummaries: subRoutinesSummaries) + } + + private func updateRoutine( + routineSummary: RoutineSummaryEntity, + subRoutineSummaries: [SubRoutineSummaryEntity], + deletedSubRoutineSummaries: [SubRoutineSummaryEntity] + ) async throws { + let updatedSubRoutines = subRoutineSummaries + .enumerated() + .map { + SubRoutineSummaryEntity( + subRoutineId: $1.subRoutineId, + subRoutineName: $1.subRoutineName, + sortOrder: $0 + 1) + } + let deletedSubRoutines = deletedSubRoutineSummaries.map { + SubRoutineSummaryEntity( + subRoutineId: $0.subRoutineId, + subRoutineName: nil, + sortOrder: nil) + } + let finalSubRoutines = updatedSubRoutines + deletedSubRoutines + + try await routineRepository.updateRoutine(routineSummary: routineSummary, subRoutineSummaries: finalSubRoutines) + } } diff --git a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift index 4f69ce3e..0cd688b0 100644 --- a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift +++ b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift @@ -66,8 +66,11 @@ public struct PresentationDependencyAssembler: DependencyAssemblerProtocol { return EmotionRegisterViewModel(emotionUseCase: emotionUseCase) } - DIContainer.shared.register(type: RoutineCreationViewModel.self) { _ in - return RoutineCreationViewModel() + DIContainer.shared.register(type: RoutineCreationViewModel.self) { container in + guard let routineUseCase = container.resolve(type: RoutineUseCaseProtocol.self) + else { fatalError("routineUseCase 의존성이 등록되지 않았습니다.") } + + return RoutineCreationViewModel(routineUseCase: routineUseCase) } DIContainer.shared.register(type: ResultRecommendedRoutineViewModel.self) { container in diff --git a/Projects/Presentation/Sources/Home/Model/MainRoutine.swift b/Projects/Presentation/Sources/Home/Model/MainRoutine.swift index bfaec215..77722cc2 100644 --- a/Projects/Presentation/Sources/Home/Model/MainRoutine.swift +++ b/Projects/Presentation/Sources/Home/Model/MainRoutine.swift @@ -18,13 +18,19 @@ struct MainRoutine { } extension RoutineEntity { - func toMainRoutine() -> MainRoutine { + func toMainRoutine() -> MainRoutine? { + guard let routineId else { return nil } + + let subRoutines = subRoutineSearchResultDto.compactMap { $0.toSubRoutine() } + let isAllConverted = subRoutines.count == subRoutineSearchResultDto.count + guard isAllConverted else { return nil } + return MainRoutine( id: routineId, title: routineName, isDone: completeYn, startTime: Date.convertToDate(from: executionTime, dateType: .time) ?? Date(), - repeatDay: repeatDay.compactMap({ Week(rawValue: $0) }), - subRoutines: subRoutineSearchResultDto.map({ $0.toSubRoutine() })) + repeatDay: repeatDay.compactMap({ Week(rawValue: $0.rawValue) }), + subRoutines: subRoutines) } } diff --git a/Projects/Presentation/Sources/Home/Model/SubRoutine.swift b/Projects/Presentation/Sources/Home/Model/SubRoutine.swift index e90867c3..0800b392 100644 --- a/Projects/Presentation/Sources/Home/Model/SubRoutine.swift +++ b/Projects/Presentation/Sources/Home/Model/SubRoutine.swift @@ -15,7 +15,9 @@ struct SubRoutine: Hashable { } extension SubRoutineEntity { - func toSubRoutine() -> SubRoutine { + func toSubRoutine() -> SubRoutine? { + guard let subRoutineId else { return nil } + return SubRoutine( id: subRoutineId, title: subRoutineName, diff --git a/Projects/Presentation/Sources/Home/View/HomeView.swift b/Projects/Presentation/Sources/Home/View/HomeView.swift index 5560b9dc..9b68b274 100644 --- a/Projects/Presentation/Sources/Home/View/HomeView.swift +++ b/Projects/Presentation/Sources/Home/View/HomeView.swift @@ -529,7 +529,7 @@ extension HomeView: RoutineDetailViewDelegate { guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } - let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel) + let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel, routineId: routine.id) routineCreationView.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(routineCreationView, animated: true) } diff --git a/Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift b/Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift index 652212e7..fc3542ac 100644 --- a/Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift +++ b/Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift @@ -98,7 +98,7 @@ final class HomeViewModel: ViewModel { do { let entities = try await routineUseCase.fetchRoutines(startDate: startDate, endDate: endDate) for (date, routineEntities) in entities { - routines[date] = routineEntities.map({ $0.toMainRoutine() }) + routines[date] = routineEntities.compactMap({ $0.toMainRoutine() }) } fetchRoutineResultSubject.send(true) } catch { diff --git a/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift b/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift index 7e7ea106..83f2146e 100644 --- a/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift +++ b/Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift @@ -77,26 +77,30 @@ final class RoutineCreationView: BaseViewController { private let registerButton = UIButton() + private let navigationTitle: String + private let registerButtonTitle: String private var repeatRoutineTitleTopConstraint: Constraint? private var startTimeTitleTopConstraint: Constraint? private var cancellables = Set() - override init(viewModel: RoutineCreationViewModel) { + init(viewModel: RoutineCreationViewModel, routineId: String? = nil) { + navigationTitle = routineId == nil ? "루틴 등록" : "루틴 수정" + registerButtonTitle = routineId == nil ? "등록하기" : "수정하기" + super.init(viewModel: viewModel) + if let routineId { + viewModel.action(input: .fetchRoutine(id: routineId)) + } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func viewDidLoad() { - super.viewDidLoad() - } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - configureNavigationBar(navigationStyle: .withBackButton(title: "루틴 등록")) + configureNavigationBar(navigationStyle: .withBackButton(title: navigationTitle)) } override func configureAttribute() { @@ -171,7 +175,7 @@ final class RoutineCreationView: BaseViewController { registerButton.layer.cornerRadius = 12 registerButton.layer.masksToBounds = true - registerButton.setTitle("등록하기", for: .normal) + registerButton.setTitle(registerButtonTitle, for: .normal) registerButton.isEnabled = false registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font } @@ -519,6 +523,12 @@ final class RoutineCreationView: BaseViewController { self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.datePickerBottomSheetHeight) }, for: .touchUpInside) + + registerButton.addAction( + UIAction { [weak self] _ in + self?.viewModel.action(input: .registerRoutine) + }, + for: .touchUpInside) } private func configureSubroutineStackView() { diff --git a/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift b/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift index b14c377b..49b86a61 100644 --- a/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift +++ b/Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift @@ -4,7 +4,9 @@ // // Created by 이동현 on 7/20/25. // + import Combine +import Domain import Foundation final class RoutineCreationViewModel: ViewModel { @@ -38,6 +40,7 @@ final class RoutineCreationViewModel: ViewModel { } enum Input { + case fetchRoutine(id: String) case configureName(name: String) case addSubRoutine case deleteSubRoutine(index: Int) @@ -60,16 +63,23 @@ final class RoutineCreationViewModel: ViewModel { private(set) var output: Output private let nameSubject = CurrentValueSubject("") - private let subRoutinesSubject = CurrentValueSubject<[String], Never>([]) + private let subRoutinesSubject = CurrentValueSubject<[SubRoutineSummaryEntity], Never>([]) private let repeatTypeSubject = CurrentValueSubject(nil) private let weekDaySubject = CurrentValueSubject, Never>([]) private let executionTimeSubject = CurrentValueSubject(.none) private let checkRoutinePublisher = PassthroughSubject() + private let routineUseCase: RoutineUseCaseProtocol + private var deletedSubroutines = Set() + private var routineId: String? + + init(routineUseCase: RoutineUseCaseProtocol) { + self.routineUseCase = routineUseCase - init() { output = Output( namePublisher: nameSubject.eraseToAnyPublisher(), - subRoutinesPublisher: subRoutinesSubject.eraseToAnyPublisher(), + subRoutinesPublisher: subRoutinesSubject + .map { $0.compactMap { $0.subRoutineName } } + .eraseToAnyPublisher(), repeatTypePublisher: repeatTypeSubject.eraseToAnyPublisher(), weekDayPublisher: weekDaySubject.eraseToAnyPublisher(), executionTimePublisher: executionTimeSubject @@ -80,6 +90,8 @@ final class RoutineCreationViewModel: ViewModel { func action(input: Input) { switch input { + case .fetchRoutine(let id): + fetchRoutine(id: id) case .configureName(let name): configureName(name: name) case .addSubRoutine: @@ -89,7 +101,7 @@ final class RoutineCreationViewModel: ViewModel { case .configureSubRoutine(let name, let index): configureSubroutine(name: name, index: index) case .configureRepeatType(let type): - configureRepeatType(type: type) + configureRepeatType(selectedType: type) case .toggleRepeatDay(let weekDay): configureWeekDay(weekDay: weekDay) case .toggleRepeatAllDay: @@ -103,6 +115,43 @@ final class RoutineCreationViewModel: ViewModel { updateIsRoutineValid() } + private func fetchRoutine(id: String) { + Task { + do { + // TODO: - routine fetch 실패 시 처리 방안 필요 (기획과 논의~) + guard let routine = try await routineUseCase.fetchRoutine(routineId: id) else { return } + + let subRoutines = routine.subRoutineSearchResultDto.map { + SubRoutineSummaryEntity( + subRoutineId: $0.subRoutineId, + subRoutineName: $0.subRoutineName, + sortOrder: $0.sortOrder) + } + let weekDay = routine.repeatDay.compactMap { Week(rawValue: $0.rawValue) } + let repeatType: RepeatType = weekDay.count == Week.allCases.count ? .daily : .week + let executionType: ExecutionType + + if routine.executionTime == "00:00:00" { + executionType = .allDay + } else { + let time = Date.convertToDate(from: routine.executionTime, dateType: .amPmTimeShort) + executionType = .time(startAt: time ?? Date()) + } + + nameSubject.send(routine.routineName) + subRoutinesSubject.send(subRoutines) + weekDaySubject.send(Set(weekDay)) + repeatTypeSubject.send(repeatType) + executionTimeSubject.send(executionType) + routineId = id + + updateIsRoutineValid() + } catch { + // TODO: - 요기도 마찬가지 (ViewModel 공통 todo) + } + } + } + private func configureName(name: String) { nameSubject.send(name) } @@ -111,7 +160,12 @@ final class RoutineCreationViewModel: ViewModel { var subRoutines = subRoutinesSubject.value guard subRoutines.count <= 3 else { return } - subRoutines.append("") + let newSubRoutine = SubRoutineSummaryEntity( + subRoutineId: nil, + subRoutineName: "", + sortOrder: subRoutines.count + 1) + + subRoutines.append(newSubRoutine) subRoutinesSubject.send(subRoutines) } @@ -122,6 +176,12 @@ final class RoutineCreationViewModel: ViewModel { index < subRoutines.count else { return } + let targetSubRoutine = subRoutines[index] + + if targetSubRoutine.subRoutineId != nil { + deletedSubroutines.insert(targetSubRoutine) + } + subRoutines.remove(at: index) subRoutinesSubject.send(subRoutines) } @@ -133,21 +193,29 @@ final class RoutineCreationViewModel: ViewModel { index < subRoutines.count else { return } - subRoutines[index] = name + let originalSubRoutine = subRoutines[index] + let newSubRoutine = SubRoutineSummaryEntity( + subRoutineId: originalSubRoutine.subRoutineId, + subRoutineName: name, + sortOrder: originalSubRoutine.sortOrder) + subRoutines[index] = newSubRoutine + subRoutinesSubject.send(subRoutines) } - private func configureRepeatType(type: RepeatType) { - var repeatType = repeatTypeSubject.value - - repeatType = repeatType == type - ? nil - : type + private func configureRepeatType(selectedType: RepeatType) { + let repeatType = repeatTypeSubject.value - if repeatType != .week { - weekDaySubject.send([]) + switch selectedType { + case .daily: + weekDaySubject.send(Set(Week.allCases)) + case .week: + if repeatType == .daily { + weekDaySubject.send([]) + } } - repeatTypeSubject.send(repeatType) + + repeatTypeSubject.send(selectedType) } private func configureWeekDay(weekDay: Week) { @@ -178,7 +246,12 @@ final class RoutineCreationViewModel: ViewModel { guard let name = nameSubject.value, !name.isEmpty, - executionTimeSubject.value != .none + executionTimeSubject.value != .none, + weekDaySubject.value.count > 0, + subRoutinesSubject + .value + .map({$0.subRoutineName}) + .allSatisfy({$0?.isEmpty == false }) else { checkRoutinePublisher.send(false) return @@ -188,6 +261,37 @@ final class RoutineCreationViewModel: ViewModel { } private func registerRoutine() { - // API 호출 + Task { + do { + let repeatDay = weekDaySubject + .value + .sorted(by: { $0.id < $1.id }) + .map { $0.rawValue } + + let executionTime: String + + switch executionTimeSubject.value { + case .time(let startAt): + executionTime = startAt.convertToString(dateType: .time) + case .allDay: + executionTime = "00:00:00" + case .none: + return + } + + let routineSummary = RoutineSummaryEntity( + routineId: routineId, + routineName: nameSubject.value ?? "", + repeatDay: repeatDay, + executionTime: executionTime) + + try await routineUseCase.saveRoutine( + routineSummary: routineSummary, + subRoutineSummaries: subRoutinesSubject.value, + deletedSubRoutineSummaries: Array(deletedSubroutines)) + } catch { + + } + } } }