파파고 번역 API 연동

This commit is contained in:
2025-12-11 16:34:22 +09:00
parent fdac55ebdf
commit 304c001a27
3 changed files with 178 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
package kr.co.vividnext.sodalive.i18n.translation
/**
* Papago 번역 API 응답 예시
*
* ```json
* {
* "message": {
* "result": {
* "srcLangType": "ko",
* "tarLangType": "en",
* "translatedText": "Hello, I like to eat apple while riding a bicycle."
* }
* }
* }
* ```
*/
/**
* 위 JSON 구조에 대응하는 최상위 응답 모델
*/
data class PapagoTranslationResponse(
val message: Message
) {
/**
* message 필드 내부 구조
*/
data class Message(
val result: Result
)
/**
* 실제 번역 결과 데이터
*/
data class Result(
val srcLangType: String,
val tarLangType: String,
val translatedText: String
)
}

View File

@@ -0,0 +1,127 @@
package kr.co.vividnext.sodalive.i18n.translation
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
@Service
class PapagoTranslationService(
@Value("\${cloud.naver.papago-client-id}")
private val papagoClientId: String,
@Value("\${cloud.naver.papago-client-secret}")
private val papagoClientSecret: String
) {
private val restTemplate: RestTemplate = RestTemplate()
private val papagoTranslateUrl = "https://papago.apigw.ntruss.com/nmt/v1/translation"
fun translate(request: TranslateRequest): TranslateResult {
if (request.texts.isEmpty()) {
return TranslateResult(emptyList())
}
validateLanguages(request.sourceLanguage, request.targetLanguage)
val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_JSON
set("X-NCP-APIGW-API-KEY-ID", papagoClientId)
set("X-NCP-APIGW-API-KEY", papagoClientSecret)
}
val chunks = chunkTexts(request.texts)
val translatedTexts = mutableListOf<String>()
chunks.forEach { chunk ->
val textsInChunkCount = chunk.split(S3P_DELIMITER).size
try {
val body = mapOf(
"source" to request.sourceLanguage,
"target" to request.targetLanguage,
"text" to chunk
)
val requestEntity = HttpEntity(body, headers)
val response = restTemplate.postForEntity(
papagoTranslateUrl,
requestEntity,
PapagoTranslationResponse::class.java
)
if (!response.statusCode.is2xxSuccessful) {
return@forEach
}
val translated = response.body?.message?.result?.translatedText
if (translated.isNullOrBlank()) {
repeat(textsInChunkCount) { translatedTexts.add("") }
} else {
translated.split(S3P_DELIMITER).forEach { translatedTexts.add(it) }
}
} catch (_: Exception) {
repeat(textsInChunkCount) { translatedTexts.add("") }
}
}
return TranslateResult(translatedTexts)
}
private fun validateLanguages(sourceLanguage: String, targetLanguage: String) {
requireSupportedLanguage(sourceLanguage)
requireSupportedLanguage(targetLanguage)
}
private fun requireSupportedLanguage(language: String) {
val normalized = language.lowercase()
if (!SUPPORTED_LANGUAGE_CODES.contains(normalized)) {
throw IllegalArgumentException("지원하지 않는 언어 코드입니다: $language")
}
}
private fun chunkTexts(texts: List<String>): List<String> {
if (texts.isEmpty()) return emptyList()
val chunks = mutableListOf<String>()
var startIndex = 0
while (startIndex < texts.size) {
var endIndex = texts.size
while (endIndex > startIndex) {
val candidate = texts.subList(startIndex, endIndex)
val joined = candidate.joinToString(S3P_DELIMITER)
if (joined.length <= MAX_TEXT_LENGTH || endIndex - startIndex == 1) {
chunks.add(joined)
startIndex = endIndex
break
}
endIndex--
}
}
return chunks
}
companion object {
private val SUPPORTED_LANGUAGE_CODES = setOf(
"ko",
"en",
"ja",
"zh-cn",
"zh-tw",
"es",
"fr",
"vi",
"th",
"id",
"de",
"ru",
"pt",
"it"
)
private const val S3P_DELIMITER = "__S3P__"
private const val MAX_TEXT_LENGTH = 3000
}
}

View File

@@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.i18n.translation
data class TranslateRequest(
val texts: List<String>,
val sourceLanguage: String,
val targetLanguage: String
)
data class TranslateResult(
val translatedText: List<String>
)