캐릭터 챗봇 #338
| @@ -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} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user