feat(banner): 정렬 순서 추가

This commit is contained in:
Klaus 2025-08-07 15:31:03 +09:00
parent 81f972edc1
commit ef8458c7a3
5 changed files with 80 additions and 8 deletions

View File

@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerListPageRespon
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerRegisterRequest
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerResponse
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerUpdateRequest
import kr.co.vividnext.sodalive.admin.chat.dto.UpdateBannerOrdersRequest
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
import kr.co.vividnext.sodalive.common.ApiResponse
@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestPart
@ -98,7 +100,7 @@ class AdminChatBannerController(
* 배너 등록 API
*
* @param image 배너 이미지
* @param request 배너 등록 요청 정보
* @param request 배너 등록 요청 정보 (캐릭터 ID와 선택적으로 정렬 순서 포함)
* @return 등록된 배너 정보
*/
@PostMapping("/register")
@ -106,8 +108,11 @@ class AdminChatBannerController(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") request: ChatCharacterBannerRegisterRequest
) = run {
// 1. 먼저 빈 이미지 경로로 배너 등록
val banner = bannerService.registerBanner(request.characterId, "")
// 1. 먼저 빈 이미지 경로로 배너 등록 (정렬 순서 포함)
val banner = bannerService.registerBanner(
characterId = request.characterId,
imagePath = ""
)
// 2. 배너 ID를 사용하여 이미지 업로드
val imagePath = saveImage(banner.id!!, image)
@ -188,4 +193,20 @@ class AdminChatBannerController(
ApiResponse.ok("배너가 성공적으로 삭제되었습니다.")
}
/**
* 배너 정렬 순서 일괄 변경 API
* ID 목록의 순서대로 정렬 순서를 1부터 순차적으로 설정합니다.
*
* @param request 정렬 순서 일괄 변경 요청 정보 (배너 ID 목록)
* @return 성공 메시지
*/
@PutMapping("/orders")
fun updateBannerOrders(
@RequestBody request: UpdateBannerOrdersRequest
) = run {
bannerService.updateBannerOrders(request.ids)
ApiResponse.ok(null, "배너 정렬 순서가 성공적으로 변경되었습니다.")
}
}

View File

@ -18,3 +18,11 @@ data class ChatCharacterBannerUpdateRequest(
// 캐릭터 ID (변경할 캐릭터)
val characterId: Long? = null
)
/**
* 캐릭터 배너 정렬 순서 일괄 변경 요청 DTO
*/
data class UpdateBannerOrdersRequest(
// 배너 ID 목록 (순서대로 정렬됨)
val ids: List<Long>
)

View File

@ -9,6 +9,7 @@ import javax.persistence.ManyToOne
/**
* 캐릭터 배너 엔티티
* 이미지와 캐릭터 ID를 가지며, 소프트 삭제(isActive = false) 지원합니다.
* 정렬 순서(sortOrder) 통해 배너의 표시 순서를 결정합니다.
*/
@Entity
class ChatCharacterBanner(
@ -20,6 +21,9 @@ class ChatCharacterBanner(
@JoinColumn(name = "character_id")
var chatCharacter: ChatCharacter,
// 정렬 순서 (낮을수록 먼저 표시)
var sortOrder: Int = 0,
// 활성화 여부 (소프트 삭제용)
var isActive: Boolean = true
) : BaseEntity()

View File

@ -4,10 +4,15 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
@Repository
interface ChatCharacterBannerRepository : JpaRepository<ChatCharacterBanner, Long> {
// 활성화된 배너 목록 조회
fun findByActiveTrue(pageable: Pageable): Page<ChatCharacterBanner>
// 활성화된 배너 목록 조회 (정렬 순서대로)
fun findByActiveTrueOrderBySortOrderAsc(pageable: Pageable): Page<ChatCharacterBanner>
// 활성화된 배너 중 최대 정렬 순서 값 조회
@Query("SELECT MAX(b.sortOrder) FROM ChatCharacterBanner b WHERE b.isActive = true")
fun findMaxSortOrder(): Int?
}

View File

@ -15,10 +15,10 @@ class ChatCharacterBannerService(
private val characterRepository: ChatCharacterRepository
) {
/**
* 활성화된 모든 배너 조회
* 활성화된 모든 배너 조회 (정렬 순서대로)
*/
fun getActiveBanners(pageable: Pageable): Page<ChatCharacterBanner> {
return bannerRepository.findByActiveTrue(pageable)
return bannerRepository.findByActiveTrueOrderBySortOrderAsc(pageable)
}
/**
@ -31,6 +31,10 @@ class ChatCharacterBannerService(
/**
* 배너 등록
*
* @param characterId 캐릭터 ID
* @param imagePath 이미지 경로
* @return 등록된 배너
*/
@Transactional
fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner {
@ -41,9 +45,13 @@ class ChatCharacterBannerService(
throw SodaException("비활성화된 캐릭터에는 배너를 등록할 수 없습니다: $characterId")
}
// 정렬 순서가 지정되지 않은 경우 가장 마지막 순서로 설정
val finalSortOrder = (bannerRepository.findMaxSortOrder() ?: 0) + 1
val banner = ChatCharacterBanner(
imagePath = imagePath,
chatCharacter = character
chatCharacter = character,
sortOrder = finalSortOrder
)
return bannerRepository.save(banner)
@ -97,4 +105,30 @@ class ChatCharacterBannerService(
banner.isActive = false
bannerRepository.save(banner)
}
/**
* 배너 정렬 순서 일괄 변경
* ID 목록의 순서대로 정렬 순서를 1부터 순차적으로 설정합니다.
*
* @param ids 배너 ID 목록 (순서대로 정렬됨)
* @return 수정된 배너 목록
*/
@Transactional
fun updateBannerOrders(ids: List<Long>): List<ChatCharacterBanner> {
val updatedBanners = mutableListOf<ChatCharacterBanner>()
for (index in ids.indices) {
val banner = bannerRepository.findById(ids[index])
.orElseThrow { SodaException("배너를 찾을 수 없습니다: ${ids[index]}") }
if (!banner.isActive) {
throw SodaException("비활성화된 배너는 수정할 수 없습니다: ${ids[index]}")
}
banner.sortOrder = index + 1
updatedBanners.add(bannerRepository.save(banner))
}
return updatedBanners
}
}