feat(content-ranking): 랭킹 공개 최근 소식을 발행한다
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.v2.ranking.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicy
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicy
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||
@@ -19,7 +20,8 @@ import java.time.ZonedDateTime
|
||||
@Service
|
||||
class CreatorRankingSnapshotRefreshService(
|
||||
private val aggregationPort: CreatorRankingAggregationPort,
|
||||
private val snapshotPort: CreatorRankingSnapshotPort
|
||||
private val snapshotPort: CreatorRankingSnapshotPort,
|
||||
private val homeFollowingNewsPublishService: HomeFollowingNewsPublishService
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
private val periodPolicy = CreatorRankingPeriodPolicy()
|
||||
@@ -47,6 +49,28 @@ class CreatorRankingSnapshotRefreshService(
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
newSnapshots = snapshots
|
||||
)
|
||||
afterCommit {
|
||||
snapshots.forEachIndexed { index, snapshot ->
|
||||
runCatching {
|
||||
homeFollowingNewsPublishService.publishCreatorRankingVisible(
|
||||
creatorId = snapshot.creatorId,
|
||||
creatorNickname = snapshot.nickname,
|
||||
creatorProfileImagePath = snapshot.profileImageUrl,
|
||||
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
rank = index + 1
|
||||
)
|
||||
}.onFailure { ex ->
|
||||
log.warn(
|
||||
"event=home_following_creator_ranking_news_publish_failure creatorId={} rank={} error={}",
|
||||
snapshot.creatorId,
|
||||
index + 1,
|
||||
ex.message,
|
||||
ex
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
aggregationResult.toLogCounts(storedCount = snapshots.size)
|
||||
}.onSuccess { counts ->
|
||||
afterCommit {
|
||||
@@ -92,12 +116,18 @@ class CreatorRankingSnapshotRefreshService(
|
||||
|
||||
private fun afterCommit(action: () -> Unit) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
action()
|
||||
runCatching(action).onFailure { ex ->
|
||||
log.warn("event=creator_ranking_after_commit_failure error={}", ex.message, ex)
|
||||
}
|
||||
return
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
object : TransactionSynchronization {
|
||||
override fun afterCommit() = action()
|
||||
override fun afterCommit() {
|
||||
runCatching(action).onFailure { ex ->
|
||||
log.warn("event=creator_ranking_after_commit_failure error={}", ex.message, ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.v2.ranking.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
|
||||
@@ -11,6 +12,7 @@ import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.boot.test.system.CapturedOutput
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||
@@ -165,16 +167,125 @@ class CreatorRankingSnapshotRefreshServiceTest {
|
||||
assertEquals(true, output.out.contains("error=aggregate failed"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("주간 스냅샷 저장 성공 후 크리에이터 랭킹 최근 소식을 순위와 함께 발행한다")
|
||||
fun shouldPublishCreatorRankingNewsAfterSnapshotsAreReplaced() {
|
||||
val aggregationPort = FakeCreatorRankingAggregationPort()
|
||||
val snapshotPort = FakeCreatorRankingSnapshotPort()
|
||||
val publishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||
val service = service(
|
||||
aggregationPort = aggregationPort,
|
||||
snapshotPort = snapshotPort,
|
||||
publishService = publishService
|
||||
)
|
||||
aggregationPort.candidates = listOf(
|
||||
candidate(creatorId = 1L, liveCanAmount = 200),
|
||||
candidate(creatorId = 2L, liveCanAmount = 100)
|
||||
)
|
||||
|
||||
service.refreshLastCompletedWeek(ZonedDateTime.of(2026, 6, 8, 6, 0, 0, 0, ZoneId.of("Asia/Seoul")))
|
||||
|
||||
Mockito.verify(publishService).publishCreatorRankingVisible(
|
||||
creatorId = 1L,
|
||||
creatorNickname = "creator-1",
|
||||
creatorProfileImagePath = "profile-1.png",
|
||||
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0),
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0),
|
||||
rank = 1
|
||||
)
|
||||
Mockito.verify(publishService).publishCreatorRankingVisible(
|
||||
creatorId = 2L,
|
||||
creatorNickname = "creator-2",
|
||||
creatorProfileImagePath = "profile-2.png",
|
||||
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0),
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0),
|
||||
rank = 2
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("주간 스냅샷 저장 실패 시 크리에이터 랭킹 최근 소식을 발행하지 않는다")
|
||||
fun shouldNotPublishCreatorRankingNewsWhenReplaceSnapshotsFails() {
|
||||
val aggregationPort = FakeCreatorRankingAggregationPort()
|
||||
val snapshotPort = FakeCreatorRankingSnapshotPort()
|
||||
val publishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||
val service = service(
|
||||
aggregationPort = aggregationPort,
|
||||
snapshotPort = snapshotPort,
|
||||
publishService = publishService
|
||||
)
|
||||
aggregationPort.candidates = listOf(candidate(creatorId = 1L, liveCanAmount = 100))
|
||||
snapshotPort.failure = IllegalStateException("replace failed")
|
||||
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
service.refreshLastCompletedWeek(ZonedDateTime.of(2026, 6, 8, 6, 0, 0, 0, ZoneId.of("Asia/Seoul")))
|
||||
}
|
||||
|
||||
Mockito.verifyNoInteractions(publishService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("일부 크리에이터 랭킹 최근 소식 발행 실패는 스냅샷 갱신을 실패시키지 않는다")
|
||||
fun shouldNotFailSnapshotRefreshWhenCreatorRankingNewsPublishFails() {
|
||||
val aggregationPort = FakeCreatorRankingAggregationPort()
|
||||
val snapshotPort = FakeCreatorRankingSnapshotPort()
|
||||
val publishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||
val service = service(
|
||||
aggregationPort = aggregationPort,
|
||||
snapshotPort = snapshotPort,
|
||||
publishService = publishService
|
||||
)
|
||||
aggregationPort.candidates = listOf(
|
||||
candidate(creatorId = 1L, liveCanAmount = 200),
|
||||
candidate(creatorId = 2L, liveCanAmount = 100)
|
||||
)
|
||||
Mockito.doAnswer { invocation ->
|
||||
if (invocation.getArgument<Long>(0) == 1L) {
|
||||
throw IllegalStateException("publish failed")
|
||||
}
|
||||
0
|
||||
}.`when`(publishService)
|
||||
.publishCreatorRankingVisible(
|
||||
creatorId = Mockito.anyLong(),
|
||||
creatorNickname = anyStringValue(),
|
||||
creatorProfileImagePath = Mockito.anyString(),
|
||||
aggregationStartAtUtc = anyLocalDateTime(),
|
||||
visibleFromAtUtc = anyLocalDateTime(),
|
||||
rank = Mockito.anyInt()
|
||||
)
|
||||
|
||||
service.refreshLastCompletedWeek(ZonedDateTime.of(2026, 6, 8, 6, 0, 0, 0, ZoneId.of("Asia/Seoul")))
|
||||
|
||||
Mockito.verify(publishService).publishCreatorRankingVisible(
|
||||
creatorId = 2L,
|
||||
creatorNickname = "creator-2",
|
||||
creatorProfileImagePath = "profile-2.png",
|
||||
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0),
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0),
|
||||
rank = 2
|
||||
)
|
||||
}
|
||||
|
||||
private fun service(
|
||||
aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingAggregationPort(),
|
||||
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingSnapshotPort()
|
||||
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingSnapshotPort(),
|
||||
publishService: HomeFollowingNewsPublishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||
): CreatorRankingSnapshotRefreshService {
|
||||
return CreatorRankingSnapshotRefreshService(
|
||||
aggregationPort = aggregationPort,
|
||||
snapshotPort = snapshotPort
|
||||
snapshotPort = snapshotPort,
|
||||
homeFollowingNewsPublishService = publishService
|
||||
)
|
||||
}
|
||||
|
||||
private fun anyStringValue(): String {
|
||||
return Mockito.anyString() ?: ""
|
||||
}
|
||||
|
||||
private fun anyLocalDateTime(): LocalDateTime {
|
||||
return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.MIN
|
||||
}
|
||||
|
||||
private fun candidate(
|
||||
creatorId: Long,
|
||||
finalScore: Double = 0.0,
|
||||
@@ -247,6 +358,7 @@ private class FakeCreatorRankingSnapshotPort : CreatorRankingSnapshotPort {
|
||||
var aggregationStartAtUtc: LocalDateTime? = null
|
||||
var aggregationEndAtUtc: LocalDateTime? = null
|
||||
var visibleFromAtUtc: LocalDateTime? = null
|
||||
var failure: RuntimeException? = null
|
||||
|
||||
override fun findSnapshotsByAggregationPeriod(
|
||||
aggregationStartAtUtc: LocalDateTime,
|
||||
@@ -281,6 +393,7 @@ private class FakeCreatorRankingSnapshotPort : CreatorRankingSnapshotPort {
|
||||
visibleFromAtUtc: LocalDateTime,
|
||||
newSnapshots: List<CreatorRankingSnapshotRecord>
|
||||
) {
|
||||
failure?.let { throw it }
|
||||
this.rankingType = rankingType
|
||||
this.aggregationStartAtUtc = aggregationStartAtUtc
|
||||
this.aggregationEndAtUtc = aggregationEndAtUtc
|
||||
|
||||
Reference in New Issue
Block a user