test #432

Merged
klaus merged 17 commits from test into main 2026-07-07 18:07:28 +00:00
3 changed files with 370 additions and 0 deletions
Showing only changes of commit cf8acacedf - Show all commits

View File

@@ -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)!!
)
}
}

View File

@@ -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
)

View File

@@ -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,