@nomadamas/k-skill 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ # 한국일보 뉴스 조회
2
+
3
+ ## What this skill does
4
+
5
+ 한국일보가 운영하는 공식 원격 MCP 서버를 직접 호출해 기사 메타데이터를 조회한다.
6
+
7
+ - 엔드포인트: `https://mcp.hankookilbo.com/mcp` (Streamable HTTP MCP, 인증 불필요)
8
+ - 공식 MCP Registry 등재명: `com.hankookilbo.mcp/hankookilbo-mcp`
9
+ - 도구 10종 — 편집 헤드라인, 많이 본, 꼼꼼히 본, 시간대 추천, 최신, 섹션별 편집 추천, 주제 검색, 섹션 목록, 오늘의 운세, MBTI 운세
10
+ - 반환값은 기사 메타데이터다: 기사 ID, 제목, 원문 URL, 썸네일 URL, 발행시각, 기사 유형, 원문 접근성, 짧은 발췌(`excerpt`)
11
+ - 기사 본문 전문은 반환하지 않는다
12
+
13
+ 서버가 무상태라 `initialize` 핸드셰이크와 세션 ID 없이 단일 POST 로 `tools/call` 이 동작한다. MCP SDK 를 설치하지 않고 `curl` 로 호출한다.
14
+
15
+ ## When to use
16
+
17
+ - "한국일보 헤드라인 보여줘"
18
+ - "한국일보에서 많이 본 기사"
19
+ - "오늘 한국일보 정치면 추천 기사"
20
+ - "한국일보에 이 사건 기사 있어?"
21
+ - "오늘의 운세", "MBTI 운세" (한국일보 지면 기준)
22
+ - 한국일보가 무엇을 머리기사로 올렸는지, 즉 편집 판단 자체가 필요할 때
23
+
24
+ ## When not to use
25
+
26
+ - 언론사를 가리지 않는 일반 뉴스 키워드 검색 — 이 서버는 한국일보 기사만 반환한다
27
+ - 기사 본문 전문이 필요할 때 — 이 스킬은 메타데이터만 반환한다. 본문은 `item.url` 원문에서만 볼 수 있다
28
+ - 한국경제(한경)나 다른 언론사 기사 — 이 서버는 종합일간지 한국일보 전용이다
29
+ - 로그인 뒤에만 보이는 기사의 본문 — `item.view_type` 이 `LoginWall` 이면 원문 열람에 로그인이 필요하다
30
+ - 주식·환율 시세, 실시간 데이터 — 이 서버는 기사만 다룬다
31
+
32
+ ## Endpoint contract
33
+
34
+ - POST 만 쓴다. GET·PUT·DELETE·OPTIONS 는 405 다.
35
+ - `Accept` 헤더에 `application/json` 과 `text/event-stream` 을 **둘 다** 넣는다. MCP Streamable HTTP 규격이 클라이언트에 요구하는 사항이고, 서버 응답이 나중에 SSE 로 바뀌어도 깨지지 않는다. `Accept` 를 아예 빼면 406 이다.
36
+ - `initialize`, `notifications/initialized`, `Mcp-Session-Id` 는 필요 없다.
37
+ - `User-Agent` 를 `k-skill-hankookilbo/1.0` 으로 보낸다. 한국일보 쪽에서 k-skill 경유 트래픽을 분리 관측한다.
38
+
39
+ 기본 호출:
40
+
41
+ ```bash
42
+ curl -fsS --max-time 35 https://mcp.hankookilbo.com/mcp \
43
+ -H 'Content-Type: application/json' \
44
+ -H 'Accept: application/json, text/event-stream' \
45
+ -H 'User-Agent: k-skill-hankookilbo/1.0' \
46
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_top_headlines","arguments":{}}}'
47
+ ```
48
+
49
+ 기사 목록은 `result.structuredContent.items` 에 들어온다. `result.content[0].text` 는 사람이 읽는 요약본이다. 목록을 정리할 때는 `structuredContent` 를 쓴다.
50
+
51
+ ```bash
52
+ curl -fsS --max-time 35 https://mcp.hankookilbo.com/mcp \
53
+ -H 'Content-Type: application/json' \
54
+ -H 'Accept: application/json, text/event-stream' \
55
+ -H 'User-Agent: k-skill-hankookilbo/1.0' \
56
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_popular_news","arguments":{"section_cd":"economy","page_size":5}}}' \
57
+ | python3 -c 'import json,sys
58
+ for i in json.load(sys.stdin)["result"]["structuredContent"]["items"]:
59
+ print(i["published_at"], "|", i["title"], "|", i["url"])'
60
+ ```
61
+
62
+ MCP 클라이언트에 직접 등록해도 된다. 이 경로에서는 서버가 보내는 instructions 도 함께 전달된다.
63
+
64
+ ```bash
65
+ claude mcp add --transport http hankookilbo https://mcp.hankookilbo.com/mcp
66
+ codex mcp add hankookilbo --url https://mcp.hankookilbo.com/mcp
67
+ ```
68
+
69
+ ## 도구 선택
70
+
71
+ | 사용자 발화 | 도구 | 인자 |
72
+ | --- | --- | --- |
73
+ | "헤드라인", "오늘 주요 뉴스", "제일 중요한 뉴스" | `list_top_headlines` | 없음 |
74
+ | "많이 본", "인기 뉴스", "지금 뜨는" | `list_popular_news` | `section_cd`, `page_size`, `exclude_section_cd` (전부 선택) |
75
+ | "꼼꼼히 본", "정독할 만한" | `list_most_read_news` | 없음 |
76
+ | "지금 볼만한", "출근길에 볼 뉴스" | `list_timely_news` | 없음 |
77
+ | "최신 기사", "방금 올라온" | `list_latest_news` | `page_num`, `page_size` (선택) |
78
+ | 섹션별 편집 추천 | `list_recommended_articles` | `section_cd` (필수), `page_size` |
79
+ | 주제·인물·사건 검색, "왜 화제야" | `search_news` | `query` (필수), `limit` |
80
+ | 섹션 코드 확인 | `list_sections` | 없음 |
81
+ | "오늘의 운세", "띠별 운세" | `get_daily_horoscope` | `date` (선택) |
82
+ | "MBTI 운세" | `get_mbti_horoscope` | `date` (선택) |
83
+
84
+ `section_cd` 값:
85
+
86
+ - `politics` 정치
87
+ - `economy` 경제
88
+ - `society` 사회
89
+ - `world` 국제
90
+ - `culture` 문화
91
+ - `sports` 스포츠
92
+ - `life` 라이프
93
+ - `people` 사람
94
+ - `local` 지역
95
+ - `opinion` 오피니언
96
+
97
+ 값을 추측하지 말고 불확실하면 `list_sections` 로 확인한다.
98
+
99
+ 세 가지 목록이 서로 다르다.
100
+
101
+ - `list_top_headlines` — 편집부가 비중 있게 배치한 머리기사
102
+ - `list_popular_news` — 조회수 기준 인기 순위. "많이 본"은 이쪽이다
103
+ - `list_recommended_articles` — 섹션별 편집 추천. 순위 값을 갖지 않는다
104
+
105
+ ## Workflow
106
+
107
+ 1. 요청을 위 표로 분류한다. 섹션이 필요한데 불확실하면 `list_sections` 를 먼저 호출한다.
108
+ 2. `curl` 로 해당 도구를 호출한다.
109
+ 3. `result.structuredContent.items` 상위 3~5건을 제목·발행시각·원문 링크로 정리한다.
110
+ 4. `search_news` 는 응답이 30초에 가까울 수 있다. `--max-time 35` 를 주고, 타임아웃되면 재시도하지 말고 질의를 좁혀 다시 묻는다.
111
+
112
+ ## Response style
113
+
114
+ - **기사 제목은 `item.title` 원문을 그대로 인용한다.** 요약·의역·말줄임·기호 변경·맞춤법 교정을 거치면 실제 보도 제목과 달라진다. `[제목](url)` 형태가 원문 표기와 가장 일치한다.
115
+ - `item.excerpt` 는 기사 도입부 일부이고 기사 전체 요약이 아니다. 발췌에 없는 해석·평가·배경·결말을 이 데이터로 단정하지 않는다.
116
+ - 본문 전문은 `item.url` 에서만 확인할 수 있다. 본문 내용을 재구성하지 않고 링크를 제시한다.
117
+ - `item.view_type` 이 `LoginWall` 이면 원문 열람에 로그인이 필요하다고 함께 알린다.
118
+ - `items` 가 비어 있지 않다는 것은 그 조건의 기사가 존재한다는 뜻이다. 목록을 임의로 누락시키지 않는다.
119
+ - 운세 도구는 운세 내용을 반환하지 않는다. 제목·발행일·원문 링크만 전달하고 운세 내용은 원문 링크로 넘긴다.
120
+ - 원문 URL 에는 서버가 `?did=mcp` 유입 파라미터를 붙여 돌려준다. 링크를 제시할 때 이 파라미터를 지우지 않는다.
121
+
122
+ ## Failure modes
123
+
124
+ - `406 Not Acceptable` — `Accept` 헤더가 없다. `application/json, text/event-stream` 을 채워 재시도한다.
125
+ - `405 Method Not Allowed` — POST 가 아닌 메서드를 썼다. POST 로 바꾼다.
126
+ - `result.isError: true` — 도구 인자가 잘못됐다. `section_cd` 오타, `query` 누락을 확인한다.
127
+ - `504` — `search_news` 가 제한 시간을 넘겼다. 재시도 루프를 만들지 말고 질의를 좁힌다.
128
+ - 빈 `items` — 해당 조건의 기사가 없다. 섹션·질의를 바꿔 다시 시도하거나 없다고 답한다.
129
+ - 연결 실패 — 한국일보 측 서버 장애다. 재시도 루프를 만들지 않고 현재 조회 불가임을 분명히 말한다.
130
+
131
+ ## Privacy
132
+
133
+ - 인증·로그인·개인화가 없는 공개 조회 전용이다. 시크릿을 요구하지 않는다.
134
+ - 기사 본문을 요청하거나 저장하지 않는다.
135
+ - 검색어와 결과를 영구 저장하지 않는다.
136
+
137
+ ## Done when
138
+
139
+ - 요청 유형에 맞는 도구를 골랐다.
140
+ - `structuredContent.items` 기준으로 상위 후보를 제목·발행시각·원문 링크로 정리했다.
141
+ - 제목을 원문 그대로 인용하고 `[제목](url)` 링크를 제시했다.
142
+ - 본문이 필요한 요청이면 본문은 원문 링크에서만 볼 수 있다고 안내했다.
143
+ - 실패 시 재시도 루프 없이 원인을 알렸다.
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "hankookilbo-news",
3
+ "description": "한국일보 공식 원격 MCP 서버(mcp.hankookilbo.com)를 인증 없이 직접 호출해 편집 헤드라인·많이 본·꼼꼼히 본·최신 기사·섹션별 편집 추천·주제 검색·오늘의 운세를 기사 메타데이터(제목·발행시각·원문 링크) 중심으로 조회한다.",
4
+ "profiles": [
5
+ "lookup"
6
+ ],
7
+ "frontmatter": "name: hankookilbo-news\ndescription: 한국일보 공식 원격 MCP 서버(mcp.hankookilbo.com)를 인증 없이 직접 호출해 편집 헤드라인·많이 본·꼼꼼히 본·최신 기사·섹션별 편집 추천·주제 검색·오늘의 운세를 기사 메타데이터(제목·발행시각·원문 링크) 중심으로 조회한다.\nlicense: MIT\nmetadata:\n category: information\n locale: ko-KR\n phase: v1"
8
+ }
@@ -167,7 +167,7 @@ else
167
167
  fi
168
168
  ```
169
169
 
170
- repo 전체를 clone받은 경우에는 같은 검증을 `bash scripts/check-setup.sh` 로 실행해도 된다.
170
+ repo 전체를 clone받은 경우에는 같은 검증을 `npx -y @nomadamas/k-skill@0 exec k-skill-setup scripts/check-setup.sh --` 로 실행해도 된다.
171
171
 
172
172
  ### 3. Offer scheduled update checks
173
173
 
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ secrets_file="${1:-$HOME/.config/k-skill/secrets.env}"
5
+
6
+ missing=0
7
+
8
+ if [[ ! -f "$secrets_file" ]]; then
9
+ echo "missing secrets file: $secrets_file"
10
+ missing=1
11
+ else
12
+ perms=$(stat -f '%Lp' "$secrets_file" 2>/dev/null || stat -c '%a' "$secrets_file" 2>/dev/null)
13
+ if [[ "$perms" != "600" ]]; then
14
+ echo "insecure permissions on $secrets_file: $perms (expected 600)"
15
+ missing=1
16
+ fi
17
+ fi
18
+
19
+ if [[ "$missing" -ne 0 ]]; then
20
+ cat <<EOF
21
+ next steps:
22
+ 1. create ~/.config/k-skill/secrets.env with your credentials
23
+ 2. chmod 0600 ~/.config/k-skill/secrets.env
24
+ 3. run this check again
25
+ EOF
26
+ exit 1
27
+ fi
28
+
29
+ echo "k-skill setup looks usable"
@@ -7,5 +7,11 @@
7
7
  "browser",
8
8
  "operations"
9
9
  ],
10
- "frontmatter": "name: k-skill-setup\ndescription: After installing the full k-skill bundle, configure and verify the shared cross-platform setup, then optionally wire update checks and GitHub starring with explicit user consent.\nlicense: MIT\nmetadata:\n category: setup\n locale: ko-KR\n phase: v1"
10
+ "frontmatter": "name: k-skill-setup\ndescription: After installing the full k-skill bundle, configure and verify the shared cross-platform setup, then optionally wire update checks and GitHub starring with explicit user consent.\nlicense: MIT\nmetadata:\n category: setup\n locale: ko-KR\n phase: v1",
11
+ "bundle": [
12
+ {
13
+ "from": "scripts/check-setup.sh",
14
+ "to": "scripts/check-setup.sh"
15
+ }
16
+ ]
11
17
  }
@@ -132,12 +132,19 @@ npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- searc
132
132
 
133
133
  ### 3. Inspect the table meta before fetching data
134
134
 
135
- 데이터를 받기 전에 분류/단위/주기를 확인한다.
135
+ 데이터를 받기 전에 분류/단위를 확인한다.
136
136
 
137
137
  ```bash
138
138
  npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- meta --table-id DT_1JC1501 --text
139
139
  ```
140
140
 
141
+ **수록주기는 `meta` 로 확정되지 않는다.** `meta --meta-type TBL` 은 표 명칭(국/영문) 위주라 `--prd-se` 에 넣을 주기를 돌려주지 않고, `search` 응답의 `PRD_SE` 는 `--prd-se` 와 코드 체계가 달라(예: `DT_1IN0001` → `PRD_SE=A` 이지만 실제 조회는 `F`) 그대로 쓰면 안 된다. `STRT_PRD_DE`~`END_PRD_DE` 도 수록 범위일 뿐 간격을 알려주지 않는다. 조사주기는 `explain` 으로 확인하거나 §4 의 프로브로 확정한다.
142
+
143
+ ```bash
144
+ npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- explain \
145
+ --org-id 101 --table-id DT_1IN0001 --meta-itm All --text
146
+ ```
147
+
141
148
  ### 4. Fetch a small bounded slice first
142
149
 
143
150
  `--prd-se`, `--start`, `--end`, `--obj-l` 으로 범위를 좁혀 작은 슬라이스를 먼저 조회한다.
@@ -148,6 +155,16 @@ npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- data
148
155
  --obj-l 1=ALL --json
149
156
  ```
150
157
 
158
+ **`--prd-se` 를 먼저 확정한다.** 주기를 틀리면 다른 파라미터가 전부 맞아도 KOSIS는 코드 `30`(결과 없음)만 돌려주며, 메시지로는 원인을 알 수 없다. `explain`(§3)으로 조사주기를 확인하거나, 좁은 기간·최소 분류로 `Y` → `F` → `IR` → `M`/`Q`/`S` 순으로 프로브해 데이터가 나오는 코드를 찾은 뒤 본 조회를 한다. 총조사·인구주택총조사처럼 5년 간격인 표는 연 단위로 보여도 `F`(다년)다.
159
+
160
+ ```bash
161
+ for p in Y F IR; do
162
+ npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- data \
163
+ --org-id 101 --table-id DT_1IN0001 --prd-se "$p" \
164
+ --start 2010 --end 2010 --itm-id ALL --obj-l 1=00 --obj-l 2=00 --text
165
+ done
166
+ ```
167
+
151
168
  표마다 필수 분류 차원 수가 다르다. **default `--obj-l 1=ALL` 만으로는 부족한 표가 많다.** KOSIS가 코드 `20` (필수요청변수값 누락 objL)을 돌려주면, `meta --table-id <ID> --meta-type ITM --json` 으로 ITM 안에 들어 있는 `OBJ_ID`(분류 차원)와 코드를 확인한 뒤 `--obj-l 1=<코드> --obj-l 2=<코드>` 형태로 필요한 차원을 모두 지정한다. (많은 표가 OBJ 메타는 비어 있고 분류가 ITM 안에 들어 있음.)
152
169
 
153
170
  40,000셀을 초과하면 KOSIS는 에러 코드 `31` 또는 `41` 을 반환한다. 기간을 좁히거나(예: 5년→1년) 분류 필터의 ALL 을 특정 코드로 바꿔(예: `--obj-l 1=11` 서울만) 호출을 분할한다. 그래도 부족하면 사용자별 통계자료(`userStatsId`)를 등록해 `bigdata` 서브커맨드를 사용한다.
@@ -171,7 +188,7 @@ npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- bigda
171
188
  ## Done when
172
189
 
173
190
  - 사용자 질문에 대응하는 통계표 ID(`org_id`/`tbl_id`)가 명확하다.
174
- - 메타데이터를 1회 이상 조회해 분류·단위·주기를 확인했다.
191
+ - 메타데이터를 1회 이상 조회해 분류·단위를 확인했고, `--prd-se` 는 실제 데이터가 나오는 코드로 확정했다.
175
192
  - 작은 슬라이스부터 단계적으로 데이터를 받았다.
176
193
  - 결과에 출처(table id, 기간, 단위, endpoint)를 명시했다.
177
194
  - 한도 초과 시 분할 또는 `bigdata` 안내로 처리했다.
@@ -182,7 +199,10 @@ npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- bigda
182
199
  - KOSIS 에러 코드 `10`/`11`: 인증키 누락/만료 → 키 점검. `bigdata` 에서 `11` 이 나오면 `userStatsId` 가 본인 KOSIS 계정에 등록된 것이 아닐 가능성이 크다.
183
200
  - 코드 `20`: 필수 분류 누락 → `meta --meta-type OBJ` (또는 비어 있으면 `ITM`) 으로 필요한 차원 수와 코드를 확인하고 `--obj-l 1=... --obj-l 2=...` 모두 지정 후 재시도
184
201
  - 코드 `21`: 잘못된 요청 변수 → `org_id`/`tbl_id`/기간 형식 재확인. tblId 의심 시 `search` 로 정확한 ID 다시 찾기
185
- - 코드 `30`: 결과 없음 키워드를 짧게 또는 다른 표현으로 바꾸거나 기간/분류 완화. **meta 호출에서 30 이 나오면** 표가 해당 메타 타입을 지원하지 않는 경우이므로 다른 `--meta-type` 시도
202
+ - 코드 `30`: 결과 없음. 어느 서브커맨드에서 났는지로 원인이 갈린다.
203
+ - **`data` 에서 30 → 먼저 `--prd-se` 오지정을 의심한다.** 표의 실제 수록주기와 다른 코드를 주면 `--start`/`--end`/`--obj-l` 이 전부 맞아도 항상 30 이다. 연 단위처럼 보이는 표라도 총조사·센서스류는 다년주기 `F`(2·3·4·5·10년) 또는 부정기 `IR` 인 경우가 많다. `explain` 으로 조사주기를 확인하거나 `Y` → `F` → `IR` → `M`/`Q`/`S` 순으로 프로브해 확정한다 (§4 참고).
204
+ - `search` 에서 30 → 키워드를 더 짧게 또는 다른 표현으로 바꾸거나 기간/분류 완화
205
+ - **`meta` 에서 30** → 표가 해당 메타 타입을 지원하지 않는 경우이므로 다른 `--meta-type` 시도
186
206
  - 코드 `31`/`41`: 한도 초과 → 기간 좁히기, 분류 ALL 을 특정 코드로 바꾸기, 또는 `bigdata` 사용
187
207
  - 코드 `40`: 분당 1,000건 호출 한도 → 잠시 대기
188
208
  - 코드 `50`: KOSIS 서버 오류 → 1~2초 후 재시도
@@ -194,6 +214,7 @@ npx -y @nomadamas/k-skill@0 exec kosis-stats scripts/run_kosis_stats.py -- bigda
194
214
 
195
215
  - 코드 20 회복: `data --table-id DT_1J22001 --prd-se M --start 202401 --end 202401` → 코드 20 → `meta --table-id DT_1J22001 --meta-type ITM --json` 으로 차원 확인 → `data ... --obj-l 1=T10 --obj-l 2=0` 재호출 → 성공
196
216
  - 코드 31 회복: `data --table-id DT_1B26001 --prd-se Y --start 2020 --end 2024 --obj-l 1=ALL --obj-l 2=ALL --obj-l 3=ALL` → 코드 31 → `... --start 2024 --end 2024 --obj-l 1=11 --obj-l 2=ALL --obj-l 3=ALL` (서울만) 재호출 → 성공
217
+ - 코드 30 회복(수록주기 오지정): `data --org-id 101 --table-id DT_1IN0001 --prd-se Y --start 1990 --end 2010 --itm-id ALL --obj-l 1=00 --obj-l 2=00` → 코드 30 → 연도를 2015·2010·2005·2000·1995·1990 으로 바꿔 재시도해도 전부 코드 30, `--prd-se IR` 도 코드 30 → `--prd-se F --start 1925 --end 2010` 으로 재호출 → 성공(수록 시점 18개, 72셀). `DT_1IN0001`(총조사인구 총괄)은 연 단위 표처럼 보이지만 5년 간격 총조사라 `F` 로만 조회된다. 이 표는 `meta --meta-type TBL` 에도 주기가 없고 `search` 응답 `PRD_SE` 는 `A` 로 나와 둘 다 `--prd-se` 값으로 쓸 수 없었다.
197
218
 
198
219
  ## Maintainer review notes
199
220
 
@@ -54,10 +54,12 @@ GET https://kosis.kr/openapi/statisticsSearch.do
54
54
  &searchNm=인구&resultCount=20&startCount=1
55
55
  ```
56
56
 
57
- 응답 필드(주요): `ORG_ID`, `ORG_NM`, `TBL_ID`, `TBL_NM`, `STAT_ID`, `STAT_NM`, `VW_CD`, `MT_ATITLE`, `STRT_PRD_DE`, `END_PRD_DE`, `LINK_URL`.
57
+ 응답 필드(주요): `ORG_ID`, `ORG_NM`, `TBL_ID`, `TBL_NM`, `STAT_ID`, `STAT_NM`, `VW_CD`, `MT_ATITLE`, `PRD_SE`, `STRT_PRD_DE`, `END_PRD_DE`, `LINK_URL`.
58
58
 
59
59
  데이터 조회는 `ORG_ID` + `TBL_ID` 조합을 다음 단계에서 사용한다.
60
60
 
61
+ ⚠️ **`search` 의 `PRD_SE` 를 §3.3 의 `prdSe` 요청 파라미터로 그대로 쓰지 말 것.** 두 필드는 코드 체계가 다르다 — `DT_1IN0001`(총조사인구 총괄)은 `search` 가 `PRD_SE=A` 를 돌려주지만 실제 데이터 조회는 `prdSe=F` 로만 성공하고 `A`·`Y`·`IR` 은 모두 코드 `30`(결과 없음)이다. `STRT_PRD_DE`~`END_PRD_DE` 도 수록 범위일 뿐 간격(주기)을 알려주지 않는다. 요청용 주기는 `statisticsExplData.do`(helper 의 `explain` 서브커맨드) 의 조사주기 항목에서 확인하거나, §3.3 표의 코드를 좁은 슬라이스로 프로브해 확정한다.
62
+
61
63
  ### 3.2 통계표 메타데이터 (`statisticsData.do?method=getMeta`)
62
64
 
63
65
  통계표의 분류·항목·단위·국문/영문명 등을 조회한다.
@@ -97,6 +99,8 @@ GET https://kosis.kr/openapi/Param/statisticsParameterData.do
97
99
  | `F` | 다년(2,3,4,5,10년) | `YYYY` |
98
100
  | `IR` | 부정기 | `YYYY` 또는 `YYYYMMDD` |
99
101
 
102
+ `prdSe` 를 틀리면 `startPrdDe`/`endPrdDe`/`objL*` 이 전부 맞아도 응답은 항상 코드 `30`(결과 없음)이고, 메시지만으로는 주기 문제인지 구분되지 않는다. 5년 간격 총조사처럼 연 단위로 보이는 표가 `F`(다년)인 경우가 흔하므로, `statisticsExplData.do` 로 조사주기를 확인하거나 최소 슬라이스로 `Y` → `F` → `IR` → `M`/`Q`/`S` 순으로 프로브해 확정한 뒤 본 조회를 한다.
103
+
100
104
  분류 파라미터는 `objL1` ~ `objL8` (필요한 만큼만), 항목은 `itmId` (`ALL` 또는 특정 ID).
101
105
 
102
106
  응답 셀 필드: `PRD_DE` (기간), `ITM_NM` (항목), `UNIT_NM` (단위), `DT` (값), `C1_NM`~`C8_NM` (분류명).
@@ -58,7 +58,7 @@ ERROR_CODE_HINTS: dict[str, str] = {
58
58
  "11": "인증키가 만료되었거나 해당 endpoint에서 무효입니다. https://kosis.kr/openapi/ 에서 갱신하거나, bigdata는 본인이 등록한 userStatsId 인지 확인하세요.",
59
59
  "20": "필수 요청 변수가 누락되었습니다. `meta --table-id <ID> --meta-type ITM --json` 으로 ITM 안에 들어 있는 OBJ_ID(분류 차원)와 코드를 확인하세요(많은 표가 OBJ 메타는 비어 있고 분류가 ITM 안에 들어 있음). 그 뒤 `--obj-l 1=<코드> --obj-l 2=<코드>` 형태로 필요한 차원을 모두 지정해 재호출하세요. 별도 OBJ 메타가 있는 표는 `--meta-type OBJ` 로도 확인 가능합니다.",
60
60
  "21": "잘못된 요청 변수입니다. orgId/tblId/기간 형식을 재확인하세요. tblId가 의심되면 `search --query <키워드>` 로 정확한 ID를 다시 찾으세요.",
61
- "30": "조회 결과가 없습니다. 키워드를 더 짧게(예: '1인 가구' → '가구') 또는 다른 표현으로 재검색하거나, 기간/분류 필터를 완화하세요. meta 호출에서 이 에러가 나면 해당 메타 타입을 표가 지원하지 않는 경우이므로 다른 `--meta-type` 을 시도하세요.",
61
+ "30": "조회 결과가 없습니다. data 호출이면 먼저 `--prd-se` 오지정을 의심하세요 — 표의 실제 수록주기와 다르면 기간/분류가 맞아도 항상 이 에러가 납니다. 연 단위로 보이는 총조사·센서스류는 다년주기 `F` 또는 부정기 `IR` 인 경우가 많으니 `explain --org-id <ORG> --table-id <TBL>` 로 조사주기를 확인하거나 좁은 슬라이스로 `Y` → `F` → `IR` → `M`/`Q`/`S` 순으로 프로브하세요(`search` 응답의 `PRD_SE` 는 코드 체계가 달라 그대로 쓸 수 없습니다). search 호출이면 키워드를 더 짧게(예: '1인 가구' → '가구') 또는 다른 표현으로 재검색하거나, 기간/분류 필터를 완화하세요. meta 호출에서 이 에러가 나면 해당 메타 타입을 표가 지원하지 않는 경우이므로 다른 `--meta-type` 을 시도하세요.",
62
62
  "31": "조회 결과가 한도(40,000셀)를 초과했습니다. 기간을 좁히거나(예: 5년→1년) 분류 필터의 ALL 을 특정 코드로 바꾸세요(예: `--obj-l 1=ALL` → `--obj-l 1=11` 서울만). 그래도 부족하면 `bigdata` 서브커맨드 + 사전 등록한 userStatsId 를 사용하세요.",
63
63
  "40": "분당 호출 한도(1,000건)를 초과했습니다. 잠시 대기 후 재시도하거나 호출 간 sleep 을 두세요.",
64
64
  "41": "1회 호출 ROW 한도를 초과했습니다. 기간이나 분류를 좁혀 쿼리를 분할하세요.",
@@ -73,13 +73,13 @@ python3 -m pip install korail2-ncard pycryptodome
73
73
  항상 helper 를 통해 조회한다.
74
74
 
75
75
  ```bash
76
- python3 scripts/ktx_booking.py search 서울 부산 20260328 090000 --limit 5
76
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- search 서울 부산 20260328 090000 --limit 5
77
77
  ```
78
78
 
79
79
  기본 `--train-type` 은 `ktx` 다. ITX-청춘(예: 남춘천↔용산)·ITX-새마을·무궁화호처럼 KTX 외 노선을 잡으려면 `--train-type` 으로 지정한다.
80
80
 
81
81
  ```bash
82
- python3 scripts/ktx_booking.py search 남춘천 용산 20260503 150000 --train-type itx-cheongchun
82
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- search 남춘천 용산 20260503 150000 --train-type itx-cheongchun
83
83
  ```
84
84
 
85
85
  선택지: `ktx`, `itx-saemaeul`, `mugunghwa`, `nuriro`, `tonggeun`, `itx-cheongchun`, `airport`, `all`.
@@ -106,19 +106,19 @@ python3 scripts/ktx_booking.py search 남춘천 용산 20260503 150000 --train-t
106
106
  기본 상세 좌석 조회:
107
107
 
108
108
  ```bash
109
- python3 scripts/ktx_booking.py seats 서울 부산 20260328 090000 --train-id <train_id>
109
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- seats 서울 부산 20260328 090000 --train-id <train_id>
110
110
  ```
111
111
 
112
112
  일반실/특실은 `--room` 으로 나눈다.
113
113
 
114
114
  ```bash
115
- python3 scripts/ktx_booking.py seats 서울 부산 20260328 090000 --train-id <train_id> --room special
115
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- seats 서울 부산 20260328 090000 --train-id <train_id> --room special
116
116
  ```
117
117
 
118
118
  남은 좌석번호만 보고 싶으면 `--available-only` 를 쓴다.
119
119
 
120
120
  ```bash
121
- python3 scripts/ktx_booking.py seats 서울 부산 20260328 090000 --train-id <train_id> --available-only
121
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- seats 서울 부산 20260328 090000 --train-id <train_id> --available-only
122
122
  ```
123
123
 
124
124
  특정 호차를 지정하지 않으면 `seats` 는 **5호차를 최우선**으로 탐색한다. 5호차가 없으면 5호차와의 거리가 가까운 호차 순으로, 같은 거리에서는 낮은 호차 번호 순으로 탐색한다(예: 1~8호차 편성은 `5, 4, 6, 3, 7, 2, 8, 1`, 1~4호차 편성은 `4, 3, 2, 1`). 일반 KTX, KTX-산천(분류 코드 07·10), KTX-청룡 모두 같은 규칙을 적용한다. 각 호차 안의 좌석은 콘센트 힌트가 있는 좌석(`direct`, `adjacent`)을 먼저, 같은 조건에서는 순방향 좌석을 먼저 보여준다.
@@ -126,19 +126,19 @@ python3 scripts/ktx_booking.py seats 서울 부산 20260328 090000 --train-id <t
126
126
  특정 호차만 확인하려면 `--car-no` 를 쓴다.
127
127
 
128
128
  ```bash
129
- python3 scripts/ktx_booking.py seats 서울 부산 20260328 090000 --train-id <train_id> --car-no 5 --available-only
129
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- seats 서울 부산 20260328 090000 --train-id <train_id> --car-no 5 --available-only
130
130
  ```
131
131
 
132
132
  콘센트 꿀팁 자리부터 확인하려면 `--power-only` 를 붙인다. 응답의 `power_outlet` 은 `direct`, `adjacent`, `none` 중 하나다.
133
133
 
134
134
  ```bash
135
- python3 scripts/ktx_booking.py seats 서울 부산 20260328 090000 --train-id <train_id> --available-only --power-only
135
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- seats 서울 부산 20260328 090000 --train-id <train_id> --available-only --power-only
136
136
  ```
137
137
 
138
138
  `seats` 도 `search` 와 같은 `--train-type` 을 넘겨야 한다. ITX-청춘 등 KTX 외 열차를 조회했다면 상세 좌석 조회에도 같은 값을 사용한다.
139
139
 
140
140
  ```bash
141
- python3 scripts/ktx_booking.py seats 남춘천 용산 20260503 150000 \
141
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- seats 남춘천 용산 20260503 150000 \
142
142
  --train-id <train_id> \
143
143
  --train-type itx-cheongchun \
144
144
  --available-only
@@ -159,13 +159,13 @@ python3 scripts/ktx_booking.py seats 남춘천 용산 20260503 150000 \
159
159
  조회 결과의 `train_id` 를 고른 뒤에만 예약한다. 이 값은 helper 가 열차 번호/운행일/시각/역 코드를 묶어 만든 stable selector 이므로, 재조회 시 같은 열차가 아직 있으면 그대로 잡고 없으면 실패한다.
160
160
 
161
161
  ```bash
162
- python3 scripts/ktx_booking.py reserve 서울 부산 20260328 090000 --train-id <train_id> --seat-option general-first
162
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- reserve 서울 부산 20260328 090000 --train-id <train_id> --seat-option general-first
163
163
  ```
164
164
 
165
165
  ITX 등 KTX 외 노선을 search 단계에서 골랐다면 reserve 에도 똑같이 `--train-type` 을 넘긴다.
166
166
 
167
167
  ```bash
168
- python3 scripts/ktx_booking.py reserve 남춘천 용산 20260503 150000 --train-id <train_id> --train-type itx-cheongchun --seat-option general-first
168
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- reserve 남춘천 용산 20260503 150000 --train-id <train_id> --train-type itx-cheongchun --seat-option general-first
169
169
  ```
170
170
 
171
171
  응답에는 예약번호, 운임, 구입기한이 포함된다. 이 시점에 **좌석 확보는 완료되었다**고 안내한다. generic fallback에서는 결제를 handoff하고, 돌쇠에서는 아래 결제 단계로 계속 진행한다.
@@ -176,19 +176,19 @@ python3 scripts/ktx_booking.py reserve 남춘천 용산 20260503 150000 --train-
176
176
  N카드 할인을 적용하려면 먼저 보유 N카드 목록을 조회해 카드 번호를 확인한다.
177
177
 
178
178
  ```bash
179
- python3 scripts/ktx_booking.py ncard-list
179
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- ncard-list
180
180
  ```
181
181
 
182
182
  N카드로 할인 열차를 조회한다 (`--ncard-index` 는 `ncard-list` 결과의 순번). `ncard-list` 는 로그/셸 노출을 줄이기 위해 카드 번호를 마스킹해 출력한다.
183
183
 
184
184
  ```bash
185
- python3 scripts/ktx_booking.py ncard-search 대전 서울 20260512 100000 --ncard-index 1 --train-type ktx
185
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- ncard-search 대전 서울 20260512 100000 --ncard-index 1 --train-type ktx
186
186
  ```
187
187
 
188
188
  응답의 `train_id` 를 복사해 `reserve` 에 같은 `--ncard-index` 를 붙여 예약한다.
189
189
 
190
190
  ```bash
191
- python3 scripts/ktx_booking.py reserve 대전 서울 20260512 100000 \
191
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- reserve 대전 서울 20260512 100000 \
192
192
  --train-id <train_id> \
193
193
  --ncard-index 1
194
194
  ```
@@ -213,13 +213,13 @@ N카드 기능은 `korail2-ncard` 패키지가 필요하다. 없으면 해당
213
213
  취소는 대상 예약을 다시 조회해 식별한 뒤에만 진행한다.
214
214
 
215
215
  ```bash
216
- python3 scripts/ktx_booking.py reservations
216
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- reservations
217
217
  ```
218
218
 
219
219
  취소 실행 직전에 `clarify`로 예약번호, 열차, 날짜·시각, 승객, 환불/위약금 정보를 확인하고 승인받는다.
220
220
 
221
221
  ```bash
222
- python3 scripts/ktx_booking.py cancel <reservation_id>
222
+ npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- cancel <reservation_id>
223
223
  ```
224
224
 
225
225
  ## Done when