feat(content-ranking): 랭킹 조회 언어를 전달한다
This commit is contained in:
+10
-5
@@ -1,5 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.content.ranking.application
|
package kr.co.vividnext.sodalive.v2.content.ranking.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
||||||
@@ -18,6 +19,7 @@ import java.time.ZonedDateTime
|
|||||||
@Service
|
@Service
|
||||||
class AudioRankingQueryService(
|
class AudioRankingQueryService(
|
||||||
private val snapshotPort: AudioRankingSnapshotPort,
|
private val snapshotPort: AudioRankingSnapshotPort,
|
||||||
|
private val langContext: LangContext,
|
||||||
private val memberContentPreferenceService: MemberContentPreferenceService,
|
private val memberContentPreferenceService: MemberContentPreferenceService,
|
||||||
private val blockPort: AudioRankingBlockPort,
|
private val blockPort: AudioRankingBlockPort,
|
||||||
private val jobService: AudioRankingSnapshotJobService,
|
private val jobService: AudioRankingSnapshotJobService,
|
||||||
@@ -29,7 +31,8 @@ class AudioRankingQueryService(
|
|||||||
|
|
||||||
fun getRankings(type: AudioRankingType, member: Member?): AudioRanking {
|
fun getRankings(type: AudioRankingType, member: Member?): AudioRanking {
|
||||||
val nowUtc = nowProvider().withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
|
val nowUtc = nowProvider().withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
|
||||||
val latestSnapshots = findLatestVisibleSnapshots(type, nowUtc)
|
val locale = langContext.lang.code
|
||||||
|
val latestSnapshots = findLatestVisibleSnapshots(type, nowUtc, locale)
|
||||||
if (latestSnapshots.isEmpty()) {
|
if (latestSnapshots.isEmpty()) {
|
||||||
return AudioRanking(showRankChange = false, type = type, items = emptyList())
|
return AudioRanking(showRankChange = false, type = type, items = emptyList())
|
||||||
}
|
}
|
||||||
@@ -37,7 +40,8 @@ class AudioRankingQueryService(
|
|||||||
val previousSnapshots = snapshotPort.findPreviousVisibleSnapshots(
|
val previousSnapshots = snapshotPort.findPreviousVisibleSnapshots(
|
||||||
rankingType = type,
|
rankingType = type,
|
||||||
currentAggregationStartAtUtc = latestSnapshots.first().aggregationStartAtUtc,
|
currentAggregationStartAtUtc = latestSnapshots.first().aggregationStartAtUtc,
|
||||||
nowUtc = nowUtc
|
nowUtc = nowUtc,
|
||||||
|
locale = locale
|
||||||
)
|
)
|
||||||
val blockedCreatorMemberIds = blockedCreatorMemberIds(member, latestSnapshots + previousSnapshots)
|
val blockedCreatorMemberIds = blockedCreatorMemberIds(member, latestSnapshots + previousSnapshots)
|
||||||
val latestVisibleSnapshots = latestSnapshots.visibleTo(canViewAdultContent, blockedCreatorMemberIds).take(ITEM_LIMIT)
|
val latestVisibleSnapshots = latestSnapshots.visibleTo(canViewAdultContent, blockedCreatorMemberIds).take(ITEM_LIMIT)
|
||||||
@@ -58,9 +62,10 @@ class AudioRankingQueryService(
|
|||||||
|
|
||||||
private fun findLatestVisibleSnapshots(
|
private fun findLatestVisibleSnapshots(
|
||||||
type: AudioRankingType,
|
type: AudioRankingType,
|
||||||
nowUtc: java.time.LocalDateTime
|
nowUtc: java.time.LocalDateTime,
|
||||||
|
locale: String
|
||||||
): List<AudioRankingSnapshotRecord> {
|
): List<AudioRankingSnapshotRecord> {
|
||||||
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(type, nowUtc)
|
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(type, nowUtc, locale)
|
||||||
if (latestSnapshots.isNotEmpty()) return latestSnapshots
|
if (latestSnapshots.isNotEmpty()) return latestSnapshots
|
||||||
|
|
||||||
runCatching { jobService.refreshLastCompletedWeekByFallback(type) }
|
runCatching { jobService.refreshLastCompletedWeekByFallback(type) }
|
||||||
@@ -72,7 +77,7 @@ class AudioRankingQueryService(
|
|||||||
ex
|
ex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return snapshotPort.findLatestVisibleSnapshots(type, nowUtc)
|
return snapshotPort.findLatestVisibleSnapshots(type, nowUtc, locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canViewAdultContent(member: Member?): Boolean {
|
private fun canViewAdultContent(member: Member?): Boolean {
|
||||||
|
|||||||
+4
-2
@@ -6,13 +6,15 @@ import java.time.LocalDateTime
|
|||||||
interface AudioRankingSnapshotPort {
|
interface AudioRankingSnapshotPort {
|
||||||
fun findLatestVisibleSnapshots(
|
fun findLatestVisibleSnapshots(
|
||||||
rankingType: AudioRankingType,
|
rankingType: AudioRankingType,
|
||||||
nowUtc: LocalDateTime
|
nowUtc: LocalDateTime,
|
||||||
|
locale: String
|
||||||
): List<AudioRankingSnapshotRecord>
|
): List<AudioRankingSnapshotRecord>
|
||||||
|
|
||||||
fun findPreviousVisibleSnapshots(
|
fun findPreviousVisibleSnapshots(
|
||||||
rankingType: AudioRankingType,
|
rankingType: AudioRankingType,
|
||||||
currentAggregationStartAtUtc: LocalDateTime,
|
currentAggregationStartAtUtc: LocalDateTime,
|
||||||
nowUtc: LocalDateTime
|
nowUtc: LocalDateTime,
|
||||||
|
locale: String
|
||||||
): List<AudioRankingSnapshotRecord>
|
): List<AudioRankingSnapshotRecord>
|
||||||
|
|
||||||
fun replaceSnapshots(
|
fun replaceSnapshots(
|
||||||
|
|||||||
+63
@@ -1,5 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.`in`.web
|
package kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.`in`.web
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
@@ -29,6 +31,25 @@ class AudioRankingControllerTest @Autowired constructor(
|
|||||||
private val mockMvc: MockMvc,
|
private val mockMvc: MockMvc,
|
||||||
private val entityManager: EntityManager
|
private val entityManager: EntityManager
|
||||||
) {
|
) {
|
||||||
|
@Test
|
||||||
|
@DisplayName("오디오 랭킹 조회는 같은 스냅샷에서 en-ja-en 제목 번역을 격리한다")
|
||||||
|
fun shouldIsolateRequestedLocaleTitlesWithoutChangingRankingResponse() {
|
||||||
|
saveSnapshot(contentId = 1L, rank = 1, aggregationStartAtUtc = PREVIOUS_START_AT, aggregationEndAtUtc = PREVIOUS_END_AT)
|
||||||
|
saveSnapshot(contentId = 2L, rank = 2, aggregationStartAtUtc = PREVIOUS_START_AT, aggregationEndAtUtc = PREVIOUS_END_AT)
|
||||||
|
saveSnapshot(contentId = 2L, rank = 1, aggregationStartAtUtc = LATEST_START_AT, aggregationEndAtUtc = LATEST_END_AT)
|
||||||
|
saveSnapshot(contentId = 3L, rank = 2, aggregationStartAtUtc = LATEST_START_AT, aggregationEndAtUtc = LATEST_END_AT)
|
||||||
|
saveContentTranslation(2L, "en", "en-title-2")
|
||||||
|
saveContentTranslation(2L, "ja", "ja-title-2")
|
||||||
|
saveContentTranslation(3L, "en", "en-title-3")
|
||||||
|
saveContentTranslation(3L, "ja", "ja-title-3")
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
|
||||||
|
expectRisingRanking("en", "en-title-2", "en-title-3")
|
||||||
|
expectRisingRanking("ja", "ja-title-2", "ja-title-3")
|
||||||
|
expectRisingRanking("en", "en-title-2", "en-title-3")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("오디오 랭킹 조회는 비회원에게 200 OK와 기본 WEEKLY_POPULAR 랭킹을 반환한다")
|
@DisplayName("오디오 랭킹 조회는 비회원에게 200 OK와 기본 WEEKLY_POPULAR 랭킹을 반환한다")
|
||||||
fun shouldReturnWeeklyPopularRankingsForAnonymousByDefault() {
|
fun shouldReturnWeeklyPopularRankingsForAnonymousByDefault() {
|
||||||
@@ -113,6 +134,48 @@ class AudioRankingControllerTest @Autowired constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveContentTranslation(contentId: Long, locale: String, title: String) {
|
||||||
|
val translation = ContentTranslation(
|
||||||
|
contentId = contentId,
|
||||||
|
locale = locale,
|
||||||
|
renderedPayload = ContentTranslationPayload(title = title, detail = "", tags = "")
|
||||||
|
)
|
||||||
|
entityManager.persist(translation)
|
||||||
|
entityManager.flush()
|
||||||
|
val payload = "{\"title\":\"$title\",\"detail\":\"\",\"tags\":\"\"}"
|
||||||
|
entityManager.createNativeQuery(
|
||||||
|
"update content_translation set rendered_payload = '$payload' format json where id = :id"
|
||||||
|
)
|
||||||
|
.setParameter("id", translation.id)
|
||||||
|
.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectRisingRanking(locale: String, firstTitle: String, secondTitle: String) {
|
||||||
|
mockMvc.perform(
|
||||||
|
get("/api/v2/audio/rankings")
|
||||||
|
.param("type", "RISING")
|
||||||
|
.header("Accept-Language", locale)
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.showRankChange").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.type").value("RISING"))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].contentId").value(2L))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].title").value(firstTitle))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].creatorNickname").value("creator-2"))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].rank").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].rankChange").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].isNew").value(false))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].coverImageUrl").value("https://cdn.test/cover-2.png"))
|
||||||
|
.andExpect(jsonPath("$.data.items[1].contentId").value(3L))
|
||||||
|
.andExpect(jsonPath("$.data.items[1].title").value(secondTitle))
|
||||||
|
.andExpect(jsonPath("$.data.items[1].creatorNickname").value("creator-3"))
|
||||||
|
.andExpect(jsonPath("$.data.items[1].rank").value(2))
|
||||||
|
.andExpect(jsonPath("$.data.items[1].rankChange").doesNotExist())
|
||||||
|
.andExpect(jsonPath("$.data.items[1].isNew").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.items[1].coverImageUrl").value("https://cdn.test/cover-3.png"))
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val PREVIOUS_START_AT = LocalDateTime.of(2026, 5, 25, 15, 0)
|
private val PREVIOUS_START_AT = LocalDateTime.of(2026, 5, 25, 15, 0)
|
||||||
private val PREVIOUS_END_AT = LocalDateTime.of(2026, 6, 1, 15, 0)
|
private val PREVIOUS_END_AT = LocalDateTime.of(2026, 6, 1, 15, 0)
|
||||||
|
|||||||
+51
-4
@@ -1,5 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.content.ranking.application
|
package kr.co.vividnext.sodalive.v2.content.ranking.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType
|
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType
|
||||||
@@ -9,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshot
|
|||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertFalse
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
@@ -21,6 +24,41 @@ import java.time.ZonedDateTime
|
|||||||
|
|
||||||
@ExtendWith(OutputCaptureExtension::class)
|
@ExtendWith(OutputCaptureExtension::class)
|
||||||
class AudioRankingQueryServiceTest {
|
class AudioRankingQueryServiceTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("랭킹은 요청 locale의 제목만 바꾸고 순위 응답은 유지한다")
|
||||||
|
fun shouldUseRequestedLocaleTitlesWithoutChangingRankingValues() {
|
||||||
|
val snapshotPort = FakeAudioRankingQuerySnapshotPort()
|
||||||
|
val originalLatestSnapshots = listOf(snapshot(contentId = 2L, rank = 1), snapshot(contentId = 1L, rank = 2))
|
||||||
|
snapshotPort.latestSnapshots = originalLatestSnapshots
|
||||||
|
snapshotPort.previousSnapshots = listOf(snapshot(contentId = 1L, rank = 1), snapshot(contentId = 2L, rank = 2))
|
||||||
|
snapshotPort.latestSnapshotsByLocale = mapOf(
|
||||||
|
"en" to originalLatestSnapshots.map { it.copy(title = "en-title-${it.contentId}") },
|
||||||
|
"ja" to originalLatestSnapshots.map { it.copy(title = "ja-title-${it.contentId}") }
|
||||||
|
)
|
||||||
|
val langContext = LangContext().apply { setLang(Lang.EN) }
|
||||||
|
val service = service(snapshotPort, langContext = langContext)
|
||||||
|
|
||||||
|
val english = service.getRankings(AudioRankingType.REVENUE, member = null)
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val japanese = service.getRankings(AudioRankingType.REVENUE, member = null)
|
||||||
|
langContext.setLang(Lang.EN)
|
||||||
|
val englishAgain = service.getRankings(AudioRankingType.REVENUE, member = null)
|
||||||
|
|
||||||
|
assertEquals(listOf("en-title-2", "en-title-1"), english.items.map { it.title })
|
||||||
|
assertEquals(listOf("ja-title-2", "ja-title-1"), japanese.items.map { it.title })
|
||||||
|
assertEquals(listOf("en-title-2", "en-title-1"), englishAgain.items.map { it.title })
|
||||||
|
assertEquals(
|
||||||
|
english.copy(items = english.items.map { it.copy(title = "") }),
|
||||||
|
japanese.copy(items = japanese.items.map { it.copy(title = "") })
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
english.copy(items = english.items.map { it.copy(title = "") }),
|
||||||
|
englishAgain.copy(items = englishAgain.items.map { it.copy(title = "") })
|
||||||
|
)
|
||||||
|
assertEquals(listOf("en", "ja", "en"), snapshotPort.latestLocales)
|
||||||
|
assertEquals(listOf("en", "ja", "en"), snapshotPort.previousLocales)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldNotWrapGetRankingsInTransactionSoFallbackRequeryUsesFreshSnapshot() {
|
fun shouldNotWrapGetRankingsInTransactionSoFallbackRequeryUsesFreshSnapshot() {
|
||||||
val method = AudioRankingQueryService::class.java.getDeclaredMethod(
|
val method = AudioRankingQueryService::class.java.getDeclaredMethod(
|
||||||
@@ -223,7 +261,8 @@ class AudioRankingQueryServiceTest {
|
|||||||
snapshotPort: FakeAudioRankingQuerySnapshotPort,
|
snapshotPort: FakeAudioRankingQuerySnapshotPort,
|
||||||
adultMember: Member? = null,
|
adultMember: Member? = null,
|
||||||
blockPort: AudioRankingBlockPort = FakeAudioRankingBlockPort(),
|
blockPort: AudioRankingBlockPort = FakeAudioRankingBlockPort(),
|
||||||
jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java)
|
jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java),
|
||||||
|
langContext: LangContext = LangContext()
|
||||||
): AudioRankingQueryService {
|
): AudioRankingQueryService {
|
||||||
val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||||
if (adultMember != null) {
|
if (adultMember != null) {
|
||||||
@@ -231,6 +270,7 @@ class AudioRankingQueryServiceTest {
|
|||||||
}
|
}
|
||||||
return AudioRankingQueryService(
|
return AudioRankingQueryService(
|
||||||
snapshotPort = snapshotPort,
|
snapshotPort = snapshotPort,
|
||||||
|
langContext = langContext,
|
||||||
memberContentPreferenceService = memberContentPreferenceService,
|
memberContentPreferenceService = memberContentPreferenceService,
|
||||||
blockPort = blockPort,
|
blockPort = blockPort,
|
||||||
jobService = jobService,
|
jobService = jobService,
|
||||||
@@ -275,25 +315,32 @@ private class FakeAudioRankingQuerySnapshotPort : AudioRankingSnapshotPort {
|
|||||||
var nowUtc: LocalDateTime? = null
|
var nowUtc: LocalDateTime? = null
|
||||||
var currentAggregationStartAtUtc: LocalDateTime? = null
|
var currentAggregationStartAtUtc: LocalDateTime? = null
|
||||||
var latestCallCount: Int = 0
|
var latestCallCount: Int = 0
|
||||||
|
var latestLocales: MutableList<String> = mutableListOf()
|
||||||
|
var previousLocales: MutableList<String> = mutableListOf()
|
||||||
|
var latestSnapshotsByLocale: Map<String, List<AudioRankingSnapshotRecord>> = emptyMap()
|
||||||
|
|
||||||
override fun findLatestVisibleSnapshots(
|
override fun findLatestVisibleSnapshots(
|
||||||
rankingType: AudioRankingType,
|
rankingType: AudioRankingType,
|
||||||
nowUtc: LocalDateTime
|
nowUtc: LocalDateTime,
|
||||||
|
locale: String
|
||||||
): List<AudioRankingSnapshotRecord> {
|
): List<AudioRankingSnapshotRecord> {
|
||||||
this.nowUtc = nowUtc
|
this.nowUtc = nowUtc
|
||||||
|
latestLocales.add(locale)
|
||||||
latestCallCount += 1
|
latestCallCount += 1
|
||||||
if (latestSnapshotsByCall.isNotEmpty()) {
|
if (latestSnapshotsByCall.isNotEmpty()) {
|
||||||
return latestSnapshotsByCall.getOrElse(latestCallCount - 1) { latestSnapshotsByCall.last() }
|
return latestSnapshotsByCall.getOrElse(latestCallCount - 1) { latestSnapshotsByCall.last() }
|
||||||
}
|
}
|
||||||
return latestSnapshots
|
return latestSnapshotsByLocale[locale] ?: latestSnapshots
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findPreviousVisibleSnapshots(
|
override fun findPreviousVisibleSnapshots(
|
||||||
rankingType: AudioRankingType,
|
rankingType: AudioRankingType,
|
||||||
currentAggregationStartAtUtc: LocalDateTime,
|
currentAggregationStartAtUtc: LocalDateTime,
|
||||||
nowUtc: LocalDateTime
|
nowUtc: LocalDateTime,
|
||||||
|
locale: String
|
||||||
): List<AudioRankingSnapshotRecord> {
|
): List<AudioRankingSnapshotRecord> {
|
||||||
this.currentAggregationStartAtUtc = currentAggregationStartAtUtc
|
this.currentAggregationStartAtUtc = currentAggregationStartAtUtc
|
||||||
|
previousLocales.add(locale)
|
||||||
return previousSnapshots
|
return previousSnapshots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user