From aa8e77c32f9599e44f551841b41d7905941c756c Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Thu, 6 Nov 2025 08:43:30 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[BOOK-421]=20feat:=20=EB=8F=85=EC=84=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=99=9C=EC=84=B1=ED=99=94=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainFlow/NoteEdit/View/NoteEditView.swift | 28 +++++++ .../View/NoteEditViewController.swift | 23 +++++- .../ViewModel/NoteEditViewModel.swift | 76 +++++++++++++++++-- 3 files changed, 116 insertions(+), 11 deletions(-) diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditView.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditView.swift index ecd503e6..29e61f32 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditView.swift @@ -9,6 +9,9 @@ import UIKit enum NoteEditViewEvent { case emotionStatusTapped case saveButtonTapped + case pageDidChange(String) + case sentenceDidChange(String) + case appreciationDidChange(String) } final class NoteEditView: BaseView { @@ -110,6 +113,26 @@ final class NoteEditView: BaseView { emotionStatusView.isUserInteractionEnabled = true // BKTextFieldView와 BKTextView는 자체적으로 탭을 처리하므로 별도 제스처 불필요 + pageField.textDidChangePublisher + .sink { [weak self] _ in + guard let self = self else { return } + self.eventPublisher.send(.pageDidChange(self.pageField.text)) + } + .store(in: &cancellables) + + sentenceTextView.textDidChangePublisher + .sink { [weak self] _ in + guard let self = self else { return } + self.eventPublisher.send(.sentenceDidChange(self.sentenceTextView.text)) + } + .store(in: &cancellables) + + appreciationTextView.textDidChangePublisher + .sink { [weak self] _ in + guard let self = self else { return } + self.eventPublisher.send(.appreciationDidChange(self.appreciationTextView.text)) + } + .store(in: &cancellables) // 전체 뷰에 탭 제스처 추가 (키보드 dismiss용) let dismissTapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) @@ -156,6 +179,7 @@ final class NoteEditView: BaseView { target: self, selector: #selector(pageFieldDidBeginEditing) ) + } @objc private func sentenceTextViewDidBeginEditing(_ notification: Notification) { @@ -270,6 +294,10 @@ final class NoteEditView: BaseView { emotionLabel.setText(text: text) } + public func setSaveButtonEnabled(_ isEnabled: Bool) { + saveButton.isDisabled = !isEnabled + } + func getCurrentFormData() -> (page: Int?, sentence: String, appreciation: String) { let pageText = pageField.text.trimmingCharacters(in: .whitespacesAndNewlines) let sentenceText = sentenceTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift index f1fcae57..837e5578 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift @@ -42,6 +42,7 @@ final class NoteEditViewController: BaseViewController, ScreenLogg ) navigationItem.leftBarButtonItem = backButton + contentView.setSaveButtonEnabled(false) viewModel.send(.onAppear) } @@ -121,16 +122,31 @@ final class NoteEditViewController: BaseViewController, ScreenLogg } .store(in: &cancellables) + viewModel.statePublisher + .map { $0.isDiff } + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] isDiff in + self?.contentView.setSaveButtonEnabled(isDiff) + } + .store(in: &cancellables) } override func bindAction() { contentView.eventPublisher .sink { [weak self] event in + guard let self = self else { return } switch event { case .emotionStatusTapped: - self?.presentEmotionEdit() + self.presentEmotionEdit() case .saveButtonTapped: - self?.handleSaveButtonTapped() + self.handleSaveButtonTapped() + case .pageDidChange(let text): + self.viewModel.send(.pageDidChange(text)) + case .sentenceDidChange(let text): + self.viewModel.send(.sentenceDidChange(text)) + case .appreciationDidChange(let text): + self.viewModel.send(.appreciationDidChange(text)) } } .store(in: &cancellables) @@ -153,8 +169,7 @@ private extension NoteEditViewController { } func handleSaveButtonTapped() { - let formData = contentView.getCurrentFormData() - viewModel.send(.saveButtonTapped(formData: formData)) + viewModel.send(.saveButtonTapped) } func presentBookMoreMenu() { diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift index 5d69aef7..d862dcc5 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift @@ -14,6 +14,12 @@ final class NoteEditViewModel: BaseViewModel { var shouldPresentEmotionEdit: (emotion: Emotion?, timestamp: Date)? var saveCompleted: Bool = false var deleteCompleted: Bool = false + + var currentFormData: (page: String, sentence: String, appreciation: String) = ("", "", "") + + var initialRecordInfo: RecordInfo? + var initialSelectedEmotion: Emotion? + var isDiff: Bool = false // 변경 내용이 있는지 추적 } enum Action { @@ -23,10 +29,15 @@ final class NoteEditViewModel: BaseViewModel { case errorHandled case presentEmotionEdit case emotionSelected(Emotion) - case saveButtonTapped(formData: (page: Int?, sentence: String, appreciation: String?)) + + case saveButtonTapped case patchRecordSuccessed(RecordInfo) case deleteButtonTapped case deleteRecordSuccessed + + case pageDidChange(String) + case sentenceDidChange(String) + case appreciationDidChange(String) } enum SideEffect { @@ -69,15 +80,27 @@ final class NoteEditViewModel: BaseViewModel { switch action { case .onAppear: newState.isLoading = true + newState.isDiff = false effects.append(.fetchRecordDetail(recordId)) case .fetchRecordDetailSuccessed(let recordInfo): newState.recordInfo = recordInfo + newState.initialRecordInfo = recordInfo + + newState.currentFormData = ( + page: "\(recordInfo.pageNumber)", + sentence: recordInfo.quote, + appreciation: recordInfo.review ?? "" + ) + // 사용자가 이미 감정을 선택했다면 덮어쓰지 않음 if newState.selectedEmotion == nil { - newState.selectedEmotion = recordInfo.emotionTags.first + let initialEmotion = recordInfo.emotionTags.first + newState.selectedEmotion = initialEmotion + newState.initialSelectedEmotion = initialEmotion } newState.isLoading = false + newState.isDiff = false case .errorOccured(let error): newState.error = error @@ -91,19 +114,25 @@ final class NoteEditViewModel: BaseViewModel { case .emotionSelected(let emotion): newState.selectedEmotion = emotion + newState.isDiff = checkForDiff(state: newState) - case .saveButtonTapped(let formData): + case .saveButtonTapped: guard let selectedEmotion = state.selectedEmotion, - let page = formData.page, - !formData.sentence.isEmpty else { + let page = Int(state.currentFormData.page), + !state.currentFormData.sentence.isEmpty else { break } + // 감상평이 비어있으면 nil, 아니면 텍스트 전달 + let appreciation = state.currentFormData.appreciation.isEmpty + ? nil + : state.currentFormData.appreciation + let noteForm = NoteForm( page: page, - sentence: formData.sentence, + sentence: state.currentFormData.sentence, emotion: selectedEmotion, - appreciation: formData.appreciation + appreciation: appreciation ) newState.isLoading = true @@ -111,8 +140,15 @@ final class NoteEditViewModel: BaseViewModel { case .patchRecordSuccessed(let recordInfo): newState.recordInfo = recordInfo + newState.initialRecordInfo = recordInfo + newState.currentFormData = ( + page: "\(recordInfo.pageNumber)", + sentence: recordInfo.quote, + appreciation: recordInfo.review ?? "" + ) newState.isLoading = false newState.saveCompleted = true + newState.isDiff = false case .deleteButtonTapped: newState.isLoading = true @@ -121,6 +157,18 @@ final class NoteEditViewModel: BaseViewModel { case .deleteRecordSuccessed: newState.isLoading = false newState.deleteCompleted = true + + case .pageDidChange(let text): + newState.currentFormData.page = text + newState.isDiff = checkForDiff(state: newState) + + case .sentenceDidChange(let text): + newState.currentFormData.sentence = text + newState.isDiff = checkForDiff(state: newState) + + case .appreciationDidChange(let text): + newState.currentFormData.appreciation = text + newState.isDiff = checkForDiff(state: newState) } return (newState, effects) @@ -159,5 +207,19 @@ final class NoteEditViewModel: BaseViewModel { .sink(receiveValue: send(_:)) .store(in: &cancellables) } + + private func checkForDiff(state: State) -> Bool { + guard let initialInfo = state.initialRecordInfo else { + return false + } + + let pageDiff = state.currentFormData.page != "\(initialInfo.pageNumber)" + let sentenceDiff = state.currentFormData.sentence != initialInfo.quote + let appreciationDiff = state.currentFormData.appreciation != (initialInfo.review ?? "") + + let emotionDiff = state.selectedEmotion != state.initialSelectedEmotion + + return pageDiff || sentenceDiff || appreciationDiff || emotionDiff + } } From 1fb8922334fc6dc1235d491e401d942e1b86de2f Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:25:26 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[BOOK-421]=20fix:=20Emotion=20Edit=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BKDomain/Sources/Entity/Emotion.swift | 2 +- .../NoteEdit/View/EmotionEditView.swift | 35 +++++++++++-------- .../View/EmotionEditViewController.swift | 29 ++++++++++----- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/Projects/BKDomain/Sources/Entity/Emotion.swift b/src/Projects/BKDomain/Sources/Entity/Emotion.swift index ebe70c95..3baadfac 100644 --- a/src/Projects/BKDomain/Sources/Entity/Emotion.swift +++ b/src/Projects/BKDomain/Sources/Entity/Emotion.swift @@ -1,6 +1,6 @@ // Copyright © 2025 Booket. All rights reserved -public enum Emotion: String, CaseIterable, Decodable { +public enum Emotion: String, CaseIterable, Decodable, Equatable { case warmth = "따뜻함" case joy = "즐거움" case sad = "슬픔" diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditView.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditView.swift index fd07a711..27779494 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditView.swift @@ -6,23 +6,21 @@ import Combine import UIKit import SnapKit +enum EmotionEditViewEvent { + case editButtonTapped + case emotionDidChange(Emotion?) +} + final class EmotionEditView: BaseView { private let scrollView = UIScrollView() private let contentView = UIView() private let emotionRegistrationView = EmotionRegistrationView() private let editButton = BKButton(style: .primary, size: .large) - private let editButtonTappedSubject = PassthroughSubject() - var editButtonTappedPublisher: AnyPublisher { - editButtonTappedSubject.eraseToAnyPublisher() - } - - private let getCurrentEmotionSubject = PassthroughSubject() - var getCurrentEmotionPublisher: AnyPublisher { - getCurrentEmotionSubject.eraseToAnyPublisher() - } + let eventPublisher = PassthroughSubject() + private var cancellables = Set() - var selectedEmotion: Emotion? { + private var currentSelectedEmotion: Emotion? { guard let form = emotionRegistrationView.registrationForm(), case .emotion(let emotionForm) = form else { return nil } return emotionForm.emotion @@ -37,6 +35,15 @@ final class EmotionEditView: BaseView { override func configure() { editButton.title = "수정하기" editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside) + + emotionRegistrationView.inputChangedPublisher + .sink { [weak self] _ in + guard let self = self else { return } + + let newEmotion = self.currentSelectedEmotion + self.eventPublisher.send(.emotionDidChange(newEmotion)) + } + .store(in: &cancellables) } override func setupLayout() { @@ -69,12 +76,12 @@ final class EmotionEditView: BaseView { emotionRegistrationView.setSelectedEmotion(emotion) } - func getCurrentSelectedEmotion() { - getCurrentEmotionSubject.send(()) + @objc private func editButtonTapped() { + eventPublisher.send(.editButtonTapped) } - @objc private func editButtonTapped() { - editButtonTappedSubject.send(()) + func setEditButtonEnabled(_ isEnabled: Bool) { + editButton.isDisabled = !isEnabled } } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditViewController.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditViewController.swift index a97093ee..8f603160 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/EmotionEditViewController.swift @@ -16,30 +16,41 @@ final class EmotionEditViewController: BaseViewController { weak var coordinator: NoteEditCoordinator? private var cancellables = Set() - private let currentEmotion: Emotion? + private let initialEmotion: Emotion? + @Published private var selectedEmotion: Emotion? private let completion: (Emotion) -> Void init(currentEmotion: Emotion?, completion: @escaping (Emotion) -> Void) { - self.currentEmotion = currentEmotion + self.initialEmotion = currentEmotion self.completion = completion + self._selectedEmotion = .init(initialValue: currentEmotion) super.init() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - // 현재 선택된 감정이 있으면 초기 설정 - if let emotion = currentEmotion { + if let emotion = initialEmotion { contentView.setSelectedEmotion(emotion) } + contentView.setEditButtonEnabled(false) } override func bindAction() { - contentView.editButtonTappedPublisher - .compactMap { [weak self] in self?.contentView.selectedEmotion } - .sink { [weak self] selectedEmotion in - self?.completion(selectedEmotion) - self?.navigationController?.popViewController(animated: true) + contentView.eventPublisher + .sink { [weak self] event in + guard let self = self else { return } + switch event { + case .emotionDidChange(let newEmotion): + self.selectedEmotion = newEmotion + let isDiff = (newEmotion != self.initialEmotion) + self.contentView.setEditButtonEnabled(isDiff) + case .editButtonTapped: + if let emotion = self.selectedEmotion { + self.completion(emotion) + self.navigationController?.popViewController(animated: true) + } + } } .store(in: &cancellables) } From 5dc1cc09c5c40377b3ce56dfb2e0e7020cef6be8 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:25:48 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[BOOK-421]=20fix:=20=EB=85=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20ViewWillAppear=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Projects/BKDomain/Sources/Entity/Emotion.swift | 2 +- .../MainFlow/NoteEdit/View/NoteEditViewController.swift | 1 - .../MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Projects/BKDomain/Sources/Entity/Emotion.swift b/src/Projects/BKDomain/Sources/Entity/Emotion.swift index 3baadfac..ebe70c95 100644 --- a/src/Projects/BKDomain/Sources/Entity/Emotion.swift +++ b/src/Projects/BKDomain/Sources/Entity/Emotion.swift @@ -1,6 +1,6 @@ // Copyright © 2025 Booket. All rights reserved -public enum Emotion: String, CaseIterable, Decodable, Equatable { +public enum Emotion: String, CaseIterable, Decodable { case warmth = "따뜻함" case joy = "즐거움" case sad = "슬픔" diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift index 837e5578..917424b0 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift @@ -42,7 +42,6 @@ final class NoteEditViewController: BaseViewController, ScreenLogg ) navigationItem.leftBarButtonItem = backButton - contentView.setSaveButtonEnabled(false) viewModel.send(.onAppear) } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift index d862dcc5..31b7db62 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/ViewModel/NoteEditViewModel.swift @@ -79,6 +79,9 @@ final class NoteEditViewModel: BaseViewModel { switch action { case .onAppear: + guard state.initialRecordInfo == nil else { + break + } newState.isLoading = true newState.isDiff = false effects.append(.fetchRecordDetail(recordId))