feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용

- CharacterCommentReplyFragment에 listReplies API 연동
- 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드
- isLoading 플래그로 중복 요청 방지
- 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가
This commit is contained in:
2025-08-20 02:48:01 +09:00
parent ec315c4747
commit b995a0b151

View File

@@ -18,13 +18,15 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentReplyBinding import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentReplyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx 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
/** /**
* 캐릭터 댓글 답글 페이지 (Stub) * 캐릭터 댓글 답글 페이지
* - 상단: 뒤로가기(텍스트 + ic_back), 닫기(X) * - 상단: 뒤로가기(텍스트 + ic_back), 닫기(X)
* - 입력 폼, divider, 원본 댓글, 답글 목록(들여쓰기) * - 입력 폼, divider, 원본 댓글, 답글 목록(들여쓰기)
* - 스크롤 하단 도달 시 더미 데이터 추가 로드 * - 스크롤 하단 도달 시 무한 스크롤 로드 (초기 1회 호출 이후 cursor != null 일 때만 추가 로드)
* - API 연동은 추후 (현재 스텁)
*/ */
class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReplyBinding>( class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReplyBinding>(
FragmentCharacterCommentReplyBinding::inflate FragmentCharacterCommentReplyBinding::inflate
@@ -33,11 +35,13 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
private lateinit var imm: InputMethodManager private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: CharacterCommentReplyAdapter private lateinit var adapter: CharacterCommentReplyAdapter
private val repository: CharacterCommentRepository by inject()
private var original: CharacterCommentResponse? = null private var original: CharacterCommentResponse? = null
private var characterId: Long = 0 private var characterId: Long = 0
private var page: Int = 1 private var cursor: Long? = null
private var isLoading: Boolean = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -149,7 +153,9 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
val lm = recyclerView.layoutManager as LinearLayoutManager val lm = recyclerView.layoutManager as LinearLayoutManager
val last = lm.findLastCompletelyVisibleItemPosition() val last = lm.findLastCompletelyVisibleItemPosition()
val total = (recyclerView.adapter?.itemCount ?: 1) - 1 val total = (recyclerView.adapter?.itemCount ?: 1) - 1
if (!recyclerView.canScrollVertically(1) && last == total) { val onlyHeader = adapter.items.size <= 1
val canLoadMore = onlyHeader || cursor != null
if (canLoadMore && !recyclerView.canScrollVertically(1) && last == total) {
loadMore() loadMore()
} }
} }
@@ -161,26 +167,51 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
imm.hideSoftInputFromWindow(view?.windowToken, 0) imm.hideSoftInputFromWindow(view?.windowToken, 0)
} }
// 스텁 페이징: 더미 데이터 생성
private fun loadMore() { private fun loadMore() {
// 원본 댓글이 있어야 함
val originalId = original?.commentId ?: return val originalId = original?.commentId ?: return
val newItems = (1..10).map { idx -> if (isLoading) return
val id = page * 10_000L + idx // 초기 로드(헤더만 있는 상태)는 허용. 그 외에는 cursor가 null이면 더 이상 조회하지 않음
val writerId = if (idx % 5 == 0) SharedPreferenceManager.userId else -idx.toLong() val onlyHeader = adapter.items.size <= 1
CharacterReplyResponse( if (!onlyHeader && cursor == null) return
replyId = id,
memberId = writerId, val token = "Bearer ${SharedPreferenceManager.token}"
memberProfileImage = "", isLoading = true
memberNickname = "게스트$id", val d = repository.listReplies(
createdAt = System.currentTimeMillis() - (idx * 60_000L * page), characterId = characterId,
comment = "캐릭터 답글 예시 텍스트 $originalId-$id" commentId = originalId,
) limit = 20,
} cursor = cursor,
val start = adapter.items.size token = token
adapter.items.addAll(newItems) )
adapter.notifyItemRangeInserted(start, newItems.size) .subscribeOn(Schedulers.io())
page++ .observeOn(AndroidSchedulers.mainThread())
.doFinally { isLoading = false }
.subscribe({ resp ->
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)
} }
companion object { companion object {