Merge branch 'dev'
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
package com.sb.web.account.controller;
|
||||||
|
|
||||||
|
import com.sb.web.account.service.VisionOcrService;
|
||||||
|
import com.sb.web.auth.dto.SessionUser;
|
||||||
|
import com.sb.web.auth.web.AuthInterceptor;
|
||||||
|
import com.sb.web.common.exception.ApiException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 영수증 OCR API (/api/account/ocr/** — 로그인 필요).
|
||||||
|
* 이미지 업로드 → Google Vision 으로 텍스트 추출 → 프론트에서 금액·날짜·상호 파싱.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/account/ocr")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OcrController {
|
||||||
|
|
||||||
|
private final VisionOcrService visionOcrService;
|
||||||
|
|
||||||
|
@PostMapping("/receipt")
|
||||||
|
public Map<String, String> receipt(
|
||||||
|
@RequestParam("image") MultipartFile image,
|
||||||
|
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) throws IOException {
|
||||||
|
if (image == null || image.isEmpty()) {
|
||||||
|
throw new ApiException(HttpStatus.BAD_REQUEST, "영수증 이미지를 첨부하세요.");
|
||||||
|
}
|
||||||
|
String text = visionOcrService.recognize(image.getBytes());
|
||||||
|
return Map.of("text", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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, "영수증 인식 결과를 처리하지 못했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,12 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: sb_bt
|
name: sb_bt
|
||||||
|
|
||||||
|
# 영수증 OCR 이미지 업로드 크기 (프론트에서 축소해 보내지만 여유 확보)
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 10MB
|
||||||
|
max-request-size: 12MB
|
||||||
|
|
||||||
# ===== SQL 초기화 ===== 기동 시 member 테이블 자동 생성(IF NOT EXISTS 라 멱등)
|
# ===== SQL 초기화 ===== 기동 시 member 테이블 자동 생성(IF NOT EXISTS 라 멱등)
|
||||||
sql:
|
sql:
|
||||||
init:
|
init:
|
||||||
@@ -56,6 +62,12 @@ mybatis:
|
|||||||
default-fetch-size: 100
|
default-fetch-size: 100
|
||||||
default-statement-timeout: 30
|
default-statement-timeout: 30
|
||||||
|
|
||||||
|
# ===== Google Cloud Vision (영수증 OCR) =====
|
||||||
|
# 키는 서버 /opt/sb-backend/.env 에 GOOGLE_VISION_API_KEY=... 로 주입 (git 미추적). 미설정이면 OCR 비활성.
|
||||||
|
google:
|
||||||
|
vision:
|
||||||
|
api-key: ${GOOGLE_VISION_API_KEY:}
|
||||||
|
|
||||||
# ===== Logging =====
|
# ===== Logging =====
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
Reference in New Issue
Block a user