diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsMappers.kt new file mode 100644 index 00000000..a749d2db --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsMappers.kt @@ -0,0 +1,40 @@ +package kr.co.vividnext.sodalive.v2.main.content.model + +import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingItemResponse +import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingResponse +import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItem +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Decrease +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Increase +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.New +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Stay +import kotlin.math.abs + +fun AudioRankingResponse.toContentRankingItems(): List = items + .filter { it.rank >= 1 } + .sortedBy { it.rank } + .map { it.toContentRankingItem(showRankChange) } + +private fun AudioRankingItemResponse.toContentRankingItem(showRankChange: Boolean): ContentRankingItem { + val changeType = toRankingChangeType() + return ContentRankingItem( + contentId = contentId.toString(), + creatorId = "", + rank = rank, + previousRank = null, + rankChangeType = changeType, + rankChangeAmount = if (changeType == New) 0 else abs(rankChange ?: 0), + contentName = title, + creatorName = creatorNickname, + imageUrl = coverImageUrl.orEmpty(), + isBlocked = false, + showRankChange = showRankChange + ) +} + +private fun AudioRankingItemResponse.toRankingChangeType(): RankingChangeType = when { + isNew -> New + rankChange == null || rankChange == 0 -> Stay + rankChange > 0 -> Increase + else -> Decrease +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingsMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingsMapperTest.kt new file mode 100644 index 00000000..084e4259 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingsMapperTest.kt @@ -0,0 +1,128 @@ +package kr.co.vividnext.sodalive.v2.main.content + +import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingItemResponse +import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingResponse +import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingType +import kr.co.vividnext.sodalive.v2.main.content.model.toContentRankingItems +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Decrease +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Increase +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.New +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Stay +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class AudioRankingsMapperTest { + + @Test + fun `응답 아이템은 rank 오름차순으로 정렬하고 유효하지 않은 rank는 제외한다`() { + val items = response( + items = listOf( + rankingItem(contentId = 3L, rank = 3), + rankingItem(contentId = 0L, rank = 0), + rankingItem(contentId = 1L, rank = 1), + rankingItem(contentId = 2L, rank = 2) + ) + ).toContentRankingItems() + + assertEquals(listOf(1, 2, 3), items.map { it.rank }) + assertEquals(listOf("1", "2", "3"), items.map { it.contentId }) + } + + @Test + fun `isNew는 rankChange보다 우선한다`() { + val item = response( + items = listOf(rankingItem(isNew = true, rankChange = -3)) + ).toContentRankingItems().single() + + assertEquals(New, item.rankChangeType) + assertEquals(0, item.rankChangeAmount) + } + + @Test + fun `rankChange가 null이거나 0이면 유지로 매핑한다`() { + val items = response( + items = listOf( + rankingItem(rank = 1, rankChange = null), + rankingItem(rank = 2, rankChange = 0) + ) + ).toContentRankingItems() + + assertEquals(listOf(Stay, Stay), items.map { it.rankChangeType }) + assertEquals(listOf(0, 0), items.map { it.rankChangeAmount }) + } + + @Test + fun `양수와 음수 rankChange는 절대값과 함께 상승과 하락으로 매핑한다`() { + val items = response( + items = listOf( + rankingItem(rank = 1, rankChange = 5), + rankingItem(rank = 2, rankChange = -3) + ) + ).toContentRankingItems() + + assertEquals(listOf(Increase, Decrease), items.map { it.rankChangeType }) + assertEquals(listOf(5, 3), items.map { it.rankChangeAmount }) + } + + @Test + fun `showRankChange false는 모든 콘텐츠 랭킹 아이템에 전달된다`() { + val items = response( + showRankChange = false, + items = listOf(rankingItem(rank = 1), rankingItem(rank = 2)) + ).toContentRankingItems() + + assertTrue(items.isNotEmpty()) + assertTrue(items.all { !it.showRankChange }) + } + + @Test + fun `nullable 필드와 누락된 UI 필드는 콘텐츠 랭킹 기본값으로 매핑한다`() { + val item = response( + items = listOf( + rankingItem( + contentId = 10L, + title = "오디오 콘텐츠", + creatorNickname = "크리에이터", + coverImageUrl = null + ) + ) + ).toContentRankingItems().single() + + assertEquals("10", item.contentId) + assertEquals("", item.creatorId) + assertEquals("오디오 콘텐츠", item.contentName) + assertEquals("크리에이터", item.creatorName) + assertEquals("", item.imageUrl) + assertFalse(item.isBlocked) + } + + private fun response( + showRankChange: Boolean = true, + type: AudioRankingType = AudioRankingType.WEEKLY_POPULAR, + items: List = listOf(rankingItem()) + ) = AudioRankingResponse( + showRankChange = showRankChange, + type = type, + items = items + ) + + private fun rankingItem( + contentId: Long = 1L, + title: String = "콘텐츠 이름", + creatorNickname: String = "크리에이터 이름", + rank: Int = 1, + rankChange: Int? = 1, + isNew: Boolean = false, + coverImageUrl: String? = "https://example.com/image.png" + ) = AudioRankingItemResponse( + contentId = contentId, + title = title, + creatorNickname = creatorNickname, + rank = rank, + rankChange = rankChange, + isNew = isNew, + coverImageUrl = coverImageUrl + ) +}