From a5ce4b6e0aab72b56c053ff52ac65e3f18ebf218 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 2 Apr 2026 18:29:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(live-recommend):=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0=20=EB=B0=B0?= =?UTF-8?q?=EB=84=88=EB=A5=BC=20=EC=96=B8=EC=96=B4=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=ED=95=98=EA=B3=A0=20=EB=85=B8=EC=B6=9C?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...live_recommend_creator_banner_lang_ddl.sql | 41 ++++++ docs/20260402_라이브추천크리에이터언어적용.md | 11 ++ .../admin/live/AdminLiveController.kt | 5 +- .../sodalive/admin/live/AdminLiveService.kt | 13 +- .../sodalive/api/live/LiveApiService.kt | 7 +- .../recommend/LiveRecommendCacheService.kt | 8 +- .../live/recommend/LiveRecommendController.kt | 8 +- .../live/recommend/LiveRecommendRepository.kt | 5 +- .../live/recommend/LiveRecommendService.kt | 6 +- .../recommend/RecommendLiveCreatorBanner.kt | 6 + .../admin/live/AdminLiveServiceTest.kt | 126 ++++++++++++++++++ .../recommend/LiveRecommendRepositoryTest.kt | 31 ++++- .../recommend/LiveRecommendServiceTest.kt | 13 +- 13 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 docs/20260402_live_recommend_creator_banner_lang_ddl.sql create mode 100644 docs/20260402_라이브추천크리에이터언어적용.md create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveServiceTest.kt diff --git a/docs/20260402_live_recommend_creator_banner_lang_ddl.sql b/docs/20260402_live_recommend_creator_banner_lang_ddl.sql new file mode 100644 index 00000000..32f72030 --- /dev/null +++ b/docs/20260402_live_recommend_creator_banner_lang_ddl.sql @@ -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; diff --git a/docs/20260402_라이브추천크리에이터언어적용.md b/docs/20260402_라이브추천크리에이터언어적용.md new file mode 100644 index 00000000..02b9b749 --- /dev/null +++ b/docs/20260402_라이브추천크리에이터언어적용.md @@ -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`이다. diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt index b4e659e9..98b581db 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt @@ -39,9 +39,10 @@ class AdminLiveController(private val service: AdminLiveService) { @RequestParam("creator_id") creatorId: Long, @RequestParam("start_date") startDate: String, @RequestParam("end_date") endDate: String, - @RequestParam("is_adult") isAdult: Boolean + @RequestParam("is_adult") isAdult: Boolean, + @RequestParam("lang") lang: String ) = ApiResponse.ok( - service.createRecommendCreatorBanner(image, creatorId, startDate, endDate, isAdult), + service.createRecommendCreatorBanner(image, creatorId, startDate, endDate, isAdult, lang), "등록되었습니다." ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt index b9cdeb15..39cdf2a0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt @@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType 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.SodaMessageSource import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner @@ -122,7 +123,8 @@ class AdminLiveService( creatorId: Long, startDateString: String, endDateString: String, - isAdult: Boolean + isAdult: Boolean, + lang: String ): Long { 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 <= 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( startDate = startDate, endDate = endDate, - isAdult = isAdult + isAdult = isAdult, + lang = bannerLang ) recommendCreatorBanner.creator = creator recommendCreatorBannerRepository.save(recommendCreatorBanner) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt index a2dcca68..7c1e64a7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/live/LiveApiService.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.api.live import kr.co.vividnext.sodalive.content.AudioContentService import kr.co.vividnext.sodalive.content.ContentType 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.room.LiveRoomService import kr.co.vividnext.sodalive.live.room.LiveRoomStatus @@ -20,8 +21,8 @@ class LiveApiService( private val recommendService: LiveRecommendService, private val creatorCommunityService: CreatorCommunityService, private val memberContentPreferenceService: MemberContentPreferenceService, - - private val blockMemberRepository: BlockMemberRepository + private val blockMemberRepository: BlockMemberRepository, + private val langContext: LangContext ) { fun fetchData( timezone: String, @@ -49,7 +50,7 @@ class LiveApiService( listOf() } - val recommendLiveList = recommendService.getRecommendLive(member) + val recommendLiveList = recommendService.getRecommendLive(member, langContext.lang) val latestFinishedLiveList = liveService.getLatestFinishedLive(member) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendCacheService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendCacheService.kt index 811302fd..0e5dd25a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendCacheService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendCacheService.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.live.recommend +import kr.co.vividnext.sodalive.i18n.Lang import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Service @@ -9,12 +10,13 @@ class LiveRecommendCacheService( ) { @Cacheable( cacheNames = ["cache_ttl_3_hours"], - key = "'getRecommendLive:' + (#memberId ?: 'guest') + ':' + #isAdult" + key = "'getRecommendLive:' + (#memberId ?: 'guest') + ':' + #isAdult + ':' + #lang.name()" ) - fun getRecommendLive(memberId: Long?, isAdult: Boolean): List { + fun getRecommendLive(memberId: Long?, isAdult: Boolean, lang: Lang): List { return repository.getRecommendLive( memberId = memberId, - isAdult = isAdult + isAdult = isAdult, + lang = lang ) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt index d1b7280a..bd29b813 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live.recommend import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -11,12 +12,15 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/live/recommend") -class LiveRecommendController(private val service: LiveRecommendService) { +class LiveRecommendController( + private val service: LiveRecommendService, + private val langContext: LangContext +) { @GetMapping fun getRecommendLive( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - ApiResponse.ok(service.getRecommendLive(member)) + ApiResponse.ok(service.getRecommendLive(member, langContext.lang)) } @GetMapping("/channel") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt index 50901f77..cece4a56 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.live.recommend import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.Expressions 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.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.MemberRole @@ -22,12 +23,14 @@ class LiveRecommendRepository( ) { fun getRecommendLive( memberId: Long?, - isAdult: Boolean + isAdult: Boolean, + lang: Lang ): List { val dateNow = LocalDateTime.now() var where = recommendLiveCreatorBanner.startDate.loe(dateNow) .and(recommendLiveCreatorBanner.endDate.goe(dateNow)) + .and(recommendLiveCreatorBanner.lang.eq(lang)) if (!isAdult) { where = where.and(recommendLiveCreatorBanner.isAdult.isFalse) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt index 108d1249..0a2a36fe 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt @@ -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.MemberRole import kr.co.vividnext.sodalive.member.block.BlockMemberRepository @@ -14,7 +15,7 @@ class LiveRecommendService( private val memberContentPreferenceService: MemberContentPreferenceService, private val liveRecommendCacheService: LiveRecommendCacheService ) { - fun getRecommendLive(member: Member?): List { + fun getRecommendLive(member: Member?, lang: Lang): List { val isAdult = if (member != null) { memberContentPreferenceService.getStoredPreference(member).isAdult } else { @@ -23,7 +24,8 @@ class LiveRecommendService( return liveRecommendCacheService.getRecommendLive( memberId = member?.id, - isAdult = isAdult + isAdult = isAdult, + lang = lang ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt index b56d0309..64bf3f16 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt @@ -1,10 +1,13 @@ package kr.co.vividnext.sodalive.live.recommend import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.member.Member import java.time.LocalDateTime import javax.persistence.Column import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated import javax.persistence.FetchType import javax.persistence.JoinColumn import javax.persistence.ManyToOne @@ -18,6 +21,9 @@ data class RecommendLiveCreatorBanner( @Column(nullable = false) var isAdult: Boolean = false, @Column(nullable = false) + @Enumerated(EnumType.STRING) + var lang: Lang = Lang.KO, + @Column(nullable = false) var orders: Int = 1, @Column(nullable = true) var image: String? = null diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveServiceTest.kt new file mode 100644 index 00000000..b8ed2976 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveServiceTest.kt @@ -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) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepositoryTest.kt index f0a75628..a1de892a 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepositoryTest.kt @@ -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" ) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendServiceTest.kt index 2190e34a..db60bf77 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendServiceTest.kt @@ -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)