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 io.reactivex.rxjava3.core.Single
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.CreateChatRoomRequest
|
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.ChatRoomEnterResponse
|
||||||
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
|
||||||
import retrofit2.http.Header
|
import retrofit2.http.Header
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface TalkApi {
|
interface TalkApi {
|
||||||
@GET("/api/chat/room/list")
|
@GET("/api/chat/room/list")
|
||||||
@@ -20,4 +24,20 @@ interface TalkApi {
|
|||||||
@Header("Authorization") authHeader: String,
|
@Header("Authorization") authHeader: String,
|
||||||
@Body request: CreateChatRoomRequest
|
@Body request: CreateChatRoomRequest
|
||||||
): Single<ApiResponse<CreateChatRoomResponse>>
|
): 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
|
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
|
package kr.co.vividnext.sodalive.chat.talk.room
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.talk.room
|
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 kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDatabase
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
@@ -10,4 +11,7 @@ val chatTalkRoomModule = module {
|
|||||||
|
|
||||||
// DAO
|
// DAO
|
||||||
single { get<ChatMessageDatabase>().chatMessageDao() }
|
single { get<ChatMessageDatabase>().chatMessageDao() }
|
||||||
|
|
||||||
|
// Repository
|
||||||
|
single { ChatRepository(chatDao = get(), talkApi = get<TalkApi>()) }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user