diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabMappers.kt new file mode 100644 index 00000000..97e6ee98 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabMappers.kt @@ -0,0 +1,47 @@ +package kr.co.vividnext.sodalive.v2.main.content.model + +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllTabResponse +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAudioResponse +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentSeriesResponse +import kr.co.vividnext.sodalive.v2.widget.AudioContentTag + +fun MainContentAllTabResponse.toContent(): MainContentAllTabUiState.Content { + return MainContentAllTabUiState.Content( + selectedType = type, + selectedSort = sort, + selectedDayOfWeek = dayOfWeek, + totalCount = totalCount, + audioItems = if (type.usesSeriesItems()) emptyList() else audios.map { it.toUiModel() }, + seriesItems = if (type.usesSeriesItems()) series.map { it.toUiModel(type) } else emptyList(), + page = page, + size = size, + hasNext = hasNext + ) +} + +fun MainContentAudioResponse.toUiModel(): MainContentAllAudioUiModel = MainContentAllAudioUiModel( + audioContentId = audioContentId, + title = title, + imageUrl = imageUrl, + price = price, + creatorNickname = creatorNickname, + tags = toAudioContentTags(), + showAdultBadge = isAdult +) + +fun MainContentSeriesResponse.toUiModel(type: MainContentAllType): MainContentAllSeriesUiModel = MainContentAllSeriesUiModel( + seriesId = seriesId, + title = title, + coverImageUrl = coverImageUrl, + creatorNickname = creatorNickname, + showOriginalTag = type == MainContentAllType.ORIGINAL || isOriginal, + showAdultBadge = isAdult +) + +private fun MainContentAudioResponse.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/model/MainContentAllTabUiModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiModels.kt new file mode 100644 index 00000000..85af8bcf --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiModels.kt @@ -0,0 +1,49 @@ +package kr.co.vividnext.sodalive.v2.main.content.model + +import androidx.annotation.StringRes +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType +import kr.co.vividnext.sodalive.v2.widget.AudioContentTag + +data class MainContentAllAudioUiModel( + val audioContentId: Long, + val title: String, + val imageUrl: String?, + val price: Int, + val creatorNickname: String, + val tags: Set, + val showAdultBadge: Boolean +) + +data class MainContentAllSeriesUiModel( + val seriesId: Long, + val title: String, + val coverImageUrl: String?, + val creatorNickname: String, + val showOriginalTag: Boolean, + val showAdultBadge: Boolean +) + +data class MainContentAllTypeTabUiModel( + val type: MainContentAllType, + @StringRes val labelResId: Int +) + +fun MainContentAllType.toContentAllTypeLabelResId(): Int = when (this) { + MainContentAllType.AUDIO -> R.string.screen_content_all_type_audio + MainContentAllType.SERIES -> R.string.screen_content_all_type_series + MainContentAllType.ORIGINAL -> R.string.screen_content_all_type_original + MainContentAllType.FREE -> R.string.screen_content_all_type_free + MainContentAllType.POINT -> R.string.screen_content_all_type_point +} + +fun MainContentAllType.usesSeriesItems(): Boolean = when (this) { + MainContentAllType.SERIES, + MainContentAllType.ORIGINAL -> true + + MainContentAllType.AUDIO, + MainContentAllType.FREE, + MainContentAllType.POINT -> false +} + +fun MainContentAllType.usesDayOfWeekQuery(): Boolean = this == MainContentAllType.SERIES diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiState.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiState.kt new file mode 100644 index 00000000..61333ce7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiState.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.v2.main.content.model + +import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek +import kr.co.vividnext.sodalive.v2.common.data.ContentSort +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType + +sealed interface MainContentAllTabUiState { + data object Loading : MainContentAllTabUiState + + data class Content( + val selectedType: MainContentAllType, + val selectedSort: ContentSort, + val selectedDayOfWeek: SeriesPublishedDaysOfWeek?, + val totalCount: Int, + val audioItems: List, + val seriesItems: List, + val page: Int, + val size: Int, + val hasNext: Boolean, + val isLoadingMore: Boolean = false, + val paginationErrorMessage: String? = null + ) : MainContentAllTabUiState + + data object Empty : MainContentAllTabUiState + + data class Error(val message: String? = null) : MainContentAllTabUiState +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllTabMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllTabMapperTest.kt new file mode 100644 index 00000000..4c2735bf --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllTabMapperTest.kt @@ -0,0 +1,154 @@ +package kr.co.vividnext.sodalive.v2.main.content + +import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek +import kr.co.vividnext.sodalive.v2.common.data.ContentSort +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllTabResponse +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAudioResponse +import kr.co.vividnext.sodalive.v2.main.content.data.MainContentSeriesResponse +import kr.co.vividnext.sodalive.v2.main.content.model.toContent +import kr.co.vividnext.sodalive.v2.main.content.model.usesDayOfWeekQuery +import kr.co.vividnext.sodalive.v2.main.content.model.usesSeriesItems +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 MainContentAllTabMapperTest { + + @Test + fun `AUDIO FREE POINT는 audios 목록만 사용한다`() { + listOf(MainContentAllType.AUDIO, MainContentAllType.FREE, MainContentAllType.POINT).forEach { type -> + val content = response(type = type, audios = listOf(audio()), series = listOf(series())).toContent() + + assertEquals(1, content.audioItems.size) + assertEquals(emptyList(), content.seriesItems) + } + } + + @Test + fun `SERIES ORIGINAL은 series 목록만 사용한다`() { + listOf(MainContentAllType.SERIES, MainContentAllType.ORIGINAL).forEach { type -> + val content = response(type = type, audios = listOf(audio()), series = listOf(series())).toContent() + + assertEquals(emptyList(), content.audioItems) + assertEquals(1, content.seriesItems.size) + } + } + + @Test + fun `ORIGINAL은 시리즈 카드 타입으로 분류된다`() { + assertTrue(MainContentAllType.ORIGINAL.usesSeriesItems()) + assertFalse(MainContentAllType.ORIGINAL.usesDayOfWeekQuery()) + } + + @Test + fun `성인 flag는 오디오와 시리즈 UI model의 adult badge로 매핑된다`() { + val audioContent = response(type = MainContentAllType.AUDIO, audios = listOf(audio(isAdult = true))).toContent() + val seriesContent = response(type = MainContentAllType.SERIES, series = listOf(series(isAdult = true))).toContent() + + assertTrue(audioContent.audioItems.single().showAdultBadge) + assertTrue(seriesContent.seriesItems.single().showAdultBadge) + } + + @Test + fun `오디오 flag와 가격은 tag로 매핑된다`() { + val item = response( + audios = listOf( + audio( + price = 0, + isPointAvailable = true, + isFirstContent = true, + isOriginalSeries = true + ) + ) + ).toContent().audioItems.single() + + assertTrue(AudioContentTag.Point in item.tags) + assertTrue(AudioContentTag.First in item.tags) + assertTrue(AudioContentTag.Original in item.tags) + assertTrue(AudioContentTag.Free in item.tags) + } + + @Test + fun `paging과 선택 조건 metadata가 content state에 보존된다`() { + val content = response( + type = MainContentAllType.SERIES, + totalCount = 42, + sort = ContentSort.POPULAR, + dayOfWeek = SeriesPublishedDaysOfWeek.WED, + page = 2, + size = 30, + hasNext = true + ).toContent() + + assertEquals(MainContentAllType.SERIES, content.selectedType) + assertEquals(ContentSort.POPULAR, content.selectedSort) + assertEquals(SeriesPublishedDaysOfWeek.WED, content.selectedDayOfWeek) + assertEquals(42, content.totalCount) + assertEquals(2, content.page) + assertEquals(30, content.size) + assertTrue(content.hasNext) + } + + private fun response( + type: MainContentAllType = MainContentAllType.AUDIO, + totalCount: Int = 1, + audios: List = emptyList(), + series: List = emptyList(), + sort: ContentSort = ContentSort.LATEST, + dayOfWeek: SeriesPublishedDaysOfWeek? = null, + page: Int = 0, + size: Int = 20, + hasNext: Boolean = false + ) = MainContentAllTabResponse( + type = type, + totalCount = totalCount, + audios = audios, + series = series, + sort = sort, + dayOfWeek = dayOfWeek, + page = page, + size = size, + hasNext = hasNext + ) + + private fun audio( + audioContentId: Long = 1L, + title: String = "오디오", + imageUrl: String? = "https://example.com/audio.png", + price: Int = 100, + isAdult: Boolean = false, + isPointAvailable: Boolean = false, + isFirstContent: Boolean = false, + isOriginalSeries: Boolean = false, + creatorNickname: String = "크리에이터" + ) = MainContentAudioResponse( + audioContentId = audioContentId, + title = title, + imageUrl = imageUrl, + price = price, + isAdult = isAdult, + isPointAvailable = isPointAvailable, + isFirstContent = isFirstContent, + isOriginalSeries = isOriginalSeries, + creatorNickname = creatorNickname + ) + + private fun series( + seriesId: Long = 1L, + title: String = "시리즈", + coverImageUrl: String? = "https://example.com/series.png", + creatorNickname: String = "크리에이터", + isOriginal: Boolean = true, + isAdult: Boolean = false + ) = MainContentSeriesResponse( + seriesId = seriesId, + title = title, + coverImageUrl = coverImageUrl, + creatorNickname = creatorNickname, + isOriginal = isOriginal, + isAdult = isAdult + ) +}