diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt index ac810c11..b73990c8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt @@ -21,6 +21,8 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommun import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.MemberKind +import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.block.QBlockMember @@ -688,12 +690,14 @@ class DefaultHomeRecommendationQueryRepository( ): List { if (characterIds.isEmpty()) return emptyList() val linkedOriginalWork = QOriginalWork("linkedOriginalWork") + val creatorMember = QMember("creatorMember") return queryFactory .select( Projections.constructor( HomeAiCharacterRecommendationRecord::class.java, chatCharacter.id, + creatorMember.id, chatCharacter.name, chatCharacter.description, chatCharacter.imagePath, @@ -702,6 +706,7 @@ class DefaultHomeRecommendationQueryRepository( ) ) .from(chatCharacter) + .join(chatCharacter.creatorMember, creatorMember) .leftJoin(chatCharacter.originalWork, linkedOriginalWork).on(linkedOriginalWork.isDeleted.isFalse) .leftJoin(chatParticipant).on( chatParticipant.character.id.eq(chatCharacter.id), @@ -712,9 +717,16 @@ class DefaultHomeRecommendationQueryRepository( chatMessage.participant.id.eq(chatParticipant.id), chatMessage.isActive.isTrue ) - .where(chatCharacter.isActive.isTrue, chatCharacter.id.`in`(characterIds)) + .where( + chatCharacter.isActive.isTrue, + chatCharacter.id.`in`(characterIds), + creatorMember.isActive.isTrue, + creatorMember.role.eq(MemberRole.CREATOR), + creatorMember.memberKind.eq(MemberKind.AI_CHARACTER) + ) .groupBy( chatCharacter.id, + creatorMember.id, chatCharacter.name, chatCharacter.description, chatCharacter.imagePath, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt index 990b96f1..ba010547 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt @@ -121,6 +121,7 @@ data class HomeFirstAudioContentRecord( data class HomeAiCharacterRecommendationRecord( 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/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt index 03b1098d..df6ea8a5 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt @@ -1169,6 +1169,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( .associateBy { it.characterId } assertEquals(setOf(characterWithWork.id, characterWithoutWork.id), details.keys) + assertEquals(characterWithWork.creatorMember!!.id, details[characterWithWork.id]!!.creatorId) + assertEquals(characterWithoutWork.creatorMember!!.id, details[characterWithoutWork.id]!!.creatorId) assertEquals("ai-detail-work", details[characterWithWork.id]!!.name) assertEquals("description", details[characterWithWork.id]!!.description) assertEquals(2L, details[characterWithWork.id]!!.totalChatCount) @@ -1177,6 +1179,37 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( assertEquals(null, details[characterWithoutWork.id]!!.originalWorkTitle) } + @Test + @DisplayName("AI 캐릭터 상세는 활성 AI 캐릭터 크리에이터 회원인 경우만 조회한다") + fun shouldFindAiCharacterRecommendationDetailsForActiveAiCreatorMembersOnly() { + val activeCharacter = saveCharacter("ai-detail-active-creator-member", isActive = true) + val missingCreatorCharacter = saveCharacter("ai-detail-missing-creator-member", isActive = true) + val inactiveCreatorCharacter = saveCharacter("ai-detail-inactive-creator-member", isActive = true).apply { + creatorMember!!.isActive = false + } + val userCreatorCharacter = saveCharacter("ai-detail-user-creator-member", isActive = true).apply { + creatorMember!!.role = MemberRole.USER + } + val humanCreatorCharacter = saveCharacter("ai-detail-human-creator-member", isActive = true).apply { + creatorMember!!.memberKind = MemberKind.HUMAN + } + detachCreatorMember(missingCreatorCharacter) + flushAndClear() + + val details = repository.findAiCharacterRecommendationDetails( + listOf( + activeCharacter.id!!, + missingCreatorCharacter.id!!, + inactiveCreatorCharacter.id!!, + userCreatorCharacter.id!!, + humanCreatorCharacter.id!! + ) + ) + + assertEquals(listOf(activeCharacter.id), details.map { it.characterId }) + assertEquals(activeCharacter.creatorMember!!.id, details.single().creatorId) + } + @Test @DisplayName("AI 캐릭터 상세는 빈 id 목록이면 빈 배열을 반환한다") fun shouldReturnEmptyAiCharacterRecommendationDetailsWhenIdsAreEmpty() { @@ -2107,4 +2140,13 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( entityManager.flush() entityManager.clear() } + + private fun detachCreatorMember(character: ChatCharacter) { + entityManager.flush() + entityManager.createNativeQuery("alter table chat_character alter column creator_member_id drop not null") + .executeUpdate() + entityManager.createNativeQuery("update chat_character set creator_member_id = null where id = :id") + .setParameter("id", character.id) + .executeUpdate() + } } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt index e72eebed..e3609e43 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt @@ -143,6 +143,7 @@ class HomeRecommendationQueryServiceTest { port.aiCharacterDetails = listOf( HomeAiCharacterRecommendationRecord( characterId = 1L, + creatorId = 101L, name = "character-1", description = "description-1", profileImage = "profile/character-1.png", @@ -151,6 +152,7 @@ class HomeRecommendationQueryServiceTest { ), HomeAiCharacterRecommendationRecord( characterId = 2L, + creatorId = 102L, name = "character-2", description = "description-2", profileImage = null, @@ -163,6 +165,7 @@ class HomeRecommendationQueryServiceTest { assertEquals((1L..10L).toList(), port.aiCharacterDetailIds) assertEquals(listOf(1L, 2L), characters.map { it.characterId }) + assertEquals(listOf(101L, 102L), characters.map { it.creatorId }) assertEquals("profile/character-1.png", characters.first().profileImage) assertEquals(null, characters.last().profileImage) assertEquals("original-work", characters.first().originalWorkTitle)