# 🔧 제어관리 외부 커넥션 통합 개선 계획서

## 📋 프로젝트 개요

### 목적

현재 외부 커넥션 관리에서 관리되고 있는 데이터베이스 커넥션 정보를 제어관리의 데이터 저장 액션에서 활용할 수 있도록 통합하여, 사용자가 다양한 외부 데이터베이스에 데이터를 저장할 수 있는 기능을 구현합니다.

### 현재 상황 분석

#### 기존 외부 커넥션 관리

- **테이블**: `external_db_connections`
- **지원 DB**: MySQL, PostgreSQL, Oracle, SQL Server, SQLite, MariaDB
- **관리 기능**: 연결 정보 CRUD, 연결 테스트, 암호화 저장
- **API**: `/api/external-db-connections/*` 엔드포인트

#### 기존 제어관리 시스템

- **연결 종류**: 현재 "데이터 저장" 타입 지원
- **액션 타입**: INSERT, UPDATE, DELETE
- **매핑**: FROM 테이블 → TO 테이블 컬럼 매핑
- **제약**: 현재는 메인 데이터베이스 내에서만 동작

### 변경 요구사항

1. **커넥션 선택 기능 추가**

   - INSERT 액션 타입 선택 시 커넥션 선택 단계 추가
   - FROM/TO 테이블 각각에 대해 독립적인 커넥션 설정

2. **테이블 선택 기능 개선**

   - 선택한 커넥션에 있는 테이블 목록 동적 로딩
   - FROM 커넥션의 테이블과 TO 커넥션의 테이블 독립 선택

3. **컬럼 매핑 규칙 유지**
   - FROM 테이블의 1개 컬럼 → TO 테이블의 2개 이상 컬럼 매핑 가능
   - FROM 테이블의 2개 이상 컬럼 → TO 테이블의 1개 컬럼 매핑 **불가**
   - 기존 UI 구조 최대한 유지

## 🏗️ 시스템 아키텍처 설계

### 1. 데이터 구조 확장

#### 기존 DataSaveSettings 구조

```typescript
interface DataSaveSettings {
  connectionType: "data-save";
  actions: Array<{
    actionType: "insert" | "update" | "delete";
    targetTable: string;
    fieldMappings: FieldMapping[];
  }>;
}
```

#### 개선된 DataSaveSettings 구조

```typescript
interface EnhancedDataSaveSettings {
  connectionType: "data-save";
  actions: Array<{
    actionType: "insert" | "update" | "delete";

    // 🆕 커넥션 정보 추가
    fromConnection?: {
      connectionId?: number;
      connectionName?: string;
      dbType?: string;
    };
    toConnection?: {
      connectionId?: number;
      connectionName?: string;
      dbType?: string;
    };

    // 기존 필드들
    targetTable: string;
    fromTable?: string; // 🆕 명시적으로 추가
    fieldMappings: EnhancedFieldMapping[];
  }>;
}

interface EnhancedFieldMapping {
  sourceTable: string;
  sourceField: string;
  targetTable: string;
  targetField: string;
  defaultValue?: string;
  transformFunction?: string;

  // 🆕 커넥션 정보 추가
  sourceConnectionId?: number;
  targetConnectionId?: number;
}
```

### 2. UI 컴포넌트 구조 개선

#### 단계별 설정 플로우

```
1. 액션 타입 선택 (INSERT/UPDATE/DELETE)
   ↓
2. [모든 액션 타입] 커넥션 설정 단계
   ├─ FROM 커넥션 선택 (데이터 소스)
   └─ TO 커넥션 선택 (데이터 대상)
   ↓
3. 테이블 선택 단계
   ├─ FROM 테이블 선택 (선택한 FROM 커넥션의 테이블들)
   └─ TO 테이블 선택 (선택한 TO 커넥션의 테이블들)
   ↓
4. 컬럼 매핑 단계 (액션 타입별 UI)
   ├─ INSERT: InsertFieldMappingPanel
   ├─ UPDATE: UpdateFieldMappingPanel
   └─ DELETE: DeleteConditionPanel
```

#### 새로운 컴포넌트 구조

```typescript
// 1. 커넥션 선택 컴포넌트 (신규)
interface ConnectionSelectionPanelProps {
  fromConnectionId?: number;
  toConnectionId?: number;
  onFromConnectionChange: (connectionId: number) => void;
  onToConnectionChange: (connectionId: number) => void;
  availableConnections: ExternalDbConnection[];
  actionType: "insert" | "update" | "delete";
  // 🆕 자기 자신 테이블 작업 지원
  allowSameConnection?: boolean;
  currentConnectionId?: number; // 현재 메인 DB 커넥션
}

// 2. 테이블 선택 컴포넌트 (확장)
interface TableSelectionPanelProps {
  fromConnectionId?: number;
  toConnectionId?: number;
  selectedFromTable?: string;
  selectedToTable?: string;
  onFromTableChange: (tableName: string) => void;
  onToTableChange: (tableName: string) => void;
  actionType: "insert" | "update" | "delete";
  // 🆕 자기 자신 테이블 작업 지원
  allowSameTable?: boolean;
  showSameTableWarning?: boolean;
}

// 3. 액션 타입별 매핑 컴포넌트 (확장)
interface InsertFieldMappingPanelProps {
  // INSERT: FROM → TO 매핑
}

interface UpdateFieldMappingPanelProps {
  // UPDATE: FROM 조건 + TO 업데이트 필드
  fromTableColumns: ColumnInfo[];
  toTableColumns: ColumnInfo[];
  updateConditions: UpdateCondition[];
  updateFields: UpdateFieldMapping[];
  onConditionsChange: (conditions: UpdateCondition[]) => void;
  onFieldsChange: (fields: UpdateFieldMapping[]) => void;
}

interface DeleteConditionPanelProps {
  // DELETE: FROM 조건 + TO 삭제 조건
  fromTableColumns: ColumnInfo[];
  toTableColumns: ColumnInfo[];
  deleteConditions: DeleteCondition[];
  onConditionsChange: (conditions: DeleteCondition[]) => void;
}
```

## 🔧 구현 세부 계획

### Phase 1: 백엔드 인프라 구축 (2주)

#### 1.1 외부 커넥션 조회 API 확장

```typescript
// 기존 API 확장
GET / api / external - db - connections / active;
// 응답: 활성화된 모든 커넥션 목록

GET / api / external - db - connections / { connectionId } / tables;
// 응답: 특정 커넥션의 테이블 목록

GET / api / external -
  db -
  connections / { connectionId } / tables / { tableName } / columns;
// 응답: 특정 테이블의 컬럼 정보
```

#### 1.2 다중 커넥션 쿼리 실행 서비스

```typescript
export class MultiConnectionQueryService {
  // 소스 커넥션에서 데이터 조회
  async fetchDataFromConnection(
    connectionId: number,
    tableName: string,
    conditions?: Record<string, any>
  ): Promise<Record<string, any>[]>;

  // 대상 커넥션에 데이터 삽입
  async insertDataToConnection(
    connectionId: number,
    tableName: string,
    data: Record<string, any>
  ): Promise<any>;

  // 🆕 대상 커넥션에 데이터 업데이트
  async updateDataToConnection(
    connectionId: number,
    tableName: string,
    data: Record<string, any>,
    conditions: Record<string, any>
  ): Promise<any>;

  // 🆕 대상 커넥션에서 데이터 삭제
  async deleteDataFromConnection(
    connectionId: number,
    tableName: string,
    conditions: Record<string, any>
  ): Promise<any>;

  // 커넥션별 테이블 목록 조회
  async getTablesFromConnection(connectionId: number): Promise<TableInfo[]>;

  // 커넥션별 컬럼 정보 조회
  async getColumnsFromConnection(
    connectionId: number,
    tableName: string
  ): Promise<ColumnInfo[]>;

  // 🆕 자기 자신 테이블 작업 전용 메서드들
  async validateSelfTableOperation(
    tableName: string,
    operation: "update" | "delete",
    conditions: any[]
  ): Promise<ValidationResult>;

  // 🆕 메인 DB 작업 (connectionId = 0인 경우)
  async executeOnMainDatabase(
    operation: "select" | "insert" | "update" | "delete",
    tableName: string,
    data?: Record<string, any>,
    conditions?: Record<string, any>
  ): Promise<any>;
}
```

#### 1.3 제어관리 서비스 확장

```typescript
export class EnhancedDataflowControlService {
  // 기존 메서드 확장
  async executeDataflowControl(
    diagramId: number,
    relationshipId: string,
    triggerType: "insert" | "update" | "delete",
    sourceData: Record<string, any>,
    tableName: string,
    // 🆕 추가 매개변수
    sourceConnectionId?: number,
    targetConnectionId?: number
  ): Promise<{
    success: boolean;
    message: string;
    executedActions?: any[];
    errors?: string[];
  }>;

  // 🆕 다중 커넥션 INSERT 실행
  private async executeMultiConnectionInsert(
    action: ControlAction,
    sourceData: Record<string, any>,
    sourceConnectionId?: number,
    targetConnectionId?: number
  ): Promise<any>;

  // 🆕 다중 커넥션 UPDATE 실행
  private async executeMultiConnectionUpdate(
    action: ControlAction,
    sourceData: Record<string, any>,
    sourceConnectionId?: number,
    targetConnectionId?: number
  ): Promise<any>;

  // 🆕 다중 커넥션 DELETE 실행
  private async executeMultiConnectionDelete(
    action: ControlAction,
    sourceData: Record<string, any>,
    sourceConnectionId?: number,
    targetConnectionId?: number
  ): Promise<any>;
}
```

### Phase 2: 프론트엔드 UI 개선 (3주)

#### 2.1 ConnectionSelectionPanel 컴포넌트 개발

```typescript
export const ConnectionSelectionPanel: React.FC<
  ConnectionSelectionPanelProps
> = ({
  fromConnectionId,
  toConnectionId,
  onFromConnectionChange,
  onToConnectionChange,
  availableConnections,
  actionType,
}) => {
  const getConnectionLabels = () => {
    switch (actionType) {
      case "insert":
        return {
          from: {
            title: "소스 데이터베이스 연결",
            desc: "데이터를 가져올 데이터베이스 연결을 선택하세요",
          },
          to: {
            title: "대상 데이터베이스 연결",
            desc: "데이터를 저장할 데이터베이스 연결을 선택하세요",
          },
        };
      case "update":
        return {
          from: {
            title: "조건 확인 데이터베이스",
            desc: "업데이트 조건을 확인할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
          },
          to: {
            title: "업데이트 대상 데이터베이스",
            desc: "데이터를 업데이트할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
          },
        };
      case "delete":
        return {
          from: {
            title: "조건 확인 데이터베이스",
            desc: "삭제 조건을 확인할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
          },
          to: {
            title: "삭제 대상 데이터베이스",
            desc: "데이터를 삭제할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
          },
        };
    }
  };

  // 🆕 자기 자신 테이블 작업 시 경고 메시지
  const getSameConnectionWarning = () => {
    if (fromConnectionId === toConnectionId && fromConnectionId) {
      switch (actionType) {
        case "update":
          return "⚠️ 같은 데이터베이스에서 UPDATE 작업을 수행합니다. 조건을 신중히 설정하세요.";
        case "delete":
          return "🚨 같은 데이터베이스에서 DELETE 작업을 수행합니다. 데이터 손실에 주의하세요.";
      }
    }
    return null;
  };

  const labels = getConnectionLabels();

  const warningMessage = getSameConnectionWarning();

  return (
    <div className="space-y-4">
      <div className="grid grid-cols-2 gap-6">
        {/* FROM 커넥션 선택 */}
        <Card>
          <CardHeader>
            <CardTitle>{labels.from.title}</CardTitle>
            <CardDescription>{labels.from.desc}</CardDescription>
          </CardHeader>
          <CardContent>
            <Select
              value={fromConnectionId?.toString() || ""}
              onValueChange={(value) => onFromConnectionChange(parseInt(value))}
            >
              <SelectTrigger>
                <SelectValue placeholder="커넥션을 선택하세요" />
              </SelectTrigger>
              <SelectContent>
                {/* 🆕 현재 메인 DB도 선택 가능 */}
                <SelectItem value="0">
                  <div className="flex items-center gap-2">
                    <Badge variant="default">현재 DB</Badge>
                    <span>메인 데이터베이스 (현재 시스템)</span>
                  </div>
                </SelectItem>
                {availableConnections.map((conn) => (
                  <SelectItem key={conn.id} value={conn.id!.toString()}>
                    <div className="flex items-center gap-2">
                      <Badge variant="outline">{conn.db_type}</Badge>
                      <span>{conn.connection_name}</span>
                    </div>
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </CardContent>
        </Card>

        {/* TO 커넥션 선택 */}
        <Card>
          <CardHeader>
            <CardTitle>{labels.to.title}</CardTitle>
            <CardDescription>{labels.to.desc}</CardDescription>
          </CardHeader>
          <CardContent>
            <Select
              value={toConnectionId?.toString() || ""}
              onValueChange={(value) => onToConnectionChange(parseInt(value))}
            >
              <SelectTrigger>
                <SelectValue placeholder="커넥션을 선택하세요" />
              </SelectTrigger>
              <SelectContent>
                {/* 🆕 현재 메인 DB도 선택 가능 */}
                <SelectItem value="0">
                  <div className="flex items-center gap-2">
                    <Badge variant="default">현재 DB</Badge>
                    <span>메인 데이터베이스 (현재 시스템)</span>
                  </div>
                </SelectItem>
                {availableConnections.map((conn) => (
                  <SelectItem key={conn.id} value={conn.id!.toString()}>
                    <div className="flex items-center gap-2">
                      <Badge variant="outline">{conn.db_type}</Badge>
                      <span>{conn.connection_name}</span>
                    </div>
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </CardContent>
        </Card>
      </div>

      {/* 🆕 자기 자신 테이블 작업 시 경고 */}
      {warningMessage && (
        <Alert variant={actionType === "delete" ? "destructive" : "default"}>
          <AlertCircle className="h-4 w-4" />
          <AlertTitle>주의사항</AlertTitle>
          <AlertDescription>{warningMessage}</AlertDescription>
        </Alert>
      )}
    </div>
  );
};
```

#### 2.2 TableSelectionPanel 컴포넌트 확장

```typescript
export const TableSelectionPanel: React.FC<TableSelectionPanelProps> = ({
  fromConnectionId,
  toConnectionId,
  selectedFromTable,
  selectedToTable,
  onFromTableChange,
  onToTableChange,
}) => {
  const [fromTables, setFromTables] = useState<TableInfo[]>([]);
  const [toTables, setToTables] = useState<TableInfo[]>([]);
  const [loading, setLoading] = useState(false);

  // 커넥션 변경 시 테이블 목록 로딩
  useEffect(() => {
    if (fromConnectionId) {
      loadTablesFromConnection(fromConnectionId, setFromTables);
    }
  }, [fromConnectionId]);

  useEffect(() => {
    if (toConnectionId) {
      loadTablesFromConnection(toConnectionId, setToTables);
    }
  }, [toConnectionId]);

  return (
    <div className="grid grid-cols-2 gap-6">
      {/* FROM 테이블 선택 */}
      <TableSelector
        title="소스 테이블"
        tables={fromTables}
        selectedTable={selectedFromTable}
        onTableChange={onFromTableChange}
        connectionId={fromConnectionId}
        disabled={!fromConnectionId}
      />

      {/* TO 테이블 선택 */}
      <TableSelector
        title="대상 테이블"
        tables={toTables}
        selectedTable={selectedToTable}
        onTableChange={onToTableChange}
        connectionId={toConnectionId}
        disabled={!toConnectionId}
      />
    </div>
  );
};
```

#### 2.3 InsertFieldMappingPanel 확장

```typescript
// 기존 컴포넌트에 커넥션 정보 추가
interface EnhancedInsertFieldMappingPanelProps
  extends InsertFieldMappingPanelProps {
  fromConnectionId?: number;
  toConnectionId?: number;
  fromConnectionName?: string;
  toConnectionName?: string;
}

// 컬럼 로딩 로직 수정
useEffect(() => {
  if (fromConnectionId && fromTableName) {
    loadColumnsFromConnection(fromConnectionId, fromTableName).then(
      setFromTableColumns
    );
  }
}, [fromConnectionId, fromTableName]);

useEffect(() => {
  if (toConnectionId && toTableName) {
    loadColumnsFromConnection(toConnectionId, toTableName).then(
      setToTableColumns
    );
  }
}, [toConnectionId, toTableName]);
```

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

#### 3.1 ActionFieldMappings 컴포넌트 통합

```typescript
export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
  action,
  actionIndex,
  settings,
  onSettingsChange,
  // ... 기존 props
}) => {
  const renderActionSpecificUI = () => {
    // 공통 단계: 커넥션 선택과 테이블 선택
    const commonSteps = (
      <>
        {/* 1단계: 커넥션 선택 */}
        <ConnectionSelectionPanel
          fromConnectionId={action.fromConnection?.connectionId}
          toConnectionId={action.toConnection?.connectionId}
          onFromConnectionChange={handleFromConnectionChange}
          onToConnectionChange={handleToConnectionChange}
          availableConnections={availableConnections}
          actionType={action.actionType}
        />

        {/* 2단계: 테이블 선택 */}
        {hasConnectionsSelected && (
          <TableSelectionPanel
            fromConnectionId={action.fromConnection?.connectionId}
            toConnectionId={action.toConnection?.connectionId}
            selectedFromTable={action.fromTable}
            selectedToTable={action.targetTable}
            onFromTableChange={handleFromTableChange}
            onToTableChange={handleToTableChange}
            actionType={action.actionType}
          />
        )}
      </>
    );

    // 3단계: 액션 타입별 매핑/조건 설정
    let specificPanel = null;
    if (hasTablesSelected) {
      switch (action.actionType) {
        case "insert":
          specificPanel = (
            <InsertFieldMappingPanel
              action={action}
              actionIndex={actionIndex}
              settings={settings}
              onSettingsChange={onSettingsChange}
              fromTableColumns={fromTableColumns}
              toTableColumns={toTableColumns}
              fromTableName={action.fromTable}
              toTableName={action.targetTable}
              fromConnectionId={action.fromConnection?.connectionId}
              toConnectionId={action.toConnection?.connectionId}
              fromConnectionName={action.fromConnection?.connectionName}
              toConnectionName={action.toConnection?.connectionName}
            />
          );
          break;

        case "update":
          specificPanel = (
            <UpdateFieldMappingPanel
              action={action}
              actionIndex={actionIndex}
              settings={settings}
              onSettingsChange={onSettingsChange}
              fromTableColumns={fromTableColumns}
              toTableColumns={toTableColumns}
              fromConnectionId={action.fromConnection?.connectionId}
              toConnectionId={action.toConnection?.connectionId}
            />
          );
          break;

        case "delete":
          specificPanel = (
            <DeleteConditionPanel
              action={action}
              actionIndex={actionIndex}
              settings={settings}
              onSettingsChange={onSettingsChange}
              fromTableColumns={fromTableColumns}
              toTableColumns={toTableColumns}
              fromConnectionId={action.fromConnection?.connectionId}
              toConnectionId={action.toConnection?.connectionId}
            />
          );
          break;
      }
    }

    return (
      <div className="space-y-6">
        {commonSteps}
        {specificPanel}
      </div>
    );
  };

  return renderActionSpecificUI();
};
```

## 🔄 액션 타입별 상세 구현

### 1. UPDATE 액션 구현

#### UpdateFieldMappingPanel 컴포넌트

```typescript
export const UpdateFieldMappingPanel: React.FC<
  UpdateFieldMappingPanelProps
> = ({
  action,
  actionIndex,
  settings,
  onSettingsChange,
  fromTableColumns,
  toTableColumns,
  fromConnectionId,
  toConnectionId,
}) => {
  const [updateConditions, setUpdateConditions] = useState<UpdateCondition[]>(
    []
  );
  const [updateFields, setUpdateFields] = useState<UpdateFieldMapping[]>([]);

  return (
    <div className="space-y-6">
      {/* UPDATE 조건 설정 */}
      <Card>
        <CardHeader>
          <CardTitle>🔍 업데이트 조건 설정</CardTitle>
          <CardDescription>
            FROM 테이블에서 어떤 조건을 만족하는 데이터가 있을 때 TO 테이블을
            업데이트할지 설정하세요
          </CardDescription>
        </CardHeader>
        <CardContent>
          <UpdateConditionBuilder
            fromTableColumns={fromTableColumns}
            conditions={updateConditions}
            onConditionsChange={setUpdateConditions}
          />
        </CardContent>
      </Card>

      {/* UPDATE 필드 매핑 */}
      <Card>
        <CardHeader>
          <CardTitle>📝 업데이트 필드 매핑</CardTitle>
          <CardDescription>
            FROM 테이블의 값을 TO 테이블의 어떤 필드에 업데이트할지 설정하세요
          </CardDescription>
        </CardHeader>
        <CardContent>
          <UpdateFieldMapper
            fromTableColumns={fromTableColumns}
            toTableColumns={toTableColumns}
            fieldMappings={updateFields}
            onFieldMappingsChange={setUpdateFields}
          />
        </CardContent>
      </Card>

      {/* WHERE 조건 설정 */}
      <Card>
        <CardHeader>
          <CardTitle>🎯 업데이트 대상 조건</CardTitle>
          <CardDescription>
            TO 테이블에서 어떤 레코드를 업데이트할지 WHERE 조건을 설정하세요
          </CardDescription>
        </CardHeader>
        <CardContent>
          <WhereConditionBuilder
            toTableColumns={toTableColumns}
            fromTableColumns={fromTableColumns}
            onConditionsChange={handleWhereConditionsChange}
          />
        </CardContent>
      </Card>
    </div>
  );
};
```

#### UPDATE 데이터 타입 정의

```typescript
interface UpdateCondition {
  id: string;
  fromColumn: string;
  operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
  value: string | string[];
  logicalOperator?: "AND" | "OR";
}

interface UpdateFieldMapping {
  id: string;
  fromColumn: string;
  toColumn: string;
  transformFunction?: string;
  defaultValue?: string;
}

interface WhereCondition {
  id: string;
  toColumn: string;
  operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
  valueSource: "from_column" | "static" | "current_timestamp";
  fromColumn?: string; // valueSource가 "from_column"인 경우
  staticValue?: string; // valueSource가 "static"인 경우
  logicalOperator?: "AND" | "OR";
}
```

### 2. DELETE 액션 구현

#### DeleteConditionPanel 컴포넌트

```typescript
export const DeleteConditionPanel: React.FC<DeleteConditionPanelProps> = ({
  action,
  actionIndex,
  settings,
  onSettingsChange,
  fromTableColumns,
  toTableColumns,
  fromConnectionId,
  toConnectionId,
}) => {
  const [deleteConditions, setDeleteConditions] = useState<DeleteCondition[]>(
    []
  );
  const [whereConditions, setWhereConditions] = useState<WhereCondition[]>([]);

  return (
    <div className="space-y-6">
      {/* DELETE 트리거 조건 설정 */}
      <Card>
        <CardHeader>
          <CardTitle>🔥 삭제 트리거 조건</CardTitle>
          <CardDescription>
            FROM 테이블에서 어떤 조건을 만족하는 데이터가 있을 때 TO 테이블에서
            삭제를 실행할지 설정하세요
          </CardDescription>
        </CardHeader>
        <CardContent>
          <DeleteTriggerConditionBuilder
            fromTableColumns={fromTableColumns}
            conditions={deleteConditions}
            onConditionsChange={setDeleteConditions}
          />
        </CardContent>
      </Card>

      {/* DELETE WHERE 조건 설정 */}
      <Card>
        <CardHeader>
          <CardTitle>🎯 삭제 대상 조건</CardTitle>
          <CardDescription>
            TO 테이블에서 어떤 레코드를 삭제할지 WHERE 조건을 설정하세요
          </CardDescription>
        </CardHeader>
        <CardContent>
          <DeleteWhereConditionBuilder
            toTableColumns={toTableColumns}
            fromTableColumns={fromTableColumns}
            conditions={whereConditions}
            onConditionsChange={setWhereConditions}
          />
        </CardContent>
      </Card>

      {/* 안전장치 설정 */}
      <Card>
        <CardHeader>
          <CardTitle>🛡️ 삭제 안전장치</CardTitle>
          <CardDescription>
            예상치 못한 대량 삭제를 방지하기 위한 안전장치를 설정하세요
          </CardDescription>
        </CardHeader>
        <CardContent>
          <DeleteSafetySettings
            maxDeleteCount={action.maxDeleteCount || 100}
            requireConfirmation={action.requireConfirmation || true}
            onSettingsChange={handleSafetySettingsChange}
          />
        </CardContent>
      </Card>
    </div>
  );
};
```

#### DELETE 데이터 타입 정의

```typescript
interface DeleteCondition {
  id: string;
  fromColumn: string;
  operator:
    | "="
    | "!="
    | ">"
    | "<"
    | ">="
    | "<="
    | "LIKE"
    | "IN"
    | "NOT IN"
    | "EXISTS"
    | "NOT EXISTS";
  value: string | string[];
  logicalOperator?: "AND" | "OR";
}

interface DeleteWhereCondition {
  id: string;
  toColumn: string;
  operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
  valueSource: "from_column" | "static" | "condition_result";
  fromColumn?: string;
  staticValue?: string;
  logicalOperator?: "AND" | "OR";
}

interface DeleteSafetySettings {
  maxDeleteCount: number;
  requireConfirmation: boolean;
  dryRunFirst: boolean;
  logAllDeletes: boolean;
}
```

## 🔒 매핑 규칙 구현

### 1. INSERT: FROM → TO 컬럼 매핑 제약사항

#### 허용되는 매핑 (기존과 동일)

```typescript
// ✅ 1:1 매핑
FROM.column1 → TO.column1

// ✅ 1:N 매핑 (하나의 FROM 컬럼이 여러 TO 컬럼에 매핑)
FROM.column1 → TO.column1
FROM.column1 → TO.column2
FROM.column1 → TO.column3
```

#### 금지되는 매핑 (신규 검증 로직)

```typescript
// ❌ N:1 매핑 (여러 FROM 컬럼이 하나의 TO 컬럼에 매핑)
FROM.column1 → TO.column1
FROM.column2 → TO.column1  // 이미 매핑된 TO.column1에 추가 매핑 시도
```

### 2. UPDATE: 조건 및 필드 매핑 제약사항

#### 허용되는 UPDATE 패턴

```typescript
// ✅ 조건부 업데이트
IF (FROM.status = 'completed')
THEN UPDATE TO.table SET status = FROM.new_status WHERE TO.id = FROM.ref_id

// ✅ 다중 필드 업데이트
UPDATE TO.table SET
  column1 = FROM.value1,
  column2 = FROM.value2,
  updated_at = CURRENT_TIMESTAMP
WHERE TO.id = FROM.ref_id

// ✅ 조건부 필드 매핑
IF (FROM.priority > 5) THEN TO.urgent_flag = 'Y'
ELSE TO.urgent_flag = 'N'
```

#### UPDATE 제약사항

```typescript
// ❌ WHERE 조건 없는 전체 테이블 업데이트 (안전장치)
// ❌ PRIMARY KEY 컬럼 업데이트
// ⚠️  자기 자신 테이블 업데이트 (허용하되 특별한 주의사항)

// 🆕 자기 자신 테이블 UPDATE 시 안전장치
const validateSelfTableUpdate = (
  fromTable: string,
  toTable: string,
  updateConditions: UpdateCondition[],
  whereConditions: WhereCondition[]
): ValidationResult => {
  if (fromTable === toTable) {
    // 1. WHERE 조건 필수
    if (!whereConditions.length) {
      return {
        isValid: false,
        error: "자기 자신 테이블 업데이트 시 WHERE 조건이 필수입니다.",
      };
    }

    // 2. 업데이트 조건과 WHERE 조건이 겹치지 않도록 체크
    const conditionColumns = updateConditions.map((c) => c.fromColumn);
    const whereColumns = whereConditions.map((c) => c.toColumn);
    const overlap = conditionColumns.filter((col) =>
      whereColumns.includes(col)
    );

    if (overlap.length > 0) {
      return {
        isValid: false,
        error: `업데이트 조건과 WHERE 조건에서 같은 컬럼(${overlap.join(
          ", "
        )})을 사용하면 예상치 못한 결과가 발생할 수 있습니다.`,
      };
    }

    // 3. 무한 루프 방지 체크
    const hasInfiniteLoopRisk = updateConditions.some((condition) =>
      whereConditions.some(
        (where) =>
          where.fromColumn === condition.toColumn &&
          where.toColumn === condition.fromColumn
      )
    );

    if (hasInfiniteLoopRisk) {
      return {
        isValid: false,
        error: "자기 참조 업데이트로 인한 무한 루프 위험이 있습니다.",
      };
    }
  }

  return { isValid: true };
};
```

### 3. DELETE: 조건 및 안전장치 제약사항

#### 허용되는 DELETE 패턴

```typescript
// ✅ 조건부 삭제
IF (FROM.is_expired = 'Y')
THEN DELETE FROM TO.table WHERE TO.ref_id = FROM.id

// ✅ 관련 데이터 정리
IF (FROM.status = 'cancelled')
THEN DELETE FROM TO.order_items WHERE TO.order_id = FROM.order_id

// ✅ 카스케이드 삭제 시뮬레이션
DELETE FROM TO.child_table WHERE TO.parent_id = FROM.deleted_id
```

#### DELETE 제약사항 및 안전장치

```typescript
// ❌ WHERE 조건 없는 전체 테이블 삭제 (강력한 안전장치)
// ❌ 일정 개수 이상의 대량 삭제 (maxDeleteCount 제한)
// ⚠️  외래키 제약조건 위반 가능성 체크
// ⚠️  자기 자신 테이블 삭제 (허용하되 특별한 주의사항)

const validateDeleteSafety = (
  fromTable: string,
  toTable: string,
  deleteConditions: DeleteCondition[],
  whereConditions: WhereCondition[],
  safetySettings: DeleteSafetySettings
): ValidationResult => {
  // 1. WHERE 조건 필수 체크
  if (!whereConditions.length) {
    return {
      isValid: false,
      error: "DELETE 작업에는 반드시 WHERE 조건이 필요합니다.",
    };
  }

  // 2. 대량 삭제 제한 체크
  if (safetySettings.maxDeleteCount < 1) {
    return {
      isValid: false,
      error: "최대 삭제 개수는 1 이상이어야 합니다.",
    };
  }

  // 🆕 3. 자기 자신 테이블 삭제 시 추가 안전장치
  if (fromTable === toTable) {
    // 강화된 안전장치: 더 엄격한 제한
    const selfDeleteMaxCount = Math.min(safetySettings.maxDeleteCount, 10);

    if (safetySettings.maxDeleteCount > selfDeleteMaxCount) {
      return {
        isValid: false,
        error: `자기 자신 테이블 삭제 시 최대 ${selfDeleteMaxCount}개까지만 허용됩니다.`,
      };
    }

    // 삭제 조건이 너무 광범위한지 체크
    const hasBroadCondition = deleteConditions.some(
      (condition) =>
        condition.operator === "!=" ||
        condition.operator === "NOT IN" ||
        condition.operator === "NOT EXISTS"
    );

    if (hasBroadCondition) {
      return {
        isValid: false,
        error:
          "자기 자신 테이블 삭제 시 부정 조건(!=, NOT IN, NOT EXISTS)은 위험합니다.",
      };
    }

    // WHERE 조건이 충분히 구체적인지 체크
    if (whereConditions.length < 2) {
      return {
        isValid: false,
        error:
          "자기 자신 테이블 삭제 시 WHERE 조건을 2개 이상 설정하는 것을 권장합니다.",
      };
    }
  }

  return { isValid: true };
};

// 🆕 자기 자신 테이블 작업 시 실제 사용 예시
const exampleSelfTableOperations = {
  // ✅ 안전한 자기 자신 테이블 UPDATE
  safeUpdate: `
    UPDATE user_info 
    SET last_login = NOW(), login_count = login_count + 1 
    WHERE user_id = 'specific_user' AND status = 'active'
  `,

  // ✅ 안전한 자기 자신 테이블 DELETE
  safeDelete: `
    DELETE FROM temp_data 
    WHERE created_at < NOW() - INTERVAL '7 days' 
    AND status = 'processed' 
    AND batch_id = 'specific_batch'
    LIMIT 10
  `,

  // ❌ 위험한 작업들
  dangerousOperations: [
    "UPDATE table SET column = value (WHERE 조건 없음)",
    "DELETE FROM table WHERE status != 'active' (부정 조건으로 예상보다 많이 삭제될 수 있음)",
    "UPDATE table SET id = new_id WHERE id = old_id (키 값 변경으로 참조 무결성 위험)",
  ],
};
```

### 4. 공통 검증 로직

#### 매핑 제약사항 통합 검증

```typescript
const validateMappingConstraints = (
  actionType: "insert" | "update" | "delete",
  newMapping: ColumnMapping,
  existingMappings: ColumnMapping[]
): ValidationResult => {
  switch (actionType) {
    case "insert":
      return validateInsertMapping(newMapping, existingMappings);
    case "update":
      return validateUpdateMapping(newMapping, existingMappings);
    case "delete":
      return validateDeleteConditions(newMapping, existingMappings);
  }
};

const validateInsertMapping = (
  newMapping: ColumnMapping,
  existingMappings: ColumnMapping[]
): ValidationResult => {
  // TO 컬럼이 이미 다른 FROM 컬럼과 매핑되어 있는지 확인
  const existingToMapping = existingMappings.find(
    (mapping) => mapping.toColumnName === newMapping.toColumnName
  );

  if (
    existingToMapping &&
    existingToMapping.fromColumnName &&
    existingToMapping.fromColumnName !== newMapping.fromColumnName
  ) {
    return {
      isValid: false,
      error: `대상 컬럼 '${newMapping.toColumnName}'은 이미 '${existingToMapping.fromColumnName}'과 매핑되어 있습니다.`,
    };
  }

  return { isValid: true };
};
```

### 2. UI에서의 제약사항 표시

#### 컬럼 선택 시 비활성화 로직

```typescript
const isToColumnClickable = (toColumn: ColumnInfo) => {
  const currentMapping = columnMappings.find(
    (m) => m.toColumnName === toColumn.columnName
  );

  // 이미 다른 FROM 컬럼과 매핑된 경우 클릭 불가
  if (currentMapping?.fromColumnName) {
    return false;
  }

  // 기본값이 설정된 경우 클릭 불가
  if (currentMapping?.defaultValue && currentMapping.defaultValue.trim()) {
    return false;
  }

  // 데이터 타입 호환성 체크
  if (!selectedFromColumn) return true;

  const fromColumn = fromTableColumns.find(
    (col) => col.columnName === selectedFromColumn
  );
  if (!fromColumn) return true;

  return fromColumn.dataType === toColumn.dataType;
};
```

#### 시각적 피드백

```typescript
// TO 컬럼 렌더링 시 상태 표시
const getToColumnStatus = (toColumn: ColumnInfo) => {
  const mapping = columnMappings.find(
    (m) => m.toColumnName === toColumn.columnName
  );

  if (mapping?.fromColumnName) {
    return {
      status: "mapped",
      color: "bg-green-100 border-green-300",
      icon: "🔗",
      label: `← ${mapping.fromColumnName}`,
    };
  }

  if (mapping?.defaultValue) {
    return {
      status: "default",
      color: "bg-blue-100 border-blue-300",
      icon: "📝",
      label: `기본값: ${mapping.defaultValue}`,
    };
  }

  return {
    status: "unmapped",
    color: "bg-gray-100 border-gray-300",
    icon: "⚪",
    label: "미설정",
  };
};
```

## 📊 데이터 플로우

### 1. 설정 저장 플로우

```
사용자 설정 입력
    ↓
ConnectionSelectionPanel → 커넥션 ID 저장
    ↓
TableSelectionPanel → 테이블명 저장
    ↓
InsertFieldMappingPanel → 필드 매핑 저장
    ↓
DataSaveSettings 업데이트
    ↓
dataflow_diagrams.plan 필드에 JSON 저장
```

### 2. 실행 플로우

#### INSERT 실행 플로우

```
제어관리 트리거 발생 (INSERT)
    ↓
EnhancedDataflowControlService.executeDataflowControl()
    ↓
소스 커넥션에서 데이터 조회 (MultiConnectionQueryService.fetchDataFromConnection)
    ↓
필드 매핑 규칙 적용 (1:N 매핑 지원)
    ↓
대상 커넥션에 데이터 삽입 (MultiConnectionQueryService.insertDataToConnection)
    ↓
결과 반환
```

#### UPDATE 실행 플로우

```
제어관리 트리거 발생 (UPDATE)
    ↓
EnhancedDataflowControlService.executeDataflowControl()
    ↓
소스 커넥션에서 조건 데이터 조회 (UPDATE 조건 확인)
    ↓
조건 만족 시 FROM 데이터 추출
    ↓
필드 매핑 규칙 적용 (FROM → TO 필드 매핑)
    ↓
WHERE 조건 생성 (TO 테이블 대상 레코드 식별)
    ↓
대상 커넥션에서 데이터 업데이트 (MultiConnectionQueryService.updateDataToConnection)
    ↓
결과 반환
```

#### DELETE 실행 플로우

```
제어관리 트리거 발생 (DELETE)
    ↓
EnhancedDataflowControlService.executeDataflowControl()
    ↓
소스 커넥션에서 삭제 트리거 조건 확인
    ↓
조건 만족 시 삭제 대상 식별
    ↓
안전장치 검증 (maxDeleteCount, WHERE 조건 필수)
    ↓
WHERE 조건 생성 (TO 테이블 삭제 대상 레코드)
    ↓
[dryRunFirst=true인 경우] 삭제 예상 개수 확인
    ↓
대상 커넥션에서 데이터 삭제 (MultiConnectionQueryService.deleteDataFromConnection)
    ↓
삭제 로그 기록 (logAllDeletes=true인 경우)
    ↓
결과 반환
```

## 🛠️ 기술적 고려사항

### 1. 성능 최적화

- **커넥션 풀링**: 외부 DB별 커넥션 풀 관리
- **캐싱**: 테이블/컬럼 정보 캐싱 (Redis 활용)
- **비동기 처리**: 대용량 데이터 처리 시 큐잉 시스템 활용

### 2. 보안 강화

- **커넥션 정보 암호화**: 기존 시스템과 동일한 수준 유지
- **접근 권한 관리**: 회사별 커넥션 접근 제어
- **감사 로깅**: 모든 외부 DB 접근 기록

### 3. 오류 처리

```typescript
export class ConnectionError extends Error {
  constructor(
    message: string,
    public connectionId: number,
    public originalError?: Error
  ) {
    super(message);
    this.name = "ConnectionError";
  }
}

export class MappingValidationError extends Error {
  constructor(message: string, public mappingErrors: ValidationError[]) {
    super(message);
    this.name = "MappingValidationError";
  }
}
```

### 4. 호환성 유지

- **기존 설정 마이그레이션**: 기존 제어관리 설정을 새 구조로 자동 변환
- **점진적 전환**: 기존 기능 유지하면서 새 기능 추가
- **롤백 계획**: 문제 발생 시 이전 버전으로 복원 가능

## 📅 일정 계획

### Week 1-2: 백엔드 인프라

- [ ] MultiConnectionQueryService 개발
- [ ] 외부 커넥션 API 확장
- [ ] EnhancedDataflowControlService 개발

### Week 3-4: 프론트엔드 UI

- [ ] ConnectionSelectionPanel 개발 (액션 타입별 라벨링)
- [ ] TableSelectionPanel 개발 (액션 타입 지원)
- [ ] InsertFieldMappingPanel 확장
- [ ] UpdateFieldMappingPanel 개발
- [ ] DeleteConditionPanel 개발

### Week 5: 통합 및 테스트

- [ ] 컴포넌트 통합
- [ ] 매핑 제약사항 검증 로직
- [ ] 종합 테스트

### Week 6: 문서화 및 배포

- [ ] 사용자 가이드 작성
- [ ] 개발자 문서 업데이트
- [ ] 배포 및 사용자 교육

## 🎯 성공 지표

### 기능적 지표

- ✅ 다양한 외부 DB에 INSERT/UPDATE/DELETE 성공률 > 95%
- ✅ 매핑 제약사항 검증 정확도 100%
- ✅ DELETE 안전장치 동작률 100%
- ✅ 기존 제어관리 기능 호환성 100%

### 성능 지표

- ✅ 커넥션 설정 UI 응답 시간 < 2초
- ✅ 테이블/컬럼 로딩 시간 < 3초
- ✅ INSERT/UPDATE/DELETE 처리 시간 < 5초
- ✅ 대량 DELETE 검증 시간 < 3초

### 사용성 지표

- ✅ 설정 완료까지 필요한 클릭 수 < 10회
- ✅ 매핑 오류 발생 시 명확한 안내 메시지 제공
- ✅ 기존 사용자의 학습 비용 최소화

## 💡 자기 자신 테이블 작업 실제 사용 케이스

### 1. UPDATE 사용 케이스

#### 케이스 1: 사용자 로그인 정보 업데이트

```sql
-- 트리거: 사용자가 로그인할 때
-- FROM: login_logs 테이블에서 최근 로그인 기록 확인
-- TO: user_info 테이블의 last_login, login_count 업데이트

IF (login_logs.status = 'success' AND login_logs.created_at > NOW() - INTERVAL '1 minute')
THEN UPDATE user_info
     SET last_login = login_logs.created_at,
         login_count = login_count + 1,
         updated_at = NOW()
     WHERE user_info.user_id = login_logs.user_id
```

#### 케이스 2: 재고 수량 실시간 업데이트

```sql
-- 트리거: 주문이 완료될 때
-- FROM: order_items 테이블에서 주문 수량 확인
-- TO: product_inventory 테이블의 재고 수량 차감

IF (order_items.status = 'confirmed')
THEN UPDATE product_inventory
     SET current_stock = current_stock - order_items.quantity,
         last_updated = NOW()
     WHERE product_inventory.product_id = order_items.product_id
```

### 2. DELETE 사용 케이스

#### 케이스 1: 임시 데이터 자동 정리

```sql
-- 트리거: 배치 작업 완료 시
-- FROM: batch_jobs 테이블에서 완료된 작업 확인
-- TO: temp_processing_data 테이블의 임시 데이터 삭제

IF (batch_jobs.status = 'completed' AND batch_jobs.completed_at < NOW() - INTERVAL '1 hour')
THEN DELETE FROM temp_processing_data
     WHERE temp_processing_data.batch_id = batch_jobs.batch_id
     AND temp_processing_data.status = 'processed'
     LIMIT 100
```

#### 케이스 2: 만료된 세션 정리

```sql
-- 트리거: 시스템 정리 작업 시
-- FROM: user_sessions 테이블에서 만료된 세션 확인
-- TO: user_sessions 테이블에서 만료된 세션 삭제

IF (user_sessions.last_activity < NOW() - INTERVAL '24 hours')
THEN DELETE FROM user_sessions
     WHERE user_sessions.last_activity < NOW() - INTERVAL '24 hours'
     AND user_sessions.status = 'inactive'
     LIMIT 50
```

### 3. 복합 시나리오

#### 케이스 3: 주문 상태 변경에 따른 연쇄 업데이트

```sql
-- 1단계: 주문 상태 업데이트
UPDATE orders SET status = 'shipped', shipped_at = NOW()
WHERE order_id = 'ORD001' AND status = 'processing'

-- 2단계: 배송 정보 생성 (INSERT)
INSERT INTO shipping_info (order_id, tracking_number, created_at)
VALUES ('ORD001', 'TRACK001', NOW())

-- 3단계: 고객 주문 이력 업데이트
UPDATE customer_stats
SET total_orders = total_orders + 1, last_order_date = NOW()
WHERE customer_id = (SELECT customer_id FROM orders WHERE order_id = 'ORD001')
```

## 🚀 향후 확장 계획

### Phase 4: 고급 기능

- **데이터 변환 함수**: 필드 매핑 시 커스텀 변환 로직 지원
- **배치 처리**: 대용량 데이터 일괄 처리
- **스케줄링**: 정기적 데이터 동기화
- **🆕 자기 자신 테이블 트랜잭션**: 복잡한 자기 참조 작업의 원자성 보장

### Phase 5: 모니터링

- **실시간 모니터링**: 외부 DB 연결 상태 실시간 추적
- **성능 분석**: 쿼리 실행 시간 및 리소스 사용량 분석
- **알림 시스템**: 오류 발생 시 자동 알림
- **🆕 자기 자신 테이블 작업 감시**: 위험한 자기 참조 작업 모니터링

### Phase 6: 안전성 강화

- **🆕 Dry Run 모드**: 실제 실행 전 결과 예측
- **🆕 롤백 시스템**: 자기 자신 테이블 작업 시 자동 백업 및 복원
- **🆕 단계별 승인**: 위험한 자기 참조 작업에 대한 관리자 승인 프로세스

이 계획서를 바탕으로 체계적이고 안전한 제어관리 기능 개선을 진행할 수 있습니다.
