test #426
@@ -0,0 +1,5 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkQueryPort
|
||||||
|
|
||||||
|
interface CreatorChannelFanTalkQueryRepository : CreatorChannelFanTalkQueryPort
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence
|
||||||
|
|
||||||
|
import com.querydsl.core.types.Projections
|
||||||
|
import com.querydsl.core.types.dsl.BooleanExpression
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers
|
||||||
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
|
import kr.co.vividnext.sodalive.member.block.QBlockMember
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkCreatorRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkReplyRecord
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class DefaultCreatorChannelFanTalkQueryRepository(
|
||||||
|
private val queryFactory: JPAQueryFactory
|
||||||
|
) : CreatorChannelFanTalkQueryRepository {
|
||||||
|
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkCreatorRecord? {
|
||||||
|
val creator = queryFactory
|
||||||
|
.select(member.id, member.role, member.nickname)
|
||||||
|
.from(member)
|
||||||
|
.where(
|
||||||
|
member.id.eq(creatorId),
|
||||||
|
member.isActive.isTrue
|
||||||
|
)
|
||||||
|
.fetchFirst() ?: return null
|
||||||
|
|
||||||
|
return CreatorChannelFanTalkCreatorRecord(
|
||||||
|
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("creatorChannelFanTalkBlockMember")
|
||||||
|
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 countFanTalks(creatorId: Long, viewerId: Long): Int {
|
||||||
|
return queryFactory
|
||||||
|
.select(creatorCheers.id.count())
|
||||||
|
.from(creatorCheers)
|
||||||
|
.where(fanTalkCondition(creatorId, viewerId))
|
||||||
|
.fetchOne()
|
||||||
|
?.toInt()
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findFanTalks(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelFanTalkRecord> {
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
CreatorChannelFanTalkRecord::class.java,
|
||||||
|
creatorCheers.id,
|
||||||
|
creatorCheers.member.id,
|
||||||
|
creatorCheers.member.nickname,
|
||||||
|
creatorCheers.member.profileImage,
|
||||||
|
creatorCheers.cheers,
|
||||||
|
creatorCheers.createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(creatorCheers)
|
||||||
|
.where(fanTalkCondition(creatorId, viewerId))
|
||||||
|
.orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit.toLong())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findCreatorReplies(
|
||||||
|
creatorId: Long,
|
||||||
|
parentFanTalkIds: List<Long>
|
||||||
|
): List<CreatorChannelFanTalkReplyRecord> {
|
||||||
|
if (parentFanTalkIds.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
CreatorChannelFanTalkReplyRecord::class.java,
|
||||||
|
creatorCheers.id,
|
||||||
|
creatorCheers.parent.id,
|
||||||
|
creatorCheers.member.id,
|
||||||
|
creatorCheers.member.nickname,
|
||||||
|
creatorCheers.member.profileImage,
|
||||||
|
creatorCheers.cheers,
|
||||||
|
creatorCheers.createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(creatorCheers)
|
||||||
|
.where(
|
||||||
|
creatorCheers.creator.id.eq(creatorId),
|
||||||
|
creatorCheers.member.id.eq(creatorId),
|
||||||
|
creatorCheers.isActive.isTrue,
|
||||||
|
creatorCheers.parent.id.`in`(parentFanTalkIds)
|
||||||
|
)
|
||||||
|
.orderBy(creatorCheers.createdAt.asc(), creatorCheers.id.asc())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fanTalkCondition(creatorId: Long, viewerId: Long): BooleanExpression {
|
||||||
|
return creatorCheers.creator.id.eq(creatorId)
|
||||||
|
.and(creatorCheers.isActive.isTrue)
|
||||||
|
.and(creatorCheers.parent.isNull)
|
||||||
|
.and(notBlockedFanTalkWriterCondition(viewerId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notBlockedFanTalkWriterCondition(viewerId: Long): BooleanExpression {
|
||||||
|
val viewerBlock = QBlockMember("viewerBlockFanTalkTabWriter")
|
||||||
|
val writerBlock = QBlockMember("writerBlockFanTalkTabViewer")
|
||||||
|
return creatorCheers.member.id.notIn(
|
||||||
|
queryFactory
|
||||||
|
.select(viewerBlock.blockedMember.id)
|
||||||
|
.from(viewerBlock)
|
||||||
|
.where(viewerBlock.member.id.eq(viewerId), viewerBlock.isActive.isTrue)
|
||||||
|
).and(
|
||||||
|
creatorCheers.member.id.notIn(
|
||||||
|
queryFactory
|
||||||
|
.select(writerBlock.member.id)
|
||||||
|
.from(writerBlock)
|
||||||
|
.where(writerBlock.blockedMember.id.eq(viewerId), writerBlock.isActive.isTrue)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user