캐릭터 챗봇 #338
| @@ -61,16 +61,16 @@ class ChatCharacter( | ||||
| ) : BaseEntity() { | ||||
|     var imagePath: String? = null | ||||
|  | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) | ||||
|     var memories: MutableList<ChatCharacterMemory> = mutableListOf() | ||||
|  | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) | ||||
|     var personalities: MutableList<ChatCharacterPersonality> = mutableListOf() | ||||
|  | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) | ||||
|     var backgrounds: MutableList<ChatCharacterBackground> = mutableListOf() | ||||
|  | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) | ||||
|     var relationships: MutableList<ChatCharacterRelationship> = mutableListOf() | ||||
|  | ||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class ChatCharacterBackground( | ||||
|  | ||||
|     // 배경 설명 | ||||
|     @Column(columnDefinition = "TEXT", nullable = false) | ||||
|     val description: String, | ||||
|     var description: String, | ||||
|  | ||||
|     @ManyToOne(fetch = FetchType.LAZY) | ||||
|     @JoinColumn(name = "chat_character_id") | ||||
|   | ||||
| @@ -18,10 +18,10 @@ class ChatCharacterMemory( | ||||
|  | ||||
|     // 기억 내용 | ||||
|     @Column(columnDefinition = "TEXT", nullable = false) | ||||
|     val content: String, | ||||
|     var content: String, | ||||
|  | ||||
|     // 감정 | ||||
|     val emotion: String, | ||||
|     var emotion: String, | ||||
|  | ||||
|     @ManyToOne(fetch = FetchType.LAZY) | ||||
|     @JoinColumn(name = "chat_character_id") | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class ChatCharacterPersonality( | ||||
|  | ||||
|     // 성격 특성 설명 | ||||
|     @Column(columnDefinition = "TEXT", nullable = false) | ||||
|     val description: String, | ||||
|     var description: String, | ||||
|  | ||||
|     @ManyToOne(fetch = FetchType.LAZY) | ||||
|     @JoinColumn(name = "chat_character_id") | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| package kr.co.vividnext.sodalive.chat.character.service | ||||
|  | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterBackgroundRequest | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterMemoryRequest | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterPersonalityRequest | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterRelationshipRequest | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest | ||||
| import kr.co.vividnext.sodalive.chat.character.CharacterType | ||||
| @@ -222,6 +225,147 @@ class ChatCharacterService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 기억(memories) 증분 업데이트 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun updateMemoriesForCharacter(chatCharacter: ChatCharacter, memories: List<ChatCharacterMemoryRequest>) { | ||||
|         val desiredByTitle = memories | ||||
|             .asSequence() | ||||
|             .distinctBy { it.title } | ||||
|             .associateBy { it.title } | ||||
|  | ||||
|         val iterator = chatCharacter.memories.iterator() | ||||
|         while (iterator.hasNext()) { | ||||
|             val current = iterator.next() | ||||
|             val desired = desiredByTitle[current.title] | ||||
|             if (desired == null) { | ||||
|                 // 요청에 없는 항목은 제거 | ||||
|                 iterator.remove() | ||||
|             } else { | ||||
|                 // 값 필드만 in-place 업데이트 | ||||
|                 if (current.content != desired.content) current.content = desired.content | ||||
|                 if (current.emotion != desired.emotion) current.emotion = desired.emotion | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 신규 추가 | ||||
|         val existingTitles = chatCharacter.memories.map { it.title }.toSet() | ||||
|         desiredByTitle.values | ||||
|             .filterNot { existingTitles.contains(it.title) } | ||||
|             .forEach { chatCharacter.addMemory(it.title, it.content, it.emotion) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 성격(personalities) 증분 업데이트 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun updatePersonalitiesForCharacter( | ||||
|         chatCharacter: ChatCharacter, | ||||
|         personalities: List<ChatCharacterPersonalityRequest> | ||||
|     ) { | ||||
|         val desiredByTrait = personalities | ||||
|             .asSequence() | ||||
|             .distinctBy { it.trait } | ||||
|             .associateBy { it.trait } | ||||
|  | ||||
|         val iterator = chatCharacter.personalities.iterator() | ||||
|         while (iterator.hasNext()) { | ||||
|             val current = iterator.next() | ||||
|             val desired = desiredByTrait[current.trait] | ||||
|             if (desired == null) { | ||||
|                 // 요청에 없는 항목은 제거 | ||||
|                 iterator.remove() | ||||
|             } else { | ||||
|                 // 값 필드만 in-place 업데이트 | ||||
|                 if (current.description != desired.description) current.description = desired.description | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 신규 추가 | ||||
|         val existingTraits = chatCharacter.personalities.map { it.trait }.toSet() | ||||
|         desiredByTrait.values | ||||
|             .filterNot { existingTraits.contains(it.trait) } | ||||
|             .forEach { chatCharacter.addPersonality(it.trait, it.description) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 배경(backgrounds) 증분 업데이트 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun updateBackgroundsForCharacter(chatCharacter: ChatCharacter, backgrounds: List<ChatCharacterBackgroundRequest>) { | ||||
|         val desiredByTopic = backgrounds | ||||
|             .asSequence() | ||||
|             .distinctBy { it.topic } | ||||
|             .associateBy { it.topic } | ||||
|  | ||||
|         val iterator = chatCharacter.backgrounds.iterator() | ||||
|         while (iterator.hasNext()) { | ||||
|             val current = iterator.next() | ||||
|             val desired = desiredByTopic[current.topic] | ||||
|             if (desired == null) { | ||||
|                 // 요청에 없는 항목은 제거 | ||||
|                 iterator.remove() | ||||
|             } else { | ||||
|                 // 값 필드만 in-place 업데이트 | ||||
|                 if (current.description != desired.description) current.description = desired.description | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 신규 추가 | ||||
|         val existingTopics = chatCharacter.backgrounds.map { it.topic }.toSet() | ||||
|         desiredByTopic.values | ||||
|             .filterNot { existingTopics.contains(it.topic) } | ||||
|             .forEach { chatCharacter.addBackground(it.topic, it.description) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 관계(relationships) 증분 업데이트 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun updateRelationshipsForCharacter( | ||||
|         chatCharacter: ChatCharacter, | ||||
|         relationships: List<ChatCharacterRelationshipRequest> | ||||
|     ) { | ||||
|         fun keyOf(p: String, r: String) = "$" + "{" + p + "}" + "::" + "{" + r + "}" | ||||
|  | ||||
|         val desiredByKey = relationships | ||||
|             .asSequence() | ||||
|             .distinctBy { keyOf(it.personName, it.relationshipName) } | ||||
|             .associateBy { keyOf(it.personName, it.relationshipName) } | ||||
|  | ||||
|         val iterator = chatCharacter.relationships.iterator() | ||||
|         while (iterator.hasNext()) { | ||||
|             val current = iterator.next() | ||||
|             val key = keyOf(current.personName, current.relationshipName) | ||||
|             val desired = desiredByKey[key] | ||||
|             if (desired == null) { | ||||
|                 iterator.remove() | ||||
|             } else { | ||||
|                 if (current.description != desired.description) current.description = desired.description | ||||
|                 if (current.importance != desired.importance) current.importance = desired.importance | ||||
|                 if (current.relationshipType != desired.relationshipType) { | ||||
|                     current.relationshipType = desired.relationshipType | ||||
|                 } | ||||
|                 if (current.currentStatus != desired.currentStatus) current.currentStatus = desired.currentStatus | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         val existingKeys = chatCharacter.relationships.map { keyOf(it.personName, it.relationshipName) }.toSet() | ||||
|         desiredByKey.values | ||||
|             .filterNot { existingKeys.contains(keyOf(it.personName, it.relationshipName)) } | ||||
|             .forEach { rr -> | ||||
|                 chatCharacter.addRelationship( | ||||
|                     rr.personName, | ||||
|                     rr.relationshipName, | ||||
|                     rr.description, | ||||
|                     rr.importance, | ||||
|                     rr.relationshipType, | ||||
|                     rr.currentStatus | ||||
|                 ) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 캐릭터 저장 | ||||
|      */ | ||||
| @@ -230,14 +374,6 @@ class ChatCharacterService( | ||||
|         return chatCharacterRepository.save(chatCharacter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * UUID로 캐릭터 조회 | ||||
|      */ | ||||
|     @Transactional(readOnly = true) | ||||
|     fun findByCharacterUUID(characterUUID: String): ChatCharacter? { | ||||
|         return chatCharacterRepository.findByCharacterUUID(characterUUID) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 이름으로 캐릭터 조회 | ||||
|      */ | ||||
| @@ -246,14 +382,6 @@ class ChatCharacterService( | ||||
|         return chatCharacterRepository.findByName(name) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 모든 캐릭터 조회 | ||||
|      */ | ||||
|     @Transactional(readOnly = true) | ||||
|     fun findAll(): List<ChatCharacter> { | ||||
|         return chatCharacterRepository.findAll() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * ID로 캐릭터 조회 | ||||
|      */ | ||||
| @@ -331,57 +459,6 @@ class ChatCharacterService( | ||||
|         return saveChatCharacter(chatCharacter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 캐릭터에 기억 추가 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun addMemoryToChatCharacter(chatCharacter: ChatCharacter, title: String, content: String, emotion: String) { | ||||
|         chatCharacter.addMemory(title, content, emotion) | ||||
|         saveChatCharacter(chatCharacter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 캐릭터에 성격 특성 추가 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun addPersonalityToChatCharacter(chatCharacter: ChatCharacter, trait: String, description: String) { | ||||
|         chatCharacter.addPersonality(trait, description) | ||||
|         saveChatCharacter(chatCharacter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 캐릭터에 배경 정보 추가 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun addBackgroundToChatCharacter(chatCharacter: ChatCharacter, topic: String, description: String) { | ||||
|         chatCharacter.addBackground(topic, description) | ||||
|         saveChatCharacter(chatCharacter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 캐릭터에 관계 추가 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun addRelationshipToChatCharacter( | ||||
|         chatCharacter: ChatCharacter, | ||||
|         personName: String, | ||||
|         relationshipName: String, | ||||
|         description: String, | ||||
|         importance: Int, | ||||
|         relationshipType: String, | ||||
|         currentStatus: String | ||||
|     ) { | ||||
|         chatCharacter.addRelationship( | ||||
|             personName, | ||||
|             relationshipName, | ||||
|             description, | ||||
|             importance, | ||||
|             relationshipType, | ||||
|             currentStatus | ||||
|         ) | ||||
|         saveChatCharacter(chatCharacter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 캐릭터 생성 시 기본 정보와 함께 추가 정보도 설정 | ||||
|      */ | ||||
| @@ -464,7 +541,6 @@ class ChatCharacterService( | ||||
|      * @param imagePath 이미지 경로 (null이면 이미지 변경 없음) | ||||
|      * @param request 수정 요청 데이터 (id를 제외한 모든 필드는 null 가능) | ||||
|      * @return 수정된 ChatCharacter 객체 | ||||
|      * @throws SodaException 캐릭터를 찾을 수 없는 경우 | ||||
|      */ | ||||
|     @Transactional | ||||
|     fun updateChatCharacterWithDetails( | ||||
| @@ -526,38 +602,19 @@ class ChatCharacterService( | ||||
|  | ||||
|         // 추가 정보 설정 - 변경된 데이터만 업데이트 | ||||
|         if (request.memories != null) { | ||||
|             chatCharacter.memories.clear() | ||||
|             request.memories.forEach { memory -> | ||||
|                 chatCharacter.addMemory(memory.title, memory.content, memory.emotion) | ||||
|             } | ||||
|             updateMemoriesForCharacter(chatCharacter, request.memories) | ||||
|         } | ||||
|  | ||||
|         if (request.personalities != null) { | ||||
|             chatCharacter.personalities.clear() | ||||
|             request.personalities.forEach { personality -> | ||||
|                 chatCharacter.addPersonality(personality.trait, personality.description) | ||||
|             } | ||||
|             updatePersonalitiesForCharacter(chatCharacter, request.personalities) | ||||
|         } | ||||
|  | ||||
|         if (request.backgrounds != null) { | ||||
|             chatCharacter.backgrounds.clear() | ||||
|             request.backgrounds.forEach { background -> | ||||
|                 chatCharacter.addBackground(background.topic, background.description) | ||||
|             } | ||||
|             updateBackgroundsForCharacter(chatCharacter, request.backgrounds) | ||||
|         } | ||||
|  | ||||
|         if (request.relationships != null) { | ||||
|             chatCharacter.relationships.clear() | ||||
|             request.relationships.forEach { rr -> | ||||
|                 chatCharacter.addRelationship( | ||||
|                     rr.personName, | ||||
|                     rr.relationshipName, | ||||
|                     rr.description, | ||||
|                     rr.importance, | ||||
|                     rr.relationshipType, | ||||
|                     rr.currentStatus | ||||
|                 ) | ||||
|             } | ||||
|             updateRelationshipsForCharacter(chatCharacter, request.relationships) | ||||
|         } | ||||
|  | ||||
|         return saveChatCharacter(chatCharacter) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user