fix(event): 메인 탭 배너에 국가별 언어 필터를 적용한다

This commit is contained in:
2026-11-03 00:29:21 +09:00
parent 7fce26adb7
commit 255da2b6aa
7 changed files with 201 additions and 6 deletions
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository
import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse
import kr.co.vividnext.sodalive.event.EventService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.rank.RankingService
@@ -67,7 +68,10 @@ class AudioContentMainTabAlarmService(
theme = alarmThemeList[0]
)
val eventBannerList = eventService.getEventList(isAdult = isAdult)
val eventBannerList = eventService.getEventList(
isAdult = isAdult,
lang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
)
val curationList = curationRepository.findByContentMainTabId(tabId = 4, isAdult = isAdult)
.map {
GetContentCurationResponse(
@@ -8,6 +8,7 @@ import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryR
import kr.co.vividnext.sodalive.content.main.tab.AudioContentMainTabRepository
import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse
import kr.co.vividnext.sodalive.event.EventService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.rank.RankingService
@@ -66,7 +67,10 @@ class AudioContentMainTabAsmrService(
emptyList()
}
val eventBannerList = eventService.getEventList(isAdult = isAdult)
val eventBannerList = eventService.getEventList(
isAdult = isAdult,
lang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
)
val curationList = curationRepository.findByContentMainTabId(tabId = tabId, isAdult = isAdult)
.map {
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryR
import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.event.EventService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.rank.RankingService
@@ -86,7 +87,10 @@ class AudioContentMainTabContentService(
)
// 이벤트 배너
val eventBannerList = eventService.getEventList(isAdult = isAdult)
val eventBannerList = eventService.getEventList(
isAdult = isAdult,
lang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
)
val contentRankCreatorList = rankingService.fetchCreatorBySellContentCountRankTop20(
memberId = member.id!!,
@@ -4,6 +4,7 @@ import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService
import kr.co.vividnext.sodalive.event.EventService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.notice.ServiceNoticeService
@@ -83,7 +84,10 @@ class AudioContentMainTabHomeService(
)
// 이벤트 배너
val eventBannerList = eventService.getEventList(isAdult = isAdult)
val eventBannerList = eventService.getEventList(
isAdult = isAdult,
lang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
)
val contentRankCreatorList = rankingService.fetchCreatorBySellContentCountRankTop20(
memberId = member?.id,
@@ -8,6 +8,7 @@ import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryR
import kr.co.vividnext.sodalive.content.main.tab.AudioContentMainTabRepository
import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse
import kr.co.vividnext.sodalive.event.EventService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.rank.RankingService
@@ -66,7 +67,10 @@ class AudioContentMainTabLiveReplayService(
emptyList()
}
val eventBannerList = eventService.getEventList(isAdult = isAdult)
val eventBannerList = eventService.getEventList(
isAdult = isAdult,
lang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
)
val curationList = curationRepository.findByContentMainTabId(tabId = tabId, isAdult = isAdult)
.map {
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.content.main.tab.RecommendSeriesRepository
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.event.EventService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.rank.RankingService
@@ -123,7 +124,10 @@ class AudioContentMainTabSeriesService(
}
// 이벤트 배너
val eventBannerList = eventService.getEventList(isAdult = isAdult)
val eventBannerList = eventService.getEventList(
isAdult = isAdult,
lang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
)
// 큐레이션
val curationList = curationRepository.findByContentMainTabId(tabId = 2, isAdult = isAdult)
@@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.event
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.jwt.TokenProvider
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreference
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceRepository
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import java.time.LocalDateTime
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.datasource.url=jdbc:h2:mem:event-banner-language-http;" +
"MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
]
)
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class EventBannerLanguageIntegrationTest @Autowired constructor(
private val restTemplate: TestRestTemplate,
private val objectMapper: ObjectMapper,
private val eventRepository: EventRepository,
private val memberRepository: MemberRepository,
private val preferenceRepository: MemberContentPreferenceRepository,
private val tokenRepository: MemberTokenRepository,
private val tokenProvider: TokenProvider,
private val jdbcTemplate: JdbcTemplate
) {
private val eventIds = mutableMapOf<Pair<Lang, Boolean?>, Long>()
private val tokens = mutableMapOf<Long, String>()
@BeforeEach
fun setUp() {
jdbcTemplate.execute(
"CREATE ALIAS IF NOT EXISTS DATE_FORMAT FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.dateFormat'"
)
jdbcTemplate.execute(
"CREATE ALIAS IF NOT EXISTS CONVERT_TZ FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.convertTz'"
)
Lang.values().forEach { lang ->
listOf(null, false, true).forEach { adult ->
val saved = eventRepository.saveAndFlush(
Event(
thumbnailImage = "$lang-$adult.png",
detailImage = null,
popupImage = null,
link = "https://example.test/$lang",
title = "$lang-$adult",
lang = lang,
isAdult = adult,
startDate = LocalDateTime.now().minusDays(1),
endDate = LocalDateTime.now().plusDays(1)
)
)
eventIds[lang to adult] = requireNotNull(saved.id)
}
}
listOf(900001L, 29721L, 16L).forEach { id ->
val saved = memberRepository.saveAndFlush(
Member(email = "banner-$id@test.com", password = "password", nickname = "banner-$id")
)
jdbcTemplate.update("update member set id = ? where id = ?", id, saved.id)
val member = memberRepository.findById(id).orElseThrow()
preferenceRepository.saveAndFlush(MemberContentPreference().apply { this.member = member })
tokens[id] = tokenProvider.createToken(
UsernamePasswordAuthenticationToken(member.email, "", listOf(SimpleGrantedAuthority("ROLE_USER"))),
id
)
}
}
@AfterEach
fun cleanUp() {
tokenRepository.deleteAll()
preferenceRepository.deleteAllInBatch()
memberRepository.deleteAllById(tokens.keys)
eventRepository.deleteAllInBatch()
tokens.clear()
eventIds.clear()
}
@ParameterizedTest
@ValueSource(strings = ["alarm", "replay", "series", "asmr", "content", "home"])
@DisplayName("여섯 메인 탭은 실제 회원 국가·강제 매핑의 언어와 기존 성인 조건을 함께 적용한다")
fun shouldFilterEachMainTabByCountryLanguageWithoutChangingAdultPolicy(tab: String) {
val cases = listOf(
Case("JP", 900001L, false, Lang.JA, false),
Case("KR", 900001L, false, Lang.KO, false),
Case("US", 900001L, false, Lang.KO, false),
Case(null, 900001L, false, Lang.KO, false),
Case("JP", 900001L, true, Lang.JA, true),
Case("KR", 900001L, true, Lang.KO, false),
Case("US", 900001L, true, Lang.KO, true),
Case(null, 900001L, true, Lang.KO, false),
Case("KR", 29721L, true, Lang.JA, true),
Case("JP", 16L, true, Lang.KO, false)
)
cases.forEach { case ->
jdbcTemplate.update(
"update member_content_preference set is_adult_content_visible = ? where member_id = ?",
case.storedAdult,
case.memberId
)
assertEventIds(tab, case.country, tokens.getValue(case.memberId), case.lang, case.adult)
}
}
@Test
@DisplayName("익명 홈은 JP만 JA이며 국가 누락·KR·US는 KO 비성인 이벤트를 반환한다")
fun shouldFilterAnonymousHomeWithoutEnablingAdultEvents() {
listOf("JP" to Lang.JA, "KR" to Lang.KO, "US" to Lang.KO, null to Lang.KO).forEach { (country, lang) ->
assertEventIds("home", country, null, lang, false)
}
}
private fun assertEventIds(tab: String, country: String?, token: String?, lang: Lang, adult: Boolean) {
val path = "/v2/audio-content/main/$tab"
val headers = HttpHeaders().apply {
token?.let { setBearerAuth(it) }
country?.let { set("CloudFront-Viewer-Country", it) }
set(HttpHeaders.ACCEPT_LANGUAGE, "en")
}
val response = restTemplate.exchange(path, HttpMethod.GET, HttpEntity<Any>(headers), String::class.java)
println("HTTP EVIDENCE GET $path country=$country lang=$lang adult=$adult ${response.statusCodeValue}: ${response.body}")
assertEquals(HttpStatus.OK, response.statusCode, response.body)
val body = objectMapper.readTree(response.body)
assertEquals(true, body["success"].asBoolean(), response.body)
val events = body["data"]["eventBannerList"]["eventList"]
val expected = listOf(eventIds.getValue(lang to adult), eventIds.getValue(lang to null)).sortedDescending()
assertEquals(expected, events.map { it["id"].asLong() }, "$path country=$country adult=$adult")
events.forEach { event ->
assertEquals(
setOf("id", "thumbnailImageUrl", "detailImageUrl", "popupImageUrl", "link"),
event.fieldNames().asSequence().toSet()
)
assertEquals(true, event["thumbnailImageUrl"].asText().startsWith("https://cdn.test/"))
}
}
private data class Case(
val country: String?,
val memberId: Long,
val storedAdult: Boolean,
val lang: Lang,
val adult: Boolean
)
}