From ca26577279608fa7027920f3641bc49245658810 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 10 Jul 2026 03:21:16 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20AI=20=EC=BA=90=EB=A6=AD=ED=84=B0?= =?UTF-8?q?=20=EB=B9=88=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EB=A7=88=EC=BB=A4?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=80=EC=9E=A5=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ecommendationSnapshotPersistenceAdapter.kt | 27 +++++++++++++++ .../RecommendationSnapshotRepository.kt | 5 +++ .../port/out/RecommendationSnapshotPort.kt | 2 ++ ...mendationSnapshotPersistenceAdapterTest.kt | 34 +++++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt index ac9082e3..f91f9386 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt @@ -18,11 +18,21 @@ class RecommendationSnapshotPersistenceAdapter( return repository.findLatestSnapshots(sectionType.name, offset, limit).map { it.toRecord() } } + override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean { + return repository.existsBySectionType(sectionType) + } + override fun replaceSnapshots( sectionType: RecommendedSectionType, snapshotAt: LocalDateTime, newSnapshots: List ) { + if (newSnapshots.isEmpty() && sectionType == RecommendedSectionType.AI_CHARACTER) { + repository.deleteBySectionType(sectionType) + repository.save(emptySnapshotMarker(sectionType, snapshotAt).toEntity()) + return + } + repository.deleteBySectionTypeAndSnapshotAt(sectionType, snapshotAt) repository.saveAll(newSnapshots.map { it.toEntity() }) } @@ -46,4 +56,21 @@ class RecommendationSnapshotPersistenceAdapter( randomTieBreaker = randomTieBreaker ) } + + private fun emptySnapshotMarker( + sectionType: RecommendedSectionType, + snapshotAt: LocalDateTime + ): RecommendationSnapshotRecord { + return RecommendationSnapshotRecord( + sectionType = sectionType, + targetId = EMPTY_SNAPSHOT_MARKER_TARGET_ID, + score = 0.0, + snapshotAt = snapshotAt, + randomTieBreaker = 0.0 + ) + } + + companion object { + private const val EMPTY_SNAPSHOT_MARKER_TARGET_ID = 0L + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotRepository.kt index fb8914f6..1a231480 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotRepository.kt @@ -17,6 +17,7 @@ interface RecommendationSnapshotRepository : JpaRepository 0 order by rs.score desc, rs.random_tie_breaker asc limit :limit offset :offset """, @@ -29,4 +30,8 @@ interface RecommendationSnapshotRepository : JpaRepository fun deleteBySectionTypeAndSnapshotAt(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime) + + fun deleteBySectionType(sectionType: RecommendedSectionType) + + fun existsBySectionType(sectionType: RecommendedSectionType): Boolean } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/RecommendationSnapshotPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/RecommendationSnapshotPort.kt index 77f873f7..38d37691 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/RecommendationSnapshotPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/RecommendationSnapshotPort.kt @@ -10,6 +10,8 @@ interface RecommendationSnapshotPort { limit: Int = Int.MAX_VALUE ): List + fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean + fun replaceSnapshots( sectionType: RecommendedSectionType, snapshotAt: LocalDateTime, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt index f34e6466..bbc08264 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt @@ -108,6 +108,40 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor( assertEquals(3, repository.findAll().size) } + @Test + fun shouldClearAiCharacterSectionSnapshotsWhenReplacingWithEmptySnapshots() { + val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59) + val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59) + repository.saveAll( + listOf( + snapshot(RecommendedSectionType.AI_CHARACTER, targetId = 1L, score = 100.0, snapshotAt = oldSnapshotAt), + snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 2L, score = 200.0, snapshotAt = oldSnapshotAt) + ) + ) + + adapter.replaceSnapshots(RecommendedSectionType.AI_CHARACTER, snapshotAt, emptyList()) + + assertEquals(emptyList(), adapter.findLatestSnapshots(RecommendedSectionType.AI_CHARACTER)) + assertEquals(listOf(2L), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId }) + assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER)) + assertEquals( + listOf(0L), + repository.findAll() + .filter { it.sectionType == RecommendedSectionType.AI_CHARACTER } + .map { it.targetId } + ) + } + + @Test + fun shouldExcludeEmptySnapshotMarkerFromLatestSnapshotResults() { + val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59) + + adapter.replaceSnapshots(RecommendedSectionType.AI_CHARACTER, snapshotAt, emptyList()) + + assertEquals(emptyList(), adapter.findLatestSnapshots(RecommendedSectionType.AI_CHARACTER)) + assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER)) + } + private fun snapshot( sectionType: RecommendedSectionType, targetId: Long,