채팅 메시지 다국어 분리

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

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