Files
sb-backend/src/main/java/com/sb/web/account/service/VisionOcrService.java
T
ByungCheol c1f97364aa
CI / build (push) Failing after 14m0s
feat: 영수증 OCR Google Vision 연동
- VisionOcrService: 이미지→Google Vision DOCUMENT_TEXT_DETECTION 호출, 전체 텍스트 반환
- OcrController: POST /api/account/ocr/receipt (멀티파트, 로그인 필요)
- API 키는 GOOGLE_VISION_API_KEY 환경변수(.env)로만 주입 — 미설정 시 503
- multipart 업로드 10MB 허용

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:37:17 +09:00

83 lines
3.2 KiB
Java

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<String, Object> 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, "영수증 인식 결과를 처리하지 못했습니다.");
}
}
}