From f5caa7127ca76ac38b9773129eaabbf2c9de2e98 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 18 Sep 2025 12:02:48 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=EA=B8=B0=EB=8A=A5(update,delete)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/prisma/schema.prisma | 57 +- backend-node/src/app.ts | 12 +- .../src/services/dataflowControlService.ts | 586 ++++++++++++++---- .../src/services/dynamicFormService.ts | 40 ++ 4 files changed, 525 insertions(+), 170 deletions(-) diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index 483964c4..e198576b 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -26,46 +26,43 @@ model external_call_configs { call_type String @db.VarChar(20) api_type String? @db.VarChar(20) config_data Json - description String? @db.Text + description String? company_code String @default("*") @db.VarChar(20) - is_active String @default("Y") @db.Char(1) + is_active String? @default("Y") @db.Char(1) created_date DateTime? @default(now()) @db.Timestamp(6) created_by String? @db.VarChar(50) updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6) updated_by String? @db.VarChar(50) - @@index([company_code]) - @@index([call_type, api_type]) - @@index([is_active]) + @@index([is_active], map: "idx_external_call_configs_active") + @@index([company_code], map: "idx_external_call_configs_company") + @@index([call_type, api_type], map: "idx_external_call_configs_type") } model external_db_connections { - id Int @id @default(autoincrement()) - connection_name String @db.VarChar(100) - description String? @db.Text - db_type String @db.VarChar(20) - host String @db.VarChar(255) - port Int - database_name String @db.VarChar(100) - username String @db.VarChar(100) - password String @db.Text - connection_timeout Int? @default(30) - query_timeout Int? @default(60) - max_connections Int? @default(10) - ssl_enabled String @default("N") @db.Char(1) - ssl_cert_path String? @db.VarChar(500) - connection_options Json? - company_code String @default("*") @db.VarChar(20) - is_active String @default("Y") @db.Char(1) - created_date DateTime? @default(now()) @db.Timestamp(6) - created_by String? @db.VarChar(50) - updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6) - updated_by String? @db.VarChar(50) + id Int @id @default(autoincrement()) + connection_name String @db.VarChar(100) + description String? + db_type String @db.VarChar(20) + host String @db.VarChar(255) + port Int + database_name String @db.VarChar(100) + username String @db.VarChar(100) + password String + connection_timeout Int? @default(30) + query_timeout Int? @default(60) + max_connections Int? @default(10) + ssl_enabled String? @default("N") @db.Char(1) + ssl_cert_path String? @db.VarChar(500) + connection_options Json? + company_code String? @default("*") @db.VarChar(20) + is_active String? @default("Y") @db.Char(1) + created_date DateTime? @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6) + updated_by String? @db.VarChar(50) - @@index([company_code]) - @@index([is_active]) - @@index([db_type]) - @@index([connection_name]) + @@index([connection_name], map: "idx_external_db_connections_name") } model admin_supply_mng { diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 3d03c5e3..1a0d193e 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -22,8 +22,6 @@ import fileRoutes from "./routes/fileRoutes"; import companyManagementRoutes from "./routes/companyManagementRoutes"; // import dataflowRoutes from "./routes/dataflowRoutes"; // 임시 주석 import dataflowDiagramRoutes from "./routes/dataflowDiagramRoutes"; -import buttonDataflowRoutes from "./routes/buttonDataflowRoutes"; -import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes"; import webTypeStandardRoutes from "./routes/webTypeStandardRoutes"; import buttonActionStandardRoutes from "./routes/buttonActionStandardRoutes"; import screenStandardRoutes from "./routes/screenStandardRoutes"; @@ -31,9 +29,7 @@ import templateStandardRoutes from "./routes/templateStandardRoutes"; import componentStandardRoutes from "./routes/componentStandardRoutes"; import layoutRoutes from "./routes/layoutRoutes"; import dataRoutes from "./routes/dataRoutes"; -import externalCallRoutes from "./routes/externalCallRoutes"; -import externalCallConfigRoutes from "./routes/externalCallConfigRoutes"; -import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes"; +import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes"; // import userRoutes from './routes/userRoutes'; // import menuRoutes from './routes/menuRoutes'; @@ -119,8 +115,6 @@ app.use("/api/files", fileRoutes); app.use("/api/company-management", companyManagementRoutes); // app.use("/api/dataflow", dataflowRoutes); // 임시 주석 app.use("/api/dataflow-diagrams", dataflowDiagramRoutes); -app.use("/api/button-dataflow", buttonDataflowRoutes); -app.use("/api/test-button-dataflow", testButtonDataflowRoutes); app.use("/api/admin/web-types", webTypeStandardRoutes); app.use("/api/admin/button-actions", buttonActionStandardRoutes); app.use("/api/admin/template-standards", templateStandardRoutes); @@ -128,9 +122,7 @@ app.use("/api/admin/component-standards", componentStandardRoutes); app.use("/api/layouts", layoutRoutes); app.use("/api/screen", screenStandardRoutes); app.use("/api/data", dataRoutes); -app.use("/api/external-calls", externalCallRoutes); -app.use("/api/external-call-configs", externalCallConfigRoutes); -app.use("/api/external-db-connections", externalDbConnectionRoutes); +app.use("/api/test-button-dataflow", testButtonDataflowRoutes); // app.use('/api/users', userRoutes); // app.use('/api/menus', menuRoutes); diff --git a/backend-node/src/services/dataflowControlService.ts b/backend-node/src/services/dataflowControlService.ts index 99e17fbc..a04e5eee 100644 --- a/backend-node/src/services/dataflowControlService.ts +++ b/backend-node/src/services/dataflowControlService.ts @@ -12,6 +12,7 @@ export interface ControlCondition { logicalOperator?: "AND" | "OR"; groupId?: string; groupLevel?: number; + tableType?: "from" | "to"; } export interface ControlAction { @@ -83,8 +84,12 @@ export class DataflowControlService { } // 제어 규칙과 실행 계획 추출 - const controlRules = (diagram.control as unknown as ControlRule[]) || []; - const executionPlans = (diagram.plan as unknown as ControlPlan[]) || []; + const controlRules = Array.isArray(diagram.control) + ? (diagram.control as unknown as ControlRule[]) + : []; + const executionPlans = Array.isArray(diagram.plan) + ? (diagram.plan as unknown as ControlPlan[]) + : []; console.log(`📋 제어 규칙:`, controlRules); console.log(`📋 실행 계획:`, executionPlans); @@ -110,7 +115,7 @@ export class DataflowControlService { sourceData ); - console.log(`🔍 조건 검증 결과:`, conditionResult); + console.log(`🔍 [전체 실행 조건] 검증 결과:`, conditionResult); if (!conditionResult.satisfied) { return { @@ -138,12 +143,20 @@ export class DataflowControlService { for (const action of targetPlan.actions) { try { console.log(`⚡ 액션 실행: ${action.name} (${action.actionType})`); + console.log(`📋 액션 상세 정보:`, { + actionId: action.id, + actionName: action.name, + actionType: action.actionType, + conditions: action.conditions, + fieldMappings: action.fieldMappings, + }); - // 액션 조건 검증 (있는 경우) + // 액션 조건 검증 (있는 경우) - 동적 테이블 지원 if (action.conditions && action.conditions.length > 0) { - const actionConditionResult = await this.evaluateConditions( - action.conditions, - sourceData + const actionConditionResult = await this.evaluateActionConditions( + action, + sourceData, + tableName ); if (!actionConditionResult.satisfied) { @@ -162,9 +175,9 @@ export class DataflowControlService { }); } catch (error) { console.error(`❌ 액션 실행 오류: ${action.name}`, error); - errors.push( - `액션 '${action.name}' 실행 오류: ${error instanceof Error ? error.message : String(error)}` - ); + const errorMessage = + error instanceof Error ? error.message : String(error); + errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`); } } @@ -176,9 +189,130 @@ export class DataflowControlService { }; } catch (error) { console.error("❌ 제어관리 실행 오류:", error); + const errorMessage = + error instanceof Error ? error.message : String(error); return { success: false, - message: `제어관리 실행 중 오류 발생: ${error instanceof Error ? error.message : String(error)}`, + message: `제어관리 실행 중 오류 발생: ${errorMessage}`, + }; + } + } + + /** + * 액션별 조건 평가 (동적 테이블 지원) + */ + 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 prisma.$queryRawUnsafe( + `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}`, }; } } @@ -197,21 +331,25 @@ export class DataflowControlService { try { // 조건을 SQL WHERE 절로 변환 const whereClause = this.buildWhereClause(conditions, data); - console.log(`🔍 생성된 WHERE 절:`, whereClause); + console.log(`🔍 [전체 실행 조건] 생성된 WHERE 절:`, whereClause); - // 간단한 조건 평가 (실제로는 더 복잡한 로직 필요) + // 전체 실행 조건 평가 (폼 데이터 기반) for (const condition of conditions) { - if (condition.type === "condition" && condition.field) { + 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})` + `🔍 [전체 실행 조건] 조건 평가: ${condition.field} ${condition.operator} ${conditionValue} (폼 데이터 실제값: ${fieldValue})` ); const result = this.evaluateSingleCondition( fieldValue, - condition.operator || "=", + condition.operator, conditionValue, condition.dataType || "string" ); @@ -219,7 +357,7 @@ export class DataflowControlService { if (!result) { return { satisfied: false, - reason: `조건 미충족: ${condition.field} ${condition.operator} ${conditionValue}`, + reason: `[전체 실행 조건] 조건 미충족: ${condition.field} ${condition.operator} ${conditionValue} (폼 데이터 기준)`, }; } } @@ -228,9 +366,11 @@ export class DataflowControlService { return { satisfied: true }; } catch (error) { console.error("조건 평가 오류:", error); + const errorMessage = + error instanceof Error ? error.message : String(error); return { satisfied: false, - reason: `조건 평가 오류: ${error instanceof Error ? error.message : String(error)}`, + reason: `조건 평가 오류: ${errorMessage}`, }; } } @@ -341,8 +481,9 @@ export class DataflowControlService { insertData[targetField] = defaultValue; } - // 동적으로 테이블 컬럼 정보 조회하여 기본 필드 추가 - await this.addDefaultFieldsForTable(targetTable, insertData); + // 기본 필드 추가 + insertData.created_at = new Date(); + insertData.updated_at = new Date(); console.log(`📝 INSERT 실행: ${targetTable}.${targetField}`, insertData); @@ -352,7 +493,7 @@ export class DataflowControlService { ` INSERT INTO ${targetTable} (${Object.keys(insertData).join(", ")}) VALUES (${Object.keys(insertData) - .map((_, index) => `$${index + 1}`) + .map(() => "?") .join(", ")}) `, ...Object.values(insertData) @@ -382,139 +523,324 @@ export class DataflowControlService { action: ControlAction, sourceData: Record ): Promise { - // UPDATE 로직 구현 - console.log("UPDATE 액션 실행 (미구현)"); - return { message: "UPDATE 액션은 아직 구현되지 않았습니다." }; + 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 prisma.$executeRawUnsafe( + 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 액션 실행 + * DELETE 액션 실행 - 조건 기반으로만 삭제 */ private async executeDeleteAction( action: ControlAction, sourceData: Record ): Promise { - // DELETE 로직 구현 - console.log("DELETE 액션 실행 (미구현)"); - return { message: "DELETE 액션은 아직 구현되지 않았습니다." }; - } + console.log(`🗑️ DELETE 액션 실행 시작:`, { + actionName: action.name, + conditions: action.conditions, + }); - /** - * 테이블의 컬럼 정보를 동적으로 조회하여 기본 필드 추가 - */ - private async addDefaultFieldsForTable( - tableName: string, - insertData: Record - ): Promise { - try { - // 테이블의 컬럼 정보 조회 - const columns = await prisma.$queryRawUnsafe< - Array<{ column_name: string; data_type: string; is_nullable: string }> - >( - ` - SELECT column_name, data_type, is_nullable - FROM information_schema.columns - WHERE table_name = $1 - ORDER BY ordinal_position - `, - tableName + // DELETE는 조건이 필수 + if (!action.conditions || action.conditions.length === 0) { + throw new Error( + "DELETE 액션에는 반드시 조건이 필요합니다. 전체 테이블 삭제는 위험합니다." ); + } - console.log(`📋 ${tableName} 테이블 컬럼 정보:`, columns); + const results = []; - const currentDate = new Date(); + // 조건에서 테이블별로 그룹화하여 삭제 실행 + const tableGroups = new Map(); - // 일반적인 타임스탬프 필드들 확인 및 추가 - const timestampFields = [ - { - names: ["created_at", "create_date", "reg_date", "regdate"], - value: currentDate, - }, - { - names: ["updated_at", "update_date", "mod_date", "moddate"], - value: currentDate, - }, - ]; + 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; - for (const fieldGroup of timestampFields) { - for (const fieldName of fieldGroup.names) { - const column = columns.find( - (col) => col.column_name.toLowerCase() === fieldName.toLowerCase() - ); - if (column && !insertData[column.column_name]) { - // 해당 컬럼이 존재하고 아직 값이 설정되지 않은 경우 - if ( - column.data_type.includes("timestamp") || - column.data_type.includes("date") - ) { - insertData[column.column_name] = fieldGroup.value; - console.log( - `📅 기본 타임스탬프 필드 추가: ${column.column_name} = ${fieldGroup.value}` - ); - } - } - } - } - - // 필수 필드 중 값이 없는 경우 기본값 설정 - for (const column of columns) { - if (column.is_nullable === "NO" && !insertData[column.column_name]) { - // NOT NULL 필드인데 값이 없는 경우 기본값 설정 - const defaultValue = this.getDefaultValueForColumn(column); - if (defaultValue !== null) { - insertData[column.column_name] = defaultValue; - console.log( - `🔧 필수 필드 기본값 설정: ${column.column_name} = ${defaultValue}` + 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 || "=", + }); } - } catch (error) { - console.error(`❌ ${tableName} 테이블 컬럼 정보 조회 실패:`, error); - // 에러가 발생해도 INSERT는 계속 진행 (기본 필드 없이) } + + 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 prisma.$executeRawUnsafe( + deleteQuery, + ...whereValues + ); + + console.log(`✅ DELETE 성공:`, { + table: tableName, + affectedRows: result, + whereClause, + }); + + results.push({ + message: `DELETE 성공: ${tableName}에서 ${result}개 행 삭제`, + affectedRows: result, + targetTable: tableName, + whereClause, + }); + } catch (error) { + console.error(`❌ DELETE 실패:`, { + table: tableName, + error: error, + }); + + const userFriendlyMessage = + error instanceof Error ? 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 getDefaultValueForColumn(column: { - column_name: string; - data_type: string; - }): any { - const dataType = column.data_type.toLowerCase(); - const columnName = column.column_name.toLowerCase(); + private async checkColumnExists( + tableName: string, + columnName: string + ): Promise { + try { + const result = await prisma.$queryRawUnsafe>( + ` + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = $1 + AND column_name = $2 + AND table_schema = 'public' + ) as exists + `, + tableName, + columnName + ); - // 컬럼명 기반 기본값 - if (columnName.includes("status")) { - return "Y"; // 상태 필드는 보통 'Y' + return result[0]?.exists || false; + } catch (error) { + console.error( + `❌ 컬럼 존재 여부 확인 오류: ${tableName}.${columnName}`, + error + ); + return false; } - if (columnName.includes("type")) { - return "default"; // 타입 필드는 'default' - } - - // 데이터 타입 기반 기본값 - if ( - dataType.includes("varchar") || - dataType.includes("text") || - dataType.includes("char") - ) { - return ""; // 문자열은 빈 문자열 - } - if ( - dataType.includes("int") || - dataType.includes("numeric") || - dataType.includes("decimal") - ) { - return 0; // 숫자는 0 - } - if (dataType.includes("bool")) { - return false; // 불린은 false - } - if (dataType.includes("timestamp") || dataType.includes("date")) { - return new Date(); // 날짜는 현재 시간 - } - - return null; // 기본값을 설정할 수 없는 경우 } } diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 10a4bc3d..c292a24c 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -481,6 +481,19 @@ export class DynamicFormService { // 트리거 오류는 로그만 남기고 메인 업데이트 프로세스는 계속 진행 } + // 🎯 제어관리 실행 (UPDATE 트리거) + try { + await this.executeDataflowControlIfConfigured( + 0, // UPDATE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요) + tableName, + updatedRecord as Record, + "update" + ); + } catch (controlError) { + console.error("⚠️ 제어관리 실행 오류:", controlError); + // 제어관리 오류는 로그만 남기고 메인 업데이트 프로세스는 계속 진행 + } + return { id: updatedRecord.id || updatedRecord.objid || id, screenId: 0, // 실제 테이블에는 screenId가 없으므로 0으로 설정 @@ -546,6 +559,22 @@ export class DynamicFormService { console.error("⚠️ 조건부 연결 트리거 실행 오류:", triggerError); // 트리거 오류는 로그만 남기고 메인 삭제 프로세스는 계속 진행 } + + // 🎯 제어관리 실행 (DELETE 트리거) + try { + if (result && Array.isArray(result) && result.length > 0) { + const deletedRecord = result[0] as Record; + await this.executeDataflowControlIfConfigured( + 0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요) + tableName, + deletedRecord, + "delete" + ); + } + } catch (controlError) { + console.error("⚠️ 제어관리 실행 오류:", controlError); + // 제어관리 오류는 로그만 남기고 메인 삭제 프로세스는 계속 진행 + } } catch (error) { console.error("❌ 서비스: 실제 테이블 삭제 실패:", error); throw new Error(`실제 테이블 삭제 실패: ${error}`); @@ -845,8 +874,19 @@ export class DynamicFormService { ) { console.log(`📊 실행된 액션들:`, controlResult.executedActions); } + + // 오류가 있는 경우 경고 로그 출력 (성공이지만 일부 액션 실패) + if (controlResult.errors && controlResult.errors.length > 0) { + console.warn( + `⚠️ 제어관리 실행 중 일부 오류 발생:`, + controlResult.errors + ); + // 오류 정보를 별도로 저장하여 필요시 사용자에게 알림 가능 + // 현재는 로그만 출력하고 메인 저장 프로세스는 계속 진행 + } } else { console.warn(`⚠️ 제어관리 실행 실패: ${controlResult.message}`); + // 제어관리 실패는 메인 저장 프로세스에 영향을 주지 않음 } // 첫 번째 설정된 제어관리만 실행 (여러 개가 있을 경우)