๐ ์จ๋ผ์ธ ์ํฌ์ ์์ฝ & ์ถ์ ๊ด๋ฆฌ ํ๋ซํผ - ์ค์๊ฐ ์ข์ ํํฉ & QR ์ฒดํฌ์ธ
workshop-booking-attendance-platform
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
์ค์ ๊ต์ก ๊ธฐ๊ด์ด๋ ํ๋ฆฌ๋์ ๊ฐ์ฌ๊ฐ ์ฌ์ฉํ ์ ์๋ ์ํฌ์ ์์ฝ ๋ฐ ์ถ์ ๊ด๋ฆฌ ํตํฉ ํ๋ซํผ์ ๋ง๋ญ๋๋ค. ์ฌ์ฉ์(์๊ฐ์)๋ ์ํฌ์์ ๊ฒ์ํ๊ณ ์์ฝํ๋ฉฐ, ๊ด๋ฆฌ์(๊ฐ์ฌ)๋ ์ํฌ์์ ์์ฑํ๊ณ ์ฐธ๊ฐ์ ์ถ์์ QR ์ฝ๋๋ก ํ์ธํฉ๋๋ค. ์ผ๋ฐ PHP 8.3+ ํ๊ฒฝ์์ PDO + SQLite๋ก ๊ตฌํํ๋ฉฐ, ์ค์๊ฐ ์ข์ ํํฉ, QR ์ฝ๋ ์์ฑ, ์์ฝ ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก, ์ถ์ ํต๊ณ ๋ฑ ์ค๋ฌด ์์ค์ ๊ธฐ๋ฅ์ ํฌํจํฉ๋๋ค.
โ ํ์ ๊ธฐ๋ฅ
- ์ฌ์ฉ์ ๊ธฐ๋ฅ
- ํ์๊ฐ์ /๋ก๊ทธ์ธ (์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ)
- ์ํฌ์ ๋ชฉ๋ก ๊ฒ์ (์นดํ ๊ณ ๋ฆฌ, ๋ ์ง, ํค์๋ ํํฐ๋ง)
- ์ํฌ์ ์์ธ ์ ๋ณด ์กฐํ (๊ฐ์ฌ, ์ผ์, ์ฅ์, ์ ์, ์์ฌ์)
- ์ค์๊ฐ ์ข์ ํํฉ ํ์ธ ๋ฐ ์์ฝ ์ ์ฒญ
- ์์ฝ ๋ด์ญ ์กฐํ ๋ฐ ์ทจ์
- ์์ฝ ํ์ธ QR ์ฝ๋ ์๋ ์์ฑ (์์ฝ ID ๊ธฐ๋ฐ)
- ์์ฝ ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก (QR ์ฝ๋ ์ด๋ฏธ์ง ์ฒจ๋ถ)
- ๊ด๋ฆฌ์ ๊ธฐ๋ฅ
- ์ํฌ์ ์์ฑ/์์ /์ญ์ (์ ๋ชฉ, ์ค๋ช , ์นดํ ๊ณ ๋ฆฌ, ์ผ์, ์ฅ์, ์ ์, ๊ฐ๊ฒฉ)
- ์์ฝ์ ๋ชฉ๋ก ์กฐํ (์ํฌ์๋ณ)
- QR ์ฝ๋ ์ค์บ ๊ธฐ๋ฅ (์นด๋ฉ๋ผ ๋๋ ํ์ผ ์ ๋ก๋)
- ์ถ์ ์ฒดํฌ (QR ์ฝ๋ ๊ฒ์ฆ ํ ์ถ์ ์ํ ์ ๋ฐ์ดํธ)
- ์ํฌ์๋ณ ์ถ์ ํต๊ณ (์ถ์๋ฅ , ๊ฒฐ์์ ๋ฆฌ์คํธ)
- ์์ฝ ๋๊ธฐ์ด ๊ด๋ฆฌ (์ ์ ์ด๊ณผ ์ ์๋ ๋๊ธฐ์ด)
- ๊ณตํต ๊ธฐ๋ฅ
- ๋ฐ์ํ UI (๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํฑ)
- CSRF ํ ํฐ ๊ธฐ๋ฐ ํผ ๋ณดํธ
- XSS ๋ฐฉ์ง (htmlspecialchars ์ถ๋ ฅ ์ด์ค์ผ์ดํ)
- SQL Injection ๋ฐฉ์ง (PDO prepared statements)
- ํ์ผ ๊ธฐ๋ฐ ์ธ์ ์บ์ฑ (์ํฌ์ ๋ชฉ๋ก 10๋ถ ์บ์)
- ์๋ฌ ๋ก๊น (error.log ํ์ผ)
๐ ์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ์ํฌ์ ๋ฆฌ๋ทฐ ๋ฐ ๋ณ์ ์์คํ
- ์์ฝ ์ ๊ฒฐ์ ์ฐ๋ (๊ฐ์ ๊ฒฐ์ ์๋ฎฌ๋ ์ด์ )
- ์ํฌ์ ์นดํ ๊ณ ๋ฆฌ๋ณ ํต๊ณ ์ฐจํธ (Chart.js)
- ์ด๋ฉ์ผ ์๋ฆผ (์ํฌ์ ์์ 1์ผ ์ ์๋ ๋ฐ์ก)
- ์์ฝ ๋๊ธฐ์ ์๋ ์น๊ฒฉ (์ทจ์ ๋ฐ์ ์)
- ๊ด๋ฆฌ์ ๋์๋ณด๋ (์ ์ฒด ์์ฝ ํํฉ, ์์ต ํต๊ณ)
- ์ํฌ์ ์ด๋ฏธ์ง ์ ๋ก๋ ๋ฐ ๊ฐค๋ฌ๋ฆฌ
- ๋ค๊ตญ์ด ์ง์ (ํ๊ตญ์ด/์์ด)
๐ ๏ธ ๊ธฐ์ ์คํ
- ๋ฐฑ์๋: PHP 8.3+ (Vanilla PHP, ํ๋ ์์ํฌ ์์)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: SQLite (PDO)
- ํ๋ก ํธ์๋: HTML5, CSS3 (Flexbox/Grid), Vanilla JavaScript (ES6+)
- QR ์ฝ๋: PHP QR Code ๋ผ์ด๋ธ๋ฌ๋ฆฌ (์๋ ๊ตฌํ ๋๋ ๋จ์ SVG ์์ฑ)
- ์ด๋ฉ์ผ: PHP mail() ํจ์ ๋๋ SMTP (PHPMailer ์์ด ๊ธฐ๋ณธ ๊ธฐ๋ฅ ํ์ฉ)
- ์บ์ฑ: ํ์ผ ๊ธฐ๋ฐ ์บ์ (JSON ํ์ผ)
- ๋ณด์: ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ, CSRF ํ ํฐ, XSS ๋ฐฉ์ง, prepared statements
๐ ์์ ํ์ผ ๊ตฌ์กฐ
workshop-booking-attendance-platform/
โโโ index.php (๋ฉ์ธ ํ์ด์ง, ์ํฌ์ ๋ชฉ๋ก)
โโโ workshop.php (์ํฌ์ ์์ธ)
โโโ booking.php (์์ฝ ์ฒ๋ฆฌ)
โโโ login.php (๋ก๊ทธ์ธ)
โโโ register.php (ํ์๊ฐ์
)
โโโ my-bookings.php (๋ด ์์ฝ ๋ด์ญ)
โโโ admin/
โ โโโ dashboard.php (๊ด๋ฆฌ์ ๋์๋ณด๋)
โ โโโ workshops.php (์ํฌ์ ๊ด๋ฆฌ)
โ โโโ attendees.php (์ฐธ๊ฐ์ ๊ด๋ฆฌ)
โ โโโ qr-scanner.php (QR ์ถ์ ์ฒดํฌ)
โ โโโ stats.php (ํต๊ณ)
โโโ includes/
โ โโโ db.php (DB ์ฐ๊ฒฐ)
โ โโโ auth.php (์ธ์ฆ ํฌํผ)
โ โโโ qr.php (QR ์ฝ๋ ์์ฑ)
โ โโโ email.php (์ด๋ฉ์ผ ๋ฐ์ก)
โ โโโ cache.php (์บ์ ํฌํผ)
โ โโโ helpers.php (์ ํธ๋ฆฌํฐ ํจ์)
โโโ assets/
โ โโโ css/style.css
โ โโโ js/main.js
โ โโโ js/qr-scanner.js
โโโ uploads/ (์ํฌ์ ์ด๋ฏธ์ง)
โโโ cache/ (์บ์ ํ์ผ)
โโโ qrcodes/ (์์ฑ๋ QR ์ฝ๋ ์ด๋ฏธ์ง)
โโโ database.sqlite (SQLite DB)
โโโ error.log
๐ ํ์ต ๋ชฉํ
- ๋ณต์กํ ๋ค์ค ์ฌ์ฉ์ ์์คํ ์ค๊ณ (์ฌ์ฉ์ vs ๊ด๋ฆฌ์ ๊ถํ)
- ์ค์๊ฐ ์ข์ ๊ด๋ฆฌ ๋ฐ ๋์์ฑ ์ฒ๋ฆฌ (ํธ๋์ญ์ )
- QR ์ฝ๋ ์์ฑ ๋ฐ ๊ฒ์ฆ ๋ก์ง ๊ตฌํ
- ํ์ผ ๊ธฐ๋ฐ ์บ์ฑ ์ ๋ต
- ์ด๋ฉ์ผ ๋ฐ์ก ๋ฐ ์ฒจ๋ถํ์ผ ์ฒ๋ฆฌ
- ๋ณต์กํ SQL ์ฟผ๋ฆฌ (JOIN, GROUP BY, ์๋ธ์ฟผ๋ฆฌ)
- ๋ณด์ ๊ฐํ (CSRF, XSS, SQL Injection ๋ฐฉ์ด)
- ์๋ฌ ํธ๋ค๋ง ๋ฐ ๋ก๊น
- ์ฌ์ฉ์ ๊ฒฝํ ์ต์ ํ (๋ก๋ฉ ์ํ, ์ค์๊ฐ ํผ๋๋ฐฑ)
์ ์ฝ ์กฐ๊ฑด
- โข PHP 8.3 ์ด์ ์ฌ์ฉ (ํ์ ํํธ, named arguments ํ์ฉ)
- โข ์ผ๋ฐ PHP ํ๋ก์ ํธ (Laravel, Symfony ๋ฑ ํ๋ ์์ํฌ ๊ธ์ง)
- โข Composer ๋ฐ ์ธ๋ถ ํจํค์ง ์ฌ์ฉ ๊ธ์ง (QR ์ฝ๋๋ ์์ PHP๋ก ๊ตฌํ ๋๋ ๊ฐ๋จํ SVG ์์ฑ)
- โข PDO + SQLite๋ก ๋ฐ์ดํฐ ์ ์ฅ (MySQL ์ค์น ๋ถํ์)
- โข ํ๋ก ํธ์๋๋ ์์ HTML/CSS/JavaScript (React, Vue ๊ธ์ง)
- โข `php -S localhost:8000`์ผ๋ก ์ฆ์ ์คํ ๊ฐ๋ฅํด์ผ ํจ
- โข ๋ชจ๋ ์ฌ์ฉ์ ์ ๋ ฅ์ htmlspecialchars()๋ก ์ด์ค์ผ์ดํ
- โข ๋ชจ๋ DB ์ฟผ๋ฆฌ๋ prepared statements ์ฌ์ฉ (SQL Injection ๋ฐฉ์ง)
- โข CSRF ํ ํฐ์ ๋ชจ๋ ํผ์ ํฌํจ (์ธ์ ๊ธฐ๋ฐ ๊ฒ์ฆ)
- โข ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ (JWT ์ฌ์ฉ ๊ธ์ง)
- โข ํ์ผ ์ ๋ก๋ ์ ํ์ฅ์ ๋ฐ MIME ํ์ ๊ฒ์ฆ ํ์
- โข ์๋ฌ ๋ฐ์ ์ error.log ํ์ผ์ ๊ธฐ๋ก
- โข ์ํฌ์ ๋ชฉ๋ก์ ํ์ผ ์บ์ ํ์ฉ (10๋ถ TTL)
- โข QR ์ฝ๋๋ ์์ฝ ID๋ฅผ ์ํธํํ์ฌ ์์ฑ (์: base64 + HMAC)
- โข ๋์ ์์ฝ ์ฒ๋ฆฌ๋ฅผ ์ํ DB ํธ๋์ญ์ ์ฌ์ฉ
- โข ์ค์๊ฐ ์ข์ ํํฉ์ AJAX๋ก ์ฃผ๊ธฐ์ ๊ฐฑ์ (ํด๋ง)
- โข ๋ฐ์ํ ๋์์ธ ํ์ (CSS Flexbox/Grid ํ์ฉ)
- โข ์ ๊ทผ์ฑ ๊ณ ๋ ค (semantic HTML, ARIA ์์ฑ)
- โข ์ต์ 3๊ฐ ์ด์์ ํ ์ด๋ธ ์ค๊ณ (users, workshops, bookings, attendance)
- โข ๊ด๋ฆฌ์ ํ์ด์ง๋ /admin/ ๋๋ ํ ๋ฆฌ์ ๊ฒฉ๋ฆฌ
- โข ๊ด๋ฆฌ์ ๊ถํ ์ฒดํฌ ๋ฏธ๋ค์จ์ด ๊ตฌํ (์ธ์ role ํ์ธ)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir workshop-booking-attendance-platform && cd workshop-booking-attendance-platform
์ด ํด๋์์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
## ๋ชฉํ
SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ค๊ณํ๊ณ , ๊ธฐ๋ณธ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ๋ฐ DB ์ฐ๊ฒฐ ํฌํผ๋ฅผ ๋ง๋ญ๋๋ค.
## ์๊ตฌ์ฌํญ
1. ๋ค์ ๋๋ ํ ๋ฆฌ ์์ฑ:
- `includes/` (ํฌํผ ํ์ผ)
- `admin/` (๊ด๋ฆฌ์ ํ์ด์ง)
- `assets/css/`, `assets/js/` (์ ์ ํ์ผ)
- `uploads/` (์ํฌ์ ์ด๋ฏธ์ง)
- `cache/` (์บ์ ํ์ผ)
- `qrcodes/` (QR ์ฝ๋ ์ด๋ฏธ์ง)
2. `includes/db.php` ์์ฑ:
- PDO๋ก SQLite ์ฐ๊ฒฐ (`database.sqlite`)
- ์๋ฌ ๋ชจ๋: ERRMODE_EXCEPTION
- ์๋์ผ๋ก ํ
์ด๋ธ ์์ฑ (์์ ๊ฒฝ์ฐ)
3. ํ
์ด๋ธ ์ค๊ณ (SQL):
```sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS workshops (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
instructor TEXT NOT NULL,
location TEXT NOT NULL,
start_datetime DATETIME NOT NULL,
end_datetime DATETIME NOT NULL,
capacity INTEGER NOT NULL,
price REAL DEFAULT 0,
image TEXT,
created_by INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS bookings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
workshop_id INTEGER NOT NULL,
status TEXT DEFAULT 'confirmed' CHECK(status IN ('confirmed', 'cancelled', 'waitlist')),
qr_code TEXT UNIQUE,
booked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (workshop_id) REFERENCES workshops(id),
UNIQUE(user_id, workshop_id)
);
CREATE TABLE IF NOT EXISTS attendance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
booking_id INTEGER NOT NULL,
checked_in_at DATETIME,
FOREIGN KEY (booking_id) REFERENCES bookings(id)
);
CREATE INDEX idx_workshops_start ON workshops(start_datetime);
CREATE INDEX idx_bookings_user ON bookings(user_id);
CREATE INDEX idx_bookings_workshop ON bookings(workshop_id);
```
4. `includes/helpers.php` ์์ฑ:
- `csrf_token()`: CSRF ํ ํฐ ์์ฑ ๋ฐ ์ธ์
์ ์ฅ
- `verify_csrf()`: ํ ํฐ ๊ฒ์ฆ
- `escape($str)`: htmlspecialchars ๋ํผ
- `redirect($url)`: ๋ฆฌ๋ค์ด๋ ํธ ํฌํผ
- `is_logged_in()`: ๋ก๊ทธ์ธ ์ฌ๋ถ ํ์ธ
- `is_admin()`: ๊ด๋ฆฌ์ ๊ถํ ํ์ธ
- `log_error($message)`: error.log ํ์ผ์ ์๋ฌ ๊ธฐ๋ก
5. ํ
์คํธ ๋ฐ์ดํฐ ์ฝ์
(๊ด๋ฆฌ์ ๊ณ์ 1๊ฐ, ์ํ ์ํฌ์ 3๊ฐ):
- ๊ด๋ฆฌ์: admin@example.com / password123 (ํด์ ์ ์ฅ)
- ์ํฌ์: '์น ๊ฐ๋ฐ ๋ถํธ์บ ํ', 'UI/UX ๋์์ธ ์ํฌ์', '๋ฐ์ดํฐ ๋ถ์ ์
๋ฌธ'
## ์๋ฃ ์กฐ๊ฑด
- `php -S localhost:8000`๋ก ์๋ฒ ์คํ ์ ์๋ฌ ์์
- `database.sqlite` ํ์ผ ์์ฑ ํ์ธ
- ํ
์ด๋ธ 4๊ฐ ์์ฑ ํ์ธ (users, workshops, bookings, attendance)
- ํ
์คํธ ๋ฐ์ดํฐ ์ฝ์
ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ๋ก์ ํธ ํด๋ ๊ตฌ์กฐ๊ฐ ์์ฑ๋๊ณ , SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ด๊ธฐํ๋๋ฉฐ, ๊ธฐ๋ณธ ํฌํผ ํจ์๋ค์ด ์๋ํฉ๋๋ค. ๊ด๋ฆฌ์ ๊ณ์ ๊ณผ ์ํ ์ํฌ์ ๋ฐ์ดํฐ๊ฐ DB์ ์ ์ฅ๋์ด ์์ด์ผ ํฉ๋๋ค.
## ๋ชฉํ
์ฌ์ฉ์ ์ธ์ฆ ์์คํ
์ ๊ตฌ์ถํฉ๋๋ค (ํ์๊ฐ์
, ๋ก๊ทธ์ธ, ๋ก๊ทธ์์).
## ์๊ตฌ์ฌํญ
1. `includes/auth.php` ์์ฑ:
- `register_user($email, $password, $name)`: ํ์๊ฐ์
- ์ด๋ฉ์ผ ์ค๋ณต ์ฒดํฌ
- ๋น๋ฐ๋ฒํธ ํด์ฑ (password_hash with PASSWORD_BCRYPT)
- DB ์ฝ์
ํ ์๋ ๋ก๊ทธ์ธ
- `login_user($email, $password)`: ๋ก๊ทธ์ธ
- ์ด๋ฉ์ผ๋ก ์ฌ์ฉ์ ์กฐํ
- password_verify๋ก ๋น๋ฐ๋ฒํธ ํ์ธ
- ์ธ์
์ user_id, email, role ์ ์ฅ
- `logout_user()`: ๋ก๊ทธ์์ (์ธ์
ํ๊ดด)
- `require_login()`: ๋ก๊ทธ์ธ ํ์ ํ์ด์ง์ฉ ๋ฏธ๋ค์จ์ด
- `require_admin()`: ๊ด๋ฆฌ์ ์ ์ฉ ํ์ด์ง์ฉ ๋ฏธ๋ค์จ์ด
2. `register.php` ์์ฑ:
- GET: ํ์๊ฐ์
ํผ ํ์
- ์ด๋ฉ์ผ, ๋น๋ฐ๋ฒํธ, ๋น๋ฐ๋ฒํธ ํ์ธ, ์ด๋ฆ ์
๋ ฅ ํ๋
- CSRF ํ ํฐ hidden ํ๋
- POST: ํ์๊ฐ์
์ฒ๋ฆฌ
- CSRF ํ ํฐ ๊ฒ์ฆ
- ์
๋ ฅ ์ ํจ์ฑ ๊ฒ์ฌ (์ด๋ฉ์ผ ํ์, ๋น๋ฐ๋ฒํธ ๊ธธ์ด 8์ ์ด์)
- ๋น๋ฐ๋ฒํธ ํ์ธ ์ผ์น ์ฌ๋ถ
- `register_user()` ํธ์ถ
- ์ฑ๊ณต ์ index.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
- ์คํจ ์ ์๋ฌ ๋ฉ์์ง ํ์
3. `login.php` ์์ฑ:
- GET: ๋ก๊ทธ์ธ ํผ ํ์
- ์ด๋ฉ์ผ, ๋น๋ฐ๋ฒํธ ์
๋ ฅ ํ๋
- CSRF ํ ํฐ hidden ํ๋
- ํ์๊ฐ์
๋งํฌ
- POST: ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
- CSRF ํ ํฐ ๊ฒ์ฆ
- `login_user()` ํธ์ถ
- ์ฑ๊ณต ์ index.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
- ์คํจ ์ ์๋ฌ ๋ฉ์์ง ํ์
4. `logout.php` ์์ฑ:
- `logout_user()` ํธ์ถ ํ login.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
5. ๊ธฐ๋ณธ CSS ์คํ์ผ๋ง (`assets/css/style.css`):
- ํผ ์คํ์ผ (์
๋ ฅ ํ๋, ๋ฒํผ, ์๋ฌ ๋ฉ์์ง)
- ๋ฐ์ํ ๋ ์ด์์ (Flexbox)
- ์์ ํ
๋ง (์ฃผ์ ์์: #3498db, ๋ณด์กฐ ์์: #2c3e50)
## ์๋ฃ ์กฐ๊ฑด
- ํ์๊ฐ์
ํผ์์ ์ ๊ณ์ ์์ฑ ๊ฐ๋ฅ
- ๋ก๊ทธ์ธ ํผ์์ ์์ฑํ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ๊ฐ๋ฅ
- ๋ก๊ทธ์ธ ํ ์ธ์
์ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ ํ์ธ
- ๋ก๊ทธ์์ ์ ์ธ์
ํ๊ดด ํ์ธ
- CSRF ํ ํฐ ๊ฒ์ฆ ๋์ ํ์ธ
- ๋น๋ฐ๋ฒํธ ํด์ฑ ํ์ธ (DB์ ํ๋ฌธ ์ ์ฅ ๊ธ์ง)
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฌ์ฉ์๊ฐ ํ์๊ฐ์ ํ๊ณ ๋ก๊ทธ์ธํ ์ ์์ผ๋ฉฐ, ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ์ด ์ ์ ์๋ํฉ๋๋ค. CSRF ๋ณดํธ๊ฐ ์ ์ฉ๋์ด ์๊ณ , ๋น๋ฐ๋ฒํธ๋ ์์ ํ๊ฒ ํด์ฑ๋์ด ์ ์ฅ๋ฉ๋๋ค.
## ๋ชฉํ
์ํฌ์ ๋ชฉ๋ก ํ์ด์ง์ ์์ธ ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค. ์ฌ์ฉ์๊ฐ ์ํฌ์์ ๊ฒ์ํ๊ณ ํํฐ๋งํ ์ ์์ด์ผ ํฉ๋๋ค.
## ์๊ตฌ์ฌํญ
1. `index.php` (์ํฌ์ ๋ชฉ๋ก ํ์ด์ง):
- ํค๋: ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๋งํฌ, ์ฌ์ฉ์ ์ด๋ฆ ํ์
- ๊ฒ์ ํผ:
- ํค์๋ ๊ฒ์ (์ ๋ชฉ, ์ค๋ช
์์ ๊ฒ์)
- ์นดํ
๊ณ ๋ฆฌ ํํฐ (๋๋กญ๋ค์ด: ์ ์ฒด, ๊ฐ๋ฐ, ๋์์ธ, ๋น์ฆ๋์ค, ๊ธฐํ)
- ๋ ์ง ํํฐ (์์์ผ ์ดํ ์ํฌ์๋ง ํ์)
- ์ํฌ์ ์นด๋ ๋ฆฌ์คํธ:
- ๊ฐ ์นด๋: ์ด๋ฏธ์ง(์ธ๋ค์ผ), ์ ๋ชฉ, ๊ฐ์ฌ, ์ผ์, ์ฅ์, ์ ์/์์ฌ์, ๊ฐ๊ฒฉ
- ์์ฌ์ ๊ณ์ฐ: `capacity - (SELECT COUNT(*) FROM bookings WHERE workshop_id = ? AND status = 'confirmed')`
- ์์ฌ์ 0์ผ ๊ฒฝ์ฐ "๋ง๊ฐ" ๋ฐฐ์ง ํ์
- "์์ธํ ๋ณด๊ธฐ" ๋ฒํผ (workshop.php?id=X)
- ํ์ด์ง๋ค์ด์
(ํ ํ์ด์ง๋น 9๊ฐ)
- ์บ์ฑ:
- ๊ฒ์ ์กฐ๊ฑด์ด ์์ ๋ ์ํฌ์ ๋ชฉ๋ก์ `cache/workshops.json`์ 10๋ถ๊ฐ ์บ์
- `includes/cache.php` ์์ฑ (`get_cache($key)`, `set_cache($key, $data, $ttl)`)
2. `workshop.php` (์ํฌ์ ์์ธ ํ์ด์ง):
- GET ํ๋ผ๋ฏธํฐ๋ก ์ํฌ์ ID ๋ฐ๊ธฐ
- ์ํฌ์ ์ ๋ณด ์กฐํ (์ ๋ชฉ, ์ค๋ช
, ๊ฐ์ฌ, ์ผ์, ์ฅ์, ์ ์, ๊ฐ๊ฒฉ, ์ด๋ฏธ์ง)
- ์ค์๊ฐ ์์ฌ์ ํ์ (AJAX ํด๋ง์ผ๋ก 10์ด๋ง๋ค ๊ฐฑ์ )
- ์์ฝ ๋ฒํผ:
- ๋ก๊ทธ์ธ ํ์ (๋ฏธ๋ก๊ทธ์ธ ์ login.php๋ก ๋ฆฌ๋ค์ด๋ ํธ)
- ์์ฌ์ ์์ ๊ฒฝ์ฐ: "์์ฝํ๊ธฐ" ๋ฒํผ
- ์์ฌ์ ์์ ๊ฒฝ์ฐ: "๋๊ธฐ ๋ฑ๋ก" ๋ฒํผ
- ์ด๋ฏธ ์์ฝํ ๊ฒฝ์ฐ: "์์ฝ ์๋ฃ" ๋ฐฐ์ง ํ์
- ์ํฌ์ ์ค๋ช
๋งํฌ๋ค์ด ๋ ๋๋ง (๊ฐ๋จํ \n -> <br> ๋ณํ)
3. JavaScript (`assets/js/main.js`):
- ์ค์๊ฐ ์์ฌ์ ๊ฐฑ์ ํจ์ (AJAX):
```javascript
async function updateAvailableSeats(workshopId) {
const response = await fetch(`api/seats.php?id=${workshopId}`);
const data = await response.json();
document.getElementById('available-seats').textContent = data.available;
if (data.available === 0) {
document.getElementById('book-btn').textContent = '๋๊ธฐ ๋ฑ๋ก';
}
}
setInterval(() => updateAvailableSeats(workshopId), 10000);
```
4. `api/seats.php` ์์ฑ:
- GET ํ๋ผ๋ฏธํฐ๋ก ์ํฌ์ ID ๋ฐ๊ธฐ
- ์์ฌ์ ๊ณ์ฐ ํ JSON ์๋ต:
```json
{"available": 5, "capacity": 20}
```
## ์๋ฃ ์กฐ๊ฑด
- ์ํฌ์ ๋ชฉ๋ก ํ์ด์ง์์ ๋ชจ๋ ์ํฌ์ ํ์ ํ์ธ
- ๊ฒ์ ๋ฐ ํํฐ๋ง ๋์ ํ์ธ
- ํ์ด์ง๋ค์ด์
๋์ ํ์ธ
- ์ํฌ์ ์์ธ ํ์ด์ง์์ ์ ๋ณด ์ ํํ ํ์
- ์ค์๊ฐ ์์ฌ์ ๊ฐฑ์ ํ์ธ (10์ด๋ง๋ค)
- ์บ์ฑ ๋์ ํ์ธ (`cache/workshops.json` ํ์ผ ์์ฑ)
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฌ์ฉ์๊ฐ ์ํฌ์ ๋ชฉ๋ก์ ๊ฒ์ํ๊ณ ํํฐ๋งํ ์ ์์ผ๋ฉฐ, ์์ธ ํ์ด์ง์์ ์ค์๊ฐ ์์ฌ์์ ํ์ธํ ์ ์์ต๋๋ค. ์บ์ฑ์ด ์ ์ฉ๋์ด DB ๋ถํ๊ฐ ์ค์ด๋ญ๋๋ค.
## ๋ชฉํ
์ํฌ์ ์์ฝ ์์คํ
์ ๊ตฌํํฉ๋๋ค. ์ฌ์ฉ์๊ฐ ์ํฌ์์ ์์ฝํ๊ณ , QR ์ฝ๋๋ฅผ ๋ฐ์ผ๋ฉฐ, ์์ฝ ํ์ธ ์ด๋ฉ์ผ์ ๋ฐ์ต๋๋ค.
## ์๊ตฌ์ฌํญ
1. `booking.php` ์์ฑ:
- POST ์์ฒญ ์ฒ๋ฆฌ (workshop_id ํ๋ผ๋ฏธํฐ)
- ๋ก๊ทธ์ธ ํ์ (`require_login()`)
- CSRF ํ ํฐ ๊ฒ์ฆ
- ์ค๋ณต ์์ฝ ์ฒดํฌ (user_id + workshop_id ์กฐํฉ)
- ์์ฌ์ ํ์ธ:
- DB ํธ๋์ญ์
์์
- SELECT ... FOR UPDATE๋ก ์ํฌ์ ์ ๊ธ
- ์์ฌ์ ๊ณ์ฐ
- ์์ฌ์ ์์ผ๋ฉด: status = 'confirmed'
- ์์ฌ์ ์์ผ๋ฉด: status = 'waitlist'
- ์์ฝ ์ฝ์
(bookings ํ
์ด๋ธ)
- QR ์ฝ๋ ์์ฑ:
- ์์ฝ ID๋ฅผ ์ํธํ (base64 + HMAC-SHA256)
- `includes/qr.php` ์์ฑ (`generate_qr_code($data)` ํจ์)
- QR ์ฝ๋ ์ด๋ฏธ์ง๋ฅผ `qrcodes/{booking_id}.png`๋ก ์ ์ฅ
- bookings ํ
์ด๋ธ์ qr_code ํ๋ ์
๋ฐ์ดํธ
- ์ถ์ ๋ ์ฝ๋ ์์ฑ (attendance ํ
์ด๋ธ, checked_in_at์ NULL)
- ํธ๋์ญ์
์ปค๋ฐ
- ์์ฝ ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก:
- `includes/email.php` ์์ฑ (`send_booking_confirmation($user_email, $booking_data)` ํจ์)
- ์ด๋ฉ์ผ ๋ด์ฉ: ์ํฌ์ ์ ๋ณด, ์์ฝ ๋ฒํธ, QR ์ฝ๋ ์ด๋ฏธ์ง ์ฒจ๋ถ
- PHP mail() ํจ์ ์ฌ์ฉ (๋๋ SMTP ์ค์ ๊ฐ๋ฅ)
- ์ฑ๊ณต ์ my-bookings.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
2. `includes/qr.php` ์์ฑ:
- `generate_qr_code($data)` ํจ์:
- ์
๋ ฅ: ์์ฝ ID
- ์ํธํ: HMAC-SHA256์ผ๋ก ์๋ช
์์ฑ (๋น๋ฐํค๋ ํ๊ฒฝ๋ณ์ ๋๋ ์์)
- QR ์ฝ๋ ๋ฐ์ดํฐ: `{"booking_id": X, "signature": "..."}`
- ๊ฐ๋จํ QR ์ฝ๋ ์์ฑ ๋ก์ง (์์ PHP๋ก ๊ตฌํ ๋๋ SVG ํ
ํ๋ฆฟ ํ์ฉ)
- PNG ์ด๋ฏธ์ง ํ์ผ๋ก ์ ์ฅ
- `verify_qr_code($qr_data)` ํจ์:
- QR ์ฝ๋ ๋ฐ์ดํฐ ํ์ฑ
- ์๋ช
๊ฒ์ฆ
- ์์ฝ ID ๋ฐํ (๊ฒ์ฆ ์คํจ ์ false)
3. `my-bookings.php` (๋ด ์์ฝ ๋ด์ญ ํ์ด์ง):
- ๋ก๊ทธ์ธ ํ์ (`require_login()`)
- ํ์ฌ ์ฌ์ฉ์์ ์์ฝ ๋ชฉ๋ก ์กฐํ:
- JOIN workshops ON bookings.workshop_id = workshops.id
- ์์ฝ ์ํ๋ณ ํํฐ๋ง (confirmed, cancelled, waitlist)
- ๊ฐ ์์ฝ ์นด๋:
- ์ํฌ์ ์ ๋ชฉ, ์ผ์, ์ฅ์
- ์์ฝ ์ํ ๋ฐฐ์ง
- QR ์ฝ๋ ์ด๋ฏธ์ง ํ์
- "์์ฝ ์ทจ์" ๋ฒํผ (์ํฌ์ ์์ 24์๊ฐ ์ ๊น์ง๋ง ๊ฐ๋ฅ)
- "QR ์ฝ๋ ๋ค์ด๋ก๋" ๋ฒํผ
4. `cancel-booking.php` ์์ฑ:
- POST ์์ฒญ ์ฒ๋ฆฌ (booking_id ํ๋ผ๋ฏธํฐ)
- ๋ก๊ทธ์ธ ํ์ + ๋ณธ์ธ ์์ฝ์ธ์ง ํ์ธ
- CSRF ํ ํฐ ๊ฒ์ฆ
- ์ทจ์ ๊ฐ๋ฅ ์๊ฐ ์ฒดํฌ (์ํฌ์ ์์ 24์๊ฐ ์ )
- ์์ฝ ์ํ๋ฅผ 'cancelled'๋ก ์
๋ฐ์ดํธ
- ๋๊ธฐ์๊ฐ ์์ผ๋ฉด ์๋์ผ๋ก ์ฒซ ๋ฒ์งธ ๋๊ธฐ์๋ฅผ 'confirmed'๋ก ๋ณ๊ฒฝ
- ์ฑ๊ณต ์ my-bookings.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
## ์๋ฃ ์กฐ๊ฑด
- ์ํฌ์ ์์ฝ ์ DB์ ์์ฝ ๋ ์ฝ๋ ์์ฑ ํ์ธ
- QR ์ฝ๋ ์ด๋ฏธ์ง ํ์ผ ์์ฑ ํ์ธ (`qrcodes/` ๋๋ ํ ๋ฆฌ)
- ์์ฝ ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก ํ์ธ (๋ก์ปฌ ํ๊ฒฝ์์๋ ๋ก๊ทธ ํ์ผ๋ก ๋์ฒด ๊ฐ๋ฅ)
- ๋ด ์์ฝ ๋ด์ญ ํ์ด์ง์์ ์์ฝ ๋ชฉ๋ก ํ์ ํ์ธ
- ์์ฝ ์ทจ์ ๋์ ํ์ธ
- ๋๊ธฐ์ ์๋ ์น๊ฒฉ ๋์ ํ์ธ
- ๋์ ์์ฝ ์ ํธ๋์ญ์
์ผ๋ก ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ณด์ฅ
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฌ์ฉ์๊ฐ ์ํฌ์์ ์์ฝํ๋ฉด QR ์ฝ๋๊ฐ ์์ฑ๋๊ณ , ์์ฝ ํ์ธ ์ด๋ฉ์ผ์ ๋ฐ์ต๋๋ค. ๋ด ์์ฝ ๋ด์ญ์์ QR ์ฝ๋๋ฅผ ํ์ธํ๊ณ , ์์ฝ์ ์ทจ์ํ ์ ์์ต๋๋ค. ๋์ ์์ฝ ์ํฉ์์๋ ์ ์ ์ด๊ณผ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
## ๋ชฉํ
๊ด๋ฆฌ์ ํ์ด์ง๋ฅผ ๊ตฌ์ถํฉ๋๋ค. ๊ด๋ฆฌ์๋ ์ํฌ์์ ์์ฑ/์์ /์ญ์ ํ๊ณ , ์์ฝ์ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ต๋๋ค.
## ์๊ตฌ์ฌํญ
1. `admin/dashboard.php` (๊ด๋ฆฌ์ ๋์๋ณด๋):
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- ํต๊ณ ์นด๋:
- ์ ์ฒด ์ํฌ์ ์
- ์ ์ฒด ์์ฝ ์
- ์ค๋ ์์ํ๋ ์ํฌ์ ์
- ์ด๋ฒ ์ฃผ ์์ฝ ์
- ์ต๊ทผ ์์ฝ ๋ชฉ๋ก (์ต๊ทผ 10๊ฐ)
- ์ํฌ์ ๊ด๋ฆฌ, ์์ฝ์ ๊ด๋ฆฌ, QR ์ค์บ ๋ฉ๋ด ๋งํฌ
2. `admin/workshops.php` (์ํฌ์ ๊ด๋ฆฌ):
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- GET: ์ํฌ์ ๋ชฉ๋ก ํ์
- ํ
์ด๋ธ ํ์ (์ ๋ชฉ, ๊ฐ์ฌ, ์ผ์, ์ ์, ์์ฝ ์, ์์
)
- ๊ฐ ํ์ "์์ ", "์ญ์ " ๋ฒํผ
- "์ ์ํฌ์ ์ถ๊ฐ" ๋ฒํผ
- GET (id ํ๋ผ๋ฏธํฐ ์์ ๊ฒฝ์ฐ): ์ํฌ์ ์์ ํผ ํ์
- ๋ชจ๋ ํ๋ ํธ์ง ๊ฐ๋ฅ (์ ๋ชฉ, ์ค๋ช
, ์นดํ
๊ณ ๋ฆฌ, ๊ฐ์ฌ, ์ฅ์, ์ผ์, ์ ์, ๊ฐ๊ฒฉ)
- ์ด๋ฏธ์ง ์
๋ก๋ ํ๋
- POST: ์ํฌ์ ์์ฑ/์์ ์ฒ๋ฆฌ
- CSRF ํ ํฐ ๊ฒ์ฆ
- ์
๋ ฅ ์ ํจ์ฑ ๊ฒ์ฌ
- ์ด๋ฏธ์ง ์
๋ก๋ ์ฒ๋ฆฌ:
- ํ์ฉ ํ์ฅ์: jpg, png, gif
- ์ต๋ ํฌ๊ธฐ: 5MB
- MIME ํ์
๊ฒ์ฆ
- ํ์ผ๋ช
: `workshop_{id}_{timestamp}.{ext}`
- `uploads/` ๋๋ ํ ๋ฆฌ์ ์ ์ฅ
- DB ์ฝ์
/์
๋ฐ์ดํธ
- ์บ์ ๋ฌดํจํ (`cache/workshops.json` ์ญ์ )
- ์ฑ๊ณต ์ admin/workshops.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
3. `admin/delete-workshop.php` ์์ฑ:
- POST ์์ฒญ ์ฒ๋ฆฌ (id ํ๋ผ๋ฏธํฐ)
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- CSRF ํ ํฐ ๊ฒ์ฆ
- ํด๋น ์ํฌ์์ ๋ชจ๋ ์์ฝ ์ทจ์ ์ฒ๋ฆฌ
- ์ํฌ์ ์ญ์ (CASCADE๋ก ์์ฝ๋ ์ญ์ )
- ์บ์ ๋ฌดํจํ
- ์ฑ๊ณต ์ admin/workshops.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
4. `admin/attendees.php` (์์ฝ์ ๊ด๋ฆฌ):
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- GET ํ๋ผ๋ฏธํฐ๋ก ์ํฌ์ ID ๋ฐ๊ธฐ
- ํด๋น ์ํฌ์์ ์์ฝ์ ๋ชฉ๋ก ์กฐํ:
- JOIN users ON bookings.user_id = users.id
- ์์ฝ ์ํ, ์์ฝ์ผ์, ์ถ์ ์ฌ๋ถ ํ์
- ํ
์ด๋ธ ํ์ (์ด๋ฆ, ์ด๋ฉ์ผ, ์์ฝ ์ํ, ์ถ์ ์ฌ๋ถ, QR ์ฝ๋)
- ๊ฐ ํ์ "์ถ์ ์ฒดํฌ" ๋ฒํผ (์๋ ์ถ์ ์ฒ๋ฆฌ)
- CSV ๋ด๋ณด๋ด๊ธฐ ๋ฒํผ
5. ๊ด๋ฆฌ์ ๋ค๋น๊ฒ์ด์
๋ฉ๋ด:
- ๋ชจ๋ ๊ด๋ฆฌ์ ํ์ด์ง์ ๊ณตํต ํค๋ ํฌํจ
- ๋ฉ๋ด: ๋์๋ณด๋, ์ํฌ์ ๊ด๋ฆฌ, QR ์ค์บ, ํต๊ณ
## ์๋ฃ ์กฐ๊ฑด
- ๊ด๋ฆฌ์ ๋์๋ณด๋์์ ํต๊ณ ์ ๋ณด ํ์ ํ์ธ
- ์ํฌ์ ์์ฑ/์์ /์ญ์ ๋์ ํ์ธ
- ์ด๋ฏธ์ง ์
๋ก๋ ๋ฐ ์ ์ฅ ํ์ธ
- ์์ฝ์ ๋ชฉ๋ก ์กฐํ ํ์ธ
- ์บ์ ๋ฌดํจํ ๋์ ํ์ธ
- ๊ด๋ฆฌ์ ๊ถํ ์ฒดํฌ ๋์ ํ์ธ (์ผ๋ฐ ์ฌ์ฉ์ ์ ๊ทผ ์ ๋ฆฌ๋ค์ด๋ ํธ)
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ด๋ฆฌ์๊ฐ ์ํฌ์์ CRUDํ ์ ์์ผ๋ฉฐ, ์์ฝ์ ๋ชฉ๋ก์ ์กฐํํ๊ณ ๊ด๋ฆฌํ ์ ์์ต๋๋ค. ์ด๋ฏธ์ง ์ ๋ก๋๊ฐ ์์ ํ๊ฒ ์ฒ๋ฆฌ๋๊ณ , ์บ์๊ฐ ์ ์ ํ ๋ฌดํจํ๋ฉ๋๋ค.
## ๋ชฉํ
QR ์ฝ๋ ์ค์บ ๋ฐ ์ถ์ ์ฒดํฌ ์์คํ
์ ๊ตฌํํฉ๋๋ค. ๊ด๋ฆฌ์๊ฐ ์ฐธ๊ฐ์์ QR ์ฝ๋๋ฅผ ์ค์บํ์ฌ ์ถ์์ ํ์ธํ ์ ์์ต๋๋ค.
## ์๊ตฌ์ฌํญ
1. `admin/qr-scanner.php` (QR ์ค์บ ํ์ด์ง):
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- ๋ ๊ฐ์ง ์ค์บ ๋ฐฉ๋ฒ ์ ๊ณต:
- ์นด๋ฉ๋ผ๋ก ์ค์๊ฐ ์ค์บ (JavaScript ์น์บ API)
- QR ์ฝ๋ ์ด๋ฏธ์ง ํ์ผ ์
๋ก๋
- JavaScript (`assets/js/qr-scanner.js`):
- ์น์บ ์ ๊ทผ ๊ถํ ์์ฒญ
- ์ค์๊ฐ ๋น๋์ค ์คํธ๋ฆผ์์ QR ์ฝ๋ ๊ฐ์ง (jsQR ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋๋ ์์ JS)
- QR ์ฝ๋ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ๋ก ์ ์ก (AJAX)
- ํ์ผ ์
๋ก๋ ํผ:
- QR ์ฝ๋ ์ด๋ฏธ์ง ํ์ผ ์ ํ
- ์๋ฒ์์ ์ด๋ฏธ์ง ํ์ฑ (PHP GD ๋๋ Imagick)
- QR ์ฝ๋ ๋ฐ์ดํฐ ์ถ์ถ
2. `admin/check-attendance.php` ์์ฑ:
- POST ์์ฒญ ์ฒ๋ฆฌ (qr_data ํ๋ผ๋ฏธํฐ)
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- CSRF ํ ํฐ ๊ฒ์ฆ
- QR ์ฝ๋ ๊ฒ์ฆ:
- `verify_qr_code($qr_data)` ํธ์ถ
- ์์ฝ ID ์ถ์ถ
- ์์ฝ ์ ๋ณด ์กฐํ (bookings ํ
์ด๋ธ)
- ์์ฝ ์ํ ํ์ธ (confirmed๋ง ์ถ์ ๊ฐ๋ฅ)
- ์ค๋ณต ์ถ์ ์ฒดํฌ (attendance ํ
์ด๋ธ์์ checked_in_at์ด NULL์ธ์ง ํ์ธ)
- ์ถ์ ์ฒ๋ฆฌ:
- attendance ํ
์ด๋ธ์ checked_in_at์ ํ์ฌ ์๊ฐ์ผ๋ก ์
๋ฐ์ดํธ
- JSON ์๋ต:
```json
{
"success": true,
"message": "์ถ์ ์๋ฃ",
"user": {"name": "ํ๊ธธ๋", "email": "hong@example.com"},
"workshop": {"title": "์น ๊ฐ๋ฐ ๋ถํธ์บ ํ"}
}
```
- ์คํจ ์:
```json
{
"success": false,
"error": "์ ํจํ์ง ์์ QR ์ฝ๋์
๋๋ค."
}
```
3. `admin/manual-checkin.php` ์์ฑ:
- POST ์์ฒญ ์ฒ๋ฆฌ (booking_id ํ๋ผ๋ฏธํฐ)
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- CSRF ํ ํฐ ๊ฒ์ฆ
- ์๋ ์ถ์ ์ฒ๋ฆฌ (QR ์ฝ๋ ์์ด ๊ด๋ฆฌ์๊ฐ ์ง์ ์ฒดํฌ)
- ์ฑ๊ณต ์ admin/attendees.php๋ก ๋ฆฌ๋ค์ด๋ ํธ
4. QR ์ค์บ UI:
- ์ค์๊ฐ ์นด๋ฉ๋ผ ํ๋ฆฌ๋ทฐ (๋น๋์ค ์์)
- ์ค์บ ๊ฒฐ๊ณผ ํ์ ์์ญ (์ฑ๊ณต/์คํจ ๋ฉ์์ง, ์ฌ์ฉ์ ์ ๋ณด)
- ์ค์บ ์ด๋ ฅ ๋ฆฌ์คํธ (์ต๊ทผ 10๊ฐ)
- "๋ค์ ์ค์บ" ๋ฒํผ
5. QR ์ฝ๋ ๋ณด์ ๊ฐํ:
- QR ์ฝ๋ ๋ฐ์ดํฐ์ ํ์์คํฌํ ํฌํจ (์ ํจ๊ธฐ๊ฐ ์ค์ , ์: 24์๊ฐ)
- ์๋ช
๊ฒ์ฆ ์ ํ์์คํฌํ ์ฒดํฌ
- ์ฌ์ฌ์ฉ ๋ฐฉ์ง (ํ ๋ฒ ์ค์บ๋ QR ์ฝ๋๋ ์ฌ์ฌ์ฉ ๋ถ๊ฐ)
## ์๋ฃ ์กฐ๊ฑด
- QR ์ค์บ ํ์ด์ง์์ ์นด๋ฉ๋ผ ์ ๊ทผ ํ์ธ
- ์ค์๊ฐ QR ์ฝ๋ ์ค์บ ๋์ ํ์ธ
- ํ์ผ ์
๋ก๋๋ก QR ์ฝ๋ ์ค์บ ๋์ ํ์ธ
- QR ์ฝ๋ ๊ฒ์ฆ ๋ฐ ์ถ์ ์ฒ๋ฆฌ ํ์ธ
- ์ค๋ณต ์ถ์ ๋ฐฉ์ง ํ์ธ
- ์ ํจํ์ง ์์ QR ์ฝ๋ ๊ฑฐ๋ถ ํ์ธ
- ์๋ ์ถ์ ์ฒดํฌ ๋์ ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ด๋ฆฌ์๊ฐ QR ์ฝ๋๋ฅผ ์ค์บํ์ฌ ์ฐธ๊ฐ์์ ์ถ์์ ํ์ธํ ์ ์์ต๋๋ค. QR ์ฝ๋๋ ์์ ํ๊ฒ ๊ฒ์ฆ๋๋ฉฐ, ์ค๋ณต ์ถ์์ด ๋ฐฉ์ง๋ฉ๋๋ค. ์ค์๊ฐ ์ค์บ๊ณผ ํ์ผ ์ ๋ก๋ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ๋ชจ๋ ์๋ํฉ๋๋ค.
## ๋ชฉํ
๊ด๋ฆฌ์ ํต๊ณ ํ์ด์ง๋ฅผ ๊ตฌํํฉ๋๋ค. ์ํฌ์๋ณ ์ถ์๋ฅ , ์์ฝ ํํฉ, ์์ต ํต๊ณ ๋ฑ์ ์๊ฐํํฉ๋๋ค.
## ์๊ตฌ์ฌํญ
1. `admin/stats.php` (ํต๊ณ ํ์ด์ง):
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- ์ ์ฒด ํต๊ณ ์นด๋:
- ์ ์ฒด ์ํฌ์ ์
- ์ ์ฒด ์์ฝ ์ (confirmed + waitlist)
- ์ ์ฒด ์ถ์ ์
- ์ ์ฒด ์์ต (์์ฝ * ๊ฐ๊ฒฉ ํฉ๊ณ)
- ์ํฌ์๋ณ ํต๊ณ ํ
์ด๋ธ:
- ๊ฐ ์ํฌ์: ์ ๋ชฉ, ์ ์, ์์ฝ ์, ์ถ์ ์, ์ถ์๋ฅ , ์์ต
- ์ถ์๋ฅ ๊ณ์ฐ: `(์ถ์ ์ / ์์ฝ ์) * 100`
- ์์ต ๊ณ์ฐ: `์์ฝ ์ * ๊ฐ๊ฒฉ`
- ์นดํ
๊ณ ๋ฆฌ๋ณ ํต๊ณ ์ฐจํธ:
- Chart.js ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ (CDN)
- ํ์ด ์ฐจํธ: ์นดํ
๊ณ ๋ฆฌ๋ณ ์ํฌ์ ์
- ๋ง๋ ์ฐจํธ: ์นดํ
๊ณ ๋ฆฌ๋ณ ์์ฝ ์
- ์๋ณ ์์ฝ ์ถ์ด ์ฐจํธ:
- ๋ผ์ธ ์ฐจํธ: ์ต๊ทผ 6๊ฐ์๊ฐ ์๋ณ ์์ฝ ์
- ๋ ์ง ๋ฒ์ ํํฐ:
- ์์์ผ ~ ์ข
๋ฃ์ผ ์ ํ (๊ธฐ๋ณธ: ์ต๊ทผ 30์ผ)
- ํํฐ ์ ์ฉ ์ ํต๊ณ ์ฌ๊ณ์ฐ
2. `api/stats-data.php` ์์ฑ:
- GET ํ๋ผ๋ฏธํฐ๋ก ๋ ์ง ๋ฒ์ ๋ฐ๊ธฐ (start_date, end_date)
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- SQL ์ฟผ๋ฆฌ๋ก ํต๊ณ ๋ฐ์ดํฐ ๊ณ์ฐ:
- ์ํฌ์๋ณ ์์ฝ ์: `SELECT workshop_id, COUNT(*) FROM bookings GROUP BY workshop_id`
- ์ํฌ์๋ณ ์ถ์ ์: `SELECT workshop_id, COUNT(*) FROM bookings JOIN attendance ON bookings.id = attendance.booking_id WHERE checked_in_at IS NOT NULL GROUP BY workshop_id`
- ์นดํ
๊ณ ๋ฆฌ๋ณ ์ํฌ์ ์: `SELECT category, COUNT(*) FROM workshops GROUP BY category`
- ์๋ณ ์์ฝ ์: `SELECT strftime('%Y-%m', booked_at) as month, COUNT(*) FROM bookings GROUP BY month`
- JSON ์๋ต:
```json
{
"workshops": [...],
"categories": {...},
"monthly": [...]
}
```
3. JavaScript ์ฐจํธ ๋ ๋๋ง (`assets/js/stats.js`):
- AJAX๋ก `api/stats-data.php` ํธ์ถ
- Chart.js๋ก ์ฐจํธ ๊ทธ๋ฆฌ๊ธฐ:
```javascript
const ctx = document.getElementById('categoryChart').getContext('2d');
new Chart(ctx, {
type: 'pie',
data: {
labels: Object.keys(data.categories),
datasets: [{
data: Object.values(data.categories),
backgroundColor: ['#3498db', '#e74c3c', '#2ecc71', '#f39c12']
}]
}
});
```
- ๋ ์ง ๋ฒ์ ํํฐ ๋ณ๊ฒฝ ์ ์ฐจํธ ์
๋ฐ์ดํธ
4. CSV ๋ด๋ณด๋ด๊ธฐ ๊ธฐ๋ฅ:
- "CSV ๋ค์ด๋ก๋" ๋ฒํผ
- `admin/export-stats.php` ์์ฑ:
- ๊ด๋ฆฌ์ ์ ์ฉ (`require_admin()`)
- ํต๊ณ ๋ฐ์ดํฐ๋ฅผ CSV ํ์์ผ๋ก ์์ฑ
- Content-Type: text/csv ํค๋
- ํ์ผ๋ช
: `workshop_stats_{date}.csv`
## ์๋ฃ ์กฐ๊ฑด
- ํต๊ณ ํ์ด์ง์์ ์ ์ฒด ํต๊ณ ์นด๋ ํ์ ํ์ธ
- ์ํฌ์๋ณ ํต๊ณ ํ
์ด๋ธ ์ ํํ ํ์
- Chart.js ์ฐจํธ ๋ ๋๋ง ํ์ธ (ํ์ด, ๋ง๋, ๋ผ์ธ ์ฐจํธ)
- ๋ ์ง ๋ฒ์ ํํฐ ๋์ ํ์ธ
- CSV ๋ด๋ณด๋ด๊ธฐ ๋์ ํ์ธ
- ํต๊ณ ๋ฐ์ดํฐ ์ ํ์ฑ ๊ฒ์ฆ
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ด๋ฆฌ์๊ฐ ์ํฌ์๋ณ, ์นดํ ๊ณ ๋ฆฌ๋ณ, ์๋ณ ํต๊ณ๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์์ต๋๋ค. Chart.js๋ก ์ฐจํธ๊ฐ ๋ ๋๋ง๋๋ฉฐ, ๋ ์ง ๋ฒ์ ํํฐ์ CSV ๋ด๋ณด๋ด๊ธฐ ๊ธฐ๋ฅ์ด ์๋ํฉ๋๋ค.
## ๋ชฉํ
์ด๋ฉ์ผ ์๋ฆผ ์์คํ
์ ๊ตฌํํฉ๋๋ค. ์ํฌ์ ์์ 1์ผ ์ ์๋ ์๋ฆผ, ๋๊ธฐ์ ์น๊ฒฉ ์๋ฆผ ๋ฑ์ ๋ฐ์กํฉ๋๋ค.
## ์๊ตฌ์ฌํญ
1. `includes/email.php` ๊ฐ์ :
- `send_booking_confirmation($user_email, $booking_data)`: ์์ฝ ํ์ธ ์ด๋ฉ์ผ
- ์ ๋ชฉ: "[์ํฌ์ ์์ฝ ์๋ฃ] {์ํฌ์ ์ ๋ชฉ}"
- ๋ด์ฉ: ์ํฌ์ ์ ๋ณด, ์์ฝ ๋ฒํธ, QR ์ฝ๋ ์ด๋ฏธ์ง ์ฒจ๋ถ
- `send_reminder($user_email, $workshop_data)`: ์ํฌ์ ์์ 1์ผ ์ ์๋ฆผ
- ์ ๋ชฉ: "[๋ด์ผ ์ํฌ์ ์์] {์ํฌ์ ์ ๋ชฉ}"
- ๋ด์ฉ: ์ผ์, ์ฅ์, ์ค๋น๋ฌผ ์๋ด
- `send_waitlist_promotion($user_email, $booking_data)`: ๋๊ธฐ์ ์น๊ฒฉ ์๋ฆผ
- ์ ๋ชฉ: "[์์ฝ ํ์ ] {์ํฌ์ ์ ๋ชฉ} ๋๊ธฐ๊ฐ ํด์ ๋์์ต๋๋ค"
- ๋ด์ฉ: ์์ฝ ํ์ ์๋ด, QR ์ฝ๋ ์ฌ๋ฐ๊ธ
- `send_cancellation_notice($user_email, $workshop_data)`: ์ํฌ์ ์ทจ์ ์๋ด
- ์ ๋ชฉ: "[์ํฌ์ ์ทจ์ ์๋ด] {์ํฌ์ ์ ๋ชฉ}"
- ๋ด์ฉ: ์ทจ์ ์ฌ์ , ํ๋ถ ์๋ด
2. `cron/send-reminders.php` ์์ฑ (ํฌ๋ก ์ก ์คํฌ๋ฆฝํธ):
- CLI ๋ชจ๋์์ ์คํ (`php cron/send-reminders.php`)
- ๋ด์ผ ์์ํ๋ ์ํฌ์ ์กฐํ:
```sql
SELECT * FROM workshops
WHERE DATE(start_datetime) = DATE('now', '+1 day')
```
- ๊ฐ ์ํฌ์์ confirmed ์์ฝ์๋ค์๊ฒ ์๋ฆผ ๋ฐ์ก
- ๋ฐ์ก ๋ก๊ทธ ๊ธฐ๋ก (`logs/email.log`)
3. ๋๊ธฐ์ ์๋ ์น๊ฒฉ ๊ฐ์ :
- `cancel-booking.php` ์์ :
- ์์ฝ ์ทจ์ ์ ๋๊ธฐ์ ๋ชฉ๋ก ์กฐํ (waitlist ์ํ, ์์ฝ์ผ์ ์)
- ์ฒซ ๋ฒ์งธ ๋๊ธฐ์๋ฅผ confirmed๋ก ๋ณ๊ฒฝ
- QR ์ฝ๋ ์ฌ์์ฑ
- ์น๊ฒฉ ์๋ฆผ ์ด๋ฉ์ผ ๋ฐ์ก
4. ์ด๋ฉ์ผ ํ
ํ๋ฆฟ ์์คํ
:
- `templates/email/` ๋๋ ํ ๋ฆฌ ์์ฑ
- HTML ์ด๋ฉ์ผ ํ
ํ๋ฆฟ ์์ฑ:
- `booking_confirmation.html`
- `reminder.html`
- `waitlist_promotion.html`
- `cancellation_notice.html`
- ํ
ํ๋ฆฟ ๋ณ์ ์นํ ํจ์:
```php
function render_email_template($template, $vars) {
$html = file_get_contents("templates/email/{$template}.html");
foreach ($vars as $key => $value) {
$html = str_replace("{{{$key}}}", $value, $html);
}
return $html;
}
```
5. ์ด๋ฉ์ผ ๋ฐ์ก ๋ก๊น
:
- ๋ชจ๋ ์ด๋ฉ์ผ ๋ฐ์ก ์ `logs/email.log`์ ๊ธฐ๋ก
- ๋ก๊ทธ ํ์: `[2024-01-15 10:30:00] TO: user@example.com, SUBJECT: ์์ฝ ํ์ธ, STATUS: sent`
## ์๋ฃ ์กฐ๊ฑด
- ์์ฝ ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก ํ์ธ (QR ์ฝ๋ ์ฒจ๋ถ)
- ์ํฌ์ 1์ผ ์ ์๋ฆผ ์ด๋ฉ์ผ ๋ฐ์ก ํ์ธ (ํฌ๋ก ์ก ์คํ)
- ๋๊ธฐ์ ์น๊ฒฉ ์๋ฆผ ์ด๋ฉ์ผ ๋ฐ์ก ํ์ธ
- ์ด๋ฉ์ผ ํ
ํ๋ฆฟ ๋ ๋๋ง ํ์ธ
- ์ด๋ฉ์ผ ๋ฐ์ก ๋ก๊ทธ ๊ธฐ๋ก ํ์ธ
- HTML ์ด๋ฉ์ผ ์คํ์ผ๋ง ํ์ธ (์ธ๋ผ์ธ CSS)
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฌ์ฉ์๊ฐ ์์ฝ ํ์ธ, ์๋ฆผ, ๋๊ธฐ์ ์น๊ฒฉ ๋ฑ์ ์ด๋ฉ์ผ์ ๋ฐ์ต๋๋ค. ์ด๋ฉ์ผ์ HTML ํ ํ๋ฆฟ์ผ๋ก ๋ ๋๋ง๋๋ฉฐ, QR ์ฝ๋๊ฐ ์ฒจ๋ถ๋ฉ๋๋ค. ํฌ๋ก ์ก์ผ๋ก ์๋ ์๋ฆผ์ด ๋ฐ์ก๋ฉ๋๋ค.
## ๋ชฉํ
๋ณด์ ๊ฐํ ๋ฐ ์๋ฌ ํธ๋ค๋ง์ ๊ฐ์ ํฉ๋๋ค. ํ๋ก๋์
ํ๊ฒฝ์ ๋๋นํ ์ต์ข
์ ๊ฒ์ ์ํํฉ๋๋ค.
## ์๊ตฌ์ฌํญ
1. ๋ณด์ ๊ฐํ:
- **์ธ์
๋ณด์**:
- `session_start()` ์ ์ `session_regenerate_id(true)` ํธ์ถ (์ธ์
๊ณ ์ ๊ณต๊ฒฉ ๋ฐฉ์ง)
- ์ธ์
์ฟ ํค ์ค์ : `httponly=true`, `secure=true` (HTTPS ํ๊ฒฝ)
- ์ธ์
ํ์์์: 30๋ถ (๋ง์ง๋ง ํ๋ ์๊ฐ ๊ธฐ๋ก)
- **๋น๋ฐ๋ฒํธ ์ ์ฑ
**:
- ์ต์ 8์, ๋์๋ฌธ์/์ซ์/ํน์๋ฌธ์ ํฌํจ ๊ฒ์ฆ
- ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ๊ธฐ๋ฅ ์ถ๊ฐ (`change-password.php`)
- **ํ์ผ ์
๋ก๋ ๋ณด์**:
- MIME ํ์
๊ฒ์ฆ ๊ฐํ (finfo_file ์ฌ์ฉ)
- ํ์ผ๋ช
๋๋คํ (UUID ๋๋ ํด์)
- ์
๋ก๋ ๋๋ ํ ๋ฆฌ .htaccess ์ค์ (PHP ์คํ ๊ธ์ง)
- **SQL Injection ์ฌํ์ธ**:
- ๋ชจ๋ ์ฟผ๋ฆฌ์ prepared statements ์ฌ์ฉ ํ์ธ
- ๋์ ์ฟผ๋ฆฌ ์ ๊ฑฐ (ORDER BY, LIMIT ๋ฑ)
- **XSS ๋ฐฉ์ด**:
- ๋ชจ๋ ์ถ๋ ฅ์ `htmlspecialchars()` ์ ์ฉ ํ์ธ
- ์ฌ์ฉ์ ์
๋ ฅ HTML ํ๊ทธ ์ ๊ฑฐ (strip_tags)
- **CSRF ํ ํฐ ๊ฐ์ **:
- ํ ํฐ ๋ง๋ฃ ์๊ฐ ์ค์ (1์๊ฐ)
- ํ ํฐ ์ฌ์์ฑ ์ฃผ๊ธฐ (ํ์ด์ง ๋ก๋๋ง๋ค)
2. ์๋ฌ ํธ๋ค๋ง:
- **์๋ฌ ๋ก๊น
๊ฐ์ **:
- `set_error_handler()`๋ก ์ปค์คํ
์๋ฌ ํธ๋ค๋ฌ ๋ฑ๋ก
- ์๋ฌ ๋ ๋ฒจ๋ณ ๋ก๊น
(ERROR, WARNING, NOTICE)
- ๋ก๊ทธ ํ์ผ ํฌ๊ธฐ ์ ํ (10MB, ๋กํ
์ด์
)
- **์์ธ ์ฒ๋ฆฌ**:
- try-catch ๋ธ๋ก์ผ๋ก DB ์ฟผ๋ฆฌ ๊ฐ์ธ๊ธฐ
- ์ฌ์ฉ์ ์นํ์ ์๋ฌ ๋ฉ์์ง ํ์
- ๊ด๋ฆฌ์์๊ฒ ์๋ฌ ์๋ฆผ ์ด๋ฉ์ผ ๋ฐ์ก (์น๋ช
์ ์๋ฌ ์)
- **404/500 ์๋ฌ ํ์ด์ง**:
- `404.php`: ํ์ด์ง๋ฅผ ์ฐพ์ ์ ์์
- `500.php`: ์๋ฒ ๋ด๋ถ ์ค๋ฅ
- .htaccess์์ ErrorDocument ์ค์
3. ์ฑ๋ฅ ์ต์ ํ:
- **DB ์ฟผ๋ฆฌ ์ต์ ํ**:
- N+1 ์ฟผ๋ฆฌ ์ ๊ฑฐ (JOIN ์ฌ์ฉ)
- ์ธ๋ฑ์ค ์ถ๊ฐ ํ์ธ (workshops.start_datetime, bookings.user_id ๋ฑ)
- **์บ์ฑ ๊ฐ์ **:
- ์ํฌ์ ๋ชฉ๋ก ์บ์ TTL ์กฐ์ (5๋ถ)
- ํต๊ณ ๋ฐ์ดํฐ ์บ์ (1์๊ฐ)
- **์ด๋ฏธ์ง ์ต์ ํ**:
- ์
๋ก๋ ์ ์๋ ๋ฆฌ์ฌ์ด์ฆ (์ต๋ 1200x800)
- WebP ๋ณํ (GD ๋๋ Imagick)
- **Gzip ์์ถ**:
- .htaccess์์ mod_deflate ์ค์
4. ํ
์คํธ:
- **๋จ์ ํ
์คํธ** (๊ฐ๋จํ ํจ์ ํ
์คํธ):
- `tests/test_helpers.php` ์์ฑ
- `csrf_token()`, `escape()` ํจ์ ํ
์คํธ
- **ํตํฉ ํ
์คํธ** (์๋๋ฆฌ์ค ๊ธฐ๋ฐ):
- ์ฌ์ฉ์ ํ์๊ฐ์
-> ๋ก๊ทธ์ธ -> ์์ฝ -> ์ถ์ ์ฒดํฌ ์ ์ฒด ํ๋ฆ
- **๋ณด์ ํ
์คํธ**:
- CSRF ํ ํฐ ์์ด ํผ ์ ์ถ ์ ๊ฑฐ๋ถ ํ์ธ
- SQL Injection ์๋ (์: `' OR '1'='1`) ์ฐจ๋จ ํ์ธ
- XSS ์๋ (์: `<script>alert('XSS')</script>`) ์ด์ค์ผ์ดํ ํ์ธ
5. ๋ฌธ์ํ:
- `README.md` ์์ฑ:
- ํ๋ก์ ํธ ๊ฐ์
- ์ค์น ๋ฐฉ๋ฒ (`php -S localhost:8000`)
- ๊ธฐ๋ฅ ๋ชฉ๋ก
- ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
- ๋ณด์ ๊ณ ๋ ค์ฌํญ
- ๋ผ์ด์ ์ค
- `SETUP.md` ์์ฑ:
- ํ๊ฒฝ ์๊ตฌ์ฌํญ (PHP 8.3+, SQLite, GD)
- ์ด๊ธฐ ์ค์ (DB ์์ฑ, ๊ด๋ฆฌ์ ๊ณ์ )
- ํฌ๋ก ์ก ์ค์ (`cron/send-reminders.php`)
## ์๋ฃ ์กฐ๊ฑด
- ์ธ์
๋ณด์ ์ค์ ํ์ธ (httponly, secure ์ฟ ํค)
- ๋น๋ฐ๋ฒํธ ์ ์ฑ
๊ฒ์ฆ ๋์ ํ์ธ
- ํ์ผ ์
๋ก๋ ๋ณด์ ํ
์คํธ ํต๊ณผ
- SQL Injection, XSS, CSRF ๊ณต๊ฒฉ ์ฐจ๋จ ํ์ธ
- ์๋ฌ ๋ก๊น
๋์ ํ์ธ (error.log ํ์ผ)
- 404/500 ์๋ฌ ํ์ด์ง ํ์ ํ์ธ
- DB ์ฟผ๋ฆฌ ์ฑ๋ฅ ์ธก์ (EXPLAIN ์ฌ์ฉ)
- ์บ์ฑ ๋์ ํ์ธ (ํ์ผ ์์ฑ ๋ฐ TTL)
- ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ฆ ๋ฐ WebP ๋ณํ ํ์ธ
- ํ
์คํธ ์คํฌ๋ฆฝํธ ์คํ ์ฑ๊ณต
- README.md ๋ฐ SETUP.md ์์ฑ ์๋ฃ
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ๋ก์ ํธ๊ฐ ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌ ๊ฐ๋ฅํ ์์ค์ ๋ณด์๊ณผ ์์ ์ฑ์ ๊ฐ์ถฅ๋๋ค. ๋ชจ๋ ๋ณด์ ์ทจ์ฝ์ ์ด ํจ์น๋๊ณ , ์๋ฌ ํธ๋ค๋ง์ด ๊ฐ์ ๋๋ฉฐ, ์ฑ๋ฅ์ด ์ต์ ํ๋ฉ๋๋ค. ๋ฌธ์๊ฐ ์์ฑ๋์ด ๋ค๋ฅธ ๊ฐ๋ฐ์๊ฐ ํ๋ก์ ํธ๋ฅผ ์ดํดํ๊ณ ์ค์นํ ์ ์์ต๋๋ค.
## ๋ชฉํ
์ต์ข
ํ
์คํธ ๋ฐ ๋ฐฐํฌ ์ค๋น๋ฅผ ์๋ฃํฉ๋๋ค. ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ณ , ํ๋ก์ ํธ๋ฅผ ๋ง๋ฌด๋ฆฌํฉ๋๋ค.
## ์๊ตฌ์ฌํญ
1. UI/UX ๊ฐ์ :
- **๋ฐ์ํ ๋์์ธ ์์ฑ**:
- ๋ชจ๋ฐ์ผ (< 768px): ๋จ์ผ ์ปฌ๋ผ ๋ ์ด์์
- ํ๋ธ๋ฆฟ (768px ~ 1024px): 2์ปฌ๋ผ ๋ ์ด์์
- ๋ฐ์คํฌํฑ (> 1024px): 3์ปฌ๋ผ ๋ ์ด์์
- ํ๋ฒ๊ฑฐ ๋ฉ๋ด (๋ชจ๋ฐ์ผ)
- **๋ก๋ฉ ์ํ ํ์**:
- AJAX ์์ฒญ ์ ์คํผ๋ ํ์
- ๋ฒํผ ๋นํ์ฑํ (์ค๋ณต ํด๋ฆญ ๋ฐฉ์ง)
- **์ฌ์ฉ์ ํผ๋๋ฐฑ**:
- Toast ์๋ฆผ (์ฑ๊ณต/์๋ฌ ๋ฉ์์ง)
- ํผ ์ ํจ์ฑ ๊ฒ์ฌ ์ค์๊ฐ ํผ๋๋ฐฑ
- **์ ๊ทผ์ฑ ๊ฐ์ **:
- ARIA ์์ฑ ์ถ๊ฐ (role, aria-label ๋ฑ)
- ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
์ง์
- ์์ ๋๋น ๊ฐ์ (WCAG 2.1 AA ๊ธฐ์ค)
2. ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ:
- **์ํฌ์ ๋ฆฌ๋ทฐ ์์คํ
**:
- ์ํฌ์ ์ข
๋ฃ ํ ๋ฆฌ๋ทฐ ์์ฑ ๊ฐ๋ฅ
- ๋ณ์ (1~5์ ), ํ
์คํธ ๋ฆฌ๋ทฐ
- ๋ฆฌ๋ทฐ ๋ชฉ๋ก ํ์ (workshop.php)
- **์์๋ฆฌ์คํธ ๊ธฐ๋ฅ**:
- ๊ด์ฌ ์ํฌ์ ๋ด๊ธฐ ๋ฒํผ
- ๋ด ์์๋ฆฌ์คํธ ํ์ด์ง (`wishlist.php`)
- **์์
๊ณต์ ๊ธฐ๋ฅ**:
- ์ํฌ์ ์์ธ ํ์ด์ง์ ๊ณต์ ๋ฒํผ (Facebook, Twitter, ์นด์นด์คํก)
- Open Graph ๋ฉํ ํ๊ทธ ์ถ๊ฐ
- **๊ฒ์ ์๋์์ฑ**:
- ์ํฌ์ ์ ๋ชฉ ๊ฒ์ ์ ์๋์์ฑ ์ ์ (AJAX)
3. ํ๋ก๋์
ํ๊ฒฝ ์ค์ :
- **ํ๊ฒฝ ๋ณ์ ๋ถ๋ฆฌ**:
- `.env` ํ์ผ ์์ฑ (DB ๊ฒฝ๋ก, ๋น๋ฐํค, SMTP ์ค์ ๋ฑ)
- `includes/config.php` ์์ฑ (ํ๊ฒฝ๋ณ์ ๋ก๋)
- **๋๋ฒ๊ทธ ๋ชจ๋ ๋นํ์ฑํ**:
- `error_reporting(0)` ์ค์
- ์๋ฌ ๋ฉ์์ง ์จ๊น (์ฌ์ฉ์์๊ฒ ๋
ธ์ถ ๊ธ์ง)
- **HTTPS ๊ฐ์ **:
- .htaccess์์ HTTP -> HTTPS ๋ฆฌ๋ค์ด๋ ํธ
- **๋ณด์ ํค๋ ์ถ๊ฐ**:
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Content-Security-Policy ์ค์
4. ์ต์ข
ํ
์คํธ:
- **์ฌ์ฉ์ ์๋๋ฆฌ์ค ํ
์คํธ**:
- ์๋๋ฆฌ์ค 1: ์ ๊ท ์ฌ์ฉ์ ํ์๊ฐ์
-> ์ํฌ์ ๊ฒ์ -> ์์ฝ -> QR ์ฝ๋ ํ์ธ
- ์๋๋ฆฌ์ค 2: ๊ด๋ฆฌ์ ๋ก๊ทธ์ธ -> ์ํฌ์ ์์ฑ -> ์์ฝ์ ์กฐํ -> QR ์ค์บ -> ์ถ์ ์ฒดํฌ
- ์๋๋ฆฌ์ค 3: ์์ฝ ์ทจ์ -> ๋๊ธฐ์ ์๋ ์น๊ฒฉ -> ์๋ฆผ ์ด๋ฉ์ผ ํ์ธ
- **๋ธ๋ผ์ฐ์ ํธํ์ฑ ํ
์คํธ**:
- Chrome, Firefox, Safari, Edge ์ต์ ๋ฒ์
- **์ฑ๋ฅ ํ
์คํธ**:
- ํ์ด์ง ๋ก๋ฉ ์๊ฐ ์ธก์ (< 2์ด)
- DB ์ฟผ๋ฆฌ ๊ฐ์ ์ต์ ํ (ํ์ด์ง๋น < 10๊ฐ)
- **๋ถํ ํ
์คํธ** (์ ํ):
- Apache Bench ๋๋ Siege๋ก ๋์ ์ ์ ํ
์คํธ
5. ๋ฐฐํฌ ๊ฐ์ด๋ ์์ฑ:
- `DEPLOYMENT.md` ์์ฑ:
- ์๋ฒ ์๊ตฌ์ฌํญ (Apache/Nginx, PHP 8.3, SQLite)
- ํ์ผ ๊ถํ ์ค์ (uploads, cache, qrcodes ๋๋ ํ ๋ฆฌ ์ฐ๊ธฐ ๊ถํ)
- .htaccess ์ค์ (URL ์ฌ์์ฑ, ๋ณด์ ํค๋)
- ํฌ๋ก ์ก ๋ฑ๋ก (`cron/send-reminders.php` ๋งค์ผ ์ค์ 9์)
- ๋ฐฑ์
์ ๋ต (DB ํ์ผ, ์
๋ก๋ ์ด๋ฏธ์ง)
## ์๋ฃ ์กฐ๊ฑด
- ๋ฐ์ํ ๋์์ธ ๋ชจ๋ ๋๋ฐ์ด์ค์์ ํ์ธ
- ๋ก๋ฉ ์ํ ๋ฐ Toast ์๋ฆผ ๋์ ํ์ธ
- ์ ๊ทผ์ฑ ํ
์คํธ ํต๊ณผ (ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
, ARIA)
- ์ํฌ์ ๋ฆฌ๋ทฐ ์์คํ
๋์ ํ์ธ
- ์์๋ฆฌ์คํธ ๊ธฐ๋ฅ ๋์ ํ์ธ
- ์์
๊ณต์ ๋ฒํผ ๋์ ํ์ธ
- ๊ฒ์ ์๋์์ฑ ๋์ ํ์ธ
- ํ๊ฒฝ ๋ณ์ ๋ถ๋ฆฌ ํ์ธ (.env ํ์ผ)
- HTTPS ๊ฐ์ ๋ฆฌ๋ค์ด๋ ํธ ํ์ธ
- ๋ณด์ ํค๋ ํ์ธ (๊ฐ๋ฐ์ ๋๊ตฌ Network ํญ)
- ๋ชจ๋ ์ฌ์ฉ์ ์๋๋ฆฌ์ค ํ
์คํธ ํต๊ณผ
- ๋ธ๋ผ์ฐ์ ํธํ์ฑ ํ
์คํธ ํต๊ณผ
- ์ฑ๋ฅ ํ
์คํธ ํต๊ณผ (ํ์ด์ง ๋ก๋ฉ < 2์ด)
- DEPLOYMENT.md ์์ฑ ์๋ฃ
- ํ๋ก์ ํธ GitHub ๋ฆฌํฌ์งํ ๋ฆฌ์ ํธ์ ์๋ฃ
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ๋ก์ ํธ๊ฐ ์์ ํ ์์ฑ๋์ด ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌํ ์ ์์ต๋๋ค. ์ฌ์ฉ์ ๊ฒฝํ์ด ๊ฐ์ ๋๊ณ , ์ถ๊ฐ ๊ธฐ๋ฅ์ด ๊ตฌํ๋๋ฉฐ, ๋ชจ๋ ํ ์คํธ๋ฅผ ํต๊ณผํฉ๋๋ค. ๋ฐฐํฌ ๊ฐ์ด๋๊ฐ ์์ฑ๋์ด ๋ค๋ฅธ ์๋ฒ์ ์ฝ๊ฒ ์ค์นํ ์ ์์ต๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!