feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용
- CharacterCommentReplyFragment에 listReplies API 연동 - 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드 - isLoading 플래그로 중복 요청 방지 - 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가
This commit is contained in:
@@ -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,
|
||||||
|
token = token
|
||||||
)
|
)
|
||||||
}
|
.subscribeOn(Schedulers.io())
|
||||||
|
.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
|
val start = adapter.items.size
|
||||||
adapter.items.addAll(newItems)
|
adapter.items.addAll(replies)
|
||||||
adapter.notifyItemRangeInserted(start, newItems.size)
|
adapter.notifyItemRangeInserted(start, replies.size)
|
||||||
page++
|
}
|
||||||
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user