test #426
@@ -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
|
||||||
@@ -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)!!
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user