feat(content-all): 전체 탭 QueryDSL 조회를 추가한다
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
package kr.co.vividnext.sodalive.v2.content.all.adapter.out.persistence
|
||||
|
||||
import com.querydsl.core.Tuple
|
||||
import com.querydsl.core.types.Expression
|
||||
import com.querydsl.core.types.dsl.BooleanExpression
|
||||
import com.querydsl.core.types.dsl.CaseBuilder
|
||||
import com.querydsl.jpa.JPAExpressions
|
||||
import com.querydsl.jpa.impl.JPAQuery
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||
import kr.co.vividnext.sodalive.content.order.QOrder
|
||||
import kr.co.vividnext.sodalive.content.series.translation.QSeriesTranslation
|
||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme
|
||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||
import kr.co.vividnext.sodalive.member.QMember
|
||||
import kr.co.vividnext.sodalive.member.QMember.member
|
||||
import kr.co.vividnext.sodalive.member.block.QBlockMember
|
||||
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
|
||||
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
||||
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllAudio
|
||||
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllSeries
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class DefaultMainContentAllQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory,
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) : MainContentAllQueryRepository {
|
||||
override fun countAudios(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
onlyFree: Boolean,
|
||||
onlyPointAvailable: Boolean
|
||||
): Int {
|
||||
return queryFactory
|
||||
.select(audioContent.id.count())
|
||||
.from(audioContent)
|
||||
.join(audioContent.member, member)
|
||||
.join(audioContent.theme, audioContentTheme)
|
||||
.where(audioCondition(memberId, canViewAdultContent, now, onlyFree, onlyPointAvailable))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun findAudios(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
sort: ContentSort,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
onlyFree: Boolean,
|
||||
onlyPointAvailable: Boolean
|
||||
): List<MainContentAllAudio> {
|
||||
val rows = findAudioRows(memberId, canViewAdultContent, now, sort, offset, limit, onlyFree, onlyPointAvailable)
|
||||
if (rows.isEmpty()) return emptyList()
|
||||
|
||||
val contentIds = rows.map { it.get(audioContent.id)!! }
|
||||
val creatorIds = rows.map { it.get(audioContent.member.id)!! }.distinct()
|
||||
val firstContentIdByCreatorId = firstAudioContentIds(creatorIds, now, canViewAdultContent)
|
||||
val originalSeriesByContentId = originalSeriesFlags(contentIds)
|
||||
|
||||
return rows.map { row ->
|
||||
val contentId = row.get(audioContent.id)!!
|
||||
val creatorId = row.get(audioContent.member.id)!!
|
||||
MainContentAllAudio(
|
||||
audioContentId = contentId,
|
||||
title = row.get(audioContent.title)!!,
|
||||
imageUrl = row.get(audioContent.coverImage).toCdnUrl(cloudFrontHost),
|
||||
price = row.get(audioContent.price)!!,
|
||||
isAdult = row.get(audioContent.isAdult)!!,
|
||||
isPointAvailable = row.get(audioContent.isPointAvailable)!!,
|
||||
isFirstContent = firstContentIdByCreatorId[creatorId] == contentId,
|
||||
isOriginalSeries = originalSeriesByContentId[contentId] ?: false,
|
||||
creatorNickname = row.get(member.nickname)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun countSeries(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
onlyOriginal: Boolean,
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek?
|
||||
): Int {
|
||||
return queryFactory
|
||||
.select(series.id.count())
|
||||
.from(series)
|
||||
.join(series.member, member)
|
||||
.where(seriesCondition(memberId, canViewAdultContent, onlyOriginal, dayOfWeek))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun findSeries(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
sort: ContentSort,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
onlyOriginal: Boolean,
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek?,
|
||||
locale: String
|
||||
): List<MainContentAllSeries> {
|
||||
val seriesIds = findSeriesIds(memberId, canViewAdultContent, now, sort, offset, limit, onlyOriginal, dayOfWeek)
|
||||
if (seriesIds.isEmpty()) return emptyList()
|
||||
|
||||
val seriesTranslation = QSeriesTranslation("mainContentAllSeriesTranslation")
|
||||
return queryFactory
|
||||
.select(
|
||||
series.id,
|
||||
series.title,
|
||||
seriesTranslation.renderedPayload,
|
||||
series.coverImage,
|
||||
member.nickname,
|
||||
series.isOriginal,
|
||||
series.isAdult
|
||||
)
|
||||
.from(series)
|
||||
.join(series.member, member)
|
||||
.leftJoin(seriesTranslation)
|
||||
.on(
|
||||
seriesTranslation.seriesId.eq(series.id),
|
||||
seriesTranslation.locale.eq(locale)
|
||||
)
|
||||
.where(series.id.`in`(seriesIds))
|
||||
.fetch()
|
||||
.sortedBy { seriesIds.indexOf(it.get(series.id)!!) }
|
||||
.map { row ->
|
||||
val translatedTitle = row.get(seriesTranslation.renderedPayload)?.title
|
||||
MainContentAllSeries(
|
||||
seriesId = row.get(series.id)!!,
|
||||
title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: row.get(series.title)!!,
|
||||
coverImageUrl = row.get(series.coverImage).toCdnUrl(cloudFrontHost),
|
||||
creatorNickname = row.get(member.nickname)!!,
|
||||
isOriginal = row.get(series.isOriginal)!!,
|
||||
isAdult = row.get(series.isAdult)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findAudioRows(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
sort: ContentSort,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
onlyFree: Boolean,
|
||||
onlyPointAvailable: Boolean
|
||||
): List<Tuple> {
|
||||
val query = queryFactory
|
||||
.select(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.coverImage,
|
||||
audioContent.price,
|
||||
audioContent.isAdult,
|
||||
audioContent.isPointAvailable,
|
||||
audioContent.member.id,
|
||||
member.nickname,
|
||||
audioContent.releaseDate
|
||||
)
|
||||
.from(audioContent)
|
||||
.join(audioContent.member, member)
|
||||
.join(audioContent.theme, audioContentTheme)
|
||||
.where(audioCondition(memberId, canViewAdultContent, now, onlyFree, onlyPointAvailable))
|
||||
|
||||
when (sort) {
|
||||
ContentSort.POPULAR -> {
|
||||
val revenueOrder = QOrder("mainContentAllAudioRevenueOrder")
|
||||
query
|
||||
.leftJoin(revenueOrder)
|
||||
.on(
|
||||
revenueOrder.audioContent.id.eq(audioContent.id),
|
||||
revenueOrder.isActive.isTrue
|
||||
)
|
||||
.groupByAudioRow()
|
||||
.orderBy(
|
||||
revenueOrder.can.sum().coalesce(0).desc(),
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.id.desc()
|
||||
)
|
||||
}
|
||||
ContentSort.PRICE_HIGH -> query.orderBy(
|
||||
audioContent.price.desc(),
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.id.desc()
|
||||
)
|
||||
ContentSort.PRICE_LOW -> query.orderBy(
|
||||
audioContent.price.asc(),
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.id.desc()
|
||||
)
|
||||
ContentSort.LATEST,
|
||||
ContentSort.OWNED -> query.orderBy(
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.id.desc()
|
||||
)
|
||||
}
|
||||
|
||||
return query.offset(offset).limit(limit.toLong()).fetch()
|
||||
}
|
||||
|
||||
private fun findSeriesIds(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
sort: ContentSort,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
onlyOriginal: Boolean,
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek?
|
||||
): List<Long> {
|
||||
val audioCreator = QMember("mainContentAllSeriesAudioCreator")
|
||||
val audioTheme = QAudioContentTheme("mainContentAllSeriesAudioTheme")
|
||||
val revenueOrder = QOrder("mainContentAllSeriesRevenueOrder")
|
||||
val publicSeriesAudioCondition = publicSeriesAudioCondition(canViewAdultContent, now, audioCreator, audioTheme)
|
||||
val latestReleaseDate = CaseBuilder()
|
||||
.`when`(publicSeriesAudioCondition)
|
||||
.then(audioContent.releaseDate)
|
||||
.otherwise(null as LocalDateTime?)
|
||||
.max()
|
||||
val highestPrice = CaseBuilder()
|
||||
.`when`(publicSeriesAudioCondition)
|
||||
.then(audioContent.price)
|
||||
.otherwise(null as Int?)
|
||||
.max()
|
||||
val lowestPrice = CaseBuilder()
|
||||
.`when`(publicSeriesAudioCondition)
|
||||
.then(audioContent.price)
|
||||
.otherwise(null as Int?)
|
||||
.min()
|
||||
val revenue = CaseBuilder()
|
||||
.`when`(publicSeriesAudioCondition)
|
||||
.then(revenueOrder.can)
|
||||
.otherwise(0)
|
||||
.sum()
|
||||
.coalesce(0)
|
||||
val latestReleaseDateNullLast = CaseBuilder().`when`(latestReleaseDate.isNull).then(1).otherwise(0)
|
||||
val highestPriceNullLast = CaseBuilder().`when`(highestPrice.isNull).then(1).otherwise(0)
|
||||
val lowestPriceNullLast = CaseBuilder().`when`(lowestPrice.isNull).then(1).otherwise(0)
|
||||
|
||||
val query = queryFactory
|
||||
.select(series.id)
|
||||
.from(series)
|
||||
.join(series.member, member)
|
||||
.leftJoin(seriesContent).on(seriesContent.series.id.eq(series.id))
|
||||
.leftJoin(audioContent).on(seriesContent.content.id.eq(audioContent.id))
|
||||
.leftJoin(audioContent.member, audioCreator)
|
||||
.leftJoin(audioContent.theme, audioTheme)
|
||||
.where(seriesCondition(memberId, canViewAdultContent, onlyOriginal, dayOfWeek))
|
||||
.groupBy(series.id)
|
||||
|
||||
when (sort) {
|
||||
ContentSort.POPULAR ->
|
||||
query
|
||||
.leftJoin(revenueOrder)
|
||||
.on(
|
||||
revenueOrder.audioContent.id.eq(audioContent.id),
|
||||
revenueOrder.isActive.isTrue
|
||||
)
|
||||
.orderBy(revenue.desc(), latestReleaseDate.desc(), series.id.desc())
|
||||
ContentSort.PRICE_HIGH -> query.orderBy(
|
||||
highestPriceNullLast.asc(),
|
||||
highestPrice.desc(),
|
||||
latestReleaseDate.desc(),
|
||||
series.id.desc()
|
||||
)
|
||||
ContentSort.PRICE_LOW -> query.orderBy(
|
||||
lowestPriceNullLast.asc(),
|
||||
lowestPrice.asc(),
|
||||
latestReleaseDate.desc(),
|
||||
series.id.desc()
|
||||
)
|
||||
ContentSort.LATEST,
|
||||
ContentSort.OWNED -> query.orderBy(
|
||||
latestReleaseDateNullLast.asc(),
|
||||
latestReleaseDate.desc(),
|
||||
series.id.desc()
|
||||
)
|
||||
}
|
||||
|
||||
return query.offset(offset).limit(limit.toLong()).fetch()
|
||||
}
|
||||
|
||||
private fun JPAQuery<Tuple>.groupByAudioRow(): JPAQuery<Tuple> {
|
||||
return groupBy(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.coverImage,
|
||||
audioContent.price,
|
||||
audioContent.isAdult,
|
||||
audioContent.isPointAvailable,
|
||||
audioContent.member.id,
|
||||
member.nickname,
|
||||
audioContent.releaseDate
|
||||
)
|
||||
}
|
||||
|
||||
private fun firstAudioContentIds(
|
||||
creatorIds: List<Long>,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Map<Long, Long> {
|
||||
return creatorIds.associateWith { creatorId ->
|
||||
queryFactory
|
||||
.select(audioContent.id)
|
||||
.from(audioContent)
|
||||
.join(audioContent.member, member)
|
||||
.join(audioContent.theme, audioContentTheme)
|
||||
.where(
|
||||
audioContent.member.id.eq(creatorId),
|
||||
publicAudioCondition(canViewAdultContent, now)
|
||||
)
|
||||
.orderBy(audioContent.releaseDate.asc(), audioContent.id.asc())
|
||||
.fetchFirst()
|
||||
}.filterValues { it != null }.mapValues { it.value!! }
|
||||
}
|
||||
|
||||
private fun originalSeriesFlags(contentIds: List<Long>): Map<Long, Boolean> {
|
||||
if (contentIds.isEmpty()) return emptyMap()
|
||||
return queryFactory
|
||||
.select(seriesContent.content.id, series.isOriginal)
|
||||
.from(seriesContent)
|
||||
.join(seriesContent.series, series)
|
||||
.where(seriesContent.content.id.`in`(contentIds))
|
||||
.fetch()
|
||||
.associate { it.get(seriesContent.content.id)!! to it.get(series.isOriginal)!! }
|
||||
}
|
||||
|
||||
private fun audioCondition(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
onlyFree: Boolean,
|
||||
onlyPointAvailable: Boolean
|
||||
): BooleanExpression {
|
||||
return publicAudioCondition(canViewAdultContent, now)
|
||||
.and(optionalAudioFreeCondition(onlyFree))
|
||||
.and(optionalAudioPointCondition(onlyPointAvailable))
|
||||
.withOptionalAnd(notBlockedCreatorCondition(memberId, audioContent.member.id))
|
||||
}
|
||||
|
||||
private fun publicAudioCondition(canViewAdultContent: Boolean, now: LocalDateTime): BooleanExpression {
|
||||
return audioContent.isActive.isTrue
|
||||
.and(audioContent.duration.isNotNull)
|
||||
.and(audioContent.releaseDate.isNotNull)
|
||||
.and(audioContent.releaseDate.loe(now))
|
||||
.and(audioContent.member.isActive.isTrue)
|
||||
.and(audioContentTheme.isActive.isTrue)
|
||||
.withOptionalAnd(adultAudioCondition(canViewAdultContent))
|
||||
}
|
||||
|
||||
private fun publicSeriesAudioCondition(
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
audioCreator: QMember,
|
||||
audioTheme: QAudioContentTheme
|
||||
): BooleanExpression {
|
||||
return audioContent.isActive.isTrue
|
||||
.and(audioContent.duration.isNotNull)
|
||||
.and(audioContent.releaseDate.isNotNull)
|
||||
.and(audioContent.releaseDate.loe(now))
|
||||
.and(audioCreator.isActive.isTrue)
|
||||
.and(audioTheme.isActive.isTrue)
|
||||
.withOptionalAnd(adultAudioCondition(canViewAdultContent))
|
||||
}
|
||||
|
||||
private fun seriesCondition(
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
onlyOriginal: Boolean,
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek?
|
||||
): BooleanExpression {
|
||||
return series.isActive.isTrue
|
||||
.and(member.isActive.isTrue)
|
||||
.and(optionalOriginalCondition(onlyOriginal))
|
||||
.withOptionalAnd(dayOfWeekCondition(dayOfWeek))
|
||||
.withOptionalAnd(adultSeriesCondition(canViewAdultContent))
|
||||
.withOptionalAnd(notBlockedCreatorCondition(memberId, series.member.id))
|
||||
}
|
||||
|
||||
private fun optionalAudioFreeCondition(onlyFree: Boolean): BooleanExpression? {
|
||||
return if (onlyFree) audioContent.price.eq(0) else null
|
||||
}
|
||||
|
||||
private fun optionalAudioPointCondition(onlyPointAvailable: Boolean): BooleanExpression? {
|
||||
return if (onlyPointAvailable) audioContent.isPointAvailable.isTrue else null
|
||||
}
|
||||
|
||||
private fun optionalOriginalCondition(onlyOriginal: Boolean): BooleanExpression? {
|
||||
return if (onlyOriginal) series.isOriginal.isTrue else null
|
||||
}
|
||||
|
||||
private fun dayOfWeekCondition(dayOfWeek: SeriesPublishedDaysOfWeek?): BooleanExpression? {
|
||||
return dayOfWeek?.let { series.publishedDaysOfWeek.contains(it) }
|
||||
}
|
||||
|
||||
private fun adultAudioCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else audioContent.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun adultSeriesCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else series.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun notBlockedCreatorCondition(memberId: Long?, creatorIdPath: Expression<Long>): BooleanExpression? {
|
||||
if (memberId == null) return null
|
||||
val blockMember = QBlockMember("mainContentAllBlockMember")
|
||||
return JPAExpressions
|
||||
.selectOne()
|
||||
.from(blockMember)
|
||||
.where(
|
||||
blockMember.isActive.isTrue,
|
||||
blockMember.member.id.eq(memberId).and(blockMember.blockedMember.id.eq(creatorIdPath))
|
||||
.or(blockMember.member.id.eq(creatorIdPath).and(blockMember.blockedMember.id.eq(memberId)))
|
||||
)
|
||||
.notExists()
|
||||
}
|
||||
|
||||
private fun BooleanExpression.withOptionalAnd(condition: BooleanExpression?): BooleanExpression {
|
||||
return if (condition == null) this else and(condition)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.v2.content.all.adapter.out.persistence
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.content.all.port.out.MainContentAllQueryPort
|
||||
|
||||
interface MainContentAllQueryRepository : MainContentAllQueryPort
|
||||
Reference in New Issue
Block a user