From 41685d4b148fa7855eda56cb9a5a6ab75a3eb231 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Tue, 18 Apr 2023 16:21:31 +0900 Subject: [PATCH 01/12] [ADD] Implement edit button binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 댓글 수정 버튼과 ViewModel 간 바인딩을 구현했습니다. --- .../Views/Home/Clipboard/BoardDetailViewController.swift | 2 +- .../CommentOptionBottomSheetViewController.swift | 7 +++++++ .../Home/Clipboard/ViewModel/BoardDetailViewModel.swift | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index 871a3531c..ce1dc3539 100644 --- a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift @@ -183,7 +183,7 @@ extension BoardDetailViewController: CommentOptionBottomSheetDelegate { } func editButtonTapped() { - print(#function) + viewModel.editOptionObserver.onNext(Void()) } func reportButtonTapped() { diff --git a/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift b/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift index bb97fd2af..54825f776 100644 --- a/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift @@ -114,5 +114,12 @@ final class CommentOptionBottomSheetViewController: BottomSheetViewController { owner.dismiss(animated: true) } .disposed(by: disposeBag) + + editCommentView.button.rx.tap + .subscribe(with: self) { owner, _ in + owner.delegate?.editButtonTapped() + owner.dismiss(animated: true) + } + .disposed(by: disposeBag) } } diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 8a027b077..5b478d527 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -26,8 +26,12 @@ protocol BoardDetailViewModelType { /// 답장할 대상자의 ID를 emit합니다. var replyIDObserver: AnyObserver { get } + /// 삭제 버튼을 누른 경우 해당 옵저버를 이용하여 emit합니다. var deleteOptionObserver: AnyObserver { get } + /// 수정 버튼을 누른 경우 해당 옵저버를 이용하여 emit합니다. + var editOptionObserver: AnyObserver { get } + //Output /// 답장할 대상자의 닉네임을 받습니다. @@ -79,6 +83,7 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let showBottomSheetSubject = PublishSubject() private let recentSelectedCommentIDSubject = ReplaySubject.create(bufferSize: 1) private let deleteOptionSubject = PublishSubject() + private let editOptionSubject = PublishSubject() // MARK: - Initializations @@ -239,6 +244,9 @@ extension BoardDetailViewModel: BoardDetailViewModelType { var deleteOptionObserver: AnyObserver { deleteOptionSubject.asObserver() } + var editOptionObserver: AnyObserver { + editOptionSubject.asObserver() + } } // MARK: - Diffable DataSource From 89827771599bfc0a46557b6b7dabf0d3382fd443 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Tue, 18 Apr 2023 17:09:30 +0900 Subject: [PATCH 02/12] [REFACTOR] Refactor ReplyView to CommentOptionDecoratorView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 답장 뿐 아니라 댓글 수정시에도 ReplyView가 사용되어 이름을 변경했습니다. 이 때 ReplyVIew에 들어가는 UILabel과 UIButton의 text도 바뀌므로 두 텍스트 값을 설정하기 위한 프로퍼티(labelText, buttonText) 설정도 함께 구현했습니다 --- PLUB.xcodeproj/project.pbxproj | 8 ++--- .../Clipboard/BoardDetailViewController.swift | 24 +++++++------ ...swift => CommentOptionDecoratorView.swift} | 35 +++++++++++-------- .../ViewModel/BoardDetailViewModel.swift | 13 ++++--- 4 files changed, 44 insertions(+), 36 deletions(-) rename PLUB/Sources/Views/Home/Clipboard/Component/{ReplyView.swift => CommentOptionDecoratorView.swift} (67%) diff --git a/PLUB.xcodeproj/project.pbxproj b/PLUB.xcodeproj/project.pbxproj index d85ebbd82..f7758586d 100644 --- a/PLUB.xcodeproj/project.pbxproj +++ b/PLUB.xcodeproj/project.pbxproj @@ -114,7 +114,7 @@ 70F1DFEA297D992100F9BC83 /* RecruitmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F1DFE9297D992100F9BC83 /* RecruitmentService.swift */; }; 70F1DFEF297D9E1C00F9BC83 /* DetailRecruitmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F1DFEE297D9E1C00F9BC83 /* DetailRecruitmentViewModel.swift */; }; 70F1DFF1297DA3CE00F9BC83 /* DetailRecruitmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F1DFF0297DA3CE00F9BC83 /* DetailRecruitmentModel.swift */; }; - BA034D3229E864CA003B7192 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA034D3129E864CA003B7192 /* ReplyView.swift */; }; + BA034D3229E864CA003B7192 /* CommentOptionDecoratorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA034D3129E864CA003B7192 /* CommentOptionDecoratorView.swift */; }; BA117A3B297440B500B37E03 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA117A3A297440B500B37E03 /* UserDefaultsWrapper.swift */; }; BA117A3D297440D900B37E03 /* UserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA117A3C297440D900B37E03 /* UserManager.swift */; }; BA117A4E297444EE00B37E03 /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = BA117A4D297444EE00B37E03 /* KakaoSDKAuth */; }; @@ -483,7 +483,7 @@ 70F1DFE9297D992100F9BC83 /* RecruitmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecruitmentService.swift; sourceTree = ""; }; 70F1DFEE297D9E1C00F9BC83 /* DetailRecruitmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailRecruitmentViewModel.swift; sourceTree = ""; }; 70F1DFF0297DA3CE00F9BC83 /* DetailRecruitmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailRecruitmentModel.swift; sourceTree = ""; }; - BA034D3129E864CA003B7192 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = ""; }; + BA034D3129E864CA003B7192 /* CommentOptionDecoratorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentOptionDecoratorView.swift; sourceTree = ""; }; BA117A3A297440B500B37E03 /* UserDefaultsWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsWrapper.swift; sourceTree = ""; }; BA117A3C297440D900B37E03 /* UserManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserManager.swift; sourceTree = ""; }; BA1BEDC2296454AB00C6BD41 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; @@ -1564,7 +1564,7 @@ children = ( BAB033F929B5C5580077C4D9 /* BoardDetailCollectionHeaderView.swift */, BAC9794429CF0E9E00A5DF1B /* CommentInputView.swift */, - BA034D3129E864CA003B7192 /* ReplyView.swift */, + BA034D3129E864CA003B7192 /* CommentOptionDecoratorView.swift */, BA4CCDF029EA79EA0040D0D7 /* CommentOptionBottomSheetViewController.swift */, ); path = Component; @@ -2255,7 +2255,7 @@ 70A4209C2988B1DC0026E9F9 /* SortControl.swift in Sources */, BA5D9EDB29E406AD00F06AB5 /* Day.swift in Sources */, 70A420A7298D0F140026E9F9 /* RecentSearchListCollectionViewCell.swift in Sources */, - BA034D3229E864CA003B7192 /* ReplyView.swift in Sources */, + BA034D3229E864CA003B7192 /* CommentOptionDecoratorView.swift in Sources */, C3E0390A29C2125700C4744C /* InquireApplicantResponse.swift in Sources */, C3B3435629BE3A2200935B73 /* MyPageService.swift in Sources */, C36F31C729E3072800AC2C8A /* ScheduleBottomSheetViewController.swift in Sources */, diff --git a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index ce1dc3539..e38c81274 100644 --- a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift @@ -32,7 +32,8 @@ final class BoardDetailViewController: BaseViewController { // MARK: Comment Posting View (댓글 작성 UI) - private let replyView = ReplyView().then { + /// 답글을 달거나 댓글을 수정할 때 보여지는 데코레이터 뷰 + private let decoratorView = CommentOptionDecoratorView().then { $0.backgroundColor = .background $0.isHidden = true } @@ -68,7 +69,7 @@ final class BoardDetailViewController: BaseViewController { override func setupLayouts() { super.setupLayouts() - [collectionView, commentInputView, replyView].forEach { + [collectionView, commentInputView, decoratorView].forEach { view.addSubview($0) } } @@ -85,7 +86,7 @@ final class BoardDetailViewController: BaseViewController { $0.bottom.equalTo(view.safeAreaLayoutGuide) } - replyView.snp.makeConstraints { + decoratorView.snp.makeConstraints { $0.directionalHorizontalEdges.equalTo(view.safeAreaLayoutGuide) $0.bottom.equalTo(commentInputView.snp.top) $0.height.equalTo(28) @@ -101,17 +102,18 @@ final class BoardDetailViewController: BaseViewController { // == comment posting delegate == commentInputView.delegate = self - replyView.delegate = self + decoratorView.delegate = self } override func bind() { super.bind() collectionView.rx.setDelegate(self).disposed(by: disposeBag) - viewModel.replyNicknameObserable - .subscribe(with: self) { owner, nickname in - owner.replyView.nickname = nickname - owner.replyView.isHidden = false + viewModel.decoratorNameObserable + .subscribe(with: self) { owner, tuple in + owner.decoratorView.labelText = tuple.labelText + owner.decoratorView.buttonText = tuple.buttonText + owner.decoratorView.isHidden = false } .disposed(by: disposeBag) @@ -166,12 +168,12 @@ extension BoardDetailViewController: CommentInputViewDelegate { } } -// MARK: - ReplyViewDelegate +// MARK: - CommentOptionViewDelegate -extension BoardDetailViewController: ReplyViewDelegate { +extension BoardDetailViewController: CommentOptionViewDelegate { func cancelButtonTapped() { viewModel.replyIDObserver.onNext(nil) - replyView.isHidden = true + decoratorView.isHidden = true } } diff --git a/PLUB/Sources/Views/Home/Clipboard/Component/ReplyView.swift b/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionDecoratorView.swift similarity index 67% rename from PLUB/Sources/Views/Home/Clipboard/Component/ReplyView.swift rename to PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionDecoratorView.swift index 42fe5cc38..49891eb1b 100644 --- a/PLUB/Sources/Views/Home/Clipboard/Component/ReplyView.swift +++ b/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionDecoratorView.swift @@ -1,5 +1,5 @@ // -// ReplyView.swift +// CommentOptionDecoratorView.swift // PLUB // // Created by 홍승현 on 2023/04/14. @@ -12,22 +12,29 @@ import RxCocoa import SnapKit import Then -protocol ReplyViewDelegate: AnyObject { +protocol CommentOptionViewDelegate: AnyObject { func cancelButtonTapped() } -final class ReplyView: UIView { +final class CommentOptionDecoratorView: UIView { // MARK: - Properties - /// 답글을 달 닉네임을 설정합니다. - var nickname: String = "Unknown" { + /// 왼쪽에 들어갈 label text를 설정합니다. + var labelText: String = "Unknown" { didSet { - replyIndicatorLabel.text = "\(nickname)님에게 답글 쓰는 중..." + indicatorLabel.text = labelText } } - weak var delegate: ReplyViewDelegate? + /// 오른쪽 버튼에 들어갈 text를 설정합니다. + var buttonText: String = "취소" { + didSet { + cancelButton.setTitle(buttonText, for: .normal) + } + } + + weak var delegate: CommentOptionViewDelegate? private let disposeBag = DisposeBag() @@ -37,12 +44,12 @@ final class ReplyView: UIView { $0.backgroundColor = .lightGray } - private let replyIndicatorLabel = UILabel().then { + private let indicatorLabel = UILabel().then { $0.font = .overLine $0.textColor = .black } - private let replyCancelButton = UIButton().then { + private let cancelButton = UIButton().then { $0.setTitleColor(.main, for: .normal) $0.setTitle("답글 작성 취소", for: .normal) $0.titleLabel?.font = .overLine @@ -64,18 +71,18 @@ final class ReplyView: UIView { // MARK: - Configuration private func setupLayouts() { - [replyIndicatorLabel, replyCancelButton, separatorLineView].forEach { + [indicatorLabel, cancelButton, separatorLineView].forEach { addSubview($0) } } private func setupConstraints() { - replyIndicatorLabel.snp.makeConstraints { + indicatorLabel.snp.makeConstraints { $0.leading.equalToSuperview().inset(Metrics.directionalHorizontalInset) $0.centerY.equalToSuperview() } - replyCancelButton.snp.makeConstraints { + cancelButton.snp.makeConstraints { $0.centerY.equalToSuperview() $0.trailing.equalToSuperview().inset(Metrics.directionalHorizontalInset) } @@ -87,7 +94,7 @@ final class ReplyView: UIView { } private func bind() { - replyCancelButton.rx.tap + cancelButton.rx.tap .subscribe(with: self) { owner, _ in owner.delegate?.cancelButtonTapped() } @@ -95,7 +102,7 @@ final class ReplyView: UIView { } } -extension ReplyView { +extension CommentOptionDecoratorView { enum Metrics { static let directionalHorizontalInset = 24 static let separatorHeight = 1 diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 5b478d527..a24ce2461 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -34,8 +34,8 @@ protocol BoardDetailViewModelType { //Output - /// 답장할 대상자의 닉네임을 받습니다. - var replyNicknameObserable: Observable { get } + /// decoratorView에 들어갈 적절한 text를 처리합니다. + var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { get } var showBottomSheetObservable: Observable { get } } @@ -78,7 +78,7 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let collectionViewSubject = PublishSubject() private let commentInputSubject = PublishSubject() private let replyIDSubject = BehaviorSubject(value: nil) - private let replyNicknameSubject = PublishSubject() + private let decoratorNameSubject = PublishSubject<(labelText: String, buttonText: String)>() private let bottomCellSubject = PublishSubject<(collectionViewHeight: CGFloat, offset: CGFloat)>() private let showBottomSheetSubject = PublishSubject() private let recentSelectedCommentIDSubject = ReplaySubject.create(bufferSize: 1) @@ -235,8 +235,8 @@ extension BoardDetailViewModel: BoardDetailViewModelType { var replyIDObserver: AnyObserver { replyIDSubject.asObserver() } - var replyNicknameObserable: Observable { - replyNicknameSubject.asObservable() + var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { + decoratorNameSubject.asObservable() } var showBottomSheetObservable: Observable { showBottomSheetSubject.asObservable() @@ -345,8 +345,7 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { else { return } - - replyNicknameSubject.onNext(commentValue.nickname) + decoratorNameSubject.onNext((labelText: "\(commentValue.nickname)에게 답글 쓰는 중...", buttonText: "답글 작성 취소")) replyIDSubject.onNext(commentID) } From 616a4741393ed475e35e6bf49898d00af8b00a85 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 14:14:47 +0900 Subject: [PATCH 03/12] [REFACTOR] CommentOptionBottomSheetVC's initializer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정 버튼, 신고 버튼 그리고 삭제 버튼이 눌렸을 때, 대상이 되는 id도 함게 반환하도록 재구현했습니다. 기존에는 BottomSheet를 누르는 즉시 id를 반환하였는데, 이 경우 문제가 될 수 있는 시나리오가 존재합니다. 예를 들면, 수정 버튼을 누르고 댓글을 수정하는 도중 다른 댓글의 옵션 버튼을 누르면 그 즉시 id가 바뀌게 됩니다. 이 경우 내가 수정하고자 했던 id와 다른 값으로 변경되기 때문에 문제가 야기될 수 있습니다. --- .../Clipboard/BoardDetailViewController.swift | 11 +++++------ .../CommentOptionBottomSheetViewController.swift | 15 +++++++++------ .../ViewModel/BoardDetailViewModel.swift | 8 ++++---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index e38c81274..aa52d8d40 100644 --- a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift @@ -118,12 +118,11 @@ final class BoardDetailViewController: BaseViewController { .disposed(by: disposeBag) viewModel.showBottomSheetObservable - .subscribe(with: self) { owner, userType in - let bottomSheetVC = CommentOptionBottomSheetViewController(userAccessType: userType).then { + .subscribe(with: self) { owner, tuple in + let bottomSheetVC = CommentOptionBottomSheetViewController(commentID: tuple.commentID, userAccessType: tuple.userType).then { $0.delegate = owner } owner.present(bottomSheetVC, animated: true) - } .disposed(by: disposeBag) @@ -180,15 +179,15 @@ extension BoardDetailViewController: CommentOptionViewDelegate { // MARK: - CommentOptionBottomSheetDelegate extension BoardDetailViewController: CommentOptionBottomSheetDelegate { - func deleteButtonTapped() { + func deleteButtonTapped(commentID: Int) { viewModel.deleteOptionObserver.onNext(Void()) } - func editButtonTapped() { + func editButtonTapped(commentID: Int) { viewModel.editOptionObserver.onNext(Void()) } - func reportButtonTapped() { + func reportButtonTapped(commentID: Int) { print(#function) } } diff --git a/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift b/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift index 54825f776..14d1b5529 100644 --- a/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift @@ -13,9 +13,9 @@ import SnapKit import Then protocol CommentOptionBottomSheetDelegate: AnyObject { - func deleteButtonTapped() - func editButtonTapped() - func reportButtonTapped() + func deleteButtonTapped(commentID: Int) + func editButtonTapped(commentID: Int) + func reportButtonTapped(commentID: Int) } @@ -35,6 +35,8 @@ final class CommentOptionBottomSheetViewController: BottomSheetViewController { // MARK: - Properties + private let commentID: Int + private let userAccessType: UserAccessType weak var delegate: CommentOptionBottomSheetDelegate? @@ -53,7 +55,8 @@ final class CommentOptionBottomSheetViewController: BottomSheetViewController { // MARK: - Initializations - init(userAccessType: UserAccessType) { + init(commentID: Int, userAccessType: UserAccessType) { + self.commentID = commentID self.userAccessType = userAccessType super.init(nibName: nil, bundle: nil) } @@ -110,14 +113,14 @@ final class CommentOptionBottomSheetViewController: BottomSheetViewController { deleteCommentView.button.rx.tap .subscribe(with: self) { owner, _ in - owner.delegate?.deleteButtonTapped() + owner.delegate?.deleteButtonTapped(commentID: owner.commentID) owner.dismiss(animated: true) } .disposed(by: disposeBag) editCommentView.button.rx.tap .subscribe(with: self) { owner, _ in - owner.delegate?.editButtonTapped() + owner.delegate?.editButtonTapped(commentID: owner.commentID) owner.dismiss(animated: true) } .disposed(by: disposeBag) diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index a24ce2461..fd11fc0ca 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -37,7 +37,7 @@ protocol BoardDetailViewModelType { /// decoratorView에 들어갈 적절한 text를 처리합니다. var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { get } - var showBottomSheetObservable: Observable { get } + var showBottomSheetObservable: Observable<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)> { get } } protocol BoardDetailDataStore { @@ -80,7 +80,7 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let replyIDSubject = BehaviorSubject(value: nil) private let decoratorNameSubject = PublishSubject<(labelText: String, buttonText: String)>() private let bottomCellSubject = PublishSubject<(collectionViewHeight: CGFloat, offset: CGFloat)>() - private let showBottomSheetSubject = PublishSubject() + private let showBottomSheetSubject = PublishSubject<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)>() private let recentSelectedCommentIDSubject = ReplaySubject.create(bufferSize: 1) private let deleteOptionSubject = PublishSubject() private let editOptionSubject = PublishSubject() @@ -238,7 +238,7 @@ extension BoardDetailViewModel: BoardDetailViewModelType { var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { decoratorNameSubject.asObservable() } - var showBottomSheetObservable: Observable { + var showBottomSheetObservable: Observable<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)> { showBottomSheetSubject.asObservable() } var deleteOptionObserver: AnyObserver { @@ -368,7 +368,7 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { accessType = .normal } - showBottomSheetSubject.onNext(accessType) + showBottomSheetSubject.onNext((commentID, accessType)) } } From 15bab7b1b53ba5639a29b1b5999956df871df1c5 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 15:42:44 +0900 Subject: [PATCH 04/12] [REFACTOR] Integrate ReplyID and recentSelectedCommentID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 답글, 댓글 수정, 댓글 삭제 시 필요한 타겟 ID를 두 Subject로 처리하고 있었는데, 이를 하나의 Subject로 통합하였습니다. --- .../Clipboard/BoardDetailViewController.swift | 4 +++- .../ViewModel/BoardDetailViewModel.swift | 23 ++++++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index aa52d8d40..489902223 100644 --- a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift @@ -171,7 +171,7 @@ extension BoardDetailViewController: CommentInputViewDelegate { extension BoardDetailViewController: CommentOptionViewDelegate { func cancelButtonTapped() { - viewModel.replyIDObserver.onNext(nil) + viewModel.targetIDObserver.onNext(nil) decoratorView.isHidden = true } } @@ -180,10 +180,12 @@ extension BoardDetailViewController: CommentOptionViewDelegate { extension BoardDetailViewController: CommentOptionBottomSheetDelegate { func deleteButtonTapped(commentID: Int) { + viewModel.targetIDObserver.onNext(commentID) viewModel.deleteOptionObserver.onNext(Void()) } func editButtonTapped(commentID: Int) { + viewModel.targetIDObserver.onNext(commentID) viewModel.editOptionObserver.onNext(Void()) } diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index fd11fc0ca..9df772f45 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -23,8 +23,8 @@ protocol BoardDetailViewModelType { /// 사용자의 댓글을 입력합니다. var commentsInput: AnyObserver { get } - /// 답장할 대상자의 ID를 emit합니다. - var replyIDObserver: AnyObserver { get } + /// 댓글 관련 작업을 처리할 대상자의 ID를 emit합니다. + var targetIDObserver: AnyObserver { get } /// 삭제 버튼을 누른 경우 해당 옵저버를 이용하여 emit합니다. var deleteOptionObserver: AnyObserver { get } @@ -77,13 +77,12 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let collectionViewSubject = PublishSubject() private let commentInputSubject = PublishSubject() - private let replyIDSubject = BehaviorSubject(value: nil) private let decoratorNameSubject = PublishSubject<(labelText: String, buttonText: String)>() private let bottomCellSubject = PublishSubject<(collectionViewHeight: CGFloat, offset: CGFloat)>() private let showBottomSheetSubject = PublishSubject<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)>() - private let recentSelectedCommentIDSubject = ReplaySubject.create(bufferSize: 1) private let deleteOptionSubject = PublishSubject() private let editOptionSubject = PublishSubject() + private let targetIDSubject = BehaviorSubject(value: nil) // MARK: - Initializations @@ -144,7 +143,7 @@ extension BoardDetailViewModel { /// - commentsObservable: 작성된 문자열과 부모 ID를 갖는 Observable private func createComments(plubbingID: Int, content: BoardModel) { commentInputSubject - .withLatestFrom(replyIDSubject) { comment, parentID in + .withLatestFrom(targetIDSubject) { comment, parentID in (comment: comment, parentID: parentID) } .flatMap { [postCommentUseCase] in @@ -198,11 +197,12 @@ extension BoardDetailViewModel { /// - content: 게시글 컨텐츠 모델 private func deleteComments(plubbingID: Int, content: BoardModel) { deleteOptionSubject - .withLatestFrom(recentSelectedCommentIDSubject) + .withLatestFrom(targetIDSubject) + .compactMap { $0 } .flatMap { [deleteCommentUseCase] commentID in deleteCommentUseCase.execute(plubbingID: plubbingID, feedID: content.feedID, commentID: commentID) } - .withLatestFrom(recentSelectedCommentIDSubject) + .withLatestFrom(targetIDSubject) .subscribe(with: self) { owner, commentID in guard let content = owner.comments.first(where: { $0.commentID == commentID }) else { return } if content.type == .normal { @@ -232,8 +232,8 @@ extension BoardDetailViewModel: BoardDetailViewModelType { var offsetObserver: AnyObserver<(collectionViewHeight: CGFloat, offset: CGFloat)> { bottomCellSubject.asObserver() } - var replyIDObserver: AnyObserver { - replyIDSubject.asObserver() + var targetIDObserver: AnyObserver { + targetIDSubject.asObserver() } var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { decoratorNameSubject.asObservable() @@ -346,7 +346,7 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { return } decoratorNameSubject.onNext((labelText: "\(commentValue.nickname)에게 답글 쓰는 중...", buttonText: "답글 작성 취소")) - replyIDSubject.onNext(commentID) + targetIDSubject.onNext(commentID) } func didTappedOptionButton(commentID: Int) { @@ -355,9 +355,6 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { return } - // 옵션 버튼을 선택한 셀의 commentID를 emit - recentSelectedCommentIDSubject.onNext(commentID) - let accessType: CommentOptionBottomSheetViewController.UserAccessType if content.isCommentAuthor { From c4e6c1509f609426a1bdb53b7bc62b32cd51c194 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 15:58:54 +0900 Subject: [PATCH 05/12] [REFACTOR] Integrate DeleteOption, EditOption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 댓글 수정 옵션과 삭제 옵션을 두 가지로 나누어 Subject를 처리했으나, 코드 가독성과 확장성을 고려하여 하나의 Subject로 통합했습니다. 통합하는 과정에서 `Void`로 emit했던 것이 `CommentOption`이라는 새로운 열거형 타입으로 변경되었고, CommentOption은 댓글 생성, 수정, 삭제와 연관되어 동작합니다. --- .../Clipboard/BoardDetailViewController.swift | 7 ++- .../ViewModel/BoardDetailViewModel.swift | 45 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index 489902223..3fe0db029 100644 --- a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift @@ -171,6 +171,7 @@ extension BoardDetailViewController: CommentInputViewDelegate { extension BoardDetailViewController: CommentOptionViewDelegate { func cancelButtonTapped() { + viewModel.commentOptionObserver.onNext(.commentOrReply) viewModel.targetIDObserver.onNext(nil) decoratorView.isHidden = true } @@ -180,13 +181,15 @@ extension BoardDetailViewController: CommentOptionViewDelegate { extension BoardDetailViewController: CommentOptionBottomSheetDelegate { func deleteButtonTapped(commentID: Int) { + // 순서 중요 viewModel.targetIDObserver.onNext(commentID) - viewModel.deleteOptionObserver.onNext(Void()) + viewModel.commentOptionObserver.onNext(.delete) } func editButtonTapped(commentID: Int) { + // 순서 중요 viewModel.targetIDObserver.onNext(commentID) - viewModel.editOptionObserver.onNext(Void()) + viewModel.commentOptionObserver.onNext(.edit) } func reportButtonTapped(commentID: Int) { diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 9df772f45..84f468c15 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -10,7 +10,7 @@ import UIKit import RxSwift import RxCocoa -protocol BoardDetailViewModelType { +protocol BoardDetailViewModelType: BoardDetailViewModel { // Input @@ -27,10 +27,7 @@ protocol BoardDetailViewModelType { var targetIDObserver: AnyObserver { get } /// 삭제 버튼을 누른 경우 해당 옵저버를 이용하여 emit합니다. - var deleteOptionObserver: AnyObserver { get } - - /// 수정 버튼을 누른 경우 해당 옵저버를 이용하여 emit합니다. - var editOptionObserver: AnyObserver { get } + var commentOptionObserver: AnyObserver { get } //Output @@ -80,9 +77,8 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let decoratorNameSubject = PublishSubject<(labelText: String, buttonText: String)>() private let bottomCellSubject = PublishSubject<(collectionViewHeight: CGFloat, offset: CGFloat)>() private let showBottomSheetSubject = PublishSubject<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)>() - private let deleteOptionSubject = PublishSubject() - private let editOptionSubject = PublishSubject() private let targetIDSubject = BehaviorSubject(value: nil) + private let commentOptionSubject = BehaviorSubject(value: .commentOrReply) // MARK: - Initializations @@ -143,6 +139,10 @@ extension BoardDetailViewModel { /// - commentsObservable: 작성된 문자열과 부모 ID를 갖는 Observable private func createComments(plubbingID: Int, content: BoardModel) { commentInputSubject + .filter { [commentOptionSubject] _ in + let value = try? commentOptionSubject.value() + return value == .commentOrReply + } .withLatestFrom(targetIDSubject) { comment, parentID in (comment: comment, parentID: parentID) } @@ -196,7 +196,8 @@ extension BoardDetailViewModel { /// - plubbingID: 플러빙 ID /// - content: 게시글 컨텐츠 모델 private func deleteComments(plubbingID: Int, content: BoardModel) { - deleteOptionSubject + commentOptionSubject + .filter { $0 == .delete } .withLatestFrom(targetIDSubject) .compactMap { $0 } .flatMap { [deleteCommentUseCase] commentID in @@ -235,18 +236,15 @@ extension BoardDetailViewModel: BoardDetailViewModelType { var targetIDObserver: AnyObserver { targetIDSubject.asObserver() } + var commentOptionObserver: AnyObserver { + commentOptionSubject.asObserver() + } var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { decoratorNameSubject.asObservable() } var showBottomSheetObservable: Observable<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)> { showBottomSheetSubject.asObservable() } - var deleteOptionObserver: AnyObserver { - deleteOptionSubject.asObserver() - } - var editOptionObserver: AnyObserver { - editOptionSubject.asObserver() - } } // MARK: - Diffable DataSource @@ -345,6 +343,9 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { else { return } + + // 현재 작성중인 옵션이 답글임을 명시 + commentOptionSubject.onNext(.commentOrReply) decoratorNameSubject.onNext((labelText: "\(commentValue.nickname)에게 답글 쓰는 중...", buttonText: "답글 작성 취소")) targetIDSubject.onNext(commentID) } @@ -369,6 +370,22 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { } } +// MARK: - DecoratorOption + +extension BoardDetailViewModel { + /// 댓글 옵션 열거형 + enum CommentOption { + /// 단순 댓글 및 답글 + case commentOrReply + + /// 댓글 수정 + case edit + + // 댓글 삭제 + case delete + } +} + // MARK: - Constants private extension BoardDetailViewModel { From d0b28029d28b281c2b24bd7172cedaeed5b862a2 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 16:26:18 +0900 Subject: [PATCH 06/12] [FEAT] Insert comment's text when try to edit comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 댓글을 수정할 수 있도록 기존 댓글 텍스트를 텍스트필드란에 삽입하는 코드를 구현했습니다. --- .../Clipboard/BoardDetailViewController.swift | 4 +++ .../Component/CommentInputView.swift | 9 +++++++ .../ViewModel/BoardDetailViewModel.swift | 26 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index 3fe0db029..4fa11a59b 100644 --- a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift @@ -126,6 +126,10 @@ final class BoardDetailViewController: BaseViewController { } .disposed(by: disposeBag) + viewModel.editCommentTextObservable + .bind(to: commentInputView.rx.commentText) + .disposed(by: disposeBag) + // ViewModel에게 `DiffableDataSource`처리를 해주기 위해 collectionView를 전달 viewModel.setCollectionViewObserver.onNext(collectionView) diff --git a/PLUB/Sources/Views/Home/Clipboard/Component/CommentInputView.swift b/PLUB/Sources/Views/Home/Clipboard/Component/CommentInputView.swift index 93b3bd74e..9651dda8b 100644 --- a/PLUB/Sources/Views/Home/Clipboard/Component/CommentInputView.swift +++ b/PLUB/Sources/Views/Home/Clipboard/Component/CommentInputView.swift @@ -24,6 +24,15 @@ final class CommentInputView: UIView { // MARK: - Properties + /// 댓글 작성란에 들어갈 텍스트를 입력합니다. + var commentText: String { + get { + textView.text + } set { + textView.text = newValue + } + } + private let disposeBag = DisposeBag() weak var delegate: CommentInputViewDelegate? diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 84f468c15..6a60d5f9b 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -31,6 +31,9 @@ protocol BoardDetailViewModelType: BoardDetailViewModel { //Output + /// 수정할 댓글의 정보를 전달합니다. + var editCommentTextObservable: Observable { get } + /// decoratorView에 들어갈 적절한 text를 처리합니다. var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { get } @@ -74,6 +77,7 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let collectionViewSubject = PublishSubject() private let commentInputSubject = PublishSubject() + private let editCommentTextSubject = PublishSubject() private let decoratorNameSubject = PublishSubject<(labelText: String, buttonText: String)>() private let bottomCellSubject = PublishSubject<(collectionViewHeight: CGFloat, offset: CGFloat)>() private let showBottomSheetSubject = PublishSubject<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)>() @@ -98,6 +102,7 @@ final class BoardDetailViewModel: BoardDetailDataStore { createComments(plubbingID: plubbingID, content: content) pagingSetup(plubbingID: plubbingID, content: content) deleteComments(plubbingID: plubbingID, content: content) + editComments(plubbingID: plubbingID, content: content) } private let disposeBag = DisposeBag() @@ -218,6 +223,24 @@ extension BoardDetailViewModel { } .disposed(by: disposeBag) } + + /// 댓글 수정 관련 파이프라인을 설정합니다. + private func editComments(plubbingID: Int, content: BoardModel) { + let editOption = commentOptionSubject.filter { $0 == .edit }.share() + + editOption + .map { _ in (labelText: "댓글 수정 중...", buttonText: "취소") } + .bind(to: decoratorNameSubject) + .disposed(by: disposeBag) + + editOption + .withLatestFrom(targetIDSubject) + .compactMap { [weak self] commentID in + self?.comments.first(where: { $0.commentID == commentID })?.content + } + .bind(to: editCommentTextSubject) + .disposed(by: disposeBag) + } } // MARK: - BoardDetailViewModelType @@ -230,6 +253,9 @@ extension BoardDetailViewModel: BoardDetailViewModelType { var commentsInput: AnyObserver { commentInputSubject.asObserver() } + var editCommentTextObservable: Observable { + editCommentTextSubject.asObservable() + } var offsetObserver: AnyObserver<(collectionViewHeight: CGFloat, offset: CGFloat)> { bottomCellSubject.asObserver() } From 2f74970bfe9b47b15e651a6c25d4299eddbf14fa Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 17:09:42 +0900 Subject: [PATCH 07/12] [ADD] Add EditCommentUseCase and inject to ViewModel --- PLUB.xcodeproj/project.pbxproj | 6 +++++- .../UseCases/Feeds/EditCommentUseCase.swift | 19 +++++++++++++++++++ .../Clipboard/ClipboardViewController.swift | 3 ++- .../ViewModel/BoardDetailViewModel.swift | 5 ++++- .../MainPage/MainPageViewController.swift | 3 ++- 5 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 PLUB/Sources/UseCases/Feeds/EditCommentUseCase.swift diff --git a/PLUB.xcodeproj/project.pbxproj b/PLUB.xcodeproj/project.pbxproj index f7758586d..afc1cd1d2 100644 --- a/PLUB.xcodeproj/project.pbxproj +++ b/PLUB.xcodeproj/project.pbxproj @@ -169,6 +169,7 @@ BA5D9EDB29E406AD00F06AB5 /* Day.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5D9EDA29E406AD00F06AB5 /* Day.swift */; }; BA5DEB302974D8E200650788 /* NetworkResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5DEB2F2974D8E200650788 /* NetworkResult.swift */; }; BA5DEB3329751E5F00650788 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = BA5DEB3229751E5F00650788 /* GoogleSignIn */; }; + BA5EC6C629EFD4E5000A68B7 /* EditCommentUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5EC6C529EFD4E5000A68B7 /* EditCommentUseCase.swift */; }; BA6D7C5329A7D46900D8E928 /* CommentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6D7C5229A7D46900D8E928 /* CommentsRequest.swift */; }; BA7255C12992445600A3E8F5 /* PLUBTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7255C02992445600A3E8F5 /* PLUBTabBarController.swift */; }; BA72BCF029BB03FF007165E5 /* BaseNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA72BCEF29BB03FF007165E5 /* BaseNavigationController.swift */; }; @@ -534,6 +535,7 @@ BA5D9ED829E3F16900F06AB5 /* ArchiveContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveContent.swift; sourceTree = ""; }; BA5D9EDA29E406AD00F06AB5 /* Day.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Day.swift; sourceTree = ""; }; BA5DEB2F2974D8E200650788 /* NetworkResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResult.swift; sourceTree = ""; }; + BA5EC6C529EFD4E5000A68B7 /* EditCommentUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EditCommentUseCase.swift; path = PLUB/Sources/UseCases/Feeds/EditCommentUseCase.swift; sourceTree = SOURCE_ROOT; }; BA6D7C5229A7D46900D8E928 /* CommentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsRequest.swift; sourceTree = ""; }; BA7255C02992445600A3E8F5 /* PLUBTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLUBTabBarController.swift; sourceTree = ""; }; BA72BCEF29BB03FF007165E5 /* BaseNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationController.swift; sourceTree = ""; }; @@ -1244,9 +1246,10 @@ BA4CCDF429EAB91D0040D0D7 /* Feeds */ = { isa = PBXGroup; children = ( - BAB2E56B29E30A13006B7BDC /* PostCommentUseCase.swift */, BA57F3F529ED30D200A9F790 /* DeleteCommentUseCase.swift */, + BA5EC6C529EFD4E5000A68B7 /* EditCommentUseCase.swift */, BAB2E56D29E30F96006B7BDC /* GetCommentsUseCase.swift */, + BAB2E56B29E30A13006B7BDC /* PostCommentUseCase.swift */, ); path = Feeds; sourceTree = ""; @@ -2368,6 +2371,7 @@ BA8E2B46297467A2009DDF32 /* AuthService.swift in Sources */, BAC923DF29546BF500385841 /* BirthViewController.swift in Sources */, C3EC027D29D9582D0024C962 /* WaitingViewController.swift in Sources */, + BA5EC6C629EFD4E5000A68B7 /* EditCommentUseCase.swift in Sources */, 70A420B329914B370026E9F9 /* SubCategoryListResponse.swift in Sources */, BA88125228E48A2F00BD832A /* HomeViewController.swift in Sources */, BA340E2529782356002BAF2C /* ProfileViewController.swift in Sources */, diff --git a/PLUB/Sources/UseCases/Feeds/EditCommentUseCase.swift b/PLUB/Sources/UseCases/Feeds/EditCommentUseCase.swift new file mode 100644 index 000000000..e6b609674 --- /dev/null +++ b/PLUB/Sources/UseCases/Feeds/EditCommentUseCase.swift @@ -0,0 +1,19 @@ +// +// EditCommentUseCase.swift +// PLUB +// +// Created by 홍승현 on 2023/04/19. +// + +import RxSwift + +protocol EditCommentUseCase { + func execute(plubbingID: Int, feedID: Int, commentID: Int, content: String) -> Observable +} + +final class DefaultEditCommentUseCase: EditCommentUseCase { + + func execute(plubbingID: Int, feedID: Int, commentID: Int, content: String) -> Observable { + FeedsService.shared.updateComment(plubbingID: plubbingID, feedID: feedID, commentID: commentID, comment: content) + } +} diff --git a/PLUB/Sources/Views/Home/Clipboard/ClipboardViewController.swift b/PLUB/Sources/Views/Home/Clipboard/ClipboardViewController.swift index cefb1a2e6..24e9e479c 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ClipboardViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ClipboardViewController.swift @@ -105,7 +105,8 @@ final class ClipboardViewController: BaseViewController { content: model.toBoardModel, getCommentsUseCase: DefaultGetCommentsUseCase(), postCommentUseCase: DefaultPostCommentUseCase(), - deleteCommentUseCase: DefaultDeleteCommentUseCase() + deleteCommentUseCase: DefaultDeleteCommentUseCase(), + editCommentUseCase: DefaultEditCommentUseCase() ) ), animated: true diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 6a60d5f9b..b154c09dc 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -72,6 +72,7 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let getCommentsUseCase: GetCommentsUseCase private let postCommentUseCase: PostCommentUseCase private let deleteCommentUseCase: DeleteCommentUseCase + private let editCommentUseCase: EditCommentUseCase // MARK: Subjects @@ -91,12 +92,14 @@ final class BoardDetailViewModel: BoardDetailDataStore { content: BoardModel, getCommentsUseCase: GetCommentsUseCase, postCommentUseCase: PostCommentUseCase, - deleteCommentUseCase: DeleteCommentUseCase + deleteCommentUseCase: DeleteCommentUseCase, + editCommentUseCase: EditCommentUseCase ) { self.content = content self.getCommentsUseCase = getCommentsUseCase self.postCommentUseCase = postCommentUseCase self.deleteCommentUseCase = deleteCommentUseCase + self.editCommentUseCase = editCommentUseCase fetchComments(plubbingID: plubbingID, content: content) createComments(plubbingID: plubbingID, content: content) diff --git a/PLUB/Sources/Views/Home/MainPage/MainPageViewController.swift b/PLUB/Sources/Views/Home/MainPage/MainPageViewController.swift index 09155f7ec..c3d57ceeb 100644 --- a/PLUB/Sources/Views/Home/MainPage/MainPageViewController.swift +++ b/PLUB/Sources/Views/Home/MainPage/MainPageViewController.swift @@ -247,7 +247,8 @@ extension MainPageViewController: BoardViewControllerDelegate { content: content, getCommentsUseCase: DefaultGetCommentsUseCase(), postCommentUseCase: DefaultPostCommentUseCase(), - deleteCommentUseCase: DefaultDeleteCommentUseCase() + deleteCommentUseCase: DefaultDeleteCommentUseCase(), + editCommentUseCase: DefaultEditCommentUseCase() ) ) vc.navigationItem.largeTitleDisplayMode = .never From dc77b711bb223b6b96644e2cc15534eeae637057 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 17:49:23 +0900 Subject: [PATCH 08/12] [FEAT] Implement comment editing feature --- .../Sources/Models/Feeds/CommentContent.swift | 2 +- .../ViewModel/BoardDetailViewModel.swift | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/PLUB/Sources/Models/Feeds/CommentContent.swift b/PLUB/Sources/Models/Feeds/CommentContent.swift index 75b7c26a9..387a2b970 100644 --- a/PLUB/Sources/Models/Feeds/CommentContent.swift +++ b/PLUB/Sources/Models/Feeds/CommentContent.swift @@ -24,7 +24,7 @@ struct CommentContent: Codable { let commentID: Int /// 댓글 내용 - let content: String + var content: String /// 프로필 이미지 URL let profileImageURL: String? diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index b154c09dc..0eccd4260 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -231,11 +231,13 @@ extension BoardDetailViewModel { private func editComments(plubbingID: Int, content: BoardModel) { let editOption = commentOptionSubject.filter { $0 == .edit }.share() + // 댓글 수정 시 decorator view의 label과 button 텍스트 수정 editOption .map { _ in (labelText: "댓글 수정 중...", buttonText: "취소") } .bind(to: decoratorNameSubject) .disposed(by: disposeBag) + // 댓글 수정 시 댓글작성란에 수정해야할 텍스트를 emit editOption .withLatestFrom(targetIDSubject) .compactMap { [weak self] commentID in @@ -243,6 +245,31 @@ extension BoardDetailViewModel { } .bind(to: editCommentTextSubject) .disposed(by: disposeBag) + + // 댓글 수정 로직 시나리오 + commentInputSubject + .filter { [commentOptionSubject] _ in + let value = try? commentOptionSubject.value() + return value == .edit + } + .withLatestFrom(targetIDSubject) { + (comment: $0, targetID: $1) + } + .compactMap { comment, targetID -> (comment: String, targetID: Int)? in + guard let targetID else { return nil } + return (comment: comment, targetID: targetID) + } + .flatMap { [editCommentUseCase] in + editCommentUseCase.execute(plubbingID: plubbingID, feedID: content.feedID, commentID: $0.targetID, content: $0.comment) + } + .subscribe(with: self) { owner, comment in + guard let index = owner.comments.firstIndex(where: { $0.commentID == comment.commentID }) + else { + return + } + owner.comments[index].content = comment.content + } + .disposed(by: disposeBag) } } From f8816956d52251ef22a605d5d855a8ebc3bbf34a Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 17:51:21 +0900 Subject: [PATCH 09/12] [FIX] Reset default values for targetID and commentOptions after API call issue --- .../Clipboard/ViewModel/BoardDetailViewModel.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 0eccd4260..67895a7d0 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -160,6 +160,10 @@ extension BoardDetailViewModel { .filter { [weak self] _ in return self?.pagingManager.isLast ?? false } + .do(onNext: { [weak self] _ in // API 호출을 위해 작업한 targetID와 commentOption을 기본값으로 초기화 + self?.targetIDSubject.onNext(nil) + self?.commentOptionSubject.onNext(.commentOrReply) + }) .subscribe(with: self) { owner, comment in // 일반 댓글은 단순 append if comment.type == .normal { @@ -211,6 +215,10 @@ extension BoardDetailViewModel { .flatMap { [deleteCommentUseCase] commentID in deleteCommentUseCase.execute(plubbingID: plubbingID, feedID: content.feedID, commentID: commentID) } + .do(onNext: { [weak self] _ in // API 호출을 위해 작업한 targetID와 commentOption을 기본값으로 초기화 + self?.targetIDSubject.onNext(nil) + self?.commentOptionSubject.onNext(.commentOrReply) + }) .withLatestFrom(targetIDSubject) .subscribe(with: self) { owner, commentID in guard let content = owner.comments.first(where: { $0.commentID == commentID }) else { return } @@ -262,6 +270,10 @@ extension BoardDetailViewModel { .flatMap { [editCommentUseCase] in editCommentUseCase.execute(plubbingID: plubbingID, feedID: content.feedID, commentID: $0.targetID, content: $0.comment) } + .do(onNext: { [weak self] _ in + self?.targetIDSubject.onNext(nil) + self?.commentOptionSubject.onNext(.commentOrReply) + }) .subscribe(with: self) { owner, comment in guard let index = owner.comments.firstIndex(where: { $0.commentID == comment.commentID }) else { From c5a2a0fbf5a8a08b867d2c92e236feddb99968c1 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 18:00:07 +0900 Subject: [PATCH 10/12] [FIX] Fix issue with Decorator View not disappearing after comment submission --- .../Sources/Views/Home/Clipboard/BoardDetailViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index 4fa11a59b..ab71d28d0 100644 --- a/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift +++ b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift @@ -168,6 +168,7 @@ extension BoardDetailViewController: CommentInputViewDelegate { func commentInputView(_ textView: UITextView, writtenText: String) { textView.text = "" viewModel.commentsInput.onNext(writtenText) + decoratorView.isHidden = true } } From a5856fcb0279233ac37db06d2e18e3bc2d181779 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 18:02:38 +0900 Subject: [PATCH 11/12] [ADD] Add Comments --- .../ViewModel/BoardDetailViewModel.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 67895a7d0..6979c6756 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -288,28 +288,39 @@ extension BoardDetailViewModel { // MARK: - BoardDetailViewModelType extension BoardDetailViewModel: BoardDetailViewModelType { + // Input + var setCollectionViewObserver: AnyObserver { collectionViewSubject.asObserver() } + var commentsInput: AnyObserver { commentInputSubject.asObserver() } - var editCommentTextObservable: Observable { - editCommentTextSubject.asObservable() - } + var offsetObserver: AnyObserver<(collectionViewHeight: CGFloat, offset: CGFloat)> { bottomCellSubject.asObserver() } + var targetIDObserver: AnyObserver { targetIDSubject.asObserver() } + var commentOptionObserver: AnyObserver { commentOptionSubject.asObserver() } + + // Output + + var editCommentTextObservable: Observable { + editCommentTextSubject.asObservable() + } + var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { decoratorNameSubject.asObservable() } + var showBottomSheetObservable: Observable<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)> { showBottomSheetSubject.asObservable() } From d0e153ffc4d5e4a3cc655398e86230214528afd3 Mon Sep 17 00:00:00 2001 From: SeungHyeon Hong Date: Wed, 19 Apr 2023 18:09:24 +0900 Subject: [PATCH 12/12] [FIX] Fix issue with View not updating after successful call to comment deletion API --- .../Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift index 6979c6756..a3c6d2499 100644 --- a/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift +++ b/PLUB/Sources/Views/Home/Clipboard/ViewModel/BoardDetailViewModel.swift @@ -215,11 +215,11 @@ extension BoardDetailViewModel { .flatMap { [deleteCommentUseCase] commentID in deleteCommentUseCase.execute(plubbingID: plubbingID, feedID: content.feedID, commentID: commentID) } + .withLatestFrom(targetIDSubject) .do(onNext: { [weak self] _ in // API 호출을 위해 작업한 targetID와 commentOption을 기본값으로 초기화 self?.targetIDSubject.onNext(nil) self?.commentOptionSubject.onNext(.commentOrReply) }) - .withLatestFrom(targetIDSubject) .subscribe(with: self) { owner, commentID in guard let content = owner.comments.first(where: { $0.commentID == commentID }) else { return } if content.type == .normal {