feat(creator-community): 게시물 번역 서비스를 추가한다
This commit is contained in:
+878
@@ -0,0 +1,878 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.translation
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository
|
||||
import kr.co.vividnext.sodalive.admin.content.series.genre.AdminContentSeriesGenreRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.OriginalWorkTranslationRepository
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
||||
import kr.co.vividnext.sodalive.content.category.CategoryRepository
|
||||
import kr.co.vividnext.sodalive.content.category.CategoryTranslationRepository
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslationRepository
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
||||
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||
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.ResourceTranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationJob
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobRepository
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationMemory
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationMemoryRepository
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationReadModelMaterializer
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationSource
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationSourceExtractor
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationSourceField
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
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 kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.transaction.PlatformTransactionManager
|
||||
import org.springframework.transaction.TransactionDefinition
|
||||
import org.springframework.transaction.annotation.Propagation
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(
|
||||
properties = [
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:creator-community-translation;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import(QueryDslConfig::class)
|
||||
class CreatorCommunityTranslationServiceTest @Autowired constructor(
|
||||
private val persistentCreatorCommunityRepository: CreatorCommunityRepository,
|
||||
private val persistentMemberRepository: MemberRepository,
|
||||
private val persistentTranslationRepository: CreatorCommunityTranslationRepository,
|
||||
private val persistentMemoryRepository: TranslationMemoryRepository,
|
||||
private val persistentTranslationJobRepository: TranslationJobRepository,
|
||||
private val entityManager: EntityManager,
|
||||
private val transactionManager: PlatformTransactionManager
|
||||
) {
|
||||
private val transactionTemplate = TransactionTemplate(transactionManager)
|
||||
private val requiresNewTransactionTemplate = TransactionTemplate(transactionManager).apply {
|
||||
propagationBehavior = Propagation.REQUIRES_NEW.value()
|
||||
}
|
||||
private val repeatableReadTransactionTemplate = TransactionTemplate(transactionManager).apply {
|
||||
isolationLevel = TransactionDefinition.ISOLATION_REPEATABLE_READ
|
||||
}
|
||||
private val applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||
|
||||
@Test
|
||||
fun shouldRequireCreatorCommunityTranslationPersistenceContract() {
|
||||
val fieldNames = CreatorCommunity::class.java.declaredFields.map { it.name }
|
||||
|
||||
assertTrue(fieldNames.contains("languageCode"))
|
||||
assertTrue(fieldNames.contains("contentRevision"))
|
||||
assertTrue(LanguageTranslationTargetType.values().any { it.name == "CREATOR_COMMUNITY" })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("표시 조회는 현재 개정과 원문 언어가 일치하는 번역만 일괄 조회해 반환한다")
|
||||
fun shouldReturnOnlyCurrentTranslationFromBatchedRead() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||
val scheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||
val service = CreatorCommunityTranslationService(
|
||||
creatorCommunityRepository,
|
||||
translationRepository,
|
||||
sourceExtractor,
|
||||
materializer,
|
||||
scheduler,
|
||||
applicationEventPublisher,
|
||||
Mockito.mock(EntityManager::class.java)
|
||||
)
|
||||
val current = creatorCommunity(postId = 1L, content = "current", languageCode = "ko", contentRevision = 2)
|
||||
val staleRevision = creatorCommunity(postId = 2L, content = "changed", languageCode = "ko", contentRevision = 3)
|
||||
val sameLanguage = creatorCommunity(postId = 3L, content = "original", languageCode = "en", contentRevision = 2)
|
||||
val inactive = creatorCommunity(postId = 4L, content = "hidden", languageCode = "ko", contentRevision = 2).apply {
|
||||
isActive = false
|
||||
}
|
||||
val currentTranslation = translation(postId = 1L, content = "translated", sourceRevision = 2, sourceText = "current")
|
||||
val staleTranslation = translation(postId = 2L, content = "old translated", sourceRevision = 2, sourceText = "changed")
|
||||
val sameLanguageTranslation = translation(
|
||||
postId = 3L,
|
||||
content = "should not show",
|
||||
sourceRevision = 2,
|
||||
sourceText = "original",
|
||||
sourceLanguage = "en"
|
||||
)
|
||||
|
||||
Mockito.`when`(creatorCommunityRepository.findAllById(listOf(1L, 2L, 3L, 4L)))
|
||||
.thenReturn(listOf(current, staleRevision, sameLanguage, inactive))
|
||||
Mockito.`when`(translationRepository.findByCreatorCommunityIdInAndLocale(listOf(1L, 2L, 3L), "en"))
|
||||
.thenReturn(listOf(currentTranslation, staleTranslation, sameLanguageTranslation))
|
||||
|
||||
val contents = service.findDisplayContents(listOf(1L, 2L, 3L, 4L), "EN")
|
||||
|
||||
assertEquals("translated", contents[1L]?.content)
|
||||
assertTrue(contents[1L]?.hasValidTranslation == true)
|
||||
assertEquals("changed", contents[2L]?.content)
|
||||
assertTrue(contents[2L]?.hasValidTranslation == false)
|
||||
assertEquals("original", contents[3L]?.content)
|
||||
assertTrue(contents[3L]?.hasValidTranslation == false)
|
||||
assertTrue(4L !in contents)
|
||||
Mockito.verifyNoInteractions(sourceExtractor, materializer, scheduler)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("번역이 없거나 원문 언어와 해시가 다르면 현재 원문을 반환한다")
|
||||
fun shouldFallbackToOriginalWhenTranslationIsMissingOrDoesNotMatchCurrentSource() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||
val scheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||
val service = CreatorCommunityTranslationService(
|
||||
creatorCommunityRepository,
|
||||
translationRepository,
|
||||
sourceExtractor,
|
||||
materializer,
|
||||
scheduler,
|
||||
applicationEventPublisher,
|
||||
Mockito.mock(EntityManager::class.java)
|
||||
)
|
||||
val missing = creatorCommunity(1L, "missing", "ko", 1)
|
||||
val sourceLanguageMismatch = creatorCommunity(2L, "language", "ko", 1)
|
||||
val sourceHashMismatch = creatorCommunity(3L, "current", "ko", 1)
|
||||
val languageMismatchTranslation = translation(
|
||||
postId = 2L,
|
||||
content = "wrong language",
|
||||
sourceRevision = 1,
|
||||
sourceText = "language",
|
||||
sourceLanguage = "ja"
|
||||
)
|
||||
val hashMismatchTranslation = translation(
|
||||
postId = 3L,
|
||||
content = "wrong hash",
|
||||
sourceRevision = 1,
|
||||
sourceText = "previous"
|
||||
)
|
||||
|
||||
Mockito.`when`(creatorCommunityRepository.findAllById(listOf(1L, 2L, 3L)))
|
||||
.thenReturn(listOf(missing, sourceLanguageMismatch, sourceHashMismatch))
|
||||
Mockito.`when`(translationRepository.findByCreatorCommunityIdInAndLocale(listOf(1L, 2L, 3L), "en"))
|
||||
.thenReturn(listOf(languageMismatchTranslation, hashMismatchTranslation))
|
||||
|
||||
val contents = service.findDisplayContents(listOf(1L, 2L, 3L), "en")
|
||||
|
||||
assertEquals("missing", contents[1L]?.content)
|
||||
assertTrue(contents[1L]?.hasValidTranslation == false)
|
||||
assertEquals("language", contents[2L]?.content)
|
||||
assertTrue(contents[2L]?.hasValidTranslation == false)
|
||||
assertEquals("current", contents[3L]?.content)
|
||||
assertTrue(contents[3L]?.hasValidTranslation == false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("현재 원문의 메모리를 재료화할 수 있으면 작업을 새로 예약하지 않는다")
|
||||
fun shouldRematerializeFromMemoryBeforeScheduling() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||
val scheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||
val service = CreatorCommunityTranslationService(
|
||||
creatorCommunityRepository,
|
||||
translationRepository,
|
||||
sourceExtractor,
|
||||
materializer,
|
||||
scheduler,
|
||||
applicationEventPublisher,
|
||||
Mockito.mock(EntityManager::class.java)
|
||||
)
|
||||
val source = TranslationSource(
|
||||
resourceType = LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||
resourceId = 1L,
|
||||
sourceLanguage = "ko",
|
||||
fields = listOf(TranslationSourceField("content", "A")),
|
||||
sourceRevision = 2
|
||||
)
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L))
|
||||
.thenReturn(creatorCommunity(1L, "A", "ko", 2))
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(source)
|
||||
Mockito.`when`(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L, "en"))
|
||||
.thenReturn(true)
|
||||
|
||||
service.requestTranslations(1L, "EN")
|
||||
|
||||
Mockito.verify(materializer).materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L, "en")
|
||||
Mockito.verifyNoInteractions(scheduler)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("현재 원문의 메모리가 없으면 기존 작업 예약기를 사용한다")
|
||||
fun shouldScheduleMissingTranslationAfterMemoryCannotMaterialize() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||
val scheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||
val service = CreatorCommunityTranslationService(
|
||||
creatorCommunityRepository,
|
||||
translationRepository,
|
||||
sourceExtractor,
|
||||
materializer,
|
||||
scheduler,
|
||||
applicationEventPublisher,
|
||||
Mockito.mock(EntityManager::class.java)
|
||||
)
|
||||
val source = TranslationSource(
|
||||
resourceType = LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||
resourceId = 1L,
|
||||
sourceLanguage = "ko",
|
||||
fields = listOf(TranslationSourceField("content", "B")),
|
||||
sourceRevision = 3
|
||||
)
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L))
|
||||
.thenReturn(creatorCommunity(1L, "B", "ko", 3))
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(source)
|
||||
Mockito.`when`(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L, "en"))
|
||||
.thenReturn(false)
|
||||
|
||||
service.requestTranslations(1L, "en")
|
||||
|
||||
Mockito.verify(scheduler).scheduleResourceTranslation(
|
||||
LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||
1L,
|
||||
"en"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("상세 요청 언어가 원문 언어와 같으면 번역 작업을 예약하지 않는다")
|
||||
fun shouldNotScheduleTranslationWhenDetailTargetLanguageEqualsSourceLanguage() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||
val scheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||
val service = CreatorCommunityTranslationService(
|
||||
creatorCommunityRepository,
|
||||
translationRepository,
|
||||
sourceExtractor,
|
||||
materializer,
|
||||
scheduler,
|
||||
applicationEventPublisher,
|
||||
Mockito.mock(EntityManager::class.java)
|
||||
)
|
||||
val post = creatorCommunity(postId = 1L, content = "original", languageCode = "ja", contentRevision = 0)
|
||||
val source = translationSource(content = "original", sourceLanguage = "ja", sourceRevision = 0)
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L)).thenReturn(post)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(source)
|
||||
|
||||
service.requestTranslations(1L, "ja")
|
||||
|
||||
Mockito.verifyNoInteractions(materializer, scheduler)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("언어가 없는 현재 커뮤니티 본문은 감지 이벤트에 개정과 요청 언어를 보존한다")
|
||||
fun shouldPublishCurrentContentDetectionEventWhenSourceLanguageIsUnknown() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||
val scheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||
val service = CreatorCommunityTranslationService(
|
||||
creatorCommunityRepository,
|
||||
translationRepository,
|
||||
sourceExtractor,
|
||||
materializer,
|
||||
scheduler,
|
||||
applicationEventPublisher,
|
||||
Mockito.mock(EntityManager::class.java)
|
||||
)
|
||||
val creatorCommunity = CreatorCommunity(
|
||||
content = "current",
|
||||
price = 0,
|
||||
isCommentAvailable = true,
|
||||
isAdult = false,
|
||||
contentRevision = 2
|
||||
).apply {
|
||||
id = 1L
|
||||
}
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L)).thenReturn(creatorCommunity)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(null)
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrue(1L)).thenReturn(creatorCommunity)
|
||||
|
||||
service.requestTranslations(1L, "ja")
|
||||
|
||||
val eventCaptor = org.mockito.ArgumentCaptor.forClass(LanguageDetectEvent::class.java)
|
||||
Mockito.verify(applicationEventPublisher).publishEvent(eventCaptor.capture())
|
||||
assertEquals(1L, eventCaptor.value.id)
|
||||
assertEquals("current", eventCaptor.value.query)
|
||||
assertEquals(LanguageDetectTargetType.CREATOR_COMMUNITY, eventCaptor.value.targetType)
|
||||
assertEquals(2L, eventCaptor.value.sourceRevision)
|
||||
assertEquals("ja", eventCaptor.value.targetLanguage)
|
||||
Mockito.verifyNoInteractions(materializer, scheduler)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이전 본문으로 돌아오면 번역 메모리에서 현재 개정의 조회 모델을 복원한다")
|
||||
fun shouldRestoreReturnedContentFromTranslationMemoryAtCurrentRevision() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
|
||||
val materializer = materializer(
|
||||
sourceExtractor,
|
||||
memoryRepository,
|
||||
creatorCommunityRepository,
|
||||
translationRepository
|
||||
)
|
||||
val source = translationSource(content = "A", sourceLanguage = "ko", sourceRevision = 2)
|
||||
val currentPost = creatorCommunity(postId = 1L, content = "A", languageCode = "ko", contentRevision = 2)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(source)
|
||||
givenTranslationMemory(memoryRepository, "A", "ko")
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L)).thenReturn(currentPost)
|
||||
Mockito.`when`(translationRepository.findByCreatorCommunityIdAndLocale(1L, "en")).thenReturn(null)
|
||||
|
||||
assertTrue(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L, "en"))
|
||||
|
||||
val translationCaptor = org.mockito.ArgumentCaptor.forClass(CreatorCommunityTranslation::class.java)
|
||||
Mockito.verify(translationRepository).save(translationCaptor.capture())
|
||||
assertEquals("translated A", translationCaptor.value.content)
|
||||
assertEquals(2L, translationCaptor.value.sourceRevision)
|
||||
assertEquals(SourceTextNormalizer.hash("A"), translationCaptor.value.sourceHash)
|
||||
assertEquals("ko", translationCaptor.value.sourceLanguage)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("원문 추출 뒤 본문 개정이 바뀌면 번역 결과를 저장하지 않는다")
|
||||
fun shouldNotStoreTranslationWhenRevisionChangesBeforeLockedWrite() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
|
||||
val materializer = materializer(
|
||||
sourceExtractor,
|
||||
memoryRepository,
|
||||
creatorCommunityRepository,
|
||||
translationRepository
|
||||
)
|
||||
val source = translationSource(content = "before", sourceLanguage = "ko", sourceRevision = 0)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(source)
|
||||
givenTranslationMemory(memoryRepository, "before", "ko")
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L))
|
||||
.thenReturn(creatorCommunity(1L, "after", "ko", 1))
|
||||
|
||||
assertFalse(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L, "en"))
|
||||
|
||||
Mockito.verifyNoInteractions(translationRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("원문 언어가 바뀌면 번역 결과를 저장하지 않는다")
|
||||
fun shouldNotStoreTranslationWhenSourceLanguageChangesBeforeLockedWrite() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
|
||||
val materializer = materializer(
|
||||
sourceExtractor,
|
||||
memoryRepository,
|
||||
creatorCommunityRepository,
|
||||
translationRepository
|
||||
)
|
||||
val source = translationSource(content = "same", sourceLanguage = "ko", sourceRevision = 0)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(source)
|
||||
givenTranslationMemory(memoryRepository, "same", "ko")
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L))
|
||||
.thenReturn(creatorCommunity(1L, "same", "en", 0))
|
||||
|
||||
assertFalse(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L, "en"))
|
||||
|
||||
Mockito.verifyNoInteractions(translationRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("원문 게시물이 비활성화되면 번역 결과를 저장하지 않는다")
|
||||
fun shouldNotStoreTranslationWhenPostBecomesInactiveBeforeLockedWrite() {
|
||||
val creatorCommunityRepository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
val translationRepository = Mockito.mock(CreatorCommunityTranslationRepository::class.java)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
|
||||
val materializer = materializer(
|
||||
sourceExtractor,
|
||||
memoryRepository,
|
||||
creatorCommunityRepository,
|
||||
translationRepository
|
||||
)
|
||||
val source = translationSource(content = "before", sourceLanguage = "ko", sourceRevision = 0)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L)).thenReturn(source)
|
||||
givenTranslationMemory(memoryRepository, "before", "ko")
|
||||
Mockito.`when`(creatorCommunityRepository.findByIdAndIsActiveTrueForUpdate(1L)).thenReturn(null)
|
||||
|
||||
assertFalse(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, 1L, "en"))
|
||||
|
||||
Mockito.verifyNoInteractions(translationRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DisplayName("이미 관리 중인 게시물이 다른 트랜잭션에서 수정되면 이전 번역을 저장하지 않는다")
|
||||
fun shouldRejectStaleManagedCreatorCommunityAfterConcurrentCommit() {
|
||||
val postId = persistCreatorCommunity("stale-A", 0)
|
||||
persistTranslationMemory("stale-A")
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = persistentMaterializer(sourceExtractor)
|
||||
val source = translationSource(postId, "stale-A", "ko", 0)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId)).thenReturn(source)
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
persistentCreatorCommunityRepository.findById(postId).orElseThrow()
|
||||
updateCreatorCommunity(postId, "stale-B", 1)
|
||||
|
||||
assertFalse(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId, "en"))
|
||||
}
|
||||
|
||||
val translation = requiresNewTransactionTemplate.execute {
|
||||
persistentTranslationRepository.findByCreatorCommunityIdAndLocale(postId, "en")
|
||||
}
|
||||
assertEquals(null, translation)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DisplayName("본문 A에서 B를 거쳐 A로 돌아오면 기존 번역 행을 현재 개정으로 갱신한다")
|
||||
fun shouldRematerializeAtoBtoAIntoExistingTranslationRow() {
|
||||
val postId = persistCreatorCommunity("restore-A", 0)
|
||||
persistTranslationMemory("restore-A")
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = persistentMaterializer(sourceExtractor)
|
||||
val firstSource = translationSource(postId, "restore-A", "ko", 0)
|
||||
val restoredSource = translationSource(postId, "restore-A", "ko", 2)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId))
|
||||
.thenReturn(firstSource, restoredSource)
|
||||
|
||||
requiresNewTransactionTemplate.executeWithoutResult {
|
||||
assertTrue(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId, "en"))
|
||||
}
|
||||
updateCreatorCommunity(postId, "restore-B", 1)
|
||||
updateCreatorCommunity(postId, "restore-A", 2)
|
||||
requiresNewTransactionTemplate.executeWithoutResult {
|
||||
assertTrue(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId, "en"))
|
||||
}
|
||||
|
||||
val translation = requiresNewTransactionTemplate.execute {
|
||||
persistentTranslationRepository.findByCreatorCommunityIdAndLocale(postId, "en")
|
||||
}!!
|
||||
assertEquals("translated restore-A", translation.content)
|
||||
assertEquals(2L, translation.sourceRevision)
|
||||
assertEquals(SourceTextNormalizer.hash("restore-A"), translation.sourceHash)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DisplayName("동시 재료화는 게시물 잠금과 locale 유일 키로 하나의 번역 행만 저장한다")
|
||||
fun shouldSerializeConcurrentCreatorCommunityUpserts() {
|
||||
val postId = persistCreatorCommunity("concurrent-A", 2)
|
||||
persistTranslationMemory("concurrent-A")
|
||||
val source = translationSource(postId, "concurrent-A", "ko", 2)
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = persistentMaterializer(sourceExtractor)
|
||||
val readyToLock = CountDownLatch(2)
|
||||
val releaseLock = CountDownLatch(1)
|
||||
val completed = CountDownLatch(2)
|
||||
val errors = ConcurrentLinkedQueue<Throwable>()
|
||||
val executor = Executors.newFixedThreadPool(2)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId)).thenAnswer {
|
||||
readyToLock.countDown()
|
||||
if (!releaseLock.await(5, TimeUnit.SECONDS)) {
|
||||
throw IllegalStateException("workers did not begin together")
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
try {
|
||||
repeat(2) {
|
||||
executor.submit {
|
||||
try {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId, "en")
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
errors.add(error)
|
||||
} finally {
|
||||
completed.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(readyToLock.await(5, TimeUnit.SECONDS))
|
||||
releaseLock.countDown()
|
||||
assertTrue(completed.await(10, TimeUnit.SECONDS))
|
||||
assertTrue(errors.isEmpty())
|
||||
} finally {
|
||||
releaseLock.countDown()
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
||||
val translations = transactionTemplate.execute {
|
||||
persistentTranslationRepository.findByCreatorCommunityIdInAndLocale(listOf(postId), "en")
|
||||
}.orEmpty()
|
||||
assertEquals(1, translations.size)
|
||||
assertEquals("translated concurrent-A", translations.single().content)
|
||||
assertEquals(2L, translations.single().sourceRevision)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DisplayName("같은 게시물의 누락 번역 예약은 하나의 작업만 만들고 둘 다 정상 종료한다")
|
||||
fun shouldSerializeConcurrentMissingMemoryTranslationJobScheduling() {
|
||||
val postId = persistCreatorCommunity("concurrent-job", 0)
|
||||
val missingJobReads = CountDownLatch(2)
|
||||
val jobRepository = delayingTranslationJobRepository(postId, "concurrent-job", missingJobReads)
|
||||
val service = persistentService(jobRepository)
|
||||
val start = CountDownLatch(1)
|
||||
val completed = CountDownLatch(2)
|
||||
val errors = ConcurrentLinkedQueue<Throwable>()
|
||||
val executor = Executors.newFixedThreadPool(2)
|
||||
|
||||
try {
|
||||
repeat(2) {
|
||||
executor.submit {
|
||||
try {
|
||||
check(start.await(5, TimeUnit.SECONDS))
|
||||
requiresNewTransactionTemplate.executeWithoutResult {
|
||||
service.requestTranslations(postId, "en")
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
errors.add(error)
|
||||
} finally {
|
||||
completed.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start.countDown()
|
||||
assertTrue(completed.await(10, TimeUnit.SECONDS))
|
||||
} finally {
|
||||
start.countDown()
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
||||
assertTrue(errors.isEmpty())
|
||||
val jobs = requiresNewTransactionTemplate.execute {
|
||||
persistentTranslationJobRepository.findAll().filter {
|
||||
it.resourceType == LanguageTranslationTargetType.CREATOR_COMMUNITY &&
|
||||
it.resourceId == postId &&
|
||||
it.targetLanguage == "en"
|
||||
}
|
||||
}.orEmpty()
|
||||
assertEquals(1, jobs.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DisplayName("반복 읽기 스냅샷 뒤 번역 행을 잠가 기존 행을 갱신한다")
|
||||
fun shouldUpdateExistingTranslationAfterRepeatableReadSnapshotWaitsForFirstWriter() {
|
||||
val postId = persistCreatorCommunity("repeatable-A", 0)
|
||||
persistTranslationMemory("repeatable-A")
|
||||
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||
val materializer = persistentMaterializer(sourceExtractor)
|
||||
val source = translationSource(postId, "repeatable-A", "ko", 0)
|
||||
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId)).thenReturn(source)
|
||||
val snapshotReady = CountDownLatch(2)
|
||||
val startFirstWriter = CountDownLatch(1)
|
||||
val firstWriterCommitted = CountDownLatch(1)
|
||||
val completed = CountDownLatch(2)
|
||||
val errors = ConcurrentLinkedQueue<Throwable>()
|
||||
val executor = Executors.newFixedThreadPool(2)
|
||||
|
||||
try {
|
||||
executor.submit {
|
||||
try {
|
||||
repeatableReadTransactionTemplate.executeWithoutResult {
|
||||
loadTranslationMemoryForSnapshot("repeatable-A")
|
||||
snapshotReady.countDown()
|
||||
startFirstWriter.await()
|
||||
assertTrue(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId, "en"))
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
errors.add(error)
|
||||
} finally {
|
||||
firstWriterCommitted.countDown()
|
||||
completed.countDown()
|
||||
}
|
||||
}
|
||||
executor.submit {
|
||||
try {
|
||||
repeatableReadTransactionTemplate.executeWithoutResult {
|
||||
loadTranslationMemoryForSnapshot("repeatable-A")
|
||||
snapshotReady.countDown()
|
||||
firstWriterCommitted.await()
|
||||
assertTrue(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, postId, "en"))
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
errors.add(error)
|
||||
} finally {
|
||||
completed.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(snapshotReady.await(5, TimeUnit.SECONDS))
|
||||
startFirstWriter.countDown()
|
||||
assertTrue(completed.await(10, TimeUnit.SECONDS))
|
||||
assertTrue(errors.isEmpty())
|
||||
} finally {
|
||||
startFirstWriter.countDown()
|
||||
firstWriterCommitted.countDown()
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
||||
val translations = requiresNewTransactionTemplate.execute {
|
||||
persistentTranslationRepository.findByCreatorCommunityIdInAndLocale(listOf(postId), "en")
|
||||
}.orEmpty()
|
||||
assertEquals(1, translations.size)
|
||||
assertEquals("translated repeatable-A", translations.single().content)
|
||||
}
|
||||
|
||||
private fun materializer(
|
||||
sourceExtractor: TranslationSourceExtractor,
|
||||
memoryRepository: TranslationMemoryRepository,
|
||||
creatorCommunityRepository: CreatorCommunityRepository,
|
||||
translationRepository: CreatorCommunityTranslationRepository,
|
||||
entityManager: EntityManager = Mockito.mock(EntityManager::class.java)
|
||||
): TranslationReadModelMaterializer {
|
||||
return TranslationReadModelMaterializer(
|
||||
sourceExtractor = sourceExtractor,
|
||||
translationMemoryRepository = memoryRepository,
|
||||
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
|
||||
aiCharacterTranslationRepository = Mockito.mock(AiCharacterTranslationRepository::class.java),
|
||||
contentThemeTranslationRepository = Mockito.mock(ContentThemeTranslationRepository::class.java),
|
||||
seriesTranslationRepository = Mockito.mock(SeriesTranslationRepository::class.java),
|
||||
seriesGenreTranslationRepository = Mockito.mock(SeriesGenreTranslationRepository::class.java),
|
||||
originalWorkTranslationRepository = Mockito.mock(OriginalWorkTranslationRepository::class.java),
|
||||
categoryTranslationRepository = Mockito.mock(CategoryTranslationRepository::class.java),
|
||||
creatorCommunityRepository = creatorCommunityRepository,
|
||||
creatorCommunityTranslationRepository = translationRepository,
|
||||
entityManager = entityManager
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistentMaterializer(sourceExtractor: TranslationSourceExtractor): TranslationReadModelMaterializer {
|
||||
return materializer(
|
||||
sourceExtractor,
|
||||
persistentMemoryRepository,
|
||||
persistentCreatorCommunityRepository,
|
||||
persistentTranslationRepository,
|
||||
entityManager
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistentService(translationJobRepository: TranslationJobRepository): CreatorCommunityTranslationService {
|
||||
val sourceExtractor = TranslationSourceExtractor(
|
||||
audioContentRepository = Mockito.mock(AudioContentRepository::class.java),
|
||||
chatCharacterRepository = Mockito.mock(ChatCharacterRepository::class.java),
|
||||
audioContentThemeRepository = Mockito.mock(AudioContentThemeQueryRepository::class.java),
|
||||
seriesRepository = Mockito.mock(AdminContentSeriesRepository::class.java),
|
||||
seriesGenreRepository = Mockito.mock(AdminContentSeriesGenreRepository::class.java),
|
||||
originalWorkRepository = Mockito.mock(OriginalWorkRepository::class.java),
|
||||
categoryRepository = Mockito.mock(CategoryRepository::class.java),
|
||||
creatorCommunityRepository = persistentCreatorCommunityRepository
|
||||
)
|
||||
return CreatorCommunityTranslationService(
|
||||
creatorCommunityRepository = persistentCreatorCommunityRepository,
|
||||
creatorCommunityTranslationRepository = persistentTranslationRepository,
|
||||
sourceExtractor = sourceExtractor,
|
||||
materializer = persistentMaterializer(sourceExtractor),
|
||||
resourceTranslationJobScheduler = ResourceTranslationJobScheduler(
|
||||
sourceExtractor,
|
||||
TranslationJobScheduler(translationJobRepository)
|
||||
),
|
||||
applicationEventPublisher = applicationEventPublisher,
|
||||
entityManager = entityManager
|
||||
)
|
||||
}
|
||||
|
||||
private fun delayingTranslationJobRepository(
|
||||
postId: Long,
|
||||
sourceText: String,
|
||||
missingJobReads: CountDownLatch
|
||||
): TranslationJobRepository {
|
||||
val sourceHash = SourceTextNormalizer.hash(sourceText)
|
||||
val translationJobRepository = Mockito.mock(TranslationJobRepository::class.java)
|
||||
Mockito.`when`(
|
||||
translationJobRepository.findByResourceTypeAndResourceIdAndFieldKeyAndTargetLanguageAndSourceHash(
|
||||
LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||
postId,
|
||||
"content",
|
||||
"en",
|
||||
sourceHash
|
||||
)
|
||||
).thenAnswer {
|
||||
val existing = persistentTranslationJobRepository
|
||||
.findByResourceTypeAndResourceIdAndFieldKeyAndTargetLanguageAndSourceHash(
|
||||
LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||
postId,
|
||||
"content",
|
||||
"en",
|
||||
sourceHash
|
||||
)
|
||||
if (existing == null) {
|
||||
missingJobReads.countDown()
|
||||
missingJobReads.await(1, TimeUnit.SECONDS)
|
||||
}
|
||||
existing
|
||||
}
|
||||
Mockito.`when`(translationJobRepository.save(Mockito.any(TranslationJob::class.java))).thenAnswer {
|
||||
persistentTranslationJobRepository.save(it.getArgument(0))
|
||||
}
|
||||
return translationJobRepository
|
||||
}
|
||||
|
||||
private fun translationSource(
|
||||
postId: Long = 1L,
|
||||
content: String,
|
||||
sourceLanguage: String,
|
||||
sourceRevision: Long
|
||||
): TranslationSource {
|
||||
return TranslationSource(
|
||||
resourceType = LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||
resourceId = postId,
|
||||
sourceLanguage = sourceLanguage,
|
||||
fields = listOf(TranslationSourceField("content", content)),
|
||||
sourceRevision = sourceRevision
|
||||
)
|
||||
}
|
||||
|
||||
private fun translationMemory(sourceText: String, sourceLanguage: String): TranslationMemory {
|
||||
return TranslationMemory(
|
||||
sourceHash = SourceTextNormalizer.hash(sourceText),
|
||||
sourceText = sourceText,
|
||||
sourceLanguage = sourceLanguage,
|
||||
targetLanguage = "en",
|
||||
translatedText = "translated $sourceText",
|
||||
provider = "papago",
|
||||
providerVersion = "nmt-v1"
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistCreatorCommunity(content: String, contentRevision: Long): Long {
|
||||
return requiresNewTransactionTemplate.execute {
|
||||
val member = Member(
|
||||
password = "password",
|
||||
nickname = "translation-${UUID.randomUUID()}",
|
||||
role = MemberRole.CREATOR
|
||||
)
|
||||
val post = CreatorCommunity(
|
||||
content = content,
|
||||
price = 0,
|
||||
isCommentAvailable = true,
|
||||
isAdult = false,
|
||||
languageCode = "ko",
|
||||
contentRevision = contentRevision
|
||||
).apply {
|
||||
this.member = persistentMemberRepository.save(member)
|
||||
}
|
||||
persistentCreatorCommunityRepository.save(post).id!!
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun persistTranslationMemory(sourceText: String) {
|
||||
requiresNewTransactionTemplate.executeWithoutResult {
|
||||
persistentMemoryRepository.save(translationMemory(sourceText, "ko"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCreatorCommunity(postId: Long, content: String, contentRevision: Long) {
|
||||
requiresNewTransactionTemplate.executeWithoutResult {
|
||||
val post = persistentCreatorCommunityRepository.findById(postId).orElseThrow()
|
||||
post.content = content
|
||||
post.contentRevision = contentRevision
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTranslationMemoryForSnapshot(sourceText: String) {
|
||||
persistentMemoryRepository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
||||
SourceTextNormalizer.hash(sourceText),
|
||||
"ko",
|
||||
"en",
|
||||
"papago",
|
||||
SourceTextNormalizer.NORMALIZATION_VERSION
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenTranslationMemory(
|
||||
memoryRepository: TranslationMemoryRepository,
|
||||
sourceText: String,
|
||||
sourceLanguage: String
|
||||
) {
|
||||
Mockito.`when`(
|
||||
memoryRepository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
||||
SourceTextNormalizer.hash(sourceText),
|
||||
sourceLanguage,
|
||||
"en",
|
||||
"papago",
|
||||
SourceTextNormalizer.NORMALIZATION_VERSION
|
||||
)
|
||||
).thenReturn(translationMemory(sourceText, sourceLanguage))
|
||||
}
|
||||
|
||||
private fun creatorCommunity(
|
||||
postId: Long,
|
||||
content: String,
|
||||
languageCode: String,
|
||||
contentRevision: Long
|
||||
): CreatorCommunity {
|
||||
return CreatorCommunity(
|
||||
content = content,
|
||||
price = 0,
|
||||
isCommentAvailable = true,
|
||||
isAdult = false,
|
||||
languageCode = languageCode,
|
||||
contentRevision = contentRevision
|
||||
).apply {
|
||||
id = postId
|
||||
}
|
||||
}
|
||||
|
||||
private fun translation(
|
||||
postId: Long,
|
||||
content: String,
|
||||
sourceRevision: Long,
|
||||
sourceText: String,
|
||||
sourceLanguage: String = "ko"
|
||||
): CreatorCommunityTranslation {
|
||||
return CreatorCommunityTranslation(
|
||||
creatorCommunityId = postId,
|
||||
locale = "en",
|
||||
content = content,
|
||||
sourceRevision = sourceRevision,
|
||||
sourceHash = SourceTextNormalizer.hash(sourceText),
|
||||
sourceLanguage = sourceLanguage
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user