Compare commits
5 Commits
c525ec0330
...
00c617ec2e
| Author | SHA1 | Date | |
|---|---|---|---|
| 00c617ec2e | |||
| 01ef738d31 | |||
| 423cbe7315 | |||
| afb003c397 | |||
| 2dc5a29220 |
@@ -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,
|
||||||
@@ -126,7 +133,7 @@ class AdminChatCharacterController(
|
|||||||
memories = request.memories.map { Triple(it.title, it.content, it.emotion) },
|
memories = request.memories.map { Triple(it.title, it.content, it.emotion) },
|
||||||
personalities = request.personalities.map { Pair(it.trait, it.description) },
|
personalities = request.personalities.map { Pair(it.trait, it.description) },
|
||||||
backgrounds = request.backgrounds.map { Pair(it.topic, it.description) },
|
backgrounds = request.backgrounds.map { Pair(it.topic, it.description) },
|
||||||
relationships = request.relationships
|
relationships = request.relationships.map { it.name to it.relationShip }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3. 이미지 저장 및 ChatCharacter에 이미지 path 설정
|
// 3. 이미지 저장 및 ChatCharacter에 이미지 path 설정
|
||||||
@@ -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}")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class ChatCharacterDetailResponse(
|
|||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val description: String,
|
val description: String,
|
||||||
val systemPrompt: String,
|
val systemPrompt: String,
|
||||||
|
val characterType: String,
|
||||||
val age: Int?,
|
val age: Int?,
|
||||||
val gender: String?,
|
val gender: String?,
|
||||||
val mbti: String?,
|
val mbti: String?,
|
||||||
@@ -20,7 +21,7 @@ data class ChatCharacterDetailResponse(
|
|||||||
val hobbies: List<String>,
|
val hobbies: List<String>,
|
||||||
val values: List<String>,
|
val values: List<String>,
|
||||||
val goals: List<String>,
|
val goals: List<String>,
|
||||||
val relationships: List<String>,
|
val relationships: List<RelationshipResponse>,
|
||||||
val personalities: List<PersonalityResponse>,
|
val personalities: List<PersonalityResponse>,
|
||||||
val backgrounds: List<BackgroundResponse>,
|
val backgrounds: List<BackgroundResponse>,
|
||||||
val memories: List<MemoryResponse>
|
val memories: List<MemoryResponse>
|
||||||
@@ -40,6 +41,7 @@ data class ChatCharacterDetailResponse(
|
|||||||
imageUrl = fullImagePath,
|
imageUrl = fullImagePath,
|
||||||
description = chatCharacter.description,
|
description = chatCharacter.description,
|
||||||
systemPrompt = chatCharacter.systemPrompt,
|
systemPrompt = chatCharacter.systemPrompt,
|
||||||
|
characterType = chatCharacter.characterType.name,
|
||||||
age = chatCharacter.age,
|
age = chatCharacter.age,
|
||||||
gender = chatCharacter.gender,
|
gender = chatCharacter.gender,
|
||||||
mbti = chatCharacter.mbti,
|
mbti = chatCharacter.mbti,
|
||||||
@@ -51,7 +53,7 @@ data class ChatCharacterDetailResponse(
|
|||||||
hobbies = chatCharacter.hobbyMappings.map { it.hobby.hobby },
|
hobbies = chatCharacter.hobbyMappings.map { it.hobby.hobby },
|
||||||
values = chatCharacter.valueMappings.map { it.value.value },
|
values = chatCharacter.valueMappings.map { it.value.value },
|
||||||
goals = chatCharacter.goalMappings.map { it.goal.goal },
|
goals = chatCharacter.goalMappings.map { it.goal.goal },
|
||||||
relationships = chatCharacter.relationships.map { it.relationShip },
|
relationships = chatCharacter.relationships.map { RelationshipResponse(it.name, it.relationShip) },
|
||||||
personalities = chatCharacter.personalities.map {
|
personalities = chatCharacter.personalities.map {
|
||||||
PersonalityResponse(it.trait, it.description)
|
PersonalityResponse(it.trait, it.description)
|
||||||
},
|
},
|
||||||
@@ -81,3 +83,8 @@ data class MemoryResponse(
|
|||||||
val content: String,
|
val content: String,
|
||||||
val emotion: String
|
val emotion: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class RelationshipResponse(
|
||||||
|
val name: String,
|
||||||
|
val relationShip: String
|
||||||
|
)
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ data class ChatCharacterMemoryRequest(
|
|||||||
@JsonProperty("emotion") val emotion: String
|
@JsonProperty("emotion") val emotion: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class ChatCharacterRelationshipRequest(
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("relationShip") val relationShip: String
|
||||||
|
)
|
||||||
|
|
||||||
data class ChatCharacterRegisterRequest(
|
data class ChatCharacterRegisterRequest(
|
||||||
@JsonProperty("name") val name: String,
|
@JsonProperty("name") val name: String,
|
||||||
@JsonProperty("systemPrompt") val systemPrompt: String,
|
@JsonProperty("systemPrompt") val systemPrompt: String,
|
||||||
@@ -28,11 +33,14 @@ 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(),
|
||||||
@JsonProperty("goals") val goals: List<String> = emptyList(),
|
@JsonProperty("goals") val goals: List<String> = emptyList(),
|
||||||
@JsonProperty("relationships") val relationships: List<String> = emptyList(),
|
@JsonProperty("relationships") val relationships: List<ChatCharacterRelationshipRequest> = emptyList(),
|
||||||
@JsonProperty("personalities") val personalities: List<ChatCharacterPersonalityRequest> = emptyList(),
|
@JsonProperty("personalities") val personalities: List<ChatCharacterPersonalityRequest> = emptyList(),
|
||||||
@JsonProperty("backgrounds") val backgrounds: List<ChatCharacterBackgroundRequest> = emptyList(),
|
@JsonProperty("backgrounds") val backgrounds: List<ChatCharacterBackgroundRequest> = emptyList(),
|
||||||
@JsonProperty("memories") val memories: List<ChatCharacterMemoryRequest> = emptyList()
|
@JsonProperty("memories") val memories: List<ChatCharacterMemoryRequest> = emptyList()
|
||||||
@@ -59,12 +67,15 @@ 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,
|
||||||
@JsonProperty("values") val values: List<String>? = null,
|
@JsonProperty("values") val values: List<String>? = null,
|
||||||
@JsonProperty("goals") val goals: List<String>? = null,
|
@JsonProperty("goals") val goals: List<String>? = null,
|
||||||
@JsonProperty("relationships") val relationships: List<String>? = null,
|
@JsonProperty("relationships") val relationships: List<ChatCharacterRelationshipRequest>? = null,
|
||||||
@JsonProperty("personalities") val personalities: List<ChatCharacterPersonalityRequest>? = null,
|
@JsonProperty("personalities") val personalities: List<ChatCharacterPersonalityRequest>? = null,
|
||||||
@JsonProperty("backgrounds") val backgrounds: List<ChatCharacterBackgroundRequest>? = null,
|
@JsonProperty("backgrounds") val backgrounds: List<ChatCharacterBackgroundRequest>? = null,
|
||||||
@JsonProperty("memories") val memories: List<ChatCharacterMemoryRequest>? = null
|
@JsonProperty("memories") val memories: List<ChatCharacterMemoryRequest>? = 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
|
||||||
|
|
||||||
@@ -14,8 +16,7 @@ class ChatCharacter(
|
|||||||
// 캐릭터 이름 (API 키 내에서 유일해야 함)
|
// 캐릭터 이름 (API 키 내에서 유일해야 함)
|
||||||
var name: String,
|
var name: String,
|
||||||
|
|
||||||
// 캐릭터 설명
|
// 캐릭터 한 줄 소개
|
||||||
@Column(columnDefinition = "TEXT", nullable = false)
|
|
||||||
var description: String,
|
var description: String,
|
||||||
|
|
||||||
// AI 시스템 프롬프트
|
// AI 시스템 프롬프트
|
||||||
@@ -42,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
|
||||||
@@ -113,8 +127,13 @@ class ChatCharacter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 관계 추가 헬퍼 메소드
|
// 관계 추가 헬퍼 메소드
|
||||||
fun addRelationship(relationShip: String) {
|
fun addRelationship(name: String, relationShip: String) {
|
||||||
val relationship = ChatCharacterRelationship(relationShip, this)
|
val relationship = ChatCharacterRelationship(name, relationShip, this)
|
||||||
relationships.add(relationship)
|
relationships.add(relationship)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class CharacterType {
|
||||||
|
CLONE,
|
||||||
|
CHARACTER
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import javax.persistence.ManyToOne
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class ChatCharacterRelationship(
|
class ChatCharacterRelationship(
|
||||||
|
var name: String,
|
||||||
val relationShip: String,
|
val relationShip: String,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import kr.co.vividnext.sodalive.chat.character.dto.CharacterBackgroundResponse
|
|||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBannerResponse
|
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBannerResponse
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterDetailResponse
|
import kr.co.vividnext.sodalive.chat.character.dto.CharacterDetailResponse
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterMainResponse
|
import kr.co.vividnext.sodalive.chat.character.dto.CharacterMainResponse
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterMemoryResponse
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterPersonalityResponse
|
import kr.co.vividnext.sodalive.chat.character.dto.CharacterPersonalityResponse
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CurationSection
|
import kr.co.vividnext.sodalive.chat.character.dto.CurationSection
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.dto.OtherCharacter
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
|
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||||
@@ -108,36 +108,39 @@ class ChatCharacterController(
|
|||||||
val character = service.getCharacterDetail(characterId)
|
val character = service.getCharacterDetail(characterId)
|
||||||
?: throw SodaException("캐릭터를 찾을 수 없습니다.")
|
?: throw SodaException("캐릭터를 찾을 수 없습니다.")
|
||||||
|
|
||||||
// 태그, 가치관, 취미, 목표 추출
|
// 태그 가공: # prefix 규칙 적용 후 공백으로 연결
|
||||||
val tags = character.tagMappings.map { it.tag.tag }
|
val tags = character.tagMappings
|
||||||
val values = character.valueMappings.map { it.value.value }
|
.map { it.tag.tag }
|
||||||
val hobbies = character.hobbyMappings.map { it.hobby.hobby }
|
.joinToString(" ") { if (it.startsWith("#")) it else "#$it" }
|
||||||
val goals = character.goalMappings.map { it.goal.goal }
|
|
||||||
|
|
||||||
// 메모리, 성격, 배경, 관계 변환
|
// 성격, 배경: 각각 첫 번째 항목만 선택
|
||||||
val memories = character.memories.map {
|
val personality: CharacterPersonalityResponse? = character.personalities.firstOrNull()?.let {
|
||||||
CharacterMemoryResponse(
|
|
||||||
title = it.title,
|
|
||||||
content = it.content,
|
|
||||||
emotion = it.emotion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val personalities = character.personalities.map {
|
|
||||||
CharacterPersonalityResponse(
|
CharacterPersonalityResponse(
|
||||||
trait = it.trait,
|
trait = it.trait,
|
||||||
description = it.description
|
description = it.description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val backgrounds = character.backgrounds.map {
|
val background: CharacterBackgroundResponse? = character.backgrounds.firstOrNull()?.let {
|
||||||
CharacterBackgroundResponse(
|
CharacterBackgroundResponse(
|
||||||
topic = it.topic,
|
topic = it.topic,
|
||||||
description = it.description
|
description = it.description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val relationships = character.relationships.map { it.relationShip }
|
// 다른 캐릭터 조회 (태그 기반, 랜덤 10개, 현재 캐릭터 제외)
|
||||||
|
val others = service.getOtherCharactersBySharedTags(characterId, 10)
|
||||||
|
.map { other ->
|
||||||
|
val otherTags = other.tagMappings
|
||||||
|
.map { it.tag.tag }
|
||||||
|
.joinToString(" ") { if (it.startsWith("#")) it else "#$it" }
|
||||||
|
OtherCharacter(
|
||||||
|
characterId = other.id!!,
|
||||||
|
name = other.name,
|
||||||
|
imageUrl = "$imageHost/${other.imagePath ?: "profile/default-profile.png"}",
|
||||||
|
tags = otherTags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 응답 생성
|
// 응답 생성
|
||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
@@ -145,21 +148,15 @@ class ChatCharacterController(
|
|||||||
characterId = character.id!!,
|
characterId = character.id!!,
|
||||||
name = character.name,
|
name = character.name,
|
||||||
description = character.description,
|
description = character.description,
|
||||||
age = character.age,
|
|
||||||
gender = character.gender,
|
|
||||||
mbti = character.mbti,
|
mbti = character.mbti,
|
||||||
speechPattern = character.speechPattern,
|
|
||||||
speechStyle = character.speechStyle,
|
|
||||||
appearance = character.appearance,
|
|
||||||
imageUrl = "$imageHost/${character.imagePath ?: "profile/default-profile.png"}",
|
imageUrl = "$imageHost/${character.imagePath ?: "profile/default-profile.png"}",
|
||||||
memories = memories,
|
personalities = personality,
|
||||||
personalities = personalities,
|
backgrounds = background,
|
||||||
backgrounds = backgrounds,
|
|
||||||
relationships = relationships,
|
|
||||||
tags = tags,
|
tags = tags,
|
||||||
values = values,
|
originalTitle = character.originalTitle,
|
||||||
hobbies = hobbies,
|
originalLink = character.originalLink,
|
||||||
goals = goals
|
characterType = character.characterType,
|
||||||
|
others = others
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.dto
|
package kr.co.vividnext.sodalive.chat.character.dto
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.CharacterType
|
||||||
|
|
||||||
data class CharacterDetailResponse(
|
data class CharacterDetailResponse(
|
||||||
val characterId: Long,
|
val characterId: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val age: Int?,
|
|
||||||
val gender: String?,
|
|
||||||
val mbti: String?,
|
val mbti: String?,
|
||||||
val speechPattern: String?,
|
|
||||||
val speechStyle: String?,
|
|
||||||
val appearance: String?,
|
|
||||||
val imageUrl: String,
|
val imageUrl: String,
|
||||||
val memories: List<CharacterMemoryResponse> = emptyList(),
|
val personalities: CharacterPersonalityResponse?,
|
||||||
val personalities: List<CharacterPersonalityResponse> = emptyList(),
|
val backgrounds: CharacterBackgroundResponse?,
|
||||||
val backgrounds: List<CharacterBackgroundResponse> = emptyList(),
|
val tags: String,
|
||||||
val relationships: List<String> = emptyList(),
|
val originalTitle: String?,
|
||||||
val tags: List<String> = emptyList(),
|
val originalLink: String?,
|
||||||
val values: List<String> = emptyList(),
|
val characterType: CharacterType,
|
||||||
val hobbies: List<String> = emptyList(),
|
val others: List<OtherCharacter>
|
||||||
val goals: List<String> = emptyList()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class CharacterMemoryResponse(
|
data class OtherCharacter(
|
||||||
val title: String,
|
val characterId: Long,
|
||||||
val content: String,
|
val name: String,
|
||||||
val emotion: String
|
val imageUrl: String,
|
||||||
|
val tags: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class CharacterPersonalityResponse(
|
data class CharacterPersonalityResponse(
|
||||||
|
|||||||
@@ -66,4 +66,25 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
|||||||
@Param("member") member: Member,
|
@Param("member") member: Member,
|
||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): List<ChatCharacter>
|
): List<ChatCharacter>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터를 무작위로 조회 (현재 캐릭터 제외)
|
||||||
|
*/
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT c FROM ChatCharacter c
|
||||||
|
JOIN c.tagMappings tm
|
||||||
|
JOIN tm.tag t
|
||||||
|
WHERE c.isActive = true
|
||||||
|
AND c.id <> :characterId
|
||||||
|
AND t.id IN (
|
||||||
|
SELECT t2.id FROM ChatCharacterTagMapping tm2 JOIN tm2.tag t2 WHERE tm2.chatCharacter.id = :characterId
|
||||||
|
)
|
||||||
|
ORDER BY function('RAND')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun findRandomBySharedTags(
|
||||||
|
@Param("characterId") characterId: Long,
|
||||||
|
pageable: Pageable
|
||||||
|
): List<ChatCharacter>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -52,6 +53,20 @@ class ChatCharacterService(
|
|||||||
return chatCharacterRepository.findByIsActiveTrueOrderByCreatedAtDesc(PageRequest.of(0, limit))
|
return chatCharacterRepository.findByIsActiveTrueOrderByCreatedAtDesc(PageRequest.of(0, limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터를 무작위로 조회 (현재 캐릭터 제외)
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun getOtherCharactersBySharedTags(characterId: Long, limit: Int = 10): List<ChatCharacter> {
|
||||||
|
val others = chatCharacterRepository.findRandomBySharedTags(
|
||||||
|
characterId,
|
||||||
|
PageRequest.of(0, limit)
|
||||||
|
)
|
||||||
|
// 태그 초기화 (지연 로딩 문제 방지)
|
||||||
|
others.forEach { it.tagMappings.size }
|
||||||
|
return others
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 태그를 찾거나 생성하여 캐릭터에 연결
|
* 태그를 찾거나 생성하여 캐릭터에 연결
|
||||||
*/
|
*/
|
||||||
@@ -208,6 +223,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 +241,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
|
||||||
)
|
)
|
||||||
|
|
||||||
// 관련 엔티티 연결
|
// 관련 엔티티 연결
|
||||||
@@ -266,8 +287,8 @@ class ChatCharacterService(
|
|||||||
* 캐릭터에 관계 추가
|
* 캐릭터에 관계 추가
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
fun addRelationshipToChatCharacter(chatCharacter: ChatCharacter, relationShip: String) {
|
fun addRelationshipToChatCharacter(chatCharacter: ChatCharacter, name: String, relationShip: String) {
|
||||||
chatCharacter.addRelationship(relationShip)
|
chatCharacter.addRelationship(name, relationShip)
|
||||||
saveChatCharacter(chatCharacter)
|
saveChatCharacter(chatCharacter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +307,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(),
|
||||||
@@ -293,11 +317,26 @@ class ChatCharacterService(
|
|||||||
memories: List<Triple<String, String, String>> = emptyList(),
|
memories: List<Triple<String, String, String>> = emptyList(),
|
||||||
personalities: List<Pair<String, String>> = emptyList(),
|
personalities: List<Pair<String, String>> = emptyList(),
|
||||||
backgrounds: List<Pair<String, String>> = emptyList(),
|
backgrounds: List<Pair<String, String>> = emptyList(),
|
||||||
relationships: List<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
|
||||||
)
|
)
|
||||||
|
|
||||||
// 추가 정보 설정
|
// 추가 정보 설정
|
||||||
@@ -313,8 +352,8 @@ class ChatCharacterService(
|
|||||||
chatCharacter.addBackground(topic, description)
|
chatCharacter.addBackground(topic, description)
|
||||||
}
|
}
|
||||||
|
|
||||||
relationships.forEach { relationShip ->
|
relationships.forEach { (name, relationShip) ->
|
||||||
chatCharacter.addRelationship(relationShip)
|
chatCharacter.addRelationship(name, relationShip)
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveChatCharacter(chatCharacter)
|
return saveChatCharacter(chatCharacter)
|
||||||
@@ -365,6 +404,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) {
|
||||||
@@ -412,7 +456,7 @@ class ChatCharacterService(
|
|||||||
if (request.relationships != null) {
|
if (request.relationships != null) {
|
||||||
chatCharacter.relationships.clear()
|
chatCharacter.relationships.clear()
|
||||||
request.relationships.forEach { relationship ->
|
request.relationships.forEach { relationship ->
|
||||||
chatCharacter.addRelationship(relationship)
|
chatCharacter.addRelationship(relationship.name, relationship.relationShip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user