From e987a56544f469e2e784e2c04d849df3dce85ef8 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 23 Dec 2025 19:03:38 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/AudioContentController.kt | 26 +-- .../sodalive/content/AudioContentService.kt | 104 ++++++---- .../content/category/CategoryController.kt | 10 +- .../content/category/CategoryService.kt | 8 +- .../comment/AudioContentCommentController.kt | 8 +- .../comment/AudioContentCommentService.kt | 23 ++- .../AudioContentDonationController.kt | 2 +- .../donation/AudioContentDonationService.kt | 6 +- .../main/AudioContentMainController.kt | 14 +- .../sodalive/i18n/SodaMessageSource.kt | 178 ++++++++++++++++++ 10 files changed, 303 insertions(+), 76 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt index 5cddb914..46fd18da 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt @@ -36,7 +36,7 @@ class AudioContentController(private val service: AudioContentService) { @RequestPart("request") requestString: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.createAudioContent( @@ -57,7 +57,7 @@ class AudioContentController(private val service: AudioContentService) { @RequestPart("request") requestString: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.modifyAudioContent( @@ -74,7 +74,7 @@ class AudioContentController(private val service: AudioContentService) { @RequestBody request: UploadCompleteRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.uploadComplete( @@ -91,7 +91,7 @@ class AudioContentController(private val service: AudioContentService) { @PathVariable id: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.deleteAudioContent( @@ -111,7 +111,7 @@ class AudioContentController(private val service: AudioContentService) { @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getAudioContentList( @@ -134,7 +134,7 @@ class AudioContentController(private val service: AudioContentService) { @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getDetail( @@ -151,7 +151,7 @@ class AudioContentController(private val service: AudioContentService) { @PathVariable id: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.generateUrl(contentId = id, member = member)) } @@ -160,7 +160,7 @@ class AudioContentController(private val service: AudioContentService) { @RequestBody request: AddAllPlaybackTrackingRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.addAllPlaybackTracking(request, member)) } @@ -170,7 +170,7 @@ class AudioContentController(private val service: AudioContentService) { @RequestBody request: PutAudioContentLikeRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.audioContentLike(request, member)) } @@ -179,7 +179,7 @@ class AudioContentController(private val service: AudioContentService) { fun getAudioContentRankingSort( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getContentRankingSortTypeList()) } @@ -221,7 +221,7 @@ class AudioContentController(private val service: AudioContentService) { @PathVariable id: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.pinToTheTop(contentId = id, member = member)) } @@ -232,7 +232,7 @@ class AudioContentController(private val service: AudioContentService) { @PathVariable id: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.unpinAtTheTop(contentId = id, member = member)) } @@ -248,7 +248,7 @@ class AudioContentController(private val service: AudioContentService) { @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getLatestContentByTheme( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index 15be2f77..8c1eb9d9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -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 { - 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) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryController.kt index 0906e5f4..05aba076 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryController.kt @@ -24,7 +24,7 @@ class CategoryController(private val service: CategoryService) { @RequestBody request: CreateCategoryRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.createCategory(request = request, member = member)) } @@ -35,7 +35,7 @@ class CategoryController(private val service: CategoryService) { @RequestBody request: ModifyCategoryRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.modifyCategory(request = request, member = member)) } @@ -46,7 +46,7 @@ class CategoryController(private val service: CategoryService) { @RequestBody request: UpdateCategoryOrdersRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.updateCategoryOrders(request = request, member = member)) } @@ -57,7 +57,7 @@ class CategoryController(private val service: CategoryService) { @PathVariable("id") categoryId: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.deleteCategory(categoryId = categoryId, member = member)) } @@ -67,7 +67,7 @@ class CategoryController(private val service: CategoryService) { @RequestParam creatorId: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getCategoryList(creatorId = creatorId, memberId = member.id!!)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryService.kt index 1fc15860..04291ffb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryService.kt @@ -66,7 +66,7 @@ class CategoryService( @Transactional fun modifyCategory(request: ModifyCategoryRequest, member: Member) { val category = repository.findByIdAndMemberId(categoryId = request.categoryId, memberId = member.id!!) - ?: throw SodaException("잘못된 요청입니다.") + ?: throw SodaException(messageKey = "common.error.invalid_request") if (!request.title.isNullOrBlank()) { validateTitle(title = request.title) @@ -108,7 +108,7 @@ class CategoryService( @Transactional fun deleteCategory(categoryId: Long, member: Member) { val category = repository.findByIdAndMemberId(categoryId = categoryId, memberId = member.id!!) - ?: throw SodaException("잘못된 요청입니다.") + ?: throw SodaException(messageKey = "common.error.invalid_request") category.isActive = false categoryContentRepository.deleteByCategoryId(categoryId = categoryId) @@ -128,7 +128,7 @@ class CategoryService( @Transactional fun getCategoryList(creatorId: Long, memberId: Long): List { val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) - if (isBlocked) throw SodaException("잘못된 접근입니다.") + if (isBlocked) throw SodaException(messageKey = "category.error.invalid_access") // 기본 카테고리 목록 조회 (원본 언어 기준) val baseList = repository.findByCreatorId(creatorId = creatorId) @@ -205,6 +205,6 @@ class CategoryService( } private fun validateTitle(title: String) { - if (title.length < 2) throw SodaException("카테고리명은 2글자 이상 입력하세요") + if (title.length < 2) throw SodaException(messageKey = "category.error.title_min_length") } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt index ad91270e..9a6e56cd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt @@ -25,7 +25,7 @@ class AudioContentCommentController( @RequestBody request: RegisterCommentRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") val commentId = service.registerComment( comment = request.comment, @@ -62,7 +62,7 @@ class AudioContentCommentController( @RequestBody request: ModifyCommentRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.modifyComment(request = request, member = member)) } @@ -74,7 +74,7 @@ class AudioContentCommentController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getCommentList( @@ -93,7 +93,7 @@ class AudioContentCommentController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ): ApiResponse { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") return ApiResponse.ok( service.getCommentReplyList( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt index c8663a28..3746b449 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt @@ -7,6 +7,8 @@ import kr.co.vividnext.sodalive.content.LanguageDetectTargetType import kr.co.vividnext.sodalive.content.order.OrderRepository 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.member.Member import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import org.springframework.beans.factory.annotation.Value @@ -24,6 +26,8 @@ class AudioContentCommentService( private val audioContentRepository: AudioContentRepository, private val applicationEventPublisher: ApplicationEventPublisher, private val orderRepository: OrderRepository, + private val messageSource: SodaMessageSource, + private val langContext: LangContext, @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String @@ -38,11 +42,13 @@ class AudioContentCommentService( languageCode: String? ): Long { val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId) - ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + ?: throw SodaException(messageKey = "content.error.invalid_content_retry") val creator = audioContent.member!! val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creator.id!!) - if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 댓글쓰기가 제한됩니다.") + if (isBlocked) { + throw SodaException(formatMessage("content.comment.error.blocked_by_creator", creator.nickname)) + } val (isExistsAudioContent, _) = orderRepository.isExistOrderedAndOrderType( memberId = member.id!!, @@ -50,7 +56,7 @@ class AudioContentCommentService( ) if (isSecret && !isExistsAudioContent) { - throw SodaException("콘텐츠 구매 후 비밀댓글을 등록할 수 있습니다.") + throw SodaException(messageKey = "content.comment.error.secret_requires_purchase") } val audioContentComment = AudioContentComment(comment = comment, languageCode = languageCode, isSecret = isSecret) @@ -78,9 +84,9 @@ class AudioContentCommentService( member.nickname }, message = if (parent != null) { - "댓글에 답글을 달았습니다.: ${audioContent.title}" + formatMessage("content.comment.notification.reply", audioContent.title) } else { - "콘텐츠에 댓글을 달았습니다.: ${audioContent.title}" + formatMessage("content.comment.notification.new", audioContent.title) }, contentId = audioContentId, commentParentId = parentId, @@ -105,7 +111,7 @@ class AudioContentCommentService( @Transactional fun modifyComment(request: ModifyCommentRequest, member: Member) { val audioContentComment = repository.findByIdOrNull(request.commentId) - ?: throw SodaException("잘못된 접근 입니다.\n확인 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "content.comment.error.invalid_access_retry") if (audioContentComment.member!!.id!! == member.id!!) { if (request.comment != null) { @@ -164,4 +170,9 @@ class AudioContentCommentService( return GetAudioContentCommentListResponse(totalCount, commentList) } + + private fun formatMessage(key: String, vararg args: Any): String { + val template = messageSource.getMessage(key, langContext.lang) ?: return "" + return String.format(template, *args) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt index ebd097a7..f6a0dddf 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt @@ -18,7 +18,7 @@ class AudioContentDonationController(private val service: AudioContentDonationSe @RequestBody request: AudioContentDonationRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.donation(request = request, member = member)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt index 99a4da50..7fc81343 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt @@ -22,11 +22,11 @@ class AudioContentDonationService( ) { @Transactional fun donation(request: AudioContentDonationRequest, member: Member) { - if (request.donationCan < 1) throw SodaException("1캔 이상 후원하실 수 있습니다.") - if (request.comment.isBlank()) throw SodaException("함께 보낼 메시지를 입력하세요.") + if (request.donationCan < 1) throw SodaException(messageKey = "content.donation.error.minimum_can") + if (request.comment.isBlank()) throw SodaException(messageKey = "content.donation.error.comment_required") val audioContent = queryRepository.findByIdAndActive(request.contentId) - ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + ?: throw SodaException(messageKey = "content.error.invalid_content_retry") canPaymentService.spendCan( memberId = member.id!!, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt index c534be5d..df73f542 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt @@ -22,7 +22,7 @@ class AudioContentMainController( fun newContentUploadCreatorList( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getNewContentUploadCreatorList( @@ -36,7 +36,7 @@ class AudioContentMainController( fun getMainBannerList( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getAudioContentMainBannerList( @@ -50,7 +50,7 @@ class AudioContentMainController( fun getMainOrderList( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( orderService.getAudioContentMainOrderList( @@ -68,7 +68,7 @@ class AudioContentMainController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getNewContentByTheme( @@ -87,7 +87,7 @@ class AudioContentMainController( @RequestParam("contentType", required = false) contentType: ContentType? = null, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getThemeList( @@ -105,7 +105,7 @@ class AudioContentMainController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getNewContentFor2WeeksByTheme( @@ -125,7 +125,7 @@ class AudioContentMainController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getAudioContentCurationListWithPaging( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt index 16624887..e67519c3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt @@ -47,6 +47,177 @@ class SodaMessageSource { ) ) + private val contentErrorMessages = mapOf( + "content.error.invalid_content_retry" to mapOf( + Lang.KO to "잘못된 콘텐츠 입니다.\n다시 시도해 주세요.", + Lang.EN to "Invalid content.\nPlease try again.", + Lang.JA to "不正なコンテンツです。\nもう一度お試しください。" + ), + "content.error.cover_image_required" to mapOf( + Lang.KO to "커버이미지를 선택해 주세요.", + Lang.EN to "Please select a cover image.", + Lang.JA to "カバー画像を選択してください。" + ), + "content.error.content_required" to mapOf( + Lang.KO to "콘텐츠를 선택해 주세요.", + Lang.EN to "Please select content.", + Lang.JA to "コンテンツを選択してください。" + ), + "content.error.invalid_theme" to mapOf( + Lang.KO to "잘못된 테마입니다. 다시 선택해 주세요.", + Lang.EN to "Invalid theme. Please select again.", + Lang.JA to "不正なテーマです。もう一度選択してください。" + ), + "content.error.alarm_theme_price_min" to mapOf( + Lang.KO to "알람, 모닝콜, 슬립콜 테마의 콘텐츠는 5캔 이상의 유료콘텐츠로 등록이 가능합니다.", + Lang.EN to "Alarm, Morning Call, and Sleep Call themes require paid content of at least 5 cans.", + Lang.JA to "アラーム、モーニングコール、スリープコールのテーマは5缶以上の有料コンテンツのみ登録できます。" + ), + "content.error.minimum_price" to mapOf( + Lang.KO to "콘텐츠의 최소금액은 5캔 입니다.", + Lang.EN to "The minimum price for content is 5 cans.", + Lang.JA to "コンテンツの最低価格は5缶です。" + ), + "content.error.preview_time_format" to mapOf( + Lang.KO to "미리 듣기 시간 형식은 00:30:00 과 같아야 합니다", + Lang.EN to "Preview time format must be like 00:30:00.", + Lang.JA to "プレビュー時間の形式は00:30:00のようにする必要があります。" + ), + "content.error.preview_time_minimum" to mapOf( + Lang.KO to "미리 듣기의 최소 시간은 15초 입니다.", + Lang.EN to "The minimum preview time is 15 seconds.", + Lang.JA to "プレビューの最小時間は15秒です。" + ), + "content.error.preview_time_both_required" to mapOf( + Lang.KO to "미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다.", + Lang.EN to "You must enter both preview start and end times, or neither.", + Lang.JA to "プレビューの開始時間と終了時間は両方入力するか、両方入力しないでください。" + ), + "content.error.user_not_found" to mapOf( + Lang.KO to "없는 사용자 입니다.", + Lang.EN to "User not found.", + Lang.JA to "ユーザーが見つかりません。" + ), + "content.error.access_restricted_by_creator" to mapOf( + Lang.KO to "%s님의 요청으로 콘텐츠 접근이 제한됩니다.", + Lang.EN to "Access to content is restricted at %s's request.", + Lang.JA to "%sさんの要請によりコンテンツへのアクセスが制限されています。" + ), + "content.error.pin_available_after_open" to mapOf( + Lang.KO to "콘텐츠 오픈 후 채널에 고정이 가능합니다.", + Lang.EN to "You can pin it to the channel after the content is opened.", + Lang.JA to "コンテンツ公開後にチャンネルへ固定できます。" + ) + ) + + private val contentNotificationMessages = mapOf( + "content.notification.upload_complete_title" to mapOf( + Lang.KO to "콘텐츠 등록완료", + Lang.EN to "Content registration complete", + Lang.JA to "コンテンツ登録完了" + ), + "content.notification.uploaded_message" to mapOf( + Lang.KO to "콘텐츠를 업로드 하였습니다. - %s", + Lang.EN to "Content uploaded. - %s", + Lang.JA to "コンテンツをアップロードしました。- %s" + ) + ) + + private val contentFormatMessages = mapOf( + "content.release_date.format" to mapOf( + Lang.KO to "yyyy년 MM월 dd일 HH시 mm분 오픈예정", + Lang.EN to "MMM dd, yyyy HH:mm 'Opens soon'", + Lang.JA to "yyyy年 MM月 dd日 HH時 mm分 公開予定" + ), + "content.ranking.date.start_format" to mapOf( + Lang.KO to "yyyy년 MM월 dd일", + Lang.EN to "MMM dd, yyyy", + Lang.JA to "yyyy年 MM月 dd日" + ), + "content.ranking.date.end_format" to mapOf( + Lang.KO to "MM월 dd일", + Lang.EN to "MMM dd", + Lang.JA to "MM月 dd日" + ) + ) + + private val contentRankingMessages = mapOf( + "content.ranking.sort_type.sales" to mapOf( + Lang.KO to "매출", + Lang.EN to "Sales", + Lang.JA to "売上" + ), + "content.ranking.sort_type.comment" to mapOf( + Lang.KO to "댓글", + Lang.EN to "Comments", + Lang.JA to "コメント" + ), + "content.ranking.sort_type.like" to mapOf( + Lang.KO to "좋아요", + Lang.EN to "Likes", + Lang.JA to "いいね" + ), + "content.ranking.sort_type.donation" to mapOf( + Lang.KO to "후원", + Lang.EN to "Donations", + Lang.JA to "支援" + ) + ) + + private val contentCommentMessages = mapOf( + "content.comment.error.blocked_by_creator" to mapOf( + Lang.KO to "%s님의 요청으로 댓글쓰기가 제한됩니다.", + Lang.EN to "Commenting is restricted at %s's request.", + Lang.JA to "%sさんの要請によりコメントの投稿が制限されています。" + ), + "content.comment.error.secret_requires_purchase" to mapOf( + Lang.KO to "콘텐츠 구매 후 비밀댓글을 등록할 수 있습니다.", + Lang.EN to "You can post a secret comment after purchasing the content.", + Lang.JA to "コンテンツ購入後に秘密コメントを登録できます。" + ), + "content.comment.notification.reply" to mapOf( + Lang.KO to "댓글에 답글을 달았습니다.: %s", + Lang.EN to "Replied to a comment: %s", + Lang.JA to "コメントに返信しました: %s" + ), + "content.comment.notification.new" to mapOf( + Lang.KO to "콘텐츠에 댓글을 달았습니다.: %s", + Lang.EN to "Commented on content: %s", + Lang.JA to "コンテンツにコメントしました: %s" + ), + "content.comment.error.invalid_access_retry" to mapOf( + Lang.KO to "잘못된 접근 입니다.\n확인 후 다시 시도해 주세요.", + Lang.EN to "Invalid access.\nPlease check and try again.", + Lang.JA to "不正なアクセスです。\n確認して再度お試しください。" + ) + ) + + private val contentDonationMessages = mapOf( + "content.donation.error.minimum_can" to mapOf( + Lang.KO to "1캔 이상 후원하실 수 있습니다.", + Lang.EN to "You can donate at least 1 can.", + Lang.JA to "1缶以上寄付できます。" + ), + "content.donation.error.comment_required" to mapOf( + Lang.KO to "함께 보낼 메시지를 입력하세요.", + Lang.EN to "Please enter a message to send.", + Lang.JA to "一緒に送るメッセージを入力してください。" + ) + ) + + private val categoryMessages = mapOf( + "category.error.invalid_access" to mapOf( + Lang.KO to "잘못된 접근입니다.", + Lang.EN to "Invalid access.", + Lang.JA to "不正なアクセスです。" + ), + "category.error.title_min_length" to mapOf( + Lang.KO to "카테고리명은 2글자 이상 입력하세요", + Lang.EN to "Category name must be at least 2 characters.", + Lang.JA to "カテゴリ名は2文字以上入力してください。" + ) + ) + private val alarmMessages = mapOf( "alarm.error.already_purchased" to mapOf( Lang.KO to "이미 구매하셨습니다", @@ -1952,6 +2123,13 @@ class SodaMessageSource { fun getMessage(key: String, lang: Lang): String? { val messageGroups = listOf( commonMessages, + contentErrorMessages, + contentNotificationMessages, + contentFormatMessages, + contentRankingMessages, + contentCommentMessages, + contentDonationMessages, + categoryMessages, alarmMessages, auditionMessages, auditionRequestMessages,