๐จ ์จ๋ผ์ธ ์ปค์คํ ๊ตฟ์ฆ ๋์์ธ ์คํ๋์ค - ์ค์๊ฐ ํ๋ฆฌ๋ทฐ & ์ฃผ๋ฌธ ํ๋ซํผ
custom-merch-design-studio
๋ฌธ์ ์ค๋ช
ํ๋ก์ ํธ ๊ฐ์
๊ณ ๊ฐ์ด ํฐ์ ์ธ , ๋จธ๊ทธ์ปต, ์คํฐ์ปค ๋ฑ ๋ค์ํ ๊ตฟ์ฆ์ ์์ ๋ง์ ํ ์คํธ์ ์์์ ์ ์ฉํ์ฌ ์ค์๊ฐ์ผ๋ก ๋์์ธ์ ํ์ธํ๊ณ ์ฃผ๋ฌธํ ์ ์๋ ํ์คํ ํ๋ซํผ์ ๋๋ค. Laravel 13.x + Inertia.js + React 19 ๋ชจ๋ ธ๋ฆฌ์ค ์ํคํ ์ฒ๋ก ๊ตฌํํ๋ฉฐ, ์ฌ์ฉ์ ์นํ์ ์ธ ๋์์ธ ํด๊ณผ ์ฃผ๋ฌธ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
ํ์ ๊ธฐ๋ฅ
- ๊ตฟ์ฆ ์นดํ๋ก๊ทธ: ํฐ์ ์ธ , ๋จธ๊ทธ์ปต, ์คํฐ์ปค ๋ฑ ๋ค์ํ ์ํ ๋ชฉ๋ก ํ์ (์ด๋ฏธ์ง, ๊ฐ๊ฒฉ, ์ค๋ช )
- ์ค์๊ฐ ๋์์ธ ํ๋ฆฌ๋ทฐ: ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ํ ์คํธ์ ์ ํํ ์์์ด ๊ตฟ์ฆ ์ด๋ฏธ์ง์ ์ค์๊ฐ์ผ๋ก ๋ฐ์๋๋ ์บ๋ฒ์ค
- ์ปค์คํฐ๋ง์ด์ง ์ต์ : ํ ์คํธ ์ ๋ ฅ, ํฐํธ ์ ํ, ์์ ํผ์ปค, ํ ์คํธ ํฌ๊ธฐ ์กฐ์
- ์ฃผ๋ฌธ ํผ: ๋์์ธ ์๋ฃ ํ ๊ณ ๊ฐ ์ ๋ณด(์ด๋ฆ, ์ด๋ฉ์ผ, ๋ฐฐ์ก์ง, ์๋) ์ ๋ ฅ ๋ฐ ์ฃผ๋ฌธ ์์ฑ
- ์ฃผ๋ฌธ ๋ด์ญ ์กฐํ: ๊ณ ๊ฐ์ด ์์ ์ ์ฃผ๋ฌธ ์ํ๋ฅผ ํ์ธํ ์ ์๋ ํ์ด์ง (์ฃผ๋ฌธ๋ฒํธ ๊ฒ์)
- ๋ฐ์ํ ๋์์ธ: ๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํฑ ๋ชจ๋ ์ง์
- Inertia.js ๋ผ์ฐํ : Laravel ๋ผ์ฐํธ์์ React ์ปดํฌ๋ํธ๋ก ๋ฐ์ดํฐ ์ ๋ฌ (๋ณ๋ API ์์)
์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ๋์์ธ ์ ์ฅ ๊ธฐ๋ฅ: ๋ก๊ทธ์ธ ์์ด ๋ธ๋ผ์ฐ์ localStorage์ ์์ ์ ์ฅ
- ์ด๋ฏธ์ง ์ ๋ก๋: ์ฌ์ฉ์๊ฐ ์์ ์ ๋ก๊ณ /์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํ์ฌ ๊ตฟ์ฆ์ ์ ์ฉ
- ์ธ๊ธฐ ๋์์ธ ๊ฐค๋ฌ๋ฆฌ: ๋ค๋ฅธ ์ฌ์ฉ์๋ค์ด ๋ง๋ ๋์์ธ ์ํ ๋ณด๊ธฐ
- ์ฃผ๋ฌธ ์ํ ์๋ ์ด๋ฉ์ผ ์๋ฆผ (Laravel Mail ํ์ฉ)
- ๋คํฌ ๋ชจ๋ ์ง์
๊ธฐ์ ์คํ
- ๋ฐฑ์๋: Laravel 13.x (์ปจํธ๋กค๋ฌ, ๋ชจ๋ธ, ๋ง์ด๊ทธ๋ ์ด์ , Eloquent)
- ํ๋ก ํธ์๋: React 19 (ํจ์ํ ์ปดํฌ๋ํธ, Hooks)
- ํ์คํ ์ฐ๊ฒฐ: Inertia.js (Laravel ๋ผ์ฐํธ โ React ํ์ด์ง ์ปดํฌ๋ํธ)
- ์คํ์ผ๋ง: Tailwind CSS 4.x
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: SQLite (๊ฐ๋ฐ ํ๊ฒฝ ๊ฐ์ํ)
- ์ํ ๊ด๋ฆฌ: React useState, useReducer (๋ณต์กํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ถํ์)
ํ์ต ๋ชฉํ
- Laravel 13.x + Inertia.js + React 19 ๋ชจ๋ ธ๋ฆฌ์ค ํ์คํ ์ํคํ ์ฒ ์ดํด
- Inertia::render()๋ฅผ ํตํ ์๋ฒ-ํด๋ผ์ด์ธํธ ๋ฐ์ดํฐ ์ ๋ฌ ๋ฐฉ์ ์ต๋
- Laravel Eloquent ๊ด๊ณ ์ค์ (์ํ - ์ฃผ๋ฌธ - ๋์์ธ ์ต์ )
- React์์ ์ค์๊ฐ UI ์ ๋ฐ์ดํธ ๋ฐ ํผ ์ํ ๊ด๋ฆฌ
- ํ์ผ ์ ๋ก๋ ๋ฐ ์คํ ๋ฆฌ์ง ๊ด๋ฆฌ (์ ํ ๊ธฐ๋ฅ)
- RESTful ๋ผ์ฐํธ ์ค๊ณ ์์ด Inertia ๋ผ์ฐํ ์ผ๋ก SPA ๊ตฌํ
์์ ์์ ์๊ฐ
4~6์๊ฐ (์ค๊ธ ๊ฐ๋ฐ์ ๊ธฐ์ค)
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x + Inertia.js + React 19 ๋ชจ๋ ธ๋ฆฌ์ค ํ์คํ ํ๋ก์ ํธ (๋ณ๋ API ์๋ฒ ์์)
- โข Laravel Breeze (React + Inertia) starter kit ๋๋ npx create laravel ์ฌ์ฉ
- โข Inertia::render()๋ก Laravel ์ปจํธ๋กค๋ฌ์์ React ํ์ด์ง ์ปดํฌ๋ํธ์ ๋ฐ์ดํฐ ์ ๋ฌ
- โข ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ SQLite ์ฌ์ฉ (config/database.php ์ค์ )
- โข Tailwind CSS 4.x๋ก ์คํ์ผ๋ง
- โข React ํจ์ํ ์ปดํฌ๋ํธ๋ง ์ฌ์ฉ (ํด๋์ค ์ปดํฌ๋ํธ ๊ธ์ง)
- โข ๋ณต์กํ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Redux, Zustand ๋ฑ) ์ฌ์ฉ ๊ธ์ง - useState/useReducer๋ง ์ฌ์ฉ
- โข ์ค์๊ฐ ์ฑํ /WebSocket ๊ธฐ๋ฅ ๊ธ์ง (HTTP ๊ธฐ๋ฐ ํผ ์ ์ถ๋ง ์ฌ์ฉ)
- โข ์ด๋ฏธ์ง ํ๋ฆฌ๋ทฐ๋ CSS position:absolute์ ํ ์คํธ ์ค๋ฒ๋ ์ด๋ก ๊ตฌํ (Canvas API ์ ํ์ฌํญ)
- โข ์ฃผ๋ฌธ ๋ฐ์ดํฐ๋ DB์ JSON ํํ๋ก ๋์์ธ ์ค์ ์ ์ฅ
- โข Laravel ๋ง์ด๊ทธ๋ ์ด์ ์ผ๋ก ํ ์ด๋ธ ์คํค๋ง ์ ์ (products, orders, design_options)
- โข Inertia Link ์ปดํฌ๋ํธ๋ก ํ์ด์ง ์ ํ (์๋ก๊ณ ์นจ ์๋ SPA ๊ฒฝํ)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir custom-merch-design-studio && cd custom-merch-design-studio ์ด ํด๋์์ Laravel 13.x + Inertia.js + React 19 ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค. ๋ค์ ์์ ์ ์ํํด์ฃผ์ธ์: 1. Laravel Breeze (React + Inertia) starter kit์ผ๋ก ํ๋ก์ ํธ ์ด๊ธฐํ: ```bash composer create-project laravel/laravel . "^13.0" composer require laravel/breeze --dev php artisan breeze:install react npm install npm run dev ``` 2. ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ SQLite๋ก ์ค์ : - `.env` ํ์ผ์์ DB_CONNECTION=sqlite๋ก ๋ณ๊ฒฝ - `database/database.sqlite` ํ์ผ ์์ฑ - `php artisan migrate` ์คํํ์ฌ ๊ธฐ๋ณธ ํ ์ด๋ธ ์์ฑ 3. ํ๋ก์ ํธ ๊ตฌ์กฐ ํ์ธ: - `app/Http/Controllers`: Laravel ์ปจํธ๋กค๋ฌ - `resources/js/Pages`: React ํ์ด์ง ์ปดํฌ๋ํธ (Inertia.js) - `resources/js/Components`: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ React ์ปดํฌ๋ํธ - `routes/web.php`: Inertia ๋ผ์ฐํธ ์ ์ 4. Tailwind CSS 4.x ์ค์ ํ์ธ (`tailwind.config.js`์์ content ๊ฒฝ๋ก ํฌํจ) 5. ๊ฐ๋ฐ ์๋ฒ ์คํ: ```bash php artisan serve # ๋ฐฑ์๋ ์๋ฒ (http://localhost:8000) npm run dev # Vite ํ๋ก ํธ์๋ ๋น๋ (๋ณ๋ ํฐ๋ฏธ๋) ``` ํ๋ก์ ํธ๊ฐ ์ ์์ ์ผ๋ก ์คํ๋๊ณ Laravel Breeze ๊ธฐ๋ณธ ํ์ด์ง๊ฐ ํ์๋๋ฉด ์ฑ๊ณต์ ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x + Breeze(React+Inertia) ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , SQLite DB๊ฐ ์ค์ ๋๋ฉฐ, ๊ฐ๋ฐ ์๋ฒ๊ฐ ์ ์ ์คํ๋ฉ๋๋ค. ๋ธ๋ผ์ฐ์ ์์ http://localhost:8000 ์ ์ ์ Laravel Breeze ๊ธฐ๋ณธ ํ๋ฉด์ด ํ์๋ฉ๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ค๊ณํ๊ณ ๋ง์ด๊ทธ๋ ์ด์ ์ ์์ฑํฉ๋๋ค. ๋ค์ ํ ์ด๋ธ์ ์์ฑํด์ฃผ์ธ์: 1. **products ํ ์ด๋ธ** (๊ตฟ์ฆ ์ํ ์ ๋ณด): ```bash php artisan make:migration create_products_table ``` - id (primary key) - name (string): ์ํ๋ช (์: "ํด๋์ ํฐ์ ์ธ ", "์ธ๋ผ๋ฏน ๋จธ๊ทธ์ปต") - type (string): ์ํ ํ์ (tshirt, mug, sticker) - base_price (decimal): ๊ธฐ๋ณธ ๊ฐ๊ฒฉ - image_url (string): ์ํ ์ด๋ฏธ์ง ๊ฒฝ๋ก (public/images/products/) - description (text): ์ํ ์ค๋ช - timestamps 2. **orders ํ ์ด๋ธ** (๊ณ ๊ฐ ์ฃผ๋ฌธ ์ ๋ณด): ```bash php artisan make:migration create_orders_table ``` - id (primary key) - product_id (foreign key โ products.id) - customer_name (string): ๊ณ ๊ฐ ์ด๋ฆ - customer_email (string): ๊ณ ๊ฐ ์ด๋ฉ์ผ - shipping_address (text): ๋ฐฐ์ก ์ฃผ์ - quantity (integer): ์ฃผ๋ฌธ ์๋ - design_data (json): ๋์์ธ ์ค์ (ํ ์คํธ, ์์, ํฐํธ ๋ฑ JSON ์ ์ฅ) - total_price (decimal): ์ด ๊ฐ๊ฒฉ - status (string, default: 'pending'): ์ฃผ๋ฌธ ์ํ (pending, processing, completed) - order_number (string, unique): ์ฃผ๋ฌธ๋ฒํธ (ORD-XXXXXX ํ์) - timestamps 3. ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ ์์ฑ ํ ์คํ: ```bash php artisan migrate ``` 4. Product ๋ชจ๋ธ ์์ฑ ๋ฐ ๊ด๊ณ ์ค์ : ```bash php artisan make:model Product ``` - `app/Models/Product.php`์์ `orders()` ๊ด๊ณ ์ ์ (hasMany) - fillable ์์ฑ์ ['name', 'type', 'base_price', 'image_url', 'description'] ์ถ๊ฐ 5. Order ๋ชจ๋ธ ์์ฑ ๋ฐ ๊ด๊ณ ์ค์ : ```bash php artisan make:model Order ``` - `app/Models/Order.php`์์ `product()` ๊ด๊ณ ์ ์ (belongsTo) - fillable ์์ฑ์ ['product_id', 'customer_name', 'customer_email', 'shipping_address', 'quantity', 'design_data', 'total_price', 'status', 'order_number'] ์ถ๊ฐ - design_data ํ๋๋ฅผ JSON์ผ๋ก ์๋ ์บ์คํ (protected $casts = ['design_data' => 'array'];) ๋ง์ด๊ทธ๋ ์ด์ ์ด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋๊ณ ๋ชจ๋ธ ๊ด๊ณ๊ฐ ์ค์ ๋๋ฉด ์๋ฃ์ ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: products, orders ํ ์ด๋ธ์ด SQLite DB์ ์์ฑ๋๊ณ , Product ๋ฐ Order ๋ชจ๋ธ์ Eloquent ๊ด๊ณ๊ฐ ์ ์๋ฉ๋๋ค. php artisan migrate ์คํ ์ ์๋ฌ ์์ด ์๋ฃ๋ฉ๋๋ค.
์๋ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ์ฌ ์ํ ๊ตฟ์ฆ ์ํ์ DB์ ์ถ๊ฐํฉ๋๋ค.
๋ค์ ์์
์ ์ํํด์ฃผ์ธ์:
1. Seeder ํ์ผ ์์ฑ:
```bash
php artisan make:seeder ProductSeeder
```
2. `database/seeders/ProductSeeder.php`์ ์ํ ์ํ ๋ฐ์ดํฐ ์ฝ์
:
```php
use App\Models\Product;
Product::create([
'name' => 'ํด๋์ ํ์ดํธ ํฐ์
์ธ ',
'type' => 'tshirt',
'base_price' => 25000,
'image_url' => '/images/products/tshirt-white.png',
'description' => '100% ์ฝํผ ์์ฌ์ ํธ์ํ ํฐ์
์ธ ์
๋๋ค. ์ํ๋ ํ
์คํธ๋ฅผ ์ถ๊ฐํ์ธ์!'
]);
Product::create([
'name' => '์ธ๋ผ๋ฏน ๋จธ๊ทธ์ปต',
'type' => 'mug',
'base_price' => 15000,
'image_url' => '/images/products/mug-white.png',
'description' => '์์นจ ์ปคํผ๋ฅผ ๋ ํน๋ณํ๊ฒ! ๋๋ง์ ๋ฌธ๊ตฌ๋ฅผ ์๊ฒจ๋ณด์ธ์.'
]);
Product::create([
'name' => '๋น๋ ์คํฐ์ปค (10x10cm)',
'type' => 'sticker',
'base_price' => 5000,
'image_url' => '/images/products/sticker-square.png',
'description' => '๋ฐฉ์ ๋น๋ ์คํฐ์ปค๋ก ๋
ธํธ๋ถ, ํ
๋ธ๋ฌ ์ด๋๋ ๋ถ์ฌ๋ณด์ธ์.'
]);
```
3. `database/seeders/DatabaseSeeder.php`์์ ProductSeeder ํธ์ถ:
```php
public function run()
{
$this->call(ProductSeeder::class);
}
```
4. Seeder ์คํ:
```bash
php artisan db:seed
```
5. ์ํ ์ด๋ฏธ์ง ์ค๋น (์ ํ์ฌํญ):
- `public/images/products/` ํด๋ ์์ฑ
- ์ํ ์ด๋ฏธ์ง ํ์ผ ์ถ๊ฐ (tshirt-white.png, mug-white.png, sticker-square.png)
- ๋๋ AI์๊ฒ "placeholder ์ด๋ฏธ์ง URL ์ฌ์ฉ" ์์ฒญ
์๋ ๋ฐ์ดํฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ฝ์
๋๋ฉด ์๋ฃ์
๋๋ค. `php artisan tinker`์์ `Product::all()`๋ก ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: products ํ ์ด๋ธ์ 3๊ฐ์ ์ํ ๊ตฟ์ฆ ์ํ(ํฐ์ ์ธ , ๋จธ๊ทธ์ปต, ์คํฐ์ปค)์ด ์ฝ์ ๋ฉ๋๋ค. php artisan db:seed ์คํ ์ ์๋ฌ ์์ด ์๋ฃ๋ฉ๋๋ค.
๊ตฟ์ฆ ์นดํ๋ก๊ทธ ํ์ด์ง๋ฅผ ๋ง๋ค๊ณ ์ํ ๋ชฉ๋ก์ ํ์ํฉ๋๋ค.
๋ค์ ์์
์ ์ํํด์ฃผ์ธ์:
1. ProductController ์์ฑ:
```bash
php artisan make:controller ProductController
```
2. `app/Http/Controllers/ProductController.php`์ index ๋ฉ์๋ ์์ฑ:
```php
use App\Models\Product;
use Inertia\Inertia;
public function index()
{
$products = Product::all();
return Inertia::render('Products/Index', [
'products' => $products
]);
}
```
3. `routes/web.php`์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
use App\Http\Controllers\ProductController;
Route::get('/', [ProductController::class, 'index'])->name('products.index');
```
4. React ํ์ด์ง ์ปดํฌ๋ํธ ์์ฑ:
- `resources/js/Pages/Products/Index.jsx` ํ์ผ ์์ฑ
- Inertia usePage ํ
์ผ๋ก products ๋ฐ์ดํฐ ๋ฐ๊ธฐ
- Tailwind CSS๋ก ๊ทธ๋ฆฌ๋ ๋ ์ด์์ ๊ตฌ์ฑ (3์ด ๋๋ ๋ฐ์ํ 2/3์ด)
- ๊ฐ ์ํ ์นด๋์ ์ด๋ฏธ์ง, ์ด๋ฆ, ๊ฐ๊ฒฉ, ์ค๋ช
ํ์
- "๋์์ธ ์์ํ๊ธฐ" ๋ฒํผ ์ถ๊ฐ (์์ง ๋์ ์ ํจ, ๋ค์ ๋จ๊ณ์์ ์ฐ๊ฒฐ)
5. ์ปดํฌ๋ํธ ์์ ๊ตฌ์กฐ:
```jsx
import { Head, Link } from '@inertiajs/react';
export default function Index({ products }) {
return (
<>
<Head title="๊ตฟ์ฆ ์นดํ๋ก๊ทธ" />
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4">
<h1 className="text-4xl font-bold text-center mb-12">๋๋ง์ ๊ตฟ์ฆ ๋ง๋ค๊ธฐ</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{products.map(product => (
<div key={product.id} className="bg-white rounded-lg shadow-lg overflow-hidden">
<img src={product.image_url} alt={product.name} className="w-full h-64 object-cover" />
<div className="p-6">
<h2 className="text-2xl font-semibold mb-2">{product.name}</h2>
<p className="text-gray-600 mb-4">{product.description}</p>
<p className="text-xl font-bold text-blue-600 mb-4">{product.base_price.toLocaleString()}์</p>
<Link href={`/products/${product.id}/design`} className="block w-full bg-blue-600 text-white text-center py-3 rounded-lg hover:bg-blue-700 transition">
๋์์ธ ์์ํ๊ธฐ
</Link>
</div>
</div>
))}
</div>
</div>
</div>
</>
);
}
```
๋ธ๋ผ์ฐ์ ์์ http://localhost:8000 ์ ์ ์ 3๊ฐ์ ๊ตฟ์ฆ ์ํ ์นด๋๊ฐ ๊ทธ๋ฆฌ๋๋ก ํ์๋๋ฉด ์ฑ๊ณต์
๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: Products/Index.jsx ํ์ด์ง๊ฐ ์์ฑ๋๊ณ , ์ํ ๋ชฉ๋ก์ด Tailwind CSS ๊ทธ๋ฆฌ๋ ๋ ์ด์์์ผ๋ก ํ์๋ฉ๋๋ค. ๊ฐ ์นด๋์ ์ด๋ฏธ์ง, ์ด๋ฆ, ๊ฐ๊ฒฉ, ์ค๋ช , "๋์์ธ ์์ํ๊ธฐ" ๋ฒํผ์ด ํฌํจ๋ฉ๋๋ค.
์ค์๊ฐ ๋์์ธ ํ๋ฆฌ๋ทฐ ํ์ด์ง๋ฅผ ๋ง๋ค๊ณ ํ
์คํธ/์์ ์ปค์คํฐ๋ง์ด์ง ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
๋ค์ ์์
์ ์ํํด์ฃผ์ธ์:
1. ProductController์ show ๋ฉ์๋ ์ถ๊ฐ:
```php
public function show($id)
{
$product = Product::findOrFail($id);
return Inertia::render('Products/Design', [
'product' => $product
]);
}
```
2. `routes/web.php`์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
Route::get('/products/{id}/design', [ProductController::class, 'show'])->name('products.design');
```
3. React ํ์ด์ง ์ปดํฌ๋ํธ ์์ฑ:
- `resources/js/Pages/Products/Design.jsx` ํ์ผ ์์ฑ
- useState๋ก ๋์์ธ ์ํ ๊ด๋ฆฌ:
```jsx
const [designText, setDesignText] = useState('');
const [textColor, setTextColor] = useState('#000000');
const [fontSize, setFontSize] = useState(24);
const [fontFamily, setFontFamily] = useState('Arial');
```
4. ์ข์ธก ์์ญ: ์ค์๊ฐ ํ๋ฆฌ๋ทฐ ์บ๋ฒ์ค
- ์ํ ์ด๋ฏธ์ง๋ฅผ ๋ฐฐ๊ฒฝ์ผ๋ก ํ์
- ์
๋ ฅํ ํ
์คํธ๋ฅผ ์ด๋ฏธ์ง ์์ ์ค๋ฒ๋ ์ด (CSS position:absolute)
- ํ
์คํธ ์คํ์ผ(์์, ํฌ๊ธฐ, ํฐํธ)์ด ์ค์๊ฐ ๋ฐ์
- ์์ ๊ตฌ์กฐ:
```jsx
<div className="relative w-full h-96 bg-gray-100 rounded-lg overflow-hidden">
<img src={product.image_url} alt={product.name} className="w-full h-full object-contain" />
<div className="absolute inset-0 flex items-center justify-center">
<p style={{ color: textColor, fontSize: `${fontSize}px`, fontFamily: fontFamily }} className="font-bold">
{designText || '์ฌ๊ธฐ์ ํ
์คํธ๋ฅผ ์
๋ ฅํ์ธ์'}
</p>
</div>
</div>
```
5. ์ฐ์ธก ์์ญ: ์ปค์คํฐ๋ง์ด์ง ์ปจํธ๋กค ํจ๋
- ํ
์คํธ ์
๋ ฅ ํ๋ (input type="text")
- ์์ ์ ํ (input type="color")
- ํฐํธ ํฌ๊ธฐ ์ฌ๋ผ์ด๋ (input type="range", min=12, max=72)
- ํฐํธ ์ ํ ๋๋กญ๋ค์ด (select: Arial, Georgia, 'Courier New', 'Comic Sans MS')
- "์ฃผ๋ฌธํ๊ธฐ" ๋ฒํผ (๋ค์ ๋จ๊ณ์์ ์ฐ๊ฒฐ)
6. ๋ฐ์ํ ๋ ์ด์์:
- ๋ฐ์คํฌํฑ: ์ข์ฐ 2์ด (ํ๋ฆฌ๋ทฐ | ์ปจํธ๋กค)
- ๋ชจ๋ฐ์ผ: ์ธ๋ก 1์ด (ํ๋ฆฌ๋ทฐ ์, ์ปจํธ๋กค ์๋)
7. Inertia Link๋ก ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ ์ถ๊ฐ:
```jsx
<Link href="/" className="text-blue-600 hover:underline">โ ์นดํ๋ก๊ทธ๋ก ๋์๊ฐ๊ธฐ</Link>
```
๋ธ๋ผ์ฐ์ ์์ "๋์์ธ ์์ํ๊ธฐ" ํด๋ฆญ ์ ์ค์๊ฐ ํ๋ฆฌ๋ทฐ ํ์ด์ง๊ฐ ํ์๋๊ณ , ํ
์คํธ/์์/ํฌ๊ธฐ ๋ณ๊ฒฝ์ด ์ฆ์ ๋ฐ์๋๋ฉด ์ฑ๊ณต์
๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: Products/Design.jsx ํ์ด์ง๊ฐ ์์ฑ๋๊ณ , ์ข์ธก์ ์ํ ์ด๋ฏธ์ง ์ ํ ์คํธ ์ค๋ฒ๋ ์ด, ์ฐ์ธก์ ์ปค์คํฐ๋ง์ด์ง ์ปจํธ๋กค์ด ํ์๋ฉ๋๋ค. ์ฌ์ฉ์ ์ ๋ ฅ์ด ์ค์๊ฐ์ผ๋ก ํ๋ฆฌ๋ทฐ์ ๋ฐ์๋ฉ๋๋ค.
์ฃผ๋ฌธ ํผ์ ๋ง๋ค๊ณ ์ฃผ๋ฌธ ๋ฐ์ดํฐ๋ฅผ DB์ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
๋ค์ ์์
์ ์ํํด์ฃผ์ธ์:
1. OrderController ์์ฑ:
```bash
php artisan make:controller OrderController
```
2. `app/Http/Controllers/OrderController.php`์ store ๋ฉ์๋ ์์ฑ:
```php
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
public function store(Request $request)
{
$validated = $request->validate([
'product_id' => 'required|exists:products,id',
'customer_name' => 'required|string|max:255',
'customer_email' => 'required|email',
'shipping_address' => 'required|string',
'quantity' => 'required|integer|min:1',
'design_data' => 'required|array',
'total_price' => 'required|numeric'
]);
$order = Order::create([
...$validated,
'order_number' => 'ORD-' . strtoupper(Str::random(8)),
'status' => 'pending'
]);
return redirect()->route('orders.show', $order->id)->with('success', '์ฃผ๋ฌธ์ด ์๋ฃ๋์์ต๋๋ค!');
}
public function show($id)
{
$order = Order::with('product')->findOrFail($id);
return Inertia::render('Orders/Show', [
'order' => $order
]);
}
```
3. `routes/web.php`์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
use App\Http\Controllers\OrderController;
Route::post('/orders', [OrderController::class, 'store'])->name('orders.store');
Route::get('/orders/{id}', [OrderController::class, 'show'])->name('orders.show');
```
4. Design.jsx ํ์ด์ง์ ์ฃผ๋ฌธ ํผ ์ถ๊ฐ:
- "์ฃผ๋ฌธํ๊ธฐ" ๋ฒํผ ํด๋ฆญ ์ ๋ชจ๋ฌ ๋๋ ํ๋จ ์น์
ํ์
- ์ฃผ๋ฌธ ํผ ํ๋:
- ๊ณ ๊ฐ ์ด๋ฆ (input type="text")
- ์ด๋ฉ์ผ (input type="email")
- ๋ฐฐ์ก ์ฃผ์ (textarea)
- ์๋ (input type="number", min=1, default=1)
- Inertia useForm ํ
์ผ๋ก ํผ ์ํ ๊ด๋ฆฌ:
```jsx
import { useForm } from '@inertiajs/react';
const { data, setData, post, processing, errors } = useForm({
product_id: product.id,
customer_name: '',
customer_email: '',
shipping_address: '',
quantity: 1,
design_data: { text: designText, color: textColor, fontSize, fontFamily },
total_price: product.base_price * 1
});
const handleSubmit = (e) => {
e.preventDefault();
post('/orders');
};
```
5. ์ฃผ๋ฌธ ์๋ฃ ํ์ด์ง ์์ฑ:
- `resources/js/Pages/Orders/Show.jsx` ํ์ผ ์์ฑ
- ์ฃผ๋ฌธ๋ฒํธ, ์ํ๋ช
, ๋์์ธ ํ๋ฆฌ๋ทฐ, ๊ณ ๊ฐ ์ ๋ณด, ๋ฐฐ์ก์ง, ์ด ๊ธ์ก ํ์
- "ํ์ผ๋ก ๋์๊ฐ๊ธฐ" ๋ฒํผ ์ถ๊ฐ
6. ์๋ฌ ์ฒ๋ฆฌ:
- Design.jsx์์ `errors` ๊ฐ์ฒด๋ก ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ํ์
- ์: `{errors.customer_email && <p className="text-red-500 text-sm">{errors.customer_email}</p>}`
๋ธ๋ผ์ฐ์ ์์ ๋์์ธ ์๋ฃ ํ ์ฃผ๋ฌธ ํผ ์์ฑ ๋ฐ ์ ์ถ ์ ์ฃผ๋ฌธ ์๋ฃ ํ์ด์ง๋ก ์ด๋ํ๊ณ , DB์ ์ฃผ๋ฌธ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋๋ฉด ์ฑ๊ณต์
๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: OrderController๊ฐ ์์ฑ๋๊ณ , ์ฃผ๋ฌธ ํผ ์ ์ถ ์ orders ํ ์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ฉ๋๋ค. Orders/Show.jsx ํ์ด์ง์์ ์ฃผ๋ฌธ ์ ๋ณด๊ฐ ํ์๋๋ฉฐ, ์ฃผ๋ฌธ๋ฒํธ๊ฐ ์๋ ์์ฑ๋ฉ๋๋ค.
์ฃผ๋ฌธ ์กฐํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ฌ ๊ณ ๊ฐ์ด ์ฃผ๋ฌธ๋ฒํธ๋ก ์ฃผ๋ฌธ ์ํ๋ฅผ ํ์ธํ ์ ์๋๋ก ํฉ๋๋ค.
๋ค์ ์์
์ ์ํํด์ฃผ์ธ์:
1. OrderController์ search ๋ฉ์๋ ์ถ๊ฐ:
```php
public function search(Request $request)
{
$orderNumber = $request->input('order_number');
$order = Order::where('order_number', $orderNumber)->with('product')->first();
if (!$order) {
return back()->withErrors(['order_number' => '์ฃผ๋ฌธ๋ฒํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.']);
}
return redirect()->route('orders.show', $order->id);
}
```
2. `routes/web.php`์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
Route::post('/orders/search', [OrderController::class, 'search'])->name('orders.search');
```
3. ํ ํ์ด์ง(Products/Index.jsx)์ ์ฃผ๋ฌธ ์กฐํ ์น์
์ถ๊ฐ:
- ํ์ด์ง ์๋จ ๋๋ ํ๋จ์ "์ฃผ๋ฌธ ์กฐํ" ์น์
๋ฐฐ์น
- ์ฃผ๋ฌธ๋ฒํธ ์
๋ ฅ ํ๋ (input type="text", placeholder="ORD-XXXXXXXX")
- "์กฐํํ๊ธฐ" ๋ฒํผ
- Inertia useForm ํ
์ผ๋ก ํผ ์ ์ถ:
```jsx
const { data, setData, post, processing, errors } = useForm({
order_number: ''
});
const handleSearch = (e) => {
e.preventDefault();
post('/orders/search');
};
```
4. ์๋ฌ ๋ฉ์์ง ํ์:
- `{errors.order_number && <p className="text-red-500 text-sm mt-2">{errors.order_number}</p>}`
5. ์ฃผ๋ฌธ ์๋ฃ ํ์ด์ง(Orders/Show.jsx) ๊ฐ์ :
- ์ฃผ๋ฌธ ์ํ(pending, processing, completed)๋ฅผ ๋ฐฐ์ง๋ก ํ์
- ๋์์ธ ํ๋ฆฌ๋ทฐ๋ฅผ ๋ค์ ๋ ๋๋ง (design_data JSON ํ์ฑ)
- ์์:
```jsx
<div className="relative w-64 h-64 bg-gray-100 rounded-lg overflow-hidden mx-auto">
<img src={order.product.image_url} alt={order.product.name} className="w-full h-full object-contain" />
<div className="absolute inset-0 flex items-center justify-center">
<p style={{
color: order.design_data.color,
fontSize: `${order.design_data.fontSize}px`,
fontFamily: order.design_data.fontFamily
}} className="font-bold">
{order.design_data.text}
</p>
</div>
</div>
```
6. UI/UX ๊ฐ์ :
- ์ฃผ๋ฌธ ์กฐํ ์น์
์ ์นด๋ ํํ๋ก ์คํ์ผ๋ง
- "์ต๊ทผ ์ฃผ๋ฌธ" ๋งํฌ ์ถ๊ฐ (์ ํ์ฌํญ, localStorage ํ์ฉ)
๋ธ๋ผ์ฐ์ ์์ ์ฃผ๋ฌธ๋ฒํธ ์
๋ ฅ ํ ์กฐํ ์ ํด๋น ์ฃผ๋ฌธ ์์ธ ํ์ด์ง๋ก ์ด๋ํ๋ฉด ์ฑ๊ณต์
๋๋ค. ์๋ชป๋ ์ฃผ๋ฌธ๋ฒํธ ์
๋ ฅ ์ ์๋ฌ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฃผ๋ฌธ ์กฐํ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋์ด Products/Index.jsx์์ ์ฃผ๋ฌธ๋ฒํธ๋ก ๊ฒ์ ๊ฐ๋ฅํฉ๋๋ค. ์ฌ๋ฐ๋ฅธ ์ฃผ๋ฌธ๋ฒํธ ์ ๋ ฅ ์ Orders/Show.jsx ํ์ด์ง๋ก ์ด๋ํ๋ฉฐ, ๋์์ธ ํ๋ฆฌ๋ทฐ๊ฐ ๋ค์ ๋ ๋๋ง๋ฉ๋๋ค.
์ต์ข UI/UX ๊ฐ์ ๋ฐ ๋ฐ์ํ ๋์์ธ์ ์์ฑํฉ๋๋ค. ๋ค์ ์์ ์ ์ํํด์ฃผ์ธ์: 1. ์ ์ญ ๋ ์ด์์ ๊ฐ์ : - `resources/js/Layouts/GuestLayout.jsx` ๋๋ ์๋ก์ด ๋ ์ด์์ ์ปดํฌ๋ํธ ์์ฑ - ์๋จ ๋ค๋น๊ฒ์ด์ ๋ฐ ์ถ๊ฐ (๋ก๊ณ , "ํ", "์ฃผ๋ฌธ ์กฐํ" ๋งํฌ) - ํธํฐ ์ถ๊ฐ (์ ์๊ถ ์ ๋ณด, ์์ ๋ฏธ๋์ด ๋งํฌ) - ๋ชจ๋ ํ์ด์ง์ ๋ ์ด์์ ์ ์ฉ 2. ์ ๋๋ฉ์ด์ ํจ๊ณผ ์ถ๊ฐ: - ์ํ ์นด๋ hover ์ scale ํจ๊ณผ (Tailwind: `hover:scale-105 transition-transform`) - ์ฃผ๋ฌธ ์๋ฃ ํ์ด์ง์ ์ฑ๊ณต ๋ฉ์์ง ํ์ด๋์ธ ์ ๋๋ฉ์ด์ - ๋ฒํผ ํด๋ฆญ ์ ๋ก๋ฉ ์คํผ๋ ํ์ (Inertia `processing` ์ํ ํ์ฉ) 3. ๋ฐ์ํ ๋์์ธ ์ต์ ํ: - ๋ชจ๋ฐ์ผ์์ ๋์์ธ ํ๋ฆฌ๋ทฐ์ ์ปจํธ๋กค์ด ์ธ๋ก๋ก ๋ฐฐ์น๋๋๋ก Tailwind ๋ธ๋ ์ดํฌํฌ์ธํธ ์กฐ์ - ์ฃผ๋ฌธ ํผ ํ๋๊ฐ ๋ชจ๋ฐ์ผ์์ ์ ์ฒด ๋๋น ์ฌ์ฉ - ๋ค๋น๊ฒ์ด์ ๋ฐ๊ฐ ๋ชจ๋ฐ์ผ์์ ํ๋ฒ๊ฑฐ ๋ฉ๋ด๋ก ์ ํ (์ ํ์ฌํญ) 4. ์ ๊ทผ์ฑ ๊ฐ์ : - ๋ชจ๋ input ํ๋์ label ํ๊ทธ ์ถ๊ฐ - ์ด๋ฏธ์ง์ alt ์์ฑ ํ์ธ - ๋ฒํผ์ aria-label ์ถ๊ฐ (ํ์์) 5. ์๋ฌ ํ์ด์ง ์ถ๊ฐ: - 404 ํ์ด์ง (์ํ์ ์ฐพ์ ์ ์์ ๋) - `resources/js/Pages/Errors/404.jsx` ์์ฑ 6. ์ฑ๋ฅ ์ต์ ํ: - ์ด๋ฏธ์ง lazy loading ์ถ๊ฐ (`<img loading="lazy" />`) - ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ๋ฐฉ์ง (React.memo ์ฌ์ฉ ๊ณ ๋ ค) 7. ์ต์ข ํ ์คํธ: - ์ ์ฒด ํ๋ก์ฐ ํ ์คํธ (์นดํ๋ก๊ทธ โ ๋์์ธ โ ์ฃผ๋ฌธ โ ์กฐํ) - ๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์์ ๋ ์ด์์ ํ์ธ - ๋ธ๋ผ์ฐ์ ์ฝ์์์ ์๋ฌ ์๋์ง ํ์ธ 8. README.md ์์ฑ: - ํ๋ก์ ํธ ์ค๋ช , ์ค์น ๋ฐฉ๋ฒ, ์คํ ๋ฐฉ๋ฒ ๋ฌธ์ํ - ์คํฌ๋ฆฐ์ท ์ถ๊ฐ (์ ํ์ฌํญ) ๋ชจ๋ ๊ฐ์ ์ฌํญ์ด ์ ์ฉ๋๊ณ , ๋ค์ํ ๋๋ฐ์ด์ค์์ ์ ์ ์๋ํ๋ฉด ํ๋ก์ ํธ ์์ฑ์ ๋๋ค!
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ ์ญ ๋ ์ด์์์ด ์ถ๊ฐ๋๊ณ , ์ ๋๋ฉ์ด์ ํจ๊ณผ์ ๋ฐ์ํ ๋์์ธ์ด ์ ์ฉ๋ฉ๋๋ค. ๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํฑ์์ ๋ชจ๋ ์ ์ ์๋ํ๋ฉฐ, ์ ๊ทผ์ฑ๊ณผ ์ฑ๋ฅ์ด ๊ฐ์ ๋ฉ๋๋ค. README.md๊ฐ ์์ฑ๋์ด ํ๋ก์ ํธ ๋ฌธ์ํ๊ฐ ์๋ฃ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!