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