create-harness-cli 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -30,9 +30,11 @@ npx create-harness-cli ./my-app --yes --dry-run # 계획만 확인
30
30
  scripts 를 감지해 실제 존재하는 것만 담는다)
31
31
  - 커밋 게이트 — 같은 스크립트 하나를 Cursor(`beforeShellExecution`)와
32
32
  Claude Code(`PreToolUse`) 양쪽에 연결. checks 실패·`.env` 스테이징·force push 시 커밋 거부
33
- - 워크플로 6종 — `/spec`(명세) → `/impl`(테스트 우선 구현) → `/verify` → `/ship`,
34
- UI 작업은 `/ds-init`(Storybook 온디맨드 설치)·`/ds-add`(레이아웃컴포넌트 선행)
35
- - 린트 강제 명명 규칙(`I` 접두 등)·공개 API 경계·색상 원시값 차단(stylelint)을 error 처리
33
+ - 워크플로 7종 — `/spec`(명세) → `/impl`(테스트 우선 구현) → `/verify` → `/ship`,
34
+ UI 작업은 `/ds-init`(Storybook 온디맨드 설치)·`/ds-add`(페이지Atomic 계층 선행)·
35
+ `/ux-review`(테스트로 옮기는 시각적 품질 리뷰 AC 강제를 우회하는 별도 트랙)
36
+ - 린트 강제 — 명명 규칙(`I` 접두 등)·공개 API 경계·Atomic 계층 역방향 import·
37
+ 색상 원시값 차단(stylelint)을 error 처리
36
38
 
37
39
  ## 옵션
38
40
 
@@ -64,9 +66,9 @@ npx create-harness-cli [대상 디렉터리] [옵션]
64
66
  대화형 실행에서는 비권장 모듈도 `(비권장)` 표시와 근거를 달고 목록에 나오므로 직접 켤 수 있습니다.
65
67
 
66
68
  모듈을 빼면 **그 모듈을 전제하는 규칙·워크플로도 함께 빠집니다.** 예를 들어
67
- `design-system` 없이 설치하면 `30-design-system` 규칙, `/ds-init`·`/ds-add` 워크플로,
68
- `AGENTS.md` 의 해당 항목이 모두 생성되지 않습니다. 존재하지 않는 파일을 가리키는
69
- 규칙은 에이전트를 헷갈리게 할 뿐입니다.
69
+ `design-system` 없이 설치하면 `30-design-system` 규칙, `/ds-init`·`/ds-add`·`/ux-review`
70
+ 워크플로, `AGENTS.md` 의 해당 항목이 모두 생성되지 않습니다. 존재하지 않는 파일을
71
+ 가리키는 규칙은 에이전트를 헷갈리게 할 뿐입니다.
70
72
 
71
73
  ## 디자인시스템에 대한 입장
72
74
 
@@ -74,6 +76,37 @@ npx create-harness-cli [대상 디렉터리] [옵션]
74
76
  색·간격을 쓰는 UI 드리프트를 막는 **결정적**(deterministic) 수단이 토큰 + stylelint
75
77
  하나뿐이기 때문입니다. 규칙 문서는 확률적으로만 지켜집니다.
76
78
 
79
+ ### UI는 Atomic 계층으로 쌓는다
80
+
81
+ 에이전트에게 "로그인 페이지 만들어줘"라고 하면 페이지 파일 하나에 마크업과 스타일을
82
+ 전부 쏟아붓습니다. 다음 화면에서도 같은 일이 반복되고, 버튼이 화면 수만큼 생깁니다.
83
+
84
+ 그래서 `/ds-add` 는 **화면 분해를 먼저 강제합니다** — atom → molecule → organism 순으로
85
+ 컴포넌트와 스토리를 만들고, 페이지는 마지막에 훅 호출 + 조립만 남깁니다.
86
+
87
+ ```
88
+ src/design-system/atoms/ 도메인 모름. 토큰만 (Button, Input, Badge)
89
+ src/design-system/molecules/ atom 2~3개 조합 (FormField, SearchBar)
90
+ src/design-system/organisms/ 의미 있는 UI 블록 (범용)
91
+ src/components/{Domain}/ 도메인 타입을 받는 organism
92
+ src/components/layouts/ template — 슬롯 레이아웃
93
+ 라우트 파일 page — 조립만
94
+ ```
95
+
96
+ 경계는 두 개뿐입니다. **도메인 타입이 들어오면 `design-system/` 을 떠난다**,
97
+ **데이터를 가져오면 컴포넌트가 아니라 page/hook이다.** 이 규칙은 문서로만 두면
98
+ 지켜지지 않으므로 `lint` 모듈의 ESLint 조각이 계층 역방향 import를 error로 끊습니다
99
+ (atom → molecule, molecule → organism, design-system → queries/stores).
100
+
101
+ ### 토큰은 primitive → semantic 2계층
102
+
103
+ `tokens.css`는 색상 램프(`--primitive-*`)와 그 위의 역할별 별칭(`--color-*`) 2계층이다.
104
+ 컴포넌트는 semantic 토큰만 쓰고, 브랜드를 바꿀 땐 primitive 램프만 교체한다 —
105
+ `-hover`·`-pressed`·`-subtle` 같은 상태 별칭이 자동으로 새 브랜드를 따라간다.
106
+ 구조는 [KRDS](https://github.com/KRDS-uiux/krds-uiux)(대한민국 디지털정부 디자인시스템)의
107
+ 토큰 계층을 참고했다 — 코드를 그대로 쓰지 않고 계층·상태 세트 패턴만 이식했다.
108
+ 그림자·트랜지션 토큰과 언제 쓰는지 기준도 함께 있다 — 근거는 `DECISIONS.md` #15.
109
+
77
110
  같은 이유로 이 하네스는 **Tailwind 를 권장하지 않습니다.** 값이 클래스 문자열 안에
78
111
  있어 stylelint 가 닿지 못하고, 임의값(`bg-[#3b82f6]`)을 막으려면 별도의 ESLint 규칙
79
112
  체계를 따로 유지해야 합니다. 새 프로젝트라면 CSS Modules + `tokens.css` 를 권장합니다.
@@ -130,7 +163,7 @@ UI 작업이 없는 저장소에 Playwright 바이너리까지 끌고 들어오
130
163
  - **Claude Code**: `/plugin` 설치는 살아있는 세션 안에서만 실행되는 명령이라 이 CLI가
131
164
  대신 실행할 수 없다. 대신 두 줄짜리 설치 명령을 "다음 단계" 맨 위에 출력한다.
132
165
 
133
- ## Storybook은 왜 미리 설치하지 않나
166
+ ## 개발
134
167
 
135
168
  ```bash
136
169
  npm run check # typecheck → build → test
package/dist/prompts.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as p from '@clack/prompts';
2
2
  const MODULE_LABELS = {
3
- 'design-system': '토큰 스켈레톤 + stylelint(색상 원시값 차단) + 스토리 템플릿',
3
+ 'design-system': '토큰 스켈레톤 + stylelint(색상 원시값 차단) + Atomic 계층 규칙 + 스토리 템플릿',
4
4
  'auth-http': 'axios 인터셉터(토큰 첨부·refresh·401) + ProtectedRoute',
5
5
  'data-fetching': 'queries 3계층 샘플 + IApiResponse + Zustand 스토어',
6
6
  lint: '명명 규칙·import 경계 ESLint 조각 + prettier + commitlint',
package/dist/registry.js CHANGED
@@ -100,16 +100,20 @@ const buildRuleActions = (options, vars) => listTemplates('core/conventions')
100
100
  };
101
101
  });
102
102
  const BASE_WORKFLOWS = ['spec', 'impl', 'verify', 'ship'];
103
- const DESIGN_SYSTEM_WORKFLOWS = ['ds-init', 'ds-add'];
103
+ // ds-init·ds-add는 Claude에서 design-system 스킬 하나로 합쳐진다 (설치·추가는 한 흐름)
104
+ const DESIGN_SYSTEM_SETUP_WORKFLOWS = ['ds-init', 'ds-add'];
105
+ // ux-review는 목적이 달라(진행 중인 UI 품질 리뷰) 독립 스킬로 둔다
106
+ const DESIGN_SYSTEM_REVIEW_WORKFLOWS = ['ux-review'];
104
107
  /** templates/core/workflows → .cursor/commands + .claude/skills(SKILL.md) fan-out */
105
108
  const buildWorkflowActions = (options, vars) => {
106
109
  const actions = [];
107
110
  const parsed = new Map();
108
111
  // 디자인시스템 워크플로는 토큰·스토리 템플릿을 전제한다 — 모듈이 빠지면 함께 뺀다
109
112
  const hasDesignSystem = options.modules.includes('design-system');
110
- const workflows = hasDesignSystem
111
- ? [...BASE_WORKFLOWS, ...DESIGN_SYSTEM_WORKFLOWS]
112
- : BASE_WORKFLOWS;
113
+ const designSystemWorkflows = hasDesignSystem
114
+ ? [...DESIGN_SYSTEM_SETUP_WORKFLOWS, ...DESIGN_SYSTEM_REVIEW_WORKFLOWS]
115
+ : [];
116
+ const workflows = [...BASE_WORKFLOWS, ...designSystemWorkflows];
113
117
  for (const name of workflows) {
114
118
  parsed.set(name, parseFrontmatter(loadTemplate(`core/workflows/${name}.md`, vars)));
115
119
  }
@@ -136,19 +140,31 @@ const buildWorkflowActions = (options, vars) => {
136
140
  module: 'core',
137
141
  });
138
142
  }
139
- // design-system 스킬 하나가 ds-init·ds-add 두 흐름을 포함한다
140
143
  if (hasDesignSystem) {
144
+ // design-system 스킬 하나가 ds-init·ds-add 두 흐름을 포함한다
141
145
  const dsInit = parsed.get('ds-init');
142
146
  const dsAdd = parsed.get('ds-add');
143
147
  actions.push({
144
148
  dest: '.claude/skills/design-system/SKILL.md',
145
149
  content: serializeFrontmatter({
146
150
  name: 'design-system',
147
- description: 'Design system workflows: one-time Storybook setup (ds-init) and adding components before layout work (ds-add).',
151
+ description: 'Design system workflows: one-time Storybook setup (ds-init) and building Atomic-layer components (atom/molecule/organism) before writing a page (ds-add).',
148
152
  }) +
149
153
  `# Design System\n\n## Part 1 — ds-init (최초 1회 설정)\n\n${dsInit.body}\n\n---\n\n## Part 2 — ds-add (UI 작업마다)\n\n${dsAdd.body}`,
150
154
  module: 'core',
151
155
  });
156
+ // ux-review는 별도 스킬 — ds-add와 ship 사이 아무 때나 부르는 독립 흐름이라 합치지 않는다
157
+ for (const name of DESIGN_SYSTEM_REVIEW_WORKFLOWS) {
158
+ const { meta, body } = parsed.get(name);
159
+ actions.push({
160
+ dest: `.claude/skills/${name}/SKILL.md`,
161
+ content: serializeFrontmatter({
162
+ name,
163
+ description: meta['description'] ?? '',
164
+ }) + body,
165
+ module: 'core',
166
+ });
167
+ }
152
168
  }
153
169
  }
154
170
  return actions;
@@ -322,7 +338,11 @@ export const requiredDevDeps = (options) => {
322
338
  deps.push('stylelint', 'stylelint-declaration-strict-value');
323
339
  }
324
340
  if (options.modules.includes('lint')) {
325
- deps.push('eslint-plugin-import', '@commitlint/cli', '@commitlint/config-conventional', 'prettier');
341
+ deps.push(
342
+ // eslint.harness.config.js 가 직접 import한다 — 추천 경로(기존 flat config
343
+ // 존재)에서는 이미 있을 확률이 높지만, --modules 로 강제 포함한 경우
344
+ // 실제로 없는 걸 실사용에서 확인했다 (예: 최신 Vite 템플릿은 oxlint 기본)
345
+ 'eslint', 'typescript-eslint', 'eslint-plugin-import', '@commitlint/cli', '@commitlint/config-conventional', 'prettier');
326
346
  }
327
347
  if (options.modules.includes('data-fetching')) {
328
348
  deps.push('@tanstack/react-query', 'zustand');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-harness-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Scaffold AI coding agent harness (conventions, verification gates, workflows) onto an existing project — for Cursor and Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,8 @@ node .harness/gates/run-checks.mjs # 전체 검증 (.harness/config.json 의 c
30
30
  | `/verify` | checks 순차 실행, 실패 시 수정 루프 |
31
31
  | `/ship` | 검증 → 커밋 → `docs/task-log.md` 기록 |
32
32
  {{#if DESIGN_SYSTEM}}| `/ds-init` | Storybook 온디맨드 설치 (최초 UI 작업 전 1회) |
33
- | `/ds-add` | 레이아웃 착수 전 디자인시스템 컴포넌트 + 스토리 선행 추가 |
33
+ | `/ds-add` | 페이지 착수 전 Atomic 계층(atom → molecule → organism) 컴포넌트 + 스토리 선행 추가 |
34
+ | `/ux-review` | 인터랙션 상태·토큰 사용·시각적 완성도 리뷰 — 테스트로 못 옮기는 품질을 다룬다 |
34
35
  {{/if}}
35
36
 
36
37
  ## 절대 금지
@@ -43,6 +44,8 @@ node .harness/gates/run-checks.mjs # 전체 검증 (.harness/config.json 의 c
43
44
  | `.env*` 파일 커밋 | 시크릿 유출 |
44
45
  | 라우트(페이지) 컴포넌트에 비즈니스 로직 | hooks/queries 레이어로 내린다 (`{{RULES_DIR}}/10-architecture` 참고) |
45
46
  {{#if DESIGN_SYSTEM}}| CSS 색상 원시값 (`#hex`, `rgb()`) | 디자인 토큰만 사용. stylelint가 error 처리 |
47
+ | 페이지 파일에 일회성 마크업·스타일 | Atomic 계층부터 만들고 페이지는 조립만 (`{{RULES_DIR}}/30-design-system`) |
48
+ | Atomic 계층 역방향 import (atom → molecule 등) | 재사용 단위가 상위 계층에 끌려간다. ESLint가 error 처리 |
46
49
  {{/if}}| 테스트 단정문 약화로 통과시키기 | 검증의 의미가 사라진다 |
47
50
 
48
51
  ## 장기 기억 문서
@@ -9,7 +9,7 @@ alwaysApply: false
9
9
  ## 레이어 (위에서 아래로만 의존)
10
10
 
11
11
  ```
12
- routes / components ← 페이지·UI. 조립만 한다
12
+ routes / components ← 페이지·UI. 조립만 한다 (내부는 Atomic 계층 — 30-design-system)
13
13
 
14
14
  hooks ← 화면 상태·데이터 오케스트레이션 (커스텀 훅)
15
15
 
@@ -52,3 +52,19 @@ import { fetchExampleList } from '../../queries/Example/exampleApi'
52
52
  - 도메인별 하위 폴더: `components/Chat/`, `hooks/chat/`, `queries/Chat/`
53
53
  - 공용은 `shared/`: `components/shared/`, `hooks/shared/`, `stores/shared/`
54
54
  - 스타일은 컴포넌트 옆 `*.module.css` (CSS Modules)
55
+
56
+ ### UI 계층 (Atomic)
57
+
58
+ `components` 레이어 안쪽은 Atomic 계층으로 다시 나뉜다. 상세 규칙·경계는
59
+ `30-design-system` 에 있고, 위치만 요약하면:
60
+
61
+ | 계층 | 위치 | 도메인 |
62
+ |------|------|--------|
63
+ | atom / molecule / 범용 organism | `src/design-system/{atoms,molecules,organisms}/` | 없음 |
64
+ | 도메인 organism | `src/components/{Domain}/` | 있음 |
65
+ | template(레이아웃) | `src/components/layouts/` | 없음 |
66
+ | page | 라우트 파일 | 있음 (훅 호출) |
67
+
68
+ - 계층은 아래에서 위로만 의존한다. atom이 molecule을 import하면 ESLint error다.
69
+ - 도메인 타입을 props로 받는 순간 `design-system/` 을 떠난다 —
70
+ `design-system/` 안의 코드는 도메인 타입을 몰라야 한다.
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: 디자인 토큰 강제, 컴포넌트 선행 추가 규칙, Storybook 온디맨드 설치
2
+ description: 디자인 토큰 강제, 장식·모션 사용 기준, Atomic 계층 구조, 컴포넌트 선행 조립 규칙, Storybook 온디맨드 설치
3
3
  globs: src/**/*.{tsx,css}
4
4
  alwaysApply: false
5
5
  ---
6
6
 
7
7
  # 디자인시스템
8
8
 
9
- ## 토큰은 닫힌 집합이다
9
+ ## 토큰은 닫힌 집합이다, 그리고 2계층이다
10
10
 
11
11
  - 색상은 **반드시** `src/design-system/tokens.css` 의 CSS 변수만 쓴다.
12
12
  원시값(`#hex`, `rgb()`, 색상 키워드)은 stylelint가 error 처리한다.
@@ -19,28 +19,149 @@ alwaysApply: false
19
19
  먼저 `tokens.css` 와 `tokens.ts` 양쪽에 추가하고 나서 쓴다.
20
20
  - TS/JSX에서 토큰 값이 필요하면 `tokens.ts` 의 타입드 상수를 import한다 (문자열 하드코딩 금지).
21
21
 
22
+ `tokens.css` 는 `--primitive-*`(색상 램프)와 그 위의 semantic 별칭 2계층이다.
23
+ **컴포넌트는 semantic 토큰만 쓴다 — `--primitive-*` 를 컴포넌트 CSS에서 직접 참조하지 않는다.**
24
+ 브랜드를 바꿀 땐 primitive 램프만 교체하면 그걸 참조하는 모든 semantic 별칭이 따라온다.
25
+
22
26
  ```css
23
- /* 좋음 */
24
- .card { background: var(--color-surface); border-radius: var(--radius-md); }
27
+ /* 좋음 — semantic 토큰. hover·pressed·subtle 이 이미 상태별로 준비돼 있다 */
28
+ .button {
29
+ background: var(--color-primary);
30
+ color: var(--color-on-primary);
31
+ border-radius: var(--radius-md);
32
+ }
33
+ .button:hover { background: var(--color-primary-hover); }
34
+ .button:active { background: var(--color-primary-pressed); }
35
+ .button:focus-visible {
36
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
37
+ outline-offset: var(--focus-ring-offset);
38
+ }
39
+
40
+ /* 나쁨 — primitive 직접 참조. semantic 계층을 두는 의미가 없어진다 */
41
+ .button { background: var(--primitive-primary-600); }
25
42
  /* 나쁨 — stylelint error */
26
- .card { background: #ffffff; border-radius: 8px; }
43
+ .button { background: #4f46e5; }
27
44
  ```
28
45
 
46
+ - **인터랙션 요소(atom 중 클릭 가능한 것)는 최소 hover·focus-visible 두 상태를 다룬다.**
47
+ 기본 상태 하나만 스타일링하고 끝내지 않는다 — `--color-primary-hover`/`-pressed`,
48
+ 상태 색상의 `-subtle`(배경)/`-strong`(강조 텍스트) 세트가 이미 준비되어 있다.
49
+ - **`:focus-visible` 에 `--focus-ring-*` 토큰으로 포커스 링을 반드시 그린다.**
50
+ `outline: none` 으로 지우기만 하고 대체 표시를 안 하는 것은 접근성 위반이다.
29
51
  - stylelint는 **색상만** 검사한다. 간격·타이포 토큰이 채워지면
30
52
  `stylelint.config.js` 의 주석 처리된 속성을 켠다.
31
53
 
32
- ## UI 작업 절차 컴포넌트가 레이아웃보다 먼저
54
+ ## 장식·모션그림자·트랜지션은 언제 쓰는가
55
+
56
+ 색상 토큰만 강제하면 "일관되지만 밋밋한" 결과가 나온다. 인터랙션 요소·뜬 요소는
57
+ 아래 기준에 해당할 때 그림자·트랜지션을 **생략하지 않는다** — 근거 없이 아무 데나
58
+ 장식을 넣으라는 뜻은 아니다.
59
+
60
+ | 상황 | 토큰 | 예 |
61
+ |------|------|----|
62
+ | 문서 표면 위에 살짝 뜬 요소 | `--shadow-sm` | 인풋 포커스, 툴팁 |
63
+ | 문서 흐름과 분리된 플로팅 요소 | `--shadow-md` | 드롭다운, 팝오버, sticky 헤더 |
64
+ | 배경을 덮는 오버레이 요소 | `--shadow-lg` | 모달, 다이얼로그 |
65
+ | 값이 바뀌는 인터랙션(hover·active·열림/닫힘) | `--duration-*` + `--easing-*` | 버튼 hover, 아코디언 펼침 |
66
+
67
+ ```css
68
+ /* 좋음 — 상태 전환에 트랜지션 토큰. 클릭 가능함이 느껴진다 */
69
+ .card {
70
+ box-shadow: var(--shadow-sm);
71
+ transition: box-shadow var(--duration-base) var(--easing-standard);
72
+ }
73
+ .card:hover { box-shadow: var(--shadow-md); }
74
+
75
+ /* 나쁨 — 값은 토큰인데 전환이 순간적이다 */
76
+ .card:hover { box-shadow: var(--shadow-md); } /* transition 없음 */
77
+ ```
78
+
79
+ - `transition`은 **바뀌는 속성만 지정**한다. `transition: all`은 의도치 않은 속성까지
80
+ 전환시켜 버벅임의 원인이 되므로 금지.
81
+ - 모달·팝오버처럼 화면에 들어오고 나가는 요소는 들어올 때 `--easing-decelerate`,
82
+ 나갈 때 `--easing-accelerate`를 쓴다. 그 외 상태 전환은 `--easing-standard`.
83
+ - `prefers-reduced-motion: reduce` 사용자에겐 전환·애니메이션을 끈다:
84
+
85
+ ```css
86
+ @media (prefers-reduced-motion: reduce) {
87
+ * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
88
+ }
89
+ ```
90
+
91
+ - **이 절은 stylelint가 아니라 `/ux-review`로 확인한다.** "그림자가 있어야 하는
92
+ 자리에 있는가"는 정적 분석보다 시각적 판단이 더 정확하다 — 그래서 게이트가 아니라
93
+ 리뷰 워크플로가 담당한다.
94
+
95
+ ## Atomic 계층 — 페이지는 조립의 결과지 출발점이 아니다
96
+
97
+ UI는 아래에서 위로 쌓는다. 각 계층은 **자기보다 아래 계층만** import한다.
98
+
99
+ ```
100
+ page (라우트) ← 훅 호출 + template/organism 조립만. 마크업·스타일 최소
101
+
102
+ template (레이아웃) ← 화면 골격. 슬롯(children/props)으로 내용을 받는다. 도메인 데이터 모름
103
+
104
+ organism ← molecule/atom 조합. 의미 있는 UI 블록 (카드, 목록, 폼 전체)
105
+
106
+ molecule ← atom 2~3개 조합. 단일 목적 (레이블+인풋+에러 = FormField)
107
+
108
+ atom ← 더 못 쪼개는 최소 단위. 토큰만 사용 (Button, Input, Badge, Icon)
109
+ ```
110
+
111
+ | 계층 | 위치 | 도메인 지식 | 데이터 |
112
+ |------|------|------------|--------|
113
+ | atom | `src/design-system/atoms/<Name>/` | 없음 | props |
114
+ | molecule | `src/design-system/molecules/<Name>/` | 없음 | props |
115
+ | organism (범용) | `src/design-system/organisms/<Name>/` | 없음 | props |
116
+ | organism (도메인) | `src/components/{Domain}/<Name>/` | 있음 | props (훅은 상위에서) |
117
+ | template | `src/components/layouts/<Name>/` | 없음 | slot |
118
+ | page | 라우트 파일 | 있음 | 훅 호출 |
119
+
120
+ ### 경계 두 가지만 기억한다
121
+
122
+ 1. **도메인이 들어오는 순간 `design-system/` 을 떠난다.**
123
+ `ReviewCard`처럼 도메인 타입(`IReview`)을 props로 받으면 `src/components/{Domain}/` 이다.
124
+ `design-system/` 안의 것은 어느 프로젝트에 옮겨도 컴파일돼야 한다.
125
+ 2. **데이터를 가져오는 순간 컴포넌트가 아니라 page/hook이다.**
126
+ organism 이하에서는 쿼리 훅·전역 스토어를 호출하지 않는다. props로 받는다
127
+ (그래야 스토리로 모든 상태를 렌더할 수 있다).
128
+
129
+ ### 역방향·횡단 import 금지
130
+
131
+ - atom → molecule/organism import 금지. molecule → organism 금지. **ESLint가 error 처리한다.**
132
+ - 같은 계층끼리의 import도 하지 않는다 (atom이 다른 atom을 쓰면 그건 molecule이다).
133
+ - 계층 폴더는 `index.ts` 공개 API로만 import한다. 내부 파일 deep import 금지.
134
+
135
+ ```ts
136
+ // 좋음
137
+ import { Button } from '../../design-system/atoms/Button'
138
+ // 나쁨 — 내부 구현 deep import (ESLint error)
139
+ import { Button } from '../../design-system/atoms/Button/Button'
140
+ // 나쁨 — atom 안에서 molecule import (ESLint error)
141
+ ```
142
+
143
+ ### 계층 승격
144
+
145
+ molecule이 커져 organism이 되는 일은 정상이다. 폴더를 옮기고 스토리 `title` 을 바꾼다.
146
+ **애매하면 낮은 계층에 두지 않는다** — 잘못 올린 것은 내리기 쉽지만,
147
+ atom에 도메인이 섞이면 그 atom을 쓰는 화면 전부가 오염된다.
148
+
149
+ ## UI 작업 절차 — 컴포넌트가 페이지보다 먼저
33
150
 
34
151
  페이지·레이아웃 작업 지시를 받으면:
35
152
 
36
- 1. `src/design-system/components/` 필요한 컴포넌트가 이미 있는지 확인
37
- 2. 없으면 **레이아웃 착수 전에** 컴포넌트 + `*.stories.tsx`(play 함수 포함)를 먼저 추가 — `/ds-add` 워크플로
38
- 3. Storybook이 아직 설치되지 않았다면 `/ds-init` 부터 실행
153
+ 1. 화면을 계층으로 쪼갠다 — 이 페이지에 필요한 organism / molecule / atom 목록을 먼저 적는다
154
+ 2. `src/design-system/` `src/components/` 이미 있는지 확인 (있으면 그대로 쓴다)
155
+ 3. 없는 것은 **낮은 계층부터** 만든다: atom → molecule → organism. 각각 `*.stories.tsx` 포함 — `/ds-add` 워크플로
156
+ 4. Storybook이 아직 설치되지 않았다면 `/ds-init` 부터 실행
157
+ 5. 마지막에 페이지를 쓴다. 페이지는 훅 호출 + 조립뿐이다
39
158
 
40
- 이 순서를 건너뛰고 페이지 안에 일회성 스타일을 쌓으면 UI가 화면마다 어긋난다(드리프트).
159
+ 이 순서를 건너뛰고 페이지 안에 일회성 마크업·스타일을 쌓으면 UI가 화면마다 어긋난다(드리프트).
160
+ 페이지 파일에 `<button className=...>` 이 등장하면 그건 빠뜨린 atom이다.
41
161
 
42
162
  ## 기존 컴포넌트 우선
43
163
 
44
- - 새 버튼·입력·모달을 만들기 전에 `src/design-system/components/` 와
164
+ - 새 버튼·입력·모달을 만들기 전에 `src/design-system/atoms|molecules|organisms/` 와
45
165
  `src/components/shared/` 를 먼저 조회한다. 비슷한 것이 있으면 variant를 추가하지 새로 만들지 않는다.
46
166
  - 스토리 작성 형식은 `src/design-system/_story-template.tsx` 를 따른다.
167
+ `title` 은 계층을 그대로 반영한다: `Atoms/Button`, `Molecules/FormField`, `Organisms/ReviewList`.
@@ -1,28 +1,58 @@
1
1
  ---
2
- description: Add a design-system component with story and play function BEFORE layout work.
2
+ description: Break a screen into Atomic layers and build components bottom-up (atom → molecule → organism) BEFORE writing the page.
3
3
  ---
4
4
 
5
- # /ds-add — 레이아웃 전에 컴포넌트부터
5
+ # /ds-add — 페이지 전에 계층부터
6
6
 
7
7
  UI 작업 지시를 받았을 때, 페이지 레이아웃에 착수하기 **전에** 실행하는 절차다.
8
+ 계층 정의와 경계 규칙은 `{{RULES_DIR}}/30-design-system` 에 있다.
8
9
 
9
10
  ## 절차
10
11
 
11
- 1. **재고 조사**: `src/design-system/components/` `src/components/shared/` 조회한다.
12
- - 필요한 컴포넌트가 이미 있으면 그대로 쓴다. 끝.
13
- - 비슷한 것이 있으면 variant/prop 추가를 우선 검토한다. 복제 금지.
14
- 2. **Storybook 확인**: `.storybook/` 이 없으면 먼저 `/ds-init` 을 실행한다.
15
- 3. **컴포넌트 작성**: `src/design-system/components/<Name>/` 에:
16
- - `<Name>.tsx` — 토큰만 사용 (`tokens.css` 변수·`tokens.ts` 상수), 원시 색상값 금지
17
- - `<Name>.module.css`
18
- - `<Name>.stories.tsx` — `src/design-system/_story-template.tsx` 형식을 따르고 **play 함수 필수**:
12
+ 1. **화면 분해**: 만들 화면을 계층으로 쪼개 목록을 먼저 적는다.
13
+
14
+ ```
15
+ OfficeDetailPage (page)
16
+ DetailLayout (template)
17
+ OfficeSummary (organism, 도메인 src/components/Office/)
18
+ RatingStars (molecule)
19
+ │ └ Icon (atom)
20
+ │ └ Badge (atom)
21
+ └ ReviewList (organism, 도메인 → src/components/Review/)
22
+ └ ReviewCard (molecule, 도메인)
23
+ └ Avatar (atom)
24
+ ```
25
+
26
+ 목록 없이 코드를 시작하지 않는다. 이 목록이 곧 작업 순서다.
27
+
28
+ 2. **재고 조사**: 목록의 각 항목이 `src/design-system/atoms|molecules|organisms/` 와
29
+ `src/components/` 에 이미 있는지 조회한다.
30
+ - 있으면 그대로 쓴다. 비슷한 것이 있으면 variant/prop 추가를 우선 검토한다. 복제 금지.
31
+ 3. **Storybook 확인**: `.storybook/` 이 없으면 먼저 `/ds-init` 을 실행한다.
32
+ 4. **계층 판정**: 없는 것마다 위치를 정한다.
33
+ - 더 못 쪼개면 atom / atom 2~3개 조합이면 molecule / 의미 있는 블록이면 organism
34
+ - 도메인 타입을 props로 받으면 `design-system/` 이 아니라 `src/components/{Domain}/`
35
+ - 쿼리 훅·전역 스토어를 부르고 싶어지면 컴포넌트가 아니다 — 그 호출은 page로 올린다
36
+ 5. **아래에서 위로 작성**: atom을 전부 끝내고 molecule, 그 다음 organism.
37
+ 각 컴포넌트 폴더에:
38
+ - `<Name>.tsx` — 토큰만 사용 (`tokens.css` 변수·`tokens.ts` 상수), 원시 색상값 금지.
39
+ 자기보다 위 계층 import 금지 (ESLint error)
40
+ - `<Name>.module.css` — 클릭 가능한 요소는 최소 `:hover`·`:focus-visible` 두 상태 포함
41
+ (`30-design-system` 필수 규칙). 뜬 요소는 계층에 맞는 `--shadow-*`, 상태 전환에는
42
+ `--duration-*`/`--easing-*` — 언제 쓰는지는 `30-design-system` 표 참고
43
+ - `<Name>.stories.tsx` — `src/design-system/_story-template.tsx` 형식을 따르고
44
+ `title` 은 계층 그대로(`Atoms/Button`), **play 함수 필수**:
19
45
  주요 상호작용(클릭·입력)과 포커스·aria 상태를 단정한다
20
46
  - `index.ts` — 공개 API
21
- 4. **검증**: 스토리 테스트와 stylelint 통과 확인.
22
- 5. 이제 페이지 레이아웃 작업에 착수한다. 페이지에서는 방금 만든 컴포넌트를 조립만 한다.
47
+ 6. **검증**: 스토리 테스트와 stylelint, lint(계층 위반 검사) 통과 확인. UI 완성도(인터랙션
48
+ 상태·트랜지션·그림자)는 정적 분석으로 잡으므로 `/ux-review`로 별도 확인한다.
49
+ 7. 이제 페이지를 쓴다. 페이지에는 훅 호출과 조립만 남는다 —
50
+ 새 마크업·스타일이 필요해지면 5번으로 돌아간다.
23
51
 
24
52
  ## 금지
25
53
 
26
54
  - 페이지 파일 안에 일회성 버튼·인풋 스타일 작성 (드리프트의 시작)
27
- - 스토리 없는 디자인시스템 컴포넌트
55
+ - 계층 건너뛰기 atom 없이 organism부터 만들기
56
+ - atom/molecule 안에서 도메인 타입·쿼리 훅·전역 스토어 사용
57
+ - 스토리 없는 컴포넌트
28
58
  - 토큰에 없는 색·간격을 쓰기 위해 인라인 style로 우회
@@ -8,7 +8,13 @@ UI 작업이 처음 필요해진 시점에 실행한다. 이미 `.storybook/`
8
8
 
9
9
  ## 절차
10
10
 
11
- 1. **공식 CLI로 설치** (손으로 설정 파일을 쓰지 않는다 — 프레임워크·빌더 감지는 CLI 한다):
11
+ 1. **브랜드 토큰 확인**: `src/design-system/tokens.css` `--primitive-primary-*`
12
+ 아직 하네스 기본값(`#4f46e5` 계열)이면, 지금이 UI 작업이 실제로 시작되는 시점이므로
13
+ 사용자에게 브랜드 색상을 묻는다. 답을 받으면 램프 10단계를 전부 교체한다(단계 하나만
14
+ 바꾸면 hover·pressed 파생값이 어긋난다). 아직 정해지지 않았다면 기본값 그대로 두되
15
+ `docs/product-spec.md` TODO에 "브랜드 컬러 확정 필요"를 남긴다 — 이 확인을 건너뛰면
16
+ 기본값이 그대로 굳어져 나중에 아무도 안 건드리게 된다.
17
+ 2. **공식 CLI로 설치** (손으로 설정 파일을 쓰지 않는다 — 프레임워크·빌더 감지는 CLI가 한다):
12
18
 
13
19
  ```bash
14
20
  {{PM_EXEC}} storybook@latest init --no-dev --yes
@@ -22,7 +28,7 @@ UI 작업이 처음 필요해진 시점에 실행한다. 이미 `.storybook/`
22
28
  {{PM_EXEC}} storybook add @storybook/addon-a11y
23
29
  ```
24
30
 
25
- 2. **접근성 위반을 검증 실패로**: `.storybook/preview.(ts|tsx)` 의 `parameters.a11y.test` 를
31
+ 3. **접근성 위반을 검증 실패로**: `.storybook/preview.(ts|tsx)` 의 `parameters.a11y.test` 를
26
32
  `'todo'`(init 기본값)에서 `'error'` 로 바꾼다:
27
33
 
28
34
  ```ts
@@ -31,29 +37,52 @@ a11y: {
31
37
  },
32
38
  ```
33
39
 
34
- 3. **린트 정합**: Storybook이 만든 파일이 프로젝트 eslint에 걸리지 않게 한다.
40
+ 4. **린트 정합**: Storybook이 만든 파일이 프로젝트 eslint에 걸리지 않게 한다.
35
41
  - 타입 인식 린트(parserOptions.project)를 쓰는 프로젝트면 eslint ignores에
36
42
  `.storybook/**` 와 `vitest.shims.d.ts`(addon-vitest 생성물) 추가
37
43
  - 스토리 export(PascalCase)가 naming-convention에 걸리면 `**/*.stories.{ts,tsx}` 오버라이드로
38
44
  해당 규칙을 끈다 (하네스 lint 모듈의 `eslint.harness.config.js` 에는 이미 포함)
39
45
  - init이 만든 예제(`src/stories/`)는 프로젝트 컨벤션에 안 맞으면 삭제한다
40
46
 
41
- 4. **checks에 등록**: `.harness/config.json` 의 `checks` 배열에서 `test` 항목 **앞**에 추가
47
+ 5. **checks에 등록**: `.harness/config.json` 의 `checks` 배열에서 `test` 항목 **앞**에 추가
42
48
  (addon-vitest 설치가 vitest workspace를 구성해준 경우):
43
49
 
44
50
  ```json
45
51
  { "id": "test-storybook", "command": "{{PM_EXEC}} vitest --project=storybook --run" }
46
52
  ```
47
53
 
48
- 5. **참조 구현 생성**: `src/design-system/examples/` 아래에 프로젝트의 토큰과 컴포넌트만 쓰는
49
- 예제 3종을 만든다 — 폼(`ExampleForm`), 데이터 테이블(`ExampleTable`), 상세 페이지(`ExampleDetail`).
50
- 각각 스토리 포함. 이 예제들은 컴파일되는 코드이므로 API가 바뀌면 깨진다 —
54
+ 6. **계층 폴더 스켈레톤 생성**: Atomic 계층을 폴더로 고정한다
55
+ (계층 정의는 `{{RULES_DIR}}/30-design-system`).
56
+
57
+ ```
58
+ src/design-system/atoms/
59
+ src/design-system/molecules/
60
+ src/design-system/organisms/
61
+ src/components/layouts/ # template 계층
62
+ ```
63
+
64
+ Storybook 사이드바가 계층 순서대로 보이도록 `.storybook/preview.(ts|tsx)` 에
65
+ `options.storySort` 를 넣는다:
66
+
67
+ ```ts
68
+ options: {
69
+ storySort: { order: ['Atoms', 'Molecules', 'Organisms', 'Layouts'] },
70
+ },
71
+ ```
72
+
73
+ 7. **참조 구현 생성**: 위 폴더에 이 프로젝트의 토큰만 쓰는 **한 줄기의 계층 예제**를 만든다 —
74
+ `atoms/Button`, `molecules/FormField`(Button + 인풋 + 에러 메시지),
75
+ `organisms/ExampleForm`(FormField 조합), `layouts/ExampleLayout`(슬롯 레이아웃).
76
+ 각각 스토리 포함, `title` 은 계층 그대로.
77
+ 흩어진 예제 3종보다 **한 화면이 atom에서 organism까지 쌓이는 과정**을 보여주는 편이
78
+ 모방 대상으로 낫다. 이 예제들은 컴파일되는 코드이므로 API가 바뀌면 깨진다 —
51
79
  그게 목적이다. 에이전트(자신 포함)가 산문 문서 대신 이 코드를 모방하게 된다.
52
- 6. **확인**: `{{PM_RUN}} storybook` 으로 기동 확인 후,
80
+ 8. **확인**: `{{PM_RUN}} storybook` 으로 기동 확인 후,
53
81
  `node .harness/gates/run-checks.mjs` 전체 통과 확인.
82
+ 계층 역방향 import가 lint error로 잡히는지 한 번 일부러 확인해 둔다.
54
83
 
55
84
  ## 완료 조건
56
85
 
57
- - `.storybook/` 존재, a11y test = 'error'
86
+ - `.storybook/` 존재, a11y test = 'error', storySort 적용
58
87
  - checks에 storybook 테스트 등록
59
- - `src/design-system/examples/` 3종 + 스토리
88
+ - `atoms` / `molecules` / `organisms` / `layouts` 폴더와 계층 예제 + 스토리
@@ -10,7 +10,8 @@ description: Implement from a spec, failing tests first (Red-Green-Refactor).
10
10
 
11
11
  1. **명세 읽기**: 수용 기준 목록을 확인한다. 모호하면 구현 전에 질문한다.
12
12
  2. **UI 작업인지 판단**: 새 화면·컴포넌트가 필요하면 먼저 디자인시스템 절차를 따른다:
13
- - 필요한 컴포넌트가 `src/design-system/components/` 없으면 `/ds-add` 선행
13
+ - 화면을 Atomic 계층(atom molecule → organism)으로 쪼개고, 없는 계층이 있으면 `/ds-add` 선행.
14
+ 페이지 파일부터 쓰지 않는다 — 페이지는 조립의 결과다
14
15
  - Storybook 미설치 상태면 `/ds-init` 선행
15
16
  3. **Red**: 수용 기준 하나당 실패하는 테스트를 먼저 쓴다. 실행해서 **실패를 확인**한다.
16
17
  (실패를 확인하지 않은 테스트는 아무것도 검증하지 않는 테스트일 수 있다)
@@ -24,6 +24,13 @@ git commit -m "feat: <무엇> — <왜>"
24
24
  5. **기록**: `docs/task-log.md` 맨 위에 한 줄 추가:
25
25
  `- YYYY-MM-DD <커밋 해시 앞 7자> <요약>`
26
26
  6. push는 사용자가 요청했을 때만 한다. `--force` 금지.
27
+ {{#if DESIGN_SYSTEM}}
28
+ ## UI 변경 시 커밋 전에 추가로 확인
29
+
30
+ 이번 변경이 `.tsx`/`.css` 를 건드렸다면 1번 단계 전에 `/ux-review` 를 아직 안
31
+ 돌렸는지 확인한다. `run-checks.mjs` 는 컴파일·테스트 통과만 보고, 인터랙션
32
+ 상태·트랜지션·그림자 같은 시각적 완성도는 안 본다.
33
+ {{/if}}
27
34
 
28
35
  ## 금지
29
36
 
@@ -0,0 +1,63 @@
1
+ ---
2
+ description: Review a UI diff for interaction states, token usage, and visual polish that acceptance criteria can't capture.
3
+ ---
4
+
5
+ # /ux-review — 시각적 품질 리뷰
6
+
7
+ `/spec`의 수용 기준은 "클릭하면 X가 된다"처럼 테스트로 옮길 수 있는 것만 담는다.
8
+ 그런데 hover 트랜지션이 있는지, 뜬 요소에 그림자가 있는지 같은 시각적 완성도는
9
+ 애초에 테스트 가능한 문장으로 못 쓴다 — 그래서 명세에 못 들어가고, 명세에 없으면
10
+ 아무도 손대지 않는다. `/ux-review`는 이 갭을 메우는 **별도 트랙**이다: `/ds-add`가
11
+ 끝난 뒤(또는 아무 때나 UI 코드를 점검하고 싶을 때) 실행하며, 여기서 찾은 항목 중
12
+ "직접 고쳐도 되는 범위"에 해당하는 건 명세를 다시 쓰지 않고 바로 고친다.
13
+
14
+ ## 언제 실행하나
15
+
16
+ - `/ds-add`로 atom/molecule/organism을 만들거나 고친 직후, `/ship` 전에
17
+ - 이미 있는 화면의 완성도를 점검하고 싶을 때 (대상 경로를 지정해 범위를 좁혀도 된다)
18
+
19
+ ## 체크리스트
20
+
21
+ diff(또는 지정된 범위)에서 아래를 확인한다. 전부 시각적 판단이 필요해 정적 분석으로
22
+ 자동화하기 애매한 것들이다 — 그래서 stylelint·ESLint가 아니라 이 리뷰가 담당한다.
23
+
24
+ | 항목 | 확인 |
25
+ |------|------|
26
+ | 인터랙션 상태 | 클릭 가능한 atom에 `:hover`·`:focus-visible` 이 있는가 (`30-design-system` 필수 규칙) |
27
+ | 눌림·비활성 | `:active`(pressed)·`disabled` 상태가 시각적으로 구분되는가 |
28
+ | 트랜지션 | 상태가 바뀌는 요소에 `--duration-*`/`--easing-*` 토큰으로 전환이 있는가, `transition: all` 을 쓰지 않았는가 |
29
+ | 뜬 요소 | 드롭다운·모달·팝오버 같은 플로팅 요소에 계층에 맞는 `--shadow-*` 가 있는가 (`30-design-system` 표 참고) |
30
+ | 토큰 준수 | 색상 외에 간격·radius·타이포도 새 값을 발명하지 않고 기존 토큰에서 골랐는가 |
31
+ | 빈·에러·로딩 상태 | 목록·폼 화면이면 이 세 상태가 빠지지 않았는가 |
32
+ | 브랜드 토큰 | `--primitive-primary-*` 가 아직 하네스 기본값(`#4f46e5` 계열)인가 — 리뷰를 막을 필요는 없지만, 여러 화면이 지난 뒤에도 그대로면 사용자에게 상기시킨다 |
33
+ | 계층 배치 | 새 컴포넌트가 `30-design-system` 의 atom/molecule/organism 판단 기준·경계(도메인 결합 여부)에 맞게 배치됐는가 |
34
+
35
+ ## 직접 고쳐도 되는 범위 — 명세 없이 바로 수정
36
+
37
+ 아래는 **기존 규칙을 이미 어긴 걸 맞추는 것**이지 새 취향을 넣는 게 아니다. 그래서
38
+ `/spec` 없이 바로 고친다:
39
+
40
+ - 빠진 `:hover`/`:focus-visible`/`:active` 상태를 토큰으로 추가
41
+ - 값 전환에 `--duration-*`/`--easing-*` 토큰으로 트랜지션 추가
42
+ - 위 표에 해당하는데 빠진 `--shadow-*` 추가
43
+ - 하드코딩된 값을 기존 토큰으로 교체
44
+
45
+ ## 명세로 돌아가야 하는 범위
46
+
47
+ 아래는 취향·구조 변경이라 리뷰 중 즉석으로 하지 않는다. 발견하면 사용자에게 제안하고
48
+ 필요하면 `/spec`으로 넘긴다:
49
+
50
+ - 새 색상·간격·radius 토큰 추가 (토큰 집합 자체를 바꾸는 일)
51
+ - 계층 재배치(atom↔molecule↔organism 승격/강등), 레이아웃 구조 변경, 새 의존성 추가
52
+ - 브랜드 색상 자체를 정하는 일 — 에이전트가 임의로 정하지 않는다
53
+
54
+ ## 금지
55
+
56
+ - 체크리스트에 없는 장식(그라디언트·애니메이션 등)을 판단 근거 없이 추가 — YAGNI는
57
+ 시각적 요소에도 적용된다. 표의 기준에 해당할 때만 그림자·트랜지션을 넣는다
58
+ - "명세로 돌아가야 하는 범위"를 리뷰 중에 임의로 처리하고 넘어가기
59
+
60
+ ## 완료 조건
61
+
62
+ - 체크리스트 각 항목을 확인했고, "직접 고쳐도 되는 범위"는 반영했다
63
+ - "명세로 돌아가야 하는 범위"에 해당하는 발견 사항은 사용자에게 보고했다
@@ -10,6 +10,46 @@
10
10
  import importPlugin from 'eslint-plugin-import'
11
11
  import tseslint from 'typescript-eslint'
12
12
 
13
+ /**
14
+ * 주의 — no-restricted-imports 는 **작성된 import 문자열**을 매칭한다 (해석된 경로가 아니다).
15
+ * design-system 내부에서 옆 계층을 부를 때 경로는 `../../molecules/FormField` 이지
16
+ * `.../design-system/molecules/...` 가 아니다. 패턴에 `design-system/` 을 넣으면
17
+ * 정작 막아야 할 계층 내부 위반이 통과한다 — 계층 폴더 이름만으로 매칭한다.
18
+ */
19
+
20
+ /**
21
+ * 공개 API 경계 (10-architecture) — 기능 폴더는 index.ts 로만 import한다.
22
+ * 아래 Atomic 계층 블록들이 no-restricted-imports 를 재정의하면서
23
+ * 이 패턴을 덮어쓰지 않도록 상수로 빼서 매번 함께 넣는다
24
+ * (flat config 는 같은 규칙을 병합하지 않고 마지막 정의로 대체한다).
25
+ */
26
+ const PUBLIC_API_PATTERN = {
27
+ group: [
28
+ '**/queries/*/*',
29
+ '!**/queries/*/index',
30
+ '**/components/*/*/*',
31
+ // <계층>/<Name>/<file> deep import (30-design-system)
32
+ '**/atoms/*/*',
33
+ '!**/atoms/*/index',
34
+ '**/molecules/*/*',
35
+ '!**/molecules/*/index',
36
+ '**/organisms/*/*',
37
+ '!**/organisms/*/index',
38
+ ],
39
+ message: '기능 폴더는 index.ts 공개 API로만 import하세요 (10-architecture).',
40
+ }
41
+
42
+ /** 상위 계층·도메인·전역 상태 차단 블록 하나를 만든다 (30-design-system) */
43
+ const layerBoundary = (layer, forbidden, message) => ({
44
+ files: [`src/design-system/${layer}/**/*.{ts,tsx}`],
45
+ rules: {
46
+ 'no-restricted-imports': [
47
+ 'error',
48
+ { patterns: [PUBLIC_API_PATTERN, { group: forbidden, message }] },
49
+ ],
50
+ },
51
+ })
52
+
13
53
  export default [
14
54
  {
15
55
  files: ['src/**/*.{ts,tsx}'],
@@ -27,6 +67,13 @@ export default [
27
67
  format: ['PascalCase', 'camelCase'],
28
68
  prefix: ['is', 'has', 'should', 'can', 'must', 'was', 'will'],
29
69
  },
70
+ // 화살표 함수 컴포넌트(const Button = () => ...)는 PascalCase 변수다 —
71
+ // 이 예외가 없으면 표준 React 컴포넌트 선언 방식 자체가 위반으로 잡힌다
72
+ {
73
+ selector: 'variable',
74
+ types: ['function'],
75
+ format: ['PascalCase', 'camelCase'],
76
+ },
30
77
  {
31
78
  selector: 'variable',
32
79
  format: ['camelCase'],
@@ -57,24 +104,8 @@ export default [
57
104
  '@typescript-eslint/no-explicit-any': 'error',
58
105
 
59
106
  // ── 공개 API 경계 (10-architecture) ─────────────────────────
60
- // 기능 폴더는 index.ts 로만 import. 내부 파일 deep import를 차단한다.
61
- // (폴더 내부의 상대 import './exampleApi' 패턴에 걸린다)
62
- 'no-restricted-imports': [
63
- 'error',
64
- {
65
- patterns: [
66
- {
67
- group: [
68
- '**/queries/*/*',
69
- '!**/queries/*/index',
70
- '**/components/*/*/*',
71
- ],
72
- message:
73
- '기능 폴더는 index.ts 공개 API로만 import하세요 (10-architecture).',
74
- },
75
- ],
76
- },
77
- ],
107
+ // 폴더 내부의 상대 import './exampleApi' 패턴에 걸린다
108
+ 'no-restricted-imports': ['error', { patterns: [PUBLIC_API_PATTERN] }],
78
109
 
79
110
  // ── import 정렬 ─────────────────────────────────────────────
80
111
  'import/order': [
@@ -90,6 +121,37 @@ export default [
90
121
  ],
91
122
  },
92
123
  },
124
+ // ── Atomic 계층 의존 방향 (30-design-system) ────────────────────
125
+ // UI는 아래에서 위로만 쌓인다. atom이 molecule을, molecule이 organism을
126
+ // import하는 순간 "재사용 가능한 최소 단위"라는 전제가 깨지고,
127
+ // 그 컴포넌트를 쓰는 화면 전부가 상위 계층에 끌려 들어간다.
128
+ // 문서로만 두면 지켜지지 않으므로 여기서 error로 끊는다.
129
+ layerBoundary(
130
+ 'atoms',
131
+ [
132
+ '**/molecules/**',
133
+ '**/organisms/**',
134
+ '**/components/**',
135
+ '**/queries/**',
136
+ '**/stores/**',
137
+ ],
138
+ 'atom은 상위 계층·도메인·전역 상태를 모른다. 조합이 필요하면 molecule로 올리세요 (30-design-system).',
139
+ ),
140
+ layerBoundary(
141
+ 'molecules',
142
+ [
143
+ '**/organisms/**',
144
+ '**/components/**',
145
+ '**/queries/**',
146
+ '**/stores/**',
147
+ ],
148
+ 'molecule은 atom만 조합한다. 도메인·전역 상태가 필요하면 organism 또는 page 계층입니다 (30-design-system).',
149
+ ),
150
+ layerBoundary(
151
+ 'organisms',
152
+ ['**/components/**', '**/queries/**', '**/stores/**'],
153
+ 'design-system 의 organism은 도메인을 모른다. 데이터는 props로 받고, 도메인 결합이 필요하면 src/components/{Domain}/ 으로 옮기세요 (30-design-system).',
154
+ ),
93
155
  {
94
156
  // 스토리 export(Default, Interaction 등)는 관례상 PascalCase — 명명 규칙 예외
95
157
  files: ['src/**/*.stories.{ts,tsx}'],
@@ -6,8 +6,11 @@
6
6
  *
7
7
  * 이 파일 자체는 렌더되지 않는 참고용이다 (일부러 *.stories.tsx 이름을 피했다 —
8
8
  * Storybook 러너가 집어들면 안 되기 때문). 실제 스토리를 만들 때:
9
- * 1. 이 파일을 복사해 components/<Name>/<Name>.stories.tsx 로
10
- * 2. 아래 TODO들을 채우고, play 함수에 상호작용·포커스·aria 단정을 쓴다
9
+ * 1. 이 파일을 복사해 <계층 폴더>/<Name>/<Name>.stories.tsx 로
10
+ * - atoms/ | molecules/ | organisms/ (도메인 결합 시 src/components/{Domain}/)
11
+ * 2. title 을 계층 그대로 쓴다 — 'Atoms/Button', 'Molecules/FormField',
12
+ * 'Organisms/ReviewList', 'Layouts/DetailLayout'
13
+ * 3. 아래 TODO들을 채우고, play 함수에 상호작용·포커스·aria 단정을 쓴다
11
14
  *
12
15
  * play 함수가 스크린샷으로 못 잡는 것(포커스 이동, 키보드 조작, aria 상태)을 잡는다.
13
16
  * 테스트를 통과시키려고 단정문을 약화시키지 않는다.
@@ -20,7 +23,8 @@ import { expect } from 'storybook/test'
20
23
  declare const MyComponent: (props: { label: string }) => JSX.Element
21
24
 
22
25
  const meta = {
23
- title: 'DesignSystem/MyComponent', // TODO: 카테고리/이름
26
+ // TODO: 계층/이름 — Atoms | Molecules | Organisms | Layouts
27
+ title: 'Atoms/MyComponent',
24
28
  component: MyComponent,
25
29
  tags: ['autodocs'],
26
30
  parameters: {
@@ -37,6 +41,11 @@ export const Default: TStory = {
37
41
  },
38
42
  }
39
43
 
44
+ /**
45
+ * 상위 계층(molecule/organism) 스토리는 하위 계층 조합의 결과를 렌더한다.
46
+ * 데이터는 반드시 args(props)로 주입한다 — 스토리 안에서 쿼리 훅·전역 스토어를 부르면
47
+ * 그 컴포넌트는 계층을 잘못 잡은 것이다 (규칙: 30-design-system).
48
+ */
40
49
  export const Interaction: TStory = {
41
50
  args: {
42
51
  label: '저장',
@@ -2,30 +2,104 @@
2
2
  * 디자인 토큰 정본 — 색상은 반드시 이 변수들만 사용한다 (stylelint가 원시값을 error 처리).
3
3
  * 존재하지 않는 토큰 이름을 발명하지 말 것. 필요하면 여기와 tokens.ts 양쪽에 먼저 추가한다.
4
4
  *
5
- * TODO: 아래 중립 회색 기본값을 프로젝트 브랜드 값으로 교체하세요.
5
+ * 2계층 구조 primitive(원시 램프) semantic(역할별 별칭, primitive만 참조).
6
+ * 컴포넌트는 semantic 토큰만 쓴다. 브랜드를 바꿀 땐 primitive 램프만 교체하면
7
+ * hover·pressed·subtle 등 그 램프를 참조하는 모든 semantic 별칭이 함께 따라간다 —
8
+ * 컴포넌트 CSS는 한 줄도 손대지 않는다.
9
+ *
10
+ * TODO: 아래 primary 램프 10단계를 프로젝트 브랜드 색상으로 교체하세요.
6
11
  */
7
12
  :root {
8
- /* ===== 색상 (stylelint 강제 대상) ===== */
9
- /* 브랜드 */
10
- --color-primary: #4f46e5; /* TODO: 브랜드 주 색상 */
11
- --color-primary-hover: #4338ca;
12
- --color-secondary: #64748b;
13
+ /* ============================================================
14
+ * PRIMITIVE — 원시 스케일. 컴포넌트·페이지 CSS에서 직접 참조하지 않는다.
15
+ * ============================================================ */
16
+
17
+ /* 브랜드 램프 (50=가장 밝음 → 900=가장 어두움) */
18
+ --primitive-primary-50: #eef2ff;
19
+ --primitive-primary-100: #e0e7ff;
20
+ --primitive-primary-200: #c7d2fe;
21
+ --primitive-primary-300: #a5b4fc;
22
+ --primitive-primary-400: #818cf8;
23
+ --primitive-primary-500: #6366f1;
24
+ --primitive-primary-600: #4f46e5; /* TODO: 브랜드 주 색상 — semantic --color-primary 가 이 단계를 참조 */
25
+ --primitive-primary-700: #4338ca;
26
+ --primitive-primary-800: #3730a3;
27
+ --primitive-primary-900: #312e81;
28
+
29
+ /* 중립 램프 */
30
+ --primitive-gray-0: #ffffff;
31
+ --primitive-gray-50: #f8fafc;
32
+ --primitive-gray-100: #f1f5f9;
33
+ --primitive-gray-200: #e2e8f0;
34
+ --primitive-gray-300: #cbd5e1;
35
+ --primitive-gray-400: #94a3b8;
36
+ --primitive-gray-500: #64748b;
37
+ --primitive-gray-600: #475569;
38
+ --primitive-gray-700: #334155;
39
+ --primitive-gray-800: #1e293b;
40
+ --primitive-gray-900: #0f172a;
41
+
42
+ /* 상태 램프 — subtle(배경)·base·strong(강조 텍스트·hover) 3단만 둔다.
43
+ 배지·알림·인라인 메시지가 주 용도라 브랜드 램프만큼 세분화할 필요가 없다. */
44
+ --primitive-success-100: #dcfce7;
45
+ --primitive-success-500: #16a34a;
46
+ --primitive-success-700: #15803d;
47
+ --primitive-warning-100: #fef3c7;
48
+ --primitive-warning-500: #d97706;
49
+ --primitive-warning-700: #b45309;
50
+ --primitive-danger-100: #fee2e2;
51
+ --primitive-danger-500: #dc2626;
52
+ --primitive-danger-700: #b91c1c;
53
+ --primitive-info-100: #e0f2fe;
54
+ --primitive-info-500: #0284c7;
55
+ --primitive-info-700: #0369a1;
56
+
57
+ /* ============================================================
58
+ * SEMANTIC — 컴포넌트·페이지 CSS는 이 계층만 참조한다.
59
+ * ============================================================ */
60
+
61
+ /* 브랜드 역할 — 버튼·링크 등 인터랙션 요소의 상태 4종 */
62
+ --color-primary: var(--primitive-primary-600);
63
+ --color-primary-hover: var(--primitive-primary-700);
64
+ --color-primary-pressed: var(--primitive-primary-800);
65
+ --color-primary-subtle: var(--primitive-primary-50);
66
+ --color-on-primary: var(--primitive-gray-0); /* primary 배경 위에 올리는 텍스트·아이콘 */
67
+ --color-secondary: var(--primitive-gray-500);
68
+ --color-secondary-hover: var(--primitive-gray-600);
13
69
 
14
70
  /* 표면·배경 */
15
- --color-background: #ffffff;
16
- --color-surface: #f8fafc;
17
- --color-border: #e2e8f0;
71
+ --color-background: var(--primitive-gray-0);
72
+ --color-surface: var(--primitive-gray-50);
73
+ --color-surface-hover: var(--primitive-gray-100); /* 리스트 행·카드 hover */
74
+ --color-border: var(--primitive-gray-200);
75
+ --color-border-strong: var(--primitive-gray-300);
18
76
 
19
77
  /* 텍스트 */
20
- --color-text: #0f172a;
21
- --color-text-muted: #64748b;
22
- --color-text-inverse: #ffffff;
78
+ --color-text: var(--primitive-gray-900);
79
+ --color-text-muted: var(--primitive-gray-500);
80
+ --color-text-disabled: var(--primitive-gray-400);
81
+ --color-text-inverse: var(--primitive-gray-0);
23
82
 
24
- /* 상태 */
25
- --color-success: #16a34a;
26
- --color-warning: #d97706;
27
- --color-error: #dc2626;
28
- --color-info: #0284c7;
83
+ /* 상태 — subtle(배경) / base(아이콘·보더) / strong(강조 텍스트) 3종 세트 */
84
+ --color-success-subtle: var(--primitive-success-100);
85
+ --color-success: var(--primitive-success-500);
86
+ --color-success-strong: var(--primitive-success-700);
87
+ --color-warning-subtle: var(--primitive-warning-100);
88
+ --color-warning: var(--primitive-warning-500);
89
+ --color-warning-strong: var(--primitive-warning-700);
90
+ --color-danger-subtle: var(--primitive-danger-100);
91
+ --color-danger: var(--primitive-danger-500);
92
+ --color-danger-strong: var(--primitive-danger-700);
93
+ --color-info-subtle: var(--primitive-info-100);
94
+ --color-info: var(--primitive-info-500);
95
+ --color-info-strong: var(--primitive-info-700);
96
+
97
+ /* 포커스 링 — 키보드 포커스 표시는 선택이 아니라 필수다. 공공 디자인시스템(KRDS 등)
98
+ 기준을 참고해 별도 토큰으로 뗀다. 사용: outline: var(--focus-ring-width) solid
99
+ var(--focus-ring-color); outline-offset: var(--focus-ring-offset); */
100
+ --focus-ring-color: var(--primitive-primary-500);
101
+ --focus-ring-width: 2px;
102
+ --focus-ring-offset: 2px;
29
103
 
30
104
  /* ===== 간격 (v0.1은 강제 안 함 — 토큰이 채워지면 stylelint.config.js 에서 켤 것) ===== */
31
105
  --space-1: 4px;
@@ -41,10 +115,19 @@
41
115
  --radius-lg: 16px;
42
116
  --radius-full: 9999px;
43
117
 
44
- /* ===== shadow ===== */
45
- --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
46
- --shadow-md: 0 4px 6px rgb(0 0 0 / 0.1);
47
- --shadow-lg: 0 10px 15px rgb(0 0 0 / 0.1);
118
+ /* ===== shadow — 문서 표면 위에 뜬 정도로 고른다. 사용 기준: 30-design-system.md ===== */
119
+ --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05); /* 인풋 포커스, 툴팁 */
120
+ --shadow-md: 0 4px 6px rgb(0 0 0 / 0.1); /* 드롭다운, 팝오버, sticky 헤더 */
121
+ --shadow-lg: 0 10px 15px rgb(0 0 0 / 0.1); /* 모달, 다이얼로그 */
122
+
123
+ /* ===== 모션 — 상태 전환에 쓴다. 사용 기준: 30-design-system.md =====
124
+ 강제 안 함(stylelint는 정적 분석이라 "전환이 있는가"를 못 본다) — /ux-review 에서 확인. */
125
+ --duration-fast: 120ms;
126
+ --duration-base: 200ms;
127
+ --duration-slow: 320ms;
128
+ --easing-standard: cubic-bezier(0.4, 0, 0.2, 1); /* 일반 상태 전환 */
129
+ --easing-decelerate: cubic-bezier(0, 0, 0.2, 1); /* 화면에 들어오는 요소(모달 열림 등) */
130
+ --easing-accelerate: cubic-bezier(0.4, 0, 1, 1); /* 화면에서 나가는 요소(모달 닫힘 등) */
48
131
 
49
132
  /* ===== 타이포 (v0.1은 강제 안 함) ===== */
50
133
  --font-size-sm: 12px;
@@ -2,21 +2,46 @@
2
2
  * tokens.css 의 타입드 미러 — TS/JSX에서 토큰 값이 필요할 때 이 상수를 import한다.
3
3
  * (예: 차트 라이브러리 색상 배열, canvas 렌더링)
4
4
  * 문자열 하드코딩 금지. tokens.css 에 변수를 추가하면 여기도 함께 추가한다.
5
+ *
6
+ * semanticColorTokens 만 컴포넌트에서 쓴다. primitive 램프는 tokens.css 안에서만
7
+ * 참조되고 TS로는 노출하지 않는다 — TS 코드가 램프를 직접 참조하면 브랜드 교체 시
8
+ * 여기도 다 고쳐야 해서, semantic 계층을 두는 의미가 없어진다.
5
9
  */
6
- export const colorTokens = {
10
+ export const semanticColorTokens = {
7
11
  primary: 'var(--color-primary)',
8
12
  primaryHover: 'var(--color-primary-hover)',
13
+ primaryPressed: 'var(--color-primary-pressed)',
14
+ primarySubtle: 'var(--color-primary-subtle)',
15
+ onPrimary: 'var(--color-on-primary)',
9
16
  secondary: 'var(--color-secondary)',
17
+ secondaryHover: 'var(--color-secondary-hover)',
10
18
  background: 'var(--color-background)',
11
19
  surface: 'var(--color-surface)',
20
+ surfaceHover: 'var(--color-surface-hover)',
12
21
  border: 'var(--color-border)',
22
+ borderStrong: 'var(--color-border-strong)',
13
23
  text: 'var(--color-text)',
14
24
  textMuted: 'var(--color-text-muted)',
25
+ textDisabled: 'var(--color-text-disabled)',
15
26
  textInverse: 'var(--color-text-inverse)',
27
+ successSubtle: 'var(--color-success-subtle)',
16
28
  success: 'var(--color-success)',
29
+ successStrong: 'var(--color-success-strong)',
30
+ warningSubtle: 'var(--color-warning-subtle)',
17
31
  warning: 'var(--color-warning)',
18
- error: 'var(--color-error)',
32
+ warningStrong: 'var(--color-warning-strong)',
33
+ dangerSubtle: 'var(--color-danger-subtle)',
34
+ danger: 'var(--color-danger)',
35
+ dangerStrong: 'var(--color-danger-strong)',
36
+ infoSubtle: 'var(--color-info-subtle)',
19
37
  info: 'var(--color-info)',
38
+ infoStrong: 'var(--color-info-strong)',
39
+ } as const
40
+
41
+ export const focusRingTokens = {
42
+ color: 'var(--focus-ring-color)',
43
+ width: 'var(--focus-ring-width)',
44
+ offset: 'var(--focus-ring-offset)',
20
45
  } as const
21
46
 
22
47
  export const spaceTokens = {
@@ -35,6 +60,26 @@ export const radiusTokens = {
35
60
  full: 'var(--radius-full)',
36
61
  } as const
37
62
 
38
- export type TColorToken = keyof typeof colorTokens
63
+ /** 사용 기준(어떤 계층에 어떤 그림자인지)은 30-design-system.md 참고 */
64
+ export const shadowTokens = {
65
+ sm: 'var(--shadow-sm)',
66
+ md: 'var(--shadow-md)',
67
+ lg: 'var(--shadow-lg)',
68
+ } as const
69
+
70
+ /** duration/easing 조합 사용 기준은 30-design-system.md 참고 */
71
+ export const motionTokens = {
72
+ durationFast: 'var(--duration-fast)',
73
+ durationBase: 'var(--duration-base)',
74
+ durationSlow: 'var(--duration-slow)',
75
+ easingStandard: 'var(--easing-standard)',
76
+ easingDecelerate: 'var(--easing-decelerate)',
77
+ easingAccelerate: 'var(--easing-accelerate)',
78
+ } as const
79
+
80
+ export type TSemanticColorToken = keyof typeof semanticColorTokens
81
+ export type TFocusRingToken = keyof typeof focusRingTokens
39
82
  export type TSpaceToken = keyof typeof spaceTokens
40
83
  export type TRadiusToken = keyof typeof radiusTokens
84
+ export type TShadowToken = keyof typeof shadowTokens
85
+ export type TMotionToken = keyof typeof motionTokens