@blastlabs/utils 1.12.1 → 1.13.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/entity-generator-guide.md +126 -0
- package/bin/generate-entity.cjs +282 -0
- package/package.json +6 -2
|
@@ -0,0 +1,126 @@
|
|
|
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 폴더를 참고하여 패턴을 확인할 수 있습니다
|
|
@@ -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);
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blastlabs/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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
|
+
},
|
|
7
10
|
"files": [
|
|
8
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
9
13
|
],
|
|
10
14
|
"scripts": {
|
|
11
15
|
"prepare": "npm run build:tsc",
|