@nomadamas/k-skill 0.7.0 → 0.9.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 +11 -0
- package/package.json +1 -1
- package/skills/bccard-eatpl-search/instruction.md +144 -0
- package/skills/bccard-eatpl-search/references/DISCLAIMER.md +11 -0
- package/skills/bccard-eatpl-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
- package/skills/bccard-eatpl-search/scripts/kakao-place-rating.mjs +254 -0
- package/skills/bccard-eatpl-search/skill.json +6 -0
- package/skills/korean-privacy-terms/instruction.md +1 -0
- package/skills/korean-privacy-terms/scripts/install.sh +17 -0
- package/skills/religious-facility-search/instruction.md +195 -0
- package/skills/religious-facility-search/scripts/religious_facility_search.py +277 -0
- package/skills/religious-facility-search/skill.json +9 -0
- package/skills/sharenuri-facility-search/instruction.md +127 -0
- package/skills/sharenuri-facility-search/references/DISCLAIMER.md +23 -0
- package/skills/sharenuri-facility-search/references/TRADEMARK-LEGAL-STATEMENT.md +9 -0
- package/skills/sharenuri-facility-search/scripts/sharenuri_search.py +344 -0
- package/skills/sharenuri-facility-search/skill.json +8 -0
- package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""공유누리(eshare.go.kr) 공공개방자원 검색 helper.
|
|
2
|
+
|
|
3
|
+
행정안전부 공유누리 포털의 로그인 없이 열려 있는 통합검색 표면을 호출해
|
|
4
|
+
전국 공공개방자원(회의실·체육시설·숙소·교육·물품 등)을 조회한다.
|
|
5
|
+
API 인증키가 필요 없는 공개 read-only 경로만 사용하며, 예약 신청·로그인·
|
|
6
|
+
결제 같은 쓰기 흐름은 자동화하지 않는다.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import http.cookiejar
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.parse
|
|
18
|
+
import urllib.request
|
|
19
|
+
|
|
20
|
+
BASE_URL = "https://www.eshare.go.kr"
|
|
21
|
+
BOOTSTRAP_URL = BASE_URL + "/UserPortal/adv/unifdSearch/UnifiedSearchMainView.do"
|
|
22
|
+
SEARCH_URL = BASE_URL + "/UserPortal/adv/unifdSearch/ConditionsSearch.do"
|
|
23
|
+
SIGUNGU_URL = BASE_URL + "/UserPortal/comm/zip/selectsigunguList.do"
|
|
24
|
+
CATEGORY_URL = BASE_URL + "/UserPortal/Upm/searchUserPortalRsrcCls.do"
|
|
25
|
+
USER_AGENT = "k-skill-sharenuri-facility-search/0.1 (+https://github.com/NomaDamas/k-skill)"
|
|
26
|
+
REQUEST_TIMEOUT = 20
|
|
27
|
+
PAGE_SIZE = 10
|
|
28
|
+
MAX_PAGES = 5
|
|
29
|
+
|
|
30
|
+
# 통합검색 상세조건의 시·도 선택 코드 (2026-09-12 공유누리 통합검색 화면 실측)
|
|
31
|
+
SIDO_CODES = {
|
|
32
|
+
"서울특별시": "11", "부산광역시": "26", "인천광역시": "28", "대전광역시": "30",
|
|
33
|
+
"전남광주통합특별시": "12", "대구광역시": "27", "울산광역시": "31",
|
|
34
|
+
"세종특별자치시": "36", "경기도": "41", "충청북도": "43", "충청남도": "44",
|
|
35
|
+
"전북특별자치도": "52", "경상북도": "47", "경상남도": "48",
|
|
36
|
+
"강원특별자치도": "51", "제주특별자치도": "50",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# 검색결과의 rsrcDcd 7~9번째 자리 → 상세페이지 경로 (통합검색 JS fnNewPageResourceDetail 기준)
|
|
40
|
+
DETAIL_PATH_BY_DCD = {
|
|
41
|
+
"001": "Upv/UprResrcGym", "002": "Upv/UprResrcFacl", "098": "Upv/UprResrcFacl",
|
|
42
|
+
"003": "Upv/UprResrcCamp", "004": "Upv/UprResrcEqp", "005": "Upv/UprResrcGds",
|
|
43
|
+
"006": "Upv/UprResrcEdu", "007": "Upv/UprResrcExpr", "008": "Upv/UprResrcPfm",
|
|
44
|
+
"009": "Upv/UprResrcEvnt",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_TAG_RE = re.compile(r"<[^>]+>")
|
|
48
|
+
_WS_RE = re.compile(r"\s+")
|
|
49
|
+
_TOTAL_RE = re.compile(r"총\s*<span>([\d,]+)</span>\s*건")
|
|
50
|
+
_ITEM_SPLIT_RE = re.compile(r'<li>\s*<!--\s*이미지영역', re.S)
|
|
51
|
+
_RSRC_RE = re.compile(r"fnNewPageResourceDetail\('([^']+)',\s*'([^']+)'\)")
|
|
52
|
+
_SR_ONLY_RE = re.compile(r'<span class="sr-only">.*?</span>', re.S)
|
|
53
|
+
_DETAIL_ROW_RE = re.compile(
|
|
54
|
+
r'<th[^>]*scope="row"[^>]*>(.*?)</th>\s*<td[^>]*>(.*?)</td>', re.S
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SharenuriError(RuntimeError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _strip_html(fragment: str) -> str:
|
|
63
|
+
text = _TAG_RE.sub(" ", fragment)
|
|
64
|
+
text = text.replace(" ", " ").replace("·", "·").replace("&", "&")
|
|
65
|
+
return _WS_RE.sub(" ", text).strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _open_session():
|
|
69
|
+
jar = http.cookiejar.CookieJar()
|
|
70
|
+
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
|
71
|
+
opener.addheaders = [("User-Agent", USER_AGENT)]
|
|
72
|
+
return opener
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _request(opener, url: str, data: dict | None = None, referer: str | None = None) -> str:
|
|
76
|
+
body = None
|
|
77
|
+
headers = {}
|
|
78
|
+
if data is not None:
|
|
79
|
+
body = urllib.parse.urlencode(data).encode("utf-8")
|
|
80
|
+
headers["X-Requested-With"] = "XMLHttpRequest"
|
|
81
|
+
if referer:
|
|
82
|
+
headers["Referer"] = referer
|
|
83
|
+
req = urllib.request.Request(url, data=body, headers=headers)
|
|
84
|
+
try:
|
|
85
|
+
with opener.open(req, timeout=REQUEST_TIMEOUT) as resp:
|
|
86
|
+
return resp.read().decode("utf-8", errors="replace")
|
|
87
|
+
except urllib.error.URLError as exc:
|
|
88
|
+
raise SharenuriError(f"공유누리 서버에 연결하지 못했습니다: {exc}") from exc
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _bootstrap(opener) -> None:
|
|
92
|
+
# 통합검색 화면을 한 번 열어 세션 쿠키를 받는다 (검색 POST의 선행 조건)
|
|
93
|
+
_request(opener, BOOTSTRAP_URL)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def normalize_sigg(sigg: str | None) -> str | None:
|
|
97
|
+
# 공유누리 통합검색 select[name=sigg] 값은 gunguCd (강남구 680).
|
|
98
|
+
# 법정동 시군구코드 5자리(11680)가 들어오면 뒤 3자리를 쓴다.
|
|
99
|
+
if not sigg:
|
|
100
|
+
return None
|
|
101
|
+
value = str(sigg).strip()
|
|
102
|
+
if len(value) == 5 and value.isdigit():
|
|
103
|
+
return value[-3:]
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def detail_path_for(rsrc_dcd: str) -> str:
|
|
108
|
+
dcd = rsrc_dcd[7:10]
|
|
109
|
+
path = DETAIL_PATH_BY_DCD.get(dcd)
|
|
110
|
+
if path is None:
|
|
111
|
+
raise SharenuriError(f"알 수 없는 자원 분류 코드입니다: {rsrc_dcd}")
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def detail_url(rsrc_dcd: str, rsrc_no: str) -> str:
|
|
116
|
+
return (
|
|
117
|
+
f"{BASE_URL}/UserPortal/{detail_path_for(rsrc_dcd)}/index.do"
|
|
118
|
+
f"?rsrc_no={urllib.parse.quote(rsrc_no)}&rsrc_dcd={urllib.parse.quote(rsrc_dcd)}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_search_fragment(html: str) -> dict:
|
|
123
|
+
# 검색어 하이라이트 마커가 title 속성 안에까지 들어 있어 태그 파싱을 깨뜨리므로 먼저 제거
|
|
124
|
+
html = html.replace("<!HS>", "").replace("<!HE>", "")
|
|
125
|
+
total_match = _TOTAL_RE.search(html)
|
|
126
|
+
total = int(total_match.group(1).replace(",", "")) if total_match else 0
|
|
127
|
+
items = []
|
|
128
|
+
for block in _ITEM_SPLIT_RE.split(html)[1:]:
|
|
129
|
+
block = "<li>" + block
|
|
130
|
+
block = _SR_ONLY_RE.sub("", block) # 접근성 숨김 텍스트(시설명 반복) 제거
|
|
131
|
+
id_match = _RSRC_RE.search(block)
|
|
132
|
+
if not id_match:
|
|
133
|
+
continue
|
|
134
|
+
rsrc_dcd, rsrc_no = id_match.group(1), id_match.group(2)
|
|
135
|
+
text = _strip_html(block)
|
|
136
|
+
item = {
|
|
137
|
+
"rsrc_dcd": rsrc_dcd,
|
|
138
|
+
"rsrc_no": rsrc_no,
|
|
139
|
+
"detail_url": detail_url(rsrc_dcd, rsrc_no),
|
|
140
|
+
}
|
|
141
|
+
title_match = re.search(r'<a class="sbj_txt"[^>]*>(.*?)</a>', block, re.S)
|
|
142
|
+
if title_match:
|
|
143
|
+
item["title"] = _strip_html(title_match.group(1))
|
|
144
|
+
for label, key in (("자원 분류", "category"), ("위치 정보", "location"), ("이용 요금", "fee")):
|
|
145
|
+
seg = re.search(label + r"\s+(.*?)(?:자원 분류|위치 정보|이용 요금|지도 보기|예약·문의하기|$)", text)
|
|
146
|
+
if seg and seg.group(1):
|
|
147
|
+
item[key] = seg.group(1).strip()
|
|
148
|
+
items.append(item)
|
|
149
|
+
return {"total_count": total, "items": items}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_detail_page(html: str) -> dict:
|
|
153
|
+
html = html.replace("<!HS>", "").replace("<!HE>", "")
|
|
154
|
+
fields = {}
|
|
155
|
+
for label, value in _DETAIL_ROW_RE.findall(html):
|
|
156
|
+
label_text = _strip_html(label)
|
|
157
|
+
value_text = _strip_html(value)
|
|
158
|
+
value_text = re.sub(r"\s*지도보기$", "", value_text)
|
|
159
|
+
value_text = value_text.replace("-->", "").strip()
|
|
160
|
+
if label_text:
|
|
161
|
+
fields[label_text] = value_text
|
|
162
|
+
return fields
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_search(opener, args) -> dict:
|
|
166
|
+
sido_code = args.ctrd
|
|
167
|
+
if args.sido:
|
|
168
|
+
if args.sido not in SIDO_CODES:
|
|
169
|
+
raise SharenuriError(
|
|
170
|
+
"알 수 없는 시·도 이름입니다. 사용 가능: " + ", ".join(SIDO_CODES)
|
|
171
|
+
)
|
|
172
|
+
sido_code = SIDO_CODES[args.sido]
|
|
173
|
+
limit = max(1, min(args.limit, PAGE_SIZE * MAX_PAGES))
|
|
174
|
+
pages = min(MAX_PAGES, (limit + PAGE_SIZE - 1) // PAGE_SIZE)
|
|
175
|
+
collected: list[dict] = []
|
|
176
|
+
total = 0
|
|
177
|
+
_bootstrap(opener)
|
|
178
|
+
for offset in range(pages):
|
|
179
|
+
payload = {
|
|
180
|
+
"searchWrd": args.query,
|
|
181
|
+
"sort": "RANK",
|
|
182
|
+
"order": "DESC",
|
|
183
|
+
"pageIndex": str(args.page + offset),
|
|
184
|
+
"collection": "resource_cate1",
|
|
185
|
+
"viewType": "UnitPage",
|
|
186
|
+
}
|
|
187
|
+
if sido_code:
|
|
188
|
+
payload["ctrd"] = sido_code
|
|
189
|
+
sigg = normalize_sigg(args.sigg)
|
|
190
|
+
if sigg:
|
|
191
|
+
payload["sigg"] = sigg
|
|
192
|
+
if args.major_code:
|
|
193
|
+
payload["mj_rsrc_cls_cd"] = args.major_code
|
|
194
|
+
if args.free:
|
|
195
|
+
payload["free_yn"] = "Y"
|
|
196
|
+
if args.reservable:
|
|
197
|
+
payload["intnet_rsrv_psbl_yn"] = "Y"
|
|
198
|
+
html = _request(opener, SEARCH_URL, data=payload, referer=BOOTSTRAP_URL)
|
|
199
|
+
parsed = parse_search_fragment(html)
|
|
200
|
+
total = parsed["total_count"]
|
|
201
|
+
collected.extend(parsed["items"])
|
|
202
|
+
if len(parsed["items"]) < PAGE_SIZE:
|
|
203
|
+
break
|
|
204
|
+
return {
|
|
205
|
+
"query": {
|
|
206
|
+
"searchWrd": args.query,
|
|
207
|
+
"ctrd": sido_code,
|
|
208
|
+
"sigg": normalize_sigg(args.sigg),
|
|
209
|
+
"mj_rsrc_cls_cd": args.major_code,
|
|
210
|
+
"free_yn": "Y" if args.free else None,
|
|
211
|
+
"intnet_rsrv_psbl_yn": "Y" if args.reservable else None,
|
|
212
|
+
"page": args.page,
|
|
213
|
+
},
|
|
214
|
+
"total_count": total,
|
|
215
|
+
"items": collected[:limit],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def cmd_detail(opener, args) -> dict:
|
|
220
|
+
url = detail_url(args.rsrc_dcd, args.rsrc_no) if args.rsrc_dcd else None
|
|
221
|
+
if url is None:
|
|
222
|
+
raise SharenuriError("--rsrc-dcd 가 필요합니다 (검색 결과의 rsrc_dcd 값)")
|
|
223
|
+
html = _request(opener, url)
|
|
224
|
+
fields = parse_detail_page(html)
|
|
225
|
+
if not fields:
|
|
226
|
+
raise SharenuriError(
|
|
227
|
+
"상세 페이지에서 기본정보를 찾지 못했습니다. rsrc_no/rsrc_dcd 확인 또는 공유누리 화면 구조 변경 가능성이 있습니다."
|
|
228
|
+
)
|
|
229
|
+
return {"detail_url": url, "fields": fields}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def cmd_sigungu(opener, args) -> dict:
|
|
233
|
+
body = _request(opener, SIGUNGU_URL, data={"addr": args.ctrd})
|
|
234
|
+
try:
|
|
235
|
+
rows = json.loads(body).get("signguNmList", [])
|
|
236
|
+
except json.JSONDecodeError as exc:
|
|
237
|
+
raise SharenuriError("시군구 목록 응답이 JSON이 아닙니다.") from exc
|
|
238
|
+
return {
|
|
239
|
+
"ctrd": args.ctrd,
|
|
240
|
+
"sigungu": [
|
|
241
|
+
{
|
|
242
|
+
"sigunguCd": row.get("sigunguCd"),
|
|
243
|
+
"gunguCd": row.get("gunguCd"),
|
|
244
|
+
"name": row.get("gunguNm"),
|
|
245
|
+
}
|
|
246
|
+
for row in rows
|
|
247
|
+
if row.get("openAt") == "Y"
|
|
248
|
+
],
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_categories(opener, args) -> dict:
|
|
253
|
+
body = _request(opener, CATEGORY_URL, data={"rsrcClsCd": "1"})
|
|
254
|
+
try:
|
|
255
|
+
rows = json.loads(body).get("rsrcClsList", [])
|
|
256
|
+
except json.JSONDecodeError as exc:
|
|
257
|
+
raise SharenuriError("자원 대분류 응답이 JSON이 아닙니다.") from exc
|
|
258
|
+
return {
|
|
259
|
+
"major_categories": [
|
|
260
|
+
{"code": row.get("rsrc_cls_cd"), "name": row.get("rsrc_cls_nm")} for row in rows
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _print(result: dict, as_json: bool) -> None:
|
|
266
|
+
if as_json:
|
|
267
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
268
|
+
return
|
|
269
|
+
if "items" in result:
|
|
270
|
+
q = result["query"]
|
|
271
|
+
print(f"검색어: {q['searchWrd']} / 총 {result['total_count']:,}건 (표시 {len(result['items'])}건)")
|
|
272
|
+
for idx, item in enumerate(result["items"], 1):
|
|
273
|
+
print(f"\n[{idx}] {item.get('title', '(제목 없음)')}")
|
|
274
|
+
if item.get("category"):
|
|
275
|
+
print(f" 분류: {item['category']}")
|
|
276
|
+
if item.get("location"):
|
|
277
|
+
print(f" 위치: {item['location']}")
|
|
278
|
+
if item.get("fee"):
|
|
279
|
+
print(f" 요금: {item['fee']}")
|
|
280
|
+
print(f" 상세: {item['detail_url']}")
|
|
281
|
+
elif "fields" in result:
|
|
282
|
+
print(f"상세 페이지: {result['detail_url']}")
|
|
283
|
+
for label, value in result["fields"].items():
|
|
284
|
+
print(f" {label}: {value}")
|
|
285
|
+
elif "sigungu" in result:
|
|
286
|
+
for row in result["sigungu"]:
|
|
287
|
+
print(f"{row['gunguCd']}\t{row['name']}\t{row['sigunguCd']}")
|
|
288
|
+
elif "major_categories" in result:
|
|
289
|
+
for row in result["major_categories"]:
|
|
290
|
+
print(f"{row['code']}\t{row['name']}")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
294
|
+
parser = argparse.ArgumentParser(
|
|
295
|
+
description="공유누리 공공개방자원 검색 (로그인·API 키 불필요, 조회 전용)"
|
|
296
|
+
)
|
|
297
|
+
parser.add_argument("--json", action="store_true", help="JSON 출력")
|
|
298
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
299
|
+
|
|
300
|
+
p_search = sub.add_parser("search", help="자원 통합검색")
|
|
301
|
+
p_search.add_argument("--query", "-q", required=True, help="검색어 (예: 회의실)")
|
|
302
|
+
p_search.add_argument("--sido", help="시·도 이름 (예: 서울특별시)")
|
|
303
|
+
p_search.add_argument("--ctrd", help="시·도 코드 (예: 11)")
|
|
304
|
+
p_search.add_argument("--sigg", help="시군구 코드 (sigungu 하위명령으로 조회)")
|
|
305
|
+
p_search.add_argument("--major-code", help="자원 대분류 코드 (categories 하위명령으로 조회)")
|
|
306
|
+
p_search.add_argument("--free", action="store_true", help="무료 자원만")
|
|
307
|
+
p_search.add_argument("--reservable", action="store_true", help="인터넷 예약 가능 자원만")
|
|
308
|
+
p_search.add_argument("--page", type=int, default=1, help="시작 페이지 (기본 1)")
|
|
309
|
+
p_search.add_argument("--limit", type=int, default=10, help="최대 건수 (기본 10, 최대 50)")
|
|
310
|
+
|
|
311
|
+
p_detail = sub.add_parser("detail", help="자원 상세정보 조회")
|
|
312
|
+
p_detail.add_argument("--rsrc-no", required=True, help="자원번호 (검색 결과의 rsrc_no)")
|
|
313
|
+
p_detail.add_argument("--rsrc-dcd", required=True, help="자원분류코드 (검색 결과의 rsrc_dcd)")
|
|
314
|
+
|
|
315
|
+
p_sigungu = sub.add_parser("sigungu", help="시·도 코드의 시군구 목록")
|
|
316
|
+
p_sigungu.add_argument("--ctrd", required=True, help="시·도 코드 (예: 11)")
|
|
317
|
+
|
|
318
|
+
sub.add_parser("categories", help="자원 대분류 코드 목록")
|
|
319
|
+
return parser
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main(argv: list[str] | None = None) -> int:
|
|
323
|
+
args = build_parser().parse_args(argv)
|
|
324
|
+
if getattr(args, "page", 1) < 1:
|
|
325
|
+
print("오류: --page 는 1 이상이어야 합니다.", file=sys.stderr)
|
|
326
|
+
return 2
|
|
327
|
+
opener = _open_session()
|
|
328
|
+
handlers = {
|
|
329
|
+
"search": cmd_search,
|
|
330
|
+
"detail": cmd_detail,
|
|
331
|
+
"sigungu": cmd_sigungu,
|
|
332
|
+
"categories": cmd_categories,
|
|
333
|
+
}
|
|
334
|
+
try:
|
|
335
|
+
result = handlers[args.command](opener, args)
|
|
336
|
+
except SharenuriError as exc:
|
|
337
|
+
print(f"오류: {exc}", file=sys.stderr)
|
|
338
|
+
return 1
|
|
339
|
+
_print(result, args.json)
|
|
340
|
+
return 0
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
sys.exit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sharenuri-facility-search",
|
|
3
|
+
"description": "공유누리(eshare.go.kr) 공개 통합검색으로 전국 공공개방자원(회의실·체육시설·숙소·교육·물품 등)을 인증키·로그인 없이 검색하고, 상세정보(요금·정원·예약방법·문의처)와 공식 예약 경로를 안내한다. 조회 전용.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"lookup"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: sharenuri-facility-search\ndescription: 공유누리(eshare.go.kr) 공개 통합검색으로 전국 공공개방자원(회의실·체육시설·숙소·교육·물품 등)을 인증키·로그인 없이 검색하고, 상세정보(요금·정원·예약방법·문의처)와 공식 예약 경로를 안내한다. 조회 전용.\nlicense: MIT\nmetadata:\n category: civic\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|