@blastlabs/utils 1.15.2 → 1.17.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.
@@ -7,18 +7,18 @@ const path = require("path");
7
7
  const RULE_CATEGORIES = {
8
8
  // 기본 규칙 (항상 포함)
9
9
  base: [
10
- "typescript-standards.mdc",
11
- "react-hooks.mdc",
12
- "testing.mdc",
13
- "documentation.mdc",
14
- "git-commit.mdc",
10
+ "typescript-standards.md",
11
+ "react-hooks.md",
12
+ "testing.md",
13
+ "documentation.md",
14
+ "git-commit.md",
15
15
  ],
16
16
  // FSD 아키텍처 규칙
17
17
  fsd: [
18
- "fsd-architecture.mdc",
19
- "entities-layer.mdc",
20
- "shared-layer.mdc",
21
- "views-layer.mdc",
18
+ "fsd-architecture.md",
19
+ "entities-layer.md",
20
+ "shared-layer.md",
21
+ "views-layer.md",
22
22
  ],
23
23
  };
24
24
 
@@ -47,7 +47,7 @@ function parseArgs(args) {
47
47
 
48
48
  function showHelp() {
49
49
  console.log("@blastlabs/utils - AI Rules Installer (Cursor + Claude)\n");
50
- console.log("프로젝트에 .cursor/rules/ 와 CLAUDE.md를 설치합니다.\n");
50
+ console.log("프로젝트에 .cursor/rules/ 와 .claude/rules/ 를 설치합니다.\n");
51
51
  console.log("사용법: npx blastlabs-init-ai-rules [options]\n");
52
52
  console.log("Options:");
53
53
  console.log(" --fsd FSD 아키텍처 규칙 포함");
@@ -60,83 +60,191 @@ function showHelp() {
60
60
  console.log(" npx blastlabs-init-ai-rules --all # 모든 규칙");
61
61
  console.log("\n생성되는 파일:");
62
62
  console.log(" .cursor/rules/*.mdc - Cursor용 규칙 파일들");
63
- console.log(" CLAUDE.md - Claude Code용 규칙 파일");
63
+ console.log(" .claude/rules/*.md - Claude Code용 규칙 파일들");
64
64
  console.log("\n⚠️ 프로젝트 루트 디렉토리에서 실행해주세요!");
65
65
  }
66
66
 
67
67
  function showList() {
68
68
  console.log("📋 설치 가능한 규칙 목록\n");
69
-
69
+
70
70
  console.log("📦 기본 규칙 (base) - 항상 포함:");
71
71
  for (const rule of RULE_CATEGORIES.base) {
72
72
  console.log(` ├── ${rule}`);
73
73
  }
74
-
74
+
75
75
  console.log("\n🏗️ FSD 아키텍처 규칙 (--fsd):");
76
76
  for (const rule of RULE_CATEGORIES.fsd) {
77
77
  console.log(` ├── ${rule}`);
78
78
  }
79
-
80
- console.log("\n💡 규칙은 globs 패턴으로 자동 적용됩니다.");
79
+
80
+ console.log("\n💡 Cursor (.mdc)와 Claude (.md) 형식으로 각각 생성됩니다.");
81
81
  }
82
82
 
83
83
  function getRulesSourceDir() {
84
- // 패키지 .cursor/rules 경로
85
- return path.join(__dirname, "..", ".cursor", "rules");
84
+ return path.join(__dirname, "..", "rules");
86
85
  }
87
86
 
88
- function copyRule(ruleName, sourceDir, targetDir) {
89
- const sourcePath = path.join(sourceDir, ruleName);
90
- const targetPath = path.join(targetDir, ruleName);
87
+ function parseFrontmatter(content) {
88
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
89
+ if (!match) {
90
+ return { frontmatter: {}, body: content };
91
+ }
91
92
 
92
- if (!fs.existsSync(sourcePath)) {
93
- console.log(` ⚠️ ${ruleName} - 소스 파일 없음, 건너뜀`);
94
- return false;
93
+ const yamlBlock = match[1];
94
+ const body = match[2];
95
+
96
+ const frontmatter = {};
97
+ let currentKey = null;
98
+
99
+ for (const line of yamlBlock.split("\n")) {
100
+ // 키-값 쌍 파싱
101
+ const kvMatch = line.match(/^(\w+):\s*(.*)$/);
102
+ if (kvMatch) {
103
+ currentKey = kvMatch[1];
104
+ let value = kvMatch[2].trim();
105
+
106
+ if (value === "true") value = true;
107
+ else if (value === "false") value = false;
108
+ else if (
109
+ (value.startsWith('"') && value.endsWith('"')) ||
110
+ (value.startsWith("'") && value.endsWith("'"))
111
+ ) {
112
+ value = value.slice(1, -1);
113
+ } else if (value === "") {
114
+ value = undefined;
115
+ }
116
+
117
+ if (value !== undefined) {
118
+ frontmatter[currentKey] = value;
119
+ }
120
+ } else if (line.startsWith(" -") && currentKey) {
121
+ // 배열 값 처리 (paths 등)
122
+ const arrayValueMatch = line.match(/^\s*-\s*"([^"]+)"\s*$/);
123
+ if (arrayValueMatch) {
124
+ const arrayValue = arrayValueMatch[1];
125
+ // 현재 키의 값이 배열이 아니면 배열로 초기화
126
+ if (!Array.isArray(frontmatter[currentKey])) {
127
+ frontmatter[currentKey] = [];
128
+ }
129
+ frontmatter[currentKey].push(arrayValue);
130
+ }
131
+ }
95
132
  }
96
133
 
97
- if (fs.existsSync(targetPath)) {
98
- console.log(` ⏭️ ${ruleName} - 이미 존재함, 건너뜀`);
99
- return false;
134
+ return { frontmatter, body };
135
+ }
136
+
137
+ function serializeCursorFrontmatter(frontmatter) {
138
+ let yaml = "---\n";
139
+ if (frontmatter.description) {
140
+ yaml += `description: ${frontmatter.description}\n`;
141
+ }
142
+ if (frontmatter.globs) {
143
+ yaml += `globs: "${frontmatter.globs}"\n`;
100
144
  }
145
+ if (frontmatter.alwaysApply !== undefined) {
146
+ yaml += `alwaysApply: ${frontmatter.alwaysApply}\n`;
147
+ }
148
+ yaml += "---\n";
149
+ return yaml;
150
+ }
101
151
 
102
- fs.copyFileSync(sourcePath, targetPath);
103
- console.log(` ✅ ${ruleName}`);
104
- return true;
152
+ function serializeClaudeFrontmatter(frontmatter) {
153
+ let yaml = "---\n";
154
+ if (frontmatter.description) {
155
+ yaml += `description: "${frontmatter.description}"\n`;
156
+ }
157
+ // paths는 항상 적용되지 않는 경우에만 포함
158
+ if (frontmatter.paths && frontmatter.alwaysApply !== true) {
159
+ if (Array.isArray(frontmatter.paths)) {
160
+ yaml += "paths:\n";
161
+ for (const p of frontmatter.paths) {
162
+ yaml += ` - "${p}"\n`;
163
+ }
164
+ } else {
165
+ yaml += `paths:\n - "${frontmatter.paths}"\n`;
166
+ }
167
+ }
168
+ // alwaysApply는 Claude에서 사용하지 않으므로 생략
169
+ yaml += "---\n";
170
+ return yaml;
105
171
  }
106
172
 
107
- function extractMdcContent(filePath) {
108
- // .mdc 파일에서 frontmatter 제거하고 내용만 추출
109
- const content = fs.readFileSync(filePath, "utf-8");
110
- const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n/);
111
- if (frontmatterMatch) {
112
- return content.slice(frontmatterMatch[0].length).trim();
173
+ function generateCursorRules(ruleFiles, sourceDir, targetDir) {
174
+ const cursorDir = path.join(targetDir, ".cursor", "rules");
175
+ if (!fs.existsSync(cursorDir)) {
176
+ fs.mkdirSync(cursorDir, { recursive: true });
113
177
  }
114
- return content.trim();
178
+
179
+ console.log(" .cursor/rules/");
180
+ let installed = 0;
181
+ let skipped = 0;
182
+
183
+ for (const ruleName of ruleFiles) {
184
+ const sourcePath = path.join(sourceDir, ruleName);
185
+ const targetName = ruleName.replace(/\.md$/, ".mdc");
186
+ const targetPath = path.join(cursorDir, targetName);
187
+
188
+ if (!fs.existsSync(sourcePath)) {
189
+ console.log(` ⚠️ ${targetName} - 소스 파일 없음, 건너뜀`);
190
+ skipped++;
191
+ continue;
192
+ }
193
+
194
+ if (fs.existsSync(targetPath)) {
195
+ console.log(` ⏭️ ${targetName} - 이미 존재함, 건너뜀`);
196
+ skipped++;
197
+ continue;
198
+ }
199
+
200
+ const content = fs.readFileSync(sourcePath, "utf-8");
201
+ const { frontmatter, body } = parseFrontmatter(content);
202
+ const output = serializeCursorFrontmatter(frontmatter) + body;
203
+
204
+ fs.writeFileSync(targetPath, output);
205
+ console.log(` ✅ ${targetName}`);
206
+ installed++;
207
+ }
208
+
209
+ return { installed, skipped };
115
210
  }
116
211
 
117
- function generateClaudeMd(ruleFiles, sourceDir, targetDir) {
118
- const claudePath = path.join(targetDir, "CLAUDE.md");
119
-
120
- if (fs.existsSync(claudePath)) {
121
- console.log(" ⏭️ CLAUDE.md - 이미 존재함, 건너뜀");
122
- return false;
212
+ function generateClaudeRules(ruleFiles, sourceDir, targetDir) {
213
+ const claudeDir = path.join(targetDir, ".claude", "rules");
214
+ if (!fs.existsSync(claudeDir)) {
215
+ fs.mkdirSync(claudeDir, { recursive: true });
123
216
  }
124
217
 
125
- let content = "# Project Development Rules\n\n";
126
- content += "이 파일은 Claude Code를 위한 프로젝트 규칙입니다.\n\n";
127
- content += "---\n\n";
218
+ console.log(" .claude/rules/");
219
+ let installed = 0;
220
+ let skipped = 0;
128
221
 
129
222
  for (const ruleName of ruleFiles) {
130
223
  const sourcePath = path.join(sourceDir, ruleName);
131
- if (fs.existsSync(sourcePath)) {
132
- const ruleContent = extractMdcContent(sourcePath);
133
- content += ruleContent + "\n\n---\n\n";
224
+ const targetPath = path.join(claudeDir, ruleName);
225
+
226
+ if (!fs.existsSync(sourcePath)) {
227
+ console.log(` ⚠️ ${ruleName} - 소스 파일 없음, 건너뜀`);
228
+ skipped++;
229
+ continue;
230
+ }
231
+
232
+ if (fs.existsSync(targetPath)) {
233
+ console.log(` ⏭️ ${ruleName} - 이미 존재함, 건너뜀`);
234
+ skipped++;
235
+ continue;
134
236
  }
237
+
238
+ const content = fs.readFileSync(sourcePath, "utf-8");
239
+ const { frontmatter, body } = parseFrontmatter(content);
240
+ const output = serializeClaudeFrontmatter(frontmatter) + body;
241
+
242
+ fs.writeFileSync(targetPath, output);
243
+ console.log(` ✅ ${ruleName}`);
244
+ installed++;
135
245
  }
136
246
 
137
- fs.writeFileSync(claudePath, content.trim() + "\n");
138
- console.log(" ✅ CLAUDE.md");
139
- return true;
247
+ return { installed, skipped };
140
248
  }
141
249
 
142
250
  function main() {
@@ -154,7 +262,6 @@ function main() {
154
262
  }
155
263
 
156
264
  const targetDir = process.cwd();
157
- const rulesTargetDir = path.join(targetDir, ".cursor", "rules");
158
265
  const rulesSourceDir = getRulesSourceDir();
159
266
 
160
267
  // 소스 디렉토리 확인
@@ -164,12 +271,6 @@ function main() {
164
271
  process.exit(1);
165
272
  }
166
273
 
167
- // .cursor/rules 디렉토리 생성
168
- if (!fs.existsSync(rulesTargetDir)) {
169
- fs.mkdirSync(rulesTargetDir, { recursive: true });
170
- console.log("📁 .cursor/rules/ 디렉토리 생성됨\n");
171
- }
172
-
173
274
  // 설치할 규칙 목록 결정
174
275
  let rulesToInstall = [...RULE_CATEGORIES.base];
175
276
 
@@ -182,33 +283,23 @@ function main() {
182
283
 
183
284
  console.log("📥 규칙 설치 중...\n");
184
285
 
185
- let installedCount = 0;
186
- let skippedCount = 0;
286
+ // Cursor 규칙 생성
287
+ console.log("🔧 Cursor 규칙 생성:\n");
288
+ const cursor = generateCursorRules(rulesToInstall, rulesSourceDir, targetDir);
187
289
 
188
- for (const rule of rulesToInstall) {
189
- const installed = copyRule(rule, rulesSourceDir, rulesTargetDir);
190
- if (installed) {
191
- installedCount++;
192
- } else {
193
- skippedCount++;
194
- }
195
- }
290
+ // Claude 규칙 생성
291
+ console.log("\n🤖 Claude 규칙 생성:\n");
292
+ const claude = generateClaudeRules(rulesToInstall, rulesSourceDir, targetDir);
196
293
 
197
- // CLAUDE.md 생성
198
- console.log("\n📄 CLAUDE.md 생성 중...\n");
199
- const claudeCreated = generateClaudeMd(rulesToInstall, rulesSourceDir, targetDir);
200
- if (claudeCreated) {
201
- installedCount++;
202
- } else {
203
- skippedCount++;
204
- }
294
+ const totalInstalled = cursor.installed + claude.installed;
295
+ const totalSkipped = cursor.skipped + claude.skipped;
205
296
 
206
297
  console.log("\n" + "─".repeat(40));
207
- console.log(`\n✨ 완료! ${installedCount}개 설치, ${skippedCount}개 건너뜀`);
298
+ console.log(`\n✨ 완료! ${totalInstalled}개 설치, ${totalSkipped}개 건너뜀`);
208
299
 
209
300
  console.log("\n📁 생성된 파일:");
210
301
  console.log(" ├── .cursor/rules/*.mdc (Cursor용)");
211
- console.log(" └── CLAUDE.md (Claude Code용)");
302
+ console.log(" └── .claude/rules/*.md (Claude Code용)");
212
303
 
213
304
  console.log("\n📦 포함된 규칙:");
214
305
  console.log(" ├── base (기본 규칙)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blastlabs/utils",
3
- "version": "1.15.2",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "bin",
14
- ".cursor/rules"
14
+ "rules"
15
15
  ],
16
16
  "scripts": {
17
17
  "prepare": "npm run build:tsc",
@@ -1,6 +1,5 @@
1
1
  ---
2
- description: 문서화 가이드라인
3
- globs:
2
+ description: "문서화 가이드라인"
4
3
  alwaysApply: false
5
4
  ---
6
5
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: "Entities 레이어 구조 및 API 패턴"
3
- globs: ["**/entities/**"]
3
+ paths:
4
+ - "**/entities/**"
4
5
  ---
5
6
 
6
7
  # Entities Layer (엔티티)
@@ -1,6 +1,5 @@
1
1
  ---
2
- description: Git 커밋 컨벤션
3
- globs:
2
+ description: "Git 커밋 컨벤션"
4
3
  alwaysApply: false
5
4
  ---
6
5
 
@@ -1,7 +1,10 @@
1
1
  ---
2
- description: React Hooks 개발 규칙 및 SSR 안전성
3
- globs: "**/use*.ts,**/use*.tsx"
4
- alwaysApply: false
2
+ description: "React Hooks 개발 규칙 및 SSR 안전성"
3
+ paths:
4
+ - "src/hooks/**/*.ts"
5
+ - "src/hooks/**/*.tsx"
6
+ - "**/use*.ts"
7
+ - "**/use*.tsx"
5
8
  ---
6
9
 
7
10
  # React Hooks Rules
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: "Shared 레이어 구조 및 lib vs utils 구분"
3
- globs: ["**/shared/**"]
3
+ paths:
4
+ - "**/shared/**"
4
5
  ---
5
6
 
6
7
  # Shared Layer (공유 레이어)
@@ -0,0 +1,117 @@
1
+ ---
2
+ description: "테스트 작성 가이드라인"
3
+ paths:
4
+ - "**/*.test.ts"
5
+ - "**/*.test.tsx"
6
+ - "**/__tests__/**"
7
+ ---
8
+
9
+ # Testing Guidelines
10
+
11
+ ## 테스트 도구
12
+
13
+ - Vitest 사용
14
+ - @testing-library/react 사용
15
+
16
+ ## 테스트 전략
17
+
18
+ ### 테스트 작성 원칙
19
+
20
+ #### 컴포넌트 (Component) - 단위 테스트 위주
21
+
22
+ - **대상**: `shared/ui/component/`, `views/*/ui/`, `views/*/components/` 등 재사용 가능한 컴포넌트
23
+ - **목적**: 컴포넌트의 독립적인 동작 검증
24
+ - **방법**:
25
+ - 컴포넌트를 격리하여 테스트
26
+ - Props와 사용자 상호작용에 집중
27
+ - 외부 의존성(API, 라우팅 등)은 mock 처리
28
+ - 렌더링, 이벤트 핸들링, 상태 변경 등을 검증
29
+ - **예시**:
30
+ ```typescript
31
+ // shared/ui/component/button/button.test.tsx
32
+ // views/seller/ui/seller-login-info-form.test.tsx
33
+ ```
34
+
35
+ #### 페이지 (Page) - 기능 테스트 위주
36
+
37
+ - **대상**: `views/*/page.tsx` 등 페이지 레벨 컴포넌트
38
+ - **목적**: 사용자 시나리오와 비즈니스 로직 검증
39
+ - **방법**:
40
+ - 실제 컴포넌트를 렌더링하여 통합 테스트
41
+ - 외부 의존성(API, toast)만 mock 처리
42
+ - 사용자 플로우 전체를 검증
43
+ - 여러 컴포넌트 간 상호작용 검증
44
+ - **Mock 최소화 원칙**:
45
+ - 외부 의존성(API 호출, toast)만 mock
46
+ - `shared` 패키지 컴포넌트는 실제로 렌더링
47
+ - 내부 컴포넌트도 실제로 렌더링하여 통합 테스트
48
+ - `data-testid` 대신 `getByRole`, `getByText` 등 접근성 기반 쿼리 사용
49
+
50
+ ### Testing Library 쿼리 우선순위
51
+
52
+ Testing Library에서는 요소를 찾을 때 사용자가 코드와 상호작용하는 방식과 최대한 비슷해야 하므로, 아래와 같은 우선순위로 쿼리를 사용해야 합니다:
53
+
54
+ **1순위 (가장 권장):**
55
+
56
+ - `getByRole` - 접근성 역할 기반 (button, textbox, checkbox 등)
57
+ - `getByLabelText` - label과 연결된 폼 요소
58
+ - `getByPlaceholderText` - placeholder가 있는 입력 필드
59
+ - `getByText` - 텍스트 내용으로 찾기
60
+ - `getByDisplayValue` - 입력 필드의 현재 값으로 찾기
61
+
62
+ **2순위:**
63
+
64
+ - `getByAltText` - 이미지의 alt 텍스트
65
+ - `getByTitle` - title 속성
66
+
67
+ **3순위 (최후의 수단):**
68
+
69
+ - `getByTestId` - `data-testid` 속성 사용 (가능한 한 피해야 함)
70
+
71
+ **예시:**
72
+
73
+ ```typescript
74
+ // ✅ 좋은 예
75
+ screen.getByRole('button', { name: '저장' });
76
+ screen.getByRole('checkbox');
77
+ screen.getByText('셀러 정보를 찾을 수 없습니다.');
78
+ screen.getByLabelText(/대표자명/);
79
+
80
+ // ❌ 나쁜 예 (가능한 한 피해야 함)
81
+ screen.getByTestId('save-button');
82
+ screen.getByTestId('checkbox');
83
+ ```
84
+
85
+ - **예시**:
86
+ ```typescript
87
+ // views/seller/seller-support-edit-page.test.tsx
88
+ // views/seller/seller-store-edit-page.test.tsx
89
+ ```
90
+
91
+ ### 테스트 파일 위치
92
+
93
+ - 컴포넌트 테스트: 컴포넌트와 같은 디렉토리에 `*.test.tsx`
94
+ - 페이지 테스트: 페이지와 같은 디렉토리에 `*.test.tsx`
95
+
96
+ ### 테스트 유틸리티
97
+
98
+ - `apps/admin/src/test/test-utils.tsx`: 공통 테스트 헬퍼 함수
99
+ - `createTestQueryClient()`: QueryClient 생성
100
+ - `renderWithProviders()`: Provider 래핑된 렌더링 헬퍼
101
+
102
+ ## 테스트 원칙
103
+
104
+ - 모든 hooks에 대해 포괄적인 테스트 작성
105
+ - SSR 시나리오 테스트 포함
106
+ - 높은 테스트 커버리지 목표
107
+
108
+ ## 테스트 실행
109
+
110
+ ```bash
111
+ npm test
112
+ ```
113
+
114
+ ## 필수 사항
115
+
116
+ - **Always** write tests for new features
117
+ - 새 기능 추가 시 반드시 테스트 포함
@@ -1,7 +1,8 @@
1
1
  ---
2
- description: TypeScript 코딩 표준 및 타입 규칙
3
- globs: "**/*.ts,**/*.tsx"
4
- alwaysApply: false
2
+ description: "TypeScript 코딩 표준 및 타입 규칙"
3
+ paths:
4
+ - "**/*.ts"
5
+ - "**/*.tsx"
5
6
  ---
6
7
 
7
8
  # TypeScript Standards
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: "Views 레이어 (페이지) 구조"
3
- globs: ["**/views/**"]
3
+ paths:
4
+ - "**/views/**"
4
5
  ---
5
6
 
6
7
  # Views Layer (페이지 레이어)
@@ -1,44 +0,0 @@
1
- ---
2
- description: @blastlabs/utils 프로젝트 개요 및 기본 가이드라인
3
- globs:
4
- alwaysApply: true
5
- ---
6
-
7
- # @blastlabs/utils Development Rules
8
-
9
- React 애플리케이션을 위한 종합 TypeScript 유틸리티 라이브러리입니다.
10
-
11
- ## 제공 기능
12
-
13
- - Authentication management (useAuth, AuthGuard)
14
- - UI/UX hooks (useTabs, useMediaQuery, useWindowSize)
15
- - Form management (useCRUDForm)
16
- - Time utilities (useCountdown, useStopwatch, useInterval)
17
- - Storage hooks (useLocalStorage, useSessionStorage, useCopyToClipboard)
18
- - State management hooks (useToggle, usePrevious)
19
- - Performance hooks (useDebounce, useThrottle, useIntersectionObserver)
20
- - Event hooks (useEventListener, useClickOutside)
21
-
22
- ## 문서
23
-
24
- - [Development Guide](docs/development-guide.md) - 개발 가이드 통합 문서
25
- - [Authentication Hooks](docs/auth-hooks.md) - useAuth, AuthGuard
26
- - [UI Hooks](docs/ui-hooks.md) - useTabs, useMediaQuery, useWindowSize
27
-
28
- ## 파일 구조
29
-
30
- ```
31
- src/
32
- ├── hooks/
33
- │ ├── auth/ # Authentication hooks
34
- │ ├── ui/ # UI/UX hooks
35
- │ ├── time/ # Time-related hooks
36
- │ ├── form/ # Form management hooks
37
- │ ├── storage/ # Storage hooks
38
- │ ├── state/ # State management hooks
39
- │ ├── performance/ # Performance hooks
40
- │ └── event/ # Event hooks
41
- └── components/
42
- ├── auth/ # Auth components (AuthGuard)
43
- └── dev/ # Development components
44
- ```
@@ -1,29 +0,0 @@
1
- ---
2
- description: 테스트 작성 가이드라인
3
- globs: "**/*.test.ts,**/*.test.tsx,**/__tests__/**"
4
- alwaysApply: false
5
- ---
6
-
7
- # Testing Guidelines
8
-
9
- ## 테스트 도구
10
-
11
- - Vitest 사용
12
- - @testing-library/react 사용
13
-
14
- ## 테스트 원칙
15
-
16
- - 모든 hooks에 대해 포괄적인 테스트 작성
17
- - SSR 시나리오 테스트 포함
18
- - 높은 테스트 커버리지 목표
19
-
20
- ## 테스트 실행
21
-
22
- ```bash
23
- npm test
24
- ```
25
-
26
- ## 필수 사항
27
-
28
- - **Always** write tests for new features
29
- - 새 기능 추가 시 반드시 테스트 포함