From 02a27f2bbaf25b62c4c729b20c9074a7d0e5dd22 Mon Sep 17 00:00:00 2001 From: hskwon Date: Fri, 5 Dec 2025 17:57:47 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EA=B7=9C=EC=B9=99=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9B=90=EC=B9=99=20=EB=AC=B8=EC=84=9C=20=EC=B2=B4=EA=B3=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - standards/ 폴더 추가 (코딩 규칙: 네이밍, 스타일) - rules/ 폴더 추가 (비즈니스 규칙: 검증, 도메인 로직) - principles/ 폴더 추가 (설계 원칙: 아키텍처, API 설계) - INDEX.md에 규칙 및 원칙 섹션 추가 - 각 폴더에 README.md 생성 --- INDEX.md | 22 + front/[API-2025-12-04] client-api-analysis.md | 122 +++- front/[API-2025-12-04] quote-api-request.md | 88 ++- principles/README.md | 24 + reference/api-rules.md | 54 ++ rules/README.md | 24 + specs/item-master-integration.md | 555 ++++++++++++++++++ standards/README.md | 23 + 8 files changed, 886 insertions(+), 26 deletions(-) create mode 100644 principles/README.md create mode 100644 rules/README.md create mode 100644 specs/item-master-integration.md create mode 100644 standards/README.md 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 →