feat(creator-channel): FanTalk 상세 상태를 구현한다

This commit is contained in:
2026-07-09 07:43:59 +09:00
parent 6916459c9e
commit 04dcb3f09f
2 changed files with 564 additions and 0 deletions
@@ -0,0 +1,244 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
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.v2.creator.channel.CreatorChannelEvent
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkReplyUiModel
class CreatorChannelFanTalkDetailViewModel(
private val repository: CreatorChannelRepository,
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() {
private val _detailStateLiveData = MutableLiveData<CreatorChannelFanTalkDetailUiState>()
val detailStateLiveData: LiveData<CreatorChannelFanTalkDetailUiState>
get() = _detailStateLiveData
private val _replacementModalEventLiveData =
MutableLiveData<CreatorChannelEvent<CreatorChannelFanTalkReplacementModalUiModel>>()
val replacementModalEventLiveData: LiveData<CreatorChannelEvent<CreatorChannelFanTalkReplacementModalUiModel>>
get() = _replacementModalEventLiveData
private val _replyChangedEventLiveData = MutableLiveData(false)
val replyChangedEventLiveData: LiveData<Boolean>
get() = _replyChangedEventLiveData
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
get() = _toastLiveData
private var creatorId: Long = 0L
private var isSubmitting: Boolean = false
private var pendingReplacementContent: String? = null
fun load(creatorId: Long, payload: CreatorChannelFanTalkDetailPayload) {
this.creatorId = creatorId
val parentFanTalk = payload.toUiModel()
_detailStateLiveData.value = CreatorChannelFanTalkDetailUiState.Content(
parentFanTalk = parentFanTalk,
reply = parentFanTalk.reply
)
}
fun updateReplyInput(input: String) {
val content = currentContent() ?: return
_detailStateLiveData.value = content.copy(replyInput = input)
}
fun submitReply() {
val content = currentContent() ?: return
val trimmedContent = content.replyInput.trim()
if (trimmedContent.isEmpty() || isSubmitting) return
val editingTarget = content.editingTarget
if (editingTarget is CreatorChannelFanTalkDetailEditTarget.Reply) {
modifyReply(content, editingTarget.replyFanTalkId, trimmedContent)
return
}
val reply = content.reply
if (reply == null) {
writeReply(content, trimmedContent)
} else {
if (reply.fanTalkId <= 0L) {
showFailureToast()
return
}
pendingReplacementContent = trimmedContent
_replacementModalEventLiveData.value = CreatorChannelEvent(CreatorChannelFanTalkReplacementModalUiModel())
}
}
fun confirmReplacement() {
val content = currentContent() ?: return
val reply = content.reply ?: return
val replacementContent = pendingReplacementContent ?: content.replyInput.trim()
if (replacementContent.isBlank() || isSubmitting) return
if (reply.fanTalkId <= 0L) {
pendingReplacementContent = null
showFailureToast()
return
}
modifyReply(content, reply.fanTalkId, replacementContent)
pendingReplacementContent = null
}
fun cancelReplacement() {
pendingReplacementContent = null
}
fun consumeReplyChangedEvent() {
_replyChangedEventLiveData.value = false
}
fun cancelReplyEdit() {
val content = currentContent() ?: return
_detailStateLiveData.value = content.copy(replyInput = "", editingTarget = null)
}
fun startReplyEdit() {
val content = currentContent() ?: return
val reply = content.reply ?: return
_detailStateLiveData.value = content.copy(
replyInput = reply.content,
editingTarget = CreatorChannelFanTalkDetailEditTarget.Reply(reply.fanTalkId)
)
}
fun deleteReply() {
val content = currentContent() ?: return
val reply = content.reply ?: return
if (isSubmitting) return
submitMutation(content, repository.deleteFanTalkReply(reply.fanTalkId, authToken())) { current ->
current.copy(reply = null, replyInput = "", editingTarget = null)
}
}
private fun writeReply(content: CreatorChannelFanTalkDetailUiState.Content, replyContent: String) {
if (creatorId <= 0L || content.parentFanTalk.fanTalkId <= 0L) return
isSubmitting = true
_detailStateLiveData.value = content.copy(isSubmitting = true)
compositeDisposable.add(
repository.writeFanTalkReply(
fanTalkId = content.parentFanTalk.fanTalkId,
creatorId = creatorId,
content = replyContent,
token = authToken()
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response -> handleWriteReplyResponse(response) },
{ error -> handleMutationError(error) }
)
)
}
private fun modifyReply(
content: CreatorChannelFanTalkDetailUiState.Content,
replyFanTalkId: Long,
replyContent: String
) {
if (replyFanTalkId <= 0L) return
submitMutation(
content = content,
request = repository.modifyFanTalkReply(replyFanTalkId, replyContent, authToken())
) { current ->
current.copy(
reply = current.reply?.copy(content = replyContent),
replyInput = "",
editingTarget = null
)
}
}
private fun submitMutation(
content: CreatorChannelFanTalkDetailUiState.Content,
request: Single<ApiResponse<CreatorChannelFanTalkReplyResponse>>,
onSuccess: (CreatorChannelFanTalkDetailUiState.Content) -> CreatorChannelFanTalkDetailUiState.Content
) {
isSubmitting = true
_detailStateLiveData.value = content.copy(isSubmitting = true)
compositeDisposable.add(
request
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response -> handleMutationResponse(response, onSuccess) },
{ error -> handleMutationError(error) }
)
)
}
private fun handleWriteReplyResponse(response: ApiResponse<CreatorChannelFanTalkReplyResponse>) {
isSubmitting = false
val current = currentContent() ?: return
if (response.success) {
_detailStateLiveData.value = current.copy(
reply = response.data?.toFanTalkReplyUiModel(relativeTimeTextFormatter) ?: current.reply,
replyInput = "",
isSubmitting = false,
editingTarget = null
)
_replyChangedEventLiveData.value = true
} else {
showFailureToast(response.message)
_detailStateLiveData.value = current.copy(isSubmitting = false)
}
}
private fun handleMutationError(error: Throwable) {
isSubmitting = false
error.message?.let { Logger.e(it) }
val current = currentContent() ?: return
showFailureToast()
_detailStateLiveData.value = current.copy(isSubmitting = false)
}
private fun handleMutationResponse(
response: ApiResponse<CreatorChannelFanTalkReplyResponse>,
onSuccess: (CreatorChannelFanTalkDetailUiState.Content) -> CreatorChannelFanTalkDetailUiState.Content
) {
isSubmitting = false
val current = currentContent() ?: return
if (response.success) {
_detailStateLiveData.value = onSuccess(current).copy(isSubmitting = false)
_replyChangedEventLiveData.value = true
} else {
showFailureToast(response.message)
_detailStateLiveData.value = current.copy(isSubmitting = false)
}
}
private fun showFailureToast(message: String? = null) {
_toastLiveData.value = CreatorChannelEvent(
if (message.isNullOrBlank()) {
ToastMessage(resId = R.string.common_error_unknown)
} else {
ToastMessage(message = message)
}
)
}
private fun currentContent(): CreatorChannelFanTalkDetailUiState.Content? {
return _detailStateLiveData.value as? CreatorChannelFanTalkDetailUiState.Content
}
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
}