feat(banner): 배너 등록/수정/삭제 API
This commit is contained in:
parent
2335050834
commit
c729a402aa
|
@ -0,0 +1,191 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat
|
||||
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService
|
||||
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerListPageResponse
|
||||
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.aws.s3.S3Uploader
|
||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
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.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RequestPart
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/chat/banner")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
class AdminChatBannerController(
|
||||
private val bannerService: ChatCharacterBannerService,
|
||||
private val adminCharacterService: AdminChatCharacterService,
|
||||
private val s3Uploader: S3Uploader,
|
||||
|
||||
@Value("\${cloud.aws.s3.bucket}")
|
||||
private val s3Bucket: String,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
) {
|
||||
/**
|
||||
* 활성화된 배너 목록 조회 API
|
||||
*
|
||||
* @param page 페이지 번호 (0부터 시작, 기본값 0)
|
||||
* @param size 페이지 크기 (기본값 20)
|
||||
* @return 페이징된 배너 목록
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
fun getBannerList(
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "20") size: Int
|
||||
) = run {
|
||||
val pageable = adminCharacterService.createDefaultPageRequest(page, size)
|
||||
val banners = bannerService.getActiveBanners(pageable)
|
||||
val response = ChatCharacterBannerListPageResponse(
|
||||
totalCount = banners.totalElements,
|
||||
content = banners.content.map { ChatCharacterBannerResponse.from(it, imageHost) }
|
||||
)
|
||||
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 상세 조회 API
|
||||
*
|
||||
* @param bannerId 배너 ID
|
||||
* @return 배너 상세 정보
|
||||
*/
|
||||
@GetMapping("/{bannerId}")
|
||||
fun getBannerDetail(@PathVariable bannerId: Long) = run {
|
||||
val banner = bannerService.getBannerById(bannerId)
|
||||
val response = ChatCharacterBannerResponse.from(banner, imageHost)
|
||||
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐릭터 검색 API (배너 등록을 위한)
|
||||
*
|
||||
* @param searchTerm 검색어 (이름, 설명, MBTI, 태그)
|
||||
* @param page 페이지 번호 (0부터 시작, 기본값 0)
|
||||
* @param size 페이지 크기 (기본값 20)
|
||||
* @return 검색된 캐릭터 목록
|
||||
*/
|
||||
@GetMapping("/search-character")
|
||||
fun searchCharacters(
|
||||
@RequestParam searchTerm: String,
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "20") size: Int
|
||||
) = run {
|
||||
val pageable = adminCharacterService.createDefaultPageRequest(page, size)
|
||||
val response = adminCharacterService.searchCharacters(searchTerm, pageable, imageHost)
|
||||
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 등록 API
|
||||
*
|
||||
* @param image 배너 이미지
|
||||
* @param request 배너 등록 요청 정보
|
||||
* @return 등록된 배너 정보
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
fun registerBanner(
|
||||
@RequestPart("image") image: MultipartFile,
|
||||
@RequestPart("request") request: ChatCharacterBannerRegisterRequest
|
||||
) = run {
|
||||
// 1. 먼저 빈 이미지 경로로 배너 등록
|
||||
val banner = bannerService.registerBanner(request.characterId, "")
|
||||
|
||||
// 2. 배너 ID를 사용하여 이미지 업로드
|
||||
val imagePath = saveImage(banner.id!!, image)
|
||||
|
||||
// 3. 이미지 경로로 배너 업데이트
|
||||
val updatedBanner = bannerService.updateBanner(banner.id!!, imagePath)
|
||||
|
||||
val response = ChatCharacterBannerResponse.from(updatedBanner, imageHost)
|
||||
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지를 S3에 업로드하고 경로를 반환
|
||||
*
|
||||
* @param bannerId 배너 ID (이미지 경로에 사용)
|
||||
* @param image 업로드할 이미지 파일
|
||||
* @return 업로드된 이미지 경로
|
||||
*/
|
||||
private fun saveImage(bannerId: Long, image: MultipartFile): String {
|
||||
try {
|
||||
val metadata = ObjectMetadata()
|
||||
metadata.contentLength = image.size
|
||||
|
||||
val fileName = generateFileName("character-banner")
|
||||
|
||||
// S3에 이미지 업로드 (배너 ID를 경로에 사용)
|
||||
return s3Uploader.upload(
|
||||
inputStream = image.inputStream,
|
||||
bucket = s3Bucket,
|
||||
filePath = "/characters/banners/$bannerId/$fileName",
|
||||
metadata = metadata
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw SodaException("이미지 저장에 실패했습니다: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 수정 API
|
||||
*
|
||||
* @param image 배너 이미지
|
||||
* @param request 배너 수정 요청 정보 (배너 ID와 선택적으로 캐릭터 ID 포함)
|
||||
* @return 수정된 배너 정보
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
fun updateBanner(
|
||||
@RequestPart("image") image: MultipartFile,
|
||||
@RequestPart("request") request: ChatCharacterBannerUpdateRequest
|
||||
) = run {
|
||||
// 배너 정보 조회
|
||||
bannerService.getBannerById(request.bannerId)
|
||||
|
||||
// 배너 ID를 사용하여 이미지 업로드
|
||||
val imagePath = saveImage(request.bannerId, image)
|
||||
|
||||
// 배너 수정 (이미지와 캐릭터 모두 수정 가능)
|
||||
val updatedBanner = bannerService.updateBanner(
|
||||
bannerId = request.bannerId,
|
||||
imagePath = imagePath,
|
||||
characterId = request.characterId
|
||||
)
|
||||
|
||||
val response = ChatCharacterBannerResponse.from(updatedBanner, imageHost)
|
||||
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 삭제 API (소프트 삭제)
|
||||
*
|
||||
* @param bannerId 배너 ID
|
||||
* @return 성공 여부
|
||||
*/
|
||||
@DeleteMapping("/{bannerId}")
|
||||
fun deleteBanner(@PathVariable bannerId: Long) = run {
|
||||
bannerService.deleteBanner(bannerId)
|
||||
|
||||
ApiResponse.ok("배너가 성공적으로 삭제되었습니다.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.character.dto
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
import org.springframework.data.domain.Page
|
||||
|
||||
/**
|
||||
* 캐릭터 검색 결과 응답 DTO
|
||||
*/
|
||||
data class ChatCharacterSearchResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val mbti: String?,
|
||||
val imagePath: String?,
|
||||
val tags: List<String>
|
||||
) {
|
||||
companion object {
|
||||
fun from(character: ChatCharacter, imageHost: String): ChatCharacterSearchResponse {
|
||||
val tags = character.tagMappings.map { it.tag.tag }
|
||||
|
||||
return ChatCharacterSearchResponse(
|
||||
id = character.id!!,
|
||||
name = character.name,
|
||||
description = character.description,
|
||||
mbti = character.mbti,
|
||||
imagePath = character.imagePath?.let { "$imageHost$it" },
|
||||
tags = tags
|
||||
)
|
||||
}
|
||||
|
||||
fun fromPage(characters: Page<ChatCharacter>, imageHost: String): Page<ChatCharacterSearchResponse> {
|
||||
return characters.map { from(it, imageHost) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@ package kr.co.vividnext.sodalive.admin.chat.character.service
|
|||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterDetailResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListPageResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchResponse
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.domain.Sort
|
||||
|
@ -61,4 +63,22 @@ class AdminChatCharacterService(
|
|||
|
||||
return ChatCharacterDetailResponse.from(chatCharacter, imageHost)
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐릭터 검색 (이름, 설명, MBTI, 태그 기반)
|
||||
*
|
||||
* @param searchTerm 검색어
|
||||
* @param pageable 페이징 정보
|
||||
* @param imageHost 이미지 호스트 URL
|
||||
* @return 검색된 캐릭터 목록 (페이징)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
fun searchCharacters(
|
||||
searchTerm: String,
|
||||
pageable: Pageable,
|
||||
imageHost: String = ""
|
||||
): Page<ChatCharacterSearchResponse> {
|
||||
val characters = chatCharacterRepository.searchCharacters(searchTerm, pageable)
|
||||
return characters.map { ChatCharacterSearchResponse.from(it, imageHost) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.dto
|
||||
|
||||
/**
|
||||
* 캐릭터 배너 등록 요청 DTO
|
||||
*/
|
||||
data class ChatCharacterBannerRegisterRequest(
|
||||
// 캐릭터 ID
|
||||
val characterId: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* 캐릭터 배너 수정 요청 DTO
|
||||
*/
|
||||
data class ChatCharacterBannerUpdateRequest(
|
||||
// 배너 ID
|
||||
val bannerId: Long,
|
||||
|
||||
// 캐릭터 ID (변경할 캐릭터)
|
||||
val characterId: Long? = null
|
||||
)
|
|
@ -0,0 +1,37 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.dto
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
|
||||
import org.springframework.data.domain.Page
|
||||
|
||||
/**
|
||||
* 캐릭터 배너 응답 DTO
|
||||
*/
|
||||
data class ChatCharacterBannerResponse(
|
||||
val id: Long,
|
||||
val imagePath: String,
|
||||
val characterId: Long,
|
||||
val characterName: String
|
||||
) {
|
||||
companion object {
|
||||
fun from(banner: ChatCharacterBanner, imageHost: String): ChatCharacterBannerResponse {
|
||||
return ChatCharacterBannerResponse(
|
||||
id = banner.id!!,
|
||||
imagePath = "$imageHost${banner.imagePath}",
|
||||
characterId = banner.chatCharacter.id!!,
|
||||
characterName = banner.chatCharacter.name
|
||||
)
|
||||
}
|
||||
|
||||
fun fromPage(banners: Page<ChatCharacterBanner>, imageHost: String): Page<ChatCharacterBannerResponse> {
|
||||
return banners.map { from(it, imageHost) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐릭터 배너 목록 페이지 응답 DTO
|
||||
*/
|
||||
data class ChatCharacterBannerListPageResponse(
|
||||
val totalCount: Long,
|
||||
val content: List<ChatCharacterBannerResponse>
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
package kr.co.vividnext.sodalive.chat.character
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
|
||||
/**
|
||||
* 캐릭터 배너 엔티티
|
||||
* 이미지와 캐릭터 ID를 가지며, 소프트 삭제(isActive = false)를 지원합니다.
|
||||
*/
|
||||
@Entity
|
||||
class ChatCharacterBanner(
|
||||
// 배너 이미지 경로
|
||||
var imagePath: String? = null,
|
||||
|
||||
// 연관된 캐릭터
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "character_id")
|
||||
var chatCharacter: ChatCharacter,
|
||||
|
||||
// 활성화 여부 (소프트 삭제용)
|
||||
var isActive: Boolean = true
|
||||
) : BaseEntity()
|
|
@ -0,0 +1,43 @@
|
|||
package kr.co.vividnext.sodalive.chat.character.repository
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
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.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface ChatCharacterBannerRepository : JpaRepository<ChatCharacterBanner, Long> {
|
||||
// 활성화된 배너 목록 조회
|
||||
fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacterBanner>
|
||||
|
||||
// 특정 캐릭터의 활성화된 배너 목록 조회
|
||||
fun findByChatCharacterAndIsActiveTrue(chatCharacter: ChatCharacter): List<ChatCharacterBanner>
|
||||
|
||||
// 특정 캐릭터 ID의 활성화된 배너 목록 조회
|
||||
fun findByChatCharacter_IdAndIsActiveTrue(characterId: Long): List<ChatCharacterBanner>
|
||||
|
||||
// 이름, 설명, MBTI, 태그로 캐릭터 검색 (배너 포함)
|
||||
@Query(
|
||||
"""
|
||||
SELECT DISTINCT b FROM ChatCharacterBanner b
|
||||
JOIN FETCH b.chatCharacter c
|
||||
LEFT JOIN c.tagMappings tm
|
||||
LEFT JOIN tm.tag t
|
||||
WHERE b.isActive = true AND c.isActive = true AND
|
||||
(
|
||||
LOWER(c.name) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
||||
LOWER(c.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
||||
(c.mbti IS NOT NULL AND LOWER(c.mbti) LIKE LOWER(CONCAT('%', :searchTerm, '%'))) OR
|
||||
(t.tag IS NOT NULL AND LOWER(t.tag) LIKE LOWER(CONCAT('%', :searchTerm, '%')))
|
||||
)
|
||||
"""
|
||||
)
|
||||
fun searchBannersByCharacterAttributes(
|
||||
@Param("searchTerm") searchTerm: String,
|
||||
pageable: Pageable
|
||||
): Page<ChatCharacterBanner>
|
||||
}
|
|
@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
|||
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.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
|
@ -11,4 +13,26 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
|||
fun findByCharacterUUID(characterUUID: String): ChatCharacter?
|
||||
fun findByName(name: String): ChatCharacter?
|
||||
fun findByActiveTrue(pageable: Pageable): Page<ChatCharacter>
|
||||
|
||||
/**
|
||||
* 이름, 설명, MBTI, 태그로 캐릭터 검색
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT DISTINCT c FROM ChatCharacter c
|
||||
LEFT JOIN c.tagMappings tm
|
||||
LEFT JOIN tm.tag t
|
||||
WHERE c.isActive = true AND
|
||||
(
|
||||
LOWER(c.name) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
||||
LOWER(c.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
||||
(c.mbti IS NOT NULL AND LOWER(c.mbti) LIKE LOWER(CONCAT('%', :searchTerm, '%'))) OR
|
||||
(t.tag IS NOT NULL AND LOWER(t.tag) LIKE LOWER(CONCAT('%', :searchTerm, '%')))
|
||||
)
|
||||
"""
|
||||
)
|
||||
fun searchCharacters(
|
||||
@Param("searchTerm") searchTerm: String,
|
||||
pageable: Pageable
|
||||
): Page<ChatCharacter>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
package kr.co.vividnext.sodalive.chat.character.service
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterBannerRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class ChatCharacterBannerService(
|
||||
private val bannerRepository: ChatCharacterBannerRepository,
|
||||
private val characterRepository: ChatCharacterRepository
|
||||
) {
|
||||
/**
|
||||
* 활성화된 모든 배너 조회
|
||||
*/
|
||||
fun getActiveBanners(pageable: Pageable): Page<ChatCharacterBanner> {
|
||||
return bannerRepository.findByIsActiveTrue(pageable)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 상세 조회
|
||||
*/
|
||||
fun getBannerById(bannerId: Long): ChatCharacterBanner {
|
||||
return bannerRepository.findById(bannerId)
|
||||
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 캐릭터의 활성화된 배너 목록 조회
|
||||
*/
|
||||
fun getActiveBannersByCharacterId(characterId: Long): List<ChatCharacterBanner> {
|
||||
return bannerRepository.findByChatCharacter_IdAndIsActiveTrue(characterId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 이름, 설명, MBTI, 태그로 캐릭터 검색 (배너 포함)
|
||||
*/
|
||||
fun searchBannersByCharacterAttributes(searchTerm: String, pageable: Pageable): Page<ChatCharacterBanner> {
|
||||
return bannerRepository.searchBannersByCharacterAttributes(searchTerm, pageable)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 등록
|
||||
*/
|
||||
@Transactional
|
||||
fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner {
|
||||
val character = characterRepository.findById(characterId)
|
||||
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") }
|
||||
|
||||
if (!character.isActive) {
|
||||
throw SodaException("비활성화된 캐릭터에는 배너를 등록할 수 없습니다: $characterId")
|
||||
}
|
||||
|
||||
val banner = ChatCharacterBanner(
|
||||
imagePath = imagePath,
|
||||
chatCharacter = character
|
||||
)
|
||||
|
||||
return bannerRepository.save(banner)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 수정
|
||||
*
|
||||
* @param bannerId 배너 ID
|
||||
* @param imagePath 이미지 경로 (변경할 경우)
|
||||
* @param characterId 캐릭터 ID (변경할 경우)
|
||||
* @return 수정된 배너
|
||||
*/
|
||||
@Transactional
|
||||
fun updateBanner(bannerId: Long, imagePath: String? = null, characterId: Long? = null): ChatCharacterBanner {
|
||||
val banner = bannerRepository.findById(bannerId)
|
||||
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
||||
|
||||
if (!banner.isActive) {
|
||||
throw SodaException("비활성화된 배너는 수정할 수 없습니다: $bannerId")
|
||||
}
|
||||
|
||||
// 이미지 경로 변경
|
||||
if (imagePath != null) {
|
||||
banner.imagePath = imagePath
|
||||
}
|
||||
|
||||
// 캐릭터 변경
|
||||
if (characterId != null) {
|
||||
val character = characterRepository.findById(characterId)
|
||||
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") }
|
||||
|
||||
if (!character.isActive) {
|
||||
throw SodaException("비활성화된 캐릭터로는 변경할 수 없습니다: $characterId")
|
||||
}
|
||||
|
||||
banner.chatCharacter = character
|
||||
}
|
||||
|
||||
return bannerRepository.save(banner)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 삭제 (소프트 삭제)
|
||||
*/
|
||||
@Transactional
|
||||
fun deleteBanner(bannerId: Long) {
|
||||
val banner = bannerRepository.findById(bannerId)
|
||||
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") }
|
||||
|
||||
banner.isActive = false
|
||||
bannerRepository.save(banner)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue