diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt index 656ed0a2..942dc8ce 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt @@ -601,7 +601,7 @@ class DefaultHomeRecommendationQueryRepository( join use_can uc on uc.id = ucc.use_can_id where ucc.status = 'RECEIVED' and uc.is_refund = false - and uc.can_usage = 'CHANNEL_DONATION' + and uc.can_usage in ('CHANNEL_DONATION', 'DONATION') and ucc.created_at >= :windowStart and ucc.created_at < :windowEndExclusive group by ucc.recipient_creator_id diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt index 963286b3..f1f2817c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt @@ -91,7 +91,7 @@ open class RecommendationSnapshotRefreshService( @Transactional open fun refreshCheerCreatorSnapshots(nowUtc: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)): Int { val startedAt = System.currentTimeMillis() - val window = windowPolicy.previousKstDayUtcWindow(nowUtc) + val window = windowPolicy.previousKstSevenDayUtcWindow(nowUtc) val snapshots = queryPort.findCheerCreatorSnapshots( window.startUtc, window.endExclusiveUtc, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt index fcac5d89..ea202b9b 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt @@ -773,8 +773,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( } @Test - @DisplayName("최근 응원 스냅샷은 전날 half-open 범위의 CHANNEL_DONATION 금액과 distinct 후원 수만 집계한다") - fun shouldFindCheerCreatorSnapshotsWithChannelDonationOnly() { + @DisplayName("최근 응원 스냅샷은 half-open 범위의 CHANNEL_DONATION과 DONATION 금액 및 distinct 후원 수만 집계한다") + fun shouldFindCheerCreatorSnapshotsWithChannelAndGeneralDonation() { val windowStart = LocalDateTime.of(2026, 5, 28, 15, 0) val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0) val snapshotAt = windowEndExclusive.minusSeconds(1) @@ -877,9 +877,9 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val snapshots = repository.findCheerCreatorSnapshots(windowStart, windowEndExclusive, limit = 10) val expectedScore = scorePolicy.calculateCheerScore( - donationAmount = 150, + donationAmount = 1_150, fanTalkCount = 1, - donationCount = 1, + donationCount = 2, newBoost = scorePolicy.calculateCheerCreatorNewBoost(LocalDateTime.of(2026, 5, 10, 12, 0), snapshotAt) ) assertEquals(1, snapshots.size) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt index 471bc131..9fad1533 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt @@ -47,7 +47,7 @@ class RecommendationSnapshotRefreshServiceTest { } @Test - @DisplayName("일 스냅샷 갱신은 전날 23시 59분 59초 기준으로 DB 점수 계산 스냅샷을 저장한다") + @DisplayName("일 스냅샷 갱신은 응원 크리에이터를 인기 커뮤니티와 같은 7일 window로 저장한다") fun shouldRefreshDailySnapshotsWithPreviousDayEndSnapshotAt(output: CapturedOutput) { val snapshotPort = FakeRecommendationSnapshotPort() val queryPort = Mockito.mock(HomeRecommendationQueryPort::class.java) @@ -71,7 +71,7 @@ class RecommendationSnapshotRefreshServiceTest { ) ) ) - Mockito.`when`(queryPort.findCheerCreatorSnapshots(aiWindowStart, aiWindowEndExclusive, 16)).thenReturn( + Mockito.`when`(queryPort.findCheerCreatorSnapshots(windowStart, windowEndExclusive, 16)).thenReturn( listOf( RecommendationSnapshotRecord( sectionType = RecommendedSectionType.CHEER_CREATOR, @@ -112,7 +112,7 @@ class RecommendationSnapshotRefreshServiceTest { assertEquals(40.0, communitySnapshots.single().score, 0.0001) Mockito.verify(queryPort).findAiCharacterSnapshots(aiWindowStart, aiWindowEndExclusive, 20) - Mockito.verify(queryPort).findCheerCreatorSnapshots(aiWindowStart, aiWindowEndExclusive, 16) + Mockito.verify(queryPort).findCheerCreatorSnapshots(windowStart, windowEndExclusive, 16) Mockito.verify(queryPort).findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20) assertEquals(true, output.out.contains("event=recommendation_snapshot_refresh_success")) assertEquals(true, output.out.contains("aiCharacterCount=1")) @@ -135,8 +135,8 @@ class RecommendationSnapshotRefreshServiceTest { ).thenReturn(emptyList()) Mockito.`when`( queryPort.findCheerCreatorSnapshots( - LocalDateTime.of(2026, 5, 28, 15, 0, 0), - LocalDateTime.of(2026, 5, 29, 15, 0, 0), + windowStart, + windowEndExclusive, 16 ) ).thenReturn(emptyList()) @@ -179,8 +179,8 @@ class RecommendationSnapshotRefreshServiceTest { ) Mockito.`when`( queryPort.findCheerCreatorSnapshots( - LocalDateTime.of(2026, 5, 28, 15, 0, 0), - aiWindowEndExclusive, + windowStart, + windowEndExclusive, 16 ) ).thenReturn( @@ -229,8 +229,8 @@ class RecommendationSnapshotRefreshServiceTest { ).thenReturn(emptyList()) Mockito.`when`( queryPort.findCheerCreatorSnapshots( - LocalDateTime.of(2026, 5, 28, 15, 0, 0), - LocalDateTime.of(2026, 5, 29, 15, 0, 0), + windowStart, + windowEndExclusive, 16 ) ).thenReturn(emptyList()) @@ -264,8 +264,6 @@ class RecommendationSnapshotRefreshServiceTest { val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0) val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0) val snapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59) - val cheerWindowStart = LocalDateTime.of(2026, 5, 28, 15, 0, 0) - val cheerWindowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0) Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.AI_CHARACTER_LOCK_KEY)).thenReturn(aiLock) Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(cheerLock) Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY)) @@ -276,7 +274,7 @@ class RecommendationSnapshotRefreshServiceTest { Mockito.`when`(cheerLock.isHeldByCurrentThread).thenReturn(true) Mockito.`when`(popularLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true) Mockito.`when`(popularLock.isHeldByCurrentThread).thenReturn(true) - Mockito.`when`(queryPort.findCheerCreatorSnapshots(cheerWindowStart, cheerWindowEndExclusive, 16)).thenReturn( + Mockito.`when`(queryPort.findCheerCreatorSnapshots(windowStart, windowEndExclusive, 16)).thenReturn( listOf(snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 10L, score = 10.0, snapshotAt = snapshotAt)) ) Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(emptyList()) @@ -285,7 +283,7 @@ class RecommendationSnapshotRefreshServiceTest { Mockito.verify(redissonClient).getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY) Mockito.verify(cheerLock).tryLock(0, -1, TimeUnit.MILLISECONDS) - Mockito.verify(queryPort).findCheerCreatorSnapshots(cheerWindowStart, cheerWindowEndExclusive, 16) + Mockito.verify(queryPort).findCheerCreatorSnapshots(windowStart, windowEndExclusive, 16) assertEquals(listOf(10L), snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId }) Mockito.verify(cheerLock).unlock() Mockito.verify(popularLock).unlock() @@ -322,8 +320,8 @@ class RecommendationSnapshotRefreshServiceTest { service.refreshDailySnapshots(now) Mockito.verify(queryPort, Mockito.never()).findCheerCreatorSnapshots( - LocalDateTime.of(2026, 5, 28, 15, 0, 0), - LocalDateTime.of(2026, 5, 29, 15, 0, 0), + windowStart, + windowEndExclusive, 16 ) assertEquals( @@ -361,8 +359,8 @@ class RecommendationSnapshotRefreshServiceTest { Mockito.`when`(popularLock.isHeldByCurrentThread).thenReturn(true) Mockito.`when`( queryPort.findCheerCreatorSnapshots( - LocalDateTime.of(2026, 5, 28, 15, 0, 0), - LocalDateTime.of(2026, 5, 29, 15, 0, 0), + windowStart, + windowEndExclusive, 16 ) ).thenReturn(