feat(creatorname): 레거시 조회와 검색에 언어별 이름을 적용한다

This commit is contained in:
2026-10-16 14:04:17 +09:00
parent 995b41e89d
commit eef2e2bf55
66 changed files with 3022 additions and 373 deletions
@@ -18,6 +18,7 @@ import kr.co.vividnext.sodalive.query.recommend.RecommendChannelQueryService
import kr.co.vividnext.sodalive.rank.ContentRankingSortType
import kr.co.vividnext.sodalive.rank.RankingRepository
import kr.co.vividnext.sodalive.rank.RankingService
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -68,6 +69,7 @@ class HomeServiceTest {
rankingRepository = rankingRepository,
explorerQueryRepository = explorerQueryRepository,
langContext = LangContext().apply { setLang(Lang.JA) },
localizedNameService = originalLocalizedNameService(),
memberContentPreferenceService = Mockito.mock(
kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService::class.java
),
@@ -99,7 +101,7 @@ class HomeServiceTest {
Mockito.doReturn(emptyList<Any>()).`when`(liveRoomService)
.getRoomList(null, LiveRoomStatus.NOW, Pageable.ofSize(10), null, timezone)
Mockito.`when`(rankingRepository.getCreatorRankings(null)).thenReturn(emptyList())
Mockito.`when`(rankingRepository.getCreatorRankings(null, Lang.JA)).thenReturn(emptyList())
Mockito.`when`(
contentThemeService.getActiveThemeOfContent(
false,
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.can
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
@@ -22,7 +23,7 @@ class CanServiceTest {
fun setUp() {
repository = mock(CanRepository::class.java)
countryContext = mock(CountryContext::class.java)
service = CanService(repository, countryContext)
service = CanService(repository, countryContext, LangContext())
}
@Test
@@ -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,
@@ -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)
}
}
@@ -0,0 +1,259 @@
package kr.co.vividnext.sodalive.explorer
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
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.profile.CreatorCheers
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberAdapter
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.BeforeEach
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.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.transaction.TestTransaction
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import javax.persistence.EntityManager
@SpringBootTest(
properties = [
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:legacy-creator-name-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@AutoConfigureMockMvc
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@Transactional
class ExplorerLocalizedNameEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc,
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@MockBean
private lateinit var countryContext: CountryContext
@BeforeEach
fun setUp() {
Mockito.doReturn("US").`when`(countryContext).countryCode
}
@Test
@DisplayName("레거시 프로필 이름은 현지화하되 팬톡 작성자·답글은 활성 JA·KO 별칭이 있어도 원본을 유지한다")
fun shouldLocalizeProfileButKeepOriginalFanTalkWriterAndReplyNamesDespiteActiveAliases() {
val viewer = saveMember("profile-viewer", MemberRole.USER)
val creator = saveMember("profile-original", MemberRole.CREATOR).apply { isVisibleDonationRank = false }
val fan = saveMember("fan-original", MemberRole.USER)
val peer = saveMember("deleted_peer-original", MemberRole.CREATOR)
val fanTalk = saveCheers(fan, creator, "fan talk")
saveCheers(peer, creator, "creator fan talk")
saveCheers(creator, creator, "creator reply", parent = fanTalk)
saveAlias(creator, "Japanese profile")
saveAlias(peer, "Japanese peer")
saveAlias(fan, "Must not replace fan")
saveAlias(creator, "Korean profile", lang = "KO")
saveAlias(peer, "Korean peer", lang = "KO")
entityManager.clear()
commitFixture()
mockMvc.perform(
get("/explorer/profile/${creator.id}")
.param("timezone", "Asia/Seoul")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.creator.creatorId").value(creator.id!!))
.andExpect(jsonPath("$.data.creator.nickname").value("Japanese profile"))
.andExpect(jsonPath("$.data.cheers.totalCount").value(2))
.andExpect(jsonPath("$.data.cheers.cheers[0].memberId").value(peer.id!!))
.andExpect(jsonPath("$.data.cheers.cheers[0].nickname").value("peer-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].nickname").value("fan-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].memberId").value(creator.id!!))
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].nickname").value("profile-original"))
listOf("ko", "en").forEach { locale ->
mockMvc.perform(
get("/explorer/profile/${creator.id}")
.param("timezone", "Asia/Seoul")
.header("Accept-Language", locale)
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(
jsonPath("$.data.creator.nickname").value(if (locale == "ko") "Korean profile" else "profile-original")
)
.andExpect(jsonPath("$.data.cheers.cheers[0].nickname").value("peer-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].nickname").value("fan-original"))
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].nickname").value("profile-original"))
}
assertEquals("profile-original", entityManager.find(Member::class.java, creator.id).nickname)
}
@Test
@DisplayName("JA 상세는 원본 변경 후에도 별칭을 유지하고 해제·미등록이면 현재 원본을 반환한다")
fun shouldKeepAliasAfterOriginalRenameAndFallbackAfterSoftClearOrMissingAlias() {
val viewer = saveMember("detail-viewer", MemberRole.USER)
val creator = saveMember("before-rename", MemberRole.CREATOR)
val unregistered = saveMember("unregistered-original", MemberRole.CREATOR)
saveAlias(creator, "Retained profile alias")
creator.nickname = "renamed-original"
entityManager.flush()
entityManager.clear()
commitFixture()
mockMvc.perform(
get("/explorer/profile/${creator.id}/detail")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.nickname").value("Retained profile alias"))
jdbcTemplate.update(
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
creator.id!!
)
entityManager.clear()
mockMvc.perform(
get("/explorer/profile/${creator.id}/detail")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.nickname").value("renamed-original"))
mockMvc.perform(
get("/explorer/profile/${unregistered.id}/detail")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.nickname").value("unregistered-original"))
}
@Test
@DisplayName("통합 검색은 creator 제목·닉네임만 JA 이름으로 바꾸고 콘텐츠·시리즈 제목과 소유자 ID를 구분한다")
fun shouldLocalizeCreatorSearchTitleAndAllOwnerNamesWithoutReplacingContentOrSeriesTitles() {
val viewer = saveMember("unified-viewer", MemberRole.USER)
saveMember("unused-member", MemberRole.USER)
val creator = saveMember("unified-original", MemberRole.CREATOR)
val theme = AudioContentTheme(theme = "unified-theme", image = "theme.png")
entityManager.persist(theme)
val content = AudioContent(
title = "Unchanged content title",
detail = "description",
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)
}
val genre = SeriesGenre(genre = "unified-genre", isAdult = false, isActive = true)
entityManager.persist(genre)
val series = Series(title = "Unchanged series title", introduction = "introduction").apply {
member = creator
this.genre = genre
coverImage = "series.png"
entityManager.persist(this)
}
entityManager.persist(
SeriesContent().apply {
this.series = series
this.content = content
}
)
saveAlias(creator, "Japanese unified creator")
entityManager.clear()
assertNotEquals(creator.id, content.id)
assertNotEquals(creator.id, series.id)
commitFixture()
mockMvc.perform(
get("/search")
.param("keyword", "unified-original")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.creatorList[0].id").value(creator.id!!))
.andExpect(jsonPath("$.data.creatorList[0].title").value("Japanese unified creator"))
.andExpect(jsonPath("$.data.creatorList[0].nickname").value("Japanese unified creator"))
.andExpect(jsonPath("$.data.contentList[0].id").value(content.id!!))
.andExpect(jsonPath("$.data.contentList[0].title").value("Unchanged content title"))
.andExpect(jsonPath("$.data.contentList[0].nickname").value("Japanese unified creator"))
.andExpect(jsonPath("$.data.seriesList[0].id").value(series.id!!))
.andExpect(jsonPath("$.data.seriesList[0].title").value("Unchanged series title"))
.andExpect(jsonPath("$.data.seriesList[0].nickname").value("Japanese unified creator"))
mockMvc.perform(
get("/search/creators")
.param("keyword", "Japanese unified")
.header("Accept-Language", "ko")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.totalCount").value(1))
.andExpect(jsonPath("$.data.items[0].title").value("unified-original"))
.andExpect(jsonPath("$.data.items[0].nickname").value("unified-original"))
}
private fun commitFixture() {
TestTransaction.flagForCommit()
TestTransaction.end()
TestTransaction.start()
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "$nickname.png",
role = role
).also { entityManager.persist(it) }
}
private fun saveCheers(writer: Member, creator: Member, text: String, parent: CreatorCheers? = null): CreatorCheers {
return CreatorCheers(cheers = text, languageCode = "ko").apply {
member = writer
this.creator = creator
this.parent = parent
entityManager.persist(this)
}
}
private fun saveAlias(member: Member, displayName: String, lang: String = "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,
displayName
)
}
}
@@ -0,0 +1,90 @@
package kr.co.vividnext.sodalive.explorer
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
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.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
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.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.jdbc.core.JdbcTemplate
import javax.persistence.EntityManager
@DataJpaTest(properties = ["spring.cache.type=none"])
@Import(QueryDslConfig::class)
class ExplorerQueryRepositoryLocalizedNameTest @Autowired constructor(
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate,
queryFactory: JPAQueryFactory
) {
private val langContext = LangContext().apply { setLang(Lang.KO) }
private val repository = ExplorerQueryRepository(queryFactory, SodaMessageSource(), langContext, "https://cdn.test")
@Test
@DisplayName("KO 채널 검색도 원본 또는 활성 JA 이름을 찾고 self·비활성·USER를 제외한다")
fun shouldSearchOriginalOrActiveJaAliasInKoWithoutBypassingChannelFilters() {
val viewer = saveMember("Comet-viewer")
val original = saveMember("Comet-original")
val aliasOnly = saveMember("another-original")
val inactive = saveMember("Comet-inactive").apply { isActive = false }
val user = saveMember("Comet-user", MemberRole.USER)
listOf(viewer, original, aliasOnly, inactive, user).forEach { saveAlias(it, "Comet alias") }
entityManager.clear()
assertAll(
{
assertEquals(
setOf(original.id, aliasOnly.id),
repository.getSearchChannel("cOmEt", viewer.id!!).map { it.id }.toSet()
)
},
{ assertEquals(2, repository.getSearchChannel("cOmEt", viewer.id!!).size) },
{ assertEquals(listOf(original.id), repository.getSearchChannel("Comet-original", viewer.id!!).map { it.id }) }
)
}
@Test
@DisplayName("채널 검색은 해제한 JA와 EN 이름을 제외하고 동일 행 재활성화 후 별칭을 다시 찾는다")
fun shouldIgnoreSoftClearedAndNonJaChannelAliasesAndSearchReactivatedAlias() {
val viewer = saveMember("channel-viewer", MemberRole.USER)
val creator = saveMember("channel-original")
saveAlias(creator, "ChannelAlias", isActive = false)
saveAlias(creator, "EnglishChannel", lang = "EN")
entityManager.clear()
assertEquals(emptyList<Long>(), repository.getSearchChannel("ChannelAlias", viewer.id!!).map { it.id })
assertEquals(emptyList<Long>(), repository.getSearchChannel("EnglishChannel", viewer.id!!).map { it.id })
assertEquals(listOf(creator.id), repository.getSearchChannel("channel-original", viewer.id!!).map { it.id })
jdbcTemplate.update(
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
creator.id!!
)
assertEquals(listOf(creator.id), repository.getSearchChannel("ChannelAlias", viewer.id!!).map { it.id })
}
private fun saveMember(nickname: String, role: MemberRole = MemberRole.CREATOR): Member {
return Member(email = "$nickname@test.com", password = "password", nickname = nickname, role = role).also {
entityManager.persist(it)
}
}
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang,
displayName,
isActive
)
}
}
@@ -11,12 +11,15 @@ import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
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.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -42,7 +45,28 @@ class ExplorerServiceTest {
}
}
private fun createService(queryRepository: ExplorerQueryRepository): ExplorerService {
@Test
@DisplayName("팬톡 수정 응답은 별칭 대신 작성자 원본 이름을 반환하고 deleted_ 접두사만 제거한다")
fun shouldKeepOriginalWriterNameAndRemoveDeletedPrefixWhenModifyingCheers() {
val queryRepository = Mockito.mock(ExplorerQueryRepository::class.java)
val localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
val service = createService(queryRepository, localizedNameService)
val writer = createMember(id = 10L, nickname = "deleted_writer", role = MemberRole.CREATOR)
val cheers = createCheers(writer, createMember(id = 20L, nickname = "creator", role = MemberRole.CREATOR))
Mockito.doReturn(cheers).`when`(queryRepository).getCheers(101L)
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(10L), Lang.JA)).thenReturn(mapOf(10L to "alias"))
val response = service.modifyCheers(PutWriteCheersRequest(cheersId = 101L, content = "modified"), writer)
assertEquals("writer", response.writerNickname)
assertEquals("modified", response.content)
Mockito.verifyNoInteractions(localizedNameService)
}
private fun createService(
queryRepository: ExplorerQueryRepository,
localizedNameService: CreatorLocalizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
): ExplorerService {
return ExplorerService(
memberService = Mockito.mock(MemberService::class.java),
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
@@ -57,7 +81,8 @@ class ExplorerServiceTest {
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java),
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
messageSource = Mockito.mock(SodaMessageSource::class.java),
langContext = Mockito.mock(LangContext::class.java),
langContext = LangContext().apply { setLang(Lang.JA) },
localizedNameService = localizedNameService,
cloudFrontHost = "https://cdn.test"
)
}
@@ -72,9 +72,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
// then: 비공개 타인 메시지는 제외되고, createdAt desc/id desc 정렬이 적용돼야 한다.
assertEquals(3, list.size)
assertEquals(secretMine.id, list[0].id)
assertEquals(publicTieSecond.id, list[1].id)
assertEquals(publicTieFirst.id, list[2].id)
assertEquals(secretMine.id, list[0].first.id)
assertEquals(publicTieSecond.id, list[1].first.id)
assertEquals(publicTieFirst.id, list[2].first.id)
assertEquals(3, totalCount)
}
@@ -127,9 +127,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
// then: 본인 조회는 비공개 메시지를 포함하고 최신순으로 반환해야 한다.
assertEquals(3, list.size)
assertEquals(recentSecretOther.id, list[0].id)
assertEquals(recentSecretMine.id, list[1].id)
assertEquals(recentPublic.id, list[2].id)
assertEquals(recentSecretOther.id, list[0].first.id)
assertEquals(recentSecretMine.id, list[1].first.id)
assertEquals(recentPublic.id, list[2].first.id)
assertEquals(3, totalCount)
}
@@ -138,7 +138,7 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
fun shouldFetchMemberWithDonationMessages() {
// given: 크리에이터/후원자와 조회 대상 메시지를 준비한다.
val creator = saveMember(nickname = "creator3", role = MemberRole.CREATOR)
val viewer = saveMember(nickname = "viewer3", role = MemberRole.USER)
val viewer = saveMember(nickname = "deleted_viewer3", role = MemberRole.USER)
val message = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
val monthStart = LocalDateTime.now().withDayOfMonth(1).toLocalDate().atStartOfDay()
val nextMonthStart = monthStart.plusMonths(1)
@@ -160,8 +160,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
// then: 조회 결과의 후원자 정보는 세션 없이도 접근 가능해야 한다.
assertEquals(1, list.size)
assertEquals(viewer.id, list[0].member!!.id)
assertEquals("viewer3", list[0].member!!.nickname)
assertEquals(viewer.id, list[0].first.member!!.id)
assertEquals("deleted_viewer3", list[0].first.member!!.nickname)
assertEquals("viewer3", list[0].second)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.explorer.profile.channelDonation
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.common.SodaException
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.member.Member
@@ -97,9 +98,10 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(listOf(message))
).thenReturn(listOf(message to viewer.nickname))
// when: 채널 후원 목록 조회를 실행한다.
val result = service.getChannelDonationList(
@@ -129,7 +131,8 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
@@ -181,9 +184,10 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(listOf(message))
).thenReturn(listOf(message to "withdrawn"))
// when: 채널 후원 목록 조회를 실행한다.
val result = service.getChannelDonationList(
@@ -228,9 +232,10 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(listOf(message))
).thenReturn(listOf(message to viewer.nickname))
// when: 목록 조회를 실행한다.
val result = service.getChannelDonationList(
@@ -275,7 +280,8 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenReturn(emptyList())
@@ -302,7 +308,8 @@ class ChannelDonationServiceTest {
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime()
anyLocalDateTime(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
@@ -150,6 +150,7 @@ class CreatorCommunityServiceTest {
assertEquals(FcmEventType.INDIVIDUAL, event.type)
assertEquals(PushNotificationCategory.COMMUNITY, event.category)
assertEquals("creator.community.fcm.new_comment", event.messageKey)
assertEquals(commenter.id, event.titleMemberId)
assertEquals(commenter.id, event.senderMemberId)
assertEquals(listOf(creator.id!!), event.recipients)
assertEquals(FcmDeepLinkValue.COMMUNITY, event.deepLinkValue)
@@ -94,7 +94,8 @@ class LiveRecommendRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
offset = 0,
limit = 20,
isBlocked = { false }
isBlocked = { false },
lang = Lang.KO
)
assertEquals(2, result.size)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
@@ -28,7 +29,8 @@ class LiveRecommendServiceTest {
repository = repository,
blockMemberRepository = blockMemberRepository,
memberContentPreferenceService = memberContentPreferenceService,
liveRecommendCacheService = liveRecommendCacheService
liveRecommendCacheService = liveRecommendCacheService,
langContext = LangContext()
)
}
@@ -0,0 +1,523 @@
package kr.co.vividnext.sodalive.live.room
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.can.use.UseCanRepository
import kr.co.vividnext.sodalive.fcm.FcmService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendService
import kr.co.vividnext.sodalive.live.reservation.LiveReservation
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
import kr.co.vividnext.sodalive.live.reservation.LiveReservationService
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
import kr.co.vividnext.sodalive.live.room.info.LiveRoomMemberRole
import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService
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.following.CreatorFollowing
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.hibernate.SessionFactory
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.cache.CacheManager
import org.springframework.data.domain.PageRequest
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.time.LocalDateTime
import javax.persistence.EntityManagerFactory
@SpringBootTest(
properties = [
"spring.cache.type=none",
"spring.jpa.properties.hibernate.generate_statistics=true",
"jwt.secret=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZg==",
"agora.app-id=0123456789abcdef0123456789abcdef",
"agora.app-certificate=0123456789abcdef0123456789abcdef"
]
)
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@Transactional
class LiveRoomLocalizedNameIntegrationTest @Autowired constructor(
private val service: LiveRoomService,
private val reservationService: LiveReservationService,
private val reservationRepository: LiveReservationRepository,
private val visitService: LiveRoomVisitService,
private val recommendService: LiveRecommendService,
private val followingRepository: CreatorFollowingRepository,
private val useCanRepository: UseCanRepository,
private val roomRepository: LiveRoomRepository,
private val roomInfoRepository: LiveRoomInfoRedisRepository,
private val memberRepository: MemberRepository,
private val jdbcTemplate: JdbcTemplate,
private val cacheManager: CacheManager,
private val entityManagerFactory: EntityManagerFactory,
private val langContext: LangContext
) {
@MockBean
private lateinit var fcmService: FcmService
@MockBean
private lateinit var s3Uploader: S3Uploader
private val roomIds = mutableListOf<Long>()
private val finishedLiveCacheKeys = mutableListOf<String>()
@BeforeEach
fun setUpRequest() {
RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest()))
}
@AfterEach
fun resetRequestAndRemoveRoomInfo() {
roomIds.forEach { roomInfoRepository.deleteById(it) }
finishedLiveCacheKeys.forEach { cacheManager.getCache("cache_ttl_10_minutes")?.evict(it) }
RequestContextHolder.resetRequestAttributes()
}
@Test
@DisplayName("라이브 info는 모든 멤버 목록을 요청 언어로 복사하고 역할과 공유 Redis 원본을 보존한다")
fun shouldRenderRoomInfoMemberNamesWithoutMutatingSharedRedisMembership() {
val fixture = createFixture()
langContext.setLang(Lang.JA)
val japanese = service.getRoomInfo(fixture.roomId, fixture.viewer)
langContext.setLang(Lang.KO)
val korean = service.getRoomInfo(fixture.roomId, fixture.viewer)
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow()
assertAll(
{ assertEquals("日本-host", japanese.creatorNickname) },
{ assertEquals("日本-host", japanese.speakerList.single().nickname) },
{ assertEquals("日本-manager", japanese.managerList.single().nickname) },
{ assertEquals(listOf("viewer", "fallback"), japanese.listenerList.map { it.nickname }) },
{ assertEquals(LiveRoomMemberRole.SPEAKER, japanese.speakerList.single().role) },
{ assertEquals(LiveRoomMemberRole.MANAGER, japanese.managerList.single().role) },
{ assertEquals("host", korean.creatorNickname) },
{ assertEquals("host", korean.speakerList.single().nickname) },
{ assertEquals("manager", korean.managerList.single().nickname) },
{ assertEquals("host", stored.speakerList.single().nickname) },
{ assertEquals("manager", stored.managerList.single().nickname) },
{ assertEquals("host", memberRepository.findById(fixture.host.id!!).orElseThrow().nickname) }
)
}
@Test
@DisplayName("라이브 상세와 멤버 프로필의 현재 이름은 JA 별칭이며 USER와 비활성 별칭은 원본이다")
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
fun shouldRenderCurrentRoomDetailAndMemberProfileNames() {
val fixture = createFixture()
langContext.setLang(Lang.JA)
val detail = service.getRoomDetail(fixture.roomId, fixture.viewer, "UTC")
val hostProfile = service.getUserProfile(fixture.roomId, fixture.host.id!!, fixture.viewer)
val viewerProfile = service.getUserProfile(fixture.roomId, fixture.viewer.id!!, fixture.viewer)
assertAll(
{ assertEquals("日本-host", detail.manager!!.nickname) },
{ assertEquals("日本-host", detail.participatingUsers.single { it.id == fixture.host.id }.nickname) },
{ assertEquals("日本-manager", detail.participatingUsers.single { it.id == fixture.manager.id }.nickname) },
{ assertEquals("viewer", detail.participatingUsers.single { it.id == fixture.viewer.id }.nickname) },
{ assertEquals("fallback", detail.participatingUsers.single { it.id == fixture.fallback.id }.nickname) },
{ assertEquals("日本-host", hostProfile.nickname) },
{ assertEquals("viewer", viewerProfile.nickname) }
)
}
@Test
@DisplayName("현재 후원 룰렛 메시지 이름은 요청별로 렌더링하며 비밀 필터와 본문 UUID 및 Redis 원본은 보존한다")
fun shouldRenderDonationAndRouletteNamesOnCopiesAfterSecretFiltering() {
val fixture = createFixture()
val roomInfo = roomInfoRepository.findById(fixture.roomId).orElseThrow()
roomInfo.addDonationMessage(fixture.manager.id!!, "manager", false, 10, "manager 후원 본문")
roomInfo.addRouletteMessage(fixture.manager.id!!, "manager", "manager 룰렛 결과")
roomInfo.addDonationMessage(fixture.viewer.id!!, "viewer", false, 10, "viewer 본문")
roomInfo.addDonationMessage(fixture.manager.id!!, "manager", true, 10, "비밀 본문")
roomInfoRepository.save(roomInfo)
langContext.setLang(Lang.JA)
val japanese = service.getDonationMessageList(fixture.roomId, fixture.viewer)
langContext.setLang(Lang.KO)
val korean = service.getDonationMessageList(fixture.roomId, fixture.viewer)
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow().donationMessageList
assertAll(
{ assertEquals(listOf("日本-manager", "日本-manager", "viewer"), japanese.map { it.nickname }) },
{ assertEquals(listOf("manager", "manager", "viewer"), korean.map { it.nickname }) },
{ assertEquals(listOf("manager 후원 본문", "manager 룰렛 결과", "viewer 본문"), japanese.map { it.donationMessage }) },
{ assertEquals(korean.map { it.uuid }, japanese.map { it.uuid }) },
{ assertEquals(listOf("manager", "manager", "viewer", "manager"), stored.map { it.nickname }) },
{ assertEquals(4, stored.size) }
)
}
@Test
@DisplayName("최근 종료 라이브의 현재 이름은 같은 회원의 KO 조회 캐시 이후에도 JA 요청을 따른다")
fun shouldRenderJapaneseFinishedLiveNameAfterKoreanCacheRead() {
val fixture = createFixture()
val room = roomRepository.findById(fixture.roomId).orElseThrow()
room.isActive = false
fixture.host.profileImage = "profile/host.png"
roomRepository.flush()
val cacheKey = "getLatestFinishedLive:${fixture.viewer.id}"
finishedLiveCacheKeys.add(cacheKey)
val cache = requireNotNull(cacheManager.getCache("cache_ttl_10_minutes"))
cache.evict(cacheKey)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
langContext.setLang(Lang.KO)
val korean = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals(1L, currentNameQueryCount())
val roomListQuery = statistics.queries.single { it.contains("from LiveRoom") }
val roomListExecutions = statistics.getQueryStatistics(roomListQuery).executionCount
val cached = cache.get(cacheKey)?.get()
assertNotNull(cached)
assertEquals(1L, roomListExecutions)
assertEquals("host", (cached as List<*>).filterIsInstance<GetLatestFinishedLiveResponse>().single().nickname)
langContext.setLang(Lang.JA)
val nameQueryCountBeforeHit = currentNameQueryCount()
val japanese = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeHit)
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
assertEquals(cached, cache.get(cacheKey)?.get())
fixture.host.nickname = "updated-host"
memberRepository.flush()
jdbcTemplate.update("UPDATE creator_localized_name SET is_active = false WHERE member_id = ?", fixture.host.id!!)
val nameQueryCountBeforeFallback = currentNameQueryCount()
val fallback = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeFallback)
assertEquals("host", korean.nickname)
assertEquals("日本-host", japanese.nickname)
assertEquals("updated-host", fallback.nickname)
assertEquals("host", korean.nickname)
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
assertEquals(cached, cache.get(cacheKey)?.get())
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'EN', 'English host', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
fixture.host.id!!
)
langContext.setLang(Lang.EN)
val nameQueryCountBeforeEnglishHit = currentNameQueryCount()
val english = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
assertEquals("English host", english.nickname)
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeEnglishHit)
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
assertEquals(cached, cache.get(cacheKey)?.get())
}
@Test
@DisplayName("언어 중립 live feed와 종료 캐시는 별칭 해제 후 변경된 현재 원본으로 fallback한다")
fun shouldFallBackToCurrentOriginalWithoutRewritingNeutralRedisNames() {
val fixture = createFixture()
val info = roomInfoRepository.findById(fixture.roomId).orElseThrow()
info.addDonationMessage(fixture.host.id!!, "host", false, 10, "host 본문")
roomInfoRepository.save(info)
fixture.host.nickname = "updated-host"
memberRepository.flush()
jdbcTemplate.update("UPDATE creator_localized_name SET is_active = false WHERE member_id = ?", fixture.host.id!!)
langContext.setLang(Lang.JA)
val response = service.getRoomInfo(fixture.roomId, fixture.viewer)
val messages = service.getDonationMessageList(fixture.roomId, fixture.viewer)
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow()
assertEquals("updated-host", response.creatorNickname)
assertEquals("updated-host", response.speakerList.single().nickname)
assertEquals("updated-host", messages.single().nickname)
assertEquals("host", stored.speakerList.single().nickname)
assertEquals("host", stored.donationMessageList.single().nickname)
assertEquals("host 본문", messages.single().donationMessage)
}
@Test
@DisplayName("예약 생성·목록·상세와 최근 방문자 조회는 실제 크리에이터 소유 ID로 JA 이름을 표시한다")
fun shouldLocalizeReservationAndRecentVisitorNames() {
val fixture = createFixture()
langContext.setLang(Lang.JA)
val reservation = reservationService.makeReservation(
MakeLiveReservationRequest(fixture.roomId, "web", "UTC"),
fixture.viewer.id!!
)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val listed = reservationService.getReservationList(fixture.viewer.id!!, true, "UTC").single()
val detail = reservationService.getReservation(reservation.reservationId, fixture.viewer.id!!, "UTC")
assertEquals(2L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
val room = roomRepository.findById(fixture.roomId).orElseThrow()
visitService.roomVisit(room, fixture.viewer)
visitService.roomVisit(room, fixture.manager)
statistics.clear()
val visitor = visitService.getRecentVisitRoomUsers(fixture.viewer).single()
assertEquals(2L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
assertEquals("日本-host", reservation.nickname)
assertEquals("日本-host", listed.masterNickname)
assertEquals("日本-host", detail.masterNickname)
assertEquals(fixture.manager.id, visitor.id)
assertEquals("日本-manager", visitor.nickname)
langContext.setLang(Lang.KO)
assertEquals("manager", visitService.getRecentVisitRoomUsers(fixture.viewer).single().nickname)
}
@Test
@DisplayName("추천과 팔로잉의 on-air 및 off-air 분기는 요청별 이름을 표시하고 count와 순서를 보존한다")
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
fun shouldLocalizeRecommendedAndFollowingChannels() {
val fixture = createFixture()
followingRepository.saveAndFlush(
CreatorFollowing().apply {
member = fixture.viewer
creator = fixture.host
}
)
followingRepository.saveAndFlush(
CreatorFollowing().apply {
member = fixture.viewer
creator = fixture.manager
}
)
langContext.setLang(Lang.JA)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val recommended = recommendService.getRecommendChannelList(null).associateBy { it.creatorId }
val aliasQueries = statistics.queries.filter {
Regex("""(?is)^select\s+\w+\.memberId,\s*\w+\.displayName\s+from\s+CreatorLocalizedNameEntity\b""")
.containsMatchIn(it)
}
val aliasQueryCount = aliasQueries.sumOf { statistics.getQueryStatistics(it).executionCount }
val queryCount = statistics.queryExecutionCount
val following = recommendService.getFollowingChannelList(fixture.viewer).associateBy { it.creatorId }
val all = recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20))
assertEquals("日本-host", recommended.getValue(fixture.host.id!!).nickname)
assertEquals("日本-manager", recommended.getValue(fixture.manager.id!!).nickname)
assertEquals("fallback", recommended.getValue(fixture.fallback.id!!).nickname)
assertEquals("日本-host", following.getValue(fixture.host.id!!).nickname)
assertEquals("日本-manager", following.getValue(fixture.manager.id!!).nickname)
assertEquals(2, all.totalCount)
assertEquals(setOf("日本-host", "日本-manager"), all.items.map { it.nickname }.toSet())
langContext.setLang(Lang.KO)
assertEquals(
setOf("host", "manager"),
recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20)).items.map { it.nickname }.toSet()
)
assertEquals(0L, aliasQueryCount, "Standalone alias SELECTs (total queries=$queryCount): $aliasQueries")
assertNoStandaloneNameQueries()
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'EN', 'English host', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
fixture.host.id!!
)
langContext.setLang(Lang.EN)
assertEquals(
setOf("English host", "manager"),
recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20)).items.map { it.nickname }.toSet()
)
assertNoStandaloneNameQueries()
}
@Test
@DisplayName("후원과 하트 집계는 donor ID로 이름만 바꾸고 USER·금액·비밀 합계와 공개 DTO 필드를 보존한다")
fun shouldLocalizeDonationAndHeartDonorNamesWithoutChangingAmounts() {
val fixture = createFixture()
val room = roomRepository.findById(fixture.roomId).orElseThrow()
listOf(CanUsage.DONATION, CanUsage.HEART).forEach { usage ->
useCanRepository.saveAndFlush(
UseCan(usage, 10, 2).apply {
member = fixture.manager
this.room = room
}
)
useCanRepository.saveAndFlush(
UseCan(usage, 5, 1).apply {
member = fixture.viewer
this.room = room
}
)
}
langContext.setLang(Lang.JA)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val donations = service.getDonationStatus(fixture.roomId, fixture.host.id!!)
val hearts = service.getHeartList(fixture.roomId)
assertEquals(3L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
assertEquals(listOf("日本-manager", "viewer"), donations.donationList.map { it.nickname })
assertEquals(listOf("日本-manager", "viewer"), hearts.heartList.map { it.nickname })
assertEquals(18, donations.totalCan)
assertEquals(0, donations.totalSecretCan)
assertEquals(18, hearts.totalHeart)
assertEquals(2, hearts.totalCount)
}
@Test
@DisplayName("진행·날짜별 예약·예정 라이브 목록은 EN 이름을 조인하고 USER·미등록·비활성 소유자를 유지한다")
fun shouldJoinEnglishNamesInEveryDatabaseRoomListWithoutMutatingOwners() {
val fixture = createFixture()
val owners = listOf(fixture.host, fixture.manager, fixture.viewer, fixture.fallback)
listOf(fixture.host, fixture.viewer, fixture.fallback).forEach {
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'EN', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
it.id!!,
"EN-${it.nickname}",
it != fixture.fallback
)
}
val rooms = listOf(roomRepository.findById(fixture.roomId).orElseThrow()) + owners.drop(1).map { owner ->
roomRepository.saveAndFlush(
LiveRoom("${owner.nickname} room", "notice", LocalDateTime.now(), 10, "cover.png", isAdult = false).apply {
member = owner
channelName = "live_${owner.id}"
}
)
}
val expected = mapOf(
fixture.host.id to "EN-host",
fixture.manager.id to "manager",
fixture.viewer.id to "viewer",
fixture.fallback.id to "fallback"
)
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val current = roomRepository.getLiveRoomListNow(0, 20, "UTC", null, false, true, null, Lang.EN)
assertEquals(expected, current.associate { it.room.member!!.id to it.creatorNickname })
assertEquals(1L, statistics.queryExecutionCount)
owners.forEach { owner ->
reservationRepository.saveAndFlush(
LiveReservation().apply {
member = owner
room = rooms.first()
}
)
}
statistics.clear()
val bookers = reservationRepository.getReservationList(fixture.roomId, Lang.EN)
assertEquals(expected, bookers.associate { it.id to it.nickname })
assertEquals(1L, statistics.queryExecutionCount)
statistics.clear()
val reservation = reservationRepository.getReservationListByMemberId(fixture.viewer.id!!, true, Lang.EN).single()
val reserved = reservationRepository.getReservationByReservationAndMemberId(reservation.id, fixture.viewer.id!!, Lang.EN)
assertEquals("EN-host", reservation.masterNickname)
assertEquals("EN-host", reserved!!.masterNickname)
assertEquals(2L, statistics.queryExecutionCount)
val date = LocalDateTime.now().plusDays(1).withHour(12).withMinute(0)
rooms.forEach {
it.channelName = null
it.beginDateTime = date
}
roomRepository.flush()
statistics.clear()
val dated = roomRepository.getLiveRoomListReservationWithDate(
date.toLocalDate().atStartOfDay(),
0,
20,
null,
false,
true,
null,
Lang.EN
)
val upcoming = roomRepository.getLiveRoomListReservationWithoutDate("UTC", null, false, true, null, Lang.EN)
assertEquals(expected, dated.associate { it.room.member!!.id to it.creatorNickname })
assertEquals(expected, upcoming.associate { it.room.member!!.id to it.creatorNickname })
assertEquals(2L, statistics.queryExecutionCount)
assertEquals(listOf("host", "manager", "viewer", "fallback"), owners.map { it.nickname })
assertNoStandaloneNameQueries()
}
private fun assertNoStandaloneNameQueries() {
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
val nameQueries = statistics.queries.filter {
Regex("""(?is)\bfrom\s+CreatorLocalizedNameEntity\b""").containsMatchIn(it) ||
Regex("""(?is)^select\s+\w+\.id,\s*coalesce\([^)]*\)\s+from\s+Member\b""").containsMatchIn(it)
}
assertEquals(0L, nameQueries.sumOf { statistics.getQueryStatistics(it).executionCount }, nameQueries.joinToString("\n"))
}
private fun currentNameQueryCount(): Long {
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
return statistics.queries.filter { it.contains("from Member") && it.contains("CreatorLocalizedNameEntity") }
.sumOf { statistics.getQueryStatistics(it).executionCount }
}
private fun createFixture(): Fixture {
val host = savedMember("host", MemberRole.CREATOR)
val manager = savedMember("manager", MemberRole.CREATOR)
val viewer = savedMember("viewer", MemberRole.USER)
val fallback = savedMember("fallback", MemberRole.CREATOR, activeAlias = false)
val room = roomRepository.saveAndFlush(
LiveRoom(
title = "room title",
notice = "notice",
beginDateTime = LocalDateTime.now().minusMinutes(1),
numberOfPeople = 10,
coverImage = "cover.png",
isAdult = false
).apply {
member = host
channelName = "localized_live_${host.id}"
}
)
roomIds.add(room.id!!)
roomInfoRepository.save(
LiveRoomInfo(room.id!!).apply {
addSpeaker(host, "https://cdn.test")
addManager(manager, "https://cdn.test")
addListener(viewer, "https://cdn.test")
addListener(fallback, "https://cdn.test")
}
)
return Fixture(room.id!!, host, manager, viewer, fallback)
}
private fun savedMember(nickname: String, role: MemberRole, activeAlias: Boolean = true): Member {
val member = memberRepository.saveAndFlush(
Member(
email = "$nickname@localized-live.test",
password = "pw",
nickname = nickname,
profileImage = "profile/$nickname.png",
role = role,
isVisibleDonationRank = false
)
)
jdbcTemplate.update(
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"VALUES (?, 'JA', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
"日本-$nickname",
activeAlias
)
return member
}
private data class Fixture(val roomId: Long, val host: Member, val manager: Member, val viewer: Member, val fallback: Member)
}
@@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
import kr.co.vividnext.sodalive.fcm.PushTokenRepository
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.live.reservation.LiveReservationRepository
@@ -30,6 +31,7 @@ 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.member.contentpreference.ViewerContentPreference
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
@@ -106,6 +108,7 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
menuService = menuService,
messageSource = messageSource,
langContext = langContext,
localizedNameService = mock<CreatorLocalizedNameService>(),
repository = repository,
rouletteRepository = rouletteRepository,
roomInfoRepository = roomInfoRepository,
@@ -152,7 +155,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 100L,
isCreator = false,
isAdult = true,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
).thenReturn(emptyList())
@@ -172,7 +176,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 100L,
isCreator = false,
isAdult = true,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
}
@@ -187,7 +192,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = null,
isCreator = false,
isAdult = true,
effectiveGender = null
effectiveGender = null,
lang = Lang.KO
)
).thenReturn(emptyList())
@@ -207,7 +213,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = null,
isCreator = false,
isAdult = true,
effectiveGender = null
effectiveGender = null,
lang = Lang.KO
)
}
@@ -222,7 +229,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 200L,
isCreator = false,
isAdult = false,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
).thenReturn(emptyList())
@@ -240,7 +248,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
memberId = 200L,
isCreator = false,
isAdult = false,
effectiveGender = Gender.NONE
effectiveGender = Gender.NONE,
lang = Lang.KO
)
}
@@ -31,6 +31,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
@@ -70,6 +71,7 @@ class LiveRoomServiceTest {
menuService = mock<LiveRoomMenuService>(),
messageSource = mock<SodaMessageSource>(),
langContext = LangContext(),
localizedNameService = mock<CreatorLocalizedNameService>(),
repository = repository,
rouletteRepository = rouletteRepository,
roomInfoRepository = roomInfoRepository,
@@ -0,0 +1,108 @@
package kr.co.vividnext.sodalive.member
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
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.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.jdbc.core.JdbcTemplate
import javax.persistence.EntityManager
@DataJpaTest(properties = ["spring.cache.type=none"])
@Import(QueryDslConfig::class)
class MemberRepositoryLocalizedNameTest @Autowired constructor(
private val repository: MemberRepository,
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@Test
@DisplayName("크리에이터 요청의 회원 검색은 USER 원본 매칭과 CREATOR 별칭을 함께 유지한다")
fun shouldRetainUserOriginalMatchesAndAddOnlyCreatorAliasesForCreatorRequester() {
val requester = saveMember("needle-requester", MemberRole.CREATOR)
val creator = saveMember("creator-original", MemberRole.CREATOR)
val user = saveMember("needle-user", MemberRole.USER)
val aiUser = saveMember("ai-user-original", MemberRole.USER).apply { memberKind = MemberKind.AI_CHARACTER }
val admin = saveMember("needle-admin", MemberRole.ADMIN)
val agent = saveMember("needle-agent", MemberRole.AGENT)
val bot = saveMember("needle-bot", MemberRole.BOT)
listOf(requester, creator, user, aiUser, admin, agent, bot).forEach { saveAlias(it, "needle-alias") }
entityManager.clear()
val matches = repository.findByNicknameAndOtherCondition("NeEdLe", requester)
assertEquals(setOf(creator.id, user.id), matches.map { it.id }.toSet())
assertEquals(2, matches.size)
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("creator-original", requester).map { it.id })
}
@Test
@DisplayName("USER 요청의 회원 검색은 기존 USER·self·관리 역할 제외 조건을 별칭에도 적용한다")
fun shouldPreserveUserRequesterRoleAndSelfExclusionsForAliases() {
val requester = saveMember("needle-viewer", MemberRole.USER)
val creator = saveMember("creator-original", MemberRole.CREATOR)
val user = saveMember("needle-user", MemberRole.USER)
val admin = saveMember("needle-admin", MemberRole.ADMIN)
val agent = saveMember("needle-agent", MemberRole.AGENT)
val bot = saveMember("needle-bot", MemberRole.BOT)
listOf(requester, creator, user, admin, agent, bot).forEach { saveAlias(it, "needle-alias") }
entityManager.clear()
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("needle", requester).map { it.id })
}
@Test
@DisplayName("비활성 JA와 다른 언어 이름은 회원 별칭 검색에서 제외하고 현재 원본은 유지한다")
fun shouldExcludeInactiveAndNonJaAliasesWhileRetainingCurrentOriginalNickname() {
val requester = saveMember("member-clear-viewer", MemberRole.CREATOR)
val creator = saveMember("before-rename", MemberRole.CREATOR)
saveAlias(creator, "RetainedAlias", isActive = false)
saveAlias(creator, "EnglishAlias", lang = "EN")
creator.nickname = "after-rename"
entityManager.flush()
entityManager.clear()
assertEquals(emptyList<Long>(), repository.findByNicknameAndOtherCondition("RetainedAlias", requester).map { it.id })
assertEquals(emptyList<Long>(), repository.findByNicknameAndOtherCondition("EnglishAlias", requester).map { it.id })
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("after-rename", requester).map { it.id })
jdbcTemplate.update(
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
creator.id!!
)
assertAll(
{
assertEquals(
listOf(creator.id),
repository.findByNicknameAndOtherCondition("RetainedAlias", requester).map { it.id }
)
},
{
assertEquals(
listOf(creator.id),
repository.findByNicknameAndOtherCondition("after-rename", requester).map { it.id }
)
}
)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return Member(email = "$nickname@test.com", password = "password", nickname = nickname, role = role).also {
entityManager.persist(it)
}
}
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang,
displayName,
isActive
)
}
}
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -62,6 +63,7 @@ class MemberServiceCacheEvictionTest {
authenticationManagerBuilder = mock(),
messageSource = SodaMessageSource(),
langContext = LangContext(),
localizedNameService = originalLocalizedNameService(),
countryContext = CountryContext(),
memberContentPreferenceService = mock<MemberContentPreferenceService>(),
objectMapper = ObjectMapper(),
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.member.stipulation.Stipulation
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
import kr.co.vividnext.sodalive.member.stipulation.StipulationIds
import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
@@ -71,6 +72,7 @@ class MemberServiceContentPreferenceTest {
authenticationManagerBuilder = mock(),
messageSource = SodaMessageSource(),
langContext = LangContext(),
localizedNameService = originalLocalizedNameService(),
countryContext = CountryContext(),
memberContentPreferenceService = memberContentPreferenceService,
objectMapper = ObjectMapper(),
@@ -250,7 +250,7 @@ class OsivLazyLoadingRegressionTest @Autowired constructor(
val creators = rankingRepository.getCreatorRankings(memberId = null)
val response = creators.single().toExplorerSectionCreator("https://cdn.test")
val response = creators.single()
assertEquals("#creator-tag", response.tags)
}
@@ -0,0 +1,297 @@
package kr.co.vividnext.sodalive.search
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
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.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.member.tag.CreatorTag
import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag
import org.hibernate.SessionFactory
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
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.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.jpa.properties.hibernate.generate_statistics=true"])
@Import(QueryDslConfig::class)
class SearchRepositoryLocalizedNameTest @Autowired constructor(
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate,
queryFactory: JPAQueryFactory
) {
private val repository = SearchRepository(queryFactory, "https://cdn.test")
@Test
@DisplayName("크리에이터 검색은 원본과 활성 JA 별칭을 함께 찾고 중복 별칭도 회원별로 한 번씩 센다")
fun shouldSearchOriginalOrActiveJaAliasWithDistinctOwnersAndMatchingCount() {
val viewer = saveMember("search-viewer", MemberRole.USER)
val original = saveMember("Orion-original")
val aliasOnly = saveMember("second-original")
saveAlias(original, "Orion Japanese")
saveAlias(aliasOnly, "Orion Japanese")
entityManager.clear()
val items = repository.searchCreatorList("oRiOn", viewer.id!!, 0, 20)
val page = repository.searchCreatorList("oRiOn", viewer.id!!, 1, 1)
assertAll(
{ assertEquals(2, repository.searchCreatorTotalCount("oRiOn", viewer.id!!)) },
{ assertEquals(listOf(aliasOnly.id, original.id), items.map { it.id }) },
{ assertEquals(listOf(original.id), page.map { it.id }) },
{
assertEquals(
listOf(original.id),
repository.searchCreatorList("Orion-original", viewer.id!!, 0, 20).map { it.id }
)
}
)
}
@Test
@DisplayName("별칭 검색은 회원 활성·역할·태그 활성·양방향 차단 필터를 우회하지 않는다")
fun shouldPreserveCreatorVisibilityTagAndBidirectionalBlockFiltersForAliasMatches() {
val viewer = saveMember("filter-viewer", MemberRole.USER)
val visible = saveMember("visible-original")
val inactive = saveMember("inactive-original").apply { isActive = false }
val user = saveMember("user-original", MemberRole.USER)
val blockedByViewer = saveMember("blocked-original")
val blockingViewer = saveMember("blocking-original")
val inactiveTagOwner = saveMember("inactive-tag-original")
saveBlock(viewer, blockedByViewer)
saveBlock(blockingViewer, viewer)
val inactiveTag = CreatorTag(tag = "inactive-tag", isActive = false)
entityManager.persist(inactiveTag)
entityManager.persist(MemberCreatorTag(member = inactiveTagOwner, tag = inactiveTag))
listOf(visible, inactive, user, blockedByViewer, blockingViewer, inactiveTagOwner).forEach {
saveAlias(it, "AliasVisibility")
}
entityManager.clear()
assertAll(
{ assertEquals(1, repository.searchCreatorTotalCount("AliasVisibility", viewer.id!!)) },
{
assertEquals(
listOf(visible.id),
repository.searchCreatorList("AliasVisibility", viewer.id!!, 0, 20).map { it.id }
)
}
)
}
@Test
@DisplayName("비활성 JA와 다른 언어 별칭은 제외하고 원본 검색과 같은 행 재활성화는 유지한다")
fun shouldExcludeSoftClearedAndNonJaAliasesWithoutLosingOriginalSearch() {
val viewer = saveMember("clear-viewer", MemberRole.USER)
val creator = saveMember("current-original")
val content = saveContent(creator, "clear content title")
val series = saveSeries(creator, "clear series title", content)
saveAlias(creator, "ClearedAlias", isActive = false)
saveAlias(creator, "EnglishAlias", lang = "EN")
entityManager.clear()
assertEquals(0, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!))
assertEquals(emptyList<Long>(), repository.searchCreatorList("ClearedAlias", viewer.id!!, 0, 20).map { it.id })
assertEquals(0, repository.searchCreatorTotalCount("EnglishAlias", viewer.id!!))
assertEquals(1, repository.searchCreatorTotalCount("current-original", viewer.id!!))
assertEquals(0, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
assertEquals(0, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
assertEquals(1, repository.searchContentTotalCount("current-original", viewer.id!!, false, ContentType.ALL))
assertEquals(1, repository.searchSeriesTotalCount("current-original", viewer.id!!, false, ContentType.ALL))
repeat(2) {
jdbcTemplate.update(
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
creator.id!!
)
assertAll(
{ assertEquals(1, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!)) },
{ assertEquals(1, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL)) },
{
assertEquals(
listOf(content.id),
repository.searchContentList("ClearedAlias", viewer.id!!, false, ContentType.ALL, 0, 20).map { it.id }
)
},
{
assertEquals(
listOf(series.id),
repository.searchSeriesList("ClearedAlias", viewer.id!!, false, ContentType.ALL, 0, 20).map { it.id }
)
},
{
assertEquals(
listOf(creator.id),
repository.searchCreatorList("ClearedAlias", viewer.id!!, 0, 20).map { it.id }
)
}
)
jdbcTemplate.update(
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
creator.id!!
)
assertEquals(0, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!))
assertEquals(0, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
assertEquals(0, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
}
}
@Test
@DisplayName("콘텐츠 검색은 소유자 별칭을 찾되 성인·활성·완성·한정·차단 필터와 콘텐츠 제목을 보존한다")
fun shouldSearchContentOwnerAliasWithMatchingCountAndExistingContentFilters() {
val viewer = saveMember("content-search-viewer", MemberRole.USER)
val creator = saveMember("audio-owner-original")
val visible = saveContent(creator, "Unchanged audio title")
saveContent(creator, "adult audio").isAdult = true
saveContent(creator, "inactive audio").isActive = false
saveContent(creator, "unfinished audio").duration = null
saveContent(creator, "limited audio", limited = 5)
val blocked = saveMember("blocked-audio-owner")
saveContent(blocked, "blocked audio")
saveBlock(viewer, blocked)
saveAlias(creator, "AudioAlias")
saveAlias(creator, "English Audio Owner", lang = "EN")
saveAlias(blocked, "AudioAlias")
entityManager.clear()
val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val items = repository.searchContentList("AudioAlias", viewer.id!!, false, ContentType.ALL, 0, 20, Lang.JA)
val aliasQueries = statistics.queries.filter {
Regex("""(?is)^select\s+\w+\.memberId,\s*\w+\.displayName\s+from\s+CreatorLocalizedNameEntity\b""")
.containsMatchIn(it)
}
val aliasQueryCount = aliasQueries.sumOf { statistics.getQueryStatistics(it).executionCount }
val queryCount = statistics.queryExecutionCount
assertAll(
{ assertEquals(1, repository.searchContentTotalCount("AudioAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(listOf(visible.id), items.map { it.id }) },
{ assertEquals(listOf("Unchanged audio title"), items.map { it.title }) },
{ assertEquals(listOf("AudioAlias"), items.map { it.nickname }) },
{ assertEquals(1, repository.searchContentTotalCount("audio-owner-original", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchContentTotalCount("Unchanged audio title", viewer.id!!, false, ContentType.ALL)) }
)
assertEquals(0L, aliasQueryCount, "Standalone alias SELECTs (total queries=$queryCount): $aliasQueries")
val englishItems = repository.searchContentList("AudioAlias", viewer.id!!, false, ContentType.ALL, 0, 20, Lang.EN)
assertEquals(listOf("English Audio Owner"), englishItems.map { it.nickname })
assertEquals(0, repository.searchContentTotalCount("English Audio Owner", viewer.id!!, false, ContentType.ALL))
}
@Test
@DisplayName("시리즈 검색은 시리즈 소유자 별칭을 찾고 콘텐츠 조인 중복과 기존 공개 필터를 보존한다")
fun shouldSearchSeriesOwnerAliasWithoutDuplicatingJoinedContentsOrChangingTitles() {
val viewer = saveMember("series-search-viewer", MemberRole.USER)
val creator = saveMember("series-owner-original")
val audioOwner = saveMember("different-audio-owner")
val visible = saveSeries(creator, "Unchanged series title", saveContent(audioOwner, "first episode"))
saveSeriesContent(visible, saveContent(audioOwner, "second episode"))
saveSeries(creator, "adult series", saveContent(creator, "public episode")).isAdult = true
saveSeries(creator, "adult episode series", saveContent(creator, "adult episode").apply { isAdult = true })
saveSeries(creator, "inactive series", saveContent(creator, "active episode")).isActive = false
saveSeries(creator, "unfinished series", saveContent(creator, "unfinished episode").apply { duration = null })
val blocked = saveMember("blocked-series-owner")
saveSeries(blocked, "blocked series", saveContent(blocked, "blocked episode"))
saveBlock(blocked, viewer)
saveAlias(creator, "SeriesAlias")
saveAlias(audioOwner, "EpisodeOwnerAlias")
saveAlias(blocked, "SeriesAlias")
entityManager.clear()
val items = repository.searchSeriesList("SeriesAlias", viewer.id!!, false, ContentType.ALL, 0, 20)
assertAll(
{ assertEquals(1, repository.searchSeriesTotalCount("SeriesAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(listOf(visible.id), items.map { it.id }) },
{ assertEquals(listOf("Unchanged series title"), items.map { it.title }) },
{ assertEquals(0, repository.searchSeriesTotalCount("EpisodeOwnerAlias", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchSeriesTotalCount("series-owner-original", viewer.id!!, false, ContentType.ALL)) },
{ assertEquals(1, repository.searchSeriesTotalCount("Unchanged series title", viewer.id!!, false, ContentType.ALL)) }
)
}
private fun saveMember(nickname: String, role: MemberRole = MemberRole.CREATOR): Member {
return Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "$nickname.png",
role = role
).also { entityManager.persist(it) }
}
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
lang,
displayName,
isActive
)
}
private fun saveBlock(member: Member, blockedMember: Member) {
entityManager.persist(
BlockMember(isActive = true).apply {
this.member = member
this.blockedMember = blockedMember
}
)
}
private fun saveContent(creator: Member, title: String, limited: Int? = null): AudioContent {
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
entityManager.persist(theme)
return AudioContent(
title = title,
detail = "description",
languageCode = "ko",
releaseDate = LocalDateTime.now().minusDays(1),
limited = limited
).apply {
member = creator
this.theme = theme
isActive = true
coverImage = "audio.png"
duration = "00:10:00"
entityManager.persist(this)
}
}
private fun saveSeries(creator: Member, title: String, content: AudioContent): Series {
val genre = SeriesGenre(genre = "genre-$title", isAdult = false, isActive = true)
entityManager.persist(genre)
return Series(title = title, introduction = "introduction", languageCode = "ko").apply {
member = creator
this.genre = genre
coverImage = "series.png"
entityManager.persist(this)
saveSeriesContent(this, content)
}
}
private fun saveSeriesContent(series: Series, content: AudioContent) {
entityManager.persist(
SeriesContent().apply {
this.series = series
this.content = content
}
)
}
}
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.search
import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
@@ -9,7 +10,7 @@ import org.mockito.Mockito
class SearchServiceTest {
private val repository: SearchRepository = Mockito.mock(SearchRepository::class.java)
private val service = SearchService(repository)
private val service = SearchService(repository, LangContext())
@Test
@DisplayName("콘텐츠 검색은 전달받은 isAdult 값을 그대로 사용한다")
@@ -132,6 +132,7 @@ class LegacyCommunityPostCharacterizationTest {
assertEquals(FcmEventType.CHANGE_NOTICE, event.type)
assertEquals(PushNotificationCategory.COMMUNITY, event.category)
assertEquals("creator.community.fcm.new_post", event.messageKey)
assertEquals(creator.id, event.titleMemberId)
assertEquals(creator.id, event.creatorId)
assertEquals(FcmDeepLinkValue.COMMUNITY, event.deepLinkValue)
assertEquals(creator.id, event.deepLinkId)
@@ -22,6 +22,7 @@ import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
@@ -42,6 +43,7 @@ class LegacyFanTalkReplyCharacterizationTest {
private lateinit var cheersRepository: CreatorCheersRepository
private lateinit var applicationEventPublisher: ApplicationEventPublisher
private lateinit var messageSource: SodaMessageSource
private lateinit var localizedNameService: CreatorLocalizedNameService
private lateinit var service: ExplorerService
@BeforeEach
@@ -51,6 +53,7 @@ class LegacyFanTalkReplyCharacterizationTest {
cheersRepository = Mockito.mock(CreatorCheersRepository::class.java)
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
messageSource = Mockito.mock(SodaMessageSource::class.java)
localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
service = ExplorerService(
memberService = memberService,
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
@@ -66,6 +69,7 @@ class LegacyFanTalkReplyCharacterizationTest {
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
messageSource = messageSource,
langContext = LangContext().apply { setLang(Lang.KO) },
localizedNameService = localizedNameService,
cloudFrontHost = "https://cdn.test"
)
}
@@ -177,6 +181,8 @@ class LegacyFanTalkReplyCharacterizationTest {
val root = createCheers(id = 300L, content = "root", member = creator, creator = creator)
Mockito.`when`(queryRepository.getMember(creator.id!!)).thenReturn(creator)
Mockito.`when`(queryRepository.getCreatorCheers(root.id!!)).thenReturn(root)
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(creator.id!!), Lang.KO))
.thenReturn(mapOf(creator.id!! to "Korean creator alias"))
var nextId = 301L
Mockito.`when`(cheersRepository.save(Mockito.any(CreatorCheers::class.java))).thenAnswer { invocation ->
invocation.getArgument<CreatorCheers>(0).also { cheers ->
@@ -185,7 +191,7 @@ class LegacyFanTalkReplyCharacterizationTest {
}
}
service.writeCheers(
val response = service.writeCheers(
PostWriteCheersRequest(root.id, creator.id!!, "first reply", "ko"),
creator
)
@@ -198,6 +204,8 @@ class LegacyFanTalkReplyCharacterizationTest {
Mockito.verify(cheersRepository, Mockito.times(2)).save(cheersCaptor.capture())
assertEquals(listOf("first reply", "second reply"), cheersCaptor.allValues.map { it.cheers })
assertTrue(cheersCaptor.allValues.all { it.parent === root })
assertEquals("duplicate-creator", response.writerNickname)
Mockito.verifyNoInteractions(localizedNameService)
}
@Test
@@ -222,6 +230,8 @@ class LegacyFanTalkReplyCharacterizationTest {
Mockito.`when`(queryRepository.getMember(creator.id!!)).thenReturn(creator)
Mockito.`when`(memberService.isBlocked(writer.id!!, creator.id!!)).thenReturn(true)
Mockito.`when`(messageSource.getMessage("explorer.creator.blocked_cheers", Lang.KO)).thenReturn("blocked %s")
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(creator.id!!), Lang.KO))
.thenReturn(mapOf(creator.id!! to "Korean blocked alias"))
val exception = assertThrows(SodaException::class.java) {
service.writeCheers(PostWriteCheersRequest(creatorId = creator.id!!, content = "reply"), writer)
@@ -229,6 +239,7 @@ class LegacyFanTalkReplyCharacterizationTest {
assertNull(exception.messageKey)
assertEquals("blocked blocked-creator", exception.message)
Mockito.verifyNoInteractions(localizedNameService)
Mockito.verifyNoInteractions(cheersRepository, applicationEventPublisher)
}
@@ -0,0 +1,467 @@
package kr.co.vividnext.sodalive.v2.creatorname.application
import kr.co.vividnext.sodalive.audition.Audition
import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.audition.AuditionRole
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantService
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantSortType
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.can.CanService
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.can.use.UseCanCalculate
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.AudioContentService
import kr.co.vividnext.sodalive.content.order.Order
import kr.co.vividnext.sodalive.content.order.OrderService
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylist
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistRedisRepository
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistService
import kr.co.vividnext.sodalive.content.playlist.PlaylistContentIdAndOrder
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.explorer.ExplorerService
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingListResponse
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingResponse
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService
import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.BeforeEach
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.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.data.domain.PageRequest
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ContextConfiguration
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import javax.persistence.EntityManager
@SpringBootTest(
properties = [
"spring.cache.type=none",
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.datasource.url=jdbc:h2:mem:legacy-current-name-reads;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@Transactional
class LegacyCurrentNameReadIntegrationTest @Autowired constructor(
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate,
private val memberService: MemberService,
private val orderService: OrderService,
private val playlistService: AudioContentPlaylistService,
private val playlistRepository: AudioContentPlaylistRedisRepository,
private val communityService: CreatorCommunityService,
private val donationService: ChannelDonationService,
private val explorerService: ExplorerService,
private val canService: CanService,
private val contentService: AudioContentService,
private val applicantService: AuditionApplicantService,
private val redisTemplate: RedisTemplate<String, Any>
) {
@MockBean
private lateinit var langContext: LangContext
@MockBean
private lateinit var countryContext: CountryContext
@MockBean
private lateinit var cloudFront: AudioContentCloudFront
@BeforeEach
fun setUp() {
jdbcTemplate.execute(
"CREATE ALIAS IF NOT EXISTS DATE_FORMAT FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.dateFormat'"
)
jdbcTemplate.execute(
"CREATE ALIAS IF NOT EXISTS CONVERT_TZ FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.convertTz'"
)
Mockito.doReturn(Lang.JA).`when`(langContext).lang
Mockito.doReturn("US").`when`(countryContext).countryCode
Mockito.doReturn("https://cdn.test/signed-audio").`when`(cloudFront)
.generateSignedURL(Mockito.anyString(), Mockito.anyLong())
}
@Test
@DisplayName("다른 회원 프로필과 차단 목록은 JA 이름을 표시하고 편집용 원본 프로필과 USER는 보존한다")
fun shouldLocalizeMemberDisplayAndBlockedListWithoutChangingRawEditorOrUserName() {
val viewer = saveMember("blocked-viewer", MemberRole.USER)
val creator = saveMember("blocked-creator", MemberRole.CREATOR)
val user = saveMember("blocked-user", MemberRole.USER)
listOf(creator, user).forEach { target ->
entityManager.persist(
BlockMember().apply {
member = viewer
blockedMember = target
}
)
}
saveAlias(creator, "Japanese blocked creator")
saveAlias(user, "Must not replace blocked USER")
entityManager.clear()
assertEquals("Japanese blocked creator", memberService.getMemberProfile(creator.id!!, viewer.id!!).nickname)
assertEquals("blocked-creator", memberService.getMember(creator.id!!, "web").nickname)
assertEquals("Japanese blocked creator", memberService.getMyPage(creator, "web").nickname)
val blocked = memberService.getBlockedMemberList(viewer.id!!, 0, 20)
assertEquals(2, blocked.totalCount)
assertEquals(
mapOf(creator.id to "Japanese blocked creator", user.id to "blocked-user"),
blocked.items.associate { it.memberId to it.nickname }
)
assertEquals("blocked-creator", entityManager.find(Member::class.java, creator.id).nickname)
}
@Test
@DisplayName("주문과 Redis 플레이리스트 상세는 콘텐츠 ID와 다른 소유자 이름을 현재 언어로 표시한다")
fun shouldLocalizeOrderAndPlaylistOwnersWithoutChangingTitlesOrStoredPlaylist() {
val viewer = saveMember("playlist-viewer", MemberRole.USER)
val creator = saveMember("playlist-creator", MemberRole.CREATOR)
val content = saveContent(creator, "Unchanged playlist title")
entityManager.persist(
Order(type = OrderType.KEEP).apply {
member = viewer
this.creator = creator
audioContent = content
}
)
saveAlias(creator, "Japanese playlist creator")
entityManager.clear()
assertNotEquals(creator.id, content.id)
val playlist = AudioContentPlaylist(
id = 100000L + viewer.id!!,
memberId = viewer.id!!,
title = "playlist metadata",
contentIdAndOrderList = listOf(PlaylistContentIdAndOrder(content.id!!, 1))
)
playlistRepository.save(playlist)
try {
val order = orderService.getAudioContentOrderList(viewer, 0, 20)
assertEquals("Japanese playlist creator", order.items.single().creatorNickname)
assertEquals("Unchanged playlist title", order.items.single().title)
val detail = playlistService.getPlaylistDetail(playlist.id, viewer)
assertEquals("Japanese playlist creator", detail.contentList.single().creatorNickname)
assertEquals("Unchanged playlist title", detail.contentList.single().title)
assertEquals(playlist, playlistRepository.findById(playlist.id).orElseThrow())
Mockito.doReturn(Lang.KO).`when`(langContext).lang
val korean = playlistService.getPlaylistDetail(playlist.id, viewer)
assertEquals("playlist-creator", korean.contentList.single().creatorNickname)
assertEquals("Japanese playlist creator", detail.contentList.single().creatorNickname)
} finally {
playlistRepository.delete(playlist)
}
}
@Test
@DisplayName("한정 콘텐츠 상세의 구매자 목록은 콘텐츠 소유자가 아닌 각 구매자 CREATOR 이름만 대체한다")
fun shouldLocalizeMixedBuyerNamesByBuyerIdWithoutAddingPublicOwnerFields() {
val owner = saveMember("buyer-content-owner", MemberRole.CREATOR)
val creatorBuyer = saveMember("buyer-creator", MemberRole.CREATOR)
val userBuyer = saveMember("buyer-user", MemberRole.USER)
val content = saveContent(owner, "Unchanged limited content title", limited = 5)
listOf(creatorBuyer, userBuyer).forEach { buyer ->
entityManager.persist(
Order(type = OrderType.KEEP).apply {
member = buyer
creator = owner
audioContent = content
}
)
}
saveAlias(owner, "Japanese content owner")
saveAlias(creatorBuyer, "Japanese buyer")
saveAlias(userBuyer, "Must not replace buyer USER")
entityManager.clear()
val detail = contentService.getDetail(content.id!!, owner, false, "Asia/Seoul")
assertEquals("Japanese content owner", detail.creator.nickname)
assertEquals(setOf("Japanese buyer", "buyer-user"), detail.buyerList.map { it.nickname }.toSet())
assertEquals("Unchanged limited content title", detail.title)
assertEquals("https://cdn.test/signed-audio", detail.contentUrl)
}
@Test
@DisplayName("레거시 커뮤니티 게시글·댓글·답글·구매 응답은 활성 JA·KO 별칭이 있어도 원본 이름을 유지한다")
fun shouldKeepOriginalCommunityNamesDespiteActiveJaAndKoAliases() {
val viewer = saveMember("community-viewer", MemberRole.USER)
val creator = saveMember("community-owner", MemberRole.CREATOR)
val peer = saveMember("deleted_community-peer", MemberRole.CREATOR)
val post = savePost(creator)
val parent = CreatorCommunityComment(comment = "unchanged creator comment").apply {
member = peer
creatorCommunity = post
entityManager.persist(this)
}
val reply = CreatorCommunityComment(comment = "unchanged USER reply").apply {
member = viewer
creatorCommunity = post
this.parent = parent
entityManager.persist(this)
}
val creatorReply = CreatorCommunityComment(comment = "unchanged creator reply").apply {
member = creator
creatorCommunity = post
this.parent = parent
entityManager.persist(this)
}
saveReceipt(viewer, CanUsage.PAID_COMMUNITY_POST, creator).communityPost = post
saveAlias(creator, "Japanese community owner")
saveAlias(peer, "Japanese community writer")
saveAlias(viewer, "Must not replace community USER")
listOf(creator, peer).forEach { writer ->
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, 'KO', ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
writer.id!!,
"Korean ${writer.nickname}"
)
}
entityManager.clear()
listOf(Lang.JA, Lang.KO).forEach { lang ->
Mockito.doReturn(lang).`when`(langContext).lang
val posts = communityService.getCommunityPostList(creator.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
assertEquals("community-owner", posts.single().creatorNickname)
assertEquals("community-peer", posts.single().firstComment!!.nickname)
assertEquals("unchanged post content", posts.single().content)
val detail = communityService.getCommunityPostDetail(post.id!!, viewer.id!!, "Asia/Seoul", false)
assertEquals("community-owner", detail.creatorNickname)
val comments = communityService.getCommunityPostCommentList(post.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
assertEquals("community-peer", comments.items.single().nickname)
val replies = communityService.getCommentReplyList(parent.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
assertEquals(
mapOf(reply.id to "community-viewer", creatorReply.id to "community-owner"),
replies.items.associate { it.id to it.nickname }
)
assertEquals("unchanged USER reply", replies.items.single { it.id == reply.id }.comment)
val purchased = communityService.purchasePost(post.id!!, viewer.id!!, "Asia/Seoul", false, "web")
assertEquals("community-owner", purchased.creatorNickname)
assertEquals("community-peer", purchased.firstComment!!.nickname)
}
}
@Test
@DisplayName("채널 후원 목록의 이름 소유자는 채널이 아니라 후원자이며 USER는 원본이다")
fun shouldLocalizeCreatorDonorsIndependentlyOfTheChannelOwner() {
val viewer = saveMember("donation-viewer", MemberRole.USER)
val creator = saveMember("donation-channel", MemberRole.CREATOR)
val donor = saveMember("donation-creator-donor", MemberRole.CREATOR)
listOf(viewer, donor).forEach { writer ->
entityManager.persist(
ChannelDonationMessage(can = 100, isSecret = false).apply {
member = writer
this.creator = creator
}
)
}
saveAlias(creator, "Wrong channel owner name")
saveAlias(donor, "Japanese donor")
saveAlias(viewer, "Must not replace donor USER")
entityManager.clear()
val response = donationService.getChannelDonationList(creator.id!!, viewer, 0, 20)
assertEquals(2, response.totalCount)
assertEquals(
mapOf(donor.id to "Japanese donor", viewer.id to "donation-viewer"),
response.items.associate { it.memberId to it.nickname }
)
assertEquals(listOf(100, 100), response.items.map { it.can })
}
@Test
@DisplayName("receipt는 각 recipient·audio·community 소유자의 현재 이름을 쓰고 리소스 제목과 금액을 보존한다")
fun shouldLocalizeReceiptOwnerNamesWithoutReplacingResourceTitlesOrAmounts() {
val viewer = saveMember("receipt-viewer", MemberRole.USER)
val owner = saveMember("receipt-owner", MemberRole.CREATOR)
val recipient = saveMember("receipt-recipient", MemberRole.CREATOR)
val content = saveContent(owner, "Unchanged receipt content title")
val post = savePost(owner)
saveReceipt(viewer, CanUsage.DONATION, owner).audioContent = content
saveReceipt(viewer, CanUsage.PAID_COMMUNITY_POST, owner).communityPost = post
saveReceipt(viewer, CanUsage.CHANNEL_DONATION, recipient)
saveReceipt(viewer, CanUsage.CHANNEL_DONATION, viewer)
saveReceipt(viewer, CanUsage.ORDER_CONTENT, owner).audioContent = content
saveAlias(owner, "Japanese receipt owner")
saveAlias(recipient, "Japanese recipient")
saveAlias(viewer, "Must not replace receipt USER")
entityManager.clear()
val rows = canService.getCanUseStatus(viewer, PageRequest.of(0, 20), "UTC", "web")
assertEquals(
setOf(
"[콘텐츠 후원] Japanese receipt owner",
"[게시글 보기] Japanese receipt owner",
"[채널 후원] Japanese recipient",
"[채널 후원] receipt-viewer",
"[콘텐츠 구매] Unchanged receipt content title"
),
rows.map { it.title }.toSet()
)
assertEquals(5, rows.size)
assertEquals(listOf(100, 100, 100, 100, 100), rows.map { it.can })
}
@Test
@DisplayName("오디션 지원자는 applicant ID가 아닌 member ID로 이름을 표시하고 USER·count·정렬을 보존한다")
fun shouldLocalizeAuditionApplicantsByMemberIdAndKeepUserNamesAndCount() {
saveMember("applicant-unused-viewer", MemberRole.USER)
val creator = saveMember("applicant-creator", MemberRole.CREATOR)
val user = saveMember("applicant-user", MemberRole.USER)
val audition = Audition(title = "Unchanged audition title", information = "information")
entityManager.persist(audition)
val role = AuditionRole(name = "Unchanged role name", information = "information").apply {
this.audition = audition
entityManager.persist(this)
}
val creatorApplicant = AuditionApplicant(phoneNumber = "01000000000", voicePath = "voice.m4a").apply {
member = creator
this.role = role
entityManager.persist(this)
}
val userApplicant = AuditionApplicant(phoneNumber = "01000000001", voicePath = "voice.m4a").apply {
member = user
this.role = role
entityManager.persist(this)
}
saveAlias(creator, "Japanese applicant")
saveAlias(user, "Must not replace applicant USER")
entityManager.clear()
assertNotEquals(creator.id, creatorApplicant.id)
val response = applicantService.getAuditionApplicantList(role.id!!, AuditionApplicantSortType.NEWEST, 0, 20)
assertEquals(2, response.totalCount)
assertEquals(listOf(userApplicant.id, creatorApplicant.id), response.items.map { it.applicantId })
assertEquals(listOf("applicant-user", "Japanese applicant"), response.items.map { it.nickname })
}
@Test
@DisplayName("중립 후원랭킹 cache hit는 요청별 현재 이름을 복사하고 원본 변경·해제 후 현재 원본으로 돌아간다")
fun shouldRenderCachedDonorNamesAfterReadAndKeepCachedRowsImmutable() {
val viewer = saveMember("cached-viewer", MemberRole.USER)
val channel = saveMember("cached-channel", MemberRole.CREATOR)
val donor = saveMember("cached-donor-original", MemberRole.CREATOR)
saveAlias(donor, "Japanese cached donor")
entityManager.clear()
val key = "creator_donation_ranking_v2:${channel.id}:CUMULATIVE:0:20:false"
val totalKey = "creator_donation_ranking_member_total_v2:${channel.id}:CUMULATIVE"
val cached = MemberDonationRankingListResponse(
listOf(MemberDonationRankingResponse(donor.id!!, "stale cached original", "image.png", 0))
)
redisTemplate.opsForValue().set(key, cached)
redisTemplate.opsForValue().set(totalKey, 1)
try {
val japanese = explorerService.getCreatorProfileDonationRanking(
channel.id!!,
DonationRankingPeriod.CUMULATIVE,
PageRequest.of(0, 20),
viewer
)
assertEquals("Japanese cached donor", japanese.userDonationRanking.single().nickname)
jdbcTemplate.update("update member set nickname = ? where id = ?", "renamed donor", donor.id!!)
jdbcTemplate.update("update creator_localized_name set is_active = false where member_id = ?", donor.id!!)
entityManager.clear()
val cleared = explorerService.getCreatorProfileDonationRanking(
channel.id!!,
DonationRankingPeriod.CUMULATIVE,
PageRequest.of(0, 20),
viewer
)
assertEquals("renamed donor", cleared.userDonationRanking.single().nickname)
assertEquals("Japanese cached donor", japanese.userDonationRanking.single().nickname)
assertEquals(cached, redisTemplate.opsForValue().get(key))
} finally {
redisTemplate.delete(listOf(key, totalKey))
}
}
private fun saveMember(nickname: String, role: MemberRole): Member =
Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "profile.png",
role = role
).also { entityManager.persist(it) }
private fun saveContent(owner: Member, title: String, limited: Int? = null): 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),
limited = limited,
remaining = limited
)
.apply {
member = owner
this.theme = theme
isActive = true
content = "output/audio.m4a"
duration = "00:10:00"
coverImage = "content.png"
entityManager.persist(this)
}
}
private fun savePost(owner: Member): CreatorCommunity =
CreatorCommunity(
content = "unchanged post content",
price = 0,
isCommentAvailable = true,
isAdult = false,
languageCode = "ko"
).apply {
member = owner
entityManager.persist(this)
}
private fun saveReceipt(viewer: Member, usage: CanUsage, recipient: Member): UseCan {
entityManager.flush()
val receipt = UseCan(canUsage = usage, can = 100, rewardCan = 0).apply {
member = viewer
entityManager.persist(this)
}
entityManager.persist(
UseCanCalculate(100, PaymentGateway.PG, UseCanCalculateStatus.RECEIVED).apply {
useCan = receipt
recipientCreatorId = recipient.id!!
}
)
return receipt
}
private fun saveAlias(member: Member, name: String) {
entityManager.flush()
jdbcTemplate.update(
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
"values (?, 'JA', ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
member.id!!,
name
)
}
}