@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,272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import base64
|
|
6
|
+
import contextlib
|
|
7
|
+
import io
|
|
8
|
+
import importlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
from srt_seats import parse_cars, parse_seats, sort_cars_for_booking, sort_seats_for_booking
|
|
16
|
+
|
|
17
|
+
SRT_SEAT_ENDPOINT = "https://etk.srail.kr/hpg/hra/01/selectPassengerResearchList.do"
|
|
18
|
+
TRAIN_ID_PREFIX = "srt:v1:"
|
|
19
|
+
TRAIN_ID_FIELDS = (
|
|
20
|
+
"train_number",
|
|
21
|
+
"dep_date",
|
|
22
|
+
"dep_time",
|
|
23
|
+
"arr_date",
|
|
24
|
+
"arr_time",
|
|
25
|
+
"train_code",
|
|
26
|
+
"dep_station_code",
|
|
27
|
+
"arr_station_code",
|
|
28
|
+
"dep_station_run_order",
|
|
29
|
+
"arr_station_run_order",
|
|
30
|
+
)
|
|
31
|
+
ROOM_CODE = {"general": "1", "special": "2"}
|
|
32
|
+
ROOM_NAME = {"general": "일반실", "special": "특실"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SrtTrainLike(Protocol):
|
|
36
|
+
train_number: str
|
|
37
|
+
dep_date: str
|
|
38
|
+
dep_time: str
|
|
39
|
+
arr_date: str
|
|
40
|
+
arr_time: str
|
|
41
|
+
train_code: str
|
|
42
|
+
train_name: str
|
|
43
|
+
dep_station_code: str
|
|
44
|
+
dep_station_name: str
|
|
45
|
+
arr_station_code: str
|
|
46
|
+
arr_station_name: str
|
|
47
|
+
dep_station_run_order: str
|
|
48
|
+
arr_station_run_order: str
|
|
49
|
+
general_seat_state: str
|
|
50
|
+
special_seat_state: str
|
|
51
|
+
reserve_wait_possible_code: str
|
|
52
|
+
|
|
53
|
+
def general_seat_available(self) -> bool: ...
|
|
54
|
+
|
|
55
|
+
def special_seat_available(self) -> bool: ...
|
|
56
|
+
|
|
57
|
+
def reserve_standby_available(self) -> bool: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ResponseLike(Protocol):
|
|
61
|
+
text: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SessionLike(Protocol):
|
|
65
|
+
def get(self, url: str, params: dict[str, str]) -> ResponseLike: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SrtClientLike(Protocol):
|
|
69
|
+
_session: SessionLike
|
|
70
|
+
|
|
71
|
+
def search_train(
|
|
72
|
+
self,
|
|
73
|
+
dep: str,
|
|
74
|
+
arr: str,
|
|
75
|
+
date: str,
|
|
76
|
+
time: str,
|
|
77
|
+
time_limit: str | None = None,
|
|
78
|
+
available_only: bool = True,
|
|
79
|
+
) -> list[SrtTrainLike]: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_srt_module() -> ModuleType:
|
|
83
|
+
try:
|
|
84
|
+
return importlib.import_module("SRT")
|
|
85
|
+
except ModuleNotFoundError as exc:
|
|
86
|
+
raise SystemExit("scripts/srt_booking.py requires SRTrain: python3 -m pip install SRTrain")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_client(auto_login: bool = False) -> SrtClientLike:
|
|
90
|
+
srt_module = load_srt_module()
|
|
91
|
+
srt_id = os.environ.get("KSKILL_SRT_ID", "")
|
|
92
|
+
srt_pw = os.environ.get("KSKILL_SRT_PASSWORD", "")
|
|
93
|
+
return srt_module.SRT(srt_id, srt_pw, auto_login=auto_login)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def train_id_payload(train: SrtTrainLike) -> dict[str, str]:
|
|
97
|
+
return {field: getattr(train, field) for field in TRAIN_ID_FIELDS}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_train_id(train: SrtTrainLike) -> str:
|
|
101
|
+
raw = json.dumps(train_id_payload(train), ensure_ascii=False, separators=(",", ":")).encode()
|
|
102
|
+
encoded = base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
103
|
+
return f"{TRAIN_ID_PREFIX}{encoded}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_train_id(train_id: str) -> dict[str, str]:
|
|
107
|
+
if not train_id.startswith(TRAIN_ID_PREFIX):
|
|
108
|
+
raise SystemExit("train_id must start with srt:v1:")
|
|
109
|
+
encoded = train_id.removeprefix(TRAIN_ID_PREFIX)
|
|
110
|
+
padded = encoded + ("=" * ((4 - len(encoded) % 4) % 4))
|
|
111
|
+
try:
|
|
112
|
+
payload = json.loads(base64.urlsafe_b64decode(padded.encode()).decode())
|
|
113
|
+
except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
114
|
+
raise SystemExit("train_id is invalid; rerun search and copy a fresh train_id") from exc
|
|
115
|
+
if not isinstance(payload, dict):
|
|
116
|
+
raise SystemExit("train_id is invalid; rerun search and copy a fresh train_id")
|
|
117
|
+
if any(not isinstance(payload.get(field), str) or not payload[field] for field in TRAIN_ID_FIELDS):
|
|
118
|
+
raise SystemExit("train_id is invalid; rerun search and copy a fresh train_id")
|
|
119
|
+
return {field: payload[field] for field in TRAIN_ID_FIELDS}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def find_train_by_id(trains: list[SrtTrainLike], train_id: str) -> SrtTrainLike | None:
|
|
123
|
+
expected = parse_train_id(train_id)
|
|
124
|
+
return next((train for train in trains if train_id_payload(train) == expected), None)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def normalize_train(train: SrtTrainLike, index: int) -> dict[str, str | bool | int]:
|
|
128
|
+
return {
|
|
129
|
+
"index": index,
|
|
130
|
+
"train_id": build_train_id(train),
|
|
131
|
+
"train_no": train.train_number,
|
|
132
|
+
"train_type": train.train_name,
|
|
133
|
+
"dep_name": train.dep_station_name,
|
|
134
|
+
"dep_date": train.dep_date,
|
|
135
|
+
"dep_time": train.dep_time,
|
|
136
|
+
"arr_name": train.arr_station_name,
|
|
137
|
+
"arr_date": train.arr_date,
|
|
138
|
+
"arr_time": train.arr_time,
|
|
139
|
+
"has_general_seat": train.general_seat_available(),
|
|
140
|
+
"has_special_seat": train.special_seat_available(),
|
|
141
|
+
"has_waiting_list": train.reserve_standby_available(),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def seat_page_params(train: SrtTrainLike, room: str, car_no: int | None) -> dict[str, str]:
|
|
146
|
+
return {
|
|
147
|
+
"runDt1": train.dep_date,
|
|
148
|
+
"dptDt1": train.dep_date,
|
|
149
|
+
"dptTm1": train.dep_time,
|
|
150
|
+
"trnNo1": f"{int(train.train_number):05d}",
|
|
151
|
+
"trnGpCd1": "300",
|
|
152
|
+
"dptRsStnCd1": train.dep_station_code,
|
|
153
|
+
"arvRsStnCd1": train.arr_station_code,
|
|
154
|
+
"dptStnRunOrdr1": train.dep_station_run_order,
|
|
155
|
+
"arvStnRunOrdr1": train.arr_station_run_order,
|
|
156
|
+
"seatAttCd1": "015",
|
|
157
|
+
"psrmClCd1": ROOM_CODE[room],
|
|
158
|
+
"index1": "0",
|
|
159
|
+
"scarNo1": "" if car_no is None else f"{car_no:04d}",
|
|
160
|
+
"chtnDvCd": "1",
|
|
161
|
+
"jrnySqno": "001",
|
|
162
|
+
"mode": "1",
|
|
163
|
+
"psgNum": "1",
|
|
164
|
+
"pageId": "",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def fetch_seat_page(client: SrtClientLike, train: SrtTrainLike, room: str, car_no: int | None) -> str:
|
|
169
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
170
|
+
response = client._session.get(SRT_SEAT_ENDPOINT, params=seat_page_params(train, room, car_no))
|
|
171
|
+
return response.text
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def command_search(args: argparse.Namespace) -> None:
|
|
175
|
+
client = build_client(auto_login=False)
|
|
176
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
177
|
+
trains = client.search_train(args.dep, args.arr, args.date, args.time, args.time_limit, args.available_only)
|
|
178
|
+
print_json({"count": len(trains[: args.limit]), "trains": [normalize_train(train, index) for index, train in enumerate(trains[: args.limit], 1)]})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def command_seats(args: argparse.Namespace) -> None:
|
|
182
|
+
client = build_client(auto_login=False)
|
|
183
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
184
|
+
trains = client.search_train(args.dep, args.arr, args.date, args.time, args.time_limit, available_only=False)
|
|
185
|
+
train = find_train_by_id(trains, args.train_id)
|
|
186
|
+
if train is None:
|
|
187
|
+
raise SystemExit("train_id no longer matches any current search result; rerun search and choose a fresh train_id")
|
|
188
|
+
|
|
189
|
+
initial_html = fetch_seat_page(client, train, args.room, args.car_no)
|
|
190
|
+
cars = [car for car in parse_cars(initial_html) if car["room_class"] == ROOM_NAME[args.room]]
|
|
191
|
+
if args.car_no is not None:
|
|
192
|
+
cars = [car for car in cars if car["car_no"] == args.car_no]
|
|
193
|
+
else:
|
|
194
|
+
cars = [car for car in cars if car["available"]]
|
|
195
|
+
if not cars:
|
|
196
|
+
raise SystemExit(f"seat car data is unavailable for {args.room}; retry search or choose another train")
|
|
197
|
+
|
|
198
|
+
car_payloads: list[dict[str, object]] = []
|
|
199
|
+
for car in sort_cars_for_booking(cars, args.car_priority):
|
|
200
|
+
html = initial_html if args.car_no == car["car_no"] else fetch_seat_page(client, train, args.room, car["car_no"])
|
|
201
|
+
seats = parse_seats(html)
|
|
202
|
+
if args.seat:
|
|
203
|
+
seats = [seat for seat in seats if seat["seat"] == args.seat]
|
|
204
|
+
seats = sort_seats_for_booking(seats, args.seat_priority)
|
|
205
|
+
if args.available_only:
|
|
206
|
+
seats = [seat for seat in seats if seat["available"]]
|
|
207
|
+
available_seats = [seat for seat in seats if seat["available"]]
|
|
208
|
+
limited = seats[: args.limit]
|
|
209
|
+
payload = dict(car)
|
|
210
|
+
payload["available_seat_count"] = len(available_seats)
|
|
211
|
+
payload["available_seats"] = [seat["seat"] for seat in available_seats]
|
|
212
|
+
payload["shown_seat_count"] = len(limited)
|
|
213
|
+
payload["seats"] = limited
|
|
214
|
+
if args.seat:
|
|
215
|
+
payload["requested_seat"] = args.seat
|
|
216
|
+
payload["requested_seat_available"] = any(seat["available"] for seat in seats)
|
|
217
|
+
car_payloads.append(payload)
|
|
218
|
+
|
|
219
|
+
print_json({
|
|
220
|
+
"train": normalize_train(train, 1),
|
|
221
|
+
"room": args.room,
|
|
222
|
+
"available_only": args.available_only,
|
|
223
|
+
"car_priority": args.car_priority,
|
|
224
|
+
"seat_priority": args.seat_priority,
|
|
225
|
+
"cars": car_payloads,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def print_json(payload: dict[str, object]) -> None:
|
|
230
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
234
|
+
parser = argparse.ArgumentParser(description="SRT search and seat lookup helper for k-skill")
|
|
235
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
236
|
+
search = subparsers.add_parser("search", help="SRT 열차를 조회합니다")
|
|
237
|
+
add_trip_args(search)
|
|
238
|
+
search.add_argument("--time-limit", default=None, help="조회 종료 시각 HHMMSS")
|
|
239
|
+
search.add_argument("--available-only", action="store_true", default=False, help="예약 가능한 열차만 출력")
|
|
240
|
+
search.add_argument("--limit", type=int, default=5, help="출력할 최대 열차 수")
|
|
241
|
+
search.set_defaults(func=command_search)
|
|
242
|
+
|
|
243
|
+
seats = subparsers.add_parser("seats", help="SRT 호차별 좌석번호를 조회합니다")
|
|
244
|
+
add_trip_args(seats)
|
|
245
|
+
seats.add_argument("--train-id", required=True, help="search 결과에서 복사한 stable train_id")
|
|
246
|
+
seats.add_argument("--time-limit", default=None, help="조회 종료 시각 HHMMSS")
|
|
247
|
+
seats.add_argument("--room", choices=sorted(ROOM_CODE), default="general")
|
|
248
|
+
seats.add_argument("--car-no", type=int, default=None, help="특정 호차만 조회")
|
|
249
|
+
seats.add_argument("--seat", default=None, help="특정 좌석번호만 조회, 예: 6C")
|
|
250
|
+
seats.add_argument("--available-only", action="store_true", help="빈 좌석만 출력")
|
|
251
|
+
seats.add_argument("--car-priority", choices=("center", "low", "high"), default="center")
|
|
252
|
+
seats.add_argument("--seat-priority", choices=("forward-window", "window-forward", "row-low"), default="forward-window")
|
|
253
|
+
seats.add_argument("--limit", type=int, default=100, help="호차별 출력할 최대 좌석 수")
|
|
254
|
+
seats.set_defaults(func=command_seats)
|
|
255
|
+
return parser
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def add_trip_args(parser: argparse.ArgumentParser) -> None:
|
|
259
|
+
parser.add_argument("dep", help="출발역")
|
|
260
|
+
parser.add_argument("arr", help="도착역")
|
|
261
|
+
parser.add_argument("date", help="출발일 YYYYMMDD")
|
|
262
|
+
parser.add_argument("time", help="희망 시작 시각 HHMMSS")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def main(argv: list[str] | None = None) -> int:
|
|
266
|
+
args = build_parser().parse_args(argv)
|
|
267
|
+
args.func(args)
|
|
268
|
+
return 0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SrtCar(TypedDict):
|
|
9
|
+
car_no: int
|
|
10
|
+
car_no_raw: str
|
|
11
|
+
room_class: str
|
|
12
|
+
available: bool
|
|
13
|
+
current: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SrtSeat(TypedDict):
|
|
17
|
+
seat: str
|
|
18
|
+
seat_no: str
|
|
19
|
+
available: bool
|
|
20
|
+
direction: str
|
|
21
|
+
position: str
|
|
22
|
+
notes: list[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CAR_RE = re.compile(
|
|
26
|
+
r'<li class="scar-(?P<car>\d+)(?P<class>[^"]*)">(?P<body>.*?)</li>',
|
|
27
|
+
re.DOTALL,
|
|
28
|
+
)
|
|
29
|
+
SEAT_LINK_RE = re.compile(
|
|
30
|
+
r"<a[^>]+selectSeatInfo\(this,\s*'(?P<seat_no>[^']+)',\s*'(?P<seat>[^']+)'\)[^>]*>"
|
|
31
|
+
r".*?<em>\((?P<detail>[^)]*)\)</em>",
|
|
32
|
+
re.DOTALL,
|
|
33
|
+
)
|
|
34
|
+
SEAT_SPAN_RE = re.compile(
|
|
35
|
+
r"<span>\s*(?P<seat>\d+[A-Z])\s*<strong><em>\((?P<detail>[^)]*)\)</em></strong></span>",
|
|
36
|
+
re.DOTALL,
|
|
37
|
+
)
|
|
38
|
+
TAG_RE = re.compile(r"<[^>]+>")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def strip_tags(value: str) -> str:
|
|
42
|
+
return TAG_RE.sub(" ", value).replace("\xa0", " ").strip()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_detail(detail: str) -> tuple[str, str, list[str]]:
|
|
46
|
+
parts = [part.strip() for part in detail.split(",")]
|
|
47
|
+
direction = next((part for part in parts if part in {"정방향", "역방향"}), "unknown")
|
|
48
|
+
position = next((part for part in parts if part in {"창측", "내측", "1인석"}), "unknown")
|
|
49
|
+
notes = [part for part in parts if part not in {direction, position} and part]
|
|
50
|
+
return direction, position, notes
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_cars(html: str) -> list[SrtCar]:
|
|
54
|
+
cars: list[SrtCar] = []
|
|
55
|
+
for match in CAR_RE.finditer(html):
|
|
56
|
+
body = match.group("body")
|
|
57
|
+
text = strip_tags(body)
|
|
58
|
+
room_class = "특실" if "특실" in text else "일반실"
|
|
59
|
+
css_class = match.group("class")
|
|
60
|
+
has_link = "selectScarInfo" in body
|
|
61
|
+
cars.append(
|
|
62
|
+
{
|
|
63
|
+
"car_no": int(match.group("car")),
|
|
64
|
+
"car_no_raw": f"{int(match.group('car')):04d}",
|
|
65
|
+
"room_class": room_class,
|
|
66
|
+
"available": has_link and "off" not in css_class.split(),
|
|
67
|
+
"current": "on" in css_class.split(),
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
return cars
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_seats(html: str) -> list[SrtSeat]:
|
|
74
|
+
seats: list[SrtSeat] = []
|
|
75
|
+
seen: set[str] = set()
|
|
76
|
+
for match in SEAT_LINK_RE.finditer(html):
|
|
77
|
+
direction, position, notes = parse_detail(match.group("detail"))
|
|
78
|
+
seat = match.group("seat")
|
|
79
|
+
seen.add(seat)
|
|
80
|
+
seats.append(
|
|
81
|
+
{
|
|
82
|
+
"seat": seat,
|
|
83
|
+
"seat_no": match.group("seat_no"),
|
|
84
|
+
"available": True,
|
|
85
|
+
"direction": direction,
|
|
86
|
+
"position": position,
|
|
87
|
+
"notes": notes,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
for match in SEAT_SPAN_RE.finditer(html):
|
|
91
|
+
seat = match.group("seat")
|
|
92
|
+
if seat in seen:
|
|
93
|
+
continue
|
|
94
|
+
direction, position, notes = parse_detail(match.group("detail"))
|
|
95
|
+
seats.append(
|
|
96
|
+
{
|
|
97
|
+
"seat": seat,
|
|
98
|
+
"seat_no": "",
|
|
99
|
+
"available": False,
|
|
100
|
+
"direction": direction,
|
|
101
|
+
"position": position,
|
|
102
|
+
"notes": notes,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
return seats
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_seat_label(seat_label: str) -> tuple[int | None, str]:
|
|
109
|
+
match = re.match(r"^(\d+)([A-Z])$", seat_label)
|
|
110
|
+
if match is None:
|
|
111
|
+
return None, ""
|
|
112
|
+
return int(match.group(1)), match.group(2)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def car_center_priority(car: SrtCar, car_numbers: list[int]) -> tuple[float, int]:
|
|
116
|
+
if not car_numbers:
|
|
117
|
+
return (0.0, car["car_no"])
|
|
118
|
+
center = (min(car_numbers) + max(car_numbers)) / 2
|
|
119
|
+
return (abs(car["car_no"] - center), car["car_no"])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def sort_cars_for_booking(cars: list[SrtCar], priority: str = "center") -> list[SrtCar]:
|
|
123
|
+
match priority:
|
|
124
|
+
case "center":
|
|
125
|
+
car_numbers = [car["car_no"] for car in cars]
|
|
126
|
+
return sorted(cars, key=lambda car: car_center_priority(car, car_numbers))
|
|
127
|
+
case "low":
|
|
128
|
+
return sorted(cars, key=lambda car: car["car_no"])
|
|
129
|
+
case "high":
|
|
130
|
+
return sorted(cars, key=lambda car: car["car_no"], reverse=True)
|
|
131
|
+
case _:
|
|
132
|
+
raise ValueError(f"unsupported car priority: {priority}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def seat_preference_key(seat: SrtSeat, priority: str = "forward-window") -> tuple[int, int, int, str]:
|
|
136
|
+
row, column = parse_seat_label(seat["seat"])
|
|
137
|
+
forward_rank = 0 if seat["direction"] == "정방향" else 1
|
|
138
|
+
window_rank = 0 if seat["position"] in {"창측", "1인석"} else 1
|
|
139
|
+
row_rank = 999 if row is None else row
|
|
140
|
+
match priority:
|
|
141
|
+
case "forward-window":
|
|
142
|
+
return (forward_rank, window_rank, row_rank, column)
|
|
143
|
+
case "window-forward":
|
|
144
|
+
return (window_rank, forward_rank, row_rank, column)
|
|
145
|
+
case "row-low":
|
|
146
|
+
return (row_rank, forward_rank, window_rank, column)
|
|
147
|
+
case _:
|
|
148
|
+
raise ValueError(f"unsupported seat priority: {priority}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def sort_seats_for_booking(seats: list[SrtSeat], priority: str = "forward-window") -> list[SrtSeat]:
|
|
152
|
+
return sorted(seats, key=lambda seat: seat_preference_key(seat, priority))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
sort_cars = sort_cars_for_booking
|
|
156
|
+
sort_seats = sort_seats_for_booking
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "srt-booking",
|
|
3
|
+
"description": "Search, reserve, inspect, and cancel SRT tickets in Korea with the SRTrain library. Use when the user asks for SRT seat availability, booking, canceling, or sold-out retry plans. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"vault",
|
|
6
|
+
"browser",
|
|
7
|
+
"action:booking"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: srt-booking\ndescription: Search, reserve, inspect, and cancel SRT tickets in Korea with the SRTrain library. Use when the user asks for SRT seat availability, booking, canceling, or sold-out retry plans. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: travel\n locale: ko-KR\n phase: v1",
|
|
10
|
+
"bundle": [
|
|
11
|
+
{
|
|
12
|
+
"from": "scripts/srt_booking.py",
|
|
13
|
+
"to": "scripts/srt_booking.py"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"from": "scripts/srt_seats.py",
|
|
17
|
+
"to": "scripts/srt_seats.py"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Subway Lost Property
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
지하철에서 잃어버린 물건을 찾기 위해 공식 경로를 구조화한다.
|
|
6
|
+
|
|
7
|
+
- LOST112 `습득물 목록 조회`에 넣을 검색 조건을 정리한다.
|
|
8
|
+
- 서울교통공사 유실물센터 진입점을 함께 안내한다.
|
|
9
|
+
- 공개 API가 명확하지 않은 상태라 v1은 **안내형/하이브리드** 범위로 유지한다.
|
|
10
|
+
|
|
11
|
+
## When to use
|
|
12
|
+
|
|
13
|
+
- "강남역에서 지갑 잃어버렸는데 어디서 찾아?"
|
|
14
|
+
- "2호선 지하철 분실물 조회 방법 알려줘"
|
|
15
|
+
- "서울 지하철 유실물 공식 사이트로 바로 찾게 도와줘"
|
|
16
|
+
|
|
17
|
+
## Inputs
|
|
18
|
+
|
|
19
|
+
- 필수: 역명 또는 보관장소 키워드
|
|
20
|
+
- 선택: 물품명, 호선, 분실/습득 추정 기간
|
|
21
|
+
|
|
22
|
+
## Official surfaces
|
|
23
|
+
|
|
24
|
+
- LOST112 습득물 목록: `https://www.lost112.go.kr/find/findList.do`
|
|
25
|
+
- 서울교통공사 유실물센터: `https://www.seoulmetro.co.kr/kr/page.do?menuIdx=541`
|
|
26
|
+
|
|
27
|
+
LOST112 검색 폼에서 확인되는 핵심 필드는 아래와 같다.
|
|
28
|
+
|
|
29
|
+
- `SITE=V`: 경찰 이외 기관(지하철, 공항 등)
|
|
30
|
+
- `DEP_PLACE`: 보관장소
|
|
31
|
+
- `PRDT_NM`: 습득물명
|
|
32
|
+
- `START_YMD`, `END_YMD`: 검색 기간
|
|
33
|
+
|
|
34
|
+
## Workflow
|
|
35
|
+
|
|
36
|
+
### 1) Ask for the minimum clues first
|
|
37
|
+
|
|
38
|
+
바로 추정하지 말고 아래 정보를 먼저 받는다.
|
|
39
|
+
|
|
40
|
+
- 어느 역/어느 구간인지
|
|
41
|
+
- 물건 종류가 무엇인지
|
|
42
|
+
- 대략 언제 잃어버렸는지
|
|
43
|
+
- 서울교통공사(1~8호선) 범위인지, 다른 운영사인지
|
|
44
|
+
|
|
45
|
+
### 2) Generate the official LOST112 search payload
|
|
46
|
+
|
|
47
|
+
repo helper를 그대로 써도 된다.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python3 scripts/subway_lost_property.py \
|
|
51
|
+
--station 강남역 \
|
|
52
|
+
--item 지갑 \
|
|
53
|
+
--days 14
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
helper는 기본적으로 `SITE=V` 를 사용하고, 역명/물품명/기간을 LOST112 form payload와 **referer까지 포함한 runnable `curl` 예시**로 정리해 준다. 예시 `curl` 은 느린 공식 응답을 감안해 `--max-time 60` 을 포함하고, 응답 HTML을 `lost112-search-result.html` 로 저장한다.
|
|
57
|
+
|
|
58
|
+
### 3) Optionally verify live reachability
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 scripts/subway_lost_property.py \
|
|
62
|
+
--station 강남역 \
|
|
63
|
+
--item 지갑 \
|
|
64
|
+
--days 14 \
|
|
65
|
+
--verify-live
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`--verify-live` 는 공식 페이지 접근 가능 여부만 보수적으로 확인한다. 사이트가 느리면 timeout을 그대로 보고하고 manual open으로 전환한다.
|
|
69
|
+
|
|
70
|
+
### 4) Guide the user conservatively
|
|
71
|
+
|
|
72
|
+
- 먼저 LOST112에서 역명 그대로 검색
|
|
73
|
+
- 결과가 없으면 `강남`처럼 `역` 없는 키워드로 재검색
|
|
74
|
+
- 필요하면 호선명도 추가 검색
|
|
75
|
+
- 서울교통공사 유실물센터 페이지를 함께 열어 후속 안내 확인
|
|
76
|
+
|
|
77
|
+
## Done when
|
|
78
|
+
|
|
79
|
+
- 사용자가 공식 조회 경로를 바로 열 수 있다.
|
|
80
|
+
- LOST112 검색 조건(`SITE=V`, 역명, 물품명, 기간)을 받았다.
|
|
81
|
+
- 자동 조회 보장 범위와 manual fallback을 분명히 설명했다.
|
|
82
|
+
|
|
83
|
+
## Failure modes
|
|
84
|
+
|
|
85
|
+
- 공식 사이트 응답이 느리거나 timeout 발생
|
|
86
|
+
- 역명이 실제 보관장소 표기와 달라 검색 결과가 비는 경우
|
|
87
|
+
- 공개 API 부재로 자동 결과 수집이 안정적이지 않은 경우
|
|
88
|
+
|
|
89
|
+
## Notes
|
|
90
|
+
|
|
91
|
+
- v1은 공식 웹 흐름을 안전하게 안내하는 범위다.
|
|
92
|
+
- 완전 자동 조회형으로 확장하려면 캡차/세션/동적 요청 안정성 재검증이 먼저 필요하다.
|
|
93
|
+
- helper는 공식 HTTPS 진입점만 사용한다.
|