package com.sb.web.account.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.sb.web.common.exception.ApiException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import java.util.Base64; import java.util.List; import java.util.Map; /** * Google Cloud Vision 영수증 OCR. 이미지(byte) → 전체 텍스트. * API 키는 환경변수(GOOGLE_VISION_API_KEY)로만 주입하며, 프론트엔드엔 노출하지 않는다. */ @Slf4j @Service public class VisionOcrService { private static final String ENDPOINT = "https://vision.googleapis.com/v1/images:annotate"; private final RestClient restClient = RestClient.builder().build(); private final ObjectMapper objectMapper; private final String apiKey; public VisionOcrService(ObjectMapper objectMapper, @Value("${google.vision.api-key:}") String apiKey) { this.objectMapper = objectMapper; this.apiKey = apiKey; } public boolean isEnabled() { return apiKey != null && !apiKey.isBlank(); } /** 영수증 이미지에서 전체 텍스트 추출 */ public String recognize(byte[] image) { if (!isEnabled()) { throw new ApiException(HttpStatus.SERVICE_UNAVAILABLE, "영수증 OCR이 설정되지 않았습니다."); } String content = Base64.getEncoder().encodeToString(image); Map body = Map.of( "requests", List.of(Map.of( "image", Map.of("content", content), "features", List.of(Map.of("type", "DOCUMENT_TEXT_DETECTION")), "imageContext", Map.of("languageHints", List.of("ko")) )) ); String responseJson; try { responseJson = restClient.post() .uri(ENDPOINT + "?key=" + apiKey) .contentType(MediaType.APPLICATION_JSON) .body(body) .retrieve() .body(String.class); } catch (Exception e) { log.warn("Vision OCR 호출 실패: {}", e.getMessage()); throw new ApiException(HttpStatus.BAD_GATEWAY, "영수증 인식 서버 호출에 실패했습니다."); } try { JsonNode r0 = objectMapper.readTree(responseJson).path("responses").path(0); if (r0.has("error")) { log.warn("Vision OCR 오류 응답: {}", r0.path("error").path("message").asText()); throw new ApiException(HttpStatus.BAD_GATEWAY, "영수증 인식에 실패했습니다."); } return r0.path("fullTextAnnotation").path("text").asText(""); } catch (ApiException e) { throw e; } catch (Exception e) { log.warn("Vision OCR 응답 파싱 실패: {}", e.getMessage()); throw new ApiException(HttpStatus.BAD_GATEWAY, "영수증 인식 결과를 처리하지 못했습니다."); } } }