VibeCoding5

ํƒ๊ฐ€์ด๋ฒ„ยท2026๋…„ 2์›” 1์ผ

VibeCoding

๋ชฉ๋ก ๋ณด๊ธฐ
5/7
post-thumbnail

โœ…Week 5 Progress: Ch 06 ~ 07

๐ŸšถBasic Mission (Required)
PROJECT 9. Create and capture the "Please Take Care of My Refrigerator" app

โ €
๐ŸƒAdditional Mission (Optional)
Building an AI Agent Team
Use AI design agents to improve the UX design of the "Please Take Care of My Refrigerator" app and capture it

๐Ÿ“‹ ์ž‘์—… ์š”์•ฝ
ํ”„๋กœ์ ํŠธ๋ช…: Smart Recipe - AI ๋ƒ‰์žฅ๊ณ  ๋ ˆ์‹œํ”ผ ์ถ”์ฒœ

์ฃผ์š” ๊ธฐ๋Šฅ (3๋‹จ๊ณ„๋กœ ๊ตฌ์„ฑ):

Step 1: ๋ƒ‰์žฅ๊ณ /์‹์žฌ๋ฃŒ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ โ†’ AI๊ฐ€ ์žฌ๋ฃŒ ์ž๋™ ์ธ์‹ (Gemma 3 ๋ชจ๋ธ)
Step 2: ์ธ์‹๋œ ์žฌ๋ฃŒ ๊ธฐ๋ฐ˜ โ†’ AI ๋งž์ถค ๋ ˆ์‹œํ”ผ ์ƒ์„ฑ (DeepSeek ๋ชจ๋ธ)
Step 3: ์‚ฌ์šฉ์ž ์ธ์ฆ(ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ) + ๋ ˆ์‹œํ”ผ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ
๊ธฐ์ˆ  ์Šคํƒ:

Backend: Flask (Python)
Database: SQLite
Frontend: Vanilla HTML/CSS/JavaScript
AI API: OpenRouter (Gemma 3, DeepSeek ๋ฌด๋ฃŒ ๋ชจ๋ธ)

์ƒ์„ฑ๋œ README.md ๋‚ด์šฉ
ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ์ง€
์ฃผ์š” ๊ธฐ๋Šฅ ํ…Œ์ด๋ธ”
ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ํŠธ๋ฆฌ
๊ธฐ์ˆ  ์Šคํƒ ์ƒ์„ธ
์„ค์น˜ ๋ฐ ์‹คํ–‰ ๊ฐ€์ด๋“œ
API ์—”๋“œํฌ์ธํŠธ ๋ฌธ์„œ
ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๋ฐฉ๋ฒ•
์‚ฌ์šฉ ํ๋ฆ„ (Mermaid ๋‹ค์ด์–ด๊ทธ๋žจ)
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ
๊ธฐ์—ฌ ๊ฐ€์ด๋“œ
Git ์ปค๋ฐ‹ ๋ฐ ํ‘ธ์‹œ

๐Ÿ‘‰ GitHub์—์„œ ํ™•์ธ: https://github.com/sechan9999/recipe
https://sechan9999.github.io/recipe/
https://github.com/sechan9999/AIdiaryPdfSummarizer
https://sechan9999.github.io/AIdiaryPdfSummarizer/

โ €

๋ณธ ๊ฐ•์˜๋Š” ใ€Žํ˜ผ์ž ๊ณต๋ถ€ํ•˜๋Š” ๋ฐ”์ด๋ธŒ ์ฝ”๋”ฉ with ํด๋กœ๋“œ ์ฝ”๋“œใ€์˜ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ œ์ž‘ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๐Ÿ‘จโ€๐Ÿซ์ฃผ์š” ๊ฐ•์˜ ๋‚ด์šฉ
Ch 06. ํด๋กœ๋“œ ์ฝ”๋“œ์— API ๋‚ ๊ฐœ ๋‹ฌ๊ธฐ
06-1 ํ‹€๋กœ๋“œ ์ฝ”๋“œ์—์„œ API ์„ค์ •ํ•˜๊ธฐ

  • API์˜ ๊ฐœ๋…๊ณผ ํ™œ์šฉ ๋ฐฉ๋ฒ•
  • AI ๋ชจ๋ธ ์„ ํƒํ•˜๊ธฐ
  • AI ๋ชจ๋ธ ์—ฐ๊ฒฐํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ธฐ

06-2 ํด๋กœ๋“œ ์ฝ”๋“œ์™€ API๋กœ ๋งŒ๋“œ๋Š” '๋ƒ‰์žฅ๊ณ ๋ฅผ ๋ถ€ํƒํ•ด'

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„์„ ์œ„ํ•œ PRD ์ž‘์„ฑํ•˜๊ธฐ
  • 1๋‹จ๊ณ„: ๋ƒ‰์žฅ๊ณ  ์ด๋ฏธ์ง€์—์„œ ์žฌ๋ฃŒ ์ธ์‹ํ•˜๊ธฐ
  • 2๋‹จ๊ณ„: ๋ ˆ์‹œํ”ผ ์ƒ์„ฑํ•˜๊ธฐ
  • 3๋‹จ๊ณ„: ์‚ฌ์šฉ์ž์˜ ํ”„๋กœํ•„์— ๋ ˆ์‹œํ”ผ ์ €์žฅํ•˜๊ธฐ

๐Ÿ‘จโ€๐Ÿซ์ฃผ์š” ๊ฐ•์˜ ๋‚ด์šฉ
Ch 07. ํด๋กœ๋“œ ์ฝ”๋“œ AI ์—์ด์ „ํŠธ๋กœ ๊ฐœ๋ฐœํŒ€ ๊ตฌ์„ฑํ•˜๊ธฐ
07-1 ํด๋กœ๋“œ ์ฝ”๋“œ์˜ AI ์—์ด์ „ํŠธ ์ดํ•ดํ•˜๊ธฐ

  • ์ฝ”๋“œ ๋ฆฌ๋ทฐ์–ด ์—์ด์ „ํŠธ
  • ์ตœ์ ํ™” ์—์ด์ „ํŠธ & UX ๋””์ž์ธ ์—์ด์ „ํŠธ
  • ์„œ๋ธŒ์—์ด์ „ํŠธ ํ˜‘์—… ํ…Œ์ŠคํŠธํ•˜๊ธฐ

07-2 AI ์—์ด์ „ํŠธ๋กœ ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ์ž๋™ํ™”ํ•˜๊ธฐ

  • ๋‚˜๋งŒ์˜ AI ๊ฐœ๋ฐœํŒ€ ๊ตฌ์ถ•ํ•˜๊ธฐ
  • โ€˜AI ๊ณต๊ฐ ๋‹ค์ด์–ด๋ฆฌโ€™ ์•ฑ ๋งŒ๋“ค๊ธฐ
  • โ€˜PDF ์š”์•ฝ AIโ€™ ์•ฑ ๋งŒ๋“ค๊ธฐ

๐Ÿ”—์‹ค์Šต ์˜ˆ์ œ: https://github.com/taehojo/vibecoding
๐Ÿ”—์ €์ž๋‹˜๊ป˜ ์งˆ๋ฌธํ•˜๊ธฐ: https://github.com/taehojo/vibecoding/issues
๐Ÿ”—ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๋Š” ๋ฐ”์ด๋ธŒ ์ฝ”๋”ฉ with ํด๋กœ๋“œ ์ฝ”๋“œ: https://www.hanbit.co.kr/store/books/look.php?p_code=B1785590517&type=book&utm_source=inflearn&utm_medium=affiliate&utm_campaign=50004

๊ณต๊ฐ ๋‹ค์ด์–ด๋ฆฌ ์•ฑ
AI ๊ณต๊ฐ ๋‹ค์ด์–ด๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด ์ค˜. ์˜ค๋Š˜ ์žˆ์—ˆ๋˜ ์ผ์„ ํ•œ ์ค„๋กœ ์“ฐ๋ฉด, AI๊ฐ€ ๊ฐ์ •์„ ๋ถ„์„ํ•˜๊ณ  ๊ณต๊ฐํ•˜๋ฉฐ ์œ„๋กœํ•ด ์ฃผ๋Š” ์ผ๊ธฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด์•ผ.
backend-architect๊ฐ€ OpenRouter API๋ฅผ ์—ฐ๋™ํ•ด์„œ ๊ฐ์ • ๋ถ„์„๊ณผ ๊ณต๊ฐ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด ์ค˜.
DeepSeek V3.1 ๋ฌด๋ฃŒ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ณ , API ํ‚ค๋Š” ํ˜„์žฌ ํด๋”์˜ โ€˜.envโ€™ ํŒŒ์ผ์— ์ €์žฅ๋œ ๊ฒƒ์„ ์‚ฌ์šฉํ•ด.
frontend-developer๊ฐ€ ๋”ฐ๋œปํ•˜๊ณ  ํŽธ์•ˆํ•œ ๋А๋‚Œ์˜ ์ผ๊ธฐ์žฅ UI๋ฅผ ๋งŒ๋“  ๋‹ค์Œ, qa-engineer๊ฐ€ ์‹ค์ œ๋กœ ์—ฌ๋Ÿฌ ์ƒํ™ฉ์—์„œ ๋ฌธ์ œ์—†์ด ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด ์ค˜.
๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด ์™„์ „ํžˆ ํ•ด๊ฒฐํ•  ๋•Œ๊นŒ์ง€ ์ˆ˜์ •ํ•˜๊ณ , ์ตœ์ข… ๋ฒ„์ „์„ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฐ”๋กœ ์—ด ์ˆ˜ ์žˆ๋Š” โ€˜index.htmlโ€™ ํŒŒ์ผ๋กœ ๋งŒ๋“ค์–ด ์ค˜.

PDF ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•˜๋ฉด AI๊ฐ€ ์š”์•ฝํ•ด์ฃผ๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ๊ฑฐ์•ผ.
๋จผ์ € product-manager-prd๊ฐ€ PDF ๋ฌธ์„œ ์š”์•ฝ ์•ฑ์˜ ์ƒ์„ธ PRD์™€ ๊ธฐ๋Šฅ ๋ช…์„ธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ ,
backend-architect๊ฐ€ PDF ํŒŒ์ผ ์—…๋กœ๋“œ, ํ…์ŠคํŠธ ์ถ”์ถœ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด.
ai-integration-specialist๊ฐ€ OpenRouter API๋ฅผ ์—ฐ๋™ํ•ด์„œ ์ถ”์ถœ๋œ ํ…์ŠคํŠธ๋ฅผ ์š”์•ฝํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด ์ค˜.
DeepSeek V3.1 ๋ฌด๋ฃŒ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ณ , API ํ‚ค๋Š” ํ˜„์žฌ ํด๋”์˜ โ€˜.envโ€™ ํŒŒ์ผ์— ์ €์žฅ๋œ ๊ฒƒ์„ ์‚ฌ์šฉํ•ด.
frontend-developer๊ฐ€ ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ํŒŒ์ผ ์—…๋กœ๋“œ UI์™€ ์š”์•ฝ ๊ฒฐ๊ณผ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ํ‘œ์‹œํ•˜๋Š” ํ•œ๊ธ€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ๋‹ค์Œ,
qa-engineer๊ฐ€ ์‹ค์ œ๋กœ ์—ฌ๋Ÿฌ ์ƒํ™ฉ์—์„œ ๋ฌธ์ œ์—†์ด ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด ์ค˜. ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด ์™„์ „ํžˆ ํ•ด๊ฒฐํ•  ๋•Œ๊นŒ์ง€ ์ˆ˜์ •ํ•˜๊ณ , ์ตœ์ข… ๋ฒ„์ „์„ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฐ”๋กœ ์—ด ์ˆ˜ ์žˆ๋Š” โ€˜index_pdf.htmlโ€™ ํŒŒ์ผ๋กœ ๋งŒ๋“ค์–ด ์ค˜.

"""
๋ƒ‰์žฅ๊ณ  ์žฌ๋ฃŒ ์ธ์‹ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - Step 1, 2 & 3
Flask ๋ฐฑ์—”๋“œ ์„œ๋ฒ„
"""

import os
import json
import re
import sqlite3
import secrets
import logging
from datetime import datetime
import urllib.request
import urllib.error
from functools import wraps
from flask import Flask, render_template, request, jsonify, session, g
from dotenv import load_dotenv
import bcrypt

load_dotenv()

# ๋กœ๊น… ์„ค์ •
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

app = Flask(__name__)

# ๋ณด์•ˆ ์„ค์ •
MAX_CONTENT_SIZE = 10 * 1024 * 1024  # 10MB
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_SIZE
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', secrets.token_hex(32))

# ์„ธ์…˜ ์ฟ ํ‚ค ๋ณด์•ˆ ์„ค์ •
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ํ™œ์„ฑํ™” (๊ฐœ๋ฐœ ์‹œ False)
app.config['SESSION_COOKIE_SECURE'] = os.getenv('FLASK_ENV') == 'production'

OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY')

# ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฒ€์ฆ
if not OPENROUTER_API_KEY:
    logging.warning("OPENROUTER_API_KEY is not set. API calls will fail.")
BASE_URL = "https://openrouter.ai/api/v1"
DATABASE = 'smart_recipe.db'

# ์ด๋ฏธ์ง€ ์ธ์‹ ๋ชจ๋ธ (๋น„์ „ ์ง€์› ๋ฌด๋ฃŒ ๋ชจ๋ธ - ์šฐ์„ ์ˆœ์œ„)
IMAGE_MODELS = [
    "qwen/qwen2.5-vl-72b-instruct:free",
    "qwen/qwen2.5-vl-32b-instruct:free",
    "meta-llama/llama-4-scout:free",
    "moonshotai/kimi-vl-a3b-thinking:free",
    "google/gemma-3-27b-it:free",
]

# ํ…์ŠคํŠธ ์ƒ์„ฑ ๋ชจ๋ธ (๋ ˆ์‹œํ”ผ์šฉ)
TEXT_MODELS = [
    "qwen/qwen3-235b-a22b:free",
    "google/gemma-3-27b-it:free",
    "deepseek/deepseek-r1-0528:free",
    "meta-llama/llama-4-maverick:free",
]


# ===== ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค =====

def get_db():
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ"""
    if 'db' not in g:
        g.db = sqlite3.connect(DATABASE)
        g.db.row_factory = sqlite3.Row
    return g.db


@app.teardown_appcontext
def close_db(exception):
    """์š”์ฒญ ์ข…๋ฃŒ ์‹œ DB ์—ฐ๊ฒฐ ํ•ด์ œ"""
    db = g.pop('db', None)
    if db is not None:
        db.close()


def init_db():
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”"""
    db = sqlite3.connect(DATABASE)
    db.executescript('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT UNIQUE NOT NULL,
            password_hash TEXT NOT NULL,
            nickname TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

        CREATE TABLE IF NOT EXISTS user_preferences (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER UNIQUE REFERENCES users(id) ON DELETE CASCADE,
            allergies TEXT DEFAULT '[]',
            dietary_restrictions TEXT DEFAULT '[]',
            preferred_cuisines TEXT DEFAULT '[]',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

        CREATE TABLE IF NOT EXISTS saved_recipes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
            recipe_name TEXT NOT NULL,
            recipe_data TEXT NOT NULL,
            ingredients TEXT DEFAULT '[]',
            cuisine_type TEXT,
            difficulty TEXT,
            cook_time TEXT,
            rating INTEGER CHECK (rating >= 1 AND rating <= 5),
            notes TEXT,
            tags TEXT DEFAULT '[]',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

        CREATE TABLE IF NOT EXISTS analysis_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
            detected_ingredients TEXT DEFAULT '[]',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

        -- ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ธ๋ฑ์Šค
        CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
        CREATE INDEX IF NOT EXISTS idx_saved_recipes_user_id ON saved_recipes(user_id);
        CREATE INDEX IF NOT EXISTS idx_saved_recipes_created_at ON saved_recipes(created_at DESC);
        CREATE INDEX IF NOT EXISTS idx_analysis_history_user_id ON analysis_history(user_id);
    ''')
    db.commit()
    db.close()


# ์•ฑ ์‹œ์ž‘ ์‹œ DB ์ดˆ๊ธฐํ™”
with app.app_context():
    init_db()


def hash_password(password):
    """๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œํ™” (bcrypt ์‚ฌ์šฉ)"""
    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')


def verify_password(password, hashed):
    """๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (SHA256 โ†’ bcrypt ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ง€์›)"""
    # bcrypt ํ•ด์‹œ๋Š” $2๋กœ ์‹œ์ž‘ (์˜ˆ: $2b$12$...)
    if hashed.startswith('$2'):
        return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
    else:
        # ๊ธฐ์กด SHA256 ํ•ด์‹œ ํ˜ธํ™˜ (64์ž hex)
        import hashlib
        return hashed == hashlib.sha256(password.encode()).hexdigest()


def upgrade_password_hash(user_id, password):
    """SHA256 ํ•ด์‹œ๋ฅผ bcrypt๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ"""
    db = get_db()
    new_hash = hash_password(password)
    db.execute('UPDATE users SET password_hash = ? WHERE id = ?', (new_hash, user_id))
    db.commit()


def login_required(f):
    """๋กœ๊ทธ์ธ ํ•„์ˆ˜ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            return jsonify({"success": False, "error": "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"}), 401
        return f(*args, **kwargs)
    return decorated_function


def get_current_user():
    """ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์กฐํšŒ"""
    if 'user_id' not in session:
        return None
    db = get_db()
    user = db.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone()
    return dict(user) if user else None


# ===== OpenRouter API =====

def call_openrouter(model, messages, timeout=60):
    """OpenRouter API ํ˜ธ์ถœ"""
    url = f"{BASE_URL}/chat/completions"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {OPENROUTER_API_KEY}",
        "HTTP-Referer": "http://localhost:5000",
        "X-Title": "Smart Recipe"
    }
    data = json.dumps({
        "model": model,
        "messages": messages
    }).encode('utf-8')

    logging.info(f"[API] ๋ชจ๋ธ ํ˜ธ์ถœ: {model}")
    req = urllib.request.Request(url, data=data, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as response:
            result = json.loads(response.read().decode('utf-8'))
            logging.info(f"[API] ์„ฑ๊ณต: {model}")
            return result
    except urllib.error.HTTPError as e:
        error_body = e.read().decode('utf-8')
        logging.error(f"[API] HTTP {e.code} ์—๋Ÿฌ ({model}): {error_body[:500]}")
        try:
            return {"error": json.loads(error_body)}
        except:
            return {"error": {"message": f"HTTP {e.code}: {error_body[:200]}"}}
    except urllib.error.URLError as e:
        logging.error(f"[API] URL ์—๋Ÿฌ ({model}): {e.reason}")
        return {"error": {"message": f"์—ฐ๊ฒฐ ์‹คํŒจ: {e.reason}"}}
    except Exception as e:
        logging.error(f"[API] ์˜ˆ์™ธ ({model}): {str(e)}")
        return {"error": {"message": str(e)}}


# ===== Step 1: ์ด๋ฏธ์ง€ ๋ถ„์„ =====

def extract_ingredients(text):
    """ํ…์ŠคํŠธ์—์„œ ์žฌ๋ฃŒ ๋ฐฐ์—ด ์ถ”์ถœ"""
    json_match = re.search(r'\[.*?\]', text, re.DOTALL)
    if json_match:
        try:
            ingredients = json.loads(json_match.group())
            if isinstance(ingredients, list):
                return [str(i).strip() for i in ingredients if i]
        except json.JSONDecodeError:
            pass

    lines = re.split(r'[,\n]', text)
    ingredients = []
    for line in lines:
        cleaned = re.sub(r'^[\d\.\-\*\โ€ข]+\s*', '', line.strip())
        cleaned = re.sub(r'["\'\[\]]', '', cleaned)
        if cleaned and len(cleaned) > 1 and len(cleaned) < 50:
            ingredients.append(cleaned)

    return ingredients[:30]


def analyze_image(base64_image, mime_type="image/jpeg"):
    """์ด๋ฏธ์ง€ ๋ถ„์„ํ•˜์—ฌ ์žฌ๋ฃŒ ์ถ”์ถœ"""
    prompt = """์ด ๋ƒ‰์žฅ๊ณ /์‹์žฌ๋ฃŒ ์‚ฌ์ง„์—์„œ ๋ณด์ด๋Š” ๋ชจ๋“  ์‹์žฌ๋ฃŒ๋ฅผ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.

๋‹ค์Œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•ด์ฃผ์„ธ์š”:
["์žฌ๋ฃŒ1", "์žฌ๋ฃŒ2", "์žฌ๋ฃŒ3"]

์˜ˆ์‹œ: ["๊ณ„๋ž€", "์šฐ์œ ", "๋‹น๊ทผ", "์–‘ํŒŒ", "๋ผ์ง€๊ณ ๊ธฐ"]

์ฃผ์˜์‚ฌํ•ญ:
- ๋ณด์ด๋Š” ์‹์žฌ๋ฃŒ๋งŒ ๋‚˜์—ด
- ํ•œ๊ธ€๋กœ ์ž‘์„ฑ
- JSON ๋ฐฐ์—ด ํ˜•์‹๋งŒ ์ถœ๋ ฅ"""

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:{mime_type};base64,{base64_image}"
                    }
                }
            ]
        }
    ]

    last_error = "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"
    for model in IMAGE_MODELS:
        result = call_openrouter(model, messages, timeout=65)
        if "error" not in result:
            try:
                content = result['choices'][0]['message']['content']
                ingredients = extract_ingredients(content)
                if ingredients:
                    return {
                        "success": True,
                        "ingredients": ingredients,
                        "model": model,
                        "raw_response": content
                    }
                else:
                    logging.warning(f"[๋ถ„์„] {model}: ์žฌ๋ฃŒ ์ถ”์ถœ ์‹คํŒจ - ์‘๋‹ต: {content[:200]}")
                    last_error = "์ด๋ฏธ์ง€์—์„œ ์žฌ๋ฃŒ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค"
                    continue
            except (KeyError, IndexError) as e:
                logging.error(f"[๋ถ„์„] {model}: ์‘๋‹ต ํŒŒ์‹ฑ ์‹คํŒจ - {e}, ์‘๋‹ต: {str(result)[:300]}")
                last_error = "API ์‘๋‹ต ํ˜•์‹ ์˜ค๋ฅ˜"
                continue

        # ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”์ถœ (๋‹ค์–‘ํ•œ ์—๋Ÿฌ ๊ตฌ์กฐ ๋Œ€์‘)
        err = result.get('error', {})
        if isinstance(err, dict):
            err_msg = err.get('message', '') or err.get('error', {}).get('message', '')
            err_code = err.get('code', '') or err.get('error', {}).get('code', '')
        else:
            err_msg = str(err)
            err_code = ''

        last_error = err_msg or "API ํ˜ธ์ถœ ์‹คํŒจ"
        logging.warning(f"[๋ถ„์„] {model} ์‹คํŒจ: code={err_code}, msg={err_msg[:200]}")

        # 429(Rate Limit)๋Š” ๋‹ค์Œ ๋ชจ๋ธ ์‹œ๋„, ๊ทธ ์™ธ ์น˜๋ช…์  ์—๋Ÿฌ๋„ ๋‹ค์Œ ๋ชจ๋ธ ์‹œ๋„
        continue

    logging.error(f"[๋ถ„์„] ๋ชจ๋“  ๋ชจ๋ธ ์‹คํŒจ. ๋งˆ์ง€๋ง‰ ์—๋Ÿฌ: {last_error}")
    return {
        "success": False,
        "error": last_error,
        "ingredients": []
    }


# ===== Step 2: ๋ ˆ์‹œํ”ผ ์ƒ์„ฑ =====

def extract_recipe_json(text):
    """ํ…์ŠคํŠธ์—์„œ ๋ ˆ์‹œํ”ผ JSON ์ถ”์ถœ"""
    json_match = re.search(r'\{[\s\S]*\}', text)
    if json_match:
        try:
            recipe = json.loads(json_match.group())
            return recipe
        except json.JSONDecodeError:
            pass
    return parse_recipe_text(text)


def parse_recipe_text(text):
    """ํ…์ŠคํŠธ๋ฅผ ๋ ˆ์‹œํ”ผ ๊ตฌ์กฐ๋กœ ํŒŒ์‹ฑ"""
    recipe = {
        "name": "์ถ”์ฒœ ๋ ˆ์‹œํ”ผ",
        "description": "",
        "difficulty": "์ค‘๊ธ‰",
        "cookTime": "30๋ถ„",
        "servings": 2,
        "ingredients": [],
        "steps": [],
        "tips": ""
    }

    lines = text.split('\n')
    current_section = None

    for line in lines:
        line = line.strip()
        if not line:
            continue

        if '์š”๋ฆฌ' in line and ('์ด๋ฆ„' in line or '๋ช…' in line):
            current_section = 'name'
        elif '์žฌ๋ฃŒ' in line:
            current_section = 'ingredients'
        elif '์ˆœ์„œ' in line or '๋‹จ๊ณ„' in line or '๋ฐฉ๋ฒ•' in line or '๊ณผ์ •' in line:
            current_section = 'steps'
        elif 'ํŒ' in line or '์ฐธ๊ณ ' in line:
            current_section = 'tips'
        elif '์„ค๋ช…' in line:
            current_section = 'description'
        elif current_section == 'name' and ':' in line:
            recipe['name'] = line.split(':', 1)[1].strip()
        elif current_section == 'ingredients':
            cleaned = re.sub(r'^[\d\.\-\*\โ€ข]+\s*', '', line)
            if cleaned and len(cleaned) > 1:
                recipe['ingredients'].append({
                    "name": cleaned,
                    "amount": "",
                    "available": True
                })
        elif current_section == 'steps':
            cleaned = re.sub(r'^[\d\.\-\*\โ€ข]+\s*', '', line)
            if cleaned and len(cleaned) > 5:
                recipe['steps'].append(cleaned)
        elif current_section == 'tips':
            recipe['tips'] += line + " "

    if recipe['name'] == "์ถ”์ฒœ ๋ ˆ์‹œํ”ผ" and lines:
        first_meaningful = next((l for l in lines if len(l.strip()) > 2), None)
        if first_meaningful:
            recipe['name'] = first_meaningful.strip()[:50]

    return recipe


def generate_recipe(ingredients, cuisine, difficulty, cook_time, servings):
    """AI๋กœ ๋ ˆ์‹œํ”ผ ์ƒ์„ฑ"""
    prompt = f"""๋‹น์‹ ์€ ์ „๋ฌธ ์š”๋ฆฌ์‚ฌ์ž…๋‹ˆ๋‹ค. ์ฃผ์–ด์ง„ ์žฌ๋ฃŒ๋กœ ๋ง›์žˆ๋Š” ์š”๋ฆฌ ๋ ˆ์‹œํ”ผ๋ฅผ ์ถ”์ฒœํ•ด์ฃผ์„ธ์š”.

์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์žฌ๋ฃŒ: {', '.join(ingredients)}
์š”๋ฆฌ ์ข…๋ฅ˜: {cuisine}
๋‚œ์ด๋„: {difficulty}
์กฐ๋ฆฌ ์‹œ๊ฐ„: {cook_time}
์ธ์›: {servings}์ธ๋ถ„

๋ฐ˜๋“œ์‹œ ๋‹ค์Œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•ด์ฃผ์„ธ์š”:
{{
  "name": "์š”๋ฆฌ ์ด๋ฆ„",
  "description": "์š”๋ฆฌ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ์„ค๋ช… (1-2๋ฌธ์žฅ)",
  "difficulty": "{difficulty}",
  "cookTime": "{cook_time}",
  "servings": {servings},
  "ingredients": [
    {{"name": "์žฌ๋ฃŒ๋ช…", "amount": "๋ถ„๋Ÿ‰", "available": true}},
    {{"name": "์ถ”๊ฐ€ ํ•„์š”ํ•œ ์žฌ๋ฃŒ", "amount": "๋ถ„๋Ÿ‰", "available": false}}
  ],
  "steps": [
    "1. ์ฒซ ๋ฒˆ์งธ ์กฐ๋ฆฌ ๋‹จ๊ณ„",
    "2. ๋‘ ๋ฒˆ์งธ ์กฐ๋ฆฌ ๋‹จ๊ณ„",
    "3. ์„ธ ๋ฒˆ์งธ ์กฐ๋ฆฌ ๋‹จ๊ณ„"
  ],
  "tips": "์กฐ๋ฆฌ ํŒ์ด๋‚˜ ์ฃผ์˜์‚ฌํ•ญ"
}}

์ฃผ์˜์‚ฌํ•ญ:
- ์ฃผ์–ด์ง„ ์žฌ๋ฃŒ๋ฅผ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜์„ธ์š”
- ์ถ”๊ฐ€๋กœ ํ•„์š”ํ•œ ๊ธฐ๋ณธ ์žฌ๋ฃŒ(์†Œ๊ธˆ, ์„คํƒ•, ์‹์šฉ์œ  ๋“ฑ)๋Š” available: false๋กœ ํ‘œ์‹œ
- ๋‹จ๊ณ„๋Š” ๊ตฌ์ฒด์ ์ด๊ณ  ๋”ฐ๋ผํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์ž‘์„ฑ
- ๋ฐ˜๋“œ์‹œ ์œ ํšจํ•œ JSON ํ˜•์‹์œผ๋กœ ์‘๋‹ต"""

    messages = [
        {"role": "user", "content": prompt}
    ]

    last_error = None
    for model in TEXT_MODELS:
        result = call_openrouter(model, messages, timeout=90)
        if "error" not in result:
            content = result['choices'][0]['message']['content']
            recipe = extract_recipe_json(content)
            return {
                "success": True,
                "recipe": recipe,
                "model": model,
                "raw_response": content
            }
        last_error = result.get('error', {})
        error_code = last_error.get('code')
        if error_code and error_code != 429:
            break

    return {
        "success": False,
        "error": last_error.get('message', '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜') if last_error else '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜',
        "recipe": None
    }


# ===== ๋ผ์šฐํŠธ: ํŽ˜์ด์ง€ =====

@app.route('/')
def index():
    """๋ฉ”์ธ ํŽ˜์ด์ง€"""
    return render_template('index.html')


# ===== ๋ผ์šฐํŠธ: Step 1 & 2 API =====

@app.route('/api/analyze', methods=['POST'])
def analyze():
    """์ด๋ฏธ์ง€ ๋ถ„์„ API"""
    try:
        data = request.get_json()
        if not data or 'image' not in data:
            return jsonify({"success": False, "error": "์ด๋ฏธ์ง€๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"}), 400

        image_data = data['image']
        if ',' in image_data:
            header, base64_image = image_data.split(',', 1)
            mime_match = re.search(r'data:([^;]+)', header)
            mime_type = mime_match.group(1) if mime_match else 'image/jpeg'
        else:
            base64_image = image_data
            mime_type = 'image/jpeg'

        # ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๊ฒ€์ฆ (base64 โ†’ ์‹ค์ œ ํฌ๊ธฐ ์•ฝ 3/4)
        img_size_mb = len(base64_image) * 3 / 4 / (1024 * 1024)
        logging.info(f"[๋ถ„์„] ์ด๋ฏธ์ง€ ํฌ๊ธฐ: {img_size_mb:.1f}MB, MIME: {mime_type}")

        if img_size_mb > 8:
            return jsonify({"success": False, "error": "์ด๋ฏธ์ง€๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค (์ตœ๋Œ€ 8MB)"}), 400

        result = analyze_image(base64_image, mime_type)

        # ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋ฉด ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ (์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ)
        if 'user_id' in session and result.get('success'):
            db = get_db()
            try:
                db.execute(
                    'INSERT INTO analysis_history (user_id, detected_ingredients) VALUES (?, ?)',
                    (session['user_id'], json.dumps(result['ingredients']))
                )
                db.commit()
            except Exception as e:
                db.rollback()
                logging.error(f"Failed to save analysis history: {e}")

        return jsonify(result)

    except Exception as e:
        logging.error(f"[๋ถ„์„] ์˜ˆ๊ธฐ์น˜ ์•Š์€ ์—๋Ÿฌ: {str(e)}", exc_info=True)
        return jsonify({"success": False, "error": f"์„œ๋ฒ„ ์˜ค๋ฅ˜: {str(e)}"}), 500


@app.route('/api/recipe', methods=['POST'])
def recipe():
    """๋ ˆ์‹œํ”ผ ์ƒ์„ฑ API"""
    data = request.get_json()
    if not data or 'ingredients' not in data:
        return jsonify({"success": False, "error": "์žฌ๋ฃŒ ๋ชฉ๋ก์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"}), 400

    ingredients = data.get('ingredients', [])
    cuisine = data.get('cuisine', '์ƒ๊ด€์—†์Œ')
    difficulty = data.get('difficulty', '์ค‘๊ธ‰')
    cook_time = data.get('cookTime', '30๋ถ„ ์ด๋‚ด')
    servings = data.get('servings', 2)

    if not ingredients:
        return jsonify({"success": False, "error": "์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ์žฌ๋ฃŒ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"}), 400

    result = generate_recipe(ingredients, cuisine, difficulty, cook_time, servings)
    return jsonify(result)


# ===== Step 3: ์ธ์ฆ API =====

@app.route('/api/auth/register', methods=['POST'])
def register():
    """ํšŒ์›๊ฐ€์ž…"""
    data = request.get_json()
    email = data.get('email', '').strip()
    password = data.get('password', '')
    nickname = data.get('nickname', '').strip()

    if not email or not password:
        return jsonify({"success": False, "error": "์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"}), 400

    if len(password) < 6:
        return jsonify({"success": False, "error": "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 6์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"}), 400

    db = get_db()
    existing = db.execute('SELECT id FROM users WHERE email = ?', (email,)).fetchone()
    if existing:
        return jsonify({"success": False, "error": "์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค"}), 400

    password_hash = hash_password(password)
    cursor = db.execute(
        'INSERT INTO users (email, password_hash, nickname) VALUES (?, ?, ?)',
        (email, password_hash, nickname or email.split('@')[0])
    )
    db.commit()

    user_id = cursor.lastrowid
    db.execute('INSERT INTO user_preferences (user_id) VALUES (?)', (user_id,))
    db.commit()

    session['user_id'] = user_id
    return jsonify({
        "success": True,
        "user": {"id": user_id, "email": email, "nickname": nickname or email.split('@')[0]}
    })


@app.route('/api/auth/login', methods=['POST'])
def login():
    """๋กœ๊ทธ์ธ"""
    data = request.get_json()
    email = data.get('email', '').strip()
    password = data.get('password', '')

    if not email or not password:
        return jsonify({"success": False, "error": "์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"}), 400

    db = get_db()
    user = db.execute('SELECT * FROM users WHERE email = ?', (email,)).fetchone()

    if not user or not verify_password(password, user['password_hash']):
        logging.warning(f"Failed login attempt for {email}")
        return jsonify({"success": False, "error": "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"}), 401

    # SHA256 โ†’ bcrypt ์ž๋™ ์—…๊ทธ๋ ˆ์ด๋“œ
    if not user['password_hash'].startswith('$2'):
        upgrade_password_hash(user['id'], password)
        logging.info(f"Password hash upgraded to bcrypt for user {user['id']}")

    session['user_id'] = user['id']
    return jsonify({
        "success": True,
        "user": {"id": user['id'], "email": user['email'], "nickname": user['nickname']}
    })


@app.route('/api/auth/logout', methods=['POST'])
def logout():
    """๋กœ๊ทธ์•„์›ƒ"""
    session.pop('user_id', None)
    return jsonify({"success": True})


@app.route('/api/auth/me', methods=['GET'])
def get_me():
    """ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด"""
    user = get_current_user()
    if not user:
        return jsonify({"success": False, "user": None})

    return jsonify({
        "success": True,
        "user": {
            "id": user['id'],
            "email": user['email'],
            "nickname": user['nickname']
        }
    })


# ===== Step 3: ํ”„๋กœํ•„ API =====

@app.route('/api/profile', methods=['GET'])
@login_required
def get_profile():
    """ํ”„๋กœํ•„ ์กฐํšŒ"""
    user = get_current_user()
    db = get_db()
    prefs = db.execute(
        'SELECT * FROM user_preferences WHERE user_id = ?',
        (session['user_id'],)
    ).fetchone()

    return jsonify({
        "success": True,
        "profile": {
            "email": user['email'],
            "nickname": user['nickname'],
            "preferences": {
                "allergies": json.loads(prefs['allergies']) if prefs else [],
                "dietary_restrictions": json.loads(prefs['dietary_restrictions']) if prefs else [],
                "preferred_cuisines": json.loads(prefs['preferred_cuisines']) if prefs else []
            }
        }
    })


@app.route('/api/profile', methods=['PUT'])
@login_required
def update_profile():
    """ํ”„๋กœํ•„ ์ˆ˜์ •"""
    data = request.get_json()
    db = get_db()

    if 'nickname' in data:
        db.execute(
            'UPDATE users SET nickname = ?, updated_at = ? WHERE id = ?',
            (data['nickname'], datetime.now(), session['user_id'])
        )

    if 'preferences' in data:
        prefs = data['preferences']
        db.execute('''
            UPDATE user_preferences SET
                allergies = ?,
                dietary_restrictions = ?,
                preferred_cuisines = ?
            WHERE user_id = ?
        ''', (
            json.dumps(prefs.get('allergies', [])),
            json.dumps(prefs.get('dietary_restrictions', [])),
            json.dumps(prefs.get('preferred_cuisines', [])),
            session['user_id']
        ))

    db.commit()
    return jsonify({"success": True})


# ===== Step 3: ๋ ˆ์‹œํ”ผ ์ €์žฅ API =====

@app.route('/api/recipes/save', methods=['POST'])
@login_required
def save_recipe():
    """๋ ˆ์‹œํ”ผ ์ €์žฅ"""
    data = request.get_json()
    recipe_data = data.get('recipe')

    if not recipe_data:
        return jsonify({"success": False, "error": "๋ ˆ์‹œํ”ผ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"}), 400

    db = get_db()
    cursor = db.execute('''
        INSERT INTO saved_recipes
        (user_id, recipe_name, recipe_data, ingredients, cuisine_type, difficulty, cook_time, notes, tags)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    ''', (
        session['user_id'],
        recipe_data.get('name', '์ €์žฅ๋œ ๋ ˆ์‹œํ”ผ'),
        json.dumps(recipe_data),
        json.dumps(data.get('ingredients', [])),
        data.get('cuisine_type', ''),
        recipe_data.get('difficulty', ''),
        recipe_data.get('cookTime', ''),
        data.get('notes', ''),
        json.dumps(data.get('tags', []))
    ))
    db.commit()

    return jsonify({"success": True, "recipe_id": cursor.lastrowid})


@app.route('/api/recipes', methods=['GET'])
@login_required
def get_saved_recipes():
    """์ €์žฅ๋œ ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก"""
    db = get_db()
    recipes = db.execute('''
        SELECT * FROM saved_recipes
        WHERE user_id = ?
        ORDER BY created_at DESC
    ''', (session['user_id'],)).fetchall()

    result = []
    for r in recipes:
        result.append({
            "id": r['id'],
            "name": r['recipe_name'],
            "recipe": json.loads(r['recipe_data']),
            "ingredients": json.loads(r['ingredients']),
            "cuisine_type": r['cuisine_type'],
            "difficulty": r['difficulty'],
            "cook_time": r['cook_time'],
            "rating": r['rating'],
            "notes": r['notes'],
            "tags": json.loads(r['tags']),
            "created_at": r['created_at']
        })

    return jsonify({"success": True, "recipes": result})


@app.route('/api/recipes/<int:recipe_id>', methods=['GET'])
@login_required
def get_recipe_detail(recipe_id):
    """๋ ˆ์‹œํ”ผ ์ƒ์„ธ"""
    db = get_db()
    r = db.execute(
        'SELECT * FROM saved_recipes WHERE id = ? AND user_id = ?',
        (recipe_id, session['user_id'])
    ).fetchone()

    if not r:
        return jsonify({"success": False, "error": "๋ ˆ์‹œํ”ผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}), 404

    return jsonify({
        "success": True,
        "recipe": {
            "id": r['id'],
            "name": r['recipe_name'],
            "recipe": json.loads(r['recipe_data']),
            "ingredients": json.loads(r['ingredients']),
            "cuisine_type": r['cuisine_type'],
            "difficulty": r['difficulty'],
            "cook_time": r['cook_time'],
            "rating": r['rating'],
            "notes": r['notes'],
            "tags": json.loads(r['tags']),
            "created_at": r['created_at']
        }
    })


@app.route('/api/recipes/<int:recipe_id>', methods=['PUT'])
@login_required
def update_recipe(recipe_id):
    """๋ ˆ์‹œํ”ผ ์ˆ˜์ • (๋ฉ”๋ชจ, ํ‰์ )"""
    data = request.get_json()
    db = get_db()

    r = db.execute(
        'SELECT id FROM saved_recipes WHERE id = ? AND user_id = ?',
        (recipe_id, session['user_id'])
    ).fetchone()

    if not r:
        return jsonify({"success": False, "error": "๋ ˆ์‹œํ”ผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}), 404

    # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ธฐ๋ฐ˜ ์—…๋ฐ์ดํŠธ (SQL Injection ๋ฐฉ์ง€)
    ALLOWED_FIELDS = {'rating', 'notes', 'tags'}
    updates = []
    params = []

    if 'rating' in data and 'rating' in ALLOWED_FIELDS:
        rating = data['rating']
        if isinstance(rating, int) and 1 <= rating <= 5:
            updates.append('rating = ?')
            params.append(rating)

    if 'notes' in data and 'notes' in ALLOWED_FIELDS:
        notes = data['notes']
        if isinstance(notes, str) and len(notes) <= 1000:
            updates.append('notes = ?')
            params.append(notes)

    if 'tags' in data and 'tags' in ALLOWED_FIELDS:
        tags = data['tags']
        if isinstance(tags, list):
            updates.append('tags = ?')
            params.append(json.dumps(tags))

    if updates:
        params.append(recipe_id)
        # ์•ˆ์ „ํ•œ ์ •์  ์ฟผ๋ฆฌ ๊ตฌ์„ฑ (ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ํ•„๋“œ๋งŒ ์‚ฌ์šฉ)
        query = 'UPDATE saved_recipes SET ' + ', '.join(updates) + ' WHERE id = ?'
        db.execute(query, params)
        db.commit()

    return jsonify({"success": True})


@app.route('/api/recipes/<int:recipe_id>', methods=['DELETE'])
@login_required
def delete_recipe(recipe_id):
    """๋ ˆ์‹œํ”ผ ์‚ญ์ œ"""
    db = get_db()

    r = db.execute(
        'SELECT id FROM saved_recipes WHERE id = ? AND user_id = ?',
        (recipe_id, session['user_id'])
    ).fetchone()

    if not r:
        return jsonify({"success": False, "error": "๋ ˆ์‹œํ”ผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}), 404

    db.execute('DELETE FROM saved_recipes WHERE id = ?', (recipe_id,))
    db.commit()

    return jsonify({"success": True})


# ===== Step 3: ํžˆ์Šคํ† ๋ฆฌ API =====

@app.route('/api/history/analysis', methods=['GET'])
@login_required
def get_analysis_history():
    """๋ถ„์„ ํžˆ์Šคํ† ๋ฆฌ"""
    db = get_db()
    history = db.execute('''
        SELECT * FROM analysis_history
        WHERE user_id = ?
        ORDER BY created_at DESC
        LIMIT 20
    ''', (session['user_id'],)).fetchall()

    result = []
    for h in history:
        result.append({
            "id": h['id'],
            "ingredients": json.loads(h['detected_ingredients']),
            "created_at": h['created_at']
        })

    return jsonify({"success": True, "history": result})


@app.route('/api/history/stats', methods=['GET'])
@login_required
def get_stats():
    """ํ†ต๊ณ„ ์ •๋ณด"""
    db = get_db()

    # ์ €์žฅ๋œ ๋ ˆ์‹œํ”ผ ์ˆ˜
    recipe_count = db.execute(
        'SELECT COUNT(*) as count FROM saved_recipes WHERE user_id = ?',
        (session['user_id'],)
    ).fetchone()['count']

    # ๋ถ„์„ ํšŸ์ˆ˜
    analysis_count = db.execute(
        'SELECT COUNT(*) as count FROM analysis_history WHERE user_id = ?',
        (session['user_id'],)
    ).fetchone()['count']

    # ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ์žฌ๋ฃŒ (SQLite JSON1 extension์œผ๋กœ DB์—์„œ ์ง์ ‘ ์ง‘๊ณ„)
    # json_each()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ JSON ๋ฐฐ์—ด์„ ํ–‰์œผ๋กœ ํ™•์žฅํ•˜๊ณ  GROUP BY๋กœ ์ง‘๊ณ„
    top_ingredients_rows = db.execute('''
        SELECT ingredient.value as name, COUNT(*) as count
        FROM analysis_history, json_each(detected_ingredients) as ingredient
        WHERE user_id = ?
        GROUP BY ingredient.value
        ORDER BY count DESC
        LIMIT 10
    ''', (session['user_id'],)).fetchall()

    top_ingredients = [(row['name'], row['count']) for row in top_ingredients_rows]

    return jsonify({
        "success": True,
        "stats": {
            "saved_recipes": recipe_count,
            "analysis_count": analysis_count,
            "top_ingredients": [{"name": k, "count": v} for k, v in top_ingredients]
        }
    })


# ===== ์ง„๋‹จ ์—”๋“œํฌ์ธํŠธ (๊ฐœ๋ฐœ์šฉ) =====

@app.route('/api/debug/test-api', methods=['GET'])
def test_api():
    """API ํ‚ค ๋ฐ ๋ชจ๋ธ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ"""
    if os.getenv('FLASK_ENV') == 'production':
        return jsonify({"error": "Not available in production"}), 403

    results = []
    key_preview = f"{OPENROUTER_API_KEY[:15]}...{OPENROUTER_API_KEY[-4:]}" if OPENROUTER_API_KEY else "NOT SET"

    # ๊ฐ„๋‹จํ•œ ํ…์ŠคํŠธ ์š”์ฒญ์œผ๋กœ API ํ‚ค ์œ ํšจ์„ฑ ํ…Œ์ŠคํŠธ
    test_messages = [{"role": "user", "content": "Say hello in Korean, one word only"}]

    for model in IMAGE_MODELS[:2]:  # ์ฒ˜์Œ 2๊ฐœ ๋ชจ๋ธ๋งŒ ํ…Œ์ŠคํŠธ
        result = call_openrouter(model, test_messages, timeout=15)
        results.append({
            "model": model,
            "success": "error" not in result,
            "response": result.get('choices', [{}])[0].get('message', {}).get('content', '')[:100] if "error" not in result else str(result.get('error', ''))[:200]
        })

    return jsonify({
        "api_key": key_preview,
        "image_models": IMAGE_MODELS,
        "text_models": TEXT_MODELS,
        "test_results": results
    })


if __name__ == '__main__':
    # ํ”„๋กœ๋•์…˜์—์„œ๋Š” FLASK_ENV=production์œผ๋กœ ์„ค์ •
    debug_mode = os.getenv('FLASK_ENV', 'development') == 'development'
    app.run(debug=debug_mode, port=5000)

profile
๋” ๋‚˜์€ ์„ธ์ƒ์€ ๊ฐ€๋Šฅํ•˜๋‹ค๋ฅผ ๋ฏฟ๊ณ  ์‹ค์ฒœํ•˜๋Š” ํ™œ๋™๊ฐ€

0๊ฐœ์˜ ๋Œ“๊ธ€