feat(content-comment): 콘텐츠 댓글/후원 시 언어 코드가 null인 경우 파파고 언어 감지 API를 호출하는 기능 추가
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.content
|
package kr.co.vividnext.sodalive.content
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
@@ -15,11 +16,18 @@ import org.springframework.util.LinkedMultiValueMap
|
|||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 오디오 콘텐츠 메타데이터(제목/내용/태그)를 기반으로 파파고 언어 감지를 요청하기 위한 이벤트.
|
* 텍스트 기반 데이터(콘텐츠, 댓글 등)에 대해 파파고 언어 감지를 요청하기 위한 이벤트.
|
||||||
*/
|
*/
|
||||||
|
enum class LanguageDetectTargetType {
|
||||||
|
CONTENT,
|
||||||
|
COMMENT
|
||||||
|
}
|
||||||
|
|
||||||
class LanguageDetectEvent(
|
class LanguageDetectEvent(
|
||||||
val contentId: Long,
|
val contentId: Long? = null,
|
||||||
val query: String
|
val query: String,
|
||||||
|
val targetType: LanguageDetectTargetType = LanguageDetectTargetType.CONTENT,
|
||||||
|
val commentId: Long? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PapagoLanguageDetectResponse(
|
data class PapagoLanguageDetectResponse(
|
||||||
@@ -29,6 +37,7 @@ data class PapagoLanguageDetectResponse(
|
|||||||
@Component
|
@Component
|
||||||
class LanguageDetectListener(
|
class LanguageDetectListener(
|
||||||
private val audioContentRepository: AudioContentRepository,
|
private val audioContentRepository: AudioContentRepository,
|
||||||
|
private val audioContentCommentRepository: AudioContentCommentRepository,
|
||||||
|
|
||||||
@Value("\${cloud.naver.papago-client-id}")
|
@Value("\${cloud.naver.papago-client-id}")
|
||||||
private val papagoClientId: String,
|
private val papagoClientId: String,
|
||||||
@@ -48,13 +57,26 @@ class LanguageDetectListener(
|
|||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
fun detectLanguage(event: LanguageDetectEvent) {
|
fun detectLanguage(event: LanguageDetectEvent) {
|
||||||
if (event.query.isBlank()) {
|
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
|
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) {
|
if (audioContent == null) {
|
||||||
log.warn("[PapagoLanguageDetect] AudioContent not found. contentId={}", event.contentId)
|
log.warn("[PapagoLanguageDetect] AudioContent not found. contentId={}", contentId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,13 +84,63 @@ class LanguageDetectListener(
|
|||||||
if (!audioContent.languageCode.isNullOrBlank()) {
|
if (!audioContent.languageCode.isNullOrBlank()) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"[PapagoLanguageDetect] languageCode already set. Skip language detection. contentId={}, languageCode={}",
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. contentId={}, languageCode={}",
|
||||||
event.contentId,
|
contentId,
|
||||||
audioContent.languageCode
|
audioContent.languageCode
|
||||||
)
|
)
|
||||||
return
|
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 {
|
val headers = HttpHeaders().apply {
|
||||||
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
set("X-NCP-APIGW-API-KEY-ID", papagoClientId)
|
set("X-NCP-APIGW-API-KEY-ID", papagoClientId)
|
||||||
@@ -76,8 +148,8 @@ class LanguageDetectListener(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val body = LinkedMultiValueMap<String, String>().apply {
|
val body = LinkedMultiValueMap<String, String>().apply {
|
||||||
// 파파고 스펙에 따라 query 파라미터에 제목/내용/태그를 공백으로 구분하여 전달
|
// 파파고 스펙에 따라 query 파라미터에 텍스트를 공백으로 구분하여 전달
|
||||||
add("query", event.query)
|
add("query", query)
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestEntity = HttpEntity(body, headers)
|
val requestEntity = HttpEntity(body, headers)
|
||||||
@@ -90,32 +162,31 @@ class LanguageDetectListener(
|
|||||||
|
|
||||||
if (!response.statusCode.is2xxSuccessful) {
|
if (!response.statusCode.is2xxSuccessful) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"[PapagoLanguageDetect] Non-success status from Papago. status={}, contentId={}",
|
"[PapagoLanguageDetect] Non-success status from Papago. status={}, targetId={}",
|
||||||
response.statusCode,
|
response.statusCode,
|
||||||
event.contentId
|
targetIdForLog
|
||||||
)
|
)
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val langCode = response.body?.langCode?.takeIf { it.isNotBlank() }
|
val langCode = response.body?.langCode?.takeIf { it.isNotBlank() }
|
||||||
if (langCode == null) {
|
if (langCode == null) {
|
||||||
log.warn("[PapagoLanguageDetect] langCode is null or blank in Papago response. contentId={}", event.contentId)
|
log.warn(
|
||||||
return
|
"[PapagoLanguageDetect] langCode is null or blank in Papago response. targetId={}",
|
||||||
|
targetIdForLog
|
||||||
|
)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
audioContent.languageCode = langCode
|
langCode
|
||||||
|
|
||||||
// REQUIRES_NEW 트랜잭션 내에서 변경 내용을 저장한다.
|
|
||||||
audioContentRepository.save(audioContent)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"[PapagoLanguageDetect] languageCode updated from Papago. contentId={}, langCode={}",
|
|
||||||
event.contentId,
|
|
||||||
langCode
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
} 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.content.comment
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
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.content.order.OrderRepository
|
||||||
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
||||||
import kr.co.vividnext.sodalive.fcm.FcmEventType
|
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!!
|
return savedContentComment.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.can.use.CanUsage
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
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.AudioContentComment
|
||||||
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@@ -14,7 +17,8 @@ import org.springframework.transaction.annotation.Transactional
|
|||||||
class AudioContentDonationService(
|
class AudioContentDonationService(
|
||||||
private val canPaymentService: CanPaymentService,
|
private val canPaymentService: CanPaymentService,
|
||||||
private val queryRepository: AudioContentRepository,
|
private val queryRepository: AudioContentRepository,
|
||||||
private val commentRepository: AudioContentCommentRepository
|
private val commentRepository: AudioContentCommentRepository,
|
||||||
|
private val applicationEventPublisher: ApplicationEventPublisher
|
||||||
) {
|
) {
|
||||||
@Transactional
|
@Transactional
|
||||||
fun donation(request: AudioContentDonationRequest, member: Member) {
|
fun donation(request: AudioContentDonationRequest, member: Member) {
|
||||||
@@ -39,6 +43,18 @@ class AudioContentDonationService(
|
|||||||
)
|
)
|
||||||
audioContentComment.audioContent = audioContent
|
audioContentComment.audioContent = audioContent
|
||||||
audioContentComment.member = member
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user