Spaces:
Running
Running
| # ํค์๋๋ฅผ ๋ฐ์ ๋ค์ด๋ฒ ์ค์๊ฐ ์กฐํ + ์บ์ฑ ํ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ FastAPI ์๋ฒ | |
| import os | |
| import time | |
| from collections import defaultdict | |
| from fastapi import FastAPI, HTTPException, Request, Header | |
| from fastapi.middleware.cors import CORSMiddleware | |
| import cache | |
| from naver_collector import fetch_keywords | |
| from analyzer import build_overview | |
| from clusterer import cluster_keywords, filter_by_relevance | |
| from intent_classifier import classify_clusters | |
| from search_path import build_search_path | |
| from autocomplete import build_autocomplete_path | |
| from datalab import demographics | |
| import supabase_store | |
| app = FastAPI(title="IntentFinder API") | |
| # โโ ๋ฐฐํฌ ๋ณดํธ ์ค์ (์ ๋ถ env ๊ธฐ๋ฐ ยท ๋ฏธ์ค์ ์ ๋ก์ปฌ ๊ฐ๋ฐ์ฉ ๊ธฐ๋ณธ๊ฐ) โโ | |
| # ALLOWED_ORIGINS: ํ์ฉ ์ถ์ฒ(์ผํ๊ตฌ๋ถ). ๋ฐฐํฌ ์ ๋์๋ณด๋ ๋๋ฉ์ธ์ผ๋ก ์ ํ. ๊ธฐ๋ณธ "*"(๋ก์ปฌ). | |
| # DT_API_KEY: ์ค์ ๋๋ฉด X-DT-Key ํค๋๊ฐ ์ผ์นํด์ผ ํธ์ถ ํ์ฉ(๊ณผ๊ธ ๋จ์ฉ ๋ฐฉ์ง). ๋ฏธ์ค์ ์ ๋ฌด์ธ์ฆ(๋ก์ปฌ). | |
| # RATE_MAX_PER_MIN: IP๋น ๋ถ๋น ์ต๋ ํธ์ถ ์. | |
| ALLOWED_ORIGINS = [o.strip() for o in os.getenv("ALLOWED_ORIGINS", "*").split(",") if o.strip()] or ["*"] | |
| DT_API_KEY = os.getenv("DT_API_KEY", "") | |
| RATE_MAX_PER_MIN = int(os.getenv("RATE_MAX_PER_MIN", "20")) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=ALLOWED_ORIGINS, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| cache.init() | |
| _hits: dict[str, list[float]] = defaultdict(list) | |
| def _check_rate(ip: str): | |
| now = time.time() | |
| recent = [t for t in _hits[ip] if now - t < 60] | |
| if len(recent) >= RATE_MAX_PER_MIN: | |
| raise HTTPException(status_code=429, detail="์์ฒญ์ด ๋ง์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํ์ธ์.") | |
| recent.append(now) | |
| _hits[ip] = recent | |
| def health(): | |
| return {"ok": True} | |
| def analyze(keyword: str, request: Request, x_dt_key: str | None = Header(default=None)): | |
| # ๊ณต์ ์ํฌ๋ฆฟ ๊ฒ์ฆ(์ค์ ๋ ๊ฒฝ์ฐ) + IP ๋ ์ดํธ๋ฆฌ๋ฐ | |
| if DT_API_KEY and x_dt_key != DT_API_KEY: | |
| raise HTTPException(status_code=401, detail="์ธ์ฆ๋์ง ์์ ์์ฒญ์ ๋๋ค.") | |
| _check_rate(request.client.host if request.client else "unknown") | |
| # ํค์๋ ๋ถ์: ์บ์ ํํธ๋ฉด ์ฆ์, ์๋๋ฉด ๋ค์ด๋ฒ ํธ์ถ ํ ์บ์ฑ | |
| keyword = keyword.strip() | |
| if not keyword: | |
| raise HTTPException(status_code=400, detail="ํค์๋๊ฐ ๋น์ด ์์ต๋๋ค.") | |
| hit = cache.get(keyword) | |
| if hit: | |
| return {**hit, "cached": True} | |
| try: | |
| df_raw = fetch_keywords(keyword) | |
| except Exception as e: | |
| raise HTTPException(status_code=502, detail=f"๋ค์ด๋ฒ API ํธ์ถ ์คํจ: {e}") | |
| # ์๋ ๋ฌด๊ด ๋ฒ์ฉ์ด(๊ณ์ฐ๊ธฐ ๋ฑ) ์ ๊ฑฐ ํ ์ง๊ณยท๊ตฐ์งํ | |
| df, emb = filter_by_relevance(df_raw, keyword) | |
| result = build_overview(keyword, df) | |
| result["raw_keyword_count"] = int(len(df_raw)) # ํํฐ ์ ์๋ณธ ์ | |
| result["filtered_out"] = int(len(df_raw) - len(df)) # ๋ฌด๊ด์ด ์ ๊ฑฐ ์ | |
| clusters = cluster_keywords(df, embeddings=emb) | |
| result["clusters"] = clusters | |
| result["cluster_count"] = len(clusters) # ๊ทธ๋ฃน ์ ์นด๋ | |
| result["clustered_keyword_count"] = sum(c["keyword_count"] for c in clusters) | |
| # ๊ฒ์ ๊ฒฝ๋ก: ๋ค์ด๋ฒ ์๋์์ฑ(์ค์ ๋์๊ฒ์) ์ฐ์ , ๋น์ฝํ๋ฉด ์๋ฒ ๋ฉ ์ถ์ ์ผ๋ก ํด๋ฐฑ | |
| vol_lookup = dict(zip( | |
| df["keyword"], | |
| (df["search_volume_pc"].fillna(0) + df["search_volume_mobile"].fillna(0)).astype(int), | |
| )) | |
| try: | |
| sp = build_autocomplete_path(keyword, vol_lookup) | |
| sp["source"] = "autocomplete" | |
| if len(sp["nodes"]) < 5: # ์๋์์ฑ์ด ๋น์ฝํ๋ฉด ์๋ฒ ๋ฉ ๊ฒฝ๋ก๋ก ๋์ฒด | |
| sp = build_search_path(keyword, df, emb) | |
| sp["source"] = "embedding" | |
| except Exception: | |
| sp = build_search_path(keyword, df, emb) | |
| sp["source"] = "embedding" | |
| result["search_path"] = sp | |
| # ํด๋ฌ์คํฐ ๋จ์ ์๋ ๋ถ๋ฅ (๋๋ ์ฐจํธ). Claude ํธ์ถ ์คํจํด๋ ๋๋จธ์ง๋ ๋ฐํ | |
| try: | |
| intent = classify_clusters(clusters) | |
| result["intent_breakdown"] = intent["intent_breakdown"] | |
| result["marketing_suggestion"] = intent["marketing_suggestion"] | |
| result["intent_usage"] = intent["usage"] | |
| except Exception as e: | |
| result["intent_breakdown"] = {} | |
| result["intent_error"] = str(e) | |
| # ์ฑ๋ณร์ฐ๋ น ์ถ์ ๋น์ค (๋ฐ์ดํฐ๋ฉ). ํค ์๊ฑฐ๋ ์คํจํด๋ ๋๋จธ์ง๋ ๋ฐํ | |
| try: | |
| result["demographics"] = demographics(keyword) | |
| except Exception as e: | |
| result["demographics"] = None | |
| result["demographics_error"] = str(e) | |
| # ํตํฉ ๋์๋ณด๋(Supabase)์ ์ ์ฌ โ ์๊ฒฉ์ฆ๋ช ์๊ฑฐ๋ ์คํจํด๋ ์๋ต์ ์ํฅ ์์ | |
| if supabase_store.enabled(): | |
| try: | |
| supabase_store.persist(result) | |
| result["supabase_persisted"] = True | |
| except Exception as e: | |
| result["supabase_error"] = str(e) | |
| cache.put(keyword, result) | |
| return {**result, "cached": False} | |