๐ŸŽ“ ์˜จ๋ผ์ธ ์›Œํฌ์ˆ ์˜ˆ์•ฝ & ์ถœ์„ ๊ด€๋ฆฌ ํ”Œ๋žซํผ - ์‹ค์‹œ๊ฐ„ ์ขŒ์„ ํ˜„ํ™ฉ & QR ์ฒดํฌ์ธ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP Vanilla PHP AI: Claude

๐ŸŽ“ ์˜จ๋ผ์ธ ์›Œํฌ์ˆ ์˜ˆ์•ฝ & ์ถœ์„ ๊ด€๋ฆฌ ํ”Œ๋žซํผ - ์‹ค์‹œ๊ฐ„ ์ขŒ์„ ํ˜„ํ™ฉ & QR ์ฒดํฌ์ธ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… workshop-booking-attendance-platform
์กฐํšŒ์ˆ˜ 53 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐ŸŽฏ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

์‹ค์ œ ๊ต์œก ๊ธฐ๊ด€์ด๋‚˜ ํ”„๋ฆฌ๋žœ์„œ ๊ฐ•์‚ฌ๊ฐ€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์›Œํฌ์ˆ ์˜ˆ์•ฝ ๋ฐ ์ถœ์„ ๊ด€๋ฆฌ ํ†ตํ•ฉ ํ”Œ๋žซํผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž(์ˆ˜๊ฐ•์ƒ)๋Š” ์›Œํฌ์ˆ์„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์˜ˆ์•ฝํ•˜๋ฉฐ, ๊ด€๋ฆฌ์ž(๊ฐ•์‚ฌ)๋Š” ์›Œํฌ์ˆ์„ ์ƒ์„ฑํ•˜๊ณ  ์ฐธ๊ฐ€์ž ์ถœ์„์„ 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์—๊ฒŒ ๋ณด๋‚ด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ณต์‚ฌ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜์„ธ์š”.

1
Step 1
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์— ์ €์žฅ๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2
Step 2
## ๋ชฉํ‘œ
์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค (ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ).

## ์š”๊ตฌ์‚ฌํ•ญ
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 ๋ณดํ˜ธ๊ฐ€ ์ ์šฉ๋˜์–ด ์žˆ๊ณ , ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์•ˆ์ „ํ•˜๊ฒŒ ํ•ด์‹ฑ๋˜์–ด ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

3
Step 3
## ๋ชฉํ‘œ
์›Œํฌ์ˆ ๋ชฉ๋ก ํŽ˜์ด์ง€์™€ ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์›Œํฌ์ˆ์„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ํ•„ํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

## ์š”๊ตฌ์‚ฌํ•ญ
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 ๋ถ€ํ•˜๊ฐ€ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.

4
Step 4
## ๋ชฉํ‘œ
์›Œํฌ์ˆ ์˜ˆ์•ฝ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์›Œํฌ์ˆ์„ ์˜ˆ์•ฝํ•˜๊ณ , 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 ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•˜๊ณ , ์˜ˆ์•ฝ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์‹œ ์˜ˆ์•ฝ ์ƒํ™ฉ์—์„œ๋„ ์ •์› ์ดˆ๊ณผ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

5
Step 5
## ๋ชฉํ‘œ
๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ์›Œํฌ์ˆ์„ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œํ•˜๊ณ , ์˜ˆ์•ฝ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

## ์š”๊ตฌ์‚ฌํ•ญ
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ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์˜ˆ์•ฝ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋˜๊ณ , ์บ์‹œ๊ฐ€ ์ ์ ˆํžˆ ๋ฌดํšจํ™”๋ฉ๋‹ˆ๋‹ค.

6
Step 6
## ๋ชฉํ‘œ
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 ์ฝ”๋“œ๋Š” ์•ˆ์ „ํ•˜๊ฒŒ ๊ฒ€์ฆ๋˜๋ฉฐ, ์ค‘๋ณต ์ถœ์„์ด ๋ฐฉ์ง€๋ฉ๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ์Šค์บ”๊ณผ ํŒŒ์ผ ์—…๋กœ๋“œ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ๋ชจ๋‘ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

7
Step 7
## ๋ชฉํ‘œ
๊ด€๋ฆฌ์ž ํ†ต๊ณ„ ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์›Œํฌ์ˆ๋ณ„ ์ถœ์„๋ฅ , ์˜ˆ์•ฝ ํ˜„ํ™ฉ, ์ˆ˜์ต ํ†ต๊ณ„ ๋“ฑ์„ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

## ์š”๊ตฌ์‚ฌํ•ญ
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 ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

8
Step 8
## ๋ชฉํ‘œ
์ด๋ฉ”์ผ ์•Œ๋ฆผ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์›Œํฌ์ˆ ์‹œ์ž‘ 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 ์ฝ”๋“œ๊ฐ€ ์ฒจ๋ถ€๋ฉ๋‹ˆ๋‹ค. ํฌ๋ก ์žก์œผ๋กœ ์ž๋™ ์•Œ๋ฆผ์ด ๋ฐœ์†ก๋ฉ๋‹ˆ๋‹ค.

9
Step 9
## ๋ชฉํ‘œ
๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋Œ€๋น„ํ•œ ์ตœ์ข… ์ ๊ฒ€์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

## ์š”๊ตฌ์‚ฌํ•ญ
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 ์ž‘์„ฑ ์™„๋ฃŒ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ”„๋กœ์ ํŠธ๊ฐ€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์˜ ๋ณด์•ˆ๊ณผ ์•ˆ์ •์„ฑ์„ ๊ฐ–์ถฅ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ํŒจ์น˜๋˜๊ณ , ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ๊ฐœ์„ ๋˜๋ฉฐ, ์„ฑ๋Šฅ์ด ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ๊ฐ€ ์ž‘์„ฑ๋˜์–ด ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ”„๋กœ์ ํŠธ๋ฅผ ์ดํ•ดํ•˜๊ณ  ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

10
Step 10
## ๋ชฉํ‘œ
์ตœ์ข… ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐฐํฌ ์ค€๋น„๋ฅผ ์™„๋ฃŒํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ณ , ํ”„๋กœ์ ํŠธ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

## ์š”๊ตฌ์‚ฌํ•ญ
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)

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

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

๋Œ“๊ธ€ (0)

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