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 isAdult = member?.auth != null && isAdultContentVisible
return seriesService.getDayOfWeekSeriesList(
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
memberId = memberId,
isAdult = isAdult,
contentType = contentType,
dayOfWeek = dayOfWeek
)
return getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
}
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.GetSeriesContentListResponse
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.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.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
@@ -39,6 +42,7 @@ class ContentSeriesService(
private val seriesTranslationRepository: SeriesTranslationRepository,
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
private val contentTranslationRepository: ContentTranslationRepository,
private val translationService: PapagoTranslationService,
@Value("\${cloud.aws.cloud-front.host}")
@@ -56,11 +60,77 @@ class ContentSeriesService(
limit: Long = 20
): List<GetSeriesListResponse.SeriesListItem> {
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> {
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(
@@ -126,7 +196,7 @@ class ContentSeriesService(
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
return GetSeriesListResponse(totalCount, items)
return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
}
@Transactional
@@ -232,14 +302,14 @@ class ContentSeriesService(
keywordList
}
val payload = kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload(
val payload = SeriesTranslationPayload(
title = translatedTitle,
introduction = translatedIntroduction,
keywords = translatedKeywords
)
seriesTranslationRepository.save(
kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation(
SeriesTranslation(
seriesId = seriesId,
locale = locale,
renderedPayload = payload
@@ -354,7 +424,33 @@ class ContentSeriesService(
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(
@@ -369,7 +465,13 @@ class ContentSeriesService(
limit = 20
).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(
@@ -384,7 +486,7 @@ class ContentSeriesService(
isAdult = isAdult,
contentType = contentType
)
return seriesToSeriesListItem(seriesList, isAdult, contentType)
return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType))
}
fun getDayOfWeekSeriesList(
@@ -414,7 +516,7 @@ class ContentSeriesService(
seriesList
}
return seriesToSeriesListItem(seriesList, isAdult, contentType)
return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType))
}
private fun seriesToSeriesListItem(

View File

@@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository
interface SeriesGenreTranslationRepository : JpaRepository<SeriesGenreTranslation, Long> {
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.HashTag
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.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -27,6 +30,8 @@ class CreatorAdminContentService(
private val objectMapper: ObjectMapper,
private val s3Uploader: S3Uploader,
private val applicationEventPublisher: ApplicationEventPublisher,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String,
@@ -194,6 +199,13 @@ class CreatorAdminContentService(
}
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.stereotype.Service
import org.springframework.web.client.RestTemplate
import org.springframework.web.client.postForEntity
@Service
class PapagoTranslationService(
@@ -46,10 +47,9 @@ class PapagoTranslationService(
val requestEntity = HttpEntity(body, headers)
val response = restTemplate.postForEntity(
val response = restTemplate.postForEntity<PapagoTranslationResponse>(
papagoTranslateUrl,
requestEntity,
PapagoTranslationResponse::class.java
requestEntity
)
if (!response.statusCode.is2xxSuccessful) {