๐ŸŽจ ์˜จ๋ผ์ธ ํฌํŠธํด๋ฆฌ์˜ค CMS - ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŽ˜์ด์ง€ ๋นŒ๋” & ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP Vanilla PHP AI: Claude

๐ŸŽจ ์˜จ๋ผ์ธ ํฌํŠธํด๋ฆฌ์˜ค CMS - ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŽ˜์ด์ง€ ๋นŒ๋” & ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… portfolio-cms-drag-drop
์กฐํšŒ์ˆ˜ 166 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

ํ”„๋ฆฌ๋žœ์„œ ๋””์ž์ด๋„ˆ, ์‚ฌ์ง„์ž‘๊ฐ€, ์ผ๋Ÿฌ์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋“ฑ ํฌ๋ฆฌ์—์ดํ„ฐ๋ฅผ ์œ„ํ•œ ์ผ๋ฐ˜ PHP ๊ธฐ๋ฐ˜ ํฌํŠธํด๋ฆฌ์˜ค CMS๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๊ด€๋ฆฌ์ž ์˜์—ญ์—์„œ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์„น์…˜์„ ๋ฐฐ์น˜ํ•˜๊ณ , ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜์—ฌ ์ž์‹ ๋งŒ์˜ ํฌํŠธํด๋ฆฌ์˜ค ์‚ฌ์ดํŠธ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ฏธ๋ฆฌ๋ณด๊ธฐํ•˜๋ฉฐ, ๋ฐฉ๋ฌธ์ž๋Š” ์™„์„ฑ๋œ ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

  1. ์‚ฌ์šฉ์ž ์ธ์ฆ & ๊ถŒํ•œ

    • ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ (๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ: password_hash/password_verify)
    • ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ (CSRF ํ† ํฐ ๊ฒ€์ฆ)
    • ๊ด€๋ฆฌ์ž ์ „์šฉ ์˜์—ญ ์ ‘๊ทผ ์ œ์–ด
  2. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŽ˜์ด์ง€ ๋นŒ๋”

    • ์„น์…˜ ํƒ€์ž…: Hero, About, Gallery, Contact, Testimonial, Skills
    • Sortable.js๋ฅผ ํ™œ์šฉํ•œ ์„น์…˜ ์ˆœ์„œ ๋ณ€๊ฒฝ
    • ๊ฐ ์„น์…˜๋ณ„ ์„ค์ • ๋ชจ๋‹ฌ (์ œ๋ชฉ, ์„ค๋ช…, ์ด๋ฏธ์ง€ ์„ ํƒ ๋“ฑ)
    • ์„น์…˜ ์ถ”๊ฐ€/์‚ญ์ œ/์ˆ˜์ • (AJAX ์ €์žฅ)
  3. ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ (JPEG, PNG, WebP ์ง€์›, ์ตœ๋Œ€ 5MB)
    • ์ธ๋„ค์ผ ์ž๋™ ์ƒ์„ฑ (GD ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)
    • ๊ทธ๋ฆฌ๋“œ ๋ทฐ๋กœ ๋ฏธ๋””์–ด ํƒ์ƒ‰
    • ์ด๋ฏธ์ง€ ์„ ํƒ ๋ชจ๋‹ฌ (ํŽ˜์ด์ง€ ๋นŒ๋”์—์„œ ์‚ฌ์šฉ)
  4. ํ”„๋ก ํŠธ์—”๋“œ ํฌํŠธํด๋ฆฌ์˜ค ๋ Œ๋”๋ง

    • SEO ์นœํ™”์  HTML ๊ตฌ์กฐ
    • ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ/๋ฐ์Šคํฌํƒ‘)
    • ๋ผ์ดํŠธ๋ฐ•์Šค ๊ฐค๋Ÿฌ๋ฆฌ (์ด๋ฏธ์ง€ ํ™•๋Œ€ ๋ณด๊ธฐ)
    • ์˜คํ”ˆ ๊ทธ๋ž˜ํ”„ ๋ฉ”ํƒ€ ํƒœ๊ทธ ์ž๋™ ์ƒ์„ฑ
  5. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ (SQLite + PDO)

    • users ํ…Œ์ด๋ธ”: id, username, email, password, created_at
    • sections ํ…Œ์ด๋ธ”: id, user_id, type, title, content, image_id, sort_order, created_at
    • media ํ…Œ์ด๋ธ”: id, user_id, filename, path, thumbnail_path, size, mime_type, created_at
    • ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด ๋ฐ ์ธ๋ฑ์Šค ์ตœ์ ํ™”
  6. ๋ณด์•ˆ & ์„ฑ๋Šฅ

    • Prepared Statement๋กœ SQL Injection ๋ฐฉ์–ด
    • htmlspecialchars()๋กœ XSS ๋ฐฉ์–ด
    • CSRF ํ† ํฐ ๊ฒ€์ฆ (๋ชจ๋“  POST ์š”์ฒญ)
    • ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฒ€์ฆ (MIME ํƒ€์ž…, ํ™•์žฅ์ž, ํฌ๊ธฐ)
    • ์ด๋ฏธ์ง€ ์บ์‹ฑ (Last-Modified ํ—ค๋”)
  7. ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

    • ํฌํŠธํด๋ฆฌ์˜ค ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์ „ํ™˜
    • ์„น์…˜๋ณ„ ์ปค์Šคํ…€ CSS ํด๋ž˜์Šค ์ง€์ •
    • ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ (์กฐํšŒ์ˆ˜ ์ง‘๊ณ„, ์ผ๋ณ„ ๋กœ๊ทธ)
    • JSON ๊ธฐ๋ฐ˜ ์„น์…˜ ์„ค์ • ์ €์žฅ

์„ ํƒ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ (๋ณด๋„ˆ์Šค)

  • ๋‹คํฌ ๋ชจ๋“œ ํ…Œ๋งˆ ์ „ํ™˜
  • ์„น์…˜ ํ…œํ”Œ๋ฆฟ ํ”„๋ฆฌ์…‹ (๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด์ง„ ๋ ˆ์ด์•„์›ƒ)
  • ์ด๋ฏธ์ง€ ํฌ๋กญ/๋ฆฌ์‚ฌ์ด์ฆˆ ์—๋””ํ„ฐ
  • ์†Œ์…œ ๋ฏธ๋””์–ด ๊ณต์œ  ๋ฒ„ํŠผ
  • Google Analytics ์—ฐ๋™ ์ฝ”๋“œ ์‚ฝ์ž…
  • ๋‹ค๊ตญ์–ด ์ง€์› (i18n)

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: PHP 8.3+ (Vanilla, ํ”„๋ ˆ์ž„์›Œํฌ ์—†์Œ)
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite (PDO)
  • ํ”„๋ก ํŠธ์—”๋“œ: HTML5, CSS3 (Tailwind CDN), Vanilla JavaScript
  • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: Sortable.js (๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ), GLightbox (๋ผ์ดํŠธ๋ฐ•์Šค)
  • ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ: GD ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • ์„œ๋ฒ„: PHP ๋‚ด์žฅ ์„œ๋ฒ„ (php -S localhost:8000)

๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ

portfolio-cms-drag-drop/
โ”œโ”€โ”€ index.php              # ํ”„๋ก ํŠธ์—”๋“œ ํฌํŠธํด๋ฆฌ์˜ค ๋ Œ๋”๋ง
โ”œโ”€โ”€ admin/
โ”‚   โ”œโ”€โ”€ login.php          # ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€
โ”‚   โ”œโ”€โ”€ dashboard.php      # ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ
โ”‚   โ”œโ”€โ”€ builder.php        # ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŽ˜์ด์ง€ ๋นŒ๋”
โ”‚   โ””โ”€โ”€ media.php          # ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ”œโ”€โ”€ api/
โ”‚   โ”œโ”€โ”€ auth.php           # ์ธ์ฆ API (๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ)
โ”‚   โ”œโ”€โ”€ sections.php       # ์„น์…˜ CRUD API
โ”‚   โ””โ”€โ”€ media.php          # ๋ฏธ๋””์–ด ์—…๋กœ๋“œ/์‚ญ์ œ API
โ”œโ”€โ”€ includes/
โ”‚   โ”œโ”€โ”€ db.php             # PDO ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
โ”‚   โ”œโ”€โ”€ auth.php           # ์ธ์ฆ ํ—ฌํผ ํ•จ์ˆ˜
โ”‚   โ””โ”€โ”€ csrf.php           # CSRF ํ† ํฐ ์ƒ์„ฑ/๊ฒ€์ฆ
โ”œโ”€โ”€ uploads/               # ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ์ €์žฅ์†Œ
โ”‚   โ”œโ”€โ”€ original/
โ”‚   โ””โ”€โ”€ thumbnails/
โ”œโ”€โ”€ assets/
โ”‚   โ”œโ”€โ”€ css/
โ”‚   โ””โ”€โ”€ js/
โ””โ”€โ”€ database.sqlite        # SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข PHP 8.3 ์ด์ƒ ์‚ฌ์šฉ (ํƒ€์ž… ํžŒํŒ…, ์†์„ฑ ํ”„๋กœ๋ชจ์…˜ ๋“ฑ ์ตœ์‹  ๋ฌธ๋ฒ• ํ™œ์šฉ)
  • โ€ข Laravel, Symfony, CMS(์›Œ๋“œํ”„๋ ˆ์Šค, ๊ทธ๋ˆ„๋ณด๋“œ ๋“ฑ) ์‚ฌ์šฉ ๊ธˆ์ง€
  • โ€ข Composer ํŒจํ‚ค์ง€ ์‚ฌ์šฉ ๊ธˆ์ง€ (์ˆœ์ˆ˜ PHP + ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ)
  • โ€ข React, Vue, Node.js ๋“ฑ ๋ณ„๋„ ๋นŒ๋“œ ๋„๊ตฌ ์‚ฌ์šฉ ๊ธˆ์ง€
  • โ€ข ํ”„๋ก ํŠธ์—”๋“œ๋Š” Tailwind CSS CDN + Vanilla JavaScript๋งŒ ์‚ฌ์šฉ
  • โ€ข PHP ๋‚ด์žฅ ์„œ๋ฒ„๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ (`php -S localhost:8000`)
  • โ€ข PDO + SQLite๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅ (MySQL/PostgreSQL ๊ธˆ์ง€)
  • โ€ข ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ `htmlspecialchars()` ๋˜๋Š” Prepared Statement๋กœ ์ด์Šค์ผ€์ดํ”„
  • โ€ข ํŒŒ์ผ ์—…๋กœ๋“œ๋Š” MIME ํƒ€์ž…, ํ™•์žฅ์ž, ํฌ๊ธฐ ๊ฒ€์ฆ ํ•„์ˆ˜
  • โ€ข CSRF ํ† ํฐ์€ ์„ธ์…˜์— ์ €์žฅํ•˜๊ณ  ๋ชจ๋“  POST ์š”์ฒญ์—์„œ ๊ฒ€์ฆ
  • โ€ข ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” `password_hash(PASSWORD_BCRYPT)` ์‚ฌ์šฉ
  • โ€ข ์ด๋ฏธ์ง€ ์ธ๋„ค์ผ์€ GD ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์ž๋™ ์ƒ์„ฑ (์ตœ๋Œ€ 300x300px)
  • โ€ข ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์€ Sortable.js CDN ์‚ฌ์šฉ (https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js)
  • โ€ข ๋ผ์ดํŠธ๋ฐ•์Šค๋Š” GLightbox CDN ์‚ฌ์šฉ (https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js)
  • โ€ข ์„น์…˜ ์„ค์ •์€ JSON ํ˜•์‹์œผ๋กœ `sections.content` ์ปฌ๋Ÿผ์— ์ €์žฅ
  • โ€ข ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„๋Š” `stats` ํ…Œ์ด๋ธ”์— ๋‚ ์งœ๋ณ„๋กœ ์ง‘๊ณ„
  • โ€ข ๊ด€๋ฆฌ์ž ์˜์—ญ์€ `/admin` ๊ฒฝ๋กœ๋กœ ์ ‘๊ทผํ•˜๋ฉฐ ์ธ์ฆ ํ•„์ˆ˜

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

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

1
Step 1
mkdir portfolio-cms-drag-drop && cd portfolio-cms-drag-drop

์ด ํด๋”์—์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

# ๋‹จ๊ณ„ 1: ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ์„ค์ • ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๊ตฌ์ถ•

## ์ž‘์—… ๋‚ด์šฉ

1. **๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ์ƒ์„ฑ**
   - `admin/`, `api/`, `includes/`, `uploads/original/`, `uploads/thumbnails/`, `assets/css/`, `assets/js/` ํด๋” ์ƒ์„ฑ

2. **SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”**
   - `includes/db.php` ํŒŒ์ผ ์ƒ์„ฑ
   - PDO๋กœ `database.sqlite` ์—ฐ๊ฒฐ
   - ํ…Œ์ด๋ธ” ์ƒ์„ฑ SQL:
     ```sql
     CREATE TABLE IF NOT EXISTS users (
       id INTEGER PRIMARY KEY AUTOINCREMENT,
       username TEXT UNIQUE NOT NULL,
       email TEXT UNIQUE NOT NULL,
       password TEXT NOT NULL,
       created_at DATETIME DEFAULT CURRENT_TIMESTAMP
     );

     CREATE TABLE IF NOT EXISTS sections (
       id INTEGER PRIMARY KEY AUTOINCREMENT,
       user_id INTEGER NOT NULL,
       type TEXT NOT NULL CHECK(type IN ('hero', 'about', 'gallery', 'contact', 'testimonial', 'skills')),
       title TEXT NOT NULL,
       content TEXT,
       image_id INTEGER,
       sort_order INTEGER NOT NULL DEFAULT 0,
       created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
       FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
       FOREIGN KEY (image_id) REFERENCES media(id) ON DELETE SET NULL
     );

     CREATE TABLE IF NOT EXISTS media (
       id INTEGER PRIMARY KEY AUTOINCREMENT,
       user_id INTEGER NOT NULL,
       filename TEXT NOT NULL,
       path TEXT NOT NULL,
       thumbnail_path TEXT NOT NULL,
       size INTEGER NOT NULL,
       mime_type TEXT NOT NULL,
       created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
       FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
     );

     CREATE TABLE IF NOT EXISTS stats (
       id INTEGER PRIMARY KEY AUTOINCREMENT,
       user_id INTEGER NOT NULL,
       date DATE NOT NULL,
       views INTEGER DEFAULT 0,
       UNIQUE(user_id, date),
       FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
     );

     CREATE INDEX idx_sections_user_sort ON sections(user_id, sort_order);
     CREATE INDEX idx_media_user ON media(user_id);
     CREATE INDEX idx_stats_user_date ON stats(user_id, date);
     ```

3. **์ธ์ฆ ํ—ฌํผ ํ•จ์ˆ˜ ์ž‘์„ฑ**
   - `includes/auth.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ํ•จ์ˆ˜ ๋ชฉ๋ก:
     - `isLoggedIn()`: ์„ธ์…˜์—์„œ ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ
     - `requireAuth()`: ๋กœ๊ทธ์ธ ํ•„์ˆ˜ ํŽ˜์ด์ง€์—์„œ ํ˜ธ์ถœ, ๋ฏธ์ธ์ฆ ์‹œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
     - `getCurrentUserId()`: ์„ธ์…˜์—์„œ ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ๋ฐ˜ํ™˜

4. **CSRF ํ† ํฐ ํ—ฌํผ ํ•จ์ˆ˜ ์ž‘์„ฑ**
   - `includes/csrf.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ํ•จ์ˆ˜ ๋ชฉ๋ก:
     - `generateCSRFToken()`: ์„ธ์…˜์— ํ† ํฐ ์ƒ์„ฑ ๋ฐ ์ €์žฅ
     - `verifyCSRFToken($token)`: POST ์š”์ฒญ์˜ ํ† ํฐ ๊ฒ€์ฆ
     - `csrfField()`: HTML ํผ์— ์‚ฝ์ž…ํ•  hidden input ์ƒ์„ฑ

5. **๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ๊ณ„์ • ์ƒ์„ฑ (ํ…Œ์ŠคํŠธ์šฉ)**
   - `setup.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ์‹คํ–‰ ์‹œ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ž๋™ ์ƒ์„ฑ:
     - username: `admin`
     - email: `admin@example.com`
     - password: `password123` (ํ•ด์‹œ ์ €์žฅ)
   - ํ„ฐ๋ฏธ๋„์—์„œ `php setup.php` ์‹คํ–‰ ํ›„ "Setup complete!" ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ

## ์š”๊ตฌ์‚ฌํ•ญ

- PHP 8.3+ ๋ฌธ๋ฒ• ์‚ฌ์šฉ (ํƒ€์ž… ํžŒํŒ…, readonly ์†์„ฑ ๋“ฑ)
- PDO ์—๋Ÿฌ ๋ชจ๋“œ๋Š” `PDO::ERRMODE_EXCEPTION`์œผ๋กœ ์„ค์ •
- ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” `password_hash($password, PASSWORD_BCRYPT)` ์‚ฌ์šฉ
- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ์€ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— `database.sqlite`๋กœ ์ƒ์„ฑ
- ๋ชจ๋“  ํŒŒ์ผ์€ UTF-8 ์ธ์ฝ”๋”ฉ

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- `database.sqlite` ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ 4๊ฐœ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ
- `includes/db.php`, `includes/auth.php`, `includes/csrf.php` ํŒŒ์ผ ์ƒ์„ฑ
- `setup.php` ์‹คํ–‰ ์‹œ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ ํ™•์ธ
- ํ„ฐ๋ฏธ๋„์—์„œ `php -S localhost:8000` ์‹คํ–‰ ์ค€๋น„ ์™„๋ฃŒ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— users, sections, media, stats ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ธ์ฆ ๋ฐ CSRF ํ—ฌํผ ํ•จ์ˆ˜๊ฐ€ ์ž‘์„ฑ๋˜๋ฉฐ, ํ…Œ์ŠคํŠธ์šฉ ๊ด€๋ฆฌ์ž ๊ณ„์ •์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

2
Step 2
# ๋‹จ๊ณ„ 2: ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ• (ํ”„๋ก ํŠธ์—”๋“œ + API)

## ์ž‘์—… ๋‚ด์šฉ

1. **๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ƒ์„ฑ**
   - `admin/login.php` ํŒŒ์ผ ์ƒ์„ฑ
   - Tailwind CSS CDN์œผ๋กœ ์Šคํƒ€์ผ๋ง
   - ํผ ๊ตฌ์„ฑ:
     - ์ด๋ฉ”์ผ ์ž…๋ ฅ ํ•„๋“œ (type="email", required)
     - ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํ•„๋“œ (type="password", required)
     - CSRF ํ† ํฐ hidden input (`csrfField()` ์‚ฌ์šฉ)
     - ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ
   - ํผ ์•ก์…˜: `POST /api/auth.php?action=login`
   - ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ ์˜์—ญ (GET ํŒŒ๋ผ๋ฏธํ„ฐ `error` ์ฒดํฌ)

2. **์ธ์ฆ API ๊ตฌํ˜„**
   - `api/auth.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ์•ก์…˜ ๋ถ„๊ธฐ:
     - `action=login`: ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
       - CSRF ํ† ํฐ ๊ฒ€์ฆ
       - ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ (PDO Prepared Statement)
       - `password_verify()`๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ
       - ์„ฑ๊ณต ์‹œ ์„ธ์…˜์— `user_id`, `username` ์ €์žฅ ํ›„ `/admin/dashboard.php` ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
       - ์‹คํŒจ ์‹œ `/admin/login.php?error=invalid` ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
     - `action=logout`: ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ
       - ์„ธ์…˜ ํŒŒ๊ดด (`session_destroy()`)
       - `/admin/login.php` ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ

3. **๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ (์ž„์‹œ)**
   - `admin/dashboard.php` ํŒŒ์ผ ์ƒ์„ฑ
   - `requireAuth()` ํ˜ธ์ถœ๋กœ ์ธ์ฆ ํ™•์ธ
   - ๊ฐ„๋‹จํ•œ ํ™˜์˜ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ: "Welcome, {username}!"
   - ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ (๋งํฌ: `/api/auth.php?action=logout`)
   - ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ํŽ˜์ด์ง€ ๋นŒ๋” ๋งํฌ ์ถ”๊ฐ€ ์˜ˆ์ •

4. **์„ธ์…˜ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ ์ถ”๊ฐ€**
   - ๋ชจ๋“  PHP ํŒŒ์ผ ์ƒ๋‹จ์— `session_start()` ์ถ”๊ฐ€
   - `includes/auth.php`์—์„œ ์„ธ์…˜ ์‹œ์ž‘ ํ™•์ธ

## ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ

- CSRF ํ† ํฐ ๊ฒ€์ฆ ์‹คํŒจ ์‹œ 403 ์—๋Ÿฌ ๋ฐ˜ํ™˜
- ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹คํŒจ ์‹œ ๊ตฌ์ฒด์ ์ธ ์ด์œ  ๋…ธ์ถœ ๊ธˆ์ง€ ("Invalid credentials" ๋ฉ”์‹œ์ง€๋งŒ)
- SQL Injection ๋ฐฉ์ง€: PDO Prepared Statement ์‚ฌ์šฉ
- XSS ๋ฐฉ์ง€: ๋ชจ๋“  ์ถœ๋ ฅ์— `htmlspecialchars()` ์ ์šฉ

## ๋””์ž์ธ ๊ฐ€์ด๋“œ

- Tailwind CSS ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค ์‚ฌ์šฉ
- ๋กœ๊ทธ์ธ ํผ์€ ํ™”๋ฉด ์ค‘์•™ ์ •๋ ฌ (`flex items-center justify-center min-h-screen`)
- ์ž…๋ ฅ ํ•„๋“œ๋Š” `border rounded-lg px-4 py-2` ์Šคํƒ€์ผ
- ๋ฒ„ํŠผ์€ `bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700` ์Šคํƒ€์ผ

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- `/admin/login.php` ์ ‘์† ์‹œ ๋กœ๊ทธ์ธ ํผ ํ‘œ์‹œ
- ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™
- ์ž˜๋ชป๋œ ์ •๋ณด ์ž…๋ ฅ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ
- ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์„ธ์…˜ ํŒŒ๊ดด ๋ฐ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์™€ ์ธ์ฆ API๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋กœ๊ทธ์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ํ•˜๋ฉฐ, ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ๋„ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

3
Step 3
# ๋‹จ๊ณ„ 3: ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ตฌ์ถ• (์—…๋กœ๋“œ + ์ธ๋„ค์ผ ์ƒ์„ฑ)

## ์ž‘์—… ๋‚ด์šฉ

1. **๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŽ˜์ด์ง€ ์ƒ์„ฑ**
   - `admin/media.php` ํŒŒ์ผ ์ƒ์„ฑ
   - `requireAuth()` ํ˜ธ์ถœ๋กœ ์ธ์ฆ ํ™•์ธ
   - ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜: Dashboard, Page Builder, Media Library (ํ˜„์žฌ ํŽ˜์ด์ง€ ๊ฐ•์กฐ)
   - ์—…๋กœ๋“œ ํผ:
     - `<input type="file" name="image" accept="image/jpeg,image/png,image/webp" multiple>`
     - CSRF ํ† ํฐ hidden input
     - ํผ ์•ก์…˜: `POST /api/media.php?action=upload`
     - enctype="multipart/form-data"
   - ๋ฏธ๋””์–ด ๊ทธ๋ฆฌ๋“œ:
     - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ๋ฏธ๋””์–ด ์กฐํšŒ (์ตœ์‹ ์ˆœ ์ •๋ ฌ)
     - ๊ฐ ๋ฏธ๋””์–ด ์นด๋“œ:
       - ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ํ‘œ์‹œ (`<img src="/uploads/thumbnails/{filename}">`)
       - ํŒŒ์ผ๋ช…, ํฌ๊ธฐ(KB), ์—…๋กœ๋“œ ๋‚ ์งœ ํ‘œ์‹œ
       - ์‚ญ์ œ ๋ฒ„ํŠผ (AJAX๋กœ `/api/media.php?action=delete` ํ˜ธ์ถœ)

2. **๋ฏธ๋””์–ด ์—…๋กœ๋“œ API ๊ตฌํ˜„**
   - `api/media.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ์•ก์…˜ ๋ถ„๊ธฐ:
     - `action=upload`: ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
       - CSRF ํ† ํฐ ๊ฒ€์ฆ
       - ํŒŒ์ผ ๊ฒ€์ฆ:
         - MIME ํƒ€์ž… ์ฒดํฌ (`image/jpeg`, `image/png`, `image/webp`๋งŒ ํ—ˆ์šฉ)
         - ํŒŒ์ผ ํฌ๊ธฐ ์ฒดํฌ (์ตœ๋Œ€ 5MB)
         - ํŒŒ์ผ ํ™•์žฅ์ž ์ฒดํฌ (ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹)
       - ๊ณ ์œ  ํŒŒ์ผ๋ช… ์ƒ์„ฑ: `{timestamp}_{random}_{original_name}`
       - ์›๋ณธ ์ด๋ฏธ์ง€ ์ €์žฅ: `/uploads/original/`
       - ์ธ๋„ค์ผ ์ƒ์„ฑ (GD ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ):
         - ์ตœ๋Œ€ 300x300px๋กœ ๋ฆฌ์‚ฌ์ด์ฆˆ
         - ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ํฌ๋กญ
         - ์ €์žฅ ๊ฒฝ๋กœ: `/uploads/thumbnails/`
       - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฏธ๋””์–ด ์ •๋ณด ์ €์žฅ (PDO Prepared Statement)
       - ์„ฑ๊ณต ์‹œ JSON ์‘๋‹ต: `{"success": true, "media_id": 123}`
     - `action=delete`: ์ด๋ฏธ์ง€ ์‚ญ์ œ
       - CSRF ํ† ํฐ ๊ฒ€์ฆ
       - ๋ฏธ๋””์–ด ์†Œ์œ ๊ถŒ ํ™•์ธ (user_id ์ผ์น˜ ์—ฌ๋ถ€)
       - ํŒŒ์ผ ์‹œ์Šคํ…œ์—์„œ ์›๋ณธ ๋ฐ ์ธ๋„ค์ผ ์‚ญ์ œ
       - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ
       - ์„ฑ๊ณต ์‹œ JSON ์‘๋‹ต: `{"success": true}`
     - `action=list`: ๋ฏธ๋””์–ด ๋ชฉ๋ก ์กฐํšŒ (JSON)
       - ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ๋ฏธ๋””์–ด ๋ชฉ๋ก ๋ฐ˜ํ™˜
       - ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์› (์„ ํƒ ์‚ฌํ•ญ)

3. **์ธ๋„ค์ผ ์ƒ์„ฑ ํ—ฌํผ ํ•จ์ˆ˜**
   - `includes/image.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ํ•จ์ˆ˜: `createThumbnail($sourcePath, $destPath, $maxWidth = 300, $maxHeight = 300)`
   - GD ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ:
     - `imagecreatefromjpeg()`, `imagecreatefrompng()`, `imagecreatefromwebp()`
     - `imagecopyresampled()` ๋กœ ๋ฆฌ์‚ฌ์ด์ฆˆ
     - `imagejpeg()`, `imagepng()`, `imagewebp()`๋กœ ์ €์žฅ
   - ๋น„์œจ ์œ ์ง€ ๊ณ„์‚ฐ ๋กœ์ง ํฌํ•จ

4. **ํ”„๋ก ํŠธ์—”๋“œ JavaScript (AJAX ์‚ญ์ œ)**
   - `assets/js/media.js` ํŒŒ์ผ ์ƒ์„ฑ
   - ์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ:
     - `fetch('/api/media.php?action=delete', { method: 'POST', body: formData })`
     - ์„ฑ๊ณต ์‹œ ํ•ด๋‹น ๋ฏธ๋””์–ด ์นด๋“œ DOM์—์„œ ์ œ๊ฑฐ
     - ์‹คํŒจ ์‹œ alert ํ‘œ์‹œ

## ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ

- ์—…๋กœ๋“œ ํŒŒ์ผ์€ ๋ฐ˜๋“œ์‹œ `$_FILES['image']['tmp_name']`์—์„œ ์ฝ๊ธฐ
- `move_uploaded_file()` ์‚ฌ์šฉ (๋ณด์•ˆ ๊ฒ€์ฆ ํฌํ•จ)
- ํŒŒ์ผ๋ช…์€ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ ๊ธˆ์ง€ (๋žœ๋ค ๋ฌธ์ž์—ด ์ถ”๊ฐ€)
- ๋ฏธ๋””์–ด ์‚ญ์ œ ์‹œ ์†Œ์œ ๊ถŒ ํ™•์ธ ํ•„์ˆ˜
- MIME ํƒ€์ž…์€ `mime_content_type()` ํ•จ์ˆ˜๋กœ ์žฌ๊ฒ€์ฆ

## ๋””์ž์ธ ๊ฐ€์ด๋“œ

- ๋ฏธ๋””์–ด ๊ทธ๋ฆฌ๋“œ๋Š” `grid grid-cols-3 gap-4` (๋ฐ˜์‘ํ˜•: `md:grid-cols-4 lg:grid-cols-6`)
- ์ธ๋„ค์ผ์€ `aspect-square object-cover rounded-lg` ์Šคํƒ€์ผ
- ์‚ญ์ œ ๋ฒ„ํŠผ์€ ์นด๋“œ ์šฐ์ธก ์ƒ๋‹จ์— ์ ˆ๋Œ€ ์œ„์น˜ (`absolute top-2 right-2`)

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- `/admin/media.php` ์ ‘์† ์‹œ ์—…๋กœ๋“œ ํผ ๋ฐ ๋ฏธ๋””์–ด ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ
- ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ ์›๋ณธ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ ํ™•์ธ
- ๋ฏธ๋””์–ด ๊ทธ๋ฆฌ๋“œ์— ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ์นด๋“œ ํ‘œ์‹œ
- ์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ AJAX๋กœ ์‚ญ์ œ ๋ฐ ์นด๋“œ ์ œ๊ฑฐ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŽ˜์ด์ง€๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ ์›๋ณธ๊ณผ ์ธ๋„ค์ผ์ด ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€๋Š” ๊ทธ๋ฆฌ๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋˜๋ฉฐ, ์‚ญ์ œ ๊ธฐ๋Šฅ๋„ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

4
Step 4
# ๋‹จ๊ณ„ 4: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŽ˜์ด์ง€ ๋นŒ๋” UI ๊ตฌ์ถ•

## ์ž‘์—… ๋‚ด์šฉ

1. **ํŽ˜์ด์ง€ ๋นŒ๋” ํŽ˜์ด์ง€ ์ƒ์„ฑ**
   - `admin/builder.php` ํŒŒ์ผ ์ƒ์„ฑ
   - `requireAuth()` ํ˜ธ์ถœ๋กœ ์ธ์ฆ ํ™•์ธ
   - ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ:
     - ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ” (์„น์…˜ ํƒ€์ž… ์„ ํƒ):
       - Hero, About, Gallery, Contact, Testimonial, Skills
       - ๊ฐ ํƒ€์ž…๋ณ„ "+ Add" ๋ฒ„ํŠผ
     - ์ค‘์•™ ์บ”๋ฒ„์Šค (๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์˜์—ญ):
       - ์„น์…˜ ์นด๋“œ ๋ชฉ๋ก (sortable)
       - ๊ฐ ์นด๋“œ:
         - ํƒ€์ž… ์•„์ด์ฝ˜ ๋ฐ ์ œ๋ชฉ ํ‘œ์‹œ
         - ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค (โ‹ฎโ‹ฎ ์•„์ด์ฝ˜)
         - ํŽธ์ง‘ ๋ฒ„ํŠผ (๋ชจ๋‹ฌ ์˜คํ”ˆ)
         - ์‚ญ์ œ ๋ฒ„ํŠผ
     - ์šฐ์ธก ํ”„๋ฆฌ๋ทฐ (์„ ํƒ ์‚ฌํ•ญ, ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„)

2. **Sortable.js ํ†ตํ•ฉ**
   - `<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>` CDN ์ถ”๊ฐ€
   - `assets/js/builder.js` ํŒŒ์ผ ์ƒ์„ฑ
   - Sortable ์ดˆ๊ธฐํ™”:
     ```javascript
     const sortable = Sortable.create(document.getElementById('sections-canvas'), {
       animation: 150,
       handle: '.drag-handle',
       onEnd: function(evt) {
         updateSortOrder();
       }
     });
     ```
   - `updateSortOrder()` ํ•จ์ˆ˜:
     - ํ˜„์žฌ ์„น์…˜ ์ˆœ์„œ๋ฅผ ๋ฐฐ์—ด๋กœ ์ˆ˜์ง‘
     - AJAX๋กœ `/api/sections.php?action=reorder` ํ˜ธ์ถœ
     - ์„œ๋ฒ„์—์„œ `sort_order` ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ

3. **์„น์…˜ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ**
   - ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”์˜ "+ Add" ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ:
     - AJAX๋กœ `/api/sections.php?action=create` ํ˜ธ์ถœ
     - POST ๋ฐ์ดํ„ฐ: `{ type: 'hero', title: 'New Hero Section', content: '' }`
     - ์„ฑ๊ณต ์‹œ ์บ”๋ฒ„์Šค์— ์ƒˆ ์„น์…˜ ์นด๋“œ ์ถ”๊ฐ€

4. **์„น์…˜ ํŽธ์ง‘ ๋ชจ๋‹ฌ**
   - ํŽธ์ง‘ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ ์˜คํ”ˆ
   - ๋ชจ๋‹ฌ ๊ตฌ์„ฑ:
     - ์„น์…˜ ํƒ€์ž…๋ณ„ ์ž…๋ ฅ ํ•„๋“œ:
       - Hero: ์ œ๋ชฉ, ๋ถ€์ œ๋ชฉ, ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์„ ํƒ ๋ฒ„ํŠผ
       - About: ์ œ๋ชฉ, ๋ณธ๋ฌธ ํ…์ŠคํŠธ (textarea), ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์„ ํƒ
       - Gallery: ์ œ๋ชฉ, ์ด๋ฏธ์ง€ ์„ ํƒ (๋‹ค์ค‘ ์„ ํƒ, ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„)
       - Contact: ์ œ๋ชฉ, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ
       - Testimonial: ์ œ๋ชฉ, ์ธ์šฉ๋ฌธ, ์ž‘์„ฑ์ž๋ช…, ์ž‘์„ฑ์ž ์‚ฌ์ง„
       - Skills: ์ œ๋ชฉ, ์Šคํ‚ฌ ๋ชฉ๋ก (JSON ํ˜•์‹)
     - ์ด๋ฏธ์ง€ ์„ ํƒ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ:
       - ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ชจ๋‹ฌ ์˜คํ”ˆ
       - ์ธ๋„ค์ผ ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ
       - ์ด๋ฏธ์ง€ ํด๋ฆญ ์‹œ ์„ ํƒ ๋ฐ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ
     - ์ €์žฅ ๋ฒ„ํŠผ: AJAX๋กœ `/api/sections.php?action=update` ํ˜ธ์ถœ

5. **์„น์…˜ ์‚ญ์ œ ๊ธฐ๋Šฅ**
   - ์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ
   - ํ™•์ธ ์‹œ AJAX๋กœ `/api/sections.php?action=delete` ํ˜ธ์ถœ
   - ์„ฑ๊ณต ์‹œ ์บ”๋ฒ„์Šค์—์„œ ์„น์…˜ ์นด๋“œ ์ œ๊ฑฐ

6. **์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ**
   - ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ `/api/sections.php?action=list` ํ˜ธ์ถœ
   - ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ์„น์…˜ ๋ชฉ๋ก ์กฐํšŒ (sort_order ์ˆœ)
   - ์บ”๋ฒ„์Šค์— ์„น์…˜ ์นด๋“œ ๋ Œ๋”๋ง

## ๋””์ž์ธ ๊ฐ€์ด๋“œ

- ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”: `w-64 bg-gray-100 p-4`
- ์ค‘์•™ ์บ”๋ฒ„์Šค: `flex-1 bg-white p-6`
- ์„น์…˜ ์นด๋“œ: `border rounded-lg p-4 mb-4 bg-white shadow-sm hover:shadow-md`
- ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค: `cursor-move text-gray-400`
- ๋ชจ๋‹ฌ: `fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50`
- ๋ชจ๋‹ฌ ์ปจํ…์ธ : `bg-white rounded-lg p-6 max-w-2xl w-full max-h-screen overflow-y-auto`

## JavaScript ์š”๊ตฌ์‚ฌํ•ญ

- ๋ชจ๋“  AJAX ์š”์ฒญ์€ `fetch()` API ์‚ฌ์šฉ
- CSRF ํ† ํฐ์€ hidden input์—์„œ ์ฝ์–ด์„œ FormData์— ์ถ”๊ฐ€
- ์—๋Ÿฌ ์ฒ˜๋ฆฌ: `catch()` ๋ธ”๋ก์—์„œ alert ํ‘œ์‹œ
- ์„ฑ๊ณต ๋ฉ”์‹œ์ง€: ํ™”๋ฉด ์šฐ์ธก ์ƒ๋‹จ์— ํ† ์ŠคํŠธ ์•Œ๋ฆผ (์„ ํƒ ์‚ฌํ•ญ)

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- `/admin/builder.php` ์ ‘์† ์‹œ ํŽ˜์ด์ง€ ๋นŒ๋” UI ํ‘œ์‹œ
- ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ์„น์…˜ ํƒ€์ž… ์„ ํƒ ํ›„ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
- ์บ”๋ฒ„์Šค์—์„œ ์„น์…˜ ์นด๋“œ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์ˆœ์„œ ๋ณ€๊ฒฝ
- ํŽธ์ง‘ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ ์˜คํ”ˆ ๋ฐ ์ž…๋ ฅ ํ•„๋“œ ํ‘œ์‹œ
- ์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ™•์ธ ํ›„ ์„น์…˜ ์ œ๊ฑฐ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŽ˜์ด์ง€ ๋นŒ๋” UI๊ฐ€ ๊ตฌ์ถ•๋˜๊ณ , ์‚ฌ์šฉ์ž๋Š” ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ , ์บ”๋ฒ„์Šค์—์„œ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉฐ, ํŽธ์ง‘ ๋ชจ๋‹ฌ์—์„œ ์„ค์ •์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5
Step 5
# ๋‹จ๊ณ„ 5: ์„น์…˜ CRUD API ๊ตฌํ˜„ (๋ฐฑ์—”๋“œ)

## ์ž‘์—… ๋‚ด์šฉ

1. **์„น์…˜ API ํŒŒ์ผ ์ƒ์„ฑ**
   - `api/sections.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ์•ก์…˜ ๋ถ„๊ธฐ:
     - `action=list`: ์„น์…˜ ๋ชฉ๋ก ์กฐํšŒ
     - `action=create`: ์„น์…˜ ์ƒ์„ฑ
     - `action=update`: ์„น์…˜ ์ˆ˜์ •
     - `action=delete`: ์„น์…˜ ์‚ญ์ œ
     - `action=reorder`: ์„น์…˜ ์ˆœ์„œ ๋ณ€๊ฒฝ

2. **์„น์…˜ ๋ชฉ๋ก ์กฐํšŒ API**
   - GET ์š”์ฒญ ์ฒ˜๋ฆฌ
   - ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ์„น์…˜ ๋ชฉ๋ก ์กฐํšŒ:
     ```sql
     SELECT s.*, m.path as image_path, m.thumbnail_path
     FROM sections s
     LEFT JOIN media m ON s.image_id = m.id
     WHERE s.user_id = :user_id
     ORDER BY s.sort_order ASC
     ```
   - JSON ์‘๋‹ต:
     ```json
     {
       "success": true,
       "sections": [
         {
           "id": 1,
           "type": "hero",
           "title": "Welcome",
           "content": "{\"subtitle\":\"My Portfolio\",\"image_id\":5}",
           "image_path": "/uploads/original/hero.jpg",
           "thumbnail_path": "/uploads/thumbnails/hero.jpg",
           "sort_order": 0
         }
       ]
     }
     ```

3. **์„น์…˜ ์ƒ์„ฑ API**
   - POST ์š”์ฒญ ์ฒ˜๋ฆฌ
   - CSRF ํ† ํฐ ๊ฒ€์ฆ
   - ์ž…๋ ฅ ๊ฒ€์ฆ:
     - `type`: hero, about, gallery, contact, testimonial, skills ์ค‘ ํ•˜๋‚˜
     - `title`: ์ตœ๋Œ€ 255์ž
     - `content`: JSON ํ˜•์‹ (์„ ํƒ ์‚ฌํ•ญ)
   - ์ƒˆ ์„น์…˜ ์‚ฝ์ž…:
     ```sql
     INSERT INTO sections (user_id, type, title, content, sort_order)
     VALUES (:user_id, :type, :title, :content, :sort_order)
     ```
   - `sort_order`๋Š” ํ˜„์žฌ ์ตœ๋Œ€๊ฐ’ + 1
   - JSON ์‘๋‹ต:
     ```json
     {
       "success": true,
       "section_id": 123
     }
     ```

4. **์„น์…˜ ์ˆ˜์ • API**
   - POST ์š”์ฒญ ์ฒ˜๋ฆฌ
   - CSRF ํ† ํฐ ๊ฒ€์ฆ
   - ์„น์…˜ ์†Œ์œ ๊ถŒ ํ™•์ธ (user_id ์ผ์น˜ ์—ฌ๋ถ€)
   - ์ž…๋ ฅ ๊ฒ€์ฆ (์ƒ์„ฑ API์™€ ๋™์ผ)
   - ์„น์…˜ ์—…๋ฐ์ดํŠธ:
     ```sql
     UPDATE sections
     SET title = :title, content = :content, image_id = :image_id
     WHERE id = :id AND user_id = :user_id
     ```
   - JSON ์‘๋‹ต:
     ```json
     {
       "success": true
     }
     ```

5. **์„น์…˜ ์‚ญ์ œ API**
   - POST ์š”์ฒญ ์ฒ˜๋ฆฌ
   - CSRF ํ† ํฐ ๊ฒ€์ฆ
   - ์„น์…˜ ์†Œ์œ ๊ถŒ ํ™•์ธ
   - ์„น์…˜ ์‚ญ์ œ:
     ```sql
     DELETE FROM sections
     WHERE id = :id AND user_id = :user_id
     ```
   - ์‚ญ์ œ ํ›„ `sort_order` ์žฌ์ •๋ ฌ (์„ ํƒ ์‚ฌํ•ญ)
   - JSON ์‘๋‹ต:
     ```json
     {
       "success": true
     }
     ```

6. **์„น์…˜ ์ˆœ์„œ ๋ณ€๊ฒฝ API**
   - POST ์š”์ฒญ ์ฒ˜๋ฆฌ
   - CSRF ํ† ํฐ ๊ฒ€์ฆ
   - ์ž…๋ ฅ: `section_ids` ๋ฐฐ์—ด (์ •๋ ฌ๋œ ์„น์…˜ ID ๋ชฉ๋ก)
   - ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์ฒ˜๋ฆฌ:
     ```php
     $pdo->beginTransaction();
     foreach ($section_ids as $index => $id) {
       $stmt = $pdo->prepare("UPDATE sections SET sort_order = :order WHERE id = :id AND user_id = :user_id");
       $stmt->execute(['order' => $index, 'id' => $id, 'user_id' => $user_id]);
     }
     $pdo->commit();
     ```
   - JSON ์‘๋‹ต:
     ```json
     {
       "success": true
     }
     ```

## ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ

- ๋ชจ๋“  POST ์š”์ฒญ์—์„œ CSRF ํ† ํฐ ๊ฒ€์ฆ
- PDO Prepared Statement ์‚ฌ์šฉ (SQL Injection ๋ฐฉ์ง€)
- ์„น์…˜ ์ˆ˜์ •/์‚ญ์ œ ์‹œ ์†Œ์œ ๊ถŒ ํ™•์ธ ํ•„์ˆ˜
- `content` ํ•„๋“œ๋Š” JSON ํ˜•์‹์œผ๋กœ ์ €์žฅ (XSS ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์ถœ๋ ฅ ์‹œ ์ด์Šค์ผ€์ดํ”„)
- ์ž…๋ ฅ ๊ธธ์ด ์ œํ•œ (title: 255์ž, content: 10000์ž)

## ์—๋Ÿฌ ์ฒ˜๋ฆฌ

- ์ธ์ฆ ์‹คํŒจ: `{"success": false, "error": "Unauthorized"}` (HTTP 401)
- CSRF ๊ฒ€์ฆ ์‹คํŒจ: `{"success": false, "error": "Invalid CSRF token"}` (HTTP 403)
- ์ž…๋ ฅ ๊ฒ€์ฆ ์‹คํŒจ: `{"success": false, "error": "Invalid input"}` (HTTP 400)
- ์†Œ์œ ๊ถŒ ํ™•์ธ ์‹คํŒจ: `{"success": false, "error": "Forbidden"}` (HTTP 403)
- ์„œ๋ฒ„ ์˜ค๋ฅ˜: `{"success": false, "error": "Internal server error"}` (HTTP 500)

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- `/api/sections.php?action=list` ํ˜ธ์ถœ ์‹œ ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ์„น์…˜ ๋ชฉ๋ก ๋ฐ˜ํ™˜
- ํŽ˜์ด์ง€ ๋นŒ๋”์—์„œ ์„น์…˜ ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ ์‹œ API ํ˜ธ์ถœ ์„ฑ๊ณต
- ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์ˆœ์„œ ๋ณ€๊ฒฝ ์‹œ `reorder` API ํ˜ธ์ถœ ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—…๋ฐ์ดํŠธ
- ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ ์ ˆํ•œ HTTP ์ƒํƒœ ์ฝ”๋“œ ๋ฐ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์„น์…˜ CRUD API๊ฐ€ ๊ตฌํ˜„๋˜์–ด ํŽ˜์ด์ง€ ๋นŒ๋”์—์„œ ์„น์…˜์„ ์ƒ์„ฑ, ์กฐํšŒ, ์ˆ˜์ •, ์‚ญ์ œ, ์ˆœ์„œ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์š”์ฒญ์€ CSRF ํ† ํฐ ๋ฐ ์†Œ์œ ๊ถŒ ๊ฒ€์ฆ์„ ๊ฑฐ์นฉ๋‹ˆ๋‹ค.

6
Step 6
# ๋‹จ๊ณ„ 6: ํ”„๋ก ํŠธ์—”๋“œ ํฌํŠธํด๋ฆฌ์˜ค ๋ Œ๋”๋ง (๊ณต๊ฐœ ํŽ˜์ด์ง€)

## ์ž‘์—… ๋‚ด์šฉ

1. **ํ”„๋ก ํŠธ์—”๋“œ ๋ฉ”์ธ ํŽ˜์ด์ง€ ์ƒ์„ฑ**
   - `index.php` ํŒŒ์ผ ์ˆ˜์ •
   - URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์‚ฌ์šฉ์ž ์‹๋ณ„: `?user=admin` (๋˜๋Š” user_id)
   - ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์„น์…˜ ๋ชฉ๋ก ์กฐํšŒ (sort_order ์ˆœ)
   - ์„น์…˜ ํƒ€์ž…๋ณ„ ๋ Œ๋”๋ง ํ•จ์ˆ˜ ํ˜ธ์ถœ

2. **์„น์…˜ ํƒ€์ž…๋ณ„ ๋ Œ๋”๋ง ํ•จ์ˆ˜**
   - `includes/render.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ํ•จ์ˆ˜ ๋ชฉ๋ก:
     - `renderHeroSection($section)`: ํžˆ์–ด๋กœ ์„น์…˜ ๋ Œ๋”๋ง
       - ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ํ‘œ์‹œ (`background-image: url(...)`)
       - ์ œ๋ชฉ ๋ฐ ๋ถ€์ œ๋ชฉ ํ‘œ์‹œ
       - CTA ๋ฒ„ํŠผ (์„ ํƒ ์‚ฌํ•ญ)
     - `renderAboutSection($section)`: ์†Œ๊ฐœ ์„น์…˜ ๋ Œ๋”๋ง
       - ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋ฐ ํ…์ŠคํŠธ ํ‘œ์‹œ
       - 2์—ด ๋ ˆ์ด์•„์›ƒ (์ด๋ฏธ์ง€ ์ขŒ์ธก, ํ…์ŠคํŠธ ์šฐ์ธก)
     - `renderGallerySection($section)`: ๊ฐค๋Ÿฌ๋ฆฌ ์„น์…˜ ๋ Œ๋”๋ง
       - ์ด๋ฏธ์ง€ ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ (`grid grid-cols-3 gap-4`)
       - ๊ฐ ์ด๋ฏธ์ง€์— ๋ผ์ดํŠธ๋ฐ•์Šค ๋งํฌ (`data-glightbox` ์†์„ฑ)
     - `renderContactSection($section)`: ์—ฐ๋ฝ์ฒ˜ ์„น์…˜ ๋ Œ๋”๋ง
       - ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, ์†Œ์…œ ๋ฏธ๋””์–ด ๋งํฌ ํ‘œ์‹œ
     - `renderTestimonialSection($section)`: ํ›„๊ธฐ ์„น์…˜ ๋ Œ๋”๋ง
       - ์ธ์šฉ๋ฌธ, ์ž‘์„ฑ์ž๋ช…, ์ž‘์„ฑ์ž ์‚ฌ์ง„ ํ‘œ์‹œ
     - `renderSkillsSection($section)`: ์Šคํ‚ฌ ์„น์…˜ ๋ Œ๋”๋ง
       - ์Šคํ‚ฌ ๋ชฉ๋ก (JSON ํŒŒ์‹ฑ)
       - ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ๋˜๋Š” ํƒœ๊ทธ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ

3. **GLightbox ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ตํ•ฉ**
   - `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css">` CDN ์ถ”๊ฐ€
   - `<script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js"></script>` CDN ์ถ”๊ฐ€
   - JavaScript ์ดˆ๊ธฐํ™”:
     ```javascript
     const lightbox = GLightbox({
       selector: '[data-glightbox]'
     });
     ```
   - ๊ฐค๋Ÿฌ๋ฆฌ ์ด๋ฏธ์ง€์— `data-glightbox` ์†์„ฑ ์ถ”๊ฐ€:
     ```html
     <a href="/uploads/original/image.jpg" data-glightbox>
       <img src="/uploads/thumbnails/image.jpg" alt="Gallery Image">
     </a>
     ```

4. **SEO ๋ฉ”ํƒ€ ํƒœ๊ทธ ์ƒ์„ฑ**
   - `index.php` ์ƒ๋‹จ์— ๋™์  ๋ฉ”ํƒ€ ํƒœ๊ทธ ์‚ฝ์ž…:
     ```html
     <title><?= htmlspecialchars($user['username']) ?>'s Portfolio</title>
     <meta name="description" content="<?= htmlspecialchars($user['bio'] ?? 'Portfolio') ?>">
     <meta property="og:title" content="<?= htmlspecialchars($user['username']) ?>'s Portfolio">
     <meta property="og:image" content="<?= htmlspecialchars($hero_image ?? '/default.jpg') ?>">
     <meta property="og:type" content="website">
     ```

5. **๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ์ง‘๊ณ„**
   - ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ `stats` ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ:
     ```sql
     INSERT INTO stats (user_id, date, views)
     VALUES (:user_id, DATE('now'), 1)
     ON CONFLICT(user_id, date)
     DO UPDATE SET views = views + 1
     ```

6. **๋ฐ˜์‘ํ˜• ๋””์ž์ธ**
   - Tailwind CSS ๋ฐ˜์‘ํ˜• ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค ์‚ฌ์šฉ
   - ๋ชจ๋ฐ”์ผ: ๋‹จ์ผ ์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ
   - ํƒœ๋ธ”๋ฆฟ: 2์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ
   - ๋ฐ์Šคํฌํƒ‘: 3์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ

## ๋””์ž์ธ ๊ฐ€์ด๋“œ

- ํžˆ์–ด๋กœ ์„น์…˜: `min-h-screen flex items-center justify-center text-white`
- ์„น์…˜ ๊ฐ„๊ฒฉ: `py-16`
- ์ œ๋ชฉ: `text-4xl font-bold mb-4`
- ๋ณธ๋ฌธ: `text-lg text-gray-700`
- ๊ฐค๋Ÿฌ๋ฆฌ ์ด๋ฏธ์ง€: `aspect-square object-cover rounded-lg`

## ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ

- ๋ชจ๋“  ์ถœ๋ ฅ์— `htmlspecialchars()` ์ ์šฉ (XSS ๋ฐฉ์ง€)
- JSON ํŒŒ์‹ฑ ์‹œ `json_decode()` ์—๋Ÿฌ ์ฒ˜๋ฆฌ
- ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ๊ฒ€์ฆ (๋””๋ ‰ํ† ๋ฆฌ ํŠธ๋ž˜๋ฒ„์„ค ๋ฐฉ์ง€)

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- `/index.php?user=admin` ์ ‘์† ์‹œ ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ํฌํŠธํด๋ฆฌ์˜ค ํ‘œ์‹œ
- ์„น์…˜ ํƒ€์ž…๋ณ„๋กœ ๋‹ค๋ฅธ ๋ ˆ์ด์•„์›ƒ ๋ Œ๋”๋ง
- ๊ฐค๋Ÿฌ๋ฆฌ ์ด๋ฏธ์ง€ ํด๋ฆญ ์‹œ ๋ผ์ดํŠธ๋ฐ•์Šค ์˜คํ”ˆ
- ๋ฐ˜์‘ํ˜• ๋””์ž์ธ์œผ๋กœ ๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ/๋ฐ์Šคํฌํƒ‘ ๋ชจ๋‘ ์ง€์›
- ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ์ž๋™ ์ง‘๊ณ„

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ”„๋ก ํŠธ์—”๋“œ ํฌํŠธํด๋ฆฌ์˜ค ํŽ˜์ด์ง€๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •ํ•œ ์„น์…˜๋“ค์ด ์ˆœ์„œ๋Œ€๋กœ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. ๊ฐค๋Ÿฌ๋ฆฌ๋Š” ๋ผ์ดํŠธ๋ฐ•์Šค๋กœ ์ด๋ฏธ์ง€๋ฅผ ํ™•๋Œ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ฐ˜์‘ํ˜• ๋””์ž์ธ์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

7
Step 7
# ๋‹จ๊ณ„ 7: ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์ „ํ™˜ & ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ

## ์ž‘์—… ๋‚ด์šฉ

1. **๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์„ค์ • ์ถ”๊ฐ€**
   - `users` ํ…Œ์ด๋ธ”์— `is_public` ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (BOOLEAN, ๊ธฐ๋ณธ๊ฐ’ 1)
   - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ SQL:
     ```sql
     ALTER TABLE users ADD COLUMN is_public INTEGER DEFAULT 1;
     ```
   - `admin/dashboard.php`์— ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€ ๋ฒ„ํŠผ ์ถ”๊ฐ€
   - AJAX๋กœ `/api/settings.php?action=toggle_public` ํ˜ธ์ถœ
   - ๋น„๊ณต๊ฐœ ์‹œ `index.php`์—์„œ "This portfolio is private" ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ

2. **์„ค์ • API ๊ตฌํ˜„**
   - `api/settings.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ์•ก์…˜ ๋ถ„๊ธฐ:
     - `action=toggle_public`: ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€
       - CSRF ํ† ํฐ ๊ฒ€์ฆ
       - `UPDATE users SET is_public = NOT is_public WHERE id = :user_id`
       - JSON ์‘๋‹ต: `{"success": true, "is_public": true/false}`

3. **๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ**
   - `admin/dashboard.php`์— ํ†ต๊ณ„ ์œ„์ ฏ ์ถ”๊ฐ€
   - ํ†ต๊ณ„ ํ•ญ๋ชฉ:
     - ์˜ค๋Š˜ ๋ฐฉ๋ฌธ์ž ์ˆ˜
     - ์ด๋ฒˆ ์ฃผ ๋ฐฉ๋ฌธ์ž ์ˆ˜
     - ์ด ๋ฐฉ๋ฌธ์ž ์ˆ˜
     - ์ตœ๊ทผ 7์ผ ๋ฐฉ๋ฌธ์ž ์ถ”์ด (์ฐจํŠธ)
   - SQL ์ฟผ๋ฆฌ:
     ```sql
     -- ์˜ค๋Š˜ ๋ฐฉ๋ฌธ์ž
     SELECT views FROM stats WHERE user_id = :user_id AND date = DATE('now')

     -- ์ด๋ฒˆ ์ฃผ ๋ฐฉ๋ฌธ์ž
     SELECT SUM(views) FROM stats
     WHERE user_id = :user_id AND date >= DATE('now', '-7 days')

     -- ์ด ๋ฐฉ๋ฌธ์ž
     SELECT SUM(views) FROM stats WHERE user_id = :user_id

     -- ์ตœ๊ทผ 7์ผ ์ถ”์ด
     SELECT date, views FROM stats
     WHERE user_id = :user_id AND date >= DATE('now', '-7 days')
     ORDER BY date ASC
     ```

4. **์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ตํ•ฉ (์„ ํƒ ์‚ฌํ•ญ)**
   - Chart.js CDN ์ถ”๊ฐ€: `<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>`
   - ๋ผ์ธ ์ฐจํŠธ๋กœ ๋ฐฉ๋ฌธ์ž ์ถ”์ด ์‹œ๊ฐํ™”:
     ```javascript
     const ctx = document.getElementById('visitsChart').getContext('2d');
     const chart = new Chart(ctx, {
       type: 'line',
       data: {
         labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
         datasets: [{
           label: 'Visits',
           data: [12, 19, 3, 5, 2, 3, 7],
           borderColor: 'rgb(59, 130, 246)',
           backgroundColor: 'rgba(59, 130, 246, 0.1)'
         }]
       }
     });
     ```

5. **๋Œ€์‹œ๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ฐœ์„ **
   - `admin/dashboard.php` ์ƒ๋‹จ์— ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด ์ถ”๊ฐ€:
     - Dashboard (ํ˜„์žฌ ํŽ˜์ด์ง€)
     - Page Builder (`/admin/builder.php`)
     - Media Library (`/admin/media.php`)
     - Logout (`/api/auth.php?action=logout`)
   - ํ˜„์žฌ ํŽ˜์ด์ง€๋Š” ํ•˜์ด๋ผ์ดํŠธ ํ‘œ์‹œ

## ๋””์ž์ธ ๊ฐ€์ด๋“œ

- ํ†ต๊ณ„ ์œ„์ ฏ: `grid grid-cols-3 gap-4 mb-8`
- ๊ฐ ์œ„์ ฏ: `bg-white rounded-lg shadow p-6`
- ์ˆซ์ž: `text-3xl font-bold text-blue-600`
- ๋ผ๋ฒจ: `text-sm text-gray-600`
- ์ฐจํŠธ: `bg-white rounded-lg shadow p-6 mt-8`

## ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ

- ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€์€ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ๊ฐ€๋Šฅ
- CSRF ํ† ํฐ ๊ฒ€์ฆ ํ•„์ˆ˜
- ๋น„๊ณต๊ฐœ ํฌํŠธํด๋ฆฌ์˜ค๋Š” ์†Œ์œ ์ž๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ์„ธ์…˜ ์ฒดํฌ

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- `/admin/dashboard.php` ์ ‘์† ์‹œ ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ์œ„์ ฏ ํ‘œ์‹œ
- ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์„ค์ • ๋ณ€๊ฒฝ
- ๋น„๊ณต๊ฐœ ์„ค์ • ์‹œ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ "Private" ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
- ์ฐจํŠธ๋กœ ์ตœ๊ทผ 7์ผ ๋ฐฉ๋ฌธ์ž ์ถ”์ด ์‹œ๊ฐํ™”

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ์— ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ์œ„์ ฏ์ด ์ถ”๊ฐ€๋˜๊ณ , ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€ ๊ธฐ๋Šฅ์ด ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. ๋น„๊ณต๊ฐœ ์„ค์ • ์‹œ ํฌํŠธํด๋ฆฌ์˜ค ํŽ˜์ด์ง€๊ฐ€ ์ˆจ๊ฒจ์ง‘๋‹ˆ๋‹ค.

8
Step 8
# ๋‹จ๊ณ„ 8: ์„น์…˜๋ณ„ ์ปค์Šคํ…€ CSS & JSON ์„ค์ • ๊ณ ๋„ํ™”

## ์ž‘์—… ๋‚ด์šฉ

1. **์„น์…˜ ํ…Œ์ด๋ธ”์— ์ปค์Šคํ…€ CSS ์ปฌ๋Ÿผ ์ถ”๊ฐ€**
   - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ SQL:
     ```sql
     ALTER TABLE sections ADD COLUMN custom_css TEXT;
     ```
   - ํŽธ์ง‘ ๋ชจ๋‹ฌ์— "Custom CSS" textarea ์ถ”๊ฐ€
   - ์ž…๋ ฅ ์˜ˆ์‹œ:
     ```css
     .section-123 {
       background-color: #f3f4f6;
       padding: 4rem 0;
     }
     ```

2. **์„น์…˜๋ณ„ CSS ํด๋ž˜์Šค ์ž๋™ ์ƒ์„ฑ**
   - ๊ฐ ์„น์…˜์— ๊ณ ์œ  ํด๋ž˜์Šค ๋ถ€์—ฌ: `section-{id}`
   - `index.php`์—์„œ ์„น์…˜ ๋ Œ๋”๋ง ์‹œ:
     ```html
     <section class="section-<?= $section['id'] ?> <?= htmlspecialchars($section['custom_css_class'] ?? '') ?>">
       <!-- ์„น์…˜ ๋‚ด์šฉ -->
     </section>
     ```

3. **์ปค์Šคํ…€ CSS ์ธ๋ผ์ธ ์‚ฝ์ž…**
   - `index.php` `<head>` ํƒœ๊ทธ์— ๋™์  `<style>` ํƒœ๊ทธ ์ถ”๊ฐ€:
     ```php
     <style>
     <?php foreach ($sections as $section): ?>
       <?php if (!empty($section['custom_css'])): ?>
         <?= $section['custom_css'] ?>
       <?php endif; ?>
     <?php endforeach; ?>
     </style>
     ```

4. **์„น์…˜ ์„ค์ • JSON ๊ตฌ์กฐ ์ •์˜**
   - `content` ์ปฌ๋Ÿผ์— ์ €์žฅ๋˜๋Š” JSON ํ˜•์‹ ํ‘œ์ค€ํ™”:
     - Hero:
       ```json
       {
         "subtitle": "My Creative Portfolio",
         "image_id": 5,
         "cta_text": "View My Work",
         "cta_link": "#gallery"
       }
       ```
     - About:
       ```json
       {
         "bio": "I'm a freelance designer...",
         "image_id": 3,
         "skills": ["Photoshop", "Illustrator", "Figma"]
       }
       ```
     - Gallery:
       ```json
       {
         "image_ids": [1, 2, 3, 4, 5, 6],
         "layout": "grid",
         "columns": 3
       }
       ```
     - Contact:
       ```json
       {
         "email": "hello@example.com",
         "phone": "+1234567890",
         "social": {
           "instagram": "@username",
           "twitter": "@username"
         }
       }
       ```
     - Testimonial:
       ```json
       {
         "quote": "Amazing work!",
         "author": "Jane Doe",
         "author_image_id": 7,
         "rating": 5
       }
       ```
     - Skills:
       ```json
       {
         "skills": [
           {"name": "PHP", "level": 90},
           {"name": "JavaScript", "level": 85}
         ]
       }
       ```

5. **ํŽธ์ง‘ ๋ชจ๋‹ฌ ์ž…๋ ฅ ํ•„๋“œ ๊ฐœ์„ **
   - JSON ๊ตฌ์กฐ์— ๋งž์ถฐ ์ž…๋ ฅ ํ•„๋“œ ๋™์  ์ƒ์„ฑ
   - ์˜ˆ: Gallery ์„น์…˜ ํŽธ์ง‘ ์‹œ:
     - ์ด๋ฏธ์ง€ ์„ ํƒ ๋ฒ„ํŠผ (๋‹ค์ค‘ ์„ ํƒ)
     - ๋ ˆ์ด์•„์›ƒ ์„ ํƒ (Grid / Masonry)
     - ์ปฌ๋Ÿผ ์ˆ˜ ์„ ํƒ (2 / 3 / 4)
   - ์ €์žฅ ์‹œ JavaScript๋กœ JSON ๊ฐ์ฒด ์ƒ์„ฑ ํ›„ ์„œ๋ฒ„ ์ „์†ก

6. **ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ํ•จ์ˆ˜ ์—…๋ฐ์ดํŠธ**
   - `includes/render.php`์˜ ๊ฐ ํ•จ์ˆ˜์—์„œ JSON ํŒŒ์‹ฑ:
     ```php
     function renderGallerySection($section) {
       $config = json_decode($section['content'], true);
       $image_ids = $config['image_ids'] ?? [];
       $columns = $config['columns'] ?? 3;
       // ์ด๋ฏธ์ง€ ์กฐํšŒ ๋ฐ ๊ทธ๋ฆฌ๋“œ ๋ Œ๋”๋ง
     }
     ```

## ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ

- ์ปค์Šคํ…€ CSS๋Š” XSS ์œ„ํ—˜์ด ์žˆ์œผ๋ฏ€๋กœ ๊ด€๋ฆฌ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ
- CSS ์ž…๋ ฅ ์‹œ `<script>` ํƒœ๊ทธ ํ•„ํ„ฐ๋ง (์„ ํƒ ์‚ฌํ•ญ)
- JSON ํŒŒ์‹ฑ ์‹œ `json_decode()` ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•„์ˆ˜

## ๋””์ž์ธ ๊ฐ€์ด๋“œ

- ํŽธ์ง‘ ๋ชจ๋‹ฌ์˜ "Custom CSS" textarea๋Š” `font-mono` ์Šคํƒ€์ผ
- JSON ์ž…๋ ฅ ํ•„๋“œ๋Š” ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ์„ธ๋กœ๋กœ ๋ฐฐ์น˜

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- ์„น์…˜ ํŽธ์ง‘ ๋ชจ๋‹ฌ์—์„œ "Custom CSS" ์ž…๋ ฅ ๊ฐ€๋Šฅ
- ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๊ฐ ์„น์…˜์— ์ปค์Šคํ…€ CSS ์ ์šฉ
- JSON ์„ค์ •์— ๋”ฐ๋ผ ์„น์…˜ ๋ Œ๋”๋ง ๋™์  ๋ณ€๊ฒฝ
- Gallery ์„น์…˜์—์„œ ์ด๋ฏธ์ง€ ๋‹ค์ค‘ ์„ ํƒ ๋ฐ ๋ ˆ์ด์•„์›ƒ ์„ค์ • ๊ฐ€๋Šฅ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์„น์…˜๋ณ„๋กœ ์ปค์Šคํ…€ CSS๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๊ณ , JSON ์„ค์ •์„ ํ†ตํ•ด ์„น์…˜์˜ ์„ธ๋ถ€ ์˜ต์…˜์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง์ด JSON ์„ค์ •์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.

9
Step 9
# ๋‹จ๊ณ„ 9: ์ด๋ฏธ์ง€ ์บ์‹ฑ & ์„ฑ๋Šฅ ์ตœ์ ํ™”

## ์ž‘์—… ๋‚ด์šฉ

1. **์ด๋ฏธ์ง€ ์บ์‹ฑ ํ—ค๋” ์ถ”๊ฐ€**
   - `uploads/.htaccess` ํŒŒ์ผ ์ƒ์„ฑ (Apache ํ™˜๊ฒฝ)
   - ์บ์‹ฑ ๊ทœ์น™ ์„ค์ •:
     ```apache
     <IfModule mod_expires.c>
       ExpiresActive On
       ExpiresByType image/jpeg "access plus 1 year"
       ExpiresByType image/png "access plus 1 year"
       ExpiresByType image/webp "access plus 1 year"
     </IfModule>

     <IfModule mod_headers.c>
       Header set Cache-Control "public, max-age=31536000"
     </IfModule>
     ```
   - PHP ๋‚ด์žฅ ์„œ๋ฒ„ ์‚ฌ์šฉ ์‹œ `index.php`์—์„œ ์ˆ˜๋™ ํ—ค๋” ์ถ”๊ฐ€:
     ```php
     if (preg_match('/\.(jpg|jpeg|png|webp)$/', $_SERVER['REQUEST_URI'])) {
       header('Cache-Control: public, max-age=31536000');
       header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
     }
     ```

2. **์ด๋ฏธ์ง€ Lazy Loading**
   - ํ”„๋ก ํŠธ์—”๋“œ `index.php`์˜ ๋ชจ๋“  `<img>` ํƒœ๊ทธ์— `loading="lazy"` ์†์„ฑ ์ถ”๊ฐ€:
     ```html
     <img src="/uploads/thumbnails/image.jpg" alt="Gallery" loading="lazy">
     ```

3. **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™”**
   - ์„น์…˜ ๋ชฉ๋ก ์กฐํšŒ ์‹œ LEFT JOIN์œผ๋กœ ๋ฏธ๋””์–ด ์ •๋ณด ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๊ธฐ:
     ```sql
     SELECT s.*, m.path, m.thumbnail_path
     FROM sections s
     LEFT JOIN media m ON s.image_id = m.id
     WHERE s.user_id = :user_id
     ORDER BY s.sort_order ASC
     ```
   - N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ ํ•ด๊ฒฐ

4. **Gallery ์„น์…˜ ์ด๋ฏธ์ง€ ์ผ๊ด„ ์กฐํšŒ**
   - Gallery ์„น์…˜์˜ `image_ids` ๋ฐฐ์—ด์„ ํŒŒ์‹ฑํ•˜์—ฌ IN ์ฟผ๋ฆฌ๋กœ ์ผ๊ด„ ์กฐํšŒ:
     ```php
     $image_ids = json_decode($section['content'], true)['image_ids'] ?? [];
     if (!empty($image_ids)) {
       $placeholders = implode(',', array_fill(0, count($image_ids), '?'));
       $stmt = $pdo->prepare("SELECT * FROM media WHERE id IN ($placeholders)");
       $stmt->execute($image_ids);
       $images = $stmt->fetchAll();
     }
     ```

5. **CSS/JS ํŒŒ์ผ ์ตœ์†Œํ™” (์„ ํƒ ์‚ฌํ•ญ)**
   - `assets/css/style.css` ํŒŒ์ผ ์ƒ์„ฑ (Tailwind ์ปค์Šคํ…€ ์Šคํƒ€์ผ)
   - `assets/js/main.js` ํŒŒ์ผ ์ƒ์„ฑ (๊ณตํ†ต JavaScript)
   - HTML์—์„œ ์ธ๋ผ์ธ ์Šคํƒ€์ผ/์Šคํฌ๋ฆฝํŠธ๋ฅผ ์™ธ๋ถ€ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ

6. **Gzip ์••์ถ• ํ™œ์„ฑํ™”**
   - `.htaccess`์— Gzip ๊ทœ์น™ ์ถ”๊ฐ€:
     ```apache
     <IfModule mod_deflate.c>
       AddOutputFilterByType DEFLATE text/html text/plain text/css application/json application/javascript
     </IfModule>
     ```

7. **์„น์…˜ ๋ Œ๋”๋ง ์บ์‹ฑ (๊ณ ๊ธ‰, ์„ ํƒ ์‚ฌํ•ญ)**
   - `includes/cache.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ํ•จ์ˆ˜: `getCachedHTML($key, $callback, $ttl = 3600)`
   - ์„น์…˜ HTML์„ ํŒŒ์ผ ์บ์‹œ์— ์ €์žฅ (`cache/{user_id}_{section_id}.html`)
   - TTL ๋งŒ๋ฃŒ ์‹œ ์žฌ์ƒ์„ฑ

## ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

- ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ Network ํƒญ์—์„œ ์ด๋ฏธ์ง€ ์บ์‹ฑ ํ™•์ธ
- Lighthouse ์Šค์ฝ”์–ด ์ธก์ • (Performance 90+ ๋ชฉํ‘œ)

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- ์ด๋ฏธ์ง€ ์žฌ๋ฐฉ๋ฌธ ์‹œ ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ์—์„œ ๋กœ๋“œ (304 Not Modified)
- ํŽ˜์ด์ง€ ๋กœ๋“œ ์†๋„ ๊ฐœ์„  (First Contentful Paint 1์ดˆ ์ด๋‚ด)
- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ํšŸ์ˆ˜ ๊ฐ์†Œ
- Lazy Loading์œผ๋กœ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ๊ฐ„ ๋‹จ์ถ•

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ด๋ฏธ์ง€ ์บ์‹ฑ ํ—ค๋”๊ฐ€ ์ถ”๊ฐ€๋˜๊ณ , Lazy Loading์ด ์ ์šฉ๋˜์–ด ํŽ˜์ด์ง€ ๋กœ๋“œ ์†๋„๊ฐ€ ํฌ๊ฒŒ ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๋„ ์ตœ์ ํ™”๋˜์–ด N+1 ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

10
Step 10
# ๋‹จ๊ณ„ 10: ํ…Œ์ŠคํŠธ & ๋ณด์•ˆ ๊ฐ•ํ™” & ๋ฐฐํฌ ์ค€๋น„

## ์ž‘์—… ๋‚ด์šฉ

1. **๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ**
   - [ ] ๋ชจ๋“  POST ์š”์ฒญ์—์„œ CSRF ํ† ํฐ ๊ฒ€์ฆ
   - [ ] PDO Prepared Statement ์‚ฌ์šฉ (SQL Injection ๋ฐฉ์ง€)
   - [ ] `htmlspecialchars()` ์ถœ๋ ฅ ์ด์Šค์ผ€์ดํ”„ (XSS ๋ฐฉ์ง€)
   - [ ] ํŒŒ์ผ ์—…๋กœ๋“œ MIME ํƒ€์ž… ๊ฒ€์ฆ
   - [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ ์ €์žฅ (`password_hash()`)
   - [ ] ์„ธ์…˜ ํ•˜์ด์žฌํ‚น ๋ฐฉ์ง€ (`session_regenerate_id()`)
   - [ ] ๋””๋ ‰ํ† ๋ฆฌ ํŠธ๋ž˜๋ฒ„์„ค ๋ฐฉ์ง€ (ํŒŒ์ผ ๊ฒฝ๋กœ ๊ฒ€์ฆ)
   - [ ] Rate Limiting (์„ ํƒ ์‚ฌํ•ญ, ๋กœ๊ทธ์ธ ์‹œ๋„ ์ œํ•œ)

2. **์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐœ์„ **
   - `includes/error.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ํ•จ์ˆ˜: `handleException($e)`, `handleError($errno, $errstr, $errfile, $errline)`
   - ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ƒ์„ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
   - ๋กœ๊ทธ ํŒŒ์ผ์— ์—๋Ÿฌ ๊ธฐ๋ก (`logs/error.log`)

3. **ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •**
   - `.env.example` ํŒŒ์ผ ์ƒ์„ฑ:
     ```
     DB_PATH=database.sqlite
     UPLOAD_DIR=uploads
     MAX_UPLOAD_SIZE=5242880
     SESSION_LIFETIME=7200
     ```
   - `includes/config.php` ํŒŒ์ผ ์ƒ์„ฑ
   - `.env` ํŒŒ์ผ ํŒŒ์‹ฑ ํ•จ์ˆ˜ ์ž‘์„ฑ
   - `.gitignore`์— `.env` ์ถ”๊ฐ€

4. **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… ์Šคํฌ๋ฆฝํŠธ**
   - `backup.php` ํŒŒ์ผ ์ƒ์„ฑ
   - SQLite ํŒŒ์ผ์„ `backups/` ํด๋”์— ๋ณต์‚ฌ
   - ํŒŒ์ผ๋ช…: `database_{timestamp}.sqlite`
   - ํฌ๋ก ์žก์œผ๋กœ ์ž๋™ ๋ฐฑ์—… ์„ค์ • ๊ฐ€๋Šฅ

5. **ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ**
   - ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค:
     1. ํšŒ์›๊ฐ€์ž… โ†’ ๋กœ๊ทธ์ธ โ†’ ์„น์…˜ ์ถ”๊ฐ€ โ†’ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ โ†’ ์„น์…˜ ํŽธ์ง‘ โ†’ ๊ณต๊ฐœ ํฌํŠธํด๋ฆฌ์˜ค ํ™•์ธ
     2. ๋น„๊ณต๊ฐœ ์ „ํ™˜ โ†’ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ "Private" ๋ฉ”์‹œ์ง€ ํ™•์ธ
     3. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์„น์…˜ ์ˆœ์„œ ๋ณ€๊ฒฝ โ†’ ํ”„๋ก ํŠธ์—”๋“œ ๋ฐ˜์˜ ํ™•์ธ
     4. ๋ฏธ๋””์–ด ์‚ญ์ œ โ†’ ์„น์…˜์—์„œ ์ด๋ฏธ์ง€ ์ œ๊ฑฐ ํ™•์ธ
     5. ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„ ์ง‘๊ณ„ ํ™•์ธ
   - ๊ฐ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ˆ˜๋™์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๊ฒฐ๊ณผ ๊ธฐ๋ก

6. **README.md ์ž‘์„ฑ**
   - ํ”„๋กœ์ ํŠธ ์„ค๋ช…
   - ์„ค์น˜ ๋ฐฉ๋ฒ•:
     ```bash
     git clone https://github.com/yourusername/portfolio-cms-drag-drop.git
     cd portfolio-cms-drag-drop
     php setup.php
     php -S localhost:8000
     ```
   - ๊ธฐ๋ณธ ๊ณ„์ • ์ •๋ณด (admin@example.com / password123)
   - ์ฃผ์š” ๊ธฐ๋Šฅ ๋ชฉ๋ก
   - ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ ์‚ฌํ•ญ)
   - ๋ผ์ด์„ ์Šค ์ •๋ณด

7. **๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ**
   - [ ] `.env` ํŒŒ์ผ ์„ค์ • (ํ”„๋กœ๋•์…˜ ๊ฐ’)
   - [ ] ์—๋Ÿฌ ๋กœ๊น… ํ™œ์„ฑํ™”
   - [ ] ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™”
   - [ ] HTTPS ์„ค์ • (Let's Encrypt)
   - [ ] ํŒŒ์ผ ๊ถŒํ•œ ์„ค์ • (`uploads/` ํด๋” ์“ฐ๊ธฐ ๊ถŒํ•œ)
   - [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… ํฌ๋ก ์žก ์„ค์ •

8. **์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง (์„ ํƒ ์‚ฌํ•ญ)**
   - `includes/monitor.php` ํŒŒ์ผ ์ƒ์„ฑ
   - ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ๊ฐ„ ์ธก์ •
   - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ํšŸ์ˆ˜ ์นด์šดํŠธ
   - ๊ฒฐ๊ณผ๋ฅผ `logs/performance.log`์— ๊ธฐ๋ก

## ์ตœ์ข… ์ ๊ฒ€

- PHP 8.3+ ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ ํ™•์ธ
- ๋ชจ๋“  ๊ธฐ๋Šฅ ์ •์ƒ ์ž‘๋™ ํ™•์ธ
- ๋ณด์•ˆ ์ทจ์•ฝ์  ์ ๊ฒ€
- ์ฝ”๋“œ ์ฃผ์„ ์ถ”๊ฐ€ (์ค‘์š” ํ•จ์ˆ˜์— PHPDoc)

## ์˜ˆ์ƒ ๊ฒฐ๊ณผ

- ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ชจ๋‘ ํ†ต๊ณผ
- ์—๋Ÿฌ ํ•ธ๋“ค๋ง์œผ๋กœ ์•ˆ์ •์ ์ธ ์„œ๋น„์Šค ์ œ๊ณต
- ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์„ค์ • ๊ด€๋ฆฌ
- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… ์Šคํฌ๋ฆฝํŠธ ์ž‘๋™
- README.md๋กœ ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ ์ œ๊ณต
- ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ๋ฐฐํฌ ์ค€๋น„ ์™„๋ฃŒ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๊ณ , ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •์ด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , README.md๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ๋ฐฐํฌ ์ค€๋น„๋ฅผ ๋งˆ์นฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (1)

๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Glitter Gim

์š”๊ตฌ์‚ฌํ•ญ, ๊ตฌํ˜„ ์ˆœ์„œ, ์ œ์•ฝ ์กฐ๊ฑด์ด ์ž˜ ์ •๋ฆฌ๋˜์–ด ์žˆ์–ด AI๊ฐ€ ๋‹จ๊ณ„์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ ์ข‹์€ ํ”„๋กœ์ ํŠธ ๋ ˆ์‹œํ”ผ๋„ค์š”. ์ €๋„ ์ด ๋ ˆ์‹œํ”ผ๋Œ€๋กœ ํ•œ๋ฒˆ ๊ตฌํ˜„ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.