feat(community): 게시물 댓글 조회 포트를 구현한다
This commit is contained in:
@@ -12,6 +12,8 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreat
|
||||
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.CreatorChannelCommunityCommentContextRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord
|
||||
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
|
||||
@@ -119,6 +121,122 @@ class DefaultCreatorChannelCommunityQueryRepository(
|
||||
return rows.toCommunityPostRecords(creatorId, viewerId)
|
||||
}
|
||||
|
||||
override fun findCommunityPost(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): CreatorChannelCommunityPostRecord? {
|
||||
val rows = queryFactory
|
||||
.selectCommunityPostRow()
|
||||
.from(creatorCommunity)
|
||||
.where(communityPostByIdCondition(postId, canViewAdultContent))
|
||||
.fetch()
|
||||
|
||||
val creatorId = rows.firstOrNull()?.creatorId ?: return null
|
||||
return rows.toCommunityPostRecords(creatorId, viewerId).firstOrNull()
|
||||
}
|
||||
|
||||
override fun countCommunityComments(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
creatorId: Long
|
||||
): Int {
|
||||
return queryFactory
|
||||
.select(creatorCommunityComment.id.count())
|
||||
.from(creatorCommunityComment)
|
||||
.where(rootCommentCondition(postId, creatorId, viewerId))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun findCommunityComments(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
creatorId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelCommunityCommentRecord> {
|
||||
return queryFactory
|
||||
.selectCommunityCommentRow()
|
||||
.from(creatorCommunityComment)
|
||||
.where(rootCommentCondition(postId, creatorId, viewerId))
|
||||
.orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc())
|
||||
.offset(offset)
|
||||
.limit(limit.toLong())
|
||||
.fetch()
|
||||
.map { it.toCommunityCommentRecord() }
|
||||
}
|
||||
|
||||
override fun findLatestRepliesByCommentIds(
|
||||
commentIds: List<Long>,
|
||||
viewerId: Long
|
||||
): List<CreatorChannelCommunityCommentRecord> {
|
||||
if (commentIds.isEmpty()) return emptyList()
|
||||
|
||||
return queryFactory
|
||||
.selectCommunityCommentRow()
|
||||
.from(creatorCommunityComment)
|
||||
.where(replyCondition(commentIds, viewerId))
|
||||
.orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc())
|
||||
.fetch()
|
||||
.map { it.toCommunityCommentRecord() }
|
||||
.groupBy { it.parentCommentId }
|
||||
.values
|
||||
.map { it.first() }
|
||||
}
|
||||
|
||||
override fun findRootCommentContext(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): CreatorChannelCommunityCommentContextRecord? {
|
||||
val row = queryFactory
|
||||
.select(
|
||||
creatorCommunityComment.id,
|
||||
creatorCommunityComment.creatorCommunity.id,
|
||||
creatorCommunityComment.creatorCommunity.member.id,
|
||||
creatorCommunityComment.creatorCommunity.isCommentAvailable
|
||||
)
|
||||
.from(creatorCommunityComment)
|
||||
.where(rootCommentContextCondition(commentId, viewerId, canViewAdultContent))
|
||||
.fetchFirst() ?: return null
|
||||
|
||||
return CreatorChannelCommunityCommentContextRecord(
|
||||
commentId = row.get(creatorCommunityComment.id)!!,
|
||||
postId = row.get(creatorCommunityComment.creatorCommunity.id)!!,
|
||||
creatorId = row.get(creatorCommunityComment.creatorCommunity.member.id)!!,
|
||||
isCommentAvailable = row.get(creatorCommunityComment.creatorCommunity.isCommentAvailable)!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun countCommunityReplies(commentId: Long, viewerId: Long): Int {
|
||||
return queryFactory
|
||||
.select(creatorCommunityComment.id.count())
|
||||
.from(creatorCommunityComment)
|
||||
.where(replyCondition(commentId, viewerId))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
override fun findCommunityReplies(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelCommunityCommentRecord> {
|
||||
return queryFactory
|
||||
.selectCommunityCommentRow()
|
||||
.from(creatorCommunityComment)
|
||||
.where(replyCondition(commentId, viewerId))
|
||||
.orderBy(creatorCommunityComment.createdAt.desc(), creatorCommunityComment.id.desc())
|
||||
.offset(offset)
|
||||
.limit(limit.toLong())
|
||||
.fetch()
|
||||
.map { it.toCommunityCommentRecord() }
|
||||
}
|
||||
|
||||
private fun JPAQueryFactory.selectCommunityPostRow() = select(
|
||||
creatorCommunity.id,
|
||||
creatorCommunity.member.id,
|
||||
@@ -134,6 +252,17 @@ class DefaultCreatorChannelCommunityQueryRepository(
|
||||
creatorCommunity.isCommentAvailable
|
||||
)
|
||||
|
||||
private fun JPAQueryFactory.selectCommunityCommentRow() = select(
|
||||
creatorCommunityComment.id,
|
||||
creatorCommunityComment.parent.id,
|
||||
creatorCommunityComment.creatorCommunity.id,
|
||||
creatorCommunityComment.member.id,
|
||||
creatorCommunityComment.member.profileImage,
|
||||
creatorCommunityComment.member.nickname,
|
||||
creatorCommunityComment.comment,
|
||||
creatorCommunityComment.createdAt
|
||||
)
|
||||
|
||||
private fun communityPostCondition(creatorId: Long, canViewAdultContent: Boolean): BooleanExpression {
|
||||
val condition = creatorCommunity.isActive.isTrue
|
||||
.and(creatorCommunity.member.id.eq(creatorId))
|
||||
@@ -142,6 +271,62 @@ class DefaultCreatorChannelCommunityQueryRepository(
|
||||
return if (canViewAdultContent) condition else condition.and(creatorCommunity.isAdult.isFalse)
|
||||
}
|
||||
|
||||
private fun communityPostByIdCondition(postId: Long, canViewAdultContent: Boolean): BooleanExpression {
|
||||
val condition = creatorCommunity.id.eq(postId)
|
||||
.and(creatorCommunity.isActive.isTrue)
|
||||
.and(creatorCommunity.member.isActive.isTrue)
|
||||
|
||||
return if (canViewAdultContent) condition else condition.and(creatorCommunity.isAdult.isFalse)
|
||||
}
|
||||
|
||||
private fun rootCommentCondition(
|
||||
postId: Long,
|
||||
creatorId: Long,
|
||||
viewerId: Long
|
||||
): BooleanExpression {
|
||||
return creatorCommunityComment.creatorCommunity.id.eq(postId)
|
||||
.and(creatorCommunityComment.isActive.isTrue)
|
||||
.and(creatorCommunityComment.parent.isNull)
|
||||
.and(visibleSecretCommentCondition(creatorId, viewerId))
|
||||
.and(notBlockedCommentWriterCondition(viewerId))
|
||||
}
|
||||
|
||||
private fun rootCommentContextCondition(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): BooleanExpression {
|
||||
val condition = creatorCommunityComment.id.eq(commentId)
|
||||
.and(creatorCommunityComment.isActive.isTrue)
|
||||
.and(creatorCommunityComment.parent.isNull)
|
||||
.and(creatorCommunityComment.creatorCommunity.isActive.isTrue)
|
||||
.and(creatorCommunityComment.creatorCommunity.member.isActive.isTrue)
|
||||
.and(
|
||||
creatorCommunityComment.isSecret.isFalse
|
||||
.or(creatorCommunityComment.creatorCommunity.member.id.eq(viewerId))
|
||||
.or(creatorCommunityComment.member.id.eq(viewerId))
|
||||
)
|
||||
.and(notBlockedCommentWriterCondition(viewerId))
|
||||
|
||||
return if (canViewAdultContent) condition else condition.and(creatorCommunityComment.creatorCommunity.isAdult.isFalse)
|
||||
}
|
||||
|
||||
private fun replyCondition(commentId: Long, viewerId: Long): BooleanExpression {
|
||||
return creatorCommunityComment.isActive.isTrue
|
||||
.and(creatorCommunityComment.parent.isNotNull)
|
||||
.and(creatorCommunityComment.parent.id.eq(commentId))
|
||||
.and(visibleSecretReplyCondition(viewerId))
|
||||
.and(notBlockedCommentWriterCondition(viewerId))
|
||||
}
|
||||
|
||||
private fun replyCondition(commentIds: List<Long>, viewerId: Long): BooleanExpression {
|
||||
return creatorCommunityComment.isActive.isTrue
|
||||
.and(creatorCommunityComment.parent.isNotNull)
|
||||
.and(creatorCommunityComment.parent.id.`in`(commentIds))
|
||||
.and(visibleSecretReplyCondition(viewerId))
|
||||
.and(notBlockedCommentWriterCondition(viewerId))
|
||||
}
|
||||
|
||||
private fun homeCommunityPostCondition(
|
||||
creatorId: Long,
|
||||
viewerId: Long,
|
||||
@@ -302,6 +487,12 @@ class DefaultCreatorChannelCommunityQueryRepository(
|
||||
.or(creatorCommunityComment.member.id.eq(viewerId))
|
||||
}
|
||||
|
||||
private fun visibleSecretReplyCondition(viewerId: Long): BooleanExpression {
|
||||
return creatorCommunityComment.isSecret.isFalse
|
||||
.or(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")
|
||||
@@ -355,4 +546,17 @@ class DefaultCreatorChannelCommunityQueryRepository(
|
||||
|
||||
private val Tuple.isCommentAvailable: Boolean
|
||||
get() = get(creatorCommunity.isCommentAvailable)!!
|
||||
|
||||
private fun Tuple.toCommunityCommentRecord(): CreatorChannelCommunityCommentRecord {
|
||||
return CreatorChannelCommunityCommentRecord(
|
||||
commentId = get(creatorCommunityComment.id)!!,
|
||||
parentCommentId = get(creatorCommunityComment.parent.id),
|
||||
postId = get(creatorCommunityComment.creatorCommunity.id)!!,
|
||||
writerId = get(creatorCommunityComment.member.id)!!,
|
||||
writerProfilePath = get(creatorCommunityComment.member.profileImage),
|
||||
writerNickname = get(creatorCommunityComment.member.nickname)!!,
|
||||
content = get(creatorCommunityComment.comment)!!,
|
||||
createdAt = get(creatorCommunityComment.createdAt)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,49 @@ interface CreatorChannelCommunityQueryPort {
|
||||
canViewAdultContent: Boolean,
|
||||
limit: Int
|
||||
): List<CreatorChannelCommunityPostRecord>
|
||||
|
||||
fun findCommunityPost(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): CreatorChannelCommunityPostRecord?
|
||||
|
||||
fun countCommunityComments(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
creatorId: Long
|
||||
): Int
|
||||
|
||||
fun findCommunityComments(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
creatorId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelCommunityCommentRecord>
|
||||
|
||||
fun findLatestRepliesByCommentIds(
|
||||
commentIds: List<Long>,
|
||||
viewerId: Long
|
||||
): List<CreatorChannelCommunityCommentRecord>
|
||||
|
||||
fun findRootCommentContext(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): CreatorChannelCommunityCommentContextRecord?
|
||||
|
||||
fun countCommunityReplies(
|
||||
commentId: Long,
|
||||
viewerId: Long
|
||||
): Int
|
||||
|
||||
fun findCommunityReplies(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelCommunityCommentRecord>
|
||||
}
|
||||
|
||||
data class CreatorChannelCommunityCreatorRecord(
|
||||
@@ -54,3 +97,21 @@ data class CreatorChannelCommunityPostRecord(
|
||||
val isPinned: Boolean,
|
||||
val isLiked: Boolean
|
||||
)
|
||||
|
||||
data class CreatorChannelCommunityCommentRecord(
|
||||
val commentId: Long,
|
||||
val parentCommentId: Long?,
|
||||
val postId: Long,
|
||||
val writerId: Long,
|
||||
val writerProfilePath: String?,
|
||||
val writerNickname: String,
|
||||
val content: String,
|
||||
val createdAt: LocalDateTime
|
||||
)
|
||||
|
||||
data class CreatorChannelCommunityCommentContextRecord(
|
||||
val commentId: Long,
|
||||
val postId: Long,
|
||||
val creatorId: Long,
|
||||
val isCommentAvailable: Boolean
|
||||
)
|
||||
|
||||
@@ -353,6 +353,111 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
|
||||
assertFalse(adultInactivePurchasedPost.id in adultBlockedPosts.map { it.postId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("게시물 ID 상세 조회는 좋아요와 구매와 성인 정책 필드를 채운다")
|
||||
fun shouldFindCommunityPostByIdWithLikePurchaseAndAdultPolicy() {
|
||||
val viewer = saveMember("detail-viewer", MemberRole.USER)
|
||||
val creator = saveMember("detail-creator", MemberRole.CREATOR)
|
||||
val liker = saveMember("detail-liker", MemberRole.USER)
|
||||
val paidPost = saveCommunity(creator, isFixed = true, price = 100, isAdult = false)
|
||||
val adultPost = saveCommunity(creator, isFixed = false, price = 0, isAdult = true)
|
||||
val inactivePost = saveCommunity(creator, isFixed = false, price = 0, isActive = false)
|
||||
saveCommunityOrder(viewer, paidPost, CanUsage.PAID_COMMUNITY_POST, isRefund = false)
|
||||
saveCommunityLike(viewer, paidPost, isActive = true)
|
||||
saveCommunityLike(liker, paidPost, isActive = true)
|
||||
saveCommunityComment(viewer, paidPost, isActive = true)
|
||||
flushAndClear()
|
||||
|
||||
val record = repository.findCommunityPost(paidPost.id!!, viewer.id!!, canViewAdultContent = true)
|
||||
val blockedAdultRecord = repository.findCommunityPost(adultPost.id!!, viewer.id!!, canViewAdultContent = false)
|
||||
val inactiveRecord = repository.findCommunityPost(inactivePost.id!!, viewer.id!!, canViewAdultContent = true)
|
||||
|
||||
assertNotNull(record)
|
||||
assertEquals(paidPost.id, record!!.postId)
|
||||
assertTrue(record.existOrdered)
|
||||
assertTrue(record.isLiked)
|
||||
assertEquals(2, record.likeCount)
|
||||
assertEquals(1, record.commentCount)
|
||||
assertTrue(record.isPinned)
|
||||
assertNull(blockedAdultRecord)
|
||||
assertNull(inactiveRecord)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최상위 댓글 목록은 비밀 댓글과 차단 작성자를 필터링하고 최신 답글을 반환한다")
|
||||
fun shouldFindVisibleRootCommentsWithLatestReply() {
|
||||
val creator = saveMember("visible-comment-creator", MemberRole.CREATOR)
|
||||
val viewer = saveMember("visible-comment-viewer", MemberRole.USER)
|
||||
val writer = saveMember("visible-comment-writer", MemberRole.USER)
|
||||
val secretWriter = saveMember("visible-secret-writer", MemberRole.USER)
|
||||
val blockedWriter = saveMember("visible-blocked-writer", MemberRole.USER)
|
||||
val replyWriter = saveMember("visible-reply-writer", MemberRole.USER)
|
||||
val post = saveCommunity(creator, isFixed = false, price = 0, isCommentAvailable = true)
|
||||
val oldComment = saveCommunityComment(writer, post, isActive = true)
|
||||
val newComment = saveCommunityComment(viewer, post, isActive = true)
|
||||
saveCommunityComment(secretWriter, post, isActive = true, isSecret = true)
|
||||
saveCommunityComment(blockedWriter, post, isActive = true)
|
||||
saveCommunityComment(writer, post, isActive = false)
|
||||
val oldReply = saveCommunityComment(replyWriter, post, isActive = true, parent = newComment)
|
||||
val latestReply = saveCommunityComment(replyWriter, post, isActive = true, parent = newComment)
|
||||
val hiddenSecretReply = saveCommunityComment(secretWriter, post, isActive = true, isSecret = true, parent = newComment)
|
||||
saveBlock(viewer, blockedWriter, isActive = true)
|
||||
flushAndClear()
|
||||
updateCreatedAt("CreatorCommunityComment", oldComment.id!!, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
updateCreatedAt("CreatorCommunityComment", newComment.id!!, LocalDateTime.of(2026, 7, 6, 11, 0))
|
||||
updateCreatedAt("CreatorCommunityComment", oldReply.id!!, LocalDateTime.of(2026, 7, 6, 11, 1))
|
||||
updateCreatedAt("CreatorCommunityComment", latestReply.id!!, LocalDateTime.of(2026, 7, 6, 11, 2))
|
||||
updateCreatedAt("CreatorCommunityComment", hiddenSecretReply.id!!, LocalDateTime.of(2026, 7, 6, 11, 3))
|
||||
flushAndClear()
|
||||
|
||||
val viewerCount = repository.countCommunityComments(post.id!!, viewer.id!!, creator.id!!)
|
||||
val creatorCount = repository.countCommunityComments(post.id!!, creator.id!!, creator.id!!)
|
||||
val comments = repository.findCommunityComments(post.id!!, viewer.id!!, creator.id!!, offset = 0, limit = 10)
|
||||
val latestReplies = repository.findLatestRepliesByCommentIds(comments.map { it.commentId }, viewer.id!!)
|
||||
|
||||
assertEquals(2, viewerCount)
|
||||
assertEquals(4, creatorCount)
|
||||
assertEquals(listOf(newComment.id, oldComment.id), comments.map { it.commentId })
|
||||
assertEquals(listOf(latestReply.id), latestReplies.map { it.commentId })
|
||||
assertEquals(newComment.id, latestReplies.single().parentCommentId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("답글 조회는 보이는 최상위 댓글 context와 활성 답글만 반환한다")
|
||||
fun shouldFindVisibleRepliesForRootCommentOnly() {
|
||||
val creator = saveMember("reply-context-creator", MemberRole.CREATOR)
|
||||
val viewer = saveMember("reply-context-viewer", MemberRole.USER)
|
||||
val writer = saveMember("reply-context-writer", MemberRole.USER)
|
||||
val blockedWriter = saveMember("reply-context-blocked", MemberRole.USER)
|
||||
val secretWriter = saveMember("reply-context-secret", MemberRole.USER)
|
||||
val post = saveCommunity(creator, isFixed = false, price = 0, isCommentAvailable = true)
|
||||
val root = saveCommunityComment(writer, post, isActive = true)
|
||||
val secretRoot = saveCommunityComment(secretWriter, post, isActive = true, isSecret = true)
|
||||
val oldReply = saveCommunityComment(writer, post, isActive = true, parent = root)
|
||||
val newReply = saveCommunityComment(writer, post, isActive = true, parent = root)
|
||||
saveCommunityComment(secretWriter, post, isActive = true, isSecret = true, parent = root)
|
||||
saveCommunityComment(writer, post, isActive = false, parent = root)
|
||||
saveCommunityComment(blockedWriter, post, isActive = true, parent = root)
|
||||
saveBlock(viewer, blockedWriter, isActive = true)
|
||||
flushAndClear()
|
||||
updateCreatedAt("CreatorCommunityComment", oldReply.id!!, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
updateCreatedAt("CreatorCommunityComment", newReply.id!!, LocalDateTime.of(2026, 7, 6, 11, 0))
|
||||
flushAndClear()
|
||||
|
||||
val context = repository.findRootCommentContext(root.id!!, viewer.id!!, canViewAdultContent = true)
|
||||
val invisibleSecretContext = repository.findRootCommentContext(secretRoot.id!!, viewer.id!!, canViewAdultContent = true)
|
||||
val replyContext = repository.findRootCommentContext(newReply.id!!, viewer.id!!, canViewAdultContent = true)
|
||||
val count = repository.countCommunityReplies(root.id!!, viewer.id!!)
|
||||
val replies = repository.findCommunityReplies(root.id!!, viewer.id!!, offset = 0, limit = 10)
|
||||
|
||||
assertNotNull(context)
|
||||
assertEquals(post.id, context!!.postId)
|
||||
assertNull(invisibleSecretContext)
|
||||
assertNull(replyContext)
|
||||
assertEquals(2, count)
|
||||
assertEquals(listOf(newReply.id, oldReply.id), replies.map { it.commentId })
|
||||
}
|
||||
|
||||
private fun saveMember(
|
||||
nickname: String,
|
||||
role: MemberRole,
|
||||
|
||||
Reference in New Issue
Block a user