# WACE 화면 구현 실행 가이드 (챗봇/AI 에이전트 전용) > **최종 업데이트**: 2026-03-13 > **용도**: 사용자가 "수주관리 화면 만들어줘"라고 요청하면, 이 문서를 참조하여 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 } } ``` --- ## 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 → 패턴 E (타임라인) → v2-timeline-scheduler └─ NO ↓ Q2. 다차원 집계/피벗 분석이 필요한가? ├─ YES → 피벗 → v2-pivot-grid └─ NO ↓ Q3. 데이터를 그룹별로 접기/펼치기가 필요한가? ├─ YES → 패턴 D (그룹화) → v2-table-grouped └─ NO ↓ Q4. 이미지+정보를 카드 형태로 표시하는가? ├─ YES → 카드뷰 → v2-card-display └─ NO ↓ Q5. 마스터 테이블 선택 시 연관 디테일이 필요한가? ├─ YES → Q5-1. 디테일에 여러 탭이 필요한가? │ ├─ YES → 패턴 C (마스터-디테일+탭) → v2-split-panel-layout + additionalTabs │ └─ NO → 패턴 B (마스터-디테일) → v2-split-panel-layout └─ NO → 패턴 A (기본 마스터) → v2-table-search-widget + v2-table-list ``` --- ## 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-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` |