From 255da2b6aa56cdf4164eb2620c000f8df34ba188 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 3 Nov 2026 00:29:21 +0900 Subject: [PATCH] =?UTF-8?q?fix(event):=20=EB=A9=94=EC=9D=B8=20=ED=83=AD=20?= =?UTF-8?q?=EB=B0=B0=EB=84=88=EC=97=90=20=EA=B5=AD=EA=B0=80=EB=B3=84=20?= =?UTF-8?q?=EC=96=B8=EC=96=B4=20=ED=95=84=ED=84=B0=EB=A5=BC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alarm/AudioContentMainTabAlarmService.kt | 6 +- .../asmr/AudioContentMainTabAsmrService.kt | 6 +- .../AudioContentMainTabContentService.kt | 6 +- .../home/AudioContentMainTabHomeService.kt | 6 +- .../AudioContentMainTabLiveReplayService.kt | 6 +- .../AudioContentMainTabSeriesService.kt | 6 +- .../EventBannerLanguageIntegrationTest.kt | 171 ++++++++++++++++++ 7 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/event/EventBannerLanguageIntegrationTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt index b803e7a3..d7bffa1e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt @@ -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( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt index 14fd3530..fc4d9bde 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt @@ -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 { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt index 8b0e8273..fb500dd8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt @@ -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!!, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt index 90d37fbd..f8e2d6ef 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt @@ -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, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt index adaf393f..59fc2d8c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt @@ -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 { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt index fd90c425..e00f9ffd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt @@ -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) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/event/EventBannerLanguageIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/event/EventBannerLanguageIntegrationTest.kt new file mode 100644 index 00000000..eae032df --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/event/EventBannerLanguageIntegrationTest.kt @@ -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, Long>() + private val tokens = mutableMapOf() + + @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(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 + ) +}