이 사이트가 뭔가요?
공무원이 매일 쓰는 서면보고와 보도자료를 AI가 자동으로 만들어주는 도구예요. 주제만 입력하면 표준 번호체계(Ⅰ/□/○)가 적용된 진짜 한글 파일(.hwpx)이 나옵니다.
이 저장소는 4가지로 구성돼 있어요
❶ JS 라이브러리 ──── 핵심 엔진. HWPX를 만드는 코드.
❷ JS 스킬 ────────── Claude AI가 ❶을 쓸 수 있게 해주는 설명서.
❸ Python 스킬 ───── 같은 기능의 Python 버전. 의존성 0.
❹ 웹 데모 ────────── ❶에 UI를 입혀서 클릭만으로 쓸 수 있게 한 것.
(지금 보고 있는 이 사이트)
각각 누가 쓰나요?
❶ JS 라이브러리 — 개발자가 npm이나 <script>로 가져다 쓰는 용도
❷ JS 스킬 — Claude AI가 Node.js 환경에서 ❶을 활용하는 용도
❸ Python 스킬 — Claude AI가 Python 환경(Timely 등)에서 활용하는 용도
❹ 웹 데모 — 비개발자(공무원)가 브라우저에서 클릭만으로 문서 생성
이것들이 어떻게 연결되나요?
❶ JS 라이브러리 (핵심 코드)
│
├──→ ❷ JS 스킬: "이걸 이렇게 써"라고 Claude에게 알려줌
│
└──→ ❹ 웹 데모: 이걸 <script>로 불러서 UI를 입힘
❸ Python 스킬:
└──→ ❶과 같은 base64 템플릿을 쓰되 Python으로 독립 구현
Skill(스킬)이 뭔가요?
Skill은 Claude AI가 특정 작업을 잘 하도록 모아놓은 "전문 지식 꾸러미"예요. SKILL.md(설명서) + 코드 + 참고자료를 폴더로 묶으면 스킬이 됩니다.
비유 — 요리사에게 "백종원 레시피북"을 건네주는 것. 요리사는 원래도 요리하지만, 그 책이 있으면 특정 요리를 훨씬 잘 만들어요. AI도 마찬가지.
라이브러리 vs 스킬: 라이브러리(❶)는 코드 자체이고, 스킬(❷❸)은 "이 코드를 이렇게 쓰면 돼"라는 설명서예요. Claude는 설명서(스킬)를 읽고 코드(라이브러리)를 실행합니다.
스킬 구하는 곳: Anthropic 공식 GitHub, Claude Code 플러그인 마켓플레이스, 또는 직접 만들기
핵심: 왜 손상 없이 열릴까?
HWPX는 ZIP 파일이에요. 안에 XML이 여러 개 들어있는데, 한컴 오피스가 까다롭게 검사하는 header.xml(폰트·스타일)은 실제 한컴에서 만든 원본을 base64로 인코딩해서 그대로 씁니다. 내용이 담기는 section0.xml만 새로 조립하니까 손상이 없어요.
❶ JS 라이브러리와 ❸ Python 스킬 모두 동일한 base64 템플릿을 공유합니다.
파일 구조
hwpx-generator/
│
├─ ❶ JS 라이브러리 (핵심 엔진)
│ ├── generate_hwpx_browser.js 브라우저용 생성기
│ ├── generate_hwpx_node.js Node.js용 생성기
│ ├── hwpx_template_browser.js 서면보고 템플릿 (base64)
│ ├── hwpx_template_node.js Node.js용 동일 템플릿
│ └── hwpx_press_template.js 보도자료 템플릿 (base64)
│
├─ ❷ JS 스킬
│ └── skill-js/
│ ├── SKILL.md Claude용 설명서 + 규칙
│ └── references/ 가이드라인 참고자료
│
├─ ❸ Python 스킬 (의존성 0)
│ └── skill-python/
│ ├── SKILL.md
│ └── scripts/
│ ├── generate_hwpx.py stdlib만 사용
│ ├── report_template.py ❶과 동일 base64 템플릿
│ └── press_template.py
│
├─ ❹ 웹 데모
│ ├── index.html 전체 UI (이 파일)
│ └── netlify/
│ ├── edge-functions/chat.ts SSE 프록시 (Deno, 50s)
│ └── functions/chat.mjs 동기 fallback (10s)
│
└── README.md
HWPX 파일 구조
보고서.hwpx (= ZIP)
├── mimetype "application/hwp+zip"
├── Contents/
│ ├── header.xml ← 폰트/스타일 (템플릿 그대로)
│ ├── section0.xml ← 실제 내용 (이것만 새로 조립)
│ └── content.hpf ← 메타 (템플릿 그대로)
├── META-INF/ ← 컨테이너 (템플릿 그대로)
└── ...
header.xml 등은 한컴에서 만든 원본을 base64 인코딩해서 그대로 사용. section0.xml만 새로 조립하므로 손상 없음.
❶→❹ 포팅: Node.js → 브라우저
// Node.js (❶)
const fs = require('fs');
const JSZip = require('jszip');
fs.writeFileSync('out.hwpx', await zip.generateAsync({type:'nodebuffer'}));
// 브라우저 (❹)
<script src="jszip.min.js"></script>
const blob = await zip.generateAsync({ type:'blob', compression:'DEFLATE' });
URL.createObjectURL(blob); // → <a download>
❷ JS 스킬 vs ❸ Python 스킬
JS 스킬: SKILL.md가 Claude에게 "루트의 generate_hwpx_node.js를 require()해서 써"라고 알려줌. 코드 자체는 ❶에 있음.
Python 스킬: 코드를 Python으로 독립 구현. pip install 불필요 — zipfile + xml.etree만 사용. ❶과 동일한 base64 템플릿을 .py 파일에 내장.
❹ 웹 데모: Edge Function SSE
Netlify sync function 10초 타임아웃 → 504 빈발. 해결: Edge Function (Deno, 50초) + SSE pass-through
// netlify/edge-functions/chat.ts
export default async (req) => {
const body = await req.json();
const r = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'x-api-key': Netlify.env.get('CLAUDE_API_KEY'), ... },
body: JSON.stringify({ ...body, stream: true }),
});
return new Response(r.body, {
headers: { 'Content-Type': 'text/event-stream' }
});
};
❹ 양식 자동 채우기
<hp:t> 텍스트 노드를 분류:
label ("용 역 명") → 불변 · blank (공백) → 값 교체 · mixed (": 금___원") → 공백에만 삽입 · body/tab → 불변
같은 ctx 중복 필드(6개 서류의 상호·대표자)는 자동 복제.
Tech Stack
❶ JS JSZip + DOMParser ·
❸ Python stdlib (zipfile + xml.etree) ·
❹ Design Fraunces + Noto Serif KR ·
❹ AI Claude Sonnet 4.5 SSE ·
❹ Infra Netlify Edge Functions (Deno) ·
GitHub