feat(creatorname): 레거시 조회와 검색에 언어별 이름을 적용한다
This commit is contained in:
+328
@@ -0,0 +1,328 @@
|
||||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository
|
||||
import kr.co.vividnext.sodalive.content.order.Order
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesRepository
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.explorer.CreatorRanking
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.rank.RankingRepository
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
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.annotation.Import
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(
|
||||
properties = [
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:legacy-content-owner-name;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import(QueryDslConfig::class, RankingRepository::class, AudioContentCurationQueryRepository::class)
|
||||
class AudioContentRepositoryLocalizedNameTest @Autowired constructor(
|
||||
private val repository: AudioContentRepository,
|
||||
private val seriesRepository: ContentSeriesRepository,
|
||||
private val rankingRepository: RankingRepository,
|
||||
private val curationRepository: AudioContentCurationQueryRepository,
|
||||
private val entityManager: EntityManager,
|
||||
private val jdbcTemplate: JdbcTemplate
|
||||
) {
|
||||
@Test
|
||||
@DisplayName("JA 메인 콘텐츠 조회는 콘텐츠 ID가 아닌 실제 소유자 ID로 크리에이터 이름만 대체한다")
|
||||
fun shouldLocalizeCurrentOwnerNameByCreatorIdWithoutChangingContentTitleOrUserName() {
|
||||
val viewer = saveMember("owner-query-viewer", MemberRole.USER)
|
||||
val creator = saveMember("owner-original", MemberRole.CREATOR)
|
||||
val user = saveMember("user-original", MemberRole.USER)
|
||||
val creatorContent = saveContent(creator, "Original audio title").takeIf { it.id != creator.id }
|
||||
?: saveContent(creator, "Original audio title")
|
||||
val userContent = saveContent(user, "User audio title")
|
||||
saveAlias(creator, "Japanese owner")
|
||||
saveAlias(user, "Must not replace USER")
|
||||
entityManager.clear()
|
||||
assertNotEquals(creator.id, creatorContent.id)
|
||||
|
||||
val items = findLatest(viewer.id!!, "ja").associateBy { it.contentId }
|
||||
|
||||
assertAll(
|
||||
{ assertEquals("Japanese owner", items[creatorContent.id]!!.creatorNickname) },
|
||||
{ assertEquals(creator.id, items[creatorContent.id]!!.creatorId) },
|
||||
{ assertEquals("Original audio title", items[creatorContent.id]!!.title) },
|
||||
{ assertEquals("user-original", items[userContent.id]!!.creatorNickname) },
|
||||
{ assertEquals("User audio title", items[userContent.id]!!.title) },
|
||||
{
|
||||
assertEquals(
|
||||
"owner-original",
|
||||
findLatest(viewer.id!!, "ko").first { it.contentId == creatorContent.id }.creatorNickname
|
||||
)
|
||||
},
|
||||
{
|
||||
assertEquals(
|
||||
"owner-original",
|
||||
findLatest(viewer.id!!, "en").first { it.contentId == creatorContent.id }.creatorNickname
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("메인 콘텐츠 이름 대체는 공개 필터를 유지하고 해제 후 변경된 현재 원본으로 돌아간다")
|
||||
fun shouldPreserveVisibilityFiltersAndFallbackToRenamedOriginalAfterSoftClear() {
|
||||
val viewer = saveMember("visibility-query-viewer", MemberRole.USER)
|
||||
val creator = saveMember("before-rename", MemberRole.CREATOR)
|
||||
val visible = saveContent(creator, "visible title")
|
||||
saveContent(creator, "future title").releaseDate = LocalDateTime.now().plusDays(1)
|
||||
saveContent(creator, "adult title").isAdult = true
|
||||
saveContent(creator, "inactive title").isActive = false
|
||||
saveContent(creator, "unfinished title").duration = null
|
||||
val blockedCreator = saveMember("blocked-owner", MemberRole.CREATOR)
|
||||
saveContent(blockedCreator, "blocked title")
|
||||
entityManager.persist(
|
||||
BlockMember(isActive = true).apply {
|
||||
member = blockedCreator
|
||||
blockedMember = viewer
|
||||
}
|
||||
)
|
||||
saveAlias(creator, "Retained owner alias")
|
||||
saveAlias(blockedCreator, "Blocked owner alias")
|
||||
creator.nickname = "renamed-original"
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
val activeItems = findLatest(viewer.id!!, "ja")
|
||||
assertEquals(listOf(visible.id), activeItems.map { it.contentId })
|
||||
assertEquals("Retained owner alias", activeItems.single().creatorNickname)
|
||||
|
||||
jdbcTemplate.update(
|
||||
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
|
||||
creator.id!!
|
||||
)
|
||||
entityManager.clear()
|
||||
assertEquals("renamed-original", findLatest(viewer.id!!, "ja").single().creatorNickname)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("테마 공유 조회는 요청한 KO·JA 별칭과 기존 제목을 반환한다")
|
||||
fun shouldLocalizeThemeNamesByRequestedLanguageWithoutChangingTitle() {
|
||||
val viewer = saveMember("raw-theme-viewer", MemberRole.USER)
|
||||
val creator = saveMember("raw-theme-original", MemberRole.CREATOR)
|
||||
val content = saveContent(creator, "Unchanged theme title")
|
||||
saveAlias(creator, "Japanese theme owner")
|
||||
saveAlias(creator, "Korean theme owner", Lang.KO)
|
||||
entityManager.clear()
|
||||
|
||||
val names = listOf(
|
||||
Lang.KO to "Korean theme owner",
|
||||
Lang.JA to "Japanese theme owner"
|
||||
)
|
||||
for ((lang, expected) in names) {
|
||||
val item = repository.findByTheme(memberId = viewer.id!!, contentType = ContentType.ALL, lang = lang).single()
|
||||
assertEquals(content.id, item.contentId)
|
||||
assertEquals(expected, item.creatorNickname)
|
||||
assertEquals("Unchanged theme title", item.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("큐레이션 조회는 활성 KO·JA 별칭이 있어도 원본 닉네임을 반환한다")
|
||||
fun shouldKeepCurationNamesRawWithActiveAliases() {
|
||||
val creator = saveMember("raw-curation-original", MemberRole.CREATOR)
|
||||
val curation = AudioContentCuration(title = "curation", description = "description")
|
||||
.also { entityManager.persist(it) }
|
||||
val content = saveContent(creator, "Unchanged curation title").apply { this.curation = curation }
|
||||
entityManager.persist(
|
||||
AudioContentCurationItem().apply {
|
||||
this.curation = curation
|
||||
this.content = content
|
||||
}
|
||||
)
|
||||
saveAlias(creator, "Japanese curation owner")
|
||||
saveAlias(creator, "Korean curation owner", Lang.KO)
|
||||
entityManager.clear()
|
||||
|
||||
val results = listOf(
|
||||
repository.findContentByCurationId(curation.id!!, false, ContentType.ALL),
|
||||
curationRepository.findByCurationId(curation.id!!, "https://cdn.test", contentType = ContentType.ALL)
|
||||
)
|
||||
for (items in results) {
|
||||
assertEquals(content.id, items.single().contentId)
|
||||
assertEquals("raw-curation-original", items.single().creatorNickname)
|
||||
assertEquals("Unchanged curation title", items.single().title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("랭킹 공유 조회는 요청한 KO·JA 별칭과 제목 번역·원본 fallback을 반환한다")
|
||||
fun shouldLocalizeRankingNamesAndTitlesByRequestedLocale() {
|
||||
val viewer = saveMember("raw-rank-viewer", MemberRole.USER)
|
||||
val creator = saveMember("raw-rank-original", MemberRole.CREATOR).apply { profileImage = "profile.png" }
|
||||
val content = saveContent(creator, "Original ranking title")
|
||||
entityManager.persist(CreatorRanking(ranking = 1).apply { member = creator })
|
||||
entityManager.persist(
|
||||
Order(type = OrderType.KEEP).apply {
|
||||
member = viewer
|
||||
this.creator = creator
|
||||
audioContent = content
|
||||
}
|
||||
)
|
||||
entityManager.persist(
|
||||
ContentTranslation(content.id!!, "ja", ContentTranslationPayload("Translated ranking title", "detail", ""))
|
||||
)
|
||||
saveAlias(creator, "Japanese ranking owner")
|
||||
saveAlias(creator, "Korean ranking owner", Lang.KO)
|
||||
// H2 JSON 컬럼에 문자열이 아닌 JSON 객체를 적재한다.
|
||||
jdbcTemplate.update(
|
||||
"update content_translation set rendered_payload = ? format json where content_id = ? and locale = 'ja'",
|
||||
"""{"title":"Translated ranking title","detail":"detail","tags":""}""",
|
||||
content.id!!
|
||||
)
|
||||
entityManager.clear()
|
||||
|
||||
val names = listOf(
|
||||
Triple(Lang.KO, "Korean ranking owner", "Original ranking title"),
|
||||
Triple(Lang.JA, "Japanese ranking owner", "Translated ranking title")
|
||||
)
|
||||
for ((lang, expected, expectedTitle) in names) {
|
||||
assertEquals(expected, rankingRepository.getCreatorRankings(viewer.id, lang).single().nickname)
|
||||
val item = rankingRepository.getAudioContentRanking(
|
||||
memberId = viewer.id,
|
||||
isAdult = false,
|
||||
contentType = ContentType.ALL,
|
||||
startDate = LocalDateTime.now().minusDays(1),
|
||||
endDate = LocalDateTime.now().plusDays(1),
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortType = "판매량",
|
||||
locale = lang.code
|
||||
).single()
|
||||
assertEquals(expected, item.creatorNickname)
|
||||
assertEquals(expectedTitle, item.title)
|
||||
}
|
||||
assertEquals(
|
||||
"Korean ranking owner",
|
||||
rankingRepository.getCreatorRankings(viewer.id).single().nickname
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오리지널 시리즈 공유 조회는 요청한 언어의 제목 번역·원본 fallback을 반환한다")
|
||||
fun shouldLocalizeOriginalSeriesTitlesByRequestedLocale() {
|
||||
val creator = saveMember("raw-series-original", MemberRole.CREATOR)
|
||||
val content = saveContent(creator, "Series content")
|
||||
val genre = SeriesGenre(genre = "genre").also { entityManager.persist(it) }
|
||||
val series = Series(title = "Original series title", introduction = "introduction", isOriginal = true).apply {
|
||||
member = creator
|
||||
this.genre = genre
|
||||
coverImage = "series.png"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
entityManager.persist(
|
||||
SeriesContent().apply {
|
||||
this.series = series
|
||||
this.content = content
|
||||
}
|
||||
)
|
||||
entityManager.persist(
|
||||
SeriesTranslation(series.id!!, "ja", SeriesTranslationPayload("Translated series title", "intro", emptyList()))
|
||||
)
|
||||
entityManager.flush()
|
||||
// H2 JSON 컬럼에 문자열이 아닌 JSON 객체를 적재한다.
|
||||
jdbcTemplate.update(
|
||||
"update series_translation set rendered_payload = ? format json where series_id = ? and locale = 'ja'",
|
||||
"""{"title":"Translated series title","introduction":"intro","keywords":[]}""",
|
||||
series.id!!
|
||||
)
|
||||
entityManager.clear()
|
||||
|
||||
val titles = listOf(
|
||||
"ja" to "Translated series title",
|
||||
"ko" to "Original series title"
|
||||
)
|
||||
for ((locale, expectedTitle) in titles) {
|
||||
val localized = seriesRepository.getOriginalAudioDramaList(
|
||||
imageHost = "https://cdn.test",
|
||||
isAdult = false,
|
||||
contentType = ContentType.ALL,
|
||||
locale = locale
|
||||
).single()
|
||||
assertEquals(expectedTitle, localized.title)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLatest(viewerId: Long, locale: String): List<AudioContentMainItem> {
|
||||
return repository.getLatestContentByTheme(
|
||||
memberId = viewerId,
|
||||
theme = emptyList(),
|
||||
contentType = ContentType.ALL,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
sortType = SortType.NEWEST,
|
||||
isFree = false,
|
||||
isAdult = false,
|
||||
locale = locale
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
return Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
profileImage = "profile.png",
|
||||
role = role
|
||||
).also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveContent(creator: Member, title: String): AudioContent {
|
||||
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
return AudioContent(
|
||||
title = title,
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
releaseDate = LocalDateTime.now().minusDays(1)
|
||||
).apply {
|
||||
member = creator
|
||||
this.theme = theme
|
||||
isActive = true
|
||||
coverImage = "audio.png"
|
||||
duration = "00:10:00"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAlias(member: Member, displayName: String, lang: Lang = Lang.JA) {
|
||||
entityManager.flush()
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, ?, ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
lang.name,
|
||||
displayName
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -114,6 +115,7 @@ class AudioContentServiceTest {
|
||||
applicationEventPublisher = applicationEventPublisher,
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
localizedNameService = originalLocalizedNameService(),
|
||||
contentThemeTranslationRepository = contentThemeTranslationRepository,
|
||||
homeFollowingNewsPublishService = homeFollowingNewsPublishService,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.order.OrderRepository
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.support.LegacyCreatorNameTestConfiguration
|
||||
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.context.annotation.Import
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(properties = ["spring.cache.type=none"])
|
||||
@Import(QueryDslConfig::class, LegacyCreatorNameTestConfiguration::class)
|
||||
class AudioContentCommentNameMetadataTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
private val repository: AudioContentCommentRepository,
|
||||
private val blockRepository: BlockMemberRepository,
|
||||
private val contentRepository: AudioContentRepository,
|
||||
private val orderRepository: OrderRepository,
|
||||
private val localizedNameService: CreatorLocalizedNameService
|
||||
) {
|
||||
@ParameterizedTest
|
||||
@CsvSource("CREATOR, USER", "USER, CREATOR")
|
||||
@DisplayName("답글 알림 제목은 부모 작성자 ID이고 snapshot 발신자 ID와 콘텐츠 제목 인자는 그대로다")
|
||||
fun shouldKeepParentTitleOwnerSeparateFromActualCommenterAndResourceTitle(
|
||||
parentRole: MemberRole,
|
||||
commenterRole: MemberRole
|
||||
) {
|
||||
val parentWriter = Member(nickname = "parent-original", password = "password", role = parentRole)
|
||||
val commenter = Member(nickname = "commenter-original", password = "password", role = commenterRole)
|
||||
val contentOwner = Member(nickname = "content-owner-original", password = "password", role = MemberRole.CREATOR)
|
||||
entityManager.persist(parentWriter)
|
||||
entityManager.persist(commenter)
|
||||
entityManager.persist(contentOwner)
|
||||
val theme = AudioContentTheme(theme = "metadata-theme", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
val content = AudioContent(title = "Unchanged content title", detail = "detail", languageCode = "ko").apply {
|
||||
member = contentOwner
|
||||
this.theme = theme
|
||||
isActive = true
|
||||
entityManager.persist(this)
|
||||
}
|
||||
val parent = AudioContentComment(comment = "parent comment", languageCode = "ko").apply {
|
||||
member = parentWriter
|
||||
audioContent = content
|
||||
entityManager.persist(this)
|
||||
}
|
||||
entityManager.flush()
|
||||
val publisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||
val service = AudioContentCommentService(
|
||||
repository,
|
||||
blockRepository,
|
||||
contentRepository,
|
||||
publisher,
|
||||
orderRepository,
|
||||
SodaMessageSource(),
|
||||
LangContext(),
|
||||
localizedNameService,
|
||||
"https://cdn.test"
|
||||
)
|
||||
|
||||
service.registerComment(commenter, "reply", content.id!!, parent.id!!, false, "ko")
|
||||
|
||||
val captor = ArgumentCaptor.forClass(FcmEvent::class.java)
|
||||
Mockito.verify(publisher).publishEvent(captor.capture())
|
||||
val event = captor.value
|
||||
assertNotEquals(parentWriter.id, commenter.id)
|
||||
assertEquals(parentWriter.id, event.titleMemberId)
|
||||
assertEquals(commenter.id, event.senderMemberId)
|
||||
assertEquals("parent-original", event.title)
|
||||
assertEquals(listOf("Unchanged content title"), event.args)
|
||||
assertEquals(emptyMap<Int, Long>(), event.nameArgumentMemberIds)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user