diff --git a/.changeset/design-skills-intake.md b/.changeset/design-skills-intake.md new file mode 100644 index 0000000..8251434 --- /dev/null +++ b/.changeset/design-skills-intake.md @@ -0,0 +1,11 @@ +--- +"designpaca": minor +"@designpaca/core": minor +"@designpaca/skill": minor +--- + +0단계 인터뷰를 다시 필수 게이트로 되돌린다. v0.11.0 에서 "결과를 크게 바꿀 때만 묻는다"로 완화됐던 조건을 없애고, 스킬 본문 앞쪽에 "이 스킬은 사용자 인터뷰에 명시적으로 의존한다"는 게이트를 둔다. 필수 브리프 슬롯이 사용자 답이나 제공 자료로 확인되지 않으면 묻고, 질문 수단은 도구 목록으로 판정하며, 물었으면 턴을 끝낸다. 서브에이전트는 질문을 호출자에게 돌려주고, 예외는 사용자의 명시적 위임뿐이다. 하네스별 질문 도구(Claude Code·Codex·Gemini CLI·VS Code Copilot·Cursor)와 한도, 계획 도구·서브에이전트·브라우저 도구를 새 참조 문서 `harness.md` 에 정리했고, 슬롯·질문 카드·라운드는 `brief-interview.md` 로 옮겼다. + +외부 디자인 스킬 10종과 의존물의 장점을 판정 위계(하드 게이트·프로젝트 계약·관찰 후보)에 맞춰 흡수했다. 새 참조 문서는 접근성 구현 계약, 입력 촉감(제스처 물리·상태·확인과 되돌림), 그림자·깊이, 색 체계, 아이콘, 제품 UI 카피, 컴포넌트 기반 위에서 일하기, 리뷰 보고 형식, diff·PR 변경 리뷰, 인쇄·이메일이다. 모션·레이아웃·타이포·토큰·이미지·슬롭 지문·프리플라이트·감사 게이트·모바일·재질 문서도 보강했다. 리뷰 전용 경로를 추가했고, 하드 게이트에 "잘린 콘텐츠의 전체 값 도달 수단"과 "초당 3회 깜빡임 한도"를 더했다. + +`design-gate.mjs` 는 터치 타깃을 WCAG 2.5.8 층(간격 예외 계산)과 프로젝트 계약 층(44px)으로 나눠 보고하고(`tapTargetPolicy`, 기본값은 이전 동작과 같다), 프로젝트에 `axe-core` 가 있으면 뷰마다 axe 를 실행하고 없으면 미검증으로 보고한다. 실제 사이트 실측에서 드러난 오탐도 고쳤다 — 장식 이미지의 `alt=""` 는 더 이상 "alt 없음"이 아니고(속성 자체가 없을 때만 실패), 두 블록을 쌓은 워드마크는 텍스트 블록마다 줄 수를 세어 수축으로 오판하지 않으며, `path` 없이 `url` 만 준 페이지 설정은 크래시 대신 파일 기반 검사를 미검증으로 건너뛴다. Codex 용 스킬 메타데이터 `agents/openai.yaml` 과 외부 출처 고지 `THIRD_PARTY_NOTICES.md` 를 동봉한다. diff --git a/AGENTS.md b/AGENTS.md index 6ca0303..5c1c58f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,8 @@ node tools/visual.mjs --update-baseline # refresh visual baselines, then commi 7. **`DESIGNPACA_STATE_DIR`** redirects designpaca's own state dir (`~/.designpaca` manifest) for CI/tests without changing user-facing install paths. CLI tests already set `HOME`/`USERPROFILE` to a tempdir; keep that isolation in new tests. 8. **Git push goes over Tailscale**, not the domain: `ssh://git@100.116.83.60:2222/yunchan/designpaca.git` (git.chanpaca.net is behind Cloudflare, port blocked). Details in `.env.example`. 9. **Never create release/dev checkouts outside this repo root.** Past release runs used ad-hoc sibling `git worktree`s (`D:/workspace/designpaca-release-v0.x.x`, `designpaca-deploy-`). These look like stray copies, carry full `node_modules`, and are actually owned by the main repo's `.git/worktrees/`. Deleting the folder by hand leaves a `prunable` ghost in `git worktree list`; deleting the main `.git` breaks them. If a separate checkout is genuinely required, place it under the repo (e.g. `.worktrees/`) or a dedicated `_worktrees/` root, and always finish with `git worktree remove ` (use `--force` only for disposable release checkouts) — never `rm -rf`. Prefer committing on a branch in this working tree when possible. +10. **Never soften the step-0 interview gate.** v0.11.0 (`45ba92e`) rewrote "almost always ask" into "ask only when it would change the result", with an empty commit body and no changelog line. Combined with harness autonomy pressure (Claude Code auto mode, Codex "bias to action"), the skill stopped asking users. `build/ci/lint-skill.mjs` now fails if the `## 인터뷰 게이트` section, the "명시적으로 의존한다" sentence, or the `harness.md`/`brief-interview.md` links disappear, or if a "~때만 묻는다" condition returns. Any change to when the skill asks must be stated in its changeset and re-checked with `build/eval/interview/` on Claude Code and Codex. +11. **Keep tool names harness-neutral in the skill body.** Every target adapter copies `SKILL.md` verbatim, so a Claude-only tool name (e.g. `AskUserQuestion`) reaches Codex, Cursor, Gemini, and others unchanged. Put per-harness tool names, limits, and fallbacks in `references/harness.md`, not in the body. ## Architecture & data flow diff --git a/apps/site/tools/design-gate.mjs b/apps/site/tools/design-gate.mjs index 788a3a4..7976000 100644 --- a/apps/site/tools/design-gate.mjs +++ b/apps/site/tools/design-gate.mjs @@ -12,6 +12,7 @@ // 철학: 없는 도구는 SKIP(실패 아님), 검사 가능한 것은 전부 검사(실패면 exit 1). import { spawnSync } from "node:child_process"; import fs from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; import url from "node:url"; @@ -32,7 +33,9 @@ const DEFAULT_CFG = { widths: [320, 375, 390, 768, 1024, 1440], fontScales: [1, 1.3], thresholds: { contrastNormal: 4.5, contrastLarge: 3, visualDiffPct: 0.1, stackLines: 3, radiusLiterals: 3, tapTargetMin: 44, inlineTargetMin: 24, leftInsetMin: 12 }, - checks: { meta: true, contrast: true, stack: true, wrap: true, rhythm: true, scaleMatrix: true, tapTargets: true, leftInset: true, static: false, visual: false, webkit: false, deadCss: true, cssHygiene: true }, + checks: { meta: true, contrast: true, stack: true, wrap: true, rhythm: true, scaleMatrix: true, tapTargets: true, leftInset: true, static: false, visual: false, webkit: false, deadCss: true, cssHygiene: true, axe: "auto" }, + // "contract"(기본, 현재 동작 유지 — 컨트롤 <44px·인라인 <24px 실패) | "wcag"(WCAG 2.5.8 간격 예외 실패만 하드 게이트로 본다, 계약 미달은 참고로만 남긴다) + tapTargetPolicy: "contract", deadCssIgnore: ["is-"], // 동적으로 조립되는 클래스 접두사(is-${type} 등). 오탐 방지 — 프로젝트마다 추가한다 chromePath: null, }; @@ -41,12 +44,20 @@ if (argv.includes("--init")) { fs.writeFileSync(cfgFile, JSON.stringify(DEFAULT_CFG, null, 2) + "\n"); console.log("gate.config.json 초안 생성 — pages/views 를 프로젝트에 맞게 고치세요."); console.log("package.json: \"scripts\": { \"verify\": \"node <이 스크립트 경로>\" } 등록을 권장합니다."); + console.log("tapTargetPolicy: \"contract\"(44/24px 프로젝트 계약, 기본) 또는 \"wcag\"(WCAG 2.5.8 간격 예외만 하드 게이트) · checks.axe: \"auto\"(axe-core 있으면 실행, 없으면 미검증) 또는 false."); process.exit(0); } const _user = JSON.parse(fs.readFileSync(cfgFile, "utf8")); // checks·thresholds 는 깊은 병합 — 새 검사·임계값이 기본 켜지도록(실측: 프로젝트 구형 설정이 새 기본값을 지우고 "≥undefinedpx"를 찍었다) const CFG = { ...DEFAULT_CFG, ..._user, checks: { ...DEFAULT_CFG.checks, ...(_user.checks || {}) }, thresholds: { ...DEFAULT_CFG.thresholds, ...(_user.thresholds || {}) } }; +// path·url 둘 다 없는 페이지는 렌더도 파일 검사도 대상이 없다 — 실행 전에 명확한 설정 오류로 끊는다 +for (const p of CFG.pages || []) { + if (!p.path && !p.url) { + console.error(`gate.config 오류: pages 항목(name=${p.name || "?"}) 에 path 또는 url 이 필요합니다.`); + process.exit(2); + } +} const UPDATE = argv.includes("--update-baseline"); const results = []; const pass = (name, detail = "") => results.push({ ok: true, name, detail }); @@ -66,13 +77,17 @@ const CHROME_CANDIDATES = [ const CHROME = CHROME_CANDIDATES.find((p) => fs.existsSync(p)); const pageUrl = (p) => (p.url ? p.url : url.pathToFileURL(path.resolve(process.cwd(), p.path)).href); +const hasPath = (p) => typeof p.path === "string" && p.path.length > 0; // ── L0 정적 (옵션) ── if (CFG.checks.static) { const css = spawnSync("npx", ["stylelint", "**/*.css"], { shell: true, encoding: "utf8" }); css.status === 0 ? pass("L0 stylelint") : fail("L0 stylelint", (css.stdout || "").slice(0, 200)); - const html = spawnSync("npx", ["html-validate", ...CFG.pages.map((p) => p.path)], { shell: true, encoding: "utf8" }); - html.status === 0 ? pass("L0 html-validate") : fail("L0 html-validate", (html.stdout || "").slice(0, 200)); + const staticPages = CFG.pages.filter(hasPath); + if (staticPages.length) { + const html = spawnSync("npx", ["html-validate", ...staticPages.map((p) => p.path)], { shell: true, encoding: "utf8" }); + html.status === 0 ? pass("L0 html-validate") : fail("L0 html-validate", (html.stdout || "").slice(0, 200)); + } else skip("L0 html-validate", "path 없음(url 전용 페이지)"); } else skip("L0 정적", "checks.static=false"); // ── L0 정적: 죽은 선택자·토큰 위생 (브라우저 불필요. 실측 사고: @@ -80,6 +95,7 @@ if (CFG.checks.static) { const pageDir = (p) => path.dirname(path.resolve(process.cwd(), p.path)); const cssFilesOf = (p) => { const out = []; + if (!hasPath(p)) return out; let html = ""; try { html = fs.readFileSync(path.resolve(process.cwd(), p.path), "utf8"); } catch { return out; } for (const tag of html.matchAll(/]+rel=["']stylesheet["'][^>]*>/g)) { @@ -114,6 +130,7 @@ const contractFilesOf = (p, key, fallback) => { const scriptFilesOf = (p) => { const out = []; + if (!hasPath(p)) return out; let html = ""; try { html = fs.readFileSync(path.resolve(process.cwd(), p.path), "utf8"); } catch { return out; } for (const tag of html.matchAll(/]+src=["']([^"']+)["'][^>]*>/g)) { @@ -128,7 +145,7 @@ const scriptFilesOf = (p) => { const selectorDocumentsOf = (p) => contractFilesOf( p, "selectorDocuments", - [path.resolve(process.cwd(), p.path), ...scriptFilesOf(p)], + hasPath(p) ? [path.resolve(process.cwd(), p.path), ...scriptFilesOf(p)] : [], ); const missingFiles = (files) => files.filter((file) => !fs.existsSync(file)); @@ -136,6 +153,7 @@ const missingFiles = (files) => files.filter((file) => !fs.existsSync(file)); if (CFG.checks.deadCss) { const ignore = CFG.deadCssIgnore || []; for (const p of CFG.pages) { + if (!hasPath(p)) { skip(`${p.name} 죽은 선택자`, "path 없음(url 전용 페이지)"); continue; } const files = contractFilesOf(p, "selectorFiles", cssFilesOf(p)); const documents = selectorDocumentsOf(p); const missing = missingFiles([...files, ...documents]); @@ -162,6 +180,7 @@ if (CFG.checks.deadCss) { if (CFG.checks.cssHygiene) { for (const p of CFG.pages) { + if (!hasPath(p)) { skip(`${p.name} 토큰 위생`, "path 없음(url 전용 페이지)"); continue; } const files = contractFilesOf(p, "hygieneFiles", cssFilesOf(p)); if (!files.length) { skip(`${p.name} 토큰 위생`, "로컬 CSS 없음"); continue; } const inferredTokens = cssFilesOf(p).filter((f) => /tokens/i.test(path.basename(f))); @@ -214,6 +233,17 @@ let puppeteer; try { puppeteer = await import("puppeteer-core"); } catch { console.log("puppeteer-core 미설치 — npm i -D puppeteer-core 필요"); process.exit(1); } +// ── axe-core 선택 해석 (checks.axe: "auto" 기본). 프로젝트 cwd 기준으로 해석되면 +// 각 page/view 에 소스를 주입해 실행하고, 안 되면 "미검증"으로 보고한다 — 통과로 추정하지 않는다. +// audit-gate.md 가 axe 를 해법으로 적어놓고 도구엔 구현이 없던 괴리를 없앤다. ── +let axeSource = null; +if (CFG.checks.axe !== false) { + try { + const req = createRequire(path.join(process.cwd(), "noop.js")); + axeSource = fs.readFileSync(req.resolve("axe-core"), "utf8"); + } catch { /* 미설치 — 아래에서 SKIP 으로 보고 */ } +} + const UTILS = ` window.__lines = (sel) => { const el = typeof sel === "string" ? document.querySelector(sel) : sel; @@ -232,6 +262,21 @@ const UTILS = ` const text = [...el.childNodes].find((node) => node.nodeType === 3 && node.textContent.trim()); return window.__lines(text || el); }; + // 요소 전체를 하나의 Range 로 재면(예: display:inline-grid 로 두 텍스트 블록을 세로로 + // 쌓은 의도된 로크업) 자식 인라인 박스 rect 가 섞여 줄 수가 과다 계산된다(실측: 조각달 + // ".wordmark" + 2블록이 "3줄"로 오카운트). 비어 있지 않은 텍스트 노드 + // 하나하나를 각자의 Range 로 재면 이 섞임이 없다 — 블록마다 실제 줄 수만 남는다. + window.__brandTextBlocks = (el) => { + if (!el) return []; + const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + const out = []; + let n; + while ((n = walker.nextNode())) { + const text = n.textContent.trim(); + if (text) out.push({ text, lines: window.__lines(n) }); + } + return out; + }; window.__heading = (sel) => { let el; try { el = document.querySelector(sel); } @@ -318,7 +363,9 @@ for (const p of CFG.pages) { return b && b !== "rgba(0, 0, 0, 0)" ? b : getComputedStyle(document.documentElement).backgroundColor; })(), h1Count: document.querySelectorAll("h1").length, - imgNoAlt: imgs.filter((i) => !i.getAttribute("alt")).length, + // alt="" 는 장식 이미지 선언(의도)이다 — 실패는 alt 속성 자체가 없는 img 만 + imgNoAlt: imgs.filter((i) => !i.hasAttribute("alt")).length, + imgDecorative: imgs.filter((i) => i.hasAttribute("alt") && i.getAttribute("alt") === "").length, jsonld: [...document.querySelectorAll('script[type="application/ld+json"]')].every((s) => { try { JSON.parse(s.textContent); return true; } catch { return false; } }), jsonldCount: document.querySelectorAll('script[type="application/ld+json"]').length, }; @@ -347,7 +394,7 @@ for (const p of CFG.pages) { if (meta.imgNoAlt) m.push(`img alt 없음 ${meta.imgNoAlt}개`); if (meta.jsonldCount && !meta.jsonld) m.push("JSON-LD 파싱 실패"); // 데모·로컬 페이지는 canonical/robots 미명시 허용 — 있으면 오히려 검증 - m.length ? fail(`${p.name} SEO/meta`, m.join(" · ")) : pass(`${p.name} SEO/meta`, `title ${meta.titleLen}자·desc ${meta.descLen}자·OG✓·alt ${meta.imgNoAlt}결`); + m.length ? fail(`${p.name} SEO/meta`, m.join(" · ")) : pass(`${p.name} SEO/meta`, `title ${meta.titleLen}자·desc ${meta.descLen}자·OG✓·alt 누락 ${meta.imgNoAlt} · 장식(alt="") ${meta.imgDecorative}`); } // ── 각 뷰 불변식 ── @@ -509,50 +556,122 @@ for (const p of CFG.pages) { } } bad.length ? fail(`${p.name} 스케일×폭 h1`, bad.join(",")) : pass(`${p.name} 스케일×폭 h1`, `줄수 정보 ${measurements.join(", ")}`); - // 브랜드 1줄 + // 브랜드 줄바꿈 — 텍스트 블록별(예: 로고의 로크업 두 줄) 각자 1줄이어야 한다. + // 여러 텍스트 블록을 한 Range 로 합쳐 재면 자식 인라인 박스가 섞여 과다 계산된다. await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true }); await page.goto(href, { waitUntil: "networkidle0" }); await page.evaluate(UTILS); if (p.brand) { // 동일 셀렉터가 여러 개(사이드바+탑바 등)면 보이는 것을 잰다 — display:none 의 rect 는 0 - const bl = await page.evaluate((sel) => { + const blocks = await page.evaluate((sel) => { const els = [...document.querySelectorAll(sel)]; const el = els.find((e) => e.offsetParent !== null || getComputedStyle(e).position === "fixed") || els[0]; - return window.__textLines(el); + return el ? window.__brandTextBlocks(el) : null; }, p.brand); - bl === 1 ? pass(`${p.name} 브랜드 1줄`) : fail(`${p.name} 브랜드 ${bl}줄(수축 의심)`); + if (blocks === null) { + fail(`${p.name} 브랜드 줄바꿈`, `선택자 매칭 없음 (${p.brand})`); + } else { + const wrapped = blocks.find((b) => b.lines >= 2); + wrapped + ? fail(`${p.name} 브랜드 줄바꿈`, `"${wrapped.text.slice(0, 12)}" ${wrapped.lines}줄(수축 의심)`) + : pass(`${p.name} 브랜드 줄바꿈 없음`, `텍스트 블록 ${blocks.length}개, 각 1줄`); + } } } - // ── 터치 타깃 (모바일 — HIG 44pt·M3 48dp, WCAG 2.5.8 인라인 최소 24px. - // 실측: coarse 포인터에서 28px 버튼이 정확히 못 눌렸다는 보고로 시작된 검사. - // 기준: 컨트롤(button·input·role) 44px — 본문 흐름 속 텍스트 링크(a)·예외 선택자는 24px) ── + // ── 터치 타깃 — 두 층으로 나눠 판정한다 (실측: coarse 포인터에서 28px 버튼이 정확히 못 눌렸다는 + // 보고로 시작된 검사). + // 1) WCAG 2.5.8(하드 게이트, 규범): 인라인이 아닌 대상 중 min(width,height) < 24px 는 + // 간격 예외(24px 지름 원이 다른 대상의 bbox·다른 undersized 대상의 원과 겹치지 않음)를 + // 계산한다. 문장 속 인라인 링크는 WCAG 2.5.8 의 inline 예외라 이 층에서 아예 빼고, + // 계약 층에서만(24px) 본다. + // 2) 계약 층(프로젝트·플랫폼 목표): HIG 44pt·M3 48dp 기준 컨트롤 ≥44px, 인라인 ≥24px. + // tapTargetPolicy:"contract"(기본, 이전 동작과 호환)면 계약 미달도 실패, + // "wcag"면 WCAG 층 실패만 실패로 보고하고 계약 미달은 통과 detail 에 참고로만 남긴다. if (CFG.checks.tapTargets) { await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true }); await page.goto(href, { waitUntil: "networkidle0" }); await page.evaluate(UTILS); for (const v of p.views || ["single"]) { await goView(v); - const bad = await page.evaluate((min, inlineMin, inlineSel) => { - const out = []; + const result = await page.evaluate((min, inlineMin, inlineSel, policy) => { + const targets = []; document.querySelectorAll("button, a, input, select, textarea, [role='button'], [role='tab'], [role='link'], [role='switch'], [role='checkbox']").forEach((el) => { const cs = getComputedStyle(el); if (cs.display === "none" || cs.visibility === "hidden") return; const r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return; const inline = cs.display === "inline" || (el.tagName === "A" && el.hasAttribute("href")) || (inlineSel || []).some((s) => el.matches(s)); - const need = inline ? inlineMin : min; - if (Math.min(r.width, r.height) < need) { - const label = el.textContent.trim().slice(0, 8) || el.getAttribute("aria-label") || el.tagName.toLowerCase(); - out.push(`"${label}" ${Math.round(r.width)}×${Math.round(r.height)}${inline ? "(인라인)" : ""}`); - } + const label = el.textContent.trim().slice(0, 8) || el.getAttribute("aria-label") || el.tagName.toLowerCase(); + targets.push({ x: r.left, y: r.top, w: r.width, h: r.height, inline, label }); }); - return [...new Set(out)].slice(0, 5); - }, CFG.thresholds.tapTargetMin, CFG.thresholds.inlineTargetMin, CFG.tapTargetsInline || []); - bad.length ? fail(`${p.name}/${v} 터치타깃`, bad.join(" · ")) : pass(`${p.name}/${v} 터치타깃`, `컨트롤 ≥${CFG.thresholds.tapTargetMin}px·인라인 ≥${CFG.thresholds.inlineTargetMin}px`); + // 간격 예외: 반지름 12 원(중심=bbox 중심)이 (a) 다른 모든 대상의 bbox 와 교차하지 않고 + // (b) 다른 undersized 대상의 원과 교차하지 않으면(중심 간 거리 ≥24) 충족. + const exceptionMet = (t) => { + const cx = t.x + t.w / 2, cy = t.y + t.h / 2; + for (const o of targets) { + if (o === t) continue; + const nx = Math.max(o.x, Math.min(cx, o.x + o.w)); + const ny = Math.max(o.y, Math.min(cy, o.y + o.h)); + if (Math.hypot(cx - nx, cy - ny) < 12) return false; + } + for (const o of targets) { + if (o === t || Math.min(o.w, o.h) >= 24) continue; + const ocx = o.x + o.w / 2, ocy = o.y + o.h / 2; + if (Math.hypot(cx - ocx, cy - ocy) < 24) return false; + } + return true; + }; + const fails = []; const notes = []; + for (const t of targets) { + const size = Math.min(t.w, t.h); + const wcagFail = !t.inline && size < 24 && !exceptionMet(t); + const contractMin = t.inline ? inlineMin : min; + const contractFail = size < contractMin; + const sizeLabel = `"${t.label}" ${Math.round(t.w)}×${Math.round(t.h)}${t.inline ? "(인라인)" : ""}`; + if (wcagFail) { + fails.push(`WCAG 2.5.8: ${sizeLabel}`); + } else if (contractFail) { + if (policy === "contract") { + fails.push(`계약 ${contractMin}px: ${sizeLabel}`); + } else { + const note = !t.inline && size < 24 ? "(WCAG 간격 예외 충족)" : ""; + notes.push(`계약 ${contractMin}px: ${sizeLabel}${note}`); + } + } + } + return { fails: [...new Set(fails)].slice(0, 5), notes: [...new Set(notes)].slice(0, 5) }; + }, CFG.thresholds.tapTargetMin, CFG.thresholds.inlineTargetMin, CFG.tapTargetsInline || [], CFG.tapTargetPolicy); + const summary = `컨트롤 ≥${CFG.thresholds.tapTargetMin}px·인라인 ≥${CFG.thresholds.inlineTargetMin}px`; + if (result.fails.length) fail(`${p.name}/${v} 터치타깃`, result.fails.join(" · ")); + else if (result.notes.length) pass(`${p.name}/${v} 터치타깃`, `${summary} — 계약 참고: ${result.notes.join(" · ")}`); + else pass(`${p.name}/${v} 터치타깃`, summary); } } else skip("터치타깃", "checks.tapTargets=false"); + // ── axe-core 접근성 (선택 — checks.axe:"auto" 기본. 해석되면 각 view 에서 WCAG 2.x AA + // 태그로 실행하고, 위반은 규칙 id·영향·대상 수로 보고한다. 해석 안 되면 미검증으로 보고하고 + // 통과로 추정하지 않는다) ── + if (CFG.checks.axe === false) { + skip(`${p.name} axe`, "checks.axe=false"); + } else if (!axeSource) { + skip(`${p.name} axe`, "미검증 — axe-core 를 찾지 못했다(npm i -D axe-core)"); + } else { + await page.setViewport({ width: 1440, height: 900 }); + await page.goto(href, { waitUntil: "networkidle0" }); + for (const v of p.views || ["single"]) { + await goView(v); + await page.addScriptTag({ content: axeSource }); + const report = await page.evaluate(async () => { + const r = await window.axe.run(document, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] } }); + return r.violations.map((v) => ({ id: v.id, impact: v.impact, count: v.nodes.length })); + }); + report.length + ? fail(`${p.name}/${v} axe`, report.map((r) => `${r.id}(${r.impact}) ${r.count}건`).join(" · ")) + : pass(`${p.name}/${v} axe`, "WCAG 2.x AA 위반 0건"); + } + } + // ── 왼쪽 인셋(모바일) — 오버플로 검사는 오른쪽만 본다. // 실측 사고: 제목 16px vs 앱바·카드 24px 혼재가 모든 게이트를 통과했고 // 사용자에게 '가장자리에 붙어 깨졌다'고 보고됐다. 텍스트 노드 Range 로 글자 시작점을 잰다 diff --git a/build/ci/lint-skill.mjs b/build/ci/lint-skill.mjs index 36ebb12..ebb7e76 100644 --- a/build/ci/lint-skill.mjs +++ b/build/ci/lint-skill.mjs @@ -38,6 +38,18 @@ if (!fmMatch) { } } +// 0단계 인터뷰 게이트가 조용히 약해지지 않았는지 확인한다. v0.11.0(45ba92e)에서 "거의 항상 묻는다"가 +// "결과를 크게 바꿀 때만 묻는다"로 바뀌었는데 커밋·체인지로그 어디에도 적히지 않아 아무도 알아채지 못했다. +if (!/^## 인터뷰 게이트/m.test(md) || !md.includes("명시적으로 의존한다")) { + errors.push("인터뷰 게이트 절(## 인터뷰 게이트, '명시적으로 의존한다')이 없다"); +} +for (const rel of ["references/harness.md", "references/brief-interview.md"]) { + if (!md.includes(rel)) errors.push(`인터뷰 게이트가 가리켜야 할 문서 링크가 없다: ${rel}`); +} +if (/때만\s*(?:아래 항목을\s*)?묻는다/.test(md)) { + errors.push("본문에 '~때만 묻는다' 조건이 있다 — 질문 여부를 모델의 자기평가에 맡기는 완화 문구다"); +} + // 본문이 참조하는 파일이 실제로 있는지 확인한다 (references/xxx.md 형태) const refs = [...md.matchAll(/references\/[A-Za-z0-9._/-]+\.md/g)].map((m) => m[0]); for (const rel of new Set(refs)) { diff --git a/docs/design-skills-intake-plan-20260924.md b/docs/design-skills-intake-plan-20260924.md new file mode 100644 index 0000000..30e4deb --- /dev/null +++ b/docs/design-skills-intake-plan-20260924.md @@ -0,0 +1,388 @@ +# 외부 디자인 스킬 10종 흡수와 인터뷰 복원 계획 · 2026-09-24 + +## 0. 요약 + +- 외부 디자인 스킬 10종을 모두 원본 저장소에서 확보했다. 9종은 `npx skills add`로 격리 샌드박스에 실제 설치했고, `adapt`는 impeccable 통합 스킬의 하위 커맨드라 단독 설치가 불가능해 클론으로 받았다. 실제 홈의 스킬 디렉터리는 건드리지 않았다. +- 10종과 그 의존물(better-interface의 자매 스킬 7종, impeccable 공통 기반)을 전수 추출해 designpaca와 대조했다. 갭 후보 335건 중 우리에게 이미 있는 것은 26건이고, 원칙과 충돌하는 것은 20건이다. 값이 큰 미보유 항목은 66건이다. +- 가장 큰 공백은 여섯 가지다. 키보드·폼·라이브 리전 수준의 접근성 구현 계약, 입력에 반응하는 촉감(제스처 물리·상태·피드백), 그림자·깊이 체계, 리뷰 전용 경로(심각도·보고 형식·diff 기반 변경 리뷰), 제품 UI 카피, 색 체계 상세다. +- 인터뷰 약화의 직접 원인은 커밋 `45ba92e`(v0.11.0, 2026-09-12)다. 이 커밋이 "위 넷은 거의 항상 묻는다"를 "결과를 크게 바꿀 때만 묻는다"로 바꿨다. 여기에 두 가지가 겹쳤다. 하나는 Claude Code auto 모드의 기본값 전환(2026-08-14, "명확화 질문 없이 계속 작업하도록 유도")이다. 다른 하나는 Codex 기본 지침의 자율 진행 압력이다. 모든 설치 어댑터가 Claude 전용 도구명 `AskUserQuestion`을 그대로 전달한다는 문제도 있다. +- 해법은 두 가지다. 첫째, 인터뷰를 "판단"이 아니라 "기계적 게이트"로 바꾼다. 브리프 슬롯이 사용자에게 확인되지 않았으면 묻고, 질문 수단의 유무는 도구 목록으로 판정하고, 물었으면 턴을 끝낸다. 둘째, 하네스별 질문 도구·한도·폴백을 명시한다. +- 새 참조 문서 12개를 만들고 기존 문서 13개를 보강한다. SKILL.md 본문은 파이프라인·게이트·라우팅만 맡는 원칙을 유지하고 500줄 이하로 묶는다. + +## 1. 목적·범위·비목표 + +**목적** + +1. 10개 스킬과 의존물의 장점을 상세 수준(수치·절차·코드)까지 흡수하되 designpaca의 장점을 하나도 버리지 않는다. +2. 0단계 인터뷰가 모든 지원 하네스에서 실제로 일어나게 만든다. +3. 하네스별 기능(질문 도구, 계획 도구, 병렬 조사, 브라우저 검증)을 쓰게 만든다. + +**범위** + +- `packages/skill/`의 SKILL.md와 references +- `packages/skill/tools/design-gate.mjs`와 그 테스트 +- `build/ci/lint-skill.mjs` +- 로컬 인터뷰 평가 스크립트 +- `AGENTS.md`의 재발 방지 주의사항 +- changeset + +**비목표** + +- 릴리스와 태그 push +- 실제 홈에 설치된 스킬의 갱신 +- 설치 어댑터가 하네스별로 본문을 컴파일하는 기능(9절에서 보류 사유를 적는다) +- 외부 스킬 원문을 그대로 붙여 넣는 일 + +## 2. 조사 방법과 증거 + +모든 원자료는 추적되지 않는 `outputs/skill-intake/` 아래에 있다. + +| 산출물 | 위치 | +|---|---| +| 원본 클론 10종 | `outputs/skill-intake/sources//repo` | +| 샌드박스 설치본 | `outputs/skill-intake/sandbox//` (HOME·USERPROFILE을 샌드박스로 격리) | +| 전수 다이제스트 17종 | `outputs/skill-intake/digests/*.md` | +| designpaca 인벤토리 | `outputs/skill-intake/our-inventory.md` | +| 1차 갭 매트릭스(141건) | `outputs/skill-intake/gap-matrix.md` | +| 인터뷰 포렌식 | `outputs/skill-intake/interview-forensics.md` | +| impeccable 컨텍스트·하네스 구조 | `outputs/skill-intake/impeccable-context-harness.md` | + +절차는 다음 순서로 진행했다. + +1. 원본을 확정하고 설치했다. +2. 다이제스트를 쓰고 우리 스킬과 갭을 대조했다. +3. 반박 검증을 돌렸다. 우리 문서가 한국어라 영어 grep만 쓰면 가짜 누락이 생기므로, 한국어 동의어로 다시 찾게 했다. +4. 1차 갭 분석이 빠뜨린 항목을 찾는 완전성 비평을 돌렸다. +5. 하네스 관련 주장을 1차 출처 원문 인용으로 교차 검증했다. + +에이전트는 모두 80개를 썼고 오류는 0건이었다. 오케스트레이터는 핵심 주장을 직접 다시 확인했다. 확인한 것은 `45ba92e` diff, 우리 문서의 grep 결과, 게이트 임계값, motion.md 문구다. + +### 원본과 라이선스 + +| 스킬 | 실제 원본 | 커밋 | 라이선스 | +|---|---|---|---| +| frontend-design | anthropics/skills `skills/frontend-design` | 34040c9 (2026-09-10) | Apache-2.0 | +| apple-design | emilkowalski/skills `skills/apple-design` | 85e8e23 | MIT | +| beautiful-shadows | MengTo/Skills `agent-skills/web-design/beautiful-shadows` | a965851 | MIT | +| accessibility | addyosmani/web-quality-skills `skills/accessibility` | afa8da9 | MIT | +| design-review | superfuture/design-review | d4d2609 | MIT | +| emil-design-eng | emilkowalski/skills `skills/emil-design-eng` | 85e8e23 | MIT | +| shadcn | shadcn-ui/ui `skills/shadcn` (ui-skills.com은 카탈로그 미러) | 98a1fe6 | MIT | +| adapt | pbakaus/impeccable `skill/reference/adapt.md` (통합 스킬의 24개 커맨드 중 하나) | e0881d2 | Apache-2.0 | +| better-interface | jakubkrehel/skills `skills/better-interface` + 자매 7종 | 267330e | MIT | +| interaction-design | wshobson/agents `plugins/ui-design/skills/interaction-design` (출처 미기재라 가장 널리 쓰이는 것을 선정) | 4236bb9 | MIT | + +MIT와 Apache-2.0은 모두 재사용할 수 있다. 조건은 두 가지다. 번역·재서술한 상당 부분에는 저작권 고지를 남긴다. Apache-2.0 원본은 NOTICE 내용과 변경 사실을 밝힌다. 그래서 `packages/skill/THIRD_PARTY_NOTICES.md`를 신설한다. + +## 3. 보존할 장점과 흡수 가드레일 + +다음은 designpaca만의 강점이다. 흡수 과정에서 약해지면 안 된다. + +- **파이프라인**: 0~6단계와 경로 판정(전체·연장·국소), 통과 조건 +- **레퍼런스 우선**: 1단계 건너뛰기 금지, 국소 레퍼런스 1′ +- **판정 위계**: 하드 게이트 / 프로젝트 계약 / 스타일 휴리스틱. 외부 스킬 대부분에는 이 구분이 없다 +- **근거 기록**: 증거 원장, 불확실성 라우팅, design.md 기록 +- **자율 검증 폐쇄 루프**: design-gate.mjs와 렌더 평가 +- **로케일 우선**과 한글 조판 +- **진실성 계약**: 지어낸 수치 금지, placeholder 통과 +- **성능 예산** + +흡수 규칙은 여섯 가지다. + +1. **층 라벨**: 외부 규칙은 들어오기 전에 판정 위계의 세 층 중 하나로 분류한다. "절대"·"정확히 이 값" 같은 어법은 층에 맞게 바꾼다. 스타일 수치는 "시작값·관찰 후보"로 내린다. +2. **출처 표기**: 외부 수치와 코드에는 출처 ID를 단다(예: `EXT-APPLE-FLUID`). 원 출처가 있으면 함께 적는다(예: WWDC 2018 Designing Fluid Interfaces). 근거 없는 매직넘버에는 "실측 조정" 표시를 붙인다. +3. **조건부 적재**: 특정 미학·스택·매체(Apple풍, shadcn, 인쇄·이메일, RTL, 다국어)는 트리거 조건이 있을 때만 여는 문서나 절에 둔다. +4. **본문 불변 원칙**: 상세 지식은 references에 둔다. SKILL.md에는 라우팅 행과 게이트만 더한다. +5. **한국어 적응**: 문서는 한국어 서술체로 쓴다. 카피 예시와 문장부호, 색의 문화적 의미는 ko-KR 기준으로 다시 쓴다(예: 한국 증권 화면은 빨강이 상승이다). +6. **모순 해소 기록**: 기존 규칙과 부딪히면 8절의 결정을 따른다. 결정 내용은 해당 문서에 각주로 남긴다. + +## 4. 스킬별 판정 — 우리에게 없던 것과 가져오는 방법 + +| 스킬 | 우리에게 없던 핵심 | 가져갈 곳 | 조건부·기각 | +|---|---|---|---| +| frontend-design | AI 생성 디자인의 5대 제네릭 클러스터(hex 포함, `#D97757`은 Anthropic 자체 액센트라 Claude 계열 도구가 만든 티로 읽힌다는 경고), 방향 결정 단계의 반사실 제네릭 검증, 제품 UI 카피의 목소리, "흩어진 효과보다 오케스트레이션된 한 순간" | antipatterns §1·§3, SKILL.md 2단계 점검 1줄, product-copy.md, motion.md §0 | "스스로 정하고 확인만" 인터뷰 태도 기각 | +| apple-design | 인터럽트 가능성(현재 렌더값에서 재시작, 속도 블렌드), 스프링 damping/response, 속도 인계식, 모멘텀 투사(d=0.998), 러버밴드(k=0.55), 제스처 인식(히스테리시스 ~10px, 포인터 캡처, 그랩 오프셋), 멀티모달 피드백 3원칙, reduced-transparency·prefers-contrast, 머티리얼 무게 위계, 입력 경로 지연 감사, 모달 스크림 차등 | interaction-feel.md, elevation.md, accessibility.md | Apple풍 미학을 기본값으로 삼는 것은 기각하고 style-playbook의 조건부 항목으로 둔다 | +| beautiful-shadows | 3단 다층 elevation 값(sm 3층, md 6층 등비, lg 6층 비선형 알파), 밀도에 따른 단계 매핑, 오용 규칙 | elevation.md, tokens.md 형태 절에서 링크 | Tailwind 임의값 문법은 CSS로 번역, "색 틴팅 금지"는 휴리스틱으로 격하 | +| accessibility | 증거 우선 감사 루프(Lighthouse·axe → 실패 노드 국소화 → 수동 검증 → 재감사, DevTools MCP가 없으면 CLI 폴백), "자동 점수 100 ≠ 준수", WCAG 2.2 신규 기준, 스킵 링크·라이브 리전·포커스 트랩·탭 ARIA 코드, 네이티브 button에 keydown을 달면 이중 실행되는 함정, 깜빡임 초당 3회 한도 | accessibility.md, audit-gate.md 감사 루프, preflight §1 행 | — | +| design-review | What·Why·Fix 3요소 finding, 심각도 랭킹과 "전부 나열 금지", Strengths로 마무리, 입력 종류별 진입(URL만 오면 스크린샷 요청), 자동 적용 화이트리스트 | critique.md, 0단계 리뷰 경로 | 익명 텔레메트리·유료 게이팅 전면 배제, 서체 ≤2개·타입 스케일 단계수 규범 기각, "시각자료가 없을 때만 질문" 태도 기각 | +| emil-design-eng | 사용 빈도로 애니메이션 여부를 정하는 표(하루 100회 이상·키보드 동작은 무애니), 체감 성능, 트랜지션과 키프레임의 재조준 차이, `scale(0)` 진입 금지, 모달 transform-origin 예외, 툴팁 그룹 즉시 열림, clip-path 레시피 4종(hold-to-delete 등), 드래그 dismiss·멀티터치 가드·마찰, Motion 축약 속성이 하드웨어 가속이 아니라는 점, 경량 CSS 3D, 모션 QA(슬로모·프레임 단위), Before·After·Why 표 | motion.md, interaction-feel.md, critique.md | "ease-in 절대 금지"는 진입 한정 각주로, velocity 0.11 같은 매직넘버는 실측 조정 시작값으로 | +| shadcn | 기존 컴포넌트 우선과 합성 규칙(Dialog Title 필수, Avatar Fallback, Group 안에 Item), Base UI와 Radix의 API 차이, `components.json`을 먼저 읽기, `--dry-run`·`--diff` 안전 병합, `--overwrite`는 명시 승인 필요, "추측하지 말라·사용자 대신 기본값을 고르지 말라" 하드 트리거 문구, 행동 계약형 eval | component-systems.md(감지 시 조건부), 6절 인터뷰 문구 강도, 7절 평가 설계 | Tailwind 위생 10규칙은 Tailwind 프로젝트 조건부 요약만, CLI 플래그와 레지스트리 스키마는 기각 | +| adapt (impeccable) | 입력 방식을 화면 크기와 별개 축으로 다루는 pointer·hover 쿼리 코드, `srcset`·`sizes`·`picture`, 컨테이너 쿼리 코드, 커스텀 컨트롤 제스처 검증(레이아웃 통과가 제스처 통과가 아니다, 증거 출처 명시, 미검증은 보고된 공백), 인쇄·이메일 적응, 표를 카드로 바꾸는 변환, 브라우저 표면(selection·caret·scrollbar) 테마화 | layout.md §4, images.md, audit-gate.md, print-email.md, antipatterns | 모바일 하단 내비 기본값은 기각(우리 결정표가 더 엄밀하다) | +| impeccable 공통 기반 | 컨텍스트 수집 순서(스캔 → 가설 → 질문 라운드 상한 → 질문 수단의 기계적 판정 → 무응답 시 추론에 라벨을 붙이고 첫 응답에 고지), "저장소 증거는 가설이지 사용자 승인이 아니다", `{{ask_instruction}}`을 하네스 19개에 맞춰 빌드 시점에 치환 | 6절 인터뷰 설계의 뼈대, harness.md | 빌드 시점 컴파일은 보류(9절) | +| better-interface + 자매 7종 | 에스컬레이션 트리거 13개(규칙 소유와 심각도 부여를 분리), 저비용 수정 사다리(삭제 → 플랫폼 → 재사용 → 값 교정 → 추가), 미검증·미점검 구분, 스코프 축소 규율, 증거 방향성 제약. 자매 스킬 쪽은 아래 여섯 영역 | critique.md, change-review.md, accessibility.md, color.md, icons.md, product-copy.md, typography·layout·tokens·motion 보강 | "근사가 아니라 정확히 이 값" 규율 기각, 고정 글자 크기·행간 하한 기각, finding 상한 15 수치 기각(절차만 채택) | +| interaction-design | 토스트, 스켈레톤 시머, 스와이프 동작, 당겨서 새로고침, 낙관적 업데이트, 토글, 글자 수 카운터 레시피. 스프링 프리셋 6종과 cubic-bezier 근사, 클린업 규율, GSAP·WAAPI·View Transitions 예제(CSS 대안 병기), 스크롤 throttle | interaction-feel.md, motion.md §4 | 리플은 기본값에서 빼고 조건부로, "질문 없이 코드 기본값으로 고정" 태도 기각 | + +better-interface 자매 스킬 쪽에서 가져오는 여섯 영역은 다음과 같다. + +- **키보드 위젯 계약**: tabindex, roving tabindex, APG 패턴표, inert, SPA 라우트 포커스 +- **폼**: autocomplete, inputmode, 붙여넣기 허용, 제출 버튼 비활성화 금지, disabled와 aria-disabled 구분, 24px 원 간격 예외, forced-colors +- **색**: 램프, APCA, 그라디언트 보간 공간, 다크모드 재조정, 전환 메커니즘, 색의 문화적 의미 +- **레이아웃**: RTL과 논리 속성, 그루핑 1:2 비율, 점진적 공개 레시피, 컨트롤 식별 +- **타이포**: text-wrap, 밑줄 메트릭, text-box trim, 문장부호, 잘린 텍스트의 도달 수단, 선택 가능성 +- **UI 마감**: 아이콘 도메인, 동심 radius, 광학 정렬, 테두리를 대신하는 그림자, 테마 전환 트랜지션 억제 +- **카피**: 용어 일관성, 톤과 위험의 매트릭스, 오류·빈 상태 문구 내용, 문장 조각 조립 금지 +- **변경 리뷰**: diff 스코프, 제거된 쪽 읽기, Introduced·Regression·Pre-existing 구분 + +## 5. 인터뷰 약화 — 확인된 원인과 하네스 사실 + +### 5-1. 스킬 쪽 원인 (git과 설치본으로 확인) + +| 커밋 | 방향 | 변경 | +|---|---|---| +| `4e1e5f0` | 강화 | 인터뷰 절 신설, "가정으로 채우지 마라", AskUserQuestion으로 한 번에 묻기 | +| `b92853b` | 강화 | "위 넷은 거의 항상 묻는다", 브랜드 색 질문은 "결과를 되돌릴 수 없게 바꾸는 질문" | +| **`45ba92e`** (v0.11.0, 2026-09-12) | **완화** | "브리프·기존 코드·브랜드 자산에서 답을 확인할 수 없고 결과를 크게 바꿀 때만 묻는다", 브랜드 색 질문도 조건부로 바뀜. 커밋 본문과 CHANGELOG에 이 변경 기록이 없다 | + +- 현재 질문을 억제하는 문구의 위치는 다음과 같다. `SKILL.md:91`(로케일), `SKILL.md:148`(인터뷰 전체의 조건), `SKILL.md:161`, `SKILL.md:164`(브랜드 색), `SKILL.md:181`("관례가 명확한 것은 묻지 않는다"), `SKILL.md:187`("되돌리기 쉬운 습작이면 가정"), `references/images.md:22`. +- `~/.agents/skills/designpaca/SKILL.md.orig`가 완화 이전 문구를 보존하고 있다. 강제 업데이트가 이전 인터뷰 절을 덮어썼다는 직접 증거다. +- `~/.gemini/antigravity/skills/designpaca`만 v0.8.0으로 남아 있다. 하네스마다 질문 빈도가 달라 보이는 한 원인이다. +- `packages/core/src/targets/*.ts`의 어댑터 10개가 모두 본문을 변형 없이 복사한다. SKILL.md에는 하네스 언급이 0건이고, `AskUserQuestion`은 144행 한 곳에만 나온다. + +### 5-2. 하네스 쪽 사실 (2026-09-24, 1차 출처 인용으로 검증) + +| 하네스 | 질문 도구와 한도 | 쓸 수 있는 조건 | 질문을 억누르는 압력 | +|---|---|---|---| +| Claude Code | `AskUserQuestion`: 질문 1~4개, 선택지 2~4개, header 12자, multiSelect, preview | 메인 세션만. Agent 도구로 띄운 서브에이전트에서는 쓸 수 없다(공식 문서). `context: fork` 스킬에서도 깨진다(이슈 #19751). 어떤 모드에서도 자동 승인되지 않는다 | auto 모드는 Pro·Max·Team에서 2026-08-14부터 기본값이다. 공식 문서: "nudges Claude to keep working without stopping for clarifying questions, though Claude still asks when your prompt or a skill explicitly relies on it." 도구 설명 자체도 "합리적 기본값으로 풀 수 있으면 묻지 말라"는 취지다 | +| Codex | `request_user_input`: 질문 1~3개(1개 권장), 선택지 2~3개, 추천안을 맨 앞에 두고 "(Recommended)" 표기, "Other"는 클라이언트가 자동 추가 | Plan 모드에서만 기본 활성. Default 모드는 개발 중인 `default_mode_request_user_input` 플래그가 필요하다. `codex exec`에서는 거부된다. 응답이 없으면 60~240초 뒤 빈 답이 자동 제출된다 | 내장 프롬프트(gpt_5_1·5_2)의 "Autonomy and Persistence": 턴 안에서 끝까지 처리하라. Codex Prompting Guide: "Bias to action … do not end your turn with clarifications unless truly blocked." | +| Gemini CLI | `ask_user`: 질문 1~4개, choice·text·yesno, multiSelect | Plan 모드가 기본 활성. Plan을 빠져나가면 YOLO로 전환된다. YOLO에서는 빈 답으로 자동 통과되는 버그가 있다(#18540) | 실행 단계가 자동으로 넘어간다 | +| VS Code Copilot | `askQuestions` (질문 캐러셀, v1.110부터 코어) | agent 모드 | cloud agent는 비동기라 대화가 없다 | +| Cursor | Plan 모드의 질문 흐름(공식 문서에 도구 이름이 없고 `AskQuestion`은 포럼 보고뿐) | Plan 모드 전용(하네스가 주입), Agent 모드에서는 쓸 수 없다 | — | +| Windsurf, Antigravity, Zcode, AGENTS.md 경로 | 공개 문서로 확인된 구조화 질문 도구 없음 | 채팅으로 묻는 것은 가능 | Antigravity "Always Proceed", Zcode의 일반 질문 5분 타임아웃(2차 조사, 재확인 필요) | + +결론은 이렇다. 조건부 문구가 판단을 모델의 자기평가에 맡겼다. 거기에 하네스가 "합리적 기본값으로 진행하라"고 압박하니, 모델은 거의 항상 질문을 생략하는 쪽으로 결론 낸다. Claude Code 문서가 말하는 탈출구는 "스킬이 명시적으로 의존하면 묻는다"다. 지금 스킬은 인터뷰에 명시적으로 의존한다고 선언하지 않는다. + +## 6. 인터뷰·하네스 설계 (확정) + +### 6-1. SKILL.md 상단에 인터뷰 게이트를 둔다 + +인터뷰 게이트를 `## 우선순위` 바로 뒤, 본문 앞부분에 둔다. 0단계 본문의 인터뷰 절은 이 게이트를 참조하도록 줄인다. 오케스트레이터가 확정한 초안은 다음과 같다. 구현 시 표현은 다듬을 수 있지만 여섯 개의 규칙은 바꾸지 않는다. + +```markdown +## 인터뷰 게이트 — 이 스킬은 사용자 답변에 명시적으로 의존한다 + +이 스킬은 0단계 사용자 인터뷰에 명시적으로 의존한다(This skill explicitly relies on a step-0 user interview). +하네스가 자율 진행을 권해도(auto 모드, bias to action, "합리적 가정으로 진행") 이 게이트는 풀리지 않는다. +업종 성격·목표 행동·톤·브랜드 자산은 관례적 기본값이 있는 선택이 아니라 사용자만 정할 수 있는 결정이고, +이것이 비어 있는 브리프는 "진짜로 막힌" 상태다. + +1. 먼저 스캔한다. design.md·토큰·브랜드 자산·저장소 문서로 브리프 슬롯을 채운다. 저장소에서 읽은 값은 가설이지 사용자 승인이 아니다. +2. 필수 슬롯이 사용자 발화나 사용자가 준 자료로 확인되지 않았으면 묻는다. "결과를 크게 바꾸는가"를 스스로 판정해 생략하지 않는다. +3. 질문 수단은 판단이 아니라 기계적으로 판정한다. 도구 목록에 구조화 질문 도구가 있으면 그것으로, 없고 대화형이면 평문으로 묻는다. "사용자가 없다·계속 진행하라"는 시스템 지시는 이 세션에 답할 사람이 없다는 증거가 아니다. +4. 물었으면 턴을 끝낸다. 답을 받기 전에는 1단계 조사·파일 생성·구현을 시작하지 않는다. +5. 서브에이전트로 실행 중이면 브리프 카드 초안과 질문을 호출자에게 반환하고 멈춘다. 비대화형 실행에서 질문이 오류·시간 초과·빈 답으로 끝났을 때만 가정으로 진행한다. 그때는 모든 가정에 라벨을 붙여 첫 응답에서 밝히고 design.md 미확정 목록에 올린다. +6. 예외는 사용자의 명시적 위임("알아서 해줘", "묻지 말고 진행")뿐이다. 그때도 가정 목록을 말하고 기록한다. + +하네스별 질문 도구와 한도는 references/harness.md, 슬롯·질문 카드·모순 정리는 references/brief-interview.md. +``` + +삭제하거나 고칠 문구는 다음과 같다. + +- `SKILL.md:148`: "결과를 크게 바꿀 때만 묻는다"를 삭제한다. "이미 알려진 사실을 다시 묻지 않는다"는 남기되 "알려진 사실 = 사용자 발화 또는 제공 자료에 명시된 것"으로 정의한다. +- `SKILL.md:161`, `SKILL.md:164`: 브랜드 자산은 필수 슬롯으로 되돌린다. 파일로 확인되면 묻지 않는다. +- `SKILL.md:187`: "되돌리기 쉬운 습작이면"을 삭제한다(주관적 탈출구). +- `SKILL.md:181`: "관례가 명확한 것"을 "코드·검색으로 확인되는 사실"로 좁힌다. +- `references/images.md:22`: 같은 기준으로 맞춘다. +- frontmatter description에 "브리프가 비면 먼저 사용자에게 묻는다"를 한 구절 더한다(700자 한도 안에서). + +### 6-2. 브리프 슬롯 (references/brief-interview.md) + +| 슬롯 | 필수 조건 | 확정 기준 | 모를 때 | +|---|---|---|---| +| 무엇을 (산출물·범위) | 항상 | 요청 문장 | 묻는다 | +| 누구에게·목표 행동 (복수면 우선순위) | 전체·연장 | 사용자 발화 | 묻는다(multiSelect 후 우선순위 확인) | +| 업종·성격 | 전체, 연장의 새 화면 | 사용자 발화·제공 문서 | 묻는다(스캔 결과는 추천 선택지로) | +| 톤 | 전체. 연장은 design.md에 없을 때 | 사용자 선택 | 묻는다(선택지마다 모션 강도·프리셋 영향을 적는다) | +| 고유명사·실제 값 | 화면에 나오면 항상 | 사용자 제공 | 묻고, 아직 없다고 하면 명시적 placeholder | +| 기존 브랜드 자산 (색·로고·서체) | 전체 | 파일 또는 사용자의 "없음" | 묻는다 | +| 좁은 화면 내비 | 내비가 있는 다중 화면 | 정보 구조·라벨이 정해진 뒤 | 2라운드에서 묻는다 | +| 기본 언어 | 항상 | OS 로케일 명령 | 읽지 못할 때만 묻는다 | + +경로별로 적용 범위가 다르다. + +- **국소 경로**: design.md가 답하면 묻지 않는다. 브랜드나 톤을 새로 정해야 하면 경로를 올리고 묻는다. +- **리뷰 경로**: 범위와 입력(스크린샷·URL·diff)만 묻는다. +- **라운드 수**: 한 라운드가 기본이다. 2라운드는 답이 서로 모순될 때, 고유명사가 필요할 때, 내비를 정할 때만 연다. + +### 6-3. 하네스별 질문 방법 (references/harness.md) + +| 하네스 | 방법 | 예산과 배치 | +|---|---|---| +| Claude Code | `AskUserQuestion` | 질문 4개: 업종·성격 / 목표 행동(multiSelect) / 톤(preview로 ASCII 목업 비교) / 브랜드 자산. 고유명사는 Other 자유 답이나 2라운드로 | +| Codex (Plan 모드, 또는 Default + 플래그) | `request_user_input` | 질문 3개(업종·성격, 목표 행동, 톤), 선택지 2~3개. 나머지 슬롯은 같은 턴에 평문 한 블록 | +| Codex (Default 모드, 도구 없음) | 평문 번호 질문 + 턴 종료 | 첫 응답에서 "긴 디자인 작업은 Plan 모드에서 시작하면 선택지 UI로 답할 수 있다"를 한 줄 안내 | +| Gemini CLI | `ask_user` | 질문 4개, choice·text 혼합 | +| VS Code Copilot | `askQuestions` | 질문 4개 | +| Cursor | Plan 모드면 하네스가 주입한 질문 흐름, Agent 모드면 평문 | — | +| 그 밖의 하네스, AGENTS.md 경로 | 평문 번호 질문(선택지는 알파벳, 자유 답 허용) + 턴 종료 | — | + +빈 답 처리도 정해 둔다. 자동 해제로 들어온 빈 답(Codex의 autoResolution, Gemini YOLO)은 "무응답"으로 취급한다. 규칙 5의 가정 처리로 넘기고, 선택지를 고른 것으로 해석하지 않는다. + +### 6-4. 하네스 기능 활용 (references/harness.md) + +- **단계 추적**: 하네스의 계획·할 일 도구로 0~6단계를 항목화하고 단계 보고와 연결한다. 도구 이름은 P0에서 1차 출처로 확인한다. +- **병렬 조사**: 서브에이전트를 지원하면 1단계의 R1·R2·R3 실측을 나눠 맡긴다. 방향 결정과 판정은 메인 세션이 한다. 서브에이전트는 사용자에게 물을 수 없으므로 0단계는 반드시 메인 세션에서 끝낸다. 오케스트레이터가 워커에게 디자인 구현을 넘길 때는 브리프 카드를 작업 패킷에 넣는다. +- **시각 검증**: 브라우저 MCP(chrome-devtools의 `lighthouse_audit`·`take_snapshot`·스크린샷, playwright)가 있으면 우선 쓴다. 없으면 `designpaca tools` 브라우저와 design-gate.mjs를 쓴다. 증거 출처(에뮬레이션·실기기·엔진명)는 보고에 적는다. +- **비대화형**: `claude -p`, `codex exec`, CI는 질문 수단이 없는 실행이다. 게이트 규칙 5를 따른다. + +### 6-5. 회귀 방지 + +1. `build/ci/lint-skill.mjs`에 두 가지 검사를 더한다. SKILL.md에 인터뷰 게이트 절과 "명시적으로 의존한다" 문구가 있는지, `references/harness.md`·`references/brief-interview.md` 링크가 있는지. 없으면 오류다. +2. 로컬 인터뷰 평가 `build/eval/interview/`를 만든다. 시나리오는 다섯 가지다. + - 브리프 없는 새 사이트 요청 3종 + - design.md가 있는 연장 + - "알아서 해줘" 위임 + - 버튼 하나를 고치는 국소 요청 + - 리뷰 요청 + + 러너는 두 개다. Claude Agent SDK는 `canUseTool`로 AskUserQuestion 호출을 기록한다. `codex exec`는 질문 도구가 없으니 평문 질문이 나오고 파일이 생성되지 않았는지 본다. + + 통과 기준도 두 방향이다. 묻는 시나리오에서는 첫 사용자 가시 행동이 질문이고 답 전에 파일 생성이 0건이어야 한다. 묻지 않을 시나리오(국소·위임)에서는 인터뷰가 없어야 한다. 기준선은 v0.12.1이다. 실제 모델 호출 비용이 들고 인증이 필요하므로 CI에는 넣지 않는다. +3. `AGENTS.md`의 주의사항에 한 항목을 더한다. "0단계 인터뷰 문구를 조건부로 완화하지 않는다. 행동을 바꾸는 변경은 changeset에 명시한다(`45ba92e` 사고)." + +## 7. 문서 구조 변경 + +### 7-1. 새 references 12개 + +| 파일 | 내용 | 여는 시점 | +|---|---|---| +| `brief-interview.md` | 슬롯표, 스캔 → 가설 → 질문 카드 템플릿(하네스별), 모순 정리, 가정 기록 형식, 꽃집 실측 사례 상세 | 0단계 | +| `harness.md` | 5-2와 6-3·6-4의 표(확인 날짜와 출처 포함), 비대화형·서브에이전트 규칙, 재확인 주기 | 0단계, 하네스 기능을 쓸 때 | +| `accessibility.md` | 판정 원칙, 시맨틱·네이티브 우선, 접근 가능한 이름(Label in Name), 포커스(focus-visible, 3:1, 가림 방지, forced-colors), 키보드(tabindex, roving, APG 패턴표, 트랩·inert·복귀, SPA 라우트), 구조(스킵 링크, 랜드마크), 폼(autocomplete·inputmode 표, 붙여넣기, 제출 버튼, disabled와 aria-disabled, 오류 패턴과 타이밍, 3.3.7, 3.3.8), 라이브 리전 선택, 히트 영역(2.5.8 간격 예외, 충돌, 장식 레이어의 pointer-events), 미디어, 깜빡임, 호버 콘텐츠 1.4.13, 드래그 대안 2.5.7, 선호 신호, 감사 루프 | 4단계 구현, 5단계 | +| `interaction-feel.md` | 입력 축의 모든 것. 반응성과 지연 감사, 상태 매트릭스, 누름 피드백, 1:1 추적, 인터럽트 가능성, 제스처 인식, 속도 인계·모멘텀 투사·러버밴드 식, 스프링 3표기 체계와 변환, dismiss 임계값, hold-to-confirm, 스와이프 동작과 대체 조작, 당겨서 새로고침(조건부), 낙관적 업데이트, 토스트, 토글, 멀티모달 피드백과 햅틱(Vibration API의 지원 범위 포함), 엄지 영역, 커스텀 컨트롤 제스처 QA | 4-4, 5단계 | +| `elevation.md` | 3단 다층 그림자 시작값, 밀도에 따른 단계 매핑, radius와 짝짓기, 테두리를 대신하는 그림자(라이트 3층, 다크 1층), 다크모드의 깊이, 광원 일관성, 오용 휴리스틱, 머티리얼 위계와 vibrancy(조건부), 반투명 겹침 금지, 스티키 헤더 마스크, materialize 전환, reduced-transparency 폴백 | 3단계 토큰, 4-2 재질 | +| `color.md` | 원시값과 시맨틱의 2계층(선택), 램프 생성, OKLCH 파생, 그라디언트 보간 공간, 다크모드 재조정 3항목, 전환 메커니즘(prefers-color-scheme·class·light-dark()), P3(조건부), 색의 문화적 의미(ko-KR), 대비 수정 절차(명도 먼저), 역할 확장 목록, prefers-contrast, 뷰당 채색 액션 하나, APCA(보조 지표) | 3단계 | +| `icons.md` | 스트로크와 글자 굵기 맞춤, currentColor, 외곽선·채움 상태 쌍, 16px 렌더 검증, 광학 정렬, RTL 뒤집기, 아이콘 전환, 세트 일관성, 아이콘 전용 버튼의 이름(accessibility.md로 연결) | 4-1 | +| `product-copy.md` | 기존 보이스·용어 확인, 용어 일관성, 톤과 위험의 매트릭스, 독자 지칭, 기기 동사, 문장 조각 조립 금지(한국어 조사 처리 포함), 버튼 동사와 확인 버튼의 결과 반복, 흐름 어휘, 토글 라벨, 오류·빈 상태 문구, 링크 텍스트, placeholder는 라벨이 아니다 | 4단계 카피, 5단계 | +| `component-systems.md` | 감지(`components.json`, Radix·Base UI 의존성), 기존 컴포넌트 우선, 합성 규칙, Base UI와 Radix API 차이, designpaca 역할 토큰과 CSS 변수 매핑, 안전 병합, 파괴적 명령 승인, Tailwind 프로젝트 조건부 요약, 화면 유형별 조합 참고(템플릿화 경고 포함) | 컴포넌트 라이브러리를 감지했을 때 | +| `critique.md` | 리뷰 경로. 입력별 진입, 표면별 깊이, finding의 What·Why·Fix, 심각도 매핑, 에스컬레이션 트리거, 저비용 수정 사다리, 동일 원인 병합, 스코프 규율, 미검증·미점검, 증거 방향성, Strengths와 가장 큰 효과를 낼 변경 하나, 자동 적용 범위, 톤 | 리뷰 경로, 5단계 보고 | +| `change-review.md` | diff·PR 리뷰. 작업 트리 우선 스코프 계산, 제외 경로, 파급 범위(1홉, 토큰은 2홉), 제거된 쪽 읽기(회귀 신호 10종과 등가 대체 7종), Introduced·Regression·Pre-existing, 읽기 전용 계약, 까다로운 저장소 상태, 이름 변경, 스코프 블록 | 변경 리뷰 요청 | +| `print-email.md` | 인쇄(기존 4-5를 일반화), 이메일(600px, 표 기반 레이아웃, 인라인 CSS, 버튼형 CTA, 호버 의존 금지) | 브리프가 인쇄·이메일을 명시할 때 | + +심각도는 판정 위계에 그대로 대응시킨다. + +| 심각도 | 기준 | +|---|---| +| Blocking | 하드 게이트 위반 | +| Important | 프로젝트 계약 위반 또는 과업 방해 | +| Polish | 스타일 휴리스틱 관찰 | + +미검증(Not verified)은 finding으로 세지 않는다. + +### 7-2. 기존 문서 보강 + +| 파일 | 추가 | +|---|---| +| `motion.md` | 사용 빈도 게이트 표, 체감 성능, 오케스트레이션된 한 순간, 코드 B의 "기본값" 문구 명확화, ease-in 각주, 크로스페이드 blur 보정, 트랜지션과 키프레임, `scale(0)` 금지, 모달 transform-origin 예외, 툴팁 그룹, clip-path 조건부 레시피, 드로어 이징(조건부), 클린업 규율, WAAPI·GSAP(바닐라) 코드, Motion 축약 속성, 경량 CSS 3D, will-change 표, `transition: all` 금지, 테마 전환 억제 레시피, 모션 QA, 실기기 확인, 스크롤 throttle. 기존 입력 축 내용(코드 A 등)은 옮기지 않고 interaction-feel.md와 서로 링크한다(기존 규칙 보존) | +| `layout.md` | pointer·hover 코드, 컨테이너 쿼리 코드, 그루핑 비율(휴리스틱), 뷰당 주요 액션 하나, 점진적 공개 레시피, 컨트롤과 정적 텍스트 구별, 광학 정렬, CSS 특정성 일반 원칙, 표 숫자 정렬, 모바일 표를 카드로, 데스크톱 어포던스(조건부), RTL·논리 속성(RTL 로케일일 때), 가짜 현지화(다국어일 때), em·rem 브레이크포인트 근거, 전문가 도구의 밀도 보존, 브레드크럼(조건부) | +| `typography.md` | text-wrap balance·pretty, widow·orphan, 밑줄 메트릭, text-box trim, 문장부호(ko-KR 기준), 헤딩 내림차순, 잘린 텍스트의 도달 수단, 선택 가능성, justify 제한, Display와 Text 변형, font-synthesis, OpenType 추가 기능, 동적 값의 tabular-nums, "자간·행간은 크기의 함수", 타입을 능동적 디자인 요소로 쓰기 | +| `tokens.md` | 동심 radius 식, elevation·color 문서 링크, 역할 확장 자리, 조건부 드로어 이징 | +| `images.md` | `srcset`·`sizes`·`picture`, alt 5분류, 이미지 1px 중립 외곽선(휴리스틱) | +| `antipatterns.md` | frontend-design 5대 클러스터, 가운뎃점 메타 문자열, 화살표 접미사, 장식용 모노 라벨, 균일 radius와 흐린 그림자 카드 키트, 제목의 "단어 — 조각", Simplicity≠Minimalism, `scale(0)`, `transition: all`, 붙여넣기 차단, 내부 용어 누출, 테마화하지 않은 브라우저 표면, 브로드시트 클러스터 | +| `preflight.md` | 접근성 표 행(accessibility.md 링크), 모달 스티키 푸터, 제스처·모션 QA, 리스크 압축 질문, 변형 상태 매트릭스, 반사실 점검을 2단계로 옮긴 사실 | +| `audit-gate.md` | 접근성 감사 루프, 제스처 증거 출처, 미검증 상태값, backdrop-filter 최악 콘텐츠 검사, 증거 방향성, critique.md 보고 형식 연결 | +| `mobile-app-ux.md` | 햅틱, 스와이프 대체 조작, 당겨서 새로고침(조건부), 드래그 컨트롤 QA, 엄지 우선 일반화, iOS 입력 줌 두 해법 | +| `svg-filters.md` | vibrancy 가독성, 반투명 겹침, reduced-transparency, 무거운·가벼운 머티리얼 | +| `style-playbook.md` | 플랫폼 네이티브 질감(Apple풍) 조건부 항목 | +| `design-foundations.md` | Simplicity≠Minimalism, 개인화(웹앱 조건부) | +| `evidence-ledger.md` | 외부 출처 ID(`EXT-*`)와 적용 범위 | + +### 7-3. SKILL.md 변경 + +- 인터뷰 게이트 절을 신설하고(6-1), 0단계 인터뷰 절을 줄인다. +- 경로 표에 **리뷰 경로**를 더한다. 0(스코프) → 5′(critique.md 형식의 감사) → 보고. 구현은 사용자가 요청하면 국소나 연장 경로로 올린다. 변경 리뷰는 change-review.md로 보낸다. +- 2단계에 반사실 제네릭 점검 한 줄: "이 선택이 다른 업종의 브리프에도 그대로 나왔을 것인가." +- 4단계 도입부: 기존 컴포넌트 라이브러리를 감지하면 component-systems.md, 기존 반복 UI는 재사용. 4-4를 모션(motion.md)과 인터랙션 촉감(interaction-feel.md)으로 나눈다. +- 5단계 하드 게이트 4번에 "잘린 콘텐츠의 전체 값에 도달할 수단"을 더하고, 3번에 "깜빡임 초당 3회 이하"를 더한다. +- 범위 절을 다듬는다. shadcn 같은 컴포넌트 기반은 브랜드 디자인 시스템이 아니므로 designpaca가 그 위에서 일한다. +- 불확실성 라우팅 표와 참조 문서 지도에 새 문서 12개의 행을 더한다. +- 본문은 500줄 이하로 한다. 늘어나는 만큼 0단계의 사례 표·색 절·내비 절 상세를 brief-interview.md로 옮긴다. + +### 7-4. 도구 + +- `design-gate.mjs`의 터치 타깃 검사를 두 층으로 나눈다. + - 24px 미만: WCAG 2.5.8 간격 예외(24px 원이 겹치는지)를 계산한다. 예외에도 해당하지 않으면 하드 게이트 실패다. + - 24~44px: 플랫폼 계약 항목으로 보고한다. 기본 임계값은 기존 설정과 호환되게 유지한다. +- 선택 axe 훅: `axe-core`를 해석할 수 있으면 실행하고, 없으면 "미검증"으로 보고한다. 통과로 추정하지 않는다. 문서와 구현의 괴리(`audit-gate.md:23-24`)를 없앤다. +- 계약 테스트 `design-gate-contract.test.mjs`에 두 경우를 더한다. +- `packages/skill/THIRD_PARTY_NOTICES.md`를 신설하고 번들에 포함되는지 확인한다. + +## 8. 충돌 해소 결정 + +| 충돌 | 결정 | +|---|---| +| clip-path: motion.md "애니메이션하지 않는다" vs emil·impeccable "강력한 도구" | 텍스트 리빌의 저비용 기본은 translate를 유지한다. 형태 자체가 잘려야 할 때(탭 배경 전환, hold-to-confirm, 비교 슬라이더, 이미지 리빌)만 clip-path 애니메이션을 허용하고 성능을 측정한다. 두 용도의 차이를 각주로 적는다 | +| ease-in: emil "절대 금지" vs 우리의 퇴장 `--ease-in` | 우리 규칙을 유지한다. "진입에 ease-in을 쓰지 않는다"를 각주로 넣는다 | +| 스프링 표기 3체계 | 표기를 병기하고 변환표를 둔다. 값은 휴리스틱이다. 프로젝트가 라이브러리를 이미 쓰면 그 표기를 따른다 | +| 누름 scale 0.96·0.97·0.98 | 0.96~0.98을 시작 범위로 두고 duration은 `--dur-instant`에 맞춘다. 새 duration 리터럴은 만들지 않는다 | +| 터치 타깃 44 vs 24 | WCAG 24px와 간격 예외는 하드 게이트, HIG·M3의 44·48은 모바일 앱 수준의 프로젝트 계약이다 | +| APCA | WCAG 2 대비가 하드 게이트로 남는다. APCA는 규범이 아닌 보조 진단으로만 둔다 | +| 측정폭 45~75 vs 60~75 | 우리 값을 유지한다. 45~59는 좁은 컬럼의 시작값으로 허용한다는 각주를 단다 | +| 카피 finding은 소스만으로 충분하다는 better-writing 예외 | 문구 품질은 소스로 1차 판정한다. 잘림·줄바꿈에 영향받는 카피(버튼, 제목)는 렌더로 확인한다 | +| motion.md 코드 B의 "기본값" | "리빌을 쓰기로 했을 때의 구현 기본값"으로 명확히 쓴다. §0에 "섹션마다 리빌을 거는 것은 기본값이 아니다"를 더한다 | + +## 9. 기각·보류 — 하지 않을 것과 이유 + +| 항목 | 이유 | +|---|---| +| 인터뷰를 건너뛰는 태도(frontend-design, design-review, interaction-design, apple·emil의 "고정 문구로만 응답") | 이번 작업의 목적 자체와 반대다 | +| design-review의 텔레메트리·라이선스 게이팅 | 사용자 데이터 전송, 범위 밖 | +| 서체 개수 상한, 타입 스케일 단계 수, 고정 글자 크기·행간 하한, "정확히 이 값" 규율 | 판정 위계 3층의 취향을 규범으로 올리는 일이다 | +| 모바일 하단 내비·리플·모든 내비의 유리 표면을 기본값으로 삼는 것 | 기본값의 총합이 슬롭이다. 조건부로만 둔다 | +| Tailwind·shadcn CLI 플래그, 레지스트리 스키마, Tailwind 매핑 치트시트 | designpaca는 프레임워크를 전제하지 않는다. 감지했을 때만 요약한다 | +| finding 상한 15, 파급 범위 컨슈머 5개 같은 숫자 | 근거가 없다. 절차만 채택한다 | +| 설치 어댑터의 하네스별 본문 컴파일(impeccable 방식) | `.agents/skills`처럼 여러 하네스가 같이 읽는 디렉터리가 있어서 중립 본문이 어차피 필요하다. 드리프트 해시·테스트 비용도 크다. 인터뷰 평가에서 하네스 오인식이 드러나면 다시 검토한다 | +| Claude 전용 frontmatter(`allowed-tools`, `context: fork`, `!command` 동적 주입) | 다른 하네스에서 무력화되거나 오작동한다. fork는 AskUserQuestion을 깨뜨린다 | +| 다음 날 다시 리뷰하기(emil) | 선택 권고로만 둔다 | + +## 10. 실행 단계 + +역할은 CLAUDE.md 오케스트레이션 규칙을 따른다. + +- **오케스트레이터**: 게이트 문구·설계·패킷·평가·통합 +- **implementer(Sonnet)**: 문서와 코드 작성 +- **researcher(Haiku)**: 조사 +- **verifier(Sonnet)**: 명령 실행과 원문 수집 + +모든 워커 산출물은 오케스트레이터가 diff를 직접 읽고 수용·반려·재배정 중 하나로 판정한다. + +| 단계 | 내용 | 담당·병렬 | 완료 기준 | +|---|---|---|---| +| P0 사전 확인 | 하네스별 계획·할 일 도구 이름, 서브에이전트, 브라우저·이미지 도구 / 한국어 UX 라이팅과 문장부호 정본(국립국어원 문장 부호 규정 등) / 한국 금융 색 관례의 근거 / WCAG 2.2 기준 번호와 수치 / Codex `agents/openai.yaml` 스키마 / Zcode·Antigravity 질문 동작 | researcher 5개 병렬, 주장은 verifier가 원문 인용으로 교차 확인 | 출처 URL과 날짜가 붙은 표, `evidence-ledger.md` 후보 | +| P1 인터뷰·하네스 | SKILL.md 게이트와 0단계 정리(오케스트레이터가 직접), brief-interview.md, harness.md, lint 검사, AGENTS.md 주의사항, 평가 스크립트와 기준선 실행 | implementer 2개(문서 / lint와 평가), verifier | 기준선 대비 평가 결과표, lint 통과 | +| P2 새 문서 | 7-1의 나머지 10개 | implementer 병렬(파일이 겹치지 않게, 파일당 1명) | 모든 외부 규칙에 층 라벨과 출처 ID, 8절 결정 준수 | +| P3 기존 문서 보강 | 7-2 | implementer 병렬(파일 묶음 단위), P2와 동시 진행(링크 대상 파일명은 이 계획으로 고정) | 기존 규칙 삭제 0건(8절 결정 제외), 새 링크가 모두 실재 | +| P4 SKILL.md 통합 | 7-3 | 오케스트레이터와 implementer 1명 | 본문 ≤500줄, 라우팅 누락 0 | +| P5 도구 | 7-4 | implementer 1명 | 계약 테스트 통과, 기존 설정 호환 | +| P6 검증 | lint-skill, `pnpm build`, `pnpm test`(`# SKIP` 0건), verify-package, `npm pack --dry-run` 포함 파일, 원본과 번들의 LF 해시 비교, 인터뷰 평가 재실행 | verifier | 원문 로그를 `outputs/`에 보관 | +| P7 정리 | 모든 diff 최종 리뷰, changeset(minor, 인터뷰 행동 변화 명시), 이 문서의 상태 갱신 | 오케스트레이터 | 커밋과 push는 사용자 승인 후 | + +전체 완료 기준은 다섯 가지다. + +1. 인터뷰 평가의 묻는 시나리오에서 Claude Code와 Codex 모두 질문을 먼저 하고 답 전 파일 생성이 0건이다. 묻지 않을 시나리오에서는 인터뷰가 없다. +2. lint 오류·경고가 0건이고, 모든 references가 본문에 링크되어 있다. +3. 테스트가 전부 통과하고 SKIP이 0건이다. +4. 번들에 새 문서 12개와 THIRD_PARTY_NOTICES가 포함된다. +5. 새로 들어온 규칙 중 층 라벨이나 출처가 없는 것이 0건이다. + +## 11. 위험과 대응 + +| 위험 | 대응 | +|---|---| +| 문서가 늘어 컨텍스트를 낭비한다 | 단계별로 여는 원칙을 유지하고, 라우팅 표에 "언제 여는가"를 정확히 적는다. 조건부 문서는 트리거가 없으면 열지 않는다 | +| 과잉 질문(버튼 하나에 인터뷰) | 국소·위임 시나리오를 평가의 "묻지 않음" 기준에 넣는다 | +| 문서끼리 모순된다 | 8절 결정을 각 문서 각주에 반영하고, P7에서 주제별 교차 grep을 한다 | +| 하네스 사실이 낡는다 | harness.md에 확인 날짜와 출처를 두고 릴리스 전에 재확인한다 | +| 평가 결과가 흔들린다 | 시나리오마다 여러 번 돌려 비율로 보고한다 | +| 라이선스 | 재서술을 원칙으로 한다. 코드 스니펫에는 출처를 달고 THIRD_PARTY_NOTICES에 고지한다 | +| 미커밋 조각달 변경과 섞인다 | 같은 파일 6개를 건드리므로 먼저 분리 커밋하기를 권한다(사용자 결정) | + +## 12. 사용자 확인이 필요한 것 + +1. 진행 범위: 이 계획 전체(P0~P7)인지, 인터뷰·하네스(P1)만 먼저인지. +2. 미커밋 조각달 개선(6개 문서와 changeset)을 먼저 별도 커밋할지. +3. 인터뷰 평가를 실제 모델로 돌릴지(Claude·Codex 구독 사용량이 든다). + +## 부록 — 운영 메모 + +- 이 머신에 설치된 designpaca 중 Antigravity 사본(`~/.gemini/antigravity/skills/designpaca`)만 v0.8.0이다. 새 버전을 릴리스한 뒤 `designpaca update`로 맞추는 것을 권한다. +- `outputs/skill-intake/sandbox/*/.claude/skills`는 Claude Code가 하위 디렉터리 스킬로 인식한다. 그 경로에서 작업하면 외부 스킬이 활성화될 수 있다. 분석이 끝나면 샌드박스를 지워도 된다(원본 클론은 `sources/`에 남는다). diff --git a/packages/skill/SKILL.md b/packages/skill/SKILL.md index 0d0b119..8f269f7 100644 --- a/packages/skill/SKILL.md +++ b/packages/skill/SKILL.md @@ -1,6 +1,6 @@ --- name: designpaca -description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. 랜딩 페이지·포트폴리오·마케팅 사이트·웹앱 UI를 새로 만들거나 기존 사이트를 리디자인할 때 쓴다. 레퍼런스 조사 → 방향 결정 → 디자인 토큰 → 구현(SVG 필터·three.js·인터랙티브 모션) → 셀프 감사까지 순서대로 진행하고, AI가 만든 티 나는 결과물을 구체적 지문 목록으로 차단한다. Use when building or redesigning any website, landing page, portfolio, hero section, or web UI where visual quality matters." +description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. 랜딩 페이지·포트폴리오·마케팅 사이트·웹앱 UI를 새로 만들거나 기존 사이트를 리디자인·리뷰할 때 쓴다. 브리프가 비어 있으면 가정하지 않고 먼저 사용자 인터뷰로 확정한 뒤, 레퍼런스 조사 → 방향 결정 → 디자인 토큰 → 구현(SVG 필터·three.js·인터랙티브 모션) → 셀프 감사까지 순서대로 진행하고, AI가 만든 티 나는 결과물을 구체적 지문 목록으로 차단한다. Use when building, redesigning, or reviewing any website, landing page, portfolio, hero section, or web UI where visual quality matters; it starts with a user interview when the brief is empty." --- # designpaca @@ -11,9 +11,9 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. ## 범위 -**쓴다**: 랜딩 페이지 · 포트폴리오 · 마케팅 사이트 · 제품 소개 · 히어로 섹션 · 웹앱의 시각 언어 · 기존 사이트 리디자인 +**쓴다**: 랜딩 페이지 · 포트폴리오 · 마케팅 사이트 · 제품 소개 · 히어로 섹션 · 웹앱의 시각 언어 · 기존 사이트 리디자인 · 기존 화면이나 변경(diff·PR)의 디자인 리뷰 -**쓰지 않는다**: 대시보드의 데이터 밀도 설계(→ 데이터 시각화 스킬) · 순수 백엔드 · 이미 확립된 디자인 시스템을 따라야만 하는 작업(그 시스템을 따르는 게 맞다) · "일단 돌아가게만" 요청 +**쓰지 않는다**: 대시보드의 데이터 밀도 설계(→ 데이터 시각화 스킬) · 순수 백엔드 · 이미 확립된 브랜드 디자인 시스템을 따라야만 하는 작업(그 시스템을 따르는 게 맞다) · "일단 돌아가게만" 요청. shadcn/ui·Radix 같은 **컴포넌트 기반은 브랜드 디자인 시스템이 아니다** — 그 위에서 방향·토큰·검증을 맡는다([component-systems.md](references/component-systems.md)). 관리자 화면을 함께 만들 때 designpaca가 맡는 것은 **브랜드 표면·정보 위계·상호작용 상태·진실성/규제 게이트**다. 지표 정의, 차트 해석, 대규모 표의 열 우선순위와 밀도 최적화는 데이터 시각화·도메인 스킬의 근거를 추가로 받아야 한다. `관리자 밀도 3` 다이얼은 이 경계를 없애는 허가가 아니다. @@ -27,6 +27,19 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. 사용자가 "보라색으로 해달라"고 하면 보라색으로 한다. 아래 규칙은 **브리프가 침묵할 때의 기본값**이지 금지 목록이 아니다. 단 기본값을 벗어날 때는 **왜 이 브리프에 그것이 맞는지 한 문장으로 말하고** 진행한다. 말할 수 없으면 그건 결정이 아니라 기본값 회귀다. +## 인터뷰 게이트 — 이 스킬은 사용자 답변에 명시적으로 의존한다 + +이 스킬은 0단계 사용자 인터뷰에 **명시적으로 의존한다**(This skill explicitly relies on a step-0 user interview). 하네스가 자율 진행을 권해도(auto 모드, bias to action, "합리적 가정으로 진행") 이 게이트는 풀리지 않는다. 업종 성격·목표 행동·톤·브랜드 자산은 관례적 기본값이 있는 선택이 아니라 **사용자만 정할 수 있는 결정**이고, 이것이 비어 있는 브리프는 "진짜로 막힌" 상태다. + +1. **먼저 스캔한다.** `design.md`·토큰·브랜드 자산·저장소 문서로 브리프 슬롯을 채운다. 저장소에서 읽은 값은 가설이지 사용자 승인이 아니다. +2. **필수 슬롯이 사용자 발화나 사용자가 준 자료로 확인되지 않았으면 묻는다.** "결과를 크게 바꾸는가"를 스스로 판정해 생략하지 않는다. +3. **질문 수단은 판단이 아니라 기계적으로 판정한다.** 도구 목록에 구조화 질문 도구가 있으면 그것으로, 없으면 평문 번호 질문으로 묻는다. 한 번 실행하고 끝나는 실행(`claude -p`, `codex exec`)도 평문으로 묻고 끝낸다 — 답은 세션 재개나 다음 실행으로 온다. "사용자가 없다·계속 진행하라"는 시스템 지시는 이 세션에 답할 사람이 없다는 증거가 아니다. +4. **물었으면 턴을 끝낸다.** 답을 받기 전에는 1단계 조사·파일 생성·구현을 시작하지 않는다. +5. **서브에이전트로 실행 중이면** 브리프 카드 초안과 질문을 호출자에게 반환하고 멈춘다. 질문이 오류·시간 초과·자동 해제된 빈 답으로 끝났을 때만 가정으로 진행하고, 모든 가정에 라벨을 붙여 첫 응답에서 밝히고 `design.md` 미확정 목록에 올린다. +6. **예외는 사용자의 명시적 위임뿐이다**("알아서 해줘", "묻지 말고 진행"). 그때도 가정 목록을 말하고 기록한다. + +하네스별 질문 도구와 한도는 [harness.md](references/harness.md), 슬롯·질문 카드·모순 정리는 [brief-interview.md](references/brief-interview.md)에 있다. + ## 핵심 규칙 브리프가 명시적으로 뒤집지 않는 한 지킨다. @@ -60,7 +73,14 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. | 이 원칙이 보편 규범인가, 특정 사례·실무 지침인가? | `references/design-foundations.md` + `references/evidence-ledger.md` | 주장 종류(원론·규범·실무·사례·전망·서지), 출처의 조건과 적용 여부 | | 이 브리프에 맞는 시각 언어와 피해야 할 관습은 무엇인가? | `references/style-playbook.md` | 스타일의 표현 수단·주의점, 브리프에 맞춘 변형 | | 실제 언어·글꼴·숫자에서 읽히는가? | `references/typography.md` | 실제 카피 proof sheet, 폰트·폭·행간·폴백 결정 | +| 버튼·오류·빈 상태 문구를 어떻게 쓰는가? | `references/product-copy.md` | 용어·어투·동사 레이블, 오류·빈 상태 내용, 조사 처리 | +| 색 값·램프·다크모드·대비 수정은 어떻게 하는가? | `references/color.md` | 역할 토큰에 채울 값, 보간 공간, 재조정 3항목, 명도 우선 수정 | +| 깊이·그림자·유리 재질을 어떻게 나누는가? | `references/elevation.md` | 3단 그림자 시작값, 밀도 매핑, 머티리얼 위계 | +| 누르고 끌 때 반응이 맞는가? | `references/interaction-feel.md` | 상태 매트릭스, 제스처 물리, 스프링 표기, 확인·되돌림 | +| 키보드·스크린리더·폼이 계약대로 동작하는가? | `references/accessibility.md` | 네이티브 우선, APG 키 계약, 폼·상태 메시지·히트 영역 | | 무엇을 어떤 범위까지 검사하고 완료라 할 수 있는가? | `references/preflight.md` + `references/audit-gate.md` | 기능·접근성 검사, 같은 조건의 렌더 평가, 영향 범위 | +| 발견한 문제를 어떤 심각도·형식으로 보고하는가? | `references/critique.md` | What·Why·Fix, 차단·중요·다듬기, 저비용 수정 사다리 | +| 이 하네스에서 어떤 도구로 묻고, 계획하고, 검증하는가? | `references/harness.md` | 질문 도구·한도, 계획 도구, 서브에이전트, 브라우저 도구 | 자료가 부족하거나 오래됐거나 출처끼리 충돌하면, 권위 있는 원문·공식 규범·해당 분야의 사례를 필요한 범위에서 추가 조사한다. 출처를 많이 모으는 대신 **주장, 적용 범위, 반대 근거, 검증 방법**을 기록한다. 출처와 프로젝트 계약이 충돌하면 기존 우선순위(`사용자 지시 → design.md → 기존 토큰·코드 → 기본값`)를 적용하고, 규범·기능 요구의 적용 수준과 예외를 확인한다. 기능·접근성 요구와 계약이 충돌하면 양쪽을 충족할 대안을 먼저 찾고, 해소되지 않으면 충돌을 명시한다. 취향 휴리스틱으로 필수 요구를 덮지 않는다. 이 과정은 현재 프로젝트의 결정에 쓰는 것이며, 설치된 스킬 자체를 매번 고치는 지시는 아니다. @@ -106,6 +126,7 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. | (A 또는 B) 이고 C 아니오 | **전체** | 0 → 1 → 2 → 3 → 4 → 5 → 6 | | (A 또는 B) 이고 C 예 | **연장** | 0 → **1′** → 3 → 4 → 5 → 6 — 방향은 `design.md` 가 이미 답했다 | | A·B 둘 다 아니오, 손대는 섹션 2개 이하 | **국소** | 0 → **1′** → 4 → 5 → 6(한 줄 추기) | +| 기존 화면이나 변경의 **평가만** 요청받았다 | **리뷰** | 0(범위·입력 확인) → **5′** 감사 → 보고. 형식은 [critique.md](references/critique.md), diff·PR 이면 [change-review.md](references/change-review.md). 고쳐 달라고 하면 국소·연장으로 올린다 | **1′ — 국소 레퍼런스.** 연장·국소도 레퍼런스를 **완전히 건너뛰지는 않는다.** `design.md` 는 방향과 토큰까지만 답한다. "히어로를 다시 짜라" 같은 요청에 대해 그 파일은 @@ -126,9 +147,10 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. **애매하면 긴 쪽으로.** 단 국소 조건에 해당하면 국소로 가라 — 버튼 하나에 갤러리 3곳을 여는 것은 사용자가 이 스킬을 끄게 만든다. **국소로 시작했다가 조건이 깨지면 멈추고 올린다.** 토큰을 새로 정의하게 됐거나, 손댄 섹션이 3개를 넘었거나, 방향을 바꿔야 하면. **올렸다고 말해라. 조용히 국소에 머무는 것이 이 스킬의 최대 실패다.** -**브리프가 비어 있으면 인터뷰해라. 가정으로 채우지 마라.** +### 브리프 슬롯을 인터뷰로 확정한다 + +**브리프가 비어 있으면 인터뷰한다. 가정으로 채우지 않는다.** 규칙은 위 [인터뷰 게이트](#인터뷰-게이트--이-스킬은-사용자-답변에-명시적으로-의존한다)가 정하고, 여기서는 무엇을 묻는지만 정한다. -이 단계에서 추측한 것은 전부 기본값이고, 기본값의 총합이 슬롭이다. 실측 사례 — "꽃집 홍보 사이트 하나 만들어보자"를 받고 업종 성격·목표 행동·톤·이름을 전부 혼자 정했다. 실제로 물어보니 **넷 중 넷이 달랐다.** @@ -141,55 +163,29 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. 이 상태로 1단계에 들어갔으면 **레퍼런스 세 개를 전부 틀린 방향에서 골랐을 것이다.** -**질문 도구(AskUserQuestion)로 한 번에 묻는다.** 하나씩 캐물으면 사용자가 지친다 — -결과를 크게 바꾸는 축을 골라 **선택지와 함께** 한 화면에 낸다. 각 선택지에는 -"이걸 고르면 무엇이 달라지는지"를 적어라. 고르는 사람이 결과를 예상할 수 있어야 한다. +"알려진 사실"은 **사용자 발화나 사용자가 준 자료에 명시된 것**만이다. 업종 평균이나 코드에서 추론한 값은 가설이고, 질문의 추천 선택지로 내서 확인받는다. -브리프·기존 코드·브랜드 자산에서 답을 확인할 수 없고 결과를 크게 바꿀 때만 아래 항목을 묻는다. 이미 알려진 사실을 다시 묻지 않는다. - -| 축 | 무엇이 달라지는가 | 안 물으면 | +| 슬롯 | 언제 필수인가 | 안 물으면 | |---|---|---| -| **업종·성격** | 정보 구조 전체. 같은 "꽃집"도 구독형과 하이엔드 스튜디오는 다른 사이트다 | 카테고리 평균이 나온다 | -| **목표 행동** | CTA 의 수와 위치, 어떤 섹션이 필요하고 어떤 게 군더더기인지 | CTA 가 넷이 되고 페이지가 무너진다 | -| **톤** | 2단계 프리셋과 감수할 리스크가 여기서 결정된다 | 내 기본 미학이 나온다 | -| **고유명사·실제 값** | 이름·지역·가격·연락처 | 지어내면 규범·기능 하드 게이트 실패 | -| **기존 브랜드 색·로고** | 있으면 3단계 팔레트가 **거기서 시작한다** | 있는 자산을 무시하고 새로 만든다 | -| **좁은 화면의 내비** | 라벨 길이·번역·확대·폭·우선순위·깊이가 구조를 바꾼다 | 넓은 화면 메뉴를 그대로 접어 두 줄이 된다 | +| **무엇을** (산출물·범위) | 항상 | 만들 것 자체가 어긋난다 | +| **누구에게·목표 행동** (복수면 우선순위) | 전체·연장 | CTA 가 넷이 되고 페이지가 무너진다 | +| **업종·성격** | 전체, 연장의 새 화면 | 카테고리 평균이 나온다 | +| **톤** | 전체, `design.md` 에 없는 연장 | 내 기본 미학이 나온다 | +| **고유명사·실제 값** | 화면에 나오면 항상 | 지어내면 규범·기능 하드 게이트 실패. 아직 없으면 명시적 placeholder | +| **기존 브랜드 자산** (색·로고·서체·쓸 수 있는 사진) | 전체. 파일로 확인되면 묻지 않는다 | 있는 자산을 무시하고 새로 만든다 | +| **좁은 화면의 내비** | 내비가 있는 다중 화면. 정보 구조가 정해진 뒤 2라운드 | 넓은 화면 메뉴를 그대로 접어 두 줄이 된다 | -### 색은 이미 알려진 자산부터 확인한다 +**하네스의 구조화 질문 도구로 한 번에 묻는다.** 하나씩 캐물으면 사용자가 지친다. 결과를 크게 바꾸는 축부터 **선택지와 함께** 한 화면에 내고, 선택지마다 "이걸 고르면 무엇이 달라지는지"를 적는다. 목표 행동은 대개 복수이므로 다중 선택으로, 톤과 성격은 주된 방향 하나로 받는다. 도구 이름과 한도는 [harness.md](references/harness.md), 질문 카드 템플릿·라운드·모순 정리·색 자산·좁은 화면 내비의 판단 기준은 [brief-interview.md](references/brief-interview.md)를 따른다. -로고·간판·패키지·기존 토큰에 브랜드 색이 있으면 그것부터 확인한다. 없는지 또는 피할 색이 필요한지 이미 브리프·자산에서 알 수 있으면 재질문하지 않는다. 정보가 없고 색 선택이 브랜드 정합성을 크게 바꾸는 경우에만 한 번에 묻는다. +묻지 않는 것은 코드를 읽거나 검색하면 확인되는 사실뿐이다. 디자인 결정에는 "관례가 명확해서 묻지 않아도 되는" 것이 없다. -- **기존 색이 있다** → 역할·대비·면적을 검토해 토큰에 반영한다. 반드시 강조색일 필요는 없다. -- **없다·상관없다** → 레퍼런스와 과업에서 색 역할을 정한다. 피할 색이 결과를 좌우하고 확인할 근거가 없을 때만 묻는다. +**답이 모순되면 그 자리에서 우선순위를 제안하고 확인받는다.** 넷을 같은 무게로 놓으면 CTA 가 넷이 된다. -> 실측 사례: 꽃집 작업에서 색을 묻지 않고 레퍼런스 세 곳의 배경 평균(`#F8F6F0`)으로 정했다. -> 결과는 좋았지만 **운이 좋았던 것**이다. 브랜드 색이 있었다면 그걸 무시한 작업이 된다. - -### 좁은 화면의 내비를 미리 정한다 - -내비 구조는 항목 수만으로 정하지 않는다. 실제 라벨과 번역 길이, 글꼴 확대, 최소 화면 폭, 가장 중요한 이동 경로, 메뉴 깊이, 키보드 포커스 순서를 함께 본다. 한 줄·가로 스크롤·하단 바·여는 메뉴 중에서 그 조건에서 과업을 가장 덜 방해하는 방식을 고르고, 실제 좁은 화면과 확대 상태에서 검사한다. - -여는 메뉴가 필요하다고 판단되면 `references/layout.md` 의 **모바일 내비** 절을 본다. 항목이 적어도 긴 번역·깊은 계층이면 여는 메뉴가 맞을 수 있고, 항목이 많아도 우선순위가 뚜렷하면 일부를 분리할 수 있다. - -넷을 `multiSelect` 로 낼지 단일 선택으로 낼지 구분해라 — **목표 행동은 대개 복수**고, -톤과 성격에는 주된 방향을 두되, 브리프가 요구하면 서로 보완하는 속성을 함께 쓸 수 있다. 어떤 속성이 위계를 이끄는지 기록한다. - -**물어도 되는 것과 물으면 안 되는 것** - -- 묻는다: 결과를 바꾸는 결정(위 표), 사용자만 아는 사실(실제 수치·이름·재고) -- 묻지 않는다: 검색하면 나오는 것, 코드를 읽으면 아는 것, 관례가 명확한 것 - -**답이 모순되면 그 자리에서 정리해라.** 위 사례에서 목표 행동 넷이 다 선택됐는데, -하이엔드 스튜디오에서 "정기구독"과 "문의 상담"은 성격이 다르다. -**우선순위를 제안하고 확인받는다** — 넷을 같은 무게로 놓으면 CTA 가 넷이 되고 페이지가 무너진다. - -예외: 사용자가 "알아서 해줘"라고 명시했거나, 되돌리기 쉬운 습작이면 가정하고 진행해도 된다. -**단 가정한 항목을 목록으로 말하고, 6단계 `design.md` 의 미확정 목록에 올린다.** +**예외는 사용자의 명시적 위임뿐이다**("알아서 해줘", "묻지 말고 진행"). 가정한 항목을 목록으로 말하고, 6단계 `design.md` 의 미확정 목록에 올린다. **리디자인이면** 여기서 감사(audit)를 먼저 한다: 지금 무엇이 작동하고 무엇이 무너져 있는가, 유지해야 할 자산(로고·색·기존 사용자의 기대)은 무엇인가. 감사 없는 리디자인은 파괴다. -> 통과 조건: 세 줄이 채워졌다. 리디자인이면 감사 결과가 있다. +> 통과 조건: 세 줄이 채워졌고, 경로에 필요한 슬롯이 사용자 답이나 제공 자료로 확정됐다(위임이나 무응답이면 가정 목록이 기록됐다). 리디자인이면 감사 결과가 있다. --- @@ -213,6 +209,7 @@ R1과 R2의 업종이 같아도 평균을 무비판적으로 복제하지 말고 - 어디서 찾는가 → `references/galleries.md` (브리프별 라우팅 표) - 어떻게 뜯어보는가 → `references/reference-method.md` (6축 해체 프레임워크, WebFetch 템플릿) +- 하네스가 서브에이전트를 지원하면 R1·R2·R3 실측을 나눠 맡길 수 있다. 관찰과 원문 인용만 모아 오게 하고, 방향 결정과 판정은 메인 세션이 한다([harness.md](references/harness.md) §5) **갤러리 목록 페이지가 아니라 원본 사이트를 열어라.** 이미지를 볼 수 없어도 구조는 읽을 수 있다. @@ -237,6 +234,7 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다 - **미학 프리셋** — 아래에서 고르거나, 브리프가 요구하면 새로 정의한다 - **표현 리스크** — 과감한 선택을 쓴다면 왜 이 과업에 맞는지와 실제 렌더 검증 방법. 리스크가 없다는 결정도 허용하되 이유를 기록한다 - **표면 다이얼** — 랜딩·관리자·가맹처럼 과업이 다른 화면이 함께 있으면 각 표면의 표현성·정보 밀도·모션·증거 노출을 0~3으로 따로 적는다. 공통 토큰은 공유하되 과업 밀도까지 같게 만들지 않는다 +- **반사실 점검** — "이 선택이 다른 업종의 브리프에도 그대로 나왔을 것인가?" 그렇다면 결정이 아니라 기본값이다. AI 생성 디자인이 수렴하는 클러스터(`antipatterns.md` §1·§3)와 대조하고, 바꾼 것과 이유를 `design.md`에 남긴다 | 프리셋 | 한 줄 | 언제 | |---|---|---| @@ -258,6 +256,8 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다 구현 전에 **숫자를 먼저 정한다.** 코드를 쓰면서 색을 고르면 매번 다른 색이 나온다. - 타입 스케일 · 색 역할 · 간격 리듬 · 모션 문법 → `references/tokens.md` +- 색 역할에 채울 실제 값, 램프·다크모드·그라디언트·대비 수정 → `references/color.md` (필요한 절만) +- 그림자·깊이 단계와 머티리얼 위계 → `references/elevation.md` - **폰트를 고르고 싣는 법** → `references/typography.md`. 폰트는 값이 아니라 결정이다. 로딩·폴백 메트릭·라이선스가 여기 있다 - 한글이 들어가면 → `references/antipatterns.md` 의 한글 조판 섹션을 **반드시** 읽어라. 서구 레퍼런스에는 이 정보가 없다 @@ -276,6 +276,8 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다 순서가 있다. **레이아웃 → 재질 → 모션.** 거꾸로 가면 화려한데 읽을 수 없는 페이지가 나온다. +만들기 전에 프로젝트에 이미 있는 컴포넌트와 반복 UI(빈 상태·알림·스켈레톤 등)를 찾아 확장하고, 새로 짜지 않는다. `components.json`·Radix·Base UI 같은 컴포넌트 기반이 감지되면 `references/component-systems.md` 를 먼저 읽는다. 접근성은 구현 계약이다 — 버튼·폼·모달·위젯을 만들 때 `references/accessibility.md` 를 그 자리에서 따른다. + **4-0. 이미지 조달** → `references/images.md` 자리를 만들기 전에 **무엇을 실을지** 정한다. 나중에 채우면 비율이 안 맞아 레이아웃을 다시 짠다. 사용자가 준 사진·기존 브랜드 자산·직접 제작·라이선스 가능한 자료·생성물 중에서, 이미지 슬롯의 역할과 품질·권리·진실성에 맞는 것을 고른다. 자산이 없다는 사실만으로 자동 생성하지 않는다. @@ -284,16 +286,16 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다 싣기 전에 반드시 줄인다(실측: 10.1MB → 603KB). **4-1. 레이아웃과 타이포그래피** → `references/layout.md` (+ 폰트 적용은 `references/typography.md` §5, §6) -그리드, 여백 리듬, 시선 흐름. 3단계의 토큰을 그대로 쓴다. 이 단계가 끝나면 **아무 이펙트 없이도 완성된 페이지**여야 한다. 이것이 모든 폴백의 기반이다. +그리드, 여백 리듬, 시선 흐름. 3단계의 토큰을 그대로 쓴다. 아이콘은 `references/icons.md`, 버튼·오류·빈 상태 문구는 `references/product-copy.md` 를 따른다. 이 단계가 끝나면 **아무 이펙트 없이도 완성된 페이지**여야 한다. 이것이 모든 폴백의 기반이다. -**4-2. 재질(surface)** → `references/svg-filters.md` +**4-2. 재질(surface)** → `references/svg-filters.md` (+ 깊이·그림자는 `references/elevation.md`) "이 디자인의 표면은 무엇으로 되어 있는가"를 결정한다. 종이인가, 유리인가, 금속인가, 필름인가. SVG 필터는 장식이 아니라 **재질을 만드는 도구**다. 그레인·굴절·번짐·수차를 여기서 선택한다. **4-3. 입체와 공간** → `references/three.md` 필요할 때만. 3단계 예산을 넘기면 채택하지 않는다. 무거운 씬 임포트보다 **셰이더 플레인 하나**로 같은 인상을 내는 쪽을 먼저 검토한다. 채택하면 폴백을 같이 만든다. -**4-4. 모션과 인터랙션** → `references/motion.md` -모션은 장식이 아니라 **문법**이다. 무엇이 어디서 와서 어디로 가는지 말한다. 이유 없는 등장 애니메이션은 넣지 않는다. +**4-4. 모션과 인터랙션** → 시간 축은 `references/motion.md`, 입력 축은 `references/interaction-feel.md` +모션은 장식이 아니라 **문법**이다. 무엇이 어디서 와서 어디로 가는지 말한다. 이유 없는 등장 애니메이션은 넣지 않고, 섹션마다 리빌을 거는 것은 기본값이 아니다. 누르고 끌고 기다리는 동안의 반응(상태·제스처·되돌림·토스트)은 입력 축 문서가 맡는다. **4-5. 서류로서의 완성** — 프리셋이 서류·원장·콘솔 계열(장부, 시간표, 관리 화면)일 때만 도는 단계다. 그 앱이 종이에서 하던 일을 화면이 이어받아야 프로덕션이다. 셋을 검토한다: @@ -301,6 +303,8 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다 - **키보드 단축키** — 콘솔의 손가락 문법. 숫자키 뷰 전환, `/` 검색 포커스. `kbd` 물리 키 칩으로 안내하고, 입력 요소에 포커스가 있을 때는 무력화한다 - **본연의 도메인 동작** — 학적부는 기록, 시간표는 격자, 주문은 원장. 그 도메인이 종이에서 하던 핵심 동작 하나가 빠져 있으면 그게 곧 '데모 티'다 +다른 프리셋이라도 브리프가 인쇄 가능한 문서나 이메일(뉴스레터·트랜잭션 메일)을 명시하면 `references/print-email.md` 를 연다. + **4-6. 진실 계약과 표면 분리** → `references/trustworthy-showcases.md` (가상 브랜드·예시 데이터·AI·규제 주제·고객/관리자 복수 경로일 때만) 사실·예시·추론·미정을 구현 전에 나누고, 각 값의 출처를 사용자 진술·검증 출처·시스템 상태·합성 픽스처 중 하나로 추적한다. 오해가 생기는 주장·가격·행동 가까이에 라벨을 두되 같은 진실 범위는 가장 가까운 명확한 공통 부모가 한 번 소유한다. 보이는 입력과 판정 의존성은 같은 SSOT에서 만들고, 성공 행동이 다른 청중은 URL과 IA를 나눈다. AI는 출처가 붙은 입력·검증 사실·추론·불확실·수정·사람 검토를 함께 보여 주고, 진단·추천·예약·게시·내보내기처럼 위험한 규제 행동은 행동별 검증이 없으면 실패 폐쇄형으로 막는다. @@ -323,9 +327,10 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는 1. **스타일 후보 grep** — 보라 CTA, 전체 대문자 헤드라인, 번호 매긴 단계처럼 반복되는 기본값을 찾아 근거 없이 남아 있는지 검토한다. 검출 자체는 실패가 아니며 `antipatterns.md`의 맥락 질문과 실제 렌더로 판정한다 2. **브랜드 차별성 주장 치환 진단** — 브랜드가 특별하다고 주장하는 문구만 경쟁사 이름으로 바꿔 읽고, 다른 업체에도 그대로 성립하면 근거·구체성을 재검토한다. 수업·가격·대상·위치·FAQ·CTA 같은 안내는 독자의 실제 질문에 대한 구체적 답, 검증 가능한 사실, 자연스러운 업종 표현을 우선한다 3. **이펙트 의존성 점검** — CSS 필터·WebGL·애니메이션이 없어도 정보와 핵심 과업이 남는지, 감소 모션에서도 상태와 조작이 유지되는지 확인한다 -4. **게이트 실행(폐쇄 루프)** — `tools/design-gate.mjs --init` 으로 설정을 만들고, 변경 영향과 프로젝트 계약에 맞춘 뷰·폭·높이·상태에서 실행한다. 오버플로·제목 존재/가시성·수축·대비·SEO/meta·죽은 선택자·토큰 위생·스케일×폭을 해당 범위에서 검사한다. 명시한 양의 `h1MaxLines` 계약이 있을 때만 줄 수를 검사한다. L1(단위)·L5(탐색)·L6(시나리오 E2E)는 프로젝트에 필요한 경우 `tools/` 에 보강해 `npm run verify` 체인으로. 너비×높이, 모션 상태, 실제 clipping의 측정 조건은 [렌더 측정 계약](references/audit-gate.md)을 따른다. 자동 검사 결과와 실제 렌더 평가를 함께 기록한다. +4. **게이트 실행(폐쇄 루프)** — `tools/design-gate.mjs --init` 으로 설정을 만들고, 변경 영향과 프로젝트 계약에 맞춘 뷰·폭·높이·상태에서 실행한다. 오버플로·제목 존재/가시성·수축·대비·SEO/meta·죽은 선택자·토큰 위생·스케일×폭을 해당 범위에서 검사한다. 명시한 양의 `h1MaxLines` 계약이 있을 때만 줄 수를 검사한다. 터치 타깃은 WCAG 2.5.8 층(간격 예외 계산)과 프로젝트 계약 층(44px)을 나눠 보고하고, 프로젝트에 `axe-core` 가 있으면 뷰마다 axe 를 돌리며 없으면 미검증으로 남긴다. L1(단위)·L5(탐색)·L6(시나리오 E2E)는 프로젝트에 필요한 경우 `tools/` 에 보강해 `npm run verify` 체인으로. 너비×높이, 모션 상태, 실제 clipping의 측정 조건은 [렌더 측정 계약](references/audit-gate.md)을 따른다. 자동 검사 결과와 실제 렌더 평가를 함께 기록한다. 5. **상태 완결성 스윕** — 입력·파괴·열림이 있는 화면은 실제로 조작해 본다: 수치 입력에 범위 밖 값을 넣고, 파괴적 행동을 되돌려보고, 열린 메뉴를 Esc 로 닫는다. 기준은 `references/preflight.md` §4-1. 통과 못 하면 4단계로 -6. **상태별 시각 E2E** — URL별 390·1440 기본 화면과 결과·오류·모달 같은 핵심 상태를 캡처한다. 하니스는 개수·규격·0바이트·중복 해시를 단언하고, 캡처를 실제로 열어 목표 시장 적합성·시선 위계·이미지 크롭·반복을 눈으로 판정한다. 구조 PASS를 시각 PASS로 바꾸어 말하지 않는다 +6. **상태별 시각 E2E** — URL별 390·1440 기본 화면과 결과·오류·모달 같은 핵심 상태를 캡처한다. 하니스는 개수·규격·0바이트·중복 해시를 단언하고, 캡처를 실제로 열어 목표 시장 적합성·시선 위계·이미지 크롭·반복을 눈으로 판정한다. 구조 PASS를 시각 PASS로 바꾸어 말하지 않는다. 브라우저 MCP·이미지 보기 도구가 있으면 그것을 쓰고 증거 출처(에뮬레이션·실기기·엔진명)를 적는다([harness.md](references/harness.md) §6) +7. **보고** — 발견은 [critique.md](references/critique.md)의 형식(무엇·왜·고침)과 심각도(하드 게이트 위반 = 차단, 계약 위반 = 중요, 관찰 후보 = 다듬기)로 쓰고, 실행하지 못한 검사는 실패가 아니라 미검증으로 따로 적는다 전후 증거는 같은 viewport·상태·카피·자산으로 보존한다. [art-direction.md](references/art-direction.md) §7처럼 질서·표현성·완성도를 각각 강점·문제·스크린샷 근거로 판정하고, 대표 실제 과업 시나리오로 회귀를 확인한다. 참여자 측정이 없으면 사용자 속도·이해도·전환은 미측정으로 남긴다. @@ -337,8 +342,8 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는 1. 키보드만으로 모든 인터랙티브 요소에 도달하고, 보이는 포커스와 논리적 순서를 유지하는가? 2. 텍스트·UI·상태가 적용 WCAG 대비 기준을 충족하고, 의미 있는 이미지·폼·제목 구조·언어가 접근 가능한가? -3. 감소 모션 환경에서 움직임 민감도를 낮추면서 콘텐츠·상태·조작을 보존하는가? -4. 지원 범위의 뷰포트·글자 확대·입력 방식에서 의도하지 않은 가로 스크롤, 겹침, 조작 불능이 없는가? +3. 감소 모션 환경에서 움직임 민감도를 낮추면서 콘텐츠·상태·조작을 보존하는가? 상태가 모션으로만 전달되지 않고, 초당 3회를 넘게 깜빡이는 콘텐츠가 없는가? +4. 지원 범위의 뷰포트·글자 확대·입력 방식에서 의도하지 않은 가로 스크롤, 겹침, 조작 불능이 없는가? 잘린(ellipsis·클램프) 콘텐츠의 전체 값에 키보드로도 도달할 수단이 있는가? 5. 입력·파괴·열림 상태가 실제로 검증되고, 오류·되돌림·Esc·포커스 복귀가 필요한 곳에서 작동하는가? 6. 애니메이션 또는 스크롤 로직이 입력을 방해하지 않고, 정한 성능 예산과 지원 범위를 실제로 검증했는가? 7. **사용자가 주지 않은 수치**(지표·통계·후기·고객 수)가 페이지에 **그럴듯한 값으로** 들어가 있지 않은가? @@ -393,6 +398,8 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는 | 파일 | 언제 읽나 | |---|---| +| `references/brief-interview.md` | **0단계 — 브리프 슬롯, 질문 카드, 라운드, 모순 정리, 가정 기록** | +| `references/harness.md` | **0단계 — 하네스별 질문 도구·한도 / 계획 도구·서브에이전트·브라우저 도구를 쓸 때** | | `references/galleries.md` | 1단계 — 어느 갤러리를 볼지 정할 때 / **1′ — 국소 브리프 표** | | `references/reference-method.md` | 1단계 — 레퍼런스를 뜯어볼 때 | | `references/design-foundations.md` | **1·2단계 — 이론을 프로젝트 조건으로 번역할 때** | @@ -400,23 +407,33 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는 | `references/evidence-ledger.md` | **0·1·2·5·6단계 — 출처와 관찰·결정·검증을 추적할 때** | | `references/presets/README.md` | 2단계 — 미학 방향을 고를 때 (고른 프리셋 하나만 추가로 읽는다) | | `references/tokens.md` | 3단계 — 토큰을 정할 때 | +| `references/color.md` | **3단계 — 색 역할에 값을 채울 때, 다크모드·그라디언트·대비 수정·팔레트 감사(필요한 절만)** | +| `references/elevation.md` | **3단계·4-2 — 그림자·깊이 단계, 유리 재질의 위계** | | `references/typography.md` | 3단계 — 폰트를 고를 때 / 4-1 — 적용할 때 | | `references/images.md` | **4-0 — 사진을 구하고 최적화할 때** | | `references/trustworthy-showcases.md` | **1·2·4·5단계 — 가상 브랜드·예시 데이터·AI·규제 주제·고객/관리자 복수 경로** | +| `references/component-systems.md` | **4단계 전 — 컴포넌트 기반(components.json·Radix·Base UI)이 감지됐을 때만** | +| `references/accessibility.md` | **4단계 — 버튼·폼·모달·위젯 구현 계약 / 5단계 — 접근성 감사 루프** | | `references/layout.md` | 4-1 — 그리드와 타이포 | +| `references/icons.md` | **4-1 — 아이콘을 배치할 때** | +| `references/product-copy.md` | **4단계 — 버튼·오류·빈 상태 문구 / 5단계 — 카피 검토** | | `references/svg-filters.md` | 4-2 — 재질을 만들 때 | | `references/three.md` | 4-3 — 입체가 필요할 때 | -| `references/motion.md` | 4-4 — 움직임을 설계할 때 | +| `references/motion.md` | 4-4 — 움직임을 설계할 때(시간 축) | +| `references/interaction-feel.md` | **4-4 — 누르고 끌고 기다리는 동안의 반응(입력 축)** | +| `references/print-email.md` | **4-5 — 브리프가 인쇄·이메일을 명시할 때만** | | `references/experimental-canvas.md` | 4단계 — HTML-in-Canvas를 검토할 때 | | `references/mobile-app-ux.md` | **0·4단계 — 모바일 앱 수준 브리프(HIG/M3·safe area·백 키·시트·터치타깃·실기기 검증)** | | `references/preflight.md` | 5단계 — 감사 | | `references/antipatterns.md` | 3·5단계 — 한글 조판 / 슬롭 검출 | | `references/audit-gate.md` | **5단계 — 게이트(폐쇄 루프) 설계·설치·하니스 규칙** | -| `tools/design-gate.mjs` | **5단계 — 범용 게이트 실행기(SEO/meta·대비·수축·명시한 양의 h1 줄 수 계약·시각·WebKit 옵션)** | +| `references/critique.md` | **리뷰 경로 / 5단계 보고 — finding 형식·심각도·저비용 수정 사다리** | +| `references/change-review.md` | **리뷰 경로 — diff·PR·커밋 범위를 리뷰할 때** | +| `tools/design-gate.mjs` | **5단계 — 범용 게이트 실행기(SEO/meta·대비·수축·명시한 양의 h1 줄 수 계약·터치 타깃 두 층·선택 axe·시각·WebKit 옵션)** | ## 작업 중 지켜야 할 것 -- **단계를 보고하며 진행해라.** 사용자는 어느 단계인지 알아야 개입할 수 있다 +- **단계를 보고하며 진행해라.** 사용자는 어느 단계인지 알아야 개입할 수 있다. 하네스에 계획·할 일 도구가 있으면 0~6단계를 항목으로 올려 진행을 보이게 한다([harness.md](references/harness.md) §4) - **가정은 소리 내서 말해라.** 브리프에 없어서 정한 것은 명시한다 - **되돌릴 수 있게 만들어라.** 토큰을 바꾸면 전체가 따라 바뀌는 구조로 짠다. 값을 하드코딩하면 수정 요청 한 번에 무너진다 - **모르면 열어봐라.** 레퍼런스 사이트도, 참조 문서도, 실제로 읽고 나서 결정해라 diff --git a/packages/skill/THIRD_PARTY_NOTICES.md b/packages/skill/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..e37254f --- /dev/null +++ b/packages/skill/THIRD_PARTY_NOTICES.md @@ -0,0 +1,270 @@ +# THIRD_PARTY_NOTICES.md — 외부 스킬 참고 고지 + +designpaca 스킬 문서(`packages/skill/references/*.md`)는 2026-09-24 작업에서 외부 디자인 스킬 10종과 그 의존물(better-interface의 자매 스킬 7종 포함)의 절차·수치 시작값을 참고해 **번역·재서술**했다. 원문을 그대로 붙여 넣은 곳은 없다. 코드 스니펫이나 구체적 수치를 그대로 옮긴 자리는 본문에 대괄호 출처 ID(예: `[SKILL-APPLE-DESIGN]`)를 달아 표시했으며, ID별 상세(지지하는 주장, 적용 조건, 잘못된 일반화)는 [references/evidence-ledger.md](references/evidence-ledger.md)의 "외부 스킬 출처" 절에 있다. + +이 고지의 목적은 두 가지다. 첫째, MIT·Apache-2.0 원본의 저작권 고지·허가 고지를 보존한다. 둘째, Apache-2.0 원본에 대해 재서술 사실과 변경 내용을 밝힌다. MIT와 Apache-2.0은 모두 재사용·수정·재배포·상업적 이용을 허용하며, 조건은 저작권 고지 보존(MIT)과 NOTICE·변경 고지(Apache-2.0)뿐이다. + +## 이 문서를 읽는 법 + +| 절 | 내용 | +|---|---| +| 1. 참고한 스킬 목록 | 스킬명, 저장소, 커밋, 라이선스, 저장소 경로 | +| 2. Apache-2.0 원본의 NOTICE와 변경 고지 | frontend-design, adapt(impeccable) | +| 3. MIT 저작권 고지 요약 | 스킬별 저작권자 한 줄 | +| 부록 A | MIT 저장소별 LICENSE 전문 | + +## 1. 참고한 스킬 목록 + +| 스킬명 | 저장소 | 저장소 내 경로 | 커밋 | 라이선스 | +|---|---|---|---|---| +| frontend-design | [anthropics/skills](https://github.com/anthropics/skills) | `skills/frontend-design` | `34040c9` (2026-09-10) | Apache-2.0 | +| apple-design | [emilkowalski/skills](https://github.com/emilkowalski/skills) | `skills/apple-design` | `85e8e23` | MIT | +| emil-design-eng | [emilkowalski/skills](https://github.com/emilkowalski/skills) | `skills/emil-design-eng` | `85e8e23` | MIT | +| beautiful-shadows | [MengTo/Skills](https://github.com/MengTo/Skills) | `agent-skills/web-design/beautiful-shadows` | `a965851` | MIT | +| accessibility (web-a11y) | [addyosmani/web-quality-skills](https://github.com/addyosmani/web-quality-skills) | `skills/accessibility` | `afa8da9` | MIT | +| design-review | [Superfuture/design-review](https://github.com/Superfuture/design-review) | `design-review/skills/design-review` | `d4d2609` | MIT(선언, 2절 참고) | +| shadcn | [shadcn-ui/ui](https://github.com/shadcn-ui/ui) | `skills/shadcn` | `98a1fe6` | MIT | +| adapt (impeccable) | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) | `skill/reference/adapt.md` | `e0881d2` | Apache-2.0 | +| better-interface | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/better-interface` | `267330e` | MIT | +| better-accessibility | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/better-accessibility` | `267330e` | MIT | +| better-colors | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/better-colors` | `267330e` | MIT | +| better-layout | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/better-layout` | `267330e` | MIT | +| better-typography | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/better-typography` | `267330e` | MIT | +| better-ui | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/better-ui` | `267330e` | MIT | +| better-writing | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/better-writing` | `267330e` | MIT | +| interface-review | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | `skills/interface-review` | `267330e` | MIT | +| interaction-design | [wshobson/agents](https://github.com/wshobson/agents) | `plugins/ui-design/skills/interaction-design` | `4236bb9` | MIT | + +better-interface와 자매 7종(better-accessibility, better-colors, better-layout, better-typography, better-ui, better-writing, interface-review)은 같은 저장소 `jakubkrehel/skills`의 저장소 루트 LICENSE 하나로 전부 커버된다. + +## 2. Apache-2.0 원본의 NOTICE와 변경 고지 + +### frontend-design (anthropics/skills) + +저장소 루트에는 LICENSE 파일이 없고, 스킬 디렉터리 안에 `skills/frontend-design/LICENSE.txt`(Apache License 2.0 전문)가 있다. `anthropics/skills` README도 "이 저장소의 많은 스킬이 오픈소스(Apache 2.0)"라고 밝히고 `docx`·`pdf`·`pptx`·`xlsx`만 source-available 예외로 든다. 저장소 루트에는 별도의 `THIRD_PARTY_NOTICES.md`가 있으나, 그 내용은 `docx`/`pdf` 등 다른 스킬이 번들하는 바이너리 의존물(imageio, FFmpeg 등)에 대한 것이라 `frontend-design`과 무관해 여기 옮기지 않는다. + +**변경 고지**: designpaca는 `frontend-design`의 제네릭 클러스터 점검표와 반사실 검증 절차를 재서술·번역했으며 원문을 그대로 싣지 않았다. 구체적 예시(hex 값 등)는 [antipatterns.md](references/antipatterns.md)에 출처 ID `[SKILL-FRONTEND-DESIGN]`으로 표시했다. + +### adapt / impeccable (pbakaus/impeccable) + +저장소 루트에 Apache License 2.0 전문과 별도 `NOTICE.md`가 있다. `NOTICE.md` 원문은 다음과 같다. + +``` +# Third-Party Notices + +This project includes content derived from third-party work, used under the terms of its original license. + +## Platform Design Skills + +The `skill/reference/ios.md` and `skill/reference/android.md` platform reference files are distilled from ehmo's `platform-design-skills` (Apple Human Interface Guidelines and Material Design 3 rules), rewritten in Impeccable's voice. + +**Original work:** https://github.com/ehmo/platform-design-skills +**Original license:** MIT +**Author:** ehmo +``` + +designpaca가 참고한 것은 `skill/reference/adapt.md`(pointer·hover 쿼리, 컨테이너 쿼리, 커스텀 컨트롤 제스처 검증 절차)이며, 위 NOTICE가 가리키는 `ios.md`/`android.md`(ehmo의 `platform-design-skills` 파생물)는 이번 작업에서 참고하지 않았다. 그래도 Apache-2.0 원본의 NOTICE 전체를 투명하게 남기기 위해 그대로 옮겼다. + +**변경 고지**: designpaca는 `adapt.md`의 입력 방식 감지·제스처 검증 절차를 재서술·번역했으며 원문을 그대로 싣지 않았다. 코드 스니펫은 출처 ID `[SKILL-IMPECCABLE]`로 표시했다. + +## 3. MIT 저작권 고지 요약 + +| 스킬(들) | 저작권 고지 | +|---|---| +| apple-design, emil-design-eng | Copyright (c) 2026 Emil Kowalski | +| beautiful-shadows | Copyright (c) 2026 Meng To | +| accessibility (web-a11y) | Copyright (c) 2026 Addy Osmani | +| design-review | "MIT"로 선언(저장소 `.claude-plugin/plugin.json`의 `license` 필드, 저자 Joey Primiani) — 저장소에 LICENSE 파일 자체는 없다. 아래 부록 A의 design-review 항목에 이 사실을 그대로 밝혔다 | +| shadcn | Copyright (c) 2023 shadcn | +| better-interface, better-accessibility, better-colors, better-layout, better-typography, better-ui, better-writing, interface-review | Copyright (c) 2026 Jakub Krehel | +| interaction-design | Copyright (c) 2024 Seth Hobson | + +각 스킬에서 재서술한 구체 규칙·수치는 본문에 해당 출처 ID(`[SKILL-APPLE-DESIGN]`, `[SKILL-BETTER-UI]` 등)로 표시했으며, 원문 문장을 그대로 복사한 곳은 없다. + +## 부록 A — MIT 저장소별 LICENSE 전문 + +### apple-design, emil-design-eng (emilkowalski/skills) + +``` +MIT License + +Copyright (c) 2026 Emil Kowalski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### beautiful-shadows (MengTo/Skills) + +``` +MIT License + +Copyright (c) 2026 Meng To + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### accessibility / web-a11y (addyosmani/web-quality-skills) + +``` +MIT License + +Copyright (c) 2026 Addy Osmani + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### design-review (Superfuture/design-review) + +저장소 클론(`d4d2609`)의 루트에는 LICENSE 파일이 없다. 라이선스는 저장소의 `.claude-plugin/plugin.json`과 `.claude-plugin/marketplace.json`이 `"license": "MIT"`, 저자를 `Joey Primiani`로 선언한 것에 근거한다. 아래는 그 선언을 따라 재구성한 표준 MIT 문면이며, 실제 LICENSE 파일에서 그대로 옮긴 것이 아니라는 점을 밝힌다. + +``` +MIT License + +Copyright (c) 2026 Joey Primiani + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### shadcn (shadcn-ui/ui) + +``` +MIT License + +Copyright (c) 2023 shadcn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### better-interface와 자매 7종 (jakubkrehel/skills) + +저장소 루트 LICENSE 하나가 `better-interface`, `better-accessibility`, `better-colors`, `better-layout`, `better-typography`, `better-ui`, `better-writing`, `interface-review`를 전부 커버한다. + +``` +MIT License + +Copyright (c) 2026 Jakub Krehel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### interaction-design (wshobson/agents) + +``` +MIT License + +Copyright (c) 2024 Seth Hobson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/packages/skill/agents/openai.yaml b/packages/skill/agents/openai.yaml new file mode 100644 index 0000000..538abd0 --- /dev/null +++ b/packages/skill/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "designpaca" + short_description: "브리프 인터뷰부터 셀프 감사까지 이어지는 웹 디자인 파이프라인" + +policy: + allow_implicit_invocation: true diff --git a/packages/skill/references/accessibility.md b/packages/skill/references/accessibility.md new file mode 100644 index 0000000..d039c30 --- /dev/null +++ b/packages/skill/references/accessibility.md @@ -0,0 +1,563 @@ +# accessibility.md — 접근성 구현 계약 + +4단계 구현부터 참조하고, 5단계 프리플라이트의 접근성 표([preflight.md](preflight.md) §1)가 가리키는 정본이다. `preflight.md`는 체크리스트 행만 두고 여기를 가리킨다 — 이 문서는 체크리스트를 반복하지 않고 **왜 실패하는지, 어떤 코드로 고치는지, 무엇을 참조하는지**를 다룬다. + +라벨은 셋 중 하나다. **하드 게이트**는 위반하면 통과시키지 않는다 — WCAG AA·키보드·포커스·대비·감소 모션·진실성처럼 규범 또는 기능에 근거한다. **프로젝트 계약**은 브리프·`design.md`·플랫폼이 그 조건을 선택했을 때만 적용된다 — AAA 기준, HIG·M3의 44/48px 같은 모바일 앱 수준 목표가 여기 속한다. **관찰 후보**는 스타일 휴리스틱의 시작값이며 실제 렌더에서 확인해 조정한다. 외부 스킬에서 가져온 수치는 "시작값"이라 적고, 근거가 약하면 "실측 조정"을 덧붙인다. + +## 이 문서를 읽는 법 + +| 상황 | 읽을 곳 | +|---|---| +| AA와 AAA, 자동 점수와 실제 준수의 관계가 헷갈린다 | §0 | +| `
` 대신 뭘 써야 하는지, ARIA를 언제 쓰는지 | §1 | +| 아이콘 전용 버튼, 인라인 SVG의 이름을 어떻게 붙이는지 | §2 | +| 포커스 링 스타일·대비·가림 방지 | §3 | +| 탭·메뉴·콤보박스·리스트박스·다이얼로그의 키보드 계약, 모달, SPA 라우팅 | §4 | +| 스킵 링크·랜드마크·링크 텍스트 | §5 | +| 라벨·자동완성·입력 타입·비활성화 규칙·오류 처리 | §6 | +| 토스트·로딩 상태를 어떻게 알리는지 | §7 | +| 터치 타깃 크기와 히트 영역 확장 | §8 | +| 깜빡임·자동재생·호버 콘텐츠 | §9 | +| 드래그로만 되는 조작의 대안 | §10 | +| 비디오·오디오 자막 | §11 | +| `prefers-*` 미디어 쿼리 중 뭐가 하드 게이트고 뭐가 점진적 향상인지 | §12 | +| 감사를 어떤 순서로 돌리는지, MCP가 없을 때 뭘 쓰는지 | §13 | +| 발견한 문제를 어떤 형식으로 보고하는지 | §14 | +| 색·모션 단독 전달 금지(다른 문서가 이 문서를 정본으로 지목한 규칙) | 부록 A | + +--- + +## 0. 판정 원칙 + +**하드 게이트.** WCAG 2.2는 A·AA·AAA 세 적합성 수준을 정의하며, 셋은 서로 다른 요구다 — AAA를 만족한다고 모두에게 접근 가능해지지는 않는다[WCAG-22]. designpaca는 **AA를 하드 게이트**로 삼는다. 이 문서의 모든 절은 별도 표기가 없는 한 AA 이하 기준을 하드 게이트로 취급한다. + +**프로젝트 계약.** AAA 기준([WCAG-FOCUS-APPEAR] 2.4.13 등)과 HIG·M3의 44/48px 같은 플랫폼 권장은 브리프가 그 수준을 선택했을 때만 적용된다. "모바일 앱 수준"이라는 말이 브리프에 있으면 project contract로 올라간다(§8). + +**자동 점수는 준수가 아니다.** Lighthouse 접근성 100점이나 axe 통과는 도구가 검출할 수 있는 하위 집합만 확인한 것이다. 100점이 WCAG 준수와 같지 않고, 낮은 점수도 그 자체로 이슈 증거를 대체하지 않는다[SKILL-WEB-A11Y]. 통과 판정은 점수가 아니라 §13의 감사 루프(키보드 조작·스크린리더 확인 포함)로 내린다. + +**표준 기반 평가는 사용자 평가로 보완한다.** W3C 자신이 표준 기반 적합성 평가와 실제 사용자 평가를 함께 써야 한다고 설명한다 — 적은 표본이나 한 번의 평가를 모든 사용자에게 일반화하지 않는다[W3C-USER-EVAL]. 자동 검사·수동 APG 대조·스크린리더 확인을 전부 통과했더라도, 실사용자 피드백이 오면 그것으로 규칙을 다시 검토한다. + +**APCA는 규범이 아니다.** 대비의 하드 게이트는 WCAG 2 비율이다. APCA는 2026년 기준으로도 WCAG 3 초안에 이름조차 실려 있지 않고 자체 문서가 스스로를 beta로 표시하는 평가 후보일 뿐이다[APCA-STATUS] — 상세는 [color.md](color.md) §6. + +--- + +## 1. 시맨틱·네이티브 우선 + +**하드 게이트.** ARIA를 쓰기 전에 같은 의미·동작을 가진 네이티브 HTML 요소가 있는지 먼저 확인한다. 다섯 가지 ARIA 원칙이다[SKILL-BETTER-A11Y]: + +1. 필요한 시맨틱·동작을 가진 네이티브 요소가 있으면 다른 요소를 ARIA로 흉내 내지 않고 그것을 쓴다. +2. 정말 필요하지 않으면 네이티브 시맨틱을 바꾸지 않는다. +3. 인터랙티브 ARIA 컨트롤은 반드시 키보드로 조작할 수 있어야 한다 — role은 그 위젯의 전체 키보드 모델을 지키겠다는 약속이다. +4. 포커스를 받는 요소에 `role="presentation"`이나 `aria-hidden="true"`를 걸지 않는다. +5. 모든 인터랙티브 요소는 접근 가능한 이름을 가져야 한다(§2). + +나쁜 ARIA보다 ARIA가 없는 편이 낫다. 스크린리더는 role을 그대로 믿으므로, 틀린 role은 아무것도 없는 것보다 더 나쁘다. + +| 요소 | 쓰는 곳 | 이유 | +|---|---|---| +| `` | 이동 — 어딘가로 가거나 URL이 바뀌는 모든 것 | Cmd/Ctrl/가운데 클릭, 우클릭으로 링크 복사, Enter로 활성화가 전부 공짜다 | +| ` +``` + +보이는 게 클릭 가능하면 실제로 클릭 가능해야 하고, 클릭 가능하면 실제 인터랙티브 요소여야 한다. 링크를 버튼으로, 버튼을 링크로 다시 만들면 사용자의 기대(가운데 클릭으로 새 탭 열기 등)가 깨진다. 이동하는 "버튼"은 스타일만 버튼인 ``다[SKILL-BETTER-A11Y]. + +**네이티브 요소를 정말 쓸 수 없을 때만** `role="button"` + `tabindex="0"`의 완전한 폴리필을 쓴다(코드는 §4). 코드량 자체가 네이티브 요소보다 항상 많다는 사실이 네이티브를 우선해야 하는 실무적 이유이기도 하다. + +**흔한 ARIA 실수**[SKILL-BETTER-A11Y] + +| 실수 | 왜 실패하는가 | +|---|---| +| `
`나 ``에 `aria-label` | 대부분의 스크린리더가 인터랙티브하지 않고 role도 없는 요소의 이름은 무시한다 | +| ` + + + +``` + +**Label in Name — [WCAG-LABEL-NAME] (2.5.3).** 보이는 라벨과 접근 가능한 이름이 다르면 음성 제어 사용자가 깨진다. 버튼에 "보내기"라고 쓰여 있는데 `aria-label="메시지 전송"`을 걸면, "보내기 클릭"이라고 말하는 사용자의 명령이 인식되지 않는다. 접근 가능한 이름은 보이는 텍스트를 반드시 포함해야 한다[SKILL-BETTER-A11Y]. + +**인라인 SVG**는 장식인지 의미가 있는지로 마크업이 갈린다[SKILL-BETTER-A11Y]. + +```html + + + + +… +``` + +`focusable="false"`는 레거시 IE·Edge가 SVG를 탭 순서에 넣는 것을 막기 위한 것이다. 단순한 경우에는 `…`가 가장 안정적으로 전달된다. + +**브랜드명·코드 토큰에는 `translate="no"`를 붙인다.** 한국어 로케일 프로젝트에서 영문 브랜드명·식별자가 브라우저 자동번역 확장에 의해 깨지는 것을 막는다[SKILL-BETTER-A11Y]. + +```html +designpaca +``` + +**모달·시트·드로어의 제목.** 열리는 표면은 항상 접근 가능한 제목을 가져야 한다. 시각적으로 숨기려면 `.sr-only`(정의는 [motion.md](motion.md) 코드 G)로라도 존재해야 하며, 완전히 생략하는 것은 [WCAG-NRV] 위반이다[SKILL-SHADCN]. 컴포넌트 시스템을 쓰는 프로젝트의 합성 규칙은 [component-systems.md](component-systems.md)를 따른다. + +--- + +## 3. 포커스 + +**하드 게이트.** `:focus-visible`만 스타일링하고 `:focus` 단독을 쓰지 않는다. 브라우저는 키보드·보조기술 포커스에서만 `:focus-visible`을 보여 주고 마우스 클릭에서는 억제한다 — 클릭에서는 이미 포커스가 명백하기 때문이다. 검증된 대체 없이 `outline: none`을 쓰지 않는다. 이는 눈으로 보는 키보드 사용자의 내비게이션 자체를 지운다[SKILL-BETTER-A11Y]. + +```css +/* 가장 안전: 브라우저 기본 링을 유지하고 여백만 준다 */ +:focus-visible { outline-offset: 2px; } + +/* 브랜드 링이 필요할 때: 검증된 토큰을 쓴다 */ +:focus-visible { + outline: 2px solid var(--ring, currentColor); + outline-offset: 2px; +} +``` + +브라우저의 기본 포커스 링은 플랫폼·고대비 설정에 맞춰 스스로 적응한다. `outline-offset`만 얹으면 그 적응력을 유지한 채 여백만 얻는다. 커스텀 링을 쓰기로 했다면 `currentColor`나 토큰이 **닿는 모든 인접 색**(카드·히어로·다크모드 표면)에서 3:1 이상인지 직접 검증한다 — [WCAG-NONTEXT] (1.4.11)이 요구하는 것은 텍스트 대비가 아니라 인접 배경과의 대비다. + +`:focus-within`은 래퍼가 켜져야 하는 경우에 쓴다 — 테두리 안에 아이콘이 있는 검색창처럼, 내부 입력이 포커스를 받으면 바깥 래퍼도 함께 밝아져야 할 때다[SKILL-BETTER-A11Y]. + +**포커스 가림 방지 — [WCAG-FOCUS-OBSCURED] (2.4.11, WCAG 2.2 신규, AA).** 키보드 포커스를 받은 요소가 스티키 헤더·고정 CTA 바에 완전히 가려지면 안 된다. 앵커 타깃과 포커스 가능 요소에 `scroll-margin-top`을 주되, 값을 손으로 맞추지 않고 [tokens.md](tokens.md)의 `--header-h` 토큰과 연결한다. + +```css +:focus-visible, [data-anchor-target] { scroll-margin-top: var(--header-h); } +``` + +**`forced-colors: active` (Windows 고대비 모드).** 이 미디어 쿼리는 Widely available 상태다(확인 2026-09-24)[WEB-BASELINE]. 기본 색 조정을 유지하거나 `Highlight` 같은 시스템 색을 명명한다. `forced-color-adjust: none`은 저작한 색을 그대로 고정하므로, 강제 색 치환 후에도 실제로 지각 가능함을 검증한 곳에서만 쓴다[SKILL-BETTER-A11Y]. + +**프로젝트 계약 — [WCAG-FOCUS-APPEAR] (2.4.13, AAA).** 포커스 표시 영역이 비포커스 상태 컴포넌트 둘레 2px 두께 이상이고, 포커스·비포커스 상태 사이 대비가 3:1 이상이어야 한다는 더 엄격한 기준이다. 브리프가 AAA를 명시했을 때만 적용한다. + +--- + +## 4. 키보드 + +### 4.1 tabindex 규칙 + +**하드 게이트.** `tabindex="0"`은 자연 탭 순서에 합류시킬 때만 쓴다 — 네이티브로 포커스되지 않는 커스텀 인터랙티브 요소 전용이다. `tabindex="-1"`은 JS로만 포커스할 대상(헤딩·모달 컨테이너·roving tabindex 멤버)에 쓴다. **양수 tabindex는 절대 쓰지 않는다** — 페이지 전체의 탭 순서를 납치한다. 순서가 틀렸으면 tabindex가 아니라 DOM 순서를 고친다[SKILL-BETTER-A11Y]. + +### 4.2 roving tabindex — 복합 위젯 + +탭·메뉴·툴바·라디오 그룹처럼 하나로 묶여 Tab 정지점 하나만 차지해야 하는 위젯은 활성 항목만 `tabindex="0"`, 나머지는 `tabindex="-1"`로 두고 화살표 키가 포커스와 `0`을 함께 옮긴다[SKILL-BETTER-A11Y]. + +```html +
+ + + +
+``` + +```js +// ArrowLeft/ArrowRight로 activeIndex를 옮기고, 옮긴 탭에만 tabindex=0을 준다 +function onTabsKeydown(e, tabs, activeIndex) { + const delta = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; + if (!delta) return; + const next = (activeIndex + delta + tabs.length) % tabs.length; // 끝에서 wrap + tabs[activeIndex].tabIndex = -1; + tabs[next].tabIndex = 0; + tabs[next].focus(); + return next; +} +``` + +### 4.3 커스텀 인터랙티브 요소의 키보드 핸들러 + +네이티브 요소를 정말 쓸 수 없을 때, `role="button"` + `tabindex="0"`의 완전한 폴리필은 클릭 리스너에 더해 `keydown`에서 Enter·Space를 처리한다. **기본 동작을 막지 않으면 Space가 페이지를 스크롤한다.** + +```js +el.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + activate(); + } +}); +``` + +**함정 — 네이티브 `