@kaydash9999/banker-plugins 0.26.1 → 0.28.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +25 -10
- package/bin/lib/codex-hooks.mjs +7 -1
- package/codex/command-skills/front-qa/SKILL.md +3 -1
- package/codex/hooks.json +15 -5
- package/codex/manifest.json +4 -0
- package/codex/skills/ralph/SKILL.md +272 -0
- package/codex/skills/ralph/scripts/gate-evidence.mjs +457 -0
- package/codex/skills/ralph/scripts/ralph-state.mjs +622 -0
- package/codex/skills/ralph/scripts/run-folder.mjs +299 -0
- package/codex/transform-matrix.md +5 -4
- package/commands/front-qa.md +3 -1
- package/hooks/ralph-continue-codex.mjs +59 -0
- package/package.json +1 -1
- package/skills/all-in-one/SKILL.md +22 -8
- package/skills/ralph-qa/references/gate-evidence.mjs +3 -3
- package/skills/ralph-qa/references/run-folder.mjs +3 -3
- package/skills/ultra-chain/SKILL.md +11 -4
- package/skills/ultra-init/SKILL.md +9 -7
- package/skills/ultraqa/references/gate-evidence.mjs +3 -3
- package/skills/ultraqa/references/run-folder.mjs +3 -3
- package/skills/video-gen/SKILL.md +141 -0
- package/skills/video-gen/assets/example.json +36 -0
- package/skills/video-gen/scripts/vg-catalog.mjs +23 -0
- package/skills/video-gen/scripts/vg-deck.mjs +72 -0
- package/skills/video-gen/scripts/vg-draw.mjs +231 -0
- package/skills/video-gen/scripts/vg-engine.mjs +74 -0
- package/skills/video-gen/scripts/vg-fx.mjs +106 -0
- package/skills/video-gen/scripts/vg-model.mjs +236 -0
- package/skills/video-gen/scripts/vg-render.mjs +151 -0
- package/skills/video-gen/scripts/vg-scenes.mjs +226 -0
- package/skills/video-gen/scripts/video-gen.mjs +212 -0
- package/skills/video-gen/vendor/README.md +25 -0
- package/skills/video-gen/vendor/aimotion/LICENSE.md +21 -0
- package/skills/video-gen/vendor/canvaskit/LICENSE.md +29 -0
- package/skills/video-gen/vendor/canvaskit/THIRD-PARTY-NOTICES.md +1366 -0
- package/skills/video-gen/vendor/canvaskit/canvaskit.js +270 -0
- package/skills/video-gen/vendor/canvaskit/canvaskit.wasm +0 -0
- package/skills/video-gen/vendor/fonts/NotoSans-Bold.ttf +0 -0
- package/skills/video-gen/vendor/fonts/NotoSans-Regular.ttf +0 -0
- package/skills/video-gen/vendor/fonts/NotoSansKR-Bold.otf +0 -0
- package/skills/video-gen/vendor/fonts/NotoSansKR-Regular.otf +0 -0
- package/skills/video-gen/vendor/fonts/OFL-noto-sans-kr.md +94 -0
- package/skills/video-gen/vendor/fonts/OFL-noto-sans.md +93 -0
- package/skills/video-gen/vendor/h264-mp4-encoder/LICENSE.md +21 -0
- package/skills/video-gen/vendor/h264-mp4-encoder/MPL-1.1.md +469 -0
- package/skills/video-gen/vendor/h264-mp4-encoder/h264-mp4-encoder.node.js +2 -0
- package/skills/visual-ralph/SKILL.md +9 -4
- package/skills/wbs-gen/SKILL.md +127 -0
- package/skills/wbs-gen/assets/example.json +58 -0
- package/skills/wbs-gen/scripts/wbs-gen.mjs +238 -0
- package/skills/wbs-gen/scripts/wg-dates.mjs +106 -0
- package/skills/wbs-gen/scripts/wg-holidays.mjs +89 -0
- package/skills/wbs-gen/scripts/wg-html.mjs +108 -0
- package/skills/wbs-gen/scripts/wg-plan.mjs +230 -0
- package/skills/wbs-gen/scripts/wg-xlsx.mjs +328 -0
- package/skills/wbs-gen/scripts/wg-zip.mjs +83 -0
- package/skills/wide-pptx/SKILL.md +121 -0
- package/skills/wide-pptx/assets/example.json +63 -0
- package/skills/wide-pptx/scripts/wide-pptx.mjs +185 -0
- package/skills/wide-pptx/scripts/wp-html.mjs +98 -0
- package/skills/wide-pptx/scripts/wp-model.mjs +236 -0
- package/skills/wide-pptx/scripts/wp-pptx.mjs +219 -0
- package/skills/wide-pptx/scripts/wp-svg.mjs +114 -0
- package/skills/wide-pptx/vendor/LICENSE-jszip.md +651 -0
- package/skills/wide-pptx/vendor/LICENSE-pako.md +21 -0
- package/skills/wide-pptx/vendor/LICENSE-pptxgenjs.md +21 -0
- package/skills/wide-pptx/vendor/README.md +12 -0
- package/skills/wide-pptx/vendor/THIRD-PARTY-NOTICES.md +142 -0
- package/skills/wide-pptx/vendor/pptxgen.bundle.js +96 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// wg-xlsx.mjs: draws the wbs-gen model as one XLSX sheet. Bars are plain cell fills (no conditional formatting), so
|
|
2
|
+
// the file looks the same in any viewer; the parts follow the layout Excel itself writes (shared strings, frozen
|
|
3
|
+
// panes, print titles, A3 landscape fit to one page wide).
|
|
4
|
+
import { zip } from "./wg-zip.mjs";
|
|
5
|
+
|
|
6
|
+
const NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
7
|
+
const NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
8
|
+
// Hangul is set in Noto Sans KR and everything else in Noto Sans. A cell that mixes the two holds rich-text runs,
|
|
9
|
+
// one font per run; spaces and punctuation stay in the run they follow. Noto Sans has no geometric shapes, so ◆
|
|
10
|
+
// goes with Noto Sans KR, which has them; so do Hanja, CJK punctuation and full-width forms (Noto Sans has none).
|
|
11
|
+
export const KR = "Noto Sans KR";
|
|
12
|
+
export const LATIN = "Noto Sans";
|
|
13
|
+
const HANGUL = /[\u1100-\u11FF\u2190-\u21FF\u2460-\u24FF\u25A0-\u27BF\u2E80-\u9FFF\uA960-\uA97F\uAC00-\uD7A3\uD7B0-\uD7FF\uF900-\uFAFF\uFF00-\uFFEF※]/;
|
|
14
|
+
const NEUTRAL = /[\s.,·:;()[\]{}\-/|'"!?~]/;
|
|
15
|
+
const GRID = "D5D8DC";
|
|
16
|
+
const GATE = "C0392B";
|
|
17
|
+
const FONTS = {
|
|
18
|
+
base: { sz: 11, name: LATIN },
|
|
19
|
+
title: { b: 1, sz: 13, color: "FFFFFF" },
|
|
20
|
+
head: { b: 1, sz: 10, color: "FFFFFF" },
|
|
21
|
+
day: { b: 1, sz: 8, color: "2C3E50" },
|
|
22
|
+
body: { sz: 8, color: "2C3E50" },
|
|
23
|
+
bodyB: { b: 1, sz: 8, color: "1B2631" },
|
|
24
|
+
mile: { b: 1, sz: 9, color: GATE },
|
|
25
|
+
note: { sz: 9, color: "2C3E50" },
|
|
26
|
+
noteB: { b: 1, sz: 10, color: "1B2631" },
|
|
27
|
+
};
|
|
28
|
+
const WIDTHS = [6, 36, 10.5, 9, 28, 7.5, 7.5, 8.2];
|
|
29
|
+
const DAY_WIDTH = 4.8;
|
|
30
|
+
const ALIGN = ["center", "left", "center", "center", "left", "center", "center", "center"];
|
|
31
|
+
|
|
32
|
+
// Control characters other than tab and new line are not allowed in XML 1.0.
|
|
33
|
+
const clean = (s) => String(s).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g, "");
|
|
34
|
+
export const esc = (s) => clean(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
35
|
+
|
|
36
|
+
export function colName(i) {
|
|
37
|
+
let s = "";
|
|
38
|
+
for (let n = i + 1; n > 0; n = Math.floor((n - 1) / 26)) s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Splits text into runs of one font: [{ text, kr }]. Text with no letters is one Noto Sans run. */
|
|
43
|
+
export function scriptRuns(text) {
|
|
44
|
+
const runs = [];
|
|
45
|
+
let lead = "";
|
|
46
|
+
for (const ch of String(text)) {
|
|
47
|
+
const last = runs[runs.length - 1];
|
|
48
|
+
if (NEUTRAL.test(ch)) {
|
|
49
|
+
if (last) last.text += ch;
|
|
50
|
+
else lead += ch;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const kr = HANGUL.test(ch);
|
|
54
|
+
if (last && last.kr === kr) last.text += ch;
|
|
55
|
+
else runs.push({ text: (last ? "" : lead) + ch, kr });
|
|
56
|
+
}
|
|
57
|
+
return runs.length ? runs : [{ text: lead, kr: false }];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Sheet names may not hold []:*?/\, start or end with ', be History, or pass 31 characters. */
|
|
61
|
+
export function sheetName(s) {
|
|
62
|
+
const trim = (x) => x.trim().replace(/^'+|'+$/g, "").trim();
|
|
63
|
+
let t = trim(clean(s).replace(/[[\]:*?/\\]/g, " "));
|
|
64
|
+
// 31 UTF-16 units, without cutting a surrogate pair; trim again in case the cut leaves a ' at the end.
|
|
65
|
+
while (t.length > 31) t = Array.from(t).slice(0, -1).join("");
|
|
66
|
+
t = trim(t);
|
|
67
|
+
return !t || /^history$/i.test(t) ? "WBS" : t;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Collects the distinct fonts, fills, borders and cell formats a sheet uses. */
|
|
71
|
+
class Styles {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.fonts = [FONTS.base];
|
|
74
|
+
this.fills = ["none", "gray125"];
|
|
75
|
+
this.borders = [{}];
|
|
76
|
+
this.xfs = [{ font: 0, fill: 0, border: 0, h: "", wrap: false }];
|
|
77
|
+
this.seen = new Map();
|
|
78
|
+
}
|
|
79
|
+
pick(list, item) {
|
|
80
|
+
const k = JSON.stringify(item);
|
|
81
|
+
let i = list.findIndex((x) => JSON.stringify(x) === k);
|
|
82
|
+
if (i < 0) i = list.push(item) - 1;
|
|
83
|
+
return i;
|
|
84
|
+
}
|
|
85
|
+
/** spec: { font, fill, border: { l, r, t, b }, h, wrap }; kr picks Noto Sans KR for the cell font. */
|
|
86
|
+
id(spec, kr) {
|
|
87
|
+
const key = JSON.stringify([spec, kr]);
|
|
88
|
+
if (this.seen.has(key)) return this.seen.get(key);
|
|
89
|
+
const xf = {
|
|
90
|
+
font: this.pick(this.fonts, fontOf(spec, kr)),
|
|
91
|
+
fill: spec.fill ? this.pick(this.fills, spec.fill) : 0,
|
|
92
|
+
border: this.pick(this.borders, spec.border ?? {}),
|
|
93
|
+
h: spec.h ?? "",
|
|
94
|
+
wrap: !!spec.wrap,
|
|
95
|
+
};
|
|
96
|
+
const i = this.pick(this.xfs, xf);
|
|
97
|
+
this.seen.set(key, i);
|
|
98
|
+
return i;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const fontOf = (spec, kr) => ({ ...(FONTS[spec.font] ?? FONTS.base), name: kr ? KR : LATIN });
|
|
103
|
+
|
|
104
|
+
const BORDER = { thin: ["thin", GRID], gate: ["medium", GATE], dotted: ["dotted", "7F8C8D"] };
|
|
105
|
+
|
|
106
|
+
// The inside of <font> (cell fonts, name) and of <rPr> (rich-text runs, rFont): same children, other name tag.
|
|
107
|
+
function fontBody(f, nameTag) {
|
|
108
|
+
const b = f.b ? "<b/>" : "";
|
|
109
|
+
const color = `<color rgb="FF${f.color ?? "000000"}"/>`;
|
|
110
|
+
const charset = f.name === KR ? `<charset val="129"/>` : "";
|
|
111
|
+
return `${b}<sz val="${f.sz}"/>${color}<${nameTag} val="${f.name}"/><family val="2"/>${charset}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fontXml = (f) => `<font>${fontBody(f, "name")}</font>`;
|
|
115
|
+
|
|
116
|
+
function fillXml(f) {
|
|
117
|
+
if (f === "none" || f === "gray125") return `<fill><patternFill patternType="${f}"/></fill>`;
|
|
118
|
+
return `<fill><patternFill patternType="solid"><fgColor rgb="FF${f}"/><bgColor indexed="64"/></patternFill></fill>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function borderXml(b) {
|
|
122
|
+
const side = (n) => {
|
|
123
|
+
const v = BORDER[b[n[0]]];
|
|
124
|
+
return v ? `<${n} style="${v[0]}"><color rgb="FF${v[1]}"/></${n}>` : `<${n}/>`;
|
|
125
|
+
};
|
|
126
|
+
return `<border>${["left", "right", "top", "bottom"].map(side).join("")}<diagonal/></border>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function xfXml(x) {
|
|
130
|
+
const flags = `${x.fill ? ' applyFill="1"' : ""}${x.border ? ' applyBorder="1"' : ""}`;
|
|
131
|
+
const align = x.h || x.wrap
|
|
132
|
+
? `<alignment${x.h ? ` horizontal="${x.h}"` : ""} vertical="center"${x.wrap ? ' wrapText="1"' : ""}/>`
|
|
133
|
+
: "";
|
|
134
|
+
const open = `<xf numFmtId="0" fontId="${x.font}" fillId="${x.fill}" borderId="${x.border}" xfId="0" applyFont="1"${flags}`;
|
|
135
|
+
return align ? `${open} applyAlignment="1">${align}</xf>` : `${open}/>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stylesXml(st) {
|
|
139
|
+
const list = (tag, items, fn) => `<${tag} count="${items.length}">${items.map(fn).join("")}</${tag}>`;
|
|
140
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<styleSheet xmlns="${NS}">`
|
|
141
|
+
+ list("fonts", st.fonts, fontXml) + list("fills", st.fills, fillXml) + list("borders", st.borders, borderXml)
|
|
142
|
+
+ `<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>`
|
|
143
|
+
+ list("cellXfs", st.xfs, xfXml)
|
|
144
|
+
+ `<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>`
|
|
145
|
+
+ `<dxfs count="0"/><tableStyles count="0" defaultTableStyle="TableStyleMedium2" defaultPivotStyle="PivotStyleLight16"/></styleSheet>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The sheet being drawn: cells by row, merged ranges, row heights, shared strings. */
|
|
149
|
+
class Sheet {
|
|
150
|
+
constructor() {
|
|
151
|
+
this.rows = new Map();
|
|
152
|
+
this.merges = [];
|
|
153
|
+
this.heights = new Map();
|
|
154
|
+
this.strings = [];
|
|
155
|
+
this.refs = 0;
|
|
156
|
+
this.stringIndex = new Map();
|
|
157
|
+
this.styles = new Styles();
|
|
158
|
+
}
|
|
159
|
+
put(r, c, value, spec) {
|
|
160
|
+
if (!this.rows.has(r)) this.rows.set(r, new Map());
|
|
161
|
+
const runs = typeof value === "string" && value ? scriptRuns(clean(value)) : [{ text: "", kr: false }];
|
|
162
|
+
const kr = runs.some((x) => x.kr);
|
|
163
|
+
const mixed = runs.length > 1 ? runs.map((x) => ({ ...x, font: fontOf(spec, x.kr) })) : null;
|
|
164
|
+
this.rows.get(r).set(c, { value, s: this.styles.id(spec, kr), runs: mixed });
|
|
165
|
+
}
|
|
166
|
+
merge(r1, c1, r2, c2) {
|
|
167
|
+
this.merges.push(`${colName(c1)}${r1}:${colName(c2)}${r2}`);
|
|
168
|
+
}
|
|
169
|
+
/** A shared string; mixed runs carry their fonts, so the same text in another cell font is another entry. */
|
|
170
|
+
sst(text, runs) {
|
|
171
|
+
const t = clean(text);
|
|
172
|
+
const key = runs ? `r:${JSON.stringify(runs)}` : `p:${t}`;
|
|
173
|
+
this.refs++;
|
|
174
|
+
if (!this.stringIndex.has(key)) this.stringIndex.set(key, this.strings.push({ text: t, runs }) - 1);
|
|
175
|
+
return this.stringIndex.get(key);
|
|
176
|
+
}
|
|
177
|
+
cellXml(r, c, cell) {
|
|
178
|
+
const ref = `${colName(c)}${r}`;
|
|
179
|
+
if (typeof cell.value === "number") return `<c r="${ref}" s="${cell.s}"><v>${cell.value}</v></c>`;
|
|
180
|
+
if (cell.value === "" || cell.value === null || cell.value === undefined) return `<c r="${ref}" s="${cell.s}"/>`;
|
|
181
|
+
return `<c r="${ref}" s="${cell.s}" t="s"><v>${this.sst(cell.value, cell.runs)}</v></c>`;
|
|
182
|
+
}
|
|
183
|
+
dataXml() {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const r of [...this.rows.keys()].sort((a, b) => a - b)) {
|
|
186
|
+
const cells = this.rows.get(r);
|
|
187
|
+
const ht = this.heights.get(r);
|
|
188
|
+
const attr = ht ? ` ht="${ht}" customHeight="1"` : "";
|
|
189
|
+
const xml = [...cells.keys()].sort((a, b) => a - b).map((c) => this.cellXml(r, c, cells.get(c))).join("");
|
|
190
|
+
out.push(`<row r="${r}"${attr}>${xml}</row>`);
|
|
191
|
+
}
|
|
192
|
+
return out.join("");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const grid = (gate) => ({ l: gate ? "gate" : "thin", r: "thin", t: "thin", b: "thin" });
|
|
197
|
+
|
|
198
|
+
function drawHead(sh, m, last) {
|
|
199
|
+
sh.put(1, 0, m.title, { font: "title", fill: "1F3864", h: "left" });
|
|
200
|
+
for (let c = 1; c <= last; c++) sh.put(1, c, "", { font: "title", fill: "1F3864" });
|
|
201
|
+
sh.merge(1, 0, 1, last);
|
|
202
|
+
sh.heights.set(1, 28).set(2, 16).set(3, 30);
|
|
203
|
+
m.head.forEach((h, c) => {
|
|
204
|
+
for (const r of [2, 3]) sh.put(r, c, r === 2 ? h : "", { font: "head", fill: "34495E", border: grid(false), h: "center", wrap: true });
|
|
205
|
+
sh.merge(2, c, 3, c);
|
|
206
|
+
});
|
|
207
|
+
const gateCols = new Set(m.gates.map((g) => g.col));
|
|
208
|
+
for (const w of m.weeks) {
|
|
209
|
+
for (let i = w.first; i < w.first + w.count; i++) {
|
|
210
|
+
sh.put(2, 8 + i, i === w.first ? w.label : "", { font: "head", fill: "5D6D7E", border: grid(gateCols.has(i)), h: "center" });
|
|
211
|
+
}
|
|
212
|
+
if (w.count > 1) sh.merge(2, 8 + w.first, 2, 8 + w.first + w.count - 1);
|
|
213
|
+
}
|
|
214
|
+
m.days.forEach((d, i) => {
|
|
215
|
+
sh.put(3, 8 + i, `${d.label}\n${d.dow}`, { font: "day", fill: "EBEDEF", border: grid(gateCols.has(i)), h: "center", wrap: true });
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function dayCell(row, i, gate) {
|
|
220
|
+
const border = grid(gate);
|
|
221
|
+
if (row.diamonds.includes(i)) {
|
|
222
|
+
const b = row.dotted ? { ...border, l: gate ? "gate" : "dotted", r: "dotted", b: "dotted" } : border;
|
|
223
|
+
return ["◆", { font: "mile", fill: row.fill, border: b, h: "center" }];
|
|
224
|
+
}
|
|
225
|
+
const fill = i >= row.from && i <= row.to ? row.bar : row.fill;
|
|
226
|
+
return ["", { font: "body", fill, border }];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function drawRows(sh, m) {
|
|
230
|
+
const gateCols = new Set(m.gates.map((g) => g.col));
|
|
231
|
+
m.rows.forEach((row, n) => {
|
|
232
|
+
const r = 4 + n;
|
|
233
|
+
const font = row.kind === "group" ? "bodyB" : "body";
|
|
234
|
+
row.cells.forEach((v, c) => {
|
|
235
|
+
sh.put(r, c, v, { font, fill: row.fill, border: grid(false), h: ALIGN[c], wrap: c === 1 || c === 4 });
|
|
236
|
+
});
|
|
237
|
+
m.days.forEach((_, i) => {
|
|
238
|
+
const [v, spec] = dayCell(row, i, gateCols.has(i));
|
|
239
|
+
sh.put(r, 8 + i, v, spec);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
return 4 + m.rows.length;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function legendCell(item) {
|
|
246
|
+
if (item.type === "diamond") return ["◆", { font: "mile", h: "center" }];
|
|
247
|
+
if (item.type === "gate") return ["", { font: "note", border: { l: "gate", r: "gate" } }];
|
|
248
|
+
const b = item.type === "dotted" ? { l: "dotted", r: "dotted", t: "dotted", b: "dotted" } : grid(false);
|
|
249
|
+
return ["", { font: "note", fill: item.color, border: b }];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function drawNotes(sh, m, start) {
|
|
253
|
+
let r = start + 1;
|
|
254
|
+
if (m.legend.length) {
|
|
255
|
+
sh.put(r++, 0, "[범례]", { font: "noteB" });
|
|
256
|
+
for (const item of m.legend) {
|
|
257
|
+
const [v, spec] = legendCell(item);
|
|
258
|
+
sh.put(r, 0, v, spec);
|
|
259
|
+
sh.put(r++, 1, item.label, { font: "note" });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const n of m.notes) {
|
|
263
|
+
r++;
|
|
264
|
+
if (n.heading) sh.put(r++, 0, n.heading, { font: "noteB" });
|
|
265
|
+
for (const line of n.lines) sh.put(r++, 0, line, { font: "note" });
|
|
266
|
+
}
|
|
267
|
+
return r - 1;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function sheetXml(sh, last, lastRow) {
|
|
271
|
+
const cols = WIDTHS.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join("")
|
|
272
|
+
+ `<col min="9" max="${last + 1}" width="${DAY_WIDTH}" customWidth="1"/>`;
|
|
273
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<worksheet xmlns="${NS}" xmlns:r="${NSR}">`
|
|
274
|
+
+ `<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr><dimension ref="A1:${colName(last)}${lastRow}"/>`
|
|
275
|
+
+ `<sheetViews><sheetView tabSelected="1" workbookViewId="0"><pane xSplit="8" ySplit="3" topLeftCell="I4" activePane="bottomRight" state="frozen"/>`
|
|
276
|
+
+ `<selection pane="topRight"/><selection pane="bottomLeft"/><selection pane="bottomRight" activeCell="I4" sqref="I4"/></sheetView></sheetViews>`
|
|
277
|
+
+ `<sheetFormatPr defaultRowHeight="16.5"/><cols>${cols}</cols><sheetData>${sh.dataXml()}</sheetData>`
|
|
278
|
+
+ `<mergeCells count="${sh.merges.length}">${sh.merges.map((x) => `<mergeCell ref="${x}"/>`).join("")}</mergeCells>`
|
|
279
|
+
+ `<printOptions horizontalCentered="1"/><pageMargins left="0.4" right="0.4" top="0.5" bottom="0.5" header="0.3" footer="0.3"/>`
|
|
280
|
+
+ `<pageSetup paperSize="8" orientation="landscape" fitToWidth="1" fitToHeight="0"/></worksheet>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function sstXml(strings, refs) {
|
|
284
|
+
const run = (x) => `<r><rPr>${fontBody(x.font, "rFont")}</rPr><t xml:space="preserve">${esc(x.text)}</t></r>`;
|
|
285
|
+
const si = strings.map((s) => (s.runs ? `<si>${s.runs.map(run).join("")}</si>` : `<si><t xml:space="preserve">${esc(s.text)}</t></si>`)).join("");
|
|
286
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<sst xmlns="${NS}" count="${refs}" uniqueCount="${strings.length}">${si}</sst>`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function packageParts(name) {
|
|
290
|
+
const quoted = `'${name.replace(/'/g, "''")}'`;
|
|
291
|
+
return {
|
|
292
|
+
types: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`
|
|
293
|
+
+ `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>`
|
|
294
|
+
+ `<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>`
|
|
295
|
+
+ `<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
296
|
+
+ `<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>`
|
|
297
|
+
+ `<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/></Types>`,
|
|
298
|
+
rels: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
|
|
299
|
+
+ `<Relationship Id="rId1" Type="${NSR}/officeDocument" Target="xl/workbook.xml"/></Relationships>`,
|
|
300
|
+
workbook: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<workbook xmlns="${NS}" xmlns:r="${NSR}">`
|
|
301
|
+
+ `<bookViews><workbookView/></bookViews><sheets><sheet name="${esc(name)}" sheetId="1" r:id="rId1"/></sheets>`
|
|
302
|
+
+ `<definedNames><definedName name="_xlnm.Print_Titles" localSheetId="0">${esc(quoted)}!$1:$3</definedName></definedNames></workbook>`,
|
|
303
|
+
wbRels: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
|
|
304
|
+
+ `<Relationship Id="rId1" Type="${NSR}/worksheet" Target="worksheets/sheet1.xml"/>`
|
|
305
|
+
+ `<Relationship Id="rId2" Type="${NSR}/styles" Target="styles.xml"/>`
|
|
306
|
+
+ `<Relationship Id="rId3" Type="${NSR}/sharedStrings" Target="sharedStrings.xml"/></Relationships>`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Returns the XLSX bytes for a layout model. */
|
|
311
|
+
export function toXlsx(m) {
|
|
312
|
+
const sh = new Sheet();
|
|
313
|
+
const last = 7 + m.days.length;
|
|
314
|
+
drawHead(sh, m, last);
|
|
315
|
+
const lastRow = drawNotes(sh, m, drawRows(sh, m));
|
|
316
|
+
const sheet = sheetXml(sh, last, lastRow);
|
|
317
|
+
const name = sheetName(m.sheet);
|
|
318
|
+
const p = packageParts(name);
|
|
319
|
+
return zip([
|
|
320
|
+
{ name: "[Content_Types].xml", data: p.types },
|
|
321
|
+
{ name: "_rels/.rels", data: p.rels },
|
|
322
|
+
{ name: "xl/workbook.xml", data: p.workbook },
|
|
323
|
+
{ name: "xl/_rels/workbook.xml.rels", data: p.wbRels },
|
|
324
|
+
{ name: "xl/worksheets/sheet1.xml", data: sheet },
|
|
325
|
+
{ name: "xl/styles.xml", data: stylesXml(sh.styles) },
|
|
326
|
+
{ name: "xl/sharedStrings.xml", data: sstXml(sh.strings, sh.refs) },
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// wg-zip.mjs: a minimal ZIP writer (deflate, no ZIP64) for the XLSX package. Pure Node: zlib is built in, and the
|
|
2
|
+
// CRC-32 is computed here because zlib.crc32 only exists from Node 20.15 / 22.2.
|
|
3
|
+
import { deflateRawSync } from "node:zlib";
|
|
4
|
+
|
|
5
|
+
const TABLE = (() => {
|
|
6
|
+
const t = new Uint32Array(256);
|
|
7
|
+
for (let n = 0; n < 256; n++) {
|
|
8
|
+
let c = n;
|
|
9
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
10
|
+
t[n] = c >>> 0;
|
|
11
|
+
}
|
|
12
|
+
return t;
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
export function crc32(buf) {
|
|
16
|
+
let c = 0xffffffff;
|
|
17
|
+
for (let i = 0; i < buf.length; i++) c = TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
18
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 1980-01-01 00:00 in DOS time: a fixed stamp so the same plan gives the same bytes.
|
|
22
|
+
const DOS_TIME = 0;
|
|
23
|
+
const DOS_DATE = (0 << 9) | (1 << 5) | 1;
|
|
24
|
+
|
|
25
|
+
function header(sig, size) {
|
|
26
|
+
const b = Buffer.alloc(size);
|
|
27
|
+
b.writeUInt32LE(sig, 0);
|
|
28
|
+
return b;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function localHeader(name, crc, comp, raw) {
|
|
32
|
+
const h = header(0x04034b50, 30);
|
|
33
|
+
h.writeUInt16LE(20, 4); // version needed
|
|
34
|
+
h.writeUInt16LE(0x0800, 6); // UTF-8 names
|
|
35
|
+
h.writeUInt16LE(8, 8); // deflate
|
|
36
|
+
h.writeUInt16LE(DOS_TIME, 10);
|
|
37
|
+
h.writeUInt16LE(DOS_DATE, 12);
|
|
38
|
+
h.writeUInt32LE(crc, 14);
|
|
39
|
+
h.writeUInt32LE(comp, 18);
|
|
40
|
+
h.writeUInt32LE(raw, 22);
|
|
41
|
+
h.writeUInt16LE(name.length, 26);
|
|
42
|
+
return h;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function centralHeader(name, crc, comp, raw, offset) {
|
|
46
|
+
const h = header(0x02014b50, 46);
|
|
47
|
+
h.writeUInt16LE(20, 4); // version made by
|
|
48
|
+
h.writeUInt16LE(20, 6);
|
|
49
|
+
h.writeUInt16LE(0x0800, 8);
|
|
50
|
+
h.writeUInt16LE(8, 10);
|
|
51
|
+
h.writeUInt16LE(DOS_TIME, 12);
|
|
52
|
+
h.writeUInt16LE(DOS_DATE, 14);
|
|
53
|
+
h.writeUInt32LE(crc, 16);
|
|
54
|
+
h.writeUInt32LE(comp, 20);
|
|
55
|
+
h.writeUInt32LE(raw, 24);
|
|
56
|
+
h.writeUInt16LE(name.length, 28);
|
|
57
|
+
h.writeUInt32LE(offset, 42);
|
|
58
|
+
return h;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** entries: [{ name, data: Buffer|string }] in order. Returns the ZIP as one Buffer. */
|
|
62
|
+
export function zip(entries) {
|
|
63
|
+
const parts = [];
|
|
64
|
+
const central = [];
|
|
65
|
+
let offset = 0;
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
const name = Buffer.from(e.name, "utf8");
|
|
68
|
+
const raw = Buffer.isBuffer(e.data) ? e.data : Buffer.from(e.data, "utf8");
|
|
69
|
+
const comp = deflateRawSync(raw, { level: 9 });
|
|
70
|
+
const crc = crc32(raw);
|
|
71
|
+
const local = localHeader(name, crc, comp.length, raw.length);
|
|
72
|
+
parts.push(local, name, comp);
|
|
73
|
+
central.push(centralHeader(name, crc, comp.length, raw.length, offset), name);
|
|
74
|
+
offset += local.length + name.length + comp.length;
|
|
75
|
+
}
|
|
76
|
+
const dir = Buffer.concat(central);
|
|
77
|
+
const end = header(0x06054b50, 22);
|
|
78
|
+
end.writeUInt16LE(entries.length, 8);
|
|
79
|
+
end.writeUInt16LE(entries.length, 10);
|
|
80
|
+
end.writeUInt32LE(dir.length, 12);
|
|
81
|
+
end.writeUInt32LE(offset, 16);
|
|
82
|
+
return Buffer.concat([...parts, dir, end]);
|
|
83
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wide-pptx
|
|
3
|
+
description: "발표용 16:9 슬라이드를 PPTX 와 단일 HTML 덱으로 만듦. 한 JSON 덱에서 두 형식을 같은 배치로 냄: 표지, 구간, 글머리, 2단, 표, 차트(막대, 꺾은선, 원), 이미지, 인용, 마무리와 발표자 노트. 글꼴은 한글 Noto Sans KR, 그 밖 Noto Sans. PptxGenJS 를 스킬 안에 넣어 설치와 내려받기 없이 오프라인에서 동작. A4 세로 인쇄 문서는 vertical-pptx. 'wide-pptx'/'발표 자료 만들어'/'프레젠테이션'/'슬라이드 만들어'/'html 발표 자료' 시 사용."
|
|
4
|
+
argument-hint: "[--pptx] [--html] [--dir <폴더>] [--deck <deck.json>] [--minutes <발표 분>]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# wide-pptx: 발표용 16:9 슬라이드(PPTX, HTML)
|
|
8
|
+
|
|
9
|
+
발표 내용을 받아 16:9 슬라이드를 PPTX 와 HTML 덱으로 만든다. 답변은 한글.
|
|
10
|
+
두 형식은 한 JSON 덱에서 같은 배치(13.333 x 7.5 in, HTML 은 1280 x 720 px)로 나온다.
|
|
11
|
+
|
|
12
|
+
## 원칙
|
|
13
|
+
|
|
14
|
+
- **설치와 내려받기가 없다.** PPTX 는 스킬 안의 `vendor/pptxgen.bundle.js`(PptxGenJS 4.0.1, MIT)로, HTML 은 도우미가 직접 쓴다. 인터넷을 쓰지 않는다.
|
|
15
|
+
- **글꼴은 한글 Noto Sans KR, 그 밖(영문, 숫자, 기호) Noto Sans 다.** PPTX 는 테마의 라틴 글꼴과 한글 글꼴로 나누어 적고, 글 상자는 테마 글꼴을 따른다. HTML 은 `"Noto Sans","Noto Sans KR"` 순서다. 글꼴 파일은 넣지 않으므로 보는 PC 에 두 글꼴이 있어야 그대로 보인다.
|
|
16
|
+
- **슬라이드는 종류별 고정 배치다.** 글이 상자보다 길면 넘친다. 도우미가 넘칠 만한 곳을 경고하면 슬라이드를 나눈다. 글자를 줄여 맞추지 않는다.
|
|
17
|
+
- **이미지는 로컬 파일만 쓴다.** PNG, JPEG, GIF. 두 형식에 같은 바이트가 들어간다. URL 은 받지 않는다.
|
|
18
|
+
- 있는 파일을 덮어쓰지 않는다. 같은 이름이 있으면 묻고, 허락을 받은 뒤에만 `--force` 를 준다.
|
|
19
|
+
- 사용자가 주지 않은 수치, 인용, 출처를 지어내지 않는다. 비어 있는 곳은 `[확인 필요]` 로 두고 보고에 적는다.
|
|
20
|
+
|
|
21
|
+
## 런타임
|
|
22
|
+
|
|
23
|
+
| 항목 | Claude Code | Codex |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 스킬 디렉터리 `<S>` | 스킬 맨 위 "Base directory for this skill:" 경로 | 이 SKILL.md 의 폴더. npm 설치는 `~/.codex/skills/banker-wide-pptx/`, 플러그인 설치는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/wide-pptx/`. 절대경로로 씀 |
|
|
26
|
+
| 부르기 | `/banker:wide-pptx` | `$banker-wide-pptx`, 플러그인 `$banker:wide-pptx` |
|
|
27
|
+
| 확인 질문 | `AskUserQuestion` | `request_user_input`. 없으면 번호 붙인 평문 선택지 |
|
|
28
|
+
| 샌드박스 | 없음 | 도우미는 작업 폴더 안에서만 읽고 써서 승인이 필요 없음. 작업 폴더 밖 이미지는 `sandbox_permissions: "require_escalated"` 로 승인을 청하거나 작업 폴더로 복사해 씀 |
|
|
29
|
+
| 셸 | bash | Windows 는 PowerShell. 도우미는 Node 라 두 셸에서 같다 |
|
|
30
|
+
|
|
31
|
+
물을 수 없는 실행(`codex exec`, 다른 스킬의 연쇄)에서는 인터뷰를 건너뛰고 받은 자료만으로 만든다. 모자란 곳은 `[확인 필요]` 로 둔다.
|
|
32
|
+
|
|
33
|
+
## 인자
|
|
34
|
+
|
|
35
|
+
| 인자 | 뜻 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `--pptx`, `--html` | 낼 형식. 둘 다 없으면 둘 다 냄 |
|
|
38
|
+
| `--dir <폴더>` | 출력 폴더. 없으면 지금 작업 폴더. 도우미의 `--out <파일>` 과 다름 |
|
|
39
|
+
| `--deck <deck.json>` | 이미 있는 덱 파일을 씀. 없으면 대화와 자료로 새로 씀 |
|
|
40
|
+
| `--minutes <분>` | 발표 시간. 슬라이드 수를 정할 때 씀(1~2분에 한 장) |
|
|
41
|
+
|
|
42
|
+
## 도우미
|
|
43
|
+
|
|
44
|
+
`P="<S>/scripts/wide-pptx.mjs"`. 머리 주석이 전체 사용법이다.
|
|
45
|
+
|
|
46
|
+
| 명령 | 하는 일 | 종료 코드 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `node "$P" example` | 예시 덱(JSON)을 냄. 덱 파일의 틀로 씀 | 0 |
|
|
49
|
+
| `node "$P" check --deck <deck.json>` | 덱을 검사하고 `OK 슬라이드 N, 표 T, 차트 C, 이미지 I, 노트 K` 한 줄을 냄. 넘칠 만한 곳은 `경고:` 줄 | 0, 덱 오류 1 |
|
|
50
|
+
| `node "$P" build --deck <deck.json> --out <파일.pptx> --out <파일.html>` | 파일을 만듦. 쓰기 전에 모든 `--out` 을 검사하고, 하나라도 이미 있으면 `--force` 없이는 아무것도 쓰지 않고 2 로 끝남 | 0, 덱 오류 1, 사용법과 파일 오류 2 |
|
|
51
|
+
|
|
52
|
+
## 덱 파일(deck.json)
|
|
53
|
+
|
|
54
|
+
`node "$P" example` 이 낸 틀을 고쳐 쓴다. 이미지 경로는 덱 파일이 있는 폴더 기준이다.
|
|
55
|
+
모든 글 칸은 글(또는 숫자)만 받는다. PowerPoint 에서 복사한 줄 바꿈(U+000B)은 줄 바꿈으로, XML 에 넣을 수 없는 제어 문자는 지운다. 제목은 한 줄을 넘을 것 같으면 경고한다.
|
|
56
|
+
|
|
57
|
+
| 칸 | 뜻 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `title`, `author` | 파일 속성의 제목과 작성자 |
|
|
60
|
+
| `footer` | 표지와 마무리를 뺀 슬라이드 아래 왼쪽 글. 오른쪽에는 쪽 번호 |
|
|
61
|
+
| `theme` | `primary`(표지, 표 머리), `accent`(강조 막대, 소제목), `dark`(본문 글), `light`(구간, 인용 바탕), `muted`(보조 글). 6자리 16진수 |
|
|
62
|
+
| `slides` | 슬라이드 목록. 아래 표. 모두 `notes`(발표자 노트)를 받음 |
|
|
63
|
+
|
|
64
|
+
| type | 칸 | 넘침 경고와 한도 |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `title` | `title`, `subtitle`, `presenter`, `date` | |
|
|
67
|
+
| `section` | `title`, `subtitle` | |
|
|
68
|
+
| `bullets` | `title`, `bullets`(글, 또는 `{ "text", "level": 2 }`) | 글머리의 줄 수를 글자 폭(한글 1, 그 밖 약 0.55)으로 어림해 상자 높이를 넘으면 경고 |
|
|
69
|
+
| `two` | `title`, `left`, `right`(각 `{ "heading", "bullets" }`) | 칸마다 같은 어림 |
|
|
70
|
+
| `table` | `title`, `header`, `rows`, `caption` | 12행, 7열 |
|
|
71
|
+
| `chart` | `title`, `chart`(`kind`: bar, line, pie / `labels` / `series`: `[{ "name", "values" }]` / `unit`), `caption` | 항목 24개(pie 12개) 경고. 값은 숫자만(null 은 오류), 계열이 둘 이상이면 이름이 서로 달라야 함, pie 는 음수 없는 계열 하나이고 합이 0 이면 오류 |
|
|
72
|
+
| `image` | `title`, `image`(로컬 보통 파일 경로, Windows 드라이브 경로 포함), `caption` | 20MB 넘으면 오류. 캡션이 PPTX 그림의 대체 텍스트가 됨 |
|
|
73
|
+
| `quote` | `quote`, `by`, `title`(선택) | |
|
|
74
|
+
| `closing` | `title`, `subtitle` | |
|
|
75
|
+
|
|
76
|
+
## 절차
|
|
77
|
+
|
|
78
|
+
### 1. 자료 모으기
|
|
79
|
+
- 사용자가 준 원고, 문서, 표, 수치, 그림을 읽는다.
|
|
80
|
+
- 아래가 없으면 한 번에 묻는다(확인 질문 하나에 최대 4문항): 발표 목적과 청중, 발표 시간, 꼭 전할 메시지 한 줄, 넣을 자료(표, 수치, 그림 경로). 색과 배치는 묻지 않고 기본값을 쓴다.
|
|
81
|
+
|
|
82
|
+
### 2. 개요 확인
|
|
83
|
+
- 슬라이드 제목 목록(번호, 종류, 제목)을 먼저 보여 주고 확인을 받는다. 장수는 발표 시간 1~2분에 한 장을 기준으로 한다.
|
|
84
|
+
- 한 슬라이드에 메시지 하나. 표지, 구간, 마무리로 흐름을 나눈다.
|
|
85
|
+
|
|
86
|
+
### 3. 덱 파일 쓰기
|
|
87
|
+
- 출력 폴더에 `<이름>-deck.json` 으로 쓴다. 같은 이름이 있으면 묻는다.
|
|
88
|
+
- 발표에서 할 말은 화면 글이 아니라 `notes` 에 적는다. 화면 글머리는 짧게(한 줄 40자 안팎).
|
|
89
|
+
- `node "$P" check --deck <파일>` 이 0 이 될 때까지 고친다. `경고:` 가 나오면 그 슬라이드를 나누거나 줄인다. 그대로 두면 보고에 적는다.
|
|
90
|
+
|
|
91
|
+
### 4. 만들기
|
|
92
|
+
- `node "$P" build --deck <파일> --out <이름>.pptx --out <이름>.html` 을 돈다. 형식 인자를 받았으면 그 형식만 준다.
|
|
93
|
+
- 종료 코드 2 에 `--force` 안내가 나오면 덮어쓸지 묻고, 허락을 받은 뒤에만 `--force` 를 더해 다시 돈다.
|
|
94
|
+
|
|
95
|
+
### 5. 확인
|
|
96
|
+
- `build` 출력의 슬라이드, 표, 차트, 이미지, 노트 수를 덱과 대조한다.
|
|
97
|
+
- 브라우저를 쓸 수 있으면 HTML 을 열어 몇 장을 본다(`<파일>.html#3` 처럼 쪽을 고름).
|
|
98
|
+
- LibreOffice 가 있으면 PPTX 를 PDF 로 바꿔 본다. `soffice --convert-to pdf` 는 같은 이름 PDF 를 묻지 않고 덮어쓰므로 `--outdir` 을 새 임시 폴더로 준다.
|
|
99
|
+
- 쓸 수 없으면 보았다고 적지 않는다.
|
|
100
|
+
|
|
101
|
+
## HTML 덱 쓰는 법
|
|
102
|
+
|
|
103
|
+
- 오른쪽 화살표, 스페이스, PageDown 은 다음, 왼쪽 화살표, PageUp 은 이전, Home 과 End 는 처음과 끝이다. 화면 왼쪽 1/3 을 누르면 이전, 나머지는 다음이다.
|
|
104
|
+
- N 은 발표자 노트를 화면 아래에 보이거나 숨긴다.
|
|
105
|
+
- 브라우저 인쇄(PDF 저장)는 한 장에 슬라이드 하나, 13.333 x 7.5 in 이다. 배경 그래픽 인쇄를 켠다.
|
|
106
|
+
|
|
107
|
+
## 보고
|
|
108
|
+
|
|
109
|
+
- 만든 파일의 절대경로
|
|
110
|
+
- 슬라이드 수와 종류별 수, 노트가 있는 장 수
|
|
111
|
+
- 남은 경고와 `[확인 필요]` 자리
|
|
112
|
+
- 확인하지 못한 것(브라우저나 LibreOffice 로 보지 않음 등)
|
|
113
|
+
- 다음 행동이 있으면 마지막 줄에. 예: "고칠 곳은 `<이름>-deck.json` 을 고치고 다시 부르십시오."
|
|
114
|
+
|
|
115
|
+
## 함정
|
|
116
|
+
|
|
117
|
+
- 보는 PC 에 Noto Sans KR, Noto Sans 가 없으면 다른 글꼴로 보이고 줄바꿈이 달라진다. 발표 PC 에 두 글꼴이 있는지 먼저 본다.
|
|
118
|
+
- LibreOffice 는 한글과 영문 사이에 간격을 넣어 보일 수 있다(PowerPoint 와 다름).
|
|
119
|
+
- 휴대폰 사진(JPEG)의 회전 정보(EXIF)는 읽지 않는다. 옆으로 누워 보이면 돌려 저장한 그림을 쓴다.
|
|
120
|
+
- PPTX 의 차트는 PowerPoint 차트 개체라 PowerPoint 에서 값을 고칠 수 있다. HTML 차트는 그림(SVG)이라 덱 파일에서 고친다.
|
|
121
|
+
- 덱 파일을 고치면 두 형식을 함께 다시 만든다. 한쪽만 고치면 둘이 달라진다.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "샘플 대시보드 2차 개발 계획",
|
|
3
|
+
"author": "기획팀",
|
|
4
|
+
"footer": "샘플 대시보드 2차 개발 | 내부 검토용",
|
|
5
|
+
"theme": { "primary": "1F3864", "accent": "2E86C1" },
|
|
6
|
+
"slides": [
|
|
7
|
+
{
|
|
8
|
+
"type": "title",
|
|
9
|
+
"title": "샘플 대시보드 2차 개발 계획",
|
|
10
|
+
"subtitle": "요건 확정부터 운영 배포까지",
|
|
11
|
+
"presenter": "홍길동",
|
|
12
|
+
"date": "2026-07-22",
|
|
13
|
+
"notes": "인사와 발표 목적. 오늘 결정할 것은 요건 확정일과 선택 범위입니다."
|
|
14
|
+
},
|
|
15
|
+
{ "type": "section", "title": "1. 배경", "subtitle": "왜 2차 개발이 필요한가" },
|
|
16
|
+
{
|
|
17
|
+
"type": "bullets",
|
|
18
|
+
"title": "현황과 문제",
|
|
19
|
+
"bullets": [
|
|
20
|
+
"1차 대시보드는 은행 지표만 보여 줌",
|
|
21
|
+
{ "text": "계열사 지표는 따로 받아 엑셀로 합침", "level": 2 },
|
|
22
|
+
"화면 진입점이 셋으로 나뉘어 사용자가 헤맴",
|
|
23
|
+
"요건 담당자 변경으로 범위가 아직 확정되지 않음"
|
|
24
|
+
],
|
|
25
|
+
"notes": "문제 세 가지를 짧게. 범위 미확정이 가장 큰 위험이라는 점을 강조."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "two",
|
|
29
|
+
"title": "범위: 필수와 선택",
|
|
30
|
+
"left": { "heading": "필수", "bullets": ["요건과 UI 확정", "API 리팩터링", "대시보드 웹 개발"] },
|
|
31
|
+
"right": { "heading": "선택(마감 위험 시 먼저 제외)", "bullets": ["계열사 종합 수치", "계열사 웹 서비스"] }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "table",
|
|
35
|
+
"title": "일정 요약",
|
|
36
|
+
"header": ["단계", "기간", "담당", "비고"],
|
|
37
|
+
"rows": [
|
|
38
|
+
["요건·UI 확정", "07/27 ~ 08/03", "기획부", "게이트"],
|
|
39
|
+
["데이터 매핑", "08/04 ~ 08/07", "데이터팀", ""],
|
|
40
|
+
["개발", "08/04 ~ 08/14", "백엔드, 프론트", "선택 항목 포함"],
|
|
41
|
+
["통합 점검", "08/18 ~ 08/21", "전 파트", ""],
|
|
42
|
+
["운영 배포", "08/28", "전 파트", "오픈"]
|
|
43
|
+
],
|
|
44
|
+
"caption": "영업일 기준, 08/17 대체공휴일 제외"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "chart",
|
|
48
|
+
"title": "파트별 투입 영업일",
|
|
49
|
+
"chart": {
|
|
50
|
+
"kind": "bar",
|
|
51
|
+
"unit": "영업일",
|
|
52
|
+
"labels": ["기획", "데이터", "백엔드", "프론트", "AI"],
|
|
53
|
+
"series": [
|
|
54
|
+
{ "name": "필수", "values": [6, 4, 9, 9, 7] },
|
|
55
|
+
{ "name": "선택", "values": [0, 5, 0, 5, 0] }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"caption": "선택 항목은 08/07 까지 범위가 확정되지 않으면 뺍니다"
|
|
59
|
+
},
|
|
60
|
+
{ "type": "quote", "title": "핵심 원칙", "quote": "요건 확정 전에는 요건에 기대지 않는 일부터 한다", "by": "프로젝트 운영 원칙" },
|
|
61
|
+
{ "type": "closing", "title": "감사합니다", "subtitle": "질문과 결정 사항을 받겠습니다" }
|
|
62
|
+
]
|
|
63
|
+
}
|