pal-crawl 1.6.0 → 1.7.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/README.md CHANGED
@@ -20,9 +20,15 @@
20
20
  - [Configuration](#configuration)
21
21
  - [Base Types](#base-types)
22
22
  - [Methods](#methods)
23
- - [get](#get)
24
- - [getContentHTML](#getcontenthtml--promisestring)
23
+ - [get](#get--promiseitable-data)
24
+ - [getContentHTML](#getcontenthtmlid-string--promisestring)
25
25
  - [getContent](#getcontentid-string--promiseicontentdata)
26
+ - [getDone](#getdone--promiseitable-data)
27
+ - [getDoneContentHTML](#getdonecontenthtmlid-string--promisestring)
28
+ - [getDoneContent](#getdonecontentid-string--promiseicontentdata)
29
+ - [search / searchDone](#searchquery-isearchquery--promiseisearchresult)
30
+ - [getPage / getDonePage](#getpagepageindex-number-pageunit-number--promiseitable-data)
31
+ - [getAllPages / getAllDonePages](#getallpagesquery-isearchquery-options-ibulkoptions--asyncgeneratorisearchresult)
26
32
  - [Examples](#examples)
27
33
  - [License](#license)
28
34
 
@@ -119,11 +125,57 @@ interface IContentData {
119
125
  }
120
126
  ```
121
127
 
128
+ `ISearchResult`는 검색/페이지 조회 결과를 나타내는 인터페이스입니다.
129
+
130
+ ```typescript
131
+ interface ISearchResult {
132
+ total: number; // 전체 건수
133
+ totalPages: number; // 전체 페이지 수
134
+ currentPage: number; // 현재 페이지
135
+ items: ITableData[]; // 현재 페이지 항목 목록
136
+ }
137
+ ```
138
+
139
+ `ISearchQuery`는 검색 필터 옵션을 나타내는 인터페이스입니다.
140
+
141
+ ```typescript
142
+ interface ISearchQuery {
143
+ pageIndex?: number; // 페이지 번호 (기본값: 1)
144
+ pageUnit?: number; // 페이지당 항목 수 (5~9, 20, 30, 50, 100; 기본값: 10)
145
+ committeeId?: string; // 소관위원회 ID (예: '9700006' = 법제사법위원회)
146
+ billName?: string; // 법안명 검색어
147
+ represent?: string; // 대표발의자 검색어
148
+ proposers?: string; // 공동발의자 검색어 (쉼표 구분 AND 조건)
149
+ ppslRsonMnCn?: string; // 제안이유 검색어
150
+ sortCol?:
151
+ | 'BILL_NO'
152
+ | 'BILL_NAME'
153
+ | 'CURR_COMMITTEE'
154
+ | 'OPN_CNT'
155
+ | 'LGSLT_PA_RG_DT';
156
+ sortGbn?: 'DESC' | 'ASC';
157
+ // 종료된 입법예고 전용:
158
+ fromAge?: number; // 시작 대수 (예: 21)
159
+ toAge?: number; // 종료 대수 (예: 22)
160
+ billNo?: string; // 의안번호 검색어
161
+ }
162
+ ```
163
+
164
+ `IBulkOptions`는 대량 수집 시 동작을 제어하는 인터페이스입니다.
165
+
166
+ ```typescript
167
+ interface IBulkOptions {
168
+ delayMs?: number; // 페이지 간 요청 지연 (밀리초, 기본값: 500)
169
+ concurrency?: number; // 동시 요청 수 (기본값: 1)
170
+ maxPages?: number; // 최대 수집 페이지 수 (기본값: 전체)
171
+ }
172
+ ```
173
+
122
174
  ---
123
175
 
124
176
  ## Methods
125
177
 
126
- ### get() => Promise<ITableData[]>
178
+ ### get() => Promise\<ITableData[]>
127
179
 
128
180
  `get` 메서드는 진행 중인 입법 예고 데이터를 가져옵니다.
129
181
 
@@ -140,7 +192,7 @@ console.log(table);
140
192
 
141
193
  ### getContentHTML(id: string) => Promise\<string>
142
194
 
143
- `getContentHTML` 메서드는 특정 의안 ID의 본문 페이지 HTML 원문을 가져옵니다.
195
+ `getContentHTML` 메서드는 진행 중인 입법 예고 중 특정 의안 ID의 본문 페이지 HTML 원문을 가져옵니다.
144
196
 
145
197
  ```typescript
146
198
  import { PalCrawl } from 'pal-crawl';
@@ -179,6 +231,171 @@ console.log(content);
179
231
  // }
180
232
  ```
181
233
 
234
+ ### getDone() => Promise\<ITableData[]>
235
+
236
+ `getDone` 메서드는 종료된 입법 예고 데이터를 가져옵니다.
237
+
238
+ 반환되는 각 항목에는 `contentId`가 포함되며, 이 값을 `getDoneContentHTML` 또는 `getDoneContent`에 전달해 본문 조회를 바로 수행할 수 있습니다.
239
+
240
+ ```typescript
241
+ import { PalCrawl } from 'pal-crawl';
242
+
243
+ const palCrawl = new PalCrawl();
244
+ const table = await palCrawl.getDone();
245
+
246
+ console.log(table);
247
+ ```
248
+
249
+ ### getDoneContentHTML(id: string) => Promise\<string>
250
+
251
+ `getDoneContentHTML` 메서드는 종료된 입법 예고 중 특정 의안 ID의 본문 페이지 HTML 원문을 가져옵니다.
252
+
253
+ ```typescript
254
+ import { PalCrawl } from 'pal-crawl';
255
+
256
+ const palCrawl = new PalCrawl();
257
+ const table = await palCrawl.getDone();
258
+
259
+ const id = table[0]?.contentId;
260
+ if (id) {
261
+ const contentHtml = await palCrawl.getDoneContentHTML(id);
262
+ console.log(contentHtml);
263
+ }
264
+ ```
265
+
266
+ ### getDoneContent(id: string) => Promise\<IContentData>
267
+
268
+ `getDoneContent` 메서드는 종료된 입법 예고 중 특정 의안 ID의 본문 페이지를 파싱해 JSON 객체(`IContentData`)로 반환합니다.
269
+
270
+ ```typescript
271
+ import { PalCrawl } from 'pal-crawl';
272
+
273
+ const palCrawl = new PalCrawl();
274
+ const content = await palCrawl.getDoneContent(
275
+ 'PRC_S2R6N0M3K2J3K1J5K3S8R3P4O3P5X6',
276
+ );
277
+
278
+ console.log(content);
279
+ // {
280
+ // title: '[2218503] 전자장치 부착 등에 관한 법률 일부개정법률안 (김기현의원 등 10인)',
281
+ // proposalReason: '...',
282
+ // billNumber: '2218503',
283
+ // proposer: '김기현의원 등 10인',
284
+ // ...
285
+ // }
286
+ ```
287
+
288
+ ---
289
+
290
+ ### search(query?: ISearchQuery) => Promise\<ISearchResult>
291
+
292
+ `search` 메서드는 진행 중인 입법 예고를 조건에 맞게 검색합니다. 검색 결과에는 총 건수, 페이지 정보, 항목 목록이 포함됩니다.
293
+
294
+ ```typescript
295
+ import { PalCrawl } from 'pal-crawl';
296
+
297
+ const palCrawl = new PalCrawl();
298
+
299
+ // 기본 조회 (1페이지 10건)
300
+ const result = await palCrawl.search();
301
+ console.log(result.total, result.totalPages, result.items);
302
+
303
+ // 법안명 필터 + 정렬
304
+ const filtered = await palCrawl.search({
305
+ billName: '도로교통법',
306
+ sortCol: 'BILL_NAME',
307
+ sortGbn: 'ASC',
308
+ pageUnit: 20,
309
+ });
310
+ ```
311
+
312
+ ### searchDone(query?: ISearchQuery) => Promise\<ISearchResult>
313
+
314
+ `searchDone` 메서드는 종료된 입법 예고를 조건에 맞게 검색합니다. `fromAge`, `toAge`, `billNo` 필터를 추가로 지원합니다.
315
+
316
+ ```typescript
317
+ import { PalCrawl } from 'pal-crawl';
318
+
319
+ const palCrawl = new PalCrawl();
320
+
321
+ // 22대 종료 입법예고만 조회
322
+ const result = await palCrawl.searchDone({ fromAge: 22, toAge: 22 });
323
+ console.log(result.total, result.items);
324
+ ```
325
+
326
+ ---
327
+
328
+ ### getPage(pageIndex: number, pageUnit?: number) => Promise\<ITableData[]>
329
+
330
+ `getPage` 메서드는 진행 중인 입법 예고에서 특정 페이지의 항목을 가져옵니다.
331
+
332
+ ```typescript
333
+ import { PalCrawl } from 'pal-crawl';
334
+
335
+ const palCrawl = new PalCrawl();
336
+
337
+ const page1 = await palCrawl.getPage(1); // 1페이지 (기본 10건)
338
+ const page2 = await palCrawl.getPage(2, 20); // 2페이지, 20건씩
339
+ ```
340
+
341
+ ### getDonePage(pageIndex: number, pageUnit?: number) => Promise\<ITableData[]>
342
+
343
+ `getDonePage` 메서드는 종료된 입법 예고에서 특정 페이지의 항목을 가져옵니다.
344
+
345
+ ```typescript
346
+ import { PalCrawl } from 'pal-crawl';
347
+
348
+ const palCrawl = new PalCrawl();
349
+ const page3 = await palCrawl.getDonePage(3);
350
+ ```
351
+
352
+ ---
353
+
354
+ ### getAllPages(query?: ISearchQuery, options?: IBulkOptions) => AsyncGenerator\<ISearchResult>
355
+
356
+ `getAllPages` 메서드는 진행 중인 입법 예고를 여러 페이지에 걸쳐 순차적으로 수집하는 비동기 제너레이터입니다. `for await...of` 루프로 한 페이지씩 처리할 수 있습니다.
357
+
358
+ - `delayMs` (기본값: 500): 페이지 요청 사이의 지연 시간(ms). 서버 부하를 줄이기 위해 설정하세요.
359
+ - `concurrency` (기본값: 1): 동시에 요청할 페이지 수.
360
+ - `maxPages` (기본값: 전체 페이지): 수집할 최대 페이지 수.
361
+
362
+ ```typescript
363
+ import { PalCrawl } from 'pal-crawl';
364
+
365
+ const palCrawl = new PalCrawl();
366
+
367
+ // 처음 5 페이지를 순서대로 수집
368
+ for await (const page of palCrawl.getAllPages(
369
+ {},
370
+ { maxPages: 5, delayMs: 300 },
371
+ )) {
372
+ console.log(
373
+ `페이지 ${page.currentPage}/${page.totalPages}: ${page.items.length}건`,
374
+ );
375
+ for (const item of page.items) {
376
+ console.log(item.subject);
377
+ }
378
+ }
379
+ ```
380
+
381
+ ### getAllDonePages(query?: ISearchQuery, options?: IBulkOptions) => AsyncGenerator\<ISearchResult>
382
+
383
+ `getAllDonePages`는 종료된 입법 예고에 대한 대량 수집 제너레이터입니다.
384
+
385
+ ```typescript
386
+ import { PalCrawl } from 'pal-crawl';
387
+
388
+ const palCrawl = new PalCrawl();
389
+
390
+ // 22대 종료 입법예고 전체 수집 (concurrency=2, 지연 200ms)
391
+ for await (const page of palCrawl.getAllDonePages(
392
+ { fromAge: 22, toAge: 22 },
393
+ { concurrency: 2, delayMs: 200 },
394
+ )) {
395
+ console.log(`${page.currentPage}/${page.totalPages}: ${page.items.length}건`);
396
+ }
397
+ ```
398
+
182
399
  ---
183
400
 
184
401
  ## Examples
@@ -243,6 +460,78 @@ const palCrawl = new PalCrawl(config);
243
460
  const data = await palCrawl.get();
244
461
  ```
245
462
 
463
+ ### 종료된 입법예고 목록에서 본문 가져오기
464
+
465
+ ```typescript
466
+ import { PalCrawl } from 'pal-crawl';
467
+
468
+ const palCrawl = new PalCrawl();
469
+ const table = await palCrawl.getDone();
470
+
471
+ const first = table.find((item) => item.contentId);
472
+ if (first?.contentId) {
473
+ const content = await palCrawl.getDoneContent(first.contentId);
474
+ console.log(content.title);
475
+ console.log(content.proposalReason);
476
+ console.log(content.billNumber);
477
+ console.log(content.noticePeriod);
478
+ }
479
+ ```
480
+
481
+ ### 검색 필터를 사용해 법안 조회하기
482
+
483
+ ```typescript
484
+ import { PalCrawl } from 'pal-crawl';
485
+
486
+ const palCrawl = new PalCrawl();
487
+
488
+ // 법안명 검색
489
+ const result = await palCrawl.search({ billName: '도로교통법', pageUnit: 20 });
490
+ console.log(`총 ${result.total}건 (${result.totalPages}페이지)`);
491
+ result.items.forEach((item) => console.log(item.subject));
492
+
493
+ // 22대 종료 입법예고에서 의안번호로 검색
494
+ const done = await palCrawl.searchDone({
495
+ fromAge: 22,
496
+ toAge: 22,
497
+ billNo: '2218',
498
+ });
499
+ done.items.forEach((item) => console.log(item.subject));
500
+ ```
501
+
502
+ ### 대량 수집 (getAllPages / getAllDonePages)
503
+
504
+ ```typescript
505
+ import { PalCrawl, type ITableData } from 'pal-crawl';
506
+
507
+ const palCrawl = new PalCrawl();
508
+ const allItems: ITableData[] = [];
509
+
510
+ // 진행 중인 입법예고 전체 페이지 수집 (500ms 지연)
511
+ for await (const page of palCrawl.getAllPages({}, { delayMs: 500 })) {
512
+ allItems.push(...page.items);
513
+ console.log(`수집 중: ${page.currentPage}/${page.totalPages} 페이지`);
514
+ }
515
+
516
+ console.log(`전체 ${allItems.length}건 수집 완료`);
517
+ ```
518
+
519
+ ```typescript
520
+ import { PalCrawl } from 'pal-crawl';
521
+
522
+ const palCrawl = new PalCrawl();
523
+
524
+ // 처음 10 페이지만, 동시 2개 요청으로 빠르게 수집
525
+ for await (const page of palCrawl.getAllDonePages(
526
+ { sortCol: 'LGSLT_PA_RG_DT', sortGbn: 'DESC' },
527
+ { maxPages: 10, concurrency: 2, delayMs: 200 },
528
+ )) {
529
+ console.log(
530
+ `페이지 ${page.currentPage}: ${page.items.map((i) => i.subject).join(', ')}`,
531
+ );
532
+ }
533
+ ```
534
+
246
535
  ### 에러 처리
247
536
 
248
537
  ```typescript
package/dist/config.d.ts CHANGED
@@ -2,5 +2,7 @@ export declare enum Config {
2
2
  DOMAIN = "https://pal.assembly.go.kr",
3
3
  LIST_URL = "/napal/lgsltpa/lgsltpaOngoing/list.do",
4
4
  CONTENT_URL = "/napal/lgsltpa/lgsltpaOngoing/view.do",
5
+ DONE_LIST_URL = "/napal/lgsltpa/lgsltpaDone/list.do",
6
+ DONE_CONTENT_URL = "/napal/lgsltpa/lgsltpaDone/view.do",
5
7
  UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
6
8
  }
package/dist/config.js CHANGED
@@ -6,6 +6,8 @@ var Config;
6
6
  Config["DOMAIN"] = "https://pal.assembly.go.kr";
7
7
  Config["LIST_URL"] = "/napal/lgsltpa/lgsltpaOngoing/list.do";
8
8
  Config["CONTENT_URL"] = "/napal/lgsltpa/lgsltpaOngoing/view.do";
9
+ Config["DONE_LIST_URL"] = "/napal/lgsltpa/lgsltpaDone/list.do";
10
+ Config["DONE_CONTENT_URL"] = "/napal/lgsltpa/lgsltpaDone/view.do";
9
11
  Config["UserAgent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
10
12
  })(Config || (exports.Config = Config = {}));
11
13
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAAA,IAAY,MAKX;AALD,WAAY,MAAM;IAChB,+CAAqC,CAAA;IACrC,4DAAkD,CAAA;IAClD,+DAAqD,CAAA;IACrD,uIAA6H,CAAA;AAC/H,CAAC,EALW,MAAM,sBAAN,MAAM,QAKjB"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAAA,IAAY,MAOX;AAPD,WAAY,MAAM;IAChB,+CAAqC,CAAA;IACrC,4DAAkD,CAAA;IAClD,+DAAqD,CAAA;IACrD,8DAAoD,CAAA;IACpD,iEAAuD,CAAA;IACvD,uIAA6H,CAAA;AAC/H,CAAC,EAPW,MAAM,sBAAN,MAAM,QAOjB"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { PalCrawl, type ITableData, type IAttachment, type IContentData, type PalCrawlConfig, } from './pal';
1
+ export { PalCrawl, type ITableData, type IAttachment, type IContentData, type ISearchResult, type ISearchQuery, type IBulkOptions, type PalCrawlConfig, } from './pal';
2
+ export { PalParser } from './parser';
2
3
  export { Config } from './config';
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Config = exports.PalCrawl = void 0;
3
+ exports.Config = exports.PalParser = exports.PalCrawl = void 0;
4
4
  var pal_1 = require("./pal");
5
5
  Object.defineProperty(exports, "PalCrawl", { enumerable: true, get: function () { return pal_1.PalCrawl; } });
6
+ var parser_1 = require("./parser");
7
+ Object.defineProperty(exports, "PalParser", { enumerable: true, get: function () { return parser_1.PalParser; } });
6
8
  var config_1 = require("./config");
7
9
  Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return config_1.Config; } });
8
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6BAMe;AALb,+FAAA,QAAQ,OAAA;AAMV,mCAAkC;AAAzB,gGAAA,MAAM,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6BASe;AARb,+FAAA,QAAQ,OAAA;AASV,mCAAqC;AAA5B,mGAAA,SAAS,OAAA;AAClB,mCAAkC;AAAzB,gGAAA,MAAM,OAAA"}
package/dist/pal.d.ts CHANGED
@@ -1,45 +1,62 @@
1
+ import { type ITableData, type IContentData, type ISearchResult } from './parser';
2
+ export type { IAttachment, ITableData, IContentData, ISearchResult, } from './parser';
1
3
  export interface PalCrawlConfig {
2
4
  userAgent?: string;
3
5
  timeout?: number;
4
6
  retryCount?: number;
5
7
  customHeaders?: Record<string, string>;
6
8
  }
7
- export interface IAttachment {
8
- pdfFile: string | null;
9
- hwpFile: string | null;
9
+ export interface ISearchQuery {
10
+ pageIndex?: number;
11
+ pageUnit?: number;
12
+ committeeId?: string;
13
+ billName?: string;
14
+ represent?: string;
15
+ proposers?: string;
16
+ ppslRsonMnCn?: string;
17
+ sortCol?: 'BILL_NO' | 'BILL_NAME' | 'CURR_COMMITTEE' | 'OPN_CNT' | 'LGSLT_PA_RG_DT';
18
+ sortGbn?: 'DESC' | 'ASC';
19
+ fromAge?: number;
20
+ toAge?: number;
21
+ billNo?: string;
10
22
  }
11
- export interface ITableData {
12
- num: number;
13
- subject: string;
14
- proposerCategory: string;
15
- committee: string;
16
- numComments: number;
17
- link: string;
18
- contentId: string | null;
19
- attachments: IAttachment;
20
- }
21
- export interface IContentData {
22
- title: string;
23
- proposalReason: string | null;
24
- billNumber: string | null;
25
- proposer: string | null;
26
- proposalDate: string | null;
27
- committee: string | null;
28
- referralDate: string | null;
29
- noticePeriod: string | null;
30
- proposalSession: string | null;
23
+ export interface IBulkOptions {
24
+ delayMs?: number;
25
+ concurrency?: number;
26
+ maxPages?: number;
31
27
  }
32
28
  export declare class PalCrawl {
33
29
  private readonly httpClient;
30
+ private readonly parser;
34
31
  constructor(config?: PalCrawlConfig);
32
+ private buildListUrl;
35
33
  getPalHTML(): Promise<string>;
36
- private extractContentId;
37
34
  parseTable(html: string): ITableData[];
38
- private normalizeText;
39
- private extractTableCellText;
40
- private parseBillInfoTable;
41
35
  parseContent(html: string): IContentData;
42
36
  get(): Promise<ITableData[]>;
43
37
  getContentHTML(id: string): Promise<string>;
44
38
  getContent(id: string): Promise<IContentData>;
39
+ getDoneHTML(): Promise<string>;
40
+ getDone(): Promise<ITableData[]>;
41
+ getDoneContentHTML(id: string): Promise<string>;
42
+ getDoneContent(id: string): Promise<IContentData>;
43
+ /** Fetch ongoing notices with optional filters and return parsed result with metadata. */
44
+ search(query?: ISearchQuery): Promise<ISearchResult>;
45
+ /** Fetch done notices with optional filters and return parsed result with metadata. */
46
+ searchDone(query?: ISearchQuery): Promise<ISearchResult>;
47
+ /** Fetch a specific page of ongoing notices. */
48
+ getPage(pageIndex: number, pageUnit?: number): Promise<ITableData[]>;
49
+ /** Fetch a specific page of done notices. */
50
+ getDonePage(pageIndex: number, pageUnit?: number): Promise<ITableData[]>;
51
+ private _getAllPagesImpl;
52
+ /**
53
+ * Async generator that yields every page of ongoing notices.
54
+ * Respects `IBulkOptions` for rate-limiting and concurrency.
55
+ */
56
+ getAllPages(query?: Omit<ISearchQuery, 'pageIndex'>, options?: IBulkOptions): AsyncGenerator<ISearchResult>;
57
+ /**
58
+ * Async generator that yields every page of done notices.
59
+ * Respects `IBulkOptions` for rate-limiting and concurrency.
60
+ */
61
+ getAllDonePages(query?: Omit<ISearchQuery, 'pageIndex'>, options?: IBulkOptions): AsyncGenerator<ISearchResult>;
45
62
  }