@blastlabs/utils 1.13.0 → 1.15.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 +174 -0
- package/package.json +5 -3
- package/templates/base.md +111 -0
- package/templates/fsd.md +150 -0
- package/templates/monorepo.md +61 -0
- package/templates/nextjs.md +67 -0
- package/templates/project-specific.md +21 -0
- package/templates/testing.md +66 -0
- package/templates/vite.md +53 -0
- package/bin/entity-generator-guide.md +0 -126
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
// 규칙 카테고리별 정의
|
|
7
|
+
const RULE_CATEGORIES = {
|
|
8
|
+
// 기본 규칙 (항상 포함)
|
|
9
|
+
base: [
|
|
10
|
+
"typescript-standards.mdc",
|
|
11
|
+
"react-hooks.mdc",
|
|
12
|
+
"testing.mdc",
|
|
13
|
+
"documentation.mdc",
|
|
14
|
+
"git-commit.mdc",
|
|
15
|
+
],
|
|
16
|
+
// FSD 아키텍처 규칙
|
|
17
|
+
fsd: [
|
|
18
|
+
"fsd-architecture.mdc",
|
|
19
|
+
"entities-layer.mdc",
|
|
20
|
+
"shared-layer.mdc",
|
|
21
|
+
"views-layer.mdc",
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function parseArgs(args) {
|
|
26
|
+
const options = {
|
|
27
|
+
fsd: false,
|
|
28
|
+
all: false,
|
|
29
|
+
list: false,
|
|
30
|
+
help: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (const arg of args) {
|
|
34
|
+
if (arg === "--help" || arg === "-h") {
|
|
35
|
+
options.help = true;
|
|
36
|
+
} else if (arg === "--fsd") {
|
|
37
|
+
options.fsd = true;
|
|
38
|
+
} else if (arg === "--all") {
|
|
39
|
+
options.all = true;
|
|
40
|
+
} else if (arg === "--list" || arg === "-l") {
|
|
41
|
+
options.list = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function showHelp() {
|
|
49
|
+
console.log("@blastlabs/utils - Cursor Rules Installer\n");
|
|
50
|
+
console.log("프로젝트에 .cursor/rules/ 규칙 파일들을 설치합니다.\n");
|
|
51
|
+
console.log("사용법: npx @blastlabs/utils init-rules [options]\n");
|
|
52
|
+
console.log("Options:");
|
|
53
|
+
console.log(" --fsd FSD 아키텍처 규칙 포함");
|
|
54
|
+
console.log(" --all 모든 규칙 설치 (base + fsd)");
|
|
55
|
+
console.log(" --list, -l 설치 가능한 규칙 목록 보기");
|
|
56
|
+
console.log(" --help, -h 도움말 보기");
|
|
57
|
+
console.log("\n예시:");
|
|
58
|
+
console.log(" npx @blastlabs/utils init-rules # 기본 규칙만");
|
|
59
|
+
console.log(" npx @blastlabs/utils init-rules --fsd # 기본 + FSD 규칙");
|
|
60
|
+
console.log(" npx @blastlabs/utils init-rules --all # 모든 규칙");
|
|
61
|
+
console.log("\n⚠️ 프로젝트 루트 디렉토리에서 실행해주세요!");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function showList() {
|
|
65
|
+
console.log("📋 설치 가능한 규칙 목록\n");
|
|
66
|
+
|
|
67
|
+
console.log("📦 기본 규칙 (base) - 항상 포함:");
|
|
68
|
+
for (const rule of RULE_CATEGORIES.base) {
|
|
69
|
+
console.log(` ├── ${rule}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log("\n🏗️ FSD 아키텍처 규칙 (--fsd):");
|
|
73
|
+
for (const rule of RULE_CATEGORIES.fsd) {
|
|
74
|
+
console.log(` ├── ${rule}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log("\n💡 각 규칙은 globs 패턴으로 자동 적용됩니다.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getRulesSourceDir() {
|
|
81
|
+
// 패키지 내 .cursor/rules 경로
|
|
82
|
+
return path.join(__dirname, "..", ".cursor", "rules");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function copyRule(ruleName, sourceDir, targetDir) {
|
|
86
|
+
const sourcePath = path.join(sourceDir, ruleName);
|
|
87
|
+
const targetPath = path.join(targetDir, ruleName);
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(sourcePath)) {
|
|
90
|
+
console.log(` ⚠️ ${ruleName} - 소스 파일 없음, 건너뜀`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (fs.existsSync(targetPath)) {
|
|
95
|
+
console.log(` ⏭️ ${ruleName} - 이미 존재함, 건너뜀`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
100
|
+
console.log(` ✅ ${ruleName}`);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function main() {
|
|
105
|
+
const args = process.argv.slice(2);
|
|
106
|
+
const options = parseArgs(args);
|
|
107
|
+
|
|
108
|
+
if (options.help) {
|
|
109
|
+
showHelp();
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (options.list) {
|
|
114
|
+
showList();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const targetDir = process.cwd();
|
|
119
|
+
const rulesTargetDir = path.join(targetDir, ".cursor", "rules");
|
|
120
|
+
const rulesSourceDir = getRulesSourceDir();
|
|
121
|
+
|
|
122
|
+
// 소스 디렉토리 확인
|
|
123
|
+
if (!fs.existsSync(rulesSourceDir)) {
|
|
124
|
+
console.error("❌ 규칙 소스 디렉토리를 찾을 수 없습니다.");
|
|
125
|
+
console.error(` 경로: ${rulesSourceDir}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// .cursor/rules 디렉토리 생성
|
|
130
|
+
if (!fs.existsSync(rulesTargetDir)) {
|
|
131
|
+
fs.mkdirSync(rulesTargetDir, { recursive: true });
|
|
132
|
+
console.log("📁 .cursor/rules/ 디렉토리 생성됨\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 설치할 규칙 목록 결정
|
|
136
|
+
let rulesToInstall = [...RULE_CATEGORIES.base];
|
|
137
|
+
|
|
138
|
+
if (options.all || options.fsd) {
|
|
139
|
+
rulesToInstall = [...rulesToInstall, ...RULE_CATEGORIES.fsd];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 중복 제거
|
|
143
|
+
rulesToInstall = [...new Set(rulesToInstall)];
|
|
144
|
+
|
|
145
|
+
console.log("📥 규칙 설치 중...\n");
|
|
146
|
+
|
|
147
|
+
let installedCount = 0;
|
|
148
|
+
let skippedCount = 0;
|
|
149
|
+
|
|
150
|
+
for (const rule of rulesToInstall) {
|
|
151
|
+
const installed = copyRule(rule, rulesSourceDir, rulesTargetDir);
|
|
152
|
+
if (installed) {
|
|
153
|
+
installedCount++;
|
|
154
|
+
} else {
|
|
155
|
+
skippedCount++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log("\n" + "─".repeat(40));
|
|
160
|
+
console.log(`\n✨ 완료! ${installedCount}개 설치, ${skippedCount}개 건너뜀`);
|
|
161
|
+
|
|
162
|
+
console.log("\n📦 설치된 규칙 카테고리:");
|
|
163
|
+
console.log(" ├── base (기본 규칙)");
|
|
164
|
+
if (options.all || options.fsd) {
|
|
165
|
+
console.log(" └── fsd (Feature-Sliced Design)");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log("\n💡 Tip:");
|
|
169
|
+
console.log(" - 각 .mdc 파일의 globs 패턴에 따라 자동 적용됩니다");
|
|
170
|
+
console.log(" - alwaysApply: true 규칙은 항상 적용됩니다");
|
|
171
|
+
console.log(" - 프로젝트에 맞게 규칙을 수정하세요!");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blastlabs/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"blastlabs-generate-entity": "./bin/generate-entity.cjs"
|
|
8
|
+
"blastlabs-generate-entity": "./bin/generate-entity.cjs",
|
|
9
|
+
"blastlabs-init-ai-rules": "./bin/init-ai-rules.cjs"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
|
12
|
-
"bin"
|
|
13
|
+
"bin",
|
|
14
|
+
"templates"
|
|
13
15
|
],
|
|
14
16
|
"scripts": {
|
|
15
17
|
"prepare": "npm run build:tsc",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} Development Rules
|
|
2
|
+
|
|
3
|
+
This file contains development guidelines for AI assistants (Cursor, Claude Code, etc.)
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
{{PROJECT_DESCRIPTION}}
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# 프로젝트 기본 정보
|
|
12
|
+
|
|
13
|
+
- **패키지 매니저**: {{PACKAGE_MANAGER}}
|
|
14
|
+
- **프레임워크**: {{FRAMEWORK}}
|
|
15
|
+
|
|
16
|
+
## 기술 스택
|
|
17
|
+
|
|
18
|
+
- TypeScript
|
|
19
|
+
- React
|
|
20
|
+
- TanStack Query
|
|
21
|
+
- Tailwind CSS
|
|
22
|
+
- Zod
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Common Rules (공통 규칙)
|
|
27
|
+
|
|
28
|
+
## Code Style & Standards
|
|
29
|
+
|
|
30
|
+
### TypeScript
|
|
31
|
+
- Use strict type checking (`strict: true` in tsconfig)
|
|
32
|
+
- Prefer explicit types over `any`
|
|
33
|
+
- Export all public types and interfaces
|
|
34
|
+
- Use generics for reusable components
|
|
35
|
+
- Use `type` for unions/intersections, `interface` for object shapes
|
|
36
|
+
|
|
37
|
+
### Naming Conventions
|
|
38
|
+
- **Files/Folders**: kebab-case (`user-profile.tsx`, `use-auth.ts`)
|
|
39
|
+
- **Components**: PascalCase (`UserProfile`, `AuthGuard`)
|
|
40
|
+
- **Hooks**: camelCase with `use` prefix (`useAuth`, `useLocalStorage`)
|
|
41
|
+
- **Constants**: UPPER_SNAKE_CASE (`API_BASE_URL`, `MAX_RETRY_COUNT`)
|
|
42
|
+
- **Types/Interfaces**: PascalCase (`UserProfile`, `AuthState`)
|
|
43
|
+
|
|
44
|
+
### Import Order
|
|
45
|
+
1. React/Framework imports
|
|
46
|
+
2. Third-party libraries
|
|
47
|
+
3. Internal modules (absolute paths)
|
|
48
|
+
4. Relative imports
|
|
49
|
+
5. Types (with `type` keyword)
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import React, { useState, useEffect } from 'react';
|
|
53
|
+
import { useQuery } from '@tanstack/react-query';
|
|
54
|
+
import { Button } from '@/components/ui';
|
|
55
|
+
import { formatDate } from '../utils';
|
|
56
|
+
import type { User } from '@/types';
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## React Best Practices
|
|
60
|
+
|
|
61
|
+
### Component Patterns
|
|
62
|
+
- Use functional components with hooks
|
|
63
|
+
- Keep components small and focused (single responsibility)
|
|
64
|
+
- Extract reusable logic into custom hooks
|
|
65
|
+
- Use composition over inheritance
|
|
66
|
+
|
|
67
|
+
### Hooks Rules
|
|
68
|
+
- Only call hooks at the top level
|
|
69
|
+
- Only call hooks from React functions
|
|
70
|
+
- Use `useCallback` for event handlers passed to children
|
|
71
|
+
- Use `useMemo` for expensive calculations
|
|
72
|
+
|
|
73
|
+
### State Management
|
|
74
|
+
- Keep state as local as possible
|
|
75
|
+
- Lift state up only when necessary
|
|
76
|
+
- Use TanStack Query for server state
|
|
77
|
+
- Use context or Zustand for client global state
|
|
78
|
+
|
|
79
|
+
## Error Handling
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
try {
|
|
83
|
+
const result = await someAsyncOperation();
|
|
84
|
+
return { data: result, error: null };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Operation failed:', error);
|
|
87
|
+
return { data: null, error: error instanceof Error ? error : new Error('Unknown error') };
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Git Commit Convention
|
|
92
|
+
|
|
93
|
+
Use conventional commits format:
|
|
94
|
+
- `feat(scope): description` - New features
|
|
95
|
+
- `fix(scope): description` - Bug fixes
|
|
96
|
+
- `refactor(scope): description` - Code refactoring
|
|
97
|
+
- `docs(scope): description` - Documentation changes
|
|
98
|
+
- `test(scope): description` - Test changes
|
|
99
|
+
- `chore(scope): description` - Build/tooling changes
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
- `feat(auth): add login functionality`
|
|
103
|
+
- `fix(api): resolve timeout error`
|
|
104
|
+
- `docs(readme): update installation guide`
|
|
105
|
+
|
|
106
|
+
## Before Committing
|
|
107
|
+
|
|
108
|
+
1. Run tests: `{{PACKAGE_MANAGER}} test`
|
|
109
|
+
2. Build successfully: `{{PACKAGE_MANAGER}} build`
|
|
110
|
+
3. Lint check: `{{PACKAGE_MANAGER}} lint`
|
|
111
|
+
4. Format code: `{{PACKAGE_MANAGER}} format`
|
package/templates/fsd.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Feature-Sliced Design (FSD)
|
|
5
|
+
|
|
6
|
+
프로젝트는 [FSD 공식문서](https://feature-sliced.github.io/documentation/kr/docs/get-started/overview)를 참고하여 구조화되어 있습니다.
|
|
7
|
+
|
|
8
|
+
## 폴더 구조
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/
|
|
12
|
+
├── app/ # 앱 설정, providers, 라우팅
|
|
13
|
+
├── entities/ # 도메인 모델, API
|
|
14
|
+
├── features/ # 사용자 기능 단위
|
|
15
|
+
├── shared/ # 공유 컴포넌트, 유틸
|
|
16
|
+
├── views/ # 페이지 컴포넌트
|
|
17
|
+
└── widgets/ # 복합 UI 블록
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Entities Layer (엔티티)
|
|
21
|
+
|
|
22
|
+
엔티티는 도메인 모델과 API 로직을 관리합니다.
|
|
23
|
+
|
|
24
|
+
### 구조
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
entities/
|
|
28
|
+
└── (엔티티 이름)/ 예: inquiry, popular-product
|
|
29
|
+
├── api/
|
|
30
|
+
│ ├── mapper/
|
|
31
|
+
│ │ ├── map-(엔티티 이름).ts
|
|
32
|
+
│ │ └── map-(엔티티 이름)-detail.ts
|
|
33
|
+
│ ├── query/
|
|
34
|
+
│ │ ├── (엔티티 이름)-list-query.ts
|
|
35
|
+
│ │ └── (엔티티 이름)-detail-query.ts
|
|
36
|
+
│ ├── get-(엔티티 이름)-list.ts
|
|
37
|
+
│ ├── get-(엔티티 이름)-detail.ts
|
|
38
|
+
│ ├── (엔티티 이름)-queries.ts
|
|
39
|
+
│ ├── mutate.ts
|
|
40
|
+
│ └── index.ts
|
|
41
|
+
├── model/
|
|
42
|
+
│ ├── (엔티티 이름).ts
|
|
43
|
+
│ ├── (엔티티 이름)-detail.ts
|
|
44
|
+
│ └── (엔티티 이름)-update.ts
|
|
45
|
+
├── schema.ts
|
|
46
|
+
└── index.ts
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 각 파일의 역할
|
|
50
|
+
|
|
51
|
+
- **mapper/**: API 응답을 도메인 모델로 변환
|
|
52
|
+
- **query/**: 쿼리 파라미터 타입 정의
|
|
53
|
+
- **get-\*-list.ts**: 리스트 조회 API 함수
|
|
54
|
+
- **get-\*-detail.ts**: 상세 조회 API 함수
|
|
55
|
+
- **\*-queries.ts**: TanStack Query queryOptions 정의 (all, lists, list, details, detail 등)
|
|
56
|
+
- **mutate.ts**: mutation 함수들
|
|
57
|
+
- **model/**: 도메인 모델 타입 정의
|
|
58
|
+
- **schema.ts**: zod 스키마 정의 및 변환 함수
|
|
59
|
+
- **index.ts**: `export * as (엔티티 이름)Api from "./api"`
|
|
60
|
+
|
|
61
|
+
### 예시
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
entities/inquiry/
|
|
65
|
+
├── api/
|
|
66
|
+
│ ├── mapper/
|
|
67
|
+
│ │ ├── map-inquiry.ts
|
|
68
|
+
│ │ └── map-inquiry-detail.ts
|
|
69
|
+
│ ├── query/
|
|
70
|
+
│ │ ├── inquiry-list-query.ts
|
|
71
|
+
│ │ └── inquiry-detail-query.ts
|
|
72
|
+
│ ├── get-inquiry-list.ts
|
|
73
|
+
│ ├── get-inquiry-detail.ts
|
|
74
|
+
│ └── inquiry-queries.ts
|
|
75
|
+
├── model/
|
|
76
|
+
│ ├── inquiry.ts
|
|
77
|
+
│ └── inquiry-detail.ts
|
|
78
|
+
└── schema.ts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Shared Layer (공유 레이어)
|
|
82
|
+
|
|
83
|
+
프로젝트 전체에서 공유되는 코드를 관리합니다.
|
|
84
|
+
|
|
85
|
+
### 구조
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
shared/
|
|
89
|
+
├── api/ API 인스턴스 설정
|
|
90
|
+
├── constant/ 공통 상수 (path.ts 등)
|
|
91
|
+
├── hooks/ 공통 React 훅
|
|
92
|
+
├── lib/ 라이브러리 래퍼 및 설정
|
|
93
|
+
│ └── (라이브러리명)/
|
|
94
|
+
│ ├── provider.tsx
|
|
95
|
+
│ └── config.ts
|
|
96
|
+
├── ui/
|
|
97
|
+
│ ├── component/ 재사용 가능한 UI 컴포넌트
|
|
98
|
+
│ │ └── (컴포넌트명)/
|
|
99
|
+
│ │ ├── index.tsx
|
|
100
|
+
│ │ ├── types.ts
|
|
101
|
+
│ │ └── (컴포넌트명).tsx
|
|
102
|
+
│ ├── theme/ 테마 관련 CSS 파일
|
|
103
|
+
│ └── utils/ UI 관련 유틸리티 함수
|
|
104
|
+
└── utils/ 일반 유틸리티 함수
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### lib vs utils 구분 기준
|
|
108
|
+
|
|
109
|
+
#### `shared/lib/`
|
|
110
|
+
외부 라이브러리를 프로젝트에 맞게 감싸거나 설정하는 코드
|
|
111
|
+
- 라이브러리 래퍼(wrapper) 컴포넌트
|
|
112
|
+
- 라이브러리 초기 설정 및 Provider
|
|
113
|
+
- 예시: `react-hook-form/form-provider.tsx`, `tanstack-query/query-client.tsx`
|
|
114
|
+
|
|
115
|
+
#### `shared/utils/`
|
|
116
|
+
순수 유틸리티 함수 및 비즈니스 로직 헬퍼
|
|
117
|
+
- 라이브러리와 무관한 순수 함수
|
|
118
|
+
- 예시: `convert-price.ts`, `export-excel.ts`, `format-date.ts`
|
|
119
|
+
|
|
120
|
+
**판단 기준:**
|
|
121
|
+
- 라이브러리를 감싸는가? → `lib/`
|
|
122
|
+
- 순수 함수 또는 유틸인가? → `utils/`
|
|
123
|
+
|
|
124
|
+
## Views Layer (페이지 레이어)
|
|
125
|
+
|
|
126
|
+
페이지 단위의 UI와 로직을 관리합니다.
|
|
127
|
+
|
|
128
|
+
### 구조
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
views/
|
|
132
|
+
└── (페이지명)/
|
|
133
|
+
├── page.tsx 메인 페이지 컴포넌트 (필수)
|
|
134
|
+
├── (페이지명)-list-page.tsx
|
|
135
|
+
├── (페이지명)-detail-page.tsx
|
|
136
|
+
├── detail/
|
|
137
|
+
│ ├── page.tsx
|
|
138
|
+
│ ├── ui/
|
|
139
|
+
│ ├── hooks/ use-(페이지명)-detail-data.tsx 등
|
|
140
|
+
│ └── components/
|
|
141
|
+
├── list/
|
|
142
|
+
│ ├── (컬럼명)-column.tsx
|
|
143
|
+
│ ├── search.tsx
|
|
144
|
+
│ └── (모달명)-modal.tsx
|
|
145
|
+
├── hooks/
|
|
146
|
+
├── components/
|
|
147
|
+
├── utils/
|
|
148
|
+
├── schema/
|
|
149
|
+
└── constants.ts
|
|
150
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Monorepo Rules
|
|
5
|
+
|
|
6
|
+
## Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
root/
|
|
10
|
+
├── apps/
|
|
11
|
+
│ ├── web/ # 웹 앱
|
|
12
|
+
│ ├── admin/ # 어드민
|
|
13
|
+
│ └── mobile/ # 모바일 앱
|
|
14
|
+
├── packages/
|
|
15
|
+
│ ├── ui/ # 공유 UI 컴포넌트
|
|
16
|
+
│ ├── utils/ # 공유 유틸리티
|
|
17
|
+
│ ├── config/ # 공유 설정 (eslint, tsconfig)
|
|
18
|
+
│ └── types/ # 공유 TypeScript 타입
|
|
19
|
+
├── package.json
|
|
20
|
+
└── pnpm-workspace.yaml
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Package Management
|
|
24
|
+
- Use workspace protocol for internal packages (`workspace:*`)
|
|
25
|
+
- Keep shared dependencies in root `package.json`
|
|
26
|
+
- App-specific dependencies in app's `package.json`
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@repo/ui": "workspace:*",
|
|
32
|
+
"@repo/utils": "workspace:*"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Build Order
|
|
38
|
+
- Build packages before apps
|
|
39
|
+
- Use Turborepo for build orchestration
|
|
40
|
+
- Configure proper `dependsOn` in turbo.json
|
|
41
|
+
|
|
42
|
+
## Shared Configurations
|
|
43
|
+
- Extend base tsconfig in all packages
|
|
44
|
+
- Share ESLint config from `packages/config`
|
|
45
|
+
- Maintain consistent formatting across packages
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 전체 빌드
|
|
51
|
+
pnpm build
|
|
52
|
+
|
|
53
|
+
# 특정 앱만 빌드
|
|
54
|
+
pnpm --filter @repo/admin build
|
|
55
|
+
|
|
56
|
+
# 특정 앱 dev 서버
|
|
57
|
+
pnpm --filter @repo/web dev
|
|
58
|
+
|
|
59
|
+
# 전체 린트
|
|
60
|
+
pnpm lint
|
|
61
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Next.js Rules
|
|
5
|
+
|
|
6
|
+
## App Router
|
|
7
|
+
- Use Server Components by default
|
|
8
|
+
- Add `'use client'` only when needed (hooks, browser APIs, interactivity)
|
|
9
|
+
- Colocate loading.tsx, error.tsx, not-found.tsx with pages
|
|
10
|
+
- Use route groups `(group)` for organization without affecting URL
|
|
11
|
+
|
|
12
|
+
## File Structure
|
|
13
|
+
```
|
|
14
|
+
app/
|
|
15
|
+
├── (auth)/
|
|
16
|
+
│ ├── login/page.tsx
|
|
17
|
+
│ └── register/page.tsx
|
|
18
|
+
├── (main)/
|
|
19
|
+
│ ├── dashboard/page.tsx
|
|
20
|
+
│ └── settings/page.tsx
|
|
21
|
+
├── api/
|
|
22
|
+
│ └── [...route]/route.ts
|
|
23
|
+
├── layout.tsx
|
|
24
|
+
└── page.tsx
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Data Fetching
|
|
28
|
+
- Prefer Server Components for data fetching
|
|
29
|
+
- Use `fetch` with caching options in Server Components
|
|
30
|
+
- Use React Query/SWR for client-side data fetching
|
|
31
|
+
- Implement proper loading and error states
|
|
32
|
+
|
|
33
|
+
## Environment Variables
|
|
34
|
+
- `NEXT_PUBLIC_*` for client-side variables
|
|
35
|
+
- Server-only variables without prefix
|
|
36
|
+
- Use `.env.local` for local development
|
|
37
|
+
|
|
38
|
+
## SSR Safety
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// ❌ Wrong - breaks SSR
|
|
42
|
+
const token = localStorage.getItem('token');
|
|
43
|
+
|
|
44
|
+
// ✅ Correct - SSR safe
|
|
45
|
+
const token = typeof window !== 'undefined'
|
|
46
|
+
? localStorage.getItem('token')
|
|
47
|
+
: null;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## SSR-Safe Hook Pattern
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const [value, setValue] = useState(() => {
|
|
54
|
+
if (typeof window === 'undefined') return defaultValue;
|
|
55
|
+
return window.localStorage.getItem(key);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (typeof window === 'undefined') return;
|
|
60
|
+
// Browser-only code
|
|
61
|
+
}, []);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Image Optimization
|
|
65
|
+
- Always use `next/image` for images
|
|
66
|
+
- Provide width and height or use `fill`
|
|
67
|
+
- Use appropriate `sizes` prop for responsive images
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Project Specific Rules (프로젝트별 추가 규칙)
|
|
5
|
+
|
|
6
|
+
<!-- 여기에 프로젝트 고유의 규칙을 추가하세요 -->
|
|
7
|
+
|
|
8
|
+
## Custom Rules
|
|
9
|
+
- Add your project-specific rules here
|
|
10
|
+
- Document any exceptions to common rules
|
|
11
|
+
- Include team agreements and decisions
|
|
12
|
+
|
|
13
|
+
## API Conventions
|
|
14
|
+
- Document API patterns used in this project
|
|
15
|
+
- Specify error handling strategies
|
|
16
|
+
- Define response/request formats
|
|
17
|
+
|
|
18
|
+
## State Management
|
|
19
|
+
- Document which state library is used
|
|
20
|
+
- Define when to use local vs global state
|
|
21
|
+
- Specify state structure conventions
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# 테스트 전략
|
|
5
|
+
|
|
6
|
+
## 테스트 작성 원칙
|
|
7
|
+
|
|
8
|
+
### 컴포넌트 (Component) - 단위 테스트
|
|
9
|
+
|
|
10
|
+
- **대상**: `shared/ui/component/`, 재사용 가능한 컴포넌트
|
|
11
|
+
- **목적**: 컴포넌트의 독립적인 동작 검증
|
|
12
|
+
- **방법**:
|
|
13
|
+
- Props와 사용자 상호작용에 집중
|
|
14
|
+
- 외부 의존성(API, 라우팅)은 mock 처리
|
|
15
|
+
- 렌더링, 이벤트 핸들링, 상태 변경 검증
|
|
16
|
+
|
|
17
|
+
### 페이지 (Page) - 기능 테스트
|
|
18
|
+
|
|
19
|
+
- **대상**: `views/*/page.tsx` 페이지 컴포넌트
|
|
20
|
+
- **목적**: 사용자 시나리오와 비즈니스 로직 검증
|
|
21
|
+
- **Mock 최소화 원칙**:
|
|
22
|
+
- 외부 의존성(API 호출, toast)만 mock
|
|
23
|
+
- 내부 컴포넌트는 실제로 렌더링하여 통합 테스트
|
|
24
|
+
- `data-testid` 대신 접근성 기반 쿼리 사용
|
|
25
|
+
|
|
26
|
+
## Testing Library 쿼리 우선순위
|
|
27
|
+
|
|
28
|
+
**1순위 (가장 권장):**
|
|
29
|
+
- `getByRole` - 접근성 역할 기반 (button, textbox, checkbox 등)
|
|
30
|
+
- `getByLabelText` - label과 연결된 폼 요소
|
|
31
|
+
- `getByPlaceholderText` - placeholder가 있는 입력 필드
|
|
32
|
+
- `getByText` - 텍스트 내용으로 찾기
|
|
33
|
+
- `getByDisplayValue` - 입력 필드의 현재 값
|
|
34
|
+
|
|
35
|
+
**2순위:**
|
|
36
|
+
- `getByAltText` - 이미지의 alt 텍스트
|
|
37
|
+
- `getByTitle` - title 속성
|
|
38
|
+
|
|
39
|
+
**3순위 (최후의 수단):**
|
|
40
|
+
- `getByTestId` - `data-testid` (가능한 한 피해야 함)
|
|
41
|
+
|
|
42
|
+
**예시:**
|
|
43
|
+
```typescript
|
|
44
|
+
// ✅ 좋은 예
|
|
45
|
+
screen.getByRole('button', { name: '저장' });
|
|
46
|
+
screen.getByRole('checkbox');
|
|
47
|
+
screen.getByText('셀러 정보를 찾을 수 없습니다.');
|
|
48
|
+
screen.getByLabelText(/대표자명/);
|
|
49
|
+
|
|
50
|
+
// ❌ 나쁜 예
|
|
51
|
+
screen.getByTestId('save-button');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 테스트 파일 위치
|
|
55
|
+
|
|
56
|
+
- 컴포넌트와 같은 디렉토리에 `*.test.tsx`
|
|
57
|
+
|
|
58
|
+
## 테스트 구조
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
describe('ComponentName', () => {
|
|
62
|
+
it('should render correctly', () => {});
|
|
63
|
+
it('should handle user interaction', () => {});
|
|
64
|
+
it('should show error state', () => {});
|
|
65
|
+
});
|
|
66
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Vite + React Rules
|
|
5
|
+
|
|
6
|
+
## Environment Variables
|
|
7
|
+
- Use `import.meta.env.VITE_*` for environment variables
|
|
8
|
+
- Only `VITE_` prefixed variables are exposed to client
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// ✅ Correct
|
|
12
|
+
const apiUrl = import.meta.env.VITE_API_URL;
|
|
13
|
+
|
|
14
|
+
// ❌ Wrong - won't work
|
|
15
|
+
const secret = import.meta.env.SECRET_KEY;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## File Structure
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
├── assets/ # Static assets
|
|
22
|
+
├── components/ # React components
|
|
23
|
+
├── hooks/ # Custom hooks
|
|
24
|
+
├── pages/ # Page components
|
|
25
|
+
├── routes/ # Routing configuration
|
|
26
|
+
├── services/ # API services
|
|
27
|
+
├── stores/ # State management
|
|
28
|
+
├── utils/ # Utility functions
|
|
29
|
+
└── main.tsx # Entry point
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Path Aliases
|
|
33
|
+
- Use `@/` alias for `src/` directory
|
|
34
|
+
- Configure in `vite.config.ts` and `tsconfig.json`
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// vite.config.ts
|
|
38
|
+
resolve: {
|
|
39
|
+
alias: {
|
|
40
|
+
'@': path.resolve(__dirname, './src'),
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Build Optimization
|
|
46
|
+
- Use dynamic imports for code splitting
|
|
47
|
+
- Lazy load routes and heavy components
|
|
48
|
+
- Analyze bundle size with `rollup-plugin-visualizer`
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Lazy loading example
|
|
52
|
+
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
|
53
|
+
```
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# 엔티티 자동 생성 스크립트 사용법
|
|
2
|
-
|
|
3
|
-
## 개요
|
|
4
|
-
|
|
5
|
-
`generate-entity.cjs` 스크립트를 사용하여 새로운 엔티티의 기본 구조를 자동으로 생성할 수 있습니다.
|
|
6
|
-
|
|
7
|
-
## 사용법
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
# entities 폴더로 이동
|
|
11
|
-
cd apps/admin/src/entities
|
|
12
|
-
|
|
13
|
-
# 엔티티 생성
|
|
14
|
-
node generate-entity.cjs <entity-name>
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## 예시
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
# user-profile 엔티티 생성
|
|
21
|
-
node generate-entity.cjs user-profile
|
|
22
|
-
|
|
23
|
-
# my-entity 엔티티 생성
|
|
24
|
-
node generate-entity.cjs my-entity
|
|
25
|
-
|
|
26
|
-
# product-review 엔티티 생성
|
|
27
|
-
node generate-entity.cjs product-review
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## 생성되는 파일 구조
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
{entity-name}/
|
|
34
|
-
├── index.ts # 엔티티 메인 export 파일
|
|
35
|
-
├── api/
|
|
36
|
-
│ ├── get-{entity-name}-list.ts # API 호출 함수
|
|
37
|
-
│ ├── {entity-name}-queries.ts # React Query 설정
|
|
38
|
-
│ ├── index.ts # API 모듈 export
|
|
39
|
-
│ ├── mapper/
|
|
40
|
-
│ │ └── map-{entity-name}.ts # 데이터 매핑 함수
|
|
41
|
-
│ └── query/
|
|
42
|
-
│ └── {entity-name}-list-query.ts # 쿼리 타입 정의
|
|
43
|
-
└── model/
|
|
44
|
-
└── {entity-name}.ts # Zod 스키마 및 타입
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## 자동 생성 후 추가 작업
|
|
48
|
-
|
|
49
|
-
스크립트 실행 후 다음 작업들을 수동으로 완료해야 합니다:
|
|
50
|
-
|
|
51
|
-
### 1. query-keys.ts 파일 업데이트
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
export const {ENTITY_NAME}_QUERY_KEYS = {
|
|
55
|
-
all: ["{entity-name}"] as const,
|
|
56
|
-
lists: () => [...{ENTITY_NAME}_QUERY_KEYS.all, "list"] as const,
|
|
57
|
-
list: (filters: {EntityName}ListParams) =>
|
|
58
|
-
[...{ENTITY_NAME}_QUERY_KEYS.lists(), filters] as const,
|
|
59
|
-
details: () => [...{ENTITY_NAME}_QUERY_KEYS.all, "detail"] as const,
|
|
60
|
-
detail: (id: number) => [...{ENTITY_NAME}_QUERY_KEYS.details(), id] as const,
|
|
61
|
-
};
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### 2. model/{entity-name}.ts 수정
|
|
65
|
-
|
|
66
|
-
실제 API 응답 구조에 맞게 Zod 스키마를 수정해주세요:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
export const {EntityName}Schema = z.object({
|
|
70
|
-
id: z.number(),
|
|
71
|
-
// 실제 필드들 추가
|
|
72
|
-
name: z.string(),
|
|
73
|
-
description: z.string().nullable(),
|
|
74
|
-
createDate: z.string(),
|
|
75
|
-
updateDate: z.string(),
|
|
76
|
-
});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 3. api/mapper/map-{entity-name}.ts 수정
|
|
80
|
-
|
|
81
|
-
API 응답 데이터를 모델에 맞게 매핑하는 로직을 수정해주세요:
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
export const map{EntityName} = (item: {EntityName}) => {
|
|
85
|
-
const res = {EntityName}Schema.safeParse({
|
|
86
|
-
id: item.id,
|
|
87
|
-
name: item.name,
|
|
88
|
-
description: item.description,
|
|
89
|
-
createDate: item.createDate,
|
|
90
|
-
updateDate: item.updateDate,
|
|
91
|
-
});
|
|
92
|
-
// ...
|
|
93
|
-
};
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### 4. api/get-{entity-name}-list.ts 수정
|
|
97
|
-
|
|
98
|
-
실제 API 엔드포인트 메서드명으로 변경해주세요:
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
export const get{EntityName}List = (filters: {EntityName}ListQuery) => {
|
|
102
|
-
const customApi = createInstance(import.meta.env.VITE_BASE_URL);
|
|
103
|
-
// 실제 API 메서드명으로 변경
|
|
104
|
-
return customApi.default.admin{EntityName}List(filters);
|
|
105
|
-
};
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 5. api/query/{entity-name}-list-query.ts 수정
|
|
109
|
-
|
|
110
|
-
필요한 필터링 옵션을 추가해주세요:
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
export type {EntityName}ListQuery = {
|
|
114
|
-
limit?: number;
|
|
115
|
-
offset?: number;
|
|
116
|
-
search?: string;
|
|
117
|
-
status?: string;
|
|
118
|
-
// 추가 필터 옵션들...
|
|
119
|
-
};
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## 참고사항
|
|
123
|
-
|
|
124
|
-
- 엔티티 이름은 kebab-case로 입력해주세요 (예: `user-profile`, `product-review`)
|
|
125
|
-
- 생성된 파일들은 기본 템플릿이므로 실제 API 구조에 맞게 수정이 필요합니다
|
|
126
|
-
- popular-product, popular-review 폴더를 참고하여 패턴을 확인할 수 있습니다
|