Skip to content

Commit

Permalink
[IDLE-466] 웹소켓 연결, 해제 관리 및 웹소켓 데이터 파이프라인 구축
Browse files Browse the repository at this point in the history
  • Loading branch information
tgyuuAn committed Nov 2, 2024
1 parent 7cadce8 commit c37d25c
Show file tree
Hide file tree
Showing 21 changed files with 194 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ object AnalyticsModule {
@DebugErrorHelper debugErrorHelper: ErrorLoggingHelper,
@ReleaseErrorHelper releaseErrorHelper: ErrorLoggingHelper,
): ErrorLoggingHelper {
return if (BuildConfig.BUILD_TYPE == "DEBUG") releaseErrorHelper
return if (BuildConfig.BUILD_TYPE == "RELEASE") releaseErrorHelper
else debugErrorHelper
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.idle.data.repository.chatting

import com.idle.domain.model.chatting.ChatMessage
import com.idle.domain.repositorry.chatting.ChattingRepository
import com.idle.network.source.websocket.WebSocketDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class ChattingRepositoryImpl @Inject constructor(
Expand All @@ -11,4 +15,9 @@ class ChattingRepositoryImpl @Inject constructor(

override suspend fun disconnectWebSocket(): Result<Unit> =
webSocketDataSource.disconnectWebSocket()

override fun subscribeChatMessage(): Flow<ChatMessage> =
webSocketDataSource.getChatMessageFlow()
.filterNotNull()
.map { it.toVO() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ class ProfileRepositoryImpl @Inject constructor(
}

WorkerProfile(
workerId = properties["workerId"]
?: throw IllegalArgumentException("Missing workerId"),
workerName = properties["workerName"]
?: throw IllegalArgumentException("Missing workerName"),
age = properties["age"]?.toInt() ?: throw NumberFormatException("Invalid age format"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ProfileRepositoryImplTest {
)

private val workerProfile = WorkerProfile(
workerId = "test",
workerName = "Test Worker",
age = 30,
gender = Gender.MAN,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ data class ChatMessage(
val senderType: SenderType,
val contents: List<Content>,
val createdAt: LocalDateTime,
)
) {
fun printPlainContents(): String = contents.joinToString { it.value }
}

data class Content(
val type: ContentType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ data class ChatRoom(
val lastMessage: String,
val lastSentAt: LocalDateTime,
val unReadMessageCount: Int,
val profileImageUrl: String,
val profileImageUrl: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.idle.domain.model.profile
import com.idle.domain.model.auth.Gender

data class WorkerProfile(
val workerId: String,
val workerName: String,
val age: Int,
val gender: Gender,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.idle.domain.repositorry.chatting

import com.idle.domain.model.chatting.ChatMessage
import kotlinx.coroutines.flow.Flow

interface ChattingRepository {
suspend fun connectWebSocket(): Result<Unit>
suspend fun disconnectWebSocket(): Result<Unit>
fun subscribeChatMessage(): Flow<ChatMessage>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.idle.domain.usecase.chatting

import com.idle.domain.model.chatting.ChatMessage
import com.idle.domain.repositorry.chatting.ChattingRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class SubscribeChatMessageUseCase @Inject constructor(
private val chattingRepository: ChattingRepository,
) {
operator fun invoke(): Flow<ChatMessage> = chattingRepository.subscribeChatMessage()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable

@Serializable
data class GetWorkerProfileResponse(
@SerialName("carerId") val workerId: String? = null,
@SerialName("carerName") val workerName: String? = null,
val age: Int? = null,
val gender: String? = null,
Expand All @@ -23,6 +24,7 @@ data class GetWorkerProfileResponse(
val profileImageUrl: String? = null,
) {
fun toVo() = WorkerProfile(
workerId = workerId ?: "",
workerName = workerName ?: "",
age = age ?: -1,
gender = Gender.create(gender ?: ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.idle.network.source.websocket
import android.util.Log
import com.idle.network.BuildConfig
import com.idle.network.model.chatting.ChatMessageResponse
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.json.Json
import okhttp3.Response
import okhttp3.WebSocket
Expand All @@ -14,8 +14,8 @@ import javax.inject.Singleton

@Singleton
class ChatMessageListener @Inject constructor(private val json: Json) : WebSocketListener() {
private val _chatMessageChannel = Channel<ChatMessageResponse>(Channel.BUFFERED)
val chatMessageFlow = _chatMessageChannel.receiveAsFlow()
private val _chatMessageChannel = MutableStateFlow<ChatMessageResponse?>(null)
val chatMessageFlow = _chatMessageChannel.asStateFlow()

override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
Expand All @@ -33,7 +33,7 @@ class ChatMessageListener @Inject constructor(private val json: Json) : WebSocke
return
}

_chatMessageChannel.trySend(chatMessageResponse)
_chatMessageChannel.value = chatMessageResponse
}

override fun onOpen(webSocket: WebSocket, response: Response) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.idle.network.source.websocket
import com.idle.domain.model.error.ErrorHandler
import com.idle.network.BuildConfig
import com.idle.network.di.WebSocketOkHttpClient
import com.idle.network.model.chatting.ChatMessageResponse
import kotlinx.coroutines.flow.StateFlow
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
Expand Down Expand Up @@ -41,5 +43,5 @@ class WebSocketDataSource @Inject constructor(
}
}

fun getChatMessageFlow() = chatMessageListener.chatMessageFlow
fun getChatMessageFlow(): StateFlow<ChatMessageResponse?> = chatMessageListener.chatMessageFlow
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ internal class CenterChattingFragment : BaseComposeFragment() {

LifecycleEventEffect(Lifecycle.Event.ON_CREATE) {
getChatRoomList()
subscribeChatMessage()
}

CenterChattingScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,76 @@ import com.idle.binding.NavigationHelper
import com.idle.binding.base.BaseViewModel
import com.idle.domain.model.chatting.ChatRoom
import com.idle.domain.model.error.ErrorHandler
import com.idle.domain.model.profile.WorkerProfile
import com.idle.domain.usecase.chatting.GetChatRoomListUseCase
import com.idle.domain.usecase.chatting.SubscribeChatMessageUseCase
import com.idle.domain.usecase.profile.GetCenterProfileUseCase
import com.idle.domain.usecase.profile.GetLocalMyWorkerProfileUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class CenterChattingViewModel @Inject constructor(
private val getCenterProfileUseCase: GetCenterProfileUseCase,
private val getChatRoomListUseCase: GetChatRoomListUseCase,
private val errorHandlerHelper: ErrorHandler,
private val subscribeChatMessageUseCase: SubscribeChatMessageUseCase,
private val errorHandler: ErrorHandler,
val navigationHelper: NavigationHelper,
) : BaseViewModel() {
private val _chatRoomList = MutableStateFlow<List<ChatRoom>?>(emptyList())
val chatRoomList = _chatRoomList.asStateFlow()
private val _chatRoomMap = MutableStateFlow<LinkedHashMap<String, ChatRoom>>(LinkedHashMap())
val chatRoomList = _chatRoomMap
.map { it.values.toList() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = null,
)

internal fun subscribeChatMessage() = viewModelScope.launch {
subscribeChatMessageUseCase().collect { chatMessage ->
val updatedMap = LinkedHashMap(_chatRoomMap.value) // 기존 맵을 복사
val roomId = chatMessage.roomId
val chatRoom = updatedMap[roomId]

if (chatRoom != null) {
// 기존 방이 있으면 업데이트 후 최상단으로 올리기 위해 제거 후 다시 추가
updatedMap.remove(roomId)
updatedMap[roomId] = chatRoom.copy(
lastMessage = chatMessage.printPlainContents(),
unReadMessageCount = chatRoom.unReadMessageCount + 1,
)
} else {
// 새로운 방이면 새로 생성 후 최상단에 추가
val newChatRoom = ChatRoom(
id = roomId,
lastMessage = chatMessage.printPlainContents(),
sender = chatMessage.senderId,
receiver = "", // Todo 센터 ID를 얻을 방법 서버와 의논
createdAt = chatMessage.createdAt,
lastSentAt = chatMessage.createdAt,
unReadMessageCount = 1,
profileImageUrl = getCenterProfileUseCase(chatMessage.senderId)
.map { it.profileImageUrl }
.getOrNull(),
)
updatedMap[roomId] = newChatRoom
}

_chatRoomMap.value = updatedMap
}
}

internal fun getChatRoomList() = viewModelScope.launch {
getChatRoomListUseCase().onSuccess {
_chatRoomList.value = it
}.onFailure { errorHandlerHelper.sendError(it) }
_chatRoomMap.value = LinkedHashMap<String, ChatRoom>().apply {
it.forEach { chatRoom -> put(chatRoom.id, chatRoom) }
}
}.onFailure { errorHandler.sendError(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ internal class ChattingDetailFragment : BaseComposeFragment() {
LifecycleEventEffect(Lifecycle.Event.ON_CREATE) {
getUserProfile(receiverUserType = receiverUserType, senderId = senderId)
getChatMessages(chattingRoomId)
subscribeChatMessage()
}

if (chatMessages != null && workerProfile != null && centerProfile != null) {
Expand Down Expand Up @@ -124,7 +125,7 @@ internal fun ChattingDetailScreen(
.fillMaxWidth()
.weight(1f)
.background(CareTheme.colors.gray050)
.padding(horizontal = 20.dp),
.padding(horizontal = 18.dp),
) {
item {
Spacer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.idle.domain.model.error.ErrorHandler
import com.idle.domain.model.profile.CenterProfile
import com.idle.domain.model.profile.WorkerProfile
import com.idle.domain.usecase.chatting.GetChatMessagesUseCase
import com.idle.domain.usecase.chatting.SubscribeChatMessageUseCase
import com.idle.domain.usecase.profile.GetCenterProfileUseCase
import com.idle.domain.usecase.profile.GetLocalMyCenterProfileUseCase
import com.idle.domain.usecase.profile.GetLocalMyWorkerProfileUseCase
Expand All @@ -25,6 +26,7 @@ class ChattingDetailViewModel @Inject constructor(
private val getCenterProfileUseCase: GetCenterProfileUseCase,
private val getWorkerProfileUseCase: GetWorkerProfileUseCase,
private val getChatMessagesUseCase: GetChatMessagesUseCase,
private val subscribeChatMessageUseCase: SubscribeChatMessageUseCase,
private val errorHandlerHelper: ErrorHandler,
) : BaseViewModel() {
private val _writingText = MutableStateFlow<String>("")
Expand Down Expand Up @@ -87,4 +89,10 @@ class ChattingDetailViewModel @Inject constructor(
_chatMessages.value = it
}
}

internal fun subscribeChatMessage() = viewModelScope.launch {
subscribeChatMessageUseCase().collect { chatMessage ->
_chatMessages.value = (_chatMessages.value ?: emptyList()) + chatMessage
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ fun CareChatSenderTextBubbleWithImage(
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier
.padding(start = 4.dp)
.padding(start = 6.dp)
.wrapContentWidth()
.align(Alignment.Bottom),
) {
Expand Down Expand Up @@ -162,7 +162,7 @@ fun CareChatSenderTextBubble(
horizontalAlignment = Alignment.Start,
modifier = Modifier
.align(Alignment.Bottom)
.padding(start = 4.dp),
.padding(start = 6.dp),
) {
if (isRead) {
Text(
Expand Down Expand Up @@ -199,7 +199,7 @@ fun CareChatReceiverTextBubble(
horizontalAlignment = Alignment.End,
modifier = Modifier
.align(Alignment.Bottom)
.padding(end = 4.dp),
.padding(end = 6.dp),
) {
if (isRead) {
Text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,17 @@ internal class WorkerChattingFragment : BaseComposeFragment() {

LifecycleEventEffect(Lifecycle.Event.ON_CREATE) {
getChatRoomList()
subscribeChatMessage()
}

WorkerChattingScreen(
chatRoomList = chatRoomList,
navigateTo = { navigationHelper.navigateTo(NavigationEvent.NavigateTo(it)) },
)
if(chatRoomList != null) {
WorkerChattingScreen(
chatRoomList = chatRoomList,
navigateTo = { navigationHelper.navigateTo(NavigationEvent.NavigateTo(it)) },
)
} else {
// Todo : 스켈레톤 UI 혹은 스피너 로딩
}
}
}
}
Expand Down
Loading

0 comments on commit c37d25c

Please sign in to comment.