diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingApi.kt new file mode 100644 index 00000000..9a0411ad --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingApi.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.v2.main.home.data + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse +import retrofit2.http.GET +import retrofit2.http.Header + +interface HomeCreatorRankingApi { + @GET("/api/v2/home/rankings/creators") + fun getCreatorRankings( + @Header("Authorization") authHeader: String + ): Single> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingModels.kt new file mode 100644 index 00000000..d1a9c760 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingModels.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.v2.main.home.data + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class HomeCreatorRankingResponse( + @SerializedName("showRankChange") val showRankChange: Boolean, + @SerializedName("items") val items: List +) + +@Keep +data class HomeCreatorRankingItemResponse( + @SerializedName("rank") val rank: Int, + @SerializedName("rankChange") val rankChange: Int?, + @SerializedName("isNew") val isNew: Boolean, + @SerializedName("creatorId") val creatorId: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileImageUrl") val profileImageUrl: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingRepository.kt new file mode 100644 index 00000000..548d38fd --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingRepository.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.v2.main.home.data + +class HomeCreatorRankingRepository(private val api: HomeCreatorRankingApi) { + fun getCreatorRankings(token: String) = api.getCreatorRankings(authHeader = token) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeCreatorRankingMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeCreatorRankingMappers.kt new file mode 100644 index 00000000..d1e65fa9 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeCreatorRankingMappers.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.v2.main.home.model + +import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorRankingItemResponse +import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorRankingResponse +import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItem +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 HomeCreatorRankingResponse.toCreatorRankingItems(): List = items + .filter { it.rank >= 1 } + .sortedBy { it.rank } + .map { it.toCreatorRankingItem(showRankChange) } + +private fun HomeCreatorRankingItemResponse.toCreatorRankingItem(showRankChange: Boolean): CreatorRankingItem { + val changeType = toRankingChangeType() + return CreatorRankingItem( + creatorId = creatorId, + rank = rank, + rankChangeType = changeType, + rankChangeAmount = if (changeType == New) 0 else abs(rankChange ?: 0), + creatorName = nickname, + imageUrl = profileImageUrl, + isBlocked = creatorId == 0L, + showRankChange = showRankChange + ) +} + +private fun HomeCreatorRankingItemResponse.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/home/HomeCreatorRankingMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorRankingMapperTest.kt new file mode 100644 index 00000000..18bf38f3 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorRankingMapperTest.kt @@ -0,0 +1,112 @@ +package kr.co.vividnext.sodalive.v2.main.home + +import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorRankingItemResponse +import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorRankingResponse +import kr.co.vividnext.sodalive.v2.main.home.model.toCreatorRankingItems +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 HomeCreatorRankingMapperTest { + + @Test + fun `응답 아이템은 rank 오름차순으로 정렬하고 유효하지 않은 rank는 제외한다`() { + val response = HomeCreatorRankingResponse( + showRankChange = true, + items = listOf( + item(rank = 3, creatorId = 3L), + item(rank = 0, creatorId = 0L), + item(rank = 1, creatorId = 1L), + item(rank = 2, creatorId = 2L) + ) + ) + + val items = response.toCreatorRankingItems() + + assertEquals(listOf(1, 2, 3), items.map { it.rank }) + assertEquals(listOf(1L, 2L, 3L), items.map { it.creatorId }) + } + + @Test + fun `신규 여부는 순위 변동보다 우선한다`() { + val item = HomeCreatorRankingResponse( + showRankChange = true, + items = listOf(item(isNew = true, rankChange = -3)) + ).toCreatorRankingItems().single() + + assertEquals(New, item.rankChangeType) + assertEquals(0, item.rankChangeAmount) + } + + @Test + fun `rankChange가 null이거나 0이면 유지로 매핑한다`() { + val items = HomeCreatorRankingResponse( + showRankChange = true, + items = listOf( + item(rank = 1, rankChange = null), + item(rank = 2, rankChange = 0) + ) + ).toCreatorRankingItems() + + assertEquals(listOf(Stay, Stay), items.map { it.rankChangeType }) + assertEquals(listOf(0, 0), items.map { it.rankChangeAmount }) + } + + @Test + fun `양수와 음수 rankChange는 절대값과 함께 상승과 하락으로 매핑한다`() { + val items = HomeCreatorRankingResponse( + showRankChange = true, + items = listOf( + item(rank = 1, rankChange = 5), + item(rank = 2, rankChange = -3) + ) + ).toCreatorRankingItems() + + assertEquals(listOf(Increase, Decrease), items.map { it.rankChangeType }) + assertEquals(listOf(5, 3), items.map { it.rankChangeAmount }) + } + + @Test + fun `순위 변동 숨김 값은 모든 크리에이터 랭킹 아이템에 전달된다`() { + val items = HomeCreatorRankingResponse( + showRankChange = false, + items = listOf(item(rank = 1), item(rank = 2)) + ).toCreatorRankingItems() + + assertTrue(items.isNotEmpty()) + assertTrue(items.all { !it.showRankChange }) + } + + @Test + fun `creatorId가 0이면 차단되고 터치할 수 없는 아이템으로 매핑한다`() { + val item = HomeCreatorRankingResponse( + showRankChange = true, + items = listOf(item(creatorId = 0L)) + ).toCreatorRankingItems().single() + + assertTrue(item.isBlocked) + assertTrue(item.isInaccessible) + assertFalse(item.isTouchable) + } + + private fun item( + rank: Int = 1, + rankChange: Int? = 1, + isNew: Boolean = false, + creatorId: Long = 1L, + nickname: String = "크리에이터 이름", + profileImageUrl: String = "https://example.com/image.png" + ) = HomeCreatorRankingItemResponse( + rank = rank, + rankChange = rankChange, + isNew = isNew, + creatorId = creatorId, + nickname = nickname, + profileImageUrl = profileImageUrl + ) +}