korean-law-mcp 3.2.3 → 3.5.3
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 +85 -5
- package/build/index.js +3 -6
- package/build/lib/api-client.d.ts +3 -2
- package/build/lib/api-client.js +8 -6
- package/build/lib/article-parser.d.ts +1 -0
- package/build/lib/article-parser.js +18 -0
- package/build/lib/decision-compact.d.ts +68 -0
- package/build/lib/decision-compact.js +229 -0
- package/build/lib/fetch-with-retry.d.ts +6 -0
- package/build/lib/fetch-with-retry.js +15 -3
- package/build/lib/law-search.d.ts +30 -0
- package/build/lib/law-search.js +122 -0
- package/build/lib/query-router.js +62 -0
- package/build/lib/search-normalizer.js +187 -0
- package/build/lib/session-state.d.ts +8 -5
- package/build/lib/session-state.js +5 -21
- package/build/lib/tool-profiles.d.ts +12 -9
- package/build/lib/tool-profiles.js +36 -38
- package/build/server/http-server.d.ts +6 -3
- package/build/server/http-server.js +84 -224
- package/build/server/sse-server.js +0 -1
- package/build/tool-registry.d.ts +1 -2
- package/build/tool-registry.js +26 -15
- package/build/tools/admin-appeals.d.ts +1 -0
- package/build/tools/admin-appeals.js +5 -1
- package/build/tools/chains.js +3 -109
- package/build/tools/constitutional-decisions.d.ts +1 -0
- package/build/tools/constitutional-decisions.js +12 -6
- package/build/tools/easy-law.d.ts +88 -0
- package/build/tools/easy-law.js +355 -0
- package/build/tools/meta-tools.js +47 -1
- package/build/tools/ordinance-search.js +9 -2
- package/build/tools/precedents.d.ts +1 -0
- package/build/tools/precedents.js +11 -3
- package/build/tools/search.js +27 -6
- package/build/tools/unified-decisions.d.ts +3 -2
- package/build/tools/unified-decisions.js +33 -22
- package/build/tools/verify-citations.d.ts +31 -0
- package/build/tools/verify-citations.js +201 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Korean Law MCP
|
|
2
2
|
|
|
3
|
-
**법제처 41개 API를
|
|
3
|
+
**법제처 41개 API를 16개 도구로.** 법령, 판례, 행정규칙, 자치법규, 조약, 해석례 + **LLM 환각 방지 인용 검증**을 AI 어시스턴트나 터미널에서 바로 사용.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/korean-law-mcp)
|
|
6
6
|
[](https://modelcontextprotocol.io)
|
|
@@ -14,7 +14,29 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## v3.
|
|
17
|
+
## v3.5 — AI 법률 답변의 환각을 잡아내다
|
|
18
|
+
|
|
19
|
+
**LLM이 지어낸 가짜 조문을 실시간으로 탐지.** 법제처 공식 DB로 모든 인용을 교차검증.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
"민법 제750조에 따라 불법행위 손해배상을 청구하고,
|
|
23
|
+
근로기준법 제60조 제1항은 연차유급휴가를 규정하며,
|
|
24
|
+
상법 제401조의2 제7항에 따라 이사 책임을 물을 수 있고,
|
|
25
|
+
형법 제9999조는 가중처벌을 정한다"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
→ `verify_citations` 한 번으로 (실제 법제처 API 교차검증 결과):
|
|
29
|
+
|
|
30
|
+
- ✓ 민법 제750조(불법행위의 내용) 실존
|
|
31
|
+
- ✓ 근로기준법 제60조(연차 유급휴가) 제1항 실존
|
|
32
|
+
- ✗ **상법 제401조의2 — 제7항 없음 (최대 제2항)**
|
|
33
|
+
- ✗ **형법 제9999조 — 해당 조문 없음 (존재 범위: 제1조~제372조)**
|
|
34
|
+
|
|
35
|
+
**ChatGPT·Claude가 쓴 법률 답변을 그대로 믿지 마세요.** 법률 AI 서비스, 로펌, 학생, 계약서 검토에서 신뢰도 체크 필수.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## v3.2.0+ — 자연어로 복합 분석
|
|
18
40
|
|
|
19
41
|
사용법은 똑같습니다. **그냥 자연어로 물어보세요.** AI가 질문을 알아듣고, 필요한 분석을 자동으로 추가해줍니다.
|
|
20
42
|
|
|
@@ -81,11 +103,69 @@
|
|
|
81
103
|
> 모든 결과 끝에 **"이어서 할 수 있는 조회"**가 제안됩니다. 복사해서 바로 이어가세요.
|
|
82
104
|
|
|
83
105
|
<details>
|
|
84
|
-
<summary>v3.2.1~v3.
|
|
106
|
+
<summary>v3.2.1~v3.5.3 변경 이력</summary>
|
|
107
|
+
|
|
108
|
+
**v3.5.3** — `verify_citations` 실증 검증 후 3개 치명 버그 수정
|
|
109
|
+
|
|
110
|
+
실제 법제처 API로 5건 테스트 → false negative 3건 발견 → 근본 원인 수정:
|
|
111
|
+
|
|
112
|
+
- **"민법" → "난민법" 부분매칭 오매칭** — 기존 `chains.ts`의 `findLaws`/`scoreLawRelevance`가 이미 해결해둔 로직인데 verify_citations가 재사용하지 않고 자체 로직으로 중복 구현했던 것. 공용 모듈 `lib/law-search.ts`로 추출하여 양쪽 재사용 (중복 제거)
|
|
113
|
+
- **원숫자(①②③…) 항번호 파싱 실패** — 법제처 API가 `항번호`를 `"① "` 형태로 리턴하는데 기존 `parseInt(raw.replace(/[^\d]/g, ""))`가 유니코드 원숫자를 제거해 NaN. 근로기준법 제60조 제1항이 실존함에도 "최대 제0항" 오판정 → `lib/article-parser.ts`에 `parseHangNumber()` 원숫자 매핑 유틸 추가
|
|
114
|
+
- **짧은 법령명 검색 누락** — 법제처 lawSearch API가 `display=20`에서 "상법"을 결과 34번째로 리턴. `apiClient.searchLaw`에 display 파라미터 추가, verify_citations는 `searchDisplay=100`으로 호출
|
|
115
|
+
|
|
116
|
+
검증 후 5/5 정확 판정 (위 예시 결과가 그 출력).
|
|
117
|
+
|
|
118
|
+
**v3.5.2** — kordoc 2.3.0 → 2.4.0 업데이트 (별표/서식 파싱 엔진)
|
|
119
|
+
|
|
120
|
+
**v3.5.1** — lite/full 프로필 체계 제거 (V3_EXPOSED 16개 고정 노출 도입 후 실질 미사용). `tool-profiles.ts`에서 `LITE_TOOLS`/`parseProfile`/`filterToolsByProfile` 제거, 헬스 엔드포인트 거짓 `profiles` 필드 → 정확한 `tools: { exposed: 16, total: 92 }` 로 교체. Breaking change 아님 (`?profile=lite`도 이미 무시되던 값)
|
|
121
|
+
|
|
122
|
+
**v3.5.0** — Killer feature: `verify_citations` 인용 검증 + Critical 핫픽스 + 보안 강화
|
|
123
|
+
|
|
124
|
+
- **`verify_citations`** 신규 — LLM 환각 방지. 사용자 텍스트에서 조문 인용 정규식 추출 + 직전 30자 lookback으로 법령명 역추적 + 법제처 DB 병렬 교차검증. 결과: ✓(실존) / ✗(없음, 존재 범위 제시) / ⚠(법령명 불명확)
|
|
125
|
+
- **Critical 핫픽스** — v3.4.0 `full` 파라미터가 12개 도메인(tax_tribunal, customs, ftc, pipc, nlrc, acr, treaty, interpretation 등)에서 스키마에 필드가 없어 묵묵히 무시되던 문제 수정. `unified-decisions.ts`가 하위 핸들러 응답을 받은 뒤 `compactLongSections()` 후처리로 계단식 축약 일괄 적용
|
|
126
|
+
- **보안 High 2건** — `fetch-with-retry.ts` 타임아웃/네트워크 에러에 API 키 포함 URL이 로그로 유출되던 문제 → `maskSensitiveUrl()`로 `OC=***` 마스킹. `trust proxy true` → `TRUST_PROXY` 환경변수(기본 `1`), X-Forwarded-For 스푸핑 rate limit 우회 차단
|
|
127
|
+
- **품질 3건** — `decision-compact.ts` 날짜 정규식 경계 가드, TAIL 경계 `". "` 오탐 제거, `stripRepeatedSummary` 종료점 정확 탐지
|
|
128
|
+
- **UX** — 체인 8개 description 구체화(LLM이 체인 선택 가능), 검색 결과 "💡 다음: get_law_text(...)" 힌트, `search_law` 약칭/오타 확장 자동 재시도, `query-router` 패턴 5개 추가, `discover_tools` 별칭 매칭 27개
|
|
129
|
+
|
|
130
|
+
**v3.4.0** — 판례 응답 토큰 평균 74% 감축 + `get_decision_text`에 `full` 파라미터 추가
|
|
131
|
+
|
|
132
|
+
법령 RAG 관점에서 판례 응답 구조를 재해석: 판시사항·판결요지·주문은 규범 재사용의 핵심이라 full 유지, "이유" 전문은 사안별 사실관계 나열이라 LLM이 대부분 소비만 하고 버림. 이 비대칭을 활용해 판례/헌재/행심(`precedent`/`constitutional`/`admin_appeal`) 3개 도메인에 **계단식 축약 + structured ref densify** 적용. `lib/decision-compact.ts` 신규:
|
|
133
|
+
|
|
134
|
+
- **`compactBody`** — 전문/이유 섹션을 앞 800자 + 중략 마커 + 뒤 400자로 축약. 판결 종결어미(`~다.`, `~라 할 것이다.`)와 문장 경계 가드 내장. `minSave` 가드로 짧은 본문(1300자 이하)은 skip
|
|
135
|
+
- **`densifyLawRefs`** — 참조조문의 괄호 설명 제거 (`제390조(채무불이행과 손해배상)` → `제390조`). 평균 40~55% 절감
|
|
136
|
+
- **`densifyPrecedentRefs`** — 참조판례의 "선고"/"판결" 제거 + 날짜 공백 압축 (`2020. 3. 26. 선고 2018두56077 판결` → `2020.3.26. 2018두56077`)
|
|
137
|
+
- **`stripRepeatedSummary`** — 법제처 API가 판시/요지를 본문 앞쪽에 또 섞어 보내는 케이스 탐지·제거
|
|
138
|
+
|
|
139
|
+
`get_decision_text`에 `full?: boolean` 파라미터 추가. 미지정(기본)=축약, `true`=전문. 응답 중간의 `⋯ 중략 N자 (full=true로 전문 조회) ⋯` 마커가 재호출 힌트 역할.
|
|
140
|
+
|
|
141
|
+
**실측 (실제 법제처 API, 고정 ID 8건)**:
|
|
142
|
+
|
|
143
|
+
| 도메인 | Before avg | After avg | 절감 |
|
|
144
|
+
|---|---:|---:|---:|
|
|
145
|
+
| 판례 | 5,230 chars | 3,049 chars | **-42%** |
|
|
146
|
+
| 헌재 | 8,368 chars | 1,703 chars | **-80%** |
|
|
147
|
+
| 행심 | 8,429 chars | 1,491 chars | **-82%** |
|
|
148
|
+
| **종합** | **7,606 chars (1,901 tok)** | **1,960 chars (490 tok)** | **-74%** |
|
|
149
|
+
|
|
150
|
+
긴 결정례(15,000자↑)에서 **80~89%** 절감이 가장 두드러짐. 짧은 본문은 `minSave` 가드로 원본 유지. 품질 손실 없음 (판시·요지·주문은 항상 full).
|
|
151
|
+
|
|
152
|
+
부가로 **ListTools 페이로드도 -14%** (9,671 → 8,296 bytes, 344 토큰↓): `chain_*` 8개 description 간결화, `search_decisions`/`get_decision_text` 필드 describe에서 17 도메인 이중 기재 제거.
|
|
153
|
+
|
|
154
|
+
**v3.3.1** — 법령 약칭 사전 대폭 확장 (11 → 52개, +41)
|
|
155
|
+
|
|
156
|
+
lexdiff에서 "산안기준규칙" 질의가 법제처 aiSearch의 키워드 부분매칭으로 **국가표준기본법**으로 환각되던 사례가 발견돼 `resolveLawAlias`의 `LAW_ALIAS_ENTRIES`를 대폭 보강. 다빈도 노무/안전(산안법·중처법·근기법 등), 개인정보/정보통신(개보법·정보통신망법), 청렴/이해충돌(청탁금지법·이해충돌방지법), 공공계약(국가계약법·지방계약법), 부동산/임대차(주임법·상임법·부거법), 공정거래(공정거래법·하도급법·약관법·표시광고법·가맹사업법), 금융(자본시장법·특금법·전금법), 도시계획(국토계획법·도정법), 환경(감염병예방법·대기환경법), 운수(여객운수법·화물운수법), 민·형사 절차(민소법·형소법·민집법), 사회보험(국건법·산재보험법·고보법), 통신(전기통신사업법) 커버. `api-client.ts`/`law-parser.ts`가 이미 `resolveLawAlias`를 사용 중이라 **데이터 추가만으로 기존 검색 경로가 자동 혜택**. 신규 41개 + 회귀 4개 포함 **45/45 테스트 통과**.
|
|
157
|
+
|
|
158
|
+
**v3.3.0** — HTTP stateless 모드 전환 + kordoc 2.3.0
|
|
159
|
+
|
|
160
|
+
원격 서버(`korean-law-mcp.fly.dev`)가 주기적으로 OOM kill로 재시작되면서 기존 세션 ID가 무효화되던 문제를 근본 해결. MCP 공식 stateless 패턴(`sessionIdGenerator: undefined`)으로 전환하여 매 요청마다 fresh `Server + Transport`를 생성, 요청 종료 시 즉시 해제. in-memory 세션 Map·InMemoryEventStore·idle cleanup 전부 제거로 누수 원인 소거. 재시작·스케일아웃·배포 모두 무손실. `GET /mcp`·`DELETE /mcp`는 공식 예제와 동일하게 `405`. API 키는 `AsyncLocalStorage`로 요청 단위 격리 (race condition 방지).
|
|
161
|
+
|
|
162
|
+
- **HTTP stateless 전환** — [src/server/http-server.ts](src/server/http-server.ts) (참고: `@modelcontextprotocol/sdk/examples/server/simpleStatelessStreamableHttp.js`)
|
|
163
|
+
- **kordoc 2.2.5 → 2.3.0** — 별표/서식 파싱 엔진 업데이트
|
|
164
|
+
- **세션 관리 코드 완전 제거** — `sessions` Map, `MAX_SESSIONS`, idle cleanup `setInterval`, `InMemoryEventStore`, POST/GET/DELETE 분기 로직 삭제 (v3.2.3의 LRU eviction 접근을 대체)
|
|
85
165
|
|
|
86
|
-
**v3.2.3** — HTTP 세션 안정성 개선. `MAX_SESSIONS`
|
|
166
|
+
**v3.2.3** — HTTP 세션 안정성 중간 개선. `MAX_SESSIONS` 100→500 + LRU eviction. _v3.3.0의 stateless 전환으로 대체됨._
|
|
87
167
|
|
|
88
|
-
**v3.2.2** — 별표/서식 조회 도구(`get_annexes`)를 기본 노출 도구에 추가. **노출 도구 수 14 → 15
|
|
168
|
+
**v3.2.2** — 별표/서식 조회 도구(`get_annexes`)를 기본 노출 도구에 추가. **노출 도구 수 14 → 15개**. 환불·감경 키워드 질의 시 별표 자동 조회 로직 추가.
|
|
89
169
|
|
|
90
170
|
**v3.2.1** — kordoc 2.2.5 업데이트.
|
|
91
171
|
|
package/build/index.js
CHANGED
|
@@ -9,14 +9,13 @@ import { LawApiClient } from "./lib/api-client.js";
|
|
|
9
9
|
import { registerTools } from "./tool-registry.js";
|
|
10
10
|
import { startHTTPServer } from "./server/http-server.js";
|
|
11
11
|
import { VERSION } from "./version.js";
|
|
12
|
-
import { parseProfile } from "./lib/tool-profiles.js";
|
|
13
12
|
// API 클라이언트 초기화 (LAW_OC 또는 KOREAN_LAW_API_KEY 지원)
|
|
14
13
|
const LAW_OC = process.env.LAW_OC || process.env.KOREAN_LAW_API_KEY || "";
|
|
15
14
|
const apiClient = new LawApiClient({ apiKey: LAW_OC });
|
|
16
15
|
// MCP 서버 팩토리 (HTTP 모드: 세션마다 새 인스턴스 필요)
|
|
17
|
-
function createServer(
|
|
16
|
+
function createServer() {
|
|
18
17
|
const s = new Server({ name: "korean-law", version: VERSION }, { capabilities: { tools: {} } });
|
|
19
|
-
registerTools(s, apiClient
|
|
18
|
+
registerTools(s, apiClient);
|
|
20
19
|
return s;
|
|
21
20
|
}
|
|
22
21
|
// 서버 시작
|
|
@@ -40,9 +39,7 @@ async function main() {
|
|
|
40
39
|
// stdout 오염 방지: MCP JSON-RPC 프로토콜 보호
|
|
41
40
|
const stderrWrite = (...args) => process.stderr.write(args.map(String).join(" ") + "\n");
|
|
42
41
|
console.log = console.warn = console.info = console.debug = stderrWrite;
|
|
43
|
-
|
|
44
|
-
const profile = parseProfile(process.env.MCP_PROFILE);
|
|
45
|
-
const server = createServer(profile);
|
|
42
|
+
const server = createServer();
|
|
46
43
|
const transport = new StdioServerTransport();
|
|
47
44
|
await server.connect(transport);
|
|
48
45
|
}
|
|
@@ -9,7 +9,7 @@ export declare class LawApiClient {
|
|
|
9
9
|
/**
|
|
10
10
|
* API 키 결정 순서:
|
|
11
11
|
* 1. 요청별 override 키
|
|
12
|
-
* 2. 현재
|
|
12
|
+
* 2. 현재 요청 컨텍스트의 API 키 (HTTP stateless 모드)
|
|
13
13
|
* 3. 환경변수 LAW_OC
|
|
14
14
|
* 4. 생성자에서 받은 기본 키
|
|
15
15
|
*/
|
|
@@ -22,8 +22,9 @@ export declare class LawApiClient {
|
|
|
22
22
|
private checkHtmlError;
|
|
23
23
|
/**
|
|
24
24
|
* 법령 검색
|
|
25
|
+
* @param display 결과 개수 (기본값 법제처 API default, 짧은 법령명("상법" 등) 정확 매칭 찾으려면 큰 값 권장)
|
|
25
26
|
*/
|
|
26
|
-
searchLaw(query: string, apiKey?: string): Promise<string>;
|
|
27
|
+
searchLaw(query: string, apiKey?: string, display?: number): Promise<string>;
|
|
27
28
|
/**
|
|
28
29
|
* 현행법령 조회
|
|
29
30
|
*/
|
package/build/lib/api-client.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { normalizeLawSearchText, resolveLawAlias } from "./search-normalizer.js";
|
|
5
5
|
import { fetchWithRetry } from "./fetch-with-retry.js";
|
|
6
|
-
import {
|
|
6
|
+
import { requestContext } from "./session-state.js";
|
|
7
7
|
const LAW_API_BASE = "https://www.law.go.kr/DRF";
|
|
8
8
|
export class LawApiClient {
|
|
9
9
|
constructor(config) {
|
|
@@ -12,14 +12,13 @@ export class LawApiClient {
|
|
|
12
12
|
/**
|
|
13
13
|
* API 키 결정 순서:
|
|
14
14
|
* 1. 요청별 override 키
|
|
15
|
-
* 2. 현재
|
|
15
|
+
* 2. 현재 요청 컨텍스트의 API 키 (HTTP stateless 모드)
|
|
16
16
|
* 3. 환경변수 LAW_OC
|
|
17
17
|
* 4. 생성자에서 받은 기본 키
|
|
18
18
|
*/
|
|
19
19
|
getApiKey(overrideKey) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const key = overrideKey || sessionApiKey || process.env.LAW_OC || process.env.KOREAN_LAW_API_KEY || this.defaultApiKey;
|
|
20
|
+
const ctxApiKey = requestContext.getStore()?.apiKey;
|
|
21
|
+
const key = overrideKey || ctxApiKey || process.env.LAW_OC || process.env.KOREAN_LAW_API_KEY || this.defaultApiKey;
|
|
23
22
|
if (!key) {
|
|
24
23
|
throw new Error("API 키가 필요합니다. 법제처(https://open.law.go.kr/LSO/openApi/guideResult.do)에서 발급받으세요.");
|
|
25
24
|
}
|
|
@@ -57,8 +56,9 @@ export class LawApiClient {
|
|
|
57
56
|
}
|
|
58
57
|
/**
|
|
59
58
|
* 법령 검색
|
|
59
|
+
* @param display 결과 개수 (기본값 법제처 API default, 짧은 법령명("상법" 등) 정확 매칭 찾으려면 큰 값 권장)
|
|
60
60
|
*/
|
|
61
|
-
async searchLaw(query, apiKey) {
|
|
61
|
+
async searchLaw(query, apiKey, display) {
|
|
62
62
|
const normalizedQuery = normalizeLawSearchText(query);
|
|
63
63
|
const aliasResolution = resolveLawAlias(normalizedQuery);
|
|
64
64
|
const finalQuery = aliasResolution.canonical;
|
|
@@ -68,6 +68,8 @@ export class LawApiClient {
|
|
|
68
68
|
target: "law",
|
|
69
69
|
query: finalQuery,
|
|
70
70
|
});
|
|
71
|
+
if (display && display > 0)
|
|
72
|
+
params.append("display", String(display));
|
|
71
73
|
const url = `${LAW_API_BASE}/lawSearch.do?${params.toString()}`;
|
|
72
74
|
const response = await fetchWithRetry(url);
|
|
73
75
|
await this.throwIfError(response, "searchLaw");
|
|
@@ -20,5 +20,6 @@ export declare function formatArticleUnit(unit: {
|
|
|
20
20
|
header: string;
|
|
21
21
|
body: string;
|
|
22
22
|
} | null;
|
|
23
|
+
export declare function parseHangNumber(raw: unknown): number;
|
|
23
24
|
/** HTML 정리 - 엔티티 디코딩 순서 중요: & 최후 처리 (이중 인코딩 방지) */
|
|
24
25
|
export declare function cleanHtml(text: string): string;
|
|
@@ -112,6 +112,24 @@ export function formatArticleUnit(unit) {
|
|
|
112
112
|
body = cleanHtml(body);
|
|
113
113
|
return { header, body };
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* 항번호 문자열을 숫자로 변환.
|
|
117
|
+
* 법제처 API는 항번호를 원숫자(①②③…)로 돌려주는 경우가 많아 일반 숫자 추출만 하면 NaN.
|
|
118
|
+
* 원숫자 ①=1 … ⑳=20 매핑 + fallback으로 일반 숫자 추출.
|
|
119
|
+
*/
|
|
120
|
+
const CIRCLED_DIGITS = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳";
|
|
121
|
+
export function parseHangNumber(raw) {
|
|
122
|
+
const s = String(raw ?? "").trim();
|
|
123
|
+
if (!s)
|
|
124
|
+
return NaN;
|
|
125
|
+
// 원숫자 매핑 (첫 글자 기준)
|
|
126
|
+
const circledIdx = CIRCLED_DIGITS.indexOf(s[0]);
|
|
127
|
+
if (circledIdx >= 0)
|
|
128
|
+
return circledIdx + 1;
|
|
129
|
+
// 일반 숫자 매칭 (예: "1", "제1항", "제 1 항")
|
|
130
|
+
const numMatch = s.match(/\d+/);
|
|
131
|
+
return numMatch ? parseInt(numMatch[0], 10) : NaN;
|
|
132
|
+
}
|
|
115
133
|
/** HTML 정리 - 엔티티 디코딩 순서 중요: & 최후 처리 (이중 인코딩 방지) */
|
|
116
134
|
export function cleanHtml(text) {
|
|
117
135
|
return text
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Compact — 판례/헌재/행심 응답 토큰 최적화 유틸
|
|
3
|
+
*
|
|
4
|
+
* B. compactBody: 본문("이유"/"전문")을 "앞 800자 + 중략 + 뒤 400자"로 계단식 축약.
|
|
5
|
+
* 판시사항/판결요지/주문은 이미 별도 필드로 분리 반환되므로 본문에만 적용.
|
|
6
|
+
* 문장 경계(마침표·판례 특유 종결어미)를 존중하여 중간 절단 방지.
|
|
7
|
+
*
|
|
8
|
+
* C. densifyLawRefs / densifyPrecedentRefs: 참조조문·참조판례의 서지 잉여 제거.
|
|
9
|
+
* 괄호 안 조문명, "선고"/"판결" 군더더기, 날짜 공백 압축. 구조 유지 + 압축.
|
|
10
|
+
*
|
|
11
|
+
* D. compactLongSections: 통합 후처리용. 응답 텍스트에서 알려진 긴 섹션 헤더
|
|
12
|
+
* ("이유:", "전문:", "회답:" 등)를 찾아 해당 섹션 본문에 compactBody 적용.
|
|
13
|
+
* unified-decisions.ts가 도메인별 스키마를 바꾸지 않고 full=false 기본값을
|
|
14
|
+
* 강제하기 위해 사용.
|
|
15
|
+
*
|
|
16
|
+
* 안전성: 모든 함수는 매칭 실패 / 빈 입력 / 짧은 입력 시 **원본 그대로 반환**.
|
|
17
|
+
*/
|
|
18
|
+
export interface CompactOptions {
|
|
19
|
+
/** true 시 축약 비활성 → 원본 반환 */
|
|
20
|
+
full?: boolean;
|
|
21
|
+
/** 앞쪽 보존 길이 (기본 800자) */
|
|
22
|
+
headSize?: number;
|
|
23
|
+
/** 뒤쪽 보존 길이 (기본 400자) */
|
|
24
|
+
tailSize?: number;
|
|
25
|
+
/** head+tail+minSave 이하면 축약 안 함 (기본 500자) */
|
|
26
|
+
minSave?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 본문 계단식 축약
|
|
30
|
+
* - 앞 800자 + 중략 마커 + 뒤 400자
|
|
31
|
+
* - 문장 경계 가드: 마침표/판례 종결어미/빈 줄에서 끊기
|
|
32
|
+
* - head 기준 50% 이상 위치에 경계 없으면 원시 슬라이스 사용 (fallback)
|
|
33
|
+
*/
|
|
34
|
+
export declare function compactBody(text: string, opts?: CompactOptions): string;
|
|
35
|
+
/**
|
|
36
|
+
* 참조조문 densify
|
|
37
|
+
*
|
|
38
|
+
* 압축 전략:
|
|
39
|
+
* 1) 조문명 괄호 설명 제거: "제390조(채무불이행과 손해배상)" → "제390조"
|
|
40
|
+
* 2) 연속 공백/구분자 정리
|
|
41
|
+
*
|
|
42
|
+
* 조문명 괄호는 평균 15~30자 × 참조조문 5~10개 = 150~300자 절감 (40%).
|
|
43
|
+
* 법령명 자체는 건드리지 않음 — LLM이 후속 도구 호출 시 파싱 필요.
|
|
44
|
+
*/
|
|
45
|
+
export declare function densifyLawRefs(text: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* 참조판례 densify
|
|
48
|
+
*
|
|
49
|
+
* 압축 전략:
|
|
50
|
+
* 1) "선고" 생략: "대법원 2020. 3. 26. 선고 2018두56077 판결" → "대법원 2020.3.26. 2018두56077"
|
|
51
|
+
* 2) 날짜 공백 압축: "2020. 3. 26." → "2020.3.26."
|
|
52
|
+
* 3) "판결" 접미어 제거
|
|
53
|
+
* 4) 구분자 정리
|
|
54
|
+
*/
|
|
55
|
+
export declare function densifyPrecedentRefs(text: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* 본문에서 판시사항/판결요지가 반복 등장하면 제거
|
|
58
|
+
*
|
|
59
|
+
* 법제처 API 판례 응답은 `판시사항`, `판결요지`가 별도 필드로 나오지만
|
|
60
|
+
* `판례내용`(전문) 앞쪽에 같은 내용이 반복되는 케이스가 많다.
|
|
61
|
+
* 이미 상단에 렌더된 부분을 본문에서 제거하여 LLM 중복 소비 방지.
|
|
62
|
+
*
|
|
63
|
+
* 경계 탐지: 요약의 앞 80자로 시작점(idx)을, 요약의 끝 60자로 실제 종료점(endIdx)을
|
|
64
|
+
* 탐지. 끝 매칭이 실패하면 보수적으로 s.length만큼만 제거하여 본문 다른 내용이
|
|
65
|
+
* 같이 날아가는 사고 방지.
|
|
66
|
+
*/
|
|
67
|
+
export declare function stripRepeatedSummary(body: string, summaries: Array<string | undefined>): string;
|
|
68
|
+
export declare function compactLongSections(text: string): string;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Compact — 판례/헌재/행심 응답 토큰 최적화 유틸
|
|
3
|
+
*
|
|
4
|
+
* B. compactBody: 본문("이유"/"전문")을 "앞 800자 + 중략 + 뒤 400자"로 계단식 축약.
|
|
5
|
+
* 판시사항/판결요지/주문은 이미 별도 필드로 분리 반환되므로 본문에만 적용.
|
|
6
|
+
* 문장 경계(마침표·판례 특유 종결어미)를 존중하여 중간 절단 방지.
|
|
7
|
+
*
|
|
8
|
+
* C. densifyLawRefs / densifyPrecedentRefs: 참조조문·참조판례의 서지 잉여 제거.
|
|
9
|
+
* 괄호 안 조문명, "선고"/"판결" 군더더기, 날짜 공백 압축. 구조 유지 + 압축.
|
|
10
|
+
*
|
|
11
|
+
* D. compactLongSections: 통합 후처리용. 응답 텍스트에서 알려진 긴 섹션 헤더
|
|
12
|
+
* ("이유:", "전문:", "회답:" 등)를 찾아 해당 섹션 본문에 compactBody 적용.
|
|
13
|
+
* unified-decisions.ts가 도메인별 스키마를 바꾸지 않고 full=false 기본값을
|
|
14
|
+
* 강제하기 위해 사용.
|
|
15
|
+
*
|
|
16
|
+
* 안전성: 모든 함수는 매칭 실패 / 빈 입력 / 짧은 입력 시 **원본 그대로 반환**.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* 본문 계단식 축약
|
|
20
|
+
* - 앞 800자 + 중략 마커 + 뒤 400자
|
|
21
|
+
* - 문장 경계 가드: 마침표/판례 종결어미/빈 줄에서 끊기
|
|
22
|
+
* - head 기준 50% 이상 위치에 경계 없으면 원시 슬라이스 사용 (fallback)
|
|
23
|
+
*/
|
|
24
|
+
export function compactBody(text, opts = {}) {
|
|
25
|
+
if (opts.full || !text)
|
|
26
|
+
return text;
|
|
27
|
+
const HEAD = opts.headSize ?? 800;
|
|
28
|
+
const TAIL = opts.tailSize ?? 400;
|
|
29
|
+
const MIN_SAVE = opts.minSave ?? 500;
|
|
30
|
+
// 짧으면 축약 이득 없음 → 원본
|
|
31
|
+
if (text.length <= HEAD + TAIL + MIN_SAVE)
|
|
32
|
+
return text;
|
|
33
|
+
// HEAD — 앞에서 HEAD자까지 중 문장 끝에서 자르기
|
|
34
|
+
const headRaw = text.slice(0, HEAD);
|
|
35
|
+
const headBoundaries = [
|
|
36
|
+
headRaw.lastIndexOf("다.\n"),
|
|
37
|
+
headRaw.lastIndexOf("라.\n"),
|
|
38
|
+
headRaw.lastIndexOf("다. "),
|
|
39
|
+
headRaw.lastIndexOf("라. "),
|
|
40
|
+
headRaw.lastIndexOf(".\n\n"),
|
|
41
|
+
headRaw.lastIndexOf("\n\n"),
|
|
42
|
+
headRaw.lastIndexOf(". "),
|
|
43
|
+
];
|
|
44
|
+
const headCutCandidate = Math.max(...headBoundaries);
|
|
45
|
+
const headCut = headCutCandidate > HEAD * 0.5 ? headCutCandidate + 2 : HEAD;
|
|
46
|
+
const head = text.slice(0, headCut).trimEnd();
|
|
47
|
+
// TAIL — 뒤에서 TAIL자 범위 중 문장 시작에서 자르기
|
|
48
|
+
// ". " 경계는 소수점("1,234.00 원")/영문 약어("No. 3") 오탐 위험으로 제외.
|
|
49
|
+
// 판례 한국어 특성상 "다." / "라." 종결어미가 지배적이라 이걸로 충분.
|
|
50
|
+
const tailStart = text.length - TAIL;
|
|
51
|
+
const tailRaw = text.slice(tailStart);
|
|
52
|
+
const tailBoundaryIdx = [
|
|
53
|
+
tailRaw.indexOf("\n\n"),
|
|
54
|
+
tailRaw.indexOf("다.\n"),
|
|
55
|
+
tailRaw.indexOf("라.\n"),
|
|
56
|
+
tailRaw.indexOf("다. "),
|
|
57
|
+
tailRaw.indexOf("라. "),
|
|
58
|
+
tailRaw.indexOf("한다. "),
|
|
59
|
+
]
|
|
60
|
+
.filter((i) => i >= 0)
|
|
61
|
+
.sort((a, b) => a - b)[0];
|
|
62
|
+
const tailFrom = tailBoundaryIdx !== undefined && tailBoundaryIdx < TAIL * 0.5
|
|
63
|
+
? tailStart + tailBoundaryIdx + 2
|
|
64
|
+
: tailStart;
|
|
65
|
+
const tail = text.slice(tailFrom).trimStart();
|
|
66
|
+
const omitted = text.length - head.length - tail.length;
|
|
67
|
+
if (omitted < MIN_SAVE)
|
|
68
|
+
return text; // 실질 절감 미달 시 원본
|
|
69
|
+
return `${head}\n\n⋯ 중략 ${omitted.toLocaleString()}자 (full=true로 전문 조회) ⋯\n\n${tail}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 참조조문 densify
|
|
73
|
+
*
|
|
74
|
+
* 압축 전략:
|
|
75
|
+
* 1) 조문명 괄호 설명 제거: "제390조(채무불이행과 손해배상)" → "제390조"
|
|
76
|
+
* 2) 연속 공백/구분자 정리
|
|
77
|
+
*
|
|
78
|
+
* 조문명 괄호는 평균 15~30자 × 참조조문 5~10개 = 150~300자 절감 (40%).
|
|
79
|
+
* 법령명 자체는 건드리지 않음 — LLM이 후속 도구 호출 시 파싱 필요.
|
|
80
|
+
*/
|
|
81
|
+
export function densifyLawRefs(text) {
|
|
82
|
+
if (!text)
|
|
83
|
+
return text;
|
|
84
|
+
const original = text;
|
|
85
|
+
// 1) 조문 뒤 괄호 설명 제거
|
|
86
|
+
// "제390조(채무불이행과 손해배상)" → "제390조"
|
|
87
|
+
// "제1항(적용범위)" → "제1항"
|
|
88
|
+
// 길이 3~40자 괄호만 타겟 (짧은 건 의미 있을 수 있음)
|
|
89
|
+
let compact = text.replace(/(제\d+조(?:의\d+)?|제\d+항|제\d+호)\s*\([^)]{3,40}\)/g, "$1");
|
|
90
|
+
// 2) 공백/구분자 정리
|
|
91
|
+
compact = compact
|
|
92
|
+
.replace(/\s*,\s*/g, ", ")
|
|
93
|
+
.replace(/\s*\/\s*/g, " / ")
|
|
94
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
95
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
96
|
+
.trim();
|
|
97
|
+
// 실질 절감 없으면 원본
|
|
98
|
+
if (compact.length >= original.length * 0.95)
|
|
99
|
+
return original;
|
|
100
|
+
return compact;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 참조판례 densify
|
|
104
|
+
*
|
|
105
|
+
* 압축 전략:
|
|
106
|
+
* 1) "선고" 생략: "대법원 2020. 3. 26. 선고 2018두56077 판결" → "대법원 2020.3.26. 2018두56077"
|
|
107
|
+
* 2) 날짜 공백 압축: "2020. 3. 26." → "2020.3.26."
|
|
108
|
+
* 3) "판결" 접미어 제거
|
|
109
|
+
* 4) 구분자 정리
|
|
110
|
+
*/
|
|
111
|
+
export function densifyPrecedentRefs(text) {
|
|
112
|
+
if (!text)
|
|
113
|
+
return text;
|
|
114
|
+
const original = text;
|
|
115
|
+
let compact = text
|
|
116
|
+
// "선고" 앞뒤 공백 포함 제거
|
|
117
|
+
.replace(/\s*선고\s*/g, " ")
|
|
118
|
+
// "판결" 접미어 (공백 포함) 제거
|
|
119
|
+
.replace(/\s*판결(?=[\s,/;]|$)/g, "")
|
|
120
|
+
// 날짜 공백 압축: "2020. 3. 26." → "2020.3.26."
|
|
121
|
+
// 경계 가드: 시작/공백/구분자/괄호 뒤의 4자리 연도만 대상
|
|
122
|
+
// (문서 중간 "제2020." 같은 오탐 방지)
|
|
123
|
+
.replace(/(^|[\s,(\[;/])(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\./g, "$1$2.$3.$4.")
|
|
124
|
+
// "[동지] ", "[공보 생략]" 같은 부가표기 제거
|
|
125
|
+
.replace(/\s*\[[^\]]{2,15}\]\s*/g, " ")
|
|
126
|
+
// 연속 공백
|
|
127
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
128
|
+
.replace(/\s*,\s*/g, ", ")
|
|
129
|
+
.trim();
|
|
130
|
+
if (compact.length >= original.length * 0.95)
|
|
131
|
+
return original;
|
|
132
|
+
return compact;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 본문에서 판시사항/판결요지가 반복 등장하면 제거
|
|
136
|
+
*
|
|
137
|
+
* 법제처 API 판례 응답은 `판시사항`, `판결요지`가 별도 필드로 나오지만
|
|
138
|
+
* `판례내용`(전문) 앞쪽에 같은 내용이 반복되는 케이스가 많다.
|
|
139
|
+
* 이미 상단에 렌더된 부분을 본문에서 제거하여 LLM 중복 소비 방지.
|
|
140
|
+
*
|
|
141
|
+
* 경계 탐지: 요약의 앞 80자로 시작점(idx)을, 요약의 끝 60자로 실제 종료점(endIdx)을
|
|
142
|
+
* 탐지. 끝 매칭이 실패하면 보수적으로 s.length만큼만 제거하여 본문 다른 내용이
|
|
143
|
+
* 같이 날아가는 사고 방지.
|
|
144
|
+
*/
|
|
145
|
+
export function stripRepeatedSummary(body, summaries) {
|
|
146
|
+
if (!body)
|
|
147
|
+
return body;
|
|
148
|
+
let result = body;
|
|
149
|
+
for (const s of summaries) {
|
|
150
|
+
if (!s || s.length < 20)
|
|
151
|
+
continue;
|
|
152
|
+
const trimmed = s.trim();
|
|
153
|
+
const headLen = Math.min(80, trimmed.length);
|
|
154
|
+
const head = trimmed.slice(0, headLen);
|
|
155
|
+
if (head.length < 20)
|
|
156
|
+
continue;
|
|
157
|
+
// 본문 앞 25% 안에서 동일 구간 탐지
|
|
158
|
+
const zone = result.slice(0, Math.floor(result.length * 0.25));
|
|
159
|
+
const idx = zone.indexOf(head);
|
|
160
|
+
if (idx < 0)
|
|
161
|
+
continue;
|
|
162
|
+
// 실제 종료점 탐지: 요약의 끝 60자를 본문에서 찾아 정확한 end 위치 계산
|
|
163
|
+
const tailLen = Math.min(60, trimmed.length - headLen);
|
|
164
|
+
let end;
|
|
165
|
+
if (tailLen >= 20) {
|
|
166
|
+
const tail = trimmed.slice(trimmed.length - tailLen);
|
|
167
|
+
// idx 이후 ±20% 범위 내에서만 tail 매칭 (다른 위치 오탐 방지)
|
|
168
|
+
const searchZone = result.slice(idx, Math.min(idx + Math.floor(trimmed.length * 1.3), result.length));
|
|
169
|
+
const tailIdxInZone = searchZone.indexOf(tail);
|
|
170
|
+
end = tailIdxInZone >= 0
|
|
171
|
+
? idx + tailIdxInZone + tail.length
|
|
172
|
+
: Math.min(idx + trimmed.length, result.length);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
end = Math.min(idx + trimmed.length, result.length);
|
|
176
|
+
}
|
|
177
|
+
result = result.slice(0, idx) + result.slice(end);
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 통합 후처리 축약 — unified-decisions.ts용
|
|
183
|
+
*
|
|
184
|
+
* 응답 포맷(도구별 getter 핸들러가 출력하는 텍스트)에서 알려진 긴 섹션
|
|
185
|
+
* 헤더("이유:", "전문:", "회답:" 등)를 찾아 해당 섹션 본문에 compactBody 적용.
|
|
186
|
+
*
|
|
187
|
+
* 이미 compactBody가 적용된 도메인(precedent, constitutional, admin_appeal)은
|
|
188
|
+
* unified-decisions.ts에서 skip 리스트로 관리되므로 여기서는 무조건 적용.
|
|
189
|
+
*
|
|
190
|
+
* 안전성:
|
|
191
|
+
* - 알려진 섹션 헤더가 없으면 원본 반환
|
|
192
|
+
* - 이미 "⋯ 중략 N자" 마커가 있으면 원본 반환 (이중 축약 방지)
|
|
193
|
+
* - 본문이 짧으면 compactBody가 원본 반환
|
|
194
|
+
*/
|
|
195
|
+
const KNOWN_SECTION_HEADERS = [
|
|
196
|
+
"이유",
|
|
197
|
+
"전문",
|
|
198
|
+
"결정내용",
|
|
199
|
+
"본문",
|
|
200
|
+
"회답",
|
|
201
|
+
"재결이유",
|
|
202
|
+
"판결이유",
|
|
203
|
+
"판례내용",
|
|
204
|
+
"심판요지",
|
|
205
|
+
"의결내용",
|
|
206
|
+
"결정이유",
|
|
207
|
+
"조문내용",
|
|
208
|
+
];
|
|
209
|
+
export function compactLongSections(text) {
|
|
210
|
+
if (!text || text.length < 1500)
|
|
211
|
+
return text;
|
|
212
|
+
// 이미 축약된 경우 skip
|
|
213
|
+
if (text.includes("⋯ 중략 ") && text.includes("(full=true로 전문 조회)"))
|
|
214
|
+
return text;
|
|
215
|
+
const pattern = new RegExp(`\\n(${KNOWN_SECTION_HEADERS.join("|")}):\\n`, "g");
|
|
216
|
+
let lastMatch = null;
|
|
217
|
+
let m;
|
|
218
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
219
|
+
lastMatch = m;
|
|
220
|
+
}
|
|
221
|
+
if (!lastMatch)
|
|
222
|
+
return text;
|
|
223
|
+
const sectionStart = lastMatch.index + lastMatch[0].length;
|
|
224
|
+
const body = text.slice(sectionStart);
|
|
225
|
+
const compacted = compactBody(body, { full: false });
|
|
226
|
+
if (compacted === body)
|
|
227
|
+
return text;
|
|
228
|
+
return text.slice(0, sectionStart) + compacted;
|
|
229
|
+
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* - Exponential backoff for 429, 503, 504
|
|
4
4
|
* - AbortController for timeout
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* URL에서 민감 정보(API 키) 마스킹 — 에러 메시지/로그 노출 방지.
|
|
8
|
+
* 법제처 API는 ?OC=KEY 쿼리 파라미터로 키를 받으므로 해당 값만 *** 처리.
|
|
9
|
+
* 추가 방어로 일반적인 키 파라미터 이름들도 마스킹.
|
|
10
|
+
*/
|
|
11
|
+
export declare function maskSensitiveUrl(url: string): string;
|
|
6
12
|
export interface FetchWithRetryOptions extends RequestInit {
|
|
7
13
|
/** Request timeout in ms (default: 30000) */
|
|
8
14
|
timeout?: number;
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
* - Exponential backoff for 429, 503, 504
|
|
4
4
|
* - AbortController for timeout
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* URL에서 민감 정보(API 키) 마스킹 — 에러 메시지/로그 노출 방지.
|
|
8
|
+
* 법제처 API는 ?OC=KEY 쿼리 파라미터로 키를 받으므로 해당 값만 *** 처리.
|
|
9
|
+
* 추가 방어로 일반적인 키 파라미터 이름들도 마스킹.
|
|
10
|
+
*/
|
|
11
|
+
export function maskSensitiveUrl(url) {
|
|
12
|
+
if (!url)
|
|
13
|
+
return url;
|
|
14
|
+
return url.replace(/([?&](?:oc|OC|apikey|apiKey|api_key|authKey|auth_key|key)=)[^&]+/g, "$1***");
|
|
15
|
+
}
|
|
6
16
|
const DEFAULT_TIMEOUT = 30000;
|
|
7
17
|
const DEFAULT_RETRIES = 3;
|
|
8
18
|
const DEFAULT_RETRY_DELAY = 1000;
|
|
@@ -37,13 +47,15 @@ export async function fetchWithRetry(url, options = {}) {
|
|
|
37
47
|
}
|
|
38
48
|
catch (error) {
|
|
39
49
|
clearTimeout(timeoutId);
|
|
40
|
-
// Timeout or network error
|
|
50
|
+
// Timeout or network error — URL에서 API 키 제거 후 에러 생성
|
|
41
51
|
if (error instanceof Error) {
|
|
42
52
|
if (error.name === "AbortError") {
|
|
43
|
-
lastError = new Error(`Request timeout after ${timeout}ms for ${url}`);
|
|
53
|
+
lastError = new Error(`Request timeout after ${timeout}ms for ${maskSensitiveUrl(url)}`);
|
|
44
54
|
}
|
|
45
55
|
else {
|
|
46
|
-
|
|
56
|
+
// fetch 네이티브 에러 메시지에도 URL이 포함될 수 있음
|
|
57
|
+
const masked = maskSensitiveUrl(error.message);
|
|
58
|
+
lastError = masked !== error.message ? new Error(masked) : error;
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
// Retry on network errors
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 공용 법령 검색 유틸 — chains / verify_citations 등에서 공유.
|
|
3
|
+
*
|
|
4
|
+
* 핵심: 법제처 lawSearch API는 부분 문자열 매칭 특성이 있어 "민법" → "난민법"
|
|
5
|
+
* 같은 엉뚱한 매칭이 발생한다. scoreLawRelevance로 정확 매칭 우선 정렬하여
|
|
6
|
+
* 첫 결과 신뢰 가능하게 만든다.
|
|
7
|
+
*/
|
|
8
|
+
import type { LawApiClient } from "./api-client.js";
|
|
9
|
+
export interface LawInfo {
|
|
10
|
+
lawName: string;
|
|
11
|
+
lawId: string;
|
|
12
|
+
mst: string;
|
|
13
|
+
lawType: string;
|
|
14
|
+
}
|
|
15
|
+
/** 법령명이 아닌 부가 키워드 제거 (법제처 lawSearch API는 법령명 검색이므로) */
|
|
16
|
+
export declare const NON_LAW_NAME_RE: RegExp;
|
|
17
|
+
export declare function stripNonLawKeywords(query: string): string;
|
|
18
|
+
/** XML에서 법령 정보 파싱 */
|
|
19
|
+
export declare function parseLawXml(xmlText: string, max: number): LawInfo[];
|
|
20
|
+
/** 쿼리 대비 법령명 관련도 점수 (높을수록 관련) */
|
|
21
|
+
export declare function scoreLawRelevance(lawName: string, query: string, queryWords: string[]): number;
|
|
22
|
+
/**
|
|
23
|
+
* 법령 검색 + 관련도 정렬 + 캐싱.
|
|
24
|
+
* 1차: 원본 쿼리 → 2차: 부가키워드 제거 → 3차: 법령명 패턴 직접 추출
|
|
25
|
+
* 이후 scoreLawRelevance로 정렬.
|
|
26
|
+
*
|
|
27
|
+
* @param searchDisplay 법제처 API display 파라미터 — 짧은 법령명("상법"은 100개 중 34번째)
|
|
28
|
+
* 정확 매칭 찾으려면 크게(100+). 기본 20은 체인 도구용.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findLaws(apiClient: LawApiClient, query: string, apiKey?: string, max?: number, searchDisplay?: number): Promise<LawInfo[]>;
|