캐릭터 챗봇 #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.ChatCharacterBannerRegisterRequest | ||||||
| import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerResponse | 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.ChatCharacterBannerUpdateRequest | ||||||
|  | import kr.co.vividnext.sodalive.admin.chat.dto.UpdateBannerOrdersRequest | ||||||
| import kr.co.vividnext.sodalive.aws.s3.S3Uploader | import kr.co.vividnext.sodalive.aws.s3.S3Uploader | ||||||
| import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService | import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService | ||||||
| import kr.co.vividnext.sodalive.common.ApiResponse | 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.PathVariable | ||||||
| import org.springframework.web.bind.annotation.PostMapping | import org.springframework.web.bind.annotation.PostMapping | ||||||
| import org.springframework.web.bind.annotation.PutMapping | 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.RequestMapping | ||||||
| import org.springframework.web.bind.annotation.RequestParam | import org.springframework.web.bind.annotation.RequestParam | ||||||
| import org.springframework.web.bind.annotation.RequestPart | import org.springframework.web.bind.annotation.RequestPart | ||||||
| @@ -98,7 +100,7 @@ class AdminChatBannerController( | |||||||
|      * 배너 등록 API |      * 배너 등록 API | ||||||
|      * |      * | ||||||
|      * @param image 배너 이미지 |      * @param image 배너 이미지 | ||||||
|      * @param request 배너 등록 요청 정보 |      * @param request 배너 등록 요청 정보 (캐릭터 ID와 선택적으로 정렬 순서 포함) | ||||||
|      * @return 등록된 배너 정보 |      * @return 등록된 배너 정보 | ||||||
|      */ |      */ | ||||||
|     @PostMapping("/register") |     @PostMapping("/register") | ||||||
| @@ -106,8 +108,11 @@ class AdminChatBannerController( | |||||||
|         @RequestPart("image") image: MultipartFile, |         @RequestPart("image") image: MultipartFile, | ||||||
|         @RequestPart("request") request: ChatCharacterBannerRegisterRequest |         @RequestPart("request") request: ChatCharacterBannerRegisterRequest | ||||||
|     ) = run { |     ) = run { | ||||||
|         // 1. 먼저 빈 이미지 경로로 배너 등록 |         // 1. 먼저 빈 이미지 경로로 배너 등록 (정렬 순서 포함) | ||||||
|         val banner = bannerService.registerBanner(request.characterId, "") |         val banner = bannerService.registerBanner( | ||||||
|  |             characterId = request.characterId, | ||||||
|  |             imagePath = "" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         // 2. 배너 ID를 사용하여 이미지 업로드 |         // 2. 배너 ID를 사용하여 이미지 업로드 | ||||||
|         val imagePath = saveImage(banner.id!!, image) |         val imagePath = saveImage(banner.id!!, image) | ||||||
| @@ -188,4 +193,20 @@ class AdminChatBannerController( | |||||||
|  |  | ||||||
|         ApiResponse.ok("배너가 성공적으로 삭제되었습니다.") |         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 (변경할 캐릭터) |     // 캐릭터 ID (변경할 캐릭터) | ||||||
|     val characterId: Long? = null |     val characterId: Long? = null | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 캐릭터 배너 정렬 순서 일괄 변경 요청 DTO | ||||||
|  |  */ | ||||||
|  | data class UpdateBannerOrdersRequest( | ||||||
|  |     // 배너 ID 목록 (순서대로 정렬됨) | ||||||
|  |     val ids: List<Long> | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import javax.persistence.ManyToOne | |||||||
| /** | /** | ||||||
|  * 캐릭터 배너 엔티티 |  * 캐릭터 배너 엔티티 | ||||||
|  * 이미지와 캐릭터 ID를 가지며, 소프트 삭제(isActive = false)를 지원합니다. |  * 이미지와 캐릭터 ID를 가지며, 소프트 삭제(isActive = false)를 지원합니다. | ||||||
|  |  * 정렬 순서(sortOrder)를 통해 배너의 표시 순서를 결정합니다. | ||||||
|  */ |  */ | ||||||
| @Entity | @Entity | ||||||
| class ChatCharacterBanner( | class ChatCharacterBanner( | ||||||
| @@ -20,6 +21,9 @@ class ChatCharacterBanner( | |||||||
|     @JoinColumn(name = "character_id") |     @JoinColumn(name = "character_id") | ||||||
|     var chatCharacter: ChatCharacter, |     var chatCharacter: ChatCharacter, | ||||||
|  |  | ||||||
|  |     // 정렬 순서 (낮을수록 먼저 표시) | ||||||
|  |     var sortOrder: Int = 0, | ||||||
|  |  | ||||||
|     // 활성화 여부 (소프트 삭제용) |     // 활성화 여부 (소프트 삭제용) | ||||||
|     var isActive: Boolean = true |     var isActive: Boolean = true | ||||||
| ) : BaseEntity() | ) : BaseEntity() | ||||||
|   | |||||||
| @@ -4,10 +4,15 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner | |||||||
| import org.springframework.data.domain.Page | import org.springframework.data.domain.Page | ||||||
| import org.springframework.data.domain.Pageable | import org.springframework.data.domain.Pageable | ||||||
| import org.springframework.data.jpa.repository.JpaRepository | import org.springframework.data.jpa.repository.JpaRepository | ||||||
|  | import org.springframework.data.jpa.repository.Query | ||||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||||
|  |  | ||||||
| @Repository | @Repository | ||||||
| interface ChatCharacterBannerRepository : JpaRepository<ChatCharacterBanner, Long> { | 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 |     private val characterRepository: ChatCharacterRepository | ||||||
| ) { | ) { | ||||||
|     /** |     /** | ||||||
|      * 활성화된 모든 배너 조회 |      * 활성화된 모든 배너 조회 (정렬 순서대로) | ||||||
|      */ |      */ | ||||||
|     fun getActiveBanners(pageable: Pageable): Page<ChatCharacterBanner> { |     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 |     @Transactional | ||||||
|     fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner { |     fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner { | ||||||
| @@ -41,9 +45,13 @@ class ChatCharacterBannerService( | |||||||
|             throw SodaException("비활성화된 캐릭터에는 배너를 등록할 수 없습니다: $characterId") |             throw SodaException("비활성화된 캐릭터에는 배너를 등록할 수 없습니다: $characterId") | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // 정렬 순서가 지정되지 않은 경우 가장 마지막 순서로 설정 | ||||||
|  |         val finalSortOrder = (bannerRepository.findMaxSortOrder() ?: 0) + 1 | ||||||
|  |  | ||||||
|         val banner = ChatCharacterBanner( |         val banner = ChatCharacterBanner( | ||||||
|             imagePath = imagePath, |             imagePath = imagePath, | ||||||
|             chatCharacter = character |             chatCharacter = character, | ||||||
|  |             sortOrder = finalSortOrder | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         return bannerRepository.save(banner) |         return bannerRepository.save(banner) | ||||||
| @@ -97,4 +105,30 @@ class ChatCharacterBannerService( | |||||||
|         banner.isActive = false |         banner.isActive = false | ||||||
|         bannerRepository.save(banner) |         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