feat(admin): 관리자 페이지 캐릭터 리스트 API 구현
1. isActive가 true인 캐릭터만 조회하는 기능 구현 2. 페이징 처리 구현 (기본 20개 조회) 3. 필요한 데이터 포함 (id, 캐릭터명, 프로필 이미지, 설명, 성별, 나이, MBTI, 태그, 성격, 말투, 등록일, 수정일)
This commit is contained in:
parent
45b6c8db96
commit
618f80fddc
|
@ -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