feat(chat-room): 채팅 API 연동 및 전송/페이징 플로우 구현 완료
- TalkApi에 입장/전송/점진 로딩 엔드포인트 구현(9.1) - ChatRepository를 통한 서버 연동 및 로컬 동기화 추가 - ChatRoomActivity에서 입장/전송/페이징 연동, 타이핑 인디케이터/에러 처리 반영(9.2)
This commit is contained in:
@@ -5,6 +5,8 @@ import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomRequest
|
|||||||
import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomResponse
|
import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomResponse
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatMessagesResponse
|
import kr.co.vividnext.sodalive.chat.talk.room.ChatMessagesResponse
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomEnterResponse
|
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomEnterResponse
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.SendMessageRequest
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.ServerChatMessage
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
@@ -32,6 +34,14 @@ interface TalkApi {
|
|||||||
@Path("roomId") roomId: Long
|
@Path("roomId") roomId: Long
|
||||||
): Single<ApiResponse<ChatRoomEnterResponse>>
|
): Single<ApiResponse<ChatRoomEnterResponse>>
|
||||||
|
|
||||||
|
// 메시지 전송 API
|
||||||
|
@POST("/api/chat/rooms/{roomId}/messages")
|
||||||
|
fun sendMessage(
|
||||||
|
@Header("Authorization") authHeader: String,
|
||||||
|
@Path("roomId") roomId: Long,
|
||||||
|
@Body request: SendMessageRequest
|
||||||
|
): Single<ApiResponse<ServerChatMessage>>
|
||||||
|
|
||||||
// 점진적 메시지 로딩 API
|
// 점진적 메시지 로딩 API
|
||||||
@GET("/api/chat/rooms/{roomId}/messages")
|
@GET("/api/chat/rooms/{roomId}/messages")
|
||||||
fun getChatRoomMessages(
|
fun getChatRoomMessages(
|
||||||
|
|||||||
@@ -19,6 +19,40 @@ class ChatRepository(
|
|||||||
private val chatDao: ChatMessageDao,
|
private val chatDao: ChatMessageDao,
|
||||||
private val talkApi: TalkApi
|
private val talkApi: TalkApi
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* 메시지 전송 API 호출
|
||||||
|
* - caller는 로컬에 이미 SENDING 상태로 추가한 후 이 메서드를 호출한다.
|
||||||
|
* - 성공 시: 로컬 SENDING 메시지를 SENT로 업데이트하고, 서버에서 내려온 메시지를 로컬에 반영한다.
|
||||||
|
* - 반환: 서버에서 내려온 메시지(ServerChatMessage) (대개 AI 응답일 것으로 가정)
|
||||||
|
*/
|
||||||
|
fun sendMessage(
|
||||||
|
token: String,
|
||||||
|
roomId: Long,
|
||||||
|
localId: String,
|
||||||
|
content: String
|
||||||
|
): Single<ServerChatMessage> {
|
||||||
|
return talkApi.sendMessage(
|
||||||
|
authHeader = token,
|
||||||
|
roomId = roomId,
|
||||||
|
request = SendMessageRequest(message = content)
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.map { ensureSuccess(it) }
|
||||||
|
.flatMap { serverMsg ->
|
||||||
|
// 1) 로컬에 사용자 메시지 상태를 SENT로 업데이트
|
||||||
|
val updateStatus = Completable.fromAction {
|
||||||
|
kotlinx.coroutines.runBlocking { chatDao.updateStatusByLocalId(roomId, localId, MessageStatus.SENT.name) }
|
||||||
|
}
|
||||||
|
// 2) 서버 응답 메시지를 로컬 DB에 저장(중복 방지: 동일 ID는 REPLACE)
|
||||||
|
val insertServer = Completable.fromAction {
|
||||||
|
val entity = serverMsg.toEntity(roomId)
|
||||||
|
kotlinx.coroutines.runBlocking { chatDao.insertMessage(entity) }
|
||||||
|
}
|
||||||
|
updateStatus.andThen(insertServer)
|
||||||
|
.andThen(Single.just(serverMsg))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 로컬에서 최근 20개 메시지 조회
|
* 로컬에서 최근 20개 메시지 조회
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -276,21 +276,32 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
// 2) 타이핑 인디케이터 표시
|
// 2) 타이핑 인디케이터 표시
|
||||||
chatAdapter.showTypingIndicator()
|
chatAdapter.showTypingIndicator()
|
||||||
|
|
||||||
// 3) 서버 전송 호출 (TODO: TalkApi 연동)
|
// 3) 서버 전송 호출: ChatRepository 사용
|
||||||
// TODO: ChatRepository를 통해 메시지 전송 API 연동 (9.x에서 구현)
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
val disposable = chatRepository.sendMessage(
|
||||||
// 4) 전송 성공/실패 시뮬레이션
|
token = token,
|
||||||
binding.rvMessages.postDelayed({
|
roomId = roomId,
|
||||||
val success = java.util.Random().nextBoolean()
|
localId = localId,
|
||||||
if (success) {
|
content = content
|
||||||
|
)
|
||||||
|
.observeOn(io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ serverMsg ->
|
||||||
|
// 성공: 타이핑 인디케이터 제거 및 상태 업데이트
|
||||||
|
chatAdapter.hideTypingIndicator()
|
||||||
updateUserMessageStatus(localId, MessageStatus.SENT)
|
updateUserMessageStatus(localId, MessageStatus.SENT)
|
||||||
addAiReply(content)
|
|
||||||
} else {
|
// 서버 응답이 AI 메시지인 경우 리스트에 추가
|
||||||
|
val domain = serverMsg.toDomain()
|
||||||
|
if (!domain.mine) {
|
||||||
|
appendMessage(ChatListItem.AiMessage(domain, characterInfo?.name))
|
||||||
|
}
|
||||||
|
}, { error ->
|
||||||
|
// 실패: 타이핑 인디케이터 제거 및 FAILED로 업데이트
|
||||||
chatAdapter.hideTypingIndicator()
|
chatAdapter.hideTypingIndicator()
|
||||||
updateUserMessageStatus(localId, MessageStatus.FAILED)
|
updateUserMessageStatus(localId, MessageStatus.FAILED)
|
||||||
// TODO: 로컬 DB에 FAILED 상태 저장 (7.x/9.x 연동 시 구현)
|
showToast(error.message ?: "메시지 전송에 실패했습니다.")
|
||||||
}
|
})
|
||||||
}, 1000)
|
compositeDisposable.add(disposable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateUserMessageStatus(localId: String, newStatus: MessageStatus) {
|
private fun updateUserMessageStatus(localId: String, newStatus: MessageStatus) {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.talk.room
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class SendMessageRequest(
|
||||||
|
@SerializedName("message") val message: String
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user