๊ฐ•๋ฏผ๊ท 
Refactor: Combine Backend and Frontend into Monorepo structure
9f03b39
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import sys
import os
import pandas as pd
import numpy as np
import threading
# ๊ฐ™์€ ํด๋”์˜ logic.py ์ž„ํฌํŠธ๋ฅผ ์œ„ํ•ด ๊ฒฝ๋กœ ์ถ”๊ฐ€
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
import logic
app = FastAPI(title="K-Recipe2Vec API")
# CORS ์„ค์ •
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Request Models ---
class IngredientRequest(BaseModel):
recipe_id: int
target: List[str]
stopwords: List[str] = []
w_w2v: float = 0.5
w_d2v: float = 0.5
w_method: float = 0.0
w_cat: float = 0.0
class CustomContextRequest(BaseModel):
context_ings: List[str]
target: List[str]
stopwords: List[str] = []
w_w2v: float = 0.5
w_d2v: float = 0.5
excluded: List[str] = []
# --- Startup ---
@app.on_event("startup")
def startup_event():
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ๋ชจ๋ธ ๋กœ๋”ฉ ์‹œ์ž‘ (์•ฑ ์‹œ์ž‘์„ ๋ง‰์ง€ ์•Š์Œ)
threading.Thread(target=logic.load_resources).start()
# --- ๋ถˆ์šฉ์–ด ์บ์‹œ ๋ฐ ํ•„ํ„ฐ๋ง ---
_stopwords_cache = None
def get_stopwords():
"""๋ถˆ์šฉ์–ด ๋ชฉ๋ก์„ ์บ์‹œํ•˜์—ฌ ๋ฐ˜ํ™˜"""
global _stopwords_cache
if _stopwords_cache is None:
try:
_stopwords_cache = set(logic.load_global_stopwords())
except:
_stopwords_cache = set()
return _stopwords_cache
def filter_ingredients(ingredients_list):
"""์žฌ๋ฃŒ ๋ชฉ๋ก์—์„œ ๋ถˆ์šฉ์–ด ์ œ๊ฑฐ"""
stopwords = get_stopwords()
if not stopwords:
return ingredients_list
return [ing for ing in ingredients_list if ing not in stopwords]
# --- Endpoints ---
@app.get("/")
def health_check():
# ๋ชจ๋ธ ๋กœ๋”ฉ ์ƒํƒœ ํ™•์ธ
status = "loading" if logic.df is None else "ok"
return {"status": status, "service": "K-Recipe2Vec API"}
@app.get("/recipes")
def list_recipes(limit: int = 50, offset: int = 0):
"""์ „์ฒด ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜)"""
logic.ensure_initialized()
try:
total = len(logic.df)
subset = logic.df.iloc[offset:offset+limit][['๋ ˆ์‹œํ”ผ์ผ๋ จ๋ฒˆํ˜ธ', '์š”๋ฆฌ๋ช…', '์žฌ๋ฃŒํ† ํฐ', '์š”๋ฆฌ๋ฐฉ๋ฒ•๋ณ„๋ช…', '์š”๋ฆฌ์ข…๋ฅ˜๋ณ„๋ช…_์„ธ๋ถ„ํ™”']]
output = []
for _, row in subset.iterrows():
output.append({
"id": int(row['๋ ˆ์‹œํ”ผ์ผ๋ จ๋ฒˆํ˜ธ']),
"name": row['์š”๋ฆฌ๋ช…'],
"ingredients": filter_ingredients(row['์žฌ๋ฃŒํ† ํฐ']),
"method": row['์š”๋ฆฌ๋ฐฉ๋ฒ•๋ณ„๋ช…'],
"category": row['์š”๋ฆฌ์ข…๋ฅ˜๋ณ„๋ช…_์„ธ๋ถ„ํ™”']
})
return {"total": total, "recipes": output}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/recipes/search")
def search_recipes(q: str):
"""์š”๋ฆฌ๋ช…์œผ๋กœ ๋ ˆ์‹œํ”ผ ๊ฒ€์ƒ‰"""
logic.ensure_initialized() # ๋กœ๋”ฉ ๋Œ€๊ธฐ
if not q: return []
try:
mask = logic.df['์š”๋ฆฌ๋ช…'].str.contains(q, case=False, na=False)
results = logic.df.loc[mask, ['๋ ˆ์‹œํ”ผ์ผ๋ จ๋ฒˆํ˜ธ', '์š”๋ฆฌ๋ช…', '์žฌ๋ฃŒํ† ํฐ']].head(20)
output = []
for _, row in results.iterrows():
output.append({
"id": int(row['๋ ˆ์‹œํ”ผ์ผ๋ จ๋ฒˆํ˜ธ']),
"name": row['์š”๋ฆฌ๋ช…'],
"ingredients": filter_ingredients(row['์žฌ๋ฃŒํ† ํฐ'])
})
return output
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/recipes/{recipe_id}")
def get_recipe_detail(recipe_id: int):
"""๋ ˆ์‹œํ”ผ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ"""
logic.ensure_initialized() # ๋กœ๋”ฉ ๋Œ€๊ธฐ
try:
row = logic.df[logic.df['๋ ˆ์‹œํ”ผ์ผ๋ จ๋ฒˆํ˜ธ'] == recipe_id]
if row.empty:
raise HTTPException(status_code=404, detail="Recipe not found")
row = row.iloc[0]
return {
"id": int(row['๋ ˆ์‹œํ”ผ์ผ๋ จ๋ฒˆํ˜ธ']),
"name": row['์š”๋ฆฌ๋ช…'],
"method": row['์š”๋ฆฌ๋ฐฉ๋ฒ•๋ณ„๋ช…'],
"category": row['์š”๋ฆฌ์ข…๋ฅ˜๋ณ„๋ช…_์„ธ๋ถ„ํ™”'],
"ingredients": filter_ingredients(row['์žฌ๋ฃŒํ† ํฐ'])
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/recommend/db/single")
def recommend_db_single(req: IngredientRequest):
"""DB ๋ ˆ์‹œํ”ผ ๊ธฐ๋ฐ˜ ๋‹จ์ผ ์žฌ๋ฃŒ ๋Œ€์ฒด"""
logic.ensure_initialized() # ๋กœ๋”ฉ ๋Œ€๊ธฐ
if not req.target:
raise HTTPException(status_code=400, detail="Target ingredient required")
try:
df = logic.substitute_single(
req.recipe_id, req.target[0], req.stopwords,
req.w_w2v, req.w_d2v, req.w_method, req.w_cat
)
if df.empty: return []
df = df.replace([np.inf, -np.inf], 0).fillna(0)
return df.to_dict(orient="records")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/recommend/db/multi")
def recommend_db_multi(req: IngredientRequest):
"""DB ๋ ˆ์‹œํ”ผ ๊ธฐ๋ฐ˜ ๋‹ค์ค‘ ์žฌ๋ฃŒ ๋Œ€์ฒด"""
logic.ensure_initialized() # ๋กœ๋”ฉ ๋Œ€๊ธฐ
try:
results = logic.substitute_multi(
req.recipe_id, req.target, req.stopwords,
req.w_w2v, req.w_d2v, req.w_method, req.w_cat
)
formatted = []
for subs, score, saving in results:
formatted.append({
"substitutes": subs,
"score": float(score),
"saving_score": int(saving)
})
return formatted
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/recommend/custom/single")
def recommend_custom_single(req: CustomContextRequest):
"""์‚ฌ์šฉ์ž ์ •์˜ ์žฌ๋ฃŒ ๊ธฐ๋ฐ˜ ๋‹จ์ผ ๋Œ€์ฒด"""
logic.ensure_initialized() # ๋กœ๋”ฉ ๋Œ€๊ธฐ
if not req.target:
raise HTTPException(status_code=400, detail="Target ingredient required")
try:
df = logic.substitute_single_custom(
req.target[0], req.context_ings, req.stopwords,
req.w_w2v, req.w_d2v, excluded_ings=req.excluded
)
if df.empty: return []
df = df.replace([np.inf, -np.inf], 0).fillna(0)
return df.to_dict(orient="records")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))