현행 위치기반 OCR 휴리스틱 → VLM 구조화 추출 + 결정론적 검증 + 국세청 진위확인으로 전환
gemini-3-flash-preview (프로젝트 기본) — 이미지 1장을 JSON Schema 구조화 출력으로 직접 추출. OCR + 위치파싱을 한 번의 호출로 대체, 개인·법인·회전·손글씨 모두 대응.모든 로직은 quality-admin-web에 존재. 파이프라인은 이미지 → Naver CLOVA OCR(general) → 위치기반 휴리스틱 추출 → 폼 → MySQL. 정규식·체크디짓·진위조회가 전무합니다.
| # | 문제 | 위치 | 영향 |
|---|---|---|---|
| 1 | 개인사업자증 인식 불가 — 앵커가 법인명(단체명)(법인 전용) | OCR/InputPhoto.tsx:46 | 개인사업자 번호 미추출 |
| 2 | 등록번호를 "라벨 앞 토큰"(fields[i-1])으로 가정 — OCR 필드 순서에 취약 | InputPhoto.tsx:47 | 순서 흔들리면 오추출 |
| 3 | 검증 부재 — 10자리 길이만 확인, 체크디짓·진위·휴폐업 없음 | Application/Form.tsx:64 | 허위·오타 번호 저장 |
| 4 | 🔴 CLOVA OCR secret 소스 평문 커밋 | routes/webapp/upload/dao.js:5 | 키 노출 |
| 5 | 추출 코드 2벌 중복(InputPhoto·ShootingPhoto) | OCR/*.tsx | 드리프트 |
핵심 전환: OCR(텍스트 나열) + 프론트 위치파싱 2단계를 → VLM 1회 호출의 구조화 추출로 축약(머스크 5단계 ②삭제·③단순화). 추출을 서버로 이동해 SSOT화.
| 옵션 | 정확도(한글문서) | 개인·법인 | 구조화출력 | 비용/장 | 비고 |
|---|---|---|---|---|---|
⭐ Gemini 3 Flash 권장gemini-3-flash-preview | 매우 높음 | 둘 다 | 네이티브(responseSchema) | 매우 저렴 | 프로젝트 기본 모델·1콜 완결 |
| CLOVA OCR 사업자등록증 템플릿 | 높음(특화) | 둘 다 | 템플릿 필드 직접반환 | 중 | 네이버 생태계 유지 시 최소변경 대안 |
Claude Opus 4.8 visionclaude-opus-4-8 | 최상 | 둘 다 | tool/JSON | 상 | 최고 정확도 필요 시 백업 경로 |
현행: CLOVA OCR general + 위치파싱 | 가변(취약) | 법인만 | 없음(수동) | 중 | 교체 대상 |
선정 이유 — 프로젝트 AI 기본이 gemini-3-flash-preview이고(모델 설정 SSOT: agent-model-settings), 단일 호출로 인식+추출+정규화를 끝내며 go-chatbot-server가 이미 Gemini 키를 보유. 정확도 극대화가 필요한 소수 케이스만 Claude Opus 4.8로 폴백.
routes/webapp/upload/biz-ocr.ts// @google/genai — 2026 통합 SDK (generative-ai-go/@google/generative-ai 대체)
import { GoogleGenAI, Type } from '@google/genai';
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const schema = {
type: Type.OBJECT,
properties: {
businessNo: { type: Type.STRING, description: '사업자등록번호 숫자 10자리, 하이픈 제외' },
companyName: { type: Type.STRING },
repName: { type: Type.STRING, description: '대표자 성명' },
businessType: { type: Type.STRING, description: '업태/종목' },
openDate: { type: Type.STRING, description: '개업연월일 YYYY-MM-DD' },
entityType: { type: Type.STRING, enum: ['개인', '법인'] },
},
required: ['businessNo', 'companyName', 'repName', 'entityType'],
};
export async function extractBizCert(buffer, mimeType) {
const res = await ai.models.generateContent({
model: 'gemini-3-flash-preview',
contents: [{ role: 'user', parts: [
{ inlineData: { mimeType, data: buffer.toString('base64') } },
{ text: '첨부한 대한민국 사업자등록증에서 필드를 추출해 스키마대로 JSON만 반환. ' +
'개인사업자면 상호=companyName·성명=repName, 법인이면 법인명·대표자. 추정 금지, 없으면 빈 문자열.' },
]}],
config: {
responseMimeType: 'application/json',
responseSchema: schema,
temperature: 0, // 결정론적
},
});
return JSON.parse(res.text);
}
프론트 extractFields(InputPhoto·ShootingPhoto)를 삭제하고, 서버가 검증까지 마친 결과만 폼에 채운다 → 추출 SSOT 1곳.
GEMINI_API_KEY=... # go-chatbot-server와 동일 키 재사용 가능
NTS_API_KEY=... # 국세청 진위확인 (data.go.kr) — 4.4
# CLOVA_OCR_SECRET 은 general OCR 폐기 시 제거. 유지 시 반드시 env 로 이전(현재 소스 하드코딩)
모델 무관·서버측·무비용. 국세청 표준 체크디짓 알고리즘으로 오인식을 즉시 걸러낸다.
// utils/bizno.ts — 사업자등록번호 유효성 (검증키 modulo)
export function normalizeBizNo(raw: string): string {
return (raw.match(/\d/g) ?? []).join(''); // 숫자만
}
export function isValidBizNo(raw: string): boolean {
const d = normalizeBizNo(raw).split('').map(Number);
if (d.length !== 10) return false;
const w = [1,3,7,1,3,7,1,3,5];
let sum = 0;
for (let i = 0; i < 9; i++) sum += d[i] * w[i];
sum += Math.floor((d[8] * 5) / 10); // 9번째 자리 가중 보정
const check = (10 - (sum % 10)) % 10;
return check === d[9]; // 마지막 자리 = 검증키
}
export function formatBizNo(raw: string): string {
const d = normalizeBizNo(raw);
return d.replace(/^(\d{3})(\d{2})(\d{5})$/, '$1-$2-$3'); // 000-00-00000
}
폼 제출 게이트에서 isValidBizNo 실패 시 저장 차단 + 재촬영 유도. 현행 Form.tsx의 "10자리 길이만" 검사를 이걸로 교체.
체크디짓은 형식만 보장 → 실존·영업 여부는 국세청 공공 API로. data.go.kr · 무료
| 서비스 | 엔드포인트 | 반환 |
|---|---|---|
| 상태조회(휴폐업) | POST /nts-businessman/v1/status?serviceKey=… | 계속사업자 / 휴업 / 폐업 |
| 진위확인 | POST /nts-businessman/v1/validate?serviceKey=… | 등록번호+대표자+개업일 일치 여부 |
// 저장 직전 게이트 (routes/webapp/submit)
const r = await fetch(
`https://api.odcloud.kr/api/nts-businessman/v1/status?serviceKey=${process.env.NTS_API_KEY}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ b_no: [normalizeBizNo(businessNo)] }) });
const { data } = await r.json();
if (data[0].b_stt_cd !== '01') throw new Error('휴업/폐업 또는 미등록 사업자'); // 01=계속사업자
biz-ocr.ts(Gemini 구조화) 신설, POST /webapp/upload가 검증완료 JSON 반환. 프론트 extractFields 2벌 삭제.utils/bizno.ts 추가, Form.tsx의 길이검사를 isValidBizNo로 교체.submit 전 상태조회, 휴폐업·미등록 차단.위치의존 파싱 제거 → 개인·법인·회전 문서 모두 인식. 체크디짓으로 오인식 자동 차단.
OCR+프론트파싱 2단계 → VLM 1콜. 추출 코드 중복 제거·서버 SSOT화.
국세청 진위·휴폐업 검증으로 허위·폐업 사업자 DB 유입 차단.
하드코딩 secret 제거, 키를 Infisical 일원화.