From c3377e39e6455359b6088de4ff7f1b8de3144c5d Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 26 Jun 2026 23:43:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(live):=20=EC=98=A8=EC=97=90=EC=96=B4=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A7=A4=ED=95=91=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../live/onair/model/HomeOnAirLiveMappers.kt | 65 +++++++++++++++ .../live/onair/model/HomeOnAirLiveUiModels.kt | 35 ++++++++ .../v2/live/onair/HomeOnAirLiveMapperTest.kt | 81 +++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveMappers.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveUiModels.kt create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveMapperTest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveMappers.kt new file mode 100644 index 00000000..6c30403c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveMappers.kt @@ -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") + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveUiModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveUiModels.kt new file mode 100644 index 00000000..ad9e745e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveUiModels.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.v2.live.onair.model + +data class HomeOnAirLiveUiState( + val items: List, + 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 +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveMapperTest.kt new file mode 100644 index 00000000..70ef906a --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveMapperTest.kt @@ -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 = 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 + ) +}