feat(creator-channel): FanTalk 탭 repository를 추가한다

This commit is contained in:
2026-06-22 15:52:03 +09:00
parent 2848f07573
commit 408a342f17
3 changed files with 370 additions and 0 deletions
@@ -0,0 +1,227 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMember
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 DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor(
private val entityManager: EntityManager,
queryFactory: JPAQueryFactory
) {
private val repository = DefaultCreatorChannelFanTalkQueryRepository(queryFactory)
@Test
@DisplayName("활성 회원은 role과 닉네임을 조회하고 비활성 회원은 조회하지 않는다")
fun shouldFindOnlyActiveCreator() {
val viewer = saveMember("creator-lookup-viewer", MemberRole.USER)
val activeCreator = saveMember("active-fantalk-creator", MemberRole.CREATOR)
val inactiveCreator = saveMember("inactive-fantalk-creator", MemberRole.CREATOR, isActive = false)
val nonCreator = saveMember("fantalk-non-creator", MemberRole.USER)
flushAndClear()
val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!)
val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!)
assertNotNull(activeRecord)
assertEquals(activeCreator.id, activeRecord!!.creatorId)
assertEquals(MemberRole.CREATOR, activeRecord.role)
assertEquals(activeCreator.nickname, activeRecord.nickname)
assertNull(inactiveRecord)
assertEquals(MemberRole.USER, nonCreatorRecord!!.role)
}
@Test
@DisplayName("조회자와 크리에이터 사이 양방향 활성 차단만 차단 상태로 조회한다")
fun shouldFindActiveBlockInBothDirections() {
val viewer = saveMember("fantalk-block-viewer", MemberRole.USER)
val creator = saveMember("fantalk-block-creator", MemberRole.CREATOR)
val otherCreator = saveMember("fantalk-other-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("최상위 FanTalk 수와 목록은 활성 루트 글만 세고 작성자 차단을 제외한다")
fun shouldCountAndFindOnlyVisibleTopLevelFanTalks() {
val viewer = saveMember("fantalk-list-viewer", MemberRole.USER)
val creator = saveMember("fantalk-list-creator", MemberRole.CREATOR)
val otherCreator = saveMember("fantalk-list-other-creator", MemberRole.CREATOR)
val visibleWriter = saveMember("visible-writer", MemberRole.USER, profileImage = "visible.png")
val blockedWriter = saveMember("blocked-writer", MemberRole.USER)
val writerBlockingViewer = saveMember("writer-blocking-viewer", MemberRole.USER)
val now = LocalDateTime.of(2026, 6, 22, 12, 0)
val older = saveCheers(visibleWriter, creator, "older", isActive = true, createdAt = now.minusHours(2))
val newer = saveCheers(visibleWriter, creator, "newer", isActive = true, createdAt = now.minusHours(1))
saveCheers(visibleWriter, creator, "inactive", isActive = false, createdAt = now)
saveCheers(visibleWriter, otherCreator, "other creator", isActive = true, createdAt = now)
saveCheers(visibleWriter, creator, "reply", isActive = true, createdAt = now, parent = older)
saveCheers(blockedWriter, creator, "viewer blocked", isActive = true, createdAt = now.plusHours(1))
saveCheers(writerBlockingViewer, creator, "writer blocked", isActive = true, createdAt = now.plusHours(2))
saveBlock(viewer, blockedWriter, isActive = true)
saveBlock(writerBlockingViewer, viewer, isActive = true)
flushAndClear()
val count = repository.countFanTalks(creator.id!!, viewer.id!!)
val firstPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 1)
val secondPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 1, limit = 2)
assertEquals(2, count)
assertEquals(listOf(newer.id), firstPage.map { it.fanTalkId })
assertEquals(listOf(older.id), secondPage.map { it.fanTalkId })
assertEquals(visibleWriter.id, firstPage.first().writerId)
assertEquals(visibleWriter.nickname, firstPage.first().writerNickname)
assertEquals(visibleWriter.profileImage, firstPage.first().writerProfileImagePath)
assertEquals("newer", firstPage.first().content)
}
@Test
@DisplayName("최상위 FanTalk 목록은 createdAt desc, id desc 순서로 정렬한다")
fun shouldOrderFanTalksByCreatedAtDescAndIdDesc() {
val viewer = saveMember("fantalk-order-viewer", MemberRole.USER)
val creator = saveMember("fantalk-order-creator", MemberRole.CREATOR)
val writer = saveMember("fantalk-order-writer", MemberRole.USER)
val sameCreatedAt = LocalDateTime.of(2026, 6, 22, 12, 0)
val first = saveCheers(writer, creator, "first", isActive = true, createdAt = sameCreatedAt)
val second = saveCheers(writer, creator, "second", isActive = true, createdAt = sameCreatedAt)
val newest = saveCheers(writer, creator, "newest", isActive = true, createdAt = sameCreatedAt.plusMinutes(1))
flushAndClear()
val records = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 10)
assertEquals(listOf(newest.id, second.id, first.id), records.map { it.fanTalkId })
}
@Test
@DisplayName("크리에이터 답글은 지정한 부모의 활성 크리에이터 작성 답글만 오래된 순으로 조회한다")
fun shouldFindOnlyActiveCreatorRepliesForRequestedParents() {
val creator = saveMember("reply-creator", MemberRole.CREATOR)
val writer = saveMember("reply-writer", MemberRole.USER)
val otherCreator = saveMember("reply-other-creator", MemberRole.CREATOR)
val now = LocalDateTime.of(2026, 6, 22, 12, 0)
val parent = saveCheers(writer, creator, "parent", isActive = true, createdAt = now.minusHours(3))
val otherParent = saveCheers(writer, creator, "other parent", isActive = true, createdAt = now.minusHours(2))
val newerReply = saveCheers(creator, creator, "newer reply", isActive = true, createdAt = now, parent = parent)
val olderReply = saveCheers(
creator,
creator,
"older reply",
isActive = true,
createdAt = now.minusMinutes(1),
parent = parent
)
saveCheers(writer, creator, "fan reply", isActive = true, createdAt = now.plusMinutes(1), parent = parent)
saveCheers(creator, creator, "inactive reply", isActive = false, createdAt = now.plusMinutes(2), parent = parent)
saveCheers(
creator,
otherCreator,
"other creator reply",
isActive = true,
createdAt = now.plusMinutes(3),
parent = parent
)
saveCheers(
creator,
creator,
"other parent reply",
isActive = true,
createdAt = now.plusMinutes(4),
parent = otherParent
)
flushAndClear()
val replies = repository.findCreatorReplies(creator.id!!, listOf(parent.id!!))
val emptyReplies = repository.findCreatorReplies(creator.id!!, emptyList())
assertEquals(listOf(olderReply.id, newerReply.id), replies.map { it.fanTalkId })
assertEquals(parent.id, replies.first().parentFanTalkId)
assertEquals(creator.id, replies.first().writerId)
assertEquals(creator.nickname, replies.first().writerNickname)
assertEquals(creator.profileImage, replies.first().writerProfileImagePath)
assertEquals("older reply", replies.first().content)
assertTrue(emptyReplies.isEmpty())
}
private fun saveMember(
nickname: String,
role: MemberRole,
isActive: Boolean = true,
profileImage: String? = "$nickname.png"
): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = profileImage,
role = role,
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 saveCheers(
member: Member,
creator: Member,
cheers: String,
isActive: Boolean,
createdAt: LocalDateTime,
parent: CreatorCheers? = null
): CreatorCheers {
val creatorCheers = CreatorCheers(cheers = cheers, languageCode = "ko", isActive = isActive)
creatorCheers.member = member
creatorCheers.creator = creator
creatorCheers.parent = parent
entityManager.persist(creatorCheers)
entityManager.flush()
updateCreatedAt("CreatorCheers", creatorCheers.id!!, createdAt)
return creatorCheers
}
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()
}
}