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