@mseep/korean-dart-mcp 0.9.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +660 -0
  3. package/build/cli.d.ts +2 -0
  4. package/build/cli.js +22 -0
  5. package/build/index.d.ts +2 -0
  6. package/build/index.js +36 -0
  7. package/build/lib/corp-code.d.ts +53 -0
  8. package/build/lib/corp-code.js +235 -0
  9. package/build/lib/dart-client.d.ts +25 -0
  10. package/build/lib/dart-client.js +71 -0
  11. package/build/lib/dart-xml.d.ts +26 -0
  12. package/build/lib/dart-xml.js +187 -0
  13. package/build/lib/xbrl-parser.d.ts +145 -0
  14. package/build/lib/xbrl-parser.js +673 -0
  15. package/build/server/mcp-server.d.ts +7 -0
  16. package/build/server/mcp-server.js +40 -0
  17. package/build/setup.d.ts +8 -0
  18. package/build/setup.js +264 -0
  19. package/build/tools/_helpers.d.ts +28 -0
  20. package/build/tools/_helpers.js +35 -0
  21. package/build/tools/buffett-quality-snapshot.d.ts +10 -0
  22. package/build/tools/buffett-quality-snapshot.js +261 -0
  23. package/build/tools/disclosure-anomaly.d.ts +14 -0
  24. package/build/tools/disclosure-anomaly.js +231 -0
  25. package/build/tools/download-document.d.ts +14 -0
  26. package/build/tools/download-document.js +89 -0
  27. package/build/tools/get-attachments.d.ts +15 -0
  28. package/build/tools/get-attachments.js +339 -0
  29. package/build/tools/get-company.d.ts +7 -0
  30. package/build/tools/get-company.js +32 -0
  31. package/build/tools/get-corporate-event.d.ts +14 -0
  32. package/build/tools/get-corporate-event.js +180 -0
  33. package/build/tools/get-executive-compensation.d.ts +13 -0
  34. package/build/tools/get-executive-compensation.js +89 -0
  35. package/build/tools/get-financials.d.ts +15 -0
  36. package/build/tools/get-financials.js +127 -0
  37. package/build/tools/get-major-holdings.d.ts +10 -0
  38. package/build/tools/get-major-holdings.js +117 -0
  39. package/build/tools/get-periodic-report.d.ts +7 -0
  40. package/build/tools/get-periodic-report.js +100 -0
  41. package/build/tools/get-shareholders.d.ts +13 -0
  42. package/build/tools/get-shareholders.js +87 -0
  43. package/build/tools/get-xbrl.d.ts +12 -0
  44. package/build/tools/get-xbrl.js +96 -0
  45. package/build/tools/index.d.ts +38 -0
  46. package/build/tools/index.js +66 -0
  47. package/build/tools/insider-signal.d.ts +15 -0
  48. package/build/tools/insider-signal.js +208 -0
  49. package/build/tools/resolve-corp-code.d.ts +7 -0
  50. package/build/tools/resolve-corp-code.js +40 -0
  51. package/build/tools/search-disclosures.d.ts +14 -0
  52. package/build/tools/search-disclosures.js +300 -0
  53. package/build/utils/safe-zip.d.ts +47 -0
  54. package/build/utils/safe-zip.js +215 -0
  55. package/build/version.d.ts +2 -0
  56. package/build/version.js +2 -0
  57. package/package.json +67 -0
@@ -0,0 +1,339 @@
1
+ /**
2
+ * get_attachments — 공시 첨부파일 목록 조회 + HWP/PDF → 마크다운 추출
3
+ *
4
+ * OpenDART API 는 첨부파일 직접 엔드포인트가 없다. DART 뷰어 HTML 을 스크래핑해야 한다.
5
+ * OpenDartReader 도 동일 패턴. 사실상 업계 표준.
6
+ *
7
+ * 1. 뷰어 HTML `/dsaf001/main.do?rcpNo=...` → JS 변수 `node1['dcmNo']` 추출
8
+ * 2. 다운로드 페이지 `/pdf/download/main.do?rcp_no=&dcm_no=` → `<td class="tL">` 파싱
9
+ * 3. 각 첨부 URL (`/pdf/download/pdf.do?...` 등) fetch → Buffer
10
+ * 4. kordoc.parse() 로 HWP/HWPX/PDF/DOCX/XLSX → 마크다운
11
+ *
12
+ * mode="list" — 첨부 목록만 반환 (가볍고 빠름, 파일명·download_url·format 힌트)
13
+ * mode="extract" — 지정한 파일을 다운로드·파싱해 마크다운 반환
14
+ */
15
+ import { z } from "zod";
16
+ import { parse as kordocParse } from "kordoc";
17
+ import { defineTool } from "./_helpers.js";
18
+ import { safeUnzipToMemory } from "../utils/safe-zip.js";
19
+ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36";
20
+ const DART_ORIGIN = "https://dart.fss.or.kr";
21
+ const Input = z
22
+ .object({
23
+ rcept_no: z
24
+ .string()
25
+ .regex(/^\d{14}$/)
26
+ .describe("접수번호 14자리"),
27
+ mode: z
28
+ .enum(["list", "extract"])
29
+ .default("list")
30
+ .describe("list: 첨부 목록만. extract: 파일 하나 다운·파싱해 마크다운"),
31
+ filename: z
32
+ .string()
33
+ .optional()
34
+ .describe("extract 모드에서 정확한 파일명 (부분일치 폴백 없음)"),
35
+ index: z
36
+ .number()
37
+ .int()
38
+ .min(0)
39
+ .optional()
40
+ .describe("extract 모드에서 0-based index (filename 우선)"),
41
+ truncate_at: z
42
+ .number()
43
+ .int()
44
+ .min(1000)
45
+ .default(100000)
46
+ .describe("extract 마크다운 최대 길이"),
47
+ outline_max_items: z
48
+ .number()
49
+ .int()
50
+ .min(0)
51
+ .max(500)
52
+ .default(50)
53
+ .describe("outline(목차) 최대 항목 수. 사업보고서 outline 은 수천 개 → 디폴트 50. 0=outline 생략."),
54
+ zip_index: z
55
+ .number()
56
+ .int()
57
+ .min(0)
58
+ .optional()
59
+ .describe("ZIP 첨부 extract 시 내부 파일 index (0-based). 미지정 & ZIP 인 경우 내부 파일 목록만 반환."),
60
+ })
61
+ .refine((v) => v.mode !== "extract" || v.filename !== undefined || v.index !== undefined, { message: "extract 모드엔 filename 또는 index 중 하나 필수" });
62
+ const EXT_FORMAT = [
63
+ [/\.hwpx$/i, "hwpx"],
64
+ [/\.hwp$/i, "hwp"],
65
+ [/\.pdf$/i, "pdf"],
66
+ [/\.docx$/i, "docx"],
67
+ [/\.doc$/i, "doc"],
68
+ [/\.xlsx$/i, "xlsx"],
69
+ [/\.xls$/i, "xls"],
70
+ [/\.zip$/i, "zip"],
71
+ [/\.html?$/i, "html"],
72
+ ];
73
+ function detectFormat(filename) {
74
+ for (const [re, fmt] of EXT_FORMAT) {
75
+ if (re.test(filename))
76
+ return fmt;
77
+ }
78
+ return "unknown";
79
+ }
80
+ /** 뷰어 HTML 에서 첫 번째 `node[12]['dcmNo']` 추출. 단일-페이지 viewDoc() 도 폴백. */
81
+ function extractDcmNo(html, rcptNo) {
82
+ const nodeRe = new RegExp(`node[12]\\['rcpNo'\\]\\s*=\\s*"${rcptNo}";\\s*node[12]\\['dcmNo'\\]\\s*=\\s*"(\\d+)"`);
83
+ const nodeMatch = nodeRe.exec(html);
84
+ if (nodeMatch)
85
+ return nodeMatch[1];
86
+ const singleRe = /viewDoc\('(\d+)',\s*'(\d+)'/;
87
+ const s = singleRe.exec(html);
88
+ return s?.[2] ?? null;
89
+ }
90
+ /** 다운로드 페이지 HTML 에서 첨부 테이블 행 추출. 주석 블록은 먼저 제거. */
91
+ function parseAttachmentTable(html) {
92
+ const cleaned = html.replace(/<!--[\s\S]*?-->/g, "");
93
+ const rowRe = /<td class="tL">\s*([^<]+?)\s*<\/td>\s*<td>\s*<a class="btnFile"\s+href="([^"]+)"/g;
94
+ const rows = [];
95
+ for (const m of cleaned.matchAll(rowRe)) {
96
+ rows.push({ filename: m[1].trim(), href: m[2] });
97
+ }
98
+ return rows;
99
+ }
100
+ async function fetchHtml(url) {
101
+ const res = await fetch(url, { headers: { "User-Agent": USER_AGENT } });
102
+ if (!res.ok)
103
+ throw new Error(`DART viewer fetch 실패: ${url} → HTTP ${res.status}`);
104
+ return await res.text();
105
+ }
106
+ async function fetchBinary(url) {
107
+ const res = await fetch(url, { headers: { "User-Agent": USER_AGENT } });
108
+ if (!res.ok)
109
+ throw new Error(`DART 첨부 다운로드 실패: ${url} → HTTP ${res.status}`);
110
+ return Buffer.from(await res.arrayBuffer());
111
+ }
112
+ function extractZipEntries(buf) {
113
+ return safeUnzipToMemory(buf);
114
+ }
115
+ async function listAttachments(rcept_no) {
116
+ const viewerUrl = `${DART_ORIGIN}/dsaf001/main.do?rcpNo=${rcept_no}`;
117
+ const viewerHtml = await fetchHtml(viewerUrl);
118
+ const dcm_no = extractDcmNo(viewerHtml, rcept_no);
119
+ if (!dcm_no) {
120
+ // 거래소공시(pblntf_ty=I) 등 일부는 DART 뷰어에 dcm_no 가 내재되지 않아
121
+ // 첨부파일 다운로드 링크 자체가 존재하지 않는다. DART 표준 API·스크래핑 모두
122
+ // 접근 불가. 본문 텍스트가 필요하면 download_document 로 원문 XML 조회.
123
+ return {
124
+ dcm_no: null,
125
+ viewer_url: viewerUrl,
126
+ download_page_url: null,
127
+ attachments: [],
128
+ supported: false,
129
+ unsupported_reason: "DART 뷰어에 dcm_no 가 내재되지 않음 (거래소공시 등). 첨부 직접 접근 불가. 대안: download_document(rcept_no) 로 원문 XML 조회.",
130
+ };
131
+ }
132
+ const dlPageUrl = `${DART_ORIGIN}/pdf/download/main.do?rcp_no=${rcept_no}&dcm_no=${dcm_no}`;
133
+ const dlHtml = await fetchHtml(dlPageUrl);
134
+ const rows = parseAttachmentTable(dlHtml);
135
+ const attachments = rows.map((r, i) => ({
136
+ index: i,
137
+ filename: r.filename,
138
+ download_url: r.href.startsWith("http") ? r.href : `${DART_ORIGIN}${r.href}`,
139
+ format: detectFormat(r.filename),
140
+ }));
141
+ return {
142
+ dcm_no,
143
+ viewer_url: viewerUrl,
144
+ download_page_url: dlPageUrl,
145
+ attachments,
146
+ supported: true,
147
+ };
148
+ }
149
+ export const getAttachmentsTool = defineTool({
150
+ name: "get_attachments",
151
+ description: "공시 첨부파일(HWP/PDF/DOCX/XLSX)을 목록 조회(mode=list) 하거나 다운받아 마크다운으로 추출(mode=extract). " +
152
+ "DART 뷰어 HTML 스크래핑 기반 — OpenDART 표준 API 에 첨부 엔드포인트가 없어 공식 뷰어를 통해 접근. " +
153
+ "extract 모드는 kordoc 엔진으로 HWP/HWPX/PDF/DOCX/XLSX → 마크다운 변환.",
154
+ input: Input,
155
+ handler: async (_ctx, args) => {
156
+ const info = await listAttachments(args.rcept_no);
157
+ // outline 잘라내기 헬퍼 — 사업보고서 PDF 는 수천 항목 outline 이라 응답 폭발
158
+ const truncOutline = (o) => {
159
+ if (!o)
160
+ return null;
161
+ if (!Array.isArray(o))
162
+ return { items: o, total: 1, truncated: false };
163
+ const total = o.length;
164
+ if (args.outline_max_items === 0) {
165
+ return { items: [], total, truncated: total > 0 };
166
+ }
167
+ const items = o.slice(0, args.outline_max_items);
168
+ return { items, total, truncated: total > args.outline_max_items };
169
+ };
170
+ if (args.mode === "list") {
171
+ return {
172
+ rcept_no: args.rcept_no,
173
+ supported: info.supported,
174
+ dcm_no: info.dcm_no,
175
+ viewer_url: info.viewer_url,
176
+ download_page_url: info.download_page_url,
177
+ count: info.attachments.length,
178
+ attachments: info.attachments,
179
+ unsupported_reason: info.unsupported_reason ?? null,
180
+ };
181
+ }
182
+ if (!info.supported) {
183
+ return {
184
+ rcept_no: args.rcept_no,
185
+ supported: false,
186
+ viewer_url: info.viewer_url,
187
+ unsupported_reason: info.unsupported_reason,
188
+ suggestion: {
189
+ tool: "download_document",
190
+ args: { rcept_no: args.rcept_no, format: "markdown" },
191
+ },
192
+ };
193
+ }
194
+ // extract 모드
195
+ let target;
196
+ if (args.filename) {
197
+ target = info.attachments.find((a) => a.filename === args.filename);
198
+ if (!target) {
199
+ throw new Error(`파일명 일치 없음: "${args.filename}". 사용 가능: ${info.attachments
200
+ .map((a) => a.filename)
201
+ .join(" / ")}`);
202
+ }
203
+ }
204
+ else if (args.index !== undefined) {
205
+ target = info.attachments[args.index];
206
+ if (!target) {
207
+ throw new Error(`index 범위 초과: ${args.index} (총 ${info.attachments.length}개)`);
208
+ }
209
+ }
210
+ if (!target) {
211
+ throw new Error("extract 모드엔 filename 또는 index 필수");
212
+ }
213
+ if (target.format === "unknown") {
214
+ return {
215
+ rcept_no: args.rcept_no,
216
+ filename: target.filename,
217
+ format: target.format,
218
+ download_url: target.download_url,
219
+ supported: false,
220
+ note: "확장자로 포맷 판별 실패. download_url 로 직접 받아 확인.",
221
+ };
222
+ }
223
+ if (target.format === "zip") {
224
+ // XBRL ZIP 은 전용 get_xbrl 로 처리하는 게 적절
225
+ if (/XBRL/i.test(target.filename)) {
226
+ return {
227
+ rcept_no: args.rcept_no,
228
+ filename: target.filename,
229
+ format: "zip",
230
+ download_url: target.download_url,
231
+ supported: false,
232
+ note: "XBRL ZIP 은 get_xbrl(rcept_no) 로 조회 — 파싱된 재무제표 + 원본 파일 경로 제공.",
233
+ };
234
+ }
235
+ const zipBuf = await fetchBinary(target.download_url);
236
+ const innerFiles = await extractZipEntries(zipBuf);
237
+ const parsable = innerFiles
238
+ .map((f, i) => ({ ...f, index: i, format: detectFormat(f.name) }))
239
+ .filter((f) => f.format !== "zip" && f.format !== "unknown");
240
+ if (args.zip_index === undefined) {
241
+ return {
242
+ rcept_no: args.rcept_no,
243
+ filename: target.filename,
244
+ format: "zip",
245
+ download_url: target.download_url,
246
+ size_bytes: zipBuf.length,
247
+ zip_mode: "list",
248
+ total_entries: innerFiles.length,
249
+ parsable_count: parsable.length,
250
+ entries: parsable.map((f) => ({
251
+ index: f.index,
252
+ name: f.name,
253
+ size_bytes: f.data.length,
254
+ format: f.format,
255
+ })),
256
+ note: "zip_index 를 지정하면 해당 파일을 kordoc 으로 파싱한다.",
257
+ };
258
+ }
259
+ const inner = innerFiles[args.zip_index];
260
+ if (!inner) {
261
+ throw new Error(`zip_index 범위 초과: ${args.zip_index} (ZIP 내부 ${innerFiles.length}개)`);
262
+ }
263
+ const innerFmt = detectFormat(inner.name);
264
+ if (innerFmt === "unknown" || innerFmt === "zip") {
265
+ return {
266
+ rcept_no: args.rcept_no,
267
+ filename: target.filename,
268
+ zip_inner_filename: inner.name,
269
+ zip_inner_format: innerFmt,
270
+ size_bytes: inner.data.length,
271
+ supported: false,
272
+ note: "내부 파일이 ZIP 또는 알 수 없는 포맷. 재귀 파싱 미지원.",
273
+ };
274
+ }
275
+ const innerSize = inner.data.length;
276
+ const innerParsed = await kordocParse(inner.data);
277
+ if (!innerParsed.success) {
278
+ return {
279
+ rcept_no: args.rcept_no,
280
+ filename: target.filename,
281
+ zip_inner_filename: inner.name,
282
+ zip_inner_format: innerFmt,
283
+ size_bytes: innerSize,
284
+ supported: false,
285
+ error: innerParsed.error,
286
+ error_code: innerParsed.code,
287
+ };
288
+ }
289
+ const innerMd = innerParsed.markdown ?? "";
290
+ const innerTruncated = innerMd.length > args.truncate_at;
291
+ return {
292
+ rcept_no: args.rcept_no,
293
+ filename: target.filename,
294
+ zip_inner_filename: inner.name,
295
+ zip_inner_format: innerFmt,
296
+ file_type_detected: innerParsed.fileType,
297
+ size_bytes: innerSize,
298
+ char_count: innerMd.length,
299
+ truncated: innerTruncated,
300
+ markdown: innerTruncated ? innerMd.slice(0, args.truncate_at) : innerMd,
301
+ warnings: innerParsed.warnings ?? [],
302
+ outline: truncOutline(innerParsed.outline),
303
+ metadata: innerParsed.metadata ?? null,
304
+ };
305
+ }
306
+ const buf = await fetchBinary(target.download_url);
307
+ // kordoc 내부에서 ArrayBuffer 를 detach 할 수 있어 파싱 전에 크기 캡처
308
+ const sizeBytes = buf.length;
309
+ const parsed = await kordocParse(buf);
310
+ if (!parsed.success) {
311
+ return {
312
+ rcept_no: args.rcept_no,
313
+ filename: target.filename,
314
+ format: target.format,
315
+ download_url: target.download_url,
316
+ size_bytes: sizeBytes,
317
+ supported: false,
318
+ error: parsed.error,
319
+ error_code: parsed.code,
320
+ };
321
+ }
322
+ const md = parsed.markdown ?? "";
323
+ const truncated = md.length > args.truncate_at;
324
+ return {
325
+ rcept_no: args.rcept_no,
326
+ filename: target.filename,
327
+ format: target.format,
328
+ file_type_detected: parsed.fileType,
329
+ download_url: target.download_url,
330
+ size_bytes: sizeBytes,
331
+ char_count: md.length,
332
+ truncated,
333
+ markdown: truncated ? md.slice(0, args.truncate_at) : md,
334
+ warnings: parsed.warnings ?? [],
335
+ outline: truncOutline(parsed.outline),
336
+ metadata: parsed.metadata ?? null,
337
+ };
338
+ },
339
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * get_company — 기업개황 조회 (company.json)
3
+ *
4
+ * 업종·설립일·대표자·주소·홈페이지 등 기본 정보.
5
+ * 참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019002
6
+ */
7
+ export declare const getCompanyTool: import("./_helpers.js").ToolDef;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * get_company — 기업개황 조회 (company.json)
3
+ *
4
+ * 업종·설립일·대표자·주소·홈페이지 등 기본 정보.
5
+ * 참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019002
6
+ */
7
+ import { z } from "zod";
8
+ import { defineTool, resolveCorp } from "./_helpers.js";
9
+ const Input = z.object({
10
+ corp: z
11
+ .string()
12
+ .min(1)
13
+ .describe("회사명/종목코드/corp_code"),
14
+ });
15
+ export const getCompanyTool = defineTool({
16
+ name: "get_company",
17
+ description: "기업의 개황(업종·설립일·대표자·주소·홈페이지·종목코드 등)을 조회합니다.",
18
+ input: Input,
19
+ handler: async (ctx, args) => {
20
+ const record = resolveCorp(ctx.resolver, args.corp);
21
+ const raw = await ctx.client.getJson("company.json", {
22
+ corp_code: record.corp_code,
23
+ });
24
+ if (raw.status !== "000") {
25
+ throw new Error(`DART 응답 오류 [${raw.status}]: ${raw.message}`);
26
+ }
27
+ return {
28
+ resolved: record,
29
+ company: raw,
30
+ };
31
+ },
32
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * get_corporate_event — DS005 주요사항보고서 36개 이벤트 enum + timeline 모드
3
+ *
4
+ * ## mode: "single"
5
+ * 단일 event_type 조회. OpenDartReader dart_event 와 동일한 매핑.
6
+ *
7
+ * ## mode: "timeline" ← 킬러 포인트
8
+ * 지정 기간 동안 **자본 관련 주요 이벤트 전부**를 병렬 조회해 날짜순 통합.
9
+ * 기존 Python 래퍼(단일 조회만 가능)에서는 불가능한 "자본 스트레스 내러티브" 뷰.
10
+ * LLM 가 바로 "최근 3년 CB 2회 + 자사주 처분 → 조달 압박" 스토리 생성 가능.
11
+ *
12
+ * 매핑 출처: FinanceData/OpenDartReader dart_event.py
13
+ */
14
+ export declare const getCorporateEventTool: import("./_helpers.js").ToolDef;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * get_corporate_event — DS005 주요사항보고서 36개 이벤트 enum + timeline 모드
3
+ *
4
+ * ## mode: "single"
5
+ * 단일 event_type 조회. OpenDartReader dart_event 와 동일한 매핑.
6
+ *
7
+ * ## mode: "timeline" ← 킬러 포인트
8
+ * 지정 기간 동안 **자본 관련 주요 이벤트 전부**를 병렬 조회해 날짜순 통합.
9
+ * 기존 Python 래퍼(단일 조회만 가능)에서는 불가능한 "자본 스트레스 내러티브" 뷰.
10
+ * LLM 가 바로 "최근 3년 CB 2회 + 자사주 처분 → 조달 압박" 스토리 생성 가능.
11
+ *
12
+ * 매핑 출처: FinanceData/OpenDartReader dart_event.py
13
+ */
14
+ import { z } from "zod";
15
+ import { defineTool, normalizeDate, resolveCorp } from "./_helpers.js";
16
+ const EVENT = {
17
+ // 부실/법적 이슈
18
+ default_occurrence: { endpoint: "dfOcr", ko: "부도발생", capital: false },
19
+ business_suspension: { endpoint: "bsnSp", ko: "영업정지", capital: false },
20
+ rehabilitation_filing: { endpoint: "ctrcvsBgrq", ko: "회생절차 개시신청", capital: false },
21
+ dissolution_cause: { endpoint: "dsRsOcr", ko: "해산사유 발생", capital: false },
22
+ bank_management_start: { endpoint: "bnkMngtPcbg", ko: "채권은행 관리절차 개시", capital: false },
23
+ bank_management_stop: { endpoint: "bnkMngtPcsp", ko: "채권은행 관리절차 중단", capital: false },
24
+ litigation: { endpoint: "lwstLg", ko: "소송 등 제기", capital: false },
25
+ // 자본 증감
26
+ rights_offering: { endpoint: "piicDecsn", ko: "유상증자 결정", capital: true },
27
+ bonus_issue: { endpoint: "fricDecsn", ko: "무상증자 결정", capital: true },
28
+ rights_bonus_combo: { endpoint: "pifricDecsn", ko: "유무상증자 결정", capital: true },
29
+ capital_reduction: { endpoint: "crDecsn", ko: "감자 결정", capital: true },
30
+ // 사채 발행
31
+ cb_issuance: { endpoint: "cvbdIsDecsn", ko: "전환사채(CB) 발행결정", capital: true },
32
+ bw_issuance: { endpoint: "bdwtIsDecsn", ko: "신주인수권부사채(BW) 발행결정", capital: true },
33
+ eb_issuance: { endpoint: "exbdIsDecsn", ko: "교환사채(EB) 발행결정", capital: true },
34
+ cocobond_issuance: { endpoint: "wdCocobdIsDecsn", ko: "상각형 조건부자본증권 발행결정", capital: true },
35
+ // 자기주식
36
+ treasury_acquisition: { endpoint: "tsstkAqDecsn", ko: "자기주식 취득 결정", capital: true },
37
+ treasury_disposal: { endpoint: "tsstkDpDecsn", ko: "자기주식 처분 결정", capital: true },
38
+ treasury_trust_contract: { endpoint: "tsstkAqTrctrCnsDecsn", ko: "자기주식취득 신탁계약 체결", capital: true },
39
+ treasury_trust_cancel: { endpoint: "tsstkAqTrctrCcDecsn", ko: "자기주식취득 신탁계약 해지", capital: true },
40
+ // 지배구조 변경 (합병·분할·교환)
41
+ stock_exchange: { endpoint: "stkExtrDecsn", ko: "주식교환·이전 결정", capital: true },
42
+ company_split_merger: { endpoint: "cmpDvmgDecsn", ko: "회사분할합병 결정", capital: true },
43
+ company_split: { endpoint: "cmpDvDecsn", ko: "회사분할 결정", capital: true },
44
+ company_merger: { endpoint: "cmpMgDecsn", ko: "회사합병 결정", capital: true },
45
+ // 자산·영업 양수도
46
+ asset_transfer_etc: { endpoint: "astInhtrfEtcPtbkOpt", ko: "자산양수도(기타)·풋백옵션", capital: true },
47
+ tangible_asset_transfer: { endpoint: "tgastTrfDecsn", ko: "유형자산 양도 결정", capital: true },
48
+ tangible_asset_acquisition: { endpoint: "tgastInhDecsn", ko: "유형자산 양수 결정", capital: true },
49
+ other_corp_stock_transfer: { endpoint: "otcprStkInvscrTrfDecsn", ko: "타법인 주식·출자증권 양도", capital: true },
50
+ other_corp_stock_acquisition: { endpoint: "otcprStkInvscrInhDecsn", ko: "타법인 주식·출자증권 양수", capital: true },
51
+ business_transfer: { endpoint: "bsnTrfDecsn", ko: "영업양도 결정", capital: true },
52
+ business_acquisition: { endpoint: "bsnInhDecsn", ko: "영업양수 결정", capital: true },
53
+ bond_with_stock_right_acquisition: { endpoint: "stkrtbdInhDecsn", ko: "주권관련 사채권 양수", capital: true },
54
+ bond_with_stock_right_transfer: { endpoint: "stkrtbdTrfDecsn", ko: "주권관련 사채권 양도", capital: true },
55
+ // 해외 상장
56
+ overseas_listing_decision: { endpoint: "ovLstDecsn", ko: "해외상장 결정", capital: false },
57
+ overseas_delisting_decision: { endpoint: "ovDlstDecsn", ko: "해외상장폐지 결정", capital: false },
58
+ overseas_listing: { endpoint: "ovLst", ko: "해외상장", capital: false },
59
+ overseas_delisting: { endpoint: "ovDlst", ko: "해외상장폐지", capital: false },
60
+ };
61
+ const EVENT_TYPES = Object.keys(EVENT);
62
+ const Input = z
63
+ .object({
64
+ corp: z.string().min(1).describe("회사명/종목코드/corp_code"),
65
+ mode: z
66
+ .enum(["single", "timeline"])
67
+ .default("single")
68
+ .describe("single: 단일 event_type 조회. timeline: 여러 자본 관련 이벤트를 날짜순 통합 (킬러 모드)"),
69
+ event_type: z
70
+ .enum(EVENT_TYPES)
71
+ .optional()
72
+ .describe("single 모드 필수. 36개 이벤트 중 하나"),
73
+ event_types: z
74
+ .array(z.enum(EVENT_TYPES))
75
+ .optional()
76
+ .describe("timeline 모드용 수동 선택. 미지정 시 자본 관련 이벤트(capital=true) 전체 자동 선택"),
77
+ start: z.string().optional().describe("시작일 (YYYY-MM-DD / YYYYMMDD)"),
78
+ end: z.string().optional().describe("종료일 (YYYY-MM-DD / YYYYMMDD)"),
79
+ })
80
+ .refine((v) => v.mode !== "single" || !!v.event_type, { message: "mode=single 일 때 event_type 필수" });
81
+ async function fetchEvent(ctx, corp_code, type, bgn_de, end_de) {
82
+ const meta = EVENT[type];
83
+ try {
84
+ const raw = await ctx.client.getJson(`${meta.endpoint}.json`, {
85
+ corp_code,
86
+ bgn_de,
87
+ end_de,
88
+ });
89
+ if (raw.status !== "000") {
90
+ return { type, endpoint: meta.endpoint, status: raw.status, message: raw.message, items: [] };
91
+ }
92
+ return {
93
+ type,
94
+ endpoint: meta.endpoint,
95
+ status: raw.status,
96
+ count: raw.list?.length ?? 0,
97
+ items: raw.list ?? [],
98
+ };
99
+ }
100
+ catch (e) {
101
+ return {
102
+ type,
103
+ endpoint: meta.endpoint,
104
+ error: e instanceof Error ? e.message : String(e),
105
+ items: [],
106
+ };
107
+ }
108
+ }
109
+ /** DART 응답 rcept_dt (접수일자) 을 YYYY-MM-DD 로 변환. YYYYMMDD / YYYY-MM-DD 모두 수용. */
110
+ function pickEventDate(item) {
111
+ const d = item.rcept_dt || item.ctrt_cnsdt || item.fd_dcsn_cnsdt || "";
112
+ const digits = d.replace(/\D/g, "");
113
+ const m = /^(\d{4})(\d{2})(\d{2})$/.exec(digits);
114
+ return m ? `${m[1]}-${m[2]}-${m[3]}` : d;
115
+ }
116
+ export const getCorporateEventTool = defineTool({
117
+ name: "get_corporate_event",
118
+ description: "DS005 주요사항보고서 36종 이벤트 조회. " +
119
+ "mode='single' 은 단일 event_type 상세, " +
120
+ "mode='timeline' 은 자본 관련 이벤트(증자·감자·CB/BW/EB·자사주·합병분할·영업양수도 등)를 지정 기간 병렬 수집 후 날짜순 통합. " +
121
+ "timeline 은 '최근 N년 자본 스트레스 내러티브' 를 한 번에 뽑기 위한 킬러 모드.",
122
+ input: Input,
123
+ handler: async (ctx, args) => {
124
+ const record = resolveCorp(ctx.resolver, args.corp);
125
+ const bgn_de = args.start ? normalizeDate(args.start) : undefined;
126
+ const end_de = args.end ? normalizeDate(args.end) : undefined;
127
+ if (args.mode === "single") {
128
+ const result = await fetchEvent(ctx, record.corp_code, args.event_type, bgn_de, end_de);
129
+ return {
130
+ mode: "single",
131
+ resolved: record,
132
+ period: { start: bgn_de ?? null, end: end_de ?? null },
133
+ ...result,
134
+ };
135
+ }
136
+ // timeline 모드
137
+ const targets = args.event_types ??
138
+ Object.entries(EVENT)
139
+ .filter(([, meta]) => meta.capital)
140
+ .map(([k]) => k);
141
+ const sections = await Promise.all(targets.map((t) => fetchEvent(ctx, record.corp_code, t, bgn_de, end_de)));
142
+ // 통합 타임라인: 각 item 에 event_type/ko 메타 주입하고 날짜 역순 정렬
143
+ const timeline = [];
144
+ for (const section of sections) {
145
+ if (!section.items)
146
+ continue;
147
+ for (const item of section.items) {
148
+ timeline.push({
149
+ event_type: section.type,
150
+ event_ko: EVENT[section.type].ko,
151
+ date: pickEventDate(item),
152
+ ...item,
153
+ });
154
+ }
155
+ }
156
+ timeline.sort((a, b) => String(b.date).localeCompare(String(a.date)));
157
+ const typeCounts = {};
158
+ for (const entry of timeline) {
159
+ const t = String(entry.event_type);
160
+ typeCounts[t] = (typeCounts[t] ?? 0) + 1;
161
+ }
162
+ return {
163
+ mode: "timeline",
164
+ resolved: record,
165
+ period: { start: bgn_de ?? null, end: end_de ?? null },
166
+ event_types: targets,
167
+ total_events: timeline.length,
168
+ event_type_counts: typeCounts,
169
+ timeline,
170
+ sections_meta: sections.map((s) => ({
171
+ type: s.type,
172
+ endpoint: s.endpoint,
173
+ status: s.status ?? null,
174
+ count: s.count ?? 0,
175
+ message: s.message ?? null,
176
+ error: s.error ?? null,
177
+ })),
178
+ };
179
+ },
180
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * get_executive_compensation — 임원 보수 6개 섹션 병렬 합성
3
+ *
4
+ * - 전체 보수 (hmvAuditAllSttus)
5
+ * - 개인별 5억 이상 (hmvAuditIndvdlBySttus)
6
+ * - 상위 5명 (indvdlByPay)
7
+ * - 미등기 임원 (unrstExctvMendngSttus)
8
+ * - 주총 승인금액 (drctrAdtAllMendngSttusGmtsckConfmAmount)
9
+ * - 유형별 지급금액 (drctrAdtAllMendngSttusMendngPymntamtTyCl)
10
+ *
11
+ * "이 회사 연봉 구조 다 보여줘" 를 1회 호출로.
12
+ */
13
+ export declare const getExecutiveCompensationTool: import("./_helpers.js").ToolDef;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * get_executive_compensation — 임원 보수 6개 섹션 병렬 합성
3
+ *
4
+ * - 전체 보수 (hmvAuditAllSttus)
5
+ * - 개인별 5억 이상 (hmvAuditIndvdlBySttus)
6
+ * - 상위 5명 (indvdlByPay)
7
+ * - 미등기 임원 (unrstExctvMendngSttus)
8
+ * - 주총 승인금액 (drctrAdtAllMendngSttusGmtsckConfmAmount)
9
+ * - 유형별 지급금액 (drctrAdtAllMendngSttusMendngPymntamtTyCl)
10
+ *
11
+ * "이 회사 연봉 구조 다 보여줘" 를 1회 호출로.
12
+ */
13
+ import { z } from "zod";
14
+ import { defineTool, resolveCorp } from "./_helpers.js";
15
+ const REPORT_CODE = {
16
+ q1: "11013",
17
+ half: "11012",
18
+ q3: "11014",
19
+ annual: "11011",
20
+ };
21
+ const SECTIONS = {
22
+ total: "hmvAuditAllSttus",
23
+ individual_5eok: "hmvAuditIndvdlBySttus",
24
+ top5: "indvdlByPay",
25
+ unregistered: "unrstExctvMendngSttus",
26
+ approval_limit: "drctrAdtAllMendngSttusGmtsckConfmAmount",
27
+ by_type: "drctrAdtAllMendngSttusMendngPymntamtTyCl",
28
+ };
29
+ const Input = z.object({
30
+ corp: z.string().min(1).describe("회사명/종목코드/corp_code"),
31
+ year: z.number().int().min(2015),
32
+ report: z.enum(["q1", "half", "q3", "annual"]).default("annual"),
33
+ sections: z
34
+ .array(z.enum(Object.keys(SECTIONS)))
35
+ .optional()
36
+ .describe("조회할 섹션 (미지정 시 6개 모두). total=전체 평균, individual_5eok=개인별 5억↑, top5=상위 5인, unregistered=미등기 임원, approval_limit=주총 승인한도, by_type=직책별 지급금액"),
37
+ });
38
+ export const getExecutiveCompensationTool = defineTool({
39
+ name: "get_executive_compensation",
40
+ description: "임원 보수 6개 섹션을 한 번에 합성 조회: 전체·개인별 5억 이상·상위 5인·미등기·주총 승인금액·유형별. " +
41
+ "(단일 섹션은 get_periodic_report 로도 조회 가능.)",
42
+ input: Input,
43
+ handler: async (ctx, args) => {
44
+ const record = resolveCorp(ctx.resolver, args.corp);
45
+ const reprt_code = REPORT_CODE[args.report];
46
+ const bsns_year = String(args.year);
47
+ const targets = args.sections ?? Object.keys(SECTIONS);
48
+ const results = await Promise.all(targets.map(async (key) => {
49
+ const endpoint = SECTIONS[key];
50
+ try {
51
+ const raw = await ctx.client.getJson(`${endpoint}.json`, {
52
+ corp_code: record.corp_code,
53
+ bsns_year,
54
+ reprt_code,
55
+ });
56
+ if (raw.status !== "000") {
57
+ return {
58
+ section: key,
59
+ endpoint,
60
+ status: raw.status,
61
+ message: raw.message,
62
+ items: [],
63
+ };
64
+ }
65
+ return {
66
+ section: key,
67
+ endpoint,
68
+ status: raw.status,
69
+ count: raw.list?.length ?? 0,
70
+ items: raw.list ?? [],
71
+ };
72
+ }
73
+ catch (e) {
74
+ return {
75
+ section: key,
76
+ endpoint,
77
+ error: e instanceof Error ? e.message : String(e),
78
+ items: [],
79
+ };
80
+ }
81
+ }));
82
+ return {
83
+ resolved: record,
84
+ year: args.year,
85
+ report: args.report,
86
+ sections: results,
87
+ };
88
+ },
89
+ });