캐릭터 챗봇 #338
| @@ -5,6 +5,7 @@ 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.ChatCharacterUpdateRequest | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ExternalApiResponse | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService | ||||
| import kr.co.vividnext.sodalive.aws.s3.S3Uploader | ||||
| import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| @@ -19,9 +20,11 @@ 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.GetMapping | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| import org.springframework.web.bind.annotation.PutMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RequestPart | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
| import org.springframework.web.client.RestTemplate | ||||
| @@ -32,6 +35,7 @@ import org.springframework.web.multipart.MultipartFile | ||||
| @PreAuthorize("hasRole('ADMIN')") | ||||
| class AdminChatCharacterController( | ||||
|     private val service: ChatCharacterService, | ||||
|     private val adminService: AdminChatCharacterService, | ||||
|     private val s3Uploader: S3Uploader, | ||||
|  | ||||
|     @Value("\${weraser.api-key}") | ||||
| @@ -41,8 +45,29 @@ class AdminChatCharacterController( | ||||
|     private val apiUrl: String, | ||||
|  | ||||
|     @Value("\${cloud.aws.s3.bucket}") | ||||
|     private val s3Bucket: String | ||||
|     private val s3Bucket: String, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
| ) { | ||||
|     /** | ||||
|      * 활성화된 캐릭터 목록 조회 API | ||||
|      * | ||||
|      * @param page 페이지 번호 (0부터 시작, 기본값 0) | ||||
|      * @param size 페이지 크기 (기본값 20) | ||||
|      * @return 페이징된 캐릭터 목록 | ||||
|      */ | ||||
|     @GetMapping("/list") | ||||
|     fun getCharacterList( | ||||
|         @RequestParam(defaultValue = "0") page: Int, | ||||
|         @RequestParam(defaultValue = "20") size: Int | ||||
|     ) = run { | ||||
|         val pageable = adminService.createDefaultPageRequest(page, size) | ||||
|         val response = adminService.getActiveChatCharacters(pageable, imageHost) | ||||
|  | ||||
|         ApiResponse.ok(response) | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/register") | ||||
|     @Retryable( | ||||
|         value = [Exception::class], | ||||
|   | ||||
| @@ -0,0 +1,62 @@ | ||||
| package kr.co.vividnext.sodalive.admin.chat.character.dto | ||||
|  | ||||
| import kr.co.vividnext.sodalive.chat.character.ChatCharacter | ||||
| import java.time.ZoneId | ||||
| import java.time.format.DateTimeFormatter | ||||
|  | ||||
| data class ChatCharacterListResponse( | ||||
|     val id: Long, | ||||
|     val name: String, | ||||
|     val imageUrl: String?, | ||||
|     val description: String, | ||||
|     val gender: String?, | ||||
|     val age: Int?, | ||||
|     val mbti: String?, | ||||
|     val speechStyle: String?, | ||||
|     val speechPattern: String?, | ||||
|     val tags: List<String>, | ||||
|     val createdAt: String?, | ||||
|     val updatedAt: String? | ||||
| ) { | ||||
|     companion object { | ||||
|         private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") | ||||
|         private val seoulZoneId = ZoneId.of("Asia/Seoul") | ||||
|  | ||||
|         fun from(chatCharacter: ChatCharacter, imageHost: String = ""): ChatCharacterListResponse { | ||||
|             val fullImagePath = if (chatCharacter.imagePath != null && imageHost.isNotEmpty()) { | ||||
|                 "$imageHost/${chatCharacter.imagePath}" | ||||
|             } else { | ||||
|                 chatCharacter.imagePath | ||||
|             } | ||||
|  | ||||
|             // UTC에서 Asia/Seoul로 시간대 변환 및 문자열 포맷팅 | ||||
|             val createdAtStr = chatCharacter.createdAt?.atZone(ZoneId.of("UTC")) | ||||
|                 ?.withZoneSameInstant(seoulZoneId) | ||||
|                 ?.format(formatter) | ||||
|  | ||||
|             val updatedAtStr = chatCharacter.updatedAt?.atZone(ZoneId.of("UTC")) | ||||
|                 ?.withZoneSameInstant(seoulZoneId) | ||||
|                 ?.format(formatter) | ||||
|  | ||||
|             return ChatCharacterListResponse( | ||||
|                 id = chatCharacter.id!!, | ||||
|                 name = chatCharacter.name, | ||||
|                 imageUrl = fullImagePath, | ||||
|                 description = chatCharacter.description, | ||||
|                 gender = chatCharacter.gender, | ||||
|                 age = chatCharacter.age, | ||||
|                 mbti = chatCharacter.mbti, | ||||
|                 speechStyle = chatCharacter.speechStyle, | ||||
|                 speechPattern = chatCharacter.speechPattern, | ||||
|                 tags = chatCharacter.tagMappings.map { it.tag.tag }, | ||||
|                 createdAt = createdAtStr, | ||||
|                 updatedAt = updatedAtStr | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| data class ChatCharacterListPageResponse( | ||||
|     val totalCount: Long, | ||||
|     val content: List<ChatCharacterListResponse> | ||||
| ) | ||||
| @@ -0,0 +1,46 @@ | ||||
| package kr.co.vividnext.sodalive.admin.chat.character.service | ||||
|  | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListPageResponse | ||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListResponse | ||||
| import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository | ||||
| import org.springframework.data.domain.PageRequest | ||||
| import org.springframework.data.domain.Pageable | ||||
| import org.springframework.data.domain.Sort | ||||
| import org.springframework.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | ||||
|  | ||||
| @Service | ||||
| class AdminChatCharacterService( | ||||
|     private val chatCharacterRepository: ChatCharacterRepository | ||||
| ) { | ||||
|     /** | ||||
|      * 활성화된 캐릭터 목록을 페이징하여 조회 | ||||
|      * | ||||
|      * @param pageable 페이징 정보 | ||||
|      * @return 페이징된 캐릭터 목록 | ||||
|      */ | ||||
|     @Transactional(readOnly = true) | ||||
|     fun getActiveChatCharacters(pageable: Pageable, imageHost: String = ""): ChatCharacterListPageResponse { | ||||
|         // isActive가 true인 캐릭터만 조회 | ||||
|         val page = chatCharacterRepository.findByIsActiveTrue(pageable) | ||||
|  | ||||
|         // 페이지 정보 생성 | ||||
|         val content = page.content.map { ChatCharacterListResponse.from(it, imageHost) } | ||||
|  | ||||
|         return ChatCharacterListPageResponse( | ||||
|             totalCount = page.totalElements, | ||||
|             content = content | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 기본 페이지 요청 생성 | ||||
|      * | ||||
|      * @param page 페이지 번호 (0부터 시작) | ||||
|      * @param size 페이지 크기 | ||||
|      * @return 페이지 요청 객체 | ||||
|      */ | ||||
|     fun createDefaultPageRequest(page: Int = 0, size: Int = 20): PageRequest { | ||||
|         return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")) | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,8 @@ | ||||
| package kr.co.vividnext.sodalive.chat.character.repository | ||||
|  | ||||
| import kr.co.vividnext.sodalive.chat.character.ChatCharacter | ||||
| import org.springframework.data.domain.Page | ||||
| import org.springframework.data.domain.Pageable | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
| import org.springframework.stereotype.Repository | ||||
|  | ||||
| @@ -8,4 +10,5 @@ import org.springframework.stereotype.Repository | ||||
| interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> { | ||||
|     fun findByCharacterUUID(characterUUID: String): ChatCharacter? | ||||
|     fun findByName(name: String): ChatCharacter? | ||||
|     fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacter> | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user