test #426
@@ -0,0 +1,54 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.donation.domain
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class CreatorChannelDonationQueryPolicy {
|
||||||
|
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 <T> limitItems(fetched: List<T>, page: CreatorChannelPage): List<T> {
|
||||||
|
return fetched.take(page.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasNext(fetched: List<*>, page: CreatorChannelPage): Boolean {
|
||||||
|
return fetched.size > page.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentKstMonthRange(now: LocalDateTime): CreatorChannelDonationMonthRange {
|
||||||
|
val nowKst = now.atZone(UTC_ZONE_ID).withZoneSameInstant(KST_ZONE_ID)
|
||||||
|
val start = nowKst.toLocalDate().withDayOfMonth(1).atStartOfDay(KST_ZONE_ID)
|
||||||
|
.withZoneSameInstant(UTC_ZONE_ID)
|
||||||
|
.toLocalDateTime()
|
||||||
|
val end = nowKst.toLocalDate().withDayOfMonth(1).atStartOfDay(KST_ZONE_ID).plusMonths(1)
|
||||||
|
.withZoneSameInstant(UTC_ZONE_ID)
|
||||||
|
.toLocalDateTime()
|
||||||
|
|
||||||
|
return CreatorChannelDonationMonthRange(
|
||||||
|
startInclusiveUtc = start,
|
||||||
|
endExclusiveUtc = end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
private val KST_ZONE_ID: ZoneId = ZoneId.of("Asia/Seoul")
|
||||||
|
private val UTC_ZONE_ID: ZoneId = ZoneId.of("UTC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelDonationMonthRange(
|
||||||
|
val startInclusiveUtc: LocalDateTime,
|
||||||
|
val endExclusiveUtc: LocalDateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.donation.domain
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class CreatorChannelDonationTab(
|
||||||
|
val donationCount: Int,
|
||||||
|
val rankings: List<CreatorChannelDonationRanking>,
|
||||||
|
val donations: List<CreatorChannelDonation>,
|
||||||
|
val page: CreatorChannelPage,
|
||||||
|
val hasNext: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelDonationRanking(
|
||||||
|
val userId: Long,
|
||||||
|
val nickname: String,
|
||||||
|
val profileImage: String,
|
||||||
|
val donationCan: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelDonation(
|
||||||
|
val nickname: String,
|
||||||
|
val profileImageUrl: String,
|
||||||
|
val can: Int,
|
||||||
|
val message: String,
|
||||||
|
val createdAt: LocalDateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
interface CreatorChannelDonationQueryPort {
|
||||||
|
fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelDonationCreatorRecord?
|
||||||
|
|
||||||
|
fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
|
||||||
|
|
||||||
|
fun countChannelDonations(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long,
|
||||||
|
now: LocalDateTime
|
||||||
|
): Int
|
||||||
|
|
||||||
|
fun findChannelDonations(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelDonationRecord>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelDonationCreatorRecord(
|
||||||
|
val creatorId: Long,
|
||||||
|
val role: MemberRole,
|
||||||
|
val nickname: String,
|
||||||
|
val isVisibleDonationRank: Boolean,
|
||||||
|
val donationRankingPeriod: DonationRankingPeriod?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelDonationRecord(
|
||||||
|
val nickname: String,
|
||||||
|
val profileImagePath: String?,
|
||||||
|
val can: Int,
|
||||||
|
val message: String?,
|
||||||
|
val createdAt: LocalDateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
|
||||||
|
|
||||||
|
interface CreatorChannelDonationRankingPort {
|
||||||
|
fun findTopRankings(
|
||||||
|
creatorId: Long,
|
||||||
|
period: DonationRankingPeriod,
|
||||||
|
withDonationCan: Boolean
|
||||||
|
): List<CreatorChannelDonationRankingRecord>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelDonationRankingRecord(
|
||||||
|
val userId: Long,
|
||||||
|
val nickname: String,
|
||||||
|
val profileImage: String,
|
||||||
|
val donationCan: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.donation.domain
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationCreatorRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRankingRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRecord
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class CreatorChannelDonationQueryPolicyTest {
|
||||||
|
private val policy = CreatorChannelDonationQueryPolicy()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("후원 탭 page 정책은 null 요청을 기본값으로 fallback하고 fetch limit을 계산한다")
|
||||||
|
fun shouldFallbackNullPageAndSizeForDonationTab() {
|
||||||
|
val page = policy.createPage(page = null, size = null)
|
||||||
|
|
||||||
|
assertEquals(0, page.page)
|
||||||
|
assertEquals(20, page.size)
|
||||||
|
assertEquals(0L, page.offset)
|
||||||
|
assertEquals(21, page.fetchLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("후원 탭 page 정책은 최소/최대 범위로 fallback하고 fetch limit을 계산한다")
|
||||||
|
fun shouldFallbackPageAndSizeForDonationTab() {
|
||||||
|
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("후원 탭 목록 정책은 요청 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))
|
||||||
|
assertFalse(policy.hasNext(emptyList<Int>(), page))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("후원 탭 월 범위 정책은 현재 UTC 시각 기준 KST 월 시작과 다음 월 시작을 UTC로 계산한다")
|
||||||
|
fun shouldCalculateCurrentKstMonthRangeAsUtc() {
|
||||||
|
val range = policy.currentKstMonthRange(LocalDateTime.of(2026, 6, 22, 3, 0))
|
||||||
|
|
||||||
|
assertEquals(LocalDateTime.of(2026, 5, 31, 15, 0), range.startInclusiveUtc)
|
||||||
|
assertEquals(LocalDateTime.of(2026, 6, 30, 15, 0), range.endExclusiveUtc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("후원 탭 domain model과 port record는 Phase 1 계약 필드를 유지한다")
|
||||||
|
fun shouldKeepDomainAndPortContract() {
|
||||||
|
val createdAt = LocalDateTime.of(2026, 6, 22, 10, 0)
|
||||||
|
val page = policy.createPage(page = 0, size = 20)
|
||||||
|
val ranking = CreatorChannelDonationRanking(
|
||||||
|
userId = 10L,
|
||||||
|
nickname = "fan",
|
||||||
|
profileImage = "https://cdn.test/fan.png",
|
||||||
|
donationCan = 100
|
||||||
|
)
|
||||||
|
val donation = CreatorChannelDonation(
|
||||||
|
nickname = "donor",
|
||||||
|
profileImageUrl = "https://cdn.test/donor.png",
|
||||||
|
can = 50,
|
||||||
|
message = "thanks",
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
val tab = CreatorChannelDonationTab(
|
||||||
|
donationCount = 1,
|
||||||
|
rankings = listOf(ranking),
|
||||||
|
donations = listOf(donation),
|
||||||
|
page = page,
|
||||||
|
hasNext = false
|
||||||
|
)
|
||||||
|
val creatorRecord = CreatorChannelDonationCreatorRecord(
|
||||||
|
creatorId = 1L,
|
||||||
|
role = MemberRole.CREATOR,
|
||||||
|
nickname = "creator",
|
||||||
|
isVisibleDonationRank = true,
|
||||||
|
donationRankingPeriod = DonationRankingPeriod.CUMULATIVE
|
||||||
|
)
|
||||||
|
val donationRecord = CreatorChannelDonationRecord(
|
||||||
|
nickname = "donor",
|
||||||
|
profileImagePath = null,
|
||||||
|
can = 50,
|
||||||
|
message = "thanks",
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
val rankingRecord = CreatorChannelDonationRankingRecord(
|
||||||
|
userId = 10L,
|
||||||
|
nickname = "fan",
|
||||||
|
profileImage = "https://cdn.test/fan.png",
|
||||||
|
donationCan = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(1, tab.donationCount)
|
||||||
|
assertEquals(ranking, tab.rankings.first())
|
||||||
|
assertEquals(donation, tab.donations.first())
|
||||||
|
assertEquals(page, tab.page)
|
||||||
|
assertFalse(tab.hasNext)
|
||||||
|
assertEquals(MemberRole.CREATOR, creatorRecord.role)
|
||||||
|
assertEquals(DonationRankingPeriod.CUMULATIVE, creatorRecord.donationRankingPeriod)
|
||||||
|
assertNull(donationRecord.profileImagePath)
|
||||||
|
assertEquals(100, rankingRecord.donationCan)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user