@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,424 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""K-Startup (data.go.kr 15125364) CLI helper for the kstartup-search skill.
|
|
3
|
+
|
|
4
|
+
์กฐํ ์ ์ฉ. ์ผ๋ฐ ํธ์ถ์ k-skill-proxy ๊ฒฝ์ , `--direct` ๋ ์ฌ์ฉ์ API ํค๋ก ์ง์ ํธ์ถ.
|
|
5
|
+
stdlib only (urllib, json, argparse, ssl).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import datetime
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import ssl
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
21
|
+
PROXY_DOWN_MSG = "์ค์ ๋ k-skill-proxy ์๋ฒ๊ฐ ์๋ตํ์ง ์์ต๋๋ค. ์ ์ ํ ์ฌ์๋ํ๊ฑฐ๋ ์ด์์์๊ฒ ๋ฌธ์ํ์ธ์."
|
|
22
|
+
PROXY_KEY_NOT_CONFIGURED_MSG = "k-skill-proxy์ ํ์ํ API ํค๊ฐ ์ค์ ๋์ด ์์ง ์์ต๋๋ค. ์ด์์์๊ฒ ๋ฌธ์ํ์ธ์."
|
|
23
|
+
KSTARTUP_UPSTREAM_BASE_URL = "https://apis.data.go.kr/B552735/kisedKstartupService01"
|
|
24
|
+
DEFAULT_SECRETS_PATH = os.path.expanduser("~/.config/k-skill/secrets.env")
|
|
25
|
+
|
|
26
|
+
OPERATIONS: Dict[str, Dict[str, Any]] = {
|
|
27
|
+
"business-info": {
|
|
28
|
+
"path": "getBusinessInformation01",
|
|
29
|
+
"allowed": ("biz_category_cd", "supt_biz_titl_nm", "biz_yr"),
|
|
30
|
+
},
|
|
31
|
+
"announcements": {
|
|
32
|
+
"path": "getAnnouncementInformation01",
|
|
33
|
+
"allowed": (
|
|
34
|
+
"intg_pbanc_yn", "intg_pbanc_biz_nm", "biz_pbanc_nm",
|
|
35
|
+
"supt_biz_clsfc", "aply_trgt_ctnt", "supt_regin",
|
|
36
|
+
"pbanc_rcpt_bgng_dt", "pbanc_rcpt_end_dt",
|
|
37
|
+
"aply_trgt", "biz_enyy", "biz_trgt_age", "prfn_matr",
|
|
38
|
+
"rcrt_prgs_yn",
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
"contents": {
|
|
42
|
+
"path": "getContentInformation01",
|
|
43
|
+
"allowed": ("clss_cd", "titl_nm"),
|
|
44
|
+
},
|
|
45
|
+
"statistics": {
|
|
46
|
+
"path": "getStatisticalInformation01",
|
|
47
|
+
"allowed": ("titl_nm", "file_nm"),
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
YN_FIELDS = {"intg_pbanc_yn", "rcrt_prgs_yn"}
|
|
52
|
+
DATE_FIELDS = {"pbanc_rcpt_bgng_dt", "pbanc_rcpt_end_dt"}
|
|
53
|
+
|
|
54
|
+
# Fields where the K-Startup upstream is observed to ignore the server-side
|
|
55
|
+
# filter and return non-matching rows. SKILL.md L121 promises that the helper
|
|
56
|
+
# re-applies these filters on the client side after receiving the response.
|
|
57
|
+
#
|
|
58
|
+
# - supt_regin: upstream returns mixed regions even when supt_regin is set.
|
|
59
|
+
# - aply_trgt: upstream returns rows whose aply_trgt does not contain the
|
|
60
|
+
# requested target (e.g. asking for "์๋น์ฐฝ์
์" returns rows
|
|
61
|
+
# with only "์ผ๋ฐ์ธ,์ผ๋ฐ๊ธฐ์
").
|
|
62
|
+
# - biz_enyy: upstream returns rows whose biz_enyy does not include the
|
|
63
|
+
# requested founding period bucket.
|
|
64
|
+
#
|
|
65
|
+
# Matching policy: substring match against the comma-separated list inside
|
|
66
|
+
# each row's field. Multiple requested values (comma-separated by the user)
|
|
67
|
+
# are AND-joined: every requested token must appear somewhere in the row.
|
|
68
|
+
# This mirrors how the K-Startup web UI narrows results.
|
|
69
|
+
CLIENT_FILTER_FIELDS = {"supt_regin", "aply_trgt", "biz_enyy"}
|
|
70
|
+
|
|
71
|
+
REGION_SHORTNAME = {
|
|
72
|
+
"์์ธํน๋ณ์": "์์ธ", "์์ธ์": "์์ธ", "์์ธ": "์์ธ",
|
|
73
|
+
"๋ถ์ฐ๊ด์ญ์": "๋ถ์ฐ", "๋ถ์ฐ์": "๋ถ์ฐ", "๋ถ์ฐ": "๋ถ์ฐ",
|
|
74
|
+
"๋๊ตฌ๊ด์ญ์": "๋๊ตฌ", "๋๊ตฌ์": "๋๊ตฌ", "๋๊ตฌ": "๋๊ตฌ",
|
|
75
|
+
"์ธ์ฒ๊ด์ญ์": "์ธ์ฒ", "์ธ์ฒ์": "์ธ์ฒ", "์ธ์ฒ": "์ธ์ฒ",
|
|
76
|
+
"๊ด์ฃผ๊ด์ญ์": "๊ด์ฃผ", "๊ด์ฃผ์": "๊ด์ฃผ", "๊ด์ฃผ": "๊ด์ฃผ",
|
|
77
|
+
"๋์ ๊ด์ญ์": "๋์ ", "๋์ ์": "๋์ ", "๋์ ": "๋์ ",
|
|
78
|
+
"์ธ์ฐ๊ด์ญ์": "์ธ์ฐ", "์ธ์ฐ์": "์ธ์ฐ", "์ธ์ฐ": "์ธ์ฐ",
|
|
79
|
+
"์ธ์ข
ํน๋ณ์์น์": "์ธ์ข
", "์ธ์ข
์": "์ธ์ข
", "์ธ์ข
": "์ธ์ข
",
|
|
80
|
+
"๊ฒฝ๊ธฐ๋": "๊ฒฝ๊ธฐ", "๊ฒฝ๊ธฐ": "๊ฒฝ๊ธฐ",
|
|
81
|
+
"๊ฐ์ํน๋ณ์์น๋": "๊ฐ์", "๊ฐ์๋": "๊ฐ์", "๊ฐ์": "๊ฐ์",
|
|
82
|
+
"์ถฉ์ฒญ๋ถ๋": "์ถฉ๋ถ", "์ถฉ๋ถ": "์ถฉ๋ถ",
|
|
83
|
+
"์ถฉ์ฒญ๋จ๋": "์ถฉ๋จ", "์ถฉ๋จ": "์ถฉ๋จ",
|
|
84
|
+
"์ ๋ถํน๋ณ์์น๋": "์ ๋ถ", "์ ๋ผ๋ถ๋": "์ ๋ถ", "์ ๋ถ": "์ ๋ถ",
|
|
85
|
+
"์ ๋ผ๋จ๋": "์ ๋จ", "์ ๋จ": "์ ๋จ",
|
|
86
|
+
"๊ฒฝ์๋ถ๋": "๊ฒฝ๋ถ", "๊ฒฝ๋ถ": "๊ฒฝ๋ถ",
|
|
87
|
+
"๊ฒฝ์๋จ๋": "๊ฒฝ๋จ", "๊ฒฝ๋จ": "๊ฒฝ๋จ",
|
|
88
|
+
"์ ์ฃผํน๋ณ์์น๋": "์ ์ฃผ", "์ ์ฃผ๋": "์ ์ฃผ", "์ ์ฃผ": "์ ์ฃผ",
|
|
89
|
+
"์ ๊ตญ": "์ ๊ตญ",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class HelperError(RuntimeError):
|
|
94
|
+
"""User-facing CLI error."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_secrets(path: str = DEFAULT_SECRETS_PATH) -> Dict[str, str]:
|
|
98
|
+
"""Read dotenv-like secrets file. Returns {} if missing."""
|
|
99
|
+
data: Dict[str, str] = {}
|
|
100
|
+
if not os.path.exists(path):
|
|
101
|
+
return data
|
|
102
|
+
try:
|
|
103
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
104
|
+
for raw_line in fh:
|
|
105
|
+
line = raw_line.strip()
|
|
106
|
+
if not line or line.startswith("#"):
|
|
107
|
+
continue
|
|
108
|
+
if "=" not in line:
|
|
109
|
+
continue
|
|
110
|
+
key, _, value = line.partition("=")
|
|
111
|
+
key = key.strip()
|
|
112
|
+
value = value.strip()
|
|
113
|
+
if value.startswith('"') and value.endswith('"') and len(value) >= 2:
|
|
114
|
+
value = value[1:-1]
|
|
115
|
+
if value.startswith("'") and value.endswith("'") and len(value) >= 2:
|
|
116
|
+
value = value[1:-1]
|
|
117
|
+
if key:
|
|
118
|
+
data[key] = value
|
|
119
|
+
except OSError:
|
|
120
|
+
return data
|
|
121
|
+
return data
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_api_key(args: argparse.Namespace) -> Optional[str]:
|
|
125
|
+
"""`--direct` ์ ์ฉ API ํค ํด์. env > secrets file ์์."""
|
|
126
|
+
env_key = os.environ.get("KSKILL_KSTARTUP_API_KEY") or os.environ.get("DATA_GO_KR_API_KEY")
|
|
127
|
+
if env_key:
|
|
128
|
+
return env_key.strip() or None
|
|
129
|
+
secrets = load_secrets(args.secrets_path or DEFAULT_SECRETS_PATH)
|
|
130
|
+
return (secrets.get("KSKILL_KSTARTUP_API_KEY") or secrets.get("DATA_GO_KR_API_KEY") or "").strip() or None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_yyyymmdd(value: str, field: str) -> str:
|
|
134
|
+
digits = "".join(c for c in value if c.isdigit())
|
|
135
|
+
if len(digits) != 8:
|
|
136
|
+
raise HelperError(f"{field} must be YYYYMMDD (got: {value!r})")
|
|
137
|
+
year = int(digits[0:4])
|
|
138
|
+
month = int(digits[4:6])
|
|
139
|
+
day = int(digits[6:8])
|
|
140
|
+
try:
|
|
141
|
+
datetime.date(year, month, day)
|
|
142
|
+
except ValueError as exc:
|
|
143
|
+
raise HelperError(f"{field} must be a valid YYYYMMDD date (got: {value!r})") from exc
|
|
144
|
+
return digits
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_query(args: argparse.Namespace, operation: str) -> Dict[str, Any]:
|
|
148
|
+
if operation not in OPERATIONS:
|
|
149
|
+
raise HelperError(f"Unknown operation: {operation}")
|
|
150
|
+
if args.page < 1:
|
|
151
|
+
raise HelperError("--page must be >= 1")
|
|
152
|
+
if args.per_page < 1 or args.per_page > 100:
|
|
153
|
+
raise HelperError("--per-page must be in [1, 100]")
|
|
154
|
+
|
|
155
|
+
query: Dict[str, Any] = {
|
|
156
|
+
"page": args.page,
|
|
157
|
+
"perPage": args.per_page,
|
|
158
|
+
"returnType": "json",
|
|
159
|
+
}
|
|
160
|
+
for field in OPERATIONS[operation]["allowed"]:
|
|
161
|
+
attr = field.lower()
|
|
162
|
+
raw = getattr(args, attr, None)
|
|
163
|
+
if raw is None or str(raw).strip() == "":
|
|
164
|
+
continue
|
|
165
|
+
value = str(raw).strip()
|
|
166
|
+
if field in DATE_FIELDS:
|
|
167
|
+
value = validate_yyyymmdd(value, field)
|
|
168
|
+
elif field in YN_FIELDS:
|
|
169
|
+
upper = value.upper()
|
|
170
|
+
if upper not in {"Y", "N"}:
|
|
171
|
+
raise HelperError(f"{field} must be Y or N (got: {value!r})")
|
|
172
|
+
value = upper
|
|
173
|
+
elif field == "biz_yr":
|
|
174
|
+
if not (len(value) == 4 and value.isdigit()):
|
|
175
|
+
raise HelperError(f"biz_yr must be 4 digits (got: {value!r})")
|
|
176
|
+
query[field] = value
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
operation == "announcements"
|
|
180
|
+
and query.get("pbanc_rcpt_bgng_dt")
|
|
181
|
+
and query.get("pbanc_rcpt_end_dt")
|
|
182
|
+
and query["pbanc_rcpt_bgng_dt"] > query["pbanc_rcpt_end_dt"]
|
|
183
|
+
):
|
|
184
|
+
raise HelperError("pbanc_rcpt_bgng_dt must be <= pbanc_rcpt_end_dt")
|
|
185
|
+
return query
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def encode_query(query: Dict[str, Any]) -> str:
|
|
189
|
+
pairs: List[Tuple[str, str]] = [(k, str(v)) for k, v in query.items()]
|
|
190
|
+
return urllib.parse.urlencode(pairs, doseq=False, safe="")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_url(operation: str, query: Dict[str, Any], *, direct: bool, api_key: Optional[str], proxy_base_url: str) -> str:
|
|
194
|
+
if direct:
|
|
195
|
+
if not api_key:
|
|
196
|
+
raise HelperError(
|
|
197
|
+
"KSKILL_KSTARTUP_API_KEY (๋๋ DATA_GO_KR_API_KEY) ๊ฐ ์์ต๋๋ค. "
|
|
198
|
+
"๊ณต๊ณต๋ฐ์ดํฐํฌํธ 15125364 ํ์ฉ์ ์ฒญ ํ ํค๋ฅผ ๋ฐ๊ธ๋ฐ์ ํ๊ฒฝ๋ณ์๋ ~/.config/k-skill/secrets.env ์ ๋์ธ์."
|
|
199
|
+
)
|
|
200
|
+
path = OPERATIONS[operation]["path"]
|
|
201
|
+
with_key = dict(query)
|
|
202
|
+
with_key["ServiceKey"] = api_key
|
|
203
|
+
return f"{KSTARTUP_UPSTREAM_BASE_URL}/{path}?{encode_query(with_key)}"
|
|
204
|
+
base = proxy_base_url.rstrip("/")
|
|
205
|
+
return f"{base}/v1/kstartup/{operation}?{encode_query(query)}"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def is_proxy_not_configured_body(body: str) -> bool:
|
|
209
|
+
try:
|
|
210
|
+
payload = json.loads(body)
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
return False
|
|
213
|
+
return isinstance(payload, dict) and payload.get("error") == "upstream_not_configured"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def http_get(url: str, *, timeout: int, via_proxy: bool = False) -> Tuple[int, str, str]:
|
|
217
|
+
headers = {
|
|
218
|
+
"accept": "application/json",
|
|
219
|
+
"user-agent": "k-skill/kstartup-search",
|
|
220
|
+
}
|
|
221
|
+
request = urllib.request.Request(url, headers=headers, method="GET")
|
|
222
|
+
context = ssl.create_default_context()
|
|
223
|
+
try:
|
|
224
|
+
with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
|
|
225
|
+
body = response.read().decode("utf-8", errors="replace")
|
|
226
|
+
return response.status, response.headers.get("content-type", ""), body
|
|
227
|
+
except urllib.error.HTTPError as exc:
|
|
228
|
+
body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
|
|
229
|
+
if exc.code == 503 and is_proxy_not_configured_body(body):
|
|
230
|
+
raise HelperError(PROXY_KEY_NOT_CONFIGURED_MSG) from exc
|
|
231
|
+
return exc.code, exc.headers.get("content-type", "") if exc.headers else "", body
|
|
232
|
+
except urllib.error.URLError as exc:
|
|
233
|
+
if not via_proxy:
|
|
234
|
+
raise HelperError(f"network error: {exc.reason}") from exc
|
|
235
|
+
raise HelperError(f"{PROXY_DOWN_MSG} (์์ธ: {exc.reason})") from exc
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _normalise_filter_token(field: str, token: str) -> str:
|
|
239
|
+
if field == "supt_regin":
|
|
240
|
+
return REGION_SHORTNAME.get(token, token)
|
|
241
|
+
return token
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _row_matches_token(row: Dict[str, Any], field: str, token: str) -> bool:
|
|
245
|
+
raw = row.get(field)
|
|
246
|
+
if raw is None:
|
|
247
|
+
return False
|
|
248
|
+
haystack = str(raw)
|
|
249
|
+
needle = _normalise_filter_token(field, token)
|
|
250
|
+
return needle in haystack
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _row_matches_field(row: Dict[str, Any], field: str, requested: str) -> bool:
|
|
254
|
+
tokens = [t.strip() for t in requested.split(",") if t.strip()]
|
|
255
|
+
if not tokens:
|
|
256
|
+
return True
|
|
257
|
+
return all(_row_matches_token(row, field, token) for token in tokens)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def apply_client_filters(
|
|
261
|
+
payload: Dict[str, Any],
|
|
262
|
+
args: argparse.Namespace,
|
|
263
|
+
operation: str,
|
|
264
|
+
) -> Dict[str, Any]:
|
|
265
|
+
if operation != "announcements":
|
|
266
|
+
return payload
|
|
267
|
+
requested: Dict[str, str] = {}
|
|
268
|
+
for field in CLIENT_FILTER_FIELDS:
|
|
269
|
+
value = getattr(args, field, None)
|
|
270
|
+
if value is None:
|
|
271
|
+
continue
|
|
272
|
+
text = str(value).strip()
|
|
273
|
+
if text:
|
|
274
|
+
requested[field] = text
|
|
275
|
+
if not requested:
|
|
276
|
+
return payload
|
|
277
|
+
data = payload.get("data")
|
|
278
|
+
if not isinstance(data, list):
|
|
279
|
+
return payload
|
|
280
|
+
upstream_count = len(data)
|
|
281
|
+
filtered = [
|
|
282
|
+
row for row in data
|
|
283
|
+
if isinstance(row, dict)
|
|
284
|
+
and all(_row_matches_field(row, field, value) for field, value in requested.items())
|
|
285
|
+
]
|
|
286
|
+
payload["data"] = filtered
|
|
287
|
+
payload["currentCount"] = len(filtered)
|
|
288
|
+
payload["client_filter"] = {
|
|
289
|
+
"fields": requested,
|
|
290
|
+
"upstream_returned": upstream_count,
|
|
291
|
+
"after_filter": len(filtered),
|
|
292
|
+
"note": "Applied after upstream response because K-Startup ignores some server-side filters.",
|
|
293
|
+
}
|
|
294
|
+
return payload
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def summarise(operation: str, payload: Dict[str, Any]) -> str:
|
|
298
|
+
items: Iterable[Dict[str, Any]] = []
|
|
299
|
+
if isinstance(payload, dict):
|
|
300
|
+
data = payload.get("data") or payload.get("items")
|
|
301
|
+
if isinstance(data, list):
|
|
302
|
+
items = data
|
|
303
|
+
elif isinstance(payload.get("response"), dict):
|
|
304
|
+
response = payload["response"]
|
|
305
|
+
body = response.get("body") or {}
|
|
306
|
+
items = body.get("items") or []
|
|
307
|
+
items = list(items or [])
|
|
308
|
+
if not items:
|
|
309
|
+
return "[summary] ๋งค์นญ๋๋ ํญ๋ชฉ์ด ์์ต๋๋ค. ํํฐ๋ฅผ ์ํํ๊ฑฐ๋ ํ์ด์ง๋ฅผ ๋๊ธฐ์ธ์."
|
|
310
|
+
lines = [f"[summary] operation={operation} count={len(items)} (page={payload.get('query', {}).get('page', payload.get('page'))} perPage={payload.get('query', {}).get('perPage', payload.get('perPage'))})"]
|
|
311
|
+
for index, item in enumerate(items, start=1):
|
|
312
|
+
title = (
|
|
313
|
+
item.get("biz_pbanc_nm")
|
|
314
|
+
or item.get("supt_biz_titl_nm")
|
|
315
|
+
or item.get("titl_nm")
|
|
316
|
+
or item.get("intg_pbanc_biz_nm")
|
|
317
|
+
or "(์ ๋ชฉ ์์)"
|
|
318
|
+
)
|
|
319
|
+
region = item.get("supt_regin") or item.get("biz_category_cd") or item.get("clss_cd") or ""
|
|
320
|
+
period = ""
|
|
321
|
+
if item.get("pbanc_rcpt_bgng_dt") or item.get("pbanc_rcpt_end_dt"):
|
|
322
|
+
period = f" {item.get('pbanc_rcpt_bgng_dt','?')} ~ {item.get('pbanc_rcpt_end_dt','?')}"
|
|
323
|
+
url = item.get("detl_pg_url") or ""
|
|
324
|
+
lines.append(f" {index:>2}. {title} {region}{period}")
|
|
325
|
+
if url:
|
|
326
|
+
lines.append(f" โ {url}")
|
|
327
|
+
return "\n".join(lines)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _add_filter_args(parser: argparse.ArgumentParser, operation: str) -> None:
|
|
331
|
+
allowed = OPERATIONS[operation]["allowed"]
|
|
332
|
+
for field in allowed:
|
|
333
|
+
flag = "--" + field.replace("_", "-").lower()
|
|
334
|
+
parser.add_argument(flag, dest=field.lower(), default=None,
|
|
335
|
+
help=f"K-Startup field: {field}")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def make_parser() -> argparse.ArgumentParser:
|
|
339
|
+
parser = argparse.ArgumentParser(
|
|
340
|
+
prog="run_kstartup.py",
|
|
341
|
+
description="์ฐฝ์
์งํฅ์ K-Startup Open API (data.go.kr 15125364) ์กฐํ helper",
|
|
342
|
+
)
|
|
343
|
+
subparsers = parser.add_subparsers(dest="operation", required=True)
|
|
344
|
+
|
|
345
|
+
for operation in OPERATIONS:
|
|
346
|
+
sub = subparsers.add_parser(operation, help=f"K-Startup {operation} endpoint")
|
|
347
|
+
sub.add_argument("--page", type=int, default=1)
|
|
348
|
+
sub.add_argument("--per-page", dest="per_page", type=int, default=10)
|
|
349
|
+
format_group = sub.add_mutually_exclusive_group()
|
|
350
|
+
format_group.add_argument("--text", action="store_true", help="์ฌ๋์ฉ ์์ฝ")
|
|
351
|
+
format_group.add_argument("--json", action="store_true", help="๊ตฌ์กฐํ JSON ์ถ๋ ฅ (๊ธฐ๋ณธ)")
|
|
352
|
+
sub.add_argument("--dry-run", action="store_true", dest="dry_run",
|
|
353
|
+
help="์์ฒญ URL/ํ๋ผ๋ฏธํฐ๋ง ์ถ๋ ฅ, ๋คํธ์ํฌ ํธ์ถ ์์")
|
|
354
|
+
sub.add_argument("--timeout", type=int, default=30)
|
|
355
|
+
sub.add_argument("--proxy-base-url", default=os.environ.get("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY_BASE_URL))
|
|
356
|
+
sub.add_argument("--direct", action="store_true",
|
|
357
|
+
help="proxy ์ฐํ, KSKILL_KSTARTUP_API_KEY ๋ก ์ง์ ํธ์ถ")
|
|
358
|
+
sub.add_argument("--secrets-path", default=DEFAULT_SECRETS_PATH,
|
|
359
|
+
help=f"--direct ์ secrets ํ์ผ ๊ฒฝ๋ก (๊ธฐ๋ณธ {DEFAULT_SECRETS_PATH})")
|
|
360
|
+
_add_filter_args(sub, operation)
|
|
361
|
+
return parser
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def run(argv: Optional[List[str]] = None) -> int:
|
|
365
|
+
parser = make_parser()
|
|
366
|
+
args = parser.parse_args(argv)
|
|
367
|
+
operation = args.operation
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
query = build_query(args, operation)
|
|
371
|
+
except HelperError as exc:
|
|
372
|
+
print(f"[error] {exc}", file=sys.stderr)
|
|
373
|
+
return 2
|
|
374
|
+
|
|
375
|
+
if args.dry_run:
|
|
376
|
+
if args.direct:
|
|
377
|
+
preview = build_url(operation, query, direct=True, api_key="<DRY-RUN>", proxy_base_url=args.proxy_base_url)
|
|
378
|
+
else:
|
|
379
|
+
preview = build_url(operation, query, direct=False, api_key=None, proxy_base_url=args.proxy_base_url)
|
|
380
|
+
preview = preview.replace(os.environ.get("KSKILL_KSTARTUP_API_KEY", ""), "<DRY-RUN>") if os.environ.get("KSKILL_KSTARTUP_API_KEY") else preview
|
|
381
|
+
preview = preview.replace(os.environ.get("DATA_GO_KR_API_KEY", ""), "<DRY-RUN>") if os.environ.get("DATA_GO_KR_API_KEY") else preview
|
|
382
|
+
result = {"operation": operation, "url": preview, "query": query, "direct": bool(args.direct)}
|
|
383
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
384
|
+
return 0
|
|
385
|
+
|
|
386
|
+
api_key = resolve_api_key(args) if args.direct else None
|
|
387
|
+
try:
|
|
388
|
+
url = build_url(operation, query, direct=args.direct, api_key=api_key, proxy_base_url=args.proxy_base_url)
|
|
389
|
+
except HelperError as exc:
|
|
390
|
+
print(f"[error] {exc}", file=sys.stderr)
|
|
391
|
+
return 3
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
status, content_type, body = http_get(url, timeout=args.timeout, via_proxy=not args.direct)
|
|
395
|
+
except HelperError as exc:
|
|
396
|
+
print(f"[error] {exc}", file=sys.stderr)
|
|
397
|
+
return 4
|
|
398
|
+
|
|
399
|
+
payload: Any
|
|
400
|
+
try:
|
|
401
|
+
payload = json.loads(body) if body else {}
|
|
402
|
+
except json.JSONDecodeError:
|
|
403
|
+
print(f"[error] upstream returned non-JSON content-type={content_type!r} status={status}", file=sys.stderr)
|
|
404
|
+
print(body[:500])
|
|
405
|
+
return 5
|
|
406
|
+
|
|
407
|
+
if not isinstance(payload, dict):
|
|
408
|
+
payload = {"raw": payload}
|
|
409
|
+
payload.setdefault("query", query)
|
|
410
|
+
|
|
411
|
+
payload = apply_client_filters(payload, args, operation)
|
|
412
|
+
|
|
413
|
+
if args.text:
|
|
414
|
+
print(summarise(operation, payload))
|
|
415
|
+
else:
|
|
416
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
417
|
+
|
|
418
|
+
if status >= 400:
|
|
419
|
+
return 6
|
|
420
|
+
return 0
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
if __name__ == "__main__":
|
|
424
|
+
raise SystemExit(run())
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kstartup-search",
|
|
3
|
+
"description": "๊ณต๊ณต๋ฐ์ดํฐํฌํธ ์ฐฝ์
์งํฅ์ K-Startup Open API(15125364)๋ก ํตํฉ ๊ณต๊ณ ์ฌ์
์ ๋ณดยท์ง์์ฌ์
๊ณต๊ณ ยท์ฐฝ์
์ฝํ
์ธ ยทํต๊ณ๋ณด๊ณ ์๋ฅผ k-skill-proxy ๊ฒฝ์ ๋ก ์กฐํํ๋ค. ๊ฒ์ ์ ์ฉ. ๋์ ์์๋ ๊ณต์ ํ๋ฉด์ ํตํ ํ์ ์ก์
๊น์ง ์งํํ๋ค.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"browser",
|
|
8
|
+
"action:submission"
|
|
9
|
+
],
|
|
10
|
+
"frontmatter": "name: kstartup-search\ndescription: ๊ณต๊ณต๋ฐ์ดํฐํฌํธ ์ฐฝ์
์งํฅ์ K-Startup Open API(15125364)๋ก ํตํฉ ๊ณต๊ณ ์ฌ์
์ ๋ณดยท์ง์์ฌ์
๊ณต๊ณ ยท์ฐฝ์
์ฝํ
์ธ ยทํต๊ณ๋ณด๊ณ ์๋ฅผ k-skill-proxy ๊ฒฝ์ ๋ก ์กฐํํ๋ค. ๊ฒ์ ์ ์ฉ. ๋์ ์์๋ ๊ณต์ ํ๋ฉด์ ํตํ ํ์ ์ก์
๊น์ง ์งํํ๋ค.\nlicense: MIT\nmetadata:\n category: business\n locale: ko-KR\n phase: v1"
|
|
11
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# KTX Booking
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
`korail2` ์์ `scripts/ktx_booking.py` helper ๋ฅผ ์น์ด KTX/Korail ์กฐํ, ํธ์ฐจ๋ณ ์ข์๋ฒํธ ํ์ธ, ์์ฝ, ์์ฝ ํ์ธ, ์ทจ์๋ฅผ ์ฒ๋ฆฌํ๋ค.
|
|
6
|
+
|
|
7
|
+
์ต๊ทผ Korail ์ฑ์ Dynapath anti-bot ์ฒดํฌ ๋๋ฌธ์ ์๋ณธ `korail2` 0.4.0 ์์ ๋ง์ผ๋ก๋ `MACRO ERROR` ๊ฐ ๋ ์ ์๋ค. ์ด ์คํฌ์ helper ๊ฐ `x-dynapath-m-token`, `Sid`, ์ต์ app version(`250601002`)์ ๋ถ์ฌ ์ค์ ์๋งค ํ๋ฆ์ ๋ณต๊ตฌํ๋ ๊ฒ์ ์ ์ ๋ก ํ๋ค.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- "์์ธ์์ ๋ถ์ฐ ๊ฐ๋ KTX ์ฐพ์์ค"
|
|
12
|
+
- "์ฝ๋ ์ผ ์์ฝ ํ์ธํด์ค"
|
|
13
|
+
- "KTX ์ทจ์ํด์ค"
|
|
14
|
+
- "์ค์ 9์ ์ดํ KTX ์ค ์ ์ผ ๋น ๋ฅธ ๊ฑฐ ์ก์์ค"
|
|
15
|
+
- "KTX ๋จ์ ์ข์ ๋ฒํธ ํ์ธํด์ค"
|
|
16
|
+
- "์ด ์ด์ฐจ ์ฝ์ผํธ ์๋ ๊ฟํ ์ข์๋ถํฐ ๋ณด์ฌ์ค"
|
|
17
|
+
- "KTX 5ํธ์ฐจ ๋จ์ ์๋ฆฌ๋ง ๋ด์ค"
|
|
18
|
+
- "์์ฝํ๊ธฐ ์ ์ ํธ์ฐจ๋ณ ์ข์ ํ์ธํด์ค"
|
|
19
|
+
- "N์นด๋๋ก ํ ์ธ ์ด์ฐจ ์ฐพ์์ค"
|
|
20
|
+
- "๋ด N์นด๋ ๋ชฉ๋ก ๋ณด์ฌ์ค"
|
|
21
|
+
- "N์นด๋ ํ ์ธ ์ ์ฉํด์ ์์ฝํด์ค"
|
|
22
|
+
|
|
23
|
+
## When not to use
|
|
24
|
+
|
|
25
|
+
- SRT ์๋งค์ธ ๊ฒฝ์ฐ
|
|
26
|
+
- ๋์ ๊ฐ ์๋๋ฉฐ ์ค๊ฒฐ์ ํ์ ๊น์ง ์๋ํํด์ผ ํ๋ ๊ฒฝ์ฐ
|
|
27
|
+
- credential ์ ํ๋ฌธ์ผ๋ก ๋ฃ์ผ๋ ค๋ ๊ฒฝ์ฐ
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
- Python 3.10+
|
|
32
|
+
- `python3 -m pip install korail2-ncard pycryptodome`
|
|
33
|
+
|
|
34
|
+
## Required environment variables
|
|
35
|
+
|
|
36
|
+
- `KSKILL_KTX_ID`
|
|
37
|
+
- `KSKILL_KTX_PASSWORD`
|
|
38
|
+
|
|
39
|
+
### Credential handling
|
|
40
|
+
|
|
41
|
+
- ๋์ credential mode์์๋ `vault-run` capability๋ฅผ ์ฌ์ฉํ๊ณ , ์์ผ๋ฉด `request_vault_credential`์ ํธ์ถํ๋ค. ID/PW ์๋ฌธ์ ์ฑํ
์ด๋ shell์ ๋ฃ์ง ์๋๋ค.
|
|
42
|
+
- ๊ทธ ๋ฐ์ ํ๊ฒฝ์์๋ ์ด๋ฏธ ์ฃผ์
๋ ํ๊ฒฝ๋ณ์ โ host vault โ `~/.config/k-skill/secrets.env` ์์๋ก ์ฌ์ฉํ๋ค.
|
|
43
|
+
|
|
44
|
+
## Inputs
|
|
45
|
+
|
|
46
|
+
- ์ถ๋ฐ์ญ
|
|
47
|
+
- ๋์ฐฉ์ญ
|
|
48
|
+
- ๋ ์ง: `YYYYMMDD`
|
|
49
|
+
- ํฌ๋ง ์์ ์๊ฐ: `HHMMSS`
|
|
50
|
+
- ์ธ์ ์์ ์น๊ฐ ์ ํ
|
|
51
|
+
- ์ข์ ์ ํธ
|
|
52
|
+
- ์ข์ ์์ธ ์กฐ๊ฑด: ๊ฐ์ค ๋ฑ๊ธ, ํธ์ฐจ ๋ฒํธ, ๋จ์ ์ข์๋ง ๋ณด๊ธฐ, ์ฝ์ผํธ ๊ฟํ ์ข์ ์ฐ์
|
|
53
|
+
- ์กฐํ ๊ฒฐ๊ณผ์์ ๋ณต์ฌํ `train_id`
|
|
54
|
+
|
|
55
|
+
## Workflow
|
|
56
|
+
|
|
57
|
+
### 0. Install the package globally when missing
|
|
58
|
+
|
|
59
|
+
`python3 -c 'import korail2, Crypto'` ๊ฐ ์คํจํ๋ฉด ๋ค๋ฅธ ๊ตฌํ์ผ๋ก ์ฐํํ์ง ๋ง๊ณ ์ ์ญ Python ํจํค์ง ์ค์น๋ฅผ ๋จผ์ ์๋ํ๋ค.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python3 -m pip install korail2-ncard pycryptodome
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 1. Ensure credentials are available
|
|
66
|
+
|
|
67
|
+
๋์ credential mode์์๋ `vault-run`์ Korail capability๋ฅผ ํ์ธํ๊ณ , ์์ผ๋ฉด `request_vault_credential`๋ก ์ฑ vault ์
๋ ฅ UI๋ฅผ ํธ์ถํ๋ค. generic fallback์์๋ง `KSKILL_KTX_ID`, `KSKILL_KTX_PASSWORD` ํ๊ฒฝ๋ณ์๋ฅผ ํ์ธํ๋ค.
|
|
68
|
+
|
|
69
|
+
์ํฌ๋ฆฟ์ด ์๋ค๋ ์ด์ ๋ก ์น์ฌ์ดํธ๋ฅผ ์ง์ ๊ธ๊ฑฐ๋ ๋ค๋ฅธ ๋น๊ณต์ ๊ฒฝ๋ก๋ฅผ ์ฐพ์ง ์๋๋ค.
|
|
70
|
+
|
|
71
|
+
### 2. Search first via the helper
|
|
72
|
+
|
|
73
|
+
ํญ์ helper ๋ฅผ ํตํด ์กฐํํ๋ค.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 scripts/ktx_booking.py search ์์ธ ๋ถ์ฐ 20260328 090000 --limit 5
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
๊ธฐ๋ณธ `--train-type` ์ `ktx` ๋ค. ITX-์ฒญ์ถ(์: ๋จ์ถ์ฒโ์ฉ์ฐ)ยทITX-์๋ง์ยท๋ฌด๊ถํํธ์ฒ๋ผ KTX ์ธ ๋
ธ์ ์ ์ก์ผ๋ ค๋ฉด `--train-type` ์ผ๋ก ์ง์ ํ๋ค.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python3 scripts/ktx_booking.py search ๋จ์ถ์ฒ ์ฉ์ฐ 20260503 150000 --train-type itx-cheongchun
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
์ ํ์ง: `ktx`, `itx-saemaeul`, `mugunghwa`, `nuriro`, `tonggeun`, `itx-cheongchun`, `airport`, `all`.
|
|
86
|
+
|
|
87
|
+
์์ฝ ๋จ๊ณ(`reserve`)์์๋ ๊ฐ์ `--train-type` ๊ฐ์ ๊ทธ๋๋ก ๋๊ฒจ์ผ stable `train_id` ๋งค์นญ์ด ๊นจ์ง์ง ์๋๋ค.
|
|
88
|
+
|
|
89
|
+
์ข์์ด ์๋ ์ด์ฐจ๋ ํ๋ณด์ ํฌํจํ๋ ค๋ฉด `--include-no-seats`, ์์ฝ ๋๊ธฐ ๊ฐ๋ฅํ ์ด์ฐจ๋ ๊ฐ์ด ๋ณด๊ณ ์ถ์ผ๋ฉด `--include-waiting-list` ๋ฅผ ๋ถ์ธ๋ค.
|
|
90
|
+
|
|
91
|
+
### 3. Present the shortlist
|
|
92
|
+
|
|
93
|
+
์๋งค ์ ์ ํญ์ ์๋๋ฅผ ํ์ธํ๋ค.
|
|
94
|
+
|
|
95
|
+
- `index`
|
|
96
|
+
- `train_id`
|
|
97
|
+
- ์ถ๋ฐ/๋์ฐฉ ์๊ฐ
|
|
98
|
+
- ์ด์ฐจ ์ข
๋ฅ (`train_type`)
|
|
99
|
+
- ์ผ๋ฐ์ค/ํน์ค ๊ฐ๋ฅ ์ฌ๋ถ
|
|
100
|
+
- ์์ฝ ๋๊ธฐ ๊ฐ๋ฅ ์ฌ๋ถ
|
|
101
|
+
|
|
102
|
+
### 4. Inspect detailed seats when the user asks for good seats
|
|
103
|
+
|
|
104
|
+
`search` ์ ์ข์ ๊ฐ๋ฅ ์ฌ๋ถ๋ ์ด์ฐจ ๋จ์ ํ๋๊ทธ๋ค. ์ฌ์ฉ์๊ฐ "๋จ์ ์ข์ ๋ฒํธ", "ํธ์ฐจ๋ณ ์ข์", "์ฝ์ผํธ", "๊ฟํ ์ข์", "์ฐฝ์ธก/์๋ฐฉํฅ ์๋ฆฌ", "์์ฝ ์ ์ ์๋ฆฌ ํ์ธ"์ฒ๋ผ ๊ตฌ์ฒด์ ์ธ ์ข์์ ๋ฌผ์ผ๋ฉด ์์ฝ ์ ์ `seats` ๋ฅผ ํธ์ถํ๋ค.
|
|
105
|
+
|
|
106
|
+
๊ธฐ๋ณธ ์์ธ ์ข์ ์กฐํ:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python3 scripts/ktx_booking.py seats ์์ธ ๋ถ์ฐ 20260328 090000 --train-id <train_id>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
์ผ๋ฐ์ค/ํน์ค์ `--room` ์ผ๋ก ๋๋๋ค.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
python3 scripts/ktx_booking.py seats ์์ธ ๋ถ์ฐ 20260328 090000 --train-id <train_id> --room special
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
๋จ์ ์ข์๋ฒํธ๋ง ๋ณด๊ณ ์ถ์ผ๋ฉด `--available-only` ๋ฅผ ์ด๋ค.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python3 scripts/ktx_booking.py seats ์์ธ ๋ถ์ฐ 20260328 090000 --train-id <train_id> --available-only
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
ํน์ ํธ์ฐจ๋ฅผ ์ง์ ํ์ง ์์ผ๋ฉด `seats` ๋ **5ํธ์ฐจ๋ฅผ ์ต์ฐ์ **์ผ๋ก ํ์ํ๋ค. 5ํธ์ฐจ๊ฐ ์์ผ๋ฉด 5ํธ์ฐจ์์ ๊ฑฐ๋ฆฌ๊ฐ ๊ฐ๊น์ด ํธ์ฐจ ์์ผ๋ก, ๊ฐ์ ๊ฑฐ๋ฆฌ์์๋ ๋ฎ์ ํธ์ฐจ ๋ฒํธ ์์ผ๋ก ํ์ํ๋ค(์: 1~8ํธ์ฐจ ํธ์ฑ์ `5, 4, 6, 3, 7, 2, 8, 1`, 1~4ํธ์ฐจ ํธ์ฑ์ `4, 3, 2, 1`). ์ผ๋ฐ KTX, KTX-์ฐ์ฒ(๋ถ๋ฅ ์ฝ๋ 07ยท10), KTX-์ฒญ๋ฃก ๋ชจ๋ ๊ฐ์ ๊ท์น์ ์ ์ฉํ๋ค. ๊ฐ ํธ์ฐจ ์์ ์ข์์ ์ฝ์ผํธ ํํธ๊ฐ ์๋ ์ข์(`direct`, `adjacent`)์ ๋จผ์ , ๊ฐ์ ์กฐ๊ฑด์์๋ ์๋ฐฉํฅ ์ข์์ ๋จผ์ ๋ณด์ฌ์ค๋ค.
|
|
125
|
+
|
|
126
|
+
ํน์ ํธ์ฐจ๋ง ํ์ธํ๋ ค๋ฉด `--car-no` ๋ฅผ ์ด๋ค.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python3 scripts/ktx_booking.py seats ์์ธ ๋ถ์ฐ 20260328 090000 --train-id <train_id> --car-no 5 --available-only
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
์ฝ์ผํธ ๊ฟํ ์๋ฆฌ๋ถํฐ ํ์ธํ๋ ค๋ฉด `--power-only` ๋ฅผ ๋ถ์ธ๋ค. ์๋ต์ `power_outlet` ์ `direct`, `adjacent`, `none` ์ค ํ๋๋ค.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python3 scripts/ktx_booking.py seats ์์ธ ๋ถ์ฐ 20260328 090000 --train-id <train_id> --available-only --power-only
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`seats` ๋ `search` ์ ๊ฐ์ `--train-type` ์ ๋๊ฒจ์ผ ํ๋ค. ITX-์ฒญ์ถ ๋ฑ KTX ์ธ ์ด์ฐจ๋ฅผ ์กฐํํ๋ค๋ฉด ์์ธ ์ข์ ์กฐํ์๋ ๊ฐ์ ๊ฐ์ ์ฌ์ฉํ๋ค.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python3 scripts/ktx_booking.py seats ๋จ์ถ์ฒ ์ฉ์ฐ 20260503 150000 \
|
|
142
|
+
--train-id <train_id> \
|
|
143
|
+
--train-type itx-cheongchun \
|
|
144
|
+
--available-only
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
์์ธ ์ข์ ์๋ต์ ๋ณด์ฌ์ค ๋๋ ์ฌ์ฉ์ ์๋์ ๋ง์ถฐ ์๋๋ฅผ ์ฐ์ ์์ฝํ๋ค.
|
|
148
|
+
|
|
149
|
+
- ํธ์ฐจ๋ณ `remaining_seats`, `available_seat_count`
|
|
150
|
+
- ๋จ์ ์ข์ ๋ฒํธ (`available_seats`)
|
|
151
|
+
- ์ข์๋ณ `direction`, `position`, `seat_type`
|
|
152
|
+
- ์ฝ์ผํธ ํํธ (`power_outlet`)
|
|
153
|
+
- ๋ฌธ ๊ทผ์ฒ ์ฌ๋ถ (`near_door`)
|
|
154
|
+
|
|
155
|
+
์ด ๊ธฐ๋ฅ์ ์ข์์ ์ ํ/์ ์ ํ์ง ์๋๋ค. ์ค์ ์์ฝ์ ๋ค์ ๋จ๊ณ์ `reserve` ๋ก๋ง ์งํํ๋ค.
|
|
156
|
+
|
|
157
|
+
### 5. Reserve only after the target train is unambiguous
|
|
158
|
+
|
|
159
|
+
์กฐํ ๊ฒฐ๊ณผ์ `train_id` ๋ฅผ ๊ณ ๋ฅธ ๋ค์๋ง ์์ฝํ๋ค. ์ด ๊ฐ์ helper ๊ฐ ์ด์ฐจ ๋ฒํธ/์ดํ์ผ/์๊ฐ/์ญ ์ฝ๋๋ฅผ ๋ฌถ์ด ๋ง๋ stable selector ์ด๋ฏ๋ก, ์ฌ์กฐํ ์ ๊ฐ์ ์ด์ฐจ๊ฐ ์์ง ์์ผ๋ฉด ๊ทธ๋๋ก ์ก๊ณ ์์ผ๋ฉด ์คํจํ๋ค.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python3 scripts/ktx_booking.py reserve ์์ธ ๋ถ์ฐ 20260328 090000 --train-id <train_id> --seat-option general-first
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
ITX ๋ฑ KTX ์ธ ๋
ธ์ ์ search ๋จ๊ณ์์ ๊ณจ๋๋ค๋ฉด reserve ์๋ ๋๊ฐ์ด `--train-type` ์ ๋๊ธด๋ค.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python3 scripts/ktx_booking.py reserve ๋จ์ถ์ฒ ์ฉ์ฐ 20260503 150000 --train-id <train_id> --train-type itx-cheongchun --seat-option general-first
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
์๋ต์๋ ์์ฝ๋ฒํธ, ์ด์, ๊ตฌ์
๊ธฐํ์ด ํฌํจ๋๋ค. ์ด ์์ ์ **์ข์ ํ๋ณด๋ ์๋ฃ๋์๋ค**๊ณ ์๋ดํ๋ค. generic fallback์์๋ ๊ฒฐ์ ๋ฅผ handoffํ๊ณ , ๋์ ์์๋ ์๋ ๊ฒฐ์ ๋จ๊ณ๋ก ๊ณ์ ์งํํ๋ค.
|
|
172
|
+
์ข์์ด ์์ ๋๋ ์กฐํ ๋จ๊ณ์์ `--include-waiting-list` ๋ฅผ ์ผ๊ณ ์์ฝ ๋จ๊ณ์์ `--try-waiting` ์ผ๋ก ์์ฝ ๋๊ธฐ๊น์ง ์๋ํ ์ ์๋ค.
|
|
173
|
+
|
|
174
|
+
### 5-1. N-card discounted reservation
|
|
175
|
+
|
|
176
|
+
N์นด๋ ํ ์ธ์ ์ ์ฉํ๋ ค๋ฉด ๋จผ์ ๋ณด์ N์นด๋ ๋ชฉ๋ก์ ์กฐํํด ์นด๋ ๋ฒํธ๋ฅผ ํ์ธํ๋ค.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python3 scripts/ktx_booking.py ncard-list
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
N์นด๋๋ก ํ ์ธ ์ด์ฐจ๋ฅผ ์กฐํํ๋ค (`--ncard-index` ๋ `ncard-list` ๊ฒฐ๊ณผ์ ์๋ฒ). `ncard-list` ๋ ๋ก๊ทธ/์
ธ ๋
ธ์ถ์ ์ค์ด๊ธฐ ์ํด ์นด๋ ๋ฒํธ๋ฅผ ๋ง์คํนํด ์ถ๋ ฅํ๋ค.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
python3 scripts/ktx_booking.py ncard-search ๋์ ์์ธ 20260512 100000 --ncard-index 1 --train-type ktx
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
์๋ต์ `train_id` ๋ฅผ ๋ณต์ฌํด `reserve` ์ ๊ฐ์ `--ncard-index` ๋ฅผ ๋ถ์ฌ ์์ฝํ๋ค.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
python3 scripts/ktx_booking.py reserve ๋์ ์์ธ 20260512 100000 \
|
|
192
|
+
--train-id <train_id> \
|
|
193
|
+
--ncard-index 1
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`--ncard-index` ๋ฅผ ์ง์ ํ๋ฉด `--adults` ๋ฑ ์น๊ฐ ์ต์
์ ๋ฌด์๋๊ณ N์นด๋ ์น๊ฐ 1๋ช
์ผ๋ก ์ฒ๋ฆฌ๋๋ค. `--ncard-no` ์ง์ ์
๋ ฅ๋ ์ง์ํ์ง๋ง ์
ธ ํ์คํ ๋ฆฌ์ ๋จ์ ์ ์์ด ๊ถ์ฅํ์ง ์๋๋ค. generic fallback์์๋ ๊ฒฐ์ ๋ฅผ handoffํ๊ณ , ๋์ ์์๋ ์๋ ๊ฒฐ์ ๋จ๊ณ๋ก ๊ณ์ ์งํํ๋ค.
|
|
197
|
+
|
|
198
|
+
N์นด๋ ๊ธฐ๋ฅ์ `korail2-ncard` ํจํค์ง๊ฐ ํ์ํ๋ค. ์์ผ๋ฉด ํด๋น ์ปค๋งจ๋ ์คํ ์ ์ค์น ์๋ด๊ฐ ์ถ๋ ฅ๋๋ค.
|
|
199
|
+
|
|
200
|
+
### 6. Continue to payment in Dolshoi
|
|
201
|
+
|
|
202
|
+
๋์ ์์ ์ฌ์ฉ์๊ฐ ์๋งค ์๋ฃ๋ฅผ ์์ฒญํ๋ค๋ฉด ์์ฝ ์ฑ๊ณต์์ ๋ฉ์ถ์ง ์๋๋ค.
|
|
203
|
+
|
|
204
|
+
1. CloakBrowser์ ๊ณต์ Korail ์์ฝ/๊ฒฐ์ ํ๋ฉด์์ ๋ฐฉ๊ธ ๋ง๋ ์์ฝ๋ฒํธ๋ฅผ ๋ค์ ์๋ณํ๋ค.
|
|
205
|
+
2. vault-backed login์ ์ฌ์ฉํ๊ณ N์นด๋/ํ ์ธ, ์น๊ฐ, ์ข์, ๊ฒฐ์ ์๋จ์ ํ์ธํ๋ค.
|
|
206
|
+
3. ์ค์ ๊ฒฐ์ ๋ฒํผ ์ง์ ์ `clarify`๋ก ์ด์ฐจ, ๋ ์งยท์๊ฐ, ์น๊ฐ, ์ข์ ๋ฑ๊ธ, ํ ์ธ, ์ด์ก์ ๋ณด์ฌ์ฃผ๊ณ ์น์ธ๋ฐ๋๋ค.
|
|
207
|
+
4. ์น์ธ๋๋ฉด ๊ฒฐ์ ๋ฅผ ์คํํ๊ณ ๊ฒฐ์ ์๋ฃ ํ๋ฉด, ์์ฝ๋ฒํธ, ์์์ฆ/๊ฒฐ์ ์ํ๋ฅผ ํ์ธํ๋ค.
|
|
208
|
+
|
|
209
|
+
๋์ ๊ฐ ์๋๊ฑฐ๋ ๊ณต์ ๊ฒฐ์ ํ๋ฉด์ ์ฌ์ฉํ ์ ์์ผ๋ฉด ์์ฝ๋ฒํธ์ ๊ตฌ์
๊ธฐํ์ ์ ๊ณตํ๊ณ generic handoff๋ก ์ข
๋ฃํ๋ค.
|
|
210
|
+
|
|
211
|
+
### 7. Inspect or cancel
|
|
212
|
+
|
|
213
|
+
์ทจ์๋ ๋์ ์์ฝ์ ๋ค์ ์กฐํํด ์๋ณํ ๋ค์๋ง ์งํํ๋ค.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
python3 scripts/ktx_booking.py reservations
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
์ทจ์ ์คํ ์ง์ ์ `clarify`๋ก ์์ฝ๋ฒํธ, ์ด์ฐจ, ๋ ์งยท์๊ฐ, ์น๊ฐ, ํ๋ถ/์์ฝ๊ธ ์ ๋ณด๋ฅผ ํ์ธํ๊ณ ์น์ธ๋ฐ๋๋ค.
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
python3 scripts/ktx_booking.py cancel <reservation_id>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Done when
|
|
226
|
+
|
|
227
|
+
- ์กฐํ๋ฉด ์ด์ฐจ ํ๋ณด๊ฐ ์ ๋ฆฌ๋์ด ์๋ค
|
|
228
|
+
- ์ข์ ์์ธ ํ์ธ์ด๋ฉด ํธ์ฐจ๋ณ ๋จ์ ์ข์๋ฒํธ์ ํ์ํ ๊ฟํ ์กฐ๊ฑด์ด ์ ๋ฆฌ๋์ด ์๋ค
|
|
229
|
+
- ์์ฝ์ด๋ฉด ์์ฝ ๊ฒฐ๊ณผ์ ์ ํ ์๊ฐ์ด ํ์ธ๋์ด ์ข์ ํ๋ณด ์๋ฃ๋ฅผ ์๋ดํ๋ค
|
|
230
|
+
- ๋์ ์ ์๋งค ์๋ฃ ์์ฒญ์ด๋ฉด `clarify` ์น์ธ ํ ๊ฒฐ์ ์๋ฃ ์ํ์ ์์์ฆ/์์ฝ๋ฒํธ๋ฅผ ํ์ธํ๋ค
|
|
231
|
+
- ์ทจ์๋ฉด ์ด๋ค ์์ฝ์ ์ทจ์ํ๋์ง ๋จ์ ์๋ค
|
|
232
|
+
|
|
233
|
+
## Failure modes
|
|
234
|
+
|
|
235
|
+
- ๋ก๊ทธ์ธ ์คํจ
|
|
236
|
+
- ๋งค์ง
|
|
237
|
+
- Korail anti-bot ๊ท์น ๋ณ๊ฒฝ
|
|
238
|
+
|
|
239
|
+
## Notes
|
|
240
|
+
|
|
241
|
+
- `scripts/ktx_booking.py` ๋ upstream `korail2` anti-bot ํ๊ท๋ฅผ ๋ณด์ํ๋ helper ๋ค
|
|
242
|
+
- `korail2` ๋ KTX/Korail ์ ์ฉ ํ๋ฉด์ด๋ผ train type ๊ณผ passenger model ์ด ๋ถ๋ช
ํ๋ค
|
|
243
|
+
- ๊ฒฐ์ ์๋ํ ๊ธ์ง๋ generic fallback์๋ง ์ ์ฉํ๋ค. ๋์ ์์๋ `clarify` ์น์ธ ํ ๊ณต์ ๊ฒฐ์ ํ๋ฉด์ผ๋ก ์๋ฃํ๋ค
|
|
244
|
+
- aggressive polling ์ ํผํ๋ค
|