๐ŸŽค ๋ผ์ด๋ธŒ ํด๋Ÿฝ ๊ณต์—ฐ ๋ผ์ธ์—… & ํ‹ฐ์ผ“ ํŒ๋งค ํŽ˜์ด์ง€ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ž…๋ฌธ PHP + JavaScript Laravel + React AI: Claude

๐ŸŽค ๋ผ์ด๋ธŒ ํด๋Ÿฝ ๊ณต์—ฐ ๋ผ์ธ์—… & ํ‹ฐ์ผ“ ํŒ๋งค ํŽ˜์ด์ง€

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… live-club-lineup-ticket-sales
์กฐํšŒ์ˆ˜ 75 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

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

๋ผ์ด๋ธŒ ํด๋Ÿฝ/์†Œ๊ทœ๋ชจ ๊ณต์—ฐ์žฅ์„ ์œ„ํ•œ ๊ณต์—ฐ ๋ผ์ธ์—… ์†Œ๊ฐœ ๋ฐ ํ‹ฐ์ผ“ ํŒ๋งค ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ์ž๋“ค์ด ์ด๋ฒˆ ์ฃผ/๋‹ค์Œ ์ฃผ ๊ณต์—ฐ ์Šค์ผ€์ค„์„ ํ™•์ธํ•˜๊ณ , ๊ด€์‹ฌ ์žˆ๋Š” ๊ณต์—ฐ์˜ ํ‹ฐ์ผ“์„ ๊ตฌ๋งค ์‹ ์ฒญํ•  ์ˆ˜ ์žˆ๋Š” ํ’€์Šคํƒ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.

โœจ ์ฃผ์š” ๊ธฐ๋Šฅ

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

  • ๐Ÿ“… ๊ณต์—ฐ ๋ผ์ธ์—… ๋ชฉ๋ก: ๋‚ ์งœ๋ณ„ ๊ณต์—ฐ ์ •๋ณด (์•„ํ‹ฐ์ŠคํŠธ๋ช…, ์žฅ๋ฅด, ๊ณต์—ฐ ์‹œ๊ฐ„, ํ‹ฐ์ผ“ ๊ฐ€๊ฒฉ)
  • ๐ŸŽซ ํ‹ฐ์ผ“ ๊ตฌ๋งค ์‹ ์ฒญ ํผ: ์ด๋ฆ„, ์ด๋ฉ”์ผ, ๋งค์ˆ˜ ์ž…๋ ฅํ•˜์—ฌ ์˜ˆ์•ฝ
  • ๐Ÿ” ์žฅ๋ฅด ํ•„ํ„ฐ๋ง: ๋ก/์žฌ์ฆˆ/ํž™ํ•ฉ/์ธ๋”” ๋“ฑ ์žฅ๋ฅด๋ณ„ ๊ณต์—ฐ ๊ฒ€์ƒ‰
  • ๐Ÿ’ณ ์˜ˆ์•ฝ ๋‚ด์—ญ ํ™•์ธ: ๋‚ด๊ฐ€ ์‹ ์ฒญํ•œ ํ‹ฐ์ผ“ ๋ชฉ๋ก ๋ณด๊ธฐ
  • ๐Ÿ“ฑ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ: ๋ชจ๋ฐ”์ผ ์šฐ์„  UI

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

  • โญ ๊ณต์—ฐ๋ณ„ ์ข‹์•„์š” ๊ธฐ๋Šฅ
  • ๐ŸŽจ ์•„ํ‹ฐ์ŠคํŠธ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
  • ๐Ÿ”” ๋งค์ง„ ์ž„๋ฐ• ๋ฐฐ์ง€ ํ‘œ์‹œ (์ž”์—ฌ์„ 5์žฅ ์ดํ•˜)
  • ๐Ÿ“Š ์ธ๊ธฐ ๊ณต์—ฐ TOP 3 ๋ฐฐ๋„ˆ

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • Laravel 13.x RESTful API ์„ค๊ณ„ (CRUD)
  • React 19 ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ UI ๊ตฌ์„ฑ
  • fetch/axios๋ฅผ ํ™œ์šฉํ•œ API ํ†ต์‹ 
  • SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (ํ”„๋ก ํŠธ+๋ฐฑ์—”๋“œ)
  • Tailwind CSS๋กœ ๋น ๋ฅธ ์Šคํƒ€์ผ๋ง

๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ

  • Backend: Laravel 13.x (API ์„œ๋ฒ„)
  • Frontend: React 19 + Vite
  • Database: SQLite
  • Styling: Tailwind CSS
  • HTTP Client: Axios

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ (composer create-project laravel/laravel)
  • โ€ข React 19 ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ (npm create vite@latest)
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๋ณ„๋„ ์„ค์น˜ ๋ถˆํ•„์š”)
  • โ€ข Laravel API๋Š” /api ๊ฒฝ๋กœ๋กœ RESTful ์„ค๊ณ„
  • โ€ข React ํ”„๋ก ํŠธ์—”๋“œ๋Š” ๋ณ„๋„ Vite ํ”„๋กœ์ ํŠธ๋กœ ๊ตฌ์„ฑ
  • โ€ข CORS ์„ค์ • ํ•„์ˆ˜ (Laravel์—์„œ ํ”„๋ก ํŠธ์—”๋“œ ์š”์ฒญ ํ—ˆ์šฉ)
  • โ€ข Tailwind CSS ์‚ฌ์šฉ ๊ถŒ์žฅ
  • โ€ข ์‹ค์‹œ๊ฐ„ ๊ธฐ๋Šฅ(WebSocket) ์‚ฌ์šฉ ๊ธˆ์ง€
  • โ€ข ๋ณต์žกํ•œ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Redux, Zustand ๋“ฑ) ์‚ฌ์šฉ ๊ธˆ์ง€ - useState๋งŒ ์‚ฌ์šฉ
  • โ€ข ํŒŒ์ผ ์—…๋กœ๋“œ๋Š” ์„ ํƒ ๊ธฐ๋Šฅ (๋ณด๋„ˆ์Šค)

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

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

1
Step 1
mkdir live-club-lineup-ticket-sales && cd live-club-lineup-ticket-sales

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

## Laravel ๋ฐฑ์—”๋“œ API ์„œ๋ฒ„ ์ƒ์„ฑ

1. `backend` ํด๋”์— Laravel 13.x ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”:
```bash
composer create-project laravel/laravel backend
cd backend
```

2. `.env` ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์—ฌ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •:
```env
DB_CONNECTION=sqlite
# DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD ์ค„์€ ์ฃผ์„ ์ฒ˜๋ฆฌ
```

3. `database/database.sqlite` ๋นˆ ํŒŒ์ผ ์ƒ์„ฑ:
```bash
touch database/database.sqlite
```

4. CORS ์„ค์ • - `config/cors.php` ํŒŒ์ผ์—์„œ ํ”„๋ก ํŠธ์—”๋“œ ์š”์ฒญ์„ ํ—ˆ์šฉํ•˜๋„๋ก ์ˆ˜์ •:
```php
'paths' => ['api/*'],
'allowed_origins' => ['http://localhost:5173'], // Vite ๊ธฐ๋ณธ ํฌํŠธ
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
'supports_credentials' => true,
```

5. `app/Http/Middleware/HandleCors.php`๊ฐ€ ์—†๋‹ค๋ฉด `php artisan make:middleware HandleCors` ์‹คํ–‰ ํ›„ `bootstrap/app.php` ๋˜๋Š” `app/Http/Kernel.php`์— ๋ฏธ๋“ค์›จ์–ด ๋“ฑ๋ก

6. ์„œ๋ฒ„ ์‹คํ–‰ ํ™•์ธ:
```bash
php artisan serve
```
http://localhost:8000 ์ ‘์†ํ•˜์—ฌ Laravel ์ดˆ๊ธฐ ํ™”๋ฉด์ด ๋ณด์ด๋ฉด ์„ฑ๊ณต!

## React ํ”„๋ก ํŠธ์—”๋“œ ์ƒ์„ฑ

1. ์ƒ์œ„ ํด๋”๋กœ ์ด๋™ ํ›„ `frontend` ํด๋”์— React ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ:
```bash
cd ..
npm create vite@latest frontend -- --template react
cd frontend
npm install
```

2. Axios์™€ Tailwind CSS ์„ค์น˜:
```bash
npm install axios
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

3. `tailwind.config.js` ์ˆ˜์ •:
```js
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

4. `src/index.css`์— Tailwind ์ง€์‹œ๋ฌธ ์ถ”๊ฐ€:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

5. ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋ฒ„ ์‹คํ–‰ ํ™•์ธ:
```bash
npm run dev
```
http://localhost:5173 ์ ‘์†ํ•˜์—ฌ Vite+React ์ดˆ๊ธฐ ํ™”๋ฉด์ด ๋ณด์ด๋ฉด ์„ฑ๊ณต!

## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

```
live-club-lineup-ticket-sales/
โ”œโ”€โ”€ backend/          (Laravel API)
โ”‚   โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ database/
โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ””โ”€โ”€ ...
โ””โ”€โ”€ frontend/         (React UI)
    โ”œโ”€โ”€ src/
    โ”œโ”€โ”€ public/
    โ””โ”€โ”€ ...
```

์ด์ œ ๋ฐฑ์—”๋“œ๋Š” http://localhost:8000, ํ”„๋ก ํŠธ์—”๋“œ๋Š” http://localhost:5173์—์„œ ๊ฐ๊ฐ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel ๋ฐฑ์—”๋“œ(API ์„œ๋ฒ„)์™€ React ํ”„๋ก ํŠธ์—”๋“œ(Vite) ํ”„๋กœ์ ํŠธ๊ฐ€ ๋ณ„๋„ ํด๋”์— ์ƒ์„ฑ๋˜๊ณ , ๊ฐ๊ฐ ์„œ๋ฒ„๊ฐ€ ์ •์ƒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. CORS ์„ค์ •์ด ์™„๋ฃŒ๋˜์–ด ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐฑ์—”๋“œ API๋ฅผ ํ˜ธ์ถœํ•  ์ค€๋น„๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

2
Step 2
## Laravel ๋ฐฑ์—”๋“œ: ๊ณต์—ฐ(Show) ๋ชจ๋ธ & ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ

`backend` ํด๋”์—์„œ ์ž‘์—…ํ•ฉ๋‹ˆ๋‹ค.

1. Show ๋ชจ๋ธ๊ณผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ:
```bash
php artisan make:model Show -m
```

2. `database/migrations/YYYY_MM_DD_create_shows_table.php` ํŒŒ์ผ ์ˆ˜์ •:
```php
public function up()
{
    Schema::create('shows', function (Blueprint $table) {
        $table->id();
        $table->string('artist_name');        // ์•„ํ‹ฐ์ŠคํŠธ๋ช…
        $table->string('genre');              // ์žฅ๋ฅด (Rock, Jazz, Hip-hop, Indie ๋“ฑ)
        $table->date('show_date');            // ๊ณต์—ฐ ๋‚ ์งœ
        $table->time('show_time');            // ๊ณต์—ฐ ์‹œ๊ฐ„
        $table->decimal('ticket_price', 8, 2); // ํ‹ฐ์ผ“ ๊ฐ€๊ฒฉ
        $table->integer('total_seats')->default(100); // ์ด ์ขŒ์„ ์ˆ˜
        $table->integer('available_seats')->default(100); // ์ž”์—ฌ ์ขŒ์„
        $table->text('description')->nullable(); // ๊ณต์—ฐ ์„ค๋ช…
        $table->timestamps();
    });
}
```

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

4. ์‹œ๋“œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - `database/seeders/DatabaseSeeder.php` ์ˆ˜์ •:
```php
public function run()
{
    \App\Models\Show::create([
        'artist_name' => 'The Midnight Blues',
        'genre' => 'Blues',
        'show_date' => '2025-02-15',
        'show_time' => '20:00',
        'ticket_price' => 25000,
        'total_seats' => 100,
        'available_seats' => 100,
        'description' => '๋ธ”๋ฃจ์Šค์˜ ์ •์ˆ˜๋ฅผ ๋А๋‚„ ์ˆ˜ ์žˆ๋Š” ๋ผ์ด๋ธŒ ๊ณต์—ฐ',
    ]);

    \App\Models\Show::create([
        'artist_name' => 'Indie Rockers',
        'genre' => 'Indie',
        'show_date' => '2025-02-16',
        'show_time' => '19:30',
        'ticket_price' => 20000,
        'total_seats' => 80,
        'available_seats' => 80,
        'description' => '์‹ ์„ ํ•œ ์ธ๋”” ์‚ฌ์šด๋“œ๋กœ ๊ฐ€๋“ํ•œ ๋ฐค',
    ]);

    \App\Models\Show::create([
        'artist_name' => 'Jazz Fusion Trio',
        'genre' => 'Jazz',
        'show_date' => '2025-02-17',
        'show_time' => '21:00',
        'ticket_price' => 30000,
        'total_seats' => 60,
        'available_seats' => 5, // ๋งค์ง„ ์ž„๋ฐ• ์˜ˆ์‹œ
        'description' => '์žฌ์ฆˆ์˜ ์ƒˆ๋กœ์šด ํ•ด์„, ํ“จ์ „์˜ ๋งค๋ ฅ',
    ]);
}
```

5. ์‹œ๋“œ ์‹คํ–‰:
```bash
php artisan db:seed
```

## Booking ๋ชจ๋ธ & ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ (ํ‹ฐ์ผ“ ์˜ˆ์•ฝ)

1. Booking ๋ชจ๋ธ๊ณผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ:
```bash
php artisan make:model Booking -m
```

2. `database/migrations/YYYY_MM_DD_create_bookings_table.php` ํŒŒ์ผ ์ˆ˜์ •:
```php
public function up()
{
    Schema::create('bookings', function (Blueprint $table) {
        $table->id();
        $table->foreignId('show_id')->constrained()->onDelete('cascade');
        $table->string('customer_name');
        $table->string('customer_email');
        $table->integer('ticket_count')->default(1);
        $table->timestamps();
    });
}
```

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

Show ๋ชจ๋ธ๊ณผ Booking ๋ชจ๋ธ์ด ์ƒ์„ฑ๋˜์—ˆ๊ณ , ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ์‹œ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel ๋ฐฑ์—”๋“œ์— shows ํ…Œ์ด๋ธ”(๊ณต์—ฐ ์ •๋ณด)๊ณผ bookings ํ…Œ์ด๋ธ”(ํ‹ฐ์ผ“ ์˜ˆ์•ฝ)์ด ์ƒ์„ฑ๋˜๊ณ , 3๊ฐœ์˜ ์ƒ˜ํ”Œ ๊ณต์—ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์‹œ๋“œ๋ฉ๋‹ˆ๋‹ค. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๊ฐ€ ์ค€๋น„๋ฉ๋‹ˆ๋‹ค.

3
Step 3
## Laravel ๋ฐฑ์—”๋“œ: API ๋ผ์šฐํŠธ & ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ

`backend` ํด๋”์—์„œ ์ž‘์—…ํ•ฉ๋‹ˆ๋‹ค.

1. ShowController ์ƒ์„ฑ:
```bash
php artisan make:controller Api/ShowController --api
```

2. `app/Http/Controllers/Api/ShowController.php` ๊ตฌํ˜„:
```php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Show;
use Illuminate\Http\Request;

class ShowController extends Controller
{
    // ๋ชจ๋“  ๊ณต์—ฐ ๋ชฉ๋ก ์กฐํšŒ (์žฅ๋ฅด ํ•„ํ„ฐ ์ง€์›)
    public function index(Request $request)
    {
        $query = Show::query();
        
        if ($request->has('genre') && $request->genre !== 'all') {
            $query->where('genre', $request->genre);
        }
        
        $shows = $query->orderBy('show_date', 'asc')->get();
        
        return response()->json($shows);
    }

    // ํŠน์ • ๊ณต์—ฐ ์ƒ์„ธ ์กฐํšŒ
    public function show($id)
    {
        $show = Show::findOrFail($id);
        return response()->json($show);
    }
}
```

3. BookingController ์ƒ์„ฑ:
```bash
php artisan make:controller Api/BookingController --api
```

4. `app/Http/Controllers/Api/BookingController.php` ๊ตฌํ˜„:
```php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Booking;
use App\Models\Show;
use Illuminate\Http\Request;

class BookingController extends Controller
{
    // ํ‹ฐ์ผ“ ์˜ˆ์•ฝ ์ƒ์„ฑ
    public function store(Request $request)
    {
        $validated = $request->validate([
            'show_id' => 'required|exists:shows,id',
            'customer_name' => 'required|string|max:100',
            'customer_email' => 'required|email',
            'ticket_count' => 'required|integer|min:1|max:10',
        ]);

        $show = Show::findOrFail($validated['show_id']);

        // ์ž”์—ฌ ์ขŒ์„ ํ™•์ธ
        if ($show->available_seats < $validated['ticket_count']) {
            return response()->json([
                'message' => '์ž”์—ฌ ์ขŒ์„์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.',
                'available_seats' => $show->available_seats,
            ], 400);
        }

        // ์˜ˆ์•ฝ ์ƒ์„ฑ
        $booking = Booking::create($validated);

        // ์ž”์—ฌ ์ขŒ์„ ์ฐจ๊ฐ
        $show->decrement('available_seats', $validated['ticket_count']);

        return response()->json([
            'message' => 'ํ‹ฐ์ผ“ ์˜ˆ์•ฝ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!',
            'booking' => $booking,
        ], 201);
    }

    // ๋‚ด ์˜ˆ์•ฝ ๋ชฉ๋ก ์กฐํšŒ (์ด๋ฉ”์ผ ๊ธฐ๋ฐ˜)
    public function index(Request $request)
    {
        $email = $request->query('email');
        
        if (!$email) {
            return response()->json(['message' => '์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•˜์„ธ์š”.'], 400);
        }

        $bookings = Booking::with('show')
            ->where('customer_email', $email)
            ->orderBy('created_at', 'desc')
            ->get();

        return response()->json($bookings);
    }
}
```

5. Booking ๋ชจ๋ธ์— ๊ด€๊ณ„ ์„ค์ • - `app/Models/Booking.php`:
```php
protected $fillable = ['show_id', 'customer_name', 'customer_email', 'ticket_count'];

public function show()
{
    return $this->belongsTo(Show::class);
}
```

6. API ๋ผ์šฐํŠธ ๋“ฑ๋ก - `routes/api.php`:
```php
use App\Http\Controllers\Api\ShowController;
use App\Http\Controllers\Api\BookingController;

Route::get('/shows', [ShowController::class, 'index']);
Route::get('/shows/{id}', [ShowController::class, 'show']);
Route::post('/bookings', [BookingController::class, 'store']);
Route::get('/bookings', [BookingController::class, 'index']);
```

7. API ํ…Œ์ŠคํŠธ (Postman ๋˜๋Š” curl):
```bash
# ๊ณต์—ฐ ๋ชฉ๋ก ์กฐํšŒ
curl http://localhost:8000/api/shows

# ์žฅ๋ฅด ํ•„ํ„ฐ๋ง
curl http://localhost:8000/api/shows?genre=Jazz

# ํ‹ฐ์ผ“ ์˜ˆ์•ฝ
curl -X POST http://localhost:8000/api/bookings \
  -H "Content-Type: application/json" \
  -d '{"show_id": 1, "customer_name": "ํ™๊ธธ๋™", "customer_email": "hong@example.com", "ticket_count": 2}'
```

Laravel API๊ฐ€ ์™„์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel ๋ฐฑ์—”๋“œ์— RESTful API๊ฐ€ ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. GET /api/shows (๊ณต์—ฐ ๋ชฉ๋ก, ์žฅ๋ฅด ํ•„ํ„ฐ), POST /api/bookings (ํ‹ฐ์ผ“ ์˜ˆ์•ฝ), GET /api/bookings (๋‚ด ์˜ˆ์•ฝ ์กฐํšŒ) ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ž‘๋™ํ•˜๋ฉฐ, ์ž”์—ฌ ์ขŒ์„ ์ฐจ๊ฐ ๋กœ์ง์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

4
Step 4
## React ํ”„๋ก ํŠธ์—”๋“œ: API ์—ฐ๋™ & ๊ณต์—ฐ ๋ชฉ๋ก UI ๊ตฌํ˜„

`frontend` ํด๋”์—์„œ ์ž‘์—…ํ•ฉ๋‹ˆ๋‹ค.

1. Axios ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ - `src/api/axios.js`:
```js
import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:8000/api',
  headers: {
    'Content-Type': 'application/json',
  },
});

export default api;
```

2. ๊ณต์—ฐ ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ - `src/components/ShowList.jsx`:
```jsx
import { useState, useEffect } from 'react';
import api from '../api/axios';

function ShowList({ onSelectShow }) {
  const [shows, setShows] = useState([]);
  const [selectedGenre, setSelectedGenre] = useState('all');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchShows();
  }, [selectedGenre]);

  const fetchShows = async () => {
    setLoading(true);
    try {
      const params = selectedGenre !== 'all' ? { genre: selectedGenre } : {};
      const response = await api.get('/shows', { params });
      setShows(response.data);
    } catch (error) {
      console.error('๊ณต์—ฐ ๋ชฉ๋ก ๋กœ๋”ฉ ์‹คํŒจ:', error);
    } finally {
      setLoading(false);
    }
  };

  const genres = ['all', 'Rock', 'Jazz', 'Hip-hop', 'Indie', 'Blues'];

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-4xl font-bold text-center mb-8 text-purple-600">
        ๐ŸŽค ์ด๋ฒˆ ์ฃผ ๋ผ์ด๋ธŒ ๊ณต์—ฐ ๋ผ์ธ์—…
      </h1>

      {/* ์žฅ๋ฅด ํ•„ํ„ฐ */}
      <div className="flex gap-2 mb-6 flex-wrap justify-center">
        {genres.map((genre) => (
          <button
            key={genre}
            onClick={() => setSelectedGenre(genre)}
            className={`px-4 py-2 rounded-full font-medium transition ${
              selectedGenre === genre
                ? 'bg-purple-600 text-white'
                : 'bg-gray-200 hover:bg-gray-300'
            }`}
          >
            {genre === 'all' ? '์ „์ฒด' : genre}
          </button>
        ))}
      </div>

      {/* ๊ณต์—ฐ ๋ชฉ๋ก */}
      {loading ? (
        <p className="text-center text-gray-500">๋กœ๋”ฉ ์ค‘...</p>
      ) : shows.length === 0 ? (
        <p className="text-center text-gray-500">๊ณต์—ฐ์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
      ) : (
        <div className="grid gap-6 md:grid-cols-2">
          {shows.map((show) => (
            <div
              key={show.id}
              className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition cursor-pointer"
              onClick={() => onSelectShow(show)}
            >
              <div className="flex justify-between items-start mb-3">
                <h2 className="text-2xl font-bold text-gray-800">{show.artist_name}</h2>
                <span className="bg-purple-100 text-purple-700 px-3 py-1 rounded-full text-sm font-medium">
                  {show.genre}
                </span>
              </div>
              <p className="text-gray-600 mb-2">{show.description}</p>
              <div className="flex justify-between items-center mt-4 pt-4 border-t">
                <div>
                  <p className="text-sm text-gray-500">๐Ÿ“… {show.show_date}</p>
                  <p className="text-sm text-gray-500">๐Ÿ• {show.show_time}</p>
                </div>
                <div className="text-right">
                  <p className="text-2xl font-bold text-purple-600">
                    โ‚ฉ{parseInt(show.ticket_price).toLocaleString()}
                  </p>
                  <p className="text-sm text-gray-500">
                    ์ž”์—ฌ {show.available_seats}์„
                    {show.available_seats <= 5 && (
                      <span className="ml-2 text-red-500 font-bold">โš ๏ธ ๋งค์ง„์ž„๋ฐ•</span>
                    )}
                  </p>
                </div>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default ShowList;
```

3. `src/App.jsx` ์ˆ˜์ •:
```jsx
import { useState } from 'react';
import ShowList from './components/ShowList';

function App() {
  const [selectedShow, setSelectedShow] = useState(null);

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50">
      <ShowList onSelectShow={setSelectedShow} />
      {selectedShow && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
          <div className="bg-white rounded-lg p-6 max-w-md w-full">
            <h3 className="text-xl font-bold mb-2">{selectedShow.artist_name}</h3>
            <p className="text-gray-600">ํ‹ฐ์ผ“ ์˜ˆ์•ฝ ํผ์€ ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค!</p>
            <button
              onClick={() => setSelectedShow(null)}
              className="mt-4 w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700"
            >
              ๋‹ซ๊ธฐ
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

export default App;
```

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๊ณต์—ฐ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œํ•˜๊ณ , ์žฅ๋ฅด ํ•„ํ„ฐ๋ง์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค!

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: React ํ”„๋ก ํŠธ์—”๋“œ์—์„œ Laravel API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ณต์—ฐ ๋ชฉ๋ก์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์žฅ๋ฅด ํ•„ํ„ฐ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์žฅ๋ฅด์˜ ๊ณต์—ฐ๋งŒ ๋ณด์ด๊ณ , ์ž”์—ฌ์„ 5์„ ์ดํ•˜์ธ ๊ณต์—ฐ์—๋Š” '๋งค์ง„์ž„๋ฐ•' ๋ฐฐ์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ณต์—ฐ ์นด๋“œ๋ฅผ ํด๋ฆญํ•˜๋ฉด ๋ชจ๋‹ฌ์ด ์—ด๋ฆฝ๋‹ˆ๋‹ค.

5
Step 5
## React ํ”„๋ก ํŠธ์—”๋“œ: ํ‹ฐ์ผ“ ์˜ˆ์•ฝ ํผ & ๋‚ด ์˜ˆ์•ฝ ์กฐํšŒ UI ๊ตฌํ˜„

`frontend/src` ํด๋”์—์„œ ์ž‘์—…ํ•ฉ๋‹ˆ๋‹ค.

1. ํ‹ฐ์ผ“ ์˜ˆ์•ฝ ํผ ์ปดํฌ๋„ŒํŠธ - `src/components/BookingForm.jsx`:
```jsx
import { useState } from 'react';
import api from '../api/axios';

function BookingForm({ show, onClose, onSuccess }) {
  const [formData, setFormData] = useState({
    customer_name: '',
    customer_email: '',
    ticket_count: 1,
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const response = await api.post('/bookings', {
        show_id: show.id,
        ...formData,
      });
      alert(response.data.message);
      onSuccess();
      onClose();
    } catch (err) {
      setError(err.response?.data?.message || '์˜ˆ์•ฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
    } finally {
      setLoading(false);
    }
  };

  const totalPrice = show.ticket_price * formData.ticket_count;

  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
      <div className="bg-white rounded-lg p-6 max-w-md w-full">
        <h3 className="text-2xl font-bold mb-4 text-purple-600">๐ŸŽซ ํ‹ฐ์ผ“ ์˜ˆ์•ฝ</h3>
        <div className="mb-4 p-4 bg-purple-50 rounded-lg">
          <p className="font-bold text-lg">{show.artist_name}</p>
          <p className="text-sm text-gray-600">{show.show_date} {show.show_time}</p>
          <p className="text-sm text-gray-600">์ž”์—ฌ์„: {show.available_seats}์„</p>
        </div>

        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              ์ด๋ฆ„ *
            </label>
            <input
              type="text"
              required
              value={formData.customer_name}
              onChange={(e) => setFormData({ ...formData, customer_name: e.target.value })}
              className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
              placeholder="ํ™๊ธธ๋™"
            />
          </div>

          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              ์ด๋ฉ”์ผ *
            </label>
            <input
              type="email"
              required
              value={formData.customer_email}
              onChange={(e) => setFormData({ ...formData, customer_email: e.target.value })}
              className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
              placeholder="hong@example.com"
            />
          </div>

          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              ํ‹ฐ์ผ“ ๋งค์ˆ˜ *
            </label>
            <input
              type="number"
              required
              min="1"
              max={show.available_seats}
              value={formData.ticket_count}
              onChange={(e) => setFormData({ ...formData, ticket_count: parseInt(e.target.value) })}
              className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
            />
          </div>

          <div className="p-4 bg-gray-50 rounded-lg">
            <div className="flex justify-between items-center">
              <span className="text-gray-600">์ด ๊ฒฐ์ œ ๊ธˆ์•ก</span>
              <span className="text-2xl font-bold text-purple-600">
                โ‚ฉ{totalPrice.toLocaleString()}
              </span>
            </div>
          </div>

          {error && (
            <div className="p-3 bg-red-50 text-red-600 rounded-lg text-sm">
              {error}
            </div>
          )}

          <div className="flex gap-3">
            <button
              type="button"
              onClick={onClose}
              className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
              disabled={loading}
            >
              ์ทจ์†Œ
            </button>
            <button
              type="submit"
              className="flex-1 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition disabled:bg-gray-300"
              disabled={loading}
            >
              {loading ? '์˜ˆ์•ฝ ์ค‘...' : '์˜ˆ์•ฝํ•˜๊ธฐ'}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

export default BookingForm;
```

2. ๋‚ด ์˜ˆ์•ฝ ์กฐํšŒ ์ปดํฌ๋„ŒํŠธ - `src/components/MyBookings.jsx`:
```jsx
import { useState } from 'react';
import api from '../api/axios';

function MyBookings() {
  const [email, setEmail] = useState('');
  const [bookings, setBookings] = useState([]);
  const [loading, setLoading] = useState(false);
  const [searched, setSearched] = useState(false);

  const handleSearch = async (e) => {
    e.preventDefault();
    setLoading(true);
    setSearched(true);

    try {
      const response = await api.get('/bookings', { params: { email } });
      setBookings(response.data);
    } catch (error) {
      console.error('์˜ˆ์•ฝ ์กฐํšŒ ์‹คํŒจ:', error);
      setBookings([]);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="max-w-2xl mx-auto p-6 mt-8">
      <h2 className="text-3xl font-bold text-center mb-6 text-purple-600">
        ๐Ÿ“‹ ๋‚ด ์˜ˆ์•ฝ ์กฐํšŒ
      </h2>

      <form onSubmit={handleSearch} className="flex gap-3 mb-6">
        <input
          type="email"
          required
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="์˜ˆ์•ฝ ์‹œ ์ž…๋ ฅํ•œ ์ด๋ฉ”์ผ"
          className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
        />
        <button
          type="submit"
          className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
          disabled={loading}
        >
          {loading ? '์กฐํšŒ ์ค‘...' : '์กฐํšŒ'}
        </button>
      </form>

      {searched && (
        <div>
          {bookings.length === 0 ? (
            <p className="text-center text-gray-500">์˜ˆ์•ฝ ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
          ) : (
            <div className="space-y-4">
              {bookings.map((booking) => (
                <div key={booking.id} className="bg-white rounded-lg shadow-md p-6">
                  <div className="flex justify-between items-start mb-3">
                    <div>
                      <h3 className="text-xl font-bold">{booking.show.artist_name}</h3>
                      <p className="text-gray-600">{booking.show.genre}</p>
                    </div>
                    <span className="bg-green-100 text-green-700 px-3 py-1 rounded-full text-sm font-medium">
                      ์˜ˆ์•ฝ์™„๋ฃŒ
                    </span>
                  </div>
                  <div className="border-t pt-3 space-y-2">
                    <p className="text-sm text-gray-600">๐Ÿ“… {booking.show.show_date} {booking.show.show_time}</p>
                    <p className="text-sm text-gray-600">๐Ÿ‘ค {booking.customer_name}</p>
                    <p className="text-sm text-gray-600">๐ŸŽซ {booking.ticket_count}๋งค</p>
                    <p className="text-sm text-gray-600">
                      ๐Ÿ’ฐ โ‚ฉ{(booking.show.ticket_price * booking.ticket_count).toLocaleString()}
                    </p>
                  </div>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default MyBookings;
```

3. `src/App.jsx` ์ตœ์ข… ํ†ตํ•ฉ:
```jsx
import { useState } from 'react';
import ShowList from './components/ShowList';
import BookingForm from './components/BookingForm';
import MyBookings from './components/MyBookings';

function App() {
  const [selectedShow, setSelectedShow] = useState(null);
  const [refreshKey, setRefreshKey] = useState(0);

  const handleBookingSuccess = () => {
    setRefreshKey((prev) => prev + 1); // ๊ณต์—ฐ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50">
      <ShowList key={refreshKey} onSelectShow={setSelectedShow} />
      <MyBookings />
      {selectedShow && (
        <BookingForm
          show={selectedShow}
          onClose={() => setSelectedShow(null)}
          onSuccess={handleBookingSuccess}
        />
      )}
    </div>
  );
}

export default App;
```

์™„์„ฑ! ๊ณต์—ฐ ์„ ํƒ โ†’ ํ‹ฐ์ผ“ ์˜ˆ์•ฝ โ†’ ๋‚ด ์˜ˆ์•ฝ ์กฐํšŒ๊นŒ์ง€ ์ „์ฒด ํ”Œ๋กœ์šฐ๊ฐ€ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: React ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ํ‹ฐ์ผ“ ์˜ˆ์•ฝ ํผ๊ณผ ๋‚ด ์˜ˆ์•ฝ ์กฐํšŒ UI๊ฐ€ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค. ๊ณต์—ฐ ์นด๋“œ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์˜ˆ์•ฝ ๋ชจ๋‹ฌ์ด ์—ด๋ฆฌ๊ณ , ์ด๋ฆ„/์ด๋ฉ”์ผ/๋งค์ˆ˜๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์˜ˆ์•ฝํ•˜๋ฉด Laravel API๋กœ POST ์š”์ฒญ์ด ์ „์†ก๋ฉ๋‹ˆ๋‹ค. ์˜ˆ์•ฝ ํ›„ ์ž”์—ฌ์„์ด ๊ฐ์†Œํ•˜๊ณ , ์ด๋ฉ”์ผ๋กœ ๋‚ด ์˜ˆ์•ฝ ๋‚ด์—ญ์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ „์ฒด ํ’€์Šคํƒ ํ”Œ๋กœ์šฐ๊ฐ€ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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