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 986a35b..925c10b 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,6 +17,7 @@ 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? @@ -24,6 +25,7 @@ class HomeController(private val service: HomeService) { ApiResponse.ok( service.fetchData( timezone = timezone, + languageCode = languageCode, isAdultContentVisible = isAdultContentVisible ?: true, contentType = contentType ?: ContentType.ALL, member @@ -34,6 +36,7 @@ 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? @@ -41,6 +44,7 @@ class HomeController(private val service: HomeService) { ApiResponse.ok( service.getLatestContentByTheme( theme = theme, + languageCode = languageCode, isAdultContentVisible = isAdultContentVisible ?: true, contentType = contentType ?: ContentType.ALL, member @@ -70,13 +74,15 @@ 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 + member = member, + languageCode = languageCode ) ) } 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 d66d1f8..70ad6cb 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 @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.api.home import kr.co.vividnext.sodalive.audition.AuditionService 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 import kr.co.vividnext.sodalive.content.AudioContentService import kr.co.vividnext.sodalive.content.ContentType @@ -11,6 +12,7 @@ import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationServic import kr.co.vividnext.sodalive.content.series.ContentSeriesService import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService +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 @@ -47,6 +49,9 @@ class HomeService( private val rankingRepository: RankingRepository, private val explorerQueryRepository: ExplorerQueryRepository, + private val contentTranslationRepository: ContentTranslationRepository, + private val aiCharacterTranslationRepository: AiCharacterTranslationRepository, + @Value("\${cloud.aws.cloud-front.host}") private val imageHost: String ) { @@ -57,6 +62,7 @@ class HomeService( fun fetchData( timezone: String, + languageCode: String?, isAdultContentVisible: Boolean, contentType: ContentType, member: Member? @@ -111,6 +117,37 @@ 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 eventBannerList = GetEventResponse( totalCount = 0, eventList = emptyList() @@ -140,6 +177,38 @@ 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 currentDateTime = LocalDateTime.now() val startDate = currentDateTime .withHour(15) @@ -159,12 +228,81 @@ 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 } + + 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) + } + } + } else { + contentRanking + } + } else { + contentRanking + } + val recommendChannelList = recommendChannelService.getRecommendChannel( memberId = memberId, isAdult = isAdult, contentType = contentType ) + /** + * recommendChannelList의 콘텐츠 번역 데이터 조회 + * + * languageCode != null + * contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale + * + * 한 번에 조회하고 contentId를 매핑하여 recommendChannelList의 콘텐츠 title을 번역 데이터로 변경한다 + */ + val translatedRecommendChannelList = if (!languageCode.isNullOrBlank()) { + val contentIds = recommendChannelList + .flatMap { it.contentList } + .map { it.contentId } + .distinct() + + if (contentIds.isNotEmpty()) { + val translations = contentTranslationRepository + .findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode) + .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) + } + } + + channel.copy(contentList = translatedContentList) + } + } else { + recommendChannelList + } + } else { + recommendChannelList + } + val freeContentList = contentService.getLatestContentByTheme( theme = contentThemeService.getActiveThemeOfContent( isAdult = isAdult, @@ -183,6 +321,37 @@ 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 + } + // 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용) val pointAvailableContentList = contentService.getLatestContentByTheme( theme = emptyList(), @@ -199,6 +368,37 @@ 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 curationList = curationService.getContentCurationList( tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용 isAdult = isAdult, @@ -210,21 +410,22 @@ class HomeService( liveList = liveList, creatorRanking = creatorRanking, latestContentThemeList = latestContentThemeList, - latestContentList = latestContentList, + latestContentList = translatedLatestContentList, bannerList = bannerList, eventBannerList = eventBannerList, originalAudioDramaList = originalAudioDramaList, auditionList = auditionList, dayOfWeekSeriesList = dayOfWeekSeriesList, - popularCharacters = popularCharacters, - contentRanking = contentRanking, - recommendChannelList = recommendChannelList, - freeContentList = freeContentList, - pointAvailableContentList = pointAvailableContentList, + popularCharacters = translatedPopularCharacters, + contentRanking = translatedContentRanking, + recommendChannelList = translatedRecommendChannelList, + freeContentList = translatedFreeContentList, + pointAvailableContentList = translatedPointAvailableContentList, recommendContentList = getRecommendContentList( isAdultContentVisible = isAdultContentVisible, contentType = contentType, - member = member + member = member, + languageCode = languageCode ), curationList = curationList ) @@ -232,6 +433,7 @@ class HomeService( fun getLatestContentByTheme( theme: String, + languageCode: String?, isAdultContentVisible: Boolean, contentType: ContentType, member: Member? @@ -249,7 +451,7 @@ class HomeService( listOf(theme) } - return contentService.getLatestContentByTheme( + val contentList = contentService.getLatestContentByTheme( theme = themeList, contentType = contentType, isFree = false, @@ -261,6 +463,39 @@ class HomeService( true } } + + /** + * 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( @@ -336,7 +571,8 @@ class HomeService( fun getRecommendContentList( isAdultContentVisible: Boolean, contentType: ContentType, - member: Member? + member: Member?, + languageCode: String? = null ): List { val memberId = member?.id val isAdult = member?.auth != null && isAdultContentVisible @@ -371,6 +607,37 @@ class HomeService( } } - return 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 + .findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode) + .associateBy { it.contentId } + + result.map { item -> + val translatedTitle = translations[item.contentId]?.renderedPayload?.title + if (translatedTitle.isNullOrBlank()) { + item + } else { + item.copy(title = translatedTitle) + } + } + } else { + result + } + } else { + result + } + + return translatedResult } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/translate/AiCharacterTranslationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/translate/AiCharacterTranslationRepository.kt index 430f253..112998b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/translate/AiCharacterTranslationRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/translate/AiCharacterTranslationRepository.kt @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository interface AiCharacterTranslationRepository : JpaRepository { fun findByCharacterIdAndLocale(characterId: Long, locale: String): AiCharacterTranslation? + + fun findByCharacterIdInAndLocale(characterIds: List, locale: String): List } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/translation/ContentTranslationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/translation/ContentTranslationRepository.kt index e197d1e..9e59a80 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/translation/ContentTranslationRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/translation/ContentTranslationRepository.kt @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository interface ContentTranslationRepository : JpaRepository { fun findByContentIdAndLocale(contentId: Long, locale: String): ContentTranslation? + + fun findByContentIdInAndLocale(contentIds: List, locale: String): List }