From 387f5388d97e5011da7680af06a2adee7de9a998 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 15 Sep 2025 15:32:20 +0900 Subject: [PATCH] =?UTF-8?q?feat(original-app):=20=EC=9B=90=EC=9E=91=20?= =?UTF-8?q?=EC=83=81=EC=84=B8,=20=EC=BA=90=EB=A6=AD=ED=84=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원작 상세에 캐릭터 20개 조회 - 지정 원작에 속한 활성 캐릭터 목록 조회 API 추가 --- .../controller/OriginalWorkController.kt | 39 ++++++++++++++++++- .../chat/original/dto/OriginalWorkAppDtos.kt | 8 ++++ .../service/OriginalWorkQueryService.kt | 20 ++++++++-- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt index 4b7dd4b..e8b6301 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.chat.original.controller import kr.co.vividnext.sodalive.chat.character.dto.Character +import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkCharactersPageResponse import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkDetailResponse import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListItemResponse import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListResponse @@ -13,6 +14,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController /** @@ -50,6 +52,7 @@ class OriginalWorkController( * - 로그인 및 본인인증 필수 * - 반환: 이미지, 제목, 콘텐츠 타입, 카테고리, 19금 여부, 작품 소개, 원작 링크 * - 해당 원작의 활성 캐릭터 리스트 [imageUrl, name, description] + * - 캐릭터는 페이징 적용: 첫 페이지 20개 */ @GetMapping("/{id}") fun detail( @@ -60,7 +63,8 @@ class OriginalWorkController( if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") val ow = queryService.getOriginalWork(id) - val characters = queryService.getActiveCharacters(id).map { + val pageRes = queryService.getActiveCharactersPage(id, page = 0, size = 20) + val characters = pageRes.content.map { val path = it.imagePath ?: "profile/default-profile.png" Character( characterId = it.id!!, @@ -72,4 +76,37 @@ class OriginalWorkController( val response = OriginalWorkDetailResponse.from(ow, imageHost, characters) ApiResponse.ok(response) } + + /** + * 지정 원작에 속한 활성 캐릭터 목록 조회 (페이징) + * - 로그인 및 본인인증 필수 + * - 기본 페이지 사이즈 20 + */ + @GetMapping("/{id}/characters") + fun listCharacters( + @PathVariable id: Long, + @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "20") size: Int, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + + val pageRes = queryService.getActiveCharactersPage(id, page, size) + val content = pageRes.content.map { + val path = it.imagePath ?: "profile/default-profile.png" + Character( + characterId = it.id!!, + name = it.name, + description = it.description, + imageUrl = "$imageHost/$path" + ) + } + ApiResponse.ok( + OriginalWorkCharactersPageResponse( + totalCount = pageRes.totalElements, + content = content + ) + ) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/dto/OriginalWorkAppDtos.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/dto/OriginalWorkAppDtos.kt index 9c38622..7520eca 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/dto/OriginalWorkAppDtos.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/dto/OriginalWorkAppDtos.kt @@ -75,3 +75,11 @@ data class OriginalWorkDetailResponse( } } } + +/** + * 앱용: 원작별 활성 캐릭터 페이징 응답 DTO + */ +data class OriginalWorkCharactersPageResponse( + @JsonProperty("totalCount") val totalCount: Long, + @JsonProperty("content") val content: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt index 1377dab..01dc3e6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt @@ -5,6 +5,9 @@ import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepositor import kr.co.vividnext.sodalive.chat.original.OriginalWork import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository import kr.co.vividnext.sodalive.common.SodaException +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -36,10 +39,21 @@ class OriginalWorkQueryService( } /** - * 지정 원작에 속한 활성 캐릭터 목록 조회 (최신순) + * 지정 원작에 속한 활성 캐릭터 페이징 조회 (최신순) */ @Transactional(readOnly = true) - fun getActiveCharacters(originalWorkId: Long): List { - return chatCharacterRepository.findByOriginalWorkIdAndIsActiveTrueOrderByCreatedAtDesc(originalWorkId) + fun getActiveCharactersPage(originalWorkId: Long, page: Int = 0, size: Int = 20): Page { + // 원작 존재 및 소프트 삭제 여부 확인 + originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) + .orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } + + val safePage = if (page < 0) 0 else page + val safeSize = when { + size <= 0 -> 20 + size > 50 -> 50 + else -> size + } + val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending()) + return chatCharacterRepository.findByOriginalWorkIdAndIsActiveTrue(originalWorkId, pageable) } }