commit
1a3a9149a2
|
@ -145,11 +145,17 @@ class AdminOriginalWorkService(
|
||||||
/** 단일 캐릭터를 지정 원작에 배정 */
|
/** 단일 캐릭터를 지정 원작에 배정 */
|
||||||
@Transactional
|
@Transactional
|
||||||
fun assignOneCharacter(originalWorkId: Long, characterId: Long) {
|
fun assignOneCharacter(originalWorkId: Long, characterId: Long) {
|
||||||
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
|
|
||||||
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
|
|
||||||
val character = chatCharacterRepository.findById(characterId)
|
val character = chatCharacterRepository.findById(characterId)
|
||||||
.orElseThrow { SodaException("해당 캐릭터를 찾을 수 없습니다") }
|
.orElseThrow { SodaException("해당 캐릭터를 찾을 수 없습니다") }
|
||||||
character.originalWork = ow
|
|
||||||
|
if (originalWorkId == 0L) {
|
||||||
|
character.originalWork = null
|
||||||
|
} else {
|
||||||
|
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
|
||||||
|
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
|
||||||
|
character.originalWork = ow
|
||||||
|
}
|
||||||
|
|
||||||
chatCharacterRepository.save(character)
|
chatCharacterRepository.save(character)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,13 @@ interface OriginalWorkRepository : JpaRepository<OriginalWork, Long> {
|
||||||
): List<OriginalWork>
|
): List<OriginalWork>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 앱용 원작 목록 조회
|
* 앱용 원작 목록 조회 (페이징)
|
||||||
* - 소프트 삭제 제외
|
* - 소프트 삭제 제외
|
||||||
* - includeAdult=false이면 19금 제외
|
* - includeAdult=false이면 19금 제외
|
||||||
* - 활성 캐릭터가 하나라도 연결된 원작만 조회
|
* - 활성 캐릭터가 하나라도 연결된 원작만 조회
|
||||||
*/
|
*/
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
value = """
|
||||||
SELECT ow FROM OriginalWork ow
|
SELECT ow FROM OriginalWork ow
|
||||||
WHERE ow.isDeleted = false
|
WHERE ow.isDeleted = false
|
||||||
AND (:includeAdult = true OR ow.isAdult = false)
|
AND (:includeAdult = true OR ow.isAdult = false)
|
||||||
|
@ -48,7 +48,16 @@ interface OriginalWorkRepository : JpaRepository<OriginalWork, Long> {
|
||||||
WHERE c.originalWork = ow AND c.isActive = true
|
WHERE c.originalWork = ow AND c.isActive = true
|
||||||
)
|
)
|
||||||
ORDER BY ow.createdAt DESC
|
ORDER BY ow.createdAt DESC
|
||||||
|
""",
|
||||||
|
countQuery = """
|
||||||
|
SELECT COUNT(ow) FROM OriginalWork ow
|
||||||
|
WHERE ow.isDeleted = false
|
||||||
|
AND (:includeAdult = true OR ow.isAdult = false)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM ChatCharacter c
|
||||||
|
WHERE c.originalWork = ow AND c.isActive = true
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun findAllForApp(@Param("includeAdult") includeAdult: Boolean): List<OriginalWork>
|
fun findAllForAppPage(@Param("includeAdult") includeAdult: Boolean, pageable: Pageable): Page<OriginalWork>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,20 +31,23 @@ class OriginalWorkController(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 원작 목록
|
* 원작 목록 (페이징)
|
||||||
* - 로그인 불필요
|
* - 로그인 불필요
|
||||||
* - 본인인증하지 않은 경우 19금 제외
|
* - 본인인증하지 않은 경우 19금 제외
|
||||||
* - 활성 캐릭터가 하나라도 연결된 원작만 노출
|
* - 활성 캐릭터가 하나라도 연결된 원작만 노출
|
||||||
|
* - 요청: page(기본 0), size(기본 20)
|
||||||
* - 반환: totalCount + [imageUrl, title, contentType]
|
* - 반환: totalCount + [imageUrl, title, contentType]
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
fun list(
|
fun list(
|
||||||
|
@RequestParam(defaultValue = "0") page: Int,
|
||||||
|
@RequestParam(defaultValue = "20") size: Int,
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = run {
|
) = run {
|
||||||
val includeAdult = member?.auth != null
|
val includeAdult = member?.auth != null
|
||||||
val list = queryService.listForApp(includeAdult)
|
val pageRes = queryService.listForAppPage(includeAdult, page, size)
|
||||||
val content = list.map { OriginalWorkListItemResponse.from(it, imageHost) }
|
val content = pageRes.content.map { OriginalWorkListItemResponse.from(it, imageHost) }
|
||||||
ApiResponse.ok(OriginalWorkListResponse(totalCount = content.size.toLong(), content = content))
|
ApiResponse.ok(OriginalWorkListResponse(totalCount = pageRes.totalElements, content = content))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,6 +55,7 @@ class OriginalWorkController(
|
||||||
* - 로그인 및 본인인증 필수
|
* - 로그인 및 본인인증 필수
|
||||||
* - 반환: 이미지, 제목, 콘텐츠 타입, 카테고리, 19금 여부, 작품 소개, 원작 링크
|
* - 반환: 이미지, 제목, 콘텐츠 타입, 카테고리, 19금 여부, 작품 소개, 원작 링크
|
||||||
* - 해당 원작의 활성 캐릭터 리스트 [imageUrl, name, description]
|
* - 해당 원작의 활성 캐릭터 리스트 [imageUrl, name, description]
|
||||||
|
* - 캐릭터는 페이징 적용: 첫 페이지 20개
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
fun detail(
|
fun detail(
|
||||||
|
@ -60,7 +66,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 +79,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
|
||||||
|
|
||||||
|
@ -18,12 +21,21 @@ class OriginalWorkQueryService(
|
||||||
private val chatCharacterRepository: ChatCharacterRepository
|
private val chatCharacterRepository: ChatCharacterRepository
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 앱용 원작 목록 조회
|
* 앱용 원작 목록 조회 (페이징)
|
||||||
* @param includeAdult true면 19금 포함, false면 제외
|
* @param includeAdult true면 19금 포함, false면 제외
|
||||||
|
* @param page 페이지 번호(0부터)
|
||||||
|
* @param size 페이지 크기(기본 20, 최대 50)
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun listForApp(includeAdult: Boolean): List<OriginalWork> {
|
fun listForAppPage(includeAdult: Boolean, page: Int = 0, size: Int = 20): Page<OriginalWork> {
|
||||||
return originalWorkRepository.findAllForApp(includeAdult)
|
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 originalWorkRepository.findAllForAppPage(includeAdult, pageable)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,10 +48,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue