채팅 메시지 다국어 분리

This commit is contained in:
2025-12-23 18:38:54 +09:00
parent 6e8a88178c
commit 9d619450ef
15 changed files with 420 additions and 144 deletions

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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!!,

View File

@@ -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<String>
): 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<String>): 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<CharacterImage>()
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)
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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<ChatCharacter> {
// 원작 존재 및 소프트 삭제 여부 확인
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
.orElseThrow { SodaException(messageKey = "chat.original.not_found") }
val safePage = if (page < 0) 0 else page
val safeSize = when {

View File

@@ -32,8 +32,8 @@ class ChatQuotaController(
fun getMyQuota(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
): ApiResponse<ChatQuotaStatusResponse> = 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<ChatQuotaStatusResponse> = 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(

View File

@@ -52,27 +52,27 @@ class ChatRoomQuotaController(
@PathVariable chatRoomId: Long,
@RequestBody req: PurchaseRoomQuotaRequest
): ApiResponse<PurchaseRoomQuotaResponse> = 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<RoomQuotaStatusResponse> = 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!!)

View File

@@ -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")
}
// 둘 다 가능 → 차감

View File

@@ -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)

View File

@@ -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(

View File

@@ -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) {