캐릭터 챗봇 #338
| @@ -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.dto.ExternalApiResponse | ||||||
| import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService | import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService | ||||||
| import kr.co.vividnext.sodalive.aws.s3.S3Uploader | 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.chat.character.service.ChatCharacterService | ||||||
| import kr.co.vividnext.sodalive.common.ApiResponse | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
| import kr.co.vividnext.sodalive.common.SodaException | import kr.co.vividnext.sodalive.common.SodaException | ||||||
| @@ -119,6 +120,12 @@ class AdminChatCharacterController( | |||||||
|             speechPattern = request.speechPattern, |             speechPattern = request.speechPattern, | ||||||
|             speechStyle = request.speechStyle, |             speechStyle = request.speechStyle, | ||||||
|             appearance = request.appearance, |             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, |             tags = request.tags, | ||||||
|             values = request.values, |             values = request.values, | ||||||
|             hobbies = request.hobbies, |             hobbies = request.hobbies, | ||||||
| @@ -152,7 +159,27 @@ class AdminChatCharacterController( | |||||||
|             headers.set("x-api-key", apiKey) // 실제 API 키로 대체 필요 |             headers.set("x-api-key", apiKey) // 실제 API 키로 대체 필요 | ||||||
|             headers.contentType = MediaType.APPLICATION_JSON |             headers.contentType = MediaType.APPLICATION_JSON | ||||||
|  |  | ||||||
|             val httpEntity = HttpEntity(request, headers) |             // 외부 API에 전달하지 않을 필드(originalTitle, originalLink, characterType)를 제외하고 바디 구성 | ||||||
|  |             val body = mutableMapOf<String, Any>() | ||||||
|  |             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( |             val response = restTemplate.exchange( | ||||||
|                 "$apiUrl/api/characters", |                 "$apiUrl/api/characters", | ||||||
| @@ -223,16 +250,22 @@ class AdminChatCharacterController( | |||||||
|         val request = objectMapper.readValue(requestString, ChatCharacterUpdateRequest::class.java) |         val request = objectMapper.readValue(requestString, ChatCharacterUpdateRequest::class.java) | ||||||
|  |  | ||||||
|         // 2. ChatCharacterUpdateRequest를 확인해서 변경한 데이터가 있는지 확인 |         // 2. ChatCharacterUpdateRequest를 확인해서 변경한 데이터가 있는지 확인 | ||||||
|         val hasChangedData = hasChanges(request) |         val hasChangedData = hasChanges(request) // 외부 API 대상으로의 변경 여부(3가지 필드 제외) | ||||||
|  |  | ||||||
|         // 3. 이미지 있는지 확인 |         // 3. 이미지 있는지 확인 | ||||||
|         val hasImage = image != null && !image.isEmpty |         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("변경된 데이터가 없습니다.") |             throw SodaException("변경된 데이터가 없습니다.") | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // 변경된 데이터가 있으면 외부 API 호출 |         // 외부 API로 전달할 변경이 있을 때만 외부 API 호출(3가지 필드만 변경된 경우는 호출하지 않음) | ||||||
|         if (hasChangedData) { |         if (hasChangedData) { | ||||||
|             val chatCharacter = service.findById(request.id) |             val chatCharacter = service.findById(request.id) | ||||||
|                 ?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}") |                 ?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}") | ||||||
|   | |||||||
| @@ -33,6 +33,9 @@ data class ChatCharacterRegisterRequest( | |||||||
|     @JsonProperty("speechPattern") val speechPattern: String?, |     @JsonProperty("speechPattern") val speechPattern: String?, | ||||||
|     @JsonProperty("speechStyle") val speechStyle: String?, |     @JsonProperty("speechStyle") val speechStyle: String?, | ||||||
|     @JsonProperty("appearance") val appearance: 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<String> = emptyList(), |     @JsonProperty("tags") val tags: List<String> = emptyList(), | ||||||
|     @JsonProperty("hobbies") val hobbies: List<String> = emptyList(), |     @JsonProperty("hobbies") val hobbies: List<String> = emptyList(), | ||||||
|     @JsonProperty("values") val values: List<String> = emptyList(), |     @JsonProperty("values") val values: List<String> = emptyList(), | ||||||
| @@ -64,6 +67,9 @@ data class ChatCharacterUpdateRequest( | |||||||
|     @JsonProperty("speechPattern") val speechPattern: String? = null, |     @JsonProperty("speechPattern") val speechPattern: String? = null, | ||||||
|     @JsonProperty("speechStyle") val speechStyle: String? = null, |     @JsonProperty("speechStyle") val speechStyle: String? = null, | ||||||
|     @JsonProperty("appearance") val appearance: 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("isActive") val isActive: Boolean? = null, | ||||||
|     @JsonProperty("tags") val tags: List<String>? = null, |     @JsonProperty("tags") val tags: List<String>? = null, | ||||||
|     @JsonProperty("hobbies") val hobbies: List<String>? = null, |     @JsonProperty("hobbies") val hobbies: List<String>? = null, | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.common.BaseEntity | |||||||
| import javax.persistence.CascadeType | import javax.persistence.CascadeType | ||||||
| import javax.persistence.Column | import javax.persistence.Column | ||||||
| import javax.persistence.Entity | import javax.persistence.Entity | ||||||
|  | import javax.persistence.EnumType | ||||||
|  | import javax.persistence.Enumerated | ||||||
| import javax.persistence.FetchType | import javax.persistence.FetchType | ||||||
| import javax.persistence.OneToMany | import javax.persistence.OneToMany | ||||||
|  |  | ||||||
| @@ -41,6 +43,19 @@ class ChatCharacter( | |||||||
|     @Column(columnDefinition = "TEXT") |     @Column(columnDefinition = "TEXT") | ||||||
|     var appearance: String? = null, |     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 |     var isActive: Boolean = true | ||||||
| ) : BaseEntity() { | ) : BaseEntity() { | ||||||
|     var imagePath: String? = null |     var imagePath: String? = null | ||||||
| @@ -117,3 +132,8 @@ class ChatCharacter( | |||||||
|         relationships.add(relationship) |         relationships.add(relationship) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | enum class CharacterType { | ||||||
|  |     CLONE, | ||||||
|  |     CHARACTER | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| 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.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.ChatCharacter | import kr.co.vividnext.sodalive.chat.character.ChatCharacter | ||||||
| import kr.co.vividnext.sodalive.chat.character.ChatCharacterGoal | import kr.co.vividnext.sodalive.chat.character.ChatCharacterGoal | ||||||
| import kr.co.vividnext.sodalive.chat.character.ChatCharacterHobby | import kr.co.vividnext.sodalive.chat.character.ChatCharacterHobby | ||||||
| @@ -208,6 +209,9 @@ class ChatCharacterService( | |||||||
|         speechPattern: String? = null, |         speechPattern: String? = null, | ||||||
|         speechStyle: String? = null, |         speechStyle: String? = null, | ||||||
|         appearance: String? = null, |         appearance: String? = null, | ||||||
|  |         originalTitle: String? = null, | ||||||
|  |         originalLink: String? = null, | ||||||
|  |         characterType: CharacterType = CharacterType.CHARACTER, | ||||||
|         tags: List<String> = emptyList(), |         tags: List<String> = emptyList(), | ||||||
|         values: List<String> = emptyList(), |         values: List<String> = emptyList(), | ||||||
|         hobbies: List<String> = emptyList(), |         hobbies: List<String> = emptyList(), | ||||||
| @@ -223,7 +227,10 @@ class ChatCharacterService( | |||||||
|             mbti = mbti, |             mbti = mbti, | ||||||
|             speechPattern = speechPattern, |             speechPattern = speechPattern, | ||||||
|             speechStyle = speechStyle, |             speechStyle = speechStyle, | ||||||
|             appearance = appearance |             appearance = appearance, | ||||||
|  |             originalTitle = originalTitle, | ||||||
|  |             originalLink = originalLink, | ||||||
|  |             characterType = characterType | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         // 관련 엔티티 연결 |         // 관련 엔티티 연결 | ||||||
| @@ -286,6 +293,9 @@ class ChatCharacterService( | |||||||
|         speechPattern: String? = null, |         speechPattern: String? = null, | ||||||
|         speechStyle: String? = null, |         speechStyle: String? = null, | ||||||
|         appearance: String? = null, |         appearance: String? = null, | ||||||
|  |         originalTitle: String? = null, | ||||||
|  |         originalLink: String? = null, | ||||||
|  |         characterType: CharacterType = CharacterType.CHARACTER, | ||||||
|         tags: List<String> = emptyList(), |         tags: List<String> = emptyList(), | ||||||
|         values: List<String> = emptyList(), |         values: List<String> = emptyList(), | ||||||
|         hobbies: List<String> = emptyList(), |         hobbies: List<String> = emptyList(), | ||||||
| @@ -296,8 +306,23 @@ class ChatCharacterService( | |||||||
|         relationships: List<Pair<String, String>> = emptyList() |         relationships: List<Pair<String, String>> = emptyList() | ||||||
|     ): ChatCharacter { |     ): ChatCharacter { | ||||||
|         val chatCharacter = createChatCharacter( |         val chatCharacter = createChatCharacter( | ||||||
|             characterUUID, name, description, systemPrompt, age, gender, mbti, |             characterUUID = characterUUID, | ||||||
|             speechPattern, speechStyle, appearance, tags, values, hobbies, goals |             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.speechPattern?.let { chatCharacter.speechPattern = it } | ||||||
|         request.speechStyle?.let { chatCharacter.speechStyle = it } |         request.speechStyle?.let { chatCharacter.speechStyle = it } | ||||||
|         request.appearance?.let { chatCharacter.appearance = 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에서 변경된 데이터만 업데이트 |         // request에서 변경된 데이터만 업데이트 | ||||||
|         if (request.tags != null) { |         if (request.tags != null) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user