๐ŸŽญ ๊ทน๋‹จ ๊ณต์—ฐ ์‹œ์ฆŒ ํ‹ฐ์ผ“ ํŒจํ‚ค์ง€ ํŒ๋งค ํ”Œ๋žซํผ - ์—ฐ๊ทน/๋ฎค์ง€์ปฌ ์‹œ๋ฆฌ์ฆˆ ๊ตฌ๋… ์‹œ์Šคํ…œ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP Laravel AI: Claude

๐ŸŽญ ๊ทน๋‹จ ๊ณต์—ฐ ์‹œ์ฆŒ ํ‹ฐ์ผ“ ํŒจํ‚ค์ง€ ํŒ๋งค ํ”Œ๋žซํผ - ์—ฐ๊ทน/๋ฎค์ง€์ปฌ ์‹œ๋ฆฌ์ฆˆ ๊ตฌ๋… ์‹œ์Šคํ…œ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… theater-season-ticket-platform
์กฐํšŒ์ˆ˜ 93 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

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

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

ํ•ต์‹ฌ ๊ธฐ๋Šฅ

  • ์‹œ์ฆŒ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ: ๊ทน๋‹จ์ด ์—ฌ๋Ÿฌ ๊ณต์—ฐ์„ ๋ฌถ์–ด ์‹œ์ฆŒ ํŒจํ‚ค์ง€ ์ƒ์„ฑ (์˜ˆ: 2025 ๋ด„ ์‹œ์ฆŒ - 4๊ฐœ ๊ณต์—ฐ)
  • ๋‹ค๋‹จ๊ณ„ ๊ฐ€๊ฒฉ ์ •์ฑ…: ์–ผ๋ฆฌ๋ฒ„๋“œ/์ •๊ทœ/ํ˜„์žฅ ๊ฐ€๊ฒฉ, VIP/์ผ๋ฐ˜์„ ๋“ฑ๊ธ‰๋ณ„ ์ฐจ๋“ฑ ๊ฐ€๊ฒฉ
  • ์ขŒ์„ ์˜ˆ์•ฝ ์‹œ์Šคํ…œ: ๊ณต์—ฐ๋ณ„๋กœ ๋‚ ์งœ/์‹œ๊ฐ„/์ขŒ์„ ์„ ํƒ ํ›„ ์˜ˆ์•ฝ (์‹œ์ฆŒ ํŒจ์Šค ์†Œ์ง€์ž ์šฐ์„  ์˜ˆ์•ฝ)
  • ํšŒ์› ๋“ฑ๊ธ‰ ์‹œ์Šคํ…œ: ๊ตฌ๋งค ์ด๋ ฅ์— ๋”ฐ๋ผ Bronze/Silver/Gold ๋“ฑ๊ธ‰, ๋“ฑ๊ธ‰๋ณ„ ํ• ์ธ ํ˜œํƒ
  • ๊ณต์—ฐ ์•Œ๋ฆผ ์ž๋™ํ™”: ์˜ˆ์•ฝํ•œ ๊ณต์—ฐ D-7, D-1 ์ž๋™ ์ด๋ฉ”์ผ/SMS ์•Œ๋ฆผ (Laravel Queue)
  • ๋ฆฌ๋ทฐ & ํ‰์ : ๊ด€๋žŒ ํ›„ ๊ณต์—ฐ๋ณ„ ๋ฆฌ๋ทฐ ์ž‘์„ฑ, ๋ณ„์  ์ง‘๊ณ„
  • ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ: ํ‹ฐ์ผ“ ํŒ๋งค ํ˜„ํ™ฉ, ์ขŒ์„ ์ ์œ ์œจ, ๋งค์ถœ ํ†ต๊ณ„ ์ฐจํŠธ (Chart.js)
  • ๊ฒฐ์ œ ์—ฐ๋™: Stripe/Toss Payments API ์—ฐ๋™ํ•˜์—ฌ ์‹ค์ œ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
  • PDF ํ‹ฐ์ผ“ ๋ฐœ๊ธ‰: ์˜ˆ์•ฝ ์™„๋ฃŒ ์‹œ QR ์ฝ”๋“œ ํฌํ•จ PDF ํ‹ฐ์ผ“ ์ž๋™ ์ƒ์„ฑ (DomPDF)
  • Redis ์บ์‹ฑ: ์ขŒ์„ ์žฌ๊ณ , ์ธ๊ธฐ ๊ณต์—ฐ ๋ชฉ๋ก ๋“ฑ ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ ์บ์‹ฑ

ํ•„์ˆ˜ ๊ตฌํ˜„ ์‚ฌํ•ญ

  1. Laravel 13.x + Blade + Tailwind CSS + Alpine.js ์Šคํƒ
  2. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํฌํ•จ)
  3. Eloquent ๊ด€๊ณ„: Season hasMany Shows, Show hasMany Seats, User hasMany Bookings (๋‹ค๋Œ€๋‹ค ํฌํ•จ)
  4. Laravel Policy๋กœ ๊ถŒํ•œ ๊ด€๋ฆฌ (๊ทน๋‹จ ๊ด€๋ฆฌ์ž/์ผ๋ฐ˜ ํšŒ์› ๊ตฌ๋ถ„)
  5. Queue๋ฅผ ์ด์šฉํ•œ ์ด๋ฉ”์ผ ์•Œ๋ฆผ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  6. ์ขŒ์„ ์„ ํƒ UI: ๊ทน์žฅ ์ขŒ์„ ๋ฐฐ์น˜๋„ ์‹œ๊ฐํ™” (์˜ˆ์•ฝ/์„ ํƒ/๋งค์ง„ ์ƒํƒœ ๊ตฌ๋ถ„)
  7. ๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค: ์žฅ๋ฐ”๊ตฌ๋‹ˆ โ†’ ๊ฒฐ์ œ ์ •๋ณด ์ž…๋ ฅ โ†’ ๊ฒฐ์ œ ์™„๋ฃŒ โ†’ ํ‹ฐ์ผ“ ๋ฐœ๊ธ‰
  8. ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€: ์‹œ์ฆŒ/๊ณต์—ฐ/์ขŒ์„ CRUD, ์˜ˆ์•ฝ ๊ด€๋ฆฌ, ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ
  9. ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (๋ชจ๋ฐ”์ผ ํ‹ฐ์ผ“ ํ™•์ธ ์ตœ์ ํ™”)
  10. PHPUnit ํ…Œ์ŠคํŠธ: ์˜ˆ์•ฝ ๋กœ์ง, ๊ฒฐ์ œ ๊ฒ€์ฆ, ์ขŒ์„ ์ค‘๋ณต ๋ฐฉ์ง€ ํ…Œ์ŠคํŠธ

๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ

  • WebSocket(Laravel Reverb)์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์ขŒ์„ ์ ์œ  ์ƒํƒœ ๋™๊ธฐํ™”
  • ๋‹ค๊ตญ์–ด ์ง€์› (ํ•œ๊ตญ์–ด/์˜์–ด)
  • ์†Œ์…œ ๋กœ๊ทธ์ธ (Google/Kakao OAuth)
  • ์ถ”์ฒœ ์‹œ์Šคํ…œ: ๊ณผ๊ฑฐ ๊ด€๋žŒ ์ด๋ ฅ ๊ธฐ๋ฐ˜ AI ์ถ”์ฒœ (OpenAI API)
  • ๊ณต์—ฐ์žฅ 3D ์ขŒ์„ ๋ทฐ (Three.js)
  • ์นœ๊ตฌ ์ดˆ๋Œ€ ํ”„๋กœ๋ชจ์…˜ (์ถ”์ฒœ์ธ ํ• ์ธ ์ฝ”๋“œ)

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x, PHP 8.3+
  • ํ”„๋ก ํŠธ์—”๋“œ: Blade Templates, Tailwind CSS 4.x, Alpine.js 3.x
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite (๊ฐœ๋ฐœ), MySQL/PostgreSQL (ํ”„๋กœ๋•์…˜ ๊ถŒ์žฅ)
  • ์บ์‹ฑ: Redis
  • ํ: Laravel Queue (database driver)
  • ๊ฒฐ์ œ: Stripe API / Toss Payments API
  • PDF: DomPDF / Snappy (wkhtmltopdf)
  • ์ฐจํŠธ: Chart.js
  • ํ…Œ์ŠคํŠธ: PHPUnit, Laravel Dusk (E2E)

ํ•™์Šต ๋ชฉํ‘œ

  • ๋ณต์žกํ•œ ์˜ˆ์•ฝ ์‹œ์Šคํ…œ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์„ค๊ณ„
  • ์ขŒ์„ ์žฌ๊ณ  ๊ด€๋ฆฌ ๋ฐ ๋™์‹œ์„ฑ ์ œ์–ด (ํŠธ๋žœ์žญ์…˜, ๋ฝ)
  • ๊ฒฐ์ œ API ์—ฐ๋™ ๋ฐ ๋ณด์•ˆ ์ฒ˜๋ฆฌ
  • Queue๋ฅผ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ
  • Policy/Gate๋ฅผ ํ†ตํ•œ ์„ธ๋ฐ€ํ•œ ๊ถŒํ•œ ๊ด€๋ฆฌ
  • Redis ์บ์‹ฑ ์ „๋žต ์ˆ˜๋ฆฝ
  • ๋Œ€์šฉ๋Ÿ‰ ์˜ˆ์•ฝ ๋ฐ์ดํ„ฐ ์ตœ์ ํ™” (์ธ๋ฑ์Šค, N+1 ์ฟผ๋ฆฌ ํ•ด๊ฒฐ)
  • ์‹ค๋ฌด ์ˆ˜์ค€์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x ํ•„์ˆ˜ (๊ตฌ๋ฒ„์ „ ์‚ฌ์šฉ ๊ธˆ์ง€)
  • โ€ข Blade ํ…œํ”Œ๋ฆฟ ์—”์ง„ ์‚ฌ์šฉ (React/Vue ๋“ฑ SPA ํ”„๋ ˆ์ž„์›Œํฌ ๊ธˆ์ง€)
  • โ€ข Tailwind CSS 4.x๋กœ ์Šคํƒ€์ผ๋ง
  • โ€ข Alpine.js๋กœ ๊ฐ„๋‹จํ•œ ์ธํ„ฐ๋ž™์…˜ ์ฒ˜๋ฆฌ (์ขŒ์„ ์„ ํƒ, ๋ชจ๋‹ฌ ๋“ฑ)
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ)
  • โ€ข Redis ์„ค์น˜ ๋ฐ ์บ์‹ฑ ์ ์šฉ ํ•„์ˆ˜
  • โ€ข Stripe ๋˜๋Š” Toss Payments ํ…Œ์ŠคํŠธ API ํ‚ค ์‚ฌ์šฉ
  • โ€ข Queue ์ž‘์—…์€ database ๋“œ๋ผ์ด๋ฒ„๋กœ ์‹œ์ž‘ (ํ”„๋กœ๋•์…˜์—์„œ๋Š” Redis ๊ถŒ์žฅ)
  • โ€ข ์ขŒ์„ ์ค‘๋ณต ์˜ˆ์•ฝ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜ ๋ฐ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ์ ์šฉ
  • โ€ข ๊ด€๋ฆฌ์ž์™€ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ์—ญํ•  ๊ตฌ๋ถ„ (Laravel Policy ํ™œ์šฉ)
  • โ€ข ๋ชจ๋“  ํผ์— CSRF ํ† ํฐ ์ ์šฉ
  • โ€ข ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ ํŒŒ์ผ ๊ฒ€์ฆ (ํ™•์žฅ์ž, ํฌ๊ธฐ ์ œํ•œ)
  • โ€ข ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” .env ํŒŒ์ผ๋กœ ๊ด€๋ฆฌ
  • โ€ข PHPUnit ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 50% ์ด์ƒ
  • โ€ข ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ํ•„์ˆ˜ (๋ชจ๋ฐ”์ผ ์šฐ์„  ์ ‘๊ทผ)

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

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

1
Step 1
mkdir theater-season-ticket-platform && cd theater-season-ticket-platform

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

Laravel 13.x ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ๋ณธ ์„ค์ •์„ ์™„๋ฃŒํ•ด์ฃผ์„ธ์š”.

```bash
composer create-project laravel/laravel . "^13.0"
```

๋‹ค์Œ ์„ค์ •์„ ์ง„ํ–‰ํ•˜์„ธ์š”:
1. .env ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ SQLite๋กœ ์„ค์ •
   - DB_CONNECTION=sqlite
   - DB_DATABASE ๊ฒฝ๋กœ๋ฅผ database/database.sqlite๋กœ ์ง€์ •
   - database/database.sqlite ํŒŒ์ผ ์ƒ์„ฑ
2. Tailwind CSS 4.x ์„ค์น˜ ๋ฐ ์„ค์ •
   - npm install -D tailwindcss@latest postcss autoprefixer
   - npx tailwindcss init -p
   - tailwind.config.js์—์„œ content ๊ฒฝ๋กœ ์„ค์ • (resources/**/*.blade.php, resources/**/*.js)
   - resources/css/app.css์— @tailwind ์ง€์‹œ๋ฌธ ์ถ”๊ฐ€
3. Alpine.js 3.x ์„ค์น˜
   - npm install alpinejs@latest
   - resources/js/app.js์—์„œ Alpine ์ดˆ๊ธฐํ™”
4. Redis ์„ค์ •
   - .env์—์„œ CACHE_DRIVER=redis, QUEUE_CONNECTION=database ์„ค์ •
   - composer require predis/predis
5. ๊ธฐ๋ณธ ์ธ์ฆ ์Šค์บํด๋”ฉ ์„ค์น˜
   - composer require laravel/breeze --dev
   - php artisan breeze:install blade
   - npm install && npm run build
6. ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์ถ”๊ฐ€ ์„ค์น˜
   - composer require barryvdh/laravel-dompdf (PDF ์ƒ์„ฑ)
   - composer require stripe/stripe-php (๊ฒฐ์ œ ์—ฐ๋™)

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰:
```bash
php artisan migrate
```

๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰:
```bash
php artisan serve
```

ํ”„๋กœ์ ํŠธ ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ€ ์™„์„ฑ๋˜๋ฉด http://localhost:8000 ์— ์ ‘์†ํ•˜์—ฌ Laravel ์›ฐ์ปด ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์„ค์ •๋˜๋ฉฐ, Tailwind CSS์™€ Alpine.js๊ฐ€ ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค. Breeze ์ธ์ฆ ์‹œ์Šคํ…œ์ด ์„ค์น˜๋˜์–ด ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ์ด ์ค€๋น„๋ฉ๋‹ˆ๋‹ค. Redis์™€ ๊ฒฐ์ œ/PDF ํŒจํ‚ค์ง€๊ฐ€ ์„ค์น˜๋˜์–ด ์ถ”ํ›„ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์ค€๋น„๊ฐ€ ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค.

2
Step 2
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์„ธ์š”.

๋‹ค์Œ ํ…Œ์ด๋ธ”๋“ค์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

1. **users ํ…Œ์ด๋ธ”** (Breeze ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ํ™•์žฅ)
   - role ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (enum: 'admin', 'user', ๊ธฐ๋ณธ๊ฐ’ 'user')
   - membership_tier ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (enum: 'bronze', 'silver', 'gold', nullable)
   - total_spent ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (decimal, ๊ธฐ๋ณธ๊ฐ’ 0) - ๋ˆ„์  ๊ตฌ๋งค ๊ธˆ์•ก

2. **seasons ํ…Œ์ด๋ธ”**
   - id (primary key)
   - title (string) - ์‹œ์ฆŒ ์ œ๋ชฉ (์˜ˆ: "2025 ๋ด„ ์‹œ์ฆŒ")
   - description (text) - ์‹œ์ฆŒ ์†Œ๊ฐœ
   - start_date, end_date (date) - ์‹œ์ฆŒ ๊ธฐ๊ฐ„
   - image (string, nullable) - ์‹œ์ฆŒ ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
   - is_active (boolean, ๊ธฐ๋ณธ๊ฐ’ true) - ํŒ๋งค ์ค‘ ์—ฌ๋ถ€
   - created_at, updated_at

3. **shows ํ…Œ์ด๋ธ”**
   - id (primary key)
   - season_id (foreign key โ†’ seasons.id, onDelete cascade)
   - title (string) - ๊ณต์—ฐ ์ œ๋ชฉ
   - description (text)
   - genre (string) - ์žฅ๋ฅด (์—ฐ๊ทน/๋ฎค์ง€์ปฌ/์˜คํŽ˜๋ผ ๋“ฑ)
   - duration_minutes (integer) - ๊ณต์—ฐ ์‹œ๊ฐ„(๋ถ„)
   - image (string, nullable)
   - created_at, updated_at

4. **show_schedules ํ…Œ์ด๋ธ”**
   - id (primary key)
   - show_id (foreign key โ†’ shows.id, onDelete cascade)
   - show_date (date) - ๊ณต์—ฐ ๋‚ ์งœ
   - show_time (time) - ๊ณต์—ฐ ์‹œ์ž‘ ์‹œ๊ฐ„
   - venue (string) - ๊ณต์—ฐ์žฅ ์ด๋ฆ„
   - total_seats (integer) - ์ด ์ขŒ์„ ์ˆ˜
   - available_seats (integer) - ๋‚จ์€ ์ขŒ์„ ์ˆ˜
   - created_at, updated_at

5. **seats ํ…Œ์ด๋ธ”**
   - id (primary key)
   - show_schedule_id (foreign key โ†’ show_schedules.id, onDelete cascade)
   - seat_number (string) - ์ขŒ์„ ๋ฒˆํ˜ธ (์˜ˆ: "A-1", "B-5")
   - seat_type (enum: 'vip', 'regular') - ์ขŒ์„ ๋“ฑ๊ธ‰
   - price (decimal) - ์ขŒ์„ ๊ฐ€๊ฒฉ
   - status (enum: 'available', 'reserved', 'sold') - ์ขŒ์„ ์ƒํƒœ
   - unique ์ œ์•ฝ ์กฐ๊ฑด: (show_schedule_id, seat_number)
   - created_at, updated_at

6. **season_packages ํ…Œ์ด๋ธ”**
   - id (primary key)
   - season_id (foreign key โ†’ seasons.id, onDelete cascade)
   - name (string) - ํŒจํ‚ค์ง€ ์ด๋ฆ„ (์˜ˆ: "์–ผ๋ฆฌ๋ฒ„๋“œ 4ํšŒ๊ถŒ")
   - description (text)
   - show_count (integer) - ํฌํ•จ๋œ ๊ณต์—ฐ ์ˆ˜
   - price (decimal) - ํŒจํ‚ค์ง€ ๊ฐ€๊ฒฉ
   - discount_rate (decimal) - ํ• ์ธ์œจ (%)
   - is_available (boolean, ๊ธฐ๋ณธ๊ฐ’ true)
   - created_at, updated_at

7. **bookings ํ…Œ์ด๋ธ”**
   - id (primary key)
   - user_id (foreign key โ†’ users.id, onDelete cascade)
   - season_package_id (foreign key โ†’ season_packages.id, nullable) - ์‹œ์ฆŒ ํŒจ์Šค ๊ตฌ๋งค ์‹œ
   - booking_number (string, unique) - ์˜ˆ์•ฝ ๋ฒˆํ˜ธ (์ž๋™ ์ƒ์„ฑ)
   - total_amount (decimal) - ์ด ๊ฒฐ์ œ ๊ธˆ์•ก
   - payment_status (enum: 'pending', 'completed', 'failed', 'refunded')
   - payment_method (string, nullable) - ๊ฒฐ์ œ ์ˆ˜๋‹จ
   - stripe_payment_id (string, nullable) - Stripe ๊ฒฐ์ œ ID
   - created_at, updated_at

8. **booking_seats ํ…Œ์ด๋ธ”** (์ค‘๊ฐ„ ํ…Œ์ด๋ธ”)
   - id (primary key)
   - booking_id (foreign key โ†’ bookings.id, onDelete cascade)
   - seat_id (foreign key โ†’ seats.id, onDelete cascade)
   - attended (boolean, ๊ธฐ๋ณธ๊ฐ’ false) - ์‹ค์ œ ๊ด€๋žŒ ์—ฌ๋ถ€
   - unique ์ œ์•ฝ ์กฐ๊ฑด: (booking_id, seat_id)
   - created_at, updated_at

9. **reviews ํ…Œ์ด๋ธ”**
   - id (primary key)
   - user_id (foreign key โ†’ users.id, onDelete cascade)
   - show_id (foreign key โ†’ shows.id, onDelete cascade)
   - rating (integer) - ๋ณ„์  (1~5)
   - comment (text, nullable) - ๋ฆฌ๋ทฐ ๋‚ด์šฉ
   - created_at, updated_at

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ƒ์„ฑ ๋ช…๋ น์–ด:
```bash
php artisan make:migration add_role_to_users_table --table=users
php artisan make:migration create_seasons_table
php artisan make:migration create_shows_table
php artisan make:migration create_show_schedules_table
php artisan make:migration create_seats_table
php artisan make:migration create_season_packages_table
php artisan make:migration create_bookings_table
php artisan make:migration create_booking_seats_table
php artisan make:migration create_reviews_table
```

๊ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์— ์œ„ ์Šคํ‚ค๋งˆ๋Œ€๋กœ ์ปฌ๋Ÿผ๊ณผ ์ œ์•ฝ ์กฐ๊ฑด์„ ์ •์˜ํ•˜๊ณ , ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•˜์„ธ์š”:
```bash
php artisan migrate
```

์ธ๋ฑ์Šค ์ถ”๊ฐ€:
- seats ํ…Œ์ด๋ธ”: (show_schedule_id, status) ๋ณตํ•ฉ ์ธ๋ฑ์Šค
- bookings ํ…Œ์ด๋ธ”: (user_id, payment_status) ๋ณตํ•ฉ ์ธ๋ฑ์Šค
- reviews ํ…Œ์ด๋ธ”: (show_id, rating) ๋ณตํ•ฉ ์ธ๋ฑ์Šค

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: 9๊ฐœ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜๊ณ  ์‹คํ–‰๋˜์–ด, ์‹œ์ฆŒ/๊ณต์—ฐ/์ขŒ์„/์˜ˆ์•ฝ/๋ฆฌ๋ทฐ ๋“ฑ ๋ชจ๋“  ํ…Œ์ด๋ธ”์ด SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด๊ณผ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

3
Step 3
Eloquent ๋ชจ๋ธ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๊ณ„๋ฅผ ์ •์˜ํ•˜์„ธ์š”.

๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ๋ชจ๋ธ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์„ธ์š”:
```bash
php artisan make:model Season
php artisan make:model Show
php artisan make:model ShowSchedule
php artisan make:model Seat
php artisan make:model SeasonPackage
php artisan make:model Booking
php artisan make:model BookingSeat
php artisan make:model Review
```

๊ฐ ๋ชจ๋ธ์— ๋‹ค์Œ ๊ด€๊ณ„๋ฅผ ์ •์˜ํ•˜์„ธ์š”:

**User ๋ชจ๋ธ** (app/Models/User.php):
- `bookings()`: hasMany(Booking::class)
- `reviews()`: hasMany(Review::class)
- `membershipTierBadge()`: Accessor - ๋“ฑ๊ธ‰์— ๋”ฐ๋ผ ๋ฐฐ์ง€ ์•„์ด์ฝ˜ ๋ฐ˜ํ™˜
- `upgradeMembershipTier()`: ๋ฉ”์„œ๋“œ - total_spent ๊ธฐ์ค€์œผ๋กœ ๋“ฑ๊ธ‰ ์ž๋™ ์—…๊ทธ๋ ˆ์ด๋“œ (Bronze: 0์›, Silver: 50๋งŒ์›, Gold: 100๋งŒ์›)

**Season ๋ชจ๋ธ**:
- `shows()`: hasMany(Show::class)
- `packages()`: hasMany(SeasonPackage::class)
- `scopeActive()`: ์ฟผ๋ฆฌ ์Šค์ฝ”ํ”„ - is_active๊ฐ€ true์ธ ์‹œ์ฆŒ๋งŒ ์กฐํšŒ

**Show ๋ชจ๋ธ**:
- `season()`: belongsTo(Season::class)
- `schedules()`: hasMany(ShowSchedule::class)
- `reviews()`: hasMany(Review::class)
- `averageRating()`: Accessor - ๋ฆฌ๋ทฐ ๋ณ„์  ํ‰๊ท  ๊ณ„์‚ฐ

**ShowSchedule ๋ชจ๋ธ**:
- `show()`: belongsTo(Show::class)
- `seats()`: hasMany(Seat::class)
- `availableSeatsCount()`: Accessor - available_seats ๊ฐ’ ๋ฐ˜ํ™˜
- `isSoldOut()`: Accessor - available_seats๊ฐ€ 0์ด๋ฉด true

**Seat ๋ชจ๋ธ**:
- `schedule()`: belongsTo(ShowSchedule::class, 'show_schedule_id')
- `bookingSeats()`: hasMany(BookingSeat::class)
- `scopeAvailable()`: ์ฟผ๋ฆฌ ์Šค์ฝ”ํ”„ - status๊ฐ€ 'available'์ธ ์ขŒ์„๋งŒ ์กฐํšŒ
- `reserve()`: ๋ฉ”์„œ๋“œ - ์ขŒ์„ ์ƒํƒœ๋ฅผ 'reserved'๋กœ ๋ณ€๊ฒฝ
- `markAsSold()`: ๋ฉ”์„œ๋“œ - ์ขŒ์„ ์ƒํƒœ๋ฅผ 'sold'๋กœ ๋ณ€๊ฒฝ

**SeasonPackage ๋ชจ๋ธ**:
- `season()`: belongsTo(Season::class)
- `bookings()`: hasMany(Booking::class)
- `discountedPrice()`: Accessor - ํ• ์ธ ์ ์šฉ ํ›„ ๊ฐ€๊ฒฉ ๊ณ„์‚ฐ

**Booking ๋ชจ๋ธ**:
- `user()`: belongsTo(User::class)
- `seasonPackage()`: belongsTo(SeasonPackage::class)
- `bookingSeats()`: hasMany(BookingSeat::class)
- `seats()`: belongsToMany(Seat::class, 'booking_seats')->withPivot('attended')->withTimestamps()
- `generateBookingNumber()`: Static ๋ฉ”์„œ๋“œ - ์˜ˆ์•ฝ ๋ฒˆํ˜ธ ์ž๋™ ์ƒ์„ฑ (์˜ˆ: "BK-20250115-0001")
- `isPaid()`: Accessor - payment_status๊ฐ€ 'completed'์ธ์ง€ ํ™•์ธ

**BookingSeat ๋ชจ๋ธ**:
- `booking()`: belongsTo(Booking::class)
- `seat()`: belongsTo(Seat::class)

**Review ๋ชจ๋ธ**:
- `user()`: belongsTo(User::class)
- `show()`: belongsTo(Show::class)
- `scopeHighRated()`: ์ฟผ๋ฆฌ ์Šค์ฝ”ํ”„ - rating์ด 4 ์ด์ƒ์ธ ๋ฆฌ๋ทฐ๋งŒ ์กฐํšŒ

๊ฐ ๋ชจ๋ธ์— $fillable ์†์„ฑ์„ ์ •์˜ํ•˜์—ฌ ๋Œ€๋Ÿ‰ ํ• ๋‹น ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋ฅผ ๋ช…์‹œํ•˜์„ธ์š”.

๋˜ํ•œ, Booking ๋ชจ๋ธ์— ๋‹ค์Œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:
- `creating` ์ด๋ฒคํŠธ: booking_number ์ž๋™ ์ƒ์„ฑ
- `updated` ์ด๋ฒคํŠธ: payment_status๊ฐ€ 'completed'๋กœ ๋ณ€๊ฒฝ๋˜๋ฉด user์˜ total_spent ์—…๋ฐ์ดํŠธ ๋ฐ ๋“ฑ๊ธ‰ ๊ฐฑ์‹ 

Cast ์„ค์ •:
- ๋‚ ์งœ ํ•„๋“œ๋Š” ๋ชจ๋‘ Carbon ์ธ์Šคํ„ด์Šค๋กœ ์บ์ŠคํŒ…
- boolean ํ•„๋“œ๋Š” boolean ํƒ€์ž…์œผ๋กœ ์บ์ŠคํŒ…
- decimal ํ•„๋“œ๋Š” float ๋˜๋Š” decimal:2๋กœ ์บ์ŠคํŒ…

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: 8๊ฐœ์˜ Eloquent ๋ชจ๋ธ์ด ์ƒ์„ฑ๋˜๊ณ , ๋ชจ๋“  ๊ด€๊ณ„๊ฐ€ ์ •์˜๋ฉ๋‹ˆ๋‹ค. Accessor, Mutator, ์ฟผ๋ฆฌ ์Šค์ฝ”ํ”„๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋ชจ๋ธ ๋ ˆ๋ฒจ์—์„œ ์บก์Аํ™”๋ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ํ†ตํ•ด ์˜ˆ์•ฝ ๋ฒˆํ˜ธ ์ž๋™ ์ƒ์„ฑ ๋ฐ ํšŒ์› ๋“ฑ๊ธ‰ ์ž๋™ ์—…๊ทธ๋ ˆ์ด๋“œ ๋กœ์ง์ด ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค.

4
Step 4
์‹œ๋”(Seeder)๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•˜์„ธ์š”.

```bash
php artisan make:seeder DatabaseSeeder
```

DatabaseSeeder ํŒŒ์ผ(database/seeders/DatabaseSeeder.php)์—์„œ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”:

1. **๊ด€๋ฆฌ์ž ๊ณ„์ • 1๊ฐœ**
   - name: "๊ทน๋‹จ ๊ด€๋ฆฌ์ž"
   - email: "admin@theater.com"
   - password: bcrypt('password')
   - role: 'admin'

2. **์ผ๋ฐ˜ ์‚ฌ์šฉ์ž 10๋ช…**
   - ํŒฉํ† ๋ฆฌ ์‚ฌ์šฉ ๋˜๋Š” ์ˆ˜๋™ ์ƒ์„ฑ
   - role: 'user'
   - membership_tier๋Š” ๋žœ๋ค (bronze, silver, gold)

3. **์‹œ์ฆŒ 2๊ฐœ**
   - "2025 ๋ด„ ์‹œ์ฆŒ" (3์›”~5์›”)
   - "2025 ์—ฌ๋ฆ„ ์‹œ์ฆŒ" (6์›”~8์›”)
   - ๊ฐ ์‹œ์ฆŒ๋งˆ๋‹ค ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ์„ค์ • (public/images/seasons/spring.jpg ๋“ฑ)

4. **๊ฐ ์‹œ์ฆŒ๋งˆ๋‹ค ๊ณต์—ฐ 4๊ฐœ**
   - ์ด 8๊ฐœ ๊ณต์—ฐ ์ƒ์„ฑ
   - ์ œ๋ชฉ ์˜ˆ์‹œ: "ํ–„๋ฆฟ", "๋กœ๋ฏธ์˜ค์™€ ์ค„๋ฆฌ์—ฃ", "์˜คํŽ˜๋ผ์˜ ์œ ๋ น", "์บฃ์ธ " ๋“ฑ
   - ์žฅ๋ฅด: ์—ฐ๊ทน, ๋ฎค์ง€์ปฌ ์„ž์–ด์„œ
   - duration_minutes: 120~180 ์‚ฌ์ด ๋žœ๋ค

5. **๊ฐ ๊ณต์—ฐ๋งˆ๋‹ค ์Šค์ผ€์ค„ 3๊ฐœ**
   - ์ด 24๊ฐœ ์Šค์ผ€์ค„
   - show_date: ์‹œ์ฆŒ ๊ธฐ๊ฐ„ ๋‚ด ๋žœ๋ค ๋‚ ์งœ
   - show_time: 14:00, 19:00 ์ค‘ ๋žœ๋ค
   - venue: "๋Œ€๊ทน์žฅ", "์†Œ๊ทน์žฅ" ์ค‘ ๋žœ๋ค
   - total_seats: 100
   - available_seats: 100 (์ดˆ๊ธฐ๊ฐ’)

6. **๊ฐ ์Šค์ผ€์ค„๋งˆ๋‹ค ์ขŒ์„ 100๊ฐœ**
   - ์ด 2400๊ฐœ ์ขŒ์„
   - seat_number: "A-1"๋ถ€ํ„ฐ "J-10"๊นŒ์ง€ (10ํ–‰ x 10์—ด)
   - seat_type: ์•ž 3ํ–‰์€ 'vip', ๋‚˜๋จธ์ง€๋Š” 'regular'
   - price: VIP 80,000์›, ์ผ๋ฐ˜์„ 50,000์›
   - status: 'available'

7. **๊ฐ ์‹œ์ฆŒ๋งˆ๋‹ค ํŒจํ‚ค์ง€ 3๊ฐœ**
   - ์ด 6๊ฐœ ํŒจํ‚ค์ง€
   - ์˜ˆ์‹œ:
     - "์–ผ๋ฆฌ๋ฒ„๋“œ 4ํšŒ๊ถŒ" (show_count: 4, discount_rate: 20%)
     - "์ •๊ทœ 4ํšŒ๊ถŒ" (show_count: 4, discount_rate: 10%)
     - "VIP 4ํšŒ๊ถŒ" (show_count: 4, discount_rate: 15%, VIP ์ขŒ์„ ํฌํ•จ)

8. **์˜ˆ์•ฝ ๋ฐ์ดํ„ฐ 20๊ฐœ**
   - ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๋“ค์ด ๋žœ๋คํ•˜๊ฒŒ ์˜ˆ์•ฝ
   - ์ผ๋ถ€๋Š” ์‹œ์ฆŒ ํŒจํ‚ค์ง€ ๊ตฌ๋งค, ์ผ๋ถ€๋Š” ๊ฐœ๋ณ„ ์ขŒ์„ ๊ตฌ๋งค
   - payment_status: 'completed' ๋˜๋Š” 'pending'
   - booking_seats ํ…Œ์ด๋ธ”์—๋„ ์—ฐ๊ฒฐ

9. **๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ 30๊ฐœ**
   - ์˜ˆ์•ฝ ์™„๋ฃŒ๋œ ๊ณต์—ฐ์— ๋Œ€ํ•ด ๋žœ๋ค ๋ฆฌ๋ทฐ ์ƒ์„ฑ
   - rating: 1~5 ์‚ฌ์ด ๋žœ๋ค
   - comment: ์ƒ˜ํ”Œ ํ…์ŠคํŠธ

์‹œ๋” ์‹คํ–‰:
```bash
php artisan db:seed
```

๋˜๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ ํ•จ๊ป˜ ์‹คํ–‰:
```bash
php artisan migrate:fresh --seed
```

์‹œ๋” ์‹คํ–‰ ํ›„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋“ค์–ด๊ฐ”๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”:
```bash
php artisan tinker
>>> \App\Models\Season::count()
>>> \App\Models\Show::count()
>>> \App\Models\Booking::count()
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: DatabaseSeeder๊ฐ€ ์‹คํ–‰๋˜์–ด ๊ด€๋ฆฌ์ž ๊ณ„์ •, ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž, ์‹œ์ฆŒ, ๊ณต์—ฐ, ์Šค์ผ€์ค„, ์ขŒ์„, ํŒจํ‚ค์ง€, ์˜ˆ์•ฝ, ๋ฆฌ๋ทฐ ๋“ฑ ๋ชจ๋“  ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ์ƒํƒœ์—์„œ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5
Step 5
๋ผ์šฐํŠธ์™€ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์‚ฌ์šฉ์ž ํŽ˜์ด์ง€์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“œ์„ธ์š”.

routes/web.php ํŒŒ์ผ์— ๋‹ค์Œ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

```php
use App\Http\Controllers\HomeController;
use App\Http\Controllers\SeasonController;
use App\Http\Controllers\ShowController;
use App\Http\Controllers\BookingController;
use App\Http\Controllers\ReviewController;

// ํ™ˆํŽ˜์ด์ง€
Route::get('/', [HomeController::class, 'index'])->name('home');

// ์‹œ์ฆŒ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ
Route::get('/seasons', [SeasonController::class, 'index'])->name('seasons.index');
Route::get('/seasons/{season}', [SeasonController::class, 'show'])->name('seasons.show');

// ๊ณต์—ฐ ์ƒ์„ธ
Route::get('/shows/{show}', [ShowController::class, 'show'])->name('shows.show');

// ์˜ˆ์•ฝ ํ”„๋กœ์„ธ์Šค (๋กœ๊ทธ์ธ ํ•„์š”)
Route::middleware(['auth'])->group(function () {
    Route::get('/shows/{show}/book', [BookingController::class, 'create'])->name('bookings.create');
    Route::post('/bookings', [BookingController::class, 'store'])->name('bookings.store');
    Route::get('/bookings/{booking}', [BookingController::class, 'show'])->name('bookings.show');
    Route::get('/bookings/{booking}/ticket', [BookingController::class, 'downloadTicket'])->name('bookings.ticket');
    Route::get('/my-bookings', [BookingController::class, 'myBookings'])->name('bookings.my');
    
    // ๋ฆฌ๋ทฐ ์ž‘์„ฑ
    Route::post('/shows/{show}/reviews', [ReviewController::class, 'store'])->name('reviews.store');
});
```

์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ:
```bash
php artisan make:controller HomeController
php artisan make:controller SeasonController
php artisan make:controller ShowController
php artisan make:controller BookingController
php artisan make:controller ReviewController
```

๊ฐ ์ปจํŠธ๋กค๋Ÿฌ์— ๋‹ค์Œ ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”:

**HomeController**:
- `index()`: ํ™œ์„ฑ ์‹œ์ฆŒ ๋ชฉ๋ก, ์ธ๊ธฐ ๊ณต์—ฐ(๋ฆฌ๋ทฐ ํ‰์  ๋†’์€ ์ˆœ), ์ตœ๊ทผ ๋ฆฌ๋ทฐ 3๊ฐœ๋ฅผ ๋ทฐ์— ์ „๋‹ฌ

**SeasonController**:
- `index()`: ๋ชจ๋“  ํ™œ์„ฑ ์‹œ์ฆŒ ๋ชฉ๋ก ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜)
- `show(Season $season)`: ํŠน์ • ์‹œ์ฆŒ ์ƒ์„ธ ์ •๋ณด, ํฌํ•จ๋œ ๊ณต์—ฐ ๋ชฉ๋ก, ์‹œ์ฆŒ ํŒจํ‚ค์ง€ ๋ชฉ๋ก

**ShowController**:
- `show(Show $show)`: ๊ณต์—ฐ ์ƒ์„ธ ์ •๋ณด, ์Šค์ผ€์ค„ ๋ชฉ๋ก, ๋ฆฌ๋ทฐ ๋ชฉ๋ก(ํ‰์  ๋†’์€ ์ˆœ, ํŽ˜์ด์ง€๋„ค์ด์…˜), ํ‰๊ท  ๋ณ„์ 

**BookingController**:
- `create(Show $show)`: ๊ณต์—ฐ์˜ ์Šค์ผ€์ค„ ์„ ํƒ ๋ฐ ์ขŒ์„ ์„ ํƒ ํผ ํ‘œ์‹œ
- `store(Request $request)`: ์˜ˆ์•ฝ ์ •๋ณด ๊ฒ€์ฆ ๋ฐ ์ €์žฅ (ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ)
  - ์ขŒ์„ ์ค‘๋ณต ์˜ˆ์•ฝ ๋ฐฉ์ง€ (DB ๋ฝ ์‚ฌ์šฉ)
  - ์ขŒ์„ ์ƒํƒœ๋ฅผ 'reserved'๋กœ ๋ณ€๊ฒฝ
  - Booking ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ
  - BookingSeat ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ
  - ๊ฒฐ์ œ ๋Œ€๊ธฐ ์ƒํƒœ๋กœ ์„ค์ •
- `show(Booking $booking)`: ์˜ˆ์•ฝ ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ (Policy๋กœ ๋ณธ์ธ ์˜ˆ์•ฝ๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ)
- `downloadTicket(Booking $booking)`: PDF ํ‹ฐ์ผ“ ์ƒ์„ฑ ๋ฐ ๋‹ค์šด๋กœ๋“œ
- `myBookings()`: ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  ์˜ˆ์•ฝ ๋ชฉ๋ก

**ReviewController**:
- `store(Request $request, Show $show)`: ๋ฆฌ๋ทฐ ์ž‘์„ฑ (๋กœ๊ทธ์ธ + ํ•ด๋‹น ๊ณต์—ฐ ์˜ˆ์•ฝ ์ด๋ ฅ ํ™•์ธ)

Policy ์ƒ์„ฑ ๋ฐ ๋“ฑ๋ก:
```bash
php artisan make:policy BookingPolicy --model=Booking
```

BookingPolicy์— `view` ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€:
- ์˜ˆ์•ฝํ•œ ์‚ฌ์šฉ์ž ๋ณธ์ธ ๋˜๋Š” ๊ด€๋ฆฌ์ž๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ

app/Providers/AuthServiceProvider.php์— Policy ๋“ฑ๋ก:
```php
protected $policies = [
    Booking::class => BookingPolicy::class,
];
```

์ปจํŠธ๋กค๋Ÿฌ์—์„œ Policy ์‚ฌ์šฉ:
```php
$this->authorize('view', $booking);
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ผ์šฐํŠธ๊ฐ€ ์ •์˜๋˜๊ณ  5๊ฐœ์˜ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋ฉ”์„œ๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ทฐ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ธฐ๋ณธ ๋กœ์ง์ด ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. Policy๋ฅผ ํ†ตํ•ด ์˜ˆ์•ฝ ์กฐํšŒ ๊ถŒํ•œ์ด ์ œ์–ด๋ฉ๋‹ˆ๋‹ค.

6
Step 6
Blade ๋ทฐ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ์‚ฌ์šฉ์ž ํŽ˜์ด์ง€์˜ UI๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

Tailwind CSS์™€ Alpine.js๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ์„ ์ ์šฉํ•˜์„ธ์š”.

**๋ ˆ์ด์•„์›ƒ ํŒŒ์ผ** (resources/views/layouts/app.blade.php):
- Breeze์˜ ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ ํ™œ์šฉ ๋˜๋Š” ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
- ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”: ํ™ˆ, ์‹œ์ฆŒ, ๋‚ด ์˜ˆ์•ฝ, ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ
- ํ‘ธํ„ฐ ์ถ”๊ฐ€

**ํ™ˆํŽ˜์ด์ง€** (resources/views/home.blade.php):
- ํžˆ์–ด๋กœ ์„น์…˜: ๊ทน๋‹จ ์†Œ๊ฐœ, ์‹œ์ฆŒ ํ•˜์ด๋ผ์ดํŠธ ์ด๋ฏธ์ง€
- ํ™œ์„ฑ ์‹œ์ฆŒ ์นด๋“œ ๋ชฉ๋ก (์ตœ๋Œ€ 3๊ฐœ ํ‘œ์‹œ)
- ์ธ๊ธฐ ๊ณต์—ฐ ์„น์…˜: ํ‰์  ๋†’์€ ๊ณต์—ฐ 4๊ฐœ, ๋ณ„์  ์•„์ด์ฝ˜ ํ‘œ์‹œ
- ์ตœ๊ทผ ๋ฆฌ๋ทฐ ์„น์…˜: ์ตœ์‹  ๋ฆฌ๋ทฐ 3๊ฐœ, ์‚ฌ์šฉ์ž ์ด๋ฆ„ + ๋ณ„์  + ์ฝ”๋ฉ˜ํŠธ

**์‹œ์ฆŒ ๋ชฉ๋ก ํŽ˜์ด์ง€** (resources/views/seasons/index.blade.php):
- ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ์‹œ์ฆŒ ์นด๋“œ ํ‘œ์‹œ
- ๊ฐ ์นด๋“œ: ์‹œ์ฆŒ ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€, ์ œ๋ชฉ, ๊ธฐ๊ฐ„, ํฌํ•จ๋œ ๊ณต์—ฐ ์ˆ˜, "์ž์„ธํžˆ ๋ณด๊ธฐ" ๋ฒ„ํŠผ
- ํŽ˜์ด์ง€๋„ค์ด์…˜

**์‹œ์ฆŒ ์ƒ์„ธ ํŽ˜์ด์ง€** (resources/views/seasons/show.blade.php):
- ์‹œ์ฆŒ ์ •๋ณด: ์ œ๋ชฉ, ์„ค๋ช…, ๊ธฐ๊ฐ„
- ํฌํ•จ๋œ ๊ณต์—ฐ ๋ชฉ๋ก: ๊ฐ ๊ณต์—ฐ ์นด๋“œ (์ด๋ฏธ์ง€, ์ œ๋ชฉ, ์žฅ๋ฅด, ํ‰๊ท  ๋ณ„์ , "์˜ˆ์•ฝํ•˜๊ธฐ" ๋ฒ„ํŠผ)
- ์‹œ์ฆŒ ํŒจํ‚ค์ง€ ์„น์…˜: ํŒจํ‚ค์ง€ ์นด๋“œ (์ด๋ฆ„, ์„ค๋ช…, ๊ฐ€๊ฒฉ, ํ• ์ธ์œจ, "๊ตฌ๋งคํ•˜๊ธฐ" ๋ฒ„ํŠผ)

**๊ณต์—ฐ ์ƒ์„ธ ํŽ˜์ด์ง€** (resources/views/shows/show.blade.php):
- ๊ณต์—ฐ ์ •๋ณด: ์ œ๋ชฉ, ์„ค๋ช…, ์žฅ๋ฅด, ๋Ÿฌ๋‹ํƒ€์ž„, ํ‰๊ท  ๋ณ„์ 
- ์Šค์ผ€์ค„ ์„ ํƒ ์„น์…˜: ๋‚ ์งœ/์‹œ๊ฐ„๋ณ„ ์Šค์ผ€์ค„ ๋ชฉ๋ก, ์ž”์—ฌ ์ขŒ์„ ์ˆ˜ ํ‘œ์‹œ
  - ๋งค์ง„๋œ ์Šค์ผ€์ค„์€ "๋งค์ง„" ๋ฐฐ์ง€ ํ‘œ์‹œ
  - ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ ์Šค์ผ€์ค„์€ "์˜ˆ์•ฝํ•˜๊ธฐ" ๋ฒ„ํŠผ
- ๋ฆฌ๋ทฐ ์„น์…˜: ๋ฆฌ๋ทฐ ๋ชฉ๋ก (๋ณ„์ , ์ž‘์„ฑ์ž, ๋‚ ์งœ, ์ฝ”๋ฉ˜ํŠธ), ํŽ˜์ด์ง€๋„ค์ด์…˜
- ๋ฆฌ๋ทฐ ์ž‘์„ฑ ํผ (๋กœ๊ทธ์ธ + ํ•ด๋‹น ๊ณต์—ฐ ์˜ˆ์•ฝ ์ด๋ ฅ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ‘œ์‹œ)

**์˜ˆ์•ฝ ํŽ˜์ด์ง€** (resources/views/bookings/create.blade.php):
- ์„ ํƒํ•œ ์Šค์ผ€์ค„ ์ •๋ณด ํ‘œ์‹œ
- ์ขŒ์„ ์„ ํƒ UI:
  - ๊ทน์žฅ ์ขŒ์„ ๋ฐฐ์น˜๋„ (10ํ–‰ x 10์—ด ๊ทธ๋ฆฌ๋“œ)
  - ์ขŒ์„ ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ ๊ตฌ๋ถ„: ์˜ˆ์•ฝ ๊ฐ€๋Šฅ(์ดˆ๋ก), ์„ ํƒ๋จ(ํŒŒ๋ž‘), ๋งค์ง„(ํšŒ์ƒ‰)
  - Alpine.js๋กœ ์ขŒ์„ ํด๋ฆญ ์‹œ ์„ ํƒ/ํ•ด์ œ ํ† ๊ธ€
  - ์„ ํƒํ•œ ์ขŒ์„ ๋ชฉ๋ก ์‹ค์‹œ๊ฐ„ ํ‘œ์‹œ
- ์ด ๊ฒฐ์ œ ๊ธˆ์•ก ๊ณ„์‚ฐ (์„ ํƒํ•œ ์ขŒ์„ ๊ฐ€๊ฒฉ ํ•ฉ๊ณ„)
- "๊ฒฐ์ œํ•˜๊ธฐ" ๋ฒ„ํŠผ (์„ ํƒํ•œ ์ขŒ์„์ด ์—†์œผ๋ฉด ๋น„ํ™œ์„ฑํ™”)

Alpine.js ์ขŒ์„ ์„ ํƒ ๋กœ์ง ์˜ˆ์‹œ:
```html
<div x-data="{
    selectedSeats: [],
    toggleSeat(seatId, price) {
        const index = this.selectedSeats.findIndex(s => s.id === seatId);
        if (index > -1) {
            this.selectedSeats.splice(index, 1);
        } else {
            this.selectedSeats.push({ id: seatId, price: price });
        }
    },
    totalPrice() {
        return this.selectedSeats.reduce((sum, seat) => sum + seat.price, 0);
    }
}">
    <!-- ์ขŒ์„ ๊ทธ๋ฆฌ๋“œ -->
    @foreach($seats as $seat)
        <button 
            @click="toggleSeat({{ $seat->id }}, {{ $seat->price }})"
            :class="selectedSeats.some(s => s.id === {{ $seat->id }}) ? 'bg-blue-500' : 'bg-green-500'"
            :disabled="'{{ $seat->status }}' !== 'available'"
            class="w-10 h-10 m-1 rounded"
        >
            {{ $seat->seat_number }}
        </button>
    @endforeach
    
    <!-- ์„ ํƒํ•œ ์ขŒ์„ ๋ชฉ๋ก -->
    <div>
        <h3>์„ ํƒํ•œ ์ขŒ์„:</h3>
        <template x-for="seat in selectedSeats" :key="seat.id">
            <p x-text="'์ขŒ์„ ID: ' + seat.id + ', ๊ฐ€๊ฒฉ: ' + seat.price + '์›'"></p>
        </template>
        <p><strong>์ด ๊ธˆ์•ก: <span x-text="totalPrice()"></span>์›</strong></p>
    </div>
    
    <!-- ํผ ์ œ์ถœ -->
    <form method="POST" action="{{ route('bookings.store') }}">
        @csrf
        <input type="hidden" name="schedule_id" value="{{ $schedule->id }}">
        <template x-for="seat in selectedSeats" :key="seat.id">
            <input type="hidden" name="seat_ids[]" :value="seat.id">
        </template>
        <button type="submit" :disabled="selectedSeats.length === 0">๊ฒฐ์ œํ•˜๊ธฐ</button>
    </form>
</div>
```

**์˜ˆ์•ฝ ์™„๋ฃŒ ํŽ˜์ด์ง€** (resources/views/bookings/show.blade.php):
- ์˜ˆ์•ฝ ๋ฒˆํ˜ธ, ๊ณต์—ฐ ์ •๋ณด, ์„ ํƒํ•œ ์ขŒ์„, ๊ฒฐ์ œ ๊ธˆ์•ก
- "PDF ํ‹ฐ์ผ“ ๋‹ค์šด๋กœ๋“œ" ๋ฒ„ํŠผ
- ๊ฒฐ์ œ ์ƒํƒœ ํ‘œ์‹œ (๋Œ€๊ธฐ ์ค‘/์™„๋ฃŒ/์‹คํŒจ)

**๋‚ด ์˜ˆ์•ฝ ๋ชฉ๋ก ํŽ˜์ด์ง€** (resources/views/bookings/my.blade.php):
- ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  ์˜ˆ์•ฝ ๋ชฉ๋ก
- ๊ฐ ์˜ˆ์•ฝ: ๊ณต์—ฐ ์ œ๋ชฉ, ๋‚ ์งœ, ์ขŒ์„, ๊ฒฐ์ œ ์ƒํƒœ, "์ƒ์„ธ ๋ณด๊ธฐ" ๋ฒ„ํŠผ
- ํŽ˜์ด์ง€๋„ค์ด์…˜

๋ชจ๋“  ๋ทฐ์—์„œ Tailwind CSS ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํƒ€์ผ๋งํ•˜์„ธ์š”.
๋ฐ˜์‘ํ˜• ๋””์ž์ธ: sm:, md:, lg: ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ํ™œ์šฉ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Blade ๋ทฐ ํŒŒ์ผ๋“ค์ด ์ƒ์„ฑ๋˜๊ณ , ํ™ˆํŽ˜์ด์ง€, ์‹œ์ฆŒ ๋ชฉ๋ก/์ƒ์„ธ, ๊ณต์—ฐ ์ƒ์„ธ, ์˜ˆ์•ฝ ํŽ˜์ด์ง€, ๋‚ด ์˜ˆ์•ฝ ํŽ˜์ด์ง€ ๋“ฑ ๋ชจ๋“  ์‚ฌ์šฉ์ž ํŽ˜์ด์ง€์˜ UI๊ฐ€ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค. Alpine.js๋ฅผ ํ™œ์šฉํ•œ ์ขŒ์„ ์„ ํƒ ์ธํ„ฐ๋ž™์…˜์ด ๊ตฌํ˜„๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์ง๊ด€์ ์œผ๋กœ ์ขŒ์„์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Tailwind CSS๋กœ ๊น”๋”ํ•˜๊ณ  ๋ฐ˜์‘ํ˜•์ธ ๋””์ž์ธ์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

7
Step 7
๊ฒฐ์ œ ์—ฐ๋™ ๋ฐ ์˜ˆ์•ฝ ์™„๋ฃŒ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

Stripe API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

1. **Stripe ์„ค์ •**:
   - .env ํŒŒ์ผ์— Stripe API ํ‚ค ์ถ”๊ฐ€:
     ```
     STRIPE_KEY=pk_test_xxxxx
     STRIPE_SECRET=sk_test_xxxxx
     ```
   - config/services.php์— Stripe ์„ค์ • ์ถ”๊ฐ€:
     ```php
     'stripe' => [
         'key' => env('STRIPE_KEY'),
         'secret' => env('STRIPE_SECRET'),
     ],
     ```

2. **๊ฒฐ์ œ ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ**:
   ```bash
   php artisan make:controller PaymentController
   ```

3. **๋ผ์šฐํŠธ ์ถ”๊ฐ€** (routes/web.php):
   ```php
   Route::middleware(['auth'])->group(function () {
       Route::post('/payment/process', [PaymentController::class, 'process'])->name('payment.process');
       Route::get('/payment/success', [PaymentController::class, 'success'])->name('payment.success');
       Route::get('/payment/cancel', [PaymentController::class, 'cancel'])->name('payment.cancel');
   });
   ```

4. **PaymentController ๊ตฌํ˜„**:
   - `process()` ๋ฉ”์„œ๋“œ:
     - ์˜ˆ์•ฝ ์ •๋ณด๋ฅผ ์„ธ์…˜์— ์ €์žฅ
     - Stripe Checkout Session ์ƒ์„ฑ
     - Checkout URL๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
   
   ```php
   use Stripe\Stripe;
   use Stripe\Checkout\Session;
   
   public function process(Request $request)
   {
       Stripe::setApiKey(config('services.stripe.secret'));
       
       // ์˜ˆ์•ฝ ์ •๋ณด ๊ฒ€์ฆ
       $validated = $request->validate([
           'schedule_id' => 'required|exists:show_schedules,id',
           'seat_ids' => 'required|array|min:1',
           'seat_ids.*' => 'exists:seats,id',
       ]);
       
       $schedule = ShowSchedule::findOrFail($validated['schedule_id']);
       $seats = Seat::whereIn('id', $validated['seat_ids'])
                    ->where('status', 'available')
                    ->lockForUpdate()
                    ->get();
       
       // ์ขŒ์„์ด ๋ชจ๋‘ ์˜ˆ์•ฝ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ
       if ($seats->count() !== count($validated['seat_ids'])) {
           return back()->withErrors(['seats' => '์„ ํƒํ•œ ์ขŒ์„ ์ค‘ ์ผ๋ถ€๊ฐ€ ์ด๋ฏธ ์˜ˆ์•ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.']);
       }
       
       // ์ด ๊ธˆ์•ก ๊ณ„์‚ฐ
       $totalAmount = $seats->sum('price');
       
       // ์˜ˆ์•ฝ ์ •๋ณด ์„ธ์…˜์— ์ €์žฅ
       session([
           'pending_booking' => [
               'schedule_id' => $schedule->id,
               'seat_ids' => $validated['seat_ids'],
               'total_amount' => $totalAmount,
           ]
       ]);
       
       // Stripe Checkout Session ์ƒ์„ฑ
       $checkoutSession = Session::create([
           'payment_method_types' => ['card'],
           'line_items' => [[
               'price_data' => [
                   'currency' => 'krw',
                   'product_data' => [
                       'name' => $schedule->show->title . ' - ' . $schedule->show_date,
                   ],
                   'unit_amount' => $totalAmount,
               ],
               'quantity' => 1,
           ]],
           'mode' => 'payment',
           'success_url' => route('payment.success') . '?session_id={CHECKOUT_SESSION_ID}',
           'cancel_url' => route('payment.cancel'),
       ]);
       
       return redirect($checkoutSession->url);
   }
   ```
   
   - `success()` ๋ฉ”์„œ๋“œ:
     - Stripe ๊ฒฐ์ œ ์„ฑ๊ณต ํ™•์ธ
     - DB ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์˜ˆ์•ฝ ์ƒ์„ฑ ๋ฐ ์ขŒ์„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
     - ์‚ฌ์šฉ์ž total_spent ์—…๋ฐ์ดํŠธ ๋ฐ ๋“ฑ๊ธ‰ ๊ฐฑ์‹ 
     - ์˜ˆ์•ฝ ์™„๋ฃŒ ์ด๋ฉ”์ผ ๋ฐœ์†ก (Queue ์‚ฌ์šฉ)
   
   ```php
   public function success(Request $request)
   {
       Stripe::setApiKey(config('services.stripe.secret'));
       
       $sessionId = $request->query('session_id');
       $checkoutSession = Session::retrieve($sessionId);
       
       if ($checkoutSession->payment_status !== 'paid') {
           return redirect()->route('home')->withErrors(['payment' => '๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.']);
       }
       
       $pendingBooking = session('pending_booking');
       if (!$pendingBooking) {
           return redirect()->route('home')->withErrors(['booking' => '์˜ˆ์•ฝ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.']);
       }
       
       DB::transaction(function () use ($pendingBooking, $sessionId) {
           // ์ขŒ์„ ๋ฝ ๋ฐ ์ƒํƒœ ํ™•์ธ
           $seats = Seat::whereIn('id', $pendingBooking['seat_ids'])
                        ->where('status', 'available')
                        ->lockForUpdate()
                        ->get();
           
           if ($seats->count() !== count($pendingBooking['seat_ids'])) {
               throw new \Exception('์ขŒ์„์ด ์ด๋ฏธ ์˜ˆ์•ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
           }
           
           // ์˜ˆ์•ฝ ์ƒ์„ฑ
           $booking = Booking::create([
               'user_id' => auth()->id(),
               'booking_number' => Booking::generateBookingNumber(),
               'total_amount' => $pendingBooking['total_amount'],
               'payment_status' => 'completed',
               'payment_method' => 'card',
               'stripe_payment_id' => $sessionId,
           ]);
           
           // ์ขŒ์„ ์˜ˆ์•ฝ ๋ฐ ์ƒํƒœ ๋ณ€๊ฒฝ
           foreach ($seats as $seat) {
               BookingSeat::create([
                   'booking_id' => $booking->id,
                   'seat_id' => $seat->id,
               ]);
               $seat->markAsSold();
           }
           
           // ์Šค์ผ€์ค„์˜ ๋‚จ์€ ์ขŒ์„ ์ˆ˜ ์—…๋ฐ์ดํŠธ
           $schedule = ShowSchedule::find($pendingBooking['schedule_id']);
           $schedule->decrement('available_seats', $seats->count());
           
           // ์‚ฌ์šฉ์ž ๋ˆ„์  ๊ตฌ๋งค ๊ธˆ์•ก ์—…๋ฐ์ดํŠธ
           $user = auth()->user();
           $user->increment('total_spent', $pendingBooking['total_amount']);
           $user->upgradeMembershipTier();
           
           // ์˜ˆ์•ฝ ์™„๋ฃŒ ์ด๋ฉ”์ผ ๋ฐœ์†ก (Queue)
           // TODO: ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„
       });
       
       session()->forget('pending_booking');
       
       return redirect()->route('bookings.my')->with('success', '์˜ˆ์•ฝ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
   }
   ```
   
   - `cancel()` ๋ฉ”์„œ๋“œ:
     - ๊ฒฐ์ œ ์ทจ์†Œ ์ฒ˜๋ฆฌ
     - ์„ธ์…˜ ์ •๋ณด ์‚ญ์ œ

5. **์˜ˆ์•ฝ ํŽ˜์ด์ง€ ์ˆ˜์ •**:
   - bookings/create.blade.php์˜ ํผ ์•ก์…˜์„ `route('payment.process')`๋กœ ๋ณ€๊ฒฝ
   - ๊ฒฐ์ œํ•˜๊ธฐ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ Stripe Checkout์œผ๋กœ ์ด๋™

6. **ํŠธ๋žœ์žญ์…˜ ๋ฐ ๋ฝ ์ฒ˜๋ฆฌ**:
   - ์ขŒ์„ ์ค‘๋ณต ์˜ˆ์•ฝ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด `lockForUpdate()` ์‚ฌ์šฉ
   - DB::transaction()์œผ๋กœ ์›์ž์„ฑ ๋ณด์žฅ

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

8
Step 8
Queue๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ˆ์•ฝ ์™„๋ฃŒ ์ด๋ฉ”์ผ ์•Œ๋ฆผ์„ ๋น„๋™๊ธฐ๋กœ ๋ฐœ์†กํ•˜๊ณ , PDF ํ‹ฐ์ผ“์„ ์ƒ์„ฑํ•˜์„ธ์š”.

1. **Queue ์„ค์ •**:
   - .env ํŒŒ์ผ์—์„œ QUEUE_CONNECTION์„ database๋กœ ์„ค์ • (์ด๋ฏธ ์„ค์ •๋จ)
   - Queue ํ…Œ์ด๋ธ” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ ๋ฐ ์‹คํ–‰:
     ```bash
     php artisan queue:table
     php artisan migrate
     ```

2. **๋ฉ”์ผ ์„ค์ •**:
   - .env ํŒŒ์ผ์— ๋ฉ”์ผ ์„ค์ • ์ถ”๊ฐ€ (ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ Mailtrap ๋˜๋Š” log ๋“œ๋ผ์ด๋ฒ„ ์‚ฌ์šฉ):
     ```
     MAIL_MAILER=log
     MAIL_FROM_ADDRESS=noreply@theater.com
     MAIL_FROM_NAME="๊ทน๋‹จ ํ‹ฐ์ผ“"
     ```

3. **Mailable ํด๋ž˜์Šค ์ƒ์„ฑ**:
   ```bash
   php artisan make:mail BookingConfirmation
   ```
   
   app/Mail/BookingConfirmation.php:
   ```php
   namespace App\Mail;
   
   use App\Models\Booking;
   use Illuminate\Bus\Queueable;
   use Illuminate\Mail\Mailable;
   use Illuminate\Queue\SerializesModels;
   
   class BookingConfirmation extends Mailable
   {
       use Queueable, SerializesModels;
       
       public $booking;
       
       public function __construct(Booking $booking)
       {
           $this->booking = $booking;
       }
       
       public function build()
       {
           return $this->subject('์˜ˆ์•ฝ ์™„๋ฃŒ ์•ˆ๋‚ด')
                       ->markdown('emails.booking-confirmation');
       }
   }
   ```

4. **๋ฉ”์ผ ๋ทฐ ์ƒ์„ฑ** (resources/views/emails/booking-confirmation.blade.php):
   ```blade
   @component('mail::message')
   # ์˜ˆ์•ฝ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!
   
   ์•ˆ๋…•ํ•˜์„ธ์š” {{ $booking->user->name }}๋‹˜,
   
   ์˜ˆ์•ฝ ๋ฒˆํ˜ธ: **{{ $booking->booking_number }}**
   
   ## ์˜ˆ์•ฝ ์ •๋ณด
   - ๊ณต์—ฐ: {{ $booking->seats->first()->schedule->show->title }}
   - ๋‚ ์งœ: {{ $booking->seats->first()->schedule->show_date }}
   - ์‹œ๊ฐ„: {{ $booking->seats->first()->schedule->show_time }}
   - ์ขŒ์„: {{ $booking->seats->pluck('seat_number')->join(', ') }}
   - ์ด ๊ธˆ์•ก: {{ number_format($booking->total_amount) }}์›
   
   @component('mail::button', ['url' => route('bookings.show', $booking)])
   ์˜ˆ์•ฝ ์ƒ์„ธ ๋ณด๊ธฐ
   @endcomponent
   
   ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค,<br>
   {{ config('app.name') }}
   @endcomponent
   ```

5. **Job ํด๋ž˜์Šค ์ƒ์„ฑ**:
   ```bash
   php artisan make:job SendBookingConfirmationEmail
   ```
   
   app/Jobs/SendBookingConfirmationEmail.php:
   ```php
   namespace App\Jobs;
   
   use App\Models\Booking;
   use App\Mail\BookingConfirmation;
   use Illuminate\Bus\Queueable;
   use Illuminate\Contracts\Queue\ShouldQueue;
   use Illuminate\Foundation\Bus\Dispatchable;
   use Illuminate\Queue\InteractsWithQueue;
   use Illuminate\Queue\SerializesModels;
   use Illuminate\Support\Facades\Mail;
   
   class SendBookingConfirmationEmail implements ShouldQueue
   {
       use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
       
       public $booking;
       
       public function __construct(Booking $booking)
       {
           $this->booking = $booking;
       }
       
       public function handle()
       {
           Mail::to($this->booking->user->email)
               ->send(new BookingConfirmation($this->booking));
       }
   }
   ```

6. **PaymentController์˜ success() ๋ฉ”์„œ๋“œ ์ˆ˜์ •**:
   - ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์—์„œ Job ๋””์ŠคํŒจ์น˜:
     ```php
     // ์˜ˆ์•ฝ ์™„๋ฃŒ ์ด๋ฉ”์ผ ๋ฐœ์†ก (Queue)
     SendBookingConfirmationEmail::dispatch($booking);
     ```

7. **Queue Worker ์‹คํ–‰**:
   ```bash
   php artisan queue:work
   ```
   - ๋˜๋Š” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” `php artisan queue:listen`

8. **PDF ํ‹ฐ์ผ“ ์ƒ์„ฑ**:
   - BookingController์˜ `downloadTicket()` ๋ฉ”์„œ๋“œ ๊ตฌํ˜„:
   
   ```php
   use Barryvdh\DomPDF\Facade\Pdf;
   
   public function downloadTicket(Booking $booking)
   {
       $this->authorize('view', $booking);
       
       if ($booking->payment_status !== 'completed') {
           return back()->withErrors(['ticket' => '๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋œ ์˜ˆ์•ฝ๋งŒ ํ‹ฐ์ผ“์„ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.']);
       }
       
       $pdf = Pdf::loadView('bookings.ticket', compact('booking'));
       
       return $pdf->download('ticket-' . $booking->booking_number . '.pdf');
   }
   ```

9. **PDF ํ‹ฐ์ผ“ ๋ทฐ ์ƒ์„ฑ** (resources/views/bookings/ticket.blade.php):
   ```blade
   <!DOCTYPE html>
   <html>
   <head>
       <meta charset="utf-8">
       <title>ํ‹ฐ์ผ“ - {{ $booking->booking_number }}</title>
       <style>
           body { font-family: DejaVu Sans, sans-serif; }
           .ticket { border: 2px solid #000; padding: 20px; }
           .qr-code { text-align: center; }
       </style>
   </head>
   <body>
       <div class="ticket">
           <h1>๊ณต์—ฐ ํ‹ฐ์ผ“</h1>
           <p><strong>์˜ˆ์•ฝ ๋ฒˆํ˜ธ:</strong> {{ $booking->booking_number }}</p>
           <p><strong>๊ณต์—ฐ:</strong> {{ $booking->seats->first()->schedule->show->title }}</p>
           <p><strong>๋‚ ์งœ:</strong> {{ $booking->seats->first()->schedule->show_date }}</p>
           <p><strong>์‹œ๊ฐ„:</strong> {{ $booking->seats->first()->schedule->show_time }}</p>
           <p><strong>์ขŒ์„:</strong> {{ $booking->seats->pluck('seat_number')->join(', ') }}</p>
           <p><strong>๊ด€๋žŒ๊ฐ:</strong> {{ $booking->user->name }}</p>
           
           <div class="qr-code">
               <!-- QR ์ฝ”๋“œ ์ƒ์„ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ (์˜ˆ: SimpleSoftwareIO/simple-qrcode) -->
               <p>[QR ์ฝ”๋“œ ์œ„์น˜]</p>
               <p>{{ $booking->booking_number }}</p>
           </div>
       </div>
   </body>
   </html>
   ```

10. **QR ์ฝ”๋“œ ์ƒ์„ฑ (์„ ํƒ์‚ฌํ•ญ)**:
    - QR ์ฝ”๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜:
      ```bash
      composer require simplesoftwareio/simple-qrcode
      ```
    - ํ‹ฐ์ผ“ ๋ทฐ์—์„œ QR ์ฝ”๋“œ ์ƒ์„ฑ:
      ```blade
      {!! QrCode::size(200)->generate($booking->booking_number) !!}
      ```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Queue ์‹œ์Šคํ…œ์ด ์„ค์ •๋˜๊ณ , ์˜ˆ์•ฝ ์™„๋ฃŒ ์‹œ ์ด๋ฉ”์ผ์ด ๋น„๋™๊ธฐ๋กœ ๋ฐœ์†ก๋ฉ๋‹ˆ๋‹ค. Queue Worker๋ฅผ ์‹คํ–‰ํ•˜๋ฉด jobs ํ…Œ์ด๋ธ”์— ์Œ“์ธ ์ž‘์—…์ด ์ฒ˜๋ฆฌ๋˜์–ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ˆ์•ฝ ํ™•์ธ ์ด๋ฉ”์ผ์ด ์ „์†ก๋ฉ๋‹ˆ๋‹ค. PDF ํ‹ฐ์ผ“ ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ์ด ๊ตฌํ˜„๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์˜ˆ์•ฝ ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ํ‹ฐ์ผ“์„ PDF๋กœ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. QR ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ํ‹ฐ์ผ“์€ ํ˜„์žฅ์—์„œ ์ž…์žฅ ํ™•์ธ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

9
Step 9
๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ์‹œ์ฆŒ/๊ณต์—ฐ/์ขŒ์„ ๊ด€๋ฆฌ ๋ฐ ํ†ต๊ณ„ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.

1. **๊ด€๋ฆฌ์ž ๋ฏธ๋“ค์›จ์–ด ์ƒ์„ฑ**:
   ```bash
   php artisan make:middleware IsAdmin
   ```
   
   app/Http/Middleware/IsAdmin.php:
   ```php
   public function handle(Request $request, Closure $next)
   {
       if (!auth()->check() || auth()->user()->role !== 'admin') {
           abort(403, '๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
       }
       return $next($request);
   }
   ```
   
   app/Http/Kernel.php์— ๋ฏธ๋“ค์›จ์–ด ๋“ฑ๋ก:
   ```php
   protected $middlewareAliases = [
       // ...
       'admin' => \App\Http\Middleware\IsAdmin::class,
   ];
   ```

2. **๊ด€๋ฆฌ์ž ๋ผ์šฐํŠธ ์ถ”๊ฐ€** (routes/web.php):
   ```php
   use App\Http\Controllers\Admin\DashboardController;
   use App\Http\Controllers\Admin\SeasonManageController;
   use App\Http\Controllers\Admin\ShowManageController;
   use App\Http\Controllers\Admin\BookingManageController;
   
   Route::prefix('admin')->middleware(['auth', 'admin'])->name('admin.')->group(function () {
       Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
       
       Route::resource('seasons', SeasonManageController::class);
       Route::resource('shows', ShowManageController::class);
       Route::get('bookings', [BookingManageController::class, 'index'])->name('bookings.index');
       Route::patch('bookings/{booking}/refund', [BookingManageController::class, 'refund'])->name('bookings.refund');
   });
   ```

3. **๊ด€๋ฆฌ์ž ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ**:
   ```bash
   php artisan make:controller Admin/DashboardController
   php artisan make:controller Admin/SeasonManageController --resource
   php artisan make:controller Admin/ShowManageController --resource
   php artisan make:controller Admin/BookingManageController
   ```

4. **DashboardController ๊ตฌํ˜„**:
   - `index()` ๋ฉ”์„œ๋“œ:
     - ์ „์ฒด ์˜ˆ์•ฝ ์ˆ˜, ์ด ๋งค์ถœ, ํ‰๊ท  ์˜ˆ์•ฝ ๊ธˆ์•ก
     - ์˜ค๋Š˜/์ด๋ฒˆ ์ฃผ/์ด๋ฒˆ ๋‹ฌ ์˜ˆ์•ฝ ํ†ต๊ณ„
     - ์ธ๊ธฐ ๊ณต์—ฐ Top 5 (์˜ˆ์•ฝ ์ˆ˜ ๊ธฐ์ค€)
     - ์ตœ๊ทผ ์˜ˆ์•ฝ 10๊ฑด
     - ์ขŒ์„ ์ ์œ ์œจ ์ฐจํŠธ ๋ฐ์ดํ„ฐ (Chart.js์šฉ JSON)
   
   ```php
   public function index()
   {
       $totalBookings = Booking::where('payment_status', 'completed')->count();
       $totalRevenue = Booking::where('payment_status', 'completed')->sum('total_amount');
       $avgBookingAmount = $totalBookings > 0 ? $totalRevenue / $totalBookings : 0;
       
       $todayBookings = Booking::whereDate('created_at', today())->count();
       $weekBookings = Booking::whereBetween('created_at', [now()->startOfWeek(), now()->endOfWeek()])->count();
       $monthBookings = Booking::whereMonth('created_at', now()->month)->count();
       
       $topShows = Show::withCount('schedules.seats.bookingSeats')
                       ->orderBy('schedules_seats_booking_seats_count', 'desc')
                       ->take(5)
                       ->get();
       
       $recentBookings = Booking::with('user', 'seats.schedule.show')
                                ->latest()
                                ->take(10)
                                ->get();
       
       // ์ขŒ์„ ์ ์œ ์œจ ์ฐจํŠธ ๋ฐ์ดํ„ฐ
       $occupancyData = ShowSchedule::selectRaw('show_id, SUM(total_seats - available_seats) as sold, SUM(total_seats) as total')
                                    ->groupBy('show_id')
                                    ->with('show')
                                    ->get()
                                    ->map(function ($schedule) {
                                        return [
                                            'show' => $schedule->show->title,
                                            'occupancy' => $schedule->total > 0 ? round(($schedule->sold / $schedule->total) * 100, 2) : 0,
                                        ];
                                    });
       
       return view('admin.dashboard', compact(
           'totalBookings', 'totalRevenue', 'avgBookingAmount',
           'todayBookings', 'weekBookings', 'monthBookings',
           'topShows', 'recentBookings', 'occupancyData'
       ));
   }
   ```

5. **SeasonManageController ๊ตฌํ˜„**:
   - `index()`: ์‹œ์ฆŒ ๋ชฉ๋ก (ํŽ˜์ด์ง€๋„ค์ด์…˜)
   - `create()`: ์‹œ์ฆŒ ์ƒ์„ฑ ํผ
   - `store()`: ์‹œ์ฆŒ ์ €์žฅ (์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ํฌํ•จ)
   - `edit()`: ์‹œ์ฆŒ ์ˆ˜์ • ํผ
   - `update()`: ์‹œ์ฆŒ ์—…๋ฐ์ดํŠธ
   - `destroy()`: ์‹œ์ฆŒ ์‚ญ์ œ (์—ฐ๊ด€๋œ ๊ณต์—ฐ๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋จ)

6. **ShowManageController ๊ตฌํ˜„**:
   - `index()`: ๊ณต์—ฐ ๋ชฉ๋ก (์‹œ์ฆŒ๋ณ„ ํ•„ํ„ฐ๋ง ๊ฐ€๋Šฅ)
   - `create()`: ๊ณต์—ฐ ์ƒ์„ฑ ํผ (์‹œ์ฆŒ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด)
   - `store()`: ๊ณต์—ฐ ์ €์žฅ (์Šค์ผ€์ค„ ๋ฐ ์ขŒ์„ ์ž๋™ ์ƒ์„ฑ)
   - `edit()`: ๊ณต์—ฐ ์ˆ˜์ • ํผ
   - `update()`: ๊ณต์—ฐ ์—…๋ฐ์ดํŠธ
   - `destroy()`: ๊ณต์—ฐ ์‚ญ์ œ

7. **BookingManageController ๊ตฌํ˜„**:
   - `index()`: ์˜ˆ์•ฝ ๋ชฉ๋ก (๊ฒ€์ƒ‰/ํ•„ํ„ฐ๋ง: ์‚ฌ์šฉ์ž๋ช…, ๊ณต์—ฐ๋ช…, ๊ฒฐ์ œ ์ƒํƒœ)
   - `refund()`: ์˜ˆ์•ฝ ํ™˜๋ถˆ ์ฒ˜๋ฆฌ (์ขŒ์„ ์ƒํƒœ 'available'๋กœ ๋ณต๊ตฌ, ๊ฒฐ์ œ ์ƒํƒœ 'refunded'๋กœ ๋ณ€๊ฒฝ)

8. **๊ด€๋ฆฌ์ž ๋ทฐ ์ƒ์„ฑ**:
   - resources/views/admin/dashboard.blade.php:
     - ํ†ต๊ณ„ ์นด๋“œ (์ด ์˜ˆ์•ฝ, ์ด ๋งค์ถœ, ํ‰๊ท  ๊ธˆ์•ก)
     - ์˜ค๋Š˜/์ด๋ฒˆ ์ฃผ/์ด๋ฒˆ ๋‹ฌ ์˜ˆ์•ฝ ์ˆ˜
     - ์ธ๊ธฐ ๊ณต์—ฐ Top 5 ๋ฆฌ์ŠคํŠธ
     - ์ตœ๊ทผ ์˜ˆ์•ฝ ํ…Œ์ด๋ธ”
     - Chart.js๋กœ ์ขŒ์„ ์ ์œ ์œจ ์ฐจํŠธ ํ‘œ์‹œ
   
   - resources/views/admin/seasons/index.blade.php:
     - ์‹œ์ฆŒ ๋ชฉ๋ก ํ…Œ์ด๋ธ” (์ œ๋ชฉ, ๊ธฐ๊ฐ„, ๊ณต์—ฐ ์ˆ˜, ์•ก์…˜ ๋ฒ„ํŠผ)
     - "์ƒˆ ์‹œ์ฆŒ ์ถ”๊ฐ€" ๋ฒ„ํŠผ
   
   - resources/views/admin/seasons/create.blade.php:
     - ์‹œ์ฆŒ ์ƒ์„ฑ ํผ (์ œ๋ชฉ, ์„ค๋ช…, ์‹œ์ž‘/์ข…๋ฃŒ ๋‚ ์งœ, ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ)
   
   - resources/views/admin/shows/index.blade.php:
     - ๊ณต์—ฐ ๋ชฉ๋ก ํ…Œ์ด๋ธ” (์ œ๋ชฉ, ์‹œ์ฆŒ, ์žฅ๋ฅด, ์Šค์ผ€์ค„ ์ˆ˜, ์•ก์…˜ ๋ฒ„ํŠผ)
   
   - resources/views/admin/bookings/index.blade.php:
     - ์˜ˆ์•ฝ ๋ชฉ๋ก ํ…Œ์ด๋ธ” (์˜ˆ์•ฝ ๋ฒˆํ˜ธ, ์‚ฌ์šฉ์ž, ๊ณต์—ฐ, ๊ฒฐ์ œ ์ƒํƒœ, ๊ธˆ์•ก, ํ™˜๋ถˆ ๋ฒ„ํŠผ)

9. **Chart.js ํ†ตํ•ฉ**:
   - resources/views/admin/dashboard.blade.php์— Chart.js CDN ์ถ”๊ฐ€:
     ```html
     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
     <script>
         const ctx = document.getElementById('occupancyChart').getContext('2d');
         const occupancyData = @json($occupancyData);
         
         new Chart(ctx, {
             type: 'bar',
             data: {
                 labels: occupancyData.map(d => d.show),
                 datasets: [{
                     label: '์ขŒ์„ ์ ์œ ์œจ (%)',
                     data: occupancyData.map(d => d.occupancy),
                     backgroundColor: 'rgba(54, 162, 235, 0.5)',
                     borderColor: 'rgba(54, 162, 235, 1)',
                     borderWidth: 1
                 }]
             },
             options: {
                 scales: {
                     y: {
                         beginAtZero: true,
                         max: 100
                     }
                 }
             }
         });
     </script>
     ```

10. **๋„ค๋น„๊ฒŒ์ด์…˜์— ๊ด€๋ฆฌ์ž ๋งํฌ ์ถ”๊ฐ€**:
    - layouts/app.blade.php์—์„œ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ด€๋ฆฌ์ž์ธ ๊ฒฝ์šฐ "๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ" ๋งํฌ ํ‘œ์‹œ:
      ```blade
      @auth
          @if(auth()->user()->role === 'admin')
              <a href="{{ route('admin.dashboard') }}">๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ</a>
          @endif
      @endauth
      ```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ด€๋ฆฌ์ž ์ „์šฉ ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ์‹œ์ฆŒ/๊ณต์—ฐ/์˜ˆ์•ฝ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” CRUD ๊ธฐ๋Šฅ์ด ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. ํ†ต๊ณ„ ์ฐจํŠธ(Chart.js)๋ฅผ ํ†ตํ•ด ์ขŒ์„ ์ ์œ ์œจ์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ตœ๊ทผ ์˜ˆ์•ฝ ๋ชฉ๋ก๊ณผ ์ธ๊ธฐ ๊ณต์—ฐ ์ˆœ์œ„๋ฅผ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ์˜ˆ์•ฝ์„ ํ™˜๋ถˆ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ขŒ์„ ์ƒํƒœ๊ฐ€ ์ž๋™์œผ๋กœ ๋ณต๊ตฌ๋ฉ๋‹ˆ๋‹ค.

10
Step 10
Redis ์บ์‹ฑ์„ ์ ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๊ณ , PHPUnit ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.

1. **Redis ์บ์‹ฑ ์ ์šฉ**:
   - ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ์— ์บ์‹ฑ ์ ์šฉ:
     - ํ™ˆํŽ˜์ด์ง€์˜ ์ธ๊ธฐ ๊ณต์—ฐ ๋ชฉ๋ก
     - ์‹œ์ฆŒ ๋ชฉ๋ก
     - ๊ณต์—ฐ ์ƒ์„ธ ์ •๋ณด
     - ์ขŒ์„ ์žฌ๊ณ  ์ •๋ณด
   
   ์˜ˆ์‹œ: HomeController์˜ index() ๋ฉ”์„œ๋“œ์— ์บ์‹ฑ ์ ์šฉ:
   ```php
   use Illuminate\Support\Facades\Cache;
   
   public function index()
   {
       $activeSeasons = Cache::remember('active_seasons', 3600, function () {
           return Season::active()->with('shows')->get();
       });
       
       $popularShows = Cache::remember('popular_shows', 3600, function () {
           return Show::withAvg('reviews', 'rating')
                      ->orderBy('reviews_avg_rating', 'desc')
                      ->take(4)
                      ->get();
       });
       
       $recentReviews = Review::with('user', 'show')
                              ->latest()
                              ->take(3)
                              ->get();
       
       return view('home', compact('activeSeasons', 'popularShows', 'recentReviews'));
   }
   ```
   
   - ์บ์‹œ ๋ฌดํšจํ™”:
     - ์‹œ์ฆŒ์ด๋‚˜ ๊ณต์—ฐ์ด ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ๋  ๋•Œ ๊ด€๋ จ ์บ์‹œ ์‚ญ์ œ:
       ```php
       Cache::forget('active_seasons');
       Cache::forget('popular_shows');
       ```

2. **์ขŒ์„ ์žฌ๊ณ  ์บ์‹ฑ**:
   - ShowSchedule ๋ชจ๋ธ์— ์บ์‹œ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€:
     ```php
     public function getAvailableSeatsAttribute()
     {
         return Cache::remember("schedule_{$this->id}_available_seats", 60, function () {
             return $this->seats()->where('status', 'available')->count();
         });
     }
     
     public function decrementAvailableSeats()
     {
         Cache::forget("schedule_{$this->id}_available_seats");
         $this->decrement('available_seats');
     }
     ```

3. **PHPUnit ํ…Œ์ŠคํŠธ ์ž‘์„ฑ**:
   - ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • (phpunit.xml):
     ```xml
     <env name="DB_CONNECTION" value="sqlite"/>
     <env name="DB_DATABASE" value=":memory:"/>
     ```
   
   - Feature ํ…Œ์ŠคํŠธ ์ƒ์„ฑ:
     ```bash
     php artisan make:test BookingTest
     php artisan make:test PaymentTest
     php artisan make:test SeatReservationTest
     ```
   
   - **BookingTest** (tests/Feature/BookingTest.php):
     ```php
     namespace Tests\Feature;
     
     use Tests\TestCase;
     use App\Models\User;
     use App\Models\Show;
     use App\Models\ShowSchedule;
     use App\Models\Seat;
     use Illuminate\Foundation\Testing\RefreshDatabase;
     
     class BookingTest extends TestCase
     {
         use RefreshDatabase;
         
         public function test_user_can_view_booking_page()
         {
             $user = User::factory()->create();
             $show = Show::factory()->create();
             $schedule = ShowSchedule::factory()->create(['show_id' => $show->id]);
             
             $response = $this->actingAs($user)
                              ->get(route('bookings.create', $show));
             
             $response->assertStatus(200);
             $response->assertSee($show->title);
         }
         
         public function test_guest_cannot_access_booking_page()
         {
             $show = Show::factory()->create();
             
             $response = $this->get(route('bookings.create', $show));
             
             $response->assertRedirect(route('login'));
         }
         
         public function test_booking_creates_record_in_database()
         {
             $user = User::factory()->create();
             $schedule = ShowSchedule::factory()->create();
             $seat = Seat::factory()->create(['show_schedule_id' => $schedule->id, 'status' => 'available']);
             
             $response = $this->actingAs($user)
                              ->post(route('payment.process'), [
                                  'schedule_id' => $schedule->id,
                                  'seat_ids' => [$seat->id],
                              ]);
             
             // Stripe Checkout์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๋Š”์ง€ ํ™•์ธ
             $response->assertRedirect();
             
             // ์„ธ์…˜์— ์˜ˆ์•ฝ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜๋Š”์ง€ ํ™•์ธ
             $this->assertNotNull(session('pending_booking'));
         }
     }
     ```
   
   - **SeatReservationTest** (tests/Feature/SeatReservationTest.php):
     ```php
     namespace Tests\Feature;
     
     use Tests\TestCase;
     use App\Models\Seat;
     use App\Models\Booking;
     use App\Models\BookingSeat;
     use Illuminate\Foundation\Testing\RefreshDatabase;
     use Illuminate\Support\Facades\DB;
     
     class SeatReservationTest extends TestCase
     {
         use RefreshDatabase;
         
         public function test_seat_status_changes_after_booking()
         {
             $seat = Seat::factory()->create(['status' => 'available']);
             
             $seat->markAsSold();
             
             $this->assertEquals('sold', $seat->fresh()->status);
         }
         
         public function test_cannot_book_already_sold_seat()
         {
             $seat = Seat::factory()->create(['status' => 'sold']);
             
             $availableSeats = Seat::where('id', $seat->id)
                                   ->where('status', 'available')
                                   ->get();
             
             $this->assertCount(0, $availableSeats);
         }
         
         public function test_concurrent_booking_prevents_double_reservation()
         {
             $seat = Seat::factory()->create(['status' => 'available']);
             
             // ๋™์‹œ ์˜ˆ์•ฝ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
             DB::transaction(function () use ($seat) {
                 $lockedSeat = Seat::where('id', $seat->id)
                                   ->where('status', 'available')
                                   ->lockForUpdate()
                                   ->first();
                 
                 $this->assertNotNull($lockedSeat);
                 $lockedSeat->markAsSold();
             });
             
             // ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ ๊ฐ™์€ ์ขŒ์„ ์˜ˆ์•ฝ ์‹œ๋„
             $lockedSeat = Seat::where('id', $seat->id)
                               ->where('status', 'available')
                               ->first();
             
             $this->assertNull($lockedSeat);
         }
     }
     ```
   
   - **PaymentTest** (tests/Feature/PaymentTest.php):
     ```php
     namespace Tests\Feature;
     
     use Tests\TestCase;
     use App\Models\User;
     use App\Models\Booking;
     use Illuminate\Foundation\Testing\RefreshDatabase;
     
     class PaymentTest extends TestCase
     {
         use RefreshDatabase;
         
         public function test_user_total_spent_increases_after_payment()
         {
             $user = User::factory()->create(['total_spent' => 0]);
             $booking = Booking::factory()->create([
                 'user_id' => $user->id,
                 'total_amount' => 100000,
                 'payment_status' => 'pending',
             ]);
             
             $booking->update(['payment_status' => 'completed']);
             
             $this->assertEquals(100000, $user->fresh()->total_spent);
         }
         
         public function test_membership_tier_upgrades_based_on_spending()
         {
             $user = User::factory()->create(['total_spent' => 0, 'membership_tier' => 'bronze']);
             
             $user->update(['total_spent' => 600000]);
             $user->upgradeMembershipTier();
             
             $this->assertEquals('silver', $user->fresh()->membership_tier);
             
             $user->update(['total_spent' => 1100000]);
             $user->upgradeMembershipTier();
             
             $this->assertEquals('gold', $user->fresh()->membership_tier);
         }
     }
     ```

4. **ํŒฉํ† ๋ฆฌ ์ƒ์„ฑ** (ํ•„์š”ํ•œ ๊ฒฝ์šฐ):
   ```bash
   php artisan make:factory ShowFactory
   php artisan make:factory ShowScheduleFactory
   php artisan make:factory SeatFactory
   php artisan make:factory BookingFactory
   ```
   
   ๊ฐ ํŒฉํ† ๋ฆฌ์— ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์ •์˜:
   ```php
   // database/factories/SeatFactory.php
   public function definition()
   {
       return [
           'show_schedule_id' => ShowSchedule::factory(),
           'seat_number' => 'A-' . $this->faker->numberBetween(1, 100),
           'seat_type' => $this->faker->randomElement(['vip', 'regular']),
           'price' => $this->faker->randomElement([50000, 80000]),
           'status' => 'available',
       ];
   }
   ```

5. **ํ…Œ์ŠคํŠธ ์‹คํ–‰**:
   ```bash
   php artisan test
   ```
   
   ๋˜๋Š” ํŠน์ • ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰:
   ```bash
   php artisan test --filter BookingTest
   ```

6. **ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์ธ** (์„ ํƒ์‚ฌํ•ญ):
   - Xdebug ์„ค์น˜ ํ›„:
     ```bash
     php artisan test --coverage
     ```

7. **N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ ํ•ด๊ฒฐ**:
   - Eager Loading ์ ์šฉ:
     ```php
     // ์˜ˆ์•ฝ ๋ชฉ๋ก ์กฐํšŒ ์‹œ
     $bookings = Booking::with('user', 'seats.schedule.show')->paginate(20);
     
     // ๊ณต์—ฐ ๋ชฉ๋ก ์กฐํšŒ ์‹œ
     $shows = Show::with('season', 'schedules')->get();
     ```
   
   - Laravel Debugbar ์„ค์น˜ํ•˜์—ฌ ์ฟผ๋ฆฌ ๋ชจ๋‹ˆํ„ฐ๋ง:
     ```bash
     composer require barryvdh/laravel-debugbar --dev
     ```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Redis ์บ์‹ฑ์ด ์ ์šฉ๋˜์–ด ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ(์ธ๊ธฐ ๊ณต์—ฐ, ์‹œ์ฆŒ ๋ชฉ๋ก ๋“ฑ)์˜ ๋กœ๋”ฉ ์†๋„๊ฐ€ ํฌ๊ฒŒ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. PHPUnit ํ…Œ์ŠคํŠธ๊ฐ€ ์ž‘์„ฑ๋˜์–ด ์˜ˆ์•ฝ ๋กœ์ง, ์ขŒ์„ ์ค‘๋ณต ๋ฐฉ์ง€, ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ๋“ฑ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด ์ž๋™์œผ๋กœ ๊ฒ€์ฆ๋ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ 50% ์ด์ƒ ๋‹ฌ์„ฑ๋˜๋ฉฐ, N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ˆ˜๊ฐ€ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค. ์ „์ฒด ์‹œ์Šคํ…œ์ด ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ, ํ–ฅํ›„ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ์—๋„ ๊ธฐ์กด ๊ธฐ๋Šฅ์˜ ์ •์ƒ ์ž‘๋™์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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