feat(creatorname): v2 조회와 채팅에 언어별 이름을 적용한다

This commit is contained in:
2026-10-16 14:05:04 +09:00
parent eef2e2bf55
commit e264f5d062
154 changed files with 2777 additions and 989 deletions
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.content.all.adapter.`in`.web package kr.co.vividnext.sodalive.v2.api.content.all.adapter.`in`.web
import kr.co.vividnext.sodalive.common.ApiResponse 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.member.Member
import kr.co.vividnext.sodalive.v2.api.content.all.application.MainContentAllFacade import kr.co.vividnext.sodalive.v2.api.content.all.application.MainContentAllFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -12,7 +13,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/audio/contents") @RequestMapping("/api/v2/audio/contents")
class MainContentAllController( class MainContentAllController(
private val facade: MainContentAllFacade private val facade: MainContentAllFacade,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getContents( fun getContents(
@@ -30,7 +32,8 @@ class MainContentAllController(
dayOfWeek = dayOfWeek, dayOfWeek = dayOfWeek,
page = page, page = page,
size = size, size = size,
member = member member = member,
lang = langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.all.application 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.Member
import kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponse import kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponse
import kr.co.vividnext.sodalive.v2.content.all.application.MainContentAllQueryService import kr.co.vividnext.sodalive.v2.content.all.application.MainContentAllQueryService
@@ -15,17 +16,18 @@ class MainContentAllFacade(
dayOfWeek: String?, dayOfWeek: String?,
page: Int?, page: Int?,
size: Int?, size: Int?,
member: Member? member: Member?,
lang: Lang
): MainContentAllTabResponse { ): MainContentAllTabResponse {
return MainContentAllTabResponse.from( val contents = queryService.getContents(
queryService.getContents( type = type,
type = type, sort = sort,
sort = sort, dayOfWeek = dayOfWeek,
dayOfWeek = dayOfWeek, page = page,
page = page, size = size,
size = size, member = member,
member = member lang = lang
)
) )
return MainContentAllTabResponse.from(contents)
} }
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.content.overview.application.ContentOverviewFacade import kr.co.vividnext.sodalive.v2.api.content.overview.application.ContentOverviewFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/contents") @RequestMapping("/api/v2/contents")
class ContentOverviewController( class ContentOverviewController(
private val facade: ContentOverviewFacade private val facade: ContentOverviewFacade,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getContents( fun getContents(
@@ -22,7 +24,7 @@ class ContentOverviewController(
@RequestParam(required = false) size: Int?, @RequestParam(required = false) size: Int?,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = 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 { private fun requireMember(member: Member?): Member {
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.overview.application 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.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewItemResponse import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewItemResponse
@@ -21,42 +22,46 @@ class ContentOverviewFacade(
private val cloudFrontHost: String, private val cloudFrontHost: String,
private val queryPolicy: ContentOverviewQueryPolicy = ContentOverviewQueryPolicy() 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 resolvedType = queryPolicy.resolveType(type)
val resolvedPage = queryPolicy.createPage(page, size) val resolvedPage = queryPolicy.createPage(page, size)
return when (resolvedType) { return when (resolvedType) {
ContentOverviewType.NEW_AND_HOT_AUDIO -> getNewAndHotContents(member, resolvedPage) ContentOverviewType.NEW_AND_HOT_AUDIO -> getNewAndHotContents(member, resolvedPage, lang)
ContentOverviewType.FIRST_AUDIO_CONTENT -> getFirstAudioContents(member, resolvedPage) 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( val fetched = audioRecommendationQueryService.findNewAndHotAudios(
member = member, member = member,
offset = page.offset, offset = page.offset,
limit = page.size + 1 limit = page.size + 1,
lang = lang
) )
val items = queryPolicy.pageItems(fetched, page)
return ContentOverviewPageResponse( return ContentOverviewPageResponse(
type = ContentOverviewType.NEW_AND_HOT_AUDIO, type = ContentOverviewType.NEW_AND_HOT_AUDIO,
items = queryPolicy.pageItems(fetched, page).map { ContentOverviewItemResponse.fromNewAndHot(it) }, items = items.map(ContentOverviewItemResponse::fromNewAndHot),
page = page.page, page = page.page,
size = page.size, size = page.size,
hasNext = queryPolicy.hasNext(fetched, page) 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( val fetched = homeRecommendationQueryService.findFirstAudioContents(
now = LocalDateTime.now(), now = LocalDateTime.now(),
offset = page.offset, offset = page.offset,
limit = page.size + 1, limit = page.size + 1,
memberId = member.id, memberId = member.id,
includeAdultContents = memberContentPreferenceService.canViewAdultContent(member) includeAdultContents = memberContentPreferenceService.canViewAdultContent(member),
lang = lang
) )
val items = queryPolicy.pageItems(fetched, page)
return ContentOverviewPageResponse( return ContentOverviewPageResponse(
type = ContentOverviewType.FIRST_AUDIO_CONTENT, type = ContentOverviewType.FIRST_AUDIO_CONTENT,
items = queryPolicy.pageItems(fetched, page).map { items = items.map {
ContentOverviewItemResponse.fromFirstAudioContent( ContentOverviewItemResponse.fromFirstAudioContent(
audio = it, audio = it,
coverImage = it.coverImage.toCdnUrl(cloudFrontHost), coverImage = it.coverImage.toCdnUrl(cloudFrontHost),
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.`in`.web package kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.`in`.web
import kr.co.vividnext.sodalive.common.ApiResponse 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.member.Member
import kr.co.vividnext.sodalive.v2.api.content.ranking.application.AudioRankingFacade import kr.co.vividnext.sodalive.v2.api.content.ranking.application.AudioRankingFacade
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType
@@ -13,13 +14,14 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/audio/rankings") @RequestMapping("/api/v2/audio/rankings")
class AudioRankingController( class AudioRankingController(
private val facade: AudioRankingFacade private val facade: AudioRankingFacade,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getRankings( fun getRankings(
@RequestParam(defaultValue = "WEEKLY_POPULAR") type: AudioRankingType, @RequestParam(defaultValue = "WEEKLY_POPULAR") type: AudioRankingType,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok(facade.getRankings(type, member)) ApiResponse.ok(facade.getRankings(type, member, langContext.lang))
} }
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.ranking.application 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.member.Member
import kr.co.vividnext.sodalive.v2.api.content.ranking.dto.AudioRankingResponse import kr.co.vividnext.sodalive.v2.api.content.ranking.dto.AudioRankingResponse
import kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryService import kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryService
@@ -10,7 +11,8 @@ import org.springframework.stereotype.Component
class AudioRankingFacade( class AudioRankingFacade(
private val queryService: AudioRankingQueryService private val queryService: AudioRankingQueryService
) { ) {
fun getRankings(type: AudioRankingType, member: Member?): AudioRankingResponse { fun getRankings(type: AudioRankingType, member: Member?, lang: Lang): AudioRankingResponse {
return AudioRankingResponse.from(queryService.getRankings(type, member)) val result = queryService.getRankings(type, member, lang)
return AudioRankingResponse.from(result)
} }
} }
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.`in`.web package kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.`in`.web
import kr.co.vividnext.sodalive.common.ApiResponse 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.member.Member
import kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacade import kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -11,12 +12,13 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/audio/recommendations") @RequestMapping("/api/v2/audio/recommendations")
class AudioRecommendationController( class AudioRecommendationController(
private val audioRecommendationFacade: AudioRecommendationFacade private val audioRecommendationFacade: AudioRecommendationFacade,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getRecommendations( fun getRecommendations(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok(audioRecommendationFacade.getRecommendations(member)) ApiResponse.ok(audioRecommendationFacade.getRecommendations(member, langContext.lang))
} }
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.recommendation.application 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.member.Member
import kr.co.vividnext.sodalive.v2.api.content.recommendation.dto.AudioRecommendationsResponse import kr.co.vividnext.sodalive.v2.api.content.recommendation.dto.AudioRecommendationsResponse
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService
@@ -9,7 +10,8 @@ import org.springframework.stereotype.Component
class AudioRecommendationFacade( class AudioRecommendationFacade(
private val queryService: AudioRecommendationQueryService private val queryService: AudioRecommendationQueryService
) { ) {
fun getRecommendations(member: Member?): AudioRecommendationsResponse { fun getRecommendations(member: Member?, lang: Lang): AudioRecommendationsResponse {
return AudioRecommendationsResponse.from(queryService.getRecommendations(member)) val result = queryService.getRecommendations(member, lang)
return AudioRecommendationsResponse.from(result)
} }
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacade import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/creator-channels") @RequestMapping("/api/v2/creator-channels")
class CreatorChannelAudioController( class CreatorChannelAudioController(
private val creatorChannelAudioFacade: CreatorChannelAudioFacade private val creatorChannelAudioFacade: CreatorChannelAudioFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/{creatorId}/audio") @GetMapping("/{creatorId}/audio")
fun getAudioTab( fun getAudioTab(
@@ -32,7 +34,8 @@ class CreatorChannelAudioController(
sort = sort, sort = sort,
themeId = themeId, themeId = themeId,
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelAudioTabResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelAudioTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryService import kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryService
@@ -19,7 +20,8 @@ class CreatorChannelAudioFacade(
themeId: Long?, themeId: Long?,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelAudioTabResponse { ): CreatorChannelAudioTabResponse {
return CreatorChannelAudioTabResponse.from( return CreatorChannelAudioTabResponse.from(
creatorChannelAudioQueryService.getAudioTab( creatorChannelAudioQueryService.getAudioTab(
@@ -29,7 +31,8 @@ class CreatorChannelAudioFacade(
themeId = themeId, themeId = themeId,
page = page, page = page,
size = size, size = size,
now = now now = now,
lang = lang
) )
) )
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacade import kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/creator-channels") @RequestMapping("/api/v2/creator-channels")
class CreatorChannelCommunityController( class CreatorChannelCommunityController(
private val creatorChannelCommunityFacade: CreatorChannelCommunityFacade private val creatorChannelCommunityFacade: CreatorChannelCommunityFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/{creatorId}/community") @GetMapping("/{creatorId}/community")
fun getCommunityTab( fun getCommunityTab(
@@ -28,7 +30,8 @@ class CreatorChannelCommunityController(
creatorId = creatorId, creatorId = creatorId,
viewer = requireMember(member), viewer = requireMember(member),
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -41,7 +44,8 @@ class CreatorChannelCommunityController(
ApiResponse.ok( ApiResponse.ok(
creatorChannelCommunityFacade.getCommunityPostDetail( creatorChannelCommunityFacade.getCommunityPostDetail(
postId = postId, postId = postId,
viewer = requireMember(member) viewer = requireMember(member),
lang = langContext.lang
) )
) )
} }
@@ -58,7 +62,8 @@ class CreatorChannelCommunityController(
postId = postId, postId = postId,
viewer = requireMember(member), viewer = requireMember(member),
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -75,7 +80,8 @@ class CreatorChannelCommunityController(
commentId = commentId, commentId = commentId,
viewer = requireMember(member), viewer = requireMember(member),
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.application 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.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.CreatorChannelCommunityCommentsResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityPostDetailResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityPostDetailResponse
@@ -20,31 +21,33 @@ class CreatorChannelCommunityFacade(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityTabResponse { ): CreatorChannelCommunityTabResponse {
return CreatorChannelCommunityTabResponse.from( val tab = creatorChannelCommunityQueryService.getCommunityTab(
creatorChannelCommunityQueryService.getCommunityTab( creatorId = creatorId,
creatorId = creatorId, viewer = viewer,
viewer = viewer, page = page,
page = page, size = size,
size = size, now = now,
now = now lang = lang
)
) )
return CreatorChannelCommunityTabResponse.from(tab)
} }
fun getCommunityPostDetail( fun getCommunityPostDetail(
postId: Long, postId: Long,
viewer: Member, viewer: Member,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityPostDetailResponse { ): CreatorChannelCommunityPostDetailResponse {
return CreatorChannelCommunityPostDetailResponse.from( val detail = creatorChannelCommunityQueryService.getCommunityPostDetail(
creatorChannelCommunityQueryService.getCommunityPostDetail( postId = postId,
postId = postId, viewer = viewer,
viewer = viewer, now = now,
now = now lang = lang
)
) )
return CreatorChannelCommunityPostDetailResponse.from(detail)
} }
fun getCommunityComments( fun getCommunityComments(
@@ -52,17 +55,18 @@ class CreatorChannelCommunityFacade(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityCommentsResponse { ): CreatorChannelCommunityCommentsResponse {
return CreatorChannelCommunityCommentsResponse.from( val comments = creatorChannelCommunityQueryService.getCommunityComments(
creatorChannelCommunityQueryService.getCommunityComments( postId = postId,
postId = postId, viewer = viewer,
viewer = viewer, page = page,
page = page, size = size,
size = size, now = now,
now = now lang = lang
)
) )
return CreatorChannelCommunityCommentsResponse.from(comments)
} }
fun getCommunityReplies( fun getCommunityReplies(
@@ -70,16 +74,17 @@ class CreatorChannelCommunityFacade(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityRepliesResponse { ): CreatorChannelCommunityRepliesResponse {
return CreatorChannelCommunityRepliesResponse.from( val replies = creatorChannelCommunityQueryService.getCommunityReplies(
creatorChannelCommunityQueryService.getCommunityReplies( commentId = commentId,
commentId = commentId, viewer = viewer,
viewer = viewer, page = page,
page = page, size = size,
size = size, now = now,
now = now lang = lang
)
) )
return CreatorChannelCommunityRepliesResponse.from(replies)
} }
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacade import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/creator-channels") @RequestMapping("/api/v2/creator-channels")
class CreatorChannelDonationController( class CreatorChannelDonationController(
private val creatorChannelDonationFacade: CreatorChannelDonationFacade private val creatorChannelDonationFacade: CreatorChannelDonationFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/{creatorId}/donations") @GetMapping("/{creatorId}/donations")
fun getDonationTab( fun getDonationTab(
@@ -28,7 +30,8 @@ class CreatorChannelDonationController(
creatorId = creatorId, creatorId = creatorId,
viewer = requireMember(member), viewer = requireMember(member),
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse 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.creator.channel.donation.application.CreatorChannelDonationQueryService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -10,22 +12,29 @@ import java.time.LocalDateTime
@Service @Service
@Transactional(readOnly = true) @Transactional(readOnly = true)
class CreatorChannelDonationFacade( class CreatorChannelDonationFacade(
private val creatorChannelDonationQueryService: CreatorChannelDonationQueryService private val creatorChannelDonationQueryService: CreatorChannelDonationQueryService,
private val creatorLocalizedNameService: CreatorLocalizedNameService
) { ) {
fun getDonationTab( fun getDonationTab(
creatorId: Long, creatorId: Long,
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelDonationTabResponse { ): 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( return CreatorChannelDonationTabResponse.from(
creatorChannelDonationQueryService.getDonationTab( tab.copy(
creatorId = creatorId, rankings = tab.rankings.map { it.copy(nickname = names[it.userId] ?: it.nickname) }
viewer = viewer,
page = page,
size = size,
now = now
) )
) )
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application.CreatorChannelFanTalkFacade import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application.CreatorChannelFanTalkFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/creator-channels") @RequestMapping("/api/v2/creator-channels")
class CreatorChannelFanTalkController( class CreatorChannelFanTalkController(
private val creatorChannelFanTalkFacade: CreatorChannelFanTalkFacade private val creatorChannelFanTalkFacade: CreatorChannelFanTalkFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/{creatorId}/fan-talks") @GetMapping("/{creatorId}/fan-talks")
fun getFanTalkTab( fun getFanTalkTab(
@@ -28,7 +30,8 @@ class CreatorChannelFanTalkController(
creatorId = creatorId, creatorId = creatorId,
viewer = requireMember(member), viewer = requireMember(member),
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application.CreatorChannelFanTalkQueryService import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application.CreatorChannelFanTalkQueryService
@@ -17,16 +18,17 @@ class CreatorChannelFanTalkFacade(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelFanTalkTabResponse { ): CreatorChannelFanTalkTabResponse {
return CreatorChannelFanTalkTabResponse.from( val tab = creatorChannelFanTalkQueryService.getFanTalkTab(
creatorChannelFanTalkQueryService.getFanTalkTab( creatorId = creatorId,
creatorId = creatorId, viewer = viewer,
viewer = viewer, page = page,
page = page, size = size,
size = size, now = now,
now = now lang = lang
)
) )
return CreatorChannelFanTalkTabResponse.from(tab)
} }
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.home.application.CreatorChannelHomeFacade import kr.co.vividnext.sodalive.v2.api.creator.channel.home.application.CreatorChannelHomeFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/creator-channels") @RequestMapping("/api/v2/creator-channels")
class CreatorChannelHomeController( class CreatorChannelHomeController(
private val creatorChannelHomeFacade: CreatorChannelHomeFacade private val creatorChannelHomeFacade: CreatorChannelHomeFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/{creatorId}/home") @GetMapping("/{creatorId}/home")
fun getHome( fun getHome(
@@ -23,7 +25,8 @@ class CreatorChannelHomeController(
ApiResponse.ok( ApiResponse.ok(
creatorChannelHomeFacade.getHome( creatorChannelHomeFacade.getHome(
creatorId = creatorId, creatorId = creatorId,
viewer = requireMember(member) viewer = requireMember(member),
lang = langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.home.application 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.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.home.dto.CreatorChannelHomeResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.home.dto.CreatorChannelHomeResponse
import kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryService import kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryService
@@ -16,14 +17,10 @@ class CreatorChannelHomeFacade(
fun getHome( fun getHome(
creatorId: Long, creatorId: Long,
viewer: Member, viewer: Member,
now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC) now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
lang: Lang
): CreatorChannelHomeResponse { ): CreatorChannelHomeResponse {
return CreatorChannelHomeResponse.from( val home = creatorChannelHomeQueryService.getHome(creatorId = creatorId, viewer = viewer, now = now, lang = lang)
creatorChannelHomeQueryService.getHome( return CreatorChannelHomeResponse.from(home)
creatorId = creatorId,
viewer = viewer,
now = now
)
)
} }
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacade import kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacade
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
@@ -15,7 +16,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/creator-channels") @RequestMapping("/api/v2/creator-channels")
class CreatorChannelLiveController( class CreatorChannelLiveController(
private val creatorChannelLiveFacade: CreatorChannelLiveFacade private val creatorChannelLiveFacade: CreatorChannelLiveFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/{creatorId}/live") @GetMapping("/{creatorId}/live")
fun getLiveTab( fun getLiveTab(
@@ -31,7 +33,8 @@ class CreatorChannelLiveController(
viewer = requireMember(member), viewer = requireMember(member),
sort = sort, sort = sort,
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.live.application 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.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveTabResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveTabResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
@@ -19,7 +20,8 @@ class CreatorChannelLiveFacade(
sort: ContentSort, sort: ContentSort,
page: Int, page: Int,
size: Int, size: Int,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelLiveTabResponse { ): CreatorChannelLiveTabResponse {
return CreatorChannelLiveTabResponse.from( return CreatorChannelLiveTabResponse.from(
creatorChannelLiveQueryService.getLiveTab( creatorChannelLiveQueryService.getLiveTab(
@@ -28,7 +30,8 @@ class CreatorChannelLiveFacade(
sort = sort, sort = sort,
page = page, page = page,
size = size, size = size,
now = now now = now,
lang = lang
) )
) )
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.series.application.CreatorChannelSeriesFacade import kr.co.vividnext.sodalive.v2.api.creator.channel.series.application.CreatorChannelSeriesFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -14,7 +15,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/creator-channels") @RequestMapping("/api/v2/creator-channels")
class CreatorChannelSeriesController( class CreatorChannelSeriesController(
private val creatorChannelSeriesFacade: CreatorChannelSeriesFacade private val creatorChannelSeriesFacade: CreatorChannelSeriesFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/{creatorId}/series") @GetMapping("/{creatorId}/series")
fun getSeriesTab( fun getSeriesTab(
@@ -30,7 +32,8 @@ class CreatorChannelSeriesController(
viewer = requireMember(member), viewer = requireMember(member),
sort = sort, sort = sort,
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.series.application 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.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.series.application.CreatorChannelSeriesQueryService import kr.co.vividnext.sodalive.v2.creator.channel.series.application.CreatorChannelSeriesQueryService
@@ -18,7 +19,8 @@ class CreatorChannelSeriesFacade(
sort: String?, sort: String?,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelSeriesTabResponse { ): CreatorChannelSeriesTabResponse {
return CreatorChannelSeriesTabResponse.from( return CreatorChannelSeriesTabResponse.from(
creatorChannelSeriesQueryService.getSeriesTab( creatorChannelSeriesQueryService.getSeriesTab(
@@ -27,7 +29,8 @@ class CreatorChannelSeriesFacade(
sort = sort, sort = sort,
page = page, page = page,
size = size, size = size,
now = now now = now,
lang = lang
) )
) )
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.gift.dto.GiftApplicationRequest import kr.co.vividnext.sodalive.v2.api.gift.dto.GiftApplicationRequest
import kr.co.vividnext.sodalive.v2.api.gift.dto.GiftApplicationResponse 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") @RequestMapping("/api/v2/gifts")
class GiftController( class GiftController(
private val queryService: GiftQueryService, private val queryService: GiftQueryService,
private val commandService: GiftCommandService private val commandService: GiftCommandService,
private val langContext: LangContext
) { ) {
@GetMapping("/form-options") @GetMapping("/form-options")
fun getFormOptions() = ApiResponse.ok(GiftFormOptionsResponse.from(queryService.getFormOptions())) fun getFormOptions() = ApiResponse.ok(GiftFormOptionsResponse.from(queryService.getFormOptions()))
@@ -49,7 +51,8 @@ class GiftController(
memberId = member?.id ?: throw SodaException(messageKey = "common.error.bad_credentials"), memberId = member?.id ?: throw SodaException(messageKey = "common.error.bad_credentials"),
type = type, type = type,
page = page, page = page,
size = size size = size,
lang = langContext.lang
) )
) )
) )
@@ -62,7 +65,8 @@ class GiftController(
GiftDetailResponse.from( GiftDetailResponse.from(
queryService.getGiftDetail( queryService.getGiftDetail(
memberId = member?.id ?: throw SodaException(messageKey = "common.error.bad_credentials"), memberId = member?.id ?: throw SodaException(messageKey = "common.error.bad_credentials"),
applicationNo = applicationNo applicationNo = applicationNo,
lang = langContext.lang
) )
) )
) )
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.home.adapter.`in`.web package kr.co.vividnext.sodalive.v2.api.home.adapter.`in`.web
import kr.co.vividnext.sodalive.common.ApiResponse 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.member.Member
import kr.co.vividnext.sodalive.v2.api.home.application.HomeCreatorRankingFacade import kr.co.vividnext.sodalive.v2.api.home.application.HomeCreatorRankingFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -11,12 +12,13 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/home/rankings") @RequestMapping("/api/v2/home/rankings")
class CreatorRankingController( class CreatorRankingController(
private val homeCreatorRankingFacade: HomeCreatorRankingFacade private val homeCreatorRankingFacade: HomeCreatorRankingFacade,
private val langContext: LangContext
) { ) {
@GetMapping("/creators") @GetMapping("/creators")
fun getCreatorRankings( fun getCreatorRankings(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok(homeCreatorRankingFacade.getCreatorRankings(member)) ApiResponse.ok(homeCreatorRankingFacade.getCreatorRankings(member, langContext.lang))
} }
} }
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacade import kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacade
import kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.FollowRecommendedCreatorsRequest 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") @RequestMapping("/api/v2/home/recommendations")
class HomeRecommendationController( class HomeRecommendationController(
private val homeRecommendationFacade: HomeRecommendationFacade, private val homeRecommendationFacade: HomeRecommendationFacade,
private val recommendedCreatorFollowService: RecommendedCreatorFollowService private val recommendedCreatorFollowService: RecommendedCreatorFollowService,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getHomeRecommendations( fun getHomeRecommendations(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok(homeRecommendationFacade.getHomeRecommendations(member)) ApiResponse.ok(homeRecommendationFacade.getHomeRecommendations(member, langContext.lang))
} }
@GetMapping("/lives") @GetMapping("/lives")
@@ -37,7 +39,8 @@ class HomeRecommendationController(
homeRecommendationFacade.getLives( homeRecommendationFacade.getLives(
requireMember(member), requireMember(member),
normalizePage(page), normalizePage(page),
normalizeSize(size) normalizeSize(size),
langContext.lang
) )
) )
} }
@@ -52,7 +55,8 @@ class HomeRecommendationController(
homeRecommendationFacade.getRecentDebutCreators( homeRecommendationFacade.getRecentDebutCreators(
requireMember(member), requireMember(member),
normalizePage(page), normalizePage(page),
normalizeSize(size) normalizeSize(size),
langContext.lang
) )
) )
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.home.application 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.member.Member
import kr.co.vividnext.sodalive.v2.api.home.dto.ranking.CreatorRankingResponse import kr.co.vividnext.sodalive.v2.api.home.dto.ranking.CreatorRankingResponse
import kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryService import kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryService
@@ -9,9 +10,8 @@ import org.springframework.stereotype.Component
class HomeCreatorRankingFacade( class HomeCreatorRankingFacade(
private val creatorRankingQueryService: CreatorRankingQueryService private val creatorRankingQueryService: CreatorRankingQueryService
) { ) {
fun getCreatorRankings(member: Member?): CreatorRankingResponse { fun getCreatorRankings(member: Member?, lang: Lang): CreatorRankingResponse {
return CreatorRankingResponse.from( val result = creatorRankingQueryService.getCreatorRankings(viewerMemberId = member?.id, lang = lang)
creatorRankingQueryService.getCreatorRankings(viewerMemberId = member?.id) return CreatorRankingResponse.from(result)
)
} }
} }
@@ -44,21 +44,23 @@ class HomeRecommendationFacade(
) { ) {
private val log = LoggerFactory.getLogger(javaClass) private val log = LoggerFactory.getLogger(javaClass)
fun getHomeRecommendations(member: Member?): HomeRecommendationResponse { fun getHomeRecommendations(member: Member?, lang: Lang): HomeRecommendationResponse {
val startedAt = System.currentTimeMillis() val startedAt = System.currentTimeMillis()
return runCatching { return runCatching {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val includeAdult = resolveAdultVisibility(member) val includeAdult = resolveAdultVisibility(member)
val bannerLang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO val bannerLang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
HomeRecommendationResponse( val lives = queryService.findLiveRecommendations(
lives = queryService.findLiveRecommendations( limit = HOME_LIVE_LIMIT,
limit = HOME_LIVE_LIMIT, memberId = member?.id,
memberId = member?.id, includeAdultLives = includeAdult,
includeAdultLives = includeAdult, isViewerCreator = member?.role == MemberRole.CREATOR,
isViewerCreator = member?.role == MemberRole.CREATOR, effectiveViewerGender = member?.effectiveGender(),
effectiveViewerGender = member?.effectiveGender() lang = lang
).map { it.toItem() }, )
val response = HomeRecommendationResponse(
lives = lives.map { it.toItem() },
banners = queryService.findHomeBanners( banners = queryService.findHomeBanners(
limit = HOME_BANNER_LIMIT, limit = HOME_BANNER_LIMIT,
memberId = member?.id, memberId = member?.id,
@@ -69,21 +71,24 @@ class HomeRecommendationFacade(
recentlyActiveCreators = queryService.findRecentlyActiveCreators( recentlyActiveCreators = queryService.findRecentlyActiveCreators(
HOME_ACTIVE_CREATOR_LIMIT, HOME_ACTIVE_CREATOR_LIMIT,
member?.id, member?.id,
includeAdult includeAdult,
lang
) )
.map { it.toItem() }, .map { it.toItem() },
recentDebutCreators = queryService.findRecentDebutCreators( recentDebutCreators = queryService.findRecentDebutCreators(
now, now,
limit = HOME_RECENT_DEBUT_CREATOR_LIMIT, limit = HOME_RECENT_DEBUT_CREATOR_LIMIT,
memberId = member?.id, memberId = member?.id,
includeAdultContents = includeAdult includeAdultContents = includeAdult,
lang = lang
) )
.map { it.toItem() }, .map { it.toItem() },
firstAudioContents = queryService.findFirstAudioContents( firstAudioContents = queryService.findFirstAudioContents(
now, now,
limit = HOME_FIRST_AUDIO_CONTENT_LIMIT, limit = HOME_FIRST_AUDIO_CONTENT_LIMIT,
memberId = member?.id, memberId = member?.id,
includeAdultContents = includeAdult includeAdultContents = includeAdult,
lang = lang
) )
.map { it.toItem() }, .map { it.toItem() },
aiCharacters = queryService.findAiCharacterRecommendations( aiCharacters = queryService.findAiCharacterRecommendations(
@@ -94,16 +99,19 @@ class HomeRecommendationFacade(
memberId = member?.id, memberId = member?.id,
includeAdultGenres = includeAdult, includeAdultGenres = includeAdult,
genreLimit = HOME_GENRE_CREATOR_GENRE_LIMIT, genreLimit = HOME_GENRE_CREATOR_GENRE_LIMIT,
creatorLimit = HOME_GENRE_CREATOR_CREATOR_LIMIT creatorLimit = HOME_GENRE_CREATOR_CREATOR_LIMIT,
lang = lang
).map { it.toItem() }, ).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() }, .map { it.toCreatorItem() },
popularCommunityPosts = queryService.findPopularCommunityRecommendations( popularCommunityPosts = queryService.findPopularCommunityRecommendations(
limit = HOME_POPULAR_COMMUNITY_LIMIT, limit = HOME_POPULAR_COMMUNITY_LIMIT,
memberId = member?.id, memberId = member?.id,
includeAdultCommunities = includeAdult includeAdultCommunities = includeAdult,
lang = lang
).map { it.toItem() } ).map { it.toItem() }
) )
response
}.onSuccess { response -> }.onSuccess { response ->
log.info( log.info(
"event=home_recommendations_query_success memberId={} elapsedMs={} emptySections={}", "event=home_recommendations_query_success memberId={} elapsedMs={} emptySections={}",
@@ -122,7 +130,7 @@ class HomeRecommendationFacade(
}.getOrThrow() }.getOrThrow()
} }
fun getLives(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeLiveItem> { fun getLives(member: Member, page: Int, size: Int, lang: Lang): HomeRecommendationPageResponse<HomeLiveItem> {
val startedAt = System.currentTimeMillis() val startedAt = System.currentTimeMillis()
return runCatching { return runCatching {
val fetched = queryService.findLiveRecommendations( val fetched = queryService.findLiveRecommendations(
@@ -131,7 +139,8 @@ class HomeRecommendationFacade(
memberId = member.id, memberId = member.id,
includeAdultLives = resolveAdultVisibility(member), includeAdultLives = resolveAdultVisibility(member),
isViewerCreator = member.role == MemberRole.CREATOR, isViewerCreator = member.role == MemberRole.CREATOR,
effectiveViewerGender = member.effectiveGender() effectiveViewerGender = member.effectiveGender(),
lang = lang
) )
fetched.toPage(page, size) { it.toItem() } fetched.toPage(page, size) { it.toItem() }
}.onSuccess { }.onSuccess {
@@ -141,7 +150,12 @@ class HomeRecommendationFacade(
}.getOrThrow() }.getOrThrow()
} }
fun getRecentDebutCreators(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeCreatorItem> { fun getRecentDebutCreators(
member: Member,
page: Int,
size: Int,
lang: Lang
): HomeRecommendationPageResponse<HomeCreatorItem> {
val startedAt = System.currentTimeMillis() val startedAt = System.currentTimeMillis()
return runCatching { return runCatching {
val fetched = queryService.findRecentDebutCreators( val fetched = queryService.findRecentDebutCreators(
@@ -149,7 +163,8 @@ class HomeRecommendationFacade(
offset = page.toOffset(size), offset = page.toOffset(size),
limit = size + 1, limit = size + 1,
memberId = member.id, memberId = member.id,
includeAdultContents = resolveAdultVisibility(member) includeAdultContents = resolveAdultVisibility(member),
lang = lang
) )
fetched.toPage(page, size) { it.toItem() } fetched.toPage(page, size) { it.toItem() }
}.onSuccess { }.onSuccess {
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.home.following.adapter.`in`.web package kr.co.vividnext.sodalive.v2.api.home.following.adapter.`in`.web
import kr.co.vividnext.sodalive.common.ApiResponse 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.member.Member
import kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacade import kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -11,12 +12,13 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/home/following") @RequestMapping("/api/v2/home/following")
class HomeFollowingController( class HomeFollowingController(
private val facade: HomeFollowingFacade private val facade: HomeFollowingFacade,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getFollowingTab( fun getFollowingTab(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok(facade.getFollowingTab(member)) ApiResponse.ok(facade.getFollowingTab(member, langContext.lang))
} }
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.home.following.application 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.Member
import kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponse import kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponse
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
@@ -11,14 +12,15 @@ class HomeFollowingFacade(
private val homeFollowingQueryService: HomeFollowingQueryService, private val homeFollowingQueryService: HomeFollowingQueryService,
private val chatRoomListService: ChatRoomListService private val chatRoomListService: ChatRoomListService
) { ) {
fun getFollowingTab(member: Member?): HomeFollowingTabResponse { fun getFollowingTab(member: Member?, lang: Lang): HomeFollowingTabResponse {
if (member == null) { if (member == null) {
return HomeFollowingTabResponse.loginRequired() return HomeFollowingTabResponse.loginRequired()
} }
val home = homeFollowingQueryService.findHomeFollowing(member) val home = homeFollowingQueryService.findHomeFollowing(member, lang)
val recentChats = chatRoomListService.getRooms( val recentChats = chatRoomListService.getRooms(
member, member,
lang = lang,
filter = "ALL", filter = "ALL",
cursor = null, cursor = null,
limit = 10, limit = 10,
@@ -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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException 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.member.Member
import kr.co.vividnext.sodalive.v2.api.home.live.application.HomeOnAirLiveFacade import kr.co.vividnext.sodalive.v2.api.home.live.application.HomeOnAirLiveFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -13,14 +14,15 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/home/on-air-lives") @RequestMapping("/api/v2/home/on-air-lives")
class HomeOnAirLiveController( class HomeOnAirLiveController(
private val homeOnAirLiveFacade: HomeOnAirLiveFacade private val homeOnAirLiveFacade: HomeOnAirLiveFacade,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getOnAirLives( fun getOnAirLives(
@RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "0") page: Int,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok(homeOnAirLiveFacade.getOnAirLives(requireMember(member), page)) ApiResponse.ok(homeOnAirLiveFacade.getOnAirLives(requireMember(member), page, langContext.lang))
} }
private fun requireMember(member: Member?): Member { private fun requireMember(member: Member?): Member {
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.home.live.application 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.Gender
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
@@ -20,7 +21,7 @@ class HomeOnAirLiveFacade(
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String 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 normalizedPage = page.coerceIn(0, MAX_PAGE)
val fetched = queryService.findLiveRecommendations( val fetched = queryService.findLiveRecommendations(
offset = normalizedPage.toLong() * PAGE_SIZE, offset = normalizedPage.toLong() * PAGE_SIZE,
@@ -28,9 +29,11 @@ class HomeOnAirLiveFacade(
memberId = member.id, memberId = member.id,
includeAdultLives = memberContentPreferenceService.canViewAdultContent(member), includeAdultLives = memberContentPreferenceService.canViewAdultContent(member),
isViewerCreator = member.role == MemberRole.CREATOR, 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( return HomeOnAirLivePageResponse(
items = items, items = items,
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.chat.controller package kr.co.vividnext.sodalive.v2.chat.controller
import kr.co.vividnext.sodalive.common.ApiResponse 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.member.Member
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListPageResponse import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListPageResponse
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
@@ -13,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/v2/chat/rooms") @RequestMapping("/api/v2/chat/rooms")
class ChatRoomListController( class ChatRoomListController(
private val service: ChatRoomListService private val service: ChatRoomListService,
private val langContext: LangContext
) { ) {
@GetMapping @GetMapping
fun getRooms( fun getRooms(
@@ -23,6 +25,6 @@ class ChatRoomListController(
@RequestParam(defaultValue = "30") limit: Int @RequestParam(defaultValue = "30") limit: Int
) = run { ) = run {
if (member == null) return@run ApiResponse.ok(ChatRoomListPageResponse(emptyList(), false, null)) 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))
} }
} }
@@ -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.ChatMessageType
import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository
import kr.co.vividnext.sodalive.common.SodaException 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.Member
import kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.toUtcIso import kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.toUtcIso
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
@@ -26,6 +27,7 @@ class ChatRoomListService(
) { ) {
fun getRooms( fun getRooms(
member: Member, member: Member,
lang: Lang,
filter: String = ChatRoomListFilter.ALL.name, filter: String = ChatRoomListFilter.ALL.name,
cursor: String? = null, cursor: String? = null,
limit: Int = DEFAULT_LIMIT, limit: Int = DEFAULT_LIMIT,
@@ -50,6 +52,7 @@ class ChatRoomListService(
parsedCursor?.chatType, parsedCursor?.chatType,
parsedCursor?.roomId, parsedCursor?.roomId,
followedCreatorsOnly, followedCreatorsOnly,
lang,
pageable pageable
) )
} }
@@ -70,6 +73,7 @@ class ChatRoomListService(
parsedCursor?.chatType, parsedCursor?.chatType,
parsedCursor?.roomId, parsedCursor?.roomId,
followedCreatorsOnly, followedCreatorsOnly,
lang,
pageable pageable
) )
} }
@@ -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.QSeries.series
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent 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.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
import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember 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.common.domain.toCdnUrl
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllAudio 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.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.beans.factory.annotation.Value
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -64,7 +66,10 @@ class DefaultMainContentAllQueryRepository(
onlyFree: Boolean, onlyFree: Boolean,
onlyPointAvailable: Boolean onlyPointAvailable: Boolean
): List<MainContentAllAudio> { ): List<MainContentAllAudio> {
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() if (rows.isEmpty()) return emptyList()
val contentIds = rows.map { it.get(audioContent.id)!! } val contentIds = rows.map { it.get(audioContent.id)!! }
@@ -85,7 +90,8 @@ class DefaultMainContentAllQueryRepository(
isPointAvailable = row.get(audioContent.isPointAvailable)!!, isPointAvailable = row.get(audioContent.isPointAvailable)!!,
isFirstContent = firstContentIdByCreatorId[creatorId] == contentId, isFirstContent = firstContentIdByCreatorId[creatorId] == contentId,
isOriginalSeries = originalSeriesByContentId[contentId] ?: false, 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() if (seriesIds.isEmpty()) return emptyList()
val seriesTranslation = QSeriesTranslation("mainContentAllSeriesTranslation") val seriesTranslation = QSeriesTranslation("mainContentAllSeriesTranslation")
val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "mainContentAllSeriesCreatorName")
return queryFactory return queryFactory
.select( .select(
series.id, series.id,
series.title, series.title,
seriesTranslation.renderedPayload, seriesTranslation.renderedPayload,
series.coverImage, series.coverImage,
member.nickname, member.id,
creatorName.displayName,
series.isOriginal, series.isOriginal,
series.isAdult series.isAdult
) )
.from(series) .from(series)
.join(series.member, member) .join(series.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(seriesTranslation) .leftJoin(seriesTranslation)
.on( .on(
seriesTranslation.seriesId.eq(series.id), seriesTranslation.seriesId.eq(series.id),
@@ -148,9 +157,10 @@ class DefaultMainContentAllQueryRepository(
seriesId = row.get(series.id)!!, seriesId = row.get(series.id)!!,
title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: row.get(series.title)!!, title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: row.get(series.title)!!,
coverImageUrl = row.get(series.coverImage).toCdnUrl(cloudFrontHost), coverImageUrl = row.get(series.coverImage).toCdnUrl(cloudFrontHost),
creatorNickname = row.get(member.nickname)!!, creatorNickname = row.get(creatorName.displayName)!!,
isOriginal = row.get(series.isOriginal)!!, 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, limit: Int,
onlyPaid: Boolean, onlyPaid: Boolean,
onlyFree: Boolean, onlyFree: Boolean,
onlyPointAvailable: Boolean onlyPointAvailable: Boolean,
creatorName: CreatorNameJoin
): List<Tuple> { ): List<Tuple> {
val query = queryFactory val query = queryFactory
.select( .select(
@@ -175,11 +186,12 @@ class DefaultMainContentAllQueryRepository(
audioContent.isAdult, audioContent.isAdult,
audioContent.isPointAvailable, audioContent.isPointAvailable,
audioContent.member.id, audioContent.member.id,
member.nickname, creatorName.displayName,
audioContent.releaseDate audioContent.releaseDate
) )
.from(audioContent) .from(audioContent)
.join(audioContent.member, member) .join(audioContent.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.join(audioContent.theme, audioContentTheme) .join(audioContent.theme, audioContentTheme)
.where(audioCondition(memberId, canViewAdultContent, now, onlyPaid, onlyFree, onlyPointAvailable)) .where(audioCondition(memberId, canViewAdultContent, now, onlyPaid, onlyFree, onlyPointAvailable))
@@ -192,7 +204,7 @@ class DefaultMainContentAllQueryRepository(
revenueOrder.audioContent.id.eq(audioContent.id), revenueOrder.audioContent.id.eq(audioContent.id),
revenueOrder.isActive.isTrue revenueOrder.isActive.isTrue
) )
.groupByAudioRow() .groupByAudioRow(creatorName)
.orderBy( .orderBy(
revenueOrder.can.sum().coalesce(0).desc(), revenueOrder.can.sum().coalesce(0).desc(),
audioContent.releaseDate.desc(), audioContent.releaseDate.desc(),
@@ -301,7 +313,7 @@ class DefaultMainContentAllQueryRepository(
return query.offset(offset).limit(limit.toLong()).fetch() return query.offset(offset).limit(limit.toLong()).fetch()
} }
private fun JPAQuery<Tuple>.groupByAudioRow(): JPAQuery<Tuple> { private fun JPAQuery<Tuple>.groupByAudioRow(creatorName: CreatorNameJoin): JPAQuery<Tuple> {
return groupBy( return groupBy(
audioContent.id, audioContent.id,
audioContent.title, audioContent.title,
@@ -311,6 +323,7 @@ class DefaultMainContentAllQueryRepository(
audioContent.isPointAvailable, audioContent.isPointAvailable,
audioContent.member.id, audioContent.member.id,
member.nickname, member.nickname,
creatorName.name.displayName,
audioContent.releaseDate audioContent.releaseDate
) )
} }
@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.v2.content.all.application package kr.co.vividnext.sodalive.v2.content.all.application
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek 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.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
@@ -19,8 +19,7 @@ import java.time.LocalDateTime
class MainContentAllQueryService( class MainContentAllQueryService(
private val queryPort: MainContentAllQueryPort, private val queryPort: MainContentAllQueryPort,
private val memberContentPreferenceService: MemberContentPreferenceService, private val memberContentPreferenceService: MemberContentPreferenceService,
private val queryPolicy: MainContentAllQueryPolicy = MainContentAllQueryPolicy(), private val queryPolicy: MainContentAllQueryPolicy = MainContentAllQueryPolicy()
private val langContext: LangContext
) { ) {
fun getContents( fun getContents(
type: String?, type: String?,
@@ -28,7 +27,8 @@ class MainContentAllQueryService(
dayOfWeek: String?, dayOfWeek: String?,
page: Int?, page: Int?,
size: Int?, size: Int?,
member: Member? member: Member?,
lang: Lang
): MainContentAll { ): MainContentAll {
val resolvedType = queryPolicy.resolveType(type) val resolvedType = queryPolicy.resolveType(type)
val resolvedSort = queryPolicy.resolveSort(sort) val resolvedSort = queryPolicy.resolveSort(sort)
@@ -47,6 +47,7 @@ class MainContentAllQueryService(
memberId = memberId, memberId = memberId,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
now = now, now = now,
lang = lang,
onlyPaid = true onlyPaid = true
) )
@@ -58,6 +59,7 @@ class MainContentAllQueryService(
memberId = memberId, memberId = memberId,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
now = now, now = now,
lang = lang,
onlyFree = true onlyFree = true
) )
@@ -69,6 +71,7 @@ class MainContentAllQueryService(
memberId = memberId, memberId = memberId,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
now = now, now = now,
lang = lang,
onlyPointAvailable = true onlyPointAvailable = true
) )
@@ -79,7 +82,8 @@ class MainContentAllQueryService(
page = resolvedPage, page = resolvedPage,
memberId = memberId, memberId = memberId,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
now = now now = now,
lang = lang
) )
MainContentAllType.ORIGINAL -> getSeriesContents( MainContentAllType.ORIGINAL -> getSeriesContents(
@@ -90,6 +94,7 @@ class MainContentAllQueryService(
memberId = memberId, memberId = memberId,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
now = now, now = now,
lang = lang,
onlyOriginal = true onlyOriginal = true
) )
} }
@@ -103,6 +108,7 @@ class MainContentAllQueryService(
memberId: Long?, memberId: Long?,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
now: LocalDateTime, now: LocalDateTime,
lang: Lang,
onlyPaid: Boolean = false, onlyPaid: Boolean = false,
onlyFree: Boolean = false, onlyFree: Boolean = false,
onlyPointAvailable: Boolean = false onlyPointAvailable: Boolean = false
@@ -122,7 +128,7 @@ class MainContentAllQueryService(
sort = sort, sort = sort,
offset = page.offset, offset = page.offset,
limit = page.size + 1, limit = page.size + 1,
locale = langContext.lang.code, locale = lang.code,
onlyPaid = onlyPaid, onlyPaid = onlyPaid,
onlyFree = onlyFree, onlyFree = onlyFree,
onlyPointAvailable = onlyPointAvailable onlyPointAvailable = onlyPointAvailable
@@ -148,6 +154,7 @@ class MainContentAllQueryService(
memberId: Long?, memberId: Long?,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
now: LocalDateTime, now: LocalDateTime,
lang: Lang,
onlyOriginal: Boolean = false onlyOriginal: Boolean = false
): MainContentAll { ): MainContentAll {
val totalCount = queryPort.countSeries(memberId, canViewAdultContent, now, onlyOriginal, dayOfWeek) val totalCount = queryPort.countSeries(memberId, canViewAdultContent, now, onlyOriginal, dayOfWeek)
@@ -160,7 +167,7 @@ class MainContentAllQueryService(
limit = page.size + 1, limit = page.size + 1,
onlyOriginal = onlyOriginal, onlyOriginal = onlyOriginal,
dayOfWeek = dayOfWeek, dayOfWeek = dayOfWeek,
locale = langContext.lang.code locale = lang.code
) )
return MainContentAll( return MainContentAll(
@@ -23,7 +23,8 @@ data class MainContentAllAudio(
val isPointAvailable: Boolean, val isPointAvailable: Boolean,
val isFirstContent: Boolean, val isFirstContent: Boolean,
val isOriginalSeries: Boolean, val isOriginalSeries: Boolean,
val creatorNickname: String val creatorNickname: String,
val creatorMemberId: Long
) )
data class MainContentAllSeries( data class MainContentAllSeries(
@@ -32,5 +33,6 @@ data class MainContentAllSeries(
val coverImageUrl: String?, val coverImageUrl: String?,
val creatorNickname: String, val creatorNickname: String,
val isOriginal: Boolean, val isOriginal: Boolean,
val isAdult: Boolean val isAdult: Boolean,
val creatorMemberId: Long
) )
@@ -1,9 +1,15 @@
package kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence 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.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.domain.AudioRankingType
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotPort 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.content.ranking.port.out.AudioRankingSnapshotRecord
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -11,14 +17,34 @@ import java.time.LocalDateTime
@Repository @Repository
class DefaultAudioRankingSnapshotPersistenceAdapter( class DefaultAudioRankingSnapshotPersistenceAdapter(
private val repository: AudioRankingSnapshotRepository, private val repository: AudioRankingSnapshotRepository,
private val contentTranslationRepository: ContentTranslationRepository private val contentTranslationRepository: ContentTranslationRepository,
private val queryFactory: JPAQueryFactory
) : AudioRankingSnapshotPort { ) : AudioRankingSnapshotPort {
override fun findLatestVisibleSnapshots( override fun findLatestVisibleSnapshots(
rankingType: AudioRankingType, rankingType: AudioRankingType,
nowUtc: LocalDateTime, nowUtc: LocalDateTime,
locale: String locale: String
): List<AudioRankingSnapshotRecord> { ): List<AudioRankingSnapshotRecord> {
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( override fun findPreviousVisibleSnapshots(
@@ -31,7 +57,7 @@ class DefaultAudioRankingSnapshotPersistenceAdapter(
rankingType = rankingType.name, rankingType = rankingType.name,
currentAggregationStartAtUtc = currentAggregationStartAtUtc, currentAggregationStartAtUtc = currentAggregationStartAtUtc,
nowUtc = nowUtc nowUtc = nowUtc
).toRecords(locale) ).map { it.toRecord() }.translateTitles(locale)
} }
@Transactional @Transactional
@@ -50,7 +76,7 @@ class DefaultAudioRankingSnapshotPersistenceAdapter(
repository.saveAll(newSnapshots.map { it.toEntity(visibleFromAtUtc) }) repository.saveAll(newSnapshots.map { it.toEntity(visibleFromAtUtc) })
} }
private fun List<AudioRankingSnapshot>.toRecords(locale: String): List<AudioRankingSnapshotRecord> { private fun List<AudioRankingSnapshotRecord>.translateTitles(locale: String): List<AudioRankingSnapshotRecord> {
if (isEmpty()) return emptyList() if (isEmpty()) return emptyList()
val translatedTitleByContentId = contentTranslationRepository.findByContentIdInAndLocale( val translatedTitleByContentId = contentTranslationRepository.findByContentIdInAndLocale(
map { it.contentId }, map { it.contentId },
@@ -58,19 +84,19 @@ class DefaultAudioRankingSnapshotPersistenceAdapter(
).mapNotNull { translation -> ).mapNotNull { translation ->
translation.renderedPayload.title.takeIf { it.isNotBlank() }?.let { translation.contentId to it } translation.renderedPayload.title.takeIf { it.isNotBlank() }?.let { translation.contentId to it }
}.toMap() }.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( return AudioRankingSnapshotRecord(
rankingType = rankingType, rankingType = rankingType,
aggregationStartAtUtc = aggregationStartAtUtc, aggregationStartAtUtc = aggregationStartAtUtc,
aggregationEndAtUtc = aggregationEndAtUtc, aggregationEndAtUtc = aggregationEndAtUtc,
visibleFromAtUtc = visibleFromAtUtc, visibleFromAtUtc = visibleFromAtUtc,
contentId = contentId, contentId = contentId,
title = displayTitle, title = title,
creatorMemberId = creatorMemberId, creatorMemberId = creatorMemberId,
creatorNickname = creatorNickname, creatorNickname = displayCreatorNickname,
coverImageUrl = coverImageUrl, coverImageUrl = coverImageUrl,
releaseDate = releaseDate, releaseDate = releaseDate,
isAdult = isAdult, isAdult = isAdult,
@@ -1,6 +1,6 @@
package kr.co.vividnext.sodalive.v2.content.ranking.application 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.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
@@ -19,7 +19,6 @@ import java.time.ZonedDateTime
@Service @Service
class AudioRankingQueryService( class AudioRankingQueryService(
private val snapshotPort: AudioRankingSnapshotPort, private val snapshotPort: AudioRankingSnapshotPort,
private val langContext: LangContext,
private val memberContentPreferenceService: MemberContentPreferenceService, private val memberContentPreferenceService: MemberContentPreferenceService,
private val blockPort: AudioRankingBlockPort, private val blockPort: AudioRankingBlockPort,
private val jobService: AudioRankingSnapshotJobService, private val jobService: AudioRankingSnapshotJobService,
@@ -29,9 +28,9 @@ class AudioRankingQueryService(
) { ) {
private val log = LoggerFactory.getLogger(javaClass) 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 nowUtc = nowProvider().withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
val locale = langContext.lang.code val locale = lang.code
val latestSnapshots = findLatestVisibleSnapshots(type, nowUtc, locale) val latestSnapshots = findLatestVisibleSnapshots(type, nowUtc, locale)
if (latestSnapshots.isEmpty()) { if (latestSnapshots.isEmpty()) {
return AudioRanking(showRankChange = false, type = type, items = emptyList()) return AudioRanking(showRankChange = false, type = type, items = emptyList())
@@ -114,7 +113,8 @@ class AudioRankingQueryService(
rank = rank, rank = rank,
rankChange = if (showRankChange && previousRank != null) previousRank - rank else null, rankChange = if (showRankChange && previousRank != null) previousRank - rank else null,
isNew = showRankChange && previousRank == null, isNew = showRankChange && previousRank == null,
coverImageUrl = coverImageUrl.toCdnUrl(cloudFrontHost) coverImageUrl = coverImageUrl.toCdnUrl(cloudFrontHost),
creatorMemberId = creatorMemberId
) )
} }
@@ -13,5 +13,6 @@ data class AudioRankingItem(
val rank: Int, val rank: Int,
val rankChange: Int?, val rankChange: Int?,
val isNew: Boolean, val isNew: Boolean,
val coverImageUrl: String? val coverImageUrl: String?,
val creatorMemberId: Long
) )
@@ -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.AudioRecommendationVisibility
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.CommentedAudio 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.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.domain.RecommendedSectionType
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
@@ -120,7 +121,7 @@ class DefaultAudioRecommendationQueryRepository(
now: LocalDateTime, now: LocalDateTime,
locale: String locale: String
): List<AudioCard> { ): List<AudioCard> {
val rows = audioRows(memberId, canViewAdultContent, now) { val rows = audioRows(memberId, canViewAdultContent, now, locale) {
orderBy(audioContent.releaseDate.desc(), audioContent.id.desc()).limit(limit.toLong()) orderBy(audioContent.releaseDate.desc(), audioContent.id.desc()).limit(limit.toLong())
} }
return rows.toAudioCards(now, canViewAdultContent, locale) return rows.toAudioCards(now, canViewAdultContent, locale)
@@ -134,7 +135,7 @@ class DefaultAudioRecommendationQueryRepository(
locale: String locale: String
): List<AudioCard> { ): List<AudioCard> {
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')") 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()) orderBy(randomTieBreaker.asc()).limit(limit.toLong())
} }
return rows.toAudioCards(now, canViewAdultContent, locale) return rows.toAudioCards(now, canViewAdultContent, locale)
@@ -152,6 +153,7 @@ class DefaultAudioRecommendationQueryRepository(
memberId, memberId,
canViewAdultContent, canViewAdultContent,
now, now,
locale,
audioContent.isPointAvailable.isTrue.and(audioContent.price.gt(0)) audioContent.isPointAvailable.isTrue.and(audioContent.price.gt(0))
) { ) {
orderBy(randomTieBreaker.asc()).limit(limit.toLong()) orderBy(randomTieBreaker.asc()).limit(limit.toLong())
@@ -168,7 +170,7 @@ class DefaultAudioRecommendationQueryRepository(
): List<AudioCard> { ): List<AudioCard> {
if (contentIds.isEmpty()) return emptyList() if (contentIds.isEmpty()) return emptyList()
val orderById = contentIds.withIndex().associate { it.value to it.index } 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 } return rows.toAudioCards(now, canViewAdultContent, locale).sortedBy { orderById[it.audioContentId] ?: Int.MAX_VALUE }
} }
@@ -444,9 +446,11 @@ class DefaultAudioRecommendationQueryRepository(
memberId: Long?, memberId: Long?,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
now: LocalDateTime, now: LocalDateTime,
locale: String,
extraCondition: BooleanExpression? = null, extraCondition: BooleanExpression? = null,
customize: com.querydsl.jpa.impl.JPAQuery<Tuple>.() -> com.querydsl.jpa.impl.JPAQuery<Tuple> customize: com.querydsl.jpa.impl.JPAQuery<Tuple>.() -> com.querydsl.jpa.impl.JPAQuery<Tuple>
): List<Tuple> { ): List<Tuple> {
val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "audioRecommendationCreatorName")
return queryFactory return queryFactory
.select( .select(
audioContent.id, audioContent.id,
@@ -457,10 +461,11 @@ class DefaultAudioRecommendationQueryRepository(
audioContent.isAdult, audioContent.isAdult,
audioContent.isPointAvailable, audioContent.isPointAvailable,
audioContent.member.id, audioContent.member.id,
member.nickname creatorName.displayName
) )
.from(audioContent) .from(audioContent)
.join(audioContent.member, member) .join(audioContent.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.join(audioContent.theme, audioContentTheme) .join(audioContent.theme, audioContentTheme)
.where(publicAudioCondition(memberId, canViewAdultContent, now), extraCondition) .where(publicAudioCondition(memberId, canViewAdultContent, now), extraCondition)
.customize() .customize()
@@ -478,6 +483,7 @@ class DefaultAudioRecommendationQueryRepository(
val firstContentIdByCreatorId = firstAudioContentIds(creatorIds, now, canViewAdultContent) val firstContentIdByCreatorId = firstAudioContentIds(creatorIds, now, canViewAdultContent)
val isOriginalSeriesByContentId = originalSeriesFlags(contentIds) val isOriginalSeriesByContentId = originalSeriesFlags(contentIds)
val translatedTitleByContentId = translatedTitles(contentIds, locale) val translatedTitleByContentId = translatedTitles(contentIds, locale)
val creatorName = CreatorNameJoin(member, Lang.fromCode(locale), "audioRecommendationCreatorName")
return map { row -> return map { row ->
val contentId = row.get(audioContent.id)!! val contentId = row.get(audioContent.id)!!
val creatorId = row.get(audioContent.member.id)!! val creatorId = row.get(audioContent.member.id)!!
@@ -491,7 +497,8 @@ class DefaultAudioRecommendationQueryRepository(
isPointAvailable = row.get(audioContent.isPointAvailable)!!, isPointAvailable = row.get(audioContent.isPointAvailable)!!,
isFirstContent = firstContentIdByCreatorId[creatorId] == contentId, isFirstContent = firstContentIdByCreatorId[creatorId] == contentId,
isOriginalSeries = isOriginalSeriesByContentId[contentId] ?: false, isOriginalSeries = isOriginalSeriesByContentId[contentId] ?: false,
creatorNickname = row.get(member.nickname)!! creatorNickname = row.get(creatorName.displayName)!!,
creatorMemberId = creatorId
) )
} }
} }
@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.v2.content.recommendation.application package kr.co.vividnext.sodalive.v2.content.recommendation.application
import kr.co.vividnext.sodalive.i18n.Lang 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.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
@@ -18,16 +17,15 @@ import java.time.ZoneId
class AudioRecommendationQueryService( class AudioRecommendationQueryService(
private val queryPort: AudioRecommendationQueryPort, private val queryPort: AudioRecommendationQueryPort,
private val memberContentPreferenceService: MemberContentPreferenceService, private val memberContentPreferenceService: MemberContentPreferenceService,
private val snapshotFallbackService: AudioRecommendationSnapshotFallbackService, private val snapshotFallbackService: AudioRecommendationSnapshotFallbackService
private val langContext: LangContext
) { ) {
fun getRecommendations(member: Member?): AudioRecommendations { fun getRecommendations(member: Member?, lang: Lang): AudioRecommendations {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val fallbackNow = LocalDateTime.now(KST_ZONE) val fallbackNow = LocalDateTime.now(KST_ZONE)
val canViewAdultContent = canViewAdultContent(member) val canViewAdultContent = canViewAdultContent(member)
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
val memberId = member?.id 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 bannerLang = if (memberContentPreferenceService.resolveCountryCode(member) == "JP") Lang.JA else Lang.KO
val newAndHotSectionType = newAndHotSectionType(visibility) val newAndHotSectionType = newAndHotSectionType(visibility)
val mostCommentedSectionType = mostCommentedSectionType(visibility) val mostCommentedSectionType = mostCommentedSectionType(visibility)
@@ -77,7 +75,7 @@ class AudioRecommendationQueryService(
) )
} }
fun findNewAndHotAudios(member: Member, offset: Long, limit: Int): List<AudioCard> { fun findNewAndHotAudios(member: Member, offset: Long, limit: Int, lang: Lang): List<AudioCard> {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val fallbackNow = LocalDateTime.now(KST_ZONE) val fallbackNow = LocalDateTime.now(KST_ZONE)
val canViewAdultContent = canViewAdultContent(member) val canViewAdultContent = canViewAdultContent(member)
@@ -90,7 +88,7 @@ class AudioRecommendationQueryService(
member.id, member.id,
canViewAdultContent, canViewAdultContent,
now, now,
langContext.lang.code lang.code
) )
} }
@@ -28,7 +28,8 @@ data class AudioCard(
val isPointAvailable: Boolean, val isPointAvailable: Boolean,
val isFirstContent: Boolean, val isFirstContent: Boolean,
val isOriginalSeries: Boolean, val isOriginalSeries: Boolean,
val creatorNickname: String val creatorNickname: String,
val creatorMemberId: Long
) )
data class CommentedAudio( data class CommentedAudio(
@@ -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.content.translation.QContentTranslation
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series 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.QSeriesContent.seriesContent
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.member.block.QBlockMember
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort 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.CreatorChannelAudioContentRecord
import kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out.CreatorChannelAudioCreatorRecord 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.creator.channel.audio.port.out.CreatorChannelAudioThemeRecord
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -28,17 +30,19 @@ import java.time.LocalDateTime
class DefaultCreatorChannelAudioQueryRepository( class DefaultCreatorChannelAudioQueryRepository(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory
) : CreatorChannelAudioQueryRepository { ) : 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 return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
CreatorChannelAudioCreatorRecord::class.java, CreatorChannelAudioCreatorRecord::class.java,
member.id, member.id,
member.role, member.role,
member.nickname creatorName.displayName
) )
) )
.from(member) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.id.eq(creatorId), member.id.eq(creatorId),
member.isActive.isTrue member.isActive.isTrue
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.creator.channel.audio.application package kr.co.vividnext.sodalive.v2.creator.channel.audio.application
import kr.co.vividnext.sodalive.common.SodaException 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.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -39,18 +40,19 @@ class CreatorChannelAudioQueryService(
themeId: Long?, themeId: Long?,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelAudioTab { ): CreatorChannelAudioTab {
val resolvedSort = queryPolicy.resolveSort(sort) val resolvedSort = queryPolicy.resolveSort(sort)
val audioPage = queryPolicy.createPage(page, size) val audioPage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! 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") ?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) { if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang) .getMessage("explorer.creator.blocked_access", lang)
.orEmpty() .orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname)) throw SodaException(message = String.format(messageTemplate, creator.nickname))
} }
@@ -1,11 +1,12 @@
package kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out 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.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import java.time.LocalDateTime import java.time.LocalDateTime
interface CreatorChannelAudioQueryPort { 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 fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
@@ -4,28 +4,37 @@ import com.querydsl.core.Tuple
import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.OrderSpecifier
import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan 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.QCreatorCommunity.creatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment 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.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.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember 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.CreatorChannelCommunityCommentContextRecord
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord 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.CreatorChannelCommunityCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityPostRecord 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 import org.springframework.stereotype.Repository
@Repository @Repository
class DefaultCreatorChannelCommunityQueryRepository( class DefaultCreatorChannelCommunityQueryRepository(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory
) : CreatorChannelCommunityQueryRepository { ) : 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 val creator = queryFactory
.select(member.id, member.role, member.nickname) .select(member.id, member.role, creatorName.displayName)
.from(member) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.id.eq(creatorId), member.id.eq(creatorId),
member.isActive.isTrue member.isActive.isTrue
@@ -35,7 +44,7 @@ class DefaultCreatorChannelCommunityQueryRepository(
return CreatorChannelCommunityCreatorRecord( return CreatorChannelCommunityCreatorRecord(
creatorId = creator.get(member.id)!!, creatorId = creator.get(member.id)!!,
role = creator.get(member.role)!!, role = creator.get(member.role)!!,
nickname = creator.get(member.nickname)!! nickname = creator.get(creatorName.displayName)!!
) )
} }
@@ -71,11 +80,15 @@ class DefaultCreatorChannelCommunityQueryRepository(
viewerId: Long, viewerId: Long,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> { ): List<CreatorChannelCommunityPostRecord> {
val creatorName = CreatorNameJoin(member, lang, "channelCommunityPostCreatorName")
val rows = queryFactory val rows = queryFactory
.selectCommunityPostRow() .selectCommunityPostRow(creatorName)
.from(creatorCommunity) .from(creatorCommunity)
.join(creatorCommunity.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(communityPostCondition(creatorId, canViewAdultContent)) .where(communityPostCondition(creatorId, canViewAdultContent))
.orderBy( .orderBy(
CaseBuilder() CaseBuilder()
@@ -104,11 +117,15 @@ class DefaultCreatorChannelCommunityQueryRepository(
viewerId: Long, viewerId: Long,
isPinned: Boolean, isPinned: Boolean,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> { ): List<CreatorChannelCommunityPostRecord> {
val creatorName = CreatorNameJoin(member, lang, "channelHomePostCreatorName")
val rows = queryFactory val rows = queryFactory
.selectCommunityPostRow() .selectCommunityPostRow(creatorName)
.from(creatorCommunity) .from(creatorCommunity)
.join(creatorCommunity.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
homeCommunityPostCondition(creatorId, viewerId, canViewAdultContent), homeCommunityPostCondition(creatorId, viewerId, canViewAdultContent),
creatorCommunity.isFixed.eq(isPinned), creatorCommunity.isFixed.eq(isPinned),
@@ -124,11 +141,15 @@ class DefaultCreatorChannelCommunityQueryRepository(
override fun findCommunityPost( override fun findCommunityPost(
postId: Long, postId: Long,
viewerId: Long, viewerId: Long,
canViewAdultContent: Boolean canViewAdultContent: Boolean,
lang: Lang
): CreatorChannelCommunityPostRecord? { ): CreatorChannelCommunityPostRecord? {
val creatorName = CreatorNameJoin(member, lang, "channelPostDetailCreatorName")
val rows = queryFactory val rows = queryFactory
.selectCommunityPostRow() .selectCommunityPostRow(creatorName)
.from(creatorCommunity) .from(creatorCommunity)
.join(creatorCommunity.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where(communityPostByIdCondition(postId, canViewAdultContent)) .where(communityPostByIdCondition(postId, canViewAdultContent))
.fetch() .fetch()
@@ -155,11 +176,15 @@ class DefaultCreatorChannelCommunityQueryRepository(
viewerId: Long, viewerId: Long,
creatorId: Long, creatorId: Long,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> { ): List<CreatorChannelCommunityCommentRecord> {
val writerName = CreatorNameJoin(member, lang, "channelCommentWriterName")
return queryFactory return queryFactory
.selectCommunityCommentRow() .selectCommunityCommentRow(writerName)
.from(creatorCommunityComment) .from(creatorCommunityComment)
.join(creatorCommunityComment.member, member)
.leftJoin(writerName.name).on(writerName.condition)
.where(rootCommentCondition(postId, creatorId, viewerId)) .where(rootCommentCondition(postId, creatorId, viewerId))
.orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc()) .orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc())
.offset(offset) .offset(offset)
@@ -170,13 +195,17 @@ class DefaultCreatorChannelCommunityQueryRepository(
override fun findLatestRepliesByCommentIds( override fun findLatestRepliesByCommentIds(
commentIds: List<Long>, commentIds: List<Long>,
viewerId: Long viewerId: Long,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> { ): List<CreatorChannelCommunityCommentRecord> {
if (commentIds.isEmpty()) return emptyList() if (commentIds.isEmpty()) return emptyList()
val writerName = CreatorNameJoin(member, lang, "channelLatestReplyWriterName")
return queryFactory return queryFactory
.selectCommunityCommentRow() .selectCommunityCommentRow(writerName)
.from(creatorCommunityComment) .from(creatorCommunityComment)
.join(creatorCommunityComment.member, member)
.leftJoin(writerName.name).on(writerName.condition)
.where(replyCondition(commentIds, viewerId)) .where(replyCondition(commentIds, viewerId))
.orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc()) .orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc())
.fetch() .fetch()
@@ -224,11 +253,15 @@ class DefaultCreatorChannelCommunityQueryRepository(
commentId: Long, commentId: Long,
viewerId: Long, viewerId: Long,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> { ): List<CreatorChannelCommunityCommentRecord> {
val writerName = CreatorNameJoin(member, lang, "channelReplyWriterName")
return queryFactory return queryFactory
.selectCommunityCommentRow() .selectCommunityCommentRow(writerName)
.from(creatorCommunityComment) .from(creatorCommunityComment)
.join(creatorCommunityComment.member, member)
.leftJoin(writerName.name).on(writerName.condition)
.where(replyCondition(commentId, viewerId)) .where(replyCondition(commentId, viewerId))
.orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc()) .orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc())
.offset(offset) .offset(offset)
@@ -237,10 +270,10 @@ class DefaultCreatorChannelCommunityQueryRepository(
.map { it.toCommunityCommentRecord() } .map { it.toCommunityCommentRecord() }
} }
private fun JPAQueryFactory.selectCommunityPostRow() = select( private fun JPAQueryFactory.selectCommunityPostRow(creatorName: CreatorNameJoin) = select(
creatorCommunity.id, creatorCommunity.id,
creatorCommunity.member.id, creatorCommunity.member.id,
creatorCommunity.member.nickname, creatorName.displayName.`as`(postCreatorDisplayName),
creatorCommunity.member.profileImage, creatorCommunity.member.profileImage,
creatorCommunity.imagePath, creatorCommunity.imagePath,
creatorCommunity.audioPath, creatorCommunity.audioPath,
@@ -252,16 +285,17 @@ class DefaultCreatorChannelCommunityQueryRepository(
creatorCommunity.isCommentAvailable creatorCommunity.isCommentAvailable
) )
private fun JPAQueryFactory.selectCommunityCommentRow() = select( private fun JPAQueryFactory.selectCommunityCommentRow(writerName: CreatorNameJoin) = select(
creatorCommunityComment.id, creatorCommunityComment.id,
creatorCommunityComment.parent.id, creatorCommunityComment.parent.id,
creatorCommunityComment.creatorCommunity.id, creatorCommunityComment.creatorCommunity.id,
creatorCommunityComment.member.id, creatorCommunityComment.member.id,
creatorCommunityComment.member.profileImage, creatorCommunityComment.member.profileImage,
creatorCommunityComment.member.nickname, writerName.displayName.`as`(commentWriterDisplayName),
creatorCommunityComment.comment, creatorCommunityComment.comment,
creatorCommunityComment.isSecret, creatorCommunityComment.isSecret,
creatorCommunityComment.createdAt creatorCommunityComment.createdAt,
writerName.name.id.isNotNull.`as`(localizedWriterName)
) )
private fun communityPostCondition(creatorId: Long, canViewAdultContent: Boolean): BooleanExpression { private fun communityPostCondition(creatorId: Long, canViewAdultContent: Boolean): BooleanExpression {
@@ -519,7 +553,7 @@ class DefaultCreatorChannelCommunityQueryRepository(
get() = get(creatorCommunity.member.id)!! get() = get(creatorCommunity.member.id)!!
private val Tuple.creatorNickname: String private val Tuple.creatorNickname: String
get() = get(creatorCommunity.member.nickname)!! get() = get(postCreatorDisplayName)!!
private val Tuple.creatorProfilePath: String? private val Tuple.creatorProfilePath: String?
get() = get(creatorCommunity.member.profileImage) get() = get(creatorCommunity.member.profileImage)
@@ -555,10 +589,11 @@ class DefaultCreatorChannelCommunityQueryRepository(
postId = get(creatorCommunityComment.creatorCommunity.id)!!, postId = get(creatorCommunityComment.creatorCommunity.id)!!,
writerId = get(creatorCommunityComment.member.id)!!, writerId = get(creatorCommunityComment.member.id)!!,
writerProfilePath = get(creatorCommunityComment.member.profileImage), writerProfilePath = get(creatorCommunityComment.member.profileImage),
writerNickname = get(creatorCommunityComment.member.nickname)!!, writerNickname = get(commentWriterDisplayName)!!,
content = get(creatorCommunityComment.comment)!!, content = get(creatorCommunityComment.comment)!!,
isSecret = get(creatorCommunityComment.isSecret)!!, isSecret = get(creatorCommunityComment.isSecret)!!,
createdAt = get(creatorCommunityComment.createdAt)!! createdAt = get(creatorCommunityComment.createdAt)!!,
isLocalizedName = get(localizedWriterName)!!
) )
} }
} }
@@ -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.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix 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.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -46,17 +47,18 @@ class CreatorChannelCommunityQueryService(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityTab { ): CreatorChannelCommunityTab {
val communityPage = queryPolicy.createPage(page, size) val communityPage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! 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") ?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) { if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang) .getMessage("explorer.creator.blocked_access", lang)
.orEmpty() .orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname)) throw SodaException(message = String.format(messageTemplate, creator.nickname))
} }
@@ -69,7 +71,8 @@ class CreatorChannelCommunityQueryService(
viewerId = viewerId, viewerId = viewerId,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
offset = communityPage.offset, offset = communityPage.offset,
limit = communityPage.fetchLimit limit = communityPage.fetchLimit,
lang = lang
) )
val communityPosts = queryPolicy.limitItems(fetchedPosts, communityPage) val communityPosts = queryPolicy.limitItems(fetchedPosts, communityPage)
val displayContents = creatorCommunityTranslationService val displayContents = creatorCommunityTranslationService
@@ -94,7 +97,8 @@ class CreatorChannelCommunityQueryService(
viewerId: Long, viewerId: Long,
isPinned: Boolean, isPinned: Boolean,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityPost> { ): List<CreatorChannelCommunityPost> {
val posts = queryPortProvider.getObject() val posts = queryPortProvider.getObject()
.findHomeCommunityPosts( .findHomeCommunityPosts(
@@ -102,7 +106,8 @@ class CreatorChannelCommunityQueryService(
viewerId = viewerId, viewerId = viewerId,
isPinned = isPinned, isPinned = isPinned,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
limit = limit limit = limit,
lang = lang
) )
val displayContents = creatorCommunityTranslationService val displayContents = creatorCommunityTranslationService
.findDisplayContents(posts.map { it.postId }, langContext.lang.code) .findDisplayContents(posts.map { it.postId }, langContext.lang.code)
@@ -115,12 +120,13 @@ class CreatorChannelCommunityQueryService(
fun getCommunityPostDetail( fun getCommunityPostDetail(
postId: Long, postId: Long,
viewer: Member, viewer: Member,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityPostDetail { ): CreatorChannelCommunityPostDetail {
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! val viewerId = viewer.id!!
val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(viewer) 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) ?: throw SodaException(messageKey = INVALID_COMMUNITY_REQUEST_MESSAGE_KEY)
validateNotBlocked(queryPort, viewerId, postRecord.creatorId) validateNotBlocked(queryPort, viewerId, postRecord.creatorId)
@@ -134,7 +140,7 @@ class CreatorChannelCommunityQueryService(
return CreatorChannelCommunityPostDetail( return CreatorChannelCommunityPostDetail(
post = postRecord.copy(content = displayContent?.content ?: postRecord.content).toDomain(viewerId), 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, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityComments { ): CreatorChannelCommunityComments {
val communityPage = queryPolicy.createPage(page, size) val communityPage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! val viewerId = viewer.id!!
val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(viewer) 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) ?: throw SodaException(messageKey = INVALID_COMMUNITY_REQUEST_MESSAGE_KEY)
validateNotBlocked(queryPort, viewerId, postRecord.creatorId) validateNotBlocked(queryPort, viewerId, postRecord.creatorId)
@@ -168,12 +175,14 @@ class CreatorChannelCommunityQueryService(
viewerId = viewerId, viewerId = viewerId,
creatorId = postRecord.creatorId, creatorId = postRecord.creatorId,
offset = communityPage.offset, offset = communityPage.offset,
limit = communityPage.fetchLimit limit = communityPage.fetchLimit,
lang = lang
) )
val limitedComments = queryPolicy.limitItems(fetchedComments, communityPage) val limitedComments = queryPolicy.limitItems(fetchedComments, communityPage)
val latestReplies = queryPort.findLatestRepliesByCommentIds( val latestReplies = queryPort.findLatestRepliesByCommentIds(
commentIds = limitedComments.map { it.commentId }, commentIds = limitedComments.map { it.commentId },
viewerId = viewerId viewerId = viewerId,
lang = lang
).associateBy { it.parentCommentId } ).associateBy { it.parentCommentId }
return CreatorChannelCommunityComments( return CreatorChannelCommunityComments(
@@ -193,7 +202,8 @@ class CreatorChannelCommunityQueryService(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelCommunityReplies { ): CreatorChannelCommunityReplies {
val communityPage = queryPolicy.createPage(page, size) val communityPage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
@@ -217,7 +227,8 @@ class CreatorChannelCommunityQueryService(
commentId = commentId, commentId = commentId,
viewerId = viewerId, viewerId = viewerId,
offset = communityPage.offset, offset = communityPage.offset,
limit = communityPage.fetchLimit limit = communityPage.fetchLimit,
lang = lang
) )
return CreatorChannelCommunityReplies( return CreatorChannelCommunityReplies(
@@ -274,7 +285,7 @@ class CreatorChannelCommunityQueryService(
commentId = commentId, commentId = commentId,
writerId = writerId, writerId = writerId,
writerProfileImageUrl = writerProfilePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), writerProfileImageUrl = writerProfilePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
writerNickname = writerNickname.removeDeletedNicknamePrefix(), writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(),
content = content, content = content,
isSecret = isSecret, isSecret = isSecret,
createdAt = createdAt, createdAt = createdAt,
@@ -287,7 +298,7 @@ class CreatorChannelCommunityQueryService(
commentId = commentId, commentId = commentId,
writerId = writerId, writerId = writerId,
writerProfileImageUrl = writerProfilePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), writerProfileImageUrl = writerProfilePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
writerNickname = writerNickname.removeDeletedNicknamePrefix(), writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(),
content = content, content = content,
createdAt = createdAt createdAt = createdAt
) )
@@ -1,10 +1,11 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.port.out 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 kr.co.vividnext.sodalive.member.MemberRole
import java.time.LocalDateTime import java.time.LocalDateTime
interface CreatorChannelCommunityQueryPort { 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 fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
@@ -19,7 +20,8 @@ interface CreatorChannelCommunityQueryPort {
viewerId: Long, viewerId: Long,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> ): List<CreatorChannelCommunityPostRecord>
fun findHomeCommunityPosts( fun findHomeCommunityPosts(
@@ -27,13 +29,15 @@ interface CreatorChannelCommunityQueryPort {
viewerId: Long, viewerId: Long,
isPinned: Boolean, isPinned: Boolean,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> ): List<CreatorChannelCommunityPostRecord>
fun findCommunityPost( fun findCommunityPost(
postId: Long, postId: Long,
viewerId: Long, viewerId: Long,
canViewAdultContent: Boolean canViewAdultContent: Boolean,
lang: Lang
): CreatorChannelCommunityPostRecord? ): CreatorChannelCommunityPostRecord?
fun countCommunityComments( fun countCommunityComments(
@@ -47,12 +51,14 @@ interface CreatorChannelCommunityQueryPort {
viewerId: Long, viewerId: Long,
creatorId: Long, creatorId: Long,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> ): List<CreatorChannelCommunityCommentRecord>
fun findLatestRepliesByCommentIds( fun findLatestRepliesByCommentIds(
commentIds: List<Long>, commentIds: List<Long>,
viewerId: Long viewerId: Long,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> ): List<CreatorChannelCommunityCommentRecord>
fun findRootCommentContext( fun findRootCommentContext(
@@ -70,7 +76,8 @@ interface CreatorChannelCommunityQueryPort {
commentId: Long, commentId: Long,
viewerId: Long, viewerId: Long,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> ): List<CreatorChannelCommunityCommentRecord>
} }
@@ -107,7 +114,8 @@ data class CreatorChannelCommunityCommentRecord(
val writerNickname: String, val writerNickname: String,
val content: String, val content: String,
val isSecret: Boolean, val isSecret: Boolean,
val createdAt: LocalDateTime val createdAt: LocalDateTime,
val isLocalizedName: Boolean = false
) )
data class CreatorChannelCommunityCommentContextRecord( data class CreatorChannelCommunityCommentContextRecord(
@@ -4,11 +4,13 @@ import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage 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.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember 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.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.CreatorChannelDonationCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRecord 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 org.springframework.stereotype.Repository
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -18,16 +20,18 @@ class DefaultCreatorChannelDonationQueryRepository(
) : CreatorChannelDonationQueryRepository { ) : CreatorChannelDonationQueryRepository {
private val queryPolicy = CreatorChannelDonationQueryPolicy() 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 val creator = queryFactory
.select( .select(
member.id, member.id,
member.role, member.role,
member.nickname, creatorName.displayName,
member.isVisibleDonationRank, member.isVisibleDonationRank,
member.donationRankingPeriod member.donationRankingPeriod
) )
.from(member) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.id.eq(creatorId), member.id.eq(creatorId),
member.isActive.isTrue member.isActive.isTrue
@@ -37,7 +41,7 @@ class DefaultCreatorChannelDonationQueryRepository(
return CreatorChannelDonationCreatorRecord( return CreatorChannelDonationCreatorRecord(
creatorId = creator.get(member.id)!!, creatorId = creator.get(member.id)!!,
role = creator.get(member.role)!!, role = creator.get(member.role)!!,
nickname = creator.get(member.nickname)!!, nickname = creator.get(creatorName.displayName)!!,
isVisibleDonationRank = creator.get(member.isVisibleDonationRank)!!, isVisibleDonationRank = creator.get(member.isVisibleDonationRank)!!,
donationRankingPeriod = creator.get(member.donationRankingPeriod) donationRankingPeriod = creator.get(member.donationRankingPeriod)
) )
@@ -75,20 +79,26 @@ class DefaultCreatorChannelDonationQueryRepository(
viewerId: Long, viewerId: Long,
now: LocalDateTime, now: LocalDateTime,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelDonationRecord> { ): List<CreatorChannelDonationRecord> {
val donorName = CreatorNameJoin(member, lang, "channelDonationDonorName")
return queryFactory return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
CreatorChannelDonationRecord::class.java, CreatorChannelDonationRecord::class.java,
channelDonationMessage.member.nickname, channelDonationMessage.member.id,
donorName.displayName,
channelDonationMessage.member.profileImage, channelDonationMessage.member.profileImage,
channelDonationMessage.can, channelDonationMessage.can,
channelDonationMessage.additionalMessage, channelDonationMessage.additionalMessage,
channelDonationMessage.createdAt channelDonationMessage.createdAt,
donorName.name.id.isNotNull
) )
) )
.from(channelDonationMessage) .from(channelDonationMessage)
.join(channelDonationMessage.member, member)
.leftJoin(donorName.name).on(donorName.condition)
.where(channelDonationCondition(creatorId, viewerId, now)) .where(channelDonationCondition(creatorId, viewerId, now))
.orderBy(channelDonationMessage.createdAt.desc(), channelDonationMessage.id.desc()) .orderBy(channelDonationMessage.createdAt.desc(), channelDonationMessage.id.desc())
.offset(offset) .offset(offset)
@@ -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.common.SodaException
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix 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.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.DonationRankingPeriod import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -30,7 +30,6 @@ class CreatorChannelDonationQueryService(
private val rankingPort: CreatorChannelDonationRankingPort, private val rankingPort: CreatorChannelDonationRankingPort,
private val queryPolicy: CreatorChannelDonationQueryPolicy, private val queryPolicy: CreatorChannelDonationQueryPolicy,
private val messageSource: SodaMessageSource, private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String private val cloudFrontHost: String
) { ) {
@@ -39,17 +38,18 @@ class CreatorChannelDonationQueryService(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime now: LocalDateTime,
lang: Lang
): CreatorChannelDonationTab { ): CreatorChannelDonationTab {
val donationPage = queryPolicy.createPage(page, size) val donationPage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! 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") ?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) { if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang) .getMessage("explorer.creator.blocked_access", lang)
.orEmpty() .orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname)) throw SodaException(message = String.format(messageTemplate, creator.nickname))
} }
@@ -61,7 +61,8 @@ class CreatorChannelDonationQueryService(
viewerId = viewerId, viewerId = viewerId,
now = now, now = now,
offset = donationPage.offset, offset = donationPage.offset,
limit = donationPage.fetchLimit limit = donationPage.fetchLimit,
lang = lang
) )
return CreatorChannelDonationTab( return CreatorChannelDonationTab(
@@ -95,7 +96,8 @@ class CreatorChannelDonationQueryService(
} }
private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation( private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation(
nickname = nickname.removeDeletedNicknamePrefix(), memberId = memberId,
nickname = if (isLocalizedName) nickname else nickname.removeDeletedNicknamePrefix(),
profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
can = can, can = can,
message = message.orEmpty(), message = message.orEmpty(),
@@ -19,6 +19,7 @@ data class CreatorChannelDonationRanking(
) )
data class CreatorChannelDonation( data class CreatorChannelDonation(
val memberId: Long,
val nickname: String, val nickname: String,
val profileImageUrl: String, val profileImageUrl: String,
val can: Int, val can: Int,
@@ -1,11 +1,12 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out 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.DonationRankingPeriod
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import java.time.LocalDateTime import java.time.LocalDateTime
interface CreatorChannelDonationQueryPort { 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 fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
@@ -20,7 +21,8 @@ interface CreatorChannelDonationQueryPort {
viewerId: Long, viewerId: Long,
now: LocalDateTime, now: LocalDateTime,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelDonationRecord> ): List<CreatorChannelDonationRecord>
} }
@@ -33,9 +35,11 @@ data class CreatorChannelDonationCreatorRecord(
) )
data class CreatorChannelDonationRecord( data class CreatorChannelDonationRecord(
val memberId: Long,
val nickname: String, val nickname: String,
val profileImagePath: String?, val profileImagePath: String?,
val can: Int, val can: Int,
val message: String?, val message: String?,
val createdAt: LocalDateTime val createdAt: LocalDateTime,
val isLocalizedName: Boolean = false
) )
@@ -4,21 +4,25 @@ import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers 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.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember 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.CreatorChannelFanTalkCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkRecord 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.creator.channel.fantalk.port.out.CreatorChannelFanTalkReplyRecord
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
class DefaultCreatorChannelFanTalkQueryRepository( class DefaultCreatorChannelFanTalkQueryRepository(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory
) : CreatorChannelFanTalkQueryRepository { ) : 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 val creator = queryFactory
.select(member.id, member.role, member.nickname) .select(member.id, member.role, creatorName.displayName)
.from(member) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.id.eq(creatorId), member.id.eq(creatorId),
member.isActive.isTrue member.isActive.isTrue
@@ -28,7 +32,7 @@ class DefaultCreatorChannelFanTalkQueryRepository(
return CreatorChannelFanTalkCreatorRecord( return CreatorChannelFanTalkCreatorRecord(
creatorId = creator.get(member.id)!!, creatorId = creator.get(member.id)!!,
role = creator.get(member.role)!!, role = creator.get(member.role)!!,
nickname = creator.get(member.nickname)!! nickname = creator.get(creatorName.displayName)!!
) )
} }
@@ -59,21 +63,26 @@ class DefaultCreatorChannelFanTalkQueryRepository(
creatorId: Long, creatorId: Long,
viewerId: Long, viewerId: Long,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelFanTalkRecord> { ): List<CreatorChannelFanTalkRecord> {
val writerName = CreatorNameJoin(member, lang, "channelFanTalkWriterName")
return queryFactory return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
CreatorChannelFanTalkRecord::class.java, CreatorChannelFanTalkRecord::class.java,
creatorCheers.id, creatorCheers.id,
creatorCheers.member.id, creatorCheers.member.id,
creatorCheers.member.nickname, writerName.displayName,
creatorCheers.member.profileImage, creatorCheers.member.profileImage,
creatorCheers.cheers, creatorCheers.cheers,
creatorCheers.createdAt creatorCheers.createdAt,
writerName.name.id.isNotNull
) )
) )
.from(creatorCheers) .from(creatorCheers)
.join(creatorCheers.member, member)
.leftJoin(writerName.name).on(writerName.condition)
.where(fanTalkCondition(creatorId, viewerId)) .where(fanTalkCondition(creatorId, viewerId))
.orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc()) .orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc())
.offset(offset) .offset(offset)
@@ -83,9 +92,11 @@ class DefaultCreatorChannelFanTalkQueryRepository(
override fun findCreatorReplies( override fun findCreatorReplies(
creatorId: Long, creatorId: Long,
parentFanTalkIds: List<Long> parentFanTalkIds: List<Long>,
lang: Lang
): List<CreatorChannelFanTalkReplyRecord> { ): List<CreatorChannelFanTalkReplyRecord> {
if (parentFanTalkIds.isEmpty()) return emptyList() if (parentFanTalkIds.isEmpty()) return emptyList()
val writerName = CreatorNameJoin(member, lang, "channelFanTalkReplyWriterName")
return queryFactory return queryFactory
.select( .select(
@@ -94,13 +105,16 @@ class DefaultCreatorChannelFanTalkQueryRepository(
creatorCheers.id, creatorCheers.id,
creatorCheers.parent.id, creatorCheers.parent.id,
creatorCheers.member.id, creatorCheers.member.id,
creatorCheers.member.nickname, writerName.displayName,
creatorCheers.member.profileImage, creatorCheers.member.profileImage,
creatorCheers.cheers, creatorCheers.cheers,
creatorCheers.createdAt creatorCheers.createdAt,
writerName.name.id.isNotNull
) )
) )
.from(creatorCheers) .from(creatorCheers)
.join(creatorCheers.member, member)
.leftJoin(writerName.name).on(writerName.condition)
.where( .where(
creatorCheers.creator.id.eq(creatorId), creatorCheers.creator.id.eq(creatorId),
creatorCheers.member.id.eq(creatorId), creatorCheers.member.id.eq(creatorId),
@@ -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.common.SodaException
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix 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.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
@@ -27,7 +27,6 @@ class CreatorChannelFanTalkQueryService(
private val queryPortProvider: ObjectProvider<CreatorChannelFanTalkQueryPort>, private val queryPortProvider: ObjectProvider<CreatorChannelFanTalkQueryPort>,
private val queryPolicy: CreatorChannelFanTalkQueryPolicy, private val queryPolicy: CreatorChannelFanTalkQueryPolicy,
private val messageSource: SodaMessageSource, private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String private val cloudFrontHost: String
) { ) {
@@ -36,17 +35,18 @@ class CreatorChannelFanTalkQueryService(
viewer: Member, viewer: Member,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelFanTalkTab { ): CreatorChannelFanTalkTab {
val fanTalkPage = queryPolicy.createPage(page, size) val fanTalkPage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! 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") ?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) { if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang) .getMessage("explorer.creator.blocked_access", lang)
.orEmpty() .orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname)) throw SodaException(message = String.format(messageTemplate, creator.nickname))
} }
@@ -57,10 +57,11 @@ class CreatorChannelFanTalkQueryService(
creatorId = creatorId, creatorId = creatorId,
viewerId = viewerId, viewerId = viewerId,
offset = fanTalkPage.offset, offset = fanTalkPage.offset,
limit = fanTalkPage.fetchLimit limit = fanTalkPage.fetchLimit,
lang = lang
) )
val fanTalkRecords = queryPolicy.limitItems(fetchedFanTalks, fanTalkPage) val fanTalkRecords = queryPolicy.limitItems(fetchedFanTalks, fanTalkPage)
val repliesByParentId = findRepliesByParentId(queryPort, creatorId, fanTalkRecords) val repliesByParentId = findRepliesByParentId(queryPort, creatorId, fanTalkRecords, lang)
return CreatorChannelFanTalkTab( return CreatorChannelFanTalkTab(
fanTalkCount = queryPort.countFanTalks(creatorId, viewerId), fanTalkCount = queryPort.countFanTalks(creatorId, viewerId),
@@ -80,11 +81,12 @@ class CreatorChannelFanTalkQueryService(
private fun findRepliesByParentId( private fun findRepliesByParentId(
queryPort: CreatorChannelFanTalkQueryPort, queryPort: CreatorChannelFanTalkQueryPort,
creatorId: Long, creatorId: Long,
fanTalkRecords: List<CreatorChannelFanTalkRecord> fanTalkRecords: List<CreatorChannelFanTalkRecord>,
lang: Lang
): Map<Long, List<CreatorChannelFanTalkReply>> { ): Map<Long, List<CreatorChannelFanTalkReply>> {
val parentFanTalkIds = fanTalkRecords.map { it.fanTalkId } val parentFanTalkIds = fanTalkRecords.map { it.fanTalkId }
if (parentFanTalkIds.isEmpty()) return emptyMap() if (parentFanTalkIds.isEmpty()) return emptyMap()
return queryPort.findCreatorReplies(creatorId, parentFanTalkIds) return queryPort.findCreatorReplies(creatorId, parentFanTalkIds, lang)
.groupBy( .groupBy(
keySelector = { it.parentFanTalkId }, keySelector = { it.parentFanTalkId },
valueTransform = { it.toDomain() } valueTransform = { it.toDomain() }
@@ -96,7 +98,7 @@ class CreatorChannelFanTalkQueryService(
) = CreatorChannelFanTalk( ) = CreatorChannelFanTalk(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
writerId = writerId, writerId = writerId,
writerNickname = writerNickname.removeDeletedNicknamePrefix(), writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(),
writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
content = content, content = content,
createdAt = createdAt, createdAt = createdAt,
@@ -106,7 +108,7 @@ class CreatorChannelFanTalkQueryService(
private fun CreatorChannelFanTalkReplyRecord.toDomain() = CreatorChannelFanTalkReply( private fun CreatorChannelFanTalkReplyRecord.toDomain() = CreatorChannelFanTalkReply(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
writerId = writerId, writerId = writerId,
writerNickname = writerNickname.removeDeletedNicknamePrefix(), writerNickname = if (isLocalizedName) writerNickname else writerNickname.removeDeletedNicknamePrefix(),
writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
content = content, content = content,
createdAt = createdAt createdAt = createdAt
@@ -1,10 +1,11 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out 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 kr.co.vividnext.sodalive.member.MemberRole
import java.time.LocalDateTime import java.time.LocalDateTime
interface CreatorChannelFanTalkQueryPort { 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 fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
@@ -14,12 +15,14 @@ interface CreatorChannelFanTalkQueryPort {
creatorId: Long, creatorId: Long,
viewerId: Long, viewerId: Long,
offset: Long, offset: Long,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelFanTalkRecord> ): List<CreatorChannelFanTalkRecord>
fun findCreatorReplies( fun findCreatorReplies(
creatorId: Long, creatorId: Long,
parentFanTalkIds: List<Long> parentFanTalkIds: List<Long>,
lang: Lang
): List<CreatorChannelFanTalkReplyRecord> ): List<CreatorChannelFanTalkReplyRecord>
} }
@@ -35,7 +38,8 @@ data class CreatorChannelFanTalkRecord(
val writerNickname: String, val writerNickname: String,
val writerProfileImagePath: String?, val writerProfileImagePath: String?,
val content: String, val content: String,
val createdAt: LocalDateTime val createdAt: LocalDateTime,
val isLocalizedName: Boolean = false
) )
data class CreatorChannelFanTalkReplyRecord( data class CreatorChannelFanTalkReplyRecord(
@@ -45,5 +49,6 @@ data class CreatorChannelFanTalkReplyRecord(
val writerNickname: String, val writerNickname: String,
val writerProfileImagePath: String?, val writerProfileImagePath: String?,
val content: String, val content: String,
val createdAt: LocalDateTime val createdAt: LocalDateTime,
val isLocalizedName: Boolean = false
) )
@@ -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.QCreatorCheers.creatorCheers
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix 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.GenderRestriction
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit
import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.MemberKind 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.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.member.block.QBlockMember
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing 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.CreatorChannelScheduleRecord
import kr.co.vividnext.sodalive.v2.creator.channel.home.port.out.CreatorChannelSeriesRecord 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.creator.channel.home.port.out.CreatorChannelSnsRecord
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -46,10 +49,12 @@ import java.time.temporal.ChronoUnit
class DefaultCreatorChannelHomeQueryRepository( class DefaultCreatorChannelHomeQueryRepository(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory
) : CreatorChannelHomeQueryRepository { ) : 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 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) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.id.eq(creatorId), member.id.eq(creatorId),
member.isActive.isTrue member.isActive.isTrue
@@ -81,7 +86,7 @@ class DefaultCreatorChannelHomeQueryRepository(
creatorId = creator.get(member.id)!!, creatorId = creator.get(member.id)!!,
role = creator.get(member.role)!!, role = creator.get(member.role)!!,
characterId = characterId, characterId = characterId,
nickname = creator.get(member.nickname)!!, nickname = creator.get(creatorName.displayName)!!,
profileImagePath = creator.get(member.profileImage), profileImagePath = creator.get(member.profileImage),
introduce = creator.get(member.introduce)!!, introduce = creator.get(member.introduce)!!,
followerCount = queryFactory followerCount = queryFactory
@@ -172,8 +177,11 @@ class DefaultCreatorChannelHomeQueryRepository(
creatorId: Long, creatorId: Long,
viewerId: Long?, viewerId: Long?,
now: LocalDateTime, now: LocalDateTime,
limit: Int limit: Int,
lang: Lang
): List<CreatorChannelDonationRecord> { ): List<CreatorChannelDonationRecord> {
val donor = QMember("channelHomeDonor")
val donorName = CreatorNameJoin(donor, lang, "channelHomeDonorName")
val kstZoneId = ZoneId.of("Asia/Seoul") val kstZoneId = ZoneId.of("Asia/Seoul")
val utcZoneId = ZoneId.of("UTC") val utcZoneId = ZoneId.of("UTC")
val nowKst = now.atZone(utcZoneId).withZoneSameInstant(kstZoneId) val nowKst = now.atZone(utcZoneId).withZoneSameInstant(kstZoneId)
@@ -187,14 +195,18 @@ class DefaultCreatorChannelHomeQueryRepository(
.select( .select(
Projections.constructor( Projections.constructor(
CreatorChannelDonationRecord::class.java, CreatorChannelDonationRecord::class.java,
channelDonationMessage.member.nickname, channelDonationMessage.member.id,
donorName.displayName,
channelDonationMessage.member.profileImage, channelDonationMessage.member.profileImage,
channelDonationMessage.can, channelDonationMessage.can,
channelDonationMessage.additionalMessage.coalesce(""), channelDonationMessage.additionalMessage.coalesce(""),
channelDonationMessage.createdAt channelDonationMessage.createdAt,
donorName.name.id.isNotNull
) )
) )
.from(channelDonationMessage) .from(channelDonationMessage)
.join(channelDonationMessage.member, donor)
.leftJoin(donorName.name).on(donorName.condition)
.where( .where(
channelDonationMessage.creator.id.eq(creatorId), channelDonationMessage.creator.id.eq(creatorId),
channelDonationMessage.createdAt.goe(start), 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 val totalCount = queryFactory
.select(creatorCheers.id.count()) .select(creatorCheers.id.count())
.from(creatorCheers) .from(creatorCheers)
@@ -371,19 +385,22 @@ class DefaultCreatorChannelHomeQueryRepository(
CreatorChannelFanTalkRecord::class.java, CreatorChannelFanTalkRecord::class.java,
creatorCheers.id, creatorCheers.id,
creatorCheers.member.id, creatorCheers.member.id,
creatorCheers.member.nickname, writerName.displayName,
creatorCheers.member.profileImage, creatorCheers.member.profileImage,
creatorCheers.cheers, creatorCheers.cheers,
creatorCheers.languageCode, creatorCheers.languageCode,
creatorCheers.createdAt creatorCheers.createdAt,
writerName.name.id.isNotNull
) )
) )
.from(creatorCheers) .from(creatorCheers)
.join(creatorCheers.member, writer)
.leftJoin(writerName.name).on(writerName.condition)
.where(fanTalkSummaryCondition(creatorId, viewerId)) .where(fanTalkSummaryCondition(creatorId, viewerId))
.orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc()) .orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc())
.limit(1) .limit(1)
.fetchFirst() .fetchFirst()
?.let { it.copy(nickname = it.nickname.removeDeletedNicknamePrefix()) } ?.let { if (it.isLocalizedName) it else it.copy(nickname = it.nickname.removeDeletedNicknamePrefix()) }
return CreatorChannelFanTalkSummaryRecord( return CreatorChannelFanTalkSummaryRecord(
totalCount = totalCount, totalCount = totalCount,
@@ -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.common.SodaException
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix 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.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Gender
@@ -54,15 +55,16 @@ class CreatorChannelHomeQueryService(
fun getHome( fun getHome(
creatorId: Long, creatorId: Long,
viewer: Member, viewer: Member,
now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC) now: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
lang: Lang
): CreatorChannelHome { ): CreatorChannelHome {
val viewerId = viewer.id!! 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") ?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) { if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang) .getMessage("explorer.creator.blocked_access", lang)
.orEmpty() .orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname)) throw SodaException(message = String.format(messageTemplate, creator.nickname))
} }
@@ -100,13 +102,14 @@ class CreatorChannelHomeQueryService(
effectiveViewerGender = effectiveViewerGender effectiveViewerGender = effectiveViewerGender
)?.toDomain(), )?.toDomain(),
latestAudioContent = latestAudioContent, 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( notices = communityQueryService.findHomeCommunityPosts(
creatorId = creatorId, creatorId = creatorId,
viewerId = viewerId, viewerId = viewerId,
isPinned = true, isPinned = true,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
limit = 3 limit = 3,
lang = lang
), ),
schedules = queryPolicy.limitSchedules( schedules = queryPolicy.limitSchedules(
queryPort.findSchedules( queryPort.findSchedules(
@@ -135,9 +138,10 @@ class CreatorChannelHomeQueryService(
viewerId = viewerId, viewerId = viewerId,
isPinned = false, isPinned = false,
canViewAdultContent = canViewAdultContent, canViewAdultContent = canViewAdultContent,
limit = 3 limit = 3,
lang = lang
), ),
fanTalk = queryPort.findFanTalkSummary(creatorId, viewerId).toDomain(), fanTalk = queryPort.findFanTalkSummary(creatorId, viewerId, lang).toDomain(),
introduce = creator.introduce, introduce = creator.introduce,
activity = queryPort.findActivity(creatorId, now).toDomain(), activity = queryPort.findActivity(creatorId, now).toDomain(),
sns = queryPort.findSns(creatorId).toDomain() sns = queryPort.findSns(creatorId).toDomain()
@@ -193,7 +197,8 @@ class CreatorChannelHomeQueryService(
) )
private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation( private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation(
nickname = nickname.removeDeletedNicknamePrefix(), memberId = memberId,
nickname = if (isLocalizedName) nickname else nickname.removeDeletedNicknamePrefix(),
profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(), profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
can = can, can = can,
message = message, message = message,
@@ -43,6 +43,7 @@ data class CreatorChannelLive(
) )
data class CreatorChannelDonation( data class CreatorChannelDonation(
val memberId: Long,
val nickname: String, val nickname: String,
val profileImageUrl: String, val profileImageUrl: String,
val can: Int, val can: Int,
@@ -1,13 +1,14 @@
package kr.co.vividnext.sodalive.v2.creator.channel.home.port.out package kr.co.vividnext.sodalive.v2.creator.channel.home.port.out
import kr.co.vividnext.sodalive.content.ContentType 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.Gender
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import java.time.LocalDateTime import java.time.LocalDateTime
interface CreatorChannelHomeQueryPort { 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 fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
@@ -32,7 +33,8 @@ interface CreatorChannelHomeQueryPort {
creatorId: Long, creatorId: Long,
viewerId: Long?, viewerId: Long?,
now: LocalDateTime, now: LocalDateTime,
limit: Int = 8 limit: Int = 8,
lang: Lang
): List<CreatorChannelDonationRecord> ): List<CreatorChannelDonationRecord>
fun findSchedules( fun findSchedules(
@@ -66,7 +68,7 @@ interface CreatorChannelHomeQueryPort {
limit: Int = 8 limit: Int = 8
): List<CreatorChannelSeriesRecord> ): List<CreatorChannelSeriesRecord>
fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord fun findFanTalkSummary(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkSummaryRecord
fun findActivity(creatorId: Long, now: LocalDateTime): CreatorChannelActivityRecord fun findActivity(creatorId: Long, now: LocalDateTime): CreatorChannelActivityRecord
@@ -112,11 +114,13 @@ data class CreatorChannelAudioContentRecord(
) )
data class CreatorChannelDonationRecord( data class CreatorChannelDonationRecord(
val memberId: Long,
val nickname: String, val nickname: String,
val profileImagePath: String?, val profileImagePath: String?,
val can: Int, val can: Int,
val message: String, val message: String,
val createdAt: LocalDateTime val createdAt: LocalDateTime,
val isLocalizedName: Boolean = false
) )
data class CreatorChannelScheduleRecord( data class CreatorChannelScheduleRecord(
@@ -148,7 +152,8 @@ data class CreatorChannelFanTalkRecord(
val profileImagePath: String?, val profileImagePath: String?,
val content: String, val content: String,
val languageCode: String?, val languageCode: String?,
val createdAt: LocalDateTime val createdAt: LocalDateTime,
val isLocalizedName: Boolean = false
) )
data class CreatorChannelActivityRecord( data class CreatorChannelActivityRecord(
@@ -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.content.theme.QAudioContentTheme.audioContentTheme
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series 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.QSeriesContent.seriesContent
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.Gender 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.CreatorChannelAudioContentRecord
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelCreatorRecord 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.creator.channel.live.port.out.CreatorChannelLiveRecord
import kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoin
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -27,17 +29,19 @@ import java.time.LocalDateTime
class DefaultCreatorChannelLiveQueryRepository( class DefaultCreatorChannelLiveQueryRepository(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory
) : CreatorChannelLiveQueryRepository { ) : 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 return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
CreatorChannelCreatorRecord::class.java, CreatorChannelCreatorRecord::class.java,
member.id, member.id,
member.role, member.role,
member.nickname creatorName.displayName
) )
) )
.from(member) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.id.eq(creatorId), member.id.eq(creatorId),
member.isActive.isTrue member.isActive.isTrue
@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.v2.creator.channel.live.application package kr.co.vividnext.sodalive.v2.creator.channel.live.application
import kr.co.vividnext.sodalive.common.SodaException 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.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -30,7 +30,6 @@ class CreatorChannelLiveQueryService(
private val queryPolicy: CreatorChannelLiveReplayQueryPolicy, private val queryPolicy: CreatorChannelLiveReplayQueryPolicy,
private val memberContentPreferenceService: MemberContentPreferenceService, private val memberContentPreferenceService: MemberContentPreferenceService,
private val messageSource: SodaMessageSource, private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String private val cloudFrontHost: String
) { ) {
@@ -40,17 +39,18 @@ class CreatorChannelLiveQueryService(
sort: ContentSort, sort: ContentSort,
page: Int, page: Int,
size: Int, size: Int,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelLiveTab { ): CreatorChannelLiveTab {
val livePage = queryPolicy.createPage(page, size) val livePage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! 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") ?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) { if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang) .getMessage("explorer.creator.blocked_access", lang)
.orEmpty() .orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname)) throw SodaException(message = String.format(messageTemplate, creator.nickname))
} }
@@ -1,12 +1,13 @@
package kr.co.vividnext.sodalive.v2.creator.channel.live.port.out 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.Gender
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import java.time.LocalDateTime import java.time.LocalDateTime
interface CreatorChannelLiveQueryPort { 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 fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
@@ -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.content.series.translation.QSeriesTranslation
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series 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.QSeriesContent.seriesContent
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember import kr.co.vividnext.sodalive.member.block.QBlockMember
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort 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.CreatorChannelSeriesCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.series.port.out.CreatorChannelSeriesRecord 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 org.springframework.stereotype.Repository
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -23,17 +25,19 @@ import java.time.LocalDateTime
class DefaultCreatorChannelSeriesQueryRepository( class DefaultCreatorChannelSeriesQueryRepository(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory
) : CreatorChannelSeriesQueryRepository { ) : 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 return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
CreatorChannelSeriesCreatorRecord::class.java, CreatorChannelSeriesCreatorRecord::class.java,
member.id, member.id,
member.role, member.role,
member.nickname creatorName.displayName
) )
) )
.from(member) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.id.eq(creatorId), member.id.eq(creatorId),
member.isActive.isTrue member.isActive.isTrue
@@ -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.common.SodaException
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState 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.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -37,18 +38,19 @@ class CreatorChannelSeriesQueryService(
sort: String?, sort: String?,
page: Int?, page: Int?,
size: Int?, size: Int?,
now: LocalDateTime = LocalDateTime.now() now: LocalDateTime = LocalDateTime.now(),
lang: Lang
): CreatorChannelSeriesTab { ): CreatorChannelSeriesTab {
val resolvedSort = queryPolicy.resolveSort(sort) val resolvedSort = queryPolicy.resolveSort(sort)
val seriesPage = queryPolicy.createPage(page, size) val seriesPage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject() val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!! 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") ?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) { if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang) .getMessage("explorer.creator.blocked_access", lang)
.orEmpty() .orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname)) throw SodaException(message = String.format(messageTemplate, creator.nickname))
} }
@@ -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.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState 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.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import java.time.LocalDateTime import java.time.LocalDateTime
interface CreatorChannelSeriesQueryPort { 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 fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.v2.gift.application package kr.co.vividnext.sodalive.v2.gift.application
import kr.co.vividnext.sodalive.common.SodaException 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.Member
import kr.co.vividnext.sodalive.member.MemberRepository 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.Gift
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
@@ -112,7 +114,8 @@ class GiftQueryService(
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val giftMailboxRepository: GiftMailboxRepository, private val giftMailboxRepository: GiftMailboxRepository,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String private val cloudFrontHost: String,
private val creatorLocalizedNameService: CreatorLocalizedNameService
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getFormOptions() = GiftFormOptionsResult( fun getFormOptions() = GiftFormOptionsResult(
@@ -134,7 +137,7 @@ class GiftQueryService(
) )
@Transactional(readOnly = true) @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 normalizedPage = page.coerceAtLeast(0)
val normalizedSize = if (size <= 0) DEFAULT_PAGE_SIZE else size val normalizedSize = if (size <= 0) DEFAULT_PAGE_SIZE else size
val pageable = PageRequest.of(normalizedPage, normalizedSize, Sort.by(Sort.Direction.DESC, "createdAt", "id")) val pageable = PageRequest.of(normalizedPage, normalizedSize, Sort.by(Sort.Direction.DESC, "createdAt", "id"))
@@ -156,13 +159,14 @@ class GiftQueryService(
val membersById = memberRepository.findAllById( val membersById = memberRepository.findAllById(
gifts.content.flatMap { listOf(it.senderMemberId, it.recipientMemberId) }.toSet() gifts.content.flatMap { listOf(it.senderMemberId, it.recipientMemberId) }.toSet()
).associateBy { it.id ?: throw invalidRequest() } ).associateBy { it.id ?: throw invalidRequest() }
val names = creatorLocalizedNameService.findLocalizedNames(membersById.keys, lang)
val deliveriesByGiftId = deliveryRepository.findByGiftIdIn( val deliveriesByGiftId = deliveryRepository.findByGiftIdIn(
gifts.content.mapNotNull { it.id } gifts.content.mapNotNull { it.id }
).associateBy { it.giftId } ).associateBy { it.giftId }
return GiftListResult( return GiftListResult(
totalCount = gifts.totalElements, totalCount = gifts.totalElements,
items = gifts.content.map { it.toGiftListItem(memberId, membersById, deliveriesByGiftId) }, items = gifts.content.map { it.toGiftListItem(memberId, membersById, deliveriesByGiftId, names) },
page = normalizedPage, page = normalizedPage,
size = normalizedSize, size = normalizedSize,
hasNext = gifts.hasNext() hasNext = gifts.hasNext()
@@ -170,12 +174,13 @@ class GiftQueryService(
} }
@Transactional(readOnly = true) @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 gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
val delivery = deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest()) ?: throw invalidRequest() val delivery = deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest()) ?: throw invalidRequest()
val direction = gift.directionFor(memberId) val direction = gift.directionFor(memberId)
val sender = memberRepository.findByIdOrNull(gift.senderMemberId) ?: throw invalidRequest() val sender = memberRepository.findByIdOrNull(gift.senderMemberId) ?: throw invalidRequest()
val recipient = memberRepository.findByIdOrNull(gift.recipientMemberId) ?: 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 val recipientAddress = if (direction == GiftDirection.RECEIVED) delivery.toRecipientAddressResult() else null
return GiftDetailResult( return GiftDetailResult(
@@ -184,8 +189,10 @@ class GiftQueryService(
status = gift.status, status = gift.status,
statusName = gift.status.displayName, statusName = gift.status.displayName,
giftInfo = GiftDetailInfoResult( giftInfo = GiftDetailInfoResult(
recipientCreatorNickname = recipient.nickname.takeIf { direction == GiftDirection.SENT }, recipientCreatorNickname = (names[gift.recipientMemberId] ?: recipient.nickname)
senderNickname = sender.nickname.takeIf { direction == GiftDirection.RECEIVED }, .takeIf { direction == GiftDirection.SENT },
senderNickname = (names[gift.senderMemberId] ?: sender.nickname)
.takeIf { direction == GiftDirection.RECEIVED },
sizeName = gift.sizeCode.displayName, sizeName = gift.sizeCode.displayName,
categoryName = gift.categoryNameSnapshot, categoryName = gift.categoryNameSnapshot,
applicationNo = gift.applicationNo.takeIf { direction == GiftDirection.SENT }, applicationNo = gift.applicationNo.takeIf { direction == GiftDirection.SENT },
@@ -212,7 +219,8 @@ class GiftQueryService(
private fun Gift.toGiftListItem( private fun Gift.toGiftListItem(
memberId: Long, memberId: Long,
membersById: Map<Long, Member>, membersById: Map<Long, Member>,
deliveriesByGiftId: Map<Long, GiftDelivery> deliveriesByGiftId: Map<Long, GiftDelivery>,
names: Map<Long, String>
): GiftListItemResult { ): GiftListItemResult {
val direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED val direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED
val counterpartId = if (direction == GiftDirection.SENT) recipientMemberId else senderMemberId val counterpartId = if (direction == GiftDirection.SENT) recipientMemberId else senderMemberId
@@ -229,7 +237,7 @@ class GiftQueryService(
categoryName = categoryNameSnapshot, categoryName = categoryNameSnapshot,
sizeName = sizeCode.displayName, sizeName = sizeCode.displayName,
counterpartMemberId = counterpartId, counterpartMemberId = counterpartId,
counterpartNickname = counterpart.nickname, counterpartNickname = names[counterpartId] ?: counterpart.nickname,
counterpartProfileImageUrl = counterpart.profileImageUrl(), counterpartProfileImageUrl = counterpart.profileImageUrl(),
recipientAddressRegistered = recipientAddressRegistered, recipientAddressRegistered = recipientAddressRegistered,
recipientAddressDeadlineAt = delivery.recipientAddressDeadlineAt.takeUnless { recipientAddressRegistered }, recipientAddressDeadlineAt = delivery.recipientAddressDeadlineAt.takeUnless { recipientAddressRegistered },
@@ -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.comment.QCreatorCommunityComment.creatorCommunityComment
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike
import kr.co.vividnext.sodalive.extensions.toUtcIso 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.GenderRestriction
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.Gender 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.member.following.QCreatorFollowing.creatorFollowing
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl 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.adapter.out.persistence.QHomeFollowingNewsInbox.homeFollowingNewsInbox
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCommunityPostNews import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCommunityPostNews
@@ -46,12 +48,14 @@ class DefaultHomeFollowingQueryRepository(
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String private val cloudFrontHost: String
) : HomeFollowingQueryRepository { ) : HomeFollowingQueryRepository {
override fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator> { override fun findFollowingCreators(memberId: Long, limit: Int, lang: Lang): List<HomeFollowingCreator> {
val creator = QMember("followingCreator") val creator = QMember("followingCreator")
val creatorName = CreatorNameJoin(creator, lang, "followingCreatorName")
return queryFactory return queryFactory
.select(creator.id, creator.nickname, creator.profileImage) .select(creator.id, creatorName.displayName, creator.profileImage)
.from(creatorFollowing) .from(creatorFollowing)
.join(creatorFollowing.creator, creator) .join(creatorFollowing.creator, creator)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
creatorFollowing.member.id.eq(memberId), creatorFollowing.member.id.eq(memberId),
creatorFollowing.isActive.isTrue, creatorFollowing.isActive.isTrue,
@@ -65,7 +69,7 @@ class DefaultHomeFollowingQueryRepository(
.map { row -> .map { row ->
HomeFollowingCreator( HomeFollowingCreator(
creatorId = row.get(creator.id)!!, creatorId = row.get(creator.id)!!,
creatorNickname = row.get(creator.nickname)!!, creatorNickname = row.get(creatorName.displayName)!!,
creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage)) creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage))
) )
} }
@@ -76,13 +80,23 @@ class DefaultHomeFollowingQueryRepository(
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
effectiveViewerGender: Gender, effectiveViewerGender: Gender,
isViewerCreator: Boolean, isViewerCreator: Boolean,
limit: Int limit: Int,
lang: Lang
): List<HomeFollowingLive> { ): List<HomeFollowingLive> {
val creator = QMember("onAirCreator") val creator = QMember("onAirCreator")
val creatorName = CreatorNameJoin(creator, lang, "followingOnAirCreatorName")
return queryFactory 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) .from(liveRoom)
.join(liveRoom.member, creator) .join(liveRoom.member, creator)
.leftJoin(creatorName.name).on(creatorName.condition)
.join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id)) .join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id))
.where( .where(
creatorFollowing.member.id.eq(memberId), creatorFollowing.member.id.eq(memberId),
@@ -104,9 +118,10 @@ class DefaultHomeFollowingQueryRepository(
HomeFollowingLive( HomeFollowingLive(
liveId = row.get(liveRoom.id)!!, liveId = row.get(liveRoom.id)!!,
creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage)), creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage)),
creatorNickname = row.get(creator.nickname)!!, creatorNickname = row.get(creatorName.displayName)!!,
title = row.get(liveRoom.title)!!, 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, effectiveViewerGender: Gender,
isViewerCreator: Boolean, isViewerCreator: Boolean,
now: LocalDateTime, now: LocalDateTime,
limit: Int limit: Int,
lang: Lang
): List<HomeFollowingSchedule> { ): List<HomeFollowingSchedule> {
val window = monthlyScheduleWindow(now) val window = monthlyScheduleWindow(now)
val liveSchedules = findLiveSchedules( val liveSchedules = findLiveSchedules(
@@ -125,9 +141,10 @@ class DefaultHomeFollowingQueryRepository(
canViewAdultContent, canViewAdultContent,
effectiveViewerGender, effectiveViewerGender,
isViewerCreator, isViewerCreator,
window window,
lang
) )
val audioSchedules = findAudioSchedules(memberId, canViewAdultContent, window) val audioSchedules = findAudioSchedules(memberId, canViewAdultContent, window, lang)
return (liveSchedules + audioSchedules) return (liveSchedules + audioSchedules)
.sortedWith( .sortedWith(
compareBy<HomeFollowingSchedule> { it.scheduledAtUtc } compareBy<HomeFollowingSchedule> { it.scheduledAtUtc }
@@ -141,17 +158,20 @@ class DefaultHomeFollowingQueryRepository(
memberId: Long, memberId: Long,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
nowUtc: LocalDateTime, nowUtc: LocalDateTime,
limit: Int limit: Int,
lang: Lang
): List<HomeFollowingNews> { ): List<HomeFollowingNews> {
val creator = QMember("newsCreator") val creator = QMember("newsCreator")
val creatorName = CreatorNameJoin(creator, lang, "followingNewsCreatorName")
val newsAudioContent = QAudioContent("recentNewsAudioContent") val newsAudioContent = QAudioContent("recentNewsAudioContent")
val newsCommunity = QCreatorCommunity("recentNewsCommunity") val newsCommunity = QCreatorCommunity("recentNewsCommunity")
val rows = queryFactory val rows = queryFactory
.select( .select(
homeFollowingNewsInbox.id, homeFollowingNewsInbox.id,
homeFollowingNewsInbox.newsType, homeFollowingNewsInbox.newsType,
creator.id,
creator.profileImage, creator.profileImage,
creator.nickname, creatorName.displayName,
homeFollowingNewsInbox.targetId, homeFollowingNewsInbox.targetId,
homeFollowingNewsInbox.visibleFromAtUtc, homeFollowingNewsInbox.visibleFromAtUtc,
homeFollowingNewsInbox.rank, homeFollowingNewsInbox.rank,
@@ -163,6 +183,7 @@ class DefaultHomeFollowingQueryRepository(
) )
.from(homeFollowingNewsInbox) .from(homeFollowingNewsInbox)
.join(creator).on(creator.id.eq(homeFollowingNewsInbox.creatorId)) .join(creator).on(creator.id.eq(homeFollowingNewsInbox.creatorId))
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(newsAudioContent).on( .leftJoin(newsAudioContent).on(
homeFollowingNewsInbox.newsType.`in`(FollowingNewsType.AUDIO_CONTENT, FollowingNewsType.CONTENT_RANKING), homeFollowingNewsInbox.newsType.`in`(FollowingNewsType.AUDIO_CONTENT, FollowingNewsType.CONTENT_RANKING),
newsAudioContent.id.eq(homeFollowingNewsInbox.targetId) newsAudioContent.id.eq(homeFollowingNewsInbox.targetId)
@@ -190,11 +211,14 @@ class DefaultHomeFollowingQueryRepository(
.map { it.get(homeFollowingNewsInbox.targetId)!! } .map { it.get(homeFollowingNewsInbox.targetId)!! }
val likeCounts = communityLikeCounts(communityPostIds) val likeCounts = communityLikeCounts(communityPostIds)
val commentCounts = communityCommentCounts(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( private fun Tuple.toHomeFollowingNews(
newsCreator: QMember, newsCreator: QMember,
creatorName: CreatorNameJoin,
newsAudioContent: QAudioContent, newsAudioContent: QAudioContent,
newsCommunity: QCreatorCommunity, newsCommunity: QCreatorCommunity,
likeCounts: Map<Long, Int>, likeCounts: Map<Long, Int>,
@@ -203,12 +227,13 @@ class DefaultHomeFollowingQueryRepository(
val type = get(homeFollowingNewsInbox.newsType)!! val type = get(homeFollowingNewsInbox.newsType)!!
val targetId = get(homeFollowingNewsInbox.targetId)!! val targetId = get(homeFollowingNewsInbox.targetId)!!
val creatorProfileImageUrl = profileImageUrl(get(newsCreator.profileImage)) val creatorProfileImageUrl = profileImageUrl(get(newsCreator.profileImage))
val creatorNickname = get(newsCreator.nickname)!! val creatorNickname = get(creatorName.displayName)!!
val visibleFromAtUtc = get(homeFollowingNewsInbox.visibleFromAtUtc)!!.toUtcIso() val visibleFromAtUtc = get(homeFollowingNewsInbox.visibleFromAtUtc)!!.toUtcIso()
val rank = get(homeFollowingNewsInbox.rank) val rank = get(homeFollowingNewsInbox.rank)
return HomeFollowingNews( return HomeFollowingNews(
newsId = get(homeFollowingNewsInbox.id)!!.toString(), newsId = get(homeFollowingNewsInbox.id)!!.toString(),
creatorMemberId = get(newsCreator.id)!!,
type = type, type = type,
visibleFromAtUtc = visibleFromAtUtc, visibleFromAtUtc = visibleFromAtUtc,
creatorRanking = if (type == FollowingNewsType.CREATOR_RANKING && rank != null) { creatorRanking = if (type == FollowingNewsType.CREATOR_RANKING && rank != null) {
@@ -264,21 +289,24 @@ class DefaultHomeFollowingQueryRepository(
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
effectiveViewerGender: Gender, effectiveViewerGender: Gender,
isViewerCreator: Boolean, isViewerCreator: Boolean,
window: ScheduleWindow window: ScheduleWindow,
lang: Lang
): List<HomeFollowingSchedule> { ): List<HomeFollowingSchedule> {
val creator = QMember("scheduleLiveCreator") val creator = QMember("scheduleLiveCreator")
val creatorName = CreatorNameJoin(creator, lang, "followingLiveScheduleCreatorName")
return queryFactory return queryFactory
.select( .select(
liveRoom.id, liveRoom.id,
creator.id, creator.id,
creator.profileImage, creator.profileImage,
creator.nickname, creatorName.displayName,
liveRoom.title, liveRoom.title,
liveRoom.beginDateTime, liveRoom.beginDateTime,
liveRoom.channelName liveRoom.channelName
) )
.from(liveRoom) .from(liveRoom)
.join(liveRoom.member, creator) .join(liveRoom.member, creator)
.leftJoin(creatorName.name).on(creatorName.condition)
.join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id)) .join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id))
.where( .where(
creatorFollowing.member.id.eq(memberId), creatorFollowing.member.id.eq(memberId),
@@ -294,26 +322,29 @@ class DefaultHomeFollowingQueryRepository(
notBlockedCreatorCondition(memberId, creator.id) notBlockedCreatorCondition(memberId, creator.id)
) )
.fetch() .fetch()
.map { row -> row.toLiveSchedule(creator) } .map { row -> row.toLiveSchedule(creator, creatorName) }
} }
private fun findAudioSchedules( private fun findAudioSchedules(
memberId: Long, memberId: Long,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
window: ScheduleWindow window: ScheduleWindow,
lang: Lang
): List<HomeFollowingSchedule> { ): List<HomeFollowingSchedule> {
val creator = QMember("scheduleAudioCreator") val creator = QMember("scheduleAudioCreator")
val creatorName = CreatorNameJoin(creator, lang, "followingAudioScheduleCreatorName")
return queryFactory return queryFactory
.select( .select(
audioContent.id, audioContent.id,
creator.id, creator.id,
creator.profileImage, creator.profileImage,
creator.nickname, creatorName.displayName,
audioContent.title, audioContent.title,
audioContent.releaseDate audioContent.releaseDate
) )
.from(audioContent) .from(audioContent)
.join(audioContent.member, creator) .join(audioContent.member, creator)
.leftJoin(creatorName.name).on(creatorName.condition)
.join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id)) .join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id))
.where( .where(
creatorFollowing.member.id.eq(memberId), creatorFollowing.member.id.eq(memberId),
@@ -328,17 +359,17 @@ class DefaultHomeFollowingQueryRepository(
notBlockedCreatorCondition(memberId, creator.id) notBlockedCreatorCondition(memberId, creator.id)
) )
.fetch() .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 liveId = get(liveRoom.id)!!
val channelName = get(liveRoom.channelName) val channelName = get(liveRoom.channelName)
return HomeFollowingSchedule( return HomeFollowingSchedule(
scheduleId = "${CreatorActivityType.LIVE}:$liveId", scheduleId = "${CreatorActivityType.LIVE}:$liveId",
creatorId = get(creator.id)!!, creatorId = get(creator.id)!!,
creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)), creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)),
creatorNickname = get(creator.nickname)!!, creatorNickname = get(creatorName.displayName)!!,
title = get(liveRoom.title)!!, title = get(liveRoom.title)!!,
type = CreatorActivityType.LIVE, type = CreatorActivityType.LIVE,
targetId = liveId, 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)!! val contentId = get(audioContent.id)!!
return HomeFollowingSchedule( return HomeFollowingSchedule(
scheduleId = "${CreatorActivityType.AUDIO}:$contentId", scheduleId = "${CreatorActivityType.AUDIO}:$contentId",
creatorId = get(creator.id)!!, creatorId = get(creator.id)!!,
creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)), creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)),
creatorNickname = get(creator.nickname)!!, creatorNickname = get(creatorName.displayName)!!,
title = get(audioContent.title)!!, title = get(audioContent.title)!!,
type = CreatorActivityType.AUDIO, type = CreatorActivityType.AUDIO,
targetId = contentId, targetId = contentId,
@@ -1,6 +1,6 @@
package kr.co.vividnext.sodalive.v2.home.following.application 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.Gender
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
@@ -19,34 +19,34 @@ import java.time.LocalDateTime
class HomeFollowingQueryService( class HomeFollowingQueryService(
private val queryPort: HomeFollowingQueryPort, private val queryPort: HomeFollowingQueryPort,
private val memberContentPreferenceService: MemberContentPreferenceService, private val memberContentPreferenceService: MemberContentPreferenceService,
private val langContext: LangContext,
private val creatorCommunityTranslationService: CreatorCommunityTranslationService, private val creatorCommunityTranslationService: CreatorCommunityTranslationService,
private val nowProvider: () -> LocalDateTime = { LocalDateTime.now(Clock.systemUTC()) } 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 memberId = requireNotNull(member.id)
val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(member) val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(member)
val effectiveViewerGender = member.effectiveGender() val effectiveViewerGender = member.effectiveGender()
val isViewerCreator = member.role == MemberRole.CREATOR val isViewerCreator = member.role == MemberRole.CREATOR
val now = nowProvider() 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 -> val communityPostIds = recentNews.mapNotNull { news ->
if (news.type == FollowingNewsType.COMMUNITY_POST) news.communityPost?.postId else null if (news.type == FollowingNewsType.COMMUNITY_POST) news.communityPost?.postId else null
} }
val displayContents = if (communityPostIds.isEmpty()) { val displayContents = if (communityPostIds.isEmpty()) {
emptyMap() emptyMap()
} else { } else {
creatorCommunityTranslationService.findDisplayContents(communityPostIds, langContext.lang.code) creatorCommunityTranslationService.findDisplayContents(communityPostIds, lang.code)
} }
return HomeFollowing( return HomeFollowing(
followingCreators = queryPort.findFollowingCreators(memberId, FOLLOWING_CREATORS_LIMIT), followingCreators = queryPort.findFollowingCreators(memberId, FOLLOWING_CREATORS_LIMIT, lang),
onAirLives = queryPort.findOnAirLives( onAirLives = queryPort.findOnAirLives(
memberId, memberId,
canViewAdultContent, canViewAdultContent,
effectiveViewerGender, effectiveViewerGender,
isViewerCreator, isViewerCreator,
ON_AIR_LIVES_LIMIT ON_AIR_LIVES_LIMIT,
lang
), ),
recentChats = emptyList(), recentChats = emptyList(),
monthlySchedules = queryPort.findMonthlySchedules( monthlySchedules = queryPort.findMonthlySchedules(
@@ -55,7 +55,8 @@ class HomeFollowingQueryService(
effectiveViewerGender, effectiveViewerGender,
isViewerCreator, isViewerCreator,
now, now,
MONTHLY_SCHEDULES_LIMIT MONTHLY_SCHEDULES_LIMIT,
lang
), ),
recentNews = recentNews.map { news -> recentNews = recentNews.map { news ->
if (news.type != FollowingNewsType.COMMUNITY_POST) return@map news if (news.type != FollowingNewsType.COMMUNITY_POST) return@map news
@@ -22,7 +22,8 @@ data class HomeFollowingLive(
val creatorProfileImageUrl: String, val creatorProfileImageUrl: String,
val creatorNickname: String, val creatorNickname: String,
val title: String, val title: String,
val startedAtUtc: String val startedAtUtc: String,
val creatorMemberId: Long
) )
data class HomeFollowingSchedule( data class HomeFollowingSchedule(
@@ -45,7 +46,8 @@ data class HomeFollowingNews(
val audioContent: HomeFollowingContentNews? = null, val audioContent: HomeFollowingContentNews? = null,
val photoContent: HomeFollowingContentNews? = null, val photoContent: HomeFollowingContentNews? = null,
val contentRanking: HomeFollowingContentRankingNews? = null, val contentRanking: HomeFollowingContentRankingNews? = null,
val communityPost: HomeFollowingCommunityPostNews? = null val communityPost: HomeFollowingCommunityPostNews? = null,
val creatorMemberId: Long
) )
data class HomeFollowingCreatorRankingNews( data class HomeFollowingCreatorRankingNews(
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.home.following.port.out 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.member.Gender
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive 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 import java.time.LocalDateTime
interface HomeFollowingQueryPort { interface HomeFollowingQueryPort {
fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator> fun findFollowingCreators(memberId: Long, limit: Int, lang: Lang): List<HomeFollowingCreator>
fun findOnAirLives( fun findOnAirLives(
memberId: Long, memberId: Long,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
effectiveViewerGender: Gender, effectiveViewerGender: Gender,
isViewerCreator: Boolean, isViewerCreator: Boolean,
limit: Int limit: Int,
lang: Lang
): List<HomeFollowingLive> ): List<HomeFollowingLive>
fun findMonthlySchedules( fun findMonthlySchedules(
@@ -24,13 +26,15 @@ interface HomeFollowingQueryPort {
effectiveViewerGender: Gender, effectiveViewerGender: Gender,
isViewerCreator: Boolean, isViewerCreator: Boolean,
now: LocalDateTime, now: LocalDateTime,
limit: Int limit: Int,
lang: Lang
): List<HomeFollowingSchedule> ): List<HomeFollowingSchedule>
fun findRecentNews( fun findRecentNews(
memberId: Long, memberId: Long,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
nowUtc: LocalDateTime, nowUtc: LocalDateTime,
limit: Int limit: Int,
lang: Lang
): List<HomeFollowingNews> ): List<HomeFollowingNews>
} }
@@ -1,5 +1,11 @@
package kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence 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.domain.CreatorRankingType
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
@@ -9,7 +15,8 @@ import java.time.LocalDateTime
@Repository @Repository
class DefaultCreatorRankingSnapshotRepository( class DefaultCreatorRankingSnapshotRepository(
private val repository: CreatorRankingSnapshotRepository private val repository: CreatorRankingSnapshotRepository,
private val queryFactory: JPAQueryFactory
) : CreatorRankingSnapshotPort { ) : CreatorRankingSnapshotPort {
override fun findSnapshotsByAggregationPeriod( override fun findSnapshotsByAggregationPeriod(
aggregationStartAtUtc: LocalDateTime, aggregationStartAtUtc: LocalDateTime,
@@ -31,9 +38,28 @@ class DefaultCreatorRankingSnapshotRepository(
override fun findLatestVisibleSnapshots( override fun findLatestVisibleSnapshots(
rankingType: CreatorRankingType, rankingType: CreatorRankingType,
nowUtc: LocalDateTime nowUtc: LocalDateTime,
lang: Lang
): List<CreatorRankingSnapshotRecord> { ): List<CreatorRankingSnapshotRecord> {
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( override fun findPreviousVisibleSnapshots(
@@ -64,14 +90,14 @@ class DefaultCreatorRankingSnapshotRepository(
repository.saveAll(newSnapshots.map { it.toEntity(rankingType, visibleFromAtUtc) }) repository.saveAll(newSnapshots.map { it.toEntity(rankingType, visibleFromAtUtc) })
} }
private fun CreatorRankingSnapshot.toRecord(): CreatorRankingSnapshotRecord { private fun CreatorRankingSnapshot.toRecord(displayNickname: String = nickname): CreatorRankingSnapshotRecord {
return CreatorRankingSnapshotRecord( return CreatorRankingSnapshotRecord(
rankingType = rankingType, rankingType = rankingType,
aggregationStartAtUtc = aggregationStartAtUtc, aggregationStartAtUtc = aggregationStartAtUtc,
aggregationEndAtUtc = aggregationEndAtUtc, aggregationEndAtUtc = aggregationEndAtUtc,
visibleFromAtUtc = visibleFromAtUtc, visibleFromAtUtc = visibleFromAtUtc,
creatorId = creatorId, creatorId = creatorId,
nickname = nickname, nickname = displayNickname,
profileImageUrl = profileImageUrl, profileImageUrl = profileImageUrl,
rankNo = rankNo, rankNo = rankNo,
finalScore = finalScore, finalScore = finalScore,
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.ranking.application 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.common.domain.toCdnUrl
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
@@ -24,11 +25,11 @@ class CreatorRankingQueryService(
) { ) {
private val log = LoggerFactory.getLogger(javaClass) private val log = LoggerFactory.getLogger(javaClass)
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult { fun getCreatorRankings(viewerMemberId: Long?, lang: Lang): CreatorRankingResult {
val startedAt = System.currentTimeMillis() val startedAt = System.currentTimeMillis()
return runCatching { return runCatching {
val nowUtc = nowUtc() val nowUtc = nowUtc()
val latestSnapshots = findLatestVisibleSnapshots(nowUtc) val latestSnapshots = findLatestVisibleSnapshots(nowUtc, lang)
val latestItems = latestSnapshots.toRankedItems() val latestItems = latestSnapshots.toRankedItems()
if (latestItems.isEmpty()) { if (latestItems.isEmpty()) {
return@runCatching QueryLogResult(CreatorRankingResult(showRankChange = false, items = emptyList()), 0) return@runCatching QueryLogResult(CreatorRankingResult(showRankChange = false, items = emptyList()), 0)
@@ -69,15 +70,15 @@ class CreatorRankingQueryService(
}.getOrThrow().result }.getOrThrow().result
} }
private fun findLatestVisibleSnapshots(nowUtc: LocalDateTime): List<CreatorRankingSnapshotRecord> { private fun findLatestVisibleSnapshots(nowUtc: LocalDateTime, lang: Lang): List<CreatorRankingSnapshotRecord> {
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc) val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc, lang)
if (latestSnapshots.isNotEmpty()) return latestSnapshots if (latestSnapshots.isNotEmpty()) return latestSnapshots
runCatching { snapshotJobService.refreshLastCompletedWeekByFallback() } runCatching { snapshotJobService.refreshLastCompletedWeekByFallback() }
.onFailure { ex -> .onFailure { ex ->
log.warn("event=creator_ranking_query_fallback_failure error={}", ex.message, 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( private data class QueryLogResult(
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.ranking.port.out 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 kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -15,7 +16,8 @@ interface CreatorRankingSnapshotPort {
fun findLatestVisibleSnapshots( fun findLatestVisibleSnapshots(
rankingType: CreatorRankingType, rankingType: CreatorRankingType,
nowUtc: LocalDateTime nowUtc: LocalDateTime,
lang: Lang
): List<CreatorRankingSnapshotRecord> ): List<CreatorRankingSnapshotRecord>
fun findPreviousVisibleSnapshots( fun findPreviousVisibleSnapshots(
@@ -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.block.QBlockMember
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing import kr.co.vividnext.sodalive.member.following.QCreatorFollowing
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType 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.RecommendationScoreSpec
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord
@@ -62,22 +63,26 @@ class DefaultHomeRecommendationQueryRepository(
memberId: Long?, memberId: Long?,
includeAdultLives: Boolean, includeAdultLives: Boolean,
isViewerCreator: Boolean, isViewerCreator: Boolean,
effectiveViewerGender: Gender? effectiveViewerGender: Gender?,
lang: Lang
): List<HomeLiveRecommendationRecord> { ): List<HomeLiveRecommendationRecord> {
val creatorName = CreatorNameJoin(member, lang, "homeLiveCreatorName")
return queryFactory return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
HomeLiveRecommendationRecord::class.java, HomeLiveRecommendationRecord::class.java,
liveRoom.id, liveRoom.id,
member.nickname, creatorName.displayName,
member.profileImage, member.profileImage,
liveRoom.title, liveRoom.title,
liveRoom.price, liveRoom.price,
liveRoom.beginDateTime liveRoom.beginDateTime,
member.id
) )
) )
.from(liveRoom) .from(liveRoom)
.join(liveRoom.member, member) .join(liveRoom.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
liveRoom.isActive.isTrue, liveRoom.isActive.isTrue,
liveRoom.channelName.isNotNull, liveRoom.channelName.isNotNull,
@@ -138,11 +143,12 @@ class DefaultHomeRecommendationQueryRepository(
override fun findRecentlyActiveCreators( override fun findRecentlyActiveCreators(
limit: Int, limit: Int,
memberId: Long?, memberId: Long?,
includeAdultActivities: Boolean includeAdultActivities: Boolean,
lang: Lang
): List<RecentlyActiveCreatorRecord> { ): List<RecentlyActiveCreatorRecord> {
val sql = """ val sql = """
select ranked.creator_id, select ranked.creator_id,
ranked.creator_nickname, coalesce(creator_name.display_name, ranked.creator_nickname),
ranked.creator_profile_image, ranked.creator_profile_image,
ranked.activity_type, ranked.activity_type,
ranked.activity_at, ranked.activity_at,
@@ -200,6 +206,9 @@ class DefaultHomeRecommendationQueryRepository(
and m.role = 'CREATOR' and m.role = 'CREATOR'
) activities ) activities
) ranked ) 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 where ranked.creator_rank = 1
and ${notBlockedCreatorSql("ranked.creator_id")} and ${notBlockedCreatorSql("ranked.creator_id")}
order by ranked.activity_at desc, ranked.target_sort_id desc order by ranked.activity_at desc, ranked.target_sort_id desc
@@ -210,6 +219,7 @@ class DefaultHomeRecommendationQueryRepository(
.setParameter("liveReplayTheme", LIVE_REPLAY_THEME) .setParameter("liveReplayTheme", LIVE_REPLAY_THEME)
.setParameter("memberId", memberId) .setParameter("memberId", memberId)
.setParameter("includeAdultActivities", includeAdultActivities) .setParameter("includeAdultActivities", includeAdultActivities)
.setParameter("lang", lang.name)
.setParameter("limit", limit) .setParameter("limit", limit)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -232,7 +242,8 @@ class DefaultHomeRecommendationQueryRepository(
offset: Long, offset: Long,
limit: Int, limit: Int,
memberId: Long?, memberId: Long?,
includeAdultContents: Boolean includeAdultContents: Boolean,
lang: Lang
): List<RecentDebutCreatorRecord> { ): List<RecentDebutCreatorRecord> {
val sql = """ val sql = """
with creator_debut as ( with creator_debut as (
@@ -328,9 +339,11 @@ class DefaultHomeRecommendationQueryRepository(
group by communication.creator_id group by communication.creator_id
) )
select m.id as 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 m.profile_image as creator_profile_image
from member m 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 join creator_debut cd on cd.creator_id = m.id
left join follow_stats fs on fs.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 left join content_stats cs on cs.creator_id = m.id
@@ -354,6 +367,7 @@ class DefaultHomeRecommendationQueryRepository(
val query = entityManager.createNativeQuery(sql) val query = entityManager.createNativeQuery(sql)
.setRecommendationQueryParameters(now, limit) .setRecommendationQueryParameters(now, limit)
.setParameter("lang", lang.name)
.setParameter("offset", offset) .setParameter("offset", offset)
.setParameter("memberId", memberId) .setParameter("memberId", memberId)
.setParameter("includeAdultContents", includeAdultContents) .setParameter("includeAdultContents", includeAdultContents)
@@ -437,7 +451,7 @@ class DefaultHomeRecommendationQueryRepository(
) )
select ec.content_id as content_id, select ec.content_id as content_id,
m.id as creator_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, m.profile_image as creator_profile_image,
ec.title as title, ec.title as title,
ec.price as price, ec.price as price,
@@ -447,6 +461,8 @@ class DefaultHomeRecommendationQueryRepository(
ec.is_original_series as is_original_series ec.is_original_series as is_original_series
from eligible_contents ec from eligible_contents ec
join member m on m.id = ec.creator_id 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 join creator_debut cd on cd.creator_id = ec.creator_id
where ec.active_rank = 1 where ec.active_rank = 1
and m.is_active = true and m.is_active = true
@@ -468,6 +484,7 @@ class DefaultHomeRecommendationQueryRepository(
val query = entityManager.createNativeQuery(sql) val query = entityManager.createNativeQuery(sql)
.setParameter("now", now) .setParameter("now", now)
.setParameter("lang", Lang.fromCode(locale).name)
.setParameter("limit", limit) .setParameter("limit", limit)
.setParameter("offset", offset) .setParameter("offset", offset)
.setParameter("memberId", memberId) .setParameter("memberId", memberId)
@@ -832,20 +849,23 @@ class DefaultHomeRecommendationQueryRepository(
override fun findCheerCreatorRecommendationDetails( override fun findCheerCreatorRecommendationDetails(
creatorIds: List<Long>, creatorIds: List<Long>,
memberId: Long? memberId: Long?,
lang: Lang
): List<HomeCheerCreatorRecommendationRecord> { ): List<HomeCheerCreatorRecommendationRecord> {
if (creatorIds.isEmpty()) return emptyList() if (creatorIds.isEmpty()) return emptyList()
val creatorName = CreatorNameJoin(member, lang, "homeCheerCreatorName")
return queryFactory return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
HomeCheerCreatorRecommendationRecord::class.java, HomeCheerCreatorRecommendationRecord::class.java,
member.id, member.id,
member.nickname, creatorName.displayName,
member.profileImage member.profileImage
) )
) )
.from(member) .from(member)
.leftJoin(creatorName.name).on(creatorName.condition)
.where( .where(
member.isActive.isTrue, member.isActive.isTrue,
member.id.`in`(creatorIds), member.id.`in`(creatorIds),
@@ -859,9 +879,11 @@ class DefaultHomeRecommendationQueryRepository(
override fun findPopularCommunityRecommendationDetails( override fun findPopularCommunityRecommendationDetails(
communityIds: List<Long>, communityIds: List<Long>,
memberId: Long?, memberId: Long?,
includeAdultCommunities: Boolean includeAdultCommunities: Boolean,
lang: Lang
): List<HomePopularCommunityRecommendationRecord> { ): List<HomePopularCommunityRecommendationRecord> {
if (communityIds.isEmpty()) return emptyList() if (communityIds.isEmpty()) return emptyList()
val creatorName = CreatorNameJoin(member, lang, "homeCommunityCreatorName")
return queryFactory return queryFactory
.select( .select(
@@ -869,7 +891,7 @@ class DefaultHomeRecommendationQueryRepository(
HomePopularCommunityRecommendationRecord::class.java, HomePopularCommunityRecommendationRecord::class.java,
creatorCommunity.id, creatorCommunity.id,
member.id, member.id,
member.nickname, creatorName.displayName,
member.profileImage, member.profileImage,
creatorCommunity.imagePath, creatorCommunity.imagePath,
creatorCommunity.audioPath, creatorCommunity.audioPath,
@@ -884,6 +906,7 @@ class DefaultHomeRecommendationQueryRepository(
) )
.from(creatorCommunity) .from(creatorCommunity)
.join(creatorCommunity.member, member) .join(creatorCommunity.member, member)
.leftJoin(creatorName.name).on(creatorName.condition)
.leftJoin(creatorCommunityLike).on( .leftJoin(creatorCommunityLike).on(
creatorCommunityLike.creatorCommunity.id.eq(creatorCommunity.id), creatorCommunityLike.creatorCommunity.id.eq(creatorCommunity.id),
creatorCommunityLike.isActive.isTrue creatorCommunityLike.isActive.isTrue
@@ -905,6 +928,7 @@ class DefaultHomeRecommendationQueryRepository(
creatorCommunity.id, creatorCommunity.id,
member.id, member.id,
member.nickname, member.nickname,
creatorName.name.displayName,
member.profileImage, member.profileImage,
creatorCommunity.imagePath, creatorCommunity.imagePath,
creatorCommunity.audioPath, creatorCommunity.audioPath,
@@ -919,7 +943,8 @@ class DefaultHomeRecommendationQueryRepository(
memberId: Long?, memberId: Long?,
includeAdultGenres: Boolean, includeAdultGenres: Boolean,
genreLimit: Int, genreLimit: Int,
creatorLimit: Int creatorLimit: Int,
lang: Lang
): List<HomeGenreCreatorRecommendationGroup> { ): List<HomeGenreCreatorRecommendationGroup> {
val groups = mutableListOf<HomeGenreCreatorRecommendationGroup>() val groups = mutableListOf<HomeGenreCreatorRecommendationGroup>()
val selectedGenreIds = mutableSetOf<Long>() val selectedGenreIds = mutableSetOf<Long>()
@@ -931,7 +956,7 @@ class DefaultHomeRecommendationQueryRepository(
) )
viewedTargets.forEach { target -> viewedTargets.forEach { target ->
groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit)) groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit, lang))
selectedGenreIds.add(target.id) selectedGenreIds.add(target.id)
} }
@@ -941,7 +966,8 @@ class DefaultHomeRecommendationQueryRepository(
memberId = memberId, memberId = memberId,
includeAdultGenres = includeAdultGenres, includeAdultGenres = includeAdultGenres,
genreLimit = genreLimit, genreLimit = genreLimit,
creatorLimit = creatorLimit creatorLimit = creatorLimit,
lang = lang
) )
return groups return groups
@@ -993,7 +1019,8 @@ class DefaultHomeRecommendationQueryRepository(
memberId: Long?, memberId: Long?,
includeAdultGenres: Boolean, includeAdultGenres: Boolean,
genreLimit: Int, genreLimit: Int,
creatorLimit: Int creatorLimit: Int,
lang: Lang
) { ) {
val fullTargets = findFallbackGenreRecommendationTargets( val fullTargets = findFallbackGenreRecommendationTargets(
memberId = memberId, memberId = memberId,
@@ -1004,7 +1031,7 @@ class DefaultHomeRecommendationQueryRepository(
) )
fullTargets.forEach { target -> fullTargets.forEach { target ->
groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit)) groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit, lang))
selectedGenreIds.add(target.id) selectedGenreIds.add(target.id)
} }
val partialTargets = findFallbackGenreRecommendationTargets( val partialTargets = findFallbackGenreRecommendationTargets(
@@ -1016,7 +1043,7 @@ class DefaultHomeRecommendationQueryRepository(
) )
partialTargets.forEach { target -> partialTargets.forEach { target ->
groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit)) groups.add(toGenreCreatorRecommendationGroup(target, memberId, includeAdultGenres, creatorLimit, lang))
selectedGenreIds.add(target.id) selectedGenreIds.add(target.id)
} }
} }
@@ -1099,7 +1126,8 @@ class DefaultHomeRecommendationQueryRepository(
target: GenreRecommendationTarget, target: GenreRecommendationTarget,
memberId: Long?, memberId: Long?,
includeAdultGenres: Boolean, includeAdultGenres: Boolean,
creatorLimit: Int creatorLimit: Int,
lang: Lang
): HomeGenreCreatorRecommendationGroup { ): HomeGenreCreatorRecommendationGroup {
return HomeGenreCreatorRecommendationGroup( return HomeGenreCreatorRecommendationGroup(
genreId = target.id, genreId = target.id,
@@ -1109,7 +1137,8 @@ class DefaultHomeRecommendationQueryRepository(
genreId = target.id, genreId = target.id,
memberId = memberId, memberId = memberId,
includeAdultGenres = includeAdultGenres, includeAdultGenres = includeAdultGenres,
creatorLimit = creatorLimit creatorLimit = creatorLimit,
lang = lang
) )
) )
} }
@@ -1152,11 +1181,12 @@ class DefaultHomeRecommendationQueryRepository(
genreId: Long, genreId: Long,
memberId: Long?, memberId: Long?,
includeAdultGenres: Boolean, includeAdultGenres: Boolean,
creatorLimit: Int creatorLimit: Int,
lang: Lang
): List<HomeGenreCreatorRecommendationRecord> { ): List<HomeGenreCreatorRecommendationRecord> {
val sql = """ val sql = """
select candidates.creator_id, select candidates.creator_id,
candidates.creator_nickname, coalesce(creator_name.display_name, candidates.creator_nickname),
candidates.creator_profile_image candidates.creator_profile_image
from ( from (
select m.id as creator_id, select m.id as creator_id,
@@ -1190,12 +1220,16 @@ class DefaultHomeRecommendationQueryRepository(
) )
group by m.id, m.nickname, m.profile_image group by m.id, m.nickname, m.profile_image
) candidates ) 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 order by rand() asc
limit :creatorLimit limit :creatorLimit
""".trimIndent() """.trimIndent()
val query = entityManager.createNativeQuery(sql) val query = entityManager.createNativeQuery(sql)
.setParameter("genreId", genreId) .setParameter("genreId", genreId)
.setParameter("lang", lang.name)
.setParameter("memberId", memberId) .setParameter("memberId", memberId)
.setParameter("includeAdultGenres", includeAdultGenres) .setParameter("includeAdultGenres", includeAdultGenres)
.setParameter("creatorLimit", creatorLimit) .setParameter("creatorLimit", creatorLimit)
@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.v2.recommendation.application package kr.co.vividnext.sodalive.v2.recommendation.application
import kr.co.vividnext.sodalive.i18n.Lang 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.Gender
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService 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 cheerCreatorSnapshotFallbackService: CheerCreatorSnapshotFallbackPort? = null,
private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null, private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null,
private val clock: Clock = Clock.systemUTC(), private val clock: Clock = Clock.systemUTC(),
private val langContext: LangContext,
private val creatorCommunityTranslationService: CreatorCommunityTranslationService private val creatorCommunityTranslationService: CreatorCommunityTranslationService
) { ) {
private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy() private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy()
@@ -44,7 +42,8 @@ class HomeRecommendationQueryService(
memberId: Long? = null, memberId: Long? = null,
includeAdultLives: Boolean = false, includeAdultLives: Boolean = false,
isViewerCreator: Boolean = false, isViewerCreator: Boolean = false,
effectiveViewerGender: Gender? = null effectiveViewerGender: Gender? = null,
lang: Lang
): List<HomeLiveRecommendationRecord> { ): List<HomeLiveRecommendationRecord> {
return queryPort.findLiveRecommendations( return queryPort.findLiveRecommendations(
offset, offset,
@@ -52,7 +51,8 @@ class HomeRecommendationQueryService(
memberId, memberId,
includeAdultLives, includeAdultLives,
isViewerCreator, isViewerCreator,
effectiveViewerGender effectiveViewerGender,
lang
) )
} }
@@ -68,9 +68,10 @@ class HomeRecommendationQueryService(
fun findRecentlyActiveCreators( fun findRecentlyActiveCreators(
limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT, limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT,
memberId: Long? = null, memberId: Long? = null,
includeAdultActivities: Boolean = false includeAdultActivities: Boolean = false,
lang: Lang
): List<RecentlyActiveCreatorRecord> { ): List<RecentlyActiveCreatorRecord> {
return queryPort.findRecentlyActiveCreators(limit, memberId, includeAdultActivities) return queryPort.findRecentlyActiveCreators(limit, memberId, includeAdultActivities, lang)
} }
fun findRecentDebutCreators( fun findRecentDebutCreators(
@@ -78,9 +79,10 @@ class HomeRecommendationQueryService(
offset: Long = 0, offset: Long = 0,
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT, limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT,
memberId: Long? = null, memberId: Long? = null,
includeAdultContents: Boolean = false includeAdultContents: Boolean = false,
lang: Lang
): List<RecentDebutCreatorRecord> { ): List<RecentDebutCreatorRecord> {
return queryPort.findRecentDebutCreators(now, offset, limit, memberId, includeAdultContents) return queryPort.findRecentDebutCreators(now, offset, limit, memberId, includeAdultContents, lang)
} }
fun findFirstAudioContents( fun findFirstAudioContents(
@@ -88,9 +90,10 @@ class HomeRecommendationQueryService(
offset: Long = 0, offset: Long = 0,
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT, limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT,
memberId: Long? = null, memberId: Long? = null,
includeAdultContents: Boolean = false includeAdultContents: Boolean = false,
lang: Lang
): List<HomeFirstAudioContentRecord> { ): List<HomeFirstAudioContentRecord> {
return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents, langContext.lang.code) return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents, lang.code)
} }
fun findAiCharacterRecommendations( fun findAiCharacterRecommendations(
@@ -128,10 +131,11 @@ class HomeRecommendationQueryService(
fun findCheerCreatorRecommendations( fun findCheerCreatorRecommendations(
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT, limit: Int = DEFAULT_CHEER_CREATOR_LIMIT,
memberId: Long? = null memberId: Long? = null,
lang: Lang
): List<HomeCheerCreatorRecommendationRecord> { ): List<HomeCheerCreatorRecommendationRecord> {
val snapshots = findCheerCreatorSnapshotsWithFallback() 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 } .associateBy { it.creatorId }
return snapshots.mapNotNull { detailsById[it.targetId] }.take(limit) return snapshots.mapNotNull { detailsById[it.targetId] }.take(limit)
@@ -155,13 +159,15 @@ class HomeRecommendationQueryService(
fun findPopularCommunityRecommendations( fun findPopularCommunityRecommendations(
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT, limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
memberId: Long? = null, memberId: Long? = null,
includeAdultCommunities: Boolean = false includeAdultCommunities: Boolean = false,
lang: Lang
): List<HomePopularCommunityRecommendationRecord> { ): List<HomePopularCommunityRecommendationRecord> {
val snapshots = findPopularCommunitySnapshotsWithFallback() val snapshots = findPopularCommunitySnapshotsWithFallback()
val detailsById = queryPort.findPopularCommunityRecommendationDetails( val detailsById = queryPort.findPopularCommunityRecommendationDetails(
snapshots.map { it.targetId }, snapshots.map { it.targetId },
memberId, memberId,
includeAdultCommunities includeAdultCommunities,
lang
) )
.associateBy { it.communityId } .associateBy { it.communityId }
val selectedCreatorIds = mutableSetOf<Long>() val selectedCreatorIds = mutableSetOf<Long>()
@@ -173,7 +179,7 @@ class HomeRecommendationQueryService(
val displayContents = creatorCommunityTranslationService.findDisplayContents( val displayContents = creatorCommunityTranslationService.findDisplayContents(
recommendations.map { it.communityId }, recommendations.map { it.communityId },
langContext.lang.code lang.code
) )
return recommendations.map { recommendation -> return recommendations.map { recommendation ->
displayContents[recommendation.communityId] displayContents[recommendation.communityId]
@@ -201,13 +207,14 @@ class HomeRecommendationQueryService(
memberId: Long?, memberId: Long?,
includeAdultGenres: Boolean, includeAdultGenres: Boolean,
genreLimit: Int = DEFAULT_GENRE_CREATOR_GENRE_LIMIT, genreLimit: Int = DEFAULT_GENRE_CREATOR_GENRE_LIMIT,
creatorLimit: Int = DEFAULT_GENRE_CREATOR_CREATOR_LIMIT creatorLimit: Int = DEFAULT_GENRE_CREATOR_CREATOR_LIMIT,
lang: Lang
): List<HomeGenreCreatorRecommendationGroup> { ): List<HomeGenreCreatorRecommendationGroup> {
val selectedCreatorIds = mutableSetOf<Long>() val selectedCreatorIds = mutableSetOf<Long>()
val partialFallbackGroups = mutableListOf<HomeGenreCreatorRecommendationGroup>() val partialFallbackGroups = mutableListOf<HomeGenreCreatorRecommendationGroup>()
val selectedGroups = mutableListOf<HomeGenreCreatorRecommendationGroup>() val selectedGroups = mutableListOf<HomeGenreCreatorRecommendationGroup>()
queryPort.findGenreCreatorRecommendations(memberId, includeAdultGenres, genreLimit, creatorLimit) queryPort.findGenreCreatorRecommendations(memberId, includeAdultGenres, genreLimit, creatorLimit, lang)
.forEach { group -> .forEach { group ->
if (selectedGroups.size >= genreLimit) return@forEach if (selectedGroups.size >= genreLimit) return@forEach
@@ -12,7 +12,8 @@ interface HomeRecommendationQueryPort {
memberId: Long? = null, memberId: Long? = null,
includeAdultLives: Boolean = false, includeAdultLives: Boolean = false,
isViewerCreator: Boolean = false, isViewerCreator: Boolean = false,
effectiveViewerGender: Gender? = null effectiveViewerGender: Gender? = null,
lang: Lang
): List<HomeLiveRecommendationRecord> ): List<HomeLiveRecommendationRecord>
fun findHomeBanners( fun findHomeBanners(
@@ -25,7 +26,8 @@ interface HomeRecommendationQueryPort {
fun findRecentlyActiveCreators( fun findRecentlyActiveCreators(
limit: Int, limit: Int,
memberId: Long? = null, memberId: Long? = null,
includeAdultActivities: Boolean = false includeAdultActivities: Boolean = false,
lang: Lang
): List<RecentlyActiveCreatorRecord> ): List<RecentlyActiveCreatorRecord>
fun findRecentDebutCreators( fun findRecentDebutCreators(
@@ -33,7 +35,8 @@ interface HomeRecommendationQueryPort {
offset: Long = 0, offset: Long = 0,
limit: Int, limit: Int,
memberId: Long? = null, memberId: Long? = null,
includeAdultContents: Boolean = false includeAdultContents: Boolean = false,
lang: Lang
): List<RecentDebutCreatorRecord> ): List<RecentDebutCreatorRecord>
fun findFirstAudioContents( fun findFirstAudioContents(
@@ -72,20 +75,23 @@ interface HomeRecommendationQueryPort {
fun findCheerCreatorRecommendationDetails( fun findCheerCreatorRecommendationDetails(
creatorIds: List<Long>, creatorIds: List<Long>,
memberId: Long? = null memberId: Long? = null,
lang: Lang
): List<HomeCheerCreatorRecommendationRecord> ): List<HomeCheerCreatorRecommendationRecord>
fun findPopularCommunityRecommendationDetails( fun findPopularCommunityRecommendationDetails(
communityIds: List<Long>, communityIds: List<Long>,
memberId: Long? = null, memberId: Long? = null,
includeAdultCommunities: Boolean includeAdultCommunities: Boolean,
lang: Lang
): List<HomePopularCommunityRecommendationRecord> ): List<HomePopularCommunityRecommendationRecord>
fun findGenreCreatorRecommendations( fun findGenreCreatorRecommendations(
memberId: Long?, memberId: Long?,
includeAdultGenres: Boolean, includeAdultGenres: Boolean,
genreLimit: Int, genreLimit: Int,
creatorLimit: Int creatorLimit: Int,
lang: Lang
): List<HomeGenreCreatorRecommendationGroup> ): List<HomeGenreCreatorRecommendationGroup>
} }
@@ -95,7 +101,8 @@ data class HomeLiveRecommendationRecord(
val creatorProfileImage: String?, val creatorProfileImage: String?,
val title: String, val title: String,
val price: Int, val price: Int,
val beginDateTime: LocalDateTime val beginDateTime: LocalDateTime,
val creatorMemberId: Long
) )
data class HomeBannerRecommendationRecord( data class HomeBannerRecommendationRecord(
@@ -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?)
@@ -1,23 +1,43 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.repository 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.UserCreatorChatMessage
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom 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.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository 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 import org.springframework.stereotype.Repository
@Repository @Repository
interface UserCreatorChatMessageRepository : JpaRepository<UserCreatorChatMessage, Long> { interface UserCreatorChatMessageRepository : JpaRepository<UserCreatorChatMessage, Long> {
fun findByChatRoomAndIsActiveTrueOrderByIdDesc( @Query(
chatRoom: UserCreatorChatRoom, """
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 pageable: Pageable
): List<UserCreatorChatMessage> ): List<UserCreatorChatMessageRow>
fun findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc(
chatRoom: UserCreatorChatRoom,
id: Long,
pageable: Pageable
): List<UserCreatorChatMessage>
fun existsByChatRoomAndIsActiveTrueAndIdLessThan(chatRoom: UserCreatorChatRoom, id: Long): Boolean fun existsByChatRoomAndIsActiveTrueAndIdLessThan(chatRoom: UserCreatorChatRoom, id: Long): Boolean
} }
@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.repository 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.UserCreatorChatParticipant
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatOpponentProfile
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
@@ -33,4 +35,27 @@ interface UserCreatorChatParticipantRepository : JpaRepository<UserCreatorChatPa
@Param("roomId") roomId: Long, @Param("roomId") roomId: Long,
@Param("memberId") memberId: Long @Param("memberId") memberId: Long
): UserCreatorChatParticipant? ): UserCreatorChatParticipant?
@Query(
"""
select new kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatOpponentProfile(
coalesce(opponentName.displayName, opponent.nickname), opponent.profileImage
)
from UserCreatorChatParticipant p
join p.member opponent
left join CreatorLocalizedNameEntity opponentName
on opponentName.memberId = opponent.id
and opponentName.lang = :lang
and opponentName.isActive = true
and opponent.role = kr.co.vividnext.sodalive.member.MemberRole.CREATOR
where p.isActive = true
and p.chatRoom.id = :roomId
and opponent.id <> :memberId
"""
)
fun findActiveOpponentProfile(
@Param("roomId") roomId: Long,
@Param("memberId") memberId: Long,
@Param("lang") lang: Lang
): UserCreatorChatOpponentProfile?
} }
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.repository 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.chat.dto.ChatRoomListQueryDto
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
@@ -41,7 +42,7 @@ interface UserCreatorChatRoomRepository : JpaRepository<UserCreatorChatRoom, Lon
select new kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto( select new kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto(
r.id, r.id,
'DM', 'DM',
opponent.member.nickname, coalesce(opponentName.displayName, opponentMember.nickname),
opponent.member.profileImage, opponent.member.profileImage,
m.textMessage, m.textMessage,
str(m.messageType), str(m.messageType),
@@ -51,6 +52,12 @@ interface UserCreatorChatRoomRepository : JpaRepository<UserCreatorChatRoom, Lon
join r.participants mine join r.participants mine
join r.participants opponent join r.participants opponent
join r.messages m join r.messages m
join opponent.member opponentMember
left join CreatorLocalizedNameEntity opponentName
on opponentName.memberId = opponentMember.id
and opponentName.lang = :lang
and opponentName.isActive = true
and opponentMember.role = kr.co.vividnext.sodalive.member.MemberRole.CREATOR
where r.isActive = true where r.isActive = true
and mine.isActive = true and mine.isActive = true
and mine.member.id = :memberId and mine.member.id = :memberId
@@ -90,6 +97,7 @@ interface UserCreatorChatRoomRepository : JpaRepository<UserCreatorChatRoom, Lon
@Param("cursorChatType") cursorChatType: String?, @Param("cursorChatType") cursorChatType: String?,
@Param("cursorRoomId") cursorRoomId: Long?, @Param("cursorRoomId") cursorRoomId: Long?,
@Param("followedCreatorsOnly") followedCreatorsOnly: Boolean, @Param("followedCreatorsOnly") followedCreatorsOnly: Boolean,
@Param("lang") lang: Lang,
pageable: Pageable pageable: Pageable
): List<ChatRoomListQueryDto> ): List<ChatRoomListQueryDto>
} }
@@ -8,11 +8,14 @@ import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberKind import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessage import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessage
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatParticipant 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.SendUserCreatorChatMessageResponse
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.SendUserCreatorVoiceMessageRequest 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.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.UserCreatorChatMessagesPageResponse
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatRoomOpenResponse import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatRoomOpenResponse
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository
@@ -52,6 +56,8 @@ class UserCreatorChatService(
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val localizedNameService: CreatorLocalizedNameService,
private val langContext: LangContext,
@Value("\${cloud.aws.s3.bucket}") @Value("\${cloud.aws.s3.bucket}")
private val bucket: String, private val bucket: String,
@@ -80,7 +86,7 @@ class UserCreatorChatService(
fun openRoom(member: Member, roomId: Long, limit: Int = 20): UserCreatorChatRoomOpenResponse { fun openRoom(member: Member, roomId: Long, limit: Int = 20): UserCreatorChatRoomOpenResponse {
val room = findRoom(roomId) val room = findRoom(roomId)
requireParticipant(roomId, member.id!!) 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") ?: throw SodaException(messageKey = "chat.room.invalid_access")
val opponentProfilePath = opponent.profileImage ?: "profile/default-profile.png" val opponentProfilePath = opponent.profileImage ?: "profile/default-profile.png"
val page = getMessages(member, roomId, cursor = null, limit = limit) val page = getMessages(member, roomId, cursor = null, limit = limit)
@@ -98,12 +104,8 @@ class UserCreatorChatService(
val room = findRoom(roomId) val room = findRoom(roomId)
requireParticipant(roomId, member.id!!) requireParticipant(roomId, member.id!!)
val pageable = PageRequest.of(0, limit.coerceIn(1, 100)) val pageable = PageRequest.of(0, limit.coerceIn(1, 100))
val fetched = if (cursor != null) { val fetched = messageRepository.findMessageHistory(room, cursor, langContext.lang, pageable)
messageRepository.findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc(room, cursor, pageable) val nextCursor = fetched.minOfOrNull { it.messageId }
} else {
messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(room, pageable)
}
val nextCursor = fetched.minByOrNull { it.id ?: Long.MAX_VALUE }?.id
val hasMore = nextCursor?.let { messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, it) } ?: false val hasMore = nextCursor?.let { messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, it) } ?: false
return UserCreatorChatMessagesPageResponse( return UserCreatorChatMessagesPageResponse(
messages = fetched.sortedBy { it.createdAt }.map { toMessageItemDto(it, member) }, messages = fetched.sortedBy { it.createdAt }.map { toMessageItemDto(it, member) },
@@ -113,19 +115,25 @@ class UserCreatorChatService(
} }
@Transactional @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") if (textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request")
val senderParticipant = validateParticipant(roomId, memberId) val senderParticipant = validateParticipant(roomId, memberId)
val sender = senderParticipant.member val sender = senderParticipant.member
val context = resolveSendContext(sender, roomId) val context = resolveSendContext(sender, roomId)
val message = saveTextMessage(context, textMessage) val message = saveTextMessage(context, textMessage)
val senderMessage = toMessageItemDto(message, sender) val senderMessage = toMessageItemDto(message, sender)
val names = localizedNameService.findLocalizedNames(listOf(sender.id!!), lang)
val opponent = context.opponentParticipant.member val opponent = context.opponentParticipant.member
val deliveredRealtime = deliverRealtime(message, opponent) val deliveredRealtime = deliverRealtime(message, opponent)
if (!deliveredRealtime) { if (!deliveredRealtime) {
publishMessagePush(message, sender, opponent) publishMessagePush(message, sender, opponent)
} }
return senderMessage return senderMessage.copy(senderNickname = names[sender.id] ?: senderMessage.senderNickname)
} }
@Transactional @Transactional
@@ -185,7 +193,9 @@ class UserCreatorChatService(
opponentParticipant: UserCreatorChatParticipant opponentParticipant: UserCreatorChatParticipant
): SendUserCreatorChatMessageResponse { ): SendUserCreatorChatMessageResponse {
val opponent = opponentParticipant.member 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) val deliveredRealtime = deliverRealtime(message, opponent)
if (deliveredRealtime) { if (deliveredRealtime) {
return SendUserCreatorChatMessageResponse(message = item, deliveredRealtime = true, pushSent = false) return SendUserCreatorChatMessageResponse(message = item, deliveredRealtime = true, pushSent = false)
@@ -239,6 +249,7 @@ class UserCreatorChatService(
messageKey = messageKey, messageKey = messageKey,
senderMemberId = sender.id, senderMemberId = sender.id,
args = listOf(sender.nickname), args = listOf(sender.nickname),
nameArgumentMemberIds = mapOf(0 to sender.id!!),
recipients = listOf(opponent.id!!), recipients = listOf(opponent.id!!),
deepLinkValue = FcmDeepLinkValue.CHAT, deepLinkValue = FcmDeepLinkValue.CHAT,
deepLinkId = message.chatRoom.id 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( private data class SendContext(
val room: UserCreatorChatRoom, val room: UserCreatorChatRoom,
val senderParticipant: UserCreatorChatParticipant, val senderParticipant: UserCreatorChatParticipant,
@@ -1,6 +1,9 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.ObjectMapper 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.Message
import org.springframework.data.redis.connection.MessageListener import org.springframework.data.redis.connection.MessageListener
import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.data.redis.core.StringRedisTemplate
@@ -16,7 +19,8 @@ class UserCreatorChatRoomMessageBroker(
private val stringRedisTemplate: StringRedisTemplate, private val stringRedisTemplate: StringRedisTemplate,
private val sessionRegistry: UserCreatorChatWebSocketSessionRegistry, private val sessionRegistry: UserCreatorChatWebSocketSessionRegistry,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
listenerContainer: RedisMessageListenerContainer listenerContainer: RedisMessageListenerContainer,
private val localizedNameService: CreatorLocalizedNameService
) : MessageListener { ) : MessageListener {
init { init {
listenerContainer.addMessageListener(this, ChannelTopic(ROOM_CHANNEL)) listenerContainer.addMessageListener(this, ChannelTopic(ROOM_CHANNEL))
@@ -36,9 +40,25 @@ class UserCreatorChatRoomMessageBroker(
String(message.body, StandardCharsets.UTF_8), String(message.body, StandardCharsets.UTF_8),
UserCreatorChatRoomPublishedMessage::class.java 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 } .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<ObjectNode>()
(localized["payload"] as ObjectNode).put("senderNickname", nickname)
objectMapper.writeValueAsString(localized)
} else {
published.payload
}
sendMessage(session, payload)
}
}
} }
private fun sendMessage(session: WebSocketSession, payload: String) { private fun sendMessage(session: WebSocketSession, payload: String) {
@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket 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.JwtFilter
import kr.co.vividnext.sodalive.jwt.TokenProvider import kr.co.vividnext.sodalive.jwt.TokenProvider
import kr.co.vividnext.sodalive.member.MemberAdapter import kr.co.vividnext.sodalive.member.MemberAdapter
import org.springframework.http.HttpHeaders
import org.springframework.http.server.ServerHttpRequest import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse import org.springframework.http.server.ServerHttpResponse
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -35,6 +37,7 @@ class UserCreatorChatWebSocketAuthInterceptor(
attributes[MEMBER_ID_ATTRIBUTE] = memberId attributes[MEMBER_ID_ATTRIBUTE] = memberId
attributes[AUTHENTICATION_ATTRIBUTE] = authentication attributes[AUTHENTICATION_ATTRIBUTE] = authentication
attributes[LANG_ATTRIBUTE] = Lang.fromAcceptLanguage(request.headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE))
return true return true
} }
@@ -58,6 +61,7 @@ class UserCreatorChatWebSocketAuthInterceptor(
companion object { companion object {
const val MEMBER_ID_ATTRIBUTE = "memberId" const val MEMBER_ID_ATTRIBUTE = "memberId"
const val AUTHENTICATION_ATTRIBUTE = "authentication" const val AUTHENTICATION_ATTRIBUTE = "authentication"
const val LANG_ATTRIBUTE = "lang"
private const val BEARER_PREFIX = "Bearer " private const val BEARER_PREFIX = "Bearer "
} }
} }
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.socket.CloseStatus import org.springframework.web.socket.CloseStatus
@@ -60,7 +61,8 @@ class UserCreatorChatWebSocketHandler(
val message = service.sendTextMessageByWebSocket( val message = service.sendTextMessageByWebSocket(
memberId = memberId(session), memberId = memberId(session),
roomId = request.roomId, 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) sendEnvelope(session, UserCreatorChatWebSocketMessageType.SEND_ACK, request.requestId, request.roomId, message)
} catch (e: SodaException) { } catch (e: SodaException) {
@@ -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.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig 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.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler 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.api.content.all.dto.MainContentAllTabResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType 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.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
@@ -41,6 +43,11 @@ class MainContentAllControllerTest @Autowired constructor(
@MockBean @MockBean
private lateinit var langContext: LangContext private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean @MockBean
private lateinit var sodaMessageSource: SodaMessageSource private lateinit var sodaMessageSource: SodaMessageSource
@@ -57,7 +64,7 @@ class MainContentAllControllerTest @Autowired constructor(
@DisplayName("전체 탭 조회는 비회원에게 200 OK와 기본 응답을 반환한다") @DisplayName("전체 탭 조회는 비회원에게 200 OK와 기본 응답을 반환한다")
fun shouldAllowAnonymousAndUseDefaultType() { fun shouldAllowAnonymousAndUseDefaultType() {
Mockito.doReturn(response(MainContentAllType.AUDIO, ContentSort.LATEST)).`when`(facade) 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")) mockMvc.perform(get("/api/v2/audio/contents"))
.andExpect(status().isOk) .andExpect(status().isOk)
@@ -76,7 +83,7 @@ class MainContentAllControllerTest @Autowired constructor(
role = MemberRole.USER role = MemberRole.USER
).apply { id = 10L } ).apply { id = 10L }
Mockito.doReturn(response(MainContentAllType.SERIES, ContentSort.POPULAR)).`when`(facade) 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( mockMvc.perform(
get("/api/v2/audio/contents") get("/api/v2/audio/contents")
@@ -92,7 +99,7 @@ class MainContentAllControllerTest @Autowired constructor(
.andExpect(jsonPath("$.data.type").value("SERIES")) .andExpect(jsonPath("$.data.type").value("SERIES"))
.andExpect(jsonPath("$.data.sort").value("POPULAR")) .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 { private fun response(type: MainContentAllType, sort: ContentSort): MainContentAllTabResponse {
@@ -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.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.hamcrest.Matchers.contains
import org.hamcrest.Matchers.nullValue 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.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.annotation.DirtiesContext import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.ContextConfiguration
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
@@ -30,6 +34,7 @@ import javax.persistence.EntityManager
properties = [ properties = [
"cloud.aws.cloud-front.host=https://cdn.test", "cloud.aws.cloud-front.host=https://cdn.test",
"spring.cache.type=none", "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" "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( class MainContentAllEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc, private val mockMvc: MockMvc,
private val entityManager: EntityManager, 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 @Test
@DisplayName("전체 탭 AUDIO API는 controller-service-repository를 거쳐 오디오 응답과 빈 series를 반환한다") @DisplayName("전체 탭 AUDIO API는 controller-service-repository를 거쳐 오디오 응답과 빈 series를 반환한다")
fun shouldReturnAudioContentsThroughControllerServiceAndRepository() { fun shouldReturnAudioContentsThroughControllerServiceAndRepository() {
@@ -196,7 +300,7 @@ class MainContentAllEndToEndTest @Autowired constructor(
entityManager.flush() entityManager.flush()
entityManager.clear() 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( private data class AudioFixture(
val audioContentId: Long val audioContentId: Long,
val creatorId: Long
) )
private data class SeriesFixture( private data class SeriesFixture(
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.all.application 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.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
@@ -36,7 +37,7 @@ class MainContentAllFacadeTest {
page = MainContentPage(1, 30), page = MainContentPage(1, 30),
hasNext = false 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( val response = facade.getContents(
type = "FREE", type = "FREE",
@@ -44,10 +45,11 @@ class MainContentAllFacadeTest {
dayOfWeek = "MON", dayOfWeek = "MON",
page = 1, page = 1,
size = 30, 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(MainContentAllType.FREE, response.type)
assertEquals(ContentSort.PRICE_LOW, response.sort) assertEquals(ContentSort.PRICE_LOW, response.sort)
} }
@@ -27,6 +27,7 @@ class MainContentAllTabResponseTest {
audios = emptyList(), audios = emptyList(),
series = listOf( series = listOf(
MainContentAllSeries( MainContentAllSeries(
creatorMemberId = 100L,
seriesId = 10L, seriesId = 10L,
title = "시리즈", title = "시리즈",
coverImageUrl = "https://cdn/series.jpg", coverImageUrl = "https://cdn/series.jpg",
@@ -58,6 +59,7 @@ class MainContentAllTabResponseTest {
totalCount = 1, totalCount = 1,
audios = listOf( audios = listOf(
MainContentAllAudio( MainContentAllAudio(
creatorMemberId = 100L,
audioContentId = 1L, audioContentId = 1L,
title = "audio", title = "audio",
imageUrl = "https://cdn/audio.jpg", imageUrl = "https://cdn/audio.jpg",
@@ -69,6 +71,7 @@ class MainContentAllTabResponseTest {
creatorNickname = "creator" creatorNickname = "creator"
), ),
MainContentAllAudio( MainContentAllAudio(
creatorMemberId = 100L,
audioContentId = 2L, audioContentId = 2L,
title = "free audio", title = "free audio",
imageUrl = "https://cdn/free-audio.jpg", imageUrl = "https://cdn/free-audio.jpg",
@@ -80,6 +83,7 @@ class MainContentAllTabResponseTest {
creatorNickname = "creator" creatorNickname = "creator"
), ),
MainContentAllAudio( MainContentAllAudio(
creatorMemberId = 100L,
audioContentId = 3L, audioContentId = 3L,
title = "point unavailable audio", title = "point unavailable audio",
imageUrl = "https://cdn/point-unavailable-audio.jpg", imageUrl = "https://cdn/point-unavailable-audio.jpg",
@@ -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.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig 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.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler 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.application.ContentOverviewFacade
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponse import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponse
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewType 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.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
@@ -41,6 +43,11 @@ class ContentOverviewControllerTest @Autowired constructor(
@MockBean @MockBean
private lateinit var langContext: LangContext private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean @MockBean
private lateinit var sodaMessageSource: SodaMessageSource private lateinit var sodaMessageSource: SodaMessageSource
@@ -62,7 +69,7 @@ class ContentOverviewControllerTest @Autowired constructor(
fun shouldPassAuthenticatedMemberAndQueryParameters() { fun shouldPassAuthenticatedMemberAndQueryParameters() {
val member = member(id = 10L) val member = member(id = 10L)
Mockito.doReturn(emptyResponse(ContentOverviewType.FIRST_AUDIO_CONTENT)).`when`(facade) 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( mockMvc.perform(
get("/api/v2/contents") get("/api/v2/contents")
@@ -75,7 +82,8 @@ class ContentOverviewControllerTest @Autowired constructor(
.andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.type").value("FIRST_AUDIO_CONTENT")) .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 <T> eqValue(value: T): T { private fun <T> eqValue(value: T): T {
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.overview.application 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.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
@@ -31,9 +32,9 @@ class ContentOverviewFacadeTest {
fun shouldReturnNewAndHotPage() { fun shouldReturnNewAndHotPage() {
val member = member(id = 10L) val member = member(id = 10L)
Mockito.doReturn((1L..21L).map { audioCard(it) }).`when`(audioRecommendationQueryService) 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(ContentOverviewType.NEW_AND_HOT_AUDIO, response.type)
assertEquals((1L..20L).toList(), response.items.map { it.contentId }) assertEquals((1L..20L).toList(), response.items.map { it.contentId })
@@ -54,10 +55,11 @@ class ContentOverviewFacadeTest {
eqValue(20L), eqValue(20L),
eqValue(21), eqValue(21),
eqValue(member.id), 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(ContentOverviewType.FIRST_AUDIO_CONTENT, response.type)
assertEquals(listOf(1L, 2L), response.items.map { it.contentId }) assertEquals(listOf(1L, 2L), response.items.map { it.contentId })
@@ -79,6 +81,7 @@ class ContentOverviewFacadeTest {
private fun audioCard(id: Long): AudioCard { private fun audioCard(id: Long): AudioCard {
return AudioCard( return AudioCard(
creatorMemberId = id + 100L,
audioContentId = id, audioContentId = id,
title = "audio$id", title = "audio$id",
duration = "00:01", duration = "00:01",
@@ -120,6 +120,7 @@ class ContentOverviewPageResponseTest {
private fun audioCard(price: Int, isPointAvailable: Boolean): AudioCard { private fun audioCard(price: Int, isPointAvailable: Boolean): AudioCard {
return AudioCard( return AudioCard(
creatorMemberId = 10L,
audioContentId = price.toLong() + 1, audioContentId = price.toLong() + 1,
title = "audio", title = "audio",
duration = "00:01", duration = "00:01",
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.ranking.application 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.member.Member
import kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryService import kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryService
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRanking import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRanking
@@ -23,6 +24,7 @@ class AudioRankingFacadeTest {
type = AudioRankingType.RISING, type = AudioRankingType.RISING,
items = listOf( items = listOf(
AudioRankingItem( AudioRankingItem(
creatorMemberId = 10L,
contentId = 1L, contentId = 1L,
title = "audio", title = "audio",
creatorNickname = "creator", 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(true, response.showRankChange)
assertEquals(AudioRankingType.RISING, response.type) assertEquals(AudioRankingType.RISING, response.type)
assertEquals(1, response.items.size) assertEquals(1, response.items.size)
@@ -19,6 +19,7 @@ class AudioRankingResponseTest {
type = AudioRankingType.RISING, type = AudioRankingType.RISING,
items = listOf( items = listOf(
AudioRankingItem( AudioRankingItem(
creatorMemberId = 10L,
contentId = 1001L, contentId = 1001L,
title = "rising audio", title = "rising audio",
creatorNickname = "creator", creatorNickname = "creator",
@@ -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.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig 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.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler 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.member.MemberRole
import kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacade import kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacade
import kr.co.vividnext.sodalive.v2.api.content.recommendation.dto.AudioRecommendationsResponse 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.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
@@ -39,6 +41,11 @@ class AudioRecommendationControllerTest @Autowired constructor(
@MockBean @MockBean
private lateinit var langContext: LangContext private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean @MockBean
private lateinit var sodaMessageSource: SodaMessageSource private lateinit var sodaMessageSource: SodaMessageSource
@@ -54,7 +61,7 @@ class AudioRecommendationControllerTest @Autowired constructor(
@Test @Test
@DisplayName("오디오 추천 조회는 비회원에게 200 OK와 ApiResponse.ok wrapper를 반환한다") @DisplayName("오디오 추천 조회는 비회원에게 200 OK와 ApiResponse.ok wrapper를 반환한다")
fun shouldReturnRecommendationsForAnonymous() { 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")) mockMvc.perform(get("/api/v2/audio/recommendations"))
.andExpect(status().isOk) .andExpect(status().isOk)
@@ -74,13 +81,13 @@ class AudioRecommendationControllerTest @Autowired constructor(
nickname = "viewer", nickname = "viewer",
role = MemberRole.USER role = MemberRole.USER
).apply { id = 10L } ).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)))) mockMvc.perform(get("/api/v2/audio/recommendations").with(user(MemberAdapter(member))))
.andExpect(status().isOk) .andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.success").value(true))
Mockito.verify(facade).getRecommendations(eqValue(member)) Mockito.verify(facade).getRecommendations(eqValue(member), eqValue(Lang.KO))
} }
private fun emptyResponse(): AudioRecommendationsResponse { private fun emptyResponse(): AudioRecommendationsResponse {
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.content.recommendation.application package kr.co.vividnext.sodalive.v2.api.content.recommendation.application
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 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.common.domain.RecommendationBanner
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
@@ -20,9 +21,9 @@ class AudioRecommendationFacadeTest {
@Test @Test
@DisplayName("facade는 도메인 추천 결과를 모든 공개 응답 필드로 변환한다") @DisplayName("facade는 도메인 추천 결과를 모든 공개 응답 필드로 변환한다")
fun shouldConvertDomainRecommendationsToResponse() { 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.banners.size)
assertEquals(1, response.originalSeries.size) assertEquals(1, response.originalSeries.size)
@@ -49,6 +50,7 @@ class AudioRecommendationFacadeTest {
private fun domain(): AudioRecommendations { private fun domain(): AudioRecommendations {
val card = AudioCard( val card = AudioCard(
creatorMemberId = 10L,
audioContentId = 1L, audioContentId = 1L,
title = "audio", title = "audio",
duration = "00:01", duration = "00:01",
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.`in`.web 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.common.CountryContext
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -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.audio.dto.CreatorChannelAudioThemeResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto.CreatorChannelAudioContentResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort 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.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
@@ -47,6 +49,11 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
@MockBean @MockBean
private lateinit var langContext: LangContext private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean @MockBean
private lateinit var sodaMessageSource: SodaMessageSource private lateinit var sodaMessageSource: SodaMessageSource
@@ -88,7 +95,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
eqValue(null), eqValue(null),
eqValue(null), eqValue(null),
eqValue(null), eqValue(null),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -118,7 +126,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
eqValue(null), eqValue(null),
eqValue(null), 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(999L),
eqValue(-1), eqValue(-1),
eqValue(100), eqValue(100),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -158,7 +168,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
eqValue(999L), eqValue(999L),
eqValue(-1), eqValue(-1),
eqValue(100), eqValue(100),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
} }
@@ -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.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule 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.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
@@ -33,7 +34,8 @@ class CreatorChannelAudioFacadeTest {
themeId = 10L, themeId = 10L,
page = 0, page = 0,
size = 20, size = 20,
now = now now = now,
lang = Lang.KO
) )
val response = facade.getAudioTab( val response = facade.getAudioTab(
@@ -43,7 +45,8 @@ class CreatorChannelAudioFacadeTest {
themeId = 10L, themeId = 10L,
page = 0, page = 0,
size = 20, size = 20,
now = now now = now,
lang = Lang.KO
) )
assertEquals(3, response.audioContentCount) assertEquals(3, response.audioContentCount)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.`in`.web 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.common.CountryContext
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@@ -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.CreatorChannelCommunityRepliesResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityReplyResponse 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 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.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
@@ -50,6 +52,11 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
@MockBean @MockBean
private lateinit var langContext: LangContext private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean @MockBean
private lateinit var sodaMessageSource: SodaMessageSource private lateinit var sodaMessageSource: SodaMessageSource
@@ -89,7 +96,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(1), eqValue(1),
eqValue(20), eqValue(20),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -117,7 +125,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(1), eqValue(1),
eqValue(20), eqValue(20),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
} }
@@ -130,7 +139,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(-1), eqValue(-1),
eqValue(100), eqValue(100),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -148,7 +158,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(-1), eqValue(-1),
eqValue(100), 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( Mockito.doReturn(createDetailResponse()).`when`(facade).getCommunityPostDetail(
eqValue(101L), eqValue(101L),
eqValue(viewer), eqValue(viewer),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -195,7 +207,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(0), eqValue(0),
eqValue(20), eqValue(20),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -222,7 +235,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(-1), eqValue(-1),
eqValue(100), eqValue(100),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -240,7 +254,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(-1), eqValue(-1),
eqValue(100), eqValue(100),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
} }
@@ -253,7 +268,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(0), eqValue(0),
eqValue(20), eqValue(20),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -278,7 +294,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(-1), eqValue(-1),
eqValue(100), eqValue(100),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
mockMvc.perform( mockMvc.perform(
@@ -296,7 +313,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer), eqValue(viewer),
eqValue(-1), eqValue(-1),
eqValue(100), eqValue(100),
anyValue(LocalDateTime.now()) anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
) )
} }
@@ -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.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean 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.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.ContextConfiguration
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
@@ -44,7 +45,8 @@ import javax.persistence.EntityManager
class CreatorChannelCommunityEndToEndTest @Autowired constructor( class CreatorChannelCommunityEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc, private val mockMvc: MockMvc,
private val entityManager: EntityManager, private val entityManager: EntityManager,
private val transactionTemplate: TransactionTemplate private val transactionTemplate: TransactionTemplate,
private val jdbcTemplate: JdbcTemplate
) { ) {
@MockBean @MockBean
private lateinit var audioContentCloudFront: AudioContentCloudFront private lateinit var audioContentCloudFront: AudioContentCloudFront
@@ -52,6 +54,67 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor(
@MockBean @MockBean
private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService 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 @Test
@DisplayName("커뮤니티 탭 API는 E2E로 정렬, fallback, 성인 필터, 유료 미디어 접근 정책을 반환한다") @DisplayName("커뮤니티 탭 API는 E2E로 정렬, fallback, 성인 필터, 유료 미디어 접근 정책을 반환한다")
fun shouldReturnCommunityTabThroughControllerServiceAndRepository() { fun shouldReturnCommunityTabThroughControllerServiceAndRepository() {
@@ -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.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule 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.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityTabResponse import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityTabResponse
@@ -71,7 +72,8 @@ class CreatorChannelCommunityFacadeTest {
viewer = viewer, viewer = viewer,
page = -1, page = -1,
size = 100, size = 100,
now = now now = now,
lang = Lang.KO
) )
val response = facade.getCommunityTab( val response = facade.getCommunityTab(
@@ -79,7 +81,8 @@ class CreatorChannelCommunityFacadeTest {
viewer = viewer, viewer = viewer,
page = -1, page = -1,
size = 100, size = 100,
now = now now = now,
lang = Lang.KO
) )
assertEquals(2, response.communityPostCount) assertEquals(2, response.communityPostCount)
@@ -99,9 +102,9 @@ class CreatorChannelCommunityFacadeTest {
val facade = CreatorChannelCommunityFacade(service) val facade = CreatorChannelCommunityFacade(service)
val viewer = createMember(id = 10L) val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 7, 6, 12, 0) 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) assertEquals(101L, response.postId)
assertTrue(response.isLiked) assertTrue(response.isLiked)
@@ -119,9 +122,9 @@ class CreatorChannelCommunityFacadeTest {
val facade = CreatorChannelCommunityFacade(service) val facade = CreatorChannelCommunityFacade(service)
val viewer = createMember(id = 10L) val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 7, 6, 12, 0) 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(1, response.commentCount)
assertEquals(0, response.page) assertEquals(0, response.page)
@@ -139,9 +142,9 @@ class CreatorChannelCommunityFacadeTest {
val facade = CreatorChannelCommunityFacade(service) val facade = CreatorChannelCommunityFacade(service)
val viewer = createMember(id = 10L) val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 7, 6, 12, 0) 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(1, response.replyCount)
assertEquals(201L, response.replies.first().commentId) assertEquals(201L, response.replies.first().commentId)

Some files were not shown because too many files have changed in this diff Show More