562 lines
15 KiB
Markdown
562 lines
15 KiB
Markdown
|
|
# 컴포넌트 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<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로 업데이트:
|
||
|
|
|
||
|
|
```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 참조가 자동 업데이트됩니다
|
||
|
|
|
||
|
|
이 구조는 **빠른 기능 확장**에 적합하지만, **타입 안정성**과 **쿼리 성능** 측면에서 추가 개선이 필요합니다.
|