feat(recommend): 홈 추천 차단 필터를 확장한다

This commit is contained in:
2026-06-01 17:55:11 +09:00
parent 65f0ff7e72
commit c681fb9a3f
5 changed files with 398 additions and 35 deletions

View File

@@ -32,6 +32,7 @@ import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Member
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.v2.recommend.domain.RecommendationScorePolicy
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 })
}
@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
@DisplayName("홈 배너는 활성 배너를 orders 오름차순 최대 20개로 조회하고 동일 orders는 랜덤 tie-breaker를 적용한다")
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
@DisplayName("최근 활동 크리에이터는 크리에이터당 최신 활동 1개를 LIVE/AUDIO/COMMUNITY/LIVE_REPLAY로 분류한다")
fun shouldFindOneLatestActivityPerCreatorWithActivityType() {
@@ -302,6 +394,28 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
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
@DisplayName("AI 캐릭터 스냅샷은 AI 발화 수와 중복 없는 활성 사용자 수를 집계하고 팔로우 증가량은 제외한다")
fun shouldFindAiCharacterSnapshotsWithoutFollowIncrease() {
@@ -863,6 +977,26 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
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
@DisplayName("최근 데뷔 크리에이터 전체보기 동점 후보는 page size 1에서도 안정적으로 겹치지 않는다")
fun shouldFindPagedRecentDebutCreatorsWithStableTieOrdering() {
@@ -970,6 +1104,26 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
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
@DisplayName("첫 오디오 전체보기 동점 후보는 page size 1에서도 안정적으로 겹치지 않는다")
fun shouldFindPagedFirstAudioContentsWithStableTieOrdering() {
@@ -1046,6 +1200,25 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
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
@DisplayName("최근 응원 크리에이터 상세는 빈 id 목록이면 빈 배열을 반환한다")
fun shouldReturnEmptyCheerCreatorRecommendationDetailsWhenIdsAreEmpty() {
@@ -1114,6 +1287,29 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
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
@DisplayName("인기 커뮤니티 상세는 빈 id 목록이면 빈 배열을 반환한다")
fun shouldReturnEmptyPopularCommunityRecommendationDetailsWhenIdsAreEmpty() {
@@ -1176,6 +1372,32 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
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
@DisplayName("장르 기반 크리에이터 추천은 series_genre가 아니라 content_theme 기준으로 그룹을 만든다")
fun shouldGroupGenreCreatorRecommendationsByContentThemeNotSeriesGenre() {
@@ -1621,6 +1843,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
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) {
entityManager.createQuery("update $entityName e set e.createdAt = :createdAt where e.id = :id")
.setParameter("createdAt", createdAt)

View File

@@ -67,27 +67,30 @@ class HomeRecommendationQueryServiceTest {
@Test
@DisplayName("홈 라이브 추천은 기본 20개를 최신순 조회 포트에 위임한다")
fun shouldFindLatestLiveRecommendationsWithDefaultLimit() {
val recommendations = service.findLiveRecommendations()
val recommendations = service.findLiveRecommendations(memberId = 100L)
assertEquals(20, port.liveLimit)
assertEquals(100L, port.liveMemberId)
assertEquals(port.liveRecommendations, recommendations)
}
@Test
@DisplayName("홈 배너 추천은 기본 20개를 활성 배너 조회 포트에 위임한다")
fun shouldFindHomeBannersWithDefaultLimit() {
val banners = service.findHomeBanners()
val banners = service.findHomeBanners(memberId = 100L)
assertEquals(20, port.bannerLimit)
assertEquals(100L, port.bannerMemberId)
assertEquals(port.banners, banners)
}
@Test
@DisplayName("최근 활동 크리에이터는 기본 10개를 최신 활동 조회 포트에 위임한다")
fun shouldFindRecentlyActiveCreatorsWithDefaultLimit() {
val creators = service.findRecentlyActiveCreators()
val creators = service.findRecentlyActiveCreators(memberId = 100L)
assertEquals(10, port.activeCreatorLimit)
assertEquals(100L, port.activeCreatorMemberId)
assertEquals(false, port.activeCreatorIncludeAdultActivities)
assertEquals(port.activeCreators, creators)
}
@@ -95,9 +98,10 @@ class HomeRecommendationQueryServiceTest {
@Test
@DisplayName("최근 활동 크리에이터는 성인 활동 노출 여부를 조회 포트에 위임한다")
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(101L, port.activeCreatorMemberId)
assertEquals(true, port.activeCreatorIncludeAdultActivities)
assertEquals(port.activeCreators, creators)
}
@@ -107,9 +111,10 @@ class HomeRecommendationQueryServiceTest {
fun shouldFindRecentDebutCreatorsWithDefaultLimitAndNow() {
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(102L, port.recentDebutMemberId)
assertEquals(10, port.recentDebutLimit)
assertEquals(port.recentDebutCreators, creators)
}
@@ -119,9 +124,10 @@ class HomeRecommendationQueryServiceTest {
fun shouldFindFirstAudioContentsWithDefaultLimitAndNow() {
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(103L, port.firstAudioMemberId)
assertEquals(10, port.firstAudioLimit)
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("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(105L, port.popularCommunityMemberId)
assertEquals(true, port.popularCommunityIncludeAdultCommunities)
assertEquals(listOf(1L, 3L), communities.map { it.communityId })
assertEquals(listOf(10L, 11L), communities.map { it.creatorId })
@@ -401,17 +409,22 @@ class HomeRecommendationQueryServiceTest {
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
var liveLimit: Int? = null
var liveOffset: Int? = null
var liveMemberId: Long? = null
var liveIncludeAdultLives: Boolean? = null
var bannerLimit: Int? = null
var bannerMemberId: Long? = null
var activeCreatorLimit: Int? = null
var activeCreatorMemberId: Long? = null
var activeCreatorIncludeAdultActivities: Boolean? = null
var recentDebutNow: LocalDateTime? = null
var recentDebutLimit: Int? = null
var recentDebutOffset: Int? = null
var recentDebutMemberId: Long? = null
var recentDebutIncludeAdultContents: Boolean? = null
var firstAudioNow: LocalDateTime? = null
var firstAudioLimit: Int? = null
var firstAudioOffset: Int? = null
var firstAudioMemberId: Long? = null
var firstAudioIncludeAdultContents: Boolean? = null
var aiCharacterDetailIds: List<Long> = emptyList()
var cheerCreatorDetailIds: List<Long> = emptyList()
@@ -481,30 +494,37 @@ class HomeRecommendationQueryServiceTest {
)
var aiCharacterDetails: List<HomeAiCharacterRecommendationRecord> = emptyList()
var cheerCreatorDetails: List<HomeCheerCreatorRecommendationRecord> = emptyList()
var cheerCreatorMemberId: Long? = null
var popularCommunityDetails: List<HomePopularCommunityRecommendationRecord> = emptyList()
var popularCommunityMemberId: Long? = null
var genreCreatorRecommendations: List<HomeGenreCreatorRecommendationGroup> = emptyList()
override fun findLiveRecommendations(
offset: Int,
limit: Int,
memberId: Long?,
includeAdultLives: Boolean
): List<HomeLiveRecommendationRecord> {
liveOffset = offset
liveLimit = limit
liveMemberId = memberId
liveIncludeAdultLives = includeAdultLives
return liveRecommendations
}
override fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord> {
override fun findHomeBanners(limit: Int, memberId: Long?): List<HomeBannerRecommendationRecord> {
bannerLimit = limit
bannerMemberId = memberId
return banners
}
override fun findRecentlyActiveCreators(
limit: Int,
memberId: Long?,
includeAdultActivities: Boolean
): List<RecentlyActiveCreatorRecord> {
activeCreatorLimit = limit
activeCreatorMemberId = memberId
activeCreatorIncludeAdultActivities = includeAdultActivities
return activeCreators
}
@@ -513,11 +533,13 @@ class HomeRecommendationQueryServiceTest {
now: LocalDateTime,
offset: Int,
limit: Int,
memberId: Long?,
includeAdultContents: Boolean
): List<RecentDebutCreatorRecord> {
recentDebutNow = now
recentDebutOffset = offset
recentDebutLimit = limit
recentDebutMemberId = memberId
recentDebutIncludeAdultContents = includeAdultContents
return recentDebutCreators
}
@@ -526,11 +548,13 @@ class HomeRecommendationQueryServiceTest {
now: LocalDateTime,
offset: Int,
limit: Int,
memberId: Long?,
includeAdultContents: Boolean
): List<HomeFirstAudioContentRecord> {
firstAudioNow = now
firstAudioOffset = offset
firstAudioLimit = limit
firstAudioMemberId = memberId
firstAudioIncludeAdultContents = includeAdultContents
return firstAudioContents
}
@@ -558,16 +582,22 @@ class HomeRecommendationQueryServiceTest {
return aiCharacterDetails
}
override fun findCheerCreatorRecommendationDetails(creatorIds: List<Long>): List<HomeCheerCreatorRecommendationRecord> {
override fun findCheerCreatorRecommendationDetails(
creatorIds: List<Long>,
memberId: Long?
): List<HomeCheerCreatorRecommendationRecord> {
cheerCreatorDetailIds = creatorIds
cheerCreatorMemberId = memberId
return cheerCreatorDetails
}
override fun findPopularCommunityRecommendationDetails(
communityIds: List<Long>,
memberId: Long?,
includeAdultCommunities: Boolean
): List<HomePopularCommunityRecommendationRecord> {
popularCommunityDetailIds = communityIds
popularCommunityMemberId = memberId
popularCommunityIncludeAdultCommunities = includeAdultCommunities
return popularCommunityDetails
}