@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,177 @@
1
+ # 예비군 훈련정보 조회 & 작년 대비 비교
2
+
3
+ ## What this skill does
4
+
5
+ 공식 예비군 홈페이지(`https://www.yebigun1.mil.kr`)에서 **이미 로그인된 브라우저 세션**을 재사용해:
6
+
7
+ - "나의 훈련정보"(`IvdTraScheDetail.do`) 페이지를 조회해 소속 정보, 이번 훈련 기간/장소/훈련종류, 그리고 같은 페이지에 이미 표시되는 과거 연도 훈련 기록까지 한 번에 가져온다 (`training-info`).
8
+ - 올해와 작년(기본값) 기록을 필드 단위로 비교한 결과를 함께 돌려준다 — 별도 로컬 기록 단계 없이 페이지 자체에 있는 과거 기록을 바로 비교한다.
9
+ - 민감한 식별 정보(군번/이름/주민등록번호 앞자리/전화번호)가 없는 **조회/목록 화면**(훈련신청 결과, 연기신청 결과, 보류·해소 신청결과, 소속부대 공지사항, 훈련안내, 나의 질의응답, 예비군부대 찾기, 휴일예비군 훈련일정 조회)을 실제로 읽어서 표 형태로 돌려준다 (`view`).
10
+ - 그런 식별 정보가 화면에 그대로 노출되는 화면(훈련 연기신청/보류 신청/해소 신청/훈련일정 자율선택/전국단위 훈련신청/휴일예비군 훈련신청/개인정보수정/예비군 상훈)은 **화면만 열어주고 데이터는 절대 읽지 않는다** (`open-menu`) — 신청/제출도 절대 자동화하지 않는다.
11
+ - 필요하면 로컬에 연도별 기록을 추가로 저장(`record`)하고 비교(`diff`)할 수도 있다 (사이트가 더 오래된 기록을 보여주지 않게 되거나, 메모를 남기고 싶을 때를 위한 보조 기능).
12
+ - 범용 페이지 조회(`inspect`)로 아직 분류 안 된 다른 페이지 구조를 확인할 수도 있다.
13
+
14
+ `hipass-receipt`와 동일한 설계 원칙을 따른다: **로그인은 항상 사용자가 직접 하고, 이 스킬은 로그인된 세션에서 조회만 한다.**
15
+
16
+ ## Hard limits
17
+
18
+ - **로그인은 반드시 사용자가 직접 한다.** PASS 본인인증, 공동인증서, 간편인증, ID/PW 중 어떤 것도 자동 입력하지 않는다.
19
+ - **조회 전용이다.** 훈련 연기 신청, 보류·해소 신청, 훈련일정 자율선택 신청처럼 제출형(side-effect) 액션은 절대 자동화하지 않는다 — 군 의무 관련 법적 효력이 있는 제출이고, 사유 작성처럼 사용자 본인의 판단이 필요한 입력이 끼어 있어서다.
20
+ - **`open-menu`는 "화면 열기"까지만이다.** 버튼 클릭이든(`selfSelect`/`nationalUnit`/`holiday`) 직접 이동이든(`delay`/`hold`/`holdCancel`/`editProfile`/`honors`) 화면으로 이동만 시켜주지만, 그 화면에서 날짜를 고르거나 사유를 입력하거나 제출 버튼을 누르는 것은 절대 하지 않는다. 다음 화면이 뜨면 즉시 멈추고 "여기서부터는 직접 진행하라"고 안내한다 — 사용자의 실제 화면(같은 Chrome 창)에 이미 그 화면이 열려 있다.
21
+ - **`view`는 식별 정보가 없는 페이지에만 쓴다.** `VIEW_MENUS`에 등록된 메뉴만 실제로 읽어서 표(headers/rows)로 돌려준다 — 이름/군번/주민등록번호/전화번호/주소가 마크업에 그대로 노출되는 페이지는 절대 `VIEW_MENUS`에 넣지 않고 `open-menu`(`APPLICATION_MENUS`)로 돌린다.
22
+ - 세션 만료/로그인 필요 응답을 감지하면 즉시 중단하고 재로그인을 안내한다.
23
+ - 개인정보(훈련기간/장소/소속부대/군번 등)가 담긴 기록은 **이 저장소 밖** `~/.cache/k-skill/yebigun-training/history.json`에만 저장한다. 절대 이 git 저장소에 실제 개인 데이터를 커밋하지 않는다 — `test/fixtures/`의 페이지 구조 fixture는 처음부터 끝까지 완전히 가상의 이름/날짜/장소로만 작성됐다(실제 로그인 세션에서 본 마크업 *구조*만 베껴서 재현했고, 그 세션에서 본 실제 값은 어디에도 옮기지 않았다).
24
+
25
+ ## Why this design (and how it was verified)
26
+
27
+ - 병무청은 예비군 훈련 일정을 공개 API로 제공하지 않는다. 개인 훈련정보는 `yebigun1.mil.kr`에 PASS/공동인증서/간편인증으로 로그인해야만 보인다 (도메인은 `mma.go.kr`이 아니라 `yebigun1.mil.kr`).
28
+ - v1 설계 당시엔 본인인증 없이 로그인 후 페이지를 미리 볼 방법이 없어 `parseTrainingInfo`를 비워뒀었다. **2026-06-24, 사용자가 직접 로그인한 세션으로 실제 "나의 훈련정보" 페이지(`/dmobis/rfh/rgt/edutrasubjpsn/IvdTraScheDetail.do`)를 `inspect`로 확인하고 구조를 확정했다:**
29
+ - 소속 정보: `<caption>소속</caption>`이 있는 표 (예비군부대/소속/계급/군번/성명/군별/연차/동원구분/비고).
30
+ - 올해 표시 연도: `<h4>훈련내용(YYYY년)</h4>` 헤딩.
31
+ - 올해 훈련 목록: `<table id="detailTb">` — 첫 행은 "총계" 합계 행(건너뜀), 나머지 행에 `data-tra-id` 등 속성과 구분/훈련일자/계획시간/실시시간/잔여시간/훈련결과/훈련장/비고 8개 열.
32
+ - **과거 연도 훈련 목록은 같은 페이지의 `<table id="detail2">`에 이미 들어있다** (화면에는 "이전 훈련내용 열기"를 눌러야 보이지만, `domcontentloaded` 시점에 이미 DOM에 존재함 — 추가 클릭 없이 바로 읽을 수 있다). 같은 8개 열 구조를 공유한다.
33
+ - 이 발견 덕분에 "작년과 비교"가 별도 로컬 저장 없이 **한 번의 `training-info` 호출**로 끝난다: 페이지 자체가 여러 해의 기록을 동시에 보여준다.
34
+ - 같은 페이지의 `goAction(mode)` 자바스크립트 함수가 `훈련일정 자율선택`/`전국단위 훈련신청`/`휴일예비군 훈련신청` 세 버튼을 각각 다른 신청서 URL로 연결한다는 것도 이때 확인했다 — 이게 `open-menu`가 클릭하는 정확한 버튼 레이블의 출처다. (이 페이지엔 `goAction`이 `TRASCHD`라는 네 번째 분기도 갖고 있었지만, 그 분기와 연결된 보이는 버튼을 이번 검증에서 찾지 못해 `APPLICATION_MENUS`에 포함하지 않았다 — 확인 안 된 걸 추측해서 넣지 않는다.)
35
+ - training-info 페이지의 전체 사이트 LNB(`<a href>` 목록)를 같은 `inspect`로 훑어서 `훈련 연기신청`(`/dmobis/rft/rgt/ivdTraDelayApplInForm.do`), `보류 신청`(`/dmobis/rfh/rrm/holdpsn/HoldPsnReqForm.do`), `해소 신청`(`/dmobis/rfh/rrm/holdpsn/HoldPsnCancelReqForm.do`)의 실제 경로도 확인했다 (2026-06-24). 이 셋은 `goAction` 버튼이 아니라 평범한 링크라서 클릭 시뮬레이션 없이 바로 `page.goto`로 이동한다 — `open-menu`가 `mode: "goto"`로 구분해서 처리한다.
36
+ - `훈련 연기신청` 화면은 실제로 열어보니 이름/주민등록번호 앞자리/주소/휴대폰·집·직장 전화번호가 hidden input으로 그대로 박혀 있었다 — training-info보다 훨씬 민감한 페이지다. 이 값들은 어디에도 기록하지 않았고, 오직 페이지 URL(사이트 구조)만 `APPLICATION_MENUS`에 남겼다.
37
+ - 2026-06-24, "모든 조회를 추가하고 민감한 건 안내까지만"이라는 요청에 따라 training-info 페이지의 전체 사이트 LNB(`<a href>` 목록, `사이트맵`과 동일 출처)를 훑어서 10개 후보 페이지를 찾고, 각각 `<thead>` 헤더와 hidden field를 스캔해서 둘로 분류했다:
38
+ - **`VIEW_MENUS`(실제로 읽음)**: 훈련신청 결과(`NationalUnitResevForcesTraRltList.do`), 연기신청 결과(`ivdTraDelayApplRltList.do`), 보류·해소 신청결과(`HoldPsnReqRsltList.do`), 휴일예비군 훈련일정 조회(`HolidayTrainingScheduleList.do`), 소속부대 공지사항(`MyPubAnnounList.do`), 훈련안내(`TraNoticeList.do`), 나의 질의응답(`MyQuestAnsList.do`), 예비군부대 찾기(`listAdminAddr.do`). 헤더에 군번/이름 같은 식별 컬럼이 없는 것만 골랐다.
39
+ - **`APPLICATION_MENUS`로 추가(절대 안 읽음)**: 개인정보수정(`ReserveForceForm.do` — 편집 폼이고 hidden field에 `cellPhone` 등 직접 노출 확인), 예비군 상훈(`ReserveForcePrzdcr.do` — 페이지 상단에 training-info의 "소속" 표와 똑같이 군번/성명이 그대로 들어있는 걸 확인, 그 아래 실제 상훈 목록은 따로 검증하지 않았다).
40
+ - **포함하지 않음**: 카드뉴스/국방영상/공지사항(전체)/자주 묻는 질문/사이버 설문조사/혁신 아이디어 공모전/예비군 제도안내/관련 법령/개인정보처리방침처럼 로그인·개인 계정과 무관한 일반 공개 게시판·안내 페이지는 이 스킬의 목적(내 예비군 신상 조회)과 거리가 멀어 추가하지 않았다. 필요하면 같은 패턴으로 더 추가할 수 있다.
41
+ - `나의 질의응답`(`myQna`)은 실제로 열어보니 "내가 쓴 글"이 아니라 **다른 사용자들의 질문도 함께 보이는 공개 게시판**이었다 (작성자 이름은 사이트 자체가 이미 "민\*연"처럼 마스킹해서 보여준다). 마스킹된 이름만 노출되므로 `VIEW_MENUS`에 남겼지만, 다른 사용자의 (마스킹된) 글 내용을 답변에 과도하게 인용하지 않는다.
42
+ - 여러 `VIEW_MENUS` 페이지(예: 훈련신청 결과)는 초기 HTML에 `Loading...` placeholder 행만 있고 실제 데이터는 AJAX로 나중에 채워진다는 것도 이때 확인했다 — `fetchInquiry`가 이 placeholder를 실제 데이터로 착각해 반환하지 않도록, 사라질 때까지 짧게 폴링한 뒤(최대 약 4.5초) 그래도 안 사라지면 명확한 에러로 멈춘다.
43
+ - 병무청 디지털서비스개방의 "청년 동원훈련 일정조회"(`openservice.go.kr/youthMilTrainSch`)는 이번 검증에서 비교 대상으로 시도하지 않았다. 이미 `yebigun1.mil.kr` 쪽 구조가 안정적으로 파싱돼서 추가 검증이 급하지 않다고 판단했다 — 필요해지면 같은 방식(`inspect`)으로 비교해본다.
44
+ - 사이트가 마크업을 바꾸면 `trainings`가 빈 배열로 나올 수 있다. 이 경우 "훈련이 없다"로 해석하지 말고 구조가 바뀌었다고 보고 재검증한다 (`Failure modes` 참고). `view`도 마찬가지: `headers`가 비어 있으면 "내용이 없다"가 아니라 그 페이지에 `<thead>`가 없다는 뜻이므로 구조 변경을 의심한다.
45
+
46
+ ## Prerequisites
47
+
48
+ - macOS 또는 Chrome 실행 가능한 환경
49
+ - `packages/yebigun-training`에서 `npm install` (`playwright-core` 포함)
50
+ - Chrome 원격 디버깅 포트 사용 가능
51
+ - 사용자가 직접 예비군 홈페이지 로그인 가능 (PASS/공동인증서/간편인증 중 본인이 쓰는 방식)
52
+
53
+ ## Workflow
54
+
55
+ ### 1. 전용 Chrome 프로필로 로그인 브라우저를 띄운다
56
+
57
+ ```bash
58
+ node packages/yebigun-training/src/cli.js chrome-command --profile-dir "$HOME/.cache/k-skill/yebigun-chrome" --debugging-port 9222
59
+ ```
60
+
61
+ 위 명령이 출력한 Chrome 실행문으로 브라우저를 띄운 뒤, 사용자가 직접 `https://www.yebigun1.mil.kr/`에서 로그인한다.
62
+
63
+ ### 2. 이번 훈련정보 + 작년 비교를 한 번에 조회한다
64
+
65
+ ```bash
66
+ node packages/yebigun-training/src/cli.js training-info --cdp-url http://127.0.0.1:9222
67
+ ```
68
+
69
+ 결과 JSON의 구조:
70
+
71
+ - `member`: 예비군부대/소속/계급/군번/성명/군별/연차/동원구분.
72
+ - `currentDisplayYear`: 사이트가 "올해"로 표시하는 연도.
73
+ - `trainings`: 올해 + 과거 연도 훈련 기록 배열(최신순), 각 항목에 `trainingType`/`startDate`/`endDate`/`plannedHours`/`actualHours`/`remainingHours`/`result`/`location`.
74
+ - `comparison`: `currentDisplayYear` vs `currentDisplayYear - 1`을 자동 비교한 `{ hasCurrentRecord, hasPreviousRecord, current, previous, changes }`.
75
+
76
+ 세션이 만료됐으면 "session is not authenticated or has expired" 에러가 즉시 뜬다 — 이때는 사용자에게 재로그인을 안내하고 중단한다.
77
+
78
+ 사용자에게 결과를 전달할 때: 올해 훈련의 기간/장소/구분과 `comparison.changes`(필드별 변경점)를 한국어로 자연스럽게 요약한다. `comparison.hasPreviousRecord`가 `false`면 "비교할 작년 기록이 없다"고 그대로 말한다 — 추측하지 않는다.
79
+
80
+ ### 3. 그 외 조회 화면을 보고 싶다고 하면: `view`로 실제 데이터를 읽어온다
81
+
82
+ ```bash
83
+ node packages/yebigun-training/src/cli.js view --menu applicationResults --cdp-url http://127.0.0.1:9222
84
+ ```
85
+
86
+ `--menu`로 지정 가능한 값 (모두 `VIEW_MENUS`, 식별 정보 없는 조회 전용):
87
+
88
+ | menu 값 | 화면 |
89
+ |---|---|
90
+ | `applicationResults` | 훈련신청 결과 |
91
+ | `delayResults` | 연기신청 결과 |
92
+ | `holdResults` | 보류·해소 신청결과 |
93
+ | `holidaySchedule` | 휴일예비군 훈련일정 조회 |
94
+ | `unitNotices` | 소속부대 공지사항 |
95
+ | `trainingNotices` | 훈련안내 |
96
+ | `myQna` | 나의 질의응답 (다른 사용자의 마스킹된 글도 함께 보이는 공개 게시판이다 — 응답에서 남의 글을 과도하게 인용하지 않는다) |
97
+ | `unitFinder` | 예비군부대 찾기 |
98
+
99
+ 결과는 항상 `{ menu, label, headers, rows }` 형태의 일반 표다 (페이지마다 다른 필드명을 따로 만들지 않았다). `headers`는 채워져 있는데 `rows`가 빈 배열이면 "등록된 게 없다"는 뜻이고, `headers`까지 비어 있으면 페이지 구조가 바뀐 것으로 보고 `inspect`로 재확인한다. `예비군부대 찾기`/`휴일예비군 훈련일정 조회`처럼 검색 조건 없이 처음 들어가면 비어 있는 게 정상인 페이지도 있다.
100
+
101
+ ### 4. 사용자가 연기/보류/일정선택/개인정보수정 등을 하고 싶다고 하면: 화면까지만 열어준다
102
+
103
+ ```bash
104
+ node packages/yebigun-training/src/cli.js open-menu --menu delay --cdp-url http://127.0.0.1:9222
105
+ ```
106
+
107
+ `--menu`로 지정 가능한 값과 동작 방식 (모두 `APPLICATION_MENUS`, 데이터는 절대 읽지 않는다):
108
+
109
+ | menu 값 | 화면 | 방식 |
110
+ |---|---|---|
111
+ | `selfSelect` | 훈련일정 자율선택 | training-info 페이지의 실제 버튼을 클릭 |
112
+ | `nationalUnit` | 전국단위 훈련신청 | training-info 페이지의 실제 버튼을 클릭 |
113
+ | `holiday` | 휴일예비군 훈련신청 | training-info 페이지의 실제 버튼을 클릭 |
114
+ | `delay` | 훈련 연기신청 | `/dmobis/rft/rgt/ivdTraDelayApplInForm.do`로 직접 이동 |
115
+ | `hold` | 보류 신청 | `/dmobis/rfh/rrm/holdpsn/HoldPsnReqForm.do`로 직접 이동 |
116
+ | `holdCancel` | 해소 신청 | `/dmobis/rfh/rrm/holdpsn/HoldPsnCancelReqForm.do`로 직접 이동 |
117
+ | `editProfile` | 개인정보수정 | `/dmobis/rfh/rrm/reserveforce/ReserveForceForm.do`로 직접 이동 |
118
+ | `honors` | 예비군 상훈 | `/dmobis/rfh/rrm/reserveforce/ReserveForcePrzdcr.do`로 직접 이동 |
119
+
120
+ 어느 방식이든 **다음 화면이 뜨면 그 자리에서 멈춘다** — 날짜 선택, 사유 입력, 제출은 절대 하지 않는다. `훈련 연기신청`/`개인정보수정` 화면은 실제로 들어가보면 이름/주민등록번호 앞자리/주소/휴대폰·집·직장 전화번호가 그대로 보일 만큼 training-info보다 훨씬 민감하다 — 이 정보를 답변이나 파일에 옮기지 않는다.
121
+
122
+ 호출 후 사용자에게는 정확히 이렇게 안내한다: "지금 보고 계신 Chrome 창에 `<label>` 화면이 열렸습니다. 여기서부터 입력과 제출은 직접 해주세요." 표에 없는 메뉴는 `open-menu`/`view` 둘 다 지원하지 않으므로(`Unknown menu` 에러), 사이트맵(`/dmobis/rfh/rgt/sitemap/sitemap.jsp`)이나 `inspect`로 먼저 실제 경로/구조를 확인한 뒤 어느 쪽(조회 vs 안내만)에 넣을지 식별 정보 노출 여부로 판단해서 사용자와 상의한다.
123
+
124
+ ### 5. (다른 페이지 구조를 더 봐야 할 때) 범용 조회
125
+
126
+ ```bash
127
+ node packages/yebigun-training/src/cli.js inspect --cdp-url http://127.0.0.1:9222 --path <경로> --full
128
+ ```
129
+
130
+ `pageInfo.pageType`이 `login`이면 재로그인을 안내한다.
131
+
132
+ ### 6. (선택) 로컬에도 따로 기록해두고 비교하기
133
+
134
+ 사이트의 `이전 훈련내용` 표가 보여주는 연도 범위를 벗어나는 기록을 직접 남겨두고 싶을 때만 쓴다. `training-info`의 `trainings` 배열에서 원하는 연도의 항목을 골라 그대로 저장하면 된다.
135
+
136
+ ```bash
137
+ node packages/yebigun-training/src/cli.js record --year 2026 --json '{"trainingType":"동원훈련Ⅱ형 1차","startDate":"2026-08-10","endDate":"2026-08-12","location":"OO과학화예비군훈련장"}'
138
+ node packages/yebigun-training/src/cli.js diff --year 2026
139
+ ```
140
+
141
+ 기본 비교 대상은 `year - 1`이다 (`--compare-year`로 다른 연도 지정 가능). 결과의 `hasPreviousRecord`가 `false`면 "작년 기록이 없어 비교할 수 없다"고 명확히 말한다.
142
+
143
+ ## Response policy
144
+
145
+ - "로그인 필수", "세션 만료 시 재로그인 필요"를 항상 명확히 적는다.
146
+ - 훈련 연기/보류/자율선택 같은 제출형 신청은 절대 자동화하지 않고, 사용자가 브라우저에서 직접 하도록 안내한다. `open-menu`로 화면까지 열어준 경우에도 "여기서부터는 직접 진행하세요"를 빼지 않는다.
147
+ - 군번/전화번호 등 식별 정보를 답변에 그대로 길게 인용하지 말고, 훈련 기간/장소/달라진 점 위주로 요약한다.
148
+ - `view`(`myQna` 등) 결과에 다른 사용자의 마스킹된 이름/글이 섞여 있으면, 사용자가 직접 물어본 본인 관련 내용에 집중하고 남의 글을 장문으로 인용하지 않는다.
149
+ - 비교 결과를 보여줄 때 추측을 섞지 않는다 — 기록되지 않은 연도는 "기록 없음"으로만 말한다.
150
+ - `view`가 빈 `rows`를 반환하면 "등록된 게 없다"고 그대로 말한다 (검색 조건이 필요한 페이지일 수도 있음을 함께 안내). `headers`까지 비어 있으면 페이지 구조가 바뀐 것으로 보고 `inspect`로 재확인을 제안한다.
151
+ - 페이지 구조가 바뀐 것으로 보이면(`trainings`가 비정상적으로 비어 있는 등) 그 가능성을 사용자에게 알리고 `inspect`로 재확인을 제안한다.
152
+
153
+ ## Verification
154
+
155
+ - 자동 검증: `npm run lint && npm test` — `detectSessionState`/`inspectYebigunPage` 분류, **완전히 가상의 데이터로 작성한 fixture**(`test/fixtures/training-info-page.html`, `test/fixtures/view-list-page.html`) 기반 `parseTrainingInfo`/`parseGenericTable` 단위 테스트, mocked-CDP `inspect`/`fetchTrainingInfo`/`fetchInquiry`(AJAX `Loading...` 폴링 포함)/`openApplicationMenu`, 그리고 `record`/`history`/`diff`의 로컬 JSON 로직 단위 테스트.
156
+ - smoke 검증(로그인 불필요): `chrome-command`로 출력된 명령이 실제 Chrome을 올바른 프로필/포트로 띄우는지 확인.
157
+ - 실서비스 검증: 사용자가 직접 로그인한 BrowserOS/Chrome CDP 세션에서 `training-info`가 소속 정보, 올해 훈련, 과거 연도 기록, 작년 대비 비교를 구조화 JSON으로 반환하는지 확인한다. 공개 문서에는 실제 날짜, 부대, 성명, 군번, 훈련장, 민원/신청 내용 같은 인증 세션 값을 기록하지 않는다.
158
+
159
+ ## Failure modes
160
+
161
+ - 세션 만료/미로그인: `inspect`/`training-info`/`view`/`open-menu`가 `pageType: "login"` 또는 "session is not authenticated or has expired" 에러를 반환한다 — 즉시 중단하고 재로그인 안내.
162
+ - `trainings`가 비정상적으로 비어 있거나 `member`가 `null`: 사이트가 마크업을 바꿨을 가능성이 높다. "훈련이 없다"고 단정하지 말고 `inspect --full`로 실제 HTML을 다시 확인하라고 안내한다.
163
+ - `diff`(로컬 기록)에서 비교 연도 기록이 없음: 추측하지 말고 "작년 기록이 없다"고 그대로 전달한다.
164
+ - CDP 연결 실패: 사용자가 `chrome-command`로 띄운 디버깅 포트 Chrome이 켜져 있는지, `--cdp-url`이 맞는지 확인하라고 안내한다.
165
+ - `open-menu`/`view`에 알 수 없는 `--menu` 값: 브라우저를 건드리기 전에 `Unknown menu` 에러로 막는다 (각자 `APPLICATION_MENUS`/`VIEW_MENUS`의 키만 허용).
166
+ - `open-menu`가 버튼을 못 찾음: "Could not find the ... button" 에러를 반환한다 — 사이트가 버튼 레이블/구조를 바꿨을 가능성이 높으므로 `inspect`로 재확인하라고 안내한다.
167
+ - `view`가 AJAX 로딩을 끝내 못 기다림: "did not finish loading in time" 에러를 반환한다 — 한 번 더 `view`를 시도하거나, 계속 반복되면 `inspect`로 실제 응답을 확인하라고 안내한다.
168
+
169
+ ## Done when
170
+
171
+ - 로그인된 세션으로 `training-info`가 소속 정보, 올해 훈련 기간/장소/종류, 과거 연도 기록, 작년 대비 비교를 한 번에 정확히 반환한다.
172
+ - 세션 만료 시 명확한 재로그인 에러로 즉시 중단된다.
173
+ - `open-menu`가 화면까지만 이동시키고(클릭이든 직접 이동이든), 그 화면의 입력/제출은 절대 건드리지 않는다 — 신청형 화면뿐 아니라 식별 정보가 노출되는 조회형 화면(개인정보수정/예비군 상훈)도 같은 원칙으로 막는다.
174
+ - `view`가 `VIEW_MENUS`에 등록된, 식별 정보 없는 화면만 실제로 읽어서 일반화된 headers/rows로 돌려준다. AJAX로 늦게 채워지는 목록을 placeholder 상태로 잘못 반환하지 않는다.
175
+ - (로컬 기록을 쓴 경우) `record`로 저장한 연도별 기록이 `~/.cache/k-skill/yebigun-training/history.json`에만 남고 저장소에는 커밋되지 않는다.
176
+ - 어떤 단계에서도 로그인 입력이나 신청/제출 액션이 자동화되지 않았다.
177
+ - 저장소에 커밋된 fixture/테스트 데이터에 실제 개인정보가 한 글자도 섞여 있지 않다.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "yebigun-training",
3
+ "description": "사용자가 직접 로그인한 예비군 홈페이지(yebigun1.mil.kr) Chrome 세션을 재사용해 이번 훈련 일정/장소를 조회하고, 작년 이전 기록과 비교해 무엇이 달라졌는지 알려준다. 조회 전용이며 로그인이나 연기·보류 신청은 절대 자동화하지 않는다. 돌쇠에서는 가능한 가장 가까운 합법적 공식 단계까지 준비한다.",
4
+ "profiles": [
5
+ "browser",
6
+ "hard-boundary"
7
+ ],
8
+ "frontmatter": "name: yebigun-training\ndescription: 사용자가 직접 로그인한 예비군 홈페이지(yebigun1.mil.kr) Chrome 세션을 재사용해 이번 훈련 일정/장소를 조회하고, 작년 이전 기록과 비교해 무엇이 달라졌는지 알려준다. 조회 전용이며 로그인이나 연기·보류 신청은 절대 자동화하지 않는다. 돌쇠에서는 가능한 가장 가까운 합법적 공식 단계까지 준비한다.\nlicense: MIT\nmetadata:\n category: legal\n locale: ko-KR\n phase: v1"
9
+ }
@@ -0,0 +1,159 @@
1
+ # Zipcode Search
2
+
3
+ ## What this skill does
4
+
5
+ 우체국 공식 통합 우편번호 검색 페이지를 조회해서 주소 키워드에 맞는 우편번호와 공식 영문 주소를 함께 찾는다.
6
+
7
+ ## When to use
8
+
9
+ - "이 주소 우편번호랑 영문 주소 같이 알려줘"
10
+ - "서울특별시 강남구 테헤란로 123 영문 주소로 바꿔줘"
11
+ - "해외 결제용으로 한국 주소 영문 표기 필요해"
12
+
13
+ ## Prerequisites
14
+
15
+ - 인터넷 연결
16
+ - `curl`
17
+ - `python3`
18
+
19
+ ## Inputs
20
+
21
+ - 주소 키워드
22
+ - 도로명 + 건물번호
23
+ - 시/군/구 + 도로명
24
+ - 동/리 + 지번
25
+
26
+ ## Workflow
27
+
28
+ ### 1. Query the official integrated ePost page first
29
+
30
+ 비공식 영문주소 변환기나 블로그 표기를 쓰지 말고 아래 우체국 공식 통합 검색 페이지를 먼저 조회한다.
31
+
32
+ ```text
33
+ https://www.epost.kr/search.RetrieveIntegrationNewZipCdList.comm
34
+ ```
35
+
36
+ 이 페이지는 `keyword` 파라미터로 우편번호, 국문 주소, `English/집배코드` 열의 공식 영문 주소를 함께 돌려준다.
37
+
38
+ ### 2. Fetch the HTML with curl and extract the `viewDetail(...)` rows
39
+
40
+ 현재 ePost 엔드포인트는 응답이 간헐적으로 reset/timeout 될 수 있으므로, 로컬 `urllib` 대신 `curl --http1.1 --tls-max 1.2` + 재시도 경로를 기본 예시로 사용한다.
41
+
42
+ ```bash
43
+ python3 - <<'PY'
44
+ import html
45
+ import re
46
+ import subprocess
47
+
48
+ query = "서울특별시 강남구 테헤란로 123"
49
+ cmd = [
50
+ "curl",
51
+ "--http1.1",
52
+ "--tls-max",
53
+ "1.2",
54
+ "--silent",
55
+ "--show-error",
56
+ "--location",
57
+ "--retry",
58
+ "3",
59
+ "--retry-all-errors",
60
+ "--retry-delay",
61
+ "1",
62
+ "--max-time",
63
+ "20",
64
+ "--get",
65
+ "--data-urlencode",
66
+ f"keyword={query}",
67
+ "https://www.epost.kr/search.RetrieveIntegrationNewZipCdList.comm",
68
+ ]
69
+ page = subprocess.run(
70
+ cmd,
71
+ check=True,
72
+ capture_output=True,
73
+ text=True,
74
+ encoding="utf-8",
75
+ ).stdout
76
+
77
+ matches = re.findall(
78
+ r"viewDetail\('([^']*)','([^']*)','([^']*)','([^']*)',\s*'[^']*'\)",
79
+ page,
80
+ )
81
+
82
+ if not matches:
83
+ raise SystemExit("검색 결과가 없습니다.")
84
+
85
+ for zip_code, road_address, english_address, jibun_address in matches[:5]:
86
+ print(zip_code)
87
+ print(html.unescape(road_address))
88
+ print(html.unescape(english_address))
89
+ print(html.unescape(jibun_address))
90
+ print("---")
91
+ PY
92
+ ```
93
+
94
+ 핵심 값은 `viewDetail(zip, roadAddress, englishAddress, jibunAddress, rowIndex)` 인자다. 공식 출력은 보통 `123, Teheran-ro, Gangnam-gu, Seoul, 06133, Rep. of KOREA` 같은 형식을 그대로 준다.
95
+
96
+ ### 3. Prefer the shipped helper for repeatable execution
97
+
98
+ 저장소에는 같은 흐름을 감싼 실행 가능한 helper가 포함되어 있다.
99
+
100
+ ```bash
101
+ python3 scripts/zipcode_search.py "서울특별시 강남구 테헤란로 123"
102
+ ./scripts/zipcode_search.py "서울특별시 강남구 테헤란로 123"
103
+ ```
104
+
105
+ 예시 출력:
106
+
107
+ ```json
108
+ {
109
+ "query": "서울특별시 강남구 테헤란로 123",
110
+ "results": [
111
+ {
112
+ "zip_code": "06133",
113
+ "road_address": "서울특별시 강남구 테헤란로 123 (역삼동, 여삼빌딩)",
114
+ "english_address": "123, Teheran-ro, Gangnam-gu, Seoul, 06133, Rep. of KOREA",
115
+ "jibun_address": "서울특별시 강남구 역삼동 648-23 (여삼빌딩)"
116
+ }
117
+ ]
118
+ }
119
+ ```
120
+
121
+ ### 4. Normalize for humans
122
+
123
+ 응답은 raw HTML이므로 그대로 붙이지 말고 아래처럼 정리한다.
124
+
125
+ - 우편번호
126
+ - 도로명 국문 주소
127
+ - 공식 영문 주소
128
+ - 필요하면 지번 주소
129
+ - 후보가 여러 개면 상위 3~5개만 보여주고 어느 항목이 가장 근접한지 짚기
130
+
131
+ ### 5. Retry with tighter and fuller keywords when needed
132
+
133
+ 검색 결과가 없거나 timeout/reset이 반복되면 아래 순서로 재시도한다.
134
+
135
+ - 짧은 도로명 + 건물번호: `테헤란로 123`
136
+ - 시/군/구 포함 전체 주소: `서울 강남구 테헤란로 123`
137
+ - 동/리 + 지번 또는 대체 표기: `역삼동 648-23`
138
+
139
+ ### 6. Prefer temp files in wrapped shells
140
+
141
+ CLI 래퍼나 에이전트 쉘에서는 here-doc + Python one-liner가 깨질 수 있으므로, 실전에서는 `mktemp` 같은 임시 파일에 HTML을 저장한 뒤 그 파일을 파싱하는 경로를 우선한다. 응답 일부만 보려고 `| head` 를 붙이면 다운스트림이 먼저 닫히면서 `curl: (23)` 이 보일 수 있으니, 이 경우도 전체 응답을 임시 파일에 저장한 뒤 확인한다.
142
+
143
+ ## Done when
144
+
145
+ - 적어도 한 개의 우편번호 후보와 공식 영문 주소가 정리되어 있다
146
+ - 다중 후보일 때 사용자가 고를 수 있게 국문/영문 주소 차이가 보인다
147
+ - 검색 결과가 없으면 재검색 키워드 방향을 제안했다
148
+
149
+ ## Failure modes
150
+
151
+ - 우체국 검색 페이지 마크업이 바뀌면 `viewDetail(...)` 추출 규칙이 깨질 수 있다
152
+ - 주소 키워드가 너무 넓으면 결과가 과하게 많아질 수 있다
153
+ - 재시도 없이 한 번만 호출하면 timeout/reset 같은 일시 오류가 날 수 있다
154
+ - `curl` 없이 다른 클라이언트로 바로 붙으면 협상/전송 오류가 날 수 있다
155
+
156
+ ## Notes
157
+
158
+ - 공식 표기 그대로 유지하는 조회형 스킬이다
159
+ - 상대 날짜/실시간 개념은 없으므로 주소 문자열 정제에 집중한다
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import html
7
+ import json
8
+ import subprocess
9
+ from dataclasses import asdict, dataclass
10
+ import re
11
+ from typing import Callable, Sequence
12
+
13
+ SEARCH_URL = "https://www.epost.kr/search.RetrieveIntegrationNewZipCdList.comm"
14
+ DEFAULT_LIMIT = 5
15
+ VIEW_DETAIL_PATTERN = re.compile(
16
+ r"viewDetail\(\s*'(?P<zip>(?:\\'|[^'])*)'\s*,\s*'(?P<road>(?:\\'|[^'])*)'\s*,\s*'(?P<english>(?:\\'|[^'])*)'\s*,\s*'(?P<jibun>(?:\\'|[^'])*)'\s*,\s*'(?P<row>(?:\\'|[^'])*)'\s*\)",
17
+ re.S,
18
+ )
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class AddressSearchResult:
23
+ zip_code: str
24
+ road_address: str
25
+ english_address: str
26
+ jibun_address: str | None = None
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class AddressSearchResponse:
31
+ query: str
32
+ results: list[AddressSearchResult]
33
+
34
+ def to_json(self) -> str:
35
+ return json.dumps(
36
+ {
37
+ "query": self.query,
38
+ "results": [asdict(item) for item in self.results],
39
+ },
40
+ ensure_ascii=False,
41
+ indent=2,
42
+ )
43
+
44
+
45
+ def clean_text(value: str | None) -> str | None:
46
+ if value is None:
47
+ return None
48
+ cleaned = html.unescape(value).replace("\\'", "'")
49
+ cleaned = " ".join(cleaned.split()).strip()
50
+ return cleaned or None
51
+
52
+
53
+ def parse_search_results(page: str) -> list[AddressSearchResult]:
54
+ items: list[AddressSearchResult] = []
55
+ for match in VIEW_DETAIL_PATTERN.finditer(page):
56
+ zip_code = clean_text(match.group("zip"))
57
+ road_address = clean_text(match.group("road"))
58
+ english_address = clean_text(match.group("english"))
59
+ jibun_address = clean_text(match.group("jibun"))
60
+ if not zip_code or not road_address or not english_address:
61
+ continue
62
+ items.append(
63
+ AddressSearchResult(
64
+ zip_code=zip_code,
65
+ road_address=road_address,
66
+ english_address=english_address,
67
+ jibun_address=jibun_address,
68
+ )
69
+ )
70
+ return items
71
+
72
+
73
+ def build_search_command(query: str) -> list[str]:
74
+ return [
75
+ "curl",
76
+ "--http1.1",
77
+ "--tls-max",
78
+ "1.2",
79
+ "--silent",
80
+ "--show-error",
81
+ "--location",
82
+ "--retry",
83
+ "3",
84
+ "--retry-all-errors",
85
+ "--retry-delay",
86
+ "1",
87
+ "--max-time",
88
+ "20",
89
+ "--get",
90
+ "--data-urlencode",
91
+ f"keyword={query}",
92
+ SEARCH_URL,
93
+ ]
94
+
95
+
96
+ Runner = Callable[..., subprocess.CompletedProcess[str]]
97
+
98
+
99
+ def fetch_search_page(query: str, *, runner: Runner = subprocess.run) -> str:
100
+ result = runner(
101
+ build_search_command(query),
102
+ check=True,
103
+ capture_output=True,
104
+ text=True,
105
+ encoding="utf-8",
106
+ )
107
+ return result.stdout
108
+
109
+
110
+ Fetcher = Callable[[str], str]
111
+
112
+
113
+ def lookup_korean_address(
114
+ query: str,
115
+ *,
116
+ limit: int = DEFAULT_LIMIT,
117
+ fetcher: Fetcher = fetch_search_page,
118
+ ) -> AddressSearchResponse:
119
+ normalized_query = " ".join(query.split()).strip()
120
+ if not normalized_query:
121
+ raise ValueError("query must not be blank")
122
+ if limit <= 0:
123
+ raise ValueError("limit must be a positive integer")
124
+
125
+ page = fetcher(normalized_query)
126
+ return AddressSearchResponse(
127
+ query=normalized_query,
128
+ results=parse_search_results(page)[:limit],
129
+ )
130
+
131
+
132
+ def build_parser() -> argparse.ArgumentParser:
133
+ parser = argparse.ArgumentParser(
134
+ description="Look up Korean postcodes and official English addresses from ePost.",
135
+ )
136
+ parser.add_argument("query", help="Korean road-name or jibun address query")
137
+ parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT, help="maximum number of rows to keep")
138
+ return parser
139
+
140
+
141
+ def main(argv: Sequence[str] | None = None) -> int:
142
+ parser = build_parser()
143
+ args = parser.parse_args(argv)
144
+ response = lookup_korean_address(args.query, limit=args.limit)
145
+ print(response.to_json())
146
+ return 0
147
+
148
+
149
+ if __name__ == "__main__":
150
+ raise SystemExit(main())
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "zipcode-search",
3
+ "description": "Look up a Korean postcode and official English address from a known address with the official ePost integrated search page.",
4
+ "profiles": [
5
+ "lookup"
6
+ ],
7
+ "frontmatter": "name: zipcode-search\ndescription: Look up a Korean postcode and official English address from a known address with the official ePost integrated search page.\nlicense: MIT\nmetadata:\n category: utility\n locale: ko-KR\n phase: v2"
8
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const packageRoot = path.join(__dirname, "..");
7
+ const templatesDir = path.join(packageRoot, "templates");
8
+ const skillsDir = path.join(packageRoot, "skills");
9
+
10
+ const KNOWN_PROFILES = [
11
+ "core",
12
+ "proxy",
13
+ "browser",
14
+ "vault",
15
+ "action:booking",
16
+ "action:commerce",
17
+ "action:communication",
18
+ "action:submission",
19
+ "action:account",
20
+ "hard-boundary",
21
+ "lookup",
22
+ "local",
23
+ ];
24
+
25
+ function profileTemplatePath(profile) {
26
+ return path.join(templatesDir, `${profile.replace(":", "-")}.md`);
27
+ }
28
+
29
+ function listSkills() {
30
+ if (!fs.existsSync(skillsDir)) return [];
31
+ return fs
32
+ .readdirSync(skillsDir, { withFileTypes: true })
33
+ .filter((entry) => entry.isDirectory())
34
+ .map((entry) => entry.name)
35
+ .filter((name) => fs.existsSync(path.join(skillsDir, name, "skill.json")))
36
+ .sort();
37
+ }
38
+
39
+ function loadSkill(skillName) {
40
+ const dir = path.join(skillsDir, skillName);
41
+ const manifestPath = path.join(dir, "skill.json");
42
+
43
+ if (!fs.existsSync(manifestPath)) {
44
+ const known = listSkills();
45
+ const error = new Error(
46
+ `unknown skill "${skillName}". Known skills: ${known.join(", ") || "(none bundled)"}`,
47
+ );
48
+ error.code = "EUNKNOWNSKILL";
49
+ throw error;
50
+ }
51
+
52
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
53
+
54
+ for (const profile of manifest.profiles ?? []) {
55
+ if (!KNOWN_PROFILES.includes(profile)) {
56
+ throw new Error(`skill "${skillName}" declares unknown profile "${profile}"`);
57
+ }
58
+ }
59
+
60
+ return {
61
+ dir,
62
+ manifest,
63
+ instruction: fs.readFileSync(path.join(dir, "instruction.md"), "utf8"),
64
+ };
65
+ }
66
+
67
+ // Template files interleave `<!-- mode:always -->`, `<!-- mode:dolshoi -->`,
68
+ // and `<!-- mode:generic -->` markers. Only the sections matching the detected
69
+ // mode (plus always-sections) are emitted.
70
+ function renderTemplate(raw, mode) {
71
+ const lines = raw.split("\n");
72
+ const output = [];
73
+ let current = "always";
74
+
75
+ for (const line of lines) {
76
+ const marker = line.match(/^<!-- mode:(always|dolshoi|generic) -->$/);
77
+ if (marker) {
78
+ current = marker[1];
79
+ continue;
80
+ }
81
+ if (current === "always" || current === mode) output.push(line);
82
+ }
83
+
84
+ return output.join("\n").trim();
85
+ }
86
+
87
+ function assemble(skillName, runtime) {
88
+ const { manifest, instruction } = loadSkill(skillName);
89
+ const profiles = ["core", ...(manifest.profiles ?? []).filter((p) => p !== "core")];
90
+
91
+ const blocks = profiles
92
+ .map((profile) => {
93
+ const templatePath = profileTemplatePath(profile);
94
+ const rendered = renderTemplate(fs.readFileSync(templatePath, "utf8"), runtime.mode);
95
+ return rendered;
96
+ })
97
+ .filter(Boolean);
98
+
99
+ const header = [
100
+ `# ${manifest.name} — assembled instructions`,
101
+ "",
102
+ `Runtime mode: ${runtime.mode}${runtime.cloakBrowser ? " (CloakBrowser available)" : ""}`,
103
+ "",
104
+ "## Runtime rules",
105
+ "",
106
+ blocks.join("\n"),
107
+ ].join("\n");
108
+
109
+ // instruction.md may also use mode markers for mode-specific sections.
110
+ return `${header}\n\n${renderTemplate(instruction, runtime.mode)}\n`;
111
+ }
112
+
113
+ function bundledFiles(skillName) {
114
+ const { dir } = loadSkill(skillName);
115
+ const results = [];
116
+
117
+ for (const sub of ["scripts", "references", "tests"]) {
118
+ const subDir = path.join(dir, sub);
119
+ if (!fs.existsSync(subDir)) continue;
120
+ const stack = [subDir];
121
+ while (stack.length) {
122
+ const currentDir = stack.pop();
123
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
124
+ const full = path.join(currentDir, entry.name);
125
+ if (entry.isDirectory()) stack.push(full);
126
+ else results.push(full);
127
+ }
128
+ }
129
+ }
130
+
131
+ return results.sort();
132
+ }
133
+
134
+ module.exports = { assemble, bundledFiles, listSkills, loadSkill, renderTemplate, KNOWN_PROFILES };