swagger-fsd-gen 1.0.1 → 1.0.3
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 +8 -0
- package/README.md +9 -2
- package/USAGE.md +10 -0
- package/package.json +3 -2
- package/scripts/generate-all.js +123 -33
- package/templates/api.ejs +13 -6
- package/templates/procedure-call.ejs +26 -11
- package/templates/route-types.ejs +3 -2
- package/templates/tanstack-query/route-types.ejs +19 -6
- package/swagger-fsd-gen-1.0.0.tgz +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.3
|
|
4
|
+
|
|
5
|
+
- Fixed invalid TypeScript identifiers in generated mutation keys when API paths contain `-` (for example `undo-upload`, `applied-campaigns`, `notification-settings`, `custom-filters`).
|
|
6
|
+
- Added stable HTTP client selection via `--http-client <ky|axios>` (default: `ky` for backward compatibility).
|
|
7
|
+
- Updated generated API templates to emit correct imports/types/calls for both `ky` and `axios`.
|
|
8
|
+
- 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,12 @@ 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
|
|
36
42
|
```
|
|
37
43
|
|
|
38
44
|
### 2. package.json에 스크립트 추가 (권장)
|
|
@@ -91,6 +97,7 @@ src/
|
|
|
91
97
|
| --uri | Swagger 문서 URL/경로 | 필수 |
|
|
92
98
|
| --username | Basic Auth 사용자명 | - |
|
|
93
99
|
| --password | Basic Auth 비밀번호 | - |
|
|
100
|
+
| --http-client | HTTP 클라이언트 선택 | ky (`axios`, `ky`) |
|
|
94
101
|
| --dto-output-path | DTO 파일 경로 | src/shared/api/dto.ts |
|
|
95
102
|
| --api-output-path | API 클래스 경로 | src/entities/{moduleName}/api/index.ts |
|
|
96
103
|
| --query-output-path | Query 훅 경로 | src/entities/{moduleName}/api/queries.ts |
|
package/USAGE.md
CHANGED
|
@@ -40,6 +40,12 @@ 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
|
+
|
|
43
49
|
# 인증이 필요한 경우
|
|
44
50
|
generate-all --uri https://api.example.com/swagger.json --username admin --password secret
|
|
45
51
|
```
|
|
@@ -65,7 +71,11 @@ src/
|
|
|
65
71
|
### 1. 필요한 패키지 설치
|
|
66
72
|
|
|
67
73
|
```bash
|
|
74
|
+
# ky를 사용할 때
|
|
68
75
|
npm install ky @tanstack/react-query
|
|
76
|
+
|
|
77
|
+
# axios를 사용할 때
|
|
78
|
+
npm install axios @tanstack/react-query
|
|
69
79
|
```
|
|
70
80
|
|
|
71
81
|
### 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.3",
|
|
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
|
*/
|
|
@@ -15,10 +15,53 @@ import { fetchSwagger } from "../utils/fetch-swagger.js";
|
|
|
15
15
|
import { writeFileToPath } from "../utils/file.js";
|
|
16
16
|
import { AnyOfSchemaParser } from "../utils/parser.js";
|
|
17
17
|
import { isUrl } from "../utils/url.js";
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import { execSync } from "child_process";
|
|
18
20
|
|
|
19
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
22
|
const __dirname = path.dirname(__filename);
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 프로젝트의 Prettier 설정을 로드
|
|
26
|
+
* @returns {Object} Prettier 설정
|
|
27
|
+
*/
|
|
28
|
+
const loadPrettierConfig = () => {
|
|
29
|
+
const configPaths = [
|
|
30
|
+
".prettierrc",
|
|
31
|
+
".prettierrc.json",
|
|
32
|
+
".prettierrc.js",
|
|
33
|
+
".prettierrc.cjs",
|
|
34
|
+
"prettier.config.js",
|
|
35
|
+
"prettier.config.cjs",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const configPath of configPaths) {
|
|
39
|
+
const fullPath = path.resolve(process.cwd(), configPath);
|
|
40
|
+
if (fs.existsSync(fullPath)) {
|
|
41
|
+
try {
|
|
42
|
+
if (configPath.endsWith(".js") || configPath.endsWith(".cjs")) {
|
|
43
|
+
return require(fullPath);
|
|
44
|
+
}
|
|
45
|
+
return JSON.parse(fs.readFileSync(fullPath, "utf-8"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`Warning: Failed to load prettier config from ${configPath}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 기본 Prettier 설정
|
|
55
|
+
return {
|
|
56
|
+
semi: true,
|
|
57
|
+
trailingComma: "es5",
|
|
58
|
+
singleQuote: true,
|
|
59
|
+
printWidth: 100,
|
|
60
|
+
tabWidth: 2,
|
|
61
|
+
arrowParens: "always",
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
22
65
|
/**
|
|
23
66
|
* 명령행 인수 파싱
|
|
24
67
|
* @returns {Object} 파싱된 인수들
|
|
@@ -29,6 +72,7 @@ const parseArguments = () => {
|
|
|
29
72
|
"uri",
|
|
30
73
|
"username",
|
|
31
74
|
"password",
|
|
75
|
+
"http-client",
|
|
32
76
|
"dto-output-path",
|
|
33
77
|
"api-output-path",
|
|
34
78
|
"api-instance-output-path",
|
|
@@ -40,6 +84,7 @@ const parseArguments = () => {
|
|
|
40
84
|
u: "uri",
|
|
41
85
|
un: "username",
|
|
42
86
|
pw: "password",
|
|
87
|
+
hc: "http-client",
|
|
43
88
|
dp: "dto-output-path",
|
|
44
89
|
ap: "api-output-path",
|
|
45
90
|
aip: "api-instance-output-path",
|
|
@@ -53,6 +98,7 @@ const parseArguments = () => {
|
|
|
53
98
|
uri: argv.uri,
|
|
54
99
|
username: argv.username,
|
|
55
100
|
password: argv.password,
|
|
101
|
+
httpClient: argv["http-client"],
|
|
56
102
|
dtoOutputPath: argv["dto-output-path"],
|
|
57
103
|
apiOutputPath: argv["api-output-path"],
|
|
58
104
|
apiInstanceOutputPath: argv["api-instance-output-path"],
|
|
@@ -62,6 +108,23 @@ const parseArguments = () => {
|
|
|
62
108
|
};
|
|
63
109
|
};
|
|
64
110
|
|
|
111
|
+
/**
|
|
112
|
+
* HTTP 클라이언트 모드 정규화
|
|
113
|
+
* @param {string | undefined} input
|
|
114
|
+
* @returns {"axios" | "ky"}
|
|
115
|
+
*/
|
|
116
|
+
const resolveHttpClient = (input) => {
|
|
117
|
+
const normalized = (input ?? "ky").toString().toLowerCase();
|
|
118
|
+
|
|
119
|
+
if (normalized === "axios" || normalized === "ky") {
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Invalid --http-client value "${input}". Supported values: axios | ky`
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
65
128
|
/**
|
|
66
129
|
* 출력 경로 설정 (FSD 패턴 기본값)
|
|
67
130
|
* @param {Object} args - 명령행 인수
|
|
@@ -129,6 +192,7 @@ const printUsage = (outputPaths) => {
|
|
|
129
192
|
console.error(
|
|
130
193
|
"Usage: generate-all --uri <swagger-url|swagger-file-name> " +
|
|
131
194
|
"[--username <username>] [--password <password>] " +
|
|
195
|
+
"[--http-client <axios|ky>] " +
|
|
132
196
|
"[--dto-output-path <dto-output-path>] " +
|
|
133
197
|
"[--api-output-path <api-output-path>] " +
|
|
134
198
|
"[--query-output-path <query-output-path>] " +
|
|
@@ -153,13 +217,14 @@ export const generateApiCode = async ({
|
|
|
153
217
|
uri,
|
|
154
218
|
username,
|
|
155
219
|
password,
|
|
220
|
+
httpClient,
|
|
156
221
|
templates,
|
|
157
222
|
...params
|
|
158
223
|
}) => {
|
|
159
224
|
const isLocal = !isUrl(uri);
|
|
225
|
+
const httpClientType = httpClient === "axios" ? "axios" : "fetch";
|
|
160
226
|
|
|
161
227
|
return generateApi({
|
|
162
|
-
// 로컬 파일 또는 원격 URL 처리
|
|
163
228
|
input: isLocal ? path.resolve(process.cwd(), uri) : undefined,
|
|
164
229
|
spec: !isLocal && (await fetchSwagger(uri, username, password)),
|
|
165
230
|
templates: templates,
|
|
@@ -167,29 +232,48 @@ export const generateApiCode = async ({
|
|
|
167
232
|
generateUnionEnums: true,
|
|
168
233
|
cleanOutput: false,
|
|
169
234
|
silent: true,
|
|
170
|
-
|
|
171
|
-
|
|
235
|
+
prettier: {
|
|
236
|
+
semi: true,
|
|
237
|
+
trailingComma: "es5",
|
|
238
|
+
singleQuote: true,
|
|
239
|
+
printWidth: 100,
|
|
240
|
+
tabWidth: 2,
|
|
241
|
+
arrowParens: "always",
|
|
242
|
+
bracketSameLine: false,
|
|
243
|
+
jsxSingleQuote: false,
|
|
244
|
+
},
|
|
172
245
|
modular: true,
|
|
173
|
-
moduleNameFirstTag: true,
|
|
246
|
+
moduleNameFirstTag: true,
|
|
174
247
|
moduleNameIndex: 1,
|
|
175
|
-
// typeSuffix: "Dto", // 타입에 Dto 접미사 추가
|
|
176
248
|
generateRouteTypes: true,
|
|
177
249
|
schemaParsers: {
|
|
178
250
|
complexAnyOf: AnyOfSchemaParser,
|
|
179
251
|
},
|
|
252
|
+
httpClientType,
|
|
180
253
|
...params,
|
|
181
254
|
});
|
|
182
255
|
};
|
|
183
256
|
|
|
257
|
+
/**
|
|
258
|
+
* 생성된 파일에 프로젝트의 prettier 적용
|
|
259
|
+
* @param {string} filePath - 파일 경로
|
|
260
|
+
*/
|
|
261
|
+
const formatWithProjectPrettier = (filePath) => {
|
|
262
|
+
try {
|
|
263
|
+
execSync(`prettier --write "${filePath}"`, { stdio: "inherit" });
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.warn(`Warning: Failed to format ${filePath}`);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
184
269
|
/**
|
|
185
270
|
* API 클래스와 DTO 파일 생성
|
|
186
271
|
* @param {Object} args - 명령행 인수
|
|
187
272
|
* @param {Object} outputPaths - 출력 경로 설정
|
|
188
273
|
*/
|
|
189
274
|
const generateApiFunctionCode = async (args, outputPaths) => {
|
|
190
|
-
const { projectTemplate, uri, username, password } = args;
|
|
275
|
+
const { projectTemplate, uri, username, password, httpClient } = args;
|
|
191
276
|
|
|
192
|
-
// 템플릿 경로 결정 (커스텀 템플릿 또는 기본 템플릿)
|
|
193
277
|
const templatePath = projectTemplate
|
|
194
278
|
? path.resolve(process.cwd(), projectTemplate)
|
|
195
279
|
: path.resolve(__dirname, "../templates");
|
|
@@ -200,41 +284,43 @@ const generateApiFunctionCode = async (args, outputPaths) => {
|
|
|
200
284
|
uri,
|
|
201
285
|
username,
|
|
202
286
|
password,
|
|
287
|
+
httpClient,
|
|
203
288
|
templates: templatePath,
|
|
289
|
+
// swagger-typescript-api + prettier 조합에서 parser 추론 실패를 방지
|
|
290
|
+
prettier: { parser: "typescript" },
|
|
204
291
|
});
|
|
205
292
|
|
|
206
|
-
// 생성된 파일들을 적절한 위치에 저장
|
|
207
293
|
for (const { fileName, fileContent } of apiFunctionCode.files) {
|
|
208
|
-
// http-client 파일은 사용하지 않음 (ky 사용)
|
|
209
294
|
if (fileName === "http-client") continue;
|
|
210
295
|
|
|
296
|
+
let outputPath;
|
|
211
297
|
if (fileName === "data-contracts") {
|
|
212
|
-
|
|
213
|
-
await writeFileToPath(
|
|
298
|
+
outputPath = outputPaths.dto.absolutePath;
|
|
299
|
+
await writeFileToPath(outputPath, fileContent);
|
|
300
|
+
formatWithProjectPrettier(outputPath);
|
|
214
301
|
console.log(`✅ Generated DTO: ${outputPaths.dto.relativePath}`);
|
|
215
302
|
} else {
|
|
216
|
-
// 모듈명 추출 (예: UserRoute -> user)
|
|
217
303
|
const moduleName = fileName.replace("Route", "").toLowerCase();
|
|
218
304
|
|
|
219
305
|
if (fileName.match(/Route$/)) {
|
|
220
|
-
|
|
221
|
-
const output = outputPaths.apiInstance.absolutePath.replace(
|
|
306
|
+
outputPath = outputPaths.apiInstance.absolutePath.replace(
|
|
222
307
|
"{moduleName}",
|
|
223
308
|
moduleName
|
|
224
309
|
);
|
|
225
|
-
await writeFileToPath(
|
|
310
|
+
await writeFileToPath(outputPath, fileContent);
|
|
311
|
+
formatWithProjectPrettier(outputPath);
|
|
226
312
|
console.log(
|
|
227
|
-
`✅ Generated API instance: ${
|
|
313
|
+
`✅ Generated API instance: ${outputPath.replace(process.cwd(), ".")}`
|
|
228
314
|
);
|
|
229
315
|
} else {
|
|
230
|
-
|
|
231
|
-
const output = outputPaths.api.absolutePath.replace(
|
|
316
|
+
outputPath = outputPaths.api.absolutePath.replace(
|
|
232
317
|
"{moduleName}",
|
|
233
318
|
moduleName
|
|
234
319
|
);
|
|
235
|
-
await writeFileToPath(
|
|
320
|
+
await writeFileToPath(outputPath, fileContent);
|
|
321
|
+
formatWithProjectPrettier(outputPath);
|
|
236
322
|
console.log(
|
|
237
|
-
`✅ Generated API class: ${
|
|
323
|
+
`✅ Generated API class: ${outputPath.replace(process.cwd(), ".")}`
|
|
238
324
|
);
|
|
239
325
|
}
|
|
240
326
|
}
|
|
@@ -247,9 +333,8 @@ const generateApiFunctionCode = async (args, outputPaths) => {
|
|
|
247
333
|
* @param {Object} outputPaths - 출력 경로 설정
|
|
248
334
|
*/
|
|
249
335
|
const generateTanstackQueryCode = async (args, outputPaths) => {
|
|
250
|
-
const { projectTemplate, uri, username, password } = args;
|
|
336
|
+
const { projectTemplate, uri, username, password, httpClient } = args;
|
|
251
337
|
|
|
252
|
-
// TanStack Query 템플릿 경로 결정
|
|
253
338
|
const templatePath = projectTemplate
|
|
254
339
|
? path.resolve(process.cwd(), projectTemplate, "tanstack-query")
|
|
255
340
|
: path.resolve(__dirname, "../templates/tanstack-query");
|
|
@@ -260,35 +345,37 @@ const generateTanstackQueryCode = async (args, outputPaths) => {
|
|
|
260
345
|
uri,
|
|
261
346
|
username,
|
|
262
347
|
password,
|
|
348
|
+
httpClient,
|
|
263
349
|
templates: templatePath,
|
|
350
|
+
// swagger-typescript-api + prettier 조합에서 parser 추론 실패를 방지
|
|
351
|
+
prettier: { parser: "typescript" },
|
|
264
352
|
});
|
|
265
353
|
|
|
266
|
-
// 생성된 파일들을 적절한 위치에 저장
|
|
267
354
|
for (const { fileName, fileContent } of tanstackQueryCode.files) {
|
|
268
|
-
// 불필요한 파일들 제외
|
|
269
355
|
if (fileName === "http-client" || fileName === "data-contracts") continue;
|
|
270
356
|
|
|
271
357
|
const moduleName = fileName.replace("Route", "").toLowerCase();
|
|
358
|
+
let outputPath;
|
|
272
359
|
|
|
273
360
|
if (fileName.match(/Route$/)) {
|
|
274
|
-
|
|
275
|
-
const output = outputPaths.mutation.absolutePath.replace(
|
|
361
|
+
outputPath = outputPaths.mutation.absolutePath.replace(
|
|
276
362
|
"{moduleName}",
|
|
277
363
|
moduleName
|
|
278
364
|
);
|
|
279
|
-
await writeFileToPath(
|
|
365
|
+
await writeFileToPath(outputPath, fileContent);
|
|
366
|
+
formatWithProjectPrettier(outputPath);
|
|
280
367
|
console.log(
|
|
281
|
-
`✅ Generated mutations: ${
|
|
368
|
+
`✅ Generated mutations: ${outputPath.replace(process.cwd(), ".")}`
|
|
282
369
|
);
|
|
283
370
|
} else {
|
|
284
|
-
|
|
285
|
-
const output = outputPaths.query.absolutePath.replace(
|
|
371
|
+
outputPath = outputPaths.query.absolutePath.replace(
|
|
286
372
|
"{moduleName}",
|
|
287
373
|
moduleName
|
|
288
374
|
);
|
|
289
|
-
await writeFileToPath(
|
|
375
|
+
await writeFileToPath(outputPath, fileContent);
|
|
376
|
+
formatWithProjectPrettier(outputPath);
|
|
290
377
|
console.log(
|
|
291
|
-
`✅ Generated queries: ${
|
|
378
|
+
`✅ Generated queries: ${outputPath.replace(process.cwd(), ".")}`
|
|
292
379
|
);
|
|
293
380
|
}
|
|
294
381
|
}
|
|
@@ -310,6 +397,9 @@ const main = async () => {
|
|
|
310
397
|
}
|
|
311
398
|
|
|
312
399
|
try {
|
|
400
|
+
args.httpClient = resolveHttpClient(args.httpClient);
|
|
401
|
+
console.log(`🌐 HTTP client mode: ${args.httpClient}`);
|
|
402
|
+
|
|
313
403
|
// 1. API 클래스와 DTO 생성
|
|
314
404
|
await generateApiFunctionCode(args, outputPaths);
|
|
315
405
|
|
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,25 @@ 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
|
+
return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`, {
|
|
98
|
+
<% if (query) { %>
|
|
99
|
+
params, // 쿼리 파라미터 설정
|
|
100
|
+
<% } %>
|
|
101
|
+
<% if (payload) { %>
|
|
102
|
+
data, // 요청 본문 설정
|
|
103
|
+
<% } %>
|
|
104
|
+
...options, // 추가 axios 옵션 병합
|
|
105
|
+
}).then((response) => response.data);
|
|
106
|
+
<% } else { %>
|
|
93
107
|
// HTTP 요청 실행 및 JSON 응답 반환
|
|
94
108
|
return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`,{
|
|
95
109
|
<% if (query) { %>
|
|
@@ -100,4 +114,5 @@ return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`
|
|
|
100
114
|
<% } %>
|
|
101
115
|
...options, // 추가 ky 옵션 병합
|
|
102
116
|
}).json();
|
|
117
|
+
<% } %>
|
|
103
118
|
}
|
|
@@ -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
|
}),
|
|
Binary file
|