git commit -m "feat(chat): 캐릭터 등록 API 구현

- 외부 API 호출 및 응답 처리 구현
- 이미지 파일 S3 업로드 기능 추가
- Multipart 요청 처리 지원"
This commit is contained in:
Klaus 2025-08-06 20:51:01 +09:00
parent 3b42399726
commit de6642b675
3 changed files with 199 additions and 0 deletions

View File

@ -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}")
}
}
}

View File

@ -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
)

View File

@ -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}