From 86ffb8827d36cf64a4159f8395e07baf172b6917 Mon Sep 17 00:00:00 2001 From: gom1n Date: Wed, 31 May 2023 21:22:44 +0900 Subject: [PATCH] =?UTF-8?q?[refactor]=20#331=20=EB=91=90=EB=B2=88=EC=A7=B8?= =?UTF-8?q?=20=EA=B0=90=EC=A0=95=EC=9D=84=20=EB=82=A8=EA=B8=B0=EB=8A=94=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- POME.xcodeproj/project.pbxproj | 4 + .../Record/RecordEmotionViewController.swift | 149 ++++++++++-------- .../Record/RecordViewController.swift | 35 ++-- .../NoSecondEmotionRecordViewModel.swift | 100 ++++++++++++ .../ViewModel/Record/RecordTabViewModel.swift | 10 +- 5 files changed, 210 insertions(+), 88 deletions(-) create mode 100644 POME/Presentation/ViewModel/Record/NoSecondEmotionRecordViewModel.swift diff --git a/POME.xcodeproj/project.pbxproj b/POME.xcodeproj/project.pbxproj index 0da4159..838a3bb 100644 --- a/POME.xcodeproj/project.pbxproj +++ b/POME.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 2375E48E2A20409300843056 /* GetMarshmallowsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2375E48D2A20409200843056 /* GetMarshmallowsUseCase.swift */; }; 2375E4902A2040FC00843056 /* MarshmallowRepositoryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2375E48F2A2040FC00843056 /* MarshmallowRepositoryInterface.swift */; }; 2375E4922A20415B00843056 /* MarshmallowRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2375E4912A20415B00843056 /* MarshmallowRepository.swift */; }; + 237CDEE12A27604100B3010F /* NoSecondEmotionRecordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237CDEE02A27604100B3010F /* NoSecondEmotionRecordViewModel.swift */; }; 23808775291B3ACF00072763 /* RegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23808774291B3ACF00072763 /* RegisterViewController.swift */; }; 23808777291B404A00072763 /* UITextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23808776291B404A00072763 /* UITextField.swift */; }; 2380877B291B445100072763 /* DefaultButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2380877A291B445100072763 /* DefaultButton.swift */; }; @@ -343,6 +344,7 @@ 2375E48D2A20409200843056 /* GetMarshmallowsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMarshmallowsUseCase.swift; sourceTree = ""; }; 2375E48F2A2040FC00843056 /* MarshmallowRepositoryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarshmallowRepositoryInterface.swift; sourceTree = ""; }; 2375E4912A20415B00843056 /* MarshmallowRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarshmallowRepository.swift; sourceTree = ""; }; + 237CDEE02A27604100B3010F /* NoSecondEmotionRecordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSecondEmotionRecordViewModel.swift; sourceTree = ""; }; 23808774291B3ACF00072763 /* RegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterViewController.swift; sourceTree = ""; }; 23808776291B404A00072763 /* UITextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextField.swift; sourceTree = ""; }; 2380877A291B445100072763 /* DefaultButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultButton.swift; sourceTree = ""; }; @@ -1606,6 +1608,7 @@ children = ( EE7D35D629D5A16300D2AD60 /* Register */, 23431C2F2A24262B00571286 /* RecordTabViewModel.swift */, + 237CDEE02A27604100B3010F /* NoSecondEmotionRecordViewModel.swift */, ); path = Record; sourceTree = ""; @@ -2175,6 +2178,7 @@ EE98F0DD2980EFE0007BFCCD /* UserRouter.swift in Sources */, 23F5782C292D4329002DD28E /* MypageMarshmallowTableViewCell.swift in Sources */, 5724EC2D29162B3500DC529B /* UIFont.swift in Sources */, + 237CDEE12A27604100B3010F /* NoSecondEmotionRecordViewModel.swift in Sources */, EE9B05302994B09D00068503 /* NetworkErrorAlertViewController.swift in Sources */, EEC28CB429D30B770075CD4C /* KeyboardController.swift in Sources */, 57CCA7C5292EEB55007E22D1 /* RecordFirstEmotionView.swift in Sources */, diff --git a/POME/Presentation/ViewControllers/Record/RecordEmotionViewController.swift b/POME/Presentation/ViewControllers/Record/RecordEmotionViewController.swift index c631463..27b496f 100644 --- a/POME/Presentation/ViewControllers/Record/RecordEmotionViewController.swift +++ b/POME/Presentation/ViewControllers/Record/RecordEmotionViewController.swift @@ -6,27 +6,32 @@ // import UIKit +import RxSwift +import RxCocoa + class RecordEmotionViewController: BaseViewController { var recordEmotionView = RecordEmotionView() + let viewModel = NoSecondEmotionRecordViewModel() + var dataIndexBy: (IndexPath) -> Int = { indexPath in return indexPath.row - 1 } - var goalContent: GoalResponseModel? - var noSecondEmotionRecord: [RecordResponseModel] = [] + var goalContent = GoalResponseModel(endDate: "", id: 0, isEnd: true, isPublic: true, name: "", nickname: "", oneLineMind: "", price: 0, startDate: "", usePrice: 0) + private let goalRelay = PublishRelay() + + private let INFO_SECTION = 0 + private let COUNT_OF_NOT_RECORD_CELL = 1 //record 이외 UI 구성하는 cell 1개 존재 + + // Cell Height var expendingCellContent = ExpandingTableViewCellContent() override func viewDidLoad() { super.viewDidLoad() - - // Do any additional setup after loading the view. - } - - override func viewDidAppear(_ animated: Bool) { - if let goalContent = goalContent { - self.getNoSecondEmotionRecords(id: goalContent.id) - } + + goalRelay.accept(goalContent) + viewModel.refreshData() } override func style() { @@ -49,6 +54,49 @@ class RecordEmotionViewController: BaseViewController { super.initialize() } + override func bind() { + RecordObserver.shared.deleteRecord + .subscribe{ [weak self] _ in + self?.viewModel.refreshData() + }.disposed(by: disposeBag) + + RecordObserver.shared.registerSecondEmotion + .subscribe{ _ in + self.viewModel.refreshData() + }.disposed(by: disposeBag) + + viewModel.deleteRecordSubject + .subscribe{ + self.recordEmotionView.recordEmotionTableView.deleteRows(at: [[self.INFO_SECTION, $0 + self.COUNT_OF_NOT_RECORD_CELL]], with: .fade) + self.showEmptyView() + }.disposed(by: disposeBag) + + viewModel.modifyRecordSubject + .subscribe{ + self.recordEmotionView.recordEmotionTableView.reloadRows(at: [[self.INFO_SECTION, $0 + self.COUNT_OF_NOT_RECORD_CELL]], with: .none) + }.disposed(by: disposeBag) + + viewModel.reloadTableView + .subscribe{ [weak self] _ in + self?.reloadData() + }.disposed(by: disposeBag) + + + viewModel.transform(NoSecondEmotionRecordViewModel.Input(goal: goalRelay.asObservable())) + + } + + private func reloadData(){ + recordEmotionView.recordEmotionTableView.reloadData() + showEmptyView() + } + + private func showEmptyView(){ + viewModel.records.isEmpty + ? EmptyView(recordEmotionView.recordEmotionTableView).showEmptyView(Image.noting, "돌아볼 씀씀이가 없어요") + : EmptyView(recordEmotionView.recordEmotionTableView).hideEmptyView() + } + // // MARK: 더보기 버튼 - Dynamic Cell Height Method // @objc func viewMoreButtonDidTap(_ sender: IndexPathTapGesture) { // let content = expendingCellContent @@ -59,30 +107,13 @@ class RecordEmotionViewController: BaseViewController { // MARK: - TableView delegate extension RecordEmotionViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let count = self.noSecondEmotionRecord.count ?? 0 - count == 0 ? EmptyView(self.recordEmotionView.recordEmotionTableView).showEmptyView(Image.noting, "돌아볼 씀씀이가 없어요") : EmptyView(self.recordEmotionView.recordEmotionTableView).hideEmptyView() - - return 1 + count + return COUNT_OF_NOT_RECORD_CELL + viewModel.records.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let tag = indexPath.row switch tag { - case 0: - guard let cell = tableView.dequeueReusableCell(withIdentifier: "RecordEmotionTableViewCell", for: indexPath) as? RecordEmotionTableViewCell else { return UITableViewCell() } - if let goalContent = self.goalContent { - cell.setUpData(goalContent, self.noSecondEmotionRecord.count ?? 0) - } - cell.selectionStyle = .none - - return cell - default: - let cardIndex = dataIndexBy(indexPath) - let record = self.noSecondEmotionRecord[cardIndex] - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ConsumeReviewTableViewCell.self).then{ - $0.delegate = self - $0.bindingData(with: record) - } - return cell + case 0: return getGoalTableViewCell(indexPath: indexPath) + default: return getRecordTableViewCell(indexPath: indexPath) // // 더보기 버튼 클릭 Gesture // let viewMoreGesture = IndexPathTapGesture(target: self, action: #selector(viewMoreButtonDidTap(_:))) @@ -100,17 +131,32 @@ extension RecordEmotionViewController: UITableViewDelegate, UITableViewDataSourc let tag = indexPath.row if tag > 0 { let vc = SecondEmotionViewController() - vc.recordId = self.noSecondEmotionRecord[indexPath.item - 1].id + vc.recordId = viewModel.records[dataIndexBy(indexPath)].id self.navigationController?.pushViewController(vc, animated: true) } tableView.deselectRow(at: indexPath, animated: true) } + + private func getGoalTableViewCell(indexPath: IndexPath) -> RecordEmotionTableViewCell{ + recordEmotionView.recordEmotionTableView.dequeueReusableCell(for: indexPath, cellType: RecordEmotionTableViewCell.self).then{ + $0.setUpData(viewModel.goal, viewModel.records.count) + $0.selectionStyle = .none + } + } + + private func getRecordTableViewCell(indexPath: IndexPath) -> ConsumeReviewTableViewCell{ + recordEmotionView.recordEmotionTableView.dequeueReusableCell(for: indexPath, cellType: ConsumeReviewTableViewCell.self).then{ + $0.delegate = self + $0.bindingData(with: viewModel.records[dataIndexBy(indexPath)]) + } + } + } // MARK: - Record Cell delegate extension RecordEmotionViewController: RecordCellDelegate{ func presentReactionSheet(indexPath: IndexPath) { - let data = noSecondEmotionRecord[dataIndexBy(indexPath)].friendReactions + let data = viewModel.records[dataIndexBy(indexPath)].friendReactions FriendReactionSheetViewController(reactions: data).show(in: self) } @@ -124,12 +170,11 @@ extension RecordEmotionViewController: RecordCellDelegate{ let editAction = UIAlertAction(title: "수정하기", style: .default){ _ in alert.dismiss(animated: true) - guard let goalContent = self.goalContent else {return} - let vc = ModifyRecordViewController(goal: goalContent, - record: self.noSecondEmotionRecord[recordIndex]) + let vc = ModifyRecordViewController(goal: self.goalContent, + record: self.viewModel.records[recordIndex]) vc.completion = { - self.noSecondEmotionRecord[recordIndex] = $0 + self.viewModel.records[recordIndex] = $0 self.recordEmotionView.recordEmotionTableView.reloadRows(at: [indexPath], with: .none) } self.navigationController?.pushViewController(vc, animated: true) @@ -138,7 +183,7 @@ extension RecordEmotionViewController: RecordCellDelegate{ alert.dismiss(animated: true) let alert = ImageAlert.deleteRecord.generateAndShow(in: self) alert.completion = { - self.deleteRecord(id: self.noSecondEmotionRecord[recordIndex].id) + self.viewModel.deleteRecord(index: recordIndex) } } let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) @@ -150,33 +195,3 @@ extension RecordEmotionViewController: RecordCellDelegate{ self.present(alert, animated: true) } } -// MARK: - API -extension RecordEmotionViewController { - // MARK: 일주일이 지났고, 두 번째 감정이 없는 기록 조회 API - private func getNoSecondEmotionRecords(id: Int) { - RecordService.shared.getNoSecondEmotionRecords(id: id) { result in - switch result{ - case .success(let data): -// print("LOG: 일주일이 지났고, 두 번째 감정이 없는 기록 조회", data) - - self.noSecondEmotionRecord = data.content - self.recordEmotionView.recordEmotionTableView.reloadData() - - break - default: - print(result) - NetworkAlert.show(in: self){ [weak self] in - self?.getNoSecondEmotionRecords(id: id) - } - break - } - } - } - // MARK: 기록 삭제 API - private func deleteRecord(id: Int) { - RecordService.shared.deleteRecord(id: id) { result in - print("기록 삭제 성공") - self.getNoSecondEmotionRecords(id: self.goalContent?.id ?? 0) - } - } -} diff --git a/POME/Presentation/ViewControllers/Record/RecordViewController.swift b/POME/Presentation/ViewControllers/Record/RecordViewController.swift index 0d9cc1b..e3c2275 100644 --- a/POME/Presentation/ViewControllers/Record/RecordViewController.swift +++ b/POME/Presentation/ViewControllers/Record/RecordViewController.swift @@ -157,7 +157,7 @@ class RecordViewController: BaseTabViewController { } private func showEmptyView(){ - viewModel.records.isEmpty || viewModel.records.isEmpty + viewModel.records.isEmpty ? EmptyView(self.recordView.recordTableView).showEmptyView(Image.noting, "기록한 씀씀이가 없어요") : EmptyView(self.recordView.recordTableView).hideEmptyView() } @@ -177,7 +177,7 @@ class RecordViewController: BaseTabViewController { 7일 이전 기록은 없으나 2차감정 기록을 하지 않았을 때 -> 아직 돌아보지 않은 기록이 있어요 바텀시트 띄우기 & 종료 페이지 진입 불가 모든 감정기록 완료했을 때 -> 종료페이지 진입 */ - let isSecondEmotionNeeded = !viewModel.records.isEmpty || !(viewModel.noSecondEmotionRecords == 0) + let isSecondEmotionNeeded = !viewModel.records.isEmpty || !(viewModel.noSecondEmotionRecordsCount == 0) isSecondEmotionNeeded ? showNoSecondEmotionWarning() : moveToAllRecords(sender) } @objc func addGoalButtonDidTap() { @@ -251,7 +251,7 @@ extension RecordViewController: UICollectionViewDelegate, UICollectionViewDataSo let itemIdx = indexPath.row cell.title = viewModel.goals[itemIdx].name - if itemIdx == self.selectedGoalIndex {cell.setSelectState()} + if itemIdx == viewModel.selectedGoalIndex {cell.setSelectState()} else if viewModel.goals[itemIdx].isGoalEnd {cell.setInactivateState()} // 기한이 지난 목표일 때 else {cell.setUnselectState()} @@ -297,11 +297,11 @@ extension RecordViewController: UITableViewDelegate, UITableViewDataSource { } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let count = viewModel.records.count - if count == 0 { - EmptyView(self.recordView.recordTableView).showEmptyView(Image.noting, "기록한 씀씀이가 없어요") - } else { - EmptyView(self.recordView.recordTableView).hideEmptyView() - } +// if count == 0 { +// EmptyView(self.recordView.recordTableView).showEmptyView(Image.noting, "기록한 씀씀이가 없어요") +// } else { +// EmptyView(self.recordView.recordTableView).hideEmptyView() +// } return count + COUNT_OF_NOT_RECORD_CELL } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -322,17 +322,20 @@ extension RecordViewController: UITableViewDelegate, UITableViewDataSource { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let tag = indexPath.row - if tag == 2 && !viewModel.goals.isEmpty { - let vc = RecordEmotionViewController() - vc.goalContent = viewModel.goals[self.selectedGoalIndex] - self.navigationController?.pushViewController(vc, animated: true) - } else if tag > 2 { - cannotAddEmotionWarning() - } + if tag == 2 && !viewModel.goals.isEmpty {moveToSecondEmotionRecords()} + else if tag > 2 {cannotAddEmotionWarning()} tableView.deselectRow(at: indexPath, animated: true) } + // 일주일이 지났고, 두번째 감정을 남기지 않은 기록 페이지 이동 + private func moveToSecondEmotionRecords() { + let vc = RecordEmotionViewController() + vc.goalContent = viewModel.goals[selectedGoalIndex] + + self.navigationController?.pushViewController(vc, animated: true) + } + private func getGoalTableViewCell(indexPath: IndexPath) -> GoalTableViewCell{ recordView.recordTableView.dequeueReusableCell(for: indexPath, cellType: GoalTableViewCell.self).then{ $0.setUpData(viewModel.goals[self.selectedGoalIndex]) @@ -381,7 +384,7 @@ extension RecordViewController: UITableViewDelegate, UITableViewDataSource { private func getNoSecondEmotionRecordsTableViewCell(indexPath: IndexPath) -> GoEmotionBannerTableViewCell{ recordView.recordTableView.dequeueReusableCell(for: indexPath, cellType: GoEmotionBannerTableViewCell.self).then{ - $0.bindingData(viewModel.noSecondEmotionRecords) + $0.bindingData(viewModel.noSecondEmotionRecordsCount) $0.selectionStyle = .none } } diff --git a/POME/Presentation/ViewModel/Record/NoSecondEmotionRecordViewModel.swift b/POME/Presentation/ViewModel/Record/NoSecondEmotionRecordViewModel.swift new file mode 100644 index 0000000..718553f --- /dev/null +++ b/POME/Presentation/ViewModel/Record/NoSecondEmotionRecordViewModel.swift @@ -0,0 +1,100 @@ +// +// NoSecondEmotionRecordViewModel.swift +// POME +// +// Created by gomin on 2023/05/31. +// + +import Foundation +import RxSwift +import RxCocoa + +/* + 일주일이 지났고 두번째 감정이 필요한 기록 조회 + 기록 삭제 + 기록 수정 + */ + +protocol NoSecondEmotionRecordViewModelInterface: BaseViewModel, ModifyRecordInterface { + var records: [RecordResponseModel] { get } +} + + +final class NoSecondEmotionRecordViewModel: NoSecondEmotionRecordViewModelInterface, DeleteRecord { + + private let deleteRecordUseCase: DeleteRecordUseCaseInterface + private let getNoSecondEmotionRecordsUseCase: GetNoSecondEmotionRecordsUseCaseInterface + + init(getNoSecondEmotionRecordsUseCase: GetNoSecondEmotionRecordsUseCaseInterface = GetNoSecondEmotionRecordsUseCase(), + deleteRecordUseCase: DeleteRecordUseCaseInterface = DeleteRecordUseCase()) { + self.getNoSecondEmotionRecordsUseCase = getNoSecondEmotionRecordsUseCase + self.deleteRecordUseCase = deleteRecordUseCase + } + + var hasNextPage: Bool = false + var page: Int = 0 + + var goal = GoalResponseModel(endDate: "", id: 0, isEnd: true, isPublic: true, name: "", nickname: "", oneLineMind: "", price: 0, startDate: "", usePrice: 0) + internal var records = [RecordResponseModel]() + + let reloadTableView = PublishRelay() + let deleteRecordSubject = PublishSubject() + let modifyRecordSubject = PublishSubject() + + let disposeBag = DisposeBag() + + struct Input{ + let goal: Observable + } + + struct Output{} + + @discardableResult + func transform(_ input: Input) -> Output{ + + input.goal + .subscribe{ [weak self] in + self?.goal = $0 + self?.initializeStateAndRequestRecord() + }.disposed(by: disposeBag) + + return Output() + } + + func refreshData() { + initializeStateAndRequestRecord() + } + + private func initializeStateAndRequestRecord(){ + hasNextPage = false + page = 0 + requestNoSecondEmotionRecords() + } + + private func requestNoSecondEmotionRecords(){ + getNoSecondEmotionRecordsUseCase + .execute(id: goal.id) + .subscribe { [weak self] in + self?.records = $0.content + self?.reloadTableView.accept(Void()) + }.disposed(by: disposeBag) + } + +} + +extension NoSecondEmotionRecordViewModel { + func deleteRecord(index: Int) { + deleteRecordUseCase.execute(requestValue: DeleteRecordRequestModel(recordId: records[index].id)) + .subscribe{ [weak self] in + if $0 == .success { + self?.records.remove(at: index) + self?.deleteRecordSubject.onNext(index) + } + }.disposed(by: disposeBag) + } + + func modifyRecord(_ record: RecordResponseModel, index: Int){ + records[index] = record + modifyRecordSubject.onNext(index) + } +} diff --git a/POME/Presentation/ViewModel/Record/RecordTabViewModel.swift b/POME/Presentation/ViewModel/Record/RecordTabViewModel.swift index bf86038..d9f55a6 100644 --- a/POME/Presentation/ViewModel/Record/RecordTabViewModel.swift +++ b/POME/Presentation/ViewModel/Record/RecordTabViewModel.swift @@ -17,8 +17,8 @@ import RxCocoa 일주일이 지났고 두번째 감정이 필요한 기록 조회 */ -protocol NoSecondEmotionRecordViewModelInterface { - var noSecondEmotionRecords: Int { get } +protocol NoSecondEmotionRecordCountViewModelInterface { + var noSecondEmotionRecordsCount: Int { get } } //protocol RecordTabViewModelInterface: BaseViewModel, ModifyRecordInterface { @@ -26,9 +26,9 @@ protocol NoSecondEmotionRecordViewModelInterface { // var records: [RecordResponseModel] { get } //} -final class RecordTabViewModel: GoalWithRecordViewModel, NoSecondEmotionRecordViewModelInterface { +final class RecordTabViewModel: GoalWithRecordViewModel, NoSecondEmotionRecordCountViewModelInterface { - internal var noSecondEmotionRecords: Int = 0 + internal var noSecondEmotionRecordsCount: Int = 0 private let deleteGoalUseCase: DeleteGoalUseCaseInterface private let getRecordsUseCase: GetRecordsOfGoalInRecordTabUseCaseInterface @@ -78,7 +78,7 @@ final class RecordTabViewModel: GoalWithRecordViewModel, NoSecondEmotionRecordVi getNoSecondEmotionRecordsUseCase .execute(id: goals[self.selectedGoalIndex].id) .subscribe { [weak self] in - self?.noSecondEmotionRecords = $0.content.count + self?.noSecondEmotionRecordsCount = $0.content.count }.disposed(by: disposeBag) }