diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentReplyFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentReplyFragment.kt index ca280d29..6e086d0e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentReplyFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentReplyFragment.kt @@ -13,8 +13,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.CircleCropTransformation -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.schedulers.Schedulers import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.common.LoadingDialog @@ -33,17 +31,15 @@ class CharacterCommentReplyFragment : BaseFragment - if (resp.success) { - // 성공 시 낙관적 UI 반영: 기존 스텁과 동일하게 즉시 목록에 추가 - val me = CharacterReplyResponse( - replyId = System.currentTimeMillis(), - memberId = SharedPreferenceManager.userId, - memberProfileImage = SharedPreferenceManager.profileImage, - memberNickname = SharedPreferenceManager.nickname, - createdAt = System.currentTimeMillis(), - comment = text - ) - val insertAt = adapter.items.size - adapter.items.add(me) - adapter.notifyItemInserted(insertAt) - binding.rvCommentReply.scrollToPosition(adapter.items.size - 1) - binding.etComment.setText("") - } else { - Toast.makeText( - requireContext(), - resp.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - } - }, { e -> - Toast.makeText( - requireContext(), - e.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - }) - compositeDisposable.add(d) + viewModel.createReply(characterId, text) + binding.etComment.setText("") } adapter = CharacterCommentReplyAdapter( @@ -146,36 +103,7 @@ class CharacterCommentReplyFragment : BaseFragment - val token = "Bearer ${SharedPreferenceManager.token}" - val d = repository.reportComment( - characterId = characterId, - commentId = reply.replyId, - reason = reason, - token = token - ).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ resp -> - if (resp.success) { - Toast.makeText( - requireContext(), - "신고가 접수되었습니다.", - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - requireContext(), - resp.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - } - }, { e -> - Toast.makeText( - requireContext(), - e.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - }) - compositeDisposable.add(d) + viewModel.reportReply(characterId, reply.replyId, reason) } reportSheet.show(parentFragmentManager, "reply_report") } @@ -184,41 +112,7 @@ class CharacterCommentReplyFragment : BaseFragment - val token = "Bearer ${SharedPreferenceManager.token}" - loadingDialog.show(screenWidth) - val d = repository.deleteComment( - characterId = characterId, - commentId = reply.replyId, - token = token - ).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doFinally { loadingDialog.dismiss() } - .subscribe({ resp -> - if (resp.success) { - val index = adapter.items - .indexOfFirst { - it is CharacterReplyResponse - && it.replyId == reply.replyId - } - if (index > 0) { - adapter.items.removeAt(index) - adapter.notifyItemRemoved(index) - } - } else { - Toast.makeText( - requireActivity(), - resp.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - } - }, { e -> - Toast.makeText( - requireActivity(), - e.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - }) - compositeDisposable.add(d) + viewModel.deleteReply(characterId, reply.replyId) } .setNegativeButton(getString(R.string.cancel), null) .show() @@ -265,10 +159,8 @@ class CharacterCommentReplyFragment : BaseFragment - if (resp.success) { - val data = resp.data - // 서버에서 original 포함되지만, 이미 헤더에 추가되어 있으므로 replies만 사용 - val replies = data?.replies ?: emptyList() - if (replies.isNotEmpty()) { - val start = adapter.items.size - adapter.items.addAll(replies) - adapter.notifyItemRangeInserted(start, replies.size) - } - cursor = data?.cursor - } else { - Toast.makeText( - requireContext(), - resp.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - } - }, { e -> - Toast.makeText( - requireContext(), - e.message ?: "요청 중 오류가 발생했습니다", - Toast.LENGTH_SHORT - ).show() - }) - compositeDisposable.add(d) + private fun bindData() { + viewModel.isLoading.observe(viewLifecycleOwner) { loading -> + if (loading) loadingDialog.show(screenWidth) else loadingDialog.dismiss() + } + viewModel.toastLiveData.observe(viewLifecycleOwner) { msg -> + msg?.let { showToast(it) } + } + viewModel.replies.observe(viewLifecycleOwner) { list -> + // 헤더(원본 댓글)는 index 0에 유지, 나머지를 교체 + val header = if (adapter.items.isNotEmpty()) adapter.items.first() else original + adapter.items.clear() + header?.let { adapter.items.add(it) } + adapter.items.addAll(list) + adapter.notifyDataSetChanged() + // 스크롤을 하단으로 이동 (신규 추가 시 사용자에게 피드백) + if (adapter.itemCount > 0) { + binding.rvCommentReply.scrollToPosition(adapter.itemCount - 1) + } + } } + companion object { private const val EXTRA_CHARACTER_ID = "extra_character_id" private const val EXTRA_ORIGINAL_COMMENT_ID = "extra_original_comment_id" diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentReplyViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentReplyViewModel.kt new file mode 100644 index 00000000..782db391 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentReplyViewModel.kt @@ -0,0 +1,185 @@ +package kr.co.vividnext.sodalive.chat.character.comment + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class CharacterCommentReplyViewModel( + private val repository: CharacterCommentRepository +) : BaseViewModel() { + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData get() = _toastLiveData + + private val _isLoading = MutableLiveData(false) + val isLoading: LiveData get() = _isLoading + + private val _original = MutableLiveData() + val original: LiveData get() = _original + + private val _replies = MutableLiveData>(emptyList()) + val replies: LiveData> get() = _replies + + private var cursor: Long? = null + private var page: Int = 1 + + fun init(original: CharacterCommentResponse) { + _original.value = original + reset() + } + + private fun reset() { + cursor = null + page = 1 + _replies.value = emptyList() + } + + fun loadReplies(characterId: Long) { + val originalId = _original.value?.commentId ?: return + if (_isLoading.value == true) return + val onlyHeader = (_replies.value?.isEmpty() ?: true) + if (!onlyHeader && cursor == null) return + + _isLoading.value = true + val token = "Bearer ${SharedPreferenceManager.token}" + compositeDisposable.add( + repository.listReplies( + characterId = characterId, + commentId = originalId, + limit = 20, + cursor = cursor, + token = token + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ resp -> + _isLoading.value = false + if (resp.success && resp.data != null) { + val newReplies = resp.data.replies + val current = _replies.value ?: emptyList() + _replies.postValue(current + newReplies) + cursor = resp.data.cursor + page += 1 + } else { + val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, { e -> + _isLoading.value = false + Logger.e(e, "Character replies load failed") + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + }) + ) + } + + fun createReply(characterId: Long, comment: String) { + if (comment.isBlank()) { + _toastLiveData.postValue("내용을 입력하세요") + return + } + val originalId = _original.value?.commentId ?: return + if (_isLoading.value == true) return + _isLoading.value = true + + val token = "Bearer ${SharedPreferenceManager.token}" + compositeDisposable.add( + repository.createReply( + characterId = characterId, + commentId = originalId, + comment = comment, + token = token + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ resp -> + _isLoading.value = false + if (resp.success) { + // 낙관적 추가 + val me = CharacterReplyResponse( + replyId = System.currentTimeMillis(), + memberId = SharedPreferenceManager.userId, + memberProfileImage = SharedPreferenceManager.profileImage, + memberNickname = SharedPreferenceManager.nickname, + createdAt = System.currentTimeMillis(), + comment = comment + ) + val current = _replies.value ?: emptyList() + _replies.postValue(current + listOf(me)) + } else { + val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, { e -> + _isLoading.value = false + Logger.e(e, "Character reply create failed") + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + }) + ) + } + + fun deleteReply(characterId: Long, replyId: Long) { + if (_isLoading.value == true) return + _isLoading.value = true + val token = "Bearer ${SharedPreferenceManager.token}" + compositeDisposable.add( + repository.deleteComment( + characterId = characterId, + commentId = replyId, + token = token + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ resp -> + _isLoading.value = false + if (resp.success) { + val current = _replies.value ?: emptyList() + _replies.postValue(current.filterNot { it.replyId == replyId }) + } else { + val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, { e -> + _isLoading.value = false + Logger.e(e, "Character reply delete failed") + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + }) + ) + } + + fun reportReply(characterId: Long, replyId: Long, reason: String) { + if (reason.isBlank()) { + _toastLiveData.postValue("신고 사유를 입력하세요") + return + } + if (_isLoading.value == true) return + _isLoading.value = true + val token = "Bearer ${SharedPreferenceManager.token}" + compositeDisposable.add( + repository.reportComment( + characterId = characterId, + commentId = replyId, + reason = reason, + token = token + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ resp -> + _isLoading.value = false + if (resp.success) { + _toastLiveData.postValue("신고가 접수되었습니다.") + } else { + val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, { e -> + _isLoading.value = false + Logger.e(e, "Character reply report failed") + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + }) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index 9be10063..e166f46f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -360,6 +360,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { CharacterDetailViewModel(get()) } viewModel { TalkTabViewModel(get()) } viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListViewModel(get()) } + viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentReplyViewModel(get()) } } private val repositoryModule = module {