Skip to content

Commit 032a2ed

Browse files
committed
Feat: 온보딩 선택 결과 바탕으로 추천 받은 루틴 등록 구현 (#T3-102)
1 parent 5612f65 commit 032a2ed

7 files changed

Lines changed: 92 additions & 6 deletions

File tree

Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Foundation
99

1010
enum OnboardingEndpoint {
1111
case registerOnboarding(accessToken: String, choices: [String: String])
12+
case registerRecommendedRoutine(accessToken: String, selectedRoutines: [Int])
1213
}
1314

1415
extension OnboardingEndpoint: Endpoint {
@@ -19,6 +20,7 @@ extension OnboardingEndpoint: Endpoint {
1920
var path: String {
2021
switch self {
2122
case .registerOnboarding: baseURL
23+
case .registerRecommendedRoutine: baseURL + "/routines"
2224
}
2325
}
2426

@@ -35,6 +37,8 @@ extension OnboardingEndpoint: Endpoint {
3537
switch self {
3638
case .registerOnboarding(let accessToken, _):
3739
headers["Authorization"] = "Bearer \(accessToken)"
40+
case .registerRecommendedRoutine(let accessToken, _):
41+
headers["Authorization"] = "Bearer \(accessToken)"
3842
}
3943

4044
return headers
@@ -48,6 +52,8 @@ extension OnboardingEndpoint: Endpoint {
4852
switch self {
4953
case .registerOnboarding(_, let choices):
5054
return choices
55+
case .registerRecommendedRoutine(_, let selectedRoutines):
56+
return ["recommendedRoutineIds": selectedRoutines]
5157
}
5258
}
5359
}

Projects/DataSource/Sources/Repository/OnboardingRepository.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ final class OnboardingRepository: OnboardingRepositoryProtocol {
2727
return recommendedRoutineEntity
2828
}
2929

30+
func registerRecommendedRoutines(selectedRoutines: [Int]) async throws {
31+
let accessToken = try loadToken(tokenType: .accessToken)
32+
let endpoint = OnboardingEndpoint.registerRecommendedRoutine(accessToken: accessToken, selectedRoutines: selectedRoutines)
33+
_ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self)
34+
}
35+
3036
private func loadToken(tokenType: TokenType) throws -> String {
3137
guard let token = keychainStorage.load(forKey: tokenType.rawValue) else {
3238
throw AuthError.tokenLoadFailed

Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ public protocol OnboardingRepositoryProtocol {
1010
/// - Parameter onboardingChoices: 선택한 온보딩 항목 Dictionary
1111
/// - Returns: 온보딩 결과를 바탕으로 받은 추천루틴 목록
1212
func registerOnboarding(onboardingChoices: [String: String]) async throws -> [RecommendedRoutineEntity]
13+
14+
/// 선택한 추천 루틴을 등록합니다.
15+
/// - Parameter selectedRoutines: 선택한 추천 루틴 ID 목록
16+
func registerRecommendedRoutines(selectedRoutines: [Int]) async throws
1317
}

Projects/Domain/Sources/Protocol/UseCase/OnboardingUseCaseProtocol.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ public protocol OnboardingUseCaseProtocol {
1010
/// - Parameter onboardingChoices: 선택한 온보딩 항목
1111
/// - Returns: 온보딩 결과를 바탕으로 받은 추천루틴 목록
1212
func registerOnboarding(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity]
13+
14+
/// 선택한 추천 루틴을 등록합니다.
15+
/// - Parameter selectedRoutines: 선택한 추천 루틴 ID 목록
16+
func registerRecommendedRoutines(selectedRoutines: [Int]) async throws
1317
}

Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public final class OnboardingUseCase: OnboardingUseCaseProtocol {
2020
return recommendedRoutines
2121
}
2222

23+
public func registerRecommendedRoutines(selectedRoutines: [Int]) async throws {
24+
try await onboardingRepository.registerRecommendedRoutines(selectedRoutines: selectedRoutines)
25+
}
26+
2327
private func convertToDictionary(onboardingChoices: [OnboardingChoiceType]) -> [String: String] {
2428
guard
2529
let timeSlot = onboardingChoices.filter({ $0.onboardingType == .time }).first,
@@ -35,7 +39,4 @@ public final class OnboardingUseCase: OnboardingUseCaseProtocol {
3539
}
3640
return result
3741
}
38-
39-
private func convertTo() {
40-
}
4142
}

Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import UIKit
99
import Combine
1010
import Domain
11+
import Shared
1112

1213
final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewModel> {
1314

@@ -20,7 +21,9 @@ final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewM
2021
static let routineStackViewTopSpacing: CGFloat = 28
2122
static let routineButtonHeight: CGFloat = 84
2223
static let registerButtonHeight: CGFloat = 54
23-
static let registerButtonBottomSpacing: CGFloat = 20
24+
static let registerButtonBottomSpacing: CGFloat = 10
25+
static let skipButtonHeight: CGFloat = 54
26+
static let skipButtonBottomSpacing: CGFloat = 20
2427

2528
static var mainLabelTopSpacing: CGFloat {
2629
let height = UIScreen.main.bounds.height
@@ -34,6 +37,8 @@ final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewM
3437
private let recommendedRoutineStackView = UIStackView()
3538
private var recommendedRoutines: [Int: OnboardingChoiceButton] = [:]
3639
private let registerButton = PrimaryButton(buttonState: .disabled, buttonTitle: "등록하기")
40+
private let skipButtonLabel = UILabel()
41+
private let skipButton = UIButton()
3742
private var cancellables: Set<AnyCancellable>
3843

3944
override init(viewModel: OnboardingViewModel) {
@@ -82,6 +87,20 @@ final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewM
8287
registerButton.addAction(UIAction { [weak self] _ in
8388
self?.viewModel.action(input: .registerRecommendedRoutine)
8489
}, for: .touchUpInside)
90+
91+
skipButtonLabel.do {
92+
$0.attributedText = BitnagilFont(
93+
fontSize: 14,
94+
lineHeight: 20,
95+
underline: true,
96+
weight: .regular
97+
).attributedString(text: "건너뛰기")
98+
$0.textColor = BitnagilColor.navy500
99+
}
100+
101+
skipButton.addAction(UIAction { [weak self] _ in
102+
self?.goToHomeView()
103+
}, for: .touchUpInside)
85104
}
86105

87106
override func configureLayout() {
@@ -92,6 +111,8 @@ final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewM
92111
view.addSubview(subLabel)
93112
view.addSubview(recommendedRoutineStackView)
94113
view.addSubview(registerButton)
114+
skipButton.addSubview(skipButtonLabel)
115+
view.addSubview(skipButton)
95116

96117
mainLabel.snp.makeConstraints { make in
97118
make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
@@ -116,9 +137,20 @@ final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewM
116137
registerButton.snp.makeConstraints { make in
117138
make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
118139
make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin)
119-
make.bottom.equalTo(safeArea).inset(Layout.registerButtonBottomSpacing)
140+
make.bottom.equalTo(skipButton.snp.top).offset(-Layout.registerButtonBottomSpacing)
120141
make.height.equalTo(Layout.registerButtonHeight)
121142
}
143+
144+
skipButtonLabel.snp.makeConstraints { make in
145+
make.center.equalToSuperview()
146+
}
147+
148+
skipButton.snp.makeConstraints { make in
149+
make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
150+
make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin)
151+
make.bottom.equalTo(safeArea).inset(Layout.skipButtonBottomSpacing)
152+
make.height.equalTo(Layout.skipButtonHeight)
153+
}
122154
}
123155

124156
override func bind() {
@@ -142,6 +174,18 @@ final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewM
142174
self?.registerButton.updateButtonState(buttonState: canRegister ? .default : .disabled)
143175
}
144176
.store(in: &cancellables)
177+
178+
viewModel.output.registerRoutineResultPublisher
179+
.receive(on: DispatchQueue.main)
180+
.sink { [weak self] registerResult in
181+
if registerResult {
182+
BitnagilLogger.log(logType: .debug, message: "추천 루틴 등록 완료")
183+
self?.goToHomeView()
184+
} else {
185+
BitnagilLogger.log(logType: .error, message: "추천 루틴 등록 실패")
186+
}
187+
}
188+
.store(in: &cancellables)
145189
}
146190

147191
private func updateRecommendedRoutines(routines: Set<RecommendedRoutine>) {
@@ -176,4 +220,12 @@ final class OnboardingRecommendedRoutineView: BaseViewController<OnboardingViewM
176220
}
177221
}
178222
}
223+
224+
private func goToHomeView() {
225+
guard let homeViewModel = DIContainer.shared.resolve(type: HomeViewModel.self) else {
226+
fatalError("homeViewModel 의존성이 등록되지 않았습니다.")
227+
}
228+
let homeView = HomeViewController(viewModel: homeViewModel)
229+
self.navigationController?.pushViewController(homeView, animated: true)
230+
}
179231
}

Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ final class OnboardingViewModel: ViewModel {
2727
let onboardingResultPublisher: AnyPublisher<[String], Never>
2828
let recommendedRoutinePublisher: AnyPublisher<Set<RecommendedRoutine>, Never>
2929
let selectedRoutinePublisher: AnyPublisher<Set<RecommendedRoutine>, Never>
30+
let registerRoutineResultPublisher: AnyPublisher<Bool, Never>
3031
let nextButtonPublisher: AnyPublisher<Bool, Never>
3132
}
3233

@@ -38,6 +39,7 @@ final class OnboardingViewModel: ViewModel {
3839
private let onboardingResultSubject = CurrentValueSubject<[String], Never>([])
3940
private let recommendedRoutineSubject = CurrentValueSubject<Set<RecommendedRoutine>, Never>([])
4041
private let selectedRoutineSubject = CurrentValueSubject<Set<RecommendedRoutine>, Never>([])
42+
private let registerRoutineResultSubject = PassthroughSubject<Bool, Never>()
4143
private let nextButtonSubject = PassthroughSubject<Bool, Never>()
4244

4345
private let onboardingUseCase: OnboardingUseCaseProtocol
@@ -51,6 +53,7 @@ final class OnboardingViewModel: ViewModel {
5153
onboardingResultPublisher: onboardingResultSubject.eraseToAnyPublisher(),
5254
recommendedRoutinePublisher: recommendedRoutineSubject.eraseToAnyPublisher(),
5355
selectedRoutinePublisher: selectedRoutineSubject.eraseToAnyPublisher(),
56+
registerRoutineResultPublisher: registerRoutineResultSubject.eraseToAnyPublisher(),
5457
nextButtonPublisher: nextButtonSubject.eraseToAnyPublisher()
5558
)
5659
}
@@ -225,6 +228,16 @@ final class OnboardingViewModel: ViewModel {
225228

226229
// 추천 루틴을 등록합니다.
227230
private func registerRecommendedRoutine() {
228-
// TODO: 서버 API 만들어진 후 UseCase와 연동하는 작업이 필요합니다.
231+
let selectedRoutinesId = selectedRoutineSubject.value.map({ $0.id })
232+
233+
Task {
234+
do {
235+
try await onboardingUseCase.registerRecommendedRoutines(selectedRoutines: selectedRoutinesId)
236+
registerRoutineResultSubject.send(true)
237+
} catch {
238+
BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)")
239+
registerRoutineResultSubject.send(false)
240+
}
241+
}
229242
}
230243
}

0 commit comments

Comments
 (0)