feat(creator-channel): FanTalk 탭 repository를 추가한다
This commit is contained in:
@@ -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