feat(original-app): 원작 상세, 캐릭터 리스트

- 원작 상세에 캐릭터 20개 조회
- 지정 원작에 속한 활성 캐릭터 목록 조회 API 추가
This commit is contained in:
Klaus 2025-09-15 15:32:20 +09:00
parent adcaa0a5fd
commit 387f5388d9
3 changed files with 63 additions and 4 deletions

View File

@ -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
)
)
}
}

View File

@ -75,3 +75,11 @@ data class OriginalWorkDetailResponse(
}
}
}
/**
* 앱용: 원작별 활성 캐릭터 페이징 응답 DTO
*/
data class OriginalWorkCharactersPageResponse(
@JsonProperty("totalCount") val totalCount: Long,
@JsonProperty("content") val content: List<Character>
)

View File

@ -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<ChatCharacter> {
return chatCharacterRepository.findByOriginalWorkIdAndIsActiveTrueOrderByCreatedAtDesc(originalWorkId)
fun getActiveCharactersPage(originalWorkId: Long, page: Int = 0, size: Int = 20): Page<ChatCharacter> {
// 원작 존재 및 소프트 삭제 여부 확인
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)
}
}