@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,549 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only foresttrip.go.kr vacancy lookup helper.
|
|
3
|
+
|
|
4
|
+
The script logs in with Playwright to obtain a CSRF token and session cookies,
|
|
5
|
+
extracts forest IDs from the official monthly reservation status page, then
|
|
6
|
+
queries the read-only monthly availability JSON endpoint.
|
|
7
|
+
|
|
8
|
+
It intentionally does not click booking buttons, submit reservation forms,
|
|
9
|
+
handle payment, solve captcha, or bypass queues.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import concurrent.futures
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
from dataclasses import asdict, dataclass
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
LOGIN_URL = "https://www.foresttrip.go.kr/com/login.do"
|
|
27
|
+
RSRVT_PAGE = "https://www.foresttrip.go.kr/rep/or/sssn/monthRsrvtSmplStatus.do"
|
|
28
|
+
POST_URL = "https://www.foresttrip.go.kr/rep/or/selectRsrvtAvailInfoListForMonthRsrvtSmpl.do"
|
|
29
|
+
DEFAULT_CONCURRENCY = 4
|
|
30
|
+
MAX_CONCURRENCY = 5
|
|
31
|
+
DEFAULT_WEEK_RANGE = 1
|
|
32
|
+
CATEGORY_CODES = {"01", "02"}
|
|
33
|
+
RESERVE_ROOM_MARKER = "예비"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Session:
|
|
38
|
+
cookies: dict[str, str]
|
|
39
|
+
csrf: str
|
|
40
|
+
user_agent: str
|
|
41
|
+
forests: dict[str, str]
|
|
42
|
+
expires_at: float
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_csv(value: str) -> list[str]:
|
|
46
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_categories(value: str) -> tuple[str, ...]:
|
|
50
|
+
categories = parse_csv(value)
|
|
51
|
+
if not categories:
|
|
52
|
+
raise argparse.ArgumentTypeError("must include at least one category code")
|
|
53
|
+
invalid = [category for category in categories if category not in CATEGORY_CODES]
|
|
54
|
+
if invalid:
|
|
55
|
+
raise argparse.ArgumentTypeError(
|
|
56
|
+
"unknown category code(s): "
|
|
57
|
+
+ ", ".join(invalid)
|
|
58
|
+
+ " (allowed: 01=lodging, 02=camping)"
|
|
59
|
+
)
|
|
60
|
+
return tuple(dict.fromkeys(categories))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_dates(value: str) -> tuple[str, ...]:
|
|
64
|
+
dates = parse_csv(value)
|
|
65
|
+
if not dates:
|
|
66
|
+
raise argparse.ArgumentTypeError("must include at least one YYYYMMDD date")
|
|
67
|
+
|
|
68
|
+
today = datetime.now().date()
|
|
69
|
+
normalized: list[str] = []
|
|
70
|
+
for raw_date in dates:
|
|
71
|
+
try:
|
|
72
|
+
parsed = datetime.strptime(raw_date, "%Y%m%d").date()
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
raise argparse.ArgumentTypeError(f"invalid YYYYMMDD date: {raw_date}") from exc
|
|
75
|
+
if parsed.strftime("%Y%m%d") != raw_date:
|
|
76
|
+
raise argparse.ArgumentTypeError(f"invalid YYYYMMDD date: {raw_date}")
|
|
77
|
+
if parsed < today:
|
|
78
|
+
raise argparse.ArgumentTypeError(f"date is in the past: {raw_date}")
|
|
79
|
+
normalized.append(raw_date)
|
|
80
|
+
return tuple(sorted(dict.fromkeys(normalized)))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_concurrency(value: str) -> int:
|
|
84
|
+
try:
|
|
85
|
+
concurrency = int(value)
|
|
86
|
+
except ValueError as exc:
|
|
87
|
+
raise argparse.ArgumentTypeError("must be an integer") from exc
|
|
88
|
+
if not 1 <= concurrency <= MAX_CONCURRENCY:
|
|
89
|
+
raise argparse.ArgumentTypeError(f"must be between 1 and {MAX_CONCURRENCY}")
|
|
90
|
+
return concurrency
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_week_range(value: str) -> int:
|
|
94
|
+
try:
|
|
95
|
+
week_range = int(value)
|
|
96
|
+
except ValueError as exc:
|
|
97
|
+
raise argparse.ArgumentTypeError("must be an integer") from exc
|
|
98
|
+
if week_range < 1:
|
|
99
|
+
raise argparse.ArgumentTypeError("must be at least 1")
|
|
100
|
+
return week_range
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def parse_args() -> argparse.Namespace:
|
|
104
|
+
parser = argparse.ArgumentParser(
|
|
105
|
+
description="Read-only foresttrip.go.kr vacancy lookup.",
|
|
106
|
+
)
|
|
107
|
+
target = parser.add_argument_group("target selection")
|
|
108
|
+
target.add_argument("--all", action="store_true", help="Scan all extracted forest IDs.")
|
|
109
|
+
target.add_argument(
|
|
110
|
+
"--forest-id",
|
|
111
|
+
action="append",
|
|
112
|
+
help="ForestTrip insttId. Can be passed multiple times or comma-separated.",
|
|
113
|
+
)
|
|
114
|
+
target.add_argument(
|
|
115
|
+
"--forest-name",
|
|
116
|
+
action="append",
|
|
117
|
+
help="Substring to match against official forest names.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
output = parser.add_mutually_exclusive_group()
|
|
121
|
+
output.add_argument("--json", action="store_true", help="Print JSON output.")
|
|
122
|
+
output.add_argument("--text", action="store_true", help="Print human-readable output.")
|
|
123
|
+
parser.add_argument("--dates", type=parse_dates, help="Comma-separated YYYYMMDD dates.")
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--categories",
|
|
126
|
+
type=parse_categories,
|
|
127
|
+
default=("01", "02"),
|
|
128
|
+
help="Comma-separated category codes: 01=lodging, 02=camping.",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--concurrency",
|
|
132
|
+
type=parse_concurrency,
|
|
133
|
+
default=DEFAULT_CONCURRENCY,
|
|
134
|
+
help=f"Parallel POST workers, 1-{MAX_CONCURRENCY}.",
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument("--week-range", type=parse_week_range, help="Weeks ahead to scan when --dates is omitted.")
|
|
137
|
+
parser.add_argument("--refresh-session", action="store_true", help="Ignore session cache.")
|
|
138
|
+
parser.add_argument("--check-deps", action="store_true", help="Check Python and Playwright runtime dependencies.")
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--session-cache",
|
|
141
|
+
default="~/.cache/k-skill/foresttrip-vacancy/session.json",
|
|
142
|
+
help="Session cache path.",
|
|
143
|
+
)
|
|
144
|
+
args = parser.parse_args()
|
|
145
|
+
if args.all and (args.forest_id or args.forest_name):
|
|
146
|
+
parser.error("--all cannot be combined with --forest-id or --forest-name")
|
|
147
|
+
if args.dates and args.week_range is not None:
|
|
148
|
+
parser.error("--week-range cannot be combined with --dates; the lookup range is derived from --dates")
|
|
149
|
+
return args
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def require_env(name: str) -> str:
|
|
153
|
+
value = os.getenv(name)
|
|
154
|
+
if not value:
|
|
155
|
+
raise SystemExit(f"missing required environment variable: {name}")
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def check_dependencies(*, launch_browser: bool = True) -> None:
|
|
160
|
+
if sys.version_info < (3, 9):
|
|
161
|
+
raise SystemExit("python 3.9+ is required")
|
|
162
|
+
try:
|
|
163
|
+
from playwright.sync_api import Error as PlaywrightError # type: ignore[reportMissingImports]
|
|
164
|
+
from playwright.sync_api import sync_playwright # type: ignore[reportMissingImports]
|
|
165
|
+
except ImportError as exc:
|
|
166
|
+
raise SystemExit(
|
|
167
|
+
"playwright is required. Install with: python3 -m pip install playwright"
|
|
168
|
+
) from exc
|
|
169
|
+
|
|
170
|
+
if not launch_browser:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
with sync_playwright() as p:
|
|
175
|
+
browser = p.chromium.launch(headless=True)
|
|
176
|
+
browser.close()
|
|
177
|
+
except PlaywrightError as exc:
|
|
178
|
+
raise SystemExit(
|
|
179
|
+
"playwright chromium browser is required. Install with: "
|
|
180
|
+
"python3 -m playwright install chromium"
|
|
181
|
+
) from exc
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def load_session_cache(path: Path) -> Session | None:
|
|
185
|
+
if not path.exists():
|
|
186
|
+
return None
|
|
187
|
+
try:
|
|
188
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
189
|
+
if time.time() > float(data.get("expires_at", 0)):
|
|
190
|
+
return None
|
|
191
|
+
return Session(
|
|
192
|
+
cookies=dict(data["cookies"]),
|
|
193
|
+
csrf=str(data["csrf"]),
|
|
194
|
+
user_agent=str(data["user_agent"]),
|
|
195
|
+
forests=dict(data["forests"]),
|
|
196
|
+
expires_at=float(data["expires_at"]),
|
|
197
|
+
)
|
|
198
|
+
except Exception:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def save_session_cache(path: Path, session: Session) -> None:
|
|
203
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
path.write_text(json.dumps(asdict(session), ensure_ascii=False), encoding="utf-8")
|
|
205
|
+
try:
|
|
206
|
+
path.chmod(0o600)
|
|
207
|
+
except OSError:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def bootstrap_session(*, forest_id: str, forest_pw: str, ttl_sec: int = 600) -> Session:
|
|
212
|
+
try:
|
|
213
|
+
from playwright.sync_api import Error as PlaywrightError # type: ignore[reportMissingImports]
|
|
214
|
+
from playwright.sync_api import sync_playwright # type: ignore[reportMissingImports]
|
|
215
|
+
except ImportError as exc:
|
|
216
|
+
raise SystemExit(
|
|
217
|
+
"playwright is required. Install with: python3 -m pip install playwright "
|
|
218
|
+
"&& python3 -m playwright install chromium"
|
|
219
|
+
) from exc
|
|
220
|
+
|
|
221
|
+
with sync_playwright() as p:
|
|
222
|
+
try:
|
|
223
|
+
browser = p.chromium.launch(headless=True)
|
|
224
|
+
except PlaywrightError as exc:
|
|
225
|
+
raise SystemExit(
|
|
226
|
+
"playwright chromium browser is required. Install with: "
|
|
227
|
+
"python3 -m playwright install chromium"
|
|
228
|
+
) from exc
|
|
229
|
+
page = browser.new_page()
|
|
230
|
+
page.goto(LOGIN_URL)
|
|
231
|
+
page.fill("#mmberId", forest_id)
|
|
232
|
+
page.fill("#gnrlMmberPssrd", forest_pw)
|
|
233
|
+
page.click("input.loginBtn")
|
|
234
|
+
page.wait_for_load_state("networkidle")
|
|
235
|
+
page.goto(RSRVT_PAGE)
|
|
236
|
+
page.wait_for_load_state("networkidle")
|
|
237
|
+
|
|
238
|
+
csrf_locator = page.locator('input[name="_csrf"]')
|
|
239
|
+
if csrf_locator.count() == 0:
|
|
240
|
+
browser.close()
|
|
241
|
+
raise SystemExit("login succeeded page did not expose a CSRF token")
|
|
242
|
+
csrf = csrf_locator.first.get_attribute("value") or ""
|
|
243
|
+
|
|
244
|
+
forests: dict[str, str] = {}
|
|
245
|
+
regions = page.evaluate(
|
|
246
|
+
"""
|
|
247
|
+
() => Array.from(document.querySelector('#srchSido').options)
|
|
248
|
+
.slice(1)
|
|
249
|
+
.map(o => ({ value: o.value, text: o.textContent.trim() }))
|
|
250
|
+
"""
|
|
251
|
+
)
|
|
252
|
+
for region in regions:
|
|
253
|
+
value = region.get("value")
|
|
254
|
+
if not value:
|
|
255
|
+
continue
|
|
256
|
+
page.select_option("#srchSido", value=value)
|
|
257
|
+
page.wait_for_timeout(500)
|
|
258
|
+
options = page.evaluate(
|
|
259
|
+
"""
|
|
260
|
+
() => Array.from(document.querySelector('#srchInstt').options)
|
|
261
|
+
.slice(1)
|
|
262
|
+
.map(o => ({ value: o.value, text: o.textContent.trim() }))
|
|
263
|
+
"""
|
|
264
|
+
)
|
|
265
|
+
for opt in options:
|
|
266
|
+
fid = str(opt.get("value") or "").strip()
|
|
267
|
+
name = str(opt.get("text") or "").strip()
|
|
268
|
+
if fid and name:
|
|
269
|
+
forests[fid] = name
|
|
270
|
+
|
|
271
|
+
cookies = {cookie["name"]: cookie["value"] for cookie in page.context.cookies()}
|
|
272
|
+
user_agent = page.evaluate("() => navigator.userAgent")
|
|
273
|
+
browser.close()
|
|
274
|
+
|
|
275
|
+
if not csrf or not cookies:
|
|
276
|
+
raise SystemExit("failed to bootstrap foresttrip session")
|
|
277
|
+
if not forests:
|
|
278
|
+
raise SystemExit("failed to extract forest list from reservation page")
|
|
279
|
+
return Session(
|
|
280
|
+
cookies=cookies,
|
|
281
|
+
csrf=csrf,
|
|
282
|
+
user_agent=user_agent,
|
|
283
|
+
forests=forests,
|
|
284
|
+
expires_at=time.time() + ttl_sec,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def get_session(args: argparse.Namespace) -> Session:
|
|
289
|
+
cache_path = Path(args.session_cache).expanduser()
|
|
290
|
+
if not args.refresh_session:
|
|
291
|
+
cached = load_session_cache(cache_path)
|
|
292
|
+
if cached is not None:
|
|
293
|
+
return cached
|
|
294
|
+
session = bootstrap_session(
|
|
295
|
+
forest_id=require_env("KSKILL_FORESTTRIP_ID"),
|
|
296
|
+
forest_pw=require_env("KSKILL_FORESTTRIP_PASSWORD"),
|
|
297
|
+
)
|
|
298
|
+
save_session_cache(cache_path, session)
|
|
299
|
+
return session
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def split_csv(values: list[str] | None) -> list[str]:
|
|
303
|
+
out: list[str] = []
|
|
304
|
+
for value in values or []:
|
|
305
|
+
out.extend(part.strip() for part in value.split(",") if part.strip())
|
|
306
|
+
return out
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def resolve_targets(args: argparse.Namespace, forests: dict[str, str]) -> dict[str, str]:
|
|
310
|
+
if args.all:
|
|
311
|
+
return dict(sorted(forests.items(), key=lambda item: item[1]))
|
|
312
|
+
|
|
313
|
+
requested_ids = split_csv(args.forest_id)
|
|
314
|
+
requested_names = split_csv(args.forest_name)
|
|
315
|
+
targets: dict[str, str] = {}
|
|
316
|
+
for fid in requested_ids:
|
|
317
|
+
targets[fid] = forests.get(fid, fid)
|
|
318
|
+
for needle in requested_names:
|
|
319
|
+
matches = {
|
|
320
|
+
fid: name
|
|
321
|
+
for fid, name in forests.items()
|
|
322
|
+
if needle.replace(" ", "") in name.replace(" ", "")
|
|
323
|
+
}
|
|
324
|
+
targets.update(matches)
|
|
325
|
+
|
|
326
|
+
if not targets:
|
|
327
|
+
raise SystemExit("choose a target with --all, --forest-id, or --forest-name")
|
|
328
|
+
return dict(sorted(targets.items(), key=lambda item: item[1]))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def build_headers(session: Session) -> dict[str, str]:
|
|
332
|
+
cookie_header = "; ".join(f"{k}={v}" for k, v in session.cookies.items())
|
|
333
|
+
return {
|
|
334
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
335
|
+
"Accept-Language": "ko-KR,ko;q=0.9",
|
|
336
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
337
|
+
"Cookie": cookie_header,
|
|
338
|
+
"Origin": "https://www.foresttrip.go.kr",
|
|
339
|
+
"Referer": RSRVT_PAGE,
|
|
340
|
+
"User-Agent": session.user_agent,
|
|
341
|
+
"X-CSRF-Token": session.csrf,
|
|
342
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def fetch_one(
|
|
347
|
+
*,
|
|
348
|
+
session: Session,
|
|
349
|
+
forest_id: str,
|
|
350
|
+
category: str,
|
|
351
|
+
today: str,
|
|
352
|
+
last_day: str,
|
|
353
|
+
) -> tuple[str, str, list[dict[str, Any]] | None, str | None]:
|
|
354
|
+
payload = {
|
|
355
|
+
"insttId": forest_id,
|
|
356
|
+
"upperGoodsClsscCd": category,
|
|
357
|
+
"srchDate": today,
|
|
358
|
+
"lastDay": last_day,
|
|
359
|
+
"inqurSctin": "02",
|
|
360
|
+
}
|
|
361
|
+
body = json.dumps(payload).encode("utf-8")
|
|
362
|
+
request = urllib.request.Request(
|
|
363
|
+
POST_URL,
|
|
364
|
+
data=body,
|
|
365
|
+
headers=build_headers(session),
|
|
366
|
+
method="POST",
|
|
367
|
+
)
|
|
368
|
+
try:
|
|
369
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
370
|
+
if response.status != 200:
|
|
371
|
+
return forest_id, category, None, f"http_{response.status}"
|
|
372
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
373
|
+
if isinstance(data, list):
|
|
374
|
+
return forest_id, category, data, None
|
|
375
|
+
return forest_id, category, None, "unexpected_payload"
|
|
376
|
+
except urllib.error.HTTPError as exc:
|
|
377
|
+
return forest_id, category, None, f"http_{exc.code}"
|
|
378
|
+
except Exception as exc:
|
|
379
|
+
return forest_id, category, None, str(exc)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def is_available(row: dict[str, Any]) -> bool:
|
|
383
|
+
count_value = row.get("rsrvtCnt")
|
|
384
|
+
if count_value is None:
|
|
385
|
+
return False
|
|
386
|
+
try:
|
|
387
|
+
reserved_count = int(count_value)
|
|
388
|
+
except ValueError:
|
|
389
|
+
return False
|
|
390
|
+
return row.get("rsrvtAvail") == "Y" and reserved_count == 0
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def is_reserve_room(row: dict[str, Any]) -> bool:
|
|
394
|
+
return RESERVE_ROOM_MARKER in (row.get("goodsNm") or "")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def normalize_row(row: dict[str, Any], forests: dict[str, str]) -> dict[str, Any]:
|
|
398
|
+
instt_id = str(row.get("insttId") or "")
|
|
399
|
+
return {
|
|
400
|
+
"forest_id": instt_id,
|
|
401
|
+
"forest": forests.get(instt_id, row.get("insttNm") or instt_id),
|
|
402
|
+
"use_dt": row.get("useDt") or "",
|
|
403
|
+
"day": row.get("dywkDtTpcd"),
|
|
404
|
+
"name": row.get("goodsNm") or "",
|
|
405
|
+
"area": row.get("insttArea"),
|
|
406
|
+
"capacity": row.get("mxmmAccptCnt"),
|
|
407
|
+
"category": row.get("goodsClsscNm"),
|
|
408
|
+
"region": row.get("insttAreaNm"),
|
|
409
|
+
"waiting_possible": row.get("wtngPssblYn"),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def collect_results(
|
|
414
|
+
*,
|
|
415
|
+
session: Session,
|
|
416
|
+
targets: dict[str, str],
|
|
417
|
+
categories: tuple[str, ...],
|
|
418
|
+
dates: tuple[str, ...] | None,
|
|
419
|
+
week_range: int | None,
|
|
420
|
+
concurrency: int,
|
|
421
|
+
) -> dict[str, Any]:
|
|
422
|
+
now = datetime.now()
|
|
423
|
+
today = now.strftime("%Y%m%d")
|
|
424
|
+
last_day = (
|
|
425
|
+
max(dates)
|
|
426
|
+
if dates
|
|
427
|
+
else (now + timedelta(weeks=week_range or DEFAULT_WEEK_RANGE)).strftime("%Y%m%d")
|
|
428
|
+
)
|
|
429
|
+
date_filter = set(dates) if dates else None
|
|
430
|
+
failures: list[dict[str, str]] = []
|
|
431
|
+
rows: list[dict[str, Any]] = []
|
|
432
|
+
|
|
433
|
+
jobs = [
|
|
434
|
+
(forest_id, category)
|
|
435
|
+
for forest_id in targets
|
|
436
|
+
for category in categories
|
|
437
|
+
]
|
|
438
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max(1, concurrency)) as pool:
|
|
439
|
+
futures = [
|
|
440
|
+
pool.submit(
|
|
441
|
+
fetch_one,
|
|
442
|
+
session=session,
|
|
443
|
+
forest_id=forest_id,
|
|
444
|
+
category=category,
|
|
445
|
+
today=today,
|
|
446
|
+
last_day=last_day,
|
|
447
|
+
)
|
|
448
|
+
for forest_id, category in jobs
|
|
449
|
+
]
|
|
450
|
+
for future in concurrent.futures.as_completed(futures):
|
|
451
|
+
forest_id, category, data, error = future.result()
|
|
452
|
+
if error is not None or data is None:
|
|
453
|
+
failures.append({"forest_id": forest_id, "category": category, "error": error or "unknown"})
|
|
454
|
+
continue
|
|
455
|
+
for row in data:
|
|
456
|
+
if not is_available(row):
|
|
457
|
+
continue
|
|
458
|
+
if is_reserve_room(row):
|
|
459
|
+
continue
|
|
460
|
+
use_dt = row.get("useDt") or ""
|
|
461
|
+
if use_dt < today or use_dt > last_day:
|
|
462
|
+
continue
|
|
463
|
+
normalized = normalize_row(row, session.forests)
|
|
464
|
+
normalized["source_category"] = category
|
|
465
|
+
if date_filter is not None and normalized["use_dt"] not in date_filter:
|
|
466
|
+
continue
|
|
467
|
+
rows.append(normalized)
|
|
468
|
+
|
|
469
|
+
seen: set[tuple[str, str, str, str]] = set()
|
|
470
|
+
deduped: list[dict[str, Any]] = []
|
|
471
|
+
for row in rows:
|
|
472
|
+
key = (row["forest_id"], row["use_dt"], row["source_category"], row["name"])
|
|
473
|
+
if key in seen:
|
|
474
|
+
continue
|
|
475
|
+
seen.add(key)
|
|
476
|
+
deduped.append(row)
|
|
477
|
+
rows = deduped
|
|
478
|
+
|
|
479
|
+
grouped: dict[str, dict[str, list[dict[str, Any]]]] = {}
|
|
480
|
+
for row in sorted(rows, key=lambda item: (item["forest"], item["use_dt"], item["name"])):
|
|
481
|
+
grouped.setdefault(row["forest"], {}).setdefault(row["use_dt"], []).append(row)
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
"forests_scanned": len(targets),
|
|
485
|
+
"filter_hits": len(rows),
|
|
486
|
+
"fetch_failures": len(failures),
|
|
487
|
+
"failures": failures[:20],
|
|
488
|
+
"concurrency": concurrency,
|
|
489
|
+
"date_range": {"from": today, "to": last_day},
|
|
490
|
+
"results": [
|
|
491
|
+
{
|
|
492
|
+
"forest": forest_name,
|
|
493
|
+
"dates": [
|
|
494
|
+
{"use_dt": use_dt, "rooms": rooms}
|
|
495
|
+
for use_dt, rooms in sorted(rows_by_date.items())
|
|
496
|
+
],
|
|
497
|
+
}
|
|
498
|
+
for forest_name, rows_by_date in sorted(grouped.items())
|
|
499
|
+
],
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def print_text(payload: dict[str, Any]) -> None:
|
|
504
|
+
print("=== ForestTrip Vacancy Lookup ===")
|
|
505
|
+
print(
|
|
506
|
+
f"filter_hits: {payload['filter_hits']} "
|
|
507
|
+
f"fetch_failures: {payload['fetch_failures']} "
|
|
508
|
+
f"forests_scanned: {payload['forests_scanned']}"
|
|
509
|
+
)
|
|
510
|
+
if not payload["results"]:
|
|
511
|
+
print("(no available rooms at lookup time)")
|
|
512
|
+
return
|
|
513
|
+
for forest in payload["results"]:
|
|
514
|
+
print(f"\n{forest['forest']}")
|
|
515
|
+
for date_group in forest["dates"]:
|
|
516
|
+
rooms = date_group["rooms"]
|
|
517
|
+
print(f" {date_group['use_dt']} - {len(rooms)} slot(s)")
|
|
518
|
+
for room in rooms[:8]:
|
|
519
|
+
capacity = room["capacity"] if room["capacity"] is not None else "?"
|
|
520
|
+
area = room["area"] if room["area"] is not None else "?"
|
|
521
|
+
print(f" - {room['name']} / {room['category']} / {area}sqm / max {capacity}")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def main() -> int:
|
|
525
|
+
args = parse_args()
|
|
526
|
+
if args.check_deps:
|
|
527
|
+
check_dependencies()
|
|
528
|
+
print("foresttrip-vacancy dependencies look ready")
|
|
529
|
+
return 0
|
|
530
|
+
session = get_session(args)
|
|
531
|
+
targets = resolve_targets(args, session.forests)
|
|
532
|
+
payload = collect_results(
|
|
533
|
+
session=session,
|
|
534
|
+
targets=targets,
|
|
535
|
+
categories=args.categories,
|
|
536
|
+
dates=args.dates,
|
|
537
|
+
week_range=args.week_range,
|
|
538
|
+
concurrency=args.concurrency,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if args.text and not args.json:
|
|
542
|
+
print_text(payload)
|
|
543
|
+
else:
|
|
544
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
545
|
+
return 0 if payload["fetch_failures"] == 0 else 1
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
if __name__ == "__main__":
|
|
549
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foresttrip-vacancy",
|
|
3
|
+
"description": "Look up available Korean national forest recreation lodging or camping slots on foresttrip.go.kr and, in Dolshoi, continue into the official booking flow with approval-gated payment.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"vault",
|
|
6
|
+
"browser",
|
|
7
|
+
"action:booking"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: foresttrip-vacancy\ndescription: Look up available Korean national forest recreation lodging or camping slots on foresttrip.go.kr and, in Dolshoi, continue into the official booking flow with approval-gated payment.\nlicense: MIT\nmetadata:\n category: travel\n locale: ko-KR\n phase: v1.5"
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 금융위 기업기본정보(법인 개요) 조회
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
공공데이터포털의 **금융위원회_기업기본정보 서비스**(data.go.kr 15043184, `getCorpOutline_V2`)를 `k-skill-proxy` 경유로 호출해 법인 개요를 조회한다.
|
|
6
|
+
|
|
7
|
+
- 법인명(`corpNm`) 기준 후보 목록: 대표자·설립일·업종 등 upstream 필드 원문
|
|
8
|
+
- 사업자번호 교차검증: 응답 item에 `bzno`가 있으면 입력 사업자번호와 정확 일치하는 후보를 분리한다 (`bzno`가 없으면 교차검증 불가 사실을 그대로 표기)
|
|
9
|
+
|
|
10
|
+
이 API의 검색 파라미터는 `crno`(법인등록번호 13자리)/`corpNm`(법인명)뿐이라 **사업자번호 단독 조회가 불가**하다. 법인명으로 조회한다.
|
|
11
|
+
|
|
12
|
+
## Design principles
|
|
13
|
+
|
|
14
|
+
- 점수·등급·해석 라벨을 만들지 않는다. upstream 사실 + 출처만 담는다.
|
|
15
|
+
- `crno`(법인등록번호)는 사업자등록번호와 별개 번호임을 혼동하지 않는다.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
- "이 법인 대표자·설립일·업종 개요 확인해줘"
|
|
20
|
+
- "법인명으로 기업 기본정보 조회해줘"
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- 인터넷 연결, `python3`
|
|
25
|
+
- `scripts/fsc_corporate_info.py` helper
|
|
26
|
+
- hosted/self-host `k-skill-proxy`의 `/v1/fsc/corp-outline` route 접근 가능
|
|
27
|
+
|
|
28
|
+
## Credential requirements
|
|
29
|
+
|
|
30
|
+
- 사용자 측 필수 시크릿 없음.
|
|
31
|
+
- `KSKILL_PROXY_BASE_URL` — self-host 프록시를 쓸 때만 설정. 비우면 hosted `https://k-skill-proxy.nomadamas.org` 사용.
|
|
32
|
+
- `DATA_GO_KR_API_KEY` 는 프록시 운영 서버 환경에만 둔다. 공공데이터포털에서 `금융위원회_기업기본정보` 활용신청이 되어 있어야 한다.
|
|
33
|
+
|
|
34
|
+
## Inputs
|
|
35
|
+
|
|
36
|
+
- `--name`: 법인명(`corpNm`) — 필수
|
|
37
|
+
- `--b-no`: 사업자등록번호. 응답에 `bzno`가 있을 때 교차검증에만 쓰인다.
|
|
38
|
+
|
|
39
|
+
## CLI examples
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python3 fsc-corporate-info/scripts/fsc_corporate_info.py \
|
|
43
|
+
--name "삼성전자" --b-no 124-81-00998
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Failure modes
|
|
47
|
+
|
|
48
|
+
- `400 bad_request`: 법인명을 주지 않음.
|
|
49
|
+
- `503 upstream_not_configured`: 프록시 서버에 `DATA_GO_KR_API_KEY` 없음.
|
|
50
|
+
- `502 upstream_forbidden`: 프록시 키가 15043184에 활용신청되지 않음.
|
|
51
|
+
- 빈 결과: 법인명 불일치 — 표기를 바꿔 재시도.
|
|
52
|
+
|
|
53
|
+
## Official surfaces
|
|
54
|
+
|
|
55
|
+
- 공공데이터포털: <https://www.data.go.kr/data/15043184/openapi.do>
|
|
56
|
+
- upstream: `https://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2`
|
|
57
|
+
- 프록시 route: `GET /v1/fsc/corp-outline`
|