package com.hlab.yanalyst.service; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.LinkedHashMap; import java.util.Map; /** * 자가호스팅 LibreTranslate 연동. 원본 스크립트를 번역해 재가공(내 버전) 초안으로 쓴다. * API: {@code POST /translate {q, source, target}} → {@code {"translatedText": "..."}}. */ @Slf4j @Service @RequiredArgsConstructor public class TranslateService { private final RestTemplate restTemplate; private final ObjectMapper objectMapper; @Value("${translate.base-url:https://h-translate.tolag.shop}") private String baseUrl; /** text 를 source→target 으로 번역해 평문을 반환. source 가 비면 자동감지("auto"). */ public String translate(String text, String source, String target) { if (text == null || text.isBlank()) { return ""; } String apiUrl = baseUrl + "/translate"; try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity> request = new HttpEntity<>(buildRequestBody(text, source, target), headers); ResponseEntity response = restTemplate.postForEntity(apiUrl, request, String.class); if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { throw new RuntimeException("Translate API 실패: " + response.getStatusCode()); } return extractTranslatedText(objectMapper, response.getBody()); } catch (Exception e) { log.error("번역 실패 (source={}, target={}, len={})", source, target, text.length(), e); throw new RuntimeException("번역 실패: " + e.getMessage(), e); } } /** * 여러 텍스트를 한 번에 번역(LibreTranslate 는 q 를 배열로 받으면 정렬 맞춰 배열로 반환). * 세그먼트별 번역에 사용해 입력 순서와 1:1 정렬을 보장한다. */ public java.util.List translateBatch(java.util.List texts, String source, String target) { if (texts == null || texts.isEmpty()) { return java.util.Collections.emptyList(); } String apiUrl = baseUrl + "/translate"; try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity> request = new HttpEntity<>(buildBatchRequestBody(texts, source, target), headers); ResponseEntity response = restTemplate.postForEntity(apiUrl, request, String.class); if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { throw new RuntimeException("Translate API 실패: " + response.getStatusCode()); } return extractTranslatedTexts(objectMapper, response.getBody()); } catch (Exception e) { log.error("배치 번역 실패 (source={}, target={}, n={})", source, target, texts.size(), e); throw new RuntimeException("번역 실패: " + e.getMessage(), e); } } /** LibreTranslate /translate 요청 바디 조립(순수). source 빈값/null → "auto". */ static Map buildRequestBody(String text, String source, String target) { Map body = new LinkedHashMap<>(); body.put("q", text); body.put("source", (source == null || source.isBlank()) ? "auto" : source.trim()); body.put("target", target); body.put("format", "text"); return body; } /** 배치 요청 바디 조립(순수). q 를 배열로 담는다. source 빈값/null → "auto". */ static Map buildBatchRequestBody(java.util.List texts, String source, String target) { Map body = new LinkedHashMap<>(); body.put("q", texts); body.put("source", (source == null || source.isBlank()) ? "auto" : source.trim()); body.put("target", target); body.put("format", "text"); return body; } /** 응답 JSON 에서 translatedText 추출(순수). 필드 없으면 빈 문자열, 파싱 실패 시 예외. */ static String extractTranslatedText(ObjectMapper objectMapper, String json) { try { return objectMapper.readTree(json).path("translatedText").asText(""); } catch (Exception e) { throw new RuntimeException("번역 응답 파싱 실패", e); } } /** 응답의 translatedText 를 리스트로 추출(순수). 배열이면 그대로, 문자열이면 단일 원소. */ static java.util.List extractTranslatedTexts(ObjectMapper objectMapper, String json) { try { com.fasterxml.jackson.databind.JsonNode node = objectMapper.readTree(json).path("translatedText"); java.util.List out = new java.util.ArrayList<>(); if (node.isArray()) { node.forEach(n -> out.add(n.asText(""))); } else { out.add(node.asText("")); } return out; } catch (Exception e) { throw new RuntimeException("번역 응답 파싱 실패", e); } } }