feat(content-ranking): 랭킹 조회 언어를 전달한다

This commit is contained in:
2026-09-09 23:22:44 +09:00
parent 21f72f2827
commit e5230c0950
4 changed files with 128 additions and 11 deletions
@@ -1,5 +1,7 @@
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.MemberAdapter
import kr.co.vividnext.sodalive.member.MemberRole
@@ -29,6 +31,25 @@ class AudioRankingControllerTest @Autowired constructor(
private val mockMvc: MockMvc,
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
@DisplayName("오디오 랭킹 조회는 비회원에게 200 OK와 기본 WEEKLY_POPULAR 랭킹을 반환한다")
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 {
private val PREVIOUS_START_AT = LocalDateTime.of(2026, 5, 25, 15, 0)
private val PREVIOUS_END_AT = LocalDateTime.of(2026, 6, 1, 15, 0)
@@ -1,5 +1,7 @@
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.contentpreference.MemberContentPreferenceService
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.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito
@@ -21,6 +24,41 @@ import java.time.ZonedDateTime
@ExtendWith(OutputCaptureExtension::class)
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
fun shouldNotWrapGetRankingsInTransactionSoFallbackRequeryUsesFreshSnapshot() {
val method = AudioRankingQueryService::class.java.getDeclaredMethod(
@@ -223,7 +261,8 @@ class AudioRankingQueryServiceTest {
snapshotPort: FakeAudioRankingQuerySnapshotPort,
adultMember: Member? = null,
blockPort: AudioRankingBlockPort = FakeAudioRankingBlockPort(),
jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java)
jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java),
langContext: LangContext = LangContext()
): AudioRankingQueryService {
val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
if (adultMember != null) {
@@ -231,6 +270,7 @@ class AudioRankingQueryServiceTest {
}
return AudioRankingQueryService(
snapshotPort = snapshotPort,
langContext = langContext,
memberContentPreferenceService = memberContentPreferenceService,
blockPort = blockPort,
jobService = jobService,
@@ -275,25 +315,32 @@ private class FakeAudioRankingQuerySnapshotPort : AudioRankingSnapshotPort {
var nowUtc: LocalDateTime? = null
var currentAggregationStartAtUtc: LocalDateTime? = null
var latestCallCount: Int = 0
var latestLocales: MutableList<String> = mutableListOf()
var previousLocales: MutableList<String> = mutableListOf()
var latestSnapshotsByLocale: Map<String, List<AudioRankingSnapshotRecord>> = emptyMap()
override fun findLatestVisibleSnapshots(
rankingType: AudioRankingType,
nowUtc: LocalDateTime
nowUtc: LocalDateTime,
locale: String
): List<AudioRankingSnapshotRecord> {
this.nowUtc = nowUtc
latestLocales.add(locale)
latestCallCount += 1
if (latestSnapshotsByCall.isNotEmpty()) {
return latestSnapshotsByCall.getOrElse(latestCallCount - 1) { latestSnapshotsByCall.last() }
}
return latestSnapshots
return latestSnapshotsByLocale[locale] ?: latestSnapshots
}
override fun findPreviousVisibleSnapshots(
rankingType: AudioRankingType,
currentAggregationStartAtUtc: LocalDateTime,
nowUtc: LocalDateTime
nowUtc: LocalDateTime,
locale: String
): List<AudioRankingSnapshotRecord> {
this.currentAggregationStartAtUtc = currentAggregationStartAtUtc
previousLocales.add(locale)
return previousSnapshots
}