feat(creator): 오디오 탭 mapper를 추가한다
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.audio.model
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
||||||
|
|
||||||
|
private const val ALL_THEME_TITLE = "전체"
|
||||||
|
|
||||||
|
fun CreatorChannelAudioTabResponse.toThemeUiModels(): List<CreatorChannelAudioThemeUiModel> =
|
||||||
|
listOf(
|
||||||
|
CreatorChannelAudioThemeUiModel(
|
||||||
|
themeId = null,
|
||||||
|
title = ALL_THEME_TITLE,
|
||||||
|
isSelected = effectiveSelectedThemeId() == null
|
||||||
|
)
|
||||||
|
) +
|
||||||
|
themes.map { theme ->
|
||||||
|
CreatorChannelAudioThemeUiModel(
|
||||||
|
themeId = theme.themeId,
|
||||||
|
title = theme.themeName,
|
||||||
|
isSelected = theme.themeId == effectiveSelectedThemeId()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CreatorChannelAudioTabResponse.effectiveSelectedThemeId(): Long? =
|
||||||
|
themeId?.takeIf { selectedThemeId -> themes.any { it.themeId == selectedThemeId } }
|
||||||
|
|
||||||
|
fun CreatorChannelAudioTabResponse.toRateUiModel(isOwner: Boolean): CreatorChannelAudioRateUiModel? =
|
||||||
|
if (!isOwner && effectiveSelectedThemeId() == null) {
|
||||||
|
CreatorChannelAudioRateUiModel(
|
||||||
|
ratePercent = purchasedAudioContentRate,
|
||||||
|
purchasedCount = purchasedAudioContentCount,
|
||||||
|
paidCount = paidAudioContentCount
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<CreatorChannelAudioContentResponse>.toAudioContentUiModels(): List<CreatorChannelAudioContentUiModel> =
|
||||||
|
mapNotNull { it.toAudioContentUiModel() }
|
||||||
|
|
||||||
|
private fun CreatorChannelAudioContentResponse.toAudioContentUiModel(): CreatorChannelAudioContentUiModel? {
|
||||||
|
val duration = duration ?: return null
|
||||||
|
return CreatorChannelAudioContentUiModel(
|
||||||
|
audioContentId = audioContentId,
|
||||||
|
title = title,
|
||||||
|
secondaryText = secondaryText(duration),
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
price = price,
|
||||||
|
showAdultBadge = isAdult,
|
||||||
|
tags = toAudioContentTags(),
|
||||||
|
status = toAudioContentStatus()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CreatorChannelAudioContentResponse.secondaryText(duration: String): String =
|
||||||
|
if (seriesName.isNullOrBlank()) {
|
||||||
|
duration
|
||||||
|
} else {
|
||||||
|
"$duration • $seriesName"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CreatorChannelAudioContentResponse.toAudioContentTags(): Set<AudioContentTag> = buildSet {
|
||||||
|
if (isOriginalSeries == true) add(AudioContentTag.Original)
|
||||||
|
if (isFirstContent) add(AudioContentTag.First)
|
||||||
|
if (isPointAvailable) add(AudioContentTag.Point)
|
||||||
|
if (price == 0) add(AudioContentTag.Free)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CreatorChannelAudioContentResponse.toAudioContentStatus(): CreatorChannelAudioContentStatus = when {
|
||||||
|
isOwned -> CreatorChannelAudioContentStatus.Owned
|
||||||
|
isRented -> CreatorChannelAudioContentStatus.Rented
|
||||||
|
price == 0 -> CreatorChannelAudioContentStatus.Play
|
||||||
|
else -> CreatorChannelAudioContentStatus.Price(price)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.audio.model
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
||||||
|
|
||||||
|
data class CreatorChannelAudioThemeUiModel(
|
||||||
|
val themeId: Long?,
|
||||||
|
val title: String,
|
||||||
|
val isSelected: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelAudioRateUiModel(
|
||||||
|
val ratePercent: Double,
|
||||||
|
val purchasedCount: Int,
|
||||||
|
val paidCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelAudioContentUiModel(
|
||||||
|
val audioContentId: Long,
|
||||||
|
val title: String,
|
||||||
|
val secondaryText: String,
|
||||||
|
val imageUrl: String?,
|
||||||
|
val price: Int,
|
||||||
|
val showAdultBadge: Boolean,
|
||||||
|
val tags: Set<AudioContentTag>,
|
||||||
|
val status: CreatorChannelAudioContentStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface CreatorChannelAudioContentStatus {
|
||||||
|
data object Play : CreatorChannelAudioContentStatus
|
||||||
|
data object Owned : CreatorChannelAudioContentStatus
|
||||||
|
data object Rented : CreatorChannelAudioContentStatus
|
||||||
|
data class Price(val price: Int) : CreatorChannelAudioContentStatus
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.audio
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioThemeResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.CreatorChannelAudioContentStatus
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.CreatorChannelAudioRateUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.effectiveSelectedThemeId
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.toAudioContentUiModels
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.toRateUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.toThemeUiModels
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class CreatorChannelAudioMapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `themes 응답 앞에 전체 tab을 추가하고 themeId null이면 전체를 선택한다`() {
|
||||||
|
val themes = audioResponse(themeId = null).toThemeUiModels()
|
||||||
|
|
||||||
|
assertEquals(listOf(null, 10L, 20L), themes.map { it.themeId })
|
||||||
|
assertEquals("전체", themes.first().title)
|
||||||
|
assertTrue(themes.first().isSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `응답 themeId가 특정 id이면 해당 서버 theme tab만 selected 상태다`() {
|
||||||
|
val themes = audioResponse(themeId = 20L).toThemeUiModels()
|
||||||
|
|
||||||
|
assertEquals(listOf(false, false, true), themes.map { it.isSelected })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `응답 themeId가 서버 themes에 없으면 전체 tab을 selected 상태로 fallback한다`() {
|
||||||
|
val themes = audioResponse(themeId = 99L).toThemeUiModels()
|
||||||
|
|
||||||
|
assertEquals(listOf(true, false, false), themes.map { it.isSelected })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `응답 themeId가 서버 themes에 없으면 effective selected themeId는 null이다`() {
|
||||||
|
val response = audioResponse(themeId = 99L)
|
||||||
|
|
||||||
|
assertNull(response.effectiveSelectedThemeId())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `seriesName이 있으면 secondary text는 duration bullet seriesName이다`() {
|
||||||
|
val item = listOf(audioContent(duration = "10:00", seriesName = "시리즈명")).toAudioContentUiModels().single()
|
||||||
|
|
||||||
|
assertEquals("10:00 • 시리즈명", item.secondaryText)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `seriesName이 null 또는 blank이면 secondary text는 duration만 사용한다`() {
|
||||||
|
val items = listOf(
|
||||||
|
audioContent(audioContentId = 1L, duration = "10:00", seriesName = null),
|
||||||
|
audioContent(audioContentId = 2L, duration = "11:00", seriesName = " ")
|
||||||
|
).toAudioContentUiModels()
|
||||||
|
|
||||||
|
assertEquals(listOf("10:00", "11:00"), items.map { it.secondaryText })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `duration null item은 mapper 결과에서 제외한다`() {
|
||||||
|
val items = listOf(
|
||||||
|
audioContent(audioContentId = 1L, duration = null),
|
||||||
|
audioContent(audioContentId = 2L, duration = "10:00")
|
||||||
|
).toAudioContentUiModels()
|
||||||
|
|
||||||
|
assertEquals(listOf(2L), items.map { it.audioContentId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `소장과 대여가 동시에 true이면 소장중 상태를 우선 매핑한다`() {
|
||||||
|
val item = listOf(audioContent(isOwned = true, isRented = true)).toAudioContentUiModels().single()
|
||||||
|
|
||||||
|
assertEquals(CreatorChannelAudioContentStatus.Owned, item.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `무료 콘텐츠는 무료 tag와 play CTA 상태로 매핑한다`() {
|
||||||
|
val item = listOf(audioContent(price = 0)).toAudioContentUiModels().single()
|
||||||
|
|
||||||
|
assertEquals(CreatorChannelAudioContentStatus.Play, item.status)
|
||||||
|
assertTrue(AudioContentTag.Free in item.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `유료 미보유 콘텐츠는 price 상태로 매핑한다`() {
|
||||||
|
val item = listOf(audioContent(price = 500)).toAudioContentUiModels().single()
|
||||||
|
|
||||||
|
assertEquals(CreatorChannelAudioContentStatus.Price(500), item.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `성인 포인트 첫 콘텐츠 오리지널 시리즈 tag와 badge는 라이브 item 정책과 동일하게 매핑한다`() {
|
||||||
|
val item = listOf(
|
||||||
|
audioContent(
|
||||||
|
isAdult = true,
|
||||||
|
isPointAvailable = true,
|
||||||
|
isFirstContent = true,
|
||||||
|
isOriginalSeries = true
|
||||||
|
)
|
||||||
|
).toAudioContentUiModels().single()
|
||||||
|
|
||||||
|
assertTrue(item.showAdultBadge)
|
||||||
|
assertTrue(AudioContentTag.Point in item.tags)
|
||||||
|
assertTrue(AudioContentTag.First in item.tags)
|
||||||
|
assertTrue(AudioContentTag.Original in item.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `소장률은 내 채널이 아니고 전체 테마일 때만 생성된다`() {
|
||||||
|
val response = audioResponse(themeId = null)
|
||||||
|
|
||||||
|
assertEquals(CreatorChannelAudioRateUiModel(75.0, 3, 4), response.toRateUiModel(isOwner = false))
|
||||||
|
assertNull(response.toRateUiModel(isOwner = true))
|
||||||
|
assertNull(audioResponse(themeId = 10L).toRateUiModel(isOwner = false))
|
||||||
|
assertEquals(CreatorChannelAudioRateUiModel(75.0, 3, 4), audioResponse(themeId = 99L).toRateUiModel(isOwner = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun audioResponse(themeId: Long?) = CreatorChannelAudioTabResponse(
|
||||||
|
audioContentCount = 1,
|
||||||
|
themes = listOf(
|
||||||
|
CreatorChannelAudioThemeResponse(themeId = 10L, themeName = "ASMR"),
|
||||||
|
CreatorChannelAudioThemeResponse(themeId = 20L, themeName = "수면")
|
||||||
|
),
|
||||||
|
themeId = themeId,
|
||||||
|
purchasedAudioContentRate = 75.0,
|
||||||
|
purchasedAudioContentCount = 3,
|
||||||
|
paidAudioContentCount = 4,
|
||||||
|
audioContents = listOf(audioContent()),
|
||||||
|
sort = ContentSort.LATEST,
|
||||||
|
page = 0,
|
||||||
|
size = 20,
|
||||||
|
hasNext = false
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun audioContent(
|
||||||
|
audioContentId: Long = 1L,
|
||||||
|
price: Int = 10,
|
||||||
|
isPointAvailable: Boolean = false,
|
||||||
|
isFirstContent: Boolean = false,
|
||||||
|
seriesName: String? = null,
|
||||||
|
isOriginalSeries: Boolean? = false,
|
||||||
|
isAdult: Boolean = false,
|
||||||
|
isOwned: Boolean = false,
|
||||||
|
isRented: Boolean = false,
|
||||||
|
duration: String? = "10:00"
|
||||||
|
) = CreatorChannelAudioContentResponse(
|
||||||
|
audioContentId = audioContentId,
|
||||||
|
title = "오디오 $audioContentId",
|
||||||
|
duration = duration,
|
||||||
|
imageUrl = "https://example.com/audio.png",
|
||||||
|
price = price,
|
||||||
|
isPointAvailable = isPointAvailable,
|
||||||
|
isFirstContent = isFirstContent,
|
||||||
|
seriesName = seriesName,
|
||||||
|
isOriginalSeries = isOriginalSeries,
|
||||||
|
isAdult = isAdult,
|
||||||
|
isOwned = isOwned,
|
||||||
|
isRented = isRented
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user