Add translation support for audio content detail

This commit is contained in:
2025-12-11 22:00:30 +09:00
parent 1748b26318
commit 608898eb0c
5 changed files with 170 additions and 4 deletions

View File

@@ -131,6 +131,7 @@ class AudioContentController(private val service: AudioContentService) {
fun getDetail(
@PathVariable id: Long,
@RequestParam timezone: String,
@RequestParam(required = false, defaultValue = "ko") languageCode: String? = "ko",
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
@@ -141,7 +142,8 @@ class AudioContentController(private val service: AudioContentService) {
id = id,
member = member,
isAdultContentVisible = isAdultContentVisible ?: true,
timezone = timezone
timezone = timezone,
languageCode = languageCode ?: "ko"
)
)
}

View File

@@ -21,10 +21,14 @@ import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.pin.PinContent
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
import kr.co.vividnext.sodalive.content.translation.TranslatedContent
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.utils.generateFileName
@@ -56,6 +60,9 @@ class AudioContentService(
private val audioContentLikeRepository: AudioContentLikeRepository,
private val pinContentRepository: PinContentRepository,
private val translationService: PapagoTranslationService,
private val contentTranslationRepository: ContentTranslationRepository,
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val audioContentCloudFront: AudioContentCloudFront,
@@ -500,7 +507,8 @@ class AudioContentService(
id: Long,
member: Member,
isAdultContentVisible: Boolean,
timezone: String
timezone: String,
languageCode: String
): GetAudioContentDetailResponse {
val isAdult = member.auth != null && isAdultContentVisible
@@ -718,6 +726,88 @@ class AudioContentService(
listOf()
}
var translated: TranslatedContent? = null
/**
* audioContent.languageCode != languageCode
*
* 번역 콘텐츠를 조회한다. - contentId, locale
* 번역 콘텐츠가 있으면
* TranslatedContent로 가공한다
*
* 번역 콘텐츠가 없으면
* 파파고 API를 통해 번역한 후 저장한다.
*
* 번역 대상: title, detail, tags
*
* 파파고로 번역한 데이터를 TranslatedContent로 가공한다
*/
if (
audioContent.languageCode != null &&
audioContent.languageCode!!.isNotBlank() &&
languageCode.isNotBlank() &&
audioContent.languageCode != languageCode
) {
val locale = languageCode.lowercase()
val existing = contentTranslationRepository
.findByContentIdAndLocale(audioContent.id!!, locale)
if (existing != null) {
val payload = existing.renderedPayload
translated = TranslatedContent(
title = payload.title,
detail = payload.detail,
tags = payload.tags
)
} else {
val texts = mutableListOf<String>()
texts.add(audioContent.title)
texts.add(audioContent.detail)
texts.add(tag)
val sourceLanguage = audioContent.languageCode ?: "ko"
val response = translationService.translate(
request = TranslateRequest(
texts = texts,
sourceLanguage = sourceLanguage,
targetLanguage = locale
)
)
val translatedTexts = response.translatedText
if (translatedTexts.size == texts.size) {
var index = 0
val translatedTitle = translatedTexts[index++]
val translatedDetail = translatedTexts[index++]
val translatedTags = translatedTexts[index]
val payload = kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload(
title = translatedTitle,
detail = translatedDetail,
tags = translatedTags
)
contentTranslationRepository.save(
kr.co.vividnext.sodalive.content.translation.ContentTranslation(
contentId = audioContent.id!!,
locale = locale,
translatedTitle = translatedTitle,
renderedPayload = payload
)
)
translated = TranslatedContent(
title = translatedTitle,
detail = translatedDetail,
tags = translatedTags
)
}
}
}
return GetAudioContentDetailResponse(
contentId = audioContent.id!!,
title = audioContent.title,
@@ -765,7 +855,8 @@ class AudioContentService(
previousContent = previousContent,
nextContent = nextContent,
buyerList = buyerList,
isAvailableUsePoint = audioContent.isPointAvailable
isAvailableUsePoint = audioContent.isPointAvailable,
translated = translated
)
}

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.content
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.content.comment.GetAudioContentCommentListItem
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.translation.TranslatedContent
data class GetAudioContentDetailResponse(
val contentId: Long,
@@ -40,7 +41,8 @@ data class GetAudioContentDetailResponse(
val previousContent: OtherContentResponse?,
val nextContent: OtherContentResponse?,
val buyerList: List<ContentBuyer>,
val isAvailableUsePoint: Boolean
val isAvailableUsePoint: Boolean,
val translated: TranslatedContent?
)
data class OtherContentResponse @QueryProjection constructor(

View File

@@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.content.translation
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.AttributeConverter
import javax.persistence.Column
import javax.persistence.Convert
import javax.persistence.Converter
import javax.persistence.Entity
import javax.persistence.Table
import javax.persistence.UniqueConstraint
@Entity
@Table(
uniqueConstraints = [
UniqueConstraint(columnNames = ["contentId", "locale"])
]
)
class ContentTranslation(
val contentId: Long,
val locale: String,
val translatedTitle: String,
@Column(columnDefinition = "json")
@Convert(converter = ContentTranslationPayloadConverter::class)
val renderedPayload: ContentTranslationPayload
) : BaseEntity()
data class ContentTranslationPayload(
val title: String,
val detail: String,
val tags: String
)
@Converter(autoApply = false)
class ContentTranslationPayloadConverter : AttributeConverter<ContentTranslationPayload, String> {
override fun convertToDatabaseColumn(attribute: ContentTranslationPayload?): String {
if (attribute == null) return "{}"
return objectMapper.writeValueAsString(attribute)
}
override fun convertToEntityAttribute(dbData: String?): ContentTranslationPayload {
if (dbData.isNullOrBlank()) {
return ContentTranslationPayload(
title = "",
detail = "",
tags = ""
)
}
return objectMapper.readValue(dbData)
}
companion object {
private val objectMapper = jacksonObjectMapper()
}
}
data class TranslatedContent(
val title: String?,
val detail: String?,
val tags: String?
)

View File

@@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.content.translation
import org.springframework.data.jpa.repository.JpaRepository
interface ContentTranslationRepository : JpaRepository<ContentTranslation, Long> {
fun findByContentIdAndLocale(contentId: Long, locale: String): ContentTranslation?
}