@nomadamas/k-skill 0.9.2 โ†’ 0.10.0

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 (54) hide show
  1. package/bin/k-skill.js +64 -43
  2. package/package.json +2 -2
  3. package/skills/biz-health-check/instruction.md +2 -0
  4. package/skills/company-analysis/instruction.md +68 -0
  5. package/skills/company-analysis/references/source-contract.md +75 -0
  6. package/skills/company-analysis/scripts/dart-rows.js +52 -0
  7. package/skills/company-analysis/scripts/dart-rows.ts +48 -0
  8. package/skills/company-analysis/scripts/evidence.js +567 -0
  9. package/skills/company-analysis/scripts/evidence.ts +520 -0
  10. package/skills/company-analysis/scripts/fetch-public.js +85 -0
  11. package/skills/company-analysis/scripts/fetch-public.ts +75 -0
  12. package/skills/company-analysis/skill.json +6 -0
  13. package/skills/express-bus-booking/scripts/kobus_express_booking.py +46 -18
  14. package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +53 -3
  15. package/skills/g2b-order-plan-search/instruction.md +17 -0
  16. package/skills/g2b-sanctioned-supplier/instruction.md +10 -1
  17. package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +8 -4
  18. package/skills/g2b-sanctioned-supplier/scripts/test_g2b_sanctioned_supplier.py +19 -0
  19. package/skills/government-bond-analysis/instruction.md +216 -0
  20. package/skills/government-bond-analysis/references/methodology.md +63 -0
  21. package/skills/government-bond-analysis/scripts/cli.mjs +268 -0
  22. package/skills/government-bond-analysis/scripts/collect.mjs +171 -0
  23. package/skills/government-bond-analysis/scripts/core.mjs +224 -0
  24. package/skills/government-bond-analysis/scripts/dual.mjs +178 -0
  25. package/skills/government-bond-analysis/scripts/harness.mjs +406 -0
  26. package/skills/government-bond-analysis/scripts/modes.mjs +136 -0
  27. package/skills/government-bond-analysis/scripts/news.mjs +113 -0
  28. package/skills/government-bond-analysis/scripts/render.mjs +120 -0
  29. package/skills/government-bond-analysis/scripts/schedule.mjs +96 -0
  30. package/skills/government-bond-analysis/scripts/ust.mjs +204 -0
  31. package/skills/government-bond-analysis/skill.json +8 -0
  32. package/skills/korean-bond-search/instruction.md +122 -0
  33. package/skills/korean-bond-search/references/fields.md +48 -0
  34. package/skills/korean-bond-search/references/source-notes.md +11 -0
  35. package/skills/korean-bond-search/scripts/bond_search.py +906 -0
  36. package/skills/korean-bond-search/skill.json +9 -0
  37. package/skills/market-event-impact/instruction.md +150 -0
  38. package/skills/market-event-impact/references/event-taxonomy.md +74 -0
  39. package/skills/market-event-impact/references/methodology.md +98 -0
  40. package/skills/market-event-impact/references/research-contract.md +120 -0
  41. package/skills/market-event-impact/references/source-priority.md +25 -0
  42. package/skills/market-event-impact/scripts/adapters.py +32 -0
  43. package/skills/market-event-impact/scripts/asset_selector.py +120 -0
  44. package/skills/market-event-impact/scripts/calendars.py +179 -0
  45. package/skills/market-event-impact/scripts/event_resolver.py +260 -0
  46. package/skills/market-event-impact/scripts/event_study.py +171 -0
  47. package/skills/market-event-impact/scripts/market_data.py +204 -0
  48. package/skills/market-event-impact/scripts/market_event.py +1065 -0
  49. package/skills/market-event-impact/scripts/research.py +389 -0
  50. package/skills/market-event-impact/skill.json +9 -0
  51. package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
  52. package/src/analytics-policy.json +18 -0
  53. package/src/analytics.js +106 -0
  54. package/src/metrics.js +49 -0
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // Capture one public document without cookies, API keys, redirects or overwrites.
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+
7
+ const MAX_BYTES = 20_000_000;
8
+ const SECRET = /^(?:api[_-]?key|service[_-]?key|crtfc[_-]?key|auth(?:orization|[_-]?key)?|access[_-]?token|key|secret|token|password|passwd|session(?:id|[_-]?key)?)$/i;
9
+
10
+ function sourceUrl(value: string): URL {
11
+ let parsed: URL;
12
+ try { parsed = new URL(value); } catch { throw new Error("invalid URL"); }
13
+ if (value.length > 2048 || parsed.protocol !== "https:" || !parsed.hostname || parsed.username || parsed.password ||
14
+ parsed.hash || (parsed.port && parsed.port !== "443") ||
15
+ /[\s<>"'\\]/.test(value) || [...parsed.searchParams.keys()].some((key) => SECRET.test(key)) ||
16
+ parsed.hostname === "localhost" || parsed.hostname.endsWith(".local") ||
17
+ /^\d+(?:\.\d+){3}$/.test(parsed.hostname) || parsed.hostname.includes(":")) {
18
+ throw new Error("public HTTPS URL without credentials or secret query required");
19
+ }
20
+ return parsed;
21
+ }
22
+
23
+ async function main(): Promise<void> {
24
+ if (process.argv.length !== 4) throw new Error("usage: node scripts/fetch-public.ts HTTPS_URL NEW_OUTPUT_DIRECTORY");
25
+ const address = sourceUrl(process.argv[2]);
26
+ const output = resolve(process.argv[3]);
27
+ if (existsSync(output)) throw new Error("output directory already exists; snapshots are immutable");
28
+ const response = await fetch(address, {
29
+ redirect: "manual",
30
+ signal: AbortSignal.timeout(15_000),
31
+ headers: {
32
+ "User-Agent": "CompanyAnalysisLocal/1.0 (public research)",
33
+ "Accept": "application/json,text/html,application/pdf,application/xml,text/plain,*/*;q=0.1",
34
+ },
35
+ });
36
+ if (response.status !== 200 || !response.body) throw new Error(`HTTP ${response.status}; source unavailable`);
37
+ const type = response.headers.get("content-type") ?? "";
38
+ if (!/^(?:application\/(?:json|pdf|xml|xhtml\+xml)|text\/(?:html|plain|xml))/i.test(type)) {
39
+ throw new Error(`unsupported content type: ${type}`);
40
+ }
41
+ const reader = response.body.getReader();
42
+ const chunks: Uint8Array[] = [];
43
+ let size = 0;
44
+ try {
45
+ while (true) {
46
+ const { done, value } = await reader.read();
47
+ if (done) break;
48
+ size += value.byteLength;
49
+ if (size > MAX_BYTES) throw new Error("source exceeds 20 MB limit");
50
+ chunks.push(value);
51
+ }
52
+ } finally {
53
+ reader.releaseLock();
54
+ }
55
+ const raw = Buffer.concat(chunks);
56
+ if (raw.length === 0) throw new Error("empty response; source unavailable");
57
+ const sha256 = createHash("sha256").update(raw).digest("hex");
58
+ mkdirSync(output);
59
+ writeFileSync(resolve(output, "raw"), raw, { flag: "wx" });
60
+ const capture = JSON.stringify({
61
+ url: address.toString(),
62
+ retrieved_at: new Date().toISOString(),
63
+ content_type: type,
64
+ bytes: raw.length,
65
+ sha256,
66
+ }, null, 2) + "\n";
67
+ writeFileSync(resolve(output, "capture.json"), capture, { flag: "wx" });
68
+ const captureSha256 = createHash("sha256").update(capture).digest("hex");
69
+ process.stdout.write(`raw_sha256=${sha256}\ncapture_sha256=${captureSha256}\npath=${output}\n`);
70
+ }
71
+
72
+ main().catch((error: unknown) => {
73
+ process.stderr.write(`CAPTURE_ERROR: ${error instanceof Error ? error.message : "unknown error"}\n`);
74
+ process.exitCode = 1;
75
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "company-analysis",
3
+ "description": "๊ตญ๋‚ดยทํ•ด์™ธ ์ƒ์žฅ๊ธฐ์—…์˜ ๊ณต๊ฐœ ๊ณต์‹œ์™€ ์‹ค์ ๋ฐœํ‘œ๋ฅผ ๋Œ€์กฐํ•ด 3๋Œ€ ์žฌ๋ฌด์ œํ‘œ์˜ ๋ณ€ํ™”, ์‚ฌ์—…ยท๊ฒฝ์Ÿ๊ตฌ๋„, ์ฃผ์š” ์ผ์ •๊ณผ ์กฐํšŒ ์‹œ์  ์ฃผ๊ฐ€๋ฅผ ์—ฌ์„ฏ ์„น์…˜์˜ ํ•œ๊ตญ์–ด ์ฑ„ํŒ… ๋ถ„์„์œผ๋กœ ์ •๋ฆฌํ•œ๋‹ค. ๊ธฐ์—…๋ถ„์„ยท์‹ค์ ๋ฐœํ‘œ ์ •๋ฆฌยท์ˆ˜์น˜ ๊ฒ€์ฆ์— ์‚ฌ์šฉํ•˜๋ฉฐ ๋งค๋งค ์ถ”์ฒœ์ด๋‚˜ PDF ๋ณด๊ณ ์„œ๋Š” ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.",
4
+ "profiles": ["lookup"],
5
+ "frontmatter": "name: company-analysis\ndescription: ๊ตญ๋‚ดยทํ•ด์™ธ ์ƒ์žฅ๊ธฐ์—…์˜ ๊ณต๊ฐœ ๊ณต์‹œ์™€ ์‹ค์ ๋ฐœํ‘œ๋ฅผ ๋Œ€์กฐํ•ด 3๋Œ€ ์žฌ๋ฌด์ œํ‘œ์˜ ๋ณ€ํ™”, ์‚ฌ์—…ยท๊ฒฝ์Ÿ๊ตฌ๋„, ์ฃผ์š” ์ผ์ •๊ณผ ์กฐํšŒ ์‹œ์  ์ฃผ๊ฐ€๋ฅผ ์—ฌ์„ฏ ์„น์…˜์˜ ํ•œ๊ตญ์–ด ์ฑ„ํŒ… ๋ถ„์„์œผ๋กœ ์ •๋ฆฌํ•œ๋‹ค. ๊ธฐ์—…๋ถ„์„ยท์‹ค์ ๋ฐœํ‘œ ์ •๋ฆฌยท์ˆ˜์น˜ ๊ฒ€์ฆ์— ์‚ฌ์šฉํ•˜๋ฉฐ ๋งค๋งค ์ถ”์ฒœ์ด๋‚˜ PDF ๋ณด๊ณ ์„œ๋Š” ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.\nlicense: MIT\nmetadata:\n category: finance\n locale: ko-KR\n phase: v1"
6
+ }
@@ -24,8 +24,16 @@ from typing import Iterable
24
24
 
25
25
  BASE_URL = "https://www.kobus.co.kr"
26
26
  UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/125 Safari/537.36"
27
- FN_SATS_RE = re.compile(r"fnSatsChc\((.*?)\)", re.DOTALL)
27
+ # Only real calls carry quoted arguments. The KOBUS response also embeds an
28
+ # HTML-commented template `<!-- fnSatsChc(deprTime,alcnDeprTime,...) -->` whose
29
+ # bare identifiers must never be parsed as a schedule, so the regex requires an
30
+ # opening quote right after `(`.
31
+ FN_SATS_RE = re.compile(r"fnSatsChc\(\s*('.*?)\)", re.DOTALL)
28
32
  ARG_RE = re.compile(r"'([^']*)'")
33
+ HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
34
+ # seat_stage_fields() reads args[1..8], args[12] and args[13]; anything shorter
35
+ # is an incomplete template and is not a bookable schedule.
36
+ MIN_SATS_ARGS = 14
29
37
  FORM_RE = re.compile(r"<form\b([^>]*)>(.*?)</form>", re.DOTALL | re.IGNORECASE)
30
38
  INPUT_RE = re.compile(r"<input\b([^>]+)>", re.DOTALL | re.IGNORECASE)
31
39
  ATTR_RE = re.compile(r"([\w:-]+)=[\"']([^\"']*)[\"']")
@@ -107,6 +115,34 @@ def parse_form(body: str, form_id: str) -> list[tuple[str, str]]:
107
115
  return []
108
116
 
109
117
 
118
+ def parse_schedules(body: str) -> list[Schedule]:
119
+ """Parse bookable schedules from the KOBUS search response.
120
+
121
+ HTML comments and inline templates are stripped first so the commented
122
+ `fnSatsChc(...)` prototype never becomes a schedule, and any call whose
123
+ quoted argument list is shorter than MIN_SATS_ARGS is dropped as incomplete.
124
+ """
125
+ visible = HTML_COMMENT_RE.sub(" ", body)
126
+ schedules: list[Schedule] = []
127
+ for m in FN_SATS_RE.finditer(visible):
128
+ args = ARG_RE.findall(m.group(1))
129
+ if len(args) < MIN_SATS_ARGS:
130
+ continue
131
+ context = strip_tags(visible[max(0, m.start() - 900) : m.start() + 900])
132
+ departure = args[1][:2] + ":" + args[1][2:4] if len(args[1]) >= 4 else None
133
+ schedules.append(
134
+ Schedule(
135
+ index=len(schedules) + 1,
136
+ departure_time=departure,
137
+ company=(re.search(r"\((?:์ฃผ|์œ )\)[^\s]+|[๊ฐ€-ํžฃ]+๊ณ ์†", context) or [None])[0],
138
+ bus_class=(re.search(r"์‹ฌ์•ผ์šฐ๋“ฑ|์šฐ๋“ฑ|ํ”„๋ฆฌ๋ฏธ์—„|๊ณ ์†", context) or [None])[0],
139
+ remaining_text=(re.search(r"์ž”์—ฌ\s*\d+์„|\d+\s*/\s*\d+", context) or [None])[0],
140
+ raw_args=args,
141
+ )
142
+ )
143
+ return schedules
144
+
145
+
110
146
  def search(op: urllib.request.OpenerDirector, depart: str, arrive: str, date: str, timeout: int) -> tuple[str, list[Schedule]]:
111
147
  open_text(op, request(f"{BASE_URL}/main.do"), timeout)
112
148
  body = open_text(
@@ -128,22 +164,7 @@ def search(op: urllib.request.OpenerDirector, depart: str, arrive: str, date: st
128
164
  ),
129
165
  timeout,
130
166
  )
131
- schedules: list[Schedule] = []
132
- for idx, m in enumerate(FN_SATS_RE.finditer(body), 1):
133
- args = ARG_RE.findall(m.group(1))
134
- context = strip_tags(body[max(0, m.start() - 900) : m.start() + 900])
135
- departure = args[1][:2] + ":" + args[1][2:4] if len(args) > 1 and len(args[1]) >= 4 else None
136
- schedules.append(
137
- Schedule(
138
- index=idx,
139
- departure_time=departure,
140
- company=(re.search(r"\((?:์ฃผ|์œ )\)[^\s]+|[๊ฐ€-ํžฃ]+๊ณ ์†", context) or [None])[0],
141
- bus_class=(re.search(r"์‹ฌ์•ผ์šฐ๋“ฑ|์šฐ๋“ฑ|ํ”„๋ฆฌ๋ฏธ์—„|๊ณ ์†", context) or [None])[0],
142
- remaining_text=(re.search(r"์ž”์—ฌ\s*\d+์„|\d+\s*/\s*\d+", context) or [None])[0],
143
- raw_args=args,
144
- )
145
- )
146
- return body, schedules
167
+ return body, parse_schedules(body)
147
168
 
148
169
 
149
170
  def seat_stage_fields(search_form: list[tuple[str, str]], schedule: Schedule) -> list[tuple[str, str]]:
@@ -231,7 +252,14 @@ def main(argv: Iterable[str] | None = None) -> int:
231
252
  op = opener()
232
253
  body, schedules = search(op, args.depart_code, args.arrive_code, args.date, args.timeout)
233
254
  result: dict[str, object] = {"route": {"depart_code": args.depart_code, "arrive_code": args.arrive_code, "date": args.date}, "count": len(schedules), "items": [asdict(s) for s in schedules[: args.limit]]}
234
- if (args.hold_first_seat or args.hold_seat) and schedules:
255
+ if args.hold_first_seat or args.hold_seat:
256
+ if not schedules:
257
+ raise SystemExit("no KOBUS schedules were parsed for this route; cannot hold a seat")
258
+ if not 1 <= args.select_index <= len(schedules):
259
+ raise SystemExit(
260
+ f"--select-index {args.select_index} is out of range: parsed {len(schedules)} schedule(s) "
261
+ f"(valid range 1-{len(schedules)})"
262
+ )
235
263
  out = Path(args.output_dir) if args.output_dir else Path(tempfile.mkdtemp(prefix="kobus-hold-"))
236
264
  result["hold"] = asdict(hold(op, body, schedules[args.select_index - 1], args.hold_seat, out, args.timeout))
237
265
  result["payment_note"] = "Opened/saved the official KOBUS payment-information page; final card entry/payment remains manual."
@@ -31,6 +31,14 @@ MAX_CONCURRENCY = 5
31
31
  DEFAULT_WEEK_RANGE = 1
32
32
  CATEGORY_CODES = {"01", "02"}
33
33
  RESERVE_ROOM_MARKER = "์˜ˆ๋น„"
34
+ LOGIN_FORM_SELECTOR = "#mmberId"
35
+ LOGIN_SUBMIT_SELECTOR = "input.loginBtn"
36
+ RESERVATION_SIDO_SELECTOR = "#srchSido"
37
+ RESERVATION_INSTT_SELECTOR = "#srchInstt"
38
+ SESSION_RETRY_HINT = (
39
+ "Fix the credentials, then rerun with --refresh-session so a stale cached "
40
+ "session is not reused."
41
+ )
34
42
 
35
43
 
36
44
  @dataclass
@@ -208,6 +216,34 @@ def save_session_cache(path: Path, session: Session) -> None:
208
216
  pass
209
217
 
210
218
 
219
+ def has_element(page: Any, selector: str) -> bool:
220
+ """Return True when the selector resolves to at least one node on the page."""
221
+ return page.locator(selector).count() > 0
222
+
223
+
224
+ def login_failure_message() -> str:
225
+ return (
226
+ "foresttrip login failed: the login form is still present after submitting credentials. "
227
+ "Check KSKILL_FORESTTRIP_ID and KSKILL_FORESTTRIP_PASSWORD. "
228
+ "If CAPTCHA or additional authentication is required, complete it on the official "
229
+ "foresttrip.go.kr login screen and retry. " + SESSION_RETRY_HINT
230
+ )
231
+
232
+
233
+ def reservation_page_failure_message(page: Any) -> str:
234
+ missing = [
235
+ selector
236
+ for selector in (RESERVATION_SIDO_SELECTOR, RESERVATION_INSTT_SELECTOR)
237
+ if not has_element(page, selector)
238
+ ]
239
+ return (
240
+ "foresttrip reservation page did not expose " + ", ".join(missing) + ". "
241
+ "The login session may have expired or ์ˆฒ๋‚˜๋“คe changed the page markup. "
242
+ + SESSION_RETRY_HINT
243
+ + " If it persists, the helper's session bootstrap needs an update."
244
+ )
245
+
246
+
211
247
  def bootstrap_session(*, forest_id: str, forest_pw: str, ttl_sec: int = 600) -> Session:
212
248
  try:
213
249
  from playwright.sync_api import Error as PlaywrightError # type: ignore[reportMissingImports]
@@ -228,17 +264,31 @@ def bootstrap_session(*, forest_id: str, forest_pw: str, ttl_sec: int = 600) ->
228
264
  ) from exc
229
265
  page = browser.new_page()
230
266
  page.goto(LOGIN_URL)
231
- page.fill("#mmberId", forest_id)
267
+ page.fill(LOGIN_FORM_SELECTOR, forest_id)
232
268
  page.fill("#gnrlMmberPssrd", forest_pw)
233
- page.click("input.loginBtn")
269
+ page.click(LOGIN_SUBMIT_SELECTOR)
234
270
  page.wait_for_load_state("networkidle")
235
271
  page.goto(RSRVT_PAGE)
236
272
  page.wait_for_load_state("networkidle")
237
273
 
274
+ failure: str | None = None
275
+ if has_element(page, LOGIN_FORM_SELECTOR):
276
+ failure = login_failure_message()
277
+ elif not has_element(page, RESERVATION_SIDO_SELECTOR) or not has_element(
278
+ page, RESERVATION_INSTT_SELECTOR
279
+ ):
280
+ failure = reservation_page_failure_message(page)
281
+ if failure is not None:
282
+ browser.close()
283
+ raise SystemExit(failure)
284
+
238
285
  csrf_locator = page.locator('input[name="_csrf"]')
239
286
  if csrf_locator.count() == 0:
240
287
  browser.close()
241
- raise SystemExit("login succeeded page did not expose a CSRF token")
288
+ raise SystemExit(
289
+ "foresttrip login appeared to succeed but the reservation page did not "
290
+ "expose a CSRF token. " + SESSION_RETRY_HINT
291
+ )
242
292
  csrf = csrf_locator.first.get_attribute("value") or ""
243
293
 
244
294
  forests: dict[str, str] = {}
@@ -105,6 +105,23 @@ The result is the proxy JSON response. Important fields:
105
105
 
106
106
  When answering, show the official source (data.go.kr dataset and g2b.go.kr manual verification URL) and state that ๋ฐœ์ฃผ๊ณ„ํš์€ ์‚ฌ์ „๊ณ„ํš์ด๋ฉฐ ์‹ค์ œ ์‚ฌ์ „๊ทœ๊ฒฉ/์ž…์ฐฐ๊ณต๊ณ /๊ณ„์•ฝ๊ณผ 1:1๋กœ ๋ณด์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค.
107
107
 
108
+ ### 4. Separate search candidates from participation eligibility
109
+
110
+ ๋ฐœ์ฃผ๊ณ„ํš ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋Š” ํ›„๋ณด์ด์ง€ ์ฐธ๊ฐ€์ž๊ฒฉ ํ™•์ธ ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹ˆ๋‹ค. `bidNtceNoList`๊ฐ€ ์žˆ๋”๋ผ๋„ ์‹ค์ œ ๊ณต๊ณ ์˜ ํ˜„์žฌ ๊ณต๊ณ ๋ฒˆํ˜ธยท์ฐจ์ˆ˜์™€ ์›๋ฌธ URL์„ ๋จผ์ € ํ™•์ธํ•œ๋‹ค. ๋ฒˆํ˜ธ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์›๋ฌธ์„ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฉด `์ฐธ๊ฐ€์ž๊ฒฉ: ํ™•์ธ ํ•„์š”`๋กœ ๋‚จ๊ธด๋‹ค. ์ด ์Šคํ‚ฌ์˜ API๋Š” ๊ณต๊ณ ๋ฌธยท์ฒจ๋ถ€ยท์ž๊ฒฉ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ด๋ฅผ ์ถ”์ •ํ•ด ์ฑ„์šฐ์ง€ ์•Š๋Š”๋‹ค.
111
+
112
+ ์ฐธ๊ฐ€ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ์š”์ฒญ๋ฐ›์•˜์„ ๋•Œ๋งŒ ๋ณ„๋„ ๊ฒ€ํ†  ํ‘œ๋ฅผ ๋งŒ๋“ ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ํšŒ์‚ฌ ์„œ๋ฅ˜์™€ ๊ณต์‹ ์ตœ์ข… ๊ณต๊ณ ๋ฌธยท์ฒจ๋ถ€๋ฅผ ๊ฐ™์€ ๋‹จ์œ„๋กœ ๋Œ€์กฐํ•œ๋‹ค. ์ง€์—ญ, ๋ฉดํ—ˆยท์—…์ข…, ์‹ค์ , ๋“ฑ๋กยท์ธ์ฆ์˜ **4ํ•ญ๋ชฉ ๋ชจ๋‘**์— ๊ณต๊ณ  ์›๋ฌธ ์ธ์šฉยทURLยท๊ณต๊ณ ๋ฒˆํ˜ธ/์ฐจ์ˆ˜ยท์ž๊ฒฉ ๊ธฐ์ค€์ผ๊ณผ ํšŒ์‚ฌ ์ธก ๊ทผ๊ฑฐ๋ฅผ ๋ถ™์ธ๋‹ค. ์ธ์ฆ์€ ๊ณต๊ณ ์—์„œ ํ•„์ˆ˜์ธ์ง€ ๊ฐ€์ ์ธ์ง€ ๊ตฌ๋ถ„ํ•œ๋‹ค. ์ด๋ฆ„์ด ๋น„์Šทํ•˜๋‹ค๋Š” ์ด์œ ๋กœ ์—…์ข… ์ฝ”๋“œ ์ผ์น˜๋ฅผ ๊ฐ€์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค.
113
+
114
+ | ํ•ญ๋ชฉ | ๊ณต๊ณ  ์š”๊ฑดยทํ•„์ˆ˜/๊ฐ€์  | ํšŒ์‚ฌ ์„œ๋ฅ˜ ๊ทผ๊ฑฐ | ๊ณต๊ณ  ์›๋ฌธยทURLยท๋ฒˆํ˜ธ/์ฐจ์ˆ˜ | ๊ธฐ์ค€์ผ | ๊ฒฐ๊ณผ |
115
+ | --- | --- | --- | --- | --- | --- |
116
+ | ์ง€์—ญ | ๋ณธ์ /์ง€์  ์ธ์ • ๋ฒ”์œ„์™€ ์†Œ์žฌ์ง€ | ์†Œ์žฌ์ง€ ๋ฐ ๋“ฑ๋ก์‹œ์  | ํ™•์ธํ•œ ์›๋ฌธ | ๊ณต๊ณ ๊ฐ€ ์ •ํ•œ ์‹œ์  | ์ถฉ์กฑ/๋ฏธ์ถฉ์กฑ/ํ™•์ธ ํ•„์š” |
117
+ | ๋ฉดํ—ˆยท์—…์ข… | ์š”๊ตฌ ๋ฉดํ—ˆยท์—…์ข… ์ฝ”๋“œ | ๋“ฑ๋ก ์ฝ”๋“œ ๋ฐ ์œ ํšจ๊ธฐ๊ฐ„ | ํ™•์ธํ•œ ์›๋ฌธ | ๊ณต๊ณ ๊ฐ€ ์ •ํ•œ ์‹œ์  | ์ถฉ์กฑ/๋ฏธ์ถฉ์กฑ/ํ™•์ธ ํ•„์š” |
118
+ | ์‹ค์  | ์ธ์ • ๊ธฐ๊ฐ„ยท์ข…๋ฅ˜ยท๊ธˆ์•ก | ์ฆ๋ช…์„œยท์™„๋ฃŒ์ผยท๊ธˆ์•ก | ํ™•์ธํ•œ ์›๋ฌธ | ์‹ค์  ๊ธฐ์‚ฐ์ผ | ์ถฉ์กฑ/๋ฏธ์ถฉ์กฑ/ํ™•์ธ ํ•„์š” |
119
+ | ๋“ฑ๋กยท์ธ์ฆ | ํ•„์ˆ˜ ๋“ฑ๋กยท์ธ์ฆ๊ณผ ๊ฐ€์  ๊ตฌ๋ถ„ | ๋“ฑ๋ก ๋ฐ ์ธ์ฆ ์œ ํšจ๊ธฐ๊ฐ„ | ํ™•์ธํ•œ ์›๋ฌธ | ๊ณต๊ณ ๊ฐ€ ์ •ํ•œ ์‹œ์  | ์ถฉ์กฑ/๋ฏธ์ถฉ์กฑ/ํ™•์ธ ํ•„์š” |
120
+
121
+ ์›๋ฌธ/์„œ๋ฅ˜/๊ธฐ์ค€์ผ์ด ์—†๊ฑฐ๋‚˜ ํ•ด์„์ด ๋ชจํ˜ธํ•˜๋ฉด `ํ™•์ธ ํ•„์š”`๋‹ค. ๋„ค ํ•ญ๋ชฉ์€ ์ ๊ฒ€ ์ˆœ์„œ์ด๋ฉฐ ๋ฒ•์  ์ž๊ฒฉ์š”๊ฑด์˜ ์ „๋ถ€๊ฐ€ ์•„๋‹ˆ๋‹ค. ๊ณต๋™์ˆ˜๊ธ‰ยท์ง์ ‘์ƒ์‚ฐํ™•์ธ ๋“ฑ ํ•ด๋‹น ๊ณต๊ณ ์˜ ์ถ”๊ฐ€ ํ•„์ˆ˜์š”๊ฑด๋„ ๋น ์ง์—†์ด ํ™•์ธํ•˜๊ณ , ๋„ค ํ•ญ๋ชฉ๋งŒ ๋งž์•˜๋‹ค๋Š” ์ด์œ ๋กœ ์ฐธ๊ฐ€ ๊ฐ€๋Šฅ์„ ๋ณด์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. ์‚ฌ์—…์ž๋“ฑ๋ก ์ƒํƒœ ์ •์ƒ์ด๋‚˜ ๋ถ€์ •๋‹น์ œ์žฌ 0๊ฑด๋„ ๊ณต๊ณ ๋ณ„ ์ž๊ฒฉ ์ถฉ์กฑ์˜ ์ฆ๊ฑฐ๊ฐ€ ์•„๋‹ˆ๋‹ค.
122
+
123
+ ์ €์žฅํ•œ ๊ณต๊ณ ๋ฅผ ์žฌ๊ฒ€ํ† ํ•  ๋•Œ๋Š” ์ตœ์ข… ์ •์ •/์ทจ์†Œ/์žฌ๊ณต๊ณ ์™€ ์ฒจ๋ถ€๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•œ๋‹ค. ์ด์ „/์ตœ์ข… ๊ณต๊ณ ๋ฒˆํ˜ธยท์ฐจ์ˆ˜ยทํ™•์ธ์‹œ๊ฐ์„ ๋‚จ๊ธฐ๊ณ  ๋งˆ๊ฐยท์˜ˆ์‚ฐยท๊ณผ์—…ยท์ง€์—ญยท๋ฉดํ—ˆ/์—…์ข…ยท์‹ค์ ยท๋“ฑ๋ก/์ธ์ฆ์„ ์ „ํ›„ ๋น„๊ตํ•œ๋‹ค. ๋ณ€๊ฒฝ๋œ ์ž๊ฒฉ์€ ์žฌ๊ฒ€ํ† ํ•˜๋ฉฐ, ์ตœ์ข… ์›๋ฌธ์„ ํ™•์ธํ•  ์ˆ˜ ์—†์œผ๋ฉด ์ด์ „ ํŒ๋‹จ์„ ์œ ์ง€ํ•˜์ง€ ์•Š๊ณ  `ํ™•์ธ ํ•„์š”`๋กœ ํ‘œ์‹œํ•œ๋‹ค. 31% ๊ฐ™์€ ๋ฏผ๊ฐ„ ์ง‘๊ณ„ ๋น„์œจ์€ ์ด ํŒ๋‹จ์˜ ๊ทผ๊ฑฐ๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
124
+
108
125
  ## Done when
109
126
 
110
127
  - The proxy route `/v1/g2b/order-plans` was queried.
@@ -5,6 +5,8 @@
5
5
  ๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ์˜ **์กฐ๋‹ฌ์ฒญ ๋‚˜๋ผ์žฅํ„ฐ ์‚ฌ์šฉ์ž์ •๋ณด ์„œ๋น„์Šค**(data.go.kr 15129466, `getUnptRsttCorpInfo02`)๋ฅผ `k-skill-proxy` ๊ฒฝ์œ ๋กœ ํ˜ธ์ถœํ•ด, ์‚ฌ์—…์ž๋“ฑ๋ก๋ฒˆํ˜ธ ์ •ํ™• ์ผ์น˜(`inqryDiv=1`)๋กœ **์กฐํšŒ์‹œ์  ํ˜„์žฌ ์œ ํšจํ•œ** ๋ถ€์ •๋‹น์ œ์žฌ๋ฅผ ์กฐํšŒํ•œ๋‹ค.
6
6
 
7
7
  - ๋ฐ˜ํ™˜: ์ œ์žฌ ์‹œ์ž‘/์ข…๋ฃŒ์ผ์ž, ์ œ์žฌ๊ธฐ๊ด€๋ช…, ๊ณ„์•ฝ๋ฒ•๊ตฌ๋ถ„, ์ œ์žฌ๊ทผ๊ฑฐ๋ฒ•๋ฅ  ๋“ฑ upstream ํ•„๋“œ ์›๋ฌธ
8
+ - ๊ฐ์‚ฌ ํ•„๋“œ: `lookup_status`, `coverage.checked_at`, ๊ณ ์ •๋œ `source.query`, ๊ทธ๋ฆฌ๊ณ  ์„ฑ๊ณตํ•œ upstream JSON ์›๋ฌธ `upstream_response`
9
+ - `lookup_status`๋Š” `active_sanctions_found`, `no_active_sanctions`, `lookup_failed` ์ค‘ ํ•˜๋‚˜๋‹ค. `no_active_sanctions`๋Š” ์„ฑ๊ณตํ•œ 0๊ฑด์ด๋ฉฐ ์กฐํšŒ ์‹คํŒจ์™€ ๋‹ค๋ฅด๋‹ค.
8
10
 
9
11
  ## Coverage boundary
10
12
 
@@ -19,6 +21,10 @@ upstream ๋ช…์„ธ์ƒ ๋‹ค์Œ์€ **์ œ๊ณต๋˜์ง€ ์•Š๋Š”๋‹ค** โ€” ๊ณผ๊ฑฐ ์ด๋ ฅ ์กฐํšŒ
19
21
 
20
22
  - ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยทํ•ด์„ ๋ผ๋ฒจ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. upstream ์‚ฌ์‹ค + ์ถœ์ฒ˜ + ์ ์šฉ๋ฒ”์œ„ ํ•œ๊ณ„๋งŒ ๋‹ด๋Š”๋‹ค.
21
23
 
24
+ ## ์ฐธ๊ฐ€์ž๊ฒฉ ํŒ๋‹จ ๊ฒฝ๊ณ„
25
+
26
+ ์ด ๊ฒฐ๊ณผ๋งŒ์œผ๋กœ ํŠน์ • ๋‚˜๋ผ์žฅํ„ฐ ๊ณต๊ณ ์˜ ์ฐธ๊ฐ€ ๊ฐ€๋Šฅ/๋ถˆ๊ฐ€๋ฅผ ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ฐธ๊ฐ€์ž๊ฒฉ์€ ๊ณต๊ณ ๋ณ„ ์›๋ฌธ์—์„œ ์ง€์—ญ, ๋ฉดํ—ˆยท์—…์ข… ์ฝ”๋“œ, ์‹ค์ , ๋“ฑ๋กยท์ธ์ฆ์˜ ํ•„์ˆ˜ ์—ฌ๋ถ€์™€ ๊ธฐ์ค€์ผ์„ ๋ณ„๋„๋กœ ๋Œ€์กฐํ•ด์•ผ ํ•œ๋‹ค. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋‚˜ ์ œ์žฌ ์—†์Œ๋งŒ์œผ๋กœ ์ฐธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ๋‹ตํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ •์ •๊ณต๊ณ ๊ฐ€ ์žˆ์œผ๋ฉด ์ €์žฅํ•ด ๋‘” ์ด์ „ ๋‚ด์šฉ์ด ์•„๋‹ˆ๋ผ ์ตœ์ข… ๊ณต๊ณ  ์›๋ฌธ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์‹œ ํ™•์ธํ•œ๋‹ค.
27
+
22
28
  ## When to use
23
29
 
24
30
  - "์ด ํšŒ์‚ฌ ์ž…์ฐฐ ์ œ์žฌ(๋ถ€์ •๋‹น์ œ์žฌ) ์ด๋ ฅ ์žˆ์–ด?"
@@ -48,11 +54,14 @@ npx -y @nomadamas/k-skill@0 exec g2b-sanctioned-supplier scripts/g2b_sanctioned_
48
54
 
49
55
  ## Failure modes
50
56
 
57
+ - `lookup_status = active_sanctions_found`: ์กฐํšŒ ์„ฑ๊ณต, ํ˜„์žฌ ์œ ํšจ ์ œ์žฌ 1๊ฑด ์ด์ƒ.
58
+ - `lookup_status = no_active_sanctions`: ์กฐํšŒ ์„ฑ๊ณต, ํ˜„์žฌ ์œ ํšจ ์ œ์žฌ 0๊ฑด. ๋งŒ๋ฃŒยทํ•ด์ œ๋œ ๊ณผ๊ฑฐ ์ œ์žฌ๊ฐ€ ์—†๋‹ค๋Š” ๋œป์€ ์•„๋‹ˆ๋‹ค.
59
+ - `lookup_status = lookup_failed`: upstream timeout/error/๊ถŒํ•œ ๊ฑฐ์ ˆ ๋“ฑ์œผ๋กœ ์กฐํšŒ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ•จ. ์ด๋ฅผ ์ œ์žฌ ์—†์Œ์œผ๋กœ ํ•ด์„ํ•˜์ง€ ์•Š๋Š”๋‹ค.
60
+ - ์„ฑ๊ณต ์‘๋‹ต์€ `upstream_response`์— ์›๋ณธ JSON์„ ๋ณด์กดํ•˜๊ณ  `coverage.checked_at`์— ์กฐํšŒ์‹œ๊ฐ์„ ๋‚จ๊ธด๋‹ค.
51
61
  - `400 bad_request`: ์‚ฌ์—…์ž๋ฒˆํ˜ธ๊ฐ€ 10์ž๋ฆฌ๊ฐ€ ์•„๋‹˜.
52
62
  - `503 upstream_not_configured`: ํ”„๋ก์‹œ ์„œ๋ฒ„์— `DATA_GO_KR_API_KEY` ์—†์Œ.
53
63
  - `502 upstream_forbidden`: ํ”„๋ก์‹œ ํ‚ค๊ฐ€ 15129466์— ํ™œ์šฉ์‹ ์ฒญ๋˜์ง€ ์•Š์Œ.
54
64
  - `coverage`: ํ˜„์žฌ ์œ ํšจ ์ œ์žฌ ๋ฒ”์œ„, ์‚ฌ์—…์ž๋ฒˆํ˜ธ ์ •ํ™• ์ผ์น˜ ๊ธฐ์ค€, ๊ณผ๊ฑฐยท๋ฏธ๋“ฑ๋ก ์ œ์™ธ ๋ฒ”์œ„, 0๊ฑด์˜ ์˜๋ฏธ, ์กฐํšŒ์‹œ๊ฐ(`checked_at`)์„ ๊ตฌ์กฐํ™”ํ•ด ์ œ๊ณตํ•œ๋‹ค.
55
- - `total_count = 0`: ์กฐํšŒ์‹œ์  ํ˜„์žฌ ์œ ํšจํ•œ ์ œ์žฌ๊ฐ€ ์กฐํšŒ๋˜์ง€ ์•Š์Œ. ๋งŒ๋ฃŒยทํ•ด์ œ๋œ ๊ณผ๊ฑฐ ์ œ์žฌ๊ฐ€ ์—†๋‹ค๋Š” ๋œป์€ ์•„๋‹ˆ๋‹ค.
56
65
 
57
66
  ## Official surfaces
58
67
 
@@ -24,9 +24,10 @@ ROUTE = "/v1/g2b/sanctioned-supplier"
24
24
 
25
25
 
26
26
  class ApiError(RuntimeError):
27
- def __init__(self, message: str, *, status_code: int | None = None):
27
+ def __init__(self, message: str, *, status_code: int | None = None, payload: dict[str, Any] | None = None):
28
28
  super().__init__(message)
29
29
  self.status_code = status_code
30
+ self.payload = payload
30
31
 
31
32
 
32
33
  def _text_or_none(value: Any) -> str | None:
@@ -73,9 +74,9 @@ def read_json_response(request: urllib.request.Request) -> dict[str, Any]:
73
74
  except json.JSONDecodeError:
74
75
  payload = None
75
76
  if error.code == 503 and isinstance(payload, dict) and payload.get("error") == "upstream_not_configured":
76
- raise ApiError(PROXY_KEY_NOT_CONFIGURED_MSG, status_code=error.code) from error
77
+ raise ApiError(PROXY_KEY_NOT_CONFIGURED_MSG, status_code=error.code, payload=payload) from error
77
78
  if isinstance(payload, dict) and payload.get("message"):
78
- raise ApiError(str(payload["message"]), status_code=error.code) from error
79
+ raise ApiError(str(payload["message"]), status_code=error.code, payload=payload) from error
79
80
  raise ApiError(f"g2b sanction proxy request failed with HTTP {error.code}", status_code=error.code) from error
80
81
  except urllib.error.URLError as error:
81
82
  raise ApiError(f"{PROXY_DOWN_MSG} (์ƒ์„ธ: {error.reason})") from error
@@ -106,7 +107,10 @@ def main(argv: list[str] | None = None) -> int:
106
107
  print(json.dumps(result, ensure_ascii=False, indent=2))
107
108
  return 0
108
109
  except (ValueError, ApiError) as error:
109
- print(json.dumps({"error": str(error)}, ensure_ascii=False, indent=2), file=sys.stderr)
110
+ output = {"error": str(error)}
111
+ if isinstance(error, ApiError):
112
+ output = {"lookup_status": "lookup_failed", **(error.payload or {}), "error": str(error)}
113
+ print(json.dumps(output, ensure_ascii=False, indent=2), file=sys.stderr)
110
114
  return 1
111
115
 
112
116
 
@@ -1,3 +1,5 @@
1
+ import contextlib
2
+ import io
1
3
  import unittest
2
4
 
3
5
  import g2b_sanctioned_supplier as subject
@@ -28,6 +30,23 @@ class CoveragePassthroughTest(unittest.TestCase):
28
30
  self.assertIs(response, payload)
29
31
  self.assertEqual(response["coverage"]["match_basis"], "exact-business-number")
30
32
 
33
+ def test_main_preserves_lookup_failure_status(self):
34
+ original = subject.query_sanctions
35
+ subject.query_sanctions = lambda *_args, **_kwargs: (_ for _ in ()).throw(
36
+ subject.ApiError(
37
+ "proxy unavailable",
38
+ status_code=502,
39
+ payload={"lookup_status": "lookup_failed", "error": "upstream_error"},
40
+ )
41
+ )
42
+ stderr = io.StringIO()
43
+ try:
44
+ with contextlib.redirect_stderr(stderr):
45
+ exit_code = subject.main(["--bizno", "1234567890"])
46
+ finally:
47
+ subject.query_sanctions = original
48
+ self.assertEqual(exit_code, 1)
49
+ self.assertEqual(subject.json.loads(stderr.getvalue())["lookup_status"], "lookup_failed")
31
50
 
32
51
  if __name__ == "__main__":
33
52
  unittest.main()
@@ -0,0 +1,216 @@
1
+ # ํ•œยท๋ฏธ ๊ตญ์ฑ„์‹œ์žฅ ๋ถ„์„
2
+
3
+ ํ•œ๊ตญ ๊ตญ๊ณ ์ฑ„(KTB)์™€ ๋ฏธ๊ตญ ๊ตญ์ฑ„(UST)์˜ ๊ธˆ๋ฆฌ ๋ณ€ํ™”์™€ ๊ณต์‹ ์ฃผ์š” ์ผ์ •์„ ํ•จ๊ป˜ ๋ถ„์„ํ•œ๋‹ค.
4
+
5
+ ```bash
6
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --market both --base 2026-09-18 --as-of 2026-09-22
7
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --market both --base 2026-09-18 --as-of 2026-09-22 --json
8
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --market both --base 2026-09-18 --as-of 2026-09-22 --out ./ktb-ust-report
9
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --market both --replay ./ktb-ust-report/evidence.mjson --out ./ktb-ust-replay
10
+ ```
11
+
12
+ `--market both`์˜ ๊ธฐ๋ณธ ์ถœ๋ ฅ์€ ๋Œ€ํ™”์— ๋ฐ”๋กœ ๋ถ™์ผ ์ˆ˜ ์žˆ๋Š” ๊ณ ์ • Markdown์ด๋‹ค. ์ˆœ์„œ๋Š” **์ƒํƒœยท๊ด€์ธก์ผ โ†’ ํ•ต์‹ฌ ๋ณ€ํ™” โ†’ ์—ฌ์„ฏ ๋งŒ๊ธฐ ํ‘œ โ†’ ์ปค๋ธŒยทํ•œ๋ฏธ ์ฐจ์ด โ†’ ์ฃผ์š” ์ผ์ • โ†’ ํ™•์ธ ์‚ฌํ•ญยท์ถœ์ฒ˜**๋‹ค. ๊ธฐ๊ณ„ ์ฒ˜๋ฆฌ์—๋Š” `--json`, ํŒŒ์ผ ๋ณด๊ณ ์„œ๋ฅผ ์š”์ฒญํ•œ ๊ฒฝ์šฐ์—๋งŒ `--out`์„ ์‚ฌ์šฉํ•œ๋‹ค. ์—์ด์ „ํŠธ๋Š” ๊ฒ€์ฆ๋œ ์ˆ˜์น˜๋ฅผ ์žฌ๊ณ„์‚ฐํ•˜๊ฑฐ๋‚˜ ๋น ์ง„ ๊ฐ’์„ ์ถ”์ •ํ•˜์ง€ ์•Š๊ณ , `INCOMPLETE`/`BLOCKED`๋„ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•œ๋‹ค.
13
+
14
+ KTB๋Š” ํ•œ๊ตญ์€ํ–‰ ECOS ๊ตญ๊ณ ์ฑ„ ์ผ๋ณ„ ์‹œ์žฅ๊ธˆ๋ฆฌ, UST๋Š” ๋ฏธ๊ตญ ์žฌ๋ฌด๋ถ€์˜ ์ผ๋ณ„ ๋ช…๋ชฉ par/constant-maturity ์ˆ˜์ต๋ฅ ์ด๋‹ค. ๊ฐ๊ฐ ์—ฌ์„ฏ ๋งŒ๊ธฐ(2ยท3ยท5ยท10ยท20ยท30๋…„)๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ์‹œ์žฅ๋ณ„ ๊ด€์ธก์ผ์„ ๋ณ„๋„๋กœ ํ‘œ์‹œํ•œ๋‹ค. ํ•œยท๋ฏธ 10๋…„ ๊ธˆ๋ฆฌ์ฐจ๋Š” ๋‘ ์›์ฒœ์— **๋™์ผํ•œ ๊ด€์ธก์ผ**์ด ์žˆ์„ ๋•Œ๋งŒ ๊ณ„์‚ฐํ•œ๋‹ค. ๊ฐ™์€ ๋‚ ์งœ๋ผ๋„ ์–‘๊ตญ ์ข…๊ฐ€ ์‹œ์ ์€ ๋‹ค๋ฅด๋ฏ€๋กœ ์ด ์ฐจ์ด๋ฅผ ๋™์‹œ ๊ฐ€๊ฒฉยทํ™˜ํ—ค์ง€ ๋น„์šฉยท์ƒ๋Œ€๊ฐ€์น˜๋กœ ํ•ด์„ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋‘ ๊ณต์‹ ๋ฐ์ดํ„ฐ ์›์ฒœ ๋ชจ๋‘ ์ด ๋ชจ๋“œ์—์„œ ๋ณ„๋„ API ํ‚ค๋ฅผ ์š”๊ตฌํ•˜์ง€ ์•Š๋Š”๋‹ค. ์›์ฒœ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์‹คํŒจํ•˜๋ฉด ํ™•๋ณด๋œ ์‹œ์žฅ์˜ ์ˆ˜์น˜๋Š” ๋ณด์กดํ•˜๋˜ ๊ฒฐ๊ณผ๋ฅผ `INCOMPLETE`๋กœ ํ‘œ์‹œํ•œ๋‹ค.
15
+
16
+ ### ํ•œยท๋ฏธ ์ฃผ์š” ์ผ์ •
17
+
18
+ `--market both`๋Š” `์ฃผ์š” ์ผ์ •` ์„น์…˜์„ ํ•ญ์ƒ ์ถœ๋ ฅํ•œ๋‹ค. ์ตœ์‹  ์ผ์ •์„ ์š”์ฒญ๋ฐ›์•˜๊ฑฐ๋‚˜ ์ด๋ฒˆ ๋ถ„์„์— ์ผ์ •์„ ํฌํ•จํ•  ๋•Œ๋Š” ๋จผ์ € **๊ณต์‹ ํŽ˜์ด์ง€๋ฅผ ์‹ค์ œ๋กœ ํ™•์ธ**ํ•˜๊ณ  `schedule-evidence.json`์„ ๋งŒ๋“ค์–ด `--schedule-evidence FILE`์„ ๋ถ™์ธ๋‹ค. ๋Œ€์ƒ์€ ํ•œ๊ตญ ๊ตญ๊ณ ์ฑ„ ์ž…์ฐฐยทํ•œ๊ตญ์€ํ–‰ ํ†ตํ™”์ •์ฑ…๋ฐฉํ–ฅ ๊ฒฐ์ •ํšŒ์˜, ๋ฏธ๊ตญ ๊ตญ์ฑ„ ์ž…์ฐฐยทFOMC ํšŒ์˜๋‹ค. ์ผ๋ฐ˜์ ์ธ ๋ฐ˜๋ณต ํŒจํ„ด์œผ๋กœ ๋‚ ์งœ๋ฅผ ์ถ”์ •ํ•˜๊ฑฐ๋‚˜ ์ด๋ฏธ ์ง€๋‚œ ์ผ์ •์„ ์˜ˆ์ • ์ผ์ •์œผ๋กœ ํ‘œ์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค.
19
+
20
+ ๊ณต์‹ ํ™•์ธ์ฒ˜: [ํ•œ๊ตญ ๊ตญ์ฑ„์‹œ์žฅ ์›”๋ณ„ ๋ฐœํ–‰ ์บ˜๋ฆฐ๋”](https://ktb.moef.go.kr/mnbyIsuCldr.do), [ํ•œ๊ตญ์€ํ–‰ ํ†ตํ™”์ •์ฑ…๋ฐฉํ–ฅ ๊ฒฐ์ •ํšŒ์˜](https://www.bok.or.kr/portal/singl/crncyPolicyDrcMtg/listYear.do?menuNo=200755&mtgSe=A), [๋ฏธ ์žฌ๋ฌด๋ถ€ ์ž…์ฐฐ ์ผ์ •](https://www.treasurydirect.gov/auctions/announcements-data-results/), [์—ฐ์ค€ FOMC ์ผ์ •](https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm). ๊ตญ๊ณ ์ฑ„ ์›”๋ณ„ ๊ณ„ํš์ด ์•„์ง ๋ฐœํ‘œ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด KR ์ž…์ฐฐ ๋‚ ์งœ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. ๋ฏธ ์žฌ๋ฌด๋ถ€ ์ž ์ • ์ผ์ •์€ `์ž ์ •`์ž„์„ ๋‹ต๋ณ€์—์„œ ๋ฐํžŒ๋‹ค. ๋ฏธ๊ตญ ์ž…์ฐฐํ‘œ๋Š” **Announcement Date / Auction Date / Settlement Date ์ค‘ Auction Date**๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. FOMC ์ดํ‹€์งœ๋ฆฌ ํšŒ์˜๋Š” **๊ฒฐ์ •์ผ์ธ ๋งˆ์ง€๋ง‰ ๋‚ **์„ ์‚ฌ์šฉํ•œ๋‹ค.
21
+
22
+ ์ฆ๊ฑฐ ํŒŒ์ผ์€ `version: 1`, ๋ณด๊ณ ์„œ `as-of`์™€ ๊ฐ™์€ `anchorDate`, UTC ISO `checkedAt`, `events` ๋ฐฐ์—ด์„ ๊ฐ€์ง„ JSON์ด๋‹ค. ๊ฐ ์ด๋ฒคํŠธ์—๋Š” `date`(YYYY-MM-DD), `market`(`KR`/`US`), `kind`(`auction`/`policy`), ๊ณต์‹ `sourceUrl`, ์›๋ฌธ์—์„œ ๊ฐ€์ ธ์˜จ ์งง์€ `sourceExcerpt`, ์›๋ฌธ ์•ˆ์˜ ๋‚ ์งœ ํ‘œํ˜„์ธ `sourceDateText`๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์ž…์ฐฐ์—๋Š” `tenor`(๋…„)๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์›๋ฌธ `2026.09.21. | 5๋…„๋ฌผ`์„ ํ™•์ธํ–ˆ๋‹ค๋ฉด `date: "2026-09-21"`, `sourceDateText: "2026.09.21."`, `tenor: 5`๋กœ ์ ๋Š”๋‹ค. ๋ชจ๋“  ์ด๋ฒคํŠธ๋Š” ๊ธฐ์ค€์ผ ์ดํ›„ 45์ผ ์•ˆ์ด์–ด์•ผ ํ•˜๋ฉฐ ๋Œ€ํ™”ยท๋ณด๊ณ ์„œ๋Š” ๋‚ ์งœ์ˆœ์œผ๋กœ ์ตœ๋Œ€ 4๊ฑด์”ฉ ๋ณด์—ฌ์ค€๋‹ค. ๋งํฌยท๋‚ ์งœ ํ‘œํ˜„ยท๋งŒ๊ธฐยท์ค‘๋ณต์„ ๊ฒ€์ฆํ•œ ๋’ค evidence์™€ ํ•จ๊ป˜ ์ €์žฅํ•˜๋ฏ€๋กœ ๊ฐ™์€ evidence๋ฅผ ์žฌ์ƒํ•˜๋ฉด ์ผ์ •๋„ ๊ทธ๋Œ€๋กœ ๋‚˜์˜จ๋‹ค.
23
+
24
+ ```bash
25
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --market both --base 2026-09-18 --as-of 2026-09-22 --schedule-evidence ./schedule-evidence.json
26
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --market both --base 2026-09-18 --as-of 2026-09-22 --schedule-evidence ./schedule-evidence.json --out ./ktb-ust-report
27
+ ```
28
+
29
+ ๊ณต์‹ ์ผ์ • ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฉด `๊ณต์‹ ์ผ์ • ๋ฏธํ™•์ธ`์œผ๋กœ ๋‚จ๊ธด๋‹ค. ์ผ์ • ๊ฒ€์ฆ ์ƒํƒœ(`VERIFIED`/`PARTIAL`/`UNVERIFIED`)๋Š” ๊ธˆ๋ฆฌ ๋ฐ์ดํ„ฐ ์ƒํƒœ์™€ ๋ณ„๋„์ด๋ฉฐ, ์ผ์ •์ด ๋ถ€์กฑํ•˜๋‹ค๊ณ  ๊ธˆ๋ฆฌ ์ˆ˜์น˜๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. ์˜ˆ์ • ์ผ์ •์€ ์ถ”ํ›„ ๋ฐ”๋€” ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ™•์ธ ์‹œ๊ฐ๊ณผ ์›๋ฌธ ๋งํฌ๋ฅผ ํ•จ๊ป˜ ๋ณด์—ฌ์ค€๋‹ค.
30
+
31
+ ## What this skill does
32
+
33
+ ํ•œ๊ตญ์€ํ–‰ ECOS์™€ ๋ฏธ๊ตญ ์žฌ๋ฌด๋ถ€์˜ ๊ตญ์ฑ„ 2ยท3ยท5ยท10ยท20ยท30๋…„ ๊ธˆ๋ฆฌ๋ฅผ ๊ฐ๊ฐ ๊ฒ€์ฆํ•ด ํ•œยท๋ฏธ ์ปค๋ธŒ ๋ณ€ํ™”์™€ ๊ณตํ†ต ๊ด€์ธก์ผ์˜ 10๋…„ ๊ธˆ๋ฆฌ์ฐจ๋ฅผ ๋ถ„์„ํ•œ๋‹ค. ๊ณต์‹ ์ž…์ฐฐยทํ†ตํ™”์ •์ฑ… ์ผ์ •๋„ ๋ณ„๋„ ์ฆ๊ฑฐ๋กœ ํ™•์ธํ•ด ๊ฐ™์€ ๋‹ต๋ณ€ ํ˜•์‹์— ๋‹ด๋Š”๋‹ค. ๊ตญ๋‚ด ๋‹จ๋… ๋ชจ๋“œ์—์„œ๋Š” ECOS ๊ตญ๊ณ ์ฑ„๋ฅผ ๊ณตํ†ต ๊ด€์ธก์ผ๋กœ ์ •๋ ฌํ•ด ์ „์ผยท1์ฃผยท์ „์›”๋ง ๋ณ€ํ™”, ์ตœ๊ทผ 5ยท20๊ฑฐ๋ž˜์ผ ์ถ”์ด, ๋ถ„๊ธฐ๋งยท์—ฐ์ดˆ ๋Œ€๋น„, ๊ตฌ๊ฐ„๋ณ„ ์Šคํ”„๋ ˆ๋“œ์™€ ๊ธฐ์ค€๊ธˆ๋ฆฌ ๋Œ€๋น„ 3๋…„๋ฌผ ๊ฐญ์„ ๊ณ„์‚ฐํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์‹œ์žฅ ๋ฐฐ๊ฒฝ๊นŒ์ง€ ์š”์ฒญํ•˜๋ฉด ๊ฐ™์€ ๊ด€์ธก์ผ์˜ ๊ณต๊ฐœ ๊ธฐ์‚ฌยท๊ณต์‹ ๋ฐœํ‘œ๋ฅผ ๋ณ„๋„ evidence๋กœ ๊ฒ€์ฆํ•ด ์ถœ์ฒ˜๊ฐ€ ๋ถ™์€ ์ฝ”๋ฉ˜ํŠธ๋ฅผ ๋ณด๊ณ ์„œ์— ์ถ”๊ฐ€ํ•œ๋‹ค.
34
+
35
+ ๋ฒ”์šฉ ํ†ต๊ณ„ ์กฐํšŒ๋Š” `bok-ecos-stats`์˜ ์—ญํ• ์ด๋‹ค. ์ด ์Šคํ‚ฌ์€ ๊ตญ๊ณ ์ฑ„ ์ปค๋ธŒ ๋ชจ๋‹ˆํ„ฐ๋ง์— ํ•„์š”ํ•œ ๋‚ ์งœ ์„ ํƒยท๋‹จ์œ„ ๊ฒ€์ฆยทbp ๊ณ„์‚ฐยท๊ธฐ๊ณ„์  ๋ถ„๋ฅ˜๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ๊ณต์‹ ์ผ๋ณ„ ํ†ต๊ณ„ ๊ธฐ๋ฐ˜ ์กฐํšŒยท๊ณ„์‚ฐ ์ „์šฉ์ด๋ฉฐ ์‹ค์‹œ๊ฐ„ ํ˜ธ๊ฐ€, ์›์ธ ์ถ”์ •, ์ „๋ง, ๋งค๋งค ์ถ”์ฒœ์€ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š”๋‹ค.
36
+
37
+ ## When to use
38
+
39
+ - โ€œ์˜ค๋Š˜ ๊ตญ๊ณ ์ฑ„ ์ปค๋ธŒ ๋ณด์—ฌ์ค˜.โ€
40
+ - โ€œํ•œยท๋ฏธ ๊ตญ์ฑ„ ๊ธˆ๋ฆฌ ๋ณ€ํ™”์™€ ์•ž์œผ๋กœ์˜ ์ฃผ์š” ์ผ์ • ์ •๋ฆฌํ•ด์ค˜.โ€
41
+ - โ€œ์ „์ผยท1์ฃผยท์ „์›” ๋ง ๋Œ€๋น„๋ฅผ ํ•œ ๋ฒˆ์— ์ •๋ฆฌํ•ด์ค˜.โ€
42
+ - โ€œ์ตœ๊ทผ 5์ผ/20์ผ ๋™์•ˆ ์–ด๋А ๋งŒ๊ธฐ๊ฐ€ ๊ฐ€์žฅ ๋งŽ์ด ์›€์ง์˜€์–ด?โ€
43
+ - โ€œ๋ถ„๊ธฐ๋ง ๋˜๋Š” ์—ฐ์ดˆ ๋Œ€๋น„ ์ปค๋ธŒ ๋ณ€ํ™” ๋ณด์—ฌ์ค˜.โ€
44
+ - โ€œ๊ตญ๊ณ ์ฑ„ 3๋…„์ด ๊ธฐ์ค€๊ธˆ๋ฆฌ๋ณด๋‹ค ๋ช‡ bp ๋†’์•„?โ€
45
+ - โ€œ๊ธˆํ†ต์œ„ ์ „ํ›„ ์ปค๋ธŒ๋ฅผ ๊ฒ€์‚ฐํ•ด์ค˜.โ€
46
+ - โ€œ์˜ค๋Š˜ ๊ธˆ๋ฆฌ ์›€์ง์ž„์˜ ๋‰ด์Šค ๋ฐฐ๊ฒฝ๊ณผ ์ถœ์ฒ˜๊นŒ์ง€ ๋ถ™์—ฌ ๋ ˆํฌํŠธ๋กœ ์ค˜.โ€
47
+
48
+ ## When not to use
49
+
50
+ - ์ž„์˜ ECOS ํ†ต๊ณ„ ๊ฒ€์ƒ‰ โ†’ `bok-ecos-stats`
51
+ - ์ฑ„๊ถŒ ์ข…๋ชฉยท๋ฐœํ–‰์ •๋ณดยท์ƒํ™˜ ์ผ์ • ๊ฒ€์ƒ‰
52
+ - ์‹ค์‹œ๊ฐ„ ํ˜ธ๊ฐ€ยท๊ฐœ๋ณ„ ์ง€ํ‘œ์ฑ„ยท๋ฏผํ‰๊ฐ€๊ฒฉ
53
+ - ์ฑ„๊ถŒ ๊ฐ€๊ฒฉยท๋“€๋ ˆ์ด์…˜ยทDV01 ๊ณ„์‚ฐ
54
+ - ๊ธˆ๋ฆฌ ์ „๋งยท๋งค๋งค ์‹ ํ˜ธยทํˆฌ์ž ๊ถŒ์œ 
55
+
56
+ ## Prerequisites
57
+
58
+ - Node.js 18 ์ด์ƒ, ๋Ÿฐํƒ€์ž„ ์™ธ๋ถ€ ํŒจํ‚ค์ง€ ์—†์Œ
59
+ - ECOS ๊ณต๊ฐœ ๋ฐ๋ชจ ํ‚ค `sample`๋กœ ๋ฌด๊ฐ€์ž… ๋™์ž‘ํ•œ๋‹ค. ์กฐํšŒ์ฐฝ์„ 10์ผ ์ดํ•˜๋กœ ๋ถ„ํ• ํ•ด ํ˜ธ์ถœ๋‹น 10ํ–‰ ์ œํ•œ์„ ์ง€ํ‚จ๋‹ค.
60
+ - ์„ ํƒ ํ™˜๊ฒฝ๋ณ€์ˆ˜ `KSKILL_BOK_ECOS_API_KEY`: ECOS์—์„œ ๋ฌด๋ฃŒ ๋ฐœ๊ธ‰ํ•œ ๊ฐœ์ธ ํ‚ค
61
+ - ์‚ฌ์šฉ์ž CSV ๋ชจ๋“œ๋Š” ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ๊ธฐ์ค€์„ ์œ„ํ•ด `--base`์™€ `--as-of`๋ฅผ ๋ชจ๋‘ ๋ช…์‹œํ•œ๋‹ค.
62
+ - ๋‰ด์Šค ์ฝ”๋ฉ˜ํŠธ์—๋Š” ๊ณต๊ฐœ ์›น ๊ฒ€์ƒ‰ยท๋ธŒ๋ผ์šฐ์ € ์ ‘๊ทผ์ด ํ•„์š”ํ•˜๋‹ค. helper๋Š” ๊ธฐ์‚ฌ๋ฅผ ์ž๋™ ํฌ๋กค๋งํ•˜์ง€ ์•Š์œผ๋ฉฐ ๊ฒ€์ฆ๋œ ์งง์€ evidence๋งŒ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค.
63
+
64
+ ## Workflow
65
+
66
+ ### 1. ๊ธฐ๋ณธ D/W/M ๋Œ€์‹œ๋ณด๋“œ
67
+
68
+ โ€œ์˜ค๋Š˜ ์ปค๋ธŒ ์ •๋ฆฌโ€, โ€œ๋ฐ์ผ๋ฆฌ ์ปค๋ธŒโ€ ์š”์ฒญ์˜ ๊ธฐ๋ณธ ๋ชจ๋“œ๋‹ค.
69
+
70
+ ```bash
71
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset dashboard --text
72
+ ```
73
+
74
+ ํ˜„์žฌ 6๊ฐœ ๋งŒ๊ธฐ์˜ ๋ ˆ๋ฒจ๊ณผ ์ „์ผยท1์ฃผยท์ „์›”๋ง ๋ณ€ํ™”, 5Yโˆ’3Yยท10Yโˆ’5Yยท10Yโˆ’3Yยท30Yโˆ’10Y ์Šคํ”„๋ ˆ๋“œ, ํ•œ๊ตญ์€ํ–‰ ๊ธฐ์ค€๊ธˆ๋ฆฌ์™€ 3Yโˆ’๊ธฐ์ค€๊ธˆ๋ฆฌ ๊ฐญ์„ ํ•จ๊ป˜ ํ‘œ์‹œํ•œ๋‹ค. โ€œ์˜ค๋Š˜โ€์€ KST ์˜ค๋Š˜ ์ด์ „์— ECOS์—์„œ ํ™•๋ณด๋˜๋Š” ์ตœ์‹  ๊ณตํ†ต ๊ด€์ธก์ผ์ด๋‹ค.
75
+
76
+ ### 2. ํ˜„์žฌ ์Šค๋ƒ…์ƒท๊ณผ ๋‹จ์ผ ๋น„๊ต ํ”„๋ฆฌ์…‹
77
+
78
+ ```bash
79
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset today
80
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset previous-session
81
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset previous-week
82
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset previous-month-end
83
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset previous-quarter-end
84
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset year-start
85
+ ```
86
+
87
+ `previous-session`์€ ๋‹ฌ๋ ฅ์ƒ ์–ด์ œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ตœ์‹  ๊ณตํ†ต ๊ด€์ธก์ผ์˜ ์ง์ „ ๊ณตํ†ต ๊ด€์ธก์ผ์ด๋‹ค. `year-start` ๊ธฐ์ค€์€ ์ง์ „ ์—ฐ๋„ ๋งˆ์ง€๋ง‰ ๊ณตํ†ต ๊ด€์ธก์ผ์ด๋‹ค.
88
+
89
+ ### 3. ์ตœ๊ทผ 5ยท20๊ฑฐ๋ž˜์ผ ์ถ”์ด
90
+
91
+ ```bash
92
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --lookback-sessions 5 --text
93
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --lookback-sessions 20 --text
94
+ ```
95
+
96
+ ๋งŒ๊ธฐ๋ณ„ ์‹œ์ž‘ยท์ข…๋ฃŒ ๊ธˆ๋ฆฌ, ์ˆœ๋ณ€ํ™”, ๊ณ ์ ยท์ €์ , ์ตœ๋Œ€ ์ผ๊ฐ„ ๋ณ€๋™๊ณผ ๋ฐœ์ƒ์ผ์„ ํ‘œ์‹œํ•œ๋‹ค. ๊ณตํ†ต ๊ด€์ธก์ผ์ด ์š”์ฒญ ๊ฐœ์ˆ˜๋ณด๋‹ค ์ ์œผ๋ฉด `INCOMPLETE`์ด๋ฉฐ ํ™•๋ณด ๊ฑด์ˆ˜๋ฅผ ๋ฐํžŒ๋‹ค.
97
+
98
+ ### 4. ์›” ๋ฒ”์œ„์™€ ์ •ํ™•ํ•œ ๋‘ ๋‚ ์งœ
99
+
100
+ ์›”๋งŒ ์ฃผ์–ด์ง€๋ฉด ์›”๋ง ๋Œ€ ์›”๋ง ์Šค๋ƒ…์ƒท์œผ๋กœ ๋น„๊ตํ•œ๋‹ค. ์ข…๋ฃŒ ์›”์ด ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ๋‹ฌ์ด๋ฉด ๋ฏธ๋ž˜ ์›”๋ง์„ ์š”๊ตฌํ•˜์ง€ ์•Š๊ณ  KST ์˜ค๋Š˜ ๊ธฐ์ค€ ์ตœ์‹  ๊ณตํ†ต ๊ด€์ธก์ผ์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋‹ต๋ณ€์— ์‹ค์ œ ๊ด€์ธก์ผ์„ ๋ฐํžŒ๋‹ค.
101
+
102
+ ```bash
103
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --from-month 2026-07 --to-month 2026-08
104
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --base 2026-09-11 --as-of 2026-09-18
105
+ ```
106
+
107
+ ### 5. ๊ธฐ์ค€๊ธˆ๋ฆฌ ๊ฐญ
108
+
109
+ ```bash
110
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset policy-gap
111
+ ```
112
+
113
+ ECOS `722Y001/0101000` ํ•œ๊ตญ์€ํ–‰ ๊ธฐ์ค€๊ธˆ๋ฆฌ๋ฅผ ๋ณ„๋„๋กœ ๊ฒ€์ฆํ•˜๊ณ  ๊ฐ™์€ ์ตœ์‹  ์‹œ์ ์˜ ๊ตญ๊ณ ์ฑ„ 3๋…„๊ณผ ์ฐจ์ด๋ฅผ bp๋กœ ๊ณ„์‚ฐํ•œ๋‹ค. ๊ธฐ์ค€๊ธˆ๋ฆฌ ์กฐํšŒ๋งŒ ์‹คํŒจํ•˜๋ฉด ์ปค๋ธŒ ๊ฒฐ๊ณผ๋Š” ์œ ์ง€ํ•˜๊ณ  ๊ฐญ๋งŒ ์ œ์™ธํ•œ๋‹ค.
114
+
115
+ ### 6. ์ด๋ฒคํŠธ ์ „ํ›„
116
+
117
+ ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ •ํ•œ ๊ธˆํ†ต์œ„ยท๋ฌผ๊ฐ€ ๋ฐœํ‘œ ๋“ฑ ์ด๋ฒคํŠธ ๋‚ ์งœ์˜ ์ง์ „ ๊ณตํ†ต ๊ด€์ธก์ผ๊ณผ ๋‹น์ผ ๋˜๋Š” ์ดํ›„ ์ตœ์ดˆ ๊ณตํ†ต ๊ด€์ธก์ผ์„ ๋น„๊ตํ•œ๋‹ค.
118
+
119
+ ```bash
120
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --event-date 2026-08-27
121
+ ```
122
+
123
+ โ€œ์ตœ๊ทผ ๊ธˆํ†ต์œ„โ€๋Š” ํ•œ๊ตญ์€ํ–‰ ๊ณต์‹ ์ผ์ •์—์„œ ํšŒ์˜์ผ์„ ํ™•์ธํ•˜๊ณ  ๋งํฌ๋ฅผ ์ œ์‹œํ•œ ๋’ค ์‹คํ–‰ํ•œ๋‹ค. ์ผ์ •์€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
124
+
125
+ ### 7. ์ถœ์ฒ˜ ์žˆ๋Š” ์‹œ์žฅ ๋ฐฐ๊ฒฝ ์ฝ”๋ฉ˜ํŠธ
126
+
127
+ ์‚ฌ์šฉ์ž๊ฐ€ ์›์ธยท๋ฐฐ๊ฒฝยทํ—ค๋“œ๋ผ์ธ ์ฝ”๋ฉ˜ํŠธ๋ฅผ ์š”์ฒญํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‹คํ–‰ํ•œ๋‹ค. ๋จผ์ € ์ปค๋ธŒ๋ฅผ ๊ณ„์‚ฐํ•ด ์‹ค์ œ ํ˜„์žฌยท์ง์ „ ๊ด€์ธก์ผ์„ ํ™•์ธํ•œ ๋’ค, ํ˜„์žฌ ๊ด€์ธก์ผ๊ณผ KST ๋‚ ์งœ๊ฐ€ ๊ฐ™์€ ๊ณต๊ฐœ ์ž๋ฃŒ๋ฅผ ์ฐพ๋Š”๋‹ค.
128
+
129
+ ์ถœ์ฒ˜ ์šฐ์„ ์ˆœ์œ„๋Š” ์—ฐํ•ฉ์ธํฌ๋งฅ์Šค `[์ฑ„๊ถŒ-๋งˆ๊ฐ]`, ํ•œ๊ตญ์€ํ–‰ยท๊ธฐํš์žฌ์ •๋ถ€ยทFed ๋“ฑ ๊ณต์‹ ๋ฐœํ‘œ, ์—ฐํ•ฉ์ธํฌ๋งฅ์Šค ์žฅ์ค‘ ๊ธฐ์‚ฌ, Investing.com ๊ณต๊ฐœ ํ—ค๋“œ๋ผ์ธ ์ˆœ์ด๋‹ค. Investing.com์ด๋‚˜ ์—ฐํ•ฉ์ธํฌ๋งฅ์Šค๋ฅผ ์ƒ์‹œ ํฌ๋กค๋งํ•˜๊ฑฐ๋‚˜ ๊ธฐ์‚ฌ ๋ณธ๋ฌธ์„ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ œ๋ชฉยท๋งํฌยท๊ฒŒ์‹œ์‹œ๊ฐ๊ณผ 280์ž ์ดํ•˜์˜ ์งง์€ ์˜์—ญ๋งŒ evidence์— ๋‚จ๊ธด๋‹ค.
130
+
131
+ ```bash
132
+ # ์‹ค์ œ ๊ด€์ธก์ผ ํ™•์ธ
133
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --preset dashboard
134
+
135
+ # ๊ด€์ธก์ผ์— ๋งž๋Š” ์ž…๋ ฅ ํ‹€ ์ƒ์„ฑ
136
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --news-template 2026-09-21 > news-evidence.json
137
+
138
+ # ๊ธฐ์‚ฌ ํ™•์ธ ํ›„ evidence๋ฅผ ์ฑ„์›Œ ๋ณด๊ณ ์„œ ์ƒ์„ฑ
139
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --base 2026-09-18 --as-of 2026-09-21 \
140
+ --news-evidence news-evidence.json --out ./bond-report
141
+ ```
142
+
143
+ ๊ธฐ์‚ฌ ํ•ญ๋ชฉ์˜ ํ•„์ˆ˜ ํ•„๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
144
+
145
+ | ํ•„๋“œ | ๊ทœ์น™ |
146
+ | --- | --- |
147
+ | `publisher`, `title`, `url` | ์‹ค์ œ ๊ณต๊ฐœ ์ถœ์ฒ˜์™€ HTTPS ์›๋ฌธ ๋งํฌ |
148
+ | `publishedAt` | UTC ISO ์‹œ๊ฐ. KST ๋‚ ์งœ๊ฐ€ ์‹ค์ œ ํ˜„์žฌ ๊ด€์ธก์ผ๊ณผ ๊ฐ™์•„์•ผ ํ•จ |
149
+ | `sourceType` | `market-close`, `market-update`, `official-release`, `headline` |
150
+ | `marketDirection` | 3Yยท10Y ๋ณ€ํ™” ๊ธฐ์ค€ `higher`, `lower`, `mixed`, `flat`; ๊ณต์‹ ๋ฐœํ‘œ๋Š” `context` |
151
+ | `relation` | ๊ธฐ์‚ฌ๊ฐ€ ์š”์ธ์„ ์ง์ ‘ ์—ฐ๊ฒฐํ•˜๋ฉด `explicit`, ๋‹จ์ˆœ ๋™์‹œ ์ด๋ฒคํŠธ๋ฉด `contextual` |
152
+ | `factor` | ๊ธฐ์‚ฌ์—์„œ ๋ช…์‹œํ•œ ์‹œ์žฅ ๋ฐฐ๊ฒฝ์„ 140์ž ์ดํ•˜๋กœ ์˜์—ญ |
153
+ | `evidenceSummary` | ๊ทผ๊ฑฐ ๋‚ด์šฉ์„ 20~280์ž๋กœ ์˜์—ญ. ๊ธฐ์‚ฌ ๋ณธ๋ฌธ ๋ณต์ œ ๊ธˆ์ง€ |
154
+
155
+ `SOURCED/HIGH`๋Š” ๊ฐ™์€ ๋‚  ์—ฐํ•ฉ์ธํฌ๋งฅ์Šค ๋งˆ๊ฐ ๊ธฐ์‚ฌ๊ฐ€ ๊ด€์ธก ๋ฐฉํ–ฅ๊ณผ ์ผ์น˜ํ•˜๊ณ  ์š”์ธ์„ ์ง์ ‘ ์„ค๋ช…ํ•  ๋•Œ๋งŒ ๋ถ€์—ฌํ•œ๋‹ค. ๋ฐฉํ–ฅ์ด ๋‹ค๋ฅธ ๊ธฐ์‚ฌ๋Š” evidence์—๋Š” ๋ณด์กดํ•˜๋˜ ์ฝ”๋ฉ˜ํŠธ์—์„œ ์ œ์™ธํ•œ๋‹ค. ๊ณต์‹ ๋ฐœํ‘œ๋งŒ ์žˆ์œผ๋ฉด `CONTEXT_ONLY`, ์ผ์น˜ํ•˜๋Š” ๊ทผ๊ฑฐ๊ฐ€ ์—†์œผ๋ฉด `UNATTRIBUTED`๋‹ค. ๋‰ด์Šค ํ™•๋ณด ์‹คํŒจ๋‚˜ ๋ถˆ์ผ์น˜๋Š” ๊ฒ€์ฆ๋œ ์ปค๋ธŒ์˜ `READY`๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค.
156
+
157
+ ### 8. ์˜๊ตฌ ๋ณด๊ณ ์„œ์™€ ์‚ฌ์šฉ์ž CSV
158
+
159
+ HTMLยทCSVยทevidence ์ €์žฅ์„ ๋ช…์‹œ์ ์œผ๋กœ ์š”์ฒญํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ •ํ™•ํ•œ ๋‘ ๋‚ ์งœ ๋น„๊ต์— `--out`์„ ๋ถ™์ธ๋‹ค.
160
+
161
+ ```bash
162
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --base 2026-09-11 --as-of 2026-09-18 --out ./bond-report
163
+ npx -y @nomadamas/k-skill@0 exec government-bond-analysis scripts/cli.mjs -- --input input.csv --base 2026-09-11 --as-of 2026-09-18
164
+ ```
165
+
166
+ CSV ํ—ค๋”๋Š” `date,tenor,yield_pct`์ด๋ฉฐ ์—ฌ์„ฏ ๋งŒ๊ธฐ์™€ ์—ฐ% ๋‹จ์œ„๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์‚ฌ์šฉ์ž ์ž๋ฃŒ๋Š” `USER_CSV`๋กœ ํ‘œ์‹œํ•œ๋‹ค.
167
+
168
+ ## Output contract
169
+
170
+ - `READY`: ํ•ด๋‹น ๋ชจ๋“œ์— ํ•„์š”ํ•œ ๋ชจ๋“  ๊ณตํ†ต ๊ด€์ธก๊ฐ’๊ณผ ์›์ฒœ ๊ฒ€์ฆ ํ†ต๊ณผ
171
+ - `INCOMPLETE`: ์ผ๋ถ€ ๊ธฐ์ค€์ผยท์„ธ์…˜๋งŒ ํ™•๋ณด. ํ™•๋ณด๋˜์ง€ ์•Š์€ ๋น„๊ต๋Š” ํ•ด์„ํ•˜์ง€ ์•Š์Œ
172
+ - `BLOCKED`: ์œ ํšจํ•œ ๊ณตํ†ต ๊ด€์ธก๊ฐ’ ์—†์Œ. ๊ธฐ์–ต๊ฐ’์ด๋‚˜ ์ถ”์ •๊ฐ’ ์‚ฌ์šฉ ๊ธˆ์ง€
173
+ - ๊ธˆ๋ฆฌ ๋ ˆ๋ฒจ์€ ์—ฐ%, ๋ณ€ํ™”์™€ ์Šคํ”„๋ ˆ๋“œ๋Š” bp
174
+ - ์š”์ฒญ์ผ๊ณผ ์‹ค์ œ ๊ด€์ธก์ผ์„ ํ•ญ์ƒ ๊ตฌ๋ถ„
175
+ - Bull/Bear ๋ฐ steepening/flattening์€ ๊ด€์ธก ๋ณ€ํ™”์˜ ๊ธฐ๊ณ„์  ์ด๋ฆ„์ด๋ฉฐ ์ „๋ง์ด ์•„๋‹˜
176
+ - ๋‰ด์Šค ์ƒํƒœ๋Š” `SOURCED`, `CONTEXT_ONLY`, `UNATTRIBUTED`๋กœ ๋ณ„๋„ ํ‘œ์‹œํ•˜๋ฉฐ ์ปค๋ธŒ ์ƒํƒœ์™€ ์„ž์ง€ ์•Š์Œ
177
+ - ๊ณต์‹ ์ผ์ • ์ƒํƒœ๋Š” `VERIFIED`, `PARTIAL`, `UNVERIFIED`๋กœ ๋ณ„๋„ ํ‘œ์‹œํ•˜๋ฉฐ ๊ธˆ๋ฆฌ ์ƒํƒœ์™€ ์„ž์ง€ ์•Š์Œ
178
+ - ๋‰ด์Šค ๋ฌธ์žฅ์€ ํ•ญ์ƒ ๋งค์ฒด์— ๊ท€์†ํ•˜๊ณ  ์ธ๊ณผ๊ด€๊ณ„๋ฅผ ์ž์ฒด ์‚ฌ์‹ค๋กœ ๋‹จ์ •ํ•˜์ง€ ์•Š์Œ
179
+
180
+ ## Verification and data source
181
+
182
+ - ์ปค๋ธŒ: https://ecos.bok.or.kr/ ยท `817Y002` ยท `์—ฐ%` ยท `D`
183
+ - 2Y `010195000`, 3Y `010200000`, 5Y `010200001`, 10Y `010210000`, 20Y `010220000`, 30Y `010230000`
184
+ - ๊ธฐ์ค€๊ธˆ๋ฆฌ: `722Y001` ยท `0101000` ยท `ํ•œ๊ตญ์€ํ–‰ ๊ธฐ์ค€๊ธˆ๋ฆฌ` ยท `์—ฐ%`
185
+ - ํ†ต๊ณ„ํ‘œยทํ†ต๊ณ„๋ช…ยทํ•ญ๋ชฉ์ฝ”๋“œยทํ•ญ๋ชฉ๋ช…ยท๋‹จ์œ„ยท๋‚ ์งœยท์‘๋‹ต ๊ฑด์ˆ˜ยท์›๋ฌธ SHA-256์„ ๊ฒ€์ฆ
186
+ - ์ผ์‹œ ์˜ค๋ฅ˜๋งŒ ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ํ•˜๊ณ , ๋™์ผ ์š”์ฒญ์˜ 168์‹œ๊ฐ„ ์ด๋‚ด ์ด์ค‘ ํ•ด์‹œ ๊ฒ€์ฆ ์บ์‹œ๋งŒ ๋Œ€์ฒด ์‚ฌ์šฉ
187
+ - ๋‰ด์Šค evidence๋Š” ํ—ˆ์šฉ๋œ HTTPS ํ˜ธ์ŠคํŠธ, KST ๊ด€์ธก์ผ, ๊ฒŒ์‹œยท์ˆ˜์ง‘ ์‹œ๊ฐ, ์ค‘๋ณต URL, ๋ฐฉํ–ฅ ์ผ์น˜์™€ source fingerprint๋ฅผ ๊ฒ€์ฆ
188
+
189
+ ```bash
190
+ npx -y @nomadamas/k-skill@0 read government-bond-analysis references/methodology.md
191
+ ```
192
+
193
+ ## Done when
194
+
195
+ - ์„ ํƒ ๋ชจ๋“œ์— ํ•„์š”ํ•œ ์—ฌ์„ฏ ๋งŒ๊ธฐ์˜ ๊ณตํ†ต ๊ด€์ธก์ผ์ด ํ™•์ธ๋จ
196
+ - ์‹ค์ œ ๊ด€์ธก์ผ, ์—ฐ%์™€ bp, ๋ชจ๋“  ๊ณต์‹ ํ•ญ๋ชฉ์ฝ”๋“œ๊ฐ€ ๊ฒ€์ฆ๋จ
197
+ - `READY` ๊ฒฐ๊ณผ๋งŒ ์™„์ „ํ•œ ๋น„๊ต๋กœ ์„ค๋ช…ํ•จ
198
+ - ํ•œ๊ตญ์€ํ–‰ ECOS ๋งํฌ์™€ source receipt๊ฐ€ ํ•จ๊ป˜ ์ œ๊ณต๋จ
199
+ - ๋‰ด์Šค ์ฝ”๋ฉ˜ํŠธ ์š”์ฒญ ์‹œ ์ฑ„ํƒยท๋งฅ๋ฝยท์ œ์™ธ ๊ธฐ์‚ฌ์˜ ๋งํฌ์™€ fingerprint๊ฐ€ ๋ชจ๋‘ ๋‚จ์Œ
200
+ - ํŒŒ์ผ ์š”์ฒญ ์‹œ manifest ๋ฐ ์žฌ์ƒ ๊ฒ€์ฆ์„ ํ†ต๊ณผํ•จ
201
+
202
+ ## Failure modes
203
+
204
+ | ์ƒํ™ฉ | ๋™์ž‘ |
205
+ | --- | --- |
206
+ | ํœด์ผยท๊ฒฐ์ธก | 7์ผ ์•ˆ์˜ ๊ฐ€์žฅ ์ตœ๊ทผ ์—ฌ์„ฏ ๋งŒ๊ธฐ ๊ณตํ†ต ๊ด€์ธก์ผ ์‚ฌ์šฉ |
207
+ | ํ•œ ๋งŒ๊ธฐยทํ•œ ๊ตฌ๊ฐ„ ์‹คํŒจ | ๋‹ค๋ฅธ ๊ตฌ๊ฐ„ ์œ ์ง€, ๊ฒ€์ฆ๋œ ์บ์‹œ๋งŒ ํ•ด๋‹น ๊ตฌ๊ฐ„์— ์‚ฌ์šฉ |
208
+ | ์‹ค๋ฐ์ดํ„ฐ ์„ฑ๊ณตยท์บ์‹œ ์ €์žฅ ์‹คํŒจ | ๊ฒ€์ฆ๋œ ์‹ค๋ฐ์ดํ„ฐ ๊ฒฐ๊ณผ๋Š” ์œ ์ง€ํ•˜๊ณ  ์บ์‹œ ์ €์žฅ ๊ฒฝ๊ณ  ํ‘œ์‹œ |
209
+ | ๊ธฐ์ค€๊ธˆ๋ฆฌ๋งŒ ์‹คํŒจ | ์ปค๋ธŒ ์œ ์ง€, ์ •์ฑ…๊ธˆ๋ฆฌ ๊ฐญ๋งŒ ์ œ์™ธํ•˜๊ณ  ๊ฒฝ๊ณ  |
210
+ | ๋‰ด์Šค ๊ฒ€์ƒ‰ ์‹คํŒจ | ์ปค๋ธŒ ์œ ์ง€, `UNATTRIBUTED` ๋˜๋Š” ๋‰ด์Šค ์„น์…˜ ์ƒ๋žต |
211
+ | ๊ธฐ์‚ฌ ๋ฐฉํ–ฅ ๋ถˆ์ผ์น˜ | evidence์—๋Š” ๋ณด์กดํ•˜๊ณ  ์ฝ”๋ฉ˜ํŠธ์—์„œ๋Š” ์ œ์™ธ |
212
+ | ๊ธฐ์‚ฌ ๋‚ ์งœยทURLยทfingerprint ๋ถˆ์ผ์น˜ | ๋‰ด์Šค evidence ๊ฑฐ๋ถ€ |
213
+ | ๋‹จ์œ„ยทํ•ญ๋ชฉยทํ†ต๊ณ„ํ‘œ ๋ถˆ์ผ์น˜ | ํ•ด๋‹น ์›์ž๋ฃŒ ๊ฑฐ๋ถ€ |
214
+ | ๋น„๊ต์ผ์ด ๊ฐ™์€ ๊ด€์ธก์ผ๋กœ ๊ท€๊ฒฐ | `INCOMPLETE`, ๋ณ€ํ™” ํ•ด์„ ์ฐจ๋‹จ |
215
+ | ๊ณตํ†ต ๊ด€์ธก๊ฐ’ ์—†์Œ | `BLOCKED`, ์ถ”์ • ๊ธˆ์ง€ |
216
+ | ํŒŒ์ผ ๋ฏธ์š”์ฒญ | stdout๋งŒ ์ถœ๋ ฅํ•˜๊ณ  ๋ณด๊ณ ์„œ ํŒŒ์ผ์„ ๋งŒ๋“ค์ง€ ์•Š์Œ |