๐ŸŽธ ๋ฎค์ง ํŽ˜์Šคํ‹ฐ๋ฒŒ ๋ผ์ธ์—… & ํƒ€์ž„ํ…Œ์ด๋ธ” ํ”Œ๋žซํผ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ค‘๊ธ‰ PHP + JavaScript Laravel + React + Inertia.js AI: Claude

๐ŸŽธ ๋ฎค์ง ํŽ˜์Šคํ‹ฐ๋ฒŒ ๋ผ์ธ์—… & ํƒ€์ž„ํ…Œ์ด๋ธ” ํ”Œ๋žซํผ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… music-festival-lineup-timetable
์กฐํšŒ์ˆ˜ 60 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐ŸŽธ ๋ฎค์ง ํŽ˜์Šคํ‹ฐ๋ฒŒ ๋ผ์ธ์—… & ํƒ€์ž„ํ…Œ์ด๋ธ” ํ”Œ๋žซํผ

์Œ์•… ํŽ˜์Šคํ‹ฐ๋ฒŒ ์ฃผ์ตœ์ž๋ฅผ ์œ„ํ•œ ๊ณต์—ฐ ๋ผ์ธ์—… ๊ด€๋ฆฌ ๋ฐ ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ณต๊ฐœ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. ํŽ˜์Šคํ‹ฐ๋ฒŒ ๊ด€๋ฆฌ์ž๊ฐ€ ์ถœ์—ฐ ์•„ํ‹ฐ์ŠคํŠธ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ๋ฌด๋Œ€๋ณ„/์‹œ๊ฐ„๋Œ€๋ณ„ ๊ณต์—ฐ ์ผ์ •์„ ๊ตฌ์„ฑํ•˜๋ฉด, ๊ด€๋žŒ๊ฐ์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” ํƒ€์ž„ํ…Œ์ด๋ธ”์„ ํ™•์ธํ•˜๊ณ  ๊ด€์‹ฌ ๊ณต์—ฐ์„ ์ฆ๊ฒจ์ฐพ๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

1. ์•„ํ‹ฐ์ŠคํŠธ ๊ด€๋ฆฌ (CRUD)

  • ์•„ํ‹ฐ์ŠคํŠธ๋ช…, ์žฅ๋ฅด, ํ”„๋กœํ•„ ์ด๋ฏธ์ง€, ์†Œ๊ฐœ๊ธ€ ๋“ฑ๋ก
  • ์•„ํ‹ฐ์ŠคํŠธ ๋ชฉ๋ก ํŽ˜์ด์ง€ (์žฅ๋ฅด๋ณ„ ํ•„ํ„ฐ๋ง)
  • ์•„ํ‹ฐ์ŠคํŠธ ์ƒ์„ธ ํŽ˜์ด์ง€ (๋””์Šค์ฝ”๊ทธ๋ž˜ํ”ผ, SNS ๋งํฌ)

2. ๋ฌด๋Œ€(Stage) ๊ด€๋ฆฌ

  • ๋ฉ”์ธ ์Šคํ…Œ์ด์ง€, ์„œ๋ธŒ ์Šคํ…Œ์ด์ง€ ๋“ฑ ์—ฌ๋Ÿฌ ๋ฌด๋Œ€ ๋“ฑ๋ก
  • ๋ฌด๋Œ€๋ณ„ ์„ค๋ช… ๋ฐ ์œ„์น˜ ์ •๋ณด

3. ๊ณต์—ฐ ์ผ์ •(Performance Schedule) ๊ด€๋ฆฌ

  • ๋‚ ์งœ, ์‹œ๊ฐ„, ๋ฌด๋Œ€, ์•„ํ‹ฐ์ŠคํŠธ ์กฐํ•ฉ์œผ๋กœ ๊ณต์—ฐ ์ผ์ • ๋“ฑ๋ก
  • ์ค‘๋ณต ์‹œ๊ฐ„๋Œ€ ๊ฒ€์ฆ (๊ฐ™์€ ๋ฌด๋Œ€์— ๋™์‹œ ๊ณต์—ฐ ๋ฐฉ์ง€)
  • ๊ณต์—ฐ ์‹œ๊ฐ„ ์ž๋™ ๊ณ„์‚ฐ (์‹œ์ž‘ ~ ์ข…๋ฃŒ ์‹œ๊ฐ„)

4. ํƒ€์ž„ํ…Œ์ด๋ธ” ๋ทฐ (๊ด€๋žŒ๊ฐ์šฉ)

  • ๋‚ ์งœ๋ณ„, ๋ฌด๋Œ€๋ณ„ ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ทธ๋ฆฌ๋“œ ํ˜•์‹ ํ‘œ์‹œ
  • ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ณต์—ฐ ์นด๋“œ (์•„ํ‹ฐ์ŠคํŠธ ์ด๋ฏธ์ง€, ์žฅ๋ฅด ๋ฑƒ์ง€)
  • ๊ด€์‹ฌ ๊ณต์—ฐ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋Šฅ (๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ํ™œ์šฉ)
  • ํ•„ํ„ฐ๋ง: ์žฅ๋ฅด๋ณ„, ๋ฌด๋Œ€๋ณ„, ๋‚ ์งœ๋ณ„

5. ๋ฐ˜์‘ํ˜• ๋””์ž์ธ

  • ๋ชจ๋ฐ”์ผ์—์„œ ์Šค์™€์ดํ”„๋กœ ํƒ€์ž„ํ…Œ์ด๋ธ” ํƒ์ƒ‰
  • ๋ฐ์Šคํฌํ†ฑ์—์„œ ์ „์ฒด ์Šค์ผ€์ค„ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

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

  • ๊ณต์—ฐ ์‹œ์ž‘ 30๋ถ„ ์ „ ์•Œ๋ฆผ (๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ API)
  • ์•„ํ‹ฐ์ŠคํŠธ๋ณ„ ๊ณต์—ฐ ์‹œ๊ฐ„ ์ž๋™ ํ•˜์ด๋ผ์ดํŠธ
  • ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ๊ณต์—ฐ๋งŒ ๋ชจ์•„๋ณด๋Š” '๋‚˜๋งŒ์˜ ํƒ€์ž„ํ…Œ์ด๋ธ”' ํŽ˜์ด์ง€
  • ๊ณต์—ฐ ์ข…๋ฃŒ ํ›„ ์…‹๋ฆฌ์ŠคํŠธ ์—…๋ฐ์ดํŠธ ๊ธฐ๋Šฅ
  • QR ์ฝ”๋“œ ์ƒ์„ฑ (ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ณต์œ ์šฉ)

๊ธฐ์ˆ  ์Šคํƒ

  • Backend: Laravel 13.x (Eloquent ORM, Validation, File Storage)
  • Frontend: React 19 (useState, useEffect, useContext)
  • Bridge: Inertia.js (SPA-like ๋ผ์šฐํŒ…)
  • Database: SQLite (Artist, Stage, Performance ๋ชจ๋ธ)
  • UI: Tailwind CSS (๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ, ์นด๋“œ ์ปดํฌ๋„ŒํŠธ)

์ด ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด Laravel์˜ ๊ด€๊ณ„ํ˜• DB ์„ค๊ณ„(hasMany, belongsTo), Inertia.js์˜ ํผ ์ฒ˜๋ฆฌ, React์˜ ๋ณต์žกํ•œ UI ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์‹ค์Šตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x + Inertia.js + React 19 ๋ชจ๋…ธ๋ฆฌ์Šค ํ’€์Šคํƒ ๊ตฌ์กฐ
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ํ•„์ˆ˜)
  • โ€ข Eloquent ๊ด€๊ณ„ ์„ค์ •: Artist hasMany Performances, Stage hasMany Performances, Performance belongsTo Artist/Stage
  • โ€ข Inertia::render()๋กœ Laravel ์ปจํŠธ๋กค๋Ÿฌ์—์„œ React ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
  • โ€ข ๋ณ„๋„ REST API ๋ผ์šฐํŠธ ์ž‘์„ฑ ๊ธˆ์ง€ (Inertia ํผ ์ฒ˜๋ฆฌ ํ™œ์šฉ)
  • โ€ข Tailwind CSS๋กœ ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ทธ๋ฆฌ๋“œ UI ๊ตฌํ˜„
  • โ€ข ๊ณต์—ฐ ์ผ์ • ์ค‘๋ณต ๊ฒ€์ฆ ๋กœ์ง ํ•„์ˆ˜ (๊ฐ™์€ ๋ฌด๋Œ€, ๊ฐ™์€ ์‹œ๊ฐ„๋Œ€)
  • โ€ข ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋Šฅ์€ localStorage ํ™œ์šฉ (DB ์ €์žฅ ๋ถˆํ•„์š”)
  • โ€ข ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (๋ชจ๋ฐ”์ผ ์šฐ์„ )
  • โ€ข ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๋Š” Laravel Storage ํ™œ์šฉ (public disk)

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

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

1
Step 1
mkdir music-festival-lineup-timetable && cd music-festival-lineup-timetable

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

Laravel 13.x + Inertia.js + React 19 ํ’€์Šคํƒ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”.

```bash
npx create laravel --react --inertia
```

๋˜๋Š” 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
```

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ํ›„ ๋‹ค์Œ ์„ค์ •์„ ํ™•์ธํ•˜์„ธ์š”:

1. `.env` ํŒŒ์ผ์—์„œ DB ์„ค์ •์„ SQLite๋กœ ๋ณ€๊ฒฝ:
```
DB_CONNECTION=sqlite
# DB_DATABASE ์ฃผ์„ ์ฒ˜๋ฆฌ ๋˜๋Š” ์‚ญ์ œ
```

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

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

4. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ (๋‘ ๊ฐœ ํ„ฐ๋ฏธ๋„ ํ•„์š”):
```bash
# ํ„ฐ๋ฏธ๋„ 1: Laravel ์„œ๋ฒ„
php artisan serve

# ํ„ฐ๋ฏธ๋„ 2: Vite ๋นŒ๋“œ
npm run dev
```

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฉด http://localhost:8000 ์—์„œ Laravel + Inertia.js + React ๊ธฐ๋ณธ ํ™”๋ฉด์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x + Inertia.js + React 19 ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ์™„๋ฃŒ. SQLite DB ์„ค์ • ์™„๋ฃŒ. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ ํ™•์ธ. ๊ธฐ๋ณธ Welcome ํŽ˜์ด์ง€๊ฐ€ Inertia.js + React๋กœ ๋ Œ๋”๋ง๋จ.

2
Step 2
์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ๊ณผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

**1. Artist ๋ชจ๋ธ ์ƒ์„ฑ:**
```bash
php artisan make:model Artist -m
```

`database/migrations/xxxx_create_artists_table.php` ํŒŒ์ผ์„ ์—ด๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •:

```php
public function up()
{
    Schema::create('artists', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('genre'); // Rock, Pop, EDM, Hip-Hop ๋“ฑ
        $table->text('bio')->nullable();
        $table->string('profile_image')->nullable();
        $table->string('instagram_url')->nullable();
        $table->string('spotify_url')->nullable();
        $table->timestamps();
    });
}
```

**2. Stage ๋ชจ๋ธ ์ƒ์„ฑ:**
```bash
php artisan make:model Stage -m
```

`database/migrations/xxxx_create_stages_table.php` ํŒŒ์ผ ์ˆ˜์ •:

```php
public function up()
{
    Schema::create('stages', function (Blueprint $table) {
        $table->id();
        $table->string('name'); // Main Stage, Sub Stage, Acoustic Tent ๋“ฑ
        $table->text('description')->nullable();
        $table->string('location')->nullable(); // ๋ฌด๋Œ€ ์œ„์น˜ ์„ค๋ช…
        $table->timestamps();
    });
}
```

**3. Performance ๋ชจ๋ธ ์ƒ์„ฑ (๊ณต์—ฐ ์ผ์ •):**
```bash
php artisan make:model Performance -m
```

`database/migrations/xxxx_create_performances_table.php` ํŒŒ์ผ ์ˆ˜์ •:

```php
public function up()
{
    Schema::create('performances', function (Blueprint $table) {
        $table->id();
        $table->foreignId('artist_id')->constrained()->onDelete('cascade');
        $table->foreignId('stage_id')->constrained()->onDelete('cascade');
        $table->date('performance_date'); // ๊ณต์—ฐ ๋‚ ์งœ
        $table->time('start_time'); // ์‹œ์ž‘ ์‹œ๊ฐ„
        $table->time('end_time'); // ์ข…๋ฃŒ ์‹œ๊ฐ„
        $table->text('setlist')->nullable(); // ์…‹๋ฆฌ์ŠคํŠธ (๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ)
        $table->timestamps();
        
        // ๊ฐ™์€ ๋ฌด๋Œ€, ๊ฐ™์€ ๋‚ ์งœ์— ์‹œ๊ฐ„ ์ค‘๋ณต ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ธ๋ฑ์Šค
        $table->index(['stage_id', 'performance_date', 'start_time']);
    });
}
```

**4. ๋ชจ๋ธ ๊ด€๊ณ„ ์„ค์ •:**

`app/Models/Artist.php`:
```php
public function performances()
{
    return $this->hasMany(Performance::class);
}
```

`app/Models/Stage.php`:
```php
public function performances()
{
    return $this->hasMany(Performance::class);
}
```

`app/Models/Performance.php`:
```php
protected $casts = [
    'performance_date' => 'date',
    'start_time' => 'datetime:H:i',
    'end_time' => 'datetime:H:i',
];

public function artist()
{
    return $this->belongsTo(Artist::class);
}

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

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

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์„ฑ๊ณตํ•˜๋ฉด artists, stages, performances ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Artist, Stage, Performance ๋ชจ๋ธ๊ณผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ ์™„๋ฃŒ. Eloquent ๊ด€๊ณ„ ์„ค์ • ์™„๋ฃŒ (hasMany, belongsTo). ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ํ›„ SQLite DB์— 3๊ฐœ ํ…Œ์ด๋ธ” ์ƒ์„ฑ๋จ.

3
Step 3
์ด์ œ ์‹œ๋”(Seeder)๋ฅผ ๋งŒ๋“ค์–ด ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

**1. ์‹œ๋” ํŒŒ์ผ ์ƒ์„ฑ:**
```bash
php artisan make:seeder FestivalSeeder
```

**2. `database/seeders/FestivalSeeder.php` ํŒŒ์ผ ์ˆ˜์ •:**

```php
use App\Models\Artist;
use App\Models\Stage;
use App\Models\Performance;
use Illuminate\Support\Facades\DB;

public function run()
{
    // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ
    DB::table('performances')->delete();
    DB::table('artists')->delete();
    DB::table('stages')->delete();

    // Stage ์ƒ์„ฑ
    $mainStage = Stage::create([
        'name' => 'Main Stage',
        'description' => '๋ฉ”์ธ ๋ฌด๋Œ€ - ํ—ค๋“œ๋ผ์ด๋„ˆ ๊ณต์—ฐ',
        'location' => '์ค‘์•™ ๊ด‘์žฅ'
    ]);

    $subStage = Stage::create([
        'name' => 'Sub Stage',
        'description' => '์„œ๋ธŒ ๋ฌด๋Œ€ - ์‹ ์ง„ ์•„ํ‹ฐ์ŠคํŠธ',
        'location' => '๋™์ธก ์ž”๋””๋ฐญ'
    ]);

    $acousticTent = Stage::create([
        'name' => 'Acoustic Tent',
        'description' => '์–ด์ฟ ์Šคํ‹ฑ ํ…ํŠธ - ์†Œ๊ทœ๋ชจ ๊ณต์—ฐ',
        'location' => '์„œ์ธก ์ˆฒ์†'
    ]);

    // Artist ์ƒ์„ฑ
    $artists = [
        ['name' => 'The Rolling Waves', 'genre' => 'Rock', 'bio' => '์ „์„ค์ ์ธ ๋ก ๋ฐด๋“œ', 'instagram_url' => 'https://instagram.com/rollingwaves'],
        ['name' => 'DJ Neon', 'genre' => 'EDM', 'bio' => '์ผ๋ ‰ํŠธ๋กœ๋‹‰ ๋ฎค์ง ํ”„๋กœ๋“€์„œ', 'spotify_url' => 'https://spotify.com/djneon'],
        ['name' => 'Luna Park', 'genre' => 'Indie Pop', 'bio' => '๊ฐ์„ฑ ์ธ๋”” ํŒ ๋“€์˜ค'],
        ['name' => 'Urban Poets', 'genre' => 'Hip-Hop', 'bio' => 'ํž™ํ•ฉ ํฌ๋ฃจ'],
        ['name' => 'Acoustic Dreams', 'genre' => 'Folk', 'bio' => 'ํฌํฌ ์‹ฑ์–ด์†ก๋ผ์ดํ„ฐ'],
        ['name' => 'Electric Souls', 'genre' => 'Electronic', 'bio' => '์‹คํ—˜์  ์ผ๋ ‰ํŠธ๋กœ๋‹ˆ์นด'],
    ];

    $createdArtists = [];
    foreach ($artists as $artist) {
        $createdArtists[] = Artist::create($artist);
    }

    // Performance ์ƒ์„ฑ (2024-07-20, 2024-07-21 ์–‘์ผ๊ฐ„)
    Performance::create([
        'artist_id' => $createdArtists[0]->id,
        'stage_id' => $mainStage->id,
        'performance_date' => '2024-07-20',
        'start_time' => '20:00',
        'end_time' => '21:30',
    ]);

    Performance::create([
        'artist_id' => $createdArtists[1]->id,
        'stage_id' => $mainStage->id,
        'performance_date' => '2024-07-20',
        'start_time' => '22:00',
        'end_time' => '23:30',
    ]);

    Performance::create([
        'artist_id' => $createdArtists[2]->id,
        'stage_id' => $subStage->id,
        'performance_date' => '2024-07-20',
        'start_time' => '18:00',
        'end_time' => '19:00',
    ]);

    Performance::create([
        'artist_id' => $createdArtists[3]->id,
        'stage_id' => $subStage->id,
        'performance_date' => '2024-07-21',
        'start_time' => '19:00',
        'end_time' => '20:30',
    ]);

    Performance::create([
        'artist_id' => $createdArtists[4]->id,
        'stage_id' => $acousticTent->id,
        'performance_date' => '2024-07-21',
        'start_time' => '16:00',
        'end_time' => '17:00',
    ]);

    Performance::create([
        'artist_id' => $createdArtists[5]->id,
        'stage_id' => $acousticTent->id,
        'performance_date' => '2024-07-20',
        'start_time' => '17:30',
        'end_time' => '18:30',
    ]);
}
```

**3. DatabaseSeeder์—์„œ ํ˜ธ์ถœ:**

`database/seeders/DatabaseSeeder.php` ํŒŒ์ผ ์ˆ˜์ •:

```php
public function run()
{
    $this->call([
        FestivalSeeder::class,
    ]);
}
```

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

์‹œ๋” ์‹คํ–‰ ํ›„ DB์— ์ƒ˜ํ”Œ ์•„ํ‹ฐ์ŠคํŠธ, ๋ฌด๋Œ€, ๊ณต์—ฐ ์ผ์ • ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: FestivalSeeder ์ƒ์„ฑ ์™„๋ฃŒ. ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ (3๊ฐœ ๋ฌด๋Œ€, 6๋ช… ์•„ํ‹ฐ์ŠคํŠธ, 6๊ฐœ ๊ณต์—ฐ ์ผ์ •) ์ƒ์„ฑ. DB์— ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ํ™•์ธ.

4
Step 4
์ด์ œ Laravel ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ผ์šฐํŠธ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ Inertia.js๋กœ React ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

**1. TimetableController ์ƒ์„ฑ:**
```bash
php artisan make:controller TimetableController
```

**2. `app/Http/Controllers/TimetableController.php` ํŒŒ์ผ ์ˆ˜์ •:**

```php
use App\Models\Performance;
use App\Models\Stage;
use App\Models\Artist;
use Inertia\Inertia;

class TimetableController extends Controller
{
    public function index()
    {
        // ๋ชจ๋“  ๊ณต์—ฐ ์ผ์ • ๋กœ๋“œ (์•„ํ‹ฐ์ŠคํŠธ, ๋ฌด๋Œ€ ๊ด€๊ณ„ ํฌํ•จ)
        $performances = Performance::with(['artist', 'stage'])
            ->orderBy('performance_date')
            ->orderBy('start_time')
            ->get();

        // ๋‚ ์งœ๋ณ„๋กœ ๊ทธ๋ฃนํ™”
        $groupedByDate = $performances->groupBy('performance_date');

        // ๋ชจ๋“  ๋ฌด๋Œ€ ๋ชฉ๋ก
        $stages = Stage::all();

        // ์žฅ๋ฅด ๋ชฉ๋ก (์ค‘๋ณต ์ œ๊ฑฐ)
        $genres = Artist::distinct()->pluck('genre');

        return Inertia::render('Timetable/Index', [
            'performances' => $performances,
            'groupedByDate' => $groupedByDate,
            'stages' => $stages,
            'genres' => $genres,
        ]);
    }

    public function artists()
    {
        $artists = Artist::with('performances.stage')->get();

        return Inertia::render('Artists/Index', [
            'artists' => $artists,
        ]);
    }

    public function artistShow($id)
    {
        $artist = Artist::with('performances.stage')->findOrFail($id);

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

**3. ๋ผ์šฐํŠธ ์„ค์ • (`routes/web.php`):**

```php
use App\Http\Controllers\TimetableController;

Route::get('/', [TimetableController::class, 'index'])->name('timetable.index');
Route::get('/artists', [TimetableController::class, 'artists'])->name('artists.index');
Route::get('/artists/{id}', [TimetableController::class, 'artistShow'])->name('artists.show');
```

๋ผ์šฐํŠธ ์„ค์ • ํ›„ http://localhost:8000 ์ ‘์† ์‹œ TimetableController์˜ index ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๋ฉฐ, Inertia.js๋ฅผ ํ†ตํ•ด React ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: TimetableController ์ƒ์„ฑ ์™„๋ฃŒ. index, artists, artistShow ๋ฉ”์„œ๋“œ ๊ตฌํ˜„. ๋ผ์šฐํŠธ ์„ค์ • ์™„๋ฃŒ. Inertia::render()๋กœ React ์ปดํฌ๋„ŒํŠธ์— ๊ณต์—ฐ ์ผ์ •, ๋ฌด๋Œ€, ์žฅ๋ฅด ๋ฐ์ดํ„ฐ ์ „๋‹ฌ.

5
Step 5
์ด์ œ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํƒ€์ž„ํ…Œ์ด๋ธ” UI๋ฅผ ๊ตฌํ˜„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

**1. Timetable/Index.jsx ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ:**

`resources/js/Pages/Timetable/Index.jsx` ํŒŒ์ผ ์ƒ์„ฑ:

```jsx
import { useState } from 'react';
import { Head } from '@inertiajs/react';

export default function TimetableIndex({ performances, groupedByDate, stages, genres }) {
    const [selectedDate, setSelectedDate] = useState(Object.keys(groupedByDate)[0] || null);
    const [selectedStage, setSelectedStage] = useState(null);
    const [selectedGenre, setSelectedGenre] = useState(null);
    const [favorites, setFavorites] = useState(() => {
        const saved = localStorage.getItem('favPerformances');
        return saved ? JSON.parse(saved) : [];
    });

    const toggleFavorite = (performanceId) => {
        const newFavorites = favorites.includes(performanceId)
            ? favorites.filter(id => id !== performanceId)
            : [...favorites, performanceId];
        setFavorites(newFavorites);
        localStorage.setItem('favPerformances', JSON.stringify(newFavorites));
    };

    const filteredPerformances = performances.filter(perf => {
        if (selectedDate && perf.performance_date !== selectedDate) return false;
        if (selectedStage && perf.stage.id !== selectedStage) return false;
        if (selectedGenre && perf.artist.genre !== selectedGenre) return false;
        return true;
    });

    return (
        <>
            <Head title="Festival Timetable" />
            <div className="min-h-screen bg-gradient-to-br from-purple-900 via-pink-800 to-orange-600 py-8 px-4">
                <div className="max-w-7xl mx-auto">
                    <h1 className="text-5xl font-bold text-white text-center mb-8 drop-shadow-lg">
                        ๐ŸŽธ Music Festival 2024
                    </h1>

                    {/* ํ•„ํ„ฐ ์„น์…˜ */}
                    <div className="bg-white/10 backdrop-blur-md rounded-xl p-6 mb-8">
                        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
                            {/* ๋‚ ์งœ ํ•„ํ„ฐ */}
                            <div>
                                <label className="block text-white font-semibold mb-2">๐Ÿ“… Date</label>
                                <select
                                    className="w-full px-4 py-2 rounded-lg bg-white/90 text-gray-800"
                                    value={selectedDate || ''}
                                    onChange={(e) => setSelectedDate(e.target.value || null)}
                                >
                                    <option value="">All Dates</option>
                                    {Object.keys(groupedByDate).map(date => (
                                        <option key={date} value={date}>{date}</option>
                                    ))}
                                </select>
                            </div>

                            {/* ๋ฌด๋Œ€ ํ•„ํ„ฐ */}
                            <div>
                                <label className="block text-white font-semibold mb-2">๐ŸŽค Stage</label>
                                <select
                                    className="w-full px-4 py-2 rounded-lg bg-white/90 text-gray-800"
                                    value={selectedStage || ''}
                                    onChange={(e) => setSelectedStage(e.target.value ? parseInt(e.target.value) : null)}
                                >
                                    <option value="">All Stages</option>
                                    {stages.map(stage => (
                                        <option key={stage.id} value={stage.id}>{stage.name}</option>
                                    ))}
                                </select>
                            </div>

                            {/* ์žฅ๋ฅด ํ•„ํ„ฐ */}
                            <div>
                                <label className="block text-white font-semibold mb-2">๐ŸŽต Genre</label>
                                <select
                                    className="w-full px-4 py-2 rounded-lg bg-white/90 text-gray-800"
                                    value={selectedGenre || ''}
                                    onChange={(e) => setSelectedGenre(e.target.value || null)}
                                >
                                    <option value="">All Genres</option>
                                    {genres.map(genre => (
                                        <option key={genre} value={genre}>{genre}</option>
                                    ))}
                                </select>
                            </div>
                        </div>
                    </div>

                    {/* ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ทธ๋ฆฌ๋“œ */}
                    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                        {filteredPerformances.map(perf => (
                            <div
                                key={perf.id}
                                className="bg-white rounded-xl shadow-2xl overflow-hidden transform transition hover:scale-105 hover:shadow-3xl"
                            >
                                <div className="bg-gradient-to-r from-pink-500 to-purple-600 p-4 text-white">
                                    <div className="flex justify-between items-start">
                                        <div>
                                            <h3 className="text-2xl font-bold">{perf.artist.name}</h3>
                                            <span className="inline-block bg-white/20 px-3 py-1 rounded-full text-sm mt-2">
                                                {perf.artist.genre}
                                            </span>
                                        </div>
                                        <button
                                            onClick={() => toggleFavorite(perf.id)}
                                            className="text-3xl transform transition hover:scale-125"
                                        >
                                            {favorites.includes(perf.id) ? 'โญ' : 'โ˜†'}
                                        </button>
                                    </div>
                                </div>
                                <div className="p-6">
                                    <div className="flex items-center text-gray-700 mb-2">
                                        <span className="font-semibold mr-2">๐Ÿ“</span>
                                        {perf.stage.name}
                                    </div>
                                    <div className="flex items-center text-gray-700 mb-2">
                                        <span className="font-semibold mr-2">๐Ÿ“…</span>
                                        {perf.performance_date}
                                    </div>
                                    <div className="flex items-center text-gray-700">
                                        <span className="font-semibold mr-2">๐Ÿ•</span>
                                        {perf.start_time} - {perf.end_time}
                                    </div>
                                </div>
                            </div>
                        ))}
                    </div>

                    {filteredPerformances.length === 0 && (
                        <div className="text-center text-white text-xl mt-12">
                            No performances found with current filters.
                        </div>
                    )}
                </div>
            </div>
        </>
    );
}
```

**2. Tailwind CSS ์„ค์ • ํ™•์ธ:**

`tailwind.config.js` ํŒŒ์ผ์—์„œ Inertia.js ๊ฒฝ๋กœ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ:

```js
content: [
    './resources/**/*.blade.php',
    './resources/**/*.jsx',
    './resources/**/*.js',
],
```

**3. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์žฌ์‹œ์ž‘:**

```bash
npm run dev
```

http://localhost:8000 ์ ‘์† ์‹œ ํƒ€์ž„ํ…Œ์ด๋ธ” UI๊ฐ€ ํ‘œ์‹œ๋˜๋ฉฐ, ๋‚ ์งœ/๋ฌด๋Œ€/์žฅ๋ฅด ํ•„ํ„ฐ๋ง๊ณผ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋Šฅ์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Timetable/Index.jsx ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์™„๋ฃŒ. ํƒ€์ž„ํ…Œ์ด๋ธ” ๊ทธ๋ฆฌ๋“œ UI ๊ตฌํ˜„. ๋‚ ์งœ/๋ฌด๋Œ€/์žฅ๋ฅด ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ. ์ฆ๊ฒจ์ฐพ๊ธฐ ํ† ๊ธ€ ๊ธฐ๋Šฅ (localStorage). Tailwind CSS ๊ทธ๋ผ๋””์–ธํŠธ ๋ฐฐ๊ฒฝ, ์นด๋“œ ๋””์ž์ธ ์ ์šฉ. ๋ฐ˜์‘ํ˜• ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ.

6
Step 6
๋งˆ์ง€๋ง‰์œผ๋กœ ์•„ํ‹ฐ์ŠคํŠธ ๋ชฉ๋ก ํŽ˜์ด์ง€์™€ ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ”„๋กœ์ ํŠธ๋ฅผ ์™„์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

**1. Artists/Index.jsx ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ:**

`resources/js/Pages/Artists/Index.jsx` ํŒŒ์ผ ์ƒ์„ฑ:

```jsx
import { Head, Link } from '@inertiajs/react';

export default function ArtistsIndex({ artists }) {
    return (
        <>
            <Head title="Artists Lineup" />
            <div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-800 to-pink-600 py-8 px-4">
                <div className="max-w-7xl mx-auto">
                    <h1 className="text-5xl font-bold text-white text-center mb-8 drop-shadow-lg">
                        ๐ŸŽค Artists Lineup
                    </h1>

                    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                        {artists.map(artist => (
                            <Link
                                key={artist.id}
                                href={`/artists/${artist.id}`}
                                className="bg-white rounded-xl shadow-2xl overflow-hidden transform transition hover:scale-105 hover:shadow-3xl block"
                            >
                                <div className="h-48 bg-gradient-to-br from-pink-400 to-purple-500 flex items-center justify-center">
                                    <span className="text-white text-6xl">๐ŸŽธ</span>
                                </div>
                                <div className="p-6">
                                    <h2 className="text-2xl font-bold text-gray-800 mb-2">{artist.name}</h2>
                                    <span className="inline-block bg-purple-100 text-purple-800 px-3 py-1 rounded-full text-sm font-semibold">
                                        {artist.genre}
                                    </span>
                                    {artist.bio && (
                                        <p className="text-gray-600 mt-3 line-clamp-2">{artist.bio}</p>
                                    )}
                                    <div className="mt-4 text-purple-600 font-semibold">
                                        {artist.performances.length} performance(s) โ†’
                                    </div>
                                </div>
                            </Link>
                        ))}
                    </div>
                </div>
            </div>
        </>
    );
}
```

**2. Artists/Show.jsx ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ:**

`resources/js/Pages/Artists/Show.jsx` ํŒŒ์ผ ์ƒ์„ฑ:

```jsx
import { Head, Link } from '@inertiajs/react';

export default function ArtistShow({ artist }) {
    return (
        <>
            <Head title={artist.name} />
            <div className="min-h-screen bg-gradient-to-br from-purple-900 via-pink-800 to-orange-600 py-8 px-4">
                <div className="max-w-4xl mx-auto">
                    <Link href="/artists" className="text-white text-lg mb-6 inline-block hover:underline">
                        โ† Back to Artists
                    </Link>

                    <div className="bg-white rounded-xl shadow-2xl overflow-hidden">
                        <div className="h-64 bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center">
                            <span className="text-white text-9xl">๐ŸŽธ</span>
                        </div>

                        <div className="p-8">
                            <h1 className="text-4xl font-bold text-gray-800 mb-4">{artist.name}</h1>
                            <span className="inline-block bg-purple-100 text-purple-800 px-4 py-2 rounded-full text-lg font-semibold mb-6">
                                {artist.genre}
                            </span>

                            {artist.bio && (
                                <p className="text-gray-700 text-lg mb-6">{artist.bio}</p>
                            )}

                            {/* SNS ๋งํฌ */}
                            <div className="flex gap-4 mb-8">
                                {artist.instagram_url && (
                                    <a
                                        href={artist.instagram_url}
                                        target="_blank"
                                        rel="noopener noreferrer"
                                        className="bg-pink-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-pink-600 transition"
                                    >
                                        ๐Ÿ“ท Instagram
                                    </a>
                                )}
                                {artist.spotify_url && (
                                    <a
                                        href={artist.spotify_url}
                                        target="_blank"
                                        rel="noopener noreferrer"
                                        className="bg-green-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-green-600 transition"
                                    >
                                        ๐ŸŽต Spotify
                                    </a>
                                )}
                            </div>

                            {/* ๊ณต์—ฐ ์ผ์ • */}
                            <h2 className="text-2xl font-bold text-gray-800 mb-4">Performance Schedule</h2>
                            <div className="space-y-4">
                                {artist.performances.map(perf => (
                                    <div key={perf.id} className="bg-purple-50 p-4 rounded-lg">
                                        <div className="flex justify-between items-center">
                                            <div>
                                                <div className="font-semibold text-gray-800">{perf.stage.name}</div>
                                                <div className="text-gray-600">{perf.performance_date}</div>
                                            </div>
                                            <div className="text-right">
                                                <div className="font-semibold text-purple-600">
                                                    {perf.start_time} - {perf.end_time}
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                ))}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </>
    );
}
```

**3. ๋„ค๋น„๊ฒŒ์ด์…˜ ์ถ”๊ฐ€ (์„ ํƒ):**

Timetable/Index.jsx ์ƒ๋‹จ์— ๋„ค๋น„๊ฒŒ์ด์…˜ ๋งํฌ ์ถ”๊ฐ€:

```jsx
<div className="flex justify-center gap-4 mb-8">
    <Link href="/" className="bg-white/20 backdrop-blur-md text-white px-6 py-3 rounded-lg font-semibold hover:bg-white/30 transition">
        ๐ŸŽซ Timetable
    </Link>
    <Link href="/artists" className="bg-white/20 backdrop-blur-md text-white px-6 py-3 rounded-lg font-semibold hover:bg-white/30 transition">
        ๐ŸŽค Artists
    </Link>
</div>
```

Artists/Index.jsx์—๋„ ๋™์ผํ•˜๊ฒŒ ์ถ”๊ฐ€.

**4. ์ตœ์ข… ํ…Œ์ŠคํŠธ:**

- http://localhost:8000 - ํƒ€์ž„ํ…Œ์ด๋ธ” ํŽ˜์ด์ง€ (ํ•„ํ„ฐ๋ง, ์ฆ๊ฒจ์ฐพ๊ธฐ ์ž‘๋™)
- http://localhost:8000/artists - ์•„ํ‹ฐ์ŠคํŠธ ๋ชฉ๋ก ํŽ˜์ด์ง€
- http://localhost:8000/artists/1 - ์•„ํ‹ฐ์ŠคํŠธ ์ƒ์„ธ ํŽ˜์ด์ง€ (๊ณต์—ฐ ์ผ์ • ํ‘œ์‹œ)

๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ์ •์ƒ ์ž‘๋™ํ•˜๋ฉด ํ”„๋กœ์ ํŠธ ์™„์„ฑ์ž…๋‹ˆ๋‹ค! ๐ŸŽ‰

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Artists/Index.jsx, Artists/Show.jsx ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์™„๋ฃŒ. ์•„ํ‹ฐ์ŠคํŠธ ๋ชฉ๋ก ๊ทธ๋ฆฌ๋“œ UI, ์•„ํ‹ฐ์ŠคํŠธ ์ƒ์„ธ ํŽ˜์ด์ง€ (๊ณต์—ฐ ์ผ์ •, SNS ๋งํฌ) ๊ตฌํ˜„. ๋„ค๋น„๊ฒŒ์ด์…˜ ๋งํฌ ์ถ”๊ฐ€. ๋ชจ๋“  ํŽ˜์ด์ง€ ์ •์ƒ ์ž‘๋™. ํ”„๋กœ์ ํŠธ ์™„์„ฑ.

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

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

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

๋Œ“๊ธ€ (0)

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