Compare commits
3 Commits
7afbf1bff8
...
e690bf8aec
| Author | SHA1 | Date | |
|---|---|---|---|
| e690bf8aec | |||
| 1ca7e1744d | |||
| 232d97e37e |
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationR
|
|||||||
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.main.curation.AudioContentCurationService
|
||||||
@@ -22,7 +23,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,10 +34,10 @@ 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,
|
||||||
@@ -64,8 +64,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,25 +95,20 @@ 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 {
|
emptySet()
|
||||||
true
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
.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(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
@@ -111,17 +117,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 translatedLatestContentList = getTranslatedContentList(contentList = latestContentList)
|
||||||
|
|
||||||
@@ -237,6 +238,7 @@ class HomeService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val freeContentList = contentService.getLatestContentByTheme(
|
val freeContentList = contentService.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
theme = contentThemeService.getActiveThemeOfContent(
|
theme = contentThemeService.getActiveThemeOfContent(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
isFree = true,
|
isFree = true,
|
||||||
@@ -246,34 +248,28 @@ class HomeService(
|
|||||||
isFree = true,
|
isFree = true,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
orderByRandom = true
|
orderByRandom = true
|
||||||
).filter {
|
)
|
||||||
if (memberId != null) {
|
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val translatedFreeContentList = getTranslatedContentList(contentList = freeContentList)
|
val translatedFreeContentList = getTranslatedContentList(contentList = freeContentList)
|
||||||
|
|
||||||
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
|
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
|
||||||
val pointAvailableContentList = contentService.getLatestContentByTheme(
|
val pointAvailableContentList = contentService.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
theme = emptyList(),
|
theme = emptyList(),
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
isFree = false,
|
isFree = false,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
orderByRandom = true,
|
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 translatedPointAvailableContentList = getTranslatedContentList(contentList = pointAvailableContentList)
|
||||||
|
|
||||||
|
val excludeContentIds = (
|
||||||
|
translatedLatestContentList.map { it.contentId } +
|
||||||
|
translatedContentRanking.map { it.contentId }
|
||||||
|
).distinct()
|
||||||
|
|
||||||
val curationList = curationService.getContentCurationList(
|
val curationList = curationService.getContentCurationList(
|
||||||
tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용
|
tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
@@ -299,7 +295,8 @@ class HomeService(
|
|||||||
recommendContentList = getRecommendContentList(
|
recommendContentList = getRecommendContentList(
|
||||||
isAdultContentVisible = isAdultContentVisible,
|
isAdultContentVisible = isAdultContentVisible,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
member = member
|
member = member,
|
||||||
|
excludeContentIds = excludeContentIds
|
||||||
),
|
),
|
||||||
curationList = curationList
|
curationList = curationList
|
||||||
)
|
)
|
||||||
@@ -326,17 +323,12 @@ class HomeService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val contentList = contentService.getLatestContentByTheme(
|
val contentList = 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)
|
return getTranslatedContentList(contentList = contentList)
|
||||||
}
|
}
|
||||||
@@ -412,46 +404,101 @@ 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 batch = contentService.getLatestContentByTheme(
|
|
||||||
theme = emptyList(), // 특정 테마에 종속되지 않도록 전체에서 랜덤 조회
|
|
||||||
contentType = contentType,
|
|
||||||
offset = 0,
|
|
||||||
limit = (RECOMMEND_TARGET_SIZE * RECOMMEND_MAX_ATTEMPTS).toLong(), // 60개 조회
|
|
||||||
isFree = false,
|
|
||||||
isAdult = isAdult,
|
|
||||||
orderByRandom = true
|
|
||||||
).filter {
|
|
||||||
if (memberId != null) {
|
|
||||||
!memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (item in batch) {
|
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
|
if (result.size >= RECOMMEND_TARGET_SIZE) break
|
||||||
if (seen.add(item.contentId)) {
|
|
||||||
result.add(item)
|
val batch = contentService.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
|
theme = emptyList(),
|
||||||
|
contentType = contentType,
|
||||||
|
offset = bucket.offset,
|
||||||
|
limit = bucket.limit,
|
||||||
|
sortType = SortType.NEWEST,
|
||||||
|
isFree = false,
|
||||||
|
isAdult = isAdult,
|
||||||
|
orderByRandom = false,
|
||||||
|
excludeContentIds = seenIds.toList()
|
||||||
|
)
|
||||||
|
|
||||||
|
val selected = pickByTimeDecay(
|
||||||
|
batch = batch,
|
||||||
|
targetSize = minOf(targetPerBucket, RECOMMEND_TARGET_SIZE - result.size),
|
||||||
|
seenIds = seenIds
|
||||||
|
)
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
result.addAll(selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTranslatedContentList(contentList = result)
|
return getTranslatedContentList(contentList = result.take(RECOMMEND_TARGET_SIZE).shuffled())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pickByTimeDecay(
|
||||||
|
batch: List<AudioContentMainItem>,
|
||||||
|
targetSize: Int,
|
||||||
|
seenIds: MutableSet<Long>
|
||||||
|
): List<AudioContentMainItem> {
|
||||||
|
if (targetSize <= 0 || batch.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val candidates = batch
|
||||||
|
.asSequence()
|
||||||
|
.filterNot { seenIds.contains(it.contentId) }
|
||||||
|
.mapIndexed { index, item -> DecayCandidate(item = item, rank = index) }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
if (candidates.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val selected = mutableListOf<AudioContentMainItem>()
|
||||||
|
while (selected.size < targetSize && candidates.isNotEmpty()) {
|
||||||
|
val selectedIndex = selectByWeight(candidates)
|
||||||
|
val chosen = candidates.removeAt(selectedIndex).item
|
||||||
|
if (seenIds.add(chosen.contentId)) {
|
||||||
|
selected.add(chosen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectByWeight(candidates: List<DecayCandidate>): Int {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -178,6 +178,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 +187,8 @@ 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()
|
||||||
): List<AudioContentMainItem>
|
): List<AudioContentMainItem>
|
||||||
|
|
||||||
fun findContentByCurationId(
|
fun findContentByCurationId(
|
||||||
@@ -1319,6 +1321,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 +1330,17 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
isFree: Boolean,
|
isFree: Boolean,
|
||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
orderByRandom: Boolean,
|
orderByRandom: Boolean,
|
||||||
isPointAvailableOnly: Boolean
|
isPointAvailableOnly: Boolean,
|
||||||
|
excludeContentIds: List<Long>
|
||||||
): 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 +1378,30 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
where = where.and(audioContent.isPointAvailable.isTrue)
|
where = where.and(audioContent.isPointAvailable.isTrue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeContentIds.isNotEmpty()) {
|
||||||
|
where = where.and(audioContent.id.notIn(excludeContentIds))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +1423,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)
|
||||||
|
|||||||
@@ -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은 한글만 처리하도록 되어 있음
|
||||||
@@ -1221,6 +1223,7 @@ class AudioContentService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val contentList = repository.getLatestContentByTheme(
|
val contentList = repository.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
theme = normalizedTheme,
|
theme = normalizedTheme,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
@@ -1229,7 +1232,8 @@ class AudioContentService(
|
|||||||
isFree = isFree,
|
isFree = isFree,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
orderByRandom = orderByRandom,
|
orderByRandom = orderByRandom,
|
||||||
isPointAvailableOnly = isPointAvailableOnly
|
isPointAvailableOnly = isPointAvailableOnly,
|
||||||
|
excludeContentIds = excludeContentIds
|
||||||
)
|
)
|
||||||
|
|
||||||
val contentIds = contentList.map { it.contentId }
|
val contentIds = contentList.map { it.contentId }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike
|
|||||||
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 +20,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 +52,7 @@ class RecommendChannelQueryRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryFactory
|
var select = queryFactory
|
||||||
.select(
|
.select(
|
||||||
QRecommendChannelResponse(
|
QRecommendChannelResponse(
|
||||||
member.id,
|
member.id,
|
||||||
@@ -52,6 +65,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,14 +80,26 @@ class RecommendChannelQueryRepository(
|
|||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContentsByCreatorIdLikeDesc(creatorId: Long, isAdult: Boolean): List<RecommendChannelContentItem> {
|
fun getContentsByCreatorIdLikeDesc(
|
||||||
|
creatorId: Long,
|
||||||
|
memberId: Long?,
|
||||||
|
isAdult: Boolean
|
||||||
|
): 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
|
var select = queryFactory
|
||||||
.select(
|
.select(
|
||||||
QRecommendChannelContentItem(
|
QRecommendChannelContentItem(
|
||||||
audioContent.id,
|
audioContent.id,
|
||||||
@@ -88,6 +120,13 @@ class RecommendChannelQueryRepository(
|
|||||||
audioContentComment.audioContent.id.eq(audioContent.id)
|
audioContentComment.audioContent.id.eq(audioContent.id)
|
||||||
.and(audioContentComment.isActive.isTrue)
|
.and(audioContentComment.isActive.isTrue)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
where = where.and(blockMember.id.isNull)
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return select
|
||||||
.where(where)
|
.where(where)
|
||||||
.groupBy(audioContent.id)
|
.groupBy(audioContent.id)
|
||||||
.orderBy(audioContentLike.id.countDistinct().desc())
|
.orderBy(audioContentLike.id.countDistinct().desc())
|
||||||
|
|||||||
@@ -18,6 +18,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,6 +26,7 @@ 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,
|
||||||
|
memberId = memberId,
|
||||||
isAdult = isAdult
|
isAdult = isAdult
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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 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,7 +16,6 @@ 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,
|
||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
@@ -25,14 +23,7 @@ class RankingService(
|
|||||||
) {
|
) {
|
||||||
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(
|
||||||
|
|||||||
Reference in New Issue
Block a user