feat(home): 응원 크리에이터 집계 쿼리를 변경한다

This commit is contained in:
2026-07-10 06:05:37 +09:00
parent c9e35f2ed4
commit 4f348c3643
4 changed files with 78 additions and 40 deletions
@@ -773,32 +773,34 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
}
@Test
@DisplayName("최근 응원 스냅샷은 CHANNEL_DONATION 후원 금액과 후원 수만 집계한다")
@DisplayName("최근 응원 스냅샷은 전날 half-open 범위의 CHANNEL_DONATION 금액과 distinct 후원 수만 집계한다")
fun shouldFindCheerCreatorSnapshotsWithChannelDonationOnly() {
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
val windowStart = LocalDateTime.of(2026, 5, 28, 15, 0)
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0)
val snapshotAt = windowEndExclusive.minusSeconds(1)
val creator = saveMember("cheer-creator", MemberRole.CREATOR)
val donor = saveMember("cheer-donor", MemberRole.USER)
saveAudioContent(creator, LocalDateTime.of(2026, 5, 20, 12, 0), isActive = true)
saveLiveRoom(creator, LocalDateTime.of(2026, 5, 10, 12, 0), channelName = "cheer-channel")
saveUseCanCalculate(
val firstDonation = saveUseCanCalculate(
donor,
creator,
CanUsage.CHANNEL_DONATION,
can = 100,
status = UseCanCalculateStatus.RECEIVED,
isRefund = false,
createdAt = windowStart.plusDays(1)
createdAt = windowStart.plusHours(1)
)
saveUseCanCalculateForUseCan(firstDonation.useCan!!, creator, can = 50, createdAt = windowStart.plusHours(2))
saveUseCanCalculate(
donor,
creator,
CanUsage.CHANNEL_DONATION,
can = 50,
can = 999,
status = UseCanCalculateStatus.RECEIVED,
isRefund = false,
createdAt = snapshotAt
createdAt = windowEndExclusive
)
saveUseCanCalculate(
donor,
@@ -807,7 +809,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
can = 1_000,
status = UseCanCalculateStatus.RECEIVED,
isRefund = false,
createdAt = windowStart.plusDays(1)
createdAt = windowStart.plusHours(1)
)
saveUseCanCalculate(
donor,
@@ -816,7 +818,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
can = 1_000,
status = UseCanCalculateStatus.RECEIVED,
isRefund = false,
createdAt = windowStart.plusDays(1)
createdAt = windowStart.plusHours(1)
)
saveUseCanCalculate(
donor,
@@ -825,7 +827,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
can = 1_000,
status = UseCanCalculateStatus.RECEIVED,
isRefund = false,
createdAt = windowStart.plusDays(1)
createdAt = windowStart.plusHours(1)
)
saveUseCanCalculate(
donor,
@@ -834,7 +836,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
can = 1_000,
status = UseCanCalculateStatus.REFUND,
isRefund = false,
createdAt = windowStart.plusDays(1)
createdAt = windowStart.plusHours(1)
)
saveUseCanCalculate(
donor,
@@ -843,7 +845,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
can = 1_000,
status = UseCanCalculateStatus.RECEIVED,
isRefund = true,
createdAt = windowStart.plusDays(1)
createdAt = windowStart.plusHours(1)
)
saveUseCanCalculate(
donor,
@@ -861,28 +863,29 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
can = 1_000,
status = UseCanCalculateStatus.RECEIVED,
isRefund = false,
createdAt = windowStart.plusDays(1)
createdAt = windowStart.plusHours(1)
)
val cheer1 = saveCreatorCheers(donor, creator, isActive = true)
val cheer2 = saveCreatorCheers(donor, creator, isActive = true)
val inactiveCheer = saveCreatorCheers(donor, creator, isActive = false)
updateCreatedAt("CreatorCheers", cheer1.id!!, windowStart.plusDays(1))
updateCreatedAt("CreatorCheers", cheer2.id!!, snapshotAt)
updateCreatedAt("CreatorCheers", inactiveCheer.id!!, windowStart.plusDays(1))
updateCreatedAt("CreatorCheers", cheer1.id!!, windowStart.plusHours(1))
updateCreatedAt("CreatorCheers", cheer2.id!!, windowEndExclusive)
updateCreatedAt("CreatorCheers", inactiveCheer.id!!, windowStart.plusHours(1))
updateCreatedAt("Member", creator.id!!, LocalDateTime.of(2026, 5, 10, 12, 0))
flushAndClear()
val snapshots = repository.findCheerCreatorSnapshots(windowStart, snapshotAt, limit = 10)
val snapshots = repository.findCheerCreatorSnapshots(windowStart, windowEndExclusive, limit = 10)
val expectedScore = scorePolicy.calculateCheerScore(
donationAmount = 150,
fanTalkCount = 2,
donationCount = 2,
newBoost = scorePolicy.calculateCreatorNewBoost(LocalDateTime.of(2026, 5, 10, 12, 0), snapshotAt)
fanTalkCount = 1,
donationCount = 1,
newBoost = scorePolicy.calculateCheerCreatorNewBoost(LocalDateTime.of(2026, 5, 10, 12, 0), snapshotAt)
)
assertEquals(1, snapshots.size)
assertEquals(RecommendedSectionType.CHEER_CREATOR, snapshots.single().sectionType)
assertEquals(creator.id, snapshots.single().targetId)
assertEquals(snapshotAt, snapshots.single().snapshotAt)
assertEquals(expectedScore, snapshots.single().score, 0.0001)
}
@@ -891,6 +894,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
fun shouldFindCheerCreatorSnapshotsWithDbScoreOrderAndLimit() {
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val donor = saveMember("cheer-score-donor", MemberRole.USER)
val oldCreator = saveMember("old-cheer-score-creator", MemberRole.CREATOR)
val newCreator = saveMember("new-cheer-score-creator", MemberRole.CREATOR)
@@ -916,17 +920,18 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
)
flushAndClear()
val snapshots = repository.findCheerCreatorSnapshots(windowStart, snapshotAt, limit = 1)
val snapshots = repository.findCheerCreatorSnapshots(windowStart, windowEndExclusive, limit = 1)
val expectedScore = scorePolicy.calculateCheerScore(
donationAmount = 2,
fanTalkCount = 0,
donationCount = 1,
newBoost = scorePolicy.calculateCreatorNewBoost(LocalDateTime.of(2026, 5, 20, 0, 0), snapshotAt)
newBoost = scorePolicy.calculateCheerCreatorNewBoost(LocalDateTime.of(2026, 5, 20, 0, 0), snapshotAt)
)
assertEquals(1, snapshots.size)
assertEquals(RecommendedSectionType.CHEER_CREATOR, snapshots.single().sectionType)
assertEquals(newCreator.id, snapshots.single().targetId)
assertEquals(snapshotAt, snapshots.single().snapshotAt)
assertEquals(expectedScore, snapshots.single().score, 0.0001)
}
@@ -1046,6 +1051,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
fun shouldExcludeCreatorSnapshotsWithoutActualDebutAt() {
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val creator = saveMember("creator-without-debut", MemberRole.CREATOR)
val donor = saveMember("donor-without-debut", MemberRole.USER)
val community = saveCommunity(creator, isCommentAvailable = true)
@@ -1063,7 +1069,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("CreatorCommunityLike", like.id!!, windowStart.plusDays(1))
flushAndClear()
val cheerSnapshots = repository.findCheerCreatorSnapshots(windowStart, snapshotAt, limit = 10)
val cheerSnapshots = repository.findCheerCreatorSnapshots(windowStart, windowEndExclusive, limit = 10)
val communitySnapshots = repository.findPopularCommunitySnapshots(windowStart, snapshotAt, limit = 10)
assertEquals(emptyList<Any>(), cheerSnapshots)
@@ -1075,6 +1081,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
fun shouldUseActualDebutAtInsteadOfMemberCreatedAtForCreatorSnapshots() {
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val memberCreatedAt = LocalDateTime.of(2026, 1, 1, 0, 0)
val firstLiveAt = LocalDateTime.of(2026, 5, 10, 12, 0)
val firstContentAt = LocalDateTime.of(2026, 5, 20, 12, 0)
@@ -1098,14 +1105,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("CreatorCommunityLike", like.id!!, windowStart.plusDays(1))
flushAndClear()
val cheerSnapshot = repository.findCheerCreatorSnapshots(windowStart, snapshotAt, limit = 10).single()
val cheerSnapshot = repository.findCheerCreatorSnapshots(windowStart, windowEndExclusive, limit = 10).single()
val communitySnapshot = repository.findPopularCommunitySnapshots(windowStart, snapshotAt, limit = 10).single()
val expectedCheerScore = scorePolicy.calculateCheerScore(
donationAmount = 100,
fanTalkCount = 0,
donationCount = 1,
newBoost = scorePolicy.calculateCreatorNewBoost(firstLiveAt, snapshotAt)
newBoost = scorePolicy.calculateCheerCreatorNewBoost(firstLiveAt, snapshotAt)
)
val expectedCommunityScore = scorePolicy.calculateCommunityScore(
likeCount = 1,
@@ -1114,6 +1121,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
newBoost = scorePolicy.calculateCreatorNewBoost(firstLiveAt, snapshotAt)
)
assertEquals(expectedCheerScore, cheerSnapshot.score, 0.0001)
assertEquals(snapshotAt, cheerSnapshot.snapshotAt)
assertEquals(expectedCommunityScore, communitySnapshot.score, 0.0001)
}
@@ -1122,6 +1130,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
fun shouldExcludeBlankChannelNameLiveFromSnapshotDebutAt() {
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val blankLiveAt = LocalDateTime.of(2026, 5, 1, 12, 0)
val contentDebutAt = LocalDateTime.of(2026, 5, 20, 12, 0)
val creator = saveMember("blank-live-debut-creator", MemberRole.CREATOR)
@@ -1143,14 +1152,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("CreatorCommunityLike", like.id!!, windowStart.plusDays(1))
flushAndClear()
val cheerSnapshot = repository.findCheerCreatorSnapshots(windowStart, snapshotAt, limit = 10).single()
val cheerSnapshot = repository.findCheerCreatorSnapshots(windowStart, windowEndExclusive, limit = 10).single()
val communitySnapshot = repository.findPopularCommunitySnapshots(windowStart, snapshotAt, limit = 10).single()
val expectedCheerScore = scorePolicy.calculateCheerScore(
donationAmount = 100,
fanTalkCount = 0,
donationCount = 1,
newBoost = scorePolicy.calculateCreatorNewBoost(contentDebutAt, snapshotAt)
newBoost = scorePolicy.calculateCheerCreatorNewBoost(contentDebutAt, snapshotAt)
)
val expectedCommunityScore = scorePolicy.calculateCommunityScore(
likeCount = 1,
@@ -1159,6 +1168,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
newBoost = scorePolicy.calculateCreatorNewBoost(contentDebutAt, snapshotAt)
)
assertEquals(expectedCheerScore, cheerSnapshot.score, 0.0001)
assertEquals(snapshotAt, cheerSnapshot.snapshotAt)
assertEquals(expectedCommunityScore, communitySnapshot.score, 0.0001)
}
@@ -2194,7 +2204,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
status: UseCanCalculateStatus,
isRefund: Boolean,
createdAt: LocalDateTime
) {
): UseCanCalculate {
val useCan = UseCan(canUsage = usage, can = can, rewardCan = 0, isRefund = isRefund)
useCan.member = donor
entityManager.persist(useCan)
@@ -2204,6 +2214,26 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
entityManager.persist(calculate)
entityManager.flush()
updateCreatedAt("UseCanCalculate", calculate.id!!, createdAt)
return calculate
}
private fun saveUseCanCalculateForUseCan(
useCan: UseCan,
creator: Member,
can: Int,
createdAt: LocalDateTime
): UseCanCalculate {
val calculate = UseCanCalculate(
can = can,
paymentGateway = PaymentGateway.PG,
status = UseCanCalculateStatus.RECEIVED
)
calculate.useCan = useCan
calculate.recipientCreatorId = creator.id
entityManager.persist(calculate)
entityManager.flush()
updateCreatedAt("UseCanCalculate", calculate.id!!, createdAt)
return calculate
}
private fun saveCreatorCheers(member: Member, creator: Member, isActive: Boolean): CreatorCheers {