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

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