From 618f80fddca1be3c1cf02da55e517f0a90067902 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 7 Aug 2025 11:57:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. isActive가 true인 캐릭터만 조회하는 기능 구현 2. 페이징 처리 구현 (기본 20개 조회) 3. 필요한 데이터 포함 (id, 캐릭터명, 프로필 이미지, 설명, 성별, 나이, MBTI, 태그, 성격, 말투, 등록일, 수정일) --- .../character/AdminChatCharacterController.kt | 27 +++++++- .../dto/ChatCharacterListResponse.kt | 62 +++++++++++++++++++ .../service/AdminChatCharacterService.kt | 46 ++++++++++++++ .../repository/ChatCharacterRepository.kt | 3 + 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/service/AdminChatCharacterService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt index 806d665..7f5bf84 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt @@ -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], diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterListResponse.kt new file mode 100644 index 0000000..bf0977c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterListResponse.kt @@ -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, + 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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/service/AdminChatCharacterService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/service/AdminChatCharacterService.kt new file mode 100644 index 0000000..d9cd176 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/service/AdminChatCharacterService.kt @@ -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")) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt index f9547dd..4d39bcb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt @@ -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 { fun findByCharacterUUID(characterUUID: String): ChatCharacter? fun findByName(name: String): ChatCharacter? + fun findByIsActiveTrue(pageable: Pageable): Page }