# 화면관리 시스템 설계문서

## 1. 개요

### 1.1 목적

ERP 시스템에서 사용자가 직관적인 드래그앤드롭 인터페이스를 통해 동적으로 화면을 설계하고 관리할 수 있는 시스템

### 1.2 주요 기능

- 드래그앤드롭 기반 화면 설계
- 실시간 미리보기 및 속성 편집
- 다양한 위젯 타입 지원
- 데이터베이스 테이블/컬럼과의 연동
- 템플릿 기반 빠른 화면 생성
- 스타일 및 레이아웃 커스터마이징

## 2. 시스템 아키텍처

### 2.1 전체 구조

```
화면 편집기 (ScreenDesigner)
├── 템플릿 패널 (TemplatesPanel)
├── 테이블 패널 (TablesPanel)
├── 속성 편집 패널 (PropertiesPanel)
├── 스타일 편집 패널 (StyleEditor)
├── 상세설정 패널 (DetailSettingsPanel)
├── 격자 설정 패널 (GridPanel)
└── 캔버스 영역 (RealtimePreview)

할당된 화면 (InteractiveScreenViewer)
├── 라벨 렌더링
├── 위젯 렌더링
└── 폼 데이터 관리
```

### 2.2 데이터 흐름

```
사용자 입력 → 컴포넌트 상태 → 레이아웃 데이터 → API 저장/불러오기 → 할당된 화면 렌더링
```

## 3. 컴포넌트 구조

### 3.1 컴포넌트 타입

```typescript
type ComponentType = "container" | "widget" | "group" | "datatable";

interface BaseComponent {
  id: string;
  type: ComponentType;
  position: { x: number; y: number; z?: number };
  size: { width: number; height: number };
  parentId?: string;
  label?: string;
  required?: boolean;
  readonly?: boolean;
  style?: ComponentStyle;
}

interface WidgetComponent extends BaseComponent {
  type: "widget";
  widgetType: WebType;
  placeholder?: string;
  columnName?: string;
  webTypeConfig?: WebTypeConfig;
}
```

### 3.2 지원하는 웹 타입

- **텍스트 입력**: text, email, tel
- **숫자 입력**: number, decimal
- **날짜/시간**: date, datetime
- **선택**: select, dropdown, radio
- **체크박스**: checkbox, boolean
- **텍스트 영역**: textarea
- **파일**: file
- **코드**: code
- **엔티티**: entity
- **버튼**: button

## 4. 주요 기능 상세

### 4.1 드래그앤드롭 시스템

#### 템플릿 드래그

- 사전 정의된 템플릿을 캔버스에 드롭
- 컨테이너와 자식 컴포넌트 관계 자동 설정
- 격자 스냅 및 자동 크기 조정

#### 컬럼 드래그

- 데이터베이스 테이블의 컬럼을 위젯으로 변환
- 컬럼 타입에 따른 자동 웹타입 매핑
- 폼 컨테이너에 드롭 시 자동 부모-자식 관계 설정

#### 다중 컴포넌트 드래그

- Ctrl/Cmd + 클릭으로 다중 선택
- 선택된 모든 컴포넌트 동시 이동
- 실시간 미리보기 제공

### 4.2 속성 편집 시스템

#### 실시간 속성 편집 패턴

```typescript
// 로컬 상태 기반 즉시 반영
const [localInputs, setLocalInputs] = useState({
  title: component.title || "",
  placeholder: component.placeholder || "",
});

// 입력과 동시에 업데이트
<Input
  value={localInputs.title}
  onChange={(e) => {
    const newValue = e.target.value;
    setLocalInputs(prev => ({ ...prev, title: newValue }));
    onUpdateProperty("title", newValue);
  }}
/>
```

#### 컴포넌트별 개별 상태 관리

```typescript
// 동적 ID 기반 상태 관리
const [localColumnInputs, setLocalColumnInputs] = useState<Record<string, string>>({});

// 컴포넌트 변경 시 기존 값 보존하면서 새 항목만 추가
useEffect(() => {
  setLocalColumnInputs((prev) => {
    const newInputs = { ...prev };
    component.columns?.forEach((col) => {
      if (!(col.id in newInputs)) {
        newInputs[col.id] = col.label;
      }
    });
    return newInputs;
  });
}, [component.columns]);
```

### 4.3 스타일 시스템

#### 스타일 적용 계층

1. **컴포넌트 기본 스타일**: `component.style`
2. **웹타입 설정 스타일**: `webTypeConfig`에서 정의
3. **라벨 스타일**: 별도 관리 (`labelColor`, `labelFontSize` 등)

#### 스타일 패널 구성

- **여백**: margin, padding, gap
- **테두리**: borderWidth, borderStyle, borderColor, borderRadius
- **배경**: backgroundColor, backgroundImage
- **텍스트**: color, fontSize, fontWeight, textAlign

### 4.4 템플릿 시스템

#### 데이터 테이블 템플릿

```typescript
{
  id: "data-table",
  name: "데이터 테이블",
  category: "table",
  components: [
    {
      id: "table-container",
      type: "datatable",
      searchFilters: [],
      columns: [
        { id: "col1", label: "컬럼 1", visible: true, sortable: true },
        { id: "col2", label: "컬럼 2", visible: true, sortable: false }
      ],
      pagination: { enabled: true, pageSize: 10 },
      actions: {
        create: { enabled: true, label: "추가" },
        edit: { enabled: true, label: "수정" },
        delete: { enabled: true, label: "삭제" }
      }
    }
  ]
}
```

#### 입력 폼 템플릿

```typescript
{
  id: "input-form",
  name: "입력 폼",
  category: "form",
  components: [
    {
      id: "form-container",
      type: "container",
      style: { backgroundColor: "#f8f9fa", borderRadius: "8px" },
      children: [
        {
          id: "save-button",
          type: "widget",
          widgetType: "button",
          parentId: "form-container",
          position: { x: 0, y: 0 },
          style: { position: "absolute", bottom: "24px", right: "104px" }
        },
        {
          id: "cancel-button",
          type: "widget",
          widgetType: "button",
          parentId: "form-container",
          position: { x: 0, y: 0 },
          style: { position: "absolute", bottom: "24px", right: "24px" }
        }
      ]
    }
  ]
}
```

#### 범용 버튼 템플릿

```typescript
{
  id: "universal-button",
  name: "버튼",
  category: "button",
  components: [
    {
      id: "button",
      type: "widget",
      widgetType: "button",
      webTypeConfig: {
        actionType: "save",
        variant: "default",
        size: "sm"
      }
    }
  ]
}
```

### 4.5 웹타입별 상세 설정

#### 버튼 설정 (ButtonConfigPanel)

```typescript
interface ButtonTypeConfig {
  actionType: ButtonActionType;
  variant: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
  size: "default" | "sm" | "lg" | "icon";
  icon?: string;
  confirmMessage?: string;
  popupTitle?: string;
  popupContent?: string;
  popupSize?: "sm" | "md" | "lg";
  navigateUrl?: string;
  navigateTarget?: "_self" | "_blank";
  customAction?: string;
  backgroundColor?: string;
  textColor?: string;
  borderColor?: string;
}

type ButtonActionType =
  | "save"
  | "cancel"
  | "delete"
  | "edit"
  | "add"
  | "search"
  | "reset"
  | "submit"
  | "close"
  | "popup"
  | "navigate"
  | "custom";
```

#### 텍스트 설정 (TextTypeConfig)

```typescript
interface TextTypeConfig {
  format: "none" | "korean" | "english" | "alphanumeric" | "numeric" | "email" | "phone" | "url";
  minLength?: number;
  maxLength?: number;
  pattern?: string;
  placeholder?: string;
  multiline?: boolean;
}
```

#### 숫자 설정 (NumberTypeConfig)

```typescript
interface NumberTypeConfig {
  min?: number;
  max?: number;
  step?: number;
  format?: "integer" | "decimal" | "currency" | "percentage";
  decimalPlaces?: number;
  thousandSeparator?: boolean;
}
```

#### 날짜 설정 (DateTypeConfig)

```typescript
interface DateTypeConfig {
  format: "YYYY-MM-DD" | "YYYY-MM-DD HH:mm" | "YYYY-MM-DD HH:mm:ss";
  showTime: boolean;
  minDate?: string;
  maxDate?: string;
  defaultValue?: string;
}
```

#### 선택박스 설정 (SelectTypeConfig)

```typescript
interface SelectTypeConfig {
  options: Array<{ label: string; value: string }>;
  multiple?: boolean;
  searchable?: boolean;
  placeholder?: string;
}
```

#### 엔티티 설정 (EntityTypeConfig)

```typescript
interface EntityTypeConfig {
  entityName: string;
  displayField: string;
  valueField: string;
  filters: Array<{ field: string; operator: string; value: any }>;
  multiple: boolean;
  searchable: boolean;
  allowClear: boolean;
  placeholder?: string;
  displayFormat?: string;
  defaultValue?: any;
}
```

### 4.6 격자 시스템

#### 격자 설정

```typescript
interface GridSettings {
  columns: number; // 격자 컬럼 수
  gap: number; // 격자 간격 (px)
  padding: number; // 캔버스 패딩 (px)
  snapToGrid: boolean; // 격자 스냅 활성화
  showGrid: boolean; // 격자 표시 여부
  gridColor: string; // 격자 선 색상
  gridOpacity: number; // 격자 투명도 (0-1)
}
```

#### 격자 스냅 로직

```typescript
const snapToGrid = (value: number, gridSize: number): number => {
  return Math.round(value / gridSize) * gridSize;
};

const snapSizeToGrid = (size: number, gridSize: number): number => {
  return Math.max(gridSize, Math.round(size / gridSize) * gridSize);
};
```

## 5. 렌더링 시스템

### 5.1 편집기 렌더링 (ScreenDesigner)

#### 컴포넌트 위치 계산

```typescript
// 절대 위치 래퍼 div
<div
  className="absolute"
  style={{
    left: `${displayComponent.position.x}px`,
    top: `${displayComponent.position.y}px`,
    width: displayComponent.style?.width || `${displayComponent.size.width}px`,
    height: displayComponent.style?.height || `${displayComponent.size.height}px`,
    zIndex: displayComponent.position.z || 1,
  }}
>
  <RealtimePreview
    component={displayComponent}
    position={{ x: 0, y: 0 }} // 래퍼 div 내에서는 상대 위치
  />
</div>
```

#### 자식 컴포넌트 상대 위치

```typescript
// 자식 컴포넌트는 부모 기준 상대 위치로 계산
const relativePosition = {
  x: child.position.x - parent.position.x,
  y: child.position.y - parent.position.y,
};
```

### 5.2 할당된 화면 렌더링 (InteractiveScreenViewer)

#### 라벨 외부 분리 렌더링

```typescript
// 라벨을 컴포넌트 외부에 별도 렌더링 (높이에 영향 없음)
{shouldShowLabel && (
  <div
    style={{
      position: "absolute",
      left: `${component.position.x}px`,
      top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 배치
      zIndex: (component.position.z || 1) + 1,
      ...labelStyle,
    }}
  >
    {labelText}
    {component.required && <span style={{ color: "#f97316" }}>*</span>}
  </div>
)}

// 실제 컴포넌트 (라벨 높이에 영향받지 않음)
<div style={{ height: component.style?.height || `${component.size.height}px` }}>
  <InteractiveScreenViewer component={component} hideLabel={true} />
</div>
```

#### 스타일 적용 시스템

```typescript
const applyStyles = (element: React.ReactElement) => {
  if (!comp.style) return element;

  return React.cloneElement(element, {
    style: {
      ...element.props.style, // 기존 스타일 유지
      ...comp.style, // 컴포넌트 스타일 적용
      width: "100%", // 부모 컨테이너에 맞춤
      height: "100%",
      minHeight: "100%", // 강제 높이 적용
      maxHeight: "100%",
      boxSizing: "border-box",
    },
  });
};
```

#### 위젯별 렌더링

```typescript
switch (widgetType) {
  case "text":
  case "email":
  case "tel":
    return applyStyles(
      <Input
        type={inputType}
        placeholder={finalPlaceholder}
        value={currentValue}
        onChange={handleInputChange}
        className="w-full"
        style={{ height: "100%" }}
      />
    );

  case "button":
    const config = widget.webTypeConfig as ButtonTypeConfig;
    return (
      <Button
        onClick={handleButtonClick}
        size={config?.size || "sm"}
        variant={config?.variant || "default"}
        className="w-full"
        style={{
          ...comp.style,
          height: "100%",
          backgroundColor: config?.backgroundColor,
          color: config?.textColor,
          borderColor: config?.borderColor,
        }}
      >
        {label || "버튼"}
      </Button>
    );

  case "entity":
    return (
      <Select>
        <SelectTrigger
          className="w-full"
          style={{
            ...comp.style,
            height: "100%",
          }}
        >
          <SelectValue placeholder={finalPlaceholder} />
        </SelectTrigger>
        <SelectContent>
          {options.map(option => (
            <SelectItem key={option.value} value={option.value}>
              {option.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    );
}
```

## 6. 상태 관리

### 6.1 레이아웃 상태

```typescript
interface LayoutData {
  components: ComponentData[];
  gridSettings: GridSettings;
}

const [layout, setLayout] = useState<LayoutData>({
  components: [],
  gridSettings: defaultGridSettings,
});
```

### 6.2 선택 상태

```typescript
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
const [selectedComponents, setSelectedComponents] = useState<ComponentData[]>([]);
```

### 6.3 드래그 상태

```typescript
interface DragState {
  isDragging: boolean;
  draggedComponents: ComponentData[];
  startPosition: { x: number; y: number };
  currentPosition: { x: number; y: number };
}
```

### 6.4 패널 상태

```typescript
interface PanelState {
  isOpen: boolean;
  position: { x: number; y: number };
  size: { width: number; height: number };
}

const [panelStates, setPanelStates] = useState<Record<string, PanelState>>({
  templates: { isOpen: true, position: { x: 0, y: 0 }, size: { width: 300, height: 400 } },
  properties: { isOpen: false, position: { x: 0, y: 0 }, size: { width: 360, height: 600 } },
  styles: { isOpen: false, position: { x: 0, y: 0 }, size: { width: 360, height: 400 } },
  // ...
});
```

## 7. API 연동

### 7.1 화면 정보 API

```typescript
// 화면 목록 조회
GET /api/screens
Response: {
  screens: Array<{
    id: number;
    name: string;
    description: string;
    createdAt: string;
    updatedAt: string;
  }>
}

// 화면 상세 조회
GET /api/screens/:id
Response: {
  id: number;
  name: string;
  description: string;
  layout: LayoutData;
}

// 화면 저장
POST /api/screens/:id/layout
Request: {
  layout: LayoutData
}
```

### 7.2 테이블 정보 API

```typescript
// 테이블 목록 조회
GET / api / tables;
Response: {
  tables: Array<{
    id: string;
    name: string;
    description: string;
    columns: Array<{
      id: string;
      name: string;
      type: string;
      nullable: boolean;
      primaryKey: boolean;
    }>;
  }>;
}
```

## 8. 성능 최적화

### 8.1 렌더링 최적화

- `useCallback`으로 이벤트 핸들러 메모이제이션
- `useMemo`로 계산 비용이 큰 값 캐싱
- 컴포넌트 분할을 통한 불필요한 리렌더링 방지

### 8.2 상태 최적화

- 로컬 상태 기반 즉시 반영으로 UI 응답성 향상
- 디바운싱을 통한 과도한 API 호출 방지
- 컴포넌트별 개별 상태 관리로 전역 상태 오염 방지

## 9. 개발 가이드

### 9.1 새로운 웹타입 추가 (간편한 3단계)

새로운 웹타입 추가가 대폭 간편해졌습니다! 이제 **데이터베이스 기반 동적 웹타입 시스템**을 사용합니다.

#### **1단계: 데이터베이스에 웹타입 등록**

```sql
-- web_type_standard 테이블에 새 웹타입 추가
INSERT INTO web_type_standard (
  web_type,
  type_name,
  config_panel,
  active
) VALUES (
  'my_new_type',           -- 웹타입 코드 (영문)
  '새로운 입력 타입',       -- 한글 표시명
  'MyNewTypeConfigPanel',  -- 설정 패널 컴포넌트명 (선택사항)
  'Y'                      -- 활성화 여부
);
```

#### **2단계: 설정 패널 컴포넌트 생성 (선택사항)**

웹타입에 특별한 설정이 필요한 경우만 생성:

```typescript
// frontend/components/screen/config-panels/MyNewTypeConfigPanel.tsx
export const MyNewTypeConfigPanel = ({ config, onConfigChange }) => {
  return (
    <div className="space-y-4">
      <h3 className="text-sm font-medium">새로운 타입 설정</h3>

      {/* 설정 UI */}
      <div>
        <label className="text-sm">옵션 1</label>
        <input
          value={config.option1 || ""}
          onChange={(e) => onConfigChange({ ...config, option1: e.target.value })}
        />
      </div>

      <div>
        <label className="text-sm">옵션 2</label>
        <select
          value={config.option2 || "default"}
          onChange={(e) => onConfigChange({ ...config, option2: e.target.value })}
        >
          <option value="default">기본값</option>
          <option value="custom">사용자 정의</option>
        </select>
      </div>
    </div>
  );
};
```

#### **3단계: 레지스트리에 등록**

```typescript
// frontend/lib/utils/availableConfigPanels.ts
import { MyNewTypeConfigPanel } from "@/components/screen/config-panels/MyNewTypeConfigPanel";

export const availableConfigPanels = {
  // 기존 패널들...
  ButtonConfigPanel,
  TextTypeConfigPanel,
  NumberTypeConfigPanel,

  // 새 패널 추가
  MyNewTypeConfigPanel, // ← 이 한 줄만 추가!
};
```

#### **완료! 🎉**

- ✅ **PropertiesPanel**: 자동으로 드롭다운에 "새로운 입력 타입" 표시
- ✅ **DetailSettingsPanel**: 위젯 타입 변경 시 자동으로 설정 패널 표시
- ✅ **실시간 업데이트**: React key props로 즉시 반영

#### **설정 패널이 없는 경우**

config_panel을 NULL로 설정하거나 레지스트리에 등록하지 않으면 "기본 설정" 메시지가 표시됩니다.

```sql
-- 설정 패널 없는 간단한 웹타입
INSERT INTO web_type_standard (web_type, type_name, active)
VALUES ('simple_type', '간단한 타입', 'Y');
```

#### **렌더링 로직 추가 (필요한 경우)**

새 웹타입이 특별한 렌더링이 필요한 경우에만 추가:

```typescript
// RealtimePreviewDynamic.tsx 또는 InteractiveScreenViewer.tsx
case "my_new_type":
  return (
    <MyNewInputComponent
      value={currentValue}
      onChange={handleInputChange}
      placeholder={finalPlaceholder}
      config={widget.webTypeConfig}
      style={{ height: "100%" }}
    />
  );
```

#### **타입 정의 추가 (TypeScript 지원)**

```typescript
// types/screen.ts
export type WebType =
  | "text"
  | "number"
  | "date"
  | "my_new_type"  // ← 새 타입 추가
  | /* 기타 타입들 */;

export interface MyNewTypeConfig {
  option1?: string;
  option2?: "default" | "custom";
  // 기타 설정 옵션들
}

export type WebTypeConfig =
  | TextTypeConfig
  | NumberTypeConfig
  | MyNewTypeConfig  // ← 새 설정 타입 추가
  | /* 기타 설정 타입들 */;
```

### 🎯 **핵심 장점**

- **플러그 앤 플레이**: 코드 수정 최소화
- **데이터베이스 기반**: 개발자 도구나 어드민에서 웹타입 관리 가능
- **자동 감지**: 별도 등록 로직 없이 자동으로 시스템에 반영
- **실시간 업데이트**: React key props로 즉시 설정 변경 반영

### 9.2 새로운 템플릿 추가

1. **TemplatesPanel에 템플릿 정의 추가**

```typescript
const templates: TemplateComponent[] = [
  {
    id: "새로운-템플릿",
    name: "새로운 템플릿",
    category: "카테고리",
    icon: <IconComponent />,
    defaultSize: { width: 400, height: 300 },
    components: [
      // 템플릿 구성 컴포넌트들
    ]
  }
];
```

2. **필요한 경우 특별한 렌더링 로직 추가**

### 9.3 코딩 컨벤션

#### 실시간 속성 편집 패턴 (필수)

```typescript
// 1. 로컬 상태 정의
const [localInputs, setLocalInputs] = useState({
  title: component.title || "",
});

// 2. 컴포넌트 변경 시 동기화
useEffect(() => {
  setLocalInputs({
    title: component.title || "",
  });
}, [component.title]);

// 3. 실시간 입력 처리
<Input
  value={localInputs.title}
  onChange={(e) => {
    const newValue = e.target.value;
    setLocalInputs(prev => ({ ...prev, title: newValue }));
    onUpdateProperty("title", newValue);
  }}
/>
```

## 10. 테스트 전략

### 10.1 단위 테스트

- 유틸리티 함수 (격자 계산, 스타일 적용 등)
- 컴포넌트 상태 관리 로직
- 데이터 변환 함수

### 10.2 통합 테스트

- 드래그앤드롭 시나리오
- 속성 편집 플로우
- API 연동 테스트

### 10.3 E2E 테스트

- 화면 생성부터 렌더링까지 전체 플로우
- 복잡한 사용자 시나리오

## 11. 향후 개선 계획

### 11.1 단기 계획

- 웹타입별 상세 설정 완성 (Date, Number, Select, Radio, File, Code, Entity)
- 조건부 표시 기능 (특정 조건에 따른 컴포넌트 표시/숨김)
- 계산 필드 기능 (다른 필드 값을 기반으로 한 자동 계산)

### 11.2 중장기 계획

- 컴포넌트 간 데이터 바인딩
- 워크플로우 연동
- 다국어 지원
- 반응형 디자인
- 컴포넌트 라이브러리 확장

## 12. 트러블슈팅

### 12.1 일반적인 문제

#### 높이가 적용되지 않는 문제

- **원인**: Tailwind CSS의 `h-full` 클래스가 인라인 스타일을 무시
- **해결**: `className="w-full"` + `style={{ height: "100%" }}` 사용

#### 라벨이 컴포넌트 높이에 포함되는 문제

- **원인**: 라벨과 위젯이 같은 컨테이너 내에 위치
- **해결**: 라벨을 외부에 별도 렌더링하여 높이에서 제외

#### 스타일이 할당된 화면에서 적용되지 않는 문제

- **원인**: `applyStyles` 함수 미사용 또는 잘못된 스타일 병합
- **해결**: 모든 위젯에서 일관된 스타일 적용 로직 사용

#### 다중 드래그 시 성능 문제

- **원인**: 과도한 리렌더링
- **해결**: `useCallback`, `useMemo` 적극 활용

### 12.2 디버깅 팁

- 브라우저 개발자 도구의 React DevTools 활용
- 콘솔 로그를 통한 상태 추적
- 컴포넌트 트리 구조 시각화

---

_본 문서는 지속적으로 업데이트되며, 새로운 기능 추가 시 해당 섹션을 업데이트해야 합니다._
