@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,424 @@
1
+ #!/usr/bin/env python3
2
+ """K-Startup (data.go.kr 15125364) CLI helper for the kstartup-search skill.
3
+
4
+ ์กฐํšŒ ์ „์šฉ. ์ผ๋ฐ˜ ํ˜ธ์ถœ์€ k-skill-proxy ๊ฒฝ์œ , `--direct` ๋Š” ์‚ฌ์šฉ์ž API ํ‚ค๋กœ ์ง์ ‘ ํ˜ธ์ถœ.
5
+ stdlib only (urllib, json, argparse, ssl).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import datetime
11
+ import json
12
+ import os
13
+ import ssl
14
+ import sys
15
+ import urllib.error
16
+ import urllib.parse
17
+ import urllib.request
18
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
19
+
20
+ DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
21
+ PROXY_DOWN_MSG = "์„ค์ •๋œ k-skill-proxy ์„œ๋ฒ„๊ฐ€ ์‘๋‹ตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ์žฌ์‹œ๋„ํ•˜๊ฑฐ๋‚˜ ์šด์˜์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”."
22
+ PROXY_KEY_NOT_CONFIGURED_MSG = "k-skill-proxy์— ํ•„์š”ํ•œ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์šด์˜์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”."
23
+ KSTARTUP_UPSTREAM_BASE_URL = "https://apis.data.go.kr/B552735/kisedKstartupService01"
24
+ DEFAULT_SECRETS_PATH = os.path.expanduser("~/.config/k-skill/secrets.env")
25
+
26
+ OPERATIONS: Dict[str, Dict[str, Any]] = {
27
+ "business-info": {
28
+ "path": "getBusinessInformation01",
29
+ "allowed": ("biz_category_cd", "supt_biz_titl_nm", "biz_yr"),
30
+ },
31
+ "announcements": {
32
+ "path": "getAnnouncementInformation01",
33
+ "allowed": (
34
+ "intg_pbanc_yn", "intg_pbanc_biz_nm", "biz_pbanc_nm",
35
+ "supt_biz_clsfc", "aply_trgt_ctnt", "supt_regin",
36
+ "pbanc_rcpt_bgng_dt", "pbanc_rcpt_end_dt",
37
+ "aply_trgt", "biz_enyy", "biz_trgt_age", "prfn_matr",
38
+ "rcrt_prgs_yn",
39
+ ),
40
+ },
41
+ "contents": {
42
+ "path": "getContentInformation01",
43
+ "allowed": ("clss_cd", "titl_nm"),
44
+ },
45
+ "statistics": {
46
+ "path": "getStatisticalInformation01",
47
+ "allowed": ("titl_nm", "file_nm"),
48
+ },
49
+ }
50
+
51
+ YN_FIELDS = {"intg_pbanc_yn", "rcrt_prgs_yn"}
52
+ DATE_FIELDS = {"pbanc_rcpt_bgng_dt", "pbanc_rcpt_end_dt"}
53
+
54
+ # Fields where the K-Startup upstream is observed to ignore the server-side
55
+ # filter and return non-matching rows. SKILL.md L121 promises that the helper
56
+ # re-applies these filters on the client side after receiving the response.
57
+ #
58
+ # - supt_regin: upstream returns mixed regions even when supt_regin is set.
59
+ # - aply_trgt: upstream returns rows whose aply_trgt does not contain the
60
+ # requested target (e.g. asking for "์˜ˆ๋น„์ฐฝ์—…์ž" returns rows
61
+ # with only "์ผ๋ฐ˜์ธ,์ผ๋ฐ˜๊ธฐ์—…").
62
+ # - biz_enyy: upstream returns rows whose biz_enyy does not include the
63
+ # requested founding period bucket.
64
+ #
65
+ # Matching policy: substring match against the comma-separated list inside
66
+ # each row's field. Multiple requested values (comma-separated by the user)
67
+ # are AND-joined: every requested token must appear somewhere in the row.
68
+ # This mirrors how the K-Startup web UI narrows results.
69
+ CLIENT_FILTER_FIELDS = {"supt_regin", "aply_trgt", "biz_enyy"}
70
+
71
+ REGION_SHORTNAME = {
72
+ "์„œ์šธํŠน๋ณ„์‹œ": "์„œ์šธ", "์„œ์šธ์‹œ": "์„œ์šธ", "์„œ์šธ": "์„œ์šธ",
73
+ "๋ถ€์‚ฐ๊ด‘์—ญ์‹œ": "๋ถ€์‚ฐ", "๋ถ€์‚ฐ์‹œ": "๋ถ€์‚ฐ", "๋ถ€์‚ฐ": "๋ถ€์‚ฐ",
74
+ "๋Œ€๊ตฌ๊ด‘์—ญ์‹œ": "๋Œ€๊ตฌ", "๋Œ€๊ตฌ์‹œ": "๋Œ€๊ตฌ", "๋Œ€๊ตฌ": "๋Œ€๊ตฌ",
75
+ "์ธ์ฒœ๊ด‘์—ญ์‹œ": "์ธ์ฒœ", "์ธ์ฒœ์‹œ": "์ธ์ฒœ", "์ธ์ฒœ": "์ธ์ฒœ",
76
+ "๊ด‘์ฃผ๊ด‘์—ญ์‹œ": "๊ด‘์ฃผ", "๊ด‘์ฃผ์‹œ": "๊ด‘์ฃผ", "๊ด‘์ฃผ": "๊ด‘์ฃผ",
77
+ "๋Œ€์ „๊ด‘์—ญ์‹œ": "๋Œ€์ „", "๋Œ€์ „์‹œ": "๋Œ€์ „", "๋Œ€์ „": "๋Œ€์ „",
78
+ "์šธ์‚ฐ๊ด‘์—ญ์‹œ": "์šธ์‚ฐ", "์šธ์‚ฐ์‹œ": "์šธ์‚ฐ", "์šธ์‚ฐ": "์šธ์‚ฐ",
79
+ "์„ธ์ข…ํŠน๋ณ„์ž์น˜์‹œ": "์„ธ์ข…", "์„ธ์ข…์‹œ": "์„ธ์ข…", "์„ธ์ข…": "์„ธ์ข…",
80
+ "๊ฒฝ๊ธฐ๋„": "๊ฒฝ๊ธฐ", "๊ฒฝ๊ธฐ": "๊ฒฝ๊ธฐ",
81
+ "๊ฐ•์›ํŠน๋ณ„์ž์น˜๋„": "๊ฐ•์›", "๊ฐ•์›๋„": "๊ฐ•์›", "๊ฐ•์›": "๊ฐ•์›",
82
+ "์ถฉ์ฒญ๋ถ๋„": "์ถฉ๋ถ", "์ถฉ๋ถ": "์ถฉ๋ถ",
83
+ "์ถฉ์ฒญ๋‚จ๋„": "์ถฉ๋‚จ", "์ถฉ๋‚จ": "์ถฉ๋‚จ",
84
+ "์ „๋ถํŠน๋ณ„์ž์น˜๋„": "์ „๋ถ", "์ „๋ผ๋ถ๋„": "์ „๋ถ", "์ „๋ถ": "์ „๋ถ",
85
+ "์ „๋ผ๋‚จ๋„": "์ „๋‚จ", "์ „๋‚จ": "์ „๋‚จ",
86
+ "๊ฒฝ์ƒ๋ถ๋„": "๊ฒฝ๋ถ", "๊ฒฝ๋ถ": "๊ฒฝ๋ถ",
87
+ "๊ฒฝ์ƒ๋‚จ๋„": "๊ฒฝ๋‚จ", "๊ฒฝ๋‚จ": "๊ฒฝ๋‚จ",
88
+ "์ œ์ฃผํŠน๋ณ„์ž์น˜๋„": "์ œ์ฃผ", "์ œ์ฃผ๋„": "์ œ์ฃผ", "์ œ์ฃผ": "์ œ์ฃผ",
89
+ "์ „๊ตญ": "์ „๊ตญ",
90
+ }
91
+
92
+
93
+ class HelperError(RuntimeError):
94
+ """User-facing CLI error."""
95
+
96
+
97
+ def load_secrets(path: str = DEFAULT_SECRETS_PATH) -> Dict[str, str]:
98
+ """Read dotenv-like secrets file. Returns {} if missing."""
99
+ data: Dict[str, str] = {}
100
+ if not os.path.exists(path):
101
+ return data
102
+ try:
103
+ with open(path, "r", encoding="utf-8") as fh:
104
+ for raw_line in fh:
105
+ line = raw_line.strip()
106
+ if not line or line.startswith("#"):
107
+ continue
108
+ if "=" not in line:
109
+ continue
110
+ key, _, value = line.partition("=")
111
+ key = key.strip()
112
+ value = value.strip()
113
+ if value.startswith('"') and value.endswith('"') and len(value) >= 2:
114
+ value = value[1:-1]
115
+ if value.startswith("'") and value.endswith("'") and len(value) >= 2:
116
+ value = value[1:-1]
117
+ if key:
118
+ data[key] = value
119
+ except OSError:
120
+ return data
121
+ return data
122
+
123
+
124
+ def resolve_api_key(args: argparse.Namespace) -> Optional[str]:
125
+ """`--direct` ์ „์šฉ API ํ‚ค ํ•ด์„. env > secrets file ์ˆœ์„œ."""
126
+ env_key = os.environ.get("KSKILL_KSTARTUP_API_KEY") or os.environ.get("DATA_GO_KR_API_KEY")
127
+ if env_key:
128
+ return env_key.strip() or None
129
+ secrets = load_secrets(args.secrets_path or DEFAULT_SECRETS_PATH)
130
+ return (secrets.get("KSKILL_KSTARTUP_API_KEY") or secrets.get("DATA_GO_KR_API_KEY") or "").strip() or None
131
+
132
+
133
+ def validate_yyyymmdd(value: str, field: str) -> str:
134
+ digits = "".join(c for c in value if c.isdigit())
135
+ if len(digits) != 8:
136
+ raise HelperError(f"{field} must be YYYYMMDD (got: {value!r})")
137
+ year = int(digits[0:4])
138
+ month = int(digits[4:6])
139
+ day = int(digits[6:8])
140
+ try:
141
+ datetime.date(year, month, day)
142
+ except ValueError as exc:
143
+ raise HelperError(f"{field} must be a valid YYYYMMDD date (got: {value!r})") from exc
144
+ return digits
145
+
146
+
147
+ def build_query(args: argparse.Namespace, operation: str) -> Dict[str, Any]:
148
+ if operation not in OPERATIONS:
149
+ raise HelperError(f"Unknown operation: {operation}")
150
+ if args.page < 1:
151
+ raise HelperError("--page must be >= 1")
152
+ if args.per_page < 1 or args.per_page > 100:
153
+ raise HelperError("--per-page must be in [1, 100]")
154
+
155
+ query: Dict[str, Any] = {
156
+ "page": args.page,
157
+ "perPage": args.per_page,
158
+ "returnType": "json",
159
+ }
160
+ for field in OPERATIONS[operation]["allowed"]:
161
+ attr = field.lower()
162
+ raw = getattr(args, attr, None)
163
+ if raw is None or str(raw).strip() == "":
164
+ continue
165
+ value = str(raw).strip()
166
+ if field in DATE_FIELDS:
167
+ value = validate_yyyymmdd(value, field)
168
+ elif field in YN_FIELDS:
169
+ upper = value.upper()
170
+ if upper not in {"Y", "N"}:
171
+ raise HelperError(f"{field} must be Y or N (got: {value!r})")
172
+ value = upper
173
+ elif field == "biz_yr":
174
+ if not (len(value) == 4 and value.isdigit()):
175
+ raise HelperError(f"biz_yr must be 4 digits (got: {value!r})")
176
+ query[field] = value
177
+
178
+ if (
179
+ operation == "announcements"
180
+ and query.get("pbanc_rcpt_bgng_dt")
181
+ and query.get("pbanc_rcpt_end_dt")
182
+ and query["pbanc_rcpt_bgng_dt"] > query["pbanc_rcpt_end_dt"]
183
+ ):
184
+ raise HelperError("pbanc_rcpt_bgng_dt must be <= pbanc_rcpt_end_dt")
185
+ return query
186
+
187
+
188
+ def encode_query(query: Dict[str, Any]) -> str:
189
+ pairs: List[Tuple[str, str]] = [(k, str(v)) for k, v in query.items()]
190
+ return urllib.parse.urlencode(pairs, doseq=False, safe="")
191
+
192
+
193
+ def build_url(operation: str, query: Dict[str, Any], *, direct: bool, api_key: Optional[str], proxy_base_url: str) -> str:
194
+ if direct:
195
+ if not api_key:
196
+ raise HelperError(
197
+ "KSKILL_KSTARTUP_API_KEY (๋˜๋Š” DATA_GO_KR_API_KEY) ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. "
198
+ "๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ 15125364 ํ™œ์šฉ์‹ ์ฒญ ํ›„ ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์•„ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋‚˜ ~/.config/k-skill/secrets.env ์— ๋‘์„ธ์š”."
199
+ )
200
+ path = OPERATIONS[operation]["path"]
201
+ with_key = dict(query)
202
+ with_key["ServiceKey"] = api_key
203
+ return f"{KSTARTUP_UPSTREAM_BASE_URL}/{path}?{encode_query(with_key)}"
204
+ base = proxy_base_url.rstrip("/")
205
+ return f"{base}/v1/kstartup/{operation}?{encode_query(query)}"
206
+
207
+
208
+ def is_proxy_not_configured_body(body: str) -> bool:
209
+ try:
210
+ payload = json.loads(body)
211
+ except json.JSONDecodeError:
212
+ return False
213
+ return isinstance(payload, dict) and payload.get("error") == "upstream_not_configured"
214
+
215
+
216
+ def http_get(url: str, *, timeout: int, via_proxy: bool = False) -> Tuple[int, str, str]:
217
+ headers = {
218
+ "accept": "application/json",
219
+ "user-agent": "k-skill/kstartup-search",
220
+ }
221
+ request = urllib.request.Request(url, headers=headers, method="GET")
222
+ context = ssl.create_default_context()
223
+ try:
224
+ with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
225
+ body = response.read().decode("utf-8", errors="replace")
226
+ return response.status, response.headers.get("content-type", ""), body
227
+ except urllib.error.HTTPError as exc:
228
+ body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
229
+ if exc.code == 503 and is_proxy_not_configured_body(body):
230
+ raise HelperError(PROXY_KEY_NOT_CONFIGURED_MSG) from exc
231
+ return exc.code, exc.headers.get("content-type", "") if exc.headers else "", body
232
+ except urllib.error.URLError as exc:
233
+ if not via_proxy:
234
+ raise HelperError(f"network error: {exc.reason}") from exc
235
+ raise HelperError(f"{PROXY_DOWN_MSG} (์ƒ์„ธ: {exc.reason})") from exc
236
+
237
+
238
+ def _normalise_filter_token(field: str, token: str) -> str:
239
+ if field == "supt_regin":
240
+ return REGION_SHORTNAME.get(token, token)
241
+ return token
242
+
243
+
244
+ def _row_matches_token(row: Dict[str, Any], field: str, token: str) -> bool:
245
+ raw = row.get(field)
246
+ if raw is None:
247
+ return False
248
+ haystack = str(raw)
249
+ needle = _normalise_filter_token(field, token)
250
+ return needle in haystack
251
+
252
+
253
+ def _row_matches_field(row: Dict[str, Any], field: str, requested: str) -> bool:
254
+ tokens = [t.strip() for t in requested.split(",") if t.strip()]
255
+ if not tokens:
256
+ return True
257
+ return all(_row_matches_token(row, field, token) for token in tokens)
258
+
259
+
260
+ def apply_client_filters(
261
+ payload: Dict[str, Any],
262
+ args: argparse.Namespace,
263
+ operation: str,
264
+ ) -> Dict[str, Any]:
265
+ if operation != "announcements":
266
+ return payload
267
+ requested: Dict[str, str] = {}
268
+ for field in CLIENT_FILTER_FIELDS:
269
+ value = getattr(args, field, None)
270
+ if value is None:
271
+ continue
272
+ text = str(value).strip()
273
+ if text:
274
+ requested[field] = text
275
+ if not requested:
276
+ return payload
277
+ data = payload.get("data")
278
+ if not isinstance(data, list):
279
+ return payload
280
+ upstream_count = len(data)
281
+ filtered = [
282
+ row for row in data
283
+ if isinstance(row, dict)
284
+ and all(_row_matches_field(row, field, value) for field, value in requested.items())
285
+ ]
286
+ payload["data"] = filtered
287
+ payload["currentCount"] = len(filtered)
288
+ payload["client_filter"] = {
289
+ "fields": requested,
290
+ "upstream_returned": upstream_count,
291
+ "after_filter": len(filtered),
292
+ "note": "Applied after upstream response because K-Startup ignores some server-side filters.",
293
+ }
294
+ return payload
295
+
296
+
297
+ def summarise(operation: str, payload: Dict[str, Any]) -> str:
298
+ items: Iterable[Dict[str, Any]] = []
299
+ if isinstance(payload, dict):
300
+ data = payload.get("data") or payload.get("items")
301
+ if isinstance(data, list):
302
+ items = data
303
+ elif isinstance(payload.get("response"), dict):
304
+ response = payload["response"]
305
+ body = response.get("body") or {}
306
+ items = body.get("items") or []
307
+ items = list(items or [])
308
+ if not items:
309
+ return "[summary] ๋งค์นญ๋˜๋Š” ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค. ํ•„ํ„ฐ๋ฅผ ์™„ํ™”ํ•˜๊ฑฐ๋‚˜ ํŽ˜์ด์ง€๋ฅผ ๋„˜๊ธฐ์„ธ์š”."
310
+ lines = [f"[summary] operation={operation} count={len(items)} (page={payload.get('query', {}).get('page', payload.get('page'))} perPage={payload.get('query', {}).get('perPage', payload.get('perPage'))})"]
311
+ for index, item in enumerate(items, start=1):
312
+ title = (
313
+ item.get("biz_pbanc_nm")
314
+ or item.get("supt_biz_titl_nm")
315
+ or item.get("titl_nm")
316
+ or item.get("intg_pbanc_biz_nm")
317
+ or "(์ œ๋ชฉ ์—†์Œ)"
318
+ )
319
+ region = item.get("supt_regin") or item.get("biz_category_cd") or item.get("clss_cd") or ""
320
+ period = ""
321
+ if item.get("pbanc_rcpt_bgng_dt") or item.get("pbanc_rcpt_end_dt"):
322
+ period = f" {item.get('pbanc_rcpt_bgng_dt','?')} ~ {item.get('pbanc_rcpt_end_dt','?')}"
323
+ url = item.get("detl_pg_url") or ""
324
+ lines.append(f" {index:>2}. {title} {region}{period}")
325
+ if url:
326
+ lines.append(f" โ†’ {url}")
327
+ return "\n".join(lines)
328
+
329
+
330
+ def _add_filter_args(parser: argparse.ArgumentParser, operation: str) -> None:
331
+ allowed = OPERATIONS[operation]["allowed"]
332
+ for field in allowed:
333
+ flag = "--" + field.replace("_", "-").lower()
334
+ parser.add_argument(flag, dest=field.lower(), default=None,
335
+ help=f"K-Startup field: {field}")
336
+
337
+
338
+ def make_parser() -> argparse.ArgumentParser:
339
+ parser = argparse.ArgumentParser(
340
+ prog="run_kstartup.py",
341
+ description="์ฐฝ์—…์ง„ํฅ์› K-Startup Open API (data.go.kr 15125364) ์กฐํšŒ helper",
342
+ )
343
+ subparsers = parser.add_subparsers(dest="operation", required=True)
344
+
345
+ for operation in OPERATIONS:
346
+ sub = subparsers.add_parser(operation, help=f"K-Startup {operation} endpoint")
347
+ sub.add_argument("--page", type=int, default=1)
348
+ sub.add_argument("--per-page", dest="per_page", type=int, default=10)
349
+ format_group = sub.add_mutually_exclusive_group()
350
+ format_group.add_argument("--text", action="store_true", help="์‚ฌ๋žŒ์šฉ ์š”์•ฝ")
351
+ format_group.add_argument("--json", action="store_true", help="๊ตฌ์กฐํ™” JSON ์ถœ๋ ฅ (๊ธฐ๋ณธ)")
352
+ sub.add_argument("--dry-run", action="store_true", dest="dry_run",
353
+ help="์š”์ฒญ URL/ํŒŒ๋ผ๋ฏธํ„ฐ๋งŒ ์ถœ๋ ฅ, ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ ์—†์Œ")
354
+ sub.add_argument("--timeout", type=int, default=30)
355
+ sub.add_argument("--proxy-base-url", default=os.environ.get("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY_BASE_URL))
356
+ sub.add_argument("--direct", action="store_true",
357
+ help="proxy ์šฐํšŒ, KSKILL_KSTARTUP_API_KEY ๋กœ ์ง์ ‘ ํ˜ธ์ถœ")
358
+ sub.add_argument("--secrets-path", default=DEFAULT_SECRETS_PATH,
359
+ help=f"--direct ์‹œ secrets ํŒŒ์ผ ๊ฒฝ๋กœ (๊ธฐ๋ณธ {DEFAULT_SECRETS_PATH})")
360
+ _add_filter_args(sub, operation)
361
+ return parser
362
+
363
+
364
+ def run(argv: Optional[List[str]] = None) -> int:
365
+ parser = make_parser()
366
+ args = parser.parse_args(argv)
367
+ operation = args.operation
368
+
369
+ try:
370
+ query = build_query(args, operation)
371
+ except HelperError as exc:
372
+ print(f"[error] {exc}", file=sys.stderr)
373
+ return 2
374
+
375
+ if args.dry_run:
376
+ if args.direct:
377
+ preview = build_url(operation, query, direct=True, api_key="<DRY-RUN>", proxy_base_url=args.proxy_base_url)
378
+ else:
379
+ preview = build_url(operation, query, direct=False, api_key=None, proxy_base_url=args.proxy_base_url)
380
+ preview = preview.replace(os.environ.get("KSKILL_KSTARTUP_API_KEY", ""), "<DRY-RUN>") if os.environ.get("KSKILL_KSTARTUP_API_KEY") else preview
381
+ preview = preview.replace(os.environ.get("DATA_GO_KR_API_KEY", ""), "<DRY-RUN>") if os.environ.get("DATA_GO_KR_API_KEY") else preview
382
+ result = {"operation": operation, "url": preview, "query": query, "direct": bool(args.direct)}
383
+ print(json.dumps(result, ensure_ascii=False, indent=2))
384
+ return 0
385
+
386
+ api_key = resolve_api_key(args) if args.direct else None
387
+ try:
388
+ url = build_url(operation, query, direct=args.direct, api_key=api_key, proxy_base_url=args.proxy_base_url)
389
+ except HelperError as exc:
390
+ print(f"[error] {exc}", file=sys.stderr)
391
+ return 3
392
+
393
+ try:
394
+ status, content_type, body = http_get(url, timeout=args.timeout, via_proxy=not args.direct)
395
+ except HelperError as exc:
396
+ print(f"[error] {exc}", file=sys.stderr)
397
+ return 4
398
+
399
+ payload: Any
400
+ try:
401
+ payload = json.loads(body) if body else {}
402
+ except json.JSONDecodeError:
403
+ print(f"[error] upstream returned non-JSON content-type={content_type!r} status={status}", file=sys.stderr)
404
+ print(body[:500])
405
+ return 5
406
+
407
+ if not isinstance(payload, dict):
408
+ payload = {"raw": payload}
409
+ payload.setdefault("query", query)
410
+
411
+ payload = apply_client_filters(payload, args, operation)
412
+
413
+ if args.text:
414
+ print(summarise(operation, payload))
415
+ else:
416
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
417
+
418
+ if status >= 400:
419
+ return 6
420
+ return 0
421
+
422
+
423
+ if __name__ == "__main__":
424
+ raise SystemExit(run())
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "kstartup-search",
3
+ "description": "๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ ์ฐฝ์—…์ง„ํฅ์› K-Startup Open API(15125364)๋กœ ํ†ตํ•ฉ ๊ณต๊ณ  ์‚ฌ์—… ์ •๋ณดยท์ง€์›์‚ฌ์—… ๊ณต๊ณ ยท์ฐฝ์—… ์ฝ˜ํ…์ธ ยทํ†ต๊ณ„๋ณด๊ณ ์„œ๋ฅผ k-skill-proxy ๊ฒฝ์œ ๋กœ ์กฐํšŒํ•œ๋‹ค. ๊ฒ€์ƒ‰ ์ „์šฉ. ๋Œ์‡ ์—์„œ๋Š” ๊ณต์‹ ํ‘œ๋ฉด์„ ํ†ตํ•œ ํ›„์† ์•ก์…˜๊นŒ์ง€ ์ง„ํ–‰ํ•œ๋‹ค.",
4
+ "profiles": [
5
+ "proxy",
6
+ "vault",
7
+ "browser",
8
+ "action:submission"
9
+ ],
10
+ "frontmatter": "name: kstartup-search\ndescription: ๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ ์ฐฝ์—…์ง„ํฅ์› K-Startup Open API(15125364)๋กœ ํ†ตํ•ฉ ๊ณต๊ณ  ์‚ฌ์—… ์ •๋ณดยท์ง€์›์‚ฌ์—… ๊ณต๊ณ ยท์ฐฝ์—… ์ฝ˜ํ…์ธ ยทํ†ต๊ณ„๋ณด๊ณ ์„œ๋ฅผ k-skill-proxy ๊ฒฝ์œ ๋กœ ์กฐํšŒํ•œ๋‹ค. ๊ฒ€์ƒ‰ ์ „์šฉ. ๋Œ์‡ ์—์„œ๋Š” ๊ณต์‹ ํ‘œ๋ฉด์„ ํ†ตํ•œ ํ›„์† ์•ก์…˜๊นŒ์ง€ ์ง„ํ–‰ํ•œ๋‹ค.\nlicense: MIT\nmetadata:\n category: business\n locale: ko-KR\n phase: v1"
11
+ }
@@ -0,0 +1,244 @@
1
+ # KTX Booking
2
+
3
+ ## What this skill does
4
+
5
+ `korail2` ์œ„์— `scripts/ktx_booking.py` helper ๋ฅผ ์–น์–ด KTX/Korail ์กฐํšŒ, ํ˜ธ์ฐจ๋ณ„ ์ขŒ์„๋ฒˆํ˜ธ ํ™•์ธ, ์˜ˆ์•ฝ, ์˜ˆ์•ฝ ํ™•์ธ, ์ทจ์†Œ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.
6
+
7
+ ์ตœ๊ทผ Korail ์•ฑ์˜ Dynapath anti-bot ์ฒดํฌ ๋•Œ๋ฌธ์— ์›๋ณธ `korail2` 0.4.0 ์˜ˆ์ œ๋งŒ์œผ๋กœ๋Š” `MACRO ERROR` ๊ฐ€ ๋‚  ์ˆ˜ ์žˆ๋‹ค. ์ด ์Šคํ‚ฌ์€ helper ๊ฐ€ `x-dynapath-m-token`, `Sid`, ์ตœ์‹  app version(`250601002`)์„ ๋ถ™์—ฌ ์‹ค์ œ ์˜ˆ๋งค ํ๋ฆ„์„ ๋ณต๊ตฌํ•˜๋Š” ๊ฒƒ์„ ์ „์ œ๋กœ ํ•œ๋‹ค.
8
+
9
+ ## When to use
10
+
11
+ - "์„œ์šธ์—์„œ ๋ถ€์‚ฐ ๊ฐ€๋Š” KTX ์ฐพ์•„์ค˜"
12
+ - "์ฝ”๋ ˆ์ผ ์˜ˆ์•ฝ ํ™•์ธํ•ด์ค˜"
13
+ - "KTX ์ทจ์†Œํ•ด์ค˜"
14
+ - "์˜ค์ „ 9์‹œ ์ดํ›„ KTX ์ค‘ ์ œ์ผ ๋น ๋ฅธ ๊ฑฐ ์žก์•„์ค˜"
15
+ - "KTX ๋‚จ์€ ์ขŒ์„ ๋ฒˆํ˜ธ ํ™•์ธํ•ด์ค˜"
16
+ - "์ด ์—ด์ฐจ ์ฝ˜์„ผํŠธ ์žˆ๋Š” ๊ฟ€ํŒ ์ขŒ์„๋ถ€ํ„ฐ ๋ณด์—ฌ์ค˜"
17
+ - "KTX 5ํ˜ธ์ฐจ ๋‚จ์€ ์ž๋ฆฌ๋งŒ ๋ด์ค˜"
18
+ - "์˜ˆ์•ฝํ•˜๊ธฐ ์ „์— ํ˜ธ์ฐจ๋ณ„ ์ขŒ์„ ํ™•์ธํ•ด์ค˜"
19
+ - "N์นด๋“œ๋กœ ํ• ์ธ ์—ด์ฐจ ์ฐพ์•„์ค˜"
20
+ - "๋‚ด N์นด๋“œ ๋ชฉ๋ก ๋ณด์—ฌ์ค˜"
21
+ - "N์นด๋“œ ํ• ์ธ ์ ์šฉํ•ด์„œ ์˜ˆ์•ฝํ•ด์ค˜"
22
+
23
+ ## When not to use
24
+
25
+ - SRT ์˜ˆ๋งค์ธ ๊ฒฝ์šฐ
26
+ - ๋Œ์‡ ๊ฐ€ ์•„๋‹ˆ๋ฉฐ ์‹ค๊ฒฐ์ œ ํ™•์ •๊นŒ์ง€ ์ž๋™ํ™”ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
27
+ - credential ์„ ํ‰๋ฌธ์œผ๋กœ ๋„ฃ์œผ๋ ค๋Š” ๊ฒฝ์šฐ
28
+
29
+ ## Prerequisites
30
+
31
+ - Python 3.10+
32
+ - `python3 -m pip install korail2-ncard pycryptodome`
33
+
34
+ ## Required environment variables
35
+
36
+ - `KSKILL_KTX_ID`
37
+ - `KSKILL_KTX_PASSWORD`
38
+
39
+ ### Credential handling
40
+
41
+ - ๋Œ์‡  credential mode์—์„œ๋Š” `vault-run` capability๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด `request_vault_credential`์„ ํ˜ธ์ถœํ•œ๋‹ค. ID/PW ์›๋ฌธ์„ ์ฑ„ํŒ…์ด๋‚˜ shell์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค.
42
+ - ๊ทธ ๋ฐ–์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ด๋ฏธ ์ฃผ์ž…๋œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ โ†’ host vault โ†’ `~/.config/k-skill/secrets.env` ์ˆœ์„œ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
43
+
44
+ ## Inputs
45
+
46
+ - ์ถœ๋ฐœ์—ญ
47
+ - ๋„์ฐฉ์—ญ
48
+ - ๋‚ ์งœ: `YYYYMMDD`
49
+ - ํฌ๋ง ์‹œ์ž‘ ์‹œ๊ฐ: `HHMMSS`
50
+ - ์ธ์› ์ˆ˜์™€ ์Šน๊ฐ ์œ ํ˜•
51
+ - ์ขŒ์„ ์„ ํ˜ธ
52
+ - ์ขŒ์„ ์ƒ์„ธ ์กฐ๊ฑด: ๊ฐ์‹ค ๋“ฑ๊ธ‰, ํ˜ธ์ฐจ ๋ฒˆํ˜ธ, ๋‚จ์€ ์ขŒ์„๋งŒ ๋ณด๊ธฐ, ์ฝ˜์„ผํŠธ ๊ฟ€ํŒ ์ขŒ์„ ์šฐ์„ 
53
+ - ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ ๋ณต์‚ฌํ•œ `train_id`
54
+
55
+ ## Workflow
56
+
57
+ ### 0. Install the package globally when missing
58
+
59
+ `python3 -c 'import korail2, Crypto'` ๊ฐ€ ์‹คํŒจํ•˜๋ฉด ๋‹ค๋ฅธ ๊ตฌํ˜„์œผ๋กœ ์šฐํšŒํ•˜์ง€ ๋ง๊ณ  ์ „์—ญ Python ํŒจํ‚ค์ง€ ์„ค์น˜๋ฅผ ๋จผ์ € ์‹œ๋„ํ•œ๋‹ค.
60
+
61
+ ```bash
62
+ python3 -m pip install korail2-ncard pycryptodome
63
+ ```
64
+
65
+ ### 1. Ensure credentials are available
66
+
67
+ ๋Œ์‡  credential mode์—์„œ๋Š” `vault-run`์˜ Korail capability๋ฅผ ํ™•์ธํ•˜๊ณ , ์—†์œผ๋ฉด `request_vault_credential`๋กœ ์•ฑ vault ์ž…๋ ฅ UI๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. generic fallback์—์„œ๋งŒ `KSKILL_KTX_ID`, `KSKILL_KTX_PASSWORD` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ํ™•์ธํ•œ๋‹ค.
68
+
69
+ ์‹œํฌ๋ฆฟ์ด ์—†๋‹ค๋Š” ์ด์œ ๋กœ ์›น์‚ฌ์ดํŠธ๋ฅผ ์ง์ ‘ ๊ธ๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๋น„๊ณต์‹ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์ง€ ์•Š๋Š”๋‹ค.
70
+
71
+ ### 2. Search first via the helper
72
+
73
+ ํ•ญ์ƒ helper ๋ฅผ ํ†ตํ•ด ์กฐํšŒํ•œ๋‹ค.
74
+
75
+ ```bash
76
+ python3 scripts/ktx_booking.py search ์„œ์šธ ๋ถ€์‚ฐ 20260328 090000 --limit 5
77
+ ```
78
+
79
+ ๊ธฐ๋ณธ `--train-type` ์€ `ktx` ๋‹ค. ITX-์ฒญ์ถ˜(์˜ˆ: ๋‚จ์ถ˜์ฒœโ†”์šฉ์‚ฐ)ยทITX-์ƒˆ๋งˆ์„ยท๋ฌด๊ถํ™”ํ˜ธ์ฒ˜๋Ÿผ KTX ์™ธ ๋…ธ์„ ์„ ์žก์œผ๋ ค๋ฉด `--train-type` ์œผ๋กœ ์ง€์ •ํ•œ๋‹ค.
80
+
81
+ ```bash
82
+ python3 scripts/ktx_booking.py search ๋‚จ์ถ˜์ฒœ ์šฉ์‚ฐ 20260503 150000 --train-type itx-cheongchun
83
+ ```
84
+
85
+ ์„ ํƒ์ง€: `ktx`, `itx-saemaeul`, `mugunghwa`, `nuriro`, `tonggeun`, `itx-cheongchun`, `airport`, `all`.
86
+
87
+ ์˜ˆ์•ฝ ๋‹จ๊ณ„(`reserve`)์—์„œ๋„ ๊ฐ™์€ `--train-type` ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋„˜๊ฒจ์•ผ stable `train_id` ๋งค์นญ์ด ๊นจ์ง€์ง€ ์•Š๋Š”๋‹ค.
88
+
89
+ ์ขŒ์„์ด ์—†๋Š” ์—ด์ฐจ๋„ ํ›„๋ณด์— ํฌํ•จํ•˜๋ ค๋ฉด `--include-no-seats`, ์˜ˆ์•ฝ ๋Œ€๊ธฐ ๊ฐ€๋Šฅํ•œ ์—ด์ฐจ๋„ ๊ฐ™์ด ๋ณด๊ณ  ์‹ถ์œผ๋ฉด `--include-waiting-list` ๋ฅผ ๋ถ™์ธ๋‹ค.
90
+
91
+ ### 3. Present the shortlist
92
+
93
+ ์˜ˆ๋งค ์ „์— ํ•ญ์ƒ ์•„๋ž˜๋ฅผ ํ™•์ธํ•œ๋‹ค.
94
+
95
+ - `index`
96
+ - `train_id`
97
+ - ์ถœ๋ฐœ/๋„์ฐฉ ์‹œ๊ฐ
98
+ - ์—ด์ฐจ ์ข…๋ฅ˜ (`train_type`)
99
+ - ์ผ๋ฐ˜์‹ค/ํŠน์‹ค ๊ฐ€๋Šฅ ์—ฌ๋ถ€
100
+ - ์˜ˆ์•ฝ ๋Œ€๊ธฐ ๊ฐ€๋Šฅ ์—ฌ๋ถ€
101
+
102
+ ### 4. Inspect detailed seats when the user asks for good seats
103
+
104
+ `search` ์˜ ์ขŒ์„ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋Š” ์—ด์ฐจ ๋‹จ์œ„ ํ”Œ๋ž˜๊ทธ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ "๋‚จ์€ ์ขŒ์„ ๋ฒˆํ˜ธ", "ํ˜ธ์ฐจ๋ณ„ ์ขŒ์„", "์ฝ˜์„ผํŠธ", "๊ฟ€ํŒ ์ขŒ์„", "์ฐฝ์ธก/์ˆœ๋ฐฉํ–ฅ ์ž๋ฆฌ", "์˜ˆ์•ฝ ์ „์— ์ž๋ฆฌ ํ™•์ธ"์ฒ˜๋Ÿผ ๊ตฌ์ฒด์ ์ธ ์ขŒ์„์„ ๋ฌผ์œผ๋ฉด ์˜ˆ์•ฝ ์ „์— `seats` ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
105
+
106
+ ๊ธฐ๋ณธ ์ƒ์„ธ ์ขŒ์„ ์กฐํšŒ:
107
+
108
+ ```bash
109
+ python3 scripts/ktx_booking.py seats ์„œ์šธ ๋ถ€์‚ฐ 20260328 090000 --train-id <train_id>
110
+ ```
111
+
112
+ ์ผ๋ฐ˜์‹ค/ํŠน์‹ค์€ `--room` ์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค.
113
+
114
+ ```bash
115
+ python3 scripts/ktx_booking.py seats ์„œ์šธ ๋ถ€์‚ฐ 20260328 090000 --train-id <train_id> --room special
116
+ ```
117
+
118
+ ๋‚จ์€ ์ขŒ์„๋ฒˆํ˜ธ๋งŒ ๋ณด๊ณ  ์‹ถ์œผ๋ฉด `--available-only` ๋ฅผ ์“ด๋‹ค.
119
+
120
+ ```bash
121
+ python3 scripts/ktx_booking.py seats ์„œ์šธ ๋ถ€์‚ฐ 20260328 090000 --train-id <train_id> --available-only
122
+ ```
123
+
124
+ ํŠน์ • ํ˜ธ์ฐจ๋ฅผ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด `seats` ๋Š” **5ํ˜ธ์ฐจ๋ฅผ ์ตœ์šฐ์„ **์œผ๋กœ ํƒ์ƒ‰ํ•œ๋‹ค. 5ํ˜ธ์ฐจ๊ฐ€ ์—†์œผ๋ฉด 5ํ˜ธ์ฐจ์™€์˜ ๊ฑฐ๋ฆฌ๊ฐ€ ๊ฐ€๊นŒ์šด ํ˜ธ์ฐจ ์ˆœ์œผ๋กœ, ๊ฐ™์€ ๊ฑฐ๋ฆฌ์—์„œ๋Š” ๋‚ฎ์€ ํ˜ธ์ฐจ ๋ฒˆํ˜ธ ์ˆœ์œผ๋กœ ํƒ์ƒ‰ํ•œ๋‹ค(์˜ˆ: 1~8ํ˜ธ์ฐจ ํŽธ์„ฑ์€ `5, 4, 6, 3, 7, 2, 8, 1`, 1~4ํ˜ธ์ฐจ ํŽธ์„ฑ์€ `4, 3, 2, 1`). ์ผ๋ฐ˜ KTX, KTX-์‚ฐ์ฒœ(๋ถ„๋ฅ˜ ์ฝ”๋“œ 07ยท10), KTX-์ฒญ๋ฃก ๋ชจ๋‘ ๊ฐ™์€ ๊ทœ์น™์„ ์ ์šฉํ•œ๋‹ค. ๊ฐ ํ˜ธ์ฐจ ์•ˆ์˜ ์ขŒ์„์€ ์ฝ˜์„ผํŠธ ํžŒํŠธ๊ฐ€ ์žˆ๋Š” ์ขŒ์„(`direct`, `adjacent`)์„ ๋จผ์ €, ๊ฐ™์€ ์กฐ๊ฑด์—์„œ๋Š” ์ˆœ๋ฐฉํ–ฅ ์ขŒ์„์„ ๋จผ์ € ๋ณด์—ฌ์ค€๋‹ค.
125
+
126
+ ํŠน์ • ํ˜ธ์ฐจ๋งŒ ํ™•์ธํ•˜๋ ค๋ฉด `--car-no` ๋ฅผ ์“ด๋‹ค.
127
+
128
+ ```bash
129
+ python3 scripts/ktx_booking.py seats ์„œ์šธ ๋ถ€์‚ฐ 20260328 090000 --train-id <train_id> --car-no 5 --available-only
130
+ ```
131
+
132
+ ์ฝ˜์„ผํŠธ ๊ฟ€ํŒ ์ž๋ฆฌ๋ถ€ํ„ฐ ํ™•์ธํ•˜๋ ค๋ฉด `--power-only` ๋ฅผ ๋ถ™์ธ๋‹ค. ์‘๋‹ต์˜ `power_outlet` ์€ `direct`, `adjacent`, `none` ์ค‘ ํ•˜๋‚˜๋‹ค.
133
+
134
+ ```bash
135
+ python3 scripts/ktx_booking.py seats ์„œ์šธ ๋ถ€์‚ฐ 20260328 090000 --train-id <train_id> --available-only --power-only
136
+ ```
137
+
138
+ `seats` ๋„ `search` ์™€ ๊ฐ™์€ `--train-type` ์„ ๋„˜๊ฒจ์•ผ ํ•œ๋‹ค. ITX-์ฒญ์ถ˜ ๋“ฑ KTX ์™ธ ์—ด์ฐจ๋ฅผ ์กฐํšŒํ–ˆ๋‹ค๋ฉด ์ƒ์„ธ ์ขŒ์„ ์กฐํšŒ์—๋„ ๊ฐ™์€ ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.
139
+
140
+ ```bash
141
+ python3 scripts/ktx_booking.py seats ๋‚จ์ถ˜์ฒœ ์šฉ์‚ฐ 20260503 150000 \
142
+ --train-id <train_id> \
143
+ --train-type itx-cheongchun \
144
+ --available-only
145
+ ```
146
+
147
+ ์ƒ์„ธ ์ขŒ์„ ์‘๋‹ต์„ ๋ณด์—ฌ์ค„ ๋•Œ๋Š” ์‚ฌ์šฉ์ž ์˜๋„์— ๋งž์ถฐ ์•„๋ž˜๋ฅผ ์šฐ์„  ์š”์•ฝํ•œ๋‹ค.
148
+
149
+ - ํ˜ธ์ฐจ๋ณ„ `remaining_seats`, `available_seat_count`
150
+ - ๋‚จ์€ ์ขŒ์„ ๋ฒˆํ˜ธ (`available_seats`)
151
+ - ์ขŒ์„๋ณ„ `direction`, `position`, `seat_type`
152
+ - ์ฝ˜์„ผํŠธ ํžŒํŠธ (`power_outlet`)
153
+ - ๋ฌธ ๊ทผ์ฒ˜ ์—ฌ๋ถ€ (`near_door`)
154
+
155
+ ์ด ๊ธฐ๋Šฅ์€ ์ขŒ์„์„ ์„ ํƒ/์„ ์ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์‹ค์ œ ์˜ˆ์•ฝ์€ ๋‹ค์Œ ๋‹จ๊ณ„์˜ `reserve` ๋กœ๋งŒ ์ง„ํ–‰ํ•œ๋‹ค.
156
+
157
+ ### 5. Reserve only after the target train is unambiguous
158
+
159
+ ์กฐํšŒ ๊ฒฐ๊ณผ์˜ `train_id` ๋ฅผ ๊ณ ๋ฅธ ๋’ค์—๋งŒ ์˜ˆ์•ฝํ•œ๋‹ค. ์ด ๊ฐ’์€ helper ๊ฐ€ ์—ด์ฐจ ๋ฒˆํ˜ธ/์šดํ–‰์ผ/์‹œ๊ฐ/์—ญ ์ฝ”๋“œ๋ฅผ ๋ฌถ์–ด ๋งŒ๋“  stable selector ์ด๋ฏ€๋กœ, ์žฌ์กฐํšŒ ์‹œ ๊ฐ™์€ ์—ด์ฐจ๊ฐ€ ์•„์ง ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์žก๊ณ  ์—†์œผ๋ฉด ์‹คํŒจํ•œ๋‹ค.
160
+
161
+ ```bash
162
+ python3 scripts/ktx_booking.py reserve ์„œ์šธ ๋ถ€์‚ฐ 20260328 090000 --train-id <train_id> --seat-option general-first
163
+ ```
164
+
165
+ ITX ๋“ฑ KTX ์™ธ ๋…ธ์„ ์„ search ๋‹จ๊ณ„์—์„œ ๊ณจ๋ž๋‹ค๋ฉด reserve ์—๋„ ๋˜‘๊ฐ™์ด `--train-type` ์„ ๋„˜๊ธด๋‹ค.
166
+
167
+ ```bash
168
+ python3 scripts/ktx_booking.py reserve ๋‚จ์ถ˜์ฒœ ์šฉ์‚ฐ 20260503 150000 --train-id <train_id> --train-type itx-cheongchun --seat-option general-first
169
+ ```
170
+
171
+ ์‘๋‹ต์—๋Š” ์˜ˆ์•ฝ๋ฒˆํ˜ธ, ์šด์ž„, ๊ตฌ์ž…๊ธฐํ•œ์ด ํฌํ•จ๋œ๋‹ค. ์ด ์‹œ์ ์— **์ขŒ์„ ํ™•๋ณด๋Š” ์™„๋ฃŒ๋˜์—ˆ๋‹ค**๊ณ  ์•ˆ๋‚ดํ•œ๋‹ค. generic fallback์—์„œ๋Š” ๊ฒฐ์ œ๋ฅผ handoffํ•˜๊ณ , ๋Œ์‡ ์—์„œ๋Š” ์•„๋ž˜ ๊ฒฐ์ œ ๋‹จ๊ณ„๋กœ ๊ณ„์† ์ง„ํ–‰ํ•œ๋‹ค.
172
+ ์ขŒ์„์ด ์—†์„ ๋•Œ๋Š” ์กฐํšŒ ๋‹จ๊ณ„์—์„œ `--include-waiting-list` ๋ฅผ ์ผœ๊ณ  ์˜ˆ์•ฝ ๋‹จ๊ณ„์—์„œ `--try-waiting` ์œผ๋กœ ์˜ˆ์•ฝ ๋Œ€๊ธฐ๊นŒ์ง€ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค.
173
+
174
+ ### 5-1. N-card discounted reservation
175
+
176
+ N์นด๋“œ ํ• ์ธ์„ ์ ์šฉํ•˜๋ ค๋ฉด ๋จผ์ € ๋ณด์œ  N์นด๋“œ ๋ชฉ๋ก์„ ์กฐํšŒํ•ด ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•œ๋‹ค.
177
+
178
+ ```bash
179
+ python3 scripts/ktx_booking.py ncard-list
180
+ ```
181
+
182
+ N์นด๋“œ๋กœ ํ• ์ธ ์—ด์ฐจ๋ฅผ ์กฐํšŒํ•œ๋‹ค (`--ncard-index` ๋Š” `ncard-list` ๊ฒฐ๊ณผ์˜ ์ˆœ๋ฒˆ). `ncard-list` ๋Š” ๋กœ๊ทธ/์…ธ ๋…ธ์ถœ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ๋งˆ์Šคํ‚นํ•ด ์ถœ๋ ฅํ•œ๋‹ค.
183
+
184
+ ```bash
185
+ python3 scripts/ktx_booking.py ncard-search ๋Œ€์ „ ์„œ์šธ 20260512 100000 --ncard-index 1 --train-type ktx
186
+ ```
187
+
188
+ ์‘๋‹ต์˜ `train_id` ๋ฅผ ๋ณต์‚ฌํ•ด `reserve` ์— ๊ฐ™์€ `--ncard-index` ๋ฅผ ๋ถ™์—ฌ ์˜ˆ์•ฝํ•œ๋‹ค.
189
+
190
+ ```bash
191
+ python3 scripts/ktx_booking.py reserve ๋Œ€์ „ ์„œ์šธ 20260512 100000 \
192
+ --train-id <train_id> \
193
+ --ncard-index 1
194
+ ```
195
+
196
+ `--ncard-index` ๋ฅผ ์ง€์ •ํ•˜๋ฉด `--adults` ๋“ฑ ์Šน๊ฐ ์˜ต์…˜์€ ๋ฌด์‹œ๋˜๊ณ  N์นด๋“œ ์Šน๊ฐ 1๋ช…์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค. `--ncard-no` ์ง์ ‘ ์ž…๋ ฅ๋„ ์ง€์›ํ•˜์ง€๋งŒ ์…ธ ํžˆ์Šคํ† ๋ฆฌ์— ๋‚จ์„ ์ˆ˜ ์žˆ์–ด ๊ถŒ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. generic fallback์—์„œ๋Š” ๊ฒฐ์ œ๋ฅผ handoffํ•˜๊ณ , ๋Œ์‡ ์—์„œ๋Š” ์•„๋ž˜ ๊ฒฐ์ œ ๋‹จ๊ณ„๋กœ ๊ณ„์† ์ง„ํ–‰ํ•œ๋‹ค.
197
+
198
+ N์นด๋“œ ๊ธฐ๋Šฅ์€ `korail2-ncard` ํŒจํ‚ค์ง€๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์—†์œผ๋ฉด ํ•ด๋‹น ์ปค๋งจ๋“œ ์‹คํ–‰ ์‹œ ์„ค์น˜ ์•ˆ๋‚ด๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค.
199
+
200
+ ### 6. Continue to payment in Dolshoi
201
+
202
+ ๋Œ์‡ ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ˆ๋งค ์™„๋ฃŒ๋ฅผ ์š”์ฒญํ–ˆ๋‹ค๋ฉด ์˜ˆ์•ฝ ์„ฑ๊ณต์—์„œ ๋ฉˆ์ถ”์ง€ ์•Š๋Š”๋‹ค.
203
+
204
+ 1. CloakBrowser์˜ ๊ณต์‹ Korail ์˜ˆ์•ฝ/๊ฒฐ์ œ ํ™”๋ฉด์—์„œ ๋ฐฉ๊ธˆ ๋งŒ๋“  ์˜ˆ์•ฝ๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ์‹๋ณ„ํ•œ๋‹ค.
205
+ 2. vault-backed login์„ ์‚ฌ์šฉํ•˜๊ณ  N์นด๋“œ/ํ• ์ธ, ์Šน๊ฐ, ์ขŒ์„, ๊ฒฐ์ œ์ˆ˜๋‹จ์„ ํ™•์ธํ•œ๋‹ค.
206
+ 3. ์‹ค์ œ ๊ฒฐ์ œ ๋ฒ„ํŠผ ์ง์ „์— `clarify`๋กœ ์—ด์ฐจ, ๋‚ ์งœยท์‹œ๊ฐ, ์Šน๊ฐ, ์ขŒ์„ ๋“ฑ๊ธ‰, ํ• ์ธ, ์ด์•ก์„ ๋ณด์—ฌ์ฃผ๊ณ  ์Šน์ธ๋ฐ›๋Š”๋‹ค.
207
+ 4. ์Šน์ธ๋˜๋ฉด ๊ฒฐ์ œ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๊ฒฐ์ œ ์™„๋ฃŒ ํ™”๋ฉด, ์˜ˆ์•ฝ๋ฒˆํ˜ธ, ์˜์ˆ˜์ฆ/๊ฒฐ์ œ ์ƒํƒœ๋ฅผ ํ™•์ธํ•œ๋‹ค.
208
+
209
+ ๋Œ์‡ ๊ฐ€ ์•„๋‹ˆ๊ฑฐ๋‚˜ ๊ณต์‹ ๊ฒฐ์ œ ํ‘œ๋ฉด์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฉด ์˜ˆ์•ฝ๋ฒˆํ˜ธ์™€ ๊ตฌ์ž…๊ธฐํ•œ์„ ์ œ๊ณตํ•˜๊ณ  generic handoff๋กœ ์ข…๋ฃŒํ•œ๋‹ค.
210
+
211
+ ### 7. Inspect or cancel
212
+
213
+ ์ทจ์†Œ๋Š” ๋Œ€์ƒ ์˜ˆ์•ฝ์„ ๋‹ค์‹œ ์กฐํšŒํ•ด ์‹๋ณ„ํ•œ ๋’ค์—๋งŒ ์ง„ํ–‰ํ•œ๋‹ค.
214
+
215
+ ```bash
216
+ python3 scripts/ktx_booking.py reservations
217
+ ```
218
+
219
+ ์ทจ์†Œ ์‹คํ–‰ ์ง์ „์— `clarify`๋กœ ์˜ˆ์•ฝ๋ฒˆํ˜ธ, ์—ด์ฐจ, ๋‚ ์งœยท์‹œ๊ฐ, ์Šน๊ฐ, ํ™˜๋ถˆ/์œ„์•ฝ๊ธˆ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๊ณ  ์Šน์ธ๋ฐ›๋Š”๋‹ค.
220
+
221
+ ```bash
222
+ python3 scripts/ktx_booking.py cancel <reservation_id>
223
+ ```
224
+
225
+ ## Done when
226
+
227
+ - ์กฐํšŒ๋ฉด ์—ด์ฐจ ํ›„๋ณด๊ฐ€ ์ •๋ฆฌ๋˜์–ด ์žˆ๋‹ค
228
+ - ์ขŒ์„ ์ƒ์„ธ ํ™•์ธ์ด๋ฉด ํ˜ธ์ฐจ๋ณ„ ๋‚จ์€ ์ขŒ์„๋ฒˆํ˜ธ์™€ ํ•„์š”ํ•œ ๊ฟ€ํŒ ์กฐ๊ฑด์ด ์ •๋ฆฌ๋˜์–ด ์žˆ๋‹ค
229
+ - ์˜ˆ์•ฝ์ด๋ฉด ์˜ˆ์•ฝ ๊ฒฐ๊ณผ์™€ ์ œํ•œ ์‹œ๊ฐ„์ด ํ™•์ธ๋˜์–ด ์ขŒ์„ ํ™•๋ณด ์™„๋ฃŒ๋ฅผ ์•ˆ๋‚ดํ–ˆ๋‹ค
230
+ - ๋Œ์‡ ์˜ ์˜ˆ๋งค ์™„๋ฃŒ ์š”์ฒญ์ด๋ฉด `clarify` ์Šน์ธ ํ›„ ๊ฒฐ์ œ ์™„๋ฃŒ ์ƒํƒœ์™€ ์˜์ˆ˜์ฆ/์˜ˆ์•ฝ๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ–ˆ๋‹ค
231
+ - ์ทจ์†Œ๋ฉด ์–ด๋–ค ์˜ˆ์•ฝ์„ ์ทจ์†Œํ–ˆ๋Š”์ง€ ๋‚จ์•„ ์žˆ๋‹ค
232
+
233
+ ## Failure modes
234
+
235
+ - ๋กœ๊ทธ์ธ ์‹คํŒจ
236
+ - ๋งค์ง„
237
+ - Korail anti-bot ๊ทœ์น™ ๋ณ€๊ฒฝ
238
+
239
+ ## Notes
240
+
241
+ - `scripts/ktx_booking.py` ๋Š” upstream `korail2` anti-bot ํšŒ๊ท€๋ฅผ ๋ณด์™„ํ•˜๋Š” helper ๋‹ค
242
+ - `korail2` ๋Š” KTX/Korail ์ „์šฉ ํ‘œ๋ฉด์ด๋ผ train type ๊ณผ passenger model ์ด ๋ถ„๋ช…ํ•˜๋‹ค
243
+ - ๊ฒฐ์ œ ์ž๋™ํ™” ๊ธˆ์ง€๋Š” generic fallback์—๋งŒ ์ ์šฉํ•œ๋‹ค. ๋Œ์‡ ์—์„œ๋Š” `clarify` ์Šน์ธ ํ›„ ๊ณต์‹ ๊ฒฐ์ œ ํ‘œ๋ฉด์œผ๋กœ ์™„๋ฃŒํ•œ๋‹ค
244
+ - aggressive polling ์€ ํ”ผํ•œ๋‹ค