๐ŸŽฏ g5se ์˜จ๋ผ์ธ ๋ ˆ์‹œํ”ผ ๋ถ - ๋‚˜๋งŒ์˜ ์š”๋ฆฌ ์•„์นด์ด๋ธŒ & ์žฌ๋ฃŒ ๊ฒ€์ƒ‰ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ค‘๊ธ‰ PHP g5se AI: Claude

๐ŸŽฏ g5se ์˜จ๋ผ์ธ ๋ ˆ์‹œํ”ผ ๋ถ - ๋‚˜๋งŒ์˜ ์š”๋ฆฌ ์•„์นด์ด๋ธŒ & ์žฌ๋ฃŒ ๊ฒ€์ƒ‰

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… g5se-recipe-book-archive
์กฐํšŒ์ˆ˜ 55 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐Ÿ“– ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

์ž์ฃผ ๋งŒ๋“œ๋Š” ์š”๋ฆฌ ๋ ˆ์‹œํ”ผ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ์ •๋ฆฌํ•˜๊ณ , ๋ƒ‰์žฅ๊ณ ์— ์žˆ๋Š” ์žฌ๋ฃŒ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์š”๋ฆฌ๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐœ์ธ ๋ ˆ์‹œํ”ผ ๋ถ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์š”๋ฆฌ ์‚ฌ์ง„, ์žฌ๋ฃŒ ๋ชฉ๋ก, ์กฐ๋ฆฌ ์ˆœ์„œ๋ฅผ ๊ธฐ๋กํ•˜๊ณ , ํƒœ๊ทธ์™€ ์žฌ๋ฃŒ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง์œผ๋กœ ์›ํ•˜๋Š” ๋ ˆ์‹œํ”ผ๋ฅผ ๋น ๋ฅด๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœจ ํ•„์ˆ˜ ๊ธฐ๋Šฅ

๋ ˆ์‹œํ”ผ ๊ด€๋ฆฌ

  • ๋ ˆ์‹œํ”ผ ๋“ฑ๋ก (์ œ๋ชฉ, ์„ค๋ช…, ์กฐ๋ฆฌ์‹œ๊ฐ„, ๋‚œ์ด๋„, ๋ถ„๋Ÿ‰)
  • ์žฌ๋ฃŒ ๋ชฉ๋ก ์ž…๋ ฅ (์žฌ๋ฃŒ๋ช…, ์ˆ˜๋Ÿ‰, ๋‹จ์œ„)
  • ์กฐ๋ฆฌ ๋‹จ๊ณ„๋ณ„ ์„ค๋ช… (์ˆœ์„œ๋Œ€๋กœ ํ…์ŠคํŠธ ์ž…๋ ฅ)
  • ์™„์„ฑ ์‚ฌ์ง„ ์—…๋กœ๋“œ (์„ ํƒ)
  • ํƒœ๊ทธ ์ถ”๊ฐ€ (ํ•œ์‹, ์–‘์‹, ๊ฐ„ํŽธ์‹, ๋””์ €ํŠธ ๋“ฑ)

๋ ˆ์‹œํ”ผ ๊ฒ€์ƒ‰ & ํ•„ํ„ฐ๋ง

  • ์žฌ๋ฃŒ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ("๊ณ„๋ž€, ์–‘ํŒŒ"๋กœ ๊ฒ€์ƒ‰ โ†’ ๋‘ ์žฌ๋ฃŒ๊ฐ€ ๋ชจ๋‘ ํฌํ•จ๋œ ๋ ˆ์‹œํ”ผ ํ‘œ์‹œ)
  • ํƒœ๊ทธ ํ•„ํ„ฐ๋ง (ํ•œ์‹๋งŒ ๋ณด๊ธฐ, ๋””์ €ํŠธ๋งŒ ๋ณด๊ธฐ)
  • ๋‚œ์ด๋„/์กฐ๋ฆฌ์‹œ๊ฐ„ ํ•„ํ„ฐ
  • ๋ ˆ์‹œํ”ผ ์นด๋“œ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ (์ธ๋„ค์ผ + ์ œ๋ชฉ + ํƒœ๊ทธ)

๋ ˆ์‹œํ”ผ ์ƒ์„ธ ํŽ˜์ด์ง€

  • ์žฌ๋ฃŒ ์ฒดํฌ๋ฆฌ์ŠคํŠธ (์ค€๋น„ํ•œ ์žฌ๋ฃŒ ์ฒดํฌ ๊ธฐ๋Šฅ)
  • ์กฐ๋ฆฌ ๋‹จ๊ณ„ ํŽผ์ณ๋ณด๊ธฐ
  • ๋ฉ”๋ชจ ๋‚จ๊ธฐ๊ธฐ ("๋‹ค์Œ์—” ์„คํƒ• ๋œ ๋„ฃ๊ธฐ" ๊ฐ™์€ ๊ฐœ์ธ ๋…ธํŠธ)
  • ์ฆ๊ฒจ์ฐพ๊ธฐ ํ‘œ์‹œ

๋Œ€์‹œ๋ณด๋“œ

  • ์ตœ๊ทผ ์ถ”๊ฐ€ํ•œ ๋ ˆ์‹œํ”ผ 3๊ฐœ
  • ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก
  • ์ „์ฒด ๋ ˆ์‹œใƒ” ์ˆ˜ ํ†ต๊ณ„

๐ŸŽ ๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ

  • ๋ ˆ์‹œํ”ผ ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์„ค์ •
  • ๋ ˆ์‹œํ”ผ ์ธ์‡„ ๋ชจ๋“œ (A4 ์šฉ์ง€์— ์ตœ์ ํ™”๋œ ๋ ˆ์ด์•„์›ƒ)
  • ์žฌ๋ฃŒ ์ž๋™์™„์„ฑ (์ด์ „์— ์ž…๋ ฅํ•œ ์žฌ๋ฃŒ๋ช… ์ œ์•ˆ)
  • ์กฐ๋ฆฌ ํƒ€์ด๋จธ (๋ ˆ์‹œํ”ผ ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ๋‹จ๊ณ„๋ณ„ ํƒ€์ด๋จธ ์‹คํ–‰)
  • ๋ ˆ์‹œํ”ผ ํ‰์  (๋ณ„์  5์  ๋งŒ์ )

๐ŸŽจ ๋””์ž์ธ ํฌ์ธํŠธ

  • ๋ ˆ์‹œํ”ผ ์นด๋“œ๋Š” Pinterest ์Šคํƒ€์ผ ๊ทธ๋ฆฌ๋“œ (masonry layout)
  • ์žฌ๋ฃŒ ์ž…๋ ฅ ์‹œ ๋™์ ์œผ๋กœ ํ–‰ ์ถ”๊ฐ€/์‚ญ์ œ ๊ฐ€๋Šฅํ•œ ํผ
  • ์กฐ๋ฆฌ ๋‹จ๊ณ„๋Š” ๋ฒˆํ˜ธ ๋ฑƒ์ง€์™€ ํ•จ๊ป˜ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ ฌ
  • ๋‹คํฌ๋ชจ๋“œ์—์„œ๋„ ์Œ์‹ ์‚ฌ์ง„์ด ์ž˜ ๋ณด์ด๋„๋ก ์นด๋“œ ๋ฐฐ๊ฒฝ ๋Œ€๋น„ ์กฐ์ •
  • ์žฌ๋ฃŒ ๊ฒ€์ƒ‰ ์ž…๋ ฅ์ฐฝ์€ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์—ฌ๋Ÿฌ ์žฌ๋ฃŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข g5se ํ”„๋ ˆ์ž„์›Œํฌ ๊ธฐ๋ฐ˜ (Laravel/React/Vue ์‚ฌ์šฉ ๊ธˆ์ง€)
  • โ€ข ํด๋” ๊ธฐ๋ฐ˜ ์ž๋™ ๋ผ์šฐํŒ… ์‚ฌ์šฉ (app/modules/recipes/index.php โ†’ /recipes)
  • โ€ข modern shell (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php) ์ ์šฉ ํ•„์ˆ˜
  • โ€ข ๋””์ž์ธ ํ† ํฐ ์‚ฌ์šฉ (--m-surface, --m-text, --m-shadow, --m-radius-lg ๋“ฑ)
  • โ€ข SQLite DB ์‚ฌ์šฉ (G5_DATA_PATH/modules/recipes.sqlite)
  • โ€ข ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๋Š” G5_DATA_PATH/modules/recipes/ ํด๋”์— ์ €์žฅ
  • โ€ข prepared statement๋กœ SQL ์ธ์ ์…˜ ๋ฐฉ์ง€
  • โ€ข htmlspecialchars()๋กœ XSS ๋ฐฉ์ง€
  • โ€ข ์™ธ๋ถ€ ๋นŒ๋“œ ๋„๊ตฌ(npm/webpack) ์‚ฌ์šฉ ๊ธˆ์ง€
  • โ€ข ์ˆœ์ˆ˜ HTML/CSS/JS๋กœ ๊ตฌํ˜„

ํ”„๋กฌํ”„ํŠธ ๋ ˆ์‹œํ”ผ

์•„๋ž˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ AI์—๊ฒŒ ๋ณด๋‚ด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ณต์‚ฌ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜์„ธ์š”.

1
Step 1
g5se๊ฐ€ ์„ค์น˜๋œ ๋””๋ ‰ํ„ฐ๋ฆฌ(๋ฃจํŠธ์— `index.php`ยท`app/`์ด ์žˆ๋Š” ๊ณณ)์—์„œ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. (g5se ์„ค์น˜๋Š” https://github.com/gnuboard/g5se ์ฐธ๊ณ )

**๋ชฉํ‘œ:** ๋ ˆ์‹œํ”ผ ๋ถ ๋ฉ”์ธ ํŽ˜์ด์ง€ ๊ณจ๊ฒฉ ์ƒ์„ฑ + DB ์ดˆ๊ธฐํ™”

---

## 1) ํด๋” & ํŒŒ์ผ ์ƒ์„ฑ

`app/modules/recipes/index.php` ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”. ์ด ํŒŒ์ผ์€ `/recipes` URL๋กœ ์ž๋™ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค.

## 2) modern shell ๊ณจ๊ฒฉ ์ ์šฉ

์•„๋ž˜ ๊ตฌ์กฐ๋กœ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”:

```php
<?php
include_once('./_common.php');

// DB ์ดˆ๊ธฐํ™” (์ฒซ ์ ‘๊ทผ ์‹œ ํ…Œ์ด๋ธ” ์ž๋™ ์ƒ์„ฑ)
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
@mkdir(G5_DATA_PATH.'/modules/recipes', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/recipes.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// recipes ํ…Œ์ด๋ธ” ์ƒ์„ฑ
$db->exec("
    CREATE TABLE IF NOT EXISTS recipes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        description TEXT,
        cooking_time INTEGER,
        difficulty TEXT,
        servings INTEGER,
        image TEXT,
        tags TEXT,
        favorite INTEGER DEFAULT 0,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP
    )
");

// ingredients ํ…Œ์ด๋ธ” ์ƒ์„ฑ
$db->exec("
    CREATE TABLE IF NOT EXISTS ingredients (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        recipe_id INTEGER NOT NULL,
        name TEXT NOT NULL,
        quantity TEXT,
        unit TEXT,
        FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
    )
");

// steps ํ…Œ์ด๋ธ” ์ƒ์„ฑ
$db->exec("
    CREATE TABLE IF NOT EXISTS steps (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        recipe_id INTEGER NOT NULL,
        step_number INTEGER NOT NULL,
        description TEXT NOT NULL,
        FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
    )
");

// notes ํ…Œ์ด๋ธ” ์ƒ์„ฑ
$db->exec("
    CREATE TABLE IF NOT EXISTS notes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        recipe_id INTEGER NOT NULL,
        content TEXT NOT NULL,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
    )
");

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <div class="recipe-header" style="text-align: center; margin-bottom: 40px;">
            <h1 style="font-size: 2.5rem; font-weight: 700; color: var(--m-text); margin-bottom: 8px;">๐Ÿณ ๋‚˜๋งŒ์˜ ๋ ˆ์‹œํ”ผ ๋ถ</h1>
            <p style="font-size: 1.1rem; color: var(--m-text-muted);">๋ง›์žˆ๋Š” ์š”๋ฆฌ๋ฅผ ๊ธฐ๋กํ•˜๊ณ , ์žฌ๋ฃŒ๋กœ ๊ฒ€์ƒ‰ํ•˜์„ธ์š”</p>
        </div>

        <div class="dashboard-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 40px;">
            <div class="stat-card" style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <div style="font-size: 2rem; margin-bottom: 8px;">๐Ÿ“š</div>
                <div style="font-size: 2rem; font-weight: 700; color: var(--m-text);">0</div>
                <div style="font-size: 0.9rem; color: var(--m-text-muted);">์ „์ฒด ๋ ˆ์‹œํ”ผ</div>
            </div>
            <div class="stat-card" style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <div style="font-size: 2rem; margin-bottom: 8px;">โญ</div>
                <div style="font-size: 2rem; font-weight: 700; color: var(--m-text);">0</div>
                <div style="font-size: 0.9rem; color: var(--m-text-muted);">์ฆ๊ฒจ์ฐพ๊ธฐ</div>
            </div>
            <div class="stat-card" style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <div style="font-size: 2rem; margin-bottom: 8px;">๐Ÿ”ฅ</div>
                <div style="font-size: 2rem; font-weight: 700; color: var(--m-text);">0</div>
                <div style="font-size: 0.9rem; color: var(--m-text-muted);">์ตœ๊ทผ 7์ผ</div>
            </div>
        </div>

        <div style="text-align: center; padding: 60px 20px; background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
            <div style="font-size: 4rem; margin-bottom: 16px;">๐Ÿฝ๏ธ</div>
            <p style="font-size: 1.2rem; color: var(--m-text-muted); margin-bottom: 24px;">์•„์ง ๋“ฑ๋ก๋œ ๋ ˆ์‹œํ”ผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</p>
            <a href="/recipes/add" style="display: inline-block; background: #FF6B35; color: white; padding: 12px 32px; border-radius: 8px; text-decoration: none; font-weight: 600;">์ฒซ ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ํ•˜๊ธฐ</a>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```

## 3) ํ™•์ธ

๋ธŒ๋ผ์šฐ์ €์—์„œ `/recipes`์— ์ ‘์†ํ•˜๋ฉด ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์ด ํ‘œ์‹œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. DB ํŒŒ์ผ(`G5_DATA_PATH/modules/recipes.sqlite`)์ด ์ž๋™ ์ƒ์„ฑ๋˜๊ณ , 4๊ฐœ ํ…Œ์ด๋ธ”(recipes, ingredients, steps, notes)์ด ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: - `/recipes` URL๋กœ ์ ‘์† ๊ฐ€๋Šฅ - modern shell ์ ์šฉ (์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜, ํ‘ธํ„ฐ, ๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€ ํฌํ•จ) - ๋Œ€์‹œ๋ณด๋“œ์— ํ†ต๊ณ„ ์นด๋“œ 3๊ฐœ (์ „์ฒด ๋ ˆ์‹œํ”ผ, ์ฆ๊ฒจ์ฐพ๊ธฐ, ์ตœ๊ทผ 7์ผ) ํ‘œ์‹œ - ๋นˆ ์ƒํƒœ ๋ฉ”์‹œ์ง€ + "์ฒซ ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ํ•˜๊ธฐ" ๋ฒ„ํŠผ - SQLite DB ํŒŒ์ผ ๋ฐ ํ…Œ์ด๋ธ” ์ž๋™ ์ƒ์„ฑ ์™„๋ฃŒ

2
Step 2
**๋ชฉํ‘œ:** ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ ํŽ˜์ด์ง€ ๊ตฌํ˜„ (์ œ๋ชฉ, ์„ค๋ช…, ์กฐ๋ฆฌ์‹œ๊ฐ„, ๋‚œ์ด๋„, ๋ถ„๋Ÿ‰, ํƒœ๊ทธ ์ž…๋ ฅ)

---

## 1) ํด๋” ์ƒ์„ฑ

`app/modules/recipes/add/index.php` ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”. ์ด ํŒŒ์ผ์€ `/recipes/add` URL๋กœ ์ž๋™ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค.

## 2) ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ ํผ ๊ตฌํ˜„

์•„๋ž˜ ์ฝ”๋“œ๋กœ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”:

```php
<?php
include_once('./_common.php');

@mkdir(G5_DATA_PATH.'/modules', 0707, true);
@mkdir(G5_DATA_PATH.'/modules/recipes', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/recipes.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$error = '';
$success = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $title = trim($_POST['title'] ?? '');
    $description = trim($_POST['description'] ?? '');
    $cooking_time = intval($_POST['cooking_time'] ?? 0);
    $difficulty = $_POST['difficulty'] ?? 'easy';
    $servings = intval($_POST['servings'] ?? 1);
    $tags = trim($_POST['tags'] ?? '');
    
    if (empty($title)) {
        $error = '๋ ˆ์‹œํ”ผ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.';
    } else {
        // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
        $image_path = '';
        if (!empty($_FILES['image']['name'])) {
            $upload_dir = G5_DATA_PATH.'/modules/recipes/';
            $ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
            $allowed = ['jpg', 'jpeg', 'png', 'gif'];
            
            if (in_array($ext, $allowed)) {
                $filename = uniqid('recipe_').'.'.$ext;
                if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir.$filename)) {
                    $image_path = $filename;
                }
            }
        }
        
        $stmt = $db->prepare("
            INSERT INTO recipes (title, description, cooking_time, difficulty, servings, image, tags)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        ");
        $stmt->execute([$title, $description, $cooking_time, $difficulty, $servings, $image_path, $tags]);
        
        $recipe_id = $db->lastInsertId();
        
        // ์žฌ๋ฃŒ ์ €์žฅ
        if (!empty($_POST['ingredients'])) {
            foreach ($_POST['ingredients'] as $ing) {
                if (!empty($ing['name'])) {
                    $stmt = $db->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)");
                    $stmt->execute([$recipe_id, $ing['name'], $ing['quantity'] ?? '', $ing['unit'] ?? '']);
                }
            }
        }
        
        // ์กฐ๋ฆฌ ๋‹จ๊ณ„ ์ €์žฅ
        if (!empty($_POST['steps'])) {
            foreach ($_POST['steps'] as $idx => $step_desc) {
                if (!empty($step_desc)) {
                    $stmt = $db->prepare("INSERT INTO steps (recipe_id, step_number, description) VALUES (?, ?, ?)");
                    $stmt->execute([$recipe_id, $idx + 1, $step_desc]);
                }
            }
        }
        
        $success = '๋ ˆ์‹œํ”ผ๊ฐ€ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค!';
        header('Location: /recipes');
        exit;
    }
}

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <div style="max-width: 800px; margin: 0 auto;">
            <h1 style="font-size: 2rem; font-weight: 700; color: var(--m-text); margin-bottom: 24px;">์ƒˆ ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€</h1>
            
            <?php if ($error): ?>
            <div style="background: #fee; color: #c33; padding: 12px 16px; border-radius: 8px; margin-bottom: 20px;">
                <?= htmlspecialchars($error) ?>
            </div>
            <?php endif; ?>
            
            <form method="post" enctype="multipart/form-data" style="background: var(--m-surface); padding: 32px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 8px;">๋ ˆ์‹œํ”ผ ์ œ๋ชฉ *</label>
                    <input type="text" name="title" required style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 8px;">์„ค๋ช…</label>
                    <textarea name="description" rows="3" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);"></textarea>
                </div>
                
                <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-bottom: 20px;">
                    <div>
                        <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 8px;">์กฐ๋ฆฌ์‹œ๊ฐ„(๋ถ„)</label>
                        <input type="number" name="cooking_time" min="1" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);">
                    </div>
                    <div>
                        <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 8px;">๋‚œ์ด๋„</label>
                        <select name="difficulty" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);">
                            <option value="easy">์‰ฌ์›€</option>
                            <option value="medium">๋ณดํ†ต</option>
                            <option value="hard">์–ด๋ ค์›€</option>
                        </select>
                    </div>
                    <div>
                        <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 8px;">๋ถ„๋Ÿ‰(์ธ๋ถ„)</label>
                        <input type="number" name="servings" min="1" value="2" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);">
                    </div>
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 8px;">์™„์„ฑ ์‚ฌ์ง„</label>
                    <input type="file" name="image" accept="image/*" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 8px;">ํƒœ๊ทธ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„)</label>
                    <input type="text" name="tags" placeholder="์˜ˆ: ํ•œ์‹, ๊ฐ„ํŽธ์‹, ์ €๋…" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 12px;">์žฌ๋ฃŒ</label>
                    <div id="ingredients-container"></div>
                    <button type="button" onclick="addIngredient()" style="margin-top: 8px; padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer;">+ ์žฌ๋ฃŒ ์ถ”๊ฐ€</button>
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: 600; color: var(--m-text); margin-bottom: 12px;">์กฐ๋ฆฌ ์ˆœ์„œ</label>
                    <div id="steps-container"></div>
                    <button type="button" onclick="addStep()" style="margin-top: 8px; padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 6px; cursor: pointer;">+ ๋‹จ๊ณ„ ์ถ”๊ฐ€</button>
                </div>
                
                <div style="display: flex; gap: 12px;">
                    <button type="submit" style="flex: 1; padding: 14px; background: #FF6B35; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer;">๋ ˆ์‹œํ”ผ ์ €์žฅ</button>
                    <a href="/recipes" style="flex: 1; padding: 14px; background: #666; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; text-align: center; text-decoration: none; display: block;">์ทจ์†Œ</a>
                </div>
            </form>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>

<script>
let ingredientCount = 0;
let stepCount = 0;

function addIngredient() {
    const container = document.getElementById('ingredients-container');
    const div = document.createElement('div');
    div.style.cssText = 'display: grid; grid-template-columns: 2fr 1fr 1fr auto; gap: 8px; margin-bottom: 8px;';
    div.innerHTML = `
        <input type="text" name="ingredients[${ingredientCount}][name]" placeholder="์žฌ๋ฃŒ๋ช…" style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; background: var(--m-surface); color: var(--m-text);">
        <input type="text" name="ingredients[${ingredientCount}][quantity]" placeholder="์ˆ˜๋Ÿ‰" style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; background: var(--m-surface); color: var(--m-text);">
        <input type="text" name="ingredients[${ingredientCount}][unit]" placeholder="๋‹จ์œ„" style="padding: 10px; border: 1px solid #ddd; border-radius: 6px; background: var(--m-surface); color: var(--m-text);">
        <button type="button" onclick="this.parentElement.remove()" style="padding: 10px 14px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer;">ร—</button>
    `;
    container.appendChild(div);
    ingredientCount++;
}

function addStep() {
    const container = document.getElementById('steps-container');
    const div = document.createElement('div');
    div.style.cssText = 'display: flex; gap: 8px; margin-bottom: 8px;';
    div.innerHTML = `
        <div style="padding: 10px; background: #FF6B35; color: white; border-radius: 6px; font-weight: 600; min-width: 40px; text-align: center;">${stepCount + 1}</div>
        <textarea name="steps[${stepCount}]" placeholder="์กฐ๋ฆฌ ๋ฐฉ๋ฒ•์„ ์ž…๋ ฅํ•˜์„ธ์š”" rows="2" style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; background: var(--m-surface); color: var(--m-text);"></textarea>
        <button type="button" onclick="this.parentElement.remove(); renumberSteps()" style="padding: 10px 14px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer;">ร—</button>
    `;
    container.appendChild(div);
    stepCount++;
}

function renumberSteps() {
    const steps = document.querySelectorAll('#steps-container > div');
    steps.forEach((step, idx) => {
        step.querySelector('div').textContent = idx + 1;
    });
}

// ์ดˆ๊ธฐ ์žฌ๋ฃŒ/๋‹จ๊ณ„ 1๊ฐœ์”ฉ ์ถ”๊ฐ€
addIngredient();
addStep();
</script>

<?php include_once('./tail.sub.php'); ?>
```

## 3) ํ™•์ธ

- `/recipes/add`์— ์ ‘์†ํ•˜๋ฉด ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ ํผ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ์žฌ๋ฃŒ/๋‹จ๊ณ„ ์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋™์ ์œผ๋กœ ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค
- ํผ ์ œ์ถœ ์‹œ DB์— ์ €์žฅ๋˜๊ณ  `/recipes`๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฉ๋‹ˆ๋‹ค

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: - ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ ํผ ํŽ˜์ด์ง€ ๋™์ž‘ - ์ œ๋ชฉ, ์„ค๋ช…, ์กฐ๋ฆฌ์‹œ๊ฐ„, ๋‚œ์ด๋„, ๋ถ„๋Ÿ‰, ํƒœ๊ทธ ์ž…๋ ฅ ๊ฐ€๋Šฅ - ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ (ํŒŒ์ผ ์ €์žฅ) - ์žฌ๋ฃŒ ๋™์  ์ถ”๊ฐ€/์‚ญ์ œ (JavaScript) - ์กฐ๋ฆฌ ๋‹จ๊ณ„ ๋™์  ์ถ”๊ฐ€/์‚ญ์ œ (๋ฒˆํ˜ธ ์ž๋™ ๋ถ€์—ฌ) - ํผ ์ œ์ถœ ์‹œ DB์— ๋ ˆ์‹œํ”ผ, ์žฌ๋ฃŒ, ๋‹จ๊ณ„ ์ €์žฅ - ์ €์žฅ ํ›„ ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™

3
Step 3
**๋ชฉํ‘œ:** ๋ฉ”์ธ ํŽ˜์ด์ง€์— ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก ํ‘œ์‹œ (์นด๋“œ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ + ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ)

---

## 1) `app/modules/recipes/index.php` ์ˆ˜์ •

๊ธฐ์กด ํŒŒ์ผ์„ ์•„๋ž˜ ์ฝ”๋“œ๋กœ ๊ต์ฒดํ•ด์ฃผ์„ธ์š”:

```php
<?php
include_once('./_common.php');

@mkdir(G5_DATA_PATH.'/modules', 0707, true);
@mkdir(G5_DATA_PATH.'/modules/recipes', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/recipes.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ํ†ต๊ณ„ ๊ณ„์‚ฐ
$total = $db->query("SELECT COUNT(*) FROM recipes")->fetchColumn();
$favorites = $db->query("SELECT COUNT(*) FROM recipes WHERE favorite = 1")->fetchColumn();
$recent = $db->query("SELECT COUNT(*) FROM recipes WHERE created_at >= datetime('now', '-7 days')")->fetchColumn();

// ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
$recipes = $db->query("
    SELECT * FROM recipes ORDER BY created_at DESC
")->fetchAll(PDO::FETCH_ASSOC);

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <div class="recipe-header" style="text-align: center; margin-bottom: 40px;">
            <h1 style="font-size: 2.5rem; font-weight: 700; color: var(--m-text); margin-bottom: 8px;">๐Ÿณ ๋‚˜๋งŒ์˜ ๋ ˆ์‹œํ”ผ ๋ถ</h1>
            <p style="font-size: 1.1rem; color: var(--m-text-muted);">๋ง›์žˆ๋Š” ์š”๋ฆฌ๋ฅผ ๊ธฐ๋กํ•˜๊ณ , ์žฌ๋ฃŒ๋กœ ๊ฒ€์ƒ‰ํ•˜์„ธ์š”</p>
        </div>

        <div class="dashboard-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 40px;">
            <div class="stat-card" style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <div style="font-size: 2rem; margin-bottom: 8px;">๐Ÿ“š</div>
                <div style="font-size: 2rem; font-weight: 700; color: var(--m-text);"><?= $total ?></div>
                <div style="font-size: 0.9rem; color: var(--m-text-muted);">์ „์ฒด ๋ ˆ์‹œํ”ผ</div>
            </div>
            <div class="stat-card" style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <div style="font-size: 2rem; margin-bottom: 8px;">โญ</div>
                <div style="font-size: 2rem; font-weight: 700; color: var(--m-text);"><?= $favorites ?></div>
                <div style="font-size: 0.9rem; color: var(--m-text-muted);">์ฆ๊ฒจ์ฐพ๊ธฐ</div>
            </div>
            <div class="stat-card" style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <div style="font-size: 2rem; margin-bottom: 8px;">๐Ÿ”ฅ</div>
                <div style="font-size: 2rem; font-weight: 700; color: var(--m-text);"><?= $recent ?></div>
                <div style="font-size: 0.9rem; color: var(--m-text-muted);">์ตœ๊ทผ 7์ผ</div>
            </div>
        </div>

        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
            <h2 style="font-size: 1.5rem; font-weight: 700; color: var(--m-text);">๋ ˆ์‹œํ”ผ ๋ชฉ๋ก</h2>
            <a href="/recipes/add" style="padding: 10px 20px; background: #FF6B35; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">+ ์ƒˆ ๋ ˆ์‹œํ”ผ</a>
        </div>

        <?php if (empty($recipes)): ?>
        <div style="text-align: center; padding: 60px 20px; background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
            <div style="font-size: 4rem; margin-bottom: 16px;">๐Ÿฝ๏ธ</div>
            <p style="font-size: 1.2rem; color: var(--m-text-muted); margin-bottom: 24px;">์•„์ง ๋“ฑ๋ก๋œ ๋ ˆ์‹œํ”ผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</p>
            <a href="/recipes/add" style="display: inline-block; background: #FF6B35; color: white; padding: 12px 32px; border-radius: 8px; text-decoration: none; font-weight: 600;">์ฒซ ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ํ•˜๊ธฐ</a>
        </div>
        <?php else: ?>
        <div class="recipe-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px;">
            <?php foreach ($recipes as $recipe): ?>
            <a href="/recipes/view?id=<?= $recipe['id'] ?>" style="text-decoration: none; color: inherit;">
                <div class="recipe-card" style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); overflow: hidden; transition: transform 0.2s;" onmouseover="this.style.transform='translateY(-4px)'" onmouseout="this.style.transform='translateY(0)'">
                    <?php if ($recipe['image']): ?>
                    <img src="<?= G5_DATA_URL ?>/modules/recipes/<?= htmlspecialchars($recipe['image']) ?>" alt="<?= htmlspecialchars($recipe['title']) ?>" style="width: 100%; height: 200px; object-fit: cover;">
                    <?php else: ?>
                    <div style="width: 100%; height: 200px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; font-size: 4rem;">๐Ÿณ</div>
                    <?php endif; ?>
                    
                    <div style="padding: 20px;">
                        <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
                            <h3 style="font-size: 1.25rem; font-weight: 700; color: var(--m-text); margin: 0;"><?= htmlspecialchars($recipe['title']) ?></h3>
                            <?php if ($recipe['favorite']): ?>
                            <span style="font-size: 1.2rem;">โญ</span>
                            <?php endif; ?>
                        </div>
                        
                        <?php if ($recipe['description']): ?>
                        <p style="color: var(--m-text-muted); font-size: 0.9rem; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
                            <?= htmlspecialchars($recipe['description']) ?>
                        </p>
                        <?php endif; ?>
                        
                        <div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px;">
                            <?php if ($recipe['cooking_time']): ?>
                            <span style="font-size: 0.85rem; color: var(--m-text-muted);">โฑ๏ธ <?= $recipe['cooking_time'] ?>๋ถ„</span>
                            <?php endif; ?>
                            <?php if ($recipe['difficulty']): ?>
                            <span style="font-size: 0.85rem; color: var(--m-text-muted);">
                                <?php 
                                $diff_emoji = ['easy' => '๐Ÿ˜Š', 'medium' => '๐Ÿค”', 'hard' => '๐Ÿ˜ฐ'];
                                echo $diff_emoji[$recipe['difficulty']] ?? '';
                                ?>
                                <?= ['easy' => '์‰ฌ์›€', 'medium' => '๋ณดํ†ต', 'hard' => '์–ด๋ ค์›€'][$recipe['difficulty']] ?? '' ?>
                            </span>
                            <?php endif; ?>
                            <?php if ($recipe['servings']): ?>
                            <span style="font-size: 0.85rem; color: var(--m-text-muted);">๐Ÿ‘ฅ <?= $recipe['servings'] ?>์ธ๋ถ„</span>
                            <?php endif; ?>
                        </div>
                        
                        <?php if ($recipe['tags']): ?>
                        <div style="display: flex; gap: 6px; flex-wrap: wrap;">
                            <?php foreach (explode(',', $recipe['tags']) as $tag): ?>
                            <span style="font-size: 0.8rem; padding: 4px 10px; background: #FF6B35; color: white; border-radius: 12px;"><?= htmlspecialchars(trim($tag)) ?></span>
                            <?php endforeach; ?>
                        </div>
                        <?php endif; ?>
                    </div>
                </div>
            </a>
            <?php endforeach; ?>
        </div>
        <?php endif; ?>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```

## 2) ํ™•์ธ

- `/recipes`์— ์ ‘์†ํ•˜๋ฉด ๋“ฑ๋ก๋œ ๋ ˆ์‹œํ”ผ๊ฐ€ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ํ†ต๊ณ„ ์นด๋“œ์— ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ์นด๋“œ์— ์ด๋ฏธ์ง€(์žˆ์œผ๋ฉด), ์ œ๋ชฉ, ์„ค๋ช…, ์กฐ๋ฆฌ์‹œ๊ฐ„, ๋‚œ์ด๋„, ๋ถ„๋Ÿ‰, ํƒœ๊ทธ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ์นด๋“œ ํ˜ธ๋ฒ„ ์‹œ ์‚ด์ง ์˜ฌ๋ผ๊ฐ€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: - ๋ ˆ์‹œํ”ผ ์นด๋“œ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ ํ‘œ์‹œ - ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ์ธ๋„ค์ผ, ์—†์œผ๋ฉด ๊ทธ๋ผ๋””์–ธํŠธ ๋ฐฐ๊ฒฝ + ์ด๋ชจ์ง€ - ์ œ๋ชฉ, ์„ค๋ช…(2์ค„ ๋ง์ค„์ž„), ์กฐ๋ฆฌ์‹œ๊ฐ„, ๋‚œ์ด๋„, ๋ถ„๋Ÿ‰, ํƒœ๊ทธ ํ‘œ์‹œ - ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ ˆ์‹œํ”ผ๋Š” ๋ณ„ ์•„์ด์ฝ˜ ํ‘œ์‹œ - ํ†ต๊ณ„ ์นด๋“œ์— ์‹ค์ œ ๋ฐ์ดํ„ฐ ๋ฐ˜์˜ - ์นด๋“œ ํด๋ฆญ ์‹œ `/recipes/view?id=X`๋กœ ์ด๋™ (์•„์ง ๊ตฌํ˜„ ์•ˆ๋จ)

4
Step 4
**๋ชฉํ‘œ:** ๋ ˆ์‹œํ”ผ ์ƒ์„ธ ํŽ˜์ด์ง€ ๊ตฌํ˜„ (์žฌ๋ฃŒ, ์กฐ๋ฆฌ ๋‹จ๊ณ„, ๋ฉ”๋ชจ, ์ฆ๊ฒจ์ฐพ๊ธฐ)

---

## 1) ํด๋” ์ƒ์„ฑ

`app/modules/recipes/view/index.php` ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”. ์ด ํŒŒ์ผ์€ `/recipes/view` URL๋กœ ์ž๋™ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค.

## 2) ๋ ˆ์‹œํ”ผ ์ƒ์„ธ ํŽ˜์ด์ง€ ๊ตฌํ˜„

์•„๋ž˜ ์ฝ”๋“œ๋กœ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”:

```php
<?php
include_once('./_common.php');

@mkdir(G5_DATA_PATH.'/modules', 0707, true);
@mkdir(G5_DATA_PATH.'/modules/recipes', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/recipes.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$id = intval($_GET['id'] ?? 0);
if (!$id) {
    header('Location: /recipes');
    exit;
}

// AJAX ์š”์ฒญ ์ฒ˜๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json');
    
    $action = $_POST['action'] ?? '';
    
    if ($action === 'toggle_favorite') {
        $stmt = $db->prepare("UPDATE recipes SET favorite = 1 - favorite WHERE id = ?");
        $stmt->execute([$id]);
        $new_state = $db->query("SELECT favorite FROM recipes WHERE id = $id")->fetchColumn();
        echo json_encode(['success' => true, 'favorite' => $new_state]);
        exit;
    }
    
    if ($action === 'add_note') {
        $content = trim($_POST['content'] ?? '');
        if ($content) {
            $stmt = $db->prepare("INSERT INTO notes (recipe_id, content) VALUES (?, ?)");
            $stmt->execute([$id, $content]);
            echo json_encode(['success' => true]);
        } else {
            echo json_encode(['success' => false, 'error' => '๋ฉ”๋ชจ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”']);
        }
        exit;
    }
    
    if ($action === 'delete_note') {
        $note_id = intval($_POST['note_id'] ?? 0);
        $stmt = $db->prepare("DELETE FROM notes WHERE id = ? AND recipe_id = ?");
        $stmt->execute([$note_id, $id]);
        echo json_encode(['success' => true]);
        exit;
    }
}

// ๋ ˆ์‹œํ”ผ ์ •๋ณด
$stmt = $db->prepare("SELECT * FROM recipes WHERE id = ?");
$stmt->execute([$id]);
$recipe = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$recipe) {
    header('Location: /recipes');
    exit;
}

// ์žฌ๋ฃŒ
$stmt = $db->prepare("SELECT * FROM ingredients WHERE recipe_id = ? ORDER BY id");
$stmt->execute([$id]);
$ingredients = $stmt->fetchAll(PDO::FETCH_ASSOC);

// ์กฐ๋ฆฌ ๋‹จ๊ณ„
$stmt = $db->prepare("SELECT * FROM steps WHERE recipe_id = ? ORDER BY step_number");
$stmt->execute([$id]);
$steps = $stmt->fetchAll(PDO::FETCH_ASSOC);

// ๋ฉ”๋ชจ
$stmt = $db->prepare("SELECT * FROM notes WHERE recipe_id = ? ORDER BY created_at DESC");
$stmt->execute([$id]);
$notes = $stmt->fetchAll(PDO::FETCH_ASSOC);

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <div style="max-width: 900px; margin: 0 auto;">
            <div style="margin-bottom: 24px;">
                <a href="/recipes" style="color: var(--m-text-muted); text-decoration: none;">โ† ๋ชฉ๋ก์œผ๋กœ</a>
            </div>
            
            <div style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); overflow: hidden; margin-bottom: 24px;">
                <?php if ($recipe['image']): ?>
                <img src="<?= G5_DATA_URL ?>/modules/recipes/<?= htmlspecialchars($recipe['image']) ?>" alt="<?= htmlspecialchars($recipe['title']) ?>" style="width: 100%; height: 400px; object-fit: cover;">
                <?php endif; ?>
                
                <div style="padding: 32px;">
                    <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px;">
                        <h1 style="font-size: 2rem; font-weight: 700; color: var(--m-text); margin: 0;"><?= htmlspecialchars($recipe['title']) ?></h1>
                        <button onclick="toggleFavorite()" id="favorite-btn" style="background: none; border: none; font-size: 2rem; cursor: pointer;">
                            <?= $recipe['favorite'] ? 'โญ' : 'โ˜†' ?>
                        </button>
                    </div>
                    
                    <?php if ($recipe['description']): ?>
                    <p style="color: var(--m-text-muted); font-size: 1.1rem; margin-bottom: 20px;"><?= htmlspecialchars($recipe['description']) ?></p>
                    <?php endif; ?>
                    
                    <div style="display: flex; gap: 20px; flex-wrap: wrap;">
                        <?php if ($recipe['cooking_time']): ?>
                        <div style="display: flex; align-items: center; gap: 8px;">
                            <span style="font-size: 1.5rem;">โฑ๏ธ</span>
                            <div>
                                <div style="font-size: 0.85rem; color: var(--m-text-muted);">์กฐ๋ฆฌ์‹œ๊ฐ„</div>
                                <div style="font-weight: 600; color: var(--m-text);"><?= $recipe['cooking_time'] ?>๋ถ„</div>
                            </div>
                        </div>
                        <?php endif; ?>
                        
                        <?php if ($recipe['difficulty']): ?>
                        <div style="display: flex; align-items: center; gap: 8px;">
                            <span style="font-size: 1.5rem;"><?= ['easy' => '๐Ÿ˜Š', 'medium' => '๐Ÿค”', 'hard' => '๐Ÿ˜ฐ'][$recipe['difficulty']] ?? '' ?></span>
                            <div>
                                <div style="font-size: 0.85rem; color: var(--m-text-muted);">๋‚œ์ด๋„</div>
                                <div style="font-weight: 600; color: var(--m-text);"><?= ['easy' => '์‰ฌ์›€', 'medium' => '๋ณดํ†ต', 'hard' => '์–ด๋ ค์›€'][$recipe['difficulty']] ?? '' ?></div>
                            </div>
                        </div>
                        <?php endif; ?>
                        
                        <?php if ($recipe['servings']): ?>
                        <div style="display: flex; align-items: center; gap: 8px;">
                            <span style="font-size: 1.5rem;">๐Ÿ‘ฅ</span>
                            <div>
                                <div style="font-size: 0.85rem; color: var(--m-text-muted);">๋ถ„๋Ÿ‰</div>
                                <div style="font-weight: 600; color: var(--m-text);"><?= $recipe['servings'] ?>์ธ๋ถ„</div>
                            </div>
                        </div>
                        <?php endif; ?>
                    </div>
                    
                    <?php if ($recipe['tags']): ?>
                    <div style="margin-top: 20px; display: flex; gap: 8px; flex-wrap: wrap;">
                        <?php foreach (explode(',', $recipe['tags']) as $tag): ?>
                        <span style="font-size: 0.9rem; padding: 6px 14px; background: #FF6B35; color: white; border-radius: 16px;"><?= htmlspecialchars(trim($tag)) ?></span>
                        <?php endforeach; ?>
                    </div>
                    <?php endif; ?>
                </div>
            </div>
            
            <?php if (!empty($ingredients)): ?>
            <div style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); padding: 32px; margin-bottom: 24px;">
                <h2 style="font-size: 1.5rem; font-weight: 700; color: var(--m-text); margin-bottom: 20px;">๐Ÿฅ˜ ์žฌ๋ฃŒ</h2>
                <div style="display: grid; gap: 12px;">
                    <?php foreach ($ingredients as $ing): ?>
                    <label style="display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--m-surface); border: 1px solid #ddd; border-radius: 8px; cursor: pointer;">
                        <input type="checkbox" style="width: 20px; height: 20px;">
                        <span style="flex: 1; color: var(--m-text); font-weight: 500;"><?= htmlspecialchars($ing['name']) ?></span>
                        <?php if ($ing['quantity'] || $ing['unit']): ?>
                        <span style="color: var(--m-text-muted);"><?= htmlspecialchars($ing['quantity']) ?> <?= htmlspecialchars($ing['unit']) ?></span>
                        <?php endif; ?>
                    </label>
                    <?php endforeach; ?>
                </div>
            </div>
            <?php endif; ?>
            
            <?php if (!empty($steps)): ?>
            <div style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); padding: 32px; margin-bottom: 24px;">
                <h2 style="font-size: 1.5rem; font-weight: 700; color: var(--m-text); margin-bottom: 20px;">๐Ÿ‘จโ€๐Ÿณ ์กฐ๋ฆฌ ์ˆœ์„œ</h2>
                <div style="display: grid; gap: 20px;">
                    <?php foreach ($steps as $step): ?>
                    <div style="display: flex; gap: 16px;">
                        <div style="min-width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; background: #FF6B35; color: white; border-radius: 50%; font-weight: 700; font-size: 1.2rem;"><?= $step['step_number'] ?></div>
                        <p style="flex: 1; color: var(--m-text); line-height: 1.6; margin: 0; padding-top: 12px;"><?= nl2br(htmlspecialchars($step['description'])) ?></p>
                    </div>
                    <?php endforeach; ?>
                </div>
            </div>
            <?php endif; ?>
            
            <div style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); padding: 32px;">
                <h2 style="font-size: 1.5rem; font-weight: 700; color: var(--m-text); margin-bottom: 20px;">๐Ÿ“ ๋‚˜๋งŒ์˜ ๋ฉ”๋ชจ</h2>
                
                <form onsubmit="addNote(event)" style="margin-bottom: 20px;">
                    <textarea id="note-input" placeholder="์กฐ๋ฆฌ ํŒ์ด๋‚˜ ๊ฐœ์„  ์‚ฌํ•ญ์„ ๋ฉ”๋ชจํ•˜์„ธ์š”" rows="3" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: var(--m-surface); color: var(--m-text); margin-bottom: 12px;"></textarea>
                    <button type="submit" style="padding: 10px 24px; background: #4CAF50; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">๋ฉ”๋ชจ ์ถ”๊ฐ€</button>
                </form>
                
                <div id="notes-container">
                    <?php foreach ($notes as $note): ?>
                    <div class="note-item" data-id="<?= $note['id'] ?>" style="padding: 16px; background: var(--m-surface); border: 1px solid #ddd; border-radius: 8px; margin-bottom: 12px;">
                        <div style="display: flex; justify-content: space-between; align-items: start;">
                            <p style="flex: 1; color: var(--m-text); margin: 0;"><?= nl2br(htmlspecialchars($note['content'])) ?></p>
                            <button onclick="deleteNote(<?= $note['id'] ?>)" style="background: none; border: none; color: #f44336; cursor: pointer; font-size: 1.2rem;">ร—</button>
                        </div>
                        <div style="font-size: 0.85rem; color: var(--m-text-muted); margin-top: 8px;"><?= date('Y-m-d H:i', strtotime($note['created_at'])) ?></div>
                    </div>
                    <?php endforeach; ?>
                </div>
            </div>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>

<script>
function toggleFavorite() {
    fetch('/recipes/view?id=<?= $id ?>', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: 'action=toggle_favorite'
    })
    .then(r => r.json())
    .then(data => {
        document.getElementById('favorite-btn').textContent = data.favorite ? 'โญ' : 'โ˜†';
    });
}

function addNote(e) {
    e.preventDefault();
    const input = document.getElementById('note-input');
    const content = input.value.trim();
    
    if (!content) return;
    
    fetch('/recipes/view?id=<?= $id ?>', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: 'action=add_note&content=' + encodeURIComponent(content)
    })
    .then(r => r.json())
    .then(data => {
        if (data.success) {
            location.reload();
        }
    });
}

function deleteNote(id) {
    if (!confirm('๋ฉ”๋ชจ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) return;
    
    fetch('/recipes/view?id=<?= $id ?>', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: 'action=delete_note&note_id=' + id
    })
    .then(r => r.json())
    .then(data => {
        if (data.success) {
            document.querySelector(`.note-item[data-id="${id}"]`).remove();
        }
    });
}
</script>

<?php include_once('./tail.sub.php'); ?>
```

## 3) ํ™•์ธ

- ๋ ˆ์‹œํ”ผ ์นด๋“œ ํด๋ฆญ ์‹œ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค
- ์ด๋ฏธ์ง€, ์ œ๋ชฉ, ์„ค๋ช…, ์กฐ๋ฆฌ์ •๋ณด๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ์žฌ๋ฃŒ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  ์ฒดํฌ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค
- ์กฐ๋ฆฌ ๋‹จ๊ณ„๊ฐ€ ๋ฒˆํ˜ธ์™€ ํ•จ๊ป˜ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ๋ฉ”๋ชจ๋ฅผ ์ถ”๊ฐ€/์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
- ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋ณ„ ์•„์ด์ฝ˜์ด ํ† ๊ธ€๋ฉ๋‹ˆ๋‹ค

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: - ๋ ˆ์‹œํ”ผ ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ (์ด๋ฏธ์ง€, ์ œ๋ชฉ, ์„ค๋ช…, ์กฐ๋ฆฌ์‹œ๊ฐ„, ๋‚œ์ด๋„, ๋ถ„๋Ÿ‰, ํƒœ๊ทธ) - ์žฌ๋ฃŒ ์ฒดํฌ๋ฆฌ์ŠคํŠธ (์ฒดํฌ๋ฐ•์Šค๋กœ ์ค€๋น„ํ•œ ์žฌ๋ฃŒ ํ‘œ์‹œ ๊ฐ€๋Šฅ) - ์กฐ๋ฆฌ ๋‹จ๊ณ„ ๋ฒˆํ˜ธ ๋ฑƒ์ง€์™€ ํ•จ๊ป˜ ํ‘œ์‹œ - ์ฆ๊ฒจ์ฐพ๊ธฐ ํ† ๊ธ€ (AJAX๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ) - ๋ฉ”๋ชจ ์ถ”๊ฐ€/์‚ญ์ œ (AJAX) - ๋‹คํฌ๋ชจ๋“œ ๋Œ€์‘ ๋””์ž์ธ

5
Step 5
**๋ชฉํ‘œ:** ์žฌ๋ฃŒ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์žฌ๋ฃŒ ์ž…๋ ฅ โ†’ ํ•ด๋‹น ์žฌ๋ฃŒ๊ฐ€ ๋ชจ๋‘ ํฌํ•จ๋œ ๋ ˆ์‹œํ”ผ ํ•„ํ„ฐ๋ง)

---

## 1) `app/modules/recipes/index.php` ์ˆ˜์ •

๊ธฐ์กด ํŒŒ์ผ์˜ ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ ๋ถ€๋ถ„์„ ์•„๋ž˜ ์ฝ”๋“œ๋กœ ๊ต์ฒดํ•ด์ฃผ์„ธ์š” (DB ์—ฐ๊ฒฐ ์•„๋ž˜, `require_once(G5_THEME_PATH...` ์œ„์—):

```php
// ๊ฒ€์ƒ‰ ์ฒ˜๋ฆฌ
$search_ingredients = [];
if (!empty($_GET['search'])) {
    $search_ingredients = array_map('trim', explode(',', $_GET['search']));
    $search_ingredients = array_filter($search_ingredients);
}

// ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
if (!empty($search_ingredients)) {
    // ์žฌ๋ฃŒ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰
    $placeholders = implode(',', array_fill(0, count($search_ingredients), '?'));
    $sql = "
        SELECT DISTINCT r.* 
        FROM recipes r
        INNER JOIN ingredients i ON r.id = i.recipe_id
        WHERE LOWER(i.name) IN ($placeholders)
        GROUP BY r.id
        HAVING COUNT(DISTINCT LOWER(i.name)) = ?
        ORDER BY r.created_at DESC
    ";
    $stmt = $db->prepare($sql);
    $params = array_map('strtolower', $search_ingredients);
    $params[] = count($search_ingredients);
    $stmt->execute($params);
    $recipes = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
    $recipes = $db->query("
        SELECT * FROM recipes ORDER BY created_at DESC
    ")->fetchAll(PDO::FETCH_ASSOC);
}
```

## 2) ๊ฒ€์ƒ‰ UI ์ถ”๊ฐ€

`<div class="dashboard-grid">` ์•„๋ž˜, `<div style="display: flex; justify-content: space-between...` ์œ„์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:

```php
<div style="margin-bottom: 32px;">
    <form method="get" style="max-width: 600px; margin: 0 auto;">
        <div style="position: relative;">
            <input type="text" name="search" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="์žฌ๋ฃŒ๋กœ ๊ฒ€์ƒ‰ (์˜ˆ: ๊ณ„๋ž€, ์–‘ํŒŒ, ๋Œ€ํŒŒ)" style="width: 100%; padding: 16px 120px 16px 20px; border: 2px solid #FF6B35; border-radius: 50px; font-size: 1rem; background: var(--m-surface); color: var(--m-text);">
            <button type="submit" style="position: absolute; right: 8px; top: 50%; transform: translateY(-50%); padding: 10px 24px; background: #FF6B35; color: white; border: none; border-radius: 50px; font-weight: 600; cursor: pointer;">๊ฒ€์ƒ‰</button>
        </div>
        <?php if (!empty($search_ingredients)): ?>
        <div style="margin-top: 12px; text-align: center;">
            <span style="color: var(--m-text-muted);">๊ฒ€์ƒ‰ ์ค‘: </span>
            <?php foreach ($search_ingredients as $ing): ?>
            <span style="display: inline-block; padding: 4px 12px; background: #FF6B35; color: white; border-radius: 12px; margin: 0 4px; font-size: 0.9rem;"><?= htmlspecialchars($ing) ?></span>
            <?php endforeach; ?>
            <a href="/recipes" style="margin-left: 12px; color: var(--m-text-muted); text-decoration: none;">ร— ๊ฒ€์ƒ‰ ์ดˆ๊ธฐํ™”</a>
        </div>
        <?php endif; ?>
    </form>
</div>
```

## 3) ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์„ ๋•Œ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€

`<?php if (empty($recipes)): ?>` ๋ธ”๋ก์„ ์•„๋ž˜๋กœ ๊ต์ฒดํ•ด์ฃผ์„ธ์š”:

```php
<?php if (empty($recipes)): ?>
<div style="text-align: center; padding: 60px 20px; background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
    <div style="font-size: 4rem; margin-bottom: 16px;">๐Ÿ”</div>
    <?php if (!empty($search_ingredients)): ?>
    <p style="font-size: 1.2rem; color: var(--m-text-muted); margin-bottom: 12px;">"<?= implode(', ', array_map('htmlspecialchars', $search_ingredients)) ?>"์„(๋ฅผ) ๋ชจ๋‘ ํฌํ•จํ•˜๋Š” ๋ ˆ์‹œํ”ผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</p>
    <a href="/recipes" style="display: inline-block; background: #666; color: white; padding: 12px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; margin-top: 12px;">์ „์ฒด ๋ ˆ์‹œํ”ผ ๋ณด๊ธฐ</a>
    <?php else: ?>
    <p style="font-size: 1.2rem; color: var(--m-text-muted); margin-bottom: 24px;">์•„์ง ๋“ฑ๋ก๋œ ๋ ˆ์‹œํ”ผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</p>
    <a href="/recipes/add" style="display: inline-block; background: #FF6B35; color: white; padding: 12px 32px; border-radius: 8px; text-decoration: none; font-weight: 600;">์ฒซ ๋ ˆ์‹œํ”ผ ์ถ”๊ฐ€ํ•˜๊ธฐ</a>
    <?php endif; ?>
</div>
```

## 4) ํ™•์ธ

- ๊ฒ€์ƒ‰์ฐฝ์— "๊ณ„๋ž€, ์–‘ํŒŒ"์ฒ˜๋Ÿผ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์žฌ๋ฃŒ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๊ฒ€์ƒ‰ํ•˜๋ฉด ๋‘ ์žฌ๋ฃŒ๊ฐ€ ๋ชจ๋‘ ํฌํ•จ๋œ ๋ ˆ์‹œํ”ผ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ๊ฒ€์ƒ‰ ์ค‘์ธ ์žฌ๋ฃŒ๊ฐ€ ํƒœ๊ทธ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- "๊ฒ€์ƒ‰ ์ดˆ๊ธฐํ™”" ํด๋ฆญ ์‹œ ์ „์ฒด ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: - ์žฌ๋ฃŒ ๊ฒ€์ƒ‰์ฐฝ ํ‘œ์‹œ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„) - ๊ฒ€์ƒ‰ ์‹œ ์ž…๋ ฅํ•œ ์žฌ๋ฃŒ๊ฐ€ ๋ชจ๋‘ ํฌํ•จ๋œ ๋ ˆ์‹œํ”ผ๋งŒ ํ•„ํ„ฐ๋ง - ๊ฒ€์ƒ‰ ์ค‘์ธ ์žฌ๋ฃŒ ํƒœ๊ทธ ํ‘œ์‹œ - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์„ ๋•Œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ - ๊ฒ€์ƒ‰ ์ดˆ๊ธฐํ™” ๋งํฌ

6
Step 6
**๋ชฉํ‘œ:** ํƒœ๊ทธ ํ•„ํ„ฐ๋ง & ๋‚œ์ด๋„/์กฐ๋ฆฌ์‹œ๊ฐ„ ํ•„ํ„ฐ ์ถ”๊ฐ€ (URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜)

---

## 1) `app/modules/recipes/index.php` ์ˆ˜์ •

๊ธฐ์กด ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ ์ฝ”๋“œ๋ฅผ ์•„๋ž˜๋กœ ๊ต์ฒดํ•ด์ฃผ์„ธ์š”:

```php
// ํ•„ํ„ฐ ์ฒ˜๋ฆฌ
$search_ingredients = [];
if (!empty($_GET['search'])) {
    $search_ingredients = array_map('trim', explode(',', $_GET['search']));
    $search_ingredients = array_filter($search_ingredients);
}

$filter_tag = $_GET['tag'] ?? '';
$filter_difficulty = $_GET['difficulty'] ?? '';
$filter_time = $_GET['time'] ?? '';

// ๋ ˆ์‹œํ”ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
$where_conditions = [];
$params = [];

if (!empty($search_ingredients)) {
    // ์žฌ๋ฃŒ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ (๋ชจ๋“  ์žฌ๋ฃŒ ํฌํ•จ)
    $placeholders = implode(',', array_fill(0, count($search_ingredients), '?'));
    $where_conditions[] = "
        id IN (
            SELECT recipe_id FROM ingredients 
            WHERE LOWER(name) IN ($placeholders)
            GROUP BY recipe_id
            HAVING COUNT(DISTINCT LOWER(name)) = ?
        )
    ";
    $params = array_merge($params, array_map('strtolower', $search_ingredients));
    $params[] = count($search_ingredients);
}

if ($filter_tag) {
    $where_conditions[] = "tags LIKE ?";
    $params[] = '%'.$filter_tag.'%';
}

if ($filter_difficulty) {
    $where_conditions[] = "difficulty = ?";
    $params[] = $filter_difficulty;
}

if ($filter_time) {
    if ($filter_time === '30') {
        $where_conditions[] = "cooking_time <= 30";
    } elseif ($filter_time === '60') {
        $where_conditions[] = "cooking_time > 30 AND cooking_time <= 60";
    } else {
        $where_conditions[] = "cooking_time > 60";
    }
}

$sql = "SELECT * FROM recipes";
if (!empty($where_conditions)) {
    $sql .= " WHERE " . implode(' AND ', $where_conditions);
}
$sql .= " ORDER BY created_at DESC";

$stmt = $db->prepare($sql);
$stmt->execute($params);
$recipes = $stmt->fetchAll(PDO::FETCH_ASSOC);

// ์ „์ฒด ํƒœ๊ทธ ๋ชฉ๋ก (ํ•„ํ„ฐ UI์šฉ)
$all_tags = [];
$tag_rows = $db->query("SELECT DISTINCT tags FROM recipes WHERE tags IS NOT NULL AND tags != ''")->fetchAll(PDO::FETCH_COLUMN);
foreach ($tag_rows as $tags_str) {
    foreach (explode(',', $tags_str) as $tag) {
        $tag = trim($tag);
        if ($tag && !in_array($tag, $all_tags)) {
            $all_tags[] = $tag;
        }
    }
}
sort($all_tags);
```

## 2) ํ•„ํ„ฐ UI ์ถ”๊ฐ€

๊ฒ€์ƒ‰์ฐฝ ์•„๋ž˜, `<div style="display: flex; justify-content: space-between...` ์œ„์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:

```php
<?php if (!empty($all_tags) || !empty($recipes)): ?>
<div style="margin-bottom: 24px; padding: 20px; background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
    <div style="display: grid; gap: 20px;">
        <?php if (!empty($all_tags)): ?>
        <div>
            <div style="font-weight: 600; color: var(--m-text); margin-bottom: 8px;">๐Ÿท๏ธ ํƒœ๊ทธ</div>
            <div style="display: flex; gap: 8px; flex-wrap: wrap;">
                <a href="?<?= http_build_query(array_merge($_GET, ['tag' => ''])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= empty($filter_tag) ? 'background: #FF6B35; color: white;' : 'background: #eee; color: #666;' ?>">์ „์ฒด</a>
                <?php foreach ($all_tags as $tag): ?>
                <a href="?<?= http_build_query(array_merge($_GET, ['tag' => $tag])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= $filter_tag === $tag ? 'background: #FF6B35; color: white;' : 'background: #eee; color: #666;' ?>"><?= htmlspecialchars($tag) ?></a>
                <?php endforeach; ?>
            </div>
        </div>
        <?php endif; ?>
        
        <div>
            <div style="font-weight: 600; color: var(--m-text); margin-bottom: 8px;">๐Ÿ˜Š ๋‚œ์ด๋„</div>
            <div style="display: flex; gap: 8px; flex-wrap: wrap;">
                <a href="?<?= http_build_query(array_merge($_GET, ['difficulty' => ''])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= empty($filter_difficulty) ? 'background: #4CAF50; color: white;' : 'background: #eee; color: #666;' ?>">์ „์ฒด</a>
                <a href="?<?= http_build_query(array_merge($_GET, ['difficulty' => 'easy'])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= $filter_difficulty === 'easy' ? 'background: #4CAF50; color: white;' : 'background: #eee; color: #666;' ?>">๐Ÿ˜Š ์‰ฌ์›€</a>
                <a href="?<?= http_build_query(array_merge($_GET, ['difficulty' => 'medium'])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= $filter_difficulty === 'medium' ? 'background: #4CAF50; color: white;' : 'background: #eee; color: #666;' ?>">๐Ÿค” ๋ณดํ†ต</a>
                <a href="?<?= http_build_query(array_merge($_GET, ['difficulty' => 'hard'])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= $filter_difficulty === 'hard' ? 'background: #4CAF50; color: white;' : 'background: #eee; color: #666;' ?>">๐Ÿ˜ฐ ์–ด๋ ค์›€</a>
            </div>
        </div>
        
        <div>
            <div style="font-weight: 600; color: var(--m-text); margin-bottom: 8px;">โฑ๏ธ ์กฐ๋ฆฌ์‹œ๊ฐ„</div>
            <div style="display: flex; gap: 8px; flex-wrap: wrap;">
                <a href="?<?= http_build_query(array_merge($_GET, ['time' => ''])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= empty($filter_time) ? 'background: #2196F3; color: white;' : 'background: #eee; color: #666;' ?>">์ „์ฒด</a>
                <a href="?<?= http_build_query(array_merge($_GET, ['time' => '30'])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= $filter_time === '30' ? 'background: #2196F3; color: white;' : 'background: #eee; color: #666;' ?>">30๋ถ„ ์ดํ•˜</a>
                <a href="?<?= http_build_query(array_merge($_GET, ['time' => '60'])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= $filter_time === '60' ? 'background: #2196F3; color: white;' : 'background: #eee; color: #666;' ?>">30~60๋ถ„</a>
                <a href="?<?= http_build_query(array_merge($_GET, ['time' => '61'])) ?>" style="padding: 6px 14px; border-radius: 16px; text-decoration: none; <?= $filter_time === '61' ? 'background: #2196F3; color: white;' : 'background: #eee; color: #666;' ?>">60๋ถ„ ์ด์ƒ</a>
            </div>
        </div>
    </div>
    
    <?php if ($filter_tag || $filter_difficulty || $filter_time || !empty($search_ingredients)): ?>
    <div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #ddd;">
        <a href="/recipes" style="color: var(--m-text-muted); text-decoration: none; font-size: 0.9rem;">๐Ÿ”„ ๋ชจ๋“  ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”</a>
    </div>
    <?php endif; ?>
</div>
<?php endif; ?>
```

## 3) ํ™•์ธ

- ํƒœ๊ทธ ํ•„ํ„ฐ๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ํƒœ๊ทธ๊ฐ€ ํฌํ•จ๋œ ๋ ˆ์‹œํ”ผ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ๋‚œ์ด๋„ ํ•„ํ„ฐ๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ๋‚œ์ด๋„์˜ ๋ ˆ์‹œํ”ผ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ์กฐ๋ฆฌ์‹œ๊ฐ„ ํ•„ํ„ฐ๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€์˜ ๋ ˆ์‹œํ”ผ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
- ์—ฌ๋Ÿฌ ํ•„ํ„ฐ๋ฅผ ๋™์‹œ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (AND ์กฐ๊ฑด)
- "๋ชจ๋“  ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”" ํด๋ฆญ ์‹œ ์ „์ฒด ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค
- ํ™œ์„ฑํ™”๋œ ํ•„ํ„ฐ๋Š” ์ƒ‰์ƒ์ด ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: - ํƒœ๊ทธ ํ•„ํ„ฐ ๋ฒ„ํŠผ ํ‘œ์‹œ (๋“ฑ๋ก๋œ ํƒœ๊ทธ ์ž๋™ ์ˆ˜์ง‘) - ๋‚œ์ด๋„ ํ•„ํ„ฐ (์‰ฌ์›€/๋ณดํ†ต/์–ด๋ ค์›€) - ์กฐ๋ฆฌ์‹œ๊ฐ„ ํ•„ํ„ฐ (30๋ถ„ ์ดํ•˜/30~60๋ถ„/60๋ถ„ ์ด์ƒ) - ํ•„ํ„ฐ ์กฐํ•ฉ ๊ฐ€๋Šฅ (์žฌ๋ฃŒ ๊ฒ€์ƒ‰ + ํƒœ๊ทธ + ๋‚œ์ด๋„ + ์‹œ๊ฐ„) - ํ™œ์„ฑ ํ•„ํ„ฐ ์‹œ๊ฐ์  ํ‘œ์‹œ (๋ฐฐ๊ฒฝ์ƒ‰ ๋ณ€๊ฒฝ) - ์ „์ฒด ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ - URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•„ํ„ฐ ์ƒํƒœ ์œ ์ง€

์ œ์ถœ๋œ ์ž‘ํ’ˆ (0)

๋กœ๊ทธ์ธ ํ›„ ์ œ์ถœ

์•„์ง ์ œ์ถœ๋œ ์ž‘ํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๋„์ „์ž๊ฐ€ ๋˜์–ด๋ณด์„ธ์š”!

๋Œ“๊ธ€ (0)

๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๋Œ“๊ธ€์„ ๋‚จ๊ฒจ๋ณด์„ธ์š”!