From da9b89a6cf969984e44ac2c546170e40f63a1992 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 25 Nov 2025 16:03:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(content-comment):=20=EC=BD=98=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EB=8C=93=EA=B8=80/=ED=9B=84=EC=9B=90=20=EC=8B=9C?= =?UTF-8?q?=20=EC=96=B8=EC=96=B4=20=EC=BD=94=EB=93=9C=EA=B0=80=20null?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=ED=8C=8C=ED=8C=8C=EA=B3=A0=20?= =?UTF-8?q?=EC=96=B8=EC=96=B4=20=EA=B0=90=EC=A7=80=20API=EB=A5=BC=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/content/LanguageDetectEvent.kt | 123 ++++++++++++++---- .../comment/AudioContentCommentService.kt | 13 ++ .../donation/AudioContentDonationService.kt | 20 ++- 3 files changed, 128 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt index d34c2ee..4321b00 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.content +import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpEntity @@ -15,11 +16,18 @@ import org.springframework.util.LinkedMultiValueMap import org.springframework.web.client.RestTemplate /** - * 오디오 콘텐츠 메타데이터(제목/내용/태그)를 기반으로 파파고 언어 감지를 요청하기 위한 이벤트. + * 텍스트 기반 데이터(콘텐츠, 댓글 등)에 대해 파파고 언어 감지를 요청하기 위한 이벤트. */ +enum class LanguageDetectTargetType { + CONTENT, + COMMENT +} + class LanguageDetectEvent( - val contentId: Long, - val query: String + val contentId: Long? = null, + val query: String, + val targetType: LanguageDetectTargetType = LanguageDetectTargetType.CONTENT, + val commentId: Long? = null ) data class PapagoLanguageDetectResponse( @@ -29,6 +37,7 @@ data class PapagoLanguageDetectResponse( @Component class LanguageDetectListener( private val audioContentRepository: AudioContentRepository, + private val audioContentCommentRepository: AudioContentCommentRepository, @Value("\${cloud.naver.papago-client-id}") private val papagoClientId: String, @@ -48,13 +57,26 @@ class LanguageDetectListener( @Transactional(propagation = Propagation.REQUIRES_NEW) fun detectLanguage(event: LanguageDetectEvent) { if (event.query.isBlank()) { - log.debug("[PapagoLanguageDetect] query is blank. Skip language detection. contentId={}", event.contentId) + log.debug("[PapagoLanguageDetect] query is blank. Skip language detection. event={}", event) return } - val audioContent = audioContentRepository.findById(event.contentId).orElse(null) + when (event.targetType) { + LanguageDetectTargetType.CONTENT -> handleContentLanguageDetect(event) + LanguageDetectTargetType.COMMENT -> handleCommentLanguageDetect(event) + } + } + + private fun handleContentLanguageDetect(event: LanguageDetectEvent) { + val contentId = event.contentId + if (contentId == null) { + log.warn("[PapagoLanguageDetect] contentId is null for CONTENT target. event={}", event) + return + } + + val audioContent = audioContentRepository.findById(contentId).orElse(null) if (audioContent == null) { - log.warn("[PapagoLanguageDetect] AudioContent not found. contentId={}", event.contentId) + log.warn("[PapagoLanguageDetect] AudioContent not found. contentId={}", contentId) return } @@ -62,13 +84,63 @@ class LanguageDetectListener( if (!audioContent.languageCode.isNullOrBlank()) { log.debug( "[PapagoLanguageDetect] languageCode already set. Skip language detection. contentId={}, languageCode={}", - event.contentId, + contentId, audioContent.languageCode ) return } - try { + val langCode = requestPapagoLanguageCode(event.query, contentId) ?: return + + audioContent.languageCode = langCode + + // REQUIRES_NEW 트랜잭션 내에서 변경 내용을 저장한다. + audioContentRepository.save(audioContent) + + log.info( + "[PapagoLanguageDetect] languageCode updated from Papago. contentId={}, langCode={}", + contentId, + langCode + ) + } + + private fun handleCommentLanguageDetect(event: LanguageDetectEvent) { + val commentId = event.commentId + if (commentId == null) { + log.warn("[PapagoLanguageDetect] commentId is null for COMMENT target. event={}", event) + return + } + + val comment = audioContentCommentRepository.findById(commentId).orElse(null) + if (comment == null) { + log.warn("[PapagoLanguageDetect] AudioContentComment not found. commentId={}", commentId) + return + } + + // 이미 언어 코드가 설정된 경우 호출하지 않음 + if (!comment.languageCode.isNullOrBlank()) { + log.debug( + "[PapagoLanguageDetect] languageCode already set. Skip language detection. commentId={}, languageCode={}", + commentId, + comment.languageCode + ) + return + } + + val langCode = requestPapagoLanguageCode(event.query, commentId) ?: return + + comment.languageCode = langCode + audioContentCommentRepository.save(comment) + + log.info( + "[PapagoLanguageDetect] languageCode updated from Papago. commentId={}, langCode={}", + commentId, + langCode + ) + } + + private fun requestPapagoLanguageCode(query: String, targetIdForLog: Long): String? { + return try { val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_FORM_URLENCODED set("X-NCP-APIGW-API-KEY-ID", papagoClientId) @@ -76,8 +148,8 @@ class LanguageDetectListener( } val body = LinkedMultiValueMap().apply { - // 파파고 스펙에 따라 query 파라미터에 제목/내용/태그를 공백으로 구분하여 전달 - add("query", event.query) + // 파파고 스펙에 따라 query 파라미터에 텍스트를 공백으로 구분하여 전달 + add("query", query) } val requestEntity = HttpEntity(body, headers) @@ -90,32 +162,31 @@ class LanguageDetectListener( if (!response.statusCode.is2xxSuccessful) { log.warn( - "[PapagoLanguageDetect] Non-success status from Papago. status={}, contentId={}", + "[PapagoLanguageDetect] Non-success status from Papago. status={}, targetId={}", response.statusCode, - event.contentId + targetIdForLog ) - return + return null } val langCode = response.body?.langCode?.takeIf { it.isNotBlank() } if (langCode == null) { - log.warn("[PapagoLanguageDetect] langCode is null or blank in Papago response. contentId={}", event.contentId) - return + log.warn( + "[PapagoLanguageDetect] langCode is null or blank in Papago response. targetId={}", + targetIdForLog + ) + return null } - audioContent.languageCode = langCode - - // REQUIRES_NEW 트랜잭션 내에서 변경 내용을 저장한다. - audioContentRepository.save(audioContent) - - log.info( - "[PapagoLanguageDetect] languageCode updated from Papago. contentId={}, langCode={}", - event.contentId, - langCode - ) + langCode } catch (ex: Exception) { // 언어 감지는 부가 기능이므로, 실패 시 예외를 전파하지 않고 로그만 남긴다. - log.error("[PapagoLanguageDetect] Failed to detect language via Papago. contentId={}", event.contentId, ex) + log.error( + "[PapagoLanguageDetect] Failed to detect language via Papago. targetId={}", + targetIdForLog, + ex + ) + null } } } 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 cb5b196..28f3717 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 @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.content.comment import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.content.LanguageDetectEvent +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 @@ -86,6 +88,17 @@ class AudioContentCommentService( ) ) + // 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다. + if (languageCode.isNullOrBlank()) { + applicationEventPublisher.publishEvent( + LanguageDetectEvent( + commentId = savedContentComment.id!!, + query = comment, + targetType = LanguageDetectTargetType.COMMENT + ) + ) + } + return savedContentComment.id!! } 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 f92634d..26cc8d6 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 @@ -4,9 +4,12 @@ import kr.co.vividnext.sodalive.can.payment.CanPaymentService import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.content.LanguageDetectEvent +import kr.co.vividnext.sodalive.content.LanguageDetectTargetType import kr.co.vividnext.sodalive.content.comment.AudioContentComment import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository import kr.co.vividnext.sodalive.member.Member +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -14,7 +17,8 @@ import org.springframework.transaction.annotation.Transactional class AudioContentDonationService( private val canPaymentService: CanPaymentService, private val queryRepository: AudioContentRepository, - private val commentRepository: AudioContentCommentRepository + private val commentRepository: AudioContentCommentRepository, + private val applicationEventPublisher: ApplicationEventPublisher ) { @Transactional fun donation(request: AudioContentDonationRequest, member: Member) { @@ -39,6 +43,18 @@ class AudioContentDonationService( ) audioContentComment.audioContent = audioContent audioContentComment.member = member - commentRepository.save(audioContentComment) + + val savedComment = commentRepository.save(audioContentComment) + + // 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다. + if (request.languageCode.isNullOrBlank()) { + applicationEventPublisher.publishEvent( + LanguageDetectEvent( + commentId = savedComment.id!!, + query = request.comment, + targetType = LanguageDetectTargetType.COMMENT + ) + ) + } } }