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:
|
util:
|
||||||
EC2MetadataUtils: error
|
EC2MetadataUtils: error
|
||||||
|
|
||||||
|
weraser:
|
||||||
|
apiUrl: {$WERASER_API_URL}
|
||||||
|
apiKey: {$WERASER_API_KEY}
|
||||||
|
|
||||||
bootpay:
|
bootpay:
|
||||||
applicationId: ${BOOTPAY_APPLICATION_ID}
|
applicationId: ${BOOTPAY_APPLICATION_ID}
|
||||||
privateKey: ${BOOTPAY_PRIVATE_KEY}
|
privateKey: ${BOOTPAY_PRIVATE_KEY}
|
||||||
|
|
Loading…
Reference in New Issue