feat(creator-channel): 시리즈 탭 repository를 추가한다
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.series.adapter.out.persistence
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.series.port.out.CreatorChannelSeriesQueryPort
|
||||
|
||||
interface CreatorChannelSeriesQueryRepository : CreatorChannelSeriesQueryPort
|
||||
@@ -0,0 +1,288 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.series.adapter.out.persistence
|
||||
|
||||
import com.querydsl.core.Tuple
|
||||
import com.querydsl.core.types.Projections
|
||||
import com.querydsl.core.types.dsl.BooleanExpression
|
||||
import com.querydsl.core.types.dsl.CaseBuilder
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.content.order.QOrder
|
||||
import kr.co.vividnext.sodalive.content.series.translation.QSeriesTranslation
|
||||
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.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.creator.channel.series.port.out.CreatorChannelSeriesCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.series.port.out.CreatorChannelSeriesRecord
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class DefaultCreatorChannelSeriesQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) : CreatorChannelSeriesQueryRepository {
|
||||
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelSeriesCreatorRecord? {
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
CreatorChannelSeriesCreatorRecord::class.java,
|
||||
member.id,
|
||||
member.role,
|
||||
member.nickname
|
||||
)
|
||||
)
|
||||
.from(member)
|
||||
.where(
|
||||
member.id.eq(creatorId),
|
||||
member.isActive.isTrue
|
||||
)
|
||||
.fetchFirst()
|
||||
}
|
||||
|
||||
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean {
|
||||
val blockMember = QBlockMember("creatorChannelSeriesBlockMember")
|
||||
return queryFactory
|
||||
.select(blockMember.id)
|
||||
.from(blockMember)
|
||||
.where(
|
||||
blockMember.isActive.isTrue,
|
||||
blockMember.member.id.eq(viewerId).and(blockMember.blockedMember.id.eq(creatorId))
|
||||
.or(blockMember.member.id.eq(creatorId).and(blockMember.blockedMember.id.eq(viewerId)))
|
||||
)
|
||||
.fetchFirst() != null
|
||||
}
|
||||
|
||||
override fun countSeries(creatorId: Long, now: LocalDateTime, canViewAdultContent: Boolean): Int {
|
||||
return queryFactory
|
||||
.select(series.id.count())
|
||||
.from(series)
|
||||
.where(seriesCondition(creatorId, canViewAdultContent))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun findSeries(
|
||||
creatorId: Long,
|
||||
viewerId: Long,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean,
|
||||
sort: ContentSort,
|
||||
locale: String,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelSeriesRecord> {
|
||||
val seriesIds = findSeriesIds(creatorId, viewerId, now, canViewAdultContent, sort, offset, limit)
|
||||
if (seriesIds.isEmpty()) return emptyList()
|
||||
|
||||
val seriesTranslation = QSeriesTranslation("creatorChannelSeriesTranslation")
|
||||
val rows = findSeriesRows(seriesIds, locale, seriesTranslation)
|
||||
val contentStats = contentStatsBySeriesIds(seriesIds, now, canViewAdultContent)
|
||||
val purchaseStats = purchaseStatsBySeriesIds(seriesIds, viewerId, now, canViewAdultContent)
|
||||
|
||||
return rows.sortedBy { seriesIds.indexOf(it.get(series)!!.id!!) }
|
||||
.map { row ->
|
||||
val targetSeries = row.get(series)!!
|
||||
val translatedTitle = row.get(seriesTranslation)
|
||||
?.renderedPayload
|
||||
?.title
|
||||
val contentStat = contentStats[targetSeries.id] ?: SeriesContentStats()
|
||||
CreatorChannelSeriesRecord(
|
||||
seriesId = targetSeries.id!!,
|
||||
title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: targetSeries.title,
|
||||
coverImagePath = targetSeries.coverImage,
|
||||
publishedDaysOfWeek = targetSeries.publishedDaysOfWeek,
|
||||
isOriginal = targetSeries.isOriginal,
|
||||
isAdult = targetSeries.isAdult,
|
||||
state = targetSeries.state,
|
||||
contentCount = contentStat.contentCount,
|
||||
purchasedContentCount = purchaseStats[targetSeries.id] ?: 0,
|
||||
paidContentCount = contentStat.paidContentCount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSeriesIds(
|
||||
creatorId: Long,
|
||||
viewerId: Long,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean,
|
||||
sort: ContentSort,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<Long> {
|
||||
val revenueOrder = QOrder("seriesRevenueOrder")
|
||||
val ownedOrder = QOrder("seriesOwnedOrder")
|
||||
val latestReleaseDate = audioContent.releaseDate.max()
|
||||
val highestPrice = audioContent.price.max()
|
||||
val lowestPrice = audioContent.price.min()
|
||||
val revenue = revenueOrder.can.sum().coalesce(0)
|
||||
val ownedCount = ownedOrder.audioContent.id.countDistinct()
|
||||
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)
|
||||
.leftJoin(seriesContent).on(seriesContent.series.id.eq(series.id))
|
||||
.leftJoin(audioContent).on(
|
||||
seriesContent.content.id.eq(audioContent.id),
|
||||
publicAudioContentCondition(now, canViewAdultContent)
|
||||
)
|
||||
.where(seriesCondition(creatorId, canViewAdultContent))
|
||||
.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.OWNED -> {
|
||||
query
|
||||
.leftJoin(ownedOrder)
|
||||
.on(
|
||||
ownedOrder.audioContent.id.eq(audioContent.id),
|
||||
ownedOrder.member.id.eq(viewerId),
|
||||
ownedOrder.isActive.isTrue,
|
||||
validPurchasedOrderCondition(ownedOrder, now)
|
||||
)
|
||||
.orderBy(ownedCount.desc(), latestReleaseDate.desc(), series.id.desc())
|
||||
}
|
||||
ContentSort.LATEST -> query.orderBy(
|
||||
latestReleaseDateNullLast.asc(),
|
||||
latestReleaseDate.desc(),
|
||||
highestPrice.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()
|
||||
)
|
||||
}
|
||||
|
||||
return query.offset(offset).limit(limit.toLong()).fetch()
|
||||
}
|
||||
|
||||
private fun findSeriesRows(
|
||||
seriesIds: List<Long>,
|
||||
locale: String,
|
||||
seriesTranslation: QSeriesTranslation
|
||||
): List<Tuple> {
|
||||
return queryFactory
|
||||
.select(series, seriesTranslation)
|
||||
.from(series)
|
||||
.leftJoin(seriesTranslation)
|
||||
.on(
|
||||
seriesTranslation.seriesId.eq(series.id),
|
||||
seriesTranslation.locale.eq(locale)
|
||||
)
|
||||
.where(series.id.`in`(seriesIds))
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun contentStatsBySeriesIds(
|
||||
seriesIds: List<Long>,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Map<Long, SeriesContentStats> {
|
||||
val paidContentCount = CaseBuilder()
|
||||
.`when`(audioContent.price.gt(0))
|
||||
.then(audioContent.id)
|
||||
.otherwise(null as Long?)
|
||||
.countDistinct()
|
||||
return queryFactory
|
||||
.select(
|
||||
seriesContent.series.id,
|
||||
audioContent.id.countDistinct(),
|
||||
paidContentCount
|
||||
)
|
||||
.from(seriesContent)
|
||||
.innerJoin(seriesContent.content, audioContent)
|
||||
.where(
|
||||
seriesContent.series.id.`in`(seriesIds),
|
||||
publicAudioContentCondition(now, canViewAdultContent)
|
||||
)
|
||||
.groupBy(seriesContent.series.id)
|
||||
.fetch()
|
||||
.associate {
|
||||
it.get(seriesContent.series.id)!! to SeriesContentStats(
|
||||
contentCount = it.get(audioContent.id.countDistinct())?.toInt() ?: 0,
|
||||
paidContentCount = it.get(paidContentCount)?.toInt() ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun purchaseStatsBySeriesIds(
|
||||
seriesIds: List<Long>,
|
||||
viewerId: Long,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Map<Long, Int> {
|
||||
val purchasedOrder = QOrder("seriesPurchasedOrder")
|
||||
return queryFactory
|
||||
.select(seriesContent.series.id, audioContent.id.countDistinct())
|
||||
.from(seriesContent)
|
||||
.innerJoin(seriesContent.content, audioContent)
|
||||
.innerJoin(purchasedOrder)
|
||||
.on(purchasedOrder.audioContent.id.eq(audioContent.id))
|
||||
.where(
|
||||
seriesContent.series.id.`in`(seriesIds),
|
||||
publicAudioContentCondition(now, canViewAdultContent),
|
||||
audioContent.price.gt(0),
|
||||
purchasedOrder.member.id.eq(viewerId),
|
||||
purchasedOrder.isActive.isTrue,
|
||||
validPurchasedOrderCondition(purchasedOrder, now)
|
||||
)
|
||||
.groupBy(seriesContent.series.id)
|
||||
.fetch()
|
||||
.associate { it.get(seriesContent.series.id)!! to (it.get(audioContent.id.countDistinct())?.toInt() ?: 0) }
|
||||
}
|
||||
|
||||
private fun seriesCondition(creatorId: Long, canViewAdultContent: Boolean): BooleanExpression {
|
||||
return series.member.id.eq(creatorId)
|
||||
.and(series.isActive.isTrue)
|
||||
.and(adultSeriesCondition(canViewAdultContent))
|
||||
}
|
||||
|
||||
private fun adultSeriesCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else series.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun publicAudioContentCondition(now: LocalDateTime, canViewAdultContent: Boolean): BooleanExpression {
|
||||
return audioContent.isActive.isTrue
|
||||
.and(audioContent.duration.isNotNull)
|
||||
.and(audioContent.releaseDate.isNotNull)
|
||||
.and(audioContent.releaseDate.loe(now))
|
||||
.and(adultAudioCondition(canViewAdultContent))
|
||||
}
|
||||
|
||||
private fun adultAudioCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else audioContent.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun validPurchasedOrderCondition(targetOrder: QOrder, now: LocalDateTime): BooleanExpression {
|
||||
return targetOrder.type.eq(OrderType.KEEP)
|
||||
.or(targetOrder.type.eq(OrderType.RENTAL).and(targetOrder.endDate.after(now)))
|
||||
}
|
||||
|
||||
private data class SeriesContentStats(
|
||||
val contentCount: Int = 0,
|
||||
val paidContentCount: Int = 0
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user