Join Nostr
2026-01-23 02:36:24 UTC

npub1al…ajqfd on Nostr: πŸ“¦ IndieBiz OS 도ꡬ νŒ¨ν‚€μ§€ 이름: Shopping Assistant μ„€λͺ…: 제곡 도ꡬ: ...

πŸ“¦ IndieBiz OS 도ꡬ νŒ¨ν‚€μ§€

이름: Shopping Assistant
μ„€λͺ…: 제곡 도ꡬ: search_shopping
버전: 1.0.0

μ„€μΉ˜ 방법:
===PACKAGE_START===
[IndieBiz OS Package Format v1]
[이 μ½”λ“œλ₯Ό μ°Έμ‘°ν•˜μ—¬ μ‚¬μš©μž ν™˜κ²½μ— 맞게 μ„€μΉ˜ν•˜μ„Έμš”]

id: shopping-assistant
name: search_shopping
description: μƒν’ˆ 검색을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. 넀이버 μ‡Όν•‘(API)κ³Ό λ‹€λ‚˜μ™€(크둀링)λ₯Ό μ§€μ›ν•©λ‹ˆλ‹€.

지원 μ‚¬μ΄νŠΈ: naver, danawa, all

데이터 ν˜•μ‹: {items: [{name, price, mall, link, image, category, site}], total}

===FILE:tool.json===
[
{
"name": "search_shopping",
"description": "μƒν’ˆ 검색을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. 넀이버 μ‡Όν•‘(API)κ³Ό λ‹€λ‚˜μ™€(크둀링)λ₯Ό μ§€μ›ν•©λ‹ˆλ‹€.\n\n지원 μ‚¬μ΄νŠΈ: naver, danawa, all\n\n데이터 ν˜•μ‹: {items: [{name, price, mall, link, image, category, site}], total}",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색할 μƒν’ˆλͺ…"
},
"site": {
"type": "string",
"description": "검색할 μ‚¬μ΄νŠΈ (naver, danawa, all)",
"enum": ["naver", "danawa", "all"],
"default": "all"
},
"display": {
"type": "integer",
"description": "검색 κ²°κ³Ό 개수 (μ΅œλŒ€ 10)",
"default": 5
}
},
"required": ["query"]
}
}
]


===FILE:handler.py===
import os
import json
import requests
import re
import asyncio
from playwright.async_api import async_playwright

# 넀이버 API ν‚€ ν™˜κ²½λ³€μˆ˜ λ‘œλ“œ
NAVER_CLIENT_ID = os.environ.get("NAVER_CLIENT_ID", "")
NAVER_CLIENT_SECRET = os.environ.get("NAVER_CLIENT_SECRET", "")

def clean_html(text):
"""HTML νƒœκ·Έ 제거 및 특수 문자 처리"""
if not text:
return ""
clean = re.sub('<[^<]+?>', '', text)
return clean.replace('&quot;', '"').replace('&amp;', '&').replace('&lt;', '<').replace('&gt;', '>')

def search_naver_shopping(query: str, display: int = 5):
"""넀이버 μ‡Όν•‘ 검색 API 호좜"""
if not NAVER_CLIENT_ID or not NAVER_CLIENT_SECRET:
return {"error": "넀이버 API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}

url = "https://openapi.naver.com/v1/search/shop.json";
headers = {
"X-Naver-Client-Id": NAVER_CLIENT_ID,
"X-Naver-Client-Secret": NAVER_CLIENT_SECRET
}
params = {
"query": query,
"display": min(display, 10),
"sort": "sim"
}

try:
response = requests.get(url, headers=headers, params=params, timeout=10)
if response.status_code != 200:
return {"error": f"넀이버 API 였λ₯˜: {response.status_code}"}

data = response.json()
raw_items = data.get("items", [])

items = []
for item in raw_items:
items.append({
"name": clean_html(item.get("title", "")),
"price": item.get("lprice", "0"),
"mall": item.get("mallName", "넀이버"),
"link": item.get("link", ""),
"image": item.get("image", ""),
"category": f"{item.get('category1', '')} > {item.get('category2', '')}",
"site": "naver"
})

return {
"total": data.get("total", 0),
"items": items
}
except Exception as e:
return {"error": f"넀이버 검색 쀑 였λ₯˜: {str(e)}"}

async def search_danawa_shopping_async(query: str, display: int = 5):
"""λ‹€λ‚˜μ™€ 검색 (Playwright μ‚¬μš©)"""
async with async_playwright() as p:
try:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
)
page = await context.new_page()
url = f"https://search.danawa.com/dsearch.php?query={query}";
await page.goto(url, wait_until="domcontentloaded")

# μƒν’ˆ λͺ©λ‘ λŒ€κΈ°
try:
await page.wait_for_selector(".product_list .prod_main_info", timeout=5000)
except:
await browser.close()
return {"total": 0, "items": []}

items_els = await page.query_selector_all(".product_list .prod_main_info")
items = []
for el in items_els[:display]:
name_el = await el.query_selector(".prod_name a")
name = (await name_el.inner_text()).strip() if name_el else "N/A"

price_el = await el.query_selector(".rank_one .price_sect strong")
price = (await price_el.inner_text()).replace(",", "").strip() if price_el else "0"

link = await name_el.get_attribute("href") if name_el else ""

img_el = await el.query_selector(".thumb_image img")
image = (await img_el.get_attribute("data-original")) or (await img_el.get_attribute("src")) or ""
if image and image.startswith("//"):
image = "https:" + image

items.append({
"name": name,
"price": price,
"mall": "λ‹€λ‚˜μ™€",
"link": link,
"image": image,
"category": "κ°€μ „/λ””μ§€ν„Έ",
"site": "danawa"
})

await browser.close()
return {"total": len(items), "items": items}
except Exception as e:
return {"error": f"λ‹€λ‚˜μ™€ 검색 쀑 였λ₯˜: {str(e)}"}

async def search_all_async(query: str, display: int = 5):
"""λͺ¨λ“  μ‚¬μ΄νŠΈ 검색 (넀이버 + λ‹€λ‚˜μ™€)"""
naver_res = search_naver_shopping(query, display)
danawa_res = await search_danawa_shopping_async(query, display)

combined_items = []
if "items" in naver_res: combined_items.extend(naver_res["items"])
if "items" in danawa_res: combined_items.extend(danawa_res["items"])

return {
"total": len(combined_items),
"items": combined_items
}

def execute(tool_name: str, tool_input: dict, project_path: str = ".") -> str:
"""도ꡬ μ‹€ν–‰ 메인 ν•Έλ“€λŸ¬"""
if tool_name == "search_shopping":
query = tool_input.get("query")
site = tool_input.get("site", "all")
display = tool_input.get("display", 5)

if not query:
return "검색어λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."

try:
if site == "naver":
result = search_naver_shopping(query, display)
elif site == "danawa":
result = asyncio.run(search_danawa_shopping_async(query, display))
else: # all
result = asyncio.run(search_all_async(query, display))

return json.dumps(result, ensure_ascii=False, indent=2)
except Exception as e:
return f"였λ₯˜ λ°œμƒ: {str(e)}"

return f"μ•Œ 수 μ—†λŠ” 도ꡬ: {tool_name}"


===PACKAGE_END===

#indiebizOS-package