Spaces:
Running
Running
File size: 5,106 Bytes
21bdc64 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | # ํค์๋๋ฅผ ๋ฐ์ ๋ค์ด๋ฒ ์ค์๊ฐ ์กฐํ + ์บ์ฑ ํ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ 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
@app.get("/health")
def health():
return {"ok": True}
@app.get("/analyze")
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}
|