feat(creator): 채널 라이브 다시듣기 저장소를 추가한다
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveQueryPort
|
||||
|
||||
interface CreatorChannelLiveQueryRepository : CreatorChannelLiveQueryPort
|
||||
@@ -0,0 +1,365 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence
|
||||
|
||||
import com.querydsl.core.Tuple
|
||||
import com.querydsl.core.types.Projections
|
||||
import com.querydsl.core.types.dsl.BooleanExpression
|
||||
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.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.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
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.live.port.out.CreatorChannelAudioContentRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveRecord
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class DefaultCreatorChannelLiveQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) : CreatorChannelLiveQueryRepository {
|
||||
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? {
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
CreatorChannelCreatorRecord::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("creatorChannelLiveBlockMember")
|
||||
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 findCurrentLive(
|
||||
creatorId: Long,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean,
|
||||
viewerId: Long?,
|
||||
isViewerCreator: Boolean,
|
||||
effectiveViewerGender: Gender?
|
||||
): CreatorChannelLiveRecord? {
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
CreatorChannelLiveRecord::class.java,
|
||||
liveRoom.id,
|
||||
liveRoom.title,
|
||||
liveRoom.coverImage,
|
||||
liveRoom.beginDateTime,
|
||||
liveRoom.price,
|
||||
liveRoom.isAdult
|
||||
)
|
||||
)
|
||||
.from(liveRoom)
|
||||
.where(
|
||||
liveRoom.member.id.eq(creatorId),
|
||||
liveRoom.member.isActive.isTrue,
|
||||
liveRoom.isActive.isTrue,
|
||||
liveRoom.channelName.isNotNull,
|
||||
liveRoom.channelName.isNotEmpty,
|
||||
liveRoom.beginDateTime.loe(now),
|
||||
adultLiveCondition(canViewAdultContent),
|
||||
genderLiveCondition(viewerId, effectiveViewerGender),
|
||||
creatorJoinLiveCondition(viewerId, isViewerCreator)
|
||||
)
|
||||
.orderBy(liveRoom.beginDateTime.desc(), liveRoom.id.desc())
|
||||
.fetchFirst()
|
||||
}
|
||||
|
||||
override fun countLiveReplayAudioContents(
|
||||
creatorId: Long,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Int {
|
||||
return queryFactory
|
||||
.select(audioContent.id.count())
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(liveReplayAudioCondition(creatorId, now, canViewAdultContent))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun findLiveReplayAudioContents(
|
||||
creatorId: Long,
|
||||
viewerId: Long?,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean,
|
||||
sort: ContentSort,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelAudioContentRecord> {
|
||||
val rows = findLiveReplayAudioRows(creatorId, viewerId, now, canViewAdultContent, sort, offset, limit)
|
||||
val contentIds = rows.map { itAudioId(it) }
|
||||
val firstContentId = firstLiveReplayAudioContentId(creatorId, now, canViewAdultContent)
|
||||
val seriesByContentId = audioSeriesByContentIds(contentIds)
|
||||
val orderStatesByContentId = orderStatesByContentIds(viewerId, contentIds, now)
|
||||
|
||||
return rows
|
||||
.map { it.toAudioRecord(firstContentId, seriesByContentId, orderStatesByContentId) }
|
||||
}
|
||||
|
||||
private fun findLiveReplayAudioRows(
|
||||
creatorId: Long,
|
||||
viewerId: 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(liveReplayAudioCondition(creatorId, now, canViewAdultContent))
|
||||
|
||||
when (sort) {
|
||||
ContentSort.POPULAR -> {
|
||||
val revenueOrder = QOrder("liveReplayRevenueOrder")
|
||||
query
|
||||
.leftJoin(revenueOrder)
|
||||
.on(
|
||||
revenueOrder.audioContent.id.eq(audioContent.id),
|
||||
revenueOrder.isActive.isTrue
|
||||
)
|
||||
.groupBy(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.duration,
|
||||
audioContent.coverImage,
|
||||
audioContent.price,
|
||||
audioContent.isAdult,
|
||||
audioContent.isPointAvailable,
|
||||
audioContent.releaseDate
|
||||
)
|
||||
.orderBy(
|
||||
revenueOrder.can.sum().coalesce(0).desc(),
|
||||
audioContent.releaseDate.desc(),
|
||||
audioContent.id.desc()
|
||||
)
|
||||
}
|
||||
ContentSort.OWNED -> {
|
||||
val ownedOrder = QOrder("liveReplayOwnedOrder")
|
||||
query
|
||||
.leftJoin(ownedOrder)
|
||||
.on(
|
||||
ownedOrder.audioContent.id.eq(audioContent.id),
|
||||
ownedOrder.member.id.eq(viewerId ?: -1L),
|
||||
ownedOrder.isActive.isTrue,
|
||||
ownedOrder.type.eq(OrderType.KEEP)
|
||||
)
|
||||
.groupBy(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.duration,
|
||||
audioContent.coverImage,
|
||||
audioContent.price,
|
||||
audioContent.isAdult,
|
||||
audioContent.isPointAvailable,
|
||||
audioContent.releaseDate
|
||||
)
|
||||
.orderBy(
|
||||
ownedOrder.id.count().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 liveReplayAudioCondition(
|
||||
creatorId: 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(audioContentTheme.theme.eq(LIVE_REPLAY_THEME))
|
||||
.and(audioContent.duration.isNotNull)
|
||||
.and(audioContent.releaseDate.isNotNull)
|
||||
.and(audioContent.releaseDate.loe(now))
|
||||
.and(adultAudioCondition(canViewAdultContent))
|
||||
}
|
||||
|
||||
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,
|
||||
publishedAt = get(audioContent.releaseDate)!!,
|
||||
seriesName = seriesSummary?.title,
|
||||
isOriginalSeries = seriesSummary?.isOriginal,
|
||||
isOwned = orderState?.isOwned ?: false,
|
||||
isRented = orderState?.isRented ?: false
|
||||
)
|
||||
}
|
||||
|
||||
private fun firstLiveReplayAudioContentId(
|
||||
creatorId: Long,
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean
|
||||
): Long? {
|
||||
return queryFactory
|
||||
.select(audioContent.id)
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(liveReplayAudioCondition(creatorId, now, canViewAdultContent))
|
||||
.orderBy(audioContent.releaseDate.asc(), audioContent.id.asc())
|
||||
.fetchFirst()
|
||||
}
|
||||
|
||||
private fun audioSeriesByContentIds(contentIds: List<Long>): Map<Long, AudioSeriesSummary> {
|
||||
if (contentIds.isEmpty()) return emptyMap()
|
||||
return queryFactory
|
||||
.select(seriesContent.content.id, series.title, series.isOriginal)
|
||||
.from(seriesContent)
|
||||
.innerJoin(seriesContent.series, series)
|
||||
.where(seriesContent.content.id.`in`(contentIds))
|
||||
.fetch()
|
||||
.associate {
|
||||
it.get(seriesContent.content.id)!! to AudioSeriesSummary(
|
||||
title = it.get(series.title)!!,
|
||||
isOriginal = it.get(series.isOriginal)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun orderStatesByContentIds(
|
||||
viewerId: Long?,
|
||||
contentIds: List<Long>,
|
||||
now: LocalDateTime
|
||||
): Map<Long, AudioOrderState> {
|
||||
if (viewerId == null || 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,
|
||||
order.type.eq(OrderType.KEEP)
|
||||
.or(order.type.eq(OrderType.RENTAL).and(order.endDate.after(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 adultLiveCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else liveRoom.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun adultAudioCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else audioContent.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun genderLiveCondition(viewerId: Long?, effectiveViewerGender: Gender?): BooleanExpression? {
|
||||
if (effectiveViewerGender == null || effectiveViewerGender == Gender.NONE) return null
|
||||
val genderCondition = when (effectiveViewerGender) {
|
||||
Gender.MALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.MALE_ONLY)
|
||||
Gender.FEMALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.FEMALE_ONLY)
|
||||
Gender.NONE -> return null
|
||||
}
|
||||
return viewerId?.let { genderCondition.or(liveRoom.member.id.eq(it)) } ?: genderCondition
|
||||
}
|
||||
|
||||
private fun creatorJoinLiveCondition(viewerId: Long?, isViewerCreator: Boolean): BooleanExpression? {
|
||||
if (!isViewerCreator || viewerId == null) return null
|
||||
return liveRoom.isAvailableJoinCreator.isTrue.or(liveRoom.member.id.eq(viewerId))
|
||||
}
|
||||
|
||||
private data class AudioSeriesSummary(
|
||||
val title: String,
|
||||
val isOriginal: Boolean
|
||||
)
|
||||
|
||||
private data class AudioOrderState(
|
||||
val isOwned: Boolean,
|
||||
val isRented: Boolean
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val LIVE_REPLAY_THEME = "다시듣기"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user