From 933e6501835b1ac3dcaca8a7a57aa1f9ed8113d6 Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 14 Aug 2025 17:14:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat-room):=20=EC=B1=84=ED=8C=85=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EC=A0=84=EC=86=A1/=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TalkApi에 입장/전송/점진 로딩 엔드포인트 구현(9.1) - ChatRepository를 통한 서버 연동 및 로컬 동기화 추가 - ChatRoomActivity에서 입장/전송/페이징 연동, 타이핑 인디케이터/에러 처리 반영(9.2) --- .../vividnext/sodalive/chat/talk/TalkApi.kt | 10 ++++++ .../sodalive/chat/talk/room/ChatRepository.kt | 34 ++++++++++++++++++ .../chat/talk/room/ChatRoomActivity.kt | 35 ++++++++++++------- .../chat/talk/room/SendMessageRequest.kt | 9 +++++ 4 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/SendMessageRequest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt index d3cb25d6..70b713fa 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt @@ -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.ChatMessagesResponse 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 retrofit2.http.Body import retrofit2.http.GET @@ -32,6 +34,14 @@ interface TalkApi { @Path("roomId") roomId: Long ): Single> + // 메시지 전송 API + @POST("/api/chat/rooms/{roomId}/messages") + fun sendMessage( + @Header("Authorization") authHeader: String, + @Path("roomId") roomId: Long, + @Body request: SendMessageRequest + ): Single> + // 점진적 메시지 로딩 API @GET("/api/chat/rooms/{roomId}/messages") fun getChatRoomMessages( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt index 5a9f4bf3..6c4d0da9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt @@ -19,6 +19,40 @@ class ChatRepository( private val chatDao: ChatMessageDao, private val talkApi: TalkApi ) { + /** + * 메시지 전송 API 호출 + * - caller는 로컬에 이미 SENDING 상태로 추가한 후 이 메서드를 호출한다. + * - 성공 시: 로컬 SENDING 메시지를 SENT로 업데이트하고, 서버에서 내려온 메시지를 로컬에 반영한다. + * - 반환: 서버에서 내려온 메시지(ServerChatMessage) (대개 AI 응답일 것으로 가정) + */ + fun sendMessage( + token: String, + roomId: Long, + localId: String, + content: String + ): Single { + 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개 메시지 조회 */ 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 d20badd2..1d8214b4 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 @@ -276,21 +276,32 @@ class ChatRoomActivity : BaseActivity( // 2) 타이핑 인디케이터 표시 chatAdapter.showTypingIndicator() - // 3) 서버 전송 호출 (TODO: TalkApi 연동) - // TODO: ChatRepository를 통해 메시지 전송 API 연동 (9.x에서 구현) - - // 4) 전송 성공/실패 시뮬레이션 - binding.rvMessages.postDelayed({ - val success = java.util.Random().nextBoolean() - if (success) { + // 3) 서버 전송 호출: ChatRepository 사용 + val token = "Bearer ${SharedPreferenceManager.token}" + val disposable = chatRepository.sendMessage( + token = token, + roomId = roomId, + localId = localId, + content = content + ) + .observeOn(io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread()) + .subscribe({ serverMsg -> + // 성공: 타이핑 인디케이터 제거 및 상태 업데이트 + chatAdapter.hideTypingIndicator() 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() updateUserMessageStatus(localId, MessageStatus.FAILED) - // TODO: 로컬 DB에 FAILED 상태 저장 (7.x/9.x 연동 시 구현) - } - }, 1000) + showToast(error.message ?: "메시지 전송에 실패했습니다.") + }) + compositeDisposable.add(disposable) } private fun updateUserMessageStatus(localId: String, newStatus: MessageStatus) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/SendMessageRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/SendMessageRequest.kt new file mode 100644 index 00000000..8df64e08 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/SendMessageRequest.kt @@ -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 +)