feat(original-app): 원작 상세, 캐릭터 리스트
- 원작 상세에 캐릭터 20개 조회 - 지정 원작에 속한 활성 캐릭터 목록 조회 API 추가
This commit is contained in:
		| @@ -1,6 +1,7 @@ | |||||||
| package kr.co.vividnext.sodalive.chat.original.controller | package kr.co.vividnext.sodalive.chat.original.controller | ||||||
|  |  | ||||||
| import kr.co.vividnext.sodalive.chat.character.dto.Character | 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.OriginalWorkDetailResponse | ||||||
| import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListItemResponse | import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListItemResponse | ||||||
| import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListResponse | 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.GetMapping | ||||||
| import org.springframework.web.bind.annotation.PathVariable | import org.springframework.web.bind.annotation.PathVariable | ||||||
| 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.RestController | import org.springframework.web.bind.annotation.RestController | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -50,6 +52,7 @@ class OriginalWorkController( | |||||||
|      * - 로그인 및 본인인증 필수 |      * - 로그인 및 본인인증 필수 | ||||||
|      * - 반환: 이미지, 제목, 콘텐츠 타입, 카테고리, 19금 여부, 작품 소개, 원작 링크 |      * - 반환: 이미지, 제목, 콘텐츠 타입, 카테고리, 19금 여부, 작품 소개, 원작 링크 | ||||||
|      * - 해당 원작의 활성 캐릭터 리스트 [imageUrl, name, description] |      * - 해당 원작의 활성 캐릭터 리스트 [imageUrl, name, description] | ||||||
|  |      * - 캐릭터는 페이징 적용: 첫 페이지 20개 | ||||||
|      */ |      */ | ||||||
|     @GetMapping("/{id}") |     @GetMapping("/{id}") | ||||||
|     fun detail( |     fun detail( | ||||||
| @@ -60,7 +63,8 @@ class OriginalWorkController( | |||||||
|         if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") |         if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") | ||||||
|  |  | ||||||
|         val ow = queryService.getOriginalWork(id) |         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" |             val path = it.imagePath ?: "profile/default-profile.png" | ||||||
|             Character( |             Character( | ||||||
|                 characterId = it.id!!, |                 characterId = it.id!!, | ||||||
| @@ -72,4 +76,37 @@ class OriginalWorkController( | |||||||
|         val response = OriginalWorkDetailResponse.from(ow, imageHost, characters) |         val response = OriginalWorkDetailResponse.from(ow, imageHost, characters) | ||||||
|         ApiResponse.ok(response) |         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 | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -75,3 +75,11 @@ data class OriginalWorkDetailResponse( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 앱용: 원작별 활성 캐릭터 페이징 응답 DTO | ||||||
|  |  */ | ||||||
|  | data class OriginalWorkCharactersPageResponse( | ||||||
|  |     @JsonProperty("totalCount") val totalCount: Long, | ||||||
|  |     @JsonProperty("content") val content: List<Character> | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -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.OriginalWork | ||||||
| import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository | import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository | ||||||
| import kr.co.vividnext.sodalive.common.SodaException | 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.stereotype.Service | ||||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||||
|  |  | ||||||
| @@ -36,10 +39,21 @@ class OriginalWorkQueryService( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 지정 원작에 속한 활성 캐릭터 목록 조회 (최신순) |      * 지정 원작에 속한 활성 캐릭터 페이징 조회 (최신순) | ||||||
|      */ |      */ | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     fun getActiveCharacters(originalWorkId: Long): List<ChatCharacter> { |     fun getActiveCharactersPage(originalWorkId: Long, page: Int = 0, size: Int = 20): Page<ChatCharacter> { | ||||||
|         return chatCharacterRepository.findByOriginalWorkIdAndIsActiveTrueOrderByCreatedAtDesc(originalWorkId) |         // 원작 존재 및 소프트 삭제 여부 확인 | ||||||
|  |         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) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user