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/.freebuff/project-id +1 -0
- package/AGENTS.md +347 -0
- package/README.md +32 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/http-client.d.ts +6 -0
- package/dist/http-client.js +25 -5
- package/dist/http-client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/likms.d.ts +45 -0
- package/dist/likms.js +151 -0
- package/dist/likms.js.map +1 -0
- package/dist/likms.test.d.ts +1 -0
- package/dist/likms.test.js +313 -0
- package/dist/likms.test.js.map +1 -0
- package/dist/pal.d.ts +15 -1
- package/dist/pal.js +70 -8
- package/dist/pal.js.map +1 -1
- package/package.json +10 -10
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d56ee6eb-546a-4a34-a6c7-6c28626bb8b8
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
에이전트(및 신규 기여자)가 이 저장소에서 작업할 때 따라야 지침입니다.
|
|
4
|
+
규칙은 실제 코드와 Git 히스토리에서 도출했거나 여기서 확정한 것이며,
|
|
5
|
+
어긋나면 코드를 규칙에 맞출 것.
|
|
6
|
+
|
|
7
|
+
**목차**: [프로젝트 개요](#프로젝트-개요) · [저장소 구조](#저장소-구조) · [개발 명령어](#개발-명령어) ·
|
|
8
|
+
[TypeScript 컨벤션](#typescript-컨벤션) · [에이전트 메모리](#에이전트-메모리-agent_memories) ·
|
|
9
|
+
[테스트 컨벤션](#테스트-컨벤션) · [브랜치 전략](#브랜치-전략) · [커밋 메시지](#커밋-메시지) ·
|
|
10
|
+
[`gh` CLI](#github-리모트-작업은-gh-cli) · [PR/병합 규칙](#pr병합-규칙) · [릴리스](#릴리스) ·
|
|
11
|
+
[주의사항](#주의사항)
|
|
12
|
+
|
|
13
|
+
## 프로젝트 개요
|
|
14
|
+
|
|
15
|
+
- **pal-crawl**: npm에 배포되는 TypeScript **라이브러리** (Node.js CLI/서버 아님).
|
|
16
|
+
- 대상 사이트 3곳 크롤링:
|
|
17
|
+
- 국회입법예고 `pal.assembly.go.kr` (PalCrawl / PalParser)
|
|
18
|
+
- 국민참여입법센터 `opinion.lawmaking.go.kr` (NsmLmSts / NsmLmStsParser)
|
|
19
|
+
- 국회 의안정보시스템 `likms.assembly.go.kr` (LikmsCrawler — 제안이유 보정용)
|
|
20
|
+
- 배포 산출물은 `dist/`만 (`.npmignore`가 `src/`·`agent_memories/` 제외).
|
|
21
|
+
gitignore 대상은 빌드 산출물·커버리지뿐 아니라 OS/에디터 파일과 에이전트 상태 파일(`.freebuff/` 등)까지 포함한다 (`.gitignore` 참조).
|
|
22
|
+
|
|
23
|
+
## 저장소 구조
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
27
|
+
index.ts # public API barrel — 모든 외부 노출 export는 여기에 등록
|
|
28
|
+
config.ts # Config enum: 도메인/URL/User-Agent 등 상수
|
|
29
|
+
http-client.ts # HttpClient: 인코딩(cp949/euc-kr 폴백), 재시도, 타임아웃
|
|
30
|
+
parser.ts # PalParser, NsmLmStsParser + 데이터 인터페이스 (HTML → 객체)
|
|
31
|
+
pal.ts # PalCrawl, NsmLmSts, ScreenshotBase + 설정 인터페이스
|
|
32
|
+
likms.ts # LikmsCrawler
|
|
33
|
+
*.test.ts # 테스트는 소스와 동봉 (별도 test/ 디렉터리 없음)
|
|
34
|
+
agent_memories/ # 에이전트 공유 메모리 (README.md = 형식 규칙, INDEX.md = 진입점)
|
|
35
|
+
.github/workflows/ # test.yml, build.yml (Node 24, PR → main에서 실행)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**설계 원칙**: 네트워크 담당(크롤러 클래스)과 HTML 파싱 담당(파서 클래스)을 분리한다.
|
|
39
|
+
새 사이트를 붙일 때도 `XxxCrawler`(요청) + `XxxParser`(파싱)로 나누고, 스크린샷은
|
|
40
|
+
`ScreenshotBase`를 상속해 공유한다.
|
|
41
|
+
|
|
42
|
+
## 개발 명령어
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm ci # 설치 (lockfile 유지, npm install 금지)
|
|
46
|
+
npm run typecheck # tsc --noEmit
|
|
47
|
+
npm run lint # eslint "src/**/*.ts" --fix (--fix가 기본 포함)
|
|
48
|
+
npm run format # prettier --write "src/**/*.ts"
|
|
49
|
+
npm test # jest
|
|
50
|
+
npm run test:cov # 커버리지
|
|
51
|
+
npm run build # tsc → dist/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**커밋/PR 전 필수 게이트**: `npm run typecheck && npm run lint && npm test && npm run build`.
|
|
55
|
+
(`prepublishOnly`는 lint → test → build — `npm run build`(tsc)가 타입 오류에서 실패하므로 사실상 typecheck까지 포함된다.)
|
|
56
|
+
|
|
57
|
+
## TypeScript 컨벤션
|
|
58
|
+
|
|
59
|
+
### 툴체인
|
|
60
|
+
|
|
61
|
+
- 대상: CommonJS / ES2020, `declaration: true`, `rootDir: src` → `outDir: dist`.
|
|
62
|
+
- 포맷팅: Prettier — `singleQuote: true`, `trailingComma: "all"`, 2스페이스 들여쓰기, 80컬럼.
|
|
63
|
+
- Lint: flat config(`@eslint/js` recommended + `typescript-eslint` recommended).
|
|
64
|
+
타입체크 기반 규칙은 없으므로 `npm run typecheck`가 사실상 타입 게이트.
|
|
65
|
+
- 아래 규칙 중 린트로 강제되지 않는 항목(타입 단언·비동기 관례·null 정책)이 있다.
|
|
66
|
+
게이트 통과만으로 충분하지 않으므로 코드도 규칙을 따를 것.
|
|
67
|
+
- `npm run lint`는 `--fix`가 기본 — 실행 후 달라진 파일을 확인하고 커밋에 포함.
|
|
68
|
+
|
|
69
|
+
### 명명
|
|
70
|
+
|
|
71
|
+
- 파싱 결과/데이터 인터페이스는 **`I` 접두사**: `ITableData`, `IContentData`, `ISearchQuery`, `INsmBillDetail`.
|
|
72
|
+
- 클래스 설정/옵션 타입은 **접두사 없음**: `PalCrawlConfig`, `HttpClientConfig`, `LikmsCrawlerConfig`, `ScreenshotOptions`.
|
|
73
|
+
- 클래스는 PascalCase, 메서드/필드는 camelCase. 상수는 `Config` enum으로 모은다(파일마다 상수를 흩어놓지 않음).
|
|
74
|
+
- **파일명은 kebab-case**: `http-client.ts`, `likms.ts`. 테스트는 `원본이름.test.ts` (`likms.test.ts`).
|
|
75
|
+
- 메서드/함수는 동사 시작 + 역할 접미사로 짓는다:
|
|
76
|
+
`get*`(요청), `parse*`(HTML → 객체), `build*`(URL·파라미터), `extract*`(특정 값 추출),
|
|
77
|
+
`hydrate*`(보정), `normalize*`(정규화), `init*`(초기화).
|
|
78
|
+
- boolean은 의미가 드러나는 이름 (`enabled`, `fullPage`, `hydrateTruncatedTitles`).
|
|
79
|
+
축약은 `id`, `url`, `html`, `pdf`, `hwp` 등 이미 관용인 것만 허용.
|
|
80
|
+
|
|
81
|
+
### 타입 시스템
|
|
82
|
+
|
|
83
|
+
- **`any` 금지** (ESLint `@typescript-eslint/no-explicit-any` 위반). 외부 입력은
|
|
84
|
+
`unknown`으로 받고 가드로 좁힌다:
|
|
85
|
+
```ts
|
|
86
|
+
// ❌ const parsed: any = JSON.parse(raw);
|
|
87
|
+
// ✅
|
|
88
|
+
const parsed: unknown = JSON.parse(raw);
|
|
89
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
90
|
+
throw new Error('invalid payload');
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
- **데이터 구조는 `interface`, 닫힌 집합·유틸리티는 `type`**:
|
|
94
|
+
```ts
|
|
95
|
+
interface IContentData { /* ... */ } // 구조 → interface
|
|
96
|
+
type SortGbn = 'DESC' | 'ASC'; // 닫힌 집합 → type
|
|
97
|
+
```
|
|
98
|
+
근거: `sortGbn?: 'DESC' | 'ASC'`, `format?: 'png' | 'jpeg'` 같은 기존 패턴.
|
|
99
|
+
- **공개 멤버·함수는 반환 타입을 항상 명시**:
|
|
100
|
+
`public async getDetailPageHTML(billId: string): Promise<string>`.
|
|
101
|
+
- 제네릭 타입 매개변수는 `T` 접두사 (`TItem`, `TResult`). 추론 가능한 곳에
|
|
102
|
+
불필요하게 제네릭·호출 위치 타입을 강제하지 않는다.
|
|
103
|
+
- 선택 옵션은 `?`로 선언하고 메서드 진입부에서 `?? 기본값`으로 정규화
|
|
104
|
+
(LikmsCrawler/HttpClient 패턴):
|
|
105
|
+
```ts
|
|
106
|
+
timeout: config?.timeout ?? 10000,
|
|
107
|
+
```
|
|
108
|
+
- 타입 단언(`as`)은 최소한만 — 검증 없이 타입을 확신시키는 단언 금지.
|
|
109
|
+
예외: 테스트의 httpClient mock 캐스팅 (테스트 컨벤션 참고).
|
|
110
|
+
- 방어는 optional chaining과 병합 연산자로: `metaMatch?.[1]`, `contentType ?? ''`.
|
|
111
|
+
|
|
112
|
+
### 값·결측 처리
|
|
113
|
+
|
|
114
|
+
- 파싱 결측값은 **`undefined`가 아니라 `null`**: `string | null`, `number | null`.
|
|
115
|
+
- 설정 옵션 미지정은 `undefined`(선택 `?`)로 표현하고 `??`로 처리 —
|
|
116
|
+
`null`과 `undefined`를 의미 없이 섞지 않는다.
|
|
117
|
+
- 원시 문자열을 그대로 반환하지 말고 필요한 정규화(`trim`, 공백 정리)를 거친다.
|
|
118
|
+
|
|
119
|
+
### 오류 처리
|
|
120
|
+
|
|
121
|
+
- 필수 인자는 메서드 진입부에서 가드:
|
|
122
|
+
```ts
|
|
123
|
+
if (!normalized) throw new Error('billNo is required');
|
|
124
|
+
```
|
|
125
|
+
- **파서(`PalParser`, `NsmLmStsParser`)는 절대 예외를 던지지 않는다.**
|
|
126
|
+
잘못된/빈 HTML이면 빈 배열·`null`·0으로 반환한다 (`returns [] for invalid html` 규약).
|
|
127
|
+
- 네트워크 오류·재시도·타임아웃은 `HttpClient`의 책임. 파서와 매핑 계층까지 전파하지 않는다.
|
|
128
|
+
- **선택적 보정(hydration) 로직은 절대 예외를 던지지 않는다.** 보정 실패 시 기존
|
|
129
|
+
값을 유지하고 조용히 넘어감 (`catch { /* 기존 값 유지 */ }`). 사용자 데이터 손실 금지.
|
|
130
|
+
- catch 바인딩 값은 `unknown`으로 취급 — 가드 없이 사용하거나 무시하지 않는다.
|
|
131
|
+
빈 catch는 위 hydration 규칙이 적용되는 선택적 보정에서만 허용.
|
|
132
|
+
- 오류 메시지는 영문 한 줄로 무엇이 잘못됐는지 설명 (`'billNo is required'`).
|
|
133
|
+
원본 오류를 감쌀 때는 맥락과 원문을 함께 남긴다.
|
|
134
|
+
|
|
135
|
+
### 비동기 처리
|
|
136
|
+
|
|
137
|
+
- `async/await`만 사용, raw `.then()` 체이닝 금지.
|
|
138
|
+
- 모든 Promise는 `await`하거나 명시적으로 실패를 처리한다 —
|
|
139
|
+
floating promise 금지 (이 규칙은 린트로 강제되지 않으므로 특히 주의).
|
|
140
|
+
- 독립적인 요청은 `Promise.all`로 병렬화, 일부 실패를 허용해야 하면
|
|
141
|
+
`Promise.allSettled`.
|
|
142
|
+
- **외부 사이트 요청은 기본적으로 순서대로.** 폭주 방지는 `IBulkOptions`의
|
|
143
|
+
`delayMs` / `concurrency`로 조절하고 서버에 부하를 주는 기본값을 만들지 않는다.
|
|
144
|
+
|
|
145
|
+
### 문서·주석 동기화
|
|
146
|
+
|
|
147
|
+
- `src/`에서 `console.*` 사용 금지 (테스트 파일 제외).
|
|
148
|
+
- JSDoc/주석은 **한국어**로 public API와 사이트 특이사항(숨김 폼, AJAX 엔드포인트 등)을 설명.
|
|
149
|
+
이유: 대상 사이트가 국내 정부 시스템이라 도메인 맥락이 곧 문서.
|
|
150
|
+
public 메서드는 1줄 한국어 요약 + 필요 시 `@param` / `@returns`.
|
|
151
|
+
- import는 `import type { ... }`로 타입만 필요한 경우 분리, 상대경로에 확장자 없음.
|
|
152
|
+
Node 내장은 `'url'`, `'https'` 형태로 임포트.
|
|
153
|
+
- **새 public API는 반드시 `src/index.ts`에 export 등록** (누출 방지),
|
|
154
|
+
같은 PR에서 README.md의 메서드·타입 문서도 갱신한다.
|
|
155
|
+
|
|
156
|
+
## 에이전트 메모리 (`agent_memories/`)
|
|
157
|
+
|
|
158
|
+
여러 에이전트/세션이 **동일한 기억**을 읽고 쓰기 위한 공유 기록소.
|
|
159
|
+
형식의 단일 진실원은 `agent_memories/README.md`이며, 요약은 다음과 같다.
|
|
160
|
+
|
|
161
|
+
- **진입점**: `agent_memories/INDEX.md` — 작업 시작 시 먼저 확인.
|
|
162
|
+
- **유형별 폴더**: `decisions/`(결정과 이유), `tasks/`(작업 이력),
|
|
163
|
+
`site-notes/`(대상 사이트 DOM·엔드포인트 변화), `findings/`(버그·원인 분석).
|
|
164
|
+
- **레코드 형식**: 파일당 1주제, 파일명 `YYYY-MM-DD-<영문-kebab-slug>.md`, YAML frontmatter 필수
|
|
165
|
+
(`id`, `type`, `title`, `status`, `author`, `created`, `updated`, `tags`).
|
|
166
|
+
- **append-only**: 기존 레코드 본문을 고치지 않는다. 내용이 바뀌면 새 레코드를 만들고
|
|
167
|
+
`supersedes`로 연결한 뒤 이전 레코드는 `status: superseded`로 표시.
|
|
168
|
+
- **쓰기 후 반드시 `INDEX.md` 갱신** (같은 작업 안에서).
|
|
169
|
+
- 기록 대상: 커밋하기 어려운 결정과 근거, 사이트 구조 변화, 재현된 버그 원인,
|
|
170
|
+
되풀이되는 작업의 체크포인트. 매사 기록하지 말고 후속 에이전트가 물어볼 내용만.
|
|
171
|
+
- **중복 금지**: 규칙으로 확정된 사항은 AGENTS.md로 승격하고 메모리에는 배경·근거만 남긴다.
|
|
172
|
+
- **충돌 시 우선순위**: 지침(AGENTS.md)과 메모리 내용이 충돌하면 **AGENTS.md가 우선**한다 —
|
|
173
|
+
메모리는 규칙이 아니라 사실·배경 기록이다. 메모리가 사실이 바뀌어 정정되면
|
|
174
|
+
supersede 방식(새 레코드 + `supersedes` 연결)으로 갱신한다.
|
|
175
|
+
- 비밀·토큰·개인정보 기록 금지. git에 커밋해 공유하고, npm 게시는 제외(`.npmignore`).
|
|
176
|
+
|
|
177
|
+
## 테스트 컨벤션
|
|
178
|
+
|
|
179
|
+
- 위치: `src/<파일명>.test.ts` (소스 옆). `testMatch: **/*.test.ts`, ts-jest.
|
|
180
|
+
- 구조: `describe('클래스명')` → `describe('메서드명')` → `test('동작 설명', ...)`.
|
|
181
|
+
- 테스트 이름은 **영문**, 동사 시작, 동작 기술 (`returns [] for invalid html`).
|
|
182
|
+
- HTML fixture는 테스트 파일 상단에 템플릿 리터럴 상수로 인라인하고
|
|
183
|
+
`// ─── 섹션명 ───` 주석으로 구분한다. (실제 캡처를 축약한 형태)
|
|
184
|
+
- 네트워크는 절대 호출하지 않는다. httpClient 주입 후 캐스팅해 mock:
|
|
185
|
+
```ts
|
|
186
|
+
const httpClient = (instance as unknown as {
|
|
187
|
+
httpClient: { get: (url: URL) => Promise<string> };
|
|
188
|
+
}).httpClient;
|
|
189
|
+
jest.spyOn(httpClient, 'get').mockImplementation(async () => FIXTURE_HTML);
|
|
190
|
+
```
|
|
191
|
+
- 파서 변경 시 관련 fixture 테스트를 먼저 깨뜨려 회귀를 잡을 것 (대상 사이트 DOM 변경에 매우 민감).
|
|
192
|
+
|
|
193
|
+
## 브랜치 전략
|
|
194
|
+
|
|
195
|
+
| 브랜치 | 역할 |
|
|
196
|
+
|--------|------|
|
|
197
|
+
| `dev` | **로컬 작업 공간의 기본 시작점·복귀지.** 피처 브랜치 파생 기준. Renovate의 `baseBranches` |
|
|
198
|
+
| `main` | 공식 브랜치 (origin HEAD). **피처 병합 대상.** CI는 **PR → main**에서만 트리거됨 |
|
|
199
|
+
| `renovate/*` | Renovate 자동 생성 (`renovate/<scope>-<version>`, 예: `renovate/typescript-6.x`) |
|
|
200
|
+
|
|
201
|
+
**로컬 작업 라이프사이클** (`dev`가 기본 시작점):
|
|
202
|
+
|
|
203
|
+
1. **시작**: `git switch dev && git pull` — 작업은 항상 최신 `dev`에서 시작한다.
|
|
204
|
+
2. **파생**: `git switch -c feat/<요약>` — 피처 브랜치는 `dev`에서 파생한다.
|
|
205
|
+
3. **병합**: 작업 완료 후 PR로 **`main`에 병합**한다 (CI가 PR→main 전용이므로 병합 대상도 main).
|
|
206
|
+
`gh pr create --base main` → `gh pr merge <n> --merge`.
|
|
207
|
+
4. **복귀·동기화**: `main` 병합이 끝나면 **`dev`로 복귀해 동기화**하고 다음 작업을 시작한다:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
git switch dev && git pull
|
|
211
|
+
git merge main --no-edit # main에 병합된 내용을 dev에 반영
|
|
212
|
+
git push origin dev # 리모트 dev도 같은 상태로 유지
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- 기능/수정 브랜치 명명은 Conventional Commits 타입을 따름:
|
|
216
|
+
`feat/<요약>`, `fix/<요약>`, `chore/<요약>` (예: `feat/hydrate-likms-reason`).
|
|
217
|
+
요약은 영문 kebab-case.
|
|
218
|
+
- `pull.rebase = false` — 히스토리는 rebase가 아니라 **merge**로 유지.
|
|
219
|
+
|
|
220
|
+
## 커밋 메시지
|
|
221
|
+
|
|
222
|
+
Conventional Commits, 영문 주제문:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
feat: Hydrate empty proposalReason from 국회 의안정보시스템 (likms)
|
|
226
|
+
fix: Preserve line breaks in proposalReason
|
|
227
|
+
chore(deps): update all dependencies to latest compatible versions
|
|
228
|
+
refactor: Use typed httpClient for mocking in NsmLmSts tests
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- 사용 타입: `feat`, `fix`, `chore`, `chore(deps)`, `refactor`, `docs`, `test`, `build`.
|
|
232
|
+
- 소콜론 뒤는 대문자 시작. 한국어 도메인 용어(의안정보시스템 등)는 그대로 허용.
|
|
233
|
+
- 버전 커밋(`2.2.0` 형식의 bare version)은 `npm version`이 자동 생성 — 직접 작성하지 않음.
|
|
234
|
+
- **커밋 서명**: Git에 PGP(GPG) 서명키가 설정되어 있으면 **커밋을 서명한다**.
|
|
235
|
+
- 설정 여부 확인: `git config --get commit.gpgsign` / `git config --get user.signingkey`
|
|
236
|
+
(서명키가 있는데 `commit.gpgsign`이 꺼져 있으면 `git commit -S`로 서명하거나
|
|
237
|
+
`git config commit.gpgsign true`로 기본값으로 활성화)
|
|
238
|
+
- 서명키가 없는 환경에서는 서명 없이 커밋해도 된다 — 키 생성을 강제하지 않는다.
|
|
239
|
+
- `npm version`이 만드는 버전 커밋도 같은 설정을 따라 자동 서명된다.
|
|
240
|
+
|
|
241
|
+
## GitHub 리모트 작업은 `gh` CLI
|
|
242
|
+
|
|
243
|
+
에이전트가 작업하든 커맨드라인에서 작업하든, GitHub 리모트 관련 작업은
|
|
244
|
+
반드시 **GitHub CLI(`gh`)** 를 사용한다. 웹 UI 수작업이나 `git` 명령만으로
|
|
245
|
+
우회하지 않는 것을 원칙으로 함.
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
gh auth status # 인증 상태 확인 (실패 시 안내부터)
|
|
249
|
+
gh pr create --base main # PR 생성 (피처 병합 대상은 main)
|
|
250
|
+
gh pr view 123 # PR 조회 (상태/체크/리뷰)
|
|
251
|
+
gh pr checks --watch # CI 결과 확인
|
|
252
|
+
gh pr merge 123 --merge # merge commit으로 병합 (squash 금지, 아래 규칙)
|
|
253
|
+
gh pr list / gh issue list # 목록 조회
|
|
254
|
+
gh api ... # 없는 API는 gh api로 직접 호출
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- `gh`가 없거나 미인증이면 작업을 멈추고 사용자에게 설치/인증을 안내할 것
|
|
258
|
+
(자동 로그인 시도나 토큰 입력 요청 금지).
|
|
259
|
+
- 리모트 푸시·PR·이슈·릴리스 등 모든 원격 조작은 `gh` + `git` 조합으로 수행.
|
|
260
|
+
|
|
261
|
+
## PR/병합 규칙
|
|
262
|
+
|
|
263
|
+
1. **병합 방식은 merge commit** (squash 금지). 제목은 GitHub 기본 형식을 따른다:
|
|
264
|
+
`Merge pull request #N from vientorepublic/<branch>`.
|
|
265
|
+
2. PR 전 게이트를 로컬에서 통과시키고 올린다:
|
|
266
|
+
`npm run typecheck && npm run lint && npm test && npm run build`.
|
|
267
|
+
⚠️ CI workflow는 `pull_request: branches: [main]` 전용 — main 대상 PR은 CI가 도우며
|
|
268
|
+
**dev 대상 PR(현재 사실상 Renovate)은 CI가 돌지 않는다.** 어느 쪽이든 로컬 게이트는
|
|
269
|
+
병합 전 필수이고, dev 대상 PR에서는 로컬 검증이 유일한 방어선이므로 생략 금지.
|
|
270
|
+
3. PR 본문에 변경 유형(feat/fix/…)과 영향받는 사이트/파서를 명시.
|
|
271
|
+
4. Renovate PR: 자동 브랜치이며 베이스는 `dev` (renovate.json 설정 — 피처는 main이 대상이어도 Renovate만 dev는 유지).
|
|
272
|
+
병합 후 dev 동기화 루틴에서 함께 최신화하며, 의존성 업데이트는 Renovate에 맡긴다 —
|
|
273
|
+
수동으로 `package.json`을 올리지 않는 것을 원칙으로 함 (잠금파일 함께 커밋).
|
|
274
|
+
5. 하나의 PR은 하나의 목적. 파서 로직 변경과 리팩터링/포맷팅을 섞지 않는다.
|
|
275
|
+
|
|
276
|
+
## 릴리스
|
|
277
|
+
|
|
278
|
+
### 배포 지시 절차
|
|
279
|
+
|
|
280
|
+
사용자가 **배포**를 지시하면 아래 순서를 따른다. 각 단계의 성공을 확인한 뒤 다음 단계로 진행한다.
|
|
281
|
+
|
|
282
|
+
1. **버전 분석 후 버전 범프** — 직전 태그 이후의 변경으로 major/minor/patch를 판단한다:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
git log $(git describe --tags --abbrev=0)..HEAD --oneline
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
- `fix:` / `chore:`만 있으면 **patch**, `feat:` (하위 호환 유지)는 **minor**,
|
|
289
|
+
공개 API 제거·시그니처 변경 등 기존 사용자 코드가 깨지는 변경은 **major**.
|
|
290
|
+
- 판단 근거(어떤 타입의 커밋이 포함됐는지)를 PR 본문에 남긴다.
|
|
291
|
+
- `npm version <major|minor|patch>`로 범프 — `package.json` 버전 커밋(예: `2.3.0`)과
|
|
292
|
+
태그(`v2.3.0`)가 기존 패턴대로 자동 생성된다. 버전을 직접 편집해 태그를 별도로 만드는 방식은 쓰지 않는다.
|
|
293
|
+
- 브랜치는 `dev`에서 파생한 `chore/release-v2.3.0` 형태 (브랜치 전략의 라이프사이클).
|
|
294
|
+
2. **lockfile 동기화** — `npm install`을 1회 실행해 `package-lock.json`이 새 버전과
|
|
295
|
+
일치하는지 확정하고 함께 커밋한다. (`npm version`이 이미 동기화했다면 차이 없음.)
|
|
296
|
+
※ 개발 명령어의 "`npm install` 금지"는 **설치** 목적 규칙이고, 여기는 lockfile **갱신** 목적이므로 예외다.
|
|
297
|
+
3. **PR 생성 (`gh`)**
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
git push -u origin chore/release-v2.3.0
|
|
301
|
+
gh pr create --base main --title "chore: Release v2.3.0" --body "..."
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- PR 본문에 버전 판단 근거와 변경 요약을 포함한다 (PR/병합 규칙 3번 참조).
|
|
305
|
+
4. **최종 검토 후 병합** — 아래를 전부 통과하고 문제가 없으면 병합한다:
|
|
306
|
+
- 로컬 게이트: **PR/병합 규칙 2번과 동일** — `npm run typecheck && npm run lint && npm test && npm run build`
|
|
307
|
+
- 형식: `npm run format` 실행 후 달라진 파일을 커밋에 포함
|
|
308
|
+
- GitHub CI/Checks: `gh pr checks --watch` 로 Test/Build 워크플로 성공 확인
|
|
309
|
+
- PR diff 최종 리뷰 (버전·lockfile 외에 불필요한 변경이 섞이지 않았는지)
|
|
310
|
+
- 문제가 있으면 병합하지 말고 수정한 뒤 이 단계를 다시 돈다.
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
gh pr merge <n> --merge # merge commit, squash 금지
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
5. **릴리스 생성** — 기존 GitHub 릴리즈 패턴과 일관된 구조로 만든다
|
|
317
|
+
(태그 `vX.Y.Z`, 제목 `release: vX.Y.Z`, 자동 노트):
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
git switch main && git pull
|
|
321
|
+
git push origin --follow-tags # 버전 태그 푸시
|
|
322
|
+
gh release create vX.Y.Z --title "release: vX.Y.Z" --generate-notes
|
|
323
|
+
npm publish # 게시 (prepublishOnly: lint → test → build)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- `--generate-notes`가 기존처럼 `**Full Changelog**: .../compare/vA...vB` 비교 링크를 포함한다.
|
|
327
|
+
- 릴리스·게시가 끝나면 **`dev`로 복귀해 동기화**한다 (라이프사이클 4단계).
|
|
328
|
+
|
|
329
|
+
### 기존 릴리스 스크립트 (같은 게이트 사용)
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npm run release:patch # npm version patch && npm publish
|
|
333
|
+
npm run release:minor
|
|
334
|
+
npm run release:major
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- `prepack`이 clean → build를, `prepublishOnly`가 lint → test → build를 자동 실행.
|
|
338
|
+
- 게시 전 `npm run release:dry`로 내용물 확인.
|
|
339
|
+
- 다만 사용자가 배포를 지시한 경우에는 위 **배포 지시 절차**를 우선한다.
|
|
340
|
+
|
|
341
|
+
## 주의사항
|
|
342
|
+
|
|
343
|
+
- `http-client.ts`의 인코딩 폴백(UTF-8 선언 ↔ cp949 실제 응답) 로직은 사이트별
|
|
344
|
+
예외 대응이므로 임의로 제거하지 말 것.
|
|
345
|
+
- Puppeteer 스크린샷은 선택 기능 (`screenshot` 설정). 기본 경로는 HTTP + cheerio 파싱이며
|
|
346
|
+
스크린샷 때문에 무거운 브라우저 의존을 기본 경로에 도입하지 말 것.
|
|
347
|
+
- `.DS_Store` 등 로컬 파일과 에이전트 상태 파일(`.freebuff/` 등)은 커밋하지 않음 — `.gitignore` 패턴이 관리한다.
|
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
- [getAllPages (NsmLmSts)](#getallpagesquery-options--asyncgeneratorinsmsearchresult)
|
|
41
41
|
- [getAllPendingPages](#getallpendingpagesquery-options--asyncgeneratorinsmsearchresult)
|
|
42
42
|
- [Screenshot (NsmLmSts)](#getnsmlisscreenshotquery--promisebuffer)
|
|
43
|
+
- [LikmsCrawler - 국회 의안정보시스템 제안이유 조회](#likmscrawler---국회-의안정보시스템-제안이유-조회)
|
|
43
44
|
- [Examples](#examples)
|
|
44
45
|
- [License](#license)
|
|
45
46
|
|
|
@@ -73,6 +74,7 @@ interface PalCrawlConfig {
|
|
|
73
74
|
customHeaders?: Record<string, string>; // 사용자 정의 헤더
|
|
74
75
|
screenshot?: ScreenshotOptions; // 헤드리스 크로뮴 스크린샷 옵션
|
|
75
76
|
hydrateTruncatedTitles?: boolean; // 잘린 제목 상세 조회 여부 (기본값: false)
|
|
77
|
+
hydrateProposalReason?: boolean; // 제안이유 빈 값일 때 의안정보시스템 보정 (기본값: true)
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
interface ScreenshotOptions {
|
|
@@ -151,6 +153,8 @@ interface IContentData {
|
|
|
151
153
|
}
|
|
152
154
|
```
|
|
153
155
|
|
|
156
|
+
PAL 상세 페이지에서 `proposalReason`이 비어 있는 의안이 있을 수 있습니다. 이때는 기본값(`hydrateProposalReason: true`)으로 국회 의안정보시스템(likms.assembly.go.kr)의 같은 의안에서 제안이유를 자동으로 보정합니다. 보정에 실패하더라도 기존 결과(`null`)를 그대로 유지하며 오류를 던지지 않습니다.
|
|
157
|
+
|
|
154
158
|
`ISearchResult`는 검색/페이지 조회 결과를 나타내는 인터페이스입니다.
|
|
155
159
|
|
|
156
160
|
```typescript
|
|
@@ -765,6 +769,34 @@ try {
|
|
|
765
769
|
|
|
766
770
|
---
|
|
767
771
|
|
|
772
|
+
## LikmsCrawler - 국회 의안정보시스템 제안이유 조회
|
|
773
|
+
|
|
774
|
+
`LikmsCrawler`는 [국회 의안정보시스템](https://likms.assembly.go.kr)의 의안상세 페이지에서 제안이유 및 주요내용을 조회합니다. 의안상세 페이지는 심사정보 탭을 AJAX(POST)로 채우므로, 숨김 폼 파라미터를 추출해 `billInfo.do`에 POST한 뒤 `#prntSummary`에서 제안이유를 파싱합니다.
|
|
775
|
+
|
|
776
|
+
PAL의 `contentId`(`PRC_...`)가 이 시스템의 `billId`와 동일하므로 그대로 전달할 수 있습니다.
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
import { LikmsCrawler } from 'pal-crawl';
|
|
780
|
+
|
|
781
|
+
const likms = new LikmsCrawler();
|
|
782
|
+
|
|
783
|
+
// 제안이유 조회 (실패 시 null, 절대 예외를 던지지 않음)
|
|
784
|
+
const reason = await likms.getProposalReason(
|
|
785
|
+
'PRC_G2G6E0D9E2Z3A1Y7Z5X1Y0W4E9E6D5',
|
|
786
|
+
);
|
|
787
|
+
console.log(reason);
|
|
788
|
+
|
|
789
|
+
// 파싱만 수행하고 싶은 경우
|
|
790
|
+
const fragmentHtml = await likms.getBillInfoHTML(
|
|
791
|
+
likms.parseDetailFormParams(await likms.getDetailPageHTML(billId)),
|
|
792
|
+
);
|
|
793
|
+
console.log(likms.parseProposalReason(fragmentHtml));
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
`PalCrawl`의 `getContent`/`getDoneContent`는 내부적으로 이 크롤러를 사용해 빈 `proposalReason`을 보정합니다(`hydrateProposalReason: false`로 비활성화 가능).
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
768
800
|
## Examples
|
|
769
801
|
|
|
770
802
|
### 기본 사용법
|
package/dist/config.d.ts
CHANGED
|
@@ -6,5 +6,9 @@ export declare enum Config {
|
|
|
6
6
|
DONE_CONTENT_URL = "/napal/lgsltpa/lgsltpaDone/view.do",
|
|
7
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",
|
|
8
8
|
NSM_DOMAIN = "https://opinion.lawmaking.go.kr",
|
|
9
|
-
NSM_LIST_URL = "/gcom/nsmLmSts/out"
|
|
9
|
+
NSM_LIST_URL = "/gcom/nsmLmSts/out",
|
|
10
|
+
LIKMS_DOMAIN = "https://likms.assembly.go.kr",
|
|
11
|
+
LIKMS_DETAIL_PAGE_URL = "/bill/bi/billDetailPage.do",
|
|
12
|
+
LIKMS_BILL_INFO_URL = "/bill/bi/bill/detail/billInfo.do",
|
|
13
|
+
LIKMS_CURR_MENU_NO = "2600044"
|
|
10
14
|
}
|
package/dist/config.js
CHANGED
|
@@ -12,5 +12,10 @@ var Config;
|
|
|
12
12
|
// 국민참여입법센터 국회입법현황 (opinion.lawmaking.go.kr)
|
|
13
13
|
Config["NSM_DOMAIN"] = "https://opinion.lawmaking.go.kr";
|
|
14
14
|
Config["NSM_LIST_URL"] = "/gcom/nsmLmSts/out";
|
|
15
|
+
// 국회 의안정보시스템 (likms.assembly.go.kr)
|
|
16
|
+
Config["LIKMS_DOMAIN"] = "https://likms.assembly.go.kr";
|
|
17
|
+
Config["LIKMS_DETAIL_PAGE_URL"] = "/bill/bi/billDetailPage.do";
|
|
18
|
+
Config["LIKMS_BILL_INFO_URL"] = "/bill/bi/bill/detail/billInfo.do";
|
|
19
|
+
Config["LIKMS_CURR_MENU_NO"] = "2600044";
|
|
15
20
|
})(Config || (exports.Config = Config = {}));
|
|
16
21
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAAA,IAAY,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAAA,IAAY,MAeX;AAfD,WAAY,MAAM;IAChB,+CAAqC,CAAA;IACrC,4DAAkD,CAAA;IAClD,+DAAqD,CAAA;IACrD,8DAAoD,CAAA;IACpD,iEAAuD,CAAA;IACvD,uIAA6H,CAAA;IAC7H,4CAA4C;IAC5C,wDAA8C,CAAA;IAC9C,6CAAmC,CAAA;IACnC,oCAAoC;IACpC,uDAA6C,CAAA;IAC7C,8DAAoD,CAAA;IACpD,kEAAwD,CAAA;IACxD,wCAA8B,CAAA;AAChC,CAAC,EAfW,MAAM,sBAAN,MAAM,QAejB"}
|
package/dist/http-client.d.ts
CHANGED
|
@@ -16,4 +16,10 @@ export declare class HttpClient {
|
|
|
16
16
|
private decodeBody;
|
|
17
17
|
private makeRequest;
|
|
18
18
|
get(url: URL): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* URL-encoded 본문을 POST로 전송합니다.
|
|
21
|
+
* 국회 의안정보시스템의 일부 조각(_fragment) 엔드포인트는 GET이 아닌 POST만 허용합니다.
|
|
22
|
+
*/
|
|
23
|
+
post(url: URL, body: string): Promise<string>;
|
|
24
|
+
private requestWithRetry;
|
|
19
25
|
}
|
package/dist/http-client.js
CHANGED
|
@@ -100,17 +100,23 @@ class HttpClient {
|
|
|
100
100
|
}
|
|
101
101
|
return body.toString('utf8');
|
|
102
102
|
}
|
|
103
|
-
async makeRequest(url) {
|
|
103
|
+
async makeRequest(url, method = 'GET', body) {
|
|
104
104
|
return new Promise((resolve, reject) => {
|
|
105
105
|
const headers = {
|
|
106
106
|
'User-Agent': this.userAgent,
|
|
107
107
|
...this.customHeaders,
|
|
108
108
|
};
|
|
109
|
+
if (method === 'POST' && body !== undefined) {
|
|
110
|
+
headers['Content-Type'] =
|
|
111
|
+
'application/x-www-form-urlencoded; charset=UTF-8';
|
|
112
|
+
headers['Content-Length'] = String(Buffer.byteLength(body));
|
|
113
|
+
}
|
|
109
114
|
const options = {
|
|
110
115
|
headers,
|
|
111
116
|
timeout: this.timeout,
|
|
117
|
+
method,
|
|
112
118
|
};
|
|
113
|
-
const req = https.
|
|
119
|
+
const req = https.request(url, options, (res) => {
|
|
114
120
|
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
115
121
|
reject(new Error(`Invalid response: ${res.statusCode} ${res.statusMessage}`));
|
|
116
122
|
return;
|
|
@@ -120,8 +126,8 @@ class HttpClient {
|
|
|
120
126
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
121
127
|
});
|
|
122
128
|
res.on('end', () => {
|
|
123
|
-
const
|
|
124
|
-
resolve(this.decodeBody(
|
|
129
|
+
const responseBody = Buffer.concat(chunks);
|
|
130
|
+
resolve(this.decodeBody(responseBody, res.headers['content-type']));
|
|
125
131
|
});
|
|
126
132
|
});
|
|
127
133
|
req.on('timeout', () => {
|
|
@@ -131,14 +137,28 @@ class HttpClient {
|
|
|
131
137
|
req.on('error', (err) => {
|
|
132
138
|
reject(err);
|
|
133
139
|
});
|
|
140
|
+
if (method === 'POST' && body !== undefined) {
|
|
141
|
+
req.write(body);
|
|
142
|
+
}
|
|
143
|
+
req.end();
|
|
134
144
|
req.setTimeout(this.timeout);
|
|
135
145
|
});
|
|
136
146
|
}
|
|
137
147
|
async get(url) {
|
|
148
|
+
return this.requestWithRetry(url, 'GET');
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* URL-encoded 본문을 POST로 전송합니다.
|
|
152
|
+
* 국회 의안정보시스템의 일부 조각(_fragment) 엔드포인트는 GET이 아닌 POST만 허용합니다.
|
|
153
|
+
*/
|
|
154
|
+
async post(url, body) {
|
|
155
|
+
return this.requestWithRetry(url, 'POST', body);
|
|
156
|
+
}
|
|
157
|
+
async requestWithRetry(url, method, body) {
|
|
138
158
|
let lastError;
|
|
139
159
|
for (let attempt = 1; attempt <= this.retryCount; attempt++) {
|
|
140
160
|
try {
|
|
141
|
-
return await this.makeRequest(url);
|
|
161
|
+
return await this.makeRequest(url, method, body);
|
|
142
162
|
}
|
|
143
163
|
catch (error) {
|
|
144
164
|
lastError = error;
|
package/dist/http-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-client.js","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"http-client.js","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { ScreenshotBase, PalCrawl, NsmLmSts, type ITableData, type IAttachment, type IContentData, type ISearchResult, type ISearchQuery, type IBulkOptions, type PalCrawlConfig, type ScreenshotOptions, type INsmSearchQuery, type INsmSearchResult, type INsmBillItem, type INsmBillDetail, type INsmAttachment, type NsmProgressStatus, type NsmProposerType, type NsmResolutionStatus, } from './pal';
|
|
2
2
|
export { PalParser, NsmLmStsParser } from './parser';
|
|
3
|
+
export { LikmsCrawler, type LikmsCrawlerConfig } from './likms';
|
|
3
4
|
export { Config } from './config';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Config = exports.NsmLmStsParser = exports.PalParser = exports.NsmLmSts = exports.PalCrawl = exports.ScreenshotBase = void 0;
|
|
3
|
+
exports.Config = exports.LikmsCrawler = exports.NsmLmStsParser = exports.PalParser = exports.NsmLmSts = exports.PalCrawl = exports.ScreenshotBase = void 0;
|
|
4
4
|
var pal_1 = require("./pal");
|
|
5
5
|
Object.defineProperty(exports, "ScreenshotBase", { enumerable: true, get: function () { return pal_1.ScreenshotBase; } });
|
|
6
6
|
Object.defineProperty(exports, "PalCrawl", { enumerable: true, get: function () { return pal_1.PalCrawl; } });
|
|
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "NsmLmSts", { enumerable: true, get: function ()
|
|
|
8
8
|
var parser_1 = require("./parser");
|
|
9
9
|
Object.defineProperty(exports, "PalParser", { enumerable: true, get: function () { return parser_1.PalParser; } });
|
|
10
10
|
Object.defineProperty(exports, "NsmLmStsParser", { enumerable: true, get: function () { return parser_1.NsmLmStsParser; } });
|
|
11
|
+
var likms_1 = require("./likms");
|
|
12
|
+
Object.defineProperty(exports, "LikmsCrawler", { enumerable: true, get: function () { return likms_1.LikmsCrawler; } });
|
|
11
13
|
var config_1 = require("./config");
|
|
12
14
|
Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return config_1.Config; } });
|
|
13
15
|
//# 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,6BAoBe;AAnBb,qGAAA,cAAc,OAAA;AACd,+FAAA,QAAQ,OAAA;AACR,+FAAA,QAAQ,OAAA;AAkBV,mCAAqD;AAA5C,mGAAA,SAAS,OAAA;AAAE,wGAAA,cAAc,OAAA;AAClC,mCAAkC;AAAzB,gGAAA,MAAM,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6BAoBe;AAnBb,qGAAA,cAAc,OAAA;AACd,+FAAA,QAAQ,OAAA;AACR,+FAAA,QAAQ,OAAA;AAkBV,mCAAqD;AAA5C,mGAAA,SAAS,OAAA;AAAE,wGAAA,cAAc,OAAA;AAClC,iCAAgE;AAAvD,qGAAA,YAAY,OAAA;AACrB,mCAAkC;AAAzB,gGAAA,MAAM,OAAA"}
|
package/dist/likms.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface LikmsCrawlerConfig {
|
|
2
|
+
userAgent?: string;
|
|
3
|
+
timeout?: number;
|
|
4
|
+
retryCount?: number;
|
|
5
|
+
customHeaders?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 국회 의안정보시스템(likms.assembly.go.kr) 크롤러.
|
|
9
|
+
*
|
|
10
|
+
* 의안상세 페이지(billDetailPage.do)는 심사정보 탭 내용을 빈 div에 AJAX(POST)로
|
|
11
|
+
* 채우므로, 정적 HTML만으로는 제안이유를 읽을 수 없다.
|
|
12
|
+
*
|
|
13
|
+
* 실제 흐름 (billDetail.js 기준):
|
|
14
|
+
* 1. GET /bill/bi/billDetailPage.do?billId=...&currMenuNo=2600044
|
|
15
|
+
* → 숨김 폼(#form)에 billId, billNo, billKindCd 등 파라미터 포함
|
|
16
|
+
* 2. POST /bill/bi/bill/detail/billInfo.do (폼 파라미터 직렬화)
|
|
17
|
+
* → 심사정보 탭 조각 HTML 반환
|
|
18
|
+
* 3. 조각의 <pre id="prntSummary"> 안에 제안이유 및 주요내용이 들어 있음
|
|
19
|
+
*
|
|
20
|
+
* PAL(입법예고) 상세 페이지에서 제안이유가 비어 있는 의안의 contentId(=PRC_...)
|
|
21
|
+
* 는 동일한 ID로 이 시스템에서 조회할 수 있다.
|
|
22
|
+
*/
|
|
23
|
+
export declare class LikmsCrawler {
|
|
24
|
+
private readonly httpClient;
|
|
25
|
+
constructor(config?: LikmsCrawlerConfig);
|
|
26
|
+
private buildDetailPageUrl;
|
|
27
|
+
/** 의안상세 페이지 HTML을 반환합니다. */
|
|
28
|
+
getDetailPageHTML(billId: string): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* 의안상세 페이지의 숨김 폼(#form input[type=hidden]) 파라미터를 추출합니다.
|
|
31
|
+
* billInfo.do AJAX 요청은 이 파라미터들에 심사정보 섹션의 렌더링 조건이 걸려
|
|
32
|
+
* 있어 원본 페이지에서 그대로 가져와야 한다.
|
|
33
|
+
*/
|
|
34
|
+
parseDetailFormParams(html: string): Record<string, string>;
|
|
35
|
+
/** 심사정보 탭 조각 HTML을 POST로 반환합니다. */
|
|
36
|
+
getBillInfoHTML(params: Record<string, string>): Promise<string>;
|
|
37
|
+
/** 심사정보 탭 조각 HTML에서 제안이유 및 주요내용을 파싱하여 반환합니다. */
|
|
38
|
+
parseProposalReason(fragmentHtml: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* billId(PAL contentId와 동일)로 제안이유 및 주요내용을 조회합니다.
|
|
41
|
+
* 네트워크·파싱 실패를 절대 던지지 않고 null을 반환해 호출부가 안전하게
|
|
42
|
+
* 동작하도록 한다.
|
|
43
|
+
*/
|
|
44
|
+
getProposalReason(billId: string): Promise<string | null>;
|
|
45
|
+
}
|