diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt index 498688c..6cb49ca 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequ import kr.co.vividnext.sodalive.admin.chat.character.dto.ExternalApiResponse import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.chat.character.CharacterType import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException @@ -119,6 +120,12 @@ class AdminChatCharacterController( speechPattern = request.speechPattern, speechStyle = request.speechStyle, appearance = request.appearance, + originalTitle = request.originalTitle, + originalLink = request.originalLink, + characterType = request.characterType?.let { + runCatching { CharacterType.valueOf(it) } + .getOrDefault(CharacterType.CHARACTER) + } ?: CharacterType.CHARACTER, tags = request.tags, values = request.values, hobbies = request.hobbies, @@ -152,7 +159,27 @@ class AdminChatCharacterController( headers.set("x-api-key", apiKey) // 실제 API 키로 대체 필요 headers.contentType = MediaType.APPLICATION_JSON - val httpEntity = HttpEntity(request, headers) + // 외부 API에 전달하지 않을 필드(originalTitle, originalLink, characterType)를 제외하고 바디 구성 + val body = mutableMapOf() + body["name"] = request.name + body["systemPrompt"] = request.systemPrompt + body["description"] = request.description + request.age?.let { body["age"] = it } + request.gender?.let { body["gender"] = it } + request.mbti?.let { body["mbti"] = it } + request.speechPattern?.let { body["speechPattern"] = it } + request.speechStyle?.let { body["speechStyle"] = it } + request.appearance?.let { body["appearance"] = it } + if (request.tags.isNotEmpty()) body["tags"] = request.tags + if (request.hobbies.isNotEmpty()) body["hobbies"] = request.hobbies + if (request.values.isNotEmpty()) body["values"] = request.values + if (request.goals.isNotEmpty()) body["goals"] = request.goals + if (request.relationships.isNotEmpty()) body["relationships"] = request.relationships + if (request.personalities.isNotEmpty()) body["personalities"] = request.personalities + if (request.backgrounds.isNotEmpty()) body["backgrounds"] = request.backgrounds + if (request.memories.isNotEmpty()) body["memories"] = request.memories + + val httpEntity = HttpEntity(body, headers) val response = restTemplate.exchange( "$apiUrl/api/characters", @@ -223,16 +250,22 @@ class AdminChatCharacterController( val request = objectMapper.readValue(requestString, ChatCharacterUpdateRequest::class.java) // 2. ChatCharacterUpdateRequest를 확인해서 변경한 데이터가 있는지 확인 - val hasChangedData = hasChanges(request) + val hasChangedData = hasChanges(request) // 외부 API 대상으로의 변경 여부(3가지 필드 제외) // 3. 이미지 있는지 확인 val hasImage = image != null && !image.isEmpty - if (!hasChangedData && !hasImage) { + // 3가지만 변경된 경우(외부 API 변경은 없지만 DB 변경은 있는 경우)를 허용하기 위해 별도 플래그 계산 + val hasDbOnlyChanges = + request.originalTitle != null || + request.originalLink != null || + request.characterType != null + + if (!hasChangedData && !hasImage && !hasDbOnlyChanges) { throw SodaException("변경된 데이터가 없습니다.") } - // 변경된 데이터가 있으면 외부 API 호출 + // 외부 API로 전달할 변경이 있을 때만 외부 API 호출(3가지 필드만 변경된 경우는 호출하지 않음) if (hasChangedData) { val chatCharacter = service.findById(request.id) ?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt index 773d814..7cdf9df 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt @@ -33,6 +33,9 @@ data class ChatCharacterRegisterRequest( @JsonProperty("speechPattern") val speechPattern: String?, @JsonProperty("speechStyle") val speechStyle: String?, @JsonProperty("appearance") val appearance: String?, + @JsonProperty("originalTitle") val originalTitle: String? = null, + @JsonProperty("originalLink") val originalLink: String? = null, + @JsonProperty("characterType") val characterType: String? = null, @JsonProperty("tags") val tags: List = emptyList(), @JsonProperty("hobbies") val hobbies: List = emptyList(), @JsonProperty("values") val values: List = emptyList(), @@ -64,6 +67,9 @@ data class ChatCharacterUpdateRequest( @JsonProperty("speechPattern") val speechPattern: String? = null, @JsonProperty("speechStyle") val speechStyle: String? = null, @JsonProperty("appearance") val appearance: String? = null, + @JsonProperty("originalTitle") val originalTitle: String? = null, + @JsonProperty("originalLink") val originalLink: String? = null, + @JsonProperty("characterType") val characterType: String? = null, @JsonProperty("isActive") val isActive: Boolean? = null, @JsonProperty("tags") val tags: List? = null, @JsonProperty("hobbies") val hobbies: List? = null, 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 2dfbf99..72f7caf 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 @@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.common.BaseEntity import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated import javax.persistence.FetchType import javax.persistence.OneToMany @@ -41,6 +43,19 @@ class ChatCharacter( @Column(columnDefinition = "TEXT") var appearance: String? = null, + // 원작 (optional) + @Column(nullable = true) + var originalTitle: String? = null, + + // 원작 링크 (optional) + @Column(nullable = true) + var originalLink: String? = null, + + // 캐릭터 유형 + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var characterType: CharacterType = CharacterType.CHARACTER, + var isActive: Boolean = true ) : BaseEntity() { var imagePath: String? = null @@ -117,3 +132,8 @@ class ChatCharacter( relationships.add(relationship) } } + +enum class CharacterType { + CLONE, + CHARACTER +} 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 2e567c0..4732670 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,6 +1,7 @@ package kr.co.vividnext.sodalive.chat.character.service 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.ChatCharacter import kr.co.vividnext.sodalive.chat.character.ChatCharacterGoal import kr.co.vividnext.sodalive.chat.character.ChatCharacterHobby @@ -208,6 +209,9 @@ class ChatCharacterService( speechPattern: String? = null, speechStyle: String? = null, appearance: String? = null, + originalTitle: String? = null, + originalLink: String? = null, + characterType: CharacterType = CharacterType.CHARACTER, tags: List = emptyList(), values: List = emptyList(), hobbies: List = emptyList(), @@ -223,7 +227,10 @@ class ChatCharacterService( mbti = mbti, speechPattern = speechPattern, speechStyle = speechStyle, - appearance = appearance + appearance = appearance, + originalTitle = originalTitle, + originalLink = originalLink, + characterType = characterType ) // 관련 엔티티 연결 @@ -286,6 +293,9 @@ class ChatCharacterService( speechPattern: String? = null, speechStyle: String? = null, appearance: String? = null, + originalTitle: String? = null, + originalLink: String? = null, + characterType: CharacterType = CharacterType.CHARACTER, tags: List = emptyList(), values: List = emptyList(), hobbies: List = emptyList(), @@ -296,8 +306,23 @@ class ChatCharacterService( relationships: List> = emptyList() ): ChatCharacter { val chatCharacter = createChatCharacter( - characterUUID, name, description, systemPrompt, age, gender, mbti, - speechPattern, speechStyle, appearance, tags, values, hobbies, goals + characterUUID = characterUUID, + name = name, + description = description, + systemPrompt = systemPrompt, + age = age, + gender = gender, + mbti = mbti, + speechPattern = speechPattern, + speechStyle = speechStyle, + appearance = appearance, + originalTitle = originalTitle, + originalLink = originalLink, + characterType = characterType, + tags = tags, + values = values, + hobbies = hobbies, + goals = goals ) // 추가 정보 설정 @@ -365,6 +390,11 @@ class ChatCharacterService( request.speechPattern?.let { chatCharacter.speechPattern = it } request.speechStyle?.let { chatCharacter.speechStyle = it } request.appearance?.let { chatCharacter.appearance = it } + request.originalTitle?.let { chatCharacter.originalTitle = it } + request.originalLink?.let { chatCharacter.originalLink = it } + request.characterType?.let { + runCatching { CharacterType.valueOf(it) }.getOrNull()?.let { ct -> chatCharacter.characterType = ct } + } // request에서 변경된 데이터만 업데이트 if (request.tags != null) {