create-harness-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/cli.js +204 -0
  4. package/dist/detect.js +128 -0
  5. package/dist/eslintPatch.js +58 -0
  6. package/dist/manifest.js +63 -0
  7. package/dist/ponytail.js +56 -0
  8. package/dist/prompts.js +85 -0
  9. package/dist/registry.js +334 -0
  10. package/dist/render.js +43 -0
  11. package/dist/suggest.js +114 -0
  12. package/dist/types.js +1 -0
  13. package/package.json +48 -0
  14. package/templates/core/AGENTS.md +55 -0
  15. package/templates/core/CLAUDE.md +11 -0
  16. package/templates/core/conventions/00-core.md +36 -0
  17. package/templates/core/conventions/10-architecture.md +54 -0
  18. package/templates/core/conventions/20-data-fetching.md +51 -0
  19. package/templates/core/conventions/30-design-system.md +46 -0
  20. package/templates/core/conventions/40-testing.md +46 -0
  21. package/templates/core/conventions/50-auth-http.md +45 -0
  22. package/templates/core/docs/architecture.md +21 -0
  23. package/templates/core/docs/decisions.md +16 -0
  24. package/templates/core/docs/product-spec.md +22 -0
  25. package/templates/core/docs/specs/_template.md +33 -0
  26. package/templates/core/docs/task-log.md +4 -0
  27. package/templates/core/gates/claude-settings.json +16 -0
  28. package/templates/core/gates/cursor-hooks.json +10 -0
  29. package/templates/core/gates/gate.mjs +115 -0
  30. package/templates/core/gates/pre-commit-gate.sh +7 -0
  31. package/templates/core/gates/run-checks.mjs +39 -0
  32. package/templates/core/workflows/ds-add.md +28 -0
  33. package/templates/core/workflows/ds-init.md +59 -0
  34. package/templates/core/workflows/impl.md +26 -0
  35. package/templates/core/workflows/ship.md +31 -0
  36. package/templates/core/workflows/spec.md +26 -0
  37. package/templates/core/workflows/verify.md +27 -0
  38. package/templates/presets/react-fe/configs/commitlint.config.js +36 -0
  39. package/templates/presets/react-fe/configs/eslint.harness.config.js +104 -0
  40. package/templates/presets/react-fe/configs/prettier.config.js +9 -0
  41. package/templates/presets/react-fe/design-system/_story-template.tsx +56 -0
  42. package/templates/presets/react-fe/design-system/stylelint.config.js +78 -0
  43. package/templates/presets/react-fe/design-system/tokens.css +57 -0
  44. package/templates/presets/react-fe/design-system/tokens.ts +40 -0
  45. package/templates/presets/react-fe/reference/auth-http/ProtectedRoute.tsx +62 -0
  46. package/templates/presets/react-fe/reference/auth-http/axiosInstance.ts +103 -0
  47. package/templates/presets/react-fe/reference/data-fetching/alertDialogStore.ts +30 -0
  48. package/templates/presets/react-fe/reference/data-fetching/api.ts +11 -0
  49. package/templates/presets/react-fe/reference/data-fetching/exampleApi.ts +46 -0
  50. package/templates/presets/react-fe/reference/data-fetching/exampleQueryKeys.ts +14 -0
  51. package/templates/presets/react-fe/reference/data-fetching/index.ts +25 -0
@@ -0,0 +1,59 @@
1
+ ---
2
+ description: One-time on-demand Storybook setup with a11y-as-error and vitest integration.
3
+ ---
4
+
5
+ # /ds-init — Storybook 온디맨드 설치 (최초 1회)
6
+
7
+ UI 작업이 처음 필요해진 시점에 실행한다. 이미 `.storybook/` 이 있으면 실행하지 않는다.
8
+
9
+ ## 절차
10
+
11
+ 1. **공식 CLI로 설치** (손으로 설정 파일을 쓰지 않는다 — 프레임워크·빌더 감지는 CLI가 한다):
12
+
13
+ ```bash
14
+ {{PM_EXEC}} storybook@latest init --no-dev --yes
15
+ ```
16
+
17
+ Vite 프로젝트면 최신 Storybook init이 `addon-vitest`·`addon-a11y`까지 함께 설치한다.
18
+ 설치 후 `package.json` 에 없으면 그때만 수동 추가:
19
+
20
+ ```bash
21
+ {{PM_EXEC}} storybook add @storybook/addon-vitest
22
+ {{PM_EXEC}} storybook add @storybook/addon-a11y
23
+ ```
24
+
25
+ 2. **접근성 위반을 검증 실패로**: `.storybook/preview.(ts|tsx)` 의 `parameters.a11y.test` 를
26
+ `'todo'`(init 기본값)에서 `'error'` 로 바꾼다:
27
+
28
+ ```ts
29
+ a11y: {
30
+ test: 'error',
31
+ },
32
+ ```
33
+
34
+ 3. **린트 정합**: Storybook이 만든 파일이 프로젝트 eslint에 걸리지 않게 한다.
35
+ - 타입 인식 린트(parserOptions.project)를 쓰는 프로젝트면 eslint ignores에
36
+ `.storybook/**` 와 `vitest.shims.d.ts`(addon-vitest 생성물) 추가
37
+ - 스토리 export(PascalCase)가 naming-convention에 걸리면 `**/*.stories.{ts,tsx}` 오버라이드로
38
+ 해당 규칙을 끈다 (하네스 lint 모듈의 `eslint.harness.config.js` 에는 이미 포함)
39
+ - init이 만든 예제(`src/stories/`)는 프로젝트 컨벤션에 안 맞으면 삭제한다
40
+
41
+ 4. **checks에 등록**: `.harness/config.json` 의 `checks` 배열에서 `test` 항목 **앞**에 추가
42
+ (addon-vitest 설치가 vitest workspace를 구성해준 경우):
43
+
44
+ ```json
45
+ { "id": "test-storybook", "command": "{{PM_EXEC}} vitest --project=storybook --run" }
46
+ ```
47
+
48
+ 5. **참조 구현 생성**: `src/design-system/examples/` 아래에 이 프로젝트의 토큰과 컴포넌트만 쓰는
49
+ 예제 3종을 만든다 — 폼(`ExampleForm`), 데이터 테이블(`ExampleTable`), 상세 페이지(`ExampleDetail`).
50
+ 각각 스토리 포함. 이 예제들은 컴파일되는 코드이므로 API가 바뀌면 깨진다 —
51
+ 그게 목적이다. 에이전트(자신 포함)가 산문 문서 대신 이 코드를 모방하게 된다.
52
+ 6. **확인**: `{{PM_RUN}} storybook` 으로 기동 확인 후,
53
+ `node .harness/gates/run-checks.mjs` 전체 통과 확인.
54
+
55
+ ## 완료 조건
56
+
57
+ - `.storybook/` 존재, a11y test = 'error'
58
+ - checks에 storybook 테스트 등록
59
+ - `src/design-system/examples/` 3종 + 스토리
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Implement from a spec, failing tests first (Red-Green-Refactor).
3
+ ---
4
+
5
+ # /impl — 명세 기반 구현 (테스트 우선)
6
+
7
+ `docs/specs/<slug>.md` 를 읽고 구현한다. 명세가 없으면 먼저 `/spec` 을 요청한다.
8
+
9
+ ## 절차
10
+
11
+ 1. **명세 읽기**: 수용 기준 목록을 확인한다. 모호하면 구현 전에 질문한다.
12
+ 2. **UI 작업인지 판단**: 새 화면·컴포넌트가 필요하면 먼저 디자인시스템 절차를 따른다:
13
+ - 필요한 컴포넌트가 `src/design-system/components/` 에 없으면 `/ds-add` 선행
14
+ - Storybook 미설치 상태면 `/ds-init` 선행
15
+ 3. **Red**: 수용 기준 하나당 실패하는 테스트를 먼저 쓴다. 실행해서 **실패를 확인**한다.
16
+ (실패를 확인하지 않은 테스트는 아무것도 검증하지 않는 테스트일 수 있다)
17
+ 4. **Green**: 테스트를 통과시키는 최소 구현을 쓴다. 이 단계에서 리팩터링·범위 확장 금지.
18
+ 5. **Refactor**: 테스트가 초록인 상태에서 정리한다. 중복 제거, 이름 개선, 레이어 위치 조정.
19
+ 6. 수용 기준을 모두 소화할 때까지 3–5를 반복한다.
20
+ 7. `node .harness/gates/run-checks.mjs` 로 전체 검증을 돌린다.
21
+
22
+ ## 금지
23
+
24
+ - 테스트 없이 구현부터 쓰기
25
+ - 실패하는 테스트를 통과시키려고 단정문 약화 (`40-testing` 규칙)
26
+ - 명세에 없는 기능 끼워넣기 — 필요해 보이면 명세에 추가 제안부터
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: Verify, review test integrity, commit with conventional message, log to task-log.
3
+ ---
4
+
5
+ # /ship — 검증하고 커밋
6
+
7
+ 작업을 마무리해 커밋한다. 게이트가 있지만, 게이트에 걸리기 전에 스스로 검증한다.
8
+
9
+ ## 절차
10
+
11
+ 1. **검증**: `node .harness/gates/run-checks.mjs` 전체 통과 확인. 실패하면 `/verify` 루프.
12
+ 2. **테스트 무결성 검토**: 이번 변경에서 수정된 테스트 파일을 diff로 확인한다.
13
+ - 단정문이 약해진 곳(`toBe` → `toBeTruthy`, 삭제된 expect, 추가된 skip)이 있으면
14
+ 커밋을 멈추고 사용자에게 보고한다.
15
+ 3. **스테이징 검토**: `git status` 와 `git diff --cached` 로 의도한 파일만 올라갔는지 확인.
16
+ - `.env*`, 디버깅 로그, 무관한 파일이 섞여 있으면 뺀다.
17
+ 4. **커밋**: Conventional Commits 형식으로. 본문에는 "왜"를 적는다.
18
+
19
+ ```bash
20
+ git add <의도한 파일들>
21
+ git commit -m "feat: <무엇> — <왜>"
22
+ ```
23
+
24
+ 5. **기록**: `docs/task-log.md` 맨 위에 한 줄 추가:
25
+ `- YYYY-MM-DD <커밋 해시 앞 7자> <요약>`
26
+ 6. push는 사용자가 요청했을 때만 한다. `--force` 금지.
27
+
28
+ ## 금지
29
+
30
+ - `git commit --no-verify` — 게이트 우회 금지
31
+ - 검증 실패 상태에서 "일단 커밋" — 없다
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Write a spec before implementation. Acceptance criteria must map 1:1 to tests.
3
+ ---
4
+
5
+ # /spec — 구현 전 명세 작성
6
+
7
+ 기능 설명을 받아 `docs/specs/<slug>.md` 를 작성한다. 코드는 건드리지 않는다.
8
+
9
+ ## 절차
10
+
11
+ 1. `docs/specs/_template.md` 를 복사해 `<slug>.md` 를 만든다 (slug는 kebab-case).
12
+ 2. 사용자와의 대화·기존 코드에서 다음을 채운다:
13
+ - **목표**: 이 기능이 해결하는 문제 한 문단
14
+ - **범위 밖**: 이번에 하지 않는 것 (스코프 크리프 방지에 가장 중요)
15
+ - **수용 기준**: 각 항목이 **테스트 하나로 번역 가능한 문장**이어야 한다.
16
+ "잘 동작한다" 같은 검증 불가능한 문장 금지.
17
+ 예: "빈 목록이면 '데이터 없음' 문구가 보인다" (O) / "UX가 좋다" (X)
18
+ 3. 영향 범위를 조사해 적는다: 만질 파일, 새 의존성, 기존 기능에의 영향.
19
+ 4. 모호한 부분이 있으면 **추측하지 말고 사용자에게 질문**한다.
20
+ 5. 완성된 명세를 사용자에게 보여주고 확인을 받는다. 확인 전에 `/impl` 로 넘어가지 않는다.
21
+
22
+ ## 완료 조건
23
+
24
+ - `docs/specs/<slug>.md` 존재
25
+ - 수용 기준 각각이 테스트로 번역 가능
26
+ - 사용자 확인 완료
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Run all checks from .harness/config.json sequentially and fix failures in a loop.
3
+ ---
4
+
5
+ # /verify — 전체 검증 실행
6
+
7
+ `.harness/config.json` 의 `checks` 를 순서대로 전부 실행하고, 실패를 고친다.
8
+
9
+ ## 절차
10
+
11
+ 1. 실행:
12
+
13
+ ```bash
14
+ node .harness/gates/run-checks.mjs
15
+ ```
16
+
17
+ 2. 실패한 체크가 있으면:
18
+ - 출력을 읽고 **원인을 고친다** (에러를 숨기거나 체크를 비활성화하지 않는다)
19
+ - 다시 1부터. 모든 체크가 통과할 때까지 반복한다.
20
+ 3. 반복해도 못 고치는 실패는 상황을 정리해 사용자에게 보고한다:
21
+ - 어떤 체크가, 어떤 에러로, 무엇을 시도했는지
22
+
23
+ ## 금지
24
+
25
+ - 체크를 통과시키기 위한 `eslint-disable`·`@ts-ignore`·`it.skip` 남발.
26
+ 정말 필요하면 이유 주석과 함께 최소 범위로만.
27
+ - `.harness/config.json` 에서 실패하는 체크를 삭제하는 것 — 체크 목록 변경은 사용자 승인 사항이다.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Conventional Commits 검증 — /ship 워크플로와 게이트가 전제하는 커밋 형식.
3
+ * https://www.conventionalcommits.org/
4
+ */
5
+ export default {
6
+ extends: ['@commitlint/config-conventional'],
7
+ rules: {
8
+ 'type-enum': [
9
+ 2,
10
+ 'always',
11
+ [
12
+ 'feat', // 새 기능 (MINOR)
13
+ 'fix', // 버그 수정 (PATCH)
14
+ 'docs', // 문서
15
+ 'style', // 포맷팅 (동작 변화 없음)
16
+ 'refactor', // 리팩토링
17
+ 'perf', // 성능
18
+ 'test', // 테스트
19
+ 'chore', // 빌드·설정
20
+ 'ci', // CI
21
+ 'build', // 빌드 시스템
22
+ 'revert', // 되돌리기
23
+ ],
24
+ ],
25
+ 'type-case': [2, 'always', 'lower-case'],
26
+ 'subject-case': [0],
27
+ 'subject-empty': [2, 'never'],
28
+ 'type-empty': [2, 'never'],
29
+ },
30
+ ignorePatterns: [
31
+ '^Merge branch',
32
+ '^Merge pull request',
33
+ '^Merge remote-tracking branch',
34
+ '^Revert "',
35
+ ],
36
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * 하네스 린트 규칙 조각 — 기존 eslint.config.js 에 spread해서 쓴다:
3
+ *
4
+ * import harnessRules from './eslint.harness.config.js'
5
+ * export default [ ...기존설정, ...harnessRules ]
6
+ *
7
+ * 문서만 있으면 에이전트가 무시한다. 컨벤션은 여기서 error로 강제된다.
8
+ * 필요 devDependency: typescript-eslint, eslint-plugin-import
9
+ */
10
+ import importPlugin from 'eslint-plugin-import'
11
+ import tseslint from 'typescript-eslint'
12
+
13
+ export default [
14
+ {
15
+ files: ['src/**/*.{ts,tsx}'],
16
+ plugins: {
17
+ '@typescript-eslint': tseslint.plugin,
18
+ import: importPlugin,
19
+ },
20
+ rules: {
21
+ // ── 명명 규칙 (10-architecture) ─────────────────────────────
22
+ '@typescript-eslint/naming-convention': [
23
+ 'error',
24
+ {
25
+ selector: 'variable',
26
+ types: ['boolean'],
27
+ format: ['PascalCase', 'camelCase'],
28
+ prefix: ['is', 'has', 'should', 'can', 'must', 'was', 'will'],
29
+ },
30
+ {
31
+ selector: 'variable',
32
+ format: ['camelCase'],
33
+ leadingUnderscore: 'allow',
34
+ },
35
+ {
36
+ selector: 'variable',
37
+ modifiers: ['const'],
38
+ format: ['UPPER_CASE', 'camelCase'],
39
+ filter: { regex: '^[_A-Z0-9]+$', match: true },
40
+ },
41
+ { selector: 'function', format: ['PascalCase', 'camelCase'] },
42
+ { selector: 'class', format: ['PascalCase'] },
43
+ {
44
+ selector: 'interface',
45
+ format: ['PascalCase'],
46
+ custom: { regex: '^I[A-Z]', match: true },
47
+ },
48
+ { selector: 'typeAlias', format: ['PascalCase'] },
49
+ {
50
+ selector: 'typeParameter',
51
+ format: ['PascalCase'],
52
+ prefix: ['T'],
53
+ },
54
+ ],
55
+
56
+ // ── any 금지 (00-core) ──────────────────────────────────────
57
+ '@typescript-eslint/no-explicit-any': 'error',
58
+
59
+ // ── 공개 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
+ ],
78
+
79
+ // ── import 정렬 ─────────────────────────────────────────────
80
+ 'import/order': [
81
+ 'error',
82
+ {
83
+ groups: [
84
+ ['builtin', 'external'],
85
+ ['internal', 'parent', 'sibling', 'index'],
86
+ ],
87
+ 'newlines-between': 'always',
88
+ alphabetize: { order: 'asc', caseInsensitive: true },
89
+ },
90
+ ],
91
+ },
92
+ },
93
+ {
94
+ // 스토리 export(Default, Interaction 등)는 관례상 PascalCase — 명명 규칙 예외
95
+ files: ['src/**/*.stories.{ts,tsx}'],
96
+ rules: {
97
+ '@typescript-eslint/naming-convention': 'off',
98
+ },
99
+ },
100
+ {
101
+ // 하네스 인프라(.harness/)와 incoming 은 린트 대상이 아니다
102
+ ignores: ['.harness/**'],
103
+ },
104
+ ]
@@ -0,0 +1,9 @@
1
+ export default {
2
+ semi: false,
3
+ singleQuote: true,
4
+ trailingComma: 'all',
5
+ printWidth: 80,
6
+ tabWidth: 4,
7
+ bracketSpacing: true,
8
+ endOfLine: 'auto',
9
+ }
@@ -0,0 +1,56 @@
1
+ /* eslint-disable */
2
+ // ↑ 이 파일은 렌더되지 않는 참고용 템플릿이라 린트 대상에서 제외한다. 복사 후 제거하세요.
3
+ /**
4
+ * 스토리 작성 템플릿 — 새 디자인시스템 컴포넌트의 스토리는 이 형식을 따른다.
5
+ * (규칙: 30-design-system, 40-testing)
6
+ *
7
+ * 이 파일 자체는 렌더되지 않는 참고용이다 (일부러 *.stories.tsx 이름을 피했다 —
8
+ * Storybook 러너가 집어들면 안 되기 때문). 실제 스토리를 만들 때:
9
+ * 1. 이 파일을 복사해 components/<Name>/<Name>.stories.tsx 로
10
+ * 2. 아래 TODO들을 채우고, play 함수에 상호작용·포커스·aria 단정을 쓴다
11
+ *
12
+ * play 함수가 스크린샷으로 못 잡는 것(포커스 이동, 키보드 조작, aria 상태)을 잡는다.
13
+ * 테스트를 통과시키려고 단정문을 약화시키지 않는다.
14
+ */
15
+ // @ts-nocheck — 템플릿 파일. 복사 후 @ts-nocheck 을 제거하세요.
16
+ import type { Meta, StoryObj } from '@storybook/react'
17
+ import { expect } from 'storybook/test'
18
+
19
+ // TODO: import MyComponent from './MyComponent'
20
+ declare const MyComponent: (props: { label: string }) => JSX.Element
21
+
22
+ const meta = {
23
+ title: 'DesignSystem/MyComponent', // TODO: 카테고리/이름
24
+ component: MyComponent,
25
+ tags: ['autodocs'],
26
+ parameters: {
27
+ // a11y 위반은 테스트 실패다 (.storybook/preview 에서 전역 설정됨)
28
+ },
29
+ } satisfies Meta<typeof MyComponent>
30
+
31
+ export default meta
32
+ type TStory = StoryObj<typeof meta>
33
+
34
+ export const Default: TStory = {
35
+ args: {
36
+ label: '기본',
37
+ },
38
+ }
39
+
40
+ export const Interaction: TStory = {
41
+ args: {
42
+ label: '저장',
43
+ },
44
+ play: async ({ canvas, userEvent }) => {
45
+ // 사용자 관점 쿼리(getByRole)를 우선한다
46
+ const button = canvas.getByRole('button', { name: '저장' })
47
+
48
+ // 키보드 접근성: Tab으로 도달 가능한가
49
+ await userEvent.tab()
50
+ await expect(button).toHaveFocus()
51
+
52
+ // 상호작용 후 상태 단정
53
+ await userEvent.click(button)
54
+ // TODO: await expect(canvas.getByRole('status')).toHaveTextContent('저장됨')
55
+ },
56
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 디자인 토큰 강제 — 색상 원시값(#hex, rgb(), 색상 키워드)을 error 처리한다.
3
+ * 에이전트가 규칙 문서를 무시해도 여기서 걸린다.
4
+ *
5
+ * 색상만 켠다. 간격·타이포까지 한 번에 강제하면 토큰이 불완전한 상태에서
6
+ * 에이전트가 존재하지 않는 토큰 이름을 발명한다 — hex보다 나쁜 결과.
7
+ * src/design-system/tokens.css 의 해당 카테고리가 채워지면 아래 주석을 해제할 것.
8
+ */
9
+ {{#if STYLELINT_BASELINE}}import { readFileSync } from 'node:fs'
10
+
11
+ /**
12
+ * 유예 목록 — 하네스 도입 시점에 이미 원시값을 쓰던 CSS {{CSS_RAW_COLOR_FILES}}개.
13
+ * 이 파일들만 warning 으로 낮춘다. **새로 만드는 CSS는 그대로 error다.**
14
+ *
15
+ * 전부 error로 켜면 첫 커밋부터 막혀 게이트를 꺼버리게 되고, 반대로 전부 warning으로
16
+ * 낮추면 새 코드의 드리프트를 못 막는다. 목록으로 끊는 이유가 그것이다.
17
+ *
18
+ * 정리할 때마다 .harness/stylelint-baseline.json 에서 해당 경로를 지운다.
19
+ * 목록이 비면 이 블록과 overrides 를 통째로 삭제하면 된다.
20
+ */
21
+ const baseline = JSON.parse(
22
+ readFileSync(new URL('./.harness/stylelint-baseline.json', import.meta.url), 'utf-8'),
23
+ )
24
+
25
+ {{/if}}const TOKEN_PROPERTIES = [
26
+ // 색상 — 항상 강제
27
+ '/color$/',
28
+ 'fill',
29
+ 'stroke',
30
+ 'background',
31
+ 'border-color',
32
+ 'outline-color',
33
+
34
+ // 간격 — tokens.css 의 --space-* 가 실사용 값으로 채워지면 해제
35
+ // 'gap', 'padding', 'margin',
36
+
37
+ // 타이포 — --font-* 가 채워지면 해제
38
+ // 'font-size', 'font-weight',
39
+ ]
40
+
41
+ const OPTIONS = {
42
+ ignoreValues: [
43
+ 'currentColor',
44
+ 'transparent',
45
+ 'inherit',
46
+ 'initial',
47
+ 'unset',
48
+ 'none',
49
+ '/^var\\(--/',
50
+ ],
51
+ message:
52
+ '원시값 대신 디자인 토큰(var(--...))을 쓰세요. 토큰이 없으면 src/design-system/tokens.css 에 먼저 추가하세요.',
53
+ }
54
+
55
+ /**
56
+ * strict-value 플러그인은 함수 값(ignoreFunctions 기본 true)을 건너뛴다.
57
+ * 그래서 `#hex` 는 잡아도 `rgb()` · `hsl()` 은 통과한다 — 색상 속성에 한해 따로 막는다.
58
+ * background-image 의 그라디언트는 대상이 아니므로 var() 조합은 그대로 쓸 수 있다.
59
+ */
60
+ const COLOR_FUNCTION_PROPERTIES =
61
+ '/^(color|fill|stroke|background|background-color|border(-(top|right|bottom|left))?-color|outline-color)$/'
62
+
63
+ const rules = (severity) => ({
64
+ 'scale-unlimited/declaration-strict-value': [
65
+ TOKEN_PROPERTIES,
66
+ severity ? { ...OPTIONS, severity } : OPTIONS,
67
+ ],
68
+ 'declaration-property-value-disallowed-list': [
69
+ { [COLOR_FUNCTION_PROPERTIES]: ['/rgba?\\(/', '/hsla?\\(/'] },
70
+ { message: OPTIONS.message, ...(severity ? { severity } : {}) },
71
+ ],
72
+ })
73
+
74
+ export default {
75
+ plugins: ['stylelint-declaration-strict-value'],
76
+ rules: rules(),
77
+ {{#if STYLELINT_BASELINE}} overrides: [{ files: baseline, rules: rules('warning') }],
78
+ {{/if}}}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 디자인 토큰 정본 — 색상은 반드시 이 변수들만 사용한다 (stylelint가 원시값을 error 처리).
3
+ * 존재하지 않는 토큰 이름을 발명하지 말 것. 필요하면 여기와 tokens.ts 양쪽에 먼저 추가한다.
4
+ *
5
+ * TODO: 아래 중립 회색 기본값을 프로젝트 브랜드 값으로 교체하세요.
6
+ */
7
+ :root {
8
+ /* ===== 색상 (stylelint 강제 대상) ===== */
9
+ /* 브랜드 */
10
+ --color-primary: #4f46e5; /* TODO: 브랜드 주 색상 */
11
+ --color-primary-hover: #4338ca;
12
+ --color-secondary: #64748b;
13
+
14
+ /* 표면·배경 */
15
+ --color-background: #ffffff;
16
+ --color-surface: #f8fafc;
17
+ --color-border: #e2e8f0;
18
+
19
+ /* 텍스트 */
20
+ --color-text: #0f172a;
21
+ --color-text-muted: #64748b;
22
+ --color-text-inverse: #ffffff;
23
+
24
+ /* 상태 */
25
+ --color-success: #16a34a;
26
+ --color-warning: #d97706;
27
+ --color-error: #dc2626;
28
+ --color-info: #0284c7;
29
+
30
+ /* ===== 간격 (v0.1은 강제 안 함 — 토큰이 채워지면 stylelint.config.js 에서 켤 것) ===== */
31
+ --space-1: 4px;
32
+ --space-2: 8px;
33
+ --space-3: 12px;
34
+ --space-4: 16px;
35
+ --space-6: 24px;
36
+ --space-8: 32px;
37
+
38
+ /* ===== radius ===== */
39
+ --radius-sm: 4px;
40
+ --radius-md: 8px;
41
+ --radius-lg: 16px;
42
+ --radius-full: 9999px;
43
+
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);
48
+
49
+ /* ===== 타이포 (v0.1은 강제 안 함) ===== */
50
+ --font-size-sm: 12px;
51
+ --font-size-base: 14px;
52
+ --font-size-lg: 16px;
53
+ --font-size-xl: 20px;
54
+ --font-weight-normal: 400;
55
+ --font-weight-medium: 500;
56
+ --font-weight-bold: 700;
57
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * tokens.css 의 타입드 미러 — TS/JSX에서 토큰 값이 필요할 때 이 상수를 import한다.
3
+ * (예: 차트 라이브러리 색상 배열, canvas 렌더링)
4
+ * 문자열 하드코딩 금지. tokens.css 에 변수를 추가하면 여기도 함께 추가한다.
5
+ */
6
+ export const colorTokens = {
7
+ primary: 'var(--color-primary)',
8
+ primaryHover: 'var(--color-primary-hover)',
9
+ secondary: 'var(--color-secondary)',
10
+ background: 'var(--color-background)',
11
+ surface: 'var(--color-surface)',
12
+ border: 'var(--color-border)',
13
+ text: 'var(--color-text)',
14
+ textMuted: 'var(--color-text-muted)',
15
+ textInverse: 'var(--color-text-inverse)',
16
+ success: 'var(--color-success)',
17
+ warning: 'var(--color-warning)',
18
+ error: 'var(--color-error)',
19
+ info: 'var(--color-info)',
20
+ } as const
21
+
22
+ export const spaceTokens = {
23
+ 1: 'var(--space-1)',
24
+ 2: 'var(--space-2)',
25
+ 3: 'var(--space-3)',
26
+ 4: 'var(--space-4)',
27
+ 6: 'var(--space-6)',
28
+ 8: 'var(--space-8)',
29
+ } as const
30
+
31
+ export const radiusTokens = {
32
+ sm: 'var(--radius-sm)',
33
+ md: 'var(--radius-md)',
34
+ lg: 'var(--radius-lg)',
35
+ full: 'var(--radius-full)',
36
+ } as const
37
+
38
+ export type TColorToken = keyof typeof colorTokens
39
+ export type TSpaceToken = keyof typeof spaceTokens
40
+ export type TRadiusToken = keyof typeof radiusTokens
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 라우트 가드 — 로그인 필요 페이지는 이 컴포넌트로 감싼다.
3
+ * (규칙: 50-auth-http. 페이지 안에서 `if (!user) navigate(...)` 금지)
4
+ *
5
+ * 사용:
6
+ * <ProtectedRoute><MyPage /></ProtectedRoute>
7
+ * <ProtectedRoute requiredRoles={['ROLE_ADMIN']}><Admin /></ProtectedRoute>
8
+ *
9
+ * TODO: 아래 AuthContext 스텁을 프로젝트의 실제 인증 Provider와 연결하세요.
10
+ * (인증 훅은 'auth:logout' 이벤트를 구독해 로그아웃·리다이렉트를 처리해야 합니다)
11
+ */
12
+ import { createContext, useContext } from 'react'
13
+ import { Navigate, useLocation } from 'react-router-dom'
14
+
15
+ export type TRole = 'ROLE_USER' | 'ROLE_ADMIN'
16
+
17
+ export interface IAuthState {
18
+ isAuthenticated: boolean
19
+ isLoading: boolean
20
+ role: TRole | null
21
+ }
22
+
23
+ // 프로젝트에 이미 AuthContext가 있다면 이 스텁을 지우고 그것을 import 하세요
24
+ export const AuthContext = createContext<IAuthState>({
25
+ isAuthenticated: false,
26
+ isLoading: true,
27
+ role: null,
28
+ })
29
+
30
+ interface IProtectedRouteProps {
31
+ children: React.ReactNode
32
+ requiredAuth?: boolean
33
+ requiredRoles?: TRole[]
34
+ }
35
+
36
+ function ProtectedRoute({
37
+ children,
38
+ requiredAuth = true,
39
+ requiredRoles,
40
+ }: IProtectedRouteProps) {
41
+ const { isAuthenticated, isLoading, role } = useContext(AuthContext)
42
+ const location = useLocation()
43
+
44
+ // 인증 여부 확인 중에는 아무것도 렌더하지 않는다 (깜빡임 방지)
45
+ if (isLoading) {
46
+ return null
47
+ }
48
+
49
+ if (!isAuthenticated && requiredAuth) {
50
+ return <Navigate to="/login" state={{ from: location }} replace />
51
+ }
52
+
53
+ if (requiredRoles && requiredRoles.length > 0) {
54
+ if (!role || !requiredRoles.includes(role)) {
55
+ return <Navigate to="/" replace />
56
+ }
57
+ }
58
+
59
+ return <>{children}</>
60
+ }
61
+
62
+ export default ProtectedRoute