Test #451
+12
-2
@@ -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(
|
||||
|
||||
+9
-1
@@ -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
-1
@@ -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)
|
||||
}
|
||||
|
||||
+12
-2
@@ -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(
|
||||
|
||||
+4
-1
@@ -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(
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.api.home
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
import kr.co.vividnext.sodalive.member.MemberKind
|
||||
@@ -372,7 +373,9 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
offset = 0,
|
||||
limit = 21,
|
||||
memberId = member.id,
|
||||
includeAdultLives = false
|
||||
includeAdultLives = false,
|
||||
isViewerCreator = false,
|
||||
effectiveViewerGender = Gender.NONE
|
||||
)
|
||||
)
|
||||
.thenThrow(IllegalStateException("page query failed"))
|
||||
|
||||
+49
-2
@@ -2,7 +2,10 @@ package kr.co.vividnext.sodalive.v2.api.home.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.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||
@@ -86,6 +89,42 @@ class HomeRecommendationFacadeTest {
|
||||
assertTrue(queryPort.bannerLang == Lang.KO)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("홈 추천은 크리에이터 여부와 인증 성별 우선 유효 성별을 라이브 조회에 전달한다")
|
||||
fun shouldPassCreatorRoleAndAuthGenderToHomeLiveQuery() {
|
||||
val member = createMember(id = 12L, role = MemberRole.CREATOR, gender = Gender.FEMALE, authGender = 1)
|
||||
|
||||
facade.getHomeRecommendations(member)
|
||||
|
||||
assertTrue(queryPort.liveIsViewerCreator == true)
|
||||
assertTrue(queryPort.liveEffectiveViewerGender == Gender.MALE)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("홈 라이브 목록은 크리에이터 여부와 회원 성별 fallback을 라이브 조회에 전달한다")
|
||||
fun shouldPassCreatorRoleAndMemberGenderToLivePageQuery() {
|
||||
val member = createMember(id = 13L, role = MemberRole.CREATOR, gender = Gender.FEMALE)
|
||||
|
||||
facade.getLives(member, page = 0, size = 20)
|
||||
|
||||
assertTrue(queryPort.liveIsViewerCreator == true)
|
||||
assertTrue(queryPort.liveEffectiveViewerGender == Gender.FEMALE)
|
||||
}
|
||||
|
||||
private fun createMember(
|
||||
id: Long,
|
||||
role: MemberRole = MemberRole.USER,
|
||||
gender: Gender = Gender.NONE,
|
||||
authGender: Int? = null
|
||||
): Member {
|
||||
return Member(password = "password", nickname = "member$id", role = role, gender = gender).apply {
|
||||
this.id = id
|
||||
authGender?.let {
|
||||
Auth(name = "name", birth = "19900101", uniqueCi = "ci$id", di = "di$id", gender = it).member = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstAudio(price: Int, isPointAvailable: Boolean): HomeFirstAudioContentRecord {
|
||||
return HomeFirstAudioContentRecord(
|
||||
contentId = price.toLong() + 1,
|
||||
@@ -105,13 +144,21 @@ class HomeRecommendationFacadeTest {
|
||||
var firstAudioContents: List<HomeFirstAudioContentRecord> = emptyList()
|
||||
var bannerIncludeAdultBanners: Boolean = false
|
||||
var bannerLang: Lang? = null
|
||||
var liveIsViewerCreator: Boolean? = null
|
||||
var liveEffectiveViewerGender: Gender? = null
|
||||
|
||||
override fun findLiveRecommendations(
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultLives: Boolean
|
||||
): List<HomeLiveRecommendationRecord> = emptyList()
|
||||
includeAdultLives: Boolean,
|
||||
isViewerCreator: Boolean,
|
||||
effectiveViewerGender: Gender?
|
||||
): List<HomeLiveRecommendationRecord> {
|
||||
liveIsViewerCreator = isViewerCreator
|
||||
liveEffectiveViewerGender = effectiveViewerGender
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun findHomeBanners(
|
||||
limit: Int,
|
||||
|
||||
+64
-7
@@ -1,7 +1,9 @@
|
||||
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.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeLiveRecommendationRecord
|
||||
@@ -25,7 +27,9 @@ class HomeOnAirLiveFacadeTest {
|
||||
eqValue(0L),
|
||||
eqValue(21),
|
||||
eqValue(member.id),
|
||||
eqValue(true)
|
||||
eqValue(true),
|
||||
eqValue(false),
|
||||
eqValue(Gender.NONE)
|
||||
)
|
||||
|
||||
val response = facade.getOnAirLives(member, page = 0)
|
||||
@@ -34,7 +38,14 @@ class HomeOnAirLiveFacadeTest {
|
||||
assertEquals(20, response.size)
|
||||
assertEquals(true, response.hasNext)
|
||||
assertEquals(20, response.items.size)
|
||||
Mockito.verify(queryService).findLiveRecommendations(eqValue(0L), eqValue(21), eqValue(member.id), eqValue(true))
|
||||
Mockito.verify(queryService).findLiveRecommendations(
|
||||
eqValue(0L),
|
||||
eqValue(21),
|
||||
eqValue(member.id),
|
||||
eqValue(true),
|
||||
eqValue(false),
|
||||
eqValue(Gender.NONE)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -46,7 +57,9 @@ class HomeOnAirLiveFacadeTest {
|
||||
eqValue(0L),
|
||||
eqValue(21),
|
||||
eqValue(member.id),
|
||||
eqValue(false)
|
||||
eqValue(false),
|
||||
eqValue(false),
|
||||
eqValue(Gender.NONE)
|
||||
)
|
||||
|
||||
val response = facade.getOnAirLives(member, page = 0)
|
||||
@@ -60,13 +73,46 @@ class HomeOnAirLiveFacadeTest {
|
||||
val member = createMember(100L)
|
||||
Mockito.doReturn(false).`when`(preferenceService).canViewAdultContent(member)
|
||||
Mockito.doReturn(listOf(record(1L, beginDateTime = LocalDateTime.of(2026, 6, 26, 12, 30)))).`when`(queryService)
|
||||
.findLiveRecommendations(eqValue(0L), eqValue(21), eqValue(member.id), eqValue(false))
|
||||
.findLiveRecommendations(
|
||||
eqValue(0L),
|
||||
eqValue(21),
|
||||
eqValue(member.id),
|
||||
eqValue(false),
|
||||
eqValue(false),
|
||||
eqValue(Gender.NONE)
|
||||
)
|
||||
|
||||
val response = facade.getOnAirLives(member, page = 0)
|
||||
|
||||
assertEquals("2026-06-26T12:30:00Z", response.items.single().beginDateTimeUtc)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("현재 진행 중인 라이브 facade는 크리에이터 여부와 인증 성별 우선 유효 성별을 전달한다")
|
||||
fun shouldPassCreatorRoleAndAuthGenderToLiveRecommendationQuery() {
|
||||
val member = createMember(id = 100L, role = MemberRole.CREATOR, gender = Gender.FEMALE, authGender = 1)
|
||||
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
||||
Mockito.doReturn(emptyList<HomeLiveRecommendationRecord>()).`when`(queryService).findLiveRecommendations(
|
||||
eqValue(0L),
|
||||
eqValue(21),
|
||||
eqValue(member.id),
|
||||
eqValue(true),
|
||||
eqValue(true),
|
||||
eqValue(Gender.MALE)
|
||||
)
|
||||
|
||||
facade.getOnAirLives(member, page = 0)
|
||||
|
||||
Mockito.verify(queryService).findLiveRecommendations(
|
||||
eqValue(0L),
|
||||
eqValue(21),
|
||||
eqValue(member.id),
|
||||
eqValue(true),
|
||||
eqValue(true),
|
||||
eqValue(Gender.MALE)
|
||||
)
|
||||
}
|
||||
|
||||
private fun record(
|
||||
id: Long,
|
||||
creatorProfileImage: String? = "profile.png",
|
||||
@@ -80,13 +126,24 @@ class HomeOnAirLiveFacadeTest {
|
||||
beginDateTime = beginDateTime
|
||||
)
|
||||
|
||||
private fun createMember(id: Long): Member {
|
||||
private fun createMember(
|
||||
id: Long,
|
||||
role: MemberRole = MemberRole.USER,
|
||||
gender: Gender = Gender.NONE,
|
||||
authGender: Int? = null
|
||||
): Member {
|
||||
return Member(
|
||||
email = "viewer$id@test.com",
|
||||
password = "password",
|
||||
nickname = "viewer$id",
|
||||
role = MemberRole.USER
|
||||
).apply { this.id = id }
|
||||
role = role,
|
||||
gender = gender
|
||||
).apply {
|
||||
this.id = id
|
||||
authGender?.let {
|
||||
Auth(name = "name", birth = "19900101", uniqueCi = "ci$id", di = "di$id", gender = it).member = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> eqValue(value: T): T {
|
||||
|
||||
+196
-2
@@ -32,7 +32,9 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommuni
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberKind
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
@@ -194,6 +196,194 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
assertEquals(listOf(visibleLive.id), lives.map { it.liveRoomId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 추천은 다른 크리에이터에게 크리에이터 입장 제한 라이브를 제외하고 방 생성자는 유지한다")
|
||||
fun shouldExcludeCreatorJoinRestrictedLiveForOtherCreatorButKeepRoomOwner() {
|
||||
val baseAt = LocalDateTime.of(2026, 9, 17, 10, 0)
|
||||
val viewer = saveMember("creator-viewer", MemberRole.CREATOR)
|
||||
val otherCreator = saveMember("restricted-live-creator", MemberRole.CREATOR)
|
||||
val availableLive = saveLiveRoom(otherCreator, baseAt, channelName = "available-live")
|
||||
saveLiveRoom(
|
||||
otherCreator,
|
||||
baseAt.plusMinutes(1),
|
||||
channelName = "creator-restricted-live",
|
||||
isAvailableJoinCreator = false
|
||||
)
|
||||
val ownerLive = saveLiveRoom(
|
||||
viewer,
|
||||
baseAt.plusMinutes(2),
|
||||
channelName = "owner-restricted-live",
|
||||
isAvailableJoinCreator = false
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val lives = repository.findLiveRecommendations(
|
||||
limit = 10,
|
||||
memberId = viewer.id,
|
||||
isViewerCreator = true,
|
||||
effectiveViewerGender = Gender.NONE
|
||||
)
|
||||
|
||||
assertEquals(listOf(ownerLive.id, availableLive.id), lives.map { it.liveRoomId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 추천은 일반 사용자에게 크리에이터 입장 제한 라이브를 노출한다")
|
||||
fun shouldKeepCreatorJoinRestrictedLiveVisibleForUserViewer() {
|
||||
val viewer = saveMember("user-viewer", MemberRole.USER)
|
||||
val creator = saveMember("creator-restricted-live-creator", MemberRole.CREATOR)
|
||||
val restrictedLive = saveLiveRoom(
|
||||
creator,
|
||||
LocalDateTime.of(2026, 9, 17, 10, 0),
|
||||
channelName = "creator-restricted-live",
|
||||
isAvailableJoinCreator = false
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val lives = repository.findLiveRecommendations(
|
||||
limit = 10,
|
||||
memberId = viewer.id,
|
||||
isViewerCreator = false,
|
||||
effectiveViewerGender = Gender.NONE
|
||||
)
|
||||
|
||||
assertEquals(listOf(restrictedLive.id), lives.map { it.liveRoomId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 추천은 남성에게 남성 전용과 전체 라이브를 노출하고 방 생성자는 성별 제한에서 예외 처리한다")
|
||||
fun shouldFilterFemaleOnlyLiveForMaleViewerButKeepOwnerLive() {
|
||||
val baseAt = LocalDateTime.of(2026, 9, 17, 10, 0)
|
||||
val viewer = saveMember("male-viewer", MemberRole.CREATOR)
|
||||
val creator = saveMember("gender-live-creator", MemberRole.CREATOR)
|
||||
val maleOnlyLive = saveLiveRoom(
|
||||
creator,
|
||||
baseAt,
|
||||
channelName = "male-only-live",
|
||||
genderRestriction = GenderRestriction.MALE_ONLY
|
||||
)
|
||||
val allLive = saveLiveRoom(creator, baseAt.plusMinutes(1), channelName = "all-live")
|
||||
saveLiveRoom(
|
||||
creator,
|
||||
baseAt.plusMinutes(2),
|
||||
channelName = "female-only-live",
|
||||
genderRestriction = GenderRestriction.FEMALE_ONLY
|
||||
)
|
||||
val ownerLive = saveLiveRoom(
|
||||
viewer,
|
||||
baseAt.plusMinutes(3),
|
||||
channelName = "owner-female-only-live",
|
||||
genderRestriction = GenderRestriction.FEMALE_ONLY
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val lives = repository.findLiveRecommendations(
|
||||
limit = 10,
|
||||
memberId = viewer.id,
|
||||
isViewerCreator = true,
|
||||
effectiveViewerGender = Gender.MALE
|
||||
)
|
||||
|
||||
assertEquals(listOf(ownerLive.id, allLive.id, maleOnlyLive.id), lives.map { it.liveRoomId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 추천은 여성 방 생성자에게 남성 전용인 자신의 라이브를 노출한다")
|
||||
fun shouldKeepMaleOnlyLiveVisibleForFemaleRoomOwner() {
|
||||
val baseAt = LocalDateTime.of(2026, 9, 17, 10, 0)
|
||||
val viewer = saveMember("female-owner", MemberRole.CREATOR)
|
||||
val creator = saveMember("female-owner-filter-creator", MemberRole.CREATOR)
|
||||
val femaleOnlyLive = saveLiveRoom(
|
||||
creator,
|
||||
baseAt.plusMinutes(1),
|
||||
channelName = "female-only-live",
|
||||
genderRestriction = GenderRestriction.FEMALE_ONLY
|
||||
)
|
||||
val ownerLive = saveLiveRoom(
|
||||
viewer,
|
||||
baseAt.plusMinutes(2),
|
||||
channelName = "owner-male-only-live",
|
||||
genderRestriction = GenderRestriction.MALE_ONLY
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val lives = repository.findLiveRecommendations(
|
||||
limit = 10,
|
||||
memberId = viewer.id,
|
||||
isViewerCreator = true,
|
||||
effectiveViewerGender = Gender.FEMALE
|
||||
)
|
||||
|
||||
assertEquals(listOf(ownerLive.id, femaleOnlyLive.id), lives.map { it.liveRoomId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 추천은 여성에게 여성 전용과 전체 라이브만 노출한다")
|
||||
fun shouldFilterMaleOnlyLiveForFemaleViewer() {
|
||||
val baseAt = LocalDateTime.of(2026, 9, 17, 10, 0)
|
||||
val viewer = saveMember("female-viewer", MemberRole.USER)
|
||||
val creator = saveMember("female-filter-creator", MemberRole.CREATOR)
|
||||
val femaleOnlyLive = saveLiveRoom(
|
||||
creator,
|
||||
baseAt.plusMinutes(2),
|
||||
channelName = "female-only-live",
|
||||
genderRestriction = GenderRestriction.FEMALE_ONLY
|
||||
)
|
||||
val allLive = saveLiveRoom(creator, baseAt.plusMinutes(1), channelName = "all-live")
|
||||
saveLiveRoom(
|
||||
creator,
|
||||
baseAt,
|
||||
channelName = "male-only-live",
|
||||
genderRestriction = GenderRestriction.MALE_ONLY
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val lives = repository.findLiveRecommendations(
|
||||
limit = 10,
|
||||
memberId = viewer.id,
|
||||
effectiveViewerGender = Gender.FEMALE
|
||||
)
|
||||
|
||||
assertEquals(listOf(femaleOnlyLive.id, allLive.id), lives.map { it.liveRoomId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 추천은 성별 미설정 또는 유효 성별이 없으면 성별 제한을 적용하지 않는다")
|
||||
fun shouldNotApplyGenderRestrictionForNoneOrNullGender() {
|
||||
val baseAt = LocalDateTime.of(2026, 9, 17, 10, 0)
|
||||
val viewer = saveMember("unrestricted-viewer", MemberRole.USER)
|
||||
val creator = saveMember("unrestricted-creator", MemberRole.CREATOR)
|
||||
val maleOnlyLive = saveLiveRoom(
|
||||
creator,
|
||||
baseAt,
|
||||
channelName = "male-only-live",
|
||||
genderRestriction = GenderRestriction.MALE_ONLY
|
||||
)
|
||||
val femaleOnlyLive = saveLiveRoom(
|
||||
creator,
|
||||
baseAt.plusMinutes(1),
|
||||
channelName = "female-only-live",
|
||||
genderRestriction = GenderRestriction.FEMALE_ONLY
|
||||
)
|
||||
val allLive = saveLiveRoom(creator, baseAt.plusMinutes(2), channelName = "all-live")
|
||||
flushAndClear()
|
||||
|
||||
val noneGenderLives = repository.findLiveRecommendations(
|
||||
limit = 10,
|
||||
memberId = viewer.id,
|
||||
effectiveViewerGender = Gender.NONE
|
||||
)
|
||||
val nullGenderLives = repository.findLiveRecommendations(
|
||||
limit = 10,
|
||||
memberId = viewer.id,
|
||||
effectiveViewerGender = null
|
||||
)
|
||||
|
||||
val expectedIds = listOf(allLive.id, femaleOnlyLive.id, maleOnlyLive.id)
|
||||
assertEquals(expectedIds, noneGenderLives.map { it.liveRoomId })
|
||||
assertEquals(expectedIds, nullGenderLives.map { it.liveRoomId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("홈 배너는 활성 배너를 orders 오름차순 최대 20개로 조회하고 동일 orders는 랜덤 tie-breaker를 적용한다")
|
||||
fun shouldFindActiveHomeBannersWithOrderLimitAndRandomTieBreaker() {
|
||||
@@ -2856,7 +3046,9 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
isAdult: Boolean = false,
|
||||
title: String = "live-${creator.nickname}-$beginDateTime",
|
||||
price: Int = 0,
|
||||
isActive: Boolean = true
|
||||
isActive: Boolean = true,
|
||||
isAvailableJoinCreator: Boolean = true,
|
||||
genderRestriction: GenderRestriction = GenderRestriction.ALL
|
||||
): LiveRoom {
|
||||
val room = LiveRoom(
|
||||
title = title,
|
||||
@@ -2864,7 +3056,9 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
beginDateTime = beginDateTime,
|
||||
numberOfPeople = 0,
|
||||
isAdult = isAdult,
|
||||
price = price
|
||||
price = price,
|
||||
isAvailableJoinCreator = isAvailableJoinCreator,
|
||||
genderRestriction = genderRestriction
|
||||
)
|
||||
room.member = creator
|
||||
room.channelName = channelName
|
||||
|
||||
+15
-4
@@ -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.CreatorCommunityDisplayContent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||
@@ -86,19 +87,23 @@ class HomeRecommendationQueryServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 추천 조회는 paging과 성인 노출 여부를 조회 포트에 그대로 위임한다")
|
||||
fun shouldDelegateLiveRecommendationQueryWithPagingAndAdultFlag() {
|
||||
@DisplayName("라이브 추천 조회는 paging, 성인 노출, 크리에이터 여부와 유효 성별을 조회 포트에 그대로 위임한다")
|
||||
fun shouldDelegateLiveRecommendationQueryWithViewerContext() {
|
||||
val recommendations = service.findLiveRecommendations(
|
||||
offset = 40,
|
||||
limit = 21,
|
||||
memberId = 100L,
|
||||
includeAdultLives = true
|
||||
includeAdultLives = true,
|
||||
isViewerCreator = true,
|
||||
effectiveViewerGender = Gender.MALE
|
||||
)
|
||||
|
||||
assertEquals(40, port.liveOffset)
|
||||
assertEquals(21, port.liveLimit)
|
||||
assertEquals(100L, port.liveMemberId)
|
||||
assertEquals(true, port.liveIncludeAdultLives)
|
||||
assertEquals(true, port.liveIsViewerCreator)
|
||||
assertEquals(Gender.MALE, port.liveEffectiveViewerGender)
|
||||
assertEquals(port.liveRecommendations, recommendations)
|
||||
}
|
||||
|
||||
@@ -1092,6 +1097,8 @@ class HomeRecommendationQueryServiceTest {
|
||||
var liveOffset: Long? = null
|
||||
var liveMemberId: Long? = null
|
||||
var liveIncludeAdultLives: Boolean? = null
|
||||
var liveIsViewerCreator: Boolean? = null
|
||||
var liveEffectiveViewerGender: Gender? = null
|
||||
var bannerLimit: Int? = null
|
||||
var bannerMemberId: Long? = null
|
||||
var bannerIncludeAdultBanners: Boolean? = null
|
||||
@@ -1187,12 +1194,16 @@ class HomeRecommendationQueryServiceTest {
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultLives: Boolean
|
||||
includeAdultLives: Boolean,
|
||||
isViewerCreator: Boolean,
|
||||
effectiveViewerGender: Gender?
|
||||
): List<HomeLiveRecommendationRecord> {
|
||||
liveOffset = offset
|
||||
liveLimit = limit
|
||||
liveMemberId = memberId
|
||||
liveIncludeAdultLives = includeAdultLives
|
||||
liveIsViewerCreator = isViewerCreator
|
||||
liveEffectiveViewerGender = effectiveViewerGender
|
||||
return liveRecommendations
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user