@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.
- package/bin/k-skill.js +60 -0
- package/package.json +37 -0
- package/skills/assembly-bill-vote-search/instruction.md +111 -0
- package/skills/assembly-bill-vote-search/skill.json +10 -0
- package/skills/biz-health-check/instruction.md +69 -0
- package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
- package/skills/biz-health-check/skill.json +10 -0
- package/skills/bok-ecos-stats/instruction.md +95 -0
- package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
- package/skills/bok-ecos-stats/skill.json +10 -0
- package/skills/building-register-search/instruction.md +69 -0
- package/skills/building-register-search/scripts/building_register.py +309 -0
- package/skills/building-register-search/scripts/building_register_xml.py +32 -0
- package/skills/building-register-search/skill.json +9 -0
- package/skills/bunjang-search/instruction.md +154 -0
- package/skills/bunjang-search/skill.json +10 -0
- package/skills/catchtable-sniper/instruction.md +270 -0
- package/skills/catchtable-sniper/skill.json +9 -0
- package/skills/cheap-gas-nearby/instruction.md +107 -0
- package/skills/cheap-gas-nearby/skill.json +10 -0
- package/skills/corporate-registration-consulting/instruction.md +117 -0
- package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
- package/skills/corporate-registration-consulting/skill.json +8 -0
- package/skills/coupang-product-search/instruction.md +219 -0
- package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
- package/skills/coupang-product-search/skill.json +10 -0
- package/skills/court-auction-notice-search/instruction.md +201 -0
- package/skills/court-auction-notice-search/skill.json +9 -0
- package/skills/court-payment-order-assistant/instruction.md +120 -0
- package/skills/court-payment-order-assistant/skill.json +10 -0
- package/skills/d2b-notice-search/instruction.md +117 -0
- package/skills/d2b-notice-search/skill.json +10 -0
- package/skills/daangn-cars-search/instruction.md +91 -0
- package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
- package/skills/daangn-cars-search/skill.json +9 -0
- package/skills/daangn-jobs-search/instruction.md +90 -0
- package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
- package/skills/daangn-jobs-search/skill.json +9 -0
- package/skills/daangn-realty-search/instruction.md +114 -0
- package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
- package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
- package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
- package/skills/daangn-realty-search/skill.json +8 -0
- package/skills/daangn-used-goods-search/instruction.md +90 -0
- package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
- package/skills/daangn-used-goods-search/skill.json +9 -0
- package/skills/daishin-report-search/instruction.md +138 -0
- package/skills/daishin-report-search/skill.json +10 -0
- package/skills/daiso-product-search/instruction.md +172 -0
- package/skills/daiso-product-search/skill.json +8 -0
- package/skills/danawa-price-search/instruction.md +183 -0
- package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
- package/skills/danawa-price-search/skill.json +8 -0
- package/skills/delivery-tracking/instruction.md +358 -0
- package/skills/delivery-tracking/skill.json +8 -0
- package/skills/donation-place-search/instruction.md +129 -0
- package/skills/donation-place-search/skill.json +9 -0
- package/skills/emergency-room-beds/instruction.md +82 -0
- package/skills/emergency-room-beds/skill.json +8 -0
- package/skills/ev-charger-nearby/instruction.md +80 -0
- package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
- package/skills/ev-charger-nearby/skill.json +10 -0
- package/skills/ev-subsidy-status/instruction.md +165 -0
- package/skills/ev-subsidy-status/skill.json +10 -0
- package/skills/express-bus-booking/instruction.md +207 -0
- package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
- package/skills/express-bus-booking/skill.json +8 -0
- package/skills/fine-dust-location/instruction.md +89 -0
- package/skills/fine-dust-location/skill.json +9 -0
- package/skills/flight-ticket-search/instruction.md +237 -0
- package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
- package/skills/flight-ticket-search/skill.json +9 -0
- package/skills/foresttrip-vacancy/instruction.md +167 -0
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
- package/skills/foresttrip-vacancy/skill.json +10 -0
- package/skills/fsc-corporate-info/instruction.md +57 -0
- package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
- package/skills/fsc-corporate-info/skill.json +10 -0
- package/skills/g2b-order-plan-search/instruction.md +131 -0
- package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
- package/skills/g2b-order-plan-search/skill.json +10 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
- package/skills/g2b-sanctioned-supplier/skill.json +10 -0
- package/skills/gangnamunni-clinic-search/instruction.md +113 -0
- package/skills/gangnamunni-clinic-search/skill.json +9 -0
- package/skills/geeknews-search/instruction.md +69 -0
- package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
- package/skills/geeknews-search/skill.json +8 -0
- package/skills/gongsijiga-search/instruction.md +128 -0
- package/skills/gongsijiga-search/skill.json +9 -0
- package/skills/gov-overseas-trip-report/instruction.md +488 -0
- package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
- package/skills/gov-overseas-trip-report/skill.json +9 -0
- package/skills/han-river-water-level/instruction.md +83 -0
- package/skills/han-river-water-level/skill.json +10 -0
- package/skills/highway-traffic-status/instruction.md +88 -0
- package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
- package/skills/highway-traffic-status/skill.json +11 -0
- package/skills/hipass-receipt/instruction.md +97 -0
- package/skills/hipass-receipt/skill.json +10 -0
- package/skills/hola-poke-yeoksam/instruction.md +247 -0
- package/skills/hola-poke-yeoksam/skill.json +8 -0
- package/skills/household-waste-info/instruction.md +117 -0
- package/skills/household-waste-info/skill.json +10 -0
- package/skills/housing-official-price/instruction.md +177 -0
- package/skills/housing-official-price/skill.json +10 -0
- package/skills/hwp/instruction.md +206 -0
- package/skills/hwp/skill.json +8 -0
- package/skills/intercity-bus-booking/instruction.md +189 -0
- package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
- package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
- package/skills/intercity-bus-booking/skill.json +8 -0
- package/skills/iros-registry-automation/instruction.md +229 -0
- package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
- package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
- package/skills/iros-registry-automation/skill.json +9 -0
- package/skills/job-posting-match/instruction.md +130 -0
- package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
- package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
- package/skills/job-posting-match/skill.json +8 -0
- package/skills/jobkorea-talent-search/instruction.md +118 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
- package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
- package/skills/jobkorea-talent-search/skill.json +9 -0
- package/skills/joseon-sillok-search/instruction.md +76 -0
- package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
- package/skills/joseon-sillok-search/skill.json +8 -0
- package/skills/k-dart/instruction.md +406 -0
- package/skills/k-dart/skill.json +8 -0
- package/skills/k-schoollunch-menu/instruction.md +109 -0
- package/skills/k-schoollunch-menu/skill.json +9 -0
- package/skills/k-skill-cleaner/instruction.md +80 -0
- package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
- package/skills/k-skill-cleaner/skill.json +8 -0
- package/skills/k-skill-setup/instruction.md +253 -0
- package/skills/k-skill-setup/skill.json +11 -0
- package/skills/kakao-bar-nearby/instruction.md +76 -0
- package/skills/kakao-bar-nearby/skill.json +8 -0
- package/skills/kakao-map/instruction.md +176 -0
- package/skills/kakao-map/skill.json +10 -0
- package/skills/kakaotalk-mac/instruction.md +189 -0
- package/skills/kakaotalk-mac/skill.json +8 -0
- package/skills/kbl-results/instruction.md +89 -0
- package/skills/kbl-results/skill.json +9 -0
- package/skills/kbo-results/instruction.md +82 -0
- package/skills/kbo-results/skill.json +8 -0
- package/skills/keris-academic-search/instruction.md +81 -0
- package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
- package/skills/keris-academic-search/skill.json +10 -0
- package/skills/kleague-results/instruction.md +92 -0
- package/skills/kleague-results/skill.json +8 -0
- package/skills/kopis-performance-search/instruction.md +109 -0
- package/skills/kopis-performance-search/skill.json +10 -0
- package/skills/korea-weather/instruction.md +93 -0
- package/skills/korea-weather/skill.json +10 -0
- package/skills/korean-character-count/instruction.md +87 -0
- package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
- package/skills/korean-character-count/skill.json +8 -0
- package/skills/korean-cinema-search/instruction.md +177 -0
- package/skills/korean-cinema-search/skill.json +8 -0
- package/skills/korean-heritage-search/instruction.md +106 -0
- package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
- package/skills/korean-heritage-search/skill.json +8 -0
- package/skills/korean-holiday-calendar/instruction.md +97 -0
- package/skills/korean-holiday-calendar/skill.json +9 -0
- package/skills/korean-humanizer/instruction.md +389 -0
- package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
- package/skills/korean-humanizer/skill.json +8 -0
- package/skills/korean-jangbu-for/instruction.md +133 -0
- package/skills/korean-jangbu-for/scripts/install.sh +237 -0
- package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
- package/skills/korean-jangbu-for/skill.json +8 -0
- package/skills/korean-law-search/instruction.md +126 -0
- package/skills/korean-law-search/skill.json +10 -0
- package/skills/korean-marathon-schedule/instruction.md +111 -0
- package/skills/korean-marathon-schedule/skill.json +9 -0
- package/skills/korean-middle-korean/instruction.md +79 -0
- package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
- package/skills/korean-middle-korean/skill.json +8 -0
- package/skills/korean-patent-search/instruction.md +79 -0
- package/skills/korean-patent-search/scripts/patent_search.py +409 -0
- package/skills/korean-patent-search/skill.json +9 -0
- package/skills/korean-privacy-terms/instruction.md +128 -0
- package/skills/korean-privacy-terms/scripts/install.sh +108 -0
- package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
- package/skills/korean-privacy-terms/skill.json +8 -0
- package/skills/korean-scholarship-search/instruction.md +317 -0
- package/skills/korean-scholarship-search/references/report-format.md +40 -0
- package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
- package/skills/korean-scholarship-search/references/search-clues.md +58 -0
- package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
- package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
- package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
- package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
- package/skills/korean-scholarship-search/skill.json +8 -0
- package/skills/korean-slang-writing/instruction.md +181 -0
- package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
- package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
- package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
- package/skills/korean-slang-writing/skill.json +8 -0
- package/skills/korean-spell-check/instruction.md +105 -0
- package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
- package/skills/korean-spell-check/skill.json +9 -0
- package/skills/korean-stock-search/instruction.md +194 -0
- package/skills/korean-stock-search/skill.json +10 -0
- package/skills/korean-transit-route/instruction.md +113 -0
- package/skills/korean-transit-route/skill.json +10 -0
- package/skills/kosis-stats/instruction.md +232 -0
- package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
- package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
- package/skills/kosis-stats/skill.json +9 -0
- package/skills/kr-whois-lookup/instruction.md +107 -0
- package/skills/kr-whois-lookup/skill.json +10 -0
- package/skills/kstartup-search/instruction.md +186 -0
- package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
- package/skills/kstartup-search/skill.json +11 -0
- package/skills/ktx-booking/instruction.md +244 -0
- package/skills/ktx-booking/skill.json +10 -0
- package/skills/lck-analytics/instruction.md +192 -0
- package/skills/lck-analytics/scripts/_lib.js +103 -0
- package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
- package/skills/lck-analytics/scripts/build-match-report.js +44 -0
- package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
- package/skills/lck-analytics/skill.json +9 -0
- package/skills/lh-notice-search/instruction.md +206 -0
- package/skills/lh-notice-search/skill.json +10 -0
- package/skills/library-book-search/instruction.md +139 -0
- package/skills/library-book-search/skill.json +9 -0
- package/skills/local-election-candidate-search/instruction.md +77 -0
- package/skills/local-election-candidate-search/skill.json +8 -0
- package/skills/localdata-business-status/instruction.md +64 -0
- package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
- package/skills/localdata-business-status/skill.json +8 -0
- package/skills/lotto-results/instruction.md +80 -0
- package/skills/lotto-results/skill.json +8 -0
- package/skills/lovebug-report/instruction.md +185 -0
- package/skills/lovebug-report/skill.json +9 -0
- package/skills/market-kurly-search/instruction.md +125 -0
- package/skills/market-kurly-search/skill.json +8 -0
- package/skills/mfds-drug-safety/instruction.md +86 -0
- package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
- package/skills/mfds-drug-safety/skill.json +10 -0
- package/skills/mfds-food-safety/instruction.md +126 -0
- package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
- package/skills/mfds-food-safety/skill.json +10 -0
- package/skills/myrealtrip-search/instruction.md +239 -0
- package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
- package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
- package/skills/myrealtrip-search/skill.json +9 -0
- package/skills/naming-house/instruction.md +146 -0
- package/skills/naming-house/skill.json +8 -0
- package/skills/national-pension-workplace/instruction.md +64 -0
- package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
- package/skills/national-pension-workplace/skill.json +10 -0
- package/skills/naver-ad-performance/instruction.md +108 -0
- package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
- package/skills/naver-ad-performance/skill.json +9 -0
- package/skills/naver-blog-research/instruction.md +128 -0
- package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
- package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
- package/skills/naver-blog-research/scripts/naver_read.py +256 -0
- package/skills/naver-blog-research/scripts/naver_search.py +192 -0
- package/skills/naver-blog-research/skill.json +8 -0
- package/skills/naver-news-search/instruction.md +103 -0
- package/skills/naver-news-search/skill.json +10 -0
- package/skills/naver-shopping-search/instruction.md +94 -0
- package/skills/naver-shopping-search/skill.json +11 -0
- package/skills/nhis-care-checkup-search/instruction.md +116 -0
- package/skills/nhis-care-checkup-search/skill.json +10 -0
- package/skills/nts-business-registration/instruction.md +115 -0
- package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
- package/skills/nts-business-registration/skill.json +10 -0
- package/skills/nts-tax-delinquency/instruction.md +54 -0
- package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
- package/skills/nts-tax-delinquency/skill.json +8 -0
- package/skills/ohou-today-deal/instruction.md +182 -0
- package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
- package/skills/ohou-today-deal/skill.json +9 -0
- package/skills/olive-young-search/instruction.md +154 -0
- package/skills/olive-young-search/skill.json +8 -0
- package/skills/parking-lot-search/instruction.md +96 -0
- package/skills/parking-lot-search/skill.json +9 -0
- package/skills/popbill/instruction.md +146 -0
- package/skills/popbill/scripts/popbill_cli.py +284 -0
- package/skills/popbill/scripts/popbill_registry.py +54 -0
- package/skills/popbill/scripts/popbill_safety.py +37 -0
- package/skills/popbill/scripts/popbill_templates.py +71 -0
- package/skills/popbill/skill.json +9 -0
- package/skills/public-restroom-nearby/instruction.md +89 -0
- package/skills/public-restroom-nearby/skill.json +9 -0
- package/skills/real-estate-search/instruction.md +172 -0
- package/skills/real-estate-search/skill.json +10 -0
- package/skills/rhwp-advanced/instruction.md +145 -0
- package/skills/rhwp-advanced/skill.json +8 -0
- package/skills/rhwp-edit/instruction.md +153 -0
- package/skills/rhwp-edit/skill.json +8 -0
- package/skills/s2b-notice-search/instruction.md +66 -0
- package/skills/s2b-notice-search/skill.json +9 -0
- package/skills/saju-fortune/instruction.md +177 -0
- package/skills/saju-fortune/skill.json +8 -0
- package/skills/saramin-talent-search/instruction.md +119 -0
- package/skills/saramin-talent-search/skill.json +9 -0
- package/skills/seoul-bike/instruction.md +83 -0
- package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
- package/skills/seoul-bike/skill.json +10 -0
- package/skills/seoul-density/instruction.md +109 -0
- package/skills/seoul-density/scripts/seoul_density.py +271 -0
- package/skills/seoul-density/skill.json +10 -0
- package/skills/seoul-subway-arrival/instruction.md +85 -0
- package/skills/seoul-subway-arrival/skill.json +9 -0
- package/skills/sh-notice-search/instruction.md +150 -0
- package/skills/sh-notice-search/skill.json +9 -0
- package/skills/srt-booking/instruction.md +181 -0
- package/skills/srt-booking/scripts/srt_booking.py +272 -0
- package/skills/srt-booking/scripts/srt_seats.py +156 -0
- package/skills/srt-booking/skill.json +20 -0
- package/skills/subway-lost-property/instruction.md +93 -0
- package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
- package/skills/subway-lost-property/skill.json +8 -0
- package/skills/ticket-availability/instruction.md +175 -0
- package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
- package/skills/ticket-availability/skill.json +9 -0
- package/skills/toss-securities/instruction.md +116 -0
- package/skills/toss-securities/skill.json +9 -0
- package/skills/used-car-price-search/instruction.md +109 -0
- package/skills/used-car-price-search/skill.json +8 -0
- package/skills/yebigun-training/instruction.md +177 -0
- package/skills/yebigun-training/skill.json +9 -0
- package/skills/zipcode-search/instruction.md +159 -0
- package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
- package/skills/zipcode-search/skill.json +8 -0
- package/src/assemble.js +134 -0
- package/src/detect.js +18 -0
- package/templates/action-account.md +4 -0
- package/templates/action-booking.md +5 -0
- package/templates/action-commerce.md +5 -0
- package/templates/action-communication.md +5 -0
- package/templates/action-submission.md +5 -0
- package/templates/browser.md +4 -0
- package/templates/core.md +5 -0
- package/templates/hard-boundary.md +3 -0
- package/templates/local.md +2 -0
- package/templates/lookup.md +2 -0
- package/templates/proxy.md +2 -0
- package/templates/vault.md +4 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ohou-today-deal — 오늘의집 공개 오늘의딜 특가 상품 조회 CLI.
|
|
3
|
+
|
|
4
|
+
조회 전용. 로그인·장바구니·구매·결제 자동화 없음.
|
|
5
|
+
__NEXT_DATA__ 서버 렌더링 초기 데이터만 읽는 read-only 스킬.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
ohou-today-deal list [--limit N] [--sort discount|price|review|annual-sales]
|
|
9
|
+
ohou-today-deal list --query 러그 --min-discount 30 --free-delivery
|
|
10
|
+
ohou-today-deal list --html-file ./fixture.html
|
|
11
|
+
|
|
12
|
+
Supported surface:
|
|
13
|
+
https://ohou.se/commerces/today_deals (공개 HTML)
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import html
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import urllib.request
|
|
23
|
+
from dataclasses import asdict, dataclass
|
|
24
|
+
from datetime import datetime, timedelta, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
DEFAULT_URL = "https://ohou.se/commerces/today_deals"
|
|
29
|
+
DEFAULT_USER_AGENT = (
|
|
30
|
+
"k-skill-ohou-today-deal/1.0 (+https://github.com/NomaDamas/k-skill)"
|
|
31
|
+
)
|
|
32
|
+
TODAY_DEAL_FEED_KEYS: tuple[tuple[str, ...], ...] = (
|
|
33
|
+
("today-deal-feed",),
|
|
34
|
+
("special-today-deal-feed",),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class OhouDeal:
|
|
40
|
+
id: str
|
|
41
|
+
title: str
|
|
42
|
+
brand: str | None
|
|
43
|
+
url: str
|
|
44
|
+
image_url: str | None
|
|
45
|
+
original_price: int | None
|
|
46
|
+
selling_price: int | None
|
|
47
|
+
discount_rate: int | None
|
|
48
|
+
best_price: int | None
|
|
49
|
+
best_discount_rate: int | None
|
|
50
|
+
best_discount_description: str | None
|
|
51
|
+
review_count: int
|
|
52
|
+
review_average: float | None
|
|
53
|
+
scrap_count: int | None
|
|
54
|
+
annual_sales: int | None
|
|
55
|
+
free_delivery: bool
|
|
56
|
+
sold_out: bool
|
|
57
|
+
start_at: str | None
|
|
58
|
+
end_at: str | None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict[str, Any]:
|
|
61
|
+
return asdict(self)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _to_int(value: Any) -> int | None:
|
|
65
|
+
if value is None or value == "":
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
return int(value)
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _to_float(value: Any) -> float | None:
|
|
74
|
+
if value is None or value == "":
|
|
75
|
+
return None
|
|
76
|
+
try:
|
|
77
|
+
return float(value)
|
|
78
|
+
except (TypeError, ValueError):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def fetch_html(url: str = DEFAULT_URL, timeout: int = 20) -> str:
|
|
83
|
+
# ohou.se Akamai 정책: 익명 UA(`python-urllib`, `Mozilla/5.0` 단독)는 403,
|
|
84
|
+
# 봇 이름+contact URL이 포함된 well-formed UA는 허용. 우회가 아닌 자기소개.
|
|
85
|
+
request = urllib.request.Request(
|
|
86
|
+
url,
|
|
87
|
+
headers={
|
|
88
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
89
|
+
"Accept": "text/html,application/json",
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
93
|
+
charset = response.headers.get_content_charset() or "utf-8"
|
|
94
|
+
return response.read().decode(charset, errors="replace")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_next_data(document: str) -> dict[str, Any]:
|
|
98
|
+
stripped = document.lstrip()
|
|
99
|
+
if stripped.startswith("{"):
|
|
100
|
+
return json.loads(stripped)
|
|
101
|
+
|
|
102
|
+
match = re.search(
|
|
103
|
+
r'<script\b[^>]*\bid=["\']__NEXT_DATA__[^>]*>(.*?)</script>',
|
|
104
|
+
document,
|
|
105
|
+
re.DOTALL,
|
|
106
|
+
)
|
|
107
|
+
if not match:
|
|
108
|
+
raise ValueError("Could not find __NEXT_DATA__ in Today Deal HTML")
|
|
109
|
+
return json.loads(html.unescape(match.group(1)))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _walk(value: Any):
|
|
113
|
+
"""스택 기반 DFS로 JSON 트리의 모든 dict 노드를 순회한다.
|
|
114
|
+
|
|
115
|
+
__NEXT_DATA__는 깊고 거대한 트리 구조를 가질 수 있어
|
|
116
|
+
재귀 대신 반복문을 사용해 sys.getrecursionlimit() 제한을 회피한다.
|
|
117
|
+
"""
|
|
118
|
+
stack = [value]
|
|
119
|
+
while stack:
|
|
120
|
+
curr = stack.pop()
|
|
121
|
+
if isinstance(curr, dict):
|
|
122
|
+
yield curr
|
|
123
|
+
stack.extend(curr.values())
|
|
124
|
+
elif isinstance(curr, list):
|
|
125
|
+
stack.extend(reversed(curr))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _looks_like_deal_node(node: dict[str, Any]) -> bool:
|
|
129
|
+
deal = node.get("deal")
|
|
130
|
+
return (
|
|
131
|
+
node.get("type") == "DEAL"
|
|
132
|
+
and isinstance(deal, dict)
|
|
133
|
+
and bool(deal.get("id"))
|
|
134
|
+
and bool(deal.get("name"))
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _iter_today_deal_feeds(payload: dict[str, Any]):
|
|
139
|
+
"""__NEXT_DATA__에서 today-deal-feed / special-today-deal-feed 쿼리만 골라낸다.
|
|
140
|
+
|
|
141
|
+
React Query dehydrated state는 `props.pageProps.dehydratedState.queries`에
|
|
142
|
+
`[{queryKey, state: {data: {...}}}]` 형태로 저장된다. queryKey가 일치하는
|
|
143
|
+
엔트리의 `state.data.todayDealFeed.slots`만 today-deal 콘텐츠로 인정한다.
|
|
144
|
+
"""
|
|
145
|
+
allowed = {tuple(key) for key in TODAY_DEAL_FEED_KEYS}
|
|
146
|
+
queries = (
|
|
147
|
+
payload.get("props", {})
|
|
148
|
+
.get("pageProps", {})
|
|
149
|
+
.get("dehydratedState", {})
|
|
150
|
+
.get("queries", [])
|
|
151
|
+
)
|
|
152
|
+
if not isinstance(queries, list):
|
|
153
|
+
return
|
|
154
|
+
for entry in queries:
|
|
155
|
+
if not isinstance(entry, dict):
|
|
156
|
+
continue
|
|
157
|
+
query_key = entry.get("queryKey")
|
|
158
|
+
if not isinstance(query_key, list):
|
|
159
|
+
continue
|
|
160
|
+
if tuple(query_key) not in allowed:
|
|
161
|
+
continue
|
|
162
|
+
state = entry.get("state") or {}
|
|
163
|
+
data = state.get("data") or {}
|
|
164
|
+
feed = data.get("todayDealFeed") or {}
|
|
165
|
+
slots = feed.get("slots")
|
|
166
|
+
if isinstance(slots, list):
|
|
167
|
+
yield tuple(query_key), slots
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _normalize_deal(node: dict[str, Any]) -> OhouDeal:
|
|
171
|
+
deal = node.get("deal", {})
|
|
172
|
+
price = deal.get("price") or {}
|
|
173
|
+
best_price = node.get("bestDiscountPrice") or {}
|
|
174
|
+
brand = deal.get("brand") or {}
|
|
175
|
+
review = deal.get("reviewStatistic") or {}
|
|
176
|
+
scrap = deal.get("scrapInfo") or {}
|
|
177
|
+
badge = deal.get("badgeProperties") or {}
|
|
178
|
+
annual_sales = node.get("salesStats", {}).get("annualCumulativeSales")
|
|
179
|
+
deal_id = str(deal.get("id", ""))
|
|
180
|
+
|
|
181
|
+
return OhouDeal(
|
|
182
|
+
id=deal_id,
|
|
183
|
+
title=str(deal.get("name") or node.get("title") or ""),
|
|
184
|
+
brand=brand.get("name"),
|
|
185
|
+
url=f"https://ohou.se/productions/{deal_id}/selling",
|
|
186
|
+
image_url=deal.get("imageUrl"),
|
|
187
|
+
original_price=_to_int(price.get("representativeOriginalPrice")),
|
|
188
|
+
selling_price=_to_int(price.get("representativeSellingPrice")),
|
|
189
|
+
discount_rate=_to_int(price.get("discountRate")),
|
|
190
|
+
best_price=_to_int(best_price.get("price")),
|
|
191
|
+
best_discount_rate=_to_int(best_price.get("discountRate")),
|
|
192
|
+
best_discount_description=best_price.get("discountPlanDescription"),
|
|
193
|
+
review_count=_to_int(review.get("reviewCount")) or 0,
|
|
194
|
+
review_average=_to_float(review.get("reviewAverage")),
|
|
195
|
+
scrap_count=_to_int(scrap.get("scrapCount")),
|
|
196
|
+
annual_sales=_to_int(annual_sales),
|
|
197
|
+
free_delivery=bool(badge.get("isFreeDelivery")),
|
|
198
|
+
sold_out=bool(deal.get("isSoldOut")),
|
|
199
|
+
start_at=node.get("startAt"),
|
|
200
|
+
end_at=node.get("endAt"),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def extract_deals(payload: dict[str, Any]) -> list[OhouDeal]:
|
|
205
|
+
seen: set[str] = set()
|
|
206
|
+
deals: list[OhouDeal] = []
|
|
207
|
+
found_feed = False
|
|
208
|
+
|
|
209
|
+
for _query_key, slots in _iter_today_deal_feeds(payload):
|
|
210
|
+
found_feed = True
|
|
211
|
+
for node in slots:
|
|
212
|
+
if not isinstance(node, dict) or not _looks_like_deal_node(node):
|
|
213
|
+
continue
|
|
214
|
+
deal = _normalize_deal(node)
|
|
215
|
+
if deal.id in seen:
|
|
216
|
+
continue
|
|
217
|
+
seen.add(deal.id)
|
|
218
|
+
deals.append(deal)
|
|
219
|
+
|
|
220
|
+
# Fixture/legacy fallback: payload에 React Query dehydratedState가 없거나
|
|
221
|
+
# queryKey 구조가 다른 경우(테스트 fixture, 단순화된 페이로드)에 한해서만
|
|
222
|
+
# 전체 트리를 DFS로 훑어 DEAL 노드를 수집한다. 라이브 페이지는 항상
|
|
223
|
+
# 위쪽 명시적 분기에서 잡히므로 이 fallback은 영향받지 않는다.
|
|
224
|
+
if not found_feed:
|
|
225
|
+
for node in _walk(payload):
|
|
226
|
+
if not _looks_like_deal_node(node):
|
|
227
|
+
continue
|
|
228
|
+
deal = _normalize_deal(node)
|
|
229
|
+
if deal.id in seen:
|
|
230
|
+
continue
|
|
231
|
+
seen.add(deal.id)
|
|
232
|
+
deals.append(deal)
|
|
233
|
+
|
|
234
|
+
return deals
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def filter_deals(
|
|
238
|
+
deals: list[OhouDeal],
|
|
239
|
+
*,
|
|
240
|
+
query: str | None = None,
|
|
241
|
+
min_discount: int | None = None,
|
|
242
|
+
free_delivery: bool = False,
|
|
243
|
+
include_sold_out: bool = False,
|
|
244
|
+
) -> list[OhouDeal]:
|
|
245
|
+
"""단일 루프로 모든 필터 조건을 검사한다."""
|
|
246
|
+
needle = query.casefold() if query else None
|
|
247
|
+
filtered: list[OhouDeal] = []
|
|
248
|
+
|
|
249
|
+
for deal in deals:
|
|
250
|
+
if not include_sold_out and deal.sold_out:
|
|
251
|
+
continue
|
|
252
|
+
if needle and needle not in deal.title.casefold() and needle not in (deal.brand or "").casefold():
|
|
253
|
+
continue
|
|
254
|
+
if min_discount is not None and (deal.best_discount_rate or deal.discount_rate or 0) < min_discount:
|
|
255
|
+
continue
|
|
256
|
+
if free_delivery and not deal.free_delivery:
|
|
257
|
+
continue
|
|
258
|
+
filtered.append(deal)
|
|
259
|
+
|
|
260
|
+
return filtered
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def sort_deals(deals: list[OhouDeal], sort_key: str) -> list[OhouDeal]:
|
|
264
|
+
if sort_key == "discount":
|
|
265
|
+
return sorted(
|
|
266
|
+
deals,
|
|
267
|
+
key=lambda deal: (deal.best_discount_rate or deal.discount_rate or -1, deal.review_count),
|
|
268
|
+
reverse=True,
|
|
269
|
+
)
|
|
270
|
+
if sort_key == "price":
|
|
271
|
+
return sorted(deals, key=lambda deal: deal.best_price or deal.selling_price or sys.maxsize)
|
|
272
|
+
if sort_key == "review":
|
|
273
|
+
return sorted(deals, key=lambda deal: (deal.review_count, deal.review_average or 0), reverse=True)
|
|
274
|
+
if sort_key == "annual-sales":
|
|
275
|
+
return sorted(deals, key=lambda deal: deal.annual_sales or -1, reverse=True)
|
|
276
|
+
return deals
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def build_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
280
|
+
document = Path(args.html_file).read_text(encoding="utf-8") if args.html_file else fetch_html(args.url)
|
|
281
|
+
payload = extract_next_data(document)
|
|
282
|
+
deals = extract_deals(payload)
|
|
283
|
+
filtered = filter_deals(
|
|
284
|
+
deals,
|
|
285
|
+
query=args.query,
|
|
286
|
+
min_discount=args.min_discount,
|
|
287
|
+
free_delivery=args.free_delivery,
|
|
288
|
+
include_sold_out=args.include_sold_out,
|
|
289
|
+
)
|
|
290
|
+
sorted_deals = sort_deals(filtered, args.sort)
|
|
291
|
+
limited_deals = sorted_deals[: args.limit]
|
|
292
|
+
|
|
293
|
+
kst = timezone(timedelta(hours=9))
|
|
294
|
+
now_utc = datetime.now(timezone.utc)
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
"source": {
|
|
298
|
+
"name": "ohou-today-deal",
|
|
299
|
+
"url": args.url,
|
|
300
|
+
"fetched_at": now_utc.isoformat(),
|
|
301
|
+
"fetched_at_kst": now_utc.astimezone(kst).strftime("%Y-%m-%d %H:%M:%S KST"),
|
|
302
|
+
"surface": "__NEXT_DATA__ today-deal-feed + special-today-deal-feed",
|
|
303
|
+
},
|
|
304
|
+
"filters": {
|
|
305
|
+
"query": args.query,
|
|
306
|
+
"min_discount": args.min_discount,
|
|
307
|
+
"free_delivery": args.free_delivery,
|
|
308
|
+
"include_sold_out": args.include_sold_out,
|
|
309
|
+
"sort": args.sort,
|
|
310
|
+
"limit": args.limit,
|
|
311
|
+
},
|
|
312
|
+
"count": len(limited_deals),
|
|
313
|
+
"total_count": len(deals),
|
|
314
|
+
"filtered_count": len(filtered),
|
|
315
|
+
"items": [deal.to_dict() for deal in limited_deals],
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _positive_int(value: str) -> int:
|
|
320
|
+
try:
|
|
321
|
+
parsed = int(value)
|
|
322
|
+
except ValueError as exc:
|
|
323
|
+
raise argparse.ArgumentTypeError(f"expected integer, got {value!r}") from exc
|
|
324
|
+
if parsed <= 0:
|
|
325
|
+
raise argparse.ArgumentTypeError(f"must be a positive integer, got {parsed}")
|
|
326
|
+
return parsed
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _discount_rate(value: str) -> int:
|
|
330
|
+
try:
|
|
331
|
+
parsed = int(value)
|
|
332
|
+
except ValueError as exc:
|
|
333
|
+
raise argparse.ArgumentTypeError(f"expected integer, got {value!r}") from exc
|
|
334
|
+
if not 0 <= parsed <= 100:
|
|
335
|
+
raise argparse.ArgumentTypeError(
|
|
336
|
+
f"discount rate must be between 0 and 100, got {parsed}"
|
|
337
|
+
)
|
|
338
|
+
return parsed
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
342
|
+
parser = argparse.ArgumentParser(description="Read Ohouse today deal products from public HTML.")
|
|
343
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
344
|
+
|
|
345
|
+
list_parser = subparsers.add_parser("list", help="오늘의집 오늘의딜 상품 목록")
|
|
346
|
+
list_parser.add_argument("--url", default=DEFAULT_URL)
|
|
347
|
+
list_parser.add_argument("--html-file", help="테스트/오프라인 검증용 HTML 또는 JSON 파일")
|
|
348
|
+
list_parser.add_argument("--query", help="상품명 또는 브랜드 키워드")
|
|
349
|
+
list_parser.add_argument("--min-discount", type=_discount_rate, help="최소 할인율 (0~100)")
|
|
350
|
+
list_parser.add_argument("--free-delivery", action="store_true", help="무료배송 상품만")
|
|
351
|
+
list_parser.add_argument("--include-sold-out", action="store_true", help="품절 상품 포함")
|
|
352
|
+
list_parser.add_argument(
|
|
353
|
+
"--sort",
|
|
354
|
+
choices=["default", "discount", "price", "review", "annual-sales"],
|
|
355
|
+
default="discount",
|
|
356
|
+
)
|
|
357
|
+
list_parser.add_argument("--limit", type=_positive_int, default=10, help="결과 개수 (양의 정수)")
|
|
358
|
+
|
|
359
|
+
return parser.parse_args(argv)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main(argv: list[str] | None = None) -> None:
|
|
363
|
+
args = parse_args(argv)
|
|
364
|
+
if args.command == "list":
|
|
365
|
+
print(json.dumps(build_payload(args), ensure_ascii=False, indent=2))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
if __name__ == "__main__":
|
|
369
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ohou-today-deal",
|
|
3
|
+
"description": "오늘의집 공개 오늘의딜 페이지에서 로그인 없이 특가 상품을 조회하고 할인율, 가격, 리뷰, 링크를 정리하는 읽기 전용 스킬. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"browser",
|
|
6
|
+
"action:commerce"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: ohou-today-deal\ndescription: 오늘의집 공개 오늘의딜 페이지에서 로그인 없이 특가 상품을 조회하고 할인율, 가격, 리뷰, 링크를 정리하는 읽기 전용 스킬. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: retail\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Olive Young Search
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
upstream 원본 [`hmmhmmhm/daiso-mcp`](https://github.com/hmmhmmhm/daiso-mcp) 와 npm package [`daiso`](https://www.npmjs.com/package/daiso) 를 그대로 사용해 **올리브영 매장 검색, 상품 검색, 재고 확인** 흐름을 안내한다.
|
|
6
|
+
|
|
7
|
+
이 저장소는 원본 MCP 서버 코드를 vendoring 하지 않는다. 대신 **MCP 서버를 Claude Code에 직접 설치하지 않고 CLI 형태로 먼저 확인하는 경로**를 기본값으로 둔다.
|
|
8
|
+
|
|
9
|
+
핵심 조회 경로:
|
|
10
|
+
|
|
11
|
+
- 매장 검색: `/api/oliveyoung/stores`
|
|
12
|
+
- 상품 검색: `/api/oliveyoung/products`
|
|
13
|
+
- 재고 확인: `/api/oliveyoung/inventory`
|
|
14
|
+
- health check: `npx --yes daiso health`
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- "명동 근처 올리브영 매장 찾아줘"
|
|
19
|
+
- "올리브영 선크림 어떤 거 있나 보여줘"
|
|
20
|
+
- "명동 근처 올리브영에서 선크림 재고 확인해줘"
|
|
21
|
+
- "올리브영 검색용 CLI 붙여줘"
|
|
22
|
+
|
|
23
|
+
## When not to use
|
|
24
|
+
|
|
25
|
+
- 로그인, 주문, 장바구니, 결제 자동화
|
|
26
|
+
- 올리브영 계정/세션이 필요한 private 기능
|
|
27
|
+
- upstream 서버 코드를 이 저장소 안에 복사해서 유지하려는 경우
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
- 인터넷 연결
|
|
32
|
+
- `node` 20 권장 (`hmmhmmhm/daiso-mcp` 2026-04-05 기준 `engines.node` 는 `>=20 <21`)
|
|
33
|
+
- `npx` 또는 `npm`
|
|
34
|
+
- 필요하면 `git`
|
|
35
|
+
|
|
36
|
+
Node 22에서도 로컬 smoke test는 성공했지만 `EBADENGINE` 경고가 보여서, **안정 경로는 Node 20 LTS** 로 본다.
|
|
37
|
+
|
|
38
|
+
## Preferred setup: CLI first, not direct MCP install
|
|
39
|
+
|
|
40
|
+
가장 빠른 경로는 MCP 연결부터 하지 않고 upstream CLI로 공개 endpoint를 확인하는 것이다.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx --yes daiso health
|
|
44
|
+
npx --yes daiso get /api/oliveyoung/stores --keyword 명동 --limit 5 --json
|
|
45
|
+
npx --yes daiso get /api/oliveyoung/products --keyword 선크림 --size 5 --json
|
|
46
|
+
npx --yes daiso get /api/oliveyoung/inventory --keyword 선크림 --storeKeyword 명동 --size 5 --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
반복 사용이면 전역 설치도 가능하다.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install -g daiso
|
|
53
|
+
export NODE_PATH="$(npm root -g)"
|
|
54
|
+
daiso health
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Fallback: clone the original repository and run the same CLI locally
|
|
58
|
+
|
|
59
|
+
public endpoint 재시도나 버전 고정이 필요하면 원본 저장소를 clone 해서 build 결과물 `dist/bin.js` 를 `node` 로 직접 실행한다.
|
|
60
|
+
clone checkout 안에서는 `npx daiso ...` 가 `Permission denied` 로 실패할 수 있으므로, local fallback은 아래 경로를 기본으로 둔다.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/hmmhmmhm/daiso-mcp.git
|
|
64
|
+
cd daiso-mcp
|
|
65
|
+
npm install
|
|
66
|
+
npm run build
|
|
67
|
+
node dist/bin.js health
|
|
68
|
+
node dist/bin.js get /api/oliveyoung/stores --keyword 명동 --limit 5 --json
|
|
69
|
+
node dist/bin.js get /api/oliveyoung/products --keyword 선크림 --size 5 --json
|
|
70
|
+
node dist/bin.js get /api/oliveyoung/inventory --keyword 선크림 --storeKeyword 명동 --size 5 --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
즉, 이 스킬의 기본 원칙은 **원본 `hmmhmmhm/daiso-mcp`를 설치/실행해서 쓰고, `k-skill`에는 가이드만 추가하는 것**이다.
|
|
74
|
+
|
|
75
|
+
## Required inputs
|
|
76
|
+
|
|
77
|
+
### 1. Store or area keyword first when place context is missing
|
|
78
|
+
|
|
79
|
+
- 권장 질문: `어느 지역/매장을 기준으로 볼까요? 예: 명동, 강남역, 성수`
|
|
80
|
+
- 재고 질문인데 지역이 없으면 먼저 지역/매장 키워드를 받는다.
|
|
81
|
+
|
|
82
|
+
### 2. Product keyword first when inventory is requested
|
|
83
|
+
|
|
84
|
+
- 권장 질문: `찾을 상품 키워드도 알려주세요. 예: 선크림, 립밤, 마스크팩`
|
|
85
|
+
- 상품 종류를 묻는 경우에도 키워드를 너무 넓게 받지 않는다.
|
|
86
|
+
|
|
87
|
+
## Workflow
|
|
88
|
+
|
|
89
|
+
### 1. Check server health
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx --yes daiso health
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Resolve nearby stores
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx --yes daiso get /api/oliveyoung/stores --keyword 명동 --limit 5 --json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
매장 후보가 여러 개면 상위 2~3개만 요약하고 다시 확인받는다.
|
|
102
|
+
|
|
103
|
+
### 3. Resolve product candidates
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx --yes daiso get /api/oliveyoung/products --keyword 선크림 --size 5 --json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
상품 후보가 많으면 `goodsNumber`, 가격, 이미지 URL, `inStock` 여부를 함께 짧게 정리한다.
|
|
110
|
+
|
|
111
|
+
### 4. Check inventory for the chosen area/store keyword
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx --yes daiso get /api/oliveyoung/inventory --keyword 선크림 --storeKeyword 명동 --size 5 --json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
응답의 `inventory.products[].storeInventory.stores[]` 안에서 다음 값을 우선 본다.
|
|
118
|
+
|
|
119
|
+
- `stockLabel`
|
|
120
|
+
- `remainQuantity`
|
|
121
|
+
- `stockStatus`
|
|
122
|
+
- `storeName`
|
|
123
|
+
|
|
124
|
+
### 5. Respond conservatively
|
|
125
|
+
|
|
126
|
+
최종 응답은 아래 순서로 짧게 정리한다.
|
|
127
|
+
|
|
128
|
+
- 기준 지역/매장 키워드
|
|
129
|
+
- 상위 매장 후보
|
|
130
|
+
- 상품 후보 또는 선택 상품
|
|
131
|
+
- 재고 있는 매장 / 품절 / 미판매 구분
|
|
132
|
+
- 필요하면 `imageUrl` 참고 링크
|
|
133
|
+
- 공개 endpoint 특성상 재고는 실시간 100% 보장값이 아니므로 방문 직전 재확인을 권장
|
|
134
|
+
|
|
135
|
+
## Done when
|
|
136
|
+
|
|
137
|
+
- `hmmhmmhm/daiso-mcp` 원본 repo와 `daiso` CLI 사용 경로를 명시했다.
|
|
138
|
+
- MCP 서버를 직접 설치하는 대신 CLI first 흐름을 제시했다.
|
|
139
|
+
- 매장 검색 → 상품 검색 → 재고 확인 순서를 따랐다.
|
|
140
|
+
- `/api/oliveyoung/stores`, `/api/oliveyoung/products`, `/api/oliveyoung/inventory` 중 필요한 호출을 실제로 안내했다.
|
|
141
|
+
- 재고 결과를 매장별 `stockLabel` 중심으로 요약했다.
|
|
142
|
+
|
|
143
|
+
## Failure modes
|
|
144
|
+
|
|
145
|
+
- public endpoint는 upstream 내부 수집 경로(Zyte 의존) 사정으로 간헐적인 5xx/503을 줄 수 있다.
|
|
146
|
+
- 지역 키워드가 너무 넓으면 멀리 떨어진 동명이점 매장이 섞일 수 있다.
|
|
147
|
+
- 인기 상품은 검색 결과가 많아 상위 몇 개만 먼저 확인받는 편이 안전하다.
|
|
148
|
+
- 재고 수량은 시점 차이로 실제 방문 시 달라질 수 있다.
|
|
149
|
+
|
|
150
|
+
## Notes
|
|
151
|
+
|
|
152
|
+
- 원본 프로젝트: `https://github.com/hmmhmmhm/daiso-mcp`
|
|
153
|
+
- npm package: `https://www.npmjs.com/package/daiso`
|
|
154
|
+
- 이 저장소는 upstream 코드를 vendoring 하지 않고 skill/docs만 유지한다.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "olive-young-search",
|
|
3
|
+
"description": "upstream daiso CLI를 사용해 올리브영 매장 검색, 상품 검색, 재고 확인을 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"action:commerce"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: olive-young-search\ndescription: upstream daiso CLI를 사용해 올리브영 매장 검색, 상품 검색, 재고 확인을 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: retail\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Parking Lot Search
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
유저가 알려준 현재 위치를 기준으로 **근처 공영주차장** 을 찾는다.
|
|
6
|
+
|
|
7
|
+
- 위치는 자동 추정하지 않는다.
|
|
8
|
+
- **반드시 먼저 현재 위치를 질문**한다.
|
|
9
|
+
- 기본값은 `공영` 주차장만 보여준다.
|
|
10
|
+
- 공식 `전국주차장정보표준데이터` Open API를 사용한다.
|
|
11
|
+
- 위치 문자열은 Kakao Map anchor 검색으로 좌표를 잡은 뒤, 공식 주차장 데이터에서 거리순으로 정리한다.
|
|
12
|
+
- 실시간 만차/잔여면/예약 여부는 공식 표준데이터에 없으므로 확정해서 말하지 않는다.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- "근처 주차장 찾아줘"
|
|
17
|
+
- "광화문 주변 공영주차장 어디 있어?"
|
|
18
|
+
- "서울역 근처 무료 주차장 있어?"
|
|
19
|
+
- "지금 여기서 가까운 공영주차장 지도 링크 줘"
|
|
20
|
+
|
|
21
|
+
## Mandatory first question
|
|
22
|
+
|
|
23
|
+
위치 정보 없이 바로 검색하지 말고 반드시 먼저 물어본다.
|
|
24
|
+
|
|
25
|
+
권장 질문:
|
|
26
|
+
|
|
27
|
+
`현재 위치를 알려주세요. 동네/역명/랜드마크/위도·경도 중 편한 형식으로 보내주시면 근처 공영주차장을 찾아볼게요.`
|
|
28
|
+
|
|
29
|
+
위치가 애매하면:
|
|
30
|
+
|
|
31
|
+
`가까운 역명이나 동 이름으로 한 번만 더 알려주세요.`
|
|
32
|
+
|
|
33
|
+
## Official surfaces
|
|
34
|
+
|
|
35
|
+
- 표준데이터 안내: `https://www.data.go.kr/data/15012896/standard.do`
|
|
36
|
+
- Open API 안내: `https://www.data.go.kr/data/15012896/openapi.do`
|
|
37
|
+
- Open API endpoint: `https://api.data.go.kr/openapi/tn_pubr_prkplce_info_api`
|
|
38
|
+
- k-skill proxy: `/v1/parking-lots/search`
|
|
39
|
+
- Kakao Map 모바일 검색: `https://m.map.kakao.com/actions/searchView?q=<query>`
|
|
40
|
+
- Kakao Map 장소 패널 JSON: `https://place-api.map.kakao.com/places/panel3/<confirmId>`
|
|
41
|
+
|
|
42
|
+
## Workflow
|
|
43
|
+
|
|
44
|
+
1. 유저에게 반드시 현재 위치를 묻는다.
|
|
45
|
+
2. 위치 문자열을 받으면 Kakao Map으로 anchor 후보를 고르고 좌표를 확보한다.
|
|
46
|
+
3. anchor 주소에서 `시도 + 시군구` address hint를 만든다.
|
|
47
|
+
4. k-skill-proxy `/v1/parking-lots/search` 또는 `parking-lot-search` 패키지로 공식 주차장 데이터를 조회한다.
|
|
48
|
+
5. 보통 3~5개만 짧게 정리하고, 지도 링크를 같이 준다.
|
|
49
|
+
6. 요금/운영시간은 데이터 기준일자와 함께 안내하고, 실시간 현황이 아님을 밝힌다.
|
|
50
|
+
|
|
51
|
+
## Responding
|
|
52
|
+
|
|
53
|
+
결과는 보통 아래 필드를 포함해 짧게 정리한다.
|
|
54
|
+
|
|
55
|
+
- 주차장명
|
|
56
|
+
- 거리
|
|
57
|
+
- 공영/민영 및 노상/노외/부설 유형
|
|
58
|
+
- 주소
|
|
59
|
+
- 운영요일/운영시간
|
|
60
|
+
- 요금정보, 기본요금, 추가요금
|
|
61
|
+
- 주차구획수
|
|
62
|
+
- 전화번호/관리기관
|
|
63
|
+
- 데이터기준일자
|
|
64
|
+
- 지도 링크
|
|
65
|
+
|
|
66
|
+
## Node.js example
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
const { searchNearbyParkingLotsByLocationQuery } = require("parking-lot-search");
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
const result = await searchNearbyParkingLotsByLocationQuery("광화문", {
|
|
73
|
+
limit: 3,
|
|
74
|
+
radius: 1500
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log(result.anchor);
|
|
78
|
+
console.log(result.items);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch((error) => {
|
|
82
|
+
console.error(error);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 모두의주차장 status
|
|
88
|
+
|
|
89
|
+
Issue #135에서 모두의주차장 연동 가능성을 언급했지만, v1은 공식 공공데이터 기반으로 시작한다. 승인된 공식/파트너 API 계약이 확인되기 전에는 모두의주차장 비공식 API 호출이나 scraping을 하지 않는다.
|
|
90
|
+
|
|
91
|
+
## Done when
|
|
92
|
+
|
|
93
|
+
- 유저의 현재 위치를 먼저 확인했다.
|
|
94
|
+
- 공식 데이터 기반으로 최소 1개 이상 nearby parking lot을 찾았거나, 못 찾은 이유와 다음 질문을 제시했다.
|
|
95
|
+
- 가장 가까운 결과를 3~5개 이내로 정리했다.
|
|
96
|
+
- 실시간 잔여면/예약 가능 여부가 아님을 필요한 경우 명확히 했다.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "parking-lot-search",
|
|
3
|
+
"description": "Use when the user asks for nearby parking lots or 근처 주차장/공영주차장. Always ask the user's current location first, then use official Data.go.kr public parking data through k-skill-proxy or the parking-lot-search package.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"proxy",
|
|
6
|
+
"lookup"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: parking-lot-search\ndescription: Use when the user asks for nearby parking lots or 근처 주차장/공영주차장. Always ask the user's current location first, then use official Data.go.kr public parking data through k-skill-proxy or the parking-lot-search package.\nlicense: MIT\nmetadata:\n category: convenience\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|