# 화면 임베딩 및 데이터 전달 시스템 구현 계획서

## 📋 목차

1. [개요](#개요)
2. [현재 문제점](#현재-문제점)
3. [목표](#목표)
4. [시스템 아키텍처](#시스템-아키텍처)
5. [데이터베이스 설계](#데이터베이스-설계)
6. [타입 정의](#타입-정의)
7. [컴포넌트 구조](#컴포넌트-구조)
8. [API 설계](#api-설계)
9. [구현 단계](#구현-단계)
10. [사용 시나리오](#사용-시나리오)

---

## 개요

### 배경

현재 화면관리 시스템은 단일 화면 단위로만 동작하며, 화면 간 데이터 전달이나 화면 임베딩이 불가능합니다. 실무에서는 "입고 등록"과 같이 **좌측에서 데이터를 선택하고 우측으로 전달하여 처리하는** 복잡한 워크플로우가 필요합니다.

### 핵심 요구사항

- **화면 임베딩**: 기존 화면을 다른 화면 안에 재사용
- **데이터 전달**: 한 화면에서 선택한 데이터를 다른 화면의 컴포넌트로 전달
- **유연한 매핑**: 테이블뿐만 아니라 입력 필드, 셀렉트 박스, 리피터 등 모든 컴포넌트에 데이터 주입 가능
- **변환 함수**: 합계, 평균, 개수 등 데이터 변환 지원

---

## 현재 문제점

### 1. 화면 재사용 불가

- 각 화면은 독립적으로만 동작
- 동일한 기능을 여러 화면에서 중복 구현

### 2. 화면 간 데이터 전달 불가

- 한 화면에서 선택한 데이터를 다른 화면으로 전달할 수 없음
- 사용자가 수동으로 복사/붙여넣기 해야 함

### 3. 복잡한 워크플로우 구현 불가

- "발주 목록 조회 → 품목 선택 → 입고 등록"과 같은 프로세스를 단일 화면에서 처리 불가
- 여러 화면을 오가며 작업해야 하는 불편함

### 4. 컴포넌트별 데이터 주입 불가

- 테이블에만 데이터를 추가할 수 있음
- 입력 필드, 셀렉트 박스 등에 자동으로 값을 설정할 수 없음

---

## 목표

### 주요 목표

1. **화면 임베딩 시스템 구축**: 기존 화면을 컨테이너로 사용
2. **범용 데이터 전달 시스템**: 모든 컴포넌트 타입 지원
3. **시각적 매핑 설정 UI**: 드래그앤드롭으로 매핑 규칙 설정
4. **실시간 미리보기**: 데이터 전달 결과를 즉시 확인

### 부가 목표

- 조건부 데이터 전달 (필터링)
- 데이터 변환 함수 (합계, 평균, 개수 등)
- 양방향 데이터 동기화
- 트랜잭션 지원 (전체 성공 또는 전체 실패)

---

## 시스템 아키텍처

### 전체 구조

```
┌─────────────────────────────────────────────────────────┐
│                  Screen Split Panel                      │
│                                                          │
│  ┌──────────────────┐         ┌──────────────────┐     │
│  │  Left Screen     │         │  Right Screen    │     │
│  │  (Source)        │         │  (Target)        │     │
│  │                  │         │                  │     │
│  │  ┌────────────┐  │         │  ┌────────────┐  │     │
│  │  │ Table      │  │         │  │ Form       │  │     │
│  │  │ (Select)   │  │         │  │            │  │     │
│  │  └────────────┘  │         │  └────────────┘  │     │
│  │                  │         │                  │     │
│  │  [✓] Row 1       │         │  Input: ____     │     │
│  │  [✓] Row 2       │         │  Select: [  ]    │     │
│  │  [ ] Row 3       │         │                  │     │
│  │                  │         │  ┌────────────┐  │     │
│  └──────────────────┘         │  │ Table      │  │     │
│           │                   │  │ (Append)   │  │     │
│           │                   │  └────────────┘  │     │
│           ▼                   │                  │     │
│    [선택 품목 추가] ──────────▶│  Row 1 (Added)   │     │
│                               │  Row 2 (Added)   │     │
│                               └──────────────────┘     │
└─────────────────────────────────────────────────────────┘
```

### 레이어 구조

```
┌─────────────────────────────────────────┐
│  Presentation Layer (UI)                │
│  - ScreenSplitPanel                     │
│  - EmbeddedScreen                       │
│  - DataMappingConfig                    │
└─────────────────────────────────────────┘
                  │
┌─────────────────────────────────────────┐
│  Business Logic Layer                   │
│  - DataTransferService                  │
│  - MappingEngine                        │
│  - TransformFunctions                   │
└─────────────────────────────────────────┘
                  │
┌─────────────────────────────────────────┐
│  Data Access Layer                      │
│  - screen_embedding (테이블)            │
│  - screen_data_transfer (테이블)        │
│  - component_data_receiver (인터페이스) │
└─────────────────────────────────────────┘
```

---

## 데이터베이스 설계

### 1. screen_embedding (화면 임베딩 설정)

```sql
CREATE TABLE screen_embedding (
  id SERIAL PRIMARY KEY,

  -- 부모 화면 (컨테이너)
  parent_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id),

  -- 자식 화면 (임베드될 화면)
  child_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id),

  -- 임베딩 위치
  position VARCHAR(20) NOT NULL,  -- 'left', 'right', 'top', 'bottom', 'center'

  -- 임베딩 모드
  mode VARCHAR(20) NOT NULL,      -- 'view', 'select', 'form', 'edit'

  -- 추가 설정
  config JSONB,
  -- {
  --   "width": "50%",
  --   "height": "100%",
  --   "resizable": true,
  --   "multiSelect": true,
  --   "showToolbar": true
  -- }

  -- 멀티테넌시
  company_code VARCHAR(20) NOT NULL,

  -- 메타데이터
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  created_by VARCHAR(50),

  CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id)
    REFERENCES screen_info(screen_id) ON DELETE CASCADE,
  CONSTRAINT fk_child_screen FOREIGN KEY (child_screen_id)
    REFERENCES screen_info(screen_id) ON DELETE CASCADE
);

-- 인덱스
CREATE INDEX idx_screen_embedding_parent ON screen_embedding(parent_screen_id, company_code);
CREATE INDEX idx_screen_embedding_child ON screen_embedding(child_screen_id, company_code);
```

### 2. screen_data_transfer (데이터 전달 설정)

```sql
CREATE TABLE screen_data_transfer (
  id SERIAL PRIMARY KEY,

  -- 소스 화면 (데이터 제공)
  source_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id),

  -- 타겟 화면 (데이터 수신)
  target_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id),

  -- 소스 컴포넌트 (선택 영역)
  source_component_id VARCHAR(100),
  source_component_type VARCHAR(50),  -- 'table', 'list', 'grid'

  -- 데이터 수신자 설정 (JSONB 배열)
  data_receivers JSONB NOT NULL,
  -- [
  --   {
  --     "targetComponentId": "table-입고처리품목",
  --     "targetComponentType": "table",
  --     "mode": "append",
  --     "mappingRules": [
  --       {
  --         "sourceField": "품목코드",
  --         "targetField": "품목코드",
  --         "transform": null
  --       }
  --     ],
  --     "condition": {
  --       "field": "상태",
  --       "operator": "equals",
  --       "value": "승인"
  --     }
  --   }
  -- ]

  -- 전달 버튼 설정
  button_config JSONB,
  -- {
  --   "label": "선택 품목 추가",
  --   "position": "center",
  --   "icon": "ArrowRight",
  --   "validation": {
  --     "requireSelection": true,
  --     "minSelection": 1,
  --     "maxSelection": 100,
  --     "customValidation": "function(rows) { return rows.length > 0; }"
  --   }
  -- }

  -- 멀티테넌시
  company_code VARCHAR(20) NOT NULL,

  -- 메타데이터
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  created_by VARCHAR(50),

  CONSTRAINT fk_source_screen FOREIGN KEY (source_screen_id)
    REFERENCES screen_info(screen_id) ON DELETE CASCADE,
  CONSTRAINT fk_target_screen FOREIGN KEY (target_screen_id)
    REFERENCES screen_info(screen_id) ON DELETE CASCADE
);

-- 인덱스
CREATE INDEX idx_screen_data_transfer_source ON screen_data_transfer(source_screen_id, company_code);
CREATE INDEX idx_screen_data_transfer_target ON screen_data_transfer(target_screen_id, company_code);
```

### 3. screen_split_panel (분할 패널 설정)

```sql
CREATE TABLE screen_split_panel (
  id SERIAL PRIMARY KEY,

  -- 부모 화면 (분할 패널 컨테이너)
  screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id),

  -- 좌측 화면 임베딩
  left_embedding_id INTEGER REFERENCES screen_embedding(id),

  -- 우측 화면 임베딩
  right_embedding_id INTEGER REFERENCES screen_embedding(id),

  -- 데이터 전달 설정
  data_transfer_id INTEGER REFERENCES screen_data_transfer(id),

  -- 레이아웃 설정
  layout_config JSONB,
  -- {
  --   "splitRatio": 50,           // 좌:우 비율 (0-100)
  --   "resizable": true,
  --   "minLeftWidth": 300,
  --   "minRightWidth": 400,
  --   "orientation": "horizontal" // 'horizontal' | 'vertical'
  -- }

  -- 멀티테넌시
  company_code VARCHAR(20) NOT NULL,

  -- 메타데이터
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),

  CONSTRAINT fk_screen FOREIGN KEY (screen_id)
    REFERENCES screen_info(screen_id) ON DELETE CASCADE,
  CONSTRAINT fk_left_embedding FOREIGN KEY (left_embedding_id)
    REFERENCES screen_embedding(id) ON DELETE SET NULL,
  CONSTRAINT fk_right_embedding FOREIGN KEY (right_embedding_id)
    REFERENCES screen_embedding(id) ON DELETE SET NULL,
  CONSTRAINT fk_data_transfer FOREIGN KEY (data_transfer_id)
    REFERENCES screen_data_transfer(id) ON DELETE SET NULL
);

-- 인덱스
CREATE INDEX idx_screen_split_panel_screen ON screen_split_panel(screen_id, company_code);
```

---

## 타입 정의

### 1. 화면 임베딩 타입

```typescript
// 임베딩 모드
type EmbeddingMode =
  | "view" // 읽기 전용
  | "select" // 선택 모드 (체크박스)
  | "form" // 폼 입력 모드
  | "edit"; // 편집 모드

// 임베딩 위치
type EmbeddingPosition = "left" | "right" | "top" | "bottom" | "center";

// 화면 임베딩 설정
interface ScreenEmbedding {
  id: number;
  parentScreenId: number;
  childScreenId: number;
  position: EmbeddingPosition;
  mode: EmbeddingMode;
  config: {
    width?: string; // "50%", "400px"
    height?: string; // "100%", "600px"
    resizable?: boolean;
    multiSelect?: boolean;
    showToolbar?: boolean;
    showSearch?: boolean;
    showPagination?: boolean;
  };
  companyCode: string;
}
```

### 2. 데이터 전달 타입

```typescript
// 컴포넌트 타입
type ComponentType =
  | "table" // 테이블
  | "input" // 입력 필드
  | "select" // 셀렉트 박스
  | "textarea" // 텍스트 영역
  | "checkbox" // 체크박스
  | "radio" // 라디오 버튼
  | "date" // 날짜 선택
  | "repeater" // 리피터 (반복 그룹)
  | "form-group" // 폼 그룹
  | "hidden"; // 히든 필드

// 데이터 수신 모드
type DataReceiveMode =
  | "append" // 기존 데이터에 추가
  | "replace" // 기존 데이터 덮어쓰기
  | "merge"; // 기존 데이터와 병합 (키 기준)

// 변환 함수
type TransformFunction =
  | "none" // 변환 없음
  | "sum" // 합계
  | "average" // 평균
  | "count" // 개수
  | "min" // 최소값
  | "max" // 최대값
  | "first" // 첫 번째 값
  | "last" // 마지막 값
  | "concat" // 문자열 결합
  | "join" // 배열 결합
  | "custom"; // 커스텀 함수

// 조건 연산자
type ConditionOperator =
  | "equals"
  | "notEquals"
  | "contains"
  | "notContains"
  | "greaterThan"
  | "lessThan"
  | "greaterThanOrEqual"
  | "lessThanOrEqual"
  | "in"
  | "notIn";

// 매핑 규칙
interface MappingRule {
  sourceField: string; // 소스 필드명
  targetField: string; // 타겟 필드명
  transform?: TransformFunction; // 변환 함수
  transformConfig?: any; // 변환 함수 설정
  defaultValue?: any; // 기본값
  required?: boolean; // 필수 여부
}

// 조건
interface Condition {
  field: string;
  operator: ConditionOperator;
  value: any;
}

// 데이터 수신자
interface DataReceiver {
  targetComponentId: string; // 타겟 컴포넌트 ID
  targetComponentType: ComponentType;
  mode: DataReceiveMode;
  mappingRules: MappingRule[];
  condition?: Condition; // 조건부 전달
  validation?: {
    required?: boolean;
    minRows?: number;
    maxRows?: number;
    customValidation?: string; // JavaScript 함수 문자열
  };
}

// 버튼 설정
interface TransferButtonConfig {
  label: string;
  position: "left" | "right" | "center";
  icon?: string;
  variant?: "default" | "outline" | "ghost";
  size?: "sm" | "default" | "lg";
  validation?: {
    requireSelection: boolean;
    minSelection?: number;
    maxSelection?: number;
    confirmMessage?: string;
    customValidation?: string;
  };
}

// 데이터 전달 설정
interface ScreenDataTransfer {
  id: number;
  sourceScreenId: number;
  targetScreenId: number;
  sourceComponentId?: string;
  sourceComponentType?: string;
  dataReceivers: DataReceiver[];
  buttonConfig: TransferButtonConfig;
  companyCode: string;
}
```

### 3. 분할 패널 타입

```typescript
// 레이아웃 설정
interface LayoutConfig {
  splitRatio: number; // 0-100 (좌측 비율)
  resizable: boolean;
  minLeftWidth?: number; // 최소 좌측 너비 (px)
  minRightWidth?: number; // 최소 우측 너비 (px)
  orientation: "horizontal" | "vertical";
}

// 분할 패널 설정
interface ScreenSplitPanel {
  id: number;
  screenId: number;
  leftEmbedding: ScreenEmbedding;
  rightEmbedding: ScreenEmbedding;
  dataTransfer: ScreenDataTransfer;
  layoutConfig: LayoutConfig;
  companyCode: string;
}
```

### 4. 컴포넌트 인터페이스

```typescript
// 모든 데이터 수신 가능 컴포넌트가 구현해야 하는 인터페이스
interface DataReceivable {
  // 컴포넌트 ID
  componentId: string;

  // 컴포넌트 타입
  componentType: ComponentType;

  // 데이터 수신
  receiveData(data: any[], mode: DataReceiveMode): Promise<void>;

  // 현재 데이터 가져오기
  getData(): any;

  // 데이터 초기화
  clearData(): void;

  // 검증
  validate(): boolean;

  // 이벤트 리스너
  onDataReceived?: (data: any[]) => void;
  onDataCleared?: () => void;
}

// 선택 가능 컴포넌트 인터페이스
interface Selectable {
  // 선택된 행/항목 가져오기
  getSelectedRows(): any[];

  // 선택 초기화
  clearSelection(): void;

  // 전체 선택
  selectAll(): void;

  // 선택 이벤트
  onSelectionChanged?: (selectedRows: any[]) => void;
}
```

---

## 컴포넌트 구조

### 1. ScreenSplitPanel (최상위 컨테이너)

```tsx
interface ScreenSplitPanelProps {
  config: ScreenSplitPanel;
  onDataTransferred?: (data: any[]) => void;
}

export function ScreenSplitPanel({
  config,
  onDataTransferred,
}: ScreenSplitPanelProps) {
  const leftScreenRef = useRef<EmbeddedScreenHandle>(null);
  const rightScreenRef = useRef<EmbeddedScreenHandle>(null);
  const [splitRatio, setSplitRatio] = useState(config.layoutConfig.splitRatio);

  // 데이터 전달 핸들러
  const handleTransferData = async () => {
    // 1. 좌측 화면에서 선택된 데이터 가져오기
    const selectedRows = leftScreenRef.current?.getSelectedRows() || [];

    if (selectedRows.length === 0) {
      toast.error("선택된 항목이 없습니다.");
      return;
    }

    // 2. 검증
    if (config.dataTransfer.buttonConfig.validation) {
      const validation = config.dataTransfer.buttonConfig.validation;

      if (
        validation.minSelection &&
        selectedRows.length < validation.minSelection
      ) {
        toast.error(`최소 ${validation.minSelection}개 이상 선택해야 합니다.`);
        return;
      }

      if (
        validation.maxSelection &&
        selectedRows.length > validation.maxSelection
      ) {
        toast.error(
          `최대 ${validation.maxSelection}개까지만 선택할 수 있습니다.`
        );
        return;
      }

      if (validation.confirmMessage) {
        const confirmed = await confirm(validation.confirmMessage);
        if (!confirmed) return;
      }
    }

    // 3. 데이터 전달
    try {
      await rightScreenRef.current?.receiveData(
        selectedRows,
        config.dataTransfer.dataReceivers
      );

      toast.success("데이터가 전달되었습니다.");
      onDataTransferred?.(selectedRows);

      // 4. 좌측 선택 초기화 (옵션)
      if (config.dataTransfer.buttonConfig.clearAfterTransfer) {
        leftScreenRef.current?.clearSelection();
      }
    } catch (error) {
      toast.error("데이터 전달 중 오류가 발생했습니다.");
      console.error(error);
    }
  };

  return (
    <div className="flex h-full">
      {/* 좌측 패널 */}
      <div style={{ width: `${splitRatio}%` }}>
        <EmbeddedScreen ref={leftScreenRef} embedding={config.leftEmbedding} />
      </div>

      {/* 리사이저 */}
      {config.layoutConfig.resizable && (
        <Resizer onResize={(newRatio) => setSplitRatio(newRatio)} />
      )}

      {/* 전달 버튼 */}
      <div className="flex items-center justify-center px-4">
        <Button
          onClick={handleTransferData}
          variant={config.dataTransfer.buttonConfig.variant || "default"}
          size={config.dataTransfer.buttonConfig.size || "default"}
        >
          {config.dataTransfer.buttonConfig.icon && (
            <Icon
              name={config.dataTransfer.buttonConfig.icon}
              className="mr-2"
            />
          )}
          {config.dataTransfer.buttonConfig.label}
        </Button>
      </div>

      {/* 우측 패널 */}
      <div style={{ width: `${100 - splitRatio}%` }}>
        <EmbeddedScreen
          ref={rightScreenRef}
          embedding={config.rightEmbedding}
        />
      </div>
    </div>
  );
}
```

### 2. EmbeddedScreen (임베드된 화면)

```tsx
interface EmbeddedScreenProps {
  embedding: ScreenEmbedding;
}

export interface EmbeddedScreenHandle {
  getSelectedRows(): any[];
  clearSelection(): void;
  receiveData(data: any[], receivers: DataReceiver[]): Promise<void>;
  getData(): any;
}

export const EmbeddedScreen = forwardRef<
  EmbeddedScreenHandle,
  EmbeddedScreenProps
>(({ embedding }, ref) => {
  const [screenData, setScreenData] = useState<any>(null);
  const [selectedRows, setSelectedRows] = useState<any[]>([]);
  const componentRefs = useRef<Map<string, DataReceivable>>(new Map());

  // 화면 데이터 로드
  useEffect(() => {
    loadScreenData(embedding.childScreenId);
  }, [embedding.childScreenId]);

  // 외부에서 호출 가능한 메서드
  useImperativeHandle(ref, () => ({
    getSelectedRows: () => selectedRows,

    clearSelection: () => {
      setSelectedRows([]);
    },

    receiveData: async (data: any[], receivers: DataReceiver[]) => {
      // 각 데이터 수신자에게 데이터 전달
      for (const receiver of receivers) {
        const component = componentRefs.current.get(receiver.targetComponentId);

        if (!component) {
          console.warn(
            `컴포넌트를 찾을 수 없습니다: ${receiver.targetComponentId}`
          );
          continue;
        }

        // 조건 확인
        let filteredData = data;
        if (receiver.condition) {
          filteredData = filterData(data, receiver.condition);
        }

        // 매핑 적용
        const mappedData = applyMappingRules(
          filteredData,
          receiver.mappingRules
        );

        // 데이터 전달
        await component.receiveData(mappedData, receiver.mode);
      }
    },

    getData: () => {
      const allData: Record<string, any> = {};
      componentRefs.current.forEach((component, id) => {
        allData[id] = component.getData();
      });
      return allData;
    },
  }));

  // 컴포넌트 등록
  const registerComponent = (id: string, component: DataReceivable) => {
    componentRefs.current.set(id, component);
  };

  return (
    <div className="h-full overflow-auto">
      {screenData && (
        <InteractiveScreenViewer
          screenData={screenData}
          mode={embedding.mode}
          onSelectionChanged={setSelectedRows}
          onComponentMount={registerComponent}
        />
      )}
    </div>
  );
});
```

### 3. DataReceivable 구현 예시

#### TableComponent

```typescript
class TableComponent implements DataReceivable {
  componentId: string;
  componentType: ComponentType = "table";
  private rows: any[] = [];

  async receiveData(data: any[], mode: DataReceiveMode): Promise<void> {
    switch (mode) {
      case "append":
        this.rows = [...this.rows, ...data];
        break;
      case "replace":
        this.rows = data;
        break;
      case "merge":
        // 키 기반 병합 (예: id 필드)
        const existingIds = new Set(this.rows.map((r) => r.id));
        const newRows = data.filter((r) => !existingIds.has(r.id));
        this.rows = [...this.rows, ...newRows];
        break;
    }

    this.render();
    this.onDataReceived?.(data);
  }

  getData(): any {
    return this.rows;
  }

  clearData(): void {
    this.rows = [];
    this.render();
    this.onDataCleared?.();
  }

  validate(): boolean {
    return this.rows.length > 0;
  }

  private render() {
    // 테이블 리렌더링
  }
}
```

#### InputComponent

```typescript
class InputComponent implements DataReceivable {
  componentId: string;
  componentType: ComponentType = "input";
  private value: any = "";

  async receiveData(data: any[], mode: DataReceiveMode): Promise<void> {
    // 입력 필드는 단일 값이므로 첫 번째 항목만 사용
    if (data.length > 0) {
      this.value = data[0];
      this.render();
      this.onDataReceived?.(data);
    }
  }

  getData(): any {
    return this.value;
  }

  clearData(): void {
    this.value = "";
    this.render();
    this.onDataCleared?.();
  }

  validate(): boolean {
    return this.value !== null && this.value !== undefined && this.value !== "";
  }

  private render() {
    // 입력 필드 리렌더링
  }
}
```

---

## API 설계

### 1. 화면 임베딩 API

```typescript
// GET /api/screen-embedding/:parentScreenId
export async function getScreenEmbeddings(
  parentScreenId: number,
  companyCode: string
): Promise<ApiResponse<ScreenEmbedding[]>> {
  const query = `
    SELECT * FROM screen_embedding
    WHERE parent_screen_id = $1
      AND company_code = $2
    ORDER BY position
  `;

  const result = await pool.query(query, [parentScreenId, companyCode]);
  return { success: true, data: result.rows };
}

// POST /api/screen-embedding
export async function createScreenEmbedding(
  embedding: Omit<ScreenEmbedding, "id">,
  companyCode: string
): Promise<ApiResponse<ScreenEmbedding>> {
  const query = `
    INSERT INTO screen_embedding (
      parent_screen_id, child_screen_id, position, mode, config, company_code
    ) VALUES ($1, $2, $3, $4, $5, $6)
    RETURNING *
  `;

  const result = await pool.query(query, [
    embedding.parentScreenId,
    embedding.childScreenId,
    embedding.position,
    embedding.mode,
    JSON.stringify(embedding.config),
    companyCode,
  ]);

  return { success: true, data: result.rows[0] };
}

// PUT /api/screen-embedding/:id
export async function updateScreenEmbedding(
  id: number,
  embedding: Partial<ScreenEmbedding>,
  companyCode: string
): Promise<ApiResponse<ScreenEmbedding>> {
  const updates: string[] = [];
  const values: any[] = [];
  let paramIndex = 1;

  if (embedding.position) {
    updates.push(`position = $${paramIndex++}`);
    values.push(embedding.position);
  }

  if (embedding.mode) {
    updates.push(`mode = $${paramIndex++}`);
    values.push(embedding.mode);
  }

  if (embedding.config) {
    updates.push(`config = $${paramIndex++}`);
    values.push(JSON.stringify(embedding.config));
  }

  updates.push(`updated_at = NOW()`);

  values.push(id, companyCode);

  const query = `
    UPDATE screen_embedding
    SET ${updates.join(", ")}
    WHERE id = $${paramIndex++}
      AND company_code = $${paramIndex++}
    RETURNING *
  `;

  const result = await pool.query(query, values);

  if (result.rowCount === 0) {
    return { success: false, message: "임베딩 설정을 찾을 수 없습니다." };
  }

  return { success: true, data: result.rows[0] };
}

// DELETE /api/screen-embedding/:id
export async function deleteScreenEmbedding(
  id: number,
  companyCode: string
): Promise<ApiResponse<void>> {
  const query = `
    DELETE FROM screen_embedding
    WHERE id = $1 AND company_code = $2
  `;

  const result = await pool.query(query, [id, companyCode]);

  if (result.rowCount === 0) {
    return { success: false, message: "임베딩 설정을 찾을 수 없습니다." };
  }

  return { success: true };
}
```

### 2. 데이터 전달 API

```typescript
// GET /api/screen-data-transfer?sourceScreenId=1&targetScreenId=2
export async function getScreenDataTransfer(
  sourceScreenId: number,
  targetScreenId: number,
  companyCode: string
): Promise<ApiResponse<ScreenDataTransfer>> {
  const query = `
    SELECT * FROM screen_data_transfer
    WHERE source_screen_id = $1
      AND target_screen_id = $2
      AND company_code = $3
  `;

  const result = await pool.query(query, [
    sourceScreenId,
    targetScreenId,
    companyCode,
  ]);

  if (result.rowCount === 0) {
    return { success: false, message: "데이터 전달 설정을 찾을 수 없습니다." };
  }

  return { success: true, data: result.rows[0] };
}

// POST /api/screen-data-transfer
export async function createScreenDataTransfer(
  transfer: Omit<ScreenDataTransfer, "id">,
  companyCode: string
): Promise<ApiResponse<ScreenDataTransfer>> {
  const query = `
    INSERT INTO screen_data_transfer (
      source_screen_id, target_screen_id, source_component_id, source_component_type,
      data_receivers, button_config, company_code
    ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    RETURNING *
  `;

  const result = await pool.query(query, [
    transfer.sourceScreenId,
    transfer.targetScreenId,
    transfer.sourceComponentId,
    transfer.sourceComponentType,
    JSON.stringify(transfer.dataReceivers),
    JSON.stringify(transfer.buttonConfig),
    companyCode,
  ]);

  return { success: true, data: result.rows[0] };
}

// PUT /api/screen-data-transfer/:id
export async function updateScreenDataTransfer(
  id: number,
  transfer: Partial<ScreenDataTransfer>,
  companyCode: string
): Promise<ApiResponse<ScreenDataTransfer>> {
  const updates: string[] = [];
  const values: any[] = [];
  let paramIndex = 1;

  if (transfer.dataReceivers) {
    updates.push(`data_receivers = $${paramIndex++}`);
    values.push(JSON.stringify(transfer.dataReceivers));
  }

  if (transfer.buttonConfig) {
    updates.push(`button_config = $${paramIndex++}`);
    values.push(JSON.stringify(transfer.buttonConfig));
  }

  updates.push(`updated_at = NOW()`);

  values.push(id, companyCode);

  const query = `
    UPDATE screen_data_transfer
    SET ${updates.join(", ")}
    WHERE id = $${paramIndex++}
      AND company_code = $${paramIndex++}
    RETURNING *
  `;

  const result = await pool.query(query, values);

  if (result.rowCount === 0) {
    return { success: false, message: "데이터 전달 설정을 찾을 수 없습니다." };
  }

  return { success: true, data: result.rows[0] };
}
```

### 3. 분할 패널 API

```typescript
// GET /api/screen-split-panel/:screenId
export async function getScreenSplitPanel(
  screenId: number,
  companyCode: string
): Promise<ApiResponse<ScreenSplitPanel>> {
  const query = `
    SELECT 
      ssp.*,
      le.* as left_embedding,
      re.* as right_embedding,
      sdt.* as data_transfer
    FROM screen_split_panel ssp
    LEFT JOIN screen_embedding le ON ssp.left_embedding_id = le.id
    LEFT JOIN screen_embedding re ON ssp.right_embedding_id = re.id
    LEFT JOIN screen_data_transfer sdt ON ssp.data_transfer_id = sdt.id
    WHERE ssp.screen_id = $1
      AND ssp.company_code = $2
  `;

  const result = await pool.query(query, [screenId, companyCode]);

  if (result.rowCount === 0) {
    return { success: false, message: "분할 패널 설정을 찾을 수 없습니다." };
  }

  return { success: true, data: result.rows[0] };
}

// POST /api/screen-split-panel
export async function createScreenSplitPanel(
  panel: Omit<ScreenSplitPanel, "id">,
  companyCode: string
): Promise<ApiResponse<ScreenSplitPanel>> {
  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    // 1. 좌측 임베딩 생성
    const leftEmbedding = await createScreenEmbedding(
      panel.leftEmbedding,
      companyCode
    );

    // 2. 우측 임베딩 생성
    const rightEmbedding = await createScreenEmbedding(
      panel.rightEmbedding,
      companyCode
    );

    // 3. 데이터 전달 설정 생성
    const dataTransfer = await createScreenDataTransfer(
      panel.dataTransfer,
      companyCode
    );

    // 4. 분할 패널 생성
    const query = `
      INSERT INTO screen_split_panel (
        screen_id, left_embedding_id, right_embedding_id, data_transfer_id,
        layout_config, company_code
      ) VALUES ($1, $2, $3, $4, $5, $6)
      RETURNING *
    `;

    const result = await client.query(query, [
      panel.screenId,
      leftEmbedding.data!.id,
      rightEmbedding.data!.id,
      dataTransfer.data!.id,
      JSON.stringify(panel.layoutConfig),
      companyCode,
    ]);

    await client.query("COMMIT");

    return { success: true, data: result.rows[0] };
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
}
```

---

## 구현 단계

### Phase 1: 기본 인프라 구축 (1-2주)

#### 1.1 데이터베이스 마이그레이션

- [ ] `screen_embedding` 테이블 생성
- [ ] `screen_data_transfer` 테이블 생성
- [ ] `screen_split_panel` 테이블 생성
- [ ] 인덱스 및 외래키 설정
- [ ] 샘플 데이터 삽입

#### 1.2 타입 정의

- [ ] TypeScript 인터페이스 작성
- [ ] `types/screen-embedding.ts`
- [ ] `types/data-transfer.ts`
- [ ] `types/split-panel.ts`

#### 1.3 백엔드 API

- [ ] 화면 임베딩 CRUD API
- [ ] 데이터 전달 설정 CRUD API
- [ ] 분할 패널 CRUD API
- [ ] 컨트롤러 및 서비스 레이어 구현

### Phase 2: 화면 임베딩 기능 (2-3주)

#### 2.1 EmbeddedScreen 컴포넌트

- [ ] 기본 임베딩 기능
- [ ] 모드별 렌더링 (view, select, form, edit)
- [ ] 선택 모드 구현 (체크박스)
- [ ] 이벤트 핸들링

#### 2.2 DataReceivable 인터페이스 구현

- [ ] TableComponent
- [ ] InputComponent
- [ ] SelectComponent
- [ ] TextareaComponent
- [ ] RepeaterComponent
- [ ] FormGroupComponent
- [ ] HiddenComponent

#### 2.3 컴포넌트 등록 시스템

- [ ] 컴포넌트 마운트 시 자동 등록
- [ ] 컴포넌트 ID 관리
- [ ] 컴포넌트 참조 관리

### Phase 3: 데이터 전달 시스템 (2-3주)

#### 3.1 매핑 엔진

- [ ] 매핑 규칙 파싱
- [ ] 필드 매핑 적용
- [ ] 변환 함수 구현
  - [ ] sum, average, count
  - [ ] min, max
  - [ ] first, last
  - [ ] concat, join

#### 3.2 조건부 전달

- [ ] 조건 파싱
- [ ] 필터링 로직
- [ ] 복합 조건 지원

#### 3.3 검증 시스템

- [ ] 필수 필드 검증
- [ ] 최소/최대 행 수 검증
- [ ] 커스텀 검증 함수 실행

### Phase 4: 분할 패널 UI (2-3주)

#### 4.1 ScreenSplitPanel 컴포넌트

- [ ] 기본 레이아웃
- [ ] 리사이저 구현
- [ ] 전달 버튼
- [ ] 반응형 디자인

#### 4.2 설정 UI

- [ ] 화면 선택 드롭다운
- [ ] 매핑 규칙 설정 UI
- [ ] 드래그앤드롭 매핑
- [ ] 미리보기 기능

#### 4.3 시각적 피드백

- [ ] 데이터 전달 애니메이션
- [ ] 로딩 상태 표시
- [ ] 성공/실패 토스트

### Phase 5: 고급 기능 (2-3주)

#### 5.1 양방향 동기화

- [ ] 우측 → 좌측 데이터 반영
- [ ] 실시간 업데이트

#### 5.2 트랜잭션 지원

- [ ] 전체 성공 또는 전체 실패
- [ ] 롤백 기능

#### 5.3 성능 최적화

- [ ] 대량 데이터 처리
- [ ] 가상 스크롤링
- [ ] 메모이제이션

### Phase 6: 테스트 및 문서화 (1-2주)

#### 6.1 단위 테스트

- [ ] 매핑 엔진 테스트
- [ ] 변환 함수 테스트
- [ ] 검증 로직 테스트

#### 6.2 통합 테스트

- [ ] 전체 워크플로우 테스트
- [ ] 실제 시나리오 테스트

#### 6.3 문서화

- [ ] 사용자 가이드
- [ ] 개발자 문서
- [ ] API 문서

---

## 사용 시나리오

### 시나리오 1: 입고 등록

#### 요구사항

- 발주 목록에서 품목을 선택하여 입고 등록
- 선택된 품목의 정보를 입고 처리 품목 테이블에 추가
- 공급자 정보를 자동으로 입력 필드에 설정
- 총 품목 수를 자동 계산

#### 설정

```typescript
const 입고등록_설정: ScreenSplitPanel = {
  screenId: 100,
  leftEmbedding: {
    childScreenId: 10, // 발주 목록 조회 화면
    position: "left",
    mode: "select",
    config: {
      width: "50%",
      multiSelect: true,
      showSearch: true,
      showPagination: true,
    },
  },
  rightEmbedding: {
    childScreenId: 20, // 입고 등록 폼 화면
    position: "right",
    mode: "form",
    config: {
      width: "50%",
    },
  },
  dataTransfer: {
    sourceScreenId: 10,
    targetScreenId: 20,
    sourceComponentId: "table-발주목록",
    sourceComponentType: "table",
    dataReceivers: [
      {
        targetComponentId: "table-입고처리품목",
        targetComponentType: "table",
        mode: "append",
        mappingRules: [
          { sourceField: "품목코드", targetField: "품목코드" },
          { sourceField: "품목명", targetField: "품목명" },
          { sourceField: "발주수량", targetField: "발주수량" },
          { sourceField: "미입고수량", targetField: "입고수량" },
        ],
      },
      {
        targetComponentId: "input-공급자",
        targetComponentType: "input",
        mode: "replace",
        mappingRules: [
          {
            sourceField: "공급자",
            targetField: "value",
            transform: "first",
          },
        ],
      },
      {
        targetComponentId: "input-품목수",
        targetComponentType: "input",
        mode: "replace",
        mappingRules: [
          {
            sourceField: "품목코드",
            targetField: "value",
            transform: "count",
          },
        ],
      },
    ],
    buttonConfig: {
      label: "선택 품목 추가",
      position: "center",
      icon: "ArrowRight",
      validation: {
        requireSelection: true,
        minSelection: 1,
      },
    },
  },
  layoutConfig: {
    splitRatio: 50,
    resizable: true,
    minLeftWidth: 400,
    minRightWidth: 600,
    orientation: "horizontal",
  },
};
```

### 시나리오 2: 수주 등록

#### 요구사항

- 견적서 목록에서 품목을 선택하여 수주 등록
- 고객 정보를 자동으로 폼에 설정
- 품목별 수량 및 금액 자동 계산
- 총 금액 합계 표시

#### 설정

```typescript
const 수주등록_설정: ScreenSplitPanel = {
  screenId: 101,
  leftEmbedding: {
    childScreenId: 30, // 견적서 목록 조회 화면
    position: "left",
    mode: "select",
    config: {
      width: "40%",
      multiSelect: true,
    },
  },
  rightEmbedding: {
    childScreenId: 40, // 수주 등록 폼 화면
    position: "right",
    mode: "form",
    config: {
      width: "60%",
    },
  },
  dataTransfer: {
    sourceScreenId: 30,
    targetScreenId: 40,
    dataReceivers: [
      {
        targetComponentId: "table-수주품목",
        targetComponentType: "table",
        mode: "append",
        mappingRules: [
          { sourceField: "품목코드", targetField: "품목코드" },
          { sourceField: "품목명", targetField: "품목명" },
          { sourceField: "수량", targetField: "수량" },
          { sourceField: "단가", targetField: "단가" },
          {
            sourceField: "수량",
            targetField: "금액",
            transform: "custom",
            transformConfig: {
              formula: "수량 * 단가",
            },
          },
        ],
      },
      {
        targetComponentId: "input-고객명",
        targetComponentType: "input",
        mode: "replace",
        mappingRules: [
          { sourceField: "고객명", targetField: "value", transform: "first" },
        ],
      },
      {
        targetComponentId: "input-총금액",
        targetComponentType: "input",
        mode: "replace",
        mappingRules: [
          {
            sourceField: "금액",
            targetField: "value",
            transform: "sum",
          },
        ],
      },
    ],
    buttonConfig: {
      label: "견적서 불러오기",
      position: "center",
      icon: "Download",
    },
  },
  layoutConfig: {
    splitRatio: 40,
    resizable: true,
    orientation: "horizontal",
  },
};
```

### 시나리오 3: 출고 등록

#### 요구사항

- 재고 목록에서 품목을 선택하여 출고 등록
- 재고 수량 확인 및 경고
- 출고 가능 수량만 필터링
- 창고별 재고 정보 표시

#### 설정

```typescript
const 출고등록_설정: ScreenSplitPanel = {
  screenId: 102,
  leftEmbedding: {
    childScreenId: 50, // 재고 목록 조회 화면
    position: "left",
    mode: "select",
    config: {
      width: "45%",
      multiSelect: true,
    },
  },
  rightEmbedding: {
    childScreenId: 60, // 출고 등록 폼 화면
    position: "right",
    mode: "form",
    config: {
      width: "55%",
    },
  },
  dataTransfer: {
    sourceScreenId: 50,
    targetScreenId: 60,
    dataReceivers: [
      {
        targetComponentId: "table-출고품목",
        targetComponentType: "table",
        mode: "append",
        mappingRules: [
          { sourceField: "품목코드", targetField: "품목코드" },
          { sourceField: "품목명", targetField: "품목명" },
          { sourceField: "재고수량", targetField: "가용수량" },
          { sourceField: "창고", targetField: "출고창고" },
        ],
        condition: {
          field: "재고수량",
          operator: "greaterThan",
          value: 0,
        },
      },
      {
        targetComponentId: "input-총출고수량",
        targetComponentType: "input",
        mode: "replace",
        mappingRules: [
          {
            sourceField: "재고수량",
            targetField: "value",
            transform: "sum",
          },
        ],
      },
    ],
    buttonConfig: {
      label: "출고 품목 추가",
      position: "center",
      icon: "ArrowRight",
      validation: {
        requireSelection: true,
        confirmMessage: "선택한 품목을 출고 처리하시겠습니까?",
      },
    },
  },
  layoutConfig: {
    splitRatio: 45,
    resizable: true,
    orientation: "horizontal",
  },
};
```

---

## 기술적 고려사항

### 1. 성능 최적화

#### 대량 데이터 처리

- 가상 스크롤링 적용
- 청크 단위 데이터 전달
- 백그라운드 처리

#### 메모리 관리

- 컴포넌트 언마운트 시 참조 해제
- 이벤트 리스너 정리
- 메모이제이션 활용

### 2. 보안

#### 권한 검증

- 화면 접근 권한 확인
- 데이터 전달 권한 확인
- 멀티테넌시 격리

#### 데이터 검증

- 입력값 검증
- SQL 인젝션 방지
- XSS 방지

### 3. 에러 처리

#### 사용자 친화적 메시지

- 명확한 오류 메시지
- 복구 방법 안내
- 로그 기록

#### 트랜잭션 롤백

- 부분 실패 시 전체 롤백
- 데이터 일관성 유지

### 4. 확장성

#### 플러그인 시스템

- 커스텀 변환 함수 등록
- 커스텀 검증 함수 등록
- 커스텀 컴포넌트 타입 추가

#### 이벤트 시스템

- 데이터 전달 전/후 이벤트
- 커스텀 이벤트 핸들러

---

## 마일스톤

### M1: 기본 인프라 (2주)

- 데이터베이스 스키마 완성
- 백엔드 API 완성
- 타입 정의 완성

### M2: 화면 임베딩 (3주)

- EmbeddedScreen 컴포넌트 완성
- DataReceivable 인터페이스 구현 완료
- 선택 모드 동작 확인

### M3: 데이터 전달 (3주)

- 매핑 엔진 완성
- 변환 함수 구현 완료
- 조건부 전달 동작 확인

### M4: 분할 패널 UI (3주)

- ScreenSplitPanel 컴포넌트 완성
- 설정 UI 완성
- 입고 등록 시나리오 완성

### M5: 고급 기능 및 최적화 (3주)

- 양방향 동기화 완성
- 성능 최적화 완료
- 전체 테스트 통과

### M6: 문서화 및 배포 (1주)

- 사용자 가이드 작성
- 개발자 문서 작성
- 프로덕션 배포

---

## 예상 일정

**총 소요 기간**: 약 15주 (3.5개월)

- Week 1-2: Phase 1 (기본 인프라)
- Week 3-5: Phase 2 (화면 임베딩)
- Week 6-8: Phase 3 (데이터 전달)
- Week 9-11: Phase 4 (분할 패널 UI)
- Week 12-14: Phase 5 (고급 기능)
- Week 15: Phase 6 (테스트 및 문서화)

---

## 성공 지표

### 기능적 지표

- [ ] 입고 등록 시나리오 완벽 동작
- [ ] 수주 등록 시나리오 완벽 동작
- [ ] 출고 등록 시나리오 완벽 동작
- [ ] 모든 컴포넌트 타입 데이터 수신 가능
- [ ] 모든 변환 함수 정상 동작

### 성능 지표

- [ ] 1000개 행 데이터 전달 < 1초
- [ ] 화면 로딩 시간 < 2초
- [ ] 메모리 사용량 < 100MB

### 사용성 지표

- [ ] 설정 UI 직관적
- [ ] 에러 메시지 명확
- [ ] 문서 완성도 90% 이상

---

## 리스크 관리

### 기술적 리스크

- **복잡도 증가**: 단계별 구현으로 관리
- **성능 문제**: 초기부터 최적화 고려
- **호환성 문제**: 기존 시스템과 충돌 방지

### 일정 리스크

- **예상 기간 초과**: 버퍼 2주 확보
- **우선순위 변경**: 핵심 기능 먼저 구현

### 인력 리스크

- **담당자 부재**: 문서화 철저히
- **지식 공유**: 주간 리뷰 미팅

---

## 결론

화면 임베딩 및 데이터 전달 시스템은 복잡한 업무 워크플로우를 효율적으로 처리할 수 있는 강력한 기능입니다. 단계별로 체계적으로 구현하면 약 3.5개월 내에 완성할 수 있으며, 이를 통해 사용자 경험을 크게 향상시킬 수 있습니다.
















