From ef8458c7a32abce7d802607b3d0ad001241b5419 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 7 Aug 2025 15:31:03 +0900 Subject: [PATCH] =?UTF-8?q?feat(banner):=20=EC=A0=95=EB=A0=AC=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/chat/AdminChatBannerController.kt | 27 +++++++++++-- .../chat/dto/ChatCharacterBannerRequest.kt | 8 ++++ .../chat/character/ChatCharacterBanner.kt | 4 ++ .../ChatCharacterBannerRepository.kt | 9 ++++- .../service/ChatCharacterBannerService.kt | 40 +++++++++++++++++-- 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/AdminChatBannerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/AdminChatBannerController.kt index 1a8f3ea..2c4b4b0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/AdminChatBannerController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/AdminChatBannerController.kt @@ -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, "배너 정렬 순서가 성공적으로 변경되었습니다.") + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/dto/ChatCharacterBannerRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/dto/ChatCharacterBannerRequest.kt index 551150c..c4f6b33 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/dto/ChatCharacterBannerRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/dto/ChatCharacterBannerRequest.kt @@ -18,3 +18,11 @@ data class ChatCharacterBannerUpdateRequest( // 캐릭터 ID (변경할 캐릭터) val characterId: Long? = null ) + +/** + * 캐릭터 배너 정렬 순서 일괄 변경 요청 DTO + */ +data class UpdateBannerOrdersRequest( + // 배너 ID 목록 (순서대로 정렬됨) + val ids: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBanner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBanner.kt index 643597b..055f3a1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBanner.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBanner.kt @@ -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() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterBannerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterBannerRepository.kt index 0f3d403..8b7ef53 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterBannerRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterBannerRepository.kt @@ -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 { - // 활성화된 배너 목록 조회 - fun findByActiveTrue(pageable: Pageable): Page + // 활성화된 배너 목록 조회 (정렬 순서대로) + fun findByActiveTrueOrderBySortOrderAsc(pageable: Pageable): Page + + // 활성화된 배너 중 최대 정렬 순서 값 조회 + @Query("SELECT MAX(b.sortOrder) FROM ChatCharacterBanner b WHERE b.isActive = true") + fun findMaxSortOrder(): Int? } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt index a96e7e2..6abc5c6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt @@ -15,10 +15,10 @@ class ChatCharacterBannerService( private val characterRepository: ChatCharacterRepository ) { /** - * 활성화된 모든 배너 조회 + * 활성화된 모든 배너 조회 (정렬 순서대로) */ fun getActiveBanners(pageable: Pageable): Page { - 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): List { + val updatedBanners = mutableListOf() + + 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 + } }