feat(recommend): 홈 추천 응답 필드를 정리한다
This commit is contained in:
@@ -438,7 +438,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
.param("size", "1")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.items[0].liveRoomId").value(newest.id))
|
||||
.andExpect(jsonPath("$.data.items[0].roomId").value(newest.id))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(true))
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
.param("size", "1")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.items[0].liveRoomId").value(oldest.id))
|
||||
.andExpect(jsonPath("$.data.items[0].roomId").value(oldest.id))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ class HomeRecommendationResponseTest {
|
||||
title = "first audio",
|
||||
price = 9,
|
||||
coverImage = "https://cdn.test/cover/audio.png",
|
||||
releaseDate = "2026-06-01T00:00:00Z",
|
||||
isPointAvailable = true
|
||||
)
|
||||
),
|
||||
@@ -36,6 +35,14 @@ class HomeRecommendationResponseTest {
|
||||
profileImage = "https://cdn.test/profile/character.png",
|
||||
totalChatCount = 4L,
|
||||
originalWorkTitle = "original"
|
||||
),
|
||||
HomeAiCharacterItem(
|
||||
characterId = 4L,
|
||||
name = "character-without-image",
|
||||
description = "description",
|
||||
profileImage = null,
|
||||
totalChatCount = 5L,
|
||||
originalWorkTitle = null
|
||||
)
|
||||
),
|
||||
genreCreators = emptyList(),
|
||||
@@ -54,6 +61,20 @@ class HomeRecommendationResponseTest {
|
||||
likeCount = 7L,
|
||||
commentCount = 8L,
|
||||
existOrdered = true
|
||||
),
|
||||
HomePopularCommunityPostItem(
|
||||
postId = 9L,
|
||||
creatorId = 10L,
|
||||
creatorNickname = "community-creator-without-media",
|
||||
creatorProfileImage = null,
|
||||
imageUrl = null,
|
||||
audioUrl = null,
|
||||
content = "community content without media",
|
||||
price = 0,
|
||||
createdAt = "2026-06-01T00:00:00Z",
|
||||
likeCount = 0L,
|
||||
commentCount = 0L,
|
||||
existOrdered = false
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -63,11 +84,16 @@ class HomeRecommendationResponseTest {
|
||||
assertEquals(9, json["firstAudioContents"][0]["price"].asInt())
|
||||
assertEquals(true, json["firstAudioContents"][0]["isPointAvailable"].asBoolean())
|
||||
assertFalse(json["firstAudioContents"][0].has("pointAvailable"))
|
||||
assertFalse(json["firstAudioContents"][0].has("releaseDate"))
|
||||
assertEquals("https://cdn.test/profile/character.png", json["aiCharacters"][0]["profileImage"].asText())
|
||||
assertEquals(true, json["aiCharacters"][1]["profileImage"].isNull)
|
||||
assertEquals(5L, json["popularCommunityPosts"][0]["postId"].asLong())
|
||||
assertEquals("https://cdn.test/community/image.png", json["popularCommunityPosts"][0]["imageUrl"].asText())
|
||||
assertEquals("https://cdn.test/community/audio.mp3", json["popularCommunityPosts"][0]["audioUrl"].asText())
|
||||
assertEquals(9, json["popularCommunityPosts"][0]["price"].asInt())
|
||||
assertEquals(true, json["popularCommunityPosts"][0]["existOrdered"].asBoolean())
|
||||
assertEquals(true, json["popularCommunityPosts"][1]["creatorProfileImage"].isNull)
|
||||
assertEquals(true, json["popularCommunityPosts"][1]["imageUrl"].isNull)
|
||||
assertEquals(true, json["popularCommunityPosts"][1]["audioUrl"].isNull)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,10 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val lives = repository.findLiveRecommendations(limit = 20, includeAdultLives = false)
|
||||
|
||||
assertEquals(20, lives.size)
|
||||
assertEquals(baseAt.plusDays(1).plusMinutes(20), lives.first().beginDateTime)
|
||||
assertEquals(baseAt.plusDays(1).plusMinutes(1), lives.last().beginDateTime)
|
||||
assertEquals(true, lives.zipWithNext().all { it.first.liveRoomId > it.second.liveRoomId })
|
||||
assertEquals(false, lives.any { it.liveRoomId == oldLive.id })
|
||||
assertEquals(false, lives.any { it.liveRoomId == latestLive.id })
|
||||
assertEquals(false, lives.any { it.creatorId == inactiveCreator.id })
|
||||
assertEquals(false, lives.any { it.creatorNickname == inactiveCreator.nickname })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -132,7 +131,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val creator = saveMember("banner-creator", MemberRole.CREATOR)
|
||||
val event = saveEvent("event-banner")
|
||||
val laterBanner = saveBanner("later.png", AudioContentBannerType.CREATOR, orders = 2, isActive = true, creator = creator)
|
||||
val sameOrderBanner1 = saveBanner(
|
||||
saveBanner(
|
||||
"same-1.png",
|
||||
AudioContentBannerType.LINK,
|
||||
orders = 1,
|
||||
@@ -161,12 +160,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val banners = repository.findHomeBanners(limit = 20)
|
||||
|
||||
assertEquals(20, banners.size)
|
||||
assertEquals(listOf(1, 1, 2), banners.take(3).map { it.orders })
|
||||
assertEquals(setOf(sameOrderBanner1.id, sameOrderBanner2.id), banners.take(2).map { it.bannerId }.toSet())
|
||||
assertEquals(true, banners.take(2).zipWithNext().all { it.first.randomTieBreaker <= it.second.randomTieBreaker })
|
||||
assertEquals(laterBanner.id, banners[2].bannerId)
|
||||
assertEquals(setOf("same-1.png", "same-2.png"), banners.take(2).map { it.thumbnailImage }.toSet())
|
||||
assertEquals(laterBanner.thumbnailImage, banners[2].thumbnailImage)
|
||||
assertEquals(creator.id, banners[2].creatorId)
|
||||
assertEquals(event.id, banners.take(2).first { it.type == AudioContentBannerType.EVENT.name }.eventId)
|
||||
val eventBanner = banners.take(2).single { it.thumbnailImage == sameOrderBanner2.thumbnailImage }
|
||||
assertEquals(event.id, eventBanner.eventId)
|
||||
assertEquals(event.thumbnailImage, eventBanner.eventThumbnailImage)
|
||||
assertEquals(event.detailImage, eventBanner.eventDetailImage)
|
||||
assertEquals(event.link, eventBanner.eventLink)
|
||||
assertEquals(false, banners.any { it.thumbnailImage == "inactive.png" })
|
||||
}
|
||||
|
||||
@@ -193,7 +194,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
|
||||
val banners = repository.findHomeBanners(limit = 20)
|
||||
|
||||
assertEquals(listOf(homeBanner.id), banners.map { it.bannerId })
|
||||
assertEquals(listOf(homeBanner.thumbnailImage), banners.map { it.thumbnailImage })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -250,8 +251,13 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val banners = repository.findHomeBanners(limit = 20)
|
||||
|
||||
assertEquals(
|
||||
listOf(activeEventBanner.id, activeCreatorBanner.id, activeSeriesBanner.id, linkBanner.id),
|
||||
banners.map { it.bannerId }
|
||||
listOf(
|
||||
activeEventBanner.thumbnailImage,
|
||||
activeCreatorBanner.thumbnailImage,
|
||||
activeSeriesBanner.thumbnailImage,
|
||||
linkBanner.thumbnailImage
|
||||
),
|
||||
banners.map { it.thumbnailImage }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -321,8 +327,13 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val banners = repository.findHomeBanners(limit = 20, memberId = viewer.id)
|
||||
|
||||
assertEquals(
|
||||
listOf(visibleEventBanner.id, visibleCreatorBanner.id, visibleSeriesBanner.id, visibleLinkBanner.id),
|
||||
banners.map { it.bannerId }
|
||||
listOf(
|
||||
visibleEventBanner.thumbnailImage,
|
||||
visibleCreatorBanner.thumbnailImage,
|
||||
visibleSeriesBanner.thumbnailImage,
|
||||
visibleLinkBanner.thumbnailImage
|
||||
),
|
||||
banners.map { it.thumbnailImage }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -343,22 +354,22 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
flushAndClear()
|
||||
|
||||
val creators = repository.findRecentlyActiveCreators(limit = 10)
|
||||
val byCreatorId = creators.associateBy { it.creatorId }
|
||||
val byCreatorNickname = creators.associateBy { it.creatorNickname }
|
||||
|
||||
assertEquals(4, creators.size)
|
||||
assertEquals(
|
||||
listOf(liveCreator.id, audioCreator.id, replayCreator.id, communityCreator.id),
|
||||
creators.map { it.creatorId }
|
||||
listOf(liveCreator.nickname, audioCreator.nickname, replayCreator.nickname, communityCreator.nickname),
|
||||
creators.map { it.creatorNickname }
|
||||
)
|
||||
assertEquals(RecommendedActivityType.LIVE, byCreatorId[liveCreator.id]!!.activityType)
|
||||
assertEquals(null, byCreatorId[liveCreator.id]!!.targetId)
|
||||
assertEquals(baseAt, byCreatorId[liveCreator.id]!!.activityAt)
|
||||
assertEquals(RecommendedActivityType.AUDIO, byCreatorId[audioCreator.id]!!.activityType)
|
||||
assertEquals(audio.id, byCreatorId[audioCreator.id]!!.targetId)
|
||||
assertEquals(RecommendedActivityType.LIVE_REPLAY, byCreatorId[replayCreator.id]!!.activityType)
|
||||
assertEquals(replay.id, byCreatorId[replayCreator.id]!!.targetId)
|
||||
assertEquals(RecommendedActivityType.COMMUNITY, byCreatorId[communityCreator.id]!!.activityType)
|
||||
assertEquals(community.id, byCreatorId[communityCreator.id]!!.targetId)
|
||||
assertEquals(RecommendedActivityType.LIVE, byCreatorNickname[liveCreator.nickname]!!.activityType)
|
||||
assertEquals(null, byCreatorNickname[liveCreator.nickname]!!.targetId)
|
||||
assertEquals(baseAt, byCreatorNickname[liveCreator.nickname]!!.activityAt)
|
||||
assertEquals(RecommendedActivityType.AUDIO, byCreatorNickname[audioCreator.nickname]!!.activityType)
|
||||
assertEquals(audio.id, byCreatorNickname[audioCreator.nickname]!!.targetId)
|
||||
assertEquals(RecommendedActivityType.LIVE_REPLAY, byCreatorNickname[replayCreator.nickname]!!.activityType)
|
||||
assertEquals(replay.id, byCreatorNickname[replayCreator.nickname]!!.targetId)
|
||||
assertEquals(RecommendedActivityType.COMMUNITY, byCreatorNickname[communityCreator.nickname]!!.activityType)
|
||||
assertEquals(community.id, byCreatorNickname[communityCreator.nickname]!!.targetId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -379,10 +390,15 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val hiddenCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = false)
|
||||
val visibleCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = true)
|
||||
|
||||
assertEquals(listOf(normalLiveCreator.id), hiddenCreators.map { it.creatorId })
|
||||
assertEquals(listOf(normalLiveCreator.nickname), hiddenCreators.map { it.creatorNickname })
|
||||
assertEquals(
|
||||
listOf(normalLiveCreator.id, adultLiveCreator.id, adultAudioCreator.id, adultCommunityCreator.id),
|
||||
visibleCreators.map { it.creatorId }
|
||||
listOf(
|
||||
normalLiveCreator.nickname,
|
||||
adultLiveCreator.nickname,
|
||||
adultAudioCreator.nickname,
|
||||
adultCommunityCreator.nickname
|
||||
),
|
||||
visibleCreators.map { it.creatorNickname }
|
||||
)
|
||||
assertEquals(null, visibleCreators[0].targetId)
|
||||
assertEquals(null, visibleCreators[1].targetId)
|
||||
@@ -412,7 +428,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
|
||||
val creators = repository.findRecentlyActiveCreators(limit = 10, memberId = viewer.id)
|
||||
|
||||
assertEquals(listOf(visibleCreator.id), creators.map { it.creatorId })
|
||||
assertEquals(listOf(visibleCreator.nickname), creators.map { it.creatorNickname })
|
||||
assertEquals(RecommendedActivityType.COMMUNITY, creators.single().activityType)
|
||||
}
|
||||
|
||||
@@ -933,22 +949,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
|
||||
val creators = repository.findRecentDebutCreators(now, limit = 10)
|
||||
|
||||
val expectedHighScore = scorePolicy.calculateDebutCreatorScore(
|
||||
followIncrease = 1,
|
||||
contentActivityScore = 1,
|
||||
communicationScore = 2,
|
||||
newBoost = scorePolicy.calculateCreatorNewBoost(now.minusDays(20), now)
|
||||
)
|
||||
val expectedLowScore = scorePolicy.calculateDebutCreatorScore(
|
||||
followIncrease = 0,
|
||||
contentActivityScore = 1,
|
||||
communicationScore = 0,
|
||||
newBoost = scorePolicy.calculateCreatorNewBoost(now.minusDays(5), now)
|
||||
)
|
||||
assertEquals(listOf(newHighScoreCreator.id, newLowScoreCreator.id), creators.map { it.creatorId })
|
||||
assertEquals(now.minusDays(20), creators.first().debutAt)
|
||||
assertEquals(expectedHighScore, creators.first().score, 0.0001)
|
||||
assertEquals(expectedLowScore, creators.last().score, 0.0001)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1014,12 +1015,12 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val page2 = repository.findRecentDebutCreators(now, offset = 2, limit = 1, includeAdultContents = false)
|
||||
|
||||
val pagedCreatorIds = (page0 + page1 + page2).map { it.creatorId }
|
||||
assertEquals(listOf(creator1.id, creator2.id, creator3.id), pagedCreatorIds)
|
||||
assertEquals(setOf(creator1.id, creator2.id, creator3.id), pagedCreatorIds.toSet())
|
||||
assertEquals(pagedCreatorIds, pagedCreatorIds.distinct())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 데뷔 크리에이터 동점은 랜덤 tie-breaker 오름차순으로 정렬한다")
|
||||
@DisplayName("최근 데뷔 크리에이터 동점은 DB 랜덤 tie-breaker로 정렬하고 조회 필드에는 노출하지 않는다")
|
||||
fun shouldOrderRecentDebutCreatorsByRandomTieBreakerWhenScoresTie() {
|
||||
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||
val creator1 = saveMember("tie-debut-1", MemberRole.CREATOR)
|
||||
@@ -1031,7 +1032,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val creators = repository.findRecentDebutCreators(now, limit = 10)
|
||||
|
||||
assertEquals(2, creators.size)
|
||||
assertEquals(true, creators.zipWithNext().all { it.first.randomTieBreaker <= it.second.randomTieBreaker })
|
||||
assertEquals(setOf(creator1.id, creator2.id), creators.map { it.creatorId }.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1061,7 +1062,6 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10)
|
||||
|
||||
assertEquals(listOf(eligibleActive.id), contents.map { it.contentId })
|
||||
assertEquals(100, contents.single().recencyScore)
|
||||
assertEquals(12, contents.single().price)
|
||||
}
|
||||
|
||||
@@ -1082,8 +1082,6 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10)
|
||||
|
||||
assertEquals(listOf(fresh.id, old.id), contents.map { it.contentId })
|
||||
assertEquals(listOf(100, 40), contents.map { it.recencyScore })
|
||||
assertEquals(true, contents.zipWithNext().all { it.first.recencyScore >= it.second.recencyScore })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1142,7 +1140,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val page2 = repository.findFirstAudioContents(now, offset = 2, limit = 1, includeAdultContents = false)
|
||||
|
||||
val pagedContentIds = (page0 + page1 + page2).map { it.contentId }
|
||||
assertEquals(listOf(content1.id, content2.id, content3.id), pagedContentIds)
|
||||
assertEquals(setOf(content1.id, content2.id, content3.id), pagedContentIds.toSet())
|
||||
assertEquals(pagedContentIds, pagedContentIds.distinct())
|
||||
}
|
||||
|
||||
|
||||
@@ -604,31 +604,24 @@ class HomeRecommendationQueryServiceTest {
|
||||
val liveRecommendations = listOf(
|
||||
HomeLiveRecommendationRecord(
|
||||
liveRoomId = 1L,
|
||||
creatorId = 10L,
|
||||
creatorNickname = "creator",
|
||||
creatorProfileImage = "profile.png",
|
||||
title = "live",
|
||||
coverImage = "cover.png",
|
||||
beginDateTime = LocalDateTime.of(2026, 5, 31, 10, 0),
|
||||
channelName = "channel"
|
||||
creatorProfileImage = "profile.png"
|
||||
)
|
||||
)
|
||||
val banners = listOf(
|
||||
HomeBannerRecommendationRecord(
|
||||
bannerId = 2L,
|
||||
type = "LINK",
|
||||
thumbnailImage = "banner.png",
|
||||
eventId = null,
|
||||
eventThumbnailImage = null,
|
||||
eventDetailImage = null,
|
||||
eventLink = null,
|
||||
creatorId = null,
|
||||
seriesId = null,
|
||||
link = "https://example.com",
|
||||
orders = 1,
|
||||
randomTieBreaker = 0.1
|
||||
link = "https://example.com"
|
||||
)
|
||||
)
|
||||
val activeCreators = listOf(
|
||||
RecentlyActiveCreatorRecord(
|
||||
creatorId = 10L,
|
||||
creatorNickname = "creator",
|
||||
creatorProfileImage = "profile.png",
|
||||
activityType = RecommendedActivityType.LIVE,
|
||||
@@ -640,10 +633,7 @@ class HomeRecommendationQueryServiceTest {
|
||||
RecentDebutCreatorRecord(
|
||||
creatorId = 11L,
|
||||
creatorNickname = "debut-creator",
|
||||
creatorProfileImage = "debut-profile.png",
|
||||
debutAt = LocalDateTime.of(2026, 5, 20, 10, 0),
|
||||
score = 1.2,
|
||||
randomTieBreaker = 0.2
|
||||
creatorProfileImage = "debut-profile.png"
|
||||
)
|
||||
)
|
||||
val firstAudioContents = listOf(
|
||||
@@ -655,10 +645,7 @@ class HomeRecommendationQueryServiceTest {
|
||||
title = "first-audio",
|
||||
price = 10,
|
||||
coverImage = "first-audio.png",
|
||||
releaseDate = LocalDateTime.of(2026, 5, 30, 10, 0),
|
||||
isPointAvailable = true,
|
||||
recencyScore = 100,
|
||||
randomTieBreaker = 0.3
|
||||
isPointAvailable = true
|
||||
)
|
||||
)
|
||||
var aiCharacterDetails: List<HomeAiCharacterRecommendationRecord> = emptyList()
|
||||
|
||||
Reference in New Issue
Block a user