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) } 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..917424b0 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteEdit/View/NoteEditViewController.swift @@ -121,16 +121,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 +168,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..31b7db62 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 { @@ -68,16 +79,31 @@ final class NoteEditViewModel: BaseViewModel { switch action { case .onAppear: + guard state.initialRecordInfo == nil else { + break + } 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 +117,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 +143,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 +160,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 +210,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 + } }