Compare commits

...

3 Commits

Author SHA1 Message Date
e690bf8aec 추천 콘텐츠 시간 감쇠 적용 2026-02-12 18:14:08 +09:00
1ca7e1744d 홈 크리에이터 랭킹 팔로우 조회 최적화
홈 API의 크리에이터 랭킹 응답에서 팔로우 여부를 일괄 조회로 계산한다.
2026-02-12 16:18:50 +09:00
232d97e37e 차단 사용자 제외를 조회 쿼리로 통합
홈, 추천 채널, 랭킹 조회에서 차단 사용자 제외를
애플리케이션 필터링 대신 DB 쿼리로 처리한다.
콘텐츠/랭킹/추천 조회 API에 memberId 인자를 전달한다.
2026-02-12 16:01:53 +09:00
8 changed files with 251 additions and 110 deletions

View File

@@ -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
} }
/** /**

View File

@@ -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)

View File

@@ -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 }

View File

@@ -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)

View File

@@ -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())

View File

@@ -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
) )

View File

@@ -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()
} }

View File

@@ -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(