feat(chat): DM 채팅 메시지 매퍼를 추가한다
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat.dm.model
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessageResponse
|
||||||
|
|
||||||
|
private const val MESSAGE_TYPE_TEXT = "TEXT"
|
||||||
|
|
||||||
|
fun DmChatMessageResponse.toUiItem(): DmChatMessageUiItem? {
|
||||||
|
if (!messageType.equals(MESSAGE_TYPE_TEXT, ignoreCase = true) || textMessage == null) return null
|
||||||
|
|
||||||
|
return DmChatMessageUiItem(
|
||||||
|
messageId = messageId,
|
||||||
|
localId = null,
|
||||||
|
mine = mine,
|
||||||
|
textMessage = textMessage,
|
||||||
|
senderNickname = senderNickname,
|
||||||
|
senderProfileImageUrl = senderProfileImageUrl,
|
||||||
|
createdAt = createdAt,
|
||||||
|
status = DmChatMessageStatus.SENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<DmChatMessageResponse>.toUiItems(): List<DmChatMessageUiItem> = mapNotNull { it.toUiItem() }
|
||||||
|
|
||||||
|
/** Sorts messages by server creation time, then by messageId for same-time messages. */
|
||||||
|
fun List<DmChatMessageUiItem>.sortByCreatedAtAndMessageId(): List<DmChatMessageUiItem> =
|
||||||
|
sortedWith(compareBy<DmChatMessageUiItem> { it.createdAt }.thenBy { it.messageId ?: Long.MAX_VALUE })
|
||||||
|
|
||||||
|
/** Merges new messages while keeping the first-arrived item when server messageId duplicates. */
|
||||||
|
fun List<DmChatMessageUiItem>.mergeByMessageId(
|
||||||
|
incoming: List<DmChatMessageUiItem>
|
||||||
|
): List<DmChatMessageUiItem> {
|
||||||
|
val seenMessageIds = mapNotNull { it.messageId }.toMutableSet()
|
||||||
|
val newItems = incoming.filter { item ->
|
||||||
|
val messageId = item.messageId
|
||||||
|
messageId == null || seenMessageIds.add(messageId)
|
||||||
|
}
|
||||||
|
return (this + newItems).sortByCreatedAtAndMessageId()
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat.dm.model
|
||||||
|
|
||||||
|
enum class DmChatMessageStatus {
|
||||||
|
SENDING,
|
||||||
|
SENT,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DmChatMessageUiItem(
|
||||||
|
val messageId: Long?,
|
||||||
|
val localId: String?,
|
||||||
|
val mine: Boolean,
|
||||||
|
val textMessage: String,
|
||||||
|
val senderNickname: String,
|
||||||
|
val senderProfileImageUrl: String,
|
||||||
|
val createdAt: Long,
|
||||||
|
val status: DmChatMessageStatus
|
||||||
|
)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatMessageStatus
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.mergeByMessageId
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.sortByCreatedAtAndMessageId
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.toUiItem
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.toUiItems
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DmChatMapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `TEXT 메시지는 전송 완료 UI item으로 매핑된다`() {
|
||||||
|
val item = response(messageType = "TEXT", textMessage = "안녕하세요").toUiItem()
|
||||||
|
|
||||||
|
requireNotNull(item)
|
||||||
|
assertEquals(10L, item.messageId)
|
||||||
|
assertNull(item.localId)
|
||||||
|
assertEquals(true, item.mine)
|
||||||
|
assertEquals("안녕하세요", item.textMessage)
|
||||||
|
assertEquals("크리에이터", item.senderNickname)
|
||||||
|
assertEquals("https://example.com/profile.png", item.senderProfileImageUrl)
|
||||||
|
assertEquals(1000L, item.createdAt)
|
||||||
|
assertEquals(DmChatMessageStatus.SENT, item.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `messageType은 대소문자를 무시하고 TEXT로 매핑된다`() {
|
||||||
|
val item = response(messageType = "text", textMessage = "소문자 타입").toUiItem()
|
||||||
|
|
||||||
|
requireNotNull(item)
|
||||||
|
assertEquals("소문자 타입", item.textMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `TEXT가 아니거나 textMessage가 null이면 UI item에서 제외된다`() {
|
||||||
|
val voice = response(messageType = "VOICE", textMessage = null, voiceMessageUrl = "https://example.com/voice.m4a")
|
||||||
|
val nullText = response(messageType = "TEXT", textMessage = null)
|
||||||
|
|
||||||
|
assertNull(voice.toUiItem())
|
||||||
|
assertNull(nullText.toUiItem())
|
||||||
|
assertEquals(emptyList<Any>(), listOf(voice, nullText).toUiItems())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `메시지는 createdAt 이후 messageId 오름차순으로 정렬된다`() {
|
||||||
|
val items = listOf(
|
||||||
|
response(messageId = 3L, createdAt = 2000L),
|
||||||
|
response(messageId = 2L, createdAt = 1000L),
|
||||||
|
response(messageId = 1L, createdAt = 1000L)
|
||||||
|
).toUiItems().sortByCreatedAtAndMessageId()
|
||||||
|
|
||||||
|
assertEquals(listOf(1L, 2L, 3L), items.map { it.messageId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `messageId가 중복된 서버 메시지는 병합 시 기존 item을 유지한다`() {
|
||||||
|
val existing = listOf(
|
||||||
|
response(messageId = 1L, textMessage = "기존").toUiItem(),
|
||||||
|
response(messageId = 2L, textMessage = "둘째").toUiItem()
|
||||||
|
).filterNotNull()
|
||||||
|
val incoming = listOf(
|
||||||
|
response(messageId = 1L, textMessage = "중복 신규").toUiItem(),
|
||||||
|
response(messageId = 3L, textMessage = "셋째").toUiItem()
|
||||||
|
).filterNotNull()
|
||||||
|
|
||||||
|
val merged = existing.mergeByMessageId(incoming)
|
||||||
|
|
||||||
|
assertEquals(listOf(1L, 2L, 3L), merged.map { it.messageId })
|
||||||
|
assertEquals("기존", merged.first { it.messageId == 1L }.textMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun response(
|
||||||
|
messageId: Long = 10L,
|
||||||
|
messageType: String = "TEXT",
|
||||||
|
mine: Boolean = true,
|
||||||
|
createdAt: Long = 1000L,
|
||||||
|
textMessage: String? = "메시지",
|
||||||
|
voiceMessageUrl: String? = null,
|
||||||
|
senderId: Long = 20L,
|
||||||
|
senderNickname: String = "크리에이터",
|
||||||
|
senderProfileImageUrl: String = "https://example.com/profile.png"
|
||||||
|
) = DmChatMessageResponse(
|
||||||
|
messageId = messageId,
|
||||||
|
messageType = messageType,
|
||||||
|
mine = mine,
|
||||||
|
createdAt = createdAt,
|
||||||
|
textMessage = textMessage,
|
||||||
|
voiceMessageUrl = voiceMessageUrl,
|
||||||
|
senderId = senderId,
|
||||||
|
senderNickname = senderNickname,
|
||||||
|
senderProfileImageUrl = senderProfileImageUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user