Skip to content

Commit ffb6323

Browse files
authored
Feat: 온보딩 UI 구현 (#T3-68)
* Feat: 온보딩 선택지 버튼 Components UI 구현 (#T3-68) * Feat: GradientProgressBar UI 구현 - Gradient 색상 Asset 추가 - GradientProgressBar를 포함한 navigationItem 구현 * Refactor: 온보딩 선택지 UI 수정 (#T3-68) - OnboardingChoiceProtocol 구현 - 온보딩 선택 화면에서 해당 프로토콜을 채택한 타입이라면 OnboardingChoiceButton을 그릴 수 있도록 수정 * Feat: 온보딩에서 사용될 enum 타입 정의 (#T3-68) - OnboardingType (온보딩 유형 값) - OnboardingChoiceType (온보딩 선택지 값) * Feat: OnboardingView 구현 (#T3-68) - 온보딩 선택 화면에서 재사용될 OnboardingView 구현 * Feat: OnboardingResultView 구현 (#T3-68) - 온보딩 결과 화면 UI 구현 - 특정 텍스트만 semiBold로 보여지기 위해 NSAttributedString extension 구현 * Feat: 온보딩 추천 루틴 UI 구현 (#T3-68) * Feat: OnboardingViewModel 구현 (#T3-68) * Feat: OnboardingViewModel 의존성 조립 및 이용 약관 화면에서 온보딩 화면으로 넘어가는 로직 구현 (#T3-68)
1 parent ce78550 commit ffb6323

19 files changed

Lines changed: 1369 additions & 3 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "srgb",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0xFF",
9+
"green" : "0xCF",
10+
"red" : "0xA9"
11+
}
12+
},
13+
"idiom" : "universal"
14+
}
15+
],
16+
"info" : {
17+
"author" : "xcode",
18+
"version" : 1
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "srgb",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0xB3",
9+
"green" : "0xCD",
10+
"red" : "0xFF"
11+
}
12+
},
13+
"idiom" : "universal"
14+
}
15+
],
16+
"info" : {
17+
"author" : "xcode",
18+
"version" : 1
19+
}
20+
}

Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ enum BitnagilColor {
1414

1515
static let kakao = UIColor(named: "Kakao", in: bundle, compatibleWith: nil)
1616

17+
// MARK: - Gradient
18+
static let gradientLeft = UIColor(named: "GradientLeft", in: bundle, compatibleWith: nil)
19+
static let gradientRight = UIColor(named: "GradientRight", in: bundle, compatibleWith: nil)
20+
1721
// MARK: - Emotion Colors
1822
static let happy = UIColor(named: "EmotionHappy", in: bundle, compatibleWith: nil)
1923
static let lethargy = UIColor(named: "EmotionLethargy", in: bundle, compatibleWith: nil)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// NSAttributedString+.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/11/25.
6+
//
7+
8+
import Foundation
9+
10+
extension NSAttributedString {
11+
static func highlighted(text: String, highlightText: String) -> NSAttributedString {
12+
let attributedString = NSMutableAttributedString(string: text)
13+
14+
attributedString.addAttribute(
15+
.font,
16+
value: BitnagilFont(style: .body2, weight: .regular).font,
17+
range: NSRange(location: 0, length: text.count)
18+
)
19+
20+
if let range = text.range(of: highlightText) {
21+
let nsRange = NSRange(range, in: text)
22+
attributedString.addAttribute(
23+
.font,
24+
value: BitnagilFont(style: .body2, weight: .semiBold).font,
25+
range: nsRange
26+
)
27+
}
28+
return attributedString
29+
}
30+
}

Projects/Presentation/Sources/Common/Extensions/UIViewController+.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,25 @@ extension UIViewController {
1313
switch navigationStyle {
1414
case .hidden:
1515
navigationController?.setNavigationBarHidden(true, animated: false)
16+
1617
case .withBackButton(let title):
1718
navigationController?.setNavigationBarHidden(false, animated: false)
1819
self.title = title
1920
configureDefaultBackButton()
21+
22+
case .withPrograssBar(let step, let stepCount):
23+
navigationController?.setNavigationBarHidden(false, animated: false)
24+
configureDefaultBackButton()
25+
configureProgressNavigationBar(step: step, stepCount: stepCount)
26+
27+
case .withPrograssBarWithCustomBackButton(let step, let stepCount):
28+
navigationController?.setNavigationBarHidden(false, animated: false)
29+
configureCustomBackButton()
30+
configureProgressNavigationBar(step: step, stepCount: stepCount)
2031
}
2132
}
2233

23-
func configureDefaultBackButton() {
34+
private func configureDefaultBackButton() {
2435
let backButton = UIBarButtonItem(
2536
image: UIImage(systemName: "chevron.left"),
2637
style: .plain,
@@ -30,12 +41,43 @@ extension UIViewController {
3041
navigationItem.leftBarButtonItem = backButton
3142
}
3243

44+
private func configureCustomBackButton() {
45+
let backButton = UIBarButtonItem(
46+
image: UIImage(systemName: "chevron.left"),
47+
style: .plain,
48+
target: self,
49+
action: #selector(popTwoViewControllers))
50+
backButton.tintColor = .black
51+
navigationItem.leftBarButtonItem = backButton
52+
}
53+
54+
private func configureProgressNavigationBar(step: Int, stepCount: Int) {
55+
self.title = ""
56+
let progressView = ProgressBarView(step: step, stepCount: stepCount)
57+
navigationItem.titleView = progressView
58+
}
59+
3360
@objc private func popViewController() {
3461
navigationController?.popViewController(animated: true)
3562
}
63+
64+
@objc private func popTwoViewControllers() {
65+
guard let navigationController = navigationController else { return }
66+
let viewControllers = navigationController.viewControllers
67+
68+
guard viewControllers.count >= 3 else {
69+
navigationController.popViewController(animated: true)
70+
return
71+
}
72+
73+
let targetViewController = viewControllers[viewControllers.count - 3]
74+
navigationController.popToViewController(targetViewController, animated: true)
75+
}
3676
}
3777

3878
enum NavigationBarStyle {
3979
case hidden
4080
case withBackButton(title: String)
81+
case withPrograssBar(step: Int, stepCount: Int)
82+
case withPrograssBarWithCustomBackButton(step: Int, stepCount: Int)
4183
}

Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public struct PresentationDependencyAssembler: DependencyAssemblerProtocol {
2323
return HomeViewModel()
2424
}
2525

26+
DIContainer.shared.register(type: OnboardingViewModel.self) { _ in
27+
return OnboardingViewModel()
28+
}
29+
2630
DIContainer.shared.register(type: LoginViewModel.self) { container in
2731
guard let loginUseCase = container.resolve(type: LoginUseCaseProtocol.self)
2832
else { return }

Projects/Presentation/Sources/Login/View/TermsAgreementView.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,15 @@ public final class TermsAgreementView: BaseViewController<LoginViewModel> {
127127

128128
viewModel.output.agreementResultPublisher
129129
.receive(on: DispatchQueue.main)
130-
.sink { agreementResult in
130+
.sink { [weak self] agreementResult in
131+
guard let self else { return }
131132
if agreementResult {
132-
// TODO: 약관 동의 성공 시 온보딩 화면으로 이동해야 합니다.
133133
BitnagilLogger.log(logType: .debug, message: "약관 동의 성공")
134+
guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) else {
135+
fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.")
136+
}
137+
let onboardingView = OnboardingView(viewModel: onboardingViewModel, onboarding: .time)
138+
self.navigationController?.pushViewController(onboardingView, animated: true)
134139
} else {
135140
// TODO: 약관 동의 실패 시, 에러 처리를 해야 합니다.
136141
BitnagilLogger.log(logType: .error, message: "약관 동의 실패")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// OnboardingChoiceProtocol.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/11/25.
6+
//
7+
8+
protocol OnboardingChoiceProtocol {
9+
var mainTitle: String { get }
10+
var subTitle: String? { get }
11+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//
2+
// OnboardingChoiceType.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/9/25.
6+
//
7+
8+
enum OnboardingChoiceType: CaseIterable, OnboardingChoiceProtocol {
9+
case morningTime
10+
case eveningTime
11+
case allTime
12+
13+
case never
14+
case rarely
15+
case sometimes
16+
case often
17+
18+
case stability
19+
case connection
20+
case growth
21+
case vitality
22+
23+
case once
24+
case twoToThree
25+
case fourOrMore
26+
case notSure
27+
28+
var onboardingType: OnboardingType {
29+
switch self {
30+
case .morningTime: .time
31+
case .eveningTime: .time
32+
case .allTime: .time
33+
34+
case .never: .frequency
35+
case .rarely: .frequency
36+
case .sometimes: .frequency
37+
case .often: .frequency
38+
39+
case .stability: .feeling
40+
case .connection: .feeling
41+
case .growth: .feeling
42+
case .vitality: .feeling
43+
44+
case .once: .outdoorGoal
45+
case .twoToThree: .outdoorGoal
46+
case .fourOrMore: .outdoorGoal
47+
case .notSure: .outdoorGoal
48+
}
49+
}
50+
51+
var mainTitle: String {
52+
switch self {
53+
case .morningTime: "아침을 잘 시작하고 싶어요."
54+
case .eveningTime: "저녁을 편안하게 마무리하고 싶어요."
55+
case .allTime: "언제든 상관 없어요."
56+
57+
case .never: "밖에 나가지 않고 집에서만 지냈어요."
58+
case .rarely: "잠깐 외출했어요."
59+
case .sometimes: "가끔 나가요."
60+
case .often: "자주 외출해요."
61+
62+
case .stability: "안정감"
63+
case .connection: "연결감"
64+
case .growth: "성장감"
65+
case .vitality: "생동감"
66+
67+
case .once: "시작이 더 중요해요."
68+
case .twoToThree: "너무 무리하지 않아도 괜찮아요."
69+
case .fourOrMore: "이 정도면 충분히 활력 있는 한 주가 될거에요."
70+
case .notSure: "목표 선택을 도와드릴게요!"
71+
}
72+
}
73+
74+
var subTitle: String? {
75+
switch self {
76+
case .stability:
77+
return "하루를 편안하게 보내고 싶어요."
78+
case .connection:
79+
return "누군가와 함께 있다는 느낌이 필요해요."
80+
case .growth:
81+
return "작은 변화라도 시작하고 싶어요."
82+
case .vitality:
83+
return "무기력을 이겨내고 활력을 찾고싶어요."
84+
85+
case .once:
86+
return "일주일에 1회"
87+
case .twoToThree:
88+
return "일주일에 2~3회"
89+
case .fourOrMore:
90+
return "일주일에 4회 이상"
91+
case .notSure:
92+
return "아직 잘 모르겠어요"
93+
94+
default:
95+
return nil
96+
}
97+
}
98+
99+
var resultTitle: String? {
100+
switch self {
101+
case .morningTime:
102+
return "아침루틴"
103+
case .eveningTime:
104+
return "저녁루틴"
105+
case .allTime:
106+
return "전체루틴"
107+
108+
case .stability:
109+
return "안정감"
110+
case .connection:
111+
return "연결감"
112+
case .growth:
113+
return "성장감"
114+
case .vitality:
115+
return "생동감"
116+
117+
case .once:
118+
return "주 1회 외출"
119+
case .twoToThree:
120+
return "주 3회 외출"
121+
case .fourOrMore:
122+
return "주 4회 이상 외출"
123+
case .notSure:
124+
return "최소한의 외출"
125+
126+
default:
127+
return nil
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)