@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.
@@ -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,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(" 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
+ 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
- if (fs.existsSync(targetPath)) {
98
- console.log(` ⏭️ ${ruleName} - 이미 존재함, 건너뜀`);
99
- return false;
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
- fs.copyFileSync(sourcePath, targetPath);
103
- console.log(` ✅ ${ruleName}`);
104
- return true;
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 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();
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
- return content.trim();
187
+
188
+ return { installed, skipped };
115
189
  }
116
190
 
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;
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
- let content = "# Project Development Rules\n\n";
126
- content += "이 파일은 Claude Code를 위한 프로젝트 규칙입니다.\n\n";
127
- content += "---\n\n";
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
- if (fs.existsSync(sourcePath)) {
132
- const ruleContent = extractMdcContent(sourcePath);
133
- content += ruleContent + "\n\n---\n\n";
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
- fs.writeFileSync(claudePath, content.trim() + "\n");
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
- let installedCount = 0;
186
- let skippedCount = 0;
265
+ // Cursor 규칙 생성
266
+ console.log("🔧 Cursor 규칙 생성:\n");
267
+ const cursor = generateCursorRules(rulesToInstall, rulesSourceDir, targetDir);
187
268
 
188
- for (const rule of rulesToInstall) {
189
- const installed = copyRule(rule, rulesSourceDir, rulesTargetDir);
190
- if (installed) {
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
- // 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
- }
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✨ 완료! ${installedCount}개 설치, ${skippedCount}개 건너뜀`);
277
+ console.log(`\n✨ 완료! ${totalInstalled}개 설치, ${totalSkipped}개 건너뜀`);
208
278
 
209
279
  console.log("\n📁 생성된 파일:");
210
280
  console.log(" ├── .cursor/rules/*.mdc (Cursor용)");
211
- console.log(" └── CLAUDE.md (Claude Code용)");
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.15.2",
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
- ".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,6 @@
1
1
  ---
2
2
  description: "Entities 레이어 구조 및 API 패턴"
3
- globs: ["**/entities/**"]
3
+ globs: "**/entities/**"
4
4
  ---
5
5
 
6
6
  # 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,5 +1,5 @@
1
1
  ---
2
- description: React Hooks 개발 규칙 및 SSR 안전성
2
+ description: "React Hooks 개발 규칙 및 SSR 안전성"
3
3
  globs: "**/use*.ts,**/use*.tsx"
4
4
  alwaysApply: false
5
5
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Shared 레이어 구조 및 lib vs utils 구분"
3
- globs: ["**/shared/**"]
3
+ globs: "**/shared/**"
4
4
  ---
5
5
 
6
6
  # Shared Layer (공유 레이어)
@@ -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,5 +1,5 @@
1
1
  ---
2
- description: TypeScript 코딩 표준 및 타입 규칙
2
+ description: "TypeScript 코딩 표준 및 타입 규칙"
3
3
  globs: "**/*.ts,**/*.tsx"
4
4
  alwaysApply: false
5
5
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Views 레이어 (페이지) 구조"
3
- globs: ["**/views/**"]
3
+ globs: "**/views/**"
4
4
  ---
5
5
 
6
6
  # 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
- - 새 기능 추가 시 반드시 테스트 포함