π¦ 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('"', '"').replace('&', '&').replace('<', '<').replace('>', '>')
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