feat(chat-room): ChatRepository 도입 및 TalkApi에 입장/메시지 조회 API 추가
- Repository 패턴 구현: 로컬 DB(Room) + 네트워크(TalkApi) 통합 - enterChatRoom, loadMoreMessages, clearAllMessagesOnLogout 제공 - TalkApi에 /enter, /messages 엔드포인트 추가 - Entity↔도메인 매퍼 추가 - Koin 모듈에 ChatRepository 바인딩
This commit is contained in:
@@ -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<ApiResponse<CreateChatRoomResponse>>
|
||||
|
||||
// 통합 채팅방 입장 API
|
||||
@GET("/api/chat/rooms/{roomId}/enter")
|
||||
fun enterChatRoom(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("roomId") roomId: Long
|
||||
): Single<ApiResponse<ChatRoomEnterResponse>>
|
||||
|
||||
// 점진적 메시지 로딩 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<ApiResponse<ChatMessagesResponse>>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 보이스온 - 점진적 메시지 로딩 응답 모델
|
||||
* 보이스온 - 점진적 메시지 로딩 응답 DTO
|
||||
*/
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
|
||||
@@ -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<List<ChatMessage>> {
|
||||
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<ChatRoomEnterResponse> {
|
||||
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<ChatMessagesResponse> {
|
||||
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 <T> ensureSuccess(response: ApiResponse<T>): T {
|
||||
if (!response.success || response.data == null) {
|
||||
throw IllegalStateException(response.message ?: "API response error")
|
||||
}
|
||||
return response.data
|
||||
}
|
||||
|
||||
private fun ensureMessagesSuccess(response: ApiResponse<ChatMessagesResponse>): ChatMessagesResponse {
|
||||
if (!response.success || response.data == null) {
|
||||
throw IllegalStateException(response.message ?: "API response error")
|
||||
}
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 보이스온 - 통합 채팅방 입장 응답 모델
|
||||
* 보이스온 - 통합 채팅방 입장 응답 DTO
|
||||
*/
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
|
||||
@@ -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<ChatMessageDatabase>().chatMessageDao() }
|
||||
|
||||
// Repository
|
||||
single { ChatRepository(chatDao = get(), talkApi = get<TalkApi>()) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user