캐릭터 챗봇 #338
| @@ -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, "배너 정렬 순서가 성공적으로 변경되었습니다.") | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -18,3 +18,11 @@ data class ChatCharacterBannerUpdateRequest( | ||||
|     // 캐릭터 ID (변경할 캐릭터) | ||||
|     val characterId: Long? = null | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * 캐릭터 배너 정렬 순서 일괄 변경 요청 DTO | ||||
|  */ | ||||
| data class UpdateBannerOrdersRequest( | ||||
|     // 배너 ID 목록 (순서대로 정렬됨) | ||||
|     val ids: List<Long> | ||||
| ) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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? | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user