diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllController.kt index 736b6770..5598ce91 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.content.all.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.content.all.application.MainContentAllFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -12,7 +13,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/audio/contents") class MainContentAllController( - private val facade: MainContentAllFacade + private val facade: MainContentAllFacade, + private val langContext: LangContext ) { @GetMapping fun getContents( @@ -30,7 +32,8 @@ class MainContentAllController( dayOfWeek = dayOfWeek, page = page, size = size, - member = member + member = member, + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacade.kt index b0dea149..f092e6cb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.content.all.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponse import kr.co.vividnext.sodalive.v2.content.all.application.MainContentAllQueryService @@ -15,17 +16,18 @@ class MainContentAllFacade( dayOfWeek: String?, page: Int?, size: Int?, - member: Member? + member: Member?, + lang: Lang ): MainContentAllTabResponse { - return MainContentAllTabResponse.from( - queryService.getContents( - type = type, - sort = sort, - dayOfWeek = dayOfWeek, - page = page, - size = size, - member = member - ) + val contents = queryService.getContents( + type = type, + sort = sort, + dayOfWeek = dayOfWeek, + page = page, + size = size, + member = member, + lang = lang ) + return MainContentAllTabResponse.from(contents) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt index 81b45124..3e32e277 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.overview.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.content.overview.application.ContentOverviewFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/contents") class ContentOverviewController( - private val facade: ContentOverviewFacade + private val facade: ContentOverviewFacade, + private val langContext: LangContext ) { @GetMapping fun getContents( @@ -22,7 +24,7 @@ class ContentOverviewController( @RequestParam(required = false) size: Int?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(facade.getContents(type, page, size, requireMember(member))) + ApiResponse.ok(facade.getContents(type, page, size, requireMember(member), langContext.lang)) } private fun requireMember(member: Member?): Member { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacade.kt index 18ed043b..d819cde3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.content.overview.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewItemResponse @@ -21,42 +22,46 @@ class ContentOverviewFacade( private val cloudFrontHost: String, private val queryPolicy: ContentOverviewQueryPolicy = ContentOverviewQueryPolicy() ) { - fun getContents(type: String?, page: Int?, size: Int?, member: Member): ContentOverviewPageResponse { + fun getContents(type: String?, page: Int?, size: Int?, member: Member, lang: Lang): ContentOverviewPageResponse { val resolvedType = queryPolicy.resolveType(type) val resolvedPage = queryPolicy.createPage(page, size) return when (resolvedType) { - ContentOverviewType.NEW_AND_HOT_AUDIO -> getNewAndHotContents(member, resolvedPage) - ContentOverviewType.FIRST_AUDIO_CONTENT -> getFirstAudioContents(member, resolvedPage) + ContentOverviewType.NEW_AND_HOT_AUDIO -> getNewAndHotContents(member, resolvedPage, lang) + ContentOverviewType.FIRST_AUDIO_CONTENT -> getFirstAudioContents(member, resolvedPage, lang) } } - private fun getNewAndHotContents(member: Member, page: ContentOverviewPage): ContentOverviewPageResponse { + private fun getNewAndHotContents(member: Member, page: ContentOverviewPage, lang: Lang): ContentOverviewPageResponse { val fetched = audioRecommendationQueryService.findNewAndHotAudios( member = member, offset = page.offset, - limit = page.size + 1 + limit = page.size + 1, + lang = lang ) + val items = queryPolicy.pageItems(fetched, page) return ContentOverviewPageResponse( type = ContentOverviewType.NEW_AND_HOT_AUDIO, - items = queryPolicy.pageItems(fetched, page).map { ContentOverviewItemResponse.fromNewAndHot(it) }, + items = items.map(ContentOverviewItemResponse::fromNewAndHot), page = page.page, size = page.size, hasNext = queryPolicy.hasNext(fetched, page) ) } - private fun getFirstAudioContents(member: Member, page: ContentOverviewPage): ContentOverviewPageResponse { + private fun getFirstAudioContents(member: Member, page: ContentOverviewPage, lang: Lang): ContentOverviewPageResponse { val fetched = homeRecommendationQueryService.findFirstAudioContents( now = LocalDateTime.now(), offset = page.offset, limit = page.size + 1, memberId = member.id, - includeAdultContents = memberContentPreferenceService.canViewAdultContent(member) + includeAdultContents = memberContentPreferenceService.canViewAdultContent(member), + lang = lang ) + val items = queryPolicy.pageItems(fetched, page) return ContentOverviewPageResponse( type = ContentOverviewType.FIRST_AUDIO_CONTENT, - items = queryPolicy.pageItems(fetched, page).map { + items = items.map { ContentOverviewItemResponse.fromFirstAudioContent( audio = it, coverImage = it.coverImage.toCdnUrl(cloudFrontHost), diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt index 36d781be..58ffe901 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.content.ranking.application.AudioRankingFacade import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType @@ -13,13 +14,14 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/audio/rankings") class AudioRankingController( - private val facade: AudioRankingFacade + private val facade: AudioRankingFacade, + private val langContext: LangContext ) { @GetMapping fun getRankings( @RequestParam(defaultValue = "WEEKLY_POPULAR") type: AudioRankingType, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(facade.getRankings(type, member)) + ApiResponse.ok(facade.getRankings(type, member, langContext.lang)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacade.kt index 40333589..ee109034 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.content.ranking.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.content.ranking.dto.AudioRankingResponse import kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryService @@ -10,7 +11,8 @@ import org.springframework.stereotype.Component class AudioRankingFacade( private val queryService: AudioRankingQueryService ) { - fun getRankings(type: AudioRankingType, member: Member?): AudioRankingResponse { - return AudioRankingResponse.from(queryService.getRankings(type, member)) + fun getRankings(type: AudioRankingType, member: Member?, lang: Lang): AudioRankingResponse { + val result = queryService.getRankings(type, member, lang) + return AudioRankingResponse.from(result) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationController.kt index 0b45e546..650e60d8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -11,12 +12,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/audio/recommendations") class AudioRecommendationController( - private val audioRecommendationFacade: AudioRecommendationFacade + private val audioRecommendationFacade: AudioRecommendationFacade, + private val langContext: LangContext ) { @GetMapping fun getRecommendations( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(audioRecommendationFacade.getRecommendations(member)) + ApiResponse.ok(audioRecommendationFacade.getRecommendations(member, langContext.lang)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacade.kt index 63277fe7..b4e19432 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.content.recommendation.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.content.recommendation.dto.AudioRecommendationsResponse import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService @@ -9,7 +10,8 @@ import org.springframework.stereotype.Component class AudioRecommendationFacade( private val queryService: AudioRecommendationQueryService ) { - fun getRecommendations(member: Member?): AudioRecommendationsResponse { - return AudioRecommendationsResponse.from(queryService.getRecommendations(member)) + fun getRecommendations(member: Member?, lang: Lang): AudioRecommendationsResponse { + val result = queryService.getRecommendations(member, lang) + return AudioRecommendationsResponse.from(result) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt index fba0ce79..128ff280 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/creator-channels") class CreatorChannelAudioController( - private val creatorChannelAudioFacade: CreatorChannelAudioFacade + private val creatorChannelAudioFacade: CreatorChannelAudioFacade, + private val langContext: LangContext ) { @GetMapping("/{creatorId}/audio") fun getAudioTab( @@ -32,7 +34,8 @@ class CreatorChannelAudioController( sort = sort, themeId = themeId, page = page, - size = size + size = size, + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt index 177638a1..614deb7a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelAudioTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryService @@ -19,7 +20,8 @@ class CreatorChannelAudioFacade( themeId: Long?, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelAudioTabResponse { return CreatorChannelAudioTabResponse.from( creatorChannelAudioQueryService.getAudioTab( @@ -29,7 +31,8 @@ class CreatorChannelAudioFacade( themeId = themeId, page = page, size = size, - now = now + now = now, + lang = lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt index 3901833c..7fb9cb67 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.`in`.w import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/creator-channels") class CreatorChannelCommunityController( - private val creatorChannelCommunityFacade: CreatorChannelCommunityFacade + private val creatorChannelCommunityFacade: CreatorChannelCommunityFacade, + private val langContext: LangContext ) { @GetMapping("/{creatorId}/community") fun getCommunityTab( @@ -28,7 +30,8 @@ class CreatorChannelCommunityController( creatorId = creatorId, viewer = requireMember(member), page = page, - size = size + size = size, + lang = langContext.lang ) ) } @@ -41,7 +44,8 @@ class CreatorChannelCommunityController( ApiResponse.ok( creatorChannelCommunityFacade.getCommunityPostDetail( postId = postId, - viewer = requireMember(member) + viewer = requireMember(member), + lang = langContext.lang ) ) } @@ -58,7 +62,8 @@ class CreatorChannelCommunityController( postId = postId, viewer = requireMember(member), page = page, - size = size + size = size, + lang = langContext.lang ) ) } @@ -75,7 +80,8 @@ class CreatorChannelCommunityController( commentId = commentId, viewer = requireMember(member), page = page, - size = size + size = size, + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt index f88a3de6..9237b288 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.community.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityCommentsResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityPostDetailResponse @@ -20,31 +21,33 @@ class CreatorChannelCommunityFacade( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityTabResponse { - return CreatorChannelCommunityTabResponse.from( - creatorChannelCommunityQueryService.getCommunityTab( - creatorId = creatorId, - viewer = viewer, - page = page, - size = size, - now = now - ) + val tab = creatorChannelCommunityQueryService.getCommunityTab( + creatorId = creatorId, + viewer = viewer, + page = page, + size = size, + now = now, + lang = lang ) + return CreatorChannelCommunityTabResponse.from(tab) } fun getCommunityPostDetail( postId: Long, viewer: Member, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityPostDetailResponse { - return CreatorChannelCommunityPostDetailResponse.from( - creatorChannelCommunityQueryService.getCommunityPostDetail( - postId = postId, - viewer = viewer, - now = now - ) + val detail = creatorChannelCommunityQueryService.getCommunityPostDetail( + postId = postId, + viewer = viewer, + now = now, + lang = lang ) + return CreatorChannelCommunityPostDetailResponse.from(detail) } fun getCommunityComments( @@ -52,17 +55,18 @@ class CreatorChannelCommunityFacade( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityCommentsResponse { - return CreatorChannelCommunityCommentsResponse.from( - creatorChannelCommunityQueryService.getCommunityComments( - postId = postId, - viewer = viewer, - page = page, - size = size, - now = now - ) + val comments = creatorChannelCommunityQueryService.getCommunityComments( + postId = postId, + viewer = viewer, + page = page, + size = size, + now = now, + lang = lang ) + return CreatorChannelCommunityCommentsResponse.from(comments) } fun getCommunityReplies( @@ -70,16 +74,17 @@ class CreatorChannelCommunityFacade( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityRepliesResponse { - return CreatorChannelCommunityRepliesResponse.from( - creatorChannelCommunityQueryService.getCommunityReplies( - commentId = commentId, - viewer = viewer, - page = page, - size = size, - now = now - ) + val replies = creatorChannelCommunityQueryService.getCommunityReplies( + commentId = commentId, + viewer = viewer, + page = page, + size = size, + now = now, + lang = lang ) + return CreatorChannelCommunityRepliesResponse.from(replies) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt index ca0b8b42..c618884f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.`in`.we import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/creator-channels") class CreatorChannelDonationController( - private val creatorChannelDonationFacade: CreatorChannelDonationFacade + private val creatorChannelDonationFacade: CreatorChannelDonationFacade, + private val langContext: LangContext ) { @GetMapping("/{creatorId}/donations") fun getDonationTab( @@ -28,7 +30,8 @@ class CreatorChannelDonationController( creatorId = creatorId, viewer = requireMember(member), page = page, - size = size + size = size, + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt index 13a96348..14d409f9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt @@ -1,8 +1,10 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryService +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @@ -10,22 +12,29 @@ import java.time.LocalDateTime @Service @Transactional(readOnly = true) class CreatorChannelDonationFacade( - private val creatorChannelDonationQueryService: CreatorChannelDonationQueryService + private val creatorChannelDonationQueryService: CreatorChannelDonationQueryService, + private val creatorLocalizedNameService: CreatorLocalizedNameService ) { fun getDonationTab( creatorId: Long, viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelDonationTabResponse { + val tab = creatorChannelDonationQueryService.getDonationTab( + creatorId = creatorId, + viewer = viewer, + page = page, + size = size, + now = now, + lang = lang + ) + val names = creatorLocalizedNameService.findCurrentNames(tab.rankings.map { it.userId }.toSet(), lang) return CreatorChannelDonationTabResponse.from( - creatorChannelDonationQueryService.getDonationTab( - creatorId = creatorId, - viewer = viewer, - page = page, - size = size, - now = now + tab.copy( + rankings = tab.rankings.map { it.copy(nickname = names[it.userId] ?: it.nickname) } ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkController.kt index 09feee1b..d8e8dc00 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application.CreatorChannelFanTalkFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/creator-channels") class CreatorChannelFanTalkController( - private val creatorChannelFanTalkFacade: CreatorChannelFanTalkFacade + private val creatorChannelFanTalkFacade: CreatorChannelFanTalkFacade, + private val langContext: LangContext ) { @GetMapping("/{creatorId}/fan-talks") fun getFanTalkTab( @@ -28,7 +30,8 @@ class CreatorChannelFanTalkController( creatorId = creatorId, viewer = requireMember(member), page = page, - size = size + size = size, + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt index 079abb9d..ec34a8c5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application.CreatorChannelFanTalkQueryService @@ -17,16 +18,17 @@ class CreatorChannelFanTalkFacade( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelFanTalkTabResponse { - return CreatorChannelFanTalkTabResponse.from( - creatorChannelFanTalkQueryService.getFanTalkTab( - creatorId = creatorId, - viewer = viewer, - page = page, - size = size, - now = now - ) + val tab = creatorChannelFanTalkQueryService.getFanTalkTab( + creatorId = creatorId, + viewer = viewer, + page = page, + size = size, + now = now, + lang = lang ) + return CreatorChannelFanTalkTabResponse.from(tab) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeController.kt index 75477284..3157cc39 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.home.application.CreatorChannelHomeFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/creator-channels") class CreatorChannelHomeController( - private val creatorChannelHomeFacade: CreatorChannelHomeFacade + private val creatorChannelHomeFacade: CreatorChannelHomeFacade, + private val langContext: LangContext ) { @GetMapping("/{creatorId}/home") fun getHome( @@ -23,7 +25,8 @@ class CreatorChannelHomeController( ApiResponse.ok( creatorChannelHomeFacade.getHome( creatorId = creatorId, - viewer = requireMember(member) + viewer = requireMember(member), + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt index 8c96e13b..cecf3e9d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.home.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.home.dto.CreatorChannelHomeResponse import kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryService @@ -16,14 +17,10 @@ class CreatorChannelHomeFacade( fun getHome( creatorId: Long, viewer: Member, - now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC) + now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC), + lang: Lang ): CreatorChannelHomeResponse { - return CreatorChannelHomeResponse.from( - creatorChannelHomeQueryService.getHome( - creatorId = creatorId, - viewer = viewer, - now = now - ) - ) + val home = creatorChannelHomeQueryService.getHome(creatorId = creatorId, viewer = viewer, now = now, lang = lang) + return CreatorChannelHomeResponse.from(home) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt index 66e4e8da..2cc7aa24 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacade import kr.co.vividnext.sodalive.v2.common.domain.ContentSort @@ -15,7 +16,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/creator-channels") class CreatorChannelLiveController( - private val creatorChannelLiveFacade: CreatorChannelLiveFacade + private val creatorChannelLiveFacade: CreatorChannelLiveFacade, + private val langContext: LangContext ) { @GetMapping("/{creatorId}/live") fun getLiveTab( @@ -31,7 +33,8 @@ class CreatorChannelLiveController( viewer = requireMember(member), sort = sort, page = page, - size = size + size = size, + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt index 8ac95583..0efa6616 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.live.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveTabResponse import kr.co.vividnext.sodalive.v2.common.domain.ContentSort @@ -19,7 +20,8 @@ class CreatorChannelLiveFacade( sort: ContentSort, page: Int, size: Int, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelLiveTabResponse { return CreatorChannelLiveTabResponse.from( creatorChannelLiveQueryService.getLiveTab( @@ -28,7 +30,8 @@ class CreatorChannelLiveFacade( sort = sort, page = page, size = size, - now = now + now = now, + lang = lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesController.kt index b44d8280..4a5a5718 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.series.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.series.application.CreatorChannelSeriesFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/creator-channels") class CreatorChannelSeriesController( - private val creatorChannelSeriesFacade: CreatorChannelSeriesFacade + private val creatorChannelSeriesFacade: CreatorChannelSeriesFacade, + private val langContext: LangContext ) { @GetMapping("/{creatorId}/series") fun getSeriesTab( @@ -30,7 +32,8 @@ class CreatorChannelSeriesController( viewer = requireMember(member), sort = sort, page = page, - size = size + size = size, + lang = langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacade.kt index 5fee5eed..4f247aa8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.series.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.series.application.CreatorChannelSeriesQueryService @@ -18,7 +19,8 @@ class CreatorChannelSeriesFacade( sort: String?, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelSeriesTabResponse { return CreatorChannelSeriesTabResponse.from( creatorChannelSeriesQueryService.getSeriesTab( @@ -27,7 +29,8 @@ class CreatorChannelSeriesFacade( sort = sort, page = page, size = size, - now = now + now = now, + lang = lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt index 17826e91..607e9343 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.gift.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.gift.dto.GiftApplicationRequest import kr.co.vividnext.sodalive.v2.api.gift.dto.GiftApplicationResponse @@ -32,7 +33,8 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/api/v2/gifts") class GiftController( private val queryService: GiftQueryService, - private val commandService: GiftCommandService + private val commandService: GiftCommandService, + private val langContext: LangContext ) { @GetMapping("/form-options") fun getFormOptions() = ApiResponse.ok(GiftFormOptionsResponse.from(queryService.getFormOptions())) @@ -49,7 +51,8 @@ class GiftController( memberId = member?.id ?: throw SodaException(messageKey = "common.error.bad_credentials"), type = type, page = page, - size = size + size = size, + lang = langContext.lang ) ) ) @@ -62,7 +65,8 @@ class GiftController( GiftDetailResponse.from( queryService.getGiftDetail( memberId = member?.id ?: throw SodaException(messageKey = "common.error.bad_credentials"), - applicationNo = applicationNo + applicationNo = applicationNo, + lang = langContext.lang ) ) ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt index 664c20a1..0347d270 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.home.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.home.application.HomeCreatorRankingFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -11,12 +12,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/home/rankings") class CreatorRankingController( - private val homeCreatorRankingFacade: HomeCreatorRankingFacade + private val homeCreatorRankingFacade: HomeCreatorRankingFacade, + private val langContext: LangContext ) { @GetMapping("/creators") fun getCreatorRankings( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(homeCreatorRankingFacade.getCreatorRankings(member)) + ApiResponse.ok(homeCreatorRankingFacade.getCreatorRankings(member, langContext.lang)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt index 9f6b2e19..35dd70a8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacade import kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.FollowRecommendedCreatorsRequest @@ -18,13 +19,14 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/api/v2/home/recommendations") class HomeRecommendationController( private val homeRecommendationFacade: HomeRecommendationFacade, - private val recommendedCreatorFollowService: RecommendedCreatorFollowService + private val recommendedCreatorFollowService: RecommendedCreatorFollowService, + private val langContext: LangContext ) { @GetMapping fun getHomeRecommendations( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(homeRecommendationFacade.getHomeRecommendations(member)) + ApiResponse.ok(homeRecommendationFacade.getHomeRecommendations(member, langContext.lang)) } @GetMapping("/lives") @@ -37,7 +39,8 @@ class HomeRecommendationController( homeRecommendationFacade.getLives( requireMember(member), normalizePage(page), - normalizeSize(size) + normalizeSize(size), + langContext.lang ) ) } @@ -52,7 +55,8 @@ class HomeRecommendationController( homeRecommendationFacade.getRecentDebutCreators( requireMember(member), normalizePage(page), - normalizeSize(size) + normalizeSize(size), + langContext.lang ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt index c0618a44..7e78b36d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.home.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.home.dto.ranking.CreatorRankingResponse import kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryService @@ -9,9 +10,8 @@ import org.springframework.stereotype.Component class HomeCreatorRankingFacade( private val creatorRankingQueryService: CreatorRankingQueryService ) { - fun getCreatorRankings(member: Member?): CreatorRankingResponse { - return CreatorRankingResponse.from( - creatorRankingQueryService.getCreatorRankings(viewerMemberId = member?.id) - ) + fun getCreatorRankings(member: Member?, lang: Lang): CreatorRankingResponse { + val result = creatorRankingQueryService.getCreatorRankings(viewerMemberId = member?.id, lang = lang) + return CreatorRankingResponse.from(result) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt index 1e36c20a..a1f86435 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt @@ -44,21 +44,23 @@ class HomeRecommendationFacade( ) { private val log = LoggerFactory.getLogger(javaClass) - fun getHomeRecommendations(member: Member?): HomeRecommendationResponse { + fun getHomeRecommendations(member: Member?, lang: Lang): HomeRecommendationResponse { val startedAt = System.currentTimeMillis() return runCatching { val now = LocalDateTime.now() val includeAdult = resolveAdultVisibility(member) val bannerLang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO - HomeRecommendationResponse( - lives = queryService.findLiveRecommendations( - limit = HOME_LIVE_LIMIT, - memberId = member?.id, - includeAdultLives = includeAdult, - isViewerCreator = member?.role == MemberRole.CREATOR, - effectiveViewerGender = member?.effectiveGender() - ).map { it.toItem() }, + val lives = queryService.findLiveRecommendations( + limit = HOME_LIVE_LIMIT, + memberId = member?.id, + includeAdultLives = includeAdult, + isViewerCreator = member?.role == MemberRole.CREATOR, + effectiveViewerGender = member?.effectiveGender(), + lang = lang + ) + val response = HomeRecommendationResponse( + lives = lives.map { it.toItem() }, banners = queryService.findHomeBanners( limit = HOME_BANNER_LIMIT, memberId = member?.id, @@ -69,21 +71,24 @@ class HomeRecommendationFacade( recentlyActiveCreators = queryService.findRecentlyActiveCreators( HOME_ACTIVE_CREATOR_LIMIT, member?.id, - includeAdult + includeAdult, + lang ) .map { it.toItem() }, recentDebutCreators = queryService.findRecentDebutCreators( now, limit = HOME_RECENT_DEBUT_CREATOR_LIMIT, memberId = member?.id, - includeAdultContents = includeAdult + includeAdultContents = includeAdult, + lang = lang ) .map { it.toItem() }, firstAudioContents = queryService.findFirstAudioContents( now, limit = HOME_FIRST_AUDIO_CONTENT_LIMIT, memberId = member?.id, - includeAdultContents = includeAdult + includeAdultContents = includeAdult, + lang = lang ) .map { it.toItem() }, aiCharacters = queryService.findAiCharacterRecommendations( @@ -94,16 +99,19 @@ class HomeRecommendationFacade( memberId = member?.id, includeAdultGenres = includeAdult, genreLimit = HOME_GENRE_CREATOR_GENRE_LIMIT, - creatorLimit = HOME_GENRE_CREATOR_CREATOR_LIMIT + creatorLimit = HOME_GENRE_CREATOR_CREATOR_LIMIT, + lang = lang ).map { it.toItem() }, - cheerCreators = queryService.findCheerCreatorRecommendations(HOME_CHEER_CREATOR_LIMIT, member?.id) + cheerCreators = queryService.findCheerCreatorRecommendations(HOME_CHEER_CREATOR_LIMIT, member?.id, lang) .map { it.toCreatorItem() }, popularCommunityPosts = queryService.findPopularCommunityRecommendations( limit = HOME_POPULAR_COMMUNITY_LIMIT, memberId = member?.id, - includeAdultCommunities = includeAdult + includeAdultCommunities = includeAdult, + lang = lang ).map { it.toItem() } ) + response }.onSuccess { response -> log.info( "event=home_recommendations_query_success memberId={} elapsedMs={} emptySections={}", @@ -122,7 +130,7 @@ class HomeRecommendationFacade( }.getOrThrow() } - fun getLives(member: Member, page: Int, size: Int): HomeRecommendationPageResponse { + fun getLives(member: Member, page: Int, size: Int, lang: Lang): HomeRecommendationPageResponse { val startedAt = System.currentTimeMillis() return runCatching { val fetched = queryService.findLiveRecommendations( @@ -131,7 +139,8 @@ class HomeRecommendationFacade( memberId = member.id, includeAdultLives = resolveAdultVisibility(member), isViewerCreator = member.role == MemberRole.CREATOR, - effectiveViewerGender = member.effectiveGender() + effectiveViewerGender = member.effectiveGender(), + lang = lang ) fetched.toPage(page, size) { it.toItem() } }.onSuccess { @@ -141,7 +150,12 @@ class HomeRecommendationFacade( }.getOrThrow() } - fun getRecentDebutCreators(member: Member, page: Int, size: Int): HomeRecommendationPageResponse { + fun getRecentDebutCreators( + member: Member, + page: Int, + size: Int, + lang: Lang + ): HomeRecommendationPageResponse { val startedAt = System.currentTimeMillis() return runCatching { val fetched = queryService.findRecentDebutCreators( @@ -149,7 +163,8 @@ class HomeRecommendationFacade( offset = page.toOffset(size), limit = size + 1, memberId = member.id, - includeAdultContents = resolveAdultVisibility(member) + includeAdultContents = resolveAdultVisibility(member), + lang = lang ) fetched.toPage(page, size) { it.toItem() } }.onSuccess { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingController.kt index f77ad8a1..468a595a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.home.following.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -11,12 +12,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/home/following") class HomeFollowingController( - private val facade: HomeFollowingFacade + private val facade: HomeFollowingFacade, + private val langContext: LangContext ) { @GetMapping fun getFollowingTab( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(facade.getFollowingTab(member)) + ApiResponse.ok(facade.getFollowingTab(member, langContext.lang)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt index 49ac1735..4c633bd1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.home.following.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponse import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService @@ -11,14 +12,15 @@ class HomeFollowingFacade( private val homeFollowingQueryService: HomeFollowingQueryService, private val chatRoomListService: ChatRoomListService ) { - fun getFollowingTab(member: Member?): HomeFollowingTabResponse { + fun getFollowingTab(member: Member?, lang: Lang): HomeFollowingTabResponse { if (member == null) { return HomeFollowingTabResponse.loginRequired() } - val home = homeFollowingQueryService.findHomeFollowing(member) + val home = homeFollowingQueryService.findHomeFollowing(member, lang) val recentChats = chatRoomListService.getRooms( member, + lang = lang, filter = "ALL", cursor = null, limit = 10, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt index 1eb7bf57..854a9c2d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home.live.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.home.live.application.HomeOnAirLiveFacade import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -13,14 +14,15 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/home/on-air-lives") class HomeOnAirLiveController( - private val homeOnAirLiveFacade: HomeOnAirLiveFacade + private val homeOnAirLiveFacade: HomeOnAirLiveFacade, + private val langContext: LangContext ) { @GetMapping fun getOnAirLives( @RequestParam(defaultValue = "0") page: Int, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(homeOnAirLiveFacade.getOnAirLives(requireMember(member), page)) + ApiResponse.ok(homeOnAirLiveFacade.getOnAirLives(requireMember(member), page, langContext.lang)) } private fun requireMember(member: Member?): Member { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacade.kt index 4328d852..c95bc8be 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacade.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.home.live.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole @@ -20,7 +21,7 @@ class HomeOnAirLiveFacade( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { - fun getOnAirLives(member: Member, page: Int): HomeOnAirLivePageResponse { + fun getOnAirLives(member: Member, page: Int, lang: Lang): HomeOnAirLivePageResponse { val normalizedPage = page.coerceIn(0, MAX_PAGE) val fetched = queryService.findLiveRecommendations( offset = normalizedPage.toLong() * PAGE_SIZE, @@ -28,9 +29,11 @@ class HomeOnAirLiveFacade( memberId = member.id, includeAdultLives = memberContentPreferenceService.canViewAdultContent(member), isViewerCreator = member.role == MemberRole.CREATOR, - effectiveViewerGender = member.effectiveGender() + effectiveViewerGender = member.effectiveGender(), + lang = lang ) - val items = fetched.take(PAGE_SIZE).map { it.toResponse() } + val pageItems = fetched.take(PAGE_SIZE) + val items = pageItems.map { it.toResponse() } return HomeOnAirLivePageResponse( items = items, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/controller/ChatRoomListController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/controller/ChatRoomListController.kt index 15a6ab59..2eb7d285 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/controller/ChatRoomListController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/controller/ChatRoomListController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.chat.controller import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListPageResponse import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService @@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/chat/rooms") class ChatRoomListController( - private val service: ChatRoomListService + private val service: ChatRoomListService, + private val langContext: LangContext ) { @GetMapping fun getRooms( @@ -23,6 +25,6 @@ class ChatRoomListController( @RequestParam(defaultValue = "30") limit: Int ) = run { if (member == null) return@run ApiResponse.ok(ChatRoomListPageResponse(emptyList(), false, null)) - ApiResponse.ok(service.getRooms(member, filter, cursor, limit)) + ApiResponse.ok(service.getRooms(member, langContext.lang, filter, cursor, limit)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/service/ChatRoomListService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/service/ChatRoomListService.kt index fd047951..f8bd48ca 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/service/ChatRoomListService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/chat/service/ChatRoomListService.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.chat.service import kr.co.vividnext.sodalive.chat.room.ChatMessageType import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.toUtcIso import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse @@ -26,6 +27,7 @@ class ChatRoomListService( ) { fun getRooms( member: Member, + lang: Lang, filter: String = ChatRoomListFilter.ALL.name, cursor: String? = null, limit: Int = DEFAULT_LIMIT, @@ -50,6 +52,7 @@ class ChatRoomListService( parsedCursor?.chatType, parsedCursor?.roomId, followedCreatorsOnly, + lang, pageable ) } @@ -70,6 +73,7 @@ class ChatRoomListService( parsedCursor?.chatType, parsedCursor?.roomId, followedCreatorsOnly, + lang, pageable ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt index 219d58f2..72e9c125 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt @@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.content.translation.QContentTranslation import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember @@ -23,6 +24,7 @@ import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllAudio import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllSeries +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -64,7 +66,10 @@ class DefaultMainContentAllQueryRepository( onlyFree: Boolean, onlyPointAvailable: Boolean ): List { - val rows = findAudioRows(memberId, canViewAdultContent, now, sort, offset, limit, onlyPaid, onlyFree, onlyPointAvailable) + val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "mainContentAllAudioCreatorName") + val rows = findAudioRows( + memberId, canViewAdultContent, now, sort, offset, limit, onlyPaid, onlyFree, onlyPointAvailable, creatorName + ) if (rows.isEmpty()) return emptyList() val contentIds = rows.map { it.get(audioContent.id)!! } @@ -85,7 +90,8 @@ class DefaultMainContentAllQueryRepository( isPointAvailable = row.get(audioContent.isPointAvailable)!!, isFirstContent = firstContentIdByCreatorId[creatorId] == contentId, isOriginalSeries = originalSeriesByContentId[contentId] ?: false, - creatorNickname = row.get(member.nickname)!! + creatorNickname = row.get(creatorName.displayName)!!, + creatorMemberId = creatorId ) } } @@ -122,18 +128,21 @@ class DefaultMainContentAllQueryRepository( if (seriesIds.isEmpty()) return emptyList() val seriesTranslation = QSeriesTranslation("mainContentAllSeriesTranslation") + val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "mainContentAllSeriesCreatorName") return queryFactory .select( series.id, series.title, seriesTranslation.renderedPayload, series.coverImage, - member.nickname, + member.id, + creatorName.displayName, series.isOriginal, series.isAdult ) .from(series) .join(series.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .leftJoin(seriesTranslation) .on( seriesTranslation.seriesId.eq(series.id), @@ -148,9 +157,10 @@ class DefaultMainContentAllQueryRepository( seriesId = row.get(series.id)!!, title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: row.get(series.title)!!, coverImageUrl = row.get(series.coverImage).toCdnUrl(cloudFrontHost), - creatorNickname = row.get(member.nickname)!!, + creatorNickname = row.get(creatorName.displayName)!!, isOriginal = row.get(series.isOriginal)!!, - isAdult = row.get(series.isAdult)!! + isAdult = row.get(series.isAdult)!!, + creatorMemberId = row.get(member.id)!! ) } } @@ -164,7 +174,8 @@ class DefaultMainContentAllQueryRepository( limit: Int, onlyPaid: Boolean, onlyFree: Boolean, - onlyPointAvailable: Boolean + onlyPointAvailable: Boolean, + creatorName: CreatorNameJoin ): List { val query = queryFactory .select( @@ -175,11 +186,12 @@ class DefaultMainContentAllQueryRepository( audioContent.isAdult, audioContent.isPointAvailable, audioContent.member.id, - member.nickname, + creatorName.displayName, audioContent.releaseDate ) .from(audioContent) .join(audioContent.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .join(audioContent.theme, audioContentTheme) .where(audioCondition(memberId, canViewAdultContent, now, onlyPaid, onlyFree, onlyPointAvailable)) @@ -192,7 +204,7 @@ class DefaultMainContentAllQueryRepository( revenueOrder.audioContent.id.eq(audioContent.id), revenueOrder.isActive.isTrue ) - .groupByAudioRow() + .groupByAudioRow(creatorName) .orderBy( revenueOrder.can.sum().coalesce(0).desc(), audioContent.releaseDate.desc(), @@ -301,7 +313,7 @@ class DefaultMainContentAllQueryRepository( return query.offset(offset).limit(limit.toLong()).fetch() } - private fun JPAQuery.groupByAudioRow(): JPAQuery { + private fun JPAQuery.groupByAudioRow(creatorName: CreatorNameJoin): JPAQuery { return groupBy( audioContent.id, audioContent.title, @@ -311,6 +323,7 @@ class DefaultMainContentAllQueryRepository( audioContent.isPointAvailable, audioContent.member.id, member.nickname, + creatorName.name.displayName, audioContent.releaseDate ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryService.kt index e19a53ba..3ee94b86 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryService.kt @@ -1,7 +1,7 @@ package kr.co.vividnext.sodalive.v2.content.all.application import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek -import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.v2.common.domain.ContentSort @@ -19,8 +19,7 @@ import java.time.LocalDateTime class MainContentAllQueryService( private val queryPort: MainContentAllQueryPort, private val memberContentPreferenceService: MemberContentPreferenceService, - private val queryPolicy: MainContentAllQueryPolicy = MainContentAllQueryPolicy(), - private val langContext: LangContext + private val queryPolicy: MainContentAllQueryPolicy = MainContentAllQueryPolicy() ) { fun getContents( type: String?, @@ -28,7 +27,8 @@ class MainContentAllQueryService( dayOfWeek: String?, page: Int?, size: Int?, - member: Member? + member: Member?, + lang: Lang ): MainContentAll { val resolvedType = queryPolicy.resolveType(type) val resolvedSort = queryPolicy.resolveSort(sort) @@ -47,6 +47,7 @@ class MainContentAllQueryService( memberId = memberId, canViewAdultContent = canViewAdultContent, now = now, + lang = lang, onlyPaid = true ) @@ -58,6 +59,7 @@ class MainContentAllQueryService( memberId = memberId, canViewAdultContent = canViewAdultContent, now = now, + lang = lang, onlyFree = true ) @@ -69,6 +71,7 @@ class MainContentAllQueryService( memberId = memberId, canViewAdultContent = canViewAdultContent, now = now, + lang = lang, onlyPointAvailable = true ) @@ -79,7 +82,8 @@ class MainContentAllQueryService( page = resolvedPage, memberId = memberId, canViewAdultContent = canViewAdultContent, - now = now + now = now, + lang = lang ) MainContentAllType.ORIGINAL -> getSeriesContents( @@ -90,6 +94,7 @@ class MainContentAllQueryService( memberId = memberId, canViewAdultContent = canViewAdultContent, now = now, + lang = lang, onlyOriginal = true ) } @@ -103,6 +108,7 @@ class MainContentAllQueryService( memberId: Long?, canViewAdultContent: Boolean, now: LocalDateTime, + lang: Lang, onlyPaid: Boolean = false, onlyFree: Boolean = false, onlyPointAvailable: Boolean = false @@ -122,7 +128,7 @@ class MainContentAllQueryService( sort = sort, offset = page.offset, limit = page.size + 1, - locale = langContext.lang.code, + locale = lang.code, onlyPaid = onlyPaid, onlyFree = onlyFree, onlyPointAvailable = onlyPointAvailable @@ -148,6 +154,7 @@ class MainContentAllQueryService( memberId: Long?, canViewAdultContent: Boolean, now: LocalDateTime, + lang: Lang, onlyOriginal: Boolean = false ): MainContentAll { val totalCount = queryPort.countSeries(memberId, canViewAdultContent, now, onlyOriginal, dayOfWeek) @@ -160,7 +167,7 @@ class MainContentAllQueryService( limit = page.size + 1, onlyOriginal = onlyOriginal, dayOfWeek = dayOfWeek, - locale = langContext.lang.code + locale = lang.code ) return MainContentAll( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/domain/MainContentAll.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/domain/MainContentAll.kt index c1d0c4e3..acf3da67 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/domain/MainContentAll.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/domain/MainContentAll.kt @@ -23,7 +23,8 @@ data class MainContentAllAudio( val isPointAvailable: Boolean, val isFirstContent: Boolean, val isOriginalSeries: Boolean, - val creatorNickname: String + val creatorNickname: String, + val creatorMemberId: Long ) data class MainContentAllSeries( @@ -32,5 +33,6 @@ data class MainContentAllSeries( val coverImageUrl: String?, val creatorNickname: String, val isOriginal: Boolean, - val isAdult: Boolean + val isAdult: Boolean, + val creatorMemberId: Long ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapter.kt index 698e5b78..496f660a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapter.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapter.kt @@ -1,9 +1,15 @@ package kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence +import com.querydsl.jpa.JPAExpressions +import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository +import kr.co.vividnext.sodalive.i18n.Lang +import kr.co.vividnext.sodalive.member.QMember.member +import kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence.QAudioRankingSnapshot.audioRankingSnapshot import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotPort import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @@ -11,14 +17,34 @@ import java.time.LocalDateTime @Repository class DefaultAudioRankingSnapshotPersistenceAdapter( private val repository: AudioRankingSnapshotRepository, - private val contentTranslationRepository: ContentTranslationRepository + private val contentTranslationRepository: ContentTranslationRepository, + private val queryFactory: JPAQueryFactory ) : AudioRankingSnapshotPort { override fun findLatestVisibleSnapshots( rankingType: AudioRankingType, nowUtc: LocalDateTime, locale: String ): List { - return repository.findLatestVisibleSnapshots(rankingType.name, nowUtc).toRecords(locale) + val snapshot = audioRankingSnapshot + val latest = QAudioRankingSnapshot("latestVisibleAudioRanking") + val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "audioRankingCreatorName") + val displayName = creatorName.displayName.coalesce(snapshot.creatorNickname) + return queryFactory + .select(snapshot, displayName) + .from(snapshot) + .leftJoin(member).on(member.id.eq(snapshot.creatorMemberId)) + .leftJoin(creatorName.name).on(creatorName.condition) + .where( + snapshot.rankingType.eq(rankingType), + snapshot.visibleFromAtUtc.eq( + JPAExpressions.select(latest.visibleFromAtUtc.max()).from(latest) + .where(latest.rankingType.eq(rankingType), latest.visibleFromAtUtc.loe(nowUtc)) + ) + ) + .orderBy(snapshot.rank.asc()) + .fetch() + .map { it.get(snapshot)!!.toRecord(displayCreatorNickname = it.get(displayName)!!) } + .translateTitles(locale) } override fun findPreviousVisibleSnapshots( @@ -31,7 +57,7 @@ class DefaultAudioRankingSnapshotPersistenceAdapter( rankingType = rankingType.name, currentAggregationStartAtUtc = currentAggregationStartAtUtc, nowUtc = nowUtc - ).toRecords(locale) + ).map { it.toRecord() }.translateTitles(locale) } @Transactional @@ -50,7 +76,7 @@ class DefaultAudioRankingSnapshotPersistenceAdapter( repository.saveAll(newSnapshots.map { it.toEntity(visibleFromAtUtc) }) } - private fun List.toRecords(locale: String): List { + private fun List.translateTitles(locale: String): List { if (isEmpty()) return emptyList() val translatedTitleByContentId = contentTranslationRepository.findByContentIdInAndLocale( map { it.contentId }, @@ -58,19 +84,19 @@ class DefaultAudioRankingSnapshotPersistenceAdapter( ).mapNotNull { translation -> translation.renderedPayload.title.takeIf { it.isNotBlank() }?.let { translation.contentId to it } }.toMap() - return map { snapshot -> snapshot.toRecord(translatedTitleByContentId[snapshot.contentId] ?: snapshot.title) } + return map { snapshot -> snapshot.copy(title = translatedTitleByContentId[snapshot.contentId] ?: snapshot.title) } } - private fun AudioRankingSnapshot.toRecord(displayTitle: String = title): AudioRankingSnapshotRecord { + private fun AudioRankingSnapshot.toRecord(displayCreatorNickname: String = creatorNickname): AudioRankingSnapshotRecord { return AudioRankingSnapshotRecord( rankingType = rankingType, aggregationStartAtUtc = aggregationStartAtUtc, aggregationEndAtUtc = aggregationEndAtUtc, visibleFromAtUtc = visibleFromAtUtc, contentId = contentId, - title = displayTitle, + title = title, creatorMemberId = creatorMemberId, - creatorNickname = creatorNickname, + creatorNickname = displayCreatorNickname, coverImageUrl = coverImageUrl, releaseDate = releaseDate, isAdult = isAdult, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryService.kt index e0c95d4c..396e2ddc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryService.kt @@ -1,6 +1,6 @@ package kr.co.vividnext.sodalive.v2.content.ranking.application -import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl @@ -19,7 +19,6 @@ import java.time.ZonedDateTime @Service class AudioRankingQueryService( private val snapshotPort: AudioRankingSnapshotPort, - private val langContext: LangContext, private val memberContentPreferenceService: MemberContentPreferenceService, private val blockPort: AudioRankingBlockPort, private val jobService: AudioRankingSnapshotJobService, @@ -29,9 +28,9 @@ class AudioRankingQueryService( ) { private val log = LoggerFactory.getLogger(javaClass) - fun getRankings(type: AudioRankingType, member: Member?): AudioRanking { + fun getRankings(type: AudioRankingType, member: Member?, lang: Lang): AudioRanking { val nowUtc = nowProvider().withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime() - val locale = langContext.lang.code + val locale = lang.code val latestSnapshots = findLatestVisibleSnapshots(type, nowUtc, locale) if (latestSnapshots.isEmpty()) { return AudioRanking(showRankChange = false, type = type, items = emptyList()) @@ -114,7 +113,8 @@ class AudioRankingQueryService( rank = rank, rankChange = if (showRankChange && previousRank != null) previousRank - rank else null, isNew = showRankChange && previousRank == null, - coverImageUrl = coverImageUrl.toCdnUrl(cloudFrontHost) + coverImageUrl = coverImageUrl.toCdnUrl(cloudFrontHost), + creatorMemberId = creatorMemberId ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRanking.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRanking.kt index 0f488f97..0232723a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRanking.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRanking.kt @@ -13,5 +13,6 @@ data class AudioRankingItem( val rank: Int, val rankChange: Int?, val isNew: Boolean, - val coverImageUrl: String? + val coverImageUrl: String?, + val creatorMemberId: Long ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt index 70923408..33fb1e81 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt @@ -26,6 +26,7 @@ import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationVisibility import kr.co.vividnext.sodalive.v2.content.recommendation.domain.CommentedAudio import kr.co.vividnext.sodalive.v2.content.recommendation.domain.OriginalSeries +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord import org.springframework.beans.factory.annotation.Value @@ -120,7 +121,7 @@ class DefaultAudioRecommendationQueryRepository( now: LocalDateTime, locale: String ): List { - val rows = audioRows(memberId, canViewAdultContent, now) { + val rows = audioRows(memberId, canViewAdultContent, now, locale) { orderBy(audioContent.releaseDate.desc(), audioContent.id.desc()).limit(limit.toLong()) } return rows.toAudioCards(now, canViewAdultContent, locale) @@ -134,7 +135,7 @@ class DefaultAudioRecommendationQueryRepository( locale: String ): List { val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')") - val rows = audioRows(memberId, canViewAdultContent, now, audioContent.price.eq(0)) { + val rows = audioRows(memberId, canViewAdultContent, now, locale, audioContent.price.eq(0)) { orderBy(randomTieBreaker.asc()).limit(limit.toLong()) } return rows.toAudioCards(now, canViewAdultContent, locale) @@ -152,6 +153,7 @@ class DefaultAudioRecommendationQueryRepository( memberId, canViewAdultContent, now, + locale, audioContent.isPointAvailable.isTrue.and(audioContent.price.gt(0)) ) { orderBy(randomTieBreaker.asc()).limit(limit.toLong()) @@ -168,7 +170,7 @@ class DefaultAudioRecommendationQueryRepository( ): List { if (contentIds.isEmpty()) return emptyList() val orderById = contentIds.withIndex().associate { it.value to it.index } - val rows = audioRows(memberId, canViewAdultContent, now, audioContent.id.`in`(contentIds)) { this } + val rows = audioRows(memberId, canViewAdultContent, now, locale, audioContent.id.`in`(contentIds)) { this } return rows.toAudioCards(now, canViewAdultContent, locale).sortedBy { orderById[it.audioContentId] ?: Int.MAX_VALUE } } @@ -444,9 +446,11 @@ class DefaultAudioRecommendationQueryRepository( memberId: Long?, canViewAdultContent: Boolean, now: LocalDateTime, + locale: String, extraCondition: BooleanExpression? = null, customize: com.querydsl.jpa.impl.JPAQuery.() -> com.querydsl.jpa.impl.JPAQuery ): List { + val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "audioRecommendationCreatorName") return queryFactory .select( audioContent.id, @@ -457,10 +461,11 @@ class DefaultAudioRecommendationQueryRepository( audioContent.isAdult, audioContent.isPointAvailable, audioContent.member.id, - member.nickname + creatorName.displayName ) .from(audioContent) .join(audioContent.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .join(audioContent.theme, audioContentTheme) .where(publicAudioCondition(memberId, canViewAdultContent, now), extraCondition) .customize() @@ -478,6 +483,7 @@ class DefaultAudioRecommendationQueryRepository( val firstContentIdByCreatorId = firstAudioContentIds(creatorIds, now, canViewAdultContent) val isOriginalSeriesByContentId = originalSeriesFlags(contentIds) val translatedTitleByContentId = translatedTitles(contentIds, locale) + val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "audioRecommendationCreatorName") return map { row -> val contentId = row.get(audioContent.id)!! val creatorId = row.get(audioContent.member.id)!! @@ -491,7 +497,8 @@ class DefaultAudioRecommendationQueryRepository( isPointAvailable = row.get(audioContent.isPointAvailable)!!, isFirstContent = firstContentIdByCreatorId[creatorId] == contentId, isOriginalSeries = isOriginalSeriesByContentId[contentId] ?: false, - creatorNickname = row.get(member.nickname)!! + creatorNickname = row.get(creatorName.displayName)!!, + creatorMemberId = creatorId ) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt index 2cf8a09a..2573c9cc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryService.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.v2.content.recommendation.application 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.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard @@ -18,16 +17,15 @@ import java.time.ZoneId class AudioRecommendationQueryService( private val queryPort: AudioRecommendationQueryPort, private val memberContentPreferenceService: MemberContentPreferenceService, - private val snapshotFallbackService: AudioRecommendationSnapshotFallbackService, - private val langContext: LangContext + private val snapshotFallbackService: AudioRecommendationSnapshotFallbackService ) { - fun getRecommendations(member: Member?): AudioRecommendations { + fun getRecommendations(member: Member?, lang: Lang): AudioRecommendations { val now = LocalDateTime.now() val fallbackNow = LocalDateTime.now(KST_ZONE) val canViewAdultContent = canViewAdultContent(member) val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE val memberId = member?.id - val locale = langContext.lang.code + val locale = lang.code val bannerLang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO val newAndHotSectionType = newAndHotSectionType(visibility) val mostCommentedSectionType = mostCommentedSectionType(visibility) @@ -77,7 +75,7 @@ class AudioRecommendationQueryService( ) } - fun findNewAndHotAudios(member: Member, offset: Long, limit: Int): List { + fun findNewAndHotAudios(member: Member, offset: Long, limit: Int, lang: Lang): List { val now = LocalDateTime.now() val fallbackNow = LocalDateTime.now(KST_ZONE) val canViewAdultContent = canViewAdultContent(member) @@ -90,7 +88,7 @@ class AudioRecommendationQueryService( member.id, canViewAdultContent, now, - langContext.lang.code + lang.code ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/domain/AudioRecommendation.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/domain/AudioRecommendation.kt index 0721a131..4756f94c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/domain/AudioRecommendation.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/domain/AudioRecommendation.kt @@ -28,7 +28,8 @@ data class AudioCard( val isPointAvailable: Boolean, val isFirstContent: Boolean, val isOriginalSeries: Boolean, - val creatorNickname: String + val creatorNickname: String, + val creatorMemberId: Long ) data class CommentedAudio( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt index 19ec10f8..67f3d5ec 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt @@ -15,12 +15,14 @@ import kr.co.vividnext.sodalive.content.theme.translation.QContentThemeTranslati import kr.co.vividnext.sodalive.content.translation.QContentTranslation import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out.CreatorChannelAudioContentRecord import kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out.CreatorChannelAudioCreatorRecord import kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out.CreatorChannelAudioThemeRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -28,17 +30,19 @@ import java.time.LocalDateTime class DefaultCreatorChannelAudioQueryRepository( private val queryFactory: JPAQueryFactory ) : CreatorChannelAudioQueryRepository { - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelAudioCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelAudioCreatorRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelAudioCreatorName") return queryFactory .select( Projections.constructor( CreatorChannelAudioCreatorRecord::class.java, member.id, member.role, - member.nickname + creatorName.displayName ) ) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.id.eq(creatorId), member.isActive.isTrue diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt index f3b8e0b5..676c2e50 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.audio.application 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 @@ -39,18 +40,19 @@ class CreatorChannelAudioQueryService( themeId: Long?, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelAudioTab { val resolvedSort = queryPolicy.resolveSort(sort) val audioPage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! - val creator = queryPort.findCreator(creatorId, viewerId) + val creator = queryPort.findCreator(creatorId, viewerId, lang) ?: throw SodaException(messageKey = "member.validation.user_not_found") if (queryPort.existsBlockedBetween(viewerId, creatorId)) { val messageTemplate = messageSource - .getMessage("explorer.creator.blocked_access", langContext.lang) + .getMessage("explorer.creator.blocked_access", lang) .orEmpty() throw SodaException(message = String.format(messageTemplate, creator.nickname)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/port/out/CreatorChannelAudioQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/port/out/CreatorChannelAudioQueryPort.kt index dfa848b1..e43460b9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/port/out/CreatorChannelAudioQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/port/out/CreatorChannelAudioQueryPort.kt @@ -1,11 +1,12 @@ package kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import java.time.LocalDateTime interface CreatorChannelAudioQueryPort { - fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelAudioCreatorRecord? + fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelAudioCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt index 9ee6efd3..eff7f526 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt @@ -4,28 +4,37 @@ import com.querydsl.core.Tuple import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.core.types.dsl.CaseBuilder +import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.QUseCan.useCan import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommunity.creatorCommunity import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentContextRecord import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCreatorRecord import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityPostRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository @Repository class DefaultCreatorChannelCommunityQueryRepository( private val queryFactory: JPAQueryFactory ) : CreatorChannelCommunityQueryRepository { - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord? { + private val postCreatorDisplayName = Expressions.stringPath("postCreatorDisplayName") + private val commentWriterDisplayName = Expressions.stringPath("commentWriterDisplayName") + private val localizedWriterName = Expressions.booleanPath("localizedWriterName") + + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCommunityCreatorRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelCommunityCreatorName") val creator = queryFactory - .select(member.id, member.role, member.nickname) + .select(member.id, member.role, creatorName.displayName) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.id.eq(creatorId), member.isActive.isTrue @@ -35,7 +44,7 @@ class DefaultCreatorChannelCommunityQueryRepository( return CreatorChannelCommunityCreatorRecord( creatorId = creator.get(member.id)!!, role = creator.get(member.role)!!, - nickname = creator.get(member.nickname)!! + nickname = creator.get(creatorName.displayName)!! ) } @@ -71,11 +80,15 @@ class DefaultCreatorChannelCommunityQueryRepository( viewerId: Long, canViewAdultContent: Boolean, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { + val creatorName = CreatorNameJoin(member, lang, "channelCommunityPostCreatorName") val rows = queryFactory - .selectCommunityPostRow() + .selectCommunityPostRow(creatorName) .from(creatorCommunity) + .join(creatorCommunity.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .where(communityPostCondition(creatorId, canViewAdultContent)) .orderBy( CaseBuilder() @@ -104,11 +117,15 @@ class DefaultCreatorChannelCommunityQueryRepository( viewerId: Long, isPinned: Boolean, canViewAdultContent: Boolean, - limit: Int + limit: Int, + lang: Lang ): List { + val creatorName = CreatorNameJoin(member, lang, "channelHomePostCreatorName") val rows = queryFactory - .selectCommunityPostRow() + .selectCommunityPostRow(creatorName) .from(creatorCommunity) + .join(creatorCommunity.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( homeCommunityPostCondition(creatorId, viewerId, canViewAdultContent), creatorCommunity.isFixed.eq(isPinned), @@ -124,11 +141,15 @@ class DefaultCreatorChannelCommunityQueryRepository( override fun findCommunityPost( postId: Long, viewerId: Long, - canViewAdultContent: Boolean + canViewAdultContent: Boolean, + lang: Lang ): CreatorChannelCommunityPostRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelPostDetailCreatorName") val rows = queryFactory - .selectCommunityPostRow() + .selectCommunityPostRow(creatorName) .from(creatorCommunity) + .join(creatorCommunity.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .where(communityPostByIdCondition(postId, canViewAdultContent)) .fetch() @@ -155,11 +176,15 @@ class DefaultCreatorChannelCommunityQueryRepository( viewerId: Long, creatorId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { + val writerName = CreatorNameJoin(member, lang, "channelCommentWriterName") return queryFactory - .selectCommunityCommentRow() + .selectCommunityCommentRow(writerName) .from(creatorCommunityComment) + .join(creatorCommunityComment.member, member) + .leftJoin(writerName.name).on(writerName.condition) .where(rootCommentCondition(postId, creatorId, viewerId)) .orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc()) .offset(offset) @@ -170,13 +195,17 @@ class DefaultCreatorChannelCommunityQueryRepository( override fun findLatestRepliesByCommentIds( commentIds: List, - viewerId: Long + viewerId: Long, + lang: Lang ): List { if (commentIds.isEmpty()) return emptyList() + val writerName = CreatorNameJoin(member, lang, "channelLatestReplyWriterName") return queryFactory - .selectCommunityCommentRow() + .selectCommunityCommentRow(writerName) .from(creatorCommunityComment) + .join(creatorCommunityComment.member, member) + .leftJoin(writerName.name).on(writerName.condition) .where(replyCondition(commentIds, viewerId)) .orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc()) .fetch() @@ -224,11 +253,15 @@ class DefaultCreatorChannelCommunityQueryRepository( commentId: Long, viewerId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { + val writerName = CreatorNameJoin(member, lang, "channelReplyWriterName") return queryFactory - .selectCommunityCommentRow() + .selectCommunityCommentRow(writerName) .from(creatorCommunityComment) + .join(creatorCommunityComment.member, member) + .leftJoin(writerName.name).on(writerName.condition) .where(replyCondition(commentId, viewerId)) .orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc()) .offset(offset) @@ -237,10 +270,10 @@ class DefaultCreatorChannelCommunityQueryRepository( .map { it.toCommunityCommentRecord() } } - private fun JPAQueryFactory.selectCommunityPostRow() = select( + private fun JPAQueryFactory.selectCommunityPostRow(creatorName: CreatorNameJoin) = select( creatorCommunity.id, creatorCommunity.member.id, - creatorCommunity.member.nickname, + creatorName.displayName.`as`(postCreatorDisplayName), creatorCommunity.member.profileImage, creatorCommunity.imagePath, creatorCommunity.audioPath, @@ -252,16 +285,17 @@ class DefaultCreatorChannelCommunityQueryRepository( creatorCommunity.isCommentAvailable ) - private fun JPAQueryFactory.selectCommunityCommentRow() = select( + private fun JPAQueryFactory.selectCommunityCommentRow(writerName: CreatorNameJoin) = select( creatorCommunityComment.id, creatorCommunityComment.parent.id, creatorCommunityComment.creatorCommunity.id, creatorCommunityComment.member.id, creatorCommunityComment.member.profileImage, - creatorCommunityComment.member.nickname, + writerName.displayName.`as`(commentWriterDisplayName), creatorCommunityComment.comment, creatorCommunityComment.isSecret, - creatorCommunityComment.createdAt + creatorCommunityComment.createdAt, + writerName.name.id.isNotNull.`as`(localizedWriterName) ) private fun communityPostCondition(creatorId: Long, canViewAdultContent: Boolean): BooleanExpression { @@ -519,7 +553,7 @@ class DefaultCreatorChannelCommunityQueryRepository( get() = get(creatorCommunity.member.id)!! private val Tuple.creatorNickname: String - get() = get(creatorCommunity.member.nickname)!! + get() = get(postCreatorDisplayName)!! private val Tuple.creatorProfilePath: String? get() = get(creatorCommunity.member.profileImage) @@ -555,10 +589,11 @@ class DefaultCreatorChannelCommunityQueryRepository( postId = get(creatorCommunityComment.creatorCommunity.id)!!, writerId = get(creatorCommunityComment.member.id)!!, writerProfilePath = get(creatorCommunityComment.member.profileImage), - writerNickname = get(creatorCommunityComment.member.nickname)!!, + writerNickname = get(commentWriterDisplayName)!!, content = get(creatorCommunityComment.comment)!!, isSecret = get(creatorCommunityComment.isSecret)!!, - createdAt = get(creatorCommunityComment.createdAt)!! + createdAt = get(creatorCommunityComment.createdAt)!!, + isLocalizedName = get(localizedWriterName)!! ) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt index 785d6015..2385b674 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community.application import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix +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 @@ -46,17 +47,18 @@ class CreatorChannelCommunityQueryService( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityTab { val communityPage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! - val creator = queryPort.findCreator(creatorId, viewerId) + val creator = queryPort.findCreator(creatorId, viewerId, lang) ?: throw SodaException(messageKey = "member.validation.user_not_found") if (queryPort.existsBlockedBetween(viewerId, creatorId)) { val messageTemplate = messageSource - .getMessage("explorer.creator.blocked_access", langContext.lang) + .getMessage("explorer.creator.blocked_access", lang) .orEmpty() throw SodaException(message = String.format(messageTemplate, creator.nickname)) } @@ -69,7 +71,8 @@ class CreatorChannelCommunityQueryService( viewerId = viewerId, canViewAdultContent = canViewAdultContent, offset = communityPage.offset, - limit = communityPage.fetchLimit + limit = communityPage.fetchLimit, + lang = lang ) val communityPosts = queryPolicy.limitItems(fetchedPosts, communityPage) val displayContents = creatorCommunityTranslationService @@ -94,7 +97,8 @@ class CreatorChannelCommunityQueryService( viewerId: Long, isPinned: Boolean, canViewAdultContent: Boolean, - limit: Int + limit: Int, + lang: Lang ): List { val posts = queryPortProvider.getObject() .findHomeCommunityPosts( @@ -102,7 +106,8 @@ class CreatorChannelCommunityQueryService( viewerId = viewerId, isPinned = isPinned, canViewAdultContent = canViewAdultContent, - limit = limit + limit = limit, + lang = lang ) val displayContents = creatorCommunityTranslationService .findDisplayContents(posts.map { it.postId }, langContext.lang.code) @@ -115,12 +120,13 @@ class CreatorChannelCommunityQueryService( fun getCommunityPostDetail( postId: Long, viewer: Member, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityPostDetail { val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(viewer) - val postRecord = queryPort.findCommunityPost(postId, viewerId, canViewAdultContent) + val postRecord = queryPort.findCommunityPost(postId, viewerId, canViewAdultContent, lang) ?: throw SodaException(messageKey = INVALID_COMMUNITY_REQUEST_MESSAGE_KEY) validateNotBlocked(queryPort, viewerId, postRecord.creatorId) @@ -134,7 +140,7 @@ class CreatorChannelCommunityQueryService( return CreatorChannelCommunityPostDetail( post = postRecord.copy(content = displayContent?.content ?: postRecord.content).toDomain(viewerId), - comments = getCommunityComments(postId, viewer, page = 0, size = 20, now = now) + comments = getCommunityComments(postId, viewer, page = 0, size = 20, now = now, lang = lang) ) } @@ -143,13 +149,14 @@ class CreatorChannelCommunityQueryService( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityComments { val communityPage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(viewer) - val postRecord = queryPort.findCommunityPost(postId, viewerId, canViewAdultContent) + val postRecord = queryPort.findCommunityPost(postId, viewerId, canViewAdultContent, lang) ?: throw SodaException(messageKey = INVALID_COMMUNITY_REQUEST_MESSAGE_KEY) validateNotBlocked(queryPort, viewerId, postRecord.creatorId) @@ -168,12 +175,14 @@ class CreatorChannelCommunityQueryService( viewerId = viewerId, creatorId = postRecord.creatorId, offset = communityPage.offset, - limit = communityPage.fetchLimit + limit = communityPage.fetchLimit, + lang = lang ) val limitedComments = queryPolicy.limitItems(fetchedComments, communityPage) val latestReplies = queryPort.findLatestRepliesByCommentIds( commentIds = limitedComments.map { it.commentId }, - viewerId = viewerId + viewerId = viewerId, + lang = lang ).associateBy { it.parentCommentId } return CreatorChannelCommunityComments( @@ -193,7 +202,8 @@ class CreatorChannelCommunityQueryService( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelCommunityReplies { val communityPage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() @@ -217,7 +227,8 @@ class CreatorChannelCommunityQueryService( commentId = commentId, viewerId = viewerId, offset = communityPage.offset, - limit = communityPage.fetchLimit + limit = communityPage.fetchLimit, + lang = lang ) return CreatorChannelCommunityReplies( @@ -274,7 +285,7 @@ class CreatorChannelCommunityQueryService( commentId = commentId, writerId = writerId, writerProfileImageUrl = writerProfilePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), - writerNickname = writerNickname.removeDeletedNicknamePrefix(), + writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(), content = content, isSecret = isSecret, createdAt = createdAt, @@ -287,7 +298,7 @@ class CreatorChannelCommunityQueryService( commentId = commentId, writerId = writerId, writerProfileImageUrl = writerProfilePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), - writerNickname = writerNickname.removeDeletedNicknamePrefix(), + writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(), content = content, createdAt = createdAt ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt index d5cdd98c..7ca0ff5d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt @@ -1,10 +1,11 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community.port.out +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.MemberRole import java.time.LocalDateTime interface CreatorChannelCommunityQueryPort { - fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord? + fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCommunityCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean @@ -19,7 +20,8 @@ interface CreatorChannelCommunityQueryPort { viewerId: Long, canViewAdultContent: Boolean, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List fun findHomeCommunityPosts( @@ -27,13 +29,15 @@ interface CreatorChannelCommunityQueryPort { viewerId: Long, isPinned: Boolean, canViewAdultContent: Boolean, - limit: Int + limit: Int, + lang: Lang ): List fun findCommunityPost( postId: Long, viewerId: Long, - canViewAdultContent: Boolean + canViewAdultContent: Boolean, + lang: Lang ): CreatorChannelCommunityPostRecord? fun countCommunityComments( @@ -47,12 +51,14 @@ interface CreatorChannelCommunityQueryPort { viewerId: Long, creatorId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List fun findLatestRepliesByCommentIds( commentIds: List, - viewerId: Long + viewerId: Long, + lang: Lang ): List fun findRootCommentContext( @@ -70,7 +76,8 @@ interface CreatorChannelCommunityQueryPort { commentId: Long, viewerId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List } @@ -107,7 +114,8 @@ data class CreatorChannelCommunityCommentRecord( val writerNickname: String, val content: String, val isSecret: Boolean, - val createdAt: LocalDateTime + val createdAt: LocalDateTime, + val isLocalizedName: Boolean = false ) data class CreatorChannelCommunityCommentContextRecord( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt index cc59ab1c..289f9633 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt @@ -4,11 +4,13 @@ import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicy import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationCreatorRecord import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -18,16 +20,18 @@ class DefaultCreatorChannelDonationQueryRepository( ) : CreatorChannelDonationQueryRepository { private val queryPolicy = CreatorChannelDonationQueryPolicy() - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelDonationCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelDonationCreatorRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelDonationCreatorName") val creator = queryFactory .select( member.id, member.role, - member.nickname, + creatorName.displayName, member.isVisibleDonationRank, member.donationRankingPeriod ) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.id.eq(creatorId), member.isActive.isTrue @@ -37,7 +41,7 @@ class DefaultCreatorChannelDonationQueryRepository( return CreatorChannelDonationCreatorRecord( creatorId = creator.get(member.id)!!, role = creator.get(member.role)!!, - nickname = creator.get(member.nickname)!!, + nickname = creator.get(creatorName.displayName)!!, isVisibleDonationRank = creator.get(member.isVisibleDonationRank)!!, donationRankingPeriod = creator.get(member.donationRankingPeriod) ) @@ -75,20 +79,26 @@ class DefaultCreatorChannelDonationQueryRepository( viewerId: Long, now: LocalDateTime, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { + val donorName = CreatorNameJoin(member, lang, "channelDonationDonorName") return queryFactory .select( Projections.constructor( CreatorChannelDonationRecord::class.java, - channelDonationMessage.member.nickname, + channelDonationMessage.member.id, + donorName.displayName, channelDonationMessage.member.profileImage, channelDonationMessage.can, channelDonationMessage.additionalMessage, - channelDonationMessage.createdAt + channelDonationMessage.createdAt, + donorName.name.id.isNotNull ) ) .from(channelDonationMessage) + .join(channelDonationMessage.member, member) + .leftJoin(donorName.name).on(donorName.condition) .where(channelDonationCondition(creatorId, viewerId, now)) .orderBy(channelDonationMessage.createdAt.desc(), channelDonationMessage.id.desc()) .offset(offset) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt index 38ec62b8..0b3fbd20 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt @@ -2,7 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.donation.application import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix -import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.member.DonationRankingPeriod import kr.co.vividnext.sodalive.member.Member @@ -30,7 +30,6 @@ class CreatorChannelDonationQueryService( private val rankingPort: CreatorChannelDonationRankingPort, private val queryPolicy: CreatorChannelDonationQueryPolicy, private val messageSource: SodaMessageSource, - private val langContext: LangContext, @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { @@ -39,17 +38,18 @@ class CreatorChannelDonationQueryService( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime + now: LocalDateTime, + lang: Lang ): CreatorChannelDonationTab { val donationPage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! - val creator = queryPort.findCreator(creatorId, viewerId) + val creator = queryPort.findCreator(creatorId, viewerId, lang) ?: throw SodaException(messageKey = "member.validation.user_not_found") if (queryPort.existsBlockedBetween(viewerId, creatorId)) { val messageTemplate = messageSource - .getMessage("explorer.creator.blocked_access", langContext.lang) + .getMessage("explorer.creator.blocked_access", lang) .orEmpty() throw SodaException(message = String.format(messageTemplate, creator.nickname)) } @@ -61,7 +61,8 @@ class CreatorChannelDonationQueryService( viewerId = viewerId, now = now, offset = donationPage.offset, - limit = donationPage.fetchLimit + limit = donationPage.fetchLimit, + lang = lang ) return CreatorChannelDonationTab( @@ -95,7 +96,8 @@ class CreatorChannelDonationQueryService( } private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation( - nickname = nickname.removeDeletedNicknamePrefix(), + memberId = memberId, + nickname = if (isLocalizedName) nickname else nickname.removeDeletedNicknamePrefix(), profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), can = can, message = message.orEmpty(), diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt index ad91d33f..3ead9d17 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt @@ -19,6 +19,7 @@ data class CreatorChannelDonationRanking( ) data class CreatorChannelDonation( + val memberId: Long, val nickname: String, val profileImageUrl: String, val can: Int, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt index 48cba338..3f50af07 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt @@ -1,11 +1,12 @@ package kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.DonationRankingPeriod import kr.co.vividnext.sodalive.member.MemberRole import java.time.LocalDateTime interface CreatorChannelDonationQueryPort { - fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelDonationCreatorRecord? + fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelDonationCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean @@ -20,7 +21,8 @@ interface CreatorChannelDonationQueryPort { viewerId: Long, now: LocalDateTime, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List } @@ -33,9 +35,11 @@ data class CreatorChannelDonationCreatorRecord( ) data class CreatorChannelDonationRecord( + val memberId: Long, val nickname: String, val profileImagePath: String?, val can: Int, val message: String?, - val createdAt: LocalDateTime + val createdAt: LocalDateTime, + val isLocalizedName: Boolean = false ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepository.kt index 5e808c63..dac6b022 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepository.kt @@ -4,21 +4,25 @@ import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkCreatorRecord import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkRecord import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkReplyRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository @Repository class DefaultCreatorChannelFanTalkQueryRepository( private val queryFactory: JPAQueryFactory ) : CreatorChannelFanTalkQueryRepository { - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkCreatorRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelFanTalkCreatorName") val creator = queryFactory - .select(member.id, member.role, member.nickname) + .select(member.id, member.role, creatorName.displayName) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.id.eq(creatorId), member.isActive.isTrue @@ -28,7 +32,7 @@ class DefaultCreatorChannelFanTalkQueryRepository( return CreatorChannelFanTalkCreatorRecord( creatorId = creator.get(member.id)!!, role = creator.get(member.role)!!, - nickname = creator.get(member.nickname)!! + nickname = creator.get(creatorName.displayName)!! ) } @@ -59,21 +63,26 @@ class DefaultCreatorChannelFanTalkQueryRepository( creatorId: Long, viewerId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { + val writerName = CreatorNameJoin(member, lang, "channelFanTalkWriterName") return queryFactory .select( Projections.constructor( CreatorChannelFanTalkRecord::class.java, creatorCheers.id, creatorCheers.member.id, - creatorCheers.member.nickname, + writerName.displayName, creatorCheers.member.profileImage, creatorCheers.cheers, - creatorCheers.createdAt + creatorCheers.createdAt, + writerName.name.id.isNotNull ) ) .from(creatorCheers) + .join(creatorCheers.member, member) + .leftJoin(writerName.name).on(writerName.condition) .where(fanTalkCondition(creatorId, viewerId)) .orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc()) .offset(offset) @@ -83,9 +92,11 @@ class DefaultCreatorChannelFanTalkQueryRepository( override fun findCreatorReplies( creatorId: Long, - parentFanTalkIds: List + parentFanTalkIds: List, + lang: Lang ): List { if (parentFanTalkIds.isEmpty()) return emptyList() + val writerName = CreatorNameJoin(member, lang, "channelFanTalkReplyWriterName") return queryFactory .select( @@ -94,13 +105,16 @@ class DefaultCreatorChannelFanTalkQueryRepository( creatorCheers.id, creatorCheers.parent.id, creatorCheers.member.id, - creatorCheers.member.nickname, + writerName.displayName, creatorCheers.member.profileImage, creatorCheers.cheers, - creatorCheers.createdAt + creatorCheers.createdAt, + writerName.name.id.isNotNull ) ) .from(creatorCheers) + .join(creatorCheers.member, member) + .leftJoin(writerName.name).on(writerName.condition) .where( creatorCheers.creator.id.eq(creatorId), creatorCheers.member.id.eq(creatorId), diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt index a5b43316..ff80e832 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt @@ -2,7 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix -import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole @@ -27,7 +27,6 @@ class CreatorChannelFanTalkQueryService( private val queryPortProvider: ObjectProvider, private val queryPolicy: CreatorChannelFanTalkQueryPolicy, private val messageSource: SodaMessageSource, - private val langContext: LangContext, @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { @@ -36,17 +35,18 @@ class CreatorChannelFanTalkQueryService( viewer: Member, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelFanTalkTab { val fanTalkPage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! - val creator = queryPort.findCreator(creatorId, viewerId) + val creator = queryPort.findCreator(creatorId, viewerId, lang) ?: throw SodaException(messageKey = "member.validation.user_not_found") if (queryPort.existsBlockedBetween(viewerId, creatorId)) { val messageTemplate = messageSource - .getMessage("explorer.creator.blocked_access", langContext.lang) + .getMessage("explorer.creator.blocked_access", lang) .orEmpty() throw SodaException(message = String.format(messageTemplate, creator.nickname)) } @@ -57,10 +57,11 @@ class CreatorChannelFanTalkQueryService( creatorId = creatorId, viewerId = viewerId, offset = fanTalkPage.offset, - limit = fanTalkPage.fetchLimit + limit = fanTalkPage.fetchLimit, + lang = lang ) val fanTalkRecords = queryPolicy.limitItems(fetchedFanTalks, fanTalkPage) - val repliesByParentId = findRepliesByParentId(queryPort, creatorId, fanTalkRecords) + val repliesByParentId = findRepliesByParentId(queryPort, creatorId, fanTalkRecords, lang) return CreatorChannelFanTalkTab( fanTalkCount = queryPort.countFanTalks(creatorId, viewerId), @@ -80,11 +81,12 @@ class CreatorChannelFanTalkQueryService( private fun findRepliesByParentId( queryPort: CreatorChannelFanTalkQueryPort, creatorId: Long, - fanTalkRecords: List + fanTalkRecords: List, + lang: Lang ): Map> { val parentFanTalkIds = fanTalkRecords.map { it.fanTalkId } if (parentFanTalkIds.isEmpty()) return emptyMap() - return queryPort.findCreatorReplies(creatorId, parentFanTalkIds) + return queryPort.findCreatorReplies(creatorId, parentFanTalkIds, lang) .groupBy( keySelector = { it.parentFanTalkId }, valueTransform = { it.toDomain() } @@ -96,7 +98,7 @@ class CreatorChannelFanTalkQueryService( ) = CreatorChannelFanTalk( fanTalkId = fanTalkId, writerId = writerId, - writerNickname = writerNickname.removeDeletedNicknamePrefix(), + writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(), writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), content = content, createdAt = createdAt, @@ -106,7 +108,7 @@ class CreatorChannelFanTalkQueryService( private fun CreatorChannelFanTalkReplyRecord.toDomain() = CreatorChannelFanTalkReply( fanTalkId = fanTalkId, writerId = writerId, - writerNickname = writerNickname.removeDeletedNicknamePrefix(), + writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(), writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), content = content, createdAt = createdAt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/port/out/CreatorChannelFanTalkQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/port/out/CreatorChannelFanTalkQueryPort.kt index 957408cb..3eddf5e1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/port/out/CreatorChannelFanTalkQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/port/out/CreatorChannelFanTalkQueryPort.kt @@ -1,10 +1,11 @@ package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.MemberRole import java.time.LocalDateTime interface CreatorChannelFanTalkQueryPort { - fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkCreatorRecord? + fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean @@ -14,12 +15,14 @@ interface CreatorChannelFanTalkQueryPort { creatorId: Long, viewerId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List fun findCreatorReplies( creatorId: Long, - parentFanTalkIds: List + parentFanTalkIds: List, + lang: Lang ): List } @@ -35,7 +38,8 @@ data class CreatorChannelFanTalkRecord( val writerNickname: String, val writerProfileImagePath: String?, val content: String, - val createdAt: LocalDateTime + val createdAt: LocalDateTime, + val isLocalizedName: Boolean = false ) data class CreatorChannelFanTalkReplyRecord( @@ -45,5 +49,6 @@ data class CreatorChannelFanTalkReplyRecord( val writerNickname: String, val writerProfileImagePath: String?, val content: String, - val createdAt: LocalDateTime + val createdAt: LocalDateTime, + val isLocalizedName: Boolean = false ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt index 6d465e6f..36ab9621 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt @@ -17,11 +17,13 @@ import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seri import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.MemberKind +import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing @@ -36,6 +38,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.home.port.out.CreatorChannelL import kr.co.vividnext.sodalive.v2.creator.channel.home.port.out.CreatorChannelScheduleRecord import kr.co.vividnext.sodalive.v2.creator.channel.home.port.out.CreatorChannelSeriesRecord import kr.co.vividnext.sodalive.v2.creator.channel.home.port.out.CreatorChannelSnsRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository import java.time.Duration import java.time.LocalDateTime @@ -46,10 +49,12 @@ import java.time.temporal.ChronoUnit class DefaultCreatorChannelHomeQueryRepository( private val queryFactory: JPAQueryFactory ) : CreatorChannelHomeQueryRepository { - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelHomeCreatorName") val creator = queryFactory - .select(member.id, member.role, member.nickname, member.profileImage, member.introduce, member.memberKind) + .select(member.id, member.role, creatorName.displayName, member.profileImage, member.introduce, member.memberKind) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.id.eq(creatorId), member.isActive.isTrue @@ -81,7 +86,7 @@ class DefaultCreatorChannelHomeQueryRepository( creatorId = creator.get(member.id)!!, role = creator.get(member.role)!!, characterId = characterId, - nickname = creator.get(member.nickname)!!, + nickname = creator.get(creatorName.displayName)!!, profileImagePath = creator.get(member.profileImage), introduce = creator.get(member.introduce)!!, followerCount = queryFactory @@ -172,8 +177,11 @@ class DefaultCreatorChannelHomeQueryRepository( creatorId: Long, viewerId: Long?, now: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List { + val donor = QMember("channelHomeDonor") + val donorName = CreatorNameJoin(donor, lang, "channelHomeDonorName") val kstZoneId = ZoneId.of("Asia/Seoul") val utcZoneId = ZoneId.of("UTC") val nowKst = now.atZone(utcZoneId).withZoneSameInstant(kstZoneId) @@ -187,14 +195,18 @@ class DefaultCreatorChannelHomeQueryRepository( .select( Projections.constructor( CreatorChannelDonationRecord::class.java, - channelDonationMessage.member.nickname, + channelDonationMessage.member.id, + donorName.displayName, channelDonationMessage.member.profileImage, channelDonationMessage.can, channelDonationMessage.additionalMessage.coalesce(""), - channelDonationMessage.createdAt + channelDonationMessage.createdAt, + donorName.name.id.isNotNull ) ) .from(channelDonationMessage) + .join(channelDonationMessage.member, donor) + .leftJoin(donorName.name).on(donorName.condition) .where( channelDonationMessage.creator.id.eq(creatorId), channelDonationMessage.createdAt.goe(start), @@ -356,7 +368,9 @@ class DefaultCreatorChannelHomeQueryRepository( } } - override fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord { + override fun findFanTalkSummary(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkSummaryRecord { + val writer = QMember("channelHomeFanTalkWriter") + val writerName = CreatorNameJoin(writer, lang, "channelHomeFanTalkWriterName") val totalCount = queryFactory .select(creatorCheers.id.count()) .from(creatorCheers) @@ -371,19 +385,22 @@ class DefaultCreatorChannelHomeQueryRepository( CreatorChannelFanTalkRecord::class.java, creatorCheers.id, creatorCheers.member.id, - creatorCheers.member.nickname, + writerName.displayName, creatorCheers.member.profileImage, creatorCheers.cheers, creatorCheers.languageCode, - creatorCheers.createdAt + creatorCheers.createdAt, + writerName.name.id.isNotNull ) ) .from(creatorCheers) + .join(creatorCheers.member, writer) + .leftJoin(writerName.name).on(writerName.condition) .where(fanTalkSummaryCondition(creatorId, viewerId)) .orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc()) .limit(1) .fetchFirst() - ?.let { it.copy(nickname = it.nickname.removeDeletedNicknamePrefix()) } + ?.let { if (it.isLocalizedName) it else it.copy(nickname = it.nickname.removeDeletedNicknamePrefix()) } return CreatorChannelFanTalkSummaryRecord( totalCount = totalCount, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt index aacc99e8..1476ed04 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.home.application import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix +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.Gender @@ -54,15 +55,16 @@ class CreatorChannelHomeQueryService( fun getHome( creatorId: Long, viewer: Member, - now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC) + now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC), + lang: Lang ): CreatorChannelHome { val viewerId = viewer.id!! - val creator = queryPort.findCreator(creatorId, viewerId) + val creator = queryPort.findCreator(creatorId, viewerId, lang) ?: throw SodaException(messageKey = "member.validation.user_not_found") if (queryPort.existsBlockedBetween(viewerId, creatorId)) { val messageTemplate = messageSource - .getMessage("explorer.creator.blocked_access", langContext.lang) + .getMessage("explorer.creator.blocked_access", lang) .orEmpty() throw SodaException(message = String.format(messageTemplate, creator.nickname)) } @@ -100,13 +102,14 @@ class CreatorChannelHomeQueryService( effectiveViewerGender = effectiveViewerGender )?.toDomain(), latestAudioContent = latestAudioContent, - channelDonations = queryPort.findChannelDonations(creatorId, viewerId, now).map { it.toDomain() }, + channelDonations = queryPort.findChannelDonations(creatorId, viewerId, now, lang = lang).map { it.toDomain() }, notices = communityQueryService.findHomeCommunityPosts( creatorId = creatorId, viewerId = viewerId, isPinned = true, canViewAdultContent = canViewAdultContent, - limit = 3 + limit = 3, + lang = lang ), schedules = queryPolicy.limitSchedules( queryPort.findSchedules( @@ -135,9 +138,10 @@ class CreatorChannelHomeQueryService( viewerId = viewerId, isPinned = false, canViewAdultContent = canViewAdultContent, - limit = 3 + limit = 3, + lang = lang ), - fanTalk = queryPort.findFanTalkSummary(creatorId, viewerId).toDomain(), + fanTalk = queryPort.findFanTalkSummary(creatorId, viewerId, lang).toDomain(), introduce = creator.introduce, activity = queryPort.findActivity(creatorId, now).toDomain(), sns = queryPort.findSns(creatorId).toDomain() @@ -193,7 +197,8 @@ class CreatorChannelHomeQueryService( ) private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation( - nickname = nickname.removeDeletedNicknamePrefix(), + memberId = memberId, + nickname = if (isLocalizedName) nickname else nickname.removeDeletedNicknamePrefix(), profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), can = can, message = message, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHome.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHome.kt index f307f829..e36d61da 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHome.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHome.kt @@ -43,6 +43,7 @@ data class CreatorChannelLive( ) data class CreatorChannelDonation( + val memberId: Long, val nickname: String, val profileImageUrl: String, val can: Int, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt index 623f3a60..5325e485 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt @@ -1,13 +1,14 @@ package kr.co.vividnext.sodalive.v2.creator.channel.home.port.out import kr.co.vividnext.sodalive.content.ContentType +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType import java.time.LocalDateTime interface CreatorChannelHomeQueryPort { - fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? + fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean @@ -32,7 +33,8 @@ interface CreatorChannelHomeQueryPort { creatorId: Long, viewerId: Long?, now: LocalDateTime, - limit: Int = 8 + limit: Int = 8, + lang: Lang ): List fun findSchedules( @@ -66,7 +68,7 @@ interface CreatorChannelHomeQueryPort { limit: Int = 8 ): List - fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord + fun findFanTalkSummary(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkSummaryRecord fun findActivity(creatorId: Long, now: LocalDateTime): CreatorChannelActivityRecord @@ -112,11 +114,13 @@ data class CreatorChannelAudioContentRecord( ) data class CreatorChannelDonationRecord( + val memberId: Long, val nickname: String, val profileImagePath: String?, val can: Int, val message: String, - val createdAt: LocalDateTime + val createdAt: LocalDateTime, + val isLocalizedName: Boolean = false ) data class CreatorChannelScheduleRecord( @@ -148,7 +152,8 @@ data class CreatorChannelFanTalkRecord( val profileImagePath: String?, val content: String, val languageCode: String?, - val createdAt: LocalDateTime + val createdAt: LocalDateTime, + val isLocalizedName: Boolean = false ) data class CreatorChannelActivityRecord( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt index 514b48bc..1df940e5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt @@ -11,6 +11,7 @@ import kr.co.vividnext.sodalive.content.order.QOrder.order import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Gender @@ -20,6 +21,7 @@ import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelAudioContentRecord import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelCreatorRecord import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -27,17 +29,19 @@ import java.time.LocalDateTime class DefaultCreatorChannelLiveQueryRepository( private val queryFactory: JPAQueryFactory ) : CreatorChannelLiveQueryRepository { - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelLiveCreatorName") return queryFactory .select( Projections.constructor( CreatorChannelCreatorRecord::class.java, member.id, member.role, - member.nickname + creatorName.displayName ) ) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.id.eq(creatorId), member.isActive.isTrue diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt index 3301359a..11cc83f4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt @@ -1,7 +1,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.live.application import kr.co.vividnext.sodalive.common.SodaException -import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member @@ -30,7 +30,6 @@ class CreatorChannelLiveQueryService( private val queryPolicy: CreatorChannelLiveReplayQueryPolicy, private val memberContentPreferenceService: MemberContentPreferenceService, private val messageSource: SodaMessageSource, - private val langContext: LangContext, @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { @@ -40,17 +39,18 @@ class CreatorChannelLiveQueryService( sort: ContentSort, page: Int, size: Int, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelLiveTab { val livePage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! - val creator = queryPort.findCreator(creatorId, viewerId) + val creator = queryPort.findCreator(creatorId, viewerId, lang) ?: throw SodaException(messageKey = "member.validation.user_not_found") if (queryPort.existsBlockedBetween(viewerId, creatorId)) { val messageTemplate = messageSource - .getMessage("explorer.creator.blocked_access", langContext.lang) + .getMessage("explorer.creator.blocked_access", lang) .orEmpty() throw SodaException(message = String.format(messageTemplate, creator.nickname)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/port/out/CreatorChannelLiveQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/port/out/CreatorChannelLiveQueryPort.kt index 86cafd42..615d3955 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/port/out/CreatorChannelLiveQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/port/out/CreatorChannelLiveQueryPort.kt @@ -1,12 +1,13 @@ package kr.co.vividnext.sodalive.v2.creator.channel.live.port.out +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import java.time.LocalDateTime interface CreatorChannelLiveQueryPort { - fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? + fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepository.kt index ba396213..03e9cbfb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepository.kt @@ -11,11 +11,13 @@ import kr.co.vividnext.sodalive.content.order.QOrder import kr.co.vividnext.sodalive.content.series.translation.QSeriesTranslation import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.creator.channel.series.port.out.CreatorChannelSeriesCreatorRecord import kr.co.vividnext.sodalive.v2.creator.channel.series.port.out.CreatorChannelSeriesRecord +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -23,17 +25,19 @@ import java.time.LocalDateTime class DefaultCreatorChannelSeriesQueryRepository( private val queryFactory: JPAQueryFactory ) : CreatorChannelSeriesQueryRepository { - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelSeriesCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelSeriesCreatorRecord? { + val creatorName = CreatorNameJoin(member, lang, "channelSeriesCreatorName") return queryFactory .select( Projections.constructor( CreatorChannelSeriesCreatorRecord::class.java, member.id, member.role, - member.nickname + creatorName.displayName ) ) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.id.eq(creatorId), member.isActive.isTrue diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryService.kt index 8c61a6f8..38db825a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryService.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.series.application import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState +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 @@ -37,18 +38,19 @@ class CreatorChannelSeriesQueryService( sort: String?, page: Int?, size: Int?, - now: LocalDateTime = LocalDateTime.now() + now: LocalDateTime = LocalDateTime.now(), + lang: Lang ): CreatorChannelSeriesTab { val resolvedSort = queryPolicy.resolveSort(sort) val seriesPage = queryPolicy.createPage(page, size) val queryPort = queryPortProvider.getObject() val viewerId = viewer.id!! - val creator = queryPort.findCreator(creatorId, viewerId) + val creator = queryPort.findCreator(creatorId, viewerId, lang) ?: throw SodaException(messageKey = "member.validation.user_not_found") if (queryPort.existsBlockedBetween(viewerId, creatorId)) { val messageTemplate = messageSource - .getMessage("explorer.creator.blocked_access", langContext.lang) + .getMessage("explorer.creator.blocked_access", lang) .orEmpty() throw SodaException(message = String.format(messageTemplate, creator.nickname)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/out/CreatorChannelSeriesQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/out/CreatorChannelSeriesQueryPort.kt index 17afc1f1..b5584372 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/out/CreatorChannelSeriesQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/out/CreatorChannelSeriesQueryPort.kt @@ -2,12 +2,13 @@ package kr.co.vividnext.sodalive.v2.creator.channel.series.port.out import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import java.time.LocalDateTime interface CreatorChannelSeriesQueryPort { - fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelSeriesCreatorRecord? + fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelSeriesCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt index 8d56c415..961060c5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt @@ -1,8 +1,10 @@ package kr.co.vividnext.sodalive.v2.gift.application import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery @@ -112,7 +114,8 @@ class GiftQueryService( private val memberRepository: MemberRepository, private val giftMailboxRepository: GiftMailboxRepository, @Value("\${cloud.aws.cloud-front.host}") - private val cloudFrontHost: String + private val cloudFrontHost: String, + private val creatorLocalizedNameService: CreatorLocalizedNameService ) { @Transactional(readOnly = true) fun getFormOptions() = GiftFormOptionsResult( @@ -134,7 +137,7 @@ class GiftQueryService( ) @Transactional(readOnly = true) - fun getGifts(memberId: Long, type: GiftListType, page: Int, size: Int): GiftListResult { + fun getGifts(memberId: Long, type: GiftListType, page: Int, size: Int, lang: Lang): GiftListResult { val normalizedPage = page.coerceAtLeast(0) val normalizedSize = if (size <= 0) DEFAULT_PAGE_SIZE else size val pageable = PageRequest.of(normalizedPage, normalizedSize, Sort.by(Sort.Direction.DESC, "createdAt", "id")) @@ -156,13 +159,14 @@ class GiftQueryService( val membersById = memberRepository.findAllById( gifts.content.flatMap { listOf(it.senderMemberId, it.recipientMemberId) }.toSet() ).associateBy { it.id ?: throw invalidRequest() } + val names = creatorLocalizedNameService.findLocalizedNames(membersById.keys, lang) val deliveriesByGiftId = deliveryRepository.findByGiftIdIn( gifts.content.mapNotNull { it.id } ).associateBy { it.giftId } return GiftListResult( totalCount = gifts.totalElements, - items = gifts.content.map { it.toGiftListItem(memberId, membersById, deliveriesByGiftId) }, + items = gifts.content.map { it.toGiftListItem(memberId, membersById, deliveriesByGiftId, names) }, page = normalizedPage, size = normalizedSize, hasNext = gifts.hasNext() @@ -170,12 +174,13 @@ class GiftQueryService( } @Transactional(readOnly = true) - fun getGiftDetail(memberId: Long, applicationNo: String): GiftDetailResult { + fun getGiftDetail(memberId: Long, applicationNo: String, lang: Lang): GiftDetailResult { val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest() val delivery = deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest()) ?: throw invalidRequest() val direction = gift.directionFor(memberId) val sender = memberRepository.findByIdOrNull(gift.senderMemberId) ?: throw invalidRequest() val recipient = memberRepository.findByIdOrNull(gift.recipientMemberId) ?: throw invalidRequest() + val names = creatorLocalizedNameService.findLocalizedNames(listOf(gift.senderMemberId, gift.recipientMemberId), lang) val recipientAddress = if (direction == GiftDirection.RECEIVED) delivery.toRecipientAddressResult() else null return GiftDetailResult( @@ -184,8 +189,10 @@ class GiftQueryService( status = gift.status, statusName = gift.status.displayName, giftInfo = GiftDetailInfoResult( - recipientCreatorNickname = recipient.nickname.takeIf { direction == GiftDirection.SENT }, - senderNickname = sender.nickname.takeIf { direction == GiftDirection.RECEIVED }, + recipientCreatorNickname = (names[gift.recipientMemberId] ?: recipient.nickname) + .takeIf { direction == GiftDirection.SENT }, + senderNickname = (names[gift.senderMemberId] ?: sender.nickname) + .takeIf { direction == GiftDirection.RECEIVED }, sizeName = gift.sizeCode.displayName, categoryName = gift.categoryNameSnapshot, applicationNo = gift.applicationNo.takeIf { direction == GiftDirection.SENT }, @@ -212,7 +219,8 @@ class GiftQueryService( private fun Gift.toGiftListItem( memberId: Long, membersById: Map, - deliveriesByGiftId: Map + deliveriesByGiftId: Map, + names: Map ): GiftListItemResult { val direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED val counterpartId = if (direction == GiftDirection.SENT) recipientMemberId else senderMemberId @@ -229,7 +237,7 @@ class GiftQueryService( categoryName = categoryNameSnapshot, sizeName = sizeCode.displayName, counterpartMemberId = counterpartId, - counterpartNickname = counterpart.nickname, + counterpartNickname = names[counterpartId] ?: counterpart.nickname, counterpartProfileImageUrl = counterpart.profileImageUrl(), recipientAddressRegistered = recipientAddressRegistered, recipientAddressDeadlineAt = delivery.recipientAddressDeadlineAt.takeUnless { recipientAddressRegistered }, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt index 9be531ca..1e453543 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt @@ -11,6 +11,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommun import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike import kr.co.vividnext.sodalive.extensions.toUtcIso +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Gender @@ -20,6 +21,7 @@ import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.QHomeFollowingNewsInbox.homeFollowingNewsInbox import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCommunityPostNews @@ -46,12 +48,14 @@ class DefaultHomeFollowingQueryRepository( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) : HomeFollowingQueryRepository { - override fun findFollowingCreators(memberId: Long, limit: Int): List { + override fun findFollowingCreators(memberId: Long, limit: Int, lang: Lang): List { val creator = QMember("followingCreator") + val creatorName = CreatorNameJoin(creator, lang, "followingCreatorName") return queryFactory - .select(creator.id, creator.nickname, creator.profileImage) + .select(creator.id, creatorName.displayName, creator.profileImage) .from(creatorFollowing) .join(creatorFollowing.creator, creator) + .leftJoin(creatorName.name).on(creatorName.condition) .where( creatorFollowing.member.id.eq(memberId), creatorFollowing.isActive.isTrue, @@ -65,7 +69,7 @@ class DefaultHomeFollowingQueryRepository( .map { row -> HomeFollowingCreator( creatorId = row.get(creator.id)!!, - creatorNickname = row.get(creator.nickname)!!, + creatorNickname = row.get(creatorName.displayName)!!, creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage)) ) } @@ -76,13 +80,23 @@ class DefaultHomeFollowingQueryRepository( canViewAdultContent: Boolean, effectiveViewerGender: Gender, isViewerCreator: Boolean, - limit: Int + limit: Int, + lang: Lang ): List { val creator = QMember("onAirCreator") + val creatorName = CreatorNameJoin(creator, lang, "followingOnAirCreatorName") return queryFactory - .select(liveRoom.id, creator.profileImage, creator.nickname, liveRoom.title, liveRoom.beginDateTime) + .select( + liveRoom.id, + creator.profileImage, + creatorName.displayName, + liveRoom.title, + liveRoom.beginDateTime, + creator.id + ) .from(liveRoom) .join(liveRoom.member, creator) + .leftJoin(creatorName.name).on(creatorName.condition) .join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id)) .where( creatorFollowing.member.id.eq(memberId), @@ -104,9 +118,10 @@ class DefaultHomeFollowingQueryRepository( HomeFollowingLive( liveId = row.get(liveRoom.id)!!, creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage)), - creatorNickname = row.get(creator.nickname)!!, + creatorNickname = row.get(creatorName.displayName)!!, title = row.get(liveRoom.title)!!, - startedAtUtc = row.get(liveRoom.beginDateTime)!!.toUtcIso() + startedAtUtc = row.get(liveRoom.beginDateTime)!!.toUtcIso(), + creatorMemberId = row.get(creator.id)!! ) } } @@ -117,7 +132,8 @@ class DefaultHomeFollowingQueryRepository( effectiveViewerGender: Gender, isViewerCreator: Boolean, now: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List { val window = monthlyScheduleWindow(now) val liveSchedules = findLiveSchedules( @@ -125,9 +141,10 @@ class DefaultHomeFollowingQueryRepository( canViewAdultContent, effectiveViewerGender, isViewerCreator, - window + window, + lang ) - val audioSchedules = findAudioSchedules(memberId, canViewAdultContent, window) + val audioSchedules = findAudioSchedules(memberId, canViewAdultContent, window, lang) return (liveSchedules + audioSchedules) .sortedWith( compareBy { it.scheduledAtUtc } @@ -141,17 +158,20 @@ class DefaultHomeFollowingQueryRepository( memberId: Long, canViewAdultContent: Boolean, nowUtc: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List { val creator = QMember("newsCreator") + val creatorName = CreatorNameJoin(creator, lang, "followingNewsCreatorName") val newsAudioContent = QAudioContent("recentNewsAudioContent") val newsCommunity = QCreatorCommunity("recentNewsCommunity") val rows = queryFactory .select( homeFollowingNewsInbox.id, homeFollowingNewsInbox.newsType, + creator.id, creator.profileImage, - creator.nickname, + creatorName.displayName, homeFollowingNewsInbox.targetId, homeFollowingNewsInbox.visibleFromAtUtc, homeFollowingNewsInbox.rank, @@ -163,6 +183,7 @@ class DefaultHomeFollowingQueryRepository( ) .from(homeFollowingNewsInbox) .join(creator).on(creator.id.eq(homeFollowingNewsInbox.creatorId)) + .leftJoin(creatorName.name).on(creatorName.condition) .leftJoin(newsAudioContent).on( homeFollowingNewsInbox.newsType.`in`(FollowingNewsType.AUDIO_CONTENT, FollowingNewsType.CONTENT_RANKING), newsAudioContent.id.eq(homeFollowingNewsInbox.targetId) @@ -190,11 +211,14 @@ class DefaultHomeFollowingQueryRepository( .map { it.get(homeFollowingNewsInbox.targetId)!! } val likeCounts = communityLikeCounts(communityPostIds) val commentCounts = communityCommentCounts(communityPostIds) - return rows.map { row -> row.toHomeFollowingNews(creator, newsAudioContent, newsCommunity, likeCounts, commentCounts) } + return rows.map { row -> + row.toHomeFollowingNews(creator, creatorName, newsAudioContent, newsCommunity, likeCounts, commentCounts) + } } private fun Tuple.toHomeFollowingNews( newsCreator: QMember, + creatorName: CreatorNameJoin, newsAudioContent: QAudioContent, newsCommunity: QCreatorCommunity, likeCounts: Map, @@ -203,12 +227,13 @@ class DefaultHomeFollowingQueryRepository( val type = get(homeFollowingNewsInbox.newsType)!! val targetId = get(homeFollowingNewsInbox.targetId)!! val creatorProfileImageUrl = profileImageUrl(get(newsCreator.profileImage)) - val creatorNickname = get(newsCreator.nickname)!! + val creatorNickname = get(creatorName.displayName)!! val visibleFromAtUtc = get(homeFollowingNewsInbox.visibleFromAtUtc)!!.toUtcIso() val rank = get(homeFollowingNewsInbox.rank) return HomeFollowingNews( newsId = get(homeFollowingNewsInbox.id)!!.toString(), + creatorMemberId = get(newsCreator.id)!!, type = type, visibleFromAtUtc = visibleFromAtUtc, creatorRanking = if (type == FollowingNewsType.CREATOR_RANKING && rank != null) { @@ -264,21 +289,24 @@ class DefaultHomeFollowingQueryRepository( canViewAdultContent: Boolean, effectiveViewerGender: Gender, isViewerCreator: Boolean, - window: ScheduleWindow + window: ScheduleWindow, + lang: Lang ): List { val creator = QMember("scheduleLiveCreator") + val creatorName = CreatorNameJoin(creator, lang, "followingLiveScheduleCreatorName") return queryFactory .select( liveRoom.id, creator.id, creator.profileImage, - creator.nickname, + creatorName.displayName, liveRoom.title, liveRoom.beginDateTime, liveRoom.channelName ) .from(liveRoom) .join(liveRoom.member, creator) + .leftJoin(creatorName.name).on(creatorName.condition) .join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id)) .where( creatorFollowing.member.id.eq(memberId), @@ -294,26 +322,29 @@ class DefaultHomeFollowingQueryRepository( notBlockedCreatorCondition(memberId, creator.id) ) .fetch() - .map { row -> row.toLiveSchedule(creator) } + .map { row -> row.toLiveSchedule(creator, creatorName) } } private fun findAudioSchedules( memberId: Long, canViewAdultContent: Boolean, - window: ScheduleWindow + window: ScheduleWindow, + lang: Lang ): List { val creator = QMember("scheduleAudioCreator") + val creatorName = CreatorNameJoin(creator, lang, "followingAudioScheduleCreatorName") return queryFactory .select( audioContent.id, creator.id, creator.profileImage, - creator.nickname, + creatorName.displayName, audioContent.title, audioContent.releaseDate ) .from(audioContent) .join(audioContent.member, creator) + .leftJoin(creatorName.name).on(creatorName.condition) .join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id)) .where( creatorFollowing.member.id.eq(memberId), @@ -328,17 +359,17 @@ class DefaultHomeFollowingQueryRepository( notBlockedCreatorCondition(memberId, creator.id) ) .fetch() - .map { row -> row.toAudioSchedule(creator) } + .map { row -> row.toAudioSchedule(creator, creatorName) } } - private fun Tuple.toLiveSchedule(creator: QMember): HomeFollowingSchedule { + private fun Tuple.toLiveSchedule(creator: QMember, creatorName: CreatorNameJoin): HomeFollowingSchedule { val liveId = get(liveRoom.id)!! val channelName = get(liveRoom.channelName) return HomeFollowingSchedule( scheduleId = "${CreatorActivityType.LIVE}:$liveId", creatorId = get(creator.id)!!, creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)), - creatorNickname = get(creator.nickname)!!, + creatorNickname = get(creatorName.displayName)!!, title = get(liveRoom.title)!!, type = CreatorActivityType.LIVE, targetId = liveId, @@ -347,13 +378,13 @@ class DefaultHomeFollowingQueryRepository( ) } - private fun Tuple.toAudioSchedule(creator: QMember): HomeFollowingSchedule { + private fun Tuple.toAudioSchedule(creator: QMember, creatorName: CreatorNameJoin): HomeFollowingSchedule { val contentId = get(audioContent.id)!! return HomeFollowingSchedule( scheduleId = "${CreatorActivityType.AUDIO}:$contentId", creatorId = get(creator.id)!!, creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)), - creatorNickname = get(creator.nickname)!!, + creatorNickname = get(creatorName.displayName)!!, title = get(audioContent.title)!!, type = CreatorActivityType.AUDIO, targetId = contentId, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt index d896247b..8bacb91c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt @@ -1,6 +1,6 @@ package kr.co.vividnext.sodalive.v2.home.following.application -import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole @@ -19,34 +19,34 @@ import java.time.LocalDateTime class HomeFollowingQueryService( private val queryPort: HomeFollowingQueryPort, private val memberContentPreferenceService: MemberContentPreferenceService, - private val langContext: LangContext, private val creatorCommunityTranslationService: CreatorCommunityTranslationService, private val nowProvider: () -> LocalDateTime = { LocalDateTime.now(Clock.systemUTC()) } ) { - fun findHomeFollowing(member: Member): HomeFollowing { + fun findHomeFollowing(member: Member, lang: Lang): HomeFollowing { val memberId = requireNotNull(member.id) val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(member) val effectiveViewerGender = member.effectiveGender() val isViewerCreator = member.role == MemberRole.CREATOR val now = nowProvider() - val recentNews = queryPort.findRecentNews(memberId, canViewAdultContent, now, RECENT_NEWS_LIMIT) + val recentNews = queryPort.findRecentNews(memberId, canViewAdultContent, now, RECENT_NEWS_LIMIT, lang) val communityPostIds = recentNews.mapNotNull { news -> if (news.type == FollowingNewsType.COMMUNITY_POST) news.communityPost?.postId else null } val displayContents = if (communityPostIds.isEmpty()) { emptyMap() } else { - creatorCommunityTranslationService.findDisplayContents(communityPostIds, langContext.lang.code) + creatorCommunityTranslationService.findDisplayContents(communityPostIds, lang.code) } return HomeFollowing( - followingCreators = queryPort.findFollowingCreators(memberId, FOLLOWING_CREATORS_LIMIT), + followingCreators = queryPort.findFollowingCreators(memberId, FOLLOWING_CREATORS_LIMIT, lang), onAirLives = queryPort.findOnAirLives( memberId, canViewAdultContent, effectiveViewerGender, isViewerCreator, - ON_AIR_LIVES_LIMIT + ON_AIR_LIVES_LIMIT, + lang ), recentChats = emptyList(), monthlySchedules = queryPort.findMonthlySchedules( @@ -55,7 +55,8 @@ class HomeFollowingQueryService( effectiveViewerGender, isViewerCreator, now, - MONTHLY_SCHEDULES_LIMIT + MONTHLY_SCHEDULES_LIMIT, + lang ), recentNews = recentNews.map { news -> if (news.type != FollowingNewsType.COMMUNITY_POST) return@map news diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt index d9ecb27f..23a78156 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt @@ -22,7 +22,8 @@ data class HomeFollowingLive( val creatorProfileImageUrl: String, val creatorNickname: String, val title: String, - val startedAtUtc: String + val startedAtUtc: String, + val creatorMemberId: Long ) data class HomeFollowingSchedule( @@ -45,7 +46,8 @@ data class HomeFollowingNews( val audioContent: HomeFollowingContentNews? = null, val photoContent: HomeFollowingContentNews? = null, val contentRanking: HomeFollowingContentRankingNews? = null, - val communityPost: HomeFollowingCommunityPostNews? = null + val communityPost: HomeFollowingCommunityPostNews? = null, + val creatorMemberId: Long ) data class HomeFollowingCreatorRankingNews( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingQueryPort.kt index 2f866374..e091942a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingQueryPort.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.home.following.port.out +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive @@ -8,14 +9,15 @@ import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingSchedule import java.time.LocalDateTime interface HomeFollowingQueryPort { - fun findFollowingCreators(memberId: Long, limit: Int): List + fun findFollowingCreators(memberId: Long, limit: Int, lang: Lang): List fun findOnAirLives( memberId: Long, canViewAdultContent: Boolean, effectiveViewerGender: Gender, isViewerCreator: Boolean, - limit: Int + limit: Int, + lang: Lang ): List fun findMonthlySchedules( @@ -24,13 +26,15 @@ interface HomeFollowingQueryPort { effectiveViewerGender: Gender, isViewerCreator: Boolean, now: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List fun findRecentNews( memberId: Long, canViewAdultContent: Boolean, nowUtc: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt index 30c21fea..d81ee73c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt @@ -1,5 +1,11 @@ package kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence +import com.querydsl.jpa.JPAExpressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.i18n.Lang +import kr.co.vividnext.sodalive.member.QMember.member +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin +import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.QCreatorRankingSnapshot.creatorRankingSnapshot import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord @@ -9,7 +15,8 @@ import java.time.LocalDateTime @Repository class DefaultCreatorRankingSnapshotRepository( - private val repository: CreatorRankingSnapshotRepository + private val repository: CreatorRankingSnapshotRepository, + private val queryFactory: JPAQueryFactory ) : CreatorRankingSnapshotPort { override fun findSnapshotsByAggregationPeriod( aggregationStartAtUtc: LocalDateTime, @@ -31,9 +38,28 @@ class DefaultCreatorRankingSnapshotRepository( override fun findLatestVisibleSnapshots( rankingType: CreatorRankingType, - nowUtc: LocalDateTime + nowUtc: LocalDateTime, + lang: Lang ): List { - return repository.findLatestVisibleSnapshots(rankingType.name, nowUtc).map { it.toRecord() } + val snapshot = creatorRankingSnapshot + val latest = QCreatorRankingSnapshot("latestVisibleCreatorRanking") + val creatorName = CreatorNameJoin(member, lang, "rankingCreatorName") + val displayName = creatorName.displayName.coalesce(snapshot.nickname) + return queryFactory + .select(snapshot, displayName) + .from(snapshot) + .leftJoin(member).on(member.id.eq(snapshot.creatorId)) + .leftJoin(creatorName.name).on(creatorName.condition) + .where( + snapshot.rankingType.eq(rankingType), + snapshot.visibleFromAtUtc.eq( + JPAExpressions.select(latest.visibleFromAtUtc.max()).from(latest) + .where(latest.rankingType.eq(rankingType), latest.visibleFromAtUtc.loe(nowUtc)) + ) + ) + .orderBy(snapshot.rankNo.asc()) + .fetch() + .map { it.get(snapshot)!!.toRecord(it.get(displayName)!!) } } override fun findPreviousVisibleSnapshots( @@ -64,14 +90,14 @@ class DefaultCreatorRankingSnapshotRepository( repository.saveAll(newSnapshots.map { it.toEntity(rankingType, visibleFromAtUtc) }) } - private fun CreatorRankingSnapshot.toRecord(): CreatorRankingSnapshotRecord { + private fun CreatorRankingSnapshot.toRecord(displayNickname: String = nickname): CreatorRankingSnapshotRecord { return CreatorRankingSnapshotRecord( rankingType = rankingType, aggregationStartAtUtc = aggregationStartAtUtc, aggregationEndAtUtc = aggregationEndAtUtc, visibleFromAtUtc = visibleFromAtUtc, creatorId = creatorId, - nickname = nickname, + nickname = displayNickname, profileImageUrl = profileImageUrl, rankNo = rankNo, finalScore = finalScore, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt index 986df8a8..b6be469c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.ranking.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType @@ -24,11 +25,11 @@ class CreatorRankingQueryService( ) { private val log = LoggerFactory.getLogger(javaClass) - fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult { + fun getCreatorRankings(viewerMemberId: Long?, lang: Lang): CreatorRankingResult { val startedAt = System.currentTimeMillis() return runCatching { val nowUtc = nowUtc() - val latestSnapshots = findLatestVisibleSnapshots(nowUtc) + val latestSnapshots = findLatestVisibleSnapshots(nowUtc, lang) val latestItems = latestSnapshots.toRankedItems() if (latestItems.isEmpty()) { return@runCatching QueryLogResult(CreatorRankingResult(showRankChange = false, items = emptyList()), 0) @@ -69,15 +70,15 @@ class CreatorRankingQueryService( }.getOrThrow().result } - private fun findLatestVisibleSnapshots(nowUtc: LocalDateTime): List { - val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc) + private fun findLatestVisibleSnapshots(nowUtc: LocalDateTime, lang: Lang): List { + val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc, lang) if (latestSnapshots.isNotEmpty()) return latestSnapshots runCatching { snapshotJobService.refreshLastCompletedWeekByFallback() } .onFailure { ex -> log.warn("event=creator_ranking_query_fallback_failure error={}", ex.message, ex) } - return snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc) + return snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc, lang) } private data class QueryLogResult( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt index 918cc2a1..9d56ade1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.ranking.port.out +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType import java.time.LocalDateTime @@ -15,7 +16,8 @@ interface CreatorRankingSnapshotPort { fun findLatestVisibleSnapshots( rankingType: CreatorRankingType, - nowUtc: LocalDateTime + nowUtc: LocalDateTime, + lang: Lang ): List fun findPreviousVisibleSnapshots( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt index 00d64eb1..0e49f283 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt @@ -32,6 +32,7 @@ import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.member.following.QCreatorFollowing import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType +import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScoreSpec import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord @@ -62,22 +63,26 @@ class DefaultHomeRecommendationQueryRepository( memberId: Long?, includeAdultLives: Boolean, isViewerCreator: Boolean, - effectiveViewerGender: Gender? + effectiveViewerGender: Gender?, + lang: Lang ): List { + val creatorName = CreatorNameJoin(member, lang, "homeLiveCreatorName") return queryFactory .select( Projections.constructor( HomeLiveRecommendationRecord::class.java, liveRoom.id, - member.nickname, + creatorName.displayName, member.profileImage, liveRoom.title, liveRoom.price, - liveRoom.beginDateTime + liveRoom.beginDateTime, + member.id ) ) .from(liveRoom) .join(liveRoom.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( liveRoom.isActive.isTrue, liveRoom.channelName.isNotNull, @@ -138,11 +143,12 @@ class DefaultHomeRecommendationQueryRepository( override fun findRecentlyActiveCreators( limit: Int, memberId: Long?, - includeAdultActivities: Boolean + includeAdultActivities: Boolean, + lang: Lang ): List { val sql = """ select ranked.creator_id, - ranked.creator_nickname, + coalesce(creator_name.display_name, ranked.creator_nickname), ranked.creator_profile_image, ranked.activity_type, ranked.activity_at, @@ -200,6 +206,9 @@ class DefaultHomeRecommendationQueryRepository( and m.role = 'CREATOR' ) activities ) ranked + join member display_owner on display_owner.id = ranked.creator_id + left join creator_localized_name creator_name on creator_name.member_id = display_owner.id + and creator_name.lang = :lang and creator_name.is_active = true and display_owner.role = 'CREATOR' where ranked.creator_rank = 1 and ${notBlockedCreatorSql("ranked.creator_id")} order by ranked.activity_at desc, ranked.target_sort_id desc @@ -210,6 +219,7 @@ class DefaultHomeRecommendationQueryRepository( .setParameter("liveReplayTheme", LIVE_REPLAY_THEME) .setParameter("memberId", memberId) .setParameter("includeAdultActivities", includeAdultActivities) + .setParameter("lang", lang.name) .setParameter("limit", limit) @Suppress("UNCHECKED_CAST") @@ -232,7 +242,8 @@ class DefaultHomeRecommendationQueryRepository( offset: Long, limit: Int, memberId: Long?, - includeAdultContents: Boolean + includeAdultContents: Boolean, + lang: Lang ): List { val sql = """ with creator_debut as ( @@ -328,9 +339,11 @@ class DefaultHomeRecommendationQueryRepository( group by communication.creator_id ) select m.id as creator_id, - m.nickname as creator_nickname, + coalesce(creator_name.display_name, m.nickname) as creator_nickname, m.profile_image as creator_profile_image from member m + left join creator_localized_name creator_name on creator_name.member_id = m.id + and creator_name.lang = :lang and creator_name.is_active = true and m.role = 'CREATOR' join creator_debut cd on cd.creator_id = m.id left join follow_stats fs on fs.creator_id = m.id left join content_stats cs on cs.creator_id = m.id @@ -354,6 +367,7 @@ class DefaultHomeRecommendationQueryRepository( val query = entityManager.createNativeQuery(sql) .setRecommendationQueryParameters(now, limit) + .setParameter("lang", lang.name) .setParameter("offset", offset) .setParameter("memberId", memberId) .setParameter("includeAdultContents", includeAdultContents) @@ -437,7 +451,7 @@ class DefaultHomeRecommendationQueryRepository( ) select ec.content_id as content_id, m.id as creator_id, - m.nickname as creator_nickname, + coalesce(creator_name.display_name, m.nickname) as creator_nickname, m.profile_image as creator_profile_image, ec.title as title, ec.price as price, @@ -447,6 +461,8 @@ class DefaultHomeRecommendationQueryRepository( ec.is_original_series as is_original_series from eligible_contents ec join member m on m.id = ec.creator_id + left join creator_localized_name creator_name on creator_name.member_id = m.id + and creator_name.lang = :lang and creator_name.is_active = true and m.role = 'CREATOR' join creator_debut cd on cd.creator_id = ec.creator_id where ec.active_rank = 1 and m.is_active = true @@ -468,6 +484,7 @@ class DefaultHomeRecommendationQueryRepository( val query = entityManager.createNativeQuery(sql) .setParameter("now", now) + .setParameter("lang", Lang.fromCode(locale).name) .setParameter("limit", limit) .setParameter("offset", offset) .setParameter("memberId", memberId) @@ -832,20 +849,23 @@ class DefaultHomeRecommendationQueryRepository( override fun findCheerCreatorRecommendationDetails( creatorIds: List, - memberId: Long? + memberId: Long?, + lang: Lang ): List { if (creatorIds.isEmpty()) return emptyList() + val creatorName = CreatorNameJoin(member, lang, "homeCheerCreatorName") return queryFactory .select( Projections.constructor( HomeCheerCreatorRecommendationRecord::class.java, member.id, - member.nickname, + creatorName.displayName, member.profileImage ) ) .from(member) + .leftJoin(creatorName.name).on(creatorName.condition) .where( member.isActive.isTrue, member.id.`in`(creatorIds), @@ -859,9 +879,11 @@ class DefaultHomeRecommendationQueryRepository( override fun findPopularCommunityRecommendationDetails( communityIds: List, memberId: Long?, - includeAdultCommunities: Boolean + includeAdultCommunities: Boolean, + lang: Lang ): List { if (communityIds.isEmpty()) return emptyList() + val creatorName = CreatorNameJoin(member, lang, "homeCommunityCreatorName") return queryFactory .select( @@ -869,7 +891,7 @@ class DefaultHomeRecommendationQueryRepository( HomePopularCommunityRecommendationRecord::class.java, creatorCommunity.id, member.id, - member.nickname, + creatorName.displayName, member.profileImage, creatorCommunity.imagePath, creatorCommunity.audioPath, @@ -884,6 +906,7 @@ class DefaultHomeRecommendationQueryRepository( ) .from(creatorCommunity) .join(creatorCommunity.member, member) + .leftJoin(creatorName.name).on(creatorName.condition) .leftJoin(creatorCommunityLike).on( creatorCommunityLike.creatorCommunity.id.eq(creatorCommunity.id), creatorCommunityLike.isActive.isTrue @@ -905,6 +928,7 @@ class DefaultHomeRecommendationQueryRepository( creatorCommunity.id, member.id, member.nickname, + creatorName.name.displayName, member.profileImage, creatorCommunity.imagePath, creatorCommunity.audioPath, @@ -919,7 +943,8 @@ class DefaultHomeRecommendationQueryRepository( memberId: Long?, includeAdultGenres: Boolean, genreLimit: Int, - creatorLimit: Int + creatorLimit: Int, + lang: Lang ): List { val groups = mutableListOf() val selectedGenreIds = mutableSetOf() @@ -931,7 +956,7 @@ class DefaultHomeRecommendationQueryRepository( ) viewedTargets.forEach { target -> - groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit)) + groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit, lang)) selectedGenreIds.add(target.id) } @@ -941,7 +966,8 @@ class DefaultHomeRecommendationQueryRepository( memberId = memberId, includeAdultGenres = includeAdultGenres, genreLimit = genreLimit, - creatorLimit = creatorLimit + creatorLimit = creatorLimit, + lang = lang ) return groups @@ -993,7 +1019,8 @@ class DefaultHomeRecommendationQueryRepository( memberId: Long?, includeAdultGenres: Boolean, genreLimit: Int, - creatorLimit: Int + creatorLimit: Int, + lang: Lang ) { val fullTargets = findFallbackGenreRecommendationTargets( memberId = memberId, @@ -1004,7 +1031,7 @@ class DefaultHomeRecommendationQueryRepository( ) fullTargets.forEach { target -> - groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit)) + groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit, lang)) selectedGenreIds.add(target.id) } val partialTargets = findFallbackGenreRecommendationTargets( @@ -1016,7 +1043,7 @@ class DefaultHomeRecommendationQueryRepository( ) partialTargets.forEach { target -> - groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit)) + groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit, lang)) selectedGenreIds.add(target.id) } } @@ -1099,7 +1126,8 @@ class DefaultHomeRecommendationQueryRepository( target: GenreRecommendationTarget, memberId: Long?, includeAdultGenres: Boolean, - creatorLimit: Int + creatorLimit: Int, + lang: Lang ): HomeGenreCreatorRecommendationGroup { return HomeGenreCreatorRecommendationGroup( genreId = target.id, @@ -1109,7 +1137,8 @@ class DefaultHomeRecommendationQueryRepository( genreId = target.id, memberId = memberId, includeAdultGenres = includeAdultGenres, - creatorLimit = creatorLimit + creatorLimit = creatorLimit, + lang = lang ) ) } @@ -1152,11 +1181,12 @@ class DefaultHomeRecommendationQueryRepository( genreId: Long, memberId: Long?, includeAdultGenres: Boolean, - creatorLimit: Int + creatorLimit: Int, + lang: Lang ): List { val sql = """ select candidates.creator_id, - candidates.creator_nickname, + coalesce(creator_name.display_name, candidates.creator_nickname), candidates.creator_profile_image from ( select m.id as creator_id, @@ -1190,12 +1220,16 @@ class DefaultHomeRecommendationQueryRepository( ) group by m.id, m.nickname, m.profile_image ) candidates + join member display_owner on display_owner.id = candidates.creator_id + left join creator_localized_name creator_name on creator_name.member_id = display_owner.id + and creator_name.lang = :lang and creator_name.is_active = true and display_owner.role = 'CREATOR' order by rand() asc limit :creatorLimit """.trimIndent() val query = entityManager.createNativeQuery(sql) .setParameter("genreId", genreId) + .setParameter("lang", lang.name) .setParameter("memberId", memberId) .setParameter("includeAdultGenres", includeAdultGenres) .setParameter("creatorLimit", creatorLimit) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt index 3e83f618..ed2837be 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.v2.recommendation.application import kr.co.vividnext.sodalive.i18n.Lang -import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService @@ -33,7 +32,6 @@ class HomeRecommendationQueryService( private val cheerCreatorSnapshotFallbackService: CheerCreatorSnapshotFallbackPort? = null, private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null, private val clock: Clock = Clock.systemUTC(), - private val langContext: LangContext, private val creatorCommunityTranslationService: CreatorCommunityTranslationService ) { private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy() @@ -44,7 +42,8 @@ class HomeRecommendationQueryService( memberId: Long? = null, includeAdultLives: Boolean = false, isViewerCreator: Boolean = false, - effectiveViewerGender: Gender? = null + effectiveViewerGender: Gender? = null, + lang: Lang ): List { return queryPort.findLiveRecommendations( offset, @@ -52,7 +51,8 @@ class HomeRecommendationQueryService( memberId, includeAdultLives, isViewerCreator, - effectiveViewerGender + effectiveViewerGender, + lang ) } @@ -68,9 +68,10 @@ class HomeRecommendationQueryService( fun findRecentlyActiveCreators( limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT, memberId: Long? = null, - includeAdultActivities: Boolean = false + includeAdultActivities: Boolean = false, + lang: Lang ): List { - return queryPort.findRecentlyActiveCreators(limit, memberId, includeAdultActivities) + return queryPort.findRecentlyActiveCreators(limit, memberId, includeAdultActivities, lang) } fun findRecentDebutCreators( @@ -78,9 +79,10 @@ class HomeRecommendationQueryService( offset: Long = 0, limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT, memberId: Long? = null, - includeAdultContents: Boolean = false + includeAdultContents: Boolean = false, + lang: Lang ): List { - return queryPort.findRecentDebutCreators(now, offset, limit, memberId, includeAdultContents) + return queryPort.findRecentDebutCreators(now, offset, limit, memberId, includeAdultContents, lang) } fun findFirstAudioContents( @@ -88,9 +90,10 @@ class HomeRecommendationQueryService( offset: Long = 0, limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT, memberId: Long? = null, - includeAdultContents: Boolean = false + includeAdultContents: Boolean = false, + lang: Lang ): List { - return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents, langContext.lang.code) + return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents, lang.code) } fun findAiCharacterRecommendations( @@ -128,10 +131,11 @@ class HomeRecommendationQueryService( fun findCheerCreatorRecommendations( limit: Int = DEFAULT_CHEER_CREATOR_LIMIT, - memberId: Long? = null + memberId: Long? = null, + lang: Lang ): List { val snapshots = findCheerCreatorSnapshotsWithFallback() - val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId }, memberId) + val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId }, memberId, lang) .associateBy { it.creatorId } return snapshots.mapNotNull { detailsById[it.targetId] }.take(limit) @@ -155,13 +159,15 @@ class HomeRecommendationQueryService( fun findPopularCommunityRecommendations( limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT, memberId: Long? = null, - includeAdultCommunities: Boolean = false + includeAdultCommunities: Boolean = false, + lang: Lang ): List { val snapshots = findPopularCommunitySnapshotsWithFallback() val detailsById = queryPort.findPopularCommunityRecommendationDetails( snapshots.map { it.targetId }, memberId, - includeAdultCommunities + includeAdultCommunities, + lang ) .associateBy { it.communityId } val selectedCreatorIds = mutableSetOf() @@ -173,7 +179,7 @@ class HomeRecommendationQueryService( val displayContents = creatorCommunityTranslationService.findDisplayContents( recommendations.map { it.communityId }, - langContext.lang.code + lang.code ) return recommendations.map { recommendation -> displayContents[recommendation.communityId] @@ -201,13 +207,14 @@ class HomeRecommendationQueryService( memberId: Long?, includeAdultGenres: Boolean, genreLimit: Int = DEFAULT_GENRE_CREATOR_GENRE_LIMIT, - creatorLimit: Int = DEFAULT_GENRE_CREATOR_CREATOR_LIMIT + creatorLimit: Int = DEFAULT_GENRE_CREATOR_CREATOR_LIMIT, + lang: Lang ): List { val selectedCreatorIds = mutableSetOf() val partialFallbackGroups = mutableListOf() val selectedGroups = mutableListOf() - queryPort.findGenreCreatorRecommendations(memberId, includeAdultGenres, genreLimit, creatorLimit) + queryPort.findGenreCreatorRecommendations(memberId, includeAdultGenres, genreLimit, creatorLimit, lang) .forEach { group -> if (selectedGroups.size >= genreLimit) return@forEach diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt index a27605bc..126a7289 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt @@ -12,7 +12,8 @@ interface HomeRecommendationQueryPort { memberId: Long? = null, includeAdultLives: Boolean = false, isViewerCreator: Boolean = false, - effectiveViewerGender: Gender? = null + effectiveViewerGender: Gender? = null, + lang: Lang ): List fun findHomeBanners( @@ -25,7 +26,8 @@ interface HomeRecommendationQueryPort { fun findRecentlyActiveCreators( limit: Int, memberId: Long? = null, - includeAdultActivities: Boolean = false + includeAdultActivities: Boolean = false, + lang: Lang ): List fun findRecentDebutCreators( @@ -33,7 +35,8 @@ interface HomeRecommendationQueryPort { offset: Long = 0, limit: Int, memberId: Long? = null, - includeAdultContents: Boolean = false + includeAdultContents: Boolean = false, + lang: Lang ): List fun findFirstAudioContents( @@ -72,20 +75,23 @@ interface HomeRecommendationQueryPort { fun findCheerCreatorRecommendationDetails( creatorIds: List, - memberId: Long? = null + memberId: Long? = null, + lang: Lang ): List fun findPopularCommunityRecommendationDetails( communityIds: List, memberId: Long? = null, - includeAdultCommunities: Boolean + includeAdultCommunities: Boolean, + lang: Lang ): List fun findGenreCreatorRecommendations( memberId: Long?, includeAdultGenres: Boolean, genreLimit: Int, - creatorLimit: Int + creatorLimit: Int, + lang: Lang ): List } @@ -95,7 +101,8 @@ data class HomeLiveRecommendationRecord( val creatorProfileImage: String?, val title: String, val price: Int, - val beginDateTime: LocalDateTime + val beginDateTime: LocalDateTime, + val creatorMemberId: Long ) data class HomeBannerRecommendationRecord( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/dto/UserCreatorChatMessageRow.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/dto/UserCreatorChatMessageRow.kt new file mode 100644 index 00000000..0bed6fe7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/dto/UserCreatorChatMessageRow.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.v2.usercreatorchat.dto + +import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType +import java.time.LocalDateTime + +data class UserCreatorChatMessageRow( + val messageId: Long, + val messageType: UserCreatorChatMessageType, + val createdAt: LocalDateTime?, + val textMessage: String?, + val voiceMessage: String?, + val senderId: Long, + val senderNickname: String, + val senderProfileImage: String? +) + +data class UserCreatorChatOpponentProfile(val nickname: String, val profileImage: String?) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatMessageRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatMessageRepository.kt index bb6ad062..b80744a8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatMessageRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatMessageRepository.kt @@ -1,23 +1,43 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.repository +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessage import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom +import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageRow import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository @Repository interface UserCreatorChatMessageRepository : JpaRepository { - fun findByChatRoomAndIsActiveTrueOrderByIdDesc( - chatRoom: UserCreatorChatRoom, + @Query( + """ + select new kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageRow( + m.id, m.messageType, m.createdAt, m.textMessage, m.voiceMessage, + sender.id, coalesce(senderName.displayName, sender.nickname), sender.profileImage + ) + from UserCreatorChatMessage m + join m.participant participant + join participant.member sender + left join CreatorLocalizedNameEntity senderName + on senderName.memberId = sender.id + and senderName.lang = :lang + and senderName.isActive = true + and sender.role = kr.co.vividnext.sodalive.member.MemberRole.CREATOR + where m.chatRoom = :chatRoom + and m.isActive = true + and (:cursor is null or m.id < :cursor) + order by m.id desc + """ + ) + fun findMessageHistory( + @Param("chatRoom") chatRoom: UserCreatorChatRoom, + @Param("cursor") cursor: Long?, + @Param("lang") lang: Lang, pageable: Pageable - ): List - - fun findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc( - chatRoom: UserCreatorChatRoom, - id: Long, - pageable: Pageable - ): List + ): List fun existsByChatRoomAndIsActiveTrueAndIdLessThan(chatRoom: UserCreatorChatRoom, id: Long): Boolean } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatParticipantRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatParticipantRepository.kt index 329faeec..884fdd8f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatParticipantRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatParticipantRepository.kt @@ -1,6 +1,8 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.repository +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatParticipant +import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatOpponentProfile import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param @@ -33,4 +35,27 @@ interface UserCreatorChatParticipantRepository : JpaRepository :memberId + """ + ) + fun findActiveOpponentProfile( + @Param("roomId") roomId: Long, + @Param("memberId") memberId: Long, + @Param("lang") lang: Lang + ): UserCreatorChatOpponentProfile? } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt index 390db13a..3a077b70 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.repository +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom import org.springframework.data.domain.Pageable @@ -41,7 +42,7 @@ interface UserCreatorChatRoomRepository : JpaRepository } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt index 86e2be85..34962c63 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt @@ -8,11 +8,14 @@ import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory +import kr.co.vividnext.sodalive.i18n.Lang +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberKind import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessage import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatParticipant @@ -21,6 +24,7 @@ import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.CreateUserCreatorChatRoom import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.SendUserCreatorChatMessageResponse import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.SendUserCreatorVoiceMessageRequest import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto +import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageRow import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessagesPageResponse import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatRoomOpenResponse import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository @@ -52,6 +56,8 @@ class UserCreatorChatService( private val applicationEventPublisher: ApplicationEventPublisher, private val objectMapper: ObjectMapper, private val s3Uploader: S3Uploader, + private val localizedNameService: CreatorLocalizedNameService, + private val langContext: LangContext, @Value("\${cloud.aws.s3.bucket}") private val bucket: String, @@ -80,7 +86,7 @@ class UserCreatorChatService( fun openRoom(member: Member, roomId: Long, limit: Int = 20): UserCreatorChatRoomOpenResponse { val room = findRoom(roomId) requireParticipant(roomId, member.id!!) - val opponent = participantRepository.findActiveOpponent(roomId, member.id!!)?.member + val opponent = participantRepository.findActiveOpponentProfile(roomId, member.id!!, langContext.lang) ?: throw SodaException(messageKey = "chat.room.invalid_access") val opponentProfilePath = opponent.profileImage ?: "profile/default-profile.png" val page = getMessages(member, roomId, cursor = null, limit = limit) @@ -98,12 +104,8 @@ class UserCreatorChatService( val room = findRoom(roomId) requireParticipant(roomId, member.id!!) val pageable = PageRequest.of(0, limit.coerceIn(1, 100)) - val fetched = if (cursor != null) { - messageRepository.findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc(room, cursor, pageable) - } else { - messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(room, pageable) - } - val nextCursor = fetched.minByOrNull { it.id ?: Long.MAX_VALUE }?.id + val fetched = messageRepository.findMessageHistory(room, cursor, langContext.lang, pageable) + val nextCursor = fetched.minOfOrNull { it.messageId } val hasMore = nextCursor?.let { messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, it) } ?: false return UserCreatorChatMessagesPageResponse( messages = fetched.sortedBy { it.createdAt }.map { toMessageItemDto(it, member) }, @@ -113,19 +115,25 @@ class UserCreatorChatService( } @Transactional - fun sendTextMessageByWebSocket(memberId: Long, roomId: Long, textMessage: String): UserCreatorChatMessageItemDto { + fun sendTextMessageByWebSocket( + memberId: Long, + roomId: Long, + textMessage: String, + lang: Lang + ): UserCreatorChatMessageItemDto { if (textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request") val senderParticipant = validateParticipant(roomId, memberId) val sender = senderParticipant.member val context = resolveSendContext(sender, roomId) val message = saveTextMessage(context, textMessage) val senderMessage = toMessageItemDto(message, sender) + val names = localizedNameService.findLocalizedNames(listOf(sender.id!!), lang) val opponent = context.opponentParticipant.member val deliveredRealtime = deliverRealtime(message, opponent) if (!deliveredRealtime) { publishMessagePush(message, sender, opponent) } - return senderMessage + return senderMessage.copy(senderNickname = names[sender.id] ?: senderMessage.senderNickname) } @Transactional @@ -185,7 +193,9 @@ class UserCreatorChatService( opponentParticipant: UserCreatorChatParticipant ): SendUserCreatorChatMessageResponse { val opponent = opponentParticipant.member - val item = toMessageItemDto(message, member) + val neutralItem = toMessageItemDto(message, member) + val names = localizedNameService.findLocalizedNames(listOf(neutralItem.senderId), langContext.lang) + val item = neutralItem.copy(senderNickname = names[neutralItem.senderId] ?: neutralItem.senderNickname) val deliveredRealtime = deliverRealtime(message, opponent) if (deliveredRealtime) { return SendUserCreatorChatMessageResponse(message = item, deliveredRealtime = true, pushSent = false) @@ -239,6 +249,7 @@ class UserCreatorChatService( messageKey = messageKey, senderMemberId = sender.id, args = listOf(sender.nickname), + nameArgumentMemberIds = mapOf(0 to sender.id!!), recipients = listOf(opponent.id!!), deepLinkValue = FcmDeepLinkValue.CHAT, deepLinkId = message.chatRoom.id @@ -298,6 +309,21 @@ class UserCreatorChatService( ) } + private fun toMessageItemDto(message: UserCreatorChatMessageRow, member: Member): UserCreatorChatMessageItemDto { + val profilePath = message.senderProfileImage ?: "profile/default-profile.png" + return UserCreatorChatMessageItemDto( + messageId = message.messageId, + messageType = message.messageType.name, + mine = message.senderId == member.id, + createdAt = message.createdAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli() ?: 0L, + textMessage = message.textMessage, + voiceMessageUrl = message.voiceMessage?.let { "$cloudFrontHost/$it" }, + senderId = message.senderId, + senderNickname = message.senderNickname, + senderProfileImageUrl = "$cloudFrontHost/$profilePath" + ) + } + private data class SendContext( val room: UserCreatorChatRoom, val senderParticipant: UserCreatorChatParticipant, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt index a0b8a744..565b3762 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt @@ -1,6 +1,9 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import kr.co.vividnext.sodalive.i18n.Lang +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import org.springframework.data.redis.connection.Message import org.springframework.data.redis.connection.MessageListener import org.springframework.data.redis.core.StringRedisTemplate @@ -16,7 +19,8 @@ class UserCreatorChatRoomMessageBroker( private val stringRedisTemplate: StringRedisTemplate, private val sessionRegistry: UserCreatorChatWebSocketSessionRegistry, private val objectMapper: ObjectMapper, - listenerContainer: RedisMessageListenerContainer + listenerContainer: RedisMessageListenerContainer, + private val localizedNameService: CreatorLocalizedNameService ) : MessageListener { init { listenerContainer.addMessageListener(this, ChannelTopic(ROOM_CHANNEL)) @@ -36,9 +40,25 @@ class UserCreatorChatRoomMessageBroker( String(message.body, StandardCharsets.UTF_8), UserCreatorChatRoomPublishedMessage::class.java ) - sessionRegistry.findSessions(published.roomId, published.memberId) + val envelope = objectMapper.readTree(published.payload) + val senderId = envelope.path("payload").path("senderId").takeIf { it.isIntegralNumber }?.asLong() + val sessions = sessionRegistry.findSessions(published.roomId, published.memberId) .filter { session -> session.isOpen } - .forEach { session -> sendMessage(session, published.payload) } + sessions.groupBy { it.attributes[UserCreatorChatWebSocketAuthInterceptor.LANG_ATTRIBUTE] as? Lang ?: Lang.KO } + .forEach { (lang, languageSessions) -> + val names = localizedNameService.findLocalizedNames(listOfNotNull(senderId), lang) + val nickname = senderId?.let { names[it] } + languageSessions.forEach { session -> + val payload = if (nickname != null) { + val localized = envelope.deepCopy() + (localized["payload"] as ObjectNode).put("senderNickname", nickname) + objectMapper.writeValueAsString(localized) + } else { + published.payload + } + sendMessage(session, payload) + } + } } private fun sendMessage(session: WebSocketSession, payload: String) { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt index 8a9e479c..99c02892 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt @@ -1,8 +1,10 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.jwt.JwtFilter import kr.co.vividnext.sodalive.jwt.TokenProvider import kr.co.vividnext.sodalive.member.MemberAdapter +import org.springframework.http.HttpHeaders import org.springframework.http.server.ServerHttpRequest import org.springframework.http.server.ServerHttpResponse import org.springframework.stereotype.Component @@ -35,6 +37,7 @@ class UserCreatorChatWebSocketAuthInterceptor( attributes[MEMBER_ID_ATTRIBUTE] = memberId attributes[AUTHENTICATION_ATTRIBUTE] = authentication + attributes[LANG_ATTRIBUTE] = Lang.fromAcceptLanguage(request.headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE)) return true } @@ -58,6 +61,7 @@ class UserCreatorChatWebSocketAuthInterceptor( companion object { const val MEMBER_ID_ATTRIBUTE = "memberId" const val AUTHENTICATION_ATTRIBUTE = "authentication" + const val LANG_ATTRIBUTE = "lang" private const val BEARER_PREFIX = "Bearer " } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt index 4ee1edb0..2b3beec2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService import org.springframework.stereotype.Component import org.springframework.web.socket.CloseStatus @@ -60,7 +61,8 @@ class UserCreatorChatWebSocketHandler( val message = service.sendTextMessageByWebSocket( memberId = memberId(session), roomId = request.roomId, - textMessage = textMessage + textMessage = textMessage, + lang = session.attributes[UserCreatorChatWebSocketAuthInterceptor.LANG_ATTRIBUTE] as? Lang ?: Lang.KO ) sendEnvelope(session, UserCreatorChatWebSocketMessageType.SEND_ACK, request.requestId, request.roomId, message) } catch (e: SodaException) { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllControllerTest.kt index c65901dc..65d65777 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllControllerTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.all.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext import kr.co.vividnext.sodalive.configs.SecurityConfig +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.jwt.JwtAccessDeniedHandler @@ -14,6 +15,7 @@ import kr.co.vividnext.sodalive.v2.api.content.all.application.MainContentAllFac import kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponse import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -41,6 +43,11 @@ class MainContentAllControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -57,7 +64,7 @@ class MainContentAllControllerTest @Autowired constructor( @DisplayName("전체 탭 조회는 비회원에게 200 OK와 기본 응답을 반환한다") fun shouldAllowAnonymousAndUseDefaultType() { Mockito.doReturn(response(MainContentAllType.AUDIO, ContentSort.LATEST)).`when`(facade) - .getContents(null, null, null, null, null, null) + .getContents(null, null, null, null, null, null, Lang.KO) mockMvc.perform(get("/api/v2/audio/contents")) .andExpect(status().isOk) @@ -76,7 +83,7 @@ class MainContentAllControllerTest @Autowired constructor( role = MemberRole.USER ).apply { id = 10L } Mockito.doReturn(response(MainContentAllType.SERIES, ContentSort.POPULAR)).`when`(facade) - .getContents("SERIES", "POPULAR", "MON", 1, 30, member) + .getContents("SERIES", "POPULAR", "MON", 1, 30, member, Lang.KO) mockMvc.perform( get("/api/v2/audio/contents") @@ -92,7 +99,7 @@ class MainContentAllControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.type").value("SERIES")) .andExpect(jsonPath("$.data.sort").value("POPULAR")) - Mockito.verify(facade).getContents("SERIES", "POPULAR", "MON", 1, 30, member) + Mockito.verify(facade).getContents("SERIES", "POPULAR", "MON", 1, 30, member, Lang.KO) } private fun response(type: MainContentAllType, sort: ContentSort): MainContentAllTabResponse { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt index be6caf15..f7a03958 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt @@ -10,12 +10,16 @@ import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDays import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.nullValue +import org.hibernate.SessionFactory +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.jdbc.core.JdbcTemplate import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.MockMvc @@ -30,6 +34,7 @@ import javax.persistence.EntityManager properties = [ "cloud.aws.cloud-front.host=https://cdn.test", "spring.cache.type=none", + "spring.jpa.properties.hibernate.generate_statistics=true", "spring.datasource.url=jdbc:h2:mem:main-content-all-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE" ] ) @@ -39,8 +44,107 @@ import javax.persistence.EntityManager class MainContentAllEndToEndTest @Autowired constructor( private val mockMvc: MockMvc, private val entityManager: EntityManager, - private val transactionTemplate: TransactionTemplate + private val transactionTemplate: TransactionTemplate, + private val jdbcTemplate: JdbcTemplate ) { + @Test + @DisplayName("전체 콘텐츠 API는 JA 별칭을 우선하고 KO 별칭 미등록 시 일본어 원본을, 별칭 없는 작성자는 현재 원본을 반환한다") + fun shouldSelectLocalizedCreatorNamesWithoutChangingAudioTitles() { + transactionTemplate.executeWithoutResult { saveMember("main-all-resource-id-control", MemberRole.USER) } + val localized = createAudioFixture("main-all-localized") + val fallback = createAudioFixture("main-all-fallback") + transactionTemplate.executeWithoutResult { + 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) + """.trimIndent(), + localized.creatorId, + "日本語クリエイター" + ) + entityManager.find(Member::class.java, localized.creatorId).nickname = "改名さくら" + } + + mockMvc.perform(get("/api/v2/audio/contents").param("type", "AUDIO").header("Accept-Language", "ko")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.totalCount").value(2)) + .andExpect( + jsonPath("$.data.audios[?(@.audioContentId == ${localized.audioContentId})].creatorNickname") + .value(contains("改名さくら")) + ) + + val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics + statistics.clear() + mockMvc.perform(get("/api/v2/audio/contents").param("type", "AUDIO").header("Accept-Language", "ja")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.totalCount").value(2)) + .andExpect( + jsonPath("$.data.audios[?(@.audioContentId == ${localized.audioContentId})].creatorNickname") + .value(contains("日本語クリエイター")) + ) + .andExpect( + jsonPath("$.data.audios[?(@.audioContentId == ${fallback.audioContentId})].creatorNickname") + .value(contains("main-all-fallback-creator")) + ) + .andExpect( + jsonPath("$.data.audios[?(@.audioContentId == ${localized.audioContentId})].title") + .value(contains("main-all-localized-audio")) + ) + .andExpect(jsonPath("$.data.audios[0].creatorId").doesNotExist()) + .andExpect(jsonPath("$.data.audios[0].creatorMemberId").doesNotExist()) + + 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 } + assertEquals( + 0L, + aliasQueryCount, + "Standalone alias SELECTs (total queries=${statistics.queryExecutionCount}): $aliasQueries" + ) + } + + @Test + @DisplayName("영어 활성 이름도 JOIN으로 읽으며 비활성 이름은 원본으로 fallback하고 인기순 집계를 유지한다") + fun shouldReadActiveEnglishCreatorNameAndFallbackWhenInactive() { + val active = createAudioFixture("main-all-english-active") + val inactive = createAudioFixture("main-all-english-inactive") + transactionTemplate.executeWithoutResult { + listOf(Triple(active.creatorId, "English creator", true), Triple(inactive.creatorId, "Inactive name", false)) + .forEach { (creatorId, name, isActive) -> + jdbcTemplate.update( + """ + insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) + values (?, 'EN', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """.trimIndent(), + creatorId, + name, + isActive + ) + } + } + + mockMvc.perform( + get("/api/v2/audio/contents").param("type", "AUDIO").param("sort", "POPULAR").header("Accept-Language", "en") + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.totalCount").value(2)) + .andExpect(jsonPath("$.data.audios.length()").value(2)) + .andExpect( + jsonPath("$.data.audios[?(@.audioContentId == ${active.audioContentId})].creatorNickname") + .value(contains("English creator")) + ) + .andExpect( + jsonPath("$.data.audios[?(@.audioContentId == ${inactive.audioContentId})].creatorNickname") + .value(contains("main-all-english-inactive-creator")) + ) + .andExpect( + jsonPath("$.data.audios[?(@.audioContentId == ${active.audioContentId})].title") + .value(contains("main-all-english-active-audio")) + ) + } + @Test @DisplayName("전체 탭 AUDIO API는 controller-service-repository를 거쳐 오디오 응답과 빈 series를 반환한다") fun shouldReturnAudioContentsThroughControllerServiceAndRepository() { @@ -196,7 +300,7 @@ class MainContentAllEndToEndTest @Autowired constructor( entityManager.flush() entityManager.clear() - AudioFixture(audioContentId = audio.id!!) + AudioFixture(audioContentId = audio.id!!, creatorId = creator.id!!) }!! } @@ -308,7 +412,8 @@ class MainContentAllEndToEndTest @Autowired constructor( } private data class AudioFixture( - val audioContentId: Long + val audioContentId: Long, + val creatorId: Long ) private data class SeriesFixture( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacadeTest.kt index fa18262b..16b16725 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacadeTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.content.all.application +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.v2.common.domain.ContentSort @@ -36,7 +37,7 @@ class MainContentAllFacadeTest { page = MainContentPage(1, 30), hasNext = false ) - ).`when`(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member) + ).`when`(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member, Lang.KO) val response = facade.getContents( type = "FREE", @@ -44,10 +45,11 @@ class MainContentAllFacadeTest { dayOfWeek = "MON", page = 1, size = 30, - member = member + member = member, + lang = Lang.KO ) - Mockito.verify(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member) + Mockito.verify(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member, Lang.KO) assertEquals(MainContentAllType.FREE, response.type) assertEquals(ContentSort.PRICE_LOW, response.sort) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt index d240725c..698e0c51 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt @@ -27,6 +27,7 @@ class MainContentAllTabResponseTest { audios = emptyList(), series = listOf( MainContentAllSeries( + creatorMemberId = 100L, seriesId = 10L, title = "시리즈", coverImageUrl = "https://cdn/series.jpg", @@ -58,6 +59,7 @@ class MainContentAllTabResponseTest { totalCount = 1, audios = listOf( MainContentAllAudio( + creatorMemberId = 100L, audioContentId = 1L, title = "audio", imageUrl = "https://cdn/audio.jpg", @@ -69,6 +71,7 @@ class MainContentAllTabResponseTest { creatorNickname = "creator" ), MainContentAllAudio( + creatorMemberId = 100L, audioContentId = 2L, title = "free audio", imageUrl = "https://cdn/free-audio.jpg", @@ -80,6 +83,7 @@ class MainContentAllTabResponseTest { creatorNickname = "creator" ), MainContentAllAudio( + creatorMemberId = 100L, audioContentId = 3L, title = "point unavailable audio", imageUrl = "https://cdn/point-unavailable-audio.jpg", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewControllerTest.kt index 8babe2a8..b84149d1 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewControllerTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.overview.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext import kr.co.vividnext.sodalive.configs.SecurityConfig +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.jwt.JwtAccessDeniedHandler @@ -13,6 +14,7 @@ import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.api.content.overview.application.ContentOverviewFacade import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponse import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewType +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -41,6 +43,11 @@ class ContentOverviewControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -62,7 +69,7 @@ class ContentOverviewControllerTest @Autowired constructor( fun shouldPassAuthenticatedMemberAndQueryParameters() { val member = member(id = 10L) Mockito.doReturn(emptyResponse(ContentOverviewType.FIRST_AUDIO_CONTENT)).`when`(facade) - .getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member)) + .getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member), eqValue(Lang.KO)) mockMvc.perform( get("/api/v2/contents") @@ -75,7 +82,8 @@ class ContentOverviewControllerTest @Autowired constructor( .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.type").value("FIRST_AUDIO_CONTENT")) - Mockito.verify(facade).getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member)) + Mockito.verify(facade) + .getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member), eqValue(Lang.KO)) } private fun eqValue(value: T): T { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacadeTest.kt index b8fc896f..ffe45558 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/application/ContentOverviewFacadeTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.content.overview.application +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.contentpreference.MemberContentPreferenceService @@ -31,9 +32,9 @@ class ContentOverviewFacadeTest { fun shouldReturnNewAndHotPage() { val member = member(id = 10L) Mockito.doReturn((1L..21L).map { audioCard(it) }).`when`(audioRecommendationQueryService) - .findNewAndHotAudios(member, offset = 0L, limit = 21) + .findNewAndHotAudios(member, offset = 0L, limit = 21, lang = Lang.KO) - val response = facade.getContents("NEW_AND_HOT_AUDIO", page = 0, size = 20, member = member) + val response = facade.getContents("NEW_AND_HOT_AUDIO", page = 0, size = 20, member = member, lang = Lang.KO) assertEquals(ContentOverviewType.NEW_AND_HOT_AUDIO, response.type) assertEquals((1L..20L).toList(), response.items.map { it.contentId }) @@ -54,10 +55,11 @@ class ContentOverviewFacadeTest { eqValue(20L), eqValue(21), eqValue(member.id), - eqValue(true) + eqValue(true), + eqValue(Lang.KO) ) - val response = facade.getContents("FIRST_AUDIO_CONTENT", page = 1, size = 20, member = member) + val response = facade.getContents("FIRST_AUDIO_CONTENT", page = 1, size = 20, member = member, lang = Lang.KO) assertEquals(ContentOverviewType.FIRST_AUDIO_CONTENT, response.type) assertEquals(listOf(1L, 2L), response.items.map { it.contentId }) @@ -79,6 +81,7 @@ class ContentOverviewFacadeTest { private fun audioCard(id: Long): AudioCard { return AudioCard( + creatorMemberId = id + 100L, audioContentId = id, title = "audio$id", duration = "00:01", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt index 848ba7bd..bf8ed8bb 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt @@ -120,6 +120,7 @@ class ContentOverviewPageResponseTest { private fun audioCard(price: Int, isPointAvailable: Boolean): AudioCard { return AudioCard( + creatorMemberId = 10L, audioContentId = price.toLong() + 1, title = "audio", duration = "00:01", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacadeTest.kt index 85c5178c..1200d5ca 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/application/AudioRankingFacadeTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.content.ranking.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryService import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRanking @@ -23,6 +24,7 @@ class AudioRankingFacadeTest { type = AudioRankingType.RISING, items = listOf( AudioRankingItem( + creatorMemberId = 10L, contentId = 1L, title = "audio", creatorNickname = "creator", @@ -33,11 +35,11 @@ class AudioRankingFacadeTest { ) ) ) - Mockito.doReturn(ranking).`when`(queryService).getRankings(AudioRankingType.RISING, member) + Mockito.doReturn(ranking).`when`(queryService).getRankings(AudioRankingType.RISING, member, Lang.KO) - val response = facade.getRankings(AudioRankingType.RISING, member) + val response = facade.getRankings(AudioRankingType.RISING, member, Lang.KO) - Mockito.verify(queryService).getRankings(AudioRankingType.RISING, member) + Mockito.verify(queryService).getRankings(AudioRankingType.RISING, member, Lang.KO) assertEquals(true, response.showRankChange) assertEquals(AudioRankingType.RISING, response.type) assertEquals(1, response.items.size) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/dto/AudioRankingResponseTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/dto/AudioRankingResponseTest.kt index 65528537..6d7f5a86 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/dto/AudioRankingResponseTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/dto/AudioRankingResponseTest.kt @@ -19,6 +19,7 @@ class AudioRankingResponseTest { type = AudioRankingType.RISING, items = listOf( AudioRankingItem( + creatorMemberId = 10L, contentId = 1001L, title = "rising audio", creatorNickname = "creator", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationControllerTest.kt index d2abfd98..50992a80 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationControllerTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext import kr.co.vividnext.sodalive.configs.SecurityConfig +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.jwt.JwtAccessDeniedHandler @@ -12,6 +13,7 @@ import kr.co.vividnext.sodalive.member.MemberAdapter import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacade import kr.co.vividnext.sodalive.v2.api.content.recommendation.dto.AudioRecommendationsResponse +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -39,6 +41,11 @@ class AudioRecommendationControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -54,7 +61,7 @@ class AudioRecommendationControllerTest @Autowired constructor( @Test @DisplayName("오디오 추천 조회는 비회원에게 200 OK와 ApiResponse.ok wrapper를 반환한다") fun shouldReturnRecommendationsForAnonymous() { - Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(null) + Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(null, Lang.KO) mockMvc.perform(get("/api/v2/audio/recommendations")) .andExpect(status().isOk) @@ -74,13 +81,13 @@ class AudioRecommendationControllerTest @Autowired constructor( nickname = "viewer", role = MemberRole.USER ).apply { id = 10L } - Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(eqValue(member)) + Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(eqValue(member), eqValue(Lang.KO)) mockMvc.perform(get("/api/v2/audio/recommendations").with(user(MemberAdapter(member)))) .andExpect(status().isOk) .andExpect(jsonPath("$.success").value(true)) - Mockito.verify(facade).getRecommendations(eqValue(member)) + Mockito.verify(facade).getRecommendations(eqValue(member), eqValue(Lang.KO)) } private fun emptyResponse(): AudioRecommendationsResponse { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt index b2d79d53..55799fd0 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.content.recommendation.application import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.common.domain.RecommendationBanner import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard @@ -20,9 +21,9 @@ class AudioRecommendationFacadeTest { @Test @DisplayName("facade는 도메인 추천 결과를 모든 공개 응답 필드로 변환한다") fun shouldConvertDomainRecommendationsToResponse() { - Mockito.doReturn(domain()).`when`(queryService).getRecommendations(null) + Mockito.doReturn(domain()).`when`(queryService).getRecommendations(null, Lang.KO) - val response = facade.getRecommendations(null) + val response = facade.getRecommendations(null, Lang.KO) assertEquals(1, response.banners.size) assertEquals(1, response.originalSeries.size) @@ -49,6 +50,7 @@ class AudioRecommendationFacadeTest { private fun domain(): AudioRecommendations { val card = AudioCard( + creatorMemberId = 10L, audioContentId = 1L, title = "audio", duration = "00:01", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioControllerTest.kt index a817086a..de3fc657 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext +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 @@ -11,6 +12,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelA import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelAudioThemeResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto.CreatorChannelAudioContentResponse import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -47,6 +49,11 @@ class CreatorChannelAudioControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -88,7 +95,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor( eqValue(null), eqValue(null), eqValue(null), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -118,7 +126,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor( eqValue(null), eqValue(null), eqValue(null), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } @@ -133,7 +142,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor( eqValue(999L), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -158,7 +168,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor( eqValue(999L), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt index 42e673bb..982bd733 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +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.v2.common.domain.ContentSort @@ -33,7 +34,8 @@ class CreatorChannelAudioFacadeTest { themeId = 10L, page = 0, size = 20, - now = now + now = now, + lang = Lang.KO ) val response = facade.getAudioTab( @@ -43,7 +45,8 @@ class CreatorChannelAudioFacadeTest { themeId = 10L, page = 0, size = 20, - now = now + now = now, + lang = Lang.KO ) assertEquals(3, response.audioContentCount) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt index ce9c67b4..cd6b6433 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext +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 @@ -14,6 +15,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChan import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityRepliesResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityReplyResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityTabResponse +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -50,6 +52,11 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -89,7 +96,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -117,7 +125,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } @@ -130,7 +139,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -148,7 +158,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } @@ -169,7 +180,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( Mockito.doReturn(createDetailResponse()).`when`(facade).getCommunityPostDetail( eqValue(101L), eqValue(viewer), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -195,7 +207,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(0), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -222,7 +235,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -240,7 +254,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } @@ -253,7 +268,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(0), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -278,7 +294,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -296,7 +313,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor( eqValue(viewer), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt index f0303992..17758620 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt @@ -22,6 +22,7 @@ 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.web.servlet.MockMvc @@ -44,7 +45,8 @@ import javax.persistence.EntityManager class CreatorChannelCommunityEndToEndTest @Autowired constructor( private val mockMvc: MockMvc, private val entityManager: EntityManager, - private val transactionTemplate: TransactionTemplate + private val transactionTemplate: TransactionTemplate, + private val jdbcTemplate: JdbcTemplate ) { @MockBean private lateinit var audioContentCloudFront: AudioContentCloudFront @@ -52,6 +54,67 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor( @MockBean private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService + @Test + @DisplayName("커뮤니티 상세의 JA 이름은 크리에이터 댓글과 최신 답글에만 적용하고 USER 이름은 유지한다") + fun shouldLocalizeCreatorAuthorsAndLatestRepliesWithoutChangingUserNames() { + val fixture = transactionTemplate.execute { + val viewer = saveMember("community-localized-viewer", MemberRole.USER) + val creator = saveMember("community-localized-creator", MemberRole.CREATOR) + savePreference(viewer, isAdultContentVisible = true) + val post = saveCommunity(creator, isFixed = false, price = 0, content = "localized-name community") + val userComment = saveCommunityComment(viewer, post) + val creatorComment = saveCommunityComment(creator, post) + saveCommunityComment(creator, post, parent = userComment) + entityManager.flush() + listOf(creator.id!! to "日本語の投稿者", viewer.id!! to "USERには使わない名前").forEach { (memberId, name) -> + 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) + """.trimIndent(), + memberId, + name + ) + } + updateCommentCreatedAt(userComment.id!!, LocalDateTime.of(2026, 10, 14, 10, 0)) + updateCommentCreatedAt(creatorComment.id!!, LocalDateTime.of(2026, 10, 14, 11, 0)) + entityManager.clear() + PostFixture(viewer = viewer, postId = post.id!!) + }!! + + mockMvc.perform( + get("/api/v2/creator-channels/community-posts/${fixture.postId}") + .header("Accept-Language", "ja") + .with(user(MemberAdapter(fixture.viewer))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.creatorNickname").value("日本語の投稿者")) + .andExpect(jsonPath("$.data.content").value("localized-name community")) + .andExpect(jsonPath("$.data.comments.comments.length()").value(2)) + .andExpect(jsonPath("$.data.comments.comments[0].writerNickname").value("日本語の投稿者")) + .andExpect(jsonPath("$.data.comments.comments[1].writerId").value(fixture.viewer.id)) + .andExpect(jsonPath("$.data.comments.comments[1].writerNickname").value("community-localized-viewer")) + .andExpect(jsonPath("$.data.comments.comments[1].latestReply.writerNickname").value("日本語の投稿者")) + + transactionTemplate.executeWithoutResult { + jdbcTemplate.update( + "update creator_localized_name set display_name = ? where member_id = ? and lang = 'JA'", + "deleted_表示名そのまま", + entityManager.find(CreatorCommunity::class.java, fixture.postId).member!!.id!! + ) + } + mockMvc.perform( + get("/api/v2/creator-channels/community-posts/${fixture.postId}") + .header("Accept-Language", "ja") + .with(user(MemberAdapter(fixture.viewer))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.creatorNickname").value("deleted_表示名そのまま")) + .andExpect(jsonPath("$.data.comments.comments[0].writerNickname").value("deleted_表示名そのまま")) + .andExpect(jsonPath("$.data.comments.comments[1].latestReply.writerNickname").value("deleted_表示名そのまま")) + } + @Test @DisplayName("커뮤니티 탭 API는 E2E로 정렬, fallback, 성인 필터, 유료 미디어 접근 정책을 반환한다") fun shouldReturnCommunityTabThroughControllerServiceAndRepository() { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt index ef2dec13..db011e51 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.community.application import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +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.v2.api.creator.channel.community.dto.CreatorChannelCommunityTabResponse @@ -71,7 +72,8 @@ class CreatorChannelCommunityFacadeTest { viewer = viewer, page = -1, size = 100, - now = now + now = now, + lang = Lang.KO ) val response = facade.getCommunityTab( @@ -79,7 +81,8 @@ class CreatorChannelCommunityFacadeTest { viewer = viewer, page = -1, size = 100, - now = now + now = now, + lang = Lang.KO ) assertEquals(2, response.communityPostCount) @@ -99,9 +102,9 @@ class CreatorChannelCommunityFacadeTest { val facade = CreatorChannelCommunityFacade(service) val viewer = createMember(id = 10L) val now = LocalDateTime.of(2026, 7, 6, 12, 0) - Mockito.doReturn(createDetail()).`when`(service).getCommunityPostDetail(101L, viewer, now) + Mockito.doReturn(createDetail()).`when`(service).getCommunityPostDetail(101L, viewer, now, Lang.KO) - val response = facade.getCommunityPostDetail(101L, viewer, now) + val response = facade.getCommunityPostDetail(101L, viewer, now, Lang.KO) assertEquals(101L, response.postId) assertTrue(response.isLiked) @@ -119,9 +122,9 @@ class CreatorChannelCommunityFacadeTest { val facade = CreatorChannelCommunityFacade(service) val viewer = createMember(id = 10L) val now = LocalDateTime.of(2026, 7, 6, 12, 0) - Mockito.doReturn(createComments()).`when`(service).getCommunityComments(101L, viewer, -1, 100, now) + Mockito.doReturn(createComments()).`when`(service).getCommunityComments(101L, viewer, -1, 100, now, Lang.KO) - val response = facade.getCommunityComments(101L, viewer, -1, 100, now) + val response = facade.getCommunityComments(101L, viewer, -1, 100, now, Lang.KO) assertEquals(1, response.commentCount) assertEquals(0, response.page) @@ -139,9 +142,9 @@ class CreatorChannelCommunityFacadeTest { val facade = CreatorChannelCommunityFacade(service) val viewer = createMember(id = 10L) val now = LocalDateTime.of(2026, 7, 6, 12, 0) - Mockito.doReturn(createReplies()).`when`(service).getCommunityReplies(201L, viewer, -1, 100, now) + Mockito.doReturn(createReplies()).`when`(service).getCommunityReplies(201L, viewer, -1, 100, now, Lang.KO) - val response = facade.getCommunityReplies(201L, viewer, -1, 100, now) + val response = facade.getCommunityReplies(201L, viewer, -1, 100, now, Lang.KO) assertEquals(1, response.replyCount) assertEquals(201L, response.replies.first().commentId) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt index addcd495..540e730a 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext +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 @@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.Crea import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.MemberDonationRankingResponse +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -46,6 +48,11 @@ class CreatorChannelDonationControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -85,7 +92,8 @@ class CreatorChannelDonationControllerTest @Autowired constructor( eqValue(viewer), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -117,7 +125,8 @@ class CreatorChannelDonationControllerTest @Autowired constructor( eqValue(viewer), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt index d04fe573..fd352035 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +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.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse @@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChanne import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationRanking import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationTab import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage +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.assertTrue @@ -57,7 +59,10 @@ class CreatorChannelDonationFacadeTest { @DisplayName("후원 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다") fun shouldMapDonationTabQueryResultToPublicResponse() { val service = Mockito.mock(CreatorChannelDonationQueryService::class.java) - val facade = CreatorChannelDonationFacade(service) + val facade = CreatorChannelDonationFacade( + service, + Mockito.mock(CreatorLocalizedNameService::class.java) + ) val viewer = createMember(id = 10L) val now = LocalDateTime.of(2026, 6, 21, 12, 0) Mockito.doReturn(createTab()).`when`(service).getDonationTab( @@ -65,7 +70,8 @@ class CreatorChannelDonationFacadeTest { viewer = viewer, page = -1, size = 100, - now = now + now = now, + lang = Lang.KO ) val response = facade.getDonationTab( @@ -73,7 +79,8 @@ class CreatorChannelDonationFacadeTest { viewer = viewer, page = -1, size = 100, - now = now + now = now, + lang = Lang.KO ) assertEquals(3, response.donationCount) @@ -106,6 +113,7 @@ class CreatorChannelDonationFacadeTest { ), donations = listOf( CreatorChannelDonation( + memberId = 10L, nickname = "donor", profileImageUrl = "https://cdn.test/donor.png", can = 50, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkControllerTest.kt index ba73eca9..21e058a8 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext +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 @@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application.Creat import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkReplyResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -46,6 +48,11 @@ class CreatorChannelFanTalkControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -85,7 +92,8 @@ class CreatorChannelFanTalkControllerTest @Autowired constructor( eqValue(viewer), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -111,7 +119,8 @@ class CreatorChannelFanTalkControllerTest @Autowired constructor( eqValue(viewer), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacadeTest.kt index f4272a21..8f01caa9 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacadeTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +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.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse @@ -59,7 +60,8 @@ class CreatorChannelFanTalkFacadeTest { viewer = viewer, page = -1, size = 100, - now = now + now = now, + lang = Lang.KO ) val response = facade.getFanTalkTab( @@ -67,7 +69,8 @@ class CreatorChannelFanTalkFacadeTest { viewer = viewer, page = -1, size = 100, - now = now + now = now, + lang = Lang.KO ) assertEquals(2, response.fanTalkCount) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt index a95f29f3..8b305a21 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext +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 @@ -21,6 +22,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelLiv import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelSchedule import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelSeries import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelSns +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -57,6 +59,11 @@ class CreatorChannelHomeControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -94,7 +101,8 @@ class CreatorChannelHomeControllerTest @Autowired constructor( Mockito.doReturn(CreatorChannelHomeResponse.from(createHome())).`when`(facade).getHome( Mockito.eq(1L), Mockito.any(Member::class.java) ?: viewer, - Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now() + Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(), + Mockito.eq(Lang.KO) ?: Lang.KO ) mockMvc.perform( @@ -147,7 +155,8 @@ class CreatorChannelHomeControllerTest @Autowired constructor( Mockito.verify(facade).getHome( Mockito.eq(1L), Mockito.eq(viewer) ?: viewer, - Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now() + Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(), + Mockito.eq(Lang.KO) ?: Lang.KO ) } @@ -158,7 +167,8 @@ class CreatorChannelHomeControllerTest @Autowired constructor( Mockito.doReturn(CreatorChannelHomeResponse.from(createEmptyHome())).`when`(facade).getHome( Mockito.eq(1L), Mockito.any(Member::class.java) ?: viewer, - Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now() + Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(), + Mockito.eq(Lang.KO) ?: Lang.KO ) mockMvc.perform( @@ -244,6 +254,7 @@ class CreatorChannelHomeControllerTest @Autowired constructor( ), channelDonations = listOf( CreatorChannelDonation( + memberId = 20L, nickname = "fan", profileImageUrl = "fan.png", can = 50, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt index be855561..9dfb070f 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.home.application +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.v2.common.domain.CreatorActivityType @@ -39,13 +40,15 @@ class CreatorChannelHomeFacadeTest { Mockito.doReturn(createHome()).`when`(service).getHome( creatorId = 1L, viewer = viewer, - now = now + now = now, + lang = Lang.KO ) val response = facade.getHome( creatorId = 1L, viewer = viewer, - now = now + now = now, + lang = Lang.KO ) assertEquals(1L, response.creator.creatorId) @@ -90,21 +93,23 @@ class CreatorChannelHomeFacadeTest { Mockito.doReturn(createHome()).`when`(service).getHome( Mockito.eq(1L), Mockito.eq(viewer) ?: viewer, - Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now() + Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(), + Mockito.eq(Lang.KO) ?: Lang.KO ) try { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")) val beforeUtc = LocalDateTime.now(ZoneOffset.UTC).minusSeconds(1) - facade.getHome(creatorId = 1L, viewer = viewer) + facade.getHome(creatorId = 1L, viewer = viewer, lang = Lang.KO) val afterUtc = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(1) val nowCaptor = ArgumentCaptor.forClass(LocalDateTime::class.java) Mockito.verify(service).getHome( Mockito.eq(1L), Mockito.eq(viewer) ?: viewer, - nowCaptor.capture() ?: LocalDateTime.MIN + nowCaptor.capture() ?: LocalDateTime.MIN, + Mockito.eq(Lang.KO) ?: Lang.KO ) val capturedNow = nowCaptor.value assertTrue(!capturedNow.isBefore(beforeUtc), "captured now should be after UTC lower bound") @@ -180,6 +185,7 @@ class CreatorChannelHomeFacadeTest { ), channelDonations = listOf( CreatorChannelDonation( + memberId = 20L, nickname = "fan", profileImageUrl = "fan.png", can = 50, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt index ff50925d..a7de8df0 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext +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 @@ -11,6 +12,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorC import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveTabResponse import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -47,6 +49,11 @@ class CreatorChannelLiveControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -87,7 +94,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor( eqValue(ContentSort.LATEST), eqValue(0), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -112,7 +120,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor( eqValue(ContentSort.LATEST), eqValue(0), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } @@ -127,7 +136,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor( eqValue(ContentSort.LATEST), eqValue(0), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -162,7 +172,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor( eqValue(ContentSort.LATEST), eqValue(-1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -185,7 +196,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor( eqValue(ContentSort.LATEST), eqValue(0), eqValue(0), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt index e17f02a0..d930f17f 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.live.application +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.v2.common.domain.ContentSort @@ -30,7 +31,8 @@ class CreatorChannelLiveFacadeTest { sort = ContentSort.LATEST, page = 0, size = 20, - now = now + now = now, + lang = Lang.KO ) val response = facade.getLiveTab( @@ -39,7 +41,8 @@ class CreatorChannelLiveFacadeTest { sort = ContentSort.LATEST, page = 0, size = 20, - now = now + now = now, + lang = Lang.KO ) assertEquals(1, response.liveReplayContentCount) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesControllerTest.kt index 7ad5dc80..9d69ce3f 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.series.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext +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 @@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.series.application.Creato import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -46,6 +48,11 @@ class CreatorChannelSeriesControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -86,7 +93,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor( eqValue("POPULAR"), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -118,7 +126,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor( eqValue("POPULAR"), eqValue(1), eqValue(20), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } @@ -132,7 +141,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor( eqValue("INVALID"), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) mockMvc.perform( @@ -153,7 +163,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor( eqValue("INVALID"), eqValue(-1), eqValue(100), - anyValue(LocalDateTime.now()) + anyValue(LocalDateTime.now()), + eqValue(Lang.KO) ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacadeTest.kt index edc813f0..bc9e0ba8 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/application/CreatorChannelSeriesFacadeTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.series.application import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +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.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse @@ -63,7 +64,8 @@ class CreatorChannelSeriesFacadeTest { sort = "OWNED", page = 1, size = 20, - now = now + now = now, + lang = Lang.KO ) val response = facade.getSeriesTab( @@ -72,7 +74,8 @@ class CreatorChannelSeriesFacadeTest { sort = "OWNED", page = 1, size = 20, - now = now + now = now, + lang = Lang.KO ) assertEquals(2, response.seriesCount) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt index 2496fe1f..bb8188b5 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt @@ -9,11 +9,13 @@ import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.CreatorRankingSnapshot import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreSpec import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType +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.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +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.web.servlet.MockMvc @@ -37,8 +39,51 @@ import javax.persistence.EntityManager class CreatorRankingControllerTest @Autowired constructor( private val mockMvc: MockMvc, private val memberRepository: MemberRepository, - private val entityManager: EntityManager + private val entityManager: EntityManager, + private val jdbcTemplate: JdbcTemplate ) { + @Test + @DisplayName("언어 중립 랭킹은 조회 언어의 현재 이름을 반환하고 저장된 스냅샷 이름은 변경하지 않는다") + fun shouldRenderCurrentLocalizedNamesWithoutRewritingRankingSnapshots() { + val localized = saveMember("ranking-localized-before", MemberRole.CREATOR) + val fallback = saveMember("ranking-fallback-before", MemberRole.CREATOR) + saveSnapshot(localized.id!!, "ranking-localized-before", null, rankNo = 1) + saveSnapshot(fallback.id!!, "ranking-fallback-before", null, rankNo = 2) + 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) + """.trimIndent(), + localized.id, + "日本語ランキング名" + ) + localized.nickname = "ranking-localized-current" + fallback.nickname = "ranking-fallback-current" + entityManager.flush() + entityManager.clear() + + mockMvc.perform(get("/api/v2/home/rankings/creators").header("Accept-Language", "ja")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.items[0].creatorId").value(localized.id)) + .andExpect(jsonPath("$.data.items[0].nickname").value("日本語ランキング名")) + .andExpect(jsonPath("$.data.items[1].creatorId").value(fallback.id)) + .andExpect(jsonPath("$.data.items[1].nickname").value("ranking-fallback-current")) + + mockMvc.perform(get("/api/v2/home/rankings/creators").header("Accept-Language", "ko")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.items[0].nickname").value("ranking-localized-current")) + .andExpect(jsonPath("$.data.items[1].nickname").value("ranking-fallback-current")) + + assertEquals( + listOf("ranking-localized-before", "ranking-fallback-before"), + entityManager.createQuery( + "select s.nickname from CreatorRankingSnapshot s order by s.rankNo", + String::class.java + ).resultList + ) + assertEquals("ranking-localized-current", memberRepository.findById(localized.id!!).get().nickname) + } + @Test @DisplayName("크리에이터 랭킹 API는 공개 응답 스키마만 노출하고 내부 스냅샷 필드를 숨긴다") fun shouldReturnOnlyPublicCreatorRankingSchemaThroughEndpoint() { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt index e46538da..85fec011 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home import kr.co.vividnext.sodalive.chat.character.ChatCharacter import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member @@ -35,6 +36,7 @@ import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.boot.test.system.CapturedOutput import org.springframework.boot.test.system.OutputCaptureExtension import org.springframework.http.MediaType +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.web.servlet.MockMvc @@ -56,11 +58,50 @@ class HomeRecommendationControllerTest @Autowired constructor( private val mockMvc: MockMvc, private val memberRepository: MemberRepository, private val creatorFollowingRepository: CreatorFollowingRepository, - private val entityManager: EntityManager + private val entityManager: EntityManager, + private val jdbcTemplate: JdbcTemplate ) { @MockBean private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService + @Test + @DisplayName("native 데뷔 크리에이터 조회도 JA 별칭을 우선하고 KO 별칭 미등록 시 현재 원본 이름을 반환한다") + fun shouldLocalizeNativeDebutCreatorResultsByRequestLanguage() { + val viewer = saveMember("localized-native-viewer", MemberRole.USER) + val creator = saveMember("localized-native-before", MemberRole.CREATOR) + saveLiveRoom(creator, LocalDateTime.now().minusDays(1), "localized-native-live", isAdult = false) + 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) + """.trimIndent(), + creator.id, + "日本語デビュー名" + ) + creator.nickname = "localized-native-current" + entityManager.flush() + entityManager.clear() + + mockMvc.perform( + get("/api/v2/home/recommendations/debut-creators") + .header("Accept-Language", "ko") + .with(user(MemberAdapter(viewer))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.items[0].creatorId").value(creator.id)) + .andExpect(jsonPath("$.data.items[0].creatorNickname").value("localized-native-current")) + + mockMvc.perform( + get("/api/v2/home/recommendations/debut-creators") + .header("Accept-Language", "ja") + .with(user(MemberAdapter(viewer))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.items[0].creatorId").value(creator.id)) + .andExpect(jsonPath("$.data.items[0].creatorNickname").value("日本語デビュー名")) + } + @Test @DisplayName("추천 크리에이터 동시 팔로우 비로그인 요청은 Spring Security에서 거부한다") fun shouldRejectAnonymousFollowRequest() { @@ -328,12 +369,16 @@ class HomeRecommendationControllerTest @Autowired constructor( fun shouldLogHomeRecommendationFailure(output: CapturedOutput) { val failingQueryService = Mockito.mock(HomeRecommendationQueryService::class.java) val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) - val facade = HomeRecommendationFacade(failingQueryService, preferenceService, "https://cdn.test") - Mockito.`when`(failingQueryService.findLiveRecommendations(limit = 20, memberId = null, includeAdultLives = false)) + val facade = HomeRecommendationFacade( + failingQueryService, + preferenceService, + "https://cdn.test" + ) + Mockito.`when`(failingQueryService.findLiveRecommendations(limit = 20, memberId = null, lang = Lang.KO)) .thenThrow(IllegalStateException("home query failed")) val exception = assertThrows(IllegalStateException::class.java) { - facade.getHomeRecommendations(member = null) + facade.getHomeRecommendations(member = null, lang = Lang.KO) } assertEquals("home query failed", exception.message) @@ -366,7 +411,11 @@ class HomeRecommendationControllerTest @Autowired constructor( val member = saveMember("page-failure-viewer", MemberRole.USER) val failingQueryService = Mockito.mock(HomeRecommendationQueryService::class.java) val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) - val facade = HomeRecommendationFacade(failingQueryService, preferenceService, "https://cdn.test") + val facade = HomeRecommendationFacade( + failingQueryService, + preferenceService, + "https://cdn.test" + ) Mockito.`when`(preferenceService.canViewAdultContent(member)).thenReturn(false) Mockito.`when`( failingQueryService.findLiveRecommendations( @@ -375,13 +424,14 @@ class HomeRecommendationControllerTest @Autowired constructor( memberId = member.id, includeAdultLives = false, isViewerCreator = false, - effectiveViewerGender = Gender.NONE + effectiveViewerGender = Gender.NONE, + lang = Lang.KO ) ) .thenThrow(IllegalStateException("page query failed")) val exception = assertThrows(IllegalStateException::class.java) { - facade.getLives(member, page = 0, size = 20) + facade.getLives(member, page = 0, size = 20, lang = Lang.KO) } assertEquals("page query failed", exception.message) @@ -395,7 +445,11 @@ class HomeRecommendationControllerTest @Autowired constructor( val member = saveMember("other-page-failure-viewer", MemberRole.USER) val failingQueryService = Mockito.mock(HomeRecommendationQueryService::class.java) val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) - val facade = HomeRecommendationFacade(failingQueryService, preferenceService, "https://cdn.test") + val facade = HomeRecommendationFacade( + failingQueryService, + preferenceService, + "https://cdn.test" + ) Mockito.`when`(preferenceService.canViewAdultContent(member)).thenReturn(false) Mockito.`when`( failingQueryService.findRecentDebutCreators( @@ -403,13 +457,16 @@ class HomeRecommendationControllerTest @Autowired constructor( offset = Mockito.eq(0L), limit = Mockito.eq(21), memberId = Mockito.eq(member.id), - includeAdultContents = Mockito.eq(false) + includeAdultContents = Mockito.eq(false), + lang = Mockito.eq(Lang.KO) ?: Lang.KO ) ).thenThrow(IllegalStateException("debut page failed")) Mockito.`when`(failingQueryService.findAiCharacterRecommendations(offset = 0L, limit = 21)) .thenThrow(IllegalStateException("ai page failed")) - assertThrows(IllegalStateException::class.java) { facade.getRecentDebutCreators(member, page = 0, size = 20) } + assertThrows(IllegalStateException::class.java) { + facade.getRecentDebutCreators(member, page = 0, size = 20, lang = Lang.KO) + } assertThrows(IllegalStateException::class.java) { facade.getAiCharacters(member, page = 0, size = 20) } assertTrue(output.out.contains("section=DEBUT_CREATOR")) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt index cd286c06..f8cedadb 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.home.application import kr.co.vividnext.sodalive.i18n.Lang -import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole @@ -35,11 +34,14 @@ class HomeRecommendationFacadeTest { private val queryService = HomeRecommendationQueryService( queryPort, EmptyRecommendationSnapshotPort(), - langContext = LangContext(), creatorCommunityTranslationService = creatorCommunityTranslationService ) private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) - private val facade = HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test") + private val facade = HomeRecommendationFacade( + queryService, + preferenceService, + "https://cdn.test" + ) @Test @DisplayName("홈 첫 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다") @@ -50,7 +52,7 @@ class HomeRecommendationFacadeTest { firstAudio(price = 100, isPointAvailable = false) ) - val response = facade.getHomeRecommendations(null) + val response = facade.getHomeRecommendations(null, Lang.KO) assertFalse(response.firstAudioContents[0].isPointAvailable) assertTrue(response.firstAudioContents[1].isPointAvailable) @@ -63,7 +65,7 @@ class HomeRecommendationFacadeTest { val member = Member(password = "password", nickname = "member").apply { id = 10L } Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member) - facade.getHomeRecommendations(member) + facade.getHomeRecommendations(member, Lang.KO) assertTrue(queryPort.bannerIncludeAdultBanners) } @@ -74,7 +76,7 @@ class HomeRecommendationFacadeTest { val member = Member(password = "password", nickname = "jp-member").apply { id = 11L } Mockito.doReturn("JP").`when`(preferenceService).resolveCountryCode(member) - facade.getHomeRecommendations(member) + facade.getHomeRecommendations(member, Lang.KO) assertTrue(queryPort.bannerLang == Lang.JA) } @@ -84,7 +86,7 @@ class HomeRecommendationFacadeTest { fun shouldPassKoToHomeBannerQueryForNonJapan() { Mockito.doReturn("KR").`when`(preferenceService).resolveCountryCode(null) - facade.getHomeRecommendations(null) + facade.getHomeRecommendations(null, Lang.KO) assertTrue(queryPort.bannerLang == Lang.KO) } @@ -94,7 +96,7 @@ class HomeRecommendationFacadeTest { fun shouldPassCreatorRoleAndAuthGenderToHomeLiveQuery() { val member = createMember(id = 12L, role = MemberRole.CREATOR, gender = Gender.FEMALE, authGender = 1) - facade.getHomeRecommendations(member) + facade.getHomeRecommendations(member, Lang.KO) assertTrue(queryPort.liveIsViewerCreator == true) assertTrue(queryPort.liveEffectiveViewerGender == Gender.MALE) @@ -105,7 +107,7 @@ class HomeRecommendationFacadeTest { fun shouldPassCreatorRoleAndMemberGenderToLivePageQuery() { val member = createMember(id = 13L, role = MemberRole.CREATOR, gender = Gender.FEMALE) - facade.getLives(member, page = 0, size = 20) + facade.getLives(member, page = 0, size = 20, lang = Lang.KO) assertTrue(queryPort.liveIsViewerCreator == true) assertTrue(queryPort.liveEffectiveViewerGender == Gender.FEMALE) @@ -153,7 +155,8 @@ class HomeRecommendationFacadeTest { memberId: Long?, includeAdultLives: Boolean, isViewerCreator: Boolean, - effectiveViewerGender: Gender? + effectiveViewerGender: Gender?, + lang: Lang ): List { liveIsViewerCreator = isViewerCreator liveEffectiveViewerGender = effectiveViewerGender @@ -174,7 +177,8 @@ class HomeRecommendationFacadeTest { override fun findRecentlyActiveCreators( limit: Int, memberId: Long?, - includeAdultActivities: Boolean + includeAdultActivities: Boolean, + lang: Lang ): List = emptyList() override fun findRecentDebutCreators( @@ -182,7 +186,8 @@ class HomeRecommendationFacadeTest { offset: Long, limit: Int, memberId: Long?, - includeAdultContents: Boolean + includeAdultContents: Boolean, + lang: Lang ): List = emptyList() override fun findFirstAudioContents( @@ -220,20 +225,23 @@ class HomeRecommendationFacadeTest { override fun findCheerCreatorRecommendationDetails( creatorIds: List, - memberId: Long? + memberId: Long?, + lang: Lang ): List = emptyList() override fun findPopularCommunityRecommendationDetails( communityIds: List, memberId: Long?, - includeAdultCommunities: Boolean + includeAdultCommunities: Boolean, + lang: Lang ): List = emptyList() override fun findGenreCreatorRecommendations( memberId: Long?, includeAdultGenres: Boolean, genreLimit: Int, - creatorLimit: Int + creatorLimit: Int, + lang: Lang ): List = emptyList() } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingControllerTest.kt index 853d8231..cadc182f 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingControllerTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home.following.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext import kr.co.vividnext.sodalive.configs.SecurityConfig +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.jwt.JwtAccessDeniedHandler @@ -12,6 +13,7 @@ import kr.co.vividnext.sodalive.member.MemberAdapter import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacade import kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponse +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -39,6 +41,11 @@ class HomeFollowingControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -54,7 +61,7 @@ class HomeFollowingControllerTest @Autowired constructor( @Test @DisplayName("팔로잉 탭 조회는 비회원에게 200 OK와 로그인 필요 응답을 반환한다") fun shouldReturnLoginRequiredForAnonymous() { - Mockito.doReturn(HomeFollowingTabResponse.loginRequired()).`when`(facade).getFollowingTab(null) + Mockito.doReturn(HomeFollowingTabResponse.loginRequired()).`when`(facade).getFollowingTab(null, Lang.KO) mockMvc.perform(get("/api/v2/home/following")) .andExpect(status().isOk) @@ -76,14 +83,14 @@ class HomeFollowingControllerTest @Autowired constructor( nickname = "viewer", role = MemberRole.USER ).apply { id = 10L } - Mockito.doReturn(loggedInEmptyResponse()).`when`(facade).getFollowingTab(eqValue(member)) + Mockito.doReturn(loggedInEmptyResponse()).`when`(facade).getFollowingTab(eqValue(member), eqValue(Lang.KO)) mockMvc.perform(get("/api/v2/home/following").with(user(MemberAdapter(member)))) .andExpect(status().isOk) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.isLoginRequired").value(false)) - Mockito.verify(facade).getFollowingTab(eqValue(member)) + Mockito.verify(facade).getFollowingTab(eqValue(member), eqValue(Lang.KO)) } private fun loggedInEmptyResponse(): HomeFollowingTabResponse { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt index a4f36fc1..7b699107 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.home.following.application +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.v2.chat.dto.ChatRoomListItemResponse @@ -24,12 +25,15 @@ import org.mockito.Mockito class HomeFollowingFacadeTest { private val queryService = Mockito.mock(HomeFollowingQueryService::class.java) private val chatRoomListService = Mockito.mock(ChatRoomListService::class.java) - private val facade = HomeFollowingFacade(queryService, chatRoomListService) + private val facade = HomeFollowingFacade( + queryService, + chatRoomListService + ) @Test @DisplayName("비로그인 회원은 로그인 필요 응답을 반환하고 조회/채팅 서비스를 호출하지 않는다") fun shouldReturnLoginRequiredWithoutCallingServicesForAnonymous() { - val response = facade.getFollowingTab(null) + val response = facade.getFollowingTab(null, Lang.KO) assertTrue(response.isLoginRequired) assertTrue(response.followingCreators.isEmpty()) @@ -58,12 +62,12 @@ class HomeFollowingFacadeTest { lastMessage = "hello", lastMessageAt = "2026-06-25T01:00:00Z" ) - Mockito.doReturn(home).`when`(queryService).findHomeFollowing(member) + Mockito.doReturn(home).`when`(queryService).findHomeFollowing(member, Lang.KO) Mockito.doReturn(ChatRoomListPageResponse(rooms = listOf(recentChat), hasMore = false, nextCursor = null)) .`when`(chatRoomListService) - .getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true) + .getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true) - val response = facade.getFollowingTab(member) + val response = facade.getFollowingTab(member, Lang.KO) assertFalse(response.isLoginRequired) assertEquals(1L, response.followingCreators.single().creatorId) @@ -71,9 +75,9 @@ class HomeFollowingFacadeTest { assertEquals(listOf(recentChat), response.recentChats) assertEquals("LIVE:4", response.monthlySchedules.single().scheduleId) assertEquals("news-5", response.recentNews.single().newsId) - Mockito.verify(queryService).findHomeFollowing(member) + Mockito.verify(queryService).findHomeFollowing(member, Lang.KO) Mockito.verify(chatRoomListService) - .getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true) + .getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true) } private fun homeFollowing(): HomeFollowing { @@ -87,6 +91,7 @@ class HomeFollowingFacadeTest { ), onAirLives = listOf( HomeFollowingLive( + creatorMemberId = 1L, liveId = 2L, creatorProfileImageUrl = "https://cdn.test/live.png", creatorNickname = "creator", @@ -110,6 +115,7 @@ class HomeFollowingFacadeTest { ), recentNews = listOf( HomeFollowingNews( + creatorMemberId = 1L, newsId = "news-5", type = FollowingNewsType.CREATOR_RANKING, visibleFromAtUtc = "2026-06-25T04:00:00Z", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt index 93e5cd5c..db506366 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt @@ -115,6 +115,7 @@ class HomeFollowingTabResponseTest { followingCreators = listOf(HomeFollowingCreator(1L, "creator", "https://cdn/profile.jpg")), onAirLives = listOf( HomeFollowingLive( + creatorMemberId = 101L, liveId = 10L, creatorProfileImageUrl = "https://cdn/live-profile.jpg", creatorNickname = "live-creator", @@ -147,6 +148,7 @@ class HomeFollowingTabResponseTest { ), recentNews = listOf( HomeFollowingNews( + creatorMemberId = 1L, newsId = "30", type = FollowingNewsType.CREATOR_RANKING, visibleFromAtUtc = "2026-06-25T09:00:00Z", @@ -158,6 +160,7 @@ class HomeFollowingTabResponseTest { ) ), HomeFollowingNews( + creatorMemberId = 2L, newsId = "31", type = FollowingNewsType.AUDIO_CONTENT, visibleFromAtUtc = "2026-06-26T00:00:00Z", @@ -170,6 +173,7 @@ class HomeFollowingTabResponseTest { ) ), HomeFollowingNews( + creatorMemberId = 3L, newsId = "32", type = FollowingNewsType.PHOTO_CONTENT, visibleFromAtUtc = "2026-06-27T00:00:00Z", @@ -182,6 +186,7 @@ class HomeFollowingTabResponseTest { ) ), HomeFollowingNews( + creatorMemberId = 4L, newsId = "33", type = FollowingNewsType.CONTENT_RANKING, visibleFromAtUtc = "2026-06-28T00:00:00Z", @@ -193,6 +198,7 @@ class HomeFollowingTabResponseTest { ) ), HomeFollowingNews( + creatorMemberId = 5L, newsId = "34", type = FollowingNewsType.COMMUNITY_POST, visibleFromAtUtc = "2026-06-25T03:00:00Z", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt index 2330d108..2a11ce23 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home.live.adapter.`in`.web import kr.co.vividnext.sodalive.common.CountryContext import kr.co.vividnext.sodalive.configs.SecurityConfig +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.jwt.JwtAccessDeniedHandler @@ -13,6 +14,7 @@ import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.api.home.live.application.HomeOnAirLiveFacade import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLivePageResponse import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLiveResponse +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -41,6 +43,11 @@ class HomeOnAirLiveControllerTest @Autowired constructor( @MockBean private lateinit var langContext: LangContext + @BeforeEach + fun setUpLanguage() { + Mockito.`when`(langContext.lang).thenReturn(Lang.KO) + } + @MockBean private lateinit var sodaMessageSource: SodaMessageSource @@ -61,7 +68,7 @@ class HomeOnAirLiveControllerTest @Autowired constructor( @DisplayName("현재 진행 중인 라이브 조회는 인증 회원과 page를 facade에 전달하고 성공 응답을 반환한다") fun shouldPassAuthenticatedMemberAndPageToFacade() { val member = createMember(100L) - Mockito.doReturn(createResponse()).`when`(facade).getOnAirLives(eqValue(member), eqValue(2)) + Mockito.doReturn(createResponse()).`when`(facade).getOnAirLives(eqValue(member), eqValue(2), eqValue(Lang.KO)) mockMvc.perform( get("/api/v2/home/on-air-lives") @@ -73,7 +80,7 @@ class HomeOnAirLiveControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.size").value(20)) .andExpect(jsonPath("$.data.items[0].title").value("paid live")) - Mockito.verify(facade).getOnAirLives(eqValue(member), eqValue(2)) + Mockito.verify(facade).getOnAirLives(eqValue(member), eqValue(2), eqValue(Lang.KO)) } private fun createResponse() = HomeOnAirLivePageResponse( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacadeTest.kt index db138bec..1d46b041 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/live/application/HomeOnAirLiveFacadeTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.home.live.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole @@ -16,7 +17,11 @@ import java.time.LocalDateTime class HomeOnAirLiveFacadeTest { private val queryService = Mockito.mock(HomeRecommendationQueryService::class.java) private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) - private val facade = HomeOnAirLiveFacade(queryService, preferenceService, "https://cdn.test") + private val facade = HomeOnAirLiveFacade( + queryService, + preferenceService, + "https://cdn.test" + ) @Test @DisplayName("현재 진행 중인 라이브 facade는 20개 고정 page와 hasNext를 조립한다") @@ -29,10 +34,11 @@ class HomeOnAirLiveFacadeTest { eqValue(member.id), eqValue(true), eqValue(false), - eqValue(Gender.NONE) + eqValue(Gender.NONE), + eqValue(Lang.KO) ) - val response = facade.getOnAirLives(member, page = 0) + val response = facade.getOnAirLives(member, page = 0, lang = Lang.KO) assertEquals(0, response.page) assertEquals(20, response.size) @@ -44,7 +50,8 @@ class HomeOnAirLiveFacadeTest { eqValue(member.id), eqValue(true), eqValue(false), - eqValue(Gender.NONE) + eqValue(Gender.NONE), + eqValue(Lang.KO) ) } @@ -59,10 +66,11 @@ class HomeOnAirLiveFacadeTest { eqValue(member.id), eqValue(false), eqValue(false), - eqValue(Gender.NONE) + eqValue(Gender.NONE), + eqValue(Lang.KO) ) - val response = facade.getOnAirLives(member, page = 0) + val response = facade.getOnAirLives(member, page = 0, lang = Lang.KO) assertEquals("https://cdn.test/profile/default-profile.png", response.items.single().creatorProfileImage) } @@ -79,10 +87,11 @@ class HomeOnAirLiveFacadeTest { eqValue(member.id), eqValue(false), eqValue(false), - eqValue(Gender.NONE) + eqValue(Gender.NONE), + eqValue(Lang.KO) ) - val response = facade.getOnAirLives(member, page = 0) + val response = facade.getOnAirLives(member, page = 0, lang = Lang.KO) assertEquals("2026-06-26T12:30:00Z", response.items.single().beginDateTimeUtc) } @@ -98,10 +107,11 @@ class HomeOnAirLiveFacadeTest { eqValue(member.id), eqValue(true), eqValue(true), - eqValue(Gender.MALE) + eqValue(Gender.MALE), + eqValue(Lang.KO) ) - facade.getOnAirLives(member, page = 0) + facade.getOnAirLives(member, page = 0, lang = Lang.KO) Mockito.verify(queryService).findLiveRecommendations( eqValue(0L), @@ -109,7 +119,8 @@ class HomeOnAirLiveFacadeTest { eqValue(member.id), eqValue(true), eqValue(true), - eqValue(Gender.MALE) + eqValue(Gender.MALE), + eqValue(Lang.KO) ) } @@ -118,6 +129,7 @@ class HomeOnAirLiveFacadeTest { creatorProfileImage: String? = "profile.png", beginDateTime: LocalDateTime = LocalDateTime.of(2026, 6, 26, 12, 30) ) = HomeLiveRecommendationRecord( + creatorMemberId = id + 100L, liveRoomId = id, creatorNickname = "creator-$id", creatorProfileImage = creatorProfileImage, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListControllerTest.kt index 6af6d61f..6926e865 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListControllerTest.kt @@ -1,5 +1,7 @@ package kr.co.vividnext.sodalive.v2.chat +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.v2.chat.controller.ChatRoomListController import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse @@ -21,7 +23,7 @@ class ChatRoomListControllerTest { @BeforeEach fun setUp() { service = Mockito.mock(ChatRoomListService::class.java) - controller = ChatRoomListController(service) + controller = ChatRoomListController(service, LangContext()) } @Test @@ -45,12 +47,12 @@ class ChatRoomListControllerTest { hasMore = false, nextCursor = null ) - Mockito.`when`(service.getRooms(member, "DM", "cursor", 10)).thenReturn(serviceResponse) + Mockito.`when`(service.getRooms(member, Lang.KO, "DM", "cursor", 10)).thenReturn(serviceResponse) val response = controller.getRooms(member = member, filter = "DM", cursor = "cursor", limit = 10) assertTrue(response.success) assertEquals(serviceResponse, response.data) - Mockito.verify(service).getRooms(member, "DM", "cursor", 10) + Mockito.verify(service).getRooms(member, Lang.KO, "DM", "cursor", 10) } } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListServiceTest.kt index f2547819..a240923f 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListServiceTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.chat import kr.co.vividnext.sodalive.chat.room.ChatMessageType import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService @@ -55,7 +56,7 @@ class ChatRoomListServiceTest { ) ) Mockito.`when`( - dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31)) + dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, Lang.KO, PageRequest.of(0, 31)) ).thenReturn( listOf( ChatRoomListQueryDto( @@ -70,7 +71,7 @@ class ChatRoomListServiceTest { ) ) - val response = service.getRooms(member, filter = "ALL", cursor = null, limit = 30) + val response = service.getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 30) assertFalse(response.hasMore) assertEquals(listOf("DM", "AI"), response.rooms.map { it.chatType }) @@ -85,7 +86,7 @@ class ChatRoomListServiceTest { fun shouldReturnOnlyDmRoomsWithVoicePreview() { val member = member(1L) Mockito.`when`( - dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31)) + dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, Lang.KO, PageRequest.of(0, 31)) ).thenReturn( listOf( ChatRoomListQueryDto( @@ -100,7 +101,7 @@ class ChatRoomListServiceTest { ) ) - val response = service.getRooms(member, filter = "DM", cursor = null, limit = 30) + val response = service.getRooms(member, Lang.KO, filter = "DM", cursor = null, limit = 30) assertEquals(1, response.rooms.size) assertEquals("DM", response.rooms[0].chatType) @@ -128,7 +129,7 @@ class ChatRoomListServiceTest { aiRoomRepository.findAiChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31)) ).thenReturn(rows) - val response = service.getRooms(member, filter = "AI", cursor = null, limit = 30) + val response = service.getRooms(member, Lang.KO, filter = "AI", cursor = null, limit = 30) assertEquals(30, response.rooms.size) assertTrue(response.hasMore) @@ -157,7 +158,7 @@ class ChatRoomListServiceTest { ) ) Mockito.`when`( - dmRoomRepository.findDmChatListRooms(1L, cursorAt, "DM", 20L, false, PageRequest.of(0, 31)) + dmRoomRepository.findDmChatListRooms(1L, cursorAt, "DM", 20L, false, Lang.KO, PageRequest.of(0, 31)) ).thenReturn( listOf( ChatRoomListQueryDto( @@ -181,7 +182,7 @@ class ChatRoomListServiceTest { ) ) - val response = service.getRooms(member, filter = "ALL", cursor = "2026-05-14T12:00:00:DM:20", limit = 30) + val response = service.getRooms(member, Lang.KO, filter = "ALL", cursor = "2026-05-14T12:00:00:DM:20", limit = 30) assertEquals(listOf(30L, 10L), response.rooms.map { it.roomId }) assertEquals("https://cdn.test/profile/default-profile.png", response.rooms[0].targetImageUrl) @@ -194,13 +195,13 @@ class ChatRoomListServiceTest { val pageable = PageRequest.of(0, 11) Mockito.`when`(aiRoomRepository.findAiChatListRooms(1L, null, null, null, true, pageable)) .thenReturn(emptyList()) - Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, true, pageable)) + Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, true, Lang.KO, pageable)) .thenReturn(emptyList()) - service.getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true) + service.getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true) Mockito.verify(aiRoomRepository).findAiChatListRooms(1L, null, null, null, true, pageable) - Mockito.verify(dmRoomRepository).findDmChatListRooms(1L, null, null, null, true, pageable) + Mockito.verify(dmRoomRepository).findDmChatListRooms(1L, null, null, null, true, Lang.KO, pageable) } private fun member(id: Long) = Member(password = "pw", nickname = "user").apply { this.id = id } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryServiceTest.kt index f8691bb0..2fdbdaa1 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/all/application/MainContentAllQueryServiceTest.kt @@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.v2.content.all.application import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek 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.MemberRole import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService @@ -27,7 +26,15 @@ class MainContentAllQueryServiceTest { val port = FakeMainContentAllQueryPort() val service = createService(port) - val tab = service.getContents(type = "AUDIO", sort = "POPULAR", dayOfWeek = "MON", page = 1, size = 20, member = null) + val tab = service.getContents( + type = "AUDIO", + sort = "POPULAR", + dayOfWeek = "MON", + page = 1, + size = 20, + member = null, + lang = Lang.EN + ) assertEquals(MainContentAllType.AUDIO, tab.type) assertEquals("audio", port.lastListKind) @@ -50,7 +57,15 @@ class MainContentAllQueryServiceTest { val port = FakeMainContentAllQueryPort() val service = createService(port) - val tab = service.getContents(type = "FREE", sort = "LATEST", dayOfWeek = null, page = 0, size = 20, member = null) + val tab = service.getContents( + type = "FREE", + sort = "LATEST", + dayOfWeek = null, + page = 0, + size = 20, + member = null, + lang = Lang.EN + ) assertEquals(MainContentAllType.FREE, tab.type) assertEquals("audio", port.lastListKind) @@ -69,7 +84,15 @@ class MainContentAllQueryServiceTest { val port = FakeMainContentAllQueryPort() val service = createService(port) - val tab = service.getContents(type = "POINT", sort = "PRICE_LOW", dayOfWeek = null, page = 0, size = 20, member = null) + val tab = service.getContents( + type = "POINT", + sort = "PRICE_LOW", + dayOfWeek = null, + page = 0, + size = 20, + member = null, + lang = Lang.EN + ) assertEquals(MainContentAllType.POINT, tab.type) assertEquals("audio", port.lastListKind) @@ -87,9 +110,9 @@ class MainContentAllQueryServiceTest { @DisplayName("SERIES는 요일을 전달하고 ORIGINAL은 original 필터와 dayOfWeek null을 전달한다") fun shouldQuerySeriesByType() { val seriesPort = FakeMainContentAllQueryPort() - val service = createService(seriesPort, lang = Lang.JA) + val service = createService(seriesPort) - val seriesTab = service.getContents("SERIES", "POPULAR", "MON", 0, 20, null) + val seriesTab = service.getContents("SERIES", "POPULAR", "MON", 0, 20, null, Lang.JA) assertEquals(MainContentAllType.SERIES, seriesTab.type) assertEquals("series", seriesPort.lastListKind) @@ -100,7 +123,7 @@ class MainContentAllQueryServiceTest { val originalPort = FakeMainContentAllQueryPort() val originalService = createService(originalPort) - val originalTab = originalService.getContents("ORIGINAL", "POPULAR", "MON", 0, 20, null) + val originalTab = originalService.getContents("ORIGINAL", "POPULAR", "MON", 0, 20, null, Lang.EN) assertEquals(MainContentAllType.ORIGINAL, originalTab.type) assertEquals("series", originalPort.lastListKind) @@ -112,7 +135,7 @@ class MainContentAllQueryServiceTest { @DisplayName("비회원은 성인 콘텐츠 비노출로 조회하고 회원은 preference 결과를 전달한다") fun shouldPassAdultVisibilityByMember() { val anonymousPort = FakeMainContentAllQueryPort() - createService(anonymousPort).getContents("AUDIO", null, null, null, null, null) + createService(anonymousPort).getContents("AUDIO", null, null, null, null, null, Lang.EN) assertEquals(null, anonymousPort.lastMemberId) assertFalse(anonymousPort.lastCanViewAdultContent) @@ -127,7 +150,7 @@ class MainContentAllQueryServiceTest { val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member) - createService(memberPort, preferenceService).getContents("AUDIO", null, null, null, null, member) + createService(memberPort, preferenceService).getContents("AUDIO", null, null, null, null, member, Lang.EN) assertEquals(10L, memberPort.lastMemberId) assertTrue(memberPort.lastCanViewAdultContent) @@ -136,16 +159,12 @@ class MainContentAllQueryServiceTest { private fun createService( port: MainContentAllQueryPort, - preferenceService: MemberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java), - lang: Lang = Lang.EN + preferenceService: MemberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java) ): MainContentAllQueryService { - val langContext = LangContext() - langContext.setLang(lang) return MainContentAllQueryService( queryPort = port, memberContentPreferenceService = preferenceService, - queryPolicy = MainContentAllQueryPolicy(), - langContext = langContext + queryPolicy = MainContentAllQueryPolicy() ) } } @@ -204,6 +223,7 @@ private class FakeMainContentAllQueryPort : MainContentAllQueryPort { lastOnlyPointAvailable = onlyPointAvailable return (1L..limit.toLong()).map { id -> MainContentAllAudio( + creatorMemberId = 10L, audioContentId = id, title = "audio-$id", imageUrl = null, @@ -253,6 +273,7 @@ private class FakeMainContentAllQueryPort : MainContentAllQueryPort { lastLocale = locale return listOf( MainContentAllSeries( + creatorMemberId = 10L, seriesId = 1L, title = "series", coverImageUrl = null, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapterTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapterTest.kt index d5f4f013..98ffc534 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapterTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapterTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence +import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.configs.QueryDslConfig import kr.co.vividnext.sodalive.content.translation.ContentTranslation import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload @@ -31,7 +32,11 @@ class DefaultAudioRankingSnapshotPersistenceAdapterTest @Autowired constructor( private val entityManager: EntityManager, private val entityManagerFactory: EntityManagerFactory ) { - private val adapter = DefaultAudioRankingSnapshotPersistenceAdapter(repository, contentTranslationRepository) + private val adapter = DefaultAudioRankingSnapshotPersistenceAdapter( + repository, + contentTranslationRepository, + JPAQueryFactory(entityManager) + ) @Test @DisplayName("SQL NULL 번역 payload는 snapshot 제목으로 fallback한다") diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryServiceTest.kt index 6ee488fe..186ae6ad 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryServiceTest.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.v2.content.ranking.application 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.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType @@ -35,14 +34,11 @@ class AudioRankingQueryServiceTest { "en" to originalLatestSnapshots.map { it.copy(title = "en-title-${it.contentId}") }, "ja" to originalLatestSnapshots.map { it.copy(title = "ja-title-${it.contentId}") } ) - val langContext = LangContext().apply { setLang(Lang.EN) } - val service = service(snapshotPort, langContext = langContext) + val service = service(snapshotPort) - val english = service.getRankings(AudioRankingType.REVENUE, member = null) - langContext.setLang(Lang.JA) - val japanese = service.getRankings(AudioRankingType.REVENUE, member = null) - langContext.setLang(Lang.EN) - val englishAgain = service.getRankings(AudioRankingType.REVENUE, member = null) + val english = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.EN) + val japanese = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.JA) + val englishAgain = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.EN) assertEquals(listOf("en-title-2", "en-title-1"), english.items.map { it.title }) assertEquals(listOf("ja-title-2", "ja-title-1"), japanese.items.map { it.title }) @@ -64,7 +60,8 @@ class AudioRankingQueryServiceTest { val method = AudioRankingQueryService::class.java.getDeclaredMethod( "getRankings", AudioRankingType::class.java, - Member::class.java + Member::class.java, + Lang::class.java ) assertEquals(null, method.getAnnotation(Transactional::class.java)) @@ -84,7 +81,7 @@ class AudioRankingQueryServiceTest { ) val service = service(snapshotPort) - val result = service.getRankings(AudioRankingType.REVENUE, member = null) + val result = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.KO) assertTrue(result.showRankChange) assertEquals(AudioRankingType.REVENUE, result.type) @@ -106,7 +103,7 @@ class AudioRankingQueryServiceTest { snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1)) val service = service(snapshotPort) - val result = service.getRankings(AudioRankingType.WEEKLY_POPULAR, member = null) + val result = service.getRankings(AudioRankingType.WEEKLY_POPULAR, member = null, lang = Lang.KO) assertFalse(result.showRankChange) assertEquals(listOf(1L), result.items.map { it.contentId }) @@ -116,7 +113,11 @@ class AudioRankingQueryServiceTest { @Test fun shouldReturnEmptyRankingWhenLatestSnapshotDoesNotExist() { - val result = service(FakeAudioRankingQuerySnapshotPort()).getRankings(AudioRankingType.LIKE_COUNT, member = null) + val result = service(FakeAudioRankingQuerySnapshotPort()).getRankings( + AudioRankingType.LIKE_COUNT, + member = null, + lang = Lang.KO + ) assertFalse(result.showRankChange) assertEquals(AudioRankingType.LIKE_COUNT, result.type) @@ -136,7 +137,7 @@ class AudioRankingQueryServiceTest { snapshot(contentId = 2L, rank = 2) ) - val result = service(snapshotPort).getRankings(AudioRankingType.REVENUE, member = null) + val result = service(snapshotPort).getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.KO) assertEquals(listOf(2L, 3L), result.items.map { it.contentId }) assertEquals(listOf(1, 2), result.items.map { it.rank }) @@ -150,7 +151,7 @@ class AudioRankingQueryServiceTest { val member = Mockito.mock(Member::class.java) snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1, isAdult = true)) - val result = service(snapshotPort, adultMember = member).getRankings(AudioRankingType.REVENUE, member) + val result = service(snapshotPort, adultMember = member).getRankings(AudioRankingType.REVENUE, member, Lang.KO) assertEquals(listOf(1L), result.items.map { it.contentId }) } @@ -170,7 +171,7 @@ class AudioRankingQueryServiceTest { snapshot(contentId = 1L, rank = 2, creatorMemberId = 101L) ) - val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member) + val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member, Lang.KO) assertEquals(listOf(1L, 3L), result.items.map { it.contentId }) assertEquals(listOf(1, 2), result.items.map { it.rank }) @@ -185,7 +186,11 @@ class AudioRankingQueryServiceTest { val blockPort = FakeAudioRankingBlockPort(blockedCreatorMemberIds = setOf(101L)) snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1, creatorMemberId = 101L)) - val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member = null) + val result = service(snapshotPort, blockPort = blockPort).getRankings( + AudioRankingType.REVENUE, + member = null, + lang = Lang.KO + ) assertEquals(listOf(1L), result.items.map { it.contentId }) assertEquals(0, blockPort.callCount) @@ -200,7 +205,11 @@ class AudioRankingQueryServiceTest { listOf(snapshot(contentId = 1L, rank = 1)) ) - val result = service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.LIKE_COUNT, member = null) + val result = service(snapshotPort, jobService = jobService).getRankings( + AudioRankingType.LIKE_COUNT, + member = null, + lang = Lang.KO + ) assertEquals(listOf(1L), result.items.map { it.contentId }) assertEquals(2, snapshotPort.latestCallCount) @@ -214,7 +223,11 @@ class AudioRankingQueryServiceTest { Mockito.doThrow(IllegalStateException("aggregate failed")) .`when`(jobService).refreshLastCompletedWeekByFallback(AudioRankingType.LIKE_COUNT) - val result = service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.LIKE_COUNT, member = null) + val result = service(snapshotPort, jobService = jobService).getRankings( + AudioRankingType.LIKE_COUNT, + member = null, + lang = Lang.KO + ) assertFalse(result.showRankChange) assertEquals(AudioRankingType.LIKE_COUNT, result.type) @@ -230,7 +243,7 @@ class AudioRankingQueryServiceTest { val jobService = Mockito.mock(AudioRankingSnapshotJobService::class.java) snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1)) - service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.REVENUE, member = null) + service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.KO) Mockito.verify(jobService, Mockito.never()).refreshLastCompletedWeekByFallback(AudioRankingType.REVENUE) } @@ -250,7 +263,7 @@ class AudioRankingQueryServiceTest { snapshot(contentId = 2L, rank = 3, creatorMemberId = 102L) ) - val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member) + val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member, Lang.KO) assertEquals(setOf(101L, 102L, 999L), blockPort.creatorMemberIds) assertEquals(listOf(0, 0), result.items.map { it.rankChange }) @@ -261,8 +274,7 @@ class AudioRankingQueryServiceTest { snapshotPort: FakeAudioRankingQuerySnapshotPort, adultMember: Member? = null, blockPort: AudioRankingBlockPort = FakeAudioRankingBlockPort(), - jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java), - langContext: LangContext = LangContext() + jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java) ): AudioRankingQueryService { val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java) if (adultMember != null) { @@ -270,7 +282,6 @@ class AudioRankingQueryServiceTest { } return AudioRankingQueryService( snapshotPort = snapshotPort, - langContext = langContext, memberContentPreferenceService = memberContentPreferenceService, blockPort = blockPort, jobService = jobService, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt index e333b67d..5f773f85 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.v2.content.recommendation.application 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.MemberRole import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService @@ -20,12 +19,10 @@ class AudioRecommendationQueryServiceTest { private val queryPort = Mockito.mock(AudioRecommendationQueryPort::class.java) private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) private val fallbackService = Mockito.mock(AudioRecommendationSnapshotFallbackService::class.java) - private val langContext = LangContext().apply { setLang(Lang.EN) } private val service = AudioRecommendationQueryService( queryPort, preferenceService, - fallbackService, - langContext + fallbackService ) @Test @@ -63,7 +60,7 @@ class AudioRecommendationQueryServiceTest { emptyList() ) - val recommendations = service.getRecommendations(null) + val recommendations = service.getRecommendations(null, Lang.EN) assertEquals(0, recommendations.mostCommentedAudios.size) verifyFallback( @@ -134,7 +131,7 @@ class AudioRecommendationQueryServiceTest { listOf(snapshot(RecommendedSectionType.RECOMMENDED_AUDIO_SAFE, 2L)) ) - service.getRecommendations(null) + service.getRecommendations(null, Lang.EN) verifyFallback( RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE, @@ -156,7 +153,7 @@ class AudioRecommendationQueryServiceTest { listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L)) ) - service.getRecommendations(member) + service.getRecommendations(member, Lang.EN) Mockito.verify(preferenceService).canViewAdultContent(member) Mockito.verify(queryPort).findBanners(AudioRecommendationQueryService.BANNER_LIMIT, member.id, true, Lang.JA) @@ -180,7 +177,7 @@ class AudioRecommendationQueryServiceTest { listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L)) ) - service.getRecommendations(member) + service.getRecommendations(member, Lang.EN) verifyFallback( RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, @@ -209,7 +206,7 @@ class AudioRecommendationQueryServiceTest { eqValue(Lang.EN.code) ) - val result = service.findNewAndHotAudios(member, offset = 20L, limit = 21) + val result = service.findNewAndHotAudios(member, offset = 20L, limit = 21, lang = Lang.EN) assertEquals(listOf(3L, 4L, 5L), result.map { it.audioContentId }) verifyFallback(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21) @@ -261,6 +258,7 @@ class AudioRecommendationQueryServiceTest { private fun audioCard(id: Long): AudioCard { return AudioCard( + creatorMemberId = id + 100L, audioContentId = id, title = "audio$id", duration = "00:01", diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt index 13753396..f69d32e8 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt @@ -14,6 +14,7 @@ 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.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.block.BlockMember @@ -62,7 +63,7 @@ class DefaultCreatorChannelAudioQueryRepositoryTest @Autowired constructor( saveBlock(creator, viewer) flushAndClear() - val record = repository.findCreator(creator.id!!, viewer.id!!) + val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO) val themes = repository.findAudioThemes(creator.id!!, now, canViewAdultContent = false, "en") assertEquals(creator.id, record!!.creatorId) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt index 6ad0f695..c053f495 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt @@ -45,7 +45,8 @@ class CreatorChannelAudioQueryServiceTest { themeId = 999L, page = -1, size = 100, - now = now + now = now, + lang = Lang.EN ) assertEquals(ContentSort.LATEST, tab.sort) @@ -78,7 +79,7 @@ class CreatorChannelAudioQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L) - val tab = service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0)) + val tab = service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN) assertEquals(0.0, tab.purchasedAudioContentRate) } @@ -91,7 +92,7 @@ class CreatorChannelAudioQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0)) + service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN) } assertEquals("member.validation.user_not_found", exception.messageKey) @@ -105,7 +106,7 @@ class CreatorChannelAudioQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0)) + service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN) } assertEquals("member.validation.creator_not_found", exception.messageKey) @@ -119,7 +120,7 @@ class CreatorChannelAudioQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0)) + service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN) } assertNull(exception.messageKey) @@ -191,7 +192,7 @@ private class FakeCreatorChannelAudioQueryPort : CreatorChannelAudioQueryPort { var themeCanViewAdultContent: Boolean? = null var themeLocale: String? = null - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelAudioCreatorRecord? = creator + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelAudioCreatorRecord? = creator override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt index 33ab5835..5f4486dc 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.configs.QueryDslConfig import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberKind import kr.co.vividnext.sodalive.member.MemberRole @@ -46,8 +47,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( val inactiveCreator = saveMember("inactive-creator", MemberRole.CREATOR, isActive = false) flushAndClear() - val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!) - val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!) + val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!, Lang.KO) + val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO) assertNotNull(activeRecord) assertEquals(activeCreator.id, activeRecord!!.creatorId) @@ -92,7 +93,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = viewer.id!!, canViewAdultContent = false, offset = 0, - limit = 10 + limit = 10, + lang = Lang.KO ) val visiblePostIds = visiblePosts.map { it.postId } @@ -125,14 +127,16 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = viewer.id!!, canViewAdultContent = true, offset = 0, - limit = 3 + limit = 3, + lang = Lang.KO ) val secondPage: List = repository.findCommunityPosts( creatorId = creator.id!!, viewerId = viewer.id!!, canViewAdultContent = true, offset = 3, - limit = 2 + limit = 2, + lang = Lang.KO ) assertEquals( @@ -168,7 +172,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = viewer.id!!, canViewAdultContent = true, offset = 0, - limit = 10 + limit = 10, + lang = Lang.KO ) val record = records.single { it.postId == post.id } val otherMemberOnlyLikedRecord = records.single { it.postId == otherMemberOnlyLikedPost.id } @@ -203,21 +208,24 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = viewer.id!!, canViewAdultContent = true, offset = 0, - limit = 10 + limit = 10, + lang = Lang.KO ).single() val writerRecord = repository.findCommunityPosts( creatorId = creator.id!!, viewerId = secretWriter.id!!, canViewAdultContent = true, offset = 0, - limit = 10 + limit = 10, + lang = Lang.KO ).single() val creatorRecord = repository.findCommunityPosts( creatorId = creator.id!!, viewerId = creator.id!!, canViewAdultContent = true, offset = 0, - limit = 10 + limit = 10, + lang = Lang.KO ).single() assertEquals(1, viewerRecord.commentCount) @@ -247,7 +255,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = viewer.id!!, canViewAdultContent = true, offset = 0, - limit = 10 + limit = 10, + lang = Lang.KO ) val recordsById = records.associateBy { it.postId } @@ -271,7 +280,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = creator.id!!, canViewAdultContent = true, offset = 0, - limit = 10 + limit = 10, + lang = Lang.KO ).single() assertTrue(record.existOrdered) @@ -297,14 +307,16 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = viewer.id!!, isPinned = true, canViewAdultContent = false, - limit = 3 + limit = 3, + lang = Lang.KO ) val normalPosts: List = repository.findHomeCommunityPosts( creatorId = creator.id!!, viewerId = viewer.id!!, isPinned = false, canViewAdultContent = false, - limit = 3 + limit = 3, + lang = Lang.KO ) assertEquals(listOf(pinned.id), pinnedPosts.map { it.postId }) @@ -337,14 +349,16 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( viewerId = viewer.id!!, isPinned = false, canViewAdultContent = true, - limit = 10 + limit = 10, + lang = Lang.KO ) val adultBlockedPosts: List = repository.findHomeCommunityPosts( creatorId = creator.id!!, viewerId = viewer.id!!, isPinned = false, canViewAdultContent = false, - limit = 10 + limit = 10, + lang = Lang.KO ) assertTrue(inactivePurchasedPost.id in adultAllowedPosts.map { it.postId }) @@ -368,9 +382,19 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( saveCommunityComment(viewer, paidPost, isActive = true) flushAndClear() - val record = repository.findCommunityPost(paidPost.id!!, viewer.id!!, canViewAdultContent = true) - val blockedAdultRecord = repository.findCommunityPost(adultPost.id!!, viewer.id!!, canViewAdultContent = false) - val inactiveRecord = repository.findCommunityPost(inactivePost.id!!, viewer.id!!, canViewAdultContent = true) + val record = repository.findCommunityPost(paidPost.id!!, viewer.id!!, canViewAdultContent = true, lang = Lang.KO) + val blockedAdultRecord = repository.findCommunityPost( + adultPost.id!!, + viewer.id!!, + canViewAdultContent = false, + lang = Lang.KO + ) + val inactiveRecord = repository.findCommunityPost( + inactivePost.id!!, + viewer.id!!, + canViewAdultContent = true, + lang = Lang.KO + ) assertNotNull(record) assertEquals(paidPost.id, record!!.postId) @@ -412,8 +436,15 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( val viewerCount = repository.countCommunityComments(post.id!!, viewer.id!!, creator.id!!) val creatorCount = repository.countCommunityComments(post.id!!, creator.id!!, creator.id!!) - val comments = repository.findCommunityComments(post.id!!, viewer.id!!, creator.id!!, offset = 0, limit = 10) - val latestReplies = repository.findLatestRepliesByCommentIds(comments.map { it.commentId }, viewer.id!!) + val comments = repository.findCommunityComments( + post.id!!, + viewer.id!!, + creator.id!!, + offset = 0, + limit = 10, + lang = Lang.KO + ) + val latestReplies = repository.findLatestRepliesByCommentIds(comments.map { it.commentId }, viewer.id!!, Lang.KO) assertEquals(2, viewerCount) assertEquals(4, creatorCount) @@ -448,7 +479,7 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor( val invisibleSecretContext = repository.findRootCommentContext(secretRoot.id!!, viewer.id!!, canViewAdultContent = true) val replyContext = repository.findRootCommentContext(newReply.id!!, viewer.id!!, canViewAdultContent = true) val count = repository.countCommunityReplies(root.id!!, viewer.id!!) - val replies = repository.findCommunityReplies(root.id!!, viewer.id!!, offset = 0, limit = 10) + val replies = repository.findCommunityReplies(root.id!!, viewer.id!!, offset = 0, limit = 10, lang = Lang.KO) assertNotNull(context) assertEquals(post.id, context!!.postId) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt index 2a6b8dc9..0229172d 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt @@ -47,7 +47,8 @@ class CreatorChannelCommunityQueryServiceTest { viewer = viewer, page = -1, size = 100, - now = now + now = now, + lang = Lang.EN ) assertEquals(60, tab.communityPostCount) @@ -72,7 +73,7 @@ class CreatorChannelCommunityQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) } assertEquals("member.validation.user_not_found", exception.messageKey) @@ -86,7 +87,7 @@ class CreatorChannelCommunityQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) } assertEquals("member.validation.creator_not_found", exception.messageKey) @@ -100,7 +101,7 @@ class CreatorChannelCommunityQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) } assertNull(exception.messageKey) @@ -129,7 +130,7 @@ class CreatorChannelCommunityQueryServiceTest { val service = createService(port, audioContentCloudFront) val viewer = createMember(id = 10L) - val posts = service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + val posts = service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) .communityPosts assertEquals("https://cdn.test/image/1.png", posts[0].imageUrl) @@ -165,7 +166,8 @@ class CreatorChannelCommunityQueryServiceTest { viewerId = 10L, isPinned = true, canViewAdultContent = false, - limit = 3 + limit = 3, + lang = Lang.KO ) assertEquals(1, posts.size) @@ -203,7 +205,7 @@ class CreatorChannelCommunityQueryServiceTest { ) ) - val posts = service.getCommunityTab(1L, createMember(id = 10L), 0, 20).communityPosts + val posts = service.getCommunityTab(1L, createMember(id = 10L), 0, 20, lang = Lang.EN).communityPosts assertEquals( listOf( @@ -230,7 +232,7 @@ class CreatorChannelCommunityQueryServiceTest { val returnedPostIds = (1L..50L).toList() Mockito.`when`(translationService.findDisplayContents(returnedPostIds, "en")).thenReturn(emptyMap()) - val tab = service.getCommunityTab(1L, createMember(id = 10L), page = 0, size = 50) + val tab = service.getCommunityTab(1L, createMember(id = 10L), page = 0, size = 50, lang = Lang.EN) assertEquals(50, tab.communityPosts.size) assertTrue(tab.hasNext) @@ -248,13 +250,13 @@ class CreatorChannelCommunityQueryServiceTest { Mockito.`when`(translationService.findDisplayContents(listOf(21L), "en")) .thenReturn(mapOf(21L to CreatorCommunityDisplayContent("translated pinned", true))) - val pinnedPosts = service.findHomeCommunityPosts(1L, 10L, true, false, 3) + val pinnedPosts = service.findHomeCommunityPosts(1L, 10L, true, false, 3, Lang.EN) port.homeCommunityPosts = listOf(communityPostRecord(22L, price = 0).copy(content = "source normal")) Mockito.`when`(translationService.findDisplayContents(listOf(22L), "en")) .thenReturn(mapOf(22L to CreatorCommunityDisplayContent("translated normal", true))) - val normalPosts = service.findHomeCommunityPosts(1L, 10L, false, false, 3) + val normalPosts = service.findHomeCommunityPosts(1L, 10L, false, false, 3, Lang.EN) assertEquals("translated pinned", pinnedPosts.single().content) assertEquals("translated normal", normalPosts.single().content) @@ -275,7 +277,7 @@ class CreatorChannelCommunityQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L) - val detail = service.getCommunityPostDetail(1L, viewer, LocalDateTime.of(2026, 7, 6, 10, 0)) + val detail = service.getCommunityPostDetail(1L, viewer, LocalDateTime.of(2026, 7, 6, 10, 0), Lang.KO) assertEquals(1L, detail.post.postId) assertEquals(true, detail.post.isLiked) @@ -298,7 +300,7 @@ class CreatorChannelCommunityQueryServiceTest { Mockito.`when`(translationService.findDisplayContents(listOf(1L), "en")) .thenReturn(mapOf(1L to CreatorCommunityDisplayContent("content-1", true))) - service.getCommunityPostDetail(1L, createMember(id = 10L), LocalDateTime.of(2026, 7, 6, 10, 0)) + service.getCommunityPostDetail(1L, createMember(id = 10L), LocalDateTime.of(2026, 7, 6, 10, 0), Lang.KO) Mockito.verify(translationService, Mockito.never()).requestTranslations(1L, "en") } @@ -311,7 +313,14 @@ class CreatorChannelCommunityQueryServiceTest { } val service = createService(port) - val comments = service.getCommunityComments(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0)) + val comments = service.getCommunityComments( + 1L, + createMember(id = 10L), + -1, + 100, + LocalDateTime.of(2026, 7, 6, 10, 0), + Lang.KO + ) assertEquals(0, comments.commentCount) assertEquals(0, comments.comments.size) @@ -332,7 +341,14 @@ class CreatorChannelCommunityQueryServiceTest { } val service = createService(port) - val comments = service.getCommunityComments(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0)) + val comments = service.getCommunityComments( + 1L, + createMember(id = 10L), + -1, + 100, + LocalDateTime.of(2026, 7, 6, 10, 0), + Lang.KO + ) assertEquals(0L, port.commentListOffset) assertEquals(51, port.commentListLimit) @@ -356,7 +372,14 @@ class CreatorChannelCommunityQueryServiceTest { } val service = createService(port) - val replies = service.getCommunityReplies(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0)) + val replies = service.getCommunityReplies( + 1L, + createMember(id = 10L), + -1, + 100, + LocalDateTime.of(2026, 7, 6, 10, 0), + Lang.KO + ) assertEquals(51, replies.replyCount) assertEquals(50, replies.replies.size) @@ -373,7 +396,7 @@ class CreatorChannelCommunityQueryServiceTest { val service = createService(port) val exception = assertThrows(SodaException::class.java) { - service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0)) + service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0), Lang.KO) } assertEquals("creator.community.invalid_request_retry", exception.messageKey) @@ -389,7 +412,14 @@ class CreatorChannelCommunityQueryServiceTest { } val service = createService(port) - val replies = service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0)) + val replies = service.getCommunityReplies( + 1L, + createMember(id = 10L), + null, + null, + LocalDateTime.of(2026, 7, 6, 10, 0), + Lang.KO + ) assertEquals(0, replies.replyCount) assertEquals(0, replies.replies.size) @@ -484,7 +514,7 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer var homeCanViewAdultContent: Boolean? = null var homeLimit: Int? = null - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord? = creator + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCommunityCreatorRecord? = creator override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked @@ -502,7 +532,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer viewerId: Long, canViewAdultContent: Boolean, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { listCanViewAdultContent = canViewAdultContent listOffset = offset @@ -515,7 +546,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer viewerId: Long, isPinned: Boolean, canViewAdultContent: Boolean, - limit: Int + limit: Int, + lang: Lang ): List { homeCreatorId = creatorId homeViewerId = viewerId @@ -528,7 +560,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer override fun findCommunityPost( postId: Long, viewerId: Long, - canViewAdultContent: Boolean + canViewAdultContent: Boolean, + lang: Lang ): CreatorChannelCommunityPostRecord? = communityPost override fun countCommunityComments(postId: Long, viewerId: Long, creatorId: Long): Int = communityCommentCount @@ -538,7 +571,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer viewerId: Long, creatorId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { commentListCalled = true commentListOffset = offset @@ -548,7 +582,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer override fun findLatestRepliesByCommentIds( commentIds: List, - viewerId: Long + viewerId: Long, + lang: Lang ): List { latestReplyCommentIds = commentIds return latestReplies @@ -566,7 +601,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer commentId: Long, viewerId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { replyListOffset = offset replyListLimit = limit diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepositoryTest.kt index eaa5b531..adc88c78 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepositoryTest.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persist import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.configs.QueryDslConfig import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.DonationRankingPeriod import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole @@ -48,9 +49,9 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor( val nonCreator = saveMember("donation-non-creator", MemberRole.USER) flushAndClear() - val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!) - val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!) - val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!) + val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO) + val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO) + val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!, Lang.KO) assertNotNull(creatorRecord) assertEquals(creator.id, creatorRecord!!.creatorId) @@ -91,7 +92,7 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor( flushAndClear() val count = repository.countChannelDonations(creator.id!!, creator.id!!, now) - val records = repository.findChannelDonations(creator.id!!, creator.id!!, now, offset = 0, limit = 10) + val records = repository.findChannelDonations(creator.id!!, creator.id!!, now, offset = 0, limit = 10, lang = Lang.KO) assertEquals(2, count) assertEquals(listOf(secret.can, monthStart.can), records.map { it.can }) @@ -121,7 +122,7 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor( flushAndClear() val count = repository.countChannelDonations(creator.id!!, viewer.id!!, now) - val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 0, limit = 10) + val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 0, limit = 10, lang = Lang.KO) assertEquals(2, count) assertEquals(listOf(ownSecretDonation.can, publicDonation.can), records.map { it.can }) @@ -141,7 +142,7 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor( saveDonation(creator, donor, 300, sameCreatedAt.plusMinutes(1), additionalMessage = "newest") flushAndClear() - val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 1, limit = 2) + val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 1, limit = 2, lang = Lang.KO) assertEquals(listOf(second.can, first.can), records.map { it.can }) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt index a610c647..b7807554 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt @@ -1,7 +1,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.donation.application import kr.co.vividnext.sodalive.common.SodaException -import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.member.DonationRankingPeriod import kr.co.vividnext.sodalive.member.Member @@ -34,7 +34,8 @@ class CreatorChannelDonationQueryServiceTest { viewer = createMember(VIEWER_ID), page = 0, size = 20, - now = NOW + now = NOW, + lang = Lang.KO ) } @@ -55,7 +56,8 @@ class CreatorChannelDonationQueryServiceTest { viewer = createMember(VIEWER_ID), page = 0, size = 20, - now = NOW + now = NOW, + lang = Lang.KO ) } @@ -74,7 +76,8 @@ class CreatorChannelDonationQueryServiceTest { viewer = createMember(VIEWER_ID), page = 0, size = 20, - now = NOW + now = NOW, + lang = Lang.KO ) } @@ -97,7 +100,8 @@ class CreatorChannelDonationQueryServiceTest { viewer = createMember(VIEWER_ID), page = -1, size = 10, - now = NOW + now = NOW, + lang = Lang.KO ) assertEquals(0L, queryPort.lastFindDonationRequest?.offset) @@ -138,7 +142,8 @@ class CreatorChannelDonationQueryServiceTest { viewer = createMember(VIEWER_ID), page = 0, size = 20, - now = NOW + now = NOW, + lang = Lang.KO ) assertEquals("donor", tab.donations[0].nickname) @@ -163,7 +168,8 @@ class CreatorChannelDonationQueryServiceTest { viewer = createMember(CREATOR_ID), page = 0, size = 20, - now = NOW + now = NOW, + lang = Lang.KO ) assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.WEEKLY, true), rankingPort.requests.single()) @@ -186,7 +192,7 @@ class CreatorChannelDonationQueryServiceTest { ) ), rankingPort = weeklyRankingPort - ).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW) + ).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO) val cumulativeRankingPort = FakeDonationRankingPort() createService( @@ -197,7 +203,7 @@ class CreatorChannelDonationQueryServiceTest { ) ), rankingPort = cumulativeRankingPort - ).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW) + ).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO) assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.WEEKLY, false), weeklyRankingPort.requests.single()) assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.CUMULATIVE, false), cumulativeRankingPort.requests.single()) @@ -214,7 +220,7 @@ class CreatorChannelDonationQueryServiceTest { rankingPort = rankingPort ) - service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW) + service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO) assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.CUMULATIVE, false), rankingPort.requests.single()) } @@ -233,7 +239,7 @@ class CreatorChannelDonationQueryServiceTest { val rankingPort = FakeDonationRankingPort() val service = createService(queryPort = queryPort, rankingPort = rankingPort) - val tab = service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW) + val tab = service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO) assertEquals(emptyList(), rankingPort.requests) assertEquals(emptyList(), tab.rankings) @@ -255,7 +261,6 @@ class CreatorChannelDonationQueryServiceTest { rankingPort = rankingPort, queryPolicy = CreatorChannelDonationQueryPolicy(), messageSource = SodaMessageSource(), - langContext = LangContext(), cloudFrontHost = "https://cdn.test" ) } @@ -282,6 +287,7 @@ class CreatorChannelDonationQueryServiceTest { createdAt: LocalDateTime = NOW ): CreatorChannelDonationRecord { return CreatorChannelDonationRecord( + memberId = VIEWER_ID, nickname = nickname, profileImagePath = profileImagePath, can = can, @@ -319,7 +325,7 @@ class CreatorChannelDonationQueryServiceTest { var lastFindDonationRequest: FindDonationRequest? = null private set - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelDonationCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelDonationCreatorRecord? { return creator } @@ -337,7 +343,8 @@ class CreatorChannelDonationQueryServiceTest { viewerId: Long, now: LocalDateTime, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { lastFindDonationRequest = FindDonationRequest(creatorId, viewerId, now, offset, limit) return donations diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt index d4020312..e990c9c3 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt @@ -78,6 +78,7 @@ class CreatorChannelDonationQueryPolicyTest { donationCan = 100 ) val donation = CreatorChannelDonation( + memberId = 10L, nickname = "donor", profileImageUrl = "https://cdn.test/donor.png", can = 50, @@ -99,6 +100,7 @@ class CreatorChannelDonationQueryPolicyTest { donationRankingPeriod = DonationRankingPeriod.CUMULATIVE ) val donationRecord = CreatorChannelDonationRecord( + memberId = 10L, nickname = "donor", profileImagePath = null, can = 50, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepositoryTest.kt index 53dcda76..d5aee793 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepositoryTest.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persiste import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.configs.QueryDslConfig import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers +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 @@ -41,9 +42,9 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor( val nonCreator = saveMember("fantalk-non-creator", MemberRole.USER) flushAndClear() - val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!) - val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!) - val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!) + val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!, Lang.KO) + val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO) + val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!, Lang.KO) assertNotNull(activeRecord) assertEquals(activeCreator.id, activeRecord!!.creatorId) @@ -90,8 +91,8 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor( flushAndClear() val count = repository.countFanTalks(creator.id!!, viewer.id!!) - val firstPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 1) - val secondPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 1, limit = 2) + val firstPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 1, lang = Lang.KO) + val secondPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 1, limit = 2, lang = Lang.KO) assertEquals(2, count) assertEquals(listOf(newer.id), firstPage.map { it.fanTalkId }) @@ -114,7 +115,7 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor( val newest = saveCheers(writer, creator, "newest", isActive = true, createdAt = sameCreatedAt.plusMinutes(1)) flushAndClear() - val records = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 10) + val records = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 10, lang = Lang.KO) assertEquals(listOf(newest.id, second.id, first.id), records.map { it.fanTalkId }) } @@ -157,8 +158,8 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor( ) flushAndClear() - val replies = repository.findCreatorReplies(creator.id!!, listOf(parent.id!!)) - val emptyReplies = repository.findCreatorReplies(creator.id!!, emptyList()) + val replies = repository.findCreatorReplies(creator.id!!, listOf(parent.id!!), Lang.KO) + val emptyReplies = repository.findCreatorReplies(creator.id!!, emptyList(), Lang.KO) assertEquals(listOf(olderReply.id, newerReply.id), replies.map { it.fanTalkId }) assertEquals(parent.id, replies.first().parentFanTalkId) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryServiceTest.kt index 67ea009c..0fb01aa7 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryServiceTest.kt @@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application 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 import kr.co.vividnext.sodalive.member.MemberProvider @@ -30,7 +29,7 @@ class CreatorChannelFanTalkQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) } assertEquals("member.validation.user_not_found", exception.messageKey) @@ -44,7 +43,7 @@ class CreatorChannelFanTalkQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) } assertEquals("member.validation.creator_not_found", exception.messageKey) @@ -58,7 +57,7 @@ class CreatorChannelFanTalkQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) } assertNull(exception.messageKey) @@ -85,7 +84,8 @@ class CreatorChannelFanTalkQueryServiceTest { viewer = viewer, page = -1, size = 10, - now = LocalDateTime.of(2026, 6, 21, 10, 0) + now = LocalDateTime.of(2026, 6, 21, 10, 0), + lang = Lang.EN ) assertEquals(60, tab.fanTalkCount) @@ -114,7 +114,7 @@ class CreatorChannelFanTalkQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L) - val tab = service.getFanTalkTab(1L, viewer, 3, 20, LocalDateTime.of(2026, 6, 21, 10, 0)) + val tab = service.getFanTalkTab(1L, viewer, 3, 20, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) assertEquals(5, tab.fanTalkCount) assertEquals(emptyList(), tab.fanTalks.map { it.fanTalkId }) @@ -156,7 +156,7 @@ class CreatorChannelFanTalkQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L) - val fanTalks = service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0)) + val fanTalks = service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN) .fanTalks assertEquals("fan", fanTalks[0].writerNickname) @@ -169,13 +169,10 @@ class CreatorChannelFanTalkQueryServiceTest { } private fun createService(port: FakeCreatorChannelFanTalkQueryPort): CreatorChannelFanTalkQueryService { - val langContext = LangContext() - langContext.setLang(Lang.EN) return CreatorChannelFanTalkQueryService( queryPortProvider = FixedCreatorChannelFanTalkQueryPortProvider(port), queryPolicy = CreatorChannelFanTalkQueryPolicy(), messageSource = SodaMessageSource(), - langContext = langContext, cloudFrontHost = "https://cdn.test" ) } @@ -216,7 +213,7 @@ private class FakeCreatorChannelFanTalkQueryPort : CreatorChannelFanTalkQueryPor var listLimit: Int? = null var replyParentFanTalkIds: List? = null - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkCreatorRecord? = creator + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkCreatorRecord? = creator override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked @@ -226,7 +223,8 @@ private class FakeCreatorChannelFanTalkQueryPort : CreatorChannelFanTalkQueryPor creatorId: Long, viewerId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List { listOffset = offset listLimit = limit @@ -235,7 +233,8 @@ private class FakeCreatorChannelFanTalkQueryPort : CreatorChannelFanTalkQueryPor override fun findCreatorReplies( creatorId: Long, - parentFanTalkIds: List + parentFanTalkIds: List, + lang: Lang ): List { replyParentFanTalkIds = parentFanTalkIds return creatorReplies diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt index 9922dfec..a49b84c4 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt @@ -18,6 +18,7 @@ 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.explorer.profile.channelDonation.ChannelDonationMessage +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisit @@ -70,8 +71,8 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( val aiCreator = saveMember("ai-creator", MemberRole.CREATOR, memberKind = MemberKind.AI_CHARACTER) flushAndClear() - val record = repository.findCreator(creator.id!!, viewer.id!!) - val aiRecord = repository.findCreator(aiCreator.id!!, viewer.id!!) + val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO) + val aiRecord = repository.findCreator(aiCreator.id!!, viewer.id!!, Lang.KO) assertNotNull(record) assertEquals(2, record!!.followerCount) @@ -91,7 +92,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveCharacter(creator, isActive = false) flushAndClear() - val record = repository.findCreator(creator.id!!, viewerId = null) + val record = repository.findCreator(creator.id!!, viewerId = null, lang = Lang.KO) assertEquals(null, record!!.characterId) assertFalse(record.isAiChatAvailable) @@ -108,7 +109,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveFollowing(saveMember("inactive-member-follower", MemberRole.USER, isActive = false), creator, isActive = true) flushAndClear() - val record = repository.findCreator(creator.id!!, viewerId = null) + val record = repository.findCreator(creator.id!!, viewerId = null, lang = Lang.KO) assertEquals(5, record!!.followerCount) } @@ -119,7 +120,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( val user = saveMember("non-creator-target", MemberRole.USER) flushAndClear() - val record = repository.findCreator(user.id!!, viewerId = null) + val record = repository.findCreator(user.id!!, viewerId = null, lang = Lang.KO) assertNotNull(record) assertEquals(MemberRole.USER, record!!.role) @@ -216,7 +217,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( updateUpdatedAt("LiveRoom", currentLive.id!!, now.minusHours(1)) flushAndClear() - val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!) + val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO) val currentLiveRecord = repository.findCurrentLive( creator.id!!, now, @@ -231,7 +232,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( canViewAdultContent = false, viewerId = viewer.id!! ) - val donations = repository.findChannelDonations(creator.id!!, viewer.id!!, now, limit = 8) + val donations = repository.findChannelDonations(creator.id!!, viewer.id!!, now, limit = 8, lang = Lang.KO) val schedules = repository.findSchedules( creator.id!!, now, @@ -250,7 +251,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( limit = 9 ) val seriesRecords = repository.findSeries(creator.id!!, viewer.id!!, now, false, ContentType.ALL, limit = 8) - val fanTalkSummary = repository.findFanTalkSummary(creator.id!!, viewer.id!!) + val fanTalkSummary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO) val activity = repository.findActivity(creator.id!!, now) val sns = repository.findSns(creator.id!!) @@ -289,7 +290,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveFollowing(viewer, creator, isActive = false, isNotify = true) flushAndClear() - val record = repository.findCreator(creator.id!!, viewer.id!!) + val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO) assertFalse(record!!.isFollow) assertFalse(record.isNotify) @@ -770,7 +771,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveCheers(writer, creator, "reply", isActive = true, LocalDateTime.of(2026, 6, 12, 14, 0), parent = older) flushAndClear() - val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!) + val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO) assertEquals(2, summary.totalCount) assertEquals(latest.id, summary.latestFanTalk!!.fanTalkId) @@ -788,9 +789,9 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( val thirdParty = saveMember("kst-donation-third-party", MemberRole.USER) flushAndClear() - val creatorView = repository.findChannelDonations(creator.id!!, creator.id!!, now, limit = 8) - val donorView = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8) - val thirdPartyView = repository.findChannelDonations(creator.id!!, thirdParty.id!!, now, limit = 8) + val creatorView = repository.findChannelDonations(creator.id!!, creator.id!!, now, limit = 8, lang = Lang.KO) + val donorView = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8, lang = Lang.KO) + val thirdPartyView = repository.findChannelDonations(creator.id!!, thirdParty.id!!, now, limit = 8, lang = Lang.KO) assertEquals(listOf(300), creatorView.map { it.can }) assertEquals(listOf(300), donorView.map { it.can }) @@ -821,7 +822,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( ) flushAndClear() - val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8) + val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8, lang = Lang.KO) assertEquals(listOf(1000, 3), records.map { it.can }) assertEquals("응원합니다", records.first().message) @@ -837,7 +838,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveDonation(creator, donor, 1000, now.minusMinutes(1), isSecret = true, additionalMessage = "cheer") flushAndClear() - val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8) + val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8, lang = Lang.KO) assertEquals("cheer", records.single().message) } @@ -852,7 +853,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveBlock(writer, viewer) flushAndClear() - val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!) + val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO) assertEquals(0, summary.totalCount) assertEquals(null, summary.latestFanTalk) @@ -1042,7 +1043,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveBlock(writerBlockedViewer, viewer) flushAndClear() - val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!) + val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO) assertEquals(1, summary.totalCount) assertEquals(visibleTalk.id, summary.latestFanTalk!!.fanTalkId) @@ -1057,7 +1058,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( saveCheers(writer, creator, "visible", isActive = true, LocalDateTime.of(2026, 6, 12, 12, 0)) flushAndClear() - val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!) + val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO) assertEquals("fan", summary.latestFanTalk!!.nickname) } @@ -1085,7 +1086,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor( updateUpdatedAt("LiveRoom", futureLive.id!!, now.plusDays(1).plusHours(1)) flushAndClear() - val creatorRecord = repository.findCreator(creator.id!!, viewerId = null) + val creatorRecord = repository.findCreator(creator.id!!, viewerId = null, lang = Lang.KO) val activity = repository.findActivity(creator.id!!, now) val sns = repository.findSns(creator.id!!) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt index 50fbc451..94ccaf13 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt @@ -70,7 +70,7 @@ class CreatorChannelHomeQueryServiceTest { val viewer = createMember(id = 10L, gender = Gender.FEMALE, authGender = 1) val now = LocalDateTime.of(2026, 6, 13, 10, 0) - val home = service.getHome(creatorId = 1L, viewer = viewer, now = now) + val home = service.getHome(creatorId = 1L, viewer = viewer, now = now, lang = Lang.KO) assertEquals(1L, home.creator.creatorId) assertEquals("https://cdn.test/profile/creator.png", home.creator.profileImageUrl) @@ -115,7 +115,7 @@ class CreatorChannelHomeQueryServiceTest { val service = createService(port, lang = Lang.JA) val viewer = createMember(id = 10L) - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.JA) assertEquals("ja", port.latestAudioLocale) assertEquals("ja", port.audioContentsLocale) @@ -130,7 +130,7 @@ class CreatorChannelHomeQueryServiceTest { val service = createService(port) val viewer = createMember(id = 1L, gender = Gender.FEMALE, authGender = null, role = MemberRole.CREATOR) - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) assertTrue(port.currentLiveIsViewerCreator == true) assertTrue(port.schedulesIsViewerCreator == true) @@ -145,7 +145,7 @@ class CreatorChannelHomeQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L, role = MemberRole.CREATOR) - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) assertTrue(port.currentLiveIsViewerCreator == true) assertTrue(port.schedulesIsViewerCreator == true) @@ -158,7 +158,7 @@ class CreatorChannelHomeQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L, role = MemberRole.USER) - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) assertFalse(port.currentLiveIsViewerCreator == true) assertFalse(port.schedulesIsViewerCreator == true) @@ -173,7 +173,7 @@ class CreatorChannelHomeQueryServiceTest { val service = createService(port, communityPort, preferenceService = preferenceService) val viewer = createMember(id = 10L) - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) assertEquals(false, port.currentLiveCanViewAdultContent) assertEquals(false, port.schedulesCanViewAdultContent) @@ -191,7 +191,7 @@ class CreatorChannelHomeQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L) - val home = service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + val home = service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) assertEquals("donor", home.channelDonations.first().nickname) } @@ -206,7 +206,7 @@ class CreatorChannelHomeQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getHome(creatorId = 999L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(999L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) } assertEquals("member.validation.user_not_found", exception.messageKey) @@ -222,7 +222,7 @@ class CreatorChannelHomeQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) } assertEquals("member.validation.creator_not_found", exception.messageKey) @@ -239,7 +239,7 @@ class CreatorChannelHomeQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) } assertNull(exception.messageKey) @@ -256,7 +256,7 @@ class CreatorChannelHomeQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0)) + service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO) } assertNull(exception.messageKey) @@ -408,6 +408,7 @@ class CreatorChannelHomeQueryServiceTest { ), channelDonations = listOf( CreatorChannelDonation( + memberId = 20L, nickname = "fan", profileImageUrl = "fan.png", can = 50, @@ -593,7 +594,7 @@ private class FakeCreatorChannelHomeQueryPort : CreatorChannelHomeQueryPort { var seriesLocale: String? = null var channelDonationNickname: String = "fan" - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? = creator + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? = creator override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked @@ -634,9 +635,11 @@ private class FakeCreatorChannelHomeQueryPort : CreatorChannelHomeQueryPort { creatorId: Long, viewerId: Long?, now: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List = listOf( CreatorChannelDonationRecord( + memberId = 20L, nickname = channelDonationNickname, profileImagePath = "profile/fan.png", can = 30, @@ -710,7 +713,7 @@ private class FakeCreatorChannelHomeQueryPort : CreatorChannelHomeQueryPort { ) } - override fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord { + override fun findFanTalkSummary(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkSummaryRecord { return CreatorChannelFanTalkSummaryRecord( totalCount = 1, latestFanTalk = CreatorChannelFanTalkRecord( @@ -798,7 +801,7 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer val homeCanViewAdultContentValues = mutableListOf() val homeLimits = mutableListOf() - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCommunityCreatorRecord? { return CreatorChannelCommunityCreatorRecord(creatorId = creatorId, role = MemberRole.CREATOR, nickname = "creator") } @@ -811,7 +814,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer viewerId: Long, canViewAdultContent: Boolean, offset: Long, - limit: Int + limit: Int, + lang: Lang ): List = emptyList() override fun findHomeCommunityPosts( @@ -819,7 +823,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer viewerId: Long, isPinned: Boolean, canViewAdultContent: Boolean, - limit: Int + limit: Int, + lang: Lang ): List { homeCreatorIds += creatorId homeViewerIds += viewerId @@ -850,7 +855,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer override fun findCommunityPost( postId: Long, viewerId: Long, - canViewAdultContent: Boolean + canViewAdultContent: Boolean, + lang: Lang ): CreatorChannelCommunityPostRecord? = null override fun countCommunityComments(postId: Long, viewerId: Long, creatorId: Long): Int = 0 @@ -860,12 +866,14 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer viewerId: Long, creatorId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ) = emptyList() override fun findLatestRepliesByCommentIds( commentIds: List, - viewerId: Long + viewerId: Long, + lang: Lang ) = emptyList() override fun findRootCommentContext( @@ -880,6 +888,7 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer commentId: Long, viewerId: Long, offset: Long, - limit: Int + limit: Int, + lang: Lang ) = emptyList() } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt index 47a28b97..fe7b0e4d 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt @@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.content.order.OrderType 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.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.member.Gender @@ -274,7 +275,7 @@ class DefaultCreatorChannelLiveQueryRepositoryTest @Autowired constructor( saveBlock(creator, viewer) flushAndClear() - val record = repository.findCreator(creator.id!!, viewer.id!!) + val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO) assertEquals(creator.id, record!!.creatorId) assertEquals(MemberRole.CREATOR, record.role) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt index 9d462af7..0f9b587b 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt @@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.v2.creator.channel.live.application 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.Gender import kr.co.vividnext.sodalive.member.Member @@ -42,7 +41,8 @@ class CreatorChannelLiveQueryServiceTest { sort = ContentSort.LATEST, page = 0, size = 20, - now = now + now = now, + lang = Lang.KO ) assertEquals(1L, port.findCreatorCreatorId) @@ -75,7 +75,8 @@ class CreatorChannelLiveQueryServiceTest { sort = ContentSort.LATEST, page = 0, size = 20, - now = LocalDateTime.of(2026, 6, 17, 10, 0) + now = LocalDateTime.of(2026, 6, 17, 10, 0), + lang = Lang.KO ) assertTrue(port.currentLiveIsViewerCreator == true) @@ -96,7 +97,8 @@ class CreatorChannelLiveQueryServiceTest { sort = ContentSort.LATEST, page = 0, size = 20, - now = LocalDateTime.of(2026, 6, 17, 10, 0) + now = LocalDateTime.of(2026, 6, 17, 10, 0), + lang = Lang.KO ) assertEquals(30, tab.liveReplayContentCount) @@ -127,7 +129,8 @@ class CreatorChannelLiveQueryServiceTest { sort = ContentSort.PRICE_LOW, page = 2, size = 20, - now = LocalDateTime.of(2026, 6, 17, 10, 0) + now = LocalDateTime.of(2026, 6, 17, 10, 0), + lang = Lang.KO ) assertEquals(30, tab.liveReplayContentCount) @@ -151,7 +154,7 @@ class CreatorChannelLiveQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0)) + service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO) } assertEquals("member.validation.user_not_found", exception.messageKey) @@ -167,7 +170,7 @@ class CreatorChannelLiveQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0)) + service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO) } assertEquals("member.validation.creator_not_found", exception.messageKey) @@ -184,7 +187,7 @@ class CreatorChannelLiveQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0)) + service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO) } assertNull(exception.messageKey) @@ -198,7 +201,7 @@ class CreatorChannelLiveQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getLiveTab(1L, viewer, ContentSort.LATEST, -1, 20, LocalDateTime.of(2026, 6, 17, 10, 0)) + service.getLiveTab(1L, viewer, ContentSort.LATEST, -1, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO) } assertEquals("common.error.invalid_request", exception.messageKey) @@ -211,7 +214,7 @@ class CreatorChannelLiveQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 51, LocalDateTime.of(2026, 6, 17, 10, 0)) + service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 51, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO) } assertEquals("common.error.invalid_request", exception.messageKey) @@ -240,14 +243,11 @@ class CreatorChannelLiveQueryServiceTest { preferenceService.canViewAdultContent(Mockito.any(Member::class.java) ?: createMember(id = 0L)) ).thenReturn(canViewAdultContent) val messageSource = SodaMessageSource() - val langContext = LangContext() - langContext.setLang(Lang.KO) return CreatorChannelLiveQueryService( queryPortProvider = portProvider, queryPolicy = CreatorChannelLiveReplayQueryPolicy(), memberContentPreferenceService = preferenceService, messageSource = messageSource, - langContext = langContext, cloudFrontHost = "https://cdn.test" ) } @@ -337,7 +337,7 @@ private class FakeCreatorChannelLiveQueryPort : CreatorChannelLiveQueryPort { var listOffset: Long? = null var listLimit: Int? = null - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? { + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? { findCreatorCreatorId = creatorId findCreatorViewerId = viewerId return creator diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepositoryTest.kt index b0a67dfa..c47c2aaa 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepositoryTest.kt @@ -13,6 +13,7 @@ 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.creator.admin.content.series.SeriesPublishedDaysOfWeek import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState +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 @@ -50,8 +51,8 @@ class DefaultCreatorChannelSeriesQueryRepositoryTest @Autowired constructor( saveBlock(creator, viewer) flushAndClear() - val record = repository.findCreator(creator.id!!, viewer.id!!) - val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!) + val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO) + val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO) assertEquals(creator.id, record!!.creatorId) assertEquals(MemberRole.CREATOR, record.role) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryServiceTest.kt index 67f1a03f..9a17a6dd 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorChannelSeriesQueryServiceTest.kt @@ -42,7 +42,8 @@ class CreatorChannelSeriesQueryServiceTest { sort = "UNKNOWN", page = -1, size = 100, - now = now + now = now, + lang = Lang.EN ) assertEquals(ContentSort.LATEST, tab.sort) @@ -70,7 +71,7 @@ class CreatorChannelSeriesQueryServiceTest { val service = createService(port) val viewer = createMember(id = 1L) - val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0)) + val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN) assertNull(tab.series.first().purchasedContentCount) assertNull(tab.series.first().paidContentCount) @@ -86,7 +87,7 @@ class CreatorChannelSeriesQueryServiceTest { val service = createService(port) val viewer = createMember(id = 10L) - val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0)) + val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN) assertNull(tab.series.first().coverImageUrl) assertEquals(0, tab.series.first().purchasedPaidContentRate) @@ -100,7 +101,7 @@ class CreatorChannelSeriesQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0)) + service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN) } assertEquals("member.validation.user_not_found", exception.messageKey) @@ -114,7 +115,7 @@ class CreatorChannelSeriesQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0)) + service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN) } assertEquals("member.validation.creator_not_found", exception.messageKey) @@ -128,7 +129,7 @@ class CreatorChannelSeriesQueryServiceTest { val viewer = createMember(id = 10L) val exception = assertThrows(SodaException::class.java) { - service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0)) + service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN) } assertNull(exception.messageKey) @@ -192,7 +193,7 @@ private class FakeCreatorChannelSeriesQueryPort : CreatorChannelSeriesQueryPort var listLimit: Int? = null var listCanViewAdultContent: Boolean? = null - override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelSeriesCreatorRecord? = creator + override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelSeriesCreatorRecord? = creator override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked override fun countSeries(creatorId: Long, now: LocalDateTime, canViewAdultContent: Boolean): Int = seriesCount diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt index 8ad5cd41..e1c06d67 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt @@ -2,8 +2,11 @@ package kr.co.vividnext.sodalive.v2.gift.application import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.configs.QueryDslConfig +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.support.LegacyCreatorNameTestConfiguration +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository @@ -35,14 +38,15 @@ import java.time.LocalDateTime "spring.datasource.url=jdbc:h2:mem:gift-query;MODE=MySQL;NON_KEYWORDS=VALUE" ] ) -@Import(QueryDslConfig::class) +@Import(QueryDslConfig::class, LegacyCreatorNameTestConfiguration::class) class GiftQueryServiceTest @Autowired constructor( private val categoryRepository: GiftCategoryRepository, private val sizePriceRepository: GiftSizePriceRepository, private val giftRepository: GiftRepository, private val deliveryRepository: GiftDeliveryRepository, private val memberRepository: MemberRepository, - private val giftMailboxRepository: GiftMailboxRepository + private val giftMailboxRepository: GiftMailboxRepository, + private val creatorLocalizedNameService: CreatorLocalizedNameService ) { @Test fun `폼 옵션은 활성 카테고리와 활성 사이즈 가격만 반환한다`() { @@ -79,7 +83,8 @@ class GiftQueryServiceTest @Autowired constructor( memberId = sender.id!!, type = GiftListType.SENT, page = 0, - size = 20 + size = 20, + lang = Lang.KO ) assertEquals(1L, result.totalCount) @@ -122,7 +127,8 @@ class GiftQueryServiceTest @Autowired constructor( memberId = recipient.id!!, type = GiftListType.RECEIVED, page = 0, - size = 20 + size = 20, + lang = Lang.KO ) assertEquals(1L, result.totalCount) @@ -162,7 +168,8 @@ class GiftQueryServiceTest @Autowired constructor( memberId = member.id!!, type = GiftListType.ALL, page = 0, - size = 20 + size = 20, + lang = Lang.KO ) assertEquals(2L, result.totalCount) @@ -220,7 +227,8 @@ class GiftQueryServiceTest @Autowired constructor( memberId = member.id!!, type = GiftListType.ALL, page = 0, - size = 20 + size = 20, + lang = Lang.KO ) val items = result.items.associateBy { it.applicationNo } @@ -245,7 +253,8 @@ class GiftQueryServiceTest @Autowired constructor( memberId = sender.id!!, type = GiftListType.SENT, page = -1, - size = 0 + size = 0, + lang = Lang.KO ) assertEquals(0, result.page) @@ -272,7 +281,7 @@ class GiftQueryServiceTest @Autowired constructor( delivery.recipientAddress = "서울시 중구" delivery.recipientAddressDetail = "202호" - val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo) + val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO) assertEquals(GiftDirection.SENT, result.direction) assertEquals(recipient.nickname, result.giftInfo.recipientCreatorNickname) @@ -335,7 +344,7 @@ class GiftQueryServiceTest @Autowired constructor( ) saveDelivery(gift).applyStatusTimes(status) - val result = service.getGiftDetail(sender.id!!, gift.applicationNo) + val result = service.getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO) assertEquals("소다라이브 선물 담당자", result.mailbox?.name) assertEquals("01012345678", result.mailbox?.phoneNumber) @@ -370,7 +379,7 @@ class GiftQueryServiceTest @Autowired constructor( ) saveDelivery(gift).applyStatusTimes(status) - val result = service.getGiftDetail(sender.id!!, gift.applicationNo) + val result = service.getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO) assertNull(result.mailbox) } @@ -383,7 +392,7 @@ class GiftQueryServiceTest @Autowired constructor( ) saveDelivery(receivedGift).applyStatusTimes(GiftStatus.TRACKING_REGISTERED) - val recipientResult = service.getGiftDetail(recipient.id!!, receivedGift.applicationNo) + val recipientResult = service.getGiftDetail(recipient.id!!, receivedGift.applicationNo, Lang.KO) assertNull(recipientResult.mailbox) } @@ -401,7 +410,7 @@ class GiftQueryServiceTest @Autowired constructor( ) saveDelivery(gift).applyStatusTimes(GiftStatus.INSPECTION_COMPLETED) - val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo) + val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO) assertNull(result.mailbox) } @@ -426,7 +435,7 @@ class GiftQueryServiceTest @Autowired constructor( recipientAddressDetail = "202호" } - val result = queryService().getGiftDetail(recipient.id!!, gift.applicationNo) + val result = queryService().getGiftDetail(recipient.id!!, gift.applicationNo, Lang.KO) assertEquals(GiftDirection.RECEIVED, result.direction) assertNull(result.giftInfo.recipientCreatorNickname) @@ -478,13 +487,13 @@ class GiftQueryServiceTest @Autowired constructor( val service = queryService() val hiddenException = assertThrows(SodaException::class.java) { - service.getGiftDetail(recipient.id!!, hiddenGift.applicationNo) + service.getGiftDetail(recipient.id!!, hiddenGift.applicationNo, Lang.KO) } val unrelatedException = assertThrows(SodaException::class.java) { - service.getGiftDetail(other.id!!, visibleGift.applicationNo) + service.getGiftDetail(other.id!!, visibleGift.applicationNo, Lang.KO) } val missingDeliveryException = assertThrows(SodaException::class.java) { - service.getGiftDetail(sender.id!!, missingDeliveryGift.applicationNo) + service.getGiftDetail(sender.id!!, missingDeliveryGift.applicationNo, Lang.KO) } assertEquals("common.error.invalid_request", hiddenException.messageKey) @@ -517,8 +526,8 @@ class GiftQueryServiceTest @Autowired constructor( } val service = queryService() - val pendingResult = service.getGiftDetail(recipient.id!!, pendingGift.applicationNo) - val terminalResult = service.getGiftDetail(recipient.id!!, terminalGift.applicationNo) + val pendingResult = service.getGiftDetail(recipient.id!!, pendingGift.applicationNo, Lang.KO) + val terminalResult = service.getGiftDetail(recipient.id!!, terminalGift.applicationNo, Lang.KO) assertEquals(true, pendingResult.recipientAddressRequired) assertNull(pendingResult.recipientAddress) @@ -551,8 +560,8 @@ class GiftQueryServiceTest @Autowired constructor( } val service = queryService() - val canceledResult = service.getGiftDetail(sender.id!!, canceledGift.applicationNo) - val undeliverableResult = service.getGiftDetail(sender.id!!, undeliverableGift.applicationNo) + val canceledResult = service.getGiftDetail(sender.id!!, canceledGift.applicationNo, Lang.KO) + val undeliverableResult = service.getGiftDetail(sender.id!!, undeliverableGift.applicationNo, Lang.KO) assertEquals(NOW.plusHours(1), canceledResult.delivery.canceledAt) assertNull(canceledResult.statusTimeline[1].occurredAt) @@ -639,7 +648,8 @@ class GiftQueryServiceTest @Autowired constructor( deliveryRepository, memberRepository, giftMailboxRepository, - "https://cdn.test" + "https://cdn.test", + creatorLocalizedNameService ) private companion object { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt index 461212b7..8bb24ff3 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.content.theme.AudioContentTheme import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.member.Gender @@ -62,7 +63,7 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( activeFollow.createdAt = LocalDateTime.of(2026, 6, 25, 0, 0) flushAndClear() - val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20) + val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20, lang = Lang.KO) assertEquals(listOf(olderCreator.id!!, activeCreator.id!!), creators.map { it.creatorId }) assertEquals("https://cdn.test/active.png", creators.last().creatorProfileImageUrl) @@ -82,7 +83,7 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( saveBlock(creatorBlockedViewer, viewer) flushAndClear() - val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20) + val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20, lang = Lang.KO) assertEquals(listOf(visibleCreator.id!!), creators.map { it.creatorId }) } @@ -110,7 +111,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( canViewAdultContent = false, effectiveViewerGender = Gender.NONE, isViewerCreator = false, - limit = 10 + limit = 10, + lang = Lang.KO ) assertEquals(listOf(latest.id!!, older.id!!), lives.map { it.liveId }) @@ -143,7 +145,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( canViewAdultContent = true, effectiveViewerGender = Gender.FEMALE, isViewerCreator = true, - limit = 10 + limit = 10, + lang = Lang.KO ) assertEquals(listOf(visible.id!!), lives.map { it.liveId }) @@ -175,7 +178,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( effectiveViewerGender = Gender.NONE, isViewerCreator = false, now = LocalDateTime.of(2026, 6, 25, 12, 0), - limit = 3 + limit = 3, + lang = Lang.KO ) assertEquals(listOf("LIVE:${live.id!!}", "AUDIO:${audio.id!!}"), schedules.map { it.scheduleId }) @@ -210,7 +214,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( effectiveViewerGender = Gender.NONE, isViewerCreator = false, now = LocalDateTime.of(2026, 6, 25, 0, 0), - limit = 3 + limit = 3, + lang = Lang.KO ) assertEquals(listOf("AUDIO:${scheduledAudio.id!!}"), schedules.map { it.scheduleId }) @@ -233,7 +238,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( effectiveViewerGender = Gender.NONE, isViewerCreator = false, now = LocalDateTime.of(2026, 6, 25, 14, 30), - limit = 3 + limit = 3, + lang = Lang.KO ) assertTrue(schedules.map { it.scheduleId }.contains("AUDIO:${sameKstDayEvening.id!!}")) @@ -259,7 +265,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( effectiveViewerGender = Gender.NONE, isViewerCreator = false, now = LocalDateTime.of(2026, 6, 25, 0, 0), - limit = 10 + limit = 10, + lang = Lang.KO ) assertEquals( @@ -302,7 +309,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( effectiveViewerGender = Gender.FEMALE, isViewerCreator = true, now = LocalDateTime.of(2026, 6, 25, 0, 0), - limit = 10 + limit = 10, + lang = Lang.KO ) assertEquals(listOf("LIVE:${visibleLive.id!!}", "AUDIO:${audio.id!!}"), schedules.map { it.scheduleId }) @@ -332,7 +340,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = false, nowUtc = LocalDateTime.of(2026, 6, 25, 9, 30), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals(listOf(latestVisible.id!!.toString()), news.map { it.newsId }) @@ -377,13 +386,15 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 14, 23, 59), - limit = 30 + limit = 30, + lang = Lang.KO ) val afterLatestVisible = repository.findRecentNews( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 15, 0, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals(listOf(oldSecondNews.id!!.toString(), oldFirstNews.id!!.toString()), beforeLatestVisible.map { it.newsId }) @@ -412,7 +423,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 15, 0, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertTrue(news.none { it.type == FollowingNewsType.CREATOR_RANKING }) @@ -475,7 +487,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0), - limit = 2 + limit = 2, + lang = Lang.KO ) assertEquals(listOf(latestFirst.id!!.toString(), latestSecond.id!!.toString()), news.map { it.newsId }) @@ -516,7 +529,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 25, 11, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals(listOf(visible.id!!.toString()), news.map { it.newsId }) @@ -556,7 +570,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = false, nowUtc = LocalDateTime.of(2026, 6, 25, 11, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals(listOf(visible.id!!.toString()), news.map { it.newsId }) @@ -577,7 +592,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = false, nowUtc = LocalDateTime.of(2026, 6, 25, 14, 30), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals(listOf(visibleNow.id!!.toString()), news.map { it.newsId }) @@ -628,7 +644,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) val communityNews = news.first { it.type == FollowingNewsType.COMMUNITY_POST }.communityPost!! @@ -668,7 +685,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertTrue(news.isEmpty()) @@ -713,7 +731,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals(listOf(negativeFreePost.id!!, freePost.id!!), news.map { it.communityPost?.postId }) @@ -759,13 +778,15 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = false, nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) val visibleNews = repository.findRecentNews( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals(emptyList(), hiddenNews.map { it.communityPost?.postId ?: it.audioContent?.contentId }) @@ -824,7 +845,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, canViewAdultContent = true, nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0), - limit = 30 + limit = 30, + lang = Lang.KO ) assertEquals( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt index cff15ee5..3e51e424 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.v2.home.following.application import kr.co.vividnext.sodalive.i18n.Lang -import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole @@ -29,13 +28,11 @@ import java.util.TimeZone class HomeFollowingQueryServiceTest { private val queryPort = RecordingHomeFollowingQueryPort() private val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java) - private val langContext = LangContext() private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java) private val now = LocalDateTime.of(2026, 6, 25, 12, 0) private val service = HomeFollowingQueryService( queryPort, memberContentPreferenceService, - langContext, creatorCommunityTranslationService ) { now } @@ -45,7 +42,7 @@ class HomeFollowingQueryServiceTest { val member = member(10L) Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(true) - val home = service.findHomeFollowing(member) + val home = service.findHomeFollowing(member, Lang.KO) assertEquals(listOf(HomeFollowingCreator(1L, "creator", "profile")), home.followingCreators) assertEquals(emptyList(), home.recentChats) @@ -63,7 +60,7 @@ class HomeFollowingQueryServiceTest { val member = member(11L) Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(false) - service.findHomeFollowing(member) + service.findHomeFollowing(member, Lang.KO) assertEquals(false, queryPort.onAirCanViewAdultContent) assertEquals(false, queryPort.monthlySchedulesCanViewAdultContent) @@ -81,7 +78,7 @@ class HomeFollowingQueryServiceTest { } Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(true) - service.findHomeFollowing(member) + service.findHomeFollowing(member, Lang.KO) assertEquals(Gender.FEMALE, queryPort.onAirEffectiveViewerGender) assertEquals(Gender.FEMALE, queryPort.monthlySchedulesEffectiveViewerGender) @@ -99,14 +96,13 @@ class HomeFollowingQueryServiceTest { val defaultService = HomeFollowingQueryService( defaultQueryPort, memberContentPreferenceService, - langContext, creatorCommunityTranslationService ) val member = member(12L) Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(true) val beforeUtc = LocalDateTime.now(ZoneOffset.UTC).minusSeconds(1) - defaultService.findHomeFollowing(member) + defaultService.findHomeFollowing(member, Lang.KO) val afterUtc = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(1) val capturedNow = defaultQueryPort.recentNewsNow!! @@ -121,9 +117,9 @@ class HomeFollowingQueryServiceTest { @Test @DisplayName("팔로잉 최근 소식은 커뮤니티 게시물만 영어 표시 본문으로 일괄 치환하고 다른 소식과 원문 폴백을 유지한다") fun shouldTranslateOnlyCommunityPostNewsInOneEnglishBatch() { - langContext.setLang(Lang.EN) val translatedNews = communityNews("community-1", 101L, "original-1") val audioNews = HomeFollowingNews( + creatorMemberId = 10L, newsId = "audio-1", type = FollowingNewsType.AUDIO_CONTENT, visibleFromAtUtc = "2026-06-25T11:59:00Z" @@ -138,7 +134,7 @@ class HomeFollowingQueryServiceTest { ) ) - val home = service.findHomeFollowing(member(14L)) + val home = service.findHomeFollowing(member(14L), Lang.EN) assertEquals("translated-1-en", home.recentNews[0].communityPost?.content) assertEquals(audioNews, home.recentNews[1]) @@ -153,6 +149,7 @@ class HomeFollowingQueryServiceTest { private fun communityNews(newsId: String, postId: Long, content: String): HomeFollowingNews { return HomeFollowingNews( + creatorMemberId = 10L, newsId = newsId, type = FollowingNewsType.COMMUNITY_POST, visibleFromAtUtc = "2026-06-25T12:00:00Z", @@ -186,7 +183,7 @@ class HomeFollowingQueryServiceTest { var recentNewsCanViewAdultContent: Boolean? = null var recentNews: List = emptyList() - override fun findFollowingCreators(memberId: Long, limit: Int): List { + override fun findFollowingCreators(memberId: Long, limit: Int, lang: Lang): List { this.memberId = memberId followingCreatorsLimit = limit return listOf(HomeFollowingCreator(1L, "creator", "profile")) @@ -197,7 +194,8 @@ class HomeFollowingQueryServiceTest { canViewAdultContent: Boolean, effectiveViewerGender: Gender, isViewerCreator: Boolean, - limit: Int + limit: Int, + lang: Lang ): List { onAirCanViewAdultContent = canViewAdultContent onAirEffectiveViewerGender = effectiveViewerGender @@ -212,7 +210,8 @@ class HomeFollowingQueryServiceTest { effectiveViewerGender: Gender, isViewerCreator: Boolean, now: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List { monthlySchedulesCanViewAdultContent = canViewAdultContent monthlySchedulesEffectiveViewerGender = effectiveViewerGender @@ -226,7 +225,8 @@ class HomeFollowingQueryServiceTest { memberId: Long, canViewAdultContent: Boolean, nowUtc: LocalDateTime, - limit: Int + limit: Int, + lang: Lang ): List { recentNewsCanViewAdultContent = canViewAdultContent recentNewsNow = nowUtc diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt index 1d3e1a95..cf36c383 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence import kr.co.vividnext.sodalive.configs.QueryDslConfig +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreSpec import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord @@ -98,7 +99,7 @@ class DefaultCreatorRankingSnapshotRepositoryTest @Autowired constructor( ) ) - val latestVisible = adapter.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, NOW) + val latestVisible = adapter.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, NOW, Lang.KO) assertEquals(listOf(3L, 4L, 2L), latestVisible.map { it.creatorId }) assertEquals(listOf(1, 2, 3), latestVisible.map { it.rankNo }) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt index 0f06c6ee..ecb7f326 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.ranking.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort @@ -19,7 +20,11 @@ class CreatorRankingQueryServiceTest { @Test @DisplayName("fallback refresh 후 재조회가 같은 read-only transaction에 묶이지 않도록 조회 메서드에는 Transactional이 없다") fun shouldNotWrapQueryMethodWithTransactional() { - val method = CreatorRankingQueryService::class.java.getDeclaredMethod("getCreatorRankings", java.lang.Long::class.java) + val method = CreatorRankingQueryService::class.java.getDeclaredMethod( + "getCreatorRankings", + java.lang.Long::class.java, + Lang::class.java + ) assertFalse(method.isAnnotationPresent(Transactional::class.java)) } @@ -32,7 +37,7 @@ class CreatorRankingQueryServiceTest { snapshotPort.latestResponses = ArrayDeque(listOf(emptyList(), listOf(snapshot(1, rankNo = 1)))) val service = service(snapshotPort = snapshotPort, snapshotJobService = jobService) - val result = service.getCreatorRankings(viewerMemberId = null) + val result = service.getCreatorRankings(viewerMemberId = null, lang = Lang.KO) Mockito.verify(jobService).refreshLastCompletedWeekByFallback() assertEquals(2, snapshotPort.latestCallCount) @@ -48,7 +53,7 @@ class CreatorRankingQueryServiceTest { Mockito.doThrow(IllegalStateException("fallback failed")).`when`(jobService).refreshLastCompletedWeekByFallback() val service = service(snapshotPort = snapshotPort, snapshotJobService = jobService) - val result = service.getCreatorRankings(viewerMemberId = null) + val result = service.getCreatorRankings(viewerMemberId = null, lang = Lang.KO) assertFalse(result.showRankChange) assertTrue(result.items.isEmpty()) @@ -66,7 +71,7 @@ class CreatorRankingQueryServiceTest { snapshotPort.previousSnapshots = listOf(snapshot(1, rankNo = 1), snapshot(2, rankNo = 2)) val service = service(snapshotPort = snapshotPort, snapshotJobService = jobService) - val result = service.getCreatorRankings(viewerMemberId = null) + val result = service.getCreatorRankings(viewerMemberId = null, lang = Lang.KO) Mockito.verify(jobService, Mockito.never()).refreshLastCompletedWeekByFallback() assertTrue(result.showRankChange) @@ -86,7 +91,7 @@ class CreatorRankingQueryServiceTest { blockPort.blockedCreatorIds = setOf(2L) val service = service(snapshotPort = snapshotPort, blockPort = blockPort) - val result = service.getCreatorRankings(viewerMemberId = 99L) + val result = service.getCreatorRankings(viewerMemberId = 99L, lang = Lang.KO) assertEquals(listOf(1, 2), result.items.map { it.rank }) assertEquals(0L, result.items[1].creatorId) @@ -133,7 +138,8 @@ private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort { override fun findLatestVisibleSnapshots( rankingType: CreatorRankingType, - nowUtc: LocalDateTime + nowUtc: LocalDateTime, + lang: Lang ): List { latestCallCount++ return if (latestResponses.size > 1) { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt index aa273d4c..8e3e5f8e 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.ranking.application +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreSpec import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate @@ -120,7 +121,7 @@ private class FakeCreatorRankingSnapshotPort : CreatorRankingSnapshotPort { ) = snapshots override fun findLatestSnapshots() = snapshots override fun findPreviousCompletedSnapshots() = snapshots - override fun findLatestVisibleSnapshots(rankingType: CreatorRankingType, nowUtc: LocalDateTime) = snapshots + override fun findLatestVisibleSnapshots(rankingType: CreatorRankingType, nowUtc: LocalDateTime, lang: Lang) = snapshots override fun findPreviousVisibleSnapshots( rankingType: CreatorRankingType, currentAggregationStartAtUtc: LocalDateTime, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt index 05ee5da9..50ec4da8 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt @@ -89,7 +89,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( } flushAndClear() - val lives = repository.findLiveRecommendations(limit = 20, includeAdultLives = false) + val lives = repository.findLiveRecommendations(limit = 20, includeAdultLives = false, lang = Lang.KO) assertEquals(20, lives.size) assertEquals(true, lives.zipWithNext().all { it.first.liveRoomId > it.second.liveRoomId }) @@ -109,8 +109,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val oldest = saveLiveRoom(creator, baseAt, channelName = "paged-live-oldest", isAdult = false) flushAndClear() - val page0 = repository.findLiveRecommendations(offset = 0L, limit = 3, includeAdultLives = false) - val page1 = repository.findLiveRecommendations(offset = 2L, limit = 3, includeAdultLives = false) + val page0 = repository.findLiveRecommendations(offset = 0L, limit = 3, includeAdultLives = false, lang = Lang.KO) + val page1 = repository.findLiveRecommendations(offset = 2L, limit = 3, includeAdultLives = false, lang = Lang.KO) assertEquals(listOf(newest.id, middle.id, oldest.id), page0.map { it.liveRoomId }) assertEquals(listOf(oldest.id), page1.map { it.liveRoomId }) @@ -135,7 +135,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( offset = 0, limit = 1, memberId = viewer.id, - includeAdultLives = true + includeAdultLives = true, + lang = Lang.KO ) assertEquals(listOf(live.id), lives.map { it.liveRoomId }) @@ -170,7 +171,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( offset = 0, limit = 10, memberId = viewer.id, - includeAdultLives = false + includeAdultLives = false, + lang = Lang.KO ) assertEquals(listOf(newerVisibleLive.id, olderVisibleLive.id), lives.map { it.liveRoomId }) @@ -191,7 +193,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( saveBlock(creatorBlockedViewer, viewer) flushAndClear() - val lives = repository.findLiveRecommendations(limit = 10, memberId = viewer.id) + val lives = repository.findLiveRecommendations(limit = 10, memberId = viewer.id, lang = Lang.KO) assertEquals(listOf(visibleLive.id), lives.map { it.liveRoomId }) } @@ -221,7 +223,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( limit = 10, memberId = viewer.id, isViewerCreator = true, - effectiveViewerGender = Gender.NONE + effectiveViewerGender = Gender.NONE, + lang = Lang.KO ) assertEquals(listOf(ownerLive.id, availableLive.id), lives.map { it.liveRoomId }) @@ -244,7 +247,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( limit = 10, memberId = viewer.id, isViewerCreator = false, - effectiveViewerGender = Gender.NONE + effectiveViewerGender = Gender.NONE, + lang = Lang.KO ) assertEquals(listOf(restrictedLive.id), lives.map { it.liveRoomId }) @@ -281,7 +285,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( limit = 10, memberId = viewer.id, isViewerCreator = true, - effectiveViewerGender = Gender.MALE + effectiveViewerGender = Gender.MALE, + lang = Lang.KO ) assertEquals(listOf(ownerLive.id, allLive.id, maleOnlyLive.id), lives.map { it.liveRoomId }) @@ -311,7 +316,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( limit = 10, memberId = viewer.id, isViewerCreator = true, - effectiveViewerGender = Gender.FEMALE + effectiveViewerGender = Gender.FEMALE, + lang = Lang.KO ) assertEquals(listOf(ownerLive.id, femaleOnlyLive.id), lives.map { it.liveRoomId }) @@ -341,7 +347,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val lives = repository.findLiveRecommendations( limit = 10, memberId = viewer.id, - effectiveViewerGender = Gender.FEMALE + effectiveViewerGender = Gender.FEMALE, + lang = Lang.KO ) assertEquals(listOf(femaleOnlyLive.id, allLive.id), lives.map { it.liveRoomId }) @@ -371,12 +378,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val noneGenderLives = repository.findLiveRecommendations( limit = 10, memberId = viewer.id, - effectiveViewerGender = Gender.NONE + effectiveViewerGender = Gender.NONE, + lang = Lang.KO ) val nullGenderLives = repository.findLiveRecommendations( limit = 10, memberId = viewer.id, - effectiveViewerGender = null + effectiveViewerGender = null, + lang = Lang.KO ) val expectedIds = listOf(allLive.id, femaleOnlyLive.id, maleOnlyLive.id) @@ -659,7 +668,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( updateCreatedAt("CreatorCommunity", community.id!!, baseAt.minusHours(3)) flushAndClear() - val creators = repository.findRecentlyActiveCreators(limit = 10) + val creators = repository.findRecentlyActiveCreators(limit = 10, lang = Lang.KO) val byCreatorNickname = creators.associateBy { it.creatorNickname } assertEquals(4, creators.size) @@ -699,7 +708,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( saveLiveRoom(emptyChannelCreator, baseAt, channelName = "", isActive = false) flushAndClear() - val creators = repository.findRecentlyActiveCreators(limit = 10) + val creators = repository.findRecentlyActiveCreators(limit = 10, lang = Lang.KO) assertEquals(listOf(inactiveLiveCreator.nickname), creators.map { it.creatorNickname }) assertEquals(inactiveLiveCreator.id, creators.single().creatorId) @@ -723,8 +732,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( updateCreatedAt("CreatorCommunity", adultCommunity.id!!, baseAt) flushAndClear() - val hiddenCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = false) - val visibleCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = true) + val hiddenCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = false, lang = Lang.KO) + val visibleCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = true, lang = Lang.KO) assertEquals(listOf(normalLiveCreator.nickname), hiddenCreators.map { it.creatorNickname }) assertEquals( @@ -762,7 +771,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( saveBlock(creatorBlockedViewer, viewer) flushAndClear() - val creators = repository.findRecentlyActiveCreators(limit = 10, memberId = viewer.id) + val creators = repository.findRecentlyActiveCreators(limit = 10, memberId = viewer.id, lang = Lang.KO) assertEquals(listOf(visibleCreator.nickname), creators.map { it.creatorNickname }) assertEquals(CreatorActivityType.COMMUNITY, creators.single().activityType) @@ -779,7 +788,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( updateCreatedAt("CreatorCommunity", community.id!!, baseAt.plusMinutes(1)) flushAndClear() - val creators = repository.findRecentlyActiveCreators(limit = 10) + val creators = repository.findRecentlyActiveCreators(limit = 10, lang = Lang.KO) assertEquals(listOf(visibleCreator.nickname), creators.map { it.creatorNickname }) assertEquals(CreatorActivityType.COMMUNITY, creators.single().activityType) @@ -1597,7 +1606,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( updateCreatedAt("Member", newHighScoreCreator.id!!, now.minusDays(60)) flushAndClear() - val creators = repository.findRecentDebutCreators(now, limit = 10) + val creators = repository.findRecentDebutCreators(now, limit = 10, lang = Lang.KO) assertEquals(listOf(newHighScoreCreator.id, newLowScoreCreator.id), creators.map { it.creatorId }) } @@ -1623,7 +1632,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( ) flushAndClear() - val creators = repository.findRecentDebutCreators(now, limit = 10) + val creators = repository.findRecentDebutCreators(now, limit = 10, lang = Lang.KO) assertEquals(listOf(endedLiveCreator.id), creators.map { it.creatorId }) } @@ -1647,7 +1656,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( updateCreatedAt("AudioContentLike", baseLike2.id!!, now.minusDays(1)) flushAndClear() - val creators = repository.findRecentDebutCreators(now, limit = 2) + val creators = repository.findRecentDebutCreators(now, limit = 2, lang = Lang.KO) assertEquals(listOf(boostedCreator.id, baseCreator.id), creators.map { it.creatorId }) } @@ -1671,8 +1680,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( updateCreatedAt("AudioContentLike", like.id!!, now.minusHours(1)) flushAndClear() - val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 2, includeAdultContents = false) - val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 2, includeAdultContents = false) + val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 2, includeAdultContents = false, lang = Lang.KO) + val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 2, includeAdultContents = false, lang = Lang.KO) assertEquals(listOf(normalNewest.id, normalOldest.id), page0.map { it.creatorId }) assertEquals(listOf(normalOldest.id), page1.map { it.creatorId }) @@ -1693,7 +1702,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( saveBlock(creatorBlockedViewer, viewer) flushAndClear() - val creators = repository.findRecentDebutCreators(now, limit = 10, memberId = viewer.id) + val creators = repository.findRecentDebutCreators(now, limit = 10, memberId = viewer.id, lang = Lang.KO) assertEquals(listOf(visibleCreator.id), creators.map { it.creatorId }) } @@ -1710,9 +1719,9 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( saveAudioContent(creator3, now.minusDays(5), isActive = true) flushAndClear() - val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 1, includeAdultContents = false) - val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 1, includeAdultContents = false) - val page2 = repository.findRecentDebutCreators(now, offset = 2L, limit = 1, includeAdultContents = false) + val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 1, includeAdultContents = false, lang = Lang.KO) + val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 1, includeAdultContents = false, lang = Lang.KO) + val page2 = repository.findRecentDebutCreators(now, offset = 2L, limit = 1, includeAdultContents = false, lang = Lang.KO) val pagedCreatorIds = (page0 + page1 + page2).map { it.creatorId } assertEquals(setOf(creator1.id, creator2.id, creator3.id), pagedCreatorIds.toSet()) @@ -1729,7 +1738,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( saveAudioContent(creator2, now.minusDays(5), isActive = true) flushAndClear() - val creators = repository.findRecentDebutCreators(now, limit = 10) + val creators = repository.findRecentDebutCreators(now, limit = 10, lang = Lang.KO) assertEquals(2, creators.size) assertEquals(setOf(creator1.id, creator2.id), creators.map { it.creatorId }.toSet()) @@ -2111,7 +2120,10 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val inactiveCreator = saveMember("cheer-detail-inactive", MemberRole.CREATOR, isActive = false) flushAndClear() - val details = repository.findCheerCreatorRecommendationDetails(listOf(activeCreator.id!!, inactiveCreator.id!!, 999L)) + val details = repository.findCheerCreatorRecommendationDetails( + listOf(activeCreator.id!!, inactiveCreator.id!!, 999L), + lang = Lang.KO + ) val detailById = details.associateBy { it.creatorId } assertEquals(setOf(activeCreator.id), detailById.keys) @@ -2131,7 +2143,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val details = repository.findCheerCreatorRecommendationDetails( listOf(viewerBlockedCreator.id!!, creatorBlockedViewer.id!!, visibleCreator.id!!), - memberId = viewer.id + memberId = viewer.id, + lang = Lang.KO ) assertEquals(listOf(visibleCreator.id), details.map { it.creatorId }) @@ -2150,7 +2163,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val details = repository.findCheerCreatorRecommendationDetails( listOf(viewer.id!!, activeFollowedCreator.id!!, inactiveFollowedCreator.id!!, visibleCreator.id!!), - memberId = viewer.id + memberId = viewer.id, + lang = Lang.KO ) assertEquals(listOf(inactiveFollowedCreator.id, visibleCreator.id), details.map { it.creatorId }) @@ -2167,7 +2181,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val details = repository.findCheerCreatorRecommendationDetails( listOf(viewer.id!!, activeFollowedCreator.id!!, visibleCreator.id!!), - memberId = null + memberId = null, + lang = Lang.KO ) assertEquals(listOf(viewer.id, activeFollowedCreator.id, visibleCreator.id), details.map { it.creatorId }) @@ -2178,7 +2193,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( fun shouldReturnEmptyCheerCreatorRecommendationDetailsWhenIdsAreEmpty() { assertEquals( emptyList(), - repository.findCheerCreatorRecommendationDetails(emptyList()) + repository.findCheerCreatorRecommendationDetails(emptyList(), lang = Lang.KO) ) } @@ -2216,7 +2231,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val details = repository.findPopularCommunityRecommendationDetails( listOf(eligible.id!!, paid.id!!, fixed.id!!, adult.id!!, inactivePost.id!!, inactiveCreatorPost.id!!, 999L), memberId = member.id, - includeAdultCommunities = false + includeAdultCommunities = false, + lang = Lang.KO ) val detailById = details.associateBy { it.communityId } @@ -2244,7 +2260,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val details = repository.findPopularCommunityRecommendationDetails( listOf(free.id!!), memberId = null, - includeAdultCommunities = false + includeAdultCommunities = false, + lang = Lang.KO ) assertEquals(listOf(free.id), details.map { it.communityId }) @@ -2265,7 +2282,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val details = repository.findPopularCommunityRecommendationDetails( listOf(adult.id!!), - includeAdultCommunities = true + includeAdultCommunities = true, + lang = Lang.KO ) val detailById = details.associateBy { it.communityId } @@ -2290,7 +2308,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( val details = repository.findPopularCommunityRecommendationDetails( listOf(viewerBlockedPost.id!!, creatorBlockedPost.id!!, visiblePost.id!!), memberId = viewer.id, - includeAdultCommunities = false + includeAdultCommunities = false, + lang = Lang.KO ) assertEquals(listOf(visiblePost.id), details.map { it.communityId }) @@ -2301,7 +2320,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( fun shouldReturnEmptyPopularCommunityRecommendationDetailsWhenIdsAreEmpty() { assertEquals( emptyList(), - repository.findPopularCommunityRecommendationDetails(emptyList(), includeAdultCommunities = false) + repository.findPopularCommunityRecommendationDetails(emptyList(), includeAdultCommunities = false, lang = Lang.KO) ) } @@ -2347,7 +2366,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, includeAdultGenres = false, genreLimit = 2, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(2, recommendations.size) @@ -2377,7 +2397,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, includeAdultGenres = false, genreLimit = 1, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(listOf(theme.id), recommendations.map { it.genreId }) @@ -2417,7 +2438,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, includeAdultGenres = false, genreLimit = 1, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(listOf(fallbackTheme.id), recommendations.map { it.genreId }) @@ -2446,7 +2468,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, includeAdultGenres = false, genreLimit = 1, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) val creatorIds = recommendations.single().creators.map { it.creatorId } @@ -2477,7 +2500,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, includeAdultGenres = false, genreLimit = 1, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) val creatorIds = recommendations.single().creators.map { it.creatorId } @@ -2500,7 +2524,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = null, includeAdultGenres = false, genreLimit = 1, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(theme.id, recommendations.single().genreId) @@ -2551,7 +2576,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, includeAdultGenres = false, genreLimit = 2, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(2, recommendations.size) @@ -2589,7 +2615,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = null, includeAdultGenres = false, genreLimit = 5, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(true, recommendations.size >= 5) @@ -2633,7 +2660,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = viewer.id!!, includeAdultGenres = false, genreLimit = 5, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(true, recommendations.size >= 5) @@ -2664,7 +2692,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = null, includeAdultGenres = false, genreLimit = 2, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(3, recommendations.size) @@ -2695,7 +2724,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = null, includeAdultGenres = false, genreLimit = 1, - creatorLimit = 2 + creatorLimit = 2, + lang = Lang.KO ) assertEquals(1, recommendations.size) @@ -2724,13 +2754,15 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( memberId = null, includeAdultGenres = false, genreLimit = 5, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) val visible = repository.findGenreCreatorRecommendations( memberId = null, includeAdultGenres = true, genreLimit = 5, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(false, hidden.any { it.genreId == adultTheme.id }) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt index bbc96032..a17a7de0 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.v2.recommendation.application import kr.co.vividnext.sodalive.i18n.Lang -import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent @@ -32,12 +31,10 @@ import java.time.ZoneOffset class HomeRecommendationQueryServiceTest { private val port = FakeHomeRecommendationQueryPort() private val snapshotPort = FakeHomeRecommendationSnapshotPort() - private val langContext = LangContext() private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java) private val service = HomeRecommendationQueryService( port, snapshotPort, - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) @@ -79,7 +76,7 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("홈 라이브 추천은 기본 20개를 최신순 조회 포트에 위임한다") fun shouldFindLatestLiveRecommendationsWithDefaultLimit() { - val recommendations = service.findLiveRecommendations(memberId = 100L) + val recommendations = service.findLiveRecommendations(memberId = 100L, lang = Lang.KO) assertEquals(20, port.liveLimit) assertEquals(100L, port.liveMemberId) @@ -95,7 +92,8 @@ class HomeRecommendationQueryServiceTest { memberId = 100L, includeAdultLives = true, isViewerCreator = true, - effectiveViewerGender = Gender.MALE + effectiveViewerGender = Gender.MALE, + lang = Lang.KO ) assertEquals(40, port.liveOffset) @@ -131,7 +129,7 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("최근 활동 크리에이터는 기본 10개를 최신 활동 조회 포트에 위임한다") fun shouldFindRecentlyActiveCreatorsWithDefaultLimit() { - val creators = service.findRecentlyActiveCreators(memberId = 100L) + val creators = service.findRecentlyActiveCreators(memberId = 100L, lang = Lang.KO) assertEquals(10, port.activeCreatorLimit) assertEquals(100L, port.activeCreatorMemberId) @@ -142,7 +140,12 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("최근 활동 크리에이터는 성인 활동 노출 여부를 조회 포트에 위임한다") fun shouldFindRecentlyActiveCreatorsWithAdultVisibilityPolicy() { - val creators = service.findRecentlyActiveCreators(limit = 8, memberId = 101L, includeAdultActivities = true) + val creators = service.findRecentlyActiveCreators( + limit = 8, + memberId = 101L, + includeAdultActivities = true, + lang = Lang.KO + ) assertEquals(8, port.activeCreatorLimit) assertEquals(101L, port.activeCreatorMemberId) @@ -155,7 +158,7 @@ class HomeRecommendationQueryServiceTest { fun shouldFindRecentDebutCreatorsWithDefaultLimitAndNow() { val now = LocalDateTime.of(2026, 5, 31, 10, 0) - val creators = service.findRecentDebutCreators(now, memberId = 102L) + val creators = service.findRecentDebutCreators(now, memberId = 102L, lang = Lang.KO) assertEquals(now, port.recentDebutNow) assertEquals(102L, port.recentDebutMemberId) @@ -168,7 +171,7 @@ class HomeRecommendationQueryServiceTest { fun shouldFindFirstAudioContentsWithDefaultLimitAndNow() { val now = LocalDateTime.of(2026, 5, 31, 10, 0) - val contents = service.findFirstAudioContents(now, memberId = 103L) + val contents = service.findFirstAudioContents(now, memberId = 103L, lang = Lang.KO) assertEquals(now, port.firstAudioNow) assertEquals(103L, port.firstAudioMemberId) @@ -185,7 +188,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, fallback, - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59) @@ -293,7 +295,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, fallback, - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) port.aiCharacterDetails = listOf( @@ -324,7 +325,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, fallback, - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) @@ -346,7 +346,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, fallback, - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) snapshotPort.replaceSnapshots( @@ -375,7 +374,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, fallback, - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) @@ -408,7 +406,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, fallback, - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) @@ -435,7 +432,6 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) @@ -459,7 +455,7 @@ class HomeRecommendationQueryServiceTest { ) ) - val creators = service.findCheerCreatorRecommendations(memberId = 104L) + val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO) assertEquals(null, fallback.offset) assertEquals(null, fallback.limit) @@ -480,7 +476,6 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) port.cheerCreatorDetails = listOf( @@ -491,7 +486,7 @@ class HomeRecommendationQueryServiceTest { ) ) - val creators = service.findCheerCreatorRecommendations(memberId = 104L) + val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO) assertEquals(0L, fallback.offset) assertEquals(16, fallback.limit) @@ -511,7 +506,6 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59) @@ -528,7 +522,7 @@ class HomeRecommendationQueryServiceTest { ) ) - val creators = service.findCheerCreatorRecommendations(memberId = 104L) + val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO) assertEquals(0L, fallback.offset) assertEquals(16, fallback.limit) @@ -548,7 +542,6 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) snapshotPort.replaceSnapshots( @@ -557,7 +550,7 @@ class HomeRecommendationQueryServiceTest { emptyList() ) - val creators = service.findCheerCreatorRecommendations(memberId = 104L) + val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO) assertEquals(null, fallback.offset) assertEquals(null, fallback.limit) @@ -572,7 +565,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) @@ -631,7 +623,11 @@ class HomeRecommendationQueryServiceTest { ) ) - val communities = service.findPopularCommunityRecommendations(memberId = 105L, includeAdultCommunities = true) + val communities = service.findPopularCommunityRecommendations( + memberId = 105L, + includeAdultCommunities = true, + lang = Lang.KO + ) assertEquals((1L..11L).toList(), port.popularCommunityDetailIds) assertEquals(105L, port.popularCommunityMemberId) @@ -652,7 +648,6 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) @@ -681,7 +676,7 @@ class HomeRecommendationQueryServiceTest { ) } - val communities = service.findPopularCommunityRecommendations() + val communities = service.findPopularCommunityRecommendations(lang = Lang.KO) assertEquals(20, port.popularCommunityDetailIds.size) assertEquals(10, communities.size) @@ -692,7 +687,6 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("인기 커뮤니티 추천은 최종 노출 게시물만 일본어 표시 본문으로 일괄 치환하고 원문 폴백을 유지한다") fun shouldTranslateOnlyFinalPopularCommunityRecommendationsInOneJapaneseBatch() { - langContext.setLang(Lang.JA) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.POPULAR_COMMUNITY, @@ -718,11 +712,10 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) - val communities = translatedService.findPopularCommunityRecommendations(limit = 2) + val communities = translatedService.findPopularCommunityRecommendations(limit = 2, lang = Lang.JA) assertEquals(listOf(1L, 3L), communities.map { it.communityId }) assertEquals(listOf("translated-1-ja", "current-original-3"), communities.map { it.content }) @@ -741,7 +734,6 @@ class HomeRecommendationQueryServiceTest { snapshotPort, popularCommunitySnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59) @@ -768,7 +760,7 @@ class HomeRecommendationQueryServiceTest { ) ) - val communities = service.findPopularCommunityRecommendations(memberId = 105L) + val communities = service.findPopularCommunityRecommendations(memberId = 105L, lang = Lang.KO) assertEquals(0L, fallback.offset) assertEquals(20, fallback.limit) @@ -788,7 +780,6 @@ class HomeRecommendationQueryServiceTest { snapshotPort, popularCommunitySnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext, creatorCommunityTranslationService = creatorCommunityTranslationService ) snapshotPort.replaceSnapshots( @@ -797,7 +788,7 @@ class HomeRecommendationQueryServiceTest { emptyList() ) - val communities = service.findPopularCommunityRecommendations(memberId = 105L) + val communities = service.findPopularCommunityRecommendations(memberId = 105L, lang = Lang.KO) assertEquals(null, fallback.offset) assertEquals(null, fallback.limit) @@ -809,8 +800,11 @@ class HomeRecommendationQueryServiceTest { @DisplayName("최신 스냅샷이 없으면 AI 캐릭터/최근 응원/인기 커뮤니티 추천은 빈 배열을 반환한다") fun shouldReturnEmptyListWhenLatestSnapshotsDoNotExist() { assertEquals(emptyList(), service.findAiCharacterRecommendations()) - assertEquals(emptyList(), service.findCheerCreatorRecommendations()) - assertEquals(emptyList(), service.findPopularCommunityRecommendations()) + assertEquals(emptyList(), service.findCheerCreatorRecommendations(lang = Lang.KO)) + assertEquals( + emptyList(), + service.findPopularCommunityRecommendations(lang = Lang.KO) + ) } @Test @@ -858,7 +852,8 @@ class HomeRecommendationQueryServiceTest { val recommendations = service.findGenreCreatorRecommendations( memberId = 100L, - includeAdultGenres = true + includeAdultGenres = true, + lang = Lang.KO ) assertEquals(100L, port.genreCreatorMemberId) @@ -912,7 +907,8 @@ class HomeRecommendationQueryServiceTest { memberId = 100L, includeAdultGenres = false, genreLimit = 2, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(listOf(1L, 3L), recommendations.map { it.genreId }) @@ -958,7 +954,8 @@ class HomeRecommendationQueryServiceTest { memberId = null, includeAdultGenres = false, genreLimit = 2, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(listOf(1L, 3L), recommendations.map { it.genreId }) @@ -1009,7 +1006,8 @@ class HomeRecommendationQueryServiceTest { memberId = null, includeAdultGenres = false, genreLimit = 2, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(listOf(1L, 3L), recommendations.map { it.genreId }) @@ -1038,7 +1036,8 @@ class HomeRecommendationQueryServiceTest { memberId = 100L, includeAdultGenres = false, genreLimit = 5, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(listOf(1L), recommendations.map { it.genreId }) @@ -1085,7 +1084,8 @@ class HomeRecommendationQueryServiceTest { memberId = 100L, includeAdultGenres = false, genreLimit = 2, - creatorLimit = 8 + creatorLimit = 8, + lang = Lang.KO ) assertEquals(listOf(1L, 2L), recommendations.map { it.genreId }) @@ -1130,6 +1130,7 @@ class HomeRecommendationQueryServiceTest { var genreCreatorCreatorLimit: Int? = null val liveRecommendations = listOf( HomeLiveRecommendationRecord( + creatorMemberId = 10L, liveRoomId = 1L, creatorNickname = "creator", creatorProfileImage = "profile.png", @@ -1196,7 +1197,8 @@ class HomeRecommendationQueryServiceTest { memberId: Long?, includeAdultLives: Boolean, isViewerCreator: Boolean, - effectiveViewerGender: Gender? + effectiveViewerGender: Gender?, + lang: Lang ): List { liveOffset = offset liveLimit = limit @@ -1223,7 +1225,8 @@ class HomeRecommendationQueryServiceTest { override fun findRecentlyActiveCreators( limit: Int, memberId: Long?, - includeAdultActivities: Boolean + includeAdultActivities: Boolean, + lang: Lang ): List { activeCreatorLimit = limit activeCreatorMemberId = memberId @@ -1236,7 +1239,8 @@ class HomeRecommendationQueryServiceTest { offset: Long, limit: Int, memberId: Long?, - includeAdultContents: Boolean + includeAdultContents: Boolean, + lang: Lang ): List { recentDebutNow = now recentDebutOffset = offset @@ -1299,7 +1303,8 @@ class HomeRecommendationQueryServiceTest { override fun findCheerCreatorRecommendationDetails( creatorIds: List, - memberId: Long? + memberId: Long?, + lang: Lang ): List { cheerCreatorDetailIds = creatorIds cheerCreatorMemberId = memberId @@ -1309,7 +1314,8 @@ class HomeRecommendationQueryServiceTest { override fun findPopularCommunityRecommendationDetails( communityIds: List, memberId: Long?, - includeAdultCommunities: Boolean + includeAdultCommunities: Boolean, + lang: Lang ): List { popularCommunityDetailIds = communityIds popularCommunityMemberId = memberId @@ -1321,7 +1327,8 @@ class HomeRecommendationQueryServiceTest { memberId: Long?, includeAdultGenres: Boolean, genreLimit: Int, - creatorLimit: Int + creatorLimit: Int, + lang: Lang ): List { genreCreatorMemberId = memberId genreCreatorIncludeAdultGenres = includeAdultGenres diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatNameJoinRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatNameJoinRepositoryTest.kt new file mode 100644 index 00000000..5e98f09c --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatNameJoinRepositoryTest.kt @@ -0,0 +1,167 @@ +package kr.co.vividnext.sodalive.v2.usercreatorchat + +import kr.co.vividnext.sodalive.chat.character.ChatCharacter +import kr.co.vividnext.sodalive.chat.room.ChatMessage +import kr.co.vividnext.sodalive.chat.room.ChatParticipant +import kr.co.vividnext.sodalive.chat.room.ChatRoom +import kr.co.vividnext.sodalive.chat.room.ParticipantType +import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository +import kr.co.vividnext.sodalive.configs.QueryDslConfig +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.following.CreatorFollowing +import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService +import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository +import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository +import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository +import org.hibernate.SessionFactory +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +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.data.domain.PageRequest +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 UserCreatorChatNameJoinRepositoryTest @Autowired constructor( + private val roomRepository: UserCreatorChatRoomRepository, + private val messageRepository: UserCreatorChatMessageRepository, + private val participantRepository: UserCreatorChatParticipantRepository, + private val aiRoomRepository: ChatRoomRepository, + private val entityManager: EntityManager, + private val jdbcTemplate: JdbcTemplate +) { + @ParameterizedTest + @EnumSource(Lang::class) + @DisplayName("DM 목록·히스토리·상대 프로필은 요청 Lang을 조인하고 USER·미등록·비활성 행을 유지한다") + fun shouldJoinRequestedNamesWithoutStandaloneNameQueries(lang: Lang) { + val viewer = member("viewer", MemberRole.USER) + val opponents = listOf( + member("creator", MemberRole.CREATOR), + member("inactive", MemberRole.CREATOR, false), + member("missing", MemberRole.CREATOR, null), + member("user", MemberRole.USER) + ) + val rooms = opponents.map { dmRoom(viewer, it) } + entityManager.flush() + entityManager.clear() + val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics + statistics.clear() + + val listed = roomRepository.findDmChatListRooms( + viewer.id!!, + null, + null, + null, + false, + lang, + PageRequest.of(0, 20) + ) + + val expectedCreator = if (lang == Lang.KO) "creator" else "${lang.name}-creator" + assertEquals(setOf(expectedCreator, "inactive", "missing", "user"), listed.map { it.targetName }.toSet()) + assertEquals(4, listed.size) + assertEquals(1L, statistics.queryExecutionCount) + assertEquals(0L, statistics.entityLoadCount) + rooms.zip(opponents).forEach { (room, opponent) -> + statistics.clear() + val expectedName = if (opponent.id == opponents.first().id) expectedCreator else opponent.nickname + val messages = messageRepository.findMessageHistory(room, null, lang, PageRequest.of(0, 20)) + assertEquals(listOf(expectedName, "viewer"), messages.map { it.senderNickname }) + assertEquals(listOf("opponent body", "viewer body"), messages.map { it.textMessage }) + assertEquals(1L, statistics.queryExecutionCount) + assertEquals(0L, statistics.entityLoadCount) + + statistics.clear() + val previous = messageRepository.findMessageHistory(room, messages.first().messageId, lang, PageRequest.of(0, 20)) + assertEquals(listOf(messages.last().messageId), previous.map { it.messageId }) + assertEquals(1L, statistics.queryExecutionCount) + + statistics.clear() + val profile = participantRepository.findActiveOpponentProfile(room.id!!, viewer.id!!, lang) + assertEquals(expectedName, profile!!.nickname) + assertEquals(1L, statistics.queryExecutionCount) + assertEquals(0L, statistics.entityLoadCount) + } + } + + @Test + @DisplayName("통합 채팅 목록은 DM만 EN 별칭을 조인하고 AI 캐릭터 이름·팔로우 필터·커서를 유지한다") + fun shouldKeepAiCharacterNameAndFollowingCursorWhenJoiningDmNames() { + val viewer = member("viewer", MemberRole.USER) + val creator = member("creator", MemberRole.CREATOR) + val room = dmRoom(viewer, creator) + val character = ChatCharacter("join-character", "AI character", "description", systemPrompt = "prompt").apply { + creatorMember = creator + } + entityManager.persist(character) + val aiRoom = ChatRoom("join-ai-session", "AI room") + entityManager.persist(aiRoom) + entityManager.persist(ChatParticipant(aiRoom, ParticipantType.USER, member = viewer)) + val characterParticipant = ChatParticipant(aiRoom, ParticipantType.CHARACTER, character = character) + entityManager.persist(characterParticipant) + entityManager.persist(ChatMessage("AI body", aiRoom, characterParticipant)) + entityManager.persist( + CreatorFollowing().apply { + member = viewer + this.creator = creator + } + ) + entityManager.flush() + entityManager.clear() + val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics + statistics.clear() + val service = ChatRoomListService(aiRoomRepository, roomRepository, "https://cdn.test") + + val first = service.getRooms(viewer, Lang.EN, limit = 1, followedCreatorsOnly = true) + val second = service.getRooms(viewer, Lang.EN, cursor = first.nextCursor, limit = 1, followedCreatorsOnly = true) + + assertEquals(true, first.hasMore) + assertEquals(false, second.hasMore) + assertEquals(listOf("AI character", "EN-creator"), (first.rooms + second.rooms).map { it.targetName }) + assertEquals(listOf(aiRoom.id, room.id), (first.rooms + second.rooms).map { it.roomId }) + assertEquals(4L, statistics.queryExecutionCount, statistics.queries.joinToString("\n")) + assertEquals(0L, statistics.entityLoadCount) + } + + private fun member(nickname: String, role: MemberRole, activeAlias: Boolean? = true): Member { + val member = Member(password = "pw", nickname = nickname, role = role) + entityManager.persist(member) + if (activeAlias != null) { + listOf(Lang.JA, Lang.EN).forEach { lang -> + 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.name, + "${lang.name}-$nickname", + activeAlias + ) + } + } + return member + } + + private fun dmRoom(viewer: Member, opponent: Member): UserCreatorChatRoom { + val room = roomRepository.save(UserCreatorChatRoom()) + val mine = participantRepository.save(UserCreatorChatParticipant(room, viewer)) + val theirs = participantRepository.save(UserCreatorChatParticipant(room, opponent)) + val now = LocalDateTime.now() + messageRepository.save(UserCreatorChatMessage(room, mine, UserCreatorChatMessageType.TEXT, "viewer body")) + .createdAt = now.minusMinutes(2) + messageRepository.save(UserCreatorChatMessage(room, theirs, UserCreatorChatMessageType.TEXT, "opponent body")) + .createdAt = now.minusMinutes(1) + messageRepository.save( + UserCreatorChatMessage(room, theirs, UserCreatorChatMessageType.TEXT, "inactive body", isActive = false) + ) + return room + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceIntegrationTest.kt index cff277d0..14502a92 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceIntegrationTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceIntegrationTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberKind import kr.co.vividnext.sodalive.member.MemberRepository @@ -112,7 +113,7 @@ class UserCreatorChatServiceIntegrationTest @Autowired constructor( entityManager.clear() val exception = assertThrows(SodaException::class.java) { - service.sendTextMessageByWebSocket(user.id!!, room.id!!, "hello") + service.sendTextMessageByWebSocket(user.id!!, room.id!!, "hello", Lang.KO) } assertEquals("message.error.recipient_not_found", exception.messageKey) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt index 5ef9bd4b..aae9b6cc 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt @@ -6,11 +6,16 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType +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.MemberRepository import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto +import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageRow +import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatOpponentProfile import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository @@ -70,6 +75,8 @@ class UserCreatorChatServiceTest { applicationEventPublisher = eventPublisher, objectMapper = ObjectMapper(), s3Uploader = s3Uploader, + localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java), + langContext = LangContext(), bucket = "test-bucket", cloudFrontHost = "https://cdn.test" ) @@ -140,13 +147,15 @@ class UserCreatorChatServiceTest { } val room = room(10L) val userParticipant = participant(100L, room, user) - val creatorParticipant = participant(101L, room, creator) Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room) Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant) - Mockito.`when`(participantRepository.findActiveOpponent(10L, 1L)).thenReturn(creatorParticipant) + Mockito.`when`(participantRepository.findActiveOpponentProfile(10L, 1L, Lang.KO)) + .thenReturn(UserCreatorChatOpponentProfile(creator.nickname, creator.profileImage)) Mockito.`when`( - messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc( + messageRepository.findMessageHistory( room, + null, + Lang.KO, PageRequest.of(0, 20) ) ).thenReturn(emptyList()) @@ -159,7 +168,7 @@ class UserCreatorChatServiceTest { assertEquals(emptyList(), response.messages) assertFalse(response.hasMore) assertEquals(null, response.nextCursor) - Mockito.verify(participantRepository).findActiveOpponent(10L, 1L) + Mockito.verify(participantRepository).findActiveOpponentProfile(10L, 1L, Lang.KO) } @Test @@ -169,13 +178,15 @@ class UserCreatorChatServiceTest { val creator = member(2L, "creator") val room = room(10L) val userParticipant = participant(100L, room, user) - val creatorParticipant = participant(101L, room, creator) Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room) Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant) - Mockito.`when`(participantRepository.findActiveOpponent(10L, 1L)).thenReturn(creatorParticipant) + Mockito.`when`(participantRepository.findActiveOpponentProfile(10L, 1L, Lang.KO)) + .thenReturn(UserCreatorChatOpponentProfile(creator.nickname, creator.profileImage)) Mockito.`when`( - messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc( + messageRepository.findMessageHistory( room, + null, + Lang.KO, PageRequest.of(0, 20) ) ).thenReturn(emptyList()) @@ -202,7 +213,7 @@ class UserCreatorChatServiceTest { (invocation.arguments[0] as UserCreatorChatMessage).apply { id = 203L } } - val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello") + val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello", lang = Lang.KO) assertEquals(203L, response.messageId) assertEquals("hello", response.textMessage) @@ -230,7 +241,7 @@ class UserCreatorChatServiceTest { (invocation.arguments[0] as UserCreatorChatMessage).apply { id = 204L } } - service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello") + service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello", lang = Lang.KO) val eventCaptor = ArgumentCaptor.forClass(FcmEvent::class.java) Mockito.verify(eventPublisher).publishEvent(eventCaptor.capture()) @@ -263,7 +274,7 @@ class UserCreatorChatServiceTest { (invocation.arguments[0] as UserCreatorChatMessage).apply { id = 207L } } - val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello") + val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello", lang = Lang.KO) assertEquals(207L, response.messageId) val eventCaptor = ArgumentCaptor.forClass(FcmEvent::class.java) @@ -421,12 +432,13 @@ class UserCreatorChatServiceTest { Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room) Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant) Mockito.`when`( - messageRepository.findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc( + messageRepository.findMessageHistory( room, 300L, + Lang.KO, PageRequest.of(0, 20) ) - ).thenReturn(listOf(newerMessage, olderMessage)) + ).thenReturn(listOf(messageRow(newerMessage), messageRow(olderMessage))) Mockito.`when`(messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, 298L)).thenReturn(true) val response = service.getMessages(user, roomId = 10L, cursor = 300L) @@ -435,15 +447,30 @@ class UserCreatorChatServiceTest { assertEquals(listOf("older", "newer"), response.messages.map { it.textMessage }) assertTrue(response.hasMore) assertEquals(298L, response.nextCursor) - Mockito.verify(messageRepository).findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc( + Mockito.verify(messageRepository).findMessageHistory( room, 300L, + Lang.KO, PageRequest.of(0, 20) ) } private fun member(id: Long, nickname: String) = Member(password = "pw", nickname = nickname).apply { this.id = id } + private fun messageRow(message: UserCreatorChatMessage): UserCreatorChatMessageRow { + val sender = message.participant.member + return UserCreatorChatMessageRow( + message.id!!, + message.messageType, + message.createdAt, + message.textMessage, + message.voiceMessage, + sender.id!!, + sender.nickname, + sender.profileImage + ) + } + private fun anyMessageItem(): UserCreatorChatMessageItemDto { return Mockito.any(UserCreatorChatMessageItemDto::class.java) ?: UserCreatorChatMessageItemDto( messageId = 0L, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatLocalizedNameIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatLocalizedNameIntegrationTest.kt new file mode 100644 index 00000000..f393955d --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatLocalizedNameIntegrationTest.kt @@ -0,0 +1,367 @@ +package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket + +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +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.jwt.TokenProvider +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberAdapter +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.member.token.MemberTokenRepository +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessage +import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType +import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository +import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository +import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository +import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService +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.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +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.boot.test.web.client.TestRestTemplate +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.server.ServletServerHttpRequest +import org.springframework.http.server.ServletServerHttpResponse +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockMultipartFile +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.support.TransactionTemplate +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import org.springframework.web.socket.TextMessage +import org.springframework.web.socket.WebSocketHttpHeaders +import org.springframework.web.socket.WebSocketSession +import org.springframework.web.socket.client.standard.StandardWebSocketClient +import org.springframework.web.socket.handler.TextWebSocketHandler +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.net.URI +import java.util.UUID +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = [ + "spring.cache.type=none", + "jwt.secret=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZg==" + ] +) +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class UserCreatorChatLocalizedNameIntegrationTest @Autowired constructor( + private val tokenProvider: TokenProvider, + private val tokenRepository: MemberTokenRepository, + private val authInterceptor: UserCreatorChatWebSocketAuthInterceptor, + private val handler: UserCreatorChatWebSocketHandler, + private val service: UserCreatorChatService, + private val memberRepository: MemberRepository, + private val roomRepository: UserCreatorChatRoomRepository, + private val participantRepository: UserCreatorChatParticipantRepository, + private val messageRepository: UserCreatorChatMessageRepository, + private val objectMapper: ObjectMapper, + private val jdbcTemplate: JdbcTemplate, + private val transactionTemplate: TransactionTemplate, + private val restTemplate: TestRestTemplate, + private val langContext: LangContext +) { + @LocalServerPort + private var port: Int = 0 + + @MockBean + private lateinit var fcmService: FcmService + + @MockBean + private lateinit var s3Uploader: S3Uploader + + private lateinit var creator: Member + private lateinit var recipient: Member + private lateinit var creatorToken: String + private lateinit var recipientToken: String + private var roomId: Long = 0 + private val connections = mutableListOf() + + @BeforeEach + fun createCommittedFixture() { + transactionTemplate.executeWithoutResult { + val suffix = UUID.randomUUID().toString() + creator = memberRepository.saveAndFlush( + Member( + email = "creator-$suffix@localized-ws.test", + password = "pw", + nickname = "original-creator", + role = MemberRole.CREATOR + ) + ) + recipient = memberRepository.saveAndFlush( + Member(email = "recipient-$suffix@localized-ws.test", password = "pw", nickname = "original-user") + ) + 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)", + creator.id!! + ) + roomId = service.createOrGetRoom(creator, recipient.id!!).roomId + } + creatorToken = token(creator) + recipientToken = token(recipient) + } + + @AfterEach + fun closeConnectionsAndRemoveTokens() { + connections.forEach { it.session.close() } + tokenRepository.deleteById(creator.id!!) + tokenRepository.deleteById(recipient.id!!) + } + + @ParameterizedTest + @CsvSource("ja-JP, JA", "ko-KR, KO", "en-US, EN", "fr-FR, KO") + @DisplayName("실제 JWT handshake는 Accept-Language를 Lang 세션 속성으로 저장한다") + fun shouldStoreExplicitLanguageOnAuthenticatedHandshake(header: String, expected: Lang) { + val request = MockHttpServletRequest().apply { + addHeader(HttpHeaders.AUTHORIZATION, "Bearer $recipientToken") + addHeader(HttpHeaders.ACCEPT_LANGUAGE, header) + } + val attributes = mutableMapOf() + + val accepted = authInterceptor.beforeHandshake( + ServletServerHttpRequest(request), + ServletServerHttpResponse(MockHttpServletResponse()), + handler, + attributes + ) + + assertTrue(accepted) + assertEquals(recipient.id, attributes[UserCreatorChatWebSocketAuthInterceptor.MEMBER_ID_ATTRIBUTE]) + assertEquals(expected, attributes["lang"]) + } + + @Test + @DisplayName("Accept-Language가 없는 실제 인증 handshake는 KO 세션으로 연결한다") + fun shouldDefaultHandshakeLanguageToKoreanWhenHeaderIsMissing() { + val request = MockHttpServletRequest().apply { addHeader(HttpHeaders.AUTHORIZATION, "Bearer $recipientToken") } + val attributes = mutableMapOf() + + val accepted = authInterceptor.beforeHandshake( + ServletServerHttpRequest(request), + ServletServerHttpResponse(MockHttpServletResponse()), + handler, + attributes + ) + + assertTrue(accepted) + assertEquals(Lang.KO, attributes["lang"]) + } + + @Test + @DisplayName("localhost WS와 Redis를 거친 같은 회원의 KO JA 세션은 이름만 다르고 원본 본문과 ID를 공유한다") + fun shouldDeliverDifferentNamesToSimultaneousSessionsOfTheSameRecipient() { + val korean = connect(recipientToken, "ko-KR") + val japanese = connect(recipientToken, "ja-JP") + val sender = connect(creatorToken, "ja-JP") + join(korean) + join(japanese) + join(sender) + sendText(sender, "original-creator 본문", "send-multilang") + + val koreanMessage = receive(korean, "MESSAGE")["payload"] + val japaneseMessage = receive(japanese, "MESSAGE")["payload"] + val ack = receive(sender, "SEND_ACK") + + assertAll( + { assertEquals("original-creator", koreanMessage["senderNickname"].asText()) }, + { assertEquals("日本のクリエイター", japaneseMessage["senderNickname"].asText()) }, + { assertEquals("original-creator 본문", koreanMessage["textMessage"].asText()) }, + { assertEquals("original-creator 본문", japaneseMessage["textMessage"].asText()) }, + { assertEquals(koreanMessage["messageId"], japaneseMessage["messageId"]) }, + { assertEquals(creator.id!!, japaneseMessage["senderId"].asLong()) }, + { assertEquals(false, japaneseMessage["mine"].asBoolean()) }, + { assertEquals("send-multilang", ack["requestId"].asText()) }, + { assertEquals("original-creator", memberRepository.findById(creator.id!!).orElseThrow().nickname) } + ) + } + + @Test + @DisplayName("JA 발신자 SEND_ACK는 KO 수신자 전달과 독립적으로 현지화하고 mine과 requestId를 유지한다") + fun shouldRenderSenderAckInSenderSessionLanguageIndependentlyOfRecipient() { + val receiver = connect(recipientToken, "ko-KR") + val sender = connect(creatorToken, "ja-JP") + join(receiver) + join(sender) + sendText(sender, "ACK 본문", "sender-ja") + + val received = receive(receiver, "MESSAGE")["payload"] + val ack = receive(sender, "SEND_ACK") + + assertAll( + { assertEquals("日本のクリエイター", ack["payload"]["senderNickname"].asText()) }, + { assertEquals("original-creator", received["senderNickname"].asText()) }, + { assertEquals(true, ack["payload"]["mine"].asBoolean()) }, + { assertEquals(false, received["mine"].asBoolean()) }, + { assertEquals(received["messageId"], ack["payload"]["messageId"]) }, + { assertEquals("sender-ja", ack["requestId"].asText()) } + ) + } + + @Test + @DisplayName("HTTP open과 메시지 페이지는 현재 요청 언어를 사용하고 이전 JA 응답을 후속 KO 조회가 바꾸지 않는다") + fun shouldRenderHttpRoomAndMessageNamesAtEachRequestBoundary() { + transactionTemplate.executeWithoutResult { + val room = roomRepository.findById(roomId).orElseThrow() + val participant = participantRepository.findActiveByRoomIdAndMemberId(roomId, creator.id!!)!! + messageRepository.saveAndFlush( + UserCreatorChatMessage( + chatRoom = room, + participant = participant, + messageType = UserCreatorChatMessageType.TEXT, + textMessage = "original-creator text" + ) + ) + } + + val japanese = getRoomResource("open", "ja-JP") + val korean = getRoomResource("open", "ko-KR") + val messages = getRoomResource("messages", "ja-JP") + + assertAll( + { assertEquals("日本のクリエイター", japanese["opponentNickname"].asText()) }, + { assertEquals("日本のクリエイター", japanese["messages"][0]["senderNickname"].asText()) }, + { assertEquals("original-creator", korean["opponentNickname"].asText()) }, + { assertEquals("original-creator", korean["messages"][0]["senderNickname"].asText()) }, + { assertEquals("日本のクリエイター", messages["messages"][0]["senderNickname"].asText()) }, + { assertEquals("original-creator text", japanese["messages"][0]["textMessage"].asText()) } + ) + } + + @Test + @DisplayName("음성 REST 응답의 JA 이름과 KO WebSocket 전달은 같은 저장 메시지를 독립 렌더링한다") + fun shouldLocalizeVoiceHttpResponseWithoutLocalizingNeutralPublishedPayload() { + val receiver = connect(recipientToken, "ko-KR") + join(receiver) + Mockito.`when`( + s3Uploader.upload( + Mockito.any(InputStream::class.java) ?: ByteArrayInputStream(byteArrayOf()), + Mockito.anyString(), + Mockito.anyString(), + Mockito.any(ObjectMetadata::class.java) + ) + ).thenReturn("voice/localized.aac") + RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest())) + try { + langContext.setLang(Lang.JA) + val response = service.sendVoiceMessage( + creator, + roomId, + MockMultipartFile("voiceMessageFile", "voice.aac", "audio/aac", byteArrayOf(1, 2)), + "{}" + ) + val delivered = receive(receiver, "MESSAGE")["payload"] + + assertEquals("日本のクリエイター", response.message.senderNickname) + assertEquals("original-creator", delivered["senderNickname"].asText()) + assertEquals(response.message.messageId, delivered["messageId"].asLong()) + assertEquals(response.message.voiceMessageUrl, delivered["voiceMessageUrl"].asText()) + assertTrue(response.deliveredRealtime) + assertEquals(false, response.pushSent) + } finally { + RequestContextHolder.resetRequestAttributes() + } + } + + private fun token(member: Member): String { + val principal = MemberAdapter(member) + return tokenProvider.createToken( + UsernamePasswordAuthenticationToken(principal, "pw", principal.authorities), + member.id!! + ) + } + + private fun connect(token: String, language: String): ClientConnection { + val messages = LinkedBlockingQueue() + val clientHandler = object : TextWebSocketHandler() { + override fun handleTextMessage(session: WebSocketSession, message: TextMessage) { + messages.add(objectMapper.readTree(message.payload)) + } + } + val headers = WebSocketHttpHeaders().apply { + setBearerAuth(token) + set(HttpHeaders.ACCEPT_LANGUAGE, language) + } + val session = StandardWebSocketClient().doHandshake( + clientHandler, + headers, + URI("ws://localhost:$port${UserCreatorChatWebSocketConfig.ENDPOINT}") + ).get(5, TimeUnit.SECONDS) + return ClientConnection(session, messages).also { connections.add(it) } + } + + private fun join(connection: ClientConnection) { + connection.session.sendMessage(TextMessage("""{"type":"JOIN_ROOM","roomId":$roomId,"payload":{}}""")) + receive(connection, "JOINED") + } + + private fun sendText(connection: ClientConnection, text: String, requestId: String) { + connection.session.sendMessage( + TextMessage( + objectMapper.writeValueAsString( + mapOf( + "type" to "SEND_TEXT", + "requestId" to requestId, + "roomId" to roomId, + "payload" to mapOf("textMessage" to text) + ) + ) + ) + ) + } + + private fun receive(connection: ClientConnection, expectedType: String): JsonNode { + val envelope = connection.messages.poll(5, TimeUnit.SECONDS) + assertNotNull(envelope, "Expected $expectedType from localhost WebSocket") + assertEquals(expectedType, envelope!!["type"].asText(), envelope.toString()) + return envelope + } + + private fun getRoomResource(resource: String, language: String): JsonNode { + val headers = HttpHeaders().apply { + setBearerAuth(recipientToken) + set(HttpHeaders.ACCEPT_LANGUAGE, language) + } + val response = restTemplate.exchange( + "http://localhost:$port/api/v2/user-creator-chat/rooms/$roomId/$resource", + HttpMethod.GET, + HttpEntity(headers), + String::class.java + ) + assertEquals(HttpStatus.OK, response.statusCode, response.body) + val body = objectMapper.readTree(response.body) + assertTrue(body["success"].asBoolean(), response.body) + return body["data"] + } + + private data class ClientConnection(val session: WebSocketSession, val messages: LinkedBlockingQueue) +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRedisIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRedisIntegrationTest.kt index 92326b37..f318a3c3 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRedisIntegrationTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRedisIntegrationTest.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer import kr.co.vividnext.sodalive.support.EmbeddedRedisTestConfiguration +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse @@ -14,6 +15,7 @@ import org.mockito.ArgumentCaptor 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.redis.core.StringRedisTemplate import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource @@ -35,6 +37,9 @@ import java.util.concurrent.TimeUnit @ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) @TestPropertySource(properties = ["user-creator-chat.websocket.server-id=redis-test-server"]) class UserCreatorChatRedisIntegrationTest { + @MockBean + private lateinit var localizedNameService: CreatorLocalizedNameService + @Autowired private lateinit var stringRedisTemplate: StringRedisTemplate diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt index 7deeb828..404a4649 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -25,7 +26,8 @@ class UserCreatorChatRoomMessageBrokerTest { stringRedisTemplate = stringRedisTemplate, sessionRegistry = registry, objectMapper = objectMapper, - listenerContainer = listenerContainer + listenerContainer = listenerContainer, + localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java) ) @Test diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt index 086aaba7..ef4071e4 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService import org.junit.jupiter.api.Assertions.assertEquals @@ -66,7 +67,7 @@ class UserCreatorChatWebSocketHandlerTest { @DisplayName("SEND_TEXT는 메시지 저장 후 sender에게 SEND_ACK를 응답한다") fun shouldSendAckToSenderWhenTextMessageIsSaved() { val session = session("session-1", memberId = 1L) - Mockito.`when`(service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello")) + Mockito.`when`(service.sendTextMessageByWebSocket(1L, 10L, "hello", Lang.KO)) .thenReturn(messageItem()) handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}")) Mockito.clearInvocations(session) @@ -103,7 +104,8 @@ class UserCreatorChatWebSocketHandlerTest { Mockito.verify(service, Mockito.never()).sendTextMessageByWebSocket( Mockito.anyLong(), Mockito.anyLong(), - Mockito.anyString() + Mockito.anyString(), + Mockito.eq(Lang.KO) ?: Lang.KO ) }