캐릭터 챗봇 #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>
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue