import { query, queryOne } from "../database/db"; export interface ControlCondition { id: string; type: "condition" | "group-start" | "group-end"; field?: string; value?: any; operator?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN"; dataType?: "string" | "number" | "date" | "boolean"; logicalOperator?: "AND" | "OR"; groupId?: string; groupLevel?: number; tableType?: "from" | "to"; } export interface ControlAction { id: string; name: string; actionType: "insert" | "update" | "delete"; logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외) conditions: ControlCondition[]; fieldMappings: { sourceField?: string; sourceTable?: string; targetField: string; targetTable: string; defaultValue?: any; }[]; splitConfig?: { delimiter?: string; sourceField?: string; targetField?: string; }; // 🆕 다중 커넥션 지원 추가 fromConnection?: { id: number; name?: string; }; toConnection?: { id: number; name?: string; }; targetTable?: string; } export interface ControlPlan { id: string; sourceTable: string; actions: ControlAction[]; } export interface ControlRule { id: string; triggerType: "insert" | "update" | "delete"; conditions: ControlCondition[]; } export class DataflowControlService { /** * 제어관리 실행 메인 함수 */ async executeDataflowControl( diagramId: number, relationshipId: string, triggerType: "insert" | "update" | "delete", sourceData: Record, tableName: string, userId: string = "system" ): Promise<{ success: boolean; message: string; executedActions?: any[]; errors?: string[]; }> { try { console.log(`🎯 제어관리 실행 시작:`, { diagramId, relationshipId, triggerType, sourceData, tableName, userId, }); // 관계도 정보 조회 const diagram = await queryOne( `SELECT * FROM dataflow_diagrams WHERE diagram_id = $1`, [diagramId] ); if (!diagram) { return { success: false, message: `관계도를 찾을 수 없습니다. (ID: ${diagramId})`, }; } // 제어 규칙과 실행 계획 추출 (기존 구조 + redesigned UI 구조 지원) let controlRules: ControlRule[] = []; let executionPlans: ControlPlan[] = []; // 🆕 redesigned UI 구조 처리 if (diagram.relationships && typeof diagram.relationships === "object") { const relationships = diagram.relationships as any; // Case 1: redesigned UI 단일 관계 구조 if (relationships.controlConditions && relationships.fieldMappings) { console.log("🔄 Redesigned UI 구조 감지, 기존 구조로 변환 중"); // redesigned → 기존 구조 변환 controlRules = [ { id: relationshipId, triggerType: triggerType, conditions: relationships.controlConditions || [], }, ]; executionPlans = [ { id: relationshipId, sourceTable: relationships.fromTable || tableName, actions: [ { id: "action_1", name: "액션 1", actionType: relationships.actionType || "insert", conditions: relationships.actionConditions || [], fieldMappings: relationships.fieldMappings || [], fromConnection: relationships.fromConnection, toConnection: relationships.toConnection, targetTable: relationships.toTable, }, ], }, ]; console.log("✅ Redesigned → 기존 구조 변환 완료"); } } // 기존 구조 처리 (하위 호환성) if (controlRules.length === 0) { controlRules = Array.isArray(diagram.control) ? (diagram.control as unknown as ControlRule[]) : []; executionPlans = Array.isArray(diagram.plan) ? (diagram.plan as unknown as ControlPlan[]) : []; } console.log(`📋 제어 규칙:`, controlRules); console.log(`📋 실행 계획:`, executionPlans); // 해당 관계의 제어 규칙 찾기 const targetRule = controlRules.find( (rule) => rule.id === relationshipId && rule.triggerType === triggerType ); if (!targetRule) { console.log( `⚠️ 해당 관계의 제어 규칙을 찾을 수 없습니다: ${relationshipId}` ); return { success: true, message: "해당 관계의 제어 규칙이 없습니다.", }; } // 제어 조건 검증 const conditionResult = await this.evaluateConditions( targetRule.conditions, sourceData ); console.log(`🔍 [전체 실행 조건] 검증 결과:`, conditionResult); if (!conditionResult.satisfied) { return { success: true, message: `제어 조건을 만족하지 않습니다: ${conditionResult.reason}`, }; } // 실행 계획 찾기 const targetPlan = executionPlans.find( (plan) => plan.id === relationshipId ); if (!targetPlan) { return { success: true, message: "실행할 계획이 없습니다.", }; } // 액션 실행 (논리 연산자 지원) const executedActions = []; const errors = []; let previousActionSuccess = false; let shouldSkipRemainingActions = false; for (let i = 0; i < targetPlan.actions.length; i++) { const action = targetPlan.actions[i]; try { // 논리 연산자에 따른 실행 여부 결정 if ( i > 0 && action.logicalOperator === "OR" && previousActionSuccess ) { console.log( `⏭️ OR 조건으로 인해 액션 건너뛰기: ${action.name} (이전 액션 성공)` ); continue; } if (shouldSkipRemainingActions && action.logicalOperator === "AND") { console.log( `⏭️ 이전 액션 실패로 인해 AND 체인 액션 건너뛰기: ${action.name}` ); continue; } console.log(`⚡ 액션 실행: ${action.name} (${action.actionType})`); console.log(`📋 액션 상세 정보:`, { actionId: action.id, actionName: action.name, actionType: action.actionType, logicalOperator: action.logicalOperator, conditions: action.conditions, fieldMappings: action.fieldMappings, fromConnection: (action as any).fromConnection, toConnection: (action as any).toConnection, targetTable: (action as any).targetTable, }); // 🆕 다중 커넥션 지원 액션 실행 const actionResult = await this.executeMultiConnectionAction( action, sourceData, targetPlan.sourceTable, userId ); executedActions.push({ actionId: action.id, actionName: action.name, actionType: action.actionType, result: actionResult, timestamp: new Date().toISOString(), }); previousActionSuccess = actionResult?.success !== false; // 액션 조건 검증은 이미 위에서 처리됨 (중복 제거) } catch (error) { console.error(`❌ 액션 실행 오류: ${action.name}`, error); const errorMessage = error instanceof Error ? error.message : String(error); errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`); previousActionSuccess = false; if (action.logicalOperator === "AND") { shouldSkipRemainingActions = true; } } } return { success: true, message: `제어관리 실행 완료. ${executedActions.length}개 액션 실행됨.`, executedActions, errors: errors.length > 0 ? errors : undefined, }; } catch (error) { console.error("❌ 제어관리 실행 오류:", error); const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, message: `제어관리 실행 중 오류 발생: ${errorMessage}`, }; } } /** * 🆕 다중 커넥션 액션 실행 */ private async executeMultiConnectionAction( action: ControlAction, sourceData: Record, sourceTable: string, userId: string = "system" ): Promise { try { const extendedAction = action as any; // redesigned UI 구조 접근 // 연결 정보 추출 const fromConnection = extendedAction.fromConnection || { id: 0 }; const toConnection = extendedAction.toConnection || { id: 0 }; const targetTable = extendedAction.targetTable || sourceTable; console.log(`🔗 다중 커넥션 액션 실행:`, { actionType: action.actionType, fromConnectionId: fromConnection.id, toConnectionId: toConnection.id, sourceTable, targetTable, }); // MultiConnectionQueryService import 필요 const { MultiConnectionQueryService } = await import( "./multiConnectionQueryService" ); const multiConnService = new MultiConnectionQueryService(); switch (action.actionType) { case "insert": return await this.executeMultiConnectionInsert( action, sourceData, sourceTable, targetTable, fromConnection.id, toConnection.id, multiConnService, userId ); case "update": return await this.executeMultiConnectionUpdate( action, sourceData, sourceTable, targetTable, fromConnection.id, toConnection.id, multiConnService, userId ); case "delete": return await this.executeMultiConnectionDelete( action, sourceData, sourceTable, targetTable, fromConnection.id, toConnection.id, multiConnService, userId ); default: throw new Error(`지원되지 않는 액션 타입: ${action.actionType}`); } } catch (error) { console.error(`❌ 다중 커넥션 액션 실행 실패:`, error); return { success: false, message: error instanceof Error ? error.message : String(error), }; } } /** * 🆕 다중 커넥션 INSERT 실행 */ private async executeMultiConnectionInsert( action: ControlAction, sourceData: Record, sourceTable: string, targetTable: string, fromConnectionId: number, toConnectionId: number, multiConnService: any, userId: string = "system" ): Promise { try { // 필드 매핑 적용 const mappedData: Record = {}; for (const mapping of action.fieldMappings) { const sourceField = mapping.sourceField; const targetField = mapping.targetField; if (mapping.defaultValue !== undefined) { // 기본값 사용 mappedData[targetField] = mapping.defaultValue; } else if (sourceField && sourceData[sourceField] !== undefined) { // 소스 데이터에서 매핑 mappedData[targetField] = sourceData[sourceField]; } } // 🆕 변경자 정보 추가 if (!mappedData.created_by) { mappedData.created_by = userId; } if (!mappedData.updated_by) { mappedData.updated_by = userId; } console.log(`📋 매핑된 데이터:`, mappedData); // 대상 연결에 데이터 삽입 const result = await multiConnService.insertDataToConnection( toConnectionId, targetTable, mappedData ); return { success: true, message: `${targetTable}에 데이터 삽입 완료`, insertedCount: 1, data: result, }; } catch (error) { console.error(`❌ INSERT 실행 실패:`, error); return { success: false, message: error instanceof Error ? error.message : String(error), }; } } /** * 🆕 다중 커넥션 UPDATE 실행 */ private async executeMultiConnectionUpdate( action: ControlAction, sourceData: Record, sourceTable: string, targetTable: string, fromConnectionId: number, toConnectionId: number, multiConnService: any, userId: string = "system" ): Promise { try { // 필드 매핑 적용 const mappedData: Record = {}; for (const mapping of action.fieldMappings) { const sourceField = mapping.sourceField; const targetField = mapping.targetField; if (mapping.defaultValue !== undefined) { mappedData[targetField] = mapping.defaultValue; } else if (sourceField && sourceData[sourceField] !== undefined) { mappedData[targetField] = sourceData[sourceField]; } } // 🆕 변경자 정보 추가 if (!mappedData.updated_by) { mappedData.updated_by = userId; } console.log(`📋 UPDATE 매핑된 데이터:`, mappedData); console.log(`⚠️ UPDATE 액션은 향후 구현 예정`); return { success: true, message: "UPDATE 액션 실행됨 (향후 구현)", updatedCount: 0, }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : String(error), }; } } /** * 🆕 다중 커넥션 DELETE 실행 */ private async executeMultiConnectionDelete( action: ControlAction, sourceData: Record, sourceTable: string, targetTable: string, fromConnectionId: number, toConnectionId: number, multiConnService: any, userId: string = "system" ): Promise { try { console.log(`⚠️ DELETE 액션은 향후 구현 예정 (변경자: ${userId})`); return { success: true, message: "DELETE 액션 실행됨 (향후 구현)", deletedCount: 0, }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : String(error), }; } } /** * 액션별 조건 평가 (동적 테이블 지원) */ private async evaluateActionConditions( action: ControlAction, sourceData: Record, sourceTable: string ): Promise<{ satisfied: boolean; reason?: string }> { if (!action.conditions || action.conditions.length === 0) { return { satisfied: true }; } try { // 조건별로 테이블 타입에 따라 데이터 소스 결정 for (const condition of action.conditions) { if (!condition.field || condition.value === undefined) { continue; } let dataToCheck: Record; let tableName: string; // UPDATE/DELETE 액션의 경우 조건은 항상 대상 테이블에서 확인 (업데이트/삭제할 기존 데이터를 찾는 용도) if ( action.actionType === "update" || action.actionType === "delete" || condition.tableType === "to" ) { // 대상 테이블(to)에서 조건 확인 const targetTable = action.fieldMappings?.[0]?.targetTable; if (!targetTable) { console.error("❌ 대상 테이블을 찾을 수 없습니다:", action); return { satisfied: false, reason: "대상 테이블 정보가 없습니다.", }; } tableName = targetTable; console.log( `🔍 대상 테이블(${tableName})에서 조건 확인: ${condition.field} = ${condition.value} (${action.actionType.toUpperCase()} 액션)` ); // 대상 테이블에서 컬럼 존재 여부 먼저 확인 const columnExists = await this.checkColumnExists( tableName, condition.field ); if (!columnExists) { console.error( `❌ 컬럼이 존재하지 않습니다: ${tableName}.${condition.field}` ); return { satisfied: false, reason: `컬럼이 존재하지 않습니다: ${tableName}.${condition.field}`, }; } // 대상 테이블에서 조건에 맞는 데이터 조회 const queryResult = await query>( `SELECT ${condition.field} FROM ${tableName} WHERE ${condition.field} = $1 LIMIT 1`, [condition.value] ); dataToCheck = Array.isArray(queryResult) && queryResult.length > 0 ? (queryResult[0] as Record) : {}; } else { // 소스 테이블(from) 또는 기본값에서 조건 확인 tableName = sourceTable; dataToCheck = sourceData; console.log( `🔍 소스 테이블(${tableName})에서 조건 확인: ${condition.field} = ${condition.value}` ); } const fieldValue = dataToCheck[condition.field]; console.log( `🔍 [액션 실행 조건] 조건 평가 결과: ${condition.field} = ${condition.value} (테이블 ${tableName} 실제값: ${fieldValue})` ); // 액션 실행 조건 평가 if ( action.actionType === "update" || action.actionType === "delete" || condition.tableType === "to" ) { // UPDATE/DELETE 액션이거나 대상 테이블의 경우 데이터 존재 여부로 판단 if (!fieldValue || fieldValue !== condition.value) { return { satisfied: false, reason: `[액션 실행 조건] 조건 미충족: ${tableName}.${condition.field} = ${condition.value} (테이블 실제값: ${fieldValue})`, }; } } else { // 소스 테이블의 경우 값 비교 if (fieldValue !== condition.value) { return { satisfied: false, reason: `[액션 실행 조건] 조건 미충족: ${tableName}.${condition.field} = ${condition.value} (테이블 실제값: ${fieldValue})`, }; } } } return { satisfied: true }; } catch (error) { console.error("❌ 액션 조건 평가 오류:", error); const errorMessage = error instanceof Error ? error.message : String(error); return { satisfied: false, reason: `액션 조건 평가 오류: ${errorMessage}`, }; } } /** * 조건 평가 */ private async evaluateConditions( conditions: ControlCondition[], data: Record ): Promise<{ satisfied: boolean; reason?: string }> { if (!conditions || conditions.length === 0) { return { satisfied: true }; } try { // 조건을 SQL WHERE 절로 변환 const whereClause = this.buildWhereClause(conditions, data); console.log(`🔍 [전체 실행 조건] 생성된 WHERE 절:`, whereClause); // 전체 실행 조건 평가 (폼 데이터 기반) for (const condition of conditions) { if ( condition.type === "condition" && condition.field && condition.operator ) { const fieldValue = data[condition.field]; const conditionValue = condition.value; console.log( `🔍 [전체 실행 조건] 조건 평가: ${condition.field} ${condition.operator} ${conditionValue} (폼 데이터 실제값: ${fieldValue})` ); const result = this.evaluateSingleCondition( fieldValue, condition.operator, conditionValue, condition.dataType || "string" ); if (!result) { return { satisfied: false, reason: `[전체 실행 조건] 조건 미충족: ${condition.field} ${condition.operator} ${conditionValue} (폼 데이터 기준)`, }; } } } return { satisfied: true }; } catch (error) { console.error("조건 평가 오류:", error); const errorMessage = error instanceof Error ? error.message : String(error); return { satisfied: false, reason: `조건 평가 오류: ${errorMessage}`, }; } } /** * 단일 조건 평가 */ private evaluateSingleCondition( fieldValue: any, operator: string, conditionValue: any, dataType: string ): boolean { // 타입 변환 let actualValue = fieldValue; let expectedValue = conditionValue; if (dataType === "number") { actualValue = parseFloat(fieldValue) || 0; expectedValue = parseFloat(conditionValue) || 0; } else if (dataType === "string") { actualValue = String(fieldValue || ""); expectedValue = String(conditionValue || ""); } // 연산자별 평가 switch (operator) { case "=": return actualValue === expectedValue; case "!=": return actualValue !== expectedValue; case ">": return actualValue > expectedValue; case "<": return actualValue < expectedValue; case ">=": return actualValue >= expectedValue; case "<=": return actualValue <= expectedValue; case "LIKE": return String(actualValue).includes(String(expectedValue)); default: console.warn(`지원되지 않는 연산자: ${operator}`); return false; } } /** * WHERE 절 생성 (복잡한 그룹 조건 처리) */ private buildWhereClause( conditions: ControlCondition[], data: Record ): string { // 실제로는 더 복잡한 그룹 처리 로직이 필요 // 현재는 간단한 AND/OR 처리만 구현 const clauses = []; for (const condition of conditions) { if (condition.type === "condition") { const clause = `${condition.field} ${condition.operator} '${condition.value}'`; clauses.push(clause); } } return clauses.join(" AND "); } /** * 액션 실행 */ private async executeAction( action: ControlAction, sourceData: Record ): Promise { console.log(`🚀 액션 실행: ${action.actionType}`, action); switch (action.actionType) { case "insert": return await this.executeInsertAction(action, sourceData); case "update": return await this.executeUpdateAction(action, sourceData); case "delete": return await this.executeDeleteAction(action, sourceData); default: throw new Error(`지원되지 않는 액션 타입: ${action.actionType}`); } } /** * INSERT 액션 실행 */ private async executeInsertAction( action: ControlAction, sourceData: Record ): Promise { const results = []; for (const mapping of action.fieldMappings) { const { targetTable, targetField, defaultValue, sourceField } = mapping; // 삽입할 데이터 준비 const insertData: Record = {}; if (sourceField && sourceData[sourceField]) { insertData[targetField] = sourceData[sourceField]; } else if (defaultValue !== undefined) { insertData[targetField] = defaultValue; } // 기본 필드 추가 insertData.created_at = new Date(); insertData.updated_at = new Date(); console.log(`📝 INSERT 실행: ${targetTable}.${targetField}`, insertData); try { // 동적 테이블 INSERT 실행 const placeholders = Object.keys(insertData) .map((_, i) => `$${i + 1}`) .join(", "); const result = await query( `INSERT INTO ${targetTable} (${Object.keys(insertData).join(", ")}) VALUES (${placeholders})`, Object.values(insertData) ); results.push({ table: targetTable, field: targetField, data: insertData, result, }); console.log(`✅ INSERT 성공: ${targetTable}.${targetField}`); } catch (error) { console.error(`❌ INSERT 실패: ${targetTable}.${targetField}`, error); throw error; } } return results; } /** * UPDATE 액션 실행 */ private async executeUpdateAction( action: ControlAction, sourceData: Record ): Promise { console.log(`🔄 UPDATE 액션 실행: ${action.name}`); console.log(`📋 액션 정보:`, JSON.stringify(action, null, 2)); console.log(`📋 소스 데이터:`, JSON.stringify(sourceData, null, 2)); // fieldMappings에서 대상 테이블과 필드 정보 추출 if (!action.fieldMappings || action.fieldMappings.length === 0) { console.error("❌ fieldMappings가 없습니다:", action); throw new Error("UPDATE 액션에는 fieldMappings가 필요합니다."); } console.log(`🎯 처리할 매핑 개수: ${action.fieldMappings.length}`); const results = []; // 각 필드 매핑별로 개별 UPDATE 실행 for (let i = 0; i < action.fieldMappings.length; i++) { const mapping = action.fieldMappings[i]; const targetTable = mapping.targetTable; const targetField = mapping.targetField; const updateValue = mapping.defaultValue || (mapping.sourceField ? sourceData[mapping.sourceField] : null); console.log(`🎯 매핑 ${i + 1}/${action.fieldMappings.length}:`, { targetTable, targetField, updateValue, defaultValue: mapping.defaultValue, sourceField: mapping.sourceField, }); if (!targetTable || !targetField) { console.error("❌ 필수 필드가 없습니다:", { targetTable, targetField }); continue; // 다음 매핑으로 계속 } try { // WHERE 조건 구성 let whereClause = ""; const whereValues: any[] = []; // action.conditions에서 WHERE 조건 생성 (PostgreSQL 형식) let conditionParamIndex = 2; // $1은 SET 값용, $2부터 WHERE 조건용 if (action.conditions && Array.isArray(action.conditions)) { const conditions = action.conditions .filter((cond) => cond.field && cond.value !== undefined) .map((cond) => `${cond.field} = $${conditionParamIndex++}`); if (conditions.length > 0) { whereClause = conditions.join(" AND "); whereValues.push( ...action.conditions .filter((cond) => cond.field && cond.value !== undefined) .map((cond) => cond.value) ); } } // WHERE 조건이 없으면 기본 조건 사용 (같은 필드로 찾기) if (!whereClause) { whereClause = `${targetField} = $${conditionParamIndex}`; whereValues.push("김철수"); // 기존 값으로 찾기 } console.log( `📝 UPDATE 쿼리 준비 (${i + 1}/${action.fieldMappings.length}):`, { targetTable, targetField, updateValue, whereClause, whereValues, } ); // 동적 테이블 UPDATE 실행 (PostgreSQL 형식) const updateQuery = `UPDATE ${targetTable} SET ${targetField} = $1 WHERE ${whereClause}`; const allValues = [updateValue, ...whereValues]; console.log( `🚀 실행할 쿼리 (${i + 1}/${action.fieldMappings.length}):`, updateQuery ); console.log(`📊 쿼리 파라미터:`, allValues); const result = await query(updateQuery, allValues); console.log( `✅ UPDATE 성공 (${i + 1}/${action.fieldMappings.length}):`, { table: targetTable, field: targetField, value: updateValue, affectedRows: result, } ); results.push({ message: `UPDATE 성공: ${targetTable}.${targetField} = ${updateValue}`, affectedRows: result, targetTable, targetField, updateValue, }); } catch (error) { console.error( `❌ UPDATE 실패 (${i + 1}/${action.fieldMappings.length}):`, { table: targetTable, field: targetField, value: updateValue, error: error, } ); // 에러가 발생해도 다음 매핑은 계속 처리 results.push({ message: `UPDATE 실패: ${targetTable}.${targetField} = ${updateValue}`, error: error instanceof Error ? error.message : String(error), targetTable, targetField, updateValue, }); } } // 전체 결과 반환 const successCount = results.filter((r) => !r.error).length; const totalCount = results.length; console.log(`🎯 전체 UPDATE 결과: ${successCount}/${totalCount} 성공`); return { message: `UPDATE 완료: ${successCount}/${totalCount} 성공`, results, successCount, totalCount, }; } /** * DELETE 액션 실행 - 보안상 외부 DB 비활성화 */ private async executeDeleteAction( action: ControlAction, sourceData: Record ): Promise { // 보안상 외부 DB에 대한 DELETE 작업은 비활성화 throw new Error( "보안상 외부 데이터베이스에 대한 DELETE 작업은 허용되지 않습니다. SELECT 쿼리만 사용해주세요." ); const results = []; // 조건에서 테이블별로 그룹화하여 삭제 실행 const tableGroups = new Map(); for (const condition of action.conditions) { if ( condition.type === "condition" && condition.field && condition.value !== undefined ) { // 조건에서 테이블명을 추출 (테이블명.필드명 형식이거나 기본 소스 테이블) const parts = condition.field!.split("."); let tableName: string; let fieldName: string; if (parts.length === 2) { // "테이블명.필드명" 형식 tableName = parts[0]; fieldName = parts[1]; } else { // 필드명만 있는 경우, 조건에 명시된 테이블 또는 소스 테이블 사용 // fieldMappings이 있다면 targetTable 사용, 없다면 에러 if (action.fieldMappings && action.fieldMappings.length > 0) { tableName = action.fieldMappings[0].targetTable; } else { throw new Error( `DELETE 조건에서 테이블을 결정할 수 없습니다. 필드를 "테이블명.필드명" 형식으로 지정하거나 fieldMappings에 targetTable을 설정하세요.` ); } fieldName = condition.field!; } if (!tableGroups.has(tableName)) { tableGroups.set(tableName, []); } tableGroups.get(tableName)!.push({ field: fieldName, value: condition.value, operator: condition.operator || "=", }); } } if (tableGroups.size === 0) { throw new Error("DELETE 액션에서 유효한 조건을 찾을 수 없습니다."); } console.log( `🎯 삭제 대상 테이블: ${Array.from(tableGroups.keys()).join(", ")}` ); // 각 테이블별로 DELETE 실행 for (const [tableName, conditions] of tableGroups) { try { console.log(`🗑️ ${tableName} 테이블에서 삭제 실행:`, conditions); // WHERE 조건 구성 let conditionParamIndex = 1; const whereConditions = conditions.map( (cond) => `${cond.field} ${cond.operator} $${conditionParamIndex++}` ); const whereClause = whereConditions.join(" AND "); const whereValues = conditions.map((cond) => cond.value); console.log(`📝 DELETE 쿼리 준비:`, { tableName, whereClause, whereValues, }); // 동적 테이블 DELETE 실행 (PostgreSQL 형식) const deleteQuery = `DELETE FROM ${tableName} WHERE ${whereClause}`; console.log(`🚀 실행할 쿼리:`, deleteQuery); console.log(`📊 쿼리 파라미터:`, whereValues); const result = await query(deleteQuery, whereValues); console.log(`✅ DELETE 성공:`, { table: tableName, affectedRows: result, whereClause, }); results.push({ message: `DELETE 성공: ${tableName}에서 ${result}개 행 삭제`, affectedRows: result, targetTable: tableName, whereClause, }); } catch (error: unknown) { console.error(`❌ DELETE 실패:`, { table: tableName, error: error, }); const userFriendlyMessage = error instanceof Error ? (error as Error).message : String(error); results.push({ message: `DELETE 실패: ${tableName}`, error: userFriendlyMessage, targetTable: tableName, }); } } // 전체 결과 반환 const successCount = results.filter((r) => !r.error).length; const totalCount = results.length; console.log(`🎯 전체 DELETE 결과: ${successCount}/${totalCount} 성공`); return { message: `DELETE 완료: ${successCount}/${totalCount} 성공`, results, successCount, totalCount, }; } /** * 테이블에 특정 컬럼이 존재하는지 확인 */ private async checkColumnExists( tableName: string, columnName: string ): Promise { try { const result = await query<{ exists: boolean }>( `SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = $2 AND table_schema = 'public' ) as exists`, [tableName, columnName] ); return result[0]?.exists || false; } catch (error) { console.error( `❌ 컬럼 존재 여부 확인 오류: ${tableName}.${columnName}`, error ); return false; } } }