파파고 번역 API 연동
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
Reference in New Issue
Block a user