Files
sam-react-prod/claudedocs/architecture/[DESIGN-2026-02-11] dynamic-field-type-extension.md
유병철 113d82c254 chore(WEB): package-lock.json git 트래킹 제거
- .gitignore에 이미 등록되어 있으나 과거 커밋으로 트래킹 중이던 파일 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:13:31 +09:00

51 KiB

품목기준관리 동적 필드 타입 확장 설계

작성일: 2026-02-11 목적: 멀티테넌시 품목기준관리의 필드 타입 확장 및 범용 테이블 섹션 설계 범위: 프론트엔드 컴포넌트 설계 + 백엔드 API 계약 + 산업별 확장 구조


목차

  1. 배경 및 목표
  2. 현재 시스템 분석
  3. 확장 필드 타입 레지스트리
  4. 범용 테이블 섹션 설계
  5. 섹션 템플릿 라이브러리
  6. 산업별 확장 구조
  7. 백엔드 API 계약
  8. 프론트엔드 컴포넌트 아키텍처
  9. 조건부 표시 확장
  10. 검증 프레임워크
  11. 구현 로드맵

1. 배경 및 목표

1.1 현재 문제

품목기준관리(/master-data/item-master-data-management)는 동적 폼 구성 시스템이 이미 존재하지만, 필드 타입이 6가지로 제한되어 제조 ERP의 다양한 요구를 충족하지 못함.

현재 있는 것 없어서 부족한 것
textbox 다른 테이블 참조/검색 선택 (거래처, 품목, 고객)
number 복수 선택 (태그형)
dropdown 파일/이미지 업로드
checkbox 통화/금액 (단위 포함)
date 값+단위 조합 (100mm, 50kg)
textarea 범용 테이블/그리드 (BOM 외)

또한 BOM이 유일한 "테이블형 섹션"이지만, 제조 ERP에서는 공정, 품질검사, 구매처, 단가이력 등도 테이블 구조가 필요함.

1.2 설계 목표

핵심 원칙: "항목을 미리 정의"하지 않고 "필드 타입과 config 조합"을 확장한다.
  1. 필드 타입 확장: 6종 → 14종으로 확장 (입력 원자 단위)
  2. 범용 테이블 섹션: BOM 전용 → config 기반 범용 테이블로 일반화
  3. 섹션 템플릿 라이브러리: 산업별 표준 섹션 프리셋 제공
  4. 백엔드 key + type + config 체계: 백엔드가 스키마만 정의하면 프론트가 자동 렌더링
  5. 멀티테넌시 확장성: 테넌트마다 다른 항목 구성 가능
  6. 산업 불문 확장: 제조/공사/유통/물류 전방위 커버

1.3 설계 원칙

  • 하위 호환: 기존 6가지 필드 타입은 그대로 유지, 기존 코드 변경 없음
  • 점진적 확장: 새 필드 타입 추가 = 새 컴포넌트 1개 추가 + DynamicFieldRenderer switch 1줄 추가
  • config 기반: 필드의 동작은 field_type + properties 조합으로 결정
  • 백엔드 독립: 프론트 컴포넌트는 미리 만들고, 백엔드는 나중에 key-config 매핑만 추가

2. 현재 시스템 분석

2.1 아키텍처

품목기준관리 (Admin)                    품목 등록/수정 (User)
ItemMasterDataManagement.tsx           DynamicItemForm/index.tsx
  ↓ 구조 정의                            ↓ 구조 기반 렌더링
  Pages → Sections → Fields            GET /pages/{id}/structure
                  → BOM Items            ↓
                                       DynamicFieldRenderer (switch)
                                         → TextField
                                         → NumberField
                                         → DropdownField
                                         → CheckboxField
                                         → DateField
                                         → TextareaField

2.2 핵심 파일

파일 줄 수 역할
DynamicItemForm/index.tsx 1,048 메인 폼 컴포넌트
DynamicItemForm/types.ts 261 타입 정의
DynamicItemForm/fields/DynamicFieldRenderer.tsx 44 필드 타입 라우터
DynamicItemForm/sections/DynamicBOMSection.tsx 515 BOM 테이블 섹션
DynamicItemForm/hooks/ 7개 훅 상태/검증/조건부 표시
types/item-master-api.ts 745 API 타입 정의
ItemMasterDataManagement.tsx 1,005 Admin 관리 페이지

2.3 현재 필드 응답 구조 (ItemFieldResponse)

{
  id: number,
  field_name: string,           // "품목명"
  field_key: string | null,     // "98_item_name"
  field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea',
  is_required: boolean,
  placeholder: string | null,
  default_value: string | null,
  options: [{label, value}] | null,           // dropdown 옵션
  properties: Record<string, any> | null,     // 추가 메타데이터
  validation_rules: Record<string, any> | null,
  display_condition: Record<string, any> | null,
}

2.4 현재 섹션 타입

section.type: 'fields' | 'bom'
// 'fields' → DynamicFieldRenderer로 각 필드 렌더링
// 'bom'    → DynamicBOMSection (하드코딩된 BOM 전용 UI)

3. 확장 필드 타입 레지스트리

3.1 전체 필드 타입 목록

기존 유지 (6종)

field_type 컴포넌트 설명
textbox TextField 단일 행 텍스트
number NumberField 숫자 입력
dropdown DropdownField 단일 선택
checkbox CheckboxField 체크박스
date DateField 날짜 선택
textarea TextareaField 여러 행 텍스트

신규 추가 (8종)

field_type 컴포넌트 설명 우선순위
reference ReferenceField 다른 테이블 참조 검색/선택 🔴 Phase 1
multi-select MultiSelectField 복수 선택 (태그형) 🔴 Phase 1
file FileField 파일/이미지 업로드 🔴 Phase 1
currency CurrencyField 통화 금액 (포맷 + 단위) 🟡 Phase 2
unit-value UnitValueField 값 + 단위 조합 🟡 Phase 2
radio RadioField 라디오 버튼 그룹 🟡 Phase 2
toggle ToggleField On/Off 토글 스위치 🟢 Phase 3
computed ComputedField 읽기 전용 계산 필드 🟢 Phase 3

3.2 각 필드 타입별 상세 스펙


3.2.1 reference — 참조 룩업 필드

용도: 다른 테이블의 데이터를 검색하여 선택 (거래처, 품목, 고객, 공정, 현장, 차량 등)

UI: 검색 입력 + 드롭다운 팝업 (SearchableSelectionModal 기반)

properties 스키마:

{
  "source": "vendors",           // 참조할 데이터 소스 (필수) — 프리셋 또는 "custom"
  "displayField": "name",        // 선택 후 표시할 필드 (기본: "name")
  "valueField": "id",            // 저장할 값 필드 (기본: "id")
  "searchFields": ["name", "code"], // 검색 대상 필드 (기본: ["name"])
  "searchApiUrl": "/api/proxy/vendors", // 검색 API URL ("custom" source일 때 필수)
  "minSearchLength": 1,          // 최소 검색 글자 수 (기본: 1)
  "modalTitle": "거래처 선택",     // 모달 제목 (선택, 없으면 field_name + " 선택")
  "columns": [                   // 검색 결과 표시 컬럼 (선택)
    { "key": "code", "label": "코드", "width": "120px" },
    { "key": "name", "label": "이름" },
    { "key": "contact", "label": "연락처", "width": "150px" }
  ],
  "displayFormat": "{code} - {name}", // 선택 후 표시 포맷 (선택)
  "returnFields": ["id", "code", "name"] // 선택 시 폼에 저장할 필드들 (선택)
}

저장되는 값:

// 단일 값: valueField 기준
{ "vendor_id": "123" }

// returnFields 설정 시: 여러 필드 동시 저장
{ "vendor_id": "123", "vendor_code": "V-001", "vendor_name": "삼성전자" }

소스 프리셋 (프론트에서 미리 정의, 산업별 확장 가능):

source 산업 API displayField
vendors 공통 /api/proxy/vendors name
items 공통 /api/proxy/items name
customers 공통 /api/proxy/customers company_name
employees 공통 /api/proxy/employees name
processes 제조 /api/proxy/processes process_name
warehouses 공통 /api/proxy/warehouses name
materials 제조 /api/proxy/item-master/materials material_name
surface_treatments 제조 /api/proxy/item-master/surface-treatments treatment_name
sites 공사 /api/proxy/sites site_name
vehicles 물류 /api/proxy/vehicles plate_number
routes 물류 /api/proxy/routes route_name
stores 유통 /api/proxy/stores store_name
custom properties.searchApiUrl properties.displayField

custom source를 사용하면 백엔드에 새 API만 추가하면 프론트 코드 수정 없이 어떤 참조든 연결 가능.


3.2.2 multi-select — 복수 선택 필드

용도: 여러 항목을 동시에 선택 (태그/칩 형태)

UI: Combobox + 태그 칩

properties 스키마:

{
  "maxSelections": 5,            // 최대 선택 수 (선택, 기본: 무제한)
  "allowCustom": false,          // 사용자 직접 입력 허용 여부 (기본: false)
  "layout": "chips"              // "chips" | "list" (기본: "chips")
}

options 사용: 기존 dropdown과 동일한 [{label, value}] 형태

저장되는 값:

{ "applicable_processes": ["CUT", "BEND", "WELD", "PAINT"] }

3.2.3 file — 파일/이미지 업로드 필드

용도: 문서, 이미지, 도면 첨부

UI: 파일 선택 버튼 + 미리보기 (이미지일 경우)

properties 스키마:

{
  "accept": ".pdf,.doc,.docx",   // 허용 파일 타입 (기본: "*")
  "maxSize": 10485760,           // 최대 파일 크기 bytes (기본: 10MB)
  "maxFiles": 5,                 // 최대 파일 수 (기본: 1)
  "preview": true,               // 미리보기 표시 여부 (기본: true, 이미지 파일만)
  "uploadApiUrl": "/api/proxy/files/upload",  // 업로드 API (선택)
  "category": "drawing"          // 파일 카테고리 태그 (선택)
}

저장되는 값:

// 단일 파일
{ "drawing_file": { "fileId": "uuid-123", "fileName": "도면_v2.pdf", "fileUrl": "/files/uuid-123" } }

// 복수 파일
{ "attachments": [
    { "fileId": "uuid-123", "fileName": "도면.pdf", "fileUrl": "/files/uuid-123" },
    { "fileId": "uuid-456", "fileName": "사진.jpg", "fileUrl": "/files/uuid-456" }
  ]
}

3.2.4 currency — 통화/금액 필드

용도: 단가, 총액 등 금액 입력 (천 단위 콤마 + 통화 기호)

UI: 숫자 입력 + 통화 기호 + 천단위 포맷

properties 스키마:

{
  "currency": "KRW",            // 통화 코드 (기본: "KRW")
  "precision": 0,               // 소수점 자릿수 (기본: KRW=0, USD=2)
  "showSymbol": true,           // 통화 기호 표시 (기본: true)
  "allowNegative": false        // 음수 허용 (기본: false)
}

저장되는 값: 숫자 ({ "unit_price": 15000 })


3.2.5 unit-value — 값+단위 조합 필드

용도: 치수, 무게, 용량, 거리 등 (숫자 + 단위 동시 입력)

UI: 숫자 입력 + 단위 선택 드롭다운 (inline)

properties 스키마:

{
  "units": ["mm", "cm", "m", "inch"],  // 선택 가능 단위 목록 (필수)
  "defaultUnit": "mm",                 // 기본 단위 (선택)
  "precision": 1,                      // 소수점 자릿수 (기본: 0)
  "showConversion": false              // 단위 변환 표시 (기본: false)
}

저장되는 값: { "thickness": { "value": 2.5, "unit": "mm" } }


3.2.6 radio — 라디오 버튼 그룹

용도: 상호 배타적 선택 (3~5개 이내 옵션에 적합)

UI: 라디오 버튼 그룹 (수평/수직)

properties 스키마:

{
  "layout": "horizontal"         // "horizontal" | "vertical" (기본: "horizontal")
}

options 사용: dropdown과 동일한 [{label, value}]


3.2.7 toggle — 토글 스위치

용도: On/Off 상태 전환

UI: Switch 컴포넌트

properties 스키마:

{
  "onLabel": "활성",             // On 상태 라벨 (선택)
  "offLabel": "비활성",          // Off 상태 라벨 (선택)
  "onValue": "active",          // On 상태 저장값 (기본: "true")
  "offValue": "inactive"        // Off 상태 저장값 (기본: "false")
}

3.2.8 computed — 읽기 전용 계산 필드

용도: 다른 필드 값을 기반으로 자동 계산되는 필드 (표시 전용)

UI: 읽기 전용 표시 (배경색 구분)

properties 스키마:

{
  "formula": "{quantity} * {unit_price}",  // 계산식 (필수)
  "dependsOn": ["quantity", "unit_price"], // 의존 필드 키 목록 (필수)
  "format": "currency",                    // 표시 포맷: "number" | "currency" | "percent" (기본: "number")
  "precision": 0                           // 소수점 자릿수 (기본: 0)
}

3.3 field_type 확장 타입 정의 (TypeScript)

// 기존
type FieldType = 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';

// 확장
type ExtendedFieldType = FieldType
  | 'reference'      // Phase 1
  | 'multi-select'   // Phase 1
  | 'file'           // Phase 1
  | 'currency'       // Phase 2
  | 'unit-value'     // Phase 2
  | 'radio'          // Phase 2
  | 'toggle'         // Phase 3
  | 'computed';      // Phase 3

4. 범용 테이블 섹션 설계

4.1 현재 문제

현재: section.type === 'bom' → DynamicBOMSection (515줄, BOM 전용 하드코딩)
필요: 공정, 품질검사, 구매처, 단가이력 등도 테이블 필요

4.2 설계: section.type 확장

// 기존
section.type: 'fields' | 'bom'

// 확장
section.type: 'fields' | 'bom' | 'table'
//                               ↑ 신규: 범용 테이블

4.3 범용 테이블 섹션 config

section.properties에 테이블 설정을 담음:

{
  "table_config": {
    // 컬럼 정의 (핵심)
    "columns": [
      {
        "key": "process_code",          // 컬럼 키 (데이터 저장 키)
        "label": "공정코드",             // 컬럼 헤더
        "type": "reference",            // 셀 입력 타입 (필드 타입과 동일한 14종)
        "width": "150px",               // 컬럼 너비 (선택)
        "required": true,               // 필수 여부
        "config": {                     // 타입별 추가 설정 (properties와 동일 구조)
          "source": "processes",
          "displayField": "process_name",
          "searchFields": ["process_name", "process_code"]
        }
      },
      {
        "key": "process_name",
        "label": "공정명",
        "type": "textbox",
        "width": "200px",
        "readOnly": true,               // 참조 필드에서 자동 채움
        "autoFillFrom": "process_code.process_name"  // 자동 채움 소스
      },
      {
        "key": "quantity",
        "label": "수량",
        "type": "number",
        "width": "100px",
        "config": { "min": 0, "precision": 2 }
      },
      {
        "key": "unit",
        "label": "단위",
        "type": "dropdown",
        "width": "100px",
        "config": { "source": "unitOptions" }
      },
      {
        "key": "start_date",
        "label": "시작일",
        "type": "date",
        "width": "140px"
      },
      {
        "key": "note",
        "label": "비고",
        "type": "textbox"                // width 미지정 = 나머지 공간
      }
    ],

    // 행 동작
    "addable": true,                     // 행 추가 가능 (기본: true)
    "deletable": true,                   // 행 삭제 가능 (기본: true)
    "reorderable": true,                 // 행 순서 변경 가능 (기본: false)
    "maxRows": 100,                      // 최대 행 수 (선택, 기본: 무제한)
    "minRows": 0,                        // 최소 행 수 (선택, 기본: 0)

    // 표시
    "showRowNumber": true,               // 행 번호 표시 (기본: true)
    "showCheckbox": false,               // 행 선택 체크박스 (기본: false)
    "emptyMessage": "데이터가 없습니다.",  // 빈 상태 메시지

    // 요약행 (선택)
    "summaryRow": {
      "enabled": true,
      "columns": {
        "quantity": { "type": "sum", "label": "합계" },
        "amount": { "type": "sum", "format": "currency" }
      }
    },

    // 데이터 소스 (기존 데이터 로드용, 선택)
    "dataApiUrl": "/api/proxy/items/{itemId}/routings",
    "saveApiUrl": "/api/proxy/items/{itemId}/routings"
  }
}

4.4 기존 BOM과의 관계

DynamicBOMSection (기존) → 유지 (하위 호환)
DynamicTableSection (신규) → 범용 테이블

section.type === 'bom'   → DynamicBOMSection (기존 그대로)
section.type === 'table' → DynamicTableSection (신규)

점진적 마이그레이션: BOM도 나중에 type: 'table'로 전환 가능하지만, 당장은 불필요.

4.5 테이블 셀 = 필드 컴포넌트 재사용

테이블 각 셀의 입력은 DynamicFieldRenderer와 동일한 컴포넌트를 사용:

table column.type === "reference"  → ReferenceField (인라인 축소판)
table column.type === "number"     → NumberField
table column.type === "dropdown"   → DropdownField
table column.type === "date"       → DateField
table column.type === "currency"   → CurrencyField
... (14종 모두 사용 가능)

즉, 필드 타입 컴포넌트 1개 = 폼 필드에서도, 테이블 셀에서도 동일하게 사용.

4.6 테이블 섹션 저장 데이터 구조

{
  "table_section_123": [
    {
      "_rowId": "uuid-1",
      "process_code": "CUT-001",
      "process_name": "절단",
      "quantity": 10,
      "unit": "EA",
      "start_date": "2026-03-01",
      "note": "레이저 절단"
    }
  ]
}

5. 섹션 템플릿 라이브러리

5.1 전체 프리셋 목록 (산업 공통 + 산업별)

공통 프리셋

프리셋 ID 이름 type 설명
basic-info 기본정보 fields 코드, 이름, 유형, 상태, 비고
drawing 도면/문서 fields 파일 업로드 + 버전 관리
custom 사용자 정의 fields/table 빈 섹션 (직접 구성)

제조 프리셋

프리셋 ID 이름 type 설명
specifications 규격/치수 fields 두께, 너비, 높이, 무게, 공차
bom BOM (자재명세서) bom 기존 BOM 구조 유지
routing 공정/라우팅 table 공정코드, 작업시간, 작업장
quality-spec 품질검사 항목 table 검사항목, 규격, 허용치, 검사방법
procurement 구매정보 table 공급업체, 단가, 리드타임, MOQ
cost-breakdown 원가 구성 table 원가항목, 금액, 비율
inventory 재고 정보 fields 창고, 안전재고, 발주점

공사 프리셋

프리셋 ID 이름 type 설명
work-schedule 공정표 table 공종, 수량, 단가, 착수일, 완료일, 진행률
material-plan 자재투입계획 table 자재, 수량, 단위, 투입시기, 발주여부
labor-plan 인력투입계획 table 직종, 인원, 기간, 일단가, 금액
equipment-plan 장비투입계획 table 장비명, 규격, 수량, 기간, 단가
safety-checklist 안전점검 항목 table 점검항목, 점검주기, 담당자, 결과
site-info 현장정보 fields 현장명, 주소, 발주처, 감리사, 공사기간

유통 프리셋

프리셋 ID 이름 type 설명
pricing 가격정보 table 거래처유형, 단가, 할인율, 적용기간
packaging 포장정보 fields 포장단위, 박스수량, 바코드, 중량
store-allocation 매장배분 table 매장, 배분수량, 배분일, 상태
promotion 프로모션 table 프로모션명, 할인율, 시작일, 종료일, 조건

물류 프리셋

프리셋 ID 이름 type 설명
transport-spec 운송규격 fields 중량, 부피, 위험물등급, 보관온도, 적재방법
route-plan 배차/경로 table 출발지, 도착지, 거리, 소요시간, 운임
loading-plan 적재계획 table 품목, 수량, 중량, 적재순서, 위치
tracking 추적정보 table 일시, 위치, 상태, 온도, 비고

5.2 프리셋 상세 예시

work-schedule (공사 — 공정표)

{
  "preset_id": "work-schedule",
  "type": "table",
  "table_config": {
    "columns": [
      { "key": "work_type", "label": "공종", "type": "reference", "width": "180px",
        "config": { "source": "custom", "searchApiUrl": "/api/proxy/work-types",
                    "displayField": "name" }, "required": true },
      { "key": "quantity", "label": "수량", "type": "number", "width": "100px",
        "config": { "min": 0, "precision": 2 } },
      { "key": "unit", "label": "단위", "type": "dropdown", "width": "80px",
        "config": { "options": [
          {"label":"m²","value":"m2"}, {"label":"m³","value":"m3"},
          {"label":"m","value":"m"}, {"label":"EA","value":"EA"},
          {"label":"TON","value":"TON"}, {"label":"식","value":"SET"}
        ]}},
      { "key": "unit_price", "label": "단가", "type": "currency", "width": "130px",
        "config": { "currency": "KRW" } },
      { "key": "amount", "label": "금액", "type": "computed", "width": "140px",
        "config": { "formula": "{quantity} * {unit_price}", "format": "currency" } },
      { "key": "start_date", "label": "착수일", "type": "date", "width": "130px" },
      { "key": "end_date", "label": "완료일", "type": "date", "width": "130px" },
      { "key": "progress", "label": "진행률(%)", "type": "number", "width": "100px",
        "config": { "min": 0, "max": 100 } },
      { "key": "note", "label": "비고", "type": "textbox" }
    ],
    "addable": true,
    "deletable": true,
    "reorderable": true,
    "summaryRow": {
      "enabled": true,
      "columns": { "amount": { "type": "sum", "format": "currency" } }
    },
    "emptyMessage": "공정 항목을 추가하세요."
  }
}

route-plan (물류 — 배차/경로)

{
  "preset_id": "route-plan",
  "type": "table",
  "table_config": {
    "columns": [
      { "key": "seq", "label": "순번", "type": "number", "width": "70px" },
      { "key": "origin", "label": "출발지", "type": "reference", "width": "180px",
        "config": { "source": "custom", "searchApiUrl": "/api/proxy/locations",
                    "displayField": "name" }, "required": true },
      { "key": "destination", "label": "도착지", "type": "reference", "width": "180px",
        "config": { "source": "custom", "searchApiUrl": "/api/proxy/locations",
                    "displayField": "name" }, "required": true },
      { "key": "distance", "label": "거리", "type": "unit-value", "width": "120px",
        "config": { "units": ["km", "m"], "defaultUnit": "km", "precision": 1 } },
      { "key": "duration", "label": "소요시간(분)", "type": "number", "width": "110px" },
      { "key": "vehicle", "label": "차량", "type": "reference", "width": "150px",
        "config": { "source": "vehicles", "displayField": "plate_number" } },
      { "key": "freight", "label": "운임", "type": "currency", "width": "130px",
        "config": { "currency": "KRW" } },
      { "key": "note", "label": "비고", "type": "textbox" }
    ],
    "addable": true,
    "deletable": true,
    "reorderable": true,
    "emptyMessage": "경로를 추가하세요."
  }
}

pricing (유통 — 가격정보)

{
  "preset_id": "pricing",
  "type": "table",
  "table_config": {
    "columns": [
      { "key": "customer_type", "label": "거래처유형", "type": "dropdown", "width": "140px",
        "config": { "options": [
          {"label":"도매","value":"WHOLESALE"}, {"label":"소매","value":"RETAIL"},
          {"label":"온라인","value":"ONLINE"}, {"label":"특판","value":"SPECIAL"}
        ]}, "required": true },
      { "key": "unit_price", "label": "단가", "type": "currency", "width": "130px",
        "config": { "currency": "KRW" }, "required": true },
      { "key": "discount_rate", "label": "할인율(%)", "type": "number", "width": "100px",
        "config": { "min": 0, "max": 100, "precision": 1 } },
      { "key": "final_price", "label": "최종가", "type": "computed", "width": "130px",
        "config": { "formula": "{unit_price} * (1 - {discount_rate}/100)", "format": "currency" } },
      { "key": "valid_from", "label": "적용시작", "type": "date", "width": "130px" },
      { "key": "valid_to", "label": "적용종료", "type": "date", "width": "130px" },
      { "key": "note", "label": "비고", "type": "textbox" }
    ],
    "addable": true,
    "deletable": true,
    "emptyMessage": "가격 정보를 추가하세요."
  }
}

6. 산업별 확장 구조

6.1 핵심 개념: 4-Level 아키텍처

┌──────────────────────────────────────────────────────────────┐
│ Level 1: 필드 타입 컴포넌트 (14종)                             │
│ ─────────────────────────────────────────────────────────── │
│ 코드 레벨. 거의 안 바뀜.                                       │
│ textbox | number | dropdown | checkbox | date | textarea     │
│ reference | multi-select | file | currency | unit-value      │
│ radio | toggle | computed                                    │
│                                                              │
│ → UI 입력의 "원자(atom)" 단위. 모든 산업의 입력 형태를 커버.     │
│ → 새 컴포넌트 추가 = 완전히 새로운 입력 패러다임이 등장할 때만.    │
└──────────────────────────────────────────────────────────────┘
                            ▼
┌──────────────────────────────────────────────────────────────┐
│ Level 2: properties config (JSON)                            │
│ ─────────────────────────────────────────────────────────── │
│ 설정 레벨. 필드 타입의 동작을 결정. 코드 변경 없음.               │
│                                                              │
│ 같은 "reference" 타입이지만:                                   │
│   제조: { "source": "processes" }       → 공정 선택            │
│   공사: { "source": "custom",                                │
│           "searchApiUrl": "/api/proxy/work-types" } → 공종선택 │
│   물류: { "source": "vehicles" }        → 차량 선택            │
│   유통: { "source": "stores" }          → 매장 선택            │
│                                                              │
│ 같은 "unit-value" 타입이지만:                                  │
│   제조: { "units": ["mm","cm","m","inch"] }  → 치수           │
│   물류: { "units": ["km","m"] }              → 거리           │
│   유통: { "units": ["g","kg","ton"] }        → 중량           │
└──────────────────────────────────────────────────────────────┘
                            ▼
┌──────────────────────────────────────────────────────────────┐
│ Level 3: 섹션 프리셋 (JSON)                                   │
│ ─────────────────────────────────────────────────────────── │
│ 템플릿 레벨. 산업별 표준 섹션 구성. 코드 변경 없음.               │
│                                                              │
│ 제조: basic-info + specifications + bom + routing + quality   │
│ 공사: basic-info + site-info + work-schedule + material-plan  │
│ 유통: basic-info + packaging + pricing + store-allocation     │
│ 물류: basic-info + transport-spec + route-plan + loading-plan │
│                                                              │
│ → 관리자가 "섹션 추가" → 프리셋 선택 → 자동 구성               │
│ → 새 프리셋 = JSON 추가만, 컴포넌트 수정 없음                   │
└──────────────────────────────────────────────────────────────┘
                            ▼
┌──────────────────────────────────────────────────────────────┐
│ Level 4: reference sources (API URL)                         │
│ ─────────────────────────────────────────────────────────── │
│ 연결 레벨. 새 데이터 소스를 reference 필드에 연결. 코드 변경 없음. │
│                                                              │
│ 새 산업/도메인 추가 시:                                        │
│   1. 백엔드에 API 추가 (예: /api/proxy/work-types)            │
│   2. reference 필드에 source: "custom" + searchApiUrl 설정    │
│   3. 끝. 프론트 코드 수정 없음.                                │
│                                                              │
│ 자주 사용되는 source는 프리셋으로 등록:                          │
│   reference-sources.ts에 추가 → source: "work_types"로 단축   │
└──────────────────────────────────────────────────────────────┘

6.2 산업별 변경 범위 매트릭스

변경 항목 코드 변경 DB 변경 config 변경
새 필드 타입 추가 컴포넌트 1개 field_type 값 추가
새 reference 소스 추가 API 엔드포인트 source config
새 테이블 섹션 구성 section + properties table_config JSON
새 섹션 프리셋 추가 (또는 프리셋 JSON 1건) 프리셋 JSON
새 산업 진출 API들 프리셋 + source
테넌트별 커스터마이징 테넌트 config

핵심: "새 산업 진출" 시에도 프론트엔드 코드 변경 = 0줄. 백엔드 API + config JSON만 추가.

6.3 산업별 구성 예시

제조업 테넌트 (금속 가공)

페이지: 제품(FG) 등록
├── 기본정보 (fields)
│   ├── 품목코드 [textbox, required]
│   ├── 품목명 [textbox, required]
│   ├── 품목유형 [dropdown: FG/PT/SM/RM/CS]
│   ├── 단위 [dropdown: EA/SET/M]
│   └── 상태 [toggle: 활성/비활성]
├── 규격/치수 (fields)
│   ├── 두께 [unit-value: mm/cm/inch]
│   ├── 너비 [unit-value: mm/cm/m]
│   ├── 높이 [unit-value: mm/cm/m]
│   ├── 무게 [unit-value: g/kg/ton]
│   ├── 재질 [reference → materials]
│   └── 표면처리 [reference → surface_treatments]
├── BOM (bom) — 기존 유지
├── 공정/라우팅 (table)
│   └── [공정, 작업장, 셋업시간, 사이클타임, 비고]
├── 품질검사 (table)
│   └── [검사항목, 규격, 상한, 하한, 검사방법, 측정장비]
└── 도면 (fields)
    ├── 도면파일 [file: .pdf/.dwg/.dxf]
    ├── 도면번호 [textbox]
    └── 도면버전 [textbox]

공사관리 테넌트 (건설)

페이지: 공사 항목 등록
├── 기본정보 (fields)
│   ├── 항목코드 [textbox, required]
│   ├── 항목명 [textbox, required]
│   ├── 공사구분 [dropdown: 토목/건축/설비/전기]
│   └── 상태 [toggle]
├── 현장정보 (fields)
│   ├── 현장 [reference → sites]
│   ├── 발주처 [reference → customers]
│   ├── 감리사 [reference → vendors]
│   ├── 착공일 [date]
│   ├── 준공예정일 [date]
│   └── 공사금액 [currency: KRW]
├── 공정표 (table)
│   └── [공종, 수량, 단위, 단가, 금액(computed), 착수일, 완료일, 진행률]
├── 자재투입계획 (table)
│   └── [자재(reference→items), 수량, 단위, 투입시기, 발주여부(checkbox)]
├── 인력투입계획 (table)
│   └── [직종, 인원, 기간(일), 일단가(currency), 금액(computed)]
└── 안전점검 (table)
    └── [점검항목, 점검주기(dropdown), 담당자(reference→employees), 최근점검일(date)]

유통업 테넌트 (도소매)

페이지: 상품 등록
├── 기본정보 (fields)
│   ├── 상품코드 [textbox, required]
│   ├── 상품명 [textbox, required]
│   ├── 카테고리 [reference → categories]
│   ├── 브랜드 [reference → brands]
│   └── 상태 [toggle]
├── 포장정보 (fields)
│   ├── 포장단위 [dropdown: 낱개/박스/팔레트]
│   ├── 입수량 [number]
│   ├── 바코드 [textbox]
│   ├── 중량 [unit-value: g/kg]
│   └── 상품이미지 [file: .jpg/.png, maxFiles:5]
├── 가격정보 (table)
│   └── [거래처유형, 단가, 할인율, 최종가(computed), 적용기간]
├── 매장배분 (table)
│   └── [매장(reference→stores), 배분수량, 배분일, 상태(dropdown)]
└── 프로모션 (table)
    └── [프로모션명, 할인율, 시작일, 종료일, 적용조건]

물류업 테넌트 (운송)

페이지: 화물 등록
├── 기본정보 (fields)
│   ├── 화물코드 [textbox, required]
│   ├── 화물명 [textbox, required]
│   ├── 화물유형 [dropdown: 일반/냉장/냉동/위험물]
│   └── 상태 [toggle]
├── 운송규격 (fields)
│   ├── 총중량 [unit-value: kg/ton]
│   ├── 부피 [unit-value: m³/CBM]
│   ├── 위험물등급 [dropdown: 1~9등급/해당없음]
│   ├── 보관온도 [unit-value: ℃]
│   ├── 적재방법 [radio: 팔레트/산적/컨테이너]
│   └── 특수요구사항 [textarea]
├── 배차/경로 (table)
│   └── [출발지, 도착지, 거리, 소요시간, 차량(reference), 운임(currency)]
├── 적재계획 (table)
│   └── [품목(reference→items), 수량, 중량, 적재순서, 위치]
└── 추적정보 (table)
    └── [일시(date), 위치, 상태(dropdown), 온도(number), 비고]

6.4 코드 변경이 필요한 예외 케이스

14종 필드 타입으로 커버 불가능한 완전히 새로운 입력 패러다임:

새 입력 패러다임 필요한 field_type 작업량
지도 위치 선택 (GPS 좌표) map-picker 컴포넌트 1개 + switch 1줄
간트차트 편집 gantt (섹션 타입) 섹션 컴포넌트 1개
전자서명/도장 signature 컴포넌트 1개 + switch 1줄
바코드/QR 스캔 barcode-scan 컴포넌트 1개 + switch 1줄
조직도 선택 org-chart-picker 컴포넌트 1개 + switch 1줄
색상 선택 (컬러피커) color-picker 컴포넌트 1개 + switch 1줄

이런 경우에도 컴포넌트 1개 파일 추가 + switch문 1줄 추가로 끝. 기존 코드 수정 없음. 다른 필드 타입에 영향 없음.

6.5 확장 가능성 요약

Q: 새 산업(예: 의료)을 추가하려면?
A: 프론트 코드 변경 0줄.
   1. 백엔드에 의료 도메인 API 추가 (환자, 의약품, 의료기기 등)
   2. reference source config 추가 (JSON)
   3. 의료 섹션 프리셋 추가 (JSON)
   4. 테넌트 생성 시 의료 프리셋 자동 적용

Q: 새 필드 타입(예: 지도)이 필요하면?
A: MapPickerField.tsx 1개 생성 + switch 1줄 추가.
   기존 14종 컴포넌트/기존 config/기존 프리셋 전부 영향 없음.

Q: 기존 테넌트가 산업을 추가하면? (제조 + 물류 겸업)
A: 해당 테넌트의 페이지에 물류 프리셋 섹션만 추가.
   코드 변경 없음. Admin UI에서 클릭으로 완료.

7. 백엔드 API 계약

7.1 field_type 확장 — DB 변경

-- item_fields 테이블의 field_type 컬럼 확장
-- 기존: ENUM('textbox','number','dropdown','checkbox','date','textarea')
-- 변경: VARCHAR(30) 또는 ENUM 확장

ALTER TABLE item_fields
MODIFY COLUMN field_type VARCHAR(30) NOT NULL DEFAULT 'textbox';

-- 허용 값: textbox, number, dropdown, checkbox, date, textarea,
--          reference, multi-select, file, currency, unit-value, radio, toggle, computed
-- 향후 추가 가능: map-picker, signature, barcode-scan 등

7.2 section type 확장

ALTER TABLE item_sections
MODIFY COLUMN type VARCHAR(20) NOT NULL DEFAULT 'fields';

-- 허용 값: fields, bom, table
-- 향후 추가 가능: gantt, calendar 등

7.3 properties 필드 활용

기존 properties 컬럼 (JSON 타입)이 이미 item_fieldsitem_sections 테이블에 존재함. 신규 필드 타입의 config는 이 컬럼에 저장.

-- reference 타입 필드
UPDATE item_fields SET
  field_type = 'reference',
  properties = '{"source":"vendors","displayField":"name","searchFields":["name","code"]}'
WHERE id = 100;

-- table 타입 섹션
UPDATE item_sections SET
  type = 'table',
  properties = '{"table_config":{"columns":[...],"addable":true}}'
WHERE id = 50;

7.4 Init API / Page Structure API — 변경 없음

기존 응답 구조 그대로. field_typeproperties에 새로운 값이 들어올 뿐.

// GET /v1/item-master/pages/{id}/structure — 응답 구조 동일
{
  "page": { ... },
  "sections": [
    {
      "section": { "type": "fields", ... },
      "fields": [
        { "field": { "field_type": "reference", "properties": { "source": "vendors" } } }
      ]
    },
    {
      "section": {
        "type": "table",
        "properties": { "table_config": { "columns": [...] } }
      },
      "fields": [],
      "bom_items": []
    }
  ]
}

7.5 테이블 섹션 데이터 CRUD API (신규)

GET  /v1/items/{itemId}/section-data/{sectionId}
→ { "data": [{ "process": "CUT-001", "cycle_time": 5.0, ... }, ...] }

PUT  /v1/items/{itemId}/section-data/{sectionId}
← { "rows": [{ "process": "CUT-001", "cycle_time": 5.0, ... }, ...] }

POST /v1/items/{itemId}/section-data/{sectionId}
← { "process": "CUT-001", "cycle_time": 5.0, ... }

DELETE /v1/items/{itemId}/section-data/{sectionId}/{rowId}

7.6 Reference 필드 검색 API

기존 API 활용 + custom source:

source API 비고
vendors GET /v1/vendors?search={q}&size=20 기존
items GET /v1/items?search={q}&size=20 기존
customers GET /v1/customers?search={q}&size=20 기존
employees GET /v1/employees?search={q}&size=20 기존
processes GET /v1/processes?search={q}&size=20 기존
warehouses GET /v1/warehouses?search={q}&size=20 기존
materials GET /v1/item-master/materials?search={q} 기존
custom properties.searchApiUrl?search={q}&size=20 신규 산업별 API

새 산업 추가 시: 해당 도메인 API 생성 → source: "custom" + searchApiUrl 설정

7.7 파일 업로드 API

POST   /v1/files/upload     ← multipart/form-data
GET    /v1/files/{fileId}   → binary
DELETE /v1/files/{fileId}

8. 프론트엔드 컴포넌트 아키텍처

8.1 파일 구조 (신규 추가분)

DynamicItemForm/
├── fields/
│   ├── DynamicFieldRenderer.tsx     # 수정: switch문 확장
│   ├── TextField.tsx                # 기존 유지
│   ├── NumberField.tsx              # 기존 유지
│   ├── DropdownField.tsx            # 기존 유지
│   ├── CheckboxField.tsx            # 기존 유지
│   ├── DateField.tsx                # 기존 유지
│   ├── TextareaField.tsx            # 기존 유지
│   ├── ReferenceField.tsx           # ★ 신규 Phase 1
│   ├── MultiSelectField.tsx         # ★ 신규 Phase 1
│   ├── FileField.tsx                # ★ 신규 Phase 1
│   ├── CurrencyField.tsx            # ★ 신규 Phase 2
│   ├── UnitValueField.tsx           # ★ 신규 Phase 2
│   ├── RadioField.tsx               # ★ 신규 Phase 2
│   ├── ToggleField.tsx              # ★ 신규 Phase 3
│   └── ComputedField.tsx            # ★ 신규 Phase 3
├── sections/
│   ├── DynamicBOMSection.tsx        # 기존 유지
│   ├── DynamicTableSection.tsx      # ★ 신규 Phase 1
│   └── TableCellRenderer.tsx        # ★ 신규 Phase 1
├── presets/
│   ├── index.ts                     # ★ 신규: 프리셋 레지스트리
│   └── section-presets.ts           # ★ 신규: 전 산업 프리셋 정의
└── config/
    └── reference-sources.ts         # ★ 신규: 참조 소스 프리셋

8.2 DynamicFieldRenderer 확장

export function DynamicFieldRenderer(props: DynamicFieldRendererProps) {
  switch (props.field.field_type) {
    // 기존 6종 (변경 없음)
    case 'textbox':     return <TextField {...props} />;
    case 'number':      return <NumberField {...props} />;
    case 'dropdown':    return <DropdownField {...props} />;
    case 'checkbox':    return <CheckboxField {...props} />;
    case 'date':        return <DateField {...props} />;
    case 'textarea':    return <TextareaField {...props} />;
    // Phase 1
    case 'reference':     return <ReferenceField {...props} />;
    case 'multi-select':  return <MultiSelectField {...props} />;
    case 'file':          return <FileField {...props} />;
    // Phase 2
    case 'currency':      return <CurrencyField {...props} />;
    case 'unit-value':    return <UnitValueField {...props} />;
    case 'radio':         return <RadioField {...props} />;
    // Phase 3
    case 'toggle':        return <ToggleField {...props} />;
    case 'computed':      return <ComputedField {...props} />;
    default:
      return <TextField {...props} />;
  }
}

8.3 테이블 셀 = 필드 컴포넌트 재사용

// sections/TableCellRenderer.tsx
// DynamicFieldRenderer를 테이블 셀용으로 래핑 (축소 UI)
export function TableCellRenderer({ column, value, onChange }: TableCellProps) {
  // column config → ItemFieldResponse 호환 객체로 변환
  const fieldLike: ItemFieldResponse = {
    field_type: column.type,
    field_name: column.label,
    properties: column.config,
    options: column.config?.options,
    is_required: column.required || false,
    // ... 최소 필수 필드
  };

  return (
    <DynamicFieldRenderer
      field={fieldLike}
      value={value}
      onChange={onChange}
      compact={true}  // 테이블 셀용 축소 모드
    />
  );
}

8.4 참조 소스 프리셋

// config/reference-sources.ts
export const REFERENCE_SOURCES: Record<string, ReferenceSourceConfig> = {
  // 공통
  vendors:     { apiUrl: '/api/proxy/vendors', displayField: 'name', ... },
  items:       { apiUrl: '/api/proxy/items', displayField: 'name', ... },
  customers:   { apiUrl: '/api/proxy/customers', displayField: 'company_name', ... },
  employees:   { apiUrl: '/api/proxy/employees', displayField: 'name', ... },
  warehouses:  { apiUrl: '/api/proxy/warehouses', displayField: 'name', ... },
  // 제조
  processes:   { apiUrl: '/api/proxy/processes', displayField: 'process_name', ... },
  materials:   { apiUrl: '/api/proxy/item-master/materials', displayField: 'material_name', ... },
  surface_treatments: { apiUrl: '/api/proxy/item-master/surface-treatments', ... },
  // 공사
  sites:       { apiUrl: '/api/proxy/sites', displayField: 'site_name', ... },
  // 물류
  vehicles:    { apiUrl: '/api/proxy/vehicles', displayField: 'plate_number', ... },
  routes:      { apiUrl: '/api/proxy/routes', displayField: 'route_name', ... },
  // 유통
  stores:      { apiUrl: '/api/proxy/stores', displayField: 'store_name', ... },
};
// "custom" source → properties.searchApiUrl 직접 사용

9. 조건부 표시 확장

9.1 현재 (유지)

{ "fieldKey": "item_type", "expectedValue": "FG", "targetFieldIds": ["150"] }

9.2 확장: 비교 연산자 지원

{ "fieldKey": "item_type", "operator": "in", "expectedValue": ["FG","PT"], "targetFieldIds": ["150"] }
operator 설명 하위 호환
equals 같음 (기본) operator 없으면 equals
not_equals 다름
in 배열 포함
not_in 배열 미포함
is_empty 비어있음
is_not_empty 비어있지 않음
greater_than 초과
less_than 미만

10. 검증 프레임워크

10.1 필드 타입별 추가 검증

field_type 추가 검증
reference 선택 항목 존재 여부
multi-select maxSelections 초과
file maxSize, accept 타입, maxFiles
currency allowNegative, precision
unit-value 값이 숫자, 단위가 유효
computed 검증 없음 (자동 계산)

10.2 테이블 섹션 검증

function validateTableRows(rows, columns): string[] {
  const errors = [];
  rows.forEach((row, idx) => {
    columns.forEach(col => {
      if (col.required && !row[col.key]) {
        errors.push(`${idx + 1}행: ${col.label}은(는) 필수입니다.`);
      }
    });
  });
  return errors;
}

11. 구현 로드맵

Phase 1: 핵심 확장 (🔴)

작업 예상 줄 수
ReferenceField ~200
MultiSelectField ~120
FileField ~180
DynamicTableSection + TableCellRenderer ~450
reference-sources.ts ~120
타입 정의 확장 +50
DynamicFieldRenderer switch 확장 +10
~1,130줄 신규, ~30줄 수정

Phase 2: 편의 필드 (🟡)

작업 예상 줄 수
CurrencyField ~80
UnitValueField ~100
RadioField ~60
섹션 프리셋 라이브러리 (전 산업) ~400
프리셋 선택 UI +100
~740줄 신규

Phase 3: 고급 필드 (🟢)

작업 예상 줄 수
ToggleField ~50
ComputedField ~120
조건부 표시 연산자 확장 +40
테이블 검증 강화 +60
~270줄 신규

백엔드 작업 (프론트와 병렬)

작업 설명
field_type 컬럼 확장 VARCHAR(30)
section type 확장 'table' 추가
테이블 데이터 API section-data CRUD
산업별 도메인 API 해당 산업 진출 시 추가
프리셋 시딩 테넌트 생성 시 산업별 프리셋 자동 적용

부록

A. 기존 코드 영향 분석

기존 파일 변경 내용
DynamicFieldRenderer.tsx switch 추가 +8 case문
DynamicItemForm/index.tsx 섹션 렌더링 +10줄 (table case)
types.ts 타입 확장 field_type union + 신규 인터페이스
item-master-api.ts field_type 확장 union 값 추가
기존 필드 컴포넌트 6개 변경 없음
DynamicBOMSection 변경 없음
hooks 7개 변경 없음

B. 전체 아키텍처 다이어그램

┌───────────────────────────────────────────────────────────┐
│                    Admin (품목기준관리)                       │
│  → 산업 선택 → 프리셋 선택 → 필드 config 설정                │
└────────────────────┬──────────────────────────────────────┘
                     │ 저장 (field_type + properties JSON)
                     ▼
┌───────────────────────────────────────────────────────────┐
│                      백엔드 DB                              │
│  item_pages → item_sections → item_fields                  │
│              type: fields/bom/table                        │
│              properties: { table_config / field config }    │
│                                                            │
│  field_type (14종): 모든 산업의 입력 원자 단위                 │
│  properties (JSON): 산업/테넌트별 동작 결정                   │
└────────────────────┬──────────────────────────────────────┘
                     │ 조회 (기존 API 구조 그대로)
                     ▼
┌───────────────────────────────────────────────────────────┐
│                User (품목 등록/수정)                          │
│  DynamicItemForm                                           │
│  ├─ DynamicFieldRenderer (14종 switch)                     │
│  │   └─ 각 컴포넌트가 properties를 읽어 동작 결정              │
│  ├─ DynamicBOMSection (기존 유지)                            │
│  └─ DynamicTableSection (columns config 기반 렌더링)         │
│      └─ TableCellRenderer → DynamicFieldRenderer 재사용     │
└───────────────────────────────────────────────────────────┘

문서 버전: 1.1 (산업별 확장 구조 추가) 마지막 업데이트: 2026-02-11 다음 단계: 백엔드 검토 → Phase 1 구현 착수