feat(chat-room): 7.2 점진적 메시지 로딩 구현 및 중복 방지 처리

- 상단 스크롤 시 loadMoreMessages로 이전 메시지 로드
- 커서(timestamp) 기반 페이징 및 hasMore/nextCursor 상태 갱신
- messageId 기반 중복 제거, prepend 시 스크롤 위치 보정
This commit is contained in:
2025-08-13 23:30:41 +09:00
parent 637595e8cd
commit 9fa270da10

View File

@@ -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}"
// 커서: API에서 내려준 nextCursor 우선, 없으면 현재 목록 중 가장 오래된 createdAt
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 isLoading = false
hasMoreMessages = false }, { error ->
isLoading = false
showToast(error.message ?: "이전 메시지를 불러오지 못했습니다.")
})
compositeDisposable.add(disposable)
} }
// endregion // endregion