395 lines
14 KiB
Kotlin
395 lines
14 KiB
Kotlin
package kr.co.vividnext.sodalive.content
|
|
|
|
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
|
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
|
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
|
import kr.co.vividnext.sodalive.content.series.ContentSeriesRepository
|
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository
|
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
|
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
|
import org.slf4j.LoggerFactory
|
|
import org.springframework.beans.factory.annotation.Value
|
|
import org.springframework.context.ApplicationEventPublisher
|
|
import org.springframework.data.repository.findByIdOrNull
|
|
import org.springframework.http.HttpEntity
|
|
import org.springframework.http.HttpHeaders
|
|
import org.springframework.http.MediaType
|
|
import org.springframework.scheduling.annotation.Async
|
|
import org.springframework.stereotype.Component
|
|
import org.springframework.transaction.annotation.Propagation
|
|
import org.springframework.transaction.annotation.Transactional
|
|
import org.springframework.transaction.event.TransactionPhase
|
|
import org.springframework.transaction.event.TransactionalEventListener
|
|
import org.springframework.util.LinkedMultiValueMap
|
|
import org.springframework.web.client.RestTemplate
|
|
|
|
/**
|
|
* 텍스트 기반 데이터(콘텐츠, 댓글 등)에 대해 파파고 언어 감지를 요청하기 위한 이벤트.
|
|
*/
|
|
enum class LanguageDetectTargetType {
|
|
CONTENT,
|
|
COMMENT,
|
|
CHARACTER,
|
|
CHARACTER_COMMENT,
|
|
CREATOR_CHEERS,
|
|
SERIES,
|
|
ORIGINAL_WORK
|
|
}
|
|
|
|
class LanguageDetectEvent(
|
|
val id: Long,
|
|
val query: String,
|
|
val targetType: LanguageDetectTargetType = LanguageDetectTargetType.CONTENT
|
|
)
|
|
|
|
data class PapagoLanguageDetectResponse(
|
|
val langCode: String?
|
|
)
|
|
|
|
@Component
|
|
class LanguageDetectListener(
|
|
private val audioContentRepository: AudioContentRepository,
|
|
private val audioContentCommentRepository: AudioContentCommentRepository,
|
|
private val chatCharacterRepository: ChatCharacterRepository,
|
|
private val characterCommentRepository: CharacterCommentRepository,
|
|
private val creatorCheersRepository: CreatorCheersRepository,
|
|
private val seriesRepository: ContentSeriesRepository,
|
|
private val originalWorkRepository: OriginalWorkRepository,
|
|
|
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
|
|
|
@Value("\${cloud.naver.papago-client-id}")
|
|
private val papagoClientId: String,
|
|
|
|
@Value("\${cloud.naver.papago-client-secret}")
|
|
private val papagoClientSecret: String
|
|
) {
|
|
|
|
private val log = LoggerFactory.getLogger(LanguageDetectListener::class.java)
|
|
|
|
private val restTemplate: RestTemplate = RestTemplate()
|
|
|
|
private val papagoDetectUrl = "https://papago.apigw.ntruss.com/langs/v1/dect"
|
|
|
|
@Async
|
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
|
fun detectLanguage(event: LanguageDetectEvent) {
|
|
if (event.query.isBlank()) {
|
|
log.debug("[PapagoLanguageDetect] query is blank. Skip language detection. event={}", event)
|
|
return
|
|
}
|
|
|
|
when (event.targetType) {
|
|
LanguageDetectTargetType.CONTENT -> handleContentLanguageDetect(event)
|
|
LanguageDetectTargetType.COMMENT -> handleCommentLanguageDetect(event)
|
|
LanguageDetectTargetType.CHARACTER -> handleCharacterLanguageDetect(event)
|
|
LanguageDetectTargetType.CHARACTER_COMMENT -> handleCharacterCommentLanguageDetect(event)
|
|
LanguageDetectTargetType.CREATOR_CHEERS -> handleCreatorCheersLanguageDetect(event)
|
|
LanguageDetectTargetType.SERIES -> handleSeriesLanguageDetect(event)
|
|
LanguageDetectTargetType.ORIGINAL_WORK -> handleOriginalWorkLanguageDetect(event)
|
|
}
|
|
}
|
|
|
|
private fun handleCharacterLanguageDetect(event: LanguageDetectEvent) {
|
|
val characterId = event.id
|
|
|
|
val character = chatCharacterRepository.findById(characterId).orElse(null)
|
|
if (character == null) {
|
|
log.warn("[PapagoLanguageDetect] ChatCharacter not found. characterId={}", characterId)
|
|
return
|
|
}
|
|
|
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
|
if (!character.languageCode.isNullOrBlank()) {
|
|
log.debug(
|
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. characterId={}, languageCode={}",
|
|
characterId,
|
|
character.languageCode
|
|
)
|
|
return
|
|
}
|
|
|
|
val langCode = requestPapagoLanguageCode(event.query, characterId) ?: return
|
|
|
|
character.languageCode = langCode
|
|
chatCharacterRepository.save(character)
|
|
|
|
applicationEventPublisher.publishEvent(
|
|
LanguageTranslationEvent(
|
|
id = characterId,
|
|
targetType = LanguageTranslationTargetType.CHARACTER
|
|
)
|
|
)
|
|
|
|
log.info(
|
|
"[PapagoLanguageDetect] languageCode updated from Papago. characterId={}, langCode={}",
|
|
characterId,
|
|
langCode
|
|
)
|
|
}
|
|
|
|
private fun handleContentLanguageDetect(event: LanguageDetectEvent) {
|
|
val contentId = event.id
|
|
|
|
val audioContent = audioContentRepository.findById(contentId).orElse(null)
|
|
if (audioContent == null) {
|
|
log.warn("[PapagoLanguageDetect] AudioContent not found. contentId={}", contentId)
|
|
return
|
|
}
|
|
|
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
|
if (!audioContent.languageCode.isNullOrBlank()) {
|
|
log.debug(
|
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. contentId={}, languageCode={}",
|
|
contentId,
|
|
audioContent.languageCode
|
|
)
|
|
return
|
|
}
|
|
|
|
val langCode = requestPapagoLanguageCode(event.query, contentId) ?: return
|
|
|
|
audioContent.languageCode = langCode
|
|
|
|
// REQUIRES_NEW 트랜잭션 내에서 변경 내용을 저장한다.
|
|
audioContentRepository.save(audioContent)
|
|
|
|
applicationEventPublisher.publishEvent(
|
|
LanguageTranslationEvent(
|
|
id = contentId,
|
|
targetType = LanguageTranslationTargetType.CONTENT
|
|
)
|
|
)
|
|
|
|
log.info(
|
|
"[PapagoLanguageDetect] languageCode updated from Papago. contentId={}, langCode={}",
|
|
contentId,
|
|
langCode
|
|
)
|
|
}
|
|
|
|
private fun handleCommentLanguageDetect(event: LanguageDetectEvent) {
|
|
val commentId = event.id
|
|
|
|
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 handleCharacterCommentLanguageDetect(event: LanguageDetectEvent) {
|
|
val commentId = event.id
|
|
|
|
val comment = characterCommentRepository.findById(commentId).orElse(null)
|
|
if (comment == null) {
|
|
log.warn("[PapagoLanguageDetect] CharacterComment not found. commentId={}", commentId)
|
|
return
|
|
}
|
|
|
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
|
if (!comment.languageCode.isNullOrBlank()) {
|
|
log.debug(
|
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. " +
|
|
"characterCommentId={}, languageCode={}",
|
|
commentId,
|
|
comment.languageCode
|
|
)
|
|
return
|
|
}
|
|
|
|
val langCode = requestPapagoLanguageCode(event.query, commentId) ?: return
|
|
|
|
comment.languageCode = langCode
|
|
characterCommentRepository.save(comment)
|
|
|
|
log.info(
|
|
"[PapagoLanguageDetect] languageCode updated from Papago. characterCommentId={}, langCode={}",
|
|
commentId,
|
|
langCode
|
|
)
|
|
}
|
|
|
|
private fun handleCreatorCheersLanguageDetect(event: LanguageDetectEvent) {
|
|
val cheersId = event.id
|
|
|
|
val cheers = creatorCheersRepository.findById(cheersId).orElse(null)
|
|
if (cheers == null) {
|
|
log.warn("[PapagoLanguageDetect] CreatorCheers not found. cheersId={}", cheersId)
|
|
return
|
|
}
|
|
|
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
|
if (!cheers.languageCode.isNullOrBlank()) {
|
|
log.debug(
|
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. cheersId={}, languageCode={}",
|
|
cheersId,
|
|
cheers.languageCode
|
|
)
|
|
return
|
|
}
|
|
|
|
val langCode = requestPapagoLanguageCode(event.query, cheersId) ?: return
|
|
|
|
cheers.languageCode = langCode
|
|
creatorCheersRepository.save(cheers)
|
|
|
|
log.info(
|
|
"[PapagoLanguageDetect] languageCode updated from Papago. cheersId={}, langCode={}",
|
|
cheersId,
|
|
langCode
|
|
)
|
|
}
|
|
|
|
private fun handleSeriesLanguageDetect(event: LanguageDetectEvent) {
|
|
val seriesId = event.id
|
|
|
|
val series = seriesRepository.findByIdOrNull(seriesId)
|
|
if (series == null) {
|
|
log.warn("[PapagoLanguageDetect] Series not found. seriesId={}", seriesId)
|
|
return
|
|
}
|
|
|
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
|
if (!series.languageCode.isNullOrBlank()) {
|
|
log.debug(
|
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. seriesId={}, languageCode={}",
|
|
seriesId,
|
|
series.languageCode
|
|
)
|
|
return
|
|
}
|
|
|
|
val langCode = requestPapagoLanguageCode(event.query, seriesId) ?: return
|
|
|
|
series.languageCode = langCode
|
|
seriesRepository.save(series)
|
|
|
|
applicationEventPublisher.publishEvent(
|
|
LanguageTranslationEvent(
|
|
id = seriesId,
|
|
targetType = LanguageTranslationTargetType.SERIES
|
|
)
|
|
)
|
|
|
|
log.info(
|
|
"[PapagoLanguageDetect] languageCode updated from Papago. seriesId={}, langCode={}",
|
|
seriesId,
|
|
langCode
|
|
)
|
|
}
|
|
|
|
private fun handleOriginalWorkLanguageDetect(event: LanguageDetectEvent) {
|
|
val originalWorkId = event.id
|
|
|
|
val originalWork = originalWorkRepository.findByIdOrNull(originalWorkId)
|
|
if (originalWork == null) {
|
|
log.warn("[PapagoLanguageDetect] OriginalWork not found. originalWorkId={}", originalWorkId)
|
|
return
|
|
}
|
|
|
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
|
if (!originalWork.languageCode.isNullOrBlank()) {
|
|
log.debug(
|
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. originalWorkId={}, languageCode={}",
|
|
originalWorkId,
|
|
originalWork.languageCode
|
|
)
|
|
return
|
|
}
|
|
|
|
val langCode = requestPapagoLanguageCode(event.query, originalWorkId) ?: return
|
|
|
|
originalWork.languageCode = langCode
|
|
originalWorkRepository.save(originalWork)
|
|
|
|
// 언어 감지가 완료된 후 언어 번역 이벤트 호출
|
|
applicationEventPublisher.publishEvent(
|
|
LanguageTranslationEvent(
|
|
id = originalWorkId,
|
|
targetType = LanguageTranslationTargetType.ORIGINAL_WORK
|
|
)
|
|
)
|
|
|
|
log.info(
|
|
"[PapagoLanguageDetect] languageCode updated from Papago. originalWorkId={}, langCode={}",
|
|
originalWorkId,
|
|
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)
|
|
set("X-NCP-APIGW-API-KEY", papagoClientSecret)
|
|
}
|
|
|
|
val body = LinkedMultiValueMap<String, String>().apply {
|
|
// 파파고 스펙에 따라 query 파라미터에 텍스트를 공백으로 구분하여 전달
|
|
add("query", query)
|
|
}
|
|
|
|
val requestEntity = HttpEntity(body, headers)
|
|
|
|
val response = restTemplate.postForEntity(
|
|
papagoDetectUrl,
|
|
requestEntity,
|
|
PapagoLanguageDetectResponse::class.java
|
|
)
|
|
|
|
if (!response.statusCode.is2xxSuccessful) {
|
|
log.warn(
|
|
"[PapagoLanguageDetect] Non-success status from Papago. status={}, targetId={}",
|
|
response.statusCode,
|
|
targetIdForLog
|
|
)
|
|
return null
|
|
}
|
|
|
|
val langCode = response.body?.langCode?.takeIf { it.isNotBlank() }
|
|
if (langCode == null) {
|
|
log.warn(
|
|
"[PapagoLanguageDetect] langCode is null or blank in Papago response. targetId={}",
|
|
targetIdForLog
|
|
)
|
|
return null
|
|
}
|
|
|
|
langCode
|
|
} catch (ex: Exception) {
|
|
// 언어 감지는 부가 기능이므로, 실패 시 예외를 전파하지 않고 로그만 남긴다.
|
|
log.error(
|
|
"[PapagoLanguageDetect] Failed to detect language via Papago. targetId={}",
|
|
targetIdForLog,
|
|
ex
|
|
)
|
|
null
|
|
}
|
|
}
|
|
}
|