daejeon-food-inspection · 기술 제안서

사업자등록증 인식 · 번호추출
2026 최적 아키텍처

현행 위치기반 OCR 휴리스틱 → VLM 구조화 추출 + 결정론적 검증 + 국세청 진위확인으로 전환

대상 앱 quality-admin-web · 작성일 2026-07-13 · 서버 EC2 13.209.138.233 · 개인용 / noindex

TL;DR 요약

  • 주 모델 = gemini-3-flash-preview (프로젝트 기본) — 이미지 1장을 JSON Schema 구조화 출력으로 직접 추출. OCR + 위치파싱을 한 번의 호출로 대체, 개인·법인·회전·손글씨 모두 대응.
  • 결정론적 후처리(필수) — 정규식 정규화 + 사업자번호 체크디짓(modulo) 검증. LLM/OCR 오인식을 무료로 즉시 필터.
  • 권위 검증 — 국세청 진위확인·휴폐업 상태조회 API(data.go.kr)로 실존·영업 여부 확인 후 저장.
  • 즉시 조치 — 소스에 하드코딩된 CLOVA OCR secret을 env/Infisical로 이전.

01 현행 진단

모든 로직은 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드리프트

02 2026 권장 파이프라인

① 캡처
클라이언트 전처리
deskew·crop·리사이즈(장변 1600px)
② 추출
Gemini 3 Flash VLM
JSON Schema 구조화 출력 (temp 0)
③ 검증
체크디짓 + 정규식
결정론적, 서버측, 무비용
④ 진위
국세청 API
실존·휴폐업 상태조회
⑤ 저장
companies
검증 통과분만 INSERT

핵심 전환: OCR(텍스트 나열) + 프론트 위치파싱 2단계를 → VLM 1회 호출의 구조화 추출로 축약(머스크 5단계 ②삭제·③단순화). 추출을 서버로 이동해 SSOT화.

03 모델 선정

옵션정확도(한글문서)개인·법인구조화출력비용/장비고
⭐ Gemini 3 Flash 권장
gemini-3-flash-preview
매우 높음둘 다네이티브(responseSchema)매우 저렴프로젝트 기본 모델·1콜 완결
CLOVA OCR 사업자등록증 템플릿높음(특화)둘 다템플릿 필드 직접반환네이버 생태계 유지 시 최소변경 대안
Claude Opus 4.8 vision
claude-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로 폴백.

04 세팅 방법 — Gemini 구조화 추출

4.1 서버 서비스 신설 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곳.

4.2 환경변수 (Infisical / apps/quality-admin-web/.env)

GEMINI_API_KEY=...            # go-chatbot-server와 동일 키 재사용 가능
NTS_API_KEY=...               # 국세청 진위확인 (data.go.kr) — 4.4
# CLOVA_OCR_SECRET 은 general OCR 폐기 시 제거. 유지 시 반드시 env 로 이전(현재 소스 하드코딩)

05 세팅 방법 — 결정론적 검증 (필수)

모델 무관·서버측·무비용. 국세청 표준 체크디짓 알고리즘으로 오인식을 즉시 걸러낸다.

// 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자리 길이만" 검사를 이걸로 교체.

06 세팅 방법 — 국세청 진위확인 (권장)

체크디짓은 형식만 보장 → 실존·영업 여부는 국세청 공공 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=계속사업자

07 마이그레이션 단계

  1. 즉시 — secret 정리CLOVA secret을 소스에서 제거 → Infisical/.env. 커밋된 키·레포 내 chatbot-key.pem 로테이션.
  2. 추출 서버 이전biz-ocr.ts(Gemini 구조화) 신설, POST /webapp/upload가 검증완료 JSON 반환. 프론트 extractFields 2벌 삭제.
  3. 결정론적 검증 게이트utils/bizno.ts 추가, Form.tsx의 길이검사를 isValidBizNo로 교체.
  4. 국세청 진위확인 연동submit 전 상태조회, 휴폐업·미등록 차단.
  5. 정리·회귀CLOVA general 경로 제거(또는 폴백 유지), 개인·법인 각 실증 이미지로 E2E 회귀.

08 기대 효과

정확도

위치의존 파싱 제거 → 개인·법인·회전 문서 모두 인식. 체크디짓으로 오인식 자동 차단.

단순화

OCR+프론트파싱 2단계 → VLM 1콜. 추출 코드 중복 제거·서버 SSOT화.

신뢰성

국세청 진위·휴폐업 검증으로 허위·폐업 사업자 DB 유입 차단.

보안

하드코딩 secret 제거, 키를 Infisical 일원화.