test #426

Merged
klaus merged 415 commits from test into main 2026-06-27 00:35:30 +00:00
3 changed files with 790 additions and 0 deletions
Showing only changes of commit 078718c041 - Show all commits

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

View File

@@ -0,0 +1,445 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityPostRecord
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(
properties = [
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE"
]
)
@Import(QueryDslConfig::class)
class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
private val entityManager: EntityManager,
queryFactory: JPAQueryFactory
) {
private val repository = DefaultCreatorChannelCommunityQueryRepository(queryFactory)
@Test
@DisplayName("활성 크리에이터는 조회되고 비활성 크리에이터는 null이다")
fun shouldFindOnlyActiveCreator() {
val viewer = saveMember("creator-lookup-viewer", MemberRole.USER)
val activeCreator = saveMember("active-creator", MemberRole.CREATOR)
val inactiveCreator = saveMember("inactive-creator", MemberRole.CREATOR, isActive = false)
flushAndClear()
val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!)
assertNotNull(activeRecord)
assertEquals(activeCreator.id, activeRecord!!.creatorId)
assertEquals(MemberRole.CREATOR, activeRecord.role)
assertEquals(activeCreator.nickname, activeRecord.nickname)
assertNull(inactiveRecord)
}
@Test
@DisplayName("조회자와 크리에이터 사이 양방향 활성 차단은 차단 상태로 조회된다")
fun shouldFindActiveBlockInBothDirections() {
val viewer = saveMember("block-viewer", MemberRole.USER)
val creator = saveMember("block-creator", MemberRole.CREATOR)
val otherCreator = saveMember("not-blocked-creator", MemberRole.CREATOR)
saveBlock(viewer, creator, isActive = true)
saveBlock(otherCreator, viewer, isActive = false)
flushAndClear()
assertTrue(repository.existsBlockedBetween(viewer.id!!, creator.id!!))
assertTrue(repository.existsBlockedBetween(creator.id!!, viewer.id!!))
assertFalse(repository.existsBlockedBetween(viewer.id!!, otherCreator.id!!))
}
@Test
@DisplayName("게시글 수는 대상 크리에이터의 활성 게시글만 세고 성인 콘텐츠 정책을 우선 적용한다")
fun shouldCountOnlyVisibleActiveCreatorPostsWithAdultFilter() {
val viewer = saveMember("count-viewer", MemberRole.USER)
val creator = saveMember("count-creator", MemberRole.CREATOR)
val otherCreator = saveMember("other-count-creator", MemberRole.CREATOR)
saveCommunity(creator, isFixed = false, price = 0, isAdult = false)
val adultPost = saveCommunity(creator, isFixed = false, price = 100, isAdult = true)
saveCommunity(creator, isFixed = false, price = 0, isActive = false)
saveCommunity(otherCreator, isFixed = false, price = 0, isAdult = false)
saveCommunityOrder(viewer, adultPost, CanUsage.PAID_COMMUNITY_POST, isRefund = false)
flushAndClear()
assertEquals(1, repository.countCommunityPosts(creator.id!!, viewer.id!!, canViewAdultContent = false))
assertEquals(2, repository.countCommunityPosts(creator.id!!, viewer.id!!, canViewAdultContent = true))
val visiblePosts: List<CreatorChannelCommunityPostRecord> = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
canViewAdultContent = false,
offset = 0,
limit = 10
)
val visiblePostIds = visiblePosts.map { it.postId }
assertFalse(adultPost.id in visiblePostIds)
}
@Test
@DisplayName("통합 목록은 고정글 우선 정렬 후 일반글 정렬을 적용하고 offset과 limit으로 페이징한다")
fun shouldFindUnifiedPagedPostsWithPinnedFirstOrdering() {
val viewer = saveMember("ordering-viewer", MemberRole.USER)
val creator = saveMember("ordering-creator", MemberRole.CREATOR)
val now = LocalDateTime.of(2026, 6, 21, 12, 0)
val oldPinned = saveCommunity(creator, isFixed = true, fixedAt = now.minusHours(2), price = 0)
val olderCreatedSameFixedPinned = saveCommunity(creator, isFixed = true, fixedAt = now.minusHours(1), price = 0)
val newerCreatedSameFixedPinned = saveCommunity(creator, isFixed = true, fixedAt = now.minusHours(1), price = 0)
val oldNormal = saveCommunity(creator, isFixed = false, price = 0)
val middleNormal = saveCommunity(creator, isFixed = false, price = 0)
val newNormal = saveCommunity(creator, isFixed = false, price = 0)
flushAndClear()
updateCreatedAt("CreatorCommunity", oldPinned.id!!, now.minusDays(10))
updateCreatedAt("CreatorCommunity", olderCreatedSameFixedPinned.id!!, now.minusDays(1))
updateCreatedAt("CreatorCommunity", newerCreatedSameFixedPinned.id!!, now.minusDays(5))
updateCreatedAt("CreatorCommunity", oldNormal.id!!, now.minusDays(3))
updateCreatedAt("CreatorCommunity", middleNormal.id!!, now.minusDays(2))
updateCreatedAt("CreatorCommunity", newNormal.id!!, now.minusDays(1))
flushAndClear()
val firstPage: List<CreatorChannelCommunityPostRecord> = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 3
)
val secondPage: List<CreatorChannelCommunityPostRecord> = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 3,
limit = 2
)
assertEquals(
listOf(newerCreatedSameFixedPinned.id, olderCreatedSameFixedPinned.id, oldPinned.id),
firstPage.map { it.postId }
)
assertEquals(listOf(newNormal.id, middleNormal.id), secondPage.map { it.postId })
assertTrue(firstPage[0].isPinned)
assertEquals(now.minusDays(5), firstPage[0].createdAt)
}
@Test
@DisplayName("좋아요는 활성 좋아요만 세고 댓글 불가 게시글의 댓글 수는 0이다")
fun shouldCountActiveLikesAndZeroCommentsWhenUnavailable() {
val viewer = saveMember("likes-viewer", MemberRole.USER)
val creator = saveMember("likes-creator", MemberRole.CREATOR)
val activeLiker = saveMember("active-liker", MemberRole.USER)
val inactiveLiker = saveMember("inactive-liker", MemberRole.USER)
val commenter = saveMember("unavailable-commenter", MemberRole.USER)
val post = saveCommunity(creator, isFixed = false, price = 0, isCommentAvailable = false)
saveCommunityLike(activeLiker, post, isActive = true)
saveCommunityLike(inactiveLiker, post, isActive = false)
saveCommunityComment(commenter, post, isActive = true)
flushAndClear()
val record = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
).single()
assertEquals(1, record.likeCount)
assertEquals(0, record.commentCount)
assertFalse(record.isCommentAvailable)
}
@Test
@DisplayName("댓글 수는 활성 최상위 댓글만 세고 비밀 댓글과 차단 작성자 정책을 적용한다")
fun shouldCountVisibleActiveRootCommentsOnly() {
val creator = saveMember("comment-creator", MemberRole.CREATOR)
val viewer = saveMember("comment-viewer", MemberRole.USER)
val secretWriter = saveMember("secret-writer", MemberRole.USER)
val blockedWriter = saveMember("blocked-writer", MemberRole.USER)
val post = saveCommunity(creator, isFixed = false, price = 0, isCommentAvailable = true)
val publicRoot = saveCommunityComment(viewer, post, isActive = true)
saveCommunityComment(viewer, post, isActive = true, parent = publicRoot)
saveCommunityComment(viewer, post, isActive = false)
saveCommunityComment(secretWriter, post, isActive = true, isSecret = true)
saveCommunityComment(blockedWriter, post, isActive = true)
saveBlock(viewer, blockedWriter, isActive = true)
flushAndClear()
val viewerRecord = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
).single()
val writerRecord = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = secretWriter.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
).single()
val creatorRecord = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = creator.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
).single()
assertEquals(1, viewerRecord.commentCount)
assertEquals(3, writerRecord.commentCount)
assertEquals(3, creatorRecord.commentCount)
}
@Test
@DisplayName("유효 구매 내역만 구매 상태로 인정하고 중복 구매는 목록 행을 중복시키지 않는다")
fun shouldUseValidPurchasesWithoutDuplicatingListItems() {
val viewer = saveMember("purchase-viewer", MemberRole.USER)
val creator = saveMember("purchase-creator", MemberRole.CREATOR)
val otherViewer = saveMember("purchase-other-viewer", MemberRole.USER)
val validPost = saveCommunity(creator, isFixed = false, price = 100, imagePath = "valid.png", audioPath = "valid.mp3")
val wrongUsagePost = saveCommunity(creator, isFixed = false, price = 100, imagePath = "wrong-usage.png")
val refundedPost = saveCommunity(creator, isFixed = false, price = 100, imagePath = "refunded.png")
val otherViewerPost = saveCommunity(creator, isFixed = false, price = 100, imagePath = "other-viewer.png")
saveCommunityOrder(viewer, validPost, CanUsage.PAID_COMMUNITY_POST, isRefund = false)
saveCommunityOrder(viewer, validPost, CanUsage.PAID_COMMUNITY_POST, isRefund = false)
saveCommunityOrder(viewer, wrongUsagePost, CanUsage.DONATION, isRefund = false)
saveCommunityOrder(viewer, refundedPost, CanUsage.PAID_COMMUNITY_POST, isRefund = true)
saveCommunityOrder(otherViewer, otherViewerPost, CanUsage.PAID_COMMUNITY_POST, isRefund = false)
flushAndClear()
val records: List<CreatorChannelCommunityPostRecord> = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
)
val recordsById = records.associateBy { it.postId }
assertEquals(records.map { it.postId }.distinct(), records.map { it.postId })
assertTrue(recordsById.getValue(validPost.id!!).existOrdered)
assertEquals("valid.mp3", recordsById.getValue(validPost.id!!).audioPath)
assertFalse(recordsById.getValue(wrongUsagePost.id!!).existOrdered)
assertFalse(recordsById.getValue(refundedPost.id!!).existOrdered)
assertFalse(recordsById.getValue(otherViewerPost.id!!).existOrdered)
}
@Test
@DisplayName("크리에이터 본인은 구매 내역이 없어도 구매 상태로 조회된다")
fun shouldMarkCreatorOwnPostAsOrdered() {
val creator = saveMember("self-order-creator", MemberRole.CREATOR)
saveCommunity(creator, isFixed = false, price = 100, imagePath = "self.png", audioPath = "self.mp3")
flushAndClear()
val record = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = creator.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
).single()
assertTrue(record.existOrdered)
assertEquals("self.mp3", record.audioPath)
}
@Test
@DisplayName("홈 커뮤니티 요약은 고정글과 일반글을 분리해 조회한다")
fun shouldFindHomeCommunityPostsByPinnedFlag() {
val viewer = saveMember("home-summary-viewer", MemberRole.USER)
val creator = saveMember("home-summary-creator", MemberRole.CREATOR)
val now = LocalDateTime.of(2026, 6, 21, 12, 0)
val pinned = saveCommunity(creator, isFixed = true, fixedAt = now.minusHours(1), price = 0)
val normal = saveCommunity(creator, isFixed = false, price = 0)
val adultPinned = saveCommunity(creator, isFixed = true, fixedAt = now, price = 0, isAdult = true)
flushAndClear()
updateCreatedAt("CreatorCommunity", normal.id!!, now.minusDays(1))
flushAndClear()
val pinnedPosts: List<CreatorChannelCommunityPostRecord> = repository.findHomeCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
isPinned = true,
canViewAdultContent = false,
limit = 3
)
val normalPosts: List<CreatorChannelCommunityPostRecord> = repository.findHomeCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
isPinned = false,
canViewAdultContent = false,
limit = 3
)
assertEquals(listOf(pinned.id), pinnedPosts.map { it.postId })
assertEquals(listOf(normal.id), normalPosts.map { it.postId })
assertFalse(adultPinned.id in pinnedPosts.map { it.postId })
assertTrue(pinnedPosts.single().isPinned)
assertFalse(normalPosts.single().isPinned)
}
@Test
@DisplayName("홈 커뮤니티 요약은 구매한 비활성 유료글을 포함하되 성인 콘텐츠 정책을 우선 적용한다")
fun shouldFindPurchasedInactiveHomeCommunityPostsWithAdultFilter() {
val viewer = saveMember("home-purchased-viewer", MemberRole.USER)
val creator = saveMember("home-purchased-creator", MemberRole.CREATOR)
val inactivePurchasedPost = saveCommunity(creator, isFixed = false, price = 100, isActive = false)
val adultInactivePurchasedPost = saveCommunity(
creator = creator,
isFixed = false,
price = 100,
isAdult = true,
isActive = false
)
saveCommunityOrder(viewer, inactivePurchasedPost, CanUsage.PAID_COMMUNITY_POST, isRefund = false)
saveCommunityOrder(viewer, adultInactivePurchasedPost, CanUsage.PAID_COMMUNITY_POST, isRefund = false)
flushAndClear()
val adultAllowedPosts: List<CreatorChannelCommunityPostRecord> = repository.findHomeCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
isPinned = false,
canViewAdultContent = true,
limit = 10
)
val adultBlockedPosts: List<CreatorChannelCommunityPostRecord> = repository.findHomeCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
isPinned = false,
canViewAdultContent = false,
limit = 10
)
assertTrue(inactivePurchasedPost.id in adultAllowedPosts.map { it.postId })
assertTrue(adultInactivePurchasedPost.id in adultAllowedPosts.map { it.postId })
assertTrue(inactivePurchasedPost.id in adultBlockedPosts.map { it.postId })
assertFalse(adultInactivePurchasedPost.id in adultBlockedPosts.map { it.postId })
}
private fun saveMember(
nickname: String,
role: MemberRole,
isActive: Boolean = true,
memberKind: MemberKind = MemberKind.HUMAN
): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = "$nickname.png",
role = role,
memberKind = memberKind,
isActive = isActive
)
entityManager.persist(member)
return member
}
private fun saveBlock(member: Member, blockedMember: Member, isActive: Boolean): BlockMember {
val block = BlockMember(isActive = isActive)
block.member = member
block.blockedMember = blockedMember
entityManager.persist(block)
return block
}
private fun saveCommunity(
creator: Member,
isFixed: Boolean,
fixedAt: LocalDateTime? = null,
price: Int,
imagePath: String? = null,
audioPath: String? = null,
isAdult: Boolean = false,
isCommentAvailable: Boolean = true,
content: String = "community",
isActive: Boolean = true
): CreatorCommunity {
val community = CreatorCommunity(
content = content,
price = price,
isCommentAvailable = isCommentAvailable,
isAdult = isAdult,
audioPath = audioPath,
imagePath = imagePath,
isActive = isActive,
isFixed = isFixed,
fixedAt = fixedAt
)
community.member = creator
entityManager.persist(community)
return community
}
private fun saveCommunityLike(member: Member, community: CreatorCommunity, isActive: Boolean): CreatorCommunityLike {
val like = CreatorCommunityLike(isActive = isActive)
like.member = member
like.creatorCommunity = community
entityManager.persist(like)
return like
}
private fun saveCommunityComment(
member: Member,
community: CreatorCommunity,
isActive: Boolean,
isSecret: Boolean = false,
parent: CreatorCommunityComment? = null
): CreatorCommunityComment {
val comment = CreatorCommunityComment(comment = "comment", isSecret = isSecret, isActive = isActive)
comment.member = member
comment.creatorCommunity = community
comment.parent = parent
entityManager.persist(comment)
return comment
}
private fun saveCommunityOrder(
member: Member,
community: CreatorCommunity,
canUsage: CanUsage,
isRefund: Boolean
): UseCan {
val useCan = UseCan(canUsage, community.price, rewardCan = 0, isRefund = isRefund)
useCan.member = member
useCan.communityPost = community
entityManager.persist(useCan)
return useCan
}
private fun updateCreatedAt(entityName: String, id: Long, createdAt: LocalDateTime) {
entityManager.createQuery("update $entityName e set e.createdAt = :createdAt where e.id = :id")
.setParameter("createdAt", createdAt)
.setParameter("id", id)
.executeUpdate()
}
private fun flushAndClear() {
entityManager.flush()
entityManager.clear()
}
}