feat(banner): 배너 등록/수정/삭제 API

This commit is contained in:
2025-08-07 14:36:48 +09:00
parent 2335050834
commit c729a402aa
9 changed files with 509 additions and 0 deletions

View File

@@ -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()

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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)
}
}