swagger-fsd-gen 1.0.2 → 1.0.4
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/CHANGELOG.md +18 -0
- package/README.md +13 -2
- package/USAGE.md +13 -0
- package/package.json +3 -2
- package/scripts/generate-all.js +141 -9
- package/templates/api.ejs +13 -6
- package/templates/procedure-call.ejs +50 -11
- package/templates/route-types.ejs +3 -2
- package/templates/tanstack-query/route-types.ejs +19 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.4
|
|
4
|
+
|
|
5
|
+
- Added stale generated module pruning (enabled by default) so removed/renamed Swagger tags no longer leave old `entities/{module}/api/*` files behind.
|
|
6
|
+
- Added `--prune-stale <true|false>` option to control stale-file pruning behavior.
|
|
7
|
+
- Fixed axios template method generation to use correct axios signatures:
|
|
8
|
+
- `post/put/patch(url, data, config)`
|
|
9
|
+
- `delete(url, config)` with optional `data` in config
|
|
10
|
+
- `get(url, config)` and other non-body methods.
|
|
11
|
+
- This resolves mixed-client leftovers (for example stale `ky` modules) when regenerating with `--http-client axios`.
|
|
12
|
+
|
|
13
|
+
## 1.0.3
|
|
14
|
+
|
|
15
|
+
- Fixed invalid TypeScript identifiers in generated mutation keys when API paths contain `-` (for example `undo-upload`, `applied-campaigns`, `notification-settings`, `custom-filters`).
|
|
16
|
+
- Added stable HTTP client selection via `--http-client <ky|axios>` (default: `ky` for backward compatibility).
|
|
17
|
+
- Updated generated API templates to emit correct imports/types/calls for both `ky` and `axios`.
|
|
18
|
+
- Updated TanStack mutation template to emit client-specific variable types and options (`KyInstance/Options` or `AxiosInstance/AxiosRequestConfig`).
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# swagger-fsd-gen
|
|
2
2
|
|
|
3
|
-
Swagger/OpenAPI 문서를 기반으로 **ky + TanStack Query + FSD(Feature-Sliced Design) 패턴**에 맞는 API 클라이언트를 자동으로 생성하는 도구입니다.
|
|
3
|
+
Swagger/OpenAPI 문서를 기반으로 **axios/ky + TanStack Query + FSD(Feature-Sliced Design) 패턴**에 맞는 API 클라이언트를 자동으로 생성하는 도구입니다.
|
|
4
4
|
|
|
5
5
|
## ✨ 주요 기능
|
|
6
6
|
|
|
7
|
-
- 🚀 **ky HTTP
|
|
7
|
+
- 🚀 **axios/ky HTTP 클라이언트 선택** 기반 API 클래스 자동 생성
|
|
8
8
|
- 🔄 **TanStack Query** 훅 자동 생성 (useQuery, useMutation)
|
|
9
9
|
- 📁 **FSD(Feature-Sliced Design)** 패턴 자동 적용
|
|
10
10
|
- 🔐 **HTTP Basic Authentication** 지원
|
|
@@ -33,6 +33,15 @@ npx generate-all --uri https://api.example.com/swagger.json --username your-user
|
|
|
33
33
|
# yarn
|
|
34
34
|
yarn fetch-swagger --url https://api.example.com/swagger.json --username your-username --password your-password
|
|
35
35
|
yarn generate-all --uri https://api.example.com/swagger.json --username your-username --password your-password
|
|
36
|
+
|
|
37
|
+
# axios 클라이언트로 생성
|
|
38
|
+
yarn generate-all --uri https://api.example.com/swagger.json --http-client axios
|
|
39
|
+
|
|
40
|
+
# ky 클라이언트로 생성 (기본값)
|
|
41
|
+
yarn generate-all --uri https://api.example.com/swagger.json --http-client ky
|
|
42
|
+
|
|
43
|
+
# stale 파일 정리를 끄고 생성
|
|
44
|
+
yarn generate-all --uri https://api.example.com/swagger.json --prune-stale false
|
|
36
45
|
```
|
|
37
46
|
|
|
38
47
|
### 2. package.json에 스크립트 추가 (권장)
|
|
@@ -91,6 +100,8 @@ src/
|
|
|
91
100
|
| --uri | Swagger 문서 URL/경로 | 필수 |
|
|
92
101
|
| --username | Basic Auth 사용자명 | - |
|
|
93
102
|
| --password | Basic Auth 비밀번호 | - |
|
|
103
|
+
| --http-client | HTTP 클라이언트 선택 | ky (`axios`, `ky`) |
|
|
104
|
+
| --prune-stale | stale 생성 파일 정리 | true (`true`, `false`) |
|
|
94
105
|
| --dto-output-path | DTO 파일 경로 | src/shared/api/dto.ts |
|
|
95
106
|
| --api-output-path | API 클래스 경로 | src/entities/{moduleName}/api/index.ts |
|
|
96
107
|
| --query-output-path | Query 훅 경로 | src/entities/{moduleName}/api/queries.ts |
|
package/USAGE.md
CHANGED
|
@@ -40,6 +40,15 @@ generate-all --uri https://api.example.com/swagger.json
|
|
|
40
40
|
# 로컬 파일에서 생성
|
|
41
41
|
generate-all --uri ./swagger/my-api.yml
|
|
42
42
|
|
|
43
|
+
# axios 클라이언트로 생성
|
|
44
|
+
generate-all --uri ./swagger/my-api.yml --http-client axios
|
|
45
|
+
|
|
46
|
+
# ky 클라이언트로 생성 (기본값)
|
|
47
|
+
generate-all --uri ./swagger/my-api.yml --http-client ky
|
|
48
|
+
|
|
49
|
+
# stale 생성 파일 정리 비활성화
|
|
50
|
+
generate-all --uri ./swagger/my-api.yml --prune-stale false
|
|
51
|
+
|
|
43
52
|
# 인증이 필요한 경우
|
|
44
53
|
generate-all --uri https://api.example.com/swagger.json --username admin --password secret
|
|
45
54
|
```
|
|
@@ -65,7 +74,11 @@ src/
|
|
|
65
74
|
### 1. 필요한 패키지 설치
|
|
66
75
|
|
|
67
76
|
```bash
|
|
77
|
+
# ky를 사용할 때
|
|
68
78
|
npm install ky @tanstack/react-query
|
|
79
|
+
|
|
80
|
+
# axios를 사용할 때
|
|
81
|
+
npm install axios @tanstack/react-query
|
|
69
82
|
```
|
|
70
83
|
|
|
71
84
|
### 2. API 인스턴스 설정
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swagger-fsd-gen",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"main": "index.js",
|
|
5
|
-
"description": "Swagger API client generator that creates type-safe API clients using ky and TanStack Query with Feature Sliced Design pattern. Automatically generates API client code from Swagger/OpenAPI specifications.",
|
|
5
|
+
"description": "Swagger API client generator that creates type-safe API clients using axios or ky and TanStack Query with Feature Sliced Design pattern. Automatically generates API client code from Swagger/OpenAPI specifications.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
8
|
},
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"api-client",
|
|
13
13
|
"typescript",
|
|
14
14
|
"ky",
|
|
15
|
+
"axios",
|
|
15
16
|
"tanstack-query",
|
|
16
17
|
"code-generator",
|
|
17
18
|
"fsd"
|
package/scripts/generate-all.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Swagger API 클라이언트 자동 생성 도구
|
|
5
|
-
* - ky HTTP 클라이언트 기반 API 클래스 생성
|
|
5
|
+
* - axios/ky HTTP 클라이언트 기반 API 클래스 생성
|
|
6
6
|
* - TanStack Query 훅 생성 (useQuery, useMutation)
|
|
7
7
|
* - FSD(Feature-Sliced Design) 패턴 적용
|
|
8
8
|
*/
|
|
@@ -72,6 +72,7 @@ const parseArguments = () => {
|
|
|
72
72
|
"uri",
|
|
73
73
|
"username",
|
|
74
74
|
"password",
|
|
75
|
+
"http-client",
|
|
75
76
|
"dto-output-path",
|
|
76
77
|
"api-output-path",
|
|
77
78
|
"api-instance-output-path",
|
|
@@ -79,16 +80,22 @@ const parseArguments = () => {
|
|
|
79
80
|
"mutation-output-path",
|
|
80
81
|
"project-template",
|
|
81
82
|
],
|
|
83
|
+
boolean: ["prune-stale"],
|
|
82
84
|
alias: {
|
|
83
85
|
u: "uri",
|
|
84
86
|
un: "username",
|
|
85
87
|
pw: "password",
|
|
88
|
+
hc: "http-client",
|
|
86
89
|
dp: "dto-output-path",
|
|
87
90
|
ap: "api-output-path",
|
|
88
91
|
aip: "api-instance-output-path",
|
|
89
92
|
qp: "query-output-path",
|
|
90
93
|
mp: "mutation-output-path",
|
|
91
94
|
pt: "project-template",
|
|
95
|
+
ps: "prune-stale",
|
|
96
|
+
},
|
|
97
|
+
default: {
|
|
98
|
+
"prune-stale": true,
|
|
92
99
|
},
|
|
93
100
|
});
|
|
94
101
|
|
|
@@ -96,15 +103,34 @@ const parseArguments = () => {
|
|
|
96
103
|
uri: argv.uri,
|
|
97
104
|
username: argv.username,
|
|
98
105
|
password: argv.password,
|
|
106
|
+
httpClient: argv["http-client"],
|
|
99
107
|
dtoOutputPath: argv["dto-output-path"],
|
|
100
108
|
apiOutputPath: argv["api-output-path"],
|
|
101
109
|
apiInstanceOutputPath: argv["api-instance-output-path"],
|
|
102
110
|
queryOutputPath: argv["query-output-path"],
|
|
103
111
|
mutationOutputPath: argv["mutation-output-path"],
|
|
104
112
|
projectTemplate: argv["project-template"],
|
|
113
|
+
pruneStale: argv["prune-stale"],
|
|
105
114
|
};
|
|
106
115
|
};
|
|
107
116
|
|
|
117
|
+
/**
|
|
118
|
+
* HTTP 클라이언트 모드 정규화
|
|
119
|
+
* @param {string | undefined} input
|
|
120
|
+
* @returns {"axios" | "ky"}
|
|
121
|
+
*/
|
|
122
|
+
const resolveHttpClient = (input) => {
|
|
123
|
+
const normalized = (input ?? "ky").toString().toLowerCase();
|
|
124
|
+
|
|
125
|
+
if (normalized === "axios" || normalized === "ky") {
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Invalid --http-client value "${input}". Supported values: axios | ky`
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
108
134
|
/**
|
|
109
135
|
* 출력 경로 설정 (FSD 패턴 기본값)
|
|
110
136
|
* @param {Object} args - 명령행 인수
|
|
@@ -172,10 +198,12 @@ const printUsage = (outputPaths) => {
|
|
|
172
198
|
console.error(
|
|
173
199
|
"Usage: generate-all --uri <swagger-url|swagger-file-name> " +
|
|
174
200
|
"[--username <username>] [--password <password>] " +
|
|
201
|
+
"[--http-client <axios|ky>] " +
|
|
175
202
|
"[--dto-output-path <dto-output-path>] " +
|
|
176
203
|
"[--api-output-path <api-output-path>] " +
|
|
177
204
|
"[--query-output-path <query-output-path>] " +
|
|
178
205
|
"[--mutation-output-path <mutation-output-path>] " +
|
|
206
|
+
"[--prune-stale <true|false>] " +
|
|
179
207
|
"[--project-template <project-template>]"
|
|
180
208
|
);
|
|
181
209
|
console.error(
|
|
@@ -187,6 +215,79 @@ const printUsage = (outputPaths) => {
|
|
|
187
215
|
);
|
|
188
216
|
};
|
|
189
217
|
|
|
218
|
+
/**
|
|
219
|
+
* 모듈 파일명에서 모듈명을 추출
|
|
220
|
+
* @param {string} fileName
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
const extractModuleName = (fileName) => fileName.replace("Route", "").toLowerCase();
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 템플릿 경로에서 현재 생성 대상이 아닌 모듈 파일을 정리
|
|
227
|
+
* @param {string} templatePath
|
|
228
|
+
* @param {Set<string>} generatedModules
|
|
229
|
+
*/
|
|
230
|
+
const pruneStaleFilesByTemplate = async (templatePath, generatedModules) => {
|
|
231
|
+
if (!templatePath.includes("{moduleName}")) return;
|
|
232
|
+
|
|
233
|
+
const [prefix, suffix] = templatePath.split("{moduleName}");
|
|
234
|
+
const modulesRoot = prefix;
|
|
235
|
+
|
|
236
|
+
if (!fs.existsSync(modulesRoot)) return;
|
|
237
|
+
|
|
238
|
+
const moduleDirs = await fs.promises.readdir(modulesRoot, {
|
|
239
|
+
withFileTypes: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
for (const dirent of moduleDirs) {
|
|
243
|
+
if (!dirent.isDirectory()) continue;
|
|
244
|
+
|
|
245
|
+
const moduleName = dirent.name;
|
|
246
|
+
if (generatedModules.has(moduleName)) continue;
|
|
247
|
+
|
|
248
|
+
const targetFilePath = path.join(modulesRoot, moduleName, suffix);
|
|
249
|
+
if (!fs.existsSync(targetFilePath)) continue;
|
|
250
|
+
|
|
251
|
+
await fs.promises.unlink(targetFilePath);
|
|
252
|
+
console.log(`🧹 Pruned stale generated file: ${targetFilePath}`);
|
|
253
|
+
|
|
254
|
+
const removableDirs = [
|
|
255
|
+
path.dirname(targetFilePath),
|
|
256
|
+
path.dirname(path.dirname(targetFilePath)),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
for (const candidateDir of removableDirs) {
|
|
260
|
+
if (!candidateDir.startsWith(modulesRoot)) continue;
|
|
261
|
+
if (!fs.existsSync(candidateDir)) continue;
|
|
262
|
+
|
|
263
|
+
const entries = await fs.promises.readdir(candidateDir);
|
|
264
|
+
if (entries.length === 0) {
|
|
265
|
+
await fs.promises.rmdir(candidateDir);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 이전 스웨거 기준 stale 생성 파일 정리
|
|
273
|
+
* @param {Object} outputPaths
|
|
274
|
+
* @param {Set<string>} generatedModules
|
|
275
|
+
*/
|
|
276
|
+
const pruneStaleGeneratedFiles = async (outputPaths, generatedModules) => {
|
|
277
|
+
if (generatedModules.size === 0) return;
|
|
278
|
+
|
|
279
|
+
await pruneStaleFilesByTemplate(outputPaths.api.absolutePath, generatedModules);
|
|
280
|
+
await pruneStaleFilesByTemplate(
|
|
281
|
+
outputPaths.apiInstance.absolutePath,
|
|
282
|
+
generatedModules
|
|
283
|
+
);
|
|
284
|
+
await pruneStaleFilesByTemplate(outputPaths.query.absolutePath, generatedModules);
|
|
285
|
+
await pruneStaleFilesByTemplate(
|
|
286
|
+
outputPaths.mutation.absolutePath,
|
|
287
|
+
generatedModules
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
190
291
|
/**
|
|
191
292
|
* swagger-typescript-api를 사용하여 API 코드 생성
|
|
192
293
|
* @param {Object} params - 생성 파라미터
|
|
@@ -196,10 +297,12 @@ export const generateApiCode = async ({
|
|
|
196
297
|
uri,
|
|
197
298
|
username,
|
|
198
299
|
password,
|
|
300
|
+
httpClient,
|
|
199
301
|
templates,
|
|
200
302
|
...params
|
|
201
303
|
}) => {
|
|
202
304
|
const isLocal = !isUrl(uri);
|
|
305
|
+
const httpClientType = httpClient === "axios" ? "axios" : "fetch";
|
|
203
306
|
|
|
204
307
|
return generateApi({
|
|
205
308
|
input: isLocal ? path.resolve(process.cwd(), uri) : undefined,
|
|
@@ -226,6 +329,7 @@ export const generateApiCode = async ({
|
|
|
226
329
|
schemaParsers: {
|
|
227
330
|
complexAnyOf: AnyOfSchemaParser,
|
|
228
331
|
},
|
|
332
|
+
httpClientType,
|
|
229
333
|
...params,
|
|
230
334
|
});
|
|
231
335
|
};
|
|
@@ -248,7 +352,7 @@ const formatWithProjectPrettier = (filePath) => {
|
|
|
248
352
|
* @param {Object} outputPaths - 출력 경로 설정
|
|
249
353
|
*/
|
|
250
354
|
const generateApiFunctionCode = async (args, outputPaths) => {
|
|
251
|
-
const { projectTemplate, uri, username, password } = args;
|
|
355
|
+
const { projectTemplate, uri, username, password, httpClient } = args;
|
|
252
356
|
|
|
253
357
|
const templatePath = projectTemplate
|
|
254
358
|
? path.resolve(process.cwd(), projectTemplate)
|
|
@@ -260,10 +364,14 @@ const generateApiFunctionCode = async (args, outputPaths) => {
|
|
|
260
364
|
uri,
|
|
261
365
|
username,
|
|
262
366
|
password,
|
|
367
|
+
httpClient,
|
|
263
368
|
templates: templatePath,
|
|
264
|
-
|
|
369
|
+
// swagger-typescript-api + prettier 조합에서 parser 추론 실패를 방지
|
|
370
|
+
prettier: { parser: "typescript" },
|
|
265
371
|
});
|
|
266
372
|
|
|
373
|
+
const generatedModules = new Set();
|
|
374
|
+
|
|
267
375
|
for (const { fileName, fileContent } of apiFunctionCode.files) {
|
|
268
376
|
if (fileName === "http-client") continue;
|
|
269
377
|
|
|
@@ -274,7 +382,8 @@ const generateApiFunctionCode = async (args, outputPaths) => {
|
|
|
274
382
|
formatWithProjectPrettier(outputPath);
|
|
275
383
|
console.log(`✅ Generated DTO: ${outputPaths.dto.relativePath}`);
|
|
276
384
|
} else {
|
|
277
|
-
const moduleName = fileName
|
|
385
|
+
const moduleName = extractModuleName(fileName);
|
|
386
|
+
generatedModules.add(moduleName);
|
|
278
387
|
|
|
279
388
|
if (fileName.match(/Route$/)) {
|
|
280
389
|
outputPath = outputPaths.apiInstance.absolutePath.replace(
|
|
@@ -299,6 +408,8 @@ const generateApiFunctionCode = async (args, outputPaths) => {
|
|
|
299
408
|
}
|
|
300
409
|
}
|
|
301
410
|
}
|
|
411
|
+
|
|
412
|
+
return generatedModules;
|
|
302
413
|
};
|
|
303
414
|
|
|
304
415
|
/**
|
|
@@ -307,7 +418,7 @@ const generateApiFunctionCode = async (args, outputPaths) => {
|
|
|
307
418
|
* @param {Object} outputPaths - 출력 경로 설정
|
|
308
419
|
*/
|
|
309
420
|
const generateTanstackQueryCode = async (args, outputPaths) => {
|
|
310
|
-
const { projectTemplate, uri, username, password } = args;
|
|
421
|
+
const { projectTemplate, uri, username, password, httpClient } = args;
|
|
311
422
|
|
|
312
423
|
const templatePath = projectTemplate
|
|
313
424
|
? path.resolve(process.cwd(), projectTemplate, "tanstack-query")
|
|
@@ -319,14 +430,19 @@ const generateTanstackQueryCode = async (args, outputPaths) => {
|
|
|
319
430
|
uri,
|
|
320
431
|
username,
|
|
321
432
|
password,
|
|
433
|
+
httpClient,
|
|
322
434
|
templates: templatePath,
|
|
323
|
-
|
|
435
|
+
// swagger-typescript-api + prettier 조합에서 parser 추론 실패를 방지
|
|
436
|
+
prettier: { parser: "typescript" },
|
|
324
437
|
});
|
|
325
438
|
|
|
439
|
+
const generatedModules = new Set();
|
|
440
|
+
|
|
326
441
|
for (const { fileName, fileContent } of tanstackQueryCode.files) {
|
|
327
442
|
if (fileName === "http-client" || fileName === "data-contracts") continue;
|
|
328
443
|
|
|
329
|
-
const moduleName = fileName
|
|
444
|
+
const moduleName = extractModuleName(fileName);
|
|
445
|
+
generatedModules.add(moduleName);
|
|
330
446
|
let outputPath;
|
|
331
447
|
|
|
332
448
|
if (fileName.match(/Route$/)) {
|
|
@@ -351,6 +467,8 @@ const generateTanstackQueryCode = async (args, outputPaths) => {
|
|
|
351
467
|
);
|
|
352
468
|
}
|
|
353
469
|
}
|
|
470
|
+
|
|
471
|
+
return generatedModules;
|
|
354
472
|
};
|
|
355
473
|
|
|
356
474
|
/**
|
|
@@ -369,11 +487,25 @@ const main = async () => {
|
|
|
369
487
|
}
|
|
370
488
|
|
|
371
489
|
try {
|
|
490
|
+
args.httpClient = resolveHttpClient(args.httpClient);
|
|
491
|
+
console.log(`🌐 HTTP client mode: ${args.httpClient}`);
|
|
492
|
+
|
|
372
493
|
// 1. API 클래스와 DTO 생성
|
|
373
|
-
await generateApiFunctionCode(args, outputPaths);
|
|
494
|
+
const generatedApiModules = await generateApiFunctionCode(args, outputPaths);
|
|
374
495
|
|
|
375
496
|
// 2. TanStack Query 훅 생성
|
|
376
|
-
await generateTanstackQueryCode(
|
|
497
|
+
const generatedTanstackModules = await generateTanstackQueryCode(
|
|
498
|
+
args,
|
|
499
|
+
outputPaths
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
if (args.pruneStale) {
|
|
503
|
+
const generatedModules = new Set([
|
|
504
|
+
...generatedApiModules,
|
|
505
|
+
...generatedTanstackModules,
|
|
506
|
+
]);
|
|
507
|
+
await pruneStaleGeneratedFiles(outputPaths, generatedModules);
|
|
508
|
+
}
|
|
377
509
|
|
|
378
510
|
console.log("\n🎉 API client generation completed successfully!");
|
|
379
511
|
console.log("\n📁 Generated files:");
|
package/templates/api.ejs
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* API 클래스 생성 템플릿
|
|
4
4
|
* - Swagger 태그별로 API 클래스 생성
|
|
5
|
-
* - ky HTTP 클라이언트 기반
|
|
5
|
+
* - axios/ky HTTP 클라이언트 기반
|
|
6
6
|
* - 타입 안전성을 위한 TypeScript 지원
|
|
7
7
|
* - 쿼리 파라미터 처리를 위한 유틸리티 메서드 포함
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { utils, route, config, modelTypes } = it;
|
|
11
11
|
const { _, pascalCase, require } = utils;
|
|
12
|
+
const isAxios = config.httpClientType === 'axios';
|
|
12
13
|
|
|
13
14
|
// 모듈명을 PascalCase로 변환하여 클래스명 생성 (예: user -> UserApi)
|
|
14
15
|
const apiClassName = pascalCase(route.moduleName);
|
|
@@ -20,27 +21,32 @@ const routes = route.routes;
|
|
|
20
21
|
const dataContracts = _.map(modelTypes, "name");
|
|
21
22
|
%>
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
<% if (isAxios) { %>
|
|
25
|
+
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
26
|
+
<% } else { %>
|
|
27
|
+
import type { KyInstance, Options } from 'ky';
|
|
28
|
+
<% } %>
|
|
24
29
|
|
|
25
30
|
<%~ includeFile('./dto-import.ejs', { ...it }) %>
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
33
|
* <%= route.moduleName %> 모듈 API 클래스
|
|
29
|
-
* -
|
|
34
|
+
* - 선택된 HTTP 클라이언트를 사용한 API 호출
|
|
30
35
|
* - 타입 안전성을 위한 TypeScript 지원
|
|
31
36
|
* - 쿼리 파라미터 자동 변환 기능
|
|
32
37
|
*/
|
|
33
38
|
export class <%= apiClassName %>Api {
|
|
34
|
-
private readonly instance: KyInstance
|
|
39
|
+
private readonly instance: <%= isAxios ? 'AxiosInstance' : 'KyInstance' %>;
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* API 클래스 생성자
|
|
38
|
-
* @param instance -
|
|
43
|
+
* @param instance - HTTP 클라이언트 인스턴스
|
|
39
44
|
*/
|
|
40
|
-
constructor(instance: KyInstance) {
|
|
45
|
+
constructor(instance: <%= isAxios ? 'AxiosInstance' : 'KyInstance' %>) {
|
|
41
46
|
this.instance = instance;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
<% if (!isAxios) { %>
|
|
44
50
|
/**
|
|
45
51
|
* 객체를 URLSearchParams로 변환하는 정적 유틸리티 메서드
|
|
46
52
|
* - 중첩된 객체나 배열을 쿼리 파라미터로 변환
|
|
@@ -70,6 +76,7 @@ static createSearchParams(
|
|
|
70
76
|
|
|
71
77
|
return urlSearchParams;
|
|
72
78
|
}
|
|
79
|
+
<% } %>
|
|
73
80
|
|
|
74
81
|
<% for (const route of routes) { %>
|
|
75
82
|
<%~ includeFile('./procedure-call.ejs', { ...it, route, apiClassName }) %>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const { utils, route, config, apiClassName } = it;
|
|
11
11
|
const { _, pascalCase } = utils;
|
|
12
12
|
const routeDocs = includeFile("./route-docs", { config, route, utils });
|
|
13
|
+
const isAxios = config.httpClientType === 'axios';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* API 메서드명 생성
|
|
@@ -42,6 +43,7 @@ const headerName = headers ? `${pascalCase(route.routeName.usage)}Headers` : nul
|
|
|
42
43
|
|
|
43
44
|
// 쿼리 파라미터 타입명 생성
|
|
44
45
|
const queryTypeName = query ? `${pascalCase(route.routeName.usage)}QueryParams` : null;
|
|
46
|
+
const instanceParamName = isAxios ? 'axiosInstance' : 'kyInstance';
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* 메서드 파라미터 문자열 생성
|
|
@@ -61,20 +63,21 @@ const generateParams = (params, query, payload, headerName) => {
|
|
|
61
63
|
...(payload ? [`data${payload.optional ? '?' : ''}: ${payload.type}`] : [])
|
|
62
64
|
];
|
|
63
65
|
|
|
64
|
-
//
|
|
66
|
+
// HTTP client options에서 제외할 타입들 (중복 방지)
|
|
65
67
|
const omitTypes = [
|
|
66
|
-
...(query ? ["'searchParams'"] : []),
|
|
67
|
-
...(payload ? ["'json'"] : []),
|
|
68
|
+
...(query ? [isAxios ? "'params'" : "'searchParams'"] : []),
|
|
69
|
+
...(payload ? [isAxios ? "'data'" : "'json'"] : []),
|
|
68
70
|
...(headerName ? ["'headers'"] : [])
|
|
69
71
|
];
|
|
70
72
|
|
|
71
|
-
//
|
|
73
|
+
// HTTP client options 타입 생성 (중복되는 속성 제외)
|
|
74
|
+
const optionsBaseType = isAxios ? 'AxiosRequestConfig' : 'Options';
|
|
72
75
|
const optionsType = omitTypes.length > 0
|
|
73
|
-
? `Omit
|
|
74
|
-
:
|
|
76
|
+
? `Omit<${optionsBaseType}, ${omitTypes.join(' | ')}>${headerName ? ` & { headers: ${headerName} }` : ''}`
|
|
77
|
+
: optionsBaseType;
|
|
75
78
|
|
|
76
|
-
//
|
|
77
|
-
paramList.push(
|
|
79
|
+
// HTTP 클라이언트 인스턴스와 옵션 파라미터 추가
|
|
80
|
+
paramList.push(`${instanceParamName}?: ${isAxios ? 'AxiosInstance' : 'KyInstance'}`, `options?: ${optionsType}`);
|
|
78
81
|
return paramList.join(', ');
|
|
79
82
|
};
|
|
80
83
|
%>
|
|
@@ -82,14 +85,49 @@ const generateParams = (params, query, payload, headerName) => {
|
|
|
82
85
|
/**
|
|
83
86
|
<%~ routeDocs.lines %> */
|
|
84
87
|
<%= functionName %>(<%~ generateParams(pathParams, query, payload, headerName) %>) {
|
|
85
|
-
//
|
|
86
|
-
const instance =
|
|
88
|
+
// HTTP 클라이언트 인스턴스 선택 (전달된 인스턴스 또는 기본 인스턴스)
|
|
89
|
+
const instance = <%= instanceParamName %> ?? this.instance;
|
|
87
90
|
|
|
88
|
-
<% if (query) { %>
|
|
91
|
+
<% if (query && !isAxios) { %>
|
|
89
92
|
// 쿼리 파라미터를 URLSearchParams로 변환
|
|
90
93
|
const urlSearchParams = <%= apiClassName %>Api.createSearchParams(params);
|
|
91
94
|
<% } %>
|
|
92
95
|
|
|
96
|
+
<% if (isAxios) { %>
|
|
97
|
+
<% if (['post', 'put', 'patch'].includes(route.request.method.toLowerCase())) { %>
|
|
98
|
+
return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(
|
|
99
|
+
`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`,
|
|
100
|
+
<% if (payload) { %>
|
|
101
|
+
data,
|
|
102
|
+
<% } else { %>
|
|
103
|
+
undefined,
|
|
104
|
+
<% } %>
|
|
105
|
+
{
|
|
106
|
+
<% if (query) { %>
|
|
107
|
+
params, // 쿼리 파라미터 설정
|
|
108
|
+
<% } %>
|
|
109
|
+
...options, // 추가 axios 옵션 병합
|
|
110
|
+
}
|
|
111
|
+
).then((response) => response.data);
|
|
112
|
+
<% } else if (route.request.method.toLowerCase() === 'delete') { %>
|
|
113
|
+
return instance.delete<<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`, {
|
|
114
|
+
<% if (query) { %>
|
|
115
|
+
params, // 쿼리 파라미터 설정
|
|
116
|
+
<% } %>
|
|
117
|
+
<% if (payload) { %>
|
|
118
|
+
data, // 요청 본문 설정
|
|
119
|
+
<% } %>
|
|
120
|
+
...options, // 추가 axios 옵션 병합
|
|
121
|
+
}).then((response) => response.data);
|
|
122
|
+
<% } else { %>
|
|
123
|
+
return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`, {
|
|
124
|
+
<% if (query) { %>
|
|
125
|
+
params, // 쿼리 파라미터 설정
|
|
126
|
+
<% } %>
|
|
127
|
+
...options, // 추가 axios 옵션 병합
|
|
128
|
+
}).then((response) => response.data);
|
|
129
|
+
<% } %>
|
|
130
|
+
<% } else { %>
|
|
93
131
|
// HTTP 요청 실행 및 JSON 응답 반환
|
|
94
132
|
return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`,{
|
|
95
133
|
<% if (query) { %>
|
|
@@ -100,4 +138,5 @@ return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`
|
|
|
100
138
|
<% } %>
|
|
101
139
|
...options, // 추가 ky 옵션 병합
|
|
102
140
|
}).json();
|
|
141
|
+
<% } %>
|
|
103
142
|
}
|
|
@@ -4,12 +4,13 @@ const { _, pascalCase, require } = utils;
|
|
|
4
4
|
const apiClassName = pascalCase(route.moduleName);
|
|
5
5
|
const routes = route.routes;
|
|
6
6
|
const dataContracts = _.map(modelTypes, "name");
|
|
7
|
+
const isAxios = config.httpClientType === 'axios';
|
|
7
8
|
%>
|
|
8
9
|
|
|
9
|
-
import { kyInstance } from "@/shared/api"
|
|
10
|
+
import { <%= isAxios ? 'axiosInstance' : 'kyInstance' %> } from "@/shared/api"
|
|
10
11
|
|
|
11
12
|
import { <%= apiClassName %>Api } from './index'
|
|
12
13
|
|
|
13
|
-
const <%= route.moduleName %>Api = new <%= apiClassName %>Api(kyInstance);
|
|
14
|
+
const <%= route.moduleName %>Api = new <%= apiClassName %>Api(<%= isAxios ? 'axiosInstance' : 'kyInstance' %>);
|
|
14
15
|
|
|
15
16
|
export { <%= route.moduleName %>Api };
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
const { utils, route, modelTypes, config } = it;
|
|
7
7
|
const { pascalCase, internalCase: camelCase } = utils;
|
|
8
8
|
const { moduleName, routes } = route;
|
|
9
|
+
const isAxios = config.httpClientType === 'axios';
|
|
10
|
+
const clientInstanceParam = isAxios ? 'axiosInstance' : 'kyInstance';
|
|
11
|
+
const clientInstanceType = isAxios ? 'AxiosInstance' : 'KyInstance';
|
|
12
|
+
const clientOptionsType = isAxios ? 'AxiosRequestConfig' : 'Options';
|
|
9
13
|
|
|
10
14
|
const getFunctionName = ({ request: { method, path } }) =>
|
|
11
15
|
`${method}${pascalCase(path
|
|
@@ -18,6 +22,12 @@ const getFunctionName = ({ request: { method, path } }) =>
|
|
|
18
22
|
.join('_'))}`;
|
|
19
23
|
|
|
20
24
|
const removeBracket = (type) => type.replace(/[()]/g, '')
|
|
25
|
+
const sanitizeIdentifierSegment = (segment) =>
|
|
26
|
+
segment
|
|
27
|
+
.replace(/[${}]/g, '')
|
|
28
|
+
.replace(/[^a-zA-Z0-9]/g, '_')
|
|
29
|
+
.replace(/_+/g, '_')
|
|
30
|
+
.replace(/^_+|_+$/g, '');
|
|
21
31
|
|
|
22
32
|
const getRequestParams = ({ parameters = [], query, payload, routeName }) => {
|
|
23
33
|
const queryParamsDto = query ? `${pascalCase(routeName.usage)}QueryParams` : '';
|
|
@@ -40,10 +50,9 @@ const buildMutationKeyName = ({ path, method }) =>
|
|
|
40
50
|
.split('/')
|
|
41
51
|
.filter(segment => segment )
|
|
42
52
|
.map(segment =>
|
|
43
|
-
segment.
|
|
44
|
-
? segment.replace(/[${}]/g, '').toUpperCase()
|
|
45
|
-
: segment.toUpperCase()
|
|
53
|
+
sanitizeIdentifierSegment(segment).toUpperCase()
|
|
46
54
|
)
|
|
55
|
+
.filter(Boolean)
|
|
47
56
|
.join('_')
|
|
48
57
|
|
|
49
58
|
const buildPathMutationKey = ({ path }) =>
|
|
@@ -84,7 +93,7 @@ if (mutationConfigs.length > 0) {
|
|
|
84
93
|
.filter(config => config.hasVariables)
|
|
85
94
|
.map(config => {
|
|
86
95
|
const headerName = config.route.specificArgs.headers ? `${pascalCase(config.route.routeName.usage)}Headers` : null;
|
|
87
|
-
return `type T${pascalCase(config.functionName)}Variables = { ${config.requestParams},
|
|
96
|
+
return `type T${pascalCase(config.functionName)}Variables = { ${config.requestParams}, ${clientInstanceParam}?: ${clientInstanceType}; options?: ${headerName ? `Omit<${clientOptionsType}, 'headers'> & { headers: ${headerName} }` : clientOptionsType}; };`
|
|
88
97
|
})
|
|
89
98
|
.join('\n');
|
|
90
99
|
|
|
@@ -98,7 +107,11 @@ UseMutationOptions,
|
|
|
98
107
|
useQueryClient,
|
|
99
108
|
} from '@tanstack/react-query';
|
|
100
109
|
import { useMutation } from '@tanstack/react-query';
|
|
110
|
+
<% if (isAxios) { %>
|
|
111
|
+
import type{ AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
112
|
+
<% } else { %>
|
|
101
113
|
import type{ KyInstance, Options } from 'ky';
|
|
114
|
+
<% } %>
|
|
102
115
|
|
|
103
116
|
<%~ includeFile('../dto-import.ejs', { ...it }) %>
|
|
104
117
|
|
|
@@ -114,8 +127,8 @@ const mutations = {
|
|
|
114
127
|
<% for (const config of mutationConfigs) { %>
|
|
115
128
|
<%= config.functionName %>: () => ({
|
|
116
129
|
mutationFn: <% if (config.hasVariables) { %>(variables: T<%= pascalCase(config.functionName) %>Variables) => {
|
|
117
|
-
const { <%= config.requestParamsWithoutTypes %>,
|
|
118
|
-
return <%= moduleName %>Api.<%= config.functionName %>(<%= config.requestParamsWithoutTypes %>,
|
|
130
|
+
const { <%= config.requestParamsWithoutTypes %>, <%= clientInstanceParam %>, options } = variables;
|
|
131
|
+
return <%= moduleName %>Api.<%= config.functionName %>(<%= config.requestParamsWithoutTypes %>, <%= clientInstanceParam %>, options);
|
|
119
132
|
}<% } else { %>() => <%= moduleName %>Api.<%= config.functionName %>()<% } %>,
|
|
120
133
|
mutationKey: <% ~moduleName.toUpperCase() %>_MUTATION_KEY.<% ~config.mutationKeyName %>,
|
|
121
134
|
}),
|