test #449
+36
-9
@@ -27,6 +27,7 @@ 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
|
||||||
import kr.co.vividnext.sodalive.utils.validateImage
|
import kr.co.vividnext.sodalive.utils.validateImage
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
@@ -39,6 +40,8 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
|
|||||||
import org.springframework.web.multipart.MultipartFile
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
import javax.persistence.LockModeType
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@@ -58,6 +61,8 @@ class CreatorCommunityService(
|
|||||||
private val messageSource: SodaMessageSource,
|
private val messageSource: SodaMessageSource,
|
||||||
private val langContext: LangContext,
|
private val langContext: LangContext,
|
||||||
private val homeFollowingNewsPublishService: HomeFollowingNewsPublishService,
|
private val homeFollowingNewsPublishService: HomeFollowingNewsPublishService,
|
||||||
|
private val creatorCommunityTranslationService: CreatorCommunityTranslationService,
|
||||||
|
private val entityManager: EntityManager,
|
||||||
|
|
||||||
@Value("\${cloud.aws.s3.bucket}")
|
@Value("\${cloud.aws.s3.bucket}")
|
||||||
private val imageBucket: String,
|
private val imageBucket: String,
|
||||||
@@ -144,6 +149,7 @@ class CreatorCommunityService(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
publishCommunityPostCreatedAfterCommit(post, member)
|
publishCommunityPostCreatedAfterCommit(post, member)
|
||||||
|
requestTranslationsAfterCommit(post.id!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun publishCommunityPostCreatedAfterCommit(post: CreatorCommunity, member: Member) {
|
private fun publishCommunityPostCreatedAfterCommit(post: CreatorCommunity, member: Member) {
|
||||||
@@ -166,6 +172,12 @@ class CreatorCommunityService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requestTranslationsAfterCommit(postId: Long) {
|
||||||
|
afterCommit {
|
||||||
|
creatorCommunityTranslationService.requestTranslations(postId, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun CreatorCommunity.newsContentPreview(): String {
|
private fun CreatorCommunity.newsContentPreview(): String {
|
||||||
if (price <= 0) {
|
if (price <= 0) {
|
||||||
return content
|
return content
|
||||||
@@ -199,13 +211,17 @@ class CreatorCommunityService(
|
|||||||
fun modifyCommunityPost(postImage: MultipartFile?, requestString: String, member: Member) {
|
fun modifyCommunityPost(postImage: MultipartFile?, requestString: String, member: Member) {
|
||||||
val request = objectMapper.readValue(requestString, ModifyCommunityPostRequest::class.java)
|
val request = objectMapper.readValue(requestString, ModifyCommunityPostRequest::class.java)
|
||||||
|
|
||||||
val post = repository.findByIdAndMemberId(id = request.creatorCommunityId, memberId = member.id!!)
|
val post = repository.findByIdAndMemberIdForUpdate(postId = request.creatorCommunityId, memberId = member.id!!)
|
||||||
?: throw SodaException(messageKey = "common.error.invalid_request")
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
entityManager.refresh(post, LockModeType.PESSIMISTIC_WRITE)
|
||||||
|
|
||||||
postImage?.let { validateImage(it, post.price > 0) }
|
postImage?.let { validateImage(it, post.price > 0) }
|
||||||
|
|
||||||
if (request.content != null) {
|
if (request.content != null && request.content != post.content) {
|
||||||
post.content = request.content
|
post.content = request.content
|
||||||
|
post.languageCode = null
|
||||||
|
post.contentRevision += 1
|
||||||
|
requestTranslationsAfterCommit(post.id!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.isCommentAvailable != null) {
|
if (request.isCommentAvailable != null) {
|
||||||
@@ -246,8 +262,9 @@ class CreatorCommunityService(
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun updateCommunityPostFixed(request: UpdateCommunityPostFixedRequest, member: Member) {
|
fun updateCommunityPostFixed(request: UpdateCommunityPostFixedRequest, member: Member) {
|
||||||
val post = repository.findByIdAndMemberId(id = request.postId, memberId = member.id!!)
|
val post = repository.findByIdAndMemberIdForUpdate(postId = request.postId, memberId = member.id!!)
|
||||||
?: throw SodaException(messageKey = "common.error.invalid_request")
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
entityManager.refresh(post, LockModeType.PESSIMISTIC_WRITE)
|
||||||
|
|
||||||
if (request.isFixed) {
|
if (request.isFixed) {
|
||||||
if (!post.isFixed) {
|
if (!post.isFixed) {
|
||||||
@@ -285,6 +302,8 @@ class CreatorCommunityService(
|
|||||||
limit = limit,
|
limit = limit,
|
||||||
isAdult = isAdult
|
isAdult = isAdult
|
||||||
)
|
)
|
||||||
|
val displayContents = creatorCommunityTranslationService
|
||||||
|
.findDisplayContents(postList.map { it.id }, langContext.lang.code)
|
||||||
|
|
||||||
return postList
|
return postList
|
||||||
.map {
|
.map {
|
||||||
@@ -337,7 +356,7 @@ class CreatorCommunityService(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
it.toCommunityPostListResponse(
|
it.copy(content = displayContents[it.id]?.content ?: it.content).toCommunityPostListResponse(
|
||||||
imageHost = imageHost,
|
imageHost = imageHost,
|
||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
date = it.date.getTimeAgoString(),
|
date = it.date.getTimeAgoString(),
|
||||||
@@ -376,6 +395,12 @@ class CreatorCommunityService(
|
|||||||
throw SodaException(message = String.format(messageTemplate, post.creatorNickname))
|
throw SodaException(message = String.format(messageTemplate, post.creatorNickname))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val displayContent = creatorCommunityTranslationService
|
||||||
|
.findDisplayContents(listOf(post.id), langContext.lang.code)[post.id]
|
||||||
|
if (displayContent?.hasValidTranslation != true) {
|
||||||
|
creatorCommunityTranslationService.requestTranslations(post.id, langContext.lang.code)
|
||||||
|
}
|
||||||
|
|
||||||
val isLike = likeRepository.findByPostIdAndMemberId(postId = post.id, memberId = memberId)?.isActive ?: false
|
val isLike = likeRepository.findByPostIdAndMemberId(postId = post.id, memberId = memberId)?.isActive ?: false
|
||||||
val likeCount = likeRepository.totalCountCommunityPostLikeByPostId(post.id)
|
val likeCount = likeRepository.totalCountCommunityPostLikeByPostId(post.id)
|
||||||
val commentCount = if (post.isCommentAvailable) {
|
val commentCount = if (post.isCommentAvailable) {
|
||||||
@@ -417,7 +442,7 @@ class CreatorCommunityService(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
return post.toCommunityPostListResponse(
|
return post.copy(content = displayContent?.content ?: post.content).toCommunityPostListResponse(
|
||||||
imageHost = imageHost,
|
imageHost = imageHost,
|
||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
date = post.date.getTimeAgoString(),
|
date = post.date.getTimeAgoString(),
|
||||||
@@ -610,11 +635,13 @@ class CreatorCommunityService(
|
|||||||
isAdult: Boolean
|
isAdult: Boolean
|
||||||
): List<GetCommunityPostListResponse> {
|
): List<GetCommunityPostListResponse> {
|
||||||
val postList = repository.getLatestPostListFromCreatorsYouFollow(memberId = memberId, isAdult = isAdult)
|
val postList = repository.getLatestPostListFromCreatorsYouFollow(memberId = memberId, isAdult = isAdult)
|
||||||
|
val visiblePostList = postList.filter {
|
||||||
return postList
|
|
||||||
.filter {
|
|
||||||
!isBlockedBetweenMembers(memberId = memberId, creatorId = it.creatorId)
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = it.creatorId)
|
||||||
}
|
}
|
||||||
|
val displayContents = creatorCommunityTranslationService
|
||||||
|
.findDisplayContents(visiblePostList.map { it.id }, langContext.lang.code)
|
||||||
|
|
||||||
|
return visiblePostList
|
||||||
.map {
|
.map {
|
||||||
val isLike =
|
val isLike =
|
||||||
likeRepository.findByPostIdAndMemberId(postId = it.id, memberId = memberId)?.isActive ?: false
|
likeRepository.findByPostIdAndMemberId(postId = it.id, memberId = memberId)?.isActive ?: false
|
||||||
@@ -649,7 +676,7 @@ class CreatorCommunityService(
|
|||||||
|
|
||||||
val existOrdered = useCanRepository.isExistCommunityPostOrdered(postId = it.id, memberId = memberId)
|
val existOrdered = useCanRepository.isExistCommunityPostOrdered(postId = it.id, memberId = memberId)
|
||||||
|
|
||||||
it.toCommunityPostListResponse(
|
it.copy(content = displayContents[it.id]?.content ?: it.content).toCommunityPostListResponse(
|
||||||
imageHost = imageHost,
|
imageHost = imageHost,
|
||||||
audioUrl = null,
|
audioUrl = null,
|
||||||
date = it.date.getTimeAgoString(),
|
date = it.date.getTimeAgoString(),
|
||||||
|
|||||||
+344
-5
@@ -17,11 +17,14 @@ import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue
|
|||||||
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.fcm.notification.PushNotificationCategory
|
import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory
|
||||||
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertFalse
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
@@ -36,10 +39,13 @@ import org.mockito.ArgumentCaptor
|
|||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||||
import org.springframework.web.multipart.MultipartFile
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
class CreatorCommunityServiceTest {
|
class CreatorCommunityServiceTest {
|
||||||
private lateinit var repository: CreatorCommunityRepository
|
private lateinit var repository: CreatorCommunityRepository
|
||||||
@@ -50,6 +56,9 @@ class CreatorCommunityServiceTest {
|
|||||||
private lateinit var s3Uploader: S3Uploader
|
private lateinit var s3Uploader: S3Uploader
|
||||||
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
||||||
private lateinit var homeFollowingNewsPublishService: HomeFollowingNewsPublishService
|
private lateinit var homeFollowingNewsPublishService: HomeFollowingNewsPublishService
|
||||||
|
private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService
|
||||||
|
private lateinit var entityManager: EntityManager
|
||||||
|
private lateinit var langContext: LangContext
|
||||||
private lateinit var service: CreatorCommunityService
|
private lateinit var service: CreatorCommunityService
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -62,6 +71,9 @@ class CreatorCommunityServiceTest {
|
|||||||
s3Uploader = Mockito.mock(S3Uploader::class.java)
|
s3Uploader = Mockito.mock(S3Uploader::class.java)
|
||||||
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||||
homeFollowingNewsPublishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
homeFollowingNewsPublishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||||
|
creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
|
||||||
|
entityManager = Mockito.mock(EntityManager::class.java)
|
||||||
|
langContext = LangContext()
|
||||||
|
|
||||||
service = CreatorCommunityService(
|
service = CreatorCommunityService(
|
||||||
canPaymentService = Mockito.mock(CanPaymentService::class.java),
|
canPaymentService = Mockito.mock(CanPaymentService::class.java),
|
||||||
@@ -75,8 +87,10 @@ class CreatorCommunityServiceTest {
|
|||||||
audioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java),
|
audioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java),
|
||||||
applicationEventPublisher = applicationEventPublisher,
|
applicationEventPublisher = applicationEventPublisher,
|
||||||
messageSource = SodaMessageSource(),
|
messageSource = SodaMessageSource(),
|
||||||
langContext = LangContext(),
|
langContext = langContext,
|
||||||
homeFollowingNewsPublishService = homeFollowingNewsPublishService,
|
homeFollowingNewsPublishService = homeFollowingNewsPublishService,
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService,
|
||||||
|
entityManager = entityManager,
|
||||||
imageBucket = "image-bucket",
|
imageBucket = "image-bucket",
|
||||||
contentBucket = "content-bucket",
|
contentBucket = "content-bucket",
|
||||||
imageHost = "https://cdn.test"
|
imageHost = "https://cdn.test"
|
||||||
@@ -252,7 +266,7 @@ class CreatorCommunityServiceTest {
|
|||||||
post.id = 501L
|
post.id = 501L
|
||||||
post.member = creator
|
post.member = creator
|
||||||
|
|
||||||
Mockito.`when`(repository.findByIdAndMemberId(post.id!!, creator.id!!)).thenReturn(post)
|
Mockito.`when`(repository.findByIdAndMemberIdForUpdate(post.id!!, creator.id!!)).thenReturn(post)
|
||||||
Mockito.`when`(repository.countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue(creator.id!!)).thenReturn(3L)
|
Mockito.`when`(repository.countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue(creator.id!!)).thenReturn(3L)
|
||||||
|
|
||||||
val exception = assertThrows(SodaException::class.java) {
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
@@ -273,7 +287,7 @@ class CreatorCommunityServiceTest {
|
|||||||
post.id = 601L
|
post.id = 601L
|
||||||
post.member = creator
|
post.member = creator
|
||||||
|
|
||||||
Mockito.`when`(repository.findByIdAndMemberId(post.id!!, creator.id!!)).thenReturn(post)
|
Mockito.`when`(repository.findByIdAndMemberIdForUpdate(post.id!!, creator.id!!)).thenReturn(post)
|
||||||
Mockito.`when`(repository.countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue(creator.id!!)).thenReturn(2L)
|
Mockito.`when`(repository.countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue(creator.id!!)).thenReturn(2L)
|
||||||
|
|
||||||
service.updateCommunityPostFixed(
|
service.updateCommunityPostFixed(
|
||||||
@@ -295,7 +309,7 @@ class CreatorCommunityServiceTest {
|
|||||||
post.isFixed = true
|
post.isFixed = true
|
||||||
post.fixedAt = LocalDateTime.now()
|
post.fixedAt = LocalDateTime.now()
|
||||||
|
|
||||||
Mockito.`when`(repository.findByIdAndMemberId(post.id!!, creator.id!!)).thenReturn(post)
|
Mockito.`when`(repository.findByIdAndMemberIdForUpdate(post.id!!, creator.id!!)).thenReturn(post)
|
||||||
|
|
||||||
service.updateCommunityPostFixed(
|
service.updateCommunityPostFixed(
|
||||||
request = UpdateCommunityPostFixedRequest(postId = post.id!!, isFixed = false),
|
request = UpdateCommunityPostFixedRequest(postId = post.id!!, isFixed = false),
|
||||||
@@ -306,6 +320,120 @@ class CreatorCommunityServiceTest {
|
|||||||
assertNull(post.fixedAt)
|
assertNull(post.fixedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("본문이 실제로 변경되면 언어를 초기화하고 본문 개정을 한 번 증가시킨다")
|
||||||
|
fun shouldResetLanguageAndIncrementRevisionWhenContentChanges() {
|
||||||
|
val creator = createMember(id = 304L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
val post = CreatorCommunity(
|
||||||
|
content = "before",
|
||||||
|
price = 0,
|
||||||
|
isCommentAvailable = true,
|
||||||
|
isAdult = false,
|
||||||
|
languageCode = "ko",
|
||||||
|
contentRevision = 7
|
||||||
|
).apply {
|
||||||
|
id = 304L
|
||||||
|
member = creator
|
||||||
|
}
|
||||||
|
Mockito.`when`(repository.findByIdAndMemberIdForUpdate(post.id!!, creator.id!!)).thenReturn(post)
|
||||||
|
|
||||||
|
service.modifyCommunityPost(
|
||||||
|
postImage = null,
|
||||||
|
requestString = """{"creatorCommunityId":304,"content":"after"}""",
|
||||||
|
member = creator
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("after", post.content)
|
||||||
|
assertNull(post.languageCode)
|
||||||
|
assertEquals(8, post.contentRevision)
|
||||||
|
Mockito.verify(creatorCommunityTranslationService).requestTranslations(post.id!!, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("동일 본문과 본문 외 수정은 언어와 개정 및 번역 요청을 유지한다")
|
||||||
|
fun shouldNotResetLanguageOrRequestTranslationsWhenContentIsUnchanged() {
|
||||||
|
val creator = createMember(id = 305L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
val post = CreatorCommunity(
|
||||||
|
content = "same",
|
||||||
|
price = 0,
|
||||||
|
isCommentAvailable = true,
|
||||||
|
isAdult = false,
|
||||||
|
languageCode = "ko",
|
||||||
|
contentRevision = 7
|
||||||
|
).apply {
|
||||||
|
id = 305L
|
||||||
|
member = creator
|
||||||
|
}
|
||||||
|
Mockito.`when`(repository.findByIdAndMemberIdForUpdate(post.id!!, creator.id!!)).thenReturn(post)
|
||||||
|
Mockito.`when`(repository.findByIdAndMemberId(post.id!!, creator.id!!)).thenReturn(post)
|
||||||
|
|
||||||
|
service.modifyCommunityPost(
|
||||||
|
postImage = null,
|
||||||
|
requestString = """
|
||||||
|
{"creatorCommunityId":305,"content":"same","isCommentAvailable":false,"isAdult":true,"isActive":false}
|
||||||
|
""".trimIndent(),
|
||||||
|
member = creator
|
||||||
|
)
|
||||||
|
service.updateCommunityPostFixed(UpdateCommunityPostFixedRequest(post.id!!, isFixed = false), creator)
|
||||||
|
|
||||||
|
assertEquals("ko", post.languageCode)
|
||||||
|
assertEquals(7, post.contentRevision)
|
||||||
|
Mockito.verifyNoInteractions(creatorCommunityTranslationService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("게시글 생성은 커밋 후에만 번역을 요청한다")
|
||||||
|
fun shouldRequestTranslationsAfterCommunityPostCreationCommits() {
|
||||||
|
val creator = createMember(id = 306L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
Mockito.`when`(repository.save(Mockito.any(CreatorCommunity::class.java))).thenAnswer { invocation ->
|
||||||
|
invocation.getArgument<CreatorCommunity>(0).apply { id = 306L }
|
||||||
|
}
|
||||||
|
TransactionSynchronizationManager.initSynchronization()
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.createCommunityPost(
|
||||||
|
audioFile = null,
|
||||||
|
postImage = null,
|
||||||
|
requestString = """{"content":"created","price":0,"isCommentAvailable":true,"isAdult":false}""",
|
||||||
|
member = creator
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.verifyNoInteractions(creatorCommunityTranslationService)
|
||||||
|
TransactionSynchronizationManager.getSynchronizations().forEach { it.afterCommit() }
|
||||||
|
|
||||||
|
Mockito.verify(creatorCommunityTranslationService).requestTranslations(306L, null)
|
||||||
|
} finally {
|
||||||
|
TransactionSynchronizationManager.clearSynchronization()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("게시글 생성 롤백은 번역을 요청하지 않는다")
|
||||||
|
fun shouldNotRequestTranslationsWhenCommunityPostCreationRollsBack() {
|
||||||
|
val creator = createMember(id = 307L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
Mockito.`when`(repository.save(Mockito.any(CreatorCommunity::class.java))).thenAnswer { invocation ->
|
||||||
|
invocation.getArgument<CreatorCommunity>(0).apply { id = 307L }
|
||||||
|
}
|
||||||
|
TransactionSynchronizationManager.initSynchronization()
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.createCommunityPost(
|
||||||
|
audioFile = null,
|
||||||
|
postImage = null,
|
||||||
|
requestString = """{"content":"rolled back","price":0,"isCommentAvailable":true,"isAdult":false}""",
|
||||||
|
member = creator
|
||||||
|
)
|
||||||
|
|
||||||
|
TransactionSynchronizationManager.getSynchronizations().forEach {
|
||||||
|
it.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK)
|
||||||
|
}
|
||||||
|
|
||||||
|
Mockito.verifyNoInteractions(creatorCommunityTranslationService)
|
||||||
|
} finally {
|
||||||
|
TransactionSynchronizationManager.clearSynchronization()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("커뮤니티 게시글 생성 성공 후 최근 소식을 게시글 정보로 발행한다")
|
@DisplayName("커뮤니티 게시글 생성 성공 후 최근 소식을 게시글 정보로 발행한다")
|
||||||
fun shouldPublishNewsAfterCommunityPostCreated() {
|
fun shouldPublishNewsAfterCommunityPostCreated() {
|
||||||
@@ -329,7 +457,7 @@ class CreatorCommunityServiceTest {
|
|||||||
Mockito.verify(homeFollowingNewsPublishService).publishCommunityPostCreated(
|
Mockito.verify(homeFollowingNewsPublishService).publishCommunityPostCreated(
|
||||||
postId = 901L,
|
postId = 901L,
|
||||||
creatorId = creator.id!!,
|
creatorId = creator.id!!,
|
||||||
creatorNickname = creator.nickname!!,
|
creatorNickname = creator.nickname,
|
||||||
creatorProfileImagePath = creator.profileImage,
|
creatorProfileImagePath = creator.profileImage,
|
||||||
title = "커뮤니티 새 게시글 본문입니다",
|
title = "커뮤니티 새 게시글 본문입니다",
|
||||||
body = "커뮤니티 새 게시글 본문입니다",
|
body = "커뮤니티 새 게시글 본문입니다",
|
||||||
@@ -369,6 +497,195 @@ class CreatorCommunityServiceTest {
|
|||||||
Mockito.verifyNoInteractions(homeFollowingNewsPublishService)
|
Mockito.verifyNoInteractions(homeFollowingNewsPublishService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 상세는 선택한 번역문에 유료 미리보기를 적용하고 요청 언어만 예약한다")
|
||||||
|
fun shouldSelectTranslatedContentBeforePaidMaskingAndRequestCurrentLanguageForLegacyDetail() {
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val post = selectCommunityPost(postId = 100L, content = "source paid content", price = 100)
|
||||||
|
Mockito.`when`(repository.getCommunityPost(100L, true)).thenReturn(post)
|
||||||
|
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(100L), "ja"))
|
||||||
|
.thenReturn(mapOf(100L to CreatorCommunityDisplayContent("translated paid content", true)))
|
||||||
|
|
||||||
|
val response = service.getCommunityPostDetail(100L, memberId = 200L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
|
||||||
|
assertEquals("translated paid...", response.content)
|
||||||
|
Mockito.verify(creatorCommunityTranslationService).findDisplayContents(listOf(100L), "ja")
|
||||||
|
Mockito.verify(creatorCommunityTranslationService, Mockito.never()).requestTranslations(100L, "ja")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 상세는 원문과 같은 유효 번역문에도 누락 예약을 요청하지 않는다")
|
||||||
|
fun shouldNotRequestTranslationsWhenLegacyDetailHasValidTranslationMatchingSourceText() {
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val post = selectCommunityPost(postId = 104L, content = "same content", price = 0)
|
||||||
|
Mockito.`when`(repository.getCommunityPost(104L, true)).thenReturn(post)
|
||||||
|
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(104L), "ja"))
|
||||||
|
.thenReturn(mapOf(104L to CreatorCommunityDisplayContent("same content", true)))
|
||||||
|
|
||||||
|
service.getCommunityPostDetail(104L, memberId = 200L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
|
||||||
|
Mockito.verify(creatorCommunityTranslationService, Mockito.never()).requestTranslations(104L, "ja")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 상세는 번역이 없으면 현재 원문을 반환하고 요청 언어만 예약한다")
|
||||||
|
fun shouldReturnOriginalAndRequestCurrentLanguageWhenLegacyDetailTranslationIsMissing() {
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val post = selectCommunityPost(postId = 101L, content = "current original", price = 0)
|
||||||
|
Mockito.`when`(repository.getCommunityPost(101L, true)).thenReturn(post)
|
||||||
|
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(101L), "ja"))
|
||||||
|
.thenReturn(emptyMap())
|
||||||
|
|
||||||
|
val response = service.getCommunityPostDetail(101L, memberId = 200L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
|
||||||
|
assertEquals("current original", response.content)
|
||||||
|
Mockito.verify(creatorCommunityTranslationService).requestTranslations(101L, "ja")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 상세는 구매자에게 선택한 유료 번역문 전체를 반환한다")
|
||||||
|
fun shouldReturnFullTranslatedContentToLegacyDetailBuyer() {
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val post = selectCommunityPost(postId = 102L, content = "source paid content", price = 100)
|
||||||
|
Mockito.`when`(repository.getCommunityPost(102L, true)).thenReturn(post)
|
||||||
|
Mockito.`when`(useCanRepository.isExistCommunityPostOrdered(102L, 200L)).thenReturn(true)
|
||||||
|
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(102L), "ja"))
|
||||||
|
.thenReturn(mapOf(102L to CreatorCommunityDisplayContent("translated paid content", true)))
|
||||||
|
|
||||||
|
val response = service.getCommunityPostDetail(102L, memberId = 200L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
|
||||||
|
assertEquals("translated paid content", response.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 상세는 소유자에게 선택한 유료 번역문 전체를 반환한다")
|
||||||
|
fun shouldReturnFullTranslatedContentToLegacyDetailOwner() {
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val post = selectCommunityPost(postId = 103L, creatorId = 200L, content = "source paid content", price = 100)
|
||||||
|
Mockito.`when`(repository.getCommunityPost(103L, true)).thenReturn(post)
|
||||||
|
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(103L), "ja"))
|
||||||
|
.thenReturn(mapOf(103L to CreatorCommunityDisplayContent("translated paid content", true)))
|
||||||
|
|
||||||
|
val response = service.getCommunityPostDetail(103L, memberId = 200L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
|
||||||
|
assertEquals("translated paid content", response.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 목록은 페이지 본문을 한 번에 번역 선택한 뒤 기존 유료 미리보기를 적용한다")
|
||||||
|
fun shouldUseStoredTranslationsOnceForLegacyCommunityListBeforePaidMasking() {
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val posts = listOf(
|
||||||
|
selectCommunityPost(postId = 201L, content = "source free", price = 0),
|
||||||
|
selectCommunityPost(postId = 202L, content = "source paid", price = 100),
|
||||||
|
selectCommunityPost(postId = 203L, content = "source buyer", price = 100),
|
||||||
|
selectCommunityPost(postId = 204L, creatorId = 200L, content = "source owner", price = 100),
|
||||||
|
selectCommunityPost(postId = 205L, content = "source stale", price = 0)
|
||||||
|
)
|
||||||
|
val postIds = posts.map { it.id }
|
||||||
|
Mockito.`when`(repository.getCommunityPostList(1L, 200L, 0L, 20L, true)).thenReturn(posts)
|
||||||
|
Mockito.`when`(useCanRepository.isExistCommunityPostOrdered(203L, 200L)).thenReturn(true)
|
||||||
|
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(postIds, "ja"))
|
||||||
|
.thenReturn(
|
||||||
|
mapOf(
|
||||||
|
201L to CreatorCommunityDisplayContent("translated free", true),
|
||||||
|
202L to CreatorCommunityDisplayContent("가나다라마바사아자차카타파하🙂끝", true),
|
||||||
|
203L to CreatorCommunityDisplayContent("translated buyer", true),
|
||||||
|
204L to CreatorCommunityDisplayContent("translated owner", true),
|
||||||
|
205L to CreatorCommunityDisplayContent("source stale", false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val responses = service.getCommunityPostList(
|
||||||
|
creatorId = 1L,
|
||||||
|
memberId = 200L,
|
||||||
|
timezone = "Asia/Seoul",
|
||||||
|
offset = 0,
|
||||||
|
limit = 20,
|
||||||
|
isAdult = true
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
"translated free",
|
||||||
|
"가나다라마바사아자차카타파하🙂...",
|
||||||
|
"translated buyer",
|
||||||
|
"translated owner",
|
||||||
|
"source stale"
|
||||||
|
),
|
||||||
|
responses.map { it.content }
|
||||||
|
)
|
||||||
|
Mockito.verify(creatorCommunityTranslationService).findDisplayContents(postIds, "ja")
|
||||||
|
Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 최신 목록은 차단 필터 뒤 남은 본문만 한 번에 번역 선택하고 작업을 만들지 않는다")
|
||||||
|
fun shouldUseStoredTranslationsForVisibleLatestCommunityPostsWithoutScheduling() {
|
||||||
|
langContext.setLang(Lang.JA)
|
||||||
|
val visiblePost = selectCommunityPost(postId = 206L, content = "source visible", price = 0)
|
||||||
|
val blockedPost = selectCommunityPost(postId = 207L, creatorId = 2L, content = "source blocked", price = 0)
|
||||||
|
Mockito.`when`(repository.getLatestPostListFromCreatorsYouFollow(200L, true))
|
||||||
|
.thenReturn(listOf(visiblePost, blockedPost))
|
||||||
|
Mockito.`when`(blockMemberRepository.isBlocked(200L, 2L)).thenReturn(true)
|
||||||
|
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(206L), "ja"))
|
||||||
|
.thenReturn(mapOf(206L to CreatorCommunityDisplayContent("translated visible", true)))
|
||||||
|
|
||||||
|
val responses = service.getLatestPostListFromCreatorsYouFollow(
|
||||||
|
timezone = "Asia/Seoul",
|
||||||
|
memberId = 200L,
|
||||||
|
isAdult = true
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("translated visible"), responses.map { it.content })
|
||||||
|
Mockito.verify(creatorCommunityTranslationService).findDisplayContents(listOf(206L), "ja")
|
||||||
|
Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("차단된 레거시 상세은 번역 조회나 예약을 시작하지 않는다")
|
||||||
|
fun shouldNotScheduleTranslationsWhenLegacyDetailIsBlocked() {
|
||||||
|
val post = selectCommunityPost(postId = 104L, content = "blocked", price = 0)
|
||||||
|
Mockito.`when`(repository.getCommunityPost(104L, true)).thenReturn(post)
|
||||||
|
Mockito.`when`(blockMemberRepository.isBlocked(200L, 1L)).thenReturn(true)
|
||||||
|
|
||||||
|
assertThrows(SodaException::class.java) {
|
||||||
|
service.getCommunityPostDetail(104L, memberId = 200L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Mockito.verifyNoInteractions(creatorCommunityTranslationService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("비활성 레거시 상세은 번역 조회나 예약을 시작하지 않는다")
|
||||||
|
fun shouldNotScheduleTranslationsWhenLegacyDetailIsInactive() {
|
||||||
|
Mockito.`when`(repository.getCommunityPost(105L, true)).thenReturn(null)
|
||||||
|
|
||||||
|
assertThrows(SodaException::class.java) {
|
||||||
|
service.getCommunityPostDetail(105L, memberId = 200L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Mockito.verifyNoInteractions(creatorCommunityTranslationService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 댓글과 구매 응답은 번역 예약 진입점이 아니다")
|
||||||
|
fun shouldNotScheduleTranslationsFromLegacyCommentOrPurchaseResponses() {
|
||||||
|
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
val post = CreatorCommunity(content = "original", price = 100, isCommentAvailable = false, isAdult = false).apply {
|
||||||
|
id = 106L
|
||||||
|
createdAt = LocalDateTime.of(2026, 9, 10, 10, 0)
|
||||||
|
member = creator
|
||||||
|
}
|
||||||
|
Mockito.`when`(repository.findByIdAndActive(106L, true)).thenReturn(post)
|
||||||
|
Mockito.`when`(useCanRepository.isExistCommunityPostOrdered(106L, 200L)).thenReturn(true)
|
||||||
|
|
||||||
|
service.getCommunityPostCommentList(106L, 200L, "Asia/Seoul", 0, 20, true)
|
||||||
|
service.purchasePost(106L, 200L, "Asia/Seoul", true, "test")
|
||||||
|
|
||||||
|
Mockito.verifyNoInteractions(creatorCommunityTranslationService)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("최근 소식 발행 실패는 커뮤니티 게시글 생성을 실패시키지 않는다")
|
@DisplayName("최근 소식 발행 실패는 커뮤니티 게시글 생성을 실패시키지 않는다")
|
||||||
fun shouldNotFailCommunityPostCreationWhenNewsPublishFails() {
|
fun shouldNotFailCommunityPostCreationWhenNewsPublishFails() {
|
||||||
@@ -457,4 +774,26 @@ class CreatorCommunityServiceTest {
|
|||||||
member.id = id
|
member.id = id
|
||||||
return member
|
return member
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun selectCommunityPost(
|
||||||
|
postId: Long,
|
||||||
|
creatorId: Long = 1L,
|
||||||
|
content: String,
|
||||||
|
price: Int
|
||||||
|
): SelectCommunityPostResponse {
|
||||||
|
return SelectCommunityPostResponse(
|
||||||
|
id = postId,
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = "creator-$creatorId",
|
||||||
|
creatorProfileUrl = "profile/$creatorId.png",
|
||||||
|
imagePath = null,
|
||||||
|
audioPath = null,
|
||||||
|
content = content,
|
||||||
|
date = LocalDateTime.of(2026, 9, 10, 10, 0),
|
||||||
|
isCommentAvailable = false,
|
||||||
|
price = price,
|
||||||
|
isAdult = false,
|
||||||
|
isFixed = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+248
@@ -0,0 +1,248 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.creatorCommunity
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
|
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
|
||||||
|
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
|
||||||
|
import kr.co.vividnext.sodalive.can.use.UseCanRepository
|
||||||
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityCommentRepository
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLikeRepository
|
||||||
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
|
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.TranslationJobRepository
|
||||||
|
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.member.block.BlockMemberRepository
|
||||||
|
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 kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||||
|
import org.hibernate.SessionFactory
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
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 javax.persistence.EntityManager
|
||||||
|
import javax.persistence.EntityManagerFactory
|
||||||
|
|
||||||
|
@DataJpaTest(
|
||||||
|
properties = [
|
||||||
|
"spring.cache.type=none",
|
||||||
|
"spring.datasource.url=" +
|
||||||
|
"jdbc:h2:mem:creator-community-detail-translation;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE",
|
||||||
|
"spring.jpa.properties.hibernate.generate_statistics=true"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||||
|
@Import(QueryDslConfig::class)
|
||||||
|
class CreatorCommunityTranslationEndToEndTest @Autowired constructor(
|
||||||
|
private val creatorCommunityRepository: CreatorCommunityRepository,
|
||||||
|
private val creatorCommunityTranslationRepository: CreatorCommunityTranslationRepository,
|
||||||
|
private val memberRepository: MemberRepository,
|
||||||
|
private val entityManager: EntityManager,
|
||||||
|
private val entityManagerFactory: EntityManagerFactory,
|
||||||
|
private val translationJobRepository: TranslationJobRepository
|
||||||
|
) {
|
||||||
|
@Test
|
||||||
|
@DisplayName("실제 번역 읽기는 작은 배치와 큰 배치에서 게시글과 번역을 각각 한 번씩 조회한다")
|
||||||
|
fun shouldReadTranslationsWithConstantSelectCountAcrossBatchSizes() {
|
||||||
|
val postIds = (1..4).map { index ->
|
||||||
|
persistPost(content = "batch source $index", languageCode = "ko", price = 0).id!!
|
||||||
|
}
|
||||||
|
val translationService = CreatorCommunityTranslationService(
|
||||||
|
creatorCommunityRepository = creatorCommunityRepository,
|
||||||
|
creatorCommunityTranslationRepository = creatorCommunityTranslationRepository,
|
||||||
|
sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java),
|
||||||
|
materializer = Mockito.mock(TranslationReadModelMaterializer::class.java),
|
||||||
|
resourceTranslationJobScheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java),
|
||||||
|
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java),
|
||||||
|
entityManager = entityManager
|
||||||
|
)
|
||||||
|
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||||
|
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
statistics.clear()
|
||||||
|
translationService.findDisplayContents(postIds.take(1), "ja")
|
||||||
|
val smallBatchStatementCount = statistics.prepareStatementCount
|
||||||
|
|
||||||
|
entityManager.clear()
|
||||||
|
statistics.clear()
|
||||||
|
translationService.findDisplayContents(postIds, "ja")
|
||||||
|
val largeBatchStatementCount = statistics.prepareStatementCount
|
||||||
|
|
||||||
|
assertEquals(2L, smallBatchStatementCount)
|
||||||
|
assertEquals(2L, largeBatchStatementCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("실제 레거시 목록 조회는 번역이 없어도 감지와 작업을 만들지 않는다")
|
||||||
|
fun shouldNotPublishOrScheduleWhenLegacyListReadsMissingTranslation() {
|
||||||
|
val post = persistPost(content = "missing translation", languageCode = null, price = 0)
|
||||||
|
val publisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||||
|
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||||
|
val scheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||||
|
translationJobRepository.deleteAll()
|
||||||
|
entityManager.flush()
|
||||||
|
val initialJobCount = translationJobRepository.count()
|
||||||
|
|
||||||
|
val responses = legacyService(
|
||||||
|
sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java),
|
||||||
|
materializer = materializer,
|
||||||
|
publisher = publisher,
|
||||||
|
scheduler = scheduler
|
||||||
|
).getCommunityPostList(
|
||||||
|
creatorId = post.member!!.id!!,
|
||||||
|
memberId = 999L,
|
||||||
|
timezone = "Asia/Seoul",
|
||||||
|
offset = 0,
|
||||||
|
limit = 10,
|
||||||
|
isAdult = true
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("missing translation"), responses.map { it.content })
|
||||||
|
assertEquals(initialJobCount, translationJobRepository.count())
|
||||||
|
Mockito.verifyNoInteractions(publisher, materializer, scheduler)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("레거시 상세는 저장된 현재 번역문을 유료 마스킹 전에 사용한다")
|
||||||
|
fun shouldUseStoredTranslationBeforeLegacyPaidMasking() {
|
||||||
|
val post = persistPost(content = "source paid content", languageCode = "ko", price = 100)
|
||||||
|
creatorCommunityTranslationRepository.save(
|
||||||
|
CreatorCommunityTranslation(
|
||||||
|
creatorCommunityId = post.id!!,
|
||||||
|
locale = "ja",
|
||||||
|
content = "translated paid content",
|
||||||
|
sourceRevision = post.contentRevision,
|
||||||
|
sourceHash = SourceTextNormalizer.hash(post.content),
|
||||||
|
sourceLanguage = "ko"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
|
||||||
|
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||||
|
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, post.id!!))
|
||||||
|
.thenReturn(translationSource(post.id!!, post.content, "ko", post.contentRevision))
|
||||||
|
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
|
||||||
|
Mockito.`when`(materializer.materialize(LanguageTranslationTargetType.CREATOR_COMMUNITY, post.id!!, "ja"))
|
||||||
|
.thenReturn(true)
|
||||||
|
|
||||||
|
val response = legacyService(sourceExtractor, materializer, Mockito.mock(ApplicationEventPublisher::class.java))
|
||||||
|
.getCommunityPostDetail(post.id!!, memberId = 999L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
|
||||||
|
assertEquals("translated paid...", response.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("언어 미확정 레거시 상세는 원문을 반환하고 요청 언어만 감지 이벤트에 전달한다")
|
||||||
|
fun shouldReturnOriginalAndRequestOnlyCurrentLanguageDetectionForLegacyDetail() {
|
||||||
|
val post = persistPost(content = "current original", languageCode = null, price = 0)
|
||||||
|
val sourceExtractor = Mockito.mock(TranslationSourceExtractor::class.java)
|
||||||
|
Mockito.`when`(sourceExtractor.extract(LanguageTranslationTargetType.CREATOR_COMMUNITY, post.id!!)).thenReturn(null)
|
||||||
|
val publisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||||
|
|
||||||
|
val response = legacyService(
|
||||||
|
sourceExtractor,
|
||||||
|
Mockito.mock(TranslationReadModelMaterializer::class.java),
|
||||||
|
publisher
|
||||||
|
).getCommunityPostDetail(post.id!!, memberId = 999L, timezone = "Asia/Seoul", isAdult = true)
|
||||||
|
|
||||||
|
assertEquals("current original", response.content)
|
||||||
|
val eventCaptor = ArgumentCaptor.forClass(LanguageDetectEvent::class.java)
|
||||||
|
Mockito.verify(publisher).publishEvent(eventCaptor.capture())
|
||||||
|
assertEquals("ja", eventCaptor.value.targetLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun legacyService(
|
||||||
|
sourceExtractor: TranslationSourceExtractor,
|
||||||
|
materializer: TranslationReadModelMaterializer,
|
||||||
|
publisher: ApplicationEventPublisher,
|
||||||
|
scheduler: ResourceTranslationJobScheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
|
||||||
|
): CreatorCommunityService {
|
||||||
|
val langContext = LangContext().apply { setLang(Lang.JA) }
|
||||||
|
val translationService = CreatorCommunityTranslationService(
|
||||||
|
creatorCommunityRepository = creatorCommunityRepository,
|
||||||
|
creatorCommunityTranslationRepository = creatorCommunityTranslationRepository,
|
||||||
|
sourceExtractor = sourceExtractor,
|
||||||
|
materializer = materializer,
|
||||||
|
resourceTranslationJobScheduler = scheduler,
|
||||||
|
applicationEventPublisher = publisher,
|
||||||
|
entityManager = entityManager
|
||||||
|
)
|
||||||
|
return CreatorCommunityService(
|
||||||
|
canPaymentService = Mockito.mock(CanPaymentService::class.java),
|
||||||
|
repository = creatorCommunityRepository,
|
||||||
|
blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java),
|
||||||
|
likeRepository = Mockito.mock(CreatorCommunityLikeRepository::class.java),
|
||||||
|
commentRepository = Mockito.mock(CreatorCommunityCommentRepository::class.java),
|
||||||
|
useCanRepository = Mockito.mock(UseCanRepository::class.java),
|
||||||
|
s3Uploader = Mockito.mock(S3Uploader::class.java),
|
||||||
|
objectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build()),
|
||||||
|
audioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java),
|
||||||
|
applicationEventPublisher = publisher,
|
||||||
|
messageSource = SodaMessageSource(),
|
||||||
|
langContext = langContext,
|
||||||
|
homeFollowingNewsPublishService = Mockito.mock(HomeFollowingNewsPublishService::class.java),
|
||||||
|
creatorCommunityTranslationService = translationService,
|
||||||
|
entityManager = entityManager,
|
||||||
|
imageBucket = "test-image-bucket",
|
||||||
|
contentBucket = "test-content-bucket",
|
||||||
|
imageHost = "https://cdn.test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistPost(content: String, languageCode: String?, price: Int): CreatorCommunity {
|
||||||
|
val creator = memberRepository.save(
|
||||||
|
Member(
|
||||||
|
email = "creator-${System.nanoTime()}@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = "creator-${System.nanoTime()}",
|
||||||
|
role = MemberRole.CREATOR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return creatorCommunityRepository.save(
|
||||||
|
CreatorCommunity(
|
||||||
|
content = content,
|
||||||
|
price = price,
|
||||||
|
isCommentAvailable = false,
|
||||||
|
isAdult = false,
|
||||||
|
languageCode = languageCode
|
||||||
|
).apply { member = creator }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun translationSource(
|
||||||
|
postId: Long,
|
||||||
|
content: String,
|
||||||
|
sourceLanguage: String,
|
||||||
|
sourceRevision: Long
|
||||||
|
): TranslationSource {
|
||||||
|
return TranslationSource(
|
||||||
|
resourceType = LanguageTranslationTargetType.CREATOR_COMMUNITY,
|
||||||
|
resourceId = postId,
|
||||||
|
sourceLanguage = sourceLanguage,
|
||||||
|
fields = listOf(TranslationSourceField("content", content)),
|
||||||
|
sourceRevision = sourceRevision
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user