ERP-node/docs/화면관리_시스템_설계.md

60 KiB

화면관리 시스템 설계 문서

📋 목차

  1. 시스템 개요
  2. 아키텍처 구조
  3. 핵심 기능
  4. 데이터베이스 설계
  5. 화면 구성 요소
  6. 드래그앤드롭 설계
  7. 테이블 타입 연계
  8. API 설계
  9. 프론트엔드 구현
  10. 백엔드 구현
  11. 사용 시나리오
  12. 개발 계획

🎯 시스템 개요

화면관리 시스템이란?

화면관리 시스템은 사용자가 속한 회사에 맞춰 화면을 드래그앤드롭으로 설계하고 관리할 수 있는 시스템입니다. 테이블 타입관리와 연계하여 각 필드가 웹에서 어떻게 표시될지를 정의하고, 사용자가 직관적으로 화면을 구성할 수 있습니다.

주요 특징

  • 회사별 화면 관리: 사용자 회사 코드에 따른 화면 접근 제어
  • 드래그앤드롭 인터페이스: 직관적인 화면 설계
  • 컨테이너 그룹화: 컴포넌트를 깔끔하게 정렬하는 그룹 기능
  • 테이블 타입 연계: 컬럼의 웹 타입에 따른 자동 위젯 생성
  • 실시간 미리보기: 설계한 화면을 실제 화면과 동일하게 확인 가능
  • 메뉴 연동: 각 회사의 메뉴에 화면 할당 및 관리

🎯 현재 테이블 구조와 100% 호환

기존 테이블 타입관리 시스템과 완벽 연계:

  • table_labels: 테이블 메타데이터와 연계
  • column_labels: 컬럼 웹 타입 및 상세 설정과 연계
  • 웹 타입 지원: text, number, date, code, entity, textarea, select, checkbox, radio, file
  • 상세 설정: JSON 형태로 유연한 설정 저장
  • 코드 연계: 공통코드 시스템과 완벽 연동
  • 엔티티 참조: 참조 테이블 시스템 완벽 지원

별도의 테이블 구조 변경 없이 바로 개발 가능! 🚀

🏢 회사별 화면 관리 시스템

사용자 권한에 따른 화면 접근 제어:

  • 일반 사용자: 자신이 속한 회사의 화면만 제작/수정 가능
  • 관리자 (회사코드 '*'): 모든 회사의 화면을 제어 가능
  • 회사별 메뉴 할당: 각 회사의 메뉴에만 화면 할당 가능
  • 권한 격리: 회사 간 화면 데이터 완전 분리

지원하는 웹 타입

테이블 타입관리에서 각 컬럼별로 설정할 수 있는 웹 타입입니다:

  • text: 일반 텍스트 입력
  • number: 숫자 입력
  • date: 날짜 선택기
  • code: 코드 선택 (공통코드)
  • entity: 엔티티 참조 (참조테이블)
  • textarea: 여러 줄 텍스트
  • select: 드롭다운 선택
  • checkbox: 체크박스
  • radio: 라디오 버튼
  • file: 파일 업로드

구현 완료: 테이블 타입관리 시스템에서 위의 모든 웹 타입을 지정할 수 있으며, 웹 타입별로 적절한 상세 설정을 자동으로 제공합니다.

웹 타입 관리 기능

1. 웹 타입 설정

  • 자동 상세 설정: 웹 타입 선택 시 해당 타입에 맞는 기본 상세 설정을 자동으로 제공
  • 실시간 저장: 웹 타입 변경 시 즉시 백엔드 데이터베이스에 저장
  • 오류 복구: 저장 실패 시 원래 상태로 자동 복원

2. 웹 타입별 상세 설정

웹 타입 기본 상세 설정 추가 설정 옵션
text 최대 길이: 255자 사용자 정의 설정
number 숫자 입력 (정수/실수) 범위, 정밀도 설정
date 날짜 형식: YYYY-MM-DD 날짜 범위, 형식 설정
textarea 여러 줄 텍스트 (최대 1000자) 행 수, 최대 길이 설정
select 드롭다운 선택 옵션 옵션 목록 설정
checkbox 체크박스 (Y/N) 기본값, 라벨 설정
radio 라디오 버튼 그룹 옵션 그룹 설정
file 파일 업로드 (최대 10MB) 파일 형식, 크기 설정
code 공통코드 선택 코드 카테고리 지정
entity 엔티티 참조 참조 테이블/컬럼 지정

3. 사용 방법

  1. 테이블 선택: 테이블 타입관리에서 관리할 테이블 선택
  2. 컬럼 확인: 해당 테이블의 모든 컬럼 정보 표시
  3. 웹 타입 설정: 각 컬럼의 웹 타입을 드롭다운에서 선택
  4. 자동 저장: 선택 즉시 백엔드에 저장되고 상세 설정 자동 적용
  5. 추가 설정: 필요시 상세 설정을 사용자 정의로 수정

🏗️ 아키텍처 구조

전체 구조도

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Frontend      │    │   Backend       │    │   Database      │
│                 │    │                 │    │                 │
│ ┌─────────────┐ │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │ Screen      │ │    │ │ Screen      │ │    │ │ screen_     │ │
│ │ Designer    │ │    │ │ Management  │ │    │ │ definitions │ │
│ │ (React)     │ │    │ │ Controller  │ │    │ │ Table       │ │
│ └─────────────┘ │    │ └─────────────┘ │    │ └─────────────┘ │
│                 │    │                 │    │                 │
│ ┌─────────────┐ │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │ Drag &      │ │    │ │ Screen      │ │    │ │ screen_     │ │
│ │ Drop        │ │    │ │ Management  │ │    │ │ layouts     │ │
│ │ Components  │ │    │ │ Service     │ │    │ │ Table       │ │
│ └─────────────┘ │    │ └─────────────┘ │    │ └─────────────┘ │
│                 │    │                 │    │                 │
│ ┌─────────────┐ │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │ Preview     │ │    │ │ Table       │ │    │ │ table_      │ │
│ │ Generator   │ │    │ │ Type        │ │    │ │ labels      │ │
│ │ (Runtime)   │ │    │ │ Service     │ │    │ │ Table       │ │
│ └─────────────┘ │    │ └─────────────┘ │    │ └─────────────┘ │
└─────────────────┘    └─────────────────┘    └─────────────────┘

회사별 권한 관리 구조

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   사용자        │    │   권한 검증      │    │   화면 데이터    │
│                 │    │                 │    │                 │
│ ┌─────────────┐ │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │ 회사코드    │ │───▶│ │ 권한 검증    │ │───▶│ │ 회사별      │ │
│ │ (company_   │ │    │ │ 미들웨어    │ │    │ │ 화면        │ │
│ │  code)      │ │    │ │             │ │    │ │ 격리        │ │
│ └─────────────┘ │    │ └─────────────┘ │    │ └─────────────┘ │
│                 │    │                 │    │                 │
│ ┌─────────────┐ │    │ ┌─────────────┐ │    │ ┌─────────────┐ │
│ │ 권한 레벨   │ │    │ │ 회사별      │ │    │ │ 메뉴 할당   │ │
│ │ (admin: '*')│ │    │ │ 데이터      │ │    │ │ 제한        │ │
│ └─────────────┘ │    │ │ 필터링      │ │    │ └─────────────┘ │
└─────────────────┘    └─────────────────┘    └─────────────────┘

데이터 흐름

  1. 테이블 타입 정의: 테이블 타입관리에서 컬럼의 웹 타입 설정
  2. 화면 설계: 드래그앤드롭으로 화면 레이아웃 구성
  3. 위젯 매핑: 컬럼과 화면 위젯을 연결
  4. 설정 저장: 화면 정의를 데이터베이스에 저장
  5. 런타임 생성: 실제 서비스 화면을 동적으로 생성

🚀 핵심 기능

1. 화면 설계기 (Screen Designer)

  • 드래그앤드롭 인터페이스: 컴포넌트를 캔버스에 배치
  • 그리드 시스템: 12컬럼 그리드 기반 레이아웃
  • 컨테이너 그룹화: 컴포넌트를 깔끔하게 정렬하는 그룹 기능
  • 실시간 미리보기: 설계한 화면을 실제 화면과 동일하게 확인

2. 컴포넌트 라이브러리

  • 입력 컴포넌트: text, number, date, textarea 등
  • 선택 컴포넌트: select, checkbox, radio 등
  • 표시 컴포넌트: label, display, image 등
  • 레이아웃 컴포넌트: container, row, column, group 등
  • 컨테이너 컴포넌트: 컴포넌트들을 그룹으로 묶는 기능

3. 회사별 권한 관리

  • 회사 코드 기반 접근 제어: 사용자 회사 코드에 따른 화면 접근
  • 관리자 권한: 회사 코드 '*'인 사용자는 모든 회사 화면 제어
  • 회사별 메뉴 할당: 각 회사의 메뉴에만 화면 할당 가능
  • 데이터 격리: 회사 간 화면 데이터 완전 분리

4. 테이블 연계 시스템

  • 자동 위젯 생성: 컬럼의 웹 타입에 따른 위젯 자동 생성
  • 데이터 바인딩: 컬럼과 위젯의 자동 연결
  • 유효성 검증: 컬럼 설정에 따른 자동 검증 규칙 적용

5. 템플릿 시스템

  • 기본 템플릿: CRUD, 목록, 상세 등 기본 패턴
  • 사용자 정의 템플릿: 자주 사용하는 레이아웃 저장
  • 템플릿 공유: 팀원 간 템플릿 공유 및 재사용

6. 메뉴 연동 시스템

  • 회사별 메뉴 할당: 각 회사의 메뉴에만 화면 할당
  • 메뉴-화면 연결: 메뉴와 화면의 1:1 또는 1:N 연결
  • 권한 기반 메뉴 표시: 사용자 권한에 따른 메뉴 표시 제어

🗄️ 데이터베이스 설계

1. 기존 테이블 구조 (테이블 타입관리)

table_labels (테이블 메타데이터)

CREATE TABLE table_labels (
  table_name varchar(100) NOT NULL,
  table_label varchar(200) NULL,
  description text NULL,
  created_date timestamp DEFAULT now() NULL,
  updated_date timestamp DEFAULT now() NULL,
  CONSTRAINT table_labels_pkey PRIMARY KEY (table_name)
);

column_labels (컬럼 메타데이터 + 웹 타입)

CREATE TABLE column_labels (
  id serial4 NOT NULL,
  table_name varchar(100) NULL,
  column_name varchar(100) NULL,
  column_label varchar(200) NULL,
  web_type varchar(50) NULL,                    -- 🎯 핵심: 웹 타입 정의
  detail_settings text NULL,                    -- 🎯 핵심: 웹 타입별 상세 설정 (JSON)
  description text NULL,
  display_order int4 DEFAULT 0 NULL,
  is_visible bool DEFAULT true NULL,
  code_category varchar(100) NULL,              -- 🎯 code 타입용: 공통코드 카테고리
  code_value varchar(100) NULL,
  reference_table varchar(100) NULL,            -- 🎯 entity 타입용: 참조 테이블
  reference_column varchar(100) NULL,           -- 🎯 entity 타입용: 참조 컬럼
  created_date timestamp DEFAULT now() NULL,
  updated_date timestamp DEFAULT now() NULL,
  CONSTRAINT column_labels_pkey PRIMARY KEY (id),
  CONSTRAINT column_labels_table_name_column_name_key UNIQUE (table_name, column_name)
);

-- 외래키 제약조건
ALTER TABLE column_labels ADD CONSTRAINT column_labels_table_name_fkey
  FOREIGN KEY (table_name) REFERENCES table_labels(table_name);

권장 인덱스 추가:

-- 자주 조회되는 컬럼 조합에 인덱스 추가
CREATE INDEX idx_column_labels_web_type ON column_labels(web_type);
CREATE INDEX idx_column_labels_code_category ON column_labels(code_category);
CREATE INDEX idx_column_labels_reference ON column_labels(reference_table, reference_column);

-- JSON 필드 검색을 위한 GIN 인덱스 (PostgreSQL)
CREATE INDEX idx_column_labels_detail_settings ON column_labels USING GIN (detail_settings);

2. 화면관리 시스템 테이블 구조

screen_definitions (화면 정의)

CREATE TABLE screen_definitions (
  screen_id SERIAL PRIMARY KEY,
  screen_name VARCHAR(100) NOT NULL,
  screen_code VARCHAR(50) UNIQUE NOT NULL,
  table_name VARCHAR(100) NOT NULL,                    -- 🎯 table_labels와 연계
  company_code VARCHAR(50) NOT NULL,                   -- 🎯 회사 코드 (권한 관리용)
  description TEXT,
  is_active CHAR(1) DEFAULT 'Y',
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  created_by VARCHAR(50),
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_by VARCHAR(50),

  -- 외래키 제약조건
  CONSTRAINT fk_screen_definitions_table_name
    FOREIGN KEY (table_name) REFERENCES table_labels(table_name)
);

-- 회사 코드 인덱스 추가
CREATE INDEX idx_screen_definitions_company_code ON screen_definitions(company_code);

screen_layouts (화면 레이아웃)

CREATE TABLE screen_layouts (
  layout_id SERIAL PRIMARY KEY,
  screen_id INTEGER REFERENCES screen_definitions(screen_id),
  component_type VARCHAR(50) NOT NULL, -- container, row, column, widget
  component_id VARCHAR(100) UNIQUE NOT NULL,
  parent_id VARCHAR(100), -- 부모 컴포넌트 ID
  position_x INTEGER NOT NULL, -- X 좌표 (그리드)
  position_y INTEGER NOT NULL, -- Y 좌표 (그리드)
  width INTEGER NOT NULL, -- 너비 (그리드 컬럼 수: 1-12)
  height INTEGER NOT NULL, -- 높이 (픽셀)
  properties JSONB, -- 컴포넌트별 속성
  display_order INTEGER DEFAULT 0,
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

screen_widgets (화면 위젯)

CREATE TABLE screen_widgets (
  widget_id SERIAL PRIMARY KEY,
  layout_id INTEGER REFERENCES screen_layouts(layout_id),
  table_name VARCHAR(100) NOT NULL,                     -- 🎯 column_labels와 연계
  column_name VARCHAR(100) NOT NULL,                    -- 🎯 column_labels와 연계
  widget_type VARCHAR(50) NOT NULL,                     -- 🎯 column_labels.web_type과 동기화
  label VARCHAR(200), -- 표시 라벨 (column_labels.column_label 사용)
  placeholder VARCHAR(200), -- 플레이스홀더
  is_required BOOLEAN DEFAULT FALSE,
  is_readonly BOOLEAN DEFAULT FALSE,
  validation_rules JSONB, -- 유효성 검증 규칙 (column_labels.detail_settings에서 자동 생성)
  display_properties JSONB, -- 표시 속성 (column_labels.detail_settings에서 자동 생성)
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  -- 외래키 제약조건
  CONSTRAINT fk_screen_widgets_column_labels
    FOREIGN KEY (table_name, column_name) REFERENCES column_labels(table_name, column_name)
);

screen_templates (화면 템플릿)

CREATE TABLE screen_templates (
  template_id SERIAL PRIMARY KEY,
  template_name VARCHAR(100) NOT NULL,
  template_type VARCHAR(50) NOT NULL, -- CRUD, LIST, DETAIL 등
  company_code VARCHAR(50) NOT NULL, -- 🎯 회사 코드 (권한 관리용)
  description TEXT,
  layout_data JSONB, -- 레이아웃 데이터
  is_public BOOLEAN DEFAULT FALSE, -- 공개 여부
  created_by VARCHAR(50),
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 회사 코드 인덱스 추가
CREATE INDEX idx_screen_templates_company_code ON screen_templates(company_code);

screen_menu_assignments (화면-메뉴 할당)

CREATE TABLE screen_menu_assignments (
  assignment_id SERIAL PRIMARY KEY,
  screen_id INTEGER NOT NULL,
  menu_id INTEGER NOT NULL,
  company_code VARCHAR(50) NOT NULL, -- 🎯 회사 코드 (권한 관리용)
  display_order INTEGER DEFAULT 0,
  is_active CHAR(1) DEFAULT 'Y',
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  created_by VARCHAR(50),

  -- 외래키 제약조건
  CONSTRAINT fk_screen_menu_assignments_screen_id
    FOREIGN KEY (screen_id) REFERENCES screen_definitions(screen_id),
  CONSTRAINT fk_screen_menu_assignments_menu_id
    FOREIGN KEY (menu_id) REFERENCES menu_info(menu_id),

  -- 유니크 제약조건 (한 메뉴에 같은 화면 중복 할당 방지)
  CONSTRAINT uk_screen_menu_company UNIQUE (screen_id, menu_id, company_code)
);

-- 회사 코드 인덱스 추가
CREATE INDEX idx_screen_menu_assignments_company_code ON screen_menu_assignments(company_code);

3. 테이블 간 연계 관계

table_labels (테이블 메타데이터)
    ↓ (1:N)
column_labels (컬럼 메타데이터 + 웹 타입)
    ↓ (1:N)
screen_definitions (화면 정의)
    ↓ (1:N)
screen_layouts (화면 레이아웃)
    ↓ (1:N)
screen_widgets (화면 위젯)
    ↓ (1:N)
screen_menu_assignments (화면-메뉴 할당)

핵심 연계 포인트:

  • screen_definitions.table_nametable_labels.table_name
  • screen_widgets.table_name, column_namecolumn_labels.table_name, column_name
  • screen_widgets.widget_typecolumn_labels.web_type (자동 동기화)
  • screen_definitions.company_code ↔ 사용자 회사 코드 (권한 관리)
  • screen_menu_assignments.company_code ↔ 메뉴 회사 코드 (메뉴 할당 제한)

🎨 화면 구성 요소

1. 레이아웃 컴포넌트

Container (컨테이너)

interface ContainerProps {
  id: string;
  type: "container";
  width: number; // 1-12
  height: number;
  padding: number;
  margin: number;
  backgroundColor?: string;
  border?: string;
  borderRadius?: number;
  shadow?: string;
}

Group (그룹)

interface GroupProps {
  id: string;
  type: "group";
  title?: string;
  width: number; // 1-12
  height: number;
  padding: number;
  margin: number;
  backgroundColor?: string;
  border?: string;
  borderRadius?: number;
  shadow?: string;
  collapsible?: boolean; // 접을 수 있는 그룹
  collapsed?: boolean; // 접힌 상태
  children: string[]; // 포함된 컴포넌트 ID 목록
}

Row (행)

interface RowProps {
  id: string;
  type: "row";
  columns: number; // 1-12
  gap: number;
  alignItems: "start" | "center" | "end";
  justifyContent: "start" | "center" | "end" | "space-between";
}

Column (컬럼)

interface ColumnProps {
  id: string;
  type: "column";
  width: number; // 1-12
  offset: number; // 오프셋
  order: number; // 순서
}

2. 입력 위젯

Text Input

interface TextInputProps {
  id: string;
  type: "text";
  label: string;
  placeholder?: string;
  required: boolean;
  maxLength?: number;
  pattern?: string;
  columnName: string; // 연결된 테이블 컬럼
}

Select Dropdown

interface SelectProps {
  id: string;
  type: "select";
  label: string;
  options: Array<{ value: string; label: string }>;
  multiple: boolean;
  searchable: boolean;
  columnName: string;
}

Date Picker

interface DatePickerProps {
  id: string;
  type: "date";
  label: string;
  format: string; // YYYY-MM-DD, MM/DD/YYYY 등
  minDate?: string;
  maxDate?: string;
  columnName: string;
}

3. 표시 위젯

Label

interface LabelProps {
  id: string;
  type: "label";
  text: string;
  fontSize: number;
  fontWeight: "normal" | "bold";
  color: string;
  alignment: "left" | "center" | "right";
}

Display Field

interface DisplayFieldProps {
  id: string;
  type: "display";
  label: string;
  columnName: string;
  format?: string; // 날짜, 숫자 포맷
  alignment: "left" | "center" | "right";
}

🖱️ 드래그앤드롭 설계

1. 드래그앤드롭 아키텍처

// 드래그 상태 관리
interface DragState {
  isDragging: boolean;
  draggedItem: ComponentData | null;
  dragSource: "toolbox" | "canvas";
  dropTarget: string | null;
  dropZone?: DropZone; // 드롭 가능한 영역 정보
}

// 그룹화 상태 관리
interface GroupState {
  isGrouping: boolean;
  selectedComponents: string[];
  groupTarget: string | null;
  groupMode: "create" | "add" | "remove";
}

// 드롭 영역 정의 interface DropZone { id: string; accepts: string[]; // 허용되는 컴포넌트 타입 position: { x: number; y: number }; size: { width: number; height: number }; }


### 2. 컴포넌트 배치 로직

```typescript
// 그리드 기반 배치
function calculateGridPosition(
  mouseX: number,
  mouseY: number,
  gridSize: number
): GridPosition {
  return {
    x: Math.floor(mouseX / gridSize),
    y: Math.floor(mouseY / gridSize),
  };
}

// 컴포넌트 크기 조정
function resizeComponent(
  component: ComponentData,
  newWidth: number,
  newHeight: number
): ComponentData {
  return {
    ...component,
    width: Math.max(1, Math.min(12, newWidth)),
    height: Math.max(50, newHeight),
  };
}

3. 실시간 미리보기

// 캔버스 상태를 실제 컴포넌트로 변환
function generatePreview(layout: LayoutData): React.ReactElement {
  return (
    <div className="screen-preview">
      {layout.components.map((component) => renderComponent(component))}
    </div>
  );
}

// 그룹 컴포넌트 렌더링
function renderGroupComponent(
  group: GroupProps,
  components: ComponentData[]
): React.ReactElement {
  const groupChildren = components.filter((c) => group.children.includes(c.id));

  return (
    <div className="component-group" style={getGroupStyles(group)}>
      {group.title && <div className="group-title">{group.title}</div>}
      <div className="group-content">
        {groupChildren.map((component) => renderComponent(component))}
      </div>
    </div>
  );
}

// 컴포넌트 렌더링 function renderComponent(component: ComponentData): React.ReactElement { switch (component.type) { case "text": return <TextInput {...component.props} />; case "select": return <Select {...component.props} />; case "date": return <DatePicker {...component.props} />; default: return

Unknown component
; } }


## 🔗 테이블 타입 연계

### 1. 웹 타입 설정 방법

#### 테이블 타입관리에서 웹 타입 설정

**현재 테이블 구조 기반 설정:**

```typescript
// 컬럼별 웹 타입 설정 인터페이스 (현재 테이블 구조와 완벽 일치)
interface ColumnWebTypeSetting {
  tableName: string; // column_labels.table_name
  columnName: string; // column_labels.column_name
  webType: WebType; // column_labels.web_type
  columnLabel?: string; // column_labels.column_label
  detailSettings?: string; // column_labels.detail_settings (JSON)
  codeCategory?: string; // column_labels.code_category
  referenceTable?: string; // column_labels.reference_table
  referenceColumn?: string; // column_labels.reference_column
  isVisible?: boolean; // column_labels.is_visible
  displayOrder?: number; // column_labels.display_order
  description?: string; // column_labels.description
}

// 웹 타입 설정 API (column_labels 테이블에 직접 저장)
async function setColumnWebType(setting: ColumnWebTypeSetting): Promise<void> {
  await api.post("/table-management/columns/web-type", setting);
}

// 웹 타입 조회 API (column_labels 테이블에서 직접 조회)
async function getColumnWebType(
  tableName: string,
  columnName: string
): Promise<ColumnWebTypeSetting> {
  return api.get(
    `/table-management/columns/${tableName}/${columnName}/web-type`
  );
}

// 테이블의 모든 컬럼 웹 타입 조회
async function getTableColumnWebTypes(
  tableName: string
): Promise<ColumnWebTypeSetting[]> {
  return api.get(`/table-management/tables/${tableName}/columns/web-types`);
}

웹 타입별 추가 설정 (현재 테이블 구조 기반)

column_labels.detail_settings 필드에 JSON 형태로 저장:

웹 타입 detail_settings JSON 구조 설명
text {"maxLength": 255, "pattern": "regex"} 최대 길이, 정규식 패턴
number {"min": 0, "max": 999, "step": 1} 최소값, 최대값, 증감 단위
date {"format": "YYYY-MM-DD", "minDate": "", "maxDate": ""} 날짜 형식, 범위 제한
code {"codeCategory": "USER_STATUS"} 공통코드 카테고리 (code_category 필드와 연계)
entity {"referenceTable": "users", "referenceColumn": "id"} 참조 테이블 및 컬럼 (reference_table, reference_column 필드와 연계)
textarea {"rows": 3, "maxLength": 1000} 행 수, 최대 길이
select {"options": [{"value": "Y", "label": "예"}, {"value": "N", "label": "아니오"}]} 옵션 목록, 다중 선택
checkbox {"defaultChecked": false, "label": "동의함"} 기본 체크 상태, 라벨
radio {"options": [{"value": "M", "label": "남성"}, {"value": "F", "label": "여성"}]} 라디오 옵션 목록
file {"accept": ".jpg,.png,.pdf", "maxSize": 10485760} 허용 파일 형식, 최대 크기

실제 데이터베이스 저장 예시:

-- text 타입 컬럼 설정
UPDATE column_labels
SET detail_settings = '{"maxLength": 255, "pattern": "^[a-zA-Z0-9가-힣]+$"}'
WHERE table_name = 'user_info' AND column_name = 'user_name';

-- code 타입 컬럼 설정 (code_category와 연계)
UPDATE column_labels
SET web_type = 'code',
    code_category = 'USER_STATUS',
    detail_settings = '{"displayFormat": "label"}'
WHERE table_name = 'user_info' AND column_name = 'status';

-- entity 타입 컬럼 설정 (reference_table, reference_column과 연계)
UPDATE column_labels
SET web_type = 'entity',
    reference_table = 'dept_info',
    reference_column = 'dept_code',
    detail_settings = '{"searchable": true, "multiple": false}'
WHERE table_name = 'user_info' AND column_name = 'dept_code';

2. 자동 위젯 생성

// 컬럼 정보를 기반으로 위젯 자동 생성 (현재 테이블 구조 기반)
function generateWidgetFromColumn(column: ColumnInfo): WidgetData {
  const baseWidget = {
    id: generateId(),
    tableName: column.table_name, // 🎯 column_labels.table_name
    columnName: column.column_name, // 🎯 column_labels.column_name
    label: column.column_label || column.column_name, // 🎯 column_labels.column_label
    required: column.is_nullable === "N",
    readonly: false,
  };

  // detail_settings JSON 파싱
  const detailSettings = column.detail_settings
    ? JSON.parse(column.detail_settings)
    : {};

  switch (column.web_type) {
    case "text":
      return {
        ...baseWidget,
        type: "text",
        maxLength: detailSettings.maxLength || column.character_maximum_length,
        placeholder: `Enter ${column.column_label || column.column_name}`,
        pattern: detailSettings.pattern,
        validationRules: {
          maxLength: detailSettings.maxLength,
          pattern: detailSettings.pattern,
        },
      };

    case "number":
      return {
        ...baseWidget,
        type: "number",
        min: detailSettings.min,
        max:
          detailSettings.max ||
          (column.numeric_precision
            ? Math.pow(10, column.numeric_precision) - 1
            : undefined),
        step:
          detailSettings.step ||
          (column.numeric_scale > 0 ? Math.pow(10, -column.numeric_scale) : 1),
        validationRules: {
          min: detailSettings.min,
          max: detailSettings.max,
          step: detailSettings.step,
        },
      };

    case "date":
      return {
        ...baseWidget,
        type: "date",
        format: detailSettings.format || "YYYY-MM-DD",
        minDate: detailSettings.minDate,
        maxDate: detailSettings.maxDate,
        validationRules: {
          minDate: detailSettings.minDate,
          maxDate: detailSettings.maxDate,
        },
      };

    case "code":
      return {
        ...baseWidget,
        type: "select",
        options: [], // 코드 카테고리에서 로드
        codeCategory: column.code_category, // 🎯 column_labels.code_category
        multiple: detailSettings.multiple || false,
        searchable: detailSettings.searchable || false,
      };

    case "entity":
      return {
        ...baseWidget,
        type: "entity-select",
        referenceTable: column.reference_table, // 🎯 column_labels.reference_table
        referenceColumn: column.reference_column, // 🎯 column_labels.reference_column
        searchable: detailSettings.searchable || true,
        multiple: detailSettings.multiple || false,
      };

    case "textarea":
      return {
        ...baseWidget,
        type: "textarea",
        rows: detailSettings.rows || 3,
        maxLength: detailSettings.maxLength || column.character_maximum_length,
        validationRules: {
          maxLength: detailSettings.maxLength,
        },
      };

    case "select":
      return {
        ...baseWidget,
        type: "select",
        options: detailSettings.options || [],
        multiple: detailSettings.multiple || false,
        searchable: detailSettings.searchable || false,
      };

    case "checkbox":
      return {
        ...baseWidget,
        type: "checkbox",
        defaultChecked: detailSettings.defaultChecked || false,
        label: detailSettings.label || column.column_label,
      };

    case "radio":
      return {
        ...baseWidget,
        type: "radio",
        options: detailSettings.options || [],
        inline: detailSettings.inline || false,
      };

    case "file":
      return {
        ...baseWidget,
        type: "file",
        accept: detailSettings.accept || "*/*",
        maxSize: detailSettings.maxSize || 10485760, // 10MB
        multiple: detailSettings.multiple || false,
      };

    default:
      return {
        ...baseWidget,
        type: "text",
      };
  }
}

2. 데이터 바인딩

// 위젯과 컬럼 연결
interface DataBinding {
  widgetId: string;
  columnName: string;
  tableName: string;
  bindingType: "input" | "output" | "bidirectional";
  transformFunction?: string; // 데이터 변환 함수
}

// 바인딩 정보 저장
function saveDataBinding(binding: DataBinding): Promise<void> {
  return api.post("/screen-management/bindings", binding);
}

3. 유효성 검증 규칙

// 컬럼 설정에 따른 자동 검증 규칙
function generateValidationRules(column: ColumnInfo): ValidationRule[] {
  const rules: ValidationRule[] = [];

  // 필수 입력 검증
  if (column.isNullable === "N") {
    rules.push({
      type: "required",
      message: `${column.displayName}은(는) 필수 입력 항목입니다.`,
    });
  }

  // 길이 제한 검증
  if (column.maxLength) {
    rules.push({
      type: "maxLength",
      value: column.maxLength,
      message: `${column.displayName}은(는) ${column.maxLength}자 이하여야 합니다.`,
    });
  }

  // 숫자 범위 검증
  if (column.webType === "number") {
    if (column.numericPrecision && column.numericScale) {
      const maxValue =
        Math.pow(10, column.numericPrecision - column.numericScale) - 1;
      rules.push({
        type: "max",
        value: maxValue,
        message: `${column.displayName}은(는) ${maxValue} 이하여야 합니다.`,
      });
    }
  }

  return rules;
}

🌐 API 설계

1. 화면 정의 API

화면 목록 조회 (회사별)

GET /api/screen-management/screens
Query: {
  companyCode?: string; // 회사 코드 (관리자는 생략 가능)
  page?: number;
  size?: number;
}
Response: {
  success: boolean;
  data: ScreenDefinition[];
  total: number;
}

화면 생성 (회사별)

POST /api/screen-management/screens
Body: {
  screenName: string;
  screenCode: string;
  tableName: string;
  companyCode: string; // 사용자 회사 코드 자동 설정
  description?: string;
}

화면 생성

POST /api/screen-management/screens
Body: {
  screenName: string;
  screenCode: string;
  tableName: string;
  description?: string;
}

화면 수정

PUT /api/screen-management/screens/:screenId
Body: {
  screenName?: string;
  description?: string;
  isActive?: boolean;
}

화면 삭제

DELETE /api/screen-management/screens/:screenId

2. 레이아웃 API

레이아웃 조회

GET /api/screen-management/screens/:screenId/layout
Response: {
  success: boolean;
  data: LayoutData;
}

레이아웃 저장

POST /api/screen-management/screens/:screenId/layout
Body: {
  components: ComponentData[];
  gridSettings: GridSettings;
}

레이아웃 복사

POST /api/screen-management/screens/:screenId/layout/copy
Body: {
  targetScreenId: number;
}

3. 위젯 API

위젯 속성 조회

GET /api/screen-management/widgets/:widgetId/properties
Response: {
  success: boolean;
  data: WidgetProperties;
}

위젯 속성 수정

PUT /api/screen-management/widgets/:widgetId/properties
Body: {
  label?: string;
  placeholder?: string;
  required?: boolean;
  readonly?: boolean;
  validationRules?: ValidationRule[];
}

4. 템플릿 API

템플릿 목록 조회 (회사별)

GET /api/screen-management/templates
Query: {
  companyCode?: string; // 회사 코드 (관리자는 생략 가능)
  type?: string;
  isPublic?: boolean;
  createdBy?: string;
}

5. 메뉴 할당 API

화면-메뉴 할당

POST /api/screen-management/screens/:screenId/menu-assignments
Body: {
  menuId: number;
  companyCode: string; // 사용자 회사 코드 자동 설정
  displayOrder?: number;
}

메뉴별 화면 목록 조회

GET /api/screen-management/menus/:menuId/screens
Query: {
  companyCode: string; // 회사 코드 필수
}
Response: {
  success: boolean;
  data: ScreenDefinition[];
}

템플릿 적용

POST /api/screen-management/screens/:screenId/apply-template
Body: {
  templateId: number;
  overrideExisting: boolean;
}

🎭 프론트엔드 구현

1. 화면 설계기 컴포넌트

// ScreenDesigner.tsx
export default function ScreenDesigner() {
  const [layout, setLayout] = useState<LayoutData>({ components: [] });
  const [selectedComponent, setSelectedComponent] = useState<string | null>(
    null
  );
  const [dragState, setDragState] = useState<DragState>({
    isDragging: false,
    draggedItem: null,
    dragSource: "toolbox",
    dropTarget: null,
  });
  const [groupState, setGroupState] = useState<GroupState>({
    isGrouping: false,
    selectedComponents: [],
    groupTarget: null,
    groupMode: "create",
  });
  const [userCompanyCode, setUserCompanyCode] = useState<string>("");

// 컴포넌트 추가 const addComponent = (component: ComponentData) => { setLayout((prev) => ({ ...prev, components: [...prev.components, component], })); };

// 컴포넌트 삭제 const removeComponent = (componentId: string) => { setLayout((prev) => ({ ...prev, components: prev.components.filter((c) => c.id !== componentId), })); };

// 컴포넌트 이동 const moveComponent = (componentId: string, newPosition: Position) => { setLayout((prev) => ({ ...prev, components: prev.components.map((c) => c.id === componentId ? { ...c, position: newPosition } : c ), })); };

// 컴포넌트 그룹화 const groupComponents = (componentIds: string[], groupTitle?: string) => { const groupId = generateId(); const groupComponent: GroupProps = { id: groupId, type: "group", title: groupTitle || "그룹", width: 12, height: 200, padding: 16, margin: 8, backgroundColor: "#f8f9fa", border: "1px solid #dee2e6", borderRadius: 8, shadow: "0 2px 4px rgba(0,0,0,0.1)", collapsible: true, collapsed: false, children: componentIds, };

setLayout((prev) => ({
  ...prev,
  components: [...prev.components, groupComponent],
}));

};

// 그룹에서 컴포넌트 제거 const ungroupComponent = (componentId: string, groupId: string) => { setLayout((prev) => ({ ...prev, components: prev.components.map((c) => { if (c.id === groupId && c.type === "group") { return { ...c, children: c.children.filter((id) => id !== componentId), }; } return c; }), })); };

return (

c.id === selectedComponent)} onPropertyChange={updateComponentProperty} onGroupCreate={groupComponents} onGroupRemove={ungroupComponent} />
); }

### 2. 드래그앤드롭 구현

```typescript
// useDragAndDrop.ts
export function useDragAndDrop() {
  const [dragState, setDragState] = useState<DragState>({
    isDragging: false,
    draggedItem: null,
    dragSource: "toolbox",
    dropTarget: null,
  });

  const startDrag = (item: ComponentData, source: "toolbox" | "canvas") => {
    setDragState({
      isDragging: true,
      draggedItem: item,
      dragSource: source,
      dropTarget: null,
    });
  };

  const endDrag = () => {
    setDragState({
      isDragging: false,
      draggedItem: null,
      dragSource: "toolbox",
      dropTarget: null,
    });
  };

  const updateDropTarget = (targetId: string | null) => {
    setDragState((prev) => ({ ...prev, dropTarget: targetId }));
  };

  return {
    dragState,
    startDrag,
    endDrag,
    updateDropTarget,
  };
}

3. 그리드 시스템

// GridSystem.tsx
export default function GridSystem({ children, columns = 12 }) {
  const gridStyle = {
    display: "grid",
    gridTemplateColumns: `repeat(${columns}, 1fr)`,
    gap: "16px",
    padding: "16px",
  };

  return (
    <div className="grid-system" style={gridStyle}>
      {children}
    </div>
  );
}

// 그룹화 도구 모음
export function GroupingToolbar({
  groupState,
  onGroupStateChange,
  onGroupCreate,
  onGroupRemove,
}) {
  const handleGroupCreate = () => {
    if (groupState.selectedComponents.length > 1) {
      const groupTitle = prompt("그룹 제목을 입력하세요:");
      onGroupCreate(groupState.selectedComponents, groupTitle);
      onGroupStateChange({
        ...groupState,
        isGrouping: false,
        selectedComponents: [],
      });
    }
  };

  const handleGroupRemove = () => {
    if (groupState.groupTarget) {
      onGroupRemove(groupState.selectedComponents[0], groupState.groupTarget);
      onGroupStateChange({
        ...groupState,
        isGrouping: false,
        selectedComponents: [],
      });
    }
  };

  return (
    <div className="grouping-toolbar">
      <button
        onClick={handleGroupCreate}
        disabled={groupState.selectedComponents.length < 2}
        className="btn btn-primary"
      >
        그룹 생성
      </button>
      <button
        onClick={handleGroupRemove}
        disabled={!groupState.groupTarget}
        className="btn btn-secondary"
      >
        그룹 해제
      </button>
    </div>
  );
}

// GridItem.tsx
interface GridItemProps {
  width: number; // 1-12
  offset?: number;
  children: React.ReactNode;
}

export default function GridItem({
  width,
  offset = 0,
  children,
}: GridItemProps) {
  const gridColumn =
    offset > 0 ? `${offset + 1} / span ${width}` : `span ${width}`;

  const style = {
    gridColumn,
    minHeight: "50px",
  };

  return (
    <div className="grid-item" style={style}>
      {children}
    </div>
  );
}

⚙️ 백엔드 구현

1. 화면 관리 서비스

// screenManagementService.ts
export class ScreenManagementService {
  // 화면 정의 생성 (회사별)
  async createScreen(
    screenData: CreateScreenRequest,
    userCompanyCode: string
  ): Promise<ScreenDefinition> {
    // 권한 검증: 사용자 회사 코드 확인
    if (userCompanyCode !== "*" && userCompanyCode !== screenData.companyCode) {
      throw new Error("해당 회사의 화면을 생성할 권한이 없습니다.");
    }

    const screen = await prisma.screen_definitions.create({
      data: {
        screen_name: screenData.screenName,
        screen_code: screenData.screenCode,
        table_name: screenData.tableName,
        company_code: screenData.companyCode,
        description: screenData.description,
        created_by: screenData.createdBy,
      },
    });

    return this.mapToScreenDefinition(screen);
  }

  // 회사별 화면 목록 조회
  async getScreensByCompany(
    companyCode: string,
    page: number = 1,
    size: number = 20
  ): Promise<{ screens: ScreenDefinition[]; total: number }> {
    const whereClause = companyCode === "*" ? {} : { company_code: companyCode };

    const [screens, total] = await Promise.all([
      prisma.screen_definitions.findMany({
        where: whereClause,
        skip: (page - 1) * size,
        take: size,
        orderBy: { created_date: "desc" },
      }),
      prisma.screen_definitions.count({ where: whereClause }),
    ]);

    return {
      screens: screens.map(this.mapToScreenDefinition),
      total,
    };


  // 레이아웃 저장
  async saveLayout(screenId: number, layoutData: LayoutData): Promise<void> {
    // 기존 레이아웃 삭제
    await prisma.screen_layouts.deleteMany({
      where: { screen_id: screenId },
    });

    // 새 레이아웃 저장
    const layoutPromises = layoutData.components.map((component) =>
      prisma.screen_layouts.create({
        data: {
          screen_id: screenId,
          component_type: component.type,
          component_id: component.id,
          parent_id: component.parentId,
          position_x: component.position.x,
          position_y: component.position.y,
          width: component.width,
          height: component.height,
          properties: component.properties,
          display_order: component.displayOrder,
        },
      })
    );

    await Promise.all(layoutPromises);
  }

  // 화면 미리보기 생성
  async generatePreview(screenId: number): Promise<PreviewData> {
    const screen = await prisma.screen_definitions.findUnique({
      where: { screen_id: screenId },
      include: {
        layouts: {
          include: { widgets: true },
          orderBy: { display_order: "asc" },
        },
      },
    });

    if (!screen) {
      throw new Error("Screen not found");
    }

    return this.buildPreviewData(screen);
  }

  private buildPreviewData(screen: any): PreviewData {
    // 레이아웃을 트리 구조로 변환
    const componentTree = this.buildComponentTree(screen.layouts);

    // 위젯 데이터 바인딩
    const boundWidgets = this.bindWidgetData(componentTree, screen.table_name);

    return {
      screenId: screen.screen_id,
      screenName: screen.screen_name,
      tableName: screen.table_name,
      components: boundWidgets,
      metadata: this.getTableMetadata(screen.table_name),
    };
  }
}

2. 테이블 타입 연계 서비스

// tableTypeIntegrationService.ts
export class TableTypeIntegrationService {
  // 컬럼 정보 조회 (웹 타입 포함) - 현재 테이블 구조 기반
  async getColumnInfo(tableName: string): Promise<ColumnInfo[]> {
    const columns = await prisma.$queryRaw`
      SELECT 
        c.column_name,
        COALESCE(cl.column_label, c.column_name) as column_label,
        c.data_type,
        COALESCE(cl.web_type, 'text') as web_type,
        c.is_nullable,
        c.column_default,
        c.character_maximum_length,
        c.numeric_precision,
        c.numeric_scale,
        cl.detail_settings,                    -- 🎯 column_labels.detail_settings
        cl.code_category,                      -- 🎯 column_labels.code_category
        cl.reference_table,                    -- 🎯 column_labels.reference_table
        cl.reference_column,                   -- 🎯 column_labels.reference_column
        cl.is_visible,                         -- 🎯 column_labels.is_visible
        cl.display_order,                      -- 🎯 column_labels.display_order
        cl.description                         -- 🎯 column_labels.description
      FROM information_schema.columns c
      LEFT JOIN column_labels cl ON c.table_name = cl.table_name 
        AND c.column_name = cl.column_name
      WHERE c.table_name = ${tableName}
      ORDER BY COALESCE(cl.display_order, c.ordinal_position)
    `;

    return columns as ColumnInfo[];
  }

  // 웹 타입 설정 - 현재 테이블 구조 기반
  async setColumnWebType(
    tableName: string,
    columnName: string,
    webType: string,
    additionalSettings?: any
  ): Promise<void> {
    await prisma.column_labels.upsert({
      where: {
        table_name_column_name: {
          table_name: tableName,
          column_name: columnName,
        },
      },
      update: {
        web_type: webType,
        column_label: additionalSettings?.columnLabel,
        detail_settings: additionalSettings?.detailSettings
          ? JSON.stringify(additionalSettings.detailSettings)
          : null,
        code_category: additionalSettings?.codeCategory,
        reference_table: additionalSettings?.referenceTable,
        reference_column: additionalSettings?.referenceColumn,
        is_visible: additionalSettings?.isVisible ?? true,
        display_order: additionalSettings?.displayOrder ?? 0,
        description: additionalSettings?.description,
        updated_date: new Date(),
      },
      create: {
        table_name: tableName,
        column_name: columnName,
        column_label: additionalSettings?.columnLabel,
        web_type: webType,
        detail_settings: additionalSettings?.detailSettings
          ? JSON.stringify(additionalSettings.detailSettings)
          : null,
        code_category: additionalSettings?.codeCategory,
        reference_table: additionalSettings?.referenceTable,
        reference_column: additionalSettings?.referenceColumn,
        is_visible: additionalSettings?.isVisible ?? true,
        display_order: additionalSettings?.displayOrder ?? 0,
        description: additionalSettings?.description,
        created_date: new Date(),
      },
    });
  }

  // 웹 타입 조회 - 현재 테이블 구조 기반
  async getColumnWebType(
    tableName: string,
    columnName: string
  ): Promise<ColumnWebTypeSetting> {
    const columnLabel = await prisma.column_labels.findUnique({
      where: {
        table_name_column_name: {
          table_name: tableName,
          column_name: columnName,
        },
      },
    });

    if (!columnLabel) {
      return {
        tableName,
        columnName,
        webType: "text", // 기본값
        columnLabel: columnName,
        detailSettings: {},
        codeCategory: null,
        referenceTable: null,
        referenceColumn: null,
        isVisible: true,
        displayOrder: 0,
        description: null,
      };
    }

    return {
      tableName,
      columnName,
      webType: columnLabel.web_type || "text",
      columnLabel: columnLabel.column_label,
      detailSettings: columnLabel.detail_settings
        ? JSON.parse(columnLabel.detail_settings)
        : {},
      codeCategory: columnLabel.code_category,
      referenceTable: columnLabel.reference_table,
      referenceColumn: columnLabel.reference_column,
      isVisible: columnLabel.is_visible ?? true,
      displayOrder: columnLabel.display_order ?? 0,
      description: columnLabel.description,
    };
  }

  // 웹 타입별 위젯 생성 - 현재 테이블 구조 기반
  generateWidgetFromColumn(column: ColumnInfo): WidgetData {
    const baseWidget = {
      id: generateId(),
      tableName: column.table_name, // 🎯 column_labels.table_name
      columnName: column.column_name, // 🎯 column_labels.column_name
      label: column.column_label || column.column_name, // 🎯 column_labels.column_label
      required: column.is_nullable === "N",
      readonly: false,
    };

    // detail_settings JSON 파싱
    const detailSettings = column.detail_settings
      ? JSON.parse(column.detail_settings)
      : {};

    switch (column.web_type) {
      case "text":
        return {
          ...baseWidget,
          type: "text",
          maxLength:
            detailSettings.maxLength || column.character_maximum_length,
          placeholder: `Enter ${column.column_label || column.column_name}`,
          pattern: detailSettings.pattern,
        };

      case "number":
        return {
          ...baseWidget,
          type: "number",
          min: detailSettings.min,
          max:
            detailSettings.max ||
            (column.numeric_precision
              ? Math.pow(10, column.numeric_precision) - 1
              : undefined),
          step:
            detailSettings.step ||
            (column.numeric_scale > 0
              ? Math.pow(10, -column.numeric_scale)
              : 1),
        };

      case "date":
        return {
          ...baseWidget,
          type: "date",
          format: detailSettings.format || "YYYY-MM-DD",
          minDate: detailSettings.minDate,
          maxDate: detailSettings.maxDate,
        };

      case "code":
        return {
          ...baseWidget,
          type: "select",
          options: [], // 코드 카테고리에서 로드
          codeCategory: column.code_category, // 🎯 column_labels.code_category
          multiple: detailSettings.multiple || false,
          searchable: detailSettings.searchable || false,
        };

      case "entity":
        return {
          ...baseWidget,
          type: "entity-select",
          referenceTable: column.reference_table, // 🎯 column_labels.reference_table
          referenceColumn: column.reference_column, // 🎯 column_labels.reference_column
          searchable: detailSettings.searchable || true,
          multiple: detailSettings.multiple || false,
        };

      case "textarea":
        return {
          ...baseWidget,
          type: "textarea",
          rows: detailSettings.rows || 3,
          maxLength:
            detailSettings.maxLength || column.character_maximum_length,
        };

      case "select":
        return {
          ...baseWidget,
          type: "select",
          options: detailSettings.options || [],
          multiple: detailSettings.multiple || false,
          searchable: detailSettings.searchable || false,
        };

      case "checkbox":
        return {
          ...baseWidget,
          type: "checkbox",
          defaultChecked: detailSettings.defaultChecked || false,
          label: detailSettings.label || column.column_label,
        };

      case "radio":
        return {
          ...baseWidget,
          type: "radio",
          options: detailSettings.options || [],
          inline: detailSettings.inline || false,
        };

      case "file":
        return {
          ...baseWidget,
          type: "file",
          accept: detailSettings.accept || "*/*",
          maxSize: detailSettings.maxSize || 10485760, // 10MB
          multiple: detailSettings.multiple || false,
        };

      default:
        return {
          ...baseWidget,
          type: "text",
        };
    }
  }

  // 코드 카테고리 옵션 로드
  async loadCodeOptions(codeCategory: string): Promise<CodeOption[]> {
    const codes = await prisma.code_info.findMany({
      where: { code_category: codeCategory, is_active: "Y" },
      select: { code_value: true, code_name: true },
      orderBy: { sort_order: "asc" },
    });

    return codes.map((code) => ({
      value: code.code_value,
      label: code.code_name,
    }));
  }

  // 참조 테이블 옵션 로드
  async loadReferenceOptions(
    referenceTable: string
  ): Promise<ReferenceOption[]> {
    const records = await prisma.$queryRaw`
      SELECT DISTINCT ${referenceTable}.id, ${referenceTable}.name
      FROM ${referenceTable}
      ORDER BY ${referenceTable}.name
    `;

    return records.map((record: any) => ({
      value: record.id,
      label: record.name,
    }));
  }
}

🎬 사용 시나리오

1. 회사별 화면 관리

일반 사용자 (회사 코드: 'COMP001')

  1. 로그인: 자신의 회사 코드로 시스템 로그인
  2. 화면 목록 조회: 자신이 속한 회사의 화면만 표시
  3. 화면 생성: 회사 코드가 자동으로 설정되어 생성
  4. 메뉴 할당: 자신의 회사 메뉴에만 화면 할당 가능

관리자 (회사 코드: '*')

  1. 로그인: 관리자 권한으로 시스템 로그인
  2. 전체 화면 조회: 모든 회사의 화면을 조회/수정 가능
  3. 회사별 화면 관리: 각 회사별로 화면 생성/수정/삭제
  4. 크로스 회사 메뉴 할당: 모든 회사의 메뉴에 화면 할당 가능

2. 웹 타입 설정 (테이블 타입관리)

  1. 테이블 선택: 테이블 타입관리에서 웹 타입을 설정할 테이블 선택
  2. 컬럼 관리: 해당 테이블의 컬럼 목록에서 웹 타입을 설정할 컬럼 선택
  3. 웹 타입 선택: 컬럼의 용도에 맞는 웹 타입 선택 (text, number, date, code, entity 등)
  4. 추가 설정: 웹 타입별 필요한 추가 설정 구성
    • code 타입: 공통코드 카테고리 선택
    • entity 타입: 참조 테이블 및 컬럼 지정
    • validation: 유효성 검증 규칙 설정
    • display: 표시 속성 설정
  5. 저장: 웹 타입 설정을 데이터베이스에 저장
  6. 연계 확인: 화면관리 시스템에서 자동 위젯 생성 확인

3. 새로운 화면 설계

  1. 테이블 선택: 테이블 타입관리에서 설계할 테이블 선택
  2. 웹 타입 확인: 각 컬럼의 웹 타입 설정 상태 확인
  3. 화면 생성: 화면명과 코드를 입력하여 새 화면 생성 (회사 코드 자동 설정)
  4. 자동 위젯 생성: 컬럼의 웹 타입에 따라 자동으로 위젯 생성
  5. 컴포넌트 배치: 드래그앤드롭으로 컴포넌트를 캔버스에 배치
  6. 컨테이너 그룹화: 관련 컴포넌트들을 그룹으로 묶어 깔끔하게 정렬
  7. 속성 설정: 각 컴포넌트의 속성을 Properties 패널에서 설정
  8. 실시간 미리보기: 설계한 화면을 실제 화면과 동일하게 확인
  9. 저장: 완성된 화면 레이아웃을 데이터베이스에 저장

4. 기존 화면 수정

  1. 화면 선택: 수정할 화면을 목록에서 선택 (권한 확인)
  2. 레이아웃 로드: 기존 레이아웃을 캔버스에 로드
  3. 컴포넌트 수정: 컴포넌트 추가/삭제/이동/수정
  4. 그룹 구조 조정: 컴포넌트 그룹화/그룹 해제/그룹 속성 변경
  5. 속성 변경: 컴포넌트 속성 변경
  6. 변경사항 확인: 실시간 미리보기로 변경사항 확인
  7. 저장: 수정된 레이아웃 저장

5. 템플릿 활용

  1. 템플릿 선택: 적합한 템플릿을 목록에서 선택 (회사별 템플릿)
  2. 템플릿 적용: 선택한 템플릿을 현재 화면에 적용
  3. 커스터마이징: 템플릿을 기반으로 필요한 부분 수정
  4. 저장: 커스터마이징된 화면 저장

6. 메뉴 할당 및 관리

  1. 메뉴 선택: 화면을 할당할 메뉴 선택 (회사별 메뉴만 표시)
  2. 화면 할당: 선택한 화면을 메뉴에 할당
  3. 할당 순서 조정: 메뉴 내 화면 표시 순서 조정
  4. 할당 해제: 메뉴에서 화면 할당 해제
  5. 권한 확인: 메뉴 할당 시 회사 코드 일치 여부 확인

7. 화면 배포

  1. 화면 활성화: 설계 완료된 화면을 활성 상태로 변경
  2. 권한 설정: 화면 접근 권한 설정 (회사별 권한)
  3. 메뉴 연결: 메뉴 시스템에 화면 연결 (회사별 메뉴)
  4. 테스트: 실제 환경에서 화면 동작 테스트
  5. 배포: 운영 환경에 화면 배포

📅 개발 계획

Phase 1: 기본 구조 및 데이터베이스 (2주)

  • 데이터베이스 스키마 설계 및 생성
  • 기본 API 구조 설계
  • 화면 정의 및 레이아웃 테이블 생성
  • 기본 CRUD API 구현

Phase 2: 드래그앤드롭 핵심 기능 (3주)

  • 드래그앤드롭 라이브러리 선택 및 구현
  • 그리드 시스템 구현
  • 컴포넌트 배치 및 이동 로직 구현
  • 컴포넌트 크기 조정 기능 구현

Phase 3: 컴포넌트 라이브러리 (2주)

  • 기본 입력 컴포넌트 구현
  • 선택 컴포넌트 구현
  • 표시 컴포넌트 구현
  • 레이아웃 컴포넌트 구현

Phase 4: 테이블 타입 연계 (2주)

  • 테이블 타입관리와 연계 API 구현
  • 웹 타입 설정 및 관리 기능 구현
  • 웹 타입별 추가 설정 관리 기능 구현
  • 자동 위젯 생성 로직 구현
  • 데이터 바인딩 시스템 구현
  • 유효성 검증 규칙 자동 적용

Phase 5: 미리보기 및 템플릿 (2주)

  • 실시간 미리보기 시스템 구현
  • 기본 템플릿 구현
  • 템플릿 저장 및 적용 기능 구현
  • 템플릿 공유 시스템 구현

Phase 6: 통합 및 테스트 (1주)

  • 전체 시스템 통합 테스트
  • 성능 최적화
  • 사용자 테스트 및 피드백 반영
  • 문서화 및 사용자 가이드 작성

🎯 결론

화면관리 시스템은 회사별 권한 관리테이블 타입관리 연계를 통해 사용자가 직관적으로 웹 화면을 설계할 수 있는 강력한 도구입니다.

🏢 회사별 화면 관리의 핵심 가치

  • 권한 격리: 사용자는 자신이 속한 회사의 화면만 제작/수정 가능
  • 관리자 통제: 회사 코드 '*'인 관리자는 모든 회사의 화면을 제어
  • 메뉴 연동: 각 회사의 메뉴에만 화면 할당하여 완벽한 데이터 분리

🎨 향상된 사용자 경험

  • 드래그앤드롭 인터페이스: 직관적인 화면 설계
  • 컨테이너 그룹화: 컴포넌트를 깔끔하게 정렬하는 그룹 기능
  • 실시간 미리보기: 설계한 화면을 실제 화면과 동일하게 확인
  • 자동 위젯 생성: 컬럼의 웹 타입에 따른 스마트한 위젯 생성

🚀 기술적 혜택

  • 기존 테이블 구조 100% 호환: 별도 스키마 변경 없이 바로 개발 가능
  • 권한 기반 보안: 회사 간 데이터 완전 격리
  • 확장 가능한 아키텍처: 새로운 웹 타입과 컴포넌트 쉽게 추가

이 시스템을 통해 ERP 시스템의 화면 개발 생산성을 크게 향상시키고, 회사별 맞춤형 화면 구성사용자 요구사항에 따른 빠른 화면 구성이 가능해질 것입니다.