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