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