# 컴포넌트 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 참조가 자동 업데이트됩니다

이 구조는 **빠른 기능 확장**에 적합하지만, **타입 안정성**과 **쿼리 성능** 측면에서 추가 개선이 필요합니다.
