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

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

View File

@@ -0,0 +1,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()
}
}