@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.
- package/bin/init-ai-rules.cjs +169 -78
- package/package.json +2 -2
- package/{.cursor/rules/documentation.mdc → rules/documentation.md} +1 -2
- package/{.cursor/rules/entities-layer.mdc → rules/entities-layer.md} +2 -1
- package/{.cursor/rules/git-commit.mdc → rules/git-commit.md} +1 -2
- package/{.cursor/rules/react-hooks.mdc → rules/react-hooks.md} +6 -3
- package/{.cursor/rules/shared-layer.mdc → rules/shared-layer.md} +2 -1
- package/rules/testing.md +117 -0
- package/{.cursor/rules/typescript-standards.mdc → rules/typescript-standards.md} +4 -3
- package/{.cursor/rules/views-layer.mdc → rules/views-layer.md} +2 -1
- package/.cursor/rules/project-overview.mdc +0 -44
- package/.cursor/rules/testing.mdc +0 -29
- /package/{.cursor/rules/fsd-architecture.mdc → rules/fsd-architecture.md} +0 -0
package/bin/init-ai-rules.cjs
CHANGED
|
@@ -7,18 +7,18 @@ const path = require("path");
|
|
|
7
7
|
const RULE_CATEGORIES = {
|
|
8
8
|
// 기본 규칙 (항상 포함)
|
|
9
9
|
base: [
|
|
10
|
-
"typescript-standards.
|
|
11
|
-
"react-hooks.
|
|
12
|
-
"testing.
|
|
13
|
-
"documentation.
|
|
14
|
-
"git-commit.
|
|
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.
|
|
19
|
-
"entities-layer.
|
|
20
|
-
"shared-layer.
|
|
21
|
-
"views-layer.
|
|
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/ 와
|
|
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("
|
|
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💡
|
|
79
|
+
|
|
80
|
+
console.log("\n💡 Cursor (.mdc)와 Claude (.md) 형식으로 각각 생성됩니다.");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function getRulesSourceDir() {
|
|
84
|
-
|
|
85
|
-
return path.join(__dirname, "..", ".cursor", "rules");
|
|
84
|
+
return path.join(__dirname, "..", "rules");
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
function
|
|
89
|
-
const
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
286
|
+
// Cursor 규칙 생성
|
|
287
|
+
console.log("🔧 Cursor 규칙 생성:\n");
|
|
288
|
+
const cursor = generateCursorRules(rulesToInstall, rulesSourceDir, targetDir);
|
|
187
289
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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✨ 완료! ${
|
|
298
|
+
console.log(`\n✨ 완료! ${totalInstalled}개 설치, ${totalSkipped}개 건너뜀`);
|
|
208
299
|
|
|
209
300
|
console.log("\n📁 생성된 파일:");
|
|
210
301
|
console.log(" ├── .cursor/rules/*.mdc (Cursor용)");
|
|
211
|
-
console.log(" └──
|
|
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.
|
|
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
|
-
"
|
|
14
|
+
"rules"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"prepare": "npm run build:tsc",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: React Hooks 개발 규칙 및 SSR 안전성
|
|
3
|
-
|
|
4
|
-
|
|
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
|
package/rules/testing.md
ADDED
|
@@ -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,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
|
-
- 새 기능 추가 시 반드시 테스트 포함
|
|
File without changes
|