@kaydash9999/banker-plugins 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: motion-graphic-make
3
+ description: "(banker) 10초 내외 내레이션 없는 무료 모션 그래픽(킨네틱 타이포·통계·차트·로고·하단자막·지도)을 hyperframes motion-graphics 워크플로에 위임 제작 — 렌더링 로직은 소유하지 않는 얇은 래퍼. 'motion-graphic-make'/'모션 그래픽 만들어줘'/'모션 그래픽 제작'/'motion graphics' 시 사용."
4
+ ---
5
+
6
+ # motion-graphic-make — 무료 모션 그래픽 제작 (hyperframes 위임)
7
+
8
+ 짧고(약 10초 내외, 최대 ~30초) 내레이션 없는 디자인-주도 모션 그래픽을 만든다.
9
+ 이 스킬은 **얇은 래퍼(thin wrapper)** 다 — 실제 창작·렌더링 로직을 전혀 소유하지 않고, hyperframes(Apache-2.0, HeyGen 오픈소스)의 `/motion-graphics` 워크플로에 그대로 위임한다.
10
+ 전제조건(Node≥22·ffmpeg) 설치는 `motion-graphic-setup` 스킬이 담당한다.
11
+ 답변은 한글(기술 토큰 영문).
12
+
13
+ ## 0. 전제조건: hyperframes (없으면 설치부터)
14
+ 이 스킬은 hyperframes CLI 가 설치·정상 동작해야 한다.
15
+ **진행 전 가용성을 먼저 확인**하고, 문제가 있으면 **설치부터 안내**한다(추정·임의 재설치 금지):
16
+ - 확인: `npx hyperframes doctor`
17
+ - 실패/미설치면 먼저 `motion-graphic-setup` 스킬로 설치(Claude Code: `/banker:setup` → motion-graphic-setup / Codex: `banker-motion-graphic-setup`). 설치·검증 후 이 스킬을 이어서 진행한다.
18
+
19
+ ## hyperframes 란 (요약)
20
+ hyperframes 는 HTML/CSS + 시크(seek) 가능한 애니메이션(GSAP·CSS·Lottie·Three.js 등)을 결정론적 MP4/투명 오버레이 비디오로 렌더링하는 오픈소스 프레임워크다.
21
+ 진입점은 `/hyperframes` 라우터다 — 모든 "영상/애니메이션 만들어줘" 요청을 적절한 창작 워크플로로 분기하는 역량 지도다.
22
+ 이 스킬이 다루는 대상은 그 창작 워크플로 중 하나인 `/motion-graphics` — 짧고 내레이션 없는 디자인-주도 모션 그래픽 하나로 한정된다.
23
+ 더 길거나·내레이션이 있거나·멀티씬이면 hyperframes 자신이 `/general-video` 등 다른 워크플로로 재라우팅한다(이 스킬이 판단하지 않는다).
24
+
25
+ ## 무료 폼 카테고리 (검색 불필요 · 유료 API 없음)
26
+ `asset_needs` 가 비어 있어 사용자가 콘텐츠를 직접 제공하는 6개 카테고리 — 전부 무료로 동작한다.
27
+
28
+ | 카테고리 | 내용 |
29
+ |---|---|
30
+ | `kinetic-type` | 문구/제목의 모션 타이포그래피 |
31
+ | `stat` | 히어로 숫자 카운트업 + 링 |
32
+ | `charts` | 막대/선/원형/레이스 차트 |
33
+ | `logo-reveal` | 로고 스팅 / 브랜드 락업 |
34
+ | `lower-thirds` | 이름/직함 바, 콜아웃, 소셜 오버레이 |
35
+ | `maps` | 지역 하이라이트 · 지점 연결 · 위치 줌 |
36
+
37
+ 검색이 필요한 카테고리(웹페이지/뉴스/트윗/이미지 합성)도 hyperframes 안에 존재하지만, 검색 프로바이더가 없으면 자동으로 asset-free 로 성능이 저하(degrade)한다 — 이 스킬의 초점은 위 6개 무료 폼 카테고리다.
38
+ 이미지 생성이 필요한 경우에 한해 `GEMINI_API_KEY`/`GOOGLE_API_KEY` 를 선택적으로 쓰며, 없으면 생성 단계를 건너뛸 뿐 실패하지 않는다.
39
+
40
+ ## 이 스킬이 소유하지 않는 것
41
+ **GSAP 애니메이션 작성, HTML 컴포지션 코딩, ffmpeg 인코딩, headless Chrome 캡처 — 이 중 어느 것도 이 스킬이 직접 구현하지 않는다.**
42
+ 전부 hyperframes 자체 서브에이전트(director/builder)와 CLI(`lint`/`check`/`snapshot`/`render`)가 수행한다.
43
+ 이 규칙을 여기서 재서술하면 hyperframes 본체와 어긋나는 사본이 생긴다 — 정본은 항상 hyperframes 쪽이다.
44
+ 이 스킬의 역할은 **호출 · 전제조건 확인 · 렌더 승인 게이트 강제**뿐이다.
45
+
46
+ ## 흐름
47
+ 1. **plan** — 사용자 요청(브리프)을 그대로 hyperframes `/motion-graphics`(필요시 `/hyperframes` 라우터 경유)에 전달한다. 카테고리 자동 분류와 초안 계획은 hyperframes 가 수행한다.
48
+ 2. **build** — hyperframes 가 카탈로그 블록을 재사용해 컴포지션 HTML 을 만든다(이 스킬의 직접 코드 작성 없음).
49
+ 3. **lint / check / snapshot** — hyperframes CLI 가 결함을 사전 검출하고, 필요하면 자체 리페어 패스를 한 번 더 돈다.
50
+ 4. **사용자 승인 게이트** — "미리보기 먼저 볼지, 바로 렌더할지" 하나만 묻는다. 미리보기를 고르면 hyperframes Studio 로 보여주고 다시 같은 질문으로 돌아온다.
51
+ 5. **render** — **명시적 렌더 승인 이후에만** 실행한다. 불투명 결과는 MP4, 오버레이가 필요하면 투명 webm/mov 로 렌더한다.
52
+
53
+ **렌더는 사용자의 명시적 승인 없이는 절대 실행하지 않는다** — 승인 전에는 미리보기 단계에서 멈추는 것이 기본값이다.
54
+
55
+ ## 완료 보고
56
+ 렌더가 끝나면 산출물 경로, 실제 길이(duration), 사용된 컴포지션/프레임 id, 확인에 쓴 스냅샷 시점을 함께 보고한다.
57
+ 렌더 없이 미리보기에서 멈췄다면, 어디까지 진행됐고 다음에 무엇을 승인하면 렌더로 이어지는지만 짧게 보고한다.
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: motion-graphic-setup
3
+ description: "(banker) 모션 그래픽 제작 도구 hyperframes 설치 전제조건 확보: Node>=22 + ffmpeg(OS 패키지 우선, project-local ffmpeg-static 폴백) 후 npx hyperframes init/skills update/doctor. 'motion-graphic-setup'/'모션 그래픽 설치'/'hyperframes 설치' 또는 /banker:setup 시 사용."
4
+ ---
5
+
6
+ # motion-graphic-setup — hyperframes(모션 그래픽 CLI) 설치
7
+
8
+ 무료(free) 모션 그래픽 제작 도구 hyperframes 의 실행 전제조건을 설치한다.
9
+ hyperframes 는 Node.js CLI 로, HTML/CSS/GSAP 로 만든 애니메이션을 headless Chrome + ffmpeg 로 MP4/투명 오버레이 비디오로 렌더링한다.
10
+ 실제 모션 그래픽 제작은 `motion-graphic-make` 스킬(hyperframes `/motion-graphics` 위임)이 담당하고, 이 스킬은 그 전제조건만 갖춘다.
11
+ **OS·arch·기존 설치 여부를 먼저 감지**해 알맞은 경로를 고른다.
12
+ 설치는 **멱등**(이미 있으면 skip·검증만). 답변은 한글(기술 토큰 영문).
13
+
14
+ ## 0. 감지 (먼저, 추정 금지)
15
+ ```bash
16
+ echo "OS=$(uname -s)"
17
+ echo "ARCH=$(uname -m)"
18
+ echo "root=$([ "$(id -u)" = 0 ] && echo yes || echo no)"
19
+ [ -f /etc/os-release ] && . /etc/os-release && echo "distro=$ID ${VERSION_ID}"
20
+ command -v node >/dev/null && echo "node=$(node -v)" || echo "node=미설치"
21
+ command -v npx >/dev/null && echo "npx=yes" || echo "npx=미설치"
22
+ command -v npm >/dev/null && echo "npm=$(npm -v)" || echo "npm=미설치"
23
+ command -v ffmpeg >/dev/null && ffmpeg -version 2>&1 | head -1 || echo "ffmpeg=미설치"
24
+ ```
25
+ ```powershell
26
+ $PSVersionTable.PSVersion
27
+ winget --version
28
+ if (Get-Command node -ErrorAction SilentlyContinue) { node -v } else { "node: 미설치" }
29
+ if (Get-Command ffmpeg -ErrorAction SilentlyContinue) { (ffmpeg -version)[0] } else { "ffmpeg: 미설치" }
30
+ ```
31
+ **이미 설치 확인**: `node -v` 가 22 이상 **이고** `ffmpeg` 가 존재 **이고** `npx hyperframes doctor` 가 통과하면, 아래 §1~§3 은 건너뛰고 §4 검증만 수행한다.
32
+
33
+ ## 1. Node ≥22 확보
34
+ 목적은 Node>=22 확보이지 특정 설치 경로 강제가 아니다.
35
+ 이미 nvm 등 버전 매니저로 관리 중이면 `nvm install --lts && nvm alias default 'lts/*'` 로 충분하다(멀티버전 관리·RHEL8 glibc 사전확인 등 상세 절차는 `setup-node` 스킬 참조).
36
+
37
+ ### macOS (Apple Silicon / Intel)
38
+ ```bash
39
+ brew install node
40
+ node -v
41
+ ```
42
+ Apple Silicon(`/opt/homebrew`)·Intel(`/usr/local`) 차이는 `brew` 가 자동 흡수 — 스크립트는 두 아키텍처에 동일 적용.
43
+
44
+ ### Linux — Ubuntu / Debian
45
+ ```bash
46
+ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
47
+ sudo apt-get install -y nodejs
48
+ node -v
49
+ ```
50
+ 배포 기본 apt 저장소의 `nodejs` 는 버전이 낮은 경우가 많아 NodeSource 22.x 저장소를 사용한다.
51
+
52
+ ### Linux — RHEL8 / Rocky8
53
+ ⚠️ dnf AppStream 모듈의 `nodejs` 스트림은 미러 구성에 따라 22 미만일 수 있다 — 사전 확인: `dnf module list nodejs`.
54
+ ```bash
55
+ sudo dnf module reset -y nodejs
56
+ sudo dnf module enable -y nodejs:22 2>/dev/null || \
57
+ curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo -E bash -
58
+ sudo dnf install -y nodejs
59
+ node -v
60
+ ```
61
+ 22 스트림이 없거나 sudo 가 불가하면 **`setup-node` 스킬로 위임**(nvm 경로, RHEL8 glibc 2.28 사전확인 포함) — 이쪽이 더 안전하다.
62
+
63
+ ### Windows
64
+ ```powershell
65
+ winget install --id OpenJS.NodeJS.LTS -e
66
+ node -v
67
+ ```
68
+ 여러 Node 버전을 넘나들어야 하면 nvm-windows: `winget install --id CoreyButler.NVMforWindows -e` 후 `nvm install lts && nvm use <설치된 버전>`.
69
+ > ⚠️ winget Node 와 nvm-windows 를 동시에 설치하지 않는다(PATH 충돌 — 상세는 `setup-node` 함정 참조).
70
+
71
+ **위 경로가 모두 막히면(권한 없음·미러 문제 등) → `setup-node` 스킬로 위임한다.**
72
+
73
+ ## 2. ffmpeg 확보
74
+ hyperframes 렌더러(headless Chrome 캡처 + ffmpeg 인코딩)가 요구하는 `ffmpeg` 실행 파일을 확보한다.
75
+ 1순위는 시스템 패키지, 2순위(권한 없음/오프라인/저장소 미제공)는 프로젝트-로컬 `ffmpeg-static` npm 폴백이다.
76
+
77
+ **★ banker 플러그인 자체(스킬 파일·npm 패키지 배포물)에는 ffmpeg 바이너리를 절대 번들하지 않는다.** 아래 폴백은 사용자 프로젝트의 `node_modules` 에만 설치된다.
78
+
79
+ ### macOS
80
+ ```bash
81
+ brew install ffmpeg
82
+ ffmpeg -version | head -1
83
+ ```
84
+
85
+ ### Linux — Ubuntu / Debian
86
+ ```bash
87
+ sudo apt-get install -y ffmpeg
88
+ ffmpeg -version | head -1
89
+ ```
90
+
91
+ ### Linux — RHEL8 / Rocky8
92
+ ⚠️ 기본 BaseOS/AppStream 저장소에는 `ffmpeg` 가 없다(라이선스 문제로 RPM Fusion 필요).
93
+ ```bash
94
+ sudo dnf install -y https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm
95
+ sudo dnf install -y ffmpeg
96
+ ffmpeg -version | head -1
97
+ ```
98
+ 사내망/에어갭 등으로 RPM Fusion 접근이 막히면 dnf 를 포기하고 바로 아래 폴백으로 진행한다.
99
+
100
+ ### Windows
101
+ ```powershell
102
+ winget install --id Gyan.FFmpeg -e
103
+ ffmpeg -version
104
+ ```
105
+
106
+ ### 폴백 — project-local `ffmpeg-static` (시스템 패키지 설치가 불가할 때만)
107
+ ```bash
108
+ npm install --no-save ffmpeg-static
109
+ export FFMPEG_PATH="$(node -e "console.log(require('ffmpeg-static'))")"
110
+ "$FFMPEG_PATH" -version | head -1
111
+ ```
112
+ - `--no-save`: 전역이 아니라 **현재 프로젝트의 `node_modules`** 에만 받는다 — 사용자 워크스페이스 소관이며 banker 플러그인 소관이 아니다.
113
+ - hyperframes 가 이 바이너리를 인식하는지는 §3 의 `npx hyperframes doctor` 로 확인한다.
114
+ - 재강조: 이 폴백은 사용자 프로젝트에 설치되는 npm 의존성일 뿐, banker 플러그인 저장소(`skills/`)나 배포 아티팩트에는 어떤 ffmpeg 바이너리도 포함되지 않는다.
115
+
116
+ ## 3. hyperframes 설치 · 최신화
117
+ ```bash
118
+ npx hyperframes init
119
+ npx hyperframes skills update
120
+ npx hyperframes doctor
121
+ ```
122
+ - `init`: 최초 실행 시 스캐폴딩과 함께 전역 스킬셋(라우터 `/hyperframes` + `hyperframes-*` 도메인 스킬 + `media-use`)을 설치하고, 이미 있으면 최신 여부만 GitHub 기준으로 확인한다.
123
+ - `skills update`: 코어 스킬만 골라 최신화한다(비대화형 환경 권장 경로) — 이미 최신이면 no-op.
124
+ - `doctor`: Node·ffmpeg·설치된 스킬 등 실행 환경을 종합 진단한다.
125
+
126
+ ## 4. 검증 (보고 의무)
127
+ ```bash
128
+ node -v # v22.x.x 이상 기대
129
+ command -v ffmpeg >/dev/null && ffmpeg -version | head -1 || echo "시스템 ffmpeg 없음 — ffmpeg-static 폴백 확인"
130
+ npx hyperframes doctor # 종합 진단(node/ffmpeg/스킬셋)
131
+ ```
132
+ 4-field 보고: 변경(설치 경로·ffmpeg 확보 방식) / Evidence(위 출력) / 검증(`node -v`≥22 + `hyperframes doctor` 통과) / Unknown(권한·네트워크 제약으로 시스템 ffmpeg 불가 등, 가짜 성공 금지).
133
+
134
+ ## 함정
135
+ - **RHEL8/Rocky8 dnf 모듈 nodejs 버전**: AppStream 스트림이 22 미만일 수 있음(사전 `dnf module list nodejs` 확인) → NodeSource 저장소 또는 `setup-node`(nvm) 로 위임.
136
+ - **RHEL8/Rocky8 ffmpeg 미제공**: BaseOS/AppStream 에 없음(라이선스) → RPM Fusion 필요, 접근 막히면 `ffmpeg-static` 폴백.
137
+ - **ffmpeg 바이너리 미번들 원칙**: `ffmpeg-static` 폴백은 사용자 프로젝트 `node_modules` 한정 — banker 플러그인 배포물에는 절대 포함하지 않는다(라이선스·배포 크기·플랫폼별 바이너리 불일치 회피).
138
+ - **GEMINI_API_KEY 는 선택**: AI 이미지 생성이 필요한 카테고리에서만 쓰이고, 무료 폼 카테고리(kinetic-type·stat·charts·logo-reveal·lower-thirds·maps)는 키 없이 전부 동작한다 — 이 스킬은 어떤 API 키도 요구하지 않는다.
139
+ - **Windows PATH 충돌**: winget Node 와 nvm-windows 동시 설치 금지(상세는 `setup-node` 함정 참조).
140
+ - **새 셸에만 반영**: NodeSource/dnf 모듈 설치 직후 PATH 갱신은 새 터미널이 필요할 수 있다.
141
+ - **hyperframes 스킬 갱신은 네트워크 필요**: `hyperframes init`/`skills update` 는 GitHub 에서 최신 스킬을 당겨온다 — 오프라인/에어갭 환경이면 실패를 정직히 보고(가짜 성공 금지).
@@ -127,9 +127,9 @@ node -e "const m=require('./codex/manifest.json');console.log(m.surfaces.filter(
127
127
  node -e "const p=require('./package.json');console.log(p.version, JSON.stringify(p.dependencies||{}))"
128
128
  ```
129
129
 
130
- **앞의 두 줄이 갈리면 그 차이가 사실이다.** 이 저장소에서 실제로 갈린다: 디렉터리는 49 인데 `SKILL.md` 보유는 48 이다.
130
+ **앞의 두 줄이 갈리면 그 차이가 사실이다.** 이 저장소에서 실제로 갈린다: 디렉터리는 55 인데 `SKILL.md` 보유는 54 이다.
131
131
  `skills/ultra-init-workspace/` 는 런타임 산출물 디렉터리라 `SKILL.md` 가 없고, 그래서 배포 매니페스트에도 없고 `package.json` `files[]` 도 negation 으로 뺀다.
132
- 즉 "디렉터리를 세면 스킬 수" 라는 그럴듯한 가정이 **여기서 1 을 더 센다.** 세 곳(매니페스트·`files[]`·테스트의 `SKILL.md` 필터)이 이미 48 을 말하는데 디렉터리 카운트만 49 라면, 틀린 쪽은 카운트다.
132
+ 즉 "디렉터리를 세면 스킬 수" 라는 그럴듯한 가정이 **여기서 1 을 더 센다.** 세 곳(매니페스트·`files[]`·테스트의 `SKILL.md` 필터)이 이미 54 를 말하는데 디렉터리 카운트만 55 라면, 틀린 쪽은 카운트다.
133
133
 
134
134
  **README 는 어떤 경우에도 사실이 아니다. 그리고 사실을 세는 명령도 틀릴 수 있다. 세 소스가 같은 값을 말할 때만 통과다.**
135
135
 
@@ -139,7 +139,7 @@ node -e "const p=require('./package.json');console.log(p.version, JSON.stringify
139
139
 
140
140
  | README 주장 | 위치 | 코드 사실 | 근거 | 판정 |
141
141
  |---|---|---|---|---|
142
- | "스킬 42개" | 10행·131행 | 48 | `ls -d skills/*/SKILL.md` = 48, 매니페스트 skill 표면 = 48 (디렉터리 카운트는 49 라 셋 중 혼자 다르다) | 드리프트: 2곳 다 고침 |
142
+ | "스킬 42개" | 10행·131행 | 53 | `ls -d skills/*/SKILL.md` = 54, 매니페스트 skill 표면 = 54 (디렉터리 카운트는 55 라 셋 중 혼자 다르다) | 드리프트: 2곳 다 고침 |
143
143
  | "`--fast` 지원" | 60행 | 없음 | 파서에 해당 플래그 없음 | 드리프트: 문장 삭제 |
144
144
  | "Node 18+ 필요" | 20행 | `engines.node: ">=18"` | package.json | 통과 |
145
145
 
@@ -0,0 +1,409 @@
1
+ ---
2
+ name: vertical-pptx
3
+ description: "(banker) A4 세로(210x297mm) 규격 PPTX 생성·점검·수리와 16:9 덱의 A4 세로 변환. 'A4 세로 pptx'/'세로형 슬라이드'/'인쇄용 pptx'/'vertical-pptx' 시 사용."
4
+ ---
5
+
6
+ # vertical-pptx - A4 세로(210x297mm) 규격 PPTX
7
+
8
+ 인쇄해서 돌리는 문서형 덱을 A4 세로 규격으로 만들고, 그 결과가 정말 A4 세로인지 파일 바이트로 판정한다.
9
+ 이 스킬이 소유하는 것은 **규격과 최소 결정론적 레이아웃**이다: 210x297mm·여백·8열 20행 그리드·한글 3슬롯·인쇄 pt·검증·수리.
10
+ 색 팔레트·차트·아이콘·디자인 감각은 소유하지 않는다. 그건 아래 §위임 에 맡긴다.
11
+
12
+ self-contained 번들 5파일이고 저장소에 의존성을 하나도 더하지 않는다.
13
+
14
+ | 파일 | 역할 |
15
+ |---|---|
16
+ | `SKILL.md` | 이 문서 |
17
+ | `build_a4p.js` | node 빌더(pptxgenjs) + 이 프로젝트의 **유일한** 마크다운 → deck JSON 변환기 |
18
+ | `build_a4p.py` | python 빌더(python-pptx). 입력은 deck JSON 뿐이고 `--md` 가 없다 |
19
+ | `verify_a4p.mjs` | 검증기·수리기·`--suggest-deck`. node stdlib 만 쓰고 외부 의존성이 0 이다 |
20
+ | `a4p.test.mjs` | 유닛(`node --test`). 외부 의존성 0 으로 상시 실행된다 |
21
+
22
+ ## When to use
23
+
24
+ - "A4 세로 pptx", "세로형 슬라이드", "인쇄용 pptx", "문서형 덱", "제안서 pptx" 를 만들 때
25
+ - 이미 있는 pptx 가 진짜 A4 세로인지 판정할 때(치수·`type` 속성·여백·도형 이탈)
26
+ - 16:9 덱을 A4 세로로 바꾸고 무엇이 파손됐는지 슬라이드별로 받아야 할 때
27
+ - 마크다운 원고를 그대로 인쇄 규격 덱으로 떨어뜨릴 때
28
+
29
+ ## 표준 레시피 (4단계)
30
+
31
+ `--suggest-deck` 은 고급 플래그가 아니라 **세 번째 단계**다.
32
+ 기본 상자에 안 들어가는 본문은 흔하고, 그때마다 왕복하는 것이 정상 워크플로다.
33
+
34
+ ```bash
35
+ S=<이 스킬 디렉터리>
36
+ F=<빌더가 보고한 fontFile 경로>
37
+
38
+ # 1) build 마크다운 → deck JSON → pptx
39
+ node $S/build_a4p.js --md 원고.md --emit-json > deck.json
40
+ node $S/build_a4p.js --in deck.json --out out.pptx
41
+
42
+ # 2) verify 규격·여백·이탈·오버플로 판정
43
+ node $S/verify_a4p.mjs out.pptx --font-file $F
44
+
45
+ # 3) suggest 오버플로가 있을 때만. rows 만 고친 새 deck JSON 을 낸다
46
+ node $S/verify_a4p.mjs out.pptx --deck deck.json --font-file $F --suggest-deck fixed.json
47
+
48
+ # 4) rebuild 고친 deck 으로 다시 빌드하고 2) 로 돌아가 확인
49
+ node $S/build_a4p.js --in fixed.json --out out.pptx
50
+ node $S/verify_a4p.mjs out.pptx --font-file $F
51
+ ```
52
+
53
+ `--emit-json` 에 `--out` 을 안 주면 stdout 은 deck JSON 전용이 되고 사람이 읽는 표는 stderr 로 간다.
54
+ 그래서 위처럼 `> deck.json` 리다이렉트가 안전하다.
55
+
56
+ python 빌더는 같은 deck JSON 을 먹는다.
57
+
58
+ ```bash
59
+ python build_a4p.py --in deck.json --out out.pptx
60
+ ```
61
+
62
+ ### 기본 상자 용량 (rows 를 언제 손대야 하는가)
63
+
64
+ `body` 기본값은 2행이다. 그 상자에 얼마나 들어가는지 모르면 `rows` 를 언제 올려야 하는지 알 수 없으므로 수치로 적는다.
65
+
66
+ - 2행 상자 높이 = `361800 * 2 + 144000` = 867600 EMU = **24.1mm**
67
+ - 10.5pt · 줄간 1.2 → 줄 높이 12.6pt = 4.445mm → **약 5줄**
68
+ - 10.5pt 에서 1em = 3.7042mm → 본문 폭 190mm = **51.3em**
69
+ - Noto Sans CJK KR 의 한글 advance 는 0.92em → **한 줄에 한글 약 55자**
70
+ - 즉 기본 2행 상자는 **한글 약 275자**(5줄 x 55자)
71
+
72
+ 가정을 함께 적는다.
73
+ `bodyPt` 10.5 · 줄간 1.2 · `marginMm` 10 · 8열 전폭 · 텍스트 상자 inset 0 · Noto Sans CJK KR 메트릭 기준이다.
74
+ 커널링과 줄바꿈 규칙은 반영하지 않았으므로 이 값은 **넉넉한 쪽 추정**이고, 마지막 줄이 꽉 차는 경우는 드물다.
75
+ `bodyPt` 를 11 로 올리면 줄당 글자 수가 약 5% 줄어든다.
76
+
77
+ 일반적인 마크다운 문단(100~300자)은 상당수가 들어가고 긴 문단은 넘친다.
78
+ "못 쓴다" 도 "대개 괜찮다" 도 아니라 **왕복이 상시로 필요한 수준**이라 4단계 레시피가 기본이다.
79
+
80
+ ## 규격 상수 (EMU)
81
+
82
+ 1mm = 36000 EMU 다. A4 세로는 그 정의에서 곧바로 나온다.
83
+
84
+ | 항목 | 식 | m=10 (기본) |
85
+ |---|---|---|
86
+ | 슬라이드 | `210 * 36000` x `297 * 36000` | **7560000 x 10692000** |
87
+ | 여백 | `36000m` | 360000 (10mm) |
88
+ | 본문 폭 | `7560000 - 72000m` | 6840000 (190mm) |
89
+ | 본문 높이 | `10692000 - 72000m` | 9972000 (277mm) |
90
+ | 거터 | 144000 (4mm 고정) | 144000 |
91
+ | 컬럼 폭 (8열) | `(182 - 2m) * 4500` | 729000 (20.25mm) |
92
+ | 행 높이 (20행) | `(221 - 2m) * 1800` | 361800 (10.05mm) |
93
+ | 컬럼 피치 | 컬럼 폭 + 144000 | 873000 |
94
+ | 행 피치 | 행 높이 + 144000 | 505800 |
95
+
96
+ 컬럼 `c` 에서 `k` 열: `x = 36000m + 컬럼피치 * c` · `cx = 컬럼폭 * k + 144000 * (k - 1)`.
97
+ 행 `r` 에서 `n` 행: `y = 36000m + 행피치 * r` · `cy = 행높이 * n + 144000 * (n - 1)`.
98
+
99
+ 검산: `k=8` → `cx = 6840000` 이고 `n=20` → `cy = 9972000` 으로 본문 영역과 정확히 일치한다.
100
+
101
+ `meta.marginMm` 은 **10 이상 40 이하의 정수**여야 한다.
102
+ 10 은 동결 스펙의 하한이고, 정수 제약은 위 식들의 EMU 정수성을 보장한다.
103
+ 범위를 벗어나거나 정수가 아니면 **exit 3**(사용법 오류)이다.
104
+
105
+ ## 프리셋 트랩 3종
106
+
107
+ 셋 다 이론이 아니라 실측이다. A4 라고 적힌 것이 A4 가 아닌 세 가지 경로다.
108
+
109
+ | # | 트랩 | 실측값 | 옳은 길 |
110
+ |---|---|---|---|
111
+ | 1 | PowerPoint 의 "A4 용지(210x297mm)" 프리셋 | 10.833 x 7.5in = **9906000 x 6858000**. 세로가 아니라 **가로**다 | 페이지 설정에서 **사용자 지정 21 x 29.7cm** 뿐이다 |
112
+ | 2 | 반올림한 인치 | `8.27in` → cx=**7562088** (+2088) · `11.69in` → cy=**10689336** (-2664) | 전정밀도 `210 / 25.4` 와 `297 / 25.4` |
113
+ | 3 | python-pptx 의 잔존 `type` | 치수는 A4 인데 `p:sldSz` 에 `type="screen4x3"` 가 그대로 남는다 | `type` 속성을 명시적으로 제거한다 |
114
+
115
+ **트랩 1 이 왜 생기나.** Microsoft 가 프린터 여백을 미리 빼고 프리셋을 만들어 뒀다.
116
+ 그래서 이름은 A4 인데 인쇄 가능 영역 치수이고, 실제 용지 규격이 아니다.
117
+ 이 값은 다른 오답과 성격이 달라서 검증기가 `PRESET_TRAP` 이라는 별도 판정 코드로 낸다.
118
+
119
+ **트랩 2 의 차이는 0.06mm 와 0.07mm 다.** 눈에는 안 보이지만 "이 파일이 A4 인가" 는 이진 질문이고 허용오차가 0 이다.
120
+ 검증기의 sldSz 비교에는 허용오차가 없다.
121
+
122
+ **트랩 3 이 무서운 이유는 치수만 봐서는 통과하기 때문이다.**
123
+ 치수와 선언이 어긋난 파일이고, 선언 쪽을 믿는 뷰어와 변환기가 있다.
124
+ **치수만으로는 파일을 판정할 수 없다**는 것이 이 트랩의 교훈이다.
125
+ pptxgenjs 는 `type` 속성을 아예 쓰지 않는데, OOXML 에서 이 속성의 기본값이 `custom` 이라 의미상 동일하다.
126
+ 그래서 판정 규칙은 "속성 부재 또는 `custom` 이면 통과, 그 밖의 값이면 실패" 다.
127
+
128
+ ## 여백과 그리드
129
+
130
+ 여백은 사방 **10mm 이상**이다.
131
+ 가정용·사무용 프린터 대부분이 가장자리 4~5mm 를 물리적으로 인쇄하지 못하기 때문이고, 재단 없이 그대로 배포되는 문서라 잘려도 복구할 방법이 없다.
132
+ 10mm 는 그 물리 한계에 여유를 얹은 값이다.
133
+
134
+ 그리드는 **8열 x 20행**이고 거터는 **4mm** 다.
135
+ 컬럼 폭 729000 EMU, 행 높이 361800 EMU 다(m=10).
136
+ 이 그리드는 A4 규격에서 도출되는 값이 아니라 이 스킬이 내린 **레이아웃 결정**이다. 그래서 소유한다고 정직하게 적는다.
137
+
138
+ ## 행 배정 규칙과 INV-G
139
+
140
+ ### INV-G (기하 불변식)
141
+
142
+ 모든 도형의 `x`·`y`·`cx`·`cy` 는 다음 값들**만**의 순수 함수다.
143
+
144
+ > `meta.marginMm` · 페이지 인덱스 · 블록 순서 · 블록 `type` · `bullets.items` 배열 길이 · 명시된 `rows` · 컬럼 스팬
145
+
146
+ **텍스트의 내용·길이·폰트·pt 는 어떤 좌표에도 들어가지 않는다.**
147
+
148
+ 이 불변식이 두 빌더를 교체 가능하게 만든다.
149
+ 같은 deck JSON 을 node 빌더와 python 빌더에 먹이면 좌표가 EMU 단위로 완전히 같아야 하고, 그 동등성 검사에 폰트 리더가 필요 없다.
150
+ 그리고 폭 측정이 빌더 출력을 한 바이트도 바꾸지 않으므로 측정은 검증기 단독 소유가 된다.
151
+
152
+ ### 행 수
153
+
154
+ ```
155
+ rows(block) = min(18, block.rows ?? default(type))
156
+
157
+ default(head) = 1
158
+ default(body) = 2
159
+ default(bullets) = max(1, items.length)
160
+ 페이지 제목(h1) = 2행 고정 (모든 슬라이드 상단에 예약)
161
+ ```
162
+
163
+ 모든 블록 타입이 `rows` 레버를 갖는다.
164
+ `body` 에만 있으면 한 줄에 안 들어가는 h2 도, 두 줄로 감기는 불릿 항목도 고칠 방법이 없다.
165
+
166
+ **18 클램프.** 제목이 2행을 먹으므로 블록이 쓸 수 있는 행은 `20 - 2 = 18` 이다.
167
+ 클램프가 없으면 `rows: 19` 짜리 합법 덱 하나가 슬라이드 밖 4.05mm 로 나가는 파일을 만든다.
168
+ `n=18` 이면 `y + cy = 10332000` 으로 본문 영역 하단과 정확히 일치하고, 이 일치는 `marginMm` 값과 무관하게 성립한다.
169
+
170
+ **클램프 발동 판정은 `block.rows` 가 아니라 `block.rows ?? default(type)` 로 한다.**
171
+ 항목 19개짜리 불릿 블록에 `rows` 를 안 적어도 기본값이 19 라 클램프가 걸린다.
172
+ 조건을 `block.rows` 로만 읽으면 경고 없이 항목 17·18 이 같은 행에 겹친 채 출하된다.
173
+ 클램프에 걸린 블록은 `BLOCK_TOO_TALL` 경고와 함께 자기 슬라이드에 혼자 놓인다.
174
+
175
+ **페이지 채우기는 세는 규칙뿐이다.** 블록을 순서대로 훑으며 `used += rows(block)` 하고, 18 을 넘기면 새 슬라이드를 연다.
176
+ **블록은 절대 쪼개지 않는다.** 쪼개려면 측정해야 하고, 측정은 여기서 하지 않는다.
177
+
178
+ **불릿 행 배분.** 항목 `i`(0-based)는 `min(i, n-1)` 행에서 시작하고 남는 행은 전부 마지막 항목이 가져간다.
179
+ `n < items.length` 면 `n-1` 행부터 항목들이 겹쳐 쌓이고, 그 겹침은 검증기가 보고한다.
180
+
181
+ ### 상자보다 긴 텍스트는 넘친다
182
+
183
+ 상자에 안 들어가는 텍스트는 **넘친다. 조용히 흘려 넣거나 옮기지 않는다.**
184
+
185
+ 이것은 결함이 아니라 **인쇄 규격 도구로서 내린 사양 결정**이다.
186
+ 자동 리플로우를 넣는 순간 텍스트 길이가 좌표에 들어가고 INV-G 가 깨진다.
187
+ 그러면 두 빌더의 동등성 검증이 폰트 리더 두 벌을 요구하게 되고, 원고를 한 글자 고칠 때마다 전체 페이지네이션이 움직여 어제 검토한 배치가 오늘 달라진다.
188
+ 인쇄용 문서에서 그건 규격 위반보다 나쁘다.
189
+
190
+ 고치는 길은 조용한 이동이 아니라 `--suggest-deck` 이다.
191
+ 검증기가 **실제로 재고 나서** `rows` 만 고친 새 deck JSON 을 내놓고, 사람이나 모델이 그 JSON 을 보고 다시 빌드한다.
192
+ 측정이 기하에 영향을 주는 통로는 이 하나뿐이고, 그 통로는 사람이 검토할 수 있는 파일을 지난다.
193
+
194
+ `rows: 18` 로도 안 들어가면 `rows: 19` 를 제안하지 않는다.
195
+ `BLOCK_TOO_TALL` 과 함께 "블록을 나눠라" 를 낸다. 어디서 나눌지는 측정이 필요하고 그건 이 스킬의 범위 밖이다.
196
+
197
+ ## 한글 3슬롯
198
+
199
+ run 의 `a:latin`·`a:ea`·`a:cs` **세 슬롯에 같은 typeface 를 넣어야 한다.**
200
+ `a:ea` 가 없으면 한글이 대체 폰트로 떨어져 일부 뷰어에서 tofu(빈 네모)가 나고, 줄바꿈 위치가 달라져 오버플로 판정 자체가 무의미해진다.
201
+
202
+ | 라이브러리 | 동작 | 필요한 조치 |
203
+ |---|---|---|
204
+ | pptxgenjs | `fontFace` 하나로 3슬롯을 모두 emit 한다 | 없음 |
205
+ | python-pptx | `run.font.name` 은 `a:latin` **하나만** emit 한다 | `a:ea`·`a:cs` 를 직접 주입한다 |
206
+
207
+ ### 폰트 이름을 하드코딩하지 않는 이유
208
+
209
+ **맑은 고딕을 박아 넣지 않는다.** Windows 전용 폰트다.
210
+ macOS 와 Linux 에서는 대체 폰트로 치환되고, 치환되면 글자 폭이 달라져 줄바꿈 위치가 이동한다.
211
+ 그러면 그리드에 맞춰 잡은 배치가 무너지고, 오버플로 판정도 함께 무효가 된다.
212
+ 검증기는 산출물에 적힌 typeface **이름**을 경로로 해석해 폭을 재기 때문에, 그 머신에 없는 이름이 적혀 있으면 판정 전체가 `estimate` 로 강등된다.
213
+
214
+ 빌더는 아래 체인을 위에서부터 훑으며 **실제로 파일이 존재하는 이름**을 고른다.
215
+
216
+ ```
217
+ --font <이름> → BANKER_PPTX_FONT → meta.font →
218
+ Noto Sans CJK KR → Malgun Gothic → Apple SD Gothic Neo → NanumGothic → 시스템 한국어 sans
219
+ ```
220
+
221
+ 명시한 이름(`--font`·환경변수·`meta.font`)이 해석되지 않으면 `FONT_NOT_FOUND` 경고를 남기고 다음으로 내려간다. 조용히 바꾸지 않는다.
222
+ **빌더는 고른 폰트 이름과 절대경로를 항상 보고한다.** 사람이 읽는 표와 stdout 마지막 줄 JSON 양쪽에 나온다.
223
+
224
+ ```json
225
+ {"skill":"vertical-pptx","builder":"node","font":"Noto Sans CJK KR","fontFile":"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc","slides":1,"warnings":[]}
226
+ ```
227
+
228
+ 그 `fontFile` 을 검증기에 `--font-file` 로 넘기면 폭을 실제 메트릭으로 잰다.
229
+ 검증기는 그 경로를 **믿지 않고 대조한다**. 폰트의 `name` 테이블 패밀리가 pptx 의 `a:latin` 과 다르면 경로를 버리고 이름 재해석으로 내려간다.
230
+ 경로는 존재하는데 다른 폰트인 경우가 이 설계에서 유일하게 남은 조용한 오답 경로라서 그렇다.
231
+
232
+ ## 본문은 10~11pt
233
+
234
+ 화면용 덱의 습관대로 18~24pt 를 쓰면 A4 한 장에 문단 몇 개밖에 안 들어간다.
235
+ 인쇄해서 손에 들고 읽는 문서의 본문 크기는 **10~11pt** 다. 기본값은 10.5pt 다.
236
+
237
+ **출처 둘.**
238
+
239
+ 1. <https://practicaltypography.com/point-size.html> - 인쇄물 본문은 10~12pt 이며, 12pt 가 기본처럼 느껴지는 것은 타자기 시대의 잔재라고 적는다.
240
+ 2. 「행정업무의 운영 및 혁신에 관한 규정」 **제8조** - 한국 공문서의 용지 규격을 210x297mm 로 정한다.
241
+
242
+ 두 번째 출처는 **조문 번호만 인용하고 조문 텍스트는 인용하지 않는다.**
243
+ law.go.kr 이 이 호스트에서 ECONNREFUSED 라 조문 원문을 열어 보지 못했기 때문이다.
244
+ 읽지 않은 문장을 인용문으로 적지 않는다는 뜻이고, 조문 텍스트가 필요하면 직접 확인해야 한다.
245
+
246
+ ## 방향은 한 파일에 섞을 수 없다
247
+
248
+ **하나의 pptx 는 하나의 슬라이드 크기만 갖는다.** `p:sldSz` 가 프레젠테이션 전체에 하나뿐이기 때문이고, Microsoft 문서도 한 프레젠테이션의 모든 슬라이드가 같은 방향을 공유한다고 명시한다.
249
+
250
+ "3페이지만 가로로" 같은 요청을 받으면 **먼저 이 제약을 말하고**, 방향별로 파일을 나누어 각각 빌드하라고 안내한다.
251
+ 가로 페이지가 섞인 deck JSON 을 넘기면 빌더가 **exit 3** 으로 거부하고 파일을 나누라는 안내를 낸다.
252
+
253
+ ## 폭 판정은 하한이다
254
+
255
+ 예측 폭은 **하한(lower bound)** 이다.
256
+ 커널링 쌍과 줄바꿈 규칙(한국어는 거의 아무 데서나 끊기고 라틴 문자는 아니다)을 재현하지 않기 때문에, 실제 폭은 계산값보다 넓을 수는 있어도 좁을 수는 없다.
257
+ 그래서 "넘친다" 는 판정은 건전하고, "안 넘친다" 는 그저 침묵이다.
258
+
259
+ `fontSource` 는 검증기 출력 필드이고 **기본값은 `estimate`** 다.
260
+ `measured` 는 실제 폰트 파일을 열고 그 `name` 테이블 패밀리가 run 의 `a:latin` 과 일치함을 확인한 뒤에야 붙는다.
261
+ `estimate` 모드에서는 검증기가 **"넘친다" 를 단정하지 않는다.** finding 은 `unconfirmed` 상태로 "확인 불가(estimate)" 라고만 적고 그 이유를 함께 낸다.
262
+
263
+ 라벨은 fail-safe 로 설계됐다.
264
+ 폰트를 못 연 릴리스는 자동으로 보수적이 되고, 틀린 폭에 `measured` 가 붙는 일은 없다.
265
+
266
+ ## 종료 코드
267
+
268
+ 세 스크립트가 같은 규약을 쓴다. 다른 스킬이 이 스크립트들을 호출할 때 기대할 계약이다.
269
+
270
+ | 코드 | 뜻 | 언제 나오나 |
271
+ |---|---|---|
272
+ | **0** | 통과 | 빌드 성공 · 검증 PASS |
273
+ | **1** | 검증 실패 | 규격 위반·도형 이탈·오버플로 확정 · 재압축 무결성 실패 |
274
+ | **2** | 환경 미비 | pptxgenjs 또는 python-pptx 미도달 · python 3.9 미만 |
275
+ | **3** | 사용법 오류 | deck 스키마 위반 · `marginMm` 범위 밖 · 방향 혼합 · 인자 오류 · 입력 파일 없음 |
276
+
277
+ exit 2 는 사용자가 그대로 따라 할 수 있는 안내라서 stdout 으로 나간다. 나머지 오류는 stderr 다.
278
+
279
+ ## node 가 이 스킬의 베이스라인이다
280
+
281
+ **검증 경로는 node 를 요구한다.**
282
+ `build_a4p.py` 는 node 가 없는 환경이 아니라 **pptxgenjs 를 설치할 수 없는 환경**을 위한 것이다.
283
+ python 으로 빌드했더라도 그 결과를 판정하려면 `verify_a4p.mjs` 를 돌려야 하고, 검증 없는 생성은 이 스킬의 가치가 아니다.
284
+
285
+ ## 위임
286
+
287
+ > **이번 세션에서 사용 가능한 스킬 목록에 pptx 문서 스킬(`document-skills:pptx`)이 보이면**, 레이아웃·색·차트·pptxgenjs 함정은 그쪽에 맡기고 이 스킬은 규격과 검증만 맡는다.
288
+ > 목록에 없으면 아래 §위임 불가 시 최소 지식 만으로 진행한다. 설치를 요구하며 멈추지 않는다.
289
+
290
+ 조건은 **런타임 이름이 아니라 관측 가능한 능력**이다.
291
+ "Codex 면" 으로 분기하면 양방향으로 틀린다.
292
+ `document-skills:pptx` 는 Claude Code 내장이 아니라 별도 플러그인이라 Claude 사용자에게도 없을 수 있고, Codex 쪽은 나중에 같은 능력을 얻을 수 있다.
293
+ 모델이 자기 컨텍스트에서 확인할 수 있는 사실("목록에 보이는가")로만 판정한다.
294
+
295
+ 위임한 뒤에도 규격 판정은 이 스킬이 한다. 상대가 만든 덱도 `verify_a4p.mjs` 를 통과해야 한다.
296
+
297
+ ## 위임 불가 시 최소 지식
298
+
299
+ 위임 대상이 목록에 없을 때 이 블록만으로 진행한다. 범위는 아래 여섯 항목이 전부다.
300
+
301
+ **색 팔레트·차트·아이콘·디자인 가이드는 여기 없다. 그건 위임 대상의 영역이다.**
302
+ **이 목록에 항목을 추가하려면 그것이 규격 판정에 필요한 이유를 함께 적는다.**
303
+
304
+ **1. 레이아웃을 슬라이드보다 먼저 확정한다.**
305
+ 슬라이드를 하나라도 추가한 뒤에 크기를 바꾸면 이미 만든 슬라이드가 옛 크기를 따른다.
306
+
307
+ ```js
308
+ // pptxgenjs
309
+ pptx.defineLayout({ name: 'A4P', width: 210 / 25.4, height: 297 / 25.4 });
310
+ pptx.layout = 'A4P';
311
+ // 그 다음에야 addSlide()
312
+ ```
313
+
314
+ ```python
315
+ # python-pptx
316
+ prs.slide_width = Emu(7560000)
317
+ prs.slide_height = Emu(10692000)
318
+ # p:sldSz 의 type 속성을 지운다 (기본값이 custom 이라 지우는 것이 곧 custom 선언이다)
319
+ ```
320
+
321
+ **2. 여백은 사방 10mm 이상이다.** 프린터가 가장자리 4~5mm 를 인쇄하지 못한다.
322
+ 좌표 단위는 EMU 이고 1mm = 36000 EMU 다. 여백 10mm = 360000, 본문 폭 = 6840000, 본문 높이 = 9972000.
323
+ pptxgenjs 의 `x`/`y`/`w`/`h` 는 100 이상이면 EMU 로, 그 미만이면 인치로 해석된다. 위 값들은 전부 100 이상이라 EMU 로 간다.
324
+
325
+ **3. A4 세로 기본 배치 4종** (단위 EMU, 여백 10mm 기준).
326
+
327
+ | 배치 | 도형 | x | y | cx | cy |
328
+ |---|---|---|---|---|---|
329
+ | 제목형 | 제목 | 360000 | 360000 | 6840000 | 867600 |
330
+ | 본문형 | 제목 | 360000 | 360000 | 6840000 | 867600 |
331
+ | | 본문 | 360000 | 1371600 | 6840000 | 867600 |
332
+ | 2단 | 좌 | 360000 | 1371600 | 3348000 | 867600 |
333
+ | | 우 | 3852000 | 1371600 | 3348000 | 867600 |
334
+ | 표 | 표 전체 | 360000 | 1371600 | 6840000 | 행 수 x 361800 |
335
+
336
+ 아래로 더 쌓을 때 `y` 는 `360000 + 505800 * r`(r = 0부터 19까지)을 쓰고, `n` 행짜리 상자의 `cy` 는 `361800 * n + 144000 * (n - 1)` 이다.
337
+ `y + cy` 가 **10332000** 을 넘으면 하단 여백을 침범한 것이다.
338
+
339
+ **4. 본문은 10~11pt 다.** 제목은 본문 + 6pt, 소제목은 본문 + 2pt 정도면 충분하다.
340
+ 줄간은 1.2 배수를 쓴다.
341
+
342
+ **5. 한글은 3슬롯 전부 채운다.** `a:latin`·`a:ea`·`a:cs` 에 같은 typeface 를 넣는다.
343
+ pptxgenjs 는 `fontFace` 하나면 되고, python-pptx 는 `a:ea`·`a:cs` 를 직접 주입해야 한다.
344
+ 폰트 이름은 그 머신에 실제로 있는 것을 쓴다. 맑은 고딕은 Windows 전용이라 하드코딩하지 않는다.
345
+
346
+ **6. 색은 `#` 없는 6자리 hex 다.** `'1A1A1A'` 는 되고 `'#1A1A1A'` 는 안 된다.
347
+ 두 라이브러리 모두 이 형식을 요구하고, `#` 를 붙이면 색이 조용히 무시되거나 XML 이 깨진다.
348
+
349
+ ## 의존성 (자동 설치하지 않는다)
350
+
351
+ | 도구 | 필요한 곳 | 없으면 |
352
+ |---|---|---|
353
+ | node (>= 16.7) | `verify_a4p.mjs` 전부 · `build_a4p.js` | 검증 자체가 불가능하다 |
354
+ | pptxgenjs (>= 4) | `build_a4p.js` 빌드 경로 | exit 2 + `docs-setup` 안내 |
355
+ | python 3.9+ 와 python-pptx | `build_a4p.py` | exit 2 + `docs-setup` 안내 |
356
+
357
+ `verify_a4p.mjs` 는 node stdlib 만 쓴다. 외부 의존성이 0 이라 어디서든 판정만은 돌릴 수 있다.
358
+
359
+ 스크립트는 **의존성을 자동 설치하지 않는다.** exit 2 와 함께 사람이 읽고 따라 할 수 있는 안내를 내고 멈춘다.
360
+ 런타임 준비 절차는 `docs-setup` 스킬(`/banker:docs-setup`)에 있다.
361
+
362
+ 일회성으로 붙여 볼 때는 프로젝트 트리를 건드리지 않는 스크래치 설치를 쓴다.
363
+
364
+ ```bash
365
+ npm i --no-save --no-audit --no-fund pptxgenjs@4
366
+ python -m pip install --target <dir> python-pptx fonttools
367
+ ```
368
+
369
+ `--target` 으로 받았으면 그 경로를 `PYTHONPATH` 에, npm 쪽은 `NODE_PATH` 에 넣어 부른다.
370
+
371
+ ## 수리와 변환
372
+
373
+ 이미 있는 덱을 고칠 때 쓴다. **도형은 하나도 움직이지 않는다.**
374
+
375
+ ```bash
376
+ node verify_a4p.mjs 남의덱.pptx --repair --out fixed.pptx # sldSz 만 A4 세로로 교체
377
+ node verify_a4p.mjs 16by9.pptx --convert-a4p --out a4p.pptx # 위 + 슬라이드별 파손 리포트
378
+ ```
379
+
380
+ 기본은 **새 파일**이다. 원본 덮어쓰기는 `--in-place` 로 명시해야 한다.
381
+ `--convert-a4p` 는 좁아진 폭 때문에 슬라이드 밖으로 나간 도형을 슬라이드별로 이탈량(mm)과 함께 열거한다.
382
+ 자동 재배치는 하지 않는다. 어디로 옮겨야 하는지는 이 도구가 아는 정보가 아니다.
383
+
384
+ **쓰기 성공과 규격 통과는 다른 주장이다.**
385
+ 수리한 뒤 찍히는 VERDICT 는 원본이 아니라 산출물을 다시 검증한 결과이고, sldSz 를 고쳐도 도형이 삐져나와 있으면 여전히 FAIL 이다.
386
+
387
+ ## 함정
388
+
389
+ - **`8.27 x 11.69` 를 쓰지 않는다.** cx 가 2088 EMU 어긋난다. `210 / 25.4` 와 `297 / 25.4` 를 그대로 넘긴다.
390
+ - **치수만 보고 판정하지 않는다.** python-pptx 산출물은 A4 치수에 `type="screen4x3"` 를 달고 나온다.
391
+ - **PowerPoint 의 "A4 용지" 프리셋을 고르지 않는다.** 그건 가로 10.833 x 7.5in 다.
392
+ - **`a:ea` 누락.** python-pptx 에서 가장 흔한 한글 깨짐 원인이다.
393
+ - **맑은 고딕 하드코딩.** Windows 전용이고, 치환되면 그리드와 오버플로 판정이 함께 무효가 된다.
394
+ - **`fontSource: estimate` 인데 "안 넘친다" 로 읽기.** 그건 판정이 아니라 침묵이다. `--font-file` 을 넘겨 `measured` 로 올린 뒤 읽는다.
395
+ - **`--suggest-deck` 을 `--deck` 없이 부르기.** pptx 는 (슬라이드, 도형)만 알려 주고 내놓아야 하는 것은 (페이지, 블록)의 `rows` 다. 원본 deck JSON 이 필수 입력이다.
396
+ - **python 빌더에 `--md` 를 찾기.** 없다. 마크다운 파서는 `build_a4p.js` 한 벌뿐이고, 두 벌이 되는 순간 같은 원고가 두 개의 덱이 된다.
397
+ - **넘치는 텍스트를 자동으로 옮겨 주길 기대하기.** 사양상 옮기지 않는다. `--suggest-deck` 이 그 자리다.
398
+
399
+ ## verify-before-claim (DoD)
400
+
401
+ "완료" 전 4-field: ① 변경 파일 ② Evidence(`verify_a4p.mjs` exit code + VERDICT 줄 + `fontSource`) ③ 검증 layer ④ Unknown.
402
+ `fontSource` 가 `estimate` 면 오버플로에 대해 "통과" 라고 적지 않는다. "미측정" 이 정직한 값이다.
403
+ 픽셀 렌더 검증은 **Unknown** 이다(soffice 부재). 구조·좌표·메트릭 검증으로 대체하고 그렇게 표기한다.
404
+
405
+ ## 참고
406
+
407
+ - `build_a4p.js --help` · `build_a4p.py --help` · `verify_a4p.mjs --help` 가 각 파일의 전체 플래그를 낸다.
408
+ - `verify_a4p.mjs --json` 이 기계 판독용 리포트를 낸다. finding 마다 `code`·`slide`·`shape`·`severity`·`status`·`detail` 이 붙는다.
409
+ - 한글 3슬롯 주입 패턴의 다른 사례: `skills/arch-diagram/build_pptx_template.py`