feat(recommend): 홈 추천 응답 필드를 정리한다
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.api.home.application
|
package kr.co.vividnext.sodalive.v2.api.home.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.event.EventItem
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
|
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
|
||||||
@@ -14,6 +15,7 @@ import kr.co.vividnext.sodalive.v2.api.home.dto.HomePopularCommunityPostItem
|
|||||||
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationPageResponse
|
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationPageResponse
|
||||||
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationResponse
|
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationResponse
|
||||||
import kr.co.vividnext.sodalive.v2.api.home.dto.imageUrl
|
import kr.co.vividnext.sodalive.v2.api.home.dto.imageUrl
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.home.dto.profileImageUrl
|
||||||
import kr.co.vividnext.sodalive.v2.api.home.dto.toUtcIso
|
import kr.co.vividnext.sodalive.v2.api.home.dto.toUtcIso
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryService
|
import kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryService
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendationRecord
|
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendationRecord
|
||||||
@@ -228,30 +230,38 @@ class HomeRecommendationFacade(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun HomeLiveRecommendationRecord.toItem() = HomeLiveItem(
|
private fun HomeLiveRecommendationRecord.toItem() = HomeLiveItem(
|
||||||
liveRoomId = liveRoomId,
|
roomId = liveRoomId,
|
||||||
creatorId = creatorId,
|
|
||||||
creatorNickname = creatorNickname,
|
creatorNickname = creatorNickname,
|
||||||
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
|
creatorProfileImage = profileImageUrl(cloudFrontHost, creatorProfileImage)
|
||||||
title = title,
|
|
||||||
coverImage = imageUrl(cloudFrontHost, coverImage),
|
|
||||||
beginDateTime = beginDateTime.toUtcIso(),
|
|
||||||
channelName = channelName
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun HomeBannerRecommendationRecord.toItem() = HomeBannerItem(
|
private fun HomeBannerRecommendationRecord.toItem() = HomeBannerItem(
|
||||||
bannerId = bannerId,
|
imageUrl = imageUrl(cloudFrontHost, thumbnailImage) ?: "",
|
||||||
type = type,
|
eventItem = eventItem(),
|
||||||
thumbnailImage = imageUrl(cloudFrontHost, thumbnailImage),
|
|
||||||
eventId = eventId,
|
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
link = link
|
link = link
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun HomeBannerRecommendationRecord.eventItem(): EventItem? {
|
||||||
|
if (eventId == null || eventThumbnailImage == null) return null
|
||||||
|
return EventItem(
|
||||||
|
id = eventId,
|
||||||
|
thumbnailImageUrl = eventImageUrl(eventThumbnailImage) ?: eventThumbnailImage,
|
||||||
|
detailImageUrl = eventImageUrl(eventDetailImage),
|
||||||
|
popupImageUrl = null,
|
||||||
|
link = eventLink
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun eventImageUrl(path: String?): String? {
|
||||||
|
if (path.isNullOrBlank()) return null
|
||||||
|
return if (path.startsWith("https://")) path else imageUrl(cloudFrontHost, path)
|
||||||
|
}
|
||||||
|
|
||||||
private fun RecentlyActiveCreatorRecord.toItem() = HomeActiveCreatorItem(
|
private fun RecentlyActiveCreatorRecord.toItem() = HomeActiveCreatorItem(
|
||||||
creatorId = creatorId,
|
|
||||||
creatorNickname = creatorNickname,
|
creatorNickname = creatorNickname,
|
||||||
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
|
creatorProfileImage = profileImageUrl(cloudFrontHost, creatorProfileImage),
|
||||||
activityType = activityType.name,
|
activityType = activityType.name,
|
||||||
activityAt = activityAt.toUtcIso(),
|
activityAt = activityAt.toUtcIso(),
|
||||||
targetId = targetId
|
targetId = targetId
|
||||||
@@ -260,18 +270,17 @@ class HomeRecommendationFacade(
|
|||||||
private fun RecentDebutCreatorRecord.toItem() = HomeCreatorItem(
|
private fun RecentDebutCreatorRecord.toItem() = HomeCreatorItem(
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
creatorNickname = creatorNickname,
|
creatorNickname = creatorNickname,
|
||||||
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage)
|
creatorProfileImage = profileImageUrl(cloudFrontHost, creatorProfileImage)
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun HomeFirstAudioContentRecord.toItem() = HomeFirstAudioContentItem(
|
private fun HomeFirstAudioContentRecord.toItem() = HomeFirstAudioContentItem(
|
||||||
contentId = contentId,
|
contentId = contentId,
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
creatorNickname = creatorNickname,
|
creatorNickname = creatorNickname,
|
||||||
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
|
creatorProfileImage = profileImageUrl(cloudFrontHost, creatorProfileImage),
|
||||||
title = title,
|
title = title,
|
||||||
price = price,
|
price = price,
|
||||||
coverImage = imageUrl(cloudFrontHost, coverImage),
|
coverImage = imageUrl(cloudFrontHost, coverImage),
|
||||||
releaseDate = releaseDate.toUtcIso(),
|
|
||||||
isPointAvailable = isPointAvailable
|
isPointAvailable = isPointAvailable
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -285,13 +294,12 @@ class HomeRecommendationFacade(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun HomeGenreCreatorRecommendationGroup.toItem() = HomeGenreCreatorGroupItem(
|
private fun HomeGenreCreatorRecommendationGroup.toItem() = HomeGenreCreatorGroupItem(
|
||||||
genreId = genreId,
|
|
||||||
genreName = genreName,
|
genreName = genreName,
|
||||||
creators = creators.map {
|
creators = creators.map {
|
||||||
HomeCreatorItem(
|
HomeCreatorItem(
|
||||||
creatorId = it.creatorId,
|
creatorId = it.creatorId,
|
||||||
creatorNickname = it.creatorNickname,
|
creatorNickname = it.creatorNickname,
|
||||||
creatorProfileImage = imageUrl(cloudFrontHost, it.creatorProfileImage)
|
creatorProfileImage = profileImageUrl(cloudFrontHost, it.creatorProfileImage)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -299,7 +307,7 @@ class HomeRecommendationFacade(
|
|||||||
private fun HomeCheerCreatorRecommendationRecord.toCreatorItem() = HomeCreatorItem(
|
private fun HomeCheerCreatorRecommendationRecord.toCreatorItem() = HomeCreatorItem(
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
creatorNickname = creatorNickname,
|
creatorNickname = creatorNickname,
|
||||||
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage)
|
creatorProfileImage = profileImageUrl(cloudFrontHost, creatorProfileImage)
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun HomePopularCommunityRecommendationRecord.toItem() = HomePopularCommunityPostItem(
|
private fun HomePopularCommunityRecommendationRecord.toItem() = HomePopularCommunityPostItem(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.api.home.dto
|
package kr.co.vividnext.sodalive.v2.api.home.dto
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import kr.co.vividnext.sodalive.event.EventItem
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
@@ -12,6 +13,10 @@ internal fun imageUrl(cloudFrontHost: String, path: String?): String? {
|
|||||||
return if (path.isNullOrBlank()) null else "$cloudFrontHost/$path"
|
return if (path.isNullOrBlank()) null else "$cloudFrontHost/$path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun profileImageUrl(cloudFrontHost: String, path: String?): String {
|
||||||
|
return imageUrl(cloudFrontHost, path) ?: "$cloudFrontHost/profile/default-profile.png"
|
||||||
|
}
|
||||||
|
|
||||||
data class HomeRecommendationResponse(
|
data class HomeRecommendationResponse(
|
||||||
val lives: List<HomeLiveItem>,
|
val lives: List<HomeLiveItem>,
|
||||||
val banners: List<HomeBannerItem>,
|
val banners: List<HomeBannerItem>,
|
||||||
@@ -25,30 +30,22 @@ data class HomeRecommendationResponse(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class HomeLiveItem(
|
data class HomeLiveItem(
|
||||||
val liveRoomId: Long,
|
val roomId: Long,
|
||||||
val creatorId: Long,
|
|
||||||
val creatorNickname: String,
|
val creatorNickname: String,
|
||||||
val creatorProfileImage: String?,
|
val creatorProfileImage: String
|
||||||
val title: String,
|
|
||||||
val coverImage: String?,
|
|
||||||
val beginDateTime: String,
|
|
||||||
val channelName: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeBannerItem(
|
data class HomeBannerItem(
|
||||||
val bannerId: Long,
|
val imageUrl: String,
|
||||||
val type: String,
|
val eventItem: EventItem?,
|
||||||
val thumbnailImage: String?,
|
|
||||||
val eventId: Long?,
|
|
||||||
val creatorId: Long?,
|
val creatorId: Long?,
|
||||||
val seriesId: Long?,
|
val seriesId: Long?,
|
||||||
val link: String?
|
val link: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeActiveCreatorItem(
|
data class HomeActiveCreatorItem(
|
||||||
val creatorId: Long,
|
|
||||||
val creatorNickname: String,
|
val creatorNickname: String,
|
||||||
val creatorProfileImage: String?,
|
val creatorProfileImage: String,
|
||||||
val activityType: String,
|
val activityType: String,
|
||||||
val activityAt: String,
|
val activityAt: String,
|
||||||
val targetId: Long?
|
val targetId: Long?
|
||||||
@@ -57,18 +54,17 @@ data class HomeActiveCreatorItem(
|
|||||||
data class HomeCreatorItem(
|
data class HomeCreatorItem(
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
val creatorNickname: String,
|
val creatorNickname: String,
|
||||||
val creatorProfileImage: String?
|
val creatorProfileImage: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeFirstAudioContentItem(
|
data class HomeFirstAudioContentItem(
|
||||||
val contentId: Long,
|
val contentId: Long,
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
val creatorNickname: String,
|
val creatorNickname: String,
|
||||||
val creatorProfileImage: String?,
|
val creatorProfileImage: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val price: Int,
|
val price: Int,
|
||||||
val coverImage: String?,
|
val coverImage: String?,
|
||||||
val releaseDate: String,
|
|
||||||
@JsonProperty("isPointAvailable")
|
@JsonProperty("isPointAvailable")
|
||||||
val isPointAvailable: Boolean
|
val isPointAvailable: Boolean
|
||||||
)
|
)
|
||||||
@@ -83,7 +79,6 @@ data class HomeAiCharacterItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class HomeGenreCreatorGroupItem(
|
data class HomeGenreCreatorGroupItem(
|
||||||
val genreId: Long,
|
|
||||||
val genreName: String,
|
val genreName: String,
|
||||||
val creators: List<HomeCreatorItem>
|
val creators: List<HomeCreatorItem>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,13 +59,8 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
Projections.constructor(
|
Projections.constructor(
|
||||||
HomeLiveRecommendationRecord::class.java,
|
HomeLiveRecommendationRecord::class.java,
|
||||||
liveRoom.id,
|
liveRoom.id,
|
||||||
member.id,
|
|
||||||
member.nickname,
|
member.nickname,
|
||||||
member.profileImage,
|
member.profileImage
|
||||||
liveRoom.title,
|
|
||||||
liveRoom.coverImage,
|
|
||||||
liveRoom.beginDateTime,
|
|
||||||
liveRoom.channelName
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.from(liveRoom)
|
.from(liveRoom)
|
||||||
@@ -96,15 +91,14 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
.select(
|
.select(
|
||||||
Projections.constructor(
|
Projections.constructor(
|
||||||
HomeBannerRecommendationRecord::class.java,
|
HomeBannerRecommendationRecord::class.java,
|
||||||
audioContentBanner.id,
|
|
||||||
audioContentBanner.type.stringValue(),
|
|
||||||
audioContentBanner.thumbnailImage,
|
audioContentBanner.thumbnailImage,
|
||||||
event.id,
|
event.id,
|
||||||
|
event.thumbnailImage,
|
||||||
|
event.detailImage,
|
||||||
|
event.link,
|
||||||
bannerCreator.id,
|
bannerCreator.id,
|
||||||
series.id,
|
series.id,
|
||||||
audioContentBanner.link,
|
audioContentBanner.link
|
||||||
audioContentBanner.orders,
|
|
||||||
randomTieBreaker
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.from(audioContentBanner)
|
.from(audioContentBanner)
|
||||||
@@ -128,8 +122,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
includeAdultActivities: Boolean
|
includeAdultActivities: Boolean
|
||||||
): List<RecentlyActiveCreatorRecord> {
|
): List<RecentlyActiveCreatorRecord> {
|
||||||
val sql = """
|
val sql = """
|
||||||
select ranked.creator_id,
|
select ranked.creator_nickname,
|
||||||
ranked.creator_nickname,
|
|
||||||
ranked.creator_profile_image,
|
ranked.creator_profile_image,
|
||||||
ranked.activity_type,
|
ranked.activity_type,
|
||||||
ranked.activity_at,
|
ranked.activity_at,
|
||||||
@@ -202,12 +195,11 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
|
|
||||||
return rows.map { row ->
|
return rows.map { row ->
|
||||||
RecentlyActiveCreatorRecord(
|
RecentlyActiveCreatorRecord(
|
||||||
creatorId = (row[0] as Number).toLong(),
|
creatorNickname = row[0] as String,
|
||||||
creatorNickname = row[1] as String,
|
creatorProfileImage = row[1] as String?,
|
||||||
creatorProfileImage = row[2] as String?,
|
activityType = RecommendedActivityType.valueOf(row[2] as String),
|
||||||
activityType = RecommendedActivityType.valueOf(row[3] as String),
|
activityAt = toLocalDateTime(row[3]),
|
||||||
activityAt = toLocalDateTime(row[4]),
|
targetId = (row[4] as Number?)?.toLong()
|
||||||
targetId = (row[5] as Number?)?.toLong()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,18 +307,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
)
|
)
|
||||||
select m.id as creator_id,
|
select m.id as creator_id,
|
||||||
m.nickname as creator_nickname,
|
m.nickname as creator_nickname,
|
||||||
m.profile_image as creator_profile_image,
|
m.profile_image as creator_profile_image
|
||||||
cd.debut_at as debut_at,
|
|
||||||
((coalesce(fs.follow_increase, 0) * ${RecommendationScoreSpec.DEBUT_FOLLOW_INCREASE_WEIGHT} +
|
|
||||||
coalesce(cs.content_activity_score, 0) * ${RecommendationScoreSpec.DEBUT_CONTENT_ACTIVITY_WEIGHT} +
|
|
||||||
coalesce(cms.communication_score, 0) * ${RecommendationScoreSpec.DEBUT_COMMUNICATION_WEIGHT}) *
|
|
||||||
case
|
|
||||||
when cd.debut_at >= :boost10Start then ${RecommendationScoreSpec.NEW_BOOST_10_DAYS}
|
|
||||||
when cd.debut_at >= :boost20Start then ${RecommendationScoreSpec.NEW_BOOST_20_DAYS}
|
|
||||||
when cd.debut_at >= :boost30Start then ${RecommendationScoreSpec.NEW_BOOST_30_DAYS}
|
|
||||||
else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST}
|
|
||||||
end) as score,
|
|
||||||
m.id as random_tie_breaker
|
|
||||||
from member m
|
from member m
|
||||||
join creator_debut cd on cd.creator_id = m.id
|
join creator_debut cd on cd.creator_id = m.id
|
||||||
left join follow_stats fs on fs.creator_id = m.id
|
left join follow_stats fs on fs.creator_id = m.id
|
||||||
@@ -336,7 +317,15 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
and cd.debut_at >= :boost30Start
|
and cd.debut_at >= :boost30Start
|
||||||
and cd.debut_at <= :now
|
and cd.debut_at <= :now
|
||||||
and ${notBlockedCreatorSql("m.id")}
|
and ${notBlockedCreatorSql("m.id")}
|
||||||
order by score desc, random_tie_breaker asc
|
order by ((coalesce(fs.follow_increase, 0) * ${RecommendationScoreSpec.DEBUT_FOLLOW_INCREASE_WEIGHT} +
|
||||||
|
coalesce(cs.content_activity_score, 0) * ${RecommendationScoreSpec.DEBUT_CONTENT_ACTIVITY_WEIGHT} +
|
||||||
|
coalesce(cms.communication_score, 0) * ${RecommendationScoreSpec.DEBUT_COMMUNICATION_WEIGHT}) *
|
||||||
|
case
|
||||||
|
when cd.debut_at >= :boost10Start then ${RecommendationScoreSpec.NEW_BOOST_10_DAYS}
|
||||||
|
when cd.debut_at >= :boost20Start then ${RecommendationScoreSpec.NEW_BOOST_20_DAYS}
|
||||||
|
when cd.debut_at >= :boost30Start then ${RecommendationScoreSpec.NEW_BOOST_30_DAYS}
|
||||||
|
else ${RecommendationScoreSpec.DEFAULT_NEW_BOOST}
|
||||||
|
end) desc, rand(m.id) asc
|
||||||
limit :limit
|
limit :limit
|
||||||
offset :offset
|
offset :offset
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
@@ -354,10 +343,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
RecentDebutCreatorRecord(
|
RecentDebutCreatorRecord(
|
||||||
creatorId = (row[0] as Number).toLong(),
|
creatorId = (row[0] as Number).toLong(),
|
||||||
creatorNickname = row[1] as String,
|
creatorNickname = row[1] as String,
|
||||||
creatorProfileImage = row[2] as String?,
|
creatorProfileImage = row[2] as String?
|
||||||
debutAt = toLocalDateTime(row[3]),
|
|
||||||
score = (row[4] as Number).toDouble(),
|
|
||||||
randomTieBreaker = (row[5] as Number).toDouble()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,17 +411,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
ec.title as title,
|
ec.title as title,
|
||||||
ec.price as price,
|
ec.price as price,
|
||||||
ec.cover_image as cover_image,
|
ec.cover_image as cover_image,
|
||||||
ec.release_date as release_date,
|
ec.is_point_available as is_point_available
|
||||||
ec.is_point_available as is_point_available,
|
|
||||||
case
|
|
||||||
when ec.release_date >= :recency3Start then 100
|
|
||||||
when ec.release_date >= :recency7Start then 80
|
|
||||||
when ec.release_date >= :recency14Start then 60
|
|
||||||
when ec.release_date >= :recency21Start then 40
|
|
||||||
when ec.release_date >= :boost30Start then 20
|
|
||||||
else 0
|
|
||||||
end as recency_score,
|
|
||||||
ec.content_id as random_tie_breaker
|
|
||||||
from eligible_contents ec
|
from eligible_contents ec
|
||||||
join member m on m.id = ec.creator_id
|
join member m on m.id = ec.creator_id
|
||||||
join creator_debut cd on cd.creator_id = ec.creator_id
|
join creator_debut cd on cd.creator_id = ec.creator_id
|
||||||
@@ -445,7 +421,14 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
and cd.debut_at <= :now
|
and cd.debut_at <= :now
|
||||||
and ec.release_date >= :boost30Start
|
and ec.release_date >= :boost30Start
|
||||||
and ${notBlockedCreatorSql("m.id")}
|
and ${notBlockedCreatorSql("m.id")}
|
||||||
order by recency_score desc, random_tie_breaker asc
|
order by case
|
||||||
|
when ec.release_date >= :recency3Start then 100
|
||||||
|
when ec.release_date >= :recency7Start then 80
|
||||||
|
when ec.release_date >= :recency14Start then 60
|
||||||
|
when ec.release_date >= :recency21Start then 40
|
||||||
|
when ec.release_date >= :boost30Start then 20
|
||||||
|
else 0
|
||||||
|
end desc, rand(ec.content_id) asc
|
||||||
limit :limit
|
limit :limit
|
||||||
offset :offset
|
offset :offset
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
@@ -477,10 +460,7 @@ class DefaultHomeRecommendationQueryRepository(
|
|||||||
title = row[4] as String,
|
title = row[4] as String,
|
||||||
price = (row[5] as Number).toInt(),
|
price = (row[5] as Number).toInt(),
|
||||||
coverImage = row[6] as String?,
|
coverImage = row[6] as String?,
|
||||||
releaseDate = toLocalDateTime(row[7]),
|
isPointAvailable = row[7] as Boolean
|
||||||
isPointAvailable = row[8] as Boolean,
|
|
||||||
recencyScore = (row[9] as Number).toInt(),
|
|
||||||
randomTieBreaker = (row[10] as Number).toDouble()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,29 +79,22 @@ interface HomeRecommendationQueryPort {
|
|||||||
|
|
||||||
data class HomeLiveRecommendationRecord(
|
data class HomeLiveRecommendationRecord(
|
||||||
val liveRoomId: Long,
|
val liveRoomId: Long,
|
||||||
val creatorId: Long,
|
|
||||||
val creatorNickname: String,
|
val creatorNickname: String,
|
||||||
val creatorProfileImage: String?,
|
val creatorProfileImage: String?
|
||||||
val title: String,
|
|
||||||
val coverImage: String?,
|
|
||||||
val beginDateTime: LocalDateTime,
|
|
||||||
val channelName: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeBannerRecommendationRecord(
|
data class HomeBannerRecommendationRecord(
|
||||||
val bannerId: Long,
|
|
||||||
val type: String,
|
|
||||||
val thumbnailImage: String,
|
val thumbnailImage: String,
|
||||||
val eventId: Long?,
|
val eventId: Long?,
|
||||||
|
val eventThumbnailImage: String?,
|
||||||
|
val eventDetailImage: String?,
|
||||||
|
val eventLink: String?,
|
||||||
val creatorId: Long?,
|
val creatorId: Long?,
|
||||||
val seriesId: Long?,
|
val seriesId: Long?,
|
||||||
val link: String?,
|
val link: String?
|
||||||
val orders: Int,
|
|
||||||
val randomTieBreaker: Double
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RecentlyActiveCreatorRecord(
|
data class RecentlyActiveCreatorRecord(
|
||||||
val creatorId: Long,
|
|
||||||
val creatorNickname: String,
|
val creatorNickname: String,
|
||||||
val creatorProfileImage: String?,
|
val creatorProfileImage: String?,
|
||||||
val activityType: RecommendedActivityType,
|
val activityType: RecommendedActivityType,
|
||||||
@@ -112,10 +105,7 @@ data class RecentlyActiveCreatorRecord(
|
|||||||
data class RecentDebutCreatorRecord(
|
data class RecentDebutCreatorRecord(
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
val creatorNickname: String,
|
val creatorNickname: String,
|
||||||
val creatorProfileImage: String?,
|
val creatorProfileImage: String?
|
||||||
val debutAt: LocalDateTime,
|
|
||||||
val score: Double,
|
|
||||||
val randomTieBreaker: Double
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeFirstAudioContentRecord(
|
data class HomeFirstAudioContentRecord(
|
||||||
@@ -126,10 +116,7 @@ data class HomeFirstAudioContentRecord(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val price: Int,
|
val price: Int,
|
||||||
val coverImage: String?,
|
val coverImage: String?,
|
||||||
val releaseDate: LocalDateTime,
|
val isPointAvailable: Boolean
|
||||||
val isPointAvailable: Boolean,
|
|
||||||
val recencyScore: Int,
|
|
||||||
val randomTieBreaker: Double
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeAiCharacterRecommendationRecord(
|
data class HomeAiCharacterRecommendationRecord(
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
.param("size", "1")
|
.param("size", "1")
|
||||||
)
|
)
|
||||||
.andExpect(status().isOk)
|
.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))
|
.andExpect(jsonPath("$.data.hasNext").value(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
.param("size", "1")
|
.param("size", "1")
|
||||||
)
|
)
|
||||||
.andExpect(status().isOk)
|
.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))
|
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class HomeRecommendationResponseTest {
|
|||||||
title = "first audio",
|
title = "first audio",
|
||||||
price = 9,
|
price = 9,
|
||||||
coverImage = "https://cdn.test/cover/audio.png",
|
coverImage = "https://cdn.test/cover/audio.png",
|
||||||
releaseDate = "2026-06-01T00:00:00Z",
|
|
||||||
isPointAvailable = true
|
isPointAvailable = true
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -36,6 +35,14 @@ class HomeRecommendationResponseTest {
|
|||||||
profileImage = "https://cdn.test/profile/character.png",
|
profileImage = "https://cdn.test/profile/character.png",
|
||||||
totalChatCount = 4L,
|
totalChatCount = 4L,
|
||||||
originalWorkTitle = "original"
|
originalWorkTitle = "original"
|
||||||
|
),
|
||||||
|
HomeAiCharacterItem(
|
||||||
|
characterId = 4L,
|
||||||
|
name = "character-without-image",
|
||||||
|
description = "description",
|
||||||
|
profileImage = null,
|
||||||
|
totalChatCount = 5L,
|
||||||
|
originalWorkTitle = null
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
genreCreators = emptyList(),
|
genreCreators = emptyList(),
|
||||||
@@ -54,6 +61,20 @@ class HomeRecommendationResponseTest {
|
|||||||
likeCount = 7L,
|
likeCount = 7L,
|
||||||
commentCount = 8L,
|
commentCount = 8L,
|
||||||
existOrdered = true
|
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(9, json["firstAudioContents"][0]["price"].asInt())
|
||||||
assertEquals(true, json["firstAudioContents"][0]["isPointAvailable"].asBoolean())
|
assertEquals(true, json["firstAudioContents"][0]["isPointAvailable"].asBoolean())
|
||||||
assertFalse(json["firstAudioContents"][0].has("pointAvailable"))
|
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("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(5L, json["popularCommunityPosts"][0]["postId"].asLong())
|
||||||
assertEquals("https://cdn.test/community/image.png", json["popularCommunityPosts"][0]["imageUrl"].asText())
|
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("https://cdn.test/community/audio.mp3", json["popularCommunityPosts"][0]["audioUrl"].asText())
|
||||||
assertEquals(9, json["popularCommunityPosts"][0]["price"].asInt())
|
assertEquals(9, json["popularCommunityPosts"][0]["price"].asInt())
|
||||||
assertEquals(true, json["popularCommunityPosts"][0]["existOrdered"].asBoolean())
|
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)
|
val lives = repository.findLiveRecommendations(limit = 20, includeAdultLives = false)
|
||||||
|
|
||||||
assertEquals(20, lives.size)
|
assertEquals(20, lives.size)
|
||||||
assertEquals(baseAt.plusDays(1).plusMinutes(20), lives.first().beginDateTime)
|
assertEquals(true, lives.zipWithNext().all { it.first.liveRoomId > it.second.liveRoomId })
|
||||||
assertEquals(baseAt.plusDays(1).plusMinutes(1), lives.last().beginDateTime)
|
|
||||||
assertEquals(false, lives.any { it.liveRoomId == oldLive.id })
|
assertEquals(false, lives.any { it.liveRoomId == oldLive.id })
|
||||||
assertEquals(false, lives.any { it.liveRoomId == latestLive.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
|
@Test
|
||||||
@@ -132,7 +131,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val creator = saveMember("banner-creator", MemberRole.CREATOR)
|
val creator = saveMember("banner-creator", MemberRole.CREATOR)
|
||||||
val event = saveEvent("event-banner")
|
val event = saveEvent("event-banner")
|
||||||
val laterBanner = saveBanner("later.png", AudioContentBannerType.CREATOR, orders = 2, isActive = true, creator = creator)
|
val laterBanner = saveBanner("later.png", AudioContentBannerType.CREATOR, orders = 2, isActive = true, creator = creator)
|
||||||
val sameOrderBanner1 = saveBanner(
|
saveBanner(
|
||||||
"same-1.png",
|
"same-1.png",
|
||||||
AudioContentBannerType.LINK,
|
AudioContentBannerType.LINK,
|
||||||
orders = 1,
|
orders = 1,
|
||||||
@@ -161,12 +160,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val banners = repository.findHomeBanners(limit = 20)
|
val banners = repository.findHomeBanners(limit = 20)
|
||||||
|
|
||||||
assertEquals(20, banners.size)
|
assertEquals(20, banners.size)
|
||||||
assertEquals(listOf(1, 1, 2), banners.take(3).map { it.orders })
|
assertEquals(setOf("same-1.png", "same-2.png"), banners.take(2).map { it.thumbnailImage }.toSet())
|
||||||
assertEquals(setOf(sameOrderBanner1.id, sameOrderBanner2.id), banners.take(2).map { it.bannerId }.toSet())
|
assertEquals(laterBanner.thumbnailImage, banners[2].thumbnailImage)
|
||||||
assertEquals(true, banners.take(2).zipWithNext().all { it.first.randomTieBreaker <= it.second.randomTieBreaker })
|
|
||||||
assertEquals(laterBanner.id, banners[2].bannerId)
|
|
||||||
assertEquals(creator.id, banners[2].creatorId)
|
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" })
|
assertEquals(false, banners.any { it.thumbnailImage == "inactive.png" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
|
|
||||||
val banners = repository.findHomeBanners(limit = 20)
|
val banners = repository.findHomeBanners(limit = 20)
|
||||||
|
|
||||||
assertEquals(listOf(homeBanner.id), banners.map { it.bannerId })
|
assertEquals(listOf(homeBanner.thumbnailImage), banners.map { it.thumbnailImage })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -250,8 +251,13 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val banners = repository.findHomeBanners(limit = 20)
|
val banners = repository.findHomeBanners(limit = 20)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(activeEventBanner.id, activeCreatorBanner.id, activeSeriesBanner.id, linkBanner.id),
|
listOf(
|
||||||
banners.map { it.bannerId }
|
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)
|
val banners = repository.findHomeBanners(limit = 20, memberId = viewer.id)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(visibleEventBanner.id, visibleCreatorBanner.id, visibleSeriesBanner.id, visibleLinkBanner.id),
|
listOf(
|
||||||
banners.map { it.bannerId }
|
visibleEventBanner.thumbnailImage,
|
||||||
|
visibleCreatorBanner.thumbnailImage,
|
||||||
|
visibleSeriesBanner.thumbnailImage,
|
||||||
|
visibleLinkBanner.thumbnailImage
|
||||||
|
),
|
||||||
|
banners.map { it.thumbnailImage }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,22 +354,22 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
flushAndClear()
|
flushAndClear()
|
||||||
|
|
||||||
val creators = repository.findRecentlyActiveCreators(limit = 10)
|
val creators = repository.findRecentlyActiveCreators(limit = 10)
|
||||||
val byCreatorId = creators.associateBy { it.creatorId }
|
val byCreatorNickname = creators.associateBy { it.creatorNickname }
|
||||||
|
|
||||||
assertEquals(4, creators.size)
|
assertEquals(4, creators.size)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(liveCreator.id, audioCreator.id, replayCreator.id, communityCreator.id),
|
listOf(liveCreator.nickname, audioCreator.nickname, replayCreator.nickname, communityCreator.nickname),
|
||||||
creators.map { it.creatorId }
|
creators.map { it.creatorNickname }
|
||||||
)
|
)
|
||||||
assertEquals(RecommendedActivityType.LIVE, byCreatorId[liveCreator.id]!!.activityType)
|
assertEquals(RecommendedActivityType.LIVE, byCreatorNickname[liveCreator.nickname]!!.activityType)
|
||||||
assertEquals(null, byCreatorId[liveCreator.id]!!.targetId)
|
assertEquals(null, byCreatorNickname[liveCreator.nickname]!!.targetId)
|
||||||
assertEquals(baseAt, byCreatorId[liveCreator.id]!!.activityAt)
|
assertEquals(baseAt, byCreatorNickname[liveCreator.nickname]!!.activityAt)
|
||||||
assertEquals(RecommendedActivityType.AUDIO, byCreatorId[audioCreator.id]!!.activityType)
|
assertEquals(RecommendedActivityType.AUDIO, byCreatorNickname[audioCreator.nickname]!!.activityType)
|
||||||
assertEquals(audio.id, byCreatorId[audioCreator.id]!!.targetId)
|
assertEquals(audio.id, byCreatorNickname[audioCreator.nickname]!!.targetId)
|
||||||
assertEquals(RecommendedActivityType.LIVE_REPLAY, byCreatorId[replayCreator.id]!!.activityType)
|
assertEquals(RecommendedActivityType.LIVE_REPLAY, byCreatorNickname[replayCreator.nickname]!!.activityType)
|
||||||
assertEquals(replay.id, byCreatorId[replayCreator.id]!!.targetId)
|
assertEquals(replay.id, byCreatorNickname[replayCreator.nickname]!!.targetId)
|
||||||
assertEquals(RecommendedActivityType.COMMUNITY, byCreatorId[communityCreator.id]!!.activityType)
|
assertEquals(RecommendedActivityType.COMMUNITY, byCreatorNickname[communityCreator.nickname]!!.activityType)
|
||||||
assertEquals(community.id, byCreatorId[communityCreator.id]!!.targetId)
|
assertEquals(community.id, byCreatorNickname[communityCreator.nickname]!!.targetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -379,10 +390,15 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val hiddenCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = false)
|
val hiddenCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = false)
|
||||||
val visibleCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = true)
|
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(
|
assertEquals(
|
||||||
listOf(normalLiveCreator.id, adultLiveCreator.id, adultAudioCreator.id, adultCommunityCreator.id),
|
listOf(
|
||||||
visibleCreators.map { it.creatorId }
|
normalLiveCreator.nickname,
|
||||||
|
adultLiveCreator.nickname,
|
||||||
|
adultAudioCreator.nickname,
|
||||||
|
adultCommunityCreator.nickname
|
||||||
|
),
|
||||||
|
visibleCreators.map { it.creatorNickname }
|
||||||
)
|
)
|
||||||
assertEquals(null, visibleCreators[0].targetId)
|
assertEquals(null, visibleCreators[0].targetId)
|
||||||
assertEquals(null, visibleCreators[1].targetId)
|
assertEquals(null, visibleCreators[1].targetId)
|
||||||
@@ -412,7 +428,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
|
|
||||||
val creators = repository.findRecentlyActiveCreators(limit = 10, memberId = viewer.id)
|
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)
|
assertEquals(RecommendedActivityType.COMMUNITY, creators.single().activityType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,22 +949,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
|
|
||||||
val creators = repository.findRecentDebutCreators(now, limit = 10)
|
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(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
|
@Test
|
||||||
@@ -1014,12 +1015,12 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val page2 = repository.findRecentDebutCreators(now, offset = 2, limit = 1, includeAdultContents = false)
|
val page2 = repository.findRecentDebutCreators(now, offset = 2, limit = 1, includeAdultContents = false)
|
||||||
|
|
||||||
val pagedCreatorIds = (page0 + page1 + page2).map { it.creatorId }
|
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())
|
assertEquals(pagedCreatorIds, pagedCreatorIds.distinct())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("최근 데뷔 크리에이터 동점은 랜덤 tie-breaker 오름차순으로 정렬한다")
|
@DisplayName("최근 데뷔 크리에이터 동점은 DB 랜덤 tie-breaker로 정렬하고 조회 필드에는 노출하지 않는다")
|
||||||
fun shouldOrderRecentDebutCreatorsByRandomTieBreakerWhenScoresTie() {
|
fun shouldOrderRecentDebutCreatorsByRandomTieBreakerWhenScoresTie() {
|
||||||
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
val creator1 = saveMember("tie-debut-1", MemberRole.CREATOR)
|
val creator1 = saveMember("tie-debut-1", MemberRole.CREATOR)
|
||||||
@@ -1031,7 +1032,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val creators = repository.findRecentDebutCreators(now, limit = 10)
|
val creators = repository.findRecentDebutCreators(now, limit = 10)
|
||||||
|
|
||||||
assertEquals(2, creators.size)
|
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
|
@Test
|
||||||
@@ -1061,7 +1062,6 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val contents = repository.findFirstAudioContents(now, limit = 10)
|
val contents = repository.findFirstAudioContents(now, limit = 10)
|
||||||
|
|
||||||
assertEquals(listOf(eligibleActive.id), contents.map { it.contentId })
|
assertEquals(listOf(eligibleActive.id), contents.map { it.contentId })
|
||||||
assertEquals(100, contents.single().recencyScore)
|
|
||||||
assertEquals(12, contents.single().price)
|
assertEquals(12, contents.single().price)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1082,8 +1082,6 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val contents = repository.findFirstAudioContents(now, limit = 10)
|
val contents = repository.findFirstAudioContents(now, limit = 10)
|
||||||
|
|
||||||
assertEquals(listOf(fresh.id, old.id), contents.map { it.contentId })
|
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
|
@Test
|
||||||
@@ -1142,7 +1140,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
val page2 = repository.findFirstAudioContents(now, offset = 2, limit = 1, includeAdultContents = false)
|
val page2 = repository.findFirstAudioContents(now, offset = 2, limit = 1, includeAdultContents = false)
|
||||||
|
|
||||||
val pagedContentIds = (page0 + page1 + page2).map { it.contentId }
|
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())
|
assertEquals(pagedContentIds, pagedContentIds.distinct())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -604,31 +604,24 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
val liveRecommendations = listOf(
|
val liveRecommendations = listOf(
|
||||||
HomeLiveRecommendationRecord(
|
HomeLiveRecommendationRecord(
|
||||||
liveRoomId = 1L,
|
liveRoomId = 1L,
|
||||||
creatorId = 10L,
|
|
||||||
creatorNickname = "creator",
|
creatorNickname = "creator",
|
||||||
creatorProfileImage = "profile.png",
|
creatorProfileImage = "profile.png"
|
||||||
title = "live",
|
|
||||||
coverImage = "cover.png",
|
|
||||||
beginDateTime = LocalDateTime.of(2026, 5, 31, 10, 0),
|
|
||||||
channelName = "channel"
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val banners = listOf(
|
val banners = listOf(
|
||||||
HomeBannerRecommendationRecord(
|
HomeBannerRecommendationRecord(
|
||||||
bannerId = 2L,
|
|
||||||
type = "LINK",
|
|
||||||
thumbnailImage = "banner.png",
|
thumbnailImage = "banner.png",
|
||||||
eventId = null,
|
eventId = null,
|
||||||
|
eventThumbnailImage = null,
|
||||||
|
eventDetailImage = null,
|
||||||
|
eventLink = null,
|
||||||
creatorId = null,
|
creatorId = null,
|
||||||
seriesId = null,
|
seriesId = null,
|
||||||
link = "https://example.com",
|
link = "https://example.com"
|
||||||
orders = 1,
|
|
||||||
randomTieBreaker = 0.1
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val activeCreators = listOf(
|
val activeCreators = listOf(
|
||||||
RecentlyActiveCreatorRecord(
|
RecentlyActiveCreatorRecord(
|
||||||
creatorId = 10L,
|
|
||||||
creatorNickname = "creator",
|
creatorNickname = "creator",
|
||||||
creatorProfileImage = "profile.png",
|
creatorProfileImage = "profile.png",
|
||||||
activityType = RecommendedActivityType.LIVE,
|
activityType = RecommendedActivityType.LIVE,
|
||||||
@@ -640,10 +633,7 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
RecentDebutCreatorRecord(
|
RecentDebutCreatorRecord(
|
||||||
creatorId = 11L,
|
creatorId = 11L,
|
||||||
creatorNickname = "debut-creator",
|
creatorNickname = "debut-creator",
|
||||||
creatorProfileImage = "debut-profile.png",
|
creatorProfileImage = "debut-profile.png"
|
||||||
debutAt = LocalDateTime.of(2026, 5, 20, 10, 0),
|
|
||||||
score = 1.2,
|
|
||||||
randomTieBreaker = 0.2
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val firstAudioContents = listOf(
|
val firstAudioContents = listOf(
|
||||||
@@ -655,10 +645,7 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
title = "first-audio",
|
title = "first-audio",
|
||||||
price = 10,
|
price = 10,
|
||||||
coverImage = "first-audio.png",
|
coverImage = "first-audio.png",
|
||||||
releaseDate = LocalDateTime.of(2026, 5, 30, 10, 0),
|
isPointAvailable = true
|
||||||
isPointAvailable = true,
|
|
||||||
recencyScore = 100,
|
|
||||||
randomTieBreaker = 0.3
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
var aiCharacterDetails: List<HomeAiCharacterRecommendationRecord> = emptyList()
|
var aiCharacterDetails: List<HomeAiCharacterRecommendationRecord> = emptyList()
|
||||||
|
|||||||
Reference in New Issue
Block a user