Files
sam-docs/system/item-master-integration.md
권혁성 d4e5f62413 docs: [종합정비] Phase 1 시스템 현황 문서 14개 작성
- system/overview.md: 전체 아키텍처 개요
- system/api-structure.md: API 구조 (220 모델, 1027 엔드포인트, 18 라우트 도메인)
- system/react-structure.md: React 구조 (249 페이지, 612 컴포넌트)
- system/mng-structure.md: MNG 구조 (171 컨트롤러, 436 Blade 뷰)
- system/docker-setup.md: Docker 7 컨테이너 구성
- system/database/README.md + 9개 도메인 스키마 (270+ 테이블)
  - core, hr, sales, production, finance, boards, files, system, erp-analysis

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:03:13 +09:00

27 KiB

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 포함:

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
{
  "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 저장 시 검증 흐름

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에서 사용

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 컴포넌트 예시

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) 연동

-- 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 모델

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 모델

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 매핑 컬럼 추가, 모델 헬퍼 메서드 문서화 -