feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용
feat(character-comment): 답글 작성 API 연동 및 성공 시 낙관적 UI 반영 - CharacterCommentReplyFragment에 listReplies API 연동 - 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드 - isLoading 플래그로 중복 요청 방지 - 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가 - CharacterCommentReplyFragment에서 createReply API 호출로 스텁 제거 - 요청 중 로딩 다이얼로그 표시, 성공 시 입력 초기화 및 리스트에 즉시 추가 - 에러 처리(토스트) 적용
This commit is contained in:
		@@ -12,14 +12,14 @@ 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
 | 
			
		||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
 | 
			
		||||
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentReplyBinding
 | 
			
		||||
import kr.co.vividnext.sodalive.extensions.dpToPx
 | 
			
		||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
 | 
			
		||||
import io.reactivex.rxjava3.schedulers.Schedulers
 | 
			
		||||
import org.koin.android.ext.android.inject
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -93,7 +93,21 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
 | 
			
		||||
            hideKeyboard()
 | 
			
		||||
            val text = binding.etComment.text.toString()
 | 
			
		||||
            if (text.isBlank()) return@setOnClickListener
 | 
			
		||||
            // 스텁: 로컬에 즉시 추가
 | 
			
		||||
            val originalId = original?.commentId ?: return@setOnClickListener
 | 
			
		||||
            val token = "Bearer ${SharedPreferenceManager.token}"
 | 
			
		||||
            loadingDialog.show(screenWidth)
 | 
			
		||||
            val d = repository.createReply(
 | 
			
		||||
                characterId = characterId,
 | 
			
		||||
                commentId = originalId,
 | 
			
		||||
                comment = text,
 | 
			
		||||
                token = token
 | 
			
		||||
            )
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .doFinally { loadingDialog.dismiss() }
 | 
			
		||||
                .subscribe({ resp ->
 | 
			
		||||
                    if (resp.success) {
 | 
			
		||||
                        // 성공 시 낙관적 UI 반영: 기존 스텁과 동일하게 즉시 목록에 추가
 | 
			
		||||
                        val me = CharacterReplyResponse(
 | 
			
		||||
                            replyId = System.currentTimeMillis(),
 | 
			
		||||
                            memberId = SharedPreferenceManager.userId,
 | 
			
		||||
@@ -107,7 +121,21 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
 | 
			
		||||
                        adapter.notifyItemInserted(insertAt)
 | 
			
		||||
                        binding.rvCommentReply.scrollToPosition(adapter.items.size - 1)
 | 
			
		||||
                        binding.etComment.setText("")
 | 
			
		||||
            Toast.makeText(requireContext(), "등록되었습니다 (stub)", 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)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        adapter = CharacterCommentReplyAdapter(
 | 
			
		||||
@@ -134,16 +162,30 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
 | 
			
		||||
 | 
			
		||||
        val recyclerView = binding.rvCommentReply
 | 
			
		||||
        recyclerView.setHasFixedSize(true)
 | 
			
		||||
        recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
 | 
			
		||||
        recyclerView.layoutManager =
 | 
			
		||||
            LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
 | 
			
		||||
        recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
 | 
			
		||||
            override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
 | 
			
		||||
            override fun getItemOffsets(
 | 
			
		||||
                outRect: Rect,
 | 
			
		||||
                view: View,
 | 
			
		||||
                parent: RecyclerView,
 | 
			
		||||
                state: RecyclerView.State
 | 
			
		||||
            ) {
 | 
			
		||||
                super.getItemOffsets(outRect, view, parent, state)
 | 
			
		||||
                outRect.left = 13.3f.dpToPx().toInt()
 | 
			
		||||
                outRect.right = 13.3f.dpToPx().toInt()
 | 
			
		||||
                when (parent.getChildAdapterPosition(view)) {
 | 
			
		||||
                    0 -> { outRect.top = 13.3f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt() }
 | 
			
		||||
                    adapter.itemCount - 1 -> { outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 13.3f.dpToPx().toInt() }
 | 
			
		||||
                    else -> { outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt() }
 | 
			
		||||
                    0 -> {
 | 
			
		||||
                        outRect.top = 13.3f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt()
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    adapter.itemCount - 1 -> {
 | 
			
		||||
                        outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 13.3f.dpToPx().toInt()
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    else -> {
 | 
			
		||||
                        outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
@@ -218,12 +260,16 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
 | 
			
		||||
        private const val EXTRA_CHARACTER_ID = "extra_character_id"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_COMMENT_ID = "extra_original_comment_id"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_MEMBER_ID = "extra_original_member_id"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_MEMBER_PROFILE_IMAGE = "extra_original_member_profile_image"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_MEMBER_PROFILE_IMAGE =
 | 
			
		||||
            "extra_original_member_profile_image"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_MEMBER_NICKNAME = "extra_original_member_nickname"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_CREATED_AT = "extra_original_created_at"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_REPLY_COUNT = "extra_original_reply_count"
 | 
			
		||||
        private const val EXTRA_ORIGINAL_COMMENT_TEXT = "extra_original_comment_text"
 | 
			
		||||
        fun newInstance(characterId: Long, original: CharacterCommentResponse): CharacterCommentReplyFragment {
 | 
			
		||||
        fun newInstance(
 | 
			
		||||
            characterId: Long,
 | 
			
		||||
            original: CharacterCommentResponse
 | 
			
		||||
        ): CharacterCommentReplyFragment {
 | 
			
		||||
            val args = Bundle().apply {
 | 
			
		||||
                putLong(EXTRA_CHARACTER_ID, characterId)
 | 
			
		||||
                putLong(EXTRA_ORIGINAL_COMMENT_ID, original.commentId)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user