feat(creator-channel): 커뮤니티 답글 상태 관리를 추가한다

This commit is contained in:
2026-07-09 00:53:12 +09:00
parent c04d603ac0
commit c9e47214ea
2 changed files with 819 additions and 0 deletions

View File

@@ -0,0 +1,373 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityRepliesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
class CreatorChannelCommunityReplyViewModel(
private val repository: CreatorChannelRepository,
private val legacyRepository: CreatorCommunityRepository,
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() {
private val _replyStateLiveData = MutableLiveData<CreatorChannelCommunityReplyUiState>()
val replyStateLiveData: LiveData<CreatorChannelCommunityReplyUiState>
get() = _replyStateLiveData
private val _replyWrittenEventLiveData = MutableLiveData<Boolean>()
val replyWrittenEventLiveData: LiveData<Boolean>
get() = _replyWrittenEventLiveData
private val _replyChangedEventLiveData = MutableLiveData<Boolean>()
val replyChangedEventLiveData: LiveData<Boolean>
get() = _replyChangedEventLiveData
private val _parentCommentChangedEventLiveData = MutableLiveData<Boolean>()
val parentCommentChangedEventLiveData: LiveData<Boolean>
get() = _parentCommentChangedEventLiveData
private val _parentCommentDeletedEventLiveData = MutableLiveData<Boolean>()
val parentCommentDeletedEventLiveData: LiveData<Boolean>
get() = _parentCommentDeletedEventLiveData
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
get() = _toastLiveData
private var postId: Long = 0L
private var parentComment: CreatorChannelCommunityCommentUiModel? = null
private var replyRequestGeneration = 0
private var isLoadingReplies = false
private var isSendingReply = false
private var isModifyingReply = false
fun loadReplies(postId: Long, parentComment: CreatorChannelCommunityCommentUiModel) {
val formattedParentComment = parentComment.copy(
createdAtText = relativeTimeTextFormatter.format(parentComment.createdAtUtc)
)
this.postId = postId
this.parentComment = formattedParentComment
replyRequestGeneration++
isLoadingReplies = false
_replyStateLiveData.value = CreatorChannelCommunityReplyUiState.Content(
parentComment = formattedParentComment,
replies = emptyList(),
replyPage = FIRST_PAGE,
hasNextReply = false
)
if (formattedParentComment.commentId > 0) {
loadReplyPage(page = FIRST_PAGE, append = false)
}
}
fun loadMoreReplies() {
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
if (!content.hasNextReply || isLoadingReplies) return
loadReplyPage(page = content.replyPage + 1, append = true)
}
fun updateReplyInput(input: String) {
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
_replyStateLiveData.value = content.copy(replyInput = input)
}
fun submitReply() {
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
val comment = content.replyInput.trim()
val commentId = content.parentComment.commentId
if (postId <= 0 || commentId <= 0 || comment.isBlank() || isSendingReply) return
isSendingReply = true
_replyStateLiveData.value = content.copy(isSendingReply = true)
compositeDisposable.add(
legacyRepository.registerComment(
postId = postId,
comment = comment,
parentId = commentId,
isSecret = false,
token = authToken()
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response -> handleWriteResponse(response) },
{
isSendingReply = false
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
?: return@subscribe
showMutationFailureToast()
_replyStateLiveData.value = current.copy(isSendingReply = false)
}
)
)
}
fun consumeReplyWrittenEvent() {
_replyWrittenEventLiveData.value = false
}
fun consumeReplyChangedEvent() {
_replyChangedEventLiveData.value = false
}
fun consumeParentCommentChangedEvent() {
_parentCommentChangedEventLiveData.value = false
}
fun consumeParentCommentDeletedEvent() {
_parentCommentDeletedEventLiveData.value = false
}
fun modifyReply(replyId: Long, comment: String) {
val trimmedComment = comment.trim()
if (replyId <= 0 || isModifyingReply) return
if (trimmedComment.isBlank()) {
showBlankCommentToast()
return
}
modifyComment(ModifyCommentRequest(commentId = replyId, comment = trimmedComment))
}
fun deleteReply(replyId: Long) {
if (replyId <= 0 || isModifyingReply) return
modifyComment(ModifyCommentRequest(commentId = replyId, isActive = false))
}
fun modifyParentComment(comment: String) {
val parentComment = parentComment ?: return
val trimmedComment = comment.trim()
if (parentComment.commentId <= 0 || isModifyingReply) return
if (trimmedComment.isBlank()) {
showBlankCommentToast()
return
}
modifyParentComment(parentComment, trimmedComment)
}
fun deleteParentComment() {
val parentComment = parentComment ?: return
if (parentComment.commentId <= 0 || isModifyingReply) return
deleteParentComment(parentComment.commentId)
}
private fun modifyParentComment(parentComment: CreatorChannelCommunityCommentUiModel, comment: String) {
isModifyingReply = true
compositeDisposable.add(
legacyRepository.modifyComment(
ModifyCommentRequest(commentId = parentComment.commentId, comment = comment),
authToken()
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isModifyingReply = false
if (response.success) {
val updatedParent = parentComment.copy(comment = comment)
this.parentComment = updatedParent
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
?: return@subscribe
_replyStateLiveData.value = current.copy(parentComment = updatedParent)
_parentCommentChangedEventLiveData.value = true
} else {
showMutationFailureToast(response.message)
}
},
{
isModifyingReply = false
showMutationFailureToast()
}
)
)
}
private fun deleteParentComment(commentId: Long) {
isModifyingReply = true
compositeDisposable.add(
legacyRepository.modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false), authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isModifyingReply = false
if (response.success) {
_parentCommentDeletedEventLiveData.value = true
} else {
showMutationFailureToast(response.message)
}
},
{
isModifyingReply = false
showMutationFailureToast()
}
)
)
}
private fun loadReplyPage(page: Int, append: Boolean) {
val commentId = parentComment?.commentId ?: return
if (commentId <= 0 || isLoadingReplies && append) return
val generation = if (append) {
replyRequestGeneration
} else {
++replyRequestGeneration
}
isLoadingReplies = true
val beforeRequest = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
if (beforeRequest != null) {
_replyStateLiveData.value = beforeRequest.copy(isLoadingReplies = true)
}
compositeDisposable.add(
repository.getCommunityCommentReplies(commentId, page, DEFAULT_PAGE_SIZE, authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response -> handleRepliesResponse(response, append, generation) },
{
if (generation != replyRequestGeneration) return@subscribe
isLoadingReplies = false
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
?: return@subscribe
_replyStateLiveData.value = current.copy(isLoadingReplies = false)
}
)
)
}
private fun handleRepliesResponse(
response: ApiResponse<CreatorChannelCommunityRepliesResponse>,
append: Boolean,
generation: Int
) {
if (generation != replyRequestGeneration) return
isLoadingReplies = false
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
val data = response.data
if (!response.success || data == null) {
_replyStateLiveData.value = current.copy(isLoadingReplies = false)
return
}
val nextReplies = data.replies.map { it.toUiModel() }
_replyStateLiveData.value = current.copy(
replies = if (append) current.replies + nextReplies else nextReplies,
replyPage = data.page,
hasNextReply = data.hasNext,
isLoadingReplies = false
)
}
private fun handleWriteResponse(response: ApiResponse<Any>) {
isSendingReply = false
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
if (response.success) {
_replyWrittenEventLiveData.value = true
_replyChangedEventLiveData.value = true
_replyStateLiveData.value = current.copy(replyInput = "", isSendingReply = false)
loadReplyPage(page = FIRST_PAGE, append = false)
} else {
showMutationFailureToast(response.message)
_replyStateLiveData.value = current.copy(isSendingReply = false)
}
}
private fun modifyComment(request: ModifyCommentRequest) {
val commentId = parentComment?.commentId ?: return
if (commentId <= 0) return
isModifyingReply = true
compositeDisposable.add(
legacyRepository.modifyComment(request, authToken())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
isModifyingReply = false
if (response.success) {
_replyChangedEventLiveData.value = true
loadReplyPage(page = FIRST_PAGE, append = false)
} else {
showMutationFailureToast(response.message)
}
},
{
isModifyingReply = false
showMutationFailureToast()
}
)
)
}
private fun showMutationFailureToast(message: String? = null) {
_toastLiveData.value = CreatorChannelEvent(
if (message.isNullOrBlank()) {
ToastMessage(resId = R.string.common_error_unknown)
} else {
ToastMessage(message = message)
}
)
}
private fun showBlankCommentToast() {
_toastLiveData.value = CreatorChannelEvent(
ToastMessage(resId = R.string.screen_creator_community_write_content_hint)
)
}
private fun CreatorChannelCommunityReplyResponse.toUiModel() = CreatorChannelCommunityReplyUiModel(
replyId = commentId,
commentId = parentComment?.commentId ?: 0L,
memberId = writerId,
memberNickname = writerNickname,
memberProfileUrl = writerProfileImageUrl,
createdAtUtc = createdAtUtc,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
comment = content,
likeCount = 0,
isLiked = false,
isMine = writerId == SharedPreferenceManager.userId,
isCreatorOwner = parentComment?.isCreatorOwner == true
)
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
companion object {
const val DEFAULT_PAGE_SIZE = 20
private const val FIRST_PAGE = 0
}
}
sealed interface CreatorChannelCommunityReplyUiState {
data class Content(
val parentComment: CreatorChannelCommunityCommentUiModel,
val replies: List<CreatorChannelCommunityReplyUiModel>,
val replyPage: Int,
val hasNextReply: Boolean,
val isLoadingReplies: Boolean = false,
val replyInput: String = "",
val isSendingReply: Boolean = false
) : CreatorChannelCommunityReplyUiState
}