diff --git a/data/message/src/main/java/com/seugi/data/message/MessageRepository.kt b/data/message/src/main/java/com/seugi/data/message/MessageRepository.kt index a826339e..62e7ea03 100644 --- a/data/message/src/main/java/com/seugi/data/message/MessageRepository.kt +++ b/data/message/src/main/java/com/seugi/data/message/MessageRepository.kt @@ -3,6 +3,7 @@ package com.seugi.data.message import com.seugi.common.model.Result import com.seugi.data.message.model.MessageLoadModel import com.seugi.data.message.model.MessageRoomEvent +import com.seugi.data.message.model.MessageStompErrorModel import com.seugi.data.message.model.MessageType import com.seugi.data.message.model.stomp.MessageStompLifecycleModel import kotlinx.coroutines.flow.Flow @@ -14,6 +15,10 @@ interface MessageRepository { suspend fun subscribeRoom(chatRoomId: String, userId: Long): Flow> + suspend fun subscribeError(): Flow> + + suspend fun closeSocket(): Boolean + suspend fun reSubscribeRoom(chatRoomId: String, userId: Long): Flow> suspend fun getMessage(chatRoomId: String, timestamp: LocalDateTime?, userId: Long): Flow> diff --git a/data/message/src/main/java/com/seugi/data/message/mapper/MessageStompErrorMapper.kt b/data/message/src/main/java/com/seugi/data/message/mapper/MessageStompErrorMapper.kt new file mode 100644 index 00000000..63d9fc45 --- /dev/null +++ b/data/message/src/main/java/com/seugi/data/message/mapper/MessageStompErrorMapper.kt @@ -0,0 +1,11 @@ +package com.seugi.data.message.mapper + +import com.seugi.data.message.model.MessageStompErrorModel +import com.seugi.network.message.response.stomp.MessageStompErrorResponse + +internal fun MessageStompErrorResponse.toModel() = MessageStompErrorModel( + status = status, + success = success, + state = state, + message = message, +) diff --git a/data/message/src/main/java/com/seugi/data/message/model/MessageStompErrorModel.kt b/data/message/src/main/java/com/seugi/data/message/model/MessageStompErrorModel.kt new file mode 100644 index 00000000..87144863 --- /dev/null +++ b/data/message/src/main/java/com/seugi/data/message/model/MessageStompErrorModel.kt @@ -0,0 +1,8 @@ +package com.seugi.data.message.model + +data class MessageStompErrorModel( + val status: Int, + val success: Boolean, + val state: String, + val message: String, +) diff --git a/data/message/src/main/java/com/seugi/data/message/repository/MessageRepositoryImpl.kt b/data/message/src/main/java/com/seugi/data/message/repository/MessageRepositoryImpl.kt index c9411082..2b404016 100644 --- a/data/message/src/main/java/com/seugi/data/message/repository/MessageRepositoryImpl.kt +++ b/data/message/src/main/java/com/seugi/data/message/repository/MessageRepositoryImpl.kt @@ -5,7 +5,6 @@ import com.seugi.common.model.Result import com.seugi.common.model.asResult import com.seugi.common.utiles.DispatcherType import com.seugi.common.utiles.SeugiDispatcher -import com.seugi.data.core.mapper.toModel import com.seugi.data.core.mapper.toModels import com.seugi.data.core.model.TimetableModel import com.seugi.data.message.MessageRepository @@ -16,6 +15,7 @@ import com.seugi.data.message.model.MessageBotRawKeyword import com.seugi.data.message.model.MessageBotRawKeywordInData import com.seugi.data.message.model.MessageLoadModel import com.seugi.data.message.model.MessageRoomEvent +import com.seugi.data.message.model.MessageStompErrorModel import com.seugi.data.message.model.MessageType import com.seugi.data.message.model.stomp.MessageStompLifecycleModel import com.seugi.local.room.dao.TokenDao @@ -54,27 +54,48 @@ class MessageRepositoryImpl @Inject constructor( override suspend fun subscribeRoom(chatRoomId: String, userId: Long): Flow> { if (!datasource.getIsConnect()) { val token = tokenDao.getToken() - datasource.connectStomp( + datasource.connectStompSocket( token?.token ?: "", ) } return datasource.subscribeRoom(chatRoomId) .flowOn(dispatcher) .map { + /**/ it.toEventModel(userId) } .asResult() } + override suspend fun subscribeError(): Flow> { + if (!datasource.getIsConnect()) { + val token = tokenDao.getToken() + datasource.connectStompSocket( + token?.token ?: "", + ) + } + return datasource.subscribeError() + .flowOn(dispatcher) + .map { + it.toModel() + } + .asResult() + } + + override suspend fun closeSocket(): Boolean { + datasource.closeStompSocket() + return true + } + override suspend fun reSubscribeRoom(chatRoomId: String, userId: Long): Flow> { val token = tokenDao.getToken() - datasource.reConnectStomp( + datasource.reConnectStompSocket( token?.token ?: "", token?.refreshToken ?: "", ) delay(200) return datasource.subscribeRoom(chatRoomId) - .flowOn(dispatcher) +// .flowOn(dispatcher) .map { it.toEventModel(userId) } diff --git a/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailScreen.kt b/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailScreen.kt index 2da7426a..287dbc91 100644 --- a/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailScreen.kt +++ b/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue @@ -254,9 +255,15 @@ internal fun ChatDetailScreen( ) } + DisposableEffect(Unit) { + onDispose { + viewModel.socketClose() + } + } + LifecycleStartEffect(key1 = Unit) { viewModel.collectStompLifecycle(chatRoomId, userId) - viewModel.channelReconnect(userId, chatRoomId) + viewModel.channelConnect(userId, chatRoomId) onStopOrDispose { viewModel.subscribeCancel() } diff --git a/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailViewModel.kt b/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailViewModel.kt index f5746bb1..ed0b5a31 100644 --- a/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailViewModel.kt +++ b/feature-main/chat-datail/src/main/java/com/seugi/chatdatail/ChatDetailViewModel.kt @@ -373,7 +373,7 @@ class ChatDetailViewModel @Inject constructor( content = content, mention = mention, ) - channelReconnect(userId) + channelConnect(userId) return@launch } // 메세지가 보내고 MESSAGE_TIMEOUT 시간만큼 지났는데도 안보내지면 실패처리 @@ -604,12 +604,12 @@ class ChatDetailViewModel @Inject constructor( } } - private fun channelConnect(userId: Long) { + fun channelConnect(userId: Long, roomId: String? = null) { viewModelScope.launch { subscribeChat?.cancel() val job = viewModelScope.async { messageRepository.subscribeRoom( - chatRoomId = state.value.roomInfo?.id ?: "", + chatRoomId = roomId ?: state.value.roomInfo?.id ?: "", userId = userId, ).collect { it.collectMessage(userId) @@ -628,6 +628,10 @@ class ChatDetailViewModel @Inject constructor( subscribeChat = null } + fun socketClose() = viewModelScope.launch { + messageRepository.closeSocket() + } + fun leftRoom(chatRoomId: String) { viewModelScope.launch { groupChatRepository.leftRoom(chatRoomId).collect { diff --git a/network/core/src/main/java/com/seugi/network/core/SeugiUrl.kt b/network/core/src/main/java/com/seugi/network/core/SeugiUrl.kt index 25ebf642..5555a641 100644 --- a/network/core/src/main/java/com/seugi/network/core/SeugiUrl.kt +++ b/network/core/src/main/java/com/seugi/network/core/SeugiUrl.kt @@ -21,6 +21,7 @@ object SeugiUrl { const val SEND = "/pub/chat.message" const val GET_MESSAGE = "${BASE_URL}/message/search" const val EMOJI = "$MESSAGE/emoji" + const val ERRORS = "/user/queue/errors" } object PersonalChat { diff --git a/network/message/src/main/java/com/seugi/network/message/MessageDataSource.kt b/network/message/src/main/java/com/seugi/network/message/MessageDataSource.kt index 5525a3d5..7e232c00 100644 --- a/network/message/src/main/java/com/seugi/network/message/MessageDataSource.kt +++ b/network/message/src/main/java/com/seugi/network/message/MessageDataSource.kt @@ -4,6 +4,7 @@ import com.seugi.network.core.response.BaseResponse import com.seugi.network.core.response.Response import com.seugi.network.message.response.MessageRoomEventResponse import com.seugi.network.message.response.message.MessageLoadResponse +import com.seugi.network.message.response.stomp.MessageStompErrorResponse import com.seugi.network.message.response.stomp.MessageStompLifecycleResponse import kotlinx.coroutines.flow.Flow import kotlinx.datetime.LocalDateTime @@ -11,11 +12,15 @@ import kotlinx.datetime.LocalDateTime interface MessageDataSource { suspend fun subscribeRoom(chatRoomId: String): Flow + suspend fun subscribeError(): Flow + suspend fun sendMessage(chatRoomId: String, message: String, messageUUID: String, type: String, mention: List): Boolean - suspend fun connectStomp(accessToken: String) + suspend fun connectStompSocket(accessToken: String) + + suspend fun reConnectStompSocket(accessToken: String, refreshToken: String) - suspend fun reConnectStomp(accessToken: String, refreshToken: String) + suspend fun closeStompSocket() suspend fun getIsConnect(): Boolean diff --git a/network/message/src/main/java/com/seugi/network/message/datasource/MessageDataSourceImpl.kt b/network/message/src/main/java/com/seugi/network/message/datasource/MessageDataSourceImpl.kt index 3b0ca610..b63fb6ca 100644 --- a/network/message/src/main/java/com/seugi/network/message/datasource/MessageDataSourceImpl.kt +++ b/network/message/src/main/java/com/seugi/network/message/datasource/MessageDataSourceImpl.kt @@ -14,6 +14,7 @@ import com.seugi.network.message.request.EmojiRequest import com.seugi.network.message.request.MessageRequest import com.seugi.network.message.response.MessageRoomEventResponse import com.seugi.network.message.response.message.MessageLoadResponse +import com.seugi.network.message.response.stomp.MessageStompErrorResponse import com.seugi.network.message.response.stomp.MessageStompLifecycleResponse import com.seugi.stompclient.StompClient import com.seugi.stompclient.dto.LifecycleEvent @@ -27,7 +28,6 @@ import io.ktor.client.request.parameter import io.ktor.client.request.post import io.ktor.client.request.put import io.ktor.client.request.setBody -import io.ktor.client.utils.EmptyContent.contentType import io.ktor.http.ContentType import io.ktor.http.contentType import javax.inject.Inject @@ -46,14 +46,18 @@ class MessageDataSourceImpl @Inject constructor( private val httpClient: HttpClient, ) : MessageDataSource { - override suspend fun connectStomp(accessToken: String) { + override suspend fun connectStompSocket(accessToken: String) { val header = listOf(StompHeader("Authorization", accessToken)) stompClient.connect(header) } - override suspend fun reConnectStomp(accessToken: String, refreshToken: String): Unit = coroutineScope { + override suspend fun reConnectStompSocket(accessToken: String, refreshToken: String): Unit = coroutineScope { + stompClient.disconnectCompletable().subscribe { } + connectStompSocket(accessToken) + } + + override suspend fun closeStompSocket() { stompClient.disconnectCompletable().subscribe { } - connectStomp(accessToken) } override suspend fun getIsConnect(): Boolean = stompClient.isConnected @@ -121,6 +125,19 @@ class MessageDataSourceImpl @Inject constructor( } } + override suspend fun subscribeError() = flow { + stompClient.topic(SeugiUrl.Message.ERRORS) + .asFlow() + .flowOn(dispatcher) + .catch { + it.printStackTrace() + } + .collect { message -> + val response = message.payload.toResponse(MessageStompErrorResponse::class.java) + emit(response) + } + } + override suspend fun sendMessage(chatRoomId: String, message: String, messageUUID: String, type: String, mention: List): Boolean { // 연결이 되지 않는 경우 연결 강제성 부여 if (!stompClient.isConnected) { diff --git a/network/message/src/main/java/com/seugi/network/message/response/stomp/MessageStompErrorResponse.kt b/network/message/src/main/java/com/seugi/network/message/response/stomp/MessageStompErrorResponse.kt new file mode 100644 index 00000000..96b14e35 --- /dev/null +++ b/network/message/src/main/java/com/seugi/network/message/response/stomp/MessageStompErrorResponse.kt @@ -0,0 +1,8 @@ +package com.seugi.network.message.response.stomp + +data class MessageStompErrorResponse( + val status: Int, + val success: Boolean, + val state: String, + val message: String, +)