feat(live-recommend): 추천 크리에이터 배너를 언어별로 등록하고 노출한다

This commit is contained in:
2026-04-02 18:29:57 +09:00
parent 7f1606a8aa
commit a5ce4b6e0a
13 changed files with 256 additions and 24 deletions

View File

@@ -0,0 +1,126 @@
package kr.co.vividnext.sodalive.admin.live
import com.amazonaws.services.s3.AmazonS3Client
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBannerRepository
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.context.ApplicationEventPublisher
import org.springframework.mock.web.MockMultipartFile
import java.net.URL
class AdminLiveServiceTest {
private val recommendCreatorBannerRepository = Mockito.mock(RecommendLiveCreatorBannerRepository::class.java)
private val roomInfoRepository = Mockito.mock(LiveRoomInfoRedisRepository::class.java)
private val roomCancelRepository = Mockito.mock(LiveRoomCancelRepository::class.java)
private val repository = Mockito.mock(AdminLiveRoomQueryRepository::class.java)
private val memberRepository = Mockito.mock(MemberRepository::class.java)
private val amazonS3Client = Mockito.mock(AmazonS3Client::class.java)
private val s3Uploader = S3Uploader(amazonS3Client)
private val useCanCalculateRepository = Mockito.mock(UseCanCalculateRepository::class.java)
private val reservationRepository = Mockito.mock(LiveReservationRepository::class.java)
private val chargeRepository = Mockito.mock(ChargeRepository::class.java)
private val canRepository = Mockito.mock(CanRepository::class.java)
private val applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
private val messageSource = SodaMessageSource()
private val langContext = LangContext()
private val service = AdminLiveService(
recommendCreatorBannerRepository = recommendCreatorBannerRepository,
roomInfoRepository = roomInfoRepository,
roomCancelRepository = roomCancelRepository,
repository = repository,
memberRepository = memberRepository,
s3Uploader = s3Uploader,
useCanCalculateRepository = useCanCalculateRepository,
reservationRepository = reservationRepository,
chargeRepository = chargeRepository,
canRepository = canRepository,
applicationEventPublisher = applicationEventPublisher,
messageSource = messageSource,
langContext = langContext,
bucket = "test-bucket",
coverImageHost = "https://cdn.test"
)
@Test
@DisplayName("추천 크리에이터 등록은 소문자 lang 코드를 Lang enum으로 저장한다")
fun shouldSaveRecommendCreatorBannerWithIsoLanguageCode() {
val image = MockMultipartFile("image", "banner.png", "image/png", "image".toByteArray())
val creator = Member(
email = "creator@test.com",
password = "password",
nickname = "creator",
role = MemberRole.CREATOR
).also {
it.id = 7L
}
var savedBanner: RecommendLiveCreatorBanner? = null
Mockito.`when`(memberRepository.findCreatorByIdOrNull(7L)).thenReturn(creator)
Mockito.doAnswer {
val banner = it.arguments[0] as RecommendLiveCreatorBanner
banner.id = 55L
savedBanner = banner
banner
}.`when`(recommendCreatorBannerRepository).save(Mockito.any(RecommendLiveCreatorBanner::class.java))
Mockito.`when`(amazonS3Client.getUrl(Mockito.eq("test-bucket"), Mockito.anyString()))
.thenAnswer { URL("https://cdn.test/${it.arguments[1]}") }
service.createRecommendCreatorBanner(
image = image,
creatorId = 7L,
startDateString = "2099-01-01 10:00",
endDateString = "2099-01-02 10:00",
isAdult = false,
lang = "ja"
)
assertEquals(Lang.JA, savedBanner?.lang)
}
@Test
@DisplayName("추천 크리에이터 등록은 지원하지 않는 lang 값이면 예외를 던진다")
fun shouldThrowWhenRecommendCreatorBannerLangIsInvalid() {
val image = MockMultipartFile("image", "banner.png", "image/png", "image".toByteArray())
val creator = Member(
email = "creator@test.com",
password = "password",
nickname = "creator",
role = MemberRole.CREATOR
).also {
it.id = 7L
}
Mockito.`when`(memberRepository.findCreatorByIdOrNull(7L)).thenReturn(creator)
val exception = assertThrows(SodaException::class.java) {
service.createRecommendCreatorBanner(
image = image,
creatorId = 7L,
startDateString = "2099-01-01 10:00",
endDateString = "2099-01-02 10:00",
isAdult = false,
lang = "fr"
)
}
assertEquals("common.error.invalid_request", exception.messageKey)
}
}

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live.recommend
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
@@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
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
@@ -34,6 +36,7 @@ class LiveRecommendRepositoryTest @Autowired constructor(
}
@Test
@DisplayName("추천 크리에이터 조회는 차단 관계를 양방향으로 제외한다")
fun shouldExcludeBlockedCreatorsInBothDirections() {
val viewer = saveMember(nickname = "viewer", role = MemberRole.USER)
val creatorBlockedByViewer = saveMember(nickname = "creator-blocked-by-viewer", role = MemberRole.CREATOR)
@@ -50,13 +53,14 @@ class LiveRecommendRepositoryTest @Autowired constructor(
entityManager.flush()
entityManager.clear()
val result = liveRecommendRepository.getRecommendLive(memberId = viewer.id, isAdult = true)
val result = liveRecommendRepository.getRecommendLive(memberId = viewer.id, isAdult = true, lang = Lang.KO)
assertEquals(1, result.size)
assertEquals(creatorAllowed.id, result[0].creatorId)
}
@Test
@DisplayName("추천 크리에이터 조회는 비활성 차단 관계를 제외하지 않는다")
fun shouldKeepCreatorWhenBlockRelationIsInactive() {
val viewer = saveMember(nickname = "viewer-inactive", role = MemberRole.USER)
val creator = saveMember(nickname = "creator-inactive", role = MemberRole.CREATOR)
@@ -67,13 +71,14 @@ class LiveRecommendRepositoryTest @Autowired constructor(
entityManager.flush()
entityManager.clear()
val result = liveRecommendRepository.getRecommendLive(memberId = viewer.id, isAdult = true)
val result = liveRecommendRepository.getRecommendLive(memberId = viewer.id, isAdult = true, lang = Lang.KO)
assertEquals(1, result.size)
assertEquals(creator.id, result[0].creatorId)
}
@Test
@DisplayName("크리에이터 팔로잉 전체 조회는 알림 여부를 포함한다")
fun shouldReturnFollowingCreatorListWithNotifyFlag() {
val viewer = saveMember(nickname = "viewer-following", role = MemberRole.USER)
val creatorA = saveMember(nickname = "creator-following-a", role = MemberRole.CREATOR)
@@ -98,6 +103,25 @@ class LiveRecommendRepositoryTest @Autowired constructor(
assertEquals(false, isNotifyByCreatorId[creatorB.id])
}
@Test
@DisplayName("추천 크리에이터 조회는 요청 언어와 일치하는 배너만 반환한다")
fun shouldReturnOnlyRequestedLanguageBanners() {
val viewer = saveMember(nickname = "viewer-lang", role = MemberRole.USER)
val koreanCreator = saveMember(nickname = "creator-ko", role = MemberRole.CREATOR)
val japaneseCreator = saveMember(nickname = "creator-ja", role = MemberRole.CREATOR)
saveBanner(creator = koreanCreator, order = 1, lang = Lang.KO)
saveBanner(creator = japaneseCreator, order = 2, lang = Lang.JA)
entityManager.flush()
entityManager.clear()
val result = liveRecommendRepository.getRecommendLive(memberId = viewer.id, isAdult = true, lang = Lang.JA)
assertEquals(1, result.size)
assertEquals(japaneseCreator.id, result.first().creatorId)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return memberRepository.saveAndFlush(
Member(
@@ -110,11 +134,12 @@ class LiveRecommendRepositoryTest @Autowired constructor(
)
}
private fun saveBanner(creator: Member, order: Int) {
private fun saveBanner(creator: Member, order: Int, lang: Lang = Lang.KO) {
val banner = RecommendLiveCreatorBanner(
startDate = LocalDateTime.now().minusDays(1),
endDate = LocalDateTime.now().plusDays(1),
isAdult = false,
lang = lang,
orders = order,
image = "recommend/$order.png"
)

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
@@ -58,12 +59,12 @@ class LiveRecommendServiceTest {
isAdult = true
)
)
Mockito.`when`(liveRecommendCacheService.getRecommendLive(memberId = member.id, isAdult = true)).thenReturn(expected)
Mockito.`when`(liveRecommendCacheService.getRecommendLive(member.id, true, Lang.JA)).thenReturn(expected)
val result = service.getRecommendLive(member)
val result = service.getRecommendLive(member, Lang.JA)
assertEquals(expected, result)
Mockito.verify(liveRecommendCacheService).getRecommendLive(memberId = member.id, isAdult = true)
Mockito.verify(liveRecommendCacheService).getRecommendLive(member.id, true, Lang.JA)
Mockito.verifyNoInteractions(repository)
Mockito.verifyNoInteractions(blockMemberRepository)
}
@@ -71,12 +72,12 @@ class LiveRecommendServiceTest {
@Test
fun shouldDelegateToRepositoryAsGuestWhenMemberIsNull() {
val expected = listOf(GetRecommendLiveResponse(imageUrl = "https://cdn.test/recommend-guest.png", creatorId = 88L))
Mockito.`when`(liveRecommendCacheService.getRecommendLive(memberId = null, isAdult = false)).thenReturn(expected)
Mockito.`when`(liveRecommendCacheService.getRecommendLive(null, false, Lang.EN)).thenReturn(expected)
val result = service.getRecommendLive(null)
val result = service.getRecommendLive(null, Lang.EN)
assertEquals(expected, result)
Mockito.verify(liveRecommendCacheService).getRecommendLive(memberId = null, isAdult = false)
Mockito.verify(liveRecommendCacheService).getRecommendLive(null, false, Lang.EN)
Mockito.verifyNoInteractions(repository)
Mockito.verifyNoInteractions(blockMemberRepository)
Mockito.verifyNoInteractions(memberContentPreferenceService)