๐ค ๋ผ์ด๋ธ ํด๋ฝ ๊ณต์ฐ ๋ผ์ธ์ & ํฐ์ผ ํ๋งค ํ์ด์ง
live-club-lineup-ticket-sales
๋ฌธ์ ์ค๋ช
๐ธ ํ๋ก์ ํธ ๊ฐ์
๋ผ์ด๋ธ ํด๋ฝ/์๊ท๋ชจ ๊ณต์ฐ์ฅ์ ์ํ ๊ณต์ฐ ๋ผ์ธ์ ์๊ฐ ๋ฐ ํฐ์ผ ํ๋งค ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค. ๋ฐฉ๋ฌธ์๋ค์ด ์ด๋ฒ ์ฃผ/๋ค์ ์ฃผ ๊ณต์ฐ ์ค์ผ์ค์ ํ์ธํ๊ณ , ๊ด์ฌ ์๋ ๊ณต์ฐ์ ํฐ์ผ์ ๊ตฌ๋งค ์ ์ฒญํ ์ ์๋ ํ์คํ ํ๋ซํผ์ ๋๋ค.
โจ ์ฃผ์ ๊ธฐ๋ฅ
ํ์ ๊ธฐ๋ฅ:
- ๐ ๊ณต์ฐ ๋ผ์ธ์ ๋ชฉ๋ก: ๋ ์ง๋ณ ๊ณต์ฐ ์ ๋ณด (์ํฐ์คํธ๋ช , ์ฅ๋ฅด, ๊ณต์ฐ ์๊ฐ, ํฐ์ผ ๊ฐ๊ฒฉ)
- ๐ซ ํฐ์ผ ๊ตฌ๋งค ์ ์ฒญ ํผ: ์ด๋ฆ, ์ด๋ฉ์ผ, ๋งค์ ์ ๋ ฅํ์ฌ ์์ฝ
- ๐ ์ฅ๋ฅด ํํฐ๋ง: ๋ก/์ฌ์ฆ/ํํฉ/์ธ๋ ๋ฑ ์ฅ๋ฅด๋ณ ๊ณต์ฐ ๊ฒ์
- ๐ณ ์์ฝ ๋ด์ญ ํ์ธ: ๋ด๊ฐ ์ ์ฒญํ ํฐ์ผ ๋ชฉ๋ก ๋ณด๊ธฐ
- ๐ฑ ๋ฐ์ํ ๋์์ธ: ๋ชจ๋ฐ์ผ ์ฐ์ 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์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
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๋ฅผ ํธ์ถํ ์ค๋น๊ฐ ๋ฉ๋๋ค.
## 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 ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ ์ด๋ธ ๊ตฌ์กฐ๊ฐ ์ค๋น๋ฉ๋๋ค.
## 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 (๋ด ์์ฝ ์กฐํ) ์๋ํฌ์ธํธ๊ฐ ์๋ํ๋ฉฐ, ์์ฌ ์ข์ ์ฐจ๊ฐ ๋ก์ง์ด ํฌํจ๋ฉ๋๋ค.
## 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์ ์ดํ์ธ ๊ณต์ฐ์๋ '๋งค์ง์๋ฐ' ๋ฐฐ์ง๊ฐ ํ์๋ฉ๋๋ค. ๊ณต์ฐ ์นด๋๋ฅผ ํด๋ฆญํ๋ฉด ๋ชจ๋ฌ์ด ์ด๋ฆฝ๋๋ค.
## 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)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!