diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt index e9fcc649..9a903a9f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.chat.character.comment import kr.co.vividnext.sodalive.common.ApiResponse 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.member.Member import org.springframework.beans.factory.annotation.Value import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -18,6 +20,8 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/api/chat/character") class CharacterCommentController( private val service: CharacterCommentService, + private val messageSource: SodaMessageSource, + private val langContext: LangContext, @Value("\${cloud.aws.cloud-front.host}") private val imageHost: String ) { @@ -28,9 +32,9 @@ class CharacterCommentController( @RequestBody request: CreateCharacterCommentRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") - if (request.comment.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") + if (request.comment.isBlank()) throw SodaException(messageKey = "chat.character.comment.required") val id = service.addComment(characterId, member, request.comment) ApiResponse.ok(id) @@ -43,9 +47,9 @@ class CharacterCommentController( @RequestBody request: CreateCharacterCommentRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") - if (request.comment.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") + if (request.comment.isBlank()) throw SodaException(messageKey = "chat.character.comment.required") val id = service.addReply(characterId, commentId, member, request.comment, request.languageCode) ApiResponse.ok(id) @@ -58,8 +62,8 @@ class CharacterCommentController( @RequestParam(required = false) cursor: Long?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val data = service.listComments(imageHost, characterId, cursor, limit) ApiResponse.ok(data) @@ -73,8 +77,8 @@ class CharacterCommentController( @RequestParam(required = false) cursor: Long?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") // characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨 val data = service.getReplies(imageHost, commentId, cursor, limit) @@ -87,10 +91,11 @@ class CharacterCommentController( @PathVariable commentId: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") service.deleteComment(characterId, commentId, member) - ApiResponse.ok(true, "댓글이 삭제되었습니다.") + val message = messageSource.getMessage("chat.character.comment.deleted", langContext.lang) + ApiResponse.ok(true, message) } @PostMapping("/{characterId}/comments/{commentId}/reports") @@ -100,9 +105,10 @@ class CharacterCommentController( @RequestBody request: ReportCharacterCommentRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") service.reportComment(characterId, commentId, member, request.content) - ApiResponse.ok(true, "신고가 접수되었습니다.") + val message = messageSource.getMessage("chat.character.comment.reported", langContext.lang) + ApiResponse.ok(true, message) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt index 1be90668..8d5cd826 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt @@ -36,7 +36,7 @@ class CharacterCommentService( entity: CharacterComment, replyCountOverride: Int? = null ): CharacterCommentResponse { - val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.") + val member = entity.member ?: throw SodaException(messageKey = "chat.character.comment.invalid") return CharacterCommentResponse( commentId = entity.id!!, memberId = member.id!!, @@ -50,7 +50,7 @@ class CharacterCommentService( } private fun toReplyResponse(imageHost: String, entity: CharacterComment): CharacterReplyResponse { - val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.") + val member = entity.member ?: throw SodaException(messageKey = "chat.character.comment.invalid") return CharacterReplyResponse( replyId = entity.id!!, memberId = member.id!!, @@ -64,9 +64,10 @@ class CharacterCommentService( @Transactional fun addComment(characterId: Long, member: Member, text: String, languageCode: String? = null): Long { - val character = chatCharacterRepository.findById(characterId).orElseThrow { SodaException("캐릭터를 찾을 수 없습니다.") } - if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.") - if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") + val character = chatCharacterRepository.findById(characterId) + .orElseThrow { SodaException(messageKey = "chat.character.not_found") } + if (!character.isActive) throw SodaException(messageKey = "chat.character.inactive") + if (text.isBlank()) throw SodaException(messageKey = "chat.character.comment.required") val entity = CharacterComment(comment = text, languageCode = languageCode) entity.chatCharacter = character @@ -95,12 +96,14 @@ class CharacterCommentService( text: String, languageCode: String? = null ): Long { - val character = chatCharacterRepository.findById(characterId).orElseThrow { SodaException("캐릭터를 찾을 수 없습니다.") } - if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.") - val parent = commentRepository.findById(parentCommentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } - if (parent.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.") - if (!parent.isActive) throw SodaException("비활성화된 댓글입니다.") - if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") + val character = chatCharacterRepository.findById(characterId) + .orElseThrow { SodaException(messageKey = "chat.character.not_found") } + if (!character.isActive) throw SodaException(messageKey = "chat.character.inactive") + val parent = commentRepository.findById(parentCommentId) + .orElseThrow { SodaException(messageKey = "chat.character.comment.not_found") } + if (parent.chatCharacter?.id != characterId) throw SodaException(messageKey = "common.error.invalid_request") + if (!parent.isActive) throw SodaException(messageKey = "chat.character.comment.inactive") + if (text.isBlank()) throw SodaException(messageKey = "chat.character.comment.required") val entity = CharacterComment(comment = text, languageCode = languageCode) entity.chatCharacter = character @@ -162,9 +165,9 @@ class CharacterCommentService( limit: Int = 20 ): CharacterCommentRepliesResponse { val original = commentRepository.findById(commentId).orElseThrow { - SodaException("댓글을 찾을 수 없습니다.") + SodaException(messageKey = "chat.character.comment.not_found") } - if (!original.isActive) throw SodaException("비활성화된 댓글입니다.") + if (!original.isActive) throw SodaException(messageKey = "chat.character.comment.inactive") val pageable = PageRequest.of(0, limit) val replies = if (cursor == null) { @@ -207,20 +210,22 @@ class CharacterCommentService( @Transactional fun deleteComment(characterId: Long, commentId: Long, member: Member) { - val comment = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } - if (comment.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.") + val comment = commentRepository.findById(commentId) + .orElseThrow { SodaException(messageKey = "chat.character.comment.not_found") } + if (comment.chatCharacter?.id != characterId) throw SodaException(messageKey = "common.error.invalid_request") if (!comment.isActive) return - val ownerId = comment.member?.id ?: throw SodaException("유효하지 않은 댓글입니다.") - if (ownerId != member.id) throw SodaException("삭제 권한이 없습니다.") + val ownerId = comment.member?.id ?: throw SodaException(messageKey = "chat.character.comment.invalid") + if (ownerId != member.id) throw SodaException(messageKey = "chat.character.comment.delete_forbidden") comment.isActive = false commentRepository.save(comment) } @Transactional fun reportComment(characterId: Long, commentId: Long, member: Member, content: String) { - val comment = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } - if (comment.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.") - if (content.isBlank()) throw SodaException("신고 내용을 입력해주세요.") + val comment = commentRepository.findById(commentId) + .orElseThrow { SodaException(messageKey = "chat.character.comment.not_found") } + if (comment.chatCharacter?.id != characterId) throw SodaException(messageKey = "common.error.invalid_request") + if (content.isBlank()) throw SodaException(messageKey = "chat.character.comment.report_content_required") val report = CharacterCommentReport(content = content) report.comment = comment diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt index 63ae72be..afa2bb37 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt @@ -155,12 +155,12 @@ class ChatCharacterController( @PathVariable characterId: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") // 캐릭터 상세 정보 조회 val character = service.getCharacterDetail(characterId) - ?: throw SodaException("캐릭터를 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.character.not_found") // 태그 가공: # prefix 규칙 적용 후 공백으로 연결 val tags = character.tagMappings diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt index 8744e26d..7e9d5899 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt @@ -36,8 +36,8 @@ class CharacterImageController( @RequestParam(required = false, defaultValue = "20") size: Int, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val pageSize = if (size <= 0) 20 else minOf(size, 20) @@ -124,8 +124,8 @@ class CharacterImageController( @RequestParam(required = false, defaultValue = "20") size: Int, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val pageSize = if (size <= 0) 20 else minOf(size, 20) val expiration = 5L * 60L * 1000L // 5분 @@ -198,18 +198,18 @@ class CharacterImageController( @RequestBody req: CharacterImagePurchaseRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val image = imageService.getById(req.imageId) - if (!image.isActive) throw SodaException("비활성화된 이미지입니다.") + if (!image.isActive) throw SodaException(messageKey = "chat.character.image.inactive") val isOwned = (image.imagePriceCan == 0L) || imageService.isOwnedImageByMember(image.id!!, member.id!!) if (!isOwned) { val needCan = image.imagePriceCan.toInt() - if (needCan <= 0) throw SodaException("구매 가격이 잘못되었습니다.") + if (needCan <= 0) throw SodaException(messageKey = "chat.purchase.invalid_price") canPaymentService.spendCanForCharacterImage( memberId = member.id!!, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt index b0bbe98f..596da874 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt @@ -64,11 +64,11 @@ class CharacterImageService( } fun getById(id: Long): CharacterImage = - imageRepository.findById(id).orElseThrow { SodaException("캐릭터 이미지를 찾을 수 없습니다: $id") } + imageRepository.findById(id).orElseThrow { SodaException(messageKey = "chat.character.image.not_found") } fun getCharacterImagePath(characterId: Long): String? { val character = characterRepository.findById(characterId) - .orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } + .orElseThrow { SodaException(messageKey = "chat.character.not_found") } return character.imagePath } @@ -94,11 +94,13 @@ class CharacterImageService( triggers: List ): CharacterImage { val character = characterRepository.findById(characterId) - .orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } + .orElseThrow { SodaException(messageKey = "chat.character.not_found") } - if (imagePriceCan < 0 || messagePriceCan < 0) throw SodaException("가격은 0 can 이상이어야 합니다.") + if (imagePriceCan < 0 || messagePriceCan < 0) { + throw SodaException(messageKey = "chat.character.image.min_price") + } - if (!character.isActive) throw SodaException("비활성화된 캐릭터에는 이미지를 등록할 수 없습니다: $characterId") + if (!character.isActive) throw SodaException(messageKey = "chat.character.inactive_image_register") val nextOrder = (imageRepository.findMaxSortOrderByCharacterId(characterId)) + 1 val entity = CharacterImage( @@ -122,7 +124,7 @@ class CharacterImageService( @Transactional fun updateTriggers(imageId: Long, triggers: List): CharacterImage { val image = getById(imageId) - if (!image.isActive) throw SodaException("비활성화된 이미지는 수정할 수 없습니다: $imageId") + if (!image.isActive) throw SodaException(messageKey = "chat.character.image.inactive_update") applyTriggers(image, triggers) return image } @@ -159,8 +161,10 @@ class CharacterImageService( val updated = mutableListOf() ids.forEachIndexed { idx, id -> val img = getById(id) - if (img.chatCharacter.id != characterId) throw SodaException("다른 캐릭터의 이미지가 포함되어 있습니다: $id") - if (!img.isActive) throw SodaException("비활성화된 이미지는 순서를 변경할 수 없습니다: $id") + if (img.chatCharacter.id != characterId) { + throw SodaException(messageKey = "chat.character.image.other_character_included") + } + if (!img.isActive) throw SodaException(messageKey = "chat.character.image.inactive_order_change") img.sortOrder = idx + 1 updated.add(img) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt index 1eeaadbb..6321c28d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt @@ -26,7 +26,7 @@ class ChatCharacterBannerService( */ fun getBannerById(bannerId: Long): ChatCharacterBanner { return bannerRepository.findById(bannerId) - .orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } + .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") } } /** @@ -39,10 +39,10 @@ class ChatCharacterBannerService( @Transactional fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner { val character = characterRepository.findById(characterId) - .orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } + .orElseThrow { SodaException(messageKey = "chat.character.not_found") } if (!character.isActive) { - throw SodaException("비활성화된 캐릭터에는 배너를 등록할 수 없습니다: $characterId") + throw SodaException(messageKey = "chat.character.inactive_banner_register") } // 정렬 순서가 지정되지 않은 경우 가장 마지막 순서로 설정 @@ -68,10 +68,10 @@ class ChatCharacterBannerService( @Transactional fun updateBanner(bannerId: Long, imagePath: String? = null, characterId: Long? = null): ChatCharacterBanner { val banner = bannerRepository.findById(bannerId) - .orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } + .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") } if (!banner.isActive) { - throw SodaException("비활성화된 배너는 수정할 수 없습니다: $bannerId") + throw SodaException(messageKey = "chat.character.banner.inactive_update") } // 이미지 경로 변경 @@ -82,10 +82,10 @@ class ChatCharacterBannerService( // 캐릭터 변경 if (characterId != null) { val character = characterRepository.findById(characterId) - .orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } + .orElseThrow { SodaException(messageKey = "chat.character.not_found") } if (!character.isActive) { - throw SodaException("비활성화된 캐릭터로는 변경할 수 없습니다: $characterId") + throw SodaException(messageKey = "chat.character.inactive_banner_change") } banner.chatCharacter = character @@ -100,7 +100,7 @@ class ChatCharacterBannerService( @Transactional fun deleteBanner(bannerId: Long) { val banner = bannerRepository.findById(bannerId) - .orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } + .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") } banner.isActive = false bannerRepository.save(banner) @@ -119,10 +119,10 @@ class ChatCharacterBannerService( for (index in ids.indices) { val banner = bannerRepository.findById(ids[index]) - .orElseThrow { SodaException("배너를 찾을 수 없습니다: ${ids[index]}") } + .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") } if (!banner.isActive) { - throw SodaException("비활성화된 배너는 수정할 수 없습니다: ${ids[index]}") + throw SodaException(messageKey = "chat.character.banner.inactive_update") } banner.sortOrder = index + 1 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt index faec7b04..60974827 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt @@ -702,7 +702,7 @@ class ChatCharacterService( ): ChatCharacter { // 캐릭터 조회 val chatCharacter = findById(request.id) - ?: throw kr.co.vividnext.sodalive.common.SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}") + ?: throw kr.co.vividnext.sodalive.common.SodaException(messageKey = "chat.character.not_found") // isActive가 false이면 isActive = false, name = "inactive_$name"으로 변경하고 나머지는 반영하지 않는다. if (request.isActive != null && !request.isActive) { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt index 0cfbdef2..ce5232b2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt @@ -126,8 +126,8 @@ class OriginalWorkController( @PathVariable id: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val ow = queryService.getOriginalWork(id) val chars = queryService.getActiveCharactersPage(id, page = 0, size = 20).content diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt index c32f3d3d..998cada9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt @@ -44,7 +44,7 @@ class OriginalWorkQueryService( @Transactional(readOnly = true) fun getOriginalWork(id: Long): OriginalWork { return originalWorkRepository.findByIdAndIsDeletedFalse(id) - .orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } + .orElseThrow { SodaException(messageKey = "chat.original.not_found") } } /** @@ -54,7 +54,7 @@ class OriginalWorkQueryService( fun getActiveCharactersPage(originalWorkId: Long, page: Int = 0, size: Int = 20): Page { // 원작 존재 및 소프트 삭제 여부 확인 originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) - .orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } + .orElseThrow { SodaException(messageKey = "chat.original.not_found") } val safePage = if (page < 0) 0 else page val safeSize = when { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/ChatQuotaController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/ChatQuotaController.kt index c82a2810..01bc064d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/ChatQuotaController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/ChatQuotaController.kt @@ -32,8 +32,8 @@ class ChatQuotaController( fun getMyQuota( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ): ApiResponse = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val s = chatQuotaService.getStatus(member.id!!) ApiResponse.ok(ChatQuotaStatusResponse(s.totalRemaining, s.nextRechargeAtEpochMillis)) @@ -44,9 +44,9 @@ class ChatQuotaController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @RequestBody request: ChatQuotaPurchaseRequest ): ApiResponse = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") - if (request.container.isBlank()) throw SodaException("container를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") + if (request.container.isBlank()) throw SodaException(messageKey = "chat.quota.container_required") // 30캔 차감 처리 (결제 기록 남김) canPaymentService.spendCan( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaController.kt index 0fed1d01..c1d8f543 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaController.kt @@ -52,27 +52,27 @@ class ChatRoomQuotaController( @PathVariable chatRoomId: Long, @RequestBody req: PurchaseRoomQuotaRequest ): ApiResponse = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") - if (req.container.isBlank()) throw SodaException("잘못된 접근입니다") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") + if (req.container.isBlank()) throw SodaException(messageKey = "chat.room.quota.invalid_access") val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") // 내 참여 여부 확인 participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + ?: throw SodaException(messageKey = "chat.room.quota.invalid_access") // 캐릭터 참여자 확인(유효한 AI 캐릭터 방인지 체크 및 characterId 기본값 보조) val characterParticipant = participantRepository .findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER) - ?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") + ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room") val character = characterParticipant.character - ?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") + ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room") val characterId = character.id - ?: throw SodaException("잘못된 요청입니다. 캐릭터 정보를 확인해주세요.") + ?: throw SodaException(messageKey = "chat.room.quota.character_required") // 서비스에서 결제 포함하여 처리 val status = chatRoomQuotaService.purchase( @@ -98,20 +98,20 @@ class ChatRoomQuotaController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @PathVariable chatRoomId: Long ): ApiResponse = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") // 내 참여 여부 확인 participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + ?: throw SodaException(messageKey = "chat.room.quota.invalid_access") // 캐릭터 확인 val characterParticipant = participantRepository .findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER) - ?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") + ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room") val character = characterParticipant.character - ?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") + ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room") // 글로벌 Lazy refill val globalStatus = chatQuotaService.getStatus(member.id!!) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaService.kt index cb9021d6..b6ba430d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaService.kt @@ -75,7 +75,7 @@ class ChatRoomQuotaService( val now = Instant.now() val nowMillis = now.toEpochMilli() val quota = repo.findForUpdate(memberId, chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") // 충전 시간이 지났다면 무료 10으로 리셋하고 next=null if (quota.nextRechargeAt != null && quota.nextRechargeAt!! <= nowMillis) { @@ -98,7 +98,7 @@ class ChatRoomQuotaService( val globalFree = globalFreeProvider() if (globalFree <= 0) { // 전송 차단: 글로벌 무료가 0이며 유료도 0 → 전송 불가 - throw SodaException("오늘의 무료 채팅이 모두 소진되었습니다. 내일 다시 이용해 주세요.") + throw SodaException(messageKey = "chat.room.quota.global_free_exhausted") } if (quota.remainingFree <= 0) { // 전송 차단: 룸 무료가 0이며 유료도 0 → 전송 불가 @@ -107,7 +107,7 @@ class ChatRoomQuotaService( quota.nextRechargeAt = now.plus(Duration.ofHours(6)).toEpochMilli() } - throw SodaException("무료 채팅이 모두 소진되었습니다.") + throw SodaException(messageKey = "chat.room.quota.room_free_exhausted") } // 둘 다 가능 → 차감 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt index 7434207b..d2b80fbb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt @@ -42,8 +42,8 @@ class ChatRoomController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @RequestBody request: CreateChatRoomRequest ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val response = chatRoomService.createOrGetChatRoom(member, request.characterId) ApiResponse.ok(response) @@ -77,8 +77,8 @@ class ChatRoomController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @PathVariable chatRoomId: Long ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val isActive = chatRoomService.isMyRoomSessionActive(member, chatRoomId) ApiResponse.ok(isActive) @@ -95,8 +95,8 @@ class ChatRoomController( @PathVariable chatRoomId: Long, @RequestParam(required = false) characterImageId: Long? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val response = chatRoomService.enterChatRoom(member, chatRoomId, characterImageId) ApiResponse.ok(response) @@ -114,8 +114,8 @@ class ChatRoomController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @PathVariable chatRoomId: Long ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") chatRoomService.leaveChatRoom(member, chatRoomId) ApiResponse.ok(true) @@ -134,8 +134,8 @@ class ChatRoomController( @RequestParam(defaultValue = "20") limit: Int, @RequestParam(required = false) cursor: Long? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val response = chatRoomService.getChatMessages(member, chatRoomId, cursor, limit) ApiResponse.ok(response) @@ -153,8 +153,8 @@ class ChatRoomController( @PathVariable chatRoomId: Long, @RequestBody request: SendChatMessageRequest ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") if (request.message.isBlank()) { ApiResponse.error() @@ -176,8 +176,8 @@ class ChatRoomController( @PathVariable messageId: Long, @RequestBody request: ChatMessagePurchaseRequest ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val result = chatRoomService.purchaseMessage(member, chatRoomId, messageId, request.container) ApiResponse.ok(result) @@ -195,8 +195,8 @@ class ChatRoomController( @PathVariable chatRoomId: Long, @RequestBody request: ChatRoomResetRequest ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") + if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required") val response = chatRoomService.resetChatRoom(member, chatRoomId, request.container) ApiResponse.ok(response) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt index 3a47432c..4219c26c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt @@ -29,6 +29,7 @@ import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.member.Member import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value @@ -54,6 +55,7 @@ class ChatRoomService( private val characterService: ChatCharacterService, private val characterImageService: CharacterImageService, private val langContext: LangContext, + private val messageSource: SodaMessageSource, private val aiCharacterTranslationRepository: AiCharacterTranslationRepository, private val canPaymentService: kr.co.vividnext.sodalive.can.payment.CanPaymentService, private val imageCloudFront: kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront, @@ -77,19 +79,19 @@ class ChatRoomService( @Transactional fun purchaseMessage(member: Member, chatRoomId: Long, messageId: Long, container: String): ChatMessageItemDto { val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") // 참여 여부 검증 participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + ?: throw SodaException(messageKey = "chat.room.invalid_access") val message = messageRepository.findById(messageId).orElseThrow { - SodaException("메시지를 찾을 수 없습니다.") + SodaException(messageKey = "chat.message.not_found") } - if (!message.isActive) throw SodaException("비활성화된 메시지입니다.") - if (message.chatRoom.id != room.id) throw SodaException("잘못된 접근입니다") + if (!message.isActive) throw SodaException(messageKey = "chat.message.inactive") + if (message.chatRoom.id != room.id) throw SodaException(messageKey = "chat.room.invalid_access") - val price = message.price ?: throw SodaException("구매할 수 없는 메시지입니다.") - if (price <= 0) throw SodaException("구매 가격이 잘못되었습니다.") + val price = message.price ?: throw SodaException(messageKey = "chat.message.not_purchasable") + if (price <= 0) throw SodaException(messageKey = "chat.purchase.invalid_price") // 이미지 메시지인 경우: 이미 소유했다면 결제 생략하고 DTO 반환 if (message.messageType == ChatMessageType.IMAGE) { @@ -124,7 +126,7 @@ class ChatRoomService( fun createOrGetChatRoom(member: Member, characterId: Long): CreateChatRoomResponse { // 1. 캐릭터 조회 val character = characterService.findById(characterId) - ?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: $characterId") + ?: throw SodaException(messageKey = "chat.room.character_not_found") // 2. 이미 참여 중인 채팅방이 있는지 확인 val existingChatRoom = chatRoomRepository.findActiveChatRoomByMemberAndCharacter(member, character) @@ -225,21 +227,21 @@ class ChatRoomService( // success가 false이면 throw if (!apiResponse.success) { - throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") + throw SodaException(messageKey = "chat.room.create_failed_retry") } // success가 true이면 파라미터로 넘긴 값과 일치하는지 확인 - val data = apiResponse.data ?: throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") + val data = apiResponse.data ?: throw SodaException(messageKey = "chat.room.create_failed_retry") if (data.userId != userId && data.character.id != characterUUID && data.status != "active") { - throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") + throw SodaException(messageKey = "chat.room.create_failed_retry") } // 세션 ID 반환 return data.sessionId } catch (e: Exception) { log.error(e.message) - throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") + throw SodaException(messageKey = "chat.room.create_failed_retry") } } @@ -264,7 +266,7 @@ class ChatRoomService( } } else { if (latest?.message.isNullOrBlank() && latest?.characterImage != null) { - "[이미지]" + messageSource.getMessage("chat.room.last_message_image", langContext.lang).orEmpty() } else { "" } @@ -304,11 +306,19 @@ class ChatRoomService( val now = LocalDateTime.now() val duration = Duration.between(time, now) val seconds = duration.seconds - if (seconds <= 60) return "방금" + if (seconds <= 60) { + return messageSource.getMessage("chat.room.time.just_now", langContext.lang).orEmpty() + } val minutes = duration.toMinutes() - if (minutes < 60) return "${minutes}분 전" + if (minutes < 60) { + val template = messageSource.getMessage("chat.room.time.minutes_ago", langContext.lang).orEmpty() + return String.format(template, minutes) + } val hours = duration.toHours() - if (hours < 24) return "${hours}시간 전" + if (hours < 24) { + val template = messageSource.getMessage("chat.room.time.hours_ago", langContext.lang).orEmpty() + return String.format(template, hours) + } // 그 외: 날짜 (yyyy-MM-dd) return time.toLocalDate().toString() } @@ -510,23 +520,23 @@ class ChatRoomService( // success가 false이면 throw if (!apiResponse.success) { - throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") + throw SodaException(messageKey = "chat.error.retry") } val status = apiResponse.data?.status return status == "active" } catch (e: Exception) { e.printStackTrace() - throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") + throw SodaException(messageKey = "chat.error.retry") } } @Transactional fun leaveChatRoom(member: Member, chatRoomId: Long, throwOnSessionEndFailure: Boolean = false) { val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") val participant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + ?: throw SodaException(messageKey = "chat.room.invalid_access") // 1) 나가기 처리 participant.isActive = false @@ -589,10 +599,9 @@ class ChatRoomService( } } // 최종 실패 처리 - val message = "채팅방 세션 종료에 실패했습니다. 다시 시도해 주세요." if (throwOnFailure) { log.error("[chat] 외부 세션 종료 최종 실패(예외 전파): sessionId={}, attempts={}", sessionId, maxAttempts) - throw SodaException(message) + throw SodaException(messageKey = "chat.room.session_end_failed") } else { log.error("[chat] 외부 세션 종료 최종 실패: sessionId={}, attempts={}", sessionId, maxAttempts) } @@ -601,9 +610,9 @@ class ChatRoomService( @Transactional(readOnly = true) fun getChatMessages(member: Member, chatRoomId: Long, cursor: Long?, limit: Int = 20): ChatMessagesPageResponse { val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + ?: throw SodaException(messageKey = "chat.room.invalid_access") val pageable = PageRequest.of(0, limit) val fetched = if (cursor != null) { @@ -636,18 +645,18 @@ class ChatRoomService( fun sendMessage(member: Member, chatRoomId: Long, message: String): SendChatMessageResponse { // 1) 방 존재 확인 val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") // 2) 참여 여부 확인 (USER) val myParticipant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + ?: throw SodaException(messageKey = "chat.room.invalid_access") // 3) 캐릭터 참여자 조회 val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue( room, ParticipantType.CHARACTER - ) ?: throw SodaException("잘못된 접근입니다") + ) ?: throw SodaException(messageKey = "chat.room.invalid_access") val character = characterParticipant.character - ?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "chat.error.retry") // 4) 외부 API 호출 준비 val userId = generateUserId(member.id!!) @@ -833,7 +842,7 @@ class ChatRoomService( } } log.error("[chat] 외부 채팅 전송 최종 실패 attempts={}", maxAttempts) - throw SodaException("메시지 전송을 실패했습니다.") + throw SodaException(messageKey = "chat.message.send_failed") } private fun callExternalApiForChatSend( @@ -875,12 +884,12 @@ class ChatRoomService( ) if (!apiResponse.success) { - throw SodaException("메시지 전송을 실패했습니다.") + throw SodaException(messageKey = "chat.message.send_failed") } - val data = apiResponse.data ?: throw SodaException("메시지 전송을 실패했습니다.") + val data = apiResponse.data ?: throw SodaException(messageKey = "chat.message.send_failed") val characterContent = data.characterResponse.content if (characterContent.isBlank()) { - throw SodaException("메시지 전송을 실패했습니다.") + throw SodaException(messageKey = "chat.message.send_failed") } return characterContent } @@ -903,16 +912,16 @@ class ChatRoomService( fun resetChatRoom(member: Member, chatRoomId: Long, container: String): CreateChatRoomResponse { // 0) 방 존재 및 내 참여 여부 확인 val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") + ?: throw SodaException(messageKey = "chat.error.room_not_found") participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + ?: throw SodaException(messageKey = "chat.room.invalid_access") // 1) AI 캐릭터 채팅방인지 확인 (CHARACTER 타입의 활성 참여자 존재 확인) val characterParticipant = participantRepository .findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER) - ?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") + ?: throw SodaException(messageKey = "chat.room.not_ai_room") val character = characterParticipant.character - ?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") + ?: throw SodaException(messageKey = "chat.room.not_ai_room") // 2) 30캔 결제 (채팅방 초기화 전용 CanUsage 사용) canPaymentService.spendCan( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt index 3dd61bbc..16624887 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt @@ -20,6 +20,11 @@ class SodaMessageSource { Lang.EN to "Please check your login information.", Lang.JA to "ログイン情報を確認してください。" ), + "common.error.adult_verification_required" to mapOf( + Lang.KO to "본인인증을 하셔야 합니다.", + Lang.EN to "Identity verification is required.", + Lang.JA to "本人認証が必要です。" + ), "common.error.max_upload_size" to mapOf( Lang.KO to "파일용량은 최대 1024MB까지 저장할 수 있습니다.", Lang.EN to "The file size can be saved up to 1024MB.", @@ -1662,6 +1667,245 @@ class SodaMessageSource { ) ) + private val chatCharacterCommentMessages = mapOf( + "chat.character.comment.required" to mapOf( + Lang.KO to "댓글 내용을 입력해주세요.", + Lang.EN to "Please enter a comment.", + Lang.JA to "コメント内容を入力してください。" + ), + "chat.character.comment.deleted" to mapOf( + Lang.KO to "댓글이 삭제되었습니다.", + Lang.EN to "The comment has been deleted.", + Lang.JA to "コメントが削除されました。" + ), + "chat.character.comment.reported" to mapOf( + Lang.KO to "신고가 접수되었습니다.", + Lang.EN to "Your report has been received.", + Lang.JA to "通報が受け付けられました。" + ), + "chat.character.comment.invalid" to mapOf( + Lang.KO to "유효하지 않은 댓글입니다.", + Lang.EN to "Invalid comment.", + Lang.JA to "無効なコメントです。" + ), + "chat.character.comment.not_found" to mapOf( + Lang.KO to "댓글을 찾을 수 없습니다.", + Lang.EN to "Comment not found.", + Lang.JA to "コメントが見つかりません。" + ), + "chat.character.comment.inactive" to mapOf( + Lang.KO to "비활성화된 댓글입니다.", + Lang.EN to "This comment is inactive.", + Lang.JA to "無効化されたコメントです。" + ), + "chat.character.comment.delete_forbidden" to mapOf( + Lang.KO to "삭제 권한이 없습니다.", + Lang.EN to "You do not have permission to delete.", + Lang.JA to "削除権限がありません。" + ), + "chat.character.comment.report_content_required" to mapOf( + Lang.KO to "신고 내용을 입력해주세요.", + Lang.EN to "Please enter a report message.", + Lang.JA to "通報内容を入力してください。" + ) + ) + + private val chatCharacterMessages = mapOf( + "chat.character.not_found" to mapOf( + Lang.KO to "캐릭터를 찾을 수 없습니다.", + Lang.EN to "Character not found.", + Lang.JA to "キャラクターが見つかりません。" + ), + "chat.character.inactive" to mapOf( + Lang.KO to "비활성화된 캐릭터입니다.", + Lang.EN to "This character is inactive.", + Lang.JA to "無効化されたキャラクターです。" + ), + "chat.character.inactive_image_register" to mapOf( + Lang.KO to "비활성화된 캐릭터에는 이미지를 등록할 수 없습니다.", + Lang.EN to "Images cannot be registered for an inactive character.", + Lang.JA to "無効化されたキャラクターには画像を登録できません。" + ), + "chat.character.inactive_banner_register" to mapOf( + Lang.KO to "비활성화된 캐릭터에는 배너를 등록할 수 없습니다.", + Lang.EN to "Banners cannot be registered for an inactive character.", + Lang.JA to "無効化されたキャラクターにはバナーを登録できません。" + ), + "chat.character.inactive_banner_change" to mapOf( + Lang.KO to "비활성화된 캐릭터로는 변경할 수 없습니다.", + Lang.EN to "You cannot change to an inactive character.", + Lang.JA to "無効化されたキャラクターには変更できません。" + ) + ) + + private val chatCharacterImageMessages = mapOf( + "chat.character.image.not_found" to mapOf( + Lang.KO to "캐릭터 이미지를 찾을 수 없습니다.", + Lang.EN to "Character image not found.", + Lang.JA to "キャラクター画像が見つかりません。" + ), + "chat.character.image.inactive" to mapOf( + Lang.KO to "비활성화된 이미지입니다.", + Lang.EN to "This image is inactive.", + Lang.JA to "無効化された画像です。" + ), + "chat.character.image.min_price" to mapOf( + Lang.KO to "가격은 0 can 이상이어야 합니다.", + Lang.EN to "Price must be at least 0 can.", + Lang.JA to "価格は0can以上である必要があります。" + ), + "chat.character.image.inactive_update" to mapOf( + Lang.KO to "비활성화된 이미지는 수정할 수 없습니다.", + Lang.EN to "Inactive images cannot be updated.", + Lang.JA to "無効化された画像は修正できません。" + ), + "chat.character.image.other_character_included" to mapOf( + Lang.KO to "다른 캐릭터의 이미지가 포함되어 있습니다.", + Lang.EN to "Images from another character are included.", + Lang.JA to "別のキャラクターの画像が含まれています。" + ), + "chat.character.image.inactive_order_change" to mapOf( + Lang.KO to "비활성화된 이미지는 순서를 변경할 수 없습니다.", + Lang.EN to "Inactive images cannot change order.", + Lang.JA to "無効化された画像の順序は変更できません。" + ) + ) + + private val chatCharacterBannerMessages = mapOf( + "chat.character.banner.not_found" to mapOf( + Lang.KO to "배너를 찾을 수 없습니다.", + Lang.EN to "Banner not found.", + Lang.JA to "バナーが見つかりません。" + ), + "chat.character.banner.inactive_update" to mapOf( + Lang.KO to "비활성화된 배너는 수정할 수 없습니다.", + Lang.EN to "Inactive banners cannot be updated.", + Lang.JA to "無効化されたバナーは修正できません。" + ) + ) + + private val chatOriginalWorkMessages = mapOf( + "chat.original.not_found" to mapOf( + Lang.KO to "해당 원작을 찾을 수 없습니다.", + Lang.EN to "Original work not found.", + Lang.JA to "該当する原作が見つかりません。" + ) + ) + + private val chatQuotaMessages = mapOf( + "chat.quota.container_required" to mapOf( + Lang.KO to "container를 확인해주세요.", + Lang.EN to "Please check the container.", + Lang.JA to "containerを確認してください。" + ) + ) + + private val chatRoomQuotaMessages = mapOf( + "chat.room.quota.invalid_access" to mapOf( + Lang.KO to "잘못된 접근입니다", + Lang.EN to "Invalid access.", + Lang.JA to "不正なアクセスです。" + ), + "chat.room.quota.not_ai_room" to mapOf( + Lang.KO to "AI 캐릭터 채팅방이 아닙니다.", + Lang.EN to "This is not an AI character chat room.", + Lang.JA to "AIキャラクターのチャットルームではありません。" + ), + "chat.room.quota.character_required" to mapOf( + Lang.KO to "잘못된 요청입니다. 캐릭터 정보를 확인해주세요.", + Lang.EN to "Invalid request. Please check the character information.", + Lang.JA to "不正なリクエストです。キャラクター情報を確認してください。" + ), + "chat.room.quota.global_free_exhausted" to mapOf( + Lang.KO to "오늘의 무료 채팅이 모두 소진되었습니다. 내일 다시 이용해 주세요.", + Lang.EN to "Today's free chats have been used up. Please try again tomorrow.", + Lang.JA to "本日の無料チャットはすべて使い切りました。明日またご利用ください。" + ), + "chat.room.quota.room_free_exhausted" to mapOf( + Lang.KO to "무료 채팅이 모두 소진되었습니다.", + Lang.EN to "Free chats have been used up.", + Lang.JA to "無料チャットはすべて使い切りました。" + ) + ) + + private val chatRoomMessages = mapOf( + "chat.room.invalid_access" to mapOf( + Lang.KO to "잘못된 접근입니다", + Lang.EN to "Invalid access.", + Lang.JA to "不正なアクセスです。" + ), + "chat.room.not_ai_room" to mapOf( + Lang.KO to "AI 캐릭터 채팅방이 아닙니다.", + Lang.EN to "This is not an AI character chat room.", + Lang.JA to "AIキャラクターのチャットルームではありません。" + ), + "chat.message.not_found" to mapOf( + Lang.KO to "메시지를 찾을 수 없습니다.", + Lang.EN to "Message not found.", + Lang.JA to "メッセージが見つかりません。" + ), + "chat.message.inactive" to mapOf( + Lang.KO to "비활성화된 메시지입니다.", + Lang.EN to "This message is inactive.", + Lang.JA to "無効化されたメッセージです。" + ), + "chat.message.not_purchasable" to mapOf( + Lang.KO to "구매할 수 없는 메시지입니다.", + Lang.EN to "This message cannot be purchased.", + Lang.JA to "購入できないメッセージです。" + ), + "chat.purchase.invalid_price" to mapOf( + Lang.KO to "구매 가격이 잘못되었습니다.", + Lang.EN to "Invalid purchase price.", + Lang.JA to "購入価格が正しくありません。" + ), + "chat.room.character_not_found" to mapOf( + Lang.KO to "해당 ID의 캐릭터를 찾을 수 없습니다.", + Lang.EN to "Character not found for the given ID.", + Lang.JA to "該当IDのキャラクターが見つかりません。" + ), + "chat.room.create_failed_retry" to mapOf( + Lang.KO to "채팅방 생성에 실패했습니다. 다시 시도해 주세요.", + Lang.EN to "Failed to create the chat room. Please try again.", + Lang.JA to "チャットルームの作成に失敗しました。もう一度お試しください。" + ), + "chat.error.retry" to mapOf( + Lang.KO to "오류가 발생했습니다. 다시 시도해 주세요.", + Lang.EN to "An error occurred. Please try again.", + Lang.JA to "エラーが発生しました。もう一度お試しください。" + ), + "chat.room.session_end_failed" to mapOf( + Lang.KO to "채팅방 세션 종료에 실패했습니다. 다시 시도해 주세요.", + Lang.EN to "Failed to end the chat room session. Please try again.", + Lang.JA to "チャットルームのセッション終了に失敗しました。もう一度お試しください。" + ), + "chat.message.send_failed" to mapOf( + Lang.KO to "메시지 전송을 실패했습니다.", + Lang.EN to "Failed to send the message.", + Lang.JA to "メッセージの送信に失敗しました。" + ), + "chat.room.last_message_image" to mapOf( + Lang.KO to "[이미지]", + Lang.EN to "[Image]", + Lang.JA to "[画像]" + ), + "chat.room.time.just_now" to mapOf( + Lang.KO to "방금", + Lang.EN to "Just now", + Lang.JA to "たった今" + ), + "chat.room.time.minutes_ago" to mapOf( + Lang.KO to "%d분 전", + Lang.EN to "%d minutes ago", + Lang.JA to "%d分前" + ), + "chat.room.time.hours_ago" to mapOf( + Lang.KO to "%d시간 전", + Lang.EN to "%d hours ago", + Lang.JA to "%d時間前" + ) + ) + private val creatorCommunityMessages = mapOf( "creator.community.paid_post_image_required" to mapOf( Lang.KO to "유료 게시글 등록을 위해서는 이미지가 필요합니다.", @@ -1772,6 +2016,14 @@ class SodaMessageSource { creatorAdminContentMessages, creatorAdminSeriesRequestMessages, creatorAdminSeriesMessages, + chatCharacterCommentMessages, + chatCharacterMessages, + chatCharacterImageMessages, + chatCharacterBannerMessages, + chatOriginalWorkMessages, + chatQuotaMessages, + chatRoomQuotaMessages, + chatRoomMessages, creatorCommunityMessages ) for (messages in messageGroups) {