# 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 →
field_type: checkbox →
field_type: date →
field_type: textarea →
3. 저장 시 attributes 객체 구성
{
[field_key]: value,
[field_key]: value,
...
}
```
### 6.2 React 컴포넌트 예시
```tsx
interface ItemField {
id: number;
field_key: string;
field_name: string;
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
is_required: boolean;
default_value?: string;
placeholder?: string;
options?: Array<{ label: string; value: string }>;
validation_rules?: Record;
}
function DynamicFieldRenderer({ field, value, onChange }: Props) {
switch (field.field_type) {
case 'textbox':
return (
onChange(field.field_key, e.target.value)}
placeholder={field.placeholder}
required={field.is_required}
/>
);
case 'number':
return (
onChange(field.field_key, val)}
min={field.validation_rules?.min}
max={field.validation_rules?.max}
required={field.is_required}
/>
);
case 'dropdown':
return (