# 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  → <Input />
   field_type: number   → <InputNumber />
   field_type: dropdown → <Select options={field.options} />
   field_type: checkbox → <Checkbox />
   field_type: date     → <DatePicker />
   field_type: textarea → <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<string, any>;
}

function DynamicFieldRenderer({ field, value, onChange }: Props) {
  switch (field.field_type) {
    case 'textbox':
      return (
        <Input
          value={value}
          onChange={(e) => onChange(field.field_key, e.target.value)}
          placeholder={field.placeholder}
          required={field.is_required}
        />
      );

    case 'number':
      return (
        <InputNumber
          value={value}
          onChange={(val) => onChange(field.field_key, val)}
          min={field.validation_rules?.min}
          max={field.validation_rules?.max}
          required={field.is_required}
        />
      );

    case 'dropdown':
      return (
        <Select
          value={value}
          onChange={(val) => onChange(field.field_key, val)}
          options={field.options}
          required={field.is_required}
        />
      );

    // ... 기타 타입
  }
}

function ProductForm() {
  const [fields, setFields] = useState<ItemField[]>([]);
  const [attributes, setAttributes] = useState<Record<string, any>>({});

  useEffect(() => {
    // 필드 정의 로드
    fetch('/api/v1/item-master/fields?group_id=1')
      .then(res => res.json())
      .then(data => setFields(data.data));
  }, []);

  const handleFieldChange = (key: string, value: any) => {
    setAttributes(prev => ({ ...prev, [key]: value }));
  };

  return (
    <form>
      {/* 고정 필드 */}
      <Input name="code" label="품목코드" required />
      <Input name="name" label="품목명" required />

      {/* 동적 필드 */}
      {fields.map(field => (
        <DynamicFieldRenderer
          key={field.id}
          field={field}
          value={attributes[field.field_key]}
          onChange={handleFieldChange}
        />
      ))}
    </form>
  );
}
```

---

## 7. 확장 가이드

### 7.1 새 엔티티 추가 시

1. **group_id 정의**: 새 그룹 ID 할당
2. **테이블 확인**: `attributes` JSON 컬럼 존재 확인 (없으면 추가)
3. **ItemMaster 필드 정의**: 해당 group_id로 필드 생성
4. **API 수정**: 저장/조회 시 attributes 처리 로직 추가

### 7.2 예시: 주문(orders) 연동

```sql
-- 1. orders 테이블에 attributes 컬럼 추가 (없는 경우)
ALTER TABLE orders ADD COLUMN attributes JSON DEFAULT NULL COMMENT '동적 필드';

-- 2. ItemMaster에 주문용 필드 정의
INSERT INTO item_fields (tenant_id, group_id, field_key, field_name, field_type, ...)
VALUES (1, 3, 'urgency', '긴급도', 'dropdown', ...);
```

---

## 8. 모델 헬퍼 메서드

### 8.1 ItemPage 모델

```php
class ItemPage extends Model
{
    /**
     * source_table에 해당하는 모델 클래스명 반환
     */
    public function getTargetModelClass(): ?string
    {
        $mapping = [
            'products' => \App\Models\Product::class,
            'materials' => \App\Models\Material::class,
        ];
        return $mapping[$this->source_table] ?? null;
    }

    /**
     * 제품 페이지인지 확인
     */
    public function isProductPage(): bool
    {
        return $this->source_table === 'products';
    }

    /**
     * 자재 페이지인지 확인
     */
    public function isMaterialPage(): bool
    {
        return $this->source_table === 'materials';
    }
}
```

### 8.2 ItemField 모델

```php
class ItemField extends Model
{
    /**
     * 시스템 필드 여부 확인 (DB 컬럼과 매핑된 필드)
     */
    public function isSystemField(): bool
    {
        return !is_null($this->source_table) && !is_null($this->source_column);
    }

    /**
     * 컬럼 저장 방식 여부 확인
     */
    public function isColumnStorage(): bool
    {
        return $this->storage_type === 'column';
    }

    /**
     * JSON 저장 방식 여부 확인
     */
    public function isJsonStorage(): bool
    {
        return $this->storage_type === 'json';
    }
}
```

### 8.3 필드 저장 방식 판단

```
┌─────────────────────────────────────────────────────────────┐
│  storage_type 판단 로직                                      │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  if (source_table && source_column) {                       │
│      // 시스템 필드 (기존 DB 컬럼과 매핑)                    │
│      if (storage_type === 'column') {                       │
│          → products.{source_column} 또는                    │
│            materials.{source_column} 에서 직접 읽기/쓰기    │
│      } else if (storage_type === 'json') {                  │
│          → {json_path} 경로로 JSON 내 읽기/쓰기             │
│      }                                                       │
│  } else {                                                    │
│      // 커스텀 필드 (동적 정의)                              │
│      → attributes.{field_key} 에 저장                       │
│  }                                                           │
│                                                              │
└─────────────────────────────────────────────────────────────┘
```

---

## 9. 구현 계획

| 순서 | 작업 | 담당 | 예상 공수 |
|------|------|------|----------|
| 1 | group_id 코드 정의 | BE | 0.5일 |
| 2 | ItemFieldValidationService 구현 | BE | 1일 |
| 3 | ProductsController 수정 (검증 연동) | BE | 0.5일 |
| 4 | MaterialsController 수정 (검증 연동) | BE | 0.5일 |
| 5 | API 응답에 field_meta 포함 옵션 | BE | 0.5일 |
| 6 | DynamicFieldRenderer 컴포넌트 | FE | 2일 |
| 7 | 품목 등록/수정 폼 연동 | FE | 1일 |
| 8 | 테스트 및 QA | 공통 | 1일 |

**총 예상 공수: 7일**

---

## 10. 변경 이력

| 날짜 | 버전 | 변경 내용 | 작성자 |
|------|------|----------|--------|
| 2025-12-05 | 1.0 | 최초 작성 | - |
| 2025-12-08 | 1.1 | source_table/source_column 매핑 컬럼 추가, 모델 헬퍼 메서드 문서화 | - |
