@blastlabs/utils 1.15.2 → 1.16.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 +148 -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} +1 -1
- package/{.cursor/rules/git-commit.mdc → rules/git-commit.md} +1 -2
- package/{.cursor/rules/react-hooks.mdc → rules/react-hooks.md} +1 -1
- package/{.cursor/rules/shared-layer.mdc → rules/shared-layer.md} +1 -1
- package/rules/testing.md +115 -0
- package/{.cursor/rules/typescript-standards.mdc → rules/typescript-standards.md} +1 -1
- package/{.cursor/rules/views-layer.mdc → rules/views-layer.md} +1 -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,170 @@ 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
|
+
for (const line of yamlBlock.split("\n")) {
|
|
98
|
+
const kvMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
99
|
+
if (kvMatch) {
|
|
100
|
+
const key = kvMatch[1];
|
|
101
|
+
let value = kvMatch[2].trim();
|
|
102
|
+
|
|
103
|
+
if (value === "true") value = true;
|
|
104
|
+
else if (value === "false") value = false;
|
|
105
|
+
else if (
|
|
106
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
107
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
108
|
+
) {
|
|
109
|
+
value = value.slice(1, -1);
|
|
110
|
+
} else if (value === "") {
|
|
111
|
+
value = undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (value !== undefined) {
|
|
115
|
+
frontmatter[key] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
return { frontmatter, body };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function serializeCursorFrontmatter(frontmatter) {
|
|
124
|
+
let yaml = "---\n";
|
|
125
|
+
if (frontmatter.description) {
|
|
126
|
+
yaml += `description: ${frontmatter.description}\n`;
|
|
127
|
+
}
|
|
128
|
+
if (frontmatter.globs) {
|
|
129
|
+
yaml += `globs: "${frontmatter.globs}"\n`;
|
|
130
|
+
}
|
|
131
|
+
if (frontmatter.alwaysApply !== undefined) {
|
|
132
|
+
yaml += `alwaysApply: ${frontmatter.alwaysApply}\n`;
|
|
100
133
|
}
|
|
134
|
+
yaml += "---\n";
|
|
135
|
+
return yaml;
|
|
136
|
+
}
|
|
101
137
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
function serializeClaudeFrontmatter(frontmatter) {
|
|
139
|
+
let yaml = "---\n";
|
|
140
|
+
if (frontmatter.description) {
|
|
141
|
+
yaml += `description: "${frontmatter.description}"\n`;
|
|
142
|
+
}
|
|
143
|
+
// globs는 alwaysApply가 true가 아닌 경우에만 포함
|
|
144
|
+
if (frontmatter.globs && frontmatter.alwaysApply !== true) {
|
|
145
|
+
yaml += `globs: "${frontmatter.globs}"\n`;
|
|
146
|
+
}
|
|
147
|
+
// alwaysApply는 Claude에서 사용하지 않으므로 생략
|
|
148
|
+
yaml += "---\n";
|
|
149
|
+
return yaml;
|
|
105
150
|
}
|
|
106
151
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
152
|
+
function generateCursorRules(ruleFiles, sourceDir, targetDir) {
|
|
153
|
+
const cursorDir = path.join(targetDir, ".cursor", "rules");
|
|
154
|
+
if (!fs.existsSync(cursorDir)) {
|
|
155
|
+
fs.mkdirSync(cursorDir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(" .cursor/rules/");
|
|
159
|
+
let installed = 0;
|
|
160
|
+
let skipped = 0;
|
|
161
|
+
|
|
162
|
+
for (const ruleName of ruleFiles) {
|
|
163
|
+
const sourcePath = path.join(sourceDir, ruleName);
|
|
164
|
+
const targetName = ruleName.replace(/\.md$/, ".mdc");
|
|
165
|
+
const targetPath = path.join(cursorDir, targetName);
|
|
166
|
+
|
|
167
|
+
if (!fs.existsSync(sourcePath)) {
|
|
168
|
+
console.log(` ⚠️ ${targetName} - 소스 파일 없음, 건너뜀`);
|
|
169
|
+
skipped++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (fs.existsSync(targetPath)) {
|
|
174
|
+
console.log(` ⏭️ ${targetName} - 이미 존재함, 건너뜀`);
|
|
175
|
+
skipped++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const content = fs.readFileSync(sourcePath, "utf-8");
|
|
180
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
181
|
+
const output = serializeCursorFrontmatter(frontmatter) + body;
|
|
182
|
+
|
|
183
|
+
fs.writeFileSync(targetPath, output);
|
|
184
|
+
console.log(` ✅ ${targetName}`);
|
|
185
|
+
installed++;
|
|
113
186
|
}
|
|
114
|
-
|
|
187
|
+
|
|
188
|
+
return { installed, skipped };
|
|
115
189
|
}
|
|
116
190
|
|
|
117
|
-
function
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
console.log(" ⏭️ CLAUDE.md - 이미 존재함, 건너뜀");
|
|
122
|
-
return false;
|
|
191
|
+
function generateClaudeRules(ruleFiles, sourceDir, targetDir) {
|
|
192
|
+
const claudeDir = path.join(targetDir, ".claude", "rules");
|
|
193
|
+
if (!fs.existsSync(claudeDir)) {
|
|
194
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
123
195
|
}
|
|
124
196
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
197
|
+
console.log(" .claude/rules/");
|
|
198
|
+
let installed = 0;
|
|
199
|
+
let skipped = 0;
|
|
128
200
|
|
|
129
201
|
for (const ruleName of ruleFiles) {
|
|
130
202
|
const sourcePath = path.join(sourceDir, ruleName);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
203
|
+
const targetPath = path.join(claudeDir, ruleName);
|
|
204
|
+
|
|
205
|
+
if (!fs.existsSync(sourcePath)) {
|
|
206
|
+
console.log(` ⚠️ ${ruleName} - 소스 파일 없음, 건너뜀`);
|
|
207
|
+
skipped++;
|
|
208
|
+
continue;
|
|
134
209
|
}
|
|
210
|
+
|
|
211
|
+
if (fs.existsSync(targetPath)) {
|
|
212
|
+
console.log(` ⏭️ ${ruleName} - 이미 존재함, 건너뜀`);
|
|
213
|
+
skipped++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const content = fs.readFileSync(sourcePath, "utf-8");
|
|
218
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
219
|
+
const output = serializeClaudeFrontmatter(frontmatter) + body;
|
|
220
|
+
|
|
221
|
+
fs.writeFileSync(targetPath, output);
|
|
222
|
+
console.log(` ✅ ${ruleName}`);
|
|
223
|
+
installed++;
|
|
135
224
|
}
|
|
136
225
|
|
|
137
|
-
|
|
138
|
-
console.log(" ✅ CLAUDE.md");
|
|
139
|
-
return true;
|
|
226
|
+
return { installed, skipped };
|
|
140
227
|
}
|
|
141
228
|
|
|
142
229
|
function main() {
|
|
@@ -154,7 +241,6 @@ function main() {
|
|
|
154
241
|
}
|
|
155
242
|
|
|
156
243
|
const targetDir = process.cwd();
|
|
157
|
-
const rulesTargetDir = path.join(targetDir, ".cursor", "rules");
|
|
158
244
|
const rulesSourceDir = getRulesSourceDir();
|
|
159
245
|
|
|
160
246
|
// 소스 디렉토리 확인
|
|
@@ -164,12 +250,6 @@ function main() {
|
|
|
164
250
|
process.exit(1);
|
|
165
251
|
}
|
|
166
252
|
|
|
167
|
-
// .cursor/rules 디렉토리 생성
|
|
168
|
-
if (!fs.existsSync(rulesTargetDir)) {
|
|
169
|
-
fs.mkdirSync(rulesTargetDir, { recursive: true });
|
|
170
|
-
console.log("📁 .cursor/rules/ 디렉토리 생성됨\n");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
253
|
// 설치할 규칙 목록 결정
|
|
174
254
|
let rulesToInstall = [...RULE_CATEGORIES.base];
|
|
175
255
|
|
|
@@ -182,33 +262,23 @@ function main() {
|
|
|
182
262
|
|
|
183
263
|
console.log("📥 규칙 설치 중...\n");
|
|
184
264
|
|
|
185
|
-
|
|
186
|
-
|
|
265
|
+
// Cursor 규칙 생성
|
|
266
|
+
console.log("🔧 Cursor 규칙 생성:\n");
|
|
267
|
+
const cursor = generateCursorRules(rulesToInstall, rulesSourceDir, targetDir);
|
|
187
268
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
installedCount++;
|
|
192
|
-
} else {
|
|
193
|
-
skippedCount++;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
269
|
+
// Claude 규칙 생성
|
|
270
|
+
console.log("\n🤖 Claude 규칙 생성:\n");
|
|
271
|
+
const claude = generateClaudeRules(rulesToInstall, rulesSourceDir, targetDir);
|
|
196
272
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const claudeCreated = generateClaudeMd(rulesToInstall, rulesSourceDir, targetDir);
|
|
200
|
-
if (claudeCreated) {
|
|
201
|
-
installedCount++;
|
|
202
|
-
} else {
|
|
203
|
-
skippedCount++;
|
|
204
|
-
}
|
|
273
|
+
const totalInstalled = cursor.installed + claude.installed;
|
|
274
|
+
const totalSkipped = cursor.skipped + claude.skipped;
|
|
205
275
|
|
|
206
276
|
console.log("\n" + "─".repeat(40));
|
|
207
|
-
console.log(`\n✨ 완료! ${
|
|
277
|
+
console.log(`\n✨ 완료! ${totalInstalled}개 설치, ${totalSkipped}개 건너뜀`);
|
|
208
278
|
|
|
209
279
|
console.log("\n📁 생성된 파일:");
|
|
210
280
|
console.log(" ├── .cursor/rules/*.mdc (Cursor용)");
|
|
211
|
-
console.log(" └──
|
|
281
|
+
console.log(" └── .claude/rules/*.md (Claude Code용)");
|
|
212
282
|
|
|
213
283
|
console.log("\n📦 포함된 규칙:");
|
|
214
284
|
console.log(" ├── base (기본 규칙)");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blastlabs/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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",
|
package/rules/testing.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
### 테스트 작성 원칙
|
|
17
|
+
|
|
18
|
+
#### 컴포넌트 (Component) - 단위 테스트 위주
|
|
19
|
+
|
|
20
|
+
- **대상**: `shared/ui/component/`, `views/*/ui/`, `views/*/components/` 등 재사용 가능한 컴포넌트
|
|
21
|
+
- **목적**: 컴포넌트의 독립적인 동작 검증
|
|
22
|
+
- **방법**:
|
|
23
|
+
- 컴포넌트를 격리하여 테스트
|
|
24
|
+
- Props와 사용자 상호작용에 집중
|
|
25
|
+
- 외부 의존성(API, 라우팅 등)은 mock 처리
|
|
26
|
+
- 렌더링, 이벤트 핸들링, 상태 변경 등을 검증
|
|
27
|
+
- **예시**:
|
|
28
|
+
```typescript
|
|
29
|
+
// shared/ui/component/button/button.test.tsx
|
|
30
|
+
// views/seller/ui/seller-login-info-form.test.tsx
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### 페이지 (Page) - 기능 테스트 위주
|
|
34
|
+
|
|
35
|
+
- **대상**: `views/*/page.tsx` 등 페이지 레벨 컴포넌트
|
|
36
|
+
- **목적**: 사용자 시나리오와 비즈니스 로직 검증
|
|
37
|
+
- **방법**:
|
|
38
|
+
- 실제 컴포넌트를 렌더링하여 통합 테스트
|
|
39
|
+
- 외부 의존성(API, toast)만 mock 처리
|
|
40
|
+
- 사용자 플로우 전체를 검증
|
|
41
|
+
- 여러 컴포넌트 간 상호작용 검증
|
|
42
|
+
- **Mock 최소화 원칙**:
|
|
43
|
+
- 외부 의존성(API 호출, toast)만 mock
|
|
44
|
+
- `shared` 패키지 컴포넌트는 실제로 렌더링
|
|
45
|
+
- 내부 컴포넌트도 실제로 렌더링하여 통합 테스트
|
|
46
|
+
- `data-testid` 대신 `getByRole`, `getByText` 등 접근성 기반 쿼리 사용
|
|
47
|
+
|
|
48
|
+
### Testing Library 쿼리 우선순위
|
|
49
|
+
|
|
50
|
+
Testing Library에서는 요소를 찾을 때 사용자가 코드와 상호작용하는 방식과 최대한 비슷해야 하므로, 아래와 같은 우선순위로 쿼리를 사용해야 합니다:
|
|
51
|
+
|
|
52
|
+
**1순위 (가장 권장):**
|
|
53
|
+
|
|
54
|
+
- `getByRole` - 접근성 역할 기반 (button, textbox, checkbox 등)
|
|
55
|
+
- `getByLabelText` - label과 연결된 폼 요소
|
|
56
|
+
- `getByPlaceholderText` - placeholder가 있는 입력 필드
|
|
57
|
+
- `getByText` - 텍스트 내용으로 찾기
|
|
58
|
+
- `getByDisplayValue` - 입력 필드의 현재 값으로 찾기
|
|
59
|
+
|
|
60
|
+
**2순위:**
|
|
61
|
+
|
|
62
|
+
- `getByAltText` - 이미지의 alt 텍스트
|
|
63
|
+
- `getByTitle` - title 속성
|
|
64
|
+
|
|
65
|
+
**3순위 (최후의 수단):**
|
|
66
|
+
|
|
67
|
+
- `getByTestId` - `data-testid` 속성 사용 (가능한 한 피해야 함)
|
|
68
|
+
|
|
69
|
+
**예시:**
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// ✅ 좋은 예
|
|
73
|
+
screen.getByRole('button', { name: '저장' });
|
|
74
|
+
screen.getByRole('checkbox');
|
|
75
|
+
screen.getByText('셀러 정보를 찾을 수 없습니다.');
|
|
76
|
+
screen.getByLabelText(/대표자명/);
|
|
77
|
+
|
|
78
|
+
// ❌ 나쁜 예 (가능한 한 피해야 함)
|
|
79
|
+
screen.getByTestId('save-button');
|
|
80
|
+
screen.getByTestId('checkbox');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- **예시**:
|
|
84
|
+
```typescript
|
|
85
|
+
// views/seller/seller-support-edit-page.test.tsx
|
|
86
|
+
// views/seller/seller-store-edit-page.test.tsx
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 테스트 파일 위치
|
|
90
|
+
|
|
91
|
+
- 컴포넌트 테스트: 컴포넌트와 같은 디렉토리에 `*.test.tsx`
|
|
92
|
+
- 페이지 테스트: 페이지와 같은 디렉토리에 `*.test.tsx`
|
|
93
|
+
|
|
94
|
+
### 테스트 유틸리티
|
|
95
|
+
|
|
96
|
+
- `apps/admin/src/test/test-utils.tsx`: 공통 테스트 헬퍼 함수
|
|
97
|
+
- `createTestQueryClient()`: QueryClient 생성
|
|
98
|
+
- `renderWithProviders()`: Provider 래핑된 렌더링 헬퍼
|
|
99
|
+
|
|
100
|
+
## 테스트 원칙
|
|
101
|
+
|
|
102
|
+
- 모든 hooks에 대해 포괄적인 테스트 작성
|
|
103
|
+
- SSR 시나리오 테스트 포함
|
|
104
|
+
- 높은 테스트 커버리지 목표
|
|
105
|
+
|
|
106
|
+
## 테스트 실행
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm test
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 필수 사항
|
|
113
|
+
|
|
114
|
+
- **Always** write tests for new features
|
|
115
|
+
- 새 기능 추가 시 반드시 테스트 포함
|
|
@@ -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
|