test #388
@@ -5,7 +5,6 @@ import kr.co.vividnext.sodalive.chat.character.dto.Character
|
|||||||
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
||||||
import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem
|
import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem
|
||||||
import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse
|
import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse
|
||||||
import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse
|
|
||||||
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
||||||
import kr.co.vividnext.sodalive.event.GetEventResponse
|
import kr.co.vividnext.sodalive.event.GetEventResponse
|
||||||
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse
|
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse
|
||||||
@@ -27,6 +26,5 @@ data class GetHomeResponse(
|
|||||||
val recommendChannelList: List<RecommendChannelResponse>,
|
val recommendChannelList: List<RecommendChannelResponse>,
|
||||||
val freeContentList: List<AudioContentMainItem>,
|
val freeContentList: List<AudioContentMainItem>,
|
||||||
val pointAvailableContentList: List<AudioContentMainItem>,
|
val pointAvailableContentList: List<AudioContentMainItem>,
|
||||||
val recommendContentList: List<AudioContentMainItem>,
|
val recommendContentList: List<AudioContentMainItem>
|
||||||
val curationList: List<GetContentCurationResponse>
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
package kr.co.vividnext.sodalive.api.home
|
package kr.co.vividnext.sodalive.api.home
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.audition.AuditionService
|
import kr.co.vividnext.sodalive.audition.AuditionService
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentService
|
import kr.co.vividnext.sodalive.content.AudioContentService
|
||||||
import kr.co.vividnext.sodalive.content.ContentType
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
|
import kr.co.vividnext.sodalive.content.SortType
|
||||||
import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem
|
import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem
|
||||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService
|
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService
|
||||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationService
|
|
||||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
||||||
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
||||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
|
|
||||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService
|
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||||
import kr.co.vividnext.sodalive.event.GetEventResponse
|
import kr.co.vividnext.sodalive.event.GetEventResponse
|
||||||
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||||
@@ -22,7 +18,6 @@ import kr.co.vividnext.sodalive.i18n.LangContext
|
|||||||
import kr.co.vividnext.sodalive.live.room.LiveRoomService
|
import kr.co.vividnext.sodalive.live.room.LiveRoomService
|
||||||
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
|
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberService
|
|
||||||
import kr.co.vividnext.sodalive.query.recommend.RecommendChannelQueryService
|
import kr.co.vividnext.sodalive.query.recommend.RecommendChannelQueryService
|
||||||
import kr.co.vividnext.sodalive.rank.ContentRankingSortType
|
import kr.co.vividnext.sodalive.rank.ContentRankingSortType
|
||||||
import kr.co.vividnext.sodalive.rank.RankingRepository
|
import kr.co.vividnext.sodalive.rank.RankingRepository
|
||||||
@@ -34,16 +29,15 @@ import java.time.DayOfWeek
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.temporal.TemporalAdjusters
|
import java.time.temporal.TemporalAdjusters
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class HomeService(
|
class HomeService(
|
||||||
private val memberService: MemberService,
|
|
||||||
private val liveRoomService: LiveRoomService,
|
private val liveRoomService: LiveRoomService,
|
||||||
private val auditionService: AuditionService,
|
private val auditionService: AuditionService,
|
||||||
private val seriesService: ContentSeriesService,
|
private val seriesService: ContentSeriesService,
|
||||||
private val contentService: AudioContentService,
|
private val contentService: AudioContentService,
|
||||||
private val bannerService: AudioContentBannerService,
|
private val bannerService: AudioContentBannerService,
|
||||||
private val curationService: AudioContentCurationService,
|
|
||||||
private val contentThemeService: AudioContentThemeService,
|
private val contentThemeService: AudioContentThemeService,
|
||||||
private val recommendChannelService: RecommendChannelQueryService,
|
private val recommendChannelService: RecommendChannelQueryService,
|
||||||
|
|
||||||
@@ -52,10 +46,6 @@ class HomeService(
|
|||||||
private val rankingRepository: RankingRepository,
|
private val rankingRepository: RankingRepository,
|
||||||
private val explorerQueryRepository: ExplorerQueryRepository,
|
private val explorerQueryRepository: ExplorerQueryRepository,
|
||||||
|
|
||||||
private val contentTranslationRepository: ContentTranslationRepository,
|
|
||||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
|
||||||
private val seriesTranslationRepository: SeriesTranslationRepository,
|
|
||||||
|
|
||||||
private val langContext: LangContext,
|
private val langContext: LangContext,
|
||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
@@ -64,8 +54,19 @@ class HomeService(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val RECOMMEND_TARGET_SIZE = 30
|
private const val RECOMMEND_TARGET_SIZE = 30
|
||||||
private const val RECOMMEND_MAX_ATTEMPTS = 3
|
private const val RECOMMEND_MAX_ATTEMPTS = 3
|
||||||
|
private const val TIME_DECAY_HALF_LIFE_RANK = 30.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class RecommendBucket(
|
||||||
|
val offset: Long,
|
||||||
|
val limit: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class DecayCandidate(
|
||||||
|
val item: AudioContentMainItem,
|
||||||
|
val rank: Int
|
||||||
|
)
|
||||||
|
|
||||||
fun fetchData(
|
fun fetchData(
|
||||||
timezone: String,
|
timezone: String,
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
@@ -84,24 +85,19 @@ class HomeService(
|
|||||||
timezone = timezone
|
timezone = timezone
|
||||||
)
|
)
|
||||||
|
|
||||||
val creatorRanking = rankingRepository
|
val creatorRankingMembers = rankingRepository.getCreatorRankings(memberId = memberId)
|
||||||
.getCreatorRankings()
|
val creatorRankingIds = creatorRankingMembers.mapNotNull { it.id }
|
||||||
.filter {
|
val followedCreatorIds = if (memberId != null) {
|
||||||
if (memberId != null) {
|
explorerQueryRepository.getFollowedCreatorIds(creatorRankingIds, memberId)
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.id!!)
|
|
||||||
} else {
|
} else {
|
||||||
true
|
emptySet()
|
||||||
}
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
val followerCount = explorerQueryRepository.getNotificationUserIds(it.id!!).size
|
|
||||||
val follow = if (memberId != null) {
|
|
||||||
explorerQueryRepository.isFollow(it.id!!, memberId = memberId)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it.toExplorerSectionCreator(imageHost, follow, followerCount = followerCount)
|
val creatorRanking = creatorRankingMembers.map { creator ->
|
||||||
|
val creatorId = creator.id!!
|
||||||
|
val follow = memberId != null && followedCreatorIds.contains(creatorId)
|
||||||
|
|
||||||
|
creator.toExplorerSectionCreator(imageHost, follow)
|
||||||
}
|
}
|
||||||
|
|
||||||
val latestContentThemeList = contentThemeService.getActiveThemeOfContent(
|
val latestContentThemeList = contentThemeService.getActiveThemeOfContent(
|
||||||
@@ -111,19 +107,12 @@ class HomeService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val latestContentList = contentService.getLatestContentByTheme(
|
val latestContentList = contentService.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
theme = latestContentThemeList,
|
theme = latestContentThemeList,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
isFree = false,
|
isFree = false,
|
||||||
isAdult = isAdult
|
isAdult = isAdult
|
||||||
).filter {
|
)
|
||||||
if (memberId != null) {
|
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val translatedLatestContentList = getTranslatedContentList(contentList = latestContentList)
|
|
||||||
|
|
||||||
val eventBannerList = GetEventResponse(
|
val eventBannerList = GetEventResponse(
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
@@ -136,28 +125,23 @@ class HomeService(
|
|||||||
isAdult = isAdult
|
isAdult = isAdult
|
||||||
)
|
)
|
||||||
|
|
||||||
// 오직 보이스온에서만
|
|
||||||
val originalAudioDramaList = seriesService.getOriginalAudioDramaList(
|
val originalAudioDramaList = seriesService.getOriginalAudioDramaList(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType,
|
contentType = contentType
|
||||||
orderByRandom = true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val translatedOriginalAudioDramaList = getTranslatedSeriesList(seriesList = originalAudioDramaList)
|
|
||||||
|
|
||||||
val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult)
|
val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult)
|
||||||
|
|
||||||
// 요일별 시리즈
|
// 요일별 시리즈
|
||||||
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
|
val translatedDayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
dayOfWeek = getDayOfWeekByTimezone(timezone)
|
dayOfWeek = getDayOfWeekByTimezone(timezone)
|
||||||
)
|
)
|
||||||
val translatedDayOfWeekSeriesList = getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
|
|
||||||
|
|
||||||
// 인기 캐릭터 조회
|
// 인기 캐릭터 조회
|
||||||
val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
|
val translatedPopularCharacters = characterService.getPopularCharacters(locale = langContext.lang.code)
|
||||||
|
|
||||||
val currentDateTime = LocalDateTime.now()
|
val currentDateTime = LocalDateTime.now()
|
||||||
val startDate = currentDateTime
|
val startDate = currentDateTime
|
||||||
@@ -178,130 +162,61 @@ class HomeService(
|
|||||||
sort = ContentRankingSortType.REVENUE
|
sort = ContentRankingSortType.REVENUE
|
||||||
)
|
)
|
||||||
|
|
||||||
val contentRankingContentIds = contentRanking.map { it.contentId }
|
|
||||||
val translatedContentRanking = if (contentRankingContentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentRankingContentIds, locale = langContext.lang.code)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
contentRanking.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentRanking
|
|
||||||
}
|
|
||||||
|
|
||||||
val recommendChannelList = recommendChannelService.getRecommendChannel(
|
val recommendChannelList = recommendChannelService.getRecommendChannel(
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType
|
contentType = contentType
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
val freeContentList = getRandomizedContentList(
|
||||||
* recommendChannelList의 콘텐츠 번역 데이터 조회
|
memberId = memberId,
|
||||||
*
|
isAdult = isAdult,
|
||||||
* languageCode != null
|
contentType = contentType,
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 recommendChannelList의 콘텐츠 title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val channelContentIds = recommendChannelList
|
|
||||||
.flatMap { it.contentList }
|
|
||||||
.map { it.contentId }
|
|
||||||
.distinct()
|
|
||||||
|
|
||||||
val translatedRecommendChannelList = if (channelContentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = channelContentIds, locale = langContext.lang.code)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
recommendChannelList.map { channel ->
|
|
||||||
val translatedContentList = channel.contentList.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.copy(contentList = translatedContentList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recommendChannelList
|
|
||||||
}
|
|
||||||
|
|
||||||
val freeContentList = contentService.getLatestContentByTheme(
|
|
||||||
theme = contentThemeService.getActiveThemeOfContent(
|
theme = contentThemeService.getActiveThemeOfContent(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
isFree = true,
|
isFree = true,
|
||||||
contentType = contentType
|
contentType = contentType
|
||||||
),
|
),
|
||||||
contentType = contentType,
|
|
||||||
isFree = true,
|
isFree = true,
|
||||||
isAdult = isAdult,
|
isPointAvailableOnly = false
|
||||||
orderByRandom = true
|
)
|
||||||
).filter {
|
|
||||||
if (memberId != null) {
|
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val translatedFreeContentList = getTranslatedContentList(contentList = freeContentList)
|
|
||||||
|
|
||||||
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
|
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
|
||||||
val pointAvailableContentList = contentService.getLatestContentByTheme(
|
val pointAvailableContentList = getRandomizedContentList(
|
||||||
|
memberId = memberId,
|
||||||
|
isAdult = isAdult,
|
||||||
|
contentType = contentType,
|
||||||
theme = emptyList(),
|
theme = emptyList(),
|
||||||
contentType = contentType,
|
|
||||||
isFree = false,
|
isFree = false,
|
||||||
isAdult = isAdult,
|
|
||||||
orderByRandom = true,
|
|
||||||
isPointAvailableOnly = true
|
isPointAvailableOnly = true
|
||||||
).filter {
|
|
||||||
if (memberId != null) {
|
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val translatedPointAvailableContentList = getTranslatedContentList(contentList = pointAvailableContentList)
|
|
||||||
|
|
||||||
val curationList = curationService.getContentCurationList(
|
|
||||||
tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용
|
|
||||||
isAdult = isAdult,
|
|
||||||
contentType = contentType,
|
|
||||||
memberId = memberId
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val excludeContentIds = (
|
||||||
|
latestContentList.map { it.contentId } +
|
||||||
|
contentRanking.map { it.contentId }
|
||||||
|
).distinct()
|
||||||
|
|
||||||
return GetHomeResponse(
|
return GetHomeResponse(
|
||||||
liveList = liveList,
|
liveList = liveList,
|
||||||
creatorRanking = creatorRanking,
|
creatorRanking = creatorRanking,
|
||||||
latestContentThemeList = latestContentThemeList,
|
latestContentThemeList = latestContentThemeList,
|
||||||
latestContentList = translatedLatestContentList,
|
latestContentList = latestContentList,
|
||||||
bannerList = bannerList,
|
bannerList = bannerList,
|
||||||
eventBannerList = eventBannerList,
|
eventBannerList = eventBannerList,
|
||||||
originalAudioDramaList = translatedOriginalAudioDramaList,
|
originalAudioDramaList = originalAudioDramaList,
|
||||||
auditionList = auditionList,
|
auditionList = auditionList,
|
||||||
dayOfWeekSeriesList = translatedDayOfWeekSeriesList,
|
dayOfWeekSeriesList = translatedDayOfWeekSeriesList,
|
||||||
popularCharacters = translatedPopularCharacters,
|
popularCharacters = translatedPopularCharacters,
|
||||||
contentRanking = translatedContentRanking,
|
contentRanking = contentRanking,
|
||||||
recommendChannelList = translatedRecommendChannelList,
|
recommendChannelList = recommendChannelList,
|
||||||
freeContentList = translatedFreeContentList,
|
freeContentList = freeContentList,
|
||||||
pointAvailableContentList = translatedPointAvailableContentList,
|
pointAvailableContentList = pointAvailableContentList,
|
||||||
recommendContentList = getRecommendContentList(
|
recommendContentList = getRecommendContentList(
|
||||||
isAdultContentVisible = isAdultContentVisible,
|
isAdultContentVisible = isAdultContentVisible,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
member = member
|
member = member,
|
||||||
),
|
excludeContentIds = excludeContentIds
|
||||||
curationList = curationList
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,20 +240,13 @@ class HomeService(
|
|||||||
listOf(theme)
|
listOf(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentList = contentService.getLatestContentByTheme(
|
return contentService.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
theme = themeList,
|
theme = themeList,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
isFree = false,
|
isFree = false,
|
||||||
isAdult = isAdult
|
isAdult = isAdult
|
||||||
).filter {
|
)
|
||||||
if (memberId != null) {
|
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getTranslatedContentList(contentList = contentList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDayOfWeekSeriesList(
|
fun getDayOfWeekSeriesList(
|
||||||
@@ -350,14 +258,12 @@ class HomeService(
|
|||||||
val memberId = member?.id
|
val memberId = member?.id
|
||||||
val isAdult = member?.auth != null && isAdultContentVisible
|
val isAdult = member?.auth != null && isAdultContentVisible
|
||||||
|
|
||||||
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
|
return seriesService.getDayOfWeekSeriesList(
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
dayOfWeek = dayOfWeek
|
dayOfWeek = dayOfWeek
|
||||||
)
|
)
|
||||||
|
|
||||||
return getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContentRankingBySort(
|
fun getContentRankingBySort(
|
||||||
@@ -412,153 +318,155 @@ class HomeService(
|
|||||||
return dayToSeriesPublishedDaysOfWeek[zonedDateTime.dayOfWeek] ?: SeriesPublishedDaysOfWeek.RANDOM
|
return dayToSeriesPublishedDaysOfWeek[zonedDateTime.dayOfWeek] ?: SeriesPublishedDaysOfWeek.RANDOM
|
||||||
}
|
}
|
||||||
|
|
||||||
// 추천 콘텐츠 조회 로직은 변경 가능성을 고려하여 별도 메서드로 추출한다.
|
|
||||||
fun getRecommendContentList(
|
fun getRecommendContentList(
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
member: Member?
|
member: Member?,
|
||||||
|
excludeContentIds: List<Long> = emptyList()
|
||||||
): List<AudioContentMainItem> {
|
): List<AudioContentMainItem> {
|
||||||
val memberId = member?.id
|
val memberId = member?.id
|
||||||
val isAdult = member?.auth != null && isAdultContentVisible
|
val isAdult = member?.auth != null && isAdultContentVisible
|
||||||
|
|
||||||
// Set + List 조합으로 중복 제거 및 순서 보존, 각 시도마다 limit=60으로 조회
|
// 3개의 버킷(최근/중간/과거)에서 후보군을 조회한 뒤, 시간감쇠 점수 기반으로 샘플링한다.
|
||||||
val seen = HashSet<Long>(RECOMMEND_TARGET_SIZE * 2)
|
val buckets = listOf(
|
||||||
val result = ArrayList<AudioContentMainItem>(RECOMMEND_TARGET_SIZE)
|
RecommendBucket(offset = 0L, limit = 50L),
|
||||||
var attempt = 0
|
RecommendBucket(offset = 50L, limit = 100L),
|
||||||
while (attempt < RECOMMEND_MAX_ATTEMPTS && result.size < RECOMMEND_TARGET_SIZE) {
|
RecommendBucket(offset = 150L, limit = 150L)
|
||||||
attempt += 1
|
)
|
||||||
|
|
||||||
|
val result = mutableListOf<AudioContentMainItem>()
|
||||||
|
val seenIds = excludeContentIds.toMutableSet()
|
||||||
|
|
||||||
|
repeat(RECOMMEND_MAX_ATTEMPTS) {
|
||||||
|
if (result.size >= RECOMMEND_TARGET_SIZE) return@repeat
|
||||||
|
|
||||||
|
val remaining = RECOMMEND_TARGET_SIZE - result.size
|
||||||
|
val targetPerBucket = maxOf(1, (remaining + buckets.size - 1) / buckets.size)
|
||||||
|
|
||||||
|
for (bucket in buckets) {
|
||||||
|
if (result.size >= RECOMMEND_TARGET_SIZE) break
|
||||||
|
|
||||||
val batch = contentService.getLatestContentByTheme(
|
val batch = contentService.getLatestContentByTheme(
|
||||||
theme = emptyList(), // 특정 테마에 종속되지 않도록 전체에서 랜덤 조회
|
memberId = memberId,
|
||||||
|
theme = emptyList(),
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
offset = 0,
|
offset = bucket.offset,
|
||||||
limit = (RECOMMEND_TARGET_SIZE * RECOMMEND_MAX_ATTEMPTS).toLong(), // 60개 조회
|
limit = bucket.limit,
|
||||||
|
sortType = SortType.NEWEST,
|
||||||
isFree = false,
|
isFree = false,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
orderByRandom = true
|
orderByRandom = false,
|
||||||
).filter {
|
excludeContentIds = seenIds.toList()
|
||||||
if (memberId != null) {
|
)
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (item in batch) {
|
val selected = pickByTimeDecay(
|
||||||
if (result.size >= RECOMMEND_TARGET_SIZE) break
|
batch = batch,
|
||||||
if (seen.add(item.contentId)) {
|
targetSize = minOf(targetPerBucket, RECOMMEND_TARGET_SIZE - result.size),
|
||||||
result.add(item)
|
seenIds = seenIds
|
||||||
|
)
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
result.addAll(selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTranslatedContentList(contentList = result)
|
return result.take(RECOMMEND_TARGET_SIZE).shuffled()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun pickByTimeDecay(
|
||||||
* 콘텐츠 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
batch: List<AudioContentMainItem>,
|
||||||
*
|
targetSize: Int,
|
||||||
* 처리 절차:
|
seenIds: MutableSet<Long>
|
||||||
* - 입력된 콘텐츠들의 contentId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
|
): List<AudioContentMainItem> {
|
||||||
* contentTranslationRepository에서 번역 데이터를 한 번에 조회한다.
|
if (targetSize <= 0 || batch.isEmpty()) return emptyList()
|
||||||
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
|
|
||||||
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
|
|
||||||
*
|
|
||||||
* 성능:
|
|
||||||
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
|
|
||||||
*
|
|
||||||
* @param contentList 번역 대상 AudioContentMainItem 목록
|
|
||||||
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
|
|
||||||
*/
|
|
||||||
private fun getTranslatedContentList(contentList: List<AudioContentMainItem>): List<AudioContentMainItem> {
|
|
||||||
val contentIds = contentList.map { it.contentId }
|
|
||||||
|
|
||||||
return if (contentIds.isNotEmpty()) {
|
val candidates = batch
|
||||||
val translations = contentTranslationRepository
|
.asSequence()
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = langContext.lang.code)
|
.filterNot { seenIds.contains(it.contentId) }
|
||||||
.associateBy { it.contentId }
|
.mapIndexed { index, item -> DecayCandidate(item = item, rank = index) }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
contentList.map { item ->
|
if (candidates.isEmpty()) return emptyList()
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
val selected = mutableListOf<AudioContentMainItem>()
|
||||||
item
|
while (selected.size < targetSize && candidates.isNotEmpty()) {
|
||||||
} else {
|
val selectedIndex = selectByWeight(candidates)
|
||||||
item.copy(title = translatedTitle)
|
val chosen = candidates.removeAt(selectedIndex).item
|
||||||
}
|
if (seenIds.add(chosen.contentId)) {
|
||||||
}
|
selected.add(chosen)
|
||||||
} else {
|
|
||||||
contentList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return selected
|
||||||
* 시리즈 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
|
||||||
*
|
|
||||||
* 처리 절차:
|
|
||||||
* - 입력된 시리즈들의 seriesId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
|
|
||||||
* seriesTranslationRepository에서 번역 데이터를 한 번에 조회한다.
|
|
||||||
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
|
|
||||||
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
|
|
||||||
*
|
|
||||||
* 성능:
|
|
||||||
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
|
|
||||||
*
|
|
||||||
* @param seriesList 번역 대상 SeriesListItem 목록
|
|
||||||
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
|
|
||||||
*/
|
|
||||||
private fun getTranslatedSeriesList(
|
|
||||||
seriesList: List<GetSeriesListResponse.SeriesListItem>
|
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
|
||||||
val seriesIds = seriesList.map { it.seriesId }
|
|
||||||
|
|
||||||
return if (seriesIds.isNotEmpty()) {
|
|
||||||
val translations = seriesTranslationRepository
|
|
||||||
.findBySeriesIdInAndLocale(seriesIds = seriesIds, locale = langContext.lang.code)
|
|
||||||
.associateBy { it.seriesId }
|
|
||||||
|
|
||||||
seriesList.map { item ->
|
|
||||||
val translatedTitle = translations[item.seriesId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
private fun selectByWeight(candidates: List<DecayCandidate>): Int {
|
||||||
seriesList
|
val weights = candidates.map { 0.5.pow(it.rank / TIME_DECAY_HALF_LIFE_RANK) }
|
||||||
|
val totalWeight = weights.sum()
|
||||||
|
|
||||||
|
var randomPoint = Math.random() * totalWeight
|
||||||
|
for (index in weights.indices) {
|
||||||
|
randomPoint -= weights[index]
|
||||||
|
if (randomPoint <= 0) {
|
||||||
|
return index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return candidates.lastIndex
|
||||||
* AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
}
|
||||||
*
|
|
||||||
* 처리 절차:
|
|
||||||
* - characterId 목록을 추출하고, 요청 언어 코드로 aiCharacterTranslationRepository에서
|
|
||||||
* 번역 데이터를 한 번에 조회한다.
|
|
||||||
* - 각 캐릭터에 대해 name과 description 모두 번역 값이 존재하고 비어있지 않을 때에만
|
|
||||||
* 해당 필드를 교체한다. 둘 중 하나라도 없으면 원본 캐릭터를 그대로 유지한다.
|
|
||||||
*
|
|
||||||
* @param aiCharacterList 번역 대상 캐릭터 목록
|
|
||||||
* @return 가능한 경우 name/description이 번역된 캐릭터 목록, 그 외는 원본 유지
|
|
||||||
*/
|
|
||||||
private fun getTranslatedAiCharacterList(aiCharacterList: List<Character>): List<Character> {
|
|
||||||
val characterIds = aiCharacterList.map { it.characterId }
|
|
||||||
|
|
||||||
return if (characterIds.isNotEmpty()) {
|
private fun getRandomizedContentList(
|
||||||
val translations = aiCharacterTranslationRepository
|
memberId: Long?,
|
||||||
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = langContext.lang.code)
|
isAdult: Boolean,
|
||||||
.associateBy { it.characterId }
|
contentType: ContentType,
|
||||||
|
theme: List<String>,
|
||||||
|
isFree: Boolean,
|
||||||
|
isPointAvailableOnly: Boolean,
|
||||||
|
targetSize: Int = 20
|
||||||
|
): List<AudioContentMainItem> {
|
||||||
|
val buckets = listOf(
|
||||||
|
RecommendBucket(offset = 0L, limit = 50L),
|
||||||
|
RecommendBucket(offset = 50L, limit = 100L),
|
||||||
|
RecommendBucket(offset = 150L, limit = 150L)
|
||||||
|
)
|
||||||
|
|
||||||
aiCharacterList.map { character ->
|
val result = mutableListOf<AudioContentMainItem>()
|
||||||
val translatedName = translations[character.characterId]?.renderedPayload?.name
|
val seenIds = mutableSetOf<Long>()
|
||||||
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
|
|
||||||
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
|
repeat(RECOMMEND_MAX_ATTEMPTS) {
|
||||||
character
|
if (result.size >= targetSize) return@repeat
|
||||||
} else {
|
|
||||||
character.copy(name = translatedName, description = translatedDesc)
|
val remaining = targetSize - result.size
|
||||||
}
|
val targetPerBucket = maxOf(1, (remaining + buckets.size - 1) / buckets.size)
|
||||||
}
|
|
||||||
} else {
|
for (bucket in buckets) {
|
||||||
aiCharacterList
|
if (result.size >= targetSize) break
|
||||||
|
|
||||||
|
val batch = contentService.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
|
theme = theme,
|
||||||
|
contentType = contentType,
|
||||||
|
offset = bucket.offset,
|
||||||
|
limit = bucket.limit,
|
||||||
|
sortType = SortType.NEWEST,
|
||||||
|
isFree = isFree,
|
||||||
|
isAdult = isAdult,
|
||||||
|
orderByRandom = false,
|
||||||
|
isPointAvailableOnly = isPointAvailableOnly,
|
||||||
|
excludeContentIds = seenIds.toList()
|
||||||
|
)
|
||||||
|
|
||||||
|
val selected = pickByTimeDecay(
|
||||||
|
batch = batch,
|
||||||
|
targetSize = minOf(targetPerBucket, targetSize - result.size),
|
||||||
|
seenIds = seenIds
|
||||||
|
)
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
result.addAll(selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.take(targetSize).shuffled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatCharacterController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 인기 캐릭터 조회
|
// 인기 캐릭터 조회
|
||||||
val popularCharacters = service.getPopularCharacters()
|
val popularCharacters = service.getPopularCharacters(locale = langContext.lang.code)
|
||||||
|
|
||||||
// 최근 등록된 캐릭터 리스트 조회
|
// 최근 등록된 캐릭터 리스트 조회
|
||||||
val newCharacters = service.getRecentCharactersPage(
|
val newCharacters = service.getRecentCharactersPage(
|
||||||
@@ -138,7 +138,7 @@ class ChatCharacterController(
|
|||||||
CharacterMainResponse(
|
CharacterMainResponse(
|
||||||
banners = banners,
|
banners = banners,
|
||||||
recentCharacters = translatedRecentCharacters,
|
recentCharacters = translatedRecentCharacters,
|
||||||
popularCharacters = getTranslatedAiCharacterList(popularCharacters),
|
popularCharacters = popularCharacters,
|
||||||
newCharacters = getTranslatedAiCharacterList(newCharacters),
|
newCharacters = getTranslatedAiCharacterList(newCharacters),
|
||||||
recommendCharacters = getTranslatedAiCharacterList(recommendCharacters),
|
recommendCharacters = getTranslatedAiCharacterList(recommendCharacters),
|
||||||
curationSections = curationSections
|
curationSections = curationSections
|
||||||
|
|||||||
@@ -77,18 +77,23 @@ class ChatCharacterService(
|
|||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
cacheNames = ["popularCharacters_24h"],
|
cacheNames = ["popularCharacters_24h_locale"],
|
||||||
key = "T(kr.co.vividnext.sodalive.chat.character.service.RankingWindowCalculator).now('popular-character').cacheKey"
|
key = "T(kr.co.vividnext.sodalive.chat.character.service.RankingWindowCalculator)" +
|
||||||
|
".now('popular-character').cacheKey + '-' + #locale"
|
||||||
)
|
)
|
||||||
fun getPopularCharacters(limit: Long = 20): List<Character> {
|
fun getPopularCharacters(locale: String, limit: Long = 20): List<Character> {
|
||||||
val window = RankingWindowCalculator.now("popular-character")
|
val window = RankingWindowCalculator.now("popular-character")
|
||||||
val topIds = popularCharacterQuery.findPopularCharacterIds(window.windowStart, window.nextBoundary, limit)
|
val results = popularCharacterQuery.findPopularCharactersWithTranslation(
|
||||||
val list = loadCharactersInOrder(topIds)
|
window.windowStart,
|
||||||
|
window.nextBoundary,
|
||||||
|
limit,
|
||||||
|
locale
|
||||||
|
)
|
||||||
|
|
||||||
val recentSet = if (list.isNotEmpty()) {
|
val recentSet = if (results.isNotEmpty()) {
|
||||||
imageRepository
|
imageRepository
|
||||||
.findCharacterIdsWithRecentImages(
|
.findCharacterIdsWithRecentImages(
|
||||||
list.map { it.id!! },
|
results.map { it.id },
|
||||||
LocalDateTime.now().minusDays(3)
|
LocalDateTime.now().minusDays(3)
|
||||||
)
|
)
|
||||||
.toSet()
|
.toSet()
|
||||||
@@ -96,11 +101,11 @@ class ChatCharacterService(
|
|||||||
emptySet()
|
emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.map {
|
return results.map {
|
||||||
Character(
|
Character(
|
||||||
characterId = it.id!!,
|
characterId = it.id,
|
||||||
name = it.name,
|
name = it.translatedPayload?.name.takeIf { name -> !name.isNullOrBlank() } ?: it.name,
|
||||||
description = it.description,
|
description = it.translatedPayload?.description.takeIf { desc -> !desc.isNullOrBlank() } ?: it.description,
|
||||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
||||||
new = recentSet.contains(it.id)
|
new = recentSet.contains(it.id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.service
|
package kr.co.vividnext.sodalive.chat.character.service
|
||||||
|
|
||||||
|
import com.querydsl.core.types.Projections
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
import kr.co.vividnext.sodalive.chat.character.QChatCharacter
|
import kr.co.vividnext.sodalive.chat.character.QChatCharacter
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRenderedPayload
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.translate.QAiCharacterTranslation
|
||||||
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
||||||
import kr.co.vividnext.sodalive.chat.room.QChatMessage
|
import kr.co.vividnext.sodalive.chat.room.QChatMessage
|
||||||
import kr.co.vividnext.sodalive.chat.room.QChatParticipant
|
import kr.co.vividnext.sodalive.chat.room.QChatParticipant
|
||||||
@@ -51,4 +54,56 @@ class PopularCharacterQuery(
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class PopularCharacterQueryResult(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val imagePath: String?,
|
||||||
|
val translatedPayload: AiCharacterTranslationRenderedPayload?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun findPopularCharactersWithTranslation(
|
||||||
|
windowStart: Instant,
|
||||||
|
endExclusive: Instant,
|
||||||
|
limit: Long,
|
||||||
|
locale: String
|
||||||
|
): List<PopularCharacterQueryResult> {
|
||||||
|
val m = QChatMessage.chatMessage
|
||||||
|
val p = QChatParticipant.chatParticipant
|
||||||
|
val c = QChatCharacter.chatCharacter
|
||||||
|
val t = QAiCharacterTranslation.aiCharacterTranslation
|
||||||
|
|
||||||
|
val start = LocalDateTime.ofInstant(windowStart, ZoneOffset.UTC)
|
||||||
|
val end = LocalDateTime.ofInstant(endExclusive, ZoneOffset.UTC)
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
PopularCharacterQueryResult::class.java,
|
||||||
|
c.id,
|
||||||
|
c.name,
|
||||||
|
c.description,
|
||||||
|
c.imagePath,
|
||||||
|
t.renderedPayload
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(m)
|
||||||
|
.join(p).on(
|
||||||
|
p.chatRoom.id.eq(m.chatRoom.id)
|
||||||
|
.and(p.participantType.eq(ParticipantType.CHARACTER))
|
||||||
|
)
|
||||||
|
.join(c).on(c.id.eq(p.character.id))
|
||||||
|
.leftJoin(t).on(t.characterId.eq(c.id).and(t.locale.eq(locale)))
|
||||||
|
.where(
|
||||||
|
m.createdAt.goe(start)
|
||||||
|
.and(m.createdAt.lt(end))
|
||||||
|
.and(m.isActive.isTrue)
|
||||||
|
.and(c.isActive.isTrue)
|
||||||
|
)
|
||||||
|
.groupBy(c.id, c.name, c.description, c.imagePath, t.id, t.renderedPayload)
|
||||||
|
.orderBy(m.id.count().desc())
|
||||||
|
.limit(limit)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent
|
|||||||
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistContent
|
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistContent
|
||||||
import kr.co.vividnext.sodalive.content.playlist.QAudioContentPlaylistContent
|
import kr.co.vividnext.sodalive.content.playlist.QAudioContentPlaylistContent
|
||||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.QContentTranslation.contentTranslation
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
||||||
import kr.co.vividnext.sodalive.event.QEvent.event
|
import kr.co.vividnext.sodalive.event.QEvent.event
|
||||||
@@ -178,6 +179,7 @@ interface AudioContentQueryRepository {
|
|||||||
fun findContentIdAndHashTagId(contentId: Long, hashTagId: Int): AudioContentHashTag?
|
fun findContentIdAndHashTagId(contentId: Long, hashTagId: Int): AudioContentHashTag?
|
||||||
|
|
||||||
fun getLatestContentByTheme(
|
fun getLatestContentByTheme(
|
||||||
|
memberId: Long? = null,
|
||||||
theme: List<String>,
|
theme: List<String>,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
@@ -186,7 +188,9 @@ interface AudioContentQueryRepository {
|
|||||||
isFree: Boolean,
|
isFree: Boolean,
|
||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
orderByRandom: Boolean = false,
|
orderByRandom: Boolean = false,
|
||||||
isPointAvailableOnly: Boolean = false
|
isPointAvailableOnly: Boolean = false,
|
||||||
|
excludeContentIds: List<Long> = emptyList(),
|
||||||
|
locale: String? = null
|
||||||
): List<AudioContentMainItem>
|
): List<AudioContentMainItem>
|
||||||
|
|
||||||
fun findContentByCurationId(
|
fun findContentByCurationId(
|
||||||
@@ -1319,6 +1323,7 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getLatestContentByTheme(
|
override fun getLatestContentByTheme(
|
||||||
|
memberId: Long?,
|
||||||
theme: List<String>,
|
theme: List<String>,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
@@ -1327,8 +1332,18 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
isFree: Boolean,
|
isFree: Boolean,
|
||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
orderByRandom: Boolean,
|
orderByRandom: Boolean,
|
||||||
isPointAvailableOnly: Boolean
|
isPointAvailableOnly: Boolean,
|
||||||
|
excludeContentIds: List<Long>,
|
||||||
|
locale: String?
|
||||||
): List<AudioContentMainItem> {
|
): List<AudioContentMainItem> {
|
||||||
|
val blockMemberCondition = if (memberId != null) {
|
||||||
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
var where = audioContent.isActive.isTrue
|
var where = audioContent.isActive.isTrue
|
||||||
.and(audioContent.duration.isNotNull)
|
.and(audioContent.duration.isNotNull)
|
||||||
.and(
|
.and(
|
||||||
@@ -1366,6 +1381,31 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
where = where.and(audioContent.isPointAvailable.isTrue)
|
where = where.and(audioContent.isPointAvailable.isTrue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeContentIds.isNotEmpty()) {
|
||||||
|
where = where.and(audioContent.id.notIn(excludeContentIds))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale == null) {
|
||||||
|
var select = queryFactory
|
||||||
|
.select(
|
||||||
|
QAudioContentMainItem(
|
||||||
|
audioContent.id,
|
||||||
|
member.id,
|
||||||
|
audioContent.title,
|
||||||
|
audioContent.coverImage.prepend("/").prepend(imageHost),
|
||||||
|
member.nickname,
|
||||||
|
audioContent.isPointAvailable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(audioContent)
|
||||||
|
.innerJoin(audioContent.member, member)
|
||||||
|
.innerJoin(audioContent.theme, audioContentTheme)
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
where = where.and(blockMember.id.isNull)
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
}
|
||||||
|
|
||||||
val orderBy = if (orderByRandom) {
|
val orderBy = if (orderByRandom) {
|
||||||
Expressions.numberTemplate(Double::class.java, "function('rand')").asc()
|
Expressions.numberTemplate(Double::class.java, "function('rand')").asc()
|
||||||
} else {
|
} else {
|
||||||
@@ -1387,20 +1427,7 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryFactory
|
return select
|
||||||
.select(
|
|
||||||
QAudioContentMainItem(
|
|
||||||
audioContent.id,
|
|
||||||
member.id,
|
|
||||||
audioContent.title,
|
|
||||||
audioContent.coverImage.prepend("/").prepend(imageHost),
|
|
||||||
member.nickname,
|
|
||||||
audioContent.isPointAvailable
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.from(audioContent)
|
|
||||||
.innerJoin(audioContent.member, member)
|
|
||||||
.innerJoin(audioContent.theme, audioContentTheme)
|
|
||||||
.where(where)
|
.where(where)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@@ -1408,6 +1435,78 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val coverImageUrl = audioContent.coverImage.prepend("/").prepend(imageHost)
|
||||||
|
|
||||||
|
var select = queryFactory
|
||||||
|
.select(
|
||||||
|
audioContent.id,
|
||||||
|
member.id,
|
||||||
|
audioContent.title,
|
||||||
|
contentTranslation.renderedPayload,
|
||||||
|
coverImageUrl,
|
||||||
|
member.nickname,
|
||||||
|
audioContent.isPointAvailable
|
||||||
|
)
|
||||||
|
.from(audioContent)
|
||||||
|
.innerJoin(audioContent.member, member)
|
||||||
|
.innerJoin(audioContent.theme, audioContentTheme)
|
||||||
|
.leftJoin(contentTranslation)
|
||||||
|
.on(contentTranslation.contentId.eq(audioContent.id).and(contentTranslation.locale.eq(locale)))
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
where = where.and(blockMember.id.isNull)
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
val orderBy = if (orderByRandom) {
|
||||||
|
Expressions.numberTemplate(Double::class.java, "function('rand')").asc()
|
||||||
|
} else {
|
||||||
|
when (sortType) {
|
||||||
|
SortType.NEWEST -> audioContent.releaseDate.desc()
|
||||||
|
SortType.PRICE_HIGH -> if (isFree) {
|
||||||
|
audioContent.releaseDate.desc()
|
||||||
|
} else {
|
||||||
|
audioContent.price.desc()
|
||||||
|
}
|
||||||
|
|
||||||
|
SortType.PRICE_LOW -> if (isFree) {
|
||||||
|
audioContent.releaseDate.asc()
|
||||||
|
} else {
|
||||||
|
audioContent.price.desc()
|
||||||
|
}
|
||||||
|
|
||||||
|
SortType.POPULARITY -> audioContent.playCount.desc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val results = select
|
||||||
|
.where(where)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.orderBy(orderBy)
|
||||||
|
.fetch()
|
||||||
|
|
||||||
|
return results.map { row ->
|
||||||
|
val contentId = row.get(audioContent.id)!!
|
||||||
|
val creatorId = row.get(member.id)!!
|
||||||
|
val originTitle = row.get(audioContent.title)!!
|
||||||
|
val payload = row.get(contentTranslation.renderedPayload)
|
||||||
|
val translatedTitle = payload?.title
|
||||||
|
val imageUrl = row.get(coverImageUrl)!!
|
||||||
|
val creatorNickname = row.get(member.nickname)!!
|
||||||
|
val isPointAvailableValue = row.get(audioContent.isPointAvailable) ?: false
|
||||||
|
|
||||||
|
AudioContentMainItem(
|
||||||
|
contentId = contentId,
|
||||||
|
creatorId = creatorId,
|
||||||
|
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
|
||||||
|
coverImageUrl = imageUrl,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
isPointAvailable = isPointAvailableValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun findContentByCurationId(
|
override fun findContentByCurationId(
|
||||||
curationId: Long,
|
curationId: Long,
|
||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
|
|||||||
@@ -1198,6 +1198,7 @@ class AudioContentService(
|
|||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getLatestContentByTheme(
|
fun getLatestContentByTheme(
|
||||||
|
memberId: Long? = null,
|
||||||
theme: List<String>,
|
theme: List<String>,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
offset: Long = 0,
|
offset: Long = 0,
|
||||||
@@ -1206,7 +1207,8 @@ class AudioContentService(
|
|||||||
isFree: Boolean = false,
|
isFree: Boolean = false,
|
||||||
isAdult: Boolean = false,
|
isAdult: Boolean = false,
|
||||||
orderByRandom: Boolean = false,
|
orderByRandom: Boolean = false,
|
||||||
isPointAvailableOnly: Boolean = false
|
isPointAvailableOnly: Boolean = false,
|
||||||
|
excludeContentIds: List<Long> = emptyList()
|
||||||
): List<AudioContentMainItem> {
|
): List<AudioContentMainItem> {
|
||||||
/**
|
/**
|
||||||
* - AS-IS theme은 한글만 처리하도록 되어 있음
|
* - AS-IS theme은 한글만 처리하도록 되어 있음
|
||||||
@@ -1220,7 +1222,8 @@ class AudioContentService(
|
|||||||
isPointAvailableOnly = isPointAvailableOnly
|
isPointAvailableOnly = isPointAvailableOnly
|
||||||
)
|
)
|
||||||
|
|
||||||
val contentList = repository.getLatestContentByTheme(
|
return repository.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
theme = normalizedTheme,
|
theme = normalizedTheme,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
@@ -1229,26 +1232,10 @@ class AudioContentService(
|
|||||||
isFree = isFree,
|
isFree = isFree,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
orderByRandom = orderByRandom,
|
orderByRandom = orderByRandom,
|
||||||
isPointAvailableOnly = isPointAvailableOnly
|
isPointAvailableOnly = isPointAvailableOnly,
|
||||||
|
excludeContentIds = excludeContentIds,
|
||||||
|
locale = langContext.lang.code
|
||||||
)
|
)
|
||||||
|
|
||||||
val contentIds = contentList.map { it.contentId }
|
|
||||||
return if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = langContext.lang.code)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
contentList.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentList
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,10 +12,8 @@ import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayl
|
|||||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
|
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
|
||||||
import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries
|
import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState
|
|
||||||
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||||
import kr.co.vividnext.sodalive.i18n.Lang
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
@@ -26,7 +24,6 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
|||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@@ -55,12 +52,21 @@ class ContentSeriesService(
|
|||||||
fun getOriginalAudioDramaList(
|
fun getOriginalAudioDramaList(
|
||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
orderByRandom: Boolean = false,
|
|
||||||
offset: Long = 0,
|
offset: Long = 0,
|
||||||
limit: Long = 20
|
limit: Long = 20
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
): List<GetSeriesListResponse.SeriesListItem> {
|
||||||
val originalAudioDramaList = repository.getOriginalAudioDramaList(isAdult, contentType, orderByRandom, offset, limit)
|
val originalAudioDramaList = repository.getOriginalAudioDramaList(
|
||||||
return getTranslatedSeriesList(seriesToSeriesListItem(originalAudioDramaList, isAdult, contentType))
|
imageHost = coverImageHost,
|
||||||
|
isAdult = isAdult,
|
||||||
|
contentType = contentType,
|
||||||
|
locale = langContext.lang.code,
|
||||||
|
offset = offset,
|
||||||
|
limit = limit
|
||||||
|
)
|
||||||
|
|
||||||
|
return originalAudioDramaList.map { item ->
|
||||||
|
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGenreList(memberId: Long, isAdult: Boolean, contentType: ContentType): List<GetSeriesGenreListResponse> {
|
fun getGenreList(memberId: Long, isAdult: Boolean, contentType: ContentType): List<GetSeriesGenreListResponse> {
|
||||||
@@ -151,10 +157,11 @@ class ContentSeriesService(
|
|||||||
isAuth = isAuth,
|
isAuth = isAuth,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
isOriginal = isOriginal,
|
isOriginal = isOriginal,
|
||||||
isCompleted = isCompleted
|
isCompleted = isCompleted,
|
||||||
|
memberId = member.id
|
||||||
)
|
)
|
||||||
|
|
||||||
val rawItems = repository.getSeriesList(
|
val items = repository.getSeriesListV2(
|
||||||
imageHost = coverImageHost,
|
imageHost = coverImageHost,
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
isAuth = isAuth,
|
isAuth = isAuth,
|
||||||
@@ -163,11 +170,14 @@ class ContentSeriesService(
|
|||||||
isCompleted = isCompleted,
|
isCompleted = isCompleted,
|
||||||
orderByRandom = orderByRandom,
|
orderByRandom = orderByRandom,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
limit = limit
|
limit = limit,
|
||||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
locale = langContext.lang.code,
|
||||||
|
memberId = member.id
|
||||||
|
).map { item ->
|
||||||
|
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
|
||||||
|
}
|
||||||
|
|
||||||
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
|
return GetSeriesListResponse(totalCount, items)
|
||||||
return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSeriesListByGenre(
|
fun getSeriesListByGenre(
|
||||||
@@ -183,20 +193,24 @@ class ContentSeriesService(
|
|||||||
val totalCount = repository.getSeriesByGenreTotalCount(
|
val totalCount = repository.getSeriesByGenreTotalCount(
|
||||||
genreId = genreId,
|
genreId = genreId,
|
||||||
isAuth = isAuth,
|
isAuth = isAuth,
|
||||||
contentType = contentType
|
contentType = contentType,
|
||||||
|
memberId = member.id
|
||||||
)
|
)
|
||||||
|
|
||||||
val rawItems = repository.getSeriesByGenreList(
|
val items = repository.getSeriesByGenreListV2(
|
||||||
imageHost = coverImageHost,
|
imageHost = coverImageHost,
|
||||||
genreId = genreId,
|
genreId = genreId,
|
||||||
isAuth = isAuth,
|
isAuth = isAuth,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
limit = limit
|
limit = limit,
|
||||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
locale = langContext.lang.code,
|
||||||
|
memberId = member.id
|
||||||
|
).map { item ->
|
||||||
|
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
|
||||||
|
}
|
||||||
|
|
||||||
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
|
return GetSeriesListResponse(totalCount, items)
|
||||||
return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -459,19 +473,16 @@ class ContentSeriesService(
|
|||||||
member: Member
|
member: Member
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
): List<GetSeriesListResponse.SeriesListItem> {
|
||||||
val isAuth = member.auth != null && isAdultContentVisible
|
val isAuth = member.auth != null && isAdultContentVisible
|
||||||
val seriesList = repository.getRecommendSeriesList(
|
return repository.getRecommendSeriesListV2(
|
||||||
|
imageHost = coverImageHost,
|
||||||
isAuth = isAuth,
|
isAuth = isAuth,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
limit = 20
|
limit = 20,
|
||||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
locale = langContext.lang.code,
|
||||||
|
memberId = member.id
|
||||||
return getTranslatedSeriesList(
|
).map { item ->
|
||||||
seriesToSeriesListItem(
|
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
|
||||||
seriesList = seriesList,
|
}
|
||||||
isAdult = isAuth,
|
|
||||||
contentType = contentType
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchSeriesByCurationId(
|
fun fetchSeriesByCurationId(
|
||||||
@@ -480,13 +491,16 @@ class ContentSeriesService(
|
|||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
): List<GetSeriesListResponse.SeriesListItem> {
|
||||||
val seriesList = repository.findByCurationId(
|
return repository.findByCurationIdV2(
|
||||||
|
imageHost = coverImageHost,
|
||||||
curationId = curationId,
|
curationId = curationId,
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType
|
contentType = contentType,
|
||||||
)
|
locale = langContext.lang.code
|
||||||
return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType))
|
).map { item ->
|
||||||
|
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDayOfWeekSeriesList(
|
fun getDayOfWeekSeriesList(
|
||||||
@@ -497,71 +511,17 @@ class ContentSeriesService(
|
|||||||
offset: Long = 0,
|
offset: Long = 0,
|
||||||
limit: Long = 10
|
limit: Long = 10
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
): List<GetSeriesListResponse.SeriesListItem> {
|
||||||
var seriesList = repository.getDayOfWeekSeriesList(
|
return repository.getDayOfWeekSeriesListV2(
|
||||||
|
imageHost = coverImageHost,
|
||||||
dayOfWeek = dayOfWeek,
|
dayOfWeek = dayOfWeek,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
limit = limit
|
limit = limit,
|
||||||
)
|
locale = langContext.lang.code,
|
||||||
|
memberId = memberId
|
||||||
seriesList = if (memberId != null) {
|
).map { item ->
|
||||||
seriesList.filter {
|
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
|
||||||
!blockMemberRepository.isBlocked(
|
|
||||||
blockedMemberId = memberId,
|
|
||||||
memberId = it.member!!.id!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
seriesList
|
|
||||||
}
|
|
||||||
|
|
||||||
return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun seriesToSeriesListItem(
|
|
||||||
seriesList: List<Series>,
|
|
||||||
isAdult: Boolean,
|
|
||||||
contentType: ContentType
|
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
|
||||||
return seriesList
|
|
||||||
.map {
|
|
||||||
GetSeriesListResponse.SeriesListItem(
|
|
||||||
seriesId = it.id!!,
|
|
||||||
title = it.title,
|
|
||||||
coverImage = "$coverImageHost/${it.coverImage!!}",
|
|
||||||
publishedDaysOfWeek = publishedDaysOfWeekText(it.publishedDaysOfWeek),
|
|
||||||
isComplete = it.state == SeriesState.COMPLETE,
|
|
||||||
creator = GetSeriesListResponse.SeriesListItemCreator(
|
|
||||||
creatorId = it.member!!.id!!,
|
|
||||||
nickname = it.member!!.nickname,
|
|
||||||
profileImage = "$coverImageHost/${it.member!!.profileImage!!}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
it.numberOfContent = seriesContentRepository.getContentCount(
|
|
||||||
seriesId = it.seriesId,
|
|
||||||
isAdult = isAdult,
|
|
||||||
contentType = contentType
|
|
||||||
)
|
|
||||||
|
|
||||||
it
|
|
||||||
}
|
|
||||||
.filter {
|
|
||||||
it.numberOfContent > 0
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
val nowDateTime = LocalDateTime.now()
|
|
||||||
|
|
||||||
it.isNew = seriesContentRepository.isNewContent(
|
|
||||||
seriesId = it.seriesId,
|
|
||||||
isAdult = isAdult,
|
|
||||||
fromDate = nowDateTime.minusDays(7),
|
|
||||||
nowDate = nowDateTime
|
|
||||||
)
|
|
||||||
|
|
||||||
it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,39 +592,4 @@ class ContentSeriesService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 시리즈 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
|
||||||
*
|
|
||||||
* 처리 절차:
|
|
||||||
* - 입력된 시리즈들의 seriesId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
|
|
||||||
* seriesTranslationRepository에서 번역 데이터를 한 번에 조회한다.
|
|
||||||
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
|
|
||||||
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
|
|
||||||
*
|
|
||||||
* 성능:
|
|
||||||
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
|
|
||||||
*
|
|
||||||
* @param seriesList 번역 대상 SeriesListItem 목록
|
|
||||||
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
|
|
||||||
*/
|
|
||||||
private fun getTranslatedSeriesList(
|
|
||||||
seriesList: List<GetSeriesListResponse.SeriesListItem>
|
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
|
||||||
val seriesIds = seriesList.map { it.seriesId }
|
|
||||||
if (seriesIds.isEmpty()) return seriesList
|
|
||||||
|
|
||||||
val translations = seriesTranslationRepository
|
|
||||||
.findBySeriesIdInAndLocale(seriesIds = seriesIds, locale = langContext.lang.code)
|
|
||||||
.associateBy { it.seriesId }
|
|
||||||
|
|
||||||
return seriesList.map { item ->
|
|
||||||
val translatedTitle = translations[item.seriesId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package kr.co.vividnext.sodalive.content.series
|
package kr.co.vividnext.sodalive.content.series
|
||||||
|
|
||||||
|
import com.querydsl.core.annotations.QueryProjection
|
||||||
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||||
|
|
||||||
data class GetSeriesListResponse(
|
data class GetSeriesListResponse(
|
||||||
val totalCount: Int,
|
val totalCount: Int,
|
||||||
val items: List<SeriesListItem>
|
val items: List<SeriesListItem>
|
||||||
) {
|
) {
|
||||||
data class SeriesListItem(
|
data class SeriesListItem @QueryProjection constructor(
|
||||||
val seriesId: Long,
|
val seriesId: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val coverImage: String,
|
val coverImage: String,
|
||||||
@@ -13,10 +16,11 @@ data class GetSeriesListResponse(
|
|||||||
val creator: SeriesListItemCreator,
|
val creator: SeriesListItemCreator,
|
||||||
var numberOfContent: Int = 0,
|
var numberOfContent: Int = 0,
|
||||||
var isNew: Boolean = false,
|
var isNew: Boolean = false,
|
||||||
var isPopular: Boolean = false
|
var isPopular: Boolean = false,
|
||||||
|
val rawPublishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek> = emptySet()
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SeriesListItemCreator(
|
data class SeriesListItemCreator @QueryProjection constructor(
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
val nickname: String,
|
val nickname: String,
|
||||||
val profileImage: String
|
val profileImage: String
|
||||||
|
|||||||
@@ -637,6 +637,23 @@ class ExplorerQueryRepository(
|
|||||||
.fetchOne() ?: false
|
.fetchOne() ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getFollowedCreatorIds(creatorIds: List<Long>, memberId: Long): Set<Long> {
|
||||||
|
if (creatorIds.isEmpty()) {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.select(creatorFollowing.creator.id)
|
||||||
|
.from(creatorFollowing)
|
||||||
|
.where(
|
||||||
|
creatorFollowing.isActive.isTrue
|
||||||
|
.and(creatorFollowing.creator.id.`in`(creatorIds))
|
||||||
|
.and(creatorFollowing.member.id.eq(memberId))
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
fun getCreatorCheers(cheersId: Long): CreatorCheers? {
|
fun getCreatorCheers(cheersId: Long): CreatorCheers? {
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.selectFrom(creatorCheers)
|
.selectFrom(creatorCheers)
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import kr.co.vividnext.sodalive.content.ContentType
|
|||||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||||
import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment
|
import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment
|
||||||
import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike
|
import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.QContentTranslation.contentTranslation
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
import kr.co.vividnext.sodalive.member.QMember.member
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
import kr.co.vividnext.sodalive.member.auth.QAuth.auth
|
import kr.co.vividnext.sodalive.member.auth.QAuth.auth
|
||||||
|
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
@@ -19,7 +22,19 @@ class RecommendChannelQueryRepository(
|
|||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
) {
|
) {
|
||||||
fun getRecommendChannelList(isAdult: Boolean, contentType: ContentType): List<RecommendChannelResponse> {
|
fun getRecommendChannelList(
|
||||||
|
memberId: Long?,
|
||||||
|
isAdult: Boolean,
|
||||||
|
contentType: ContentType
|
||||||
|
): List<RecommendChannelResponse> {
|
||||||
|
val blockMemberCondition = if (memberId != null) {
|
||||||
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
var where = member.role.eq(MemberRole.CREATOR)
|
var where = member.role.eq(MemberRole.CREATOR)
|
||||||
.and(audioContent.isActive.isTrue)
|
.and(audioContent.isActive.isTrue)
|
||||||
|
|
||||||
@@ -39,7 +54,7 @@ class RecommendChannelQueryRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryFactory
|
var select = queryFactory
|
||||||
.select(
|
.select(
|
||||||
QRecommendChannelResponse(
|
QRecommendChannelResponse(
|
||||||
member.id,
|
member.id,
|
||||||
@@ -52,6 +67,13 @@ class RecommendChannelQueryRepository(
|
|||||||
.from(member)
|
.from(member)
|
||||||
.innerJoin(auth).on(auth.member.id.eq(member.id))
|
.innerJoin(auth).on(auth.member.id.eq(member.id))
|
||||||
.innerJoin(audioContent).on(audioContent.member.id.eq(member.id))
|
.innerJoin(audioContent).on(audioContent.member.id.eq(member.id))
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
where = where.and(blockMember.id.isNull)
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return select
|
||||||
.where(where)
|
.where(where)
|
||||||
.groupBy(member.id)
|
.groupBy(member.id)
|
||||||
.having(audioContent.id.count().goe(3))
|
.having(audioContent.id.count().goe(3))
|
||||||
@@ -60,22 +82,43 @@ class RecommendChannelQueryRepository(
|
|||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContentsByCreatorIdLikeDesc(creatorId: Long, isAdult: Boolean): List<RecommendChannelContentItem> {
|
fun getContentsByCreatorIdLikeDesc(
|
||||||
|
creatorId: Long,
|
||||||
|
memberId: Long?,
|
||||||
|
isAdult: Boolean,
|
||||||
|
locale: String? = null
|
||||||
|
): List<RecommendChannelContentItem> {
|
||||||
|
val blockMemberCondition = if (memberId != null) {
|
||||||
|
blockMember.member.id.eq(audioContent.member.id)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
var where = audioContent.member.id.eq(creatorId)
|
var where = audioContent.member.id.eq(creatorId)
|
||||||
|
|
||||||
if (!isAdult) {
|
if (!isAdult) {
|
||||||
where = where.and(audioContent.isAdult.isFalse)
|
where = where.and(audioContent.isAdult.isFalse)
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryFactory
|
val coverImageUrl = audioContent.coverImage.prepend("/").prepend(imageHost)
|
||||||
|
val payloadExpression = if (locale != null) {
|
||||||
|
contentTranslation.renderedPayload
|
||||||
|
} else {
|
||||||
|
Expressions.nullExpression(ContentTranslationPayload::class.java)
|
||||||
|
}
|
||||||
|
val likeCountExpression = audioContentLike.id.countDistinct()
|
||||||
|
val commentCountExpression = audioContentComment.id.countDistinct()
|
||||||
|
|
||||||
|
var select = queryFactory
|
||||||
.select(
|
.select(
|
||||||
QRecommendChannelContentItem(
|
|
||||||
audioContent.id,
|
audioContent.id,
|
||||||
audioContent.title,
|
audioContent.title,
|
||||||
audioContent.coverImage.prepend("/").prepend(imageHost),
|
payloadExpression,
|
||||||
audioContentLike.id.countDistinct(),
|
coverImageUrl,
|
||||||
audioContentComment.id.countDistinct()
|
likeCountExpression,
|
||||||
)
|
commentCountExpression
|
||||||
)
|
)
|
||||||
.from(audioContent)
|
.from(audioContent)
|
||||||
.leftJoin(audioContentLike)
|
.leftJoin(audioContentLike)
|
||||||
@@ -88,10 +131,43 @@ class RecommendChannelQueryRepository(
|
|||||||
audioContentComment.audioContent.id.eq(audioContent.id)
|
audioContentComment.audioContent.id.eq(audioContent.id)
|
||||||
.and(audioContentComment.isActive.isTrue)
|
.and(audioContentComment.isActive.isTrue)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (locale != null) {
|
||||||
|
select = select.leftJoin(contentTranslation)
|
||||||
|
.on(
|
||||||
|
contentTranslation.contentId.eq(audioContent.id)
|
||||||
|
.and(contentTranslation.locale.eq(locale))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
where = where.and(blockMember.id.isNull)
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
val results = select
|
||||||
.where(where)
|
.where(where)
|
||||||
.groupBy(audioContent.id)
|
.groupBy(audioContent.id)
|
||||||
.orderBy(audioContentLike.id.countDistinct().desc())
|
.orderBy(likeCountExpression.desc())
|
||||||
.limit(3)
|
.limit(3)
|
||||||
.fetch()
|
.fetch()
|
||||||
|
|
||||||
|
return results.map { row ->
|
||||||
|
val contentId = row.get(audioContent.id)!!
|
||||||
|
val originTitle = row.get(audioContent.title)!!
|
||||||
|
val payload = row.get(payloadExpression)
|
||||||
|
val translatedTitle = payload?.title
|
||||||
|
val thumbnailImageUrl = row.get(coverImageUrl)!!
|
||||||
|
val likeCount = row.get(likeCountExpression) ?: 0L
|
||||||
|
val commentCount = row.get(commentCountExpression) ?: 0L
|
||||||
|
|
||||||
|
RecommendChannelContentItem(
|
||||||
|
contentId = contentId,
|
||||||
|
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
|
||||||
|
thumbnailImageUrl = thumbnailImageUrl,
|
||||||
|
likeCount = likeCount,
|
||||||
|
commentCount = commentCount
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package kr.co.vividnext.sodalive.query.recommend
|
package kr.co.vividnext.sodalive.query.recommend
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.content.ContentType
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import org.springframework.cache.annotation.Cacheable
|
import org.springframework.cache.annotation.Cacheable
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
class RecommendChannelQueryService(private val repository: RecommendChannelQueryRepository) {
|
class RecommendChannelQueryService(
|
||||||
|
private val repository: RecommendChannelQueryRepository,
|
||||||
|
private val langContext: LangContext
|
||||||
|
) {
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
cacheNames = ["default"],
|
cacheNames = ["default"],
|
||||||
key = "'recommendChannel:' + (#memberId ?: 'guest') + ':' + #isAdult + ':' + #contentType"
|
key = "'recommendChannel:' + (#memberId ?: 'guest') + ':' + #isAdult + ':' + #contentType"
|
||||||
@@ -18,6 +22,7 @@ class RecommendChannelQueryService(private val repository: RecommendChannelQuery
|
|||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<RecommendChannelResponse> {
|
): List<RecommendChannelResponse> {
|
||||||
val recommendChannelList = repository.getRecommendChannelList(
|
val recommendChannelList = repository.getRecommendChannelList(
|
||||||
|
memberId = memberId,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType
|
contentType = contentType
|
||||||
)
|
)
|
||||||
@@ -25,7 +30,9 @@ class RecommendChannelQueryService(private val repository: RecommendChannelQuery
|
|||||||
return recommendChannelList.map {
|
return recommendChannelList.map {
|
||||||
it.contentList = repository.getContentsByCreatorIdLikeDesc(
|
it.contentList = repository.getContentsByCreatorIdLikeDesc(
|
||||||
creatorId = it.channelId,
|
creatorId = it.channelId,
|
||||||
isAdult = isAdult
|
memberId = memberId,
|
||||||
|
isAdult = isAdult,
|
||||||
|
locale = langContext.lang.code
|
||||||
)
|
)
|
||||||
|
|
||||||
it
|
it
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.content.main.QContentCreatorResponse
|
|||||||
import kr.co.vividnext.sodalive.content.main.QGetAudioContentRankingItem
|
import kr.co.vividnext.sodalive.content.main.QGetAudioContentRankingItem
|
||||||
import kr.co.vividnext.sodalive.content.order.QOrder.order
|
import kr.co.vividnext.sodalive.content.order.QOrder.order
|
||||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||||
@@ -33,11 +34,29 @@ class RankingRepository(
|
|||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
) {
|
) {
|
||||||
fun getCreatorRankings(): List<Member> {
|
fun getCreatorRankings(memberId: Long? = null): List<Member> {
|
||||||
return queryFactory
|
val blockMemberCondition = if (memberId != null) {
|
||||||
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
var select = queryFactory
|
||||||
.select(member)
|
.select(member)
|
||||||
.from(creatorRanking)
|
.from(creatorRanking)
|
||||||
.innerJoin(creatorRanking.member, member)
|
.innerJoin(creatorRanking.member, member)
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
select = select.where(blockMember.id.isNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
return select
|
||||||
.orderBy(creatorRanking.ranking.asc())
|
.orderBy(creatorRanking.ranking.asc())
|
||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
@@ -51,7 +70,8 @@ class RankingRepository(
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long,
|
limit: Long,
|
||||||
sortType: String,
|
sortType: String,
|
||||||
theme: String = ""
|
theme: String = "",
|
||||||
|
locale: String? = null
|
||||||
): List<GetAudioContentRankingItem> {
|
): List<GetAudioContentRankingItem> {
|
||||||
val blockMemberCondition = if (memberId != null) {
|
val blockMemberCondition = if (memberId != null) {
|
||||||
blockMember.member.id.eq(member.id)
|
blockMember.member.id.eq(member.id)
|
||||||
@@ -61,6 +81,8 @@ class RankingRepository(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val contentTranslation = kr.co.vividnext.sodalive.content.translation.QContentTranslation.contentTranslation
|
||||||
|
|
||||||
var where = audioContent.isActive.isTrue
|
var where = audioContent.isActive.isTrue
|
||||||
.and(audioContent.member.isActive.isTrue)
|
.and(audioContent.member.isActive.isTrue)
|
||||||
.and(audioContent.member.isNotNull)
|
.and(audioContent.member.isNotNull)
|
||||||
@@ -91,20 +113,27 @@ class RankingRepository(
|
|||||||
where = where.and(audioContentTheme.theme.eq(theme))
|
where = where.and(audioContentTheme.theme.eq(theme))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val coverImageUrl = audioContent.coverImage.prepend("/").prepend(imageHost)
|
||||||
|
val creatorProfileImageUrl = member.profileImage.prepend("/").prepend(imageHost)
|
||||||
|
val payloadExpression = if (locale != null) {
|
||||||
|
contentTranslation.renderedPayload
|
||||||
|
} else {
|
||||||
|
Expressions.nullExpression(ContentTranslationPayload::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
var select = queryFactory
|
var select = queryFactory
|
||||||
.select(
|
.select(
|
||||||
QGetAudioContentRankingItem(
|
|
||||||
audioContent.id,
|
audioContent.id,
|
||||||
audioContent.title,
|
audioContent.title,
|
||||||
audioContent.coverImage.prepend("/").prepend(imageHost),
|
payloadExpression,
|
||||||
|
coverImageUrl,
|
||||||
audioContentTheme.theme,
|
audioContentTheme.theme,
|
||||||
audioContent.price,
|
audioContent.price,
|
||||||
audioContent.duration,
|
audioContent.duration,
|
||||||
member.id,
|
member.id,
|
||||||
member.nickname,
|
member.nickname,
|
||||||
audioContent.isPointAvailable,
|
audioContent.isPointAvailable,
|
||||||
member.profileImage.prepend("/").prepend(imageHost)
|
creatorProfileImageUrl
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
select = when (sortType) {
|
select = when (sortType) {
|
||||||
@@ -149,6 +178,11 @@ class RankingRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (locale != null) {
|
||||||
|
select = select.leftJoin(contentTranslation)
|
||||||
|
.on(contentTranslation.contentId.eq(audioContent.id).and(contentTranslation.locale.eq(locale)))
|
||||||
|
}
|
||||||
|
|
||||||
if (memberId != null) {
|
if (memberId != null) {
|
||||||
where = where.and(blockMember.id.isNull)
|
where = where.and(blockMember.id.isNull)
|
||||||
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
@@ -217,10 +251,38 @@ class RankingRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return select
|
val results = select
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.fetch()
|
.fetch()
|
||||||
|
|
||||||
|
return results.map { row ->
|
||||||
|
val contentId = row.get(audioContent.id)!!
|
||||||
|
val originTitle = row.get(audioContent.title)!!
|
||||||
|
val payload = row.get(payloadExpression)
|
||||||
|
val translatedTitle = payload?.title
|
||||||
|
val imageUrl = row.get(coverImageUrl)!!
|
||||||
|
val themeStr = row.get(audioContentTheme.theme) ?: ""
|
||||||
|
val price = row.get(audioContent.price) ?: 0
|
||||||
|
val duration = row.get(audioContent.duration) ?: ""
|
||||||
|
val creatorId = row.get(member.id)!!
|
||||||
|
val creatorNickname = row.get(member.nickname)!!
|
||||||
|
val isPointAvailable = row.get(audioContent.isPointAvailable) ?: false
|
||||||
|
val creatorProfileImageUrlValue = row.get(creatorProfileImageUrl)!!
|
||||||
|
|
||||||
|
GetAudioContentRankingItem(
|
||||||
|
contentId = contentId,
|
||||||
|
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
|
||||||
|
coverImageUrl = imageUrl,
|
||||||
|
themeStr = themeStr,
|
||||||
|
price = price,
|
||||||
|
duration = duration,
|
||||||
|
creatorId = creatorId,
|
||||||
|
creatorNickname = creatorNickname,
|
||||||
|
isPointAvailable = isPointAvailable,
|
||||||
|
creatorProfileImageUrl = creatorProfileImageUrlValue
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSeriesRanking(
|
fun getSeriesRanking(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
|||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState
|
||||||
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse
|
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse
|
||||||
import kr.co.vividnext.sodalive.member.MemberService
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -17,22 +17,15 @@ import java.time.LocalDateTime
|
|||||||
@Service
|
@Service
|
||||||
class RankingService(
|
class RankingService(
|
||||||
private val repository: RankingRepository,
|
private val repository: RankingRepository,
|
||||||
private val memberService: MemberService,
|
|
||||||
private val seriesContentRepository: ContentSeriesContentRepository,
|
private val seriesContentRepository: ContentSeriesContentRepository,
|
||||||
|
private val langContext: LangContext,
|
||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
) {
|
) {
|
||||||
fun getCreatorRanking(memberId: Long?, rankingDate: String): GetExplorerSectionResponse {
|
fun getCreatorRanking(memberId: Long?, rankingDate: String): GetExplorerSectionResponse {
|
||||||
val creatorRankings = repository
|
val creatorRankings = repository
|
||||||
.getCreatorRankings()
|
.getCreatorRankings(memberId = memberId)
|
||||||
.filter {
|
|
||||||
if (memberId != null) {
|
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.id!!)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { it.toExplorerSectionCreator(imageHost) }
|
.map { it.toExplorerSectionCreator(imageHost) }
|
||||||
|
|
||||||
return GetExplorerSectionResponse(
|
return GetExplorerSectionResponse(
|
||||||
@@ -68,7 +61,8 @@ class RankingService(
|
|||||||
offset = offset,
|
offset = offset,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
sortType = sortType,
|
sortType = sortType,
|
||||||
theme = theme
|
theme = theme,
|
||||||
|
locale = langContext.lang.code
|
||||||
)
|
)
|
||||||
loopCount++
|
loopCount++
|
||||||
} while (contentList.size < 5 && loopCount < 5)
|
} while (contentList.size < 5 && loopCount < 5)
|
||||||
|
|||||||
Reference in New Issue
Block a user