저장버튼 제어기능(update,delete)
This commit is contained in:
parent
29f506fb27
commit
f5caa7127c
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, any>,
|
||||
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<string, any>;
|
||||
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<string, any>)
|
||||
: {};
|
||||
} 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<string, any>
|
||||
): Promise<any> {
|
||||
// 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<string, any>
|
||||
): Promise<any> {
|
||||
// DELETE 로직 구현
|
||||
console.log("DELETE 액션 실행 (미구현)");
|
||||
return { message: "DELETE 액션은 아직 구현되지 않았습니다." };
|
||||
}
|
||||
console.log(`🗑️ DELETE 액션 실행 시작:`, {
|
||||
actionName: action.name,
|
||||
conditions: action.conditions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 테이블의 컬럼 정보를 동적으로 조회하여 기본 필드 추가
|
||||
*/
|
||||
private async addDefaultFieldsForTable(
|
||||
tableName: string,
|
||||
insertData: Record<string, any>
|
||||
): Promise<void> {
|
||||
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<string, any[]>();
|
||||
|
||||
// 일반적인 타임스탬프 필드들 확인 및 추가
|
||||
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<boolean> {
|
||||
try {
|
||||
const result = await prisma.$queryRawUnsafe<Array<{ 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
|
||||
);
|
||||
|
||||
// 컬럼명 기반 기본값
|
||||
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; // 기본값을 설정할 수 없는 경우
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -481,6 +481,19 @@ export class DynamicFormService {
|
|||
// 트리거 오류는 로그만 남기고 메인 업데이트 프로세스는 계속 진행
|
||||
}
|
||||
|
||||
// 🎯 제어관리 실행 (UPDATE 트리거)
|
||||
try {
|
||||
await this.executeDataflowControlIfConfigured(
|
||||
0, // UPDATE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
|
||||
tableName,
|
||||
updatedRecord as Record<string, any>,
|
||||
"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<string, any>;
|
||||
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}`);
|
||||
// 제어관리 실패는 메인 저장 프로세스에 영향을 주지 않음
|
||||
}
|
||||
|
||||
// 첫 번째 설정된 제어관리만 실행 (여러 개가 있을 경우)
|
||||
|
|
|
|||
Loading…
Reference in New Issue