๐จ ํฌ๋ฆฌ์์ดํฐ ์ปค๋ฏธ์ ์ํฌํ๋ก์ฐ ๋งค๋์ - ์๋ขฐ๋ถํฐ ๋ฉํ๊น์ง ์ ๊ณผ์ ์๋ํ
creator-commission-workflow-manager
๋ฌธ์ ์ค๋ช
ํ๋ก์ ํธ ๊ฐ์
์ผ๋ฌ์คํธ๋ ์ดํฐ, ๋์์ด๋, ์๊ฐ ๋ฑ ํฌ๋ฆฌ์์ดํฐ๋ฅผ ์ํ ์ปค๋ฏธ์ (commission) ์์ ์ ๊ณผ์ ์ ๊ด๋ฆฌํ๋ ํ์คํ ํ๋ซํผ์ ๋๋ค. ๊ณ ๊ฐ์ด ์๋ขฐ์๋ฅผ ์ ์ถํ๋ฉด ํฌ๋ฆฌ์์ดํฐ๋ ๊ฒฌ์ ์ ๋ณด๋ด๊ณ , ๊ณ์ฝ์ด ์ฑ์ฌ๋๋ฉด ์์ ๋จ๊ณ๋ณ๋ก ์งํ ์ํฉ์ ์ ๋ฐ์ดํธํ๋ฉฐ, ์ค๊ฐ ๊ฒฐ๊ณผ๋ฌผ์ ๊ณต์ ํ๊ณ ํผ๋๋ฐฑ์ ๋ฐ์ ์ต์ข ๋ฉํ๊น์ง ๊ด๋ฆฌํฉ๋๋ค.
ํต์ฌ ๊ธฐ๋ฅ
ํ์ ๊ธฐ๋ฅ:
- ๐ฏ ์ปค๋ฏธ์ ์๋ขฐ ์์คํ : ๊ณ ๊ฐ์ด ์์ ์ ํ(์บ๋ฆญํฐ ๋์์ธ, ๋ก๊ณ , ์ผ๋ฌ์คํธ ๋ฑ) ์ ํ ํ ์์ธ ์๊ตฌ์ฌํญ ์์ฑ
- ๐ฐ ๊ฒฌ์ ์ ๋ฐ์ก & ์น์ธ: ํฌ๋ฆฌ์์ดํฐ๊ฐ ์๋ขฐ๋ฅผ ๊ฒํ ํ๊ณ ๊ฐ๊ฒฉ/์ผ์ ํฌํจ ๊ฒฌ์ ์ ๋ฐ์ก, ๊ณ ๊ฐ ์น์ธ ์ ๊ณ์ฝ ์ฑ์ฌ
- ๐ ์์ ๋จ๊ณ๋ณ ์ํฌํ๋ก์ฐ: ์ค์ผ์น โ ์ด์ โ ์์ โ ์ต์ข ๋ณธ ๋ฑ ๋จ๊ณ๋ณ ์งํ ์ํฉ ์๊ฐํ (Kanban ๋ณด๋ ์คํ์ผ)
- ๐ค ํ์ผ ์ ๋ก๋ & ๋ฒ์ ๊ด๋ฆฌ: ๊ฐ ๋จ๊ณ๋ง๋ค ์์ ๋ฌผ ์ ๋ก๋, ๋ฒ์ ํ์คํ ๋ฆฌ ์๋ ๊ธฐ๋ก
- ๐ฌ ๊ณ ๊ฐ ํผ๋๋ฐฑ ์์คํ : ๊ฐ ๋จ๊ณ๋ณ ๊ฒฐ๊ณผ๋ฌผ์ ๋๊ธ๋ก ์์ ์์ฒญ ์์ฑ, ํฌ๋ฆฌ์์ดํฐ ๋ต๋ณ
- ๐ ์ด๋ฉ์ผ ์๋ฆผ ์๋ํ: ์๋ขฐ ์ ์, ๊ฒฌ์ ๋ฐ์ก, ๋จ๊ณ ๋ณ๊ฒฝ, ํผ๋๋ฐฑ ๋ฑ๋ก ์ ์๋ ์ด๋ฉ์ผ ๋ฐ์ก (Laravel Queue + Mailable)
- ๐ ๋ฉํฐ ์ ์ ์ธ์ฆ: ํฌ๋ฆฌ์์ดํฐ์ ๊ณ ๊ฐ ์ญํ ๋ถ๋ฆฌ, Laravel Breeze ๊ธฐ๋ฐ ์ธ์ฆ
- ๐ ๋์๋ณด๋: ์งํ ์ค/์๋ฃ๋ ์ปค๋ฏธ์ ๋ชฉ๋ก, ์์ต ํต๊ณ, ํ๊ท ์์ ์์ ์๊ฐ ์ฐจํธ
๋ณด๋์ค ๊ธฐ๋ฅ:
- ๐ณ ๊ฒฐ์ ์ฐ๋: Stripe/Toss Payments API๋ก ๊ณ์ฝ๊ธ/์๊ธ ๊ฒฐ์ (ํ ์คํธ ๋ชจ๋)
- ๐ ์บ๋ฆฐ๋ ๋ทฐ: ๋ฉํ ๋ง๊ฐ์ผ ๊ธฐ๋ฐ ์ปค๋ฏธ์ ์ผ์ ์บ๋ฆฐ๋
- โญ ์๋ฃ ํ ๋ฆฌ๋ทฐ ์์คํ : ๊ณ ๊ฐ์ด ๋ณ์ & ํ๊ธฐ ์์ฑ, ํฌ๋ฆฌ์์ดํฐ ํฌํธํด๋ฆฌ์ค์ ์๋ ๋ฐ์
- ๐ ์๋ ๋ฆฌ๋ง์ธ๋: ๋ฉํ 3์ผ ์ ํฌ๋ฆฌ์์ดํฐ์๊ฒ ์๋ฆผ (Laravel Scheduler)
- ๐จ ํฌํธํด๋ฆฌ์ค ์๋ ์์ฑ: ์๋ฃ๋ ์ปค๋ฏธ์ ์ ๊ณต๊ฐ ํฌํธํด๋ฆฌ์ค ํ์ด์ง๋ก ์๋ ๋ณํ
๊ธฐ์ ์คํ
- ๋ฐฑ์๋: Laravel 13.x (Eloquent ORM, Queue, Scheduler, Mailable, Storage)
- ํ๋ก ํธ์๋: Blade ํ ํ๋ฆฟ + Tailwind CSS + Alpine.js (๋์ UI)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: MySQL (๊ด๊ณํ ์ค๊ณ: users, commissions, quotes, workflow_stages, files, feedbacks)
- ํ์ผ ์ ์ฅ: Laravel Storage (๋ก์ปฌ ๋๋ S3)
- ์ด๋ฉ์ผ: Laravel Mail + Mailtrap/SMTP
- ์ธ์ฆ: Laravel Breeze (Blade ๋ฒ์ )
์ด ํ๋ก์ ํธ๋ ์ค์ ํ๋ฆฌ๋์ ํฌ๋ฆฌ์์ดํฐ๊ฐ ๊ณ ๊ฐ ๊ด๋ฆฌ์ ์ฌ์ฉํ ์ ์๋ ์ค๋ฌด ์์ค์ ์์คํ ์ ๋๋ค.
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x ํ์ (์ต์ ์์ ๋ฒ์ )
- โข Blade ํ ํ๋ฆฟ ์์ง ์ฌ์ฉ (React/Vue ๊ธ์ง)
- โข Tailwind CSS๋ก ๋ฐ์ํ UI ๊ตฌํ
- โข Alpine.js๋ก ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์ธํฐ๋์ (๋ชจ๋ฌ, ๋๋กญ๋ค์ด, ํญ ๋ฑ)
- โข MySQL ๋ฐ์ดํฐ๋ฒ ์ด์ค (๋ง์ด๊ทธ๋ ์ด์ ํ์ผ ํ์)
- โข Laravel Breeze๋ก ์ธ์ฆ ์์คํ ๊ตฌ์ถ
- โข ํ์ผ ์ ๋ก๋๋ Laravel Storage ์ฌ์ฉ (public disk ๋๋ s3)
- โข ์ด๋ฉ์ผ์ Queue๋ก ๋น๋๊ธฐ ๋ฐ์ก (database driver ์ฌ์ฉ)
- โข ์ฝ๋ ์ฃผ์ ํ์ (๊ฐ ์ปจํธ๋กค๋ฌ ๋ฉ์๋, ๋ธ๋ ์ด๋ ์ปดํฌ๋ํธ ์ค๋ช )
- โข ํ๊ฒฝ๋ณ์(.env) ์ค์ ๊ฐ์ด๋ ํฌํจ
- โข Seeder๋ก ๋๋ฏธ ๋ฐ์ดํฐ ์์ฑ (ํฌ๋ฆฌ์์ดํฐ 1๋ช , ๊ณ ๊ฐ 3๋ช , ์ปค๋ฏธ์ 5๊ฐ)
- โข Policy๋ก ๊ถํ ๊ด๋ฆฌ (์์ ์ ์ปค๋ฏธ์ ๋ง ์์ ๊ฐ๋ฅ)
- โข ํผ ์์ฒญ ๊ฒ์ฆ (FormRequest ํด๋์ค ํ์ฉ)
- โข ์๋ฌ ํธ๋ค๋ง (try-catch, ์ฌ์ฉ์ ์นํ์ ์๋ฌ ๋ฉ์์ง)
- โข Git ์ปค๋ฐ ๋ฉ์์ง ๊ท์น ์ค์ (feat:, fix:, refactor: ๋ฑ)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir creator-commission-workflow-manager && cd creator-commission-workflow-manager
์ด ํด๋์์ Laravel 13.x ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
**๋ชฉํ**: Laravel ํ๋ก์ ํธ ์ด๊ธฐ ์ค์ ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์ค๊ณ
๋ค์์ ์ํํ์ธ์:
1. **Laravel 13.x ํ๋ก์ ํธ ์์ฑ**:
- `composer create-project laravel/laravel . "13.*"`
- `.env` ํ์ผ์์ DB ์ค์ (MySQL ์ฌ์ฉ, DB๋ช
: commission_manager)
- `php artisan key:generate`
2. **Laravel Breeze ์ค์น** (Blade ์คํ):
- `composer require laravel/breeze --dev`
- `php artisan breeze:install blade`
- `npm install && npm run build`
- ๋ง์ด๊ทธ๋ ์ด์
์คํ: `php artisan migrate`
3. **๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์ค๊ณ ๋ฐ ๋ง์ด๊ทธ๋ ์ด์
์์ฑ**:
๋ค์ ํ
์ด๋ธ์ ์์ฑํ๋ ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ์ ๋ง๋์ธ์:
**a) users ํ
์ด๋ธ (Breeze ๊ธฐ๋ณธ + ํ์ฅ)**:
- role: enum('creator', 'client') - ์ฌ์ฉ์ ์ญํ
- bio: text nullable - ํฌ๋ฆฌ์์ดํฐ ์๊ฐ
- portfolio_url: string nullable
- hourly_rate: decimal(10,2) nullable - ์๊ฐ๋น ์๊ธ (ํฌ๋ฆฌ์์ดํฐ์ฉ)
**b) commissions ํ
์ด๋ธ**:
- id, uuid (๊ณ ์ ์๋ณ์)
- client_id (users ์ธ๋ํค)
- creator_id (users ์ธ๋ํค, nullable - ๊ฒฌ์ ์น์ธ ์ ๊น์ง null)
- title: string - ์ปค๋ฏธ์
์ ๋ชฉ
- type: enum('character_design', 'logo', 'illustration', 'animation', 'other')
- description: text - ์์ธ ์๊ตฌ์ฌํญ
- reference_images: json nullable - ์ฐธ๊ณ ์ด๋ฏธ์ง URL ๋ฐฐ์ด
- budget_range: string nullable - ์์ฐ ๋ฒ์ (์: "$100-$200")
- deadline: date nullable - ํฌ๋ง ๋ฉํ์ผ
- status: enum('pending', 'quoted', 'in_progress', 'completed', 'cancelled') default 'pending'
- timestamps
**c) quotes ํ
์ด๋ธ** (๊ฒฌ์ ์):
- id
- commission_id (์ธ๋ํค)
- creator_id (์ธ๋ํค)
- price: decimal(10,2) - ๊ฒฌ์ ๊ธ์ก
- estimated_days: integer - ์์ ์์ ์ผ์
- message: text - ๊ฒฌ์ ์ค๋ช
- status: enum('pending', 'accepted', 'rejected') default 'pending'
- timestamps
**d) workflow_stages ํ
์ด๋ธ** (์์
๋จ๊ณ):
- id
- commission_id (์ธ๋ํค)
- name: string - ๋จ๊ณ ์ด๋ฆ (์: "์ค์ผ์น", "์ด์", "์ต์ข
๋ณธ")
- order: integer - ์์
- status: enum('pending', 'in_progress', 'completed') default 'pending'
- completed_at: timestamp nullable
- timestamps
**e) commission_files ํ
์ด๋ธ** (์
๋ก๋ ํ์ผ):
- id
- commission_id (์ธ๋ํค)
- workflow_stage_id (์ธ๋ํค, nullable)
- uploader_id (users ์ธ๋ํค)
- file_path: string - storage ๊ฒฝ๋ก
- file_name: string - ์๋ณธ ํ์ผ๋ช
- file_size: integer - ๋ฐ์ดํธ ๋จ์
- version: integer default 1 - ๋ฒ์ ๋ฒํธ
- timestamps
**f) feedbacks ํ
์ด๋ธ** (ํผ๋๋ฐฑ/๋๊ธ):
- id
- commission_id (์ธ๋ํค)
- workflow_stage_id (์ธ๋ํค, nullable)
- user_id (์ธ๋ํค)
- comment: text
- timestamps
4. **๋ง์ด๊ทธ๋ ์ด์
์คํ**:
- `php artisan migrate`
5. **๋ชจ๋ธ ์์ฑ ๋ฐ ๊ด๊ณ ์ค์ **:
- Commission, Quote, WorkflowStage, CommissionFile, Feedback ๋ชจ๋ธ ์์ฑ
- Eloquent ๊ด๊ณ ์ ์:
- Commission: belongsTo(User, 'client_id'), belongsTo(User, 'creator_id'), hasMany(Quote), hasMany(WorkflowStage), hasMany(CommissionFile), hasMany(Feedback)
- Quote: belongsTo(Commission), belongsTo(User, 'creator_id')
- WorkflowStage: belongsTo(Commission), hasMany(CommissionFile)
- CommissionFile: belongsTo(Commission), belongsTo(WorkflowStage), belongsTo(User, 'uploader_id')
- Feedback: belongsTo(Commission), belongsTo(WorkflowStage), belongsTo(User)
๋ชจ๋ ์ค์ ์ด ์๋ฃ๋๋ฉด `php artisan migrate:status`๋ก ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , Breeze ์ธ์ฆ ์์คํ ์ด ์ค์น๋๋ฉฐ, 6๊ฐ ํ ์ด๋ธ(users ํ์ฅ, commissions, quotes, workflow_stages, commission_files, feedbacks)์ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋ฉ๋๋ค. ๋ชจ๋ธ ํ์ผ๋ค์ด app/Models์ ์์ฑ๋๊ณ ๊ด๊ณ๊ฐ ์ ์๋ฉ๋๋ค.
**๋ชฉํ**: ๊ณ ๊ฐ์ด ์ปค๋ฏธ์
์๋ขฐ์๋ฅผ ์์ฑํ๊ณ ์ ์ถํ๋ ๊ธฐ๋ฅ ๊ตฌํ
๋ค์์ ์ํํ์ธ์:
1. **CommissionController ์์ฑ**:
- `php artisan make:controller CommissionController --resource`
2. **๋ผ์ฐํธ ์ค์ ** (routes/web.php):
```php
Route::middleware(['auth'])->group(function () {
Route::resource('commissions', CommissionController::class);
});
```
3. **์ปค๋ฏธ์
์์ฑ ํผ ๋ทฐ** (resources/views/commissions/create.blade.php):
- Tailwind CSS๋ก ์คํ์ผ๋ง๋ ํผ ์์ฑ
- ํ๋:
- ์ ๋ชฉ (input text, required)
- ์์
์ ํ (select: character_design, logo, illustration, animation, other)
- ์์ธ ์๊ตฌ์ฌํญ (textarea, required, ์ต์ 50์)
- ์ฐธ๊ณ ์ด๋ฏธ์ง ์
๋ก๋ (multiple file input, ์ ํ์ฌํญ, ์ด๋ฏธ์ง๋ง ํ์ฉ)
- ์์ฐ ๋ฒ์ (input text, ์: "$100-$200")
- ํฌ๋ง ๋ฉํ์ผ (date input)
- Alpine.js๋ก ํ์ผ ์
๋ก๋ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋ฅ ์ถ๊ฐ
- CSRF ํ ํฐ ํฌํจ
4. **CommissionController@store ๋ฉ์๋**:
- FormRequest ์์ฑ: `php artisan make:request StoreCommissionRequest`
- ๊ฒ์ฆ ๊ท์น:
- title: required, max:255
- type: required, in:character_design,logo,illustration,animation,other
- description: required, min:50
- reference_images.*: image, max:5120 (5MB)
- budget_range: nullable, max:50
- deadline: nullable, date, after:today
- ๋ก์ง:
- ์ฐธ๊ณ ์ด๋ฏธ์ง๋ฅผ storage/app/public/references์ ์ ์ฅ
- Commission ๋ ์ฝ๋ ์์ฑ (client_id๋ auth()->id())
- reference_images๋ JSON ๋ฐฐ์ด๋ก ์ ์ฅ
- ์ฑ๊ณต ์ ์ฑ๊ณต ๋ฉ์์ง์ ํจ๊ป ๋์๋ณด๋๋ก ๋ฆฌ๋๋ ์
5. **๋์๋ณด๋ ๋ทฐ ์์ ** (resources/views/dashboard.blade.php):
- ์ญํ ๋ณ ๋์๋ณด๋:
- **๊ณ ๊ฐ**: "์ ์ปค๋ฏธ์
์๋ขฐํ๊ธฐ" ๋ฒํผ, ๋ด ์๋ขฐ ๋ชฉ๋ก (์ํ๋ณ ํํฐ)
- **ํฌ๋ฆฌ์์ดํฐ**: ์๋ก์ด ์๋ขฐ ๋ชฉ๋ก (status='pending'), ์งํ ์ค์ธ ์์
๋ชฉ๋ก
- ๊ฐ ์ปค๋ฏธ์
์นด๋์ ์ ๋ชฉ, ์ ํ, ์ํ, ์์ฑ์ผ ํ์
- ์นด๋ ํด๋ฆญ ์ ์์ธ ํ์ด์ง๋ก ์ด๋
6. **๋ค๋น๊ฒ์ด์
๋ฉ๋ด ์
๋ฐ์ดํธ** (resources/views/layouts/navigation.blade.php):
- "๋ด ์ปค๋ฏธ์
" ๋งํฌ ์ถ๊ฐ
- ์ญํ ์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ฉ๋ด ํ์ (ํฌ๋ฆฌ์์ดํฐ๋ "์๋ขฐ ๋ชฉ๋ก", ๊ณ ๊ฐ์ "๋ด ์๋ขฐ")
7. **ํ
์คํธ**:
- ๊ณ ๊ฐ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ํ ์ปค๋ฏธ์
์์ฑ ํผ ์์ฑ ๋ฐ ์ ์ถ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ ์ ์ฅ ํ์ธ
- ์ด๋ฏธ์ง ํ์ผ์ด storage/app/public/references์ ์ ์ฅ๋๋์ง ํ์ธ
- ์ฌ๋ณผ๋ฆญ ๋งํฌ ์์ฑ: `php artisan storage:link`
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ณ ๊ฐ์ด ์ปค๋ฏธ์ ์๋ขฐ์๋ฅผ ์์ฑํ๊ณ ์ ์ถํ ์ ์๋ ํผ์ด ์์ฑ๋ฉ๋๋ค. ์ฐธ๊ณ ์ด๋ฏธ์ง ์ ๋ก๋ ๊ธฐ๋ฅ์ด ์๋ํ๋ฉฐ, ์ ์ถ ํ ๋์๋ณด๋์์ ์๋ขฐ ๋ชฉ๋ก์ ํ์ธํ ์ ์์ต๋๋ค. FormRequest๋ก ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง๋ฉฐ, ์๋ฌ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค.
**๋ชฉํ**: ํฌ๋ฆฌ์์ดํฐ๊ฐ ์๋ขฐ๋ฅผ ๊ฒํ ํ๊ณ ๊ฒฌ์ ์๋ฅผ ๋ฐ์กํ๋ ๊ธฐ๋ฅ ๊ตฌํ
๋ค์์ ์ํํ์ธ์:
1. **QuoteController ์์ฑ**:
- `php artisan make:controller QuoteController`
2. **๋ผ์ฐํธ ์ถ๊ฐ** (routes/web.php):
```php
Route::middleware(['auth'])->group(function () {
Route::post('commissions/{commission}/quotes', [QuoteController::class, 'store'])->name('quotes.store');
Route::patch('quotes/{quote}/accept', [QuoteController::class, 'accept'])->name('quotes.accept');
Route::patch('quotes/{quote}/reject', [QuoteController::class, 'reject'])->name('quotes.reject');
});
```
3. **์ปค๋ฏธ์
์์ธ ํ์ด์ง** (resources/views/commissions/show.blade.php):
- ์ปค๋ฏธ์
์ ๋ณด ํ์:
- ์ ๋ชฉ, ์ ํ, ์์ธ ์ค๋ช
, ์์ฐ ๋ฒ์, ํฌ๋ง ๋ฉํ์ผ
- ์ฐธ๊ณ ์ด๋ฏธ์ง ๊ฐค๋ฌ๋ฆฌ (์๋ ๊ฒฝ์ฐ)
- ํ์ฌ ์ํ ๋ฐฐ์ง (pending, quoted, in_progress, completed)
- **ํฌ๋ฆฌ์์ดํฐ ์ ์ฉ ์น์
** (status='pending'์ผ ๋๋ง ํ์):
- ๊ฒฌ์ ์ ์์ฑ ํผ:
- ๊ฒฌ์ ๊ธ์ก (input number, required)
- ์์ ์์ ์ผ์ (input number, required)
- ๊ฒฌ์ ์ค๋ช
(textarea, required)
- "๊ฒฌ์ ์ ๋ฐ์ก" ๋ฒํผ
- **๊ณ ๊ฐ ์ ์ฉ ์น์
** (status='quoted'์ผ ๋๋ง ํ์):
- ๋ฐ์ ๊ฒฌ์ ์ ๋ชฉ๋ก (๊ฐ๊ฒฉ, ์์ ์ผ์, ์ค๋ช
, ํฌ๋ฆฌ์์ดํฐ ์ ๋ณด)
- ๊ฐ ๊ฒฌ์ ์๋ง๋ค "์น์ธ" / "๊ฑฐ์ " ๋ฒํผ
- Alpine.js๋ก ๊ฒฌ์ ์ ํผ ํ ๊ธ ๊ธฐ๋ฅ ์ถ๊ฐ
4. **QuoteController@store ๋ฉ์๋**:
- FormRequest ์์ฑ: `php artisan make:request StoreQuoteRequest`
- ๊ฒ์ฆ ๊ท์น:
- price: required, numeric, min:1
- estimated_days: required, integer, min:1
- message: required, min:20
- ๋ก์ง:
- ํฌ๋ฆฌ์์ดํฐ๋ง ๊ฒฌ์ ์ ์์ฑ ๊ฐ๋ฅ ํ์ธ (role='creator')
- Quote ๋ ์ฝ๋ ์์ฑ (creator_id๋ auth()->id())
- Commission ์ํ๋ฅผ 'quoted'๋ก ์
๋ฐ์ดํธ
- ๊ณ ๊ฐ์๊ฒ ์ด๋ฉ์ผ ๋ฐ์ก (Queue ์ฌ์ฉ)
- ์ฑ๊ณต ์ "๊ฒฌ์ ์๊ฐ ๋ฐ์ก๋์์ต๋๋ค" ๋ฉ์์ง
5. **QuoteController@accept ๋ฉ์๋**:
- ๋ก์ง:
- ๊ณ ๊ฐ๋ง ์น์ธ ๊ฐ๋ฅ ํ์ธ (commission์ client_id์ auth()->id() ์ผ์น)
- Quote ์ํ๋ฅผ 'accepted'๋ก ๋ณ๊ฒฝ
- Commission์ creator_id๋ฅผ ๊ฒฌ์ ์ ์์ฑ์๋ก ์ค์
- Commission ์ํ๋ฅผ 'in_progress'๋ก ๋ณ๊ฒฝ
- ๊ธฐ๋ณธ ์ํฌํ๋ก์ฐ ๋จ๊ณ ์๋ ์์ฑ:
- "์ค์ผ์น" (order=1, status='in_progress')
- "์ด์" (order=2, status='pending')
- "์์ " (order=3, status='pending')
- "์ต์ข
๋ณธ" (order=4, status='pending')
- ํฌ๋ฆฌ์์ดํฐ์๊ฒ "๊ณ์ฝ ์ฑ์ฌ" ์ด๋ฉ์ผ ๋ฐ์ก
- ์ฑ๊ณต ์ "๊ฒฌ์ ์๊ฐ ์น์ธ๋์์ต๋๋ค. ์์
์ด ์์๋ฉ๋๋ค!" ๋ฉ์์ง
6. **QuoteController@reject ๋ฉ์๋**:
- Quote ์ํ๋ฅผ 'rejected'๋ก ๋ณ๊ฒฝ
- ํฌ๋ฆฌ์์ดํฐ์๊ฒ ๊ฑฐ์ ์๋ฆผ ์ด๋ฉ์ผ ๋ฐ์ก
7. **Mailable ์์ฑ**:
- `php artisan make:mail QuoteSentMail --markdown=emails.quote-sent`
- `php artisan make:mail QuoteAcceptedMail --markdown=emails.quote-accepted`
- ๊ฐ ๋ฉ์ผ์ ์ปค๋ฏธ์
์ ๋ณด, ๊ฒฌ์ ์ ๋ณด ํฌํจ
8. **Queue ์ค์ **:
- .env์์ QUEUE_CONNECTION=database ์ค์
- `php artisan queue:table`
- `php artisan migrate`
- ์ด๋ฉ์ผ ๋ฐ์ก ์ `Mail::to($user)->queue(new QuoteSentMail($quote))`
9. **ํ
์คํธ**:
- ํฌ๋ฆฌ์์ดํฐ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ํ pending ์ปค๋ฏธ์
์ ๊ฒฌ์ ์ ์์ฑ
- ๊ณ ๊ฐ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ํ ๊ฒฌ์ ์ ์น์ธ
- ์ํฌํ๋ก์ฐ ๋จ๊ณ๊ฐ ์๋ ์์ฑ๋๋์ง ํ์ธ
- Queue ์์ปค ์คํ: `php artisan queue:work`
- ์ด๋ฉ์ผ์ด ๋ฐ์ก๋๋์ง ํ์ธ (Mailtrap ์ฌ์ฉ ๊ถ์ฅ)
๊ธฐ๋ ๊ฒฐ๊ณผ: ํฌ๋ฆฌ์์ดํฐ๊ฐ ์๋ขฐ์ ๋ํ ๊ฒฌ์ ์๋ฅผ ์์ฑํ์ฌ ๋ฐ์กํ ์ ์์ผ๋ฉฐ, ๊ณ ๊ฐ์ด ๊ฒฌ์ ์๋ฅผ ์น์ธํ๋ฉด ๊ณ์ฝ์ด ์ฑ์ฌ๋๊ณ ์์ ๋จ๊ณ๊ฐ ์๋ ์์ฑ๋ฉ๋๋ค. ์ด๋ฉ์ผ ์๋ฆผ์ด Queue๋ฅผ ํตํด ๋น๋๊ธฐ๋ก ๋ฐ์ก๋ฉ๋๋ค.
**๋ชฉํ**: ์์
๋จ๊ณ๋ณ ์งํ ์ํฉ ๊ด๋ฆฌ ๋ฐ ํ์ผ ์
๋ก๋ ๊ธฐ๋ฅ ๊ตฌํ (Kanban ๋ณด๋ ์คํ์ผ)
๋ค์์ ์ํํ์ธ์:
1. **WorkflowStageController ์์ฑ**:
- `php artisan make:controller WorkflowStageController`
2. **๋ผ์ฐํธ ์ถ๊ฐ** (routes/web.php):
```php
Route::middleware(['auth'])->group(function () {
Route::patch('workflow-stages/{stage}/status', [WorkflowStageController::class, 'updateStatus'])->name('workflow-stages.update-status');
Route::post('workflow-stages/{stage}/files', [WorkflowStageController::class, 'uploadFile'])->name('workflow-stages.upload-file');
});
```
3. **์ปค๋ฏธ์
์์ธ ํ์ด์ง ํ์ฅ** (resources/views/commissions/show.blade.php):
- **์ํฌํ๋ก์ฐ ๋ณด๋ ์น์
** (status='in_progress'์ผ ๋๋ง ํ์):
- Kanban ์คํ์ผ ๋ณด๋ ๋ ์ด์์ (Tailwind CSS Grid ์ฌ์ฉ)
- ๊ฐ ๋จ๊ณ๋ฅผ ์นด๋๋ก ํ์:
- ๋จ๊ณ๋ช
(์: "์ค์ผ์น")
- ์ํ ๋ฐฐ์ง (pending, in_progress, completed)
- ์
๋ก๋๋ ํ์ผ ๋ชฉ๋ก (์ธ๋ค์ผ + ํ์ผ๋ช
+ ๋ค์ด๋ก๋ ๋งํฌ)
- ํ์ผ ์
๋ก๋ ๋ฒํผ (ํฌ๋ฆฌ์์ดํฐ๋ง ํ์)
- ์ํ ๋ณ๊ฒฝ ๋ฒํผ (ํฌ๋ฆฌ์์ดํฐ๋ง ํ์):
- pending โ "์์
์์" (in_progress๋ก ๋ณ๊ฒฝ)
- in_progress โ "์๋ฃ" (completed๋ก ๋ณ๊ฒฝ, ๋ค์ ๋จ๊ณ๋ฅผ in_progress๋ก)
- Alpine.js๋ก ํ์ผ ์
๋ก๋ ๋ชจ๋ฌ ๊ตฌํ
- ๋๋๊ทธ์ค๋๋กญ ํ์ผ ์
๋ก๋ ์์ญ ์ถ๊ฐ
4. **WorkflowStageController@updateStatus ๋ฉ์๋**:
- ๋ก์ง:
- ํฌ๋ฆฌ์์ดํฐ๋ง ์ํ ๋ณ๊ฒฝ ๊ฐ๋ฅ ํ์ธ
- ํ์ฌ ๋จ๊ณ ์ํ ์
๋ฐ์ดํธ
- completed๋ก ๋ณ๊ฒฝ ์:
- completed_at ํ์์คํฌํ ๊ธฐ๋ก
- ๋ค์ ์์ ๋จ๊ณ๋ฅผ ์๋์ผ๋ก 'in_progress'๋ก ๋ณ๊ฒฝ
- ๋ชจ๋ ๋จ๊ณ๊ฐ ์๋ฃ๋๋ฉด Commission ์ํ๋ฅผ 'completed'๋ก ๋ณ๊ฒฝ
- ๊ณ ๊ฐ์๊ฒ ๋จ๊ณ ์๋ฃ ์๋ฆผ ์ด๋ฉ์ผ ๋ฐ์ก
- ์ฑ๊ณต ์ "๋จ๊ณ๊ฐ ์
๋ฐ์ดํธ๋์์ต๋๋ค" ๋ฉ์์ง
5. **WorkflowStageController@uploadFile ๋ฉ์๋**:
- FormRequest ์์ฑ: `php artisan make:request UploadStageFileRequest`
- ๊ฒ์ฆ ๊ท์น:
- file: required, mimes:jpg,png,pdf,zip,psd,ai, max:20480 (20MB)
- ๋ก์ง:
- ํฌ๋ฆฌ์์ดํฐ๋ง ์
๋ก๋ ๊ฐ๋ฅ ํ์ธ
- ํ์ผ์ storage/app/public/commission-files/{commission_id}์ ์ ์ฅ
- CommissionFile ๋ ์ฝ๋ ์์ฑ:
- file_path, file_name, file_size ์ ์ฅ
- version์ ํด๋น ๋จ๊ณ์ ๊ธฐ์กด ํ์ผ ๊ฐ์ + 1
- ๊ณ ๊ฐ์๊ฒ "์ ํ์ผ์ด ์
๋ก๋๋์์ต๋๋ค" ์ด๋ฉ์ผ ๋ฐ์ก
- ์ฑ๊ณต ์ "ํ์ผ์ด ์
๋ก๋๋์์ต๋๋ค" ๋ฉ์์ง
6. **ํ์ผ ๋ค์ด๋ก๋ ๋ผ์ฐํธ ์ถ๊ฐ**:
```php
Route::get('commission-files/{file}/download', [CommissionFileController::class, 'download'])->name('commission-files.download');
```
- CommissionFileController@download ๋ฉ์๋:
- Policy๋ก ๊ถํ ํ์ธ (์ปค๋ฏธ์
์ ๊ณ ๊ฐ ๋๋ ํฌ๋ฆฌ์์ดํฐ๋ง ๋ค์ด๋ก๋ ๊ฐ๋ฅ)
- `return Storage::download($file->file_path, $file->file_name);`
7. **Policy ์์ฑ**:
- `php artisan make:policy CommissionPolicy --model=Commission`
- update ๋ฉ์๋: ์ปค๋ฏธ์
์ ํฌ๋ฆฌ์์ดํฐ๋ง ์์ ๊ฐ๋ฅ
- view ๋ฉ์๋: ์ปค๋ฏธ์
์ ๊ณ ๊ฐ ๋๋ ํฌ๋ฆฌ์์ดํฐ๋ง ์กฐํ ๊ฐ๋ฅ
- AuthServiceProvider์ ๋ฑ๋ก
8. **Mailable ์์ฑ**:
- `php artisan make:mail StageCompletedMail --markdown=emails.stage-completed`
- `php artisan make:mail FileUploadedMail --markdown=emails.file-uploaded`
9. **UI ๊ฐ์ **:
- ๊ฐ ๋จ๊ณ ์นด๋์ ์งํ๋ฅ ๋ฐ ์ถ๊ฐ (completed ๋จ๊ณ ๊ฐ์ / ์ ์ฒด ๋จ๊ณ ๊ฐ์)
- ์๋ฃ๋ ๋จ๊ณ๋ ์ด๋ก์ ๋ฐฐ๊ฒฝ, ์งํ ์ค์ ํ๋์, ๋๊ธฐ ์ค์ ํ์์ผ๋ก ํ์
- ํ์ผ ์ธ๋ค์ผ (์ด๋ฏธ์ง์ธ ๊ฒฝ์ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ๊ทธ ์ธ๋ ํ์ผ ์์ด์ฝ)
10. **ํ
์คํธ**:
- ํฌ๋ฆฌ์์ดํฐ ๊ณ์ ์ผ๋ก ๊ฐ ๋จ๊ณ์ ํ์ผ ์
๋ก๋
- ๋จ๊ณ ์ํ ๋ณ๊ฒฝ ๋ฐ ์๋ ์งํ ํ์ธ
- ๊ณ ๊ฐ ๊ณ์ ์ผ๋ก ์
๋ก๋๋ ํ์ผ ๋ค์ด๋ก๋
- ๋ชจ๋ ๋จ๊ณ ์๋ฃ ์ ์ปค๋ฏธ์
์ํ๊ฐ 'completed'๋ก ๋ณ๊ฒฝ๋๋์ง ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ์์ ๋จ๊ณ๋ณ๋ก ํ์ผ์ ์ ๋ก๋ํ๊ณ ์ํ๋ฅผ ๊ด๋ฆฌํ ์ ์๋ Kanban ์คํ์ผ ๋ณด๋๊ฐ ์์ฑ๋ฉ๋๋ค. ํฌ๋ฆฌ์์ดํฐ๊ฐ ๋จ๊ณ๋ฅผ ์๋ฃํ๋ฉด ์๋์ผ๋ก ๋ค์ ๋จ๊ณ๊ฐ ํ์ฑํ๋๋ฉฐ, ๊ณ ๊ฐ์๊ฒ ์ด๋ฉ์ผ ์๋ฆผ์ด ๋ฐ์ก๋ฉ๋๋ค. Policy๋ฅผ ํตํด ๊ถํ์ด ์ ์ด๋ฉ๋๋ค.
**๋ชฉํ**: ๊ณ ๊ฐ ํผ๋๋ฐฑ ์์คํ
๋ฐ ๋๊ธ ๊ธฐ๋ฅ ๊ตฌํ
๋ค์์ ์ํํ์ธ์:
1. **FeedbackController ์์ฑ**:
- `php artisan make:controller FeedbackController`
2. **๋ผ์ฐํธ ์ถ๊ฐ** (routes/web.php):
```php
Route::middleware(['auth'])->group(function () {
Route::post('commissions/{commission}/feedbacks', [FeedbackController::class, 'store'])->name('feedbacks.store');
Route::delete('feedbacks/{feedback}', [FeedbackController::class, 'destroy'])->name('feedbacks.destroy');
});
```
3. **์ปค๋ฏธ์
์์ธ ํ์ด์ง์ ํผ๋๋ฐฑ ์น์
์ถ๊ฐ** (resources/views/commissions/show.blade.php):
- ๊ฐ ์ํฌํ๋ก์ฐ ๋จ๊ณ ์นด๋ ํ๋จ์ "ํผ๋๋ฐฑ" ํญ ์ถ๊ฐ
- ํผ๋๋ฐฑ ๋ชฉ๋ก ํ์:
- ์์ฑ์ ์ด๋ฆ (๊ณ ๊ฐ/ํฌ๋ฆฌ์์ดํฐ ๊ตฌ๋ถ ๋ฐฐ์ง)
- ๋๊ธ ๋ด์ฉ
- ์์ฑ ์๊ฐ (Carbon diffForHumans ์ฌ์ฉ)
- ์ญ์ ๋ฒํผ (๋ณธ์ธ๋ง ํ์)
- ํผ๋๋ฐฑ ์์ฑ ํผ:
- textarea (placeholder: "์ด ๋จ๊ณ์ ๋ํ ํผ๋๋ฐฑ์ ๋จ๊ฒจ์ฃผ์ธ์")
- "ํผ๋๋ฐฑ ๋ฑ๋ก" ๋ฒํผ
- Alpine.js๋ก ๋๊ธ ํ ๊ธ ๋ฐ ์ค์๊ฐ ์ถ๊ฐ ๊ธฐ๋ฅ
4. **FeedbackController@store ๋ฉ์๋**:
- FormRequest ์์ฑ: `php artisan make:request StoreFeedbackRequest`
- ๊ฒ์ฆ ๊ท์น:
- comment: required, min:10, max:1000
- workflow_stage_id: nullable, exists:workflow_stages,id
- ๋ก์ง:
- Feedback ๋ ์ฝ๋ ์์ฑ (user_id๋ auth()->id())
- ์๋๋ฐฉ(๊ณ ๊ฐโํฌ๋ฆฌ์์ดํฐ)์๊ฒ "์ ํผ๋๋ฐฑ์ด ๋ฑ๋ก๋์์ต๋๋ค" ์ด๋ฉ์ผ ๋ฐ์ก
- AJAX ์์ฒญ์ผ๋ก ์ฒ๋ฆฌ (JSON ์๋ต)
- ์ฑ๊ณต ์ ์ ํผ๋๋ฐฑ HTML ๋ฐํ
5. **FeedbackController@destroy ๋ฉ์๋**:
- Policy๋ก ๋ณธ์ธ๋ง ์ญ์ ๊ฐ๋ฅ ํ์ธ
- Feedback ๋ ์ฝ๋ ์ญ์
- AJAX ์์ฒญ์ผ๋ก ์ฒ๋ฆฌ
6. **Blade ์ปดํฌ๋ํธ ์์ฑ**:
- `php artisan make:component FeedbackItem`
- resources/views/components/feedback-item.blade.php:
- ํผ๋๋ฐฑ ํ ๊ฐ๋ฅผ ํ์ํ๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ
- props: $feedback, $canDelete
7. **Mailable ์์ฑ**:
- `php artisan make:mail FeedbackReceivedMail --markdown=emails.feedback-received`
8. **์ค์๊ฐ ์
๋ฐ์ดํธ (์ ํ์ฌํญ)**:
- Alpine.js๋ก 5์ด๋ง๋ค ์ ํผ๋๋ฐฑ ์๋ ๋ก๋ (fetch API ์ฌ์ฉ)
- ๋๋ Laravel Echo + Pusher๋ก ์ค์๊ฐ ์๋ฆผ (๋ณด๋์ค)
9. **UI ๊ฐ์ **:
- ํผ๋๋ฐฑ ๋ชฉ๋ก์ ์๊ฐ์ ์ ๋ ฌ (์ต์ ์)
- ๊ณ ๊ฐ ํผ๋๋ฐฑ์ ํ๋์, ํฌ๋ฆฌ์์ดํฐ ํผ๋๋ฐฑ์ ์ด๋ก์ ๋ฐฐ๊ฒฝ
- ํผ๋๋ฐฑ ๊ฐ์ ๋ฐฐ์ง ํ์ (๊ฐ ๋จ๊ณ๋ณ)
10. **ํ
์คํธ**:
- ๊ณ ๊ฐ ๊ณ์ ์ผ๋ก ๊ฐ ๋จ๊ณ์ ํผ๋๋ฐฑ ์์ฑ
- ํฌ๋ฆฌ์์ดํฐ ๊ณ์ ์ผ๋ก ๋ต๋ณ ์์ฑ
- ํผ๋๋ฐฑ ์ญ์ ๊ธฐ๋ฅ ํ์ธ
- ์ด๋ฉ์ผ ์๋ฆผ ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ฐ ์์ ๋จ๊ณ์ ๊ณ ๊ฐ๊ณผ ํฌ๋ฆฌ์์ดํฐ๊ฐ ๋๊ธ ํํ๋ก ํผ๋๋ฐฑ์ ์ฃผ๊ณ ๋ฐ์ ์ ์๋ ์์คํ ์ด ์์ฑ๋ฉ๋๋ค. AJAX๋ก ํ์ด์ง ์๋ก๊ณ ์นจ ์์ด ํผ๋๋ฐฑ์ด ์ถ๊ฐ/์ญ์ ๋๋ฉฐ, ์๋๋ฐฉ์๊ฒ ์ด๋ฉ์ผ ์๋ฆผ์ด ๋ฐ์ก๋ฉ๋๋ค.
**๋ชฉํ**: ๋์๋ณด๋ ํต๊ณ ๋ฐ ์์ต ๋ถ์ ๊ธฐ๋ฅ ๊ตฌํ
๋ค์์ ์ํํ์ธ์:
1. **DashboardController ์์ฑ**:
- `php artisan make:controller DashboardController`
2. **๋ผ์ฐํธ ์์ ** (routes/web.php):
```php
Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth', 'verified'])->name('dashboard');
```
3. **DashboardController@index ๋ฉ์๋**:
- ์ญํ ๋ณ ๋ฐ์ดํฐ ๋ก๋:
**ํฌ๋ฆฌ์์ดํฐ ๋์๋ณด๋**:
- ํต๊ณ:
- ์ด ์์ต: ์น์ธ๋ ๊ฒฌ์ ์(status='accepted')์ price ํฉ๊ณ
- ์งํ ์ค์ธ ์ปค๋ฏธ์
๊ฐ์ (status='in_progress')
- ์๋ฃ๋ ์ปค๋ฏธ์
๊ฐ์ (status='completed')
- ํ๊ท ์์
์์ ์๊ฐ: (์๋ฃ์ผ - ์น์ธ์ผ)์ ํ๊ท
- ์ฐจํธ ๋ฐ์ดํฐ:
- ์๋ณ ์์ต (์ต๊ทผ 6๊ฐ์)
- ์์
์ ํ๋ณ ์ปค๋ฏธ์
๊ฐ์ (ํ์ด ์ฐจํธ์ฉ)
- ์ต๊ทผ ํผ๋๋ฐฑ 3๊ฐ
- ๋ค๊ฐ์ค๋ ๋ง๊ฐ์ผ (deadline ๊ธฐ์ค 7์ผ ์ด๋ด)
**๊ณ ๊ฐ ๋์๋ณด๋**:
- ํต๊ณ:
- ์ด ์๋ขฐ ๊ฐ์
- ์งํ ์ค์ธ ์ปค๋ฏธ์
๊ฐ์
- ์๋ฃ๋ ์ปค๋ฏธ์
๊ฐ์
- ์ด ์ง์ถ ๊ธ์ก
- ์ต๊ทผ ์๋ขฐ ๋ชฉ๋ก (์ํ๋ณ ํํฐ ํญ)
- ์ต๊ทผ ๋ฐ์ ํผ๋๋ฐฑ 3๊ฐ
4. **๋์๋ณด๋ ๋ทฐ ๋ฆฌํฉํ ๋ง** (resources/views/dashboard.blade.php):
- Tailwind CSS๋ก ์นด๋ ๋ ์ด์์ ๊ตฌ์ฑ
- ํต๊ณ ์นด๋:
- ์์ด์ฝ + ์ซ์ + ๋ ์ด๋ธ
- ์ ์ ๋๋น ์ฆ๊ฐ๋ฅ ํ์ (์ ํ์ฌํญ)
- Chart.js ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐ:
- CDN: `<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>`
- ์๋ณ ์์ต ์ ๊ทธ๋ํ
- ์์
์ ํ๋ณ ๋๋ ์ฐจํธ
- ์ต๊ทผ ํ๋ ํ์๋ผ์ธ (ํผ๋๋ฐฑ, ํ์ผ ์
๋ก๋, ๋จ๊ณ ์๋ฃ ๋ฑ)
5. **Chart.js ๋ฐ์ดํฐ ์ ๋ฌ**:
- ์ปจํธ๋กค๋ฌ์์ JSON ๋ฐ์ดํฐ๋ฅผ ๋ทฐ๋ก ์ ๋ฌ
- Blade์์ JavaScript ๋ณ์๋ก ๋ณํ:
```blade
<script>
const monthlyRevenue = @json($monthlyRevenue);
const typeDistribution = @json($typeDistribution);
</script>
```
6. **ํํฐ ๋ฐ ์ ๋ ฌ ๊ธฐ๋ฅ**:
- ์ปค๋ฏธ์
๋ชฉ๋ก์ ํํฐ ์ถ๊ฐ:
- ์ํ๋ณ (pending, in_progress, completed)
- ์์
์ ํ๋ณ
- ๋ ์ง ๋ฒ์
- Alpine.js๋ก ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ํํฐ๋ง
7. **๊ฒ์ ๊ธฐ๋ฅ**:
- ์ปค๋ฏธ์
์ ๋ชฉ/์ค๋ช
๊ฒ์ (LIKE ์ฟผ๋ฆฌ)
- ๊ฒ์ ์
๋ ฅ ์ debounce ์ฒ๋ฆฌ (Alpine.js)
8. **ํ์ด์ง๋ค์ด์
**:
- ์ปค๋ฏธ์
๋ชฉ๋ก์ ํ์ด์ง๋ค์ด์
์ถ๊ฐ (Laravel paginate)
- Tailwind CSS ์คํ์ผ ํ์ด์ง๋ค์ด์
์ปดํฌ๋ํธ
9. **๋ฐ์ํ ๋์์ธ**:
- ๋ชจ๋ฐ์ผ์์๋ ์ฐจํธ๊ฐ ์ ์ ํ์๋๋๋ก ์กฐ์
- ์นด๋ ๋ ์ด์์์ ๋ชจ๋ฐ์ผ์์๋ 1์ด, ํ๋ธ๋ฆฟ 2์ด, ๋ฐ์คํฌํฑ 3์ด๋ก ๋ฐ์ํ ๊ตฌ์ฑ
10. **ํ
์คํธ**:
- ๋๋ฏธ ๋ฐ์ดํฐ๋ก ์ฐจํธ๊ฐ ์ ์ ๋ ๋๋ง๋๋์ง ํ์ธ
- ํํฐ ๋ฐ ๊ฒ์ ๊ธฐ๋ฅ ๋์ ํ์ธ
- ๋ชจ๋ฐ์ผ ๋ทฐ ํ
์คํธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ญํ ๋ณ๋ก ๋ง์ถคํ ๋์๋ณด๋๊ฐ ์์ฑ๋ฉ๋๋ค. ํฌ๋ฆฌ์์ดํฐ๋ ์์ต ํต๊ณ์ ์ฐจํธ๋ฅผ ํ์ธํ ์ ์๊ณ , ๊ณ ๊ฐ์ ์๋ขฐ ํํฉ์ ํ๋์ ๋ณผ ์ ์์ต๋๋ค. Chart.js๋ก ์๊ฐํ๋ ํต๊ณ๊ฐ ํ์๋๋ฉฐ, ํํฐ/๊ฒ์ ๊ธฐ๋ฅ์ด ์๋ํฉ๋๋ค.
**๋ชฉํ**: ์ด๋ฉ์ผ ์๋ฆผ ์๋ํ ๋ฐ Laravel Scheduler ์ค์
๋ค์์ ์ํํ์ธ์:
1. **๋ชจ๋ Mailable ํด๋์ค ์์ฑ**:
- ์ง๊ธ๊น์ง ์์ฑํ Mailable ํด๋์ค๋ค์ ๋งํฌ๋ค์ด ํ
ํ๋ฆฟ ์์ฑ:
- QuoteSentMail: ๊ฒฌ์ ์ ์ ๋ณด, ์ปค๋ฏธ์
์์ธ ๋งํฌ ํฌํจ
- QuoteAcceptedMail: ๊ณ์ฝ ์ฑ์ฌ ์ถํ, ์ํฌํ๋ก์ฐ ๋งํฌ
- StageCompletedMail: ์๋ฃ๋ ๋จ๊ณ๋ช
, ๋ค์ ๋จ๊ณ ์๋ด
- FileUploadedMail: ์
๋ก๋๋ ํ์ผ ์ ๋ณด, ๋ค์ด๋ก๋ ๋งํฌ
- FeedbackReceivedMail: ํผ๋๋ฐฑ ๋ด์ฉ, ๋ต๋ณํ๊ธฐ ๋งํฌ
- ๊ฐ ๋ฉ์ผ์ ์ปค๋ฏธ์
์ ๋ชฉ, ๊ณ ์ ๋ฒํธ(uuid), CTA ๋ฒํผ ํฌํจ
- ๋ธ๋๋ ์ปฌ๋ฌ ์ ์ฉ (config/mail.php์์ theme ์ค์ )
2. **์ปค์คํ
Notification ์์ฑ** (์ ํ์ฌํญ):
- `php artisan make:notification DeadlineReminderNotification`
- ๋ง๊ฐ์ผ 3์ผ ์ ํฌ๋ฆฌ์์ดํฐ์๊ฒ ์๋ฆผ
- ์ด๋ฉ์ผ + ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฑ๋ ์ฌ์ฉ
3. **Laravel Scheduler ์ค์ **:
- app/Console/Kernel.php์ schedule ๋ฉ์๋์ ์์
๋ฑ๋ก:
```php
protected function schedule(Schedule $schedule)
{
// ๋งค์ผ ์ค์ 9์์ ๋ง๊ฐ์ผ 3์ผ ์ ์ธ ์ปค๋ฏธ์
ํ์ธ
$schedule->call(function () {
$commissions = Commission::where('status', 'in_progress')
->whereDate('deadline', '=', now()->addDays(3)->toDateString())
->with('creator')
->get();
foreach ($commissions as $commission) {
$commission->creator->notify(new DeadlineReminderNotification($commission));
}
})->dailyAt('09:00');
}
```
4. **Cron ์ค์ ์๋ด ๋ฌธ์ ์์ฑ** (README.md):
- ํ๋ก๋์
์๋ฒ์์ ์ค์ผ์ค๋ฌ ์คํ ๋ฐฉ๋ฒ:
```bash
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
```
- ๋ก์ปฌ ๊ฐ๋ฐ ์: `php artisan schedule:work`
5. **Queue ์์ปค ์๋ ์ฌ์์**:
- Supervisor ์ค์ ์์ ์์ฑ (README.md์ ํฌํจ)
- Laravel Horizon ๋์ ๊ฐ๋จํ queue:work ์ฌ์ฉ
6. **์ด๋ฉ์ผ ํ
์คํธ**:
- Mailtrap ๋๋ MailHog์ผ๋ก ๋ชจ๋ ์ด๋ฉ์ผ ํ
ํ๋ฆฟ ํ์ธ
- ๊ฐ ์ด๋ฒคํธ๋ณ๋ก ์ด๋ฉ์ผ ๋ฐ์ก ํ
์คํธ:
- ๊ฒฌ์ ์ ๋ฐ์ก
- ๊ฒฌ์ ์ ์น์ธ
- ๋จ๊ณ ์๋ฃ
- ํ์ผ ์
๋ก๋
- ํผ๋๋ฐฑ ๋ฑ๋ก
- ๋ง๊ฐ์ผ ๋ฆฌ๋ง์ธ๋
7. **์๋ฌ ์ฒ๋ฆฌ**:
- ์ด๋ฉ์ผ ๋ฐ์ก ์คํจ ์ ๋ก๊ทธ ๊ธฐ๋ก
- Queue ์คํจ ์ฒ๋ฆฌ (failed_jobs ํ
์ด๋ธ ํ์ฉ)
- `php artisan queue:failed` ๋ช
๋ น์ผ๋ก ์คํจํ ์์
ํ์ธ
8. **ํ๊ฒฝ๋ณ์ ์ค์ ๊ฐ์ด๋** (.env.example ์
๋ฐ์ดํธ):
```
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@commissionmanager.test
MAIL_FROM_NAME="Commission Manager"
QUEUE_CONNECTION=database
```
9. **์๋ฆผ ํ๊ฒฝ์ค์ ์ถ๊ฐ**:
- ์ฌ์ฉ์ ํ๋กํ์ "์ด๋ฉ์ผ ์๋ฆผ ์์ ์ฌ๋ถ" ์ค์ ์ถ๊ฐ (์ ํ์ฌํญ)
- users ํ
์ด๋ธ์ email_notifications boolean ์ปฌ๋ผ ์ถ๊ฐ
- ์๋ฆผ ๋ฐ์ก ์ ์ฌ์ฉ์ ์ค์ ํ์ธ
10. **ํ
์คํธ**:
- Queue ์์ปค ์คํ ์ํ์์ ๊ฐ ์ด๋ฒคํธ ํธ๋ฆฌ๊ฑฐ
- ์ด๋ฉ์ผ์ด ์ ์ ๋ฐ์ก๋๋์ง ํ์ธ
- Scheduler ์๋ ํ
์คํธ (์๊ฐ์ ์กฐ์ํ์ฌ ํ์ธ)
- ์คํจํ ์์
์ฌ์๋: `php artisan queue:retry all`
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ชจ๋ ์ด๋ฉ์ผ ํ ํ๋ฆฟ์ด ์์ฑ๋๊ณ , Queue๋ฅผ ํตํด ๋น๋๊ธฐ๋ก ๋ฐ์ก๋ฉ๋๋ค. Laravel Scheduler๋ก ๋ง๊ฐ์ผ ๋ฆฌ๋ง์ธ๋๊ฐ ์๋ ๋ฐ์ก๋๋ฉฐ, ์๋ฌ ์ฒ๋ฆฌ ๋ฐ ์ฌ์๋ ๋ก์ง์ด ๊ตฌํ๋ฉ๋๋ค. ๊ฐ๋ฐ์๊ฐ ์ฝ๊ฒ ์ค์ ํ ์ ์๋๋ก ๋ฌธ์ํ๋ฉ๋๋ค.
**๋ชฉํ**: ์ฌ์ฉ์ ํ๋กํ ๊ด๋ฆฌ ๋ฐ ํฌ๋ฆฌ์์ดํฐ ํฌํธํด๋ฆฌ์ค ํ์ด์ง ๊ตฌํ
๋ค์์ ์ํํ์ธ์:
1. **ProfileController ํ์ฅ**:
- Breeze ๊ธฐ๋ณธ ํ๋กํ ์ปจํธ๋กค๋ฌ์ ์ถ๊ฐ ํ๋ ์
๋ฐ์ดํธ ๊ธฐ๋ฅ ์ถ๊ฐ
2. **ํ๋กํ ํธ์ง ํผ** (resources/views/profile/edit.blade.php):
- ๊ธฐ์กด Breeze ํผ์ ์ถ๊ฐ ํ๋:
- **ํฌ๋ฆฌ์์ดํฐ ์ ์ฉ**:
- ์๊ธฐ์๊ฐ (bio, textarea)
- ํฌํธํด๋ฆฌ์ค URL (portfolio_url, input url)
- ์๊ฐ๋น ์๊ธ (hourly_rate, input number)
- ํ๋กํ ์ฌ์ง ์
๋ก๋ (storage์ ์ ์ฅ)
- **๊ณตํต**:
- ์ด๋ฉ์ผ ์๋ฆผ ์์ ์ฌ๋ถ (checkbox)
- Alpine.js๋ก ์ญํ ๋ณ ํ๋ ํ ๊ธ
3. **ProfileController@update ํ์ฅ**:
- FormRequest ์์ฑ: `php artisan make:request UpdateProfileRequest`
- ๊ฒ์ฆ ๊ท์น:
- bio: nullable, max:500
- portfolio_url: nullable, url
- hourly_rate: nullable, numeric, min:0
- profile_photo: nullable, image, max:2048
- ํ๋กํ ์ฌ์ง ์
๋ก๋ ์ฒ๋ฆฌ:
- storage/app/public/profile-photos์ ์ ์ฅ
- ๊ธฐ์กด ์ฌ์ง์ด ์์ผ๋ฉด ์ญ์ ํ ์ ์ฌ์ง ์ ์ฅ
4. **ํฌ๋ฆฌ์์ดํฐ ํฌํธํด๋ฆฌ์ค ํ์ด์ง** (resources/views/creators/show.blade.php):
- ๊ณต๊ฐ ํ์ด์ง (์ธ์ฆ ๋ถํ์)
- ๋ผ์ฐํธ: `Route::get('/creators/{user}', [CreatorController::class, 'show'])->name('creators.show');`
- ํ์ ๋ด์ฉ:
- ํ๋กํ ์ฌ์ง, ์ด๋ฆ, ์๊ธฐ์๊ฐ
- ์๊ฐ๋น ์๊ธ, ํฌํธํด๋ฆฌ์ค ๋งํฌ
- ์๋ฃ๋ ์ปค๋ฏธ์
๊ฐค๋ฌ๋ฆฌ:
- ๊ณ ๊ฐ์ด ๊ณต๊ฐ ํ์ฉํ ์ปค๋ฏธ์
๋ง ํ์ (commissions ํ
์ด๋ธ์ is_public boolean ์ปฌ๋ผ ์ถ๊ฐ)
- ๊ฐ ์ปค๋ฏธ์
์ ์ต์ข
๊ฒฐ๊ณผ๋ฌผ ์ด๋ฏธ์ง (commission_files์์ workflow_stage์ order๊ฐ ๊ฐ์ฅ ๋์ ํ์ผ)
- ์์
์ ํ, ์๋ฃ์ผ ํ์
- ๊ณ ๊ฐ ๋ฆฌ๋ทฐ (๋ณด๋์ค ๊ธฐ๋ฅ)
- Tailwind CSS๋ก ๊ทธ๋ฆฌ๋ ๋ ์ด์์ ๊ฐค๋ฌ๋ฆฌ
- Lightbox ํจ๊ณผ (Alpine.js ๋๋ Fancybox ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
5. **CreatorController ์์ฑ**:
- `php artisan make:controller CreatorController`
- show ๋ฉ์๋:
- ํฌ๋ฆฌ์์ดํฐ ์ ๋ณด ๋ก๋ (role='creator'๋ง)
- ๊ณต๊ฐ ์ปค๋ฏธ์
๋ก๋ (is_public=true, status='completed')
- ์ต์ข
๊ฒฐ๊ณผ๋ฌผ ํ์ผ ๋ก๋ (eager loading)
6. **์ปค๋ฏธ์
๊ณต๊ฐ ์ค์ ๊ธฐ๋ฅ**:
- ์ปค๋ฏธ์
์์ธ ํ์ด์ง์ "ํฌํธํด๋ฆฌ์ค์ ๊ณต๊ฐ" ํ ๊ธ ์ถ๊ฐ (ํฌ๋ฆฌ์์ดํฐ ์ ์ฉ)
- AJAX๋ก is_public ๊ฐ ์
๋ฐ์ดํธ
- ๊ณ ๊ฐ ๋์ ํ์ ์ ๊ณ ๊ฐ์๊ฒ ์๋ฆผ ๋ฐ์ก (์ ํ์ฌํญ)
7. **ํฌ๋ฆฌ์์ดํฐ ๋ชฉ๋ก ํ์ด์ง** (resources/views/creators/index.blade.php):
- ๋ชจ๋ ํฌ๋ฆฌ์์ดํฐ ๋ชฉ๋ก ํ์
- ํํฐ:
- ์์
์ ํ (character_design, logo ๋ฑ)
- ์๊ฐ๋น ์๊ธ ๋ฒ์
- ์ ๋ ฌ:
- ์ต์ ๊ฐ์
์
- ์๋ฃ ์์
๋ง์ ์
- ๊ฐ ํฌ๋ฆฌ์์ดํฐ ์นด๋์ ํ๋กํ ์ฌ์ง, ์ด๋ฆ, ์๋ฃ ์์
๊ฐ์, ์๊ฐ๋น ์๊ธ ํ์
- ์นด๋ ํด๋ฆญ ์ ํฌํธํด๋ฆฌ์ค ํ์ด์ง๋ก ์ด๋
8. **๋ค๋น๊ฒ์ด์
๋ฉ๋ด ์
๋ฐ์ดํธ**:
- "ํฌ๋ฆฌ์์ดํฐ ๋๋ฌ๋ณด๊ธฐ" ๋งํฌ ์ถ๊ฐ
- ํฌ๋ฆฌ์์ดํฐ๋ "๋ด ํฌํธํด๋ฆฌ์ค" ๋งํฌ ์ถ๊ฐ
9. **SEO ์ต์ ํ** (์ ํ์ฌํญ):
- ํฌ๋ฆฌ์์ดํฐ ํฌํธํด๋ฆฌ์ค ํ์ด์ง์ ๋ฉํ ํ๊ทธ ์ถ๊ฐ
- Open Graph ์ด๋ฏธ์ง (ํ๋กํ ์ฌ์ง ์ฌ์ฉ)
10. **ํ
์คํธ**:
- ํฌ๋ฆฌ์์ดํฐ ํ๋กํ ํธ์ง ๋ฐ ํฌํธํด๋ฆฌ์ค URL ํ์ธ
- ์๋ฃ๋ ์ปค๋ฏธ์
์ ๊ณต๊ฐ๋ก ์ค์ ํ ํฌํธํด๋ฆฌ์ค ํ์ด์ง์ ํ์๋๋์ง ํ์ธ
- ํฌ๋ฆฌ์์ดํฐ ๋ชฉ๋ก ํ์ด์ง์์ ํํฐ/์ ๋ ฌ ๊ธฐ๋ฅ ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ํฌ๋ฆฌ์์ดํฐ๊ฐ ์์ ์ ํ๋กํ์ ํธ์งํ๊ณ , ์๋ฃ๋ ์์ ์ ํฌํธํด๋ฆฌ์ค๋ก ๊ณต๊ฐํ ์ ์์ต๋๋ค. ๊ณต๊ฐ ํฌํธํด๋ฆฌ์ค ํ์ด์ง์์ ๊ฐค๋ฌ๋ฆฌ ํํ๋ก ์์ ๋ฌผ์ด ํ์๋๋ฉฐ, ํฌ๋ฆฌ์์ดํฐ ๋ชฉ๋ก ํ์ด์ง์์ ํํฐ๋งํ์ฌ ์ฐพ์ ์ ์์ต๋๋ค.
**๋ชฉํ**: Seeder๋ก ๋๋ฏธ ๋ฐ์ดํฐ ์์ฑ ๋ฐ ํ
์คํธ ํ๊ฒฝ ๊ตฌ์ถ
๋ค์์ ์ํํ์ธ์:
1. **Factory ์์ฑ**:
- `php artisan make:factory CommissionFactory`
- `php artisan make:factory QuoteFactory`
- `php artisan make:factory WorkflowStageFactory`
- `php artisan make:factory CommissionFileFactory`
- `php artisan make:factory FeedbackFactory`
2. **Factory ์ ์**:
- **CommissionFactory**:
- title: ๋ฌด์์ ์ปค๋ฏธ์
์ ๋ชฉ (์: "์บ๋ฆญํฐ ๋์์ธ ์๋ขฐ", "ํ์ฌ ๋ก๊ณ ์ ์")
- type: ๋ฌด์์ enum ๊ฐ
- description: ์ต์ 100์ ์ด์์ ๋๋ฏธ ํ
์คํธ
- budget_range: "$100-$500" ํ์
- deadline: 7~30์ผ ํ ๋ฌด์์ ๋ ์ง
- status: ๋ฌด์์ (pending, quoted, in_progress, completed)
- uuid: Str::uuid()
- **QuoteFactory**:
- price: 100~1000 ์ฌ์ด ๋ฌด์์
- estimated_days: 3~21 ์ฌ์ด ๋ฌด์์
- message: ๋๋ฏธ ๊ฒฌ์ ์ค๋ช
- status: ๋ฌด์์ (pending, accepted, rejected)
- **WorkflowStageFactory**:
- name: ["์ค์ผ์น", "์ด์", "์์ ", "์ต์ข
๋ณธ"] ์ค ํ๋
- order: 1~4
- status: ๋ฌด์์
3. **DatabaseSeeder ์์ฑ** (database/seeders/DatabaseSeeder.php):
```php
public function run()
{
// ํฌ๋ฆฌ์์ดํฐ 1๋ช
$creator = User::factory()->create([
'name' => 'Jane Creator',
'email' => 'creator@example.com',
'role' => 'creator',
'bio' => 'Professional illustrator with 5 years of experience.',
'hourly_rate' => 50.00,
]);
// ๊ณ ๊ฐ 3๋ช
$clients = User::factory()->count(3)->create([
'role' => 'client',
]);
// ๊ฐ ๊ณ ๊ฐ๋น ์ปค๋ฏธ์
2๊ฐ์ฉ ์์ฑ
foreach ($clients as $client) {
$commissions = Commission::factory()->count(2)->create([
'client_id' => $client->id,
'creator_id' => $creator->id,
'status' => 'in_progress',
]);
foreach ($commissions as $commission) {
// ๊ฒฌ์ ์ ์์ฑ
Quote::factory()->create([
'commission_id' => $commission->id,
'creator_id' => $creator->id,
'status' => 'accepted',
]);
// ์ํฌํ๋ก์ฐ ๋จ๊ณ ์์ฑ
$stages = [
['name' => '์ค์ผ์น', 'order' => 1, 'status' => 'completed'],
['name' => '์ด์', 'order' => 2, 'status' => 'in_progress'],
['name' => '์์ ', 'order' => 3, 'status' => 'pending'],
['name' => '์ต์ข
๋ณธ', 'order' => 4, 'status' => 'pending'],
];
foreach ($stages as $stageData) {
$stage = WorkflowStage::factory()->create([
'commission_id' => $commission->id,
...$stageData,
]);
// ์๋ฃ๋ ๋จ๊ณ์ ํ์ผ ์ถ๊ฐ
if ($stageData['status'] === 'completed') {
CommissionFile::factory()->count(2)->create([
'commission_id' => $commission->id,
'workflow_stage_id' => $stage->id,
'uploader_id' => $creator->id,
]);
}
}
// ํผ๋๋ฐฑ ์์ฑ
Feedback::factory()->count(3)->create([
'commission_id' => $commission->id,
'user_id' => fake()->randomElement([$client->id, $creator->id]),
]);
}
}
}
```
4. **๋๋ฏธ ํ์ผ ์์ฑ**:
- storage/app/public/commission-files ํด๋์ ์ํ ์ด๋ฏธ์ง ํ์ผ ๋ฐฐ์น
- CommissionFileFactory์์ ์ค์ ํ์ผ ๊ฒฝ๋ก ์ฐธ์กฐ
5. **Seeder ์คํ**:
- `php artisan migrate:fresh --seed`
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ ํ ๋๋ฏธ ๋ฐ์ดํฐ ์์ฑ
6. **ํ
์คํธ ๊ณ์ ์๋ด** (README.md):
```markdown
## ํ
์คํธ ๊ณ์
- **ํฌ๋ฆฌ์์ดํฐ**: creator@example.com / password
- **๊ณ ๊ฐ 1**: (์๋ ์์ฑ๋ ์ด๋ฉ์ผ) / password
- **๊ณ ๊ฐ 2**: (์๋ ์์ฑ๋ ์ด๋ฉ์ผ) / password
- **๊ณ ๊ฐ 3**: (์๋ ์์ฑ๋ ์ด๋ฉ์ผ) / password
```
7. **ํ๊ฒฝ ์ค์ ๊ฒ์ฆ ์คํฌ๋ฆฝํธ**:
- Artisan ์ปค๋งจ๋ ์์ฑ: `php artisan make:command CheckEnvironment`
- ์คํ ์ ๋ค์ ํญ๋ชฉ ๊ฒ์ฆ:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
- Storage ์ฌ๋ณผ๋ฆญ ๋งํฌ
- Queue ์ค์
- Mail ์ค์
- ํ์ ๋๋ ํ ๋ฆฌ ๊ถํ
8. **ํ
์คํธ**:
- ๋๋ฏธ ๋ฐ์ดํฐ๋ก ๋ชจ๋ ๊ธฐ๋ฅ ๋์ ํ์ธ
- ํฌ๋ฆฌ์์ดํฐ/๊ณ ๊ฐ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํ์ฌ UI ํ์ธ
- ๋์๋ณด๋ ํต๊ณ๊ฐ ์ ์ ํ์๋๋์ง ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: Seeder๋ฅผ ํตํด ํฌ๋ฆฌ์์ดํฐ 1๋ช , ๊ณ ๊ฐ 3๋ช , ์ปค๋ฏธ์ 6๊ฐ, ์ํฌํ๋ก์ฐ ๋จ๊ณ, ํ์ผ, ํผ๋๋ฐฑ ๋ฑ ์์ ํ ๋๋ฏธ ๋ฐ์ดํฐ๊ฐ ์์ฑ๋ฉ๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ๋ฐ์๊ฐ ์ฆ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ ์คํธํ ์ ์์ต๋๋ค.
**๋ชฉํ**: ํ๋ก์ ํธ ๋ฌธ์ํ, ๋ฐฐํฌ ์ค๋น, ์ต์ข
ํ
์คํธ ๋ฐ ๋ณด์ ๊ฐํ
๋ค์์ ์ํํ์ธ์:
1. **README.md ์์ฑ**:
```markdown
# ๐จ Creator Commission Workflow Manager
ํฌ๋ฆฌ์์ดํฐ๋ฅผ ์ํ ์ปค๋ฏธ์
์์
์ ๊ณผ์ ๊ด๋ฆฌ ํ๋ซํผ
## ๊ธฐ๋ฅ
- โ
์ปค๋ฏธ์
์๋ขฐ ์์คํ
- โ
๊ฒฌ์ ์ ๋ฐ์ก & ์น์ธ
- โ
์์
๋จ๊ณ๋ณ ์ํฌํ๋ก์ฐ (Kanban ๋ณด๋)
- โ
ํ์ผ ์
๋ก๋ & ๋ฒ์ ๊ด๋ฆฌ
- โ
๊ณ ๊ฐ ํผ๋๋ฐฑ ์์คํ
- โ
์ด๋ฉ์ผ ์๋ฆผ ์๋ํ
- โ
๋์๋ณด๋ ํต๊ณ & ์ฐจํธ
- โ
ํฌ๋ฆฌ์์ดํฐ ํฌํธํด๋ฆฌ์ค ํ์ด์ง
## ๊ธฐ์ ์คํ
- Laravel 13.x
- Blade Templates
- Tailwind CSS
- Alpine.js
- Chart.js
- MySQL
## ์ค์น ๋ฐฉ๋ฒ
1. ๋ฆฌํฌ์งํ ๋ฆฌ ํด๋ก
2. `composer install`
3. `npm install && npm run build`
4. `.env` ์ค์ (๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ฉ์ผ)
5. `php artisan key:generate`
6. `php artisan migrate --seed`
7. `php artisan storage:link`
8. `php artisan serve`
## ํ
์คํธ ๊ณ์
- ํฌ๋ฆฌ์์ดํฐ: creator@example.com / password
- ๊ณ ๊ฐ: (Seeder๋ก ์๋ ์์ฑ)
## ๋ผ์ด์ ์ค
MIT
```
2. **๋ณด์ ๊ฐํ**:
- **CSRF ๋ณดํธ**: ๋ชจ๋ ํผ์ @csrf ํ์ธ
- **XSS ๋ฐฉ์ง**: Blade {{ }} ์ด์ค์ผ์ดํ ํ์ธ, ์ฌ์ฉ์ ์
๋ ฅ ์ถ๋ ฅ ์ htmlspecialchars
- **SQL Injection ๋ฐฉ์ง**: Eloquent ORM ์ฌ์ฉ, ์ง์ ์ฟผ๋ฆฌ ๊ธ์ง
- **ํ์ผ ์
๋ก๋ ๊ฒ์ฆ**:
- MIME ํ์
๊ฒ์ฆ (mimes ๊ท์น)
- ํ์ผ ํฌ๊ธฐ ์ ํ
- ์
๋ก๋ ๋๋ ํ ๋ฆฌ ๊ถํ ์ค์ (755)
- **Rate Limiting**:
- ๋ก๊ทธ์ธ ์๋ ์ ํ (Breeze ๊ธฐ๋ณธ ์ ๊ณต)
- API ์๋ํฌ์ธํธ์ throttle ๋ฏธ๋ค์จ์ด ์ถ๊ฐ
- **.env ๋ณด์**:
- .env.example ์ ๊ณต, .env๋ .gitignore์ ํฌํจ
- APP_DEBUG=false (ํ๋ก๋์
)
- APP_ENV=production
3. **์ฑ๋ฅ ์ต์ ํ**:
- **Eager Loading**: N+1 ์ฟผ๋ฆฌ ๋ฐฉ์ง
- ์ปค๋ฏธ์
๋ชฉ๋ก ๋ก๋ ์ with(['client', 'creator', 'workflowStages'])
- **์บ์ฑ**:
- ๋์๋ณด๋ ํต๊ณ ์บ์ (5๋ถ TTL)
- `Cache::remember('creator-stats-' . $userId, 300, function() {...})`
- **์ธ๋ฑ์ค ์ถ๊ฐ**:
- commissions ํ
์ด๋ธ: client_id, creator_id, status์ ์ธ๋ฑ์ค
- workflow_stages: commission_id, order์ ๋ณตํฉ ์ธ๋ฑ์ค
- **์ด๋ฏธ์ง ์ต์ ํ**:
- ์
๋ก๋ ์ ์๋ ๋ฆฌ์ฌ์ด์ฆ (Intervention Image ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
- ์ธ๋ค์ผ ์์ฑ
4. **์๋ฌ ์ฒ๋ฆฌ ๊ฐ์ **:
- **์ปค์คํ
์๋ฌ ํ์ด์ง**:
- resources/views/errors/404.blade.php
- resources/views/errors/500.blade.php
- Tailwind CSS๋ก ์คํ์ผ๋ง
- **๋ก๊ทธ ์ค์ **:
- config/logging.php์์ daily ์ฑ๋ ์ฌ์ฉ
- ์ค์ ์์
(๊ฒฐ์ , ํ์ผ ์
๋ก๋)์ ๋ก๊ทธ ์ถ๊ฐ
- **์ฌ์ฉ์ ์นํ์ ๋ฉ์์ง**:
- try-catch๋ก ์์ธ ์ฒ๋ฆฌ
- ์๋ฌ ๋ฐ์ ์ "๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์" ๋ฉ์์ง
5. **์ฝ๋ ํ์ง ๊ฒ์ฆ**:
- **Laravel Pint** (์ฝ๋ ์คํ์ผ):
- `composer require laravel/pint --dev`
- `./vendor/bin/pint`
- **PHPStan** (์ ์ ๋ถ์, ์ ํ์ฌํญ):
- `composer require --dev phpstan/phpstan`
- `./vendor/bin/phpstan analyse`
6. **๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ** (DEPLOYMENT.md ์์ฑ):
```markdown
## ๋ฐฐํฌ ์ ์ฒดํฌ๋ฆฌ์คํธ
- [ ] .env ์ค์ (APP_ENV=production, APP_DEBUG=false)
- [ ] ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
์คํ
- [ ] Storage ์ฌ๋ณผ๋ฆญ ๋งํฌ ์์ฑ
- [ ] Composer autoload ์ต์ ํ (`composer install --optimize-autoloader --no-dev`)
- [ ] Config ์บ์ (`php artisan config:cache`)
- [ ] Route ์บ์ (`php artisan route:cache`)
- [ ] View ์บ์ (`php artisan view:cache`)
- [ ] Queue ์์ปค ์ค์ (Supervisor)
- [ ] Cron ์์
๋ฑ๋ก (Scheduler)
- [ ] HTTPS ์ค์
- [ ] ๋ฐฉํ๋ฒฝ ๊ท์น ์ค์
```
7. **Git ์ค์ **:
- .gitignore ํ์ธ:
- /node_modules
- /public/hot
- /public/storage
- /storage/*.key
- .env
- ์ฒซ ์ปค๋ฐ:
```bash
git init
git add .
git commit -m "feat: initial commit - commission workflow manager"
```
8. **์ต์ข
๊ธฐ๋ฅ ํ
์คํธ**:
- [ ] ์ฌ์ฉ์ ๋ฑ๋ก/๋ก๊ทธ์ธ
- [ ] ์ปค๋ฏธ์
์์ฑ (๊ณ ๊ฐ)
- [ ] ๊ฒฌ์ ์ ๋ฐ์ก (ํฌ๋ฆฌ์์ดํฐ)
- [ ] ๊ฒฌ์ ์ ์น์ธ (๊ณ ๊ฐ)
- [ ] ํ์ผ ์
๋ก๋ (ํฌ๋ฆฌ์์ดํฐ)
- [ ] ๋จ๊ณ ์งํ ๋ฐ ์๋ฃ
- [ ] ํผ๋๋ฐฑ ์์ฑ (์๋ฐฉํฅ)
- [ ] ์ด๋ฉ์ผ ๋ฐ์ก ํ์ธ
- [ ] ๋์๋ณด๋ ํต๊ณ ํ์ธ
- [ ] ํฌํธํด๋ฆฌ์ค ํ์ด์ง ํ์ธ
- [ ] ๋ชจ๋ฐ์ผ ๋ฐ์ํ ํ์ธ
9. **์ ํ ์ฌํญ - ๋ณด๋์ค ๊ธฐ๋ฅ**:
- Stripe ๊ฒฐ์ ์ฐ๋ (ํ
์คํธ ๋ชจ๋)
- ๊ณ ๊ฐ ๋ฆฌ๋ทฐ ์์คํ
- ์ค์๊ฐ ์๋ฆผ (Laravel Echo + Pusher)
- ๋ค๊ตญ์ด ์ง์ (Laravel Localization)
10. **ํ๋ก์ ํธ ์๋ฃ ํ์ธ**:
- ๋ชจ๋ ๊ธฐ๋ฅ์ด ์ ์ ์๋ํ๋์ง ์ต์ข
๊ฒ์ฆ
- README.md์ ์คํฌ๋ฆฐ์ท ์ถ๊ฐ (์ ํ์ฌํญ)
- GitHub์ ํธ์
- ๋ฐ๋ชจ ์ฌ์ดํธ ๋ฐฐํฌ (์ ํ์ฌํญ)
์ถํํฉ๋๋ค! ๐ ์ค๋ฌด ์์ค์ ์ปค๋ฏธ์
๊ด๋ฆฌ ํ๋ซํผ์ด ์์ฑ๋์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ๋ก์ ํธ๊ฐ ์์ ํ ๋ฌธ์ํ๋๊ณ , ๋ณด์์ด ๊ฐํ๋๋ฉฐ, ์ฑ๋ฅ ์ต์ ํ๊ฐ ์๋ฃ๋ฉ๋๋ค. ๋ฐฐํฌ ์ค๋น๊ฐ ๋๋๊ณ ๋ชจ๋ ๊ธฐ๋ฅ์ด ์ต์ข ํ ์คํธ๋ฅผ ํต๊ณผํฉ๋๋ค. ๊ฐ๋ฐ์๋ ์ด ํ๋ก์ ํธ๋ฅผ ํฌํธํด๋ฆฌ์ค๋ก ํ์ฉํ๊ฑฐ๋ ์ค์ ์๋น์ค๋ก ๋ฐฐํฌํ ์ ์์ต๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!