@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,381 @@
1
+ #!/usr/bin/env python3
2
+ """Search and optionally hold Tmoney intercity-bus seats through official flows.
3
+
4
+ Default mode is read-only timetable parsing. With --hold-seat, the helper performs
5
+ Tmoney's temporary seat-hold POST and saves the official card-information page.
6
+ It never submits card fields or final payment.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import html
12
+ import http.cookiejar
13
+ import json
14
+ import re
15
+ import ssl
16
+ import sys
17
+ import tempfile
18
+ import urllib.parse
19
+ import urllib.request
20
+ from dataclasses import asdict, dataclass
21
+ from pathlib import Path
22
+ from typing import Iterable
23
+
24
+ BASE_URL = "https://intercitybus.tmoney.co.kr"
25
+ ENTRY_PATH = "/otck/trmlInfEnty.do"
26
+ TIMETABLE_PATH = "/otck/readAlcnList.do"
27
+ SEAT_STAGE_PATH = "/otck/readSatsFee.do"
28
+ HOLD_PATH = "/otck/readPcpySats.do"
29
+ DEFAULT_UA = (
30
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
31
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125 Safari/537.36"
32
+ )
33
+
34
+ ROW_RE = re.compile(r"<tr>\s*(.*?)readSasFeeInf\((.*?)\).*?</tr>", re.DOTALL | re.IGNORECASE)
35
+ TD_WRAP_RE = re.compile(r'<div class="td_wrap1">(.*?)</div>', re.DOTALL | re.IGNORECASE)
36
+ TAG_RE = re.compile(r"<[^>]+>")
37
+ ARG_RE = re.compile(r"'((?:\\'|[^'])*)'")
38
+ FORM_RE = re.compile(r"<form\b([^>]*)>(.*?)</form>", re.DOTALL | re.IGNORECASE)
39
+ INPUT_RE = re.compile(r"<input\b([^>]+)>", re.DOTALL | re.IGNORECASE)
40
+ ATTR_RE = re.compile(r"([\w:-]+)=[\"']([^\"']*)[\"']")
41
+ SEAT_RE = re.compile(r"<li([^>]*)>\s*<a[^>]*>.*?<span>(\d+)</span>", re.DOTALL | re.IGNORECASE)
42
+
43
+
44
+ @dataclass
45
+ class Schedule:
46
+ departure_time: str | None
47
+ company: str | None
48
+ duration: str | None
49
+ bus_class: str | None
50
+ adult_fare: str | None
51
+ child_fare: str | None
52
+ student_fare: str | None
53
+ remaining_seats: int | None
54
+ total_seats: int | None
55
+ raw_args: list[str]
56
+
57
+
58
+ @dataclass
59
+ class HoldResult:
60
+ success: bool
61
+ hold_id: str | None
62
+ seat: str
63
+ card_page_path: str | None
64
+ cancel_fields_path: str | None
65
+ markers: dict[str, int]
66
+ failure_message: str | None = None
67
+
68
+
69
+ def _ssl_context() -> ssl.SSLContext:
70
+ # Tmoney has historically required curl -k in probes on some machines.
71
+ # Keep this helper resilient while limiting it to the official host.
72
+ return ssl._create_unverified_context() # noqa: SLF001
73
+
74
+
75
+ def _strip(value: str) -> str:
76
+ value = re.sub(r"<!--.*?-->", "", value, flags=re.DOTALL)
77
+ value = TAG_RE.sub("", value)
78
+ return html.unescape(value).replace("\xa0", " ").strip()
79
+
80
+
81
+ def _attrs(fragment: str) -> dict[str, str]:
82
+ return {k.lower(): html.unescape(v) for k, v in ATTR_RE.findall(fragment)}
83
+
84
+
85
+ def _open(opener: urllib.request.OpenerDirector, request: urllib.request.Request, timeout: int) -> str:
86
+ with opener.open(request, timeout=timeout) as response:
87
+ charset = response.headers.get_content_charset() or "utf-8"
88
+ return response.read().decode(charset, errors="replace")
89
+
90
+
91
+ def build_opener() -> urllib.request.OpenerDirector:
92
+ jar = http.cookiejar.CookieJar()
93
+ return urllib.request.build_opener(
94
+ urllib.request.HTTPCookieProcessor(jar),
95
+ urllib.request.HTTPSHandler(context=_ssl_context()),
96
+ )
97
+
98
+
99
+ def _request(url: str, data: list[tuple[str, str]] | dict[str, str] | None = None, referer: str | None = None) -> urllib.request.Request:
100
+ headers = {"User-Agent": DEFAULT_UA}
101
+ if referer:
102
+ headers["Referer"] = referer
103
+ if data is None:
104
+ return urllib.request.Request(url, headers=headers, method="GET")
105
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
106
+ encoded = urllib.parse.urlencode(data).encode("utf-8")
107
+ return urllib.request.Request(url, data=encoded, headers=headers, method="POST")
108
+
109
+
110
+ def search_timetable(
111
+ depart_code: str,
112
+ arrive_code: str,
113
+ depart_name: str,
114
+ arrive_name: str,
115
+ date: str,
116
+ time: str = "000000",
117
+ adults: int = 1,
118
+ students: int = 0,
119
+ children: int = 0,
120
+ veterans: int = 0,
121
+ timeout: int = 20,
122
+ opener: urllib.request.OpenerDirector | None = None,
123
+ ) -> tuple[urllib.request.OpenerDirector, str, list[Schedule]]:
124
+ opener = opener or build_opener()
125
+ _open(opener, _request(f"{BASE_URL}{ENTRY_PATH}"), timeout)
126
+
127
+ fields = {
128
+ "depr_Trml_Cd": depart_code,
129
+ "arvl_Trml_Cd": arrive_code,
130
+ "depr_Trml_Nm": depart_name,
131
+ "arvl_Trml_Nm": arrive_name,
132
+ "ig": str(adults),
133
+ "im": str(students),
134
+ "ic": str(children),
135
+ "iv": str(veterans),
136
+ "depr_Dt": date,
137
+ "depr_Time": time,
138
+ # Required by the browser JS readAlcnListEntry(). Missing either field
139
+ # returns a generic error page with no schedule rows.
140
+ "bef_Aft_Dvs": "D",
141
+ "req_Rec_Num": "10",
142
+ }
143
+ body = _open(opener, _request(f"{BASE_URL}{TIMETABLE_PATH}", fields, f"{BASE_URL}{ENTRY_PATH}"), timeout)
144
+ return opener, body, parse_schedules(body)
145
+
146
+
147
+ def parse_schedules(body: str) -> list[Schedule]:
148
+ schedules: list[Schedule] = []
149
+ for row_html, arg_text in ROW_RE.findall(body):
150
+ args = [a.replace("\\'", "'") for a in ARG_RE.findall(arg_text)]
151
+ cells = [_strip(x) for x in TD_WRAP_RE.findall(row_html)]
152
+ departure = cells[0] if len(cells) > 0 else (args[8][:2] + ":" + args[8][2:4] if len(args) > 8 else None)
153
+ company_cell = cells[1] if len(cells) > 1 else None
154
+ company = args[11] if len(args) > 11 else None
155
+ duration = None
156
+ if company_cell and company and company_cell.startswith(company):
157
+ duration = company_cell[len(company):].strip() or None
158
+ elif company_cell:
159
+ duration = company_cell
160
+ bus_class = args[12] if len(args) > 12 else (cells[2] if len(cells) > 2 else None)
161
+ remaining = int(args[16]) if len(args) > 16 and args[16].isdigit() else None
162
+ total = int(args[17]) if len(args) > 17 and args[17].isdigit() else None
163
+ schedules.append(
164
+ Schedule(
165
+ departure_time=departure,
166
+ company=company,
167
+ duration=duration,
168
+ bus_class=bus_class,
169
+ adult_fare=cells[3] if len(cells) > 3 else None,
170
+ child_fare=cells[4] if len(cells) > 4 else None,
171
+ student_fare=cells[5] if len(cells) > 5 else None,
172
+ remaining_seats=remaining,
173
+ total_seats=total,
174
+ raw_args=args,
175
+ )
176
+ )
177
+ return schedules
178
+
179
+
180
+ def _seat_stage_fields(schedule: Schedule, search_time: str) -> dict[str, str]:
181
+ a = schedule.raw_args
182
+ if len(a) < 21:
183
+ raise ValueError("schedule raw_args does not contain the expected readSasFeeInf payload")
184
+ return {
185
+ "atl_Depr_Dt_S1": a[2],
186
+ "atl_Depr_Time_S1": search_time,
187
+ "rot_Id": a[0],
188
+ "rot_Sqno": a[1],
189
+ "alcn_Dt": a[2],
190
+ "alcn_Sqno": a[3],
191
+ "depr_Trml_Cd": a[4],
192
+ "arvl_Trml_Cd": a[5],
193
+ "depr_Trml_Nm": a[6],
194
+ "arvl_Trml_Nm": a[7],
195
+ "depr_Time": a[8],
196
+ "bus_Cacm_Cd": a[9],
197
+ "bus_Cls_Cd": a[10],
198
+ "bus_Cacm_Nm": a[11],
199
+ "bus_Cls_Nm": a[12],
200
+ "ig": a[13],
201
+ "im": a[14],
202
+ "ic": a[15],
203
+ "rmn_Scnt": a[16],
204
+ "sats_Num": a[17],
205
+ "atl_Depr_Dt": a[18],
206
+ "atl_Depr_Time": a[19],
207
+ "dc_Psb_Yn": a[20],
208
+ }
209
+
210
+
211
+ def _form_fields(body: str, form_id: str) -> list[tuple[str, str]]:
212
+ for attrs_text, form_body in FORM_RE.findall(body):
213
+ attrs = _attrs(attrs_text)
214
+ if attrs.get("id") == form_id or attrs.get("name") == form_id:
215
+ fields: list[tuple[str, str]] = []
216
+ for input_text in INPUT_RE.findall(form_body):
217
+ input_attrs = _attrs(input_text)
218
+ name = input_attrs.get("name")
219
+ if name:
220
+ fields.append((name, input_attrs.get("value", "")))
221
+ return fields
222
+ return []
223
+
224
+
225
+ def _available_seats(seat_stage_body: str) -> list[str]:
226
+ seats: list[str] = []
227
+ for li_attrs, seat_no in SEAT_RE.findall(seat_stage_body):
228
+ classes = _attrs(li_attrs).get("class", "")
229
+ if "disabled" not in classes.split():
230
+ seats.append(seat_no)
231
+ return seats
232
+
233
+
234
+ def hold_seat(
235
+ opener: urllib.request.OpenerDirector,
236
+ schedule: Schedule,
237
+ search_time: str,
238
+ seat: str | None,
239
+ output_dir: Path,
240
+ timeout: int = 20,
241
+ ) -> tuple[str, list[str], HoldResult]:
242
+ seat_stage_body = _open(
243
+ opener,
244
+ _request(f"{BASE_URL}{SEAT_STAGE_PATH}", _seat_stage_fields(schedule, search_time), f"{BASE_URL}{TIMETABLE_PATH}"),
245
+ timeout,
246
+ )
247
+ available = _available_seats(seat_stage_body)
248
+ selected = seat or (available[0] if available else "")
249
+ if not selected:
250
+ return seat_stage_body, available, HoldResult(False, None, "", None, None, {}, "No selectable seat was found")
251
+ if selected not in available:
252
+ return seat_stage_body, available, HoldResult(False, None, selected, None, None, {}, f"Seat {selected} is not selectable")
253
+
254
+ fields = _form_fields(seat_stage_body, "readPcpySats")
255
+ if not fields:
256
+ return seat_stage_body, available, HoldResult(False, None, selected, None, None, {}, "No readPcpySats form found")
257
+
258
+ # Mirror pcpySats() in /js/tckmrs/readSatsInfo.js for a normal adult-only hold.
259
+ field_map = dict(fields)
260
+ fields.extend(
261
+ [
262
+ ("pcpy_Num", "1"),
263
+ ("sats_No", selected),
264
+ ("rtrp_Depr_Dt", ""),
265
+ ("bus_Tck_Knd_Cd", field_map.get("ig_Knd_Cd", "IG00")),
266
+ ("cty_Bus_Dc_Knd_Cd", "Z"),
267
+ ("dcrt_Dvs_Cd", "0"),
268
+ ]
269
+ )
270
+ hold_body = _open(opener, _request(f"{BASE_URL}{HOLD_PATH}", fields, f"{BASE_URL}{SEAT_STAGE_PATH}"), timeout)
271
+ markers = {k: hold_body.count(k) for k in ["카드정보 입력", "sats_Pcpy_Id", "이미 발매된 좌석", "발행을 실패", "errorCont"]}
272
+ hold_ids = re.findall(r'name=["\']sats_Pcpy_Id["\'][^>]*value=["\']([^"\']+)', hold_body)
273
+ success = bool(hold_ids and markers["카드정보 입력"] and not markers["errorCont"])
274
+
275
+ output_dir.mkdir(parents=True, exist_ok=True)
276
+ card_path = output_dir / "tmoney-intercity-card-info.html"
277
+ card_path.write_text(hold_body)
278
+ cancel_fields = _form_fields(hold_body, "alcnInfo") or _form_fields(hold_body, "onwayInfo")
279
+ cancel_path = output_dir / "tmoney-intercity-cancel-fields.txt"
280
+ if cancel_fields:
281
+ cancel_path.write_text("\n".join(f"{k}={v}" for k, v in cancel_fields))
282
+ else:
283
+ cancel_path = None # type: ignore[assignment]
284
+
285
+ failure = None if success else _strip(hold_body[hold_body.find("[처리결과]") : hold_body.find("[처리결과]") + 500]) or "Hold did not reach card-information page"
286
+ return seat_stage_body, available, HoldResult(
287
+ success=success,
288
+ hold_id=hold_ids[0] if hold_ids else None,
289
+ seat=selected,
290
+ card_page_path=str(card_path),
291
+ cancel_fields_path=str(cancel_path) if cancel_path else None,
292
+ markers=markers,
293
+ failure_message=failure,
294
+ )
295
+
296
+
297
+ def main(argv: Iterable[str] | None = None) -> int:
298
+ parser = argparse.ArgumentParser(description="Search Tmoney intercity-bus timetable and optionally create a temporary seat hold")
299
+ parser.add_argument("--depart-code", required=True)
300
+ parser.add_argument("--arrive-code", required=True)
301
+ parser.add_argument("--depart-name", required=True)
302
+ parser.add_argument("--arrive-name", required=True)
303
+ parser.add_argument("--date", required=True, help="YYYYMMDD")
304
+ parser.add_argument("--time", default="000000", help="HHMMSS, default 000000")
305
+ parser.add_argument("--adults", type=int, default=1)
306
+ parser.add_argument("--students", type=int, default=0)
307
+ parser.add_argument("--children", type=int, default=0)
308
+ parser.add_argument("--veterans", type=int, default=0)
309
+ parser.add_argument("--timeout", type=int, default=20)
310
+ parser.add_argument("--limit", type=int, default=20)
311
+ parser.add_argument("--select-index", type=int, default=1, help="1-based schedule index for --hold-seat")
312
+ parser.add_argument("--hold-seat", help="Temporarily hold this seat number and save the official card-info page")
313
+ parser.add_argument("--hold-first-seat", action="store_true", help="Hold the first selectable seat for the selected schedule")
314
+ parser.add_argument("--output-dir", help="Directory for saved hold/card page files; defaults to a temp directory")
315
+ args = parser.parse_args(argv)
316
+
317
+ if not re.fullmatch(r"\d{8}", args.date):
318
+ parser.error("--date must be YYYYMMDD")
319
+ if not re.fullmatch(r"\d{6}", args.time):
320
+ parser.error("--time must be HHMMSS")
321
+ if args.students or args.children or args.veterans:
322
+ parser.error("seat holding currently supports adult-only payloads; use search mode for mixed passenger counts")
323
+ if args.select_index < 1:
324
+ parser.error("--select-index must be 1 or greater")
325
+
326
+ opener, body, schedules = search_timetable(
327
+ depart_code=args.depart_code,
328
+ arrive_code=args.arrive_code,
329
+ depart_name=args.depart_name,
330
+ arrive_name=args.arrive_name,
331
+ date=args.date,
332
+ time=args.time,
333
+ adults=args.adults,
334
+ students=args.students,
335
+ children=args.children,
336
+ veterans=args.veterans,
337
+ timeout=args.timeout,
338
+ )
339
+ result: dict[str, object] = {
340
+ "route": {
341
+ "depart_code": args.depart_code,
342
+ "arrive_code": args.arrive_code,
343
+ "depart_name": args.depart_name,
344
+ "arrive_name": args.arrive_name,
345
+ "date": args.date,
346
+ "time": args.time,
347
+ },
348
+ "count": len(schedules),
349
+ "items": [asdict(s) for s in schedules[: args.limit]],
350
+ "failure_mode": None,
351
+ }
352
+ if not schedules:
353
+ result["failure_mode"] = (
354
+ "No readSasFeeInf schedule rows found. Check terminal codes/date, sold-out/no-service state, "
355
+ "or whether Tmoney returned its generic error page."
356
+ )
357
+ result["error_page_marker_count"] = body.count("errorCont")
358
+ print(json.dumps(result, ensure_ascii=False, indent=2))
359
+ return 2
360
+
361
+ if args.hold_seat or args.hold_first_seat:
362
+ if args.select_index > len(schedules):
363
+ parser.error(f"--select-index {args.select_index} exceeds schedule count {len(schedules)}")
364
+ output_dir = Path(args.output_dir) if args.output_dir else Path(tempfile.mkdtemp(prefix="tmoney-intercity-hold-"))
365
+ _, available, hold = hold_seat(opener, schedules[args.select_index - 1], args.time, args.hold_seat, output_dir, args.timeout)
366
+ result["selected_schedule"] = asdict(schedules[args.select_index - 1])
367
+ result["available_seats"] = available
368
+ result["hold"] = asdict(hold)
369
+ result["payment_window_note"] = (
370
+ "The live card-information page did not expose an exact countdown/expiry text in probes. "
371
+ "Treat the hold as short-lived and complete payment immediately; use the saved cancel fields to release abandoned holds."
372
+ )
373
+ print(json.dumps(result, ensure_ascii=False, indent=2))
374
+ return 0 if hold.success else 3
375
+
376
+ print(json.dumps(result, ensure_ascii=False, indent=2))
377
+ return 0
378
+
379
+
380
+ if __name__ == "__main__":
381
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "intercity-bus-booking",
3
+ "description": "Search and assist Korean 시외버스/Tmoney bookings using official HTTP/API-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
4
+ "profiles": [
5
+ "action:booking"
6
+ ],
7
+ "frontmatter": "name: intercity-bus-booking\ndescription: Search and assist Korean 시외버스/Tmoney bookings using official HTTP/API-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: travel\n locale: ko-KR\n phase: v1"
8
+ }
@@ -0,0 +1,229 @@
1
+ # 인터넷등기소 등기부등본 자동화
2
+
3
+ ## What this skill does
4
+
5
+ 대법원 인터넷등기소(IROS, `https://www.iros.go.kr`)에서 **법인/부동산 등기부등본(등기사항증명서)** 을 여러 건 발급해야 할 때, 원 저작자 `challengekim`의 참고 구현 [`challengekim/iros-registry-automation`](https://github.com/challengekim/iros-registry-automation) (MIT)을 기준으로 안전한 작업 순서와 로컬 실행 방식을 안내한다.
6
+
7
+ - 법인등기부등본: 법인등록번호 또는 상호명으로 장바구니에 담고, 사용자가 직접 결제한 뒤 열람·저장한다.
8
+ - 부동산등기부등본: 주소/동호수 목록으로 장바구니에 담되, 결제·열람·다운로드는 인터넷등기소 웹 UI에서 수동으로 처리하는 것을 기본 권장한다.
9
+ - TouchEn nxKey, 공동인증서/간편인증, 카드 결제처럼 사용자 PC·인증수단이 필요한 준비사항을 체크한다.
10
+ - 발급 파일과 입력 목록에 들어가는 개인정보/민감정보를 저장소 밖에서 다루도록 안내한다.
11
+
12
+ ## Hard limits
13
+
14
+ - **로그인은 사용자가 브라우저에서 직접 한다.** ID/PW, 공동인증서 비밀번호, 간편인증, OTP, 보안카드, 카드번호를 에이전트가 입력·저장하지 않는다.
15
+ - **결제는 사용자가 직접 한다.** 카드 승인, 결제 확인, 결제 실패 대응은 사람이 브라우저에서 처리한다.
16
+ - 법률 자문, 권리관계 해석, 제출/발급 결과의 법적 유효성 보장은 하지 않는다. 이 스킬은 참고용 자동화 가이드다.
17
+ - IROS 보안 프로그램(TouchEn nxKey 등)이 요구되면 먼저 설치하고 브라우저/PC를 재시작한 뒤 다시 시작한다.
18
+ - 법인 결제는 upstream 문서 기준 **페이지당 10건** 단위 제약을 전제로 안내한다. 그 이상은 사용자가 10건 단위로 반복 결제한다.
19
+ - 부동산은 인터넷등기소가 로그인 상태에서 10만원 미만 일괄 결제와 일괄열람출력/일괄저장 UI를 제공하므로, v1에서는 장바구니 반복 작업만 자동화 가치가 큰 영역으로 본다.
20
+
21
+ ## Prerequisites
22
+
23
+ - Chrome/Chromium 실행 가능한 환경
24
+ - Python 3.10+
25
+ - Playwright / Chromium 설치 가능 환경
26
+ - IROS 로그인 수단(아이디, 공동인증서, 간편인증 등)
27
+ - 결제 카드
28
+ - TouchEn nxKey 사전 설치
29
+ - upstream 참고 구현 clone 또는 사용자가 관리하는 로컬 사본. 실행 전 반드시 이 스킬 저장소의 `iros-registry-automation/scripts/upstream.pin`에 적힌 reviewed SHA로 고정한다.
30
+
31
+ ```bash
32
+ git clone https://github.com/challengekim/iros-registry-automation.git
33
+ cd iros-registry-automation
34
+ git checkout 7c6924b2ff88d693a12556659188cb91041e5097
35
+ python3 -m venv .venv
36
+ source .venv/bin/activate
37
+ pip install -r requirements.txt
38
+ playwright install chromium
39
+ cp config.json.example config.json
40
+ ```
41
+
42
+ 업스트림 핀 업데이트는 로그인·인증·결제 인접 브라우저 자동화의 신뢰 경계를 바꾸는 작업이다. `scripts/upstream.pin` 값을 바꾸기 전에는 새 upstream diff를 검토하고, 설치 예시의 `git checkout` SHA와 함께 같은 PR에서 갱신한다.
43
+
44
+ ## Workflow
45
+
46
+ ### 1. 입력 파일을 저장소 밖 안전한 폴더에 준비한다
47
+
48
+ 발급 대상에는 법인등록번호, 상호명, 주소, 동호수 등 민감할 수 있는 정보가 들어간다. 공개 저장소, PR, 테스트 로그에 넣지 않는다.
49
+
50
+ ```bash
51
+ workdir="$(mktemp -d "${TMPDIR:-/tmp}/iros-registry.XXXXXX")"
52
+ chmod 700 "$workdir"
53
+ mkdir -p "$workdir/downloads" "$workdir/logs" "$workdir/output" "$workdir/tmp-downloads"
54
+ ```
55
+
56
+ 법인등록번호 기반 입력은 upstream repo `data/`가 아니라 `$workdir/corp-input.json` 같은 저장소 밖 파일에 둔다. 실제 법인등록번호/주소 원문을 upstream `data/` 디렉터리, git 저장소, PR 첨부, 테스트 로그에 넣지 않는다.
57
+
58
+ `iros_download.py`는 결제 후 열람·저장 단계에서 `companies_list`를 열어 저장 파일명을 맞춘다. 법인등록번호 흐름을 쓰더라도 결제 전 `$workdir/companies-input.json`을 함께 만들어 둔다.
59
+
60
+ ```bash
61
+ cat > "$workdir/corp-input.json" <<'JSON'
62
+ {
63
+ "1101111234567": "예시 주식회사",
64
+ "1101117654321": "샘플 주식회사"
65
+ }
66
+ JSON
67
+
68
+ python3 - "$workdir" <<'PY'
69
+ import json
70
+ import pathlib
71
+ import sys
72
+
73
+ workdir = pathlib.Path(sys.argv[1])
74
+ corp_input = json.loads((workdir / "corp-input.json").read_text())
75
+ companies = list(corp_input.values())
76
+ (workdir / "companies-input.json").write_text(
77
+ json.dumps(companies, ensure_ascii=False, indent=2) + "\n"
78
+ )
79
+ PY
80
+ ```
81
+
82
+ 부동산 주소 기반 입력 예시는 동/호수까지 필요한 경우가 있으므로 `data/iros_realties.json` 형식을 upstream README에서 확인하되, 실제 주소 원문은 `$workdir/realty-input.json` 같은 로컬 파일에만 둔다.
83
+
84
+ 사업자번호 조회나 종합 리포트 마법사 흐름에서 쓰는 고객 Excel도 upstream repo `data/`가 아니라 `$workdir/customer-list.xlsx` 같은 저장소 밖 파일에 둔다. 실제 고객 목록을 upstream `data/고객리스트.xlsx`에 복사하지 않는다.
85
+
86
+ `config.json`도 저장소에 커밋하지 않는 로컬 파일로 두고, 민감 입력·로그·산출물 경로를 모두 `$workdir` 아래로 돌린다.
87
+
88
+ ```bash
89
+ python3 - "$workdir" <<'PY'
90
+ import json
91
+ import pathlib
92
+ import sys
93
+
94
+ workdir = pathlib.Path(sys.argv[1])
95
+ config = json.loads(pathlib.Path("config.json").read_text())
96
+ config.update({
97
+ "corpnum_list": str(workdir / "corp-input.json"),
98
+ "companies_list": str(workdir / "companies-input.json"),
99
+ "realty_list": str(workdir / "realty-input.json"),
100
+ "excel_path": str(workdir / "customer-list.xlsx"),
101
+ "save_dir": str(workdir / "downloads"),
102
+ "realty_save_dir": str(workdir / "downloads" / "realty"),
103
+ "pdf_dir": str(workdir / "downloads"),
104
+ "report_output": str(workdir / "output" / "corp-report.xlsx"),
105
+ "extract_output": str(workdir / "output" / "corp-extract.json"),
106
+ "bizno_cache": str(workdir / "logs" / "bizno-cache.json"),
107
+ "bizno_results": str(workdir / "logs" / "bizno-results.json"),
108
+ "realty_cart_log": str(workdir / "logs" / "cart-realty-log.json"),
109
+ "realty_download_log": str(workdir / "logs" / "download-realty-log.json"),
110
+ "cart_log": str(workdir / "logs" / "cart-log.json"),
111
+ "cart_corpnum_log": str(workdir / "logs" / "cart-corpnum-log.json"),
112
+ "download_log": str(workdir / "logs" / "download-log.json"),
113
+ "download_temp": str(workdir / "tmp-downloads"),
114
+ })
115
+ pathlib.Path("config.json").write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n")
116
+ PY
117
+ ```
118
+
119
+ ### 2. TouchEn nxKey와 로그인 수단을 먼저 확인한다
120
+
121
+ 1. 인터넷등기소 로그인 페이지를 브라우저로 직접 연다.
122
+ 2. TouchEn nxKey 설치 안내가 나오면 설치 후 브라우저 또는 PC를 재시작한다.
123
+ 3. 공동인증서/간편인증/아이디 로그인 중 사용자가 선택한 방식으로 직접 로그인한다.
124
+ 4. 카드 결제가 가능한지 확인한다.
125
+
126
+ ### 3. 법인등기부등본 장바구니 담기
127
+
128
+ 법인등록번호를 알고 있으면 정확도가 높은 upstream `iros_cart_by_corpnum.py` 흐름을 우선한다. 상호명만 있으면 `iros_cart.py`를 사용하되 사명변경·특수문자 때문에 실패할 수 있어 실패분은 법인등록번호로 재시도한다.
129
+
130
+ ```bash
131
+ python iros_cart_by_corpnum.py
132
+ # 또는
133
+ python iros_cart.py
134
+ ```
135
+
136
+ 완료되면 브라우저의 결제대상목록으로 이동한다. 사용자는 브라우저에서 페이지당 10건 단위로 직접 카드 결제를 완료하고, 터미널에는 결제가 끝난 뒤 Enter를 입력한다.
137
+
138
+ ### 4. 법인 결제 후 열람·저장
139
+
140
+ 결제가 끝난 법인 등기부등본은 upstream `iros_download.py` 또는 마법사 메뉴의 법인 열람·저장 흐름으로 저장한다.
141
+
142
+ ```bash
143
+ python iros_download.py
144
+ ```
145
+
146
+ 저장 경로는 `config.json`의 `save_dir`로 관리하되, 위 예시처럼 `$workdir/downloads`를 사용하고 공개 저장소 하위 경로를 사용하지 않는다. `companies_list`가 `$workdir/companies-input.json`을 가리키는지 결제 전에 확인하면 결제 후 `iros_download.py`가 `FileNotFoundError`로 중단되는 일을 피할 수 있다.
147
+
148
+ ### 5. 부동산등기부등본 장바구니 담기
149
+
150
+ 부동산은 주소 목록 반복 입력과 장바구니 담기까지만 자동화를 우선 권장한다.
151
+
152
+ ```bash
153
+ python iros_cart_realty.py
154
+ ```
155
+
156
+ 결제, 열람, 다운로드는 인터넷등기소 웹 UI에서 사용자가 직접 일괄 결제·일괄열람출력·일괄저장을 수행하는 것이 보통 더 빠르고 안전하다. 필요할 때만 `iros_download_realty.py`를 검토한다.
157
+
158
+ ### 6. 마법사 경로
159
+
160
+ 처음 쓰는 사용자는 upstream `iros_wizard.py` 메뉴가 가장 안전하다.
161
+
162
+ ```bash
163
+ python iros_wizard.py
164
+ ```
165
+
166
+ 메뉴 요약:
167
+
168
+ - 법인등기부등본 — 장바구니 담기
169
+ - 법인등기부등본 — 결제 후 열람·저장
170
+ - 부동산등기부등본 — 장바구니 담기
171
+ - 부동산등기부등본 — 결제 후 열람·저장
172
+ - 사업자번호 → 법인정보 조회 (`excel_path`는 `$workdir/customer-list.xlsx`)
173
+ - 다운로드된 법인 PDF → 종합 리포트 엑셀 생성 (`excel_path`와 `pdf_dir`는 저장소 밖 경로)
174
+
175
+ ### 7. 발급 후 — 등기부등본 PDF 요약 (선택)
176
+
177
+ 발급·저장이 끝난 등기부등본 PDF(`$workdir/downloads/*.pdf`)에서 **근저당·가압류·소유권 변동 등 핵심 항목만 사실로 뽑아** 사람이 읽기 쉬운 요약을 만든다. 로그인·결제를 다시 하지 않는 **발급 후 단계 전용**이다.
178
+
179
+ - **판단하지 않는다.** "PDF에 이렇게 적혀 있다"는 사실(종류·발급일·소유권이전 건수·근저당 채권최고액/설정일/말소여부·가압류·가처분)까지만. "안전/위험/추천" 같은 결론을 내지 않는다.
180
+ - **원격 전송 없음.** 이 스크립트는 PDF를 어디에도 보내지 않고 로컬에서만 텍스트를 추출한다. 요약 결과에도 개인정보가 있을 수 있으므로 `$workdir/output/`에만 두고 저장소·PR·로그에 남기지 않는다. 주민등록번호는 마스킹한다.
181
+
182
+ ```bash
183
+ # 텍스트 레이어가 있는 일반 등기 PDF (기본) — pypdf 또는 pdfplumber 필요
184
+ pip install pypdf # 또는 pip install pdfplumber (표 추출에 유리)
185
+ python iros-registry-automation/scripts/iros_pdf_summary.py \
186
+ "$workdir/downloads/등기.pdf" --out "$workdir/output"
187
+
188
+ # 스캔(이미지) PDF인 경우: 사용자가 로컬에 self-host한 변환기(예: marker)로 먼저
189
+ # 텍스트/마크다운으로 바꾼 뒤 그 결과를 요약한다 (원격 전송 없이 로컬 파일만).
190
+ python iros-registry-automation/scripts/iros_pdf_summary.py 변환결과.md --from-text --out "$workdir/output"
191
+ ```
192
+
193
+ 출력(모두 사실 나열, 판단 없음): 등기 종류(부동산/법인), 열람·발급 일시, 소유권 변동 건수, 근저당권 목록(채권최고액·설정일·말소여부), 가압류/가처분/압류 건수. 추출 텍스트 구조에 따라 부정확할 수 있으니 **원문 대조를 권한다.**
194
+
195
+ 스캔 PDF 변환 참고 구현(로컬 self-host, 사용자가 직접 실행): `NomaDamas/marker-api-server` — https://github.com/NomaDamas/marker-api-server (PDF→Markdown). 이 스킬은 marker에 PDF를 원격으로 보내지 않으며, 사용자가 로컬 변환 결과를 `--from-text`로 넘기는 경우에만 이용한다.
196
+
197
+ ## Response policy
198
+
199
+ - 먼저 “로그인과 결제는 사용자가 직접”이라고 말한다.
200
+ - 법인과 부동산을 구분해 권장 자동화 범위를 설명한다.
201
+ - TouchEn nxKey 사전 설치와 브라우저 재시작 가능성을 안내한다.
202
+ - 발급 대상 목록, PDF, Excel, 보고서에는 개인정보/민감정보가 있을 수 있으므로 저장소 밖 비공개 폴더를 사용하게 한다.
203
+ - 법률 자문이나 권리관계 해석으로 보일 수 있는 표현을 피하고, 등기부등본 발급 보조와 파일 정리까지만 돕는다.
204
+ - PDF 요약(섹션 7)은 "PDF에 이렇게 적혀 있다"는 **사실 추출까지만** 한다. 요약이나 답변에서 "안전/위험/추천/괜찮다" 같은 판단형 결론을 내지 않는다. 요약 결과는 개인정보를 포함할 수 있으므로 저장소·PR·로그에 남기지 않는다.
205
+ - 원 저작자/참고 구현 링크를 문서나 답변에 남긴다: `challengekim/iros-registry-automation` — https://github.com/challengekim/iros-registry-automation. PDF→Markdown 변환 참고: `NomaDamas/marker-api-server` — https://github.com/NomaDamas/marker-api-server.
206
+
207
+ ## Verification
208
+
209
+ 로그인 없이 가능한 검증:
210
+
211
+ - upstream 저장소 clone
212
+ - `pip install -r requirements.txt`
213
+ - `playwright install chromium`
214
+ - `python iros_wizard.py` 실행 후 메뉴/입력 파일 안내가 정상 표시되는지 확인
215
+
216
+ 로그인 세션이 필요한 최종 smoke:
217
+
218
+ 1. 사용자가 직접 IROS에 로그인한다.
219
+ 2. 테스트용 1건을 장바구니에 담는다.
220
+ 3. 사용자가 직접 결제한다.
221
+ 4. 열람·저장 경로가 PDF를 저장하는지 확인한다.
222
+ 5. 산출물 경로와 개인정보를 PR/로그에 남기지 않는다.
223
+
224
+ ## Done when
225
+
226
+ - 법인/부동산 대상 유형과 입력 형식이 구분됐다.
227
+ - 로그인, 인증, 결제를 사람이 직접 처리한다는 안내가 명확하다.
228
+ - TouchEn nxKey와 페이지당 10건 결제 제약이 안내됐다.
229
+ - 원 저작자 `challengekim`과 참고 구현 링크가 포함됐다.