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