diff --git a/ByeBoo-iOS/ByeBoo-iOS/Data/Model/PostCommonQuestLikeResponseDTO.swift b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/PostCommonQuestLikeResponseDTO.swift new file mode 100644 index 00000000..4370f012 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/PostCommonQuestLikeResponseDTO.swift @@ -0,0 +1,19 @@ +// +// postCommonQuestLikeResponseDTO.swift +// ByeBoo-iOS +// +// Created by 이나연 on 6/10/26. +// + +import Foundation + +struct PostCommonQuestLikeResponseDTO: Decodable { + let likeCount: Int + let isLiked: Bool +} + +extension PostCommonQuestLikeResponseDTO { + func toEntity() -> CommonQuestLikeEntity { + .init(isLiked: isLiked, likeCount: likeCount) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Data/Network/EndPoint/CommonQuestAPI.swift b/ByeBoo-iOS/ByeBoo-iOS/Data/Network/EndPoint/CommonQuestAPI.swift index 1a83bec4..5468fc36 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Data/Network/EndPoint/CommonQuestAPI.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Data/Network/EndPoint/CommonQuestAPI.swift @@ -15,6 +15,7 @@ enum CommonQuestAPI { case fetchCommonQuestDetail(asnwerID: Int) case updateCommonQuest(answerID: Int, dto: UpdateCommonQuestRequestDTO) case deleteCommonQuest(answerID: Int) + case postCommonQuestLike(answerID: Int) } extension CommonQuestAPI: EndPoint { @@ -23,7 +24,7 @@ extension CommonQuestAPI: EndPoint { switch self { case .fetchCommonQuest, .fetchCommonQuestDetail: return "/api/v2/common-quests" - case .postCommonQuest, .updateCommonQuest, .deleteCommonQuest: + case .postCommonQuest, .updateCommonQuest, .deleteCommonQuest, .postCommonQuestLike: return "/api/v1/common-quests" } } @@ -36,12 +37,14 @@ extension CommonQuestAPI: EndPoint { return "" case .updateCommonQuest(let answerID, _), .deleteCommonQuest(let answerID), .fetchCommonQuestDetail(let answerID): return "/\(answerID)" + case .postCommonQuestLike(let answerID): + return "/\(answerID)/likes" } } var method: HTTPMethod { switch self { - case .postCommonQuest: + case .postCommonQuest, .postCommonQuestLike: return .post case .fetchCommonQuest, .fetchCommonQuestDetail: return .get @@ -54,14 +57,15 @@ extension CommonQuestAPI: EndPoint { var headers: HeaderType { switch self { - case .postCommonQuest, .fetchCommonQuest, .updateCommonQuest, .deleteCommonQuest, .fetchCommonQuestDetail: + case .postCommonQuest, .fetchCommonQuest, .updateCommonQuest, .deleteCommonQuest, .fetchCommonQuestDetail, + .postCommonQuestLike: return .withAuth } } var parameterEncoding: any ParameterEncoding { switch self { - case .postCommonQuest, .updateCommonQuest: + case .postCommonQuest, .updateCommonQuest, .postCommonQuestLike: return JSONEncoding.default case .fetchCommonQuest, .deleteCommonQuest, .fetchCommonQuestDetail: return URLEncoding.default @@ -70,7 +74,7 @@ extension CommonQuestAPI: EndPoint { var queryParameters: [String : String]? { switch self { - case .postCommonQuest, .updateCommonQuest, .deleteCommonQuest, .fetchCommonQuestDetail: + case .postCommonQuest, .updateCommonQuest, .deleteCommonQuest, .fetchCommonQuestDetail, .postCommonQuestLike: return nil case .fetchCommonQuest(let date, let cursor): if let cursor { @@ -87,7 +91,7 @@ extension CommonQuestAPI: EndPoint { switch self { case .postCommonQuest(_, let dto): return try? dto.toDictionary() - case .fetchCommonQuest, .deleteCommonQuest, .fetchCommonQuestDetail: + case .fetchCommonQuest, .deleteCommonQuest, .fetchCommonQuestDetail, .postCommonQuestLike: return nil case .updateCommonQuest(_, let dto): return try? dto.toDictionary() diff --git a/ByeBoo-iOS/ByeBoo-iOS/Data/Repository/CommonQuestRepository.swift b/ByeBoo-iOS/ByeBoo-iOS/Data/Repository/CommonQuestRepository.swift index 9057aacd..8a766109 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Data/Repository/CommonQuestRepository.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Data/Repository/CommonQuestRepository.swift @@ -74,4 +74,12 @@ struct DefaultCommonQuestRepository: CommonQuestInterface { return commonQuestDetail.toEntity(userID: userID) } + + func postCommonQuestLikes(answerID: Int) async throws -> CommonQuestLikeEntity { + let response = try await network.request( + CommonQuestAPI.postCommonQuestLike(answerID: answerID), + decodingType: PostCommonQuestLikeResponseDTO.self + ) + return response.toEntity() + } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Domain/DomainDependencyAssembler.swift b/ByeBoo-iOS/ByeBoo-iOS/Domain/DomainDependencyAssembler.swift index 462e36cc..8cca03fa 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Domain/DomainDependencyAssembler.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Domain/DomainDependencyAssembler.swift @@ -207,8 +207,13 @@ struct DomainDependencyAssembler: DependencyAssembler { DIContainer.shared.register(type: DeleteCommonQuestUseCase.self) { _ in return DefaultDeleteCommonQuestUseCase(repository: commonQuestRepository) } + DIContainer.shared.register(type: FetchCommonQuestDetailUseCase.self) { _ in return DefaultFetchCommonQuestDetailUseCase(repository: commonQuestRepository) } + + DIContainer.shared.register(type: PostCommonQuestLikeUseCase.self) { _ in + return DefaultPostCommonQuestLikeUseCase(repository: commonQuestRepository) + } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestLikeEntity.swift b/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestLikeEntity.swift new file mode 100644 index 00000000..9f30a2cf --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestLikeEntity.swift @@ -0,0 +1,13 @@ +// +// CommonQuestLikeEntity.swift +// ByeBoo-iOS +// +// Created by 이나연 on 6/13/26. +// + +import Foundation + +struct CommonQuestLikeEntity { + let isLiked: Bool + let likeCount: Int +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Domain/Interface/CommonQuestInterface.swift b/ByeBoo-iOS/ByeBoo-iOS/Domain/Interface/CommonQuestInterface.swift index d2647f63..478df520 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Domain/Interface/CommonQuestInterface.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Domain/Interface/CommonQuestInterface.swift @@ -13,4 +13,5 @@ protocol CommonQuestInterface { func updateCommonQuest(answerID: Int, answer: String) async throws func deleteCommonQuest(answerID: Int) async throws func fetchCommonQuestDetail(answerID: Int) async throws -> CommonQuestDetailEntity + func postCommonQuestLikes(answerID: Int) async throws -> CommonQuestLikeEntity } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Domain/UseCase/PostCommonQuestLikeUseCase.swift b/ByeBoo-iOS/ByeBoo-iOS/Domain/UseCase/PostCommonQuestLikeUseCase.swift new file mode 100644 index 00000000..50a0e7b4 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Domain/UseCase/PostCommonQuestLikeUseCase.swift @@ -0,0 +1,24 @@ +// +// PostCommonQuestLikeUseCase.swift +// ByeBoo-iOS +// +// Created by 이나연 on 6/10/26. +// + +import Foundation + +protocol PostCommonQuestLikeUseCase { + func execute(answerID: Int) async throws -> CommonQuestLikeEntity +} + +struct DefaultPostCommonQuestLikeUseCase: PostCommonQuestLikeUseCase { + private let repository: CommonQuestInterface + + init(repository: CommonQuestInterface) { + self.repository = repository + } + + func execute(answerID: Int) async throws -> CommonQuestLikeEntity { + return try await repository.postCommonQuestLikes(answerID: answerID) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift index ea9ea4a4..4ca91827 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift @@ -94,6 +94,7 @@ extension CommonQuestAnswerCell { userNicknameLabel.text = answer.writer answerID = answer.answerID questContentView.configure( + answerID: answer.answerID, content: answer.content, writtenAt: writtenAt, isLiked: answer.isLiked, diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift index 650ce1ec..9f6164c4 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift @@ -95,6 +95,7 @@ final class CommonQuestMyAnswerCell: UITableViewCell { extension CommonQuestMyAnswerCell { func bind( + answerID: Int, question: String, content: String, writtenAt: String, @@ -104,6 +105,7 @@ extension CommonQuestMyAnswerCell { ) { questionContentLabel.text = question questContentView.configure( + answerID: answerID, content: content, writtenAt: writtenAt, isLiked: isLiked, diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/QuestContentView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/QuestContentView.swift index c405fcc8..8306dc88 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/QuestContentView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/QuestContentView.swift @@ -8,7 +8,7 @@ import UIKit protocol CommonQuestLikeCommentProtocol: AnyObject { - func likeButtonDidTap() + func likeButtonDidTap(answerID: Int) } final class QuestContentView: BaseView { @@ -26,6 +26,7 @@ final class QuestContentView: BaseView { weak var delegate: CommonQuestLikeCommentProtocol? + private var answerID: Int = 0 private var likeCounts: Int = 0 override func setUI() { @@ -102,6 +103,7 @@ final class QuestContentView: BaseView { } func configure( + answerID: Int, content: String, writtenAt: String? = nil, isLiked: Bool, @@ -109,6 +111,7 @@ final class QuestContentView: BaseView { commentCount: Int, showAllText: Bool ) { + self.answerID = answerID self.likeCounts = likeCount answerContentTextView.do { $0.textContainer.maximumNumberOfLines = showAllText ? 0 : 2 @@ -123,11 +126,13 @@ final class QuestContentView: BaseView { commentCountLabel.text = String(commentCount) } + func updateUI(likeCount: Int, isLiked: Bool) { + likeCountLabel.text = String(likeCount) + likeButton.isSelected = isLiked + } + @objc private func likeButtonDidTap() { - likeButton.isSelected.toggle() - likeCounts += likeButton.isSelected ? 1 : -1 - likeCountLabel.text = String(likeCounts) - delegate?.likeButtonDidTap() + delegate?.likeButtonDidTap(answerID: self.answerID) } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift index d807133c..0554be48 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift @@ -23,7 +23,7 @@ final class CommonQuestHistoryView: BaseView { private let answerView = UIView() private let profileIconImageView = UIImageView() private let userNicknameLabel = UILabel() - private let questContentView = QuestContentView() + private(set) var questContentView = QuestContentView() private(set) var commentListView = SelfSizingTableView() private let commentTextView = CommentTextView() @@ -165,6 +165,7 @@ extension CommonQuestHistoryView { extension CommonQuestHistoryView { func configure( + answerID: Int, question: String, writtenAt: String, profileIcon: UIImage, @@ -177,6 +178,7 @@ extension CommonQuestHistoryView { questionContentLabel.text = question dateLabel.text = writtenAt questContentView.configure( + answerID: answerID, content: content, isLiked: isLiked, likeCount: likeCount, diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift index 8ff99038..8079f645 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift @@ -76,6 +76,7 @@ final class CommonQuestHistoryViewController: BaseViewController { $0.separatorStyle = .none $0.register(CommentTableViewCell.self) } + rootView.questContentView.delegate = self } } @@ -231,6 +232,19 @@ extension CommonQuestHistoryViewController { } } .store(in: &cancellable) + + viewModel.output.commonQuestLikeCountPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + switch result { + case .success(let result): + let entity = result.entity + self?.rootView.questContentView.updateUI(likeCount: entity.likeCount, isLiked: entity.isLiked) + case .failure(let error): + ByeBooLogger.error(error) + } + } + .store(in: &cancellable) } private func bindData(entity: CommonQuestDetailEntity) { @@ -238,6 +252,7 @@ extension CommonQuestHistoryViewController { self.writerID = answer.writerID rootView.configure( + answerID: answerID, question: entity.question, writtenAt: ServerDateFormatter.shared.relativeTimeString(from: answer.writtenAt) ?? "", //TODO: ViewModel로 수정 profileIcon: ProfileIcon.image(for: answer.profileIcon) ?? .relievedBadge, @@ -288,3 +303,9 @@ extension CommonQuestHistoryViewController: KeyboardHandleProtocol { } } } + +extension CommonQuestHistoryViewController: CommonQuestLikeCommentProtocol { + func likeButtonDidTap(answerID: Int) { + viewModel.action(.likeButtonDidTap(answerID: answerID)) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift index acfa8a41..c4abda6d 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift @@ -69,6 +69,7 @@ extension CommonQuestMyAnswersViewController { private func bind() { bindName() bindCommonQuestAnswers() + bindLikeCount() } private func bindName() { @@ -98,6 +99,38 @@ extension CommonQuestMyAnswersViewController { } .store(in: &cancellable) } + + private func bindLikeCount() { + viewModel.output.commonQuestLikeCountPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + switch result { + case .success(let result): + let entity = result.entity + self?.updateLikeCount( + answerID: result.answerID, + likeCount: entity.likeCount, + isLiked: entity.isLiked + ) + case .failure(let error): + ByeBooLogger.error(error) + } + } + .store(in: &cancellable) + } + + private func updateLikeCount(answerID: Int, likeCount: Int, isLiked: Bool) { + guard let answerIndex = viewModel.indexOfAnswer(answerID: answerID) else { + return + } + + let indexPath = IndexPath(row: 0, section: answerIndex) + guard let cell = rootView.answersTableView.cellForRow(at: indexPath) as? CommonQuestMyAnswerCell else { + return + } + + cell.questContentView.updateUI(likeCount: likeCount, isLiked: isLiked) + } } extension CommonQuestMyAnswersViewController: UITableViewDelegate { @@ -191,6 +224,7 @@ extension CommonQuestMyAnswersViewController: UITableViewDataSource { let cell: CommonQuestMyAnswerCell = tableView.dequeueReusableCell(for: indexPath) cell.questContentView.delegate = self cell.bind( + answerID: answer.answerID, question: answer.question, content: answer.content, writtenAt: answer.writtenAt, @@ -203,7 +237,7 @@ extension CommonQuestMyAnswersViewController: UITableViewDataSource { } extension CommonQuestMyAnswersViewController: CommonQuestLikeCommentProtocol { - func likeButtonDidTap() { - // TODO: like button + func likeButtonDidTap(answerID: Int) { + viewModel.action(.likeButtonDidTap(answerID: answerID)) } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift index 48dbb117..0f4efc65 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift @@ -86,6 +86,32 @@ extension CommonQuestViewController { } } .store(in: &cancellable) + + viewModel.output.commonQuestLikeCountPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + switch result { + case .success(let result): + let entity = result.entity + self?.updateLikeCount(answerID: result.answerID, likeCount: entity.likeCount, isLiked: entity.isLiked) + case .failure(let error): + ByeBooLogger.error(error) + } + } + .store(in: &cancellable) + } + + private func updateLikeCount(answerID: Int, likeCount: Int, isLiked: Bool) { + guard let answerIndex = viewModel.indexOfAnswer(answerID: answerID) else { + return + } + + let indexPath = IndexPath(row: answerIndex + 1, section: 0) + guard let cell = rootView.commonQuestTableView.cellForRow(at: indexPath) as? CommonQuestAnswerCell else { + return + } + + cell.questContentView.updateUI(likeCount: likeCount, isLiked: isLiked) } } @@ -266,7 +292,8 @@ extension CommonQuestViewController: UITableViewDataSource { } extension CommonQuestViewController: CommonQuestLikeCommentProtocol { - func likeButtonDidTap() { - // TODO: like button + func likeButtonDidTap(answerID: Int) { + ByeBooLogger.debug("answerID: \(answerID)") + viewModel.action(.likeButtonDidTap(answerID: answerID)) } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestHistoryViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestHistoryViewModel.swift index 9c0d1fe6..2edfd06d 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestHistoryViewModel.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestHistoryViewModel.swift @@ -10,20 +10,26 @@ import Foundation final class CommonQuestHistoryViewModel { private let fetchCommonQuestCommentsUseCase: FetchCommonQuestDetailUseCase + private let postCommonQuestLikeUseCase: PostCommonQuestLikeUseCase private let fetchCommentListSubject: PassthroughSubject, Never> = .init() + private let likeCountSubject = PassthroughSubject, Never>.init() private var entity: CommonQuestDetailEntity? = nil private var cancellables = Set() + private var likeTasks: [Int: Task] = [:] private(set) var output: Output init( - fetchCommonQuestCommentsUseCase: FetchCommonQuestDetailUseCase + fetchCommonQuestCommentsUseCase: FetchCommonQuestDetailUseCase, + postCommonQuestLikeUseCase: PostCommonQuestLikeUseCase ) { self.fetchCommonQuestCommentsUseCase = fetchCommonQuestCommentsUseCase + self.postCommonQuestLikeUseCase = postCommonQuestLikeUseCase output = Output( - fetchCommonQuestDetailPublisher: fetchCommentListSubject.eraseToAnyPublisher() + fetchCommonQuestDetailPublisher: fetchCommentListSubject.eraseToAnyPublisher(), + commonQuestLikeCountPublisher: likeCountSubject.eraseToAnyPublisher() ) } } @@ -31,16 +37,20 @@ final class CommonQuestHistoryViewModel { extension CommonQuestHistoryViewModel: ViewModelType { enum Input { case viewWillAppear(answerID: Int) + case likeButtonDidTap(answerID: Int) } struct Output { let fetchCommonQuestDetailPublisher: AnyPublisher, Never> + let commonQuestLikeCountPublisher: AnyPublisher, Never> } func action(_ trigger: Input) { switch trigger { case .viewWillAppear(let answerID): fetchCommonQuestComments(answerID: answerID) + case .likeButtonDidTap(let answerID): + postCommonQuestLike(answerID: answerID) } } } @@ -72,4 +82,25 @@ extension CommonQuestHistoryViewModel { } } } + + private func postCommonQuestLike(answerID: Int) { + likeTasks[answerID]?.cancel() + + likeTasks[answerID] = Task { + do { + let entity = try await postCommonQuestLikeUseCase.execute(answerID: answerID) + try Task.checkCancellation() + likeCountSubject.send(.success((answerID, entity))) + } catch is CancellationError { + ByeBooLogger.debug("Task 취소됨") + } catch { + guard let error = error as? ByeBooError else { + return + } + guard !Task.isCancelled else { return } + likeCountSubject.send(.failure(error)) + } + likeTasks[answerID] = nil + } + } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestMyAnswerViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestMyAnswerViewModel.swift index a41dafa8..6a2ed058 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestMyAnswerViewModel.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestMyAnswerViewModel.swift @@ -13,8 +13,12 @@ final class CommonQuestMyAnswerViewModel { private let cancellables = Set() private let nameSubject = PassthroughSubject, Never>.init() private let answersSubject = PassthroughSubject, Never>.init() + private let likeCountSubject = PassthroughSubject, Never>.init() + private let getUserNameUseCase: GetUserNameUseCase private let fetchCommonQuestMyAnswersUseCase: FetchCommonQuestMyAnswersUseCase + private let postCommonQuestLikeUseCase: PostCommonQuestLikeUseCase + private(set) var output: Output private var commonQuestAnswers: CommonQuestMyAnswersEntity? @@ -22,15 +26,21 @@ final class CommonQuestMyAnswerViewModel { private(set) var hasMorePages = true private var nextCursor: Int? = nil + private var likeTasks: [Int: Task] = [:] + init( getUserNameUseCase: GetUserNameUseCase, - fetchCommonQuestMyAnswersUseCase: FetchCommonQuestMyAnswersUseCase + fetchCommonQuestMyAnswersUseCase: FetchCommonQuestMyAnswersUseCase, + postCommonQuestLikeUseCase: PostCommonQuestLikeUseCase ) { self.getUserNameUseCase = getUserNameUseCase self.fetchCommonQuestMyAnswersUseCase = fetchCommonQuestMyAnswersUseCase + self.postCommonQuestLikeUseCase = postCommonQuestLikeUseCase + self.output = Output( namePublisher: nameSubject.eraseToAnyPublisher(), - answersPublisher: answersSubject.eraseToAnyPublisher() + answersPublisher: answersSubject.eraseToAnyPublisher(), + commonQuestLikeCountPublisher: likeCountSubject.eraseToAnyPublisher() ) } @@ -58,6 +68,25 @@ final class CommonQuestMyAnswerViewModel { } } } + + private func postCommonQuestLike(answerID: Int) { + likeTasks[answerID]?.cancel() + + likeTasks[answerID] = Task { + do { + let entity = try await postCommonQuestLikeUseCase.execute(answerID: answerID) + guard !Task.isCancelled else { return } + likeCountSubject.send(.success((answerID: answerID, entity))) + } catch { + guard let error = error as? ByeBooError else { + return + } + guard !Task.isCancelled else { return } + likeCountSubject.send(.failure(error)) + } + likeTasks[answerID] = nil + } + } } extension CommonQuestMyAnswerViewModel: ViewModelType { @@ -65,11 +94,13 @@ extension CommonQuestMyAnswerViewModel: ViewModelType { enum Input { case viewWillAppear case scrollAnswer + case likeButtonDidTap(answerID: Int) } struct Output { let namePublisher: AnyPublisher, Never> let answersPublisher: AnyPublisher, Never> + let commonQuestLikeCountPublisher: AnyPublisher, Never> } func action(_ trigger: Input) { @@ -79,6 +110,8 @@ extension CommonQuestMyAnswerViewModel: ViewModelType { fetchUserCommonQuestAnswers() case .scrollAnswer: fetchUserCommonQuestAnswers(cursor: nextCursor) + case .likeButtonDidTap(let answerID): + postCommonQuestLike(answerID: answerID) } } } @@ -89,6 +122,10 @@ extension CommonQuestMyAnswerViewModel { answers.count } + func indexOfAnswer(answerID: Int) -> Int? { + answers.firstIndex { $0.answerID == answerID } + } + func getAnswer(at index: Int) -> CommonQuestMyAnswerEntity? { guard index >= 0 && index < answersCount else { return nil diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift index 1c84b4e6..86454f62 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift @@ -10,9 +10,12 @@ import UIKit final class CommonQuestViewModel { - private let cancellables = Set() + private var cancellables = Set() private let commonQuestSubject = PassthroughSubject, Never>.init() + private let likeCountSubject = PassthroughSubject, Never>.init() + private let fetchCommonQuestByDateUseCase: FetchCommonQuestByDateUseCase + private let postCommonQuestLikeUseCase: PostCommonQuestLikeUseCase private let minute: Double = 60 private let hour: Double = 3600 private let day: Double = 86400 @@ -23,11 +26,18 @@ final class CommonQuestViewModel { private(set) var hasMorePages = true private var nextCursor: Int? = nil private var currentDate: String = DateFormatter.toAPIDateString(from: .now) + private var likeTasks: [Int: Task] = [:] - init(fetchCommonQuestByDateUseCase: FetchCommonQuestByDateUseCase) { + init( + fetchCommonQuestByDateUseCase: FetchCommonQuestByDateUseCase, + postCommonQuestLikeUseCase: PostCommonQuestLikeUseCase + ) { self.fetchCommonQuestByDateUseCase = fetchCommonQuestByDateUseCase + self.postCommonQuestLikeUseCase = postCommonQuestLikeUseCase + self.output = Output( - commonQuestPublisher: commonQuestSubject.eraseToAnyPublisher() + commonQuestPublisher: commonQuestSubject.eraseToAnyPublisher(), + commonQuestLikeCountPublisher: likeCountSubject.eraseToAnyPublisher() ) } @@ -57,6 +67,25 @@ final class CommonQuestViewModel { } } } + + private func postCommonQuestLike(answerID: Int) { + likeTasks[answerID]?.cancel() + + likeTasks[answerID] = Task { + do { + let entity = try await postCommonQuestLikeUseCase.execute(answerID: answerID) + guard !Task.isCancelled else { return } + likeCountSubject.send(.success((answerID: answerID, entity))) + } catch { + guard let error = error as? ByeBooError else { + return + } + guard !Task.isCancelled else { return } + likeCountSubject.send(.failure(error)) + } + likeTasks[answerID] = nil + } + } } extension CommonQuestViewModel: ViewModelType { @@ -65,10 +94,12 @@ extension CommonQuestViewModel: ViewModelType { case viewWillAppear case moveDateButtonDidTap(selectedDate: String) case scrollAnswer + case likeButtonDidTap(answerID: Int) } struct Output { let commonQuestPublisher: AnyPublisher, Never> + let commonQuestLikeCountPublisher: AnyPublisher, Never> } func action(_ trigger: Input) { @@ -85,6 +116,8 @@ extension CommonQuestViewModel: ViewModelType { return } fetchCommonQuestByDate(date: currentDate, cursor: nextCursor) + case .likeButtonDidTap(let answerID): + postCommonQuestLike(answerID: answerID) } } } @@ -127,6 +160,10 @@ extension CommonQuestViewModel { } return answers[index].answerID } + + func indexOfAnswer(answerID: Int) -> Int? { + answers.firstIndex { $0.answerID == answerID } + } func getProfileIcon(at index: Int) -> UIImage? { guard index >= 0 && index < answers.count else { return nil } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift index 8b693e49..5d7c1297 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift @@ -286,26 +286,30 @@ struct PresentationDependencyAssembler: DependencyAssembler { } DIContainer.shared.register(type: CommonQuestViewModel.self) { container in - guard let fetchCommonQuestByDateUseCase = container.resolve(type: FetchCommonQuestByDateUseCase.self) else { + guard let fetchCommonQuestByDateUseCase = container.resolve(type: FetchCommonQuestByDateUseCase.self), + let postCommonQuestLikeUseCase = container.resolve(type: PostCommonQuestLikeUseCase.self) else { ByeBooLogger.error(ByeBooError.DIFailedError) return } return CommonQuestViewModel( - fetchCommonQuestByDateUseCase: fetchCommonQuestByDateUseCase + fetchCommonQuestByDateUseCase: fetchCommonQuestByDateUseCase, + postCommonQuestLikeUseCase: postCommonQuestLikeUseCase ) } DIContainer.shared.register(type: CommonQuestMyAnswerViewModel.self) { container in guard let getUserNameUseCase = container.resolve(type: GetUserNameUseCase.self), - let fetchCommonQuestMyAnswersUseCase = container.resolve(type: FetchCommonQuestMyAnswersUseCase.self) else { + let fetchCommonQuestMyAnswersUseCase = container.resolve(type: FetchCommonQuestMyAnswersUseCase.self), + let postCommonQuestLikeUseCase = container.resolve(type: PostCommonQuestLikeUseCase.self) else { ByeBooLogger.error(ByeBooError.DIFailedError) return } return CommonQuestMyAnswerViewModel( getUserNameUseCase: getUserNameUseCase, - fetchCommonQuestMyAnswersUseCase: fetchCommonQuestMyAnswersUseCase + fetchCommonQuestMyAnswersUseCase: fetchCommonQuestMyAnswersUseCase, + postCommonQuestLikeUseCase: postCommonQuestLikeUseCase ) } @@ -351,13 +355,15 @@ struct PresentationDependencyAssembler: DependencyAssembler { } DIContainer.shared.register(type: CommonQuestHistoryViewModel.self) { container in - guard let fetchCommonQuestDetailUseCase = container.resolve(type: FetchCommonQuestDetailUseCase.self) else { + guard let fetchCommonQuestDetailUseCase = container.resolve(type: FetchCommonQuestDetailUseCase.self), + let postCommonQuestLikeUseCase = container.resolve(type: PostCommonQuestLikeUseCase.self) else { ByeBooLogger.error(ByeBooError.DIFailedError) return } return CommonQuestHistoryViewModel( - fetchCommonQuestCommentsUseCase: fetchCommonQuestDetailUseCase + fetchCommonQuestCommentsUseCase: fetchCommonQuestDetailUseCase, + postCommonQuestLikeUseCase: postCommonQuestLikeUseCase ) } }