콘텐츠 메시지 다국어 처리

This commit is contained in:
2025-12-23 19:03:38 +09:00
parent 9d619450ef
commit e987a56544
10 changed files with 303 additions and 76 deletions

View File

@@ -31,6 +31,7 @@ import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
@@ -74,6 +75,7 @@ class AudioContentService(
private val audioContentCloudFront: AudioContentCloudFront,
private val applicationEventPublisher: ApplicationEventPublisher,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
@@ -117,7 +119,7 @@ class AudioContentService(
val request = objectMapper.readValue(requestString, ModifyAudioContentRequest::class.java)
val audioContent = repository.findByIdAndCreatorId(request.contentId, member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
if (request.title != null) audioContent.title = request.title
if (request.detail != null) audioContent.detail = request.detail
@@ -189,7 +191,7 @@ class AudioContentService(
@Transactional
fun deleteAudioContent(audioContentId: Long, member: Member) {
val audioContent = repository.findByIdAndCreatorId(audioContentId, member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
audioContent.isActive = false
audioContent.releaseDate = null
@@ -203,7 +205,7 @@ class AudioContentService(
member: Member
): CreateAudioContentResponse {
// coverImage 체크
if (coverImage == null) throw SodaException("커버이미지를 선택해 주세요.")
if (coverImage == null) throw SodaException(messageKey = "content.error.cover_image_required")
// request 내용 파싱
val request = objectMapper.readValue(requestString, CreateAudioContentRequest::class.java)
@@ -222,18 +224,18 @@ class AudioContentService(
// contentFile 체크
if (contentFile == null) {
throw SodaException("콘텐츠를 선택해 주세요.")
throw SodaException(messageKey = "content.error.content_required")
}
// 테마 체크
val theme = themeQueryRepository.findThemeByIdAndActive(id = request.themeId)
?: throw SodaException("잘못된 테마입니다. 다시 선택해 주세요.")
?: throw SodaException(messageKey = "content.error.invalid_theme")
if ((request.themeId == 12L || request.themeId == 13L || request.themeId == 14L) && request.price < 5) {
throw SodaException("알람, 모닝콜, 슬립콜 테마의 콘텐츠는 5캔 이상의 유료콘텐츠로 등록이 가능합니다.")
throw SodaException(messageKey = "content.error.alarm_theme_price_min")
}
if (request.price in 1..4) throw SodaException("콘텐츠의 최소금액은 5캔 입니다.")
if (request.price in 1..4) throw SodaException(messageKey = "content.error.minimum_price")
val isFullDetailVisible = if (request.price >= 50) {
request.isFullDetailVisible
@@ -388,34 +390,34 @@ class AudioContentService(
if (previewStartTime != null && previewEndTime != null) {
val startTimeArray = previewStartTime.split(":")
if (startTimeArray.size != 3) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
throw SodaException(messageKey = "content.error.preview_time_format")
}
for (time in startTimeArray) {
if (time.length != 2) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
throw SodaException(messageKey = "content.error.preview_time_format")
}
}
val endTimeArray = previewEndTime.split(":")
if (endTimeArray.size != 3) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
throw SodaException(messageKey = "content.error.preview_time_format")
}
for (time in endTimeArray) {
if (time.length != 2) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
throw SodaException(messageKey = "content.error.preview_time_format")
}
}
val timeDifference = timeDifference(previewStartTime, previewEndTime)
if (timeDifference < 15000) {
throw SodaException("미리 듣기의 최소 시간은 15초 입니다.")
throw SodaException(messageKey = "content.error.preview_time_minimum")
}
} else {
if (previewStartTime != null || previewEndTime != null) {
throw SodaException("미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다.")
throw SodaException(messageKey = "content.error.preview_time_both_required")
}
}
}
@@ -445,10 +447,10 @@ class AudioContentService(
@Transactional
fun uploadComplete(contentId: Long, content: String, duration: String) {
val keyFileName = content.split("/").last()
if (!keyFileName.startsWith(contentId.toString())) throw SodaException("잘못된 요청입니다.")
if (!keyFileName.startsWith(contentId.toString())) throw SodaException(messageKey = "common.error.invalid_request")
val audioContent = repository.findByIdOrNull(contentId)
?: throw SodaException("잘못된 요청입니다.")
?: throw SodaException(messageKey = "common.error.invalid_request")
audioContent.content = content
audioContent.duration = duration
@@ -456,7 +458,7 @@ class AudioContentService(
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.INDIVIDUAL,
title = "콘텐츠 등록완료",
title = formatMessage("content.notification.upload_complete_title"),
message = audioContent.title,
recipients = listOf(audioContent.member!!.id!!),
isAuth = null,
@@ -471,7 +473,7 @@ class AudioContentService(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult,
contentId = contentId,
creatorId = audioContent.member!!.id,
@@ -483,7 +485,7 @@ class AudioContentService(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult,
contentId = contentId,
creatorId = audioContent.member!!.id,
@@ -505,7 +507,7 @@ class AudioContentService(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult,
contentId = audioContent.id!!,
creatorId = audioContent.member!!.id,
@@ -517,7 +519,7 @@ class AudioContentService(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult,
contentId = audioContent.id!!,
creatorId = audioContent.member!!.id,
@@ -538,12 +540,12 @@ class AudioContentService(
// 오디오 콘텐츠 조회 (content_id, 제목, 내용, 테마, 태그, 19여부, 이미지, 콘텐츠 PATH)
val audioContent = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
// 크리에이터(유저) 정보
val creatorId = audioContent.member!!.id!!
val creator = explorerQueryRepository.getMember(creatorId)
?: throw SodaException("없는 사용자 입니다.")
?: throw SodaException(messageKey = "content.error.user_not_found")
val creatorFollowing = explorerQueryRepository.getCreatorFollowing(
creatorId = creatorId,
@@ -557,7 +559,9 @@ class AudioContentService(
// 차단된 사용자 체크
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
if (isBlocked && !isExistsAudioContent) throw SodaException("${creator.nickname}님의 요청으로 콘텐츠 접근이 제한됩니다.")
if (isBlocked && !isExistsAudioContent) {
throw SodaException(formatMessage("content.error.access_restricted_by_creator", creator.nickname))
}
val orderSequence = if (isExistsAudioContent) {
limitedEditionOrderRepository.getOrderSequence(
@@ -595,7 +599,7 @@ class AudioContentService(
audioContent.releaseDate != null &&
audioContent.releaseDate!! < LocalDateTime.now()
) {
throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
throw SodaException(messageKey = "content.error.invalid_content_retry")
}
// 댓글
@@ -628,11 +632,13 @@ class AudioContentService(
audioContent.releaseDate != null &&
audioContent.releaseDate!! >= LocalDateTime.now()
) {
val releaseDatePattern = messageSource.getMessage("content.release_date.format", langContext.lang)
?: "yyyy년 MM월 dd일 HH시 mm분 오픈예정"
audioContent.releaseDate!!
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.toLocalDateTime()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 오픈예정"))
.format(DateTimeFormatter.ofPattern(releaseDatePattern, langContext.lang.locale))
} else {
null
}
@@ -1114,8 +1120,13 @@ class AudioContentService(
limit: Long,
sortType: String = "매출"
): GetAudioContentRanking {
val startDateFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")
val endDateFormatter = DateTimeFormatter.ofPattern("MM월 dd일")
val normalizedSortType = normalizeRankingSortType(sortType)
val startDatePattern = messageSource.getMessage("content.ranking.date.start_format", langContext.lang)
?: "yyyy년 MM월 dd일"
val endDatePattern = messageSource.getMessage("content.ranking.date.end_format", langContext.lang)
?: "MM월 dd일"
val startDateFormatter = DateTimeFormatter.ofPattern(startDatePattern, langContext.lang.locale)
val endDateFormatter = DateTimeFormatter.ofPattern(endDatePattern, langContext.lang.locale)
val contentRankingItemList = repository
.getAudioContentRanking(
@@ -1126,7 +1137,7 @@ class AudioContentService(
contentType = contentType,
offset = offset,
limit = limit,
sortType = sortType
sortType = normalizedSortType
)
return GetAudioContentRanking(
@@ -1137,16 +1148,19 @@ class AudioContentService(
}
fun getContentRankingSortTypeList(): List<String> {
return listOf("매출", "댓글", "좋아요")
val salesLabel = messageSource.getMessage("content.ranking.sort_type.sales", langContext.lang) ?: "매출"
val commentLabel = messageSource.getMessage("content.ranking.sort_type.comment", langContext.lang) ?: "댓글"
val likeLabel = messageSource.getMessage("content.ranking.sort_type.like", langContext.lang) ?: "좋아요"
return listOf(salesLabel, commentLabel, likeLabel)
}
@Transactional
fun pinToTheTop(contentId: Long, member: Member) {
val audioContent = repository.findByIdAndCreatorId(contentId = contentId, creatorId = member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
if (audioContent.releaseDate != null && audioContent.releaseDate!! >= LocalDateTime.now()) {
throw SodaException("콘텐츠 오픈 후 채널에 고정이 가능합니다.")
throw SodaException(messageKey = "content.error.pin_available_after_open")
}
var pinContent = pinContentRepository.findByContentIdAndMemberId(
@@ -1176,14 +1190,14 @@ class AudioContentService(
val pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId,
memberId = member.id!!
) ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
) ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
pinContent.isActive = false
}
fun generateUrl(contentId: Long, member: Member): GenerateUrlResponse {
val audioContent = repository.findByIdOrNull(contentId)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
val isExistsAudioContent = orderRepository.isExistOrdered(
memberId = member.id!!,
@@ -1312,4 +1326,28 @@ class AudioContentService(
.distinct()
.toList()
}
private fun normalizeRankingSortType(sortType: String?): String {
val trimmed = sortType?.trim().orEmpty()
val internalTypes = setOf("매출", "댓글", "좋아요", "후원")
if (trimmed in internalTypes) return trimmed
val salesLabel = messageSource.getMessage("content.ranking.sort_type.sales", langContext.lang)
val commentLabel = messageSource.getMessage("content.ranking.sort_type.comment", langContext.lang)
val likeLabel = messageSource.getMessage("content.ranking.sort_type.like", langContext.lang)
val donationLabel = messageSource.getMessage("content.ranking.sort_type.donation", langContext.lang)
return when (trimmed) {
salesLabel -> "매출"
commentLabel -> "댓글"
likeLabel -> "좋아요"
donationLabel -> "후원"
else -> "매출"
}
}
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
return String.format(template, *args)
}
}