๐ŸŽช ์„œ์ปค์Šค ๊ณต์—ฐ ์‹œ์ฆŒ ํŒจ์Šค ํŒ๋งค ํ”Œ๋žซํผ - ํšŒ์ฐจ๋ณ„ ํ‹ฐ์ผ“ ๊ตฌ๋งค & ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ค‘๊ธ‰ PHP + JavaScript Laravel + React + Inertia.js AI: Claude

๐ŸŽช ์„œ์ปค์Šค ๊ณต์—ฐ ์‹œ์ฆŒ ํŒจ์Šค ํŒ๋งค ํ”Œ๋žซํผ - ํšŒ์ฐจ๋ณ„ ํ‹ฐ์ผ“ ๊ตฌ๋งค & ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ

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

๋ฌธ์ œ ์„ค๋ช…

๐ŸŽช ์„œ์ปค์Šค ๊ณต์—ฐ ์‹œ์ฆŒ ํŒจ์Šค ํŒ๋งค ํ”Œ๋žซํผ

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

ํ•„์ˆ˜ ๊ธฐ๋Šฅ

  • ์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก: ์‹œ์ฆŒ๋ณ„ ํŒจ์Šค ์ƒํ’ˆ ์นด๋“œ (์˜ˆ: ๋ด„ ์‹œ์ฆŒ 5ํšŒ๊ถŒ, ์—ฌ๋ฆ„ ์‹œ์ฆŒ 10ํšŒ๊ถŒ)
  • ํŒจ์Šค ์ƒ์„ธ ํŽ˜์ด์ง€: ํฌํ•จ๋œ ๊ณต์—ฐ ํšŒ์ฐจ ๋ชฉ๋ก, ๊ฐ€๊ฒฉ, ํ˜œํƒ ์ •๋ณด
  • ๊ตฌ๋งค ํผ: ํŒจ์Šค ์„ ํƒ โ†’ ๊ณ ๊ฐ ์ •๋ณด ์ž…๋ ฅ โ†’ ๊ตฌ๋งค ์™„๋ฃŒ (๊ฒฐ์ œ ์—ฐ๋™ ์—†์ด ์ฃผ๋ฌธ ์ ‘์ˆ˜๋งŒ)
  • ๋‚ด ํŒจ์Šค ๋Œ€์‹œ๋ณด๋“œ: ๊ตฌ๋งคํ•œ ํŒจ์Šค ๋ชฉ๋ก, ์‚ฌ์šฉ ๊ฐ€๋Šฅ ํšŸ์ˆ˜, ๊ด€๋žŒ ์™„๋ฃŒ ์ฒดํฌ
  • ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ: ์–ด๋–ค ๊ณต์—ฐ์„ ์–ธ์ œ ๋ดค๋Š”์ง€ ํƒ€์ž„๋ผ์ธ์œผ๋กœ ํ™•์ธ
  • ํšŒ์ฐจ ์ฒดํฌ์ธ: QR ์ฝ”๋“œ ๋˜๋Š” ํ‹ฐ์ผ“ ๋ฒˆํ˜ธ๋กœ ๊ด€๋žŒ ์™„๋ฃŒ ์ฒ˜๋ฆฌ

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

  • ํŒจ์Šค ์‚ฌ์šฉ๋ฅ  ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” (5ํšŒ ์ค‘ 3ํšŒ ์‚ฌ์šฉ ๋“ฑ)
  • ์‹œ์ฆŒ๋ณ„ ๊ณต์—ฐ ํฌ์Šคํ„ฐ ๊ฐค๋Ÿฌ๋ฆฌ
  • ๊ด€๋žŒ ํ›„ ๊ฐ„๋‹จํ•œ ๋ณ„์ /ํ›„๊ธฐ ๋‚จ๊ธฐ๊ธฐ
  • ํŒจ์Šค ์„ ๋ฌผํ•˜๊ธฐ (์นœ๊ตฌ ์ด๋ฉ”์ผ๋กœ ์ „์†ก)
  • ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€: ํŒจ์Šค ํŒ๋งค ํ†ต๊ณ„, ํšŒ์ฐจ๋ณ„ ์ž…์žฅ ํ˜„ํ™ฉ

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x (๋ชจ๋ธ, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, ์ปจํŠธ๋กค๋Ÿฌ, Eloquent ๊ด€๊ณ„)
  • ํ”„๋ก ํŠธ์—”๋“œ: React 19 (์ปดํฌ๋„ŒํŠธ, useState, ํผ ํ•ธ๋“ค๋ง)
  • ํ’€์Šคํƒ ์—ฐ๊ฒฐ: Inertia.js (Laravel ๋ผ์šฐํŠธ โ†’ React ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ)
  • ์Šคํƒ€์ผ๋ง: Tailwind CSS
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite (๊ฐœ๋ฐœ์šฉ, ๋ฐฐํฌ ์‹œ MySQL/PostgreSQL)

๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์˜ˆ์‹œ

  • SeasonPass: ์‹œ์ฆŒ ํŒจ์Šค ์ƒํ’ˆ (์ด๋ฆ„, ์„ค๋ช…, ๊ฐ€๊ฒฉ, ํฌํ•จ ํšŒ์ฐจ ์ˆ˜, ์œ ํšจ๊ธฐ๊ฐ„)
  • Purchase: ๊ตฌ๋งค ๋‚ด์—ญ (๊ณ ๊ฐ ์ •๋ณด, ํŒจ์Šค ID, ๊ตฌ๋งค ๋‚ ์งœ, ๋‚จ์€ ํšŒ์ฐจ ์ˆ˜)
  • Performance: ๊ณต์—ฐ ํšŒ์ฐจ (์ œ๋ชฉ, ๋‚ ์งœ, ์žฅ์†Œ, ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€)
  • Checkin: ๊ด€๋žŒ ๊ธฐ๋ก (๊ตฌ๋งค ID, ๊ณต์—ฐ ID, ์ฒดํฌ์ธ ์‹œ๊ฐ, ๋ณ„์ /ํ›„๊ธฐ)

์ด ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด Laravel์˜ Eloquent ๊ด€๊ณ„(1:N, N:M), Inertia.js์˜ ํผ ํ•ธ๋“ค๋ง, React์˜ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ตํž ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x + Inertia.js + React 19 ๋ชจ๋…ธ๋ฆฌ์Šค ํ’€์Šคํƒ ๊ตฌ์กฐ (๋ณ„๋„ API ์—†์Œ)
  • โ€ข Laravel Breeze (React + Inertia) starter kit ๋˜๋Š” `npx create laravel` ์‚ฌ์šฉ
  • โ€ข Inertia::render()๋กœ Laravel ์ปจํŠธ๋กค๋Ÿฌ์—์„œ React ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•„์ˆ˜)
  • โ€ข Eloquent ๊ด€๊ณ„ ํ™œ์šฉ: SeasonPass hasMany Purchase, Purchase belongsTo SeasonPass, Purchase hasMany Checkin, Checkin belongsTo Performance
  • โ€ข Tailwind CSS๋กœ ๋ฐ˜์‘ํ˜• UI ๊ตฌํ˜„
  • โ€ข ๊ฒฐ์ œ ์—ฐ๋™ ์—†์Œ (์ฃผ๋ฌธ ์ ‘์ˆ˜๋งŒ, ์‹ค์ œ ๊ฒฐ์ œ๋Š” ๋ณด๋„ˆ์Šค)
  • โ€ข ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ ํฌํ•จ (Laravel Breeze ๊ธฐ๋ณธ ์ œ๊ณต)
  • โ€ข ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ: Laravel Request Validation + React ์—๋Ÿฌ ํ‘œ์‹œ
  • โ€ข ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ: Laravel Storage (public ๋””์Šคํฌ, ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€)

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

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

1
Step 1
mkdir circus-season-pass-platform && cd circus-season-pass-platform

์ด ํด๋”์—์„œ Laravel 13.x + Inertia.js + React 19 ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

## ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ์„ค์ •

Laravel Breeze (React + Inertia) starter kit์„ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”.

```bash
composer create-project laravel/laravel .
composer require laravel/breeze --dev
php artisan breeze:install react
npm install
npm run build
```

- `.env` ํŒŒ์ผ์—์„œ `DB_CONNECTION=sqlite`๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , `database/database.sqlite` ํŒŒ์ผ ์ƒ์„ฑ
- `php artisan migrate` ์‹คํ–‰ํ•˜์—ฌ ๊ธฐ๋ณธ ์ธ์ฆ ํ…Œ์ด๋ธ” ์ƒ์„ฑ
- `php artisan serve`๋กœ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ (http://localhost:8000)
- ๋ณ„๋„ ํ„ฐ๋ฏธ๋„์—์„œ `npm run dev` ์‹คํ–‰ (Vite HMR)

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰๋˜๊ณ , ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- Laravel Breeze ๊ธฐ๋ณธ ์ธ์ฆ ์‹œ์Šคํ…œ ๋™์ž‘
- React ์ปดํฌ๋„ŒํŠธ๊ฐ€ Inertia.js๋ฅผ ํ†ตํ•ด ๋ Œ๋”๋ง๋จ
- Tailwind CSS ์Šคํƒ€์ผ ์ ์šฉ๋จ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x + Breeze + React + Inertia.js ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ๊ธฐ๋ณธ ์ธ์ฆ ํŽ˜์ด์ง€(๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…)๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ์„œ๋ฒ„์™€ Vite๊ฐ€ ์‹คํ–‰ ์ค‘์ด๋ฉฐ, SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

2
Step 2
## ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ

๋‹ค์Œ 4๊ฐœ ํ…Œ์ด๋ธ”์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•˜์„ธ์š”:

### 1. season_passes ํ…Œ์ด๋ธ”
```bash
php artisan make:migration create_season_passes_table
```

์ปฌ๋Ÿผ:
- `id` (primary key)
- `name` (string, ์‹œ์ฆŒ ํŒจ์Šค ์ด๋ฆ„, ์˜ˆ: "๋ด„ ์‹œ์ฆŒ 5ํšŒ๊ถŒ")
- `description` (text, ์„ค๋ช…)
- `price` (decimal, ๊ฐ€๊ฒฉ)
- `included_performances` (integer, ํฌํ•จ ํšŒ์ฐจ ์ˆ˜)
- `valid_from` (date, ์œ ํšจ๊ธฐ๊ฐ„ ์‹œ์ž‘)
- `valid_until` (date, ์œ ํšจ๊ธฐ๊ฐ„ ์ข…๋ฃŒ)
- `poster_image` (string, nullable, ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ)
- `timestamps`

### 2. purchases ํ…Œ์ด๋ธ”
```bash
php artisan make:migration create_purchases_table
```

์ปฌ๋Ÿผ:
- `id` (primary key)
- `user_id` (foreignId, users ํ…Œ์ด๋ธ” ์ฐธ์กฐ, onDelete cascade)
- `season_pass_id` (foreignId, season_passes ํ…Œ์ด๋ธ” ์ฐธ์กฐ, onDelete cascade)
- `customer_name` (string)
- `customer_email` (string)
- `customer_phone` (string)
- `purchased_at` (timestamp, ๊ตฌ๋งค ๋‚ ์งœ)
- `remaining_uses` (integer, ๋‚จ์€ ์‚ฌ์šฉ ํšŸ์ˆ˜, ๊ธฐ๋ณธ๊ฐ’์€ season_pass์˜ included_performances)
- `timestamps`

### 3. performances ํ…Œ์ด๋ธ”
```bash
php artisan make:migration create_performances_table
```

์ปฌ๋Ÿผ:
- `id` (primary key)
- `title` (string, ๊ณต์—ฐ ์ œ๋ชฉ)
- `description` (text)
- `performance_date` (dateTime, ๊ณต์—ฐ ๋‚ ์งœ/์‹œ๊ฐ„)
- `venue` (string, ๊ณต์—ฐ ์žฅ์†Œ)
- `poster_image` (string, nullable)
- `timestamps`

### 4. checkins ํ…Œ์ด๋ธ”
```bash
php artisan make:migration create_checkins_table
```

์ปฌ๋Ÿผ:
- `id` (primary key)
- `purchase_id` (foreignId, purchases ํ…Œ์ด๋ธ” ์ฐธ์กฐ, onDelete cascade)
- `performance_id` (foreignId, performances ํ…Œ์ด๋ธ” ์ฐธ์กฐ, onDelete cascade)
- `checked_in_at` (timestamp, ์ฒดํฌ์ธ ์‹œ๊ฐ)
- `rating` (integer, nullable, 1~5 ๋ณ„์ )
- `review` (text, nullable, ํ›„๊ธฐ)
- `timestamps`

๋ชจ๋“  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ์ž‘์„ฑํ•œ ํ›„ `php artisan migrate`๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- 4๊ฐœ ํ…Œ์ด๋ธ”์ด SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒ์„ฑ๋จ
- ์™ธ๋ž˜ ํ‚ค ๊ด€๊ณ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋จ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: season_passes, purchases, performances, checkins ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜๊ณ , ์™ธ๋ž˜ ํ‚ค ๊ด€๊ณ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. `php artisan migrate:status`๋กœ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

3
Step 3
## Eloquent ๋ชจ๋ธ ์ƒ์„ฑ ๋ฐ ๊ด€๊ณ„ ์„ค์ •

๋‹ค์Œ 4๊ฐœ ๋ชจ๋ธ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•˜์„ธ์š”:

### 1. SeasonPass ๋ชจ๋ธ
```bash
php artisan make:model SeasonPass
```

`app/Models/SeasonPass.php`:
- `$fillable`: name, description, price, included_performances, valid_from, valid_until, poster_image
- `$casts`: valid_from, valid_until์„ 'date'๋กœ ์บ์ŠคํŒ…
- ๊ด€๊ณ„: `public function purchases() { return $this->hasMany(Purchase::class); }`

### 2. Purchase ๋ชจ๋ธ
```bash
php artisan make:model Purchase
```

`app/Models/Purchase.php`:
- `$fillable`: user_id, season_pass_id, customer_name, customer_email, customer_phone, purchased_at, remaining_uses
- `$casts`: purchased_at์„ 'datetime'์œผ๋กœ ์บ์ŠคํŒ…
- ๊ด€๊ณ„:
  - `public function user() { return $this->belongsTo(User::class); }`
  - `public function seasonPass() { return $this->belongsTo(SeasonPass::class); }`
  - `public function checkins() { return $this->hasMany(Checkin::class); }`

### 3. Performance ๋ชจ๋ธ
```bash
php artisan make:model Performance
```

`app/Models/Performance.php`:
- `$fillable`: title, description, performance_date, venue, poster_image
- `$casts`: performance_date๋ฅผ 'datetime'์œผ๋กœ ์บ์ŠคํŒ…
- ๊ด€๊ณ„: `public function checkins() { return $this->hasMany(Checkin::class); }`

### 4. Checkin ๋ชจ๋ธ
```bash
php artisan make:model Checkin
```

`app/Models/Checkin.php`:
- `$fillable`: purchase_id, performance_id, checked_in_at, rating, review
- `$casts`: checked_in_at์„ 'datetime'์œผ๋กœ ์บ์ŠคํŒ…
- ๊ด€๊ณ„:
  - `public function purchase() { return $this->belongsTo(Purchase::class); }`
  - `public function performance() { return $this->belongsTo(Performance::class); }`

๋ชจ๋“  ๋ชจ๋ธ ํŒŒ์ผ์„ ์ž‘์„ฑํ•˜์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- 4๊ฐœ Eloquent ๋ชจ๋ธ์ด ์ƒ์„ฑ๋˜๊ณ , ๊ด€๊ณ„๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋จ
- `php artisan tinker`์—์„œ ๊ด€๊ณ„ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๊ฐ€๋Šฅ (์˜ˆ: `SeasonPass::first()->purchases`)

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: SeasonPass, Purchase, Performance, Checkin ๋ชจ๋ธ์ด ์ƒ์„ฑ๋˜๊ณ , Eloquent ๊ด€๊ณ„(hasMany, belongsTo)๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Tinker์—์„œ ๊ด€๊ณ„ ๋ฉ”์„œ๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4
Step 4
## ์‹œ๋”(Seeder)๋กœ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ

๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”.

### 1. SeasonPassSeeder
```bash
php artisan make:seeder SeasonPassSeeder
```

`database/seeders/SeasonPassSeeder.php`:
- 3๊ฐœ์˜ ์‹œ์ฆŒ ํŒจ์Šค ์ƒ์„ฑ (์˜ˆ: "๋ด„ ์‹œ์ฆŒ 5ํšŒ๊ถŒ", "์—ฌ๋ฆ„ ์‹œ์ฆŒ 10ํšŒ๊ถŒ", "๊ฐ€์„ ์‹œ์ฆŒ 3ํšŒ๊ถŒ")
- ๊ฐ ํŒจ์Šค์˜ ๊ฐ€๊ฒฉ, ํฌํ•จ ํšŒ์ฐจ ์ˆ˜, ์œ ํšจ๊ธฐ๊ฐ„ ์„ค์ •
- `SeasonPass::create([...])` ์‚ฌ์šฉ

### 2. PerformanceSeeder
```bash
php artisan make:seeder PerformanceSeeder
```

`database/seeders/PerformanceSeeder.php`:
- 10๊ฐœ ์ •๋„์˜ ๊ณต์—ฐ ํšŒ์ฐจ ์ƒ์„ฑ (์˜ˆ: "์„œ์ปค์Šค ์˜คํ”„๋‹ ์‡ผ", "๊ณต์ค‘ ๊ณก์˜ˆ ํŠน์ง‘", "๋งˆ์ˆ ๊ณผ ์ €๊ธ€๋ง์˜ ๋ฐค" ๋“ฑ)
- ๊ฐ ๊ณต์—ฐ์˜ ๋‚ ์งœ, ์žฅ์†Œ ์„ค์ • (๋ฏธ๋ž˜ ๋‚ ์งœ ํฌํ•จ)
- `Performance::create([...])` ์‚ฌ์šฉ

### 3. DatabaseSeeder์— ๋“ฑ๋ก
`database/seeders/DatabaseSeeder.php`์˜ `run()` ๋ฉ”์„œ๋“œ์—์„œ:
```php
$this->call([
    SeasonPassSeeder::class,
    PerformanceSeeder::class,
]);
```

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

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- season_passes ํ…Œ์ด๋ธ”์— 3๊ฐœ ๋ ˆ์ฝ”๋“œ
- performances ํ…Œ์ด๋ธ”์— 10๊ฐœ ๋ ˆ์ฝ”๋“œ
- `php artisan tinker`์—์„œ `SeasonPass::count()`, `Performance::count()` ํ™•์ธ ๊ฐ€๋Šฅ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์‹œ๋”๊ฐ€ ์‹คํ–‰๋˜์–ด season_passes ํ…Œ์ด๋ธ”์— 3๊ฐœ, performances ํ…Œ์ด๋ธ”์— 10๊ฐœ์˜ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. Tinker์—์„œ ๋ฐ์ดํ„ฐ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

5
Step 5
## ์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก ํŽ˜์ด์ง€ (ํ™ˆ) ๊ตฌํ˜„

Laravel ์ปจํŠธ๋กค๋Ÿฌ์™€ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก์„ ํ‘œ์‹œํ•˜์„ธ์š”.

### 1. SeasonPassController ์ƒ์„ฑ
```bash
php artisan make:controller SeasonPassController
```

`app/Http/Controllers/SeasonPassController.php`์˜ `index()` ๋ฉ”์„œ๋“œ:
```php
public function index()
{
    $seasonPasses = SeasonPass::orderBy('valid_from', 'desc')->get();
    return Inertia::render('SeasonPass/Index', [
        'seasonPasses' => $seasonPasses
    ]);
}
```

### 2. ๋ผ์šฐํŠธ ๋“ฑ๋ก
`routes/web.php`์— ์ถ”๊ฐ€:
```php
use App\Http\Controllers\SeasonPassController;

Route::get('/', [SeasonPassController::class, 'index'])->name('home');
```

### 3. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
`resources/js/Pages/SeasonPass/Index.jsx` ํŒŒ์ผ ์ƒ์„ฑ:
- props๋กœ `seasonPasses` ๋ฐฐ์—ด ๋ฐ›๊ธฐ
- Tailwind CSS๋กœ ์นด๋“œ ๋ ˆ์ด์•„์›ƒ ๊ตฌํ˜„ (๊ทธ๋ฆฌ๋“œ ๋˜๋Š” flexbox)
- ๊ฐ ์นด๋“œ์— ํŒจ์Šค ์ด๋ฆ„, ์„ค๋ช…, ๊ฐ€๊ฒฉ, ํฌํ•จ ํšŒ์ฐจ ์ˆ˜, ์œ ํšจ๊ธฐ๊ฐ„ ํ‘œ์‹œ
- "์ž์„ธํžˆ ๋ณด๊ธฐ" ๋ฒ„ํŠผ ์ถ”๊ฐ€ (์•„์ง ๋งํฌ ์—†์Œ, ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„)

์˜ˆ์‹œ ๊ตฌ์กฐ:
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';

export default function Index({ auth, seasonPasses }) {
    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title="์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก" />
            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <h1 className="text-3xl font-bold mb-6">๐ŸŽช ์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก</h1>
                    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
                        {seasonPasses.map(pass => (
                            <div key={pass.id} className="bg-white rounded-lg shadow-md p-6">
                                <h2 className="text-xl font-semibold mb-2">{pass.name}</h2>
                                <p className="text-gray-600 mb-4">{pass.description}</p>
                                <div className="text-2xl font-bold text-indigo-600 mb-2">
                                    {pass.price.toLocaleString()}์›
                                </div>
                                <p className="text-sm text-gray-500 mb-4">
                                    ํฌํ•จ ํšŒ์ฐจ: {pass.included_performances}ํšŒ
                                </p>
                                <p className="text-xs text-gray-400 mb-4">
                                    ์œ ํšจ๊ธฐ๊ฐ„: {pass.valid_from} ~ {pass.valid_until}
                                </p>
                                <button className="w-full bg-indigo-600 text-white py-2 rounded hover:bg-indigo-700">
                                    ์ž์„ธํžˆ ๋ณด๊ธฐ
                                </button>
                            </div>
                        ))}
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8000 ์ ‘์†ํ•˜์—ฌ ์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก์ด ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- ํ™ˆ ํŽ˜์ด์ง€์— ์‹œ์ฆŒ ํŒจ์Šค 3๊ฐœ๊ฐ€ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋จ
- ๊ฐ ์นด๋“œ์— ์ด๋ฆ„, ์„ค๋ช…, ๊ฐ€๊ฒฉ, ํšŒ์ฐจ ์ˆ˜, ์œ ํšจ๊ธฐ๊ฐ„ ํ‘œ์‹œ
- Tailwind CSS ์Šคํƒ€์ผ ์ ์šฉ๋จ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ™ˆ ํŽ˜์ด์ง€(/)์—์„œ ์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก์ด ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ์นด๋“œ๋Š” ํŒจ์Šค ์ •๋ณด๋ฅผ ๋ณด๊ธฐ ์ข‹๊ฒŒ ํ‘œ์‹œํ•˜๋ฉฐ, "์ž์„ธํžˆ ๋ณด๊ธฐ" ๋ฒ„ํŠผ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

6
Step 6
## ์‹œ์ฆŒ ํŒจ์Šค ์ƒ์„ธ ํŽ˜์ด์ง€ & ๊ตฌ๋งค ํผ ๊ตฌํ˜„

### 1. SeasonPassController์— show() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
`app/Http/Controllers/SeasonPassController.php`:
```php
public function show(SeasonPass $seasonPass)
{
    return Inertia::render('SeasonPass/Show', [
        'seasonPass' => $seasonPass
    ]);
}
```

### 2. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
`routes/web.php`:
```php
Route::get('/season-pass/{seasonPass}', [SeasonPassController::class, 'show'])->name('season-pass.show');
```

### 3. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
`resources/js/Pages/SeasonPass/Show.jsx`:
- props๋กœ `seasonPass` ๊ฐ์ฒด ๋ฐ›๊ธฐ
- ํŒจ์Šค ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ (์ด๋ฆ„, ์„ค๋ช…, ๊ฐ€๊ฒฉ, ํฌํ•จ ํšŒ์ฐจ, ์œ ํšจ๊ธฐ๊ฐ„)
- ๊ตฌ๋งค ํผ ์ถ”๊ฐ€:
  - ๊ณ ๊ฐ ์ด๋ฆ„ (customer_name)
  - ์ด๋ฉ”์ผ (customer_email)
  - ์ „ํ™”๋ฒˆํ˜ธ (customer_phone)
  - "๊ตฌ๋งคํ•˜๊ธฐ" ๋ฒ„ํŠผ
- Inertia.js์˜ `useForm` ํ›… ์‚ฌ์šฉํ•˜์—ฌ ํผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
- ํผ ์ œ์ถœ ์‹œ POST ์š”์ฒญ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„)

์˜ˆ์‹œ:
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';

export default function Show({ auth, seasonPass }) {
    const { data, setData, post, processing, errors } = useForm({
        customer_name: auth.user?.name || '',
        customer_email: auth.user?.email || '',
        customer_phone: '',
    });

    const handleSubmit = (e) => {
        e.preventDefault();
        post(route('purchase.store', seasonPass.id));
    };

    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title={seasonPass.name} />
            <div className="py-12">
                <div className="max-w-4xl mx-auto sm:px-6 lg:px-8">
                    <div className="bg-white rounded-lg shadow-md p-8">
                        <h1 className="text-3xl font-bold mb-4">{seasonPass.name}</h1>
                        <p className="text-gray-600 mb-6">{seasonPass.description}</p>
                        <div className="grid grid-cols-2 gap-4 mb-8">
                            <div>
                                <span className="text-gray-500">๊ฐ€๊ฒฉ:</span>
                                <span className="ml-2 text-2xl font-bold text-indigo-600">
                                    {seasonPass.price.toLocaleString()}์›
                                </span>
                            </div>
                            <div>
                                <span className="text-gray-500">ํฌํ•จ ํšŒ์ฐจ:</span>
                                <span className="ml-2 text-xl font-semibold">
                                    {seasonPass.included_performances}ํšŒ
                                </span>
                            </div>
                            <div className="col-span-2">
                                <span className="text-gray-500">์œ ํšจ๊ธฐ๊ฐ„:</span>
                                <span className="ml-2">
                                    {seasonPass.valid_from} ~ {seasonPass.valid_until}
                                </span>
                            </div>
                        </div>

                        <form onSubmit={handleSubmit} className="space-y-4">
                            <h2 className="text-2xl font-semibold mb-4">๊ตฌ๋งค ์ •๋ณด ์ž…๋ ฅ</h2>
                            <div>
                                <label className="block text-sm font-medium mb-1">์ด๋ฆ„</label>
                                <input
                                    type="text"
                                    value={data.customer_name}
                                    onChange={e => setData('customer_name', e.target.value)}
                                    className="w-full border rounded px-3 py-2"
                                    required
                                />
                                {errors.customer_name && <p className="text-red-500 text-sm">{errors.customer_name}</p>}
                            </div>
                            <div>
                                <label className="block text-sm font-medium mb-1">์ด๋ฉ”์ผ</label>
                                <input
                                    type="email"
                                    value={data.customer_email}
                                    onChange={e => setData('customer_email', e.target.value)}
                                    className="w-full border rounded px-3 py-2"
                                    required
                                />
                                {errors.customer_email && <p className="text-red-500 text-sm">{errors.customer_email}</p>}
                            </div>
                            <div>
                                <label className="block text-sm font-medium mb-1">์ „ํ™”๋ฒˆํ˜ธ</label>
                                <input
                                    type="tel"
                                    value={data.customer_phone}
                                    onChange={e => setData('customer_phone', e.target.value)}
                                    className="w-full border rounded px-3 py-2"
                                    required
                                />
                                {errors.customer_phone && <p className="text-red-500 text-sm">{errors.customer_phone}</p>}
                            </div>
                            <button
                                type="submit"
                                disabled={processing}
                                className="w-full bg-indigo-600 text-white py-3 rounded-lg hover:bg-indigo-700 disabled:opacity-50"
                            >
                                {processing ? '์ฒ˜๋ฆฌ ์ค‘...' : '๊ตฌ๋งคํ•˜๊ธฐ'}
                            </button>
                        </form>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

### 4. Index.jsx์˜ "์ž์„ธํžˆ ๋ณด๊ธฐ" ๋ฒ„ํŠผ ๋งํฌ ์—ฐ๊ฒฐ
`resources/js/Pages/SeasonPass/Index.jsx`์˜ ๋ฒ„ํŠผ์„:
```jsx
import { Link } from '@inertiajs/react';

<Link
    href={route('season-pass.show', pass.id)}
    className="block w-full bg-indigo-600 text-white py-2 rounded hover:bg-indigo-700 text-center"
>
    ์ž์„ธํžˆ ๋ณด๊ธฐ
</Link>
```

๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹œ์ฆŒ ํŒจ์Šค ์นด๋“œ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ƒ์„ธ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- ์‹œ์ฆŒ ํŒจ์Šค ์ƒ์„ธ ํŽ˜์ด์ง€์— ํŒจ์Šค ์ •๋ณด์™€ ๊ตฌ๋งค ํผ ํ‘œ์‹œ
- ํผ ํ•„๋“œ์— ๊ธฐ๋ณธ๊ฐ’ ์ž๋™ ์ž…๋ ฅ (๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ด๋ฆ„/์ด๋ฉ”์ผ)
- "๊ตฌ๋งคํ•˜๊ธฐ" ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์•„์ง ๋™์ž‘ํ•˜์ง€ ์•Š์Œ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„)

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์‹œ์ฆŒ ํŒจ์Šค ์ƒ์„ธ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๊ณ , ๊ตฌ๋งค ํผ์ด ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์ž๋™์œผ๋กœ ์ž…๋ ฅ๋˜๋ฉฐ, ํผ ์ œ์ถœ ์ค€๋น„๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

7
Step 7
## ๊ตฌ๋งค ๊ธฐ๋Šฅ ๊ตฌํ˜„ (Purchase ์ƒ์„ฑ)

### 1. PurchaseController ์ƒ์„ฑ
```bash
php artisan make:controller PurchaseController
```

`app/Http/Controllers/PurchaseController.php`์˜ `store()` ๋ฉ”์„œ๋“œ:
```php
use App\Models\Purchase;
use App\Models\SeasonPass;
use Illuminate\Http\Request;

public function store(Request $request, SeasonPass $seasonPass)
{
    $validated = $request->validate([
        'customer_name' => 'required|string|max:255',
        'customer_email' => 'required|email|max:255',
        'customer_phone' => 'required|string|max:20',
    ]);

    Purchase::create([
        'user_id' => auth()->id(),
        'season_pass_id' => $seasonPass->id,
        'customer_name' => $validated['customer_name'],
        'customer_email' => $validated['customer_email'],
        'customer_phone' => $validated['customer_phone'],
        'purchased_at' => now(),
        'remaining_uses' => $seasonPass->included_performances,
    ]);

    return redirect()->route('my-passes')->with('success', '์‹œ์ฆŒ ํŒจ์Šค ๊ตฌ๋งค๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
}
```

### 2. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
`routes/web.php`:
```php
use App\Http\Controllers\PurchaseController;

Route::middleware('auth')->group(function () {
    Route::post('/purchase/{seasonPass}', [PurchaseController::class, 'store'])->name('purchase.store');
});
```

### 3. "๋‚ด ํŒจ์Šค" ํŽ˜์ด์ง€ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
`app/Http/Controllers/PurchaseController.php`์— `index()` ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€:
```php
public function index()
{
    $purchases = Purchase::with('seasonPass')
        ->where('user_id', auth()->id())
        ->orderBy('purchased_at', 'desc')
        ->get();

    return Inertia::render('Purchase/Index', [
        'purchases' => $purchases
    ]);
}
```

### 4. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
```php
Route::middleware('auth')->group(function () {
    Route::get('/my-passes', [PurchaseController::class, 'index'])->name('my-passes');
});
```

### 5. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
`resources/js/Pages/Purchase/Index.jsx`:
- props๋กœ `purchases` ๋ฐฐ์—ด ๋ฐ›๊ธฐ
- ๊ฐ ๊ตฌ๋งค ๋‚ด์—ญ์„ ์นด๋“œ๋กœ ํ‘œ์‹œ (์‹œ์ฆŒ ํŒจ์Šค ์ด๋ฆ„, ๊ตฌ๋งค ๋‚ ์งœ, ๋‚จ์€ ํšŒ์ฐจ ์ˆ˜)
- ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”๋กœ ์‚ฌ์šฉ๋ฅ  ํ‘œ์‹œ (์˜ˆ: 5ํšŒ ์ค‘ 2ํšŒ ์‚ฌ์šฉ = 40%)

์˜ˆ์‹œ:
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';

export default function Index({ auth, purchases }) {
    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title="๋‚ด ํŒจ์Šค" />
            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <h1 className="text-3xl font-bold mb-6">๐ŸŽŸ๏ธ ๋‚ด ํŒจ์Šค</h1>
                    {purchases.length === 0 ? (
                        <p className="text-gray-500">๊ตฌ๋งคํ•œ ํŒจ์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
                    ) : (
                        <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                            {purchases.map(purchase => {
                                const totalUses = purchase.season_pass.included_performances;
                                const usedUses = totalUses - purchase.remaining_uses;
                                const usagePercent = (usedUses / totalUses) * 100;

                                return (
                                    <div key={purchase.id} className="bg-white rounded-lg shadow-md p-6">
                                        <h2 className="text-xl font-semibold mb-2">{purchase.season_pass.name}</h2>
                                        <p className="text-sm text-gray-500 mb-4">
                                            ๊ตฌ๋งค์ผ: {new Date(purchase.purchased_at).toLocaleDateString()}
                                        </p>
                                        <div className="mb-4">
                                            <div className="flex justify-between text-sm mb-1">
                                                <span>์‚ฌ์šฉ ํ˜„ํ™ฉ</span>
                                                <span>{usedUses} / {totalUses}ํšŒ</span>
                                            </div>
                                            <div className="w-full bg-gray-200 rounded-full h-2">
                                                <div
                                                    className="bg-indigo-600 h-2 rounded-full"
                                                    style={{ width: `${usagePercent}%` }}
                                                ></div>
                                            </div>
                                        </div>
                                        <p className="text-lg font-semibold text-indigo-600">
                                            ๋‚จ์€ ํšŸ์ˆ˜: {purchase.remaining_uses}ํšŒ
                                        </p>
                                    </div>
                                );
                            })}
                        </div>
                    )}
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

### 6. ๋„ค๋น„๊ฒŒ์ด์…˜์— "๋‚ด ํŒจ์Šค" ๋งํฌ ์ถ”๊ฐ€
`resources/js/Layouts/AuthenticatedLayout.jsx`์˜ ๋„ค๋น„๊ฒŒ์ด์…˜์—:
```jsx
<Link href={route('my-passes')} className="...">
    ๋‚ด ํŒจ์Šค
</Link>
```

์‹œ์ฆŒ ํŒจ์Šค๋ฅผ ๊ตฌ๋งคํ•˜๊ณ , "๋‚ด ํŒจ์Šค" ํŽ˜์ด์ง€์—์„œ ๊ตฌ๋งค ๋‚ด์—ญ์ด ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- ์‹œ์ฆŒ ํŒจ์Šค ๊ตฌ๋งค ์‹œ Purchase ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ๋จ
- "๋‚ด ํŒจ์Šค" ํŽ˜์ด์ง€์— ๊ตฌ๋งคํ•œ ํŒจ์Šค ๋ชฉ๋ก ํ‘œ์‹œ
- ๊ฐ ํŒจ์Šค์˜ ์‚ฌ์šฉ๋ฅ ์ด ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”๋กœ ํ‘œ์‹œ๋จ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์‹œ์ฆŒ ํŒจ์Šค ๊ตฌ๋งค๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ณ , "๋‚ด ํŒจ์Šค" ํŽ˜์ด์ง€์—์„œ ๊ตฌ๋งค ๋‚ด์—ญ๊ณผ ์‚ฌ์šฉ ํ˜„ํ™ฉ(ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”)์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋‚จ์€ ํšŸ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.

8
Step 8
## ๊ณต์—ฐ ํšŒ์ฐจ ๋ชฉ๋ก & ์ฒดํฌ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„

### 1. PerformanceController ์ƒ์„ฑ
```bash
php artisan make:controller PerformanceController
```

`app/Http/Controllers/PerformanceController.php`์˜ `index()` ๋ฉ”์„œ๋“œ:
```php
use App\Models\Performance;

public function index()
{
    $performances = Performance::orderBy('performance_date', 'asc')->get();
    return Inertia::render('Performance/Index', [
        'performances' => $performances
    ]);
}
```

### 2. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
`routes/web.php`:
```php
use App\Http\Controllers\PerformanceController;

Route::get('/performances', [PerformanceController::class, 'index'])->name('performances.index');
```

### 3. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
`resources/js/Pages/Performance/Index.jsx`:
- props๋กœ `performances` ๋ฐฐ์—ด ๋ฐ›๊ธฐ
- ๊ฐ ๊ณต์—ฐ์„ ์นด๋“œ๋กœ ํ‘œ์‹œ (์ œ๋ชฉ, ์„ค๋ช…, ๋‚ ์งœ, ์žฅ์†Œ)
- "์ฒดํฌ์ธํ•˜๊ธฐ" ๋ฒ„ํŠผ ์ถ”๊ฐ€ (๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ํ‘œ์‹œ)

### 4. CheckinController ์ƒ์„ฑ
```bash
php artisan make:controller CheckinController
```

`app/Http/Controllers/CheckinController.php`์˜ `store()` ๋ฉ”์„œ๋“œ:
```php
use App\Models\Checkin;
use App\Models\Purchase;
use App\Models\Performance;
use Illuminate\Http\Request;

public function store(Request $request, Performance $performance)
{
    // ์‚ฌ์šฉ์ž์˜ ํŒจ์Šค ์ค‘ ๋‚จ์€ ํšŸ์ˆ˜๊ฐ€ ์žˆ๋Š” ๊ฒƒ ์ฐพ๊ธฐ
    $purchase = Purchase::where('user_id', auth()->id())
        ->where('remaining_uses', '>', 0)
        ->first();

    if (!$purchase) {
        return back()->withErrors(['error' => '์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํŒจ์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.']);
    }

    // ์ด๋ฏธ ์ฒดํฌ์ธํ–ˆ๋Š”์ง€ ํ™•์ธ
    $existingCheckin = Checkin::where('purchase_id', $purchase->id)
        ->where('performance_id', $performance->id)
        ->exists();

    if ($existingCheckin) {
        return back()->withErrors(['error' => '์ด๋ฏธ ์ฒดํฌ์ธํ•œ ๊ณต์—ฐ์ž…๋‹ˆ๋‹ค.']);
    }

    // ์ฒดํฌ์ธ ์ƒ์„ฑ
    Checkin::create([
        'purchase_id' => $purchase->id,
        'performance_id' => $performance->id,
        'checked_in_at' => now(),
    ]);

    // ๋‚จ์€ ํšŸ์ˆ˜ ๊ฐ์†Œ
    $purchase->decrement('remaining_uses');

    return back()->with('success', '์ฒดํฌ์ธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
}
```

### 5. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
```php
Route::middleware('auth')->group(function () {
    Route::post('/checkin/{performance}', [CheckinController::class, 'store'])->name('checkin.store');
});
```

### 6. Performance/Index.jsx ์—…๋ฐ์ดํŠธ
์ฒดํฌ์ธ ๋ฒ„ํŠผ ์ถ”๊ฐ€:
```jsx
import { useForm } from '@inertiajs/react';

export default function Index({ auth, performances }) {
    const { post, processing } = useForm();

    const handleCheckin = (performanceId) => {
        if (confirm('์ด ๊ณต์—ฐ์— ์ฒดํฌ์ธํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) {
            post(route('checkin.store', performanceId));
        }
    };

    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title="๊ณต์—ฐ ์ผ์ •" />
            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <h1 className="text-3xl font-bold mb-6">๐ŸŽช ๊ณต์—ฐ ์ผ์ •</h1>
                    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                        {performances.map(performance => (
                            <div key={performance.id} className="bg-white rounded-lg shadow-md p-6">
                                <h2 className="text-xl font-semibold mb-2">{performance.title}</h2>
                                <p className="text-gray-600 mb-4">{performance.description}</p>
                                <p className="text-sm text-gray-500 mb-2">
                                    ๐Ÿ“… {new Date(performance.performance_date).toLocaleString()}
                                </p>
                                <p className="text-sm text-gray-500 mb-4">
                                    ๐Ÿ“ {performance.venue}
                                </p>
                                {auth.user && (
                                    <button
                                        onClick={() => handleCheckin(performance.id)}
                                        disabled={processing}
                                        className="w-full bg-green-600 text-white py-2 rounded hover:bg-green-700 disabled:opacity-50"
                                    >
                                        ์ฒดํฌ์ธํ•˜๊ธฐ
                                    </button>
                                )}
                            </div>
                        ))}
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

### 7. ๋„ค๋น„๊ฒŒ์ด์…˜์— "๊ณต์—ฐ ์ผ์ •" ๋งํฌ ์ถ”๊ฐ€
`resources/js/Layouts/AuthenticatedLayout.jsx`:
```jsx
<Link href={route('performances.index')} className="...">
    ๊ณต์—ฐ ์ผ์ •
</Link>
```

๊ณต์—ฐ ์ผ์ • ํŽ˜์ด์ง€์—์„œ ์ฒดํฌ์ธํ•˜๊ณ , "๋‚ด ํŒจ์Šค" ํŽ˜์ด์ง€์—์„œ ๋‚จ์€ ํšŸ์ˆ˜๊ฐ€ ๊ฐ์†Œํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- ๊ณต์—ฐ ์ผ์ • ํŽ˜์ด์ง€์— ๋ชจ๋“  ๊ณต์—ฐ ํ‘œ์‹œ
- ์ฒดํฌ์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ Checkin ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ ๋ฐ ๋‚จ์€ ํšŸ์ˆ˜ ๊ฐ์†Œ
- ์ค‘๋ณต ์ฒดํฌ์ธ ๋ฐฉ์ง€
- ํŒจ์Šค๊ฐ€ ์—†์œผ๋ฉด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ณต์—ฐ ์ผ์ • ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๊ณ , ์ฒดํฌ์ธ ๊ธฐ๋Šฅ์ด ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์ฒดํฌ์ธ ์‹œ ๋‚จ์€ ํšŸ์ˆ˜๊ฐ€ ๊ฐ์†Œํ•˜๋ฉฐ, ์ค‘๋ณต ์ฒดํฌ์ธ์ด ๋ฐฉ์ง€๋ฉ๋‹ˆ๋‹ค.

9
Step 9
## ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ & ํ›„๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„

### 1. PurchaseController์— show() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
`app/Http/Controllers/PurchaseController.php`:
```php
public function show(Purchase $purchase)
{
    // ๋ณธ์ธ์˜ ํŒจ์Šค๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ
    if ($purchase->user_id !== auth()->id()) {
        abort(403);
    }

    $purchase->load(['seasonPass', 'checkins.performance']);

    return Inertia::render('Purchase/Show', [
        'purchase' => $purchase
    ]);
}
```

### 2. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
```php
Route::middleware('auth')->group(function () {
    Route::get('/my-passes/{purchase}', [PurchaseController::class, 'show'])->name('my-passes.show');
});
```

### 3. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
`resources/js/Pages/Purchase/Show.jsx`:
- props๋กœ `purchase` ๊ฐ์ฒด ๋ฐ›๊ธฐ (checkins ๊ด€๊ณ„ ํฌํ•จ)
- ํŒจ์Šค ์ •๋ณด ํ‘œ์‹œ (์ด๋ฆ„, ๊ตฌ๋งค์ผ, ์‚ฌ์šฉ ํ˜„ํ™ฉ)
- ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ ํƒ€์ž„๋ผ์ธ ํ‘œ์‹œ (์ฒดํฌ์ธํ•œ ๊ณต์—ฐ ๋ชฉ๋ก)
- ๊ฐ ๊ณต์—ฐ์— ๋ณ„์ /ํ›„๊ธฐ ๋‚จ๊ธฐ๊ธฐ ํผ ์ถ”๊ฐ€

์˜ˆ์‹œ:
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';
import { useState } from 'react';

export default function Show({ auth, purchase }) {
    const [editingCheckin, setEditingCheckin] = useState(null);
    const { data, setData, put, processing } = useForm({
        rating: 0,
        review: '',
    });

    const handleReviewSubmit = (e, checkinId) => {
        e.preventDefault();
        put(route('checkin.update', checkinId), {
            onSuccess: () => setEditingCheckin(null),
        });
    };

    const totalUses = purchase.season_pass.included_performances;
    const usedUses = totalUses - purchase.remaining_uses;
    const usagePercent = (usedUses / totalUses) * 100;

    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title="ํŒจ์Šค ์ƒ์„ธ" />
            <div className="py-12">
                <div className="max-w-4xl mx-auto sm:px-6 lg:px-8">
                    <div className="bg-white rounded-lg shadow-md p-8 mb-6">
                        <h1 className="text-3xl font-bold mb-4">{purchase.season_pass.name}</h1>
                        <p className="text-sm text-gray-500 mb-4">
                            ๊ตฌ๋งค์ผ: {new Date(purchase.purchased_at).toLocaleDateString()}
                        </p>
                        <div className="mb-4">
                            <div className="flex justify-between text-sm mb-1">
                                <span>์‚ฌ์šฉ ํ˜„ํ™ฉ</span>
                                <span>{usedUses} / {totalUses}ํšŒ</span>
                            </div>
                            <div className="w-full bg-gray-200 rounded-full h-3">
                                <div
                                    className="bg-indigo-600 h-3 rounded-full"
                                    style={{ width: `${usagePercent}%` }}
                                ></div>
                            </div>
                        </div>
                    </div>

                    <div className="bg-white rounded-lg shadow-md p-8">
                        <h2 className="text-2xl font-semibold mb-6">๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ</h2>
                        {purchase.checkins.length === 0 ? (
                            <p className="text-gray-500">์•„์ง ๊ด€๋žŒํ•œ ๊ณต์—ฐ์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
                        ) : (
                            <div className="space-y-6">
                                {purchase.checkins.map(checkin => (
                                    <div key={checkin.id} className="border-l-4 border-indigo-600 pl-4">
                                        <h3 className="text-lg font-semibold">{checkin.performance.title}</h3>
                                        <p className="text-sm text-gray-500 mb-2">
                                            ์ฒดํฌ์ธ: {new Date(checkin.checked_in_at).toLocaleString()}
                                        </p>
                                        <p className="text-sm text-gray-600 mb-2">
                                            ๐Ÿ“ {checkin.performance.venue}
                                        </p>
                                        {checkin.rating ? (
                                            <div className="bg-gray-50 p-4 rounded">
                                                <div className="flex items-center mb-2">
                                                    <span className="text-yellow-500 text-xl">
                                                        {'โญ'.repeat(checkin.rating)}
                                                    </span>
                                                </div>
                                                <p className="text-gray-700">{checkin.review}</p>
                                            </div>
                                        ) : (
                                            editingCheckin === checkin.id ? (
                                                <form onSubmit={(e) => handleReviewSubmit(e, checkin.id)} className="bg-gray-50 p-4 rounded">
                                                    <div className="mb-3">
                                                        <label className="block text-sm font-medium mb-1">๋ณ„์ </label>
                                                        <select
                                                            value={data.rating}
                                                            onChange={e => setData('rating', e.target.value)}
                                                            className="w-full border rounded px-3 py-2"
                                                            required
                                                        >
                                                            <option value="">์„ ํƒํ•˜์„ธ์š”</option>
                                                            <option value="1">โญ 1์ </option>
                                                            <option value="2">โญโญ 2์ </option>
                                                            <option value="3">โญโญโญ 3์ </option>
                                                            <option value="4">โญโญโญโญ 4์ </option>
                                                            <option value="5">โญโญโญโญโญ 5์ </option>
                                                        </select>
                                                    </div>
                                                    <div className="mb-3">
                                                        <label className="block text-sm font-medium mb-1">ํ›„๊ธฐ</label>
                                                        <textarea
                                                            value={data.review}
                                                            onChange={e => setData('review', e.target.value)}
                                                            className="w-full border rounded px-3 py-2"
                                                            rows="3"
                                                            placeholder="๊ณต์—ฐ ์†Œ๊ฐ์„ ๋‚จ๊ฒจ์ฃผ์„ธ์š”"
                                                        />
                                                    </div>
                                                    <div className="flex gap-2">
                                                        <button
                                                            type="submit"
                                                            disabled={processing}
                                                            className="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700"
                                                        >
                                                            ์ €์žฅ
                                                        </button>
                                                        <button
                                                            type="button"
                                                            onClick={() => setEditingCheckin(null)}
                                                            className="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400"
                                                        >
                                                            ์ทจ์†Œ
                                                        </button>
                                                    </div>
                                                </form>
                                            ) : (
                                                <button
                                                    onClick={() => setEditingCheckin(checkin.id)}
                                                    className="text-indigo-600 hover:underline text-sm"
                                                >
                                                    ํ›„๊ธฐ ๋‚จ๊ธฐ๊ธฐ
                                                </button>
                                            )
                                        )}
                                    </div>
                                ))}
                            </div>
                        )}
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

### 4. CheckinController์— update() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
`app/Http/Controllers/CheckinController.php`:
```php
public function update(Request $request, Checkin $checkin)
{
    // ๋ณธ์ธ์˜ ์ฒดํฌ์ธ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ
    if ($checkin->purchase->user_id !== auth()->id()) {
        abort(403);
    }

    $validated = $request->validate([
        'rating' => 'required|integer|min:1|max:5',
        'review' => 'nullable|string|max:1000',
    ]);

    $checkin->update($validated);

    return back()->with('success', 'ํ›„๊ธฐ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
}
```

### 5. ๋ผ์šฐํŠธ ์ถ”๊ฐ€
```php
Route::middleware('auth')->group(function () {
    Route::put('/checkin/{checkin}', [CheckinController::class, 'update'])->name('checkin.update');
});
```

### 6. Purchase/Index.jsx์—์„œ ํŒจ์Šค ์ƒ์„ธ ๋งํฌ ์ถ”๊ฐ€
๊ฐ ํŒจ์Šค ์นด๋“œ์— "์ƒ์„ธ ๋ณด๊ธฐ" ๋ฒ„ํŠผ ์ถ”๊ฐ€:
```jsx
import { Link } from '@inertiajs/react';

<Link
    href={route('my-passes.show', purchase.id)}
    className="block w-full bg-indigo-600 text-white py-2 rounded hover:bg-indigo-700 text-center mt-4"
>
    ์ƒ์„ธ ๋ณด๊ธฐ
</Link>
```

"๋‚ด ํŒจ์Šค" ํŽ˜์ด์ง€์—์„œ ํŒจ์Šค๋ฅผ ํด๋ฆญํ•˜์—ฌ ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ํ›„๊ธฐ๋ฅผ ๋‚จ๊ฒจ๋ณด์„ธ์š”.

### ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ
- ํŒจ์Šค ์ƒ์„ธ ํŽ˜์ด์ง€์— ์ฒดํฌ์ธํ•œ ๊ณต์—ฐ ๋ชฉ๋ก์ด ํƒ€์ž„๋ผ์ธ์œผ๋กœ ํ‘œ์‹œ๋จ
- ๊ฐ ๊ณต์—ฐ์— ๋ณ„์ /ํ›„๊ธฐ ๋‚จ๊ธฐ๊ธฐ ๊ฐ€๋Šฅ
- ํ›„๊ธฐ ์ €์žฅ ํ›„ ํŽ˜์ด์ง€์— ๋ฐ”๋กœ ๋ฐ˜์˜๋จ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํŒจ์Šค ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ํƒ€์ž„๋ผ์ธ์œผ๋กœ ํ‘œ์‹œ๋˜๊ณ , ๊ฐ ๊ณต์—ฐ์— ๋ณ„์ ๊ณผ ํ›„๊ธฐ๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ›„๊ธฐ๊ฐ€ ์ €์žฅ๋˜๋ฉด ํ™”๋ฉด์— ์ฆ‰์‹œ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.

10
Step 10
## ์ตœ์ข… ๋งˆ๋ฌด๋ฆฌ & ๊ฐœ์„  ์‚ฌํ•ญ

### 1. ํ”Œ๋ž˜์‹œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
`resources/js/Components/FlashMessage.jsx` ์ƒ์„ฑ:
```jsx
import { usePage } from '@inertiajs/react';
import { useEffect, useState } from 'react';

export default function FlashMessage() {
    const { flash } = usePage().props;
    const [visible, setVisible] = useState(false);

    useEffect(() => {
        if (flash.success || flash.error) {
            setVisible(true);
            setTimeout(() => setVisible(false), 3000);
        }
    }, [flash]);

    if (!visible) return null;

    return (
        <div className="fixed top-4 right-4 z-50 animate-fade-in">
            {flash.success && (
                <div className="bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg">
                    โœ“ {flash.success}
                </div>
            )}
            {flash.error && (
                <div className="bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg">
                    โœ— {flash.error}
                </div>
            )}
        </div>
    );
}
```

`resources/js/Layouts/AuthenticatedLayout.jsx`์— ์ถ”๊ฐ€:
```jsx
import FlashMessage from '@/Components/FlashMessage';

// Layout ๋‚ด๋ถ€์—
<FlashMessage />
```

### 2. ํ™ˆ ํŽ˜์ด์ง€์— ํžˆ์–ด๋กœ ์„น์…˜ ์ถ”๊ฐ€
`resources/js/Pages/SeasonPass/Index.jsx` ์ƒ๋‹จ์—:
```jsx
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 text-white py-16 mb-8">
    <div className="max-w-7xl mx-auto text-center px-4">
        <h1 className="text-5xl font-bold mb-4">๐ŸŽช ์„œ์ปค์Šค ์‹œ์ฆŒ ํŒจ์Šค</h1>
        <p className="text-xl">์—ฌ๋Ÿฌ ๊ณต์—ฐ์„ ํ•œ ๋ฒˆ์—! ์‹œ์ฆŒ ํŒจ์Šค๋กœ ๋” ์ €๋ ดํ•˜๊ฒŒ ์ฆ๊ธฐ์„ธ์š”</p>
    </div>
</div>
```

### 3. ํŒจ์Šค ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ ์ถ”๊ฐ€
`app/Http/Controllers/CheckinController.php`์˜ `store()` ๋ฉ”์„œ๋“œ์— ์œ ํšจ๊ธฐ๊ฐ„ ์ฒดํฌ ์ถ”๊ฐ€:
```php
// ํŒจ์Šค ์œ ํšจ๊ธฐ๊ฐ„ ํ™•์ธ
$seasonPass = $purchase->seasonPass;
if (now()->lt($seasonPass->valid_from) || now()->gt($seasonPass->valid_until)) {
    return back()->withErrors(['error' => '์ด ํŒจ์Šค๋Š” ํ˜„์žฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.']);
}
```

### 4. ๊ณต์—ฐ ๋‚ ์งœ ํ•„ํ„ฐ๋ง (์ง€๋‚˜๊ฐ„ ๊ณต์—ฐ ํ‘œ์‹œ)
`app/Http/Controllers/PerformanceController.php`:
```php
public function index()
{
    $upcomingPerformances = Performance::where('performance_date', '>=', now())
        ->orderBy('performance_date', 'asc')
        ->get();
    
    $pastPerformances = Performance::where('performance_date', '<', now())
        ->orderBy('performance_date', 'desc')
        ->get();

    return Inertia::render('Performance/Index', [
        'upcomingPerformances' => $upcomingPerformances,
        'pastPerformances' => $pastPerformances,
    ]);
}
```

`Performance/Index.jsx`์—์„œ ๋‘ ์„น์…˜์œผ๋กœ ๋‚˜๋ˆ„์–ด ํ‘œ์‹œ.

### 5. README.md ์ž‘์„ฑ
ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— `README.md` ์ƒ์„ฑ:
```markdown
# ๐ŸŽช ์„œ์ปค์Šค ์‹œ์ฆŒ ํŒจ์Šค ํ”Œ๋žซํผ

Laravel 13.x + Inertia.js + React 19๋กœ ๊ตฌํ˜„ํ•œ ์‹œ์ฆŒ ํŒจ์Šค ํŒ๋งค ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.

## ๊ธฐ๋Šฅ
- ์‹œ์ฆŒ ํŒจ์Šค ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ํŽ˜์ด์ง€
- ํŒจ์Šค ๊ตฌ๋งค (์ฃผ๋ฌธ ์ ‘์ˆ˜)
- ๋‚ด ํŒจ์Šค ๋Œ€์‹œ๋ณด๋“œ (์‚ฌ์šฉ ํ˜„ํ™ฉ)
- ๊ณต์—ฐ ์ผ์ • ์กฐํšŒ ๋ฐ ์ฒดํฌ์ธ
- ๊ด€๋žŒ ํžˆ์Šคํ† ๋ฆฌ & ๋ณ„์ /ํ›„๊ธฐ

## ์„ค์น˜ ๋ฐฉ๋ฒ•
```bash
composer install
npm install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate --seed
npm run build
php artisan serve
```

## ๊ธฐ์ˆ  ์Šคํƒ
- Laravel 13.x
- Inertia.js
- React 19
- Tailwind CSS
- SQLite
```

### 6. ์ตœ์ข… ํ…Œ์ŠคํŠธ
- ํšŒ์›๊ฐ€์ž… โ†’ ์‹œ์ฆŒ ํŒจ์Šค ๊ตฌ๋งค โ†’ ๊ณต์—ฐ ์ฒดํฌ์ธ โ†’ ํ›„๊ธฐ ๋‚จ๊ธฐ๊ธฐ ์ „์ฒด ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ
- ํŒจ์Šค ๋‚จ์€ ํšŸ์ˆ˜๊ฐ€ 0์ด ๋˜๋ฉด ์ฒดํฌ์ธ ๋ถˆ๊ฐ€ ํ™•์ธ
- ์ค‘๋ณต ์ฒดํฌ์ธ ๋ฐฉ์ง€ ํ™•์ธ
- ํ”Œ๋ž˜์‹œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ํ™•์ธ

ํ”„๋กœ์ ํŠธ๊ฐ€ ์™„์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰

### ์ถ”๊ฐ€ ๊ฐœ์„  ์•„์ด๋””์–ด
- ํŒจ์Šค ์„ ๋ฌผํ•˜๊ธฐ (์ด๋ฉ”์ผ ์ „์†ก)
- QR ์ฝ”๋“œ ์ƒ์„ฑ/์Šค์บ” ์ฒดํฌ์ธ
- ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ (ํŒจ์Šค ํŒ๋งค ํ†ต๊ณ„)
- ๊ฒฐ์ œ ์—ฐ๋™ (Stripe, Toss Payments)
- ์ด๋ฉ”์ผ ์•Œ๋ฆผ (๊ตฌ๋งค ํ™•์ธ, ๊ณต์—ฐ ๋ฆฌ๋งˆ์ธ๋”)
- ํŒจ์Šค ํ™˜๋ถˆ ๊ธฐ๋Šฅ
- ๊ณต์—ฐ ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ”Œ๋ž˜์‹œ ๋ฉ”์‹œ์ง€, ํžˆ์–ด๋กœ ์„น์…˜, ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ, ๊ณต์—ฐ ๋‚ ์งœ ํ•„ํ„ฐ๋ง ๋“ฑ ๊ฐœ์„  ์‚ฌํ•ญ์ด ๋ชจ๋‘ ์ ์šฉ๋˜์–ด ์™„์„ฑ๋„ ๋†’์€ ํ”Œ๋žซํผ์ด ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค. README.md๊ฐ€ ์ž‘์„ฑ๋˜์–ด ํ”„๋กœ์ ํŠธ ์„ค์น˜ ๋ฐ ์‹คํ–‰ ๋ฐฉ๋ฒ•์ด ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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