Compare commits
4 Commits
4c0be733d0
...
8ae6943c2a
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ae6943c2a | |||
| 82f53ed8ab | |||
| 4e4235369c | |||
| 30a104981c |
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user