๐ŸŽญ ๋ผ์ด๋ธŒ ์—ฐ๊ทน ํ‹ฐ์ผ“ ํ†ตํ•ฉ ์˜ˆ๋งค ํ”Œ๋žซํผ - ๋‹ค์ค‘ ๊ทน์žฅ & ๊ณต์—ฐ ์‹ค์‹œ๊ฐ„ ์ขŒ์„ ์˜ˆ์•ฝ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP Laravel AI: Claude

๐ŸŽญ ๋ผ์ด๋ธŒ ์—ฐ๊ทน ํ‹ฐ์ผ“ ํ†ตํ•ฉ ์˜ˆ๋งค ํ”Œ๋žซํผ - ๋‹ค์ค‘ ๊ทน์žฅ & ๊ณต์—ฐ ์‹ค์‹œ๊ฐ„ ์ขŒ์„ ์˜ˆ์•ฝ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… theater-multi-venue-booking-platform
์กฐํšŒ์ˆ˜ 107 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

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

์—ฌ๋Ÿฌ ๊ทน์žฅ๊ณผ ๊ณต์—ฐ์„ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๋Š” ์ „๋ฌธ ํ‹ฐ์ผ“ ์˜ˆ๋งค ํ”Œ๋žซํผ์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. Laravel 13.x + Blade๋กœ ๊ตฌํ˜„ํ•˜๋ฉฐ, ์‹ค๋ฌด ์ˆ˜์ค€์˜ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

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

1. ๋‹ค์ค‘ ๊ทน์žฅ/๊ณต์—ฐ ๊ด€๋ฆฌ

  • ๊ทน์žฅ(Venue) ๋“ฑ๋ก: ์ด๋ฆ„, ์ฃผ์†Œ, ์ขŒ์„ ๋ฐฐ์น˜๋„ ์ด๋ฏธ์ง€
  • ๊ณต์—ฐ(Show) ๋“ฑ๋ก: ์ œ๋ชฉ, ์žฅ๋ฅด, ๋Ÿฌ๋‹ํƒ€์ž„, ์—ฐ๋ น์ œํ•œ, ํฌ์Šคํ„ฐ
  • ํšŒ์ฐจ(Showtime) ๊ด€๋ฆฌ: ๋‚ ์งœ, ์‹œ๊ฐ„, ๊ทน์žฅ ์—ฐ๊ฒฐ, ์ขŒ์„ ๊ตฌ์„ฑ
  • ๊ทน์žฅ๋ณ„ ์ขŒ์„ ๊ตฌ์—ญ(Section): VIP์„, R์„, S์„, A์„ ๋“ฑ ๋‹ค๋‹จ๊ณ„ ๊ฐ€๊ฒฉ

2. ์‹ค์‹œ๊ฐ„ ์ขŒ์„ ์˜ˆ์•ฝ ์‹œ์Šคํ…œ

  • ์ขŒ์„ ๋ฐฐ์น˜๋„ ์‹œ๊ฐํ™” (Blade + Tailwind CSS ๊ทธ๋ฆฌ๋“œ)
  • ์‹ค์‹œ๊ฐ„ ์ขŒ์„ ์ƒํƒœ: ์˜ˆ์•ฝ ๊ฐ€๋Šฅ(available), ์„ ํƒ ์ค‘(pending), ์˜ˆ์•ฝ ์™„๋ฃŒ(booked)
  • ๋™์‹œ ์ ‘์†์ž ์ถฉ๋Œ ๋ฐฉ์ง€: DB ํŠธ๋žœ์žญ์…˜ + ๋‚™๊ด€์  ์ž ๊ธˆ(Optimistic Locking)
  • 5๋ถ„ ํƒ€์ด๋จธ: ์„ ํƒ ํ›„ ๊ฒฐ์ œ ๋ฏธ์™„๋ฃŒ ์‹œ ์ž๋™ ํ•ด์ œ (Laravel Queue + Job)

3. ์˜ˆ์•ฝ ์›Œํฌํ”Œ๋กœ์šฐ

  • ๊ณต์—ฐ ๊ฒ€์ƒ‰/ํ•„ํ„ฐ๋ง: ์žฅ๋ฅด, ๋‚ ์งœ, ๊ทน์žฅ, ๊ฐ€๊ฒฉ๋Œ€
  • ํšŒ์ฐจ ์„ ํƒ โ†’ ์ขŒ์„ ์„ ํƒ โ†’ ์˜ˆ์•ฝ์ž ์ •๋ณด ์ž…๋ ฅ โ†’ ๊ฒฐ์ œ
  • ์˜ˆ์•ฝ ํ™•์ธ ์ด๋ฉ”์ผ ๋ฐœ์†ก (Laravel Mail + Markdown ํ…œํ”Œ๋ฆฟ)
  • ์˜ˆ์•ฝ ๋‚ด์—ญ ์กฐํšŒ: ์˜ˆ์•ฝ๋ฒˆํ˜ธ๋กœ ํ‹ฐ์ผ“ ํ™•์ธ & QR ์ฝ”๋“œ ์ƒ์„ฑ

4. ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ (๋ณ„๋„ ์–ด๋“œ๋ฏผ ๋ผ์šฐํŠธ)

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

5. ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

  • ์บ์‹ฑ: Redis๋กœ ๊ณต์—ฐ ๋ชฉ๋ก, ์ขŒ์„ ์ƒํƒœ ์บ์‹ฑ (์„ฑ๋Šฅ ์ตœ์ ํ™”)
  • ํ/์žก: ์˜ˆ์•ฝ ํƒ€์ž„์•„์›ƒ ์ฒ˜๋ฆฌ, ์ด๋ฉ”์ผ ๋ฐœ์†ก์„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์œผ๋กœ
  • ์—‘์…€ ๋‹ค์šด๋กœ๋“œ: ์˜ˆ์•ฝ ๋‚ด์—ญ Export (Laravel Excel)
  • ๊ฒ€์ƒ‰ ์ตœ์ ํ™”: DB ์ธ๋ฑ์Šค ์„ค๊ณ„, Eager Loading์œผ๋กœ N+1 ์ฟผ๋ฆฌ ๋ฐฉ์ง€
  • ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„: ๊ณต์—ฐ-๋ฐฐ์šฐ(Actor) ๊ด€๊ณ„, ๊ณต์—ฐ-์žฅ๋ฅด(Genre) ํƒœ๊ทธ

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x + Blade Templates
  • ํ”„๋ก ํŠธ์—”๋“œ: Tailwind CSS 4.x, Alpine.js (๊ฒฝ๋Ÿ‰ ์ธํ„ฐ๋ž™์…˜)
  • DB: MySQL/PostgreSQL (๋ณต์žกํ•œ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ)
  • ์บ์‹ฑ: Redis
  • ํ: Laravel Queue (database driver)
  • ์ด๋ฉ”์ผ: Laravel Mail + Mailtrap(๊ฐœ๋ฐœ), SMTP(ํ”„๋กœ๋•์…˜)
  • ์ฐจํŠธ: Chart.js
  • QR ์ฝ”๋“œ: SimpleSoftwareIO/simple-qrcode
  • ์—‘์…€: Maatwebsite/Laravel-Excel

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

  • ๊ฒฐ์ œ ์—ฐ๋™ (Stripe/Toss Payments API ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
  • ํšŒ์›์ œ ํ• ์ธ: ๋“ฑ๊ธ‰๋ณ„ ํ• ์ธ์œจ (์ผ๋ฐ˜/VIP/VVIP)
  • ๊ณต์—ฐ ๋ฆฌ๋ทฐ ์‹œ์Šคํ…œ: ๊ด€๋žŒ ํ›„ ๋ณ„์  & ํ›„๊ธฐ ์ž‘์„ฑ
  • ์•Œ๋ฆผ ์‹œ์Šคํ…œ: ๊ณต์—ฐ ์˜คํ”ˆ ์•Œ๋ฆผ, ์ขŒ์„ ํ•ด์ œ ์•Œ๋ฆผ (Laravel Notification)
  • ์ขŒ์„ ์ถ”์ฒœ ์•Œ๊ณ ๋ฆฌ์ฆ˜: ๊ฐ€๊ฒฉ๋Œ€/์œ„์น˜ ๊ธฐ๋ฐ˜ ์ตœ์  ์ขŒ์„ ์ œ์•ˆ
  • ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x ํ•„์ˆ˜ (composer create-project laravel/laravel ๋˜๋Š” ์ตœ์‹  stable ๋ฒ„์ „)
  • โ€ข Blade ํ…œํ”Œ๋ฆฟ ์—”์ง„ ์‚ฌ์šฉ (React/Vue ๊ธˆ์ง€)
  • โ€ข Tailwind CSS 4.x (CDN ๋˜๋Š” npm ์„ค์น˜)
  • โ€ข Alpine.js๋กœ ๊ฒฝ๋Ÿ‰ ํ”„๋ก ํŠธ์—”๋“œ ์ธํ„ฐ๋ž™์…˜ (์ขŒ์„ ์„ ํƒ ํ† ๊ธ€, ํƒ€์ด๋จธ ๋“ฑ)
  • โ€ข MySQL ๋˜๋Š” PostgreSQL (SQLite๋Š” ๋™์‹œ์„ฑ ์ œ์–ด ํ•œ๊ณ„๋กœ ๋น„์ถ”์ฒœ)
  • โ€ข Redis ์„ค์น˜ ๋ฐ ์บ์‹œ ๋“œ๋ผ์ด๋ฒ„ ์„ค์ • ํ•„์ˆ˜
  • โ€ข Laravel Queue ์„ค์ • (database driver ์‚ฌ์šฉ, supervisor๋กœ ์›Œ์ปค ์‹คํ–‰)
  • โ€ข ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์œผ๋กœ ํ…Œ์ด๋ธ” ์„ค๊ณ„: venues, shows, showtimes, seats, bookings, users ๋“ฑ
  • โ€ข Eloquent ๊ด€๊ณ„: hasMany, belongsTo, belongsToMany ํ™œ์šฉ
  • โ€ข ํŠธ๋žœ์žญ์…˜ & ๋‚™๊ด€์  ์ž ๊ธˆ์œผ๋กœ ์ขŒ์„ ์ค‘๋ณต ์˜ˆ์•ฝ ๋ฐฉ์ง€
  • โ€ข Policy/Gate๋กœ ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ์ œ์–ด
  • โ€ข Form Request Validation์œผ๋กœ ์ž…๋ ฅ ๊ฒ€์ฆ
  • โ€ข Seeders/Factories๋กœ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (ํ…Œ์ŠคํŠธ์šฉ)
  • โ€ข ํ™˜๊ฒฝ ๋ณ€์ˆ˜(.env)๋กœ ๋ฉ”์ผ/Redis/DB ์„ค์ • ๊ด€๋ฆฌ

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

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

1
Step 1
mkdir theater-multi-venue-booking-platform && cd theater-multi-venue-booking-platform

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

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

```bash
composer create-project laravel/laravel .
php artisan key:generate
```

.env ํŒŒ์ผ ์„ค์ •:
- DB_CONNECTION=mysql (๋˜๋Š” pgsql)
- DB_DATABASE=theater_booking
- CACHE_DRIVER=redis
- QUEUE_CONNECTION=database
- MAIL_MAILER=smtp (๊ฐœ๋ฐœ ์ค‘์—๋Š” log๋กœ ์„ค์ • ๊ฐ€๋Šฅ)

Tailwind CSS 4.x ์„ค์น˜:
```bash
npm install -D tailwindcss@latest postcss autoprefixer
npx tailwindcss init -p
```

tailwind.config.js ์„ค์ •:
```javascript
module.exports = {
  content: [
    './resources/**/*.blade.php',
    './resources/**/*.js',
  ],
  theme: { extend: {} },
  plugins: [],
}
```

resources/css/app.css:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Alpine.js CDN ์ถ”๊ฐ€ (resources/views/layouts/app.blade.php):
```html
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
```

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

Redis ์„ค์น˜ ํ™•์ธ (๋กœ์ปฌ ํ™˜๊ฒฝ):
```bash
redis-cli ping  # PONG ์‘๋‹ต ํ™•์ธ
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ์™„๋ฃŒ. Tailwind CSS, Alpine.js ์„ค์ • ์™„๋ฃŒ. .env์— DB/Redis/Queue ์„ค์ • ์™„๋ฃŒ. ๊ธฐ๋ณธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์™„๋ฃŒ.

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

ํ•„์š”ํ•œ ํ…Œ์ด๋ธ”:

1. venues (๊ทน์žฅ)
   - id, name, address, city, seat_map_image, created_at, updated_at

2. shows (๊ณต์—ฐ)
   - id, title, description, genre, runtime_minutes, age_rating, poster_image, created_at, updated_at

3. showtimes (ํšŒ์ฐจ)
   - id, show_id (FK), venue_id (FK), show_date, show_time, total_seats, available_seats, created_at, updated_at
   - ์ธ๋ฑ์Šค: show_id, venue_id, show_date

4. seats (์ขŒ์„)
   - id, showtime_id (FK), section (VIP/R/S/A), row, number, price, status (available/pending/booked), locked_at (ํƒ€์ž„์•„์›ƒ ์ฒดํฌ์šฉ), locked_by_session, created_at, updated_at
   - ์ธ๋ฑ์Šค: showtime_id, status
   - ์œ ๋‹ˆํฌ ์ œ์•ฝ: showtime_id + section + row + number

5. bookings (์˜ˆ์•ฝ)
   - id, user_id (FK, nullable), showtime_id (FK), seat_ids (JSON), booker_name, booker_email, booker_phone, total_price, status (pending/confirmed/cancelled), payment_method, booking_number (unique), qr_code, created_at, updated_at
   - ์ธ๋ฑ์Šค: booking_number, user_id, showtime_id

6. actors (๋ฐฐ์šฐ) - ๋ณด๋„ˆ์Šค
   - id, name, bio, photo, created_at, updated_at

7. actor_show (ํ”ผ๋ฒ— ํ…Œ์ด๋ธ”) - ๋ณด๋„ˆ์Šค
   - id, show_id, actor_id

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ ๋ช…๋ น:
```bash
php artisan make:migration create_venues_table
php artisan make:migration create_shows_table
php artisan make:migration create_showtimes_table
php artisan make:migration create_seats_table
php artisan make:migration create_bookings_table
php artisan make:migration create_actors_table
php artisan make:migration create_actor_show_table
```

๊ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์— ์Šคํ‚ค๋งˆ ์ •์˜ ํ›„ ์‹คํ–‰:
```bash
php artisan migrate
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: 7๊ฐœ ํ…Œ์ด๋ธ” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ƒ์„ฑ ์™„๋ฃŒ. ์™ธ๋ž˜ํ‚ค, ์ธ๋ฑ์Šค, ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด ํฌํ•จ. php artisan migrate ์„ฑ๊ณต.

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

```bash
php artisan make:model Venue
php artisan make:model Show
php artisan make:model Showtime
php artisan make:model Seat
php artisan make:model Booking
php artisan make:model Actor
```

๊ฐ ๋ชจ๋ธ์— ๊ด€๊ณ„ ์ •์˜:

**Venue ๋ชจ๋ธ**:
- hasMany(Showtime::class)

**Show ๋ชจ๋ธ**:
- hasMany(Showtime::class)
- belongsToMany(Actor::class) (๋ณด๋„ˆ์Šค)

**Showtime ๋ชจ๋ธ**:
- belongsTo(Show::class)
- belongsTo(Venue::class)
- hasMany(Seat::class)
- hasMany(Booking::class)

**Seat ๋ชจ๋ธ**:
- belongsTo(Showtime::class)
- $casts = ['locked_at' => 'datetime'];
- ์Šค์ฝ”ํ”„ ์ถ”๊ฐ€:
  - scopeAvailable($query): status๊ฐ€ 'available'์ธ ์ขŒ์„
  - scopeExpiredLocks($query): locked_at์ด 5๋ถ„ ์ด์ƒ ์ง€๋‚œ pending ์ขŒ์„

**Booking ๋ชจ๋ธ**:
- belongsTo(User::class, 'user_id')->nullable()
- belongsTo(Showtime::class)
- $casts = ['seat_ids' => 'array'];

**Actor ๋ชจ๋ธ** (๋ณด๋„ˆ์Šค):
- belongsToMany(Show::class)

$fillable ์†์„ฑ์„ ๊ฐ ๋ชจ๋ธ์— ์ถ”๊ฐ€ํ•˜์—ฌ ๋Œ€๋Ÿ‰ ํ• ๋‹น ํ—ˆ์šฉ.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: 6๊ฐœ Eloquent ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ. hasMany, belongsTo, belongsToMany ๊ด€๊ณ„ ์ •์˜ ์™„๋ฃŒ. ์Šค์ฝ”ํ”„ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ์™„๋ฃŒ.

4
Step 4
Seeder์™€ Factory๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“œ์„ธ์š”.

```bash
php artisan make:factory VenueFactory
php artisan make:factory ShowFactory
php artisan make:factory ShowtimeFactory
php artisan make:factory SeatFactory
php artisan make:seeder DatabaseSeeder
```

**VenueFactory**: ๊ทน์žฅ 5๊ฐœ ์ƒ์„ฑ (name, address, city)
**ShowFactory**: ๊ณต์—ฐ 20๊ฐœ ์ƒ์„ฑ (title, description, genre, runtime_minutes, age_rating)
**ShowtimeFactory**: ๊ฐ ๊ณต์—ฐ๋‹น 3~5๊ฐœ ํšŒ์ฐจ ์ƒ์„ฑ (show_date๋Š” ํ–ฅํ›„ 30์ผ ๋‚ด ๋žœ๋ค)
**SeatFactory**: ๊ฐ ํšŒ์ฐจ๋‹น ์ขŒ์„ 100์„ ์ƒ์„ฑ
  - section: VIP(10์„), R(30์„), S(30์„), A(30์„)
  - row: A~J (๋žœ๋ค)
  - number: 1~10 (๋žœ๋ค)
  - price: VIP(100000), R(80000), S(60000), A(40000)
  - status: 'available'

**DatabaseSeeder**:
```php
Venue::factory(5)->create()->each(function ($venue) {
    Show::factory(4)->create()->each(function ($show) use ($venue) {
        Showtime::factory(3)->create([
            'show_id' => $show->id,
            'venue_id' => $venue->id,
        ])->each(function ($showtime) {
            Seat::factory(100)->create(['showtime_id' => $showtime->id]);
        });
    });
});
```

์‹คํ–‰:
```bash
php artisan db:seed
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ. ๊ทน์žฅ 5๊ฐœ, ๊ณต์—ฐ 20๊ฐœ, ํšŒ์ฐจ 60๊ฐœ, ์ขŒ์„ 6000์„ ์ƒ์„ฑ. ๋ฐ์ดํ„ฐ ํ™•์ธ ๊ฐ€๋Šฅ.

5
Step 5
์‚ฌ์šฉ์ž ๊ณต์—ฐ ๋ชฉ๋ก ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

**๋ผ์šฐํŠธ (routes/web.php)**:
```php
Route::get('/', [ShowController::class, 'index'])->name('home');
Route::get('/shows', [ShowController::class, 'index'])->name('shows.index');
Route::get('/shows/{show}', [ShowController::class, 'show'])->name('shows.show');
```

**ShowController**:
```bash
php artisan make:controller ShowController
```

index() ๋ฉ”์„œ๋“œ:
- ๊ณต์—ฐ ๋ชฉ๋ก ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜ 12๊ฐœ์”ฉ)
- ํ•„ํ„ฐ๋ง: ์žฅ๋ฅด(genre), ๋‚ ์งœ ๋ฒ”์œ„(date_from, date_to), ๊ทน์žฅ(venue_id)
- ์บ์‹ฑ: Redis์— 5๋ถ„๊ฐ„ ์บ์‹œ
```php
$shows = Cache::remember('shows_list', 300, function () {
    return Show::with('showtimes.venue')->paginate(12);
});
```

show() ๋ฉ”์„œ๋“œ:
- ํŠน์ • ๊ณต์—ฐ ์ƒ์„ธ ์ •๋ณด
- ํšŒ์ฐจ ๋ชฉ๋ก (ํ–ฅํ›„ ๋‚ ์งœ๋งŒ, ๋‚ ์งœ์ˆœ ์ •๋ ฌ)
- Eager Loading์œผ๋กœ N+1 ์ฟผ๋ฆฌ ๋ฐฉ์ง€
```php
$show = Show::with(['showtimes' => function ($query) {
    $query->where('show_date', '>=', now())
          ->orderBy('show_date')
          ->orderBy('show_time')
          ->with('venue');
}])->findOrFail($id);
```

**Blade ๋ทฐ (resources/views/shows/index.blade.php)**:
- Tailwind CSS ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ (grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6)
- ๊ณต์—ฐ ์นด๋“œ: ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€, ์ œ๋ชฉ, ์žฅ๋ฅด, ๋Ÿฌ๋‹ํƒ€์ž„
- ํ•„ํ„ฐ ํผ: ์žฅ๋ฅด ์…€๋ ‰ํŠธ, ๋‚ ์งœ ๋ฒ”์œ„ input, ๊ทน์žฅ ์…€๋ ‰ํŠธ
- ํŽ˜์ด์ง€๋„ค์ด์…˜ ({{ $shows->links() }})

**Blade ๋ทฐ (resources/views/shows/show.blade.php)**:
- ๊ณต์—ฐ ์ƒ์„ธ ์ •๋ณด: ํฌ์Šคํ„ฐ, ์ œ๋ชฉ, ์„ค๋ช…, ์žฅ๋ฅด, ๋Ÿฌ๋‹ํƒ€์ž„, ์—ฐ๋ น์ œํ•œ
- ํšŒ์ฐจ ๋ชฉ๋ก: ๋‚ ์งœ, ์‹œ๊ฐ„, ๊ทน์žฅ๋ช…, ์ž”์—ฌ์„, [์˜ˆ์•ฝํ•˜๊ธฐ] ๋ฒ„ํŠผ
- [์˜ˆ์•ฝํ•˜๊ธฐ] ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ขŒ์„ ์„ ํƒ ํŽ˜์ด์ง€๋กœ ์ด๋™ (route('showtimes.seats', $showtime->id))

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ณต์—ฐ ๋ชฉ๋ก ํŽ˜์ด์ง€์™€ ์ƒ์„ธ ํŽ˜์ด์ง€ ์™„์„ฑ. ํ•„ํ„ฐ๋ง ๋ฐ ์บ์‹ฑ ์ ์šฉ. Tailwind CSS๋กœ ๋ฐ˜์‘ํ˜• UI ๊ตฌํ˜„. ํšŒ์ฐจ๋ณ„ ์˜ˆ์•ฝ ๋ฒ„ํŠผ ๋™์ž‘.

6
Step 6
์ขŒ์„ ์„ ํƒ ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

**๋ผ์šฐํŠธ**:
```php
Route::get('/showtimes/{showtime}/seats', [BookingController::class, 'selectSeats'])->name('showtimes.seats');
Route::post('/showtimes/{showtime}/lock-seats', [BookingController::class, 'lockSeats'])->name('showtimes.lock');
```

**BookingController**:
```bash
php artisan make:controller BookingController
```

selectSeats() ๋ฉ”์„œ๋“œ:
- ํšŒ์ฐจ ์ •๋ณด ์กฐํšŒ (with show, venue)
- ์ขŒ์„ ๋ชฉ๋ก ์กฐํšŒ (section๋ณ„ ๊ทธ๋ฃนํ™”)
```php
$showtime = Showtime::with('show', 'venue')->findOrFail($id);
$seats = Seat::where('showtime_id', $showtime->id)
             ->orderBy('section')
             ->orderBy('row')
             ->orderBy('number')
             ->get()
             ->groupBy('section');
```

**Blade ๋ทฐ (resources/views/bookings/select-seats.blade.php)**:
- ํšŒ์ฐจ ์ •๋ณด ํ‘œ์‹œ: ๊ณต์—ฐ๋ช…, ๋‚ ์งœ, ์‹œ๊ฐ„, ๊ทน์žฅ๋ช…
- ์ขŒ์„ ๋ฐฐ์น˜๋„ (section๋ณ„ ๊ทธ๋ฆฌ๋“œ)
  - VIP/R/S/A ๊ตฌ์—ญ๋ณ„๋กœ div ๋ถ„๋ฆฌ
  - ์ขŒ์„ ๋ฒ„ํŠผ: Alpine.js๋กœ ์„ ํƒ ํ† ๊ธ€
  ```html
  <div x-data="{ selectedSeats: [] }">
    @foreach($seats as $section => $sectionSeats)
      <div class="mb-8">
        <h3 class="text-lg font-bold mb-2">{{ $section }}์„</h3>
        <div class="grid grid-cols-10 gap-2">
          @foreach($sectionSeats as $seat)
            <button
              @click="toggleSeat({{ $seat->id }})"
              :class="{
                'bg-gray-300': '{{ $seat->status }}' === 'booked',
                'bg-yellow-300': '{{ $seat->status }}' === 'pending',
                'bg-green-500': selectedSeats.includes({{ $seat->id }}),
                'bg-blue-500': '{{ $seat->status }}' === 'available' && !selectedSeats.includes({{ $seat->id }})
              }"
              :disabled="'{{ $seat->status }}' !== 'available'"
              class="w-10 h-10 rounded text-xs font-semibold"
            >
              {{ $seat->row }}{{ $seat->number }}
            </button>
          @endforeach
        </div>
      </div>
    @endforeach
    <button @click="lockSeats()" class="mt-4 bg-purple-600 text-white px-6 py-3 rounded">์„ ํƒ ์™„๋ฃŒ</button>
  </div>
  ```
- Alpine.js ๋ฉ”์„œ๋“œ:
  - toggleSeat(seatId): selectedSeats ๋ฐฐ์—ด์— ์ถ”๊ฐ€/์ œ๊ฑฐ
  - lockSeats(): AJAX๋กœ POST /showtimes/{showtime}/lock-seats (์„ ํƒํ•œ ์ขŒ์„ ID ์ „์†ก)

lockSeats() ๋ฉ”์„œ๋“œ (BookingController):
- ์„ ํƒํ•œ ์ขŒ์„๋“ค์˜ status๋ฅผ 'pending'์œผ๋กœ ๋ณ€๊ฒฝ
- locked_at = now(), locked_by_session = session()->getId()
- DB ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ
- ์ค‘๋ณต ์˜ˆ์•ฝ ๋ฐฉ์ง€: ๋‚™๊ด€์  ์ž ๊ธˆ (whereIn + where status = 'available')
```php
DB::transaction(function () use ($seatIds, $sessionId) {
    $updated = Seat::whereIn('id', $seatIds)
                   ->where('status', 'available')
                   ->update([
                       'status' => 'pending',
                       'locked_at' => now(),
                       'locked_by_session' => $sessionId,
                   ]);
    if ($updated !== count($seatIds)) {
        throw new \Exception('์ผ๋ถ€ ์ขŒ์„์ด ์ด๋ฏธ ์˜ˆ์•ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
    }
});
```
- ์„ฑ๊ณต ์‹œ ์˜ˆ์•ฝ ์ •๋ณด ์ž…๋ ฅ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (route('bookings.create', ['showtime' => $showtime->id, 'seats' => implode(',', $seatIds)]))

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ขŒ์„ ์„ ํƒ ํŽ˜์ด์ง€ ์™„์„ฑ. Alpine.js๋กœ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์ขŒ์„ ํ† ๊ธ€ ๊ตฌํ˜„. AJAX๋กœ ์ขŒ์„ ์ž ๊ธˆ ์ฒ˜๋ฆฌ. DB ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋™์‹œ์„ฑ ์ œ์–ด ์™„๋ฃŒ.

7
Step 7
์˜ˆ์•ฝ ์ •๋ณด ์ž…๋ ฅ ๋ฐ ํ™•์ • ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

**๋ผ์šฐํŠธ**:
```php
Route::get('/bookings/create', [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');
```

create() ๋ฉ”์„œ๋“œ:
- ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ showtime_id, seats (์ฝค๋งˆ ๊ตฌ๋ถ„ ์ขŒ์„ ID) ๋ฐ›๊ธฐ
- ์ขŒ์„ ์ •๋ณด ์กฐํšŒ (locked_by_session์ด ํ˜„์žฌ ์„ธ์…˜์ธ์ง€ ํ™•์ธ)
```php
$seatIds = explode(',', request('seats'));
$seats = Seat::whereIn('id', $seatIds)
             ->where('locked_by_session', session()->getId())
             ->where('status', 'pending')
             ->get();
if ($seats->count() !== count($seatIds)) {
    return redirect()->route('shows.index')->with('error', '์ขŒ์„ ์„ ํƒ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}
```
- ์ด ๊ธˆ์•ก ๊ณ„์‚ฐ: $totalPrice = $seats->sum('price');

**Blade ๋ทฐ (resources/views/bookings/create.blade.php)**:
- ํšŒ์ฐจ ์ •๋ณด, ์„ ํƒํ•œ ์ขŒ์„ ๋ชฉ๋ก, ์ด ๊ธˆ์•ก ํ‘œ์‹œ
- ์˜ˆ์•ฝ์ž ์ •๋ณด ํผ: ์ด๋ฆ„, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, ๊ฒฐ์ œ ๋ฐฉ๋ฒ• (์นด๋“œ/๊ณ„์ขŒ์ด์ฒด/ํ˜„์žฅ๊ฒฐ์ œ)
- 5๋ถ„ ํƒ€์ด๋จธ ํ‘œ์‹œ (Alpine.js)
  ```html
  <div x-data="{ timeLeft: 300 }" x-init="setInterval(() => { timeLeft--; if (timeLeft <= 0) alert('์‹œ๊ฐ„ ์ดˆ๊ณผ! ๋‹ค์‹œ ์„ ํƒํ•ด์ฃผ์„ธ์š”.'); }, 1000)">
    <p>๋‚จ์€ ์‹œ๊ฐ„: <span x-text="Math.floor(timeLeft / 60)"></span>:<span x-text="(timeLeft % 60).toString().padStart(2, '0')"></span></p>
  </div>
  ```
- [์˜ˆ์•ฝ ํ™•์ •] ๋ฒ„ํŠผ

store() ๋ฉ”์„œ๋“œ:
- Form Request Validation ์ƒ์„ฑ
```bash
php artisan make:request StoreBookingRequest
```
StoreBookingRequest:
```php
public function rules() {
    return [
        'booker_name' => 'required|string|max:100',
        'booker_email' => 'required|email',
        'booker_phone' => 'required|string|max:20',
        'payment_method' => 'required|in:card,bank_transfer,on_site',
        'seat_ids' => 'required|array',
    ];
}
```
- DB ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์˜ˆ์•ฝ ์ƒ์„ฑ
```php
$booking = DB::transaction(function () use ($validated, $showtimeId) {
    // ์ขŒ์„ ์ƒํƒœ๋ฅผ 'booked'๋กœ ๋ณ€๊ฒฝ
    Seat::whereIn('id', $validated['seat_ids'])
        ->where('status', 'pending')
        ->where('locked_by_session', session()->getId())
        ->update(['status' => 'booked']);
    
    // ์˜ˆ์•ฝ ์ƒ์„ฑ
    $booking = Booking::create([
        'showtime_id' => $showtimeId,
        'seat_ids' => $validated['seat_ids'],
        'booker_name' => $validated['booker_name'],
        'booker_email' => $validated['booker_email'],
        'booker_phone' => $validated['booker_phone'],
        'total_price' => Seat::whereIn('id', $validated['seat_ids'])->sum('price'),
        'status' => 'confirmed',
        'payment_method' => $validated['payment_method'],
        'booking_number' => 'BK' . strtoupper(Str::random(10)),
    ]);
    
    return $booking;
});
```
- QR ์ฝ”๋“œ ์ƒ์„ฑ (SimpleSoftwareIO/simple-qrcode ํŒจํ‚ค์ง€)
```bash
composer require simplesoftwareio/simple-qrcode
```
```php
$qrCode = QrCode::size(200)->generate(route('bookings.show', $booking->id));
$booking->update(['qr_code' => $qrCode]);
```
- ์˜ˆ์•ฝ ํ™•์ธ ์ด๋ฉ”์ผ ๋ฐœ์†ก (Queue๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ)
```bash
php artisan make:mail BookingConfirmationMail --markdown=emails.booking-confirmation
```
```php
Mail::to($booking->booker_email)->queue(new BookingConfirmationMail($booking));
```
- ์˜ˆ์•ฝ ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (route('bookings.show', $booking->id))

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์˜ˆ์•ฝ ์ •๋ณด ์ž…๋ ฅ ํผ ์™„์„ฑ. Form Request Validation ์ ์šฉ. DB ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์˜ˆ์•ฝ ํ™•์ • ์ฒ˜๋ฆฌ. QR ์ฝ”๋“œ ์ƒ์„ฑ ๋ฐ ์ด๋ฉ”์ผ ๋ฐœ์†ก ์™„๋ฃŒ.

8
Step 8
์˜ˆ์•ฝ ํ™•์ธ ํŽ˜์ด์ง€์™€ ํƒ€์ž„์•„์›ƒ ์ž๋™ ํ•ด์ œ Job์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.

**์˜ˆ์•ฝ ํ™•์ธ ํŽ˜์ด์ง€ (BookingController::show)**:
```php
public function show(Booking $booking) {
    $booking->load('showtime.show', 'showtime.venue');
    $seats = Seat::whereIn('id', $booking->seat_ids)->get();
    return view('bookings.show', compact('booking', 'seats'));
}
```

**Blade ๋ทฐ (resources/views/bookings/show.blade.php)**:
- ์˜ˆ์•ฝ ๋ฒˆํ˜ธ, ์˜ˆ์•ฝ์ž ์ •๋ณด, ๊ณต์—ฐ ์ •๋ณด, ํšŒ์ฐจ, ๊ทน์žฅ, ์ขŒ์„ ๋ชฉ๋ก, ์ด ๊ธˆ์•ก
- QR ์ฝ”๋“œ ์ด๋ฏธ์ง€ ํ‘œ์‹œ
```html
<div class="text-center">
  {!! $booking->qr_code !!}
  <p class="mt-2 text-sm text-gray-600">์ž…์žฅ ์‹œ QR ์ฝ”๋“œ๋ฅผ ์ œ์‹œํ•ด์ฃผ์„ธ์š”.</p>
</div>
```
- [์˜ˆ์•ฝ ์ทจ์†Œ] ๋ฒ„ํŠผ (์„ ํƒ ๊ธฐ๋Šฅ)

**ํƒ€์ž„์•„์›ƒ ์ž๋™ ํ•ด์ œ Job ์ƒ์„ฑ**:
```bash
php artisan make:job ReleasePendingSeatsJob
```

ReleasePendingSeatsJob:
```php
public function handle() {
    Seat::where('status', 'pending')
        ->where('locked_at', '<', now()->subMinutes(5))
        ->update([
            'status' => 'available',
            'locked_at' => null,
            'locked_by_session' => null,
        ]);
}
```

**์Šค์ผ€์ค„๋Ÿฌ ๋“ฑ๋ก (app/Console/Kernel.php)**:
```php
protected function schedule(Schedule $schedule) {
    $schedule->job(new ReleasePendingSeatsJob)->everyMinute();
}
```

**ํฌ๋ก  ์„ค์ • (๋กœ์ปฌ ๊ฐœ๋ฐœ ์ค‘์—๋Š” `php artisan schedule:work` ์‹คํ–‰)**:
```bash
php artisan schedule:work
```

๋˜๋Š” ์›Œ์ปค ์‹คํ–‰:
```bash
php artisan queue:work
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์˜ˆ์•ฝ ํ™•์ธ ํŽ˜์ด์ง€ ์™„์„ฑ. QR ์ฝ”๋“œ ํ‘œ์‹œ ์™„๋ฃŒ. ํƒ€์ž„์•„์›ƒ ์ž๋™ ํ•ด์ œ Job ์ƒ์„ฑ ๋ฐ ์Šค์ผ€์ค„๋Ÿฌ ๋“ฑ๋ก ์™„๋ฃŒ. 5๋ถ„ ํ›„ pending ์ขŒ์„ ์ž๋™ ํ•ด์ œ ๋™์ž‘ ํ™•์ธ.

9
Step 9
๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

**๊ด€๋ฆฌ์ž ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ƒ์„ฑ**:
```bash
php artisan make:middleware AdminMiddleware
```

AdminMiddleware:
```php
public function handle($request, Closure $next) {
    if (!auth()->check() || !auth()->user()->is_admin) {
        abort(403, 'Unauthorized');
    }
    return $next($request);
}
```

app/Http/Kernel.php์— ๋“ฑ๋ก:
```php
protected $routeMiddleware = [
    'admin' => \App\Http\Middleware\AdminMiddleware::class,
];
```

users ํ…Œ์ด๋ธ”์— is_admin ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜:
```bash
php artisan make:migration add_is_admin_to_users_table
```
```php
Schema::table('users', function (Blueprint $table) {
    $table->boolean('is_admin')->default(false);
});
```

**๊ด€๋ฆฌ์ž ๋ผ์šฐํŠธ (routes/web.php)**:
```php
Route::prefix('admin')->middleware(['auth', 'admin'])->group(function () {
    Route::get('/dashboard', [AdminController::class, 'dashboard'])->name('admin.dashboard');
    Route::resource('venues', VenueController::class);
    Route::resource('shows', AdminShowController::class);
    Route::resource('showtimes', ShowtimeController::class);
    Route::get('/bookings', [AdminBookingController::class, 'index'])->name('admin.bookings.index');
    Route::delete('/bookings/{booking}', [AdminBookingController::class, 'destroy'])->name('admin.bookings.destroy');
});
```

**AdminController::dashboard**:
```bash
php artisan make:controller Admin/AdminController
```

```php
public function dashboard() {
    $totalBookings = Booking::where('status', 'confirmed')->count();
    $totalRevenue = Booking::where('status', 'confirmed')->sum('total_price');
    $upcomingShowtimes = Showtime::where('show_date', '>=', now())->count();
    $recentBookings = Booking::with('showtime.show')->latest()->take(10)->get();
    
    // ์ผ๋ณ„ ๋งค์ถœ ์ฐจํŠธ ๋ฐ์ดํ„ฐ (์ตœ๊ทผ 7์ผ)
    $dailyRevenue = Booking::where('status', 'confirmed')
        ->where('created_at', '>=', now()->subDays(7))
        ->selectRaw('DATE(created_at) as date, SUM(total_price) as revenue')
        ->groupBy('date')
        ->orderBy('date')
        ->get();
    
    return view('admin.dashboard', compact('totalBookings', 'totalRevenue', 'upcomingShowtimes', 'recentBookings', 'dailyRevenue'));
}
```

**Blade ๋ทฐ (resources/views/admin/dashboard.blade.php)**:
- ํ†ต๊ณ„ ์นด๋“œ: ์ด ์˜ˆ์•ฝ ์ˆ˜, ์ด ๋งค์ถœ, ์˜ˆ์ •๋œ ํšŒ์ฐจ ์ˆ˜
- ์ตœ๊ทผ ์˜ˆ์•ฝ ๋ชฉ๋ก (ํ…Œ์ด๋ธ”)
- ์ผ๋ณ„ ๋งค์ถœ ์ฐจํŠธ (Chart.js)
```html
<canvas id="revenueChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('revenueChart').getContext('2d');
const chart = new Chart(ctx, {
    type: 'line',
    data: {
        labels: @json($dailyRevenue->pluck('date')),
        datasets: [{
            label: '์ผ๋ณ„ ๋งค์ถœ',
            data: @json($dailyRevenue->pluck('revenue')),
            borderColor: 'rgb(75, 192, 192)',
            tension: 0.1
        }]
    }
});
</script>
```

**๊ทน์žฅ/๊ณต์—ฐ/ํšŒ์ฐจ CRUD ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ**:
```bash
php artisan make:controller Admin/VenueController --resource
php artisan make:controller Admin/AdminShowController --resource
php artisan make:controller Admin/ShowtimeController --resource
php artisan make:controller Admin/AdminBookingController
```

๊ฐ ์ปจํŠธ๋กค๋Ÿฌ์— CRUD ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ (index, create, store, edit, update, destroy).

Blade ๋ทฐ๋Š” Tailwind CSS๋กœ ํผ ์Šคํƒ€์ผ๋ง.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ ์™„์„ฑ. ํ†ต๊ณ„ ์นด๋“œ, ์ตœ๊ทผ ์˜ˆ์•ฝ ๋ชฉ๋ก, ์ผ๋ณ„ ๋งค์ถœ ์ฐจํŠธ ํ‘œ์‹œ. ๊ทน์žฅ/๊ณต์—ฐ/ํšŒ์ฐจ CRUD ๊ธฐ๋Šฅ ๊ตฌํ˜„. ๊ด€๋ฆฌ์ž ๋ฏธ๋“ค์›จ์–ด๋กœ ์ ‘๊ทผ ์ œ์–ด ์™„๋ฃŒ.

10
Step 10
์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

**1. ์บ์‹ฑ ์ตœ์ ํ™”**:
- ๊ณต์—ฐ ๋ชฉ๋ก ์บ์‹ฑ (์ด๋ฏธ ๊ตฌํ˜„)
- ํšŒ์ฐจ๋ณ„ ์ขŒ์„ ์ƒํƒœ ์บ์‹ฑ (Redis)
```php
$seats = Cache::remember("seats_showtime_{$showtimeId}", 60, function () use ($showtimeId) {
    return Seat::where('showtime_id', $showtimeId)->get();
});
```
- ์ขŒ์„ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ์บ์‹œ ๋ฌดํšจํ™”
```php
Cache::forget("seats_showtime_{$showtimeId}");
```

**2. ์ธ๋ฑ์Šค ์ตœ์ ํ™”**:
- ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (์ด๋ฏธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์— ํฌํ•จ)
- Explain ์ฟผ๋ฆฌ๋กœ ์„ฑ๋Šฅ ํ™•์ธ

**3. Eager Loading**:
- N+1 ์ฟผ๋ฆฌ ๋ฐฉ์ง€ (์ด๋ฏธ ๊ตฌํ˜„)

**4. ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ**:
```bash
composer require maatwebsite/excel
```

์˜ˆ์•ฝ ๋‚ด์—ญ Export:
```bash
php artisan make:export BookingsExport --model=Booking
```

BookingsExport:
```php
public function collection() {
    return Booking::with('showtime.show')->where('status', 'confirmed')->get();
}
```

๊ด€๋ฆฌ์ž ๋ผ์šฐํŠธ:
```php
Route::get('/admin/bookings/export', [AdminBookingController::class, 'export'])->name('admin.bookings.export');
```

AdminBookingController::export:
```php
public function export() {
    return Excel::download(new BookingsExport, 'bookings.xlsx');
}
```

**5. ๊ณต์—ฐ ๋ฆฌ๋ทฐ ์‹œ์Šคํ…œ (๋ณด๋„ˆ์Šค)**:
```bash
php artisan make:migration create_reviews_table
```

reviews ํ…Œ์ด๋ธ”:
- id, booking_id (FK), rating (1~5), comment, created_at, updated_at

Review ๋ชจ๋ธ ์ƒ์„ฑ ๋ฐ ๊ด€๊ณ„ ์ •์˜.

๊ณต์—ฐ ์ƒ์„ธ ํŽ˜์ด์ง€์— ๋ฆฌ๋ทฐ ๋ชฉ๋ก ํ‘œ์‹œ.

**6. ์•Œ๋ฆผ ์‹œ์Šคํ…œ (๋ณด๋„ˆ์Šค)**:
```bash
php artisan make:notification ShowOpenNotification
```

๊ณต์—ฐ ์˜คํ”ˆ ์‹œ ๊ตฌ๋…์ž์—๊ฒŒ ์•Œ๋ฆผ ๋ฐœ์†ก (Queue๋กœ ์ฒ˜๋ฆฌ).

**7. ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ**:
- Tailwind CSS ๋ฐ˜์‘ํ˜• ํด๋ž˜์Šค ํ™œ์šฉ (sm:, md:, lg:)
- ์ขŒ์„ ์„ ํƒ ํŽ˜์ด์ง€ ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™” (์ž‘์€ ํ™”๋ฉด์—์„œ ๊ทธ๋ฆฌ๋“œ ์กฐ์ •)

**8. ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์„ ํƒ)**:
```bash
php artisan make:test BookingTest
```

์˜ˆ์•ฝ ์ƒ์„ฑ, ์ขŒ์„ ์ž ๊ธˆ, ํƒ€์ž„์•„์›ƒ ํ•ด์ œ ๋“ฑ ์ฃผ์š” ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์บ์‹ฑ, ์ธ๋ฑ์Šค ์ตœ์ ํ™” ์™„๋ฃŒ. ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€. ๊ณต์—ฐ ๋ฆฌ๋ทฐ ์‹œ์Šคํ…œ ๊ตฌํ˜„ (์„ ํƒ). ์•Œ๋ฆผ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ (์„ ํƒ). ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ์™„์„ฑ. ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ์™„๋ฃŒ (์„ ํƒ). ํ”„๋กœ์ ํŠธ ์™„์„ฑ!

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

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

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

๋Œ“๊ธ€ (0)

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