test #433

Merged
klaus merged 41 commits from test into main 2026-07-10 07:20:07 +00:00
3 changed files with 236 additions and 66 deletions
Showing only changes of commit 2a0d2a385a - Show all commits

View File

@@ -483,74 +483,89 @@ class DefaultHomeRecommendationQueryRepository(
override fun findAiCharacterSnapshots( override fun findAiCharacterSnapshots(
windowStart: LocalDateTime, windowStart: LocalDateTime,
snapshotAt: LocalDateTime, windowEndExclusive: LocalDateTime,
limit: Int limit: Int
): List<RecommendationSnapshotRecord> { ): List<RecommendationSnapshotRecord> {
val sql = """ val sql = """
select ranked.target_id,
ranked.score,
ranked.snapshot_rank as random_tie_breaker
from (
select scored.target_id,
scored.score,
row_number() over (order by scored.score desc, scored.character_created_at desc, scored.target_id desc) as snapshot_rank
from (
select cc.id as target_id, select cc.id as target_id,
(((select count(cm.id) cc.created_at as character_created_at,
from chat_message cm ((coalesce(acs.ai_chat_count, 0) * ${RecommendationScoreSpec.AI_CHAT_WEIGHT} +
join chat_participant cp on cp.id = cm.participant_id coalesce(aus.active_user_count, 0) * ${RecommendationScoreSpec.AI_ACTIVE_USER_WEIGHT} +
where cp.character_id = cc.id coalesce(fs.follow_increase_count, 0) * ${RecommendationScoreSpec.AI_FOLLOW_INCREASE_WEIGHT}) *
and cm.created_at >= :windowStart
and cm.created_at <= :snapshotAt
and cm.is_active = true
and cp.is_active = true
and cp.participant_type = 'CHARACTER') * ${RecommendationScoreSpec.AI_RECENT_CHAT_WEIGHT} +
(select count(distinct up.member_id)
from chat_message cm
join chat_participant up on up.id = cm.participant_id
join chat_participant cp on cp.chat_room_id = cm.chat_room_id
join member m on m.id = up.member_id
where cp.character_id = cc.id
and cm.created_at >= :windowStart
and cm.created_at <= :snapshotAt
and cm.is_active = true
and up.is_active = true
and cp.is_active = true
and up.participant_type = 'USER'
and cp.participant_type = 'CHARACTER'
and m.is_active = true) * ${RecommendationScoreSpec.AI_RECENT_ACTIVE_USER_WEIGHT}) *
case case
when cc.created_at >= :boost10Start then ${RecommendationScoreSpec.NEW_BOOST_10_DAYS} when cc.created_at >= :boost10Start then ${RecommendationScoreSpec.AI_NEW_BOOST_10_DAYS}
when cc.created_at >= :boost20Start then ${RecommendationScoreSpec.NEW_BOOST_20_DAYS} when cc.created_at >= :boost20Start then ${RecommendationScoreSpec.AI_NEW_BOOST_20_DAYS}
when cc.created_at >= :boost30Start then ${RecommendationScoreSpec.NEW_BOOST_30_DAYS} when cc.created_at >= :boost30Start then ${RecommendationScoreSpec.AI_NEW_BOOST_30_DAYS}
else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST} else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST}
end) as score, end) as score
rand() as random_tie_breaker
from chat_character cc from chat_character cc
where cc.is_active = true join member creator_member on creator_member.id = cc.creator_member_id
and (exists ( left join (
select 1 select cp.character_id as character_id, count(cm.id) as ai_chat_count
from chat_message cm from chat_message cm
join chat_participant cp on cp.id = cm.participant_id join chat_participant cp on cp.id = cm.participant_id
where cp.character_id = cc.id where cm.created_at >= :windowStart
and cm.created_at >= :windowStart and cm.created_at < :windowEndExclusive
and cm.created_at <= :snapshotAt
and cm.is_active = true and cm.is_active = true
and cp.is_active = true and cp.is_active = true
and cp.participant_type = 'CHARACTER' and cp.participant_type = 'CHARACTER'
) or exists ( group by cp.character_id
select 1 ) acs on acs.character_id = cc.id
left join (
select cp.character_id as character_id, count(distinct up.member_id) as active_user_count
from chat_message cm from chat_message cm
join chat_participant up on up.id = cm.participant_id join chat_participant up on up.id = cm.participant_id
join chat_participant cp on cp.chat_room_id = cm.chat_room_id join chat_participant cp on cp.chat_room_id = cm.chat_room_id
join member m on m.id = up.member_id join member m on m.id = up.member_id
where cp.character_id = cc.id where cm.created_at >= :windowStart
and cm.created_at >= :windowStart and cm.created_at < :windowEndExclusive
and cm.created_at <= :snapshotAt
and cm.is_active = true and cm.is_active = true
and up.is_active = true and up.is_active = true
and cp.is_active = true and cp.is_active = true
and up.participant_type = 'USER' and up.participant_type = 'USER'
and cp.participant_type = 'CHARACTER' and cp.participant_type = 'CHARACTER'
and m.is_active = true and m.is_active = true
)) group by cp.character_id
order by score desc, random_tie_breaker asc ) aus on aus.character_id = cc.id
left join (
select cf.creator_id as creator_id, count(cf.id) as follow_increase_count
from creator_following cf
join member follower_member on follower_member.id = cf.member_id
where cf.created_at >= :windowStart
and cf.created_at < :windowEndExclusive
and cf.is_active = true
and follower_member.is_active = true
group by cf.creator_id
) fs on fs.creator_id = cc.creator_member_id
where cc.is_active = true
and cc.created_at is not null
and cc.created_at < :windowEndExclusive
and creator_member.is_active = true
and creator_member.role = 'CREATOR'
and creator_member.member_kind = 'AI_CHARACTER'
) scored
where scored.score > 0
) ranked
order by ranked.score desc, ranked.snapshot_rank asc
limit :limit limit :limit
""".trimIndent() """.trimIndent()
return executeSnapshotQuery(sql, RecommendedSectionType.AI_CHARACTER, windowStart, snapshotAt, limit) return executeSnapshotQuery(
sql,
RecommendedSectionType.AI_CHARACTER,
windowStart,
windowEndExclusive.minusSeconds(1),
limit,
windowEndExclusive
)
} }
override fun findCheerCreatorSnapshots( override fun findCheerCreatorSnapshots(
@@ -1129,12 +1144,19 @@ class DefaultHomeRecommendationQueryRepository(
sectionType: RecommendedSectionType, sectionType: RecommendedSectionType,
windowStart: LocalDateTime, windowStart: LocalDateTime,
snapshotAt: LocalDateTime, snapshotAt: LocalDateTime,
limit: Int limit: Int,
windowEndExclusive: LocalDateTime? = null
): List<RecommendationSnapshotRecord> { ): List<RecommendationSnapshotRecord> {
val query = entityManager.createNativeQuery(sql) val query = entityManager.createNativeQuery(sql)
.setParameter("windowStart", windowStart) .setParameter("windowStart", windowStart)
.setParameter("snapshotAt", snapshotAt)
.setParameter("limit", limit) .setParameter("limit", limit)
if (sql.contains(":snapshotAt")) {
query.setParameter("snapshotAt", snapshotAt)
}
if (windowEndExclusive != null) {
query.setParameter("windowEndExclusive", windowEndExclusive)
}
query
.setParameter( .setParameter(
"boost10Start", "boost10Start",
snapshotAt.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_10_DAY_LIMIT).atStartOfDay() snapshotAt.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_10_DAY_LIMIT).atStartOfDay()

View File

@@ -40,7 +40,7 @@ interface HomeRecommendationQueryPort {
fun findAiCharacterSnapshots( fun findAiCharacterSnapshots(
windowStart: LocalDateTime, windowStart: LocalDateTime,
snapshotAt: LocalDateTime, windowEndExclusive: LocalDateTime,
limit: Int limit: Int
): List<RecommendationSnapshotRecord> ): List<RecommendationSnapshotRecord>

View File

@@ -523,12 +523,15 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
} }
@Test @Test
@DisplayName("AI 캐릭터 스냅샷은 AI 발화 수 중복 없는 활성 사용자 수를 집계하고 팔로우 증가량은 제외한다") @DisplayName("AI 캐릭터 스냅샷은 전날 AI 발화 수, 중복 없는 활성 사용자 수, 신규 팔로우 증가량을 집계한다")
fun shouldFindAiCharacterSnapshotsWithoutFollowIncrease() { fun shouldFindAiCharacterSnapshotsWithFollowIncrease() {
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0) val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val user1 = saveMember("ai-user-1", MemberRole.USER) val user1 = saveMember("ai-user-1", MemberRole.USER)
val user2 = saveMember("ai-user-2", MemberRole.USER) val user2 = saveMember("ai-user-2", MemberRole.USER)
val inactiveFollower = saveMember("ai-inactive-follower", MemberRole.USER)
val inactiveMemberFollower = saveMember("ai-inactive-member-follower", MemberRole.USER, isActive = false)
val character = saveCharacter("character-1", isActive = true) val character = saveCharacter("character-1", isActive = true)
val inactiveCharacter = saveCharacter("character-2", isActive = false) val inactiveCharacter = saveCharacter("character-2", isActive = false)
val room = saveChatRoom("room-1") val room = saveChatRoom("room-1")
@@ -547,23 +550,32 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val inactiveMessage = saveMessage(room, characterParticipant, "message-4", isActive = false) val inactiveMessage = saveMessage(room, characterParticipant, "message-4", isActive = false)
val oldMessage = saveMessage(room, characterParticipant, "message-5", isActive = true) val oldMessage = saveMessage(room, characterParticipant, "message-5", isActive = true)
val inactiveCharacterMessage = saveMessage(otherRoom, inactiveCharacterUser, "message-6", isActive = true) val inactiveCharacterMessage = saveMessage(otherRoom, inactiveCharacterUser, "message-6", isActive = true)
updateCreatedAt("ChatMessage", message1.id!!, windowStart.plusDays(1)) updateCreatedAt("ChatMessage", message1.id!!, windowStart.plusHours(1))
updateCreatedAt("ChatMessage", message2.id!!, windowStart.plusDays(2)) updateCreatedAt("ChatMessage", message2.id!!, windowStart.plusHours(2))
updateCreatedAt("ChatMessage", message3.id!!, snapshotAt) updateCreatedAt("ChatMessage", message3.id!!, snapshotAt)
updateCreatedAt("ChatMessage", characterMessage1.id!!, windowStart.plusDays(1)) updateCreatedAt("ChatMessage", characterMessage1.id!!, windowStart.plusHours(1))
updateCreatedAt("ChatMessage", characterMessage2.id!!, snapshotAt) updateCreatedAt("ChatMessage", characterMessage2.id!!, snapshotAt)
updateCreatedAt("ChatMessage", inactiveMessage.id!!, windowStart.plusDays(3)) updateCreatedAt("ChatMessage", inactiveMessage.id!!, windowStart.plusHours(3))
updateCreatedAt("ChatMessage", oldMessage.id!!, windowStart.minusSeconds(1)) updateCreatedAt("ChatMessage", oldMessage.id!!, windowStart.minusSeconds(1))
updateCreatedAt("ChatMessage", inactiveCharacterMessage.id!!, windowStart.plusDays(1)) updateCreatedAt("ChatMessage", inactiveCharacterMessage.id!!, windowStart.plusHours(1))
updateCreatedAt("ChatCharacter", character.id!!, LocalDateTime.of(2026, 5, 20, 12, 0)) val follow1 = saveFollowing(user1, character.creatorMember!!, isActive = true)
val follow2 = saveFollowing(user2, character.creatorMember!!, isActive = true)
val inactiveFollow = saveFollowing(inactiveFollower, character.creatorMember!!, isActive = false)
val inactiveMemberFollow = saveFollowing(inactiveMemberFollower, character.creatorMember!!, isActive = true)
updateCreatedAt("CreatorFollowing", follow1.id!!, windowStart.plusHours(1))
updateCreatedAt("CreatorFollowing", follow2.id!!, snapshotAt)
updateCreatedAt("CreatorFollowing", inactiveFollow.id!!, windowStart.plusHours(1))
updateCreatedAt("CreatorFollowing", inactiveMemberFollow.id!!, windowStart.plusHours(1))
updateCreatedAt("ChatCharacter", character.id!!, LocalDateTime.of(2026, 7, 1, 12, 0))
flushAndClear() flushAndClear()
val snapshots = repository.findAiCharacterSnapshots(windowStart, snapshotAt, limit = 10) val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 10)
val expectedScore = scorePolicy.calculateAiChatScore( val expectedScore = scorePolicy.calculateAiChatScore(
recentChatCount = 2, recentChatCount = 2,
recentActiveUserCount = 2, recentActiveUserCount = 2,
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 5, 20, 12, 0), snapshotAt) followIncreaseCount = 2,
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 7, 1, 12, 0), snapshotAt)
) )
assertEquals(1, snapshots.size) assertEquals(1, snapshots.size)
assertEquals(RecommendedSectionType.AI_CHARACTER, snapshots.single().sectionType) assertEquals(RecommendedSectionType.AI_CHARACTER, snapshots.single().sectionType)
@@ -572,11 +584,75 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
assertEquals(snapshotAt, snapshots.single().snapshotAt) assertEquals(snapshotAt, snapshots.single().snapshotAt)
} }
@Test
@DisplayName("AI 캐릭터 스냅샷은 종료 시각을 exclusive 경계로 집계한다")
fun shouldFindAiCharacterSnapshotsWithExclusiveWindowEnd() {
val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
val windowEndExclusive = LocalDateTime.of(2026, 7, 9, 15, 0)
val snapshotAt = windowEndExclusive.minusSeconds(1)
val includedUser = saveMember("ai-window-included-user", MemberRole.USER)
val excludedUser = saveMember("ai-window-excluded-user", MemberRole.USER)
val character = saveCharacter("ai-window-character", isActive = true)
val excludedCharacter = saveCharacter("ai-window-excluded-character", isActive = true)
val room = saveChatRoom("ai-window-room")
val excludedRoom = saveChatRoom("ai-window-excluded-room")
val includedUserParticipant = saveParticipant(room, ParticipantType.USER, member = includedUser)
val includedCharacterParticipant = saveParticipant(room, ParticipantType.CHARACTER, character = character)
val excludedUserParticipant = saveParticipant(excludedRoom, ParticipantType.USER, member = excludedUser)
val excludedCharacterParticipant = saveParticipant(excludedRoom, ParticipantType.CHARACTER, character = excludedCharacter)
updateCreatedAt(
"ChatMessage",
saveMessage(room, includedUserParticipant, "included-user", true).id!!,
windowEndExclusive.minusSeconds(1)
)
updateCreatedAt(
"ChatMessage",
saveMessage(room, includedCharacterParticipant, "included-character", true).id!!,
windowEndExclusive.minusSeconds(1)
)
updateCreatedAt(
"ChatMessage",
saveMessage(excludedRoom, excludedUserParticipant, "excluded-user", true).id!!,
windowEndExclusive
)
updateCreatedAt(
"ChatMessage",
saveMessage(excludedRoom, excludedCharacterParticipant, "excluded-character", true).id!!,
windowEndExclusive
)
updateCreatedAt(
"CreatorFollowing",
saveFollowing(includedUser, character.creatorMember!!, isActive = true).id!!,
windowEndExclusive.minusSeconds(1)
)
updateCreatedAt(
"CreatorFollowing",
saveFollowing(excludedUser, excludedCharacter.creatorMember!!, isActive = true).id!!,
windowEndExclusive
)
updateCreatedAt("ChatCharacter", character.id!!, windowStart)
updateCreatedAt("ChatCharacter", excludedCharacter.id!!, windowEndExclusive)
flushAndClear()
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 10)
val expectedScore = scorePolicy.calculateAiChatScore(
recentChatCount = 1,
recentActiveUserCount = 1,
followIncreaseCount = 1,
newBoost = scorePolicy.calculateAiCharacterNewBoost(windowStart, snapshotAt)
)
assertEquals(listOf(character.id), snapshots.map { it.targetId })
assertEquals(expectedScore, snapshots.single().score, 0.0001)
assertEquals(snapshotAt, snapshots.single().snapshotAt)
}
@Test @Test
@DisplayName("AI 캐릭터 스냅샷은 DB에서 최종 점수를 계산한 뒤 정렬하고 limit을 적용한다") @DisplayName("AI 캐릭터 스냅샷은 DB에서 최종 점수를 계산한 뒤 정렬하고 limit을 적용한다")
fun shouldFindAiCharacterSnapshotsWithDbScoreOrderAndLimit() { fun shouldFindAiCharacterSnapshotsWithDbScoreOrderAndLimit() {
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0) val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0)
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59) val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val user = saveMember("ai-score-user", MemberRole.USER) val user = saveMember("ai-score-user", MemberRole.USER)
val oldHighActivityCharacter = saveCharacter("old-high-activity", isActive = true) val oldHighActivityCharacter = saveCharacter("old-high-activity", isActive = true)
val newLowerActivityCharacter = saveCharacter("new-lower-activity", isActive = true) val newLowerActivityCharacter = saveCharacter("new-lower-activity", isActive = true)
@@ -610,20 +686,92 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("ChatCharacter", newLowerActivityCharacter.id!!, LocalDateTime.of(2026, 5, 20, 0, 0)) updateCreatedAt("ChatCharacter", newLowerActivityCharacter.id!!, LocalDateTime.of(2026, 5, 20, 0, 0))
flushAndClear() flushAndClear()
val snapshots = repository.findAiCharacterSnapshots(windowStart, snapshotAt, limit = 1) val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 1)
val expectedScore = scorePolicy.calculateAiChatScore( val expectedScore = scorePolicy.calculateAiChatScore(
recentChatCount = 2, recentChatCount = 3,
recentActiveUserCount = 1, recentActiveUserCount = 1,
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 5, 20, 0, 0), snapshotAt) followIncreaseCount = 0,
newBoost = scorePolicy.calculateAiCharacterNewBoost(LocalDateTime.of(2026, 4, 1, 0, 0), snapshotAt)
) )
assertEquals(1, snapshots.size) assertEquals(1, snapshots.size)
assertEquals(RecommendedSectionType.AI_CHARACTER, snapshots.single().sectionType) assertEquals(RecommendedSectionType.AI_CHARACTER, snapshots.single().sectionType)
assertEquals(newLowerActivityCharacter.id, snapshots.single().targetId) assertEquals(oldHighActivityCharacter.id, snapshots.single().targetId)
assertEquals(expectedScore, snapshots.single().score, 0.0001) assertEquals(expectedScore, snapshots.single().score, 0.0001)
assertEquals(snapshotAt, snapshots.single().snapshotAt) assertEquals(snapshotAt, snapshots.single().snapshotAt)
} }
@Test
@DisplayName("AI 캐릭터 스냅샷은 생성일 누락/미래와 유효하지 않은 AI 크리에이터 회원을 제외한다")
fun shouldExcludeInvalidAiCharacterSnapshotCandidates() {
val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val user = saveMember("ai-exclude-user", MemberRole.USER)
val visibleCharacter = saveCharacter("ai-exclude-visible", isActive = true)
val futureCharacter = saveCharacter("ai-exclude-future", isActive = true)
val inactiveCreatorCharacter = saveCharacter("ai-exclude-inactive-creator", isActive = true).apply {
creatorMember!!.isActive = false
}
val userCreatorCharacter = saveCharacter("ai-exclude-user-creator", isActive = true).apply {
creatorMember!!.role = MemberRole.USER
}
val humanCreatorCharacter = saveCharacter("ai-exclude-human-creator", isActive = true).apply {
creatorMember!!.memberKind = MemberKind.HUMAN
}
listOf(visibleCharacter, futureCharacter, inactiveCreatorCharacter, userCreatorCharacter, humanCreatorCharacter)
.forEachIndexed { index, character ->
val room = saveChatRoom("ai-exclude-room-$index")
val userParticipant = saveParticipant(room, ParticipantType.USER, member = user)
val characterParticipant = saveParticipant(room, ParticipantType.CHARACTER, character = character)
updateCreatedAt(
"ChatMessage",
saveMessage(room, userParticipant, "user-$index", true).id!!,
windowStart.plusHours(1)
)
updateCreatedAt(
"ChatMessage",
saveMessage(room, characterParticipant, "character-$index", true).id!!,
windowStart.plusHours(1)
)
}
updateCreatedAt("ChatCharacter", visibleCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
updateCreatedAt("ChatCharacter", futureCharacter.id!!, snapshotAt.plusSeconds(1))
updateCreatedAt("ChatCharacter", inactiveCreatorCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
updateCreatedAt("ChatCharacter", userCreatorCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
updateCreatedAt("ChatCharacter", humanCreatorCharacter.id!!, LocalDateTime.of(2026, 7, 1, 0, 0))
flushAndClear()
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 10)
assertEquals(listOf(visibleCharacter.id), snapshots.map { it.targetId })
}
@Test
@DisplayName("AI 캐릭터 스냅샷은 점수 동점이면 더 늦게 생성된 캐릭터를 먼저 반환하고 상위 20개만 저장 후보로 반환한다")
fun shouldOrderAiCharacterSnapshotsByScoreThenCreatedAtDescWithTopTwentyLimit() {
val windowStart = LocalDateTime.of(2026, 7, 8, 15, 0)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
val windowEndExclusive = snapshotAt.plusSeconds(1)
val user = saveMember("ai-top-user", MemberRole.USER)
val characters = (0..20).map { index ->
val character = saveCharacter("ai-top-$index", isActive = true)
val room = saveChatRoom("ai-top-room-$index")
val participant = saveParticipant(room, ParticipantType.USER, member = user)
saveParticipant(room, ParticipantType.CHARACTER, character = character)
updateCreatedAt("ChatMessage", saveMessage(room, participant, "message-$index", true).id!!, windowStart.plusHours(1))
updateCreatedAt("ChatCharacter", character.id!!, LocalDateTime.of(2026, 6, 1, 0, 0).plusMinutes(index.toLong()))
character
}
flushAndClear()
val snapshots = repository.findAiCharacterSnapshots(windowStart, windowEndExclusive, limit = 20)
assertEquals(20, snapshots.size)
assertEquals(characters.drop(1).reversed().map { it.id }, snapshots.map { it.targetId })
assertEquals(false, snapshots.any { it.targetId == characters.first().id })
}
@Test @Test
@DisplayName("최근 응원 스냅샷은 CHANNEL_DONATION 후원 금액과 후원 수만 집계한다") @DisplayName("최근 응원 스냅샷은 CHANNEL_DONATION 후원 금액과 후원 수만 집계한다")
fun shouldFindCheerCreatorSnapshotsWithChannelDonationOnly() { fun shouldFindCheerCreatorSnapshotsWithChannelDonationOnly() {