Compare commits

...

4 Commits

5 changed files with 131 additions and 13 deletions

View File

@@ -348,12 +348,14 @@ class HomeService(
val memberId = member?.id val memberId = member?.id
val isAdult = member?.auth != null && isAdultContentVisible val isAdult = member?.auth != null && isAdultContentVisible
return seriesService.getDayOfWeekSeriesList( val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
memberId = memberId, memberId = memberId,
isAdult = isAdult, isAdult = isAdult,
contentType = contentType, contentType = contentType,
dayOfWeek = dayOfWeek dayOfWeek = dayOfWeek
) )
return getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
} }
fun getContentRankingBySort( fun getContentRankingBySort(

View File

@@ -7,8 +7,11 @@ import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.series.content.ContentSeriesContentRepository import kr.co.vividnext.sodalive.content.series.content.ContentSeriesContentRepository
import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListResponse import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListResponse
import kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslationRepository import kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslationRepository
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
import kr.co.vividnext.sodalive.creator.admin.content.series.Series import kr.co.vividnext.sodalive.creator.admin.content.series.Series
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.creator.admin.content.series.SeriesSortType import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
@@ -39,6 +42,7 @@ class ContentSeriesService(
private val seriesTranslationRepository: SeriesTranslationRepository, private val seriesTranslationRepository: SeriesTranslationRepository,
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository, private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
private val contentTranslationRepository: ContentTranslationRepository,
private val translationService: PapagoTranslationService, private val translationService: PapagoTranslationService,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
@@ -56,11 +60,77 @@ class ContentSeriesService(
limit: Long = 20 limit: Long = 20
): List<GetSeriesListResponse.SeriesListItem> { ): List<GetSeriesListResponse.SeriesListItem> {
val originalAudioDramaList = repository.getOriginalAudioDramaList(isAdult, contentType, orderByRandom, offset, limit) val originalAudioDramaList = repository.getOriginalAudioDramaList(isAdult, contentType, orderByRandom, offset, limit)
return seriesToSeriesListItem(originalAudioDramaList, isAdult, contentType) return getTranslatedSeriesList(seriesToSeriesListItem(originalAudioDramaList, isAdult, contentType))
} }
fun getGenreList(memberId: Long, isAdult: Boolean, contentType: ContentType): List<GetSeriesGenreListResponse> { fun getGenreList(memberId: Long, isAdult: Boolean, contentType: ContentType): List<GetSeriesGenreListResponse> {
return repository.getGenreList(memberId = memberId, isAdult = isAdult, contentType = contentType) /**
* langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환
* 번역이 없으면 번역 API 호출 후 저장하고 반환
*/
val genres = repository.getGenreList(memberId = memberId, isAdult = isAdult, contentType = contentType)
val currentLang = langContext.lang
if (currentLang == Lang.EN || currentLang == Lang.JA) {
val targetLocale = currentLang.code
val ids = genres.map { it.id }
// 기존 번역 일괄 조회
val existing = if (ids.isNotEmpty()) {
// 메서드가 없을 경우 개별 조회로 대체되지만, 성능을 위해 우선 시도
try {
seriesGenreTranslationRepository
.findBySeriesGenreIdInAndLocale(ids, targetLocale)
} catch (_: Exception) {
// Spring Data 메서드 미존재 시 안전하게 개별 조회로 폴백
ids.mapNotNull { id ->
seriesGenreTranslationRepository.findBySeriesGenreIdAndLocale(id, targetLocale)
}
}
} else {
emptyList()
}
val existingMap = existing.associateBy { it.seriesGenreId }.toMutableMap()
// 미번역 항목 수집
val untranslated = genres.filter { existingMap[it.id] == null }
if (untranslated.isNotEmpty()) {
val texts = untranslated.map { it.genre }
val response = translationService.translate(
request = TranslateRequest(
texts = texts,
sourceLanguage = "ko",
targetLanguage = targetLocale
)
)
val translatedTexts = response.translatedText
val toSave = mutableListOf<kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslation>()
untranslated.forEachIndexed { index, item ->
val translated = translatedTexts.getOrNull(index) ?: item.genre
toSave.add(
kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslation(
seriesGenreId = item.id,
locale = targetLocale,
genre = translated
)
)
}
if (toSave.isNotEmpty()) {
seriesGenreTranslationRepository.saveAll(toSave)
toSave.forEach { saved -> existingMap[saved.seriesGenreId] = saved }
}
}
// 원래 순서 보존하여 결과 조립
return genres.map { g ->
val translated = existingMap[g.id]?.genre ?: g.genre
GetSeriesGenreListResponse(id = g.id, genre = translated)
}
}
return genres
} }
fun getSeriesList( fun getSeriesList(
@@ -126,7 +196,7 @@ class ContentSeriesService(
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } ).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType) val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
return GetSeriesListResponse(totalCount, items) return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
} }
@Transactional @Transactional
@@ -232,14 +302,14 @@ class ContentSeriesService(
keywordList keywordList
} }
val payload = kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload( val payload = SeriesTranslationPayload(
title = translatedTitle, title = translatedTitle,
introduction = translatedIntroduction, introduction = translatedIntroduction,
keywords = translatedKeywords keywords = translatedKeywords
) )
seriesTranslationRepository.save( seriesTranslationRepository.save(
kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation( SeriesTranslation(
seriesId = seriesId, seriesId = seriesId,
locale = locale, locale = locale,
renderedPayload = payload renderedPayload = payload
@@ -354,7 +424,33 @@ class ContentSeriesService(
it it
} }
return GetSeriesContentListResponse(totalCount, contentList) /**
* 콘텐츠 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
*
* 처리 절차:
* - 입력된 콘텐츠들의 contentId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
* contentTranslationRepository에서 번역 데이터를 한 번에 조회한다.
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
*
* 성능:
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
*/
val contentIds = contentList.map { it.contentId }
val translatedItems = 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 {
contentList
}
return GetSeriesContentListResponse(totalCount, translatedItems)
} }
fun getRecommendSeriesList( fun getRecommendSeriesList(
@@ -369,7 +465,13 @@ class ContentSeriesService(
limit = 20 limit = 20
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } ).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAuth, contentType = contentType) return getTranslatedSeriesList(
seriesToSeriesListItem(
seriesList = seriesList,
isAdult = isAuth,
contentType = contentType
)
)
} }
fun fetchSeriesByCurationId( fun fetchSeriesByCurationId(
@@ -384,7 +486,7 @@ class ContentSeriesService(
isAdult = isAdult, isAdult = isAdult,
contentType = contentType contentType = contentType
) )
return seriesToSeriesListItem(seriesList, isAdult, contentType) return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType))
} }
fun getDayOfWeekSeriesList( fun getDayOfWeekSeriesList(
@@ -414,7 +516,7 @@ class ContentSeriesService(
seriesList seriesList
} }
return seriesToSeriesListItem(seriesList, isAdult, contentType) return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType))
} }
private fun seriesToSeriesListItem( private fun seriesToSeriesListItem(

View File

@@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository
interface SeriesGenreTranslationRepository : JpaRepository<SeriesGenreTranslation, Long> { interface SeriesGenreTranslationRepository : JpaRepository<SeriesGenreTranslation, Long> {
fun findBySeriesGenreIdAndLocale(seriesGenreId: Long, locale: String): SeriesGenreTranslation? fun findBySeriesGenreIdAndLocale(seriesGenreId: Long, locale: String): SeriesGenreTranslation?
fun findBySeriesGenreIdInAndLocale(seriesGenreIds: List<Long>, locale: String): List<SeriesGenreTranslation>
} }

View File

@@ -10,9 +10,12 @@ import kr.co.vividnext.sodalive.content.ContentPriceChangeLogRepository
import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag
import kr.co.vividnext.sodalive.content.hashtag.HashTag import kr.co.vividnext.sodalive.content.hashtag.HashTag
import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@@ -27,6 +30,8 @@ class CreatorAdminContentService(
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val applicationEventPublisher: ApplicationEventPublisher,
@Value("\${cloud.aws.s3.bucket}") @Value("\${cloud.aws.s3.bucket}")
private val bucket: String, private val bucket: String,
@@ -194,6 +199,13 @@ class CreatorAdminContentService(
} }
audioContent.audioContentHashTags.addAll(newAudioContentHashTagList) audioContent.audioContentHashTags.addAll(newAudioContentHashTagList)
applicationEventPublisher.publishEvent(
LanguageTranslationEvent(
id = request.id,
targetType = LanguageTranslationTargetType.CONTENT
)
)
} }
} }
} }

View File

@@ -6,6 +6,7 @@ import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate import org.springframework.web.client.RestTemplate
import org.springframework.web.client.postForEntity
@Service @Service
class PapagoTranslationService( class PapagoTranslationService(
@@ -46,10 +47,9 @@ class PapagoTranslationService(
val requestEntity = HttpEntity(body, headers) val requestEntity = HttpEntity(body, headers)
val response = restTemplate.postForEntity( val response = restTemplate.postForEntity<PapagoTranslationResponse>(
papagoTranslateUrl, papagoTranslateUrl,
requestEntity, requestEntity
PapagoTranslationResponse::class.java
) )
if (!response.statusCode.is2xxSuccessful) { if (!response.statusCode.is2xxSuccessful) {