Add translation support for audio content detail
This commit is contained in:
@@ -131,6 +131,7 @@ class AudioContentController(private val service: AudioContentService) {
|
|||||||
fun getDetail(
|
fun getDetail(
|
||||||
@PathVariable id: Long,
|
@PathVariable id: Long,
|
||||||
@RequestParam timezone: String,
|
@RequestParam timezone: String,
|
||||||
|
@RequestParam(required = false, defaultValue = "ko") languageCode: String? = "ko",
|
||||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = run {
|
) = run {
|
||||||
@@ -141,7 +142,8 @@ class AudioContentController(private val service: AudioContentService) {
|
|||||||
id = id,
|
id = id,
|
||||||
member = member,
|
member = member,
|
||||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||||
timezone = timezone
|
timezone = timezone,
|
||||||
|
languageCode = languageCode ?: "ko"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.PinContent
|
||||||
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
|
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
|
||||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
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.explorer.ExplorerQueryRepository
|
||||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||||
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
||||||
import kr.co.vividnext.sodalive.fcm.FcmEventType
|
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.Member
|
||||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||||
@@ -56,6 +60,9 @@ class AudioContentService(
|
|||||||
private val audioContentLikeRepository: AudioContentLikeRepository,
|
private val audioContentLikeRepository: AudioContentLikeRepository,
|
||||||
private val pinContentRepository: PinContentRepository,
|
private val pinContentRepository: PinContentRepository,
|
||||||
|
|
||||||
|
private val translationService: PapagoTranslationService,
|
||||||
|
private val contentTranslationRepository: ContentTranslationRepository,
|
||||||
|
|
||||||
private val s3Uploader: S3Uploader,
|
private val s3Uploader: S3Uploader,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val audioContentCloudFront: AudioContentCloudFront,
|
private val audioContentCloudFront: AudioContentCloudFront,
|
||||||
@@ -500,7 +507,8 @@ class AudioContentService(
|
|||||||
id: Long,
|
id: Long,
|
||||||
member: Member,
|
member: Member,
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
timezone: String
|
timezone: String,
|
||||||
|
languageCode: String
|
||||||
): GetAudioContentDetailResponse {
|
): GetAudioContentDetailResponse {
|
||||||
val isAdult = member.auth != null && isAdultContentVisible
|
val isAdult = member.auth != null && isAdultContentVisible
|
||||||
|
|
||||||
@@ -718,6 +726,88 @@ class AudioContentService(
|
|||||||
listOf()
|
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(
|
return GetAudioContentDetailResponse(
|
||||||
contentId = audioContent.id!!,
|
contentId = audioContent.id!!,
|
||||||
title = audioContent.title,
|
title = audioContent.title,
|
||||||
@@ -765,7 +855,8 @@ class AudioContentService(
|
|||||||
previousContent = previousContent,
|
previousContent = previousContent,
|
||||||
nextContent = nextContent,
|
nextContent = nextContent,
|
||||||
buyerList = buyerList,
|
buyerList = buyerList,
|
||||||
isAvailableUsePoint = audioContent.isPointAvailable
|
isAvailableUsePoint = audioContent.isPointAvailable,
|
||||||
|
translated = translated
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.content
|
|||||||
import com.querydsl.core.annotations.QueryProjection
|
import com.querydsl.core.annotations.QueryProjection
|
||||||
import kr.co.vividnext.sodalive.content.comment.GetAudioContentCommentListItem
|
import kr.co.vividnext.sodalive.content.comment.GetAudioContentCommentListItem
|
||||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.TranslatedContent
|
||||||
|
|
||||||
data class GetAudioContentDetailResponse(
|
data class GetAudioContentDetailResponse(
|
||||||
val contentId: Long,
|
val contentId: Long,
|
||||||
@@ -40,7 +41,8 @@ data class GetAudioContentDetailResponse(
|
|||||||
val previousContent: OtherContentResponse?,
|
val previousContent: OtherContentResponse?,
|
||||||
val nextContent: OtherContentResponse?,
|
val nextContent: OtherContentResponse?,
|
||||||
val buyerList: List<ContentBuyer>,
|
val buyerList: List<ContentBuyer>,
|
||||||
val isAvailableUsePoint: Boolean
|
val isAvailableUsePoint: Boolean,
|
||||||
|
val translated: TranslatedContent?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OtherContentResponse @QueryProjection constructor(
|
data class OtherContentResponse @QueryProjection constructor(
|
||||||
|
|||||||
@@ -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?
|
||||||
|
)
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user