diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt index 1e5e30a..96738f0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt @@ -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 = mutableListOf() - @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) + @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) var personalities: MutableList = mutableListOf() - @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) + @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) var backgrounds: MutableList = mutableListOf() - @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) + @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) var relationships: MutableList = mutableListOf() @OneToMany(mappedBy = "chatCharacter", cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBackground.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBackground.kt index a3297fa..b1cd2c3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBackground.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBackground.kt @@ -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") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterMemory.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterMemory.kt index 9ef9380..1f59eec 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterMemory.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterMemory.kt @@ -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") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterPersonality.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterPersonality.kt index 647c234..3cfff76 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterPersonality.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterPersonality.kt @@ -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") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt index cdec44d..c052972 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt @@ -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) { + 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 + ) { + 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) { + 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 + ) { + 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 { - 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)