이거 # 백엔드-프론트엔드 협업 가이드 ## 메타데이터 기반 동적 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
case: assemblyType, material, color
rod: diameter, length, material
등 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 (
{/* ... 20개 필드 */}
); } if (formData.itemType === "PT" && formData.partType === "ASSEMBLY") { if (formData.category1 === "guide_rail") { return (
{/* ... 15개 필드 */}
); } 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) ↓ ┌─────────────────────────────────────────────────────────────┐ │ │ │ ├─ 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` | `` | `{required, min, max, pattern}` | | `number` | `` | `{required, min, max}` | | `select` | `` | `{required, minDate, maxDate}` | | `textarea` | `