import Foundation import Combine import Moya final class CreatorChannelReplyDetailViewModel: ObservableObject { private let communityRepository = CreatorCommunityRepository() private let explorerRepository = ExplorerRepository() private let fanTalkRepository = CreatorChannelFanTalkRepository() private var subscription = Set() private var context: CreatorChannelReplyDetailContext? private var onFanTalkRefresh: (() -> Void)? private var communityReplyPage = 1 private let communityReplyPageSize = 20 private var communityReplyTotalCount = 0 private var isCommunityReplyLast = false private let fanTalkReplyPageSize = 20 @Published var replyText = "" @Published var parentItem: CreatorChannelReplyDetailDisplayItem? @Published var replies = [CreatorChannelReplyDetailDisplayItem]() @Published var isLoading = false @Published var isLoadingNextPage = false @Published var errorMessage = "" @Published var isShowPopup = false @Published var selectedActionItem: CreatorChannelReplyDetailDisplayItem? @Published var editingItem: CreatorChannelReplyDetailDisplayItem? @Published var deletingItem: CreatorChannelReplyDetailDisplayItem? @Published var isShowDeleteDialog = false var isSendEnabled: Bool { replyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false } var isEditing: Bool { editingItem != nil } func configure(context: CreatorChannelReplyDetailContext, onFanTalkRefresh: (() -> Void)?) { guard self.context == nil else { return } self.context = context self.onFanTalkRefresh = onFanTalkRefresh switch context { case .community(_, _, let parentComment): parentItem = CreatorChannelReplyDetailDisplayItem(communityComment: parentComment) fetchCommunityReplies(reset: true) case .fanTalk(let creatorId, let parentFanTalk): parentItem = CreatorChannelReplyDetailDisplayItem(fanTalk: parentFanTalk) replies = parentFanTalk.creatorReplies.map { CreatorChannelReplyDetailDisplayItem(fanTalkReply: $0, creatorId: creatorId) } } } func canEdit(_ item: CreatorChannelReplyDetailDisplayItem) -> Bool { switch item.source { case .community: return item.isMine case .fanTalk: guard item.id > 0 else { return false } return item.isMine || item.isCreatorReply } } func canDelete(_ item: CreatorChannelReplyDetailDisplayItem) -> Bool { switch item.source { case .community: if item.isMine { return true } if case .community(let creatorId, _, _) = context { return creatorId == UserDefaults.int(forKey: .userId) } return false case .fanTalk: guard item.id > 0 else { return false } if item.isMine || item.isCreatorReply { return true } if case .fanTalk(let creatorId, _) = context { return creatorId == UserDefaults.int(forKey: .userId) } return false } } func selectActionItem(_ item: CreatorChannelReplyDetailDisplayItem) { selectedActionItem = selectedActionItem?.id == item.id ? nil : item } func beginEditing(item: CreatorChannelReplyDetailDisplayItem) { guard canEdit(item) else { return } selectedActionItem = nil editingItem = item replyText = item.content } func cancelEditing() { editingItem = nil replyText = "" } func presentDeleteDialog(item: CreatorChannelReplyDetailDisplayItem) { guard canDelete(item) else { return } selectedActionItem = nil deletingItem = item isShowDeleteDialog = true } func send() { guard isSendEnabled, isLoading == false else { return } if let editingItem { modify(item: editingItem) } else { createReply() } } func confirmDelete() { guard let deletingItem, isLoading == false else { return } isShowDeleteDialog = false switch deletingItem.source { case .community: deleteCommunity(item: deletingItem) case .fanTalk: deleteFanTalk(item: deletingItem) } } func fetchCommunityReplies(reset: Bool = false) { guard case .community(_, _, let parentComment) = context else { return } guard isLoading == false, isLoadingNextPage == false else { return } if reset { communityReplyPage = 1 communityReplyTotalCount = 0 isCommunityReplyLast = false } else if isCommunityReplyLast { return } if reset { isLoading = true } else { isLoadingNextPage = true } let requestPage = communityReplyPage communityRepository.getCommentReplyList(commentId: parentComment.id, page: requestPage, size: communityReplyPageSize) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in guard let self else { return } self.isLoading = false self.isLoadingNextPage = false do { let decoded = try JSONDecoder().decode(ApiResponse.self, from: response.data) if let data = decoded.data, decoded.success { let newReplies = data.items.map { CreatorChannelReplyDetailDisplayItem(communityComment: $0) } if reset || requestPage == 1 { self.replies = newReplies } else { self.replies.append(contentsOf: newReplies) } self.communityReplyTotalCount = data.totalCount self.communityReplyPage = requestPage + 1 self.isCommunityReplyLast = self.replies.count >= data.totalCount || newReplies.isEmpty } else { self.errorMessage = decoded.message ?? I18n.Common.commonError self.isShowPopup = true } } catch { ERROR_LOG(error.localizedDescription) self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } .store(in: &subscription) } func fetchNextCommunityRepliesIfNeeded(currentItem: CreatorChannelReplyDetailDisplayItem) { guard currentItem == replies.last else { return } fetchCommunityReplies() } private func createReply() { let content = replyText.trimmingCharacters(in: .whitespacesAndNewlines) switch context { case .community(_, let postId, let parentComment): isLoading = true communityRepository.createCommunityPostComment( comment: content, postId: postId, parentId: parentComment.id, isSecret: parentComment.isSecret ) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in self?.handleCommunityMutationResponse(response) } .store(in: &subscription) case .fanTalk(let creatorId, let parentFanTalk): isLoading = true explorerRepository.writeCheers(parentCheersId: parentFanTalk.fanTalkId, creatorId: creatorId, content: content) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in self?.handleFanTalkMutationResponse(response) } .store(in: &subscription) case .none: return } } private func modify(item: CreatorChannelReplyDetailDisplayItem) { let content = replyText.trimmingCharacters(in: .whitespacesAndNewlines) guard content.isEmpty == false, content != item.content else { errorMessage = I18n.Explorer.noChanges isShowPopup = true return } switch item.source { case .community: isLoading = true var request = ModifyCommunityPostCommentRequest(commentId: item.id) request.comment = content communityRepository.modifyComment(request: request) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in self?.handleCommunityMutationResponse(response) } .store(in: &subscription) case .fanTalk: guard item.id > 0 else { return } isLoading = true explorerRepository.modifyCheers(cheersId: item.id, content: content, isActive: nil) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in self?.handleFanTalkMutationResponse(response) } .store(in: &subscription) } } private func deleteCommunity(item: CreatorChannelReplyDetailDisplayItem) { isLoading = true var request = ModifyCommunityPostCommentRequest(commentId: item.id) request.isActive = false communityRepository.modifyComment(request: request) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in self?.handleCommunityMutationResponse(response) } .store(in: &subscription) } private func deleteFanTalk(item: CreatorChannelReplyDetailDisplayItem) { guard item.id > 0 else { return } isLoading = true explorerRepository.modifyCheers(cheersId: item.id, content: nil, isActive: false) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in self?.handleFanTalkMutationResponse(response) } .store(in: &subscription) } private func fetchFanTalkReplies(page: Int = 0) { guard case .fanTalk(let creatorId, let parentFanTalk) = context else { return } fanTalkRepository.getFanTalks(creatorId: creatorId, page: page, size: fanTalkReplyPageSize) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in guard let self else { return } do { let decoded = try JSONDecoder().decode(ApiResponse.self, from: response.data) if let data = decoded.data, decoded.success { if let fanTalk = data.fanTalks.first(where: { $0.fanTalkId == parentFanTalk.fanTalkId }) { self.isLoading = false self.replies = fanTalk.creatorReplies.map { CreatorChannelReplyDetailDisplayItem(fanTalkReply: $0, creatorId: creatorId) } } else if data.hasNext { self.fetchFanTalkReplies(page: page + 1) } else { self.isLoading = false } } else { self.isLoading = false self.errorMessage = decoded.message ?? I18n.Common.commonError self.isShowPopup = true } } catch { ERROR_LOG(error.localizedDescription) self.isLoading = false self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } .store(in: &subscription) } private func handleCommunityMutationResponse(_ response: Moya.Response) { isLoading = false do { let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) if decoded.success { applyLocalSuccessState() fetchCommunityReplies(reset: true) } else { errorMessage = decoded.message ?? I18n.Common.commonError isShowPopup = true } } catch { ERROR_LOG(error.localizedDescription) errorMessage = I18n.Common.commonError isShowPopup = true } } private func handleFanTalkMutationResponse(_ response: Moya.Response) { isLoading = false do { let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) if decoded.success { applyLocalSuccessState() onFanTalkRefresh?() fetchFanTalkReplies() } else { errorMessage = decoded.message ?? I18n.Common.commonError isShowPopup = true } } catch { ERROR_LOG(error.localizedDescription) errorMessage = I18n.Common.commonError isShowPopup = true } } private func applyLocalSuccessState() { let trimmedReplyText = replyText.trimmingCharacters(in: .whitespacesAndNewlines) if let editingItem { let updatedItem = editingItem.updatingContent(trimmedReplyText) if parentItem?.id == editingItem.id && parentItem?.source == editingItem.source { parentItem = updatedItem } else if let index = replies.firstIndex(where: { $0.id == editingItem.id && $0.source == editingItem.source }) { replies[index] = updatedItem } } if let deletingItem { if parentItem?.id == deletingItem.id && parentItem?.source == deletingItem.source { AppState.shared.back() } else { replies.removeAll { $0.id == deletingItem.id && $0.source == deletingItem.source } } } replyText = "" editingItem = nil deletingItem = nil } private func applyFailureState() { isLoading = false errorMessage = I18n.Common.commonError isShowPopup = true } }