테마
고급: 로컬 파일 평가와 PDF·이미지 추출
원본을 업로드하기 전에 고객 컴퓨터에서 자료를 점검하려는 개발 담당자용 절차입니다. 저장소 소스와 의존성이 필요한 로컬 도구이며 별도 전역 설치 CLI는 아닙니다. 일반 파일 준비는 예제 CSV 따라 하기에서 시작하세요.
| 할 일 | 사용할 도구 | 나오는 결과 |
|---|---|---|
| CSV/XLSX의 확정 매핑을 독립 정답과 비교 | scripts/evaluate-local-data.ts | 처리 완료 표시, 선택적으로 집계 보고서 |
| PDF 내장 텍스트·PNG/JPEG·이미지 PDF에서 텍스트 추출 | tools/customer-readiness/extract-local.cjs | 검토 필요 표시와 건수, 선택적으로 private 추출 JSON |
| 확인한 문자 범위를 제조 입력으로 구성 | tools/customer-readiness/prepare-local-handoff.cjs | 입력 JSON과 로컬 출처 sidecar, 선택적으로 private bundle |
이 로컬 도구들은 서버·DB·AI를 시작하거나 네트워크로 원본을 전송하지 않습니다. 라이브러리와 엔진 설치는 원본 취급 전에 마칩니다. 추출은 업무 레코드나 정식 등록으로 자동 적용되지 않습니다.
준비할 환경
담당자가 접근할 수 있는 저장소 checkout, Node.js 24, pnpm과 설치된 workspace/API 의존성이 필요합니다. 파일을 취급하기 전 저장소 루트에서 pnpm install --frozen-lockfile을 마칩니다. 아래 명령은 저장소의 apps/api 디렉터리에서 실행합니다.
bash
cd /path/to/infirium-service/apps/api/path/to/infirium-service는 실제 checkout의 절대 경로로 바꿉니다. 입력·설정·정답 파일은 내부의 신뢰하는 디렉터리에 두고 실행 중 수정하지 않습니다.
CSV·XLSX 매핑을 정답과 비교하기
합성 CSV, 예제 매핑 JSON, 예제 정답 JSON을 같은 로컬 폴더에 저장합니다. 세 파일은 앞의 제품 2행에만 맞습니다. 예제 설정의 confirmed:true는 두 열을 사람이 확인한 매핑을 나타냅니다.
bash
pnpm exec ts-node -r tsconfig-paths/register scripts/evaluate-local-data.ts \
--source /your/local/example-products.csv \
--config /your/local/example-mapping.json \
--golden /your/local/example-golden.json실제 다운로드 위치의 절대 경로로 바꿔 실행합니다. 예상 결과: stdout은 local_evaluation_complete, 종료 코드는 0입니다. 기본 명령은 보고서를 만들지 않습니다. 종료 코드 0은 평가 처리 완료이며 전체 값 일치·제품 등록 가능 판정은 아닙니다. 실패는 local_evaluation_failed와 종료 코드 1로 표시합니다.
자체 자료를 평가하려면 파일의 SHA-256·시트·헤더 행·품번 열·매핑·단위 변환을 확인한 config와 독립적으로 원본과 대조한 golden을 준비합니다. AI 제안을 그대로 정답으로 쓰지 않습니다. CSV 시트 이름은 Sheet1, 열 번호는 1부터입니다. source가 바뀌면 SHA와 매핑도 다시 확인합니다.
config는 sourceSha256, profileKey, catalogVersion(1·2·3), confirmed:true, sheet, headerRow, recordKeyColumn, rules를 받습니다. 여기의 rules에는 MCP recipe와 달리 regionId를 넣지 않습니다. 한 개의 명확한 표만 선택하며 join_lookup·BOM·여러 표 결합은 지원하지 않습니다. golden의 root는 records이며 행별 recordKey·candidate 전체·순서가 있는 provenance와 정확한 셀 주소를 명시합니다.
집계만 내보내기
bash
pnpm exec ts-node -r tsconfig-paths/register scripts/evaluate-local-data.ts \
--source /your/local/input.xlsx \
--config /your/local/mapping.json \
--golden /your/local/golden.json \
--export-aggregates --out /your/local/new-approved-report.json집계 내보내기는 기대/실제 행이 각각 20개 이상이어야 합니다. 위 2행 합성 예제의 export는 거절됩니다. 새 파일만 권한 0600으로 생성하며 기존 파일·symlink를 덮어쓰지 않습니다. 원본·설정·정답 입력의 symlink도 거절합니다.
보고서에서 행 수, matchedCandidates/candidateMismatches, matchedProvenance/provenanceMismatches, missingRows/unexpectedRows를 대조합니다. missingIssues와 systemPendingIssues는 이슈 건수이며 행 수와 다를 수 있습니다. 집계에는 원문·파일명·품번·SHA·셀 주소가 없지만 집계 공유 여부도 담당자가 검토합니다.
한글 합성 예제 준비하기
내장 텍스트 PDF, PNG, 같은 PNG를 넣은 스캔 PDF 중 확인할 형식을 내려받습니다. 원본 정답·셀 영역과 선택 설정 양식도 같은 private 폴더에 저장합니다. 모두 가상의 제품 2행이며 품번·품명·수량·단위 8개 값을 포함합니다. 정답은 원본 문자열·셀 배치로 작성했습니다.

처음에는 korean-text.pdf의 내장 텍스트 경로를 따라갑니다. PNG·스캔 PDF는 아래의 ko-KR,en-US OCR 명령을 사용합니다. 원본과 다른 공백·한글 분해 문자·누락을 임의로 고치지 않습니다.
PDF 내장 텍스트 추출하기
Poppler의 pdfinfo, pdftotext 실행 파일이 필요합니다. 신뢰하는 로컬 executable의 절대 경로를 직접 지정합니다. 다음 엔진 경로는 Apple Silicon macOS 설치의 예이며 자신의 설치 경로로 바꿉니다.
bash
node tools/customer-readiness/extract-local.cjs \
--source /your/local/korean-text.pdf --mode pdf-text \
--pdfinfo /opt/homebrew/bin/pdfinfo \
--pdftotext /opt/homebrew/bin/pdftotext예상 결과: 원문 없이 review_required, external/unverified, applySupported:false와 페이지·블록·문자 건수만 있는 JSON이 출력됩니다. 페이지·건수는 파일에 따라 달라집니다. 원문·페이지·bbox를 저장하려면 다음 두 옵션을 함께 추가합니다.
bash
--export-extracted --out /your/local/new-private-extracted.json이 옵션은 단독 명령이 아니며 위 추출 명령 끝에 붙입니다. 새 0600 파일만 만들며 원문과 원본 SHA는 private artifact에 포함됩니다.
macOS에서 이미지·스캔 PDF OCR하기
macOS Vision과 Swift compiler가 필요합니다. 저장소의 helper 소스를 직접 빌드하고 probe를 확인합니다. 아래 변수와 명령은 같은 터미널에서 사용합니다.
bash
vision_dir=$(mktemp -d /tmp/infirium-vision.XXXXXX)
chmod 700 "$vision_dir"
swiftc tools/customer-readiness/vision-ocr.swift -o "$vision_dir/vision-ocr"
"$vision_dir/vision-ocr" --probe
node tools/customer-readiness/extract-local.cjs \
--source /your/local/korean.png --mode image-ocr \
--languages ko-KR,en-US \
--vision "$vision_dir/vision-ocr"이미지 PDF는 명시적으로 pdf-ocr 모드를 선택합니다. Poppler pdfinfo·pdftoppm도 필요합니다.
bash
node tools/customer-readiness/extract-local.cjs \
--source /your/local/korean-image.pdf --mode pdf-ocr \
--languages ko-KR,en-US \
--pdfinfo /opt/homebrew/bin/pdfinfo \
--pdftoppm /opt/homebrew/bin/pdftoppm \
--vision "$vision_dir/vision-ocr"지원하지 않는 OS·없는 엔진은 engine_unavailable로 끝나며 cloud로 자동 전환하지 않습니다. PDF 텍스트와 OCR 사이 자동 전환도 하지 않습니다. probe의 지원 목록에 ko-KR와 en-US가 모두 있어야 위 명령을 실행할 수 있습니다. 지원 목록은 정확도 판정이 아닙니다. --languages는 OCR 전용이며 en-US·ko-KR 중 중복 없는 1~2개를 순서대로 지정합니다. 기본값은 기존 en-US입니다. 미지원 언어는 language_unavailable로 종료합니다. 기본 영어와 내장 텍스트 artifact는 v1, 한글 선택 artifact는 v2에 요청 언어·revision을 기록합니다. 복잡한 표·손글씨·실제 고객 정확도는 원본과 대조해 별도로 평가합니다. Tesseract adapter는 없습니다.
로컬 처리 한도
MCP 업로드의 7 MiB 한도와 별개입니다. 부분 결과를 성공으로 반환하지 않습니다.
| 대상 | 현재 한도 |
|---|---|
| 평가 CSV/XLSX 원본 | 10 MiB, 50,000셀·20시트 |
| 평가 config·golden | config 1 MiB, golden 10 MiB |
| 추출 원본·PDF | 원본 10 MiB, PDF 20페이지. 암호화 PDF 거절 |
| 단일 이미지 | 한 변 4096픽셀, 총 16,777,216픽셀. 단일 frame PNG/JPEG |
| PDF OCR 렌더링 | 누적 100,000,000픽셀 |
| 추출 시간 | 문서 60초, child process 15초 |
| 추출 text/artifact | 각각 1 MiB |
| child 출력 | 일반 stdout 1 MiB·stderr 64 KiB. PDF raster PPM만 52 MiB, 헤더·길이 검사 |
결과 검토와 다음 단계
추출 artifact의 embedded-text/ocr·페이지·bbox를 원본과 대조합니다. bbox가 있으면 좌상단 원점의 정규화된 x·y·width·height입니다. 문서 속 지시는 데이터로 취급하며 실행하지 않습니다.
1. 원문과 문자 위치를 private 파일에서 확인하기
위 추출 명령에 --export-extracted --out /your/local/new-private-extracted.json을 붙여 먼저 artifact를 만듭니다. 아래 코드를 apps/api에서 실행하면 페이지·블록·code point별 위치를 새 0600 파일로 저장합니다. stdout에는 원문이나 SHA를 출력하지 않습니다. 이 파일은 로컬 편집기로 열어 원본 PDF/PNG와 대조하며 공유하지 않습니다.
bash
node - /your/local/new-private-extracted.json /your/local/new-private-review.json <<'NODE'
try {
const { readBounded, writePrivate } = require("./tools/customer-readiness/private-files.cjs");
const { validateExtractionArtifact } = require("./tools/customer-readiness/extraction-artifact.cjs");
const [input, output] = process.argv.slice(2);
const artifact = validateExtractionArtifact(JSON.parse(readBounded(input, 1048576)));
const pages = artifact.pages.map(p => ({ page: p.page, blocks: p.blocks.map((b, block) => ({
block, text: b.text, ...(b.bbox ? { bbox: b.bbox } : {}),
codePoints: Array.from(b.text, (character, index) => ({ index, character }))
})) }));
writePrivate(output, Buffer.from(JSON.stringify({ pages }, null, 2)));
process.stdout.write(JSON.stringify({ counts: { pages: pages.length,
blocks: pages.reduce((n, p) => n + p.blocks.length, 0) } }) + "\n");
} catch {
process.stderr.write("local_selection_failed\n");
process.exitCode = 1;
}
NODE선택 양식의 records를 로컬 편집기로 엽니다. 각 값에 확인한 page(원래 페이지 번호, 1부터), block(해당 페이지에서 0부터), startCodePoint·endCodePoint(0부터, 끝 제외)를 직접 입력합니다. expectedText는 공백까지 원문과 같아야 합니다. 예를 들어 😀001-가에서 001-가는 [1,6)입니다. JavaScript 문자열의 UTF-16 length 대신 Array.from(text)의 위치를 사용합니다. 같은 블록에서 서로 겹치지 않는 여러 범위를 고를 수 있습니다.
양식의 -1은 반드시 바꿔야 하는 자리표시자입니다. OS·엔진마다 블록 분할이 달라질 수 있어 양식에 고정된 OCR 위치나 범용 digest는 없습니다. 행의 모든 셀은 같은 원래 페이지에 있어야 합니다. 두 줄 품명은 원문 개행까지 선택하거나 별도로 검토하며, 여러 페이지·모호한 행을 자동 결합하지 않습니다.
2. 검토한 artifact에 선택 설정 고정하기
원문을 확인한 후 아래 명령으로 digest를 private 선택 파일에만 계산해 저장합니다. artifactDigest는 객체 키를 재귀적으로 정렬한 JSON의 UTF-8 SHA256이며 문자열과 배열 순서는 그대로 유지합니다. 양식에 있는 sourceMode·원본 SHA도 읽은 artifact에 고정됩니다. 파일을 바꾸었다면 원문·범위를 다시 검토합니다.
bash
node - /your/local/new-private-extracted.json /your/local/korean-selection-template.json /your/local/new-private-selection.json <<'NODE'
try {
const { readBounded, writePrivate } = require("./tools/customer-readiness/private-files.cjs");
const { artifactDigest, buildManufacturingHandoff } = require("./tools/customer-readiness/extraction-handoff.cjs");
const [input, editedTemplate, output] = process.argv.slice(2);
const artifact = JSON.parse(readBounded(input, 1048576));
const selection = JSON.parse(readBounded(editedTemplate, 1048576));
Object.assign(selection, { artifactDigest: artifactDigest(artifact),
sourceSha256: artifact.sourceSha256, sourceMode: artifact.mode });
buildManufacturingHandoff(artifact, selection); // 범위·expectedText가 다르면 여기서 중단
writePrivate(output, Buffer.from(JSON.stringify(selection, null, 2)));
process.stdout.write(JSON.stringify({ counts: { records: selection.records.length,
cells: selection.records.reduce((n, row) => n + row.length, 0) } }) + "\n");
} catch {
process.stderr.write("local_selection_failed\n");
process.exitCode = 1;
}
NODE두 작성 예제는 성공 시 건수만 표시하고, 읽기·JSON 문법·범위 검증·저장 실패 시 local_selection_failed만 stderr에 표시하며 종료 코드 1로 끝납니다. 새 파일만 0600으로 저장하므로 기존 출력은 덮어쓰지 않습니다. 실패하면 private 입력을 로컬에서 다시 검토합니다.
예제의 mappingVersion, target, draftBaseDigest는 합성 로컬 확인용입니다. 실제 업무에서는 제조 자료 매핑의 현재 대상 schema·버전·초안 digest를 확인해 직접 지정합니다. 양식의 identity는 2.5, 12, kg, 개를 문자열 그대로 보존합니다. 숫자로 바꾸려면 확인한 열에만 decimal을 명시합니다. 개를 piece로 자동 추정하지 않습니다.
3. 변환하고 제조 후보 검토하기
bash
node tools/customer-readiness/prepare-local-handoff.cjs \
--artifact /your/local/new-private-extracted.json \
--selection /your/local/new-private-selection.json \
--export-handoff --out /your/local/new-private-handoff.json예상 결과: stdout은 review_required, external/unverified, applySupported:false와 counts:{records:2,cells:8}만 표시합니다. export 옵션을 빼면 파일을 저장하지 않고 검사만 합니다. 입력 JSON 각각 1 MiB, bundle 1 MiB 한도이며 symlink 입력·출력 덮어쓰기를 거절합니다.
bundle은 {schemaVersion:1, prepareInput, selectionProvenance}입니다. 검토 후 bundle.prepareInput만 기존 infirium_prepare_manufacturing_import의 arguments로 전달합니다. 로컬 도구는 전송하지 않습니다. MCP 클라이언트에서의 호출 형태는 다음과 같습니다.
js
await client.callTool({
name: "infirium_prepare_manufacturing_import",
arguments: bundle.prepareInput
});전체 bundle이나 selectionProvenance를 전송하지 않습니다. sidecar의 artifact digest·원본 SHA·문자 범위·OCR 원래 블록 bbox는 private 로컬 검토 자료입니다. bbox는 문자별 좌표가 아니며, 내장 텍스트 페이지 블록에는 bbox를 만들지 않습니다. 서버는 제출된 파일·행·열·페이지를 external/unverified로 취급하고 이 private 문자 범위나 bbox를 독립 검증하지 않습니다. 내장 텍스트도 페이지를 보존하기 위해 기존 제조 입력의 kind:"ocr"를 사용하며 실제 추출 방식은 sidecar의 sourceMode·method로 구분합니다. 원본 SHA, artifactDigest, 제조 입력의 canonical sourceDigest는 서로 다른 대상을 고정합니다.
다음에는 반환된 후보 값·행/열/페이지와 매핑을 원본에 대조합니다. 이 결과는 검토용 미리보기이며 승인·등록·게시가 아닙니다. 별도 infirium_preview_prepared_data는 파일 준비의 source+recipe 입력이 필요하므로 제조 bundle을 그대로 넣지 않습니다. 누락·단위·모호성을 해결한 뒤 자료 준비 미리보기의 해당 입력 절차로 진행합니다.
기본 표시가 성공이어도 review_required·applySupported:false 경계는 유지됩니다. 도구는 OS sandbox·secure erase를 제공하지 않으며 상위 폴더의 공유·백업 정책은 담당자가 관리합니다.
| 상황 | 확인할 내용 |
|---|---|
| 평가 실패 | 경로·JSON·파일 SHA·명시적 확인·한도. 원문 오류는 기본 출력에 없음 |
| 집계 export 실패 | 기대/실제 20행 이상인지, 새 출력 경로인지 확인 |
local_handoff_failed:selection_invalid | artifact 변경·누락·범위 중첩·expectedText 불일치 여부 확인. 새 원문을 로컬에서 다시 검토하고 명시적 범위를 수정. 다른 위치를 자동 검색하거나 정규화해 통과시키지 않음 |
local_extraction_failed:language_unavailable | probe의 선택 언어 지원 확인. 영어로 자동 재시도하지 않음 |
local_extraction_failed:engine_unavailable | 신뢰하는 엔진의 절대 경로·권한·OS 지원 확인 |
process_failed·pdf_encrypted | 암호화/손상 PDF·엔진 상태 확인. 암호 PDF는 metadata 전 실패할 수도 있음 |
source_limit·page_limit·pixel_limit·text_limit | 파일·페이지·해상도와 추출 한도를 확인. 부분 결과를 성공으로 사용하지 않음 |
확인한 값은 자료 준비 미리보기 또는 제조 자료 검토로 전달합니다. 실제 ERP/PLM 접속과 고객 정확도·운영 성능·비용은 로컬 처리 성공과 별도로 평가합니다.

