diff --git a/Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift b/Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift index eae36cde..7ad284c1 100644 --- a/Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift +++ b/Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift @@ -41,6 +41,10 @@ final class DataDependencyAssembler: DependencyAssembler { DIContainer.shared.register(type: TreatmentInterface.self) { return DefaultTreatmentRepository(networkService: self.networkService, userDefaultService: self.userDefaultService) } + DIContainer.shared.register(type: ChallengeInterface.self) { + return DefaultChallengeRepository( + networkService: self.networkService, userDefaultService: self.userDefaultService) + } DIContainer.shared.register(type: DemoInterface.self) { return DefaultDemoRepository( diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Model/ChallengeRoutineRequestDTO.swift b/Cherrish-iOS/Cherrish-iOS/Data/Model/ChallengeRoutineRequestDTO.swift new file mode 100644 index 00000000..851cec5c --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Model/ChallengeRoutineRequestDTO.swift @@ -0,0 +1,24 @@ +// +// ChallengeRoutineRequestDTO.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/19/26. +// + +import Foundation + +struct ChallengeRoutineRequestDTO: Decodable { + let id: Int + let name: String + let description: String +} + +extension ChallengeRoutineRequestDTO { + func toEntity() -> RoutineEntity { + RoutineEntity( + id: id, + name: name, + description: description + ) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Model/Extension/Encodable+.swift b/Cherrish-iOS/Cherrish-iOS/Data/Model/Extension/Encodable+.swift new file mode 100644 index 00000000..45628517 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Model/Extension/Encodable+.swift @@ -0,0 +1,19 @@ +// +// Encodable+.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/21/26. +// + +import Foundation + +extension Encodable { + func toDictionary() throws -> [String: Any] { + let data = try JSONEncoder().encode(self) + let json = try JSONSerialization.jsonObject(with: data) + guard let dictionary = json as? [String: Any] else { + throw CherrishError.encodingError + } + return dictionary + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Model/MakeChallengeRequestDTO.swift b/Cherrish-iOS/Cherrish-iOS/Data/Model/MakeChallengeRequestDTO.swift new file mode 100644 index 00000000..ecc743ed --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Model/MakeChallengeRequestDTO.swift @@ -0,0 +1,13 @@ +// +// MakeChallengeRequestDTO.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/21/26. +// + +import Foundation + +struct MakeChallengeRequestDTO: Encodable { + let homecareRoutineId: Int + let routineNames: [String] +} diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Model/RecommendMissionsRequestDTO.swift b/Cherrish-iOS/Cherrish-iOS/Data/Model/RecommendMissionsRequestDTO.swift new file mode 100644 index 00000000..76490518 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Model/RecommendMissionsRequestDTO.swift @@ -0,0 +1,12 @@ +// +// RecommendMissionsRequestDTO.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/20/26. +// + +import Foundation + +struct RecommendMissionsRequestDTO: Encodable { + let homecareRoutineId: Int +} diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Model/RecommendMissionsResponseDTO.swift b/Cherrish-iOS/Cherrish-iOS/Data/Model/RecommendMissionsResponseDTO.swift new file mode 100644 index 00000000..3121e05a --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Model/RecommendMissionsResponseDTO.swift @@ -0,0 +1,20 @@ +// +// RecommentMisssionsResponseDTO.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/20/26. +// + +import Foundation + +struct RecommendMissionsResponseDTO: Decodable { + let routines: [String] +} + +extension RecommendMissionsResponseDTO { + func toEntities() -> [ChallengeMissionEntity] { + return routines.enumerated().map { index, title in + ChallengeMissionEntity(id: index, title: title) + } + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/ChallengeAPI.swift b/Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/ChallengeAPI.swift new file mode 100644 index 00000000..9c019540 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/ChallengeAPI.swift @@ -0,0 +1,76 @@ +// +// ChallengeAPI.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/19/26. +// + +import Foundation + +import Alamofire + +enum ChallengeAPI: EndPoint { + case fetchRoutines + case aiRecommendations(homecareRoutineId: Int) + + var basePath: String { + return "/api/challenges" + } + + var path: String { + switch self { + case .fetchRoutines: + return "/homecare-routines" + case .aiRecommendations: + return "/ai-recommendations" + } + } + + + var method: Alamofire.HTTPMethod{ + switch self { + case .fetchRoutines: + return .get + case .aiRecommendations: + return .post + } + } + + + var headers: HeaderType { + switch self { + case .fetchRoutines: + return .basic + case .aiRecommendations: + return .basic + } + } + + var parameterEncoding: any Alamofire.ParameterEncoding { + switch self { + case .fetchRoutines: + return URLEncoding.default + case .aiRecommendations: + return JSONEncoding.default + } + } + + var queryParameters: [String : Any]? { + switch self { + case .fetchRoutines: + return nil + case .aiRecommendations: + return nil + } + } + + var bodyParameters: Parameters? { + switch self { + case .fetchRoutines: + return nil + case .aiRecommendations(let homecareRoutineId): + return ["homecareRoutineId" : homecareRoutineId] + } + } + +} diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/ChallengeDemoAPI.swift b/Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/ChallengeDemoAPI.swift new file mode 100644 index 00000000..3db1a634 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/ChallengeDemoAPI.swift @@ -0,0 +1,60 @@ +// +// ChallengeDemoAPI.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/21/26. +// + +import Foundation +import Alamofire + +enum ChallengeDemoAPI: EndPoint { + case createChallenge(userID: Int, requestDTO: MakeChallengeRequestDTO) + + var basePath: String { + return "/api/demo" + } + + var path: String { + switch self { + case .createChallenge: + return "/challenges" + } + } + + var method: Alamofire.HTTPMethod{ + switch self { + case .createChallenge: + return .post + } + } + + + var headers: HeaderType { + switch self { + case .createChallenge(let userID, _): + return .withAuth(userID: userID) + } + } + + var parameterEncoding: any Alamofire.ParameterEncoding { + switch self { + case .createChallenge: + return JSONEncoding.default + } + } + + var queryParameters: [String : Any]? { + switch self { + case .createChallenge: + return nil + } + } + + var bodyParameters: Parameters? { + switch self { + case .createChallenge(_, let dto): + return try? dto.toDictionary() + } + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Data/Repository/ChallengeRepository.swift b/Cherrish-iOS/Cherrish-iOS/Data/Repository/ChallengeRepository.swift new file mode 100644 index 00000000..fc0ea21d --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Data/Repository/ChallengeRepository.swift @@ -0,0 +1,46 @@ +// +// ChallengeRepository.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/19/26. +// + +import Foundation + +import Alamofire + +struct DefaultChallengeRepository: ChallengeInterface { + private let networkService: NetworkService + private let userDefaultService: UserDefaultService + + init(networkService: NetworkService, userDefaultService: UserDefaultService) { + self.networkService = networkService + self.userDefaultService = userDefaultService + } + + func fetchHomecareRoutines() async throws -> [RoutineEntity] { + let response = try await networkService.request(ChallengeAPI.fetchRoutines, decodingType: [ChallengeRoutineRequestDTO].self) + + return response.map { $0.toEntity() } + } + + func aiRecommendations(id: Int) async throws -> [ChallengeMissionEntity] { + let response = try await + networkService.request(ChallengeAPI.aiRecommendations(homecareRoutineId: id), + decodingType: RecommendMissionsResponseDTO.self) + CherrishLogger.debug(response) + return response.toEntities() + } + + func createChallenge(missionIds: Int, routineNames: [String]) async throws { + let userID: Int = userDefaultService.load(key: .userID) ?? 1 + let response: () = try await networkService.request( + ChallengeDemoAPI.createChallenge(userID: userID, requestDTO: + .init( + homecareRoutineId: missionIds, + routineNames: routineNames + ) + ) + ) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift b/Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift index 7d7c0b30..d5bfd173 100644 --- a/Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift +++ b/Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift @@ -92,5 +92,22 @@ final class DomainDependencyAssembler: DependencyAssembler { DIContainer.shared .register(type: CreateUserProcedureUseCase.self) { return DefaultCreateUserProcedureUseCase(repository: treatmentRepository) } + + guard let challengeRepository = DIContainer.shared.resolve(type: ChallengeInterface.self) else { + return + } + + DIContainer.shared.register(type: FetchChllengeHomecareRoutinesUseCase.self) { + return DefaultFetchChallengeHomecareRoutinesUseCase(repository: challengeRepository) + } + + + DIContainer.shared.register(type: PostChallengeRecommendUseCase.self) { + return DefaultSubmitChallengRecommendUseCase(repository: challengeRepository) + } + + DIContainer.shared.register(type: CreateChallengeUseCase.self) { + return DefaultCreateChallengeUseCase(repository: challengeRepository) + } } } diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/Interface/ChallengeInterface.swift b/Cherrish-iOS/Cherrish-iOS/Domain/Interface/ChallengeInterface.swift new file mode 100644 index 00000000..dcb718fe --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Domain/Interface/ChallengeInterface.swift @@ -0,0 +1,14 @@ +// +// ChallengeInterface.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/19/26. +// + +import Foundation + +protocol ChallengeInterface { + func fetchHomecareRoutines() async throws -> [RoutineEntity] + func aiRecommendations(id: Int) async throws -> [ChallengeMissionEntity] + func createChallenge(missionIds: Int, routineNames: [String]) async throws +} diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/Model/ChallengeEntity.swift b/Cherrish-iOS/Cherrish-iOS/Domain/Model/ChallengeEntity.swift new file mode 100644 index 00000000..09d9d2ff --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Domain/Model/ChallengeEntity.swift @@ -0,0 +1,13 @@ +// +// ChallengeEntity.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/21/26. +// + +import Foundation + +struct ChallengeEntity: Identifiable { + let id: Int + let routineNames: [String] +} diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/Model/ChallengeMissionEntity.swift b/Cherrish-iOS/Cherrish-iOS/Domain/Model/ChallengeMissionEntity.swift new file mode 100644 index 00000000..7af0615a --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Domain/Model/ChallengeMissionEntity.swift @@ -0,0 +1,13 @@ +// +// ChallengeMissionEntity.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/20/26. +// + +import Foundation + +struct ChallengeMissionEntity: Identifiable, Equatable, Hashable { + let id: Int + let title: String +} diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/Model/RoutineEntity.swift b/Cherrish-iOS/Cherrish-iOS/Domain/Model/RoutineEntity.swift new file mode 100644 index 00000000..d0ff4062 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Domain/Model/RoutineEntity.swift @@ -0,0 +1,12 @@ +// +// RoutineEntity.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/19/26. +// + +struct RoutineEntity: Identifiable, Equatable { + let id: Int + let name: String + let description: String +} diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/CreateChallengeUseCase.swift b/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/CreateChallengeUseCase.swift new file mode 100644 index 00000000..0891ab7c --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/CreateChallengeUseCase.swift @@ -0,0 +1,24 @@ +// +// CreateChallengeUseCase.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/21/26. +// + +import Foundation + +protocol CreateChallengeUseCase { + func execute(id: Int, routines: [String]) async throws +} + +struct DefaultCreateChallengeUseCase: CreateChallengeUseCase { + private let repository: ChallengeInterface + + init(repository: ChallengeInterface) { + self.repository = repository + } + + func execute(id: Int, routines: [String]) async throws { + _ = try await repository.createChallenge(missionIds: id, routineNames: routines) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchChllengeHomecareRoutinesUseCase.swift b/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchChllengeHomecareRoutinesUseCase.swift new file mode 100644 index 00000000..70bce056 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchChllengeHomecareRoutinesUseCase.swift @@ -0,0 +1,25 @@ +// +// FetchChllengeHomecareRoutinesUseCase.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/20/26. +// + +import Foundation + +protocol FetchChllengeHomecareRoutinesUseCase { + func excute() async throws -> [RoutineEntity] +} + +struct DefaultFetchChallengeHomecareRoutinesUseCase: FetchChllengeHomecareRoutinesUseCase { + + private let repository: ChallengeInterface + + init(repository: ChallengeInterface) { + self.repository = repository + } + + func excute() async throws -> [RoutineEntity] { + return try await repository.fetchHomecareRoutines() + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/PostChallengeRecommendUseCase.swift b/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/PostChallengeRecommendUseCase.swift new file mode 100644 index 00000000..56e49032 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Domain/UseCase/PostChallengeRecommendUseCase.swift @@ -0,0 +1,25 @@ +// +// PostChallengeRecommendUseCase.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/20/26. +// + +import Foundation + +protocol PostChallengeRecommendUseCase { + func excute(id: Int) async throws -> [ChallengeMissionEntity] +} + +struct DefaultSubmitChallengRecommendUseCase: PostChallengeRecommendUseCase { + + private let repository: ChallengeInterface + + init(repository: ChallengeInterface) { + self.repository = repository + } + + func excute(id: Int) async throws -> [ChallengeMissionEntity] { + return try await repository.aiRecommendations(id: id) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift index f6c7343b..d5ff8a3f 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift @@ -60,7 +60,7 @@ struct ChallengeProgressView: View { .padding(.trailing, 12.adjustedW) TypographyText("7일 플랜", style: .body3_m_12, color: .gray700) .padding(.horizontal, 8.adjustedW) - .padding(.vertical, 4.adjustedH) + .padding(.vertical, 3.adjustedH) .overlay( RoundedRectangle(cornerRadius: 4) .stroke(.gray700, lineWidth: 1) diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeView.swift deleted file mode 100644 index e5ec0391..00000000 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeView.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ChallengeView.swift -// Cherrish-iOS -// -// Created by sumin Kong on 1/9/26. -// - -import SwiftUI - -struct ChallengeView: View { - var body: some View { - ZStack { - Color.red600 - Text("ChallengeView") - } - } -} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift index 55c21015..4cc489ad 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift @@ -10,9 +10,7 @@ import SwiftUI enum ChallengeRoute: PresentationTypeProtocol { case root case startChallenge - case selectRoutine - case loading - case selectMission + case createChallenge case challengeProgress } diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinatorView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinatorView.swift index 41a9f8b1..c2bb85fa 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinatorView.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinatorView.swift @@ -17,22 +17,12 @@ struct ChallengeCoordinatorView: View { Group { switch route { case .root: - ViewFactory.shared.makeChallengeView() + ViewFactory.shared.makeHomeView() case .startChallenge: ViewFactory.shared.makeStartChallengeView() - case .selectRoutine: - ViewFactory.shared.makeSelectRoutineView() - .onAppear() { - tabBarCoordinator.isTabbarHidden = true - } - case .loading: - ViewFactory.shared.makeLoadingView() - .onAppear() { - tabBarCoordinator.isTabbarHidden = true - } - case .selectMission: - ViewFactory.shared.makeSelectMissionView() - .onAppear() { + case .createChallenge: + ViewFactory.shared.makeCreateChallengeView() + .onAppear { tabBarCoordinator.isTabbarHidden = true } case .challengeProgress: diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift deleted file mode 100644 index b6d0257c..00000000 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// SelectRoutineViewModel.swift -// Cherrish-iOS -// -// Created by sumin Kong on 1/16/26. -// - -import SwiftUI -import Combine - -enum RoutineType: CaseIterable, Identifiable { - case skinCondition - case lifeStyle - case bodyShaping - case wellness - - var id: Self { self } - - var title: String { - switch self { - case .skinCondition: - return "피부 컨디션" - case .lifeStyle: - return "생활습관" - case .bodyShaping: - return "체형 관리" - case .wellness: - return "웰니스∙마음챙김" - } - } -} - -@MainActor -final class SelectRoutineViewModel: ObservableObject { - -// @Published var routines: [RoutineType] = [] - @Published var routines: [RoutineType] = RoutineType.allCases - - @Published var selectedRoutine: RoutineType? = nil - - var nextButtonState: ButtonState { - selectedRoutine == nil ? .normal : .active - } - - func select(_ routine: RoutineType) { - selectedRoutine = routine - } -} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift deleted file mode 100644 index b692a9b8..00000000 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// LoadingView.swift -// Cherrish-iOS -// -// Created by sumin Kong on 1/10/26. -// - -import SwiftUI - -struct LoadingView: View { - @EnvironmentObject private var challengeCoordinator: ChallengeCoordinator - @State private var navigationTask: Task? - - var body: some View { - VStack { - CherrishNavigationBar( - isDisplayRightButton:false, - leftButtonAction: challengeCoordinator.pop - ) - VStack(spacing: 4.adjustedH) { - highlight(highlightText: "피부 컨디션", normalText: "관리 방향을 바탕으로") - .padding(.top, 113.adjustedH) - TypographyText("TO-DO 미션을 만들고 있어요.", style: .title1_sb_18, color: .gray800) - Image(.loading) - .padding(.top, 17.adjustedH) - TypographyText("잠시만 기다려주세요!", style: .title2_sb_16, color: .gray800) - .padding(.top, 17.adjustedH) - Spacer() - TypographyText("AI가 맞춤형 루틴을 제작하고 있어요.", style: .body3_m_12, color: .gray600) - .padding(.bottom, 30.adjustedH) - } - } - .frame(maxHeight: .infinity) - .onAppear { - moveNextAfterDelay() - } - .onDisappear { - navigationTask?.cancel() - } - .ignoresSafeArea(edges: .bottom) - } - - private func moveNextAfterDelay() { - Task { - navigationTask = Task { - try? await Task.sleep(nanoseconds: 3_000_000_000) - guard !Task.isCancelled else { return } - await MainActor.run { - challengeCoordinator.push(.selectMission) - } - } - } - } -} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift deleted file mode 100644 index 5e7fec3a..00000000 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -//SelectMissionView.swift -// Cherrish-iOS -// -// Created by sumin Kong on 1/10/26. -// - -import SwiftUI - -struct SelectMissionView: View { - - @EnvironmentObject private var challengeCoordinator: ChallengeCoordinator - - @State private var missions: [String] = [ - "진정 토너 + 세럼", - "진정 토너 + 세럼", - "진정 토너 + 세럼", - "선크림 3번 바르기", - "선크림 3번 바르기", - "선크림 3번 바르기" - ] - @State private var selectedStates: [Bool] = Array(repeating: false, count: 6) - - private var nextButtonState: ButtonState { - selectedStates.contains(true) ? .active : .normal - } - - var body: some View { - VStack { - Spacer() - .frame(height: 20.adjustedH) - CherrishNavigationBar( - title: "TO-DO 미션 선택", - leftButtonAction: challengeCoordinator.pop, - rightButtonAction: challengeCoordinator.popToRoot - ) - .padding(.top, 20.adjustedH) - Spacer() - .frame(height: 48.adjustedH) - VStack { - HStack { - VStack(alignment: .leading) { - TypographyText("챌린지 기간 동안", - style: .title1_sb_18, - color: .gray1000 - ) - TypographyText("진행할 미션을 선택해주세요.", - style: .title1_sb_18, - color: .gray1000 - ) - TypographyText("복수 선택이 가능해요.", - style: .body1_r_14, - color: .gray700 - ) - .padding(.top, 4.adjustedH) - } - Spacer() - } - Spacer() - .frame(height: 30.adjustedH) - VStack(spacing: 10.adjustedH) { - ForEach(missions.indices, id: \.self) { index in - MissionCard( - missionText: missions[index], - isSelected: $selectedStates[index] - ) - } - } - } - .padding(.horizontal, 34.adjustedW) - Spacer() - .frame(height: 48.adjustedH) - .padding(.horizontal, 33.adjustedW) - - CherrishButton( - title: "플래너에 추가하기", - type: .large, - state: .constant(nextButtonState), - leadingIcon: nil, - trailingIcon: nil - ){ - challengeCoordinator.push(.challengeProgress) - } - .padding(.horizontal, 24.adjustedW) - .padding(.top, 64.adjustedH) - .padding(.bottom, 38.adjustedH) - } - .padding(.top, 20.adjustedH) - .onAppear { - selectedStates = Array(repeating: false, count: missions.count) - } - .onChange(of: missions) { newMissions in - selectedStates = Array(repeating: false, count: newMissions.count) - } - .ignoresSafeArea(edges: .bottom) - } -} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift deleted file mode 100644 index 573b3065..00000000 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// SelectRoutineView.swift -// Cherrish-iOS -// -// Created by sumin Kong on 1/13/26. -// - -import SwiftUI - -struct SelectRoutineView: View { - - @EnvironmentObject private var challengeCoordinator: ChallengeCoordinator - @EnvironmentObject private var tabBarCoordinator: TabBarCoordinator - - @StateObject private var viewModel = SelectRoutineViewModel() - - private let columns = [ - GridItem(.flexible(), spacing: 12), - GridItem(.flexible(), spacing: 12) - ] - - - var body: some View { - VStack(spacing: 0) { - CherrishNavigationBar( - title: "루틴 챌린지 선택", - leftButtonAction: { - challengeCoordinator.pop() - tabBarCoordinator.isTabbarHidden = false - }, - rightButtonAction: { - challengeCoordinator.popToRoot() - tabBarCoordinator.isTabbarHidden = false - } - ) - HStack{ - VStack(alignment: .leading){ - TypographyText("지금 나에게 가장 필요한\n관리 루틴을 선택해주세요.", - style: .title1_sb_18, - color: .gray1000 - ) - } - Spacer() - } - .padding(.top, 80.adjustedH) - .padding(.horizontal, 33.adjustedW) - - VStack(spacing: 12) { - LazyVGrid(columns: columns, spacing: 12) { - ForEach(viewModel.routines) { routine in - routineChip(routine) - } - } - .padding(.horizontal, 33.adjustedW) - .padding(.top, 40.adjustedH) - } - - Spacer() - - CherrishButton( - title: "다음", - type: .large, - state: .constant(viewModel.nextButtonState), - leadingIcon: nil, - trailingIcon: nil - ){ - challengeCoordinator.push(.loading) - } - .padding(.bottom, 38.adjustedH) - .padding(.horizontal, 24.adjustedW) - } - .ignoresSafeArea(edges: .bottom) - } -} - -private extension SelectRoutineView { - func routineChip(_ routine: RoutineType) -> some View { - SelectionChip( - title: routine.title, - isSelected: Binding( - get: {viewModel.selectedRoutine == routine}, - set: {isSelected in - guard isSelected else { return } - viewModel.select(routine)} - ) - ) - } -} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift index 9f04c1e5..72207bfe 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift @@ -7,7 +7,7 @@ import SwiftUI -struct StartChallengeView: View { +struct ChallengeStartChallengeView: View { @EnvironmentObject private var challengeCoordinator: ChallengeCoordinator @EnvironmentObject private var tabBarCoordinator: TabBarCoordinator @@ -43,8 +43,7 @@ struct StartChallengeView: View { leadingIcon: nil, trailingIcon: nil ) { - - challengeCoordinator.push(.selectRoutine) + challengeCoordinator.push(.createChallenge) tabBarCoordinator.isTabbarHidden = true } .padding(.horizontal, 24.adjustedW) diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeLoadingView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeLoadingView.swift new file mode 100644 index 00000000..8d587720 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeLoadingView.swift @@ -0,0 +1,31 @@ +// +// LoadingView.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/10/26. +// + +import SwiftUI + +struct ChallengeLoadingView: View { + @ObservedObject var viewModel: CreateChallengeViewModel + + var body: some View { + VStack { + VStack(spacing: 4.adjustedH) { + highlight(highlightText: viewModel.selectedRoutine?.description ?? "", normalText: "관리 방향을 바탕으로") + .padding(.top, 113.adjustedH) + TypographyText("TO-DO 미션을 만들고 있어요.", style: .title1_sb_18, color: .gray800) + Image(.loading) + .padding(.top, 17.adjustedH) + TypographyText("잠시만 기다려주세요!", style: .title2_sb_16, color: .gray800) + .padding(.top, 17.adjustedH) + Spacer() + TypographyText("AI가 맞춤형 루틴을 제작하고 있어요.", style: .body3_m_12, color: .gray600) + .padding(.bottom, 30.adjustedH) + } + } + .frame(maxHeight: .infinity) + .ignoresSafeArea(edges: .bottom) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeSelectMissionView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeSelectMissionView.swift new file mode 100644 index 00000000..a044ace9 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeSelectMissionView.swift @@ -0,0 +1,82 @@ +// +//SelectMissionView.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/10/26. +// + +import SwiftUI + +struct ChallengeSelectMissionView: View { + + @EnvironmentObject private var challengeCoordinator: ChallengeCoordinator + @ObservedObject var viewModel: CreateChallengeViewModel + + + var body: some View { + VStack { + VStack { + HStack { + VStack(alignment: .leading) { + TypographyText("챌린지 기간 동안", + style: .title1_sb_18, + color: .gray1000 + ) + TypographyText("진행할 미션을 선택해주세요.", + style: .title1_sb_18, + color: .gray1000 + ) + TypographyText("복수 선택이 가능해요.", + style: .body1_r_14, + color: .gray700 + ) + .padding(.top, 4.adjustedH) + } + Spacer() + } + .padding(.bottom, 30.adjustedH) + VStack(spacing: 10.adjustedH) { + ForEach(viewModel.missions) { mission in + MissionCard( + missionText: mission.title, + + isSelected: Binding( + get: { + viewModel.missionsSelectedState[mission] ?? false + }, + set: { newValue in + viewModel.missionsSelectedState[mission] = newValue + } + ) + ) + } + } + } + .padding(.horizontal, 34.adjustedW) + .padding(.top, 48.adjustedH) + Spacer() + + CherrishButton( + title: "플래너에 추가하기", + type: .large, + state: $viewModel.nextButtonState, + leadingIcon: nil, + trailingIcon: nil + ) + { + Task { + do { + try await viewModel.makeChallenge() + challengeCoordinator.push(.challengeProgress) + } catch { + CherrishLogger.error(error) + } + } + } + .padding(.horizontal, 24.adjustedW) + .padding(.bottom, 38.adjustedH) + } + .frame(maxHeight: .infinity) + .ignoresSafeArea(edges: .bottom) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeSelectRoutineView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeSelectRoutineView.swift new file mode 100644 index 00000000..4ee31641 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/ChallengeSelectRoutineView.swift @@ -0,0 +1,104 @@ +// +// SelectRoutineView.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/13/26. +// + +import SwiftUI + +struct ChallengeSelectRoutineView: View { + @ObservedObject var viewModel: CreateChallengeViewModel + + private let columns = [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12) + ] + + var body: some View { + VStack { + HStack{ + VStack(alignment: .leading){ + TypographyText("지금 나에게 가장 필요한\n관리 루틴을 선택해주세요.", + style: .title1_sb_18, + color: .gray1000 + ) + } + Spacer() + } + .padding(.top, 80.adjustedH) + .padding(.horizontal, 33.adjustedW) + + VStack(spacing: 12) { + LazyVGrid(columns: columns, spacing: 12) { + ForEach(viewModel.routines) { routine in + routineChip(routine) + .onTapGesture { + viewModel.selectedRoutine = routine + viewModel.nextButtonState = .active + } + } + } + .padding(.horizontal, 33.adjustedW) + .padding(.top, 40.adjustedH) + + } + + Spacer() + + CherrishButton( + title: "다음", + type: .large, + state: $viewModel.nextButtonState, + leadingIcon: nil, + trailingIcon: nil + ){ + guard let routineId = viewModel.selectedRoutine?.id else { + return + } + viewModel.next() + + Task { + do { + async let requestTask: () = viewModel.postChallengeRecommend() + async let minimumDelay: () = Task.sleep(nanoseconds: 3_000_000_000) + + _ = try await (requestTask, minimumDelay) + + viewModel.isLoading = false + viewModel.next() + } catch { + CherrishLogger.error(error) + } + } + } + .padding(.bottom, 38.adjustedH) + .padding(.horizontal, 24.adjustedW) + } + .ignoresSafeArea(edges: .bottom) + .task { + do { + try await viewModel.fetchRoutines() + } catch { + CherrishLogger.error(error) + } + } + + } +} + +private extension ChallengeSelectRoutineView { + @ViewBuilder + func routineChip(_ routine: RoutineEntity) -> some View { + SelectionChip( + title: routine.description, + isSelected: Binding( + get: {viewModel.selectedRoutine == routine}, + set: { isSelected in + guard isSelected else { return } + viewModel.selectRoutine(id: routine.id) + } + ) + ) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/CreateChallengeView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/CreateChallengeView.swift new file mode 100644 index 00000000..2721fcdc --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/View/CreateChallengeView.swift @@ -0,0 +1,45 @@ +// +// CreateChallengeView.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/21/26. +// + +import SwiftUI + +struct CreateChallengeView: View { + @EnvironmentObject var challengeCoordinator: ChallengeCoordinator + @EnvironmentObject var tabBarCoordinator: TabBarCoordinator + @StateObject var viewModel: CreateChallengeViewModel + + var body: some View { + VStack { + CherrishNavigationBar( + isDisplayLeftButton: viewModel.viewState.isLeftButton, + isDisplayRightButton: viewModel.viewState.isRightButton, + title: viewModel.viewState.title, + leftButtonAction: { + if viewModel.viewState == .routine { + tabBarCoordinator.isTabbarHidden = false + challengeCoordinator.popToRoot() + } else { + viewModel.previous() + } + }, + rightButtonAction: { + tabBarCoordinator.isTabbarHidden = false + challengeCoordinator.popToRoot() + + } + ) + switch viewModel.viewState { + case .routine: + ChallengeSelectRoutineView(viewModel: viewModel) + case .loading: + ChallengeLoadingView(viewModel: viewModel) + case .mission: + ChallengeSelectMissionView(viewModel: viewModel) + } + } + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ViewModel/CreateChallengeViewModel.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ViewModel/CreateChallengeViewModel.swift new file mode 100644 index 00000000..e2133567 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ViewModel/CreateChallengeViewModel.swift @@ -0,0 +1,114 @@ +// +// MakeChallengeViewModel.swift +// Cherrish-iOS +// +// Created by sumin Kong on 1/21/26. +// + +import Foundation + +enum ChallengeViewState: StepNavigatable { + case routine + case loading + case mission + + var title: String { + switch self { + case .routine: + return "루틴 챌린지 선택" + case .loading: + return "" + case .mission: + return "TO-DO 미션 선택" + } + } + + var isLeftButton: Bool { + return true + } + + var isRightButton: Bool { + switch self { + case .routine: + return true + case .loading: + return false + case .mission: + return true + } + } +} + +final class CreateChallengeViewModel: ObservableObject { + @Published var viewState: ChallengeViewState = .routine + @Published private(set) var routines: [RoutineEntity] = [] + @Published var selectedRoutine: RoutineEntity? + @Published var isLoading: Bool = false + @Published private(set) var missions: [ChallengeMissionEntity] = [] + @Published var nextButtonState: ButtonState = .normal + + @Published var missionsSelectedState: [ChallengeMissionEntity: Bool] = [:] + + private let fetchRoutineUseCase: FetchChllengeHomecareRoutinesUseCase + private let postChallengeRecommendUseCase: PostChallengeRecommendUseCase + private let createChallengeUseCase: CreateChallengeUseCase + + init( + fetchRoutineUseCase: FetchChllengeHomecareRoutinesUseCase, + postChallengeRecommendUseCase: PostChallengeRecommendUseCase, + createChallengeUseCase: CreateChallengeUseCase + ) { + self.fetchRoutineUseCase = fetchRoutineUseCase + self.postChallengeRecommendUseCase = postChallengeRecommendUseCase + self.createChallengeUseCase = createChallengeUseCase + } + + @MainActor + func fetchRoutines() async throws { + routines = try await fetchRoutineUseCase.excute() + CherrishLogger.debug("루틴 \(routines)") + } + + func next() { + viewState.next() + } + + func previous() { + viewState.previous() + } + + func selectRoutine(id: Int) { + guard let routine = routines.first(where: { $0.id == id }) else { + selectedRoutine = nil + nextButtonState = .normal + return + } + selectedRoutine = routine + nextButtonState = .active + } + + @MainActor + func postChallengeRecommend() async throws { + guard let selectedRoutine else { return } + isLoading = true + missions = try await postChallengeRecommendUseCase.excute(id: selectedRoutine.id) + CherrishLogger.debug("뷰모델 미션 \(missions)") + } + + func selectMission(mission: ChallengeMissionEntity) { + missionsSelectedState[mission]?.toggle() + CherrishLogger.debug("미션 체크 상태 \(missionsSelectedState)") + } + + @MainActor + func makeChallenge() async throws { + guard let selectedRoutine else { return } + + let selectedMissionList = missionsSelectedState + .filter(\.value) + .map(\.key.title) + CherrishLogger.debug("선택한 미션들: \(selectedMissionList)") + + try await createChallengeUseCase.execute(id: selectedRoutine.id, routines: selectedMissionList) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift index 2990017c..3230a7c9 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift @@ -104,6 +104,21 @@ final class PresentationDependencyAssembler: DependencyAssembler { return MyPageViewModel(fetchUserInfoUseCase: fetchUserInfoUseCase) } + guard let fetchChallengeHomecareRoutines = DIContainer.shared.resolve(type: FetchChllengeHomecareRoutinesUseCase.self) else { + return + } + guard let postChallengeRecommendUseCase = DIContainer.shared.resolve(type: PostChallengeRecommendUseCase.self) else { + return + } + + guard let createChallengeUseCase = DIContainer.shared.resolve(type: CreateChallengeUseCase.self) else { + return + } + + DIContainer.shared.register(type: CreateChallengeViewModel.self) { + return CreateChallengeViewModel(fetchRoutineUseCase: fetchChallengeHomecareRoutines, postChallengeRecommendUseCase: postChallengeRecommendUseCase, createChallengeUseCase: createChallengeUseCase) + } + guard let fetchChallengeUseCase = DIContainer.shared.resolve(type: FetchChallengeUseCase.self), let toggleRoutineUseCase = DIContainer.shared.resolve(type: ToggleRoutineUseCase.self), let advanceDayUseCase = DIContainer.shared.resolve(type: AdvanceDayUseCase.self) @@ -120,4 +135,5 @@ final class PresentationDependencyAssembler: DependencyAssembler { ) } } + } diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift index 7f95e4d6..f1584184 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift @@ -14,13 +14,10 @@ protocol ViewFactoryProtocol { func makeNoTreatmentView() -> NoTreatmentView func makeTreatmentView() -> TreatmentView func makeCalendarView() -> CalendarView - func makeChallengeView() -> ChallengeView func makeMyPageView() -> MyPageView func makeSelectTreatmentView() -> SelectTreatmentView - func makeStartChallengeView() -> StartChallengeView - func makeSelectRoutineView() -> SelectRoutineView - func makeSelectMissionView() -> SelectMissionView - func makeLoadingView() -> LoadingView + func makeStartChallengeView() -> ChallengeStartChallengeView + func makeCreateChallengeView() -> CreateChallengeView func makeChallengeProgressView() -> ChallengeProgressView } @@ -57,10 +54,6 @@ final class ViewFactory: ViewFactoryProtocol { return CalendarView(viewModel: viewModel) } - func makeChallengeView() -> ChallengeView { - return ChallengeView() - } - func makeMyPageView() -> MyPageView { guard let viewModel = DIContainer.shared.resolve(type: MyPageViewModel.self) else { fatalError() @@ -89,20 +82,15 @@ final class ViewFactory: ViewFactoryProtocol { return TreatmentView(viewModel: viewModel) } - func makeStartChallengeView() -> StartChallengeView { - return StartChallengeView() - } - - func makeSelectRoutineView() -> SelectRoutineView { - return SelectRoutineView() + func makeStartChallengeView() -> ChallengeStartChallengeView { + return ChallengeStartChallengeView() } - func makeSelectMissionView() -> SelectMissionView { - return SelectMissionView() - } - - func makeLoadingView() -> LoadingView { - return LoadingView() + func makeCreateChallengeView() -> CreateChallengeView { + guard let viewModel = DIContainer.shared.resolve(type: CreateChallengeViewModel.self) else { + fatalError() + } + return CreateChallengeView(viewModel: viewModel) } func makeChallengeProgressView() -> ChallengeProgressView {