feat(chat-room): 7.2 점진적 메시지 로딩 구현 및 중복 방지 처리
- 상단 스크롤 시 loadMoreMessages로 이전 메시지 로드 - 커서(timestamp) 기반 페이징 및 hasMore/nextCursor 상태 갱신 - messageId 기반 중복 제거, prepend 시 스크롤 위치 보정
This commit is contained in:
		@@ -16,6 +16,7 @@ import coil.load
 | 
				
			|||||||
import kr.co.vividnext.sodalive.R
 | 
					import kr.co.vividnext.sodalive.R
 | 
				
			||||||
import kr.co.vividnext.sodalive.base.BaseActivity
 | 
					import kr.co.vividnext.sodalive.base.BaseActivity
 | 
				
			||||||
import kr.co.vividnext.sodalive.chat.character.detail.CharacterType
 | 
					import kr.co.vividnext.sodalive.chat.character.detail.CharacterType
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.common.SharedPreferenceManager
 | 
				
			||||||
import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
 | 
					import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
 | 
				
			||||||
import org.koin.android.ext.android.inject
 | 
					import org.koin.android.ext.android.inject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -435,16 +436,65 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** 상단 도달 시 이전 메시지를 로드한다. Repository 연동은 추후(7.x) 예정. */
 | 
					    /** 상단 도달 시 이전(더 오래된) 메시지를 서버에서 불러와 상단에 추가한다. */
 | 
				
			||||||
    private fun loadMoreMessages() {
 | 
					    private fun loadMoreMessages() {
 | 
				
			||||||
 | 
					        if (isLoading) return
 | 
				
			||||||
        isLoading = true
 | 
					        isLoading = true
 | 
				
			||||||
        // TODO: 7.x에서 Repository 연동하여 서버에서 가져오기. 여기서는 구조만 구현.
 | 
					 | 
				
			||||||
        // 예시: 가장 오래된 메시지 createdAt을 커서로 사용.
 | 
					 | 
				
			||||||
        // val cursor = (items.lastOrNull() as? ChatListItem.UserMessage)?.data?.createdAt
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 현재 단계에서는 더 로드할 메시지가 없다고 가정하고 즉시 종료
 | 
					        val token = "Bearer ${SharedPreferenceManager.token}"
 | 
				
			||||||
        isLoading = false
 | 
					        // 커서: API에서 내려준 nextCursor 우선, 없으면 현재 목록 중 가장 오래된 createdAt
 | 
				
			||||||
        hasMoreMessages = false
 | 
					        val fallbackOldestCreatedAt: Long? = items.firstOrNull()?.let {
 | 
				
			||||||
 | 
					            when (it) {
 | 
				
			||||||
 | 
					                is ChatListItem.UserMessage -> it.data.createdAt
 | 
				
			||||||
 | 
					                is ChatListItem.AiMessage -> it.data.createdAt
 | 
				
			||||||
 | 
					                else -> null
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        val cursor: Long? = nextCursor ?: fallbackOldestCreatedAt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val disposable = chatRepository.loadMoreMessages(token = token, roomId = roomId, cursor = cursor)
 | 
				
			||||||
 | 
					            .observeOn(io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread())
 | 
				
			||||||
 | 
					            .subscribe({ response ->
 | 
				
			||||||
 | 
					                // 서버에서 받은 메시지(이전 것들)를 오래된 -> 최신 순으로 정렬
 | 
				
			||||||
 | 
					                val sorted = response.messages.sortedBy { it.createdAt }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 중복 제거: 기존 목록의 messageId 집합과 비교
 | 
				
			||||||
 | 
					                val existingIds: Set<Long> = items.mapNotNull {
 | 
				
			||||||
 | 
					                    when (it) {
 | 
				
			||||||
 | 
					                        is ChatListItem.UserMessage -> it.data.messageId
 | 
				
			||||||
 | 
					                        is ChatListItem.AiMessage -> it.data.messageId
 | 
				
			||||||
 | 
					                        else -> null
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }.toSet()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                val newChatItems: List<ChatListItem> = sorted
 | 
				
			||||||
 | 
					                    .map { it.toDomain() }
 | 
				
			||||||
 | 
					                    .filter { !existingIds.contains(it.messageId) }
 | 
				
			||||||
 | 
					                    .map { domain ->
 | 
				
			||||||
 | 
					                        if (domain.mine) ChatListItem.UserMessage(domain)
 | 
				
			||||||
 | 
					                        else ChatListItem.AiMessage(domain, characterInfo?.name)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 상단에 추가하면서 스크롤 위치 보정
 | 
				
			||||||
 | 
					                prependMessages(newChatItems)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 페이징 상태 갱신
 | 
				
			||||||
 | 
					                hasMoreMessages = response.hasMore
 | 
				
			||||||
 | 
					                nextCursor = response.nextCursor ?: newChatItems.firstOrNull()?.let {
 | 
				
			||||||
 | 
					                    when (it) {
 | 
				
			||||||
 | 
					                        is ChatListItem.UserMessage -> it.data.createdAt
 | 
				
			||||||
 | 
					                        is ChatListItem.AiMessage -> it.data.createdAt
 | 
				
			||||||
 | 
					                        else -> null
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                isLoading = false
 | 
				
			||||||
 | 
					            }, { error ->
 | 
				
			||||||
 | 
					                isLoading = false
 | 
				
			||||||
 | 
					                showToast(error.message ?: "이전 메시지를 불러오지 못했습니다.")
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        compositeDisposable.add(disposable)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // endregion
 | 
					    // endregion
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user