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