diff --git a/PLUB.xcodeproj/project.pbxproj b/PLUB.xcodeproj/project.pbxproj index d85ebbd82..afc1cd1d2 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 */; }; @@ -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 */; }; @@ -483,7 +484,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 = ""; }; @@ -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 = ""; @@ -1564,7 +1567,7 @@ children = ( BAB033F929B5C5580077C4D9 /* BoardDetailCollectionHeaderView.swift */, BAC9794429CF0E9E00A5DF1B /* CommentInputView.swift */, - BA034D3129E864CA003B7192 /* ReplyView.swift */, + BA034D3129E864CA003B7192 /* CommentOptionDecoratorView.swift */, BA4CCDF029EA79EA0040D0D7 /* CommentOptionBottomSheetViewController.swift */, ); path = Component; @@ -2255,7 +2258,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 */, @@ -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/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/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/BoardDetailViewController.swift b/PLUB/Sources/Views/Home/Clipboard/BoardDetailViewController.swift index 871a3531c..ab71d28d0 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,30 +102,34 @@ 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) 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) + viewModel.editCommentTextObservable + .bind(to: commentInputView.rx.commentText) + .disposed(by: disposeBag) + // ViewModel에게 `DiffableDataSource`처리를 해주기 위해 collectionView를 전달 viewModel.setCollectionViewObserver.onNext(collectionView) @@ -163,30 +168,36 @@ extension BoardDetailViewController: CommentInputViewDelegate { func commentInputView(_ textView: UITextView, writtenText: String) { textView.text = "" viewModel.commentsInput.onNext(writtenText) + decoratorView.isHidden = true } } -// MARK: - ReplyViewDelegate +// MARK: - CommentOptionViewDelegate -extension BoardDetailViewController: ReplyViewDelegate { +extension BoardDetailViewController: CommentOptionViewDelegate { func cancelButtonTapped() { - viewModel.replyIDObserver.onNext(nil) - replyView.isHidden = true + viewModel.commentOptionObserver.onNext(.commentOrReply) + viewModel.targetIDObserver.onNext(nil) + decoratorView.isHidden = true } } // MARK: - CommentOptionBottomSheetDelegate extension BoardDetailViewController: CommentOptionBottomSheetDelegate { - func deleteButtonTapped() { - viewModel.deleteOptionObserver.onNext(Void()) + func deleteButtonTapped(commentID: Int) { + // 순서 중요 + viewModel.targetIDObserver.onNext(commentID) + viewModel.commentOptionObserver.onNext(.delete) } - func editButtonTapped() { - print(#function) + func editButtonTapped(commentID: Int) { + // 순서 중요 + viewModel.targetIDObserver.onNext(commentID) + viewModel.commentOptionObserver.onNext(.edit) } - func reportButtonTapped() { + func reportButtonTapped(commentID: Int) { print(#function) } } 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/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/Component/CommentOptionBottomSheetViewController.swift b/PLUB/Sources/Views/Home/Clipboard/Component/CommentOptionBottomSheetViewController.swift index bb97fd2af..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,7 +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(commentID: owner.commentID) owner.dismiss(animated: true) } .disposed(by: disposeBag) 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 8a027b077..a3c6d2499 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 @@ -23,17 +23,21 @@ protocol BoardDetailViewModelType { /// 사용자의 댓글을 입력합니다. var commentsInput: AnyObserver { get } - /// 답장할 대상자의 ID를 emit합니다. - var replyIDObserver: AnyObserver { get } + /// 댓글 관련 작업을 처리할 대상자의 ID를 emit합니다. + var targetIDObserver: AnyObserver { get } - var deleteOptionObserver: AnyObserver { get } + /// 삭제 버튼을 누른 경우 해당 옵저버를 이용하여 emit합니다. + var commentOptionObserver: AnyObserver { get } //Output - /// 답장할 대상자의 닉네임을 받습니다. - var replyNicknameObserable: Observable { get } + /// 수정할 댓글의 정보를 전달합니다. + var editCommentTextObservable: Observable { get } - var showBottomSheetObservable: Observable { get } + /// decoratorView에 들어갈 적절한 text를 처리합니다. + var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { get } + + var showBottomSheetObservable: Observable<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)> { get } } protocol BoardDetailDataStore { @@ -68,17 +72,18 @@ final class BoardDetailViewModel: BoardDetailDataStore { private let getCommentsUseCase: GetCommentsUseCase private let postCommentUseCase: PostCommentUseCase private let deleteCommentUseCase: DeleteCommentUseCase + private let editCommentUseCase: EditCommentUseCase // MARK: Subjects private let collectionViewSubject = PublishSubject() private let commentInputSubject = PublishSubject() - private let replyIDSubject = BehaviorSubject(value: nil) - private let replyNicknameSubject = 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() - private let recentSelectedCommentIDSubject = ReplaySubject.create(bufferSize: 1) - private let deleteOptionSubject = PublishSubject() + private let showBottomSheetSubject = PublishSubject<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)>() + private let targetIDSubject = BehaviorSubject(value: nil) + private let commentOptionSubject = BehaviorSubject(value: .commentOrReply) // MARK: - Initializations @@ -87,17 +92,20 @@ 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) pagingSetup(plubbingID: plubbingID, content: content) deleteComments(plubbingID: plubbingID, content: content) + editComments(plubbingID: plubbingID, content: content) } private let disposeBag = DisposeBag() @@ -139,7 +147,11 @@ extension BoardDetailViewModel { /// - commentsObservable: 작성된 문자열과 부모 ID를 갖는 Observable private func createComments(plubbingID: Int, content: BoardModel) { commentInputSubject - .withLatestFrom(replyIDSubject) { comment, parentID in + .filter { [commentOptionSubject] _ in + let value = try? commentOptionSubject.value() + return value == .commentOrReply + } + .withLatestFrom(targetIDSubject) { comment, parentID in (comment: comment, parentID: parentID) } .flatMap { [postCommentUseCase] in @@ -148,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 { @@ -192,12 +208,18 @@ extension BoardDetailViewModel { /// - plubbingID: 플러빙 ID /// - content: 게시글 컨텐츠 모델 private func deleteComments(plubbingID: Int, content: BoardModel) { - deleteOptionSubject - .withLatestFrom(recentSelectedCommentIDSubject) + commentOptionSubject + .filter { $0 == .delete } + .withLatestFrom(targetIDSubject) + .compactMap { $0 } .flatMap { [deleteCommentUseCase] commentID in deleteCommentUseCase.execute(plubbingID: plubbingID, feedID: content.feedID, commentID: commentID) } - .withLatestFrom(recentSelectedCommentIDSubject) + .withLatestFrom(targetIDSubject) + .do(onNext: { [weak self] _ in // API 호출을 위해 작업한 targetID와 commentOption을 기본값으로 초기화 + self?.targetIDSubject.onNext(nil) + self?.commentOptionSubject.onNext(.commentOrReply) + }) .subscribe(with: self) { owner, commentID in guard let content = owner.comments.first(where: { $0.commentID == commentID }) else { return } if content.type == .normal { @@ -212,32 +234,95 @@ extension BoardDetailViewModel { } .disposed(by: disposeBag) } + + /// 댓글 수정 관련 파이프라인을 설정합니다. + 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 + self?.comments.first(where: { $0.commentID == commentID })?.content + } + .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) + } + .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 { + return + } + owner.comments[index].content = comment.content + } + .disposed(by: disposeBag) + } } // MARK: - BoardDetailViewModelType extension BoardDetailViewModel: BoardDetailViewModelType { + // Input + var setCollectionViewObserver: AnyObserver { collectionViewSubject.asObserver() } + var commentsInput: AnyObserver { commentInputSubject.asObserver() } + var offsetObserver: AnyObserver<(collectionViewHeight: CGFloat, offset: CGFloat)> { bottomCellSubject.asObserver() } - var replyIDObserver: AnyObserver { - replyIDSubject.asObserver() + + var targetIDObserver: AnyObserver { + targetIDSubject.asObserver() + } + + var commentOptionObserver: AnyObserver { + commentOptionSubject.asObserver() } - var replyNicknameObserable: Observable { - replyNicknameSubject.asObservable() + + // Output + + var editCommentTextObservable: Observable { + editCommentTextSubject.asObservable() } - var showBottomSheetObservable: Observable { - showBottomSheetSubject.asObservable() + + var decoratorNameObserable: Observable<(labelText: String, buttonText: String)> { + decoratorNameSubject.asObservable() } - var deleteOptionObserver: AnyObserver { - deleteOptionSubject.asObserver() + + var showBottomSheetObservable: Observable<(commentID: Int, userType: CommentOptionBottomSheetViewController.UserAccessType)> { + showBottomSheetSubject.asObservable() } } @@ -338,8 +423,10 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { return } - replyNicknameSubject.onNext(commentValue.nickname) - replyIDSubject.onNext(commentID) + // 현재 작성중인 옵션이 답글임을 명시 + commentOptionSubject.onNext(.commentOrReply) + decoratorNameSubject.onNext((labelText: "\(commentValue.nickname)에게 답글 쓰는 중...", buttonText: "답글 작성 취소")) + targetIDSubject.onNext(commentID) } func didTappedOptionButton(commentID: Int) { @@ -348,9 +435,6 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { return } - // 옵션 버튼을 선택한 셀의 commentID를 emit - recentSelectedCommentIDSubject.onNext(commentID) - let accessType: CommentOptionBottomSheetViewController.UserAccessType if content.isCommentAuthor { @@ -361,7 +445,23 @@ extension BoardDetailViewModel: BoardDetailCollectionViewCellDelegate { accessType = .normal } - showBottomSheetSubject.onNext(accessType) + showBottomSheetSubject.onNext((commentID, accessType)) + } +} + +// MARK: - DecoratorOption + +extension BoardDetailViewModel { + /// 댓글 옵션 열거형 + enum CommentOption { + /// 단순 댓글 및 답글 + case commentOrReply + + /// 댓글 수정 + case edit + + // 댓글 삭제 + case delete } } 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