Home API에서 api 마다 languageCode를 별도로 받던 것을 LangContext를 사용하도록 리팩토링

This commit is contained in:
2025-12-12 17:22:50 +09:00
parent 082f255773
commit ba1844a6c2
2 changed files with 104 additions and 238 deletions

View File

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

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.api.home
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.translate.AiCharacterTranslationRepository
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.event.GetEventResponse
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.LiveRoomStatus
import kr.co.vividnext.sodalive.member.Member
@@ -52,6 +54,8 @@ class HomeService(
private val contentTranslationRepository: ContentTranslationRepository,
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
@@ -62,7 +66,6 @@ class HomeService(
fun fetchData(
timezone: String,
languageCode: String?,
isAdultContentVisible: Boolean,
contentType: ContentType,
member: Member?
@@ -117,36 +120,7 @@ class HomeService(
}
}
/**
* 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 translatedLatestContentList = getTranslatedContentList(contentList = latestContentList)
val eventBannerList = GetEventResponse(
totalCount = 0,
@@ -175,39 +149,7 @@ class HomeService(
)
// 인기 캐릭터 조회
val popularCharacters = 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 translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
val currentDateTime = LocalDateTime.now()
val startDate = currentDateTime
@@ -228,32 +170,19 @@ class HomeService(
sort = ContentRankingSortType.REVENUE
)
/**
* contentRanking 번역 데이터 조회
*
* languageCode != null
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
*
* 한 번에 조회하고 contentId를 매핑하여 contentRanking title을 번역 데이터로 변경한다
*/
val translatedContentRanking = if (!languageCode.isNullOrBlank()) {
val contentIds = contentRanking.map { it.contentId }
val contentRankingContentIds = contentRanking.map { it.contentId }
val translatedContentRanking = if (contentRankingContentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentRankingContentIds, locale = langContext.lang.code)
.associateBy { it.contentId }
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
contentRanking.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
contentRanking.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
} else {
contentRanking
}
} else {
contentRanking
@@ -273,31 +202,27 @@ class HomeService(
*
* 한 번에 조회하고 contentId를 매핑하여 recommendChannelList의 콘텐츠 title을 번역 데이터로 변경한다
*/
val translatedRecommendChannelList = if (!languageCode.isNullOrBlank()) {
val contentIds = recommendChannelList
.flatMap { it.contentList }
.map { it.contentId }
.distinct()
val channelContentIds = recommendChannelList
.flatMap { it.contentList }
.map { it.contentId }
.distinct()
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
val translatedRecommendChannelList = if (channelContentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = channelContentIds, locale = langContext.lang.code)
.associateBy { it.contentId }
recommendChannelList.map { channel ->
val translatedContentList = channel.contentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
recommendChannelList.map { channel ->
val translatedContentList = channel.contentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
channel.copy(contentList = translatedContentList)
}
} else {
recommendChannelList
channel.copy(contentList = translatedContentList)
}
} else {
recommendChannelList
@@ -321,36 +246,7 @@ class HomeService(
}
}
/**
* 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
}
val translatedFreeContentList = getTranslatedContentList(contentList = freeContentList)
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
val pointAvailableContentList = contentService.getLatestContentByTheme(
@@ -368,36 +264,7 @@ class HomeService(
}
}
/**
* 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 translatedPointAvailableContentList = getTranslatedContentList(contentList = pointAvailableContentList)
val curationList = curationService.getContentCurationList(
tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용
@@ -424,8 +291,7 @@ class HomeService(
recommendContentList = getRecommendContentList(
isAdultContentVisible = isAdultContentVisible,
contentType = contentType,
member = member,
languageCode = languageCode
member = member
),
curationList = curationList
)
@@ -433,7 +299,6 @@ class HomeService(
fun getLatestContentByTheme(
theme: String,
languageCode: String?,
isAdultContentVisible: Boolean,
contentType: ContentType,
member: Member?
@@ -464,38 +329,7 @@ class HomeService(
}
}
/**
* 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
return getTranslatedContentList(contentList = contentList)
}
fun getDayOfWeekSeriesList(
@@ -571,8 +405,7 @@ class HomeService(
fun getRecommendContentList(
isAdultContentVisible: Boolean,
contentType: ContentType,
member: Member?,
languageCode: String? = null
member: Member?
): List<AudioContentMainItem> {
val memberId = member?.id
val isAdult = member?.auth != null && isAdultContentVisible
@@ -607,37 +440,76 @@ class HomeService(
}
}
/**
* 추천 콘텐츠 번역 데이터 조회
*
* languageCode != null
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
*
* 한 번에 조회하고 contentId를 매핑하여 result의 title을 번역 데이터로 변경한다
*/
val translatedResult = if (!languageCode.isNullOrBlank()) {
val contentIds = result.map { it.contentId }
return getTranslatedContentList(contentList = result)
}
if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
.associateBy { it.contentId }
/**
* 콘텐츠 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
*
* 처리 절차:
* - 입력된 콘텐츠들의 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 ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
return if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = langContext.lang.code)
.associateBy { it.contentId }
contentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
} else {
result
}
} 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
}
}
}