pal-crawl 2.2.1 → 2.3.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/AGENTS.md +17 -4
- package/README.md +424 -8
- package/dist/config.d.ts +23 -1
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -1
- package/dist/http-client.d.ts +10 -2
- package/dist/http-client.js +44 -7
- package/dist/http-client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/likms.d.ts +282 -0
- package/dist/likms.js +409 -21
- package/dist/likms.js.map +1 -1
- package/dist/nsm-compat.d.ts +198 -0
- package/dist/nsm-compat.js +342 -0
- package/dist/nsm-compat.js.map +1 -0
- package/dist/pal.d.ts +7 -3
- package/dist/pal.js.map +1 -1
- package/dist/parser.d.ts +159 -0
- package/dist/parser.js +301 -1
- package/dist/parser.js.map +1 -1
- package/package.json +6 -1
- package/.freebuff/project-id +0 -1
- package/dist/likms.test.d.ts +0 -1
- package/dist/likms.test.js +0 -313
- package/dist/likms.test.js.map +0 -1
- package/dist/pal.test.d.ts +0 -1
- package/dist/pal.test.js +0 -1276
- package/dist/pal.test.js.map +0 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { IBulkOptions, INsmBillDetail, INsmBillItem, INsmSearchQuery, INsmSearchResult } from './pal';
|
|
2
|
+
import { LikmsStateCrawler, type LikmsStateCrawlerConfig } from './likms';
|
|
3
|
+
/**
|
|
4
|
+
* 국민참여입법센터(`NsmLmSts`)와 동일한 '계류 법안 목록 조회' 인터페이스(공통 포트).
|
|
5
|
+
*
|
|
6
|
+
* - `NsmLmSts`가 구조적으로 이 포트를 만족한다.
|
|
7
|
+
* - `LikmsNsmAdapter`가 이 포트를 구현해 호출부 수정 없이 국회 의안정보시스템의
|
|
8
|
+
* 계류의안 조회로 교체할 수 있다.
|
|
9
|
+
*
|
|
10
|
+
* `searchPending` / `getAllPendingPages`는 일부러 포함하지 않는다. `NsmLmSts`는 서버 필터로
|
|
11
|
+
* 좁히지만 likms에는 그런 필터가 없어, 같은 이름으로 감추면 스캔 비용이 드러나지 않는다.
|
|
12
|
+
* 이 둘을 쓰던 호출부는 컴파일 오류로 한 번 멈추고, `LikmsNsmAdapter`의
|
|
13
|
+
* `searchPending`/`getAllPendingPages`(스캔 기반 대안, JSDoc 참조)를 직접 고르게 한다.
|
|
14
|
+
*/
|
|
15
|
+
export interface INsmListCrawler {
|
|
16
|
+
/** 목록 페이지 HTML을 반환합니다. */
|
|
17
|
+
getListHTML(query?: INsmSearchQuery): Promise<string>;
|
|
18
|
+
/** 목록 HTML을 검색 결과로 파싱합니다. */
|
|
19
|
+
parseList(html: string): INsmSearchResult;
|
|
20
|
+
/** 목록을 검색합니다. */
|
|
21
|
+
search(query?: INsmSearchQuery): Promise<INsmSearchResult>;
|
|
22
|
+
/** 특정 페이지의 목록을 반환합니다. */
|
|
23
|
+
getPage(pageIndex: number, query?: Omit<INsmSearchQuery, 'pageIndex'>): Promise<INsmBillItem[]>;
|
|
24
|
+
/** 전체 페이지를 순차적으로 yield합니다. */
|
|
25
|
+
getAllPages(query?: Omit<INsmSearchQuery, 'pageIndex'>, options?: IBulkOptions): AsyncGenerator<INsmSearchResult>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 국민참여입법센터(`NsmLmSts`)와 동일한 '법안 상세 조회' 인터페이스(공통 포트).
|
|
29
|
+
*
|
|
30
|
+
* - `NsmLmSts`가 구조적으로 이 포트를 만족한다.
|
|
31
|
+
* - `LikmsNsmAdapter`가 이 포트를 구현해 호출부 수정 없이 국회 의안정보시스템의
|
|
32
|
+
* 의안상세 조회로 교체할 수 있다.
|
|
33
|
+
*
|
|
34
|
+
* 값 형식은 양쪽이 다르다(예: likms의 `proposalDate`는 `2026-10-13`, `session`은
|
|
35
|
+
* `제22대(2024~2028)제439회`). 문자열 비교를 하던 호출부는 값을 확인해야 한다.
|
|
36
|
+
*/
|
|
37
|
+
export interface INsmDetailCrawler {
|
|
38
|
+
/** 상세 페이지 HTML을 반환합니다. */
|
|
39
|
+
getDetailHTML(billNo: string): Promise<string>;
|
|
40
|
+
/** 상세 HTML을 법안 상세 정보로 파싱합니다(네트워크 호출 없음). */
|
|
41
|
+
parseDetail(html: string): INsmBillDetail;
|
|
42
|
+
/** 법안 상세 정보를 조회합니다. */
|
|
43
|
+
getDetail(billNo: string): Promise<INsmBillDetail>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 국민참여입법센터(`NsmLmSts`) 호출부를 국회 의안정보시스템 계류의안 조회로
|
|
47
|
+
* 바꿔주는 얇은 어댑터. `INsmListCrawler` 포트를 구현한다.
|
|
48
|
+
*
|
|
49
|
+
* ## 쿼리 매핑 (NsmLmSts → likms)
|
|
50
|
+
* | INsmSearchQuery | ILikmsStateQuery | 비고 |
|
|
51
|
+
* |---|---|---|
|
|
52
|
+
* | `pageIndex` | `pageIndex` | |
|
|
53
|
+
* | `pageSize` | `pageSize`(`rows`) | 양쪽 다 임의 값 반영(NSM 13·100, likms 13·25·200 실측). 기본값만 다르다(NSM 20건 ↔ likms 50건) — 미지정 시 결과 건수·페이지 수가 달라진다 |
|
|
54
|
+
* | `sugCd` / `endSugCd` | `ageFrom` / `ageTo` | 제안대수 |
|
|
55
|
+
* | `sgtCls` | `proposerKind` | 900201→GOV, 900202→MEMASEMBY, 900203→CHRPR |
|
|
56
|
+
* | `scPpsUsr` | — (미지원) | 제안자명 — likms는 **이름을 받지 않아** 옮길 수 없다. 지정하면 예외를 던진다(조용히 무시하면 필터가 빠진 전체 결과가 나온다) |
|
|
57
|
+
* | `scCptPpostCmt` | `ageCmtId` | 상임위 **이름**을 `/bi/bill/sch/findAgeCmt.do` 목록에서 코드로 해석한다. 대수 범위(`sugCd`/`endSugCd`) 안에서 찾고, 이름이 없거나 여러 코드면 예외 |
|
|
58
|
+
* | `searchStDtNew` / `searchEdDtNew` | `fromPropDt` / `toPropDt` | 제안일자, likms는 **양쪽 모두 줄 때만** 서버가 적용한다(한쪽만 주면 전체). NSM 쪽은 `YYYYMMDD`여야 하므로(`YYYY-MM-DD`로 두 값을 주면 0건) 그대로 옮기면 필터가 사라진다 |
|
|
59
|
+
* | `sortOrder` | `ordCd` | ASC/DESC |
|
|
60
|
+
* | `scBlNmSct` | `billNo`(숫자) 또는 `billNm`(문자) | 값 형태로 자동 판별 |
|
|
61
|
+
*
|
|
62
|
+
* ## 매핑하지 않는 필드 (likms에 동일 축이 없음)
|
|
63
|
+
* `rslRsltNmL`, `rslRsltNmR`(심사·의결현황), `cptOfiOrgCd`(소관부처),
|
|
64
|
+
* `sortCol`(값 체계 상이), `issLawitmYn`, `stDt`/`edDt`.
|
|
65
|
+
* 이 필드들은 무시되므로, 해당 조건이 반드시 필요하면 `likms` 프로퍼티로
|
|
66
|
+
* `LikmsStateCrawler`를 직접 써서 매핑해야 한다.
|
|
67
|
+
*
|
|
68
|
+
* 반대로 likms에만 있고 `INsmSearchQuery`에 없는 축도 있다: `cmtResultCd`(소관위
|
|
69
|
+
* 처리결과 계류/가결/부결/폐기), `jntPrpslYn`(공동발의 여부), `stateDtlDivCd`(탭).
|
|
70
|
+
* 어댑터는 추측해서 채우지 않으므로, 필요하면 `adapter.likms.search()`로 직접 지정한다.
|
|
71
|
+
*
|
|
72
|
+
* `scPpsUsr`만은 무시하지 않고 **예외를 던진다**. likms는 제안자명을 받지 않고
|
|
73
|
+
* 의원 ID(`representId`)만 받는데, 어댑터는 이름을 ID로 확정할 수 없기
|
|
74
|
+
* 때문이다(동명이인·미등록 가능). 이름밖에 모를 때는
|
|
75
|
+
* `adapter.likms.findRepresentCandidates(name)`으로 사이트가 제시하는 후보를 보고
|
|
76
|
+
* 고른 `membId`를 `adapter.likms.search({ representId, representKindCd })`로 넘긴다.
|
|
77
|
+
*
|
|
78
|
+
* ## 결과 매핑 (likms → NsmLmSts)
|
|
79
|
+
* `billName`/`billNo`/`link`/`proposer`/`proposalDate`/`committee`/`progressStatus`는
|
|
80
|
+
* 그대로 채우고, 계류의안 목록에 없는 `ministry`/`progressDate`/`resolutionStatus`/
|
|
81
|
+
* `resolutionDate`는 빈 문자열로 둔다.
|
|
82
|
+
*
|
|
83
|
+
* 주의: `representId`로 좁힌 결과에도 공동발의 건이 섞인다(발의구분 기본 '전체').
|
|
84
|
+
* 목록의 제안자 표기(`proposer`)는 대표발의자이므로 찾는 사람과 다른 행이 나올 수 있다.
|
|
85
|
+
* 발의구분까지 특정하려면 `likms` 프로퍼티로 `representKindCd`를 함께 보낸다.
|
|
86
|
+
*
|
|
87
|
+
* 제안이유(`LikmsStateCrawler`의 `hydrateProposalReason`)는 `INsmBillItem`에 대응 필드가
|
|
88
|
+
* 없어 어댑터 결과에 담기지 않는다. 필요하면 `likms` 프로퍼티로 직접 조회한다.
|
|
89
|
+
*/
|
|
90
|
+
export declare class LikmsNsmAdapter implements INsmListCrawler, INsmDetailCrawler {
|
|
91
|
+
/**
|
|
92
|
+
* 계류의안 전용 API(`hasProposalReason`, `billId`, `clearSession` 등)를 쓰기 위한
|
|
93
|
+
* 내부 크롤러. 어댑터가 제공하지 않는 값이 필요하면 이걸 직접 사용한다.
|
|
94
|
+
*/
|
|
95
|
+
readonly likms: LikmsStateCrawler;
|
|
96
|
+
/** 의안상세 전용 크롤러 (billId 단위 조회) */
|
|
97
|
+
private readonly detail;
|
|
98
|
+
private readonly detailParser;
|
|
99
|
+
constructor(config?: LikmsStateCrawlerConfig);
|
|
100
|
+
/**
|
|
101
|
+
* 캐시된 CSRF 토큰/세션과 소관위 목록 캐시를 버립니다.
|
|
102
|
+
* 다음 조회에서 토큰·소관위 목록을 새로 받아옵니다.
|
|
103
|
+
*/
|
|
104
|
+
clearSession(): void;
|
|
105
|
+
/**
|
|
106
|
+
* 상임위 이름 → 소관위 코드 해석에 쓰는 대수별 목록 캐시.
|
|
107
|
+
* 위원회 구성은 대수 단위로만 바뀌므로 (범위 → 목록)으로 캐시한다.
|
|
108
|
+
*/
|
|
109
|
+
private readonly committeeLists;
|
|
110
|
+
/** 대수 범위의 소관위 목록을 캐시로 감싸 조회합니다. */
|
|
111
|
+
private getCommitteeList;
|
|
112
|
+
/**
|
|
113
|
+
* 상임위 **이름**(NSM `scCptPpostCmt`)을 likms 소관위 코드(`ageCmtId`)로 해석합니다.
|
|
114
|
+
*
|
|
115
|
+
* likms 서버는 이름이 아니라 코드를 받으므로 사이트 목록에서 찾는다. 대수 범위
|
|
116
|
+
* (`sugCd`/`endSugCd`)로 좁혀 찾고, 꼬리표·공백을 제거해 비교한다. 확정할 수 없으면
|
|
117
|
+
* **요청 전에** 예외를 던진다(필터가 빠진 전체 결과를 돌려주지 않는다):
|
|
118
|
+
* - 이름이 목록에 없을 때 (`unknown committee`)
|
|
119
|
+
* - 같은 이름이 여러 코드로 존재할 때 — 범위를 넓히면 `보건복지위원회`처럼
|
|
120
|
+
* 대수별로 코드가 나뉜다 (`ambiguous committee`)
|
|
121
|
+
*/
|
|
122
|
+
private resolveCommitteeId;
|
|
123
|
+
/**
|
|
124
|
+
* NSM 쿼리 → likms 쿼리.
|
|
125
|
+
*
|
|
126
|
+
* 이름 기반 필터(`scPpsUsr`, `scCptPpostCmt`)는 서버가 확정 값을 받지 못하는 축이라
|
|
127
|
+
* 조용히 버리지 않는다. 해석할 수 없으면 **요청 전에** 예외를 던져, 필터가 빠진
|
|
128
|
+
* 전체 결과를 오답으로 돌려주는 일을 막는다.
|
|
129
|
+
*/
|
|
130
|
+
private toLikmsQuery;
|
|
131
|
+
private static toNsmItem;
|
|
132
|
+
/** likms 의안상세 → `INsmBillDetail` (값이 있는 쪽을 우선한다) */
|
|
133
|
+
private static mergeDetail;
|
|
134
|
+
/**
|
|
135
|
+
* likms 의안상세 → `INsmBillDetail`.
|
|
136
|
+
*
|
|
137
|
+
* likms에는 NSM의 '발의정보' 원문 한 줄이 없어, 접수정보로 같은 형식의 문자열을
|
|
138
|
+
* 만들어 `proposalInfo`에 넣는다(`"제안자, 제N호(제안일자). 제안회기"`).
|
|
139
|
+
* `attachments.fileId`에는 NSM의 숫자 ID 대신 다운로드 URL이 들어간다.
|
|
140
|
+
*/
|
|
141
|
+
private static toNsmDetail;
|
|
142
|
+
/**
|
|
143
|
+
* 의안상세 페이지 HTML을 반환합니다.
|
|
144
|
+
*
|
|
145
|
+
* 인자는 NSM과 같이 의안번호(`2221883`)를 받고, likms의 billId(`PRC_...`)도 허용한다.
|
|
146
|
+
* 의안번호는 `findBillDetail.do`로 billId로 해석하며, 존재하지 않으면
|
|
147
|
+
* `Error('bill not found: ...')`를 던진다.
|
|
148
|
+
*/
|
|
149
|
+
getDetailHTML(billNoOrBillId: string): Promise<string>;
|
|
150
|
+
/**
|
|
151
|
+
* 의안상세 HTML을 `INsmBillDetail`로 파싱합니다(네트워크 호출 없음).
|
|
152
|
+
*
|
|
153
|
+
* likms는 제목이 상세 페이지에, 발의정보·제안이유가 심사정보 탭 조각에 나뉘어 있어
|
|
154
|
+
* 어느 쪽 HTML을 줘도 들어 있는 값만 채워진다. 전부 채우려면 `getDetail`을 쓴다.
|
|
155
|
+
*/
|
|
156
|
+
parseDetail(html: string): INsmBillDetail;
|
|
157
|
+
/**
|
|
158
|
+
* 의안 상세(제목·발의정보·제안이유·의안원문)를 조회합니다.
|
|
159
|
+
*
|
|
160
|
+
* likms는 제목과 나머지가 서로 다른 응답에 있어 상세 페이지 + 심사정보 조각을
|
|
161
|
+
* 두 번 요청한다(숨김 폼 파라미터는 상세 페이지에서 추출).
|
|
162
|
+
*/
|
|
163
|
+
getDetail(billNoOrBillId: string): Promise<INsmBillDetail>;
|
|
164
|
+
/** 의안번호면 billId로 해석하고, billId(`PRC_...`)면 그대로 사용한다. */
|
|
165
|
+
private resolveBillId;
|
|
166
|
+
private static toNsmResult;
|
|
167
|
+
/** 목록 조각 HTML을 반환합니다. */
|
|
168
|
+
getListHTML(query?: INsmSearchQuery): Promise<string>;
|
|
169
|
+
/** 목록 조각 HTML을 NSM 형식의 검색 결과로 파싱합니다. */
|
|
170
|
+
parseList(html: string): INsmSearchResult;
|
|
171
|
+
/** 계류의안을 검색해 NSM 형식의 결과로 반환합니다. */
|
|
172
|
+
search(query?: INsmSearchQuery): Promise<INsmSearchResult>;
|
|
173
|
+
/** 특정 페이지의 계류의안 목록을 NSM 아이템 형식으로 반환합니다. */
|
|
174
|
+
getPage(pageIndex: number, query?: Omit<INsmSearchQuery, 'pageIndex'>): Promise<INsmBillItem[]>;
|
|
175
|
+
/** 계류의안 전체 페이지를 NSM 형식으로 순차 yield합니다. */
|
|
176
|
+
getAllPages(query?: Omit<INsmSearchQuery, 'pageIndex'>, options?: IBulkOptions): AsyncGenerator<INsmSearchResult>;
|
|
177
|
+
private getAllPagesImpl;
|
|
178
|
+
/**
|
|
179
|
+
* 발의 상태(위원회 회부 이전) 법안만 조회합니다 — `NsmLmSts.searchPending`의 대안.
|
|
180
|
+
*
|
|
181
|
+
* `NsmLmSts`는 `rslRsltNmL=900101`로 서버에서 좁히지만, likms에는 심사진행상태를
|
|
182
|
+
* 좁히는 유효한 필터가 없어(실측) 목록을 앞에서부터 **스캔**하며 `progressStatus`가
|
|
183
|
+
* `'접수'`인 항목만 걸러낸다. 그래서:
|
|
184
|
+
* - 비용이 크다: 접수 항목이 나올 때까지 페이지를 순차 요청한다(`options.maxPages`, 기본 10).
|
|
185
|
+
* - `options.maxPages` 안에서 못 찾으면 빈 결과(`items: []`)를 반환한다.
|
|
186
|
+
* - `items`는 접수 항목만이고 `total`/`totalPages`는 전체(접수 포함) 기준 원본 값이다.
|
|
187
|
+
* - `progressStatus`는 likms 어휘(`'접수'`)이고 NSM의 `'발의'`와 문자열이 다르다.
|
|
188
|
+
*
|
|
189
|
+
* 페이지 단위로 전부 순회하려면 `getAllPendingPages`를 쓴다.
|
|
190
|
+
*/
|
|
191
|
+
searchPending(query?: INsmSearchQuery, options?: IBulkOptions): Promise<INsmSearchResult>;
|
|
192
|
+
/**
|
|
193
|
+
* 접수(위원회 회부 이전) 법안을 페이지 단위로 순차적으로 yield합니다.
|
|
194
|
+
* 서버 필터가 없어 전체 목록을 스캔하며, `options.maxPages`로 범위를 제한합니다.
|
|
195
|
+
*/
|
|
196
|
+
getAllPendingPages(query?: Omit<INsmSearchQuery, 'pageIndex'>, options?: IBulkOptions): AsyncGenerator<INsmSearchResult>;
|
|
197
|
+
private getAllPendingPagesImpl;
|
|
198
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LikmsNsmAdapter = void 0;
|
|
4
|
+
const likms_1 = require("./likms");
|
|
5
|
+
const parser_1 = require("./parser");
|
|
6
|
+
/**
|
|
7
|
+
* 상임위 **이름** 비교용 정규화.
|
|
8
|
+
*
|
|
9
|
+
* likms의 소관위 목록은 대수 범위를 넓히면 같은 이름을 `(18대~22대)` 같은 꼬리표로
|
|
10
|
+
* 구분하므로, 꼬리표와 공백을 제거해 비교한다(`법제사법 위원회` ↔ `법제사법위원회`).
|
|
11
|
+
*/
|
|
12
|
+
const normalizeCommitteeName = (name) => name.replace(/\s+/g, '').replace(/\([^()]*\)$/, '');
|
|
13
|
+
/** NsmLmSts 발의구분 코드 → likms 제안자구분 코드 */
|
|
14
|
+
const PROPOSER_KIND_BY_SGT_CLS = {
|
|
15
|
+
'900201': 'GOV', // 정부
|
|
16
|
+
'900202': 'MEMASEMBY', // 의원
|
|
17
|
+
'900203': 'CHRPR', // 위원장
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* 국민참여입법센터(`NsmLmSts`) 호출부를 국회 의안정보시스템 계류의안 조회로
|
|
21
|
+
* 바꿔주는 얇은 어댑터. `INsmListCrawler` 포트를 구현한다.
|
|
22
|
+
*
|
|
23
|
+
* ## 쿼리 매핑 (NsmLmSts → likms)
|
|
24
|
+
* | INsmSearchQuery | ILikmsStateQuery | 비고 |
|
|
25
|
+
* |---|---|---|
|
|
26
|
+
* | `pageIndex` | `pageIndex` | |
|
|
27
|
+
* | `pageSize` | `pageSize`(`rows`) | 양쪽 다 임의 값 반영(NSM 13·100, likms 13·25·200 실측). 기본값만 다르다(NSM 20건 ↔ likms 50건) — 미지정 시 결과 건수·페이지 수가 달라진다 |
|
|
28
|
+
* | `sugCd` / `endSugCd` | `ageFrom` / `ageTo` | 제안대수 |
|
|
29
|
+
* | `sgtCls` | `proposerKind` | 900201→GOV, 900202→MEMASEMBY, 900203→CHRPR |
|
|
30
|
+
* | `scPpsUsr` | — (미지원) | 제안자명 — likms는 **이름을 받지 않아** 옮길 수 없다. 지정하면 예외를 던진다(조용히 무시하면 필터가 빠진 전체 결과가 나온다) |
|
|
31
|
+
* | `scCptPpostCmt` | `ageCmtId` | 상임위 **이름**을 `/bi/bill/sch/findAgeCmt.do` 목록에서 코드로 해석한다. 대수 범위(`sugCd`/`endSugCd`) 안에서 찾고, 이름이 없거나 여러 코드면 예외 |
|
|
32
|
+
* | `searchStDtNew` / `searchEdDtNew` | `fromPropDt` / `toPropDt` | 제안일자, likms는 **양쪽 모두 줄 때만** 서버가 적용한다(한쪽만 주면 전체). NSM 쪽은 `YYYYMMDD`여야 하므로(`YYYY-MM-DD`로 두 값을 주면 0건) 그대로 옮기면 필터가 사라진다 |
|
|
33
|
+
* | `sortOrder` | `ordCd` | ASC/DESC |
|
|
34
|
+
* | `scBlNmSct` | `billNo`(숫자) 또는 `billNm`(문자) | 값 형태로 자동 판별 |
|
|
35
|
+
*
|
|
36
|
+
* ## 매핑하지 않는 필드 (likms에 동일 축이 없음)
|
|
37
|
+
* `rslRsltNmL`, `rslRsltNmR`(심사·의결현황), `cptOfiOrgCd`(소관부처),
|
|
38
|
+
* `sortCol`(값 체계 상이), `issLawitmYn`, `stDt`/`edDt`.
|
|
39
|
+
* 이 필드들은 무시되므로, 해당 조건이 반드시 필요하면 `likms` 프로퍼티로
|
|
40
|
+
* `LikmsStateCrawler`를 직접 써서 매핑해야 한다.
|
|
41
|
+
*
|
|
42
|
+
* 반대로 likms에만 있고 `INsmSearchQuery`에 없는 축도 있다: `cmtResultCd`(소관위
|
|
43
|
+
* 처리결과 계류/가결/부결/폐기), `jntPrpslYn`(공동발의 여부), `stateDtlDivCd`(탭).
|
|
44
|
+
* 어댑터는 추측해서 채우지 않으므로, 필요하면 `adapter.likms.search()`로 직접 지정한다.
|
|
45
|
+
*
|
|
46
|
+
* `scPpsUsr`만은 무시하지 않고 **예외를 던진다**. likms는 제안자명을 받지 않고
|
|
47
|
+
* 의원 ID(`representId`)만 받는데, 어댑터는 이름을 ID로 확정할 수 없기
|
|
48
|
+
* 때문이다(동명이인·미등록 가능). 이름밖에 모를 때는
|
|
49
|
+
* `adapter.likms.findRepresentCandidates(name)`으로 사이트가 제시하는 후보를 보고
|
|
50
|
+
* 고른 `membId`를 `adapter.likms.search({ representId, representKindCd })`로 넘긴다.
|
|
51
|
+
*
|
|
52
|
+
* ## 결과 매핑 (likms → NsmLmSts)
|
|
53
|
+
* `billName`/`billNo`/`link`/`proposer`/`proposalDate`/`committee`/`progressStatus`는
|
|
54
|
+
* 그대로 채우고, 계류의안 목록에 없는 `ministry`/`progressDate`/`resolutionStatus`/
|
|
55
|
+
* `resolutionDate`는 빈 문자열로 둔다.
|
|
56
|
+
*
|
|
57
|
+
* 주의: `representId`로 좁힌 결과에도 공동발의 건이 섞인다(발의구분 기본 '전체').
|
|
58
|
+
* 목록의 제안자 표기(`proposer`)는 대표발의자이므로 찾는 사람과 다른 행이 나올 수 있다.
|
|
59
|
+
* 발의구분까지 특정하려면 `likms` 프로퍼티로 `representKindCd`를 함께 보낸다.
|
|
60
|
+
*
|
|
61
|
+
* 제안이유(`LikmsStateCrawler`의 `hydrateProposalReason`)는 `INsmBillItem`에 대응 필드가
|
|
62
|
+
* 없어 어댑터 결과에 담기지 않는다. 필요하면 `likms` 프로퍼티로 직접 조회한다.
|
|
63
|
+
*/
|
|
64
|
+
class LikmsNsmAdapter {
|
|
65
|
+
constructor(config) {
|
|
66
|
+
/**
|
|
67
|
+
* 상임위 이름 → 소관위 코드 해석에 쓰는 대수별 목록 캐시.
|
|
68
|
+
* 위원회 구성은 대수 단위로만 바뀌므로 (범위 → 목록)으로 캐시한다.
|
|
69
|
+
*/
|
|
70
|
+
this.committeeLists = new Map();
|
|
71
|
+
this.likms = new likms_1.LikmsStateCrawler(config);
|
|
72
|
+
this.detail = new likms_1.LikmsCrawler(config);
|
|
73
|
+
this.detailParser = new parser_1.LikmsDetailParser();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 캐시된 CSRF 토큰/세션과 소관위 목록 캐시를 버립니다.
|
|
77
|
+
* 다음 조회에서 토큰·소관위 목록을 새로 받아옵니다.
|
|
78
|
+
*/
|
|
79
|
+
clearSession() {
|
|
80
|
+
this.likms.clearSession();
|
|
81
|
+
this.committeeLists.clear();
|
|
82
|
+
}
|
|
83
|
+
/** 대수 범위의 소관위 목록을 캐시로 감싸 조회합니다. */
|
|
84
|
+
async getCommitteeList(ageFrom, ageTo) {
|
|
85
|
+
const key = `${ageFrom ?? ''}~${ageTo ?? ''}`;
|
|
86
|
+
const cached = this.committeeLists.get(key);
|
|
87
|
+
if (cached) {
|
|
88
|
+
return cached;
|
|
89
|
+
}
|
|
90
|
+
const list = await this.likms.findCommittees(ageFrom, ageTo);
|
|
91
|
+
this.committeeLists.set(key, list);
|
|
92
|
+
return list;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 상임위 **이름**(NSM `scCptPpostCmt`)을 likms 소관위 코드(`ageCmtId`)로 해석합니다.
|
|
96
|
+
*
|
|
97
|
+
* likms 서버는 이름이 아니라 코드를 받으므로 사이트 목록에서 찾는다. 대수 범위
|
|
98
|
+
* (`sugCd`/`endSugCd`)로 좁혀 찾고, 꼬리표·공백을 제거해 비교한다. 확정할 수 없으면
|
|
99
|
+
* **요청 전에** 예외를 던진다(필터가 빠진 전체 결과를 돌려주지 않는다):
|
|
100
|
+
* - 이름이 목록에 없을 때 (`unknown committee`)
|
|
101
|
+
* - 같은 이름이 여러 코드로 존재할 때 — 범위를 넓히면 `보건복지위원회`처럼
|
|
102
|
+
* 대수별로 코드가 나뉜다 (`ambiguous committee`)
|
|
103
|
+
*/
|
|
104
|
+
async resolveCommitteeId(name, ageFrom, ageTo) {
|
|
105
|
+
const normalized = name?.trim();
|
|
106
|
+
if (!normalized) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const list = await this.getCommitteeList(ageFrom, ageTo);
|
|
110
|
+
const exact = list.filter((committee) => committee.comCdName.replace(/\s+/g, '') ===
|
|
111
|
+
normalized.replace(/\s+/g, ''));
|
|
112
|
+
const matches = exact.length > 0
|
|
113
|
+
? exact
|
|
114
|
+
: list.filter((committee) => normalizeCommitteeName(committee.comCdName) ===
|
|
115
|
+
normalizeCommitteeName(normalized));
|
|
116
|
+
const codes = [...new Set(matches.map((committee) => committee.comCd))];
|
|
117
|
+
if (codes.length === 1) {
|
|
118
|
+
return codes[0];
|
|
119
|
+
}
|
|
120
|
+
if (codes.length === 0) {
|
|
121
|
+
throw new Error(`unknown committee: ${normalized} — use likms.findCommittees() to pick a comCd and pass it via ageCmtId`);
|
|
122
|
+
}
|
|
123
|
+
const candidates = matches
|
|
124
|
+
.map((committee) => `${committee.comCd} ${committee.comCdName}`)
|
|
125
|
+
.join(', ');
|
|
126
|
+
throw new Error(`ambiguous committee: ${normalized} matches ${codes.length} codes (${candidates}) — narrow sugCd/endSugCd or pass ageCmtId via likms.search`);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* NSM 쿼리 → likms 쿼리.
|
|
130
|
+
*
|
|
131
|
+
* 이름 기반 필터(`scPpsUsr`, `scCptPpostCmt`)는 서버가 확정 값을 받지 못하는 축이라
|
|
132
|
+
* 조용히 버리지 않는다. 해석할 수 없으면 **요청 전에** 예외를 던져, 필터가 빠진
|
|
133
|
+
* 전체 결과를 오답으로 돌려주는 일을 막는다.
|
|
134
|
+
*/
|
|
135
|
+
async toLikmsQuery(query) {
|
|
136
|
+
// likms는 제안자명을 받지 않는다 — 무시하면 필터가 빠진 전체 결과(오답)를 돌려주므로 알린다.
|
|
137
|
+
if (query.scPpsUsr?.trim()) {
|
|
138
|
+
throw new Error('scPpsUsr is not supported: likms filters by member ID only — pick a membId from likms.findRepresentCandidates() and search with representId');
|
|
139
|
+
}
|
|
140
|
+
const needle = query.scBlNmSct?.trim();
|
|
141
|
+
const looksLikeBillNo = needle !== undefined && /^[0-9]+$/.test(needle);
|
|
142
|
+
// 상임위 이름은 대수 범위 안에서 코드로 해석한다(실패 시 여기서 예외).
|
|
143
|
+
const ageCmtId = await this.resolveCommitteeId(query.scCptPpostCmt, query.sugCd, query.endSugCd);
|
|
144
|
+
return {
|
|
145
|
+
pageIndex: query.pageIndex,
|
|
146
|
+
pageSize: query.pageSize,
|
|
147
|
+
billNm: looksLikeBillNo ? undefined : needle,
|
|
148
|
+
billNo: looksLikeBillNo ? needle : undefined,
|
|
149
|
+
proposerKind: query.sgtCls
|
|
150
|
+
? PROPOSER_KIND_BY_SGT_CLS[query.sgtCls]
|
|
151
|
+
: undefined,
|
|
152
|
+
ageCmtId,
|
|
153
|
+
ageFrom: query.sugCd,
|
|
154
|
+
ageTo: query.endSugCd,
|
|
155
|
+
fromPropDt: query.searchStDtNew,
|
|
156
|
+
toPropDt: query.searchEdDtNew,
|
|
157
|
+
ordCd: query.sortOrder,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
static toNsmItem(item) {
|
|
161
|
+
return {
|
|
162
|
+
billName: item.billName,
|
|
163
|
+
billNo: item.billNo,
|
|
164
|
+
link: item.link,
|
|
165
|
+
proposer: item.proposer,
|
|
166
|
+
proposalDate: item.proposalDate,
|
|
167
|
+
committee: item.committee,
|
|
168
|
+
// 계류의안 목록에는 없는 필드는 빈 문자열로 둔다.
|
|
169
|
+
ministry: '',
|
|
170
|
+
progressStatus: item.progressStatus,
|
|
171
|
+
progressDate: '',
|
|
172
|
+
resolutionStatus: '',
|
|
173
|
+
resolutionDate: '',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** likms 의안상세 → `INsmBillDetail` (값이 있는 쪽을 우선한다) */
|
|
177
|
+
static mergeDetail(base, extra) {
|
|
178
|
+
return {
|
|
179
|
+
title: extra.title || base.title,
|
|
180
|
+
billNo: extra.billNo || base.billNo,
|
|
181
|
+
proposer: extra.proposer || base.proposer,
|
|
182
|
+
proposalDate: extra.proposalDate || base.proposalDate,
|
|
183
|
+
session: extra.session || base.session,
|
|
184
|
+
proposalReason: extra.proposalReason ?? base.proposalReason,
|
|
185
|
+
attachments: extra.attachments.length > 0 ? extra.attachments : base.attachments,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* likms 의안상세 → `INsmBillDetail`.
|
|
190
|
+
*
|
|
191
|
+
* likms에는 NSM의 '발의정보' 원문 한 줄이 없어, 접수정보로 같은 형식의 문자열을
|
|
192
|
+
* 만들어 `proposalInfo`에 넣는다(`"제안자, 제N호(제안일자). 제안회기"`).
|
|
193
|
+
* `attachments.fileId`에는 NSM의 숫자 ID 대신 다운로드 URL이 들어간다.
|
|
194
|
+
*/
|
|
195
|
+
static toNsmDetail(info) {
|
|
196
|
+
const parts = [];
|
|
197
|
+
if (info.proposer) {
|
|
198
|
+
parts.push(info.proposer);
|
|
199
|
+
}
|
|
200
|
+
if (info.billNo) {
|
|
201
|
+
const date = info.proposalDate ? `(${info.proposalDate})` : '';
|
|
202
|
+
parts.push(`제${info.billNo}호${date}`);
|
|
203
|
+
}
|
|
204
|
+
const head = parts.join(', ');
|
|
205
|
+
const proposalInfo = info.session
|
|
206
|
+
? head
|
|
207
|
+
? `${head}. ${info.session}`
|
|
208
|
+
: info.session
|
|
209
|
+
: head;
|
|
210
|
+
const attachments = info.attachments.map((item) => ({
|
|
211
|
+
filename: item.filename,
|
|
212
|
+
fileId: item.url,
|
|
213
|
+
}));
|
|
214
|
+
return {
|
|
215
|
+
title: info.title,
|
|
216
|
+
billNo: info.billNo,
|
|
217
|
+
proposalInfo,
|
|
218
|
+
proposer: info.proposer,
|
|
219
|
+
proposalDate: info.proposalDate,
|
|
220
|
+
session: info.session,
|
|
221
|
+
proposalReason: info.proposalReason,
|
|
222
|
+
attachments,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* 의안상세 페이지 HTML을 반환합니다.
|
|
227
|
+
*
|
|
228
|
+
* 인자는 NSM과 같이 의안번호(`2221883`)를 받고, likms의 billId(`PRC_...`)도 허용한다.
|
|
229
|
+
* 의안번호는 `findBillDetail.do`로 billId로 해석하며, 존재하지 않으면
|
|
230
|
+
* `Error('bill not found: ...')`를 던진다.
|
|
231
|
+
*/
|
|
232
|
+
async getDetailHTML(billNoOrBillId) {
|
|
233
|
+
const billId = await this.resolveBillId(billNoOrBillId);
|
|
234
|
+
return this.detail.getDetailPageHTML(billId);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 의안상세 HTML을 `INsmBillDetail`로 파싱합니다(네트워크 호출 없음).
|
|
238
|
+
*
|
|
239
|
+
* likms는 제목이 상세 페이지에, 발의정보·제안이유가 심사정보 탭 조각에 나뉘어 있어
|
|
240
|
+
* 어느 쪽 HTML을 줘도 들어 있는 값만 채워진다. 전부 채우려면 `getDetail`을 쓴다.
|
|
241
|
+
*/
|
|
242
|
+
parseDetail(html) {
|
|
243
|
+
return LikmsNsmAdapter.toNsmDetail(this.detailParser.parseDetail(html));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 의안 상세(제목·발의정보·제안이유·의안원문)를 조회합니다.
|
|
247
|
+
*
|
|
248
|
+
* likms는 제목과 나머지가 서로 다른 응답에 있어 상세 페이지 + 심사정보 조각을
|
|
249
|
+
* 두 번 요청한다(숨김 폼 파라미터는 상세 페이지에서 추출).
|
|
250
|
+
*/
|
|
251
|
+
async getDetail(billNoOrBillId) {
|
|
252
|
+
const billId = await this.resolveBillId(billNoOrBillId);
|
|
253
|
+
const pageHtml = await this.detail.getDetailPageHTML(billId);
|
|
254
|
+
const params = this.detail.parseDetailFormParams(pageHtml);
|
|
255
|
+
if (!params.billId) {
|
|
256
|
+
params.billId = billId;
|
|
257
|
+
}
|
|
258
|
+
const fragmentHtml = await this.detail.getBillInfoHTML(params);
|
|
259
|
+
return LikmsNsmAdapter.toNsmDetail(LikmsNsmAdapter.mergeDetail(this.detailParser.parseDetail(pageHtml), this.detailParser.parseDetail(fragmentHtml)));
|
|
260
|
+
}
|
|
261
|
+
/** 의안번호면 billId로 해석하고, billId(`PRC_...`)면 그대로 사용한다. */
|
|
262
|
+
async resolveBillId(billNoOrBillId) {
|
|
263
|
+
const normalized = billNoOrBillId.trim();
|
|
264
|
+
if (!normalized) {
|
|
265
|
+
throw new Error('billNo is required');
|
|
266
|
+
}
|
|
267
|
+
if (normalized.startsWith('PRC_')) {
|
|
268
|
+
return normalized;
|
|
269
|
+
}
|
|
270
|
+
const billId = await this.detail.resolveBillId(normalized);
|
|
271
|
+
if (!billId) {
|
|
272
|
+
throw new Error(`bill not found: ${normalized}`);
|
|
273
|
+
}
|
|
274
|
+
return billId;
|
|
275
|
+
}
|
|
276
|
+
static toNsmResult(result) {
|
|
277
|
+
return {
|
|
278
|
+
total: result.total,
|
|
279
|
+
totalPages: result.totalPages,
|
|
280
|
+
currentPage: result.currentPage,
|
|
281
|
+
items: result.items.map((item) => LikmsNsmAdapter.toNsmItem(item)),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/** 목록 조각 HTML을 반환합니다. */
|
|
285
|
+
async getListHTML(query = {}) {
|
|
286
|
+
return this.likms.getStateListHTML(await this.toLikmsQuery(query));
|
|
287
|
+
}
|
|
288
|
+
/** 목록 조각 HTML을 NSM 형식의 검색 결과로 파싱합니다. */
|
|
289
|
+
parseList(html) {
|
|
290
|
+
return LikmsNsmAdapter.toNsmResult(this.likms.parseList(html));
|
|
291
|
+
}
|
|
292
|
+
/** 계류의안을 검색해 NSM 형식의 결과로 반환합니다. */
|
|
293
|
+
async search(query = {}) {
|
|
294
|
+
const result = await this.likms.search(await this.toLikmsQuery(query));
|
|
295
|
+
return LikmsNsmAdapter.toNsmResult(result);
|
|
296
|
+
}
|
|
297
|
+
/** 특정 페이지의 계류의안 목록을 NSM 아이템 형식으로 반환합니다. */
|
|
298
|
+
async getPage(pageIndex, query = {}) {
|
|
299
|
+
const result = await this.search({ ...query, pageIndex });
|
|
300
|
+
return result.items;
|
|
301
|
+
}
|
|
302
|
+
/** 계류의안 전체 페이지를 NSM 형식으로 순차 yield합니다. */
|
|
303
|
+
getAllPages(query, options) {
|
|
304
|
+
return this.getAllPagesImpl(query ?? {}, options ?? {});
|
|
305
|
+
}
|
|
306
|
+
async *getAllPagesImpl(query, options) {
|
|
307
|
+
for await (const page of this.likms.getAllPages(await this.toLikmsQuery(query), options)) {
|
|
308
|
+
yield LikmsNsmAdapter.toNsmResult(page);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 발의 상태(위원회 회부 이전) 법안만 조회합니다 — `NsmLmSts.searchPending`의 대안.
|
|
313
|
+
*
|
|
314
|
+
* `NsmLmSts`는 `rslRsltNmL=900101`로 서버에서 좁히지만, likms에는 심사진행상태를
|
|
315
|
+
* 좁히는 유효한 필터가 없어(실측) 목록을 앞에서부터 **스캔**하며 `progressStatus`가
|
|
316
|
+
* `'접수'`인 항목만 걸러낸다. 그래서:
|
|
317
|
+
* - 비용이 크다: 접수 항목이 나올 때까지 페이지를 순차 요청한다(`options.maxPages`, 기본 10).
|
|
318
|
+
* - `options.maxPages` 안에서 못 찾으면 빈 결과(`items: []`)를 반환한다.
|
|
319
|
+
* - `items`는 접수 항목만이고 `total`/`totalPages`는 전체(접수 포함) 기준 원본 값이다.
|
|
320
|
+
* - `progressStatus`는 likms 어휘(`'접수'`)이고 NSM의 `'발의'`와 문자열이 다르다.
|
|
321
|
+
*
|
|
322
|
+
* 페이지 단위로 전부 순회하려면 `getAllPendingPages`를 쓴다.
|
|
323
|
+
*/
|
|
324
|
+
async searchPending(query = {}, options) {
|
|
325
|
+
const result = await this.likms.searchPending(await this.toLikmsQuery(query), options);
|
|
326
|
+
return LikmsNsmAdapter.toNsmResult(result);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 접수(위원회 회부 이전) 법안을 페이지 단위로 순차적으로 yield합니다.
|
|
330
|
+
* 서버 필터가 없어 전체 목록을 스캔하며, `options.maxPages`로 범위를 제한합니다.
|
|
331
|
+
*/
|
|
332
|
+
getAllPendingPages(query, options) {
|
|
333
|
+
return this.getAllPendingPagesImpl(query ?? {}, options ?? {});
|
|
334
|
+
}
|
|
335
|
+
async *getAllPendingPagesImpl(query, options) {
|
|
336
|
+
for await (const page of this.likms.getAllPendingPages(await this.toLikmsQuery(query), options)) {
|
|
337
|
+
yield LikmsNsmAdapter.toNsmResult(page);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
exports.LikmsNsmAdapter = LikmsNsmAdapter;
|
|
342
|
+
//# sourceMappingURL=nsm-compat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nsm-compat.js","sourceRoot":"","sources":["../src/nsm-compat.ts"],"names":[],"mappings":"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"}
|
package/dist/pal.d.ts
CHANGED
|
@@ -154,7 +154,7 @@ export type NsmProposerType = '900201' | '900202' | '900203';
|
|
|
154
154
|
export type NsmResolutionStatus = '902911' | '902912' | '902913' | '902914' | '902915' | '902916' | '902917';
|
|
155
155
|
export interface INsmSearchQuery {
|
|
156
156
|
pageIndex?: number;
|
|
157
|
-
/** 페이지당 건수 (
|
|
157
|
+
/** 페이지당 건수 (기본값 20). 사이트는 임의 값을 그대로 반영한다(실측: 13·100) */
|
|
158
158
|
pageSize?: number;
|
|
159
159
|
/** 제안대수 시작 (예: "22" → 제22대) */
|
|
160
160
|
sugCd?: string;
|
|
@@ -170,9 +170,13 @@ export interface INsmSearchQuery {
|
|
|
170
170
|
rslRsltNmR?: NsmResolutionStatus;
|
|
171
171
|
/** 상임위 (예: "법제사법위원회") */
|
|
172
172
|
scCptPpostCmt?: string;
|
|
173
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* 제안일자 시작. 사이트는 **`YYYYMMDD`(8자리)** 로 보낼 때만 의도대로 해석한다(실측 2026-10-13).
|
|
175
|
+
* `20260901` → 888건, `2026-09-01`(하이픈) → 5,903건(범위 불명), 하이픈으로 시작·종료를
|
|
176
|
+
* 함께 주면 0건이 된다.
|
|
177
|
+
*/
|
|
174
178
|
searchStDtNew?: string;
|
|
175
|
-
/** 제안일자
|
|
179
|
+
/** 제안일자 종료. `searchStDtNew`와 함께 `YYYYMMDD`로 줄 때만 적용된다(`20260901`~`20260910` → 346건) */
|
|
176
180
|
searchEdDtNew?: string;
|
|
177
181
|
/** 제안자 */
|
|
178
182
|
scPpsUsr?: string;
|