@nomadamas/k-skill 0.9.1 โ 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.
- package/bin/k-skill.js +64 -43
- package/package.json +2 -2
- package/skills/biz-health-check/instruction.md +2 -0
- package/skills/company-analysis/instruction.md +68 -0
- package/skills/company-analysis/references/source-contract.md +75 -0
- package/skills/company-analysis/scripts/dart-rows.js +52 -0
- package/skills/company-analysis/scripts/dart-rows.ts +48 -0
- package/skills/company-analysis/scripts/evidence.js +567 -0
- package/skills/company-analysis/scripts/evidence.ts +520 -0
- package/skills/company-analysis/scripts/fetch-public.js +85 -0
- package/skills/company-analysis/scripts/fetch-public.ts +75 -0
- package/skills/company-analysis/skill.json +6 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +46 -18
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +53 -3
- package/skills/g2b-order-plan-search/instruction.md +17 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +10 -1
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +8 -4
- package/skills/g2b-sanctioned-supplier/scripts/test_g2b_sanctioned_supplier.py +19 -0
- package/skills/government-bond-analysis/instruction.md +216 -0
- package/skills/government-bond-analysis/references/methodology.md +63 -0
- package/skills/government-bond-analysis/scripts/cli.mjs +268 -0
- package/skills/government-bond-analysis/scripts/collect.mjs +171 -0
- package/skills/government-bond-analysis/scripts/core.mjs +224 -0
- package/skills/government-bond-analysis/scripts/dual.mjs +178 -0
- package/skills/government-bond-analysis/scripts/harness.mjs +406 -0
- package/skills/government-bond-analysis/scripts/modes.mjs +136 -0
- package/skills/government-bond-analysis/scripts/news.mjs +113 -0
- package/skills/government-bond-analysis/scripts/render.mjs +120 -0
- package/skills/government-bond-analysis/scripts/schedule.mjs +96 -0
- package/skills/government-bond-analysis/scripts/ust.mjs +204 -0
- package/skills/government-bond-analysis/skill.json +8 -0
- package/skills/korean-bond-search/instruction.md +122 -0
- package/skills/korean-bond-search/references/fields.md +48 -0
- package/skills/korean-bond-search/references/source-notes.md +11 -0
- package/skills/korean-bond-search/scripts/bond_search.py +906 -0
- package/skills/korean-bond-search/skill.json +9 -0
- package/skills/market-event-impact/instruction.md +150 -0
- package/skills/market-event-impact/references/event-taxonomy.md +74 -0
- package/skills/market-event-impact/references/methodology.md +98 -0
- package/skills/market-event-impact/references/research-contract.md +120 -0
- package/skills/market-event-impact/references/source-priority.md +25 -0
- package/skills/market-event-impact/scripts/adapters.py +32 -0
- package/skills/market-event-impact/scripts/asset_selector.py +120 -0
- package/skills/market-event-impact/scripts/calendars.py +179 -0
- package/skills/market-event-impact/scripts/event_resolver.py +260 -0
- package/skills/market-event-impact/scripts/event_study.py +171 -0
- package/skills/market-event-impact/scripts/market_data.py +204 -0
- package/skills/market-event-impact/scripts/market_event.py +1065 -0
- package/skills/market-event-impact/scripts/research.py +389 -0
- package/skills/market-event-impact/skill.json +9 -0
- package/skills/multi-asset-morning-briefing/instruction.md +117 -0
- package/skills/multi-asset-morning-briefing/references/format-rules.md +236 -0
- package/skills/multi-asset-morning-briefing/scripts/market_data.py +1003 -0
- package/skills/multi-asset-morning-briefing/skill.json +9 -0
- package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
- package/src/analytics-policy.json +18 -0
- package/src/analytics.js +106 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
267
|
+
page.fill(LOGIN_FORM_SELECTOR, forest_id)
|
|
232
268
|
page.fill("#gnrlMmberPssrd", forest_pw)
|
|
233
|
-
page.click(
|
|
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(
|
|
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
|
-
|
|
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๋ง ์ถ๋ ฅํ๊ณ ๋ณด๊ณ ์ ํ์ผ์ ๋ง๋ค์ง ์์ |
|