๐ŸŽจ ํฌ๋ฆฌ์—์ดํ„ฐ ์ž‘ํ’ˆ ํŒ๋งค ์ƒต - ๋””์ง€ํ„ธ ์•„ํŠธ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ž…๋ฌธ PHP + JavaScript Laravel + React AI: Claude

๐ŸŽจ ํฌ๋ฆฌ์—์ดํ„ฐ ์ž‘ํ’ˆ ํŒ๋งค ์ƒต - ๋””์ง€ํ„ธ ์•„ํŠธ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… digital-art-marketplace-laravel
์กฐํšŒ์ˆ˜ 109 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

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

์ผ๋Ÿฌ์ŠคํŠธ๋ ˆ์ดํ„ฐ, ๋””์ž์ด๋„ˆ, ์‚ฌ์ง„์ž‘๊ฐ€ ๋“ฑ ํฌ๋ฆฌ์—์ดํ„ฐ๊ฐ€ ์ž์‹ ์˜ ๋””์ง€ํ„ธ ์ž‘ํ’ˆ์„ ํŒ๋งคํ•  ์ˆ˜ ์žˆ๋Š” ๋ฏธ๋‹ˆ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. Laravel API๋กœ ์ž‘ํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , React๋กœ ์‹œ๊ฐ์ ์œผ๋กœ ๋งค๋ ฅ์ ์ธ ๊ฐค๋Ÿฌ๋ฆฌ ํ˜•ํƒœ์˜ ์‡ผํ•‘ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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

  • ์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ: ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ์ž‘ํ’ˆ ์ธ๋„ค์ผ ํ‘œ์‹œ (์ด๋ฏธ์ง€, ์ œ๋ชฉ, ๊ฐ€๊ฒฉ)
  • ์ž‘ํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€: ํฐ ์ด๋ฏธ์ง€, ์ƒ์„ธ ์„ค๋ช…, ํฌ๋ฆฌ์—์ดํ„ฐ ์ •๋ณด, ๊ตฌ๋งค ๋ฒ„ํŠผ
  • ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ธฐ๋Šฅ: React useState๋กœ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ด€๋ฆฌ, ์ด ๊ธˆ์•ก ๊ณ„์‚ฐ
  • ๊ฐ„๋‹จํ•œ ๊ฒฐ์ œ ํผ: ๊ตฌ๋งค์ž ์ •๋ณด ์ž…๋ ฅ ํผ (์ด๋ฆ„, ์ด๋ฉ”์ผ, ๊ฒฐ์ œ ๋ฐฉ๋ฒ• ์„ ํƒ)
  • ์ฃผ๋ฌธ ์™„๋ฃŒ ํŽ˜์ด์ง€: ์ฃผ๋ฌธ ๋ฒˆํ˜ธ์™€ ๊ฐ์‚ฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
  • ๋ฐ˜์‘ํ˜• ๋””์ž์ธ: ๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ/๋ฐ์Šคํฌํ†ฑ ๋ชจ๋‘ ์ง€์›

๐ŸŽ ๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ

  • ์ž‘ํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ๋ง (์ผ๋Ÿฌ์ŠคํŠธ, ์‚ฌ์ง„, 3D ์•„ํŠธ ๋“ฑ)
  • ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ํ•„ํ„ฐ ์Šฌ๋ผ์ด๋”
  • ์ž‘ํ’ˆ ์ข‹์•„์š”/์œ„์‹œ๋ฆฌ์ŠคํŠธ ๊ธฐ๋Šฅ
  • ํฌ๋ฆฌ์—์ดํ„ฐ ํ”„๋กœํ•„ ํŽ˜์ด์ง€
  • ์ตœ๊ทผ ๋ณธ ์ž‘ํ’ˆ ํžˆ์Šคํ† ๋ฆฌ (localStorage)
  • ์ž‘ํ’ˆ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ
  • ์ธ๊ธฐ ์ž‘ํ’ˆ / ์‹ ์ž‘ ์ •๋ ฌ ์˜ต์…˜

๐ŸŽจ ๋””์ž์ธ ํฌ์ธํŠธ

  • Tailwind CSS๋กœ ์„ธ๋ จ๋œ ์นด๋“œ ๋””์ž์ธ
  • Hover ์‹œ ์ž‘ํ’ˆ ํ™•๋Œ€/๊ทธ๋ฆผ์ž ํšจ๊ณผ
  • ๋ถ€๋“œ๋Ÿฌ์šด ํŽ˜์ด์ง€ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜
  • ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์•„์ด์ฝ˜์— ์•„์ดํ…œ ๊ฐœ์ˆ˜ ๋ฑƒ์ง€
  • ๊ตฌ๋งค ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x ๋ฐฑ์—”๋“œ API + React 19 ํ”„๋ก ํŠธ์—”๋“œ(Vite) ๊ตฌ์„ฑ
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ
  • โ€ข Tailwind CSS๋กœ ์Šคํƒ€์ผ๋ง
  • โ€ข fetch ๋˜๋Š” axios๋กœ API ํ†ต์‹ 
  • โ€ข React Router DOM์œผ๋กœ ํŽ˜์ด์ง€ ๋ผ์šฐํŒ…
  • โ€ข useState/useEffect๋งŒ ์‚ฌ์šฉ (Redux ๋“ฑ ๋ณต์žกํ•œ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธˆ์ง€)
  • โ€ข ์‹ค์ œ ๊ฒฐ์ œ ์—ฐ๋™ ์—†์ด ํผ ์ œ์ถœ๋งŒ ๊ตฌํ˜„
  • โ€ข ์ด๋ฏธ์ง€๋Š” public ํด๋”์˜ ์ƒ˜ํ”Œ ์ด๋ฏธ์ง€ ๋˜๋Š” ์™ธ๋ถ€ URL ์‚ฌ์šฉ

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

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

1
Step 1
mkdir digital-art-marketplace-laravel && cd digital-art-marketplace-laravel

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

**Laravel 13.x API ๋ฐฑ์—”๋“œ ์„ค์ •:**

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

2. .env ํŒŒ์ผ ์ˆ˜์ •:
```
DB_CONNECTION=sqlite
# DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD ์ฃผ์„ ์ฒ˜๋ฆฌ
```

3. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ ์ƒ์„ฑ:
```bash
touch database/database.sqlite
```

4. CORS ์„ค์ • (config/cors.php):
```php
'paths' => ['api/*'],
'allowed_origins' => ['http://localhost:5173'],
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
```

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

6. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ž‘์„ฑ (database/migrations/xxxx_create_artworks_table.php):
```php
public function up()
{
    Schema::create('artworks', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description');
        $table->string('category'); // illustration, photo, 3d-art, digital-painting
        $table->decimal('price', 10, 2);
        $table->string('image_url');
        $table->string('creator_name');
        $table->string('creator_avatar')->nullable();
        $table->integer('likes')->default(0);
        $table->timestamps();
    });
}
```

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

8. Seeder ์ƒ์„ฑ ๋ฐ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€:
```bash
php artisan make:seeder ArtworkSeeder
```

database/seeders/ArtworkSeeder.php:
```php
public function run()
{
    $artworks = [
        [
            'title' => 'Sunset Dreams',
            'description' => '๋”ฐ๋œปํ•œ ์ƒ‰๊ฐ์˜ ์ผ๋Ÿฌ์ŠคํŠธ๋ ˆ์ด์…˜',
            'category' => 'illustration',
            'price' => 29.99,
            'image_url' => 'https://picsum.photos/seed/art1/800/600',
            'creator_name' => 'Luna Park',
            'creator_avatar' => 'https://i.pravatar.cc/150?img=1',
            'likes' => 124,
        ],
        [
            'title' => 'Urban Geometry',
            'description' => '๋„์‹œ์˜ ๊ธฐํ•˜ํ•™์  ํŒจํ„ด',
            'category' => 'photo',
            'price' => 49.99,
            'image_url' => 'https://picsum.photos/seed/art2/800/600',
            'creator_name' => 'Alex Kim',
            'creator_avatar' => 'https://i.pravatar.cc/150?img=2',
            'likes' => 89,
        ],
        [
            'title' => 'Future City',
            'description' => '๋ฏธ๋ž˜ ๋„์‹œ 3D ๋ Œ๋”๋ง',
            'category' => '3d-art',
            'price' => 79.99,
            'image_url' => 'https://picsum.photos/seed/art3/800/600',
            'creator_name' => 'Chris Lee',
            'creator_avatar' => 'https://i.pravatar.cc/150?img=3',
            'likes' => 203,
        ],
        [
            'title' => 'Ocean Waves',
            'description' => 'ํŒŒ๋„์˜ ์—ญ๋™์„ฑ์„ ๋‹ด์€ ๋””์ง€ํ„ธ ํŽ˜์ธํŒ…',
            'category' => 'digital-painting',
            'price' => 39.99,
            'image_url' => 'https://picsum.photos/seed/art4/800/600',
            'creator_name' => 'Emma Stone',
            'creator_avatar' => 'https://i.pravatar.cc/150?img=4',
            'likes' => 156,
        ],
        [
            'title' => 'Neon Nights',
            'description' => '๋„ค์˜จ ์‚ฌ์ธ์ด ๋น›๋‚˜๋Š” ๋ฐค๊ฑฐ๋ฆฌ',
            'category' => 'illustration',
            'price' => 34.99,
            'image_url' => 'https://picsum.photos/seed/art5/800/600',
            'creator_name' => 'Luna Park',
            'creator_avatar' => 'https://i.pravatar.cc/150?img=1',
            'likes' => 178,
        ],
        [
            'title' => 'Mountain Peak',
            'description' => '์žฅ์—„ํ•œ ์‚ฐ์˜ ์ •์ƒ',
            'category' => 'photo',
            'price' => 44.99,
            'image_url' => 'https://picsum.photos/seed/art6/800/600',
            'creator_name' => 'Alex Kim',
            'creator_avatar' => 'https://i.pravatar.cc/150?img=2',
            'likes' => 92,
        ],
    ];

    foreach ($artworks as $artwork) {
        \App\Models\Artwork::create($artwork);
    }
}
```

9. Seeder ์‹คํ–‰:
```bash
php artisan db:seed --class=ArtworkSeeder
```

10. API ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ:
```bash
php artisan make:controller Api/ArtworkController
```

app/Http/Controllers/Api/ArtworkController.php:
```php
namespace App\Http\Controllers\Api;

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

class ArtworkController extends Controller
{
    public function index()
    {
        $artworks = Artwork::orderBy('created_at', 'desc')->get();
        return response()->json($artworks);
    }

    public function show($id)
    {
        $artwork = Artwork::findOrFail($id);
        return response()->json($artwork);
    }
}
```

11. API ๋ผ์šฐํŠธ ์„ค์ • (routes/api.php):
```php
use App\Http\Controllers\Api\ArtworkController;

Route::get('/artworks', [ArtworkController::class, 'index']);
Route::get('/artworks/{id}', [ArtworkController::class, 'show']);
```

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

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8000/api/artworks ์ ‘์†ํ•˜์—ฌ JSON ๋ฐ์ดํ„ฐ ํ™•์ธ.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x API ์„œ๋ฒ„๊ฐ€ http://localhost:8000์—์„œ ์‹คํ–‰๋˜๊ณ , /api/artworks ์—”๋“œํฌ์ธํŠธ์—์„œ ์ž‘ํ’ˆ ๋ชฉ๋ก JSON ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— 6๊ฐœ์˜ ์ƒ˜ํ”Œ ์ž‘ํ’ˆ์ด ์ €์žฅ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

2
Step 2
**React 19 ํ”„๋ก ํŠธ์—”๋“œ ์„ค์ •:**

1. ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ๋กœ ๋Œ์•„๊ฐ€์„œ React + Vite ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ:
```bash
cd ..
npm create vite@latest frontend -- --template react
cd frontend
npm install
```

2. Tailwind CSS ์„ค์น˜:
```bash
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. React Router DOM ์„ค์น˜:
```bash
npm install react-router-dom
```

6. src/App.jsx๋ฅผ ๋ผ์šฐํ„ฐ๋กœ ๊ตฌ์„ฑ:
```jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import HomePage from './pages/HomePage'
import ArtworkDetailPage from './pages/ArtworkDetailPage'
import CartPage from './pages/CartPage'
import CheckoutPage from './pages/CheckoutPage'
import ThankYouPage from './pages/ThankYouPage'
import Header from './components/Header'
import { CartProvider } from './context/CartContext'

function App() {
  return (
    <CartProvider>
      <BrowserRouter>
        <div className="min-h-screen bg-gray-50">
          <Header />
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/artwork/:id" element={<ArtworkDetailPage />} />
            <Route path="/cart" element={<CartPage />} />
            <Route path="/checkout" element={<CheckoutPage />} />
            <Route path="/thank-you" element={<ThankYouPage />} />
          </Routes>
        </div>
      </BrowserRouter>
    </CartProvider>
  )
}

export default App
```

7. ์žฅ๋ฐ”๊ตฌ๋‹ˆ Context ์ƒ์„ฑ (src/context/CartContext.jsx):
```jsx
import { createContext, useState, useContext } from 'react'

const CartContext = createContext()

export function CartProvider({ children }) {
  const [cartItems, setCartItems] = useState([])

  const addToCart = (artwork) => {
    setCartItems(prev => {
      const existing = prev.find(item => item.id === artwork.id)
      if (existing) {
        return prev.map(item => 
          item.id === artwork.id 
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      }
      return [...prev, { ...artwork, quantity: 1 }]
    })
  }

  const removeFromCart = (id) => {
    setCartItems(prev => prev.filter(item => item.id !== id))
  }

  const updateQuantity = (id, quantity) => {
    if (quantity === 0) {
      removeFromCart(id)
    } else {
      setCartItems(prev => 
        prev.map(item => 
          item.id === id ? { ...item, quantity } : item
        )
      )
    }
  }

  const getTotalPrice = () => {
    return cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  }

  const getTotalItems = () => {
    return cartItems.reduce((sum, item) => sum + item.quantity, 0)
  }

  const clearCart = () => {
    setCartItems([])
  }

  return (
    <CartContext.Provider value={{
      cartItems,
      addToCart,
      removeFromCart,
      updateQuantity,
      getTotalPrice,
      getTotalItems,
      clearCart
    }}>
      {children}
    </CartContext.Provider>
  )
}

export function useCart() {
  return useContext(CartContext)
}
```

8. Header ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ (src/components/Header.jsx):
```jsx
import { Link } from 'react-router-dom'
import { useCart } from '../context/CartContext'

function Header() {
  const { getTotalItems } = useCart()
  const totalItems = getTotalItems()

  return (
    <header className="bg-white shadow-sm sticky top-0 z-50">
      <div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
        <Link to="/" className="text-2xl font-bold text-indigo-600">
          ๐ŸŽจ ArtMarket
        </Link>
        <Link to="/cart" className="relative">
          <svg className="w-8 h-8 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
          </svg>
          {totalItems > 0 && (
            <span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center">
              {totalItems}
            </span>
          )}
        </Link>
      </div>
    </header>
  )
}

export default Header
```

9. ํŽ˜์ด์ง€ ํด๋” ์ƒ์„ฑ ๋ฐ ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ ์ƒ์„ฑ:
```bash
mkdir -p src/pages src/components src/context
touch src/pages/HomePage.jsx src/pages/ArtworkDetailPage.jsx src/pages/CartPage.jsx src/pages/CheckoutPage.jsx src/pages/ThankYouPage.jsx
```

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

http://localhost:5173 ์ ‘์†ํ•˜์—ฌ Header๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: React ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ http://localhost:5173์—์„œ ์‹คํ–‰๋˜๊ณ , Tailwind CSS๊ฐ€ ์ ์šฉ๋œ Header์™€ ๋ผ์šฐํ„ฐ ๊ตฌ์กฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. CartContext๋ฅผ ํ†ตํ•ด ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

3
Step 3
**ํ™ˆํŽ˜์ด์ง€ ์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ ๊ตฌํ˜„:**

src/pages/HomePage.jsx:
```jsx
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'

function HomePage() {
  const [artworks, setArtworks] = useState([])
  const [loading, setLoading] = useState(true)
  const [selectedCategory, setSelectedCategory] = useState('all')

  useEffect(() => {
    fetch('http://localhost:8000/api/artworks')
      .then(res => res.json())
      .then(data => {
        setArtworks(data)
        setLoading(false)
      })
      .catch(err => {
        console.error('Error fetching artworks:', err)
        setLoading(false)
      })
  }, [])

  const categories = [
    { value: 'all', label: '์ „์ฒด' },
    { value: 'illustration', label: '์ผ๋Ÿฌ์ŠคํŠธ' },
    { value: 'photo', label: '์‚ฌ์ง„' },
    { value: '3d-art', label: '3D ์•„ํŠธ' },
    { value: 'digital-painting', label: '๋””์ง€ํ„ธ ํŽ˜์ธํŒ…' },
  ]

  const filteredArtworks = selectedCategory === 'all'
    ? artworks
    : artworks.filter(art => art.category === selectedCategory)

  if (loading) {
    return (
      <div className="flex justify-center items-center min-h-screen">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
      </div>
    )
  }

  return (
    <div className="max-w-7xl mx-auto px-4 py-8">
      <div className="text-center mb-12">
        <h1 className="text-4xl font-bold text-gray-900 mb-4">๋””์ง€ํ„ธ ์•„ํŠธ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค</h1>
        <p className="text-gray-600 text-lg">ํฌ๋ฆฌ์—์ดํ„ฐ์˜ ๋ฉ‹์ง„ ์ž‘ํ’ˆ์„ ๋งŒ๋‚˜๋ณด์„ธ์š”</p>
      </div>

      {/* ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ */}
      <div className="flex justify-center gap-4 mb-8 flex-wrap">
        {categories.map(cat => (
          <button
            key={cat.value}
            onClick={() => setSelectedCategory(cat.value)}
            className={`px-6 py-2 rounded-full font-medium transition-all ${
              selectedCategory === cat.value
                ? 'bg-indigo-600 text-white shadow-lg'
                : 'bg-white text-gray-700 hover:bg-gray-100'
            }`}
          >
            {cat.label}
          </button>
        ))}
      </div>

      {/* ์ž‘ํ’ˆ ๊ทธ๋ฆฌ๋“œ */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
        {filteredArtworks.map(artwork => (
          <Link
            key={artwork.id}
            to={`/artwork/${artwork.id}`}
            className="group bg-white rounded-xl shadow-md overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2"
          >
            <div className="aspect-[4/3] overflow-hidden">
              <img
                src={artwork.image_url}
                alt={artwork.title}
                className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
              />
            </div>
            <div className="p-6">
              <h3 className="text-xl font-bold text-gray-900 mb-2">{artwork.title}</h3>
              <div className="flex items-center gap-2 mb-3">
                <img
                  src={artwork.creator_avatar}
                  alt={artwork.creator_name}
                  className="w-8 h-8 rounded-full"
                />
                <span className="text-sm text-gray-600">{artwork.creator_name}</span>
              </div>
              <div className="flex justify-between items-center">
                <span className="text-2xl font-bold text-indigo-600">${artwork.price}</span>
                <div className="flex items-center gap-1 text-gray-500">
                  <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                    <path d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" />
                  </svg>
                  <span className="text-sm">{artwork.likes}</span>
                </div>
              </div>
            </div>
          </Link>
        ))}
      </div>

      {filteredArtworks.length === 0 && (
        <div className="text-center py-12">
          <p className="text-gray-500 text-lg">ํ•ด๋‹น ์นดํ…Œ๊ณ ๋ฆฌ์— ์ž‘ํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
        </div>
      )}
    </div>
  )
}

export default HomePage
```

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5173 ์ ‘์†ํ•˜์—ฌ ์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ๊ฐ€ ํ‘œ์‹œ๋˜๊ณ , ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ๊ฐ€ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. ์นด๋“œ์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ํ™•๋Œ€ ํšจ๊ณผ๊ฐ€ ๋‚˜ํƒ€๋‚˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ™ˆํŽ˜์ด์ง€์— ์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ๊ฐ€ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ํ‘œ์‹œ๋˜๊ณ , ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ๋ฒ„ํŠผ์œผ๋กœ ์ž‘ํ’ˆ์„ ํ•„ํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์นด๋“œ๋Š” hover ์‹œ ํ™•๋Œ€๋˜๊ณ  ๊ทธ๋ฆผ์ž๊ฐ€ ์ง„ํ•ด์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

4
Step 4
**์ž‘ํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ ๋ฐ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„:**

src/pages/ArtworkDetailPage.jsx:
```jsx
import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useCart } from '../context/CartContext'

function ArtworkDetailPage() {
  const { id } = useParams()
  const navigate = useNavigate()
  const { addToCart } = useCart()
  const [artwork, setArtwork] = useState(null)
  const [loading, setLoading] = useState(true)
  const [addedToCart, setAddedToCart] = useState(false)

  useEffect(() => {
    fetch(`http://localhost:8000/api/artworks/${id}`)
      .then(res => res.json())
      .then(data => {
        setArtwork(data)
        setLoading(false)
      })
      .catch(err => {
        console.error('Error fetching artwork:', err)
        setLoading(false)
      })
  }, [id])

  const handleAddToCart = () => {
    addToCart(artwork)
    setAddedToCart(true)
    setTimeout(() => setAddedToCart(false), 2000)
  }

  if (loading) {
    return (
      <div className="flex justify-center items-center min-h-screen">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
      </div>
    )
  }

  if (!artwork) {
    return (
      <div className="max-w-7xl mx-auto px-4 py-12 text-center">
        <p className="text-gray-500 text-lg">์ž‘ํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</p>
      </div>
    )
  }

  const categoryLabels = {
    'illustration': '์ผ๋Ÿฌ์ŠคํŠธ',
    'photo': '์‚ฌ์ง„',
    '3d-art': '3D ์•„ํŠธ',
    'digital-painting': '๋””์ง€ํ„ธ ํŽ˜์ธํŒ…'
  }

  return (
    <div className="max-w-7xl mx-auto px-4 py-12">
      <button
        onClick={() => navigate(-1)}
        className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-8"
      >
        <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
        </svg>
        ๋’ค๋กœ ๊ฐ€๊ธฐ
      </button>

      <div className="grid md:grid-cols-2 gap-12">
        {/* ์ด๋ฏธ์ง€ */}
        <div className="bg-white rounded-2xl shadow-lg overflow-hidden">
          <img
            src={artwork.image_url}
            alt={artwork.title}
            className="w-full h-full object-cover"
          />
        </div>

        {/* ์ •๋ณด */}
        <div>
          <span className="inline-block px-4 py-1 bg-indigo-100 text-indigo-700 rounded-full text-sm font-medium mb-4">
            {categoryLabels[artwork.category]}
          </span>
          <h1 className="text-4xl font-bold text-gray-900 mb-4">{artwork.title}</h1>
          
          <div className="flex items-center gap-3 mb-6">
            <img
              src={artwork.creator_avatar}
              alt={artwork.creator_name}
              className="w-12 h-12 rounded-full"
            />
            <div>
              <p className="font-medium text-gray-900">{artwork.creator_name}</p>
              <p className="text-sm text-gray-500">ํฌ๋ฆฌ์—์ดํ„ฐ</p>
            </div>
          </div>

          <div className="flex items-center gap-3 mb-6">
            <svg className="w-6 h-6 text-red-500" fill="currentColor" viewBox="0 0 20 20">
              <path d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" />
            </svg>
            <span className="text-gray-600">{artwork.likes}๋ช…์ด ์ข‹์•„ํ•ฉ๋‹ˆ๋‹ค</span>
          </div>

          <p className="text-gray-700 text-lg leading-relaxed mb-8">{artwork.description}</p>

          <div className="border-t border-gray-200 pt-8">
            <div className="flex items-end justify-between mb-6">
              <div>
                <p className="text-gray-500 text-sm mb-1">๊ฐ€๊ฒฉ</p>
                <p className="text-4xl font-bold text-indigo-600">${artwork.price}</p>
              </div>
            </div>

            <button
              onClick={handleAddToCart}
              disabled={addedToCart}
              className={`w-full py-4 rounded-xl font-bold text-lg transition-all ${
                addedToCart
                  ? 'bg-green-500 text-white'
                  : 'bg-indigo-600 text-white hover:bg-indigo-700 active:scale-95'
              }`}
            >
              {addedToCart ? (
                <span className="flex items-center justify-center gap-2">
                  <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
                  </svg>
                  ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€๋จ!
                </span>
              ) : (
                '์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธฐ'
              )}
            </button>

            <button
              onClick={() => navigate('/cart')}
              className="w-full mt-3 py-4 border-2 border-indigo-600 text-indigo-600 rounded-xl font-bold text-lg hover:bg-indigo-50 transition-all"
            >
              ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ณด๊ธฐ
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ArtworkDetailPage
```

์ž‘ํ’ˆ ์นด๋“œ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ณ , '์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธฐ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€๋˜๊ณ  ๋ฒ„ํŠผ์ด ์ดˆ๋ก์ƒ‰์œผ๋กœ ๋ณ€ํ•˜๋ฉฐ ์ฒดํฌ ์•„์ด์ฝ˜์ด ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ž‘ํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ํฐ ์ด๋ฏธ์ง€, ์ œ๋ชฉ, ์„ค๋ช…, ํฌ๋ฆฌ์—์ดํ„ฐ ์ •๋ณด, ๊ฐ€๊ฒฉ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. '์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธฐ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ž‘ํ’ˆ์ด ์ถ”๊ฐ€๋˜๊ณ , ๋ฒ„ํŠผ์ด 2์ดˆ๊ฐ„ ์ดˆ๋ก์ƒ‰์œผ๋กœ ๋ณ€ํ•˜๋ฉฐ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Header์˜ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์•„์ด์ฝ˜์— ๋ฑƒ์ง€ ์ˆซ์ž๊ฐ€ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

5
Step 5
**์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€ ๋ฐ ๊ฒฐ์ œ ํ๋ฆ„ ์™„์„ฑ:**

1. src/pages/CartPage.jsx:
```jsx
import { useNavigate } from 'react-router-dom'
import { useCart } from '../context/CartContext'

function CartPage() {
  const navigate = useNavigate()
  const { cartItems, removeFromCart, updateQuantity, getTotalPrice } = useCart()

  if (cartItems.length === 0) {
    return (
      <div className="max-w-7xl mx-auto px-4 py-12 text-center">
        <svg className="w-24 h-24 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
        </svg>
        <h2 className="text-2xl font-bold text-gray-900 mb-4">์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค</h2>
        <p className="text-gray-600 mb-6">๋ฉ‹์ง„ ์ž‘ํ’ˆ์„ ๋‘˜๋Ÿฌ๋ณด๊ณ  ๋งˆ์Œ์— ๋“œ๋Š” ์ž‘ํ’ˆ์„ ๋‹ด์•„๋ณด์„ธ์š”!</p>
        <button
          onClick={() => navigate('/')}
          className="bg-indigo-600 text-white px-8 py-3 rounded-xl font-medium hover:bg-indigo-700 transition-all"
        >
          ์ž‘ํ’ˆ ๋‘˜๋Ÿฌ๋ณด๊ธฐ
        </button>
      </div>
    )
  }

  return (
    <div className="max-w-7xl mx-auto px-4 py-12">
      <h1 className="text-3xl font-bold text-gray-900 mb-8">์žฅ๋ฐ”๊ตฌ๋‹ˆ</h1>

      <div className="grid lg:grid-cols-3 gap-8">
        {/* ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์•„์ดํ…œ */}
        <div className="lg:col-span-2 space-y-4">
          {cartItems.map(item => (
            <div key={item.id} className="bg-white rounded-xl shadow-md p-6 flex gap-6">
              <img
                src={item.image_url}
                alt={item.title}
                className="w-32 h-32 object-cover rounded-lg"
              />
              <div className="flex-1">
                <h3 className="text-xl font-bold text-gray-900 mb-2">{item.title}</h3>
                <p className="text-gray-600 mb-2">{item.creator_name}</p>
                <p className="text-2xl font-bold text-indigo-600">${item.price}</p>
              </div>
              <div className="flex flex-col justify-between items-end">
                <button
                  onClick={() => removeFromCart(item.id)}
                  className="text-red-500 hover:text-red-700"
                >
                  <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                  </svg>
                </button>
                <div className="flex items-center gap-3">
                  <button
                    onClick={() => updateQuantity(item.id, item.quantity - 1)}
                    className="w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 flex items-center justify-center"
                  >
                    -
                  </button>
                  <span className="font-bold text-lg">{item.quantity}</span>
                  <button
                    onClick={() => updateQuantity(item.id, item.quantity + 1)}
                    className="w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 flex items-center justify-center"
                  >
                    +
                  </button>
                </div>
              </div>
            </div>
          ))}
        </div>

        {/* ์ฃผ๋ฌธ ์š”์•ฝ */}
        <div className="lg:col-span-1">
          <div className="bg-white rounded-xl shadow-md p-6 sticky top-24">
            <h2 className="text-xl font-bold text-gray-900 mb-4">์ฃผ๋ฌธ ์š”์•ฝ</h2>
            <div className="space-y-3 mb-6">
              <div className="flex justify-between text-gray-600">
                <span>์†Œ๊ณ„</span>
                <span>${getTotalPrice().toFixed(2)}</span>
              </div>
              <div className="flex justify-between text-gray-600">
                <span>๋ฐฐ์†ก๋น„</span>
                <span className="text-green-600">๋ฌด๋ฃŒ</span>
              </div>
              <div className="border-t pt-3 flex justify-between text-xl font-bold">
                <span>์ด ๊ธˆ์•ก</span>
                <span className="text-indigo-600">${getTotalPrice().toFixed(2)}</span>
              </div>
            </div>
            <button
              onClick={() => navigate('/checkout')}
              className="w-full bg-indigo-600 text-white py-4 rounded-xl font-bold text-lg hover:bg-indigo-700 active:scale-95 transition-all"
            >
              ๊ฒฐ์ œํ•˜๊ธฐ
            </button>
            <button
              onClick={() => navigate('/')}
              className="w-full mt-3 text-indigo-600 py-3 rounded-xl font-medium hover:bg-indigo-50 transition-all"
            >
              ์‡ผํ•‘ ๊ณ„์†ํ•˜๊ธฐ
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default CartPage
```

2. src/pages/CheckoutPage.jsx:
```jsx
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useCart } from '../context/CartContext'

function CheckoutPage() {
  const navigate = useNavigate()
  const { cartItems, getTotalPrice, clearCart } = useCart()
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: '',
    address: '',
    paymentMethod: 'credit-card'
  })
  const [loading, setLoading] = useState(false)

  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    })
  }

  const handleSubmit = (e) => {
    e.preventDefault()
    setLoading(true)
    
    // ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2์ดˆ ํ›„ ์™„๋ฃŒ)
    setTimeout(() => {
      const orderNumber = 'ORD-' + Date.now()
      clearCart()
      navigate('/thank-you', { state: { orderNumber, ...formData } })
    }, 2000)
  }

  if (cartItems.length === 0) {
    return (
      <div className="max-w-7xl mx-auto px-4 py-12 text-center">
        <p className="text-gray-600 mb-4">์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.</p>
        <button
          onClick={() => navigate('/')}
          className="bg-indigo-600 text-white px-8 py-3 rounded-xl font-medium hover:bg-indigo-700"
        >
          ์ž‘ํ’ˆ ๋‘˜๋Ÿฌ๋ณด๊ธฐ
        </button>
      </div>
    )
  }

  return (
    <div className="max-w-4xl mx-auto px-4 py-12">
      <h1 className="text-3xl font-bold text-gray-900 mb-8">๊ฒฐ์ œํ•˜๊ธฐ</h1>

      <div className="grid md:grid-cols-3 gap-8">
        <form onSubmit={handleSubmit} className="md:col-span-2 space-y-6">
          {/* ๊ตฌ๋งค์ž ์ •๋ณด */}
          <div className="bg-white rounded-xl shadow-md p-6">
            <h2 className="text-xl font-bold text-gray-900 mb-4">๊ตฌ๋งค์ž ์ •๋ณด</h2>
            <div className="space-y-4">
              <div>
                <label className="block text-gray-700 font-medium mb-2">์ด๋ฆ„</label>
                <input
                  type="text"
                  name="name"
                  value={formData.name}
                  onChange={handleChange}
                  required
                  className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
                />
              </div>
              <div>
                <label className="block text-gray-700 font-medium mb-2">์ด๋ฉ”์ผ</label>
                <input
                  type="email"
                  name="email"
                  value={formData.email}
                  onChange={handleChange}
                  required
                  className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
                />
              </div>
              <div>
                <label className="block text-gray-700 font-medium mb-2">์ „ํ™”๋ฒˆํ˜ธ</label>
                <input
                  type="tel"
                  name="phone"
                  value={formData.phone}
                  onChange={handleChange}
                  required
                  className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
                />
              </div>
              <div>
                <label className="block text-gray-700 font-medium mb-2">์ฃผ์†Œ</label>
                <textarea
                  name="address"
                  value={formData.address}
                  onChange={handleChange}
                  required
                  rows={3}
                  className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
                />
              </div>
            </div>
          </div>

          {/* ๊ฒฐ์ œ ๋ฐฉ๋ฒ• */}
          <div className="bg-white rounded-xl shadow-md p-6">
            <h2 className="text-xl font-bold text-gray-900 mb-4">๊ฒฐ์ œ ๋ฐฉ๋ฒ•</h2>
            <div className="space-y-3">
              {[
                { value: 'credit-card', label: '์‹ ์šฉ์นด๋“œ' },
                { value: 'paypal', label: 'PayPal' },
                { value: 'bank-transfer', label: '๊ณ„์ขŒ์ด์ฒด' }
              ].map(method => (
                <label key={method.value} className="flex items-center gap-3 cursor-pointer">
                  <input
                    type="radio"
                    name="paymentMethod"
                    value={method.value}
                    checked={formData.paymentMethod === method.value}
                    onChange={handleChange}
                    className="w-5 h-5 text-indigo-600"
                  />
                  <span className="text-gray-700">{method.label}</span>
                </label>
              ))}
            </div>
          </div>

          <button
            type="submit"
            disabled={loading}
            className="w-full bg-indigo-600 text-white py-4 rounded-xl font-bold text-lg hover:bg-indigo-700 active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? (
              <span className="flex items-center justify-center gap-2">
                <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
                ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ์ค‘...
              </span>
            ) : (
              `$${getTotalPrice().toFixed(2)} ๊ฒฐ์ œํ•˜๊ธฐ`
            )}
          </button>
        </form>

        {/* ์ฃผ๋ฌธ ์š”์•ฝ */}
        <div className="md:col-span-1">
          <div className="bg-white rounded-xl shadow-md p-6 sticky top-24">
            <h2 className="text-lg font-bold text-gray-900 mb-4">์ฃผ๋ฌธ ์š”์•ฝ</h2>
            <div className="space-y-3 mb-4">
              {cartItems.map(item => (
                <div key={item.id} className="flex justify-between text-sm">
                  <span className="text-gray-600">{item.title} x{item.quantity}</span>
                  <span className="font-medium">${(item.price * item.quantity).toFixed(2)}</span>
                </div>
              ))}
            </div>
            <div className="border-t pt-3 flex justify-between text-lg font-bold">
              <span>์ด ๊ธˆ์•ก</span>
              <span className="text-indigo-600">${getTotalPrice().toFixed(2)}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default CheckoutPage
```

3. src/pages/ThankYouPage.jsx:
```jsx
import { useLocation, useNavigate } from 'react-router-dom'
import { useEffect } from 'react'

function ThankYouPage() {
  const location = useLocation()
  const navigate = useNavigate()
  const orderData = location.state

  useEffect(() => {
    if (!orderData) {
      navigate('/')
    }
  }, [orderData, navigate])

  if (!orderData) return null

  return (
    <div className="max-w-2xl mx-auto px-4 py-12 text-center">
      <div className="bg-white rounded-2xl shadow-lg p-12">
        <div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
          <svg className="w-12 h-12 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
          </svg>
        </div>
        
        <h1 className="text-3xl font-bold text-gray-900 mb-4">์ฃผ๋ฌธ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!</h1>
        <p className="text-gray-600 text-lg mb-8">๊ตฌ๋งคํ•ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ž‘ํ’ˆ์„ ์ฆ๊ฒจ๋ณด์„ธ์š”!</p>
        
        <div className="bg-gray-50 rounded-xl p-6 mb-8">
          <p className="text-sm text-gray-500 mb-2">์ฃผ๋ฌธ ๋ฒˆํ˜ธ</p>
          <p className="text-2xl font-bold text-indigo-600 mb-6">{orderData.orderNumber}</p>
          
          <div className="text-left space-y-2">
            <div className="flex justify-between">
              <span className="text-gray-600">์ด๋ฆ„:</span>
              <span className="font-medium">{orderData.name}</span>
            </div>
            <div className="flex justify-between">
              <span className="text-gray-600">์ด๋ฉ”์ผ:</span>
              <span className="font-medium">{orderData.email}</span>
            </div>
          </div>
        </div>
        
        <p className="text-gray-600 mb-6">์ฃผ๋ฌธ ํ™•์ธ ์ด๋ฉ”์ผ์ด {orderData.email}๋กœ ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</p>
        
        <button
          onClick={() => navigate('/')}
          className="bg-indigo-600 text-white px-8 py-4 rounded-xl font-bold text-lg hover:bg-indigo-700 active:scale-95 transition-all"
        >
          ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
        </button>
      </div>
    </div>
  )
}

export default ThankYouPage
```

์žฅ๋ฐ”๊ตฌ๋‹ˆ์—์„œ '๊ฒฐ์ œํ•˜๊ธฐ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ณ , ๊ฒฐ์ œ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ ํ›„ ๊ฒฐ์ œ๋ฅผ ์™„๋ฃŒํ•˜๋ฉด ์ฃผ๋ฌธ ์™„๋ฃŒ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. ์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋น„์›Œ์ง€๊ณ  ์ฃผ๋ฌธ ๋ฒˆํ˜ธ๊ฐ€ ์ƒ์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€์—์„œ ์ˆ˜๋Ÿ‰ ์กฐ์ ˆ, ์•„์ดํ…œ ์‚ญ์ œ, ์ด ๊ธˆ์•ก ๊ณ„์‚ฐ์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ์ œ ํŽ˜์ด์ง€์—์„œ ๊ตฌ๋งค์ž ์ •๋ณด์™€ ๊ฒฐ์ œ ๋ฐฉ๋ฒ•์„ ์ž…๋ ฅํ•˜๊ณ  ์ œ์ถœํ•˜๋ฉด 2์ดˆ ํ›„ ์ฃผ๋ฌธ ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋ฉฐ, ์ฃผ๋ฌธ ๋ฒˆํ˜ธ์™€ ๊ฐ์‚ฌ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ „์ฒด ๊ตฌ๋งค ํ๋ฆ„์ด ์™„์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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