@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,113 @@
|
|
|
1
|
+
"""FSC corporate-outline lookup via k-skill-proxy.
|
|
2
|
+
|
|
3
|
+
The proxy holds DATA_GO_KR_API_KEY server-side; this helper only builds the
|
|
4
|
+
query and reads the structured response. No user secret is required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import urllib.request
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
PROXY_BASE_URL_ENV_VAR = "KSKILL_PROXY_BASE_URL"
|
|
20
|
+
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
21
|
+
PROXY_DOWN_MSG = "설정된 k-skill-proxy 서버가 응답하지 않습니다. 잠시 후 재시도하거나 운영자에게 문의하세요."
|
|
22
|
+
PROXY_KEY_NOT_CONFIGURED_MSG = "k-skill-proxy에 필요한 API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요."
|
|
23
|
+
ROUTE = "/v1/fsc/corp-outline"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ApiError(RuntimeError):
|
|
27
|
+
def __init__(self, message: str, *, status_code: int | None = None):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _text_or_none(value: Any) -> str | None:
|
|
33
|
+
if value is None:
|
|
34
|
+
return None
|
|
35
|
+
text = str(value).strip()
|
|
36
|
+
return text or None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_proxy_base_url(explicit: str | None = None, env: dict[str, str] | None = None) -> str:
|
|
40
|
+
env = os.environ if env is None else env
|
|
41
|
+
candidate = _text_or_none(explicit or env.get(PROXY_BASE_URL_ENV_VAR))
|
|
42
|
+
if candidate and candidate.casefold() in {"off", "false", "0", "disable", "disabled", "none"}:
|
|
43
|
+
raise ValueError("KSKILL_PROXY_BASE_URL 가 비활성화되어 있습니다.")
|
|
44
|
+
if candidate and candidate != "replace-me":
|
|
45
|
+
return candidate.rstrip("/")
|
|
46
|
+
return DEFAULT_PROXY_BASE_URL
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_json_response(request: urllib.request.Request) -> dict[str, Any]:
|
|
50
|
+
try:
|
|
51
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
52
|
+
try:
|
|
53
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
54
|
+
except json.JSONDecodeError as error:
|
|
55
|
+
raise ApiError("fsc corp-outline proxy returned invalid JSON.") from error
|
|
56
|
+
if not isinstance(payload, dict):
|
|
57
|
+
raise ApiError("fsc corp-outline proxy returned a non-object JSON payload.")
|
|
58
|
+
return payload
|
|
59
|
+
except urllib.error.HTTPError as error:
|
|
60
|
+
body = error.read().decode("utf-8", errors="replace")
|
|
61
|
+
try:
|
|
62
|
+
payload = json.loads(body)
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
payload = None
|
|
65
|
+
if error.code == 503 and isinstance(payload, dict) and payload.get("error") == "upstream_not_configured":
|
|
66
|
+
raise ApiError(PROXY_KEY_NOT_CONFIGURED_MSG, status_code=error.code) from error
|
|
67
|
+
if isinstance(payload, dict) and payload.get("message"):
|
|
68
|
+
raise ApiError(str(payload["message"]), status_code=error.code) from error
|
|
69
|
+
raise ApiError(f"fsc corp-outline proxy request failed with HTTP {error.code}", status_code=error.code) from error
|
|
70
|
+
except urllib.error.URLError as error:
|
|
71
|
+
raise ApiError(f"{PROXY_DOWN_MSG} (상세: {error.reason})") from error
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def query_corp_outline(name: str, b_no: str | None = None, *, base_url: str | None = None,
|
|
75
|
+
read_json: Any = read_json_response) -> dict[str, Any]:
|
|
76
|
+
name = _text_or_none(name)
|
|
77
|
+
if not name:
|
|
78
|
+
raise ValueError("법인명(corpNm)을 입력하세요. 이 API는 사업자번호 단독 조회가 불가합니다.")
|
|
79
|
+
params = {"name": name}
|
|
80
|
+
if _text_or_none(b_no):
|
|
81
|
+
digits = re.sub(r"\D", "", str(b_no))
|
|
82
|
+
if not re.fullmatch(r"\d{10}", digits):
|
|
83
|
+
raise ValueError("사업자등록번호는 숫자 10자리여야 합니다 (하이픈 허용).")
|
|
84
|
+
params["b_no"] = digits
|
|
85
|
+
url = f"{resolve_proxy_base_url(base_url)}{ROUTE}?{urllib.parse.urlencode(params)}"
|
|
86
|
+
request = urllib.request.Request(url, headers={
|
|
87
|
+
"Accept": "application/json",
|
|
88
|
+
"User-Agent": "k-skill-fsc-corporate-info/1.0",
|
|
89
|
+
}, method="GET")
|
|
90
|
+
return read_json(request)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
94
|
+
parser = argparse.ArgumentParser(description="금융위 기업기본정보(법인 개요) 조회 (k-skill-proxy 경유)")
|
|
95
|
+
parser.add_argument("--name", required=True, help="법인명(corpNm) — 필수")
|
|
96
|
+
parser.add_argument("--b-no", help="사업자등록번호 — 응답에 bzno가 있을 때 교차검증에만 사용")
|
|
97
|
+
parser.add_argument("--proxy-base-url")
|
|
98
|
+
return parser
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main(argv: list[str] | None = None) -> int:
|
|
102
|
+
args = build_parser().parse_args(argv)
|
|
103
|
+
try:
|
|
104
|
+
result = query_corp_outline(args.name, args.b_no, base_url=args.proxy_base_url)
|
|
105
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
106
|
+
return 0
|
|
107
|
+
except (ValueError, ApiError) as error:
|
|
108
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fsc-corporate-info",
|
|
3
|
+
"description": "금융위원회 기업기본정보(법인 개요)를 공공데이터포털 API(k-skill-proxy 경유)로 조회한다. 법인명으로 대표자·설립일·업종 등 법인 개요를 확인하고, 응답에 사업자번호가 있으면 입력 번호와 교차검증한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"lookup"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: fsc-corporate-info\ndescription: 금융위원회 기업기본정보(법인 개요)를 공공데이터포털 API(k-skill-proxy 경유)로 조회한다. 법인명으로 대표자·설립일·업종 등 법인 개요를 확인하고, 응답에 사업자번호가 있으면 입력 번호와 교차검증한다.\nlicense: MIT\nmetadata:\n category: business\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# 나라장터 발주계획 검색
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
`g2b-order-plan-search`는 공공데이터포털의 **조달청_나라장터 발주계획현황서비스**(data.go.kr 15129462, `OrderPlanSttusService`)를 `k-skill-proxy` 경유로 호출해 나라장터에 등록된 발주계획 목록을 조회한다.
|
|
6
|
+
|
|
7
|
+
- 대상 업무: 물품, 공사, 용역, 외자
|
|
8
|
+
- 검색 조건: 발주년월 범위, 게시일시 범위, 발주기관명/코드, 사업명 키워드, 조달방식, 기관소재지, 세부품명번호, 업무유형, 공종 등
|
|
9
|
+
- 반환: 발주년도/월, 발주기관, 사업명, 계약방법, 발주금액, 담당부서/담당자/전화번호, 입찰공고번호목록 등 upstream 원문 필드
|
|
10
|
+
|
|
11
|
+
입찰 참가, 로그인, 입찰서 제출, 인증서 작업, 결제, 낙찰/계약 처리 자동화는 하지 않는다.
|
|
12
|
+
|
|
13
|
+
## Public access path discovered
|
|
14
|
+
|
|
15
|
+
### Primary source: 공공데이터포털 API through k-skill-proxy
|
|
16
|
+
|
|
17
|
+
- data.go.kr dataset: <https://www.data.go.kr/data/15129462/openapi.do>
|
|
18
|
+
- upstream base: `https://apis.data.go.kr/1230000/ao/OrderPlanSttusService`
|
|
19
|
+
- proxy route: `GET /v1/g2b/order-plans`
|
|
20
|
+
- API key location: proxy server only (`DATA_GO_KR_API_KEY`)
|
|
21
|
+
|
|
22
|
+
Operations used:
|
|
23
|
+
|
|
24
|
+
| kind | endpoint |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `goods` / `물품` | `getOrderPlanSttusListThngPPSSrch` |
|
|
27
|
+
| `construction` / `공사` | `getOrderPlanSttusListCnstwkPPSSrch` |
|
|
28
|
+
| `service` / `용역` | `getOrderPlanSttusListServcPPSSrch` |
|
|
29
|
+
| `foreign` / `외자` | `getOrderPlanSttusListFrgcptPPSSrch` |
|
|
30
|
+
| `all` / `전체` | above four endpoints, merged with `_order_plan_kind` |
|
|
31
|
+
|
|
32
|
+
The API is a free data.go.kr service that requires an application key, so it follows the repository's free-API proxy policy and does not expose the key to users.
|
|
33
|
+
|
|
34
|
+
## When to use
|
|
35
|
+
|
|
36
|
+
- "나라장터 발주목록에서 청소 용역 찾아줘"
|
|
37
|
+
- "조달청 물품 발주계획 2025년 1월 목록 조회"
|
|
38
|
+
- "서울 기관 공사 발주계획 검색"
|
|
39
|
+
- "발주기관명으로 나라장터 발주계획 보여줘"
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
|
|
43
|
+
- 인터넷 연결, `python3`
|
|
44
|
+
- hosted/self-host `k-skill-proxy`의 `/v1/g2b/order-plans` route 접근 가능
|
|
45
|
+
- proxy 운영 서버의 `DATA_GO_KR_API_KEY`가 data.go.kr 15129462에 활용신청되어 있어야 함
|
|
46
|
+
|
|
47
|
+
사용자 측 필수 시크릿은 없다. Self-host proxy를 쓸 때만 `KSKILL_PROXY_BASE_URL`을 설정한다.
|
|
48
|
+
|
|
49
|
+
## Inputs
|
|
50
|
+
|
|
51
|
+
CLI flags:
|
|
52
|
+
|
|
53
|
+
- `--kind`: `goods`/`물품`, `construction`/`공사`, `service`/`용역`, `foreign`/`외자`, `all`/`전체` (default `goods`)
|
|
54
|
+
- `--keyword`, `-q`: 사업명 검색어 (`bizNm`)
|
|
55
|
+
- `--order-from`, `--order-to`: 발주년월 범위 (`YYYY-MM` 또는 `YYYYMM`)
|
|
56
|
+
- `--posted-from`, `--posted-to`: 게시일시 범위 (`YYYY-MM-DD`, `YYYYMMDD`, `YYYYMMDDHHMM`)
|
|
57
|
+
- `--institution`: 발주기관명
|
|
58
|
+
- `--institution-code`: 발주기관코드
|
|
59
|
+
- `--region`: 기관소재지명
|
|
60
|
+
- `--procurement-method`: 조달방식
|
|
61
|
+
- `--product-code`: 세부품명번호(물품)
|
|
62
|
+
- `--business-type`: 업무유형명/업무유형코드(공사·용역·외자)
|
|
63
|
+
- `--construction-type`: 공종구분명(공사)
|
|
64
|
+
- `--page`, `--limit`: 페이지와 한 페이지 결과 수(최대 100)
|
|
65
|
+
|
|
66
|
+
## Workflow
|
|
67
|
+
|
|
68
|
+
### 1. Search order plans
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python3 g2b-order-plan-search/scripts/g2b_order_plan.py \
|
|
72
|
+
--kind service \
|
|
73
|
+
--keyword 청소 \
|
|
74
|
+
--order-from 2025-01 \
|
|
75
|
+
--order-to 2025-03 \
|
|
76
|
+
--posted-from 2025-01-01 \
|
|
77
|
+
--posted-to 2025-01-31 \
|
|
78
|
+
--limit 10
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Narrow by institution or location
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python3 g2b-order-plan-search/scripts/g2b_order_plan.py \
|
|
85
|
+
--kind goods \
|
|
86
|
+
--institution 조달청 \
|
|
87
|
+
--region 대전 \
|
|
88
|
+
--order-from 2025-01 \
|
|
89
|
+
--order-to 2025-01
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Read output conservatively
|
|
93
|
+
|
|
94
|
+
The result is the proxy JSON response. Important fields:
|
|
95
|
+
|
|
96
|
+
- `query`: normalized upstream parameters and selected operation
|
|
97
|
+
- `total_count`, `page`, `page_size`
|
|
98
|
+
- `items[]`: upstream order-plan rows
|
|
99
|
+
- `items[].orderPlanUntyNo`: 발주계획통합번호
|
|
100
|
+
- `items[].bizNm`: 사업명
|
|
101
|
+
- `items[].orderInsttNm`: 발주기관명
|
|
102
|
+
- `items[].orderMnth`: 발주월
|
|
103
|
+
- `items[].sumOrderAmt`, `items[].orderContrctAmt`: 발주금액 fields when upstream provides them
|
|
104
|
+
- `items[].bidNtceNoList`: linked bid notice numbers when upstream provides them
|
|
105
|
+
|
|
106
|
+
When answering, show the official source (data.go.kr dataset and g2b.go.kr manual verification URL) and state that 발주계획은 사전계획이며 실제 사전규격/입찰공고/계약과 1:1로 보장되지 않는다.
|
|
107
|
+
|
|
108
|
+
## Done when
|
|
109
|
+
|
|
110
|
+
- The proxy route `/v1/g2b/order-plans` was queried.
|
|
111
|
+
- The selected `kind` mapped to the correct PPS order-plan endpoint.
|
|
112
|
+
- Date/month filters and keyword/institution filters are reflected in `query`.
|
|
113
|
+
- Results include source fields and manual verification guidance.
|
|
114
|
+
- No login, bidding, certificate, submission, or payment flow was automated.
|
|
115
|
+
|
|
116
|
+
## Failure modes
|
|
117
|
+
|
|
118
|
+
- `503 upstream_not_configured`: proxy server has no `DATA_GO_KR_API_KEY`.
|
|
119
|
+
- `502 upstream_forbidden`: proxy key is not approved for data.go.kr service 15129462.
|
|
120
|
+
- `400 bad_request`: invalid kind, date/month, page, or limit input.
|
|
121
|
+
- `total_count = 0`: no order plans matched the selected conditions.
|
|
122
|
+
- Upstream resultCode `03`: data.go.kr reports no data.
|
|
123
|
+
- API changed endpoint names, parameters, or envelope shape.
|
|
124
|
+
- 발주계획은 실제 입찰공고 등록의 필수 선행조건이 아니며, 발주계획에 없는 사전규격·입찰공고·계약도 있을 수 있다.
|
|
125
|
+
|
|
126
|
+
## Official surfaces
|
|
127
|
+
|
|
128
|
+
- 공공데이터포털: <https://www.data.go.kr/data/15129462/openapi.do>
|
|
129
|
+
- upstream: `https://apis.data.go.kr/1230000/ao/OrderPlanSttusService`
|
|
130
|
+
- 수동 대조: 나라장터 <https://www.g2b.go.kr>
|
|
131
|
+
- proxy route: `GET /v1/g2b/order-plans`
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""나라장터 발주계획현황 조회 via k-skill-proxy.
|
|
2
|
+
|
|
3
|
+
The helper calls k-skill-proxy /v1/g2b/order-plans so DATA_GO_KR_API_KEY stays
|
|
4
|
+
server-side. It is read-only and does not automate g2b.go.kr login flows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import urllib.request
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
PROXY_BASE_URL_ENV_VAR = "KSKILL_PROXY_BASE_URL"
|
|
20
|
+
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
21
|
+
PROXY_DOWN_MSG = "설정된 k-skill-proxy 서버가 응답하지 않습니다. 잠시 후 재시도하거나 운영자에게 문의하세요."
|
|
22
|
+
PROXY_KEY_NOT_CONFIGURED_MSG = "k-skill-proxy에 필요한 API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요."
|
|
23
|
+
ROUTE = "/v1/g2b/order-plans"
|
|
24
|
+
USER_AGENT = "k-skill-g2b-order-plan-search/0.1 (+https://github.com/NomaDamas/k-skill)"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ApiError(RuntimeError):
|
|
28
|
+
def __init__(self, message: str, *, status_code: int | None = None):
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _text_or_none(value: Any) -> str | None:
|
|
34
|
+
if value is None:
|
|
35
|
+
return None
|
|
36
|
+
text = str(value).strip()
|
|
37
|
+
return text or None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_proxy_base_url(explicit: str | None = None, env: Mapping[str, str] | None = None) -> str:
|
|
41
|
+
env = os.environ if env is None else env
|
|
42
|
+
raw = _text_or_none(explicit) or _text_or_none(env.get(PROXY_BASE_URL_ENV_VAR))
|
|
43
|
+
if raw:
|
|
44
|
+
parsed = urllib.parse.urlparse(raw)
|
|
45
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
46
|
+
raise ValueError(f"{PROXY_BASE_URL_ENV_VAR} must be an http(s) URL")
|
|
47
|
+
return raw.rstrip("/")
|
|
48
|
+
return DEFAULT_PROXY_BASE_URL
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normalize_month(value: str | None, label: str) -> str | None:
|
|
52
|
+
raw = _text_or_none(value)
|
|
53
|
+
if raw is None:
|
|
54
|
+
return None
|
|
55
|
+
digits = "".join(ch for ch in raw if ch.isdigit())
|
|
56
|
+
if len(digits) != 6 or not 1 <= int(digits[4:6]) <= 12:
|
|
57
|
+
raise ValueError(f"{label} must be YYYY-MM or YYYYMM")
|
|
58
|
+
return digits
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _normalize_date(value: str | None, label: str) -> str | None:
|
|
62
|
+
raw = _text_or_none(value)
|
|
63
|
+
if raw is None:
|
|
64
|
+
return None
|
|
65
|
+
digits = "".join(ch for ch in raw if ch.isdigit())
|
|
66
|
+
if len(digits) not in {8, 12}:
|
|
67
|
+
raise ValueError(f"{label} must be YYYY-MM-DD, YYYYMMDD, or YYYYMMDDHHMM")
|
|
68
|
+
return digits
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_query(args: argparse.Namespace) -> dict[str, str]:
|
|
72
|
+
query: dict[str, str] = {
|
|
73
|
+
"kind": args.kind,
|
|
74
|
+
"page": str(args.page),
|
|
75
|
+
"limit": str(args.limit),
|
|
76
|
+
}
|
|
77
|
+
optional = {
|
|
78
|
+
"keyword": args.keyword,
|
|
79
|
+
"orderFrom": _normalize_month(args.order_from, "--order-from"),
|
|
80
|
+
"orderTo": _normalize_month(args.order_to, "--order-to"),
|
|
81
|
+
"postedFrom": _normalize_date(args.posted_from, "--posted-from"),
|
|
82
|
+
"postedTo": _normalize_date(args.posted_to, "--posted-to"),
|
|
83
|
+
"institution": args.institution,
|
|
84
|
+
"institutionCode": args.institution_code,
|
|
85
|
+
"region": args.region,
|
|
86
|
+
"procurementMethod": args.procurement_method,
|
|
87
|
+
"productCode": args.product_code,
|
|
88
|
+
"businessType": args.business_type,
|
|
89
|
+
"constructionType": args.construction_type,
|
|
90
|
+
}
|
|
91
|
+
for key, value in optional.items():
|
|
92
|
+
text = _text_or_none(value)
|
|
93
|
+
if text is not None:
|
|
94
|
+
query[key] = text
|
|
95
|
+
return query
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_json_response(request: urllib.request.Request) -> dict[str, Any]:
|
|
99
|
+
try:
|
|
100
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
101
|
+
charset = response.headers.get_content_charset() or "utf-8"
|
|
102
|
+
return json.loads(response.read().decode(charset))
|
|
103
|
+
except urllib.error.HTTPError as error:
|
|
104
|
+
body = error.read().decode("utf-8", errors="replace")
|
|
105
|
+
try:
|
|
106
|
+
parsed = json.loads(body)
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
parsed = {"message": body}
|
|
109
|
+
if error.code == 503 and parsed.get("error") == "upstream_not_configured":
|
|
110
|
+
raise ApiError(PROXY_KEY_NOT_CONFIGURED_MSG, status_code=error.code) from error
|
|
111
|
+
message = parsed.get("message") or parsed.get("error") or body or str(error)
|
|
112
|
+
raise ApiError(f"g2b order-plan proxy returned HTTP {error.code}: {message}", status_code=error.code) from error
|
|
113
|
+
except urllib.error.URLError as error:
|
|
114
|
+
raise ApiError(f"{PROXY_DOWN_MSG} (상세: {error.reason})") from error
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def search_order_plans(query: dict[str, str], *, base_url: str | None = None,
|
|
118
|
+
read_json: Any = read_json_response) -> dict[str, Any]:
|
|
119
|
+
resolved_base = resolve_proxy_base_url(base_url)
|
|
120
|
+
url = f"{resolved_base}{ROUTE}?{urllib.parse.urlencode(query)}"
|
|
121
|
+
request = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": USER_AGENT})
|
|
122
|
+
return read_json(request)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
126
|
+
parser = argparse.ArgumentParser(description="나라장터 발주계획현황 조회 (k-skill-proxy 경유)")
|
|
127
|
+
parser.add_argument("--kind", default="goods", help="goods/물품, construction/공사, service/용역, foreign/외자, all/전체")
|
|
128
|
+
parser.add_argument("--keyword", "-q", help="사업명 검색어(bizNm)")
|
|
129
|
+
parser.add_argument("--order-from", help="발주시작년월 YYYY-MM 또는 YYYYMM")
|
|
130
|
+
parser.add_argument("--order-to", help="발주종료년월 YYYY-MM 또는 YYYYMM")
|
|
131
|
+
parser.add_argument("--posted-from", help="게시 시작일 YYYY-MM-DD 또는 YYYYMMDD[HHMM]")
|
|
132
|
+
parser.add_argument("--posted-to", help="게시 종료일 YYYY-MM-DD 또는 YYYYMMDD[HHMM]")
|
|
133
|
+
parser.add_argument("--institution", help="발주기관명")
|
|
134
|
+
parser.add_argument("--institution-code", help="발주기관코드")
|
|
135
|
+
parser.add_argument("--region", help="기관소재지명")
|
|
136
|
+
parser.add_argument("--procurement-method", help="조달방식")
|
|
137
|
+
parser.add_argument("--product-code", help="세부품명번호(물품)")
|
|
138
|
+
parser.add_argument("--business-type", help="업무유형명/업무유형코드(공사/용역/외자)")
|
|
139
|
+
parser.add_argument("--construction-type", help="공종구분명(공사)")
|
|
140
|
+
parser.add_argument("--page", type=int, default=1)
|
|
141
|
+
parser.add_argument("--limit", type=int, default=10)
|
|
142
|
+
parser.add_argument("--proxy-base-url", help=f"override {PROXY_BASE_URL_ENV_VAR}")
|
|
143
|
+
return parser
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main(argv: list[str] | None = None) -> int:
|
|
147
|
+
args = build_parser().parse_args(argv)
|
|
148
|
+
try:
|
|
149
|
+
result = search_order_plans(build_query(args), base_url=args.proxy_base_url)
|
|
150
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
151
|
+
return 0
|
|
152
|
+
except (ApiError, ValueError) as error:
|
|
153
|
+
print(json.dumps({"error": error.__class__.__name__, "message": str(error)}, ensure_ascii=False), file=sys.stderr)
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "g2b-order-plan-search",
|
|
3
|
+
"description": "조달청 나라장터 발주계획현황서비스로 물품·공사·용역·외자 발주계획 목록을 검색한다. 돌쇠에서는 가능한 가장 가까운 합법적 공식 단계까지 준비한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"hard-boundary"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: g2b-order-plan-search\ndescription: 조달청 나라장터 발주계획현황서비스로 물품·공사·용역·외자 발주계획 목록을 검색한다. 돌쇠에서는 가능한 가장 가까운 합법적 공식 단계까지 준비한다.\nlicense: MIT\nmetadata:\n category: business\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 나라장터 부정당제재업체정보 조회
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
공공데이터포털의 **조달청 나라장터 사용자정보 서비스**(data.go.kr 15129466, `getUnptRsttCorpInfo02`)를 `k-skill-proxy` 경유로 호출해, 사업자등록번호 정확 일치(`inqryDiv=1`)로 **조회시점 현재 유효한** 부정당제재를 조회한다.
|
|
6
|
+
|
|
7
|
+
- 반환: 제재 시작/종료일자, 제재기관명, 계약법구분, 제재근거법률 등 upstream 필드 원문
|
|
8
|
+
|
|
9
|
+
## Coverage boundary
|
|
10
|
+
|
|
11
|
+
upstream 명세상 다음은 **제공되지 않는다** — 과거 이력 조회가 아니다.
|
|
12
|
+
|
|
13
|
+
- 조회시점에 제재만료·해제된 건
|
|
14
|
+
- 나라장터 미등록업체·개인에 대한 제재
|
|
15
|
+
|
|
16
|
+
만료 이력까지 보려면 나라장터(<https://www.g2b.go.kr>)에서 수동 확인이 필요하다.
|
|
17
|
+
|
|
18
|
+
## Design principles
|
|
19
|
+
|
|
20
|
+
- 점수·등급·해석 라벨을 만들지 않는다. upstream 사실 + 출처 + 적용범위 한계만 담는다.
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- "이 회사 입찰 제재(부정당제재) 이력 있어?"
|
|
25
|
+
- "거래/계약 전에 부정당업자 제재 여부 확인해줘"
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
- 인터넷 연결, `python3`
|
|
30
|
+
- `scripts/g2b_sanctioned_supplier.py` helper
|
|
31
|
+
- hosted/self-host `k-skill-proxy`의 `/v1/g2b/sanctioned-supplier` route 접근 가능
|
|
32
|
+
|
|
33
|
+
## Credential requirements
|
|
34
|
+
|
|
35
|
+
- 사용자 측 필수 시크릿 없음.
|
|
36
|
+
- `KSKILL_PROXY_BASE_URL` — self-host 프록시를 쓸 때만 설정. 비우면 hosted `https://k-skill-proxy.nomadamas.org` 사용.
|
|
37
|
+
- `DATA_GO_KR_API_KEY` 는 프록시 운영 서버 환경에만 둔다. 공공데이터포털에서 `조달청_나라장터 사용자정보 서비스`(부정당제재업체정보조회 포함) 활용신청이 되어 있어야 한다.
|
|
38
|
+
|
|
39
|
+
## Inputs
|
|
40
|
+
|
|
41
|
+
- `--bizno`: 사업자등록번호 10자리(하이픈 허용) — 필수
|
|
42
|
+
|
|
43
|
+
## CLI examples
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py --bizno 124-81-00998
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Failure modes
|
|
50
|
+
|
|
51
|
+
- `400 bad_request`: 사업자번호가 10자리가 아님.
|
|
52
|
+
- `503 upstream_not_configured`: 프록시 서버에 `DATA_GO_KR_API_KEY` 없음.
|
|
53
|
+
- `502 upstream_forbidden`: 프록시 키가 15129466에 활용신청되지 않음.
|
|
54
|
+
- `total_count = 0`: 조회시점 현재 유효한 제재 없음 (만료·미등록업체는 미제공임에 유의).
|
|
55
|
+
|
|
56
|
+
## Official surfaces
|
|
57
|
+
|
|
58
|
+
- 공공데이터포털: <https://www.data.go.kr/data/15129466/openapi.do>
|
|
59
|
+
- upstream: `https://apis.data.go.kr/1230000/ao/UsrInfoService02/getUnptRsttCorpInfo02`
|
|
60
|
+
- 수동 대조: 나라장터 <https://www.g2b.go.kr>
|
|
61
|
+
- 프록시 route: `GET /v1/g2b/sanctioned-supplier`
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Procurement (나라장터) sanctioned-supplier lookup via k-skill-proxy.
|
|
2
|
+
|
|
3
|
+
The proxy holds DATA_GO_KR_API_KEY server-side; this helper only builds the
|
|
4
|
+
query and reads the structured response. No user secret is required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import urllib.request
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
PROXY_BASE_URL_ENV_VAR = "KSKILL_PROXY_BASE_URL"
|
|
20
|
+
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
21
|
+
PROXY_DOWN_MSG = "설정된 k-skill-proxy 서버가 응답하지 않습니다. 잠시 후 재시도하거나 운영자에게 문의하세요."
|
|
22
|
+
PROXY_KEY_NOT_CONFIGURED_MSG = "k-skill-proxy에 필요한 API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요."
|
|
23
|
+
ROUTE = "/v1/g2b/sanctioned-supplier"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ApiError(RuntimeError):
|
|
27
|
+
def __init__(self, message: str, *, status_code: int | None = None):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _text_or_none(value: Any) -> str | None:
|
|
33
|
+
if value is None:
|
|
34
|
+
return None
|
|
35
|
+
text = str(value).strip()
|
|
36
|
+
return text or None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_proxy_base_url(explicit: str | None = None, env: dict[str, str] | None = None) -> str:
|
|
40
|
+
env = os.environ if env is None else env
|
|
41
|
+
candidate = _text_or_none(explicit or env.get(PROXY_BASE_URL_ENV_VAR))
|
|
42
|
+
if candidate and candidate.casefold() in {"off", "false", "0", "disable", "disabled", "none"}:
|
|
43
|
+
raise ValueError("KSKILL_PROXY_BASE_URL 가 비활성화되어 있습니다.")
|
|
44
|
+
if candidate and candidate != "replace-me":
|
|
45
|
+
return candidate.rstrip("/")
|
|
46
|
+
return DEFAULT_PROXY_BASE_URL
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def normalize_bizno(value: Any) -> str:
|
|
50
|
+
raw = _text_or_none(value)
|
|
51
|
+
if not raw:
|
|
52
|
+
raise ValueError("사업자등록번호(bizno)를 입력하세요.")
|
|
53
|
+
normalized = re.sub(r"\D", "", raw)
|
|
54
|
+
if not re.fullmatch(r"\d{10}", normalized):
|
|
55
|
+
raise ValueError("사업자등록번호는 숫자 10자리여야 합니다.")
|
|
56
|
+
return normalized
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_json_response(request: urllib.request.Request) -> dict[str, Any]:
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
62
|
+
try:
|
|
63
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
64
|
+
except json.JSONDecodeError as error:
|
|
65
|
+
raise ApiError("g2b sanction proxy returned invalid JSON.") from error
|
|
66
|
+
if not isinstance(payload, dict):
|
|
67
|
+
raise ApiError("g2b sanction proxy returned a non-object JSON payload.")
|
|
68
|
+
return payload
|
|
69
|
+
except urllib.error.HTTPError as error:
|
|
70
|
+
body = error.read().decode("utf-8", errors="replace")
|
|
71
|
+
try:
|
|
72
|
+
payload = json.loads(body)
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
payload = None
|
|
75
|
+
if error.code == 503 and isinstance(payload, dict) and payload.get("error") == "upstream_not_configured":
|
|
76
|
+
raise ApiError(PROXY_KEY_NOT_CONFIGURED_MSG, status_code=error.code) from error
|
|
77
|
+
if isinstance(payload, dict) and payload.get("message"):
|
|
78
|
+
raise ApiError(str(payload["message"]), status_code=error.code) from error
|
|
79
|
+
raise ApiError(f"g2b sanction proxy request failed with HTTP {error.code}", status_code=error.code) from error
|
|
80
|
+
except urllib.error.URLError as error:
|
|
81
|
+
raise ApiError(f"{PROXY_DOWN_MSG} (상세: {error.reason})") from error
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def query_sanctions(bizno: str, *, base_url: str | None = None,
|
|
85
|
+
read_json: Any = read_json_response) -> dict[str, Any]:
|
|
86
|
+
normalized = normalize_bizno(bizno)
|
|
87
|
+
url = f"{resolve_proxy_base_url(base_url)}{ROUTE}?{urllib.parse.urlencode({'bizno': normalized})}"
|
|
88
|
+
request = urllib.request.Request(url, headers={
|
|
89
|
+
"Accept": "application/json",
|
|
90
|
+
"User-Agent": "k-skill-g2b-sanctioned-supplier/1.0",
|
|
91
|
+
}, method="GET")
|
|
92
|
+
return read_json(request)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
96
|
+
parser = argparse.ArgumentParser(description="나라장터 부정당제재업체정보 조회 (k-skill-proxy 경유)")
|
|
97
|
+
parser.add_argument("--bizno", required=True, help="사업자등록번호 10자리(하이픈 허용)")
|
|
98
|
+
parser.add_argument("--proxy-base-url")
|
|
99
|
+
return parser
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: list[str] | None = None) -> int:
|
|
103
|
+
args = build_parser().parse_args(argv)
|
|
104
|
+
try:
|
|
105
|
+
result = query_sanctions(args.bizno, base_url=args.proxy_base_url)
|
|
106
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
107
|
+
return 0
|
|
108
|
+
except (ValueError, ApiError) as error:
|
|
109
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "g2b-sanctioned-supplier",
|
|
3
|
+
"description": "조달청 나라장터 부정당제재업체정보를 공공데이터포털 API(k-skill-proxy 경유)로 조회한다. 사업자등록번호 정확 일치로 조회시점 현재 유효한 입찰참가자격 제한(부정당제재)의 기간·제재기관·근거법률을 확인한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"lookup"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: g2b-sanctioned-supplier\ndescription: 조달청 나라장터 부정당제재업체정보를 공공데이터포털 API(k-skill-proxy 경유)로 조회한다. 사업자등록번호 정확 일치로 조회시점 현재 유효한 입찰참가자격 제한(부정당제재)의 기간·제재기관·근거법률을 확인한다.\nlicense: MIT\nmetadata:\n category: business\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|