feat(live): 온에어 라이브 매핑을 추가한다
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.live.onair.model
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.live.onair.data.HomeOnAirLivePageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.live.onair.data.HomeOnAirLiveResponse
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
fun HomeOnAirLivePageResponse.toUiState(
|
||||||
|
deviceTimeZone: TimeZone = TimeZone.getDefault()
|
||||||
|
): HomeOnAirLiveUiState = HomeOnAirLiveUiState(
|
||||||
|
items = items.map { it.toUiModel(deviceTimeZone) },
|
||||||
|
page = page,
|
||||||
|
size = size,
|
||||||
|
hasNext = hasNext
|
||||||
|
)
|
||||||
|
|
||||||
|
fun HomeOnAirLiveResponse.toUiModel(
|
||||||
|
deviceTimeZone: TimeZone = TimeZone.getDefault()
|
||||||
|
): HomeOnAirLiveUiModel = HomeOnAirLiveUiModel(
|
||||||
|
roomId = roomId,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
creatorProfileImage = creatorProfileImage,
|
||||||
|
title = title,
|
||||||
|
liveTimeText = "LIVE ${beginDateTimeUtc.toLiveStartTime(deviceTimeZone)}",
|
||||||
|
price = price.toPriceUiModel()
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Int.toPriceUiModel(): HomeOnAirLivePriceUiModel {
|
||||||
|
return if (this > 0) {
|
||||||
|
HomeOnAirLivePriceUiModel.Paid(
|
||||||
|
amount = this,
|
||||||
|
amountText = NumberFormat.getNumberInstance(Locale.KOREA).format(this)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
HomeOnAirLivePriceUiModel.Free
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toLiveStartTime(deviceTimeZone: TimeZone): String {
|
||||||
|
val parsedDate = UTC_DATE_FORMATS.firstNotNullOfOrNull { format ->
|
||||||
|
try {
|
||||||
|
format.parse(this)
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: return "--:--"
|
||||||
|
|
||||||
|
return SimpleDateFormat("HH:mm", Locale.US).apply {
|
||||||
|
timeZone = deviceTimeZone
|
||||||
|
}.format(parsedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val UTC_DATE_FORMATS = listOf(
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss"
|
||||||
|
).map { pattern ->
|
||||||
|
SimpleDateFormat(pattern, Locale.US).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.live.onair.model
|
||||||
|
|
||||||
|
data class HomeOnAirLiveUiState(
|
||||||
|
val items: List<HomeOnAirLiveUiModel>,
|
||||||
|
val page: Int,
|
||||||
|
val size: Int,
|
||||||
|
val hasNext: Boolean,
|
||||||
|
val isLoadingMore: Boolean = false,
|
||||||
|
val paginationErrorMessage: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HomeOnAirLiveUiModel(
|
||||||
|
val roomId: Long,
|
||||||
|
val creatorNickname: String,
|
||||||
|
val creatorProfileImage: String,
|
||||||
|
val title: String,
|
||||||
|
val liveTimeText: String,
|
||||||
|
val price: HomeOnAirLivePriceUiModel
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface HomeOnAirLivePriceUiModel {
|
||||||
|
data class Paid(
|
||||||
|
val amount: Int,
|
||||||
|
val amountText: String
|
||||||
|
) : HomeOnAirLivePriceUiModel
|
||||||
|
|
||||||
|
data object Free : HomeOnAirLivePriceUiModel
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface HomeOnAirLivePageUiState {
|
||||||
|
data object Loading : HomeOnAirLivePageUiState
|
||||||
|
data object Empty : HomeOnAirLivePageUiState
|
||||||
|
data class Content(val content: HomeOnAirLiveUiState) : HomeOnAirLivePageUiState
|
||||||
|
data class Error(val message: String?) : HomeOnAirLivePageUiState
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.live.onair
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.live.onair.data.HomeOnAirLivePageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.live.onair.data.HomeOnAirLiveResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.live.onair.model.HomeOnAirLivePriceUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.live.onair.model.toUiState
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
class HomeOnAirLiveMapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `beginDateTimeUtc를 디바이스 Timezone 기준 LIVE HH mm으로 변환한다`() {
|
||||||
|
val state = pageResponse(
|
||||||
|
items = listOf(live(beginDateTimeUtc = "2026-06-26T12:34:00Z"))
|
||||||
|
).toUiState(TimeZone.getTimeZone("Asia/Seoul"))
|
||||||
|
|
||||||
|
assertEquals("LIVE 21:34", state.items.single().liveTimeText)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `price가 0보다 크면 유료 가격 표시 모델로 매핑한다`() {
|
||||||
|
val price = pageResponse(items = listOf(live(price = 150))).toUiState().items.single().price
|
||||||
|
|
||||||
|
assertTrue(price is HomeOnAirLivePriceUiModel.Paid)
|
||||||
|
assertEquals(150, (price as HomeOnAirLivePriceUiModel.Paid).amount)
|
||||||
|
assertEquals("150", price.amountText)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `price가 0이면 무료 표시 모델로 매핑한다`() {
|
||||||
|
val price = pageResponse(items = listOf(live(price = 0))).toUiState().items.single().price
|
||||||
|
|
||||||
|
assertTrue(price is HomeOnAirLivePriceUiModel.Free)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `page 응답의 metadata와 items를 UI state로 유지한다`() {
|
||||||
|
val state = pageResponse(
|
||||||
|
page = 2,
|
||||||
|
size = 20,
|
||||||
|
hasNext = true,
|
||||||
|
items = listOf(live(roomId = 10L), live(roomId = 11L))
|
||||||
|
).toUiState()
|
||||||
|
|
||||||
|
assertEquals(2, state.page)
|
||||||
|
assertEquals(20, state.size)
|
||||||
|
assertTrue(state.hasNext)
|
||||||
|
assertEquals(listOf(10L, 11L), state.items.map { it.roomId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pageResponse(
|
||||||
|
items: List<HomeOnAirLiveResponse> = listOf(live()),
|
||||||
|
page: Int = 0,
|
||||||
|
size: Int = 20,
|
||||||
|
hasNext: Boolean = false
|
||||||
|
) = HomeOnAirLivePageResponse(
|
||||||
|
items = items,
|
||||||
|
page = page,
|
||||||
|
size = size,
|
||||||
|
hasNext = hasNext
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun live(
|
||||||
|
roomId: Long = 1L,
|
||||||
|
creatorNickname: String = "크리에이터",
|
||||||
|
creatorProfileImage: String = "https://example.com/profile.png",
|
||||||
|
title: String = "라이브 제목",
|
||||||
|
price: Int = 100,
|
||||||
|
beginDateTimeUtc: String = "2026-06-26T12:00:00Z"
|
||||||
|
) = HomeOnAirLiveResponse(
|
||||||
|
roomId = roomId,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
creatorProfileImage = creatorProfileImage,
|
||||||
|
title = title,
|
||||||
|
price = price,
|
||||||
|
beginDateTimeUtc = beginDateTimeUtc
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user