From f27074167aee107aed0c531d84b346e9d66e8ad1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 23 Jun 2026 11:57:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(home-recommendation):=20AI=20=EC=BA=90?= =?UTF-8?q?=EB=A6=AD=ED=84=B0=20creatorId=20=EC=9D=91=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HomeRecommendationFacade.kt | 1 + .../HomeRecommendationResponse.kt | 1 + .../home/HomeRecommendationControllerTest.kt | 60 +++++++++++++++++++ .../HomeRecommendationResponseTest.kt | 3 + 4 files changed, 65 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt index a21f376a..4283b8e1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt @@ -286,6 +286,7 @@ class HomeRecommendationFacade( private fun HomeAiCharacterRecommendationRecord.toItem() = HomeAiCharacterItem( characterId = characterId, + creatorId = creatorId, name = name, description = description, profileImage = imageUrl(cloudFrontHost, profileImage), diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponse.kt index 927ee410..a5248f37 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponse.kt @@ -71,6 +71,7 @@ data class HomeFirstAudioContentItem( data class HomeAiCharacterItem( val characterId: Long, + val creatorId: Long, val name: String, val description: String, val profileImage: String?, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt index fd17d1b9..21a57a38 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt @@ -1,8 +1,10 @@ package kr.co.vividnext.sodalive.v2.api.home +import kr.co.vividnext.sodalive.chat.character.ChatCharacter import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberAdapter +import kr.co.vividnext.sodalive.member.MemberKind import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreference @@ -11,7 +13,9 @@ import kr.co.vividnext.sodalive.member.following.CreatorFollowing import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer import kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacade +import kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshot import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService +import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertThrows @@ -481,6 +485,29 @@ class HomeRecommendationControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.size").value(20)) } + @Test + @DisplayName("AI 캐릭터 추천은 홈 통합과 전체보기 응답에 characterId와 creatorId를 함께 노출한다") + fun shouldExposeCreatorIdOnAiCharacterRecommendations() { + val member = saveMember("ai-character-page-viewer", MemberRole.USER) + val character = saveAiCharacter("ai-character-api") + saveRecommendationSnapshot(character.id!!) + entityManager.flush() + entityManager.clear() + + mockMvc.perform(get("/api/v2/home/recommendations")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.aiCharacters[0].characterId").value(character.id)) + .andExpect(jsonPath("$.data.aiCharacters[0].creatorId").value(character.creatorMember!!.id)) + + mockMvc.perform( + get("/api/v2/home/recommendations/ai-characters") + .with(user(MemberAdapter(member))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.items[0].characterId").value(character.id)) + .andExpect(jsonPath("$.data.items[0].creatorId").value(character.creatorMember!!.id)) + } + private fun saveMember(seed: String, role: MemberRole): Member { return memberRepository.saveAndFlush( Member( @@ -519,4 +546,37 @@ class HomeRecommendationControllerTest @Autowired constructor( entityManager.persist(room) return room } + + private fun saveAiCharacter(name: String): ChatCharacter { + val creatorMember = Member( + email = null, + password = "", + nickname = name, + role = MemberRole.CREATOR, + memberKind = MemberKind.AI_CHARACTER + ) + entityManager.persist(creatorMember) + val character = ChatCharacter( + characterUUID = "$name-uuid", + name = name, + description = "description", + systemPrompt = "system", + isActive = true + ) + character.creatorMember = creatorMember + entityManager.persist(character) + return character + } + + private fun saveRecommendationSnapshot(characterId: Long) { + entityManager.persist( + RecommendationSnapshot( + sectionType = RecommendedSectionType.AI_CHARACTER, + targetId = characterId, + score = 100.0, + snapshotAt = LocalDateTime.of(2026, 6, 1, 23, 59, 59), + randomTieBreaker = 0.1 + ) + ) + } } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponseTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponseTest.kt index 71b8f031..d1a15022 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponseTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponseTest.kt @@ -30,6 +30,7 @@ class HomeRecommendationResponseTest { aiCharacters = listOf( HomeAiCharacterItem( characterId = 3L, + creatorId = 13L, name = "character", description = "description", profileImage = "https://cdn.test/profile/character.png", @@ -38,6 +39,7 @@ class HomeRecommendationResponseTest { ), HomeAiCharacterItem( characterId = 4L, + creatorId = 14L, name = "character-without-image", description = "description", profileImage = null, @@ -86,6 +88,7 @@ class HomeRecommendationResponseTest { assertFalse(json["firstAudioContents"][0].has("pointAvailable")) assertFalse(json["firstAudioContents"][0].has("releaseDate")) assertEquals("https://cdn.test/profile/character.png", json["aiCharacters"][0]["profileImage"].asText()) + assertEquals(13L, json["aiCharacters"][0]["creatorId"].asLong()) assertEquals(true, json["aiCharacters"][1]["profileImage"].isNull) assertEquals(5L, json["popularCommunityPosts"][0]["postId"].asLong()) assertEquals("https://cdn.test/community/image.png", json["popularCommunityPosts"][0]["imageUrl"].asText())