diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminController.kt index aa6870f..f67002e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminController.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.admin.chat.character.curation import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException import org.springframework.beans.factory.annotation.Value import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.DeleteMapping @@ -60,7 +61,12 @@ class CharacterCurationAdminController( fun addCharacter( @PathVariable curationId: Long, @RequestBody request: CharacterCurationAddCharacterRequest - ) = ApiResponse.ok(service.addCharacter(curationId, request.characterId)) + ): ApiResponse { + 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}") fun removeCharacter( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminDto.kt index bb46b03..6266ebd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminDto.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminDto.kt @@ -18,7 +18,7 @@ data class CharacterCurationOrderUpdateRequest( ) data class CharacterCurationAddCharacterRequest( - val characterId: Long + val characterIds: List ) data class CharacterCurationReorderCharactersRequest( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminService.kt index 72b7690..df5e6d0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/curation/CharacterCurationAdminService.kt @@ -60,26 +60,42 @@ class CharacterCurationAdminService( } @Transactional - fun addCharacter(curationId: Long, characterId: Long) { + fun addCharacters(curationId: Long, characterIds: List) { + if (characterIds.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다") + val curation = curationRepository.findById(curationId) .orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } if (!curation.isActive) throw SodaException("비활성화된 큐레이션입니다: $curationId") - val character = characterRepository.findById(characterId) - .orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } - if (!character.isActive) throw SodaException("비활성화된 캐릭터는 추가할 수 없습니다: $characterId") + val uniqueIds = characterIds.filter { it > 0 }.distinct() + if (uniqueIds.isEmpty()) throw SodaException("유효한 캐릭터 ID가 없습니다") - val existing = mappingRepository.findByCuration(curation) - .firstOrNull { it.chatCharacter.id == characterId } - if (existing != null) return // 이미 존재하면 무시 + // 활성 캐릭터만 조회 (조회 단계에서 검증 포함) + val characters = characterRepository.findByIdInAndIsActiveTrue(uniqueIds) + val characterMap = characters.associateBy { it.id!! } - val nextOrder = (mappingRepository.findByCuration(curation).maxOfOrNull { it.sortOrder } ?: 0) + 1 - val mapping = CharacterCurationMapping( - curation = curation, - chatCharacter = character, - sortOrder = nextOrder - ) - mappingRepository.save(mapping) + // 조회 결과에 존재하는 캐릭터만 유효 + val validIds = uniqueIds.filter { id -> characterMap.containsKey(id) } + + val existingMappings = mappingRepository.findByCuration(curation) + val existingCharacterIds = existingMappings.mapNotNull { it.chatCharacter.id }.toSet() + var nextOrder = (existingMappings.maxOfOrNull { it.sortOrder } ?: 0) + 1 + + val toSave = mutableListOf() + 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 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt index ede9fa5..d03ee4f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt @@ -61,4 +61,6 @@ interface ChatCharacterRepository : JpaRepository { @Param("characterId") characterId: Long, pageable: Pageable ): List + + fun findByIdInAndIsActiveTrue(ids: List): List }