@nomadamas/k-skill 0.1.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 +60 -0
- package/package.json +37 -0
- package/skills/assembly-bill-vote-search/instruction.md +111 -0
- package/skills/assembly-bill-vote-search/skill.json +10 -0
- package/skills/biz-health-check/instruction.md +69 -0
- package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
- package/skills/biz-health-check/skill.json +10 -0
- package/skills/bok-ecos-stats/instruction.md +95 -0
- package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
- package/skills/bok-ecos-stats/skill.json +10 -0
- package/skills/building-register-search/instruction.md +69 -0
- package/skills/building-register-search/scripts/building_register.py +309 -0
- package/skills/building-register-search/scripts/building_register_xml.py +32 -0
- package/skills/building-register-search/skill.json +9 -0
- package/skills/bunjang-search/instruction.md +154 -0
- package/skills/bunjang-search/skill.json +10 -0
- package/skills/catchtable-sniper/instruction.md +270 -0
- package/skills/catchtable-sniper/skill.json +9 -0
- package/skills/cheap-gas-nearby/instruction.md +107 -0
- package/skills/cheap-gas-nearby/skill.json +10 -0
- package/skills/corporate-registration-consulting/instruction.md +117 -0
- package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
- package/skills/corporate-registration-consulting/skill.json +8 -0
- package/skills/coupang-product-search/instruction.md +219 -0
- package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
- package/skills/coupang-product-search/skill.json +10 -0
- package/skills/court-auction-notice-search/instruction.md +201 -0
- package/skills/court-auction-notice-search/skill.json +9 -0
- package/skills/court-payment-order-assistant/instruction.md +120 -0
- package/skills/court-payment-order-assistant/skill.json +10 -0
- package/skills/d2b-notice-search/instruction.md +117 -0
- package/skills/d2b-notice-search/skill.json +10 -0
- package/skills/daangn-cars-search/instruction.md +91 -0
- package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
- package/skills/daangn-cars-search/skill.json +9 -0
- package/skills/daangn-jobs-search/instruction.md +90 -0
- package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
- package/skills/daangn-jobs-search/skill.json +9 -0
- package/skills/daangn-realty-search/instruction.md +114 -0
- package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
- package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
- package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
- package/skills/daangn-realty-search/skill.json +8 -0
- package/skills/daangn-used-goods-search/instruction.md +90 -0
- package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
- package/skills/daangn-used-goods-search/skill.json +9 -0
- package/skills/daishin-report-search/instruction.md +138 -0
- package/skills/daishin-report-search/skill.json +10 -0
- package/skills/daiso-product-search/instruction.md +172 -0
- package/skills/daiso-product-search/skill.json +8 -0
- package/skills/danawa-price-search/instruction.md +183 -0
- package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
- package/skills/danawa-price-search/skill.json +8 -0
- package/skills/delivery-tracking/instruction.md +358 -0
- package/skills/delivery-tracking/skill.json +8 -0
- package/skills/donation-place-search/instruction.md +129 -0
- package/skills/donation-place-search/skill.json +9 -0
- package/skills/emergency-room-beds/instruction.md +82 -0
- package/skills/emergency-room-beds/skill.json +8 -0
- package/skills/ev-charger-nearby/instruction.md +80 -0
- package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
- package/skills/ev-charger-nearby/skill.json +10 -0
- package/skills/ev-subsidy-status/instruction.md +165 -0
- package/skills/ev-subsidy-status/skill.json +10 -0
- package/skills/express-bus-booking/instruction.md +207 -0
- package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
- package/skills/express-bus-booking/skill.json +8 -0
- package/skills/fine-dust-location/instruction.md +89 -0
- package/skills/fine-dust-location/skill.json +9 -0
- package/skills/flight-ticket-search/instruction.md +237 -0
- package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
- package/skills/flight-ticket-search/skill.json +9 -0
- package/skills/foresttrip-vacancy/instruction.md +167 -0
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
- package/skills/foresttrip-vacancy/skill.json +10 -0
- package/skills/fsc-corporate-info/instruction.md +57 -0
- package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
- package/skills/fsc-corporate-info/skill.json +10 -0
- package/skills/g2b-order-plan-search/instruction.md +131 -0
- package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
- package/skills/g2b-order-plan-search/skill.json +10 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
- package/skills/g2b-sanctioned-supplier/skill.json +10 -0
- package/skills/gangnamunni-clinic-search/instruction.md +113 -0
- package/skills/gangnamunni-clinic-search/skill.json +9 -0
- package/skills/geeknews-search/instruction.md +69 -0
- package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
- package/skills/geeknews-search/skill.json +8 -0
- package/skills/gongsijiga-search/instruction.md +128 -0
- package/skills/gongsijiga-search/skill.json +9 -0
- package/skills/gov-overseas-trip-report/instruction.md +488 -0
- package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
- package/skills/gov-overseas-trip-report/skill.json +9 -0
- package/skills/han-river-water-level/instruction.md +83 -0
- package/skills/han-river-water-level/skill.json +10 -0
- package/skills/highway-traffic-status/instruction.md +88 -0
- package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
- package/skills/highway-traffic-status/skill.json +11 -0
- package/skills/hipass-receipt/instruction.md +97 -0
- package/skills/hipass-receipt/skill.json +10 -0
- package/skills/hola-poke-yeoksam/instruction.md +247 -0
- package/skills/hola-poke-yeoksam/skill.json +8 -0
- package/skills/household-waste-info/instruction.md +117 -0
- package/skills/household-waste-info/skill.json +10 -0
- package/skills/housing-official-price/instruction.md +177 -0
- package/skills/housing-official-price/skill.json +10 -0
- package/skills/hwp/instruction.md +206 -0
- package/skills/hwp/skill.json +8 -0
- package/skills/intercity-bus-booking/instruction.md +189 -0
- package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
- package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
- package/skills/intercity-bus-booking/skill.json +8 -0
- package/skills/iros-registry-automation/instruction.md +229 -0
- package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
- package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
- package/skills/iros-registry-automation/skill.json +9 -0
- package/skills/job-posting-match/instruction.md +130 -0
- package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
- package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
- package/skills/job-posting-match/skill.json +8 -0
- package/skills/jobkorea-talent-search/instruction.md +118 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
- package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
- package/skills/jobkorea-talent-search/skill.json +9 -0
- package/skills/joseon-sillok-search/instruction.md +76 -0
- package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
- package/skills/joseon-sillok-search/skill.json +8 -0
- package/skills/k-dart/instruction.md +406 -0
- package/skills/k-dart/skill.json +8 -0
- package/skills/k-schoollunch-menu/instruction.md +109 -0
- package/skills/k-schoollunch-menu/skill.json +9 -0
- package/skills/k-skill-cleaner/instruction.md +80 -0
- package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
- package/skills/k-skill-cleaner/skill.json +8 -0
- package/skills/k-skill-setup/instruction.md +253 -0
- package/skills/k-skill-setup/skill.json +11 -0
- package/skills/kakao-bar-nearby/instruction.md +76 -0
- package/skills/kakao-bar-nearby/skill.json +8 -0
- package/skills/kakao-map/instruction.md +176 -0
- package/skills/kakao-map/skill.json +10 -0
- package/skills/kakaotalk-mac/instruction.md +189 -0
- package/skills/kakaotalk-mac/skill.json +8 -0
- package/skills/kbl-results/instruction.md +89 -0
- package/skills/kbl-results/skill.json +9 -0
- package/skills/kbo-results/instruction.md +82 -0
- package/skills/kbo-results/skill.json +8 -0
- package/skills/keris-academic-search/instruction.md +81 -0
- package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
- package/skills/keris-academic-search/skill.json +10 -0
- package/skills/kleague-results/instruction.md +92 -0
- package/skills/kleague-results/skill.json +8 -0
- package/skills/kopis-performance-search/instruction.md +109 -0
- package/skills/kopis-performance-search/skill.json +10 -0
- package/skills/korea-weather/instruction.md +93 -0
- package/skills/korea-weather/skill.json +10 -0
- package/skills/korean-character-count/instruction.md +87 -0
- package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
- package/skills/korean-character-count/skill.json +8 -0
- package/skills/korean-cinema-search/instruction.md +177 -0
- package/skills/korean-cinema-search/skill.json +8 -0
- package/skills/korean-heritage-search/instruction.md +106 -0
- package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
- package/skills/korean-heritage-search/skill.json +8 -0
- package/skills/korean-holiday-calendar/instruction.md +97 -0
- package/skills/korean-holiday-calendar/skill.json +9 -0
- package/skills/korean-humanizer/instruction.md +389 -0
- package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
- package/skills/korean-humanizer/skill.json +8 -0
- package/skills/korean-jangbu-for/instruction.md +133 -0
- package/skills/korean-jangbu-for/scripts/install.sh +237 -0
- package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
- package/skills/korean-jangbu-for/skill.json +8 -0
- package/skills/korean-law-search/instruction.md +126 -0
- package/skills/korean-law-search/skill.json +10 -0
- package/skills/korean-marathon-schedule/instruction.md +111 -0
- package/skills/korean-marathon-schedule/skill.json +9 -0
- package/skills/korean-middle-korean/instruction.md +79 -0
- package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
- package/skills/korean-middle-korean/skill.json +8 -0
- package/skills/korean-patent-search/instruction.md +79 -0
- package/skills/korean-patent-search/scripts/patent_search.py +409 -0
- package/skills/korean-patent-search/skill.json +9 -0
- package/skills/korean-privacy-terms/instruction.md +128 -0
- package/skills/korean-privacy-terms/scripts/install.sh +108 -0
- package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
- package/skills/korean-privacy-terms/skill.json +8 -0
- package/skills/korean-scholarship-search/instruction.md +317 -0
- package/skills/korean-scholarship-search/references/report-format.md +40 -0
- package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
- package/skills/korean-scholarship-search/references/search-clues.md +58 -0
- package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
- package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
- package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
- package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
- package/skills/korean-scholarship-search/skill.json +8 -0
- package/skills/korean-slang-writing/instruction.md +181 -0
- package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
- package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
- package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
- package/skills/korean-slang-writing/skill.json +8 -0
- package/skills/korean-spell-check/instruction.md +105 -0
- package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
- package/skills/korean-spell-check/skill.json +9 -0
- package/skills/korean-stock-search/instruction.md +194 -0
- package/skills/korean-stock-search/skill.json +10 -0
- package/skills/korean-transit-route/instruction.md +113 -0
- package/skills/korean-transit-route/skill.json +10 -0
- package/skills/kosis-stats/instruction.md +232 -0
- package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
- package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
- package/skills/kosis-stats/skill.json +9 -0
- package/skills/kr-whois-lookup/instruction.md +107 -0
- package/skills/kr-whois-lookup/skill.json +10 -0
- package/skills/kstartup-search/instruction.md +186 -0
- package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
- package/skills/kstartup-search/skill.json +11 -0
- package/skills/ktx-booking/instruction.md +244 -0
- package/skills/ktx-booking/skill.json +10 -0
- package/skills/lck-analytics/instruction.md +192 -0
- package/skills/lck-analytics/scripts/_lib.js +103 -0
- package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
- package/skills/lck-analytics/scripts/build-match-report.js +44 -0
- package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
- package/skills/lck-analytics/skill.json +9 -0
- package/skills/lh-notice-search/instruction.md +206 -0
- package/skills/lh-notice-search/skill.json +10 -0
- package/skills/library-book-search/instruction.md +139 -0
- package/skills/library-book-search/skill.json +9 -0
- package/skills/local-election-candidate-search/instruction.md +77 -0
- package/skills/local-election-candidate-search/skill.json +8 -0
- package/skills/localdata-business-status/instruction.md +64 -0
- package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
- package/skills/localdata-business-status/skill.json +8 -0
- package/skills/lotto-results/instruction.md +80 -0
- package/skills/lotto-results/skill.json +8 -0
- package/skills/lovebug-report/instruction.md +185 -0
- package/skills/lovebug-report/skill.json +9 -0
- package/skills/market-kurly-search/instruction.md +125 -0
- package/skills/market-kurly-search/skill.json +8 -0
- package/skills/mfds-drug-safety/instruction.md +86 -0
- package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
- package/skills/mfds-drug-safety/skill.json +10 -0
- package/skills/mfds-food-safety/instruction.md +126 -0
- package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
- package/skills/mfds-food-safety/skill.json +10 -0
- package/skills/myrealtrip-search/instruction.md +239 -0
- package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
- package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
- package/skills/myrealtrip-search/skill.json +9 -0
- package/skills/naming-house/instruction.md +146 -0
- package/skills/naming-house/skill.json +8 -0
- package/skills/national-pension-workplace/instruction.md +64 -0
- package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
- package/skills/national-pension-workplace/skill.json +10 -0
- package/skills/naver-ad-performance/instruction.md +108 -0
- package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
- package/skills/naver-ad-performance/skill.json +9 -0
- package/skills/naver-blog-research/instruction.md +128 -0
- package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
- package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
- package/skills/naver-blog-research/scripts/naver_read.py +256 -0
- package/skills/naver-blog-research/scripts/naver_search.py +192 -0
- package/skills/naver-blog-research/skill.json +8 -0
- package/skills/naver-news-search/instruction.md +103 -0
- package/skills/naver-news-search/skill.json +10 -0
- package/skills/naver-shopping-search/instruction.md +94 -0
- package/skills/naver-shopping-search/skill.json +11 -0
- package/skills/nhis-care-checkup-search/instruction.md +116 -0
- package/skills/nhis-care-checkup-search/skill.json +10 -0
- package/skills/nts-business-registration/instruction.md +115 -0
- package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
- package/skills/nts-business-registration/skill.json +10 -0
- package/skills/nts-tax-delinquency/instruction.md +54 -0
- package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
- package/skills/nts-tax-delinquency/skill.json +8 -0
- package/skills/ohou-today-deal/instruction.md +182 -0
- package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
- package/skills/ohou-today-deal/skill.json +9 -0
- package/skills/olive-young-search/instruction.md +154 -0
- package/skills/olive-young-search/skill.json +8 -0
- package/skills/parking-lot-search/instruction.md +96 -0
- package/skills/parking-lot-search/skill.json +9 -0
- package/skills/popbill/instruction.md +146 -0
- package/skills/popbill/scripts/popbill_cli.py +284 -0
- package/skills/popbill/scripts/popbill_registry.py +54 -0
- package/skills/popbill/scripts/popbill_safety.py +37 -0
- package/skills/popbill/scripts/popbill_templates.py +71 -0
- package/skills/popbill/skill.json +9 -0
- package/skills/public-restroom-nearby/instruction.md +89 -0
- package/skills/public-restroom-nearby/skill.json +9 -0
- package/skills/real-estate-search/instruction.md +172 -0
- package/skills/real-estate-search/skill.json +10 -0
- package/skills/rhwp-advanced/instruction.md +145 -0
- package/skills/rhwp-advanced/skill.json +8 -0
- package/skills/rhwp-edit/instruction.md +153 -0
- package/skills/rhwp-edit/skill.json +8 -0
- package/skills/s2b-notice-search/instruction.md +66 -0
- package/skills/s2b-notice-search/skill.json +9 -0
- package/skills/saju-fortune/instruction.md +177 -0
- package/skills/saju-fortune/skill.json +8 -0
- package/skills/saramin-talent-search/instruction.md +119 -0
- package/skills/saramin-talent-search/skill.json +9 -0
- package/skills/seoul-bike/instruction.md +83 -0
- package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
- package/skills/seoul-bike/skill.json +10 -0
- package/skills/seoul-density/instruction.md +109 -0
- package/skills/seoul-density/scripts/seoul_density.py +271 -0
- package/skills/seoul-density/skill.json +10 -0
- package/skills/seoul-subway-arrival/instruction.md +85 -0
- package/skills/seoul-subway-arrival/skill.json +9 -0
- package/skills/sh-notice-search/instruction.md +150 -0
- package/skills/sh-notice-search/skill.json +9 -0
- package/skills/srt-booking/instruction.md +181 -0
- package/skills/srt-booking/scripts/srt_booking.py +272 -0
- package/skills/srt-booking/scripts/srt_seats.py +156 -0
- package/skills/srt-booking/skill.json +20 -0
- package/skills/subway-lost-property/instruction.md +93 -0
- package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
- package/skills/subway-lost-property/skill.json +8 -0
- package/skills/ticket-availability/instruction.md +175 -0
- package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
- package/skills/ticket-availability/skill.json +9 -0
- package/skills/toss-securities/instruction.md +116 -0
- package/skills/toss-securities/skill.json +9 -0
- package/skills/used-car-price-search/instruction.md +109 -0
- package/skills/used-car-price-search/skill.json +8 -0
- package/skills/yebigun-training/instruction.md +177 -0
- package/skills/yebigun-training/skill.json +9 -0
- package/skills/zipcode-search/instruction.md +159 -0
- package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
- package/skills/zipcode-search/skill.json +8 -0
- package/src/assemble.js +134 -0
- package/src/detect.js +18 -0
- package/templates/action-account.md +4 -0
- package/templates/action-booking.md +5 -0
- package/templates/action-commerce.md +5 -0
- package/templates/action-communication.md +5 -0
- package/templates/action-submission.md +5 -0
- package/templates/browser.md +4 -0
- package/templates/core.md +5 -0
- package/templates/hard-boundary.md +3 -0
- package/templates/local.md +2 -0
- package/templates/lookup.md +2 -0
- package/templates/proxy.md +2 -0
- package/templates/vault.md +4 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only EV charger info/status helper using stdlib only."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import pathlib
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
18
|
+
DEFAULT_SECRETS_PATH = pathlib.Path("~/.config/k-skill/secrets.env").expanduser()
|
|
19
|
+
UPSTREAM_BASE_URL = "https://apis.data.go.kr/B552584/EvCharger"
|
|
20
|
+
OPERATIONS = {"info": "getChargerInfo", "status": "getChargerStatus"}
|
|
21
|
+
PROXY_DOWN_MSG = "설정된 k-skill-proxy 프록시 서버가 응답하지 않습니다. 잠시 후 재시도하거나 운영자에게 문의하세요."
|
|
22
|
+
PROXY_NOT_CONFIGURED_MSG = "k-skill-proxy에 EV 충전소 API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요."
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HelperError(RuntimeError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_secrets(path: pathlib.Path) -> Dict[str, str]:
|
|
30
|
+
values: Dict[str, str] = {}
|
|
31
|
+
try:
|
|
32
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
33
|
+
except OSError:
|
|
34
|
+
return values
|
|
35
|
+
for raw in lines:
|
|
36
|
+
line = raw.strip()
|
|
37
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
38
|
+
continue
|
|
39
|
+
key, value = line.split("=", 1)
|
|
40
|
+
value = value.strip().strip('"').strip("'")
|
|
41
|
+
values[key.strip()] = value
|
|
42
|
+
return values
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_api_key(args: argparse.Namespace) -> Optional[str]:
|
|
46
|
+
env_key = os.environ.get("KSKILL_EV_CHARGER_API_KEY") or os.environ.get("DATA_GO_KR_API_KEY")
|
|
47
|
+
if env_key and env_key.strip():
|
|
48
|
+
return env_key.strip()
|
|
49
|
+
secrets = load_secrets(pathlib.Path(args.secrets_path).expanduser())
|
|
50
|
+
value = secrets.get("KSKILL_EV_CHARGER_API_KEY") or secrets.get("DATA_GO_KR_API_KEY")
|
|
51
|
+
return value.strip() if value and value.strip() else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _bounded_integer(value: int, label: str, minimum: int, maximum: int) -> int:
|
|
55
|
+
if value < minimum or value > maximum:
|
|
56
|
+
raise HelperError(f"{label} 값은 {minimum}~{maximum} 범위여야 합니다.")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_code(value: Optional[str], label: str, maximum: int, digits: Optional[int] = None) -> Optional[str]:
|
|
61
|
+
if value is None:
|
|
62
|
+
return None
|
|
63
|
+
text = value.strip()
|
|
64
|
+
if not text:
|
|
65
|
+
return None
|
|
66
|
+
if len(text) > maximum or (digits is not None and (len(text) != digits or not text.isdigit())):
|
|
67
|
+
raise HelperError(f"올바른 {label} 값을 입력하세요.")
|
|
68
|
+
if digits is None and not all(ch.isalnum() or ch in "_-" for ch in text):
|
|
69
|
+
raise HelperError(f"올바른 {label} 값을 입력하세요.")
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_query(args: argparse.Namespace) -> Dict[str, Any]:
|
|
74
|
+
query: Dict[str, Any] = {
|
|
75
|
+
"pageNo": _bounded_integer(args.page_no, "pageNo", 1, 100000),
|
|
76
|
+
"numOfRows": _bounded_integer(args.num_of_rows, "numOfRows", 10, 9999),
|
|
77
|
+
}
|
|
78
|
+
for key, maximum, digits in (("zcode", 2, 2), ("zscode", 5, 5), ("stat_id", 40, None), ("chger_id", 10, None)):
|
|
79
|
+
value = _validate_code(getattr(args, key), key.replace("_", ""), maximum, digits)
|
|
80
|
+
if value is not None:
|
|
81
|
+
query[{"stat_id": "statId", "chger_id": "chgerId"}.get(key, key)] = value
|
|
82
|
+
if args.command == "info" and args.location:
|
|
83
|
+
location = args.location.strip()
|
|
84
|
+
if len(location) > 100:
|
|
85
|
+
raise HelperError("location 값은 100자 이하여야 합니다.")
|
|
86
|
+
if args.direct:
|
|
87
|
+
raise HelperError("--direct에서는 location을 지원하지 않습니다. --zcode와 --zscode를 사용하세요.")
|
|
88
|
+
query["location"] = location
|
|
89
|
+
if args.command == "status":
|
|
90
|
+
if args.limit_yn:
|
|
91
|
+
limit_yn = args.limit_yn.strip().upper()
|
|
92
|
+
if limit_yn not in {"Y", "N"}:
|
|
93
|
+
raise HelperError("limitYn 값은 Y 또는 N이어야 합니다.")
|
|
94
|
+
query["limitYn"] = limit_yn
|
|
95
|
+
if args.period is not None:
|
|
96
|
+
query["period"] = _bounded_integer(args.period, "period", 1, 10)
|
|
97
|
+
return query
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_url(args: argparse.Namespace, query: Dict[str, Any], api_key: Optional[str]) -> str:
|
|
101
|
+
if args.direct:
|
|
102
|
+
if not api_key:
|
|
103
|
+
raise HelperError(
|
|
104
|
+
"KSKILL_EV_CHARGER_API_KEY 또는 DATA_GO_KR_API_KEY가 없습니다. "
|
|
105
|
+
"공공데이터포털 데이터셋 15076352 활용신청 후 키를 환경변수나 ~/.config/k-skill/secrets.env에 설정하세요."
|
|
106
|
+
)
|
|
107
|
+
direct_query = dict(query)
|
|
108
|
+
direct_query["serviceKey"] = api_key
|
|
109
|
+
direct_query["dataType"] = "JSON"
|
|
110
|
+
return f"{UPSTREAM_BASE_URL}/{OPERATIONS[args.command]}?{urllib.parse.urlencode(direct_query)}"
|
|
111
|
+
return f"{args.proxy_base_url.rstrip('/')}/v1/ev-charger/{args.command}?{urllib.parse.urlencode(query)}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def http_get_json(url: str, timeout: int, via_proxy: bool) -> Dict[str, Any]:
|
|
115
|
+
request = urllib.request.Request(url, headers={"accept": "application/json", "user-agent": "k-skill/ev-charger-nearby"})
|
|
116
|
+
try:
|
|
117
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
118
|
+
raw = response.read().decode("utf-8", errors="replace")
|
|
119
|
+
except urllib.error.HTTPError as error:
|
|
120
|
+
raw = error.read().decode("utf-8", errors="replace") if error.fp else ""
|
|
121
|
+
try:
|
|
122
|
+
payload = json.loads(raw)
|
|
123
|
+
except json.JSONDecodeError:
|
|
124
|
+
payload = {}
|
|
125
|
+
if via_proxy and error.code == 503 and payload.get("error") == "upstream_not_configured":
|
|
126
|
+
raise HelperError(PROXY_NOT_CONFIGURED_MSG) from error
|
|
127
|
+
message = payload.get("message") or f"API HTTP 오류: {error.code} {error.reason}"
|
|
128
|
+
raise HelperError(str(message)) from error
|
|
129
|
+
except urllib.error.URLError as error:
|
|
130
|
+
if via_proxy:
|
|
131
|
+
raise HelperError(f"{PROXY_DOWN_MSG} (상세: {error.reason})") from error
|
|
132
|
+
raise HelperError(f"상류 API 네트워크 오류: {error.reason}") from error
|
|
133
|
+
except TimeoutError as error:
|
|
134
|
+
target = "프록시 서버" if via_proxy else "상류 API"
|
|
135
|
+
raise HelperError(f"{target} 요청 시간이 초과되었습니다.") from error
|
|
136
|
+
try:
|
|
137
|
+
payload = json.loads(raw)
|
|
138
|
+
except json.JSONDecodeError as error:
|
|
139
|
+
raise HelperError("API 응답이 올바른 JSON이 아닙니다.") from error
|
|
140
|
+
if not isinstance(payload, dict):
|
|
141
|
+
raise HelperError("API 응답 형식이 올바르지 않습니다.")
|
|
142
|
+
return payload
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def format_text(payload: Dict[str, Any]) -> str:
|
|
146
|
+
response = payload.get("response")
|
|
147
|
+
if isinstance(response, dict) and isinstance(response.get("body"), dict):
|
|
148
|
+
payload = response["body"]
|
|
149
|
+
items_value = payload.get("items") or []
|
|
150
|
+
if isinstance(items_value, dict):
|
|
151
|
+
items_value = items_value.get("item") or []
|
|
152
|
+
items = items_value if isinstance(items_value, list) else [items_value]
|
|
153
|
+
total_count = payload.get("total_count", payload.get("totalCount", len(items)))
|
|
154
|
+
lines = [f"전기차 충전기 조회 결과: {total_count}건"]
|
|
155
|
+
for item in items:
|
|
156
|
+
name = item.get("statNm") or item.get("statId") or "이름 없음"
|
|
157
|
+
address = item.get("addr") or item.get("location") or "주소 없음"
|
|
158
|
+
charger = item.get("chgerId") or "-"
|
|
159
|
+
status = item.get("stat") or "상태 미상"
|
|
160
|
+
lines.append(f"- {name} / {address} / 충전기 {charger} / 상태 {status}")
|
|
161
|
+
if not items:
|
|
162
|
+
lines.append("조건에 맞는 충전기 정보가 없습니다.")
|
|
163
|
+
return "\n".join(lines)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def add_common_arguments(parser: argparse.ArgumentParser) -> None:
|
|
167
|
+
parser.add_argument("--zcode")
|
|
168
|
+
parser.add_argument("--zscode")
|
|
169
|
+
parser.add_argument("--stat-id")
|
|
170
|
+
parser.add_argument("--chger-id")
|
|
171
|
+
parser.add_argument("--page-no", type=int, default=1)
|
|
172
|
+
parser.add_argument("--num-of-rows", type=int, default=10)
|
|
173
|
+
parser.add_argument("--proxy-base-url", default=os.environ.get("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY_BASE_URL))
|
|
174
|
+
parser.add_argument("--direct", action="store_true")
|
|
175
|
+
parser.add_argument("--secrets-path", default=str(DEFAULT_SECRETS_PATH))
|
|
176
|
+
parser.add_argument("--timeout", type=int, default=90)
|
|
177
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
178
|
+
parser.add_argument("--json", action="store_true")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
182
|
+
parser = argparse.ArgumentParser(description="전기차 충전소 정보와 상태 조회")
|
|
183
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
184
|
+
info = subparsers.add_parser("info", help="충전소 위치와 기본 정보 조회")
|
|
185
|
+
add_common_arguments(info)
|
|
186
|
+
info.add_argument("--location")
|
|
187
|
+
status = subparsers.add_parser("status", help="충전기 운영 상태 조회")
|
|
188
|
+
add_common_arguments(status)
|
|
189
|
+
status.add_argument("--limit-yn")
|
|
190
|
+
status.add_argument("--period", type=int)
|
|
191
|
+
return parser
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
|
195
|
+
return build_parser().parse_args(argv)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def run(argv: Optional[list[str]] = None) -> int:
|
|
199
|
+
try:
|
|
200
|
+
args = parse_args(argv)
|
|
201
|
+
query = build_query(args)
|
|
202
|
+
api_key = resolve_api_key(args) if args.direct else None
|
|
203
|
+
if args.dry_run and args.direct:
|
|
204
|
+
url = build_url(args, query, "REDACTED")
|
|
205
|
+
else:
|
|
206
|
+
url = build_url(args, query, api_key)
|
|
207
|
+
if args.dry_run:
|
|
208
|
+
print(json.dumps({"operation": args.command, "url": url, "query": query}, ensure_ascii=False, indent=2))
|
|
209
|
+
return 0
|
|
210
|
+
payload = http_get_json(url, args.timeout, via_proxy=not args.direct)
|
|
211
|
+
if args.json:
|
|
212
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
213
|
+
else:
|
|
214
|
+
print(format_text(payload))
|
|
215
|
+
return 0
|
|
216
|
+
except HelperError as error:
|
|
217
|
+
print(str(error), file=sys.stderr)
|
|
218
|
+
return 1
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
raise SystemExit(run())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ev-charger-nearby",
|
|
3
|
+
"description": "Use when the user asks for Korean EV charging-station locations, charger types, or current charger status by region, station, or location text. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"action:booking"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: ev-charger-nearby\ndescription: Use when the user asks for Korean EV charging-station locations, charger types, or current charger status by region, station, or location text. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: transit\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# EV Subsidy Status
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Node.js 18+
|
|
6
|
+
- `ev-subsidy-status` npm package
|
|
7
|
+
|
|
8
|
+
기본 조회에는 브라우저, API 키, 로그인, 프록시가 필요하지 않다. 공식 화면 구조 변경을 진단할 때만 사용자가 실행한 Aside Browser, BrowserOS 또는 Chrome/Chromium CDP 세션을 선택적으로 사용한다.
|
|
9
|
+
|
|
10
|
+
## Public access path
|
|
11
|
+
|
|
12
|
+
기본 화면:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
https://ev.or.kr/nportal/buySupprt/initSubsidyPaymentCheckAction.do
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
모델별 보조금 화면:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
POST /nportal/buySupprt/psPopupLocalCarModelPrice.do
|
|
22
|
+
year=<year>&local_cd=<region-code>&car_type=<11|12|13>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
확인된 DOM:
|
|
26
|
+
|
|
27
|
+
- 시도: `#localDo_cd`
|
|
28
|
+
- 시군구: `#local_cd1`
|
|
29
|
+
- 결과 열: 시도, 지역구분, 차종구분, 공고파일, 접수방법, 민간공고대수, 접수대수, 출고대수, 출고잔여대수, 비고
|
|
30
|
+
|
|
31
|
+
페이지는 로그인 없이 공개되지만 `pnp4web`/`penc`가 본문을 보호한다. 기본 경로는 공개 응답에서 공식 `pnp4web.js` 문자표를 파싱하고 원격 코드를 실행하지 않은 채 HTML을 복원한다. 전국 행이 포함된 지급현황 표에서 요청 지역만 선택한다. 공개 keyless 화면이므로 `k-skill-proxy`를 사용하지 않는다.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
### 1. 지역과 차종을 정규화한다
|
|
36
|
+
|
|
37
|
+
- 지역은 가능하면 `시도 + 시군구`로 받는다. 예: `경기 성남시`
|
|
38
|
+
- `중구`, `강서구`처럼 중복되는 이름이면 `regions`로 후보를 찾고 사용자가 시도를 고르게 한다.
|
|
39
|
+
- 차종 기본값은 `passenger`/`전기승용`이다.
|
|
40
|
+
- 지원 차종: `passenger`/`승용`, `cargo`/`화물`, `bus`/`승합`
|
|
41
|
+
- 연도 기본값은 Asia/Seoul 기준 현재 연도다.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx ev-subsidy-status regions --query 중구
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 공식 지급현황을 조회한다
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx ev-subsidy-status status \
|
|
51
|
+
--region "경기 성남시" \
|
|
52
|
+
--vehicle passenger \
|
|
53
|
+
--year 2026 \
|
|
54
|
+
--json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
결과의 `transport`가 `direct-http`인지 확인한다. 기본 경로는 사용자 브라우저가 없어도 동작해야 한다.
|
|
58
|
+
|
|
59
|
+
반환값에서 다음을 우선 확인한다.
|
|
60
|
+
|
|
61
|
+
- `status.notice_count`: 민간공고대수
|
|
62
|
+
- `status.application_count`: 접수대수
|
|
63
|
+
- `status.delivered_count`: 출고대수
|
|
64
|
+
- `status.delivery_remaining_count`: 출고잔여대수
|
|
65
|
+
- `availability.label`: 공고 상태 판정
|
|
66
|
+
- `status.note`: 지자체 비고 원문
|
|
67
|
+
- `source.fetched_at`: KST 조회 시각
|
|
68
|
+
|
|
69
|
+
숫자 셀은 `전체 / 우선순위 / 법인·기관 / 예약 대상군 / 일반`으로 보존한다. 승용의 예약 대상군은 `taxi`, 화물은 `small_business` alias도 함께 반환한다.
|
|
70
|
+
|
|
71
|
+
### 3. 비고를 숫자보다 우선한다
|
|
72
|
+
|
|
73
|
+
다음 우선순위로 설명한다.
|
|
74
|
+
|
|
75
|
+
1. `마감`, `소진`, `접수 종료` → `closed`
|
|
76
|
+
2. `접수 예정`, `추경 예정`, `추가 공고 예정` → `scheduled`
|
|
77
|
+
3. `접수 중`, `신청 기간`, `신청 가능` → `open`
|
|
78
|
+
4. 명시 문구 없이 잔여 대수만 양수 → `unknown_with_remaining_count`
|
|
79
|
+
5. 판단 근거 없음 → `unknown`
|
|
80
|
+
|
|
81
|
+
잔여 대수가 양수여도 비고가 마감이면 신청 가능하다고 말하지 않는다.
|
|
82
|
+
|
|
83
|
+
### 4. 모델을 지정했을 때만 원화 환산치를 조회한다
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx ev-subsidy-status status \
|
|
87
|
+
--region "서울 강남구" \
|
|
88
|
+
--vehicle passenger \
|
|
89
|
+
--model "모델명" \
|
|
90
|
+
--json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
직접 HTTP 경로에서도 공식 모델별 보조금 POST 표면을 조회한다. 입력한 이름이 여러 세부 모델과 일치하면 임의로 하나를 고르지 않고 `model_subsidy_candidates`에 모든 후보와 후보별 환산치를 반환한다. 정확한 세부 모델과 하나만 일치할 때는 `model_subsidy`와 `remaining_budget.model_equivalent_estimate_krw`도 함께 반환한다.
|
|
94
|
+
|
|
95
|
+
`remaining_budget.model_equivalent_estimate_krw`는 다음 가정의 환산치다.
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
공식 출고잔여대수 × 선택 모델의 국비+지방비
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
이를 지자체의 정확한 예산 잔액이라고 표현하지 않는다. 모델별 금액, 구매자 추가지원, 물량 전환, 접수 후 예약·취소가 달라 실제 가용 예산과 다르다. 모델 조회가 실패해도 지역별 대수 결과는 유지하고 `model_lookup_error`와 경고를 보여준다.
|
|
102
|
+
|
|
103
|
+
### 5. 사용자에게 보수적으로 답한다
|
|
104
|
+
|
|
105
|
+
최종 답변에 포함한다.
|
|
106
|
+
|
|
107
|
+
- 지역, 차종, 기준년도
|
|
108
|
+
- 민간공고·접수·출고·출고잔여 대수
|
|
109
|
+
- 공고 상태와 비고의 핵심 문구
|
|
110
|
+
- 모델 환산치가 있으면 계산 가정
|
|
111
|
+
- “출고잔여대수는 실제 신청 가능 대수 및 정확한 원화 잔액과 다를 수 있음” 경고
|
|
112
|
+
- 공식 출처 URL과 KST 조회 시각
|
|
113
|
+
|
|
114
|
+
## Direct HTTP behavior
|
|
115
|
+
|
|
116
|
+
- `POST initSubsidyPaymentCheckAction.do`에 `year1`, `car_type`, 전국 지역 조건을 전송한다.
|
|
117
|
+
- 응답의 `pnp4web.js` URL과 보호 payload를 추출한다.
|
|
118
|
+
- 원격 JavaScript를 `eval`/`vm`으로 실행하지 않는다.
|
|
119
|
+
- 스크립트에 선언된 조각 문자표만 파싱해 payload를 UTF-8 HTML로 복원한다.
|
|
120
|
+
- 지급현황 표의 공식 행과 비고를 파싱하고 지역 코드는 공고 다운로드 함수 인자에서 읽는다.
|
|
121
|
+
- 모델이 있으면 `psPopupLocalCarModelPrice.do`를 같은 연도·지역 코드·차종으로 호출하고 모델별 국비·지방비·합계를 파싱한다.
|
|
122
|
+
- 공식 스크립트나 표 구조가 바뀌면 `UPSTREAM_DECODE_FAILED` 또는 `DOM_CHANGED`로 중단한다.
|
|
123
|
+
|
|
124
|
+
## Optional browser behavior
|
|
125
|
+
|
|
126
|
+
- `domcontentloaded` 후 목표 selector와 지역 행을 명시적으로 기다린다.
|
|
127
|
+
- `networkidle`은 pnp4web 백그라운드 요청 때문에 사용하지 않는다.
|
|
128
|
+
- 렌더링된 `<option>` label/value로 지역 코드를 동적으로 해석한다.
|
|
129
|
+
- 기존 사용자 탭과 프로필을 닫지 않는다.
|
|
130
|
+
- 스킬이 만든 페이지·컨텍스트만 정리하고 지원되는 client만 disconnect한다.
|
|
131
|
+
- 로그인, CAPTCHA, 결제, 전자서명, 최종 제출 경계를 우회하지 않는다.
|
|
132
|
+
|
|
133
|
+
공식 화면 변경을 진단하거나 모델별 환산을 조회하기 위해 브라우저 경로를 선택할 때:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider aside
|
|
137
|
+
npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider browseros
|
|
138
|
+
npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider chrome-cdp
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Done when
|
|
142
|
+
|
|
143
|
+
- 요청 지역과 차종이 공식 표의 행과 일치한다.
|
|
144
|
+
- 공고·접수·출고·출고잔여 수치와 지자체 비고를 반환한다.
|
|
145
|
+
- 명시적 마감 문구를 양수 잔여 대수보다 우선한다.
|
|
146
|
+
- 정확한 원화 예산 잔액을 알 수 없으면 `exact_available=false`와 이유를 반환한다.
|
|
147
|
+
- 출처 URL과 KST 조회 시각을 표시한다.
|
|
148
|
+
|
|
149
|
+
## Failure modes
|
|
150
|
+
|
|
151
|
+
- `REGION_REQUIRED`: 지역 입력이 없다.
|
|
152
|
+
- `REGION_AMBIGUOUS`: 같은 시군구 이름이 여러 시도에 있다.
|
|
153
|
+
- `REGION_NOT_FOUND`: 공식 지역 옵션에 없다.
|
|
154
|
+
- `YEAR_NOT_AVAILABLE`: 요청 연도가 선택 목록에 없다.
|
|
155
|
+
- `VEHICLE_TYPE_NOT_AVAILABLE`: 요청 차종을 지원하지 않거나 화면에서 찾지 못했다.
|
|
156
|
+
- `BROWSER_UNAVAILABLE`: 연결 가능한 사용자 실행 브라우저가 없다.
|
|
157
|
+
- `UPSTREAM_BLOCKED`: 빈 껍데기, 차단 또는 비정상 페이지다.
|
|
158
|
+
- `CAPTCHA_DETECTED`: CAPTCHA가 나타났다. 우회하지 않는다.
|
|
159
|
+
- `AUTH_REQUIRED`: 공개 화면이 로그인 흐름으로 바뀌었다. 우회하지 않는다.
|
|
160
|
+
- `RESULT_EMPTY`: 목표 지역의 결과 행이 없다.
|
|
161
|
+
- `DOM_CHANGED`: 공식 페이지의 선택자나 표 구조가 바뀌었다.
|
|
162
|
+
- `UPSTREAM_DECODE_FAILED`: `pnp4web` 문자표 또는 보호 payload 형식이 바뀌었다.
|
|
163
|
+
- `MODEL_LOOKUP_FAILED`: 모델별 보조금 표를 읽지 못했다.
|
|
164
|
+
|
|
165
|
+
대상군 값이 음수이거나 전체와 부분합이 다르면 값을 0으로 고치지 않는다. 원본 수치와 경고를 함께 반환한다.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ev-subsidy-status",
|
|
3
|
+
"description": "한국 지자체별 전기차 구매보조금 민간공고·접수·출고·출고잔여 대수와 공고 상태를 환경부 무공해차 통합누리집에서 조회한다. 사용자가 사는 지역의 전기승용·전기화물·전기승합 보조금 잔여 현황, 접수 가능 여부, 마감·소진·추경 예정, 특정 모델 기준 보조금 환산치를 물을 때 사용한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"browser",
|
|
7
|
+
"action:submission"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: ev-subsidy-status\ndescription: 한국 지자체별 전기차 구매보조금 민간공고·접수·출고·출고잔여 대수와 공고 상태를 환경부 무공해차 통합누리집에서 조회한다. 사용자가 사는 지역의 전기승용·전기화물·전기승합 보조금 잔여 현황, 접수 가능 여부, 마감·소진·추경 예정, 특정 모델 기준 보조금 환산치를 물을 때 사용한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: local-info\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Express Bus Booking (KOBUS)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Use this skill for Korean 고속버스 / KOBUS timetable lookup and reservation assistance. The preferred workflow is HTTP/API-first: resolve routes, query timetables, inspect remaining seats and fares, and only use browser automation when endpoint discovery or an official web-only step blocks progress.
|
|
6
|
+
|
|
7
|
+
This skill is intentionally separate from 시외버스. KOBUS terminal codes, route structures, and checkout pages differ from Tmoney 시외버스. Use `intercity-bus-booking` for 시외버스/Tmoney flows.
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- The user asks for `고속버스 예매`, `고속버스 시간표`, `고속버스 예약`, `프리미엄 고속버스`, or `우등 고속버스`.
|
|
12
|
+
- The route is clearly a KOBUS 고속버스 route or the user names KOBUS/코버스/고속버스통합예매.
|
|
13
|
+
- The user wants assisted reservation up to seat selection, temporary hold, or a payment-entry link.
|
|
14
|
+
|
|
15
|
+
Do **not** use this for:
|
|
16
|
+
|
|
17
|
+
- 시외버스/Tmoney routes — use `intercity-bus-booking`.
|
|
18
|
+
- Final card submission or payment without explicit, narrowly scoped confirmation.
|
|
19
|
+
- Blind browser automation before trying the official HTTP flow.
|
|
20
|
+
|
|
21
|
+
## Core Principles
|
|
22
|
+
|
|
23
|
+
- Use official KOBUS surfaces: `https://www.kobus.co.kr`.
|
|
24
|
+
- Keep a cookie jar and reuse referers. Stateless one-off POSTs are less reliable.
|
|
25
|
+
- Prefer desktop User-Agent and HTTP/1.1 during probing if the server behaves differently under HTTP/2.
|
|
26
|
+
- Default scope is assisted manual checkout: lookup, candidate presentation, seat-stage readiness, temporary seat hold, and official payment-entry page handoff.
|
|
27
|
+
- Do not submit card fields, agree to terms, or complete payment unless the user explicitly confirms that exact action.
|
|
28
|
+
|
|
29
|
+
## Known HTTP Flow
|
|
30
|
+
|
|
31
|
+
See `references/kobus-http-flow.md` for session-proven endpoint details and parameter examples.
|
|
32
|
+
|
|
33
|
+
### 1. Start a Session
|
|
34
|
+
|
|
35
|
+
Fetch either the main page or route/search page with cookies enabled:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
GET https://www.kobus.co.kr/main.do
|
|
39
|
+
GET https://www.kobus.co.kr/mrs/rotinf.do
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use a realistic desktop User-Agent and a cookie jar.
|
|
43
|
+
|
|
44
|
+
### 2. Resolve Route / Terminal Candidates
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
POST /mrs/readRotLinInf.ajax
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The response is JSON and can include `rotInfList`, `tfrInfList`, `len`, and `codeYn`. Prefer code-confirmed routes over display-name matching because terminal names are not always unique.
|
|
51
|
+
|
|
52
|
+
### 3. Query Timetable
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
POST /mrs/alcnSrch.do
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Typical fields include:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
deprCd=010
|
|
62
|
+
arvlCd=700
|
|
63
|
+
pathDvs=sngl
|
|
64
|
+
pathStep=1
|
|
65
|
+
deprDtm=YYYYMMDD
|
|
66
|
+
busClsCd=0
|
|
67
|
+
rtrpChc=1
|
|
68
|
+
timeLinkMin=00
|
|
69
|
+
timeLinkMax=23
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Parse the returned HTML for schedule rows/cards and `fnSatsChc(...)` onclick arguments. Respect `mrsPsbYn=N` or any equivalent booking-disabled marker.
|
|
73
|
+
|
|
74
|
+
### 4. Enter Seat / Fare Stage
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
POST /mrs/satschc.do
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Send the original search form fields plus selected values from `fnSatsChc(...)`, commonly including:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
deprTime
|
|
84
|
+
alcnDeprTime
|
|
85
|
+
alcnDeprTrmlNo
|
|
86
|
+
alcnArvlTrmlNo
|
|
87
|
+
indVBusClsCd
|
|
88
|
+
cacmCd
|
|
89
|
+
dcDvsCd
|
|
90
|
+
prvtBbizEmpAcmtRt
|
|
91
|
+
chldSftySatsYn
|
|
92
|
+
dsprSatsYn
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The response should include `form#satsChcFrm` and hidden fare/seat values such as `adltFee`, `rmnSatsNum`, `totSatsNum`, and terminal/time fields.
|
|
96
|
+
|
|
97
|
+
### 5. Temporary Seat Hold
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
POST /mrs/setPcpy.ajax
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Submit the `satsChcFrm` hidden fields plus selected seat/count fields. A successful response includes:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
MSG_CD=S0000
|
|
107
|
+
pcpyNoAll
|
|
108
|
+
satsNoAll
|
|
109
|
+
ESTM_AMT
|
|
110
|
+
DC_AMT
|
|
111
|
+
TISSU_AMT
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Treat the hold as short-lived. If testing, if the user does not proceed, or if a new seat is chosen, release it explicitly.
|
|
115
|
+
|
|
116
|
+
### 6. Release Temporary Hold
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
POST /mrs/cancPcpy.ajax
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use the same relevant form fields plus the returned `pcpyNoAll` and `satsNoAll`. A successful response returns `MSG_CD=S0000`.
|
|
123
|
+
|
|
124
|
+
## Helper Script
|
|
125
|
+
|
|
126
|
+
Use the bundled helper for KOBUS lookup and optional temporary holds:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python3 express-bus-booking/scripts/kobus_express_booking.py \
|
|
130
|
+
--depart-code 021 \
|
|
131
|
+
--arrive-code 500 \
|
|
132
|
+
--date 20260520 \
|
|
133
|
+
--select-index 1 \
|
|
134
|
+
--hold-first-seat \
|
|
135
|
+
--output-dir /tmp/kobus-hold
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For Seoul to Gwangju, the verified KOBUS route is `센트럴시티(서울)` code `021` to `광주(유·스퀘어)` code `500`. A successful hold returns `MSG_CD=S0000`, `pcpyNoAll`, `satsNoAll`, fare amounts, and saves a local auto-submit helper for the official KOBUS payment-information page. Final card entry and payment remain manual. Cancel abandoned holds with `/mrs/cancPcpy.ajax` using the saved cancel fields.
|
|
139
|
+
|
|
140
|
+
## Checkout-Entry Link Helper
|
|
141
|
+
|
|
142
|
+
A plain official checkout URL is not enough because KOBUS expects a POST body containing the selected schedule, seat, fare, and hold identifiers. The practical user-facing pattern is:
|
|
143
|
+
|
|
144
|
+
1. Create the temporary hold server-side via `setPcpy.ajax`.
|
|
145
|
+
2. Generate a short helper page that auto-submits a POST form to the official KOBUS checkout endpoint.
|
|
146
|
+
3. Send the helper link to the user.
|
|
147
|
+
4. The user completes card/payment fields manually on the official KOBUS page.
|
|
148
|
+
|
|
149
|
+
Desktop action:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
https://www.kobus.co.kr/mrs/stplcfmpym.do
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Mobile-friendly action:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
https://www.kobus.co.kr/mrs/stplcfmpym.do?keep=/mrs/pay
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The POST body should include the original seat form fields plus returned hold values such as `pcpyNoAll`, `satsNoAll`, `estmAmt`, `dcAmt`, `tissuAmt`, and `nonMbrsYn=Y` when using a non-member checkout flow.
|
|
162
|
+
|
|
163
|
+
## Mobile Redirect Caveat
|
|
164
|
+
|
|
165
|
+
KOBUS common JavaScript can redirect narrow/mobile screens to `/mblIdx.do` unless `location.href` contains a mobile-allowed path fragment such as `/mrs/pay`. In testing, posting to:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
/mrs/stplcfmpym.do?keep=/mrs/pay
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
preserved the same checkout POST body while making the final browser URL contain `/mrs/pay`, avoiding the client-side mobile-main redirect condition. Use this variant for Discord/mobile users, but still verify with the user because in-app browsers may add their own quirks.
|
|
172
|
+
|
|
173
|
+
If mobile still lands on the homepage, recommend opening the helper link in a normal external browser or desktop browser. Do not keep retrying holds indefinitely; cancel stale holds.
|
|
174
|
+
|
|
175
|
+
## Suggested Output Format
|
|
176
|
+
|
|
177
|
+
Keep candidate lists concise and actionable:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
고속버스 서울경부 → 부산 / 2026-05-09
|
|
181
|
+
1. 00:30 심야우등 / 천일고속 / 잔여 10석 / 성인 47,600원
|
|
182
|
+
2. 13:50 우등 / ...
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
When a hold/checkout helper is created, state that the next step opens the official KOBUS payment page and that payment remains manual.
|
|
186
|
+
|
|
187
|
+
For Discord/mobile, provide links as normal text links, not fenced code blocks, so the user can tap them directly.
|
|
188
|
+
|
|
189
|
+
## Common Pitfalls
|
|
190
|
+
|
|
191
|
+
1. **Mixing KOBUS and Tmoney codes.** KOBUS terminal codes are not Tmoney 시외버스 terminal codes.
|
|
192
|
+
2. **Assuming a cart exists.** KOBUS does not expose a shopping-cart style hold list in the tested web flow. The realistic UX is temporary seat hold plus checkout-entry handoff.
|
|
193
|
+
3. **Ignoring hold cleanup.** Always cancel test holds or abandoned holds with `cancPcpy.ajax`.
|
|
194
|
+
4. **Treating lookup success as payment permission.** Lookup, seat-stage entry, and temporary hold are not authorization to submit payment.
|
|
195
|
+
5. **Mobile homepage redirects.** Use the `?keep=/mrs/pay` helper action for mobile; otherwise KOBUS JS may send the user to `/mblIdx.do`.
|
|
196
|
+
6. **Browser automation too early.** Try direct HTTP first. Use browser tooling only to discover changed endpoints or verify final user-facing behavior.
|
|
197
|
+
|
|
198
|
+
## Verification Checklist
|
|
199
|
+
|
|
200
|
+
- [ ] Route/terminal codes were resolved from KOBUS, not guessed.
|
|
201
|
+
- [ ] Timetable was queried with cookies and a realistic User-Agent.
|
|
202
|
+
- [ ] Candidate output includes date, departure/arrival terminals, time, class/operator when available, fare, and remaining seats.
|
|
203
|
+
- [ ] Seat-stage response contains expected hidden fields before attempting a hold.
|
|
204
|
+
- [ ] Temporary hold success was confirmed with `MSG_CD=S0000` before sending a checkout helper link.
|
|
205
|
+
- [ ] Stale/test holds were cancelled with `cancPcpy.ajax`.
|
|
206
|
+
- [ ] Payment/card fields were left for the user unless they explicitly confirmed otherwise.
|
|
207
|
+
- [ ] Mobile helper links use the `/mrs/pay` marker variant when sent to mobile-heavy platforms.
|