Skip to content

Commit cdeb65e

Browse files
committed
Feat: 감정 구슬 화면 UI 구현 (#T3-69)
- 감정 구슬 enum 타입 정의 (EmotionType) - 감정 구슬 CollectionViewCell UI 구현 (EmotionOrbCollectionViewCell) - 감정 구슬 등록 화면 UI 구현 (EmotionRegisterView)
1 parent ed9ca81 commit cdeb65e

4 files changed

Lines changed: 282 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// EmotionType.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/28/25.
6+
//
7+
8+
import UIKit
9+
10+
enum EmotionType: CaseIterable {
11+
case calm
12+
case lethargy
13+
case vitality
14+
case anxiety
15+
case satisfied
16+
case tired
17+
18+
var title: String {
19+
switch self {
20+
case .calm: "평온함"
21+
case .lethargy: "무기력함"
22+
case .vitality: "활기참"
23+
case .anxiety: "불안함"
24+
case .satisfied: "만족함"
25+
case .tired: "피로함"
26+
}
27+
}
28+
29+
var image: UIImage? {
30+
switch self {
31+
case .calm: BitnagilGraphic.calmOrb
32+
case .lethargy: BitnagilGraphic.lethargyOrb
33+
case .vitality: BitnagilGraphic.vitalityOrb
34+
case .anxiety: BitnagilGraphic.anxietyOrb
35+
case .satisfied: BitnagilGraphic.satisfiedOrb
36+
case .tired: BitnagilGraphic.tiredOrb
37+
}
38+
}
39+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// EmotionOrbCollectionViewCell.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/28/25.
6+
//
7+
8+
import SnapKit
9+
import UIKit
10+
11+
final class EmotionOrbCollectionViewCell: UICollectionViewCell {
12+
13+
private enum Layout {
14+
static let emotionOrbImageSize: CGFloat = 96
15+
static let emotionLabelTopSpacing: CGFloat = 6
16+
static let emotionLabelHeight: CGFloat = 24
17+
}
18+
19+
private let emotionOrbImage = UIImageView()
20+
private let emotionLabel = UILabel()
21+
22+
override init(frame: CGRect) {
23+
super.init(frame: frame)
24+
configureAttribute()
25+
configureLayout()
26+
}
27+
28+
required init?(coder: NSCoder) {
29+
fatalError("init(coder:) has not been implemented")
30+
}
31+
32+
private func configureAttribute() {
33+
emotionLabel.text = ""
34+
emotionLabel.textAlignment = .center
35+
emotionLabel.font = BitnagilFont(style: .body1, weight: .regular).font
36+
emotionLabel.textColor = BitnagilColor.gray20
37+
}
38+
39+
private func configureLayout() {
40+
contentView.addSubview(emotionOrbImage)
41+
contentView.addSubview(emotionLabel)
42+
43+
emotionOrbImage.snp.makeConstraints { make in
44+
make.top.equalToSuperview()
45+
make.horizontalEdges.equalToSuperview()
46+
make.size.equalTo(Layout.emotionOrbImageSize)
47+
}
48+
49+
emotionLabel.snp.makeConstraints { make in
50+
make.top.equalTo(emotionOrbImage.snp.bottom).offset(Layout.emotionLabelTopSpacing)
51+
make.horizontalEdges.equalToSuperview()
52+
make.height.equalTo(Layout.emotionLabelHeight)
53+
}
54+
}
55+
56+
func configureCell(emotion: EmotionType) {
57+
emotionOrbImage.image = emotion.image
58+
emotionLabel.text = emotion.title
59+
}
60+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// EmotionRegisterView.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/28/25.
6+
//
7+
8+
import Combine
9+
import Shared
10+
import SnapKit
11+
import UIKit
12+
13+
final class EmotionRegisterView: BaseViewController<EmotionRegisterViewModel> {
14+
15+
private enum Layout {
16+
static let mainLabelTopSpacing: CGFloat = 32
17+
static let mainLabelHeight: CGFloat = 30
18+
static let subLabelTopSpacing: CGFloat = 6
19+
static let subLabelHeight: CGFloat = 28
20+
static let emotionOrbCellWidth: CGFloat = 96
21+
static let emotionOrbCellHeight: CGFloat = 126
22+
static let emotionOrbCollectionViewItemSpacing: CGFloat = 13
23+
static let emotionOrbCollectionViewLineSpacing: CGFloat = 28
24+
static let emotionOrbCollectionViewTopSpacing: CGFloat = 56
25+
static let emotionOrbCollectionViewHorizontalMargin: CGFloat = 30
26+
static let emotionOrbCollectionViewHeight: CGFloat = 280
27+
}
28+
29+
private let mainLabel = UILabel()
30+
private let subLabel = UILabel()
31+
private var emotionOrbCollectionView: UICollectionView = {
32+
let layout = UICollectionViewFlowLayout()
33+
layout.itemSize = CGSize(width: Layout.emotionOrbCellWidth, height: Layout.emotionOrbCellHeight)
34+
layout.minimumInteritemSpacing = Layout.emotionOrbCollectionViewItemSpacing
35+
layout.minimumLineSpacing = Layout.emotionOrbCollectionViewLineSpacing
36+
layout.sectionInset = .zero
37+
38+
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
39+
return collectionView
40+
}()
41+
42+
private var cancellables: Set<AnyCancellable>
43+
44+
override init(viewModel: EmotionRegisterViewModel) {
45+
cancellables = []
46+
super.init(viewModel: viewModel)
47+
}
48+
49+
required init?(coder: NSCoder) {
50+
fatalError("init(coder:) has not been implemented")
51+
}
52+
53+
override func viewDidLoad() {
54+
super.viewDidLoad()
55+
}
56+
57+
override func viewWillAppear(_ animated: Bool) {
58+
super.viewWillAppear(animated)
59+
configureNavigationBar(navigationStyle: .withBackButton(title: ""))
60+
}
61+
62+
override func configureAttribute() {
63+
mainLabel.text = "오늘의 감정구슬을 골라보세요"
64+
mainLabel.textAlignment = .center
65+
mainLabel.font = BitnagilFont(style: .title2, weight: .bold).font
66+
mainLabel.textColor = BitnagilColor.navy500
67+
68+
subLabel.text = "감정구슬을 등록하면 루틴을 추천받아요!"
69+
subLabel.textAlignment = .center
70+
subLabel.font = BitnagilFont(style: .subtitle1, weight: .regular).font
71+
subLabel.textColor = BitnagilColor.navy300
72+
73+
emotionOrbCollectionView.backgroundColor = .clear
74+
75+
emotionOrbCollectionView.delegate = self
76+
emotionOrbCollectionView.dataSource = self
77+
emotionOrbCollectionView.register(EmotionOrbCollectionViewCell.self, forCellWithReuseIdentifier: EmotionOrbCollectionViewCell.className)
78+
}
79+
80+
override func configureLayout() {
81+
let safeArea = view.safeAreaLayoutGuide
82+
view.backgroundColor = .systemBackground
83+
84+
view.addSubview(mainLabel)
85+
view.addSubview(subLabel)
86+
view.addSubview(emotionOrbCollectionView)
87+
88+
mainLabel.snp.makeConstraints { make in
89+
make.top.equalTo(safeArea).offset(Layout.mainLabelTopSpacing)
90+
make.horizontalEdges.equalTo(safeArea)
91+
make.height.equalTo(Layout.mainLabelHeight)
92+
}
93+
94+
subLabel.snp.makeConstraints { make in
95+
make.top.equalTo(mainLabel.snp.bottom).offset(Layout.subLabelTopSpacing)
96+
make.horizontalEdges.equalTo(safeArea)
97+
make.height.equalTo(Layout.subLabelHeight)
98+
}
99+
100+
emotionOrbCollectionView.snp.makeConstraints { make in
101+
make.top.equalTo(subLabel.snp.bottom).offset(Layout.emotionOrbCollectionViewTopSpacing)
102+
make.leading.equalTo(safeArea).offset(Layout.emotionOrbCollectionViewHorizontalMargin)
103+
make.trailing.equalTo(safeArea).inset(Layout.emotionOrbCollectionViewHorizontalMargin)
104+
make.height.equalTo(Layout.emotionOrbCollectionViewHeight)
105+
}
106+
}
107+
108+
override func bind() {
109+
viewModel.output.registerEmotionResultPublisher
110+
.receive(on: DispatchQueue.main)
111+
.sink { [weak self] registerEmotionResult in
112+
if registerEmotionResult {
113+
// TODO: 추천 루틴 화면 보여주기
114+
BitnagilLogger.log(logType: .error, message: "감정 등록 성공")
115+
} else {
116+
BitnagilLogger.log(logType: .error, message: "감정 등록 실패")
117+
}
118+
}
119+
.store(in: &cancellables)
120+
}
121+
}
122+
123+
// MARK: UICollectionViewDelegate
124+
extension EmotionRegisterView: UICollectionViewDelegate {
125+
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
126+
let selectedEmotionType = EmotionType.allCases[indexPath.item]
127+
viewModel.action(input: .selectEmotion(emotion: selectedEmotionType))
128+
}
129+
}
130+
131+
// MARK: UICollectionViewDataSource
132+
extension EmotionRegisterView: UICollectionViewDataSource {
133+
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
134+
return EmotionType.allCases.count
135+
}
136+
137+
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
138+
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: EmotionOrbCollectionViewCell.className, for: indexPath) as? EmotionOrbCollectionViewCell
139+
else { return UICollectionViewCell() }
140+
141+
let emotion = EmotionType.allCases[indexPath.item]
142+
cell.configureCell(emotion: emotion)
143+
return cell
144+
}
145+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// EmotionRegisterViewModel.swift
3+
// Presentation
4+
//
5+
// Created by 최정인 on 7/28/25.
6+
//
7+
8+
import Combine
9+
10+
final class EmotionRegisterViewModel: ViewModel {
11+
enum Input {
12+
case selectEmotion(emotion: EmotionType)
13+
}
14+
15+
struct Output {
16+
let registerEmotionResultPublisher: AnyPublisher<Bool, Never>
17+
}
18+
19+
private(set) var output: Output
20+
private let registerEmotionResultSubject = PassthroughSubject<Bool, Never>()
21+
init() {
22+
output = Output(
23+
registerEmotionResultPublisher: registerEmotionResultSubject.eraseToAnyPublisher()
24+
)
25+
}
26+
27+
func action(input: Input) {
28+
switch input {
29+
case .selectEmotion(let emotion):
30+
registerEmotion(emotion: emotion)
31+
}
32+
}
33+
34+
private func registerEmotion(emotion: EmotionType) {
35+
// TODO: 서버 통신 로직
36+
registerEmotionResultSubject.send(true)
37+
}
38+
}

0 commit comments

Comments
 (0)