캐릭터 챗봇 #338
| @@ -61,16 +61,16 @@ class ChatCharacter( | |||||||
| ) : BaseEntity() { | ) : BaseEntity() { | ||||||
|     var imagePath: String? = null |     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() |     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() |     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() |     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() |     var relationships: MutableList<ChatCharacterRelationship> = mutableListOf() | ||||||
|  |  | ||||||
|     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) |     @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ class ChatCharacterBackground( | |||||||
|  |  | ||||||
|     // 배경 설명 |     // 배경 설명 | ||||||
|     @Column(columnDefinition = "TEXT", nullable = false) |     @Column(columnDefinition = "TEXT", nullable = false) | ||||||
|     val description: String, |     var description: String, | ||||||
|  |  | ||||||
|     @ManyToOne(fetch = FetchType.LAZY) |     @ManyToOne(fetch = FetchType.LAZY) | ||||||
|     @JoinColumn(name = "chat_character_id") |     @JoinColumn(name = "chat_character_id") | ||||||
|   | |||||||
| @@ -18,10 +18,10 @@ class ChatCharacterMemory( | |||||||
|  |  | ||||||
|     // 기억 내용 |     // 기억 내용 | ||||||
|     @Column(columnDefinition = "TEXT", nullable = false) |     @Column(columnDefinition = "TEXT", nullable = false) | ||||||
|     val content: String, |     var content: String, | ||||||
|  |  | ||||||
|     // 감정 |     // 감정 | ||||||
|     val emotion: String, |     var emotion: String, | ||||||
|  |  | ||||||
|     @ManyToOne(fetch = FetchType.LAZY) |     @ManyToOne(fetch = FetchType.LAZY) | ||||||
|     @JoinColumn(name = "chat_character_id") |     @JoinColumn(name = "chat_character_id") | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ class ChatCharacterPersonality( | |||||||
|  |  | ||||||
|     // 성격 특성 설명 |     // 성격 특성 설명 | ||||||
|     @Column(columnDefinition = "TEXT", nullable = false) |     @Column(columnDefinition = "TEXT", nullable = false) | ||||||
|     val description: String, |     var description: String, | ||||||
|  |  | ||||||
|     @ManyToOne(fetch = FetchType.LAZY) |     @ManyToOne(fetch = FetchType.LAZY) | ||||||
|     @JoinColumn(name = "chat_character_id") |     @JoinColumn(name = "chat_character_id") | ||||||
|   | |||||||
| @@ -1,5 +1,8 @@ | |||||||
| package kr.co.vividnext.sodalive.chat.character.service | 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.ChatCharacterRelationshipRequest | ||||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest | import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest | ||||||
| import kr.co.vividnext.sodalive.chat.character.CharacterType | 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) |         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) |         return chatCharacterRepository.findByName(name) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 모든 캐릭터 조회 |  | ||||||
|      */ |  | ||||||
|     @Transactional(readOnly = true) |  | ||||||
|     fun findAll(): List<ChatCharacter> { |  | ||||||
|         return chatCharacterRepository.findAll() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * ID로 캐릭터 조회 |      * ID로 캐릭터 조회 | ||||||
|      */ |      */ | ||||||
| @@ -331,57 +459,6 @@ class ChatCharacterService( | |||||||
|         return saveChatCharacter(chatCharacter) |         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 imagePath 이미지 경로 (null이면 이미지 변경 없음) | ||||||
|      * @param request 수정 요청 데이터 (id를 제외한 모든 필드는 null 가능) |      * @param request 수정 요청 데이터 (id를 제외한 모든 필드는 null 가능) | ||||||
|      * @return 수정된 ChatCharacter 객체 |      * @return 수정된 ChatCharacter 객체 | ||||||
|      * @throws SodaException 캐릭터를 찾을 수 없는 경우 |  | ||||||
|      */ |      */ | ||||||
|     @Transactional |     @Transactional | ||||||
|     fun updateChatCharacterWithDetails( |     fun updateChatCharacterWithDetails( | ||||||
| @@ -526,38 +602,19 @@ class ChatCharacterService( | |||||||
|  |  | ||||||
|         // 추가 정보 설정 - 변경된 데이터만 업데이트 |         // 추가 정보 설정 - 변경된 데이터만 업데이트 | ||||||
|         if (request.memories != null) { |         if (request.memories != null) { | ||||||
|             chatCharacter.memories.clear() |             updateMemoriesForCharacter(chatCharacter, request.memories) | ||||||
|             request.memories.forEach { memory -> |  | ||||||
|                 chatCharacter.addMemory(memory.title, memory.content, memory.emotion) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (request.personalities != null) { |         if (request.personalities != null) { | ||||||
|             chatCharacter.personalities.clear() |             updatePersonalitiesForCharacter(chatCharacter, request.personalities) | ||||||
|             request.personalities.forEach { personality -> |  | ||||||
|                 chatCharacter.addPersonality(personality.trait, personality.description) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (request.backgrounds != null) { |         if (request.backgrounds != null) { | ||||||
|             chatCharacter.backgrounds.clear() |             updateBackgroundsForCharacter(chatCharacter, request.backgrounds) | ||||||
|             request.backgrounds.forEach { background -> |  | ||||||
|                 chatCharacter.addBackground(background.topic, background.description) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (request.relationships != null) { |         if (request.relationships != null) { | ||||||
|             chatCharacter.relationships.clear() |             updateRelationshipsForCharacter(chatCharacter, request.relationships) | ||||||
|             request.relationships.forEach { rr -> |  | ||||||
|                 chatCharacter.addRelationship( |  | ||||||
|                     rr.personName, |  | ||||||
|                     rr.relationshipName, |  | ||||||
|                     rr.description, |  | ||||||
|                     rr.importance, |  | ||||||
|                     rr.relationshipType, |  | ||||||
|                     rr.currentStatus |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return saveChatCharacter(chatCharacter) |         return saveChatCharacter(chatCharacter) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user