@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,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
PROXY_BASE_URL_ENV_VAR = "KSKILL_PROXY_BASE_URL"
|
|
14
|
+
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
15
|
+
PROXY_DOWN_MSG = "설정된 k-skill-proxy 서버가 응답하지 않습니다. 잠시 후 재시도하거나 운영자에게 문의하세요."
|
|
16
|
+
PROXY_KEY_NOT_CONFIGURED_MSG = "k-skill-proxy에 필요한 API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요."
|
|
17
|
+
BATCH_LIMIT = 100
|
|
18
|
+
VALIDATE_TEXT_FIELD_LIMITS = {
|
|
19
|
+
"p_nm": 30,
|
|
20
|
+
"p_nm2": 30,
|
|
21
|
+
"b_nm": 200,
|
|
22
|
+
"b_sector": 100,
|
|
23
|
+
"b_type": 100,
|
|
24
|
+
"b_adr": 500,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ApiError(RuntimeError):
|
|
29
|
+
def __init__(self, message: str, *, status_code: int | None = None, url: str | None = None):
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
self.url = url
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _text_or_none(value: Any) -> str | None:
|
|
36
|
+
if value is None:
|
|
37
|
+
return None
|
|
38
|
+
text = str(value).strip()
|
|
39
|
+
return text or None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_proxy_base_url(explicit_base_url: str | None = None, env: dict[str, str] | None = None) -> str:
|
|
43
|
+
env = os.environ if env is None else env
|
|
44
|
+
candidate = _text_or_none(explicit_base_url or env.get(PROXY_BASE_URL_ENV_VAR))
|
|
45
|
+
if candidate and candidate.casefold() in {"off", "false", "0", "disable", "disabled", "none"}:
|
|
46
|
+
raise ValueError("KSKILL_PROXY_BASE_URL 가 비활성화되어 있습니다.")
|
|
47
|
+
if candidate and candidate != "replace-me":
|
|
48
|
+
return candidate.rstrip("/")
|
|
49
|
+
return DEFAULT_PROXY_BASE_URL
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_business_number(value: Any) -> str:
|
|
53
|
+
raw = _text_or_none(value)
|
|
54
|
+
if not raw:
|
|
55
|
+
raise ValueError("사업자등록번호(b_no)를 입력하세요.")
|
|
56
|
+
normalized = re.sub(r"\D", "", raw)
|
|
57
|
+
if not re.fullmatch(r"\d{10}", normalized):
|
|
58
|
+
raise ValueError("사업자등록번호는 숫자 10자리여야 합니다.")
|
|
59
|
+
return normalized
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize_start_date(value: Any) -> str:
|
|
63
|
+
raw = _text_or_none(value)
|
|
64
|
+
if not raw:
|
|
65
|
+
raise ValueError("개업일자(start_dt)를 YYYYMMDD 형식으로 입력하세요.")
|
|
66
|
+
normalized = re.sub(r"\D", "", raw)
|
|
67
|
+
if not re.fullmatch(r"\d{8}", normalized):
|
|
68
|
+
raise ValueError("개업일자는 YYYYMMDD 형식이어야 합니다.")
|
|
69
|
+
try:
|
|
70
|
+
dt.date(int(normalized[:4]), int(normalized[4:6]), int(normalized[6:8]))
|
|
71
|
+
except ValueError as error:
|
|
72
|
+
raise ValueError("개업일자는 유효한 날짜여야 합니다.") from error
|
|
73
|
+
return normalized
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def normalize_validate_text(value: Any, field_name: str, *, required: bool = False) -> str | None:
|
|
77
|
+
text = _text_or_none(value)
|
|
78
|
+
if not text:
|
|
79
|
+
if required:
|
|
80
|
+
raise ValueError(f"{field_name}을(를) 입력하세요.")
|
|
81
|
+
return None
|
|
82
|
+
max_length = VALIDATE_TEXT_FIELD_LIMITS.get(field_name)
|
|
83
|
+
if max_length and len(text) > max_length:
|
|
84
|
+
raise ValueError(f"{field_name}은(는) {max_length}자 이하여야 합니다.")
|
|
85
|
+
return text
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def normalize_corp_no(value: Any) -> str | None:
|
|
89
|
+
raw = _text_or_none(value)
|
|
90
|
+
if not raw:
|
|
91
|
+
return None
|
|
92
|
+
normalized = re.sub(r"\D", "", raw)
|
|
93
|
+
if not re.fullmatch(r"\d{13}", normalized):
|
|
94
|
+
raise ValueError("corp_no는 숫자 13자리여야 합니다.")
|
|
95
|
+
return normalized
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_status_payload(business_numbers: list[Any]) -> dict[str, list[str]]:
|
|
99
|
+
numbers = [normalize_business_number(value) for value in business_numbers]
|
|
100
|
+
numbers = list(dict.fromkeys(numbers))
|
|
101
|
+
if not numbers:
|
|
102
|
+
raise ValueError("사업자등록번호를 1개 이상 입력하세요.")
|
|
103
|
+
if len(numbers) > BATCH_LIMIT:
|
|
104
|
+
raise ValueError("한 번에 조회할 수 있는 사업자등록번호는 100개까지입니다.")
|
|
105
|
+
return {"b_no": numbers}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_validate_business(**kwargs: Any) -> dict[str, str]:
|
|
109
|
+
p_nm = normalize_validate_text(kwargs.get("p_nm"), "p_nm", required=True)
|
|
110
|
+
|
|
111
|
+
business = {
|
|
112
|
+
"b_no": normalize_business_number(kwargs.get("b_no")),
|
|
113
|
+
"start_dt": normalize_start_date(kwargs.get("start_dt")),
|
|
114
|
+
"p_nm": p_nm,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for key in ("p_nm2", "b_nm", "b_sector", "b_type", "b_adr"):
|
|
118
|
+
value = normalize_validate_text(kwargs.get(key), key)
|
|
119
|
+
if value:
|
|
120
|
+
business[key] = value
|
|
121
|
+
|
|
122
|
+
corp_no = normalize_corp_no(kwargs.get("corp_no"))
|
|
123
|
+
if corp_no:
|
|
124
|
+
business["corp_no"] = corp_no
|
|
125
|
+
return business
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_validate_payload(businesses: list[dict[str, Any]]) -> dict[str, list[dict[str, str]]]:
|
|
129
|
+
if not businesses:
|
|
130
|
+
raise ValueError("진위확인 대상 businesses를 1개 이상 입력하세요.")
|
|
131
|
+
if len(businesses) > BATCH_LIMIT:
|
|
132
|
+
raise ValueError("한 번에 진위확인할 수 있는 사업자는 100개까지입니다.")
|
|
133
|
+
return {"businesses": [build_validate_business(**business) for business in businesses]}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def read_json_response(request: urllib.request.Request) -> dict[str, Any]:
|
|
137
|
+
try:
|
|
138
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
139
|
+
return json.loads(response.read().decode("utf-8"))
|
|
140
|
+
except urllib.error.HTTPError as error:
|
|
141
|
+
body = error.read().decode("utf-8", errors="replace")
|
|
142
|
+
try:
|
|
143
|
+
payload = json.loads(body)
|
|
144
|
+
except json.JSONDecodeError:
|
|
145
|
+
payload = None
|
|
146
|
+
if error.code == 503 and isinstance(payload, dict) and payload.get("error") == "upstream_not_configured":
|
|
147
|
+
raise ApiError(PROXY_KEY_NOT_CONFIGURED_MSG, status_code=error.code, url=getattr(error, "url", None)) from error
|
|
148
|
+
if isinstance(payload, dict) and payload.get("message"):
|
|
149
|
+
raise ApiError(str(payload["message"]), status_code=error.code, url=getattr(error, "url", None)) from error
|
|
150
|
+
raise ApiError(f"NTS business proxy request failed with HTTP {error.code}", status_code=error.code, url=getattr(error, "url", None)) from error
|
|
151
|
+
except urllib.error.URLError as error:
|
|
152
|
+
raise ApiError(f"{PROXY_DOWN_MSG} (상세: {error.reason})") from error
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _post_json(path: str, payload: dict[str, Any], *, base_url: str | None = None, read_json: Any = read_json_response) -> dict[str, Any]:
|
|
156
|
+
resolved_base_url = resolve_proxy_base_url(base_url)
|
|
157
|
+
request = urllib.request.Request(
|
|
158
|
+
f"{resolved_base_url}{path}",
|
|
159
|
+
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
|
160
|
+
headers={
|
|
161
|
+
"Accept": "application/json",
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
"User-Agent": "k-skill-nts-business-registration/1.0",
|
|
164
|
+
},
|
|
165
|
+
method="POST",
|
|
166
|
+
)
|
|
167
|
+
return read_json(request)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def query_status(business_numbers: list[Any], *, base_url: str | None = None, read_json: Any = read_json_response) -> dict[str, Any]:
|
|
171
|
+
return _post_json("/v1/nts-business/status", build_status_payload(business_numbers), base_url=base_url, read_json=read_json)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def validate_businesses(businesses: list[dict[str, Any]], *, base_url: str | None = None, read_json: Any = read_json_response) -> dict[str, Any]:
|
|
175
|
+
return _post_json("/v1/nts-business/validate", build_validate_payload(businesses), base_url=base_url, read_json=read_json)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_business_json(value: str) -> dict[str, Any]:
|
|
179
|
+
payload = json.loads(value)
|
|
180
|
+
if not isinstance(payload, dict):
|
|
181
|
+
raise argparse.ArgumentTypeError("business JSON must be an object")
|
|
182
|
+
return payload
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
186
|
+
parser = argparse.ArgumentParser(description="NTS business registration status/authenticity helper")
|
|
187
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
188
|
+
|
|
189
|
+
status = subparsers.add_parser("status", help="사업자등록번호 상태조회")
|
|
190
|
+
status.add_argument("--b-no", action="append", required=True, help="사업자등록번호(10자리; 하이픈 허용). 여러 번 지정 가능")
|
|
191
|
+
status.add_argument("--proxy-base-url")
|
|
192
|
+
|
|
193
|
+
validate = subparsers.add_parser("validate", help="사업자등록정보 진위확인")
|
|
194
|
+
validate.add_argument("--business-json", action="append", type=_parse_business_json, required=True, help='예: {"b_no":"1234567890","start_dt":"20200101","p_nm":"홍길동"}')
|
|
195
|
+
validate.add_argument("--proxy-base-url")
|
|
196
|
+
return parser
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main(argv: list[str] | None = None) -> int:
|
|
200
|
+
args = build_parser().parse_args(argv)
|
|
201
|
+
try:
|
|
202
|
+
if args.command == "status":
|
|
203
|
+
print(json.dumps(query_status(args.b_no, base_url=args.proxy_base_url), ensure_ascii=False, indent=2))
|
|
204
|
+
return 0
|
|
205
|
+
if args.command == "validate":
|
|
206
|
+
print(json.dumps(validate_businesses(args.business_json, base_url=args.proxy_base_url), ensure_ascii=False, indent=2))
|
|
207
|
+
return 0
|
|
208
|
+
except (ValueError, ApiError) as error:
|
|
209
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
return 1
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nts-business-registration",
|
|
3
|
+
"description": "국세청 사업자등록정보 진위확인 및 사업자등록 상태조회를 공공데이터포털 API(k-skill-proxy 경유)로 수행한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"vault",
|
|
7
|
+
"action:submission"
|
|
8
|
+
],
|
|
9
|
+
"frontmatter": "name: nts-business-registration\ndescription: 국세청 사업자등록정보 진위확인 및 사업자등록 상태조회를 공공데이터포털 API(k-skill-proxy 경유)로 수행한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: business\n locale: ko-KR\n phase: v1"
|
|
10
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# 국세청 고액·상습체납자 명단공개 검색
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
국세청 누리집의 **고액·상습체납자 명단공개**(국세기본법 제85조의5)를 무인증 공개 검색으로 직접 조회한다.
|
|
6
|
+
|
|
7
|
+
- 법인 명단: 법인명으로 검색 — 공개년도·법인명·대표자·업종·소재지·총 체납액·세목·체납건수·체납요지
|
|
8
|
+
- 개인 명단: 상호로 검색 — 공개년도·성명·연령·상호·직업·주소·총 체납액·세목·체납요지
|
|
9
|
+
|
|
10
|
+
이 명단에는 **사업자등록번호가 수록되지 않는다.** 상호·법인명 문자열 일치 후보의 공개 사실만 나열하며, 동명 상호 가능성은 사용자가 판단한다.
|
|
11
|
+
|
|
12
|
+
## Design principles
|
|
13
|
+
|
|
14
|
+
- 점수·등급·해석 라벨을 만들지 않는다. 공개된 사실 + 출처만 담는다.
|
|
15
|
+
- 인증 없이 동작하는 공개 read-only 검색이므로 프록시를 거치지 않고 사용자 머신에서 직접 호출한다.
|
|
16
|
+
- HTML 스크래핑이므로 페이지 마커가 어긋나면 즉시 `unavailable`로 강등하고 수동 확인 경로를 안내한다.
|
|
17
|
+
|
|
18
|
+
## When to use
|
|
19
|
+
|
|
20
|
+
- "이 회사(거래처/의뢰인) 국세 체납 명단공개에 올라 있어?"
|
|
21
|
+
- "상호로 고액·상습체납자 명단 대조해줘"
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- 인터넷 연결, `python3` (stdlib만 사용 — 추가 의존성 없음)
|
|
26
|
+
- `scripts/nts_tax_delinquency.py` helper
|
|
27
|
+
|
|
28
|
+
## Credential requirements
|
|
29
|
+
|
|
30
|
+
- 없음. 무인증 공개 검색이다.
|
|
31
|
+
|
|
32
|
+
## Inputs
|
|
33
|
+
|
|
34
|
+
- `--name`: 상호·법인명 — 필수 (명단에 사업자등록번호가 없어 번호로는 검색 불가)
|
|
35
|
+
|
|
36
|
+
## Privacy boundary
|
|
37
|
+
|
|
38
|
+
- 입력한 상호·법인명은 국세청 누리집으로 전송된다.
|
|
39
|
+
- 명단공개 자료에 사업자등록번호가 없어 상호·법인명 문자열 일치의 공개 사실만 나열한다.
|
|
40
|
+
|
|
41
|
+
## CLI examples
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python3 nts-tax-delinquency/scripts/nts_tax_delinquency.py --name "○○건설"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Failure modes
|
|
48
|
+
|
|
49
|
+
- `unavailable` + 안내: 상호 미입력, 네트워크 오류, 페이지 구조 변경 추정 — 수동 확인 URL 제공.
|
|
50
|
+
- 0건: 두 명단 모두 매치 없음 (`match_count: 0`).
|
|
51
|
+
|
|
52
|
+
## Official surfaces
|
|
53
|
+
|
|
54
|
+
- 명단공개 검색: `https://www.nts.go.kr/nts/ad/openInfo/selectList.do`
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""NTS high-amount/habitual tax-delinquent disclosure search (unauthenticated).
|
|
2
|
+
|
|
3
|
+
국세청 고액·상습체납자 명단공개를 nts.go.kr 공개 검색으로 직접 조회한다.
|
|
4
|
+
인증키가 필요 없는 공개 read-only endpoint이므로 프록시를 거치지 않는다.
|
|
5
|
+
|
|
6
|
+
The disclosure list does NOT contain business registration numbers, so this is a
|
|
7
|
+
trade-name / corporate-name string match only — it cannot assert that a hit is
|
|
8
|
+
the same entity as a given business number.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import datetime as dt
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.parse
|
|
20
|
+
import urllib.request
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
URL = "https://www.nts.go.kr/nts/ad/openInfo/selectList.do"
|
|
24
|
+
SOURCE = ("국세청 고액·상습체납자 명단공개 검색 — nts.go.kr 누리집 공개 검색 "
|
|
25
|
+
"(무인증, www.nts.go.kr/nts/ad/openInfo/selectList.do)")
|
|
26
|
+
MANUAL_NOTE = f"수동 확인: 브라우저에서 {URL} 접속 후 명단공개 검색"
|
|
27
|
+
USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
28
|
+
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36")
|
|
29
|
+
KST = dt.timezone(dt.timedelta(hours=9))
|
|
30
|
+
|
|
31
|
+
CORP_COLUMNS = ("no", "공개년도", "법인명", "대표자", "업종", "법인소재지",
|
|
32
|
+
"대표자주소", "총체납액", "세목", "납기", "체납건수", "체납요지")
|
|
33
|
+
INDIV_COLUMNS = ("no", "공개년도", "성명", "연령", "상호", "직업(업종)", "체납자주소",
|
|
34
|
+
"총체납액", "세목", "납기", "체납건수", "체납요지")
|
|
35
|
+
|
|
36
|
+
IDENTITY_NOTE = ("명단공개 자료에는 사업자등록번호가 수록되지 않아 입력 사업자번호와의 "
|
|
37
|
+
"동일성은 확인할 수 없다 — 상호·법인명 문자열 일치 후보의 공개 사실만 "
|
|
38
|
+
"나열하며, 동명 상호일 가능성은 사용자가 판단한다.")
|
|
39
|
+
|
|
40
|
+
_HEADING_MARKER = "고액상습체납자"
|
|
41
|
+
_ZERO_MARKER = "조회된 데이터가 없습니다"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StructureChanged(RuntimeError):
|
|
45
|
+
"""페이지 구조가 기대 마커와 다름 — 우아한 강등 트리거."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _now_iso() -> str:
|
|
49
|
+
return dt.datetime.now(KST).isoformat(timespec="seconds")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _envelope(status: str, *, result: dict | None = None, note: str | None = None) -> dict:
|
|
53
|
+
return {
|
|
54
|
+
"source": SOURCE,
|
|
55
|
+
"looked_up_at": _now_iso(),
|
|
56
|
+
"status": status,
|
|
57
|
+
"result": result,
|
|
58
|
+
"origin": "unauthenticated-public",
|
|
59
|
+
"note": note,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _strip_tags(fragment: str) -> str:
|
|
64
|
+
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", fragment)).strip()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_rows(html: str, columns: tuple) -> list[dict]:
|
|
68
|
+
if _HEADING_MARKER not in html.replace(" ", ""):
|
|
69
|
+
raise StructureChanged("명단공개 페이지 마커(고액상습체납자) 미발견")
|
|
70
|
+
if _ZERO_MARKER in html:
|
|
71
|
+
return []
|
|
72
|
+
cells = [_strip_tags(td) for td in re.findall(r"<td[^>]*>(.*?)</td>", html, re.S)]
|
|
73
|
+
if not cells or len(cells) % len(columns) != 0:
|
|
74
|
+
raise StructureChanged(f"표 셀 수({len(cells)})가 컬럼 수({len(columns)})의 배수가 아님")
|
|
75
|
+
return [dict(zip(columns, cells[i:i + len(columns)]))
|
|
76
|
+
for i in range(0, len(cells), len(columns))]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _post(data: dict[str, str], *, opener: Any = None) -> str:
|
|
80
|
+
request = urllib.request.Request(
|
|
81
|
+
URL,
|
|
82
|
+
data=urllib.parse.urlencode(data).encode("utf-8"),
|
|
83
|
+
headers={
|
|
84
|
+
"User-Agent": USER_AGENT,
|
|
85
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
86
|
+
},
|
|
87
|
+
method="POST",
|
|
88
|
+
)
|
|
89
|
+
open_fn = opener or urllib.request.urlopen
|
|
90
|
+
with open_fn(request, timeout=20) as response:
|
|
91
|
+
status = getattr(response, "status", 200)
|
|
92
|
+
if status != 200:
|
|
93
|
+
raise StructureChanged(f"HTTP {status}")
|
|
94
|
+
return response.read().decode("utf-8", errors="replace")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _search(tcd: str, search_type: str, value: str, columns: tuple, *, opener: Any = None) -> list[dict]:
|
|
98
|
+
html = _post({
|
|
99
|
+
"tcd": tcd,
|
|
100
|
+
"searchType": search_type,
|
|
101
|
+
"searchValue": value,
|
|
102
|
+
"searchYear": "",
|
|
103
|
+
"currPage": "1",
|
|
104
|
+
"pageIndex": "100",
|
|
105
|
+
"search_order": "1",
|
|
106
|
+
}, opener=opener)
|
|
107
|
+
return parse_rows(html, columns)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def lookup(name: str, *, opener: Any = None) -> dict:
|
|
111
|
+
"""고액·상습체납자 명단공개 대조 — 법인 명단(법인명)·개인 명단(상호) 각 1회."""
|
|
112
|
+
if not (name or "").strip():
|
|
113
|
+
return _envelope("unavailable",
|
|
114
|
+
note=("명단공개 자료에 사업자등록번호가 수록되지 않아 상호·법인명 없이 "
|
|
115
|
+
f"검색할 수 없습니다. --name 으로 상호를 지정하세요. {MANUAL_NOTE}"))
|
|
116
|
+
name = name.strip()
|
|
117
|
+
try:
|
|
118
|
+
corp_rows = _search("1", "1", name, CORP_COLUMNS, opener=opener)
|
|
119
|
+
indiv_rows = _search("2", "3", name, INDIV_COLUMNS, opener=opener)
|
|
120
|
+
except urllib.error.URLError as err:
|
|
121
|
+
return _envelope("unavailable", note=f"네트워크 오류: {err.reason}. {MANUAL_NOTE}")
|
|
122
|
+
except StructureChanged as err:
|
|
123
|
+
return _envelope("unavailable", note=f"페이지 구조 변경 추정({err}). {MANUAL_NOTE}")
|
|
124
|
+
except Exception as err: # 경계 계약: 어떤 오류든 강등, 크래시 금지
|
|
125
|
+
return _envelope("unavailable", note=f"예상 외 오류({type(err).__name__}). {MANUAL_NOTE}")
|
|
126
|
+
|
|
127
|
+
result = {
|
|
128
|
+
"query_name": name,
|
|
129
|
+
"list_basis": "국세청 고액·상습체납자 명단공개 (국세기본법 제85조의5)",
|
|
130
|
+
"corporate_list": {"searched_by": "법인명", "match_count": len(corp_rows), "matches": corp_rows},
|
|
131
|
+
"individual_list": {"searched_by": "상호", "match_count": len(indiv_rows), "matches": indiv_rows},
|
|
132
|
+
"identity_note": IDENTITY_NOTE,
|
|
133
|
+
}
|
|
134
|
+
return _envelope("ok", result=result)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
138
|
+
parser = argparse.ArgumentParser(description="국세청 고액·상습체납자 명단공개 검색 (무인증)")
|
|
139
|
+
parser.add_argument("--name", required=True, help="상호·법인명 — 필수 (명단에 사업자번호 없음)")
|
|
140
|
+
return parser
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main(argv: list[str] | None = None) -> int:
|
|
144
|
+
args = build_parser().parse_args(argv)
|
|
145
|
+
print(json.dumps(lookup(args.name), ensure_ascii=False, indent=2))
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nts-tax-delinquency",
|
|
3
|
+
"description": "국세청 고액·상습체납자 명단공개를 nts.go.kr 공개 검색으로 조회한다. 상호·법인명으로 법인 명단과 개인 명단을 대조해 공개된 체납 사실(총 체납액·세목·체납요지 등)을 나열한다. 인증키 불필요. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"action:submission"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: nts-tax-delinquency\ndescription: 국세청 고액·상습체납자 명단공개를 nts.go.kr 공개 검색으로 조회한다. 상호·법인명으로 법인 명단과 개인 명단을 대조해 공개된 체납 사실(총 체납액·세목·체납요지 등)을 나열한다. 인증키 불필요. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: business\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# 오늘의집 오늘의딜 조회
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
오늘의집 공개 오늘의딜 페이지(`https://ohou.se/commerces/today_deals`)의 서버 렌더링 초기 데이터(`__NEXT_DATA__`)를 읽어 특가 상품을 조회한다.
|
|
6
|
+
|
|
7
|
+
- 오늘의딜/스페셜딜 상품 목록 조회
|
|
8
|
+
- 할인율, 원가, 판매가, 쿠폰/결제혜택 반영 최저가 정리
|
|
9
|
+
- 브랜드, 리뷰 수, 평점, 무료배송 여부, 상품 링크 확인
|
|
10
|
+
- 키워드, 최소 할인율, 무료배송 필터
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
- "오늘의집 오늘의딜 뭐 있어?"
|
|
15
|
+
- "오늘의집에서 할인율 높은 특가 상품 3개 보여줘"
|
|
16
|
+
- "오늘의집 무료배송 특가만 골라줘"
|
|
17
|
+
- "오늘의집에서 러그 특가 찾아줘"
|
|
18
|
+
|
|
19
|
+
## When not to use
|
|
20
|
+
|
|
21
|
+
- 로그인, 장바구니, 구매, 결제 자동화 — 이 스킬은 의도적으로 구매 플로우를 포함하지 않는다.
|
|
22
|
+
- 개인화 추천, 사용자별 쿠폰 적용 확정, 실시간 재고 보장.
|
|
23
|
+
- 법적 증빙 수준의 가격 확정 — 조회 시점 기준 참고용이다.
|
|
24
|
+
- 차단 우회, CAPTCHA 우회 — 표준 라이브러리 `urllib` 한 호출로 안 되면 실패 모드로 처리한다.
|
|
25
|
+
|
|
26
|
+
## Required inputs
|
|
27
|
+
|
|
28
|
+
별도 입력 없이 실행 가능. 선택적으로 아래를 지정할 수 있다:
|
|
29
|
+
|
|
30
|
+
- `--query`: 상품명/브랜드 키워드
|
|
31
|
+
- `--min-discount`: 최소 할인율 (0~100 정수)
|
|
32
|
+
- `--free-delivery`: 무료배송 상품만
|
|
33
|
+
- `--sort`: 정렬 기준 (`discount`, `price`, `review`, `annual-sales`)
|
|
34
|
+
- `--limit`: 결과 개수 (양의 정수, 기본 10)
|
|
35
|
+
- `--html-file`: 오프라인 HTML/JSON fixture 경로
|
|
36
|
+
|
|
37
|
+
## Official/public surface
|
|
38
|
+
|
|
39
|
+
- 오늘의집 오늘의딜 페이지: `https://ohou.se/commerces/today_deals`
|
|
40
|
+
- 현재 웹 페이지는 canonical/OG URL로 `https://store.ohou.se/today_deals`를 노출하지만, 브라우저 접근용 공개 URL은 `ohou.se/commerces/today_deals`다.
|
|
41
|
+
- 응답 HTML의 Next.js `__NEXT_DATA__` 안 React Query `dehydratedState`에서 `today-deal-feed`, `special-today-deal-feed` queryKey 두 곳의 `todayDealFeed.slots`만 명시적으로 읽는다. 다른 페이지 모듈(navigation, banner 등)에 `type: DEAL` 노드가 있어도 무시한다.
|
|
42
|
+
- HTTP 요청은 `User-Agent: k-skill-ohou-today-deal/1.0 (+https://github.com/NomaDamas/k-skill)`로 보낸다. ohou.se 앞단 Akamai bot manager는 익명/단축 UA를 차단하지만 봇 이름 + contact URL이 포함된 well-formed UA는 통과시키므로 우회/조작 없이 정직한 자기소개로 요청한다.
|
|
43
|
+
|
|
44
|
+
## Prerequisites
|
|
45
|
+
|
|
46
|
+
- `python3`
|
|
47
|
+
- 별도 로그인/API 키 없음
|
|
48
|
+
|
|
49
|
+
## Workflow
|
|
50
|
+
|
|
51
|
+
### 1. 오늘의딜 상품 조회
|
|
52
|
+
|
|
53
|
+
오늘의집 오늘의딜 공개 페이지에서 상품 목록을 가져온다. 기본 정렬은 할인율 높은 순이다.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 ohou-today-deal/scripts/ohou_today_deal.py list --limit 10
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
응답 예시:
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"source": {
|
|
63
|
+
"name": "ohou-today-deal",
|
|
64
|
+
"url": "https://ohou.se/commerces/today_deals",
|
|
65
|
+
"fetched_at": "2026-05-18T01:44:16+00:00",
|
|
66
|
+
"surface": "__NEXT_DATA__ today-deal-feed + special-today-deal-feed"
|
|
67
|
+
},
|
|
68
|
+
"filters": {"query": null, "min_discount": null, "free_delivery": false, "sort": "discount", "limit": 10},
|
|
69
|
+
"count": 10,
|
|
70
|
+
"total_count": 72,
|
|
71
|
+
"filtered_count": 72,
|
|
72
|
+
"items": [
|
|
73
|
+
{
|
|
74
|
+
"id": "823405",
|
|
75
|
+
"title": "삼익가구 BEST상품 총집합",
|
|
76
|
+
"brand": "삼익가구",
|
|
77
|
+
"url": "https://ohou.se/productions/823405/selling",
|
|
78
|
+
"original_price": 449000,
|
|
79
|
+
"selling_price": 132000,
|
|
80
|
+
"discount_rate": 70,
|
|
81
|
+
"best_price": 118800,
|
|
82
|
+
"best_discount_rate": 73,
|
|
83
|
+
"best_discount_description": "쿠폰 할인가",
|
|
84
|
+
"review_count": 53818,
|
|
85
|
+
"review_average": 4.7,
|
|
86
|
+
"free_delivery": false,
|
|
87
|
+
"sold_out": false
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. 할인율 높은 순 정렬
|
|
94
|
+
|
|
95
|
+
`bestDiscountPrice.discountRate`(쿠폰/결제혜택 반영 할인율)가 있으면 우선 사용하고, 없으면 상품 기본 `discountRate`를 사용한다.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 ohou-today-deal/scripts/ohou_today_deal.py list \
|
|
99
|
+
--sort discount \
|
|
100
|
+
--limit 5
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
정렬 옵션: `discount`(할인율), `price`(낮은 가격), `review`(리뷰 많은 순), `annual-sales`(연간 판매량).
|
|
104
|
+
|
|
105
|
+
### 3. 키워드·할인율·무료배송 필터
|
|
106
|
+
|
|
107
|
+
상품명 또는 브랜드에 키워드가 포함된 상품만 걸러내고, 최소 할인율과 무료배송 조건을 조합할 수 있다.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python3 ohou-today-deal/scripts/ohou_today_deal.py list \
|
|
111
|
+
--query 러그 \
|
|
112
|
+
--min-discount 30 \
|
|
113
|
+
--free-delivery \
|
|
114
|
+
--limit 5
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 4. 오프라인 fixture로 검증
|
|
118
|
+
|
|
119
|
+
실제 네트워크 없이 저장된 HTML/JSON 파일로 동일한 파싱을 테스트한다.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python3 ohou-today-deal/scripts/ohou_today_deal.py list \
|
|
123
|
+
--html-file ./today-deals.html \
|
|
124
|
+
--limit 3
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Output format
|
|
128
|
+
|
|
129
|
+
기본 출력은 들여쓰기 JSON (`indent=2`). 파이프/스크립트에서 사용할 때는 출력을 `jq` 등으로 후처리한다.
|
|
130
|
+
|
|
131
|
+
주요 필드:
|
|
132
|
+
|
|
133
|
+
| 필드 | 설명 |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `source.fetched_at` | 조회 시각 (UTC ISO 8601) |
|
|
136
|
+
| `count` | 반환된 상품 수 |
|
|
137
|
+
| `total_count` | 전체 오늘의딜 상품 수 |
|
|
138
|
+
| `filtered_count` | 필터 적용 후 상품 수 |
|
|
139
|
+
| `items[].best_price` | 쿠폰/결제혜택 반영 최저가 (없으면 null) |
|
|
140
|
+
| `items[].best_discount_rate` | 혜택 반영 할인율 (없으면 null) |
|
|
141
|
+
| `items[].free_delivery` | 무료배송 여부 |
|
|
142
|
+
| `items[].sold_out` | 품절 여부 |
|
|
143
|
+
|
|
144
|
+
## Endpoints used
|
|
145
|
+
|
|
146
|
+
이 스킬이 호출하는 공개 endpoint:
|
|
147
|
+
|
|
148
|
+
| Method | URL | 용도 |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| GET | `https://ohou.se/commerces/today_deals` | 오늘의딜 공개 HTML (서버 렌더링) |
|
|
151
|
+
|
|
152
|
+
비로그인 / 무인증. 헤더는 `User-Agent` + `Accept` 만.
|
|
153
|
+
|
|
154
|
+
## Response policy
|
|
155
|
+
|
|
156
|
+
- 상위 3~5개만 먼저 보여준다.
|
|
157
|
+
- 상품명, 브랜드, 할인가, 원가, 할인율, 평점/리뷰 수, 무료배송 여부, 링크를 정리한다.
|
|
158
|
+
- 가격, 할인, 품절, 쿠폰/결제혜택은 "조회 시각 기준"으로 변동 가능하다고 명시한다.
|
|
159
|
+
- 구매/장바구니/결제는 자동화하지 말고 상품 링크만 제공한다.
|
|
160
|
+
- "지금 사라" 같은 행위 유도 금지 — 사용자가 직접 페이지에서 구매한다.
|
|
161
|
+
|
|
162
|
+
## Done when
|
|
163
|
+
|
|
164
|
+
- 오늘의딜 상품 후보가 JSON 또는 요약 목록으로 반환된다.
|
|
165
|
+
- 할인율/가격 기준과 조회 시점이 분리되어 설명된다.
|
|
166
|
+
- 로그인, 구매, 결제, 개인화 기능을 시도하지 않았다.
|
|
167
|
+
|
|
168
|
+
## Failure modes
|
|
169
|
+
|
|
170
|
+
- **`__NEXT_DATA__` 미발견**: 오늘의집이 Next.js SSR 구조를 변경하거나, 서버 렌더링 대신 클라이언트 렌더링으로 전환하면 `ValueError` 발생. 스킬 파서 수정이 필요하다.
|
|
171
|
+
- **today-deal-feed queryKey 미발견**: React Query 키 이름이 바뀌면 `extract_deals()`는 빈 리스트를 반환한다 (`total_count: 0`). `TODAY_DEAL_FEED_KEYS` 상수를 새 키 이름으로 업데이트해야 한다.
|
|
172
|
+
- **HTTP 403**: ohou.se 앞단 Akamai bot manager가 요청을 차단한 경우. `User-Agent` 헤더가 변경되어 봇 자기소개 + contact URL 시그니처를 잃었을 가능성이 높다. 우회 시도하지 않고 에러 출력 후 종료한다.
|
|
173
|
+
- **HTTP 4xx/5xx (기타)**: 일시 장애. 우회 시도하지 않고 에러 출력 후 종료.
|
|
174
|
+
- **빈 응답 (`total_count: 0`)**: 오늘의딜이 아직 업데이트되지 않았거나, 페이지 구조가 바뀐 경우. 브라우저에서 직접 확인을 안내한다.
|
|
175
|
+
- **가격/쿠폰 변동**: `best_price`는 조회 시점 기준이며, 사용자별 쿠폰/결제수단에 따라 실제 결제가는 다를 수 있다.
|
|
176
|
+
- **필드 누락**: 일부 상품에 `bestDiscountPrice`, `badgeProperties.isFreeDelivery`, `scrapInfo` 등이 없을 수 있다. null로 처리된다.
|
|
177
|
+
|
|
178
|
+
## Notes
|
|
179
|
+
|
|
180
|
+
- read-only 스킬이다.
|
|
181
|
+
- 화면 선택자보다 서버 렌더링 초기 JSON을 우선한다.
|
|
182
|
+
- 새 dependency 없이 Python 표준 라이브러리만 사용한다.
|