feat(content-comment): 콘텐츠 댓글/후원 시 언어 코드가 null인 경우 파파고 언어 감지 API를 호출하는 기능 추가

This commit is contained in:
2025-11-25 16:03:52 +09:00
parent 5ee5107364
commit da9b89a6cf
3 changed files with 128 additions and 28 deletions

View File

@@ -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<String, String>().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
}
}
}

View File

@@ -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!!
}

View File

@@ -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
)
)
}
}
}