๐ธ ๋ฎค์ง ํ์คํฐ๋ฒ ๋ผ์ธ์ & ํ์ํ ์ด๋ธ ํ๋ซํผ
music-festival-lineup-timetable
๋ฌธ์ ์ค๋ช
๐ธ ๋ฎค์ง ํ์คํฐ๋ฒ ๋ผ์ธ์ & ํ์ํ ์ด๋ธ ํ๋ซํผ
์์ ํ์คํฐ๋ฒ ์ฃผ์ต์๋ฅผ ์ํ ๊ณต์ฐ ๋ผ์ธ์ ๊ด๋ฆฌ ๋ฐ ํ์ํ ์ด๋ธ ๊ณต๊ฐ ํ๋ซํผ์ ๋๋ค. ํ์คํฐ๋ฒ ๊ด๋ฆฌ์๊ฐ ์ถ์ฐ ์ํฐ์คํธ๋ฅผ ๋ฑ๋กํ๊ณ ๋ฌด๋๋ณ/์๊ฐ๋๋ณ ๊ณต์ฐ ์ผ์ ์ ๊ตฌ์ฑํ๋ฉด, ๊ด๋๊ฐ์ ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋๋ ํ์ํ ์ด๋ธ์ ํ์ธํ๊ณ ๊ด์ฌ ๊ณต์ฐ์ ์ฆ๊ฒจ์ฐพ๊ธฐํ ์ ์์ต๋๋ค.
ํต์ฌ ๊ธฐ๋ฅ
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์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
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๋ก ๋ ๋๋ง๋จ.
์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์
์ ์์ฑํ๊ฒ ์ต๋๋ค.
**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๊ฐ ํ ์ด๋ธ ์์ฑ๋จ.
์ด์ ์๋(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์ ๋ฐ์ดํฐ ์ฝ์ ํ์ธ.
์ด์ 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 ์ปดํฌ๋ํธ์ ๊ณต์ฐ ์ผ์ , ๋ฌด๋, ์ฅ๋ฅด ๋ฐ์ดํฐ ์ ๋ฌ.
์ด์ 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 ๊ทธ๋ผ๋์ธํธ ๋ฐฐ๊ฒฝ, ์นด๋ ๋์์ธ ์ ์ฉ. ๋ฐ์ํ ๊ทธ๋ฆฌ๋ ๋ ์ด์์.
๋ง์ง๋ง์ผ๋ก ์ํฐ์คํธ ๋ชฉ๋ก ํ์ด์ง์ ์์ธ ํ์ด์ง๋ฅผ ์ถ๊ฐํ์ฌ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค.
**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)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!