swagger-fsd-gen 1.0.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/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +1 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/USAGE.md +137 -0
- package/package.json +40 -0
- package/scripts/fetch-swagger.js +90 -0
- package/scripts/generate-all.js +341 -0
- package/swagger-fsd-gen-1.0.0.tgz +0 -0
- package/templates/api.ejs +77 -0
- package/templates/data-contracts.ejs +94 -0
- package/templates/dto-import.ejs +27 -0
- package/templates/procedure-call.ejs +103 -0
- package/templates/route-docs.ejs +29 -0
- package/templates/route-types.ejs +15 -0
- package/templates/tanstack-query/api.ejs +195 -0
- package/templates/tanstack-query/route-types.ejs +152 -0
- package/utils/fetch-swagger.js +46 -0
- package/utils/file.js +81 -0
- package/utils/parser.js +111 -0
- package/utils/string.js +33 -0
- package/utils/url.js +34 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Swagger API 클라이언트 자동 생성 도구
|
|
5
|
+
* - ky HTTP 클라이언트 기반 API 클래스 생성
|
|
6
|
+
* - TanStack Query 훅 생성 (useQuery, useMutation)
|
|
7
|
+
* - FSD(Feature-Sliced Design) 패턴 적용
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import minimist from "minimist";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { generateApi } from "swagger-typescript-api";
|
|
14
|
+
import { fetchSwagger } from "../utils/fetch-swagger.js";
|
|
15
|
+
import { writeFileToPath } from "../utils/file.js";
|
|
16
|
+
import { AnyOfSchemaParser } from "../utils/parser.js";
|
|
17
|
+
import { isUrl } from "../utils/url.js";
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 명령행 인수 파싱
|
|
24
|
+
* @returns {Object} 파싱된 인수들
|
|
25
|
+
*/
|
|
26
|
+
const parseArguments = () => {
|
|
27
|
+
const argv = minimist(process.argv.slice(2), {
|
|
28
|
+
string: [
|
|
29
|
+
"uri",
|
|
30
|
+
"username",
|
|
31
|
+
"password",
|
|
32
|
+
"dto-output-path",
|
|
33
|
+
"api-output-path",
|
|
34
|
+
"api-instance-output-path",
|
|
35
|
+
"query-output-path",
|
|
36
|
+
"mutation-output-path",
|
|
37
|
+
"project-template",
|
|
38
|
+
],
|
|
39
|
+
alias: {
|
|
40
|
+
u: "uri",
|
|
41
|
+
un: "username",
|
|
42
|
+
pw: "password",
|
|
43
|
+
dp: "dto-output-path",
|
|
44
|
+
ap: "api-output-path",
|
|
45
|
+
aip: "api-instance-output-path",
|
|
46
|
+
qp: "query-output-path",
|
|
47
|
+
mp: "mutation-output-path",
|
|
48
|
+
pt: "project-template",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
uri: argv.uri,
|
|
54
|
+
username: argv.username,
|
|
55
|
+
password: argv.password,
|
|
56
|
+
dtoOutputPath: argv["dto-output-path"],
|
|
57
|
+
apiOutputPath: argv["api-output-path"],
|
|
58
|
+
apiInstanceOutputPath: argv["api-instance-output-path"],
|
|
59
|
+
queryOutputPath: argv["query-output-path"],
|
|
60
|
+
mutationOutputPath: argv["mutation-output-path"],
|
|
61
|
+
projectTemplate: argv["project-template"],
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 출력 경로 설정 (FSD 패턴 기본값)
|
|
67
|
+
* @param {Object} args - 명령행 인수
|
|
68
|
+
* @returns {Object} 설정된 출력 경로들
|
|
69
|
+
*/
|
|
70
|
+
const setupOutputPaths = (args) => {
|
|
71
|
+
return {
|
|
72
|
+
// DTO 타입 정의 파일 (공통)
|
|
73
|
+
dto: {
|
|
74
|
+
relativePath: args.dtoOutputPath ?? "src/shared/api/dto.ts",
|
|
75
|
+
absolutePath: path.resolve(
|
|
76
|
+
process.cwd(),
|
|
77
|
+
args.dtoOutputPath ?? "src/shared/api/dto.ts"
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
// API 클래스 파일 (모듈별)
|
|
81
|
+
api: {
|
|
82
|
+
relativePath:
|
|
83
|
+
args.apiOutputPath ?? "src/entities/{moduleName}/api/index.ts",
|
|
84
|
+
absolutePath: path.resolve(
|
|
85
|
+
process.cwd(),
|
|
86
|
+
args.apiOutputPath ?? "src/entities/{moduleName}/api/index.ts"
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
// API 인스턴스 파일 (모듈별)
|
|
90
|
+
apiInstance: {
|
|
91
|
+
relativePath:
|
|
92
|
+
args.apiInstanceOutputPath ??
|
|
93
|
+
"src/entities/{moduleName}/api/instance.ts",
|
|
94
|
+
absolutePath: path.resolve(
|
|
95
|
+
process.cwd(),
|
|
96
|
+
args.apiInstanceOutputPath ??
|
|
97
|
+
"src/entities/{moduleName}/api/instance.ts"
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
// TanStack Query 훅 파일 (모듈별)
|
|
101
|
+
query: {
|
|
102
|
+
relativePath:
|
|
103
|
+
args.queryOutputPath ?? "src/entities/{moduleName}/api/queries.ts",
|
|
104
|
+
absolutePath: path.resolve(
|
|
105
|
+
process.cwd(),
|
|
106
|
+
args.queryOutputPath ?? "src/entities/{moduleName}/api/queries.ts"
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
// TanStack Mutation 훅 파일 (모듈별)
|
|
110
|
+
mutation: {
|
|
111
|
+
relativePath:
|
|
112
|
+
args.mutationOutputPath ?? "src/entities/{moduleName}/api/mutations.ts",
|
|
113
|
+
absolutePath: path.resolve(
|
|
114
|
+
process.cwd(),
|
|
115
|
+
args.mutationOutputPath ?? "src/entities/{moduleName}/api/mutations.ts"
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 사용법 출력
|
|
123
|
+
* @param {Object} outputPaths - 출력 경로 설정
|
|
124
|
+
*/
|
|
125
|
+
const printUsage = (outputPaths) => {
|
|
126
|
+
console.error(
|
|
127
|
+
"❗️ Error: Please provide the swagger URL or swagger file name"
|
|
128
|
+
);
|
|
129
|
+
console.error(
|
|
130
|
+
"Usage: generate-all --uri <swagger-url|swagger-file-name> " +
|
|
131
|
+
"[--username <username>] [--password <password>] " +
|
|
132
|
+
"[--dto-output-path <dto-output-path>] " +
|
|
133
|
+
"[--api-output-path <api-output-path>] " +
|
|
134
|
+
"[--query-output-path <query-output-path>] " +
|
|
135
|
+
"[--mutation-output-path <mutation-output-path>] " +
|
|
136
|
+
"[--project-template <project-template>]"
|
|
137
|
+
);
|
|
138
|
+
console.error(
|
|
139
|
+
`\nCurrent output paths:\n` +
|
|
140
|
+
` DTO Path: ${outputPaths.dto.relativePath}\n` +
|
|
141
|
+
` API Path: ${outputPaths.api.relativePath}\n` +
|
|
142
|
+
` Query Path: ${outputPaths.query.relativePath}\n` +
|
|
143
|
+
` Mutation Path: ${outputPaths.mutation.relativePath}`
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* swagger-typescript-api를 사용하여 API 코드 생성
|
|
149
|
+
* @param {Object} params - 생성 파라미터
|
|
150
|
+
* @returns {Promise<Object>} 생성된 파일들
|
|
151
|
+
*/
|
|
152
|
+
export const generateApiCode = async ({
|
|
153
|
+
uri,
|
|
154
|
+
username,
|
|
155
|
+
password,
|
|
156
|
+
templates,
|
|
157
|
+
...params
|
|
158
|
+
}) => {
|
|
159
|
+
const isLocal = !isUrl(uri);
|
|
160
|
+
|
|
161
|
+
return generateApi({
|
|
162
|
+
// 로컬 파일 또는 원격 URL 처리
|
|
163
|
+
input: isLocal ? path.resolve(process.cwd(), uri) : undefined,
|
|
164
|
+
spec: !isLocal && (await fetchSwagger(uri, username, password)),
|
|
165
|
+
templates: templates,
|
|
166
|
+
generateClient: true,
|
|
167
|
+
generateUnionEnums: true,
|
|
168
|
+
cleanOutput: false,
|
|
169
|
+
silent: true,
|
|
170
|
+
// Prettier 설정
|
|
171
|
+
prettier: {
|
|
172
|
+
semi: true,
|
|
173
|
+
trailingComma: "es5",
|
|
174
|
+
singleQuote: true,
|
|
175
|
+
printWidth: 100,
|
|
176
|
+
tabWidth: 2,
|
|
177
|
+
arrowParens: "always",
|
|
178
|
+
jsxBracketSameLine: false,
|
|
179
|
+
jsxSingleQuote: false,
|
|
180
|
+
},
|
|
181
|
+
modular: true,
|
|
182
|
+
moduleNameFirstTag: true, // Swagger 태그를 모듈명으로 사용
|
|
183
|
+
moduleNameIndex: 1,
|
|
184
|
+
// typeSuffix: "Dto", // 타입에 Dto 접미사 추가
|
|
185
|
+
generateRouteTypes: true,
|
|
186
|
+
schemaParsers: {
|
|
187
|
+
complexAnyOf: AnyOfSchemaParser,
|
|
188
|
+
},
|
|
189
|
+
...params,
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* API 클래스와 DTO 파일 생성
|
|
195
|
+
* @param {Object} args - 명령행 인수
|
|
196
|
+
* @param {Object} outputPaths - 출력 경로 설정
|
|
197
|
+
*/
|
|
198
|
+
const generateApiFunctionCode = async (args, outputPaths) => {
|
|
199
|
+
const { projectTemplate, uri, username, password } = args;
|
|
200
|
+
|
|
201
|
+
// 템플릿 경로 결정 (커스텀 템플릿 또는 기본 템플릿)
|
|
202
|
+
const templatePath = projectTemplate
|
|
203
|
+
? path.resolve(process.cwd(), projectTemplate)
|
|
204
|
+
: path.resolve(__dirname, "../templates");
|
|
205
|
+
|
|
206
|
+
console.log("🔄 Generating API classes and DTOs...");
|
|
207
|
+
|
|
208
|
+
const apiFunctionCode = await generateApiCode({
|
|
209
|
+
uri,
|
|
210
|
+
username,
|
|
211
|
+
password,
|
|
212
|
+
templates: templatePath,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// 생성된 파일들을 적절한 위치에 저장
|
|
216
|
+
for (const { fileName, fileContent } of apiFunctionCode.files) {
|
|
217
|
+
// http-client 파일은 사용하지 않음 (ky 사용)
|
|
218
|
+
if (fileName === "http-client") continue;
|
|
219
|
+
|
|
220
|
+
if (fileName === "data-contracts") {
|
|
221
|
+
// DTO 타입 정의 파일 저장
|
|
222
|
+
await writeFileToPath(outputPaths.dto.absolutePath, fileContent);
|
|
223
|
+
console.log(`✅ Generated DTO: ${outputPaths.dto.relativePath}`);
|
|
224
|
+
} else {
|
|
225
|
+
// 모듈명 추출 (예: UserRoute -> user)
|
|
226
|
+
const moduleName = fileName.replace("Route", "").toLowerCase();
|
|
227
|
+
|
|
228
|
+
if (fileName.match(/Route$/)) {
|
|
229
|
+
// API 인스턴스 파일 생성 (예: UserRoute -> user/api/instance.ts)
|
|
230
|
+
const output = outputPaths.apiInstance.absolutePath.replace(
|
|
231
|
+
"{moduleName}",
|
|
232
|
+
moduleName
|
|
233
|
+
);
|
|
234
|
+
await writeFileToPath(output, fileContent);
|
|
235
|
+
console.log(
|
|
236
|
+
`✅ Generated API instance: ${output.replace(process.cwd(), ".")}`
|
|
237
|
+
);
|
|
238
|
+
} else {
|
|
239
|
+
// API 클래스 파일 생성 (예: User -> user/api/index.ts)
|
|
240
|
+
const output = outputPaths.api.absolutePath.replace(
|
|
241
|
+
"{moduleName}",
|
|
242
|
+
moduleName
|
|
243
|
+
);
|
|
244
|
+
await writeFileToPath(output, fileContent);
|
|
245
|
+
console.log(
|
|
246
|
+
`✅ Generated API class: ${output.replace(process.cwd(), ".")}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* TanStack Query 훅 파일 생성
|
|
255
|
+
* @param {Object} args - 명령행 인수
|
|
256
|
+
* @param {Object} outputPaths - 출력 경로 설정
|
|
257
|
+
*/
|
|
258
|
+
const generateTanstackQueryCode = async (args, outputPaths) => {
|
|
259
|
+
const { projectTemplate, uri, username, password } = args;
|
|
260
|
+
|
|
261
|
+
// TanStack Query 템플릿 경로 결정
|
|
262
|
+
const templatePath = projectTemplate
|
|
263
|
+
? path.resolve(process.cwd(), projectTemplate, "tanstack-query")
|
|
264
|
+
: path.resolve(__dirname, "../templates/tanstack-query");
|
|
265
|
+
|
|
266
|
+
console.log("🔄 Generating TanStack Query hooks...");
|
|
267
|
+
|
|
268
|
+
const tanstackQueryCode = await generateApiCode({
|
|
269
|
+
uri,
|
|
270
|
+
username,
|
|
271
|
+
password,
|
|
272
|
+
templates: templatePath,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// 생성된 파일들을 적절한 위치에 저장
|
|
276
|
+
for (const { fileName, fileContent } of tanstackQueryCode.files) {
|
|
277
|
+
// 불필요한 파일들 제외
|
|
278
|
+
if (fileName === "http-client" || fileName === "data-contracts") continue;
|
|
279
|
+
|
|
280
|
+
const moduleName = fileName.replace("Route", "").toLowerCase();
|
|
281
|
+
|
|
282
|
+
if (fileName.match(/Route$/)) {
|
|
283
|
+
// Mutation 훅 파일 생성
|
|
284
|
+
const output = outputPaths.mutation.absolutePath.replace(
|
|
285
|
+
"{moduleName}",
|
|
286
|
+
moduleName
|
|
287
|
+
);
|
|
288
|
+
await writeFileToPath(output, fileContent);
|
|
289
|
+
console.log(
|
|
290
|
+
`✅ Generated mutations: ${output.replace(process.cwd(), ".")}`
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
// Query 훅 파일 생성
|
|
294
|
+
const output = outputPaths.query.absolutePath.replace(
|
|
295
|
+
"{moduleName}",
|
|
296
|
+
moduleName
|
|
297
|
+
);
|
|
298
|
+
await writeFileToPath(output, fileContent);
|
|
299
|
+
console.log(
|
|
300
|
+
`✅ Generated queries: ${output.replace(process.cwd(), ".")}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 메인 실행 함수
|
|
308
|
+
*/
|
|
309
|
+
const main = async () => {
|
|
310
|
+
console.log("🚀 Starting Swagger API client generation...\n");
|
|
311
|
+
|
|
312
|
+
const args = parseArguments();
|
|
313
|
+
const outputPaths = setupOutputPaths(args);
|
|
314
|
+
|
|
315
|
+
// URI 필수 체크
|
|
316
|
+
if (!args.uri) {
|
|
317
|
+
printUsage(outputPaths);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
// 1. API 클래스와 DTO 생성
|
|
323
|
+
await generateApiFunctionCode(args, outputPaths);
|
|
324
|
+
|
|
325
|
+
// 2. TanStack Query 훅 생성
|
|
326
|
+
await generateTanstackQueryCode(args, outputPaths);
|
|
327
|
+
|
|
328
|
+
console.log("\n🎉 API client generation completed successfully!");
|
|
329
|
+
console.log("\n📁 Generated files:");
|
|
330
|
+
console.log(` - DTOs: ${outputPaths.dto.relativePath}`);
|
|
331
|
+
console.log(` - API classes: ${outputPaths.api.relativePath}`);
|
|
332
|
+
console.log(` - Query hooks: ${outputPaths.query.relativePath}`);
|
|
333
|
+
console.log(` - Mutation hooks: ${outputPaths.mutation.relativePath}`);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error("\n❌ Error during generation:");
|
|
336
|
+
console.error(error.message);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
main();
|
|
Binary file
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<%
|
|
2
|
+
/**
|
|
3
|
+
* API 클래스 생성 템플릿
|
|
4
|
+
* - Swagger 태그별로 API 클래스 생성
|
|
5
|
+
* - ky HTTP 클라이언트 기반
|
|
6
|
+
* - 타입 안전성을 위한 TypeScript 지원
|
|
7
|
+
* - 쿼리 파라미터 처리를 위한 유틸리티 메서드 포함
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { utils, route, config, modelTypes } = it;
|
|
11
|
+
const { _, pascalCase, require } = utils;
|
|
12
|
+
|
|
13
|
+
// 모듈명을 PascalCase로 변환하여 클래스명 생성 (예: user -> UserApi)
|
|
14
|
+
const apiClassName = pascalCase(route.moduleName);
|
|
15
|
+
|
|
16
|
+
// 해당 모듈의 모든 API 엔드포인트
|
|
17
|
+
const routes = route.routes;
|
|
18
|
+
|
|
19
|
+
// 사용 가능한 DTO 타입들의 이름 목록
|
|
20
|
+
const dataContracts = _.map(modelTypes, "name");
|
|
21
|
+
%>
|
|
22
|
+
|
|
23
|
+
import { KyInstance, Options } from 'ky';
|
|
24
|
+
|
|
25
|
+
<%~ includeFile('./dto-import.ejs', { ...it }) %>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* <%= route.moduleName %> 모듈 API 클래스
|
|
29
|
+
* - ky HTTP 클라이언트를 사용한 API 호출
|
|
30
|
+
* - 타입 안전성을 위한 TypeScript 지원
|
|
31
|
+
* - 쿼리 파라미터 자동 변환 기능
|
|
32
|
+
*/
|
|
33
|
+
export class <%= apiClassName %>Api {
|
|
34
|
+
private readonly instance: KyInstance;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* API 클래스 생성자
|
|
38
|
+
* @param instance - ky HTTP 클라이언트 인스턴스
|
|
39
|
+
*/
|
|
40
|
+
constructor(instance: KyInstance) {
|
|
41
|
+
this.instance = instance;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 객체를 URLSearchParams로 변환하는 정적 유틸리티 메서드
|
|
46
|
+
* - 중첩된 객체나 배열을 쿼리 파라미터로 변환
|
|
47
|
+
* - undefined 값은 자동으로 제외
|
|
48
|
+
* - 배열 값은 여러 개의 동일한 키로 변환
|
|
49
|
+
*
|
|
50
|
+
* @param params - 쿼리 파라미터 객체
|
|
51
|
+
* @returns URLSearchParams 인스턴스
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* createSearchParams({ name: 'john', tags: ['tag1', 'tag2'] })
|
|
55
|
+
* // 결과: name=john&tags=tag1&tags=tag2
|
|
56
|
+
*/
|
|
57
|
+
static createSearchParams(
|
|
58
|
+
params?: Record<string, string | number | boolean | Array<string | number | boolean>>
|
|
59
|
+
): URLSearchParams {
|
|
60
|
+
const urlSearchParams = new URLSearchParams();
|
|
61
|
+
|
|
62
|
+
if (params) {
|
|
63
|
+
Object.entries(params)
|
|
64
|
+
.filter(([, value]) => value !== undefined)
|
|
65
|
+
.forEach(([key, value]) => {
|
|
66
|
+
const values = Array.isArray(value) ? value : [value];
|
|
67
|
+
values.forEach((v) => urlSearchParams.append(key, v.toString()));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return urlSearchParams;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
<% for (const route of routes) { %>
|
|
75
|
+
<%~ includeFile('./procedure-call.ejs', { ...it, route, apiClassName }) %>
|
|
76
|
+
<% } %>
|
|
77
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<%
|
|
2
|
+
/**
|
|
3
|
+
* DTO(Data Transfer Object) 타입 정의 템플릿
|
|
4
|
+
* - Swagger 스키마에서 TypeScript 타입 정의 생성
|
|
5
|
+
* - enum, interface, type 등 다양한 타입 지원
|
|
6
|
+
* - 제네릭 타입 지원
|
|
7
|
+
* - Query 파라미터 및 Header 타입 자동 생성
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { modelTypes, utils, config, routes } = it;
|
|
11
|
+
const { formatDescription, require, _, Ts, pascalCase } = utils;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 제네릭 타입 문자열 생성
|
|
15
|
+
* @param {Object} contract - 타입 계약 객체
|
|
16
|
+
* @returns {string} 제네릭 타입 문자열 (예: <T extends string = any>)
|
|
17
|
+
*/
|
|
18
|
+
const buildGenerics = (contract) => {
|
|
19
|
+
if (!contract.genericArgs || !contract.genericArgs.length) return '';
|
|
20
|
+
|
|
21
|
+
return '<' + contract.genericArgs.map(({ name, default: defaultType, extends: extendsType }) => {
|
|
22
|
+
return [
|
|
23
|
+
name,
|
|
24
|
+
extendsType && `extends ${extendsType}`,
|
|
25
|
+
defaultType && `= ${defaultType}`,
|
|
26
|
+
].join('')
|
|
27
|
+
}).join(',') + '>'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 데이터 계약 템플릿 정의
|
|
32
|
+
* - enum: 열거형 타입 생성
|
|
33
|
+
* - interface: 인터페이스 타입 생성 (실제로는 type alias 사용)
|
|
34
|
+
* - type: 타입 별칭 생성
|
|
35
|
+
*/
|
|
36
|
+
const dataContractTemplates = {
|
|
37
|
+
// 열거형 타입 템플릿
|
|
38
|
+
enum: (contract) => {
|
|
39
|
+
return `enum ${contract.name} {\r\n${contract.content} \r\n }`;
|
|
40
|
+
},
|
|
41
|
+
// 인터페이스 타입 템플릿 (객체 형태)
|
|
42
|
+
interface: (contract) => {
|
|
43
|
+
return `type ${contract.name}${buildGenerics(contract)} = {\r\n${contract.content}}`;
|
|
44
|
+
},
|
|
45
|
+
// 일반 타입 별칭 템플릿
|
|
46
|
+
type: (contract) => {
|
|
47
|
+
return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
%>
|
|
51
|
+
|
|
52
|
+
<% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %>
|
|
53
|
+
/**
|
|
54
|
+
* 유틸리티 타입: 특정 키를 필수로 만드는 헬퍼 타입
|
|
55
|
+
* @template T - 원본 타입
|
|
56
|
+
* @template K - 필수로 만들 키들
|
|
57
|
+
*/
|
|
58
|
+
type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %><T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
|
|
59
|
+
<% } %>
|
|
60
|
+
|
|
61
|
+
<% for (const contract of modelTypes) { %>
|
|
62
|
+
<%~ includeFile('@base/data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %>
|
|
63
|
+
<%~ contract.internal ? '' : 'export'%> <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
<% } %>
|
|
67
|
+
|
|
68
|
+
<%
|
|
69
|
+
/**
|
|
70
|
+
* API 엔드포인트별 Query 파라미터 및 Header 타입 생성
|
|
71
|
+
* - 각 API 엔드포인트의 쿼리 파라미터를 별도 타입으로 정의
|
|
72
|
+
* - 각 API 엔드포인트의 헤더를 별도 타입으로 정의
|
|
73
|
+
* - TanStack Query와 API 클래스에서 사용
|
|
74
|
+
*/
|
|
75
|
+
%>
|
|
76
|
+
<% for (const route of routes.combined) { %>
|
|
77
|
+
<% for (const api of route.routes) { %>
|
|
78
|
+
|
|
79
|
+
<% const queryName = `${pascalCase(api.routeName.usage)}QueryParams`; %>
|
|
80
|
+
<% const queryContent = api.specificArgs.query?.type; %>
|
|
81
|
+
<% if (queryContent) { %>
|
|
82
|
+
/** <%= api.routeName.usage %> API의 쿼리 파라미터 타입 */
|
|
83
|
+
export type <%= queryName %> = <%= queryContent %>;
|
|
84
|
+
<% } %>
|
|
85
|
+
|
|
86
|
+
<% const headerName = `${pascalCase(api.routeName.usage)}Headers`; %>
|
|
87
|
+
<% const headerContent = api.specificArgs.headers?.type; %>
|
|
88
|
+
<% if (headerContent) { %>
|
|
89
|
+
/** <%= api.routeName.usage %> API의 헤더 타입 */
|
|
90
|
+
export type <%~ headerName %> = <%~ headerContent %>;
|
|
91
|
+
<% } %>
|
|
92
|
+
|
|
93
|
+
<% } %>
|
|
94
|
+
<% } %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%
|
|
2
|
+
/**
|
|
3
|
+
* DTO Import 템플릿
|
|
4
|
+
* - 생성된 DTO 타입들을 import하는 구문 생성
|
|
5
|
+
* - 상대 경로를 사용하여 dto.ts 파일에서 타입 가져오기
|
|
6
|
+
* - API 클래스와 TanStack Query 훅에서 사용
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { modelTypes, utils } = it;
|
|
10
|
+
const { _ } = utils;
|
|
11
|
+
|
|
12
|
+
// 사용 가능한 모든 DTO 타입명 추출
|
|
13
|
+
const dataContracts = _.map(modelTypes, "name");
|
|
14
|
+
%>
|
|
15
|
+
|
|
16
|
+
<% if (dataContracts?.length) { %>
|
|
17
|
+
/**
|
|
18
|
+
* 생성된 DTO 타입들을 import
|
|
19
|
+
* - 모든 API에서 사용되는 공통 타입 정의
|
|
20
|
+
* - 요청/응답 데이터의 타입 안전성 보장
|
|
21
|
+
*/
|
|
22
|
+
import type {
|
|
23
|
+
<% for (const dataContract of dataContracts) { %>
|
|
24
|
+
<%= dataContract %>,
|
|
25
|
+
<% } %>
|
|
26
|
+
} from '../../../shared/api/dto';
|
|
27
|
+
<% } %>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<%
|
|
2
|
+
/**
|
|
3
|
+
* API 메서드 호출 템플릿
|
|
4
|
+
* - 각 Swagger 엔드포인트에 대한 TypeScript 메서드 생성
|
|
5
|
+
* - ky HTTP 클라이언트를 사용한 요청 처리
|
|
6
|
+
* - 경로 파라미터, 쿼리 파라미터, 요청 본문, 헤더 지원
|
|
7
|
+
* - 타입 안전성을 위한 TypeScript 타입 적용
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { utils, route, config, apiClassName } = it;
|
|
11
|
+
const { _, pascalCase } = utils;
|
|
12
|
+
const routeDocs = includeFile("./route-docs", { config, route, utils });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* API 메서드명 생성
|
|
16
|
+
* HTTP 메서드 + 경로를 조합하여 camelCase 함수명 생성
|
|
17
|
+
* 예: GET /users/{id} -> getUserById
|
|
18
|
+
*/
|
|
19
|
+
const functionName = route.request.method + pascalCase(`${route.request.path
|
|
20
|
+
.split('/')
|
|
21
|
+
.map((segment) =>
|
|
22
|
+
segment.includes('${') ? `By_${segment.replace(/[${}]/g, '')}` : segment
|
|
23
|
+
)
|
|
24
|
+
.join('_')}`);
|
|
25
|
+
|
|
26
|
+
// 응답 타입 추출 (성공 응답의 타입 또는 any)
|
|
27
|
+
const responseType = route.responseBodyInfo?.success?.type || 'any';
|
|
28
|
+
|
|
29
|
+
// 경로 파라미터 추출 (예: /users/{id}에서 id)
|
|
30
|
+
const pathParams = _.values(route.request.parameters);
|
|
31
|
+
const hasPathParams = pathParams.length > 0;
|
|
32
|
+
|
|
33
|
+
// 요청 본문 데이터 (POST, PUT, PATCH에서 사용)
|
|
34
|
+
const payload = route.request.payload;
|
|
35
|
+
|
|
36
|
+
// 쿼리 파라미터 (URL 뒤에 ?key=value 형태)
|
|
37
|
+
const query = route.request.query;
|
|
38
|
+
|
|
39
|
+
// 커스텀 헤더
|
|
40
|
+
const headers = route.request.headers;
|
|
41
|
+
const headerName = headers ? `${pascalCase(route.routeName.usage)}Headers` : null;
|
|
42
|
+
|
|
43
|
+
// 쿼리 파라미터 타입명 생성
|
|
44
|
+
const queryTypeName = query ? `${pascalCase(route.routeName.usage)}QueryParams` : null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 메서드 파라미터 문자열 생성
|
|
48
|
+
* @param {Array} params - 경로 파라미터 배열
|
|
49
|
+
* @param {Object} query - 쿼리 파라미터 객체
|
|
50
|
+
* @param {Object} payload - 요청 본문 객체
|
|
51
|
+
* @param {string} headerName - 헤더 타입명
|
|
52
|
+
* @returns {string} 메서드 파라미터 문자열
|
|
53
|
+
*/
|
|
54
|
+
const generateParams = (params, query, payload, headerName) => {
|
|
55
|
+
const paramList = [
|
|
56
|
+
// 경로 파라미터 (예: id: number)
|
|
57
|
+
...(params ? params.map(param => `${param.name}${param.optional ? '?' : ''}: ${param.type}`) : []),
|
|
58
|
+
// 쿼리 파라미터 (예: params?: UserQueryParams)
|
|
59
|
+
...(query ? [`params${query.optional ? '?' : ''}: ${queryTypeName || query.type}`] : []),
|
|
60
|
+
// 요청 본문 (예: data: CreateUserDto)
|
|
61
|
+
...(payload ? [`data${payload.optional ? '?' : ''}: ${payload.type}`] : [])
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// ky Options에서 제외할 타입들 (중복 방지)
|
|
65
|
+
const omitTypes = [
|
|
66
|
+
...(query ? ["'searchParams'"] : []),
|
|
67
|
+
...(payload ? ["'json'"] : []),
|
|
68
|
+
...(headerName ? ["'headers'"] : [])
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// ky Options 타입 생성 (중복되는 속성 제외)
|
|
72
|
+
const optionsType = omitTypes.length > 0
|
|
73
|
+
? `Omit<Options, ${omitTypes.join(' | ')}>${headerName ? ` & { headers: ${headerName} }` : ''}`
|
|
74
|
+
: 'Options';
|
|
75
|
+
|
|
76
|
+
// ky 인스턴스와 옵션 파라미터 추가
|
|
77
|
+
paramList.push('kyInstance?: KyInstance', `options?: ${optionsType}`);
|
|
78
|
+
return paramList.join(', ');
|
|
79
|
+
};
|
|
80
|
+
%>
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
<%~ routeDocs.lines %> */
|
|
84
|
+
<%= functionName %>(<%~ generateParams(pathParams, query, payload, headerName) %>) {
|
|
85
|
+
// ky 인스턴스 선택 (전달된 인스턴스 또는 기본 인스턴스)
|
|
86
|
+
const instance = kyInstance ?? this.instance;
|
|
87
|
+
|
|
88
|
+
<% if (query) { %>
|
|
89
|
+
// 쿼리 파라미터를 URLSearchParams로 변환
|
|
90
|
+
const urlSearchParams = <%= apiClassName %>Api.createSearchParams(params);
|
|
91
|
+
<% } %>
|
|
92
|
+
|
|
93
|
+
// HTTP 요청 실행 및 JSON 응답 반환
|
|
94
|
+
return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`,{
|
|
95
|
+
<% if (query) { %>
|
|
96
|
+
searchParams: urlSearchParams, // 쿼리 파라미터 설정
|
|
97
|
+
<% } %>
|
|
98
|
+
<% if (payload) { %>
|
|
99
|
+
json: data, // 요청 본문 JSON 설정
|
|
100
|
+
<% } %>
|
|
101
|
+
...options, // 추가 ky 옵션 병합
|
|
102
|
+
}).json();
|
|
103
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { config, route, utils } = it;
|
|
3
|
+
const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils;
|
|
4
|
+
const { raw, request, routeName } = route;
|
|
5
|
+
|
|
6
|
+
const jsDocDescription = raw.description ?
|
|
7
|
+
` * @description ${formatDescription(raw.description, true)}` :
|
|
8
|
+
fmtToJSDocLine('No description', { eol: false });
|
|
9
|
+
const jsDocLines = _.compact([
|
|
10
|
+
_.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`,
|
|
11
|
+
raw.summary && ` * @summary ${raw.summary}`,
|
|
12
|
+
` * @request ${_.upperCase(request.method)}:${raw.route}`,
|
|
13
|
+
raw.deprecated && ` * @deprecated`,
|
|
14
|
+
routeName.duplicate && ` * @originalName ${routeName.original}`,
|
|
15
|
+
routeName.duplicate && ` * @duplicate`,
|
|
16
|
+
request.security && ` * @secure`,
|
|
17
|
+
...(config.generateResponses && raw.responsesTypes.length
|
|
18
|
+
? raw.responsesTypes.map(
|
|
19
|
+
({ type, status, description, isSuccess }) =>
|
|
20
|
+
` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`,
|
|
21
|
+
)
|
|
22
|
+
: []),
|
|
23
|
+
]).map(str => str.trimEnd()).join("\n");
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
description: jsDocDescription,
|
|
27
|
+
lines: jsDocLines,
|
|
28
|
+
}
|
|
29
|
+
%>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const { utils, route, config, modelTypes } = it;
|
|
3
|
+
const { _, pascalCase, require } = utils;
|
|
4
|
+
const apiClassName = pascalCase(route.moduleName);
|
|
5
|
+
const routes = route.routes;
|
|
6
|
+
const dataContracts = _.map(modelTypes, "name");
|
|
7
|
+
%>
|
|
8
|
+
|
|
9
|
+
import { kyInstance } from "@/shared/api"
|
|
10
|
+
|
|
11
|
+
import { <%= apiClassName %>Api } from './index'
|
|
12
|
+
|
|
13
|
+
const <%= route.moduleName %>Api = new <%= apiClassName %>Api(kyInstance);
|
|
14
|
+
|
|
15
|
+
export { <%= route.moduleName %>Api };
|