From 9fa270da10e6e3e4b20d3fef29d791c3e72665ea Mon Sep 17 00:00:00 2001 From: klaus Date: Wed, 13 Aug 2025 23:30:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat-room):=207.2=20=EC=A0=90=EC=A7=84?= =?UTF-8?q?=EC=A0=81=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상단 스크롤 시 loadMoreMessages로 이전 메시지 로드 - 커서(timestamp) 기반 페이징 및 hasMore/nextCursor 상태 갱신 - messageId 기반 중복 제거, prepend 시 스크롤 위치 보정 --- .../chat/talk/room/ChatRoomActivity.kt | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt index da1ac217..9cfecc90 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt @@ -16,6 +16,7 @@ import coil.load import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.chat.character.detail.CharacterType +import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding import org.koin.android.ext.android.inject @@ -435,16 +436,65 @@ class ChatRoomActivity : BaseActivity( } } - /** 상단 도달 시 이전 메시지를 로드한다. Repository 연동은 추후(7.x) 예정. */ + /** 상단 도달 시 이전(더 오래된) 메시지를 서버에서 불러와 상단에 추가한다. */ private fun loadMoreMessages() { + if (isLoading) return isLoading = true - // TODO: 7.x에서 Repository 연동하여 서버에서 가져오기. 여기서는 구조만 구현. - // 예시: 가장 오래된 메시지 createdAt을 커서로 사용. - // val cursor = (items.lastOrNull() as? ChatListItem.UserMessage)?.data?.createdAt - // 현재 단계에서는 더 로드할 메시지가 없다고 가정하고 즉시 종료 - isLoading = false - hasMoreMessages = false + 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 = items.mapNotNull { + when (it) { + is ChatListItem.UserMessage -> it.data.messageId + is ChatListItem.AiMessage -> it.data.messageId + else -> null + } + }.toSet() + + val newChatItems: List = 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