804 lines
27 KiB
Markdown
804 lines
27 KiB
Markdown
|
|
이거 # 백엔드-프론트엔드 협업 가이드
|
|||
|
|
## 메타데이터 기반 동적 UI 구현 전략
|
|||
|
|
|
|||
|
|
**작성 일자:** 2025-11-12
|
|||
|
|
**목적:** 백엔드-프론트엔드 협업을 위한 DB 설계 및 API 구조 논의
|
|||
|
|
**대상:** 백엔드 개발자 + 프론트엔드 개발자
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 목차
|
|||
|
|
|
|||
|
|
1. [현재 상황 분석](#현재-상황-분석)
|
|||
|
|
2. [목표 아키텍처](#목표-아키텍처)
|
|||
|
|
3. [DB 스키마 설계](#db-스키마-설계)
|
|||
|
|
4. [API 설계](#api-설계)
|
|||
|
|
5. [백엔드-프론트 계약](#백엔드-프론트-계약)
|
|||
|
|
6. [논의 포인트](#논의-포인트)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 현재 상황 분석
|
|||
|
|
|
|||
|
|
### 문제 상황
|
|||
|
|
|
|||
|
|
**ItemManagement.tsx: 6,521줄 고정 화면**
|
|||
|
|
- 품목 유형별로 완전히 다른 화면 구성
|
|||
|
|
- 새 필드 추가 시 코드 수정 필요
|
|||
|
|
- 12개 부품 카테고리마다 다른 폼
|
|||
|
|
- 유지보수 불가능한 구조
|
|||
|
|
|
|||
|
|
### 품목 유형별 필드 구성
|
|||
|
|
|
|||
|
|
| 품목 유형 | 주요 필드 | 특이사항 |
|
|||
|
|
|-----------|-----------|----------|
|
|||
|
|
| **FG (제품)** | productName, itemName, specification, unit, salesPrice | BOM 구성 필요 |
|
|||
|
|
| **PT-ASSEMBLY (조립 부품)** | 12개 카테고리별 다름 | guide_rail: installationType, assemblyType, sideSpecWidth<br>case: assemblyType, material, color<br>rod: diameter, length, material<br>등 12가지 |
|
|||
|
|
| **PT-BENDING (절곡 부품)** | material, thickness, bendingDiagram, bendingDetails[] | 전개도 + 데이터 테이블 |
|
|||
|
|
| **PT-PURCHASED (구매 부품)** | category1, purchaseSource, specification, leadTime | 단순 구매 |
|
|||
|
|
| **RM (원자재)** | material, specification, unit, purchasePrice, supplier | 자재 관리 |
|
|||
|
|
| **SM (부자재)** | material, specification, unit, purchasePrice | 자재 관리 |
|
|||
|
|
| **CS (소모품)** | specification, unit, purchasePrice | 자재 관리 |
|
|||
|
|
|
|||
|
|
**핵심 문제:**
|
|||
|
|
- PT-ASSEMBLY 카테고리 12개 × 평균 5개 필드 = 60개 필드
|
|||
|
|
- 조건부 렌더링: itemType=PT → partType 표시 → partType=ASSEMBLY → category1 표시 → category1=guide_rail → installationType 표시
|
|||
|
|
- 모든 조건이 코드에 하드코딩됨
|
|||
|
|
|
|||
|
|
### 현재 코드 구조
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 6,521줄 중 일부 (의사 코드)
|
|||
|
|
if (formData.itemType === "FG") {
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<Input label="상품명" name="productName" required />
|
|||
|
|
<Input label="품목명" name="itemName" required />
|
|||
|
|
<Input label="규격" name="specification" required />
|
|||
|
|
{/* ... 20개 필드 */}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (formData.itemType === "PT" && formData.partType === "ASSEMBLY") {
|
|||
|
|
if (formData.category1 === "guide_rail") {
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<Select label="설치유형" name="installationType" options={["wall", "ceiling"]} required />
|
|||
|
|
<Select label="조립유형" name="assemblyType" options={["M", "T", "P", "B", "S"]} required />
|
|||
|
|
<Input label="사이드 폭" name="sideSpecWidth" type="number" required />
|
|||
|
|
{/* ... 15개 필드 */}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
if (formData.category1 === "case") {
|
|||
|
|
// 또 다른 15개 필드
|
|||
|
|
}
|
|||
|
|
// ... 10개 카테고리 더
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ... 총 6,521줄
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**문제:**
|
|||
|
|
- 새 카테고리 추가 → 코드 수정 (1일 작업)
|
|||
|
|
- 필드 하나 추가 → 10곳 수정 필요 (4시간 작업)
|
|||
|
|
- 검증 규칙 변경 → 코드 수정 + 배포
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 목표 아키텍처
|
|||
|
|
|
|||
|
|
### 메타데이터 기반 동적 UI
|
|||
|
|
|
|||
|
|
**컨셉:**
|
|||
|
|
- **DB에 화면 구성 정보 저장** (필드 정의, 섹션, 조건부 규칙)
|
|||
|
|
- **API로 메타데이터 전달** (GET /api/v1/items/metadata)
|
|||
|
|
- **프론트: 메타데이터 기반 동적 렌더링** (MetaFormBuilder)
|
|||
|
|
|
|||
|
|
**장점:**
|
|||
|
|
- ✅ 새 카테고리 추가: DB INSERT만 (1시간)
|
|||
|
|
- ✅ 필드 추가: DB INSERT만 (5분)
|
|||
|
|
- ✅ 검증 규칙 변경: DB UPDATE만 (5분)
|
|||
|
|
- ✅ 코드 변경 없음 → 배포 불필요
|
|||
|
|
- ✅ 관리자 화면에서 필드 관리 가능
|
|||
|
|
|
|||
|
|
### 아키텍처 다이어그램
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Database │
|
|||
|
|
├─────────────────────────────────────────────────────────────┤
|
|||
|
|
│ item_field_definitions (필드 메타데이터) │
|
|||
|
|
│ item_field_groups (섹션/그룹) │
|
|||
|
|
│ item_field_group_fields (필드-그룹 매핑) │
|
|||
|
|
│ item_render_rules (조건부 렌더링 규칙) │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
↓
|
|||
|
|
Backend API (Laravel)
|
|||
|
|
↓
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ GET /api/v1/items/metadata?itemType=FG │
|
|||
|
|
│ Response: { │
|
|||
|
|
│ fields: [...], // 필드 정의 배열 │
|
|||
|
|
│ groups: [...], // 섹션 배열 │
|
|||
|
|
│ rules: [...] // 조건부 규칙 배열 │
|
|||
|
|
│ } │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
↓
|
|||
|
|
Frontend (React)
|
|||
|
|
↓
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ <MetaFormBuilder metadata={metadata} /> │
|
|||
|
|
│ ├─ MetaFieldRenderer (필드 동적 렌더링) │
|
|||
|
|
│ ├─ MetaValidation (검증 동적 실행) │
|
|||
|
|
│ └─ MetaRuleEngine (조건부 표시 처리) │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### EAV 패턴 활용
|
|||
|
|
|
|||
|
|
**이미 게시판 시스템에서 사용 중:**
|
|||
|
|
- `board_settings` (게시판 설정 메타데이터)
|
|||
|
|
- `post_custom_field_values` (게시물 동적 필드 값)
|
|||
|
|
|
|||
|
|
**동일 패턴을 품목 관리에 적용:**
|
|||
|
|
- `item_field_definitions` (품목 필드 메타데이터)
|
|||
|
|
- `item_field_values` (품목 동적 필드 값) ← 필요 시
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## DB 스키마 설계
|
|||
|
|
|
|||
|
|
### 1. item_field_definitions (필드 메타데이터)
|
|||
|
|
|
|||
|
|
**목적:** 모든 필드의 정의를 저장
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE item_field_definitions (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
field_key VARCHAR(50) NOT NULL UNIQUE COMMENT '필드 키 (예: productName, installationType)',
|
|||
|
|
field_label VARCHAR(100) NOT NULL COMMENT '한글 레이블 (예: 상품명, 설치유형)',
|
|||
|
|
field_type VARCHAR(20) NOT NULL COMMENT 'text, select, number, date, textarea, checkbox, etc.',
|
|||
|
|
field_options JSON COMMENT 'select/radio의 옵션 배열 [{"value":"FG","label":"제품"}]',
|
|||
|
|
validation_rules JSON COMMENT '검증 규칙 {"required":true,"min":2,"max":100}',
|
|||
|
|
placeholder VARCHAR(200) COMMENT '입력 힌트',
|
|||
|
|
help_text VARCHAR(500) COMMENT '도움말 텍스트',
|
|||
|
|
display_order INT DEFAULT 0 COMMENT '표시 순서',
|
|||
|
|
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부',
|
|||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
INDEX idx_field_key (field_key),
|
|||
|
|
INDEX idx_is_active (is_active)
|
|||
|
|
) COMMENT='품목 필드 메타데이터';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**샘플 데이터:**
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO item_field_definitions (field_key, field_label, field_type, field_options, validation_rules, display_order) VALUES
|
|||
|
|
('itemType', '품목유형', 'select',
|
|||
|
|
'[{"value":"FG","label":"제품"},{"value":"PT","label":"부품"},{"value":"RM","label":"원자재"}]',
|
|||
|
|
'{"required":true}', 1),
|
|||
|
|
|
|||
|
|
('productName', '상품명', 'text', NULL,
|
|||
|
|
'{"required":true,"min":2,"max":100}', 2),
|
|||
|
|
|
|||
|
|
('installationType', '설치유형', 'select',
|
|||
|
|
'[{"value":"wall","label":"벽면형"},{"value":"ceiling","label":"천정형"}]',
|
|||
|
|
'{"required":true}', 10),
|
|||
|
|
|
|||
|
|
('sideSpecWidth', '사이드 폭', 'number', NULL,
|
|||
|
|
'{"required":true,"min":100,"max":5000}', 11),
|
|||
|
|
|
|||
|
|
('bendingDiagram', '절곡 전개도', 'file', NULL,
|
|||
|
|
'{"required":true,"accept":"image/*"}', 20);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. item_field_groups (섹션/그룹)
|
|||
|
|
|
|||
|
|
**목적:** 필드를 논리적 그룹으로 묶음
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE item_field_groups (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
group_key VARCHAR(50) NOT NULL UNIQUE COMMENT '그룹 키 (예: basic_info, spec_info)',
|
|||
|
|
group_label VARCHAR(100) NOT NULL COMMENT '한글 레이블 (예: 기본 정보, 규격 정보)',
|
|||
|
|
group_description VARCHAR(500) COMMENT '그룹 설명',
|
|||
|
|
display_order INT DEFAULT 0 COMMENT '표시 순서',
|
|||
|
|
is_collapsible BOOLEAN DEFAULT FALSE COMMENT '접기 가능 여부',
|
|||
|
|
is_collapsed_default BOOLEAN DEFAULT FALSE COMMENT '기본 접힘 상태',
|
|||
|
|
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부',
|
|||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
INDEX idx_group_key (group_key)
|
|||
|
|
) COMMENT='품목 필드 그룹 (섹션)';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**샘플 데이터:**
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO item_field_groups (group_key, group_label, group_description, display_order) VALUES
|
|||
|
|
('basic_info', '기본 정보', '품목의 기본 정보를 입력합니다', 1),
|
|||
|
|
('spec_info', '규격 정보', '품목의 규격 정보를 입력합니다', 2),
|
|||
|
|
('price_info', '가격 정보', '판매가 및 구매가 정보를 입력합니다', 3),
|
|||
|
|
('bom_info', 'BOM 정보', '구성 품목 정보를 입력합니다', 4),
|
|||
|
|
('file_info', '첨부파일', '관련 파일을 첨부합니다', 5);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. item_field_group_fields (필드-그룹 매핑)
|
|||
|
|
|
|||
|
|
**목적:** 어떤 필드가 어떤 그룹에 속하는지 정의
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE item_field_group_fields (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
field_definition_id BIGINT NOT NULL COMMENT '필드 ID',
|
|||
|
|
field_group_id BIGINT NOT NULL COMMENT '그룹 ID',
|
|||
|
|
display_order INT DEFAULT 0 COMMENT '그룹 내 표시 순서',
|
|||
|
|
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부',
|
|||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
FOREIGN KEY (field_definition_id) REFERENCES item_field_definitions(id) ON DELETE CASCADE,
|
|||
|
|
FOREIGN KEY (field_group_id) REFERENCES item_field_groups(id) ON DELETE CASCADE,
|
|||
|
|
INDEX idx_group (field_group_id),
|
|||
|
|
INDEX idx_field (field_definition_id)
|
|||
|
|
) COMMENT='필드-그룹 매핑';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**샘플 데이터:**
|
|||
|
|
```sql
|
|||
|
|
-- basic_info 그룹에 itemType, productName, itemName 할당
|
|||
|
|
INSERT INTO item_field_group_fields (field_definition_id, field_group_id, display_order) VALUES
|
|||
|
|
(1, 1, 1), -- itemType → basic_info
|
|||
|
|
(2, 1, 2), -- productName → basic_info
|
|||
|
|
(3, 1, 3); -- itemName → basic_info
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. item_render_rules (조건부 렌더링 규칙)
|
|||
|
|
|
|||
|
|
**목적:** 특정 필드 값에 따라 다른 필드 표시/숨김
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE item_render_rules (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
target_field_id BIGINT NOT NULL COMMENT '적용 대상 필드 ID',
|
|||
|
|
condition_field_key VARCHAR(50) NOT NULL COMMENT '조건 필드 키 (예: itemType)',
|
|||
|
|
condition_operator VARCHAR(20) NOT NULL COMMENT '연산자 (==, !=, in, not_in, >, <, >=, <=)',
|
|||
|
|
condition_value VARCHAR(500) NOT NULL COMMENT '조건 값 (JSON 배열 가능)',
|
|||
|
|
action VARCHAR(20) NOT NULL COMMENT '액션 (show, hide, required, optional)',
|
|||
|
|
display_order INT DEFAULT 0 COMMENT '규칙 우선순위',
|
|||
|
|
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 여부',
|
|||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
FOREIGN KEY (target_field_id) REFERENCES item_field_definitions(id) ON DELETE CASCADE,
|
|||
|
|
INDEX idx_target (target_field_id),
|
|||
|
|
INDEX idx_condition (condition_field_key)
|
|||
|
|
) COMMENT='조건부 렌더링 규칙';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**샘플 데이터:**
|
|||
|
|
```sql
|
|||
|
|
-- partType 필드는 itemType=PT일 때만 표시
|
|||
|
|
INSERT INTO item_render_rules (target_field_id, condition_field_key, condition_operator, condition_value, action) VALUES
|
|||
|
|
(4, 'itemType', '==', 'PT', 'show'),
|
|||
|
|
|
|||
|
|
-- installationType 필드는 partType=ASSEMBLY AND category1=guide_rail일 때만 표시
|
|||
|
|
-- (복합 조건은 JSON으로 표현 또는 여러 규칙 조합)
|
|||
|
|
(10, 'partType', '==', 'ASSEMBLY', 'show'),
|
|||
|
|
(10, 'category1', '==', 'guide_rail', 'show');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**복합 조건 처리 방법:**
|
|||
|
|
- 방법 1: 여러 규칙 생성 (AND 조건)
|
|||
|
|
- 방법 2: condition_value를 JSON으로 확장
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operator": "AND",
|
|||
|
|
"conditions": [
|
|||
|
|
{"field": "partType", "op": "==", "value": "ASSEMBLY"},
|
|||
|
|
{"field": "category1", "op": "==", "value": "guide_rail"}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## API 설계
|
|||
|
|
|
|||
|
|
### GET /api/v1/items/metadata
|
|||
|
|
|
|||
|
|
**목적:** 품목 유형별 화면 구성 정보 조회
|
|||
|
|
|
|||
|
|
**Request:**
|
|||
|
|
```
|
|||
|
|
GET /api/v1/items/metadata?itemType=PT&partType=ASSEMBLY&category1=guide_rail
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Query Parameters:**
|
|||
|
|
- `itemType` (optional): 품목 유형 (FG, PT, RM, SM, CS)
|
|||
|
|
- `partType` (optional): 부품 유형 (ASSEMBLY, BENDING, PURCHASED)
|
|||
|
|
- `category1` (optional): 카테고리 (guide_rail, case, etc.)
|
|||
|
|
|
|||
|
|
**Response:**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"success": true,
|
|||
|
|
"data": {
|
|||
|
|
"fields": [
|
|||
|
|
{
|
|||
|
|
"key": "itemType",
|
|||
|
|
"label": "품목유형",
|
|||
|
|
"type": "select",
|
|||
|
|
"required": true,
|
|||
|
|
"options": [
|
|||
|
|
{"value": "FG", "label": "제품"},
|
|||
|
|
{"value": "PT", "label": "부품"},
|
|||
|
|
{"value": "RM", "label": "원자재"},
|
|||
|
|
{"value": "SM", "label": "부자재"},
|
|||
|
|
{"value": "CS", "label": "소모품"}
|
|||
|
|
],
|
|||
|
|
"validation": {
|
|||
|
|
"required": true
|
|||
|
|
},
|
|||
|
|
"placeholder": "품목 유형을 선택하세요",
|
|||
|
|
"helpText": null,
|
|||
|
|
"displayOrder": 1
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"key": "productName",
|
|||
|
|
"label": "상품명",
|
|||
|
|
"type": "text",
|
|||
|
|
"required": true,
|
|||
|
|
"options": null,
|
|||
|
|
"validation": {
|
|||
|
|
"required": true,
|
|||
|
|
"min": 2,
|
|||
|
|
"max": 100
|
|||
|
|
},
|
|||
|
|
"placeholder": "상품명을 입력하세요",
|
|||
|
|
"helpText": "고객에게 표시되는 상품명입니다",
|
|||
|
|
"displayOrder": 2
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"key": "installationType",
|
|||
|
|
"label": "설치유형",
|
|||
|
|
"type": "select",
|
|||
|
|
"required": true,
|
|||
|
|
"options": [
|
|||
|
|
{"value": "wall", "label": "벽면형"},
|
|||
|
|
{"value": "ceiling", "label": "천정형"}
|
|||
|
|
],
|
|||
|
|
"validation": {
|
|||
|
|
"required": true
|
|||
|
|
},
|
|||
|
|
"displayOrder": 10
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"groups": [
|
|||
|
|
{
|
|||
|
|
"key": "basic_info",
|
|||
|
|
"label": "기본 정보",
|
|||
|
|
"description": "품목의 기본 정보를 입력합니다",
|
|||
|
|
"isCollapsible": false,
|
|||
|
|
"isCollapsedDefault": false,
|
|||
|
|
"fields": ["itemType", "productName", "itemName"],
|
|||
|
|
"displayOrder": 1
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"key": "spec_info",
|
|||
|
|
"label": "규격 정보",
|
|||
|
|
"description": "품목의 규격 정보를 입력합니다",
|
|||
|
|
"isCollapsible": true,
|
|||
|
|
"isCollapsedDefault": false,
|
|||
|
|
"fields": ["installationType", "assemblyType", "sideSpecWidth", "assemblyLength"],
|
|||
|
|
"displayOrder": 2
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"rules": [
|
|||
|
|
{
|
|||
|
|
"targetField": "partType",
|
|||
|
|
"condition": {
|
|||
|
|
"field": "itemType",
|
|||
|
|
"operator": "==",
|
|||
|
|
"value": "PT"
|
|||
|
|
},
|
|||
|
|
"action": "show"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"targetField": "installationType",
|
|||
|
|
"condition": {
|
|||
|
|
"operator": "AND",
|
|||
|
|
"conditions": [
|
|||
|
|
{"field": "partType", "operator": "==", "value": "ASSEMBLY"},
|
|||
|
|
{"field": "category1", "operator": "==", "value": "guide_rail"}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"action": "show"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"targetField": "bendingDiagram",
|
|||
|
|
"condition": {
|
|||
|
|
"field": "partType",
|
|||
|
|
"operator": "==",
|
|||
|
|
"value": "BENDING"
|
|||
|
|
},
|
|||
|
|
"action": "required"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### POST /api/v1/items (품목 생성)
|
|||
|
|
|
|||
|
|
**기존 API와 동일하지만 동적 필드 처리:**
|
|||
|
|
|
|||
|
|
**Request:**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"itemType": "PT",
|
|||
|
|
"partType": "ASSEMBLY",
|
|||
|
|
"category1": "guide_rail",
|
|||
|
|
"installationType": "wall",
|
|||
|
|
"assemblyType": "M",
|
|||
|
|
"sideSpecWidth": 3500,
|
|||
|
|
"assemblyLength": 5300,
|
|||
|
|
// ... 동적 필드들
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Backend 처리:**
|
|||
|
|
1. itemType, partType, category1에 따라 필요한 필드 조회 (metadata)
|
|||
|
|
2. 동적 필드 검증 (validation_rules 적용)
|
|||
|
|
3. products 또는 materials 테이블에 저장
|
|||
|
|
4. 동적 필드는 attributes JSON 컬럼에 저장 (기존 구조 활용)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 백엔드-프론트 계약
|
|||
|
|
|
|||
|
|
### 필드 타입 정의
|
|||
|
|
|
|||
|
|
| field_type | 프론트 렌더링 | 검증 예시 |
|
|||
|
|
|------------|--------------|-----------|
|
|||
|
|
| `text` | `<Input type="text" />` | `{required, min, max, pattern}` |
|
|||
|
|
| `number` | `<Input type="number" />` | `{required, min, max}` |
|
|||
|
|
| `select` | `<Select options={...} />` | `{required, in:["FG","PT"]}` |
|
|||
|
|
| `radio` | `<RadioGroup options={...} />` | `{required}` |
|
|||
|
|
| `checkbox` | `<Checkbox />` | `{required}` |
|
|||
|
|
| `date` | `<Input type="date" />` | `{required, minDate, maxDate}` |
|
|||
|
|
| `textarea` | `<Textarea />` | `{required, min, max}` |
|
|||
|
|
| `file` | `<FileUpload accept={...} />` | `{required, accept, maxSize}` |
|
|||
|
|
| `custom` | 커스텀 컴포넌트 | 커스텀 검증 |
|
|||
|
|
|
|||
|
|
### 조건부 렌더링 액션
|
|||
|
|
|
|||
|
|
| action | 의미 | 프론트 처리 |
|
|||
|
|
|--------|------|-------------|
|
|||
|
|
| `show` | 필드 표시 | display: block |
|
|||
|
|
| `hide` | 필드 숨김 | display: none |
|
|||
|
|
| `required` | 필수 입력으로 변경 | validation.required = true |
|
|||
|
|
| `optional` | 선택 입력으로 변경 | validation.required = false |
|
|||
|
|
| `readonly` | 읽기 전용 | disabled = true |
|
|||
|
|
| `editable` | 편집 가능 | disabled = false |
|
|||
|
|
|
|||
|
|
### 검증 규칙 정의
|
|||
|
|
|
|||
|
|
**validation_rules JSON 구조:**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"required": true,
|
|||
|
|
"min": 2,
|
|||
|
|
"max": 100,
|
|||
|
|
"pattern": "^[a-zA-Z0-9-]+$",
|
|||
|
|
"minDate": "2020-01-01",
|
|||
|
|
"maxDate": "2030-12-31",
|
|||
|
|
"in": ["FG", "PT", "RM"],
|
|||
|
|
"notIn": ["deprecated_value"],
|
|||
|
|
"accept": "image/*",
|
|||
|
|
"maxSize": 5242880
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**프론트 검증 처리:**
|
|||
|
|
```typescript
|
|||
|
|
// 의사 코드
|
|||
|
|
const validateField = (field, value, validationRules) => {
|
|||
|
|
if (validationRules.required && !value) {
|
|||
|
|
return `${field.label}을(를) 입력하세요`;
|
|||
|
|
}
|
|||
|
|
if (validationRules.min && value.length < validationRules.min) {
|
|||
|
|
return `최소 ${validationRules.min}자 이상 입력하세요`;
|
|||
|
|
}
|
|||
|
|
// ... 나머지 검증
|
|||
|
|
return null; // 검증 통과
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**백엔드 검증 처리:**
|
|||
|
|
```php
|
|||
|
|
// 의사 코드
|
|||
|
|
$rules = [];
|
|||
|
|
foreach ($fields as $field) {
|
|||
|
|
$validation = $field->validation_rules;
|
|||
|
|
if ($validation['required']) {
|
|||
|
|
$rules[$field->field_key][] = 'required';
|
|||
|
|
}
|
|||
|
|
if (isset($validation['min'])) {
|
|||
|
|
$rules[$field->field_key][] = "min:{$validation['min']}";
|
|||
|
|
}
|
|||
|
|
// ... 나머지 검증
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$request->validate($rules);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 조건부 렌더링 처리
|
|||
|
|
|
|||
|
|
**프론트 Rule Engine (의사 코드):**
|
|||
|
|
```typescript
|
|||
|
|
const evaluateCondition = (condition, formData) => {
|
|||
|
|
if (condition.operator === 'AND') {
|
|||
|
|
return condition.conditions.every(c => evaluateCondition(c, formData));
|
|||
|
|
}
|
|||
|
|
if (condition.operator === 'OR') {
|
|||
|
|
return condition.conditions.some(c => evaluateCondition(c, formData));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const fieldValue = formData[condition.field];
|
|||
|
|
switch (condition.operator) {
|
|||
|
|
case '==':
|
|||
|
|
return fieldValue === condition.value;
|
|||
|
|
case '!=':
|
|||
|
|
return fieldValue !== condition.value;
|
|||
|
|
case 'in':
|
|||
|
|
return condition.value.includes(fieldValue);
|
|||
|
|
// ... 나머지 연산자
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const isFieldVisible = (field, rules, formData) => {
|
|||
|
|
const applicableRules = rules.filter(r => r.targetField === field.key);
|
|||
|
|
return applicableRules.every(rule => {
|
|||
|
|
if (rule.action === 'show') {
|
|||
|
|
return evaluateCondition(rule.condition, formData);
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 논의 포인트
|
|||
|
|
|
|||
|
|
### 1. 유연성 범위
|
|||
|
|
|
|||
|
|
**Q1: 어디까지 메타데이터로 관리할까?**
|
|||
|
|
|
|||
|
|
| 항목 | 메타데이터 관리 | 하드코딩 |
|
|||
|
|
|------|----------------|----------|
|
|||
|
|
| 필드 정의 | ✅ 권장 | |
|
|||
|
|
| 섹션/그룹 | ✅ 권장 | |
|
|||
|
|
| 조건부 렌더링 | ✅ 권장 | |
|
|||
|
|
| 검증 규칙 | ✅ 권장 | |
|
|||
|
|
| **품목 코드 생성 규칙** | ❓ 논의 필요 | ✅ 복잡한 비즈니스 로직 |
|
|||
|
|
| **BOM 계산 로직** | | ✅ 복잡한 비즈니스 로직 |
|
|||
|
|
| **가격 계산 로직** | | ✅ 복잡한 비즈니스 로직 |
|
|||
|
|
|
|||
|
|
**제안:**
|
|||
|
|
- 단순 필드 정의, 검증, 조건부 표시 → 메타데이터
|
|||
|
|
- 복잡한 비즈니스 로직 (코드 생성, 계산) → 하드코딩 or 별도 룰 엔진
|
|||
|
|
|
|||
|
|
**Q2: 모든 품목 유형을 메타데이터로?**
|
|||
|
|
- Option A: FG, PT, RM, SM, CS 모두 메타데이터
|
|||
|
|
- Option B: 자주 변경되는 PT만 메타데이터, 나머지는 하드코딩
|
|||
|
|
- **권장: Option A** (일관성 + 확장성)
|
|||
|
|
|
|||
|
|
### 2. 검증 위치
|
|||
|
|
|
|||
|
|
**Q3: 검증을 어디서 할까?**
|
|||
|
|
|
|||
|
|
| 위치 | 장점 | 단점 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| **프론트 Only** | 빠른 피드백 | 보안 취약 (우회 가능) |
|
|||
|
|
| **백엔드 Only** | 보안 강력 | UX 나쁨 (서버 왕복 필요) |
|
|||
|
|
| **프론트 + 백엔드 (권장)** | UX + 보안 | 검증 로직 중복 |
|
|||
|
|
|
|||
|
|
**제안:**
|
|||
|
|
- 프론트: metadata의 validation_rules 사용 (UX)
|
|||
|
|
- 백엔드: 동일한 validation_rules 적용 (보안)
|
|||
|
|
- **규칙 한 곳(DB)에서 관리 → 양쪽 동일 적용**
|
|||
|
|
|
|||
|
|
### 3. 품목 코드 생성 규칙
|
|||
|
|
|
|||
|
|
**Q4: 품목 코드 생성을 어떻게?**
|
|||
|
|
|
|||
|
|
**현재:** 177줄 generateItemCode() 함수 (12개 품목 타입별 로직)
|
|||
|
|
|
|||
|
|
**Option A: DB 룰 테이블**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE item_code_generation_rules (
|
|||
|
|
id BIGINT PRIMARY KEY,
|
|||
|
|
item_type VARCHAR(10),
|
|||
|
|
part_type VARCHAR(20),
|
|||
|
|
category VARCHAR(50),
|
|||
|
|
code_pattern VARCHAR(200) COMMENT '예: {typeCode}{kindCode}{sizeCode}',
|
|||
|
|
code_logic JSON COMMENT '생성 로직 (함수명 또는 수식)',
|
|||
|
|
display_order INT
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
- 장점: 유연함, 관리자 화면에서 수정 가능
|
|||
|
|
- 단점: 복잡한 로직 표현 어려움, 보안 위험 (eval 사용 시)
|
|||
|
|
|
|||
|
|
**Option B: 백엔드 하드코딩 (현재 방식)**
|
|||
|
|
```php
|
|||
|
|
class ItemCodeGenerator {
|
|||
|
|
public function generate($itemType, $partType, $category, $data) {
|
|||
|
|
// 12개 분기 로직
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- 장점: 복잡한 로직 표현 가능, 안전함
|
|||
|
|
- 단점: 코드 수정 필요, 배포 필요
|
|||
|
|
|
|||
|
|
**Option C: Hybrid (권장)**
|
|||
|
|
- 단순 패턴 (FG, RM, SM, CS): DB 룰 테이블
|
|||
|
|
- 복잡한 패턴 (PT 12개 카테고리): 백엔드 하드코딩
|
|||
|
|
- DB에 `code_generator_class` 필드 → 동적 호출
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO item_code_generation_rules VALUES
|
|||
|
|
('FG', NULL, NULL, '{itemName}-{specification}', NULL, 1),
|
|||
|
|
('PT', 'ASSEMBLY', 'guide_rail', NULL, 'GuideRailCodeGenerator::class', 2);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 복합 조건 처리
|
|||
|
|
|
|||
|
|
**Q5: 복합 조건 (AND, OR, NOT)을 어떻게?**
|
|||
|
|
|
|||
|
|
**현재 DB 스키마:** 단일 조건만 표현
|
|||
|
|
|
|||
|
|
**Option A: 여러 규칙 생성 (암시적 AND)**
|
|||
|
|
```sql
|
|||
|
|
-- installationType은 partType=ASSEMBLY AND category1=guide_rail일 때만
|
|||
|
|
INSERT INTO item_render_rules VALUES
|
|||
|
|
(10, 'partType', '==', 'ASSEMBLY', 'show', 1),
|
|||
|
|
(10, 'category1', '==', 'guide_rail', 'show', 2);
|
|||
|
|
```
|
|||
|
|
- 프론트: 같은 targetField의 모든 규칙 AND 처리
|
|||
|
|
|
|||
|
|
**Option B: JSON 조건 (명시적 AND/OR)**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"operator": "AND",
|
|||
|
|
"conditions": [
|
|||
|
|
{"field": "partType", "op": "==", "value": "ASSEMBLY"},
|
|||
|
|
{"field": "category1", "op": "==", "value": "guide_rail"}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**권장: Option B** (명확함, 유연성)
|
|||
|
|
|
|||
|
|
### 5. 데이터 저장 방식
|
|||
|
|
|
|||
|
|
**Q6: 동적 필드 값을 어떻게 저장?**
|
|||
|
|
|
|||
|
|
**현재 구조:**
|
|||
|
|
- `products` 테이블: 고정 컬럼 (id, itemCode, itemName, etc.)
|
|||
|
|
- `products.attributes` JSON: 동적 필드
|
|||
|
|
|
|||
|
|
**Option A: 기존 구조 유지**
|
|||
|
|
```json
|
|||
|
|
// products.attributes
|
|||
|
|
{
|
|||
|
|
"installationType": "wall",
|
|||
|
|
"assemblyType": "M",
|
|||
|
|
"sideSpecWidth": 3500
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- 장점: 기존 코드 호환
|
|||
|
|
- 단점: JSON 쿼리 성능 (WHERE 조건 사용 어려움)
|
|||
|
|
|
|||
|
|
**Option B: EAV 테이블 (완전 동적)**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE item_field_values (
|
|||
|
|
id BIGINT PRIMARY KEY,
|
|||
|
|
item_id BIGINT,
|
|||
|
|
field_definition_id BIGINT,
|
|||
|
|
field_value TEXT,
|
|||
|
|
FOREIGN KEY (item_id) REFERENCES products(id),
|
|||
|
|
FOREIGN KEY (field_definition_id) REFERENCES item_field_definitions(id)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
- 장점: 완전 유연, 필드별 쿼리 가능
|
|||
|
|
- 단점: 조인 많음, 성능 이슈 가능
|
|||
|
|
|
|||
|
|
**권장: Option A (Hybrid EAV)**
|
|||
|
|
- 자주 검색하는 필드: 고정 컬럼 (itemCode, itemType, partType)
|
|||
|
|
- 나머지 동적 필드: JSON (attributes)
|
|||
|
|
- 필요 시 JSON 인덱스 활용 (MySQL 5.7+)
|
|||
|
|
|
|||
|
|
### 6. 성능 최적화
|
|||
|
|
|
|||
|
|
**Q7: 메타데이터 캐싱?**
|
|||
|
|
|
|||
|
|
**문제:**
|
|||
|
|
- 매번 metadata API 호출 → DB 쿼리 4개 (fields, groups, mapping, rules)
|
|||
|
|
|
|||
|
|
**해결:**
|
|||
|
|
- **Backend 캐싱:** Redis or File Cache (TTL: 1시간)
|
|||
|
|
```php
|
|||
|
|
$cacheKey = "item_metadata_{$itemType}_{$partType}_{$category}";
|
|||
|
|
$metadata = Cache::remember($cacheKey, 3600, function() {
|
|||
|
|
return $this->buildMetadata();
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
- **Frontend 캐싱:** LocalStorage or SessionStorage
|
|||
|
|
```typescript
|
|||
|
|
const metadata = localStorage.getItem('metadata_PT_ASSEMBLY_guide_rail');
|
|||
|
|
if (metadata && !isExpired(metadata)) {
|
|||
|
|
return JSON.parse(metadata);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7. 버전 관리
|
|||
|
|
|
|||
|
|
**Q8: 메타데이터 변경 이력 관리?**
|
|||
|
|
|
|||
|
|
**문제:**
|
|||
|
|
- 필드 정의 변경 시 기존 데이터 호환성?
|
|||
|
|
|
|||
|
|
**해결:**
|
|||
|
|
- `item_field_definitions.version` 컬럼 추가
|
|||
|
|
- 변경 시 새 버전 생성 (Copy-on-Write)
|
|||
|
|
- 기존 데이터는 이전 버전 참조
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 다음 단계
|
|||
|
|
|
|||
|
|
### 1단계: 프로토타입 개발 (2-3주)
|
|||
|
|
|
|||
|
|
**Backend:**
|
|||
|
|
- [ ] DB 스키마 생성 (4개 테이블)
|
|||
|
|
- [ ] 샘플 데이터 INSERT (FG, PT-guide_rail)
|
|||
|
|
- [ ] metadata API 구현
|
|||
|
|
- [ ] 캐싱 적용
|
|||
|
|
|
|||
|
|
**Frontend:**
|
|||
|
|
- [ ] MetaFormBuilder 컴포넌트 개발
|
|||
|
|
- [ ] MetaFieldRenderer 컴포넌트 개발
|
|||
|
|
- [ ] Rule Engine 구현
|
|||
|
|
- [ ] 1개 품목 유형 테스트 (FG or PT-guide_rail)
|
|||
|
|
|
|||
|
|
### 2단계: 전체 적용 (4-6주)
|
|||
|
|
|
|||
|
|
- [ ] 모든 품목 유형 메타데이터 작성
|
|||
|
|
- [ ] 기존 ItemManagement.tsx 제거
|
|||
|
|
- [ ] 관리자 화면 개발 (필드 관리 UI)
|
|||
|
|
- [ ] 테스트 및 검증
|
|||
|
|
|
|||
|
|
### 3단계: 최적화 및 확장 (2-3주)
|
|||
|
|
|
|||
|
|
- [ ] 성능 테스트 및 최적화
|
|||
|
|
- [ ] 품목 코드 생성 규칙 통합
|
|||
|
|
- [ ] BOM 편집기 메타데이터 적용
|
|||
|
|
- [ ] 문서화
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 참고 문서
|
|||
|
|
|
|||
|
|
1. **METADATA_DRIVEN_UI_DETAILED_ANALYSIS.md** - 상세 버전 (현재 분석 + DB 스키마 상세 + 구현 예시)
|
|||
|
|
2. **PHASE_0_FINAL_REPORT.md** - Phase 0 종합 보고서
|
|||
|
|
3. **react_code_analysis_summary.md** - React 코드 완전 분석
|
|||
|
|
4. **api_gap_validation_report.md** - Gap 검증 보고서
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**작성자:** 백엔드 개발자
|
|||
|
|
**검토 요청:** 프론트엔드 개발자
|
|||
|
|
**논의 일정:** TBD
|
|||
|
|
**업데이트:** 2025-11-12
|