feat(recommend): 홈 추천 조회 쿼리를 추가한다
This commit is contained in:
@@ -1,16 +1,427 @@
|
||||
package kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence
|
||||
|
||||
import com.querydsl.core.types.Projections
|
||||
import com.querydsl.core.types.dsl.BooleanExpression
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
||||
import kr.co.vividnext.sodalive.chat.original.QOriginalWork
|
||||
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
||||
import kr.co.vividnext.sodalive.chat.room.QChatMessage.chatMessage
|
||||
import kr.co.vividnext.sodalive.chat.room.QChatParticipant.chatParticipant
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
|
||||
import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
||||
import kr.co.vividnext.sodalive.event.QEvent.event
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommunity.creatorCommunity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike
|
||||
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||
import kr.co.vividnext.sodalive.member.QMember
|
||||
import kr.co.vividnext.sodalive.member.QMember.member
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScoreSpec
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeBannerRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeCheerCreatorRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeFirstAudioContentRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeLiveRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentDebutCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotRecord
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.sql.Timestamp
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@Repository
|
||||
class DefaultHomeRecommendationQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory,
|
||||
private val entityManager: EntityManager
|
||||
) : HomeRecommendationQueryRepository {
|
||||
override fun findLiveRecommendations(limit: Int): List<HomeLiveRecommendationRecord> {
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
HomeLiveRecommendationRecord::class.java,
|
||||
liveRoom.id,
|
||||
member.id,
|
||||
member.nickname,
|
||||
member.profileImage,
|
||||
liveRoom.title,
|
||||
liveRoom.coverImage,
|
||||
liveRoom.beginDateTime,
|
||||
liveRoom.channelName
|
||||
)
|
||||
)
|
||||
.from(liveRoom)
|
||||
.join(liveRoom.member, member)
|
||||
.where(
|
||||
liveRoom.isActive.isTrue,
|
||||
liveRoom.channelName.isNotNull,
|
||||
liveRoom.channelName.isNotEmpty,
|
||||
member.isActive.isTrue
|
||||
)
|
||||
.orderBy(liveRoom.beginDateTime.desc(), liveRoom.id.desc())
|
||||
.limit(limit.toLong())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord> {
|
||||
val bannerCreator = QMember("bannerCreator")
|
||||
val seriesOwner = QMember("seriesOwner")
|
||||
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')")
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
HomeBannerRecommendationRecord::class.java,
|
||||
audioContentBanner.id,
|
||||
audioContentBanner.type.stringValue(),
|
||||
audioContentBanner.thumbnailImage,
|
||||
event.id,
|
||||
bannerCreator.id,
|
||||
series.id,
|
||||
audioContentBanner.link,
|
||||
audioContentBanner.orders,
|
||||
randomTieBreaker
|
||||
)
|
||||
)
|
||||
.from(audioContentBanner)
|
||||
.leftJoin(audioContentBanner.event, event)
|
||||
.leftJoin(audioContentBanner.creator, bannerCreator)
|
||||
.leftJoin(audioContentBanner.series, series)
|
||||
.leftJoin(series.member, seriesOwner)
|
||||
.where(
|
||||
audioContentBanner.isActive.isTrue,
|
||||
audioContentBanner.tab.isNull,
|
||||
activeBannerTargetCondition(bannerCreator, seriesOwner)
|
||||
)
|
||||
.orderBy(audioContentBanner.orders.asc(), randomTieBreaker.asc())
|
||||
.limit(limit.toLong())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findRecentlyActiveCreators(limit: Int): List<RecentlyActiveCreatorRecord> {
|
||||
val sql = """
|
||||
select ranked.creator_id,
|
||||
ranked.creator_nickname,
|
||||
ranked.creator_profile_image,
|
||||
ranked.activity_type,
|
||||
ranked.activity_at,
|
||||
ranked.target_id
|
||||
from (
|
||||
select activities.*,
|
||||
row_number() over (
|
||||
partition by activities.creator_id
|
||||
order by activities.activity_at desc, activities.target_sort_id desc
|
||||
) as creator_rank
|
||||
from (
|
||||
select m.id as creator_id,
|
||||
m.nickname as creator_nickname,
|
||||
m.profile_image as creator_profile_image,
|
||||
'LIVE' as activity_type,
|
||||
lr.begin_date_time as activity_at,
|
||||
null as target_id,
|
||||
lr.id as target_sort_id
|
||||
from live_room lr
|
||||
join member m on m.id = lr.member_id
|
||||
where lr.is_active = true
|
||||
and lr.channel_name is not null
|
||||
and lr.channel_name <> ''
|
||||
and m.is_active = true
|
||||
union all
|
||||
select m.id as creator_id,
|
||||
m.nickname as creator_nickname,
|
||||
m.profile_image as creator_profile_image,
|
||||
case when act.theme = :liveReplayTheme then 'LIVE_REPLAY' else 'AUDIO' end as activity_type,
|
||||
ac.release_date as activity_at,
|
||||
ac.id as target_id,
|
||||
ac.id as target_sort_id
|
||||
from content ac
|
||||
join member m on m.id = ac.member_id
|
||||
join content_theme act on act.id = ac.theme_id
|
||||
where ac.is_active = true
|
||||
and ac.release_date is not null
|
||||
and m.is_active = true
|
||||
union all
|
||||
select m.id as creator_id,
|
||||
m.nickname as creator_nickname,
|
||||
m.profile_image as creator_profile_image,
|
||||
'COMMUNITY' as activity_type,
|
||||
cc.created_at as activity_at,
|
||||
cc.id as target_id,
|
||||
cc.id as target_sort_id
|
||||
from creator_community cc
|
||||
join member m on m.id = cc.member_id
|
||||
where cc.is_active = true
|
||||
and m.is_active = true
|
||||
) activities
|
||||
) ranked
|
||||
where ranked.creator_rank = 1
|
||||
order by ranked.activity_at desc, ranked.target_sort_id desc
|
||||
limit :limit
|
||||
""".trimIndent()
|
||||
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setParameter("liveReplayTheme", LIVE_REPLAY_THEME)
|
||||
.setParameter("limit", limit)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val rows = query.resultList as List<Array<Any?>>
|
||||
|
||||
return rows.map { row ->
|
||||
RecentlyActiveCreatorRecord(
|
||||
creatorId = (row[0] as Number).toLong(),
|
||||
creatorNickname = row[1] as String,
|
||||
creatorProfileImage = row[2] as String?,
|
||||
activityType = RecommendedActivityType.valueOf(row[3] as String),
|
||||
activityAt = toLocalDateTime(row[4]),
|
||||
targetId = (row[5] as Number?)?.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findRecentDebutCreators(now: LocalDateTime, limit: Int): List<RecentDebutCreatorRecord> {
|
||||
val sql = """
|
||||
with creator_debut as (
|
||||
select debut_events.creator_id as creator_id, min(debut_events.debut_at) as debut_at
|
||||
from (
|
||||
select ac.member_id as creator_id, ac.release_date as debut_at
|
||||
from content ac
|
||||
where ac.is_active = true
|
||||
and ac.release_date is not null
|
||||
and ac.release_date <= :now
|
||||
union all
|
||||
select lr.member_id as creator_id, lr.begin_date_time as debut_at
|
||||
from live_room lr
|
||||
where lr.is_active = true
|
||||
and lr.channel_name is not null
|
||||
and lr.channel_name <> ''
|
||||
and lr.begin_date_time <= :now
|
||||
) debut_events
|
||||
group by debut_events.creator_id
|
||||
),
|
||||
follow_stats as (
|
||||
select cf.creator_id as creator_id, count(distinct cf.id) as follow_increase
|
||||
from creator_following cf
|
||||
where cf.is_active = true
|
||||
and cf.created_at >= :window7Start
|
||||
and cf.created_at <= :now
|
||||
group by cf.creator_id
|
||||
),
|
||||
content_stats as (
|
||||
select activity.creator_id as creator_id, count(activity.activity_id) as content_activity_score
|
||||
from (
|
||||
select ac.member_id as creator_id, ac.id as activity_id
|
||||
from content ac
|
||||
where ac.is_active = true
|
||||
and ac.release_date is not null
|
||||
and ac.release_date >= :window30Start
|
||||
and ac.release_date <= :now
|
||||
union all
|
||||
select lr.member_id as creator_id, lr.id as activity_id
|
||||
from live_room lr
|
||||
where lr.is_active = true
|
||||
and lr.channel_name is not null
|
||||
and lr.channel_name <> ''
|
||||
and lr.begin_date_time >= :window30Start
|
||||
and lr.begin_date_time <= :now
|
||||
) activity
|
||||
group by activity.creator_id
|
||||
),
|
||||
communication_stats as (
|
||||
select communication.creator_id as creator_id, count(communication.activity_id) as communication_score
|
||||
from (
|
||||
select cc.member_id as creator_id, cc.id as activity_id
|
||||
from creator_community cc
|
||||
where cc.is_active = true
|
||||
and cc.created_at >= :window7Start
|
||||
and cc.created_at <= :now
|
||||
union all
|
||||
select cc.member_id as creator_id, ccc.id as activity_id
|
||||
from creator_community_comment ccc
|
||||
join creator_community cc on cc.id = ccc.creator_community_id
|
||||
where ccc.is_active = true
|
||||
and cc.is_active = true
|
||||
and ccc.created_at >= :window7Start
|
||||
and ccc.created_at <= :now
|
||||
union all
|
||||
select cc.member_id as creator_id, ccl.id as activity_id
|
||||
from creator_community_like ccl
|
||||
join creator_community cc on cc.id = ccl.creator_community_id
|
||||
where ccl.is_active = true
|
||||
and cc.is_active = true
|
||||
and ccl.created_at >= :window7Start
|
||||
and ccl.created_at <= :now
|
||||
union all
|
||||
select ac.member_id as creator_id, acc.id as activity_id
|
||||
from content_comment acc
|
||||
join content ac on ac.id = acc.content_id
|
||||
where acc.is_active = true
|
||||
and ac.is_active = true
|
||||
and acc.created_at >= :window7Start
|
||||
and acc.created_at <= :now
|
||||
union all
|
||||
select ac.member_id as creator_id, acl.id as activity_id
|
||||
from content_like acl
|
||||
join content ac on ac.id = acl.content_id
|
||||
where acl.is_active = true
|
||||
and ac.is_active = true
|
||||
and acl.created_at >= :window7Start
|
||||
and acl.created_at <= :now
|
||||
) communication
|
||||
group by communication.creator_id
|
||||
)
|
||||
select m.id as creator_id,
|
||||
m.nickname as creator_nickname,
|
||||
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,
|
||||
rand() as random_tie_breaker
|
||||
from member m
|
||||
join creator_debut cd on cd.creator_id = m.id
|
||||
left join follow_stats fs on fs.creator_id = m.id
|
||||
left join content_stats cs on cs.creator_id = m.id
|
||||
left join communication_stats cms on cms.creator_id = m.id
|
||||
where m.is_active = true
|
||||
and cd.debut_at >= :boost30Start
|
||||
and cd.debut_at <= :now
|
||||
order by score desc, random_tie_breaker asc
|
||||
limit :limit
|
||||
""".trimIndent()
|
||||
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setRecommendationQueryParameters(now, limit)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val rows = query.resultList as List<Array<Any?>>
|
||||
|
||||
return rows.map { row ->
|
||||
RecentDebutCreatorRecord(
|
||||
creatorId = (row[0] as Number).toLong(),
|
||||
creatorNickname = row[1] as String,
|
||||
creatorProfileImage = row[2] as String?,
|
||||
debutAt = toLocalDateTime(row[3]),
|
||||
score = (row[4] as Number).toDouble(),
|
||||
randomTieBreaker = (row[5] as Number).toDouble()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findFirstAudioContents(now: LocalDateTime, limit: Int): List<HomeFirstAudioContentRecord> {
|
||||
val sql = """
|
||||
with creator_debut as (
|
||||
select debut_events.creator_id as creator_id, min(debut_events.debut_at) as debut_at
|
||||
from (
|
||||
select ac.member_id as creator_id, ac.release_date as debut_at
|
||||
from content ac
|
||||
where ac.is_active = true
|
||||
and ac.release_date is not null
|
||||
and ac.release_date <= :now
|
||||
union all
|
||||
select lr.member_id as creator_id, lr.begin_date_time as debut_at
|
||||
from live_room lr
|
||||
where lr.is_active = true
|
||||
and lr.channel_name is not null
|
||||
and lr.channel_name <> ''
|
||||
and lr.begin_date_time <= :now
|
||||
) debut_events
|
||||
group by debut_events.creator_id
|
||||
),
|
||||
ranked_uploads as (
|
||||
select ac.id as content_id,
|
||||
ac.member_id as creator_id,
|
||||
ac.title as title,
|
||||
ac.cover_image as cover_image,
|
||||
ac.release_date as release_date,
|
||||
ac.is_active as is_active,
|
||||
row_number() over (
|
||||
partition by ac.member_id
|
||||
order by ac.created_at asc, ac.release_date asc, ac.id asc
|
||||
) as upload_rank
|
||||
from content ac
|
||||
where ac.release_date is not null
|
||||
),
|
||||
eligible_contents as (
|
||||
select ranked_uploads.*,
|
||||
row_number() over (
|
||||
partition by ranked_uploads.creator_id
|
||||
order by ranked_uploads.upload_rank asc
|
||||
) as active_rank
|
||||
from ranked_uploads
|
||||
where ranked_uploads.upload_rank <= 3
|
||||
and ranked_uploads.is_active = true
|
||||
and ranked_uploads.release_date <= :now
|
||||
)
|
||||
select ec.content_id as content_id,
|
||||
m.id as creator_id,
|
||||
m.nickname as creator_nickname,
|
||||
m.profile_image as creator_profile_image,
|
||||
ec.title as title,
|
||||
ec.cover_image as cover_image,
|
||||
ec.release_date as release_date,
|
||||
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,
|
||||
rand() as random_tie_breaker
|
||||
from eligible_contents ec
|
||||
join member m on m.id = ec.creator_id
|
||||
join creator_debut cd on cd.creator_id = ec.creator_id
|
||||
where ec.active_rank = 1
|
||||
and m.is_active = true
|
||||
and cd.debut_at >= :boost30Start
|
||||
and cd.debut_at <= :now
|
||||
and ec.release_date >= :boost30Start
|
||||
order by recency_score desc, random_tie_breaker asc
|
||||
limit :limit
|
||||
""".trimIndent()
|
||||
|
||||
val query = entityManager.createNativeQuery(sql)
|
||||
.setParameter("now", now)
|
||||
.setParameter("limit", limit)
|
||||
.setParameter(
|
||||
"boost30Start",
|
||||
now.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_30_DAY_LIMIT).atStartOfDay()
|
||||
)
|
||||
.setParameter("recency3Start", now.toLocalDate().minusDays(3).atStartOfDay())
|
||||
.setParameter("recency7Start", now.toLocalDate().minusDays(7).atStartOfDay())
|
||||
.setParameter("recency14Start", now.toLocalDate().minusDays(14).atStartOfDay())
|
||||
.setParameter("recency21Start", now.toLocalDate().minusDays(21).atStartOfDay())
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val rows = query.resultList as List<Array<Any?>>
|
||||
|
||||
return rows.map { row ->
|
||||
HomeFirstAudioContentRecord(
|
||||
contentId = (row[0] as Number).toLong(),
|
||||
creatorId = (row[1] as Number).toLong(),
|
||||
creatorNickname = row[2] as String,
|
||||
creatorProfileImage = row[3] as String?,
|
||||
title = row[4] as String,
|
||||
coverImage = row[5] as String?,
|
||||
releaseDate = toLocalDateTime(row[6]),
|
||||
recencyScore = (row[7] as Number).toInt(),
|
||||
randomTieBreaker = (row[8] as Number).toDouble()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findAiCharacterSnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
snapshotAt: LocalDateTime,
|
||||
@@ -102,6 +513,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
from live_room lr
|
||||
where lr.is_active = true
|
||||
and lr.channel_name is not null
|
||||
and lr.channel_name <> ''
|
||||
and lr.begin_date_time <= :snapshotAt
|
||||
) debut_events
|
||||
group by debut_events.creator_id
|
||||
@@ -171,6 +583,7 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
from live_room lr
|
||||
where lr.is_active = true
|
||||
and lr.channel_name is not null
|
||||
and lr.channel_name <> ''
|
||||
and lr.begin_date_time <= :snapshotAt
|
||||
) debut_events
|
||||
group by debut_events.creator_id
|
||||
@@ -227,6 +640,107 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
return executeSnapshotQuery(sql, RecommendedSectionType.POPULAR_COMMUNITY, windowStart, snapshotAt, limit)
|
||||
}
|
||||
|
||||
override fun findAiCharacterRecommendationDetails(
|
||||
characterIds: List<Long>
|
||||
): List<HomeAiCharacterRecommendationRecord> {
|
||||
if (characterIds.isEmpty()) return emptyList()
|
||||
val linkedOriginalWork = QOriginalWork("linkedOriginalWork")
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
HomeAiCharacterRecommendationRecord::class.java,
|
||||
chatCharacter.id,
|
||||
chatCharacter.name,
|
||||
chatCharacter.description,
|
||||
chatMessage.id.count(),
|
||||
linkedOriginalWork.title
|
||||
)
|
||||
)
|
||||
.from(chatCharacter)
|
||||
.leftJoin(chatCharacter.originalWork, linkedOriginalWork).on(linkedOriginalWork.isDeleted.isFalse)
|
||||
.leftJoin(chatParticipant).on(
|
||||
chatParticipant.character.id.eq(chatCharacter.id),
|
||||
chatParticipant.participantType.eq(ParticipantType.CHARACTER),
|
||||
chatParticipant.isActive.isTrue
|
||||
)
|
||||
.leftJoin(chatMessage).on(
|
||||
chatMessage.participant.id.eq(chatParticipant.id),
|
||||
chatMessage.isActive.isTrue
|
||||
)
|
||||
.where(chatCharacter.isActive.isTrue, chatCharacter.id.`in`(characterIds))
|
||||
.groupBy(chatCharacter.id, chatCharacter.name, chatCharacter.description, linkedOriginalWork.title)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findCheerCreatorRecommendationDetails(
|
||||
creatorIds: List<Long>
|
||||
): List<HomeCheerCreatorRecommendationRecord> {
|
||||
if (creatorIds.isEmpty()) return emptyList()
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
HomeCheerCreatorRecommendationRecord::class.java,
|
||||
member.id,
|
||||
member.nickname,
|
||||
member.profileImage
|
||||
)
|
||||
)
|
||||
.from(member)
|
||||
.where(member.isActive.isTrue, member.id.`in`(creatorIds))
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findPopularCommunityRecommendationDetails(
|
||||
communityIds: List<Long>,
|
||||
includeAdultCommunities: Boolean
|
||||
): List<HomePopularCommunityRecommendationRecord> {
|
||||
if (communityIds.isEmpty()) return emptyList()
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
HomePopularCommunityRecommendationRecord::class.java,
|
||||
creatorCommunity.id,
|
||||
member.id,
|
||||
member.nickname,
|
||||
member.profileImage,
|
||||
creatorCommunity.content,
|
||||
creatorCommunity.createdAt,
|
||||
creatorCommunityLike.id.countDistinct(),
|
||||
creatorCommunityComment.id.countDistinct()
|
||||
)
|
||||
)
|
||||
.from(creatorCommunity)
|
||||
.join(creatorCommunity.member, member)
|
||||
.leftJoin(creatorCommunityLike).on(
|
||||
creatorCommunityLike.creatorCommunity.id.eq(creatorCommunity.id),
|
||||
creatorCommunityLike.isActive.isTrue
|
||||
)
|
||||
.leftJoin(creatorCommunityComment).on(
|
||||
creatorCommunityComment.creatorCommunity.id.eq(creatorCommunity.id),
|
||||
creatorCommunityComment.isActive.isTrue
|
||||
)
|
||||
.where(
|
||||
creatorCommunity.isActive.isTrue,
|
||||
member.isActive.isTrue,
|
||||
creatorCommunity.price.eq(0),
|
||||
creatorCommunity.isFixed.isFalse,
|
||||
includeAdultCommunityCondition(includeAdultCommunities),
|
||||
creatorCommunity.id.`in`(communityIds)
|
||||
)
|
||||
.groupBy(
|
||||
creatorCommunity.id,
|
||||
member.id,
|
||||
member.nickname,
|
||||
member.profileImage,
|
||||
creatorCommunity.content,
|
||||
creatorCommunity.createdAt
|
||||
)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun executeSnapshotQuery(
|
||||
sql: String,
|
||||
sectionType: RecommendedSectionType,
|
||||
@@ -264,4 +778,56 @@ class DefaultHomeRecommendationQueryRepository(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun activeBannerTargetCondition(
|
||||
bannerCreator: QMember,
|
||||
seriesOwner: QMember
|
||||
): BooleanExpression {
|
||||
return audioContentBanner.type.eq(AudioContentBannerType.LINK)
|
||||
.or(audioContentBanner.type.eq(AudioContentBannerType.EVENT).and(event.isActive.isTrue))
|
||||
.or(audioContentBanner.type.eq(AudioContentBannerType.CREATOR).and(bannerCreator.isActive.isTrue))
|
||||
.or(
|
||||
audioContentBanner.type.eq(AudioContentBannerType.SERIES)
|
||||
.and(series.isActive.isTrue)
|
||||
.and(seriesOwner.isActive.isTrue)
|
||||
)
|
||||
}
|
||||
|
||||
private fun includeAdultCommunityCondition(includeAdultCommunities: Boolean): BooleanExpression? {
|
||||
return if (includeAdultCommunities) null else creatorCommunity.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun javax.persistence.Query.setRecommendationQueryParameters(
|
||||
now: LocalDateTime,
|
||||
limit: Int
|
||||
): javax.persistence.Query {
|
||||
return setParameter("now", now)
|
||||
.setParameter("window7Start", now.toLocalDate().minusDays(7).atStartOfDay())
|
||||
.setParameter("window30Start", now.toLocalDate().minusDays(30).atStartOfDay())
|
||||
.setParameter("limit", limit)
|
||||
.setParameter(
|
||||
"boost10Start",
|
||||
now.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_10_DAY_LIMIT).atStartOfDay()
|
||||
)
|
||||
.setParameter(
|
||||
"boost20Start",
|
||||
now.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_20_DAY_LIMIT).atStartOfDay()
|
||||
)
|
||||
.setParameter(
|
||||
"boost30Start",
|
||||
now.toLocalDate().minusDays(RecommendationScoreSpec.NEW_BOOST_30_DAY_LIMIT).atStartOfDay()
|
||||
)
|
||||
}
|
||||
|
||||
private fun toLocalDateTime(value: Any?): LocalDateTime {
|
||||
return when (value) {
|
||||
is LocalDateTime -> value
|
||||
is Timestamp -> value.toLocalDateTime()
|
||||
else -> error("Unsupported LocalDateTime value: $value")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LIVE_REPLAY_THEME = "다시듣기"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user