gongdata 0.1.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/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/index.cjs +663 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +509 -0
- package/dist/index.d.ts +509 -0
- package/dist/index.js +650 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
2
|
+
|
|
3
|
+
// src/core/parser.ts
|
|
4
|
+
|
|
5
|
+
// src/core/error.ts
|
|
6
|
+
var ResultCode = {
|
|
7
|
+
/** 정상 */
|
|
8
|
+
SUCCESS: "00",
|
|
9
|
+
// === 공공데이터포털 공통 에러 ===
|
|
10
|
+
/** 어플리케이션 에러 */
|
|
11
|
+
APPLICATION_ERROR: "1",
|
|
12
|
+
/** 잘못된 요청 파라미터 */
|
|
13
|
+
INVALID_REQUEST_PARAMETER: "10",
|
|
14
|
+
/** 해당 오픈API 서비스 없음 또는 폐기 */
|
|
15
|
+
NO_OPENAPI_SERVICE: "12",
|
|
16
|
+
/** 서비스 접근 거부 */
|
|
17
|
+
SERVICE_ACCESS_DENIED: "20",
|
|
18
|
+
/** 서비스 요청 제한 횟수 초과 */
|
|
19
|
+
REQUEST_LIMIT_EXCEEDED: "22",
|
|
20
|
+
/** 등록되지 않은 서비스키 */
|
|
21
|
+
UNREGISTERED_SERVICE_KEY: "30",
|
|
22
|
+
/** 서비스키 기한 만료 */
|
|
23
|
+
EXPIRED_SERVICE_KEY: "31",
|
|
24
|
+
/** 등록되지 않은 IP */
|
|
25
|
+
UNREGISTERED_IP: "32",
|
|
26
|
+
/** 기타 에러 */
|
|
27
|
+
UNKNOWN_ERROR: "99",
|
|
28
|
+
// === SDK 내부 에러 ===
|
|
29
|
+
/** HTTP 에러 */
|
|
30
|
+
HTTP_ERROR: "HTTP_ERROR",
|
|
31
|
+
/** 서비스 타임아웃 */
|
|
32
|
+
SERVICE_TIMEOUT: "SERVICE_TIMEOUT",
|
|
33
|
+
// === HRDK(한국산업인력공단) 자체 에러 ===
|
|
34
|
+
/** 게이트웨이 인증 오류 */
|
|
35
|
+
GATEWAY_AUTH_ERROR: "900",
|
|
36
|
+
/** 필수 파라미터 누락 */
|
|
37
|
+
MISSING_REQUIRED_PARAMETER: "910",
|
|
38
|
+
/** 잘못된 dataFormat (json/xml만 가능) */
|
|
39
|
+
INVALID_DATA_FORMAT: "920",
|
|
40
|
+
/** 최대 목록 수 초과 */
|
|
41
|
+
MAX_ROWS_EXCEEDED: "930",
|
|
42
|
+
/** 잘못된 토큰 파라미터 */
|
|
43
|
+
INVALID_TOKEN: "940",
|
|
44
|
+
/** 토큰 유효기한 만료 (1시간) */
|
|
45
|
+
TOKEN_EXPIRED: "941",
|
|
46
|
+
/** 파일을 찾을 수 없음 */
|
|
47
|
+
FILE_NOT_FOUND: "950",
|
|
48
|
+
/** HRDK 서버 에러 */
|
|
49
|
+
HRDK_SERVER_ERROR: "990"
|
|
50
|
+
};
|
|
51
|
+
var RESULT_MESSAGES = {
|
|
52
|
+
[ResultCode.SUCCESS]: "\uC815\uC0C1",
|
|
53
|
+
[ResultCode.APPLICATION_ERROR]: "\uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uC5D0\uB7EC",
|
|
54
|
+
[ResultCode.INVALID_REQUEST_PARAMETER]: "\uC798\uBABB\uB41C \uC694\uCCAD \uD30C\uB77C\uBBF8\uD130",
|
|
55
|
+
[ResultCode.NO_OPENAPI_SERVICE]: "\uD574\uB2F9 \uC624\uD508API \uC11C\uBE44\uC2A4 \uC5C6\uC74C",
|
|
56
|
+
[ResultCode.SERVICE_ACCESS_DENIED]: "\uC11C\uBE44\uC2A4 \uC811\uADFC \uAC70\uBD80",
|
|
57
|
+
[ResultCode.REQUEST_LIMIT_EXCEEDED]: "\uC11C\uBE44\uC2A4 \uC694\uCCAD \uC81C\uD55C \uD69F\uC218 \uCD08\uACFC",
|
|
58
|
+
[ResultCode.UNREGISTERED_SERVICE_KEY]: "\uB4F1\uB85D\uB418\uC9C0 \uC54A\uC740 \uC11C\uBE44\uC2A4\uD0A4",
|
|
59
|
+
[ResultCode.EXPIRED_SERVICE_KEY]: "\uC11C\uBE44\uC2A4\uD0A4 \uAE30\uD55C \uB9CC\uB8CC",
|
|
60
|
+
[ResultCode.UNREGISTERED_IP]: "\uB4F1\uB85D\uB418\uC9C0 \uC54A\uC740 IP",
|
|
61
|
+
[ResultCode.UNKNOWN_ERROR]: "\uAE30\uD0C0 \uC5D0\uB7EC",
|
|
62
|
+
[ResultCode.HTTP_ERROR]: "HTTP \uC5D0\uB7EC",
|
|
63
|
+
[ResultCode.SERVICE_TIMEOUT]: "\uC11C\uBE44\uC2A4 \uD0C0\uC784\uC544\uC6C3",
|
|
64
|
+
[ResultCode.GATEWAY_AUTH_ERROR]: "\uAC8C\uC774\uD2B8\uC6E8\uC774 \uC778\uC99D \uC624\uB958",
|
|
65
|
+
[ResultCode.MISSING_REQUIRED_PARAMETER]: "\uD544\uC218 \uD30C\uB77C\uBBF8\uD130 \uB204\uB77D",
|
|
66
|
+
[ResultCode.INVALID_DATA_FORMAT]: "\uC798\uBABB\uB41C dataFormat (json/xml\uB9CC \uAC00\uB2A5)",
|
|
67
|
+
[ResultCode.MAX_ROWS_EXCEEDED]: "\uCD5C\uB300 \uBAA9\uB85D \uC218 \uCD08\uACFC",
|
|
68
|
+
[ResultCode.INVALID_TOKEN]: "\uC798\uBABB\uB41C \uD1A0\uD070 \uD30C\uB77C\uBBF8\uD130",
|
|
69
|
+
[ResultCode.TOKEN_EXPIRED]: "\uD1A0\uD070 \uC720\uD6A8\uAE30\uD55C \uB9CC\uB8CC",
|
|
70
|
+
[ResultCode.FILE_NOT_FOUND]: "\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C",
|
|
71
|
+
[ResultCode.HRDK_SERVER_ERROR]: "HRDK \uC11C\uBC84 \uC5D0\uB7EC"
|
|
72
|
+
};
|
|
73
|
+
var GongdataError = class _GongdataError extends Error {
|
|
74
|
+
code;
|
|
75
|
+
originalResponse;
|
|
76
|
+
constructor(code, message, originalResponse) {
|
|
77
|
+
const errorMessage = message ?? RESULT_MESSAGES[code] ?? `Unknown error: ${code}`;
|
|
78
|
+
super(errorMessage);
|
|
79
|
+
this.name = "GongdataError";
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.originalResponse = originalResponse;
|
|
82
|
+
if (Error.captureStackTrace) {
|
|
83
|
+
Error.captureStackTrace(this, _GongdataError);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
static isGongdataError(error) {
|
|
87
|
+
return error instanceof _GongdataError;
|
|
88
|
+
}
|
|
89
|
+
static fromResponse(header, originalResponse) {
|
|
90
|
+
return new _GongdataError(header.resultCode, header.resultMsg, originalResponse);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/core/parser.ts
|
|
95
|
+
var xmlParser = new XMLParser({
|
|
96
|
+
ignoreAttributes: false,
|
|
97
|
+
attributeNamePrefix: "@_",
|
|
98
|
+
textNodeName: "#text",
|
|
99
|
+
parseTagValue: false,
|
|
100
|
+
// '00' 같은 값을 숫자로 변환하지 않음
|
|
101
|
+
trimValues: true
|
|
102
|
+
});
|
|
103
|
+
function parseXml(xml) {
|
|
104
|
+
return xmlParser.parse(xml);
|
|
105
|
+
}
|
|
106
|
+
function isXmlResponse(contentType) {
|
|
107
|
+
return contentType?.includes("application/xml") ?? contentType?.includes("text/xml") ?? false;
|
|
108
|
+
}
|
|
109
|
+
function validateResponse(header) {
|
|
110
|
+
const { resultCode, resultMsg } = header;
|
|
111
|
+
if (resultCode !== ResultCode.SUCCESS) {
|
|
112
|
+
throw GongdataError.fromResponse({ resultCode, resultMsg });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function normalizeXmlItems(items) {
|
|
116
|
+
if (items === "" || items === null || items === void 0) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const { item } = items;
|
|
120
|
+
if (item === null || item === void 0) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(item)) {
|
|
124
|
+
return item;
|
|
125
|
+
}
|
|
126
|
+
return [item];
|
|
127
|
+
}
|
|
128
|
+
function normalizeJsonItems(items) {
|
|
129
|
+
if (items === "" || items === null || items === void 0) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(items)) {
|
|
133
|
+
return items;
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
function parseResponse(data, contentType) {
|
|
138
|
+
if (typeof data === "string") {
|
|
139
|
+
if (isXmlResponse(contentType)) {
|
|
140
|
+
const parsed = parseXml(data);
|
|
141
|
+
validateResponse(parsed.response.header);
|
|
142
|
+
return {
|
|
143
|
+
header: parsed.response.header,
|
|
144
|
+
body: {
|
|
145
|
+
items: normalizeXmlItems(parsed.response.body.items),
|
|
146
|
+
numOfRows: Number(parsed.response.body.numOfRows),
|
|
147
|
+
pageNo: Number(parsed.response.body.pageNo),
|
|
148
|
+
totalCount: Number(parsed.response.body.totalCount)
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
} else {
|
|
152
|
+
const parsed = JSON.parse(data);
|
|
153
|
+
validateResponse(parsed.header);
|
|
154
|
+
return {
|
|
155
|
+
header: parsed.header,
|
|
156
|
+
body: {
|
|
157
|
+
items: normalizeJsonItems(parsed.body.items),
|
|
158
|
+
numOfRows: parsed.body.numOfRows,
|
|
159
|
+
pageNo: parsed.body.pageNo,
|
|
160
|
+
totalCount: parsed.body.totalCount
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const parsed = data;
|
|
166
|
+
validateResponse(parsed.header);
|
|
167
|
+
return {
|
|
168
|
+
header: parsed.header,
|
|
169
|
+
body: {
|
|
170
|
+
items: normalizeJsonItems(parsed.body.items),
|
|
171
|
+
numOfRows: parsed.body.numOfRows,
|
|
172
|
+
pageNo: parsed.body.pageNo,
|
|
173
|
+
totalCount: parsed.body.totalCount
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/core/http.ts
|
|
180
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
181
|
+
var DEFAULT_NUM_OF_ROWS = 100;
|
|
182
|
+
var HttpClient = class {
|
|
183
|
+
serviceKey;
|
|
184
|
+
timeout;
|
|
185
|
+
maxRetries;
|
|
186
|
+
retryDelay;
|
|
187
|
+
constructor(config) {
|
|
188
|
+
this.serviceKey = config.serviceKey;
|
|
189
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
190
|
+
this.maxRetries = config.retry?.maxRetries ?? 3;
|
|
191
|
+
this.retryDelay = config.retry?.delay ?? 1e3;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 서비스 키 반환 (일부 API에서 직접 URL 구성 시 필요)
|
|
195
|
+
*/
|
|
196
|
+
getServiceKey() {
|
|
197
|
+
return this.serviceKey;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* URL 파라미터 빌드
|
|
201
|
+
*/
|
|
202
|
+
buildParams(params) {
|
|
203
|
+
const searchParams = new URLSearchParams();
|
|
204
|
+
searchParams.set("serviceKey", this.serviceKey);
|
|
205
|
+
for (const [key, value] of Object.entries(params)) {
|
|
206
|
+
if (value !== void 0) {
|
|
207
|
+
searchParams.set(key, String(value));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return searchParams;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 단일 요청 실행 (재시도 포함)
|
|
214
|
+
*/
|
|
215
|
+
async request(baseUrl, options) {
|
|
216
|
+
const { path, params = {}, pageNo = 1, numOfRows = DEFAULT_NUM_OF_ROWS } = options;
|
|
217
|
+
const url = `${baseUrl}${path}`;
|
|
218
|
+
const searchParams = this.buildParams({
|
|
219
|
+
...params,
|
|
220
|
+
pageNo,
|
|
221
|
+
numOfRows,
|
|
222
|
+
dataFormat: "json"
|
|
223
|
+
// 필수 파라미터
|
|
224
|
+
});
|
|
225
|
+
const fullUrl = `${url}?${searchParams.toString()}`;
|
|
226
|
+
let lastError = null;
|
|
227
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
228
|
+
try {
|
|
229
|
+
const controller = new AbortController();
|
|
230
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
231
|
+
const response = await fetch(fullUrl, {
|
|
232
|
+
method: "GET",
|
|
233
|
+
signal: controller.signal,
|
|
234
|
+
headers: {
|
|
235
|
+
Accept: "application/json, application/xml"
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
clearTimeout(timeoutId);
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new GongdataError(
|
|
241
|
+
ResultCode.HTTP_ERROR,
|
|
242
|
+
`HTTP ${response.status}: ${response.statusText}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const contentType = response.headers.get("content-type");
|
|
246
|
+
const text = await response.text();
|
|
247
|
+
const parsed = parseResponse(text, contentType);
|
|
248
|
+
const { body } = parsed;
|
|
249
|
+
return {
|
|
250
|
+
data: body.items,
|
|
251
|
+
pagination: {
|
|
252
|
+
pageNo: body.pageNo,
|
|
253
|
+
numOfRows: body.numOfRows,
|
|
254
|
+
totalCount: body.totalCount
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
259
|
+
if (lastError.name === "AbortError") {
|
|
260
|
+
lastError = new GongdataError(ResultCode.SERVICE_TIMEOUT, "Request timeout");
|
|
261
|
+
}
|
|
262
|
+
if (GongdataError.isGongdataError(lastError)) {
|
|
263
|
+
const nonRetryableCodes = [
|
|
264
|
+
ResultCode.UNREGISTERED_SERVICE_KEY,
|
|
265
|
+
ResultCode.EXPIRED_SERVICE_KEY,
|
|
266
|
+
ResultCode.SERVICE_ACCESS_DENIED,
|
|
267
|
+
ResultCode.INVALID_REQUEST_PARAMETER,
|
|
268
|
+
ResultCode.UNREGISTERED_IP,
|
|
269
|
+
ResultCode.MISSING_REQUIRED_PARAMETER,
|
|
270
|
+
ResultCode.INVALID_DATA_FORMAT
|
|
271
|
+
];
|
|
272
|
+
if (nonRetryableCodes.includes(lastError.code)) {
|
|
273
|
+
throw lastError;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (attempt < this.maxRetries) {
|
|
277
|
+
await this.sleep(this.retryDelay * (attempt + 1));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw lastError ?? new GongdataError(ResultCode.APPLICATION_ERROR, "Unknown error");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* 전체 페이지 데이터 수집
|
|
285
|
+
*/
|
|
286
|
+
async requestAll(baseUrl, options, numOfRows = DEFAULT_NUM_OF_ROWS) {
|
|
287
|
+
const allItems = [];
|
|
288
|
+
let pageNo = 1;
|
|
289
|
+
let totalCount = 0;
|
|
290
|
+
do {
|
|
291
|
+
const result = await this.request(baseUrl, {
|
|
292
|
+
...options,
|
|
293
|
+
pageNo,
|
|
294
|
+
numOfRows
|
|
295
|
+
});
|
|
296
|
+
allItems.push(...result.data);
|
|
297
|
+
totalCount = result.pagination.totalCount;
|
|
298
|
+
pageNo++;
|
|
299
|
+
} while (allItems.length < totalCount);
|
|
300
|
+
return allItems;
|
|
301
|
+
}
|
|
302
|
+
sleep(ms) {
|
|
303
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/core/response.ts
|
|
308
|
+
var DataResponse = class {
|
|
309
|
+
/** 정규화된 데이터 (getter) */
|
|
310
|
+
get data() {
|
|
311
|
+
return this.getData();
|
|
312
|
+
}
|
|
313
|
+
/** 원본 데이터 (getter) */
|
|
314
|
+
get rawData() {
|
|
315
|
+
return this.getRawData();
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 데이터가 비어있는지 확인
|
|
319
|
+
*/
|
|
320
|
+
isEmpty() {
|
|
321
|
+
return this.data.length === 0;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* 데이터 개수
|
|
325
|
+
*/
|
|
326
|
+
count() {
|
|
327
|
+
return this.data.length;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var PaginatedResponse = class extends DataResponse {
|
|
331
|
+
/** 페이지네이션 정보 (getter) */
|
|
332
|
+
get pagination() {
|
|
333
|
+
return this.getPagination();
|
|
334
|
+
}
|
|
335
|
+
/** 전체 데이터 개수 */
|
|
336
|
+
get totalCount() {
|
|
337
|
+
return this.pagination.totalCount;
|
|
338
|
+
}
|
|
339
|
+
/** 현재 페이지 번호 */
|
|
340
|
+
get pageNo() {
|
|
341
|
+
return this.pagination.pageNo;
|
|
342
|
+
}
|
|
343
|
+
/** 페이지 당 항목 수 */
|
|
344
|
+
get numOfRows() {
|
|
345
|
+
return this.pagination.numOfRows;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* 다음 페이지 존재 여부
|
|
349
|
+
*/
|
|
350
|
+
hasNextPage() {
|
|
351
|
+
const { pageNo, numOfRows, totalCount } = this.pagination;
|
|
352
|
+
return pageNo * numOfRows < totalCount;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/services/base.ts
|
|
357
|
+
var BaseService = class {
|
|
358
|
+
http;
|
|
359
|
+
constructor(http) {
|
|
360
|
+
this.http = http;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 단일 페이지 요청
|
|
364
|
+
*/
|
|
365
|
+
async request(options) {
|
|
366
|
+
return this.http.request(this.baseUrl, options);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 전체 페이지 자동 수집
|
|
370
|
+
*/
|
|
371
|
+
async requestAll(options, numOfRows) {
|
|
372
|
+
return this.http.requestAll(this.baseUrl, options, numOfRows);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// src/services/qualification/transformers.ts
|
|
377
|
+
function formatDate(yyyymmdd) {
|
|
378
|
+
if (!yyyymmdd || yyyymmdd.length !== 8) return yyyymmdd;
|
|
379
|
+
return `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`;
|
|
380
|
+
}
|
|
381
|
+
function transformExamSchedule(raw) {
|
|
382
|
+
const writtenExam = {
|
|
383
|
+
registrationStart: formatDate(raw.docRegStartDt),
|
|
384
|
+
registrationEnd: formatDate(raw.docRegEndDt),
|
|
385
|
+
examStart: formatDate(raw.docExamStartDt),
|
|
386
|
+
examEnd: formatDate(raw.docExamEndDt),
|
|
387
|
+
resultDate: formatDate(raw.docPassDt)
|
|
388
|
+
};
|
|
389
|
+
const practicalExam = {
|
|
390
|
+
registrationStart: formatDate(raw.pracRegStartDt),
|
|
391
|
+
registrationEnd: formatDate(raw.pracRegEndDt),
|
|
392
|
+
examStart: formatDate(raw.pracExamStartDt),
|
|
393
|
+
examEnd: formatDate(raw.pracExamEndDt),
|
|
394
|
+
resultDate: formatDate(raw.pracPassDt)
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
year: Number(raw.implYy),
|
|
398
|
+
round: raw.implSeq,
|
|
399
|
+
category: {
|
|
400
|
+
code: raw.qualgbCd,
|
|
401
|
+
name: raw.qualgbNm
|
|
402
|
+
},
|
|
403
|
+
description: raw.description,
|
|
404
|
+
writtenExam,
|
|
405
|
+
practicalExam
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function transformExamSchedules(rawList) {
|
|
409
|
+
return rawList.map(transformExamSchedule);
|
|
410
|
+
}
|
|
411
|
+
function transformSubject(raw) {
|
|
412
|
+
return {
|
|
413
|
+
code: raw.jmcd,
|
|
414
|
+
name: raw.jmfldnm,
|
|
415
|
+
category: {
|
|
416
|
+
code: raw.qualgbcd,
|
|
417
|
+
name: raw.qualgbnm
|
|
418
|
+
},
|
|
419
|
+
series: {
|
|
420
|
+
code: raw.seriescd,
|
|
421
|
+
name: raw.seriesnm
|
|
422
|
+
},
|
|
423
|
+
majorJobField: {
|
|
424
|
+
code: raw.obligfldcd,
|
|
425
|
+
name: raw.obligfldnm
|
|
426
|
+
},
|
|
427
|
+
minorJobField: {
|
|
428
|
+
code: raw.mdobligfldcd,
|
|
429
|
+
name: raw.mdobligfldnm
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function transformSubjects(rawList) {
|
|
434
|
+
return rawList.map(transformSubject);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/services/qualification/response.ts
|
|
438
|
+
var ScheduleResponse = class extends PaginatedResponse {
|
|
439
|
+
_raw;
|
|
440
|
+
_data;
|
|
441
|
+
_pagination;
|
|
442
|
+
constructor(raw, data, pagination) {
|
|
443
|
+
super();
|
|
444
|
+
this._raw = raw;
|
|
445
|
+
this._data = data;
|
|
446
|
+
this._pagination = pagination;
|
|
447
|
+
}
|
|
448
|
+
getData() {
|
|
449
|
+
return this._data;
|
|
450
|
+
}
|
|
451
|
+
getRawData() {
|
|
452
|
+
return this._raw;
|
|
453
|
+
}
|
|
454
|
+
getPagination() {
|
|
455
|
+
return this._pagination;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
var AllSchedulesResponse = class extends DataResponse {
|
|
459
|
+
_raw;
|
|
460
|
+
_data;
|
|
461
|
+
constructor(raw, data) {
|
|
462
|
+
super();
|
|
463
|
+
this._raw = raw;
|
|
464
|
+
this._data = data;
|
|
465
|
+
}
|
|
466
|
+
getData() {
|
|
467
|
+
return this._data;
|
|
468
|
+
}
|
|
469
|
+
getRawData() {
|
|
470
|
+
return this._raw;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
var SubjectResponse = class extends DataResponse {
|
|
474
|
+
_raw;
|
|
475
|
+
_data;
|
|
476
|
+
constructor(raw, data) {
|
|
477
|
+
super();
|
|
478
|
+
this._raw = raw;
|
|
479
|
+
this._data = data;
|
|
480
|
+
}
|
|
481
|
+
getData() {
|
|
482
|
+
return this._data;
|
|
483
|
+
}
|
|
484
|
+
getRawData() {
|
|
485
|
+
return this._raw;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* 종목코드로 찾기
|
|
489
|
+
*/
|
|
490
|
+
findByCode(code) {
|
|
491
|
+
return this._data.find((s) => s.code === code);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* 종목명으로 찾기
|
|
495
|
+
*/
|
|
496
|
+
findByName(name) {
|
|
497
|
+
return this._data.find((s) => s.name === name);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 자격구분으로 필터링
|
|
501
|
+
*/
|
|
502
|
+
filterByCategory(categoryCode) {
|
|
503
|
+
return this._data.filter((s) => s.category.code === categoryCode);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/services/qualification/service.ts
|
|
508
|
+
var SCHEDULE_API_BASE_URL = "http://apis.data.go.kr/B490007/qualExamSchd";
|
|
509
|
+
var SUBJECT_API_BASE_URL = "http://openapi.q-net.or.kr/api/service/rest/InquiryListNationalQualifcationSVC";
|
|
510
|
+
var QualificationService = class extends BaseService {
|
|
511
|
+
baseUrl = SCHEDULE_API_BASE_URL;
|
|
512
|
+
/**
|
|
513
|
+
* 시험일정 목록 조회 (단일 페이지)
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* const result = await client.qualification.getSchedules({ year: 2026 });
|
|
518
|
+
*
|
|
519
|
+
* // 정규화된 데이터 (SDK 보장)
|
|
520
|
+
* result.getData()[0].writtenExam.registrationStart // '2026-01-24'
|
|
521
|
+
*
|
|
522
|
+
* // 원본 데이터 (공공 API 그대로)
|
|
523
|
+
* result.getRawData()[0].docRegStartDt // '20260124'
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
async getSchedules(params, options) {
|
|
527
|
+
const result = await this.request({
|
|
528
|
+
path: "/getQualExamSchdList",
|
|
529
|
+
params: {
|
|
530
|
+
implYy: params.year,
|
|
531
|
+
qualgbCd: params.category,
|
|
532
|
+
jmCd: params.jmCode
|
|
533
|
+
},
|
|
534
|
+
pageNo: options?.pageNo,
|
|
535
|
+
numOfRows: options?.numOfRows
|
|
536
|
+
});
|
|
537
|
+
return new ScheduleResponse(
|
|
538
|
+
result.data,
|
|
539
|
+
transformExamSchedules(result.data),
|
|
540
|
+
result.pagination
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* 시험일정 전체 조회 (모든 페이지 자동 수집)
|
|
545
|
+
*/
|
|
546
|
+
async getAllSchedules(params) {
|
|
547
|
+
const raw = await this.requestAll({
|
|
548
|
+
path: "/getQualExamSchdList",
|
|
549
|
+
params: {
|
|
550
|
+
implYy: params.year,
|
|
551
|
+
qualgbCd: params.category,
|
|
552
|
+
jmCd: params.jmCode
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
return new AllSchedulesResponse(raw, transformExamSchedules(raw));
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* 국가자격 종목 전체 목록 조회
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```typescript
|
|
562
|
+
* const result = await client.qualification.getSubjects();
|
|
563
|
+
*
|
|
564
|
+
* // 정규화된 데이터
|
|
565
|
+
* result.getData()[0].name // '정보처리기사'
|
|
566
|
+
*
|
|
567
|
+
* // 편의 메서드
|
|
568
|
+
* result.findByName('정보처리기사') // Subject
|
|
569
|
+
* result.filterByCategory('T') // 국가기술자격만
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
async getSubjects() {
|
|
573
|
+
const url = `${SUBJECT_API_BASE_URL}/getList?serviceKey=${this.http.getServiceKey()}`;
|
|
574
|
+
const response = await fetch(url, {
|
|
575
|
+
method: "GET",
|
|
576
|
+
headers: { Accept: "application/xml" }
|
|
577
|
+
});
|
|
578
|
+
if (!response.ok) {
|
|
579
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
580
|
+
}
|
|
581
|
+
const xml = await response.text();
|
|
582
|
+
const parsed = parseXml(xml);
|
|
583
|
+
validateResponse(parsed.response.header);
|
|
584
|
+
const raw = normalizeXmlItems(parsed.response.body.items);
|
|
585
|
+
return new SubjectResponse(raw, transformSubjects(raw));
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// src/services/qualification/constants.ts
|
|
590
|
+
var QualificationCategory = {
|
|
591
|
+
/** 국가기술자격 */
|
|
592
|
+
NATIONAL_TECHNICAL: "T",
|
|
593
|
+
/** 과정평가형자격 */
|
|
594
|
+
COURSE_EVALUATION: "C",
|
|
595
|
+
/** 일학습병행자격 */
|
|
596
|
+
WORK_LEARNING: "W",
|
|
597
|
+
/** 국가전문자격 */
|
|
598
|
+
NATIONAL_PROFESSIONAL: "S"
|
|
599
|
+
};
|
|
600
|
+
var JmCode = {
|
|
601
|
+
// === 정보통신 ===
|
|
602
|
+
/** 정보처리기사 */
|
|
603
|
+
INFORMATION_PROCESSING_ENGINEER: "1320",
|
|
604
|
+
/** 정보처리산업기사 */
|
|
605
|
+
INFORMATION_PROCESSING_INDUSTRIAL_ENGINEER: "2290",
|
|
606
|
+
/** 정보관리기술사 */
|
|
607
|
+
INFORMATION_MANAGEMENT_PROFESSIONAL_ENGINEER: "0601",
|
|
608
|
+
/** 컴퓨터시스템응용기술사 */
|
|
609
|
+
COMPUTER_SYSTEM_APPLICATION_PROFESSIONAL_ENGINEER: "0622",
|
|
610
|
+
// === 전기/전자 ===
|
|
611
|
+
/** 전기기사 */
|
|
612
|
+
ELECTRICAL_ENGINEER: "1150",
|
|
613
|
+
/** 전기기능사 */
|
|
614
|
+
ELECTRICAL_TECHNICIAN: "7780",
|
|
615
|
+
/** 전기공사기사 */
|
|
616
|
+
ELECTRICAL_CONSTRUCTION_ENGINEER: "1160",
|
|
617
|
+
/** 전자기사 */
|
|
618
|
+
ELECTRONICS_ENGINEER: "1230",
|
|
619
|
+
// === 기계 ===
|
|
620
|
+
/** 기계기사 */
|
|
621
|
+
MECHANICAL_ENGINEER: "1431",
|
|
622
|
+
/** 용접기사 */
|
|
623
|
+
WELDING_ENGINEER: "1022",
|
|
624
|
+
/** 지게차운전기능사 */
|
|
625
|
+
FORKLIFT_DRIVER_TECHNICIAN: "7875",
|
|
626
|
+
// === 건설 ===
|
|
627
|
+
/** 건축기사 */
|
|
628
|
+
ARCHITECTURE_ENGINEER: "1190",
|
|
629
|
+
/** 토목기사 */
|
|
630
|
+
CIVIL_ENGINEERING_ENGINEER: "1080",
|
|
631
|
+
// === 조리/서비스 ===
|
|
632
|
+
/** 한식조리기능사 */
|
|
633
|
+
KOREAN_CUISINE_TECHNICIAN: "7910",
|
|
634
|
+
/** 양식조리기능사 */
|
|
635
|
+
WESTERN_CUISINE_TECHNICIAN: "7911",
|
|
636
|
+
/** 미용사(일반) */
|
|
637
|
+
BEAUTICIAN_GENERAL: "7937"
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// src/client.ts
|
|
641
|
+
function createClient(config) {
|
|
642
|
+
const http = new HttpClient(config);
|
|
643
|
+
return {
|
|
644
|
+
qualification: new QualificationService(http)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export { AllSchedulesResponse, BaseService, DataResponse, GongdataError, JmCode, PaginatedResponse, QualificationCategory, QualificationService, ResultCode, ScheduleResponse, SubjectResponse, createClient };
|
|
649
|
+
//# sourceMappingURL=index.js.map
|
|
650
|
+
//# sourceMappingURL=index.js.map
|