관리자 채팅 메시지 다국어 처리

This commit is contained in:
2025-12-22 22:30:05 +09:00
parent 14d0ae9851
commit 280b21c3cb
10 changed files with 231 additions and 50 deletions

View File

@@ -13,6 +13,8 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
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
@@ -35,6 +37,8 @@ class AdminChatBannerController(
private val bannerService: ChatCharacterBannerService, private val bannerService: ChatCharacterBannerService,
private val adminCharacterService: AdminChatCharacterService, private val adminCharacterService: AdminChatCharacterService,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val langContext: LangContext,
private val messageSource: SodaMessageSource,
@Value("\${cloud.aws.s3.bucket}") @Value("\${cloud.aws.s3.bucket}")
private val s3Bucket: String, private val s3Bucket: String,
@@ -158,8 +162,8 @@ class AdminChatBannerController(
filePath = "characters/banners/$bannerId/$fileName", filePath = "characters/banners/$bannerId/$fileName",
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (_: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.banner.image_save_failed")
} }
} }
@@ -208,7 +212,8 @@ class AdminChatBannerController(
fun deleteBanner(@PathVariable bannerId: Long) = run { fun deleteBanner(@PathVariable bannerId: Long) = run {
bannerService.deleteBanner(bannerId) bannerService.deleteBanner(bannerId)
ApiResponse.ok("배너가 성공적으로 삭제되었습니다.") val message = messageSource.getMessage("admin.chat.banner.delete_success", langContext.lang)
ApiResponse.ok(message)
} }
/** /**
@@ -224,6 +229,7 @@ class AdminChatBannerController(
) = run { ) = run {
bannerService.updateBannerOrders(request.ids) bannerService.updateBannerOrders(request.ids)
ApiResponse.ok(null, "배너 정렬 순서가 성공적으로 변경되었습니다.") val message = messageSource.getMessage("admin.chat.banner.reorder_success", langContext.lang)
ApiResponse.ok(null, message)
} }
} }

View File

@@ -29,13 +29,13 @@ class AdminChatCalculateService(
val todayKst = LocalDate.now(kstZone) val todayKst = LocalDate.now(kstZone)
if (endDate.isAfter(todayKst)) { if (endDate.isAfter(todayKst)) {
throw SodaException("끝 날짜는 오늘 날짜까지만 입력 가능합니다.") throw SodaException(messageKey = "admin.chat.calculate.end_date_max_today")
} }
if (startDate.isAfter(endDate)) { if (startDate.isAfter(endDate)) {
throw SodaException("시작 날짜는 끝 날짜보다 이후일 수 없습니다.") throw SodaException(messageKey = "admin.chat.calculate.start_date_after_end")
} }
if (endDate.isAfter(startDate.plusMonths(6))) { if (endDate.isAfter(startDate.plusMonths(6))) {
throw SodaException("조회 가능 기간은 최대 6개월입니다.") throw SodaException(messageKey = "admin.chat.calculate.max_period_6_months")
} }
val startUtc = startDateStr.convertLocalDateTime() val startUtc = startDateStr.convertLocalDateTime()

View File

@@ -124,7 +124,7 @@ class AdminChatCharacterController(
// 외부 API 호출 전 DB에 동일한 이름이 있는지 조회 // 외부 API 호출 전 DB에 동일한 이름이 있는지 조회
val existingCharacter = service.findByName(request.name) val existingCharacter = service.findByName(request.name)
if (existingCharacter != null) { if (existingCharacter != null) {
throw SodaException("동일한 이름은 등록이 불가능합니다: ${request.name}") throw SodaException(messageKey = "admin.chat.character.duplicate_name")
} }
// 1. 외부 API 호출 // 1. 외부 API 호출
@@ -233,14 +233,18 @@ class AdminChatCharacterController(
// success가 false이면 throw // success가 false이면 throw
if (!apiResponse.success) { if (!apiResponse.success) {
throw SodaException(apiResponse.message ?: "등록에 실패했습니다. 다시 시도해 주세요.") val apiMessage = apiResponse.message
if (apiMessage.isNullOrBlank()) {
throw SodaException(messageKey = "admin.chat.character.register_failed_retry")
}
throw SodaException(apiMessage)
} }
// success가 true이면 data.id 반환 // success가 true이면 data.id 반환
return apiResponse.data?.id ?: throw SodaException("등록에 실패했습니다. 응답에 ID가 없습니다.") return apiResponse.data?.id ?: throw SodaException(messageKey = "admin.chat.character.register_failed_no_id")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
throw SodaException("${e.message}, 등록에 실패했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "admin.chat.character.register_failed_retry")
} }
} }
@@ -257,7 +261,7 @@ class AdminChatCharacterController(
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.character.image_save_failed")
} }
} }
@@ -297,19 +301,19 @@ class AdminChatCharacterController(
request.originalWorkId != null request.originalWorkId != null
if (!hasChangedData && !hasImage && !hasDbOnlyChanges) { if (!hasChangedData && !hasImage && !hasDbOnlyChanges) {
throw SodaException("변경된 데이터가 없습니다.") throw SodaException(messageKey = "admin.chat.character.no_changes")
} }
// 외부 API로 전달할 변경이 있을 때만 외부 API 호출(3가지 필드만 변경된 경우는 호출하지 않음) // 외부 API로 전달할 변경이 있을 때만 외부 API 호출(3가지 필드만 변경된 경우는 호출하지 않음)
if (hasChangedData) { if (hasChangedData) {
val chatCharacter = service.findById(request.id) val chatCharacter = service.findById(request.id)
?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}") ?: throw SodaException(messageKey = "admin.chat.character.not_found")
// 이름이 수정된 경우 DB에 동일한 이름이 있는지 확인 // 이름이 수정된 경우 DB에 동일한 이름이 있는지 확인
if (request.name != null && request.name != chatCharacter.name) { if (request.name != null && request.name != chatCharacter.name) {
val existingCharacter = service.findByName(request.name) val existingCharacter = service.findByName(request.name)
if (existingCharacter != null) { if (existingCharacter != null) {
throw SodaException("동일한 이름은 등록이 불가능합니다: ${request.name}") throw SodaException(messageKey = "admin.chat.character.duplicate_name")
} }
} }
@@ -438,11 +442,15 @@ class AdminChatCharacterController(
// success가 false이면 throw // success가 false이면 throw
if (!apiResponse.success) { if (!apiResponse.success) {
throw SodaException(apiResponse.message ?: "수정에 실패했습니다. 다시 시도해 주세요.") val apiMessage = apiResponse.message
if (apiMessage.isNullOrBlank()) {
throw SodaException(messageKey = "admin.chat.character.update_failed_retry")
}
throw SodaException(apiMessage)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
throw SodaException("${e.message} 수정에 실패했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "admin.chat.character.update_failed_retry")
} }
} }
} }

View File

@@ -63,7 +63,7 @@ class CharacterCurationAdminController(
@RequestBody request: CharacterCurationAddCharacterRequest @RequestBody request: CharacterCurationAddCharacterRequest
): ApiResponse<Boolean> { ): ApiResponse<Boolean> {
val ids = request.characterIds.filter { it > 0 }.distinct() val ids = request.characterIds.filter { it > 0 }.distinct()
if (ids.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다") if (ids.isEmpty()) throw SodaException(messageKey = "admin.chat.curation.character_ids_empty")
service.addCharacters(curationId, ids) service.addCharacters(curationId, ids)
return ApiResponse.ok(true) return ApiResponse.ok(true)
} }

View File

@@ -32,7 +32,7 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun update(request: CharacterCurationUpdateRequest): CharacterCuration { fun update(request: CharacterCurationUpdateRequest): CharacterCuration {
val curation = curationRepository.findById(request.id) val curation = curationRepository.findById(request.id)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: ${request.id}") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
request.title?.let { curation.title = it } request.title?.let { curation.title = it }
request.isAdult?.let { curation.isAdult = it } request.isAdult?.let { curation.isAdult = it }
@@ -44,7 +44,7 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun softDelete(curationId: Long) { fun softDelete(curationId: Long) {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
curation.isActive = false curation.isActive = false
curationRepository.save(curation) curationRepository.save(curation)
} }
@@ -53,7 +53,7 @@ class CharacterCurationAdminService(
fun reorder(ids: List<Long>) { fun reorder(ids: List<Long>) {
ids.forEachIndexed { index, id -> ids.forEachIndexed { index, id ->
val curation = curationRepository.findById(id) val curation = curationRepository.findById(id)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $id") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
curation.sortOrder = index + 1 curation.sortOrder = index + 1
curationRepository.save(curation) curationRepository.save(curation)
} }
@@ -61,14 +61,14 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun addCharacters(curationId: Long, characterIds: List<Long>) { fun addCharacters(curationId: Long, characterIds: List<Long>) {
if (characterIds.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다") if (characterIds.isEmpty()) throw SodaException(messageKey = "admin.chat.curation.character_ids_empty")
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
if (!curation.isActive) throw SodaException("비활성화된 큐레이션입니다: $curationId") if (!curation.isActive) throw SodaException(messageKey = "admin.chat.curation.inactive")
val uniqueIds = characterIds.filter { it > 0 }.distinct() val uniqueIds = characterIds.filter { it > 0 }.distinct()
if (uniqueIds.isEmpty()) throw SodaException("유효한 캐릭터 ID가 없습니다") if (uniqueIds.isEmpty()) throw SodaException(messageKey = "admin.chat.curation.invalid_character_ids")
// 활성 캐릭터만 조회 (조회 단계에서 검증 포함) // 활성 캐릭터만 조회 (조회 단계에서 검증 포함)
val characters = characterRepository.findByIdInAndIsActiveTrue(uniqueIds) val characters = characterRepository.findByIdInAndIsActiveTrue(uniqueIds)
@@ -101,23 +101,23 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun removeCharacter(curationId: Long, characterId: Long) { fun removeCharacter(curationId: Long, characterId: Long) {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
val mappings = mappingRepository.findByCuration(curation) val mappings = mappingRepository.findByCuration(curation)
val target = mappings.firstOrNull { it.chatCharacter.id == characterId } val target = mappings.firstOrNull { it.chatCharacter.id == characterId }
?: throw SodaException("매핑을 찾을 수 없습니다: curation=$curationId, character=$characterId") ?: throw SodaException(messageKey = "admin.chat.curation.mapping_not_found")
mappingRepository.delete(target) mappingRepository.delete(target)
} }
@Transactional @Transactional
fun reorderCharacters(curationId: Long, characterIds: List<Long>) { fun reorderCharacters(curationId: Long, characterIds: List<Long>) {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
val mappings = mappingRepository.findByCuration(curation) val mappings = mappingRepository.findByCuration(curation)
val mappingByCharacterId = mappings.associateBy { it.chatCharacter.id } val mappingByCharacterId = mappings.associateBy { it.chatCharacter.id }
characterIds.forEachIndexed { index, cid -> characterIds.forEachIndexed { index, cid ->
val mapping = mappingByCharacterId[cid] val mapping = mappingByCharacterId[cid]
?: throw SodaException("큐레이션에 포함되지 않은 캐릭터입니다: $cid") ?: throw SodaException(messageKey = "admin.chat.curation.character_not_in_curation")
mapping.sortOrder = index + 1 mapping.sortOrder = index + 1
mappingRepository.save(mapping) mappingRepository.save(mapping)
} }
@@ -146,7 +146,7 @@ class CharacterCurationAdminService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun listCharacters(curationId: Long): List<ChatCharacter> { fun listCharacters(curationId: Long): List<ChatCharacter> {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
val mappings = mappingRepository.findByCurationWithCharacterOrderBySortOrderAsc(curation) val mappings = mappingRepository.findByCurationWithCharacterOrderBySortOrderAsc(curation)
return mappings.map { it.chatCharacter } return mappings.map { it.chatCharacter }
} }

View File

@@ -11,6 +11,8 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.image.CharacterImageService import kr.co.vividnext.sodalive.chat.character.image.CharacterImageService
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.utils.ImageBlurUtil import kr.co.vividnext.sodalive.utils.ImageBlurUtil
import kr.co.vividnext.sodalive.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
@@ -34,6 +36,8 @@ class AdminCharacterImageController(
private val imageService: CharacterImageService, private val imageService: CharacterImageService,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val imageCloudFront: ImageContentCloudFront, private val imageCloudFront: ImageContentCloudFront,
private val langContext: LangContext,
private val messageSource: SodaMessageSource,
@Value("\${cloud.aws.s3.content-bucket}") @Value("\${cloud.aws.s3.content-bucket}")
private val s3Bucket: String, private val s3Bucket: String,
@@ -106,14 +110,18 @@ class AdminCharacterImageController(
@DeleteMapping("/{imageId}") @DeleteMapping("/{imageId}")
fun delete(@PathVariable imageId: Long) = run { fun delete(@PathVariable imageId: Long) = run {
imageService.deleteImage(imageId) imageService.deleteImage(imageId)
ApiResponse.ok(null, "이미지가 삭제되었습니다.") val message = messageSource.getMessage("admin.chat.character.image_deleted", langContext.lang)
ApiResponse.ok(null, message)
} }
@PutMapping("/orders") @PutMapping("/orders")
fun updateOrders(@RequestBody request: UpdateCharacterImageOrdersRequest) = run { fun updateOrders(@RequestBody request: UpdateCharacterImageOrdersRequest) = run {
if (request.characterId == null) throw SodaException("characterId는 필수입니다") if (request.characterId == null) {
throw SodaException(messageKey = "admin.chat.character.character_id_required")
}
imageService.updateOrders(request.characterId, request.ids) imageService.updateOrders(request.characterId, request.ids)
ApiResponse.ok(null, "정렬 순서가 변경되었습니다.") val message = messageSource.getMessage("admin.chat.character.order_updated", langContext.lang)
ApiResponse.ok(null, message)
} }
private fun buildS3Key(characterId: Long): String { private fun buildS3Key(characterId: Long): String {
@@ -132,7 +140,7 @@ class AdminCharacterImageController(
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.character.image_save_failed")
} }
} }
@@ -141,7 +149,7 @@ class AdminCharacterImageController(
// 멀티파트를 BufferedImage로 읽기 // 멀티파트를 BufferedImage로 읽기
val bytes = image.bytes val bytes = image.bytes
val bimg = javax.imageio.ImageIO.read(java.io.ByteArrayInputStream(bytes)) val bimg = javax.imageio.ImageIO.read(java.io.ByteArrayInputStream(bytes))
?: throw SodaException("이미지 포맷을 인식할 수 없습니다.") ?: throw SodaException(messageKey = "admin.chat.character.image_format_invalid")
val blurred = ImageBlurUtil.blurFast(bimg) val blurred = ImageBlurUtil.blurFast(bimg)
// PNG로 저장(알파 유지), JPEG 업로드가 필요하면 포맷 변경 가능 // PNG로 저장(알파 유지), JPEG 업로드가 필요하면 포맷 변경 가능
@@ -164,7 +172,7 @@ class AdminCharacterImageController(
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (e: Exception) {
throw SodaException("블러 이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.character.blur_image_save_failed")
} }
} }
} }

View File

@@ -58,7 +58,7 @@ class AdminChatCharacterService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getChatCharacterDetail(characterId: Long, imageHost: String = ""): ChatCharacterDetailResponse { fun getChatCharacterDetail(characterId: Long, imageHost: String = ""): ChatCharacterDetailResponse {
val chatCharacter = chatCharacterRepository.findById(characterId) val chatCharacter = chatCharacterRepository.findById(characterId)
.orElseThrow { SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: $characterId") } .orElseThrow { SodaException(messageKey = "admin.chat.character.not_found") }
return ChatCharacterDetailResponse.from(chatCharacter, imageHost) return ChatCharacterDetailResponse.from(chatCharacter, imageHost)
} }

View File

@@ -192,8 +192,8 @@ class AdminOriginalWorkController(
filePath = "originals/$originalWorkId/${generateFileName(prefix = "original")}", filePath = "originals/$originalWorkId/${generateFileName(prefix = "original")}",
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (_: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.original.image_save_failed")
} }
} }
} }

View File

@@ -38,7 +38,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun createOriginalWork(request: OriginalWorkRegisterRequest): OriginalWork { fun createOriginalWork(request: OriginalWorkRegisterRequest): OriginalWork {
originalWorkRepository.findByTitleAndIsDeletedFalse(request.title)?.let { originalWorkRepository.findByTitleAndIsDeletedFalse(request.title)?.let {
throw SodaException("동일한 제목의 원작이 이미 존재합니다: ${request.title}") throw SodaException(messageKey = "admin.chat.original.duplicate_title")
} }
val entity = OriginalWork( val entity = OriginalWork(
title = request.title, title = request.title,
@@ -107,7 +107,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun updateOriginalWork(request: OriginalWorkUpdateRequest, imagePath: String? = null): OriginalWork { fun updateOriginalWork(request: OriginalWorkUpdateRequest, imagePath: String? = null): OriginalWork {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(request.id) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(request.id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
request.title?.let { ow.title = it } request.title?.let { ow.title = it }
request.contentType?.let { ow.contentType = it } request.contentType?.let { ow.contentType = it }
@@ -177,7 +177,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun updateOriginalWorkImage(originalWorkId: Long, imagePath: String): OriginalWork { fun updateOriginalWorkImage(originalWorkId: Long, imagePath: String): OriginalWork {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
ow.imagePath = imagePath ow.imagePath = imagePath
return originalWorkRepository.save(ow) return originalWorkRepository.save(ow)
} }
@@ -186,7 +186,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun deleteOriginalWork(id: Long) { fun deleteOriginalWork(id: Long) {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(id) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다: $id") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
ow.isDeleted = true ow.isDeleted = true
originalWorkRepository.save(ow) originalWorkRepository.save(ow)
} }
@@ -195,7 +195,7 @@ class AdminOriginalWorkService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getOriginalWork(id: Long): OriginalWork { fun getOriginalWork(id: Long): OriginalWork {
return originalWorkRepository.findByIdAndIsDeletedFalse(id) return originalWorkRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
} }
/** 원작 페이징 조회 */ /** 원작 페이징 조회 */
@@ -216,7 +216,7 @@ class AdminOriginalWorkService(
fun getCharactersOfOriginalWorkPage(originalWorkId: Long, page: Int, size: Int): Page<ChatCharacter> { fun getCharactersOfOriginalWorkPage(originalWorkId: Long, page: Int, size: Int): Page<ChatCharacter> {
// 원작 존재 및 소프트 삭제 여부 확인 // 원작 존재 및 소프트 삭제 여부 확인
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
val safePage = if (page < 0) 0 else page val safePage = if (page < 0) 0 else page
val safeSize = when { val safeSize = when {
@@ -238,7 +238,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun assignCharacters(originalWorkId: Long, characterIds: List<Long>) { fun assignCharacters(originalWorkId: Long, characterIds: List<Long>) {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
if (characterIds.isEmpty()) return if (characterIds.isEmpty()) return
val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds) val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds)
characters.forEach { it.originalWork = ow } characters.forEach { it.originalWork = ow }
@@ -250,7 +250,7 @@ class AdminOriginalWorkService(
fun unassignCharacters(originalWorkId: Long, characterIds: List<Long>) { fun unassignCharacters(originalWorkId: Long, characterIds: List<Long>) {
// 원작 존재 확인 (소프트 삭제 제외) // 원작 존재 확인 (소프트 삭제 제외)
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
if (characterIds.isEmpty()) return if (characterIds.isEmpty()) return
val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds) val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds)
characters.forEach { it.originalWork = null } characters.forEach { it.originalWork = null }
@@ -261,13 +261,13 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun assignOneCharacter(originalWorkId: Long, characterId: Long) { fun assignOneCharacter(originalWorkId: Long, characterId: Long) {
val character = chatCharacterRepository.findById(characterId) val character = chatCharacterRepository.findById(characterId)
.orElseThrow { SodaException("해당 캐릭터를 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.character.not_found") }
if (originalWorkId == 0L) { if (originalWorkId == 0L) {
character.originalWork = null character.originalWork = null
} else { } else {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
character.originalWork = ow character.originalWork = ow
} }

View File

@@ -150,6 +150,159 @@ class SodaMessageSource {
) )
) )
private val adminChatBannerMessages = mapOf(
"admin.chat.banner.image_save_failed" to mapOf(
Lang.KO to "이미지 저장에 실패했습니다.",
Lang.EN to "Failed to save image.",
Lang.JA to "画像の保存に失敗しました。"
),
"admin.chat.banner.delete_success" to mapOf(
Lang.KO to "배너가 성공적으로 삭제되었습니다.",
Lang.EN to "Banner deleted successfully.",
Lang.JA to "バナーが削除されました。"
),
"admin.chat.banner.reorder_success" to mapOf(
Lang.KO to "배너 정렬 순서가 성공적으로 변경되었습니다.",
Lang.EN to "Banner order updated successfully.",
Lang.JA to "バナーの並び順が変更されました。"
)
)
private val adminChatCalculateMessages = mapOf(
"admin.chat.calculate.end_date_max_today" to mapOf(
Lang.KO to "끝 날짜는 오늘 날짜까지만 입력 가능합니다.",
Lang.EN to "End date can be at most today.",
Lang.JA to "終了日は本日まで指定できます。"
),
"admin.chat.calculate.start_date_after_end" to mapOf(
Lang.KO to "시작 날짜는 끝 날짜보다 이후일 수 없습니다.",
Lang.EN to "Start date cannot be after end date.",
Lang.JA to "開始日は終了日より後にできません。"
),
"admin.chat.calculate.max_period_6_months" to mapOf(
Lang.KO to "조회 가능 기간은 최대 6개월입니다.",
Lang.EN to "Maximum query period is 6 months.",
Lang.JA to "照会期間は最大6ヶ月です。"
)
)
private val adminChatCharacterMessages = mapOf(
"admin.chat.character.duplicate_name" to mapOf(
Lang.KO to "동일한 이름은 등록이 불가능합니다.",
Lang.EN to "A character with the same name already exists.",
Lang.JA to "同じ名前は登録できません。"
),
"admin.chat.character.register_failed_retry" to mapOf(
Lang.KO to "등록에 실패했습니다. 다시 시도해 주세요.",
Lang.EN to "Registration failed. Please try again.",
Lang.JA to "登録に失敗しました。もう一度お試しください。"
),
"admin.chat.character.register_failed_no_id" to mapOf(
Lang.KO to "등록에 실패했습니다. 응답에 ID가 없습니다.",
Lang.EN to "Registration failed. No ID in response.",
Lang.JA to "登録に失敗しました。応答にIDがありません。"
),
"admin.chat.character.image_save_failed" to mapOf(
Lang.KO to "이미지 저장에 실패했습니다.",
Lang.EN to "Failed to save image.",
Lang.JA to "画像の保存に失敗しました。"
),
"admin.chat.character.no_changes" to mapOf(
Lang.KO to "변경된 데이터가 없습니다.",
Lang.EN to "No changes detected.",
Lang.JA to "変更されたデータがありません。"
),
"admin.chat.character.not_found" to mapOf(
Lang.KO to "해당 캐릭터를 찾을 수 없습니다.",
Lang.EN to "Character not found.",
Lang.JA to "該当キャラクターが見つかりません。"
),
"admin.chat.character.update_failed_retry" to mapOf(
Lang.KO to "수정에 실패했습니다. 다시 시도해 주세요.",
Lang.EN to "Update failed. Please try again.",
Lang.JA to "更新に失敗しました。もう一度お試しください。"
)
)
private val adminChatCurationMessages = mapOf(
"admin.chat.curation.not_found" to mapOf(
Lang.KO to "큐레이션을 찾을 수 없습니다.",
Lang.EN to "Curation not found.",
Lang.JA to "キュレーションが見つかりません。"
),
"admin.chat.curation.character_ids_empty" to mapOf(
Lang.KO to "등록할 캐릭터 ID 리스트가 비어있습니다",
Lang.EN to "Character ID list to register is empty.",
Lang.JA to "登録するキャラクターIDリストが空です。"
),
"admin.chat.curation.inactive" to mapOf(
Lang.KO to "비활성화된 큐레이션입니다.",
Lang.EN to "Curation is inactive.",
Lang.JA to "無効化されたキュレーションです。"
),
"admin.chat.curation.invalid_character_ids" to mapOf(
Lang.KO to "유효한 캐릭터 ID가 없습니다",
Lang.EN to "No valid character IDs.",
Lang.JA to "有効なキャラクターIDがありません。"
),
"admin.chat.curation.mapping_not_found" to mapOf(
Lang.KO to "매핑을 찾을 수 없습니다.",
Lang.EN to "Mapping not found.",
Lang.JA to "マッピングが見つかりません。"
),
"admin.chat.curation.character_not_in_curation" to mapOf(
Lang.KO to "큐레이션에 포함되지 않은 캐릭터입니다.",
Lang.EN to "Character not included in this curation.",
Lang.JA to "このキュレーションに含まれていないキャラクターです。"
)
)
private val adminChatCharacterImageMessages = mapOf(
"admin.chat.character.image_deleted" to mapOf(
Lang.KO to "이미지가 삭제되었습니다.",
Lang.EN to "Image deleted.",
Lang.JA to "画像が削除されました。"
),
"admin.chat.character.character_id_required" to mapOf(
Lang.KO to "characterId는 필수입니다",
Lang.EN to "characterId is required.",
Lang.JA to "characterIdは必須です。"
),
"admin.chat.character.order_updated" to mapOf(
Lang.KO to "정렬 순서가 변경되었습니다.",
Lang.EN to "Order updated.",
Lang.JA to "並び順が変更されました。"
),
"admin.chat.character.image_format_invalid" to mapOf(
Lang.KO to "이미지 포맷을 인식할 수 없습니다.",
Lang.EN to "Unsupported image format.",
Lang.JA to "画像形式を認識できません。"
),
"admin.chat.character.blur_image_save_failed" to mapOf(
Lang.KO to "블러 이미지 저장에 실패했습니다.",
Lang.EN to "Failed to save blurred image.",
Lang.JA to "ぼかし画像の保存に失敗しました。"
)
)
private val adminChatOriginalWorkMessages = mapOf(
"admin.chat.original.image_save_failed" to mapOf(
Lang.KO to "이미지 저장에 실패했습니다.",
Lang.EN to "Failed to save image.",
Lang.JA to "画像の保存に失敗しました。"
),
"admin.chat.original.duplicate_title" to mapOf(
Lang.KO to "동일한 제목의 원작이 이미 존재합니다.",
Lang.EN to "An original work with the same title already exists.",
Lang.JA to "同じタイトルの原作が既に存在します。"
),
"admin.chat.original.not_found" to mapOf(
Lang.KO to "해당 원작을 찾을 수 없습니다.",
Lang.EN to "Original work not found.",
Lang.JA to "該当の原作が見つかりません。"
)
)
fun getMessage(key: String, lang: Lang): String? { fun getMessage(key: String, lang: Lang): String? {
val messageGroups = listOf( val messageGroups = listOf(
commonMessages, commonMessages,
@@ -158,7 +311,13 @@ class SodaMessageSource {
auditionNotificationMessages, auditionNotificationMessages,
auditionRoleMessages, auditionRoleMessages,
settlementRatioMessages, settlementRatioMessages,
adminCanMessages adminCanMessages,
adminChatBannerMessages,
adminChatCalculateMessages,
adminChatCharacterMessages,
adminChatCurationMessages,
adminChatCharacterImageMessages,
adminChatOriginalWorkMessages
) )
for (messages in messageGroups) { for (messages in messageGroups) {
val translations = messages[key] ?: continue val translations = messages[key] ?: continue