# 컴포넌트 JSON 관리 시스템 분석 보고서 ## 1. 개요 WACE 솔루션의 화면 컴포넌트는 **JSONB 형식**으로 데이터베이스에 저장되어 관리됩니다. 이 방식은 스키마 변경 없이 유연하게 컴포넌트 설정을 확장할 수 있는 장점이 있습니다. --- ## 2. 데이터베이스 구조 ### 2.1 핵심 테이블: `screen_layouts` ```sql CREATE TABLE screen_layouts ( layout_id SERIAL PRIMARY KEY, screen_id INTEGER REFERENCES screen_definitions(screen_id), component_type VARCHAR(50) NOT NULL, -- 'container', 'row', 'column', 'widget', 'component' component_id VARCHAR(100) UNIQUE NOT NULL, parent_id VARCHAR(100), -- 부모 컴포넌트 ID position_x INTEGER NOT NULL, -- X 좌표 (그리드) position_y INTEGER NOT NULL, -- Y 좌표 (그리드) width INTEGER NOT NULL, -- 너비 (그리드 컬럼 수: 1-12) height INTEGER NOT NULL, -- 높이 (픽셀) properties JSONB, -- ⭐ 컴포넌트별 속성 (핵심 JSON 필드) display_order INTEGER DEFAULT 0, layout_type VARCHAR(50), layout_config JSONB, zones_config JSONB, zone_id VARCHAR(100) ); ``` ### 2.2 화면 정의: `screen_definitions` ```sql CREATE TABLE screen_definitions ( screen_id SERIAL PRIMARY KEY, screen_name VARCHAR(100) NOT NULL, screen_code VARCHAR(50) UNIQUE NOT NULL, table_name VARCHAR(100) NOT NULL, company_code VARCHAR(50) NOT NULL, description TEXT, is_active CHAR(1) DEFAULT 'Y', data_source_type VARCHAR(20), -- 'database' | 'restapi' rest_api_endpoint VARCHAR(500), rest_api_json_path VARCHAR(100) ); ``` --- ## 3. JSON 구조 상세 분석 ### 3.1 `properties` 필드의 최상위 구조 ```typescript interface ComponentProperties { // 기본 식별 정보 id: string; type: "widget" | "container" | "row" | "column" | "component"; // 위치 및 크기 position: { x: number; y: number; z?: number }; size: { width: number; height: number }; parentId?: string; // 표시 정보 label?: string; title?: string; required?: boolean; readonly?: boolean; // 🆕 새 컴포넌트 시스템 componentType?: string; // 예: "v2-table-list", "v2-button-primary" componentConfig?: any; // 컴포넌트별 상세 설정 // 레거시 위젯 시스템 widgetType?: string; // 예: "text-input", "select-basic" webTypeConfig?: WebTypeConfig; // 테이블/컬럼 정보 tableName?: string; columnName?: string; // 스타일 style?: ComponentStyle; className?: string; // 반응형 설정 responsiveConfig?: ResponsiveComponentConfig; // 조건부 표시 conditional?: { enabled: boolean; field: string; operator: "=" | "!=" | ">" | "<" | "in" | "notIn"; value: unknown; action: "show" | "hide" | "enable" | "disable"; }; // 자동 입력 autoFill?: { enabled: boolean; sourceTable: string; filterColumn: string; userField: "companyCode" | "userId" | "deptCode"; displayColumn: string; }; } ``` ### 3.2 컴포넌트별 `componentConfig` 구조 #### 테이블 리스트 (`v2-table-list`) ```typescript { componentConfig: { tableName: "user_info", selectedTable: "user_info", displayMode: "table" | "card", columns: [ { columnName: "user_id", displayName: "사용자 ID", visible: true, sortable: true, searchable: true, width: 150, align: "left", format: "text", order: 0, editable: true, hidden: false, fixed: "left" | "right" | false, autoGeneration: { type: "uuid" | "numbering_rule", enabled: false, options: { numberingRuleId: "rule-123" } } } ], pagination: { enabled: true, pageSize: 20, showSizeSelector: true, pageSizeOptions: [10, 20, 50, 100] }, toolbar: { showEditMode: true, showExcel: true, showRefresh: true }, checkbox: { enabled: true, multiple: true, position: "left" }, filter: { enabled: true, filters: [] } } } ``` #### 버튼 (`v2-button-primary`) ```typescript { componentConfig: { action: { type: "save" | "delete" | "navigate" | "popup" | "excel" | "quickInsert", // 화면 이동용 targetScreenId?: number, targetScreenCode?: string, navigateUrl?: string, // 채번 규칙 연동 numberingRuleId?: string, excelNumberingRuleId?: string, // 엑셀 업로드 후 플로우 실행 excelAfterUploadFlows?: Array<{ flowId: number }>, // 데이터 전송 설정 dataTransfer?: { targetTable: string, columnMappings: [ { sourceColumn: string, targetColumn: string } ] } } } } ``` #### 분할 패널 레이아웃 (`v2-split-panel-layout`) ```typescript { componentConfig: { leftPanel: { tableName: "order_list", displayMode: "table" | "card", columns: [...], addConfig: { targetTable: "order_detail", columnMappings: [...] } }, rightPanel: { tableName: "order_detail", displayMode: "table", columns: [...] }, dataTransfer: { enabled: true, buttonConfig: { label: "선택 항목 추가", position: "center" } } } } ``` #### 플로우 위젯 (`flow-widget`) ```typescript { webTypeConfig: { dataflowConfig: { flowConfig: { flowId: 29 }, selectedDiagramId: 1, flowControls: [ { flowId: 30 }, { flowId: 31 } ] } } } ``` #### 탭 위젯 (`v2-tabs-widget`) ```typescript { componentConfig: { tabs: [ { id: "tab-1", label: "기본 정보", screenId: 45, order: 0, disabled: false }, { id: "tab-2", label: "상세 정보", screenId: 46, order: 1 } ], defaultTab: "tab-1", orientation: "horizontal", variant: "default" } } ``` ### 3.3 메타데이터 저장 (`_metadata` 타입) 화면 전체 설정은 `component_type = "_metadata"`인 별도 레코드로 저장: ```typescript { properties: { gridSettings: { columns: 12, gap: 16, padding: 16, snapToGrid: true, showGrid: true }, screenResolution: { width: 1920, height: 1080, name: "Full HD", category: "desktop" } } } ``` --- ## 4. 프론트엔드 레지스트리 구조 ### 4.1 디렉토리 구조 ``` frontend/lib/registry/ ├── init.ts # 레지스트리 초기화 ├── ComponentRegistry.ts # 컴포넌트 등록 시스템 ├── WebTypeRegistry.ts # 웹타입 레지스트리 └── components/ # 컴포넌트별 폴더 ├── v2-table-list/ │ ├── index.ts # 컴포넌트 등록 │ ├── types.ts # 타입 정의 │ ├── TableListComponent.tsx │ ├── TableListRenderer.tsx │ └── TableListConfigPanel.tsx ├── v2-button-primary/ ├── v2-split-panel-layout/ ├── text-input/ ├── select-basic/ └── ... (70+ 컴포넌트) ``` ### 4.2 컴포넌트 등록 패턴 ```typescript // frontend/lib/registry/components/v2-table-list/index.ts import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; ComponentRegistry.register({ id: "v2-table-list", name: "테이블 리스트", category: "display", component: TableListComponent, renderer: TableListRenderer, configPanel: TableListConfigPanel, defaultConfig: { tableName: "", columns: [], pagination: { enabled: true, pageSize: 20 } } }); ``` ### 4.3 현재 등록된 주요 컴포넌트 (70+ 개) | 카테고리 | 컴포넌트 | |---------|---------| | **입력** | text-input, number-input, date-input, select-basic, checkbox-basic, radio-basic, textarea-basic, slider-basic, toggle-switch | | **표시** | v2-table-list, v2-card-display, v2-text-display, image-display | | **레이아웃** | v2-split-panel-layout, v2-section-card, v2-section-paper, accordion-basic, conditional-container | | **버튼** | v2-button-primary, related-data-buttons | | **고급** | flow-widget, v2-tabs-widget, v2-pivot-grid, v2-category-manager, v2-aggregation-widget | | **파일** | file-upload | | **반복** | repeat-container, repeater-field-group, simple-repeater-table, modal-repeater-table | | **검색** | entity-search-input, autocomplete-search-input, table-search-widget | | **특수** | numbering-rule, mail-recipient-selector, rack-structure, map | --- ## 5. 백엔드 서비스 로직 ### 5.1 레이아웃 저장 (`saveLayout`) ```typescript // backend-node/src/services/screenManagementService.ts async saveLayout(screenId: number, layoutData: LayoutData, companyCode: string) { // 1. 기존 레이아웃 삭제 await query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]); // 2. 메타데이터 저장 if (layoutData.gridSettings || layoutData.screenResolution) { const metadata = { gridSettings: layoutData.gridSettings, screenResolution: layoutData.screenResolution }; await query(` INSERT INTO screen_layouts ( screen_id, component_type, component_id, properties, display_order ) VALUES ($1, '_metadata', $2, $3, -1) `, [screenId, `_metadata_${screenId}`, JSON.stringify(metadata)]); } // 3. 컴포넌트 저장 for (const component of layoutData.components) { const properties = { ...componentData, position: { x, y, z }, size: { width, height } }; await query(` INSERT INTO screen_layouts (...) VALUES (...) `, [screenId, componentType, componentId, ..., JSON.stringify(properties)]); } } ``` ### 5.2 레이아웃 조회 (`getLayout`) ```typescript async getLayout(screenId: number, companyCode: string): Promise { // 레이아웃 조회 const layouts = await query(` SELECT * FROM screen_layouts WHERE screen_id = $1 ORDER BY display_order ASC `, [screenId]); // 메타데이터와 컴포넌트 분리 const metadataLayout = layouts.find(l => l.component_type === "_metadata"); const componentLayouts = layouts.filter(l => l.component_type !== "_metadata"); // 컴포넌트 변환 (JSONB → TypeScript 객체) const components = componentLayouts.map(layout => { const properties = layout.properties as any; // ⭐ JSONB 자동 파싱 return { id: layout.component_id, type: layout.component_type, position: { x: layout.position_x, y: layout.position_y }, size: { width: layout.width, height: layout.height }, ...properties // 모든 properties 확장 }; }); return { components, gridSettings, screenResolution }; } ``` ### 5.3 ID 참조 업데이트 (화면 복사 시) 화면 복사 시 JSON 내부의 ID 참조를 새 ID로 업데이트: ```typescript // 채번 규칙 ID 업데이트 updateNumberingRuleIdsInProperties(properties, ruleIdMap) { // componentConfig.autoGeneration.options.numberingRuleId // componentConfig.action.numberingRuleId // componentConfig.action.excelNumberingRuleId } // 화면 ID 업데이트 updateTabScreenIdsInProperties(properties, screenIdMap) { // componentConfig.tabs[].screenId } // 플로우 ID 업데이트 updateFlowIdsInProperties(properties, flowIdMap) { // webTypeConfig.dataflowConfig.flowConfig.flowId // webTypeConfig.dataflowConfig.flowControls[].flowId } ``` --- ## 6. 장단점 분석 ### 6.1 장점 | 장점 | 설명 | |-----|-----| | **유연성** | 스키마 변경 없이 새 컴포넌트 설정 추가 가능 | | **확장성** | 새 컴포넌트 타입 추가 시 DB 마이그레이션 불필요 | | **버전 호환성** | 이전 버전 컴포넌트도 그대로 동작 | | **빠른 개발** | 프론트엔드에서 설정 구조 변경 후 바로 저장 가능 | | **복잡한 구조** | 중첩된 설정 (예: columns 배열) 저장 용이 | ### 6.2 단점 | 단점 | 설명 | |-----|-----| | **타입 안정성** | 런타임에만 타입 검증 가능 | | **쿼리 복잡도** | JSONB 내부 필드 검색/수정 어려움 | | **인덱싱 한계** | 전체 JSON 검색 시 성능 저하 | | **마이그레이션** | JSON 구조 변경 시 데이터 마이그레이션 필요 | | **디버깅** | JSON 구조 파악 어려움 | --- ## 7. 현재 구조의 특징 ### 7.1 레거시 + 신규 컴포넌트 공존 ```typescript // 레거시 방식 (widgetType + webTypeConfig) { type: "widget", widgetType: "text", webTypeConfig: { ... } } // 신규 방식 (componentType + componentConfig) { type: "component", componentType: "v2-table-list", componentConfig: { ... } } ``` ### 7.2 계층 구조 ``` screen_layouts ├── _metadata (격자 설정, 해상도) ├── container (최상위 컨테이너) │ ├── row (행) │ │ ├── column (열) │ │ │ └── widget/component (실제 컴포넌트) │ │ └── column │ └── row └── component (독립 컴포넌트) ``` ### 7.3 ID 참조 관계 ``` properties.componentConfig ├── action.targetScreenId → screen_definitions.screen_id ├── action.numberingRuleId → numbering_rule.rule_id ├── action.excelAfterUploadFlows[].flowId → flow_definitions.flow_id ├── tabs[].screenId → screen_definitions.screen_id └── webTypeConfig.dataflowConfig.flowConfig.flowId → flow_definitions.flow_id ``` --- ## 8. 개선 권장사항 ### 8.1 단기 개선 1. **타입 문서화**: 각 컴포넌트의 `componentConfig` 타입을 TypeScript 인터페이스로 명확히 정의 2. **검증 레이어**: 저장 전 JSON 스키마 검증 추가 3. **마이그레이션 도구**: JSON 구조 변경 시 자동 마이그레이션 스크립트 ### 8.2 장기 개선 1. **버전 관리**: `properties` 내에 `version` 필드 추가 2. **인덱스 최적화**: 자주 검색되는 JSONB 필드에 GIN 인덱스 추가 3. **로깅 강화**: 컴포넌트 설정 변경 이력 추적 --- ## 9. 결론 현재 시스템은 **JSONB를 활용한 유연한 컴포넌트 설정 관리** 방식을 채택하고 있습니다. - **70개 이상의 컴포넌트**가 등록되어 있으며 - **`screen_layouts.properties`** 필드에 모든 컴포넌트 설정이 저장됩니다 - 레거시(`widgetType`)와 신규(`componentType`) 컴포넌트가 공존하며 - 화면 복사 시 JSON 내부의 ID 참조가 자동 업데이트됩니다 이 구조는 **빠른 기능 확장**에 적합하지만, **타입 안정성**과 **쿼리 성능** 측면에서 추가 개선이 필요합니다.