Files
vexplor/제어관리_외부커넥션_통합_개선_계획서.md

43 KiB

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

📋 프로젝트 개요

목적

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

현재 상황 분석

기존 외부 커넥션 관리

  • 테이블: 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 구조

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

개선된 DataSaveSettings 구조

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

새로운 컴포넌트 구조

// 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 확장

// 기존 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 다중 커넥션 쿼리 실행 서비스

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 제어관리 서비스 확장

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 컴포넌트 개발

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 컴포넌트 확장

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 확장

// 기존 컴포넌트에 커넥션 정보 추가
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 컴포넌트 통합

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 컴포넌트

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 데이터 타입 정의

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 컴포넌트

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 데이터 타입 정의

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 컬럼 매핑 제약사항

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

// ✅ 1:1 매핑
FROM.column1  TO.column1

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

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

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

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

허용되는 UPDATE 패턴

// ✅ 조건부 업데이트
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 제약사항

// ❌ 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 패턴

// ✅ 조건부 삭제
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 제약사항 및 안전장치

// ❌ 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. 공통 검증 로직

매핑 제약사항 통합 검증

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에서의 제약사항 표시

컬럼 선택 시 비활성화 로직

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;
};

시각적 피드백

// 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. 오류 처리

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: 사용자 로그인 정보 업데이트

-- 트리거: 사용자가 로그인할 때
-- 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: 재고 수량 실시간 업데이트

-- 트리거: 주문이 완료될 때
-- 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: 임시 데이터 자동 정리

-- 트리거: 배치 작업 완료 시
-- 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: 만료된 세션 정리

-- 트리거: 시스템 정리 작업 시
-- 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: 주문 상태 변경에 따른 연쇄 업데이트

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

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