feat(recommend): 홈 추천 차단 필터를 확장한다
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence
|
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.Projections
|
||||||
import com.querydsl.core.types.dsl.BooleanExpression
|
import com.querydsl.core.types.dsl.BooleanExpression
|
||||||
import com.querydsl.core.types.dsl.Expressions
|
import com.querydsl.core.types.dsl.Expressions
|
||||||
|
import com.querydsl.jpa.JPAExpressions
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
||||||
import kr.co.vividnext.sodalive.chat.original.QOriginalWork
|
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.live.room.QLiveRoom.liveRoom
|
||||||
import kr.co.vividnext.sodalive.member.QMember
|
import kr.co.vividnext.sodalive.member.QMember
|
||||||
import kr.co.vividnext.sodalive.member.QMember.member
|
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.RecommendationScoreSpec
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
|
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
|
||||||
@@ -46,6 +49,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
override fun findLiveRecommendations(
|
override fun findLiveRecommendations(
|
||||||
offset: Int,
|
offset: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultLives: Boolean
|
includeAdultLives: Boolean
|
||||||
): List<HomeLiveRecommendationRecord> {
|
): List<HomeLiveRecommendationRecord> {
|
||||||
return queryFactory
|
return queryFactory
|
||||||
@@ -69,6 +73,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
liveRoom.channelName.isNotNull,
|
liveRoom.channelName.isNotNull,
|
||||||
liveRoom.channelName.isNotEmpty,
|
liveRoom.channelName.isNotEmpty,
|
||||||
includeAdultLiveCondition(includeAdultLives),
|
includeAdultLiveCondition(includeAdultLives),
|
||||||
|
notBlockedCreatorCondition(memberId, member.id),
|
||||||
member.isActive.isTrue
|
member.isActive.isTrue
|
||||||
)
|
)
|
||||||
.orderBy(liveRoom.beginDateTime.desc(), liveRoom.id.desc())
|
.orderBy(liveRoom.beginDateTime.desc(), liveRoom.id.desc())
|
||||||
@@ -77,7 +82,10 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord> {
|
override fun findHomeBanners(
|
||||||
|
limit: Int,
|
||||||
|
memberId: Long?
|
||||||
|
): List<HomeBannerRecommendationRecord> {
|
||||||
val bannerCreator = QMember("bannerCreator")
|
val bannerCreator = QMember("bannerCreator")
|
||||||
val seriesOwner = QMember("seriesOwner")
|
val seriesOwner = QMember("seriesOwner")
|
||||||
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')")
|
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')")
|
||||||
@@ -105,7 +113,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
.where(
|
.where(
|
||||||
audioContentBanner.isActive.isTrue,
|
audioContentBanner.isActive.isTrue,
|
||||||
audioContentBanner.tab.isNull,
|
audioContentBanner.tab.isNull,
|
||||||
activeBannerTargetCondition(bannerCreator, seriesOwner)
|
activeBannerTargetCondition(memberId, bannerCreator, seriesOwner)
|
||||||
)
|
)
|
||||||
.orderBy(audioContentBanner.orders.asc(), randomTieBreaker.asc())
|
.orderBy(audioContentBanner.orders.asc(), randomTieBreaker.asc())
|
||||||
.limit(limit.toLong())
|
.limit(limit.toLong())
|
||||||
@@ -114,6 +122,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
|
|
||||||
override fun findRecentlyActiveCreators(
|
override fun findRecentlyActiveCreators(
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultActivities: Boolean
|
includeAdultActivities: Boolean
|
||||||
): List<RecentlyActiveCreatorRecord> {
|
): List<RecentlyActiveCreatorRecord> {
|
||||||
val sql = """
|
val sql = """
|
||||||
@@ -175,12 +184,14 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
) activities
|
) activities
|
||||||
) ranked
|
) ranked
|
||||||
where ranked.creator_rank = 1
|
where ranked.creator_rank = 1
|
||||||
|
and ${notBlockedCreatorSql("ranked.creator_id")}
|
||||||
order by ranked.activity_at desc, ranked.target_sort_id desc
|
order by ranked.activity_at desc, ranked.target_sort_id desc
|
||||||
limit :limit
|
limit :limit
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
val query = entityManager.createNativeQuery(sql)
|
val query = entityManager.createNativeQuery(sql)
|
||||||
.setParameter("liveReplayTheme", LIVE_REPLAY_THEME)
|
.setParameter("liveReplayTheme", LIVE_REPLAY_THEME)
|
||||||
|
.setParameter("memberId", memberId)
|
||||||
.setParameter("includeAdultActivities", includeAdultActivities)
|
.setParameter("includeAdultActivities", includeAdultActivities)
|
||||||
.setParameter("limit", limit)
|
.setParameter("limit", limit)
|
||||||
|
|
||||||
@@ -203,6 +214,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultContents: Boolean
|
includeAdultContents: Boolean
|
||||||
): List<RecentDebutCreatorRecord> {
|
): List<RecentDebutCreatorRecord> {
|
||||||
val sql = """
|
val sql = """
|
||||||
@@ -321,6 +333,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
where m.is_active = true
|
where m.is_active = true
|
||||||
and cd.debut_at >= :boost30Start
|
and cd.debut_at >= :boost30Start
|
||||||
and cd.debut_at <= :now
|
and cd.debut_at <= :now
|
||||||
|
and ${notBlockedCreatorSql("m.id")}
|
||||||
order by score desc, random_tie_breaker asc
|
order by score desc, random_tie_breaker asc
|
||||||
limit :limit
|
limit :limit
|
||||||
offset :offset
|
offset :offset
|
||||||
@@ -329,6 +342,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
val query = entityManager.createNativeQuery(sql)
|
val query = entityManager.createNativeQuery(sql)
|
||||||
.setRecommendationQueryParameters(now, limit)
|
.setRecommendationQueryParameters(now, limit)
|
||||||
.setParameter("offset", offset)
|
.setParameter("offset", offset)
|
||||||
|
.setParameter("memberId", memberId)
|
||||||
.setParameter("includeAdultContents", includeAdultContents)
|
.setParameter("includeAdultContents", includeAdultContents)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -350,6 +364,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultContents: Boolean
|
includeAdultContents: Boolean
|
||||||
): List<HomeFirstAudioContentRecord> {
|
): List<HomeFirstAudioContentRecord> {
|
||||||
val sql = """
|
val sql = """
|
||||||
@@ -423,6 +438,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
and cd.debut_at >= :boost30Start
|
and cd.debut_at >= :boost30Start
|
||||||
and cd.debut_at <= :now
|
and cd.debut_at <= :now
|
||||||
and ec.release_date >= :boost30Start
|
and ec.release_date >= :boost30Start
|
||||||
|
and ${notBlockedCreatorSql("m.id")}
|
||||||
order by recency_score desc, random_tie_breaker asc
|
order by recency_score desc, random_tie_breaker asc
|
||||||
limit :limit
|
limit :limit
|
||||||
offset :offset
|
offset :offset
|
||||||
@@ -432,6 +448,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
.setParameter("now", now)
|
.setParameter("now", now)
|
||||||
.setParameter("limit", limit)
|
.setParameter("limit", limit)
|
||||||
.setParameter("offset", offset)
|
.setParameter("offset", offset)
|
||||||
|
.setParameter("memberId", memberId)
|
||||||
.setParameter("includeAdultContents", includeAdultContents)
|
.setParameter("includeAdultContents", includeAdultContents)
|
||||||
.setParameter(
|
.setParameter(
|
||||||
"boost30Start",
|
"boost30Start",
|
||||||
@@ -712,7 +729,8 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun findCheerCreatorRecommendationDetails(
|
override fun findCheerCreatorRecommendationDetails(
|
||||||
creatorIds: List<Long>
|
creatorIds: List<Long>,
|
||||||
|
memberId: Long?
|
||||||
): List<HomeCheerCreatorRecommendationRecord> {
|
): List<HomeCheerCreatorRecommendationRecord> {
|
||||||
if (creatorIds.isEmpty()) return emptyList()
|
if (creatorIds.isEmpty()) return emptyList()
|
||||||
|
|
||||||
@@ -726,12 +744,13 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.from(member)
|
.from(member)
|
||||||
.where(member.isActive.isTrue, member.id.`in`(creatorIds))
|
.where(member.isActive.isTrue, member.id.`in`(creatorIds), notBlockedCreatorCondition(memberId, member.id))
|
||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findPopularCommunityRecommendationDetails(
|
override fun findPopularCommunityRecommendationDetails(
|
||||||
communityIds: List<Long>,
|
communityIds: List<Long>,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultCommunities: Boolean
|
includeAdultCommunities: Boolean
|
||||||
): List<HomePopularCommunityRecommendationRecord> {
|
): List<HomePopularCommunityRecommendationRecord> {
|
||||||
if (communityIds.isEmpty()) return emptyList()
|
if (communityIds.isEmpty()) return emptyList()
|
||||||
@@ -766,6 +785,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
creatorCommunity.price.eq(0),
|
creatorCommunity.price.eq(0),
|
||||||
creatorCommunity.isFixed.isFalse,
|
creatorCommunity.isFixed.isFalse,
|
||||||
includeAdultCommunityCondition(includeAdultCommunities),
|
includeAdultCommunityCondition(includeAdultCommunities),
|
||||||
|
notBlockedCreatorCondition(memberId, member.id),
|
||||||
creatorCommunity.id.`in`(communityIds)
|
creatorCommunity.id.`in`(communityIds)
|
||||||
)
|
)
|
||||||
.groupBy(
|
.groupBy(
|
||||||
@@ -846,6 +866,16 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
and cf.creator_id = m.id
|
and cf.creator_id = m.id
|
||||||
and cf.is_active = true
|
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
|
) selected
|
||||||
order by selected.source_rank asc, selected.random_tie_breaker asc
|
order by selected.source_rank asc, selected.random_tie_breaker asc
|
||||||
@@ -897,6 +927,16 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
and cf.creator_id = m.id
|
and cf.creator_id = m.id
|
||||||
and cf.is_active = true
|
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
|
group by m.id, m.nickname, m.profile_image
|
||||||
) candidates
|
) candidates
|
||||||
order by rand() asc
|
order by rand() asc
|
||||||
@@ -960,17 +1000,26 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun activeBannerTargetCondition(
|
private fun activeBannerTargetCondition(
|
||||||
|
memberId: Long?,
|
||||||
bannerCreator: QMember,
|
bannerCreator: QMember,
|
||||||
seriesOwner: QMember
|
seriesOwner: QMember
|
||||||
): BooleanExpression {
|
): BooleanExpression {
|
||||||
return audioContentBanner.type.eq(AudioContentBannerType.LINK)
|
val creatorCondition = audioContentBanner.type.eq(AudioContentBannerType.CREATOR)
|
||||||
.or(audioContentBanner.type.eq(AudioContentBannerType.EVENT).and(event.isActive.isTrue))
|
.and(bannerCreator.isActive.isTrue)
|
||||||
.or(audioContentBanner.type.eq(AudioContentBannerType.CREATOR).and(bannerCreator.isActive.isTrue))
|
.withOptionalAnd(notBlockedCreatorCondition(memberId, bannerCreator.id))
|
||||||
.or(
|
val seriesCondition = audioContentBanner.type.eq(AudioContentBannerType.SERIES)
|
||||||
audioContentBanner.type.eq(AudioContentBannerType.SERIES)
|
|
||||||
.and(series.isActive.isTrue)
|
.and(series.isActive.isTrue)
|
||||||
.and(seriesOwner.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(creatorCondition)
|
||||||
|
.or(seriesCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BooleanExpression.withOptionalAnd(condition: BooleanExpression?): BooleanExpression {
|
||||||
|
return if (condition == null) this else and(condition)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun includeAdultCommunityCondition(includeAdultCommunities: Boolean): BooleanExpression? {
|
private fun includeAdultCommunityCondition(includeAdultCommunities: Boolean): BooleanExpression? {
|
||||||
@@ -981,6 +1030,35 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
return if (includeAdultLives) null else liveRoom.isAdult.isFalse
|
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(
|
private fun javax.persistence.Query.setRecommendationQueryParameters(
|
||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
limit: Int
|
limit: Int
|
||||||
|
|||||||
@@ -26,38 +26,45 @@ class HomeRecommendationQueryService(
|
|||||||
fun findLiveRecommendations(
|
fun findLiveRecommendations(
|
||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
limit: Int = DEFAULT_LIVE_LIMIT,
|
limit: Int = DEFAULT_LIVE_LIMIT,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultLives: Boolean = false
|
includeAdultLives: Boolean = false
|
||||||
): List<HomeLiveRecommendationRecord> {
|
): List<HomeLiveRecommendationRecord> {
|
||||||
return queryPort.findLiveRecommendations(offset, limit, includeAdultLives)
|
return queryPort.findLiveRecommendations(offset, limit, memberId, includeAdultLives)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findHomeBanners(limit: Int = DEFAULT_BANNER_LIMIT): List<HomeBannerRecommendationRecord> {
|
fun findHomeBanners(
|
||||||
return queryPort.findHomeBanners(limit)
|
limit: Int = DEFAULT_BANNER_LIMIT,
|
||||||
|
memberId: Long? = null
|
||||||
|
): List<HomeBannerRecommendationRecord> {
|
||||||
|
return queryPort.findHomeBanners(limit, memberId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findRecentlyActiveCreators(
|
fun findRecentlyActiveCreators(
|
||||||
limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT,
|
limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultActivities: Boolean = false
|
includeAdultActivities: Boolean = false
|
||||||
): List<RecentlyActiveCreatorRecord> {
|
): List<RecentlyActiveCreatorRecord> {
|
||||||
return queryPort.findRecentlyActiveCreators(limit, includeAdultActivities)
|
return queryPort.findRecentlyActiveCreators(limit, memberId, includeAdultActivities)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findRecentDebutCreators(
|
fun findRecentDebutCreators(
|
||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT,
|
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultContents: Boolean = false
|
includeAdultContents: Boolean = false
|
||||||
): List<RecentDebutCreatorRecord> {
|
): List<RecentDebutCreatorRecord> {
|
||||||
return queryPort.findRecentDebutCreators(now, offset, limit, includeAdultContents)
|
return queryPort.findRecentDebutCreators(now, offset, limit, memberId, includeAdultContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findFirstAudioContents(
|
fun findFirstAudioContents(
|
||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT,
|
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultContents: Boolean = false
|
includeAdultContents: Boolean = false
|
||||||
): List<HomeFirstAudioContentRecord> {
|
): List<HomeFirstAudioContentRecord> {
|
||||||
return queryPort.findFirstAudioContents(now, offset, limit, includeAdultContents)
|
return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findAiCharacterRecommendations(
|
fun findAiCharacterRecommendations(
|
||||||
@@ -72,23 +79,26 @@ class HomeRecommendationQueryService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun findCheerCreatorRecommendations(
|
fun findCheerCreatorRecommendations(
|
||||||
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT
|
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT,
|
||||||
|
memberId: Long? = null
|
||||||
): List<HomeCheerCreatorRecommendationRecord> {
|
): List<HomeCheerCreatorRecommendationRecord> {
|
||||||
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).take(limit)
|
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).take(CHEER_CREATOR_CANDIDATE_LIMIT)
|
||||||
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId })
|
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId }, memberId)
|
||||||
.associateBy { it.creatorId }
|
.associateBy { it.creatorId }
|
||||||
|
|
||||||
return snapshots.mapNotNull { detailsById[it.targetId] }
|
return snapshots.mapNotNull { detailsById[it.targetId] }.take(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findPopularCommunityRecommendations(
|
fun findPopularCommunityRecommendations(
|
||||||
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
|
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultCommunities: Boolean = false
|
includeAdultCommunities: Boolean = false
|
||||||
): List<HomePopularCommunityRecommendationRecord> {
|
): List<HomePopularCommunityRecommendationRecord> {
|
||||||
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY)
|
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY)
|
||||||
.take(POPULAR_COMMUNITY_CANDIDATE_LIMIT)
|
.take(POPULAR_COMMUNITY_CANDIDATE_LIMIT)
|
||||||
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
|
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
|
||||||
snapshots.map { it.targetId },
|
snapshots.map { it.targetId },
|
||||||
|
memberId,
|
||||||
includeAdultCommunities
|
includeAdultCommunities
|
||||||
)
|
)
|
||||||
.associateBy { it.communityId }
|
.associateBy { it.communityId }
|
||||||
@@ -132,6 +142,7 @@ class HomeRecommendationQueryService(
|
|||||||
private const val DEFAULT_FIRST_AUDIO_CONTENT_LIMIT = 10
|
private const val DEFAULT_FIRST_AUDIO_CONTENT_LIMIT = 10
|
||||||
private const val DEFAULT_AI_CHARACTER_LIMIT = 10
|
private const val DEFAULT_AI_CHARACTER_LIMIT = 10
|
||||||
private const val DEFAULT_CHEER_CREATOR_LIMIT = 8
|
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_POPULAR_COMMUNITY_LIMIT = 10
|
||||||
private const val DEFAULT_GENRE_CREATOR_GENRE_LIMIT = 5
|
private const val DEFAULT_GENRE_CREATOR_GENRE_LIMIT = 5
|
||||||
private const val DEFAULT_GENRE_CREATOR_CREATOR_LIMIT = 8
|
private const val DEFAULT_GENRE_CREATOR_CREATOR_LIMIT = 8
|
||||||
|
|||||||
@@ -7,17 +7,26 @@ interface HomeRecommendationQueryPort {
|
|||||||
fun findLiveRecommendations(
|
fun findLiveRecommendations(
|
||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultLives: Boolean = false
|
includeAdultLives: Boolean = false
|
||||||
): List<HomeLiveRecommendationRecord>
|
): 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(
|
fun findRecentDebutCreators(
|
||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultContents: Boolean = false
|
includeAdultContents: Boolean = false
|
||||||
): List<RecentDebutCreatorRecord>
|
): List<RecentDebutCreatorRecord>
|
||||||
|
|
||||||
@@ -25,6 +34,7 @@ interface HomeRecommendationQueryPort {
|
|||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultContents: Boolean = false
|
includeAdultContents: Boolean = false
|
||||||
): List<HomeFirstAudioContentRecord>
|
): List<HomeFirstAudioContentRecord>
|
||||||
|
|
||||||
@@ -48,10 +58,14 @@ interface HomeRecommendationQueryPort {
|
|||||||
|
|
||||||
fun findAiCharacterRecommendationDetails(characterIds: List<Long>): List<HomeAiCharacterRecommendationRecord>
|
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(
|
fun findPopularCommunityRecommendationDetails(
|
||||||
communityIds: List<Long>,
|
communityIds: List<Long>,
|
||||||
|
memberId: Long? = null,
|
||||||
includeAdultCommunities: Boolean
|
includeAdultCommunities: Boolean
|
||||||
): List<HomePopularCommunityRecommendationRecord>
|
): List<HomePopularCommunityRecommendationRecord>
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import kr.co.vividnext.sodalive.i18n.Lang
|
|||||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScorePolicy
|
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScorePolicy
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
||||||
@@ -105,6 +106,26 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals(listOf(oldest.id), page1.map { it.liveRoomId })
|
assertEquals(listOf(oldest.id), page1.map { it.liveRoomId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("라이브 추천은 회원과 크리에이터의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromLiveRecommendations() {
|
||||||
|
val baseAt = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
|
val viewer = saveMember("blocked-live-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-live", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-live", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-live", MemberRole.CREATOR)
|
||||||
|
saveLiveRoom(viewerBlockedCreator, baseAt.plusMinutes(3), channelName = "viewer-blocked-live")
|
||||||
|
saveLiveRoom(creatorBlockedViewer, baseAt.plusMinutes(2), channelName = "creator-blocked-live")
|
||||||
|
val visibleLive = saveLiveRoom(visibleCreator, baseAt.plusMinutes(1), channelName = "visible-live")
|
||||||
|
saveBlock(viewer, viewerBlockedCreator)
|
||||||
|
saveBlock(creatorBlockedViewer, viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val lives = repository.findLiveRecommendations(limit = 10, memberId = viewer.id)
|
||||||
|
|
||||||
|
assertEquals(listOf(visibleLive.id), lives.map { it.liveRoomId })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("홈 배너는 활성 배너를 orders 오름차순 최대 20개로 조회하고 동일 orders는 랜덤 tie-breaker를 적용한다")
|
@DisplayName("홈 배너는 활성 배너를 orders 오름차순 최대 20개로 조회하고 동일 orders는 랜덤 tie-breaker를 적용한다")
|
||||||
fun shouldFindActiveHomeBannersWithOrderLimitAndRandomTieBreaker() {
|
fun shouldFindActiveHomeBannersWithOrderLimitAndRandomTieBreaker() {
|
||||||
@@ -234,6 +255,77 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("홈 배너는 크리에이터와 시리즈 소유자의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromHomeBanners() {
|
||||||
|
val viewer = saveMember("blocked-banner-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-banner", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-banner", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-banner", MemberRole.CREATOR)
|
||||||
|
val viewerBlockedSeries = saveSeries("viewer-blocked-series-banner", viewerBlockedCreator, isActive = true)
|
||||||
|
val visibleSeries = saveSeries("visible-series-banner", visibleCreator, isActive = true)
|
||||||
|
val event = saveEvent("blocked-banner-event")
|
||||||
|
val visibleEventBanner = saveBanner(
|
||||||
|
"visible-event-banner.png",
|
||||||
|
AudioContentBannerType.EVENT,
|
||||||
|
orders = 1,
|
||||||
|
isActive = true,
|
||||||
|
event = event
|
||||||
|
)
|
||||||
|
saveBanner(
|
||||||
|
"viewer-blocked-creator-banner.png",
|
||||||
|
AudioContentBannerType.CREATOR,
|
||||||
|
orders = 2,
|
||||||
|
isActive = true,
|
||||||
|
creator = viewerBlockedCreator
|
||||||
|
)
|
||||||
|
saveBanner(
|
||||||
|
"creator-blocked-viewer-banner.png",
|
||||||
|
AudioContentBannerType.CREATOR,
|
||||||
|
orders = 3,
|
||||||
|
isActive = true,
|
||||||
|
creator = creatorBlockedViewer
|
||||||
|
)
|
||||||
|
val visibleCreatorBanner = saveBanner(
|
||||||
|
"visible-creator-banner.png",
|
||||||
|
AudioContentBannerType.CREATOR,
|
||||||
|
orders = 4,
|
||||||
|
isActive = true,
|
||||||
|
creator = visibleCreator
|
||||||
|
)
|
||||||
|
saveBanner(
|
||||||
|
"viewer-blocked-series-banner.png",
|
||||||
|
AudioContentBannerType.SERIES,
|
||||||
|
orders = 5,
|
||||||
|
isActive = true,
|
||||||
|
series = viewerBlockedSeries
|
||||||
|
)
|
||||||
|
val visibleSeriesBanner = saveBanner(
|
||||||
|
"visible-series-banner.png",
|
||||||
|
AudioContentBannerType.SERIES,
|
||||||
|
orders = 6,
|
||||||
|
isActive = true,
|
||||||
|
series = visibleSeries
|
||||||
|
)
|
||||||
|
val visibleLinkBanner = saveBanner(
|
||||||
|
"visible-link-banner.png",
|
||||||
|
AudioContentBannerType.LINK,
|
||||||
|
orders = 7,
|
||||||
|
isActive = true,
|
||||||
|
link = "https://visible-link.test"
|
||||||
|
)
|
||||||
|
saveBlock(viewer, viewerBlockedCreator)
|
||||||
|
saveBlock(creatorBlockedViewer, viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val banners = repository.findHomeBanners(limit = 20, memberId = viewer.id)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(visibleEventBanner.id, visibleCreatorBanner.id, visibleSeriesBanner.id, visibleLinkBanner.id),
|
||||||
|
banners.map { it.bannerId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("최근 활동 크리에이터는 크리에이터당 최신 활동 1개를 LIVE/AUDIO/COMMUNITY/LIVE_REPLAY로 분류한다")
|
@DisplayName("최근 활동 크리에이터는 크리에이터당 최신 활동 1개를 LIVE/AUDIO/COMMUNITY/LIVE_REPLAY로 분류한다")
|
||||||
fun shouldFindOneLatestActivityPerCreatorWithActivityType() {
|
fun shouldFindOneLatestActivityPerCreatorWithActivityType() {
|
||||||
@@ -302,6 +394,28 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals(RecommendedActivityType.COMMUNITY, visibleCreators[3].activityType)
|
assertEquals(RecommendedActivityType.COMMUNITY, visibleCreators[3].activityType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최근 활동 크리에이터는 회원과 크리에이터의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromRecentlyActiveCreators() {
|
||||||
|
val baseAt = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
|
val viewer = saveMember("blocked-activity-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-activity", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-activity", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-activity", MemberRole.CREATOR)
|
||||||
|
saveLiveRoom(viewerBlockedCreator, baseAt.plusMinutes(3), channelName = "viewer-blocked-activity")
|
||||||
|
saveAudioContent(creatorBlockedViewer, baseAt.plusMinutes(2), isActive = true)
|
||||||
|
val community = saveCommunity(visibleCreator, isCommentAvailable = true)
|
||||||
|
updateCreatedAt("CreatorCommunity", community.id!!, baseAt.plusMinutes(1))
|
||||||
|
saveBlock(viewer, viewerBlockedCreator)
|
||||||
|
saveBlock(creatorBlockedViewer, viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val creators = repository.findRecentlyActiveCreators(limit = 10, memberId = viewer.id)
|
||||||
|
|
||||||
|
assertEquals(listOf(visibleCreator.id), creators.map { it.creatorId })
|
||||||
|
assertEquals(RecommendedActivityType.COMMUNITY, creators.single().activityType)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("AI 캐릭터 스냅샷은 AI 발화 수와 중복 없는 활성 사용자 수를 집계하고 팔로우 증가량은 제외한다")
|
@DisplayName("AI 캐릭터 스냅샷은 AI 발화 수와 중복 없는 활성 사용자 수를 집계하고 팔로우 증가량은 제외한다")
|
||||||
fun shouldFindAiCharacterSnapshotsWithoutFollowIncrease() {
|
fun shouldFindAiCharacterSnapshotsWithoutFollowIncrease() {
|
||||||
@@ -863,6 +977,26 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals(listOf(normalOldest.id), page1.map { it.creatorId })
|
assertEquals(listOf(normalOldest.id), page1.map { it.creatorId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최근 데뷔 크리에이터는 회원과 크리에이터의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromRecentDebutCreators() {
|
||||||
|
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
|
val viewer = saveMember("blocked-debut-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-debut", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-debut", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-debut", MemberRole.CREATOR)
|
||||||
|
saveAudioContent(viewerBlockedCreator, now.minusDays(3), isActive = true)
|
||||||
|
saveAudioContent(creatorBlockedViewer, now.minusDays(2), isActive = true)
|
||||||
|
saveAudioContent(visibleCreator, now.minusDays(1), isActive = true)
|
||||||
|
saveBlock(viewer, viewerBlockedCreator)
|
||||||
|
saveBlock(creatorBlockedViewer, viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val creators = repository.findRecentDebutCreators(now, limit = 10, memberId = viewer.id)
|
||||||
|
|
||||||
|
assertEquals(listOf(visibleCreator.id), creators.map { it.creatorId })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("최근 데뷔 크리에이터 전체보기 동점 후보는 page size 1에서도 안정적으로 겹치지 않는다")
|
@DisplayName("최근 데뷔 크리에이터 전체보기 동점 후보는 page size 1에서도 안정적으로 겹치지 않는다")
|
||||||
fun shouldFindPagedRecentDebutCreatorsWithStableTieOrdering() {
|
fun shouldFindPagedRecentDebutCreatorsWithStableTieOrdering() {
|
||||||
@@ -970,6 +1104,26 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals(listOf(oldest.id), page1.map { it.contentId })
|
assertEquals(listOf(oldest.id), page1.map { it.contentId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("첫 오디오 콘텐츠는 회원과 크리에이터의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromFirstAudioContents() {
|
||||||
|
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
|
val viewer = saveMember("blocked-first-audio-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-first-audio", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-first-audio", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-first-audio", MemberRole.CREATOR)
|
||||||
|
saveAudioContent(viewerBlockedCreator, now.minusDays(3), isActive = true)
|
||||||
|
saveAudioContent(creatorBlockedViewer, now.minusDays(2), isActive = true)
|
||||||
|
val visibleContent = saveAudioContent(visibleCreator, now.minusDays(1), isActive = true)
|
||||||
|
saveBlock(viewer, viewerBlockedCreator)
|
||||||
|
saveBlock(creatorBlockedViewer, viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val contents = repository.findFirstAudioContents(now, limit = 10, memberId = viewer.id)
|
||||||
|
|
||||||
|
assertEquals(listOf(visibleContent.id), contents.map { it.contentId })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("첫 오디오 전체보기 동점 후보는 page size 1에서도 안정적으로 겹치지 않는다")
|
@DisplayName("첫 오디오 전체보기 동점 후보는 page size 1에서도 안정적으로 겹치지 않는다")
|
||||||
fun shouldFindPagedFirstAudioContentsWithStableTieOrdering() {
|
fun shouldFindPagedFirstAudioContentsWithStableTieOrdering() {
|
||||||
@@ -1046,6 +1200,25 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals("cheer-detail-active", detailById[activeCreator.id]!!.creatorNickname)
|
assertEquals("cheer-detail-active", detailById[activeCreator.id]!!.creatorNickname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최근 응원 크리에이터 상세는 회원과 크리에이터의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromCheerCreatorDetails() {
|
||||||
|
val viewer = saveMember("blocked-cheer-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-cheer", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-cheer", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-cheer", MemberRole.CREATOR)
|
||||||
|
saveBlock(viewer, viewerBlockedCreator)
|
||||||
|
saveBlock(creatorBlockedViewer, viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val details = repository.findCheerCreatorRecommendationDetails(
|
||||||
|
listOf(viewerBlockedCreator.id!!, creatorBlockedViewer.id!!, visibleCreator.id!!),
|
||||||
|
memberId = viewer.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(visibleCreator.id), details.map { it.creatorId })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("최근 응원 크리에이터 상세는 빈 id 목록이면 빈 배열을 반환한다")
|
@DisplayName("최근 응원 크리에이터 상세는 빈 id 목록이면 빈 배열을 반환한다")
|
||||||
fun shouldReturnEmptyCheerCreatorRecommendationDetailsWhenIdsAreEmpty() {
|
fun shouldReturnEmptyCheerCreatorRecommendationDetailsWhenIdsAreEmpty() {
|
||||||
@@ -1114,6 +1287,29 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals(1L, detailById[adult.id]!!.likeCount)
|
assertEquals(1L, detailById[adult.id]!!.likeCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("인기 커뮤니티 상세는 회원과 크리에이터의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromPopularCommunityDetails() {
|
||||||
|
val viewer = saveMember("blocked-community-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-community", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-community", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-community", MemberRole.CREATOR)
|
||||||
|
val viewerBlockedPost = saveCommunity(viewerBlockedCreator, isCommentAvailable = true)
|
||||||
|
val creatorBlockedPost = saveCommunity(creatorBlockedViewer, isCommentAvailable = true)
|
||||||
|
val visiblePost = saveCommunity(visibleCreator, isCommentAvailable = true)
|
||||||
|
saveBlock(viewer, viewerBlockedCreator)
|
||||||
|
saveBlock(creatorBlockedViewer, viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val details = repository.findPopularCommunityRecommendationDetails(
|
||||||
|
listOf(viewerBlockedPost.id!!, creatorBlockedPost.id!!, visiblePost.id!!),
|
||||||
|
memberId = viewer.id,
|
||||||
|
includeAdultCommunities = false
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(visiblePost.id), details.map { it.communityId })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("인기 커뮤니티 상세는 빈 id 목록이면 빈 배열을 반환한다")
|
@DisplayName("인기 커뮤니티 상세는 빈 id 목록이면 빈 배열을 반환한다")
|
||||||
fun shouldReturnEmptyPopularCommunityRecommendationDetailsWhenIdsAreEmpty() {
|
fun shouldReturnEmptyPopularCommunityRecommendationDetailsWhenIdsAreEmpty() {
|
||||||
@@ -1176,6 +1372,32 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals(true, recommendations.flatMap { it.creators }.any { it.creatorId == fallbackCreator.id })
|
assertEquals(true, recommendations.flatMap { it.creators }.any { it.creatorId == fallbackCreator.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("장르 기반 크리에이터 추천은 회원과 크리에이터의 양방향 차단 관계를 제외한다")
|
||||||
|
fun shouldExcludeBidirectionalBlockedCreatorsFromGenreCreatorRecommendations() {
|
||||||
|
val viewer = saveMember("blocked-genre-viewer", MemberRole.USER)
|
||||||
|
val viewerBlockedCreator = saveMember("viewer-blocked-creator", MemberRole.CREATOR)
|
||||||
|
val creatorBlockedViewer = saveMember("creator-blocked-viewer", MemberRole.CREATOR)
|
||||||
|
val visibleCreator = saveMember("visible-block-creator", MemberRole.CREATOR)
|
||||||
|
val theme = saveTheme("blocked-genre-theme")
|
||||||
|
saveAudioContent(viewerBlockedCreator, LocalDateTime.of(2026, 5, 30, 10, 0), isActive = true, theme = theme)
|
||||||
|
saveAudioContent(creatorBlockedViewer, LocalDateTime.of(2026, 5, 30, 11, 0), isActive = true, theme = theme)
|
||||||
|
saveAudioContent(visibleCreator, LocalDateTime.of(2026, 5, 30, 12, 0), isActive = true, theme = theme)
|
||||||
|
saveBlock(member = viewer, blockedMember = viewerBlockedCreator)
|
||||||
|
saveBlock(member = creatorBlockedViewer, blockedMember = viewer)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val recommendations = repository.findGenreCreatorRecommendations(
|
||||||
|
memberId = viewer.id!!,
|
||||||
|
includeAdultGenres = false,
|
||||||
|
genreLimit = 1,
|
||||||
|
creatorLimit = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(theme.id), recommendations.map { it.genreId })
|
||||||
|
assertEquals(listOf(visibleCreator.id), recommendations.single().creators.map { it.creatorId })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("장르 기반 크리에이터 추천은 series_genre가 아니라 content_theme 기준으로 그룹을 만든다")
|
@DisplayName("장르 기반 크리에이터 추천은 series_genre가 아니라 content_theme 기준으로 그룹을 만든다")
|
||||||
fun shouldGroupGenreCreatorRecommendationsByContentThemeNotSeriesGenre() {
|
fun shouldGroupGenreCreatorRecommendationsByContentThemeNotSeriesGenre() {
|
||||||
@@ -1621,6 +1843,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
return following
|
return following
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveBlock(member: Member, blockedMember: Member): BlockMember {
|
||||||
|
val blockMember = BlockMember(isActive = true)
|
||||||
|
blockMember.member = member
|
||||||
|
blockMember.blockedMember = blockedMember
|
||||||
|
entityManager.persist(blockMember)
|
||||||
|
return blockMember
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateCreatedAt(entityName: String, id: Long, createdAt: LocalDateTime) {
|
private fun updateCreatedAt(entityName: String, id: Long, createdAt: LocalDateTime) {
|
||||||
entityManager.createQuery("update $entityName e set e.createdAt = :createdAt where e.id = :id")
|
entityManager.createQuery("update $entityName e set e.createdAt = :createdAt where e.id = :id")
|
||||||
.setParameter("createdAt", createdAt)
|
.setParameter("createdAt", createdAt)
|
||||||
|
|||||||
@@ -67,27 +67,30 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("홈 라이브 추천은 기본 20개를 최신순 조회 포트에 위임한다")
|
@DisplayName("홈 라이브 추천은 기본 20개를 최신순 조회 포트에 위임한다")
|
||||||
fun shouldFindLatestLiveRecommendationsWithDefaultLimit() {
|
fun shouldFindLatestLiveRecommendationsWithDefaultLimit() {
|
||||||
val recommendations = service.findLiveRecommendations()
|
val recommendations = service.findLiveRecommendations(memberId = 100L)
|
||||||
|
|
||||||
assertEquals(20, port.liveLimit)
|
assertEquals(20, port.liveLimit)
|
||||||
|
assertEquals(100L, port.liveMemberId)
|
||||||
assertEquals(port.liveRecommendations, recommendations)
|
assertEquals(port.liveRecommendations, recommendations)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("홈 배너 추천은 기본 20개를 활성 배너 조회 포트에 위임한다")
|
@DisplayName("홈 배너 추천은 기본 20개를 활성 배너 조회 포트에 위임한다")
|
||||||
fun shouldFindHomeBannersWithDefaultLimit() {
|
fun shouldFindHomeBannersWithDefaultLimit() {
|
||||||
val banners = service.findHomeBanners()
|
val banners = service.findHomeBanners(memberId = 100L)
|
||||||
|
|
||||||
assertEquals(20, port.bannerLimit)
|
assertEquals(20, port.bannerLimit)
|
||||||
|
assertEquals(100L, port.bannerMemberId)
|
||||||
assertEquals(port.banners, banners)
|
assertEquals(port.banners, banners)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("최근 활동 크리에이터는 기본 10개를 최신 활동 조회 포트에 위임한다")
|
@DisplayName("최근 활동 크리에이터는 기본 10개를 최신 활동 조회 포트에 위임한다")
|
||||||
fun shouldFindRecentlyActiveCreatorsWithDefaultLimit() {
|
fun shouldFindRecentlyActiveCreatorsWithDefaultLimit() {
|
||||||
val creators = service.findRecentlyActiveCreators()
|
val creators = service.findRecentlyActiveCreators(memberId = 100L)
|
||||||
|
|
||||||
assertEquals(10, port.activeCreatorLimit)
|
assertEquals(10, port.activeCreatorLimit)
|
||||||
|
assertEquals(100L, port.activeCreatorMemberId)
|
||||||
assertEquals(false, port.activeCreatorIncludeAdultActivities)
|
assertEquals(false, port.activeCreatorIncludeAdultActivities)
|
||||||
assertEquals(port.activeCreators, creators)
|
assertEquals(port.activeCreators, creators)
|
||||||
}
|
}
|
||||||
@@ -95,9 +98,10 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("최근 활동 크리에이터는 성인 활동 노출 여부를 조회 포트에 위임한다")
|
@DisplayName("최근 활동 크리에이터는 성인 활동 노출 여부를 조회 포트에 위임한다")
|
||||||
fun shouldFindRecentlyActiveCreatorsWithAdultVisibilityPolicy() {
|
fun shouldFindRecentlyActiveCreatorsWithAdultVisibilityPolicy() {
|
||||||
val creators = service.findRecentlyActiveCreators(limit = 8, includeAdultActivities = true)
|
val creators = service.findRecentlyActiveCreators(limit = 8, memberId = 101L, includeAdultActivities = true)
|
||||||
|
|
||||||
assertEquals(8, port.activeCreatorLimit)
|
assertEquals(8, port.activeCreatorLimit)
|
||||||
|
assertEquals(101L, port.activeCreatorMemberId)
|
||||||
assertEquals(true, port.activeCreatorIncludeAdultActivities)
|
assertEquals(true, port.activeCreatorIncludeAdultActivities)
|
||||||
assertEquals(port.activeCreators, creators)
|
assertEquals(port.activeCreators, creators)
|
||||||
}
|
}
|
||||||
@@ -107,9 +111,10 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
fun shouldFindRecentDebutCreatorsWithDefaultLimitAndNow() {
|
fun shouldFindRecentDebutCreatorsWithDefaultLimitAndNow() {
|
||||||
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
|
|
||||||
val creators = service.findRecentDebutCreators(now)
|
val creators = service.findRecentDebutCreators(now, memberId = 102L)
|
||||||
|
|
||||||
assertEquals(now, port.recentDebutNow)
|
assertEquals(now, port.recentDebutNow)
|
||||||
|
assertEquals(102L, port.recentDebutMemberId)
|
||||||
assertEquals(10, port.recentDebutLimit)
|
assertEquals(10, port.recentDebutLimit)
|
||||||
assertEquals(port.recentDebutCreators, creators)
|
assertEquals(port.recentDebutCreators, creators)
|
||||||
}
|
}
|
||||||
@@ -119,9 +124,10 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
fun shouldFindFirstAudioContentsWithDefaultLimitAndNow() {
|
fun shouldFindFirstAudioContentsWithDefaultLimitAndNow() {
|
||||||
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
|
|
||||||
val contents = service.findFirstAudioContents(now)
|
val contents = service.findFirstAudioContents(now, memberId = 103L)
|
||||||
|
|
||||||
assertEquals(now, port.firstAudioNow)
|
assertEquals(now, port.firstAudioNow)
|
||||||
|
assertEquals(103L, port.firstAudioMemberId)
|
||||||
assertEquals(10, port.firstAudioLimit)
|
assertEquals(10, port.firstAudioLimit)
|
||||||
assertEquals(port.firstAudioContents, contents)
|
assertEquals(port.firstAudioContents, contents)
|
||||||
}
|
}
|
||||||
@@ -192,9 +198,10 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val creators = service.findCheerCreatorRecommendations()
|
val creators = service.findCheerCreatorRecommendations(memberId = 104L)
|
||||||
|
|
||||||
assertEquals((1L..8L).toList(), port.cheerCreatorDetailIds)
|
assertEquals((1L..9L).toList(), port.cheerCreatorDetailIds)
|
||||||
|
assertEquals(104L, port.cheerCreatorMemberId)
|
||||||
assertEquals(listOf(1L, 2L), creators.map { it.creatorId })
|
assertEquals(listOf(1L, 2L), creators.map { it.creatorId })
|
||||||
assertEquals(listOf("creator-1", "creator-2"), creators.map { it.creatorNickname })
|
assertEquals(listOf("creator-1", "creator-2"), creators.map { it.creatorNickname })
|
||||||
}
|
}
|
||||||
@@ -243,9 +250,10 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val communities = service.findPopularCommunityRecommendations(includeAdultCommunities = true)
|
val communities = service.findPopularCommunityRecommendations(memberId = 105L, includeAdultCommunities = true)
|
||||||
|
|
||||||
assertEquals((1L..11L).toList(), port.popularCommunityDetailIds)
|
assertEquals((1L..11L).toList(), port.popularCommunityDetailIds)
|
||||||
|
assertEquals(105L, port.popularCommunityMemberId)
|
||||||
assertEquals(true, port.popularCommunityIncludeAdultCommunities)
|
assertEquals(true, port.popularCommunityIncludeAdultCommunities)
|
||||||
assertEquals(listOf(1L, 3L), communities.map { it.communityId })
|
assertEquals(listOf(1L, 3L), communities.map { it.communityId })
|
||||||
assertEquals(listOf(10L, 11L), communities.map { it.creatorId })
|
assertEquals(listOf(10L, 11L), communities.map { it.creatorId })
|
||||||
@@ -401,17 +409,22 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
|
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
|
||||||
var liveLimit: Int? = null
|
var liveLimit: Int? = null
|
||||||
var liveOffset: Int? = null
|
var liveOffset: Int? = null
|
||||||
|
var liveMemberId: Long? = null
|
||||||
var liveIncludeAdultLives: Boolean? = null
|
var liveIncludeAdultLives: Boolean? = null
|
||||||
var bannerLimit: Int? = null
|
var bannerLimit: Int? = null
|
||||||
|
var bannerMemberId: Long? = null
|
||||||
var activeCreatorLimit: Int? = null
|
var activeCreatorLimit: Int? = null
|
||||||
|
var activeCreatorMemberId: Long? = null
|
||||||
var activeCreatorIncludeAdultActivities: Boolean? = null
|
var activeCreatorIncludeAdultActivities: Boolean? = null
|
||||||
var recentDebutNow: LocalDateTime? = null
|
var recentDebutNow: LocalDateTime? = null
|
||||||
var recentDebutLimit: Int? = null
|
var recentDebutLimit: Int? = null
|
||||||
var recentDebutOffset: Int? = null
|
var recentDebutOffset: Int? = null
|
||||||
|
var recentDebutMemberId: Long? = null
|
||||||
var recentDebutIncludeAdultContents: Boolean? = null
|
var recentDebutIncludeAdultContents: Boolean? = null
|
||||||
var firstAudioNow: LocalDateTime? = null
|
var firstAudioNow: LocalDateTime? = null
|
||||||
var firstAudioLimit: Int? = null
|
var firstAudioLimit: Int? = null
|
||||||
var firstAudioOffset: Int? = null
|
var firstAudioOffset: Int? = null
|
||||||
|
var firstAudioMemberId: Long? = null
|
||||||
var firstAudioIncludeAdultContents: Boolean? = null
|
var firstAudioIncludeAdultContents: Boolean? = null
|
||||||
var aiCharacterDetailIds: List<Long> = emptyList()
|
var aiCharacterDetailIds: List<Long> = emptyList()
|
||||||
var cheerCreatorDetailIds: List<Long> = emptyList()
|
var cheerCreatorDetailIds: List<Long> = emptyList()
|
||||||
@@ -481,30 +494,37 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
)
|
)
|
||||||
var aiCharacterDetails: List<HomeAiCharacterRecommendationRecord> = emptyList()
|
var aiCharacterDetails: List<HomeAiCharacterRecommendationRecord> = emptyList()
|
||||||
var cheerCreatorDetails: List<HomeCheerCreatorRecommendationRecord> = emptyList()
|
var cheerCreatorDetails: List<HomeCheerCreatorRecommendationRecord> = emptyList()
|
||||||
|
var cheerCreatorMemberId: Long? = null
|
||||||
var popularCommunityDetails: List<HomePopularCommunityRecommendationRecord> = emptyList()
|
var popularCommunityDetails: List<HomePopularCommunityRecommendationRecord> = emptyList()
|
||||||
|
var popularCommunityMemberId: Long? = null
|
||||||
var genreCreatorRecommendations: List<HomeGenreCreatorRecommendationGroup> = emptyList()
|
var genreCreatorRecommendations: List<HomeGenreCreatorRecommendationGroup> = emptyList()
|
||||||
|
|
||||||
override fun findLiveRecommendations(
|
override fun findLiveRecommendations(
|
||||||
offset: Int,
|
offset: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultLives: Boolean
|
includeAdultLives: Boolean
|
||||||
): List<HomeLiveRecommendationRecord> {
|
): List<HomeLiveRecommendationRecord> {
|
||||||
liveOffset = offset
|
liveOffset = offset
|
||||||
liveLimit = limit
|
liveLimit = limit
|
||||||
|
liveMemberId = memberId
|
||||||
liveIncludeAdultLives = includeAdultLives
|
liveIncludeAdultLives = includeAdultLives
|
||||||
return liveRecommendations
|
return liveRecommendations
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord> {
|
override fun findHomeBanners(limit: Int, memberId: Long?): List<HomeBannerRecommendationRecord> {
|
||||||
bannerLimit = limit
|
bannerLimit = limit
|
||||||
|
bannerMemberId = memberId
|
||||||
return banners
|
return banners
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findRecentlyActiveCreators(
|
override fun findRecentlyActiveCreators(
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultActivities: Boolean
|
includeAdultActivities: Boolean
|
||||||
): List<RecentlyActiveCreatorRecord> {
|
): List<RecentlyActiveCreatorRecord> {
|
||||||
activeCreatorLimit = limit
|
activeCreatorLimit = limit
|
||||||
|
activeCreatorMemberId = memberId
|
||||||
activeCreatorIncludeAdultActivities = includeAdultActivities
|
activeCreatorIncludeAdultActivities = includeAdultActivities
|
||||||
return activeCreators
|
return activeCreators
|
||||||
}
|
}
|
||||||
@@ -513,11 +533,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultContents: Boolean
|
includeAdultContents: Boolean
|
||||||
): List<RecentDebutCreatorRecord> {
|
): List<RecentDebutCreatorRecord> {
|
||||||
recentDebutNow = now
|
recentDebutNow = now
|
||||||
recentDebutOffset = offset
|
recentDebutOffset = offset
|
||||||
recentDebutLimit = limit
|
recentDebutLimit = limit
|
||||||
|
recentDebutMemberId = memberId
|
||||||
recentDebutIncludeAdultContents = includeAdultContents
|
recentDebutIncludeAdultContents = includeAdultContents
|
||||||
return recentDebutCreators
|
return recentDebutCreators
|
||||||
}
|
}
|
||||||
@@ -526,11 +548,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultContents: Boolean
|
includeAdultContents: Boolean
|
||||||
): List<HomeFirstAudioContentRecord> {
|
): List<HomeFirstAudioContentRecord> {
|
||||||
firstAudioNow = now
|
firstAudioNow = now
|
||||||
firstAudioOffset = offset
|
firstAudioOffset = offset
|
||||||
firstAudioLimit = limit
|
firstAudioLimit = limit
|
||||||
|
firstAudioMemberId = memberId
|
||||||
firstAudioIncludeAdultContents = includeAdultContents
|
firstAudioIncludeAdultContents = includeAdultContents
|
||||||
return firstAudioContents
|
return firstAudioContents
|
||||||
}
|
}
|
||||||
@@ -558,16 +582,22 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
return aiCharacterDetails
|
return aiCharacterDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findCheerCreatorRecommendationDetails(creatorIds: List<Long>): List<HomeCheerCreatorRecommendationRecord> {
|
override fun findCheerCreatorRecommendationDetails(
|
||||||
|
creatorIds: List<Long>,
|
||||||
|
memberId: Long?
|
||||||
|
): List<HomeCheerCreatorRecommendationRecord> {
|
||||||
cheerCreatorDetailIds = creatorIds
|
cheerCreatorDetailIds = creatorIds
|
||||||
|
cheerCreatorMemberId = memberId
|
||||||
return cheerCreatorDetails
|
return cheerCreatorDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findPopularCommunityRecommendationDetails(
|
override fun findPopularCommunityRecommendationDetails(
|
||||||
communityIds: List<Long>,
|
communityIds: List<Long>,
|
||||||
|
memberId: Long?,
|
||||||
includeAdultCommunities: Boolean
|
includeAdultCommunities: Boolean
|
||||||
): List<HomePopularCommunityRecommendationRecord> {
|
): List<HomePopularCommunityRecommendationRecord> {
|
||||||
popularCommunityDetailIds = communityIds
|
popularCommunityDetailIds = communityIds
|
||||||
|
popularCommunityMemberId = memberId
|
||||||
popularCommunityIncludeAdultCommunities = includeAdultCommunities
|
popularCommunityIncludeAdultCommunities = includeAdultCommunities
|
||||||
return popularCommunityDetails
|
return popularCommunityDetails
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user