feat(creator-community): 게시물 번역 서비스를 추가한다
This commit is contained in:
+53
-1
@@ -18,8 +18,13 @@ import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslatio
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityRepository
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.adapter.out.persistence.CreatorCommunityTranslation
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.adapter.out.persistence.CreatorCommunityTranslationRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import javax.persistence.EntityManager
|
||||
import javax.persistence.LockModeType
|
||||
|
||||
@Service
|
||||
class TranslationReadModelMaterializer(
|
||||
@@ -31,7 +36,10 @@ class TranslationReadModelMaterializer(
|
||||
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
|
||||
private val originalWorkTranslationRepository: OriginalWorkTranslationRepository,
|
||||
private val categoryTranslationRepository: CategoryTranslationRepository
|
||||
private val categoryTranslationRepository: CategoryTranslationRepository,
|
||||
private val creatorCommunityRepository: CreatorCommunityRepository,
|
||||
private val creatorCommunityTranslationRepository: CreatorCommunityTranslationRepository,
|
||||
private val entityManager: EntityManager
|
||||
) {
|
||||
@Transactional
|
||||
fun materialize(resourceType: LanguageTranslationTargetType, resourceId: Long, targetLanguage: String): Boolean {
|
||||
@@ -46,6 +54,9 @@ class TranslationReadModelMaterializer(
|
||||
LanguageTranslationTargetType.SERIES_GENRE -> upsertSeriesGenre(resourceId, targetLanguage, translations)
|
||||
LanguageTranslationTargetType.ORIGINAL_WORK -> upsertOriginalWork(resourceId, targetLanguage, translations)
|
||||
LanguageTranslationTargetType.CREATOR_CONTENT_CATEGORY -> upsertCategory(resourceId, targetLanguage, translations)
|
||||
LanguageTranslationTargetType.CREATOR_COMMUNITY -> {
|
||||
return upsertCreatorCommunity(source, targetLanguage.lowercase(), translations)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -180,6 +191,47 @@ class TranslationReadModelMaterializer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun upsertCreatorCommunity(
|
||||
source: TranslationSource,
|
||||
targetLanguage: String,
|
||||
translations: Map<String, String>
|
||||
): Boolean {
|
||||
val sourceRevision = source.sourceRevision ?: return false
|
||||
val sourceHash = SourceTextNormalizer.hash(source.fields.single().sourceText)
|
||||
val creatorCommunity = creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(source.resourceId) ?: return false
|
||||
entityManager.refresh(creatorCommunity, LockModeType.PESSIMISTIC_WRITE)
|
||||
if (
|
||||
!creatorCommunity.isActive ||
|
||||
creatorCommunity.contentRevision != sourceRevision ||
|
||||
creatorCommunity.languageCode?.lowercase() != source.sourceLanguage.lowercase() ||
|
||||
SourceTextNormalizer.hash(creatorCommunity.content) != sourceHash
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
val existing = creatorCommunityTranslationRepository
|
||||
.findByCreatorCommunityIdAndLocaleForUpdate(source.resourceId, targetLanguage)
|
||||
if (existing == null) {
|
||||
creatorCommunityTranslationRepository.save(
|
||||
CreatorCommunityTranslation(
|
||||
creatorCommunityId = source.resourceId,
|
||||
locale = targetLanguage,
|
||||
content = translations["content"].orEmpty(),
|
||||
sourceRevision = sourceRevision,
|
||||
sourceHash = sourceHash,
|
||||
sourceLanguage = source.sourceLanguage.lowercase()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
existing.content = translations["content"].orEmpty()
|
||||
existing.sourceRevision = sourceRevision
|
||||
existing.sourceHash = sourceHash
|
||||
existing.sourceLanguage = source.sourceLanguage.lowercase()
|
||||
creatorCommunityTranslationRepository.save(existing)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PROVIDER = "papago"
|
||||
}
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application
|
||||
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityRepository
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService.Companion.getTranslatableLanguageCodes
|
||||
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationReadModelMaterializer
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationSourceExtractor
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.adapter.out.persistence.CreatorCommunityTranslation
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.adapter.out.persistence.CreatorCommunityTranslationRepository
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Propagation
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import javax.persistence.EntityManager
|
||||
import javax.persistence.LockModeType
|
||||
|
||||
@Service
|
||||
class CreatorCommunityTranslationService(
|
||||
private val creatorCommunityRepository: CreatorCommunityRepository,
|
||||
private val creatorCommunityTranslationRepository: CreatorCommunityTranslationRepository,
|
||||
private val sourceExtractor: TranslationSourceExtractor,
|
||||
private val materializer: TranslationReadModelMaterializer,
|
||||
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler,
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
private val entityManager: EntityManager
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun findDisplayContents(postIds: List<Long>, locale: String): Map<Long, CreatorCommunityDisplayContent> {
|
||||
if (postIds.isEmpty()) return emptyMap()
|
||||
|
||||
val normalizedLocale = locale.lowercase()
|
||||
val posts = creatorCommunityRepository.findAllById(postIds).filter { it.isActive }
|
||||
val translations = creatorCommunityTranslationRepository
|
||||
.findByCreatorCommunityIdInAndLocale(posts.mapNotNull { it.id }, normalizedLocale)
|
||||
.associateBy { it.creatorCommunityId }
|
||||
|
||||
return posts.associate { post ->
|
||||
post.id!! to translations[post.id].displayContentOr(post, normalizedLocale)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
fun requestTranslations(postId: Long, targetLanguage: String? = null) {
|
||||
val creatorCommunity = creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(postId) ?: return
|
||||
entityManager.refresh(creatorCommunity, LockModeType.PESSIMISTIC_WRITE)
|
||||
|
||||
val source = sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId)
|
||||
if (source == null) {
|
||||
requestLanguageDetection(postId, targetLanguage)
|
||||
return
|
||||
}
|
||||
|
||||
val sourceLanguage = source.sourceLanguage.lowercase()
|
||||
val targetLanguages = targetLanguage?.let { setOf(it.lowercase()) }
|
||||
?: getTranslatableLanguageCodes(sourceLanguage)
|
||||
|
||||
targetLanguages
|
||||
.filter { it != sourceLanguage }
|
||||
.forEach { language ->
|
||||
if (!materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId, language)) {
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||
resourceId = postId,
|
||||
targetLanguage = language
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestLanguageDetection(postId: Long, targetLanguage: String?) {
|
||||
val creatorCommunity = creatorCommunityRepository.findByIdAndIsActiveTrue(postId) ?: return
|
||||
if (!creatorCommunity.languageCode.isNullOrBlank() || creatorCommunity.content.isBlank()) return
|
||||
|
||||
applicationEventPublisher.publishEvent(
|
||||
LanguageDetectEvent(
|
||||
id = postId,
|
||||
query = creatorCommunity.content,
|
||||
targetType = LanguageDetectTargetType.CREATOR_COMMUNITY,
|
||||
sourceRevision = creatorCommunity.contentRevision,
|
||||
targetLanguage = targetLanguage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorCommunityTranslation?.displayContentOr(
|
||||
creatorCommunity: CreatorCommunity,
|
||||
locale: String
|
||||
): CreatorCommunityDisplayContent {
|
||||
val sourceLanguage = creatorCommunity.languageCode?.lowercase()
|
||||
return if (
|
||||
this != null &&
|
||||
sourceLanguage != null &&
|
||||
sourceLanguage != locale &&
|
||||
sourceRevision == creatorCommunity.contentRevision &&
|
||||
sourceLanguage == this.sourceLanguage.lowercase() &&
|
||||
sourceHash == SourceTextNormalizer.hash(creatorCommunity.content)
|
||||
) {
|
||||
CreatorCommunityDisplayContent(content = content, hasValidTranslation = true)
|
||||
} else {
|
||||
CreatorCommunityDisplayContent(content = creatorCommunity.content, hasValidTranslation = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CreatorCommunityDisplayContent(
|
||||
val content: String,
|
||||
val hasValidTranslation: Boolean
|
||||
)
|
||||
Reference in New Issue
Block a user