채팅 메시지 다국어 분리
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!!,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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!!)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
// 둘 다 가능 → 차감
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user