@nomadamas/k-skill 0.4.3 → 0.6.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.
@@ -0,0 +1,66 @@
1
+ # KAMIS 농수축산물 가격 조회
2
+
3
+ ## What this skill does
4
+
5
+ 한국농수산식품유통공사 KAMIS의 `dailyPriceByCategoryList`를
6
+ `k-skill-proxy`의 좁은 read-only route로 호출한다. 농산물·축산물·수산물의
7
+ 부류별 도매/소매 가격과 전일, 1주일, 1개월, 1년, 평년 비교값을 반환한다.
8
+
9
+ 이 skill은 쇼핑몰 최저가나 투자 추천이 아니라 공식 유통가격 조회용이다.
10
+
11
+ ## Inputs
12
+
13
+ - `p_productclscode`: `01` 소매, `02` 도매. 기본 `01`
14
+ - `p_itemcategorycode`: `100` 식량작물, `200` 채소류, `300` 특용작물,
15
+ `400` 과일류, `500` 축산물, `600` 수산물. 기본 `100`
16
+ - `p_countycode`: 지역 코드(예: `1101` 서울). 생략하면 전체 지역
17
+ - `p_regday`: `YYYY-MM-DD`, 생략하면 최근 조사일
18
+ - `p_convert_kg_yn`: `Y` 또는 `N`. 기본 `N`
19
+
20
+ 사용자가 문서식 별칭인 `p_product_cls_code`, `p_country_code`,
21
+ `p_item_category_code`를 주어도 proxy가 실제 upstream 계약명으로 정규화한다.
22
+
23
+ ## Workflow
24
+
25
+ ```bash
26
+ BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
27
+ curl -fsS --get "$BASE/v1/kamis/food-price/daily-category" \
28
+ --data-urlencode "p_productclscode=01" \
29
+ --data-urlencode "p_itemcategorycode=200" \
30
+ --data-urlencode "p_countycode=1101" \
31
+ --data-urlencode "p_convert_kg_yn=N"
32
+ ```
33
+
34
+ 응답의 `items` 각 항목에서 `item_name`, `kind_name`, `rank`, `unit`,
35
+ `dpr1`(조회일), `dpr2`(1일 전), `dpr3`(1주일 전), `dpr5`(1개월 전),
36
+ `dpr6`(1년 전), `dpr7`(평년)을 확인한다. 가격 문자열의 쉼표와 `-`를
37
+ 숫자로 바꿀 때는 빈 값 여부를 먼저 보존한다.
38
+
39
+ ## Access path and credentials
40
+
41
+ 일반 사용자는 hosted `k-skill-proxy`를 사용하므로 KAMIS 키가 필요 없다.
42
+ proxy 운영자는 KAMIS에서 발급받은 `KAMIS_API_KEY`를 서버 runtime env에
43
+ 보관한다. upstream의 `p_cert_id`는 별도 API key가 아니라 요청자 ID
44
+ 파라미터이며, 현재 proxy는 live 계약에서 검증한 `TEST` 값을 사용한다.
45
+ 키를 URL, 로그, 저장소, 사용자 응답에 노출하지 않는다.
46
+
47
+ 공식 문서:
48
+ <https://www.kamis.or.kr/customer/reference/openapi_list.do?action=detail&boardno=1>
49
+
50
+ 실제 upstream 경로는 `xml.do`이지만 `p_returntype=json`을 사용한다. 이는
51
+ KAMIS의 공식 계약이며 URL을 `json.do`로 바꾸지 않는다.
52
+
53
+ ## Failure modes
54
+
55
+ - `400 bad_request`: 잘못된 날짜, 지역 코드, 부류 코드, 도소매 값
56
+ - `503 upstream_not_configured`: proxy runtime에 `KAMIS_API_KEY` 없음
57
+ - `400` 또는 `502 upstream_error`: KAMIS code `200`, `900`, 네트워크/상류 오류
58
+ - `items: []`, upstream code `001`: 해당 조건의 가격 없음
59
+ - KAMIS가 JSON 대신 점검 HTML/XML을 반환하면 `upstream_invalid_response`
60
+ - 가격은 조사 시점의 공식 데이터이며 구매 보장·법률 판단·투자 조언이 아니다
61
+
62
+ ## Done when
63
+
64
+ - 조회 조건과 `query`가 응답에 남아 있다.
65
+ - 각 가격 항목의 조회일과 비교 기간을 구분했다.
66
+ - 답변에 KAMIS 원문 출처와 조회 조건을 함께 적었다.
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """Read KAMIS prices through the hosted proxy, with an optional direct mode."""
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ import urllib.error
8
+ import urllib.parse
9
+ import urllib.request
10
+
11
+ DEFAULT_PROXY = "https://k-skill-proxy.nomadamas.org"
12
+ UPSTREAM = "https://www.kamis.or.kr/service/price/xml.do"
13
+
14
+
15
+ def secrets(path):
16
+ values = {}
17
+ try:
18
+ with open(path, encoding="utf-8") as stream:
19
+ for line in stream:
20
+ line = line.strip()
21
+ if line and not line.startswith("#") and "=" in line:
22
+ key, value = line.split("=", 1)
23
+ values[key.strip()] = value.strip().strip("\"'")
24
+ except OSError:
25
+ pass
26
+ return values
27
+
28
+
29
+ def query(args):
30
+ if args.product_class not in {"01", "02"}:
31
+ raise ValueError("--product-class must be 01 or 02")
32
+ if args.category not in {"100", "200", "300", "400", "500", "600"}:
33
+ raise ValueError("--category must be 100, 200, 300, 400, 500, or 600")
34
+ if args.convert_kg not in {"Y", "N"}:
35
+ raise ValueError("--convert-kg must be Y or N")
36
+ result = {
37
+ "p_productclscode": args.product_class,
38
+ "p_itemcategorycode": args.category,
39
+ "p_convert_kg_yn": args.convert_kg,
40
+ "p_returntype": "json",
41
+ }
42
+ if args.county:
43
+ if not args.county.isdigit() or len(args.county) != 4:
44
+ raise ValueError("--county must be a four-digit KAMIS code")
45
+ result["p_countycode"] = args.county
46
+ if args.date:
47
+ if len(args.date) != 10 or args.date[4] != "-" or args.date[7] != "-":
48
+ raise ValueError("--date must be YYYY-MM-DD")
49
+ result["p_regday"] = args.date
50
+ return result
51
+
52
+
53
+ def main(argv=None):
54
+ parser = argparse.ArgumentParser(description="KAMIS 농수축산물 가격 조회")
55
+ parser.add_argument("--product-class", default="01")
56
+ parser.add_argument("--category", default="100")
57
+ parser.add_argument("--county")
58
+ parser.add_argument("--date")
59
+ parser.add_argument("--convert-kg", default="N")
60
+ parser.add_argument("--text", action="store_true")
61
+ parser.add_argument("--dry-run", action="store_true")
62
+ parser.add_argument("--direct", action="store_true")
63
+ parser.add_argument("--proxy-base-url", default=os.getenv("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY))
64
+ parser.add_argument("--secrets-path", default=os.path.expanduser("~/.config/k-skill/secrets.env"))
65
+ args = parser.parse_args(argv)
66
+ key = None
67
+ try:
68
+ params = query(args)
69
+ except ValueError as error:
70
+ print(f"[error] {error}", file=sys.stderr)
71
+ return 2
72
+
73
+ if args.direct:
74
+ local = secrets(args.secrets_path)
75
+ key = os.getenv("KSKILL_KAMIS_API_KEY") or local.get("KSKILL_KAMIS_API_KEY")
76
+ if not key:
77
+ print("[error] --direct requires KSKILL_KAMIS_API_KEY", file=sys.stderr)
78
+ return 3
79
+ assert key is not None
80
+ params = {**params, "action": "dailyPriceByCategoryList", "p_cert_key": key, "p_cert_id": "TEST"}
81
+ url = f"{UPSTREAM}?{urllib.parse.urlencode(params)}"
82
+ else:
83
+ url = f"{args.proxy_base_url.rstrip('/')}/v1/kamis/food-price/daily-category?{urllib.parse.urlencode(params)}"
84
+
85
+ if args.dry_run:
86
+ display_params = {**params}
87
+ if args.direct:
88
+ display_params["p_cert_key"] = "<redacted>"
89
+ display_url = url.replace(key, "<redacted>") if args.direct and key is not None else url
90
+ print(json.dumps({"url": display_url, "query": display_params}, ensure_ascii=False, indent=2))
91
+ return 0
92
+
93
+ try:
94
+ with urllib.request.urlopen(urllib.request.Request(url, headers={"accept": "application/json"}), timeout=30) as response:
95
+ payload = json.loads(response.read())
96
+ except (OSError, ValueError, urllib.error.HTTPError) as error:
97
+ print(f"[error] KAMIS request failed: {error}", file=sys.stderr)
98
+ return 4
99
+
100
+ if args.text:
101
+ items = payload.get("items", payload.get("data", {}).get("item", []))
102
+ for item in items:
103
+ print(f"{item.get('item_name', '?')} {item.get('kind_name', '')} {item.get('dpr1', '-')}{item.get('unit', '')}")
104
+ else:
105
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
106
+ return 0
107
+
108
+
109
+ if __name__ == "__main__":
110
+ raise SystemExit(main())
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "kamis-food-price",
3
+ "description": "KAMIS 농수축산물 유통 Open API를 k-skill-proxy 경유로 호출해 부류별 도소매 가격과 전일·주간·월간·연간 비교값을 조회한다. 조회 전용.",
4
+ "profiles": [
5
+ "proxy",
6
+ "lookup"
7
+ ],
8
+ "frontmatter": "name: kamis-food-price\ndescription: KAMIS 농수축산물 유통 Open API를 k-skill-proxy 경유로 호출해 부류별 도소매 가격과 전일·주간·월간·연간 비교값을 조회한다. 조회 전용.\nlicense: MIT\nmetadata:\n category: food\n locale: ko-KR\n phase: v1"
9
+ }
@@ -136,6 +136,12 @@ CGV는 시간표 중심으로 본다.
136
136
  npx --yes daiso get /api/cgv/timetable --keyword 강남 --playDate <YYYYMMDD> --json
137
137
  ```
138
138
 
139
+ CGV 공개 endpoint가 `503 CGV_UPSTREAM_UNAVAILABLE`을 반환하면 CGV 공식 웹사이트
140
+ (`https://www.cgv.co.kr`)를 브라우저 사용 스킬로 대체 조회한다. 이때도 공개 상영정보
141
+ 조회만 수행하고, 로그인·예매·결제는 진행하지 않는다. 공식 웹사이트에서 비정상 접속
142
+ 차단, CAPTCHA, 브라우저 검증 또는 유사한 접근 제한이 표시되면 이를 우회하지 말고
143
+ CGV 조회를 중단한다.
144
+
139
145
  메가박스와 롯데시네마는 잔여석 endpoint를 사용할 수 있다.
140
146
 
141
147
  ```bash
@@ -165,7 +171,7 @@ npx --yes daiso get /api/lottecinema/seats --keyword 월드타워 --playDate <YY
165
171
 
166
172
  ## Failure modes
167
173
 
168
- - public endpoint upstream 상태에 따라 간헐적인 5xx를 수 있다.
174
+ - CGV 공개 endpoint CGV 원본 서비스의 차단·접속 제한을 받으면 `503 CGV_UPSTREAM_UNAVAILABLE`을 반환할 수 있다. 이 응답은 skill 또는 CLI 입력 오류가 아니라 upstream 접근 실패이므로 CGV 공식 웹사이트를 브라우저 사용 스킬로 대체 조회한다. 공식 웹사이트에서도 차단, CAPTCHA, 브라우저 검증 또는 유사한 접근 제한이 표시되면 우회하지 말고 중단한다.
169
175
  - 지역 키워드가 넓으면 다른 지점이 섞일 수 있다.
170
176
  - 시간표와 잔여석은 시점에 따라 달라진다.
171
177
  - 일부 체인은 상영작, 시간표, 잔여석 endpoint의 입력값이 다르므로 theaterId, movieId가 있으면 그 값을 우선 사용한다.
@@ -0,0 +1,62 @@
1
+ # 외교부 해외안전·여행경보 조회
2
+
3
+ ## What this skill does
4
+
5
+ 외교부 `국가·지역별 여행경보 목록 조회(0404 대륙정보)` API를
6
+ `k-skill-proxy`의 좁은 read-only route로 조회한다. 국가명 또는 ISO 2자리
7
+ 코드로 공식 여행경보 단계, 지역 유형, 경보 내용, 작성일과 공식 지도 URL을
8
+ 확인한다.
9
+
10
+ 이 skill은 자체적인 안전/위험 점수나 여행 허가 판단을 만들지 않는다.
11
+ 응답된 외교부 공식 경보와 원문 링크를 사실 그대로 요약한다.
12
+
13
+ ## Inputs
14
+
15
+ - `country_iso_alp2`: ISO 2자리 국가코드(예: `RU`)
16
+ - `country_nm`: 한글 국가명(예: `러시아`)
17
+ - `page`: 기본 `1`
18
+ - `perPage`: 기본 `10`, 최대 `100`
19
+
20
+ 국가명과 ISO 코드는 동시에 주지 않는다.
21
+
22
+ ## Workflow
23
+
24
+ ```bash
25
+ BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
26
+ curl -fsS --get "$BASE/v1/mofa-travel-safety/travel-alerts" \
27
+ --data-urlencode "country_iso_alp2=RU" \
28
+ --data-urlencode "perPage=10"
29
+ ```
30
+
31
+ 응답 `items`의 `country_nm`, `country_iso_alp2`, `alarm_lvl`,
32
+ `region_ty`, `remark`, `written_dt`, `dang_map_download_url`을 원문 기준으로
33
+ 요약한다. `alarm_lvl` 숫자의 의미는 답변에서 임의로 재분류하지 말고
34
+ 외교부 원문/0404 설명을 함께 제시한다.
35
+
36
+ ## Access path and credentials
37
+
38
+ 일반 사용자는 hosted `k-skill-proxy`를 사용하므로 data.go.kr 키가 필요
39
+ 없다. proxy 운영자는 승인된 `DATA_GO_KR_API_KEY`를 서버 runtime env에
40
+ 보관한다. 키는 URL, 로그, 저장소, 사용자 응답에 노출하지 않는다.
41
+
42
+ 공식 API:
43
+ <https://www.data.go.kr/data/15095500/openapi.do>
44
+
45
+ 실제 upstream:
46
+ `https://apis.data.go.kr/1262000/TravelAlarmService0404/getTravelAlarm0404List`
47
+
48
+ ## Fallback and failure modes
49
+
50
+ - 먼저 proxy route를 사용하고, 실패하면 공식 data.go.kr API 문서와 0404
51
+ 원문 링크만 안내한다. 임의의 안전 판정을 대신하지 않는다.
52
+ - `400 bad_request`: 잘못된 ISO 코드, 국가명, 페이지 값
53
+ - `503 upstream_not_configured`: proxy runtime에 data.go.kr 키 없음
54
+ - `502 upstream_error`: 외교부/data.go.kr 인증·quota·상류 장애
55
+ - `upstream_invalid_response`: JSON 대신 XML/HTML 오류 응답
56
+ - `items: []`: 해당 조건에 맞는 국가/지역 기록 없음
57
+
58
+ ## Done when
59
+
60
+ - 국가명 또는 ISO 코드와 조회 시각을 답변에 적었다.
61
+ - `alarm_lvl`, `region_ty`, `remark`를 공식 필드명과 함께 제시했다.
62
+ - 원문 API 문서와 0404 공식 링크를 함께 제공했다.
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python3
2
+ """Read MOFA 0404 travel alerts through the hosted proxy."""
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ import urllib.error
8
+ import urllib.parse
9
+ import urllib.request
10
+
11
+ DEFAULT_PROXY = "https://k-skill-proxy.nomadamas.org"
12
+
13
+
14
+ def main(argv=None):
15
+ parser = argparse.ArgumentParser(description="외교부 국가별 여행경보 조회")
16
+ parser.add_argument("--country-iso")
17
+ parser.add_argument("--country-name")
18
+ parser.add_argument("--page", type=int, default=1)
19
+ parser.add_argument("--per-page", type=int, default=10)
20
+ parser.add_argument("--text", action="store_true")
21
+ parser.add_argument("--dry-run", action="store_true")
22
+ parser.add_argument("--proxy-base-url", default=os.getenv("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY))
23
+ args = parser.parse_args(argv)
24
+ if args.country_iso and args.country_name:
25
+ print("[error] use either --country-iso or --country-name", file=sys.stderr)
26
+ return 2
27
+ if not 1 <= args.page or not 1 <= args.per_page <= 100:
28
+ print("[error] invalid page or per-page", file=sys.stderr)
29
+ return 2
30
+ params = {"page": args.page, "perPage": args.per_page}
31
+ if args.country_iso:
32
+ if len(args.country_iso) != 2 or not args.country_iso.isalpha():
33
+ print("[error] --country-iso must be two letters", file=sys.stderr)
34
+ return 2
35
+ params["country_iso_alp2"] = args.country_iso.upper()
36
+ if args.country_name:
37
+ params["country_nm"] = args.country_name
38
+ url = f"{args.proxy_base_url.rstrip('/')}/v1/mofa-travel-safety/travel-alerts?{urllib.parse.urlencode(params)}"
39
+ if args.dry_run:
40
+ print(json.dumps({"url": url, "query": params}, ensure_ascii=False, indent=2))
41
+ return 0
42
+ try:
43
+ with urllib.request.urlopen(urllib.request.Request(url, headers={"accept": "application/json"}), timeout=30) as response:
44
+ payload = json.loads(response.read())
45
+ except (OSError, ValueError, urllib.error.HTTPError) as error:
46
+ print(f"[error] MOFA request failed: {error}", file=sys.stderr)
47
+ return 3
48
+ if args.text:
49
+ for item in payload.get("items", []):
50
+ print(f"{item.get('country_nm')} ({item.get('country_iso_alp2')}): level={item.get('alarm_lvl')} region={item.get('region_ty')}")
51
+ else:
52
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
53
+ return 0
54
+
55
+
56
+ if __name__ == "__main__":
57
+ raise SystemExit(main())
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "mofa-travel-safety",
3
+ "description": "외교부 0404 국가·지역별 여행경보 Open API를 k-skill-proxy 경유로 조회해 국가별 공식 경보 단계와 원문 정보를 제공한다. 조회 전용.",
4
+ "profiles": [
5
+ "proxy",
6
+ "lookup"
7
+ ],
8
+ "frontmatter": "name: mofa-travel-safety\ndescription: 외교부 0404 국가·지역별 여행경보 Open API를 k-skill-proxy 경유로 조회해 국가별 공식 경보 단계와 원문 정보를 제공한다. 조회 전용.\nlicense: MIT\nmetadata:\n category: travel\n locale: ko-KR\n phase: v1"
9
+ }
@@ -28,9 +28,26 @@
28
28
 
29
29
  ## Prerequisites
30
30
 
31
- 없음. 사용자는 별도 API key를 준비할 필요가 없다. upstream key는 proxy 서버에서만 주입한다.
31
+ 없음. 사용자는 별도 API key를 준비할 필요가 없다. 개별 조회의 upstream key는 proxy 서버에서만 주입하며, 전국 일일 보고는 아래 공식 RTMS CSV를 직접 사용한다.
32
32
 
33
- ## Default path
33
+ ## 전국 일일 보고 — 공식 RTMS CSV 직접 조회
34
+
35
+ proxy나 ServiceKey 없이 국토교통부 실거래가 자료제공 화면의 실제 CSV 응답을 집계한다.
36
+
37
+ ```bash
38
+ npx -y @nomadamas/k-skill@0 exec real-estate-search scripts/nationwide_daily_report.py -- --as-of YYYY-MM-DD
39
+ ```
40
+
41
+ `--as-of`를 생략하면 KST 오늘을 사용한다. 공식 시·도/시·군·구 목록을 조회한 뒤 아파트·오피스텔·연립다세대·단독다가구의 매매·전월세 8개 조합을 전국 일괄 CSV로 실행한다. 현재 월이 0건이면 직전 월을 한 번 확인한다.
42
+
43
+ - `success`: CSV를 내려받아 파싱한 조합. 지역별 0건은 `empty`로 집계한다.
44
+ - `empty`: 현재 월과 직전 월의 공식 건수가 모두 0인 조합.
45
+ - `failure`: 네트워크, HTTP, 응답 크기, JSON/CSV 계약 또는 건수 불일치 오류.
46
+ - `unexecuted`: 실행 결과가 없는 조합. 실패나 빈 결과를 이 상태로 바꾸지 않는다.
47
+
48
+ 한 조합이라도 실패하면 보고서는 성공/빈 결과/실패 범위를 모두 출력하고 종료코드 1을 반환한다. 원문 근거는 실제로 접속한 `https://rt.molit.go.kr/pt/xls/xls.do`로 고정한다.
49
+
50
+ ## 개별 조회 기본 경로
34
51
 
35
52
  추가 client API 레이어는 불필요하다. 그냥 프록시 서버에 HTTP 요청만 넣으면 된다.
36
53
 
@@ -156,17 +173,20 @@ curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/real-estate/officetel/re
156
173
  - 프록시 서버에 `DATA_GO_KR_API_KEY` 가 없으면 503 응답
157
174
  - upstream MOLIT API 오류면 502 + `molit_api_XXX` 에러 코드
158
175
  - 해당 지역/기간에 데이터가 없으면 빈 `items` 배열 반환
176
+ - 전국 일일 보고의 공식 RTMS 응답이 실패하거나 계약과 다르면 해당 조합을 `failure`로 유지하고 누락 수치나 더미 데이터를 만들지 않는다.
159
177
 
160
178
  ## Done when
161
179
 
162
180
  - 요청 자산 타입에 맞는 endpoint를 선택했다.
163
181
  - 필요한 경우 `region-code` 로 지역코드를 먼저 확인했다.
164
182
  - 실거래가/전월세 결과를 조회하고 요약했다.
183
+ - 전국 일일 보고 요청이면 공식 RTMS CSV helper를 실행하고 success/empty/failure/unexecuted를 구분했다.
165
184
  - 원본 데이터 출처(국토교통부 실거래가 신고)를 함께 남겼다.
166
185
 
167
186
  ## Notes
168
187
 
169
188
  - 원본 참고: `https://github.com/tae0y/real-estate-mcp/tree/main`
170
189
  - 공식 데이터 출처: 공공데이터포털 (`https://www.data.go.kr`)
190
+ - 공식 RTMS CSV 자료제공: `https://rt.molit.go.kr/pt/xls/xls.do`
171
191
  - 가격 단위: `price_10k`, `deposit_10k` = 만원 단위 (예: 245000 = 24억 5천만원)
172
192
  - 취소된 거래는 서버에서 자동 필터링된다.