양식 일괄 작성 가이드
양식 1개 + 명단 N행 → 완성본 N부. 채우기 규칙과 검수 흐름.
원문: docs/BULK-GUIDE.md · 이 페이지는 빌드할 때 그 파일에서 생성됩니다.
기관 제출용 한글 양식에 100명분을 채워야 할 때 쓰는 표면이다. 양식에서 채울 자리를 한 번 정해 두고, 명단을 넣으면, 사람 수만큼 완성본이 나온다. 채우는 과정은 전부 규칙 기반이라 AI 없이 동작하고 (LLM 호출 0회), 웹에서 하든 터미널에서 하든 같은 엔진·같은 검증 규칙을 쓴다.
두 가지 방법이 있다:
웹 (/bulk) | CLI (inspect → fill) | |
|---|---|---|
| 채울 자리 정하기 | 문서 위에서 셀을 클릭 | 자동 유도 초안을 JSON으로 손 검수 |
| 형식 규정(날짜·전화번호…) | 있음 | 없음(v1) — 필수 여부만 |
| 검수 화면 | 실제 렌더 + 채운 칸 하이라이트, 한 명씩 넘겨보기 | report.json |
| 자동화·배치 | 수동 | 스크립트로 반복 |
| 설치 | 없음 (브라우저) | cargo install |
두 표면 모두 규격 파일(autohwp.fillmap.v1)을 공유하므로, 웹에서 규격을 잡고 CLI로 돌려도 된다.
웹으로 하기 — /bulk
전 과정이 브라우저 안에서 끝난다. 양식도 명단도 업로드되지 않는다.
1단계 · 양식 — .hwp/.hwpx를 올리면(파일 선택 또는 점선 칸에 끌어다 놓기) 그 자리에서
라벨(성명·연락처·기업명 …)을 찾아 채울 자리 초안을 만든다. 양식 파일이 없다면 "샘플로 체험" 한 번으로
샘플 양식 + 데모 명단 3명이 채워진 상태에서 시작할 수 있다.
2단계 · 채울 칸(영역 지정·규격화) — 문서가 실제로 렌더된 화면에서 셀을 클릭해 채울 자리를 더하거나 뺀다. 자동 초안은 상자와 이름표로 표시된다. 각 자리마다:
- 이름 — 명단의 열 이름(키)과 이 이름이 일치해야 값이 들어간다. 한글 그대로 쓴다.
- 형식 — 텍스트 / 날짜 / 전화번호 / 사업자번호 / 숫자·금액. 어긋난 값도 채우되 검수에 보고한다.
- 필수 — 비어 있으면 검수에 보고한다.
"규격 저장"을 누르면 양식명.fillmap.json이 내려온다. 다음 배치에서 불러오면 자리 지정을 다시
하지 않아도 된다. 파일에는 양식의 지문(sha256)이 들어 있어, 다른 양식에 불러오면 경고가 뜬다.
3단계 · 명단 — 아래 네 형식을 자동으로 알아본다. 엑셀에서 표를 복사해 그대로 붙여넣어도 되고,
파일(.csv/.txt/.tsv/.json)을 열어도 된다(UTF-8이 아니면 EUC-KR로 다시 읽는다 — 한국 엑셀 CSV 대응).
| 형식 | 모양 | 언제 |
|---|---|---|
| "키: 값" 블록 | 성명: 김하나 줄들, 사람 사이는 빈 줄 | 손으로 쓸 때 가장 쉽다 (권장) |
| TSV | 탭 구분, 첫 줄이 헤더 | 엑셀에서 복사·붙여넣기 |
| CSV | 콤마 구분, 첫 줄이 헤더 | 값에 콤마·따옴표가 없을 때만 |
| JSON | 객체 배열 | 값에 콤마·줄바꿈이 섞일 때 |
이름이 맞아야 값이 들어간다. 명단의 열 이름과 2단계에서 정한 영역 이름을 실시간으로 대조해, 필드
이름표(칩)에 매칭 여부(✓/✕)를 표시하고 명단에만 있는 열은 따로 보여준다. 생성 시에도 양쪽을 다시 대조해
어긋난 이름을 배너와 report.json에 남긴다(unmatched_column / unmatched_field) — 조용한 빈칸은 없다.
원본 자료(엑셀·메모·기존 문서)가 제각각이면 접혀 있는 **"AI로 명단 정리하기"**를 편다 — 번호 붙은 3단계다: ⑴ 프롬프트 복사(2단계에서 정한 칸 이름·형식이 담긴다) → ⑵ ChatGPT·Claude 등에 붙여넣고 결과 복사 → ⑶ 결과 붙여넣기(형식 확인 후 위 명단 칸으로 합쳐진다). 이 경로만은 명단의 개인정보가 외부 AI 서비스로 전송된다(위저드 안에도 고지된다) — 양식·채움·검증은 그대로 브라우저 안에서만 처리된다.
4단계 · 생성·검수 — 만들기 버튼을 누르면 진행률과 함께 N부가 생성되고, 끝나면 인원별 결과를 실제로
다시 렌더해 보여준다. 채운 칸은 문서 위에 하이라이트되고 ◀▶로 한 명씩 넘겨 확인한다. 문제가 있는 행에는
배지가 붙고, 하단 고정 바에 집계(생성 N·실패 n·경고 n — report.json과 같은 수)가 항상 보인다.
5단계 · 내려받기 — 하단 바의 zip 버튼이 종착지다(개별 .hwpx + report.json). 내려받은 뒤에도
검수로 돌아가거나 처음부터 다시 시작할 수 있다.
터미널로 하기 — inspect → 검수 → fill
설치는 CLI 가이드를 따른다.
1. 초안 유도
auto-hwp inspect 양식.hwpx --out fill-map.json
라벨 사전(성명·생년월일·연락처·주소·기업명·대표자·사업자등록번호·계약기간 등)으로 표를 훑어
라벨 오른쪽 첫 칸을 값칸 후보로 잡는다. --out을 생략하면 표준출력으로 나온다.
2. 검수 — 이 단계를 건너뛰지 마라
inspect가 내는 것은 초안이다. 실물 공공기관 양식 49종을 훑은 결과 같은 라벨이 여러 곳에 있는
경우가 37건이라, 라벨만으로는 어느 칸인지 확정할 수 없다. 그래서 실행은 pin(명시 주소)만 신뢰한다 —
pin이 없는 필드는 채우지 않고 unpinned로 보고한다.
{
"schema": "autohwp.fillmap.v1",
"template": { "path": "양식.hwpx" },
"fields": [
{
"key": "성명", // 명단의 열 이름과 일치해야 한다
"target": { "kind": "label-right", "label": "성 명" },
"pin": { "section": 0, "index": 6, "row": 1, "col": 1 }, // 실행이 신뢰하는 유일한 주소
"example": "김곤충", // 원래 그 칸에 있던 값(참고용)
"required": false,
"ambiguous": 3 // 같은 라벨이 3곳 — 반드시 확인
}
]
}
target.kind:
| kind | 뜻 |
|---|---|
label-right | 라벨 오른쪽 첫 칸 (기본 — 원본을 고치지 않은 양식) |
cell | 표의 특정 셀 |
replace | 본문 문자열 치환. target.query에 찾을 문자열을 넣는다 |
placeholder | {{키}} 자리표시자 (우리가 만든 템플릿용) |
ambiguous가 붙은 필드는 pin을 직접 확인하라. 값칸이 빈칸이라는 보장은 없다 — 실측상 예시
텍스트가 들어 있는 칸이 빈칸보다 많다(example이 그 원래 값이다).
3. 채우기
auto-hwp fill 양식.hwpx \
--map fill-map.json \
--data 명단.xlsx \
--out 결과/ \
--pattern '{index:03d}_{성명}.hwpx'
| 인자 | 뜻 |
|---|---|
--map | 검수를 마친 fill-map JSON |
--data | 명단. .xlsx(첫 시트, 1행=키) · .json(객체 배열) · 단순 .csv(첫 줄 = 헤더 = 키) |
--out | 출력 디렉토리 (기본 fill-out) — 개별 파일 + output.zip + report.json |
--pattern | 파일명. {index} · {index:03d} · {키} 를 끼워 넣는다 (기본 {index:03d}_{성명}.hwpx) |
--strict | 검증에 걸린 행을 만들지 않고 건너뛴다 (기본: 만들되 needsReview로 보고) |
CLI의 CSV 파서는 단순하다 — 따옴표를 발견하면 조용히 잘못 읽는 대신 정직하게 거부한다.
값에 콤마·따옴표가 들어가면 .xlsx나 JSON 명단을 써라. .xlsx는 첫 시트만 읽고, 시트가
둘 이상이거나 병합 셀이 있으면 거부한다. 열 수가 헤더와 다른 행도 에러다.
검증 — 무엇을 보장하나
산출물은 만든 뒤 다시 열어서 확인한다. 조용히 넘어가는 실패는 없다.
| 검사 | 방법 |
|---|---|
| 값이 실제로 들어갔나 | 산출물을 다시 열어 평문에서 채운 값을 찾는다 |
| 넘쳐서 쪽이 늘지 않았나 | 쪽수를 기준선과 비교한다 (아래 참고) |
| 형식이 맞나 (웹만) | 날짜·전화번호·사업자번호·숫자 정규식 |
쪽수 기준선은 "원본 쪽수"가 아니라 "아무것도 고치지 않고 한 번 왕복시킨 산출물의 쪽수"다.
HWPX 입력은 왕복이 바이트 동일이라 원본 쪽수와 같다. .hwp 입력은 변환 과정 자체에 리플로가 있어서
원본 쪽수를 기준으로 삼으면 편집 탓이 아닌 차이를 편집 탓으로 보고하게 된다.
report.json:
{
"template": "양식.hwpx",
"baselinePages": 17,
"rows": [
{ "file": "001_김하나.hwpx", "created": true, "needsReview": false, "reasons": [] },
{ "file": "002_이두리.hwpx", "created": true, "needsReview": true,
"reasons": ["missing_required:연락처"] }
],
"created": 2,
"skipped": 0
}
사유 코드:
| 코드 | 뜻 |
|---|---|
missing_required:<키> | 필수 필드인데 명단에 값이 없다 |
unpinned:<키> | pin이 없어 채우지 않았다 (검수 미완) |
bad_target:<키> | replace인데 target.query가 없다 |
apply_failed:<키>:<사유> | 엔진이 그 편집을 거부했다 (예: 병합 커버 셀) |
value_not_found:<값> | 채웠다는 값이 산출물 평문에 없다 |
overflow:pages_<실제>_vs_<기준선> | 쪽수가 기준선과 다르다 — 값이 길어 넘쳤을 수 있다 |
format_mismatch:<키>(…) | (웹) 규정한 형식에 맞지 않는 값 |
row_failed:<사유> | (웹) 그 행이 생성 도중 실패했다 — zip에서 빠지고(created: false) 나머지 부수는 그대로 남는다 |
unmatched_column:<열> | (웹) 명단의 열이 어느 영역 이름과도 같지 않아 문서에 들어가지 않았다 |
unmatched_field:<키> | (웹) 영역에 대응하는 명단 열이 없어 전 행이 빈칸이다 |
웹 report.json은 행 사유코드와 별도로 배치 전체의 이름 대조 결과를 warnings(위 unmatched_* 코드)에
담고, 만들어진 부수를 created/skipped로 적는다.
한계 (정직 고지)
.hwp입력은 산출물이 변환본이다. 채우는 것 자체는 되지만,.hwp→ HWPX 변환에 리플로가 있어 쪽 나눔·표 너비가 원본과 달라질 수 있다(실사용에서 관측됨). 편집 탓이 아니라 변환 축의 문제다. 서식이 그대로여야 한다면 한글에서.hwpx로 저장한 양식을 쓰라 — HWPX 입력은 고치지 않은 영역이 바이트 그대로 보존된다..hwp로는 저장할 수 없다. 산출물은 항상.hwpx다(한글이 네이티브로 연다). 기관이.hwp만 받는다면 산출된.hwpx를 한글에서 열어.hwp로 저장하는 우회가 필요하다.- 누름틀(click-here) 필드는 채우지 못한다. 읽기는 되지만 쓰기 명령이 아직 없다. 대부분의 기관 양식은 표 셀 기반이라 v1은 셀 + 본문 치환으로 충분하다.
- 셀 안 도장·서명 이미지는 넣지 못한다. 이미지 삽입은 블록 단위만 된다.
- 병합된 칸의 커버 셀은 편집을 거부한다(정직 거부) — 실제 값이 들어가는 활성 셀만 대상이다.
- 라벨 자동 유도는 만능이 아니다: 실물 49종 중 30종에서 유도됐고, 유도 0인 19종은 보도자료·공고문류 (애초에 채울 양식이 아니다). 값칸의 96%가 라벨 오른쪽이었다.
- 산출물이 기관 PC의 한글에서 잘 열리는지가 결국 전부다. 제출 전 한 부는 직접 열어 확인하라.
더 읽기
- 설계 근거와 실측(코퍼스 스윕·생태계 조사·실사용 리포트):
docs/issues/073-bulk-fill.md - CLI 전반: CLI 가이드
- 구현:
crates/auto-hwp-cli/src/fill.rs(터미널) ·apps/hwp-lab/src/app/bulk/page.tsx(웹) — 편집 레인이 같은apply_intent경로라 두 표면의 검증·거부 규칙이 한 벌이다.