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