From d26e0a89f6b6f08dff3b3a1d45c28e875b277a74 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 28 Aug 2025 19:22:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin-curation):=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=BA=90=EB=A6=AD=ED=84=B0=20=EB=8B=A4?= =?UTF-8?q?=EC=A4=91=20=EB=93=B1=EB=A1=9D=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 중복 ID 제거 및 0 이하 ID 필터링 - 조회 단계에서 활성 캐릭터만 조회하여 검증 포함 - 존재하지 않거나 비활성인 캐릭터는 건너뛰고 나머지만 등록 - 기존 매핑 있는 캐릭터는 무시, 다음 정렬 순서(nextOrder)로 일괄 추가 --- .../CharacterCurationAdminController.kt | 8 +++- .../curation/CharacterCurationAdminDto.kt | 2 +- .../curation/CharacterCurationAdminService.kt | 44 +++++++++++++------ .../repository/ChatCharacterRepository.kt | 2 + 4 files changed, 40 insertions(+), 16 deletions(-) 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 }