@nomadamas/k-skill 0.6.0 → 0.8.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 (36) hide show
  1. package/package.json +1 -1
  2. package/skills/bccard-eatpl-search/instruction.md +124 -0
  3. package/skills/bccard-eatpl-search/references/DISCLAIMER.md +11 -0
  4. package/skills/bccard-eatpl-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
  5. package/skills/bccard-eatpl-search/skill.json +6 -0
  6. package/skills/komsa-ferry-info/instruction.md +54 -0
  7. package/skills/komsa-ferry-info/scripts/komsa_ferry_info.py +76 -0
  8. package/skills/komsa-ferry-info/skill.json +6 -0
  9. package/skills/korean-privacy-terms/instruction.md +1 -0
  10. package/skills/korean-privacy-terms/scripts/install.sh +17 -0
  11. package/skills/railway-timetable/instruction.md +40 -0
  12. package/skills/railway-timetable/references/AUTOMATION-LEGAL-STATEMENT.md +5 -0
  13. package/skills/railway-timetable/references/DISCLAIMER.md +7 -0
  14. package/skills/railway-timetable/references/TRADEMARK-LEGAL-STATEMENT.md +3 -0
  15. package/skills/{ktx-booking/scripts/ktx_booking.py → railway-timetable/scripts/ktx_backend.py} +0 -0
  16. package/skills/{ktx-booking → railway-timetable}/scripts/ktx_timetable.py +0 -2
  17. package/skills/railway-timetable/scripts/railway_timetable.py +95 -0
  18. package/skills/railway-timetable/scripts/railway_timetable.py.lock +27 -0
  19. package/skills/railway-timetable/skill.json +8 -0
  20. package/skills/religious-facility-search/instruction.md +195 -0
  21. package/skills/religious-facility-search/scripts/religious_facility_search.py +277 -0
  22. package/skills/religious-facility-search/skill.json +9 -0
  23. package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
  24. package/skills/ktx-booking/instruction.md +0 -61
  25. package/skills/ktx-booking/references/AUTOMATION-LEGAL-STATEMENT.md +0 -10
  26. package/skills/ktx-booking/references/DISCLAIMER.md +0 -14
  27. package/skills/ktx-booking/references/TRADEMARK-LEGAL-STATEMENT.md +0 -9
  28. package/skills/ktx-booking/skill.json +0 -8
  29. package/skills/srt-booking/instruction.md +0 -75
  30. package/skills/srt-booking/references/AUTOMATION-LEGAL-STATEMENT.md +0 -25
  31. package/skills/srt-booking/references/DISCLAIMER.md +0 -13
  32. package/skills/srt-booking/references/TRADEMARK-LEGAL-STATEMENT.md +0 -9
  33. package/skills/srt-booking/scripts/srt_booking.py +0 -171
  34. package/skills/srt-booking/scripts/srt_booking.py.lock +0 -207
  35. package/skills/srt-booking/skill.json +0 -8
  36. /package/skills/{ktx-booking/scripts/ktx_booking.py.lock → railway-timetable/scripts/ktx_backend.py.lock} +0 -0
@@ -0,0 +1,195 @@
1
+ # 종교시설 찾기 / Religious Facility Search
2
+
3
+ ## What this skill does
4
+
5
+ 동네·역명·랜드마크 텍스트를 기준점으로 잡고 주변 **종교시설을 거리순으로 조회**한다.
6
+ 교회, 성당, 사찰을 같은 경로로 다루며 기본 대상은 교회다.
7
+
8
+ - 조회 전용이다. 등록, 연락, 방문 예약 같은 액션은 하지 않는다.
9
+ - 위치는 사용자가 제공한 텍스트만 쓴다. 자동 위치 추적을 하지 않는다.
10
+ - **공식 Kakao Local REST API**를 `k-skill-proxy` 경유로 호출한다. 사용자 머신에는
11
+ API 키가 필요 없고, 프록시 운영자의 `KAKAO_REST_API_KEY`로 인증한다.
12
+
13
+ ## When to use
14
+
15
+ - "근처 교회 찾아줘"
16
+ - "강남역 근처 교회 알려줘"
17
+ - "명동 성당 위치가 어디야"
18
+ - "경주에 절 어디 있어?"
19
+ - "이 근처 종교시설 알려줘"
20
+ - "온누리교회 주소 알려줘"
21
+
22
+ ## Inputs
23
+
24
+ - `location`: 동네·역명·랜드마크. 예: `강남역`, `성수동`, `경주`
25
+ - `--name`: 시설 이름으로 직접 검색. 예: `--name 온누리교회`
26
+ - `--type`: `교회`(기본) / `성당` / `절` / `사찰` / `전체`
27
+ - `--radius`: 기준점으로부터 최대 거리(m, 최대 20000). Kakao Local API의 상한이다
28
+ - `--limit`: 표시 개수 (기본 5, 최대 45). 페이지당 15건씩 최대 3페이지를 가져온다
29
+ - `--json`: JSON 출력
30
+
31
+ 사용자가 "성당", "절", "사찰"을 명시하면 `--type`을 그에 맞게 지정한다. 종류를 특정하지
32
+ 않고 "종교시설"이라고만 하면 `--type 전체`를 쓴다.
33
+
34
+ 위치도 이름도 없으면 **먼저 사용자에게 위치를 묻는다.** 비대화형 자동화에서는
35
+ 임의로 좁히지 말고 "입력 없음"을 명시한다.
36
+
37
+ 권장 질문:
38
+
39
+ ```text
40
+ 어느 동네나 역 근처에서 찾을까요? (예: 강남역, 성수동)
41
+ ```
42
+
43
+ ## Public access path discovered
44
+
45
+ ### 선택한 공개 경로
46
+
47
+ 공식 Kakao Local REST API의 keyword 검색 하나만 쓴다. `k-skill-proxy`가 운영자 키로
48
+ 인증을 대행하므로 사용자 머신에는 키가 필요 없다.
49
+
50
+ 1. `GET {proxy}/v1/kakao-map/search/keyword?q=<위치>`
51
+ → 첫 번째 결과를 기준점(anchor) 좌표로 쓴다.
52
+ 2. `GET {proxy}/v1/kakao-map/search/keyword?q=<종류>&x=<lon>&y=<lat>&radius=<m>&sort=distance&size=15&page=1..3`
53
+ → 거리순 장소 목록. 거리는 upstream이 주는 공식 `distance` 필드를 그대로 쓴다.
54
+
55
+ `{proxy}`는 기본 `https://k-skill-proxy.nomadamas.org`이며 `KSKILL_PROXY_BASE_URL`로
56
+ 바꿀 수 있다. 같은 라우트를 `kakao-map` 스킬이 이미 쓰고 있다.
57
+
58
+ ### 왜 이 경로인가
59
+
60
+ - **공식 문서화 API**: Kakao Developers가 스키마를 보장한다. 이전의 모바일 웹 표면
61
+ (`m.map.kakao.com`) 스크래핑은 문서가 없어 구조가 바뀌면 깨지는 상수 리스크가 있었다.
62
+ - **proxy 편입 규칙 적합**: upstream이 API 키를 요구하므로 `AGENTS.md` 규칙상
63
+ `k-skill-proxy`를 거치는 것이 맞다. 프록시에 캐시·rate-limit이 있어 쿼터도 보호된다.
64
+ - **거리 계산 불필요**: `sort=distance` + `distance` 필드가 공식 제공되어 haversine을
65
+ 직접 돌릴 필요가 없다.
66
+ - **전국 커버리지**: 지자체 공공데이터(예: 서울 종로구 교회 현황
67
+ `https://www.data.go.kr/data/15117640/fileData.do`)는 구 단위로만 공개되어 전국
68
+ 표준데이터가 없다. 주 경로로 쓸 수 없어 참고 출처로만 둔다.
69
+ - **분류가 데이터에 있음**: `category_name`에 종교 분류가 그대로 들어온다.
70
+
71
+ ```
72
+ 교회 → 문화,예술 > 종교 > 기독교 > 교회
73
+ 성당 → 문화,예술 > 종교 > 천주교 > 성당
74
+ 사찰 → 문화,예술 > 종교 > 불교 > 절,사찰
75
+ 기타 → 문화,예술 > 종교 > 종교시설 / 문화,예술 > 종교 > 통일교 …
76
+ ```
77
+
78
+ `category_name`에 `종교` 노드가 있는 결과만 통과시키므로 `써브웨이 명동성당점` 같은
79
+ 이름만 비슷한 음식점·서점이 결과에 섞이지 않는다. `--type`은 그 안에서 마지막
80
+ 두 분류 토큰으로 한 번 더 좁힌다.
81
+
82
+ ### fallback 순서
83
+
84
+ 1. 기준점 해석 실패 → 거리 없이 결과 목록만 제공한다 (stderr에 사유 표시).
85
+ 2. 지정한 종류가 0건 → `--type 전체`로 넓히거나 `--radius`를 걸었다면 풀고 재시도한다.
86
+ 3. 검색 결과 0건 → 상위 행정구역으로 넓혀 재시도한다.
87
+ 4. 프록시 장애가 계속되면 결과를 지어내지 말고 실패를 그대로 알린다.
88
+
89
+ ## Workflow
90
+
91
+ 1. 사용자에게 위치와 시설 종류를 확인한다. 위치가 없으면 먼저 묻는다.
92
+ 2. helper를 실행한다.
93
+
94
+ ```bash
95
+ npx -y @nomadamas/k-skill@0 exec religious-facility-search scripts/religious_facility_search.py -- "강남역" --limit 5
96
+ ```
97
+
98
+ 성당:
99
+
100
+ ```bash
101
+ npx -y @nomadamas/k-skill@0 exec religious-facility-search scripts/religious_facility_search.py -- "명동" --type 성당 --limit 5
102
+ ```
103
+
104
+ 사찰:
105
+
106
+ ```bash
107
+ npx -y @nomadamas/k-skill@0 exec religious-facility-search scripts/religious_facility_search.py -- "경주" --type 절 --limit 5
108
+ ```
109
+
110
+ 종류를 가리지 않을 때:
111
+
112
+ ```bash
113
+ npx -y @nomadamas/k-skill@0 exec religious-facility-search scripts/religious_facility_search.py -- "성수동" --type 전체 --radius 600
114
+ ```
115
+
116
+ 이름으로 검색:
117
+
118
+ ```bash
119
+ npx -y @nomadamas/k-skill@0 exec religious-facility-search scripts/religious_facility_search.py -- --name "온누리교회"
120
+ ```
121
+
122
+ 3. 결과를 3~5개로 요약한다. 각 항목에 이름, 주소, 거리, 연락처를 붙인다.
123
+ 4. 예배·미사·법회 시간을 물으면 **추정하지 말고** 카카오맵 장소 페이지나 전화로 안내한다.
124
+
125
+ ## Output
126
+
127
+ 기준 위치와 검색어를 먼저 보여주고 거리순 목록을 출력한다.
128
+
129
+ ```
130
+ 기준 위치: 강남역 2호선 (37.49809, 127.02800)
131
+ 검색어: "교회" · 후보 43건 중 종교시설 40건 (가까운 5건 표시)
132
+
133
+ 1. 강남성서침례교회 (교회 · 기독교)
134
+ 서울 서초구 강남대로55길 9-11
135
+ 약 313m
136
+ 전화 02-3474-3609
137
+ 지도 https://place.map.kakao.com/85095074
138
+ ```
139
+
140
+ `--json`은 다음 구조로 출력한다.
141
+
142
+ ```json
143
+ {
144
+ "query": "교회",
145
+ "anchor": { "name": "강남역 2호선", "lat": 37.49809, "lon": 127.02800 },
146
+ "scanned": 43,
147
+ "matched": 40,
148
+ "items": [
149
+ {
150
+ "id": "85095074",
151
+ "name": "강남성서침례교회",
152
+ "category": "교회",
153
+ "denomination": "기독교",
154
+ "category_group": "종교",
155
+ "lat": 37.4964,
156
+ "lon": 127.0380,
157
+ "address": "서울 서초구 강남대로55길 9-11",
158
+ "phone": "02-3474-3609",
159
+ "homepage": null,
160
+ "place_url": "https://place.map.kakao.com/85095074",
161
+ "distance_m": 313
162
+ }
163
+ ]
164
+ }
165
+ ```
166
+
167
+ `homepage`는 Kakao Local API가 외부 홈페이지를 제공하지 않아 항상 `null`이다.
168
+ 대신 `place_url`(카카오맵 장소 페이지)로 안내한다.
169
+
170
+ ## Done when
171
+
172
+ - 기준 위치와 시설 종류를 확정하고 사용자에게 알렸다.
173
+ - `category_name`의 `종교` 노드로 검증된 후보를 거리순 3~5개로 정리했다.
174
+ - 각 후보에 주소와, 있으면 전화번호·카카오맵 링크를 붙였다.
175
+ - 예배·미사·법회 시간을 임의로 지어내지 않고 공식 출처로 넘겼다.
176
+
177
+ ## Failure modes
178
+
179
+ - **검색 결과 없음**: 검색어가 좁거나 오탈자다. 상위 행정구역으로 넓혀 재시도한다.
180
+ - **지정 종류 0건**: 후보는 잡혔지만 해당 종류가 없다. `--type 전체`로 넓히거나, `--radius`를 걸었다면 풀고 확인한다. `--radius`는 결과를 좁히는 옵션이라 늘려도 새 후보를 가져오지 않는다.
181
+ - **프록시 503 `upstream_not_configured`**: 프록시에 `KAKAO_REST_API_KEY`가 없다. 운영자에게 키 설정을 요청한다.
182
+ - **프록시 401/403 → 503 변환**: Kakao 키 revoke나 일일 쿼터 초과 신호다. 잠시 후 재시도하고 계속되면 운영자에게 알린다.
183
+ - **프록시 5xx / 타임아웃**: 프록시 또는 upstream 일시 장애. 재시도하고 계속 실패하면 그대로 보고한다.
184
+ - **좌표 입력**: v1은 `위도,경도` 입력을 지원하지 않는다. helper가 명시적으로 거부하고
185
+ 동네·역명을 요구한다.
186
+ - **시·군 단위 입력**: 기준점이 그 안의 특정 장소로 잡혀 절대 거리가 대략치가 된다.
187
+ 동·역 단위에서 가장 정확하다.
188
+ - **예배·미사·법회 시간**: 이 데이터에 없다. 제공하지 않는다.
189
+
190
+ ## Notes
191
+
192
+ - 공개 정보 조회 전용이다. 종교나 교단의 우열을 평가하거나 이단 여부를 판별하지 않는다.
193
+ - 특정 시설이나 교단을 권유하지 않는다. 거리와 공개 정보만 근거로 나열한다.
194
+ - 예배·미사·법회 시간, 성직자, 교단 소속은 이 데이터에 없다. 추정하지 않고 공식 출처로 넘긴다.
195
+ - 사용자가 제공한 위치 텍스트만 쓴다. 위치를 자동으로 알아내려 하지 않는다.
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env python3
2
+ """종교시설 찾기 — 공식 Kakao Local API (k-skill-proxy 경유).
3
+
4
+ 공개 접근 경로:
5
+ 1) GET {proxy}/v1/kakao-map/search/keyword?q=<위치> → 기준점 좌표 (anchor)
6
+ 2) GET {proxy}/v1/kakao-map/search/keyword?q=<종류>&x=..&y=..&radius=..&sort=distance
7
+ → 거리순 장소 목록 (공식 distance 필드 사용)
8
+
9
+ upstream이 Kakao Developers REST API 키를 요구하므로 k-skill-proxy를 거친다
10
+ (사용자 머신에는 키가 필요 없고, 프록시 운영자 키로 인증한다).
11
+ category_name에 "종교" 노드가 있는 결과만 통과시키므로
12
+ 이름에 "교회"가 들어간 카페·서점 같은 오탐이 걸러진다.
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import re
19
+ import sys
20
+ import urllib.error
21
+ import urllib.parse
22
+ import urllib.request
23
+
24
+ DEFAULT_PROXY_BASE = "https://k-skill-proxy.nomadamas.org"
25
+ SEARCH_KEYWORD_PATH = "/v1/kakao-map/search/keyword"
26
+
27
+ USER_AGENT = "k-skill-religious-facility-search/1"
28
+
29
+ RELIGION_CATEGORY = "종교"
30
+ TYPE_ALIASES = {
31
+ "교회": ["교회"],
32
+ "성당": ["성당"],
33
+ "절": ["절", "사찰"],
34
+ "사찰": ["절", "사찰"],
35
+ "전체": [],
36
+ }
37
+ # --type 전체: "종교시설" 단일 키워드는 교회·사찰을 놓치므로 종류별로 합친다
38
+ ALL_TYPE_QUERIES = ["교회", "성당", "사찰"]
39
+ COORD_RE = re.compile(r"^\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*$")
40
+ PAGE_SIZE = 15 # Kakao Local keyword 검색의 페이지당 최대 건수
41
+ MAX_PAGES = 3 # 페이지네이션 상한 → 최대 45개 후보
42
+ MAX_CANDIDATES = PAGE_SIZE * MAX_PAGES
43
+
44
+
45
+ class LookupError_(Exception):
46
+ """조회 실패를 명시적 실패 모드로 올린다."""
47
+
48
+
49
+ def proxy_base():
50
+ return (os.environ.get("KSKILL_PROXY_BASE_URL") or DEFAULT_PROXY_BASE).rstrip("/")
51
+
52
+
53
+ def fetch_json(url, timeout=15):
54
+ request = urllib.request.Request(url, headers={"user-agent": USER_AGENT, "accept": "application/json"})
55
+ try:
56
+ with urllib.request.urlopen(request, timeout=timeout) as response:
57
+ raw = response.read().decode("utf-8", errors="replace")
58
+ except urllib.error.HTTPError as error:
59
+ detail = ""
60
+ try:
61
+ detail = error.read().decode("utf-8", errors="replace")[:200]
62
+ except Exception:
63
+ pass
64
+ raise LookupError_(f"프록시 HTTP {error.code} ({detail or '응답 본문 없음'})") from error
65
+ except urllib.error.URLError as error:
66
+ raise LookupError_(f"네트워크 오류: {error.reason}") from error
67
+
68
+ try:
69
+ return json.loads(raw)
70
+ except json.JSONDecodeError as error:
71
+ raise LookupError_("프록시 응답이 JSON이 아니다 (proxy 변경 가능)") from error
72
+
73
+
74
+ def search_places(query, x=None, y=None, radius=None, max_pages=MAX_PAGES):
75
+ """proxy keyword 검색. (documents, meta)를 돌려준다. 첫 페이지가 비면 명시적 실패."""
76
+ params = {"q": query, "size": PAGE_SIZE}
77
+ if x is not None and y is not None:
78
+ params.update({"x": x, "y": y, "sort": "distance"})
79
+ if radius:
80
+ params["radius"] = radius
81
+
82
+ documents, meta = [], {}
83
+ for page in range(1, max_pages + 1):
84
+ params["page"] = page
85
+ url = f"{proxy_base()}{SEARCH_KEYWORD_PATH}?{urllib.parse.urlencode(params)}"
86
+ payload = fetch_json(url)
87
+ batch = payload.get("documents") or []
88
+ meta = payload.get("meta") or {}
89
+ documents.extend(batch)
90
+ if page == 1 and not documents:
91
+ raise LookupError_(f'"{query}" 검색 결과가 비었다 (검색어를 넓히거나 지역명을 붙여볼 것)')
92
+ if meta.get("is_end", True) or not batch:
93
+ break
94
+ return documents, meta
95
+
96
+
97
+ def map_place(document):
98
+ parts = [p.strip() for p in (document.get("category_name") or "").split(">") if p.strip()]
99
+ distance_raw = (document.get("distance") or "").strip()
100
+ return {
101
+ "id": document.get("id"),
102
+ "name": document.get("place_name"),
103
+ "category": parts[-1] if parts else None,
104
+ "denomination": parts[-2] if len(parts) >= 4 else None,
105
+ "category_group": RELIGION_CATEGORY if RELIGION_CATEGORY in parts else None,
106
+ "lat": float(document["y"]) if document.get("y") else None,
107
+ "lon": float(document["x"]) if document.get("x") else None,
108
+ "address": (document.get("road_address_name") or document.get("address_name") or "").strip() or None,
109
+ "jibun": (document.get("address_name") or "").strip() or None,
110
+ "phone": (document.get("phone") or "").strip() or None,
111
+ "homepage": None, # Kakao Local API는 외부 홈페이지를 제공하지 않는다
112
+ "place_url": document.get("place_url"),
113
+ "distance_m": int(distance_raw) if distance_raw else None,
114
+ }
115
+
116
+
117
+ def resolve_anchor(location):
118
+ """위치 문자열을 좌표로 바꾼다. 실패하면 None (거리 없이 결과만 제공)."""
119
+ try:
120
+ documents, _ = search_places(location)
121
+ except LookupError_:
122
+ return None
123
+ if not documents:
124
+ return None
125
+ top = documents[0]
126
+ return {"name": top.get("place_name"), "lat": float(top["y"]), "lon": float(top["x"])}
127
+
128
+
129
+ def _category_tokens(place):
130
+ tokens = []
131
+ for part in (place.get("category"), place.get("denomination")):
132
+ if not part:
133
+ continue
134
+ tokens.extend(token.strip() for token in part.replace(",", " ").split() if token.strip())
135
+ return tokens
136
+
137
+
138
+ def matches_type(place, facility_type):
139
+ if place.get("category_group") != RELIGION_CATEGORY:
140
+ return False
141
+ wanted = TYPE_ALIASES.get(facility_type, [facility_type])
142
+ if not wanted:
143
+ return True
144
+ tokens = _category_tokens(place)
145
+ return any(token in tokens for token in wanted)
146
+
147
+
148
+ def _dedupe_by_id(places):
149
+ seen, unique = set(), []
150
+ for place in places:
151
+ if place["id"] not in seen:
152
+ seen.add(place["id"])
153
+ unique.append(place)
154
+ return unique
155
+
156
+
157
+ def collect(query, facility_type, anchor, radius_m, limit):
158
+ x = y = None
159
+ if anchor:
160
+ x, y = anchor["lon"], anchor["lat"]
161
+
162
+ if facility_type == "전체" and query == "전체":
163
+ # "종교시설" 단일 검색은 교회·사찰을 놓치므로 종류별 검색을 합친다
164
+ documents, scanned, hits = [], 0, 0
165
+ for token in ALL_TYPE_QUERIES:
166
+ try:
167
+ docs, _ = search_places(token, x=x, y=y, radius=radius_m)
168
+ except LookupError_:
169
+ continue # 한 종류가 0건이어도 나머지 종류는 돌려준다
170
+ hits += 1
171
+ scanned += len(docs)
172
+ documents.extend(docs)
173
+ if hits == 0:
174
+ raise LookupError_('"전체" 검색 결과가 비었다 (지역을 넓히거나 --type을 좁혀볼 것)')
175
+ else:
176
+ documents, _ = search_places(query, x=x, y=y, radius=radius_m)
177
+ scanned = len(documents)
178
+
179
+ places = _dedupe_by_id(map_place(doc) for doc in documents)
180
+ places = [p for p in places if matches_type(p, facility_type)]
181
+
182
+ if anchor:
183
+ places.sort(key=lambda p: (p["distance_m"] is None, p["distance_m"] or 0))
184
+
185
+ return scanned, len(places), places[:limit]
186
+
187
+
188
+ def format_report(query, anchor, scanned, matched, places, facility_type):
189
+ lines = []
190
+ if anchor:
191
+ lines.append(f"기준 위치: {anchor['name']} ({anchor['lat']:.5f}, {anchor['lon']:.5f})")
192
+ summary = f'검색어: "{query}" · 후보 {scanned}건 중 종교시설 {matched}건'
193
+ if matched > len(places):
194
+ summary += f" (가까운 {len(places)}건 표시)"
195
+ lines.append(summary)
196
+ lines.append("")
197
+
198
+ if not places:
199
+ lines.append(f"조건에 맞는 {facility_type}을(를) 찾지 못했다. 지역을 넓혀 다시 찾거나 "
200
+ f"--type 전체 로 확인할 것 (--radius 는 결과를 좁히는 옵션이다).")
201
+ return "\n".join(lines)
202
+
203
+ for index, place in enumerate(places, start=1):
204
+ label = place["category"] or "종교시설"
205
+ if place.get("denomination"):
206
+ label = f"{label} · {place['denomination']}"
207
+ lines.append(f"{index}. {place['name']} ({label})")
208
+ if place.get("address"):
209
+ lines.append(f" {place['address']}")
210
+ if place.get("distance_m") is not None:
211
+ lines.append(f" 약 {place['distance_m']:,}m")
212
+ contact = []
213
+ if place.get("phone"):
214
+ contact.append(f"전화 {place['phone']}")
215
+ if place.get("homepage"):
216
+ contact.append(place["homepage"])
217
+ if contact:
218
+ lines.append(" " + " ".join(contact))
219
+ lines.append(f" 지도 {place['place_url']}")
220
+ lines.append("")
221
+
222
+ lines.append("예배·미사·법회 시간은 이 데이터에 없다. 각 홈페이지나 전화로 확인할 것.")
223
+ return "\n".join(lines).rstrip()
224
+
225
+
226
+ def main():
227
+ parser = argparse.ArgumentParser(description="종교시설 찾기 (공식 Kakao Local API, k-skill-proxy 경유)")
228
+ parser.add_argument("location", nargs="?", help="동네·역명·랜드마크 (예: 강남역, 성수동)")
229
+ parser.add_argument("--name", help="교회/시설 이름으로 직접 검색")
230
+ parser.add_argument("--type", default="교회", choices=sorted(TYPE_ALIASES), help="시설 종류 (기본: 교회)")
231
+ parser.add_argument("--radius", type=int, help="기준 위치로부터 최대 거리(m, 최대 20000)")
232
+ parser.add_argument("--limit", type=int, default=5, help=f"표시 개수 (기본 5, 최대 {MAX_CANDIDATES})")
233
+ parser.add_argument("--json", action="store_true", help="JSON으로 출력")
234
+ args = parser.parse_args()
235
+
236
+ if not args.location and not args.name:
237
+ parser.error("location 또는 --name 중 하나는 필요하다. 사용자에게 현재 위치를 먼저 물어볼 것.")
238
+
239
+ if not 1 <= args.limit <= MAX_CANDIDATES:
240
+ parser.error(f"--limit은 1~{MAX_CANDIDATES} 사이여야 한다. 페이지 {MAX_PAGES}장(페이지당 "
241
+ f"{PAGE_SIZE}건)까지만 가져오므로 그보다 많이 표시할 수 없다.")
242
+
243
+ if args.radius is not None and not 1 <= args.radius <= 20000:
244
+ parser.error("--radius는 1~20000(m) 사이여야 한다. Kakao Local API의 상한이다.")
245
+
246
+ if args.location and COORD_RE.match(args.location):
247
+ parser.error("좌표 검색은 지원하지 않는다. 동네·역명·랜드마크로 입력할 것 (예: 강남역, 성수동).")
248
+
249
+ if args.name:
250
+ # 위치는 좌표(x,y)로 전달하므로 검색어는 시설 이름만 쓴다
251
+ # ("강남역 온누리교회"처럼 붙이면 keyword API가 0건을 돌려준다)
252
+ query = args.name
253
+ elif args.type == "전체":
254
+ query = args.type # collect()가 종류별 검색으로 풀어서 합친다
255
+ else:
256
+ query = args.type
257
+
258
+ try:
259
+ anchor = resolve_anchor(args.location) if args.location else None
260
+ if args.location and anchor is None:
261
+ print(f"기준 위치 \"{args.location}\"을(를) 찾지 못해 거리 없이 조회한다.", file=sys.stderr)
262
+ scanned, matched, places = collect(query, args.type, anchor, args.radius, args.limit)
263
+ except LookupError_ as error:
264
+ print(f"조회 실패: {error}", file=sys.stderr)
265
+ return 1
266
+
267
+ if args.json:
268
+ print(json.dumps({"query": query, "anchor": anchor, "scanned": scanned,
269
+ "matched": matched, "items": places},
270
+ ensure_ascii=False, indent=2))
271
+ else:
272
+ print(format_report(query, anchor, scanned, matched, places, args.type))
273
+ return 0
274
+
275
+
276
+ if __name__ == "__main__":
277
+ sys.exit(main())
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "religious-facility-search",
3
+ "description": "Use when the user asks 근처 교회 찾기, 성당 위치, 절·사찰 찾기, 종교시설 조회, or church/cathedral/temple lookup by Korean neighborhood, station, or landmark. 공식 Kakao Local API를 k-skill-proxy 경유로 호출해 종교시설을 거리순으로 조회하고 주소·전화·카카오맵 링크를 정리한다. 사용자 API 키가 필요 없는 조회 전용 스킬이다.",
4
+ "profiles": [
5
+ "proxy",
6
+ "lookup"
7
+ ],
8
+ "frontmatter": "name: religious-facility-search\ndescription: Use when the user asks 근처 교회 찾기, 성당 위치, 절·사찰 찾기, 종교시설 조회, or church/cathedral/temple lookup by Korean neighborhood, station, or landmark. 공식 Kakao Local API를 k-skill-proxy 경유로 호출해 종교시설을 거리순으로 조회하고 주소·전화·카카오맵 링크를 정리한다. 사용자 API 키가 필요 없는 조회 전용 스킬이다.\nlicense: MIT\nmetadata:\n category: religion\n locale: ko-KR\n phase: v1"
9
+ }
@@ -1,61 +0,0 @@
1
- # KTX Official Timetable Lookup
2
-
3
- ## What this skill does
4
-
5
- 한국철도공사가 로그인 없이 공개하는 최신 KTX 운행시간표 XLSX를 내려받아 출발역·도착역·출발시간 조건에 맞는 열차를 찾는다.
6
-
7
- 이 스킬은 **공식 계획 시간표 조회 전용**이다.
8
-
9
- - 회원 로그인, credential, Korail 앱 내부 API를 사용하지 않는다.
10
- - Dynapath 또는 anti-bot token을 생성·재현·우회하지 않는다.
11
- - 실시간 잔여석, 실제 운휴·지연, 호차·좌석번호는 조회하지 않는다.
12
- - 예약, 예약대기, 좌석 선점, 결제, 취소, 자동 재조회는 실행하지 않는다.
13
- - 구매와 실시간 운행 확인은 공식 코레일 페이지에서 사용자가 직접 진행한다.
14
-
15
- 실행 환경에는 Python 3.11 이상과 `uv`가 필요하다. `uv`가 helper에 고정된 `openpyxl` 환경을 자동 설치한다.
16
-
17
- ## Commands
18
-
19
- ```bash
20
- npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- \
21
- search --dep 서울 --arr 부산 --date 20260819 --time 0600 --time-limit 1200 --limit 5
22
- ```
23
-
24
- ```bash
25
- npx -y @nomadamas/k-skill@0 exec ktx-booking scripts/ktx_booking.py -- source
26
- ```
27
-
28
- ## Inputs
29
-
30
- - `--dep`: 출발역명. `서울`과 `서울역` 모두 허용한다.
31
- - `--arr`: 도착역명.
32
- - `--date`: `YYYYMMDD`.
33
- - `--time`: 가장 이른 출발시각 `HHMM`, 기본 `0000`.
34
- - `--time-limit`: 가장 늦은 출발시각 `HHMM`, 기본 `2359`.
35
- - `--limit`: 최대 결과 수, 1~50.
36
-
37
- ## Output
38
-
39
- - `count`
40
- - `trains[]`: `train_no`, `train_type`, `dep`, `arr`, `dep_time`, `arr_time`
41
- - `date`
42
- - `schedule_note`
43
- - `source`: 공식 게시물 제목·게시일·XLSX URL·게시판 URL
44
- - `booking_url`
45
-
46
- 항상 이 결과는 **공개 운행계획 시간표**이며 실시간 잔여석·운휴·지연 정보가 아니라는 점을 명시한다.
47
-
48
- ## Failure modes
49
-
50
- - 공식 시간표 게시판 또는 XLSX 다운로드 실패
51
- - 요청일에 적용되는 KTX 시간표 없음
52
- - XLSX 형식 변경으로 header를 찾지 못함
53
- - 요청 구간·시간대 결과 없음: 오류가 아니라 `count: 0`
54
- - 잘못된 날짜·시간·limit: 실행 전 오류
55
-
56
- ## Hard boundaries
57
-
58
- - 로그인·credential 요청 금지
59
- - `ScheduleView`, `korail2`, Dynapath 등 앱 내부 경로 사용 금지
60
- - 예약·결제·취소·좌석 선점 금지
61
- - CAPTCHA·인증·anti-bot 통제 우회 금지
@@ -1,10 +0,0 @@
1
- # KTX 공식 시간표 조회 관련 고지
2
-
3
- 이 스킬은 한국철도공사가 공개한 시간표 게시판과 XLSX 첨부파일만 읽는다.
4
-
5
- - 회원 로그인과 credential을 사용하지 않는다.
6
- - Korail 앱 내부 API, `ScheduleView`, Dynapath token을 호출·생성·재현하지 않는다.
7
- - CAPTCHA, anti-bot 또는 접근통제를 우회하지 않는다.
8
- - 실시간 잔여석, 예약, 좌석 선점, 결제, 취소를 실행하지 않는다.
9
-
10
- 결과는 공개 운행계획 시간표이며 실제 운휴·지연·잔여석 정보가 아니다. 실제 이용 전에는 공식 코레일 예매 페이지에서 다시 확인해야 한다.
@@ -1,14 +0,0 @@
1
- # DISCLAIMER — `ktx-booking`
2
-
3
- 이 스킬은 한국철도공사·코레일·KTX의 공식 기능 또는 공식 지원 도구가 아니며, 공식 제휴·후원·승인·인증 또는 협업한 사실이 전혀 없습니다. 상표와 서비스명은 열차 조회·호환 대상을 설명하기 위해서만 사용합니다.
4
-
5
- 대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)(소니용 리모컨 사건)은 기능 설명용 표장과 출처표시를 구별했습니다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)에 따른 출처 혼동 판단은 별도입니다.
6
-
7
- 대법원 2022. 5. 12. 선고 [2021도1533 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=221765)은 공개정보 수집만으로 곧바로 [정보통신망법 제48조](https://www.law.go.kr/법령/정보통신망이용촉진및정보보호등에관한법률/제48조) 위반이 되는 것은 아닌 사정을 제시했지만, 철도 예매 자동화·접근통제 우회·반복 선점을 허용하지 않습니다. [저작권법 제93조](https://www.law.go.kr/법령/저작권법/제93조)와 [형법 제314조 제2항](https://www.law.go.kr/법령/형법/제314조)도 별도입니다.
8
-
9
- - 공개 운행정보 자동 수집은 반드시 개인의 정보 조회용으로만 사용합니다.
10
- - 매진 감시, 조직적·대량 크롤링, 반복 선점·예약·취소, 공격적 polling을 하지 않습니다.
11
- - CAPTCHA·anti-bot·로그인·접근통제·rate limit·IP 차단을 우회하거나 차단 회피를 하지 않습니다.
12
- - `MACRO ERROR`나 접근 거부가 나타나면 즉시 중단하며 정상 예매 영업을 방해하지 않습니다.
13
-
14
- 이 문서는 적법성을 보증하는 법률 자문이 아닙니다.
@@ -1,9 +0,0 @@
1
- # 상표 사용 법적 고지 — `ktx-booking`
2
-
3
- 이 스킬에서 `KTX`, `코레일`, `Korail` 명칭은 열차 조회·예약·취소의 **대상 철도 서비스와 운영기관**을 식별하기 위해 사용한다. k-skill의 출처나 운영주체를 코레일로 표시하려는 사용이 아니다.
4
-
5
- 대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)은 타인의 표장을 출처표시가 아니라 상품 기능 또는 적용 기종을 밝히기 위해 사용하고 상표 사용으로 인식될 수 없는 경우 침해가 아니라고 판시했다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)와 대법원 [2011다18802](https://www.law.go.kr/LSW/precInfoP.do?precSeq=167457), [2019후10418](https://law.go.kr/LSW/precInfoP.do?mode=0&precSeq=230725) 판결에 따라 실제 거래계에서 출처표시로 기능하는지는 표시 태양과 사용 경위 등을 종합해 판단해야 한다.
6
-
7
- 따라서 이 명칭은 필요한 범위의 평문으로만 사용하고, 별도 근거 없이 로고, 공식·제휴·후원·인증·파트너 표현을 사용하지 않는다. 각 상표의 권리는 해당 권리자에게 있다. 이 문서는 운송계약, API·서비스 약관, 개인정보 또는 예약·결제 흐름의 적법성을 판단하지 않는다.
8
-
9
- 전체 검토: [제3자 상표의 기능 설명·호환 대상 표시 검토](https://github.com/NomaDamas/k-skill/blob/dev/docs/legal/trademark-use-review.md)
@@ -1,8 +0,0 @@
1
- {
2
- "name": "ktx-booking",
3
- "description": "Read-only KTX operating-timetable lookup from official Korail public XLSX attachments. No login, internal mobile API, anti-bot bypass, live seat availability, reservation, or payment.",
4
- "profiles": [
5
- "lookup"
6
- ],
7
- "frontmatter": "name: ktx-booking\ndescription: Read-only KTX operating-timetable lookup from official Korail public XLSX attachments. No login, internal mobile API, anti-bot bypass, live seat availability, reservation, or payment.\nlicense: MIT\nmetadata:\n category: travel\n locale: ko-KR\n phase: v1"
8
- }
@@ -1,75 +0,0 @@
1
- # SRT Live Timetable Lookup
2
-
3
- ## What this skill does
4
-
5
- `SRTrain`의 시간표 검색 경로를 이용해 현재 SRT 운행 후보와 일반실·특실 예약 가능 여부를 조회한다.
6
-
7
- 이 스킬은 회원 로그인 불필요한 **라이브 조회 전용**이다.
8
-
9
- - `auto_login=False` 익명 client를 사용한다.
10
- - NetFunnel 대기열과 시간표 검색 요청만 실행한다.
11
- - 예약, 예약대기, 좌석 선점, 결제, 취소, 자동 재조회는 실행하지 않는다.
12
- - 정확한 호차·좌석번호를 조회하지 않는다.
13
- - 구매는 공식 SRT 페이지에서 사용자가 직접 진행한다.
14
-
15
- 실행 환경에는 Python 3.11 이상과 `uv`가 필요하다. `uv`가 없으면 공식 설치 문서를 안내하고 조회를 실행하지 않는다.
16
-
17
- ## Commands
18
-
19
- ```bash
20
- npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- \
21
- search \
22
- --dep 수서 \
23
- --arr 부산 \
24
- --date 20260819 \
25
- --time 0600 \
26
- --time-limit 1200 \
27
- --limit 5
28
- ```
29
-
30
- 현재 라이브 조회 endpoint와 안전 경계:
31
-
32
- ```bash
33
- npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- source
34
- ```
35
-
36
- 출력:
37
-
38
- - 열차번호·열차종류
39
- - 출발역·도착역
40
- - 현재 출발·도착 시각
41
- - 일반실·특실 예약 가능 여부
42
- - 사용한 라이브 search endpoint
43
- - 공식 SRT 페이지
44
-
45
- ## Workflow
46
-
47
- 1. 출발역, 도착역, 날짜, 시간대를 확인한다.
48
- 2. `search`를 한 번 실행한다. `수서역`처럼 `역`이 붙은 입력은 helper가 표준 역명으로 정규화한다.
49
- 3. 현재 운행 후보를 제시한다.
50
- 4. 좌석 구매가 필요하면 `booking_url`을 제공하고 종료한다.
51
- 5. 사용자가 다시 요청하지 않는 한 polling·매진 감시를 시작하지 않는다.
52
-
53
- ## Hard boundaries
54
-
55
- - `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD` 또는 회원 로그인을 요구하지 않는다.
56
- - helper에 `reserve`, `reservations`, `cancel`, `payment`, waiting-list 명령을 추가하지 않는다.
57
- - 예약 endpoint, 결제 endpoint 또는 계정 endpoint를 호출하지 않는다.
58
- - NetFunnel key를 예약·선점 자동화에 사용하지 않는다.
59
- - CAPTCHA·접근 거부·계정 제한이 발생하면 즉시 중단한다.
60
- - 사용자 요청 한 번을 반복 수집이나 장기 실행으로 확장하지 않는다.
61
-
62
- ## Failure modes
63
-
64
- - NetFunnel 대기 또는 SRT 접근 제한
65
- - `SRTrain` 설치·호환성 문제
66
- - 날짜·시간 또는 역명 오류
67
- - 조건에 맞는 열차 없음
68
-
69
- 이 경우 차단을 우회하지 않고 [SRT 공식 조회 페이지](https://etk.srail.kr/hpg/hra/01/selectScheduleList.do?pageId=TK0101010000)를 안내한다.
70
-
71
- ## Legal notice
72
-
73
- ```bash
74
- npx -y @nomadamas/k-skill@0 read srt-booking references/AUTOMATION-LEGAL-STATEMENT.md
75
- ```