
적격 청구서 PDF를 JSON으로 변환하기 — 스키마는 '예시'로 정의한다
요약
LDX hub의 AnalyzeDoc API를 사용하여 적격 청구서 PDF를 JSON 구조화 데이터로 변환하는 방법을 소개합니다. 별도의 JSON Schema 작성 없이 원하는 출력 예시(example_output)만 제공하면 모델이 타입을 추론하는 'schema-by-example' 방식을 활용합니다.
핵심 포인트
- AnalyzeDoc API는 예시 데이터를 통해 JSON 스키마를 자동으로 정의함
- 예시 값의 데이터 타입(정수, 실수 등)이 출력 스키마의 타입을 결정함
- 시스템 프롬프트에는 필드 설명 대신 데이터 추출 규칙만 작성하는 것이 효율적임
- 혼재된 데이터는 배열 구조를 사용하여 모델의 정확도를 높임

인보이스 제도(Invoice System)가 시작되면서, 청구서에는 등록 번호(T+13자리)와 세율별 내역이 기재되게 되었습니다. 사람에게는 읽기 편해졌지만, 시스템에 도입하는 수고는 변하지 않았습니다. 8%와 10%가 섞인 명세, 세율별 집계, "※는 경감 세율 대상"이라는 주석——구조는 존재하지만, PDF 상태로는 구조화 데이터(Structured Data)가 아닙니다.
이 기사에서는 적격 청구서 PDF를 API 한 번으로 JSON으로 만드는 절차를 실제 실행 결과와 함께 보여드립니다. 사용하는 것은 LDX hub의 AnalyzeDoc입니다. PDF/JPEG/PNG를 던지면 구조화된 JSON이 반환되는 vision 계열의 API입니다.
특징은 단 하나, JSON Schema를 작성하지 않는 것입니다. 원하는 출력의 '예시(example)'를 하나 전달하면, 그것이 그대로 스키마 정의가 됩니다.
샘플 청구서
검증에는 가상의 적격 청구서를 사용합니다 (샘플 PDF. job.json 및 검증된 출력물은 모두 ldxhub-io/examples에 있습니다).
적격 청구서의 기재 요건——등록 번호, 세율별로 구분한 대가 금액과 적용 세율, 세율별 소비세액, 경감 세율 대상 품목 명기(※ 표시)——를 한 차례 모두 포함하고 있습니다. 명세는 4행이며, 8% 대상(식품) 2품목과 10% 대상 2품목이 혼재되어 있습니다. 합계는 42,210엔입니다.

스키마는 example_output로 정의한다
AnalyzeDoc에는 스키마 정의 파일이 없습니다. 대신 example_output로서 "이러한 JSON을 원한다"라는 예시를 전달합니다.
{
"invoice_number": "INV-2025-0001",
"issue_date": "2025-01-15",
...
예시를 작성할 때의 포인트가 3가지 있습니다.
값은 타입 선언입니다. 금액을 10000과 같이 정수로 쓰면 integer 타입으로, 1234.56과 같이 쓰면 number 타입으로 추론됩니다. 일본 엔화에는 소수가 존재하지 않으므로, 여기서는 정수로 쓰는 것이 옳습니다. 반대로 달러 결제 청구서라면, 예시의 금액은 반드시 소수점을 포함하여 작성합니다. 예시 값의 작성 방식이 그대로 출력의 타입을 결정합니다—schema-by-example의 가장 중요한 규칙입니다.
혼재된 데이터는 배열(Array)로 받습니다. tax_summary를 배열로 만든 이유는 8%와 10%의 혼재가 적격 청구서의 핵심이기 때문입니다. "세율별로 1개의 엔트리"라는 구조를 스키마 측에서 받아들이면 모델은 헤매지 않습니다.
예시 값은 실제 데이터와 다르게 설정합니다. 예시와 동일한 청구서를 읽게 하면, 모델이 예시를 그대로 베낀 것인지 실제로 읽은 것인지 구분할 수 없습니다. 번호도 날짜도 회사명도 모두 다른 값으로 설정해 둡니다.
system_prompt는 '규칙'만 작성한다
필드에 대한 설명은 쓰지 않습니다. 그것은 example_output의 역할입니다. 프롬프트에 쓰는 것은 예시만으로는 표현할 수 없는 **규칙(Rule)**뿐입니다.
Extract the invoice data from this Japanese qualified invoice (適格請求書).
Rules:
- Dates in YYYY-MM-DD format.
...
마지막 전화번호 규칙은 실무적인 지혜입니다. +81로 시작하는 문자열은 Google Sheets로 보내면 수식으로 취급되어 사고가 발생할 수 있습니다. 추출 API의 출구에서 미리 처리해 두는 것이 저렴합니다.
실행하기
4단계입니다. 폴링 루프(Polling loop)는 작성하지 않습니다—?wait=30을 통해 서버 측에서 대기해 줍니다.
# 1. 업로드 → file_id
curl -s -X POST https://gw.ldxhub.io/files \
-H "Authorization: Bearer $LDXHUB_API_KEY" \
...
job.json은 모델 ID, file_id, 출력 형식, 프롬프트, 예시의 5종 세트입니다.
{
"model": "google/gemini-3.5-flash@high",
"file_id": "<step 1의 file_id>",
...
결과
26초 만에 완료되었습니다. 반환된 JSON의 전문은 다음과 같습니다.
{"invoice_number":"INV-2026-0157","issue_date":"2026-06-30","due_date":"2026-07-31","issuer_name":"株式会社グリーンリーフ食品","registration_number":"T1234567890123","issuer_phone":"03-1234-5678","customer_name":"サンプルマート株式会社","line_items":[{"description":"有機緑茶ギフトセット ※","quantity":2,"unit_price":3000,"amount":6000,"tax_rate":8},{"description":"国産純粋はちみつ 500g ※","quantity":10,"unit_price":1200,"amount":12000,"tax_rate":8},{"description":"陶器マグカップ(箱入)","quantity":24,"unit_price":800,"amount":19200,"tax_rate":10},{"description":"配送料","quantity":1,"unit_price":1500,"amount":1500,"tax_rate":10}],"tax_summary":[{"tax_rate":8,"taxable_amount":18000,"tax_amount":1440},{"tax_rate":10,"taxable_amount":20700,"tax_amount":2070}],"subtotal":38700,"total_tax":3510,"total":42210,"bank_details":"サンプル銀行 本店 普通 1234567 カ)グリーンリーフショクヒン"}
검산합니다. 명세 4행의 합계 38,700 = 소계. 8% 대상 18,000 × 0.08 = 1,440. 10% 대상 20,700 × 0.10 = 2,070. 소비세 합계 3,510. 총액 42,210. 전체 15개 필드가 원본과 일치하며, 숫자 오기나 존재하지 않는 필드 조작이 없습니다.
모델의 세부 이해가 보인다
숫자가 맞는 것은 전제이며, 흥미로운 점은 디테일입니다.
'御中(어중)'이 사라졌다. 원본의 수신처는 'サンプルマート株式会社 御中'이지만, customer_name에는 'サンプルマート株式会社'만 들어갔습니다. 모델은 경칭은 회사명이 아니라고 판단한 것입니다. 이는 저희가 아무런 지시를 하지 않았음에도 불구하고 발생했습니다.
※ 표시는 남아있다. 명세의 '有機緑茶ギフトセット ※'는 그대로 번역되었습니다. 감면세율 여부 판별은 tax_rate: 8이 담당합니다. description 필드는 원문에 충실하고, 의미는 구조가 담당하는—이 분업은 스키마를 그렇게 설계했기 때문에 성립할 수 있었습니다.
날짜의 한자 표기가 정규화되었다. 원본의 '2026年6月30日'가 2026-06-30으로 바뀌었습니다. 프롬프트의 단 한 줄('Dates in YYYY-MM-DD format')만으로 통일할 수 있었습니다.
금액이 모두 정수입니다. 예시를 정수로 작성했기 때문입니다. 6000이고 6000.0이 아닙니다. 하위 시스템이 타입에 민감하더라도 그대로 흘려보낼 수 있습니다.
푸터의 면책 문구는 어디에도 섞여있지 않다. 샘플 PDF 하단에는 '본 청구서는 샘플입니다'라는 주석이 있지만, 스키마에 대응하는 필드가 없기 때문에 정확하게 무시되었습니다.
모델은 선택 가능하다
이번에 사용한 것은 google/gemini-3.5-flash@high (입력 571자・출력 812자・26초)입니다. 모델 ID 문자열을 하나만 변경하는 것만으로 OpenAI / Azure / Google / Anthropic / Amazon의 각 모델로 전환할 수 있습니다(작성 시점 기준 15개 모델).
청구서와 같은 정형 문서는 빠르고 저렴한 모델로 충분하며, 레이아웃이 무너진 양식이나 손글씨가 섞인 것은 상위 모델을 사용하는 식으로 같은 코드로 사용 구분이 가능합니다. 스키마 호환성은 저희가 맞춰두었기 때문에, 모델을 바꿔도 JSON 형태는 변하지 않습니다.
비용
이 한 장의 처리에 약 285 크레딧($0.029, 4원 정도)이 들었습니다. Free 플랜에 25,000 크레딧이 제공되므로, 80장 이상은 무료로 시도해 볼 수 있는 계산입니다.
60초 만에 시도하기
가입부터 샘플 실행까지, 가장 짧게 60초 안에 완료할 수 있도록 quickstart를 만들어 두었습니다.
가지고 있는 청구서를 읽을 수 있는지 여부가 가장 빠른 평가 방법이라고 생각합니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기