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