diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewMappers.kt new file mode 100644 index 00000000..819ce1c0 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewMappers.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.v2.main.content.overview.model + +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewItemResponse +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewPageResponse +import kr.co.vividnext.sodalive.v2.widget.AudioContentTag + +fun ContentOverviewPageResponse.toContent(): ContentOverviewUiState.Content { + return ContentOverviewUiState.Content( + type = type, + totalCount = items.size, + items = items.map { it.toUiModel() }, + page = page, + size = size, + hasNext = hasNext + ) +} + +fun ContentOverviewItemResponse.toUiModel(): ContentOverviewUiModel = ContentOverviewUiModel( + contentId = contentId, + title = title, + coverImage = coverImage, + creatorNickname = creatorNickname, + tags = toAudioContentTags(), + showAdultBadge = isAdult +) + +private fun ContentOverviewItemResponse.toAudioContentTags(): Set = buildSet { + if (isOriginalSeries) add(AudioContentTag.Original) + if (isFirstContent) add(AudioContentTag.First) + if (isPointAvailable) add(AudioContentTag.Point) + if (price == 0) add(AudioContentTag.Free) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiModels.kt new file mode 100644 index 00000000..c374b22c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiModels.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.v2.main.content.overview.model + +import androidx.annotation.StringRes +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType +import kr.co.vividnext.sodalive.v2.widget.AudioContentTag + +data class ContentOverviewUiModel( + val contentId: Long, + val title: String, + val coverImage: String?, + val creatorNickname: String, + val tags: Set, + val showAdultBadge: Boolean +) + +@StringRes +fun ContentOverviewType.toTitleResId(): Int = when (this) { + ContentOverviewType.NEW_AND_HOT_AUDIO -> R.string.content_recommendation_section_new_and_hot + ContentOverviewType.FIRST_AUDIO_CONTENT -> R.string.home_recommendation_section_first_audio_contents +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiState.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiState.kt new file mode 100644 index 00000000..2ec60c2b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiState.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.v2.main.content.overview.model + +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType + +sealed interface ContentOverviewUiState { + data class Loading(val type: ContentOverviewType) : ContentOverviewUiState + data class Empty(val type: ContentOverviewType, val totalCount: Int) : ContentOverviewUiState + data class Error(val type: ContentOverviewType, val message: String?) : ContentOverviewUiState + data class Content( + val type: ContentOverviewType, + val totalCount: Int, + val items: List, + val page: Int, + val size: Int, + val hasNext: Boolean, + val isLoadingMore: Boolean = false, + val paginationErrorMessage: String? = null + ) : ContentOverviewUiState +} diff --git a/app/src/main/res/drawable-mdpi/ic_feed_community_heart_fill.png b/app/src/main/res/drawable-mdpi/ic_feed_community_heart_fill.png new file mode 100644 index 00000000..cd9968e5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_feed_community_heart_fill.png differ diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewMapperTest.kt new file mode 100644 index 00000000..2e759dd2 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewMapperTest.kt @@ -0,0 +1,112 @@ +package kr.co.vividnext.sodalive.v2.main.content.overview + +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewItemResponse +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewPageResponse +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType +import kr.co.vividnext.sodalive.v2.main.content.overview.model.toContent +import kr.co.vividnext.sodalive.v2.main.content.overview.model.toTitleResId +import kr.co.vividnext.sodalive.v2.main.content.overview.model.toUiModel +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 ContentOverviewMapperTest { + + @Test + fun `type title resource maps to overview screen title`() { + assertEquals( + R.string.content_recommendation_section_new_and_hot, + ContentOverviewType.NEW_AND_HOT_AUDIO.toTitleResId() + ) + assertEquals( + R.string.home_recommendation_section_first_audio_contents, + ContentOverviewType.FIRST_AUDIO_CONTENT.toTitleResId() + ) + } + + @Test + fun `audio item maps tags and adult badge`() { + val item = item( + price = 0, + isPointAvailable = true, + isFirstContent = true, + isOriginalSeries = true, + isAdult = true + ).toUiModel() + + assertEquals( + setOf( + AudioContentTag.Original, + AudioContentTag.First, + AudioContentTag.Point, + AudioContentTag.Free + ), + item.tags + ) + assertTrue(item.showAdultBadge) + } + + @Test + fun `paid non adult item has no free tag and hides adult badge`() { + val item = item(price = 500, isAdult = false).toUiModel() + + assertFalse(item.tags.contains(AudioContentTag.Free)) + assertFalse(item.showAdultBadge) + } + + @Test + fun `page response preserves paging metadata and type`() { + val content = pageResponse( + type = ContentOverviewType.FIRST_AUDIO_CONTENT, + page = 2, + size = 20, + hasNext = true, + items = listOf(item(contentId = 7L)) + ).toContent() + + assertEquals(ContentOverviewType.FIRST_AUDIO_CONTENT, content.type) + assertEquals(2, content.page) + assertEquals(20, content.size) + assertTrue(content.hasNext) + assertEquals(listOf(7L), content.items.map { it.contentId }) + } + + private fun pageResponse( + type: ContentOverviewType = ContentOverviewType.NEW_AND_HOT_AUDIO, + items: List = listOf(item()), + page: Int = 0, + size: Int = 20, + hasNext: Boolean = false + ) = ContentOverviewPageResponse( + type = type, + items = items, + page = page, + size = size, + hasNext = hasNext + ) + + private fun item( + contentId: Long = 1L, + title: String = "content", + coverImage: String? = "https://example.com/image.png", + price: Int = 100, + isAdult: Boolean = false, + isPointAvailable: Boolean = false, + isFirstContent: Boolean = false, + isOriginalSeries: Boolean = false, + creatorNickname: String = "creator" + ) = ContentOverviewItemResponse( + contentId = contentId, + title = title, + coverImage = coverImage, + price = price, + isAdult = isAdult, + isPointAvailable = isPointAvailable, + isFirstContent = isFirstContent, + isOriginalSeries = isOriginalSeries, + creatorNickname = creatorNickname + ) +}