๐จ ์จ๋ผ์ธ ํฌํธํด๋ฆฌ์ค CMS - ๋๋๊ทธ ์ค ๋๋กญ ํ์ด์ง ๋น๋ & ๋ฏธ๋์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ
portfolio-cms-drag-drop
๋ฌธ์ ์ค๋ช
ํ๋ก์ ํธ ๊ฐ์
ํ๋ฆฌ๋์ ๋์์ด๋, ์ฌ์ง์๊ฐ, ์ผ๋ฌ์คํธ๋ ์ดํฐ ๋ฑ ํฌ๋ฆฌ์์ดํฐ๋ฅผ ์ํ ์ผ๋ฐ PHP ๊ธฐ๋ฐ ํฌํธํด๋ฆฌ์ค CMS๋ฅผ ๋ง๋ญ๋๋ค. ์ฌ์ฉ์๋ ๊ด๋ฆฌ์ ์์ญ์์ ๋๋๊ทธ ์ค ๋๋กญ์ผ๋ก ์น์ ์ ๋ฐฐ์นํ๊ณ , ๋ฏธ๋์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํ์ฌ ์์ ๋ง์ ํฌํธํด๋ฆฌ์ค ์ฌ์ดํธ๋ฅผ ๊ตฌ์ถํ ์ ์์ต๋๋ค. ํ๋ก ํธ์๋๋ ์ค์๊ฐ์ผ๋ก ๋ณ๊ฒฝ์ฌํญ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐํ๋ฉฐ, ๋ฐฉ๋ฌธ์๋ ์์ฑ๋ ํฌํธํด๋ฆฌ์ค๋ฅผ ํ์ํ ์ ์์ต๋๋ค.
ํ์ ๊ธฐ๋ฅ
-
์ฌ์ฉ์ ์ธ์ฆ & ๊ถํ
- ํ์๊ฐ์
/๋ก๊ทธ์ธ (๋น๋ฐ๋ฒํธ ํด์:
password_hash/password_verify) - ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ (CSRF ํ ํฐ ๊ฒ์ฆ)
- ๊ด๋ฆฌ์ ์ ์ฉ ์์ญ ์ ๊ทผ ์ ์ด
- ํ์๊ฐ์
/๋ก๊ทธ์ธ (๋น๋ฐ๋ฒํธ ํด์:
-
๋๋๊ทธ ์ค ๋๋กญ ํ์ด์ง ๋น๋
- ์น์ ํ์ : Hero, About, Gallery, Contact, Testimonial, Skills
- Sortable.js๋ฅผ ํ์ฉํ ์น์ ์์ ๋ณ๊ฒฝ
- ๊ฐ ์น์ ๋ณ ์ค์ ๋ชจ๋ฌ (์ ๋ชฉ, ์ค๋ช , ์ด๋ฏธ์ง ์ ํ ๋ฑ)
- ์น์ ์ถ๊ฐ/์ญ์ /์์ (AJAX ์ ์ฅ)
-
๋ฏธ๋์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ์ด๋ฏธ์ง ์ ๋ก๋ (JPEG, PNG, WebP ์ง์, ์ต๋ 5MB)
- ์ธ๋ค์ผ ์๋ ์์ฑ (GD ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
- ๊ทธ๋ฆฌ๋ ๋ทฐ๋ก ๋ฏธ๋์ด ํ์
- ์ด๋ฏธ์ง ์ ํ ๋ชจ๋ฌ (ํ์ด์ง ๋น๋์์ ์ฌ์ฉ)
-
ํ๋ก ํธ์๋ ํฌํธํด๋ฆฌ์ค ๋ ๋๋ง
- SEO ์นํ์ HTML ๊ตฌ์กฐ
- ๋ฐ์ํ ๋์์ธ (๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํ)
- ๋ผ์ดํธ๋ฐ์ค ๊ฐค๋ฌ๋ฆฌ (์ด๋ฏธ์ง ํ๋ ๋ณด๊ธฐ)
- ์คํ ๊ทธ๋ํ ๋ฉํ ํ๊ทธ ์๋ ์์ฑ
-
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ (SQLite + PDO)
usersํ ์ด๋ธ: id, username, email, password, created_atsectionsํ ์ด๋ธ: id, user_id, type, title, content, image_id, sort_order, created_atmediaํ ์ด๋ธ: id, user_id, filename, path, thumbnail_path, size, mime_type, created_at- ์ธ๋ ํค ์ ์ฝ ์กฐ๊ฑด ๋ฐ ์ธ๋ฑ์ค ์ต์ ํ
-
๋ณด์ & ์ฑ๋ฅ
- Prepared Statement๋ก SQL Injection ๋ฐฉ์ด
htmlspecialchars()๋ก XSS ๋ฐฉ์ด- CSRF ํ ํฐ ๊ฒ์ฆ (๋ชจ๋ POST ์์ฒญ)
- ํ์ผ ์ ๋ก๋ ๊ฒ์ฆ (MIME ํ์ , ํ์ฅ์, ํฌ๊ธฐ)
- ์ด๋ฏธ์ง ์บ์ฑ (Last-Modified ํค๋)
-
๊ณ ๊ธ ๊ธฐ๋ฅ
- ํฌํธํด๋ฆฌ์ค ๊ณต๊ฐ/๋น๊ณต๊ฐ ์ ํ
- ์น์ ๋ณ ์ปค์คํ 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์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
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: ๋ก๊ทธ์ธ ์์คํ
๊ตฌ์ถ (ํ๋ก ํธ์๋ + 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: ๋ฏธ๋์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ตฌ์ถ (์
๋ก๋ + ์ธ๋ค์ผ ์์ฑ)
## ์์
๋ด์ฉ
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: ๋๋๊ทธ ์ค ๋๋กญ ํ์ด์ง ๋น๋ 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: ์น์
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: ํ๋ก ํธ์๋ ํฌํธํด๋ฆฌ์ค ๋ ๋๋ง (๊ณต๊ฐ ํ์ด์ง)
## ์์
๋ด์ฉ
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: ๊ณต๊ฐ/๋น๊ณต๊ฐ ์ ํ & ๋ฐฉ๋ฌธ์ ํต๊ณ ๋์๋ณด๋
## ์์
๋ด์ฉ
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: ์น์
๋ณ ์ปค์คํ
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: ์ด๋ฏธ์ง ์บ์ฑ & ์ฑ๋ฅ ์ต์ ํ
## ์์
๋ด์ฉ
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: ํ
์คํธ & ๋ณด์ ๊ฐํ & ๋ฐฐํฌ ์ค๋น
## ์์
๋ด์ฉ
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)
์๊ตฌ์ฌํญ, ๊ตฌํ ์์, ์ ์ฝ ์กฐ๊ฑด์ด ์ ์ ๋ฆฌ๋์ด ์์ด AI๊ฐ ๋จ๊ณ์ ์ผ๋ก ๊ตฌํํ๊ธฐ ์ข์ ํ๋ก์ ํธ ๋ ์ํผ๋ค์. ์ ๋ ์ด ๋ ์ํผ๋๋ก ํ๋ฒ ๊ตฌํํด ๋ณด๊ฒ ์ต๋๋ค.