feat(home): 크리에이터 채널 홈 정책을 보강한다

This commit is contained in:
2026-07-31 01:46:39 +09:00
parent f1c2e6c50b
commit 13e0b42375
20 changed files with 1811 additions and 51 deletions
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository
import kr.co.vividnext.sodalive.content.order.LimitedEditionOrderRepository
import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.pin.PinContent
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
@@ -20,6 +21,7 @@ import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
import kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryService
@@ -52,6 +54,7 @@ class AudioContentServiceTest {
private lateinit var commentRepository: AudioContentCommentRepository
private lateinit var audioContentLikeRepository: AudioContentLikeRepository
private lateinit var pinContentRepository: PinContentRepository
private lateinit var memberRepository: MemberRepository
private lateinit var resourceTranslationJobScheduler: ResourceTranslationJobScheduler
private lateinit var contentTranslationRepository: ContentTranslationRepository
private lateinit var s3Uploader: S3Uploader
@@ -76,6 +79,7 @@ class AudioContentServiceTest {
commentRepository = Mockito.mock(AudioContentCommentRepository::class.java)
audioContentLikeRepository = Mockito.mock(AudioContentLikeRepository::class.java)
pinContentRepository = Mockito.mock(PinContentRepository::class.java)
memberRepository = Mockito.mock(MemberRepository::class.java)
resourceTranslationJobScheduler = Mockito.mock(ResourceTranslationJobScheduler::class.java)
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java)
s3Uploader = Mockito.mock(S3Uploader::class.java)
@@ -97,6 +101,7 @@ class AudioContentServiceTest {
commentRepository = commentRepository,
audioContentLikeRepository = audioContentLikeRepository,
pinContentRepository = pinContentRepository,
memberRepository = memberRepository,
resourceTranslationJobScheduler = resourceTranslationJobScheduler,
contentTranslationRepository = contentTranslationRepository,
s3Uploader = s3Uploader,
@@ -450,6 +455,123 @@ class AudioContentServiceTest {
)
}
@Test
@DisplayName("오디오 상세의 상단 고정 가능 여부는 활성 고정 9개 상한을 따른다")
fun shouldExposeAvailablePinByNineActivePinLimit() {
val creator = createMember(id = 2400L, nickname = "pin-limit-creator")
val audioContent = createAudioContent(creator = creator)
stubSuccessfulDetailDependencies(creator, creator, audioContent)
Mockito.`when`(pinContentRepository.getPinContentList(memberId = creator.id!!, active = true))
.thenReturn(createPinContents(8))
.thenReturn(createPinContents(9))
val availableResponse = service.getDetail(
id = audioContent.id!!,
member = creator,
isAdultContentVisible = false,
timezone = "Asia/Seoul"
)
val unavailableResponse = service.getDetail(
id = audioContent.id!!,
member = creator,
isAdultContentVisible = false,
timezone = "Asia/Seoul"
)
assertTrue(availableResponse.isAvailablePin)
assertEquals(false, unavailableResponse.isAvailablePin)
}
@Test
@DisplayName("10번째 상단 고정은 가장 오래된 활성 고정을 새 콘텐츠로 교체한다")
fun shouldReuseOldestActivePinWhenPinningTenthContent() {
val creator = createMember(id = 2410L, nickname = "pin-reuse-creator")
val audioContent = createAudioContent(creator = creator)
val oldestPinContent = createPinContents(1).single()
Mockito.`when`(repository.findByIdAndCreatorId(contentId = audioContent.id!!, creatorId = creator.id!!))
.thenReturn(audioContent)
Mockito.`when`(memberRepository.findByIdForUpdate(creator.id!!)).thenReturn(creator)
Mockito.`when`(
pinContentRepository.findByContentIdAndMemberId(
contentId = audioContent.id!!,
memberId = creator.id!!
)
).thenReturn(null)
Mockito.`when`(pinContentRepository.getPinContentList(memberId = creator.id!!, active = true))
.thenReturn(listOf(oldestPinContent) + createPinContents(8))
service.pinToTheTop(contentId = audioContent.id!!, member = creator)
Mockito.inOrder(memberRepository, repository, pinContentRepository).apply {
verify(memberRepository).findByIdForUpdate(creator.id!!)
verify(repository).findByIdAndCreatorId(contentId = audioContent.id!!, creatorId = creator.id!!)
verify(pinContentRepository).findByContentIdAndMemberId(
contentId = audioContent.id!!,
memberId = creator.id!!
)
verify(pinContentRepository).getPinContentList(memberId = creator.id!!, active = true)
}
assertTrue(oldestPinContent.isActive)
assertEquals(creator, oldestPinContent.member)
assertEquals(audioContent, oldestPinContent.content)
Mockito.verify(pinContentRepository).save(oldestPinContent)
}
@Test
@DisplayName("상단 고정 해제는 고정 행 조회 전에 크리에이터를 잠근다")
fun shouldLockCreatorBeforeFindingPinWhenUnpinningContent() {
val creator = createMember(id = 2415L, nickname = "pin-unpin-lock-creator")
val audioContent = createAudioContent(creator = creator)
val pinContent = PinContent(isActive = true)
pinContent.member = creator
pinContent.content = audioContent
Mockito.`when`(memberRepository.findByIdForUpdate(creator.id!!)).thenReturn(creator)
Mockito.`when`(
pinContentRepository.findByContentIdAndMemberId(
contentId = audioContent.id!!,
memberId = creator.id!!
)
).thenReturn(pinContent)
service.unpinAtTheTop(contentId = audioContent.id!!, member = creator)
Mockito.inOrder(memberRepository, pinContentRepository).apply {
verify(memberRepository).findByIdForUpdate(creator.id!!)
verify(pinContentRepository).findByContentIdAndMemberId(
contentId = audioContent.id!!,
memberId = creator.id!!
)
}
assertEquals(false, pinContent.isActive)
}
@Test
@DisplayName("비활성 고정을 다시 활성화할 때도 활성 고정은 9개를 넘지 않는다")
fun shouldKeepNineActivePinsWhenReactivatingInactivePin() {
val creator = createMember(id = 2420L, nickname = "pin-reactivate-creator")
val audioContent = createAudioContent(creator = creator)
val inactivePinContent = PinContent(isActive = false)
inactivePinContent.member = creator
inactivePinContent.content = audioContent
val oldestActivePinContent = createPinContents(1).single()
Mockito.`when`(repository.findByIdAndCreatorId(contentId = audioContent.id!!, creatorId = creator.id!!))
.thenReturn(audioContent)
Mockito.`when`(memberRepository.findByIdForUpdate(creator.id!!)).thenReturn(creator)
Mockito.`when`(
pinContentRepository.findByContentIdAndMemberId(
contentId = audioContent.id!!,
memberId = creator.id!!
)
).thenReturn(inactivePinContent)
Mockito.`when`(pinContentRepository.getPinContentList(memberId = creator.id!!, active = true))
.thenReturn(listOf(oldestActivePinContent) + createPinContents(8))
service.pinToTheTop(contentId = audioContent.id!!, member = creator)
assertEquals(false, oldestActivePinContent.isActive)
assertTrue(inactivePinContent.isActive)
}
private fun createMember(id: Long, nickname: String): Member {
val member = Member(
email = "$nickname@test.com",
@@ -496,6 +618,10 @@ class AudioContentServiceTest {
return audioContent
}
private fun createPinContents(count: Int): List<PinContent> {
return (1..count).map { PinContent() }
}
private fun stubSuccessfulDetailDependencies(viewer: Member, creator: Member, audioContent: AudioContent) {
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
Mockito.`when`(explorerQueryRepository.getMember(creator.id!!)).thenReturn(creator)