|
|
|
|
@@ -0,0 +1,147 @@
|
|
|
|
|
package kr.co.vividnext.sodalive.admin.chat.character
|
|
|
|
|
|
|
|
|
|
import com.amazonaws.services.s3.model.ObjectMetadata
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper
|
|
|
|
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterRegisterRequest
|
|
|
|
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ExternalApiResponse
|
|
|
|
|
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
|
|
|
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
|
|
|
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
|
|
|
|
import kr.co.vividnext.sodalive.common.SodaException
|
|
|
|
|
import kr.co.vividnext.sodalive.utils.generateFileName
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value
|
|
|
|
|
import org.springframework.http.HttpEntity
|
|
|
|
|
import org.springframework.http.HttpHeaders
|
|
|
|
|
import org.springframework.http.HttpMethod
|
|
|
|
|
import org.springframework.http.MediaType
|
|
|
|
|
import org.springframework.http.client.SimpleClientHttpRequestFactory
|
|
|
|
|
import org.springframework.retry.annotation.Backoff
|
|
|
|
|
import org.springframework.retry.annotation.Retryable
|
|
|
|
|
import org.springframework.security.access.prepost.PreAuthorize
|
|
|
|
|
import org.springframework.web.bind.annotation.PostMapping
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestMapping
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestPart
|
|
|
|
|
import org.springframework.web.bind.annotation.RestController
|
|
|
|
|
import org.springframework.web.client.RestTemplate
|
|
|
|
|
import org.springframework.web.multipart.MultipartFile
|
|
|
|
|
|
|
|
|
|
@RestController
|
|
|
|
|
@RequestMapping("/admin/chat/character")
|
|
|
|
|
@PreAuthorize("hasRole('ADMIN')")
|
|
|
|
|
class AdminChatCharacterController(
|
|
|
|
|
private val service: ChatCharacterService,
|
|
|
|
|
private val s3Uploader: S3Uploader,
|
|
|
|
|
|
|
|
|
|
@Value("\${weraser.api-key}")
|
|
|
|
|
private val apiKey: String,
|
|
|
|
|
|
|
|
|
|
@Value("\${weraser.api-url}")
|
|
|
|
|
private val apiUrl: String,
|
|
|
|
|
|
|
|
|
|
@Value("\${cloud.aws.s3.bucket}")
|
|
|
|
|
private val s3Bucket: String
|
|
|
|
|
) {
|
|
|
|
|
@PostMapping("/register")
|
|
|
|
|
@Retryable(
|
|
|
|
|
value = [Exception::class],
|
|
|
|
|
maxAttempts = 3,
|
|
|
|
|
backoff = Backoff(delay = 1000)
|
|
|
|
|
)
|
|
|
|
|
fun registerCharacter(
|
|
|
|
|
@RequestPart("image") image: MultipartFile,
|
|
|
|
|
@RequestPart("request") requestString: String
|
|
|
|
|
) = run {
|
|
|
|
|
// JSON 문자열을 ChatCharacterRegisterRequest 객체로 변환
|
|
|
|
|
val objectMapper = ObjectMapper()
|
|
|
|
|
val request = objectMapper.readValue(requestString, ChatCharacterRegisterRequest::class.java)
|
|
|
|
|
|
|
|
|
|
// 1. 외부 API 호출
|
|
|
|
|
val characterUUID = callExternalApi(request)
|
|
|
|
|
|
|
|
|
|
// 2. ChatCharacter 저장
|
|
|
|
|
val chatCharacter = service.createChatCharacterWithDetails(
|
|
|
|
|
characterUUID = characterUUID,
|
|
|
|
|
name = request.name,
|
|
|
|
|
description = request.description,
|
|
|
|
|
systemPrompt = request.systemPrompt,
|
|
|
|
|
age = request.age?.toIntOrNull(),
|
|
|
|
|
gender = request.gender,
|
|
|
|
|
mbti = request.mbti,
|
|
|
|
|
speechPattern = request.speechPattern,
|
|
|
|
|
speechStyle = request.speechStyle,
|
|
|
|
|
appearance = request.appearance,
|
|
|
|
|
tags = request.tags,
|
|
|
|
|
values = request.values,
|
|
|
|
|
hobbies = request.hobbies,
|
|
|
|
|
goals = request.goals,
|
|
|
|
|
memories = request.memories.map { Triple(it.title, it.content, it.emotion) },
|
|
|
|
|
personalities = request.personalities.map { Pair(it.trait, it.description) },
|
|
|
|
|
backgrounds = request.backgrounds.map { Pair(it.topic, it.description) },
|
|
|
|
|
relationships = request.relationships
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 3. 이미지 저장 및 ChatCharacter에 이미지 path 설정
|
|
|
|
|
val imagePath = saveImage(
|
|
|
|
|
characterId = chatCharacter.id!!,
|
|
|
|
|
image = image
|
|
|
|
|
)
|
|
|
|
|
chatCharacter.imagePath = imagePath
|
|
|
|
|
service.saveChatCharacter(chatCharacter)
|
|
|
|
|
|
|
|
|
|
ApiResponse.ok(null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun callExternalApi(request: ChatCharacterRegisterRequest): String {
|
|
|
|
|
try {
|
|
|
|
|
val factory = SimpleClientHttpRequestFactory()
|
|
|
|
|
factory.setConnectTimeout(20000) // 20초
|
|
|
|
|
factory.setReadTimeout(20000) // 20초
|
|
|
|
|
|
|
|
|
|
val restTemplate = RestTemplate(factory)
|
|
|
|
|
|
|
|
|
|
val headers = HttpHeaders()
|
|
|
|
|
headers.set("x-api-key", apiKey) // 실제 API 키로 대체 필요
|
|
|
|
|
headers.contentType = MediaType.APPLICATION_JSON
|
|
|
|
|
|
|
|
|
|
val httpEntity = HttpEntity(request, headers)
|
|
|
|
|
|
|
|
|
|
val response = restTemplate.exchange(
|
|
|
|
|
"$apiUrl/api/characters",
|
|
|
|
|
HttpMethod.POST,
|
|
|
|
|
httpEntity,
|
|
|
|
|
String::class.java
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 응답 파싱
|
|
|
|
|
val objectMapper = ObjectMapper()
|
|
|
|
|
val apiResponse = objectMapper.readValue(response.body, ExternalApiResponse::class.java)
|
|
|
|
|
|
|
|
|
|
// success가 false이면 throw
|
|
|
|
|
if (!apiResponse.success) {
|
|
|
|
|
throw SodaException(apiResponse.message ?: "등록에 실패했습니다. 다시 시도해 주세요.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// success가 true이면 data.id 반환
|
|
|
|
|
return apiResponse.data?.id ?: throw SodaException("등록에 실패했습니다. 응답에 ID가 없습니다.")
|
|
|
|
|
} catch (_: Exception) {
|
|
|
|
|
throw SodaException("등록에 실패했습니다. 다시 시도해 주세요.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun saveImage(characterId: Long, image: MultipartFile): String {
|
|
|
|
|
try {
|
|
|
|
|
val metadata = ObjectMetadata()
|
|
|
|
|
metadata.contentLength = image.size
|
|
|
|
|
|
|
|
|
|
// S3에 이미지 업로드
|
|
|
|
|
return s3Uploader.upload(
|
|
|
|
|
inputStream = image.inputStream,
|
|
|
|
|
bucket = s3Bucket,
|
|
|
|
|
filePath = "characters/$characterId/${generateFileName(prefix = "character")}",
|
|
|
|
|
metadata = metadata
|
|
|
|
|
)
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
throw SodaException("이미지 저장에 실패했습니다: ${e.message}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|