feat(home-recommendation): 라이브 입장 제한 필터를 적용한다

This commit is contained in:
2026-09-17 19:14:29 +09:00
parent 123b9b1a0e
commit 542bf81b58
10 changed files with 387 additions and 23 deletions
@@ -2,7 +2,9 @@ package kr.co.vividnext.sodalive.v2.api.home.application
import kr.co.vividnext.sodalive.event.EventItem
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.api.common.dto.RecommendationBannerResponse
import kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.HomeActiveCreatorItem
@@ -53,7 +55,9 @@ class HomeRecommendationFacade(
lives = queryService.findLiveRecommendations(
limit = HOME_LIVE_LIMIT,
memberId = member?.id,
includeAdultLives = includeAdult
includeAdultLives = includeAdult,
isViewerCreator = member?.role == MemberRole.CREATOR,
effectiveViewerGender = member?.effectiveGender()
).map { it.toItem() },
banners = queryService.findHomeBanners(
limit = HOME_BANNER_LIMIT,
@@ -125,7 +129,9 @@ class HomeRecommendationFacade(
offset = page.toOffset(size),
limit = size + 1,
memberId = member.id,
includeAdultLives = resolveAdultVisibility(member)
includeAdultLives = resolveAdultVisibility(member),
isViewerCreator = member.role == MemberRole.CREATOR,
effectiveViewerGender = member.effectiveGender()
)
fetched.toPage(page, size) { it.toItem() }
}.onSuccess {
@@ -209,6 +215,10 @@ class HomeRecommendationFacade(
return memberContentPreferenceService.canViewAdultContent(member)
}
private fun Member.effectiveGender(): Gender {
return auth?.let { if (it.gender == 1) Gender.MALE else Gender.FEMALE } ?: gender
}
private fun Int.toOffset(size: Int): Long = this.toLong() * size
private fun <S, T> List<S>.toPage(
@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.v2.api.home.live.application
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLivePageResponse
import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLiveResponse
@@ -24,7 +26,9 @@ class HomeOnAirLiveFacade(
offset = normalizedPage.toLong() * PAGE_SIZE,
limit = PAGE_SIZE + 1,
memberId = member.id,
includeAdultLives = memberContentPreferenceService.canViewAdultContent(member)
includeAdultLives = memberContentPreferenceService.canViewAdultContent(member),
isViewerCreator = member.role == MemberRole.CREATOR,
effectiveViewerGender = member.effectiveGender()
)
val items = fetched.take(PAGE_SIZE).map { it.toResponse() }
@@ -57,6 +61,10 @@ class HomeOnAirLiveFacade(
return atOffset(ZoneOffset.UTC).toInstant().toString()
}
private fun Member.effectiveGender(): Gender {
return auth?.let { if (it.gender == 1) Gender.MALE else Gender.FEMALE } ?: gender
}
companion object {
private const val PAGE_SIZE = 20
private const val MAX_PAGE = 10_000
@@ -22,7 +22,9 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommun
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember
@@ -58,7 +60,9 @@ class DefaultHomeRecommendationQueryRepository(
offset: Long,
limit: Int,
memberId: Long?,
includeAdultLives: Boolean
includeAdultLives: Boolean,
isViewerCreator: Boolean,
effectiveViewerGender: Gender?
): List<HomeLiveRecommendationRecord> {
return queryFactory
.select(
@@ -79,6 +83,8 @@ class DefaultHomeRecommendationQueryRepository(
liveRoom.channelName.isNotNull,
liveRoom.channelName.isNotEmpty,
includeAdultLiveCondition(includeAdultLives),
genderLiveCondition(memberId, effectiveViewerGender),
creatorJoinLiveCondition(memberId, isViewerCreator),
notBlockedCreatorCondition(memberId, member.id),
member.isActive.isTrue
)
@@ -1294,6 +1300,21 @@ class DefaultHomeRecommendationQueryRepository(
return if (includeAdultLives) null else liveRoom.isAdult.isFalse
}
private fun genderLiveCondition(viewerId: Long?, effectiveViewerGender: Gender?): BooleanExpression? {
if (effectiveViewerGender == null || effectiveViewerGender == Gender.NONE) return null
val genderCondition = when (effectiveViewerGender) {
Gender.MALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.MALE_ONLY)
Gender.FEMALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.FEMALE_ONLY)
Gender.NONE -> return null
}
return viewerId?.let { genderCondition.or(liveRoom.member.id.eq(it)) } ?: genderCondition
}
private fun creatorJoinLiveCondition(viewerId: Long?, isViewerCreator: Boolean): BooleanExpression? {
if (!isViewerCreator || viewerId == null) return null
return liveRoom.isAvailableJoinCreator.isTrue.or(liveRoom.member.id.eq(viewerId))
}
private fun excludeAiCharacterCondition(excludeCharacterIds: List<Long>): BooleanExpression? {
return if (excludeCharacterIds.isEmpty()) null else chatCharacter.id.notIn(excludeCharacterIds)
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.recommendation.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicy
@@ -41,9 +42,18 @@ class HomeRecommendationQueryService(
offset: Long = 0,
limit: Int = DEFAULT_LIVE_LIMIT,
memberId: Long? = null,
includeAdultLives: Boolean = false
includeAdultLives: Boolean = false,
isViewerCreator: Boolean = false,
effectiveViewerGender: Gender? = null
): List<HomeLiveRecommendationRecord> {
return queryPort.findLiveRecommendations(offset, limit, memberId, includeAdultLives)
return queryPort.findLiveRecommendations(
offset,
limit,
memberId,
includeAdultLives,
isViewerCreator,
effectiveViewerGender
)
}
fun findHomeBanners(
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.recommendation.port.out
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import java.time.LocalDateTime
@@ -9,7 +10,9 @@ interface HomeRecommendationQueryPort {
offset: Long = 0,
limit: Int,
memberId: Long? = null,
includeAdultLives: Boolean = false
includeAdultLives: Boolean = false,
isViewerCreator: Boolean = false,
effectiveViewerGender: Gender? = null
): List<HomeLiveRecommendationRecord>
fun findHomeBanners(