diff --git a/INDEX.md b/INDEX.md index 0a83317..f3bcb05 100644 --- a/INDEX.md +++ b/INDEX.md @@ -14,6 +14,23 @@ --- +## πŸ“ κ·œμΉ™ 및 원칙 + +개발 μ‹œ μ°Έκ³ ν•΄μ•Ό ν•  κ·œμΉ™κ³Ό 원칙 λ¬Έμ„œμž…λ‹ˆλ‹€. + +| κ·œμΉ™ μœ ν˜• | 폴더 | μš©λ„ | μ˜ˆμ‹œ | +|----------|------|------|------| +| **μ½”λ”© κ·œμΉ™** | [standards/](standards/README.md) | 넀이밍 μ»¨λ²€μ…˜, μ½”λ“œ μŠ€νƒ€μΌ | λ³€μˆ˜λͺ…, 파일λͺ…, ν¬λ§·νŒ… | +| **λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™** | [rules/](rules/README.md) | 도메인 λ‘œμ§μ—μ„œ νŒŒμƒλœ κ·œμΉ™ | ν’ˆλͺ©μ½”λ“œ 생성, 검증 κ·œμΉ™ | +| **섀계 원칙** | [principles/](principles/README.md) | μ•„ν‚€ν…μ²˜/섀계 κ²°μ • κΈ°μ€€ | Service-First, API 섀계 | + +### λ¬Έμ„œ λΆ„λ₯˜ κΈ°μ€€ +- **Standards**: "μ–΄λ–»κ²Œ μ½”λ“œλ₯Ό μž‘μ„±ν•  것인가" (How to write) +- **Rules**: "무엇이 μœ νš¨ν•œ 데이터/μƒνƒœμΈκ°€" (What is valid) +- **Principles**: "μ™œ μ΄λ ‡κ²Œ μ„€κ³„ν•˜λŠ”κ°€" (Why we design) + +--- + ## πŸ“– 개발 κ°€μ΄λ“œ ### Reference (일상 μ°Έκ³  λ¬Έμ„œ) @@ -129,6 +146,11 @@ ## πŸ”„ λ¬Έμ„œ ꡬ쑰 λ³€κ²½ 이λ ₯ +- **2025-12-05**: κ·œμΉ™ 및 원칙 λ¬Έμ„œ 체계 μΆ”κ°€ + - standards/ - μ½”λ”© κ·œμΉ™ (넀이밍, μŠ€νƒ€μΌ) + - rules/ - λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ (검증, 도메인 둜직) + - principles/ - 섀계 원칙 (μ•„ν‚€ν…μ²˜, API 섀계) + - **2025-11-20**: λ¬Έμ„œ ꡬ쑰 λŒ€κ·œλͺ¨ μž¬μ •λ¦¬ - .cursor/docs μ‚­μ œ - claudedocs β†’ docs/ 체계화 diff --git a/front/[API-2025-12-04] client-api-analysis.md b/front/[API-2025-12-04] client-api-analysis.md index 52358e9..62f5542 100644 --- a/front/[API-2025-12-04] client-api-analysis.md +++ b/front/[API-2025-12-04] client-api-analysis.md @@ -96,36 +96,124 @@ protected $fillable = [ ## 4. λ°±μ—”λ“œ μˆ˜μ • μš”μ²­ 사항 -### 4.1 Client λͺ¨λΈ ν•„λ“œ μΆ”κ°€ μš”μ²­ +### 4.1 1μ°¨ ν•„λ“œ μΆ”κ°€ βœ… μ™„λ£Œ (2025-12-04) -```markdown -## λ°±μ—”λ“œ API μˆ˜μ • μš”μ²­ +| ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | μƒνƒœ | +|--------|------|------|------| +| `business_no` | string(20) | μ‚¬μ—…μžλ“±λ‘λ²ˆν˜Έ | βœ… 좔가됨 | +| `business_type` | string(50) | μ—…νƒœ | βœ… 좔가됨 | +| `business_item` | string(100) | μ—…μ’… | βœ… 좔가됨 | -### 파일 μœ„μΉ˜ -`app/Models/Orders/Client.php` - Client λͺ¨λΈ +--- -### ν˜„μž¬ 문제 -ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ μ‚¬μš©ν•˜λŠ” μ‚¬μ—…μž 정보 ν•„λ“œκ°€ λ°±μ—”λ“œμ— μ—†μŒ +### 4.2 🚨 2μ°¨ ν•„λ“œ μΆ”κ°€ μš”μ²­ (sam-design κΈ°μ€€) - 2025-12-04 -### μˆ˜μ • μš”μ²­ -λ‹€μŒ ν•„λ“œλ₯Ό Client λͺ¨λΈμ— μΆ”κ°€: +> **μ°Έκ³ **: `sam-design/src/components/ClientRegistration.tsx` κΈ°μ€€μœΌλ‘œ UI κ΅¬ν˜„ ν•„μš” +> ν˜„μž¬ λ°±μ—”λ“œ API에 λˆ„λ½λœ ν•„λ“œλ“€ μΆ”κ°€ μš”μ²­ +#### μ„Ήμ…˜ 1: κΈ°λ³Έ 정보 μΆ”κ°€ ν•„λ“œ +| ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | nullable | λΉ„κ³  | +|--------|------|------|----------|------| +| `client_type` | enum('λ§€μž…','맀좜','λ§€μž…λ§€μΆœ') | 거래처 μœ ν˜• | NO | κΈ°λ³Έκ°’ 'λ§€μž…' | + +#### μ„Ήμ…˜ 2: μ—°λ½μ²˜ 정보 μΆ”κ°€ ν•„λ“œ | ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | nullable | |--------|------|------|----------| -| `business_no` | string(20) | μ‚¬μ—…μžλ“±λ‘λ²ˆν˜Έ | nullable | -| `business_type` | string(50) | μ—…νƒœ | nullable | -| `business_item` | string(100) | μ—…μ’… | nullable | +| `mobile` | string(20) | λͺ¨λ°”일 번호 | YES | +| `fax` | string(20) | 팩슀 번호 | YES | + +#### μ„Ήμ…˜ 3: λ‹΄λ‹Ήμž 정보 μΆ”κ°€ ν•„λ“œ +| ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | nullable | +|--------|------|------|----------| +| `manager_name` | string(50) | λ‹΄λ‹Ήμžλͺ… | YES | +| `manager_tel` | string(20) | λ‹΄λ‹Ήμž μ „ν™” | YES | +| `system_manager` | string(50) | μ‹œμŠ€ν…œ κ΄€λ¦¬μž | YES | + +#### μ„Ήμ…˜ 4: 발주처 μ„€μ • μΆ”κ°€ ν•„λ“œ +| ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | nullable | +|--------|------|------|----------| +| `account_id` | string(50) | 계정 ID | YES | +| `account_password` | string(255) | λΉ„λ°€λ²ˆν˜Έ (μ•”ν˜Έν™”) | YES | +| `purchase_payment_day` | string(20) | λ§€μž… 결제일 | YES | +| `sales_payment_day` | string(20) | 맀좜 결제일 | YES | + +#### μ„Ήμ…˜ 5: μ•½μ • μ„ΈκΈˆ μΆ”κ°€ ν•„λ“œ +| ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | nullable | +|--------|------|------|----------| +| `tax_agreement` | boolean | μ„ΈκΈˆ μ•½μ • μ—¬λΆ€ | YES | +| `tax_amount` | decimal(15,2) | μ•½μ • κΈˆμ•‘ | YES | +| `tax_start_date` | date | μ•½μ • μ‹œμž‘μΌ | YES | +| `tax_end_date` | date | μ•½μ • μ’…λ£ŒμΌ | YES | + +#### μ„Ήμ…˜ 6: μ•…μ„±μ±„κΆŒ 정보 μΆ”κ°€ ν•„λ“œ +| ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | nullable | +|--------|------|------|----------| +| `bad_debt` | boolean | μ•…μ„±μ±„κΆŒ μ—¬λΆ€ | YES | +| `bad_debt_amount` | decimal(15,2) | μ•…μ„±μ±„κΆŒ κΈˆμ•‘ | YES | +| `bad_debt_receive_date` | date | μ±„κΆŒ λ°œμƒμΌ | YES | +| `bad_debt_end_date` | date | μ±„κΆŒ 만료일 | YES | +| `bad_debt_progress` | enum('ν˜‘μ˜μ€‘','μ†Œμ†‘μ€‘','νšŒμˆ˜μ™„λ£Œ','λŒ€μ†μ²˜λ¦¬') | μ§„ν–‰ μƒνƒœ | YES | + +#### μ„Ήμ…˜ 7: 기타 정보 μΆ”κ°€ ν•„λ“œ +| ν•„λ“œλͺ… | νƒ€μž… | μ„€λͺ… | nullable | +|--------|------|------|----------| +| `memo` | text | λ©”λͺ¨ | YES | + +--- + +### 4.3 λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ˜ˆμ‹œ -### λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ˜ˆμ‹œ ```sql -ALTER TABLE clients ADD COLUMN business_no VARCHAR(20) NULL; -ALTER TABLE clients ADD COLUMN business_type VARCHAR(50) NULL; -ALTER TABLE clients ADD COLUMN business_item VARCHAR(100) NULL; -``` +-- κΈ°λ³Έ 정보 +ALTER TABLE clients ADD COLUMN client_type ENUM('λ§€μž…','맀좜','λ§€μž…λ§€μΆœ') DEFAULT 'λ§€μž…'; + +-- μ—°λ½μ²˜ 정보 +ALTER TABLE clients ADD COLUMN mobile VARCHAR(20) NULL; +ALTER TABLE clients ADD COLUMN fax VARCHAR(20) NULL; + +-- λ‹΄λ‹Ήμž 정보 +ALTER TABLE clients ADD COLUMN manager_name VARCHAR(50) NULL; +ALTER TABLE clients ADD COLUMN manager_tel VARCHAR(20) NULL; +ALTER TABLE clients ADD COLUMN system_manager VARCHAR(50) NULL; + +-- 발주처 μ„€μ • +ALTER TABLE clients ADD COLUMN account_id VARCHAR(50) NULL; +ALTER TABLE clients ADD COLUMN account_password VARCHAR(255) NULL; +ALTER TABLE clients ADD COLUMN purchase_payment_day VARCHAR(20) NULL; +ALTER TABLE clients ADD COLUMN sales_payment_day VARCHAR(20) NULL; + +-- μ•½μ • μ„ΈκΈˆ +ALTER TABLE clients ADD COLUMN tax_agreement TINYINT(1) DEFAULT 0; +ALTER TABLE clients ADD COLUMN tax_amount DECIMAL(15,2) NULL; +ALTER TABLE clients ADD COLUMN tax_start_date DATE NULL; +ALTER TABLE clients ADD COLUMN tax_end_date DATE NULL; + +-- μ•…μ„±μ±„κΆŒ 정보 +ALTER TABLE clients ADD COLUMN bad_debt TINYINT(1) DEFAULT 0; +ALTER TABLE clients ADD COLUMN bad_debt_amount DECIMAL(15,2) NULL; +ALTER TABLE clients ADD COLUMN bad_debt_receive_date DATE NULL; +ALTER TABLE clients ADD COLUMN bad_debt_end_date DATE NULL; +ALTER TABLE clients ADD COLUMN bad_debt_progress ENUM('ν˜‘μ˜μ€‘','μ†Œμ†‘μ€‘','νšŒμˆ˜μ™„λ£Œ','λŒ€μ†μ²˜λ¦¬') NULL; + +-- 기타 정보 +ALTER TABLE clients ADD COLUMN memo TEXT NULL; ``` --- +### 4.4 μˆ˜μ • ν•„μš” 파일 λͺ©λ‘ + +| 파일 | μˆ˜μ • λ‚΄μš© | +|------|----------| +| `app/Models/Orders/Client.php` | fillable에 μƒˆ ν•„λ“œ μΆ”κ°€, casts μ„€μ • | +| `database/migrations/xxxx_add_client_extended_fields.php` | λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 생성 | +| `app/Services/ClientService.php` | μƒˆ ν•„λ“œ 처리 둜직 μΆ”κ°€ | +| `app/Http/Requests/Client/ClientStoreRequest.php` | μœ νš¨μ„± 검증 κ·œμΉ™ μΆ”κ°€ | +| `app/Http/Requests/Client/ClientUpdateRequest.php` | μœ νš¨μ„± 검증 κ·œμΉ™ μΆ”κ°€ | +| `app/Swagger/v1/ClientApi.php` | API λ¬Έμ„œ μ—…λ°μ΄νŠΈ | + +--- + ## 5. ν”„λ‘ νŠΈμ—”λ“œ API 연동 κ΅¬ν˜„ κ³„νš ### 5.1 ν•„μš”ν•œ μž‘μ—… diff --git a/front/[API-2025-12-04] quote-api-request.md b/front/[API-2025-12-04] quote-api-request.md index d3154f1..ebe9174 100644 --- a/front/[API-2025-12-04] quote-api-request.md +++ b/front/[API-2025-12-04] quote-api-request.md @@ -187,7 +187,15 @@ interface QuoteRevision { | `GET` | `/api/v1/quotes/{id}/document/calculation` | μ‚°μΆœλ‚΄μ—­μ„œ PDF | | | `GET` | `/api/v1/quotes/{id}/document/purchase-order` | λ°œμ£Όμ„œ PDF | | -### 3.5 견적번호 생성 +### 3.5 λ¬Έμ„œ λ°œμ†‘ API ⭐ μ‹ κ·œ μš”μ²­ + +| Method | Endpoint | μ„€λͺ… | λΉ„κ³  | +|--------|----------|------|------| +| `POST` | `/api/v1/quotes/{id}/send/email` | 이메일 λ°œμ†‘ | μ²¨λΆ€νŒŒμΌ 포함 | +| `POST` | `/api/v1/quotes/{id}/send/fax` | 팩슀 λ°œμ†‘ | 팩슀 μ„œλΉ„μŠ€ 연동 | +| `POST` | `/api/v1/quotes/{id}/send/kakao` | μΉ΄μΉ΄μ˜€ν†‘ λ°œμ†‘ | μ•Œλ¦Όν†‘/μΉœκ΅¬ν†‘ | + +### 3.6 견적번호 생성 | Method | Endpoint | μ„€λͺ… | λΉ„κ³  | |--------|----------|------|------| @@ -530,10 +538,76 @@ sort_order: 'asc' | 'desc' --- -## 7. ν”„λ‘ νŠΈμ—”λ“œ 연동 κ³„νš +## 7. ν”„λ‘ νŠΈμ—”λ“œ κ΅¬ν˜„ ν˜„ν™© (2025-12-04 μ—…λ°μ΄νŠΈ) + +### 7.1 κ΅¬ν˜„ μ™„λ£Œλœ 파일 + +| 파일 | μ„€λͺ… | μƒνƒœ | +|------|------|------| +| `quote-management/page.tsx` | 견적 λͺ©λ‘ νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ (μƒ˜ν”Œ 데이터) | +| `quote-management/new/page.tsx` | 견적 등둝 νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `quote-management/[id]/page.tsx` | 견적 상세 νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `quote-management/[id]/edit/page.tsx` | 견적 μˆ˜μ • νŽ˜μ΄μ§€ | βœ… μ™„λ£Œ | +| `components/quotes/QuoteRegistration.tsx` | 견적 등둝/μˆ˜μ • μ»΄ν¬λ„ŒνŠΈ | βœ… μ™„λ£Œ | +| `components/quotes/QuoteDocument.tsx` | κ²¬μ μ„œ λ¬Έμ„œ μ»΄ν¬λ„ŒνŠΈ | βœ… μ™„λ£Œ | +| `components/quotes/QuoteCalculationReport.tsx` | μ‚°μΆœλ‚΄μ—­μ„œ λ¬Έμ„œ μ»΄ν¬λ„ŒνŠΈ | βœ… μ™„λ£Œ | +| `components/quotes/PurchaseOrderDocument.tsx` | λ°œμ£Όμ„œ λ¬Έμ„œ μ»΄ν¬λ„ŒνŠΈ | βœ… μ™„λ£Œ | + +### 7.2 UI κΈ°λŠ₯ κ΅¬ν˜„ ν˜„ν™© + +| κΈ°λŠ₯ | μƒνƒœ | λΉ„κ³  | +|------|------|------| +| 견적 λͺ©λ‘ 쑰회 | βœ… UI μ™„λ£Œ | μƒ˜ν”Œ 데이터, API 연동 ν•„μš” | +| 견적 검색/ν•„ν„° | βœ… UI μ™„λ£Œ | 둜컬 필터링, API 연동 ν•„μš” | +| 견적 등둝 폼 | βœ… UI μ™„λ£Œ | API 연동 ν•„μš” | +| 견적 상세 νŽ˜μ΄μ§€ | βœ… UI μ™„λ£Œ | API 연동 ν•„μš” | +| 견적 μˆ˜μ • 폼 | βœ… UI μ™„λ£Œ | API 연동 ν•„μš” | +| 견적 μ‚­μ œ | βœ… UI μ™„λ£Œ | 둜컬 μƒνƒœ, API 연동 ν•„μš” | +| 견적 일괄 μ‚­μ œ | βœ… UI μ™„λ£Œ | 둜컬 μƒνƒœ, API 연동 ν•„μš” | +| μžλ™ 견적 μ‚°μΆœ | ⏳ λ²„νŠΌλ§Œ | λ°±μ—”λ“œ μˆ˜μ‹ μ—”μ§„ ν•„μš” | +| 발주처 선택 | ⏳ μƒ˜ν”Œ 데이터 | `/api/v1/clients` 연동 ν•„μš” | +| ν˜„μž₯ 선택 | ⏳ μƒ˜ν”Œ 데이터 | 발주처 연동 ν›„ ν˜„μž₯ API ν•„μš” | +| μ œν’ˆ 선택 | ⏳ μƒ˜ν”Œ 데이터 | `/api/v1/item-masters` 연동 ν•„μš” | +| **κ²¬μ μ„œ λͺ¨λ‹¬** | βœ… UI μ™„λ£Œ | PDF/이메일/팩슀/카톑 λ²„νŠΌ, **λ°œμ†‘ API ν•„μš”** | +| **μ‚°μΆœλ‚΄μ—­μ„œ λͺ¨λ‹¬** | βœ… UI μ™„λ£Œ | PDF/이메일/팩슀/카톑 λ²„νŠΌ, **λ°œμ†‘ API ν•„μš”** | +| **λ°œμ£Όμ„œ λͺ¨λ‹¬** | βœ… UI μ™„λ£Œ | PDF/이메일/팩슀/카톑 λ²„νŠΌ, **λ°œμ†‘ API ν•„μš”** | +| μ΅œμ’…ν™•μ • λ²„νŠΌ | βœ… UI μ™„λ£Œ | API 연동 ν•„μš” | + +### 7.3 견적 등둝/μˆ˜μ • 폼 ν•„λ“œ (κ΅¬ν˜„ μ™„λ£Œ) + +**κΈ°λ³Έ 정보 μ„Ήμ…˜:** +- 등둝일 (readonly, 였늘 λ‚ μ§œ) +- μž‘μ„±μž (readonly, 둜그인 μ‚¬μš©μž) +- 발주처 선택 * (ν•„μˆ˜) +- ν˜„μž₯λͺ… (발주처 선택 μ‹œ 연동) +- 발주 λ‹΄λ‹Ήμž +- μ—°λ½μ²˜ +- 납기일 +- λΉ„κ³  + +**μžλ™ 견적 μ‚°μΆœ μ„Ήμ…˜ (동적 ν•­λͺ©):** +- 측수 +- λΆ€ν˜Έ +- μ œν’ˆ μΉ΄ν…Œκ³ λ¦¬ (PC) * +- μ œν’ˆλͺ… * +- μ˜€ν”ˆμ‚¬μ΄μ¦ˆ (W0) * +- μ˜€ν”ˆμ‚¬μ΄μ¦ˆ (H0) * +- κ°€μ΄λ“œλ ˆμΌ μ„€μΉ˜ μœ ν˜• (GT) * +- λͺ¨ν„° 전원 (MP) * +- μ—°λ™μ œμ–΄κΈ° (CT) * +- μˆ˜λŸ‰ (QTY) * +- 마ꡬ리 λ‚ κ°œμΉ˜μˆ˜ (WS) +- 검사비 (INSP) + +**κΈ°λŠ₯:** +- 견적 ν•­λͺ© μΆ”κ°€/볡사/μ‚­μ œ +- μžλ™ 견적 μ‚°μΆœ λ²„νŠΌ +- μƒ˜ν”Œ 데이터 생성 λ²„νŠΌ + +### 7.4 λ‹€μŒ 단계 (API 연동) -### 7.1 useQuoteList ν›… (λͺ©λ‘) ```typescript +// useQuoteList ν›… (λͺ©λ‘) const { quotes, pagination, @@ -542,10 +616,8 @@ const { deleteQuote, bulkDelete } = useQuoteList(); -``` -### 7.2 useQuote ν›… (단건 CRUD) -```typescript +// useQuote ν›… (단건 CRUD) const { quote, isLoading, @@ -555,10 +627,8 @@ const { finalizeQuote, convertToOrder } = useQuote(); -``` -### 7.3 useQuoteCalculation ν›… (μžλ™ μ‚°μΆœ) -```typescript +// useQuoteCalculation ν›… (μžλ™ μ‚°μΆœ) const { calculationResult, isCalculating, diff --git a/principles/README.md b/principles/README.md new file mode 100644 index 0000000..7751091 --- /dev/null +++ b/principles/README.md @@ -0,0 +1,24 @@ +# Principles (섀계 원칙) + +> μ‹œμŠ€ν…œ 섀계와 μ•„ν‚€ν…μ²˜ κ²°μ •μ˜ 근간이 λ˜λŠ” 원칙 + +## λͺ©μ  +- μΌκ΄€λœ μ•„ν‚€ν…μ²˜ κ²°μ • κΈ°μ€€ 제곡 +- 기술 뢀채 λ°©μ§€ +- ν™•μž₯μ„±κ³Ό μœ μ§€λ³΄μˆ˜μ„± 확보 + +## 포함 λ‚΄μš© +- μ•„ν‚€ν…μ²˜ 원칙 (Service-First, Multi-tenancy) +- API 섀계 원칙 (RESTful, 버전 관리) +- λ°μ΄ν„°λ² μ΄μŠ€ 섀계 원칙 +- λ³΄μ•ˆ 섀계 원칙 +- μ„±λŠ₯ μ΅œμ ν™” 원칙 + +## λ¬Έμ„œ λͺ©λ‘ +| λ¬Έμ„œ | μ„€λͺ… | +|------|------| +| *(μž‘μ„± μ˜ˆμ •)* | | + +## κ΄€λ ¨ λ¬Έμ„œ +- [μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜](../reference/architecture.md) - 전체 μ‹œμŠ€ν…œ ꡬ쑰 +- [API 개발 κ·œμΉ™](../reference/api-rules.md) - API 섀계 ν‘œμ€€ \ No newline at end of file diff --git a/reference/api-rules.md b/reference/api-rules.md index b124965..2a775e5 100644 --- a/reference/api-rules.md +++ b/reference/api-rules.md @@ -21,6 +21,60 @@ - Common columns: tenant_id, created_by, updated_by, deleted_by (COMMENT required) - FK constraints: Created during design, minimal in production +### 2.1 ModelTrait μ‚¬μš© κ°€μ΄λ“œ + +`ModelTrait`λŠ” λͺ¨λ“  λͺ¨λΈμ—μ„œ κ³΅ν†΅μœΌλ‘œ μ‚¬μš©ν•˜λŠ” κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. + +**μœ„μΉ˜**: `app/Traits/ModelTrait.php` + +**제곡 κΈ°λŠ₯**: +```php +// 1. λ‚ μ§œ 직렬화 포맷 (Y-m-d H:i:s) +protected function serializeDate(DateTimeInterface $date) + +// 2. Active μƒνƒœ 쑰회 Scope +public function scopeActive($query) +// μ‚¬μš©: Model::active()->get() +// SQL: WHERE is_active = 1 +``` + +**⚠️ ν•„μˆ˜ μš”κ΅¬μ‚¬ν•­**: + +`scopeActive()` λ©”μ„œλ“œ μ‚¬μš© μ‹œ ν…Œμ΄λΈ”μ— `is_active` 컬럼이 **λ°˜λ“œμ‹œ μ‘΄μž¬ν•΄μ•Ό 함** + +```sql +-- λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ˜ˆμ‹œ +$table->boolean('is_active') + ->default(true) + ->comment('ν™œμ„±ν™” μ—¬λΆ€ (ModelTrait::scopeActive() μ‚¬μš©)'); +``` + +**λͺ¨λΈ μ„€μ •**: +```php +class YourModel extends Model +{ + use BelongsToTenant, ModelTrait, SoftDeletes; + + protected $fillable = [ + // ... + 'is_active', // λ°˜λ“œμ‹œ μΆ”κ°€ + ]; + + protected $casts = [ + 'is_active' => 'boolean', // λ°˜λ“œμ‹œ μΆ”κ°€ + ]; +} +``` + +**is_active 컬럼 적용 ν…Œμ΄λΈ”** (2025-12-05 κΈ°μ€€): +| ν…Œμ΄λΈ” | is_active | λΉ„κ³  | +|--------|-----------|------| +| materials | βœ… 있음 | | +| products | βœ… 있음 | | +| item_pages | βœ… 있음 | | +| item_fields | βœ… 있음 | 2025-12-05 μΆ”κ°€ | +| item_sections | ❌ μ—†μŒ | ν•„μš”μ‹œ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μΆ”κ°€ | + --- ## 3. Middleware Stack diff --git a/rules/README.md b/rules/README.md new file mode 100644 index 0000000..d5f7af2 --- /dev/null +++ b/rules/README.md @@ -0,0 +1,24 @@ +# Rules (λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™) + +> 도메인 둜직과 λΉ„μ¦ˆλ‹ˆμŠ€ μš”κ΅¬μ‚¬ν•­μ—μ„œ νŒŒμƒλœ κ·œμΉ™ + +## λͺ©μ  +- λΉ„μ¦ˆλ‹ˆμŠ€ 둜직의 λͺ…ν™•ν•œ λ¬Έμ„œν™” +- 개발 μ‹œ κ·œμΉ™ 기반 κ΅¬ν˜„ κ°€μ΄λ“œ +- 검증 둜직의 κ·Όκ±° 제곡 + +## 포함 λ‚΄μš© +- ν’ˆλͺ©μ½”λ“œ 생성 κ·œμΉ™ +- ν•„λ“œ 검증 κ·œμΉ™ +- μƒνƒœ 전이 κ·œμΉ™ +- κΆŒν•œ/μ ‘κ·Ό μ œμ–΄ κ·œμΉ™ +- 데이터 무결성 κ·œμΉ™ + +## λ¬Έμ„œ λͺ©λ‘ +| λ¬Έμ„œ | μ„€λͺ… | +|------|------| +| *(μž‘μ„± μ˜ˆμ •)* | | + +## κ΄€λ ¨ λ¬Έμ„œ +- [ν’ˆλͺ©κΈ°μ€€κ΄€λ¦¬ μŠ€νŽ™](../specs/item-master-integration.md) - ν’ˆλͺ© 관리 λͺ…μ„Έ +- [λ³΄μ•ˆ μ •μ±…](../specs/security-policy.md) - λ³΄μ•ˆ κ΄€λ ¨ κ·œμΉ™ \ No newline at end of file diff --git a/specs/item-master-integration.md b/specs/item-master-integration.md new file mode 100644 index 0000000..60ab2cf --- /dev/null +++ b/specs/item-master-integration.md @@ -0,0 +1,555 @@ +# ItemMaster 연동 μ„€κ³„μ„œ + +**μž‘μ„±μΌ**: 2025-12-05 +**버전**: 1.0 +**μƒνƒœ**: Draft + +--- + +## 1. κ°œμš” + +### 1.1 λͺ©μ  +ν’ˆλͺ©κΈ°μ€€κ΄€λ¦¬(ItemMaster)μ—μ„œ μ •μ˜ν•œ ν•„λ“œμ™€ μ‹€μ œ μ—”ν‹°ν‹° 데이터λ₯Ό μ—°λ™ν•˜μ—¬, 동적 ν•„λ“œ μ •μ˜ 및 κ°’ μ €μž₯을 κ°€λŠ₯ν•˜κ²Œ ν•œλ‹€. + +### 1.2 섀계 원칙 +- **κΈ°μ‘΄ ν…Œμ΄λΈ” ν™œμš©**: μ‹ κ·œ ν…Œμ΄λΈ” μΆ”κ°€ 없이 κΈ°μ‘΄ `attributes` JSON 컬럼 ν™œμš© +- **λ²”μš©μ„±**: ν’ˆλͺ©(products, materials) 외에도 λ‹€λ₯Έ μ—”ν‹°ν‹°(orders, clients λ“±) ν™•μž₯ κ°€λŠ₯ +- **μ„±λŠ₯**: JOIN 없이 단일 쿼리둜 쑰회 κ°€λŠ₯ +- **μœ μ—°μ„±**: ν…Œλ„ŒνŠΈ/그룹별 λ‹€λ₯Έ ν•„λ“œ ꡬ성 지원 + +--- + +## 2. ν˜„μž¬ ꡬ쑰 + +### 2.1 ItemMaster ν…Œμ΄λΈ” ꡬ쑰 + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ item_pages (νŽ˜μ΄μ§€ μ •μ˜) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ id, tenant_id, page_name, item_type, is_active β”‚ +β”‚ β”‚ +β”‚ item_type: FG(μ™„μ œν’ˆ), PT(λ°˜μ œν’ˆ), SM(λΆ€μžμž¬), β”‚ +β”‚ RM(μ›μžμž¬), CS(μ†Œλͺ¨ν’ˆ) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ 1:N + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ item_sections (μ„Ήμ…˜ μ •μ˜) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ id, tenant_id, page_id, title, type, order_no β”‚ +β”‚ β”‚ +β”‚ type: fields(ν•„λ“œν˜•), bom(BOMν˜•) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ 1:N + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ item_fields (ν•„λ“œ μ •μ˜) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ id, tenant_id, group_id, section_id (nullable) β”‚ +β”‚ field_key ← attributes JSON 킀와 λ§€ν•‘ β”‚ +β”‚ field_name ← ν™”λ©΄ ν‘œμ‹œλͺ… β”‚ +β”‚ field_type ← textbox, number, dropdown, checkbox... β”‚ +β”‚ is_required ← ν•„μˆ˜ μ—¬λΆ€ β”‚ +β”‚ default_value ← κΈ°λ³Έκ°’ β”‚ +β”‚ placeholder ← μž…λ ₯ 힌트 β”‚ +β”‚ validation_rules ← 검증 κ·œμΉ™ JSON β”‚ +β”‚ options ← 선택 μ˜΅μ…˜ JSON β”‚ +β”‚ properties ← μΆ”κ°€ 속성 JSON β”‚ +β”‚ category ← ν•„λ“œ μΉ΄ν…Œκ³ λ¦¬ β”‚ +β”‚ is_common ← 곡톡 ν•„λ“œ μ—¬λΆ€ β”‚ +β”‚ is_active ← ν™œμ„± μ—¬λΆ€ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 2.2 μ—”ν‹°ν‹° ν…Œμ΄λΈ” ꡬ쑰 + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ products β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [κ³ μ • ν•„λ“œ] β”‚ +β”‚ id, tenant_id, code, name, unit, category_id β”‚ +β”‚ product_type, is_active, is_sellable, is_purchasable... β”‚ +β”‚ β”‚ +β”‚ [동적 ν•„λ“œ] β”‚ +β”‚ attributes JSON ← ItemMaster ν•„λ“œ κ°’ μ €μž₯ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ materials β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [κ³ μ • ν•„λ“œ] β”‚ +β”‚ id, tenant_id, material_code, name, unit, category_id β”‚ +β”‚ material_type, is_active... β”‚ +β”‚ β”‚ +β”‚ [동적 ν•„λ“œ] β”‚ +β”‚ attributes JSON ← ItemMaster ν•„λ“œ κ°’ μ €μž₯ β”‚ +β”‚ options JSON ← μΆ”κ°€ μ˜΅μ…˜ μ €μž₯ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 3. 연동 섀계 + +### 3.1 λ§€ν•‘ κ·œμΉ™ + +``` +ItemMaster Entity.attributes +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ group_id: 1 β”‚ β”‚ β”‚ +β”‚ field_key: "color" β”‚ ◀═══맀핑═══▢ β”‚ {"color": "λΉ¨κ°•"} β”‚ +β”‚ field_key: "weight" β”‚ ◀═══맀핑═══▢ β”‚ {"weight": 1.5} β”‚ +β”‚ field_key: "spec" β”‚ ◀═══맀핑═══▢ β”‚ {"spec": "10x20"} β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +핡심: item_fields.field_key = attributes JSON의 key +``` + +### 3.2 Group ID μ •μ˜ + +| group_id | μ—”ν‹°ν‹° | λŒ€μƒ ν…Œμ΄λΈ” | λΉ„κ³  | +|----------|--------|-------------|------| +| 1 | ν’ˆλͺ©-μ œν’ˆ | products | product_type: FG, PT | +| 2 | ν’ˆλͺ©-자재 | materials | material_type: SM, RM, CS | +| 3 | μ£Όλ¬Έ | orders | ν–₯ν›„ ν™•μž₯ | +| 4 | 고객 | clients | ν–₯ν›„ ν™•μž₯ | +| ... | ... | ... | ν•„μš” μ‹œ μΆ”κ°€ | + +> **μ°Έκ³ **: group_idλŠ” `common_codes` ν…Œμ΄λΈ”μ—μ„œ κ΄€λ¦¬ν•˜κ±°λ‚˜, 별도 enum으둜 μ •μ˜ κ°€λŠ₯ + +### 3.3 데이터 흐름 + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 1. κ΄€λ¦¬μž: ItemMasterμ—μ„œ ν•„λ“œ μ •μ˜ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ POST /api/v1/item-master/fields β”‚ +β”‚ { β”‚ +β”‚ "group_id": 1, β”‚ +β”‚ "field_key": "color", β”‚ +β”‚ "field_name": "색상", β”‚ +β”‚ "field_type": "dropdown", β”‚ +β”‚ "is_required": true, β”‚ +β”‚ "options": [ β”‚ +β”‚ {"label": "λΉ¨κ°•", "value": "red"}, β”‚ +β”‚ {"label": "νŒŒλž‘", "value": "blue"} β”‚ +β”‚ ] β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ β†’ item_fields ν…Œμ΄λΈ”μ— μ €μž₯ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 2. μ‚¬μš©μž: ν’ˆλͺ© 등둝 ν™”λ©΄ μ§„μž… β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ GET /api/v1/item-master/fields?group_id=1 β”‚ +β”‚ β”‚ +β”‚ β†’ μ •μ˜λœ ν•„λ“œ λͺ©λ‘ λ°˜ν™˜ β”‚ +β”‚ β†’ ν”„λ‘ νŠΈμ—”λ“œκ°€ 동적 폼 λ Œλ”λ§ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ [색상 β–Ό] ← dropdown으둜 ν‘œμ‹œ β”‚ β”‚ +β”‚ β”‚ λΉ¨κ°• β”‚ β”‚ +β”‚ β”‚ νŒŒλž‘ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 3. μ‚¬μš©μž: ν’ˆλͺ© μ €μž₯ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ POST /api/v1/products β”‚ +β”‚ { β”‚ +β”‚ "code": "P-001", ← κ³ μ • ν•„λ“œ β”‚ +β”‚ "name": "ν‹°μ…”μΈ ", β”‚ +β”‚ "unit": "EA", β”‚ +β”‚ "product_type": "FG", β”‚ +β”‚ "attributes": { ← 동적 ν•„λ“œ β”‚ +β”‚ "color": "red", (field_key: value) β”‚ +β”‚ "size": "XL" β”‚ +β”‚ } β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ β†’ products ν…Œμ΄λΈ”μ— μ €μž₯ β”‚ +β”‚ β†’ attributes JSON에 동적 ν•„λ“œ κ°’ 포함 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 4. μ‚¬μš©μž: ν’ˆλͺ© 쑰회 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ GET /api/v1/products/1 β”‚ +β”‚ β”‚ +β”‚ { β”‚ +β”‚ "id": 1, β”‚ +β”‚ "code": "P-001", β”‚ +β”‚ "name": "ν‹°μ…”μΈ ", β”‚ +β”‚ "attributes": { β”‚ +β”‚ "color": "red", β”‚ +β”‚ "size": "XL" β”‚ +β”‚ } β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ β†’ JOIN 없이 ν•œ λ²ˆμ— 쑰회! β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 4. API 섀계 + +### 4.1 ItemMaster API (κΈ°μ‘΄) + +| Method | Endpoint | μ„€λͺ… | +|--------|----------|------| +| GET | `/api/v1/item-master/fields` | ν•„λ“œ λͺ©λ‘ 쑰회 | +| GET | `/api/v1/item-master/fields/{id}` | ν•„λ“œ 상세 쑰회 | +| POST | `/api/v1/item-master/fields` | ν•„λ“œ 생성 | +| PUT | `/api/v1/item-master/fields/{id}` | ν•„λ“œ μˆ˜μ • | +| DELETE | `/api/v1/item-master/fields/{id}` | ν•„λ“œ μ‚­μ œ | + +**ν•„ν„° νŒŒλΌλ―Έν„°**: +- `group_id`: μ—”ν‹°ν‹° κ·Έλ£Ή ν•„ν„° +- `section_id`: μ„Ήμ…˜ ν•„ν„° +- `is_active`: ν™œμ„± ν•„ν„° +- `is_common`: 곡톡 ν•„λ“œ ν•„ν„° + +### 4.2 μ—”ν‹°ν‹° API μˆ˜μ • + +#### 4.2.1 Products API + +**μ €μž₯ μ‹œ attributes 포함**: +```json +POST /api/v1/products +{ + "code": "P-001", + "name": "μ œν’ˆλͺ…", + "unit": "EA", + "product_type": "FG", + "attributes": { + "color": "red", + "weight": 1.5, + "custom_field": "value" + } +} +``` + +**쑰회 μ‹œ ν•„λ“œ 메타데이터 포함 (선택)**: +``` +GET /api/v1/products/1?include_field_meta=true +``` + +```json +{ + "id": 1, + "code": "P-001", + "name": "μ œν’ˆλͺ…", + "attributes": { + "color": "red", + "weight": 1.5 + }, + "field_meta": [ + { + "field_key": "color", + "field_name": "색상", + "field_type": "dropdown", + "value": "red", + "options": [...] + }, + { + "field_key": "weight", + "field_name": "μ€‘λŸ‰", + "field_type": "number", + "value": 1.5 + } + ] +} +``` + +--- + +## 5. 검증 둜직 + +### 5.1 μ €μž₯ μ‹œ 검증 흐름 + +```php +class ItemFieldValidationService +{ + /** + * attributes 값을 ItemMaster κΈ°μ€€μœΌλ‘œ 검증 + */ + public function validate(int $groupId, array $attributes): array + { + $errors = []; + + // 1. ν•΄λ‹Ή 그룹의 ν•„λ“œ μ •μ˜ 쑰회 + $fields = ItemField::where('group_id', $groupId) + ->where('is_active', true) + ->get() + ->keyBy('field_key'); + + // 2. ν•„μˆ˜ ν•„λ“œ 체크 + foreach ($fields->where('is_required', true) as $field) { + if (!isset($attributes[$field->field_key])) { + $errors[$field->field_key] = "{$field->field_name}은(λŠ”) ν•„μˆ˜μž…λ‹ˆλ‹€."; + } + } + + // 3. νƒ€μž…λ³„ 검증 + foreach ($attributes as $key => $value) { + if (!$fields->has($key)) { + continue; // μ •μ˜λ˜μ§€ μ•Šμ€ ν•„λ“œλŠ” μŠ€ν‚΅ (λ˜λŠ” μ—λŸ¬) + } + + $field = $fields->get($key); + $fieldError = $this->validateFieldValue($field, $value); + if ($fieldError) { + $errors[$key] = $fieldError; + } + } + + return $errors; + } + + /** + * ν•„λ“œ νƒ€μž…λ³„ κ°’ 검증 + */ + private function validateFieldValue(ItemField $field, mixed $value): ?string + { + return match($field->field_type) { + 'number' => $this->validateNumber($field, $value), + 'dropdown' => $this->validateDropdown($field, $value), + 'date' => $this->validateDate($field, $value), + 'checkbox' => $this->validateCheckbox($field, $value), + default => null + }; + } + + private function validateNumber(ItemField $field, mixed $value): ?string + { + if (!is_numeric($value)) { + return "{$field->field_name}은(λŠ”) μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."; + } + + $rules = $field->validation_rules ?? []; + if (isset($rules['min']) && $value < $rules['min']) { + return "{$field->field_name}은(λŠ”) {$rules['min']} 이상이어야 ν•©λ‹ˆλ‹€."; + } + if (isset($rules['max']) && $value > $rules['max']) { + return "{$field->field_name}은(λŠ”) {$rules['max']} μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."; + } + + return null; + } + + private function validateDropdown(ItemField $field, mixed $value): ?string + { + $options = $field->options ?? []; + $validValues = array_column($options, 'value'); + + if (!in_array($value, $validValues)) { + return "{$field->field_name}의 값이 μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."; + } + + return null; + } +} +``` + +### 5.2 Controllerμ—μ„œ μ‚¬μš© + +```php +class ProductsController extends Controller +{ + public function store(ProductStoreRequest $request) + { + $validated = $request->validated(); + + // attributes 검증 (선택적) + if (isset($validated['attributes'])) { + $groupId = 1; // ν’ˆλͺ©-μ œν’ˆ κ·Έλ£Ή + $errors = $this->fieldValidationService->validate( + $groupId, + $validated['attributes'] + ); + + if (!empty($errors)) { + return ApiResponse::error('검증 μ‹€νŒ¨', $errors, 422); + } + } + + $product = $this->productService->create($validated); + + return ApiResponse::success($product, __('message.created')); + } +} +``` + +--- + +## 6. ν”„λ‘ νŠΈμ—”λ“œ 연동 + +### 6.1 동적 폼 λ Œλ”λ§ 흐름 + +``` +1. νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ + GET /api/v1/item-master/fields?group_id=1 + +2. ν•„λ“œ μ •μ˜ 기반 폼 μ»΄ν¬λ„ŒνŠΈ λ Œλ”λ§ + field_type: textbox β†’ + field_type: number β†’ + field_type: dropdown β†’