diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt new file mode 100644 index 0000000..bea7f13 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt @@ -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}") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt new file mode 100644 index 0000000..299bdfd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt @@ -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 = emptyList(), + val hobbies: List = emptyList(), + val values: List = emptyList(), + val goals: List = emptyList(), + val relationships: List = emptyList(), + val personalities: List = emptyList(), + val backgrounds: List = emptyList(), + val memories: List = emptyList() +) + +data class ExternalApiResponse( + val success: Boolean, + val data: ExternalApiData? = null, + val message: String? = null +) + +data class ExternalApiData( + val id: String +) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fbc0fd4..0c81b0a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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}