From ba1844a6c2a4216061a4debfa0365ae5124ddbfe Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 12 Dec 2025 17:22:50 +0900 Subject: [PATCH] =?UTF-8?q?Home=20API=EC=97=90=EC=84=9C=20api=20=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20languageCode=EB=A5=BC=20=EB=B3=84=EB=8F=84=EB=A1=9C?= =?UTF-8?q?=20=EB=B0=9B=EB=8D=98=20=EA=B2=83=EC=9D=84=20LangContext?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/api/home/HomeController.kt | 8 +- .../sodalive/api/home/HomeService.kt | 334 ++++++------------ 2 files changed, 104 insertions(+), 238 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt index 925c10b..986a35b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeController.kt @@ -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 ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt index 70ad6cb..7e2581b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt @@ -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 { 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): List { + 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): List { + 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 + } } }