feat(recommend): 홈 추천 차단 필터를 확장한다
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence
|
||||
|
||||
import com.querydsl.core.types.Expression
|
||||
import com.querydsl.core.types.Projections
|
||||
import com.querydsl.core.types.dsl.BooleanExpression
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.jpa.JPAExpressions
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
||||
import kr.co.vividnext.sodalive.chat.original.QOriginalWork
|
||||
@@ -19,6 +21,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorC
|
||||
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||
import kr.co.vividnext.sodalive.member.QMember
|
||||
import kr.co.vividnext.sodalive.member.QMember.member
|
||||
import kr.co.vividnext.sodalive.member.block.QBlockMember
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScoreSpec
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
|
||||
@@ -46,6 +49,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
override fun findLiveRecommendations(
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultLives: Boolean
|
||||
): List<HomeLiveRecommendationRecord> {
|
||||
return queryFactory
|
||||
@@ -69,6 +73,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
liveRoom.channelName.isNotNull,
|
||||
liveRoom.channelName.isNotEmpty,
|
||||
includeAdultLiveCondition(includeAdultLives),
|
||||
notBlockedCreatorCondition(memberId, member.id),
|
||||
member.isActive.isTrue
|
||||
)
|
||||
.orderBy(liveRoom.beginDateTime.desc(), liveRoom.id.desc())
|
||||
@@ -77,7 +82,10 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord> {
|
||||
override fun findHomeBanners(
|
||||
limit: Int,
|
||||
memberId: Long?
|
||||
): List<HomeBannerRecommendationRecord> {
|
||||
val bannerCreator = QMember("bannerCreator")
|
||||
val seriesOwner = QMember("seriesOwner")
|
||||
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')")
|
||||
@@ -105,7 +113,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
.where(
|
||||
audioContentBanner.isActive.isTrue,
|
||||
audioContentBanner.tab.isNull,
|
||||
activeBannerTargetCondition(bannerCreator, seriesOwner)
|
||||
activeBannerTargetCondition(memberId, bannerCreator, seriesOwner)
|
||||
)
|
||||
.orderBy(audioContentBanner.orders.asc(), randomTieBreaker.asc())
|
||||
.limit(limit.toLong())
|
||||
@@ -114,6 +122,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
|
||||
override fun findRecentlyActiveCreators(
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultActivities: Boolean
|
||||
): List<RecentlyActiveCreatorRecord> {
|
||||
val sql = """
|
||||
@@ -175,12 +184,14 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
) activities
|
||||
) ranked
|
||||
where ranked.creator_rank = 1
|
||||
and ${notBlockedCreatorSql("ranked.creator_id")}
|
||||
order by ranked.activity_at desc, ranked.target_sort_id desc
|
||||
limit :limit
|
||||
""".trimIndent()
|
||||
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setParameter("liveReplayTheme", LIVE_REPLAY_THEME)
|
||||
.setParameter("memberId", memberId)
|
||||
.setParameter("includeAdultActivities", includeAdultActivities)
|
||||
.setParameter("limit", limit)
|
||||
|
||||
@@ -203,6 +214,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
now: LocalDateTime,
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultContents: Boolean
|
||||
): List<RecentDebutCreatorRecord> {
|
||||
val sql = """
|
||||
@@ -321,6 +333,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
where m.is_active = true
|
||||
and cd.debut_at >= :boost30Start
|
||||
and cd.debut_at <= :now
|
||||
and ${notBlockedCreatorSql("m.id")}
|
||||
order by score desc, random_tie_breaker asc
|
||||
limit :limit
|
||||
offset :offset
|
||||
@@ -329,6 +342,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setRecommendationQueryParameters(now, limit)
|
||||
.setParameter("offset", offset)
|
||||
.setParameter("memberId", memberId)
|
||||
.setParameter("includeAdultContents", includeAdultContents)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -350,6 +364,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
now: LocalDateTime,
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultContents: Boolean
|
||||
): List<HomeFirstAudioContentRecord> {
|
||||
val sql = """
|
||||
@@ -423,6 +438,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
and cd.debut_at >= :boost30Start
|
||||
and cd.debut_at <= :now
|
||||
and ec.release_date >= :boost30Start
|
||||
and ${notBlockedCreatorSql("m.id")}
|
||||
order by recency_score desc, random_tie_breaker asc
|
||||
limit :limit
|
||||
offset :offset
|
||||
@@ -432,6 +448,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
.setParameter("now", now)
|
||||
.setParameter("limit", limit)
|
||||
.setParameter("offset", offset)
|
||||
.setParameter("memberId", memberId)
|
||||
.setParameter("includeAdultContents", includeAdultContents)
|
||||
.setParameter(
|
||||
"boost30Start",
|
||||
@@ -712,7 +729,8 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
}
|
||||
|
||||
override fun findCheerCreatorRecommendationDetails(
|
||||
creatorIds: List<Long>
|
||||
creatorIds: List<Long>,
|
||||
memberId: Long?
|
||||
): List<HomeCheerCreatorRecommendationRecord> {
|
||||
if (creatorIds.isEmpty()) return emptyList()
|
||||
|
||||
@@ -726,12 +744,13 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
)
|
||||
)
|
||||
.from(member)
|
||||
.where(member.isActive.isTrue, member.id.`in`(creatorIds))
|
||||
.where(member.isActive.isTrue, member.id.`in`(creatorIds), notBlockedCreatorCondition(memberId, member.id))
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findPopularCommunityRecommendationDetails(
|
||||
communityIds: List<Long>,
|
||||
memberId: Long?,
|
||||
includeAdultCommunities: Boolean
|
||||
): List<HomePopularCommunityRecommendationRecord> {
|
||||
if (communityIds.isEmpty()) return emptyList()
|
||||
@@ -766,6 +785,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
creatorCommunity.price.eq(0),
|
||||
creatorCommunity.isFixed.isFalse,
|
||||
includeAdultCommunityCondition(includeAdultCommunities),
|
||||
notBlockedCreatorCondition(memberId, member.id),
|
||||
creatorCommunity.id.`in`(communityIds)
|
||||
)
|
||||
.groupBy(
|
||||
@@ -846,7 +866,17 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
and cf.creator_id = m.id
|
||||
and cf.is_active = true
|
||||
)
|
||||
)
|
||||
and not exists (
|
||||
select 1
|
||||
from block_member bm
|
||||
where :memberId is not null
|
||||
and bm.is_active = true
|
||||
and (
|
||||
(bm.member_id = :memberId and bm.blocked_member_id = m.id)
|
||||
or (bm.member_id = m.id and bm.blocked_member_id = :memberId)
|
||||
)
|
||||
)
|
||||
)
|
||||
) selected
|
||||
order by selected.source_rank asc, selected.random_tie_breaker asc
|
||||
limit :targetLimit
|
||||
@@ -897,6 +927,16 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
and cf.creator_id = m.id
|
||||
and cf.is_active = true
|
||||
)
|
||||
and not exists (
|
||||
select 1
|
||||
from block_member bm
|
||||
where :memberId is not null
|
||||
and bm.is_active = true
|
||||
and (
|
||||
(bm.member_id = :memberId and bm.blocked_member_id = m.id)
|
||||
or (bm.member_id = m.id and bm.blocked_member_id = :memberId)
|
||||
)
|
||||
)
|
||||
group by m.id, m.nickname, m.profile_image
|
||||
) candidates
|
||||
order by rand() asc
|
||||
@@ -960,17 +1000,26 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
}
|
||||
|
||||
private fun activeBannerTargetCondition(
|
||||
memberId: Long?,
|
||||
bannerCreator: QMember,
|
||||
seriesOwner: QMember
|
||||
): BooleanExpression {
|
||||
val creatorCondition = audioContentBanner.type.eq(AudioContentBannerType.CREATOR)
|
||||
.and(bannerCreator.isActive.isTrue)
|
||||
.withOptionalAnd(notBlockedCreatorCondition(memberId, bannerCreator.id))
|
||||
val seriesCondition = audioContentBanner.type.eq(AudioContentBannerType.SERIES)
|
||||
.and(series.isActive.isTrue)
|
||||
.and(seriesOwner.isActive.isTrue)
|
||||
.withOptionalAnd(notBlockedCreatorCondition(memberId, seriesOwner.id))
|
||||
|
||||
return audioContentBanner.type.eq(AudioContentBannerType.LINK)
|
||||
.or(audioContentBanner.type.eq(AudioContentBannerType.EVENT).and(event.isActive.isTrue))
|
||||
.or(audioContentBanner.type.eq(AudioContentBannerType.CREATOR).and(bannerCreator.isActive.isTrue))
|
||||
.or(
|
||||
audioContentBanner.type.eq(AudioContentBannerType.SERIES)
|
||||
.and(series.isActive.isTrue)
|
||||
.and(seriesOwner.isActive.isTrue)
|
||||
)
|
||||
.or(creatorCondition)
|
||||
.or(seriesCondition)
|
||||
}
|
||||
|
||||
private fun BooleanExpression.withOptionalAnd(condition: BooleanExpression?): BooleanExpression {
|
||||
return if (condition == null) this else and(condition)
|
||||
}
|
||||
|
||||
private fun includeAdultCommunityCondition(includeAdultCommunities: Boolean): BooleanExpression? {
|
||||
@@ -981,6 +1030,35 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
return if (includeAdultLives) null else liveRoom.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun notBlockedCreatorCondition(memberId: Long?, creatorIdPath: Expression<Long>): BooleanExpression? {
|
||||
if (memberId == null) return null
|
||||
val blockMember = QBlockMember("recommendationBlockMember")
|
||||
return JPAExpressions
|
||||
.selectOne()
|
||||
.from(blockMember)
|
||||
.where(
|
||||
blockMember.isActive.isTrue,
|
||||
blockMember.member.id.eq(memberId).and(blockMember.blockedMember.id.eq(creatorIdPath))
|
||||
.or(blockMember.member.id.eq(creatorIdPath).and(blockMember.blockedMember.id.eq(memberId)))
|
||||
)
|
||||
.notExists()
|
||||
}
|
||||
|
||||
private fun notBlockedCreatorSql(creatorIdExpression: String): String {
|
||||
return """
|
||||
not exists (
|
||||
select 1
|
||||
from block_member bm
|
||||
where :memberId is not null
|
||||
and bm.is_active = true
|
||||
and (
|
||||
(bm.member_id = :memberId and bm.blocked_member_id = $creatorIdExpression)
|
||||
or (bm.member_id = $creatorIdExpression and bm.blocked_member_id = :memberId)
|
||||
)
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun javax.persistence.Query.setRecommendationQueryParameters(
|
||||
now: LocalDateTime,
|
||||
limit: Int
|
||||
|
||||
@@ -26,38 +26,45 @@ class HomeRecommendationQueryService(
|
||||
fun findLiveRecommendations(
|
||||
offset: Int = 0,
|
||||
limit: Int = DEFAULT_LIVE_LIMIT,
|
||||
memberId: Long? = null,
|
||||
includeAdultLives: Boolean = false
|
||||
): List<HomeLiveRecommendationRecord> {
|
||||
return queryPort.findLiveRecommendations(offset, limit, includeAdultLives)
|
||||
return queryPort.findLiveRecommendations(offset, limit, memberId, includeAdultLives)
|
||||
}
|
||||
|
||||
fun findHomeBanners(limit: Int = DEFAULT_BANNER_LIMIT): List<HomeBannerRecommendationRecord> {
|
||||
return queryPort.findHomeBanners(limit)
|
||||
fun findHomeBanners(
|
||||
limit: Int = DEFAULT_BANNER_LIMIT,
|
||||
memberId: Long? = null
|
||||
): List<HomeBannerRecommendationRecord> {
|
||||
return queryPort.findHomeBanners(limit, memberId)
|
||||
}
|
||||
|
||||
fun findRecentlyActiveCreators(
|
||||
limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT,
|
||||
memberId: Long? = null,
|
||||
includeAdultActivities: Boolean = false
|
||||
): List<RecentlyActiveCreatorRecord> {
|
||||
return queryPort.findRecentlyActiveCreators(limit, includeAdultActivities)
|
||||
return queryPort.findRecentlyActiveCreators(limit, memberId, includeAdultActivities)
|
||||
}
|
||||
|
||||
fun findRecentDebutCreators(
|
||||
now: LocalDateTime,
|
||||
offset: Int = 0,
|
||||
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT,
|
||||
memberId: Long? = null,
|
||||
includeAdultContents: Boolean = false
|
||||
): List<RecentDebutCreatorRecord> {
|
||||
return queryPort.findRecentDebutCreators(now, offset, limit, includeAdultContents)
|
||||
return queryPort.findRecentDebutCreators(now, offset, limit, memberId, includeAdultContents)
|
||||
}
|
||||
|
||||
fun findFirstAudioContents(
|
||||
now: LocalDateTime,
|
||||
offset: Int = 0,
|
||||
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT,
|
||||
memberId: Long? = null,
|
||||
includeAdultContents: Boolean = false
|
||||
): List<HomeFirstAudioContentRecord> {
|
||||
return queryPort.findFirstAudioContents(now, offset, limit, includeAdultContents)
|
||||
return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents)
|
||||
}
|
||||
|
||||
fun findAiCharacterRecommendations(
|
||||
@@ -72,23 +79,26 @@ class HomeRecommendationQueryService(
|
||||
}
|
||||
|
||||
fun findCheerCreatorRecommendations(
|
||||
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT
|
||||
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT,
|
||||
memberId: Long? = null
|
||||
): List<HomeCheerCreatorRecommendationRecord> {
|
||||
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).take(limit)
|
||||
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId })
|
||||
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).take(CHEER_CREATOR_CANDIDATE_LIMIT)
|
||||
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId }, memberId)
|
||||
.associateBy { it.creatorId }
|
||||
|
||||
return snapshots.mapNotNull { detailsById[it.targetId] }
|
||||
return snapshots.mapNotNull { detailsById[it.targetId] }.take(limit)
|
||||
}
|
||||
|
||||
fun findPopularCommunityRecommendations(
|
||||
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
|
||||
memberId: Long? = null,
|
||||
includeAdultCommunities: Boolean = false
|
||||
): List<HomePopularCommunityRecommendationRecord> {
|
||||
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY)
|
||||
.take(POPULAR_COMMUNITY_CANDIDATE_LIMIT)
|
||||
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
|
||||
snapshots.map { it.targetId },
|
||||
memberId,
|
||||
includeAdultCommunities
|
||||
)
|
||||
.associateBy { it.communityId }
|
||||
@@ -132,6 +142,7 @@ class HomeRecommendationQueryService(
|
||||
private const val DEFAULT_FIRST_AUDIO_CONTENT_LIMIT = 10
|
||||
private const val DEFAULT_AI_CHARACTER_LIMIT = 10
|
||||
private const val DEFAULT_CHEER_CREATOR_LIMIT = 8
|
||||
private const val CHEER_CREATOR_CANDIDATE_LIMIT = 16
|
||||
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
|
||||
|
||||
@@ -7,17 +7,26 @@ interface HomeRecommendationQueryPort {
|
||||
fun findLiveRecommendations(
|
||||
offset: Int = 0,
|
||||
limit: Int,
|
||||
memberId: Long? = null,
|
||||
includeAdultLives: Boolean = false
|
||||
): List<HomeLiveRecommendationRecord>
|
||||
|
||||
fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord>
|
||||
fun findHomeBanners(
|
||||
limit: Int,
|
||||
memberId: Long? = null
|
||||
): List<HomeBannerRecommendationRecord>
|
||||
|
||||
fun findRecentlyActiveCreators(limit: Int, includeAdultActivities: Boolean = false): List<RecentlyActiveCreatorRecord>
|
||||
fun findRecentlyActiveCreators(
|
||||
limit: Int,
|
||||
memberId: Long? = null,
|
||||
includeAdultActivities: Boolean = false
|
||||
): List<RecentlyActiveCreatorRecord>
|
||||
|
||||
fun findRecentDebutCreators(
|
||||
now: LocalDateTime,
|
||||
offset: Int = 0,
|
||||
limit: Int,
|
||||
memberId: Long? = null,
|
||||
includeAdultContents: Boolean = false
|
||||
): List<RecentDebutCreatorRecord>
|
||||
|
||||
@@ -25,6 +34,7 @@ interface HomeRecommendationQueryPort {
|
||||
now: LocalDateTime,
|
||||
offset: Int = 0,
|
||||
limit: Int,
|
||||
memberId: Long? = null,
|
||||
includeAdultContents: Boolean = false
|
||||
): List<HomeFirstAudioContentRecord>
|
||||
|
||||
@@ -48,10 +58,14 @@ interface HomeRecommendationQueryPort {
|
||||
|
||||
fun findAiCharacterRecommendationDetails(characterIds: List<Long>): List<HomeAiCharacterRecommendationRecord>
|
||||
|
||||
fun findCheerCreatorRecommendationDetails(creatorIds: List<Long>): List<HomeCheerCreatorRecommendationRecord>
|
||||
fun findCheerCreatorRecommendationDetails(
|
||||
creatorIds: List<Long>,
|
||||
memberId: Long? = null
|
||||
): List<HomeCheerCreatorRecommendationRecord>
|
||||
|
||||
fun findPopularCommunityRecommendationDetails(
|
||||
communityIds: List<Long>,
|
||||
memberId: Long? = null,
|
||||
includeAdultCommunities: Boolean
|
||||
): List<HomePopularCommunityRecommendationRecord>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user