diff --git a/Projects/DataSource/Sources/DTO/OnboardingDTO.swift b/Projects/DataSource/Sources/DTO/OnboardingDTO.swift new file mode 100644 index 00000000..c71f19dd --- /dev/null +++ b/Projects/DataSource/Sources/DTO/OnboardingDTO.swift @@ -0,0 +1,23 @@ +// +// OnboardingDTO.swift +// DataSource +// +// Created by 최정인 on 8/27/25. +// + +import Domain + +struct OnboardingDTO: Encodable { + let timeSlot: String + let emotionType: [String] + let realOutingFrequency: String? + let targetOutingFrequency: String + + func toOnboardingEntity() -> OnboardingEntity { + return OnboardingEntity( + time: timeSlot, + feeling: emotionType, + frequency: realOutingFrequency, + outdoor: targetOutingFrequency) + } +} diff --git a/Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift b/Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift new file mode 100644 index 00000000..7ac7e19b --- /dev/null +++ b/Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift @@ -0,0 +1,21 @@ +// +// OnboardingResponseDTO.swift +// DataSource +// +// Created by 최정인 on 9/1/25. +// + +import Domain + +struct OnboardingResponseDTO: Decodable { + let timeSlot: String + let emotionTypes: [String] + let targetOutingFrequency: String + + func toOnboardingEntity() -> OnboardingEntity { + return OnboardingEntity( + time: timeSlot, + feeling: emotionTypes, + outdoor: targetOutingFrequency) + } +} diff --git a/Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift b/Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift index 6746cb18..a7f61baa 100644 --- a/Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift +++ b/Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift @@ -12,7 +12,7 @@ struct RecommendedRoutineDTO: Decodable { let routineName: String let routineDescription: String let routineLevel: String? - let routineType: String + let routineType: String? let subRoutines: [RecommendedSubRoutineDTO] enum CodingKeys: String, CodingKey { @@ -32,6 +32,11 @@ extension RecommendedRoutineDTO { routineCategory = RoutineCategoryType(rawValue: category) } + var type: RoutineCategoryType? + if let routineType { + type = RoutineCategoryType(rawValue: routineType) + } + var level: RoutineLevelType? if let routineLevel { level = RoutineLevelType(rawValue: routineLevel) @@ -41,7 +46,7 @@ extension RecommendedRoutineDTO { title: routineName, description: routineDescription, category: routineCategory, - type: RoutineCategoryType(rawValue: routineType) ?? .rest, + type: type, level: level, subRoutines: subRoutines.compactMap({ $0.toRecommendedSubRoutineEntity() })) } diff --git a/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift b/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift index 36cd8ddc..de24680a 100644 --- a/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift +++ b/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift @@ -6,29 +6,32 @@ // enum OnboardingEndpoint { - case registerOnboarding(choices: [String: String]) + case loadOnboardingResult + case registerOnboarding(onboarding: OnboardingDTO) case registerRecommendedRoutine(selectedRoutines: [Int]) } extension OnboardingEndpoint: Endpoint { var baseURL: String { - switch self { - case .registerOnboarding: - return AppProperties.baseURL + "/api/v1/onboardings" - case .registerRecommendedRoutine: - return AppProperties.baseURL + "/api/v2/onboardings" - } + return AppProperties.baseURL + "/api/v2/onboardings" } var path: String { switch self { - case .registerOnboarding: baseURL - case .registerRecommendedRoutine: baseURL + "/routines" + case .registerOnboarding, .loadOnboardingResult: + return baseURL + case .registerRecommendedRoutine: + return baseURL + "/routines" } } var method: HTTPMethod { - return .post + switch self { + case .loadOnboardingResult: + return .get + case .registerOnboarding, .registerRecommendedRoutine: + return .post + } } var headers: [String : String] { @@ -45,10 +48,12 @@ extension OnboardingEndpoint: Endpoint { var bodyParameters: [String : Any] { switch self { - case .registerOnboarding(let choices): - return choices + case .registerOnboarding(let onboarding): + return onboarding.dictionary case .registerRecommendedRoutine(let selectedRoutines): return ["recommendedRoutineIds": selectedRoutines] + default: + return [:] } } diff --git a/Projects/DataSource/Sources/Repository/OnboardingRepository.swift b/Projects/DataSource/Sources/Repository/OnboardingRepository.swift index 5a687bd2..e3de37e5 100644 --- a/Projects/DataSource/Sources/Repository/OnboardingRepository.swift +++ b/Projects/DataSource/Sources/Repository/OnboardingRepository.swift @@ -11,25 +11,29 @@ final class OnboardingRepository: OnboardingRepositoryProtocol { private let networkService = NetworkService.shared private let userDefaultsStorage = UserDefaultsStorage.shared - func registerOnboarding(onboardingChoices: [String : String]) async throws -> [RecommendedRoutineEntity] { - let endpoint = OnboardingEndpoint.registerOnboarding(choices: onboardingChoices) - guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self) - else { return [] } + func loadOnboardingResult() async throws -> OnboardingEntity { + let endpoint = OnboardingEndpoint.loadOnboardingResult + guard let response = try await networkService.request(endpoint: endpoint, type: OnboardingResponseDTO.self) + else { throw UserError.onboardingLoadFailed } - guard userDefaultsStorage.save(true, forKey: UserDefaultsKey.onboarding.rawValue) - else { throw UserError.onboardingSaveFailed } + let onboardingEntity = response.toOnboardingEntity() + return onboardingEntity + } + func registerOnboarding(onboardingEntity: OnboardingEntity) async throws -> [RecommendedRoutineEntity] { + let onboardingDTO = OnboardingDTO( + timeSlot: onboardingEntity.time, + emotionType: onboardingEntity.feeling, + realOutingFrequency: onboardingEntity.frequency, + targetOutingFrequency: onboardingEntity.outdoor) + let endpoint = OnboardingEndpoint.registerOnboarding(onboarding: onboardingDTO) + guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self) + else { return [] } + let recommendedRoutineEntity = response.recommendedRoutines.compactMap({ $0.toRecommendedRoutineEntity() }) return recommendedRoutineEntity } - func isOnboardingDone() -> Bool { - guard let isOnboardingDone: Bool = userDefaultsStorage.load(forKey: UserDefaultsKey.onboarding.rawValue) - else { return false } - - return isOnboardingDone - } - func registerRecommendedRoutines(selectedRoutines: [Int]) async throws { let endpoint = OnboardingEndpoint.registerRecommendedRoutine(selectedRoutines: selectedRoutines) _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) diff --git a/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift b/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift index 1774043b..c2470108 100644 --- a/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift +++ b/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift @@ -5,25 +5,25 @@ // Created by 최정인 on 7/15/25. // -public enum OnboardingChoiceType: CaseIterable { - case morningTime - case eveningTime - case allTime - - case never - case rarely - case sometimes - case often - - case stability - case connection - case growth - case vitality - - case once - case twoToThree - case fourOrMore - case notSure +public enum OnboardingChoiceType: String, CaseIterable { + case morningTime = "08:00:00" + case eveningTime = "20:00:00" + case allTime = "00:00:00" + + case never = "NEVER" + case rarely = "SHORT" + case sometimes = "SOMETIMES" + case often = "OFTEN" + + case stability = "STABILITY" + case connection = "CONNECTEDNESS" + case growth = "GROWTH" + case vitality = "VITALITY" + + case once = "ONE_PER_WEEK" + case twoToThree = "TWO_TO_THREE_PER_WEEK" + case fourOrMore = "MORE_THAN_FOUR_PER_WEEK" + case notSure = "UNKNOWN" public var onboardingType: OnboardingType { switch self { @@ -47,27 +47,4 @@ public enum OnboardingChoiceType: CaseIterable { case .notSure: .outdoor } } - - var value: String { - switch self { - case .morningTime: "08:00:00" - case .eveningTime: "20:00:00" - case .allTime: "00:00:00" - - case .never: "NEVER" - case .rarely: "SHORT" - case .sometimes: "SOMETIMES" - case .often: "OFTEN" - - case .stability: "STABILITY" - case .connection: "CONNECTEDNESS" - case .growth: "GROWTH" - case .vitality: "VITALITY" - - case .once: "ONE_PER_WEEK" - case .twoToThree: "TWO_TO_THREE_PER_WEEK" - case .fourOrMore: "MORE_THAN_FOUR_PER_WEEK" - case .notSure: "UNKNOWN" - } - } } diff --git a/Projects/Domain/Sources/Entity/OnboardingEntity.swift b/Projects/Domain/Sources/Entity/OnboardingEntity.swift new file mode 100644 index 00000000..8109472c --- /dev/null +++ b/Projects/Domain/Sources/Entity/OnboardingEntity.swift @@ -0,0 +1,25 @@ +// +// OnboardingEntity.swift +// Domain +// +// Created by 최정인 on 8/27/25. +// + +public struct OnboardingEntity { + public let time: String + public let feeling: [String] + public let frequency: String? + public let outdoor: String + + public init( + time: String, + feeling: [String], + frequency: String? = nil, + outdoor: String + ) { + self.time = time + self.feeling = feeling + self.frequency = frequency + self.outdoor = outdoor + } +} diff --git a/Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift b/Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift index 66384bf7..59811930 100644 --- a/Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift +++ b/Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift @@ -10,7 +10,7 @@ public struct RecommendedRoutineEntity { public let title: String public let description: String public let category: RoutineCategoryType? - public let type: RoutineCategoryType + public let type: RoutineCategoryType? public let level: RoutineLevelType? public let subRoutines: [RecommendedSubRoutineEntity] @@ -19,7 +19,7 @@ public struct RecommendedRoutineEntity { title: String, description: String, category: RoutineCategoryType?, - type: RoutineCategoryType, + type: RoutineCategoryType?, level: RoutineLevelType?, subRoutines: [RecommendedSubRoutineEntity] ) { diff --git a/Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift index 69f46cf9..ef2515aa 100644 --- a/Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift +++ b/Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift @@ -6,16 +6,17 @@ // public protocol OnboardingRepositoryProtocol { + /// 이전에 선택했던 온보딩 결과를 불러옵니다. + /// - Returns: 온보딩 선택 결과 + func loadOnboardingResult() async throws -> OnboardingEntity + /// 선택한 온보딩 결과를 저장하고, 추천 루틴을 받습니다. - /// - Parameter onboardingChoices: 선택한 온보딩 항목 Dictionary + /// - Parameter onboardingEntity: 선택한 온보딩 항목 /// - Returns: 온보딩 결과를 바탕으로 받은 추천루틴 목록 - func registerOnboarding(onboardingChoices: [String: String]) async throws -> [RecommendedRoutineEntity] + func registerOnboarding(onboardingEntity: OnboardingEntity) async throws -> [RecommendedRoutineEntity] /// 선택한 추천 루틴을 등록합니다. /// - Parameter selectedRoutines: 선택한 추천 루틴 ID 목록 func registerRecommendedRoutines(selectedRoutines: [Int]) async throws - /// 온보딩 여부를 반환합니다. - /// - Returns: 온보딩 여부 - func isOnboardingDone() -> Bool } diff --git a/Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift b/Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift index 81cfe6e6..5db4dbde 100644 --- a/Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift +++ b/Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift @@ -9,9 +9,9 @@ import Foundation public protocol ResultRecommendedRoutineUseCaseProtocol { /// 선택한 온보딩 결과를 저장하고, 추천 루틴을 받습니다. - /// - Parameter onboardingChoices: 선택한 온보딩 항목 list + /// - Parameter onboardingEntity: 선택한 온보딩 항목 /// - Returns: 온보딩 결과를 바탕으로 받은 추천루틴 목록 - func fetchResultRecommendedRoutines(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity] + func fetchResultRecommendedRoutines(onboardingEntity: OnboardingEntity) async throws -> [RecommendedRoutineEntity] /// 감정 구슬을 등록하고 그에 따른 추천 루틴 리스트를 받습니다. /// - Parameter emotion: 감정 구슬 타입 String diff --git a/Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift b/Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift index f344058e..cd8ed6f5 100644 --- a/Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift +++ b/Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift @@ -15,9 +15,8 @@ public final class ResultRecommendedRoutineUseCase: ResultRecommendedRoutineUseC self.emotionRepository = emotionRepository } - public func fetchResultRecommendedRoutines(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity] { - let choices = convertToDictionary(onboardingChoices: onboardingChoices) - let recommendedRoutines = try await onboardingRepository.registerOnboarding(onboardingChoices: choices) + public func fetchResultRecommendedRoutines(onboardingEntity: OnboardingEntity) async throws -> [RecommendedRoutineEntity] { + let recommendedRoutines = try await onboardingRepository.registerOnboarding(onboardingEntity: onboardingEntity) return recommendedRoutines } @@ -29,15 +28,4 @@ public final class ResultRecommendedRoutineUseCase: ResultRecommendedRoutineUseC public func registerRecommendedRoutines(selectedRoutines: [Int]) async throws { try await onboardingRepository.registerRecommendedRoutines(selectedRoutines: selectedRoutines) } - - private func convertToDictionary(onboardingChoices: [OnboardingChoiceType]) -> [String: String] { - var result: [String: String] = [:] - let onboardingTypes: [OnboardingType] = [.time, .frequency, .feeling, .outdoor] - for type in onboardingTypes { - guard let choice = onboardingChoices.first(where: { $0.onboardingType == type }) - else { break } - result[choice.onboardingType.key] = choice.value - } - return result - } } diff --git a/Projects/Presentation/Sources/Common/Component/ToastMessageView.swift b/Projects/Presentation/Sources/Common/Component/ToastMessageView.swift index 720f83cc..1d9e3dd9 100644 --- a/Projects/Presentation/Sources/Common/Component/ToastMessageView.swift +++ b/Projects/Presentation/Sources/Common/Component/ToastMessageView.swift @@ -39,7 +39,7 @@ public final class ToastMessageView: UIView { self.alpha = 0 self.isHidden = true - backgroundColor = BitnagilColor.navy400 + backgroundColor = BitnagilColor.gray30 layer.cornerRadius = 8 layer.masksToBounds = true diff --git a/Projects/Presentation/Sources/Common/Component/ToastView.swift b/Projects/Presentation/Sources/Common/Component/ToastView.swift new file mode 100644 index 00000000..584dbb69 --- /dev/null +++ b/Projects/Presentation/Sources/Common/Component/ToastView.swift @@ -0,0 +1,83 @@ +// +// ToastView.swift +// Presentation +// +// Created by 최정인 on 9/2/25. +// + +import SnapKit +import UIKit + +final class ToastView: UIView { + private enum Layout { + static let horizontalMargin: CGFloat = 16 + static let checkIconSize: CGFloat = 24 + static let messageLabelLeadingSpacing: CGFloat = 8 + } + + private let checkIcon = UIImageView() + private let messageLabel = UILabel() + private let message: String + + init(message: String) { + self.message = message + super.init(frame: .zero) + configureAttribute() + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureAttribute() { + alpha = 0 + isHidden = true + + layer.masksToBounds = true + layer.cornerRadius = 12 + backgroundColor = BitnagilColor.gray30 + + checkIcon.image = BitnagilIcon.orangeCheckedCircleIcon + + messageLabel.text = message + messageLabel.font = BitnagilFont(style: .body2, weight: .medium).font + messageLabel.textColor = .white + } + + private func configureLayout() { + addSubview(checkIcon) + addSubview(messageLabel) + + checkIcon.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview().offset(Layout.horizontalMargin) + make.size.equalTo(Layout.checkIconSize) + } + + messageLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(checkIcon.snp.trailing).offset(Layout.messageLabelLeadingSpacing) + make.trailing.equalToSuperview().offset(-Layout.horizontalMargin) + } + } + + func showToastMessageView() { + alpha = 0 + isHidden = false + + UIView.animate(withDuration: 0.35, delay: 0.01) { + self.alpha = 1 + } completion: { [weak self] _ in + self?.hideToastMessageView() + } + } + + private func hideToastMessageView() { + UIView.animate(withDuration: 0.35, delay: 2.0) { + self.alpha = 0 + } completion: { _ in + self.isHidden = true + } + } +} diff --git a/Projects/Presentation/Sources/Common/Extension/Notification+.swift b/Projects/Presentation/Sources/Common/Extension/Notification+.swift new file mode 100644 index 00000000..2afe4099 --- /dev/null +++ b/Projects/Presentation/Sources/Common/Extension/Notification+.swift @@ -0,0 +1,12 @@ +// +// Notification+.swift +// Presentation +// +// Created by 최정인 on 9/3/25. +// + +import Foundation + +extension Notification.Name { + static let showRecommendedRoutineToast = Notification.Name("showRecommendedRoutineToast") +} diff --git a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift index c5deaaad..2de21f0d 100644 --- a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift +++ b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift @@ -50,7 +50,10 @@ public struct PresentationDependencyAssembler: DependencyAssemblerProtocol { guard let userDataRepository = container.resolve(type: UserDataRepositoryProtocol.self) else { fatalError("userDataRepository 의존성이 등록되지 않았습니다.") } - return OnboardingViewModel(userDataRepository: userDataRepository) + guard let onboardingRepository = container.resolve(type: OnboardingRepositoryProtocol.self) + else { fatalError("userDataRepository 의존성이 등록되지 않았습니다.") } + + return OnboardingViewModel(userDataRepository: userDataRepository, onboardingRepository: onboardingRepository) } DIContainer.shared.register(type: RecommendedRoutineViewModel.self) { container in diff --git a/Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift b/Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift index f5f7d7fb..78a97486 100644 --- a/Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift +++ b/Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift @@ -127,7 +127,7 @@ extension EmotionRegisterView: UICollectionViewDelegate { else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } resultRecommendedRoutineViewModel.configure(viewModelType: .emotion(emotion: selectedEmotion)) - let resultRecommendedRoutineView = ResultRecommendedRoutineView(entryPoint: .emotion, viewModel: resultRecommendedRoutineViewModel) + let resultRecommendedRoutineView = ResultRecommendedRoutineViewController(entryPoint: .emotion, viewModel: resultRecommendedRoutineViewModel) self.navigationController?.pushViewController(resultRecommendedRoutineView, animated: true) } } diff --git a/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift b/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift index 8537c92b..d8f8dbb7 100644 --- a/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift +++ b/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift @@ -46,7 +46,7 @@ final class TotalAgreementButton: UIButton { stackView.isUserInteractionEnabled = false checkButton.image = BitnagilIcon.checkIcon - checkButton.tintColor = BitnagilColor.navy100 + checkButton.tintColor = BitnagilColor.gray90 checkButton.contentMode = .scaleAspectFit buttonLabel.text = "전체동의" diff --git a/Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift b/Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift index 959b6caf..0da5b4fc 100644 --- a/Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift +++ b/Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift @@ -120,11 +120,12 @@ final class TermsAgreementViewController: BaseViewController { guard let self else { return } if agreementResult { BitnagilLogger.log(logType: .debug, message: "약관 동의 성공") - guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) else { - fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.") - } - let onboardingView = OnboardingViewController(viewModel: onboardingViewModel, onboarding: .time) - self.navigationController?.pushViewController(onboardingView, animated: true) + + guard let introViewModel = DIContainer.shared.resolve(type: IntroViewModel.self) + else { fatalError("introViewModel 의존성이 등록되지 않았습니다.") } + + let introView = IntroViewController(viewModel: introViewModel) + self.navigationController?.pushViewController(introView, animated: true) } else { // TODO: 약관 동의 실패 시, 에러 처리를 해야 합니다. BitnagilLogger.log(logType: .error, message: "약관 동의 실패") diff --git a/Projects/Presentation/Sources/MyPage/View/MypageView.swift b/Projects/Presentation/Sources/MyPage/View/MypageView.swift index da8ccb76..fe900e14 100644 --- a/Projects/Presentation/Sources/MyPage/View/MypageView.swift +++ b/Projects/Presentation/Sources/MyPage/View/MypageView.swift @@ -167,14 +167,10 @@ extension MypageView: UITableViewDataSource { return } - guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) else { - fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.") - } + guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) + else { fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.") } - let onboardingView = OnboardingViewController( - viewModel: onboardingViewModel, - onboarding: .time, - isFromMypage: true) + let onboardingView = OnboardingResultViewController(viewModel: onboardingViewModel, entryPoint: .myPagePrevious) onboardingView.hidesBottomBarWhenPushed = true navigationController?.pushViewController(onboardingView, animated: true) } diff --git a/Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift b/Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift index 76059f41..f5b68adf 100644 --- a/Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift +++ b/Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift @@ -43,7 +43,7 @@ extension RecommendedRoutineEntity { subTitle: description, subRoutines: subRoutines.map({ $0.title }), routineCategory: category ?? .recommendation, - routineType: type, + routineType: type ?? .recommendation, routineLevel: level ?? .easy ) } diff --git a/Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift b/Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift index b07cc723..ae4c1bd6 100644 --- a/Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift +++ b/Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift @@ -12,6 +12,12 @@ import SnapKit import UIKit final class OnboardingResultViewController: BaseViewController { + enum EntryPoint { + case onboarding + case myPagePrevious + case myPageResult + } + private enum Layout { static let horizontalMargin: CGFloat = 20 static let mainLabelMinTopSpacing: CGFloat = 60 @@ -42,17 +48,17 @@ final class OnboardingResultViewController: BaseViewController - init(viewModel: OnboardingViewModel, isFromMypage: Bool = false) { - self.isFromMypage = isFromMypage + init(viewModel: OnboardingViewModel, entryPoint: EntryPoint = .onboarding) { + self.entryPoint = entryPoint cancellables = [] super.init(viewModel: viewModel) } @@ -64,7 +70,14 @@ final class OnboardingResultViewController: BaseViewController { onboarding: nextStep, isFromMypage: isFromMypage) } else { - nextView = OnboardingResultViewController(viewModel: viewModel, isFromMypage: isFromMypage) + if isFromMypage { + nextView = OnboardingResultViewController(viewModel: viewModel, entryPoint: .myPageResult) + } else { + nextView = OnboardingResultViewController(viewModel: viewModel, entryPoint: .onboarding) + } } guard let nextView else { return } self.navigationController?.pushViewController(nextView, animated: true) diff --git a/Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift b/Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift index 2cc7f3e6..22ac7c69 100644 --- a/Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift +++ b/Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift @@ -15,6 +15,7 @@ final class OnboardingViewModel: ViewModel { case selectOnboardingChoice(selectedChoice: OnboardingChoiceType) case fetchOnboardingChoice(onboarding: OnboardingType) case fetchOnboardingChoices + case loadOnboardingResult case makeOnboardingResult } @@ -25,7 +26,7 @@ final class OnboardingViewModel: ViewModel { let feelingOnboardingChoicePublisher: AnyPublisher, Never> let outdoorOnboardingChoicePublisher: AnyPublisher let onboardingResultPublisher: AnyPublisher<[String], Never> - let onboardingChoicesPublisher: AnyPublisher<[OnboardingChoiceType], Never> + let onboardingChoicesPublisher: AnyPublisher let nextButtonPublisher: AnyPublisher } @@ -36,12 +37,14 @@ final class OnboardingViewModel: ViewModel { private let feelingOnboardingChoiceSubject = CurrentValueSubject, Never>([]) private let outdoorOnboardingChoiceSubject = CurrentValueSubject(nil) private let onboardingResultSubject = CurrentValueSubject<[String], Never>([]) - private let onboardingChoicesSubject = PassthroughSubject<[OnboardingChoiceType], Never>() + private let onboardingChoicesSubject = PassthroughSubject() private let nextButtonSubject = PassthroughSubject() private let userDataRepository: UserDataRepositoryProtocol - init(userDataRepository: UserDataRepositoryProtocol) { + private let onboardingRepository: OnboardingRepositoryProtocol + init(userDataRepository: UserDataRepositoryProtocol, onboardingRepository: OnboardingRepositoryProtocol) { self.userDataRepository = userDataRepository + self.onboardingRepository = onboardingRepository self.output = Output( nicknamePublisher: nicknameSubject.eraseToAnyPublisher(), timeOnboardingChoicePublisher: timeOnboardingChoiceSubject.eraseToAnyPublisher(), @@ -68,6 +71,9 @@ final class OnboardingViewModel: ViewModel { case .fetchOnboardingChoices: makeOnboardingChoices() + case .loadOnboardingResult: + loadOnboardingResult() + case .makeOnboardingResult: makeOnboardingResult() } @@ -166,6 +172,30 @@ final class OnboardingViewModel: ViewModel { nextButtonSubject.send(result) } + // 이전 온보딩 선택 결과를 불러옵니다. + private func loadOnboardingResult() { + Task { + do { + let onboardingEntity = try await onboardingRepository.loadOnboardingResult() + + let feelingOnboardingChoices = onboardingEntity.feeling.compactMap({ OnboardingChoiceType(rawValue: $0 )}) + let feelingResult = feelingOnboardingChoices.compactMap({ $0.resultTitle }).joined(separator: ", ") + + guard + let timeOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.time), + let outdoorOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.outdoor), + let timeResult = timeOnboardingChoice.resultTitle, + let outdoorResult = outdoorOnboardingChoice.resultTitle + else { return } + + let result = [timeResult, feelingResult, outdoorResult] + onboardingResultSubject.send(result) + } catch { + // TODO: 에러 처리 + } + } + } + // 온보딩 결과 텍스트를 만듭니다. private func makeOnboardingResult() { let feelingOnboardingChoice = feelingOnboardingChoiceSubject.value @@ -186,8 +216,6 @@ final class OnboardingViewModel: ViewModel { // 온보딩 선택지를 통합합니다. private func makeOnboardingChoices() { - var onboardingChoices: [OnboardingChoiceType] = [] - let feelingOnboarding = Array(feelingOnboardingChoiceSubject.value) guard let timeOnboarding = timeOnboardingChoiceSubject.value, @@ -196,11 +224,12 @@ final class OnboardingViewModel: ViewModel { let outdoorOnboarding = outdoorOnboardingChoiceSubject.value else { return } - onboardingChoices.append(timeOnboarding) - onboardingChoices += feelingOnboarding - onboardingChoices.append(frequencyOnboarding) - onboardingChoices.append(outdoorOnboarding) + let onboardingEntity = OnboardingEntity( + time: timeOnboarding.rawValue, + feeling: feelingOnboarding.map({ $0.rawValue }), + frequency: frequencyOnboarding.rawValue, + outdoor: outdoorOnboarding.rawValue) - onboardingChoicesSubject.send(onboardingChoices) + onboardingChoicesSubject.send(onboardingEntity) } } diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift index 48ab349f..363d1542 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift @@ -32,7 +32,8 @@ final class RecommendedRoutineViewController: BaseViewController public override init(viewModel: RecommendedRoutineViewModel) { @@ -128,6 +130,8 @@ final class RecommendedRoutineViewController: BaseViewController { - +final class ResultRecommendedRoutineViewController: BaseViewController { enum EntryPoint { case onboarding case mypage @@ -23,18 +22,16 @@ final class ResultRecommendedRoutineView: BaseViewController