korean-law-mcp 2.3.0 → 2.3.2

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.
Files changed (55) hide show
  1. package/README.md +128 -43
  2. package/build/lib/api-client.js +3 -22
  3. package/build/lib/article-parser.d.ts +15 -0
  4. package/build/lib/article-parser.js +51 -0
  5. package/build/lib/errors.d.ts +0 -15
  6. package/build/lib/errors.js +7 -40
  7. package/build/lib/fetch-with-retry.js +16 -4
  8. package/build/lib/schemas.d.ts +0 -8
  9. package/build/lib/schemas.js +0 -15
  10. package/build/lib/three-tier-parser.js +110 -115
  11. package/build/lib/types.d.ts +10 -7
  12. package/build/server/http-server.js +4 -2
  13. package/build/tool-registry.d.ts +0 -4
  14. package/build/tool-registry.js +7 -1
  15. package/build/tools/admin-appeals.js +6 -16
  16. package/build/tools/admin-rule.js +13 -24
  17. package/build/tools/advanced-search.js +3 -2
  18. package/build/tools/annex.js +8 -8
  19. package/build/tools/article-compare.js +5 -9
  20. package/build/tools/article-detail.js +5 -6
  21. package/build/tools/article-link-parser.js +1 -1
  22. package/build/tools/article-with-precedents.js +2 -3
  23. package/build/tools/autocomplete.js +4 -4
  24. package/build/tools/batch-articles.js +10 -44
  25. package/build/tools/chains.js +20 -0
  26. package/build/tools/committee-decisions.js +7 -11
  27. package/build/tools/comparison.js +5 -5
  28. package/build/tools/constitutional-decisions.js +7 -19
  29. package/build/tools/customs-interpretations.js +7 -7
  30. package/build/tools/english-law.js +4 -11
  31. package/build/tools/external-links.js +6 -6
  32. package/build/tools/historical-law.js +8 -14
  33. package/build/tools/interpretations.js +5 -15
  34. package/build/tools/kb-utils.js +2 -2
  35. package/build/tools/knowledge-base.js +30 -34
  36. package/build/tools/law-history.js +2 -1
  37. package/build/tools/law-linkage.d.ts +24 -4
  38. package/build/tools/law-statistics.js +2 -2
  39. package/build/tools/law-system-tree.js +6 -8
  40. package/build/tools/law-text.js +15 -62
  41. package/build/tools/law-tree.js +2 -2
  42. package/build/tools/legal-terms.js +2 -10
  43. package/build/tools/life-law.js +6 -13
  44. package/build/tools/meta-tools.js +8 -0
  45. package/build/tools/ordinance-search.js +2 -2
  46. package/build/tools/ordinance.js +4 -11
  47. package/build/tools/precedent-keywords.js +1 -1
  48. package/build/tools/precedent-summary.js +1 -1
  49. package/build/tools/precedents.js +11 -18
  50. package/build/tools/search.js +1 -1
  51. package/build/tools/similar-precedents.js +1 -1
  52. package/build/tools/tax-tribunal-decisions.js +9 -9
  53. package/build/tools/three-tier.js +7 -7
  54. package/build/tools/treaties.js +1 -1
  55. package/package.json +1 -1
package/README.md CHANGED
@@ -14,12 +14,24 @@
14
14
 
15
15
  ---
16
16
 
17
- ## v2.3.0 변경사항
17
+ ## v2.3 변경사항
18
18
 
19
- - **도구 프로필 (lite/full)** 클라이언트(Claude.ai 등) lite 프로필 도입. 89개 → 14개로 자동 축소하여 컨텍스트 소비 87% 절감. `/mcp?profile=lite`로 사용.
20
- - **체인 8개** + 핵심 직접 도구 4개 + 메타 도구 2개 = 14개로 동일 기능 커버
21
- - `discover_tools`: 의도/카테고리로 숨겨진 전문 도구 검색
22
- - `execute_tool`: discover로 찾은 도구를 프록시 실행
19
+ **원격 MCP 주소** (`your-key` 부분에 [법제처 Open API](https://open.law.go.kr/LSO/openApi/guideResult.do)에서 발급받은 본인 인증키(OC)를 넣으세요):
20
+
21
+ | 프로필 | URL | 도구 수 | 용도 |
22
+ |--------|-----|---------|------|
23
+ | lite | `https://korean-law-mcp.fly.dev/mcp?profile=lite&oc=your-key` | 14개 | Claude.ai 등 웹 클라이언트 (컨텍스트 87% 절감) |
24
+ | full | `https://korean-law-mcp.fly.dev/mcp?oc=your-key` | 89개 | Claude Desktop, Cursor 등 네이티브 클라이언트 |
25
+
26
+ 예시: 발급받은 인증키가 `honggildong`이면 → `https://korean-law-mcp.fly.dev/mcp?profile=lite&oc=honggildong`
27
+
28
+ > lite는 체인 8개 + 핵심 4개 + 메타 2개로 동일 기능 커버. 특수 도구는 `discover_tools` → `execute_tool`로 접근.
29
+
30
+ - **도구 프로필 (lite/full)** — 89개 → 14개 자동 축소. 체인 도구가 내부에서 하위 도구를 직접 호출하므로 기능 손실 없음.
31
+ - **URL 쿼리 API 키** — `?oc=your-key`로 세션 전체에 API 키 자동 적용. 커스텀 헤더 설정이 어려운 웹 클라이언트에서 필수.
32
+ - **체인 자동 전문 조회** — `chain_ordinance_compare`가 자치법규 검색 후 상위 1건 전문을 자동 조회. 별도 `get_ordinance` 호출 불필요.
33
+ - **lite 도구 라우팅 개선** — 체인/메타 도구 description을 사용자 질문 의도 기반으로 재작성 + 예시 포함. Claude 웹이 "광진구 복무 조례" 같은 자치법규 질문에도 정확한 도구 선택.
34
+ - **도구 힌트 통일** — 비lite 도구 안내를 `execute_tool()` 호출 예시로 변경. 존재하지 않는 도구 직접 호출 문제 방지.
23
35
  - **kordoc 통합 파서** — 자체 HWP5/HWPX/PDF 파서 5개를 [kordoc](https://github.com/chrisryugj/kordoc) 통합 파서로 교체. 의존성 경량화.
24
36
 
25
37
  <details>
@@ -54,81 +66,154 @@
54
66
 
55
67
  ---
56
68
 
57
- ## 빠른 시작
69
+ ## 설치 및 사용법
58
70
 
59
- ### MCP 서버 (Claude Desktop / Cursor / Windsurf)
71
+ ### 0단계: API 발급 (무료, 1분)
60
72
 
61
- ```bash
62
- npm install -g korean-law-mcp
73
+ 모든 방법에 공통으로 필요한 **법제처 Open API 인증키(OC)**를 먼저 발급받으세요.
74
+
75
+ 1. [법제처 Open API 신청 페이지](https://open.law.go.kr/LSO/openApi/guideList.do)에 접속합니다.
76
+ 2. 회원가입 후 로그인합니다.
77
+ 3. **"Open API 사용 신청"** 버튼을 누릅니다.
78
+ 4. 신청서를 작성하면 **인증키(OC)**가 발급됩니다. (예: `honggildong`)
79
+ 5. 이 인증키를 아래 설정에서 사용합니다.
80
+
81
+ ---
82
+
83
+ ### 방법 1: Claude.ai 웹에서 바로 사용 (설치 없음, 가장 쉬움)
84
+
85
+ 아무것도 설치하지 않고, 주소 하나만 입력하면 됩니다. Claude Pro/Max/Team/Enterprise 요금제가 필요합니다 (Free는 커넥터 1개만 가능).
86
+
87
+ **커넥터 추가 방법:**
88
+
89
+ 1. [claude.ai](https://claude.ai)에 로그인합니다.
90
+ 2. 왼쪽 사이드바 하단의 **본인 이름**을 클릭합니다.
91
+ 3. **"설정"** (또는 Settings)을 선택합니다.
92
+ 4. **"커넥터"** (또는 Connectors) 메뉴로 들어갑니다.
93
+ 5. **"커스텀 커넥터"** 영역에서 **"커스텀 커넥터 추가"** 버튼을 클릭합니다.
94
+ 6. 아래 내용을 입력합니다:
95
+ - **이름**: `korean-law` (원하는 이름 아무거나 OK)
96
+ - **URL**: 아래 주소를 붙여넣으세요. `honggildong` 부분을 **0단계에서 발급받은 본인 인증키**로 바꾸세요:
97
+
98
+ ```
99
+ https://korean-law-mcp.fly.dev/mcp?profile=lite&oc=honggildong
63
100
  ```
64
101
 
65
- MCP 클라이언트 설정에 추가:
102
+ 7. **추가** 버튼을 누르면 등록 완료!
103
+
104
+ **도구 활성화 (중요!):**
105
+
106
+ 8. 추가한 커넥터의 **"구성"** (또는 Configure)을 클릭합니다.
107
+ 9. 도구 목록이 나오면, 모든 도구를 **"항상 사용"** (또는 Always allow)으로 설정합니다.
108
+ 10. 이렇게 하면 매번 승인할 필요 없이 AI가 바로 법령을 검색할 수 있습니다.
109
+
110
+ **사용하기:**
111
+
112
+ 11. 채팅 화면으로 돌아가서 "근로기준법 제74조 알려줘"라고 입력하면 끝!
113
+
114
+ > **참고**: 커넥터 URL을 수정하려면 삭제 후 다시 추가해야 합니다.
115
+
116
+ > **lite vs full 차이**: 위 주소는 lite 모드(14개 도구)입니다. 14개로도 89개 전체 기능을 사용할 수 있어요 — AI가 필요할 때 나머지 도구를 알아서 꺼내 씁니다. 모든 도구를 직접 보고 싶으면 주소에서 `profile=lite&`를 빼면 됩니다.
117
+
118
+ ---
119
+
120
+ ### 방법 2: AI 데스크톱 앱에서 사용 (설치 없음)
121
+
122
+ Claude Desktop, Cursor, Windsurf 같은 **데스크톱 앱**을 쓰고 있다면, 설정 파일에 아래 내용을 추가하세요.
123
+
124
+ **설정 파일 위치 찾기:**
125
+
126
+ | 앱 이름 | Windows | Mac |
127
+ |---------|---------|-----|
128
+ | Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` | `~/Library/Application Support/Claude/claude_desktop_config.json` |
129
+ | Cursor | 프로젝트 폴더 안 `.cursor/mcp.json` | 프로젝트 폴더 안 `.cursor/mcp.json` |
130
+ | Windsurf | 프로젝트 폴더 안 `.windsurf/mcp.json` | 프로젝트 폴더 안 `.windsurf/mcp.json` |
131
+
132
+ **설정 파일에 추가할 내용** (`honggildong`을 본인 인증키로 바꾸세요):
66
133
 
67
134
  ```json
68
135
  {
69
136
  "mcpServers": {
70
137
  "korean-law": {
71
- "command": "korean-law-mcp",
72
- "env": {
73
- "LAW_OC": "your-api-key"
74
- }
138
+ "url": "https://korean-law-mcp.fly.dev/mcp?oc=honggildong"
75
139
  }
76
140
  }
77
141
  }
78
142
  ```
79
143
 
80
- API 키는 [법제처 Open API](https://open.law.go.kr/LSO/openApi/guideResult.do)에서 무료 발급.
144
+ > 이미 다른 MCP 서버가 설정되어 있다면, `"mcpServers": { ... }` 안에 `"korean-law": { ... }` 부분만 추가하면 됩니다.
81
145
 
82
- | 클라이언트 | 설정 파일 |
83
- |-----------|----------|
84
- | Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` (Win) / `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac) |
85
- | Cursor | `.cursor/mcp.json` |
86
- | Windsurf | `.windsurf/mcp.json` |
87
- | Continue | `~/.continue/config.json` |
88
- | Zed | `~/.config/zed/settings.json` |
146
+ 저장 앱을 **재시작**하면 법령 도구가 활성화됩니다.
89
147
 
90
- ### 원격 MCP (설치 없이 바로)
148
+ ---
91
149
 
92
- ```json
93
- {
94
- "mcpServers": {
95
- "korean-law": {
96
- "url": "https://korean-law-mcp.fly.dev/mcp"
97
- }
98
- }
99
- }
150
+ ### 방법 3: 내 컴퓨터에 직접 설치 (오프라인 가능)
151
+
152
+ 인터넷 없이 쓰고 싶거나, 원격 서버를 거치지 않으려면 직접 설치할 수 있습니다.
153
+
154
+ **사전 준비:** [Node.js](https://nodejs.org) 18 이상이 설치되어 있어야 합니다.
155
+
156
+ **1. 터미널(명령 프롬프트)을 열고 설치합니다:**
157
+
158
+ ```bash
159
+ npm install -g korean-law-mcp
100
160
  ```
101
161
 
102
- **Claude.ai 클라이언트** 컨텍스트 절약을 위해 lite 프로필 권장:
162
+ **2. AI 설정 파일에 아래 내용을 추가합니다** (`honggildong`을 본인 인증키로 바꾸세요):
103
163
 
104
164
  ```json
105
165
  {
106
166
  "mcpServers": {
107
167
  "korean-law": {
108
- "url": "https://korean-law-mcp.fly.dev/mcp?profile=lite"
168
+ "command": "korean-law-mcp",
169
+ "env": {
170
+ "LAW_OC": "honggildong"
171
+ }
109
172
  }
110
173
  }
111
174
  }
112
175
  ```
113
176
 
114
- > lite 프로필은 체인 8개 + 핵심 4개 + 메타 2개 = **14개 도구**로 동일 기능 커버. 특수 도구가 필요하면 `discover_tools` → `execute_tool`로 접근.
177
+ **3. 앱을 재시작하면 완료!**
178
+
179
+ ---
180
+
181
+ ### 방법 4: 터미널(CLI)에서 직접 사용
115
182
 
116
- ### CLI
183
+ 개발자라면 터미널에서 직접 법령을 검색할 수 있습니다.
117
184
 
118
185
  ```bash
186
+ # 설치
119
187
  npm install -g korean-law-mcp
120
- export LAW_OC=your-api-key
121
-
122
- korean-law search_law --query "관세법"
123
- korean-law get_law_text --mst 160001 --jo "제38조"
124
- korean-law search_precedents --query "부당해고"
125
- korean-law list # 89개 전체 도구 목록
126
- korean-law list --category 판례 # 카테고리별 필터
127
- korean-law help search_law # 도구 도움말
188
+
189
+ # 인증키 설정 (honggildong을 본인 키로 바꾸세요)
190
+ export LAW_OC=honggildong # Mac/Linux
191
+ set LAW_OC=honggildong # Windows CMD
192
+ $env:LAW_OC="honggildong" # Windows PowerShell
193
+
194
+ # 사용 예시
195
+ korean-law "민법 제1조" # 자연어로 바로 조회
196
+ korean-law search_law --query "관세법" # 도구 직접 호출
197
+ korean-law list # 89개 전체 도구 목록
198
+ korean-law list --category 판례 # 카테고리별 필터
199
+ korean-law help search_law # 도구별 도움말
128
200
  ```
129
201
 
130
202
  ---
131
203
 
204
+ ### API 키 전달 방법 정리
205
+
206
+ 여러 방법으로 인증키를 전달할 수 있습니다. 위에서부터 우선 적용됩니다:
207
+
208
+ | 방법 | 사용법 | 언제 쓰나 |
209
+ |------|--------|-----------|
210
+ | URL에 포함 | 주소 끝에 `?oc=내키` | 웹 클라이언트에서 가장 간편 |
211
+ | HTTP 헤더 | `apikey: 내키` | 프로그래밍으로 연동할 때 |
212
+ | 환경변수 | `LAW_OC=내키` | 로컬 설치(방법 3, 4) |
213
+ | 도구 파라미터 | `apiKey: "내키"` | 특정 요청만 다른 키 쓸 때 |
214
+
215
+ ---
216
+
132
217
  ## 사용 예시
133
218
 
134
219
  ```
@@ -81,28 +81,9 @@ export class LawApiClient {
81
81
  const response = await fetchWithRetry(url);
82
82
  this.throwIfError(response, "getLawText");
83
83
  const text = await response.text();
84
- if (text.includes("<!DOCTYPE html") || text.includes("<html")) {
85
- let errorMsg = "법령을 찾을 수 없습니다.";
86
- if (params.jo) {
87
- errorMsg += "\n\n💡 개선 방법:";
88
- errorMsg += "\n 1. 전체 법령 조회 (조문 범위 확인):";
89
- if (params.mst) {
90
- errorMsg += `\n get_law_text(mst="${params.mst}")`;
91
- }
92
- else if (params.lawId) {
93
- errorMsg += `\n get_law_text(lawId="${params.lawId}")`;
94
- }
95
- errorMsg += "\n\n 2. 키워드 검색:";
96
- errorMsg += `\n search_all(query="관련 키워드")`;
97
- errorMsg += "\n\n 3. 법령 검색:";
98
- errorMsg += `\n search_law(query="법령명")`;
99
- errorMsg += "\n\n ℹ️ 일부 법령은 조문 수가 적습니다 (예: 약사법 시행령 제1~39조)";
100
- }
101
- else {
102
- errorMsg += " MST 또는 법령명을 확인해주세요.";
103
- }
104
- throw new Error(errorMsg);
105
- }
84
+ this.checkHtmlError(text, params.jo
85
+ ? `법령 조문(${params.jo})을 찾을 수 없습니다. MST/lawId와 조문번호를 확인해주세요.`
86
+ : "법령을 찾을 수 없습니다. MST 또는 법령명을 확인해주세요.");
106
87
  return text;
107
88
  }
108
89
  /**
@@ -5,5 +5,20 @@
5
5
  export declare function flattenContent(value: any): string;
6
6
  /** 항 배열에서 내용 추출 (재귀적으로 호/목 처리) */
7
7
  export declare function extractHangContent(hangInput: any[] | any): string;
8
+ /**
9
+ * 조문단위 객체를 텍스트로 포맷팅 (law-text, batch-articles, article-detail 공통)
10
+ * 조문 헤더(제X조 제목) + 본문 + 항/호/목을 결합하여 반환
11
+ */
12
+ export declare function formatArticleUnit(unit: {
13
+ 조문여부?: string;
14
+ 조문번호?: string;
15
+ 조문가지번호?: string;
16
+ 조문제목?: string;
17
+ 조문내용?: unknown;
18
+ 항?: unknown;
19
+ }): {
20
+ header: string;
21
+ body: string;
22
+ } | null;
8
23
  /** HTML 정리 - 엔티티 디코딩 순서 중요: &amp; 최후 처리 (이중 인코딩 방지) */
9
24
  export declare function cleanHtml(text: string): string;
@@ -61,6 +61,57 @@ export function extractHangContent(hangInput) {
61
61
  }
62
62
  return content;
63
63
  }
64
+ /**
65
+ * 조문단위 객체를 텍스트로 포맷팅 (law-text, batch-articles, article-detail 공통)
66
+ * 조문 헤더(제X조 제목) + 본문 + 항/호/목을 결합하여 반환
67
+ */
68
+ export function formatArticleUnit(unit) {
69
+ if (unit.조문여부 !== "조문")
70
+ return null;
71
+ const joNum = unit.조문번호 || "";
72
+ const joBranch = unit.조문가지번호 || "";
73
+ const joTitle = unit.조문제목 || "";
74
+ // 헤더
75
+ let header = "";
76
+ if (joNum) {
77
+ const displayNum = joBranch && joBranch !== "0" ? `제${joNum}조의${joBranch}` : `제${joNum}조`;
78
+ header = joTitle ? `${displayNum} ${joTitle}` : displayNum;
79
+ }
80
+ // 본문: 조문내용에서 헤더 패턴 제거
81
+ let mainContent = "";
82
+ if (unit.조문내용) {
83
+ const contentStr = flattenContent(unit.조문내용);
84
+ if (contentStr) {
85
+ const headerMatch = contentStr.match(/^(제\d+조(?:의\d+)?\s*(?:\([^)]+\))?)[\s\S]*/);
86
+ if (headerMatch) {
87
+ const bodyPart = contentStr.substring(headerMatch[1].length).trim();
88
+ mainContent = bodyPart || contentStr;
89
+ }
90
+ else {
91
+ mainContent = contentStr;
92
+ }
93
+ }
94
+ }
95
+ // 항/호/목
96
+ let paraContent = "";
97
+ if (unit.항) {
98
+ paraContent = extractHangContent(unit.항);
99
+ }
100
+ // 결합
101
+ let body = "";
102
+ if (mainContent) {
103
+ body = mainContent;
104
+ if (paraContent)
105
+ body += "\n" + paraContent;
106
+ }
107
+ else {
108
+ body = paraContent;
109
+ }
110
+ // HTML 정리
111
+ if (body)
112
+ body = cleanHtml(body);
113
+ return { header, body };
114
+ }
64
115
  /** HTML 정리 - 엔티티 디코딩 순서 중요: &amp; 최후 처리 (이중 인코딩 방지) */
65
116
  export function cleanHtml(text) {
66
117
  return text
@@ -21,9 +21,6 @@ export declare class LawApiError extends Error {
21
21
  code: ErrorCode;
22
22
  suggestions: string[];
23
23
  constructor(message: string, code: ErrorCode, suggestions?: string[]);
24
- /**
25
- * 사용자 친화적 포맷
26
- */
27
24
  format(): string;
28
25
  }
29
26
  /**
@@ -35,15 +32,3 @@ export declare class LawApiError extends Error {
35
32
  * 💡 제안: ...
36
33
  */
37
34
  export declare function formatToolError(error: unknown, context?: string): ToolResponse;
38
- /**
39
- * 법령 없음 에러
40
- */
41
- export declare function notFoundError(lawName: string, suggestions?: string[]): LawApiError;
42
- /**
43
- * API 에러
44
- */
45
- export declare function apiError(status: number, endpoint?: string): LawApiError;
46
- /**
47
- * 파라미터 검증 에러
48
- */
49
- export declare function invalidParamError(param: string, expected: string): LawApiError;
@@ -22,15 +22,12 @@ export class LawApiError extends Error {
22
22
  this.code = code;
23
23
  this.suggestions = suggestions;
24
24
  }
25
- /**
26
- * 사용자 친화적 포맷
27
- */
28
25
  format() {
29
- let result = `❌ ${this.message}`;
26
+ let result = `[ERROR] ${this.message}`;
30
27
  if (this.suggestions.length > 0) {
31
- result += "\n\n💡 개선 방법:";
28
+ result += "\n제안:";
32
29
  this.suggestions.forEach((s, i) => {
33
- result += `\n ${i + 1}. ${s}`;
30
+ result += `\n ${i + 1}. ${s}`;
34
31
  });
35
32
  }
36
33
  return result;
@@ -73,49 +70,19 @@ export function formatToolError(error, context) {
73
70
  msg = String(error);
74
71
  suggestions = [];
75
72
  }
76
- // 구조화된 텍스트 조립
77
73
  const lines = [];
78
- lines.push(`❌ [${code}] ${msg}`);
74
+ lines.push(`[${code}] ${msg}`);
79
75
  if (context) {
80
- lines.push(`🔧 도구: ${context}`);
76
+ lines.push(`도구: ${context}`);
81
77
  }
82
78
  if (suggestions.length > 0) {
83
- lines.push("💡 제안:");
79
+ lines.push("제안:");
84
80
  suggestions.forEach((s, i) => {
85
- lines.push(` ${i + 1}. ${s}`);
81
+ lines.push(` ${i + 1}. ${s}`);
86
82
  });
87
83
  }
88
- else {
89
- lines.push("💡 제안: (없음)");
90
- }
91
84
  return {
92
85
  content: [{ type: "text", text: lines.join("\n") }],
93
86
  isError: true,
94
87
  };
95
88
  }
96
- /**
97
- * 법령 없음 에러
98
- */
99
- export function notFoundError(lawName, suggestions) {
100
- return new LawApiError(`'${lawName}'을(를) 찾을 수 없습니다.`, ErrorCodes.NOT_FOUND, suggestions || [
101
- `search_law(query="${lawName}")로 법령 검색`,
102
- "법령명 철자 확인",
103
- ]);
104
- }
105
- /**
106
- * API 에러
107
- */
108
- export function apiError(status, endpoint) {
109
- const suggestions = status === 429
110
- ? ["잠시 후 다시 시도", "요청 빈도 줄이기"]
111
- : status >= 500
112
- ? ["법제처 API 상태 확인", "잠시 후 다시 시도"]
113
- : ["요청 파라미터 확인"];
114
- return new LawApiError(`API 오류 (${status})${endpoint ? ` - ${endpoint}` : ""}`, status === 429 ? ErrorCodes.RATE_LIMITED : ErrorCodes.API_ERROR, suggestions);
115
- }
116
- /**
117
- * 파라미터 검증 에러
118
- */
119
- export function invalidParamError(param, expected) {
120
- return new LawApiError(`잘못된 파라미터: ${param}`, ErrorCodes.INVALID_PARAM, [`${param}는 ${expected} 형식이어야 합니다.`]);
121
- }
@@ -28,8 +28,7 @@ export async function fetchWithRetry(url, options = {}) {
28
28
  }
29
29
  // Retryable error - check if we have retries left
30
30
  if (attempt < retries) {
31
- const baseDelay = retryDelay * Math.pow(2, attempt);
32
- const delay = baseDelay + Math.random() * baseDelay * 0.5;
31
+ const delay = getRetryDelay(response, retryDelay, attempt);
33
32
  await sleep(delay);
34
33
  continue;
35
34
  }
@@ -49,8 +48,7 @@ export async function fetchWithRetry(url, options = {}) {
49
48
  }
50
49
  // Retry on network errors
51
50
  if (attempt < retries) {
52
- const baseDelay = retryDelay * Math.pow(2, attempt);
53
- const delay = baseDelay + Math.random() * baseDelay * 0.5;
51
+ const delay = getRetryDelay(null, retryDelay, attempt);
54
52
  await sleep(delay);
55
53
  continue;
56
54
  }
@@ -58,6 +56,20 @@ export async function fetchWithRetry(url, options = {}) {
58
56
  }
59
57
  throw lastError || new Error("Request failed after retries");
60
58
  }
59
+ /** Retry-After 헤더 우선, 없으면 exponential backoff + jitter */
60
+ function getRetryDelay(response, retryDelay, attempt) {
61
+ if (response) {
62
+ const retryAfter = response.headers.get("Retry-After");
63
+ if (retryAfter) {
64
+ const seconds = Number(retryAfter);
65
+ if (!isNaN(seconds) && seconds > 0) {
66
+ return seconds * 1000;
67
+ }
68
+ }
69
+ }
70
+ const baseDelay = retryDelay * Math.pow(2, attempt);
71
+ return baseDelay + Math.random() * baseDelay * 0.5;
72
+ }
61
73
  function sleep(ms) {
62
74
  return new Promise((resolve) => setTimeout(resolve, ms));
63
75
  }
@@ -10,10 +10,6 @@ export declare const dateSchema: z.ZodString;
10
10
  * 선택적 날짜 스키마
11
11
  */
12
12
  export declare const optionalDateSchema: z.ZodOptional<z.ZodString>;
13
- /**
14
- * API 키 스키마
15
- */
16
- export declare const apiKeySchema: z.ZodOptional<z.ZodString>;
17
13
  /**
18
14
  * 페이지네이션 스키마
19
15
  */
@@ -21,10 +17,6 @@ export declare const paginationSchema: z.ZodObject<{
21
17
  display: z.ZodDefault<z.ZodNumber>;
22
18
  page: z.ZodDefault<z.ZodNumber>;
23
19
  }, z.core.$strip>;
24
- /**
25
- * 날짜 포맷터 (YYYYMMDD → "2024년 1월 1일")
26
- */
27
- export declare function formatDateKorean(dateStr: string | undefined | null): string;
28
20
  /**
29
21
  * 응답 크기 제한 (50KB)
30
22
  */
@@ -30,10 +30,6 @@ export const dateSchema = z
30
30
  * 선택적 날짜 스키마
31
31
  */
32
32
  export const optionalDateSchema = dateSchema.optional();
33
- /**
34
- * API 키 스키마
35
- */
36
- export const apiKeySchema = z.string().optional().describe("API 키 (생략시 환경변수 사용)");
37
33
  /**
38
34
  * 페이지네이션 스키마
39
35
  */
@@ -41,17 +37,6 @@ export const paginationSchema = z.object({
41
37
  display: z.number().min(1).max(100).default(20).describe("결과 수 (기본:20, 최대:100)"),
42
38
  page: z.number().min(1).default(1).describe("페이지 번호 (기본:1)"),
43
39
  });
44
- /**
45
- * 날짜 포맷터 (YYYYMMDD → "2024년 1월 1일")
46
- */
47
- export function formatDateKorean(dateStr) {
48
- if (!dateStr || dateStr.length < 8)
49
- return dateStr || "N/A";
50
- const y = dateStr.substring(0, 4);
51
- const m = parseInt(dateStr.substring(4, 6), 10);
52
- const d = parseInt(dateStr.substring(6, 8), 10);
53
- return `${y}년 ${m}월 ${d}일`;
54
- }
55
40
  /**
56
41
  * 응답 크기 제한 (50KB)
57
42
  */