From 6388895e6ec1323d14a71c3cf88c01f4f7d841d8 Mon Sep 17 00:00:00 2001 From: klaus Date: Wed, 13 Aug 2025 17:30:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat-room):=20ChatRepository=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20TalkApi=EC=97=90=20=EC=9E=85=EC=9E=A5/?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Repository 패턴 구현: 로컬 DB(Room) + 네트워크(TalkApi) 통합 - enterChatRoom, loadMoreMessages, clearAllMessagesOnLogout 제공 - TalkApi에 /enter, /messages 엔드포인트 추가 - Entity↔도메인 매퍼 추가 - Koin 모듈에 ChatRepository 바인딩 --- .../vividnext/sodalive/chat/talk/TalkApi.kt | 20 +++++ .../talk/room/ChatMessageEntityMappers.kt | 47 ++++++++++ .../chat/talk/room/ChatMessagesResponse.kt | 2 +- .../sodalive/chat/talk/room/ChatRepository.kt | 90 +++++++++++++++++++ .../chat/talk/room/ChatRoomEnterResponse.kt | 2 +- .../chat/talk/room/ChatTalkRoomModule.kt | 4 + 6 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageEntityMappers.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.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 c52b6ac3..d3cb25d6 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 @@ -3,11 +3,15 @@ package kr.co.vividnext.sodalive.chat.talk import io.reactivex.rxjava3.core.Single 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.common.ApiResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query interface TalkApi { @GET("/api/chat/room/list") @@ -20,4 +24,20 @@ interface TalkApi { @Header("Authorization") authHeader: String, @Body request: CreateChatRoomRequest ): Single> + + // 통합 채팅방 입장 API + @GET("/api/chat/rooms/{roomId}/enter") + fun enterChatRoom( + @Header("Authorization") authHeader: String, + @Path("roomId") roomId: Long + ): Single> + + // 점진적 메시지 로딩 API + @GET("/api/chat/rooms/{roomId}/messages") + fun getChatRoomMessages( + @Header("Authorization") authHeader: String, + @Path("roomId") roomId: Long, + @Query("cursor") cursor: Long?, + @Query("limit") limit: Int = 20 + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageEntityMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageEntityMappers.kt new file mode 100644 index 00000000..05cd0a65 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageEntityMappers.kt @@ -0,0 +1,47 @@ +/* + * 보이스온 - 채팅 메시지 Entity/도메인 매퍼 + */ +package kr.co.vividnext.sodalive.chat.talk.room + +import androidx.annotation.Keep +import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageEntity + +@Keep +fun ChatMessageEntity.toDomain(): ChatMessage { + return ChatMessage( + messageId = messageId, + message = message, + profileImageUrl = profileImageUrl, + mine = mine, + createdAt = createdAt, + status = status, + localId = localId, + isGrouped = false + ) +} + +@Keep +fun ChatMessage.toEntity(roomId: Long): ChatMessageEntity { + return ChatMessageEntity( + messageId = messageId, + roomId = roomId, + message = message, + profileImageUrl = profileImageUrl, + mine = mine, + createdAt = createdAt, + status = status, + localId = localId + ) +} + +@Keep +fun ServerChatMessage.toEntity(roomId: Long): ChatMessageEntity { + return ChatMessageEntity( + messageId = messageId, + roomId = roomId, + message = message, + profileImageUrl = profileImageUrl, + mine = mine, + createdAt = createdAt + ) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessagesResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessagesResponse.kt index 556e6adc..2382cfc8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessagesResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessagesResponse.kt @@ -1,5 +1,5 @@ /* - * 보이스온 - 점진적 메시지 로딩 응답 모델 + * 보이스온 - 점진적 메시지 로딩 응답 DTO */ package kr.co.vividnext.sodalive.chat.talk.room 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 new file mode 100644 index 00000000..2e7e4517 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt @@ -0,0 +1,90 @@ +/* + * 보이스온 - 채팅방 ChatRepository (Repository 패턴) + * - 로컬 DB(Room) + 네트워크(TalkApi) 통합 관리 + * - 통합 채팅방 입장 API 호출 및 로컬 동기화 + * - 점진적 메시지 로딩 + * - 로그아웃 시 로컬 메시지 삭제 + */ +package kr.co.vividnext.sodalive.chat.talk.room + +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.chat.talk.TalkApi +import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDao +import kr.co.vividnext.sodalive.common.ApiResponse +import java.util.concurrent.Callable + +class ChatRepository( + private val chatDao: ChatMessageDao, + private val talkApi: TalkApi +) { + /** + * 로컬에서 최근 20개 메시지 조회 + */ + fun getRecentMessagesFromLocal(roomId: Long): Single> { + return Single.fromCallable(Callable { + runCatching { + val entities = kotlinx.coroutines.runBlocking { chatDao.getRecentMessages(roomId) } + entities.map { it.toDomain() } + }.getOrDefault(emptyList()) + }).subscribeOn(Schedulers.io()) + } + + /** + * 통합 채팅방 입장: 서버 캐릭터 정보 + 최신 메시지 수신 후 로컬 DB 업데이트 + * - 로컬 데이터가 없더라도 서버 응답을 기준으로 동기화 + */ + fun enterChatRoom(token: String, roomId: Long): Single { + return talkApi.enterChatRoom(authHeader = token, roomId = roomId) + .subscribeOn(Schedulers.io()) + .flatMap { response -> + val data = ensureSuccess(response) + // 로컬 DB 동기화 + val entities = data.messages.map { it.toEntity(roomId) } + Single.fromCallable { + kotlinx.coroutines.runBlocking { chatDao.insertMessages(entities) } + data + }.subscribeOn(Schedulers.io()) + } + } + + /** + * 점진적 메시지 로딩: 커서 기반으로 이전 메시지를 조회 + */ + fun loadMoreMessages(token: String, roomId: Long, cursor: Long?): Single { + return talkApi.getChatRoomMessages(authHeader = token, roomId = roomId, cursor = cursor, limit = 20) + .subscribeOn(Schedulers.io()) + .flatMap { response -> + val data = ensureMessagesSuccess(response) + val entities = data.messages.map { it.toEntity(roomId) } + Single.fromCallable { + kotlinx.coroutines.runBlocking { chatDao.insertMessages(entities) } + data + }.subscribeOn(Schedulers.io()) + } + } + + /** + * 로그아웃 시 모든 로컬 메시지 삭제 + */ + fun clearAllMessagesOnLogout(): Completable { + return Completable.fromAction { + kotlinx.coroutines.runBlocking { chatDao.deleteAllMessages() } + }.subscribeOn(Schedulers.io()) + } + + private fun ensureSuccess(response: ApiResponse): T { + if (!response.success || response.data == null) { + throw IllegalStateException(response.message ?: "API response error") + } + return response.data + } + + private fun ensureMessagesSuccess(response: ApiResponse): ChatMessagesResponse { + if (!response.success || response.data == null) { + throw IllegalStateException(response.message ?: "API response error") + } + return response.data + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomEnterResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomEnterResponse.kt index b378e244..191d18de 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomEnterResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomEnterResponse.kt @@ -1,5 +1,5 @@ /* - * 보이스온 - 통합 채팅방 입장 응답 모델 + * 보이스온 - 통합 채팅방 입장 응답 DTO */ package kr.co.vividnext.sodalive.chat.talk.room diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatTalkRoomModule.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatTalkRoomModule.kt index 3c6e9bb4..0a09f5ce 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatTalkRoomModule.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatTalkRoomModule.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.chat.talk.room +import kr.co.vividnext.sodalive.chat.talk.TalkApi import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDatabase import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -10,4 +11,7 @@ val chatTalkRoomModule = module { // DAO single { get().chatMessageDao() } + + // Repository + single { ChatRepository(chatDao = get(), talkApi = get()) } }