@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,430 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ticket-availability — YES24 / 인터파크 공연 일정 + 잔여석 조회 CLI.
|
|
3
|
+
|
|
4
|
+
조회 전용. 예매·결제·로그인 자동화 없음.
|
|
5
|
+
공연법 §4조의2 (매크로 입장권 부정구매·판매 금지) 비적용.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
ticket-availability schedule <url>
|
|
9
|
+
ticket-availability seats <url> [--all-dates]
|
|
10
|
+
ticket-availability health
|
|
11
|
+
|
|
12
|
+
Supported URLs:
|
|
13
|
+
YES24: https://ticket.yes24.com/Perf/<perf_id>
|
|
14
|
+
https://ticket.yes24.com/New/Perf/Detail/View/<perf_id>
|
|
15
|
+
yes24:<perf_id>
|
|
16
|
+
인터파크: https://tickets.interpark.com/goods/<goods_code>
|
|
17
|
+
interpark:<goods_code>
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import httpx
|
|
31
|
+
except ModuleNotFoundError: # pragma: no cover - depends on user environment
|
|
32
|
+
httpx = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MissingHttpxError(RuntimeError):
|
|
36
|
+
"""Raised when the optional httpx runtime dependency is unavailable."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _require_httpx():
|
|
40
|
+
if httpx is None:
|
|
41
|
+
raise MissingHttpxError(
|
|
42
|
+
"Python package 'httpx' is required. Install it with: python3 -m pip install httpx"
|
|
43
|
+
)
|
|
44
|
+
return httpx
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
HTTPX_HTTP_ERROR = (
|
|
48
|
+
getattr(httpx, "HTTPError", MissingHttpxError) if httpx else MissingHttpxError
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ── URL Parsing ───────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_url(url: str) -> tuple[str, str]:
|
|
55
|
+
"""Return (platform, id). Accepts full URL or `platform:id` shorthand."""
|
|
56
|
+
if url.startswith("yes24:"):
|
|
57
|
+
return "yes24", url[6:]
|
|
58
|
+
if url.startswith("interpark:"):
|
|
59
|
+
return "interpark", url[10:]
|
|
60
|
+
|
|
61
|
+
m = re.search(
|
|
62
|
+
r"yes24\.com/(?:[Nn]ew/)?[Pp]erf/(?:[Dd]etail/)?(?:[Vv]iew/)?(\d+)", url
|
|
63
|
+
)
|
|
64
|
+
if m:
|
|
65
|
+
return "yes24", m.group(1)
|
|
66
|
+
|
|
67
|
+
m = re.search(r"interpark\.com/goods/(\d+)", url, re.IGNORECASE)
|
|
68
|
+
if m:
|
|
69
|
+
return "interpark", m.group(1)
|
|
70
|
+
|
|
71
|
+
if re.fullmatch(r"\d+", url):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"플랫폼을 명시하세요: yes24:{url} 또는 interpark:{url}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
raise ValueError(f"URL을 인식할 수 없습니다: {url}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _fmt_date(d: str) -> str:
|
|
80
|
+
if d and len(d) == 8 and d.isdigit():
|
|
81
|
+
return f"{d[:4]}-{d[4:6]}-{d[6:]}"
|
|
82
|
+
return d
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _fmt_time(t: str) -> str:
|
|
86
|
+
if t and len(t) == 4 and t.isdigit():
|
|
87
|
+
return f"{t[:2]}:{t[2:]}"
|
|
88
|
+
return t
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── HTTP Setup ────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
UA = (
|
|
94
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
95
|
+
"Chrome/124.0.0.0 Safari/537.36"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
HEADERS_YES24 = {
|
|
99
|
+
"User-Agent": UA,
|
|
100
|
+
"Referer": "https://ticket.yes24.com/",
|
|
101
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
102
|
+
"Accept": "*/*",
|
|
103
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
HEADERS_INTERPARK = {
|
|
107
|
+
"User-Agent": UA,
|
|
108
|
+
"Referer": "https://tickets.interpark.com/",
|
|
109
|
+
"Accept": "application/json",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
YES24_BASE = "https://ticket.yes24.com"
|
|
113
|
+
INTERPARK_BASE = "https://api-ticketfront.interpark.com"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── YES24 Client ──────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Yes24Client:
|
|
120
|
+
def __init__(self) -> None:
|
|
121
|
+
http = _require_httpx()
|
|
122
|
+
self.http = http.Client(
|
|
123
|
+
headers=HEADERS_YES24, timeout=20, follow_redirects=True
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _dates(self, perf_id: str, month_count: int) -> list[str]:
|
|
127
|
+
now = datetime.now()
|
|
128
|
+
months: list[str] = []
|
|
129
|
+
for delta in range(month_count):
|
|
130
|
+
month = now.month + delta
|
|
131
|
+
year = now.year + (month - 1) // 12
|
|
132
|
+
month = ((month - 1) % 12) + 1
|
|
133
|
+
months.append(f"{year:04d}-{month:02d}")
|
|
134
|
+
|
|
135
|
+
dates: list[str] = []
|
|
136
|
+
cutoff = now.strftime("%Y%m%d")
|
|
137
|
+
for month_str in months:
|
|
138
|
+
r = self.http.post(
|
|
139
|
+
f"{YES24_BASE}/New/Perf/Sale/Ajax/axPerfDay.aspx",
|
|
140
|
+
data={
|
|
141
|
+
"pGetMode": "days",
|
|
142
|
+
"pIdPerf": perf_id,
|
|
143
|
+
"pPerfMonth": month_str,
|
|
144
|
+
"pIdCode": "",
|
|
145
|
+
"pIsMania": "0",
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
r.raise_for_status()
|
|
149
|
+
text = r.text.strip().strip(",")
|
|
150
|
+
if not text:
|
|
151
|
+
continue
|
|
152
|
+
for raw in text.split(","):
|
|
153
|
+
d = raw.strip()
|
|
154
|
+
if not d:
|
|
155
|
+
continue
|
|
156
|
+
normalized = d.replace("-", "")
|
|
157
|
+
if normalized >= cutoff:
|
|
158
|
+
dates.append(normalized)
|
|
159
|
+
return sorted(set(dates))
|
|
160
|
+
|
|
161
|
+
def get_dates(self, perf_id: str) -> list[str]:
|
|
162
|
+
"""Available dates within ~3 weeks (fast)."""
|
|
163
|
+
return self._dates(perf_id, month_count=3)
|
|
164
|
+
|
|
165
|
+
def get_all_dates(self, perf_id: str) -> list[str]:
|
|
166
|
+
"""Available dates across 6 months (full schedule)."""
|
|
167
|
+
return self._dates(perf_id, month_count=6)
|
|
168
|
+
|
|
169
|
+
def get_slots(self, perf_id: str, perf_day: str) -> list[dict]:
|
|
170
|
+
r = self.http.post(
|
|
171
|
+
f"{YES24_BASE}/NEw/Perf/Detail/Ajax/axPerfPlayTime.aspx",
|
|
172
|
+
data={"IdPerf": perf_id, "PerfDay": perf_day},
|
|
173
|
+
)
|
|
174
|
+
r.raise_for_status()
|
|
175
|
+
html = r.text
|
|
176
|
+
slots: list[dict] = []
|
|
177
|
+
seen: set[str] = set()
|
|
178
|
+
for m in re.finditer(r"idTime='(\d+)'", html):
|
|
179
|
+
id_time = m.group(1)
|
|
180
|
+
if id_time in seen:
|
|
181
|
+
continue
|
|
182
|
+
seen.add(id_time)
|
|
183
|
+
ctx_start = max(0, m.start() - 200)
|
|
184
|
+
ctx = html[ctx_start : m.end() + 200]
|
|
185
|
+
time_m = re.search(r"(\d{1,2}:\d{2}|\d[회]|[12]\d{3}회)", ctx)
|
|
186
|
+
label = time_m.group(0) if time_m else id_time
|
|
187
|
+
slots.append({"idTime": id_time, "label": label})
|
|
188
|
+
return slots
|
|
189
|
+
|
|
190
|
+
def get_seats(self, id_time: str) -> list[dict]:
|
|
191
|
+
r = self.http.post(
|
|
192
|
+
f"{YES24_BASE}/New/Perf/Detail/Ajax/axPerfRemainSeat.aspx",
|
|
193
|
+
data={"Type": "calendar", "IdTime": id_time, "IdLock": "0"},
|
|
194
|
+
)
|
|
195
|
+
r.raise_for_status()
|
|
196
|
+
html = r.text
|
|
197
|
+
seats: list[dict] = []
|
|
198
|
+
for m in re.finditer(
|
|
199
|
+
r"<dt>([^<]+)</dt>\s*<dd>([^<]*)<span[^>]*>\(잔여:(\d+)석\)</span>",
|
|
200
|
+
html,
|
|
201
|
+
):
|
|
202
|
+
seats.append(
|
|
203
|
+
{
|
|
204
|
+
"grade": m.group(1).strip(),
|
|
205
|
+
"price": m.group(2).strip().rstrip(",").strip(),
|
|
206
|
+
"remain": int(m.group(3)),
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
if not seats:
|
|
210
|
+
for i, m in enumerate(re.finditer(r"\(잔여:(\d+)석\)", html)):
|
|
211
|
+
seats.append({"grade": f"좌석{i+1}", "price": "", "remain": int(m.group(1))})
|
|
212
|
+
return seats
|
|
213
|
+
|
|
214
|
+
def schedule(self, perf_id: str, all_dates: bool) -> list[dict]:
|
|
215
|
+
"""Schedule = dates × slots flattened. No seat lookup."""
|
|
216
|
+
dates = self.get_all_dates(perf_id) if all_dates else self.get_dates(perf_id)
|
|
217
|
+
out: list[dict] = []
|
|
218
|
+
for d in dates:
|
|
219
|
+
for slot in self.get_slots(perf_id, d):
|
|
220
|
+
out.append(
|
|
221
|
+
{
|
|
222
|
+
"date": _fmt_date(d),
|
|
223
|
+
"time_label": slot["label"],
|
|
224
|
+
"id_time": slot["idTime"],
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
return out
|
|
228
|
+
|
|
229
|
+
def all_seats(self, perf_id: str, all_dates: bool) -> dict:
|
|
230
|
+
result: dict = {}
|
|
231
|
+
dates = self.get_all_dates(perf_id) if all_dates else self.get_dates(perf_id)
|
|
232
|
+
for d in dates:
|
|
233
|
+
for slot in self.get_slots(perf_id, d):
|
|
234
|
+
seats = self.get_seats(slot["idTime"])
|
|
235
|
+
key = f"{_fmt_date(d)}|{slot['label']}"
|
|
236
|
+
result[key] = {
|
|
237
|
+
"date": _fmt_date(d),
|
|
238
|
+
"time_label": slot["label"],
|
|
239
|
+
"id_time": slot["idTime"],
|
|
240
|
+
"seats": seats,
|
|
241
|
+
}
|
|
242
|
+
time.sleep(0.4)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ── Interpark Client ──────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class InterparkClient:
|
|
250
|
+
def __init__(self) -> None:
|
|
251
|
+
http = _require_httpx()
|
|
252
|
+
self.http = http.Client(
|
|
253
|
+
headers=HEADERS_INTERPARK, timeout=20, follow_redirects=True
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def get_schedule(self, goods_code: str) -> list[dict]:
|
|
257
|
+
now = datetime.now()
|
|
258
|
+
r = self.http.get(
|
|
259
|
+
f"{INTERPARK_BASE}/v1/goods/{goods_code}/playSeq",
|
|
260
|
+
params={
|
|
261
|
+
"goodsCode": goods_code,
|
|
262
|
+
"isBookableDate": "true",
|
|
263
|
+
"page": "1",
|
|
264
|
+
"pageSize": "200",
|
|
265
|
+
"startDate": now.strftime("%Y%m%d"),
|
|
266
|
+
"endDate": f"{now.year + 1}{now.month:02d}{now.day:02d}",
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
r.raise_for_status()
|
|
270
|
+
data = r.json()
|
|
271
|
+
if isinstance(data, list):
|
|
272
|
+
return data
|
|
273
|
+
return data.get("response", {}).get("data") or data.get("data") or []
|
|
274
|
+
|
|
275
|
+
def get_seats(self, goods_code: str, play_seq: str) -> list[dict]:
|
|
276
|
+
r = self.http.get(
|
|
277
|
+
f"{INTERPARK_BASE}/v1/goods/{goods_code}/playSeq/PlaySeq/{play_seq}/REMAINSEAT"
|
|
278
|
+
)
|
|
279
|
+
r.raise_for_status()
|
|
280
|
+
data = r.json()
|
|
281
|
+
if isinstance(data, dict):
|
|
282
|
+
return (
|
|
283
|
+
data.get("remainSeat")
|
|
284
|
+
or (data.get("data") or {}).get("remainSeat")
|
|
285
|
+
or data.get("response", {}).get("remainSeat")
|
|
286
|
+
or []
|
|
287
|
+
)
|
|
288
|
+
return []
|
|
289
|
+
|
|
290
|
+
def schedule(self, goods_code: str) -> list[dict]:
|
|
291
|
+
out: list[dict] = []
|
|
292
|
+
for item in self.get_schedule(goods_code):
|
|
293
|
+
out.append(
|
|
294
|
+
{
|
|
295
|
+
"date": _fmt_date(item.get("playDate", "")),
|
|
296
|
+
"time": _fmt_time(item.get("playTime", "")),
|
|
297
|
+
"play_seq": item.get("playSeq", ""),
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
return out
|
|
301
|
+
|
|
302
|
+
def all_seats(self, goods_code: str) -> dict:
|
|
303
|
+
result: dict = {}
|
|
304
|
+
for item in self.get_schedule(goods_code):
|
|
305
|
+
seq = item.get("playSeq", "")
|
|
306
|
+
if not seq:
|
|
307
|
+
continue
|
|
308
|
+
seats_raw = self.get_seats(goods_code, seq)
|
|
309
|
+
normalized = [
|
|
310
|
+
{
|
|
311
|
+
"grade": s.get("seatGradeName", s.get("seatGrade", "")),
|
|
312
|
+
"remain": int(s.get("remainCnt", 0)),
|
|
313
|
+
}
|
|
314
|
+
for s in seats_raw
|
|
315
|
+
]
|
|
316
|
+
key = f"{_fmt_date(item.get('playDate', ''))}|{_fmt_time(item.get('playTime', ''))}|{seq}"
|
|
317
|
+
result[key] = {
|
|
318
|
+
"date": _fmt_date(item.get("playDate", "")),
|
|
319
|
+
"time": _fmt_time(item.get("playTime", "")),
|
|
320
|
+
"play_seq": seq,
|
|
321
|
+
"seats": normalized,
|
|
322
|
+
}
|
|
323
|
+
time.sleep(0.3)
|
|
324
|
+
return result
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ── CLI ───────────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _dump(obj: Any, compact: bool) -> str:
|
|
331
|
+
if compact:
|
|
332
|
+
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
333
|
+
return json.dumps(obj, ensure_ascii=False, indent=2)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def cmd_schedule(args: argparse.Namespace) -> int:
|
|
337
|
+
platform, pid = parse_url(args.url)
|
|
338
|
+
if platform == "yes24":
|
|
339
|
+
out = Yes24Client().schedule(pid, all_dates=args.all_dates)
|
|
340
|
+
else:
|
|
341
|
+
out = InterparkClient().schedule(pid)
|
|
342
|
+
print(_dump({"platform": platform, "id": pid, "schedule": out}, args.compact))
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def cmd_seats(args: argparse.Namespace) -> int:
|
|
347
|
+
platform, pid = parse_url(args.url)
|
|
348
|
+
if platform == "yes24":
|
|
349
|
+
out = Yes24Client().all_seats(pid, all_dates=args.all_dates)
|
|
350
|
+
else:
|
|
351
|
+
out = InterparkClient().all_seats(pid)
|
|
352
|
+
print(_dump({"platform": platform, "id": pid, "seats": out}, args.compact))
|
|
353
|
+
return 0
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def cmd_health(args: argparse.Namespace) -> int:
|
|
357
|
+
http = _require_httpx()
|
|
358
|
+
results: dict = {}
|
|
359
|
+
for name, url in [
|
|
360
|
+
("yes24",
|
|
361
|
+
f"{YES24_BASE}/New/Perf/Sale/Ajax/axPerfDay.aspx"),
|
|
362
|
+
("interpark",
|
|
363
|
+
f"{INTERPARK_BASE}/v1/goods/00000000/playSeq"),
|
|
364
|
+
]:
|
|
365
|
+
try:
|
|
366
|
+
if name == "yes24":
|
|
367
|
+
r = http.post(url, headers=HEADERS_YES24,
|
|
368
|
+
data={"pGetMode": "days", "pIdPerf": "0",
|
|
369
|
+
"pPerfMonth": "2000-01", "pIdCode": "",
|
|
370
|
+
"pIsMania": "0"}, timeout=10)
|
|
371
|
+
else:
|
|
372
|
+
r = http.get(url, headers=HEADERS_INTERPARK,
|
|
373
|
+
params={"goodsCode": "00000000",
|
|
374
|
+
"isBookableDate": "true",
|
|
375
|
+
"page": "1", "pageSize": "1",
|
|
376
|
+
"startDate": "20000101",
|
|
377
|
+
"endDate": "20000102"},
|
|
378
|
+
timeout=10)
|
|
379
|
+
results[name] = {"status": r.status_code, "ok": r.status_code < 500}
|
|
380
|
+
except Exception as e:
|
|
381
|
+
results[name] = {"status": 0, "ok": False, "error": str(e)}
|
|
382
|
+
print(_dump(results, args.compact))
|
|
383
|
+
return 0 if all(v.get("ok") for v in results.values()) else 1
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def main(argv: list[str] | None = None) -> int:
|
|
387
|
+
parser = argparse.ArgumentParser(
|
|
388
|
+
prog="ticket-availability",
|
|
389
|
+
description="YES24 / 인터파크 공연 일정 + 잔여석 조회 (조회 전용)",
|
|
390
|
+
)
|
|
391
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
392
|
+
|
|
393
|
+
def _common(p: argparse.ArgumentParser) -> None:
|
|
394
|
+
p.add_argument("--compact", action="store_true",
|
|
395
|
+
help="One-line JSON (기본: 들여쓰기 출력)")
|
|
396
|
+
|
|
397
|
+
p_sch = sub.add_parser("schedule", help="공연 일정 조회")
|
|
398
|
+
p_sch.add_argument("url", help="공연 URL 또는 platform:id")
|
|
399
|
+
p_sch.add_argument("--all-dates", action="store_true",
|
|
400
|
+
help="YES24 — 6개월 전체 (기본: 3주)")
|
|
401
|
+
_common(p_sch)
|
|
402
|
+
p_sch.set_defaults(func=cmd_schedule)
|
|
403
|
+
|
|
404
|
+
p_st = sub.add_parser("seats", help="등급별 잔여석 조회 (전 일정)")
|
|
405
|
+
p_st.add_argument("url", help="공연 URL 또는 platform:id")
|
|
406
|
+
p_st.add_argument("--all-dates", action="store_true",
|
|
407
|
+
help="YES24 — 6개월 전체 (기본: 3주)")
|
|
408
|
+
_common(p_st)
|
|
409
|
+
p_st.set_defaults(func=cmd_seats)
|
|
410
|
+
|
|
411
|
+
p_h = sub.add_parser("health", help="API endpoint reachability check")
|
|
412
|
+
_common(p_h)
|
|
413
|
+
p_h.set_defaults(func=cmd_health)
|
|
414
|
+
|
|
415
|
+
args = parser.parse_args(argv)
|
|
416
|
+
try:
|
|
417
|
+
return args.func(args)
|
|
418
|
+
except ValueError as e:
|
|
419
|
+
print(f"error: {e}", file=sys.stderr)
|
|
420
|
+
return 2
|
|
421
|
+
except MissingHttpxError as e:
|
|
422
|
+
print(f"dependency error: {e}", file=sys.stderr)
|
|
423
|
+
return 4
|
|
424
|
+
except HTTPX_HTTP_ERROR as e:
|
|
425
|
+
print(f"http error: {e}", file=sys.stderr)
|
|
426
|
+
return 3
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
sys.exit(main())
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ticket-availability",
|
|
3
|
+
"description": "YES24 / 인터파크 공연의 공개 일정 + 등급별 잔여석을 단일 HTTP 호출로 조회 (조회 전용, 예매·결제 없음). 돌쇠에서는 가능한 가장 가까운 합법적 공식 단계까지 준비한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"browser",
|
|
6
|
+
"hard-boundary"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: ticket-availability\ndescription: YES24 / 인터파크 공연의 공개 일정 + 등급별 잔여석을 단일 HTTP 호출로 조회 (조회 전용, 예매·결제 없음). 돌쇠에서는 가능한 가장 가까운 합법적 공식 단계까지 준비한다.\nlicense: MIT\nmetadata:\n category: lifestyle\n subcategory: ticket\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Toss Securities
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
토스증권 **조회 전용(read-only)** 흐름을 실행한다. 두 경로가 있다.
|
|
6
|
+
|
|
7
|
+
1. **공식 Open API (권장)** — 토스증권 공식 Open API(`https://openapi.tossinvest.com`)를 OAuth 2.0 Client Credentials 토큰으로 호출.
|
|
8
|
+
2. **tossctl fallback** — 공식 credentials가 없을 때 `JungHoonGhae/tossinvest-cli` 의 `tossctl` 을 사용.
|
|
9
|
+
|
|
10
|
+
조회 항목:
|
|
11
|
+
|
|
12
|
+
- 계좌 목록 / 보유 주식
|
|
13
|
+
- 시세(현재가/호가/체결/상하한가/캔들) / 종목 정보 / 매수 유의사항
|
|
14
|
+
- 환율 / 장 운영 캘린더(KR·US)
|
|
15
|
+
- 대기중 주문 조회 / 주문 상세 / 매수가능금액 / 판매가능수량 / 수수료
|
|
16
|
+
- (tossctl fallback) 계좌 요약, 포트폴리오 비중, 관심종목
|
|
17
|
+
|
|
18
|
+
## When to use
|
|
19
|
+
|
|
20
|
+
- "토스증권 삼성전자 현재가 확인해줘"
|
|
21
|
+
- "내 보유 주식 보여줘"
|
|
22
|
+
- "대기중 주문 조회해줘"
|
|
23
|
+
- "원달러 환율 알려줘"
|
|
24
|
+
|
|
25
|
+
## 1. Prefer the official Open API
|
|
26
|
+
|
|
27
|
+
### Prerequisites
|
|
28
|
+
|
|
29
|
+
- 토스증권 OpenAPI 콘솔에서 발급한 `client_id` / `client_secret`
|
|
30
|
+
- Node.js 18+ (global `fetch`)
|
|
31
|
+
|
|
32
|
+
자격 증명은 사용자 환경변수로 두고 helper가 토스 서버로 **직접** 호출한다. 공유 프록시로 보내지 않는다.
|
|
33
|
+
|
|
34
|
+
| 환경변수 | 설명 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `TOSSINVEST_CLIENT_ID` | client id (필수) |
|
|
37
|
+
| `TOSSINVEST_CLIENT_SECRET` | client secret (필수) |
|
|
38
|
+
| `TOSSINVEST_ACCOUNT` | accountSeq. 계좌·자산·주문조회에 필요 (선택) |
|
|
39
|
+
| `TOSSINVEST_API_BASE_URL` | 기본 `https://openapi.tossinvest.com` (선택) |
|
|
40
|
+
|
|
41
|
+
### Workflow
|
|
42
|
+
|
|
43
|
+
helper는 내부적으로 `POST /oauth2/token` 으로 토큰을 발급(Client Credentials)받아 `Authorization: Bearer` 로 호출한다. 계좌·자산·주문조회 API는 `X-Tossinvest-Account` 헤더가 추가로 필요하다.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const {
|
|
47
|
+
getPrices,
|
|
48
|
+
listOfficialAccounts,
|
|
49
|
+
getHoldings
|
|
50
|
+
} = require("toss-securities");
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const prices = await getPrices(["005930", "AAPL"]);
|
|
54
|
+
|
|
55
|
+
const accounts = await listOfficialAccounts();
|
|
56
|
+
const accountSeq = accounts.data.result[0].accountSeq;
|
|
57
|
+
const holdings = await getHoldings({ account: accountSeq });
|
|
58
|
+
|
|
59
|
+
console.log(prices.data);
|
|
60
|
+
console.log(holdings.data);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main().catch((error) => {
|
|
64
|
+
console.error(error);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `429` 는 `Retry-After`/`X-RateLimit-Reset` 만큼 대기 후 백오프 재시도한다.
|
|
70
|
+
- `401` 은 토큰을 1회 재발급해 재시도한다.
|
|
71
|
+
- `client_secret`/토큰은 에러 메시지에서 마스킹된다.
|
|
72
|
+
|
|
73
|
+
## 2. tossctl fallback
|
|
74
|
+
|
|
75
|
+
공식 credentials가 없으면 비공식 `tossctl` 을 fallback으로 쓴다.
|
|
76
|
+
|
|
77
|
+
### Install `tossctl` first when missing
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
brew tap JungHoonGhae/tossinvest-cli
|
|
81
|
+
brew install tossctl
|
|
82
|
+
tossctl doctor
|
|
83
|
+
tossctl auth doctor
|
|
84
|
+
tossctl auth login
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
로그인 세션이 없으면 먼저 위 흐름을 끝낸다. 다른 비공식 크롤링이나 임의 HTTP 재구현으로 우회하지 않는다.
|
|
88
|
+
|
|
89
|
+
지원하는 read-only 명령:
|
|
90
|
+
|
|
91
|
+
- `tossctl account summary --output json`
|
|
92
|
+
- `tossctl portfolio positions --output json`
|
|
93
|
+
- `tossctl quote get TSLA --output json`
|
|
94
|
+
- `tossctl watchlist list --output json`
|
|
95
|
+
- `tossctl orders completed --market all --output json`
|
|
96
|
+
|
|
97
|
+
패키지 wrapper(`getAccountSummary`, `getPortfolioPositions`, `getQuote`, `listWatchlist` 등)도 그대로 쓸 수 있다.
|
|
98
|
+
|
|
99
|
+
## Answer conservatively
|
|
100
|
+
|
|
101
|
+
- 계좌번호/민감정보는 꼭 필요한 범위만 노출한다.
|
|
102
|
+
- 사용자가 "오늘" 같은 상대 날짜를 말하면 절대 날짜로 풀어 답한다.
|
|
103
|
+
- 이 스킬은 조회 전용이다. 실거래 mutation 은 범위 밖이라고 분명히 말한다.
|
|
104
|
+
|
|
105
|
+
## Done when
|
|
106
|
+
|
|
107
|
+
- 공식 API credentials(또는 tossctl 로그인) 상태가 확인되었다.
|
|
108
|
+
- 요청에 맞는 read-only 호출을 실행했다.
|
|
109
|
+
- 결과를 한국어로 짧게 정리했다.
|
|
110
|
+
|
|
111
|
+
## Failure modes
|
|
112
|
+
|
|
113
|
+
- 공식 API credentials(`TOSSINVEST_CLIENT_ID`/`SECRET`)가 없으면 `TossCredentialsError` 로 명확히 실패한다.
|
|
114
|
+
- 계좌·자산·주문조회 helper에 `X-Tossinvest-Account` 가 없으면 네트워크 호출 전에 실패한다.
|
|
115
|
+
- tossctl fallback은 `auth login` 전이면 계좌/포트폴리오 조회가 실패할 수 있다.
|
|
116
|
+
- 계좌/주문 정보는 민감하므로 출력 범위를 과도하게 넓히지 않는다.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "toss-securities",
|
|
3
|
+
"description": "토스증권 조회형 질문을 공식 Open API(OAuth2)로 우선 처리하고, 공식 credentials가 없으면 tossinvest-cli의 tossctl을 fallback으로 써서 계좌, 보유주식, 시세/종목/시장정보, 주문조회를 안전한 read-only 흐름으로 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"vault",
|
|
6
|
+
"action:account"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: toss-securities\ndescription: 토스증권 조회형 질문을 공식 Open API(OAuth2)로 우선 처리하고, 공식 credentials가 없으면 tossinvest-cli의 tossctl을 fallback으로 써서 계좌, 보유주식, 시세/종목/시장정보, 주문조회를 안전한 read-only 흐름으로 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: finance\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Used Car Price Search
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
한국의 대표 렌터카 업체를 먼저 비교하고, 현재는 **가장 기술적으로 구현이 쉬운 공급자**로 확인된 `SK렌터카 다이렉트 타고BUY`를 사용해 중고차 가격을 조회한다.
|
|
6
|
+
|
|
7
|
+
- 한국의 주요 렌터카 업체로 `SK렌터카`, `롯데렌탈(롯데오토옥션)`, `레드캡렌터카`를 먼저 확인한다.
|
|
8
|
+
- 각 업체의 공개 표면에서 **직접 API 제공 여부**, 웹 크롤링 난이도, 기존 **MCP / Skill** 존재 여부를 먼저 점검한다.
|
|
9
|
+
- 이 저장소와 현재 세션에는 중고차 가격 조회용 전용 **MCP** 나 **Skill** 이 없으므로 새 스킬이 직접 조회를 담당한다.
|
|
10
|
+
- 최종 선택 공급자는 `https://www.skdirect.co.kr/tb` 이다.
|
|
11
|
+
- 이 페이지는 로그인 없이 열리고, HTML 안의 `__NEXT_DATA__` 에 현재 inventory snapshot 이 들어 있어 반복 조회가 쉽다.
|
|
12
|
+
- 결과는 **월 렌트료**와 **인수가**를 함께 보여 준다.
|
|
13
|
+
|
|
14
|
+
## Provider survey
|
|
15
|
+
|
|
16
|
+
| 업체 | 점검 결과 | v1 채택 여부 |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| SK렌터카 다이렉트 `타고BUY` | `https://www.skdirect.co.kr/tb` 공개 HTML 에 `__NEXT_DATA__` inventory snapshot 포함. 로그인/세션 없이 반복 조회 가능. 별도 공개 API 문서는 못 찾았지만 SSR 데이터 추출이 가장 단순함. | 채택 |
|
|
19
|
+
| 롯데렌탈 / 롯데오토옥션 | `https://www.lotteautoauction.net/hp/pub/cmm/viewMain.do` 공개 진입점은 열리지만, 일반 매물 검색은 legacy `.do` 흐름 중심이고 공개 목록 계약이 불명확했다. 추정 목록 URL은 404/에러 페이지가 섞여 v1 공급자로는 불안정했다. | 미채택 |
|
|
20
|
+
| 레드캡렌터카 | 공식 진입점이 `https://biz.redcap.co.kr/rent/` business portal 로 이어졌고, 공개 중고차 inventory 검색 표면이나 직접 API를 확인하지 못했다. | 미채택 |
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- "아반떼 중고차 가격 봐줘"
|
|
25
|
+
- "SK렌터카 타고BUY에 K3 얼마야?"
|
|
26
|
+
- "캐스퍼 인수가/월 렌트료 같이 알려줘"
|
|
27
|
+
- "중고차 시세를 렌터카 업체 기준으로 보고 싶어"
|
|
28
|
+
|
|
29
|
+
## When not to use
|
|
30
|
+
|
|
31
|
+
- 실제 구매/계약/상담 신청까지 자동화해야 하는 경우
|
|
32
|
+
- 특정 VIN/성능기록부/사고이력 원문까지 강제해야 하는 경우
|
|
33
|
+
- 여러 업체 통합 최저가 비교가 필요한 경우
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
- 인터넷 연결
|
|
38
|
+
- `node` 18+
|
|
39
|
+
- `used-car-price-search` package 또는 동일 로직
|
|
40
|
+
|
|
41
|
+
## Required inputs
|
|
42
|
+
|
|
43
|
+
### 1. Ask the car model/keyword first if it is missing
|
|
44
|
+
|
|
45
|
+
차종 키워드가 없으면 먼저 물어본다.
|
|
46
|
+
|
|
47
|
+
- 권장 질문: `어떤 차종을 찾을까요? 예: 아반떼, K3, 캐스퍼`
|
|
48
|
+
- 너무 넓으면: `제조사나 차종을 조금 더 구체적으로 알려주세요. 예: 현대 아반떼, 기아 K3`
|
|
49
|
+
|
|
50
|
+
## Official surface used in v1
|
|
51
|
+
|
|
52
|
+
- SK direct used-car inventory page: `https://www.skdirect.co.kr/tb`
|
|
53
|
+
|
|
54
|
+
## Workflow
|
|
55
|
+
|
|
56
|
+
1. 차종 키워드가 없으면 먼저 질문한다.
|
|
57
|
+
2. `SK렌터카`, `롯데렌탈`, `레드캡렌터카` 비교 결과를 짧게 기억하고, 현재 공급자는 `SK렌터카 다이렉트 타고BUY` 임을 유지한다.
|
|
58
|
+
3. `https://www.skdirect.co.kr/tb` HTML 을 가져온다.
|
|
59
|
+
4. HTML 의 `__NEXT_DATA__` JSON 에서 `carListProd` inventory snapshot 을 읽는다.
|
|
60
|
+
5. 차종 키워드로 inventory 를 필터링한다.
|
|
61
|
+
6. 상위 결과에서 `인수가`, `월 렌트료`, `연식`, `주행거리`, `연료`, `변속기`를 정리한다.
|
|
62
|
+
7. 같은 차종이라도 재고가 수시로 바뀔 수 있으므로 snapshot 기준 응답임을 짧게 알린다.
|
|
63
|
+
|
|
64
|
+
## Node.js example
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const { lookupUsedCarPrices } = require("used-car-price-search")
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
const result = await lookupUsedCarPrices("아반떼", { limit: 5 })
|
|
71
|
+
|
|
72
|
+
console.log({
|
|
73
|
+
provider: result.provider,
|
|
74
|
+
matchedCount: result.matchedCount,
|
|
75
|
+
summary: result.summary,
|
|
76
|
+
items: result.items
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch((error) => {
|
|
81
|
+
console.error(error)
|
|
82
|
+
process.exitCode = 1
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Respond conservatively
|
|
87
|
+
|
|
88
|
+
응답은 아래 순서로 짧게 정리한다.
|
|
89
|
+
|
|
90
|
+
- 공급자: `SK렌터카 다이렉트 타고BUY`
|
|
91
|
+
- 차종 키워드
|
|
92
|
+
- 매칭된 차량 수
|
|
93
|
+
- 인수가 범위
|
|
94
|
+
- 월 렌트료 범위
|
|
95
|
+
- 대표 차량 2~5개
|
|
96
|
+
- `공개 inventory snapshot 기준이라 실시간 재고/가격은 바뀔 수 있다`는 안내
|
|
97
|
+
|
|
98
|
+
## Done when
|
|
99
|
+
|
|
100
|
+
- 주요 렌터카 업체 비교와 공급자 선택 이유를 설명했다.
|
|
101
|
+
- 차종 키워드 기준으로 결과를 최소 1건 이상 또는 보수적 빈 결과로 반환했다.
|
|
102
|
+
- 결과에 `인수가` 와 `월 렌트료` 를 함께 담았다.
|
|
103
|
+
- 라이브 검증에서 **최소 10회 이상** 반복 조회가 가능함을 확인했다.
|
|
104
|
+
|
|
105
|
+
## Failure modes
|
|
106
|
+
|
|
107
|
+
- 공개 inventory snapshot 은 페이지 갱신 타이밍에 따라 달라질 수 있다.
|
|
108
|
+
- 별도 공개 API 문서는 찾지 못했으므로 v1 은 HTML 내 `__NEXT_DATA__` 의 안정성에 의존한다.
|
|
109
|
+
- 특정 차종 키워드가 너무 넓으면 유사 모델이 함께 섞일 수 있다.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "used-car-price-search",
|
|
3
|
+
"description": "주요 한국 렌터카 업체를 비교한 뒤 SK렌터카 다이렉트 타고BUY inventory snapshot 으로 중고차 가격/인수가를 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"action:commerce"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: used-car-price-search\ndescription: 주요 한국 렌터카 업체를 비교한 뒤 SK렌터카 다이렉트 타고BUY inventory snapshot 으로 중고차 가격/인수가를 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: automotive\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|