pal-crawl 2.0.5 → 2.2.1

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/dist/likms.js ADDED
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LikmsCrawler = void 0;
37
+ const url_1 = require("url");
38
+ const cheerio = __importStar(require("cheerio"));
39
+ const config_1 = require("./config");
40
+ const http_client_1 = require("./http-client");
41
+ /**
42
+ * 국회 의안정보시스템(likms.assembly.go.kr) 크롤러.
43
+ *
44
+ * 의안상세 페이지(billDetailPage.do)는 심사정보 탭 내용을 빈 div에 AJAX(POST)로
45
+ * 채우므로, 정적 HTML만으로는 제안이유를 읽을 수 없다.
46
+ *
47
+ * 실제 흐름 (billDetail.js 기준):
48
+ * 1. GET /bill/bi/billDetailPage.do?billId=...&currMenuNo=2600044
49
+ * → 숨김 폼(#form)에 billId, billNo, billKindCd 등 파라미터 포함
50
+ * 2. POST /bill/bi/bill/detail/billInfo.do (폼 파라미터 직렬화)
51
+ * → 심사정보 탭 조각 HTML 반환
52
+ * 3. 조각의 <pre id="prntSummary"> 안에 제안이유 및 주요내용이 들어 있음
53
+ *
54
+ * PAL(입법예고) 상세 페이지에서 제안이유가 비어 있는 의안의 contentId(=PRC_...)
55
+ * 는 동일한 ID로 이 시스템에서 조회할 수 있다.
56
+ */
57
+ class LikmsCrawler {
58
+ constructor(config) {
59
+ this.httpClient = new http_client_1.HttpClient({
60
+ userAgent: config?.userAgent ?? config_1.Config.UserAgent,
61
+ timeout: config?.timeout ?? 10000,
62
+ retryCount: config?.retryCount ?? 3,
63
+ customHeaders: config?.customHeaders ?? {},
64
+ });
65
+ }
66
+ buildDetailPageUrl(billId) {
67
+ const url = new url_1.URL(config_1.Config.LIKMS_DETAIL_PAGE_URL, config_1.Config.LIKMS_DOMAIN);
68
+ url.searchParams.set('billId', billId);
69
+ url.searchParams.set('currMenuNo', config_1.Config.LIKMS_CURR_MENU_NO);
70
+ return url;
71
+ }
72
+ /** 의안상세 페이지 HTML을 반환합니다. */
73
+ async getDetailPageHTML(billId) {
74
+ const normalized = billId.trim();
75
+ if (!normalized) {
76
+ throw new Error('billId is required');
77
+ }
78
+ return this.httpClient.get(this.buildDetailPageUrl(normalized));
79
+ }
80
+ /**
81
+ * 의안상세 페이지의 숨김 폼(#form input[type=hidden]) 파라미터를 추출합니다.
82
+ * billInfo.do AJAX 요청은 이 파라미터들에 심사정보 섹션의 렌더링 조건이 걸려
83
+ * 있어 원본 페이지에서 그대로 가져와야 한다.
84
+ */
85
+ parseDetailFormParams(html) {
86
+ const $ = cheerio.load(html);
87
+ const params = {};
88
+ $('#form input[type="hidden"]').each((_, el) => {
89
+ const name = $(el).attr('name');
90
+ if (!name) {
91
+ return;
92
+ }
93
+ params[name] = $(el).attr('value') ?? '';
94
+ });
95
+ return params;
96
+ }
97
+ /** 심사정보 탭 조각 HTML을 POST로 반환합니다. */
98
+ async getBillInfoHTML(params) {
99
+ const url = new url_1.URL(config_1.Config.LIKMS_BILL_INFO_URL, config_1.Config.LIKMS_DOMAIN);
100
+ const body = new URLSearchParams(params).toString();
101
+ return this.httpClient.post(url, body);
102
+ }
103
+ /** 심사정보 탭 조각 HTML에서 제안이유 및 주요내용을 파싱하여 반환합니다. */
104
+ parseProposalReason(fragmentHtml) {
105
+ const $ = cheerio.load(fragmentHtml);
106
+ const pre = $('#prntSummary').first();
107
+ if (!pre.length) {
108
+ return null;
109
+ }
110
+ const clone = pre.clone();
111
+ clone.find('br').replaceWith('\n');
112
+ const lines = clone
113
+ .text()
114
+ .replace(/\u00a0/g, ' ')
115
+ .replace(/\r/g, '')
116
+ .split('\n')
117
+ .map((line) => line.replace(/\s+/g, ' ').trim())
118
+ .filter(Boolean);
119
+ // 첫 줄이 섹션 제목("제안이유 및 주요내용")을 반복하는 경우 제거
120
+ if (lines[0]?.replace(/\s+/g, '') === '제안이유및주요내용') {
121
+ lines.shift();
122
+ }
123
+ const text = lines.join('\n');
124
+ return text || null;
125
+ }
126
+ /**
127
+ * billId(PAL contentId와 동일)로 제안이유 및 주요내용을 조회합니다.
128
+ * 네트워크·파싱 실패를 절대 던지지 않고 null을 반환해 호출부가 안전하게
129
+ * 동작하도록 한다.
130
+ */
131
+ async getProposalReason(billId) {
132
+ const normalized = billId.trim();
133
+ if (!normalized) {
134
+ return null;
135
+ }
136
+ try {
137
+ const detailHtml = await this.getDetailPageHTML(normalized);
138
+ const params = this.parseDetailFormParams(detailHtml);
139
+ if (!params.billId) {
140
+ params.billId = normalized;
141
+ }
142
+ const fragmentHtml = await this.getBillInfoHTML(params);
143
+ return this.parseProposalReason(fragmentHtml);
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ }
150
+ exports.LikmsCrawler = LikmsCrawler;
151
+ //# sourceMappingURL=likms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"likms.js","sourceRoot":"","sources":["../src/likms.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const pal_1 = require("./pal");
4
+ const likms_1 = require("./likms");
5
+ // ─── HTML Fixtures (실제 캡처 구조를 축약) ─────────────────────────────────────
6
+ const LIKMS_DETAIL_PAGE_HTML = `
7
+ <div class="content" id="content">
8
+ <div class="page_title">
9
+ <h3 class="detailh3">[2221798] 유아교육법 일부개정법률안(김문수의원 등 12인)</h3>
10
+ </div>
11
+ <div class="content">
12
+ <form id="form" name="form">
13
+ <input type="hidden" id="billId" name="billId" value="PRC_G2G6E0D9E2Z3A1Y7Z5X1Y0W4E9E6D5">
14
+ <input type="hidden" id="billNo" name="billNo" value="2221798">
15
+ <input type="hidden" id="billKindCd" name="billKindCd" value="BKC_0004">
16
+ <input type="hidden" id="currCmtName" name="currCmtName" value="위원회 심사">
17
+ <input type="hidden" id="reexamYn" name="reexamYn" value="N">
18
+ <input type="hidden" id="mainUpdateBillYn" name="mainUpdateBillYn" value="N">
19
+ <input type="hidden" id="billGbnCd" name="billGbnCd" value="">
20
+ <input type="hidden" id="tmprAnYn" name="tmprAnYn" value="N">
21
+ <input type="hidden" id="headMemo" name="headMemo" value="Y">
22
+ <input type="hidden" id="headMemoInfo" name="headMemoInfo" value="비용추계요구서 제출됨.">
23
+ <input type="hidden" id="withdrawCnt" name="withdrawCnt" value="0">
24
+ <input type="hidden" id="intrsBillCnt" name="intrsBillCnt" value="0">
25
+ <input type="hidden" id="stageMemo" name="stageMemo" value="N"/>
26
+ </form>
27
+ </div>
28
+ </div>
29
+ `;
30
+ const LIKMS_BILL_INFO_FRAGMENT_HTML = `
31
+ <div id="tab_billInfo_sect">
32
+ <div class="container bill_info" id="container" style="height: auto;">
33
+ <form id="billInfoForm">
34
+ <div class="bill_info_div div_on" id="stage_list">
35
+ <div class="theme_title_wrap"><h3>심사진행단계</h3></div>
36
+ </div>
37
+ <div id="rcp_list" class="theme_area bill_info_div">
38
+ <div class="theme_list">
39
+ <h3>접수정보</h3>
40
+ <div>
41
+ <h4>의안접수정보</h4>
42
+ <table class="align_center">
43
+ <tbody id="insc-rcp-row">
44
+ <tr>
45
+ <td class="billNo"><i>의안번호</i><span>2221798</span></td>
46
+ <td class="proposeDt"><i>제안일자</i><span>2026-10-06</span></td>
47
+ </tr>
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ </div>
52
+ <div class="theme_list" id="prntsummary-sect">
53
+ <div>
54
+ <h4>제안이유 및 주요내용</h4>
55
+ <pre id="prntSummary" class="theme_list_summary" style="height: 130px;">
56
+ 제안이유 및 주요내용
57
+
58
+ 현행 「유아교육법」은 원장 등 교원의 유아생활지도에 관한 근거를 두고 있으며, 정당한 유아생활지도에 대해서는 「아동복지법」상 신체적 학대행위로 보지 아니하도록 규정하고 있음.
59
+ 그런데 정서적 학대 및 방임의 개념이 포괄적으로 규정되어 있어 유치원에서 이루어지는 정당한 교육활동이나 생활지도까지 아동학대로 신고ㆍ조사되는 등 교육활동이 위축되고 있다는 지적이 있음.
60
+ 이에 「아동복지법」 제17조제2항에 따라 원장과 교원이 교육활동 과정에서 하여서는 아니 되는 행위를 구체적으로 규정하여 정당한 교육활동을 보장하고 유아를 보호하려는 것임(안 제21조의3제4항 신설).
61
+
62
+
63
+ 참고사항
64
+
65
+ 이 법률안은 김문수의원이 대표발의한 「아동복지법 일부개정법률안」(의안번호 제21797호)의 의결을 전제로 하는 것이므로 같은 법률안이 의결되지 아니하거나 수정의결되는 경우에는 이에 맞추어 조정되어야 할 것임.</pre>
66
+ </div>
67
+ <div class="member_list_more">
68
+ <a href="javascript:;" id="btnPrntSummaryMore" class="closed">+더보기</a>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </form>
73
+ </div>
74
+ </div>
75
+ `;
76
+ const LIKMS_BILL_INFO_FRAGMENT_NO_REASON_HTML = `
77
+ <div id="tab_billInfo_sect">
78
+ <div id="rcp_list" class="theme_area bill_info_div">
79
+ <div class="theme_list">
80
+ <h3>접수정보</h3>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ `;
85
+ const PAL_CONTENT_HTML_EMPTY_REASON = `
86
+ <div class="card-wrap">
87
+ <div class="item">
88
+ <h4>제안이유 및 주요내용</h4>
89
+ <div class="desc">
90
+
91
+ </div>
92
+ </div>
93
+ <div class="item">
94
+ <h4>의견제출 방법</h4>
95
+ <div class="desc">
96
+ 서울시 영등포구 의사당대로 1(여의도동) 교육위원회
97
+ </div>
98
+ </div>
99
+ </div>
100
+ `;
101
+ const PAL_CONTENT_HTML_WITH_REASON = `
102
+ <div class="card-wrap">
103
+ <div class="item">
104
+ <h4>제안이유 및 주요내용</h4>
105
+ <div class="desc">
106
+ 현행법은 과태료 처분 기준이 없어 문제라는 지적이 있음.
107
+ 이에 과태료 부과 기준을 신설하려는 것임.
108
+ </div>
109
+ </div>
110
+ </div>
111
+ `;
112
+ const EXPECTED_LIKMS_REASON = [
113
+ '현행 「유아교육법」은 원장 등 교원의 유아생활지도에 관한 근거를 두고 있으며, 정당한 유아생활지도에 대해서는 「아동복지법」상 신체적 학대행위로 보지 아니하도록 규정하고 있음.',
114
+ '그런데 정서적 학대 및 방임의 개념이 포괄적으로 규정되어 있어 유치원에서 이루어지는 정당한 교육활동이나 생활지도까지 아동학대로 신고ㆍ조사되는 등 교육활동이 위축되고 있다는 지적이 있음.',
115
+ '이에 「아동복지법」 제17조제2항에 따라 원장과 교원이 교육활동 과정에서 하여서는 아니 되는 행위를 구체적으로 규정하여 정당한 교육활동을 보장하고 유아를 보호하려는 것임(안 제21조의3제4항 신설).',
116
+ '참고사항',
117
+ '이 법률안은 김문수의원이 대표발의한 「아동복지법 일부개정법률안」(의안번호 제21797호)의 의결을 전제로 하는 것이므로 같은 법률안이 의결되지 아니하거나 수정의결되는 경우에는 이에 맞추어 조정되어야 할 것임.',
118
+ ].join('\n');
119
+ const TEST_BILL_ID = 'PRC_G2G6E0D9E2Z3A1Y7Z5X1Y0W4E9E6D5';
120
+ const getLikmsHttpClient = (crawler) => crawler.httpClient;
121
+ // ─── LikmsCrawler ─────────────────────────────────────────────────────────────
122
+ describe('LikmsCrawler', () => {
123
+ describe('parseDetailFormParams', () => {
124
+ const crawler = new likms_1.LikmsCrawler();
125
+ test('extracts hidden form params from detail page', () => {
126
+ const params = crawler.parseDetailFormParams(LIKMS_DETAIL_PAGE_HTML);
127
+ expect(params.billId).toBe(TEST_BILL_ID);
128
+ expect(params.billNo).toBe('2221798');
129
+ expect(params.billKindCd).toBe('BKC_0004');
130
+ expect(params.tmprAnYn).toBe('N');
131
+ expect(params.reexamYn).toBe('N');
132
+ expect(params.withdrawCnt).toBe('0');
133
+ });
134
+ test('keeps empty-valued params (rendering condition)', () => {
135
+ const params = crawler.parseDetailFormParams(LIKMS_DETAIL_PAGE_HTML);
136
+ expect(params.billGbnCd).toBe('');
137
+ });
138
+ test('returns empty object when form is absent', () => {
139
+ expect(crawler.parseDetailFormParams('<div>no form</div>')).toEqual({});
140
+ });
141
+ });
142
+ describe('parseProposalReason', () => {
143
+ const crawler = new likms_1.LikmsCrawler();
144
+ test('extracts reason from #prntSummary and strips repeated heading', () => {
145
+ const reason = crawler.parseProposalReason(LIKMS_BILL_INFO_FRAGMENT_HTML);
146
+ expect(reason).toBe(EXPECTED_LIKMS_REASON);
147
+ });
148
+ test('reason does not contain the repeated section heading', () => {
149
+ const reason = crawler.parseProposalReason(LIKMS_BILL_INFO_FRAGMENT_HTML);
150
+ expect(reason).not.toMatch(/^제안이유/);
151
+ expect(reason).not.toContain('의안접수정보');
152
+ });
153
+ test('returns null when fragment has no #prntSummary', () => {
154
+ expect(crawler.parseProposalReason(LIKMS_BILL_INFO_FRAGMENT_NO_REASON_HTML)).toBeNull();
155
+ });
156
+ test('returns null when #prntSummary is empty', () => {
157
+ expect(crawler.parseProposalReason('<pre id="prntSummary"></pre>')).toBe(null);
158
+ });
159
+ test('returns null when #prntSummary contains only the heading', () => {
160
+ expect(crawler.parseProposalReason('<pre id="prntSummary">제안이유 및 주요내용</pre>')).toBeNull();
161
+ });
162
+ test('converts <br> tags to newlines', () => {
163
+ const reason = crawler.parseProposalReason('<pre id="prntSummary">제안이유 및 주요내용\n첫 줄<br>두 줄</pre>');
164
+ expect(reason).toBe('첫 줄\n두 줄');
165
+ });
166
+ });
167
+ describe('getProposalReason', () => {
168
+ const crawler = new likms_1.LikmsCrawler();
169
+ test('fetches detail page then posts fragment endpoint', async () => {
170
+ const httpClient = getLikmsHttpClient(crawler);
171
+ const getSpy = jest
172
+ .spyOn(httpClient, 'get')
173
+ .mockResolvedValue(LIKMS_DETAIL_PAGE_HTML);
174
+ const postSpy = jest
175
+ .spyOn(httpClient, 'post')
176
+ .mockResolvedValue(LIKMS_BILL_INFO_FRAGMENT_HTML);
177
+ const reason = await crawler.getProposalReason(` ${TEST_BILL_ID} `);
178
+ expect(reason).toBe(EXPECTED_LIKMS_REASON);
179
+ const getUrl = getSpy.mock.calls[0][0];
180
+ expect(getUrl.toString()).toContain('https://likms.assembly.go.kr/bill/bi/billDetailPage.do');
181
+ expect(getUrl.searchParams.get('billId')).toBe(TEST_BILL_ID);
182
+ expect(getUrl.searchParams.get('currMenuNo')).toBe('2600044');
183
+ const [postUrl, postBody] = postSpy.mock.calls[0];
184
+ expect(postUrl.toString()).toContain('https://likms.assembly.go.kr/bill/bi/bill/detail/billInfo.do');
185
+ expect(postBody).toContain(`billId=${TEST_BILL_ID}`);
186
+ expect(postBody).toContain('billNo=2221798');
187
+ expect(postBody).toContain('billKindCd=BKC_0004');
188
+ getSpy.mockRestore();
189
+ postSpy.mockRestore();
190
+ });
191
+ test('returns null when fragment has no proposal reason', async () => {
192
+ const httpClient = getLikmsHttpClient(crawler);
193
+ const getSpy = jest
194
+ .spyOn(httpClient, 'get')
195
+ .mockResolvedValue(LIKMS_DETAIL_PAGE_HTML);
196
+ const postSpy = jest
197
+ .spyOn(httpClient, 'post')
198
+ .mockResolvedValue(LIKMS_BILL_INFO_FRAGMENT_NO_REASON_HTML);
199
+ await expect(crawler.getProposalReason(TEST_BILL_ID)).resolves.toBeNull();
200
+ getSpy.mockRestore();
201
+ postSpy.mockRestore();
202
+ });
203
+ test('never throws when detail page fetch fails', async () => {
204
+ const httpClient = getLikmsHttpClient(crawler);
205
+ const getSpy = jest
206
+ .spyOn(httpClient, 'get')
207
+ .mockRejectedValue(new Error('network down'));
208
+ await expect(crawler.getProposalReason(TEST_BILL_ID)).resolves.toBeNull();
209
+ getSpy.mockRestore();
210
+ });
211
+ test('never throws when fragment post fails', async () => {
212
+ const httpClient = getLikmsHttpClient(crawler);
213
+ const getSpy = jest
214
+ .spyOn(httpClient, 'get')
215
+ .mockResolvedValue(LIKMS_DETAIL_PAGE_HTML);
216
+ const postSpy = jest
217
+ .spyOn(httpClient, 'post')
218
+ .mockRejectedValue(new Error('Bad Request'));
219
+ await expect(crawler.getProposalReason(TEST_BILL_ID)).resolves.toBeNull();
220
+ getSpy.mockRestore();
221
+ postSpy.mockRestore();
222
+ });
223
+ test('returns null for empty billId without network calls', async () => {
224
+ const httpClient = getLikmsHttpClient(crawler);
225
+ const getSpy = jest.spyOn(httpClient, 'get');
226
+ const postSpy = jest.spyOn(httpClient, 'post');
227
+ await expect(crawler.getProposalReason('')).resolves.toBeNull();
228
+ await expect(crawler.getProposalReason(' ')).resolves.toBeNull();
229
+ expect(getSpy).not.toHaveBeenCalled();
230
+ expect(postSpy).not.toHaveBeenCalled();
231
+ getSpy.mockRestore();
232
+ postSpy.mockRestore();
233
+ });
234
+ });
235
+ describe('getDetailPageHTML', () => {
236
+ const crawler = new likms_1.LikmsCrawler();
237
+ test('throws when billId is empty', async () => {
238
+ await expect(crawler.getDetailPageHTML('')).rejects.toThrow('billId is required');
239
+ });
240
+ });
241
+ });
242
+ // ─── PalCrawl ↔ LikmsCrawler 연계 ─────────────────────────────────────────────
243
+ describe('PalCrawl hydrateProposalReason', () => {
244
+ const setup = (config) => {
245
+ const pal = new pal_1.PalCrawl(config);
246
+ const palHttp = pal
247
+ .httpClient;
248
+ const likms = pal.likms;
249
+ return { pal, palHttp, likmsHttp: likms.httpClient };
250
+ };
251
+ afterEach(() => {
252
+ jest.restoreAllMocks();
253
+ });
254
+ test('fills empty proposalReason from likms (getContent)', async () => {
255
+ const { pal, palHttp, likmsHttp } = setup();
256
+ jest.spyOn(palHttp, 'get').mockResolvedValue(PAL_CONTENT_HTML_EMPTY_REASON);
257
+ jest.spyOn(likmsHttp, 'get').mockResolvedValue(LIKMS_DETAIL_PAGE_HTML);
258
+ jest
259
+ .spyOn(likmsHttp, 'post')
260
+ .mockResolvedValue(LIKMS_BILL_INFO_FRAGMENT_HTML);
261
+ const content = await pal.getContent(TEST_BILL_ID);
262
+ expect(content.proposalReason).toBe(EXPECTED_LIKMS_REASON);
263
+ });
264
+ test('fills empty proposalReason from likms (getDoneContent)', async () => {
265
+ const { pal, palHttp, likmsHttp } = setup();
266
+ jest.spyOn(palHttp, 'get').mockResolvedValue(PAL_CONTENT_HTML_EMPTY_REASON);
267
+ jest.spyOn(likmsHttp, 'get').mockResolvedValue(LIKMS_DETAIL_PAGE_HTML);
268
+ jest
269
+ .spyOn(likmsHttp, 'post')
270
+ .mockResolvedValue(LIKMS_BILL_INFO_FRAGMENT_HTML);
271
+ const content = await pal.getDoneContent(TEST_BILL_ID);
272
+ expect(content.proposalReason).toBe(EXPECTED_LIKMS_REASON);
273
+ });
274
+ test('skips likms when PAL proposalReason already exists', async () => {
275
+ const { pal, palHttp, likmsHttp } = setup();
276
+ jest.spyOn(palHttp, 'get').mockResolvedValue(PAL_CONTENT_HTML_WITH_REASON);
277
+ const getSpy = jest.spyOn(likmsHttp, 'get');
278
+ const postSpy = jest.spyOn(likmsHttp, 'post');
279
+ const content = await pal.getContent(TEST_BILL_ID);
280
+ expect(content.proposalReason).toContain('과태료 부과 기준');
281
+ expect(getSpy).not.toHaveBeenCalled();
282
+ expect(postSpy).not.toHaveBeenCalled();
283
+ });
284
+ test('skips likms when hydrateProposalReason is false', async () => {
285
+ const { pal, palHttp, likmsHttp } = setup({ hydrateProposalReason: false });
286
+ jest.spyOn(palHttp, 'get').mockResolvedValue(PAL_CONTENT_HTML_EMPTY_REASON);
287
+ const getSpy = jest.spyOn(likmsHttp, 'get');
288
+ const postSpy = jest.spyOn(likmsHttp, 'post');
289
+ const content = await pal.getContent(TEST_BILL_ID);
290
+ expect(content.proposalReason).toBeNull();
291
+ expect(getSpy).not.toHaveBeenCalled();
292
+ expect(postSpy).not.toHaveBeenCalled();
293
+ });
294
+ test('keeps null proposalReason when likms lookup fails', async () => {
295
+ const { pal, palHttp, likmsHttp } = setup();
296
+ jest.spyOn(palHttp, 'get').mockResolvedValue(PAL_CONTENT_HTML_EMPTY_REASON);
297
+ jest.spyOn(likmsHttp, 'get').mockRejectedValue(new Error('network down'));
298
+ const content = await pal.getContent(TEST_BILL_ID);
299
+ expect(content.proposalReason).toBeNull();
300
+ expect(content.title).toBe('');
301
+ });
302
+ test('keeps null proposalReason when likms has no reason either', async () => {
303
+ const { pal, palHttp, likmsHttp } = setup();
304
+ jest.spyOn(palHttp, 'get').mockResolvedValue(PAL_CONTENT_HTML_EMPTY_REASON);
305
+ jest.spyOn(likmsHttp, 'get').mockResolvedValue(LIKMS_DETAIL_PAGE_HTML);
306
+ jest
307
+ .spyOn(likmsHttp, 'post')
308
+ .mockResolvedValue(LIKMS_BILL_INFO_FRAGMENT_NO_REASON_HTML);
309
+ const content = await pal.getContent(TEST_BILL_ID);
310
+ expect(content.proposalReason).toBeNull();
311
+ });
312
+ });
313
+ //# sourceMappingURL=likms.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"likms.test.js","sourceRoot":"","sources":["../src/likms.test.ts"],"names":[],"mappings":"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"}
package/dist/pal.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Browser } from 'puppeteer';
1
+ import type { Browser } from 'puppeteer';
2
2
  import { type ITableData, type IContentData, type ISearchResult, type INsmBillItem, type INsmBillDetail, type INsmSearchResult } from './parser';
3
3
  export type { IAttachment, ITableData, IContentData, ISearchResult, INsmAttachment, INsmBillItem, INsmBillDetail, INsmSearchResult, } from './parser';
4
4
  export interface ScreenshotOptions {
@@ -17,6 +17,12 @@ export interface PalCrawlConfig {
17
17
  screenshot?: ScreenshotOptions;
18
18
  /** 목록의 잘린 법안 제목을 상세 페이지에서 보정할지 여부 */
19
19
  hydrateTruncatedTitles?: boolean;
20
+ /**
21
+ * PAL 상세 페이지에서 제안이유가 비어 있을 때 국회 의안정보시스템
22
+ * (likms.assembly.go.kr)에서 제안이유를 보정할지 여부 (기본값: true).
23
+ * 보정 실패 시 기존 값(null)을 그대로 유지하며 오류를 던지지 않습니다.
24
+ */
25
+ hydrateProposalReason?: boolean;
20
26
  }
21
27
  export interface ISearchQuery {
22
28
  pageIndex?: number;
@@ -57,6 +63,8 @@ export declare abstract class ScreenshotBase {
57
63
  export declare class PalCrawl extends ScreenshotBase {
58
64
  private readonly httpClient;
59
65
  private readonly parser;
66
+ private readonly likms;
67
+ private readonly hydrateProposalReason;
60
68
  constructor(config?: PalCrawlConfig);
61
69
  /** 진행 중인 입법예고 목록 페이지 스크린샷을 반환합니다. */
62
70
  getPalScreenshot(): Promise<Buffer>;
@@ -77,6 +85,12 @@ export declare class PalCrawl extends ScreenshotBase {
77
85
  get(): Promise<ITableData[]>;
78
86
  /** ID로 진행 중인 입법예고의 법률안 상세 페이지 HTML을 반환합니다. */
79
87
  getContentHTML(id: string): Promise<string>;
88
+ /**
89
+ * PAL 상세 페이지에서 파싱한 제안이유가 비어 있으면 국회 의안정보시스템
90
+ * (likms)에서 같은 의안의 제안이유를 조회해 채웁니다.
91
+ * 보정 실패·의안 없음 등의 경우 기존 결과를 그대로 반환합니다.
92
+ */
93
+ private hydrateProposalReasonIfEmpty;
80
94
  /** ID로 진행 중인 입법예고의 법률안 상세 정보를 조회합니다. */
81
95
  getContent(id: string): Promise<IContentData>;
82
96
  /** 완료된 입법예고 목록 페이지의 HTML을 반환합니다. */