๐ŸŽจ ์›นํˆฐ ์ž‘๊ฐ€ ํฌํŠธํด๋ฆฌ์˜ค & ์ปค๋ฏธ์…˜ ์ ‘์ˆ˜ ํ”Œ๋žซํผ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ค‘๊ธ‰ PHP + JavaScript Laravel + React + Inertia.js AI: Claude

๐ŸŽจ ์›นํˆฐ ์ž‘๊ฐ€ ํฌํŠธํด๋ฆฌ์˜ค & ์ปค๋ฏธ์…˜ ์ ‘์ˆ˜ ํ”Œ๋žซํผ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… webtoon-portfolio-commission-platform
์กฐํšŒ์ˆ˜ 86 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐ŸŽจ ์›นํˆฐ ์ž‘๊ฐ€ ํฌํŠธํด๋ฆฌ์˜ค & ์ปค๋ฏธ์…˜ ์ ‘์ˆ˜ ํ”Œ๋žซํผ

์›นํˆฐ ์ž‘๊ฐ€, ์ผ๋Ÿฌ์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ ์ž์‹ ์˜ ์ž‘ํ’ˆ์„ ๊ฐค๋Ÿฌ๋ฆฌ ํ˜•ํƒœ๋กœ ์ „์‹œํ•˜๊ณ , ํŒฌ๋“ค์ด ์ปค๋ฏธ์…˜(์œ ๋ฃŒ ์˜๋ขฐ)์„ ์‹ ์ฒญํ•  ์ˆ˜ ์žˆ๋Š” ํ’€์Šคํƒ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. ์ž‘๊ฐ€๋Š” ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ์—…๋กœ๋“œํ•˜๊ณ , ์˜๋ขฐ์ธ์€ ์›ํ•˜๋Š” ์ž‘์—… ์œ ํ˜•(์บ๋ฆญํ„ฐ ๋””์ž์ธ, ์ผ๋Ÿฌ์ŠคํŠธ, ์›นํˆฐ ์ปท ๋“ฑ)์„ ์„ ํƒํ•˜์—ฌ ๊ฒฌ์ ์„ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

์„ ํƒ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ (๋ณด๋„ˆ์Šค)

  • ์ž‘ํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€์— ์Šฌ๋ผ์ด๋“œ์‡ผ ํšจ๊ณผ
  • ์ปค๋ฏธ์…˜ ๊ฐ€๊ฒฉํ‘œ ํŽ˜์ด์ง€ (์ž‘์—… ์œ ํ˜•๋ณ„ ๊ธฐ๋ณธ ์š”๊ธˆ ์•ˆ๋‚ด)
  • ์ž‘๊ฐ€ ํ”„๋กœํ•„ ํŽ˜์ด์ง€ (์†Œ๊ฐœ, SNS ๋งํฌ, ์ž‘์—… ์Šคํƒ€์ผ ์„ค๋ช…)
  • ์˜๋ขฐ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ์ด๋ฉ”์ผ ์•Œ๋ฆผ (Laravel Notification)
  • ์ž‘ํ’ˆ ์—…๋กœ๋“œ ์‹œ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ง• (Intervention Image)

๊ธฐ์ˆ  ์Šคํƒ

  • Laravel 13.x + Inertia.js + React 19
  • Tailwind CSS (๋ฐ˜์‘ํ˜• UI)
  • SQLite (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์šฉ)
  • Laravel Breeze (Inertia React starter kit)

์ด ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด ํŒŒ์ผ ์—…๋กœ๋“œ, ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„(์ž‘ํ’ˆ-ํƒœ๊ทธ), ์ƒํƒœ ๊ด€๋ฆฌ, Inertia.js ํผ ์ฒ˜๋ฆฌ ๋“ฑ์„ ํ•™์Šตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x + Inertia.js + React 19 ๋ชจ๋…ธ๋ฆฌ์Šค ๊ตฌ์กฐ (๋ณ„๋„ API ๋ผ์šฐํŠธ ์—†์Œ)
  • โ€ข Laravel Breeze (React + Inertia) starter kit์œผ๋กœ ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”
  • โ€ข Tailwind CSS๋กœ ๋ฐ˜์‘ํ˜• UI ๊ตฌํ˜„
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํฌํ•จ)
  • โ€ข ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๋Š” Laravel Storage ์‚ฌ์šฉ (public disk)
  • โ€ข Inertia::render()๋กœ Laravel ์ปจํŠธ๋กค๋Ÿฌ์—์„œ React ์ปดํฌ๋„ŒํŠธ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
  • โ€ข ๋ณต์žกํ•œ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ธˆ์ง€ (useState, useReducer๋งŒ ์‚ฌ์šฉ)
  • โ€ข ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ธฐ๋Šฅ ๊ธˆ์ง€ (์ปค๋ฏธ์…˜ ์˜๋ขฐ๋Š” ํผ ์ œ์ถœ ๋ฐฉ์‹)

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

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

1
Step 1
mkdir webtoon-portfolio-commission-platform && cd webtoon-portfolio-commission-platform

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

Laravel 13.x + Inertia.js + React 19 ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”. Laravel Breeze (React + Inertia) starter kit์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

```bash
npx create laravel webtoon-portfolio-commission-platform
cd webtoon-portfolio-commission-platform
composer require laravel/breeze --dev
php artisan breeze:install react
npm install
npm run dev
```

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” SQLite๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. `.env` ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์—ฌ:
```
DB_CONNECTION=sqlite
```

`database/database.sqlite` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•˜์„ธ์š”:
```bash
touch database/database.sqlite
php artisan migrate
```

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š” (`php artisan serve`).

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel Breeze (React + Inertia) ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์„ค์ •๋˜๋ฉฐ, ๊ธฐ๋ณธ ์ธ์ฆ ํŽ˜์ด์ง€(๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…)๊ฐ€ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. http://localhost:8000 ์ ‘์† ์‹œ Welcome ํŽ˜์ด์ง€๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค.

2
Step 2
์ž‘ํ’ˆ(Portfolio) ๋ชจ๋ธ๊ณผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•˜์„ธ์š”.

```bash
php artisan make:model Portfolio -m
```

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ(`database/migrations/xxxx_create_portfolios_table.php`)์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑ:
```php
public function up()
{
    Schema::create('portfolios', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
        $table->string('title');
        $table->text('description')->nullable();
        $table->string('image_path');
        $table->string('type'); // ์˜ˆ: 'character', 'background', 'webtoon'
        $table->integer('likes_count')->default(0);
        $table->timestamps();
    });
}
```

ํƒœ๊ทธ(Tag) ๋ชจ๋ธ๊ณผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ:
```bash
php artisan make:model Tag -m
```

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ:
```php
public function up()
{
    Schema::create('tags', function (Blueprint $table) {
        $table->id();
        $table->string('name')->unique();
        $table->timestamps();
    });
}
```

์ž‘ํ’ˆ-ํƒœ๊ทธ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ:
```bash
php artisan make:migration create_portfolio_tag_table
```

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ:
```php
public function up()
{
    Schema::create('portfolio_tag', function (Blueprint $table) {
        $table->foreignId('portfolio_id')->constrained()->onDelete('cascade');
        $table->foreignId('tag_id')->constrained()->onDelete('cascade');
        $table->primary(['portfolio_id', 'tag_id']);
    });
}
```

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

Portfolio ๋ชจ๋ธ์— ๊ด€๊ณ„ ์„ค์ •:
```php
class Portfolio extends Model
{
    protected $fillable = ['user_id', 'title', 'description', 'image_path', 'type', 'likes_count'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}
```

Tag ๋ชจ๋ธ:
```php
class Tag extends Model
{
    protected $fillable = ['name'];

    public function portfolios()
    {
        return $this->belongsToMany(Portfolio::class);
    }
}
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: portfolios, tags, portfolio_tag ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜๊ณ , Portfolio์™€ Tag ๋ชจ๋ธ์— ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„๊ฐ€ ์„ค์ •๋ฉ๋‹ˆ๋‹ค.

3
Step 3
์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

PortfolioController ์ƒ์„ฑ:
```bash
php artisan make:controller PortfolioController
```

`app/Http/Controllers/PortfolioController.php`:
```php
use App\Models\Portfolio;
use Inertia\Inertia;

class PortfolioController extends Controller
{
    public function index(Request $request)
    {
        $query = Portfolio::with(['user', 'tags']);

        if ($request->has('type') && $request->type !== 'all') {
            $query->where('type', $request->type);
        }

        if ($request->has('tag')) {
            $query->whereHas('tags', function($q) use ($request) {
                $q->where('name', $request->tag);
            });
        }

        $portfolios = $query->latest()->paginate(12);
        $tags = \App\Models\Tag::all();

        return Inertia::render('Portfolio/Index', [
            'portfolios' => $portfolios,
            'tags' => $tags,
            'filters' => $request->only(['type', 'tag'])
        ]);
    }
}
```

๋ผ์šฐํŠธ ์ถ”๊ฐ€ (`routes/web.php`):
```php
use App\Http\Controllers\PortfolioController;

Route::get('/portfolios', [PortfolioController::class, 'index'])->name('portfolios.index');
```

React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ (`resources/js/Pages/Portfolio/Index.jsx`):
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
import { useState } from 'react';

export default function Index({ portfolios, tags, filters }) {
    const [selectedType, setSelectedType] = useState(filters.type || 'all');
    const [selectedTag, setSelectedTag] = useState(filters.tag || '');

    return (
        <AuthenticatedLayout>
            <Head title="์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ" />
            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <h1 className="text-3xl font-bold mb-8">์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ</h1>

                    {/* ํ•„ํ„ฐ */}
                    <div className="mb-6 flex gap-4 flex-wrap">
                        <div>
                            <label className="block text-sm font-medium mb-2">์ž‘ํ’ˆ ์œ ํ˜•</label>
                            <select
                                value={selectedType}
                                onChange={(e) => window.location.href = `/portfolios?type=${e.target.value}`}
                                className="border rounded px-3 py-2"
                            >
                                <option value="all">์ „์ฒด</option>
                                <option value="character">์บ๋ฆญํ„ฐ</option>
                                <option value="background">๋ฐฐ๊ฒฝ</option>
                                <option value="webtoon">์›นํˆฐ ์ปท</option>
                            </select>
                        </div>
                        <div>
                            <label className="block text-sm font-medium mb-2">ํƒœ๊ทธ</label>
                            <select
                                value={selectedTag}
                                onChange={(e) => window.location.href = `/portfolios?tag=${e.target.value}`}
                                className="border rounded px-3 py-2"
                            >
                                <option value="">์ „์ฒด</option>
                                {tags.map(tag => (
                                    <option key={tag.id} value={tag.name}>{tag.name}</option>
                                ))}
                            </select>
                        </div>
                    </div>

                    {/* ๊ฐค๋Ÿฌ๋ฆฌ ๊ทธ๋ฆฌ๋“œ */}
                    <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
                        {portfolios.data.map(portfolio => (
                            <div key={portfolio.id} className="bg-white rounded-lg shadow hover:shadow-lg transition">
                                <img
                                    src={`/storage/${portfolio.image_path}`}
                                    alt={portfolio.title}
                                    className="w-full h-48 object-cover rounded-t-lg"
                                />
                                <div className="p-4">
                                    <h3 className="font-bold text-lg mb-2">{portfolio.title}</h3>
                                    <p className="text-sm text-gray-600 mb-2">{portfolio.description}</p>
                                    <div className="flex gap-2 flex-wrap mb-2">
                                        {portfolio.tags.map(tag => (
                                            <span key={tag.id} className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
                                                #{tag.name}
                                            </span>
                                        ))}
                                    </div>
                                    <div className="flex items-center justify-between">
                                        <span className="text-sm text-gray-500">by {portfolio.user.name}</span>
                                        <button className="text-red-500">โค๏ธ {portfolio.likes_count}</button>
                                    </div>
                                </div>
                            </div>
                        ))}
                    </div>

                    {/* ํŽ˜์ด์ง€๋„ค์ด์…˜ */}
                    <div className="mt-6 flex justify-center gap-2">
                        {portfolios.links.map((link, index) => (
                            <Link
                                key={index}
                                href={link.url}
                                className={`px-3 py-2 border rounded ${link.active ? 'bg-blue-500 text-white' : 'bg-white'}`}
                                dangerouslySetInnerHTML={{ __html: link.label }}
                            />
                        ))}
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด์— '์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ' ๋งํฌ ์ถ”๊ฐ€ (`resources/js/Layouts/AuthenticatedLayout.jsx`).

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง๋˜๊ณ , ์ž‘ํ’ˆ ์œ ํ˜• ๋ฐ ํƒœ๊ทธ๋ณ„ ํ•„ํ„ฐ๋ง์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

4
Step 4
์ž‘ํ’ˆ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”. (์ž‘๊ฐ€๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)

PortfolioController์— create, store ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€:
```php
public function create()
{
    $tags = \App\Models\Tag::all();
    return Inertia::render('Portfolio/Create', ['tags' => $tags]);
}

public function store(Request $request)
{
    $request->validate([
        'title' => 'required|max:255',
        'description' => 'nullable',
        'image' => 'required|image|max:2048',
        'type' => 'required|in:character,background,webtoon',
        'tags' => 'array'
    ]);

    $path = $request->file('image')->store('portfolios', 'public');

    $portfolio = Portfolio::create([
        'user_id' => auth()->id(),
        'title' => $request->title,
        'description' => $request->description,
        'image_path' => $path,
        'type' => $request->type
    ]);

    if ($request->has('tags')) {
        $portfolio->tags()->sync($request->tags);
    }

    return redirect()->route('portfolios.index')->with('success', '์ž‘ํ’ˆ์ด ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}
```

๋ผ์šฐํŠธ ์ถ”๊ฐ€:
```php
Route::middleware('auth')->group(function () {
    Route::get('/portfolios/create', [PortfolioController::class, 'create'])->name('portfolios.create');
    Route::post('/portfolios', [PortfolioController::class, 'store'])->name('portfolios.store');
});
```

React ์ปดํฌ๋„ŒํŠธ (`resources/js/Pages/Portfolio/Create.jsx`):
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';

export default function Create({ tags }) {
    const { data, setData, post, processing, errors } = useForm({
        title: '',
        description: '',
        image: null,
        type: 'character',
        tags: []
    });

    const handleSubmit = (e) => {
        e.preventDefault();
        post(route('portfolios.store'));
    };

    return (
        <AuthenticatedLayout>
            <Head title="์ž‘ํ’ˆ ์—…๋กœ๋“œ" />
            <div className="py-12">
                <div className="max-w-2xl mx-auto sm:px-6 lg:px-8">
                    <h1 className="text-3xl font-bold mb-8">์ž‘ํ’ˆ ์—…๋กœ๋“œ</h1>
                    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow">
                        <div className="mb-4">
                            <label className="block font-medium mb-2">์ œ๋ชฉ</label>
                            <input
                                type="text"
                                value={data.title}
                                onChange={e => setData('title', e.target.value)}
                                className="w-full border rounded px-3 py-2"
                            />
                            {errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
                        </div>

                        <div className="mb-4">
                            <label className="block font-medium mb-2">์„ค๋ช…</label>
                            <textarea
                                value={data.description}
                                onChange={e => setData('description', e.target.value)}
                                className="w-full border rounded px-3 py-2"
                                rows="4"
                            />
                        </div>

                        <div className="mb-4">
                            <label className="block font-medium mb-2">์ด๋ฏธ์ง€</label>
                            <input
                                type="file"
                                onChange={e => setData('image', e.target.files[0])}
                                className="w-full border rounded px-3 py-2"
                            />
                            {errors.image && <p className="text-red-500 text-sm mt-1">{errors.image}</p>}
                        </div>

                        <div className="mb-4">
                            <label className="block font-medium mb-2">์ž‘ํ’ˆ ์œ ํ˜•</label>
                            <select
                                value={data.type}
                                onChange={e => setData('type', e.target.value)}
                                className="w-full border rounded px-3 py-2"
                            >
                                <option value="character">์บ๋ฆญํ„ฐ</option>
                                <option value="background">๋ฐฐ๊ฒฝ</option>
                                <option value="webtoon">์›นํˆฐ ์ปท</option>
                            </select>
                        </div>

                        <div className="mb-4">
                            <label className="block font-medium mb-2">ํƒœ๊ทธ (๋ณต์ˆ˜ ์„ ํƒ ๊ฐ€๋Šฅ)</label>
                            <div className="flex flex-wrap gap-2">
                                {tags.map(tag => (
                                    <label key={tag.id} className="flex items-center">
                                        <input
                                            type="checkbox"
                                            value={tag.id}
                                            checked={data.tags.includes(tag.id)}
                                            onChange={e => {
                                                if (e.target.checked) {
                                                    setData('tags', [...data.tags, tag.id]);
                                                } else {
                                                    setData('tags', data.tags.filter(t => t !== tag.id));
                                                }
                                            }}
                                            className="mr-2"
                                        />
                                        {tag.name}
                                    </label>
                                ))}
                            </div>
                        </div>

                        <button
                            type="submit"
                            disabled={processing}
                            className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
                        >
                            ์—…๋กœ๋“œ
                        </button>
                    </form>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

Storage ๋งํฌ ์ƒ์„ฑ:
```bash
php artisan storage:link
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ž‘ํ’ˆ ์—…๋กœ๋“œ ํŽ˜์ด์ง€๊ฐ€ ์ž‘๋™ํ•˜๊ณ , ์ด๋ฏธ์ง€ ํŒŒ์ผ๊ณผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ œ์ถœํ•˜๋ฉด DB์— ์ €์žฅ๋˜๊ณ  ๊ฐค๋Ÿฌ๋ฆฌ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

5
Step 5
์ปค๋ฏธ์…˜ ์˜๋ขฐ ๋ชจ๋ธ๊ณผ ํผ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.

Commission ๋ชจ๋ธ ์ƒ์„ฑ:
```bash
php artisan make:model Commission -m
```

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ:
```php
public function up()
{
    Schema::create('commissions', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->onDelete('cascade'); // ์˜๋ขฐ์ธ
        $table->string('commission_type'); // ์˜ˆ: 'character', 'illustration', 'webtoon'
        $table->decimal('budget', 10, 2);
        $table->text('details');
        $table->string('status')->default('pending'); // pending, in_progress, completed
        $table->timestamps();
    });
}
```

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

Commission ๋ชจ๋ธ:
```php
class Commission extends Model
{
    protected $fillable = ['user_id', 'commission_type', 'budget', 'details', 'status'];

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

CommissionController ์ƒ์„ฑ:
```bash
php artisan make:controller CommissionController
```

`app/Http/Controllers/CommissionController.php`:
```php
use App\Models\Commission;
use Inertia\Inertia;

class CommissionController extends Controller
{
    public function create()
    {
        return Inertia::render('Commission/Create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'commission_type' => 'required',
            'budget' => 'required|numeric|min:0',
            'details' => 'required'
        ]);

        Commission::create([
            'user_id' => auth()->id(),
            'commission_type' => $request->commission_type,
            'budget' => $request->budget,
            'details' => $request->details
        ]);

        return redirect()->route('commissions.index')->with('success', '์ปค๋ฏธ์…˜ ์˜๋ขฐ๊ฐ€ ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
    }

    public function index()
    {
        $commissions = Commission::with('user')->latest()->paginate(10);
        return Inertia::render('Commission/Index', ['commissions' => $commissions]);
    }
}
```

๋ผ์šฐํŠธ ์ถ”๊ฐ€:
```php
Route::middleware('auth')->group(function () {
    Route::get('/commissions/create', [CommissionController::class, 'create'])->name('commissions.create');
    Route::post('/commissions', [CommissionController::class, 'store'])->name('commissions.store');
    Route::get('/commissions', [CommissionController::class, 'index'])->name('commissions.index');
});
```

React ์ปดํฌ๋„ŒํŠธ (`resources/js/Pages/Commission/Create.jsx`):
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';

export default function Create() {
    const { data, setData, post, processing, errors } = useForm({
        commission_type: 'character',
        budget: '',
        details: ''
    });

    const handleSubmit = (e) => {
        e.preventDefault();
        post(route('commissions.store'));
    };

    return (
        <AuthenticatedLayout>
            <Head title="์ปค๋ฏธ์…˜ ์˜๋ขฐ" />
            <div className="py-12">
                <div className="max-w-2xl mx-auto sm:px-6 lg:px-8">
                    <h1 className="text-3xl font-bold mb-8">์ปค๋ฏธ์…˜ ์˜๋ขฐํ•˜๊ธฐ</h1>
                    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow">
                        <div className="mb-4">
                            <label className="block font-medium mb-2">์ž‘์—… ์œ ํ˜•</label>
                            <select
                                value={data.commission_type}
                                onChange={e => setData('commission_type', e.target.value)}
                                className="w-full border rounded px-3 py-2"
                            >
                                <option value="character">์บ๋ฆญํ„ฐ ๋””์ž์ธ</option>
                                <option value="illustration">์ผ๋Ÿฌ์ŠคํŠธ</option>
                                <option value="webtoon">์›นํˆฐ ์ปท</option>
                            </select>
                        </div>

                        <div className="mb-4">
                            <label className="block font-medium mb-2">์˜ˆ์‚ฐ (์›)</label>
                            <input
                                type="number"
                                value={data.budget}
                                onChange={e => setData('budget', e.target.value)}
                                className="w-full border rounded px-3 py-2"
                            />
                            {errors.budget && <p className="text-red-500 text-sm mt-1">{errors.budget}</p>}
                        </div>

                        <div className="mb-4">
                            <label className="block font-medium mb-2">์ƒ์„ธ ์š”๊ตฌ์‚ฌํ•ญ</label>
                            <textarea
                                value={data.details}
                                onChange={e => setData('details', e.target.value)}
                                className="w-full border rounded px-3 py-2"
                                rows="6"
                                placeholder="์›ํ•˜๋Š” ์Šคํƒ€์ผ, ์บ๋ฆญํ„ฐ ํŠน์ง•, ๋ฐฐ๊ฒฝ ์š”์†Œ ๋“ฑ์„ ์ž์„ธํžˆ ์ ์–ด์ฃผ์„ธ์š”."
                            />
                            {errors.details && <p className="text-red-500 text-sm mt-1">{errors.details}</p>}
                        </div>

                        <button
                            type="submit"
                            disabled={processing}
                            className="bg-green-500 text-white px-6 py-2 rounded hover:bg-green-600 disabled:opacity-50"
                        >
                            ์˜๋ขฐ ์ ‘์ˆ˜
                        </button>
                    </form>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ปค๋ฏธ์…˜ ์˜๋ขฐ ํผ์ด ์ž‘๋™ํ•˜๊ณ , ์ œ์ถœ ์‹œ DB์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ์ž‘๊ฐ€๋Š” ๋ฐ›์€ ์˜๋ขฐ ๋ชฉ๋ก์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6
Step 6
์˜๋ขฐ ๊ด€๋ฆฌ ํŽ˜์ด์ง€์™€ ์ข‹์•„์š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.

์˜๋ขฐ ๋ชฉ๋ก ํŽ˜์ด์ง€ (`resources/js/Pages/Commission/Index.jsx`):
```jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';

export default function Index({ commissions }) {
    const statusLabels = {
        pending: '๋Œ€๊ธฐ์ค‘',
        in_progress: '์ง„ํ–‰์ค‘',
        completed: '์™„๋ฃŒ'
    };

    const statusColors = {
        pending: 'bg-yellow-100 text-yellow-800',
        in_progress: 'bg-blue-100 text-blue-800',
        completed: 'bg-green-100 text-green-800'
    };

    return (
        <AuthenticatedLayout>
            <Head title="์ปค๋ฏธ์…˜ ์˜๋ขฐ ๋ชฉ๋ก" />
            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <h1 className="text-3xl font-bold mb-8">๋ฐ›์€ ์ปค๋ฏธ์…˜ ์˜๋ขฐ</h1>
                    <div className="bg-white rounded-lg shadow overflow-hidden">
                        <table className="min-w-full">
                            <thead className="bg-gray-50">
                                <tr>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์˜๋ขฐ์ธ</th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์ž‘์—… ์œ ํ˜•</th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์˜ˆ์‚ฐ</th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์ƒํƒœ</th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์ ‘์ˆ˜์ผ</th>
                                </tr>
                            </thead>
                            <tbody className="divide-y divide-gray-200">
                                {commissions.data.map(commission => (
                                    <tr key={commission.id}>
                                        <td className="px-6 py-4">{commission.user.name}</td>
                                        <td className="px-6 py-4">{commission.commission_type}</td>
                                        <td className="px-6 py-4">{commission.budget.toLocaleString()}์›</td>
                                        <td className="px-6 py-4">
                                            <span className={`px-2 py-1 rounded text-xs ${statusColors[commission.status]}`}>
                                                {statusLabels[commission.status]}
                                            </span>
                                        </td>
                                        <td className="px-6 py-4">{new Date(commission.created_at).toLocaleDateString()}</td>
                                    </tr>
                                ))}
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}
```

์ข‹์•„์š” ๊ธฐ๋Šฅ ๊ตฌํ˜„ (PortfolioController์— ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€):
```php
public function like(Portfolio $portfolio)
{
    $portfolio->increment('likes_count');
    return back();
}
```

๋ผ์šฐํŠธ ์ถ”๊ฐ€:
```php
Route::post('/portfolios/{portfolio}/like', [PortfolioController::class, 'like'])->name('portfolios.like');
```

๊ฐค๋Ÿฌ๋ฆฌ ํŽ˜์ด์ง€์—์„œ ์ข‹์•„์š” ๋ฒ„ํŠผ ์ˆ˜์ • (`resources/js/Pages/Portfolio/Index.jsx`):
```jsx
import { useForm } from '@inertiajs/react';

// ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€
const { post } = useForm();

const handleLike = (portfolioId) => {
    post(route('portfolios.like', portfolioId));
};

// ์ข‹์•„์š” ๋ฒ„ํŠผ
<button
    onClick={() => handleLike(portfolio.id)}
    className="text-red-500 hover:text-red-600"
>
    โค๏ธ {portfolio.likes_count}
</button>
```

๋„ค๋น„๊ฒŒ์ด์…˜์— '์ปค๋ฏธ์…˜ ์˜๋ขฐ', '์˜๋ขฐ ๋ชฉ๋ก' ๋งํฌ ์ถ”๊ฐ€.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์˜๋ขฐ ๊ด€๋ฆฌ ํŽ˜์ด์ง€์—์„œ ๋ฐ›์€ ์ปค๋ฏธ์…˜ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋˜๊ณ , ์ž‘ํ’ˆ ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์ข‹์•„์š” ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์นด์šดํŠธ๊ฐ€ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ํ†ตํ•ฉ๋˜์–ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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