# ItemMaster 연동 설계서 **작성일**: 2025-12-05 **최종 수정**: 2025-12-08 **버전**: 1.1 **상태**: 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, source_table, │ │ is_active │ │ │ │ item_type: FG(완제품), PT(반제품), SM(부자재), │ │ RM(원자재), CS(소모품) │ │ │ │ source_table: 실제 저장 테이블명 │ │ - 'products' (FG, PT) │ │ - 'materials' (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 ← 활성 여부 │ │ │ │ [내부용 매핑 컬럼 - API 응답에서 hidden] │ │ source_table ← 원본 테이블명 (products, materials 등) │ │ source_column ← 원본 컬럼명 (code, name 등) │ │ storage_type ← 저장방식 (column=DB컬럼, json=JSON) │ │ json_path ← JSON 저장 경로 (예: attributes.size) │ └─────────────────────────────────────────────────────────────┘ ``` ### 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 →