From f3a574a54a090d7ca3cbaec1a0f2391083600108 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 19 Jun 2026 15:16:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator-channel):=20=EC=98=A4=EB=94=94?= =?UTF-8?q?=EC=98=A4=20=ED=83=AD=20=EC=A1=B0=ED=9A=8C=20=EC=A0=95=EC=B1=85?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/CreatorChannelAudioQueryPolicy.kt | 43 ++++++++++++++ .../CreatorChannelAudioQueryPolicyTest.kt | 56 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicy.kt create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicyTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicy.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicy.kt new file mode 100644 index 00000000..f57f71bc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicy.kt @@ -0,0 +1,43 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.audio.domain + +import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage +import org.springframework.stereotype.Component + +@Component +class CreatorChannelAudioQueryPolicy { + fun resolveSort(sort: String?): ContentSort { + return runCatching { ContentSort.valueOf(sort ?: ContentSort.LATEST.name) } + .getOrDefault(ContentSort.LATEST) + } + + fun createPage(page: Int?, size: Int?): CreatorChannelPage { + return CreatorChannelPage( + page = page?.coerceAtLeast(MIN_PAGE) ?: DEFAULT_PAGE, + size = size?.coerceIn(MIN_PAGE_SIZE, MAX_PAGE_SIZE) ?: DEFAULT_PAGE_SIZE + ) + } + + fun limitItems(fetched: List, page: CreatorChannelPage): List { + return fetched.take(page.size) + } + + fun hasNext(fetched: List<*>, page: CreatorChannelPage): Boolean { + return fetched.size > page.size + } + + fun purchaseRate(paidAudioContentCount: Int, purchasedAudioContentCount: Int): Double { + if (paidAudioContentCount == 0) { + return 0.0 + } + return purchasedAudioContentCount.toDouble() / paidAudioContentCount * 100 + } + + companion object { + private const val DEFAULT_PAGE = 0 + private const val DEFAULT_PAGE_SIZE = 20 + private const val MIN_PAGE = 0 + private const val MIN_PAGE_SIZE = 20 + private const val MAX_PAGE_SIZE = 50 + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicyTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicyTest.kt new file mode 100644 index 00000000..f6f893bc --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicyTest.kt @@ -0,0 +1,56 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.audio.domain + +import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class CreatorChannelAudioQueryPolicyTest { + private val policy = CreatorChannelAudioQueryPolicy() + + @Test + @DisplayName("오디오 탭 page 정책은 page와 size를 fallback하고 fetch limit을 계산한다") + fun shouldFallbackPageAndSizeForAudioTab() { + val minimumPage = policy.createPage(page = -1, size = 10) + val maximumPage = policy.createPage(page = 2, size = 100) + + assertEquals(0, minimumPage.page) + assertEquals(20, minimumPage.size) + assertEquals(0L, minimumPage.offset) + assertEquals(21, minimumPage.fetchLimit) + assertEquals(2, maximumPage.page) + assertEquals(50, maximumPage.size) + assertEquals(100L, maximumPage.offset) + assertEquals(51, maximumPage.fetchLimit) + } + + @Test + @DisplayName("오디오 탭 sort 정책은 null과 알 수 없는 값을 LATEST로 fallback한다") + fun shouldFallbackInvalidSortToLatest() { + assertEquals(ContentSort.LATEST, policy.resolveSort(null)) + assertEquals(ContentSort.LATEST, policy.resolveSort("UNKNOWN")) + assertEquals(ContentSort.POPULAR, policy.resolveSort("POPULAR")) + } + + @Test + @DisplayName("오디오 탭 목록 정책은 요청 size만 남기고 다음 페이지 여부를 계산한다") + fun shouldLimitItemsAndCalculateHasNext() { + val page = policy.createPage(page = 0, size = 20) + val fetched = (1..21).toList() + + val items = policy.limitItems(fetched, page) + + assertEquals((1..20).toList(), items) + assertTrue(policy.hasNext(fetched, page)) + assertFalse(policy.hasNext((1..20).toList(), page)) + } + + @Test + @DisplayName("오디오 탭 소장률은 유료 콘텐츠가 없으면 0이고 있으면 백분율로 계산한다") + fun shouldCalculatePurchaseRate() { + assertEquals(0.0, policy.purchaseRate(paidAudioContentCount = 0, purchasedAudioContentCount = 3)) + assertEquals(75.0, policy.purchaseRate(paidAudioContentCount = 4, purchasedAudioContentCount = 3)) + } +}