@blastlabs/utils 1.15.1 → 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
 
@@ -46,8 +46,8 @@ function parseArgs(args) {
46
46
  }
47
47
 
48
48
  function showHelp() {
49
- console.log("@blastlabs/utils - Cursor Rules Installer\n");
50
- console.log("프로젝트에 .cursor/rules/ 규칙 파일들을 설치합니다.\n");
49
+ console.log("@blastlabs/utils - AI Rules Installer (Cursor + Claude)\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 아키텍처 규칙 포함");
@@ -58,47 +58,172 @@ function showHelp() {
58
58
  console.log(" npx blastlabs-init-ai-rules # 기본 규칙만");
59
59
  console.log(" npx blastlabs-init-ai-rules --fsd # 기본 + FSD 규칙");
60
60
  console.log(" npx blastlabs-init-ai-rules --all # 모든 규칙");
61
+ console.log("\n생성되는 파일:");
62
+ console.log(" .cursor/rules/*.mdc - Cursor용 규칙 파일들");
63
+ console.log(" .claude/rules/*.md - Claude Code용 규칙 파일들");
61
64
  console.log("\n⚠️ 프로젝트 루트 디렉토리에서 실행해주세요!");
62
65
  }
63
66
 
64
67
  function showList() {
65
68
  console.log("📋 설치 가능한 규칙 목록\n");
66
-
69
+
67
70
  console.log("📦 기본 규칙 (base) - 항상 포함:");
68
71
  for (const rule of RULE_CATEGORIES.base) {
69
72
  console.log(` ├── ${rule}`);
70
73
  }
71
-
74
+
72
75
  console.log("\n🏗️ FSD 아키텍처 규칙 (--fsd):");
73
76
  for (const rule of RULE_CATEGORIES.fsd) {
74
77
  console.log(` ├── ${rule}`);
75
78
  }
76
-
77
- console.log("\n💡 규칙은 globs 패턴으로 자동 적용됩니다.");
79
+
80
+ console.log("\n💡 Cursor (.mdc)와 Claude (.md) 형식으로 각각 생성됩니다.");
78
81
  }
79
82
 
80
83
  function getRulesSourceDir() {
81
- // 패키지 .cursor/rules 경로
82
- return path.join(__dirname, "..", ".cursor", "rules");
84
+ return path.join(__dirname, "..", "rules");
83
85
  }
84
86
 
85
- function copyRule(ruleName, sourceDir, targetDir) {
86
- const sourcePath = path.join(sourceDir, ruleName);
87
- 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
+ }
92
+
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();
88
102
 
89
- if (!fs.existsSync(sourcePath)) {
90
- console.log(` ⚠️ ${ruleName} - 소스 파일 없음, 건너뜀`);
91
- return false;
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
+ }
92
118
  }
93
119
 
94
- if (fs.existsSync(targetPath)) {
95
- console.log(` ⏭️ ${ruleName} - 이미 존재함, 건너뜀`);
96
- 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`;
97
133
  }
134
+ yaml += "---\n";
135
+ return yaml;
136
+ }
98
137
 
99
- fs.copyFileSync(sourcePath, targetPath);
100
- console.log(` ✅ ${ruleName}`);
101
- 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;
150
+ }
151
+
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++;
186
+ }
187
+
188
+ return { installed, skipped };
189
+ }
190
+
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 });
195
+ }
196
+
197
+ console.log(" .claude/rules/");
198
+ let installed = 0;
199
+ let skipped = 0;
200
+
201
+ for (const ruleName of ruleFiles) {
202
+ const sourcePath = path.join(sourceDir, ruleName);
203
+ const targetPath = path.join(claudeDir, ruleName);
204
+
205
+ if (!fs.existsSync(sourcePath)) {
206
+ console.log(` ⚠️ ${ruleName} - 소스 파일 없음, 건너뜀`);
207
+ skipped++;
208
+ continue;
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++;
224
+ }
225
+
226
+ return { installed, skipped };
102
227
  }
103
228
 
104
229
  function main() {
@@ -116,7 +241,6 @@ function main() {
116
241
  }
117
242
 
118
243
  const targetDir = process.cwd();
119
- const rulesTargetDir = path.join(targetDir, ".cursor", "rules");
120
244
  const rulesSourceDir = getRulesSourceDir();
121
245
 
122
246
  // 소스 디렉토리 확인
@@ -126,12 +250,6 @@ function main() {
126
250
  process.exit(1);
127
251
  }
128
252
 
129
- // .cursor/rules 디렉토리 생성
130
- if (!fs.existsSync(rulesTargetDir)) {
131
- fs.mkdirSync(rulesTargetDir, { recursive: true });
132
- console.log("📁 .cursor/rules/ 디렉토리 생성됨\n");
133
- }
134
-
135
253
  // 설치할 규칙 목록 결정
136
254
  let rulesToInstall = [...RULE_CATEGORIES.base];
137
255
 
@@ -144,31 +262,31 @@ function main() {
144
262
 
145
263
  console.log("📥 규칙 설치 중...\n");
146
264
 
147
- let installedCount = 0;
148
- let skippedCount = 0;
265
+ // Cursor 규칙 생성
266
+ console.log("🔧 Cursor 규칙 생성:\n");
267
+ const cursor = generateCursorRules(rulesToInstall, rulesSourceDir, targetDir);
149
268
 
150
- for (const rule of rulesToInstall) {
151
- const installed = copyRule(rule, rulesSourceDir, rulesTargetDir);
152
- if (installed) {
153
- installedCount++;
154
- } else {
155
- skippedCount++;
156
- }
157
- }
269
+ // Claude 규칙 생성
270
+ console.log("\n🤖 Claude 규칙 생성:\n");
271
+ const claude = generateClaudeRules(rulesToInstall, rulesSourceDir, targetDir);
272
+
273
+ const totalInstalled = cursor.installed + claude.installed;
274
+ const totalSkipped = cursor.skipped + claude.skipped;
158
275
 
159
276
  console.log("\n" + "─".repeat(40));
160
- console.log(`\n✨ 완료! ${installedCount}개 설치, ${skippedCount}개 건너뜀`);
277
+ console.log(`\n✨ 완료! ${totalInstalled}개 설치, ${totalSkipped}개 건너뜀`);
278
+
279
+ console.log("\n📁 생성된 파일:");
280
+ console.log(" ├── .cursor/rules/*.mdc (Cursor용)");
281
+ console.log(" └── .claude/rules/*.md (Claude Code용)");
161
282
 
162
- console.log("\n📦 설치된 규칙 카테고리:");
283
+ console.log("\n📦 포함된 규칙:");
163
284
  console.log(" ├── base (기본 규칙)");
164
285
  if (options.all || options.fsd) {
165
286
  console.log(" └── fsd (Feature-Sliced Design)");
166
287
  }
167
288
 
168
- console.log("\n💡 Tip:");
169
- console.log(" - 각 .mdc 파일의 globs 패턴에 따라 자동 적용됩니다");
170
- console.log(" - alwaysApply: true 규칙은 항상 적용됩니다");
171
- console.log(" - 프로젝트에 맞게 규칙을 수정하세요!");
289
+ console.log("\n💡 Tip: 프로젝트에 맞게 규칙 파일들을 수정하세요!");
172
290
  }
173
291
 
174
292
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blastlabs/utils",
3
- "version": "1.15.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
- ".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
- - 새 기능 추가 시 반드시 테스트 포함