캐릭터 챗봇 #338

Merged
klaus merged 119 commits from test into main 2025-09-10 06:08:47 +00:00
4 changed files with 40 additions and 16 deletions
Showing only changes of commit d26e0a89f6 - Show all commits

View File

@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.admin.chat.character.curation package kr.co.vividnext.sodalive.admin.chat.character.curation
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.DeleteMapping
@ -60,7 +61,12 @@ class CharacterCurationAdminController(
fun addCharacter( fun addCharacter(
@PathVariable curationId: Long, @PathVariable curationId: Long,
@RequestBody request: CharacterCurationAddCharacterRequest @RequestBody request: CharacterCurationAddCharacterRequest
) = ApiResponse.ok(service.addCharacter(curationId, request.characterId)) ): ApiResponse<Boolean> {
val ids = request.characterIds.filter { it > 0 }.distinct()
if (ids.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다")
service.addCharacters(curationId, ids)
return ApiResponse.ok(true)
}
@DeleteMapping("/{curationId}/characters/{characterId}") @DeleteMapping("/{curationId}/characters/{characterId}")
fun removeCharacter( fun removeCharacter(

View File

@ -18,7 +18,7 @@ data class CharacterCurationOrderUpdateRequest(
) )
data class CharacterCurationAddCharacterRequest( data class CharacterCurationAddCharacterRequest(
val characterId: Long val characterIds: List<Long>
) )
data class CharacterCurationReorderCharactersRequest( data class CharacterCurationReorderCharactersRequest(

View File

@ -60,26 +60,42 @@ class CharacterCurationAdminService(
} }
@Transactional @Transactional
fun addCharacter(curationId: Long, characterId: Long) { fun addCharacters(curationId: Long, characterIds: List<Long>) {
if (characterIds.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다")
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") }
if (!curation.isActive) throw SodaException("비활성화된 큐레이션입니다: $curationId") if (!curation.isActive) throw SodaException("비활성화된 큐레이션입니다: $curationId")
val character = characterRepository.findById(characterId) val uniqueIds = characterIds.filter { it > 0 }.distinct()
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } if (uniqueIds.isEmpty()) throw SodaException("유효한 캐릭터 ID가 없습니다")
if (!character.isActive) throw SodaException("비활성화된 캐릭터는 추가할 수 없습니다: $characterId")
val existing = mappingRepository.findByCuration(curation) // 활성 캐릭터만 조회 (조회 단계에서 검증 포함)
.firstOrNull { it.chatCharacter.id == characterId } val characters = characterRepository.findByIdInAndIsActiveTrue(uniqueIds)
if (existing != null) return // 이미 존재하면 무시 val characterMap = characters.associateBy { it.id!! }
val nextOrder = (mappingRepository.findByCuration(curation).maxOfOrNull { it.sortOrder } ?: 0) + 1 // 조회 결과에 존재하는 캐릭터만 유효
val mapping = CharacterCurationMapping( val validIds = uniqueIds.filter { id -> characterMap.containsKey(id) }
curation = curation,
chatCharacter = character, val existingMappings = mappingRepository.findByCuration(curation)
sortOrder = nextOrder val existingCharacterIds = existingMappings.mapNotNull { it.chatCharacter.id }.toSet()
) var nextOrder = (existingMappings.maxOfOrNull { it.sortOrder } ?: 0) + 1
mappingRepository.save(mapping)
val toSave = mutableListOf<CharacterCurationMapping>()
validIds.forEach { id ->
if (!existingCharacterIds.contains(id)) {
val character = characterMap[id] ?: return@forEach
toSave += CharacterCurationMapping(
curation = curation,
chatCharacter = character,
sortOrder = nextOrder++
)
}
}
if (toSave.isNotEmpty()) {
mappingRepository.saveAll(toSave)
}
} }
@Transactional @Transactional

View File

@ -61,4 +61,6 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
@Param("characterId") characterId: Long, @Param("characterId") characterId: Long,
pageable: Pageable pageable: Pageable
): List<ChatCharacter> ): List<ChatCharacter>
fun findByIdInAndIsActiveTrue(ids: List<Long>): List<ChatCharacter>
} }