Skip to content

Commit 7c1303b

Browse files
committed
[BOOK-508] feat: 기록 수정 감정 선택 UI 개선 - #272
1 parent 0712714 commit 7c1303b

14 files changed

Lines changed: 405 additions & 212 deletions

File tree

src/Projects/BKData/Sources/Constant/APIConfig.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@ private final class BKDataBundleToken {}
77
enum APIConfig {
88
private static let bundle = Bundle(for: BKDataBundleToken.self)
99

10-
/// V1 API Base URL (auth, books, users, home)
11-
static let baseURL: String = {
10+
/// API Base URL (xcconfig에서 /api까지만 포함)
11+
private static let baseURL: String = {
1212
guard let value = bundle.object(forInfoDictionaryKey: "BASE_API_URL") as? String else {
1313
fatalError("Can't load environment: BKData.BASE_API_URL")
1414
}
1515
return value
1616
}()
1717

18+
/// V1 API Base URL (auth, books, users, home)
19+
static let baseURLv1: String = {
20+
return baseURL + "/v1"
21+
}()
22+
1823
/// V2 API Base URL (emotions, reading-records)
1924
static let baseURLv2: String = {
20-
// V1 URL에서 v2로 변경
21-
return baseURL.replacingOccurrences(of: "/api/v1", with: "/api/v2")
25+
return baseURL + "/v2"
2226
}()
2327
}

src/Projects/BKData/Sources/DataAssembly.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ public struct DataAssembly: Assembly {
166166
)
167167
}
168168

169+
container.register(type: ExternalLinkRepository.self) { _ in
170+
return DefaultExternalLinkRepository()
171+
}
172+
169173
container.register(
170174
type: EmotionRepository.self,
171175
scope: .singleton

src/Projects/BKDomain/Sources/Entity/PrimaryEmotion.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,4 @@ public enum PrimaryEmotion: String, CaseIterable, Codable {
3131
case .other: return "네 가지 감정으로 표현하기 어려울 때"
3232
}
3333
}
34-
35-
/// Emotion으로 변환
36-
public func toEmotion() -> Emotion {
37-
switch self {
38-
case .warmth: return .warmth
39-
case .joy: return .joy
40-
case .sadness: return .sad
41-
case .insight: return .insight
42-
case .other: return .other
43-
}
44-
}
45-
46-
/// Emotion에서 변환
47-
public static func from(emotion: Emotion) -> PrimaryEmotion {
48-
switch emotion {
49-
case .warmth: return .warmth
50-
case .joy: return .joy
51-
case .sad: return .sadness
52-
case .insight: return .insight
53-
case .other: return .other
54-
}
55-
}
5634
}

src/Projects/BKDomain/Sources/Entity/RecordInfo.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,4 @@ public struct RecordInfo: Decodable, Equatable {
4646
self.bookCoverImageUrl = bookCoverImageUrl
4747
self.author = author
4848
}
49-
50-
/// 이전 API와의 호환성을 위한 computed property
51-
public var emotionTags: [Emotion] {
52-
[primaryEmotion.toEmotion()]
53-
}
5449
}

src/Projects/BKDomain/Sources/VO/RecordDetails/RecordVO.swift

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,4 @@ public struct RecordVO {
2222
self.primaryEmotion = primaryEmotion
2323
self.detailEmotionIds = detailEmotionIds
2424
}
25-
26-
/// 이전 API와의 호환성을 위한 생성자
27-
public init(
28-
pageNumber: Int?,
29-
quote: String,
30-
review: String?,
31-
emotionTags: [String]
32-
) {
33-
self.pageNumber = pageNumber
34-
self.quote = quote
35-
self.memo = review
36-
self.primaryEmotion = .other
37-
self.detailEmotionIds = emotionTags
38-
}
3925
}

src/Projects/BKPresentation/Sources/MainFlow/Note/ViewModel/NoteForm.swift

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,6 @@ struct NoteForm: Equatable {
88
let memo: String?
99
let primaryEmotion: PrimaryEmotion
1010
let detailEmotions: [DetailEmotion]
11-
12-
/// 이전 API 호환성 생성자
13-
init(
14-
page: Int?,
15-
sentence: String,
16-
emotion: Emotion,
17-
appreciation: String?
18-
) {
19-
self.page = page
20-
self.sentence = sentence
21-
self.memo = appreciation
22-
self.primaryEmotion = PrimaryEmotion.from(emotion: emotion)
23-
self.detailEmotions = []
24-
}
25-
26-
init(
27-
page: Int?,
28-
sentence: String,
29-
memo: String?,
30-
primaryEmotion: PrimaryEmotion,
31-
detailEmotions: [DetailEmotion]
32-
) {
33-
self.page = page
34-
self.sentence = sentence
35-
self.memo = memo
36-
self.primaryEmotion = primaryEmotion
37-
self.detailEmotions = detailEmotions
38-
}
3911
}
4012

4113
extension NoteForm {

src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/Coordinator/NoteEditCoordinator.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ extension NoteEditCoordinator: AuthenticationRequiredNotifying, ErrorHandleable
3737
}
3838

3939
extension NoteEditCoordinator {
40-
func didTapEmotionEdit(currentEmotion: Emotion?, completion: @escaping (Emotion) -> Void) {
40+
func didTapEmotionEdit(
41+
currentEmotion: PrimaryEmotion?,
42+
currentDetailEmotions: [DetailEmotion],
43+
completion: @escaping (PrimaryEmotion, [DetailEmotion]) -> Void
44+
) {
4145
let emotionEditViewController = EmotionEditViewController(
4246
currentEmotion: currentEmotion,
47+
currentDetailEmotions: currentDetailEmotions,
4348
completion: completion
4449
)
4550
emotionEditViewController.coordinator = self

src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditView.swift

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import SnapKit
88

99
enum EmotionEditViewEvent {
1010
case editButtonTapped
11-
case emotionDidChange(Emotion?)
11+
case emotionDidChange(PrimaryEmotion?)
12+
case emotionSelected(PrimaryEmotion) // 감정 탭 시 (바텀시트 표시용)
1213
}
1314

1415
final class EmotionEditView: BaseView {
@@ -20,10 +21,10 @@ final class EmotionEditView: BaseView {
2021
let eventPublisher = PassthroughSubject<EmotionEditViewEvent, Never>()
2122
private var cancellables = Set<AnyCancellable>()
2223

23-
private var currentSelectedEmotion: Emotion? {
24+
private var currentSelectedPrimaryEmotion: PrimaryEmotion? {
2425
guard let form = emotionRegistrationView.registrationForm(),
2526
case .emotion(let emotionForm) = form else { return nil }
26-
return emotionForm.emotion
27+
return emotionForm.primaryEmotion
2728
}
2829

2930
override func setupView() {
@@ -35,12 +36,17 @@ final class EmotionEditView: BaseView {
3536
override func configure() {
3637
editButton.title = "수정하기"
3738
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
38-
39+
40+
// 감정 탭 이벤트 전달 (바텀시트 표시용)
41+
emotionRegistrationView.onEmotionSelected = { [weak self] emotion in
42+
self?.eventPublisher.send(.emotionSelected(emotion))
43+
}
44+
3945
emotionRegistrationView.inputChangedPublisher
4046
.sink { [weak self] _ in
4147
guard let self = self else { return }
42-
43-
let newEmotion = self.currentSelectedEmotion
48+
49+
let newEmotion = self.currentSelectedPrimaryEmotion
4450
self.eventPublisher.send(.emotionDidChange(newEmotion))
4551
}
4652
.store(in: &cancellables)
@@ -72,17 +78,31 @@ final class EmotionEditView: BaseView {
7278
}
7379
}
7480

75-
func setSelectedEmotion(_ emotion: Emotion) {
76-
emotionRegistrationView.setSelectedEmotion(emotion)
81+
func setSelectedPrimaryEmotion(_ emotion: PrimaryEmotion) {
82+
emotionRegistrationView.setSelectedPrimaryEmotion(emotion)
7783
}
78-
84+
7985
@objc private func editButtonTapped() {
8086
eventPublisher.send(.editButtonTapped)
8187
}
82-
88+
8389
func setEditButtonEnabled(_ isEnabled: Bool) {
8490
editButton.isDisabled = !isEnabled
8591
}
92+
93+
// MARK: - Detail Emotions
94+
95+
func setDetailEmotions(_ detailEmotions: [DetailEmotion]) {
96+
emotionRegistrationView.setDetailEmotions(detailEmotions)
97+
}
98+
99+
func getSelectedDetailEmotions() -> [DetailEmotion] {
100+
return emotionRegistrationView.getSelectedDetailEmotions()
101+
}
102+
103+
func setLoadingEmotions(_ isLoading: Bool) {
104+
emotionRegistrationView.setLoadingEmotions(isLoading)
105+
}
86106
}
87107

88108
private extension EmotionEditView {

src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditViewController.swift

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,50 +8,122 @@ import UIKit
88

99
final class EmotionEditViewController: BaseViewController<EmotionEditView> {
1010
override var bkNavigationTitle: String { "" }
11-
11+
1212
override var bkNavigationBarStyle: UINavigationController.BKNavigationBarStyle {
1313
.standard(viewController: self)
1414
}
15-
15+
1616
weak var coordinator: NoteEditCoordinator?
1717
private var cancellables = Set<AnyCancellable>()
18-
19-
private let initialEmotion: Emotion?
20-
@Published private var selectedEmotion: Emotion?
21-
private let completion: (Emotion) -> Void
22-
23-
init(currentEmotion: Emotion?, completion: @escaping (Emotion) -> Void) {
24-
self.initialEmotion = currentEmotion
18+
19+
private let initialPrimaryEmotion: PrimaryEmotion?
20+
private let initialDetailEmotions: [DetailEmotion]
21+
@Published private var selectedPrimaryEmotion: PrimaryEmotion?
22+
private let completion: (PrimaryEmotion, [DetailEmotion]) -> Void
23+
24+
@Autowired private var fetchDetailEmotionsUseCase: FetchDetailEmotionsUseCase
25+
26+
// 바텀시트 표시 대기용
27+
private var pendingEmotionForSheet: PrimaryEmotion?
28+
private var isLoadingEmotions: Bool = false
29+
30+
init(
31+
currentEmotion: PrimaryEmotion?,
32+
currentDetailEmotions: [DetailEmotion] = [],
33+
completion: @escaping (PrimaryEmotion, [DetailEmotion]) -> Void
34+
) {
35+
self.initialPrimaryEmotion = currentEmotion
36+
self.initialDetailEmotions = currentDetailEmotions
2537
self.completion = completion
26-
self._selectedEmotion = .init(initialValue: currentEmotion)
38+
self._selectedPrimaryEmotion = .init(initialValue: currentEmotion)
2739
super.init()
2840
}
29-
41+
3042
override func viewWillAppear(_ animated: Bool) {
3143
super.viewWillAppear(animated)
32-
33-
if let emotion = initialEmotion {
34-
contentView.setSelectedEmotion(emotion)
44+
45+
if let emotion = initialPrimaryEmotion {
46+
contentView.setSelectedPrimaryEmotion(emotion)
47+
contentView.setDetailEmotions(initialDetailEmotions)
3548
}
3649
contentView.setEditButtonEnabled(false)
3750
}
38-
51+
3952
override func bindAction() {
4053
contentView.eventPublisher
4154
.sink { [weak self] event in
4255
guard let self = self else { return }
4356
switch event {
4457
case .emotionDidChange(let newEmotion):
45-
self.selectedEmotion = newEmotion
46-
let isDiff = (newEmotion != self.initialEmotion)
47-
self.contentView.setEditButtonEnabled(isDiff)
58+
self.selectedPrimaryEmotion = newEmotion
59+
self.updateEditButtonState()
60+
61+
case .emotionSelected(let emotion):
62+
// other가 아닌 경우 세부감정 바텀시트 표시
63+
if emotion != .other {
64+
self.fetchAndPresentDetailEmotionSheet(for: emotion)
65+
}
66+
4867
case .editButtonTapped:
49-
if let emotion = self.selectedEmotion {
50-
self.completion(emotion)
68+
if let emotion = self.selectedPrimaryEmotion {
69+
let detailEmotions = self.contentView.getSelectedDetailEmotions()
70+
self.completion(emotion, detailEmotions)
5171
self.navigationController?.popViewController(animated: true)
5272
}
5373
}
5474
}
5575
.store(in: &cancellables)
5676
}
77+
78+
private func updateEditButtonState() {
79+
let emotionChanged = selectedPrimaryEmotion != initialPrimaryEmotion
80+
let detailEmotionsChanged = contentView.getSelectedDetailEmotions() != initialDetailEmotions
81+
let isDiff = emotionChanged || detailEmotionsChanged
82+
contentView.setEditButtonEnabled(isDiff)
83+
}
84+
85+
private func fetchAndPresentDetailEmotionSheet(for emotion: PrimaryEmotion) {
86+
guard !isLoadingEmotions else { return }
87+
88+
isLoadingEmotions = true
89+
contentView.setLoadingEmotions(true)
90+
91+
fetchDetailEmotionsUseCase.execute(for: emotion)
92+
.receive(on: DispatchQueue.main)
93+
.sink(
94+
receiveCompletion: { [weak self] completion in
95+
self?.isLoadingEmotions = false
96+
self?.contentView.setLoadingEmotions(false)
97+
if case .failure = completion {
98+
// 에러 처리 - 필요시 coordinator에서 처리
99+
}
100+
},
101+
receiveValue: { [weak self] detailEmotions in
102+
self?.presentDetailEmotionSheet(for: emotion, detailEmotions: detailEmotions)
103+
}
104+
)
105+
.store(in: &cancellables)
106+
}
107+
108+
private func presentDetailEmotionSheet(for primaryEmotion: PrimaryEmotion, detailEmotions: [DetailEmotion]) {
109+
let currentDetailEmotions = contentView.getSelectedDetailEmotions()
110+
let sheet = BKBottomSheetViewController.makeDetailEmotionSheet(
111+
primaryEmotion: primaryEmotion,
112+
detailEmotions: detailEmotions,
113+
initialSelectedDetailEmotions: currentDetailEmotions,
114+
skipAction: { [weak self] in
115+
// 건너뛰기: 세부감정 초기화
116+
self?.contentView.setDetailEmotions([])
117+
self?.updateEditButtonState()
118+
self?.dismiss(animated: true)
119+
},
120+
confirmAction: { [weak self] selectedDetailEmotions in
121+
self?.contentView.setDetailEmotions(selectedDetailEmotions)
122+
self?.updateEditButtonState()
123+
self?.dismiss(animated: true)
124+
}
125+
)
126+
127+
sheet.show(from: self, animated: true)
128+
}
57129
}

0 commit comments

Comments
 (0)