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,41 @@
SET @schema_name := DATABASE();
SET @lang_column_exists := (
SELECT COUNT(1)
FROM information_schema.columns
WHERE table_schema = @schema_name
AND table_name = 'recommend_live_creator_banner'
AND column_name = 'lang'
);
SET @add_lang_column_sql := IF(
@lang_column_exists = 0,
'ALTER TABLE recommend_live_creator_banner ADD COLUMN lang VARCHAR(10) NULL COMMENT ''배너 노출 언어'' AFTER is_adult',
'SELECT ''recommend_live_creator_banner.lang already exists'' AS message'
);
PREPARE add_lang_column_stmt FROM @add_lang_column_sql;
EXECUTE add_lang_column_stmt;
DEALLOCATE PREPARE add_lang_column_stmt;
UPDATE recommend_live_creator_banner
SET lang = 'KO'
WHERE lang IS NULL;
SET @lang_column_nullable := (
SELECT IS_NULLABLE
FROM information_schema.columns
WHERE table_schema = @schema_name
AND table_name = 'recommend_live_creator_banner'
AND column_name = 'lang'
);
SET @alter_lang_column_sql := IF(
@lang_column_nullable = 'YES',
'ALTER TABLE recommend_live_creator_banner MODIFY COLUMN lang VARCHAR(10) NOT NULL DEFAULT ''KO'' COMMENT ''배너 노출 언어 (KO 기본, EN/JA 추가 가능)''',
'SELECT ''recommend_live_creator_banner.lang already normalized'' AS message'
);
PREPARE alter_lang_column_stmt FROM @alter_lang_column_sql;
EXECUTE alter_lang_column_stmt;
DEALLOCATE PREPARE alter_lang_column_stmt;

View File

@@ -0,0 +1,11 @@
- [x] 추천 크리에이터 배너 등록·조회 경로와 언어 처리 기준을 확인한다.
- [x] 추천 크리에이터 등록 API에 `lang` 파라미터를 추가하고 `Lang` 기준으로 저장하도록 수정한다.
- [x] 관리자 추천 크리에이터 목록은 전체 언어를 유지하고, `LiveApiService.fetchData`의 추천 크리에이터 조회는 사용자 언어에 맞는 배너만 반환하도록 수정한다.
- [x] 변경 파일 기준으로 검증을 수행하고 결과를 기록한다.
## 검증 기록
### 1차 구현
- 무엇을: 추천 크리에이터 배너 엔티티와 관리자 등록 API에 `lang`을 추가하고, 라이브 메인 `fetchData``/live/recommend` 조회가 현재 요청 언어와 일치하는 배너만 조회하도록 수정했다. 운영 반영용으로 `recommend_live_creator_banner.lang` 컬럼 DDL 문서도 추가했다.
- 왜: 관리자에서는 언어별 추천 크리에이터 배너를 등록할 수 있어야 하고, 사용자 라이브 화면에서는 자신의 언어와 맞는 추천 크리에이터만 노출되어야 하기 때문이다. 관리자 목록 API는 기존처럼 전체 언어 배너를 그대로 조회해야 한다.
- 어떻게: Kotlin LSP가 없어 정적 진단은 Gradle 검증으로 대체했고, `./gradlew test --tests "kr.co.vividnext.sodalive.admin.live.AdminLiveServiceTest" --tests "kr.co.vividnext.sodalive.live.recommend.LiveRecommendServiceTest" --tests "kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepositoryTest"`로 소문자 `lang` 저장, 서비스 언어 전달, 언어별 추천 배너 조회를 검증했다. 이어서 `./gradlew ktlintCheck``./gradlew build`를 실행했고 모두 `BUILD SUCCESSFUL`이다.

View File

@@ -39,9 +39,10 @@ class AdminLiveController(private val service: AdminLiveService) {
@RequestParam("creator_id") creatorId: Long, @RequestParam("creator_id") creatorId: Long,
@RequestParam("start_date") startDate: String, @RequestParam("start_date") startDate: String,
@RequestParam("end_date") endDate: String, @RequestParam("end_date") endDate: String,
@RequestParam("is_adult") isAdult: Boolean @RequestParam("is_adult") isAdult: Boolean,
@RequestParam("lang") lang: String
) = ApiResponse.ok( ) = ApiResponse.ok(
service.createRecommendCreatorBanner(image, creatorId, startDate, endDate, isAdult), service.createRecommendCreatorBanner(image, creatorId, startDate, endDate, isAdult, lang),
"등록되었습니다." "등록되었습니다."
) )

View File

@@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner
@@ -122,7 +123,8 @@ class AdminLiveService(
creatorId: Long, creatorId: Long,
startDateString: String, startDateString: String,
endDateString: String, endDateString: String,
isAdult: Boolean isAdult: Boolean,
lang: String
): Long { ): Long {
if (creatorId < 1) throw SodaException(messageKey = "admin.live.creator_required") if (creatorId < 1) throw SodaException(messageKey = "admin.live.creator_required")
@@ -150,10 +152,17 @@ class AdminLiveService(
if (endDate < nowDate) throw SodaException(messageKey = "admin.live.end_after_now") if (endDate < nowDate) throw SodaException(messageKey = "admin.live.end_after_now")
if (endDate <= startDate) throw SodaException(messageKey = "admin.live.start_before_end") if (endDate <= startDate) throw SodaException(messageKey = "admin.live.start_before_end")
val bannerLang = try {
Lang.fromCode(lang)
} catch (_: IllegalArgumentException) {
throw SodaException(messageKey = "common.error.invalid_request")
}
val recommendCreatorBanner = RecommendLiveCreatorBanner( val recommendCreatorBanner = RecommendLiveCreatorBanner(
startDate = startDate, startDate = startDate,
endDate = endDate, endDate = endDate,
isAdult = isAdult isAdult = isAdult,
lang = bannerLang
) )
recommendCreatorBanner.creator = creator recommendCreatorBanner.creator = creator
recommendCreatorBannerRepository.save(recommendCreatorBanner) recommendCreatorBannerRepository.save(recommendCreatorBanner)

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.api.live
import kr.co.vividnext.sodalive.content.AudioContentService import kr.co.vividnext.sodalive.content.AudioContentService
import kr.co.vividnext.sodalive.content.ContentType import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendService import kr.co.vividnext.sodalive.live.recommend.LiveRecommendService
import kr.co.vividnext.sodalive.live.room.LiveRoomService import kr.co.vividnext.sodalive.live.room.LiveRoomService
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
@@ -20,8 +21,8 @@ class LiveApiService(
private val recommendService: LiveRecommendService, private val recommendService: LiveRecommendService,
private val creatorCommunityService: CreatorCommunityService, private val creatorCommunityService: CreatorCommunityService,
private val memberContentPreferenceService: MemberContentPreferenceService, private val memberContentPreferenceService: MemberContentPreferenceService,
private val blockMemberRepository: BlockMemberRepository,
private val blockMemberRepository: BlockMemberRepository private val langContext: LangContext
) { ) {
fun fetchData( fun fetchData(
timezone: String, timezone: String,
@@ -49,7 +50,7 @@ class LiveApiService(
listOf() listOf()
} }
val recommendLiveList = recommendService.getRecommendLive(member) val recommendLiveList = recommendService.getRecommendLive(member, langContext.lang)
val latestFinishedLiveList = liveService.getLatestFinishedLive(member) val latestFinishedLiveList = liveService.getLatestFinishedLive(member)

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.live.recommend package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.i18n.Lang
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -9,12 +10,13 @@ class LiveRecommendCacheService(
) { ) {
@Cacheable( @Cacheable(
cacheNames = ["cache_ttl_3_hours"], cacheNames = ["cache_ttl_3_hours"],
key = "'getRecommendLive:' + (#memberId ?: 'guest') + ':' + #isAdult" key = "'getRecommendLive:' + (#memberId ?: 'guest') + ':' + #isAdult + ':' + #lang.name()"
) )
fun getRecommendLive(memberId: Long?, isAdult: Boolean): List<GetRecommendLiveResponse> { fun getRecommendLive(memberId: Long?, isAdult: Boolean, lang: Lang): List<GetRecommendLiveResponse> {
return repository.getRecommendLive( return repository.getRecommendLive(
memberId = memberId, memberId = memberId,
isAdult = isAdult isAdult = isAdult,
lang = lang
) )
} }
} }

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -11,12 +12,15 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/live/recommend") @RequestMapping("/live/recommend")
class LiveRecommendController(private val service: LiveRecommendService) { class LiveRecommendController(
private val service: LiveRecommendService,
private val langContext: LangContext
) {
@GetMapping @GetMapping
fun getRecommendLive( fun getRecommendLive(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
ApiResponse.ok(service.getRecommendLive(member)) ApiResponse.ok(service.getRecommendLive(member, langContext.lang))
} }
@GetMapping("/channel") @GetMapping("/channel")

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.live.recommend
import com.querydsl.core.types.Projections import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.Expressions import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
@@ -22,12 +23,14 @@ class LiveRecommendRepository(
) { ) {
fun getRecommendLive( fun getRecommendLive(
memberId: Long?, memberId: Long?,
isAdult: Boolean isAdult: Boolean,
lang: Lang
): List<GetRecommendLiveResponse> { ): List<GetRecommendLiveResponse> {
val dateNow = LocalDateTime.now() val dateNow = LocalDateTime.now()
var where = recommendLiveCreatorBanner.startDate.loe(dateNow) var where = recommendLiveCreatorBanner.startDate.loe(dateNow)
.and(recommendLiveCreatorBanner.endDate.goe(dateNow)) .and(recommendLiveCreatorBanner.endDate.goe(dateNow))
.and(recommendLiveCreatorBanner.lang.eq(lang))
if (!isAdult) { if (!isAdult) {
where = where.and(recommendLiveCreatorBanner.isAdult.isFalse) where = where.and(recommendLiveCreatorBanner.isAdult.isFalse)

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.live.recommend 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.Member
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
@@ -14,7 +15,7 @@ class LiveRecommendService(
private val memberContentPreferenceService: MemberContentPreferenceService, private val memberContentPreferenceService: MemberContentPreferenceService,
private val liveRecommendCacheService: LiveRecommendCacheService private val liveRecommendCacheService: LiveRecommendCacheService
) { ) {
fun getRecommendLive(member: Member?): List<GetRecommendLiveResponse> { fun getRecommendLive(member: Member?, lang: Lang): List<GetRecommendLiveResponse> {
val isAdult = if (member != null) { val isAdult = if (member != null) {
memberContentPreferenceService.getStoredPreference(member).isAdult memberContentPreferenceService.getStoredPreference(member).isAdult
} else { } else {
@@ -23,7 +24,8 @@ class LiveRecommendService(
return liveRecommendCacheService.getRecommendLive( return liveRecommendCacheService.getRecommendLive(
memberId = member?.id, memberId = member?.id,
isAdult = isAdult isAdult = isAdult,
lang = lang
) )
} }

View File

@@ -1,10 +1,13 @@
package kr.co.vividnext.sodalive.live.recommend package kr.co.vividnext.sodalive.live.recommend
import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.persistence.Column import javax.persistence.Column
import javax.persistence.Entity import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
import javax.persistence.FetchType import javax.persistence.FetchType
import javax.persistence.JoinColumn import javax.persistence.JoinColumn
import javax.persistence.ManyToOne import javax.persistence.ManyToOne
@@ -18,6 +21,9 @@ data class RecommendLiveCreatorBanner(
@Column(nullable = false) @Column(nullable = false)
var isAdult: Boolean = false, var isAdult: Boolean = false,
@Column(nullable = false) @Column(nullable = false)
@Enumerated(EnumType.STRING)
var lang: Lang = Lang.KO,
@Column(nullable = false)
var orders: Int = 1, var orders: Int = 1,
@Column(nullable = true) @Column(nullable = true)
var image: String? = null var image: String? = null

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 com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig 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.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole 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 kr.co.vividnext.sodalive.member.following.CreatorFollowing
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
@@ -34,6 +36,7 @@ class LiveRecommendRepositoryTest @Autowired constructor(
} }
@Test @Test
@DisplayName("추천 크리에이터 조회는 차단 관계를 양방향으로 제외한다")
fun shouldExcludeBlockedCreatorsInBothDirections() { fun shouldExcludeBlockedCreatorsInBothDirections() {
val viewer = saveMember(nickname = "viewer", role = MemberRole.USER) val viewer = saveMember(nickname = "viewer", role = MemberRole.USER)
val creatorBlockedByViewer = saveMember(nickname = "creator-blocked-by-viewer", role = MemberRole.CREATOR) val creatorBlockedByViewer = saveMember(nickname = "creator-blocked-by-viewer", role = MemberRole.CREATOR)
@@ -50,13 +53,14 @@ class LiveRecommendRepositoryTest @Autowired constructor(
entityManager.flush() entityManager.flush()
entityManager.clear() 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(1, result.size)
assertEquals(creatorAllowed.id, result[0].creatorId) assertEquals(creatorAllowed.id, result[0].creatorId)
} }
@Test @Test
@DisplayName("추천 크리에이터 조회는 비활성 차단 관계를 제외하지 않는다")
fun shouldKeepCreatorWhenBlockRelationIsInactive() { fun shouldKeepCreatorWhenBlockRelationIsInactive() {
val viewer = saveMember(nickname = "viewer-inactive", role = MemberRole.USER) val viewer = saveMember(nickname = "viewer-inactive", role = MemberRole.USER)
val creator = saveMember(nickname = "creator-inactive", role = MemberRole.CREATOR) val creator = saveMember(nickname = "creator-inactive", role = MemberRole.CREATOR)
@@ -67,13 +71,14 @@ class LiveRecommendRepositoryTest @Autowired constructor(
entityManager.flush() entityManager.flush()
entityManager.clear() 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(1, result.size)
assertEquals(creator.id, result[0].creatorId) assertEquals(creator.id, result[0].creatorId)
} }
@Test @Test
@DisplayName("크리에이터 팔로잉 전체 조회는 알림 여부를 포함한다")
fun shouldReturnFollowingCreatorListWithNotifyFlag() { fun shouldReturnFollowingCreatorListWithNotifyFlag() {
val viewer = saveMember(nickname = "viewer-following", role = MemberRole.USER) val viewer = saveMember(nickname = "viewer-following", role = MemberRole.USER)
val creatorA = saveMember(nickname = "creator-following-a", role = MemberRole.CREATOR) val creatorA = saveMember(nickname = "creator-following-a", role = MemberRole.CREATOR)
@@ -98,6 +103,25 @@ class LiveRecommendRepositoryTest @Autowired constructor(
assertEquals(false, isNotifyByCreatorId[creatorB.id]) 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 { private fun saveMember(nickname: String, role: MemberRole): Member {
return memberRepository.saveAndFlush( return memberRepository.saveAndFlush(
Member( 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( val banner = RecommendLiveCreatorBanner(
startDate = LocalDateTime.now().minusDays(1), startDate = LocalDateTime.now().minusDays(1),
endDate = LocalDateTime.now().plusDays(1), endDate = LocalDateTime.now().plusDays(1),
isAdult = false, isAdult = false,
lang = lang,
orders = order, orders = order,
image = "recommend/$order.png" image = "recommend/$order.png"
) )

View File

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