git commit -m "feat(chat): 캐릭터 등록 API 구현
- 외부 API 호출 및 응답 처리 구현 - 이미지 파일 S3 업로드 기능 추가 - Multipart 요청 처리 지원"
This commit is contained in:
parent
3b42399726
commit
de6642b675
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.character.dto
|
||||
|
||||
data class ChatCharacterPersonalityRequest(
|
||||
val trait: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
data class ChatCharacterBackgroundRequest(
|
||||
val topic: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
data class ChatCharacterMemoryRequest(
|
||||
val title: String,
|
||||
val content: String,
|
||||
val emotion: String
|
||||
)
|
||||
|
||||
data class ChatCharacterRegisterRequest(
|
||||
val name: String,
|
||||
val systemPrompt: String,
|
||||
val description: String,
|
||||
val age: String?,
|
||||
val gender: String?,
|
||||
val mbti: String?,
|
||||
val speechPattern: String?,
|
||||
val speechStyle: String?,
|
||||
val appearance: String?,
|
||||
val isActive: Boolean = true,
|
||||
val tags: List<String> = emptyList(),
|
||||
val hobbies: List<String> = emptyList(),
|
||||
val values: List<String> = emptyList(),
|
||||
val goals: List<String> = emptyList(),
|
||||
val relationships: List<String> = emptyList(),
|
||||
val personalities: List<ChatCharacterPersonalityRequest> = emptyList(),
|
||||
val backgrounds: List<ChatCharacterBackgroundRequest> = emptyList(),
|
||||
val memories: List<ChatCharacterMemoryRequest> = emptyList()
|
||||
)
|
||||
|
||||
data class ExternalApiResponse(
|
||||
val success: Boolean,
|
||||
val data: ExternalApiData? = null,
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
data class ExternalApiData(
|
||||
val id: String
|
||||
)
|
|
@ -8,6 +8,10 @@ logging:
|
|||
util:
|
||||
EC2MetadataUtils: error
|
||||
|
||||
weraser:
|
||||
apiUrl: {$WERASER_API_URL}
|
||||
apiKey: {$WERASER_API_KEY}
|
||||
|
||||
bootpay:
|
||||
applicationId: ${BOOTPAY_APPLICATION_ID}
|
||||
privateKey: ${BOOTPAY_PRIVATE_KEY}
|
||||
|
|
Loading…
Reference in New Issue