feat(creator): 답글 상세 상태를 추가한다
This commit is contained in:
@@ -0,0 +1,416 @@
|
|||||||
|
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<AnyCancellable>()
|
||||||
|
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<GetCommunityPostCommentListResponse>.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<CreatorChannelFanTalkTabResponse>.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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user