feat(creator-channel): 커뮤니티 탭 repository를 추가한다

This commit is contained in:
2026-06-21 20:44:24 +09:00
parent 2ebe7afab7
commit 078718c041
3 changed files with 790 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityQueryPort
interface CreatorChannelCommunityQueryRepository : CreatorChannelCommunityQueryPort

View File

@@ -0,0 +1,340 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence
import com.querydsl.core.Tuple
import com.querydsl.core.types.OrderSpecifier
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.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
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.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityPostRecord
import org.springframework.stereotype.Repository
@Repository
class DefaultCreatorChannelCommunityQueryRepository(
private val queryFactory: JPAQueryFactory
) : CreatorChannelCommunityQueryRepository {
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord? {
val creator = queryFactory
.select(member.id, member.role, member.nickname)
.from(member)
.where(
member.id.eq(creatorId),
member.isActive.isTrue
)
.fetchFirst() ?: return null
return CreatorChannelCommunityCreatorRecord(
creatorId = creator.get(member.id)!!,
role = creator.get(member.role)!!,
nickname = creator.get(member.nickname)!!
)
}
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean {
val blockMember = QBlockMember("creatorChannelCommunityBlockMember")
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 countCommunityPosts(
creatorId: Long,
viewerId: Long,
canViewAdultContent: Boolean
): Int {
return queryFactory
.select(creatorCommunity.id.count())
.from(creatorCommunity)
.where(communityPostCondition(creatorId, canViewAdultContent))
.fetchOne()
?.toInt()
?: 0
}
override fun findCommunityPosts(
creatorId: Long,
viewerId: Long,
canViewAdultContent: Boolean,
offset: Long,
limit: Int
): List<CreatorChannelCommunityPostRecord> {
val rows = queryFactory
.selectCommunityPostRow()
.from(creatorCommunity)
.where(communityPostCondition(creatorId, canViewAdultContent))
.orderBy(
CaseBuilder()
.`when`(creatorCommunity.isFixed.isTrue)
.then(1)
.otherwise(0)
.desc(),
creatorCommunity.fixedAt.desc().nullsLast(),
CaseBuilder()
.`when`(creatorCommunity.isFixed.isTrue)
.then(creatorCommunity.id)
.otherwise(0L)
.desc(),
creatorCommunity.createdAt.desc(),
creatorCommunity.id.desc()
)
.offset(offset)
.limit(limit.toLong())
.fetch()
return rows.toCommunityPostRecords(creatorId, viewerId)
}
override fun findHomeCommunityPosts(
creatorId: Long,
viewerId: Long,
isPinned: Boolean,
canViewAdultContent: Boolean,
limit: Int
): List<CreatorChannelCommunityPostRecord> {
val rows = queryFactory
.selectCommunityPostRow()
.from(creatorCommunity)
.where(
homeCommunityPostCondition(creatorId, viewerId, canViewAdultContent),
creatorCommunity.isFixed.eq(isPinned),
pinnedPostCondition(isPinned)
)
.orderBy(*homeCommunityPostOrder(isPinned))
.limit(limit.toLong())
.fetch()
return rows.toCommunityPostRecords(creatorId, viewerId)
}
private fun JPAQueryFactory.selectCommunityPostRow() = 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
)
private fun communityPostCondition(creatorId: Long, canViewAdultContent: Boolean): BooleanExpression {
val condition = creatorCommunity.isActive.isTrue
.and(creatorCommunity.member.id.eq(creatorId))
.and(creatorCommunity.member.isActive.isTrue)
return if (canViewAdultContent) condition else condition.and(creatorCommunity.isAdult.isFalse)
}
private fun homeCommunityPostCondition(
creatorId: Long,
viewerId: Long,
canViewAdultContent: Boolean
): BooleanExpression {
val condition = creatorCommunity.member.id.eq(creatorId)
.and(creatorCommunity.member.isActive.isTrue)
.and(
creatorCommunity.isActive.isTrue.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()
)
)
return if (canViewAdultContent) condition else condition.and(creatorCommunity.isAdult.isFalse)
}
private fun pinnedPostCondition(isPinned: Boolean): BooleanExpression? {
return if (isPinned) creatorCommunity.fixedAt.isNotNull else null
}
private fun homeCommunityPostOrder(isPinned: Boolean): Array<OrderSpecifier<*>> {
return if (isPinned) {
arrayOf(creatorCommunity.fixedAt.desc(), creatorCommunity.id.desc())
} else {
arrayOf(creatorCommunity.createdAt.desc(), creatorCommunity.id.desc())
}
}
private fun List<Tuple>.toCommunityPostRecords(
creatorId: Long,
viewerId: Long
): List<CreatorChannelCommunityPostRecord> {
val postIds = map { it.postId }
val orderedPostIds = orderedCommunityPostIds(creatorId, viewerId, postIds)
val likeCounts = communityLikeCounts(postIds)
val commentAvailablePostIds = filter { it.isCommentAvailable }.map { it.postId }
val commentCounts = communityCommentCounts(commentAvailablePostIds, creatorId, viewerId)
return map { row ->
val postId = row.postId
val isPinned = row.isPinned
CreatorChannelCommunityPostRecord(
postId = postId,
creatorId = row.creatorId,
creatorNickname = row.creatorNickname,
creatorProfilePath = row.creatorProfilePath,
imagePath = row.imagePath,
audioPath = row.audioPath,
content = row.content,
price = row.price,
createdAt = row.createdAt,
existOrdered = postId in orderedPostIds,
isCommentAvailable = row.isCommentAvailable,
likeCount = likeCounts[postId] ?: 0,
commentCount = commentCounts[postId] ?: 0,
isPinned = isPinned
)
}
}
private fun orderedCommunityPostIds(
creatorId: Long,
viewerId: Long,
postIds: List<Long>
): Set<Long> {
if (postIds.isEmpty()) return emptySet()
if (creatorId == viewerId) return postIds.toSet()
return queryFactory
.select(useCan.communityPost.id)
.distinct()
.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>,
creatorId: Long,
viewerId: Long
): Map<Long, Int> {
if (postIds.isEmpty()) return emptyMap()
return queryFactory
.select(creatorCommunityComment.creatorCommunity.id, creatorCommunityComment.id.count())
.from(creatorCommunityComment)
.where(
creatorCommunityComment.creatorCommunity.id.`in`(postIds),
creatorCommunityComment.isActive.isTrue,
creatorCommunityComment.parent.isNull,
visibleSecretCommentCondition(creatorId, viewerId),
notBlockedCommentWriterCondition(viewerId)
)
.groupBy(creatorCommunityComment.creatorCommunity.id)
.fetch()
.associate {
it.get(creatorCommunityComment.creatorCommunity.id)!! to
(it.get(creatorCommunityComment.id.count())?.toInt() ?: 0)
}
}
private fun visibleSecretCommentCondition(creatorId: Long, viewerId: Long): BooleanExpression {
return creatorCommunityComment.isSecret.isFalse
.or(
creatorCommunityComment.creatorCommunity.member.id.eq(creatorId)
.and(creatorCommunityComment.creatorCommunity.member.id.eq(viewerId))
)
.or(creatorCommunityComment.member.id.eq(viewerId))
}
private fun notBlockedCommentWriterCondition(viewerId: Long): BooleanExpression {
val viewerBlock = QBlockMember("communityCommentViewerBlockWriter")
val writerBlock = QBlockMember("communityCommentWriterBlockViewer")
return creatorCommunityComment.member.id.notIn(
queryFactory
.select(viewerBlock.blockedMember.id)
.from(viewerBlock)
.where(viewerBlock.member.id.eq(viewerId), viewerBlock.isActive.isTrue)
).and(
creatorCommunityComment.member.id.notIn(
queryFactory
.select(writerBlock.member.id)
.from(writerBlock)
.where(writerBlock.blockedMember.id.eq(viewerId), writerBlock.isActive.isTrue)
)
)
}
private val Tuple.postId: Long
get() = get(creatorCommunity.id)!!
private val Tuple.creatorId: Long
get() = get(creatorCommunity.member.id)!!
private val Tuple.creatorNickname: String
get() = get(creatorCommunity.member.nickname)!!
private val Tuple.creatorProfilePath: String?
get() = get(creatorCommunity.member.profileImage)
private val Tuple.imagePath: String?
get() = get(creatorCommunity.imagePath)
private val Tuple.audioPath: String?
get() = get(creatorCommunity.audioPath)
private val Tuple.content: String
get() = get(creatorCommunity.content)!!
private val Tuple.price: Int
get() = get(creatorCommunity.price)!!
private val Tuple.createdAt
get() = get(creatorCommunity.createdAt)!!
private val Tuple.fixedAt
get() = get(creatorCommunity.fixedAt)
private val Tuple.isPinned: Boolean
get() = get(creatorCommunity.isFixed)!!
private val Tuple.isCommentAvailable: Boolean
get() = get(creatorCommunity.isCommentAvailable)!!
}