feat(creator): 채널 홈 조회 어댑터를 추가한다
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelHomeQueryPort
|
||||||
|
|
||||||
|
interface CreatorChannelHomeQueryRepository : CreatorChannelHomeQueryPort
|
||||||
@@ -0,0 +1,916 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.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.can.use.CanUsage
|
||||||
|
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
||||||
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
|
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||||
|
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.explorer.profile.QCreatorCheers.creatorCheers
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage
|
||||||
|
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.extensions.removeDeletedNicknamePrefix
|
||||||
|
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||||
|
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||||
|
import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit
|
||||||
|
import kr.co.vividnext.sodalive.member.Gender
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberKind
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
|
import kr.co.vividnext.sodalive.member.block.QBlockMember
|
||||||
|
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelActivityRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelAudioContentRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelCommunityPostRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelCreatorRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelDonationRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelFanTalkRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelFanTalkSummaryRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelLiveRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelScheduleRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelSeriesRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelSnsRecord
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class DefaultCreatorChannelHomeQueryRepository(
|
||||||
|
private val queryFactory: JPAQueryFactory
|
||||||
|
) : CreatorChannelHomeQueryRepository {
|
||||||
|
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? {
|
||||||
|
val creator = queryFactory
|
||||||
|
.select(member.id, member.nickname, member.profileImage, member.introduce, member.memberKind)
|
||||||
|
.from(member)
|
||||||
|
.where(
|
||||||
|
member.id.eq(creatorId),
|
||||||
|
member.role.eq(MemberRole.CREATOR),
|
||||||
|
member.isActive.isTrue
|
||||||
|
)
|
||||||
|
.fetchFirst() ?: return null
|
||||||
|
|
||||||
|
val following = viewerId?.let {
|
||||||
|
queryFactory
|
||||||
|
.select(creatorFollowing.isActive, creatorFollowing.isNotify)
|
||||||
|
.from(creatorFollowing)
|
||||||
|
.where(
|
||||||
|
creatorFollowing.member.id.eq(it),
|
||||||
|
creatorFollowing.creator.id.eq(creatorId),
|
||||||
|
creatorFollowing.isActive.isTrue
|
||||||
|
)
|
||||||
|
.fetchFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
val characterId = queryFactory
|
||||||
|
.select(chatCharacter.id)
|
||||||
|
.from(chatCharacter)
|
||||||
|
.where(
|
||||||
|
chatCharacter.creatorMember.id.eq(creatorId),
|
||||||
|
chatCharacter.isActive.isTrue
|
||||||
|
)
|
||||||
|
.fetchFirst()
|
||||||
|
|
||||||
|
return CreatorChannelCreatorRecord(
|
||||||
|
creatorId = creator.get(member.id)!!,
|
||||||
|
characterId = characterId,
|
||||||
|
nickname = creator.get(member.nickname)!!,
|
||||||
|
profileImagePath = creator.get(member.profileImage),
|
||||||
|
introduce = creator.get(member.introduce)!!,
|
||||||
|
followerCount = queryFactory
|
||||||
|
.select(creatorFollowing.id.count())
|
||||||
|
.from(creatorFollowing)
|
||||||
|
.where(
|
||||||
|
creatorFollowing.creator.id.eq(creatorId),
|
||||||
|
creatorFollowing.isActive.isTrue,
|
||||||
|
creatorFollowing.member.isActive.isTrue
|
||||||
|
)
|
||||||
|
.fetchOne()
|
||||||
|
?.toInt()
|
||||||
|
?: 0,
|
||||||
|
isAiChatAvailable = characterId != null,
|
||||||
|
isDmAvailable = creator.get(member.memberKind) != MemberKind.AI_CHARACTER,
|
||||||
|
isFollow = following?.get(creatorFollowing.isActive) ?: false,
|
||||||
|
isNotify = following?.get(creatorFollowing.isNotify) ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean {
|
||||||
|
val blockMember = QBlockMember("creatorChannelBlockMember")
|
||||||
|
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 findLatestAudioContent(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean
|
||||||
|
): CreatorChannelAudioContentRecord? {
|
||||||
|
val row = findAudioContentRows(creatorId, now, null, canViewAdultContent, 1).firstOrNull() ?: return null
|
||||||
|
return row.toAudioRecord(
|
||||||
|
firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent),
|
||||||
|
seriesByContentId = audioSeriesByContentIds(listOf(itAudioId(row)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findChannelDonations(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
now: LocalDateTime,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelDonationRecord> {
|
||||||
|
val kstZoneId = ZoneId.of("Asia/Seoul")
|
||||||
|
val utcZoneId = ZoneId.of("UTC")
|
||||||
|
val nowKst = now.atZone(utcZoneId).withZoneSameInstant(kstZoneId)
|
||||||
|
val start = nowKst.toLocalDate().withDayOfMonth(1).atStartOfDay(kstZoneId)
|
||||||
|
.withZoneSameInstant(utcZoneId)
|
||||||
|
.toLocalDateTime()
|
||||||
|
val end = nowKst.toLocalDate().withDayOfMonth(1).atStartOfDay(kstZoneId).plusMonths(1)
|
||||||
|
.withZoneSameInstant(utcZoneId)
|
||||||
|
.toLocalDateTime()
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
CreatorChannelDonationRecord::class.java,
|
||||||
|
channelDonationMessage.member.nickname,
|
||||||
|
channelDonationMessage.member.profileImage,
|
||||||
|
channelDonationMessage.can,
|
||||||
|
channelDonationMessage.additionalMessage.coalesce(""),
|
||||||
|
channelDonationMessage.createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(channelDonationMessage)
|
||||||
|
.where(
|
||||||
|
channelDonationMessage.creator.id.eq(creatorId),
|
||||||
|
channelDonationMessage.createdAt.goe(start),
|
||||||
|
channelDonationMessage.createdAt.lt(end),
|
||||||
|
donationVisibilityCondition(creatorId, viewerId)
|
||||||
|
)
|
||||||
|
.orderBy(channelDonationMessage.createdAt.desc(), channelDonationMessage.id.desc())
|
||||||
|
.limit(limit.toLong())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findCommunityPosts(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
isFixed: Boolean,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelCommunityPostRecord> {
|
||||||
|
val posts = queryFactory
|
||||||
|
.select(
|
||||||
|
creatorCommunity.id,
|
||||||
|
creatorCommunity.member.id,
|
||||||
|
creatorCommunity.member.nickname,
|
||||||
|
creatorCommunity.member.profileImage,
|
||||||
|
creatorCommunity.imagePath,
|
||||||
|
creatorCommunity.audioPath,
|
||||||
|
creatorCommunity.content,
|
||||||
|
creatorCommunity.price,
|
||||||
|
creatorCommunity.createdAt,
|
||||||
|
creatorCommunity.fixedAt,
|
||||||
|
creatorCommunity.isFixed,
|
||||||
|
creatorCommunity.isCommentAvailable
|
||||||
|
)
|
||||||
|
.from(creatorCommunity)
|
||||||
|
.where(
|
||||||
|
creatorCommunity.member.id.eq(creatorId),
|
||||||
|
creatorCommunity.member.isActive.isTrue,
|
||||||
|
visibleCommunityPostCondition(viewerId),
|
||||||
|
creatorCommunity.isFixed.eq(isFixed),
|
||||||
|
fixedNoticeCondition(isFixed),
|
||||||
|
adultCommunityCondition(canViewAdultContent)
|
||||||
|
)
|
||||||
|
.orderBy(
|
||||||
|
if (isFixed) creatorCommunity.fixedAt.desc() else creatorCommunity.createdAt.desc(),
|
||||||
|
creatorCommunity.id.desc()
|
||||||
|
)
|
||||||
|
.limit(limit.toLong())
|
||||||
|
.fetch()
|
||||||
|
|
||||||
|
val postIds = posts.map { it.get(creatorCommunity.id)!! }
|
||||||
|
val orderedPostIds = orderedCommunityPostIds(creatorId, viewerId, postIds)
|
||||||
|
val likeCounts = communityLikeCounts(postIds)
|
||||||
|
val commentCounts = communityCommentCounts(
|
||||||
|
postIds = posts.filter { it.get(creatorCommunity.isCommentAvailable)!! }.map { it.get(creatorCommunity.id)!! },
|
||||||
|
viewerId = viewerId,
|
||||||
|
isContentCreator = viewerId == creatorId
|
||||||
|
)
|
||||||
|
|
||||||
|
return posts
|
||||||
|
.map {
|
||||||
|
val postId = it.get(creatorCommunity.id)!!
|
||||||
|
val postCreatorId = it.get(creatorCommunity.member.id)!!
|
||||||
|
val isFixedPost = it.get(creatorCommunity.isFixed)!!
|
||||||
|
val price = it.get(creatorCommunity.price)!!
|
||||||
|
val existOrdered = postId in orderedPostIds
|
||||||
|
val canAccessPaidContent = canAccessPaidCommunityContent(
|
||||||
|
price = price,
|
||||||
|
viewerId = viewerId,
|
||||||
|
creatorId = postCreatorId,
|
||||||
|
existOrdered = existOrdered
|
||||||
|
)
|
||||||
|
CreatorChannelCommunityPostRecord(
|
||||||
|
postId = postId,
|
||||||
|
creatorId = postCreatorId,
|
||||||
|
creatorNickname = it.get(creatorCommunity.member.nickname)!!,
|
||||||
|
creatorProfilePath = it.get(creatorCommunity.member.profileImage),
|
||||||
|
imagePath = it.get(creatorCommunity.imagePath),
|
||||||
|
audioPath = if (canAccessPaidContent) it.get(creatorCommunity.audioPath) else null,
|
||||||
|
content = maskPaidCommunityContent(
|
||||||
|
content = it.get(creatorCommunity.content)!!,
|
||||||
|
canAccessPaidContent = canAccessPaidContent
|
||||||
|
),
|
||||||
|
price = price,
|
||||||
|
date = if (isFixedPost) {
|
||||||
|
it.get(creatorCommunity.fixedAt) ?: it.get(creatorCommunity.createdAt)!!
|
||||||
|
} else {
|
||||||
|
it.get(creatorCommunity.createdAt)!!
|
||||||
|
},
|
||||||
|
existOrdered = existOrdered,
|
||||||
|
likeCount = likeCounts[postId] ?: 0,
|
||||||
|
commentCount = commentCounts[postId] ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findSchedules(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
viewerId: Long?,
|
||||||
|
isViewerCreator: Boolean,
|
||||||
|
effectiveViewerGender: Gender?,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelScheduleRecord> {
|
||||||
|
val liveSchedules = queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
CreatorChannelScheduleRecord::class.java,
|
||||||
|
liveRoom.beginDateTime,
|
||||||
|
liveRoom.title,
|
||||||
|
Expressions.constant(CreatorActivityType.LIVE),
|
||||||
|
liveRoom.id,
|
||||||
|
liveRoom.isAdult
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(liveRoom)
|
||||||
|
.where(
|
||||||
|
liveRoom.member.id.eq(creatorId),
|
||||||
|
liveRoom.member.isActive.isTrue,
|
||||||
|
liveRoom.isActive.isTrue,
|
||||||
|
liveRoom.channelName.isNull.or(liveRoom.channelName.isEmpty),
|
||||||
|
liveRoom.beginDateTime.gt(now),
|
||||||
|
adultLiveCondition(canViewAdultContent),
|
||||||
|
genderLiveCondition(viewerId, effectiveViewerGender),
|
||||||
|
creatorJoinLiveCondition(viewerId, isViewerCreator)
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
|
||||||
|
val audioSchedules = queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
CreatorChannelScheduleRecord::class.java,
|
||||||
|
audioContent.releaseDate,
|
||||||
|
audioContent.title,
|
||||||
|
Expressions.constant(CreatorActivityType.AUDIO),
|
||||||
|
audioContent.id,
|
||||||
|
audioContent.isAdult
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(audioContent)
|
||||||
|
.where(
|
||||||
|
audioContent.member.id.eq(creatorId),
|
||||||
|
audioContent.member.isActive.isTrue,
|
||||||
|
audioContent.duration.isNotNull,
|
||||||
|
audioContent.releaseDate.isNotNull,
|
||||||
|
audioContent.releaseDate.gt(now),
|
||||||
|
adultAudioCondition(canViewAdultContent)
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
|
||||||
|
return (liveSchedules + audioSchedules)
|
||||||
|
.sortedWith(compareBy<CreatorChannelScheduleRecord> { it.scheduledAt }.thenBy { it.type.sortOrder })
|
||||||
|
.take(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findAudioContents(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
latestAudioContentId: Long?,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelAudioContentRecord> {
|
||||||
|
val rows = findAudioContentRows(creatorId, now, latestAudioContentId, canViewAdultContent, limit)
|
||||||
|
val firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent)
|
||||||
|
val seriesByContentId = audioSeriesByContentIds(rows.map { itAudioId(it) })
|
||||||
|
return rows.map { it.toAudioRecord(firstContentId, seriesByContentId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findSeries(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
contentType: ContentType,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelSeriesRecord> {
|
||||||
|
val seriesRows = queryFactory
|
||||||
|
.select(
|
||||||
|
series.id,
|
||||||
|
series.title,
|
||||||
|
series.coverImage,
|
||||||
|
series.isOriginal
|
||||||
|
)
|
||||||
|
.from(series)
|
||||||
|
.where(
|
||||||
|
series.member.id.eq(creatorId),
|
||||||
|
series.member.isActive.isTrue,
|
||||||
|
series.isActive.isTrue,
|
||||||
|
adultSeriesCondition(canViewAdultContent),
|
||||||
|
contentTypeSeriesCondition(canViewAdultContent, contentType),
|
||||||
|
notBlockedSeriesCreatorCondition(viewerId)
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
|
||||||
|
val seriesIds = seriesRows.map { it.get(series.id)!! }
|
||||||
|
val contentStats = seriesContentStats(seriesIds, now, canViewAdultContent)
|
||||||
|
val newSeriesIds = newSeriesIds(seriesIds, now, canViewAdultContent)
|
||||||
|
return seriesRows
|
||||||
|
.mapNotNull { seriesRow ->
|
||||||
|
contentStats[seriesRow.get(series.id)!!]?.let { seriesRow to it }
|
||||||
|
}
|
||||||
|
.sortedByDescending { it.second.latestPublishedAt }
|
||||||
|
.take(limit)
|
||||||
|
.map { (seriesRow, stats) ->
|
||||||
|
val seriesId = seriesRow.get(series.id)!!
|
||||||
|
CreatorChannelSeriesRecord(
|
||||||
|
seriesId = seriesId,
|
||||||
|
title = seriesRow.get(series.title)!!,
|
||||||
|
coverImagePath = seriesRow.get(series.coverImage),
|
||||||
|
numberOfContent = stats.contentCount,
|
||||||
|
isNew = seriesId in newSeriesIds,
|
||||||
|
isOriginal = seriesRow.get(series.isOriginal)!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord {
|
||||||
|
val totalCount = queryFactory
|
||||||
|
.select(creatorCheers.id.count())
|
||||||
|
.from(creatorCheers)
|
||||||
|
.where(fanTalkSummaryCondition(creatorId, viewerId))
|
||||||
|
.fetchOne()
|
||||||
|
?.toInt()
|
||||||
|
?: 0
|
||||||
|
|
||||||
|
val latestTalk = queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
CreatorChannelFanTalkRecord::class.java,
|
||||||
|
creatorCheers.id,
|
||||||
|
creatorCheers.member.id,
|
||||||
|
creatorCheers.member.nickname,
|
||||||
|
creatorCheers.member.profileImage,
|
||||||
|
creatorCheers.cheers,
|
||||||
|
creatorCheers.languageCode,
|
||||||
|
creatorCheers.createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(creatorCheers)
|
||||||
|
.where(fanTalkSummaryCondition(creatorId, viewerId))
|
||||||
|
.orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc())
|
||||||
|
.limit(1)
|
||||||
|
.fetchFirst()
|
||||||
|
?.let { it.copy(nickname = it.nickname.removeDeletedNicknamePrefix()) }
|
||||||
|
|
||||||
|
return CreatorChannelFanTalkSummaryRecord(
|
||||||
|
totalCount = totalCount,
|
||||||
|
latestFanTalk = latestTalk
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findActivity(creatorId: Long, now: LocalDateTime): CreatorChannelActivityRecord {
|
||||||
|
val firstLiveAt = queryFactory
|
||||||
|
.select(liveRoom.beginDateTime.min())
|
||||||
|
.from(liveRoom)
|
||||||
|
.where(liveRoom.member.id.eq(creatorId), liveRoom.channelName.isNotNull, liveRoom.beginDateTime.loe(now))
|
||||||
|
.fetchFirst()
|
||||||
|
val firstAudioAt = firstAudioDebutAt(creatorId, now)
|
||||||
|
val debutDate = listOfNotNull(firstLiveAt, firstAudioAt).minOrNull()
|
||||||
|
|
||||||
|
return CreatorChannelActivityRecord(
|
||||||
|
debutDate = debutDate,
|
||||||
|
dDay = debutDate?.let { "D+${ChronoUnit.DAYS.between(it.toLocalDate(), now.toLocalDate())}" }.orEmpty(),
|
||||||
|
liveCount = queryFactory
|
||||||
|
.select(liveRoom.id.count())
|
||||||
|
.from(liveRoom)
|
||||||
|
.where(liveRoom.member.id.eq(creatorId), liveRoom.channelName.isNotNull)
|
||||||
|
.fetchOne()
|
||||||
|
?: 0L,
|
||||||
|
liveDurationHours = liveDurationHours(creatorId),
|
||||||
|
liveContributorCount = queryFactory
|
||||||
|
.select(liveRoomVisit.member.id.count())
|
||||||
|
.from(liveRoomVisit)
|
||||||
|
.innerJoin(liveRoomVisit.room, liveRoom)
|
||||||
|
.where(liveRoom.member.id.eq(creatorId), liveRoom.channelName.isNotNull)
|
||||||
|
.fetchOne()
|
||||||
|
?: 0L,
|
||||||
|
audioContentCount = queryFactory
|
||||||
|
.select(audioContent.id.count())
|
||||||
|
.from(audioContent)
|
||||||
|
.where(
|
||||||
|
audioContent.member.id.eq(creatorId),
|
||||||
|
audioContent.isActive.isTrue,
|
||||||
|
audioContent.releaseDate.isNotNull,
|
||||||
|
audioContent.releaseDate.loe(now)
|
||||||
|
)
|
||||||
|
.fetchOne()
|
||||||
|
?: 0L,
|
||||||
|
seriesCount = queryFactory
|
||||||
|
.select(series.id.count())
|
||||||
|
.from(series)
|
||||||
|
.where(series.member.id.eq(creatorId), series.isActive.isTrue)
|
||||||
|
.fetchOne()
|
||||||
|
?: 0L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findSns(creatorId: Long): CreatorChannelSnsRecord {
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
CreatorChannelSnsRecord::class.java,
|
||||||
|
member.instagramUrl.coalesce(""),
|
||||||
|
member.fancimmUrl.coalesce(""),
|
||||||
|
member.xUrl.coalesce(""),
|
||||||
|
member.youtubeUrl.coalesce(""),
|
||||||
|
member.websiteUrl.coalesce("")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(member)
|
||||||
|
.where(member.id.eq(creatorId))
|
||||||
|
.fetchFirst()
|
||||||
|
?: CreatorChannelSnsRecord(
|
||||||
|
instagramUrl = "",
|
||||||
|
fancimmUrl = "",
|
||||||
|
xUrl = "",
|
||||||
|
youtubeUrl = "",
|
||||||
|
kakaoOpenChatUrl = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findAudioContentRows(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
excludedContentId: Long?,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
limit: Int
|
||||||
|
) = queryFactory
|
||||||
|
.select(
|
||||||
|
audioContent.id,
|
||||||
|
audioContent.title,
|
||||||
|
audioContent.duration,
|
||||||
|
audioContent.coverImage,
|
||||||
|
audioContent.price,
|
||||||
|
audioContent.isAdult,
|
||||||
|
audioContent.isPointAvailable,
|
||||||
|
audioContent.releaseDate,
|
||||||
|
audioContent.createdAt
|
||||||
|
)
|
||||||
|
.from(audioContent)
|
||||||
|
.where(
|
||||||
|
audioContent.member.id.eq(creatorId),
|
||||||
|
audioContent.member.isActive.isTrue,
|
||||||
|
audioContent.isActive.isTrue,
|
||||||
|
audioContent.duration.isNotNull,
|
||||||
|
audioContent.releaseDate.isNotNull,
|
||||||
|
audioContent.releaseDate.loe(now),
|
||||||
|
excludedContentId?.let { audioContent.id.ne(it) },
|
||||||
|
adultAudioCondition(canViewAdultContent)
|
||||||
|
)
|
||||||
|
.orderBy(audioContent.releaseDate.desc(), audioContent.id.desc())
|
||||||
|
.limit(limit.toLong())
|
||||||
|
.fetch()
|
||||||
|
|
||||||
|
private fun itAudioId(row: com.querydsl.core.Tuple): Long = row.get(audioContent.id)!!
|
||||||
|
|
||||||
|
private fun com.querydsl.core.Tuple.toAudioRecord(
|
||||||
|
firstContentId: Long?,
|
||||||
|
seriesByContentId: Map<Long, AudioSeriesSummary>
|
||||||
|
): CreatorChannelAudioContentRecord {
|
||||||
|
val audioContentId = get(audioContent.id)!!
|
||||||
|
val seriesSummary = seriesByContentId[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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 firstAudioContentId(creatorId: Long, now: LocalDateTime, canViewAdultContent: Boolean): Long? {
|
||||||
|
return queryFactory
|
||||||
|
.select(audioContent.id)
|
||||||
|
.from(audioContent)
|
||||||
|
.where(
|
||||||
|
audioContent.member.id.eq(creatorId),
|
||||||
|
audioContent.member.isActive.isTrue,
|
||||||
|
audioContent.isActive.isTrue,
|
||||||
|
audioContent.duration.isNotNull,
|
||||||
|
audioContent.releaseDate.isNotNull,
|
||||||
|
audioContent.releaseDate.loe(now),
|
||||||
|
adultAudioCondition(canViewAdultContent)
|
||||||
|
)
|
||||||
|
.orderBy(audioContent.releaseDate.asc(), audioContent.id.asc())
|
||||||
|
.fetchFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun orderedCommunityPostIds(creatorId: Long, viewerId: Long?, postIds: List<Long>): Set<Long> {
|
||||||
|
if (viewerId == null || postIds.isEmpty()) return emptySet()
|
||||||
|
if (viewerId == creatorId) return postIds.toSet()
|
||||||
|
return queryFactory
|
||||||
|
.select(useCan.communityPost.id)
|
||||||
|
.from(useCan)
|
||||||
|
.where(
|
||||||
|
useCan.member.id.eq(viewerId),
|
||||||
|
useCan.communityPost.id.`in`(postIds),
|
||||||
|
useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST),
|
||||||
|
useCan.isRefund.isFalse
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun communityLikeCounts(postIds: List<Long>): Map<Long, Int> {
|
||||||
|
if (postIds.isEmpty()) return emptyMap()
|
||||||
|
return queryFactory
|
||||||
|
.select(creatorCommunityLike.creatorCommunity.id, creatorCommunityLike.id.count())
|
||||||
|
.from(creatorCommunityLike)
|
||||||
|
.where(creatorCommunityLike.creatorCommunity.id.`in`(postIds), creatorCommunityLike.isActive.isTrue)
|
||||||
|
.groupBy(creatorCommunityLike.creatorCommunity.id)
|
||||||
|
.fetch()
|
||||||
|
.associate {
|
||||||
|
it.get(creatorCommunityLike.creatorCommunity.id)!! to
|
||||||
|
(it.get(creatorCommunityLike.id.count())?.toInt() ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun communityCommentCounts(postIds: List<Long>, viewerId: Long?, isContentCreator: Boolean): Map<Long, Int> {
|
||||||
|
if (postIds.isEmpty()) return emptyMap()
|
||||||
|
var where = creatorCommunityComment.creatorCommunity.id.`in`(postIds)
|
||||||
|
.and(creatorCommunityComment.isActive.isTrue)
|
||||||
|
.and(creatorCommunityComment.parent.isNull)
|
||||||
|
|
||||||
|
if (viewerId != null) {
|
||||||
|
where = where
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockedMemberIdSubQuery(viewerId)))
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockingMemberIdSubQuery(viewerId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isContentCreator) {
|
||||||
|
where = where.and(
|
||||||
|
creatorCommunityComment.isSecret.isFalse.or(
|
||||||
|
viewerId?.let { creatorCommunityComment.member.id.eq(it) }
|
||||||
|
?: creatorCommunityComment.isSecret.isFalse
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.select(creatorCommunityComment.creatorCommunity.id, creatorCommunityComment.id.count())
|
||||||
|
.from(creatorCommunityComment)
|
||||||
|
.where(where)
|
||||||
|
.groupBy(creatorCommunityComment.creatorCommunity.id)
|
||||||
|
.fetch()
|
||||||
|
.associate {
|
||||||
|
it.get(creatorCommunityComment.creatorCommunity.id)!! to
|
||||||
|
(it.get(creatorCommunityComment.id.count())?.toInt() ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun blockedMemberIdSubQuery(viewerId: Long) = QBlockMember("communityCommentViewerBlock").let { viewerBlock ->
|
||||||
|
queryFactory
|
||||||
|
.select(viewerBlock.blockedMember.id)
|
||||||
|
.from(viewerBlock)
|
||||||
|
.where(viewerBlock.member.id.eq(viewerId), viewerBlock.isActive.isTrue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun blockingMemberIdSubQuery(viewerId: Long) = QBlockMember("communityCommentWriterBlock").let { writerBlock ->
|
||||||
|
queryFactory
|
||||||
|
.select(writerBlock.member.id)
|
||||||
|
.from(writerBlock)
|
||||||
|
.where(writerBlock.blockedMember.id.eq(viewerId), writerBlock.isActive.isTrue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canAccessPaidCommunityContent(
|
||||||
|
price: Int,
|
||||||
|
viewerId: Long?,
|
||||||
|
creatorId: Long,
|
||||||
|
existOrdered: Boolean
|
||||||
|
): Boolean {
|
||||||
|
return price <= 0 || viewerId == creatorId || existOrdered
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maskPaidCommunityContent(content: String, canAccessPaidContent: Boolean): String {
|
||||||
|
if (canAccessPaidContent) return content
|
||||||
|
val length = content.codePointCount(0, content.length)
|
||||||
|
val endIndex = if (length > 15) {
|
||||||
|
content.offsetByCodePoints(0, 15)
|
||||||
|
} else {
|
||||||
|
content.offsetByCodePoints(0, length / 2)
|
||||||
|
}
|
||||||
|
return content.substring(0, endIndex).plus("...")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun firstAudioDebutAt(creatorId: Long, now: LocalDateTime): LocalDateTime? {
|
||||||
|
val firstThreeUploads = queryFactory
|
||||||
|
.select(audioContent.releaseDate, audioContent.createdAt)
|
||||||
|
.from(audioContent)
|
||||||
|
.where(
|
||||||
|
audioContent.member.id.eq(creatorId),
|
||||||
|
audioContent.duration.isNotNull
|
||||||
|
)
|
||||||
|
.orderBy(audioContent.createdAt.asc(), audioContent.id.asc())
|
||||||
|
.limit(3)
|
||||||
|
.fetch()
|
||||||
|
|
||||||
|
val firstPublishedAt = firstThreeUploads
|
||||||
|
.mapNotNull { it.get(audioContent.releaseDate) }
|
||||||
|
.firstOrNull { !it.isAfter(now) }
|
||||||
|
if (firstPublishedAt != null) return firstPublishedAt
|
||||||
|
|
||||||
|
val thirdUpload = firstThreeUploads.getOrNull(2) ?: return null
|
||||||
|
return if (thirdUpload.get(audioContent.releaseDate) == null) {
|
||||||
|
thirdUpload.get(audioContent.createdAt)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun liveDurationHours(creatorId: Long): Long {
|
||||||
|
return queryFactory
|
||||||
|
.select(liveRoom.beginDateTime, liveRoom.updatedAt)
|
||||||
|
.from(liveRoom)
|
||||||
|
.where(liveRoom.member.id.eq(creatorId), liveRoom.channelName.isNotNull)
|
||||||
|
.fetch()
|
||||||
|
.sumOf { Duration.between(it.get(liveRoom.beginDateTime), it.get(liveRoom.updatedAt)).toSeconds() } / 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
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 fun adultCommunityCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||||
|
return if (canViewAdultContent) null else creatorCommunity.isAdult.isFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixedNoticeCondition(isFixed: Boolean): BooleanExpression? {
|
||||||
|
return if (isFixed) creatorCommunity.fixedAt.isNotNull else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visibleCommunityPostCondition(viewerId: Long?): BooleanExpression {
|
||||||
|
val activePost = creatorCommunity.isActive.isTrue
|
||||||
|
if (viewerId == null) return activePost
|
||||||
|
return activePost.or(
|
||||||
|
queryFactory
|
||||||
|
.select(useCan.id)
|
||||||
|
.from(useCan)
|
||||||
|
.where(
|
||||||
|
useCan.member.id.eq(viewerId),
|
||||||
|
useCan.communityPost.id.eq(creatorCommunity.id),
|
||||||
|
useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST),
|
||||||
|
useCan.isRefund.isFalse
|
||||||
|
)
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adultSeriesCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||||
|
return if (canViewAdultContent) null else series.isAdult.isFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contentTypeSeriesCondition(
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
contentType: ContentType
|
||||||
|
): BooleanExpression? {
|
||||||
|
if (!canViewAdultContent || contentType == ContentType.ALL) return null
|
||||||
|
return series.member.isNull.or(
|
||||||
|
series.member.auth.gender.eq(
|
||||||
|
if (contentType == ContentType.MALE) 0 else 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notBlockedSeriesCreatorCondition(viewerId: Long?): BooleanExpression? {
|
||||||
|
if (viewerId == null) return null
|
||||||
|
val seriesCreatorBlock = QBlockMember("seriesCreatorBlockViewer")
|
||||||
|
return queryFactory
|
||||||
|
.select(seriesCreatorBlock.id)
|
||||||
|
.from(seriesCreatorBlock)
|
||||||
|
.where(
|
||||||
|
seriesCreatorBlock.isActive.isTrue,
|
||||||
|
seriesCreatorBlock.member.id.eq(series.member.id).and(seriesCreatorBlock.blockedMember.id.eq(viewerId))
|
||||||
|
.or(seriesCreatorBlock.member.id.eq(viewerId).and(seriesCreatorBlock.blockedMember.id.eq(series.member.id)))
|
||||||
|
)
|
||||||
|
.exists()
|
||||||
|
.not()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun donationVisibilityCondition(creatorId: Long, viewerId: Long?): BooleanExpression? {
|
||||||
|
return if (viewerId == null) {
|
||||||
|
channelDonationMessage.isSecret.isFalse
|
||||||
|
} else if (viewerId == creatorId) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
channelDonationMessage.isSecret.isFalse.or(channelDonationMessage.member.id.eq(viewerId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun seriesContentStats(
|
||||||
|
seriesIds: List<Long>,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean
|
||||||
|
): Map<Long, SeriesContentStats> {
|
||||||
|
if (seriesIds.isEmpty()) return emptyMap()
|
||||||
|
val publishedAt = audioContent.releaseDate.coalesce(audioContent.createdAt)
|
||||||
|
return queryFactory
|
||||||
|
.select(seriesContent.series.id, seriesContent.id.count(), publishedAt.max())
|
||||||
|
.from(seriesContent)
|
||||||
|
.innerJoin(seriesContent.content, audioContent)
|
||||||
|
.where(
|
||||||
|
seriesContent.series.id.`in`(seriesIds),
|
||||||
|
audioContent.isActive.isTrue,
|
||||||
|
audioContent.duration.isNotNull,
|
||||||
|
audioContent.releaseDate.isNotNull,
|
||||||
|
audioContent.releaseDate.loe(now),
|
||||||
|
adultAudioCondition(canViewAdultContent)
|
||||||
|
)
|
||||||
|
.groupBy(seriesContent.series.id)
|
||||||
|
.fetch()
|
||||||
|
.associate {
|
||||||
|
it.get(seriesContent.series.id)!! to SeriesContentStats(
|
||||||
|
contentCount = it.get(seriesContent.id.count())?.toInt() ?: 0,
|
||||||
|
latestPublishedAt = it.get(publishedAt.max())!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newSeriesIds(
|
||||||
|
seriesIds: List<Long>,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean
|
||||||
|
): Set<Long> {
|
||||||
|
if (seriesIds.isEmpty()) return emptySet()
|
||||||
|
return queryFactory
|
||||||
|
.select(seriesContent.series.id)
|
||||||
|
.from(seriesContent)
|
||||||
|
.innerJoin(seriesContent.content, audioContent)
|
||||||
|
.where(
|
||||||
|
seriesContent.series.id.`in`(seriesIds),
|
||||||
|
audioContent.isActive.isTrue,
|
||||||
|
audioContent.duration.isNotNull,
|
||||||
|
audioContent.releaseDate.between(now.minusDays(7), now),
|
||||||
|
adultAudioCondition(canViewAdultContent)
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notBlockedFanTalkWriterCondition(viewerId: Long?): BooleanExpression? {
|
||||||
|
if (viewerId == null) return null
|
||||||
|
val viewerBlock = QBlockMember("viewerBlockFanTalkWriter")
|
||||||
|
val writerBlock = QBlockMember("writerBlockViewerFanTalk")
|
||||||
|
return creatorCheers.member.id.notIn(
|
||||||
|
queryFactory
|
||||||
|
.select(viewerBlock.blockedMember.id)
|
||||||
|
.from(viewerBlock)
|
||||||
|
.where(viewerBlock.member.id.eq(viewerId), viewerBlock.isActive.isTrue)
|
||||||
|
).and(
|
||||||
|
creatorCheers.member.id.notIn(
|
||||||
|
queryFactory
|
||||||
|
.select(writerBlock.member.id)
|
||||||
|
.from(writerBlock)
|
||||||
|
.where(writerBlock.blockedMember.id.eq(viewerId), writerBlock.isActive.isTrue)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fanTalkSummaryCondition(creatorId: Long, viewerId: Long?): BooleanExpression {
|
||||||
|
return creatorCheers.creator.id.eq(creatorId)
|
||||||
|
.and(creatorCheers.isActive.isTrue)
|
||||||
|
.and(creatorCheers.parent.isNull)
|
||||||
|
.and(notBlockedFanTalkWriterCondition(viewerId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CreatorActivityType.sortOrder: Int
|
||||||
|
get() = when (this) {
|
||||||
|
CreatorActivityType.LIVE -> 0
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AudioSeriesSummary(
|
||||||
|
val title: String,
|
||||||
|
val isOriginal: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class SeriesContentStats(
|
||||||
|
val contentCount: Int,
|
||||||
|
val latestPublishedAt: LocalDateTime
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.port.out
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
|
import kr.co.vividnext.sodalive.member.Gender
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
interface CreatorChannelHomeQueryPort {
|
||||||
|
fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord?
|
||||||
|
|
||||||
|
fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
|
||||||
|
|
||||||
|
fun findCurrentLive(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
viewerId: Long?,
|
||||||
|
isViewerCreator: Boolean,
|
||||||
|
effectiveViewerGender: Gender?
|
||||||
|
): CreatorChannelLiveRecord?
|
||||||
|
|
||||||
|
fun findLatestAudioContent(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean
|
||||||
|
): CreatorChannelAudioContentRecord?
|
||||||
|
|
||||||
|
fun findChannelDonations(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
now: LocalDateTime,
|
||||||
|
limit: Int = 8
|
||||||
|
): List<CreatorChannelDonationRecord>
|
||||||
|
|
||||||
|
fun findCommunityPosts(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
isFixed: Boolean,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
limit: Int = 3
|
||||||
|
): List<CreatorChannelCommunityPostRecord>
|
||||||
|
|
||||||
|
fun findSchedules(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
viewerId: Long?,
|
||||||
|
isViewerCreator: Boolean,
|
||||||
|
effectiveViewerGender: Gender?,
|
||||||
|
limit: Int = 3
|
||||||
|
): List<CreatorChannelScheduleRecord>
|
||||||
|
|
||||||
|
fun findAudioContents(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
latestAudioContentId: Long?,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
limit: Int = 9
|
||||||
|
): List<CreatorChannelAudioContentRecord>
|
||||||
|
|
||||||
|
fun findSeries(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
contentType: ContentType,
|
||||||
|
limit: Int = 8
|
||||||
|
): List<CreatorChannelSeriesRecord>
|
||||||
|
|
||||||
|
fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord
|
||||||
|
|
||||||
|
fun findActivity(creatorId: Long, now: LocalDateTime): CreatorChannelActivityRecord
|
||||||
|
|
||||||
|
fun findSns(creatorId: Long): CreatorChannelSnsRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelCreatorRecord(
|
||||||
|
val creatorId: Long,
|
||||||
|
val characterId: Long?,
|
||||||
|
val nickname: String,
|
||||||
|
val profileImagePath: String?,
|
||||||
|
val introduce: String,
|
||||||
|
val followerCount: Int,
|
||||||
|
val isAiChatAvailable: Boolean,
|
||||||
|
val isDmAvailable: Boolean,
|
||||||
|
val isFollow: Boolean,
|
||||||
|
val isNotify: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelLiveRecord(
|
||||||
|
val liveId: Long,
|
||||||
|
val title: String,
|
||||||
|
val coverImagePath: String?,
|
||||||
|
val beginDateTime: LocalDateTime,
|
||||||
|
val price: Int,
|
||||||
|
val isAdult: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelAudioContentRecord(
|
||||||
|
val audioContentId: Long,
|
||||||
|
val title: String,
|
||||||
|
val duration: String?,
|
||||||
|
val imagePath: String?,
|
||||||
|
val price: Int,
|
||||||
|
val isAdult: Boolean,
|
||||||
|
val isPointAvailable: Boolean,
|
||||||
|
val isFirstContent: Boolean,
|
||||||
|
val publishedAt: LocalDateTime,
|
||||||
|
val seriesName: String?,
|
||||||
|
val isOriginalSeries: Boolean?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelDonationRecord(
|
||||||
|
val nickname: String,
|
||||||
|
val profileImagePath: String?,
|
||||||
|
val can: Int,
|
||||||
|
val message: String,
|
||||||
|
val createdAt: LocalDateTime
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelScheduleRecord(
|
||||||
|
val scheduledAt: LocalDateTime,
|
||||||
|
val title: String,
|
||||||
|
val type: CreatorActivityType,
|
||||||
|
val targetId: Long,
|
||||||
|
val isAdult: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelSeriesRecord(
|
||||||
|
val seriesId: Long,
|
||||||
|
val title: String,
|
||||||
|
val coverImagePath: String?,
|
||||||
|
val numberOfContent: Int,
|
||||||
|
val isNew: Boolean,
|
||||||
|
val isOriginal: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelCommunityPostRecord(
|
||||||
|
val postId: Long,
|
||||||
|
val creatorId: Long,
|
||||||
|
val creatorNickname: String,
|
||||||
|
val creatorProfilePath: String?,
|
||||||
|
val imagePath: String?,
|
||||||
|
val audioPath: String?,
|
||||||
|
val content: String,
|
||||||
|
val price: Int,
|
||||||
|
val date: LocalDateTime,
|
||||||
|
val existOrdered: Boolean,
|
||||||
|
val likeCount: Int,
|
||||||
|
val commentCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelFanTalkSummaryRecord(
|
||||||
|
val totalCount: Int,
|
||||||
|
val latestFanTalk: CreatorChannelFanTalkRecord?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelFanTalkRecord(
|
||||||
|
val fanTalkId: Long,
|
||||||
|
val memberId: Long,
|
||||||
|
val nickname: String,
|
||||||
|
val profileImagePath: String?,
|
||||||
|
val content: String,
|
||||||
|
val languageCode: String?,
|
||||||
|
val createdAt: LocalDateTime
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelActivityRecord(
|
||||||
|
val debutDate: LocalDateTime?,
|
||||||
|
val dDay: String,
|
||||||
|
val liveCount: Long,
|
||||||
|
val liveDurationHours: Long,
|
||||||
|
val liveContributorCount: Long,
|
||||||
|
val audioContentCount: Long,
|
||||||
|
val seriesCount: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelSnsRecord(
|
||||||
|
val instagramUrl: String,
|
||||||
|
val fancimmUrl: String,
|
||||||
|
val xUrl: String,
|
||||||
|
val youtubeUrl: String,
|
||||||
|
val kakaoOpenChatUrl: String
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user