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,
 | 
				
			||||||
        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 {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user