- .gitignore에 이미 등록되어 있으나 과거 커밋으로 트래킹 중이던 파일 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
51 KiB
품목기준관리 동적 필드 타입 확장 설계
작성일: 2026-02-11 목적: 멀티테넌시 품목기준관리의 필드 타입 확장 및 범용 테이블 섹션 설계 범위: 프론트엔드 컴포넌트 설계 + 백엔드 API 계약 + 산업별 확장 구조
목차
- 배경 및 목표
- 현재 시스템 분석
- 확장 필드 타입 레지스트리
- 범용 테이블 섹션 설계
- 섹션 템플릿 라이브러리
- 산업별 확장 구조
- 백엔드 API 계약
- 프론트엔드 컴포넌트 아키텍처
- 조건부 표시 확장
- 검증 프레임워크
- 구현 로드맵
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 조합"을 확장한다.
- 필드 타입 확장: 6종 → 14종으로 확장 (입력 원자 단위)
- 범용 테이블 섹션: BOM 전용 → config 기반 범용 테이블로 일반화
- 섹션 템플릿 라이브러리: 산업별 표준 섹션 프리셋 제공
- 백엔드 key + type + config 체계: 백엔드가 스키마만 정의하면 프론트가 자동 렌더링
- 멀티테넌시 확장성: 테넌트마다 다른 항목 구성 가능
- 산업 불문 확장: 제조/공사/유통/물류 전방위 커버
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 |
customsource를 사용하면 백엔드에 새 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_fields와 item_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_type과 properties에 새로운 값이 들어올 뿐.
// 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 구현 착수