feat(content): 추천 UI 모델 매핑을 추가한다
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.content.model
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioBannerResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioCardResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioRecommendationsResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.CommentedAudioResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.OriginalSeriesResponse
|
||||
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
||||
|
||||
fun AudioRecommendationsResponse.toContent(): AudioRecommendationsUiState.Content = AudioRecommendationsUiState.Content(
|
||||
banners = ContentBannerSection(banners.map { it.toUiModel() }),
|
||||
originalSeries = ContentOriginalSeriesSection(originalSeries.map { it.toUiModel() }),
|
||||
latestAudios = ContentAudioCardSection(latestAudios.map { it.toUiModel() }),
|
||||
newAndHotAudios = ContentAudioCardSection(newAndHotAudios.map { it.toUiModel() }),
|
||||
freeAudios = ContentAudioCardSection(freeAudios.map { it.toUiModel() }),
|
||||
pointAudios = ContentAudioCardSection(pointAudios.map { it.toUiModel() }),
|
||||
mostCommentedAudios = ContentCommentedAudioSection(mostCommentedAudios.map { it.toUiModel() }),
|
||||
recommendedAudios = ContentAudioCardSection(recommendedAudios.map { it.toUiModel() })
|
||||
)
|
||||
|
||||
fun AudioBannerResponse.toUiModel(): ContentBannerUiModel = ContentBannerUiModel(
|
||||
imageUrl = imageUrl,
|
||||
eventItem = eventItem,
|
||||
creatorId = creatorId,
|
||||
seriesId = seriesId,
|
||||
link = link
|
||||
)
|
||||
|
||||
fun OriginalSeriesResponse.toUiModel(): ContentOriginalSeriesUiModel = ContentOriginalSeriesUiModel(
|
||||
seriesId = seriesId,
|
||||
coverImageUrl = coverImageUrl
|
||||
)
|
||||
|
||||
fun AudioCardResponse.toUiModel(): ContentAudioCardUiModel = ContentAudioCardUiModel(
|
||||
audioContentId = audioContentId,
|
||||
title = title,
|
||||
imageUrl = imageUrl,
|
||||
price = price,
|
||||
creatorNickname = creatorNickname,
|
||||
tags = toAudioContentTags(),
|
||||
showAdultBadge = isAdult
|
||||
)
|
||||
|
||||
fun CommentedAudioResponse.toUiModel(): ContentCommentedAudioUiModel = ContentCommentedAudioUiModel(
|
||||
audioContentId = audioContentId,
|
||||
title = title,
|
||||
imageUrl = imageUrl,
|
||||
latestComment = latestComment,
|
||||
latestCommentWriterProfileImageUrl = latestCommentWriterProfileImageUrl,
|
||||
showLatestComment = latestComment.isNotBlank()
|
||||
)
|
||||
|
||||
private fun AudioCardResponse.toAudioContentTags(): Set<AudioContentTag> = buildSet {
|
||||
if (isOriginalSeries) add(AudioContentTag.Original)
|
||||
if (isFirstContent) add(AudioContentTag.First)
|
||||
if (isPointAvailable) add(AudioContentTag.Point)
|
||||
if (price == 0) add(AudioContentTag.Free)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.content.model
|
||||
|
||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
||||
|
||||
data class ContentBannerSection(
|
||||
val items: List<ContentBannerUiModel>
|
||||
)
|
||||
|
||||
data class ContentOriginalSeriesSection(
|
||||
val items: List<ContentOriginalSeriesUiModel>
|
||||
)
|
||||
|
||||
data class ContentAudioCardSection(
|
||||
val items: List<ContentAudioCardUiModel>
|
||||
)
|
||||
|
||||
data class ContentCommentedAudioSection(
|
||||
val items: List<ContentCommentedAudioUiModel>
|
||||
)
|
||||
|
||||
data class ContentBannerUiModel(
|
||||
val imageUrl: String,
|
||||
val eventItem: EventItem?,
|
||||
val creatorId: Long?,
|
||||
val seriesId: Long?,
|
||||
val link: String?
|
||||
)
|
||||
|
||||
data class ContentOriginalSeriesUiModel(
|
||||
val seriesId: Long,
|
||||
val coverImageUrl: String?
|
||||
)
|
||||
|
||||
data class ContentAudioCardUiModel(
|
||||
val audioContentId: Long,
|
||||
val title: String,
|
||||
val imageUrl: String?,
|
||||
val price: Int,
|
||||
val creatorNickname: String,
|
||||
val tags: Set<AudioContentTag>,
|
||||
val showAdultBadge: Boolean
|
||||
)
|
||||
|
||||
data class ContentCommentedAudioUiModel(
|
||||
val audioContentId: Long,
|
||||
val title: String,
|
||||
val imageUrl: String?,
|
||||
val latestComment: String,
|
||||
val latestCommentWriterProfileImageUrl: String,
|
||||
val showLatestComment: Boolean
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.content.model
|
||||
|
||||
sealed interface AudioRecommendationsUiState {
|
||||
data object Loading : AudioRecommendationsUiState
|
||||
|
||||
data class Content(
|
||||
val banners: ContentBannerSection,
|
||||
val originalSeries: ContentOriginalSeriesSection,
|
||||
val latestAudios: ContentAudioCardSection,
|
||||
val newAndHotAudios: ContentAudioCardSection,
|
||||
val freeAudios: ContentAudioCardSection,
|
||||
val pointAudios: ContentAudioCardSection,
|
||||
val mostCommentedAudios: ContentCommentedAudioSection,
|
||||
val recommendedAudios: ContentAudioCardSection
|
||||
) : AudioRecommendationsUiState {
|
||||
val isEmpty: Boolean = listOf(
|
||||
banners.items,
|
||||
originalSeries.items,
|
||||
latestAudios.items,
|
||||
newAndHotAudios.items,
|
||||
freeAudios.items,
|
||||
pointAudios.items,
|
||||
mostCommentedAudios.items,
|
||||
recommendedAudios.items
|
||||
).all { it.isEmpty() }
|
||||
}
|
||||
|
||||
data object Empty : AudioRecommendationsUiState
|
||||
|
||||
data class Error(
|
||||
val message: String? = null
|
||||
) : AudioRecommendationsUiState
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.content
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioBannerResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioCardResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioRecommendationsResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.CommentedAudioResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.OriginalSeriesResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContent
|
||||
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AudioRecommendationsMapperTest {
|
||||
|
||||
@Test
|
||||
fun `price가 0이면 무료 tag가 포함된다`() {
|
||||
val item = response(latestAudios = listOf(audio(price = 0))).toContent().latestAudios.items.single()
|
||||
|
||||
assertTrue(AudioContentTag.Free in item.tags)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `포인트 첫 콘텐츠 오리지널 시리즈 flag는 tag로 매핑된다`() {
|
||||
val item = response(
|
||||
latestAudios = listOf(
|
||||
audio(
|
||||
isPointAvailable = true,
|
||||
isFirstContent = true,
|
||||
isOriginalSeries = true
|
||||
)
|
||||
)
|
||||
).toContent().latestAudios.items.single()
|
||||
|
||||
assertTrue(AudioContentTag.Point in item.tags)
|
||||
assertTrue(AudioContentTag.First in item.tags)
|
||||
assertTrue(AudioContentTag.Original in item.tags)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `성인 콘텐츠 flag는 showAdultBadge로 매핑된다`() {
|
||||
val item = response(latestAudios = listOf(audio(isAdult = true))).toContent().latestAudios.items.single()
|
||||
|
||||
assertTrue(item.showAdultBadge)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `duration은 공통 오디오 UI model에 노출하지 않는다`() {
|
||||
val fields = response(latestAudios = listOf(audio(duration = "10:00")))
|
||||
.toContent()
|
||||
.latestAudios
|
||||
.items
|
||||
.single()
|
||||
.javaClass
|
||||
.declaredFields
|
||||
.map { it.name }
|
||||
|
||||
assertFalse(fields.contains("duration"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `빈 리스트 섹션은 content의 empty 판단에 반영된다`() {
|
||||
val content = response().toContent()
|
||||
|
||||
assertTrue(content.isEmpty)
|
||||
assertEquals(emptyList<Any>(), content.latestAudios.items)
|
||||
assertEquals(emptyList<Any>(), content.recommendedAudios.items)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `latestComment가 blank이면 댓글 영역 표시 flag가 false다`() {
|
||||
val item = response(mostCommentedAudios = listOf(commentedAudio(latestComment = " ")))
|
||||
.toContent()
|
||||
.mostCommentedAudios
|
||||
.items
|
||||
.single()
|
||||
|
||||
assertFalse(item.showLatestComment)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `응답 리스트는 섹션별 UI model로 매핑된다`() {
|
||||
val content = response(
|
||||
banners = listOf(banner()),
|
||||
originalSeries = listOf(originalSeries()),
|
||||
latestAudios = listOf(audio(audioContentId = 1L)),
|
||||
newAndHotAudios = listOf(audio(audioContentId = 2L)),
|
||||
freeAudios = listOf(audio(audioContentId = 3L)),
|
||||
pointAudios = listOf(audio(audioContentId = 4L)),
|
||||
mostCommentedAudios = listOf(commentedAudio(audioContentId = 5L)),
|
||||
recommendedAudios = listOf(audio(audioContentId = 6L))
|
||||
).toContent()
|
||||
|
||||
assertFalse(content.isEmpty)
|
||||
assertEquals(1, content.banners.items.size)
|
||||
assertEquals(10L, content.originalSeries.items.single().seriesId)
|
||||
assertEquals(1L, content.latestAudios.items.single().audioContentId)
|
||||
assertEquals(2L, content.newAndHotAudios.items.single().audioContentId)
|
||||
assertEquals(3L, content.freeAudios.items.single().audioContentId)
|
||||
assertEquals(4L, content.pointAudios.items.single().audioContentId)
|
||||
assertEquals(5L, content.mostCommentedAudios.items.single().audioContentId)
|
||||
assertEquals(6L, content.recommendedAudios.items.single().audioContentId)
|
||||
}
|
||||
|
||||
private fun response(
|
||||
banners: List<AudioBannerResponse> = emptyList(),
|
||||
originalSeries: List<OriginalSeriesResponse> = emptyList(),
|
||||
latestAudios: List<AudioCardResponse> = emptyList(),
|
||||
newAndHotAudios: List<AudioCardResponse> = emptyList(),
|
||||
freeAudios: List<AudioCardResponse> = emptyList(),
|
||||
pointAudios: List<AudioCardResponse> = emptyList(),
|
||||
mostCommentedAudios: List<CommentedAudioResponse> = emptyList(),
|
||||
recommendedAudios: List<AudioCardResponse> = emptyList()
|
||||
) = AudioRecommendationsResponse(
|
||||
banners = banners,
|
||||
originalSeries = originalSeries,
|
||||
latestAudios = latestAudios,
|
||||
newAndHotAudios = newAndHotAudios,
|
||||
freeAudios = freeAudios,
|
||||
pointAudios = pointAudios,
|
||||
mostCommentedAudios = mostCommentedAudios,
|
||||
recommendedAudios = recommendedAudios
|
||||
)
|
||||
|
||||
private fun banner() = AudioBannerResponse(
|
||||
imageUrl = "https://example.com/banner.png",
|
||||
eventItem = null,
|
||||
creatorId = null,
|
||||
seriesId = null,
|
||||
link = null
|
||||
)
|
||||
|
||||
private fun originalSeries() = OriginalSeriesResponse(
|
||||
seriesId = 10L,
|
||||
coverImageUrl = "https://example.com/series.png"
|
||||
)
|
||||
|
||||
private fun audio(
|
||||
audioContentId: Long = 1L,
|
||||
title: String = "오디오",
|
||||
duration: String? = "10:00",
|
||||
imageUrl: String? = "https://example.com/audio.png",
|
||||
price: Int = 100,
|
||||
isAdult: Boolean = false,
|
||||
isPointAvailable: Boolean = false,
|
||||
isFirstContent: Boolean = false,
|
||||
isOriginalSeries: Boolean = false,
|
||||
creatorNickname: String = "크리에이터"
|
||||
) = AudioCardResponse(
|
||||
audioContentId = audioContentId,
|
||||
title = title,
|
||||
duration = duration,
|
||||
imageUrl = imageUrl,
|
||||
price = price,
|
||||
isAdult = isAdult,
|
||||
isPointAvailable = isPointAvailable,
|
||||
isFirstContent = isFirstContent,
|
||||
isOriginalSeries = isOriginalSeries,
|
||||
creatorNickname = creatorNickname
|
||||
)
|
||||
|
||||
private fun commentedAudio(
|
||||
audioContentId: Long = 1L,
|
||||
title: String = "댓글 오디오",
|
||||
imageUrl: String? = "https://example.com/commented.png",
|
||||
latestComment: String = "좋아요",
|
||||
latestCommentWriterProfileImageUrl: String = "https://example.com/profile.png"
|
||||
) = CommentedAudioResponse(
|
||||
audioContentId = audioContentId,
|
||||
title = title,
|
||||
imageUrl = imageUrl,
|
||||
latestComment = latestComment,
|
||||
latestCommentWriterProfileImageUrl = latestCommentWriterProfileImageUrl
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user