apick-api 3.4.0 → 3.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.0 — 2026-09-30
4
+
5
+ - 간편인증 조회 상품 2종을 추가했습니다. `requestCashReceiptDeduction()`/`getCashReceiptDeduction()`은 현금영수증 소득공제 내역을 `incomeYears`(1~3)로, `requestTaxReturnHistory()`/`getTaxReturnHistory()`는 국세 신고내역을 `years`(1~10)로 조회합니다.
6
+ - Add two simple-auth data products: cash receipt income deductions (`incomeYears` 1-3) and national tax return history (`years` 1-10), each with a `request*()`/`get*()` pair and result payload types.
7
+ - 유튜브 공개 영상 API 4종을 추가했습니다: `youtubeMetadata()`, `youtubeThumbnail()`(JPG), `youtubeSubtitleList()`, `youtubeSubtitle()`(VTT·SRT·TXT).
8
+ - Add four public YouTube video methods: metadata, thumbnail (JPG), subtitle language list, and subtitle download (VTT/SRT/TXT).
9
+
10
+ ## 3.4.1 — 2026-09-28
11
+
12
+ - README와 한국어·영어 가이드의 간편인증 5종 예제에 모든 진행 상태와 `resultAvailable` 종료 기준을 적용했습니다. 전체·부분 성공, 인증 거부·만료·실패와 `RESULT_EXPIRED`를 구분합니다.
13
+ - Align all five simple-auth polling examples in the README and Korean/English guides with every progress state and the `resultAvailable` completion criterion. Distinguish full/partial success, rejected/expired authentication, failure, and `RESULT_EXPIRED`.
14
+ - 순차 폴링 간격을 5→10→20→30초로 늘린 뒤 30초를 유지하며, 인증·전체 대기시간 제한과 자동 재접수 방지를 안내합니다. 문서 코드를 직접 실행하는 회귀 테스트를 추가했습니다.
15
+ - Document sequential polling at 5→10→20→30 seconds, capped at 30 seconds, with authentication/overall waiting limits and no automatic resubmission. Add regression tests that execute the documentation examples.
16
+
3
17
  ## 3.4.0 — 2026-09-27
4
18
 
5
19
  - 간편인증 기반 조회 상품 5종(재직·보험료 확인, 금융소득 조회, 국민연금 가입내역, 운전면허 조회, 국가 건강검진 결과)을 추가했습니다. 각 상품은 `request*()`로 본인 간편인증을 접수하고 `get*()`로 상태·결과를 폴링합니다.
package/README.md CHANGED
@@ -71,6 +71,10 @@ Leave the allowed-IP list blank for unrestricted access. To restrict access, reg
71
71
  | `googleSearch(keyword, options)` | 웹 검색 / Web search | JSON |
72
72
  | `googleImageSearch(keyword, options)` | 이미지 검색 / Image search | JSON |
73
73
  | `screenshot(url)` | 웹페이지 화면캡처 / Web screenshot | Binary |
74
+ | `youtubeMetadata(url)` | 유튜브 영상 정보 / YouTube video metadata | JSON |
75
+ | `youtubeThumbnail(url)` | 유튜브 썸네일 / YouTube thumbnail | JPG |
76
+ | `youtubeSubtitleList(url)` | 유튜브 자막 언어 목록 / YouTube subtitle languages | JSON |
77
+ | `youtubeSubtitle(url, lang, options)` | 유튜브 자막 다운로드 / YouTube subtitles | VTT·SRT·TXT |
74
78
  | `createTtsJob(text, options)` | 한국어 내레이션 작업 접수 / Create TTS job | JSON |
75
79
  | `getTtsJob(jobId)` | TTS 작업 상태 / TTS job status | JSON |
76
80
  | `cancelTtsJob(jobId)` | 대기·생성 중 TTS 작업 취소 / Cancel waiting or processing TTS job | JSON |
@@ -95,6 +99,8 @@ Leave the allowed-IP list blank for unrestricted access. To restrict access, reg
95
99
  | `requestNpsJoinHistory(input)` / `getNpsJoinHistory(transactionId)` | 국민연금 가입내역조회 / National Pension join history | JSON |
96
100
  | `requestDrivingLicense(input)` / `getDrivingLicense(transactionId)` | 운전면허 조회 / Driver's license check | JSON |
97
101
  | `requestHealthCheckup(input)` / `getHealthCheckup(transactionId)` | 국가 건강검진 결과 조회 / National health checkup results | JSON |
102
+ | `requestCashReceiptDeduction(input)` / `getCashReceiptDeduction(transactionId)` | 현금영수증 소득공제 내역 / Cash receipt income deductions | JSON |
103
+ | `requestTaxReturnHistory(input)` / `getTaxReturnHistory(transactionId)` | 국세 신고내역 조회 / National tax return history | JSON |
98
104
 
99
105
  ## JSON 결과 / JSON results
100
106
 
@@ -237,32 +243,86 @@ The four document-specific methods return JSON. `maskResidentNumber` returns PNG
237
243
 
238
244
  ## 간편인증 기반 데이터 조회 / Simple-auth data lookups
239
245
 
240
- 본인 간편인증이 필요한 조회 상품(재직·소득·연금·면허·건강검진)은 접수(`request*`)와 결과 조회(`get*`)가 분리되어 있습니다. 접수 응답의 `transactionId`로 결과를 폴링하세요.
246
+ 본인 간편인증이 필요한 조회 상품(재직·소득·연금·면허·건강검진·현금영수증·국세 신고내역)은 접수(`request*`)와 결과 조회(`get*`)가 분리되어 있습니다. 접수 응답의 `transactionId`로 결과를 폴링하세요.
241
247
 
242
- Products that require the user's own simple-auth verification (employment, income, pension, driver's license, health checkup) split the call into a `request*()` acceptance and a `get*()` poll. Use the `transactionId` from the accepted response to poll for the result.
248
+ Products that require the user's own simple-auth verification (employment, income, pension, driver's license, health checkup, cash receipts, tax returns) split the call into a `request*()` acceptance and a `get*()` poll. Use the `transactionId` from the accepted response to poll for the result.
243
249
 
250
+ 아래 함수는 7종 모두에 공통으로 사용합니다. 같은 `transactionId`로 순차 조회하며 5→10→20→30초 간격으로 늘린 뒤 30초를 유지합니다. `resultAvailable === true`이면 즉시 결과를 반환합니다. `SUCCESS`는 전체 성공, `PARTIAL_SUCCESS`는 부분 성공이므로 `sources`에서 누락·실패 항목을 확인하세요. `AUTH_REJECTED`·`AUTH_EXPIRED`·`FAILED`는 실패 종료이며, `errorCode: 'RESULT_EXPIRED'`는 결과 보관 기간 만료입니다. 실패·만료 시 자동으로 재접수하지 않습니다.
251
+
252
+ Use this helper for all seven products. Poll sequentially with the same `transactionId`, waiting 5→10→20→30 seconds and then keeping the 30-second interval. Return the result immediately when `resultAvailable === true`. `SUCCESS` means full success; `PARTIAL_SUCCESS` means partial success, so inspect `sources` for missing or failed items. `AUTH_REJECTED`, `AUTH_EXPIRED`, and `FAILED` are terminal failures; `errorCode: 'RESULT_EXPIRED'` means the retained result has expired. Never resubmit automatically after failure or expiry.
253
+
254
+ <!-- simple-auth-polling:start -->
255
+ ```js
256
+ async function pollDataResult(getResult, accepted, { timeoutMs = 600_000 } = {}) {
257
+ const pending = new Set([
258
+ 'AUTH_REQUESTED', 'AUTH_WAITING', 'AUTH_COMPLETED', 'COLLECTING', 'COLLECTED'
259
+ ]);
260
+ const failed = new Set(['AUTH_REJECTED', 'AUTH_EXPIRED', 'FAILED']);
261
+ const completed = new Set(['SUCCESS', 'PARTIAL_SUCCESS']);
262
+ const delays = [5_000, 10_000, 20_000, 30_000];
263
+ const deadline = Date.now() + timeoutMs;
264
+ const transactionId = accepted.data.transactionId;
265
+ const stop = code => { throw Object.assign(new Error(code), { code }); };
266
+ let response = accepted;
267
+ let attempt = 0;
268
+
269
+ for (;;) {
270
+ const data = response.data;
271
+ if (data.errorCode === 'RESULT_EXPIRED') stop('RESULT_EXPIRED');
272
+ if (failed.has(data.status)) stop(data.errorCode || data.status);
273
+ if (data.resultAvailable === true) {
274
+ if (data.result == null) stop('INVALID_RESULT');
275
+ return response;
276
+ }
277
+ if (completed.has(data.status)) stop('RESULT_NOT_AVAILABLE');
278
+ if (!pending.has(data.status)) stop('UNKNOWN_STATUS');
279
+
280
+ const awaitingApproval = ['AUTH_REQUESTED', 'AUTH_WAITING'].includes(data.status);
281
+ const authDeadline = Date.parse(data.expiresAt || accepted.data.expiresAt);
282
+ const limit = awaitingApproval && Number.isFinite(authDeadline)
283
+ ? Math.min(deadline, authDeadline) : deadline;
284
+ const timeoutCode = awaitingApproval && limit === authDeadline
285
+ ? 'AUTH_WAIT_TIMEOUT' : 'CLIENT_POLL_TIMEOUT';
286
+ const remaining = limit - Date.now();
287
+ if (remaining <= 0) stop(timeoutCode);
288
+ await new Promise(resolve => setTimeout(resolve,
289
+ Math.min(delays[Math.min(attempt++, delays.length - 1)], remaining)));
290
+ if (Date.now() >= limit) stop(timeoutCode);
291
+ response = await getResult(transactionId);
292
+ }
293
+ }
294
+ ```
295
+ <!-- simple-auth-polling:end -->
296
+
297
+ `AUTH_REQUESTED`·`AUTH_WAITING`은 사용자 승인을 기다리고, `AUTH_COMPLETED`·`COLLECTING`·`COLLECTED`는 결과가 준비될 때까지 계속 조회합니다. 과금 여부(`charged`, `success`)는 완료 기준이 아닙니다. 인증 대기에만 `expiresAt`을 적용하고 승인 후에는 전체 대기 한도를 적용합니다. 예제의 10분 한도는 클라이언트 정책이며, 진행 중인 호출의 제한 시간은 SDK의 `timeoutMs`로 별도 설정하세요. `AUTH_WAIT_TIMEOUT`·`CLIENT_POLL_TIMEOUT`·`RESULT_NOT_AVAILABLE`·`INVALID_RESULT`·`UNKNOWN_STATUS`는 예제에서 만드는 로컬 오류로, 응답 모순이나 알 수 없는 상태에서 무한 반복하지 않습니다. 통신 오류도 재시도 없이 호출자에게 전달합니다.
298
+
299
+ `AUTH_REQUESTED` and `AUTH_WAITING` wait for the user's approval; `AUTH_COMPLETED`, `COLLECTING`, and `COLLECTED` keep polling until a result is available. Billing fields (`charged`, `success`) do not indicate completion. Apply `expiresAt` only while awaiting approval, then use the overall waiting limit. The example's 10-minute limit is a client policy; configure the SDK's `timeoutMs` separately to bound each in-flight call. `AUTH_WAIT_TIMEOUT`, `CLIENT_POLL_TIMEOUT`, `RESULT_NOT_AVAILABLE`, `INVALID_RESULT`, and `UNKNOWN_STATUS` are local example errors that prevent endless polling on inconsistent responses or unknown states. Transport errors propagate without retries.
300
+
301
+ <!-- simple-auth-usage:start -->
244
302
  ```js
245
303
  const accepted = await apick.requestEmployment({
246
304
  name: '홍길동',
247
305
  birthDate: '19900101',
248
306
  phone: '01011112222',
249
- authProvider: 'kakao', // AUTH_PROVIDERS 참고 / see AUTH_PROVIDERS
307
+ authProvider: 'kakao',
250
308
  insuranceYears: 3
251
309
  });
252
- const transactionId = accepted.data.transactionId;
253
-
254
- let result;
255
- do {
256
- await new Promise(resolve => setTimeout(resolve, 3000));
257
- result = await apick.getEmployment(transactionId);
258
- } while (result.data.status === 'AUTH_WAITING' || result.data.status === 'COLLECTING');
259
310
 
260
- if (result.data.status === 'SUCCESS' || result.data.status === 'PARTIAL_SUCCESS') {
261
- console.log(result.data.result.employment);
262
- } else {
263
- console.log(result.data.errorCode); // AUTH_EXPIRED | AUTH_REJECTED | COLLECT_FAILED
311
+ try {
312
+ const result = await pollDataResult(id => apick.getEmployment(id), accepted);
313
+ if (result.data.status === 'PARTIAL_SUCCESS') console.warn(result.data.sources);
314
+ console.log(result.data.result);
315
+ } catch (error) {
316
+ if ((error.serviceCode || error.code) === 'RESULT_EXPIRED') {
317
+ console.error('결과 보관 기간 만료: 사용자 확인 후 새 인증을 요청하세요.');
318
+ } else {
319
+ throw error;
320
+ }
264
321
  }
265
322
  ```
323
+ <!-- simple-auth-usage:end -->
324
+
325
+ 근거: [APICK 개발가이드](https://apick.app/dev_guide/data_health_checkup) · [MCP 3.5.0 상태 계약](https://github.com/lead788/apick-mcp/blob/a803abcb81d07377c85f49bb0b670baf0c17ed04/TOOLS.md)
266
326
 
267
327
  지원 간편인증 방식(`authProvider`) 13종은 `AUTH_PROVIDERS`로 제공됩니다: `kakao`, `naver`, `toss`, `pass`, `samsung`, `kb`, `shinhan`, `hana`, `woori`, `ibk`, `nh`, `kakaobank`, `banksalad`.
268
328
  The 13 supported `authProvider` values are exported as `AUTH_PROVIDERS`: `kakao`, `naver`, `toss`, `pass`, `samsung`, `kb`, `shinhan`, `hana`, `woori`, `ibk`, `nh`, `kakaobank`, `banksalad`.
@@ -277,6 +337,8 @@ Acceptance is billed at a flat rate; the result is billed only on its first succ
277
337
  | 국민연금 가입내역 / NPS join history | `requestNpsJoinHistory` / `getNpsJoinHistory` | `from`, `to` (`YYYY-MM`) |
278
338
  | 운전면허 조회 / Driver's license | `requestDrivingLicense` / `getDrivingLicense` | — |
279
339
  | 국가 건강검진 결과 / Health checkup | `requestHealthCheckup` / `getHealthCheckup` | — |
340
+ | 현금영수증 소득공제 내역 / Cash receipt deductions | `requestCashReceiptDeduction` / `getCashReceiptDeduction` | `incomeYears` (1–3) |
341
+ | 국세 신고내역 조회 / Tax return history | `requestTaxReturnHistory` / `getTaxReturnHistory` | `years` (1–10) |
280
342
 
281
343
  ## 오류 처리 / Error handling
282
344
 
package/docs/guide.en.md CHANGED
@@ -94,6 +94,13 @@ TTS supports 16 voice IDs. Use `TTS_VOICE_IDS` and the developer guide for the c
94
94
  const screenshot = await client.screenshot('https://example.com');
95
95
  await screenshot.save('./example.jpeg');
96
96
 
97
+ // Public YouTube videos: a video URL or the 11-character video ID
98
+ const video = await client.youtubeMetadata('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
99
+ const tracks = await client.youtubeSubtitleList(video.data.video_id);
100
+ const subtitle = await client.youtubeSubtitle(video.data.video_id, 'en', { format: 'srt' });
101
+ await subtitle.save('./' + subtitle.filename);
102
+ await (await client.youtubeThumbnail(video.data.video_id)).save('./thumbnail.jpg');
103
+
97
104
  const created = await client.createTtsJob('오늘의 이야기를 시작합니다.', { voiceId: 'v2_ann_m_30s_01' });
98
105
  const jobId = created.data.job_id;
99
106
  let job = await client.getTtsJob(jobId);
@@ -187,8 +194,56 @@ The document-specific methods are `maskResidenceCard`, `maskPassport`, `maskIdCa
187
194
 
188
195
  ## Simple-auth data lookups
189
196
 
190
- Employment, income, pension, driver's license, and health checkup lookups require the user's own simple-auth verification, so the call is split into acceptance (`request*`) and result polling (`get*`).
197
+ Employment, income, pension, driver's license, health checkup, cash receipt deduction, and tax return history lookups require the user's own simple-auth verification, so the call is split into acceptance (`request*`) and result polling (`get*`).
198
+
199
+ Use this helper for all seven products. Poll sequentially with the same `transactionId`, waiting 5→10→20→30 seconds and then keeping the 30-second interval. Return the result immediately when `resultAvailable === true`. `SUCCESS` means full success; `PARTIAL_SUCCESS` means partial success, so inspect `sources` for missing or failed items. `AUTH_REJECTED`, `AUTH_EXPIRED`, and `FAILED` are terminal failures; `errorCode: 'RESULT_EXPIRED'` means the retained result has expired. Never resubmit automatically after failure or expiry.
200
+
201
+ <!-- simple-auth-polling:start -->
202
+ ```js
203
+ async function pollDataResult(getResult, accepted, { timeoutMs = 600_000 } = {}) {
204
+ const pending = new Set([
205
+ 'AUTH_REQUESTED', 'AUTH_WAITING', 'AUTH_COMPLETED', 'COLLECTING', 'COLLECTED'
206
+ ]);
207
+ const failed = new Set(['AUTH_REJECTED', 'AUTH_EXPIRED', 'FAILED']);
208
+ const completed = new Set(['SUCCESS', 'PARTIAL_SUCCESS']);
209
+ const delays = [5_000, 10_000, 20_000, 30_000];
210
+ const deadline = Date.now() + timeoutMs;
211
+ const transactionId = accepted.data.transactionId;
212
+ const stop = code => { throw Object.assign(new Error(code), { code }); };
213
+ let response = accepted;
214
+ let attempt = 0;
215
+
216
+ for (;;) {
217
+ const data = response.data;
218
+ if (data.errorCode === 'RESULT_EXPIRED') stop('RESULT_EXPIRED');
219
+ if (failed.has(data.status)) stop(data.errorCode || data.status);
220
+ if (data.resultAvailable === true) {
221
+ if (data.result == null) stop('INVALID_RESULT');
222
+ return response;
223
+ }
224
+ if (completed.has(data.status)) stop('RESULT_NOT_AVAILABLE');
225
+ if (!pending.has(data.status)) stop('UNKNOWN_STATUS');
226
+
227
+ const awaitingApproval = ['AUTH_REQUESTED', 'AUTH_WAITING'].includes(data.status);
228
+ const authDeadline = Date.parse(data.expiresAt || accepted.data.expiresAt);
229
+ const limit = awaitingApproval && Number.isFinite(authDeadline)
230
+ ? Math.min(deadline, authDeadline) : deadline;
231
+ const timeoutCode = awaitingApproval && limit === authDeadline
232
+ ? 'AUTH_WAIT_TIMEOUT' : 'CLIENT_POLL_TIMEOUT';
233
+ const remaining = limit - Date.now();
234
+ if (remaining <= 0) stop(timeoutCode);
235
+ await new Promise(resolve => setTimeout(resolve,
236
+ Math.min(delays[Math.min(attempt++, delays.length - 1)], remaining)));
237
+ if (Date.now() >= limit) stop(timeoutCode);
238
+ response = await getResult(transactionId);
239
+ }
240
+ }
241
+ ```
242
+ <!-- simple-auth-polling:end -->
243
+
244
+ `AUTH_REQUESTED` and `AUTH_WAITING` wait for the user's approval; `AUTH_COMPLETED`, `COLLECTING`, and `COLLECTED` keep polling until a result is available. Billing fields (`charged`, `success`) do not indicate completion. Apply `expiresAt` only while awaiting approval, then use the overall waiting limit. The example's 10-minute limit is a client policy; configure the SDK's `timeoutMs` separately to bound each in-flight call. `AUTH_WAIT_TIMEOUT`, `CLIENT_POLL_TIMEOUT`, `RESULT_NOT_AVAILABLE`, `INVALID_RESULT`, and `UNKNOWN_STATUS` are local example errors that prevent endless polling on inconsistent responses or unknown states. Transport errors propagate without retries.
191
245
 
246
+ <!-- simple-auth-usage:start -->
192
247
  ```js
193
248
  const accepted = await client.requestDrivingLicense({
194
249
  name: 'Hong Gildong',
@@ -197,14 +252,23 @@ const accepted = await client.requestDrivingLicense({
197
252
  authProvider: 'kakao'
198
253
  });
199
254
 
200
- let result;
201
- do {
202
- await new Promise(resolve => setTimeout(resolve, 3000));
203
- result = await client.getDrivingLicense(accepted.data.transactionId);
204
- } while (result.data.status === 'AUTH_WAITING' || result.data.status === 'COLLECTING');
255
+ try {
256
+ const result = await pollDataResult(id => client.getDrivingLicense(id), accepted);
257
+ if (result.data.status === 'PARTIAL_SUCCESS') console.warn(result.data.sources);
258
+ console.log(result.data.result);
259
+ } catch (error) {
260
+ if ((error.serviceCode || error.code) === 'RESULT_EXPIRED') {
261
+ console.error('Result expired. Ask the user before starting a new authentication request.');
262
+ } else {
263
+ throw error;
264
+ }
265
+ }
205
266
  ```
267
+ <!-- simple-auth-usage:end -->
268
+
269
+ Sources: [APICK development guide](https://apick.app/dev_guide/data_health_checkup) · [MCP 3.5.0 status contract](https://github.com/lead788/apick-mcp/blob/a803abcb81d07377c85f49bb0b670baf0c17ed04/TOOLS.md)
206
270
 
207
- `authProvider` is one of the 13 values in `AUTH_PROVIDERS` (kakao, naver, toss, pass, samsung, kb, shinhan, hana, woori, ibk, nh, kakaobank, banksalad). Acceptance is billed at a flat rate; the result is billed only on its first return and free to re-poll afterward. `requestEmployment` takes an optional `insuranceYears` (1-3), `requestPersonalIncome` takes `incomeYears` (1-5), and `requestNpsJoinHistory` takes optional `from`/`to` (`YYYY-MM`). The remaining products are `requestDrivingLicense` and `requestHealthCheckup`.
271
+ `authProvider` is one of the 13 values in `AUTH_PROVIDERS` (kakao, naver, toss, pass, samsung, kb, shinhan, hana, woori, ibk, nh, kakaobank, banksalad). Acceptance is billed at a flat rate; the result is billed only on its first return and free to re-poll afterward. `requestEmployment` takes an optional `insuranceYears` (1-3), `requestPersonalIncome` takes `incomeYears` (1-5), and `requestNpsJoinHistory` takes optional `from`/`to` (`YYYY-MM`). `requestCashReceiptDeduction` takes optional `incomeYears` (1-3) and `requestTaxReturnHistory` takes optional `years` (1-10). The remaining products are `requestDrivingLicense` and `requestHealthCheckup`.
208
272
 
209
273
  ## Errors and retries
210
274
 
package/docs/guide.ko.md CHANGED
@@ -94,6 +94,13 @@ TTS는 16개 목소리 ID를 지원합니다. 정확한 목록은 `TTS_VOICE_IDS
94
94
  const screenshot = await client.screenshot('https://example.com');
95
95
  await screenshot.save('./example.jpeg');
96
96
 
97
+ // 유튜브 공개 영상: 영상 주소 또는 11자리 영상 ID
98
+ const video = await client.youtubeMetadata('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
99
+ const tracks = await client.youtubeSubtitleList(video.data.video_id);
100
+ const subtitle = await client.youtubeSubtitle(video.data.video_id, 'en', { format: 'srt' });
101
+ await subtitle.save('./' + subtitle.filename);
102
+ await (await client.youtubeThumbnail(video.data.video_id)).save('./thumbnail.jpg');
103
+
97
104
  const created = await client.createTtsJob('오늘의 이야기를 시작합니다.', { voiceId: 'v2_ann_m_30s_01' });
98
105
  const jobId = created.data.job_id;
99
106
  let job = await client.getTtsJob(jobId);
@@ -189,8 +196,56 @@ console.log(result.data.result.fields);
189
196
 
190
197
  ## 간편인증 데이터 조회
191
198
 
192
- 재직·소득·연금·면허·건강검진 조회는 본인 간편인증이 필요해 접수(`request*`)와 결과 조회(`get*`)가 나뉩니다.
199
+ 재직·소득·연금·면허·건강검진·현금영수증·국세 신고내역 조회는 본인 간편인증이 필요해 접수(`request*`)와 결과 조회(`get*`)가 나뉩니다.
200
+
201
+ 아래 함수는 7종 모두에 공통으로 사용합니다. 같은 `transactionId`로 순차 조회하며 5→10→20→30초 간격으로 늘린 뒤 30초를 유지합니다. `resultAvailable === true`이면 즉시 결과를 반환합니다. `SUCCESS`는 전체 성공, `PARTIAL_SUCCESS`는 부분 성공이므로 `sources`에서 누락·실패 항목을 확인하세요. `AUTH_REJECTED`·`AUTH_EXPIRED`·`FAILED`는 실패 종료이며, `errorCode: 'RESULT_EXPIRED'`는 결과 보관 기간 만료입니다. 실패·만료 시 자동으로 재접수하지 않습니다.
202
+
203
+ <!-- simple-auth-polling:start -->
204
+ ```js
205
+ async function pollDataResult(getResult, accepted, { timeoutMs = 600_000 } = {}) {
206
+ const pending = new Set([
207
+ 'AUTH_REQUESTED', 'AUTH_WAITING', 'AUTH_COMPLETED', 'COLLECTING', 'COLLECTED'
208
+ ]);
209
+ const failed = new Set(['AUTH_REJECTED', 'AUTH_EXPIRED', 'FAILED']);
210
+ const completed = new Set(['SUCCESS', 'PARTIAL_SUCCESS']);
211
+ const delays = [5_000, 10_000, 20_000, 30_000];
212
+ const deadline = Date.now() + timeoutMs;
213
+ const transactionId = accepted.data.transactionId;
214
+ const stop = code => { throw Object.assign(new Error(code), { code }); };
215
+ let response = accepted;
216
+ let attempt = 0;
217
+
218
+ for (;;) {
219
+ const data = response.data;
220
+ if (data.errorCode === 'RESULT_EXPIRED') stop('RESULT_EXPIRED');
221
+ if (failed.has(data.status)) stop(data.errorCode || data.status);
222
+ if (data.resultAvailable === true) {
223
+ if (data.result == null) stop('INVALID_RESULT');
224
+ return response;
225
+ }
226
+ if (completed.has(data.status)) stop('RESULT_NOT_AVAILABLE');
227
+ if (!pending.has(data.status)) stop('UNKNOWN_STATUS');
228
+
229
+ const awaitingApproval = ['AUTH_REQUESTED', 'AUTH_WAITING'].includes(data.status);
230
+ const authDeadline = Date.parse(data.expiresAt || accepted.data.expiresAt);
231
+ const limit = awaitingApproval && Number.isFinite(authDeadline)
232
+ ? Math.min(deadline, authDeadline) : deadline;
233
+ const timeoutCode = awaitingApproval && limit === authDeadline
234
+ ? 'AUTH_WAIT_TIMEOUT' : 'CLIENT_POLL_TIMEOUT';
235
+ const remaining = limit - Date.now();
236
+ if (remaining <= 0) stop(timeoutCode);
237
+ await new Promise(resolve => setTimeout(resolve,
238
+ Math.min(delays[Math.min(attempt++, delays.length - 1)], remaining)));
239
+ if (Date.now() >= limit) stop(timeoutCode);
240
+ response = await getResult(transactionId);
241
+ }
242
+ }
243
+ ```
244
+ <!-- simple-auth-polling:end -->
245
+
246
+ `AUTH_REQUESTED`·`AUTH_WAITING`은 사용자 승인을 기다리고, `AUTH_COMPLETED`·`COLLECTING`·`COLLECTED`는 결과가 준비될 때까지 계속 조회합니다. 과금 여부(`charged`, `success`)는 완료 기준이 아닙니다. 인증 대기에만 `expiresAt`을 적용하고 승인 후에는 전체 대기 한도를 적용합니다. 예제의 10분 한도는 클라이언트 정책이며, 진행 중인 호출의 제한 시간은 SDK의 `timeoutMs`로 별도 설정하세요. `AUTH_WAIT_TIMEOUT`·`CLIENT_POLL_TIMEOUT`·`RESULT_NOT_AVAILABLE`·`INVALID_RESULT`·`UNKNOWN_STATUS`는 예제에서 만드는 로컬 오류로, 응답 모순이나 알 수 없는 상태에서 무한 반복하지 않습니다. 통신 오류도 재시도 없이 호출자에게 전달합니다.
193
247
 
248
+ <!-- simple-auth-usage:start -->
194
249
  ```js
195
250
  const accepted = await client.requestDrivingLicense({
196
251
  name: '홍길동',
@@ -199,14 +254,23 @@ const accepted = await client.requestDrivingLicense({
199
254
  authProvider: 'kakao'
200
255
  });
201
256
 
202
- let result;
203
- do {
204
- await new Promise(resolve => setTimeout(resolve, 3000));
205
- result = await client.getDrivingLicense(accepted.data.transactionId);
206
- } while (result.data.status === 'AUTH_WAITING' || result.data.status === 'COLLECTING');
257
+ try {
258
+ const result = await pollDataResult(id => client.getDrivingLicense(id), accepted);
259
+ if (result.data.status === 'PARTIAL_SUCCESS') console.warn(result.data.sources);
260
+ console.log(result.data.result);
261
+ } catch (error) {
262
+ if ((error.serviceCode || error.code) === 'RESULT_EXPIRED') {
263
+ console.error('결과 보관 기간 만료: 사용자 확인 후 새 인증을 요청하세요.');
264
+ } else {
265
+ throw error;
266
+ }
267
+ }
207
268
  ```
269
+ <!-- simple-auth-usage:end -->
270
+
271
+ 근거: [APICK 개발가이드](https://apick.app/dev_guide/data_health_checkup) · [MCP 3.5.0 상태 계약](https://github.com/lead788/apick-mcp/blob/a803abcb81d07377c85f49bb0b670baf0c17ed04/TOOLS.md)
208
272
 
209
- `authProvider`는 `AUTH_PROVIDERS`(13종: kakao, naver, toss, pass, samsung, kb, shinhan, hana, woori, ibk, nh, kakaobank, banksalad) 중 하나입니다. 접수는 정액 과금, 결과는 최초 반환에서만 과금되며 재조회는 무료입니다. `requestEmployment`는 `insuranceYears`(1~3), `requestPersonalIncome`은 `incomeYears`(1~5), `requestNpsJoinHistory`는 `from`/`to`(`YYYY-MM`) 선택 입력을 받습니다. 나머지 상품은 `requestDrivingLicense`, `requestHealthCheckup`입니다.
273
+ `authProvider`는 `AUTH_PROVIDERS`(13종: kakao, naver, toss, pass, samsung, kb, shinhan, hana, woori, ibk, nh, kakaobank, banksalad) 중 하나입니다. 접수는 정액 과금, 결과는 최초 반환에서만 과금되며 재조회는 무료입니다. `requestEmployment`는 `insuranceYears`(1~3), `requestPersonalIncome`은 `incomeYears`(1~5), `requestNpsJoinHistory`는 `from`/`to`(`YYYY-MM`) 선택 입력을 받습니다. `requestCashReceiptDeduction`은 `incomeYears`(1~3), `requestTaxReturnHistory`는 `years`(1~10) 선택 입력을 받습니다. 나머지 상품은 `requestDrivingLicense`, `requestHealthCheckup`입니다.
210
274
 
211
275
  ## 오류와 재시도
212
276
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apick-api",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Official zero-dependency Node.js client for APICK data, AI, and image APIs. 에이픽 데이터·AI·이미지 API 공식 Node.js SDK.",
5
5
  "type": "module",
6
6
  "main": "./src/index.cjs",
package/src/index.cjs CHANGED
@@ -36,6 +36,10 @@ const SERVICE_DEFINITIONS = Object.freeze({
36
36
  googleSearch: { endpoint: '/rest/google_search', timeoutMs: 35_000, output: 'json' },
37
37
  googleImageSearch: { endpoint: '/rest/google_image_search', timeoutMs: 35_000, output: 'json' },
38
38
  screenshot: { endpoint: '/rest/url_screenshot', timeoutMs: 75_000, output: 'binary', filename: 'screenshot.jpeg' },
39
+ youtubeMetadata: { endpoint: '/rest/youtube_metadata', timeoutMs: 60_000, output: 'json' },
40
+ youtubeThumbnail: { endpoint: '/rest/youtube_thumbnail', timeoutMs: 60_000, output: 'binary', filename: 'thumbnail.jpg' },
41
+ youtubeSubtitleList: { endpoint: '/rest/youtube_subtitle_list', timeoutMs: 60_000, output: 'json' },
42
+ youtubeSubtitle: { endpoint: '/rest/youtube_subtitle', timeoutMs: 60_000, output: 'binary', filename: 'subtitle.vtt' },
39
43
  createTtsJob: { endpoint: '/rest/tts/jobs', timeoutMs: 35_000, output: 'json' },
40
44
  createVideoJob: { endpoint: '/rest/seedance/jobs', timeoutMs: 60_000, output: 'json' },
41
45
  htmlToPdf: { endpoint: '/rest/html_to_pdf', timeoutMs: 25_000, output: 'binary', filename: 'document.pdf' },
@@ -47,7 +51,9 @@ const SERVICE_DEFINITIONS = Object.freeze({
47
51
  requestPersonalIncome: { endpoint: '/rest/req_personal_income', timeoutMs: 35_000, output: 'json' },
48
52
  requestNpsJoinHistory: { endpoint: '/rest/req_nps_join_history', timeoutMs: 35_000, output: 'json' },
49
53
  requestDrivingLicense: { endpoint: '/rest/req_driving_license', timeoutMs: 35_000, output: 'json' },
50
- requestHealthCheckup: { endpoint: '/rest/req_health_checkup', timeoutMs: 35_000, output: 'json' }
54
+ requestHealthCheckup: { endpoint: '/rest/req_health_checkup', timeoutMs: 35_000, output: 'json' },
55
+ requestCashReceiptDeduction: { endpoint: '/rest/req_cash_receipt_deduction', timeoutMs: 35_000, output: 'json' },
56
+ requestTaxReturnHistory: { endpoint: '/rest/req_tax_return_history', timeoutMs: 35_000, output: 'json' }
51
57
  });
52
58
 
53
59
  const SERVICES = Object.freeze(Object.fromEntries(
@@ -524,6 +530,33 @@ class ApickClient {
524
530
  return this._call('screenshot', { url: normalizeUrl(url) });
525
531
  }
526
532
 
533
+ // 유튜브 공개 영상. url 은 영상 주소(watch·youtu.be·shorts) 또는 11자리 영상 ID를 받는다.
534
+ youtubeMetadata(url) {
535
+ return this._call('youtubeMetadata', { url: requiredString('url', url, 2048) });
536
+ }
537
+
538
+ youtubeThumbnail(url) {
539
+ return this._call('youtubeThumbnail', { url: requiredString('url', url, 2048) });
540
+ }
541
+
542
+ youtubeSubtitleList(url) {
543
+ return this._call('youtubeSubtitleList', { url: requiredString('url', url, 2048) });
544
+ }
545
+
546
+ youtubeSubtitle(url, lang, options) {
547
+ const config = options || {};
548
+ const payload = { url: requiredString('url', url, 2048), lang: requiredString('lang', lang, 32) };
549
+ if (config.format !== undefined) {
550
+ if (!['vtt', 'srt', 'txt'].includes(config.format)) throw new RangeError('format must be vtt, srt or txt.');
551
+ payload.format = config.format;
552
+ }
553
+ if (config.type !== undefined) {
554
+ if (!['any', 'manual', 'auto'].includes(config.type)) throw new RangeError('type must be any, manual or auto.');
555
+ payload.type = config.type;
556
+ }
557
+ return this._call('youtubeSubtitle', payload, null, { filename: 'subtitle.' + (payload.format || 'vtt') });
558
+ }
559
+
527
560
  async createVideoJob(model, prompt, options) {
528
561
  if (!['seedance', 'veo', 'kling'].includes(model)) throw new RangeError('model must be seedance, veo or kling.');
529
562
  const config = options || {};
@@ -792,6 +825,28 @@ class ApickClient {
792
825
  getHealthCheckup(transactionId) {
793
826
  return this._call('requestHealthCheckup', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_health_checkup' });
794
827
  }
828
+
829
+ requestCashReceiptDeduction(input) {
830
+ const payload = dataRequestInput(input);
831
+ const incomeYears = optionalRangeInteger('incomeYears', (input || {}).incomeYears, 1, 3);
832
+ if (incomeYears !== undefined) payload.incomeYears = incomeYears;
833
+ return this._call('requestCashReceiptDeduction', payload);
834
+ }
835
+
836
+ getCashReceiptDeduction(transactionId) {
837
+ return this._call('requestCashReceiptDeduction', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_cash_receipt_deduction' });
838
+ }
839
+
840
+ requestTaxReturnHistory(input) {
841
+ const payload = dataRequestInput(input);
842
+ const years = optionalRangeInteger('years', (input || {}).years, 1, 10);
843
+ if (years !== undefined) payload.years = years;
844
+ return this._call('requestTaxReturnHistory', payload);
845
+ }
846
+
847
+ getTaxReturnHistory(transactionId) {
848
+ return this._call('requestTaxReturnHistory', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_tax_return_history' });
849
+ }
795
850
  }
796
851
 
797
852
  module.exports = {
package/src/index.d.ts CHANGED
@@ -61,6 +61,8 @@ export interface RequestPersonalIncomeInput extends DataRequestInput { incomeYea
61
61
  export interface RequestNpsJoinHistoryInput extends DataRequestInput { from?: string; to?: string; }
62
62
  export interface RequestDrivingLicenseInput extends DataRequestInput {}
63
63
  export interface RequestHealthCheckupInput extends DataRequestInput {}
64
+ export interface RequestCashReceiptDeductionInput extends DataRequestInput { incomeYears?: number; }
65
+ export interface RequestTaxReturnHistoryInput extends DataRequestInput { years?: number; }
64
66
 
65
67
  export type DataRequestStatus =
66
68
  | 'AUTH_REQUESTED' | 'AUTH_WAITING' | 'AUTH_COMPLETED' | 'AUTH_REJECTED' | 'AUTH_EXPIRED'
@@ -159,6 +161,44 @@ export interface HealthCheckupEntry { 검진연도: string; 검진종류: string
159
161
  export interface HealthCheckupInfo { 이름: string; 건수: number; 검진내역: HealthCheckupEntry[]; }
160
162
  export interface HealthCheckupResultPayload { healthCheckup: HealthCheckupInfo; }
161
163
 
164
+ export interface CashReceiptTotals { 건수: number; 사용금액: number; 소득공제건수: number; 소득공제금액: number; }
165
+ export interface CashReceiptEntry {
166
+ 거래일시: string; 가맹점: string; 금액: number; 승인번호: string;
167
+ 거래구분: string; 거래상태: string; 소득공제대상: boolean; 소득공제반영: boolean;
168
+ }
169
+ export interface CashReceiptYear { 귀속연도: string; 합계: CashReceiptTotals; 사용내역: CashReceiptEntry[]; }
170
+ export interface CashReceiptDeductionInfo { 조회연도: string[]; 전체합계: CashReceiptTotals; 연도별: CashReceiptYear[]; }
171
+ export interface CashReceiptDeductionResultPayload { cashReceiptDeduction: CashReceiptDeductionInfo; }
172
+
173
+ export interface TaxReturnEntry {
174
+ 신고일: string; 과세기간: string; 신고서: string; 신고구분: string; 신고상세: string;
175
+ 세목: string; 작성방법: string; 납부년월: string; 납부금액: number; 고지금액: number;
176
+ }
177
+ export interface TaxReturnHistoryInfo {
178
+ 조회기간: { 시작: string; 끝: string };
179
+ 합계: { 건수: number; 납부금액: number; 고지금액: number };
180
+ 신고내역: TaxReturnEntry[];
181
+ }
182
+ export interface TaxReturnHistoryResultPayload { taxReturnHistory: TaxReturnHistoryInfo; }
183
+
184
+ export interface YoutubeThumbnail { url: string; width: number; height: number; }
185
+ export interface YoutubeChapter { title: string; start_time: number | null; end_time: number | null; }
186
+ export interface YoutubeMetadata {
187
+ video_id: string; url: string; title: string | null; description: string;
188
+ channel: { id: string | null; name: string | null; url: string | null; handle: string | null; follower_count: number | null; is_verified: boolean };
189
+ upload_date: string | null; duration: number | null; view_count: number | null; like_count: number | null; comment_count: number | null;
190
+ categories: string[]; tags: string[]; language: string | null; live_status: string | null; availability: string | null;
191
+ age_limit: number | null; chapters: YoutubeChapter[]; thumbnail: string | null; thumbnails: YoutubeThumbnail[];
192
+ subtitle_languages: string[]; automatic_caption_count: number;
193
+ }
194
+ export interface YoutubeSubtitleTrack { lang: string; name: string | null; auto: boolean; formats: string[]; translated?: boolean; }
195
+ export interface YoutubeSubtitleList {
196
+ video_id: string; title: string | null; original_language: string | null;
197
+ subtitle_count: number; automatic_caption_count: number;
198
+ subtitles: YoutubeSubtitleTrack[]; automatic_captions: YoutubeSubtitleTrack[];
199
+ }
200
+ export interface YoutubeSubtitleOptions { format?: 'vtt' | 'srt' | 'txt'; type?: 'any' | 'manual' | 'auto'; }
201
+
162
202
  export const TTS_VOICE_IDS: readonly [
163
203
  'v2_ann_m_30s_01', 'v2_ann_m_30s_02', 'v2_ann_m_30s_04', 'v2_ann_m_30s_05', 'v2_ann_f_30s_01', 'v2_ann_f_30s_02', 'v2_ann_f_30s_03', 'v2_ann_f_30s_04', 'v2_ann_f_30s_05', 'v2_m_teen_01', 'v2_m_young_01', 'v2_m_mid_01', 'v2_m_senior_01', 'v2_f_teen_01', 'v2_f_young_01', 'v2_f_senior_01'
164
204
  ];
@@ -244,6 +284,10 @@ export class ApickClient {
244
284
  googleSearch(keyword: string, options?: { page?: number }): Promise<ApickResult>;
245
285
  googleImageSearch(keyword: string, options?: { page?: number }): Promise<ApickResult>;
246
286
  screenshot(url: string): Promise<ApickBinaryResult>;
287
+ youtubeMetadata(url: string): Promise<ApickResult<YoutubeMetadata>>;
288
+ youtubeThumbnail(url: string): Promise<ApickBinaryResult>;
289
+ youtubeSubtitleList(url: string): Promise<ApickResult<YoutubeSubtitleList>>;
290
+ youtubeSubtitle(url: string, lang: string, options?: YoutubeSubtitleOptions): Promise<ApickBinaryResult>;
247
291
  createTtsJob(text: string, options?: { voiceId?: TtsVoiceId }): Promise<ApickResult<TtsJobData>>;
248
292
  getTtsJob(jobId: string): Promise<ApickResult<TtsJobData>>;
249
293
  cancelTtsJob(jobId: string): Promise<ApickResult<TtsJobData>>;
@@ -273,6 +317,10 @@ export class ApickClient {
273
317
  getDrivingLicense(transactionId: string): Promise<ApickResult<DataRequestResult<DrivingLicenseResultPayload>>>;
274
318
  requestHealthCheckup(input: RequestHealthCheckupInput): Promise<ApickResult<DataRequestAcceptedData>>;
275
319
  getHealthCheckup(transactionId: string): Promise<ApickResult<DataRequestResult<HealthCheckupResultPayload>>>;
320
+ requestCashReceiptDeduction(input: RequestCashReceiptDeductionInput): Promise<ApickResult<DataRequestAcceptedData>>;
321
+ getCashReceiptDeduction(transactionId: string): Promise<ApickResult<DataRequestResult<CashReceiptDeductionResultPayload>>>;
322
+ requestTaxReturnHistory(input: RequestTaxReturnHistoryInput): Promise<ApickResult<DataRequestAcceptedData>>;
323
+ getTaxReturnHistory(transactionId: string): Promise<ApickResult<DataRequestResult<TaxReturnHistoryResultPayload>>>;
276
324
  }
277
325
 
278
326
  export default ApickClient;