feat(live-recommend): 추천 크리에이터 배너를 언어별로 등록하고 노출한다
This commit is contained in:
41
docs/20260402_live_recommend_creator_banner_lang_ddl.sql
Normal file
41
docs/20260402_live_recommend_creator_banner_lang_ddl.sql
Normal 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;
|
||||||
11
docs/20260402_라이브추천크리에이터언어적용.md
Normal file
11
docs/20260402_라이브추천크리에이터언어적용.md
Normal 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`이다.
|
||||||
@@ -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),
|
||||||
"등록되었습니다."
|
"등록되었습니다."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user