feat(creator-channel): 오디오 탭 repository를 추가한다
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out.CreatorChannelAudioQueryPort
|
||||
|
||||
interface CreatorChannelAudioQueryRepository : CreatorChannelAudioQueryPort
|
||||
@@ -0,0 +1,407 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.audio.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.order.QOrder.order
|
||||
import kr.co.vividnext.sodalive.content.series.translation.QSeriesTranslation
|
||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
||||
import kr.co.vividnext.sodalive.content.theme.translation.QContentThemeTranslation
|
||||
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.audio.port.out.CreatorChannelAudioContentRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out.CreatorChannelAudioCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out.CreatorChannelAudioThemeRecord
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class DefaultCreatorChannelAudioQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) : CreatorChannelAudioQueryRepository {
|
||||
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelAudioCreatorRecord? {
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
CreatorChannelAudioCreatorRecord::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("creatorChannelAudioBlockMember")
|
||||
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 findActiveThemeId(themeId: Long): Long? {
|
||||
return queryFactory
|
||||
.select(audioContentTheme.id)
|
||||
.from(audioContentTheme)
|
||||
.where(
|
||||
audioContentTheme.id.eq(themeId),
|
||||
audioContentTheme.isActive.isTrue
|
||||
)
|
||||
.fetchFirst()
|
||||
}
|
||||
|
||||
override fun findAudioThemes(locale: String): List<CreatorChannelAudioThemeRecord> {
|
||||
val themeTranslation = QContentThemeTranslation("audioThemeTranslation")
|
||||
return queryFactory
|
||||
.select(audioContentTheme.id, audioContentTheme.theme, themeTranslation.theme)
|
||||
.from(audioContentTheme)
|
||||
.leftJoin(themeTranslation)
|
||||
.on(
|
||||
themeTranslation.contentThemeId.eq(audioContentTheme.id),
|
||||
themeTranslation.locale.eq(locale)
|
||||
)
|
||||
.where(audioContentTheme.isActive.isTrue)
|
||||
.orderBy(audioContentTheme.orders.asc(), audioContentTheme.id.asc())
|
||||
.fetch()
|
||||
.map {
|
||||
CreatorChannelAudioThemeRecord(
|
||||
themeId = it.get(audioContentTheme.id)!!,
|
||||
themeName = it.get(themeTranslation.theme).takeUnless(String?::isNullOrBlank)
|
||||
?: it.get(audioContentTheme.theme)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun countAudioContents(
|
||||
creatorId: Long,
|
||||
themeId: Long?,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Int {
|
||||
return queryFactory
|
||||
.select(audioContent.id.count())
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(audioContentCondition(creatorId, themeId, now, canViewAdultContent))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun countPaidAudioContents(
|
||||
creatorId: Long,
|
||||
themeId: Long?,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Int {
|
||||
return queryFactory
|
||||
.select(audioContent.id.count())
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(
|
||||
audioContentCondition(creatorId, themeId, now, canViewAdultContent),
|
||||
audioContent.price.gt(0)
|
||||
)
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun countPurchasedAudioContents(
|
||||
creatorId: Long,
|
||||
viewerId: Long,
|
||||
themeId: Long?,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Int {
|
||||
val purchasedOrder = QOrder("audioPurchasedOrder")
|
||||
return queryFactory
|
||||
.select(audioContent.id.countDistinct())
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.innerJoin(purchasedOrder)
|
||||
.on(purchasedOrder.audioContent.id.eq(audioContent.id))
|
||||
.where(
|
||||
audioContentCondition(creatorId, themeId, now, canViewAdultContent),
|
||||
audioContent.price.gt(0),
|
||||
purchasedOrder.member.id.eq(viewerId),
|
||||
purchasedOrder.isActive.isTrue,
|
||||
validPurchasedOrderCondition(purchasedOrder, now)
|
||||
)
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun findAudioContents(
|
||||
creatorId: Long,
|
||||
viewerId: Long,
|
||||
themeId: Long?,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean,
|
||||
sort: ContentSort,
|
||||
locale: String,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelAudioContentRecord> {
|
||||
val rows = findAudioContentRows(creatorId, viewerId, themeId, now, canViewAdultContent, sort, offset, limit)
|
||||
val contentIds = rows.map { itAudioId(it) }
|
||||
val firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent)
|
||||
val seriesByContentId = audioSeriesByContentIds(contentIds, locale)
|
||||
val orderStatesByContentId = orderStatesByContentIds(viewerId, contentIds, now)
|
||||
|
||||
return rows.map { it.toAudioRecord(firstContentId, seriesByContentId, orderStatesByContentId) }
|
||||
}
|
||||
|
||||
private fun findAudioContentRows(
|
||||
creatorId: Long,
|
||||
viewerId: Long,
|
||||
themeId: Long?,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean,
|
||||
sort: ContentSort,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<Tuple> {
|
||||
val query = queryFactory
|
||||
.select(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.duration,
|
||||
audioContent.coverImage,
|
||||
audioContent.price,
|
||||
audioContent.isAdult,
|
||||
audioContent.isPointAvailable,
|
||||
audioContent.releaseDate
|
||||
)
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(audioContentCondition(creatorId, themeId, now, canViewAdultContent))
|
||||
|
||||
when (sort) {
|
||||
ContentSort.POPULAR -> {
|
||||
val revenueOrder = QOrder("audioRevenueOrder")
|
||||
query
|
||||
.leftJoin(revenueOrder)
|
||||
.on(
|
||||
revenueOrder.audioContent.id.eq(audioContent.id),
|
||||
revenueOrder.isActive.isTrue
|
||||
)
|
||||
.groupByAudioContentRow()
|
||||
.orderBy(
|
||||
revenueOrder.can.sum().coalesce(0).desc(),
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.id.desc()
|
||||
)
|
||||
}
|
||||
ContentSort.OWNED -> {
|
||||
val ownedOrder = QOrder("audioOwnedOrder")
|
||||
query
|
||||
.leftJoin(ownedOrder)
|
||||
.on(
|
||||
ownedOrder.audioContent.id.eq(audioContent.id),
|
||||
ownedOrder.member.id.eq(viewerId),
|
||||
ownedOrder.isActive.isTrue,
|
||||
validPurchasedOrderCondition(ownedOrder, now)
|
||||
)
|
||||
.groupByAudioContentRow()
|
||||
.orderBy(
|
||||
CaseBuilder()
|
||||
.`when`(ownedOrder.id.countDistinct().gt(0))
|
||||
.then(1)
|
||||
.otherwise(0)
|
||||
.desc(),
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.id.desc()
|
||||
)
|
||||
}
|
||||
ContentSort.LATEST -> query.orderBy(
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.price.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()
|
||||
)
|
||||
}
|
||||
|
||||
return query
|
||||
.offset(offset)
|
||||
.limit(limit.toLong())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun com.querydsl.jpa.impl.JPAQuery<Tuple>.groupByAudioContentRow(): com.querydsl.jpa.impl.JPAQuery<Tuple> {
|
||||
return groupBy(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.duration,
|
||||
audioContent.coverImage,
|
||||
audioContent.price,
|
||||
audioContent.isAdult,
|
||||
audioContent.isPointAvailable,
|
||||
audioContent.releaseDate
|
||||
)
|
||||
}
|
||||
|
||||
private fun audioContentCondition(
|
||||
creatorId: Long,
|
||||
themeId: Long?,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): BooleanExpression {
|
||||
return audioContent.member.id.eq(creatorId)
|
||||
.and(audioContent.member.isActive.isTrue)
|
||||
.and(audioContent.isActive.isTrue)
|
||||
.and(audioContentTheme.isActive.isTrue)
|
||||
.and(themeCondition(themeId))
|
||||
.and(audioContent.duration.isNotNull)
|
||||
.and(audioContent.releaseDate.isNotNull)
|
||||
.and(audioContent.releaseDate.loe(now))
|
||||
.and(adultAudioCondition(canViewAdultContent))
|
||||
}
|
||||
|
||||
private fun themeCondition(themeId: Long?): BooleanExpression? {
|
||||
return themeId?.let { audioContentTheme.id.eq(it) }
|
||||
}
|
||||
|
||||
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 fun itAudioId(row: Tuple): Long = row.get(audioContent.id)!!
|
||||
|
||||
private fun Tuple.toAudioRecord(
|
||||
firstContentId: Long?,
|
||||
seriesByContentId: Map<Long, AudioSeriesSummary>,
|
||||
orderStatesByContentId: Map<Long, AudioOrderState>
|
||||
): CreatorChannelAudioContentRecord {
|
||||
val audioContentId = get(audioContent.id)!!
|
||||
val seriesSummary = seriesByContentId[audioContentId]
|
||||
val orderState = orderStatesByContentId[audioContentId]
|
||||
return CreatorChannelAudioContentRecord(
|
||||
audioContentId = audioContentId,
|
||||
title = get(audioContent.title)!!,
|
||||
duration = get(audioContent.duration),
|
||||
imagePath = get(audioContent.coverImage),
|
||||
price = get(audioContent.price)!!,
|
||||
isAdult = get(audioContent.isAdult)!!,
|
||||
isPointAvailable = get(audioContent.isPointAvailable)!!,
|
||||
isFirstContent = firstContentId == audioContentId,
|
||||
seriesName = seriesSummary?.title,
|
||||
isOriginalSeries = seriesSummary?.isOriginal,
|
||||
isOwned = orderState?.isOwned ?: false,
|
||||
isRented = orderState?.isRented ?: false
|
||||
)
|
||||
}
|
||||
|
||||
private fun firstAudioContentId(
|
||||
creatorId: Long,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Long? {
|
||||
return queryFactory
|
||||
.select(audioContent.id)
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(audioContentCondition(creatorId, themeId = null, now, canViewAdultContent))
|
||||
.orderBy(audioContent.releaseDate.asc(), audioContent.id.asc())
|
||||
.fetchFirst()
|
||||
}
|
||||
|
||||
private fun audioSeriesByContentIds(contentIds: List<Long>, locale: String): Map<Long, AudioSeriesSummary> {
|
||||
if (contentIds.isEmpty()) return emptyMap()
|
||||
val seriesTranslation = QSeriesTranslation("audioSeriesTranslation")
|
||||
return queryFactory
|
||||
.select(
|
||||
seriesContent.content.id,
|
||||
series.title,
|
||||
series.isOriginal,
|
||||
seriesTranslation
|
||||
)
|
||||
.from(seriesContent)
|
||||
.innerJoin(seriesContent.series, series)
|
||||
.leftJoin(seriesTranslation)
|
||||
.on(
|
||||
seriesTranslation.seriesId.eq(series.id),
|
||||
seriesTranslation.locale.eq(locale)
|
||||
)
|
||||
.where(seriesContent.content.id.`in`(contentIds))
|
||||
.fetch()
|
||||
.associate {
|
||||
val originalTitle = it.get(series.title)!!
|
||||
val translatedTitle = it.get(seriesTranslation)?.renderedPayload?.title
|
||||
it.get(seriesContent.content.id)!! to AudioSeriesSummary(
|
||||
title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: originalTitle,
|
||||
isOriginal = it.get(series.isOriginal)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun orderStatesByContentIds(
|
||||
viewerId: Long,
|
||||
contentIds: List<Long>,
|
||||
now: LocalDateTime
|
||||
): Map<Long, AudioOrderState> {
|
||||
if (contentIds.isEmpty()) return emptyMap()
|
||||
return queryFactory
|
||||
.select(order.audioContent.id, order.type)
|
||||
.from(order)
|
||||
.where(
|
||||
order.member.id.eq(viewerId),
|
||||
order.audioContent.id.`in`(contentIds),
|
||||
order.isActive.isTrue,
|
||||
validPurchasedOrderCondition(order, now)
|
||||
)
|
||||
.fetch()
|
||||
.groupBy { it.get(order.audioContent.id)!! }
|
||||
.mapValues { (_, rows) ->
|
||||
val types = rows.map { it.get(order.type)!! }.toSet()
|
||||
AudioOrderState(
|
||||
isOwned = OrderType.KEEP in types,
|
||||
isRented = OrderType.RENTAL in types
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun adultAudioCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else audioContent.isAdult.isFalse
|
||||
}
|
||||
|
||||
private data class AudioSeriesSummary(
|
||||
val title: String,
|
||||
val isOriginal: Boolean
|
||||
)
|
||||
|
||||
private data class AudioOrderState(
|
||||
val isOwned: Boolean,
|
||||
val isRented: Boolean
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user