캐릭터 챗봇 #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.ChatCharacterRegisterRequest | ||||||
| import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest | 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.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.aws.s3.S3Uploader | ||||||
| import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService | import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService | ||||||
| import kr.co.vividnext.sodalive.common.ApiResponse | 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.Backoff | ||||||
| import org.springframework.retry.annotation.Retryable | import org.springframework.retry.annotation.Retryable | ||||||
| import org.springframework.security.access.prepost.PreAuthorize | 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.PostMapping | ||||||
| import org.springframework.web.bind.annotation.PutMapping | import org.springframework.web.bind.annotation.PutMapping | ||||||
| import org.springframework.web.bind.annotation.RequestMapping | 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.RequestPart | ||||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||||
| import org.springframework.web.client.RestTemplate | import org.springframework.web.client.RestTemplate | ||||||
| @@ -32,6 +35,7 @@ import org.springframework.web.multipart.MultipartFile | |||||||
| @PreAuthorize("hasRole('ADMIN')") | @PreAuthorize("hasRole('ADMIN')") | ||||||
| class AdminChatCharacterController( | class AdminChatCharacterController( | ||||||
|     private val service: ChatCharacterService, |     private val service: ChatCharacterService, | ||||||
|  |     private val adminService: AdminChatCharacterService, | ||||||
|     private val s3Uploader: S3Uploader, |     private val s3Uploader: S3Uploader, | ||||||
|  |  | ||||||
|     @Value("\${weraser.api-key}") |     @Value("\${weraser.api-key}") | ||||||
| @@ -41,8 +45,29 @@ class AdminChatCharacterController( | |||||||
|     private val apiUrl: String, |     private val apiUrl: String, | ||||||
|  |  | ||||||
|     @Value("\${cloud.aws.s3.bucket}") |     @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") |     @PostMapping("/register") | ||||||
|     @Retryable( |     @Retryable( | ||||||
|         value = [Exception::class], |         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 | package kr.co.vividnext.sodalive.chat.character.repository | ||||||
|  |  | ||||||
| import kr.co.vividnext.sodalive.chat.character.ChatCharacter | 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.data.jpa.repository.JpaRepository | ||||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||||
|  |  | ||||||
| @@ -8,4 +10,5 @@ import org.springframework.stereotype.Repository | |||||||
| interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> { | interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> { | ||||||
|     fun findByCharacterUUID(characterUUID: String): ChatCharacter? |     fun findByCharacterUUID(characterUUID: String): ChatCharacter? | ||||||
|     fun findByName(name: String): ChatCharacter? |     fun findByName(name: String): ChatCharacter? | ||||||
|  |     fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacter> | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user