feat(recommend): 장르 기반 크리에이터 추천 조회를 추가한다
This commit is contained in:
@@ -26,6 +26,8 @@ import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendat
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeBannerRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeCheerCreatorRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeFirstAudioContentRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeGenreCreatorRecommendationGroup
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeGenreCreatorRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeLiveRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentDebutCreatorRecord
|
||||
@@ -741,6 +743,148 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findGenreCreatorRecommendations(
|
||||
memberId: Long?,
|
||||
includeAdultGenres: Boolean,
|
||||
genreLimit: Int,
|
||||
creatorLimit: Int
|
||||
): List<HomeGenreCreatorRecommendationGroup> {
|
||||
val genres = findGenreRecommendationTargets(
|
||||
memberId = memberId,
|
||||
includeAdultGenres = includeAdultGenres,
|
||||
targetLimit = (genreLimit * creatorLimit).coerceAtLeast(genreLimit)
|
||||
)
|
||||
|
||||
return genres.asSequence().mapNotNull { genre ->
|
||||
val creators = findCreatorsByGenre(
|
||||
genreId = genre.id,
|
||||
memberId = memberId,
|
||||
includeAdultGenres = includeAdultGenres,
|
||||
creatorLimit = creatorLimit
|
||||
)
|
||||
creators.takeIf { it.isNotEmpty() }?.let {
|
||||
HomeGenreCreatorRecommendationGroup(
|
||||
genreId = genre.id,
|
||||
genreName = genre.name,
|
||||
creators = it
|
||||
)
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
|
||||
private fun findGenreRecommendationTargets(
|
||||
memberId: Long?,
|
||||
includeAdultGenres: Boolean,
|
||||
targetLimit: Int
|
||||
): List<GenreRecommendationTarget> {
|
||||
val sql = """
|
||||
select selected.id,
|
||||
selected.genre
|
||||
from (
|
||||
select ct.id,
|
||||
ct.theme as genre,
|
||||
case when viewed.theme_id is null then 1 else 0 end as source_rank,
|
||||
rand() as random_tie_breaker
|
||||
from content_theme ct
|
||||
left join (
|
||||
select distinct c.theme_id
|
||||
from creator_content_view_history ccvh
|
||||
join content c on c.id = ccvh.content_id
|
||||
where (:memberId is not null and ccvh.member_id = :memberId)
|
||||
) viewed on viewed.theme_id = ct.id
|
||||
where ct.is_active = true
|
||||
and exists (
|
||||
select 1
|
||||
from content c
|
||||
join member m on m.id = c.member_id
|
||||
where c.theme_id = ct.id
|
||||
and c.is_active = true
|
||||
and (:includeAdultGenres = true or c.is_adult = false)
|
||||
and m.is_active = true
|
||||
and m.role = 'CREATOR'
|
||||
and not exists (
|
||||
select 1
|
||||
from creator_following cf
|
||||
where :memberId is not null
|
||||
and cf.member_id = :memberId
|
||||
and cf.creator_id = m.id
|
||||
and cf.is_active = true
|
||||
)
|
||||
)
|
||||
) selected
|
||||
order by selected.source_rank asc, selected.random_tie_breaker asc
|
||||
limit :targetLimit
|
||||
""".trimIndent()
|
||||
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setParameter("memberId", memberId)
|
||||
.setParameter("includeAdultGenres", includeAdultGenres)
|
||||
.setParameter("targetLimit", targetLimit)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val rows = query.resultList as List<Array<Any?>>
|
||||
|
||||
return rows.map { row ->
|
||||
GenreRecommendationTarget(
|
||||
id = (row[0] as Number).toLong(),
|
||||
name = row[1] as String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findCreatorsByGenre(
|
||||
genreId: Long,
|
||||
memberId: Long?,
|
||||
includeAdultGenres: Boolean,
|
||||
creatorLimit: Int
|
||||
): List<HomeGenreCreatorRecommendationRecord> {
|
||||
val sql = """
|
||||
select candidates.creator_id,
|
||||
candidates.creator_nickname,
|
||||
candidates.creator_profile_image
|
||||
from (
|
||||
select m.id as creator_id,
|
||||
m.nickname as creator_nickname,
|
||||
m.profile_image as creator_profile_image
|
||||
from content c
|
||||
join member m on m.id = c.member_id
|
||||
where c.theme_id = :genreId
|
||||
and c.is_active = true
|
||||
and (:includeAdultGenres = true or c.is_adult = false)
|
||||
and m.is_active = true
|
||||
and m.role = 'CREATOR'
|
||||
and not exists (
|
||||
select 1
|
||||
from creator_following cf
|
||||
where :memberId is not null
|
||||
and cf.member_id = :memberId
|
||||
and cf.creator_id = m.id
|
||||
and cf.is_active = true
|
||||
)
|
||||
group by m.id, m.nickname, m.profile_image
|
||||
) candidates
|
||||
order by rand() asc
|
||||
limit :creatorLimit
|
||||
""".trimIndent()
|
||||
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setParameter("genreId", genreId)
|
||||
.setParameter("memberId", memberId)
|
||||
.setParameter("includeAdultGenres", includeAdultGenres)
|
||||
.setParameter("creatorLimit", creatorLimit)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val rows = query.resultList as List<Array<Any?>>
|
||||
|
||||
return rows.map { row ->
|
||||
HomeGenreCreatorRecommendationRecord(
|
||||
creatorId = (row[0] as Number).toLong(),
|
||||
creatorNickname = row[1] as String,
|
||||
creatorProfileImage = row[2] as String?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeSnapshotQuery(
|
||||
sql: String,
|
||||
sectionType: RecommendedSectionType,
|
||||
@@ -830,4 +974,9 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
companion object {
|
||||
private const val LIVE_REPLAY_THEME = "다시듣기"
|
||||
}
|
||||
|
||||
private data class GenreRecommendationTarget(
|
||||
val id: Long,
|
||||
val name: String
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendat
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeBannerRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeCheerCreatorRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeFirstAudioContentRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeGenreCreatorRecommendationGroup
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeLiveRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeRecommendationQueryPort
|
||||
@@ -86,6 +87,23 @@ class HomeRecommendationQueryService(
|
||||
}.take(limit)
|
||||
}
|
||||
|
||||
fun findGenreCreatorRecommendations(
|
||||
memberId: Long?,
|
||||
includeAdultGenres: Boolean,
|
||||
genreLimit: Int = DEFAULT_GENRE_CREATOR_GENRE_LIMIT,
|
||||
creatorLimit: Int = DEFAULT_GENRE_CREATOR_CREATOR_LIMIT
|
||||
): List<HomeGenreCreatorRecommendationGroup> {
|
||||
val selectedCreatorIds = mutableSetOf<Long>()
|
||||
val candidateLimit = genreLimit * creatorLimit
|
||||
|
||||
return queryPort.findGenreCreatorRecommendations(memberId, includeAdultGenres, genreLimit, candidateLimit)
|
||||
.map { group ->
|
||||
group.copy(creators = group.creators.filter { selectedCreatorIds.add(it.creatorId) }.take(creatorLimit))
|
||||
}
|
||||
.filter { it.creators.isNotEmpty() }
|
||||
.take(genreLimit)
|
||||
}
|
||||
|
||||
fun resolveAudioContentActivityType(theme: String): RecommendedActivityType {
|
||||
return if (theme == LIVE_REPLAY_THEME) {
|
||||
RecommendedActivityType.LIVE_REPLAY
|
||||
@@ -107,6 +125,8 @@ class HomeRecommendationQueryService(
|
||||
private const val DEFAULT_AI_CHARACTER_LIMIT = 10
|
||||
private const val DEFAULT_CHEER_CREATOR_LIMIT = 8
|
||||
private const val DEFAULT_POPULAR_COMMUNITY_LIMIT = 10
|
||||
private const val DEFAULT_GENRE_CREATOR_GENRE_LIMIT = 5
|
||||
private const val DEFAULT_GENRE_CREATOR_CREATOR_LIMIT = 8
|
||||
private const val POPULAR_COMMUNITY_CANDIDATE_LIMIT = 20
|
||||
private const val LIVE_REPLAY_THEME = "다시듣기"
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ interface HomeRecommendationQueryPort {
|
||||
communityIds: List<Long>,
|
||||
includeAdultCommunities: Boolean
|
||||
): List<HomePopularCommunityRecommendationRecord>
|
||||
|
||||
fun findGenreCreatorRecommendations(
|
||||
memberId: Long?,
|
||||
includeAdultGenres: Boolean,
|
||||
genreLimit: Int,
|
||||
creatorLimit: Int
|
||||
): List<HomeGenreCreatorRecommendationGroup>
|
||||
}
|
||||
|
||||
data class HomeLiveRecommendationRecord(
|
||||
@@ -119,3 +126,15 @@ data class HomePopularCommunityRecommendationRecord(
|
||||
val likeCount: Long,
|
||||
val commentCount: Long
|
||||
)
|
||||
|
||||
data class HomeGenreCreatorRecommendationGroup(
|
||||
val genreId: Long,
|
||||
val genreName: String,
|
||||
val creators: List<HomeGenreCreatorRecommendationRecord>
|
||||
)
|
||||
|
||||
data class HomeGenreCreatorRecommendationRecord(
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImage: String?
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user