- 화면 복제 기능을 개선하여 DB 구조 개편 후의 효율적인 화면 관리를 지원합니다. - 그룹 복제 시 버튼의 `targetScreenId`가 새 화면으로 매핑되지 않는 버그를 수정하였습니다. - 관련된 서비스 및 쿼리에서 `table_type_columns`를 사용하여 라벨 정보를 조회하도록 변경하였습니다. - 여러 컨트롤러 및 서비스에서 `column_labels` 대신 `table_type_columns`를 참조하도록 업데이트하였습니다.
15 KiB
15 KiB
컴포넌트 JSON 관리 시스템 분석 보고서
1. 개요
WACE 솔루션의 화면 컴포넌트는 JSONB 형식으로 데이터베이스에 저장되어 관리됩니다. 이 방식은 스키마 변경 없이 유연하게 컴포넌트 설정을 확장할 수 있는 장점이 있습니다.
2. 데이터베이스 구조
2.1 핵심 테이블: screen_layouts
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
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 필드의 최상위 구조
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)
{
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)
{
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)
{
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)
{
webTypeConfig: {
dataflowConfig: {
flowConfig: {
flowId: 29
},
selectedDiagramId: 1,
flowControls: [
{ flowId: 30 },
{ flowId: 31 }
]
}
}
}
탭 위젯 (v2-tabs-widget)
{
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"인 별도 레코드로 저장:
{
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 컴포넌트 등록 패턴
// 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)
// 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)
async getLayout(screenId: number, companyCode: string): Promise<LayoutData | null> {
// 레이아웃 조회
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로 업데이트:
// 채번 규칙 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 레거시 + 신규 컴포넌트 공존
// 레거시 방식 (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 단기 개선
- 타입 문서화: 각 컴포넌트의
componentConfig타입을 TypeScript 인터페이스로 명확히 정의 - 검증 레이어: 저장 전 JSON 스키마 검증 추가
- 마이그레이션 도구: JSON 구조 변경 시 자동 마이그레이션 스크립트
8.2 장기 개선
- 버전 관리:
properties내에version필드 추가 - 인덱스 최적화: 자주 검색되는 JSONB 필드에 GIN 인덱스 추가
- 로깅 강화: 컴포넌트 설정 변경 이력 추적
9. 결론
현재 시스템은 JSONB를 활용한 유연한 컴포넌트 설정 관리 방식을 채택하고 있습니다.
- 70개 이상의 컴포넌트가 등록되어 있으며
screen_layouts.properties필드에 모든 컴포넌트 설정이 저장됩니다- 레거시(
widgetType)와 신규(componentType) 컴포넌트가 공존하며 - 화면 복사 시 JSON 내부의 ID 참조가 자동 업데이트됩니다
이 구조는 빠른 기능 확장에 적합하지만, 타입 안정성과 쿼리 성능 측면에서 추가 개선이 필요합니다.