@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,108 @@
1
+ # 네이버 검색광고 성과 조회
2
+
3
+ ## What this skill does
4
+
5
+ 네이버 검색광고 API(HMAC-SHA256 서명 인증)를 감싸서 광고 계정의 성과 지표를 조회한다. **읽기 전용**이며 입찰가·캠페인·키워드를 바꾸는 쓰기 작업은 하지 않는다.
6
+
7
+ - 캠페인/광고그룹/키워드 목록 조회
8
+ - 기간별 성과(노출수·클릭수·광고비·CTR·CPC·평균노출순위·전환수) 조회
9
+ - 연관키워드·월간 조회수·경쟁정도 조회 (키워드 도구)
10
+ - 키 셋업 여부와 네이버 API 도달성 진단(`doctor`)
11
+
12
+ ## When to use
13
+
14
+ - "네이버 광고 노출수/클릭수/광고비/CTR 보여줘"
15
+ - "이번달 캠페인 성과 어때"
16
+ - "이 키워드 CTR·CPC 얼마야"
17
+ - "연관키워드랑 월간 조회수 뽑아줘"
18
+ - "네이버 검색광고 키 셋업이 됐는지 확인해줘"
19
+
20
+ ## When not to use
21
+
22
+ - **클라우드 샌드박스 환경**: 네이버 검색광고 API는 로컬 egress가 필요하다. 클라우드 실행 에이전트(허용 도메인 목록 방식)에서는 도달하지 못할 수 있다 — 먼저 `doctor`로 확인한다.
23
+ - **입찰가 변경, 캠페인/키워드 생성·수정·삭제**: 이 스킬은 조회 전용이다. 광고 운영에 영향을 주는 쓰기 작업은 이 스킬 범위 밖이며 구현돼 있지 않다.
24
+ - 네이버 검색(블로그/뉴스/쇼핑) 결과 조회는 `naver-blog-research`, `naver-news-search`, `naver-shopping-search` 스킬을 쓴다 — 이 스킬과는 다른 API다.
25
+
26
+ ## Prerequisites
27
+
28
+ - Python 3.9+
29
+ - 네이버 검색광고 계정 (searchad.naver.com) → **도구 > API 사용 관리**에서 API 키/시크릿 발급
30
+ - 환경변수 3개: `NAVER_AD_API_KEY`, `NAVER_AD_SECRET_KEY`, `NAVER_AD_CUSTOMER_ID`
31
+
32
+ 키는 사용자가 직접 발급한다. 돌쇠에서는 에이전트가 값을 보지 않도록 provisioned `vault-run` capability를 사용하고, 없으면 `request_vault_credential`로 앱 vault 입력 UI를 호출한다. generic fallback에서는 환경변수/host vault/`secrets.env` 순서를 사용한다.
33
+
34
+ ## Workflow
35
+
36
+ ### 1. 진단
37
+
38
+ ```bash
39
+ python3 naver-ad-performance/scripts/naver_ad_performance.py doctor
40
+ ```
41
+
42
+ 키 3개 존재 여부와 `api.searchad.naver.com` 도달 가능 여부를 보여준다. `reachable: false`면 로컬(클로드 코드/코덱스 CLI) 환경에서 다시 실행하도록 안내한다.
43
+
44
+ ### 2. 구조 조회 (id가 필요할 때)
45
+
46
+ ```bash
47
+ python3 naver-ad-performance/scripts/naver_ad_performance.py campaigns
48
+ python3 naver-ad-performance/scripts/naver_ad_performance.py adgroups --campaign <nccCampaignId>
49
+ python3 naver-ad-performance/scripts/naver_ad_performance.py keywords --adgroup <nccAdgroupId>
50
+ ```
51
+
52
+ ### 3. 성과 조회 (핵심)
53
+
54
+ ```bash
55
+ python3 naver-ad-performance/scripts/naver_ad_performance.py stats --ids <id1,id2> --since 2026-06-01 --until 2026-06-30
56
+ python3 naver-ad-performance/scripts/naver_ad_performance.py stats --ids <id> --since 2026-06-01 --until 2026-06-30 --by day
57
+ ```
58
+
59
+ `ids`는 캠페인/광고그룹/키워드 id를 콤마로 구분해 섞어 넣을 수 있다. `--since`/`--until`을 생략하면 네이버 기본 기간(최근)으로 조회된다.
60
+
61
+ ### 4. 키워드 도구
62
+
63
+ ```bash
64
+ python3 naver-ad-performance/scripts/naver_ad_performance.py keywordtool --keywords "제주여행,게스트하우스"
65
+ ```
66
+
67
+ ## Signing (참고)
68
+
69
+ 요청마다 `X-Timestamp`(ms epoch), `X-API-KEY`, `X-Customer`, `X-Signature` 헤더가 필요하다.
70
+
71
+ ```
72
+ message = "{timestamp}.{method}.{uri_path_only}" # 쿼리스트링 제외, method는 대문자
73
+ signature = base64(HMAC_SHA256(message, SECRET_KEY))
74
+ ```
75
+
76
+ ## Output fields
77
+
78
+ `stats`: `impCnt`(노출수), `clkCnt`(클릭수), `salesAmt`(광고비), `ctr`(CTR, 없으면 클릭수/노출수로 계산), `cpc`(CPC, 없으면 광고비/클릭수로 계산), `avgRnk`(평균노출순위), `ccnt`(전환수). 각 행에 한글 라벨(`labels`)을 덧붙인다.
79
+
80
+ `keywordtool`: `relKeyword`(연관키워드), `monthlyPcQcCnt`/`monthlyMobileQcCnt`(월간 PC/모바일 조회수), `compIdx`(경쟁정도) 등.
81
+
82
+ ## Scope (읽기 전용 — 이 경계를 넘지 않는다)
83
+
84
+ 허용: `/ncc/campaigns`, `/ncc/adgroups`, `/ncc/keywords`, `/stats`, `/keywordstool` GET 조회.
85
+
86
+ 금지 (구현하지 않음): 입찰가 변경, 캠페인/광고그룹/키워드 생성·수정·삭제, 예산 변경, 상태(운영중지 등) 토글. 돈이 직접 나가는 쓰기 작업과 조회를 안전 경계로 분리하기 위해 의도적으로 뺐다. 쓰기 기능이 필요해지면 별도 스킬/별도 승인 흐름으로 분리한다.
87
+
88
+ ## Failure modes
89
+
90
+ | 증상 | 원인 |
91
+ |---|---|
92
+ | `missing required env var(s): ...` | 환경변수 미설정. 정확히 어떤 이름이 빠졌는지 출력됨 |
93
+ | HTTP 401 | 서명 실패 — 키 값 또는 시스템 시계 확인 |
94
+ | HTTP 403 | 이 `customer_id`에 조회 권한 없음 |
95
+ | HTTP 404 | campaign/adgroup id가 잘못됨 |
96
+ | HTTP 429 | 호출 한도 초과 — 잠시 후 재시도 |
97
+ | `egress unreachable` | 클라우드 샌드박스 등에서 네이버로 나가는 네트워크가 막힘 — 로컬 실행 필요 |
98
+
99
+ ## Done when
100
+
101
+ - `stats` 명령이 지정한 id·기간에 대해 노출수·클릭수·광고비·CTR·CPC를 반환한다.
102
+ - 입찰가/캠페인/키워드를 바꾸는 어떤 요청도 보내지 않았다.
103
+ - 키가 없을 때 우회하지 않고 정확한 환경변수 이름을 안내했다.
104
+
105
+ ## Notes
106
+
107
+ - 대용량 기간 리포트(TSV 비동기 생성, `/stat-reports`)는 이번 v1 범위 밖이다. 필요해지면 별도로 추가한다.
108
+ - 참고 구현: [`NariP/naver-searchad`](https://github.com/NariP/naver-searchad) (MIT) — 서명 방식과 명령 설계를 참고했다.
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import base64
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import os
9
+ import sys
10
+ import time
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+
15
+ BASE_URL = "https://api.searchad.naver.com"
16
+ API_KEY_ENV = "NAVER_AD_API_KEY"
17
+ SECRET_KEY_ENV = "NAVER_AD_SECRET_KEY"
18
+ CUSTOMER_ID_ENV = "NAVER_AD_CUSTOMER_ID"
19
+ DEFAULT_TIMEOUT = 20
20
+
21
+ STATS_FIELDS = ["impCnt", "clkCnt", "salesAmt", "ctr", "cpc", "avgRnk", "ccnt"]
22
+ STATS_LABELS = {
23
+ "impCnt": "노출수",
24
+ "clkCnt": "클릭수",
25
+ "salesAmt": "광고비",
26
+ "ctr": "CTR",
27
+ "cpc": "CPC",
28
+ "avgRnk": "평균노출순위",
29
+ "ccnt": "전환수",
30
+ }
31
+ KEYWORDTOOL_LABELS = {
32
+ "relKeyword": "연관키워드",
33
+ "monthlyPcQcCnt": "월간PC조회수",
34
+ "monthlyMobileQcCnt": "월간모바일조회수",
35
+ "monthlyAvePcClkCnt": "월평균PC클릭수",
36
+ "monthlyAveMobileClkCnt": "월평균모바일클릭수",
37
+ "compIdx": "경쟁정도",
38
+ }
39
+
40
+ # 이 스킬은 읽기 전용이다. 캠페인/그룹/키워드/입찰가를 바꾸는 write 엔드포인트는
41
+ # 의도적으로 구현하지 않는다 (docs 참고: naver-ad-performance/SKILL.md의 "쓰기 금지").
42
+
43
+
44
+ class CredentialError(RuntimeError):
45
+ pass
46
+
47
+
48
+ class ApiError(RuntimeError):
49
+ def __init__(self, status: int, body: str):
50
+ super().__init__(f"HTTP {status}: {body}")
51
+ self.status = status
52
+ self.body = body
53
+
54
+
55
+ def resolve_credentials() -> tuple[str, str, str]:
56
+ api_key = os.getenv(API_KEY_ENV)
57
+ secret_key = os.getenv(SECRET_KEY_ENV)
58
+ customer_id = os.getenv(CUSTOMER_ID_ENV)
59
+ missing = [
60
+ name
61
+ for name, value in (
62
+ (API_KEY_ENV, api_key),
63
+ (SECRET_KEY_ENV, secret_key),
64
+ (CUSTOMER_ID_ENV, customer_id),
65
+ )
66
+ if not value
67
+ ]
68
+ if missing:
69
+ raise CredentialError(
70
+ "missing required env var(s): " + ", ".join(missing) + ". "
71
+ "발급: 네이버 검색광고 > 도구 > API 사용 관리."
72
+ )
73
+ return api_key, secret_key, customer_id
74
+
75
+
76
+ def build_signature(secret_key: str, timestamp: str, method: str, uri: str) -> str:
77
+ message = f"{timestamp}.{method}.{uri}".encode("utf-8")
78
+ digest = hmac.new(secret_key.encode("utf-8"), message, hashlib.sha256).digest()
79
+ return base64.b64encode(digest).decode("utf-8")
80
+
81
+
82
+ def request(method: str, uri: str, params: dict | None = None, timeout: int = DEFAULT_TIMEOUT):
83
+ api_key, secret_key, customer_id = resolve_credentials()
84
+ timestamp = str(round(time.time() * 1000))
85
+ signature = build_signature(secret_key, timestamp, method, uri)
86
+ headers = {
87
+ "X-Timestamp": timestamp,
88
+ "X-API-KEY": api_key,
89
+ "X-Customer": customer_id,
90
+ "X-Signature": signature,
91
+ "Accept": "application/json",
92
+ }
93
+ url = BASE_URL + uri
94
+ if params:
95
+ url += "?" + urllib.parse.urlencode(params)
96
+ req = urllib.request.Request(url, headers=headers, method=method)
97
+ try:
98
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
99
+ return json.loads(resp.read().decode("utf-8"))
100
+ except urllib.error.HTTPError as exc:
101
+ body = exc.read().decode("utf-8", errors="replace")
102
+ raise ApiError(exc.code, body) from exc
103
+ except urllib.error.URLError as exc:
104
+ raise ApiError(0, f"egress unreachable: {exc.reason}") from exc
105
+
106
+
107
+ def with_derived_stats(row: dict) -> dict:
108
+ imp = row.get("impCnt") or 0
109
+ clk = row.get("clkCnt") or 0
110
+ sales = row.get("salesAmt") or 0
111
+ if "ctr" not in row:
112
+ row["ctr"] = round(clk / imp * 100, 2) if imp else 0
113
+ if "cpc" not in row:
114
+ row["cpc"] = round(sales / clk, 2) if clk else 0
115
+ return row
116
+
117
+
118
+ def label_stats(rows: list[dict]) -> list[dict]:
119
+ labeled = []
120
+ for row in rows:
121
+ row = with_derived_stats(dict(row))
122
+ row["labels"] = {field: STATS_LABELS[field] for field in STATS_FIELDS if field in row}
123
+ labeled.append(row)
124
+ return labeled
125
+
126
+
127
+ def label_keywordtool(rows: list[dict]) -> list[dict]:
128
+ labeled = []
129
+ for row in rows:
130
+ row = dict(row)
131
+ row["labels"] = {field: label for field, label in KEYWORDTOOL_LABELS.items() if field in row}
132
+ labeled.append(row)
133
+ return labeled
134
+
135
+
136
+ def cmd_doctor(_args: argparse.Namespace) -> dict:
137
+ result = {"env": {}, "reachable": None}
138
+ for name in (API_KEY_ENV, SECRET_KEY_ENV, CUSTOMER_ID_ENV):
139
+ result["env"][name] = bool(os.getenv(name))
140
+ try:
141
+ req = urllib.request.Request(BASE_URL, method="GET")
142
+ urllib.request.urlopen(req, timeout=8).close()
143
+ result["reachable"] = True
144
+ except urllib.error.HTTPError:
145
+ result["reachable"] = True # HTTP 응답을 받았다는 것 자체가 egress는 열려있다는 뜻
146
+ except urllib.error.URLError as exc:
147
+ result["reachable"] = False
148
+ result["reachable_error"] = str(exc.reason)
149
+ return result
150
+
151
+
152
+ def cmd_campaigns(_args: argparse.Namespace) -> list[dict]:
153
+ return request("GET", "/ncc/campaigns")
154
+
155
+
156
+ def cmd_adgroups(args: argparse.Namespace) -> list[dict]:
157
+ return request("GET", "/ncc/adgroups", {"nccCampaignId": args.campaign})
158
+
159
+
160
+ def cmd_keywords(args: argparse.Namespace) -> list[dict]:
161
+ return request("GET", "/ncc/keywords", {"nccAdgroupId": args.adgroup})
162
+
163
+
164
+ def cmd_stats(args: argparse.Namespace) -> list[dict]:
165
+ ids = [i.strip() for i in args.ids.split(",") if i.strip()]
166
+ params = {
167
+ "ids": json.dumps(ids),
168
+ "fields": json.dumps(STATS_FIELDS),
169
+ }
170
+ if args.since or args.until:
171
+ if not (args.since and args.until):
172
+ raise CredentialError("--since and --until must be given together")
173
+ params["timeRange"] = json.dumps({"since": args.since, "until": args.until})
174
+ if args.by:
175
+ params["breakdown"] = args.by
176
+ data = request("GET", "/stats", params)
177
+ rows = data.get("data", data) if isinstance(data, dict) else data
178
+ return label_stats(rows)
179
+
180
+
181
+ def cmd_keywordtool(args: argparse.Namespace) -> list[dict]:
182
+ params = {"hintKeywords": args.keywords, "showDetail": 1}
183
+ data = request("GET", "/keywordstool", params)
184
+ rows = data.get("keywordList", data) if isinstance(data, dict) else data
185
+ return label_keywordtool(rows)
186
+
187
+
188
+ def build_parser() -> argparse.ArgumentParser:
189
+ p = argparse.ArgumentParser(description="네이버 검색광고 성과 조회 (읽기 전용)")
190
+ sub = p.add_subparsers(dest="command", required=True)
191
+
192
+ sub.add_parser("doctor", help="키 존재 여부와 네이버 도달성 점검").set_defaults(func=cmd_doctor)
193
+ sub.add_parser("campaigns", help="캠페인 목록").set_defaults(func=cmd_campaigns)
194
+
195
+ ag = sub.add_parser("adgroups", help="광고그룹 목록")
196
+ ag.add_argument("--campaign", required=True, help="nccCampaignId")
197
+ ag.set_defaults(func=cmd_adgroups)
198
+
199
+ kw = sub.add_parser("keywords", help="키워드 목록")
200
+ kw.add_argument("--adgroup", required=True, help="nccAdgroupId")
201
+ kw.set_defaults(func=cmd_keywords)
202
+
203
+ st = sub.add_parser("stats", help="성과 조회 (노출수·클릭수·광고비·CTR·CPC·평균순위·전환수)")
204
+ st.add_argument("--ids", required=True, help="캠페인/그룹/키워드 id, 콤마구분")
205
+ st.add_argument("--since", help="YYYY-MM-DD")
206
+ st.add_argument("--until", help="YYYY-MM-DD")
207
+ st.add_argument("--by", choices=["day"], help="breakdown 단위")
208
+ st.set_defaults(func=cmd_stats)
209
+
210
+ kt = sub.add_parser("keywordtool", help="연관키워드·월간 조회수·경쟁정도")
211
+ kt.add_argument("--keywords", required=True, help="힌트 키워드, 콤마구분")
212
+ kt.set_defaults(func=cmd_keywordtool)
213
+
214
+ return p
215
+
216
+
217
+ def main(argv: list[str] | None = None) -> int:
218
+ args = build_parser().parse_args(argv)
219
+ try:
220
+ result = args.func(args)
221
+ except CredentialError as exc:
222
+ print(str(exc), file=sys.stderr)
223
+ return 1
224
+ except ApiError as exc:
225
+ hints = {
226
+ 401: "서명 실패 — 키가 맞는지 / 시스템 시계가 정확한지 확인",
227
+ 403: "권한 없음 — 이 customer_id에 조회 권한이 있는지 확인",
228
+ 404: "id를 찾을 수 없음 — campaign/adgroup id 확인",
229
+ 429: "호출 한도 초과 — 잠시 후 재시도",
230
+ 0: "egress 차단 — 로컬 환경(클로드 코드/코덱스)에서 실행했는지 확인",
231
+ }
232
+ hint = hints.get(exc.status, "")
233
+ print(f"{exc}" + (f" ({hint})" if hint else ""), file=sys.stderr)
234
+ return 2
235
+ print(json.dumps(result, ensure_ascii=False, indent=2))
236
+ return 0
237
+
238
+
239
+ if __name__ == "__main__":
240
+ sys.exit(main())
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "naver-ad-performance",
3
+ "description": "네이버 검색광고(키워드광고)의 노출수·클릭수·광고비·CTR·CPC·전환수를 조회한다. 캠페인/광고그룹/키워드 목록, 기간별 성과, 연관키워드·월간 조회수 도구 포함. 읽기 전용, 로컬 실행 전용. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
4
+ "profiles": [
5
+ "vault",
6
+ "action:account"
7
+ ],
8
+ "frontmatter": "name: naver-ad-performance\ndescription: 네이버 검색광고(키워드광고)의 노출수·클릭수·광고비·CTR·CPC·전환수를 조회한다. 캠페인/광고그룹/키워드 목록, 기간별 성과, 연관키워드·월간 조회수 도구 포함. 읽기 전용, 로컬 실행 전용. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: marketing\n locale: ko-KR\n phase: v1"
9
+ }
@@ -0,0 +1,128 @@
1
+ # 네이버 블로그 리서치
2
+
3
+ ## What this skill does
4
+
5
+ 네이버 블로그를 검색하고, 개별 포스트의 원문을 읽고, 이미지를 로컬에 다운로드한다.
6
+
7
+ - API 키 없이 `python3` 표준 라이브러리만으로 동작한다.
8
+ - 검색 결과를 구조화된 JSON으로 출력한다.
9
+ - 모바일 버전(`m.blog.naver.com`)을 이용해 iframe 없이 본문을 직접 추출한다.
10
+ - 블로그 이미지 CDN(`blogfiles.naver.net`, `postfiles.pstatic.net`)에서 이미지를 다운로드한다.
11
+
12
+ ## When to use
13
+
14
+ - "네이버 블로그에서 결혼식 체크리스트 검색해줘"
15
+ - "네이버 블로그 리서치 해줘"
16
+ - "한국 블로그에서 관련 정보 조사해줘"
17
+ - "네이버 블로그 글 읽어줘"
18
+ - "이 네이버 블로그 포스트에서 이미지 다운로드해줘"
19
+ - 한국어 콘텐츠 리서치에서 구글 외 네이버 블로그 소스가 필요한 상황
20
+
21
+ ## When not to use
22
+
23
+ - 네이버 뉴스, 카페, 지식iN 등 블로그 외 네이버 서비스 검색
24
+ - 대량 크롤링/스크래핑 (한 세션에 수십 건 이상의 요청)
25
+ - 상업적 데이터 수집
26
+
27
+ ## Prerequisites
28
+
29
+ - 인터넷 연결
30
+ - `python3` 3.8+
31
+ - 이 스킬 디렉토리의 `scripts/` 안에 포함된 helper 스크립트
32
+
33
+ ## Workflow
34
+
35
+ ### 1. 네이버 블로그 검색
36
+
37
+ ```bash
38
+ python3 scripts/naver_search.py "검색어" --count 10 --sort sim
39
+ ```
40
+
41
+ | 인자 | 필수 | 설명 | 기본값 |
42
+ |------|------|------|--------|
43
+ | query | O | 검색어 | - |
44
+ | --count | X | 결과 수 (최대 30) | 10 |
45
+ | --sort | X | sim(관련도), date(최신) | sim |
46
+ | --timeout | X | 요청 타임아웃(초) | 15 |
47
+
48
+ 출력 예시:
49
+
50
+ ```json
51
+ {
52
+ "query": "결혼식 체크리스트",
53
+ "total_results": 7,
54
+ "results": [
55
+ {
56
+ "title": "결혼식 체크리스트 총정리",
57
+ "url": "https://blog.naver.com/user123/224212849946",
58
+ "mobile_url": "https://m.blog.naver.com/user123/224212849946",
59
+ "snippet": "결혼식 1주일 전에 반드시 확인해야 할...",
60
+ "author": "user123"
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+
66
+ ### 2. 블로그 원문 읽기
67
+
68
+ 검색 결과에서 관심 있는 포스트의 URL을 선택하여 원문을 읽는다.
69
+
70
+ ```bash
71
+ python3 scripts/naver_read.py "https://blog.naver.com/user123/224212849946"
72
+ ```
73
+
74
+ | 인자 | 필수 | 설명 | 기본값 |
75
+ |------|------|------|--------|
76
+ | url | O | 블로그 포스트 URL (PC 또는 모바일) | - |
77
+ | --no-images | X | 이미지 URL 제외 | false |
78
+ | --max-length | X | 본문 최대 글자 수 (0=무제한) | 0 |
79
+ | --timeout | X | 요청 타임아웃(초) | 20 |
80
+
81
+ PC URL을 넣어도 자동으로 모바일 URL로 변환하여 요청한다.
82
+
83
+ ### 3. 이미지 다운로드 (필요 시)
84
+
85
+ ```bash
86
+ python3 scripts/naver_download_images.py --urls "url1,url2,url3" --output ./images/
87
+ ```
88
+
89
+ 또는 `naver_read.py` 결과를 파이프로 전달:
90
+
91
+ ```bash
92
+ python3 scripts/naver_read.py "https://..." | python3 scripts/naver_download_images.py --output ./images/
93
+ ```
94
+
95
+ | 인자 | 필수 | 설명 | 기본값 |
96
+ |------|------|------|--------|
97
+ | --urls | X | 쉼표 구분 이미지 URL | - |
98
+ | --output | X | 저장 디렉토리 | ./naver-images/ |
99
+ | --max | X | 최대 다운로드 수 | 10 |
100
+ | --timeout | X | 요청 타임아웃(초) | 15 |
101
+
102
+ ### 추천 워크플로우
103
+
104
+ 1. `naver_search.py`로 검색 → 상위 3~5개 결과 확인
105
+ 2. 관련도 높은 포스트를 `naver_read.py`로 원문 읽기
106
+ 3. 필요 시 `naver_download_images.py`로 이미지 저장
107
+ 4. WebSearch(구글) 결과와 교차 검증하여 정보 신뢰도 높이기
108
+
109
+ ## Response policy
110
+
111
+ - 검색 결과와 본문은 사용자에게 요약하여 전달한다.
112
+ - 블로그 출처(URL, 작성자)를 반드시 함께 안내한다.
113
+ - 한 세션에 과도한 요청(수십 건 이상)을 자제한다.
114
+ - 이미지 다운로드 시 사용자에게 저장 경로를 안내한다.
115
+
116
+ ## Done when
117
+
118
+ - 검색 결과가 JSON으로 정상 출력된다.
119
+ - 블로그 원문 텍스트가 추출된다.
120
+ - 필요한 이미지가 로컬에 저장된다.
121
+ - 출처가 명시된다.
122
+
123
+ ## Notes
124
+
125
+ - 네이버 검색엔진을 직접 요청하므로 대량/자동화 사용 시 IP 차단 가능성이 있다.
126
+ - 이 스킬은 소량, 비상업적 콘텐츠 리서치 용도로 설계되었다.
127
+ - 네이버 HTML 구조는 변경될 수 있어, 파싱 실패 시 에러 메시지를 확인하고 스크립트 업데이트가 필요할 수 있다.
128
+ - PC 버전(`blog.naver.com`)은 iframe 구조여서 모바일 버전(`m.blog.naver.com`)을 사용한다.
@@ -0,0 +1,58 @@
1
+ """Shared HTTP utilities for Naver blog scripts (SSL handling, URL validation, urlopen wrapper)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import ssl
7
+ import sys
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+
12
+
13
+ TAG_RE = re.compile(r"<[^>]+>")
14
+
15
+ _ssl_ctx_secure: ssl.SSLContext | None = None
16
+ _ssl_ctx_insecure: ssl.SSLContext | None = None
17
+
18
+
19
+ def _get_ssl_context(*, insecure: bool = False) -> ssl.SSLContext:
20
+ global _ssl_ctx_secure, _ssl_ctx_insecure
21
+ if insecure:
22
+ if _ssl_ctx_insecure is None:
23
+ ctx = ssl.create_default_context()
24
+ ctx.check_hostname = False
25
+ ctx.verify_mode = ssl.CERT_NONE
26
+ _ssl_ctx_insecure = ctx
27
+ return _ssl_ctx_insecure
28
+ if _ssl_ctx_secure is None:
29
+ _ssl_ctx_secure = ssl.create_default_context()
30
+ return _ssl_ctx_secure
31
+
32
+
33
+ _NAVER_DOMAINS = (".naver.com", ".naver.net", ".pstatic.net")
34
+
35
+
36
+ def is_naver_url(url: str) -> bool:
37
+ host = urllib.parse.urlparse(url).hostname or ""
38
+ return any(host == d.lstrip(".") or host.endswith(d) for d in _NAVER_DOMAINS)
39
+
40
+
41
+ def urlopen(request: urllib.request.Request, timeout: int, *, insecure: bool = False):
42
+ """urlopen with explicit SSL insecure mode for Naver domains.
43
+
44
+ When *insecure* is True and the target is a Naver domain, SSL certificate
45
+ verification is skipped. A warning is printed to stderr on every call so
46
+ the caller is always aware.
47
+ """
48
+ if insecure:
49
+ if not is_naver_url(request.full_url):
50
+ raise ValueError("insecure 모드는 네이버 도메인에만 사용할 수 있습니다.")
51
+ print(
52
+ "[warn] SSL 인증서 검증이 비활성화되었습니다. 연결이 안전하지 않을 수 있습니다.",
53
+ file=sys.stderr,
54
+ )
55
+ return urllib.request.urlopen(
56
+ request, timeout=timeout, context=_get_ssl_context(insecure=True),
57
+ )
58
+ return urllib.request.urlopen(request, timeout=timeout, context=_get_ssl_context())