๐ŸŽธ ์ธ๋”” ๋ฎค์ง€์…˜ EP ์•จ๋ฒ” ํ”„๋ฆฌ์˜ค๋” ํŽ˜์ด์ง€ - ํ•œ์ •ํŒ ์Œ๋ฐ˜ ์˜ˆ์•ฝ ํŒ๋งค - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ž…๋ฌธ PHP + JavaScript Laravel + React AI: Claude

๐ŸŽธ ์ธ๋”” ๋ฎค์ง€์…˜ EP ์•จ๋ฒ” ํ”„๋ฆฌ์˜ค๋” ํŽ˜์ด์ง€ - ํ•œ์ •ํŒ ์Œ๋ฐ˜ ์˜ˆ์•ฝ ํŒ๋งค

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… indie-album-preorder-page
์กฐํšŒ์ˆ˜ 48 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

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

์ธ๋”” ๋ฎค์ง€์…˜์ด๋‚˜ ์†Œ๊ทœ๋ชจ ๋ ˆ์ด๋ธ”์ด ์‹ ๊ทœ ์•จ๋ฒ”์„ ์ถœ์‹œํ•˜๊ธฐ ์ „, ํŒฌ๋“ค์—๊ฒŒ ์„ ์ฃผ๋ฌธ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํ”„๋ฆฌ์˜ค๋” ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์•„ํ‹ฐ์ŠคํŠธ ์†Œ๊ฐœ, ์•จ๋ฒ” ํŠธ๋ž™๋ฆฌ์ŠคํŠธ, ํ•œ์ •ํŒ ํŒจํ‚ค์ง€ ์˜ต์…˜, ์˜ˆ์•ฝ ๊ตฌ๋งค ํผ์ด ํฌํ•จ๋œ ๋งค๋ ฅ์ ์ธ ๋žœ๋”ฉํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค.

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

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

  • ์•จ๋ฒ” ์†Œ๊ฐœ ์„น์…˜: ์•จ๋ฒ” ์ปค๋ฒ„ ์ด๋ฏธ์ง€, ์•„ํ‹ฐ์ŠคํŠธ ํ”„๋กœํ•„, ๋ฐœ๋งค ์˜ˆ์ •์ผ ํ‘œ์‹œ
  • ํŠธ๋ž™๋ฆฌ์ŠคํŠธ ํ…Œ์ด๋ธ”: ๊ณก ์ œ๋ชฉ, ์žฌ์ƒ ์‹œ๊ฐ„, ๊ฐ„๋‹จํ•œ ์„ค๋ช…
  • ํŒจํ‚ค์ง€ ์˜ต์…˜ ์นด๋“œ: ์ผ๋ฐ˜ํŒ/ํ•œ์ •ํŒ/๋””๋Ÿญ์Šค ์—๋””์…˜ ๋“ฑ ์„ ํƒ์ง€ (๊ฐ€๊ฒฉ, ๊ตฌ์„ฑํ’ˆ ์„ค๋ช…)
  • ํ”„๋ฆฌ์˜ค๋” ํผ: ๊ตฌ๋งค์ž ์ด๋ฆ„, ์ด๋ฉ”์ผ, ์„ ํƒํ•œ ํŒจํ‚ค์ง€, ์ˆ˜๋Ÿ‰ ์ž…๋ ฅ
  • ์˜ˆ์•ฝ ํ˜„ํ™ฉ ํ‘œ์‹œ: ํ˜„์žฌ๊นŒ์ง€ ์˜ˆ์•ฝ๋œ ์•จ๋ฒ” ์ˆ˜๋Ÿ‰ ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ ("๐Ÿ”ฅ ํ˜„์žฌ 327์žฅ ์˜ˆ์•ฝ ์™„๋ฃŒ!")
  • ๊ด€๋ฆฌ์ž ๋ทฐ: ์˜ˆ์•ฝ ๋ชฉ๋ก ํ™•์ธ (์ด๋ฆ„, ์ด๋ฉ”์ผ, ํŒจํ‚ค์ง€, ์ˆ˜๋Ÿ‰, ์˜ˆ์•ฝ์ผ์‹œ)

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

  • ์–ผ๋ฆฌ๋ฒ„๋“œ ํ• ์ธ ํƒ€์ด๋จธ ("โฐ 24์‹œ๊ฐ„ ๋‚จ์Œ! 20% ํ• ์ธ")
  • ์†Œ์…œ ๊ณต์œ  ๋ฒ„ํŠผ (Twitter, Instagram)
  • ์•จ๋ฒ” ํ‹ฐ์ € ์˜์ƒ ์ž„๋ฒ ๋“œ (YouTube)
  • ํŒจํ‚ค์ง€๋ณ„ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ์ œํ•œ ์„ค์ •

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x (API ์„œ๋ฒ„, SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค)
  • ํ”„๋ก ํŠธ์—”๋“œ: React 19 (Vite)
  • ์Šคํƒ€์ผ๋ง: Tailwind CSS
  • ์•„์ด์ฝ˜: Heroicons

ํ•™์Šต ๋ชฉํ‘œ

  • Laravel API ๋ผ์šฐํŠธ์™€ ์ปจํŠธ๋กค๋Ÿฌ ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ดํ•ด
  • React์—์„œ fetch/axios๋กœ API ํ˜ธ์ถœํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์—ฐ๋™
  • ๊ฐ„๋‹จํ•œ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ ๋ชจ๋ธ ์ž‘์„ฑ
  • ํผ ์ œ์ถœ๊ณผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ธฐ์ดˆ
  • useState๋ฅผ ํ™œ์šฉํ•œ ์ƒํƒœ ๊ด€๋ฆฌ
  • Tailwind CSS๋กœ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ (๊ตฌ๋ฒ„์ „ ๊ธˆ์ง€)
  • โ€ข React 19 ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (์„ค์ • ๊ฐ„๋‹จ)
  • โ€ข ๋ณ„๋„์˜ API ์„œ๋ฒ„(Laravel)์™€ ํ”„๋ก ํŠธ์—”๋“œ(React+Vite) ํ”„๋กœ์ ํŠธ๋กœ ๋ถ„๋ฆฌ
  • โ€ข CORS ์„ค์ • ํ•„์ˆ˜ (Laravel์—์„œ React ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์š”์ฒญ ํ—ˆ์šฉ)
  • โ€ข ๋ณต์žกํ•œ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ธˆ์ง€ (useState๋งŒ ์‚ฌ์šฉ)
  • โ€ข ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ธฐ๋Šฅ ๊ธˆ์ง€ (HTTP ๊ธฐ๋ฐ˜ ํผ ์ œ์ถœ๋งŒ ์‚ฌ์šฉ)
  • โ€ข ๊ด€๋ฆฌ์ž ์ธ์ฆ์€ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ฒ˜๋ฆฌ (Laravel Sanctum ๋˜๋Š” ๊ธฐ๋ณธ ์ธ์ฆ)
  • โ€ข ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ํ•„์ˆ˜ (๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ/๋ฐ์Šคํฌํ†ฑ ๋Œ€์‘)

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

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

1
Step 1
mkdir indie-album-preorder-page && cd indie-album-preorder-page

์ด ํด๋”์—์„œ Laravel 13.x API ๋ฐฑ์—”๋“œ์™€ React 19 ํ”„๋ก ํŠธ์—”๋“œ๋กœ ๊ตฌ์„ฑ๋œ ์ธ๋”” ์•จ๋ฒ” ํ”„๋ฆฌ์˜ค๋” ํŽ˜์ด์ง€ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

## 1๋‹จ๊ณ„: Laravel API ๋ฐฑ์—”๋“œ ์„ค์ •

### Laravel ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
```bash
composer create-project laravel/laravel:^13.0 backend
cd backend
```

### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • (SQLite)
`.env` ํŒŒ์ผ ์ˆ˜์ •:
```
DB_CONNECTION=sqlite
# DB_HOST, DB_PORT, DB_DATABASE ๋“ฑ ์ฃผ์„ ์ฒ˜๋ฆฌ
```

SQLite ํŒŒ์ผ ์ƒ์„ฑ:
```bash
touch database/database.sqlite
```

### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
`preorders` ํ…Œ์ด๋ธ”์„ ์œ„ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ƒ์„ฑ:
```bash
php artisan make:migration create_preorders_table
```

`database/migrations/xxxx_create_preorders_table.php` ๋‚ด์šฉ:
```php
public function up()
{
    Schema::create('preorders', function (Blueprint $table) {
        $table->id();
        $table->string('customer_name');
        $table->string('email');
        $table->enum('package_type', ['standard', 'limited', 'deluxe']);
        $table->integer('quantity')->default(1);
        $table->timestamps();
    });
}
```

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

### ๋ชจ๋ธ ์ƒ์„ฑ
```bash
php artisan make:model Preorder
```

`app/Models/Preorder.php`:
```php
protected $fillable = ['customer_name', 'email', 'package_type', 'quantity'];
```

### CORS ์„ค์ •
`config/cors.php`์—์„œ `'paths' => ['api/*']` ํ™•์ธ

`'allowed_origins' => ['http://localhost:5173']` ์ถ”๊ฐ€ (React ๊ฐœ๋ฐœ ์„œ๋ฒ„)

### API ๋ผ์šฐํŠธ ์ž‘์„ฑ
`routes/api.php`:
```php
use App\Models\Preorder;
use Illuminate\Http\Request;

Route::get('/preorders/count', function () {
    return response()->json(['count' => Preorder::sum('quantity')]);
});

Route::post('/preorders', function (Request $request) {
    $validated = $request->validate([
        'customer_name' => 'required|string|max:255',
        'email' => 'required|email',
        'package_type' => 'required|in:standard,limited,deluxe',
        'quantity' => 'required|integer|min:1|max:10'
    ]);

    $preorder = Preorder::create($validated);
    return response()->json($preorder, 201);
});

Route::get('/preorders', function () {
    return response()->json(Preorder::latest()->get());
});
```

### Laravel ์„œ๋ฒ„ ์‹คํ–‰
```bash
php artisan serve
```

API๊ฐ€ `http://localhost:8000`์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์ด์ œ ๋ฐฑ์—”๋“œ API ์„œ๋ฒ„๊ฐ€ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ React ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x ๋ฐฑ์—”๋“œ API ์„œ๋ฒ„๊ฐ€ http://localhost:8000์—์„œ ์‹คํ–‰๋˜๋ฉฐ, SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— preorders ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜๊ณ , API ์—”๋“œํฌ์ธํŠธ(/api/preorders/count, /api/preorders POST, /api/preorders GET)๊ฐ€ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. Postman์ด๋‚˜ curl๋กœ API ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ.

2
Step 2
## 2๋‹จ๊ณ„: React 19 ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

### ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ๋กœ ์ด๋™
```bash
cd ..
```

### React + Vite ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
```bash
npm create vite@latest frontend -- --template react
cd frontend
npm install
```

### Tailwind CSS ์„ค์น˜
```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

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

`src/index.css`:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

### ๊ธฐ๋ณธ App.jsx ๊ตฌ์กฐ
`src/App.jsx`:
```jsx
import { useState, useEffect } from 'react';

function App() {
  const [preorderCount, setPreorderCount] = useState(0);

  useEffect(() => {
    fetch('http://localhost:8000/api/preorders/count')
      .then(res => res.json())
      .then(data => setPreorderCount(data.count))
      .catch(err => console.error(err));
  }, []);

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-800 to-blue-900">
      <div className="container mx-auto px-4 py-12">
        <h1 className="text-5xl font-bold text-white text-center mb-4">
          ๐ŸŽธ Midnight Echo - New EP "Neon Dreams"
        </h1>
        <p className="text-xl text-purple-200 text-center mb-8">
          Pre-order Now | Release Date: 2025.03.15
        </p>
        <div className="text-center mb-12">
          <span className="inline-block bg-yellow-400 text-purple-900 font-bold px-6 py-2 rounded-full text-lg">
            ๐Ÿ”ฅ {preorderCount} albums pre-ordered!
          </span>
        </div>
      </div>
    </div>
  );
}

export default App;
```

### ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰
```bash
npm run dev
```

React ์•ฑ์ด `http://localhost:5173`์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

๋ธŒ๋ผ์šฐ์ €์—์„œ ํŽ˜์ด์ง€๋ฅผ ์—ด๋ฉด ์•จ๋ฒ” ์ œ๋ชฉ๊ณผ ํ˜„์žฌ ์˜ˆ์•ฝ ์ˆ˜๋Ÿ‰์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. Laravel API์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: React 19 + Vite ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ http://localhost:5173์—์„œ ์‹คํ–‰๋˜๋ฉฐ, Tailwind CSS๊ฐ€ ์ ์šฉ๋œ ๊ทธ๋ผ๋ฐ์ด์…˜ ๋ฐฐ๊ฒฝ์˜ ์•จ๋ฒ” ์†Œ๊ฐœ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. Laravel API์—์„œ ์˜ˆ์•ฝ ์ˆ˜๋Ÿ‰์„ fetchํ•˜์—ฌ ํ™”๋ฉด์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

3
Step 3
## 3๋‹จ๊ณ„: ์•จ๋ฒ” ์†Œ๊ฐœ & ํŠธ๋ž™๋ฆฌ์ŠคํŠธ ์„น์…˜ ์ถ”๊ฐ€

`src/App.jsx`๋ฅผ ํ™•์žฅํ•˜์—ฌ ์•จ๋ฒ” ์ปค๋ฒ„ ์ด๋ฏธ์ง€, ํŠธ๋ž™๋ฆฌ์ŠคํŠธ, ํŒจํ‚ค์ง€ ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

```jsx
import { useState, useEffect } from 'react';

function App() {
  const [preorderCount, setPreorderCount] = useState(0);

  useEffect(() => {
    fetch('http://localhost:8000/api/preorders/count')
      .then(res => res.json())
      .then(data => setPreorderCount(data.count))
      .catch(err => console.error(err));
  }, []);

  const tracks = [
    { no: 1, title: 'Neon Lights', duration: '3:45' },
    { no: 2, title: 'Midnight Drive', duration: '4:12' },
    { no: 3, title: 'Echo in the Dark', duration: '3:58' },
    { no: 4, title: 'Digital Dreams', duration: '5:20' },
    { no: 5, title: 'Last Call', duration: '4:05' },
  ];

  const packages = [
    {
      type: 'standard',
      name: 'Standard Edition',
      price: 15000,
      items: ['CD', 'Digital Download Code', 'Photocard (Random 1 of 5)']
    },
    {
      type: 'limited',
      name: 'Limited Edition',
      price: 25000,
      items: ['CD', 'Digital Download Code', 'Photocard Set (5 pcs)', 'Poster', 'Sticker Pack'],
      badge: 'POPULAR'
    },
    {
      type: 'deluxe',
      name: 'Deluxe Box Set',
      price: 45000,
      items: ['CD', 'Vinyl LP', 'Digital Download Code', 'Photocard Set', 'Poster', 'Sticker Pack', 'Art Book (40 pages)', 'Signed Postcard']
    }
  ];

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-800 to-blue-900">
      <div className="container mx-auto px-4 py-12">
        {/* Header */}
        <h1 className="text-5xl font-bold text-white text-center mb-4">
          ๐ŸŽธ Midnight Echo - New EP "Neon Dreams"
        </h1>
        <p className="text-xl text-purple-200 text-center mb-8">
          Pre-order Now | Release Date: 2025.03.15
        </p>
        <div className="text-center mb-12">
          <span className="inline-block bg-yellow-400 text-purple-900 font-bold px-6 py-2 rounded-full text-lg">
            ๐Ÿ”ฅ {preorderCount} albums pre-ordered!
          </span>
        </div>

        {/* Album Cover & Artist Info */}
        <div className="max-w-4xl mx-auto mb-16">
          <div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 shadow-2xl">
            <div className="grid md:grid-cols-2 gap-8 items-center">
              <div>
                <div className="aspect-square bg-gradient-to-br from-pink-500 to-purple-600 rounded-xl shadow-lg flex items-center justify-center">
                  <span className="text-white text-6xl font-bold">NE</span>
                </div>
              </div>
              <div>
                <h2 className="text-3xl font-bold text-white mb-4">About the Album</h2>
                <p className="text-purple-100 mb-4">
                  "Neon Dreams" is Midnight Echo's sophomore EP, featuring 5 tracks of synth-driven indie rock. Recorded in Seoul, this album captures the energy of late-night city drives and neon-lit streets.
                </p>
                <p className="text-purple-200 text-sm">
                  Producer: Kim Jae-hyun | Label: Echo Records | Genre: Indie Rock, Synth Pop
                </p>
              </div>
            </div>
          </div>
        </div>

        {/* Tracklist */}
        <div className="max-w-4xl mx-auto mb-16">
          <h2 className="text-3xl font-bold text-white mb-6 text-center">๐Ÿ“€ Tracklist</h2>
          <div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl">
            <table className="w-full text-white">
              <thead>
                <tr className="border-b border-purple-400/30">
                  <th className="text-left py-3 px-4">No.</th>
                  <th className="text-left py-3 px-4">Title</th>
                  <th className="text-right py-3 px-4">Duration</th>
                </tr>
              </thead>
              <tbody>
                {tracks.map(track => (
                  <tr key={track.no} className="border-b border-purple-400/10 hover:bg-white/5">
                    <td className="py-3 px-4">{track.no}</td>
                    <td className="py-3 px-4 font-medium">{track.title}</td>
                    <td className="py-3 px-4 text-right text-purple-300">{track.duration}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        {/* Package Options */}
        <div className="max-w-6xl mx-auto mb-16">
          <h2 className="text-3xl font-bold text-white mb-6 text-center">๐ŸŽ Package Options</h2>
          <div className="grid md:grid-cols-3 gap-6">
            {packages.map(pkg => (
              <div key={pkg.type} className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl hover:scale-105 transition relative">
                {pkg.badge && (
                  <span className="absolute top-4 right-4 bg-yellow-400 text-purple-900 text-xs font-bold px-3 py-1 rounded-full">
                    {pkg.badge}
                  </span>
                )}
                <h3 className="text-2xl font-bold text-white mb-2">{pkg.name}</h3>
                <p className="text-3xl font-bold text-yellow-400 mb-4">โ‚ฉ{pkg.price.toLocaleString()}</p>
                <ul className="text-purple-100 space-y-2">
                  {pkg.items.map((item, i) => (
                    <li key={i} className="flex items-start">
                      <span className="mr-2">โœ“</span>
                      <span>{item}</span>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
```

๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์•จ๋ฒ” ์ปค๋ฒ„(์ž„์‹œ ๊ทธ๋ผ๋ฐ์ด์…˜), ํŠธ๋ž™๋ฆฌ์ŠคํŠธ ํ…Œ์ด๋ธ”, 3๊ฐ€์ง€ ํŒจํ‚ค์ง€ ์˜ต์…˜ ์นด๋“œ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์•จ๋ฒ” ์†Œ๊ฐœ ์„น์…˜(์ปค๋ฒ„ ์ด๋ฏธ์ง€, ์•„ํ‹ฐ์ŠคํŠธ ์„ค๋ช…), ํŠธ๋ž™๋ฆฌ์ŠคํŠธ ํ…Œ์ด๋ธ”(5๊ณก), 3๊ฐ€์ง€ ํŒจํ‚ค์ง€ ์˜ต์…˜ ์นด๋“œ(Standard/Limited/Deluxe)๊ฐ€ Tailwind CSS๋กœ ์Šคํƒ€์ผ๋ง๋˜์–ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ํ˜ธ๋ฒ„ ํšจ๊ณผ์™€ ๋ฑƒ์ง€(POPULAR)๊ฐ€ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

4
Step 4
## 4๋‹จ๊ณ„: ํ”„๋ฆฌ์˜ค๋” ํผ ์ถ”๊ฐ€ ๋ฐ API ์—ฐ๋™

์‚ฌ์šฉ์ž๊ฐ€ ํŒจํ‚ค์ง€๋ฅผ ์„ ํƒํ•˜๊ณ  ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ ํ›„ ํ”„๋ฆฌ์˜ค๋”๋ฅผ ์ œ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ํผ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

`src/App.jsx`์— ํผ ์ƒํƒœ์™€ ์ œ์ถœ ๋กœ์ง์„ ์ถ”๊ฐ€:

```jsx
import { useState, useEffect } from 'react';

function App() {
  const [preorderCount, setPreorderCount] = useState(0);
  const [formData, setFormData] = useState({
    customer_name: '',
    email: '',
    package_type: 'limited',
    quantity: 1
  });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitMessage, setSubmitMessage] = useState('');

  useEffect(() => {
    fetchPreorderCount();
  }, []);

  const fetchPreorderCount = () => {
    fetch('http://localhost:8000/api/preorders/count')
      .then(res => res.json())
      .then(data => setPreorderCount(data.count))
      .catch(err => console.error(err));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    setSubmitMessage('');

    try {
      const response = await fetch('http://localhost:8000/api/preorders', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData)
      });

      if (response.ok) {
        setSubmitMessage('โœ… Pre-order confirmed! Check your email for details.');
        setFormData({
          customer_name: '',
          email: '',
          package_type: 'limited',
          quantity: 1
        });
        fetchPreorderCount(); // ์˜ˆ์•ฝ ์ˆ˜๋Ÿ‰ ์—…๋ฐ์ดํŠธ
      } else {
        const error = await response.json();
        setSubmitMessage('โŒ Error: ' + (error.message || 'Please try again.'));
      }
    } catch (err) {
      setSubmitMessage('โŒ Network error. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };

  const tracks = [
    { no: 1, title: 'Neon Lights', duration: '3:45' },
    { no: 2, title: 'Midnight Drive', duration: '4:12' },
    { no: 3, title: 'Echo in the Dark', duration: '3:58' },
    { no: 4, title: 'Digital Dreams', duration: '5:20' },
    { no: 5, title: 'Last Call', duration: '4:05' },
  ];

  const packages = [
    {
      type: 'standard',
      name: 'Standard Edition',
      price: 15000,
      items: ['CD', 'Digital Download Code', 'Photocard (Random 1 of 5)']
    },
    {
      type: 'limited',
      name: 'Limited Edition',
      price: 25000,
      items: ['CD', 'Digital Download Code', 'Photocard Set (5 pcs)', 'Poster', 'Sticker Pack'],
      badge: 'POPULAR'
    },
    {
      type: 'deluxe',
      name: 'Deluxe Box Set',
      price: 45000,
      items: ['CD', 'Vinyl LP', 'Digital Download Code', 'Photocard Set', 'Poster', 'Sticker Pack', 'Art Book (40 pages)', 'Signed Postcard']
    }
  ];

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-800 to-blue-900">
      <div className="container mx-auto px-4 py-12">
        {/* Header */}
        <h1 className="text-5xl font-bold text-white text-center mb-4">
          ๐ŸŽธ Midnight Echo - New EP "Neon Dreams"
        </h1>
        <p className="text-xl text-purple-200 text-center mb-8">
          Pre-order Now | Release Date: 2025.03.15
        </p>
        <div className="text-center mb-12">
          <span className="inline-block bg-yellow-400 text-purple-900 font-bold px-6 py-2 rounded-full text-lg">
            ๐Ÿ”ฅ {preorderCount} albums pre-ordered!
          </span>
        </div>

        {/* Album Cover & Artist Info */}
        <div className="max-w-4xl mx-auto mb-16">
          <div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 shadow-2xl">
            <div className="grid md:grid-cols-2 gap-8 items-center">
              <div>
                <div className="aspect-square bg-gradient-to-br from-pink-500 to-purple-600 rounded-xl shadow-lg flex items-center justify-center">
                  <span className="text-white text-6xl font-bold">NE</span>
                </div>
              </div>
              <div>
                <h2 className="text-3xl font-bold text-white mb-4">About the Album</h2>
                <p className="text-purple-100 mb-4">
                  "Neon Dreams" is Midnight Echo's sophomore EP, featuring 5 tracks of synth-driven indie rock. Recorded in Seoul, this album captures the energy of late-night city drives and neon-lit streets.
                </p>
                <p className="text-purple-200 text-sm">
                  Producer: Kim Jae-hyun | Label: Echo Records | Genre: Indie Rock, Synth Pop
                </p>
              </div>
            </div>
          </div>
        </div>

        {/* Tracklist */}
        <div className="max-w-4xl mx-auto mb-16">
          <h2 className="text-3xl font-bold text-white mb-6 text-center">๐Ÿ“€ Tracklist</h2>
          <div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl">
            <table className="w-full text-white">
              <thead>
                <tr className="border-b border-purple-400/30">
                  <th className="text-left py-3 px-4">No.</th>
                  <th className="text-left py-3 px-4">Title</th>
                  <th className="text-right py-3 px-4">Duration</th>
                </tr>
              </thead>
              <tbody>
                {tracks.map(track => (
                  <tr key={track.no} className="border-b border-purple-400/10 hover:bg-white/5">
                    <td className="py-3 px-4">{track.no}</td>
                    <td className="py-3 px-4 font-medium">{track.title}</td>
                    <td className="py-3 px-4 text-right text-purple-300">{track.duration}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        {/* Package Options */}
        <div className="max-w-6xl mx-auto mb-16">
          <h2 className="text-3xl font-bold text-white mb-6 text-center">๐ŸŽ Package Options</h2>
          <div className="grid md:grid-cols-3 gap-6">
            {packages.map(pkg => (
              <div key={pkg.type} className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl hover:scale-105 transition relative">
                {pkg.badge && (
                  <span className="absolute top-4 right-4 bg-yellow-400 text-purple-900 text-xs font-bold px-3 py-1 rounded-full">
                    {pkg.badge}
                  </span>
                )}
                <h3 className="text-2xl font-bold text-white mb-2">{pkg.name}</h3>
                <p className="text-3xl font-bold text-yellow-400 mb-4">โ‚ฉ{pkg.price.toLocaleString()}</p>
                <ul className="text-purple-100 space-y-2">
                  {pkg.items.map((item, i) => (
                    <li key={i} className="flex items-start">
                      <span className="mr-2">โœ“</span>
                      <span>{item}</span>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        </div>

        {/* Pre-order Form */}
        <div className="max-w-2xl mx-auto mb-16">
          <h2 className="text-3xl font-bold text-white mb-6 text-center">๐Ÿ›’ Pre-order Form</h2>
          <div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 shadow-2xl">
            <form onSubmit={handleSubmit} className="space-y-6">
              <div>
                <label className="block text-white font-medium mb-2">Your Name</label>
                <input
                  type="text"
                  required
                  value={formData.customer_name}
                  onChange={(e) => setFormData({...formData, customer_name: e.target.value})}
                  className="w-full px-4 py-3 rounded-lg bg-white/20 text-white placeholder-purple-300 focus:outline-none focus:ring-2 focus:ring-yellow-400"
                  placeholder="Kim Min-soo"
                />
              </div>
              <div>
                <label className="block text-white font-medium mb-2">Email</label>
                <input
                  type="email"
                  required
                  value={formData.email}
                  onChange={(e) => setFormData({...formData, email: e.target.value})}
                  className="w-full px-4 py-3 rounded-lg bg-white/20 text-white placeholder-purple-300 focus:outline-none focus:ring-2 focus:ring-yellow-400"
                  placeholder="your@email.com"
                />
              </div>
              <div>
                <label className="block text-white font-medium mb-2">Package</label>
                <select
                  value={formData.package_type}
                  onChange={(e) => setFormData({...formData, package_type: e.target.value})}
                  className="w-full px-4 py-3 rounded-lg bg-white/20 text-white focus:outline-none focus:ring-2 focus:ring-yellow-400"
                >
                  <option value="standard" className="bg-purple-900">Standard Edition - โ‚ฉ15,000</option>
                  <option value="limited" className="bg-purple-900">Limited Edition - โ‚ฉ25,000</option>
                  <option value="deluxe" className="bg-purple-900">Deluxe Box Set - โ‚ฉ45,000</option>
                </select>
              </div>
              <div>
                <label className="block text-white font-medium mb-2">Quantity</label>
                <input
                  type="number"
                  min="1"
                  max="10"
                  required
                  value={formData.quantity}
                  onChange={(e) => setFormData({...formData, quantity: parseInt(e.target.value)})}
                  className="w-full px-4 py-3 rounded-lg bg-white/20 text-white focus:outline-none focus:ring-2 focus:ring-yellow-400"
                />
              </div>
              <button
                type="submit"
                disabled={isSubmitting}
                className="w-full bg-yellow-400 text-purple-900 font-bold py-4 rounded-lg hover:bg-yellow-300 transition disabled:opacity-50 disabled:cursor-not-allowed"
              >
                {isSubmitting ? 'Processing...' : '๐ŸŽ‰ Confirm Pre-order'}
              </button>
              {submitMessage && (
                <p className={`text-center font-medium ${submitMessage.startsWith('โœ…') ? 'text-green-300' : 'text-red-300'}`}>
                  {submitMessage}
                </p>
              )}
            </form>
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
```

ํผ์„ ์ œ์ถœํ•˜๋ฉด Laravel API๋กœ POST ์š”์ฒญ์ด ์ „์†ก๋˜๊ณ , ์„ฑ๊ณต ์‹œ ์˜ˆ์•ฝ ์ˆ˜๋Ÿ‰์ด ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ…Œ์ŠคํŠธํ•ด๋ณด์„ธ์š”!

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ”„๋ฆฌ์˜ค๋” ํผ์ด ์ถ”๊ฐ€๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฆ„, ์ด๋ฉ”์ผ, ํŒจํ‚ค์ง€ ํƒ€์ž…, ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•˜๊ณ  ์ œ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ œ์ถœ ์‹œ Laravel API๋กœ POST ์š”์ฒญ์ด ์ „์†ก๋˜๊ณ , ์„ฑ๊ณต ์‹œ 'โœ… Pre-order confirmed!' ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋ฉฐ ํŽ˜์ด์ง€ ์ƒ๋‹จ์˜ ์˜ˆ์•ฝ ์ˆ˜๋Ÿ‰์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋„ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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