# WACE 화면 구현 실행 가이드 (챗봇/AI 에이전트 전용)

> **최종 업데이트**: 2026-03-16
> **용도**: 사용자가 "수주관리 화면 만들어줘"라고 요청하면, 이 문서를 참조하여 SQL을 직접 생성하고 화면을 구현하는 AI 챗봇용 실행 가이드
> **핵심**: 이 문서의 SQL 템플릿을 따라 INSERT하면 화면이 자동으로 생성된다

---

## 0. 절대 규칙

1. 사용자 업무 화면(수주, 생산, 품질 등)은 **React 코드(.tsx) 작성 금지** → DB INSERT로만 구현
2. 모든 DB 컬럼은 **VARCHAR(500)** (날짜 컬럼만 TIMESTAMP)
3. 모든 테이블에 **기본 5개 컬럼** 필수: id, created_date, updated_date, writer, company_code
4. 모든 INSERT에 **ON CONFLICT** 절 필수 (중복 방지)
5. 컴포넌트는 반드시 **v2-** 접두사 사용
6. **[최우선] 비즈니스 테이블 CREATE TABLE 시 NOT NULL / UNIQUE 제약조건 절대 금지!**

> **왜 DB 레벨 제약조건을 걸면 안 되는가?**
>
> 이 시스템은 **멀티테넌시(Multi-Tenancy)** 환경이다.
> 각 회사(tenant)마다 같은 테이블을 공유하되, **필수값/유니크 규칙이 회사별로 다를 수 있다.**
>
> 따라서 제약조건은 DB에 직접 거는 것이 아니라, **관리자 메뉴에서 회사별 메타데이터**로 논리적으로 제어한다:
> - **필수값**: `table_type_columns.is_nullable = 'N'` → 애플리케이션 레벨에서 검증
> - **유니크**: `table_type_columns.is_unique = 'Y'` → 애플리케이션 레벨에서 검증
>
> DB 레벨에서 NOT NULL이나 UNIQUE를 걸면, **특정 회사에만 적용해야 할 규칙이 모든 회사에 강제되어** 멀티테넌시가 깨진다.
>
> **허용**: 기본 5개 컬럼의 `id` PRIMARY KEY, `DEFAULT` 값만 DB 레벨에서 설정
> **금지**: 비즈니스 컬럼에 `NOT NULL`, `UNIQUE`, `CHECK`, `FOREIGN KEY` 등 DB 제약조건 직접 적용

---

## 1. 화면 생성 전체 파이프라인

사용자가 화면을 요청하면 아래 7단계를 순서대로 실행한다.

```
Step 1: 비즈니스 테이블 CREATE TABLE
Step 2: table_labels INSERT (테이블 라벨)
Step 3: table_type_columns INSERT (컬럼 타입 정의, company_code='*')
Step 4: column_labels INSERT (컬럼 한글 라벨)
Step 5: screen_definitions INSERT → screen_id 획득
Step 6: screen_layouts_v2 INSERT (레이아웃 JSON)
Step 7: menu_info INSERT (메뉴 등록)
```

**선택적 추가 단계**:
- 채번 규칙이 필요하면: numbering_rules + numbering_rule_parts INSERT
- 카테고리가 필요하면: table_column_category_values INSERT
- 비즈니스 로직(버튼 액션)이 필요하면: dataflow_diagrams INSERT

---

## 2. Step 1: 비즈니스 테이블 생성 (CREATE TABLE)

### 템플릿

> **[최우선] 비즈니스 컬럼에 NOT NULL / UNIQUE / CHECK / FOREIGN KEY 제약조건 절대 금지!**
> 멀티테넌시 환경에서 회사별로 규칙이 다르므로, `table_type_columns`의 `is_nullable`, `is_unique` 메타데이터로 논리적 제어한다.

```sql
CREATE TABLE "{테이블명}" (
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),

  "{비즈니스_컬럼1}" varchar(500),
  "{비즈니스_컬럼2}" varchar(500),
  "{비즈니스_컬럼3}" varchar(500)
  -- NOT NULL, UNIQUE, CHECK, FOREIGN KEY 금지!
);
```

### 마스터-디테일인 경우 (2개 테이블)

```sql
-- 마스터 테이블
CREATE TABLE "{마스터_테이블}" (
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),
  "{컬럼1}" varchar(500),
  "{컬럼2}" varchar(500)
  -- NOT NULL, UNIQUE, FOREIGN KEY 금지!
);

-- 디테일 테이블
CREATE TABLE "{디테일_테이블}" (
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),
  "{마스터_FK}" varchar(500),  -- 마스터 테이블 id 참조 (FOREIGN KEY 제약조건은 걸지 않는다!)
  "{컬럼1}" varchar(500),
  "{컬럼2}" varchar(500)
  -- NOT NULL, UNIQUE, FOREIGN KEY 금지!
);
```

**금지 사항**:
- INTEGER, NUMERIC, BOOLEAN, TEXT, DATE 등 DB 타입 직접 사용 금지. 반드시 VARCHAR(500).
- 비즈니스 컬럼에 NOT NULL, UNIQUE, CHECK, FOREIGN KEY 등 DB 레벨 제약조건 금지.

---

## 3. Step 2: table_labels INSERT

```sql
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ('{테이블명}', '{한글_라벨}', '{설명}', now(), now())
ON CONFLICT (table_name)
DO UPDATE SET table_label = EXCLUDED.table_label, description = EXCLUDED.description, updated_date = now();
```

**예시**:
```sql
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ('order_master', '수주 마스터', '수주 헤더 정보 관리', now(), now())
ON CONFLICT (table_name)
DO UPDATE SET table_label = EXCLUDED.table_label, description = EXCLUDED.description, updated_date = now();

INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
VALUES ('order_detail', '수주 상세', '수주 품목별 상세 정보', now(), now())
ON CONFLICT (table_name)
DO UPDATE SET table_label = EXCLUDED.table_label, description = EXCLUDED.description, updated_date = now();
```

---

## 4. Step 3: table_type_columns INSERT

> `company_code = '*'` 로 등록한다 (전체 공통 설정).

### 기본 5개 컬럼 (모든 테이블 공통)

```sql
INSERT INTO table_type_columns (
  table_name, column_name, company_code, input_type, detail_settings,
  is_nullable, is_unique, display_order, column_label, description, is_visible,
  created_date, updated_date
)
VALUES
  ('{테이블명}', 'id',           '*', 'text', '{}', 'N', 'Y', -5, 'ID',     '기본키 (자동생성)', false, now(), now()),
  ('{테이블명}', 'created_date', '*', 'date', '{}', 'Y', 'N', -4, '생성일시', '레코드 생성일시',   false, now(), now()),
  ('{테이블명}', 'updated_date', '*', 'date', '{}', 'Y', 'N', -3, '수정일시', '레코드 수정일시',   false, now(), now()),
  ('{테이블명}', 'writer',       '*', 'text', '{}', 'Y', 'N', -2, '작성자',   '레코드 작성자',     false, now(), now()),
  ('{테이블명}', 'company_code', '*', 'text', '{}', 'Y', 'N', -1, '회사코드', '회사 구분 코드',    false, now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings,
  is_nullable = EXCLUDED.is_nullable, is_unique = EXCLUDED.is_unique,
  display_order = EXCLUDED.display_order, column_label = EXCLUDED.column_label,
  description = EXCLUDED.description, is_visible = EXCLUDED.is_visible, updated_date = now();
```

### 비즈니스 컬럼 (display_order 0부터)

```sql
INSERT INTO table_type_columns (
  table_name, column_name, company_code, input_type, detail_settings,
  is_nullable, is_unique, display_order, column_label, description, is_visible,
  created_date, updated_date
)
VALUES
  ('{테이블명}', '{컬럼명}', '*', '{input_type}', '{detail_settings_json}', 'Y', 'N', {순서}, '{한글라벨}', '{설명}', true, now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings,
  is_nullable = EXCLUDED.is_nullable, is_unique = EXCLUDED.is_unique,
  display_order = EXCLUDED.display_order, column_label = EXCLUDED.column_label,
  description = EXCLUDED.description, is_visible = EXCLUDED.is_visible, updated_date = now();
```

### input_type 선택 기준

| 데이터 성격 | input_type | detail_settings 예시 |
|------------|-----------|---------------------|
| 일반 텍스트 | `text` | `'{}'` |
| 숫자 (수량, 금액) | `number` | `'{}'` |
| 날짜 | `date` | `'{}'` |
| 여러 줄 텍스트 (비고) | `textarea` | `'{}'` |
| 공통코드 선택 (상태 등) | `code` | `'{"codeCategory":"STATUS_CODE"}'` |
| 다른 테이블 참조 (거래처 등) | `entity` | `'{"referenceTable":"customer_info","referenceColumn":"id","displayColumn":"customer_name"}'` |
| 정적 옵션 선택 | `select` | `'{"options":[{"label":"옵션1","value":"v1"},{"label":"옵션2","value":"v2"}]}'` |
| 체크박스 | `checkbox` | `'{}'` |
| 라디오 | `radio` | `'{}'` |
| 카테고리 | `category` | `'{"categoryRef":"CAT_ID"}'` |
| 자동 채번 | `numbering` | `'{"numberingRuleId":"rule_id"}'` |

---

## 5. Step 4: column_labels INSERT

> 레거시 호환용이지만 **필수 등록**이다. table_type_columns와 동일한 값을 넣되, `column_label`(한글명)을 추가.
>
> **주의**: `column_labels` 테이블의 UNIQUE 제약조건은 `(table_name, column_name, company_code)` 3개 컬럼이다. 반드시 `company_code`를 포함해야 한다.

```sql
-- 기본 5개 컬럼
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, company_code, created_date, updated_date)
VALUES
  ('{테이블명}', 'id', 'ID', 'text', '{}', '기본키 (자동생성)', -5, true, '*', now(), now()),
  ('{테이블명}', 'created_date', '생성일시', 'date', '{}', '레코드 생성일시', -4, true, '*', now(), now()),
  ('{테이블명}', 'updated_date', '수정일시', 'date', '{}', '레코드 수정일시', -3, true, '*', now(), now()),
  ('{테이블명}', 'writer', '작성자', 'text', '{}', '레코드 작성자', -2, true, '*', now(), now()),
  ('{테이블명}', 'company_code', '회사코드', 'text', '{}', '회사 구분 코드', -1, true, '*', now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type,
  detail_settings = EXCLUDED.detail_settings, description = EXCLUDED.description,
  display_order = EXCLUDED.display_order, is_visible = EXCLUDED.is_visible, updated_date = now();

-- 비즈니스 컬럼
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, company_code, created_date, updated_date)
VALUES
  ('{테이블명}', '{컬럼명}', '{한글라벨}', '{input_type}', '{detail_settings}', '{설명}', {순서}, true, '*', now(), now())
ON CONFLICT (table_name, column_name, company_code)
DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type,
  detail_settings = EXCLUDED.detail_settings, description = EXCLUDED.description,
  display_order = EXCLUDED.display_order, is_visible = EXCLUDED.is_visible, updated_date = now();
```

---

## 6. Step 5: screen_definitions INSERT

```sql
INSERT INTO screen_definitions (
  screen_name, screen_code, table_name, company_code, description, is_active,
  db_source_type, data_source_type, created_date
)
VALUES (
  '{화면명}',           -- 예: '수주관리'
  '{screen_code}',     -- 예: 'COMPANY_A_ORDER_MNG' (회사코드_식별자)
  '{메인_테이블명}',    -- 예: 'order_master'
  '{company_code}',    -- 예: 'COMPANY_A'
  '{설명}',
  'Y',
  'internal',
  'database',
  now()
)
RETURNING screen_id;
```

**screen_code 규칙**: `{company_code}_{영문식별자}` (예: `ILSHIN_ORDER_MNG`, `COMPANY_19_ITEM_INFO`)

**중요**: Step 6, 7에서 `screen_id`가 필요하다. 서브쿼리로 참조하면 하드코딩 실수를 방지할 수 있다:
```sql
(SELECT screen_id FROM screen_definitions WHERE screen_code = '{screen_code}')
```

> **screen_code 조건부 UNIQUE 규칙**:
> `screen_code`는 단순 UNIQUE가 아니라 **`WHERE is_active <> 'D'`** 조건부 UNIQUE이다.
> - 삭제된 화면(`is_active = 'D'`)과 동일한 코드로 새 화면을 만들 수 있다.
> - 활성 상태(`'Y'` 또는 `'N'`)에서는 같은 `screen_code`가 중복되면 에러가 발생한다.
> - 화면 삭제 시 `DELETE`가 아닌 `UPDATE SET is_active = 'D'`로 소프트 삭제하므로, 이전 코드의 재사용이 가능하다.

---

## 7. Step 6: screen_layouts_v2 INSERT (핵심)

### 기본 구조

```sql
INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, created_at, updated_at)
VALUES (
  (SELECT screen_id FROM screen_definitions WHERE screen_code = '{screen_code}'),
  '{company_code}',
  1,                     -- 기본 레이어
  '기본 레이어',
  '{layout_data_json}'::jsonb,
  now(),
  now()
)
ON CONFLICT (screen_id, company_code, layer_id)
DO UPDATE SET layout_data = EXCLUDED.layout_data, updated_at = now();
```

### layout_data JSON 뼈대

```json
{
  "version": "2.0",
  "components": [
    {
      "id": "{고유ID}",
      "url": "@/lib/registry/components/{컴포넌트타입}",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 1920, "height": 800 },
      "displayOrder": 0,
      "overrides": { /* 컴포넌트별 설정 */ }
    }
  ],
  "gridSettings": { "columns": 12, "gap": 16, "padding": 16 },
  "screenResolution": { "width": 1920, "height": 1080 }
}
```

### 컴포넌트 url 매핑표

| 컴포넌트 | url 값 |
|----------|--------|
| v2-table-list | `@/lib/registry/components/v2-table-list` |
| v2-table-search-widget | `@/lib/registry/components/v2-table-search-widget` |
| v2-split-panel-layout | `@/lib/registry/components/v2-split-panel-layout` |
| v2-table-grouped | `@/lib/registry/components/v2-table-grouped` |
| v2-tabs-widget | `@/lib/registry/components/v2-tabs-widget` |
| v2-button-primary | `@/lib/registry/components/v2-button-primary` |
| v2-input | `@/lib/registry/components/v2-input` |
| v2-select | `@/lib/registry/components/v2-select` |
| v2-date | `@/lib/registry/components/v2-date` |
| v2-card-display | `@/lib/registry/components/v2-card-display` |
| v2-pivot-grid | `@/lib/registry/components/v2-pivot-grid` |
| v2-timeline-scheduler | `@/lib/registry/components/v2-timeline-scheduler` |
| v2-text-display | `@/lib/registry/components/v2-text-display` |
| v2-aggregation-widget | `@/lib/registry/components/v2-aggregation-widget` |
| v2-numbering-rule | `@/lib/registry/components/v2-numbering-rule` |
| v2-file-upload | `@/lib/registry/components/v2-file-upload` |
| v2-section-card | `@/lib/registry/components/v2-section-card` |
| v2-divider-line | `@/lib/registry/components/v2-divider-line` |
| v2-bom-tree | `@/lib/registry/components/v2-bom-tree` |
| v2-approval-step | `@/lib/registry/components/v2-approval-step` |
| v2-status-count | `@/lib/registry/components/v2-status-count` |
| v2-section-paper | `@/lib/registry/components/v2-section-paper` |
| v2-split-line | `@/lib/registry/components/v2-split-line` |
| v2-repeat-container | `@/lib/registry/components/v2-repeat-container` |
| v2-repeater | `@/lib/registry/components/v2-repeater` |
| v2-category-manager | `@/lib/registry/components/v2-category-manager` |
| v2-media | `@/lib/registry/components/v2-media` |
| v2-location-swap-selector | `@/lib/registry/components/v2-location-swap-selector` |
| v2-rack-structure | `@/lib/registry/components/v2-rack-structure` |
| v2-process-work-standard | `@/lib/registry/components/v2-process-work-standard` |
| v2-item-routing | `@/lib/registry/components/v2-item-routing` |
| v2-bom-item-editor | `@/lib/registry/components/v2-bom-item-editor` |

---

## 8. 패턴별 layout_data 완전 예시

### 8.1 패턴 A: 기본 마스터 (검색 + 테이블)

**사용 조건**: 단일 테이블 CRUD, 마스터-디테일 관계 없음

```json
{
  "version": "2.0",
  "components": [
    {
      "id": "search_1",
      "url": "@/lib/registry/components/v2-table-search-widget",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 1920, "height": 100 },
      "displayOrder": 0,
      "overrides": {
        "label": "검색",
        "autoSelectFirstTable": true,
        "showTableSelector": false
      }
    },
    {
      "id": "table_1",
      "url": "@/lib/registry/components/v2-table-list",
      "position": { "x": 0, "y": 120 },
      "size": { "width": 1920, "height": 700 },
      "displayOrder": 1,
      "overrides": {
        "label": "{화면제목}",
        "tableName": "{테이블명}",
        "autoLoad": true,
        "displayMode": "table",
        "checkbox": { "enabled": true, "multiple": true, "position": "left", "selectAll": true },
        "pagination": { "enabled": true, "pageSize": 20, "showSizeSelector": true, "showPageInfo": true },
        "horizontalScroll": { "enabled": true, "maxVisibleColumns": 8 },
        "toolbar": { "showEditMode": true, "showExcel": true, "showRefresh": true }
      }
    }
  ],
  "gridSettings": { "columns": 12, "gap": 16, "padding": 16 },
  "screenResolution": { "width": 1920, "height": 1080 }
}
```

### 8.2 패턴 B: 마스터-디테일 (좌우 분할)

**사용 조건**: 좌측 마스터 테이블 선택 → 우측 디테일 테이블 연동. 두 테이블 간 FK 관계.

```json
{
  "version": "2.0",
  "components": [
    {
      "id": "split_1",
      "url": "@/lib/registry/components/v2-split-panel-layout",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 1920, "height": 850 },
      "displayOrder": 0,
      "overrides": {
        "label": "{화면제목}",
        "splitRatio": 35,
        "resizable": true,
        "autoLoad": true,
        "syncSelection": true,
        "leftPanel": {
          "title": "{마스터_제목}",
          "displayMode": "table",
          "tableName": "{마스터_테이블명}",
          "showSearch": true,
          "showAdd": true,
          "showEdit": false,
          "showDelete": true,
          "columns": [
            { "name": "{컬럼1}", "label": "{라벨1}", "width": 120, "sortable": true },
            { "name": "{컬럼2}", "label": "{라벨2}", "width": 150 },
            { "name": "{컬럼3}", "label": "{라벨3}", "width": 100 }
          ],
          "addButton": { "enabled": true, "mode": "auto" },
          "deleteButton": { "enabled": true, "confirmMessage": "선택한 항목을 삭제하시겠습니까?" }
        },
        "rightPanel": {
          "title": "{디테일_제목}",
          "displayMode": "table",
          "tableName": "{디테일_테이블명}",
          "relation": {
            "type": "detail",
            "leftColumn": "id",
            "rightColumn": "{마스터FK_컬럼}",
            "foreignKey": "{마스터FK_컬럼}"
          },
          "columns": [
            { "name": "{컬럼1}", "label": "{라벨1}", "width": 120 },
            { "name": "{컬럼2}", "label": "{라벨2}", "width": 150 },
            { "name": "{컬럼3}", "label": "{라벨3}", "width": 100, "editable": true }
          ],
          "addButton": { "enabled": true, "mode": "auto" },
          "editButton": { "enabled": true, "mode": "auto" },
          "deleteButton": { "enabled": true, "confirmMessage": "삭제하시겠습니까?" }
        }
      }
    }
  ],
  "gridSettings": { "columns": 12, "gap": 16, "padding": 16 },
  "screenResolution": { "width": 1920, "height": 1080 }
}
```

### 8.3 패턴 C: 마스터-디테일 + 탭

**사용 조건**: 패턴 B에서 우측에 여러 종류의 상세를 탭으로 구분

패턴 B의 rightPanel에 **additionalTabs** 추가:

```json
{
  "rightPanel": {
    "title": "{디테일_제목}",
    "displayMode": "table",
    "tableName": "{기본탭_테이블}",
    "relation": {
      "type": "detail",
      "leftColumn": "id",
      "rightColumn": "{FK_컬럼}",
      "foreignKey": "{FK_컬럼}"
    },
    "additionalTabs": [
      {
        "tabId": "tab_basic",
        "label": "기본정보",
        "tableName": "{기본정보_테이블}",
        "displayMode": "table",
        "relation": { "type": "detail", "leftColumn": "id", "rightColumn": "{FK}", "foreignKey": "{FK}" },
        "columns": [ /* 컬럼 배열 */ ],
        "addButton": { "enabled": true },
        "deleteButton": { "enabled": true }
      },
      {
        "tabId": "tab_history",
        "label": "이력",
        "tableName": "{이력_테이블}",
        "displayMode": "table",
        "relation": { "type": "detail", "leftColumn": "id", "rightColumn": "{FK}", "foreignKey": "{FK}" },
        "columns": [ /* 컬럼 배열 */ ]
      },
      {
        "tabId": "tab_files",
        "label": "첨부파일",
        "tableName": "{파일_테이블}",
        "displayMode": "table",
        "relation": { "type": "detail", "leftColumn": "id", "rightColumn": "{FK}", "foreignKey": "{FK}" },
        "columns": [ /* 컬럼 배열 */ ]
      }
    ]
  }
}
```

### 8.4 패턴 D: 그룹화 테이블

```json
{
  "version": "2.0",
  "components": [
    {
      "id": "grouped_1",
      "url": "@/lib/registry/components/v2-table-grouped",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 1920, "height": 800 },
      "displayOrder": 0,
      "overrides": {
        "label": "{화면제목}",
        "selectedTable": "{테이블명}",
        "groupConfig": {
          "groupByColumn": "{그룹기준_컬럼}",
          "groupLabelFormat": "{value}",
          "defaultExpanded": true,
          "sortDirection": "asc",
          "summary": { "showCount": true, "sumColumns": ["{합계컬럼1}", "{합계컬럼2}"] }
        },
        "columns": [
          { "columnName": "{컬럼1}", "displayName": "{라벨1}", "visible": true, "width": 120 },
          { "columnName": "{컬럼2}", "displayName": "{라벨2}", "visible": true, "width": 150 }
        ],
        "showCheckbox": true,
        "showExpandAllButton": true
      }
    }
  ],
  "gridSettings": { "columns": 12, "gap": 16, "padding": 16 },
  "screenResolution": { "width": 1920, "height": 1080 }
}
```

### 8.5 패턴 E: 타임라인/간트차트 (리소스 기반)

**사용 조건**: 설비/작업자 등 리소스 기준으로 스케줄을 시간축에 표시

```json
{
  "version": "2.0",
  "components": [
    {
      "id": "timeline_1",
      "url": "@/lib/registry/components/v2-timeline-scheduler",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 1920, "height": 800 },
      "displayOrder": 0,
      "overrides": {
        "label": "{화면제목}",
        "selectedTable": "{스케줄_테이블}",
        "resourceTable": "{리소스_테이블}",
        "fieldMapping": {
          "id": "id",
          "resourceId": "{리소스FK_컬럼}",
          "title": "{제목_컬럼}",
          "startDate": "{시작일_컬럼}",
          "endDate": "{종료일_컬럼}",
          "status": "{상태_컬럼}",
          "progress": "{진행률_컬럼}"
        },
        "resourceFieldMapping": {
          "id": "id",
          "name": "{리소스명_컬럼}",
          "group": "{그룹_컬럼}"
        },
        "defaultZoomLevel": "day",
        "editable": true,
        "allowDrag": true,
        "allowResize": true
      }
    }
  ],
  "gridSettings": { "columns": 12, "gap": 16, "padding": 16 },
  "screenResolution": { "width": 1920, "height": 1080 }
}
```

### 8.6 패턴 E-2: 타임라인 (품목 그룹 뷰 + 연결 필터)

**사용 조건**: 좌측 테이블에서 선택한 품목 기반으로 타임라인을 필터링 표시. 품목별 카드 그룹 뷰.

> 리소스(설비) 기반이 아닌, **품목(item_code)별로 카드 그룹** 형태로 스케줄을 표시한다.
> 좌측 테이블에서 행을 선택하면 `linkedFilter`로 해당 품목의 스케줄만 필터링.
> `staticFilters`로 완제품/반제품 등 데이터 유형을 고정 필터링.

```json
{
  "id": "timeline_finished",
  "url": "@/lib/registry/components/v2-timeline-scheduler",
  "position": { "x": 0, "y": 0 },
  "size": { "width": 1920, "height": 800 },
  "displayOrder": 0,
  "overrides": {
    "label": "완제품 생산계획",
    "selectedTable": "{스케줄_테이블}",
    "viewMode": "itemGrouped",
    "fieldMapping": {
      "id": "id",
      "resourceId": "item_code",
      "title": "item_name",
      "startDate": "start_date",
      "endDate": "end_date",
      "status": "status"
    },
    "defaultZoomLevel": "day",
    "staticFilters": {
      "product_type": "완제품"
    },
    "linkedFilter": {
      "sourceField": "part_code",
      "targetField": "item_code",
      "sourceTableName": "{좌측_테이블명}",
      "emptyMessage": "좌측 목록에서 품목을 선택하세요",
      "showEmptyWhenNoSelection": true
    }
  }
}
```

**핵심 설정 설명**:

| 설정 | 용도 |
|------|------|
| `viewMode: "itemGrouped"` | 리소스 행이 아닌, 품목별 카드 그룹으로 표시 |
| `staticFilters` | DB 조회 시 항상 적용 (서버측 WHERE 조건) |
| `linkedFilter` | 다른 컴포넌트 선택 이벤트로 클라이언트 측 필터링 |
| `linkedFilter.sourceField` | 소스 테이블에서 가져올 값의 컬럼명 |
| `linkedFilter.targetField` | 타임라인 데이터에서 매칭할 컬럼명 |

> **주의**: `linkedFilter`와 `staticFilters`의 차이
> - `staticFilters`: DB SELECT 쿼리의 WHERE 절에 포함 → 서버에서 필터링
> - `linkedFilter`: 전체 데이터를 불러온 후, 선택 이벤트에 따라 클라이언트에서 필터링

### 8.7 패턴 F: 복합 화면 (좌측 테이블 + 우측 탭 내 타임라인)

**사용 조건**: 생산계획처럼 좌측 마스터 테이블 + 우측에 탭으로 여러 타임라인/테이블을 표시하는 복합 화면.
`v2-split-panel-layout`의 `rightPanel.displayMode: "custom"` + `v2-tabs-widget` + `v2-timeline-scheduler` 조합.

**구조 개요**:

```
┌──────────────────────────────────────────────────┐
│ v2-split-panel-layout                            │
│ ┌──────────┬─────────────────────────────────┐   │
│ │ leftPanel │  rightPanel (displayMode:custom)│   │
│ │           │  ┌─────────────────────────────┐│   │
│ │ v2-table- │  │ v2-tabs-widget              ││   │
│ │ grouped   │  │ ┌───────┬───────┬─────────┐ ││   │
│ │ (수주목록) │  │ │완제품  │반제품  │기타 탭  │ ││   │
│ │           │  │ └───────┴───────┴─────────┘ ││   │
│ │           │  │ ┌─────────────────────────┐ ││   │
│ │           │  │ │ v2-timeline-scheduler   │ ││   │
│ │           │  │ │ (품목별 그룹 뷰)         │ ││   │
│ │           │  │ └─────────────────────────┘ ││   │
│ │           │  └─────────────────────────────┘│   │
│ └──────────┴─────────────────────────────────┘   │
└──────────────────────────────────────────────────┘
```

**실제 layout_data 예시** (생산계획 화면 참고):

```json
{
  "version": "2.0",
  "components": [
    {
      "id": "split_pp",
      "url": "@/lib/registry/components/v2-split-panel-layout",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 1920, "height": 850 },
      "displayOrder": 0,
      "overrides": {
        "label": "생산계획",
        "splitRatio": 25,
        "resizable": true,
        "autoLoad": true,
        "syncSelection": true,
        "leftPanel": {
          "title": "수주 목록",
          "displayMode": "custom",
          "components": [
            {
              "id": "grouped_orders",
              "componentType": "v2-table-grouped",
              "label": "수주별 품목",
              "position": { "x": 0, "y": 0 },
              "size": { "width": 600, "height": 800 },
              "componentConfig": {
                "selectedTable": "sales_order_mng",
                "groupConfig": {
                  "groupByColumn": "order_number",
                  "groupLabelFormat": "{value}",
                  "defaultExpanded": true,
                  "summary": { "showCount": true }
                },
                "columns": [
                  { "columnName": "part_code", "displayName": "품번", "visible": true, "width": 100 },
                  { "columnName": "part_name", "displayName": "품명", "visible": true, "width": 120 },
                  { "columnName": "order_qty", "displayName": "수량", "visible": true, "width": 60 },
                  { "columnName": "delivery_date", "displayName": "납기일", "visible": true, "width": 90 }
                ],
                "showCheckbox": true,
                "checkboxMode": "multi"
              }
            }
          ]
        },
        "rightPanel": {
          "title": "생산 계획",
          "displayMode": "custom",
          "components": [
            {
              "id": "tabs_pp",
              "componentType": "v2-tabs-widget",
              "label": "생산계획 탭",
              "position": { "x": 0, "y": 0 },
              "size": { "width": 1400, "height": 800 },
              "componentConfig": {
                "tabs": [
                  {
                    "id": "tab_finished",
                    "label": "완제품",
                    "order": 1,
                    "components": [
                      {
                        "id": "timeline_finished",
                        "componentType": "v2-timeline-scheduler",
                        "label": "완제품 타임라인",
                        "position": { "x": 0, "y": 0 },
                        "size": { "width": 1380, "height": 750 },
                        "componentConfig": {
                          "selectedTable": "production_plan_mng",
                          "viewMode": "itemGrouped",
                          "fieldMapping": {
                            "id": "id",
                            "resourceId": "item_code",
                            "title": "item_name",
                            "startDate": "start_date",
                            "endDate": "end_date",
                            "status": "status"
                          },
                          "defaultZoomLevel": "day",
                          "staticFilters": {
                            "product_type": "완제품"
                          },
                          "linkedFilter": {
                            "sourceField": "part_code",
                            "targetField": "item_code",
                            "sourceTableName": "sales_order_mng",
                            "emptyMessage": "좌측 수주 목록에서 품목을 선택하세요",
                            "showEmptyWhenNoSelection": true
                          }
                        }
                      }
                    ]
                  },
                  {
                    "id": "tab_semi",
                    "label": "반제품",
                    "order": 2,
                    "components": [
                      {
                        "id": "timeline_semi",
                        "componentType": "v2-timeline-scheduler",
                        "label": "반제품 타임라인",
                        "position": { "x": 0, "y": 0 },
                        "size": { "width": 1380, "height": 750 },
                        "componentConfig": {
                          "selectedTable": "production_plan_mng",
                          "viewMode": "itemGrouped",
                          "fieldMapping": {
                            "id": "id",
                            "resourceId": "item_code",
                            "title": "item_name",
                            "startDate": "start_date",
                            "endDate": "end_date",
                            "status": "status"
                          },
                          "defaultZoomLevel": "day",
                          "staticFilters": {
                            "product_type": "반제품"
                          }
                        }
                      }
                    ]
                  }
                ],
                "defaultTab": "tab_finished"
              }
            }
          ]
        }
      }
    }
  ],
  "gridSettings": { "columns": 12, "gap": 16, "padding": 16 },
  "screenResolution": { "width": 1920, "height": 1080 }
}
```

**패턴 F 핵심 포인트**:

| 포인트 | 설명 |
|--------|------|
| `leftPanel.displayMode: "custom"` | 좌측에 v2-table-grouped 등 자유 배치 |
| `rightPanel.displayMode: "custom"` | 우측에 v2-tabs-widget 등 자유 배치 |
| `componentConfig` | custom 내부 컴포넌트는 overrides 대신 componentConfig 사용 |
| `componentType` | custom 내부에서는 url 대신 componentType 사용 |
| 완제품 탭에만 `linkedFilter` | 좌측 테이블과 연동 필터링 |
| 반제품 탭에는 `linkedFilter` 없음 | 반제품 item_code가 수주 품목과 다르므로 전체 표시 |
| 자동 스케줄 생성 버튼 | `staticFilters.product_type === "완제품"` 일 때 자동 표시 |

> **displayMode: "custom" 내부 컴포넌트 규칙**:
> - `url` 대신 `componentType` 사용 (예: `"v2-timeline-scheduler"`, `"v2-table-grouped"`)
> - `overrides` 대신 `componentConfig` 사용
> - `position`, `size`는 동일하게 사용

---

## 9. Step 7: menu_info INSERT

```sql
INSERT INTO menu_info (
  objid, menu_type, parent_obj_id,
  menu_name_kor, menu_name_eng, seq,
  menu_url, menu_desc, writer, regdate, status,
  company_code, screen_code
)
VALUES (
  {고유_objid},
  0,
  {부모_메뉴_objid},
  '{메뉴명_한글}',
  '{메뉴명_영문}',
  {정렬순서},
  '/screen/{screen_code}',
  '{메뉴_설명}',
  'admin',
  now(),
  'active',
  '{company_code}',
  '{screen_code}'
);
```

- `objid`: BIGINT 고유값. `extract(epoch from now())::bigint * 1000` 으로 생성
- `menu_type`: `0` = 말단 메뉴(화면), `1` = 폴더
- `parent_obj_id`: 상위 폴더 메뉴의 objid

**objid 생성 규칙 및 주의사항**:

기본 생성: `extract(epoch from now())::bigint * 1000`

> **여러 메뉴를 한 트랜잭션에서 동시에 INSERT할 때 PK 중복 위험!**
> `now()`는 같은 트랜잭션 안에서 동일한 값을 반환하므로, 복수 INSERT 시 objid가 충돌한다.
> 반드시 순서값을 더해서 고유성을 보장할 것:
>
> ```sql
> -- 폴더 메뉴
> extract(epoch from now())::bigint * 1000 + 1
> -- 화면 메뉴 1
> extract(epoch from now())::bigint * 1000 + 2
> -- 화면 메뉴 2
> extract(epoch from now())::bigint * 1000 + 3
> ```

---

## 10. 선택적 단계: 채번 규칙 설정

자동으로 코드/번호를 생성해야 하는 컬럼이 있을 때 사용.

### numbering_rules INSERT

```sql
INSERT INTO numbering_rules (
  rule_id, rule_name, description, separator, reset_period,
  current_sequence, table_name, column_name, company_code,
  created_at, updated_at, created_by
)
VALUES (
  '{rule_id}',           -- 예: 'ORDER_NO_RULE'
  '{규칙명}',            -- 예: '수주번호 채번'
  '{설명}',
  '-',                   -- 구분자
  'year',                -- 'none', 'year', 'month', 'day'
  1,                     -- 시작 순번
  '{테이블명}',          -- 예: 'order_master'
  '{컬럼명}',            -- 예: 'order_no'
  '{company_code}',
  now(), now(), 'admin'
);
```

### numbering_rule_parts INSERT (채번 구성 파트)

```sql
-- 파트 1: 접두사
INSERT INTO numbering_rule_parts (rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at)
VALUES ('{rule_id}', 1, 'prefix', 'auto', '{"prefix": "SO", "separatorAfter": "-"}'::jsonb, '{}'::jsonb, '{company_code}', now());

-- 파트 2: 날짜
INSERT INTO numbering_rule_parts (rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at)
VALUES ('{rule_id}', 2, 'date', 'auto', '{"format": "YYYYMM", "separatorAfter": "-"}'::jsonb, '{}'::jsonb, '{company_code}', now());

-- 파트 3: 순번
INSERT INTO numbering_rule_parts (rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at)
VALUES ('{rule_id}', 3, 'sequence', 'auto', '{"digits": 4, "startFrom": 1}'::jsonb, '{}'::jsonb, '{company_code}', now());
```

**결과**: `SO-202603-0001`, `SO-202603-0002`, ...

---

## 11. 선택적 단계: 카테고리 값 설정

상태, 유형 등을 카테고리로 관리할 때 사용.

### table_column_category_values INSERT

```sql
INSERT INTO table_column_category_values (
  table_name, column_name, value_code, value_label, value_order,
  parent_value_id, depth, description, color, company_code, created_by
)
VALUES
  ('{테이블명}', '{컬럼명}', 'ACTIVE', '활성', 1, NULL, 1, '활성 상태', '#22c55e', '{company_code}', 'admin'),
  ('{테이블명}', '{컬럼명}', 'INACTIVE', '비활성', 2, NULL, 1, '비활성 상태', '#ef4444', '{company_code}', 'admin'),
  ('{테이블명}', '{컬럼명}', 'PENDING', '대기', 3, NULL, 1, '승인 대기', '#f59e0b', '{company_code}', 'admin');
```

---

## 12. 패턴 판단 의사결정 트리

사용자가 화면을 요청하면 이 트리로 패턴을 결정한다.

```
Q1. 좌측 마스터 + 우측에 탭으로 타임라인/테이블 등 복합 구성이 필요한가?
├─ YES → 패턴 F (복합 화면) → v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler
└─ NO ↓

Q2. 시간축 기반 일정/간트차트가 필요한가?
├─ YES → Q2-1. 품목별 카드 그룹 뷰인가?
│        ├─ YES → 패턴 E-2 (품목 그룹 타임라인) → v2-timeline-scheduler(viewMode:itemGrouped)
│        └─ NO  → 패턴 E (리소스 기반 타임라인) → v2-timeline-scheduler
└─ NO ↓

Q3. 다차원 집계/피벗 분석이 필요한가?
├─ YES → 피벗 → v2-pivot-grid
└─ NO ↓

Q4. 데이터를 그룹별로 접기/펼치기가 필요한가?
├─ YES → 패턴 D (그룹화) → v2-table-grouped
└─ NO ↓

Q5. 이미지+정보를 카드 형태로 표시하는가?
├─ YES → 카드뷰 → v2-card-display
└─ NO ↓

Q6. 마스터 테이블 선택 시 연관 디테일이 필요한가?
├─ YES → Q6-1. 디테일에 여러 탭이 필요한가?
│        ├─ YES → 패턴 C (마스터-디테일+탭) → v2-split-panel-layout + additionalTabs
│        └─ NO  → 패턴 B (마스터-디테일) → v2-split-panel-layout
└─ NO → 패턴 A (기본 마스터) → v2-table-search-widget + v2-table-list
```

### 패턴 선택 빠른 참조

| 패턴 | 대표 화면 | 핵심 컴포넌트 |
|------|----------|-------------|
| A | 거래처관리, 코드관리 | v2-table-search-widget + v2-table-list |
| B | 수주관리, 발주관리 | v2-split-panel-layout |
| C | 수주관리(멀티탭) | v2-split-panel-layout + additionalTabs |
| D | 재고현황, 그룹별조회 | v2-table-grouped |
| E | 설비 작업일정 | v2-timeline-scheduler (리소스 기반) |
| E-2 | 단독 품목별 타임라인 | v2-timeline-scheduler (viewMode: itemGrouped) |
| F | 생산계획, 작업지시 | v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler |

---

## 13. 화면 간 연결 관계 정의

### 13.1 마스터-디테일 관계 (v2-split-panel-layout)

좌측 마스터 테이블의 행을 선택하면, 우측 디테일 테이블이 해당 FK로 필터링된다.

**relation 설정**:

> **JSON 안에 주석(`//`, `/* */`) 절대 금지!** PostgreSQL `::jsonb` 캐스팅 시 파싱 에러 발생. 설명은 반드시 JSON 바깥에 작성한다.

- `type`: `"detail"` (FK 관계)
- `leftColumn`: 마스터 테이블의 PK 컬럼 (보통 `"id"`)
- `rightColumn`: 디테일 테이블의 FK 컬럼
- `foreignKey`: `rightColumn`과 동일한 값

```json
{
  "relation": {
    "type": "detail",
    "leftColumn": "id",
    "rightColumn": "master_id",
    "foreignKey": "master_id"
  }
}
```

**복합 키인 경우**:

```json
{
  "relation": {
    "type": "detail",
    "keys": [
      { "leftColumn": "order_no", "rightColumn": "order_no" },
      { "leftColumn": "company_code", "rightColumn": "company_code" }
    ]
  }
}
```

### 13.2 엔티티 조인 (테이블 참조 표시)

디테일 테이블의 FK 컬럼에 다른 테이블의 이름을 표시하고 싶을 때.

**table_type_columns에서 설정**:

```sql
INSERT INTO table_type_columns (table_name, column_name, company_code, input_type, detail_settings, ...)
VALUES ('order_detail', 'item_id', '*', 'entity',
  '{"referenceTable":"item_info","referenceColumn":"id","displayColumn":"item_name"}', ...);
```

**v2-table-list columns에서 설정**:

```json
{
  "columns": [
    {
      "name": "item_id",
      "label": "품목",
      "isEntityJoin": true,
      "joinInfo": {
        "sourceTable": "order_detail",
        "sourceColumn": "item_id",
        "referenceTable": "item_info",
        "joinAlias": "item_name"
      }
    }
  ]
}
```

### 13.3 모달 화면 연결

추가/편집 버튼 클릭 시 별도 모달 화면을 띄우는 경우.

1. **모달용 screen_definitions INSERT** (별도 화면 생성)
2. split-panel의 addButton/editButton에서 연결:

```json
{
  "addButton": {
    "enabled": true,
    "mode": "modal",
    "modalScreenId": "{모달_screen_id}"
  },
  "editButton": {
    "enabled": true,
    "mode": "modal",
    "modalScreenId": "{모달_screen_id}"
  }
}
```

---

## 14. 비즈니스 로직 설정 (제어관리)

버튼 클릭 시 INSERT/UPDATE/DELETE, 상태 변경, 이력 기록 등이 필요한 경우.

### 14.1 v2-button-primary overrides

```json
{
  "id": "btn_confirm",
  "url": "@/lib/registry/components/v2-button-primary",
  "position": { "x": 1700, "y": 10 },
  "size": { "width": 100, "height": 40 },
  "overrides": {
    "text": "확정",
    "variant": "primary",
    "actionType": "button",
    "action": { "type": "custom" },
    "webTypeConfig": {
      "enableDataflowControl": true,
      "dataflowConfig": {
        "controlMode": "relationship",
        "relationshipConfig": {
          "relationshipId": "{관계_ID}",
          "relationshipName": "{관계명}",
          "executionTiming": "after"
        }
      }
    }
  }
}
```

### 14.2 dataflow_diagrams INSERT

```sql
INSERT INTO dataflow_diagrams (
  diagram_name, company_code,
  relationships, control, plan, node_positions
)
VALUES (
  '{관계도명}',
  '{company_code}',
  '[{"fromTable":"{소스_테이블}","toTable":"{타겟_테이블}","relationType":"data_save"}]'::jsonb,
  '[{
    "conditions": [{"field":"status","operator":"=","value":"대기","dataType":"string"}],
    "triggerType": "update"
  }]'::jsonb,
  '[{
    "actions": [
      {
        "actionType": "update",
        "targetTable": "{타겟_테이블}",
        "conditions": [{"field":"status","operator":"=","value":"대기"}],
        "fieldMappings": [{"targetField":"status","defaultValue":"확정"}]
      },
      {
        "actionType": "insert",
        "targetTable": "{이력_테이블}",
        "fieldMappings": [
          {"sourceField":"order_no","targetField":"order_no"},
          {"targetField":"action","defaultValue":"확정"}
        ]
      }
    ]
  }]'::jsonb,
  '[]'::jsonb
)
RETURNING diagram_id;
```

**executionTiming 선택**:
- `before`: 메인 액션 전 → 조건 체크 (조건 불충족 시 메인 액션 중단)
- `after`: 메인 액션 후 → 후처리 (이력 기록, 상태 변경 등)
- `replace`: 메인 액션 대체 → 제어만 실행

---

## 15. 전체 예시: "수주관리 화면 만들어줘"

### 요구사항 해석
- 마스터: order_master (수주번호, 거래처, 수주일자, 상태)
- 디테일: order_detail (품목, 수량, 단가, 금액)
- 패턴: B (마스터-디테일)

### 실행 SQL

```sql
-- ===== Step 1: 테이블 생성 =====
CREATE TABLE "order_master" (
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),
  "order_no" varchar(500),
  "customer_id" varchar(500),
  "order_date" varchar(500),
  "delivery_date" varchar(500),
  "status" varchar(500),
  "total_amount" varchar(500),
  "notes" varchar(500)
);

CREATE TABLE "order_detail" (
  "id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
  "created_date" timestamp DEFAULT now(),
  "updated_date" timestamp DEFAULT now(),
  "writer" varchar(500) DEFAULT NULL,
  "company_code" varchar(500),
  "order_master_id" varchar(500),
  "item_id" varchar(500),
  "quantity" varchar(500),
  "unit_price" varchar(500),
  "amount" varchar(500),
  "notes" varchar(500)
);

-- ===== Step 2: table_labels =====
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date) VALUES
  ('order_master', '수주 마스터', '수주 헤더 정보', now(), now()),
  ('order_detail', '수주 상세', '수주 품목별 상세', now(), now())
ON CONFLICT (table_name) DO UPDATE SET table_label = EXCLUDED.table_label, description = EXCLUDED.description, updated_date = now();

-- ===== Step 3: table_type_columns (확장 컬럼 포함) =====
-- order_master 기본 + 비즈니스 컬럼
INSERT INTO table_type_columns (
  table_name, column_name, company_code, input_type, detail_settings,
  is_nullable, is_unique, display_order, column_label, description, is_visible,
  created_date, updated_date
) VALUES
  ('order_master', 'id',            '*', 'text',     '{}', 'N', 'Y', -5, 'ID',     '기본키',           false, now(), now()),
  ('order_master', 'created_date',  '*', 'date',     '{}', 'Y', 'N', -4, '생성일시', '레코드 생성일시', false, now(), now()),
  ('order_master', 'updated_date',  '*', 'date',     '{}', 'Y', 'N', -3, '수정일시', '레코드 수정일시', false, now(), now()),
  ('order_master', 'writer',        '*', 'text',     '{}', 'Y', 'N', -2, '작성자',   '레코드 작성자',   false, now(), now()),
  ('order_master', 'company_code',  '*', 'text',     '{}', 'Y', 'N', -1, '회사코드', '회사 구분 코드',  false, now(), now()),
  ('order_master', 'order_no',      '*', 'text',     '{}', 'N', 'Y',  0, '수주번호', '수주 식별번호',   true,  now(), now()),
  ('order_master', 'customer_id',   '*', 'entity',   '{"referenceTable":"customer_info","referenceColumn":"id","displayColumn":"customer_name"}', 'N', 'N', 1, '거래처', '거래처 참조', true, now(), now()),
  ('order_master', 'order_date',    '*', 'date',     '{}', 'N', 'N',  2, '수주일자', '',               true,  now(), now()),
  ('order_master', 'delivery_date', '*', 'date',     '{}', 'Y', 'N',  3, '납기일',   '',               true,  now(), now()),
  ('order_master', 'status',        '*', 'code',     '{"codeCategory":"ORDER_STATUS"}', 'Y', 'N', 4, '상태', '수주 상태', true, now(), now()),
  ('order_master', 'total_amount',  '*', 'number',   '{}', 'Y', 'N',  5, '총금액',   '',               true,  now(), now()),
  ('order_master', 'notes',         '*', 'textarea', '{}', 'Y', 'N',  6, '비고',     '',               true,  now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET
  input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings,
  is_nullable = EXCLUDED.is_nullable, is_unique = EXCLUDED.is_unique,
  display_order = EXCLUDED.display_order, column_label = EXCLUDED.column_label,
  description = EXCLUDED.description, is_visible = EXCLUDED.is_visible, updated_date = now();

-- order_detail 기본 + 비즈니스 컬럼
INSERT INTO table_type_columns (
  table_name, column_name, company_code, input_type, detail_settings,
  is_nullable, is_unique, display_order, column_label, description, is_visible,
  created_date, updated_date
) VALUES
  ('order_detail', 'id',              '*', 'text',     '{}', 'N', 'Y', -5, 'ID',         '기본키',           false, now(), now()),
  ('order_detail', 'created_date',    '*', 'date',     '{}', 'Y', 'N', -4, '생성일시',    '레코드 생성일시', false, now(), now()),
  ('order_detail', 'updated_date',    '*', 'date',     '{}', 'Y', 'N', -3, '수정일시',    '레코드 수정일시', false, now(), now()),
  ('order_detail', 'writer',          '*', 'text',     '{}', 'Y', 'N', -2, '작성자',      '레코드 작성자',   false, now(), now()),
  ('order_detail', 'company_code',    '*', 'text',     '{}', 'Y', 'N', -1, '회사코드',    '회사 구분 코드',  false, now(), now()),
  ('order_detail', 'order_master_id', '*', 'text',     '{}', 'N', 'N',  0, '수주마스터ID', 'FK',             false, now(), now()),
  ('order_detail', 'item_id',         '*', 'entity',   '{"referenceTable":"item_info","referenceColumn":"id","displayColumn":"item_name"}', 'N', 'N', 1, '품목', '품목 참조', true, now(), now()),
  ('order_detail', 'quantity',        '*', 'number',   '{}', 'N', 'N',  2, '수량',   '',  true,  now(), now()),
  ('order_detail', 'unit_price',      '*', 'number',   '{}', 'Y', 'N',  3, '단가',   '',  true,  now(), now()),
  ('order_detail', 'amount',          '*', 'number',   '{}', 'Y', 'N',  4, '금액',   '',  true,  now(), now()),
  ('order_detail', 'notes',           '*', 'textarea', '{}', 'Y', 'N',  5, '비고',   '',  true,  now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET
  input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings,
  is_nullable = EXCLUDED.is_nullable, is_unique = EXCLUDED.is_unique,
  display_order = EXCLUDED.display_order, column_label = EXCLUDED.column_label,
  description = EXCLUDED.description, is_visible = EXCLUDED.is_visible, updated_date = now();

-- ===== Step 4: column_labels (company_code 필수!) =====
INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, company_code, created_date, updated_date) VALUES
  ('order_master', 'id', 'ID', 'text', '{}', '기본키', -5, true, '*', now(), now()),
  ('order_master', 'created_date', '생성일시', 'date', '{}', '', -4, true, '*', now(), now()),
  ('order_master', 'updated_date', '수정일시', 'date', '{}', '', -3, true, '*', now(), now()),
  ('order_master', 'writer', '작성자', 'text', '{}', '', -2, true, '*', now(), now()),
  ('order_master', 'company_code', '회사코드', 'text', '{}', '', -1, true, '*', now(), now()),
  ('order_master', 'order_no', '수주번호', 'text', '{}', '수주 식별번호', 0, true, '*', now(), now()),
  ('order_master', 'customer_id', '거래처', 'entity', '{"referenceTable":"customer_info","referenceColumn":"id","displayColumn":"customer_name"}', '거래처 참조', 1, true, '*', now(), now()),
  ('order_master', 'order_date', '수주일자', 'date', '{}', '', 2, true, '*', now(), now()),
  ('order_master', 'delivery_date', '납기일', 'date', '{}', '', 3, true, '*', now(), now()),
  ('order_master', 'status', '상태', 'code', '{"codeCategory":"ORDER_STATUS"}', '수주 상태', 4, true, '*', now(), now()),
  ('order_master', 'total_amount', '총금액', 'number', '{}', '', 5, true, '*', now(), now()),
  ('order_master', 'notes', '비고', 'textarea', '{}', '', 6, true, '*', now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET
  column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type,
  detail_settings = EXCLUDED.detail_settings, description = EXCLUDED.description,
  display_order = EXCLUDED.display_order, is_visible = EXCLUDED.is_visible, updated_date = now();

INSERT INTO column_labels (table_name, column_name, column_label, input_type, detail_settings, description, display_order, is_visible, company_code, created_date, updated_date) VALUES
  ('order_detail', 'id', 'ID', 'text', '{}', '기본키', -5, true, '*', now(), now()),
  ('order_detail', 'created_date', '생성일시', 'date', '{}', '', -4, true, '*', now(), now()),
  ('order_detail', 'updated_date', '수정일시', 'date', '{}', '', -3, true, '*', now(), now()),
  ('order_detail', 'writer', '작성자', 'text', '{}', '', -2, true, '*', now(), now()),
  ('order_detail', 'company_code', '회사코드', 'text', '{}', '', -1, true, '*', now(), now()),
  ('order_detail', 'order_master_id', '수주마스터ID', 'text', '{}', 'FK', 0, true, '*', now(), now()),
  ('order_detail', 'item_id', '품목', 'entity', '{"referenceTable":"item_info","referenceColumn":"id","displayColumn":"item_name"}', '품목 참조', 1, true, '*', now(), now()),
  ('order_detail', 'quantity', '수량', 'number', '{}', '', 2, true, '*', now(), now()),
  ('order_detail', 'unit_price', '단가', 'number', '{}', '', 3, true, '*', now(), now()),
  ('order_detail', 'amount', '금액', 'number', '{}', '', 4, true, '*', now(), now()),
  ('order_detail', 'notes', '비고', 'textarea', '{}', '', 5, true, '*', now(), now())
ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET
  column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type,
  detail_settings = EXCLUDED.detail_settings, description = EXCLUDED.description,
  display_order = EXCLUDED.display_order, is_visible = EXCLUDED.is_visible, updated_date = now();

-- ===== Step 5: screen_definitions =====
INSERT INTO screen_definitions (screen_name, screen_code, table_name, company_code, description, is_active, db_source_type, data_source_type, created_date)
VALUES ('수주관리', 'ILSHIN_ORDER_MNG', 'order_master', 'ILSHIN', '수주 마스터-디테일 관리', 'Y', 'internal', 'database', now());

-- ===== Step 6: screen_layouts_v2 (서브쿼리로 screen_id 참조) =====
INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, created_at, updated_at)
VALUES (
  (SELECT screen_id FROM screen_definitions WHERE screen_code = 'ILSHIN_ORDER_MNG'),
  'ILSHIN', 1, '기본 레이어',
  '{
    "version": "2.0",
    "components": [
      {
        "id": "split_order",
        "url": "@/lib/registry/components/v2-split-panel-layout",
        "position": {"x": 0, "y": 0},
        "size": {"width": 1920, "height": 850},
        "displayOrder": 0,
        "overrides": {
          "label": "수주관리",
          "splitRatio": 35,
          "resizable": true,
          "autoLoad": true,
          "syncSelection": true,
          "leftPanel": {
            "title": "수주 목록",
            "displayMode": "table",
            "tableName": "order_master",
            "showSearch": true,
            "showAdd": true,
            "showDelete": true,
            "columns": [
              {"name": "order_no", "label": "수주번호", "width": 120, "sortable": true},
              {"name": "customer_id", "label": "거래처", "width": 150, "isEntityJoin": true, "joinInfo": {"sourceTable": "order_master", "sourceColumn": "customer_id", "referenceTable": "customer_info", "joinAlias": "customer_name"}},
              {"name": "order_date", "label": "수주일자", "width": 100},
              {"name": "status", "label": "상태", "width": 80},
              {"name": "total_amount", "label": "총금액", "width": 120}
            ],
            "addButton": {"enabled": true, "mode": "auto"},
            "deleteButton": {"enabled": true, "confirmMessage": "선택한 수주를 삭제하시겠습니까?"}
          },
          "rightPanel": {
            "title": "수주 상세",
            "displayMode": "table",
            "tableName": "order_detail",
            "relation": {
              "type": "detail",
              "leftColumn": "id",
              "rightColumn": "order_master_id",
              "foreignKey": "order_master_id"
            },
            "columns": [
              {"name": "item_id", "label": "품목", "width": 150, "isEntityJoin": true, "joinInfo": {"sourceTable": "order_detail", "sourceColumn": "item_id", "referenceTable": "item_info", "joinAlias": "item_name"}},
              {"name": "quantity", "label": "수량", "width": 80, "editable": true},
              {"name": "unit_price", "label": "단가", "width": 100, "editable": true},
              {"name": "amount", "label": "금액", "width": 100},
              {"name": "notes", "label": "비고", "width": 200, "editable": true}
            ],
            "addButton": {"enabled": true, "mode": "auto"},
            "editButton": {"enabled": true, "mode": "auto"},
            "deleteButton": {"enabled": true, "confirmMessage": "삭제하시겠습니까?"}
          }
        }
      }
    ],
    "gridSettings": {"columns": 12, "gap": 16, "padding": 16},
    "screenResolution": {"width": 1920, "height": 1080}
  }'::jsonb,
  now(), now()
)
ON CONFLICT (screen_id, company_code, layer_id) DO UPDATE SET layout_data = EXCLUDED.layout_data, updated_at = now();

-- ===== Step 7: menu_info (objid에 순서값 더해서 PK 충돌 방지) =====
INSERT INTO menu_info (
  objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng,
  seq, menu_url, menu_desc, writer, regdate, status, company_code, screen_code
)
VALUES (
  extract(epoch from now())::bigint * 1000 + 1, 0, {부모_메뉴_objid},
  '수주관리', 'Order Management',
  1, '/screen/ILSHIN_ORDER_MNG', '수주 마스터-디테일 관리',
  'admin', now(), 'active', 'ILSHIN', 'ILSHIN_ORDER_MNG'
);
```

---

## 16. 컴포넌트 빠른 참조표

| 요구사항 | 컴포넌트 url | 핵심 overrides |
|----------|-------------|---------------|
| 데이터 테이블 | v2-table-list | `tableName`, `columns`, `pagination` |
| 검색 바 | v2-table-search-widget | `autoSelectFirstTable` |
| 좌우 분할 | v2-split-panel-layout | `leftPanel`, `rightPanel`, `relation`, `splitRatio` |
| 그룹화 테이블 | v2-table-grouped | `groupConfig.groupByColumn`, `summary` |
| 간트차트 (리소스 기반) | v2-timeline-scheduler | `fieldMapping`, `resourceTable` |
| 타임라인 (품목 그룹) | v2-timeline-scheduler | `viewMode:"itemGrouped"`, `staticFilters`, `linkedFilter` |
| 피벗 분석 | v2-pivot-grid | `fields(area, summaryType)` |
| 카드 뷰 | v2-card-display | `columnMapping`, `cardsPerRow` |
| 액션 버튼 | v2-button-primary | `text`, `actionType`, `webTypeConfig.dataflowConfig` |
| 텍스트 입력 | v2-input | `inputType`, `tableName`, `columnName` |
| 선택 | v2-select | `mode`, `source` |
| 날짜 | v2-date | `dateType` |
| 자동 채번 | v2-numbering-rule | `rule` |
| BOM 트리 | v2-bom-tree | `detailTable`, `foreignKey`, `parentKey` |
| BOM 편집 | v2-bom-item-editor | `detailTable`, `sourceTable`, `itemCodeField` |
| 결재 스테퍼 | v2-approval-step | `targetTable`, `displayMode` |
| 파일 업로드 | v2-file-upload | `multiple`, `accept`, `maxSize` |
| 상태별 건수 | v2-status-count | `tableName`, `statusColumn`, `items` |
| 집계 카드 | v2-aggregation-widget | `tableName`, `items` |
| 반복 데이터 관리 | v2-repeater | `renderMode`, `mainTableName`, `foreignKeyColumn` |
| 반복 렌더링 | v2-repeat-container | `dataSourceType`, `layout`, `gridColumns` |
| 그룹 컨테이너 (테두리) | v2-section-card | `title`, `collapsible`, `borderStyle` |
| 그룹 컨테이너 (배경색) | v2-section-paper | `backgroundColor`, `shadow`, `padding` |
| 캔버스 분할선 | v2-split-line | `resizable`, `lineColor`, `lineWidth` |
| 카테고리 관리 | v2-category-manager | `tableName`, `columnName`, `menuObjid` |
| 미디어 | v2-media | `mediaType`, `multiple`, `maxSize` |
| 위치 교환 | v2-location-swap-selector | `dataSource`, `departureField`, `destinationField` |
| 창고 랙 | v2-rack-structure | `codePattern`, `namePattern`, `maxRows` |
| 공정 작업기준 | v2-process-work-standard | `dataSource.itemTable`, `dataSource.routingDetailTable` |
| 품목 라우팅 | v2-item-routing | `dataSource.itemTable`, `dataSource.routingDetailTable` |

---

## 17. v2-timeline-scheduler 고급 설정 가이드

### 17.1 viewMode 선택 기준

| viewMode | 용도 | Y축 |
|----------|------|-----|
| (미설정) | 설비별 작업일정, 보전계획 | 설비/작업자 행 |
| `"itemGrouped"` | 생산계획, 출하계획 | 품목별 카드 그룹 |

### 17.2 staticFilters vs linkedFilter 비교

| 구분 | staticFilters | linkedFilter |
|------|--------------|-------------|
| **적용 시점** | DB SELECT 쿼리 시 | 클라이언트 렌더링 시 |
| **위치** | 서버 측 (WHERE 절) | 프론트 측 (JS 필터링) |
| **변경 가능** | 고정 (layout에 하드코딩) | 동적 (이벤트 기반) |
| **용도** | 완제품/반제품 구분 등 | 좌측 테이블 선택 연동 |

**조합 예시**:
```
staticFilters: { product_type: "완제품" }  → DB에서 완제품만 조회
linkedFilter: { sourceField: "part_code", targetField: "item_code" }
  → 완제품 중 좌측에서 선택한 품목만 표시
```

### 17.3 자동 스케줄 생성 (내장 기능)

`viewMode: "itemGrouped"` + `staticFilters.product_type === "완제품"` 조건 충족 시,
타임라인 툴바에 **완제품 계획 생성** / **반제품 계획 생성** 버튼이 자동 표시됨.

**완제품 계획 생성 플로우**:
```
1. linkedFilter로 선택된 수주 품목 수집
2. POST /production/generate-schedule/preview → 미리보기 다이얼로그
3. 사용자 확인 → POST /production/generate-schedule → 실제 생성
4. 타임라인 자동 새로고침
```

**반제품 계획 생성 플로우**:
```
1. 현재 타임라인의 완제품 스케줄 ID 수집
2. POST /production/generate-semi-schedule/preview → BOM 기반 소요량 계산
3. 미리보기 다이얼로그 (기존 반제품 계획 삭제/유지 정보 포함)
4. 사용자 확인 → POST /production/generate-semi-schedule → 실제 생성
5. 반제품 탭으로 전환 시 새 데이터 표시
```

### 17.4 반제품 탭 주의사항

반제품 전용 타임라인에는 `linkedFilter`를 **걸지 않는다**.

이유: 반제품의 `item_code`(예: `SEMI-001`)와 수주 품목의 `part_code`(예: `ITEM-001`)가
서로 다른 값이므로 매칭이 불가능하다. `staticFilters: { product_type: "반제품" }`만 설정.

```json
{
  "id": "timeline_semi",
  "componentType": "v2-timeline-scheduler",
  "componentConfig": {
    "selectedTable": "production_plan_mng",
    "viewMode": "itemGrouped",
    "staticFilters": { "product_type": "반제품" },
    "fieldMapping": { "..." : "..." }
  }
}
```

### 17.5 이벤트 연동 (v2EventBus)

타임라인 컴포넌트는 `v2EventBus`를 통해 다른 컴포넌트와 통신한다.

| 이벤트 | 방향 | 설명 |
|--------|------|------|
| `TABLE_SELECTION_CHANGE` | 수신 | 좌측 테이블 행 선택 시 linkedFilter 적용 |
| `TIMELINE_REFRESH` | 발신/수신 | 타임라인 데이터 새로고침 |

**연결 필터 이벤트 페이로드**:
```typescript
{
  eventType: "TABLE_SELECTION_CHANGE",
  source: "grouped_orders",
  tableName: "sales_order_mng",
  selectedRows: [
    { id: "...", part_code: "ITEM-001", ... },
    { id: "...", part_code: "ITEM-002", ... }
  ]
}
```

타임라인은 `selectedRows`에서 `linkedFilter.sourceField` 값을 추출하여,
자신의 데이터 중 `linkedFilter.targetField`가 일치하는 항목만 표시.
