@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,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gov-overseas-trip-report",
|
|
3
|
+
"description": "선관위·권익위·정보공개포털·주요 지방의회 등 검증된 공개 표면에서 공무국외출장/국외훈련 보고서·현황을 조회하고, 첨부 원문이 있으면 kordoc으로 사실·공개성·검토 신호만 구조화한다. 부정부패 판정 도구가 아니다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"browser",
|
|
6
|
+
"lookup"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: gov-overseas-trip-report\ndescription: 선관위·권익위·정보공개포털·주요 지방의회 등 검증된 공개 표면에서 공무국외출장/국외훈련 보고서·현황을 조회하고, 첨부 원문이 있으면 kordoc으로 사실·공개성·검토 신호만 구조화한다. 부정부패 판정 도구가 아니다.\nlicense: MIT\nmetadata:\n category: civic\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Han River Water Level
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
기본적으로 `https://k-skill-proxy.nomadamas.org/v1/han-river/water-level` 로 요청해서 현재 수위와 유량을 요약한다.
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- "한강대교 지금 수위 어때?"
|
|
10
|
+
- "잠수교 유량 알려줘"
|
|
11
|
+
- "1018683 관측소 현재 값 보여줘"
|
|
12
|
+
|
|
13
|
+
## Inputs
|
|
14
|
+
|
|
15
|
+
- 기본 입력: 관측소명/교량명(`stationName`)
|
|
16
|
+
- 대체 입력: 관측소코드(`stationCode`)
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- optional: `jq`
|
|
21
|
+
|
|
22
|
+
## Default path
|
|
23
|
+
|
|
24
|
+
추가 client API 레이어는 불필요하다. 그냥 프록시 서버에 HTTP 요청만 넣으면 된다.
|
|
25
|
+
|
|
26
|
+
사용자는 별도 HRFCO `ServiceKey` 를 준비할 필요가 없다. upstream key는 proxy 서버에서만 주입한다.
|
|
27
|
+
|
|
28
|
+
`KSKILL_PROXY_BASE_URL` 환경변수가 있으면 그 값을 사용하고, 없으면 기본 경로 `https://k-skill-proxy.nomadamas.org` 를 사용한다.
|
|
29
|
+
|
|
30
|
+
## Example requests
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/han-river/water-level' \
|
|
34
|
+
--data-urlencode 'stationName=한강대교'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
관측소코드로 바로 조회해도 된다.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/han-river/water-level' \
|
|
41
|
+
--data-urlencode 'stationCode=1018683'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Keep the answer compact
|
|
45
|
+
|
|
46
|
+
응답에는 아래만 먼저 정리한다.
|
|
47
|
+
|
|
48
|
+
- 관측소명 / 관측소코드
|
|
49
|
+
- 관측 시각
|
|
50
|
+
- 현재 수위(m)
|
|
51
|
+
- 현재 유량(m^3/s)
|
|
52
|
+
- 기준 수위(관심/주의/경보/심각) 중 값이 있는 항목
|
|
53
|
+
|
|
54
|
+
## Ambiguous station names
|
|
55
|
+
|
|
56
|
+
입력이 너무 넓으면 proxy 는 `ambiguous_station` 과 함께 `candidate_stations` 를 돌려준다.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/han-river/water-level' \
|
|
60
|
+
--data-urlencode 'stationName=한강'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
이때는 후보 중 하나를 골라 다시 `stationName` 또는 `stationCode` 로 조회한다.
|
|
64
|
+
|
|
65
|
+
## Detailed API paths
|
|
66
|
+
|
|
67
|
+
구현 세부는 아래 문서만 참고한다.
|
|
68
|
+
|
|
69
|
+
- `docs/features/han-river-water-level.md`
|
|
70
|
+
- `docs/features/k-skill-proxy.md`
|
|
71
|
+
|
|
72
|
+
## Failure modes
|
|
73
|
+
|
|
74
|
+
- 관측소명이 너무 넓어서 여러 관측소가 동시에 잡히는 경우
|
|
75
|
+
- 잘못된 관측소코드/관측소명으로 station lookup 이 실패하는 경우
|
|
76
|
+
- 프록시 서버에 `HRFCO_OPEN_API_KEY` 가 비어 있는 경우
|
|
77
|
+
- 실시간 자료 갱신 지연으로 최신 10분 자료가 비어 있는 경우
|
|
78
|
+
|
|
79
|
+
## Notes
|
|
80
|
+
|
|
81
|
+
- 기본 경로는 항상 `k-skill-proxy.nomadamas.org` 의 water-level endpoint 다.
|
|
82
|
+
- upstream 은 `waterlevel/info.json` 으로 관측소 메타데이터를 찾고, `waterlevel/list/10M/{WLOBSCD}.json` 으로 최신값을 조회한다.
|
|
83
|
+
- 결과는 원시자료 기반이므로 조회 시각을 함께 적는다.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "han-river-water-level",
|
|
3
|
+
"description": "한강홍수통제소 기반 현재 수위/유량을 관측소명 또는 관측소코드로 조회한다. 기본 경로는 k-skill-proxy의 han-river water-level endpoint다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"lookup"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: han-river-water-level\ndescription: 한강홍수통제소 기반 현재 수위/유량을 관측소명 또는 관측소코드로 조회한다. 기본 경로는 k-skill-proxy의 han-river water-level endpoint다.\nlicense: MIT\nmetadata:\n category: utility\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 고속도로 교통량·소통·CCTV 조회
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
- 한국도로공사 공공데이터포털(`data.ex.co.kr`)의 실시간 교통량 API로 전국 고속도로 콘존(구간)별 속도, 교통량, 소통등급(원활/서행/정체)을 조회한다.
|
|
6
|
+
- 국가교통정보센터 ITS(`openapi.its.go.kr`)의 CCTV 정보 API로 좌표 범위 내 고속도로 CCTV 이름·좌표·HLS 스트림 URL을 조회한다.
|
|
7
|
+
|
|
8
|
+
이 스킬은 **조회 전용**이다. 경로 안내, 내비게이션, 통행료 계산은 범위 밖이다.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- "지금 경부고속도로 막혀?"
|
|
13
|
+
- "서울 요금소 쪽 정체 상황 알려줘"
|
|
14
|
+
- "서해안선 상행 소통 어때?"
|
|
15
|
+
- "판교 근처 고속도로 CCTV 보여줘"
|
|
16
|
+
|
|
17
|
+
## When not to use
|
|
18
|
+
|
|
19
|
+
- 대중교통 길찾기 → `korean-transit-route`
|
|
20
|
+
- 자동차 경로/내비게이션 → `kakao-map`
|
|
21
|
+
- 시내 도로·일반국도 상세 소통 (v1은 고속도로 중심)
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- Python 3.9+ (stdlib only, 외부 패키지 없음)
|
|
26
|
+
- 사용자 API 키 **불필요** — 두 upstream 모두 공개 데모 키(`test`)로 동작함을 확인했다 (2026-07-21). 공개 엔드포인트이므로 k-skill-proxy를 경유하지 않고 직접 호출한다.
|
|
27
|
+
|
|
28
|
+
선택 환경변수 (데모 키 회수/쿼터 대비):
|
|
29
|
+
|
|
30
|
+
- `KSKILL_EXDATA_API_KEY` — data.ex.co.kr 개인 인증키 (https://data.ex.co.kr 회원가입 후 발급)
|
|
31
|
+
- `KSKILL_ITS_API_KEY` — openapi.its.go.kr 개인 인증키 (https://www.its.go.kr/opendata)
|
|
32
|
+
|
|
33
|
+
`~/.config/k-skill/secrets.env` 의 같은 이름 키도 읽는다.
|
|
34
|
+
|
|
35
|
+
## Workflow
|
|
36
|
+
|
|
37
|
+
### 1. 실시간 소통/교통량 조회
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python3 highway-traffic-status/scripts/highway_traffic.py traffic --route 경부 --text
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `--route`: 노선명 일부(`경부`, `서해안`) 또는 노선번호(`0010`)
|
|
44
|
+
- `--keyword`: 구간(콘존) 이름 키워드 (예: `서울TG`, `양재`)
|
|
45
|
+
- `--limit N`: 출력 행 수 (기본 30)
|
|
46
|
+
- `--text` 없이 실행하면 구조화 JSON을 출력한다.
|
|
47
|
+
|
|
48
|
+
JSON 출력 필드: `route_name`, `route_no`, `conzone_name`, `direction`(상행/하행), `speed_kmh`, `traffic_volume`, `travel_time_sec`, `congestion`(원활/서행/정체), `observed_at`.
|
|
49
|
+
|
|
50
|
+
### 2. CCTV 메타데이터 조회
|
|
51
|
+
|
|
52
|
+
좌표 범위(경도 `--min-x`/`--max-x`, 위도 `--min-y`/`--max-y`)가 필수다. 사용자가 지명을 말하면 대략적인 bounding box로 변환해 호출한다.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python3 highway-traffic-status/scripts/highway_traffic.py cctv \
|
|
56
|
+
--min-x 126.9 --max-x 127.2 --min-y 37.3 --max-y 37.6 --text
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- 응답의 `url`은 HLS 스트림 주소다. 스트림 재생은 사용자 환경(브라우저/플레이어) 몫이며 스킬은 메타데이터만 제공한다.
|
|
60
|
+
- `--road-type`: `ex`(고속도로, 기본) / `its`(국도) / `all`
|
|
61
|
+
|
|
62
|
+
### 3. 결과 요약 규칙
|
|
63
|
+
|
|
64
|
+
- 소통 상태는 upstream `grade`를 그대로 원활/서행/정체로 표시하고, 자체 판단을 덧붙이지 않는다.
|
|
65
|
+
- `observed_at` 기준 시각을 함께 알려준다 (실시간 스냅샷임을 명시).
|
|
66
|
+
- 운전 중 조작 금지: 사용자가 운전 중으로 보이면 음성/동승자 확인을 권한다.
|
|
67
|
+
|
|
68
|
+
## Data sources & fallback order
|
|
69
|
+
|
|
70
|
+
1. `https://data.ex.co.kr/openapi/odtraffic/trafficAmountByRealtime?key=<key>&type=json` — 전국 VDS 실시간 스냅샷. 필터(노선/구간)는 helper가 클라이언트 측에서 수행한다.
|
|
71
|
+
2. `https://openapi.its.go.kr:9443/cctvInfo?apiKey=<key>&type=ex&cctvType=1&minX=..&getType=json` — 성공 응답은 XML(문서와 달리 `getType=json`이어도 XML)이며 helper가 XML을 파싱한다.
|
|
72
|
+
|
|
73
|
+
두 표면 모두 데모 키 `test`가 유효하다. 개인 키가 환경변수에 있으면 그것을 우선 사용한다.
|
|
74
|
+
|
|
75
|
+
## Failure modes
|
|
76
|
+
|
|
77
|
+
| 상황 | 동작 |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| 빈 결과 (`result: "empty"`) | 조건에 맞는 구간/CCTV 없음 — 노선명·좌표 범위를 넓혀 재시도 안내 |
|
|
80
|
+
| 인증키 오류 (`인증키가 유효하지 않습니다` / ITS 401 resultCode 4005) | 데모 키 회수 가능성 안내, 개인 키 발급 후 환경변수 지정 안내 |
|
|
81
|
+
| upstream HTTP 오류/타임아웃 | 잠시 후 재시도 안내, 점검 가능성 언급 |
|
|
82
|
+
| JSON/XML 파싱 실패 | 차단 또는 형식 변경 가능성 — 이슈 보고 안내 |
|
|
83
|
+
| 좌표 범위 오류 | 한국 범위(경도 124~132, 위도 33~39.5) 및 min<max 검증 메시지 출력 |
|
|
84
|
+
|
|
85
|
+
## Notes
|
|
86
|
+
|
|
87
|
+
- upstream 데모 키 정책이 바뀌면 이 스킬은 개인 키(BYOK)로만 동작하게 된다. 그 시점에 proxy route 편입을 재검토한다.
|
|
88
|
+
- CCTV 스트림 URL은 시간이 지나면 만료될 수 있는 서명 URL이다. 재조회로 갱신한다.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only Korean highway traffic/CCTV helper using stdlib only.
|
|
3
|
+
|
|
4
|
+
Site-dependent access paths (discovered live 2026-07-21):
|
|
5
|
+
|
|
6
|
+
1. 한국도로공사 공공데이터포털 (data.ex.co.kr)
|
|
7
|
+
GET https://data.ex.co.kr/openapi/odtraffic/trafficAmountByRealtime
|
|
8
|
+
?key=<key>&type=json[&numOfRows=N&pageNo=N]
|
|
9
|
+
- The published demo key ``test`` works without registration and returns
|
|
10
|
+
the full nationwide VDS snapshot (콘존별 speed/교통량/등급).
|
|
11
|
+
- An arbitrary invalid key returns HTTP 200 with
|
|
12
|
+
{"code": "ERROR", "message": "인증키가 유효하지 않습니다."} — treat as typed failure.
|
|
13
|
+
|
|
14
|
+
2. 국가교통정보센터 ITS (openapi.its.go.kr:9443)
|
|
15
|
+
GET https://openapi.its.go.kr:9443/cctvInfo
|
|
16
|
+
?apiKey=<key>&type=ex&cctvType=1&minX=&maxX=&minY=&maxY=&getType=json
|
|
17
|
+
- The published demo key ``test`` works; invalid keys return HTTP 401 JSON
|
|
18
|
+
{"header": {"resultCode": 4005, ...}}.
|
|
19
|
+
- Despite getType=json the success body is XML; parse XML first and fall
|
|
20
|
+
back to the JSON error envelope.
|
|
21
|
+
|
|
22
|
+
Both surfaces work without user registration via the demo key, so per the
|
|
23
|
+
free API proxy policy this skill calls upstream directly (no k-skill-proxy
|
|
24
|
+
route). Users may override the key via env for higher quota.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import contextlib
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import pathlib
|
|
34
|
+
import sys
|
|
35
|
+
import urllib.error
|
|
36
|
+
import urllib.parse
|
|
37
|
+
import urllib.request
|
|
38
|
+
import xml.etree.ElementTree as ET
|
|
39
|
+
from typing import Any, Dict, List, Optional
|
|
40
|
+
|
|
41
|
+
EXDATA_BASE_URL = "https://data.ex.co.kr/openapi/odtraffic/trafficAmountByRealtime"
|
|
42
|
+
ITS_CCTV_BASE_URL = "https://openapi.its.go.kr:9443/cctvInfo"
|
|
43
|
+
EXDATA_DEMO_KEY = "test"
|
|
44
|
+
ITS_DEMO_KEY = "test"
|
|
45
|
+
DEFAULT_SECRETS_PATH = pathlib.Path("~/.config/k-skill/secrets.env").expanduser()
|
|
46
|
+
|
|
47
|
+
# 대한민국 근해 좌표 범위 (경도/위도)
|
|
48
|
+
KOREA_LON_RANGE = (124.0, 132.0)
|
|
49
|
+
KOREA_LAT_RANGE = (33.0, 39.5)
|
|
50
|
+
|
|
51
|
+
GRADE_LABELS = {"1": "원활", "2": "서행", "3": "정체"}
|
|
52
|
+
DIRECTION_LABELS = {"S": "상행", "E": "하행", "N": "상행", "W": "하행"}
|
|
53
|
+
|
|
54
|
+
USER_AGENT = "k-skill-highway-traffic/0.1 (+https://github.com/NomaDamas/k-skill)"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HelperError(RuntimeError):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_secrets(path: pathlib.Path) -> Dict[str, str]:
|
|
62
|
+
values: Dict[str, str] = {}
|
|
63
|
+
try:
|
|
64
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
65
|
+
except OSError:
|
|
66
|
+
return values
|
|
67
|
+
for raw in lines:
|
|
68
|
+
line = raw.strip()
|
|
69
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
70
|
+
continue
|
|
71
|
+
key, value = line.split("=", 1)
|
|
72
|
+
values[key.strip()] = value.strip().strip('"').strip("'")
|
|
73
|
+
return values
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_api_key(env_name: str, secrets_path: str) -> Optional[str]:
|
|
77
|
+
env_value = os.environ.get(env_name)
|
|
78
|
+
if env_value and env_value.strip():
|
|
79
|
+
return env_value.strip()
|
|
80
|
+
secrets = load_secrets(pathlib.Path(secrets_path).expanduser())
|
|
81
|
+
value = secrets.get(env_name)
|
|
82
|
+
return value.strip() if value and value.strip() else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_traffic_url(args: argparse.Namespace, api_key: Optional[str]) -> str:
|
|
86
|
+
query = {
|
|
87
|
+
"key": api_key or EXDATA_DEMO_KEY,
|
|
88
|
+
"type": "json",
|
|
89
|
+
}
|
|
90
|
+
return f"{EXDATA_BASE_URL}?{urllib.parse.urlencode(query)}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _validate_bbox(args: argparse.Namespace) -> None:
|
|
94
|
+
for name in ("min_x", "max_x", "min_y", "max_y"):
|
|
95
|
+
if getattr(args, name) is None:
|
|
96
|
+
raise HelperError("cctv 조회에는 --min-x/--max-x/--min-y/--max-y 좌표 범위가 필요합니다.")
|
|
97
|
+
if args.min_x >= args.max_x:
|
|
98
|
+
raise HelperError("--min-x 는 --max-x 보다 작아야 합니다.")
|
|
99
|
+
if args.min_y >= args.max_y:
|
|
100
|
+
raise HelperError("--min-y 는 --max-y 보다 작아야 합니다.")
|
|
101
|
+
if not (KOREA_LON_RANGE[0] <= args.min_x <= KOREA_LON_RANGE[1] and KOREA_LON_RANGE[0] <= args.max_x <= KOREA_LON_RANGE[1]):
|
|
102
|
+
raise HelperError(f"경도는 한국 범위({KOREA_LON_RANGE[0]}~{KOREA_LON_RANGE[1]}) 안이어야 합니다.")
|
|
103
|
+
if not (KOREA_LAT_RANGE[0] <= args.min_y <= KOREA_LAT_RANGE[1] and KOREA_LAT_RANGE[0] <= args.max_y <= KOREA_LAT_RANGE[1]):
|
|
104
|
+
raise HelperError(f"위도는 한국 범위({KOREA_LAT_RANGE[0]}~{KOREA_LAT_RANGE[1]}) 안이어야 합니다.")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def build_cctv_url(args: argparse.Namespace, api_key: Optional[str]) -> str:
|
|
108
|
+
_validate_bbox(args)
|
|
109
|
+
query = {
|
|
110
|
+
"apiKey": api_key or ITS_DEMO_KEY,
|
|
111
|
+
"type": args.road_type,
|
|
112
|
+
"cctvType": "1",
|
|
113
|
+
"minX": args.min_x,
|
|
114
|
+
"maxX": args.max_x,
|
|
115
|
+
"minY": args.min_y,
|
|
116
|
+
"maxY": args.max_y,
|
|
117
|
+
"getType": "json",
|
|
118
|
+
}
|
|
119
|
+
return f"{ITS_CCTV_BASE_URL}?{urllib.parse.urlencode(query)}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def http_get_text(url: str, timeout: int) -> str:
|
|
123
|
+
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
124
|
+
try:
|
|
125
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
126
|
+
return response.read().decode("utf-8", "replace")
|
|
127
|
+
except urllib.error.HTTPError as exc:
|
|
128
|
+
body = ""
|
|
129
|
+
with contextlib.suppress(Exception):
|
|
130
|
+
body = exc.read().decode("utf-8", "replace")
|
|
131
|
+
if exc.code == 401 or "인증키" in body:
|
|
132
|
+
raise HelperError("upstream 인증키 오류입니다. 데모 키가 회수되었을 수 있으니 개인 키 발급 후 환경변수로 지정하세요.") from exc
|
|
133
|
+
raise HelperError(f"upstream HTTP 오류: {exc.code}") from exc
|
|
134
|
+
except urllib.error.URLError as exc:
|
|
135
|
+
raise HelperError(f"upstream 접속 실패: {exc.reason}") from exc
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def http_get_json(url: str, timeout: int) -> Dict[str, Any]:
|
|
139
|
+
text = http_get_text(url, timeout)
|
|
140
|
+
try:
|
|
141
|
+
return json.loads(text)
|
|
142
|
+
except json.JSONDecodeError as exc:
|
|
143
|
+
raise HelperError("upstream 응답이 JSON이 아닙니다 (차단 또는 점검 가능성).") from exc
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def normalize_traffic(payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
147
|
+
if not isinstance(payload, dict):
|
|
148
|
+
raise HelperError("upstream 응답 형식이 올바르지 않습니다.")
|
|
149
|
+
if payload.get("code") == "ERROR":
|
|
150
|
+
message = str(payload.get("message") or "알 수 없는 upstream 오류")
|
|
151
|
+
raise HelperError(f"upstream 오류: {message}")
|
|
152
|
+
rows = payload.get("list")
|
|
153
|
+
if not isinstance(rows, list):
|
|
154
|
+
raise HelperError("upstream 응답에 교통량 목록이 없습니다.")
|
|
155
|
+
|
|
156
|
+
def to_int(value: Any) -> Optional[int]:
|
|
157
|
+
text = str(value or "").strip()
|
|
158
|
+
return int(text) if text.lstrip("-").isdigit() else None
|
|
159
|
+
|
|
160
|
+
normalized = []
|
|
161
|
+
for row in rows:
|
|
162
|
+
if not isinstance(row, dict):
|
|
163
|
+
continue
|
|
164
|
+
grade = str(row.get("grade") or "")
|
|
165
|
+
direction_code = str(row.get("updownTypeCode") or "")
|
|
166
|
+
normalized.append({
|
|
167
|
+
"route_name": str(row.get("routeName") or ""),
|
|
168
|
+
"route_no": str(row.get("routeNo") or ""),
|
|
169
|
+
"conzone_name": str(row.get("conzoneName") or ""),
|
|
170
|
+
"conzone_id": str(row.get("conzoneId") or ""),
|
|
171
|
+
"direction": DIRECTION_LABELS.get(direction_code, direction_code),
|
|
172
|
+
"speed_kmh": to_int(row.get("speed")),
|
|
173
|
+
"traffic_volume": to_int(row.get("trafficAmout")),
|
|
174
|
+
"travel_time_sec": to_int(row.get("timeAvg")),
|
|
175
|
+
"congestion": GRADE_LABELS.get(grade, grade),
|
|
176
|
+
"observed_at": f"{row.get('stdDate', '')} {row.get('stdHour', '')}".strip(),
|
|
177
|
+
})
|
|
178
|
+
return normalized
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def filter_traffic(
|
|
182
|
+
rows: List[Dict[str, Any]],
|
|
183
|
+
route: Optional[str] = None,
|
|
184
|
+
keyword: Optional[str] = None,
|
|
185
|
+
) -> List[Dict[str, Any]]:
|
|
186
|
+
result = rows
|
|
187
|
+
if route:
|
|
188
|
+
needle = route.strip()
|
|
189
|
+
result = [
|
|
190
|
+
row for row in result
|
|
191
|
+
if needle in row["route_name"] or needle == row["route_no"]
|
|
192
|
+
]
|
|
193
|
+
if keyword:
|
|
194
|
+
needle = keyword.strip()
|
|
195
|
+
result = [row for row in result if needle in row["conzone_name"]]
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def normalize_cctv(body: str) -> List[Dict[str, Any]]:
|
|
200
|
+
text = body.strip()
|
|
201
|
+
if text.startswith("{"):
|
|
202
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
203
|
+
payload = json.loads(text)
|
|
204
|
+
header = payload.get("header") if isinstance(payload, dict) else None
|
|
205
|
+
message = str((header or {}).get("resultMsg") or "upstream 오류")
|
|
206
|
+
raise HelperError(f"upstream 오류: {message} (인증키 확인 필요)")
|
|
207
|
+
try:
|
|
208
|
+
root = ET.fromstring(text)
|
|
209
|
+
except ET.ParseError as exc:
|
|
210
|
+
raise HelperError("upstream CCTV 응답을 해석할 수 없습니다 (차단 또는 형식 변경 가능성).") from exc
|
|
211
|
+
|
|
212
|
+
cameras = []
|
|
213
|
+
for item in root.iter("data"):
|
|
214
|
+
def field(tag: str) -> str:
|
|
215
|
+
node = item.find(tag)
|
|
216
|
+
return (node.text or "").strip() if node is not None and node.text else ""
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
lat = float(field("coordy"))
|
|
220
|
+
lon = float(field("coordx"))
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
cameras.append({
|
|
224
|
+
"name": field("cctvname"),
|
|
225
|
+
"url": field("cctvurl"),
|
|
226
|
+
"format": field("cctvformat"),
|
|
227
|
+
"lat": lat,
|
|
228
|
+
"lon": lon,
|
|
229
|
+
})
|
|
230
|
+
if not cameras and root.tag != "response":
|
|
231
|
+
raise HelperError("upstream CCTV 응답에 데이터가 없습니다.")
|
|
232
|
+
return cameras
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def render_traffic_text(rows: List[Dict[str, Any]]) -> str:
|
|
236
|
+
if not rows:
|
|
237
|
+
return "조건에 맞는 구간이 없습니다."
|
|
238
|
+
lines = []
|
|
239
|
+
for row in rows:
|
|
240
|
+
speed = f"{row['speed_kmh']}km/h" if row["speed_kmh"] is not None else "속도 미상"
|
|
241
|
+
lines.append(
|
|
242
|
+
f"[{row['route_name']} {row['direction']}] {row['conzone_name']}: "
|
|
243
|
+
f"{row['congestion']} · {speed} · 교통량 {row['traffic_volume']} (기준 {row['observed_at']})"
|
|
244
|
+
)
|
|
245
|
+
return "\n".join(lines)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_args(argv: List[str]) -> argparse.Namespace:
|
|
249
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
250
|
+
common.add_argument("--secrets-path", default=str(DEFAULT_SECRETS_PATH))
|
|
251
|
+
common.add_argument("--timeout", type=int, default=30)
|
|
252
|
+
common.add_argument("--text", action="store_true", help="사람용 요약 출력")
|
|
253
|
+
|
|
254
|
+
parser = argparse.ArgumentParser(description="한국 고속도로 실시간 소통/교통량/CCTV 조회")
|
|
255
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
256
|
+
|
|
257
|
+
traffic = subparsers.add_parser("traffic", parents=[common], help="콘존별 실시간 속도/교통량/소통등급")
|
|
258
|
+
traffic.add_argument("--route", help="노선명 일부 또는 노선번호 (예: 경부, 0010)")
|
|
259
|
+
traffic.add_argument("--keyword", help="구간(콘존) 이름 키워드 (예: 서울TG)")
|
|
260
|
+
traffic.add_argument("--limit", type=int, default=30, help="출력 행 수 (기본 30)")
|
|
261
|
+
|
|
262
|
+
cctv = subparsers.add_parser("cctv", parents=[common], help="좌표 범위 내 고속도로 CCTV 메타데이터")
|
|
263
|
+
cctv.add_argument("--min-x", type=float, help="최소 경도")
|
|
264
|
+
cctv.add_argument("--max-x", type=float, help="최대 경도")
|
|
265
|
+
cctv.add_argument("--min-y", type=float, help="최소 위도")
|
|
266
|
+
cctv.add_argument("--max-y", type=float, help="최대 위도")
|
|
267
|
+
cctv.add_argument("--road-type", default="ex", choices=["ex", "its", "all"], help="도로 유형 (기본 ex=고속도로)")
|
|
268
|
+
|
|
269
|
+
return parser.parse_args(argv)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def run(argv: List[str]) -> int:
|
|
273
|
+
args = parse_args(argv)
|
|
274
|
+
try:
|
|
275
|
+
if args.command == "traffic":
|
|
276
|
+
api_key = resolve_api_key("KSKILL_EXDATA_API_KEY", args.secrets_path)
|
|
277
|
+
url = build_traffic_url(args, api_key)
|
|
278
|
+
payload = http_get_json(url, args.timeout)
|
|
279
|
+
rows = normalize_traffic(payload)
|
|
280
|
+
matched = filter_traffic(rows, route=args.route, keyword=args.keyword)
|
|
281
|
+
visible = matched[: max(args.limit, 0)]
|
|
282
|
+
if args.text:
|
|
283
|
+
print(render_traffic_text(visible))
|
|
284
|
+
else:
|
|
285
|
+
print(json.dumps({
|
|
286
|
+
"result": "ok" if visible else "empty",
|
|
287
|
+
"total_matched": len(matched),
|
|
288
|
+
"rows": visible,
|
|
289
|
+
"source": "data.ex.co.kr trafficAmountByRealtime",
|
|
290
|
+
}, ensure_ascii=False, indent=2))
|
|
291
|
+
return 0
|
|
292
|
+
|
|
293
|
+
api_key = resolve_api_key("KSKILL_ITS_API_KEY", args.secrets_path)
|
|
294
|
+
url = build_cctv_url(args, api_key)
|
|
295
|
+
body = http_get_text(url, args.timeout)
|
|
296
|
+
cameras = normalize_cctv(body)
|
|
297
|
+
if args.text:
|
|
298
|
+
if not cameras:
|
|
299
|
+
print("좌표 범위 안에 CCTV가 없습니다.")
|
|
300
|
+
for cam in cameras:
|
|
301
|
+
print(f"{cam['name']} ({cam['format']}) — {cam['url']}")
|
|
302
|
+
else:
|
|
303
|
+
print(json.dumps({
|
|
304
|
+
"result": "ok" if cameras else "empty",
|
|
305
|
+
"cameras": cameras,
|
|
306
|
+
"source": "openapi.its.go.kr cctvInfo",
|
|
307
|
+
}, ensure_ascii=False, indent=2))
|
|
308
|
+
return 0
|
|
309
|
+
except HelperError as exc:
|
|
310
|
+
print(str(exc), file=sys.stderr)
|
|
311
|
+
return 1
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
sys.exit(run(sys.argv[1:]))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "highway-traffic-status",
|
|
3
|
+
"description": "한국도로공사·국가교통정보센터 공개 API로 고속도로 실시간 소통(구간별 속도/교통량/정체 등급)과 CCTV 스트림 메타데이터를 조회한다. Use when the user asks 고속도로 정체, 교통 상황, 노선/구간 소통, 고속도로 CCTV. Not for 대중교통 길찾기 or 자동차 경로 안내.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"browser",
|
|
8
|
+
"lookup"
|
|
9
|
+
],
|
|
10
|
+
"frontmatter": "name: highway-traffic-status\ndescription: 한국도로공사·국가교통정보센터 공개 API로 고속도로 실시간 소통(구간별 속도/교통량/정체 등급)과 CCTV 스트림 메타데이터를 조회한다. Use when the user asks 고속도로 정체, 교통 상황, 노선/구간 소통, 고속도로 CCTV. Not for 대중교통 길찾기 or 자동차 경로 안내.\nlicense: MIT\nmetadata:\n category: transit\n locale: ko-KR\n phase: v1"
|
|
11
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# 하이패스 영수증 발급
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
공식 하이패스 홈페이지(`https://www.hipass.co.kr`)에서 **이미 로그인된 브라우저 세션**을 재사용해:
|
|
6
|
+
|
|
7
|
+
- 사용내역 조회
|
|
8
|
+
- 특정 행 선택
|
|
9
|
+
- 영수증 팝업/출력 화면 진입
|
|
10
|
+
- 세션 만료 감지 후 재로그인 안내
|
|
11
|
+
|
|
12
|
+
까지를 반자동으로 돕는다.
|
|
13
|
+
|
|
14
|
+
## Hard limits
|
|
15
|
+
|
|
16
|
+
- **로그인은 반드시 사용자가 직접 해야 한다.**
|
|
17
|
+
- 이 스킬은 **로그인된 세션에서만** 동작한다.
|
|
18
|
+
- ID/PW, 인증코드, OTP, 공동인증서 절차를 자동 입력하지 않는다.
|
|
19
|
+
- `JSESSIONID` 쿠키만 저장해 장시간 재사용하는 방식은 지원하지 않는다.
|
|
20
|
+
- 권장 세션 형태는 **Playwright persistent context** 또는 Chrome `user-data-dir` / remote-debugging 재사용이다.
|
|
21
|
+
- **세션이 만료되면 즉시 중단하고 다시 로그인**해야 한다.
|
|
22
|
+
|
|
23
|
+
## Why this design
|
|
24
|
+
|
|
25
|
+
현재 공개 페이지 기준으로:
|
|
26
|
+
|
|
27
|
+
- 로그인 페이지와 메인 페이지에 `session_time=1200` 이 노출된다.
|
|
28
|
+
- 세션 연장은 `/comm/sessionCheck.do`
|
|
29
|
+
- 세션 종료는 `/comm//sessionout.do`
|
|
30
|
+
- 미로그인/세션 종료 보호 응답은 `mgs_type 11/12` 후 `/comm/lginpg.do` 로 이동한다.
|
|
31
|
+
- 사용내역 조회는 `/usepculr/InitUsePculrTabSearch.do` → `hpForm` submit → `/usepculr/UsePculrTabSearchList.do` 흐름이다.
|
|
32
|
+
- 영수증은 `/usepculr/UsePculrReceiptPrint.do` 팝업 진입으로 이어진다.
|
|
33
|
+
|
|
34
|
+
즉 v1은 **“로그인된 Chrome 세션 재사용”** 이 가장 현실적이다.
|
|
35
|
+
|
|
36
|
+
## Prerequisites
|
|
37
|
+
|
|
38
|
+
- macOS 또는 Chrome 실행 가능한 환경
|
|
39
|
+
- `npm install hipass-receipt` 또는 이 레포에서 `npm install` (`playwright-core` 포함)
|
|
40
|
+
- Chrome 원격 디버깅 포트 사용 가능
|
|
41
|
+
- 사용자가 직접 하이패스 로그인 가능
|
|
42
|
+
|
|
43
|
+
## Workflow
|
|
44
|
+
|
|
45
|
+
### 1. 전용 Chrome 프로필로 로그인 브라우저를 띄운다
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
hipass-receipt chrome-command --profile-dir "$HOME/.cache/k-skill/hipass-chrome" --debugging-port 9222
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
위 명령이 출력한 Chrome 실행문으로 브라우저를 띄운 뒤, 사용자가 직접 `https://www.hipass.co.kr/comm/lginpg.do` 에 로그인한다.
|
|
52
|
+
|
|
53
|
+
### 2. 사용내역을 조회한다
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
hipass-receipt list \
|
|
57
|
+
--cdp-url http://127.0.0.1:9222 \
|
|
58
|
+
--start-date 2026-04-01 \
|
|
59
|
+
--end-date 2026-04-07 \
|
|
60
|
+
--page-size 30
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- 카드사/암호화 카드번호를 알고 있으면 `--encrypted-card-number` 등으로 더 좁힐 수 있다.
|
|
64
|
+
- `--encrypted-card-number` 는 CLI의 기존 `--ecd-no` 별칭이다.
|
|
65
|
+
- 결과 JSON에서 `rowIndex` 를 확인한다.
|
|
66
|
+
|
|
67
|
+
### 3. 특정 row의 영수증 팝업을 연다
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
hipass-receipt receipt \
|
|
71
|
+
--cdp-url http://127.0.0.1:9222 \
|
|
72
|
+
--start-date 2026-04-01 \
|
|
73
|
+
--end-date 2026-04-07 \
|
|
74
|
+
--row-index 1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- 선택한 행의 `영수증`/`출력` control 을 클릭한다.
|
|
78
|
+
- 팝업이 열리면 URL/title 을 반환한다.
|
|
79
|
+
|
|
80
|
+
## Response policy
|
|
81
|
+
|
|
82
|
+
- “로그인 필수”, “세션 만료 시 재로그인 필요”를 항상 명확히 적는다.
|
|
83
|
+
- 하이패스 계정 비밀번호를 받아 저장하거나 새 env var를 만들지 않는다.
|
|
84
|
+
- 세션이 만료됐으면 즉시 실패시키고 `/comm/lginpg.do` 재로그인만 안내한다.
|
|
85
|
+
- v1 범위를 넘어서는 완전 무인 로그인 유지/백그라운드 재인증은 약속하지 않는다.
|
|
86
|
+
|
|
87
|
+
## Verification
|
|
88
|
+
|
|
89
|
+
- 자동 검증: fixture 기반 query/parser/session-detection 테스트
|
|
90
|
+
- smoke 검증: `hipass-receipt fixture-demo --fixture ...`
|
|
91
|
+
- 최종 실서비스 검증: **로그인된 세션으로 수동 smoke test**
|
|
92
|
+
|
|
93
|
+
## Done when
|
|
94
|
+
|
|
95
|
+
- 로그인된 세션으로 사용내역 조회가 가능하다.
|
|
96
|
+
- 특정 row를 선택해 영수증 팝업 진입을 시도할 수 있다.
|
|
97
|
+
- 세션 종료 응답을 감지하면 재로그인을 요구한다.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hipass-receipt",
|
|
3
|
+
"description": "공식 하이패스 홈페이지에서 사용자가 직접 로그인한 Chrome 세션을 재사용해 사용내역 조회와 영수증 팝업 진입을 돕는다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"vault",
|
|
6
|
+
"browser",
|
|
7
|
+
"action:account"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: hipass-receipt\ndescription: 공식 하이패스 홈페이지에서 사용자가 직접 로그인한 Chrome 세션을 재사용해 사용내역 조회와 영수증 팝업 진입을 돕는다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: transport\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|