# 동적 필드 타입 확장 — 백엔드 API 스펙

> 작성일: 2026-02-12
> 프론트 구현 완료: `[IMPL-2026-02-11] dynamic-field-components.md`
> 설계 근거: `[DESIGN-2026-02-11] dynamic-field-type-extension.md`

---

## 요약

프론트엔드에서 **14종 필드 타입** + **table 섹션 타입** + **조건부 표시 연산자 9종**을 렌더링할 준비가 완료되었습니다.

백엔드에서 해당 `field_type`과 `properties` JSON을 DB에 저장하고 API로 반환하면, 프론트에서 추가 작업 없이 자동 렌더링됩니다.

---

## 1. field_type 확장

### 현재 (6종)
```
textbox | number | dropdown | checkbox | date | textarea
```

### 추가 필요 (8종)
```
reference | multi-select | file | currency | unit-value | radio | toggle | computed
```

### 전체 (14종)
```sql
-- item_fields.field_type 컬럼의 허용값
ENUM('textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea',
     'reference', 'multi-select', 'file',
     'currency', 'unit-value', 'radio',
     'toggle', 'computed')
```

### 영향 범위
- `item_fields` 테이블의 `field_type` 컬럼
- `item_master_fields` 테이블의 `field_type` 컬럼 (deprecated지만 아직 사용 중이면)
- 필드 생성/수정 API의 validation rule

---

## 2. 각 field_type별 properties JSON 스펙

모든 설정은 기존 `item_fields.properties` JSON 컬럼에 저장합니다.

### 2-1. reference (다른 테이블 검색/선택)

```jsonc
{
  "source": "vendors",        // 필수. 프리셋: vendors|items|customers|employees|warehouses|processes|equipment|sites|vehicles
  "displayField": "name",     // 선택. 기본 "name"
  "valueField": "id",         // 선택. 기본 "id"
  "searchFields": ["name"],   // 선택. 검색 대상 필드
  "searchApiUrl": "/api/...", // source="custom"일 때만 필수
  "columns": [                // 선택. 검색 모달 표시 컬럼
    { "key": "code", "label": "코드", "width": "100px" },
    { "key": "name", "label": "이름" }
  ],
  "displayFormat": "{code} - {name}",  // 선택. 선택 후 표시 포맷
  "returnFields": ["id", "code", "name"] // 선택. 프론트에 반환할 필드
}
```

**저장값**: `string` (선택한 항목의 valueField 값, 기본 id)

**프론트 동작**: source 프리셋이 있으면 `/api/proxy/{source}?search={query}` 호출. 없으면 searchApiUrl 사용.

---

### 2-2. multi-select (복수 선택 태그)

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

**options**: 기존 dropdown과 동일 `[{label, value}]` — `item_fields.options` 컬럼 사용

**저장값**: `string[]` JSON (예: `["CUT","BEND","WELD"]`)

---

### 2-3. file (파일/이미지 첨부)

```jsonc
{
  "accept": ".pdf,.doc,.jpg,.png",  // 선택. 허용 확장자 (기본: "*")
  "maxSize": 10485760,              // 선택. 최대 파일 크기 bytes (기본: 10MB)
  "maxFiles": 3,                    // 선택. 최대 파일 수 (기본: 1)
  "preview": true,                  // 선택. 이미지 미리보기 (기본: true)
  "category": "drawing"             // 선택. 파일 카테고리 태그
}
```

**저장값**: 파일 업로드 후 반환된 URL 또는 file ID

**필요 API**: `POST /v1/files/upload` (multipart) — 이미 있으면 그대로 사용

---

### 2-4. currency (통화 금액)

```jsonc
{
  "currency": "KRW",       // 선택. 기본 "KRW". 지원: KRW|USD|EUR|JPY|CNY|GBP
  "precision": 0,          // 선택. 소수점 자리 (기본: 0)
  "showSymbol": true,      // 선택. 통화 기호 표시 (기본: true)
  "allowNegative": false   // 선택. 음수 허용 (기본: false)
}
```

**저장값**: `number` (포맷 없는 원시 숫자)

---

### 2-5. unit-value (값 + 단위 조합)

```jsonc
{
  "units": [                // 필수. 단위 목록
    { "label": "mm", "value": "mm" },
    { "label": "cm", "value": "cm" },
    { "label": "m", "value": "m" }
  ],
  "defaultUnit": "mm",     // 선택. 기본 단위
  "precision": 2            // 선택. 소수점 자리
}
```

**저장값**: `{"value": 100, "unit": "mm"}` JSON

---

### 2-6. radio (라디오 버튼 그룹)

```jsonc
{
  "layout": "horizontal"   // 선택. "horizontal" | "vertical" (기본: "vertical")
}
```

**options**: 기존 dropdown과 동일 `[{label, value}]` — `item_fields.options` 컬럼 사용

**저장값**: `string` (선택한 value)

---

### 2-7. toggle (On/Off 스위치)

```jsonc
{
  "onLabel": "사용",        // 선택. 기본 "ON"
  "offLabel": "미사용",     // 선택. 기본 "OFF"
  "onValue": "Y",          // 선택. 기본 "true"
  "offValue": "N"           // 선택. 기본 "false"
}
```

**저장값**: `string` (onValue 또는 offValue)

---

### 2-8. computed (계산 필드, 읽기전용)

```jsonc
{
  "formula": "unit_price * quantity",  // 필수. 수식 (field_key 참조)
  "dependsOn": ["unit_price", "quantity"], // 필수. 의존 필드 field_key 목록
  "format": "currency",               // 선택. "currency" | "number" | "percent"
  "precision": 0                       // 선택. 소수점 자리
}
```

**저장값**: `number` (프론트에서 자동 계산, 백엔드 저장 시에도 계산 검증 권장)

**지원 연산**: `+`, `-`, `*`, `/`, `(`, `)` — field_key를 변수로 사용

---

## 3. section type 확장

### 현재
```sql
item_sections.type: ENUM('fields', 'bom')
```

### 추가 필요
```sql
item_sections.type: ENUM('fields', 'bom', 'table')
```

### table 섹션의 properties JSON

`item_sections.properties` 컬럼 (신규 또는 기존 description 등과 함께):

```jsonc
{
  "tableConfig": {
    "columns": [
      {
        "key": "process_name",      // 컬럼 키
        "label": "공정명",          // 컬럼 헤더
        "fieldType": "reference",   // 셀 입력 타입 (ItemFieldType)
        "width": "150px",          // 선택
        "isRequired": true,        // 선택
        "isReadonly": false,       // 선택
        "options": [...],          // dropdown/radio/multi-select용
        "properties": {            // 해당 필드 타입의 properties
          "source": "processes"
        }
      },
      {
        "key": "cycle_time",
        "label": "사이클타임(초)",
        "fieldType": "number",
        "width": "100px"
      }
    ],
    "minRows": 1,               // 선택. 최소 행 수
    "maxRows": 30,              // 선택. 최대 행 수
    "summaryRow": [             // 선택. 합계행
      { "columnKey": "cycle_time", "type": "sum" },
      { "columnKey": "setup_time", "type": "avg" }
    ]
  }
}
```

**summaryRow type**: `"sum"` | `"avg"` | `"count"` | `"label"`

### table 섹션 데이터 저장/조회 API

```
GET  /v1/items/{itemId}/section-data/{sectionId}
PUT  /v1/items/{itemId}/section-data/{sectionId}
```

**요청/응답 body**:
```jsonc
{
  "rows": [
    { "process_name": "절단", "cycle_time": 30, "setup_time": 10 },
    { "process_name": "절곡", "cycle_time": 45, "setup_time": 15 }
  ]
}
```

> 이미 유사한 API가 있으면 그대로 사용. 없으면 신규 생성 필요.

---

## 4. display_condition 연산자 확장

### 현재
```jsonc
// item_fields.display_condition
{
  "fieldConditions": [
    {
      "fieldKey": "item_type",
      "expectedValue": "FG",          // 정확히 일치 (equals만)
      "targetFieldIds": ["10", "11"]
    }
  ]
}
```

### 확장 (operator 필드 추가)

```jsonc
{
  "fieldConditions": [
    {
      "fieldKey": "item_type",
      "operator": "in",                // 신규: 연산자
      "expectedValue": "FG,PT",        // in/not_in은 콤마 구분
      "targetFieldIds": ["10", "11"]
    },
    {
      "fieldKey": "quantity",
      "operator": "greater_than",
      "expectedValue": "100",
      "targetSectionIds": ["5"]
    }
  ]
}
```

### 지원 연산자 목록

| operator | 설명 | expectedValue 형식 | 예시 |
|----------|------|-------------------|------|
| `equals` | 정확히 일치 (기본값) | 단일값 | `"FG"` |
| `not_equals` | 불일치 | 단일값 | `"RM"` |
| `in` | 목록 중 하나 일치 | 콤마 구분 | `"FG,PT,SM"` |
| `not_in` | 목록 중 어느 것에도 불일치 | 콤마 구분 | `"RM,CS"` |
| `greater_than` | 초과 (숫자) | 숫자 문자열 | `"100"` |
| `less_than` | 미만 (숫자) | 숫자 문자열 | `"0"` |
| `gte` | 이상 (숫자) | 숫자 문자열 | `"50"` |
| `lte` | 이하 (숫자) | 숫자 문자열 | `"1000"` |
| `contains` | 부분 문자열 포함 | 문자열 | `"강판"` |

**하위호환**: `operator`가 없으면 프론트에서 `equals`로 처리. 기존 데이터 마이그레이션 불필요.

---

## 5. 기존 API 변경 없음 (확인)

다음 API들은 이미 `field_type`과 `properties`를 통과시키므로 **변경 불필요**:

| API | 용도 | 비고 |
|-----|------|------|
| `GET /v1/item-master/pages/{id}/structure` | 페이지 구조 조회 | field_type, properties 이미 반환 |
| `POST /v1/item-master/fields` | 필드 생성 | field_type, properties 이미 수신 |
| `PUT /v1/item-master/fields/{id}` | 필드 수정 | 동일 |
| `GET /v1/item-master/init` | 초기화 조회 | field_type 이미 반환 |

**변경이 필요한 부분**:
1. `field_type` validation rule에 8종 추가
2. `item_sections.type`에 `'table'` 추가
3. (선택) table 섹션 데이터 저장/조회 API 신규

---

## 6. 작업 우선순위 제안

### 즉시 (프론트와 바로 연동 가능)

| # | 작업 | 난이도 | 설명 |
|---|------|--------|------|
| 1 | field_type 허용값 8종 추가 | 낮음 | DB ENUM 또는 validation 수정만 |
| 2 | display_condition operator 저장 허용 | 낮음 | JSON이므로 스키마 변경 없음 |

### 다음 단계

| # | 작업 | 난이도 | 설명 |
|---|------|--------|------|
| 3 | section type 'table' 추가 | 중간 | DB + API validation |
| 4 | table 섹션 데이터 API | 중간 | 신규 엔드포인트 (CRUD) |
| 5 | reference 소스별 검색 API | 높음 | 각 소스(vendors, items 등)의 검색 API 표준화 |

### 나중 (필요 시)

| # | 작업 | 난이도 | 설명 |
|---|------|--------|------|
| 6 | 파일 업로드 API | 높음 | `POST /v1/files/upload` (이미 있으면 불필요) |
| 7 | computed 필드 서버사이드 검증 | 중간 | 저장 시 수식 재계산하여 값 검증 |

---

## 7. 테스트 방법

백엔드 작업 완료 후 확인 순서:

1. **페이지 빌더** (`/dev/page-builder`)에서 신규 field_type으로 필드 추가
2. **품목 등록** 페이지에서 해당 필드가 올바르게 렌더링되는지 확인
3. **저장/조회** 사이클 테스트 (값이 properties 형식에 맞게 저장/복원되는지)

---

## 부록: 프론트 파일 매핑

| field_type | 프론트 컴포넌트 | 파일 |
|------------|---------------|------|
| textbox | TextField | `DynamicItemForm/fields/TextField.tsx` |
| number | NumberField | `DynamicItemForm/fields/NumberField.tsx` |
| dropdown | DropdownField | `DynamicItemForm/fields/DropdownField.tsx` |
| checkbox | CheckboxField | `DynamicItemForm/fields/CheckboxField.tsx` |
| date | DateField | `DynamicItemForm/fields/DateField.tsx` |
| textarea | TextareaField | `DynamicItemForm/fields/TextareaField.tsx` |
| **reference** | **ReferenceField** | `DynamicItemForm/fields/ReferenceField.tsx` |
| **multi-select** | **MultiSelectField** | `DynamicItemForm/fields/MultiSelectField.tsx` |
| **file** | **FileField** | `DynamicItemForm/fields/FileField.tsx` |
| **currency** | **CurrencyField** | `DynamicItemForm/fields/CurrencyField.tsx` |
| **unit-value** | **UnitValueField** | `DynamicItemForm/fields/UnitValueField.tsx` |
| **radio** | **RadioField** | `DynamicItemForm/fields/RadioField.tsx` |
| **toggle** | **ToggleField** | `DynamicItemForm/fields/ToggleField.tsx` |
| **computed** | **ComputedField** | `DynamicItemForm/fields/ComputedField.tsx` |

| section type | 프론트 컴포넌트 | 파일 |
|-------------|---------------|------|
| fields | (기존 필드 렌더링) | `DynamicItemForm/index.tsx` |
| bom | DynamicBOMSection | `DynamicItemForm/sections/DynamicBOMSection.tsx` |
| **table** | **DynamicTableSection** | `DynamicItemForm/sections/DynamicTableSection.tsx` |
