From 90c5149df8608fac9c3a4af86d0bf43aa52508f3 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 24 Jun 2026 19:01:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(content-ranking):=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20=EC=A1=B0=ED=9A=8C=20=ED=8F=AC=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultAudioRankingBlockRepository.kt | 39 +++++++ .../ranking/port/out/AudioRankingBlockPort.kt | 5 + .../DefaultAudioRankingBlockRepositoryTest.kt | 100 ++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/port/out/AudioRankingBlockPort.kt create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepositoryTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepository.kt new file mode 100644 index 00000000..1bc97e3e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepository.kt @@ -0,0 +1,39 @@ +package kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.block.QBlockMember +import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingBlockPort +import org.springframework.stereotype.Repository + +@Repository +class DefaultAudioRankingBlockRepository( + private val queryFactory: JPAQueryFactory +) : AudioRankingBlockPort { + override fun findBlockedCreatorMemberIds(memberId: Long, creatorMemberIds: Set): Set { + if (creatorMemberIds.isEmpty()) return emptySet() + val viewerBlock = QBlockMember("audioRankingViewerBlock") + val creatorBlock = QBlockMember("audioRankingCreatorBlock") + + val viewerBlockedIds = queryFactory + .select(viewerBlock.blockedMember.id) + .from(viewerBlock) + .where( + viewerBlock.isActive.isTrue, + viewerBlock.member.id.eq(memberId), + viewerBlock.blockedMember.id.`in`(creatorMemberIds) + ) + .fetch() + + val creatorBlockedIds = queryFactory + .select(creatorBlock.member.id) + .from(creatorBlock) + .where( + creatorBlock.isActive.isTrue, + creatorBlock.member.id.`in`(creatorMemberIds), + creatorBlock.blockedMember.id.eq(memberId) + ) + .fetch() + + return (viewerBlockedIds + creatorBlockedIds).toSet() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/port/out/AudioRankingBlockPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/port/out/AudioRankingBlockPort.kt new file mode 100644 index 00000000..26ae6805 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/port/out/AudioRankingBlockPort.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.v2.content.ranking.port.out + +interface AudioRankingBlockPort { + fun findBlockedCreatorMemberIds(memberId: Long, creatorMemberIds: Set): Set +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepositoryTest.kt new file mode 100644 index 00000000..ad36da59 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepositoryTest.kt @@ -0,0 +1,100 @@ +package kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.configs.QueryDslConfig +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.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 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 DefaultAudioRankingBlockRepositoryTest @Autowired constructor( + private val entityManager: EntityManager, + queryFactory: JPAQueryFactory +) { + private val repository = DefaultAudioRankingBlockRepository(queryFactory) + + @Test + @DisplayName("활성 양방향 차단된 크리에이터 member id만 조회한다") + fun shouldFindActiveBlockedCreatorMemberIdsInBothDirections() { + val viewer = saveUser("viewer") + val blockedByViewer = saveCreator("blocked-by-viewer") + val blocksViewer = saveCreator("blocks-viewer") + val inactiveBlocked = saveCreator("inactive-blocked") + val allowed = saveCreator("allowed") + val outsideInput = saveCreator("outside-input") + saveBlock(viewer, blockedByViewer, isActive = true) + saveBlock(blocksViewer, viewer, isActive = true) + saveBlock(viewer, inactiveBlocked, isActive = false) + saveBlock(viewer, outsideInput, isActive = true) + flushAndClear() + + val blockedCreatorMemberIds = repository.findBlockedCreatorMemberIds( + memberId = viewer.id!!, + creatorMemberIds = setOf(blockedByViewer.id!!, blocksViewer.id!!, inactiveBlocked.id!!, allowed.id!!) + ) + + assertEquals(setOf(blockedByViewer.id, blocksViewer.id), blockedCreatorMemberIds) + } + + @Test + @DisplayName("크리에이터 member id 목록이 비어 있으면 빈 집합을 반환한다") + fun shouldReturnEmptySetWhenCreatorMemberIdsIsEmpty() { + val viewer = saveUser("empty-viewer") + val creator = saveCreator("empty-creator") + saveBlock(viewer, creator, isActive = true) + flushAndClear() + + val blockedCreatorMemberIds = repository.findBlockedCreatorMemberIds( + memberId = viewer.id!!, + creatorMemberIds = emptySet() + ) + + assertEquals(emptySet(), blockedCreatorMemberIds) + } + + private fun saveCreator(nickname: String): Member { + return saveMember(nickname, MemberRole.CREATOR) + } + + private fun saveUser(nickname: String): Member { + return saveMember(nickname, MemberRole.USER) + } + + private fun saveMember(nickname: String, role: MemberRole): Member { + val member = Member( + email = "$nickname@test.com", + password = "password", + nickname = nickname, + role = role, + isActive = true + ) + entityManager.persist(member) + entityManager.flush() + return member + } + + private fun saveBlock(member: Member, blockedMember: Member, isActive: Boolean) { + val block = BlockMember(isActive = isActive) + block.member = member + block.blockedMember = blockedMember + entityManager.persist(block) + } + + private fun flushAndClear() { + entityManager.flush() + entityManager.clear() + } +}