feat(recommend): 홈 추천 응답 필드를 정리한다

This commit is contained in:
2026-06-05 18:15:19 +09:00
parent 7606796fe3
commit 6b469c1fad
8 changed files with 164 additions and 183 deletions

View File

@@ -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))
}

View File

@@ -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)
}
}

View File

@@ -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())
}

View File

@@ -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()