@blastlabs/utils 1.12.1 → 1.14.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/generate-entity.cjs +282 -0
- package/bin/init-ai-rules.cjs +167 -0
- package/package.json +8 -2
- 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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
// 엔티티 이름을 kebab-case로 변환
|
|
7
|
+
function toKebabCase(str) {
|
|
8
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 엔티티 이름을 PascalCase로 변환
|
|
12
|
+
function toPascalCase(str) {
|
|
13
|
+
return str
|
|
14
|
+
.split("-")
|
|
15
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
16
|
+
.join("");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 엔티티 이름을 camelCase로 변환
|
|
20
|
+
function toCamelCase(str) {
|
|
21
|
+
const pascalCase = toPascalCase(str);
|
|
22
|
+
return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createEntityStructure(entityName, targetDir) {
|
|
26
|
+
const kebabName = toKebabCase(entityName);
|
|
27
|
+
const pascalName = toPascalCase(kebabName);
|
|
28
|
+
const camelName = toCamelCase(kebabName);
|
|
29
|
+
|
|
30
|
+
// 현재 작업 디렉토리 기준으로 생성
|
|
31
|
+
const basePath = path.join(targetDir, kebabName);
|
|
32
|
+
|
|
33
|
+
// 디렉토리 생성
|
|
34
|
+
const dirs = [
|
|
35
|
+
basePath,
|
|
36
|
+
path.join(basePath, "api"),
|
|
37
|
+
path.join(basePath, "api", "mapper"),
|
|
38
|
+
path.join(basePath, "api", "query"),
|
|
39
|
+
path.join(basePath, "model"),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
dirs.forEach((dir) => {
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 파일 템플릿 생성
|
|
49
|
+
createFiles(basePath, kebabName, pascalName, camelName);
|
|
50
|
+
|
|
51
|
+
console.log(`✅ ${kebabName} 엔티티가 성공적으로 생성되었습니다!`);
|
|
52
|
+
console.log(`📍 위치: ${basePath}`);
|
|
53
|
+
console.log(`\n생성된 파일들:`);
|
|
54
|
+
console.log(`📁 ${kebabName}/`);
|
|
55
|
+
console.log(`├── index.ts`);
|
|
56
|
+
console.log(`├── api/`);
|
|
57
|
+
console.log(`│ ├── get-${kebabName}-list.ts`);
|
|
58
|
+
console.log(`│ ├── get-${kebabName}-detail.ts`);
|
|
59
|
+
console.log(`│ ├── ${kebabName}-queries.ts`);
|
|
60
|
+
console.log(`│ ├── index.ts`);
|
|
61
|
+
console.log(`│ ├── mapper/`);
|
|
62
|
+
console.log(`│ │ ├── map-${kebabName}.ts`);
|
|
63
|
+
console.log(`│ │ └── map-${kebabName}-detail.ts`);
|
|
64
|
+
console.log(`│ └── query/`);
|
|
65
|
+
console.log(`│ └── ${kebabName}-list-query.ts`);
|
|
66
|
+
console.log(`└── model/`);
|
|
67
|
+
console.log(` ├── ${kebabName}.ts`);
|
|
68
|
+
console.log(` └── ${kebabName}-detail.ts`);
|
|
69
|
+
|
|
70
|
+
console.log(`\n⚠️ 추가 작업이 필요합니다:`);
|
|
71
|
+
console.log(
|
|
72
|
+
`1. query-keys.ts 파일에 ${pascalName.toUpperCase()}_QUERY_KEYS 추가`,
|
|
73
|
+
);
|
|
74
|
+
console.log(`2. API 스키마에 맞게 model과 mapper 수정`);
|
|
75
|
+
console.log(`3. 실제 API 엔드포인트에 맞게 get-${kebabName}-list.ts 수정`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createFiles(basePath, kebabName, pascalName, camelName) {
|
|
79
|
+
// index.ts
|
|
80
|
+
const indexContent = `export * as ${camelName}Api from "./api";
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
// api/index.ts
|
|
84
|
+
const apiIndexContent = `export { get${pascalName}List } from "./get-${kebabName}-list";
|
|
85
|
+
export { get${pascalName}Detail } from "./get-${kebabName}-detail";
|
|
86
|
+
export { ${camelName}Queries } from "./${kebabName}-queries";
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
// api/get-{entity}-list.ts
|
|
90
|
+
const getListContent = `// TODO: API 클라이언트 import 추가
|
|
91
|
+
// import { apiClient } from "@/shared/api";
|
|
92
|
+
import { ${pascalName}ListQuery } from "./query/${kebabName}-list-query";
|
|
93
|
+
|
|
94
|
+
export const get${pascalName}List = async (filters: ${pascalName}ListQuery) => {
|
|
95
|
+
// TODO: 실제 API 호출로 변경 필요
|
|
96
|
+
// return apiClient.get("/api/${kebabName}", { params: filters });
|
|
97
|
+
throw new Error("API 호출 구현 필요");
|
|
98
|
+
};
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
// api/get-{entity}-detail.ts
|
|
102
|
+
const getDetailContent = `// TODO: API 클라이언트 import 추가
|
|
103
|
+
// import { apiClient } from "@/shared/api";
|
|
104
|
+
|
|
105
|
+
export const get${pascalName}Detail = async (id: number) => {
|
|
106
|
+
// TODO: 실제 API 호출로 변경 필요
|
|
107
|
+
// return apiClient.get(\`/api/${kebabName}/\${id}\`);
|
|
108
|
+
throw new Error("API 호출 구현 필요");
|
|
109
|
+
};
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
// api/{entity}-queries.ts
|
|
113
|
+
const queriesContent = `import { queryOptions } from "@tanstack/react-query";
|
|
114
|
+
import { ${pascalName}ListQuery } from "./query/${kebabName}-list-query";
|
|
115
|
+
import { get${pascalName}List } from "./get-${kebabName}-list";
|
|
116
|
+
import { get${pascalName}Detail } from "./get-${kebabName}-detail";
|
|
117
|
+
import { map${pascalName} } from "./mapper/map-${kebabName}";
|
|
118
|
+
import { map${pascalName}Detail } from "./mapper/map-${kebabName}-detail";
|
|
119
|
+
|
|
120
|
+
export const ${camelName}Queries = {
|
|
121
|
+
all: () => ["${kebabName}"],
|
|
122
|
+
lists: () => [...${camelName}Queries.all(), "list"],
|
|
123
|
+
list: (filters: ${pascalName}ListQuery) =>
|
|
124
|
+
queryOptions({
|
|
125
|
+
queryKey: [...${camelName}Queries.lists(), filters],
|
|
126
|
+
queryFn: () => get${pascalName}List(filters),
|
|
127
|
+
select: (data) => {
|
|
128
|
+
return {
|
|
129
|
+
...data,
|
|
130
|
+
items: data.items.map(map${pascalName}),
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
details: () => [...${camelName}Queries.all(), "detail"],
|
|
135
|
+
detail: (id: number) =>
|
|
136
|
+
queryOptions({
|
|
137
|
+
queryKey: [...${camelName}Queries.details(), id],
|
|
138
|
+
queryFn: () => get${pascalName}Detail(id),
|
|
139
|
+
select: (data) => map${pascalName}Detail(data),
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
// api/mapper/map-{entity}.ts
|
|
145
|
+
const mapperContent = `import { ${pascalName}Schema } from "../../model/${kebabName}";
|
|
146
|
+
|
|
147
|
+
// TODO: API 응답 타입을 실제 타입으로 변경
|
|
148
|
+
type ${pascalName}Response = {
|
|
149
|
+
id: number;
|
|
150
|
+
createDate: string;
|
|
151
|
+
updateDate: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const map${pascalName} = (item: ${pascalName}Response) => {
|
|
155
|
+
const res = ${pascalName}Schema.safeParse({
|
|
156
|
+
id: item.id,
|
|
157
|
+
// TODO: 실제 API 응답 구조에 맞게 매핑 수정 필요
|
|
158
|
+
createDate: item.createDate,
|
|
159
|
+
updateDate: item.updateDate,
|
|
160
|
+
});
|
|
161
|
+
if (!res.success) {
|
|
162
|
+
console.error(res.error);
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return res.data;
|
|
166
|
+
};
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
// api/mapper/map-{entity}-detail.ts
|
|
170
|
+
const mapperDetailContent = `import { ${pascalName}DetailSchema } from "../../model/${kebabName}-detail";
|
|
171
|
+
|
|
172
|
+
// TODO: API 응답 타입을 실제 타입으로 변경
|
|
173
|
+
type ${pascalName}DetailResponse = {
|
|
174
|
+
id: number;
|
|
175
|
+
createDate: string;
|
|
176
|
+
updateDate: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const map${pascalName}Detail = (item: ${pascalName}DetailResponse) => {
|
|
180
|
+
const res = ${pascalName}DetailSchema.safeParse({
|
|
181
|
+
id: item.id,
|
|
182
|
+
// TODO: 실제 API 응답 구조에 맞게 매핑 수정 필요
|
|
183
|
+
createDate: item.createDate,
|
|
184
|
+
updateDate: item.updateDate,
|
|
185
|
+
});
|
|
186
|
+
if (!res.success) {
|
|
187
|
+
console.error(res.error);
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
return res.data;
|
|
191
|
+
};
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
// api/query/{entity}-list-query.ts
|
|
195
|
+
const queryContent = `export type ${pascalName}ListQuery = {
|
|
196
|
+
limit?: number;
|
|
197
|
+
offset?: number;
|
|
198
|
+
// TODO: 추가 필터링 옵션이 필요한 경우 여기에 추가
|
|
199
|
+
};
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
// model/{entity}.ts
|
|
203
|
+
const modelContent = `import { z } from "zod";
|
|
204
|
+
|
|
205
|
+
export const ${pascalName}Schema = z.object({
|
|
206
|
+
id: z.number(),
|
|
207
|
+
createDate: z.string(),
|
|
208
|
+
updateDate: z.string(),
|
|
209
|
+
// TODO: 실제 데이터 구조에 맞게 스키마 수정 필요
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
export type ${pascalName}Type = z.infer<typeof ${pascalName}Schema>;
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
//model/{entity}-detail.ts
|
|
216
|
+
const detailContent = `import { z } from "zod";
|
|
217
|
+
|
|
218
|
+
export const ${pascalName}DetailSchema = z.object({
|
|
219
|
+
id: z.number(),
|
|
220
|
+
createDate: z.string(),
|
|
221
|
+
updateDate: z.string(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export type ${pascalName}DetailType = z.infer<typeof ${pascalName}DetailSchema>;
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
// 파일 쓰기
|
|
228
|
+
fs.writeFileSync(path.join(basePath, "index.ts"), indexContent);
|
|
229
|
+
fs.writeFileSync(path.join(basePath, "api", "index.ts"), apiIndexContent);
|
|
230
|
+
fs.writeFileSync(
|
|
231
|
+
path.join(basePath, "api", `get-${kebabName}-list.ts`),
|
|
232
|
+
getListContent,
|
|
233
|
+
);
|
|
234
|
+
fs.writeFileSync(
|
|
235
|
+
path.join(basePath, "api", `get-${kebabName}-detail.ts`),
|
|
236
|
+
getDetailContent,
|
|
237
|
+
);
|
|
238
|
+
fs.writeFileSync(
|
|
239
|
+
path.join(basePath, "api", `${kebabName}-queries.ts`),
|
|
240
|
+
queriesContent,
|
|
241
|
+
);
|
|
242
|
+
fs.writeFileSync(
|
|
243
|
+
path.join(basePath, "api", "mapper", `map-${kebabName}.ts`),
|
|
244
|
+
mapperContent,
|
|
245
|
+
);
|
|
246
|
+
fs.writeFileSync(
|
|
247
|
+
path.join(basePath, "api", "mapper", `map-${kebabName}-detail.ts`),
|
|
248
|
+
mapperDetailContent,
|
|
249
|
+
);
|
|
250
|
+
fs.writeFileSync(
|
|
251
|
+
path.join(basePath, "api", "query", `${kebabName}-list-query.ts`),
|
|
252
|
+
queryContent,
|
|
253
|
+
);
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
path.join(basePath, "model", `${kebabName}.ts`),
|
|
256
|
+
modelContent,
|
|
257
|
+
);
|
|
258
|
+
fs.writeFileSync(
|
|
259
|
+
path.join(basePath, "model", `${kebabName}-detail.ts`),
|
|
260
|
+
detailContent,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// CLI 실행
|
|
265
|
+
const args = process.argv.slice(2);
|
|
266
|
+
|
|
267
|
+
if (args.length === 0) {
|
|
268
|
+
console.log("@blastlabs/utils - FSD Entity Generator\n");
|
|
269
|
+
console.log("사용법: npx blastlabs-generate-entity <entity-name>\n");
|
|
270
|
+
console.log("⚠️ entities 폴더 내에서 실행해주세요!\n");
|
|
271
|
+
console.log("예시:");
|
|
272
|
+
console.log(" cd src/entities");
|
|
273
|
+
console.log(" npx blastlabs-generate-entity user");
|
|
274
|
+
console.log(" npx blastlabs-generate-entity my-new-entity");
|
|
275
|
+
console.log(" npx blastlabs-generate-entity MyNewEntity");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const entityName = args[0];
|
|
280
|
+
const targetDir = process.cwd();
|
|
281
|
+
|
|
282
|
+
createEntityStructure(entityName, targetDir);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const TEMPLATES = {
|
|
7
|
+
base: "base.md",
|
|
8
|
+
fsd: "fsd.md",
|
|
9
|
+
testing: "testing.md",
|
|
10
|
+
nextjs: "nextjs.md",
|
|
11
|
+
vite: "vite.md",
|
|
12
|
+
monorepo: "monorepo.md",
|
|
13
|
+
project: "project-specific.md",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function parseArgs(args) {
|
|
17
|
+
const options = {
|
|
18
|
+
projectName: null,
|
|
19
|
+
projectDescription: "프로젝트 설명을 작성해주세요.",
|
|
20
|
+
framework: null, // 'nextjs' or 'vite'
|
|
21
|
+
fsd: false,
|
|
22
|
+
testing: false,
|
|
23
|
+
monorepo: false,
|
|
24
|
+
packageManager: "pnpm",
|
|
25
|
+
help: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
|
|
31
|
+
if (arg === "--help" || arg === "-h") {
|
|
32
|
+
options.help = true;
|
|
33
|
+
} else if (arg === "--next" || arg === "--nextjs") {
|
|
34
|
+
options.framework = "nextjs";
|
|
35
|
+
} else if (arg === "--vite") {
|
|
36
|
+
options.framework = "vite";
|
|
37
|
+
} else if (arg === "--fsd") {
|
|
38
|
+
options.fsd = true;
|
|
39
|
+
} else if (arg === "--testing" || arg === "--test") {
|
|
40
|
+
options.testing = true;
|
|
41
|
+
} else if (arg === "--monorepo") {
|
|
42
|
+
options.monorepo = true;
|
|
43
|
+
} else if (arg === "--npm") {
|
|
44
|
+
options.packageManager = "npm";
|
|
45
|
+
} else if (arg === "--yarn") {
|
|
46
|
+
options.packageManager = "yarn";
|
|
47
|
+
} else if (arg === "--pnpm") {
|
|
48
|
+
options.packageManager = "pnpm";
|
|
49
|
+
} else if (!arg.startsWith("--") && !options.projectName) {
|
|
50
|
+
options.projectName = arg;
|
|
51
|
+
} else if (!arg.startsWith("--") && options.projectName && !options.projectDescription) {
|
|
52
|
+
options.projectDescription = arg;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function showHelp() {
|
|
60
|
+
console.log("@blastlabs/utils - AI Rules Generator\n");
|
|
61
|
+
console.log("프로젝트에 .cursorrules와 CLAUDE.md 파일을 생성합니다.\n");
|
|
62
|
+
console.log("사용법: npx blastlabs-init-ai-rules <project-name> [options]\n");
|
|
63
|
+
console.log("Options:");
|
|
64
|
+
console.log(" --next, --nextjs Next.js 프로젝트 규칙 포함");
|
|
65
|
+
console.log(" --vite Vite 프로젝트 규칙 포함");
|
|
66
|
+
console.log(" --fsd Feature-Sliced Design 규칙 포함");
|
|
67
|
+
console.log(" --testing, --test 테스트 전략 규칙 포함");
|
|
68
|
+
console.log(" --monorepo 모노레포 규칙 포함");
|
|
69
|
+
console.log(" --npm npm 사용 (기본값: pnpm)");
|
|
70
|
+
console.log(" --yarn yarn 사용");
|
|
71
|
+
console.log(" --pnpm pnpm 사용 (기본값)");
|
|
72
|
+
console.log("\n예시:");
|
|
73
|
+
console.log(' npx blastlabs-init-ai-rules "My Project" --vite --fsd');
|
|
74
|
+
console.log(' npx blastlabs-init-ai-rules "Admin" --next --fsd --testing --monorepo');
|
|
75
|
+
console.log(' npx blastlabs-init-ai-rules "Web App" "React 웹 애플리케이션" --vite');
|
|
76
|
+
console.log("\n⚠️ 프로젝트 루트 디렉토리에서 실행해주세요!");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function loadTemplate(name) {
|
|
80
|
+
const templatePath = path.join(__dirname, "..", "templates", name);
|
|
81
|
+
try {
|
|
82
|
+
return fs.readFileSync(templatePath, "utf-8");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`❌ 템플릿 파일을 찾을 수 없습니다: ${name}`);
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function main() {
|
|
90
|
+
const args = process.argv.slice(2);
|
|
91
|
+
const options = parseArgs(args);
|
|
92
|
+
|
|
93
|
+
if (options.help || !options.projectName) {
|
|
94
|
+
showHelp();
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const targetDir = process.cwd();
|
|
99
|
+
|
|
100
|
+
// 템플릿 조합
|
|
101
|
+
let content = loadTemplate(TEMPLATES.base);
|
|
102
|
+
|
|
103
|
+
if (options.fsd) {
|
|
104
|
+
content += "\n" + loadTemplate(TEMPLATES.fsd);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.testing) {
|
|
108
|
+
content += "\n" + loadTemplate(TEMPLATES.testing);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (options.framework === "nextjs") {
|
|
112
|
+
content += "\n" + loadTemplate(TEMPLATES.nextjs);
|
|
113
|
+
} else if (options.framework === "vite") {
|
|
114
|
+
content += "\n" + loadTemplate(TEMPLATES.vite);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.monorepo) {
|
|
118
|
+
content += "\n" + loadTemplate(TEMPLATES.monorepo);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 항상 project-specific 섹션 추가
|
|
122
|
+
content += "\n" + loadTemplate(TEMPLATES.project);
|
|
123
|
+
|
|
124
|
+
// 플레이스홀더 치환
|
|
125
|
+
const frameworkName = options.framework === "nextjs" ? "Next.js" :
|
|
126
|
+
options.framework === "vite" ? "Vite" : "React";
|
|
127
|
+
|
|
128
|
+
content = content
|
|
129
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName)
|
|
130
|
+
.replace(/\{\{PROJECT_DESCRIPTION\}\}/g, options.projectDescription)
|
|
131
|
+
.replace(/\{\{PACKAGE_MANAGER\}\}/g, options.packageManager)
|
|
132
|
+
.replace(/\{\{FRAMEWORK\}\}/g, frameworkName);
|
|
133
|
+
|
|
134
|
+
// .cursorrules 파일 생성
|
|
135
|
+
const cursorrulesPath = path.join(targetDir, ".cursorrules");
|
|
136
|
+
if (fs.existsSync(cursorrulesPath)) {
|
|
137
|
+
console.log("⚠️ .cursorrules 파일이 이미 존재합니다. 덮어쓰기를 건너뜁니다.");
|
|
138
|
+
} else {
|
|
139
|
+
fs.writeFileSync(cursorrulesPath, content);
|
|
140
|
+
console.log("✅ .cursorrules 생성 완료");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// CLAUDE.md 파일 생성 (루트)
|
|
144
|
+
const claudePath = path.join(targetDir, "CLAUDE.md");
|
|
145
|
+
if (fs.existsSync(claudePath)) {
|
|
146
|
+
console.log("⚠️ CLAUDE.md 파일이 이미 존재합니다. 덮어쓰기를 건너뜁니다.");
|
|
147
|
+
} else {
|
|
148
|
+
fs.writeFileSync(claudePath, content);
|
|
149
|
+
console.log("✅ CLAUDE.md 생성 완료");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log("\n📁 생성된 파일들:");
|
|
153
|
+
console.log("├── .cursorrules");
|
|
154
|
+
console.log("└── CLAUDE.md");
|
|
155
|
+
|
|
156
|
+
console.log("\n📦 포함된 규칙:");
|
|
157
|
+
console.log("├── base (공통 규칙)");
|
|
158
|
+
if (options.fsd) console.log("├── fsd (Feature-Sliced Design)");
|
|
159
|
+
if (options.testing) console.log("├── testing (테스트 전략)");
|
|
160
|
+
if (options.framework) console.log(`├── ${options.framework}`);
|
|
161
|
+
if (options.monorepo) console.log("├── monorepo");
|
|
162
|
+
console.log("└── project-specific");
|
|
163
|
+
|
|
164
|
+
console.log("\n💡 Tip: 생성된 파일을 프로젝트에 맞게 수정하세요!");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blastlabs/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"blastlabs-generate-entity": "./bin/generate-entity.cjs",
|
|
9
|
+
"blastlabs-init-ai-rules": "./bin/init-ai-rules.cjs"
|
|
10
|
+
},
|
|
7
11
|
"files": [
|
|
8
|
-
"dist"
|
|
12
|
+
"dist",
|
|
13
|
+
"bin",
|
|
14
|
+
"templates"
|
|
9
15
|
],
|
|
10
16
|
"scripts": {
|
|
11
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
|
+
```
|