Spaces:
Paused
Paused
| import os, json, re | |
| from fastapi import APIRouter, Request | |
| from fastapi.responses import JSONResponse | |
| from shared import GROQ_API_KEY, NAVER_CLIENT_ID, NAVER_CLIENT_SECRET, _sanitize_text | |
| router = APIRouter() | |
| async def writer_endpoint(request: Request): | |
| if not GROQ_API_KEY: | |
| return JSONResponse({"error": "GROQ_API_KEY not set"}, status_code=500) | |
| try: | |
| body = await request.json() | |
| except: | |
| return JSONResponse({"error": "invalid json"}, status_code=400) | |
| style = (body.get("style") or "블로그")[:20] | |
| topic = _sanitize_text(body.get("topic") or "")[:1000] | |
| context = _sanitize_text(body.get("context") or "")[:3000] | |
| tone = (body.get("tone") or "전문적")[:20] | |
| if not topic and not context: | |
| return JSONResponse({"error": "주제 또는 참고 내용이 필요합니다"}, status_code=400) | |
| style_guides = { | |
| "블로그": "SEO에 적합한 블로그 포스트. 제목(H1) + 소제목(H2) 구조. 도입-본문-마무리. 키워드 자연스럽게 배치.", | |
| "SNS": "인스타그램/페이스북용 짧고 임팩트 있는 글. 이모지 활용. 해시태그 5~10개 포함. 300자 이내.", | |
| "이메일": "비즈니스 이메일. 제목줄 + 인사 + 본문 + 마무리. 정중하고 간결하게.", | |
| "보도자료": "언론 보도자료 형식. 제목 + 부제 + 리드문(누가/언제/어디서/무엇을/왜) + 본문 + 회사소개.", | |
| "광고카피": "짧고 강렬한 광고 카피. 헤드라인 + 서브카피 + CTA. 다양한 버전 3개 제시.", | |
| "유튜브대본": "유튜브 영상 대본. 후킹 오프닝 + 본문(타임스탬프 포함) + 아웃트로 + CTA.", | |
| } | |
| guide = style_guides.get(style, style_guides["블로그"]) | |
| prompt = f"""다음 조건으로 글을 작성해줘: | |
| - 스타일: {style} | |
| - 톤: {tone} | |
| - 작성 가이드: {guide} | |
| """ | |
| if topic: | |
| prompt += f"\n- 주제: {topic}\n" | |
| if context: | |
| prompt += f"\n- 참고 내용:\n{context}\n" | |
| prompt += "\n반드시 한국어로 작성. 바로 사용할 수 있는 완성된 글로 출력." | |
| import httpx | |
| try: | |
| async with httpx.AsyncClient(timeout=60.0) as client: | |
| resp = await client.post( | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| headers={"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"}, | |
| json={"model": "openai/gpt-oss-120b", "messages": [{"role": "user", "content": prompt}], | |
| "max_completion_tokens": 3000, "temperature": 0.8} | |
| ) | |
| if resp.status_code != 200: | |
| return JSONResponse({"error": f"API error {resp.status_code}"}, status_code=502) | |
| rd = resp.json() | |
| text = rd.get("choices", [{}])[0].get("message", {}).get("content", "글을 생성하지 못했습니다.") | |
| return {"ok": True, "content": text, "style": style} | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)[:200]}, status_code=500) | |
| async def shopping_endpoint(request: Request): | |
| if not NAVER_CLIENT_ID or not NAVER_CLIENT_SECRET: | |
| return JSONResponse({"error": "NAVER API 키가 설정되지 않았습니다"}, status_code=500) | |
| try: | |
| body = await request.json() | |
| except: | |
| return JSONResponse({"error": "invalid json"}, status_code=400) | |
| query = _sanitize_text(body.get("query") or "")[:100] | |
| sort = body.get("sort", "sim") | |
| if not query: | |
| return JSONResponse({"error": "검색어가 필요합니다"}, status_code=400) | |
| import httpx | |
| try: | |
| async with httpx.AsyncClient(timeout=15.0) as client: | |
| resp = await client.get( | |
| "https://openapi.naver.com/v1/search/shop.json", | |
| params={"query": query, "display": 20, "sort": sort}, | |
| headers={ | |
| "X-Naver-Client-Id": NAVER_CLIENT_ID, | |
| "X-Naver-Client-Secret": NAVER_CLIENT_SECRET, | |
| } | |
| ) | |
| if resp.status_code != 200: | |
| print(f"[shopping] Naver API {resp.status_code}: {resp.text[:200]}") | |
| return JSONResponse({"error": f"네이버 API 오류 ({resp.status_code})"}, status_code=502) | |
| data = resp.json() | |
| items = data.get("items", []) | |
| if not items: | |
| return {"ok": True, "items": [], "total": 0, "query": query, "tip": "검색 결과가 없습니다."} | |
| results = [] | |
| for item in items[:20]: | |
| title_clean = re.sub(r'</?b>', '', item.get("title", "")) | |
| results.append({ | |
| "title": title_clean, | |
| "price": int(item.get("lprice", 0)), | |
| "hprice": int(item.get("hprice", 0)) if item.get("hprice") else None, | |
| "mall": item.get("mallName", ""), | |
| "link": item.get("link", ""), | |
| "image": item.get("image", ""), | |
| "category": "/".join(filter(None, [ | |
| item.get("category1", ""), | |
| item.get("category2", ""), | |
| item.get("category3", ""), | |
| ])), | |
| "type": "price_compare" if item.get("productType") == "2" else "single", | |
| }) | |
| results.sort(key=lambda x: x["price"] if x["price"] > 0 else 99999999) | |
| lowest = results[0]["price"] if results else 0 | |
| highest = max((r["price"] for r in results), default=0) | |
| return { | |
| "ok": True, | |
| "items": results, | |
| "total": data.get("total", len(results)), | |
| "query": query, | |
| "lowest": lowest, | |
| "highest": highest, | |
| "sort": sort, | |
| } | |
| except Exception as e: | |
| print(f"[shopping] error: {e}") | |
| return JSONResponse({"error": str(e)[:200]}, status_code=500) |