Compare commits

..

3 Commits

Author SHA1 Message Date
ba1844a6c2 Home API에서 api 마다 languageCode를 별도로 받던 것을 LangContext를 사용하도록 리팩토링 2025-12-12 17:22:50 +09:00
082f255773 요청 스코프 언어 컨텍스트와 인터셉터 추가
- Interceptor에서 Accept-Language 헤더를 파싱
- 요청 단위 LangContext에 언어 정보 저장
- 서비스 및 예외 처리 계층에서 언어 컨텍스트 주입
- enum 및 when 기반 언어 정책을 한 곳으로 통합
2025-12-12 16:57:34 +09:00
04281817a5 크리에이터 채널 - languageCode에 따라 콘텐츠 번역 데이터 조회 2025-12-12 13:58:49 +09:00
8 changed files with 200 additions and 240 deletions

View File

@@ -17,7 +17,6 @@ class HomeController(private val service: HomeService) {
@GetMapping @GetMapping
fun fetchData( fun fetchData(
@RequestParam timezone: String, @RequestParam timezone: String,
@RequestParam(required = false) languageCode: String? = null,
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
@@ -25,7 +24,6 @@ class HomeController(private val service: HomeService) {
ApiResponse.ok( ApiResponse.ok(
service.fetchData( service.fetchData(
timezone = timezone, timezone = timezone,
languageCode = languageCode,
isAdultContentVisible = isAdultContentVisible ?: true, isAdultContentVisible = isAdultContentVisible ?: true,
contentType = contentType ?: ContentType.ALL, contentType = contentType ?: ContentType.ALL,
member member
@@ -36,7 +34,6 @@ class HomeController(private val service: HomeService) {
@GetMapping("/latest-content") @GetMapping("/latest-content")
fun getLatestContentByTheme( fun getLatestContentByTheme(
@RequestParam("theme") theme: String, @RequestParam("theme") theme: String,
@RequestParam(required = false) languageCode: String? = null,
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
@@ -44,7 +41,6 @@ class HomeController(private val service: HomeService) {
ApiResponse.ok( ApiResponse.ok(
service.getLatestContentByTheme( service.getLatestContentByTheme(
theme = theme, theme = theme,
languageCode = languageCode,
isAdultContentVisible = isAdultContentVisible ?: true, isAdultContentVisible = isAdultContentVisible ?: true,
contentType = contentType ?: ContentType.ALL, contentType = contentType ?: ContentType.ALL,
member member
@@ -74,15 +70,13 @@ class HomeController(private val service: HomeService) {
fun getRecommendContents( fun getRecommendContents(
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@RequestParam(required = false) languageCode: String? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok( ApiResponse.ok(
service.getRecommendContentList( service.getRecommendContentList(
isAdultContentVisible = isAdultContentVisible ?: true, isAdultContentVisible = isAdultContentVisible ?: true,
contentType = contentType ?: ContentType.ALL, contentType = contentType ?: ContentType.ALL,
member = member, member = member
languageCode = languageCode
) )
) )
} }

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.api.home package kr.co.vividnext.sodalive.api.home
import kr.co.vividnext.sodalive.audition.AuditionService import kr.co.vividnext.sodalive.audition.AuditionService
import kr.co.vividnext.sodalive.chat.character.dto.Character
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
import kr.co.vividnext.sodalive.content.AudioContentMainItem import kr.co.vividnext.sodalive.content.AudioContentMainItem
@@ -16,6 +17,7 @@ import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.event.GetEventResponse import kr.co.vividnext.sodalive.event.GetEventResponse
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.live.room.LiveRoomService import kr.co.vividnext.sodalive.live.room.LiveRoomService
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -52,6 +54,8 @@ class HomeService(
private val contentTranslationRepository: ContentTranslationRepository, private val contentTranslationRepository: ContentTranslationRepository,
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository, private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String private val imageHost: String
) { ) {
@@ -62,7 +66,6 @@ class HomeService(
fun fetchData( fun fetchData(
timezone: String, timezone: String,
languageCode: String?,
isAdultContentVisible: Boolean, isAdultContentVisible: Boolean,
contentType: ContentType, contentType: ContentType,
member: Member? member: Member?
@@ -117,36 +120,7 @@ class HomeService(
} }
} }
/** val translatedLatestContentList = getTranslatedContentList(contentList = latestContentList)
* latestContentList 번역 데이터 조회
*
* languageCode != null
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
*
* 한 번에 조회하고 contentId를 매핑하여 latestContentList의 title을 번역 데이터로 변경한다
*/
val translatedLatestContentList = if (!languageCode.isNullOrBlank()) {
val contentIds = latestContentList.map { it.contentId }
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
latestContentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
} else {
latestContentList
}
} else {
latestContentList
}
val eventBannerList = GetEventResponse( val eventBannerList = GetEventResponse(
totalCount = 0, totalCount = 0,
@@ -175,39 +149,7 @@ class HomeService(
) )
// 인기 캐릭터 조회 // 인기 캐릭터 조회
val popularCharacters = characterService.getPopularCharacters() val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
/**
* popularCharacters 캐릭터 이름 번역 데이터 조회
*
* languageCode != null
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
*
* 한 번에 조회하고 characterId 매핑하여 popularCharacters의 캐릭터 이름을 번역 데이터로 변경한다
*/
val translatedPopularCharacters = if (!languageCode.isNullOrBlank()) {
val characterIds = popularCharacters.map { it.characterId }
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
popularCharacters.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
}
} else {
popularCharacters
}
} else {
popularCharacters
}
val currentDateTime = LocalDateTime.now() val currentDateTime = LocalDateTime.now()
val startDate = currentDateTime val startDate = currentDateTime
@@ -228,32 +170,19 @@ class HomeService(
sort = ContentRankingSortType.REVENUE sort = ContentRankingSortType.REVENUE
) )
/** val contentRankingContentIds = contentRanking.map { it.contentId }
* contentRanking 번역 데이터 조회 val translatedContentRanking = if (contentRankingContentIds.isNotEmpty()) {
* val translations = contentTranslationRepository
* languageCode != null .findByContentIdInAndLocale(contentIds = contentRankingContentIds, locale = langContext.lang.code)
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale .associateBy { it.contentId }
*
* 한 번에 조회하고 contentId를 매핑하여 contentRanking title을 번역 데이터로 변경한다
*/
val translatedContentRanking = if (!languageCode.isNullOrBlank()) {
val contentIds = contentRanking.map { it.contentId }
if (contentIds.isNotEmpty()) { contentRanking.map { item ->
val translations = contentTranslationRepository val translatedTitle = translations[item.contentId]?.renderedPayload?.title
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode) if (translatedTitle.isNullOrBlank()) {
.associateBy { it.contentId } item
} else {
contentRanking.map { item -> item.copy(title = translatedTitle)
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
} }
} else {
contentRanking
} }
} else { } else {
contentRanking contentRanking
@@ -273,31 +202,27 @@ class HomeService(
* *
* 한 번에 조회하고 contentId를 매핑하여 recommendChannelList의 콘텐츠 title을 번역 데이터로 변경한다 * 한 번에 조회하고 contentId를 매핑하여 recommendChannelList의 콘텐츠 title을 번역 데이터로 변경한다
*/ */
val translatedRecommendChannelList = if (!languageCode.isNullOrBlank()) { val channelContentIds = recommendChannelList
val contentIds = recommendChannelList .flatMap { it.contentList }
.flatMap { it.contentList } .map { it.contentId }
.map { it.contentId } .distinct()
.distinct()
if (contentIds.isNotEmpty()) { val translatedRecommendChannelList = if (channelContentIds.isNotEmpty()) {
val translations = contentTranslationRepository val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode) .findByContentIdInAndLocale(contentIds = channelContentIds, locale = langContext.lang.code)
.associateBy { it.contentId } .associateBy { it.contentId }
recommendChannelList.map { channel -> recommendChannelList.map { channel ->
val translatedContentList = channel.contentList.map { item -> val translatedContentList = channel.contentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) { if (translatedTitle.isNullOrBlank()) {
item item
} else { } else {
item.copy(title = translatedTitle) item.copy(title = translatedTitle)
}
} }
channel.copy(contentList = translatedContentList)
} }
} else {
recommendChannelList channel.copy(contentList = translatedContentList)
} }
} else { } else {
recommendChannelList recommendChannelList
@@ -321,36 +246,7 @@ class HomeService(
} }
} }
/** val translatedFreeContentList = getTranslatedContentList(contentList = freeContentList)
* freeContentList 번역 데이터 조회
*
* languageCode != null
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
*
* 한 번에 조회하고 contentId를 매핑하여 freeContentList title을 번역 데이터로 변경한다
*/
val translatedFreeContentList = if (!languageCode.isNullOrBlank()) {
val contentIds = freeContentList.map { it.contentId }
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
freeContentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
} else {
freeContentList
}
} else {
freeContentList
}
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용) // 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
val pointAvailableContentList = contentService.getLatestContentByTheme( val pointAvailableContentList = contentService.getLatestContentByTheme(
@@ -368,36 +264,7 @@ class HomeService(
} }
} }
/** val translatedPointAvailableContentList = getTranslatedContentList(contentList = pointAvailableContentList)
* pointAvailableContentList 번역 데이터 조회
*
* languageCode != null
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
*
* 한 번에 조회하고 contentId를 매핑하여 pointAvailableContentList title을 번역 데이터로 변경한다
*/
val translatedPointAvailableContentList = if (!languageCode.isNullOrBlank()) {
val contentIds = pointAvailableContentList.map { it.contentId }
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
pointAvailableContentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
} else {
pointAvailableContentList
}
} else {
pointAvailableContentList
}
val curationList = curationService.getContentCurationList( val curationList = curationService.getContentCurationList(
tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용 tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용
@@ -424,8 +291,7 @@ class HomeService(
recommendContentList = getRecommendContentList( recommendContentList = getRecommendContentList(
isAdultContentVisible = isAdultContentVisible, isAdultContentVisible = isAdultContentVisible,
contentType = contentType, contentType = contentType,
member = member, member = member
languageCode = languageCode
), ),
curationList = curationList curationList = curationList
) )
@@ -433,7 +299,6 @@ class HomeService(
fun getLatestContentByTheme( fun getLatestContentByTheme(
theme: String, theme: String,
languageCode: String?,
isAdultContentVisible: Boolean, isAdultContentVisible: Boolean,
contentType: ContentType, contentType: ContentType,
member: Member? member: Member?
@@ -464,38 +329,7 @@ class HomeService(
} }
} }
/** return getTranslatedContentList(contentList = contentList)
* contentList 번역 데이터 조회
*
* languageCode != null
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
*
* 한 번에 조회하고 contentId를 매핑하여 contentList title을 번역 데이터로 변경한다
*/
val translatedContentList = if (!languageCode.isNullOrBlank()) {
val contentIds = contentList.map { it.contentId }
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
contentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
} else {
contentList
}
} else {
contentList
}
return translatedContentList
} }
fun getDayOfWeekSeriesList( fun getDayOfWeekSeriesList(
@@ -571,8 +405,7 @@ class HomeService(
fun getRecommendContentList( fun getRecommendContentList(
isAdultContentVisible: Boolean, isAdultContentVisible: Boolean,
contentType: ContentType, contentType: ContentType,
member: Member?, member: Member?
languageCode: String? = null
): List<AudioContentMainItem> { ): List<AudioContentMainItem> {
val memberId = member?.id val memberId = member?.id
val isAdult = member?.auth != null && isAdultContentVisible val isAdult = member?.auth != null && isAdultContentVisible
@@ -607,37 +440,76 @@ class HomeService(
} }
} }
/** return getTranslatedContentList(contentList = result)
* 추천 콘텐츠 번역 데이터 조회 }
*
* languageCode != null
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
*
* 한 번에 조회하고 contentId를 매핑하여 result의 title을 번역 데이터로 변경한다
*/
val translatedResult = if (!languageCode.isNullOrBlank()) {
val contentIds = result.map { it.contentId }
if (contentIds.isNotEmpty()) { /**
val translations = contentTranslationRepository * 콘텐츠 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode) *
.associateBy { it.contentId } * 처리 절차:
* - 입력된 콘텐츠들의 contentId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
* contentTranslationRepository에서 번역 데이터를 한 번에 조회한다.
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
*
* 성능:
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
*
* @param contentList 번역 대상 AudioContentMainItem 목록
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
*/
private fun getTranslatedContentList(contentList: List<AudioContentMainItem>): List<AudioContentMainItem> {
val contentIds = contentList.map { it.contentId }
result.map { item -> return if (contentIds.isNotEmpty()) {
val translatedTitle = translations[item.contentId]?.renderedPayload?.title val translations = contentTranslationRepository
if (translatedTitle.isNullOrBlank()) { .findByContentIdInAndLocale(contentIds = contentIds, locale = langContext.lang.code)
item .associateBy { it.contentId }
} else {
item.copy(title = translatedTitle) contentList.map { item ->
} val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
} }
} else {
result
} }
} else { } else {
result contentList
} }
}
return translatedResult /**
* AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다.
*
* 처리 절차:
* - characterId 목록을 추출하고, 요청 언어 코드로 aiCharacterTranslationRepository에서
* 번역 데이터를 한 번에 조회한다.
* - 각 캐릭터에 대해 name과 description 모두 번역 값이 존재하고 비어있지 않을 때에만
* 해당 필드를 교체한다. 둘 중 하나라도 없으면 원본 캐릭터를 그대로 유지한다.
*
* @param aiCharacterList 번역 대상 캐릭터 목록
* @return 가능한 경우 name/description이 번역된 캐릭터 목록, 그 외는 원본 유지
*/
private fun getTranslatedAiCharacterList(aiCharacterList: List<Character>): List<Character> {
val characterIds = aiCharacterList.map { it.characterId }
return if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = langContext.lang.code)
.associateBy { it.characterId }
aiCharacterList.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
}
} else {
aiCharacterList
}
} }
} }

View File

@@ -1,11 +1,19 @@
package kr.co.vividnext.sodalive.configs package kr.co.vividnext.sodalive.configs
import kr.co.vividnext.sodalive.i18n.LangInterceptor
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration @Configuration
class WebConfig : WebMvcConfigurer { class WebConfig(
private val langInterceptor: LangInterceptor
) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(langInterceptor).addPathPatterns("/**")
}
override fun addCorsMappings(registry: CorsRegistry) { override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOrigins( .allowedOrigins(

View File

@@ -52,6 +52,7 @@ class ExplorerController(private val service: ExplorerService) {
fun getCreatorProfile( fun getCreatorProfile(
@PathVariable("id") creatorId: Long, @PathVariable("id") creatorId: Long,
@RequestParam timezone: String, @RequestParam timezone: String,
@RequestParam("languageCode", required = false) languageCode: String? = null,
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
@@ -60,6 +61,7 @@ class ExplorerController(private val service: ExplorerService) {
service.getCreatorProfile( service.getCreatorProfile(
creatorId = creatorId, creatorId = creatorId,
timezone = timezone, timezone = timezone,
languageCode = languageCode,
isAdultContentVisible = isAdultContentVisible ?: true, isAdultContentVisible = isAdultContentVisible ?: true,
member = member member = member
) )

View File

@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.content.LanguageDetectEvent
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
import kr.co.vividnext.sodalive.content.SortType import kr.co.vividnext.sodalive.content.SortType
import kr.co.vividnext.sodalive.content.series.ContentSeriesService import kr.co.vividnext.sodalive.content.series.ContentSeriesService
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponse import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponse
import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponseItem import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponseItem
import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice
@@ -46,6 +47,7 @@ class ExplorerService(
private val seriesService: ContentSeriesService, private val seriesService: ContentSeriesService,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val contentTranslationRepository: ContentTranslationRepository,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String private val cloudFrontHost: String
@@ -170,6 +172,7 @@ class ExplorerService(
fun getCreatorProfile( fun getCreatorProfile(
creatorId: Long, creatorId: Long,
timezone: String, timezone: String,
languageCode: String?,
isAdultContentVisible: Boolean, isAdultContentVisible: Boolean,
member: Member member: Member
): GetCreatorProfileResponse { ): GetCreatorProfileResponse {
@@ -233,6 +236,29 @@ class ExplorerService(
listOf() listOf()
} }
val translatedContentList = if (!languageCode.isNullOrBlank() && contentList.isNotEmpty()) {
val contentIds = contentList.map { it.contentId }
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
contentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
} else {
contentList
}
} else {
contentList
}
// 크리에이터의 최신 오디오 콘텐츠 1개 // 크리에이터의 최신 오디오 콘텐츠 1개
val latestContent = if (isCreator) { val latestContent = if (isCreator) {
audioContentService.getLatestCreatorAudioContent(creatorId, member, isAdultContentVisible) audioContentService.getLatestCreatorAudioContent(creatorId, member, isAdultContentVisible)
@@ -333,7 +359,7 @@ class ExplorerService(
userDonationRanking = memberDonationRanking, userDonationRanking = memberDonationRanking,
similarCreatorList = similarCreatorList, similarCreatorList = similarCreatorList,
liveRoomList = liveRoomList, liveRoomList = liveRoomList,
contentList = contentList, contentList = translatedContentList,
latestContent = latestContent, latestContent = latestContent,
totalContentCount = totalContentCount, totalContentCount = totalContentCount,
ownedContentCount = ownedContentCount, ownedContentCount = ownedContentCount,

View File

@@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.i18n
import java.util.Locale
enum class Lang(val code: String, val locale: Locale) {
KO("ko", Locale.KOREAN),
EN("en", Locale.ENGLISH),
JA("ja", Locale.JAPANESE);
companion object {
fun fromAcceptLanguage(header: String?): Lang {
if (header.isNullOrBlank()) return KO
val two = header.trim().lowercase().take(2) // 앱은 2자리만 보내지만 안전하게 처리
return when (two) {
"ko" -> KO
"en" -> EN
"ja" -> JA
else -> KO
}
}
}
}

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.i18n
import org.springframework.stereotype.Component
import org.springframework.web.context.annotation.RequestScope
@Component
@RequestScope
class LangContext {
var lang: Lang = Lang.KO
internal set
fun setLang(lang: Lang) {
this.lang = lang
}
}

View File

@@ -0,0 +1,21 @@
package kr.co.vividnext.sodalive.i18n
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class LangInterceptor(
private val langContext: LangContext
) : HandlerInterceptor {
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
val acceptLanguage = request.getHeader("Accept-Language")
langContext.setLang(Lang.fromAcceptLanguage(acceptLanguage))
return true
}
}