feat(chat): Talk 탭에 RecyclerView 스크롤 페이지네이션 추가
- /api/chat/room/list 호출에 page 파라미터 적용 (0부터 시작) - ViewModel에 currentPage/lastPageReached 상태 추가 및 append 로직 구현 - Fragment에 스크롤 리스너로 바닥 근접 시 다음 페이지 자동 로드 - 빈 데이터 시 마지막 페이지로 간주하여 추가 로딩 중단
This commit is contained in:
		@@ -35,8 +35,8 @@ android {
 | 
				
			|||||||
        applicationId "kr.co.vividnext.sodalive"
 | 
					        applicationId "kr.co.vividnext.sodalive"
 | 
				
			||||||
        minSdk 23
 | 
					        minSdk 23
 | 
				
			||||||
        targetSdk 34
 | 
					        targetSdk 34
 | 
				
			||||||
        versionCode 181
 | 
					        versionCode 184
 | 
				
			||||||
        versionName "1.41.0"
 | 
					        versionName "1.42.0"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    buildTypes {
 | 
					    buildTypes {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,8 @@ import retrofit2.http.Query
 | 
				
			|||||||
interface TalkApi {
 | 
					interface TalkApi {
 | 
				
			||||||
    @GET("/api/chat/room/list")
 | 
					    @GET("/api/chat/room/list")
 | 
				
			||||||
    fun getTalkRooms(
 | 
					    fun getTalkRooms(
 | 
				
			||||||
        @Header("Authorization") authHeader: String
 | 
					        @Header("Authorization") authHeader: String,
 | 
				
			||||||
 | 
					        @Query("page") page: Int
 | 
				
			||||||
    ): Single<ApiResponse<List<TalkRoom>>>
 | 
					    ): Single<ApiResponse<List<TalkRoom>>>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @POST("/api/chat/room/create")
 | 
					    @POST("/api/chat/room/create")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,8 +29,8 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
 | 
				
			|||||||
        setupRecyclerView()
 | 
					        setupRecyclerView()
 | 
				
			||||||
        observeViewModel()
 | 
					        observeViewModel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 데이터 로드
 | 
					        // 데이터 로드 (스크롤 페이지네이션: 0페이지부터 시작)
 | 
				
			||||||
        viewModel.loadTalkRooms()
 | 
					        viewModel.refreshTalkRooms()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun setupRecyclerView() {
 | 
					    private fun setupRecyclerView() {
 | 
				
			||||||
@@ -78,8 +78,24 @@ class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        recyclerView.apply {
 | 
					        recyclerView.apply {
 | 
				
			||||||
            layoutManager = LinearLayoutManager(requireContext())
 | 
					            val lm = LinearLayoutManager(requireContext())
 | 
				
			||||||
 | 
					            layoutManager = lm
 | 
				
			||||||
            adapter = this@TalkTabFragment.adapter
 | 
					            adapter = this@TalkTabFragment.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 스크롤 로딩 리스너: 끝에 도달하면 다음 페이지 로드
 | 
				
			||||||
 | 
					            addOnScrollListener(object : RecyclerView.OnScrollListener() {
 | 
				
			||||||
 | 
					                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
 | 
				
			||||||
 | 
					                    super.onScrolled(recyclerView, dx, dy)
 | 
				
			||||||
 | 
					                    if (dy <= 0) return // 아래로 스크롤할 때만 체크
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    val totalItemCount = lm.itemCount
 | 
				
			||||||
 | 
					                    val lastVisible = lm.findLastVisibleItemPosition()
 | 
				
			||||||
 | 
					                    val threshold = 3 // 바닥 3개 전에서 미리 로드
 | 
				
			||||||
 | 
					                    if (totalItemCount > 0 && lastVisible >= totalItemCount - 1 - threshold) {
 | 
				
			||||||
 | 
					                        viewModel.loadNextPage()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.chat.talk
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class TalkTabRepository(private val api: TalkApi) {
 | 
					class TalkTabRepository(private val api: TalkApi) {
 | 
				
			||||||
    fun getTalkRooms(
 | 
					    fun getTalkRooms(
 | 
				
			||||||
        token: String
 | 
					        token: String,
 | 
				
			||||||
    ) = api.getTalkRooms(authHeader = token)
 | 
					        page: Int
 | 
				
			||||||
 | 
					    ) = api.getTalkRooms(authHeader = token, page = page)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,25 +20,51 @@ class TalkTabViewModel(private val repository: TalkTabRepository) : BaseViewMode
 | 
				
			|||||||
    val toastLiveData: LiveData<String?>
 | 
					    val toastLiveData: LiveData<String?>
 | 
				
			||||||
        get() = _toastLiveData
 | 
					        get() = _toastLiveData
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun loadTalkRooms() {
 | 
					    private var currentPage: Int = 0
 | 
				
			||||||
 | 
					    private var lastPageReached: Boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 초기화 후 첫 페이지를 로드한다. page는 0부터 시작한다.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    fun refreshTalkRooms() {
 | 
				
			||||||
 | 
					        currentPage = 0
 | 
				
			||||||
 | 
					        lastPageReached = false
 | 
				
			||||||
 | 
					        _talkRooms.value = emptyList()
 | 
				
			||||||
 | 
					        loadNextPage()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 다음 페이지를 로드한다. 빈 데이터를 반환하면 마지막 페이지로 간주한다.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    fun loadNextPage() {
 | 
				
			||||||
 | 
					        if (_isLoading.value == true || lastPageReached) return
 | 
				
			||||||
        _isLoading.value = true
 | 
					        _isLoading.value = true
 | 
				
			||||||
 | 
					        val token = "Bearer ${SharedPreferenceManager.token}"
 | 
				
			||||||
        compositeDisposable.add(
 | 
					        compositeDisposable.add(
 | 
				
			||||||
            repository.getTalkRooms(token = "Bearer ${SharedPreferenceManager.token}")
 | 
					            repository.getTalkRooms(token = token, page = currentPage)
 | 
				
			||||||
                .subscribeOn(Schedulers.io())
 | 
					                .subscribeOn(Schedulers.io())
 | 
				
			||||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
					                .observeOn(AndroidSchedulers.mainThread())
 | 
				
			||||||
                .subscribe(
 | 
					                .subscribe(
 | 
				
			||||||
                    { response ->
 | 
					                    { response ->
 | 
				
			||||||
                        _isLoading.value = false
 | 
					                        _isLoading.value = false
 | 
				
			||||||
                        if (response.success) {
 | 
					                        if (response.success) {
 | 
				
			||||||
                            _talkRooms.value = response.data ?: emptyList()
 | 
					                            val newItems = response.data ?: emptyList()
 | 
				
			||||||
 | 
					                            if (newItems.isEmpty()) {
 | 
				
			||||||
 | 
					                                // 마지막 페이지 도달
 | 
				
			||||||
 | 
					                                lastPageReached = true
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                val current = _talkRooms.value.orEmpty()
 | 
				
			||||||
 | 
					                                _talkRooms.value = current + newItems
 | 
				
			||||||
 | 
					                                currentPage += 1
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                            _toastLiveData.value =
 | 
					                            _toastLiveData.value =
 | 
				
			||||||
                                response.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
 | 
					                                response.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    {
 | 
					                    { throwable ->
 | 
				
			||||||
                        _isLoading.value = false
 | 
					                        _isLoading.value = false
 | 
				
			||||||
                        it.message?.let { message -> Logger.e(message) }
 | 
					                        throwable.message?.let { message -> Logger.e(message) }
 | 
				
			||||||
                        _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
 | 
					                        _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user