Compare commits
8 Commits
a93fb0f684
...
b84f35d514
| Author | SHA1 | Date |
|---|---|---|
|
|
b84f35d514 | |
|
|
767c031629 | |
|
|
d22e83d234 | |
|
|
151de4148c | |
|
|
cb1a6ad672 | |
|
|
352d4c3126 | |
|
|
6fc140b423 | |
|
|
808a0244d5 |
|
|
@ -47,6 +47,7 @@
|
|||
"@types/pg": "^8.15.5",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.55.0",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"@types/pg": "^8.15.5",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.55.0",
|
||||
|
|
|
|||
|
|
@ -29,13 +29,23 @@ export async function executeDataAction(
|
|||
|
||||
// 연결 정보에 따라 다른 데이터베이스에 저장
|
||||
let result;
|
||||
|
||||
|
||||
if (connection && connection.id !== 0) {
|
||||
// 외부 데이터베이스 연결
|
||||
result = await executeExternalDatabaseAction(tableName, data, actionType, connection);
|
||||
result = await executeExternalDatabaseAction(
|
||||
tableName,
|
||||
data,
|
||||
actionType,
|
||||
connection
|
||||
);
|
||||
} else {
|
||||
// 메인 데이터베이스 (현재 시스템)
|
||||
result = await executeMainDatabaseAction(tableName, data, actionType, companyCode);
|
||||
result = await executeMainDatabaseAction(
|
||||
tableName,
|
||||
data,
|
||||
actionType,
|
||||
companyCode
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`데이터 액션 실행 완료: ${actionType} on ${tableName}`, result);
|
||||
|
|
@ -45,7 +55,6 @@ export async function executeDataAction(
|
|||
message: `데이터 액션 실행 완료: ${actionType}`,
|
||||
data: result,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error("데이터 액션 실행 실패:", error);
|
||||
res.status(500).json({
|
||||
|
|
@ -73,13 +82,13 @@ async function executeMainDatabaseAction(
|
|||
};
|
||||
|
||||
switch (actionType.toLowerCase()) {
|
||||
case 'insert':
|
||||
case "insert":
|
||||
return await executeInsert(tableName, dataWithCompany);
|
||||
case 'update':
|
||||
case "update":
|
||||
return await executeUpdate(tableName, dataWithCompany);
|
||||
case 'upsert':
|
||||
case "upsert":
|
||||
return await executeUpsert(tableName, dataWithCompany);
|
||||
case 'delete':
|
||||
case "delete":
|
||||
return await executeDelete(tableName, dataWithCompany);
|
||||
default:
|
||||
throw new Error(`지원하지 않는 액션 타입: ${actionType}`);
|
||||
|
|
@ -100,25 +109,37 @@ async function executeExternalDatabaseAction(
|
|||
connection: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
logger.info(`외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})`);
|
||||
logger.info(
|
||||
`외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})`
|
||||
);
|
||||
logger.info(`테이블: ${tableName}, 액션: ${actionType}`, data);
|
||||
|
||||
// 🔥 실제 외부 DB 연결 및 실행 로직 구현
|
||||
const { MultiConnectionQueryService } = await import('../services/multiConnectionQueryService');
|
||||
const { MultiConnectionQueryService } = await import(
|
||||
"../services/multiConnectionQueryService"
|
||||
);
|
||||
const queryService = new MultiConnectionQueryService();
|
||||
|
||||
let result;
|
||||
switch (actionType.toLowerCase()) {
|
||||
case 'insert':
|
||||
result = await queryService.insertDataToConnection(connection.id, tableName, data);
|
||||
case "insert":
|
||||
result = await queryService.insertDataToConnection(
|
||||
connection.id,
|
||||
tableName,
|
||||
data
|
||||
);
|
||||
logger.info(`외부 DB INSERT 성공:`, result);
|
||||
break;
|
||||
case 'update':
|
||||
case "update":
|
||||
// TODO: UPDATE 로직 구현 (조건 필요)
|
||||
throw new Error('UPDATE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다.');
|
||||
case 'delete':
|
||||
throw new Error(
|
||||
"UPDATE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다."
|
||||
);
|
||||
case "delete":
|
||||
// TODO: DELETE 로직 구현 (조건 필요)
|
||||
throw new Error('DELETE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다.');
|
||||
throw new Error(
|
||||
"DELETE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다."
|
||||
);
|
||||
default:
|
||||
throw new Error(`지원하지 않는 액션 타입: ${actionType}`);
|
||||
}
|
||||
|
|
@ -139,12 +160,15 @@ async function executeExternalDatabaseAction(
|
|||
/**
|
||||
* INSERT 실행
|
||||
*/
|
||||
async function executeInsert(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
async function executeInsert(
|
||||
tableName: string,
|
||||
data: Record<string, any>
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 동적 테이블 접근을 위한 raw query 사용
|
||||
const columns = Object.keys(data).join(', ');
|
||||
const columns = Object.keys(data).join(", ");
|
||||
const values = Object.values(data);
|
||||
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
|
||||
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
|
||||
|
||||
const insertQuery = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
|
||||
|
||||
|
|
@ -154,7 +178,7 @@ async function executeInsert(tableName: string, data: Record<string, any>): Prom
|
|||
|
||||
return {
|
||||
success: true,
|
||||
action: 'insert',
|
||||
action: "insert",
|
||||
tableName,
|
||||
data: result,
|
||||
affectedRows: result.length,
|
||||
|
|
@ -168,29 +192,76 @@ async function executeInsert(tableName: string, data: Record<string, any>): Prom
|
|||
/**
|
||||
* UPDATE 실행
|
||||
*/
|
||||
async function executeUpdate(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
async function executeUpdate(
|
||||
tableName: string,
|
||||
data: Record<string, any>
|
||||
): Promise<any> {
|
||||
try {
|
||||
// ID 또는 기본키를 기준으로 업데이트
|
||||
const { id, ...updateData } = data;
|
||||
logger.info(`UPDATE 액션 시작:`, { tableName, receivedData: data });
|
||||
|
||||
if (!id) {
|
||||
throw new Error('UPDATE를 위한 ID가 필요합니다');
|
||||
// 1. 테이블의 실제 기본키 조회
|
||||
const primaryKeyQuery = `
|
||||
SELECT a.attname as column_name
|
||||
FROM pg_index i
|
||||
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||
WHERE i.indrelid = $1::regclass AND i.indisprimary
|
||||
`;
|
||||
|
||||
const pkResult = await query<{ column_name: string }>(primaryKeyQuery, [
|
||||
tableName,
|
||||
]);
|
||||
|
||||
if (!pkResult || pkResult.length === 0) {
|
||||
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다`);
|
||||
}
|
||||
|
||||
const primaryKeyColumn = pkResult[0].column_name;
|
||||
logger.info(`테이블 ${tableName}의 기본키:`, primaryKeyColumn);
|
||||
|
||||
// 2. 기본키 값 추출
|
||||
const primaryKeyValue = data[primaryKeyColumn];
|
||||
|
||||
if (!primaryKeyValue && primaryKeyValue !== 0) {
|
||||
logger.error(`UPDATE 실패: 기본키 값이 없음`, {
|
||||
primaryKeyColumn,
|
||||
receivedData: data,
|
||||
availableKeys: Object.keys(data),
|
||||
});
|
||||
throw new Error(
|
||||
`UPDATE를 위한 기본키 값이 필요합니다 (${primaryKeyColumn})`
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 업데이트할 데이터에서 기본키 제외
|
||||
const updateData = { ...data };
|
||||
delete updateData[primaryKeyColumn];
|
||||
|
||||
logger.info(`UPDATE 데이터 준비:`, {
|
||||
primaryKeyColumn,
|
||||
primaryKeyValue,
|
||||
updateFields: Object.keys(updateData),
|
||||
});
|
||||
|
||||
// 4. 동적 UPDATE 쿼리 생성
|
||||
const setClause = Object.keys(updateData)
|
||||
.map((key, index) => `${key} = $${index + 1}`)
|
||||
.join(', ');
|
||||
.join(", ");
|
||||
|
||||
const values = Object.values(updateData);
|
||||
const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE id = $${values.length + 1} RETURNING *`;
|
||||
const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE ${primaryKeyColumn} = $${values.length + 1} RETURNING *`;
|
||||
|
||||
logger.info(`UPDATE 쿼리 실행:`, { query: updateQuery, values: [...values, id] });
|
||||
logger.info(`UPDATE 쿼리 실행:`, {
|
||||
query: updateQuery,
|
||||
values: [...values, primaryKeyValue],
|
||||
});
|
||||
|
||||
const result = await query<any>(updateQuery, [...values, id]);
|
||||
const result = await query<any>(updateQuery, [...values, primaryKeyValue]);
|
||||
|
||||
logger.info(`UPDATE 성공:`, { affectedRows: result.length });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'update',
|
||||
action: "update",
|
||||
tableName,
|
||||
data: result,
|
||||
affectedRows: result.length,
|
||||
|
|
@ -204,7 +275,10 @@ async function executeUpdate(tableName: string, data: Record<string, any>): Prom
|
|||
/**
|
||||
* UPSERT 실행
|
||||
*/
|
||||
async function executeUpsert(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
async function executeUpsert(
|
||||
tableName: string,
|
||||
data: Record<string, any>
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 먼저 INSERT를 시도하고, 실패하면 UPDATE
|
||||
try {
|
||||
|
|
@ -223,12 +297,15 @@ async function executeUpsert(tableName: string, data: Record<string, any>): Prom
|
|||
/**
|
||||
* DELETE 실행
|
||||
*/
|
||||
async function executeDelete(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
async function executeDelete(
|
||||
tableName: string,
|
||||
data: Record<string, any>
|
||||
): Promise<any> {
|
||||
try {
|
||||
const { id } = data;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('DELETE를 위한 ID가 필요합니다');
|
||||
throw new Error("DELETE를 위한 ID가 필요합니다");
|
||||
}
|
||||
|
||||
const deleteQuery = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`;
|
||||
|
|
@ -239,7 +316,7 @@ async function executeDelete(tableName: string, data: Record<string, any>): Prom
|
|||
|
||||
return {
|
||||
success: true,
|
||||
action: 'delete',
|
||||
action: "delete",
|
||||
tableName,
|
||||
data: result,
|
||||
affectedRows: result.length,
|
||||
|
|
|
|||
|
|
@ -1498,7 +1498,7 @@ export class TableManagementService {
|
|||
|
||||
// 전체 개수 조회
|
||||
const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`;
|
||||
const countResult = await query<any>(countQuery, ...searchValues);
|
||||
const countResult = await query<any>(countQuery, searchValues);
|
||||
const total = parseInt(countResult[0].count);
|
||||
|
||||
// 데이터 조회
|
||||
|
|
@ -1509,7 +1509,7 @@ export class TableManagementService {
|
|||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`;
|
||||
|
||||
let data = await query<any>(dataQuery, ...searchValues, size, offset);
|
||||
let data = await query<any>(dataQuery, [...searchValues, size, offset]);
|
||||
|
||||
// 🎯 파일 컬럼이 있으면 파일 정보 보강
|
||||
if (fileColumns.length > 0) {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ COPY package*.json ./
|
|||
RUN npm ci --prefer-offline --no-audit
|
||||
|
||||
# 소스 코드는 볼륨 마운트로 처리
|
||||
# Prisma 클라이언트 생성용 스키마만 복사
|
||||
COPY prisma ./prisma
|
||||
RUN npx prisma generate
|
||||
|
||||
# 포트 노출
|
||||
EXPOSE 8080
|
||||
|
|
|
|||
|
|
@ -9,14 +9,10 @@ RUN apt-get update \
|
|||
&& apt-get install -y --no-install-recommends openssl ca-certificates curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Dependencies stage (install deps and generate Prisma client)
|
||||
# Dependencies stage (install production dependencies)
|
||||
FROM base AS deps
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev --prefer-offline --no-audit && npm cache clean --force
|
||||
# Copy prisma schema and generate client (glibc target will be detected)
|
||||
COPY prisma ./prisma
|
||||
ENV PRISMA_SKIP_POSTINSTALL_GENERATE=true
|
||||
RUN npx prisma generate
|
||||
|
||||
# Build stage (compile TypeScript)
|
||||
FROM node:20-bookworm-slim AS build
|
||||
|
|
@ -25,8 +21,6 @@ COPY package*.json ./
|
|||
RUN npm ci --prefer-offline --no-audit && npm cache clean --force
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
COPY prisma ./prisma
|
||||
RUN npx prisma generate
|
||||
RUN npm run build
|
||||
|
||||
# Runtime image - base 이미지 재사용으로 중복 설치 제거
|
||||
|
|
@ -36,7 +30,7 @@ ENV NODE_ENV=production
|
|||
# Create non-root user
|
||||
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
|
||||
|
||||
# Copy node_modules with generated Prisma client
|
||||
# Copy production node_modules
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
# Copy built files
|
||||
COPY --from=build /app/dist ./dist
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ const ControlConditionStep: React.FC<ControlConditionStepProps> = ({ state, acti
|
|||
{[...fromColumns, ...toColumns]
|
||||
.filter(
|
||||
(col, index, array) =>
|
||||
col.columnName && // 빈 문자열 제외
|
||||
array.findIndex((c) => c.columnName === col.columnName) === index,
|
||||
)
|
||||
.map((col) => (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { toast } from "sonner";
|
|||
import { screenApi } from "@/lib/api/screen";
|
||||
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
||||
import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor";
|
||||
import type { ExtendedControlContext } from "@/types/control-management";
|
||||
|
||||
/**
|
||||
* 버튼 액션 타입 정의
|
||||
|
|
@ -51,6 +52,7 @@ export interface ButtonActionConfig {
|
|||
// 제어관리 관련
|
||||
enableDataflowControl?: boolean;
|
||||
dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용)
|
||||
dataflowTiming?: "before" | "after" | "replace"; // 제어 실행 타이밍
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -68,6 +70,11 @@ export interface ButtonActionContext {
|
|||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||
selectedRows?: any[];
|
||||
selectedRowsData?: any[];
|
||||
|
||||
// 제어 실행을 위한 추가 정보
|
||||
buttonId?: string;
|
||||
userId?: string;
|
||||
companyCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -226,6 +233,12 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
|
||||
console.log("✅ 저장 성공:", saveResult);
|
||||
|
||||
// 🔥 저장 성공 후 연결된 제어 실행 (dataflowTiming이 'after'인 경우)
|
||||
if (config.enableDataflowControl && config.dataflowConfig) {
|
||||
console.log("🎯 저장 후 제어 실행 시작:", config.dataflowConfig);
|
||||
await this.executeAfterSaveControl(config, context);
|
||||
}
|
||||
} else {
|
||||
throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
|
||||
}
|
||||
|
|
@ -853,6 +866,68 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장 후 제어 실행 (After Timing)
|
||||
*/
|
||||
private static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<void> {
|
||||
console.log("🎯 저장 후 제어 실행:", {
|
||||
enableDataflowControl: config.enableDataflowControl,
|
||||
dataflowConfig: config.dataflowConfig,
|
||||
dataflowTiming: config.dataflowTiming,
|
||||
});
|
||||
|
||||
// dataflowTiming이 'after'가 아니면 실행하지 않음
|
||||
if (config.dataflowTiming && config.dataflowTiming !== "after") {
|
||||
console.log("⏭️ dataflowTiming이 'after'가 아니므로 제어 실행 건너뜀:", config.dataflowTiming);
|
||||
return;
|
||||
}
|
||||
|
||||
// 제어 데이터 소스 결정
|
||||
let controlDataSource = config.dataflowConfig?.controlDataSource;
|
||||
if (!controlDataSource) {
|
||||
controlDataSource = "form"; // 저장 후에는 기본적으로 form 데이터 사용
|
||||
}
|
||||
|
||||
const extendedContext: ExtendedControlContext = {
|
||||
formData: context.formData || {},
|
||||
selectedRows: context.selectedRows || [],
|
||||
selectedRowsData: context.selectedRowsData || [],
|
||||
controlDataSource,
|
||||
};
|
||||
|
||||
// 관계 기반 제어 실행
|
||||
if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
|
||||
console.log("🔗 저장 후 관계 기반 제어 실행:", config.dataflowConfig.relationshipConfig);
|
||||
|
||||
const buttonConfig = {
|
||||
actionType: config.type,
|
||||
dataflowConfig: config.dataflowConfig,
|
||||
enableDataflowControl: true,
|
||||
};
|
||||
|
||||
const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(
|
||||
buttonConfig,
|
||||
context.formData || {},
|
||||
{
|
||||
buttonId: context.buttonId || "unknown",
|
||||
screenId: context.screenId || "unknown",
|
||||
userId: context.userId || "unknown",
|
||||
companyCode: context.companyCode || "*",
|
||||
startTime: Date.now(),
|
||||
contextData: context,
|
||||
}
|
||||
);
|
||||
|
||||
if (executionResult.success) {
|
||||
console.log("✅ 저장 후 제어 실행 완료:", executionResult);
|
||||
// 성공 토스트는 save 액션에서 이미 표시했으므로 추가로 표시하지 않음
|
||||
} else {
|
||||
console.error("❌ 저장 후 제어 실행 실패:", executionResult);
|
||||
toast.error("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도에서 가져온 액션들을 실행
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 🔥 개선된 버튼 액션 실행기
|
||||
*
|
||||
*
|
||||
* 계획서에 따른 새로운 실행 플로우:
|
||||
* 1. Before 타이밍 제어 실행
|
||||
* 2. 메인 액션 실행 (replace가 아닌 경우)
|
||||
|
|
@ -65,7 +65,7 @@ export class ImprovedButtonActionExecutor {
|
|||
static async executeButtonAction(
|
||||
buttonConfig: ExtendedButtonTypeConfig,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ButtonExecutionResult> {
|
||||
console.log("🔥 ImprovedButtonActionExecutor 시작:", {
|
||||
buttonConfig,
|
||||
|
|
@ -92,15 +92,11 @@ export class ImprovedButtonActionExecutor {
|
|||
// 1. Before 타이밍 제어 실행
|
||||
if (executionPlan.beforeControls.length > 0) {
|
||||
console.log("⏰ Before 제어 실행 시작");
|
||||
const beforeResults = await this.executeControls(
|
||||
executionPlan.beforeControls,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
const beforeResults = await this.executeControls(executionPlan.beforeControls, formData, context);
|
||||
results.push(...beforeResults);
|
||||
|
||||
// Before 제어 중 실패가 있으면 중단
|
||||
const hasFailure = beforeResults.some(r => !r.success);
|
||||
const hasFailure = beforeResults.some((r) => !r.success);
|
||||
if (hasFailure) {
|
||||
throw new Error("Before 제어 실행 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
|
@ -109,11 +105,7 @@ export class ImprovedButtonActionExecutor {
|
|||
// 2. 메인 액션 실행 (replace가 아닌 경우에만)
|
||||
if (!executionPlan.hasReplaceControl) {
|
||||
console.log("⚡ 메인 액션 실행:", buttonConfig.actionType);
|
||||
const mainResult = await this.executeMainAction(
|
||||
buttonConfig,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
const mainResult = await this.executeMainAction(buttonConfig, formData, context);
|
||||
results.push(mainResult);
|
||||
|
||||
if (!mainResult.success) {
|
||||
|
|
@ -126,11 +118,7 @@ export class ImprovedButtonActionExecutor {
|
|||
// 3. After 타이밍 제어 실행
|
||||
if (executionPlan.afterControls.length > 0) {
|
||||
console.log("⏰ After 제어 실행 시작");
|
||||
const afterResults = await this.executeControls(
|
||||
executionPlan.afterControls,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
const afterResults = await this.executeControls(executionPlan.afterControls, formData, context);
|
||||
results.push(...afterResults);
|
||||
}
|
||||
|
||||
|
|
@ -144,10 +132,10 @@ export class ImprovedButtonActionExecutor {
|
|||
};
|
||||
} catch (error) {
|
||||
console.error("❌ 버튼 액션 실행 실패:", error);
|
||||
|
||||
|
||||
// 롤백 처리
|
||||
await this.handleExecutionError(error, results, buttonConfig);
|
||||
|
||||
|
||||
return {
|
||||
success: false,
|
||||
results,
|
||||
|
|
@ -210,18 +198,14 @@ export class ImprovedButtonActionExecutor {
|
|||
private static async executeControls(
|
||||
controls: ControlConfig[],
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult[]> {
|
||||
const results: ExecutionResult[] = [];
|
||||
|
||||
for (const control of controls) {
|
||||
try {
|
||||
// 관계 실행만 지원
|
||||
const result = await this.executeRelationship(
|
||||
control.relationshipConfig,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
const result = await this.executeRelationship(control.relationshipConfig, formData, context);
|
||||
|
||||
results.push(result);
|
||||
|
||||
|
|
@ -255,7 +239,7 @@ export class ImprovedButtonActionExecutor {
|
|||
contextData?: Record<string, any>;
|
||||
},
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
console.log(`🔗 관계 실행 시작: ${config.relationshipName} (ID: ${config.relationshipId})`);
|
||||
|
|
@ -266,13 +250,13 @@ export class ImprovedButtonActionExecutor {
|
|||
throw new Error(`관계 정보를 찾을 수 없습니다: ${config.relationshipId}`);
|
||||
}
|
||||
|
||||
console.log(`📋 관계 데이터 로드 완료:`, relationshipData);
|
||||
console.log("📋 관계 데이터 로드 완료:", relationshipData);
|
||||
|
||||
// 2. 관계 타입에 따른 실행
|
||||
const relationships = relationshipData.relationships;
|
||||
const connectionType = relationships.connectionType;
|
||||
|
||||
console.log(`🔍 관계 상세 정보:`, {
|
||||
|
||||
console.log("🔍 관계 상세 정보:", {
|
||||
connectionType,
|
||||
hasExternalCallConfig: !!relationships.externalCallConfig,
|
||||
externalCallConfig: relationships.externalCallConfig,
|
||||
|
|
@ -301,7 +285,6 @@ export class ImprovedButtonActionExecutor {
|
|||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`❌ 관계 실행 실패: ${config.relationshipName}`, error);
|
||||
const errorResult = {
|
||||
|
|
@ -310,7 +293,7 @@ export class ImprovedButtonActionExecutor {
|
|||
executionTime: 0,
|
||||
error: error.message,
|
||||
};
|
||||
|
||||
|
||||
toast.error(errorResult.message);
|
||||
return errorResult;
|
||||
}
|
||||
|
|
@ -322,18 +305,18 @@ export class ImprovedButtonActionExecutor {
|
|||
private static async getRelationshipData(relationshipId: string): Promise<any> {
|
||||
try {
|
||||
console.log(`🔍 관계 데이터 조회 시작: ${relationshipId}`);
|
||||
|
||||
|
||||
const response = await apiClient.get(`/dataflow-diagrams/${relationshipId}`);
|
||||
|
||||
console.log(`✅ 관계 데이터 조회 성공:`, response.data);
|
||||
|
||||
|
||||
console.log("✅ 관계 데이터 조회 성공:", response.data);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || '관계 데이터 조회 실패');
|
||||
throw new Error(response.data.message || "관계 데이터 조회 실패");
|
||||
}
|
||||
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.error('관계 데이터 조회 오류:', error);
|
||||
console.error("관계 데이터 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -344,39 +327,39 @@ export class ImprovedButtonActionExecutor {
|
|||
private static async executeExternalCall(
|
||||
relationships: any,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
console.log(`🔍 외부 호출 실행 시작 - relationships 구조:`, relationships);
|
||||
|
||||
console.log("🔍 외부 호출 실행 시작 - relationships 구조:", relationships);
|
||||
|
||||
const externalCallConfig = relationships.externalCallConfig;
|
||||
console.log(`🔍 externalCallConfig:`, externalCallConfig);
|
||||
|
||||
console.log("🔍 externalCallConfig:", externalCallConfig);
|
||||
|
||||
if (!externalCallConfig) {
|
||||
console.error('❌ 외부 호출 설정이 없습니다. relationships 구조:', relationships);
|
||||
throw new Error('외부 호출 설정이 없습니다');
|
||||
console.error("❌ 외부 호출 설정이 없습니다. relationships 구조:", relationships);
|
||||
throw new Error("외부 호출 설정이 없습니다");
|
||||
}
|
||||
|
||||
const restApiSettings = externalCallConfig.restApiSettings;
|
||||
if (!restApiSettings) {
|
||||
throw new Error('REST API 설정이 없습니다');
|
||||
throw new Error("REST API 설정이 없습니다");
|
||||
}
|
||||
|
||||
console.log(`🌐 외부 API 호출: ${restApiSettings.apiUrl}`);
|
||||
|
||||
// API 호출 준비
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
...restApiSettings.headers,
|
||||
};
|
||||
|
||||
// 인증 처리
|
||||
if (restApiSettings.authentication?.type === 'api-key') {
|
||||
headers['Authorization'] = `Bearer ${restApiSettings.authentication.apiKey}`;
|
||||
if (restApiSettings.authentication?.type === "api-key") {
|
||||
headers["Authorization"] = `Bearer ${restApiSettings.authentication.apiKey}`;
|
||||
}
|
||||
|
||||
// 요청 바디 준비 (템플릿 처리)
|
||||
let requestBody = restApiSettings.bodyTemplate || '';
|
||||
let requestBody = restApiSettings.bodyTemplate || "";
|
||||
if (requestBody) {
|
||||
// 간단한 템플릿 치환 ({{변수명}} 형태)
|
||||
requestBody = requestBody.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => {
|
||||
|
|
@ -385,11 +368,11 @@ export class ImprovedButtonActionExecutor {
|
|||
}
|
||||
|
||||
// 백엔드 프록시를 통한 외부 API 호출 (CORS 문제 해결)
|
||||
console.log(`🌐 백엔드 프록시를 통한 외부 API 호출 준비:`, {
|
||||
console.log("🌐 백엔드 프록시를 통한 외부 API 호출 준비:", {
|
||||
originalUrl: restApiSettings.apiUrl,
|
||||
method: restApiSettings.httpMethod || 'GET',
|
||||
method: restApiSettings.httpMethod || "GET",
|
||||
headers,
|
||||
body: restApiSettings.httpMethod !== 'GET' ? requestBody : undefined,
|
||||
body: restApiSettings.httpMethod !== "GET" ? requestBody : undefined,
|
||||
});
|
||||
|
||||
// 백엔드 프록시 API 호출 - GenericApiSettings 형식에 맞게 전달
|
||||
|
|
@ -400,53 +383,48 @@ export class ImprovedButtonActionExecutor {
|
|||
callType: "rest-api",
|
||||
apiType: "generic",
|
||||
url: restApiSettings.apiUrl,
|
||||
method: restApiSettings.httpMethod || 'POST',
|
||||
method: restApiSettings.httpMethod || "POST",
|
||||
headers: restApiSettings.headers || {},
|
||||
body: requestBody,
|
||||
authentication: restApiSettings.authentication || { type: 'none' },
|
||||
authentication: restApiSettings.authentication || { type: "none" },
|
||||
timeout: restApiSettings.timeout || 30000,
|
||||
retryCount: restApiSettings.retryCount || 3,
|
||||
},
|
||||
templateData: restApiSettings.httpMethod !== 'GET' && requestBody ? JSON.parse(requestBody) : formData,
|
||||
templateData: restApiSettings.httpMethod !== "GET" && requestBody ? JSON.parse(requestBody) : formData,
|
||||
};
|
||||
|
||||
console.log(`📤 백엔드로 전송할 데이터:`, requestPayload);
|
||||
console.log("📤 백엔드로 전송할 데이터:", requestPayload);
|
||||
|
||||
const proxyResponse = await apiClient.post(`/external-calls/execute`, requestPayload);
|
||||
const proxyResponse = await apiClient.post("/external-calls/execute", requestPayload);
|
||||
|
||||
console.log(`📡 백엔드 프록시 응답:`, proxyResponse.data);
|
||||
console.log("📡 백엔드 프록시 응답:", proxyResponse.data);
|
||||
|
||||
if (!proxyResponse.data.success) {
|
||||
throw new Error(`프록시 API 호출 실패: ${proxyResponse.data.error || proxyResponse.data.message}`);
|
||||
}
|
||||
|
||||
const responseData = proxyResponse.data.result;
|
||||
console.log(`✅ 외부 API 호출 성공 (프록시):`, responseData);
|
||||
console.log("✅ 외부 API 호출 성공 (프록시):", responseData);
|
||||
|
||||
// 데이터 매핑 처리 (inbound mapping)
|
||||
if (externalCallConfig.dataMappingConfig?.inboundMapping) {
|
||||
console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`);
|
||||
console.log(`📥 매핑 설정:`, externalCallConfig.dataMappingConfig.inboundMapping);
|
||||
console.log(`📥 응답 데이터:`, responseData);
|
||||
|
||||
await this.processInboundMapping(
|
||||
externalCallConfig.dataMappingConfig.inboundMapping,
|
||||
responseData,
|
||||
context
|
||||
);
|
||||
console.log("📥 매핑 설정:", externalCallConfig.dataMappingConfig.inboundMapping);
|
||||
console.log("📥 응답 데이터:", responseData);
|
||||
|
||||
await this.processInboundMapping(externalCallConfig.dataMappingConfig.inboundMapping, responseData, context);
|
||||
} else {
|
||||
console.log(`ℹ️ 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '외부 호출 실행 완료',
|
||||
message: "외부 호출 실행 완료",
|
||||
executionTime: Date.now() - context.startTime,
|
||||
data: responseData,
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('외부 호출 실행 오류:', error);
|
||||
console.error("외부 호출 실행 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `외부 호출 실행 실패: ${error.message}`,
|
||||
|
|
@ -462,10 +440,10 @@ export class ImprovedButtonActionExecutor {
|
|||
private static async executeDataSave(
|
||||
relationships: any,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
console.log(`💾 데이터 저장 실행 시작`);
|
||||
console.log("💾 데이터 저장 실행 시작");
|
||||
|
||||
// 제어 조건 확인
|
||||
const controlConditions = relationships.controlConditions || [];
|
||||
|
|
@ -474,7 +452,7 @@ export class ImprovedButtonActionExecutor {
|
|||
if (!conditionsMet) {
|
||||
return {
|
||||
success: false,
|
||||
message: '제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다',
|
||||
message: "제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다",
|
||||
executionTime: Date.now() - context.startTime,
|
||||
};
|
||||
}
|
||||
|
|
@ -498,12 +476,7 @@ export class ImprovedButtonActionExecutor {
|
|||
continue;
|
||||
}
|
||||
|
||||
const actionResult = await this.executeDataAction(
|
||||
action,
|
||||
relationships,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
const actionResult = await this.executeDataAction(action, relationships, formData, context);
|
||||
results.push(actionResult);
|
||||
|
||||
if (!actionResult.success) {
|
||||
|
|
@ -512,7 +485,7 @@ export class ImprovedButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const successCount = results.filter((r) => r.success).length;
|
||||
const totalCount = results.length;
|
||||
|
||||
return {
|
||||
|
|
@ -525,9 +498,8 @@ export class ImprovedButtonActionExecutor {
|
|||
totalCount,
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('데이터 저장 실행 오류:', error);
|
||||
console.error("데이터 저장 실행 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `데이터 저장 실행 실패: ${error.message}`,
|
||||
|
|
@ -544,48 +516,53 @@ export class ImprovedButtonActionExecutor {
|
|||
action: any,
|
||||
relationships: any,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
console.log(`🔧 데이터 액션 실행: ${action.name} (${action.actionType})`);
|
||||
console.log("📥 받은 formData:", formData);
|
||||
console.log("📥 formData 키들:", Object.keys(formData));
|
||||
|
||||
// 필드 매핑 처리
|
||||
const mappedData: Record<string, any> = {};
|
||||
|
||||
// 🔥 UPDATE 액션의 경우 formData를 기본으로 시작 (기본키 포함)
|
||||
const mappedData: Record<string, any> = action.actionType === "update" ? { ...formData } : {};
|
||||
|
||||
// 필드 매핑 처리 (기존 데이터에 덮어쓰기)
|
||||
for (const mapping of action.fieldMappings) {
|
||||
if (mapping.valueType === 'static') {
|
||||
if (mapping.valueType === "static") {
|
||||
// 정적 값 처리
|
||||
let value = mapping.value;
|
||||
if (value === '#NOW') {
|
||||
if (value === "#NOW") {
|
||||
value = new Date().toISOString();
|
||||
}
|
||||
mappedData[mapping.targetField] = value;
|
||||
console.log(`🔧 정적 값 매핑: ${mapping.targetField} = ${value}`);
|
||||
} else {
|
||||
// 필드 매핑 처리
|
||||
const sourceField = mapping.fromField?.columnName;
|
||||
if (sourceField && formData[sourceField] !== undefined) {
|
||||
mappedData[mapping.toField.columnName] = formData[sourceField];
|
||||
console.log(`🔧 필드 매핑: ${sourceField} → ${mapping.toField.columnName} = ${formData[sourceField]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📋 매핑된 데이터:`, mappedData);
|
||||
console.log("📋 최종 매핑된 데이터:", mappedData);
|
||||
console.log("🔑 기본키 포함 여부 체크:", {
|
||||
hasId: "id" in mappedData,
|
||||
keys: Object.keys(mappedData),
|
||||
values: Object.values(mappedData),
|
||||
});
|
||||
|
||||
// 대상 연결 정보
|
||||
const toConnection = relationships.toConnection;
|
||||
const targetTable = relationships.toTable?.tableName;
|
||||
|
||||
if (!targetTable) {
|
||||
throw new Error('대상 테이블이 지정되지 않았습니다');
|
||||
throw new Error("대상 테이블이 지정되지 않았습니다");
|
||||
}
|
||||
|
||||
// 데이터 저장 API 호출
|
||||
const saveResult = await this.saveDataToTable(
|
||||
targetTable,
|
||||
mappedData,
|
||||
action.actionType,
|
||||
toConnection
|
||||
);
|
||||
const saveResult = await this.saveDataToTable(targetTable, mappedData, action.actionType, toConnection);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -593,7 +570,6 @@ export class ImprovedButtonActionExecutor {
|
|||
executionTime: Date.now() - context.startTime,
|
||||
data: saveResult,
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`데이터 액션 실행 오류: ${action.name}`, error);
|
||||
return {
|
||||
|
|
@ -612,17 +588,17 @@ export class ImprovedButtonActionExecutor {
|
|||
tableName: string,
|
||||
data: Record<string, any>,
|
||||
actionType: string,
|
||||
connection?: any
|
||||
connection?: any,
|
||||
): Promise<any> {
|
||||
try {
|
||||
console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, {
|
||||
actionType,
|
||||
data,
|
||||
connection
|
||||
connection,
|
||||
});
|
||||
|
||||
// 데이터 저장 API 호출 (apiClient 사용)
|
||||
const response = await apiClient.post('/dataflow/execute-data-action', {
|
||||
const response = await apiClient.post("/dataflow/execute-data-action", {
|
||||
tableName,
|
||||
data,
|
||||
actionType,
|
||||
|
|
@ -632,7 +608,7 @@ export class ImprovedButtonActionExecutor {
|
|||
console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('테이블 데이터 저장 오류:', error);
|
||||
console.error("테이블 데이터 저장 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -643,9 +619,9 @@ export class ImprovedButtonActionExecutor {
|
|||
private static evaluateConditions(
|
||||
conditions: any[],
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): boolean {
|
||||
console.log(`🔍 조건 평가 시작:`, {
|
||||
console.log("🔍 조건 평가 시작:", {
|
||||
conditions,
|
||||
formDataKeys: Object.keys(formData),
|
||||
formData,
|
||||
|
|
@ -657,7 +633,7 @@ export class ImprovedButtonActionExecutor {
|
|||
const conditionValue = condition.value;
|
||||
const operator = condition.operator;
|
||||
|
||||
console.log(`🔍 개별 조건 검증:`, {
|
||||
console.log("🔍 개별 조건 검증:", {
|
||||
field: condition.field,
|
||||
operator,
|
||||
expectedValue: conditionValue,
|
||||
|
|
@ -668,22 +644,22 @@ export class ImprovedButtonActionExecutor {
|
|||
|
||||
let conditionMet = false;
|
||||
switch (operator) {
|
||||
case '=':
|
||||
case "=":
|
||||
conditionMet = fieldValue === conditionValue;
|
||||
break;
|
||||
case '!=':
|
||||
case "!=":
|
||||
conditionMet = fieldValue !== conditionValue;
|
||||
break;
|
||||
case '>':
|
||||
case ">":
|
||||
conditionMet = Number(fieldValue) > Number(conditionValue);
|
||||
break;
|
||||
case '<':
|
||||
case "<":
|
||||
conditionMet = Number(fieldValue) < Number(conditionValue);
|
||||
break;
|
||||
case '>=':
|
||||
case ">=":
|
||||
conditionMet = Number(fieldValue) >= Number(conditionValue);
|
||||
break;
|
||||
case '<=':
|
||||
case "<=":
|
||||
conditionMet = Number(fieldValue) <= Number(conditionValue);
|
||||
break;
|
||||
default:
|
||||
|
|
@ -693,13 +669,13 @@ export class ImprovedButtonActionExecutor {
|
|||
|
||||
if (!conditionMet) {
|
||||
console.log(`❌ 조건 불만족: ${condition.field} ${operator} ${conditionValue} (실제값: ${fieldValue})`);
|
||||
console.log(`❌ 사용 가능한 필드들:`, Object.keys(formData));
|
||||
console.log(`❌ 전체 formData:`, formData);
|
||||
console.log("❌ 사용 가능한 필드들:", Object.keys(formData));
|
||||
console.log("❌ 전체 formData:", formData);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 모든 조건 만족`);
|
||||
console.log("✅ 모든 조건 만족");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -708,94 +684,94 @@ export class ImprovedButtonActionExecutor {
|
|||
*/
|
||||
private static extractActualData(responseData: any): any {
|
||||
console.log(`🔍 데이터 추출 시작 - 원본 타입: ${typeof responseData}`);
|
||||
|
||||
|
||||
// null이나 undefined인 경우
|
||||
if (!responseData) {
|
||||
console.log(`⚠️ 응답 데이터가 null 또는 undefined`);
|
||||
console.log("⚠️ 응답 데이터가 null 또는 undefined");
|
||||
return [];
|
||||
}
|
||||
|
||||
// 이미 배열인 경우 (직접 배열 응답)
|
||||
if (Array.isArray(responseData)) {
|
||||
console.log(`✅ 직접 배열 응답 감지`);
|
||||
console.log("✅ 직접 배열 응답 감지");
|
||||
return responseData;
|
||||
}
|
||||
|
||||
// 문자열인 경우 JSON 파싱 시도
|
||||
if (typeof responseData === 'string') {
|
||||
console.log(`🔄 JSON 문자열 파싱 시도`);
|
||||
if (typeof responseData === "string") {
|
||||
console.log("🔄 JSON 문자열 파싱 시도");
|
||||
try {
|
||||
const parsed = JSON.parse(responseData);
|
||||
console.log(`✅ JSON 파싱 성공, 재귀 호출`);
|
||||
console.log("✅ JSON 파싱 성공, 재귀 호출");
|
||||
return this.extractActualData(parsed);
|
||||
} catch (error) {
|
||||
console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error);
|
||||
console.log("⚠️ JSON 파싱 실패, 원본 문자열 반환:", error);
|
||||
return [responseData];
|
||||
}
|
||||
}
|
||||
|
||||
// 객체가 아닌 경우 (숫자 등)
|
||||
if (typeof responseData !== 'object') {
|
||||
if (typeof responseData !== "object") {
|
||||
console.log(`⚠️ 객체가 아닌 응답: ${typeof responseData}`);
|
||||
return [responseData];
|
||||
}
|
||||
|
||||
// 일반적인 데이터 필드명들을 우선순위대로 확인
|
||||
const commonDataFields = [
|
||||
'data', // { data: [...] }
|
||||
'result', // { result: [...] }
|
||||
'results', // { results: [...] }
|
||||
'items', // { items: [...] }
|
||||
'list', // { list: [...] }
|
||||
'records', // { records: [...] }
|
||||
'rows', // { rows: [...] }
|
||||
'content', // { content: [...] }
|
||||
'payload', // { payload: [...] }
|
||||
'response', // { response: [...] }
|
||||
"data", // { data: [...] }
|
||||
"result", // { result: [...] }
|
||||
"results", // { results: [...] }
|
||||
"items", // { items: [...] }
|
||||
"list", // { list: [...] }
|
||||
"records", // { records: [...] }
|
||||
"rows", // { rows: [...] }
|
||||
"content", // { content: [...] }
|
||||
"payload", // { payload: [...] }
|
||||
"response", // { response: [...] }
|
||||
];
|
||||
|
||||
for (const field of commonDataFields) {
|
||||
if (responseData[field] !== undefined) {
|
||||
console.log(`✅ '${field}' 필드에서 데이터 추출`);
|
||||
|
||||
|
||||
const extractedData = responseData[field];
|
||||
|
||||
|
||||
// 추출된 데이터가 문자열인 경우 JSON 파싱 시도
|
||||
if (typeof extractedData === 'string') {
|
||||
console.log(`🔄 추출된 데이터가 JSON 문자열, 파싱 시도`);
|
||||
if (typeof extractedData === "string") {
|
||||
console.log("🔄 추출된 데이터가 JSON 문자열, 파싱 시도");
|
||||
try {
|
||||
const parsed = JSON.parse(extractedData);
|
||||
console.log(`✅ JSON 파싱 성공, 재귀 호출`);
|
||||
console.log("✅ JSON 파싱 성공, 재귀 호출");
|
||||
return this.extractActualData(parsed);
|
||||
} catch (error) {
|
||||
console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error);
|
||||
console.log("⚠️ JSON 파싱 실패, 원본 문자열 반환:", error);
|
||||
return [extractedData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 추출된 데이터가 객체이고 또 다른 중첩 구조일 수 있으므로 재귀 호출
|
||||
if (typeof extractedData === 'object' && !Array.isArray(extractedData)) {
|
||||
console.log(`🔄 중첩된 객체 감지, 재귀 추출 시도`);
|
||||
if (typeof extractedData === "object" && !Array.isArray(extractedData)) {
|
||||
console.log("🔄 중첩된 객체 감지, 재귀 추출 시도");
|
||||
return this.extractActualData(extractedData);
|
||||
}
|
||||
|
||||
|
||||
return extractedData;
|
||||
}
|
||||
}
|
||||
|
||||
// 특별한 필드가 없는 경우, 객체의 값들 중에서 배열을 찾기
|
||||
const objectValues = Object.values(responseData);
|
||||
const arrayValue = objectValues.find(value => Array.isArray(value));
|
||||
|
||||
const arrayValue = objectValues.find((value) => Array.isArray(value));
|
||||
|
||||
if (arrayValue) {
|
||||
console.log(`✅ 객체 값 중 배열 발견`);
|
||||
console.log("✅ 객체 값 중 배열 발견");
|
||||
return arrayValue;
|
||||
}
|
||||
|
||||
// 객체의 값들 중에서 객체를 찾아서 재귀 탐색
|
||||
for (const value of objectValues) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
console.log(`🔄 객체 값에서 재귀 탐색`);
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
console.log("🔄 객체 값에서 재귀 탐색");
|
||||
const nestedResult = this.extractActualData(value);
|
||||
if (Array.isArray(nestedResult) && nestedResult.length > 0) {
|
||||
return nestedResult;
|
||||
|
|
@ -804,7 +780,7 @@ export class ImprovedButtonActionExecutor {
|
|||
}
|
||||
|
||||
// 모든 시도가 실패한 경우, 원본 객체를 단일 항목 배열로 반환
|
||||
console.log(`📦 원본 객체를 단일 항목으로 처리`);
|
||||
console.log("📦 원본 객체를 단일 항목으로 처리");
|
||||
return [responseData];
|
||||
}
|
||||
|
||||
|
|
@ -814,77 +790,76 @@ export class ImprovedButtonActionExecutor {
|
|||
private static async processInboundMapping(
|
||||
inboundMapping: any,
|
||||
responseData: any,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log(`📥 인바운드 데이터 매핑 처리 시작`);
|
||||
console.log(`📥 원본 응답 데이터:`, responseData);
|
||||
console.log("📥 인바운드 데이터 매핑 처리 시작");
|
||||
console.log("📥 원본 응답 데이터:", responseData);
|
||||
|
||||
const targetTable = inboundMapping.targetTable;
|
||||
const fieldMappings = inboundMapping.fieldMappings || [];
|
||||
const insertMode = inboundMapping.insertMode || 'insert';
|
||||
const insertMode = inboundMapping.insertMode || "insert";
|
||||
|
||||
console.log(`📥 매핑 설정:`, {
|
||||
console.log("📥 매핑 설정:", {
|
||||
targetTable,
|
||||
fieldMappings,
|
||||
insertMode
|
||||
insertMode,
|
||||
});
|
||||
|
||||
// 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원)
|
||||
let actualData = this.extractActualData(responseData);
|
||||
|
||||
console.log(`📥 추출된 실제 데이터:`, actualData);
|
||||
const actualData = this.extractActualData(responseData);
|
||||
|
||||
console.log("📥 추출된 실제 데이터:", actualData);
|
||||
|
||||
// 배열이 아닌 경우 배열로 변환
|
||||
const dataArray = Array.isArray(actualData) ? actualData : [actualData];
|
||||
|
||||
console.log(`📥 처리할 데이터 배열:`, dataArray);
|
||||
|
||||
console.log("📥 처리할 데이터 배열:", dataArray);
|
||||
|
||||
if (dataArray.length === 0) {
|
||||
console.log(`⚠️ 처리할 데이터가 없습니다`);
|
||||
console.log("⚠️ 처리할 데이터가 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of dataArray) {
|
||||
const mappedData: Record<string, any> = {};
|
||||
|
||||
console.log(`📥 개별 아이템 처리:`, item);
|
||||
console.log("📥 개별 아이템 처리:", item);
|
||||
|
||||
// 필드 매핑 적용
|
||||
for (const mapping of fieldMappings) {
|
||||
const sourceValue = item[mapping.sourceField];
|
||||
console.log(`📥 필드 매핑: ${mapping.sourceField} -> ${mapping.targetField} = ${sourceValue}`);
|
||||
|
||||
|
||||
if (sourceValue !== undefined && sourceValue !== null) {
|
||||
mappedData[mapping.targetField] = sourceValue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📋 매핑된 데이터:`, mappedData);
|
||||
console.log("📋 매핑된 데이터:", mappedData);
|
||||
|
||||
// 매핑된 데이터가 비어있지 않은 경우에만 저장
|
||||
if (Object.keys(mappedData).length > 0) {
|
||||
await this.saveDataToTable(targetTable, mappedData, insertMode);
|
||||
} else {
|
||||
console.log(`⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다`);
|
||||
console.log("⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 인바운드 데이터 매핑 완료`);
|
||||
console.log("✅ 인바운드 데이터 매핑 완료");
|
||||
} catch (error) {
|
||||
console.error('인바운드 데이터 매핑 오류:', error);
|
||||
console.error("인바운드 데이터 매핑 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 메인 액션 실행
|
||||
*/
|
||||
private static async executeMainAction(
|
||||
buttonConfig: ExtendedButtonTypeConfig,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
context: ButtonExecutionContext,
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
// 기존 ButtonActionExecutor 로직을 여기서 호출하거나
|
||||
|
|
@ -918,7 +893,7 @@ export class ImprovedButtonActionExecutor {
|
|||
private static async handleExecutionError(
|
||||
error: Error,
|
||||
results: ExecutionResult[],
|
||||
buttonConfig: ExtendedButtonTypeConfig
|
||||
buttonConfig: ExtendedButtonTypeConfig,
|
||||
): Promise<void> {
|
||||
console.error("🔄 실행 오류 처리 시작:", error.message);
|
||||
|
||||
|
|
@ -926,10 +901,10 @@ export class ImprovedButtonActionExecutor {
|
|||
const rollbackNeeded = buttonConfig.dataflowConfig?.executionOptions?.rollbackOnError;
|
||||
if (rollbackNeeded) {
|
||||
console.log("🔄 롤백 처리 시작...");
|
||||
|
||||
|
||||
// 성공한 결과들을 역순으로 롤백
|
||||
const successfulResults = results.filter(r => r.success).reverse();
|
||||
|
||||
const successfulResults = results.filter((r) => r.success).reverse();
|
||||
|
||||
for (const result of successfulResults) {
|
||||
try {
|
||||
// 롤백 로직 구현 (필요시)
|
||||
|
|
|
|||
Loading…
Reference in New Issue