fix(home-recommendation): 통합 응답의 AI 캐릭터 목록을 비운다
This commit is contained in:
+1
-5
@@ -97,10 +97,7 @@ class HomeRecommendationFacade(
|
|||||||
lang = lang
|
lang = lang
|
||||||
)
|
)
|
||||||
.map { it.toItem() },
|
.map { it.toItem() },
|
||||||
aiCharacters = queryService.findAiCharacterRecommendations(
|
aiCharacters = emptyList(),
|
||||||
limit = HOME_AI_CHARACTER_LIMIT,
|
|
||||||
backfillRandom = true
|
|
||||||
).map { it.toItem() },
|
|
||||||
genreCreators = queryService.findGenreCreatorRecommendations(
|
genreCreators = queryService.findGenreCreatorRecommendations(
|
||||||
memberId = member?.id,
|
memberId = member?.id,
|
||||||
includeAdultGenres = includeAdult,
|
includeAdultGenres = includeAdult,
|
||||||
@@ -357,7 +354,6 @@ class HomeRecommendationFacade(
|
|||||||
private const val HOME_ACTIVE_CREATOR_LIMIT = 10
|
private const val HOME_ACTIVE_CREATOR_LIMIT = 10
|
||||||
private const val HOME_RECENT_DEBUT_CREATOR_LIMIT = 10
|
private const val HOME_RECENT_DEBUT_CREATOR_LIMIT = 10
|
||||||
private const val HOME_FIRST_AUDIO_CONTENT_LIMIT = 10
|
private const val HOME_FIRST_AUDIO_CONTENT_LIMIT = 10
|
||||||
private const val HOME_AI_CHARACTER_LIMIT = 20
|
|
||||||
private const val HOME_GENRE_CREATOR_GENRE_LIMIT = 5
|
private const val HOME_GENRE_CREATOR_GENRE_LIMIT = 5
|
||||||
private const val HOME_GENRE_CREATOR_CREATOR_LIMIT = 8
|
private const val HOME_GENRE_CREATOR_CREATOR_LIMIT = 8
|
||||||
private const val HOME_CHEER_CREATOR_LIMIT = 8
|
private const val HOME_CHEER_CREATOR_LIMIT = 8
|
||||||
|
|||||||
+35
-8
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.member.MemberAdapter
|
|||||||
import kr.co.vividnext.sodalive.member.MemberKind
|
import kr.co.vividnext.sodalive.member.MemberKind
|
||||||
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
|
||||||
|
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
||||||
@@ -29,6 +30,8 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
@@ -350,9 +353,29 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
.andExpect(jsonPath("$.data.lives").isArray)
|
.andExpect(jsonPath("$.data.lives").isArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
@DisplayName("메인 홈 통합 조회는 AI 캐릭터를 최신 스냅샷 기준 최대 20개까지 반환한다")
|
@CsvSource(
|
||||||
fun shouldReturnTwentyAiCharactersOnHomeRecommendations() {
|
"ANONYMOUS, ko",
|
||||||
|
"ANONYMOUS, ja",
|
||||||
|
"UNVERIFIED, ko",
|
||||||
|
"UNVERIFIED, ja",
|
||||||
|
"VERIFIED, ko",
|
||||||
|
"VERIFIED, ja",
|
||||||
|
"ADMIN, ko",
|
||||||
|
"ADMIN, ja"
|
||||||
|
)
|
||||||
|
@DisplayName("메인 홈 통합 조회는 AI 스냅샷이 있어도 모든 조회자와 KO·JA 요청에 빈 AI 배열을 반환한다")
|
||||||
|
fun shouldReturnEmptyAiCharactersOnHomeRecommendations(viewerType: String, language: String) {
|
||||||
|
val member = when (viewerType) {
|
||||||
|
"ANONYMOUS" -> null
|
||||||
|
"ADMIN" -> saveMember("home-ai-admin", MemberRole.ADMIN)
|
||||||
|
else -> saveMember("home-ai-viewer", MemberRole.USER)
|
||||||
|
}
|
||||||
|
if (viewerType == "VERIFIED") {
|
||||||
|
entityManager.persist(
|
||||||
|
Auth("verified", "19900101", "home-ai-ci", "home-ai-di", 1).apply { this.member = member }
|
||||||
|
)
|
||||||
|
}
|
||||||
(1..22).forEach { index ->
|
(1..22).forEach { index ->
|
||||||
val character = saveAiCharacter("home-ai-character-$index")
|
val character = saveAiCharacter("home-ai-character-$index")
|
||||||
saveRecommendationSnapshot(character.id!!)
|
saveRecommendationSnapshot(character.id!!)
|
||||||
@@ -360,10 +383,14 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
entityManager.clear()
|
entityManager.clear()
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v2/home/recommendations"))
|
val request = get("/api/v2/home/recommendations").header("Accept-Language", language)
|
||||||
|
member?.let { request.with(user(MemberAdapter(it))) }
|
||||||
|
|
||||||
|
mockMvc.perform(request)
|
||||||
.andExpect(status().isOk)
|
.andExpect(status().isOk)
|
||||||
.andExpect(jsonPath("$.success").value(true))
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
.andExpect(jsonPath("$.data.aiCharacters.length()").value(20))
|
.andExpect(jsonPath("$.data.aiCharacters").isArray)
|
||||||
|
.andExpect(jsonPath("$.data.aiCharacters").isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -630,7 +657,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("AI 캐릭터 추천은 홈 통합과 전체보기 응답에 characterId와 creatorId를 함께 노출한다")
|
@DisplayName("홈 통합 AI 목록은 비어 있고 전용 전체보기는 characterId와 creatorId를 함께 노출한다")
|
||||||
fun shouldExposeCreatorIdOnAiCharacterRecommendations() {
|
fun shouldExposeCreatorIdOnAiCharacterRecommendations() {
|
||||||
val member = saveMember("ai-character-page-viewer", MemberRole.USER)
|
val member = saveMember("ai-character-page-viewer", MemberRole.USER)
|
||||||
val character = saveAiCharacter("ai-character-api")
|
val character = saveAiCharacter("ai-character-api")
|
||||||
@@ -640,8 +667,8 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
|
|
||||||
mockMvc.perform(get("/api/v2/home/recommendations"))
|
mockMvc.perform(get("/api/v2/home/recommendations"))
|
||||||
.andExpect(status().isOk)
|
.andExpect(status().isOk)
|
||||||
.andExpect(jsonPath("$.data.aiCharacters[0].characterId").value(character.id))
|
.andExpect(jsonPath("$.data.aiCharacters").isArray)
|
||||||
.andExpect(jsonPath("$.data.aiCharacters[0].creatorId").value(character.creatorMember!!.id))
|
.andExpect(jsonPath("$.data.aiCharacters").isEmpty)
|
||||||
|
|
||||||
mockMvc.perform(
|
mockMvc.perform(
|
||||||
get("/api/v2/home/recommendations/ai-characters")
|
get("/api/v2/home/recommendations/ai-characters")
|
||||||
|
|||||||
+70
-8
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
|
|||||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
|
||||||
|
import kr.co.vividnext.sodalive.v2.recommendation.application.AiCharacterSnapshotFallbackPort
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord
|
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord
|
||||||
@@ -22,6 +23,8 @@ import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecentlyActiveCreator
|
|||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||||
import kr.co.vividnext.sodalive.v2.subbanner.application.SubBannerService
|
import kr.co.vividnext.sodalive.v2.subbanner.application.SubBannerService
|
||||||
|
import org.junit.jupiter.api.Assertions.assertAll
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertFalse
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
@@ -31,10 +34,13 @@ import java.time.LocalDateTime
|
|||||||
|
|
||||||
class HomeRecommendationFacadeTest {
|
class HomeRecommendationFacadeTest {
|
||||||
private val queryPort = FakeHomeRecommendationQueryPort()
|
private val queryPort = FakeHomeRecommendationQueryPort()
|
||||||
|
private val snapshotPort = EmptyRecommendationSnapshotPort()
|
||||||
|
private val aiFallbackPort = FakeAiCharacterSnapshotFallbackPort()
|
||||||
private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
|
private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
|
||||||
private val queryService = HomeRecommendationQueryService(
|
private val queryService = HomeRecommendationQueryService(
|
||||||
queryPort,
|
queryPort,
|
||||||
EmptyRecommendationSnapshotPort(),
|
snapshotPort,
|
||||||
|
aiCharacterSnapshotFallbackService = aiFallbackPort,
|
||||||
creatorCommunityTranslationService = creatorCommunityTranslationService
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||||
@@ -45,6 +51,22 @@ class HomeRecommendationFacadeTest {
|
|||||||
"https://cdn.test"
|
"https://cdn.test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("홈 추천은 AI 스냅샷과 상세 및 무작위 보충이나 fallback 조회 없이 빈 AI 목록을 반환한다")
|
||||||
|
fun shouldReturnEmptyAiCharactersWithoutQueryingAiRecommendations() {
|
||||||
|
val response = facade.getHomeRecommendations(null, Lang.KO)
|
||||||
|
|
||||||
|
assertAll(
|
||||||
|
{ assertTrue(response.aiCharacters.isEmpty()) },
|
||||||
|
{ assertEquals(0, snapshotPort.aiSnapshotCalls) },
|
||||||
|
{ assertEquals(0, snapshotPort.aiSnapshotExistenceCalls) },
|
||||||
|
{ assertEquals(0, queryPort.aiSnapshotCalls) },
|
||||||
|
{ assertEquals(0, queryPort.aiDetailCalls) },
|
||||||
|
{ assertEquals(0, queryPort.aiRandomCalls) },
|
||||||
|
{ assertEquals(0, aiFallbackPort.calls) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("홈 첫 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
|
@DisplayName("홈 첫 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
|
||||||
fun shouldNormalizePointAvailabilityForFreeFirstAudioContent() {
|
fun shouldNormalizePointAvailabilityForFreeFirstAudioContent() {
|
||||||
@@ -150,6 +172,9 @@ class HomeRecommendationFacadeTest {
|
|||||||
var bannerLang: Lang? = null
|
var bannerLang: Lang? = null
|
||||||
var liveIsViewerCreator: Boolean? = null
|
var liveIsViewerCreator: Boolean? = null
|
||||||
var liveEffectiveViewerGender: Gender? = null
|
var liveEffectiveViewerGender: Gender? = null
|
||||||
|
var aiSnapshotCalls: Int = 0
|
||||||
|
var aiDetailCalls: Int = 0
|
||||||
|
var aiRandomCalls: Int = 0
|
||||||
|
|
||||||
override fun findLiveRecommendations(
|
override fun findLiveRecommendations(
|
||||||
offset: Long,
|
offset: Long,
|
||||||
@@ -205,7 +230,10 @@ class HomeRecommendationFacadeTest {
|
|||||||
windowStart: LocalDateTime,
|
windowStart: LocalDateTime,
|
||||||
windowEndExclusive: LocalDateTime,
|
windowEndExclusive: LocalDateTime,
|
||||||
limit: Int
|
limit: Int
|
||||||
): List<RecommendationSnapshotRecord> = emptyList()
|
): List<RecommendationSnapshotRecord> {
|
||||||
|
aiSnapshotCalls++
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
override fun findCheerCreatorSnapshots(
|
override fun findCheerCreatorSnapshots(
|
||||||
windowStart: LocalDateTime,
|
windowStart: LocalDateTime,
|
||||||
@@ -221,9 +249,15 @@ class HomeRecommendationFacadeTest {
|
|||||||
|
|
||||||
override fun findAiCharacterRecommendationDetails(
|
override fun findAiCharacterRecommendationDetails(
|
||||||
characterIds: List<Long>
|
characterIds: List<Long>
|
||||||
): List<HomeAiCharacterRecommendationRecord> = emptyList()
|
): List<HomeAiCharacterRecommendationRecord> {
|
||||||
|
aiDetailCalls++
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
override fun findRandomAiCharacterRecommendationIds(excludeCharacterIds: List<Long>, limit: Int): List<Long> = emptyList()
|
override fun findRandomAiCharacterRecommendationIds(excludeCharacterIds: List<Long>, limit: Int): List<Long> {
|
||||||
|
aiRandomCalls++
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
override fun findCheerCreatorRecommendationDetails(
|
override fun findCheerCreatorRecommendationDetails(
|
||||||
creatorIds: List<Long>,
|
creatorIds: List<Long>,
|
||||||
@@ -248,22 +282,37 @@ class HomeRecommendationFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class EmptyRecommendationSnapshotPort : RecommendationSnapshotPort {
|
private class EmptyRecommendationSnapshotPort : RecommendationSnapshotPort {
|
||||||
|
var aiSnapshotCalls: Int = 0
|
||||||
|
var aiSnapshotExistenceCalls: Int = 0
|
||||||
|
|
||||||
override fun findLatestSnapshots(
|
override fun findLatestSnapshots(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int
|
limit: Int
|
||||||
): List<RecommendationSnapshotRecord> = emptyList()
|
): List<RecommendationSnapshotRecord> {
|
||||||
|
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotCalls++
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
override fun findSnapshots(
|
override fun findSnapshots(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
snapshotAt: LocalDateTime,
|
snapshotAt: LocalDateTime,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int
|
limit: Int
|
||||||
): List<RecommendationSnapshotRecord> = emptyList()
|
): List<RecommendationSnapshotRecord> {
|
||||||
|
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotCalls++
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean = true
|
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean {
|
||||||
|
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotExistenceCalls++
|
||||||
|
return sectionType != RecommendedSectionType.AI_CHARACTER
|
||||||
|
}
|
||||||
|
|
||||||
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean = true
|
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean {
|
||||||
|
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotExistenceCalls++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun replaceSnapshots(
|
override fun replaceSnapshots(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
@@ -271,4 +320,17 @@ class HomeRecommendationFacadeTest {
|
|||||||
newSnapshots: List<RecommendationSnapshotRecord>
|
newSnapshots: List<RecommendationSnapshotRecord>
|
||||||
) = Unit
|
) = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class FakeAiCharacterSnapshotFallbackPort : AiCharacterSnapshotFallbackPort {
|
||||||
|
var calls: Int = 0
|
||||||
|
|
||||||
|
override fun refreshIfMissing(
|
||||||
|
offset: Long,
|
||||||
|
limit: Int,
|
||||||
|
nowUtc: LocalDateTime
|
||||||
|
): List<RecommendationSnapshotRecord> {
|
||||||
|
calls++
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user