diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt index a844b37b..929ef50d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt @@ -237,29 +237,28 @@ class HomeService( recommendChannelList } - val freeContentList = contentService.getLatestContentByTheme( + val freeContentList = getRandomizedContentList( memberId = memberId, + isAdult = isAdult, + contentType = contentType, theme = contentThemeService.getActiveThemeOfContent( isAdult = isAdult, isFree = true, contentType = contentType ), - contentType = contentType, isFree = true, - isAdult = isAdult, - orderByRandom = true + isPointAvailableOnly = false ) val translatedFreeContentList = getTranslatedContentList(contentList = freeContentList) // 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용) - val pointAvailableContentList = contentService.getLatestContentByTheme( + val pointAvailableContentList = getRandomizedContentList( memberId = memberId, - theme = emptyList(), - contentType = contentType, - isFree = false, isAdult = isAdult, - orderByRandom = true, + contentType = contentType, + theme = emptyList(), + isFree = false, isPointAvailableOnly = true ) @@ -501,6 +500,61 @@ class HomeService( return candidates.lastIndex } + private fun getRandomizedContentList( + memberId: Long?, + isAdult: Boolean, + contentType: ContentType, + theme: List, + isFree: Boolean, + isPointAvailableOnly: Boolean, + targetSize: Int = 20 + ): List { + val buckets = listOf( + RecommendBucket(offset = 0L, limit = 50L), + RecommendBucket(offset = 50L, limit = 100L), + RecommendBucket(offset = 150L, limit = 150L) + ) + + val result = mutableListOf() + val seenIds = mutableSetOf() + + repeat(RECOMMEND_MAX_ATTEMPTS) { + if (result.size >= targetSize) return@repeat + + val remaining = targetSize - result.size + val targetPerBucket = maxOf(1, (remaining + buckets.size - 1) / buckets.size) + + for (bucket in buckets) { + 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 getTranslatedContentList(contentList = result.take(targetSize).shuffled()) + } + /** * 콘텐츠 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다. *