rest api 액션노드 기능변경
This commit is contained in:
parent
68308efd22
commit
1274f58c3c
|
|
@ -21,7 +21,7 @@
|
||||||
"imap": "^0.8.19",
|
"imap": "^0.8.19",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mailparser": "^3.7.4",
|
"mailparser": "^3.7.5",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.15.0",
|
"mysql2": "^3.15.0",
|
||||||
|
|
@ -7723,48 +7723,52 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mailparser": {
|
"node_modules/mailparser": {
|
||||||
"version": "3.7.4",
|
"version": "3.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.5.tgz",
|
||||||
"integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==",
|
"integrity": "sha512-o59RgZC+4SyCOn4xRH1mtRiZ1PbEmi6si6Ufnd3tbX/V9zmZN1qcqu8xbXY62H6CwIclOT3ppm5u/wV2nujn4g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"encoding-japanese": "2.2.0",
|
"encoding-japanese": "2.2.0",
|
||||||
"he": "1.2.0",
|
"he": "1.2.0",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"iconv-lite": "0.6.3",
|
"iconv-lite": "0.7.0",
|
||||||
"libmime": "5.3.7",
|
"libmime": "5.3.7",
|
||||||
"linkify-it": "5.0.0",
|
"linkify-it": "5.0.0",
|
||||||
"mailsplit": "5.4.5",
|
"mailsplit": "5.4.6",
|
||||||
"nodemailer": "7.0.4",
|
"nodemailer": "7.0.9",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"tlds": "1.259.0"
|
"tlds": "1.260.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mailparser/node_modules/iconv-lite": {
|
"node_modules/mailparser/node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mailparser/node_modules/nodemailer": {
|
"node_modules/mailparser/node_modules/nodemailer": {
|
||||||
"version": "7.0.4",
|
"version": "7.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz",
|
||||||
"integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==",
|
"integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mailsplit": {
|
"node_modules/mailsplit": {
|
||||||
"version": "5.4.5",
|
"version": "5.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.6.tgz",
|
||||||
"integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==",
|
"integrity": "sha512-M+cqmzaPG/mEiCDmqQUz8L177JZLZmXAUpq38owtpq2xlXlTSw+kntnxRt2xsxVFFV6+T8Mj/U0l5s7s6e0rNw==",
|
||||||
"license": "(MIT OR EUPL-1.1+)",
|
"license": "(MIT OR EUPL-1.1+)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libbase64": "1.3.0",
|
"libbase64": "1.3.0",
|
||||||
|
|
@ -9787,9 +9791,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tlds": {
|
"node_modules/tlds": {
|
||||||
"version": "1.259.0",
|
"version": "1.260.0",
|
||||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz",
|
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.260.0.tgz",
|
||||||
"integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==",
|
"integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tlds": "bin.js"
|
"tlds": "bin.js"
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"imap": "^0.8.19",
|
"imap": "^0.8.19",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mailparser": "^3.7.4",
|
"mailparser": "^3.7.5",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.15.0",
|
"mysql2": "^3.15.0",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
import { query, queryOne, transaction } from "../database/db";
|
import { query, queryOne, transaction } from "../database/db";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
// ===== 타입 정의 =====
|
// ===== 타입 정의 =====
|
||||||
|
|
||||||
|
|
@ -410,6 +411,9 @@ export class NodeFlowExecutionService {
|
||||||
case "tableSource":
|
case "tableSource":
|
||||||
return this.executeTableSource(node, context);
|
return this.executeTableSource(node, context);
|
||||||
|
|
||||||
|
case "restAPISource":
|
||||||
|
return this.executeRestAPISource(node, context);
|
||||||
|
|
||||||
case "dataTransform":
|
case "dataTransform":
|
||||||
return this.executeDataTransform(node, inputData, context);
|
return this.executeDataTransform(node, inputData, context);
|
||||||
|
|
||||||
|
|
@ -440,6 +444,123 @@ export class NodeFlowExecutionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API 소스 노드 실행
|
||||||
|
*/
|
||||||
|
private static async executeRestAPISource(
|
||||||
|
node: FlowNode,
|
||||||
|
context: ExecutionContext
|
||||||
|
): Promise<any[]> {
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
method = "GET",
|
||||||
|
headers = {},
|
||||||
|
body,
|
||||||
|
timeout = 30000,
|
||||||
|
responseMapping,
|
||||||
|
authentication,
|
||||||
|
} = node.data;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("REST API URL이 설정되지 않았습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`🌐 REST API 호출: ${method} ${url}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 헤더 설정
|
||||||
|
const requestHeaders: any = { ...headers };
|
||||||
|
|
||||||
|
// 인증 헤더 추가
|
||||||
|
if (authentication) {
|
||||||
|
if (authentication.type === "bearer" && authentication.token) {
|
||||||
|
requestHeaders["Authorization"] = `Bearer ${authentication.token}`;
|
||||||
|
} else if (
|
||||||
|
authentication.type === "basic" &&
|
||||||
|
authentication.username &&
|
||||||
|
authentication.password
|
||||||
|
) {
|
||||||
|
const credentials = Buffer.from(
|
||||||
|
`${authentication.username}:${authentication.password}`
|
||||||
|
).toString("base64");
|
||||||
|
requestHeaders["Authorization"] = `Basic ${credentials}`;
|
||||||
|
} else if (authentication.type === "apikey" && authentication.token) {
|
||||||
|
const headerName = authentication.apiKeyHeader || "X-API-Key";
|
||||||
|
requestHeaders[headerName] = authentication.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestHeaders["Content-Type"]) {
|
||||||
|
requestHeaders["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
const response = await axios({
|
||||||
|
method: method.toLowerCase(),
|
||||||
|
url,
|
||||||
|
headers: requestHeaders,
|
||||||
|
data: body,
|
||||||
|
timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`✅ REST API 응답 수신: ${response.status}`);
|
||||||
|
|
||||||
|
let responseData = response.data;
|
||||||
|
|
||||||
|
// 🔥 표준 API 응답 형식 자동 감지 { success, message, data }
|
||||||
|
if (
|
||||||
|
!responseMapping &&
|
||||||
|
responseData &&
|
||||||
|
typeof responseData === "object" &&
|
||||||
|
"success" in responseData &&
|
||||||
|
"data" in responseData
|
||||||
|
) {
|
||||||
|
logger.info("🔍 표준 API 응답 형식 감지, data 속성 자동 추출");
|
||||||
|
responseData = responseData.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// responseMapping이 있으면 해당 경로의 데이터 추출
|
||||||
|
if (responseMapping && responseData) {
|
||||||
|
logger.info(`🔍 응답 매핑 적용: ${responseMapping}`);
|
||||||
|
const path = responseMapping.split(".");
|
||||||
|
for (const key of path) {
|
||||||
|
if (
|
||||||
|
responseData &&
|
||||||
|
typeof responseData === "object" &&
|
||||||
|
key in responseData
|
||||||
|
) {
|
||||||
|
responseData = responseData[key];
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`⚠️ 응답 매핑 경로를 찾을 수 없습니다: ${responseMapping}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 배열이 아니면 배열로 변환
|
||||||
|
if (!Array.isArray(responseData)) {
|
||||||
|
logger.info("🔄 단일 객체를 배열로 변환");
|
||||||
|
responseData = [responseData];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`📦 REST API 데이터 ${responseData.length}건 반환`);
|
||||||
|
|
||||||
|
// 첫 번째 데이터 샘플 상세 로깅
|
||||||
|
if (responseData.length > 0) {
|
||||||
|
console.log("🔍 REST API 응답 데이터 샘플 (첫 번째 항목):");
|
||||||
|
console.log(JSON.stringify(responseData[0], null, 2));
|
||||||
|
console.log("🔑 사용 가능한 필드명:", Object.keys(responseData[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`❌ REST API 호출 실패:`, error.message);
|
||||||
|
throw new Error(`REST API 호출 실패: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블 소스 노드 실행
|
* 테이블 소스 노드 실행
|
||||||
*/
|
*/
|
||||||
|
|
@ -521,6 +642,19 @@ export class NodeFlowExecutionService {
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { targetTable, fieldMappings } = node.data;
|
const { targetTable, fieldMappings } = node.data;
|
||||||
|
|
||||||
|
logger.info(`💾 INSERT 노드 실행: ${targetTable}`);
|
||||||
|
console.log(
|
||||||
|
"📥 입력 데이터 타입:",
|
||||||
|
typeof inputData,
|
||||||
|
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inputData && inputData.length > 0) {
|
||||||
|
console.log("📄 첫 번째 입력 데이터:");
|
||||||
|
console.log(JSON.stringify(inputData[0], null, 2));
|
||||||
|
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
|
||||||
|
}
|
||||||
|
|
||||||
return transaction(async (client) => {
|
return transaction(async (client) => {
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
let insertedCount = 0;
|
let insertedCount = 0;
|
||||||
|
|
@ -529,12 +663,17 @@ export class NodeFlowExecutionService {
|
||||||
const fields: string[] = [];
|
const fields: string[] = [];
|
||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
|
|
||||||
|
console.log("🗺️ 필드 매핑 처리 중...");
|
||||||
fieldMappings.forEach((mapping: any) => {
|
fieldMappings.forEach((mapping: any) => {
|
||||||
fields.push(mapping.targetField);
|
fields.push(mapping.targetField);
|
||||||
const value =
|
const value =
|
||||||
mapping.staticValue !== undefined
|
mapping.staticValue !== undefined
|
||||||
? mapping.staticValue
|
? mapping.staticValue
|
||||||
: data[mapping.sourceField];
|
: data[mapping.sourceField];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
` ${mapping.sourceField} → ${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}`
|
||||||
|
);
|
||||||
values.push(value);
|
values.push(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -543,6 +682,9 @@ export class NodeFlowExecutionService {
|
||||||
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})
|
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
console.log("📝 실행할 SQL:", sql);
|
||||||
|
console.log("📊 바인딩 값:", values);
|
||||||
|
|
||||||
await client.query(sql, values);
|
await client.query(sql, values);
|
||||||
insertedCount++;
|
insertedCount++;
|
||||||
}
|
}
|
||||||
|
|
@ -682,7 +824,6 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
logger.info(`🌐 REST API INSERT 시작: ${apiMethod} ${apiEndpoint}`);
|
logger.info(`🌐 REST API INSERT 시작: ${apiMethod} ${apiEndpoint}`);
|
||||||
|
|
||||||
const axios = require("axios");
|
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
|
|
||||||
|
|
@ -895,6 +1036,19 @@ export class NodeFlowExecutionService {
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { targetTable, fieldMappings, whereConditions } = node.data;
|
const { targetTable, fieldMappings, whereConditions } = node.data;
|
||||||
|
|
||||||
|
logger.info(`🔄 UPDATE 노드 실행: ${targetTable}`);
|
||||||
|
console.log(
|
||||||
|
"📥 입력 데이터 타입:",
|
||||||
|
typeof inputData,
|
||||||
|
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inputData && inputData.length > 0) {
|
||||||
|
console.log("📄 첫 번째 입력 데이터:");
|
||||||
|
console.log(JSON.stringify(inputData[0], null, 2));
|
||||||
|
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
|
||||||
|
}
|
||||||
|
|
||||||
return transaction(async (client) => {
|
return transaction(async (client) => {
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
|
|
@ -904,11 +1058,16 @@ export class NodeFlowExecutionService {
|
||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
console.log("🗺️ 필드 매핑 처리 중...");
|
||||||
fieldMappings.forEach((mapping: any) => {
|
fieldMappings.forEach((mapping: any) => {
|
||||||
const value =
|
const value =
|
||||||
mapping.staticValue !== undefined
|
mapping.staticValue !== undefined
|
||||||
? mapping.staticValue
|
? mapping.staticValue
|
||||||
: data[mapping.sourceField];
|
: data[mapping.sourceField];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
` ${mapping.sourceField} → ${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}`
|
||||||
|
);
|
||||||
setClauses.push(`${mapping.targetField} = $${paramIndex}`);
|
setClauses.push(`${mapping.targetField} = $${paramIndex}`);
|
||||||
values.push(value);
|
values.push(value);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
|
|
@ -926,6 +1085,9 @@ export class NodeFlowExecutionService {
|
||||||
${whereClause}
|
${whereClause}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
console.log("📝 실행할 SQL:", sql);
|
||||||
|
console.log("📊 바인딩 값:", values);
|
||||||
|
|
||||||
const result = await client.query(sql, values);
|
const result = await client.query(sql, values);
|
||||||
updatedCount += result.rowCount || 0;
|
updatedCount += result.rowCount || 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1086,7 +1248,6 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
logger.info(`🌐 REST API UPDATE 시작: ${apiMethod} ${apiEndpoint}`);
|
logger.info(`🌐 REST API UPDATE 시작: ${apiMethod} ${apiEndpoint}`);
|
||||||
|
|
||||||
const axios = require("axios");
|
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
|
|
||||||
|
|
@ -1197,15 +1358,31 @@ export class NodeFlowExecutionService {
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { targetTable, whereConditions } = node.data;
|
const { targetTable, whereConditions } = node.data;
|
||||||
|
|
||||||
|
logger.info(`🗑️ DELETE 노드 실행: ${targetTable}`);
|
||||||
|
console.log(
|
||||||
|
"📥 입력 데이터 타입:",
|
||||||
|
typeof inputData,
|
||||||
|
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inputData && inputData.length > 0) {
|
||||||
|
console.log("📄 첫 번째 입력 데이터:");
|
||||||
|
console.log(JSON.stringify(inputData[0], null, 2));
|
||||||
|
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
|
||||||
|
}
|
||||||
|
|
||||||
return transaction(async (client) => {
|
return transaction(async (client) => {
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
|
|
||||||
for (const data of dataArray) {
|
for (const data of dataArray) {
|
||||||
|
console.log("🔍 WHERE 조건 처리 중...");
|
||||||
const whereClause = this.buildWhereClause(whereConditions, data, 1);
|
const whereClause = this.buildWhereClause(whereConditions, data, 1);
|
||||||
|
|
||||||
const sql = `DELETE FROM ${targetTable} ${whereClause}`;
|
const sql = `DELETE FROM ${targetTable} ${whereClause}`;
|
||||||
|
|
||||||
|
console.log("📝 실행할 SQL:", sql);
|
||||||
|
|
||||||
const result = await client.query(sql, []);
|
const result = await client.query(sql, []);
|
||||||
deletedCount += result.rowCount || 0;
|
deletedCount += result.rowCount || 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1339,7 +1516,6 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
logger.info(`🌐 REST API DELETE 시작: ${apiEndpoint}`);
|
logger.info(`🌐 REST API DELETE 시작: ${apiEndpoint}`);
|
||||||
|
|
||||||
const axios = require("axios");
|
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
|
|
||||||
|
|
@ -1440,6 +1616,20 @@ export class NodeFlowExecutionService {
|
||||||
throw new Error("UPSERT 액션에 충돌 키(Conflict Keys)가 필요합니다.");
|
throw new Error("UPSERT 액션에 충돌 키(Conflict Keys)가 필요합니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`🔀 UPSERT 노드 실행: ${targetTable}`);
|
||||||
|
console.log(
|
||||||
|
"📥 입력 데이터 타입:",
|
||||||
|
typeof inputData,
|
||||||
|
Array.isArray(inputData) ? `배열(${inputData.length}건)` : "단일 객체"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inputData && inputData.length > 0) {
|
||||||
|
console.log("📄 첫 번째 입력 데이터:");
|
||||||
|
console.log(JSON.stringify(inputData[0], null, 2));
|
||||||
|
console.log("🔑 입력 데이터 필드명:", Object.keys(inputData[0]));
|
||||||
|
}
|
||||||
|
console.log("🔑 충돌 키:", conflictKeys);
|
||||||
|
|
||||||
return transaction(async (client) => {
|
return transaction(async (client) => {
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
let insertedCount = 0;
|
let insertedCount = 0;
|
||||||
|
|
@ -1466,7 +1656,10 @@ export class NodeFlowExecutionService {
|
||||||
(key: string) => conflictKeyValues[key]
|
(key: string) => conflictKeyValues[key]
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkSql = `SELECT id FROM ${targetTable} WHERE ${whereConditions} LIMIT 1`;
|
console.log("🔍 존재 여부 확인 - WHERE 조건:", whereConditions);
|
||||||
|
console.log("🔍 존재 여부 확인 - 바인딩 값:", whereValues);
|
||||||
|
|
||||||
|
const checkSql = `SELECT 1 FROM ${targetTable} WHERE ${whereConditions} LIMIT 1`;
|
||||||
const existingRow = await client.query(checkSql, whereValues);
|
const existingRow = await client.query(checkSql, whereValues);
|
||||||
|
|
||||||
if (existingRow.rows.length > 0) {
|
if (existingRow.rows.length > 0) {
|
||||||
|
|
@ -1780,7 +1973,6 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
logger.info(`🌐 REST API UPSERT 시작: ${apiMethod} ${apiEndpoint}`);
|
logger.info(`🌐 REST API UPSERT 시작: ${apiMethod} ${apiEndpoint}`);
|
||||||
|
|
||||||
const axios = require("axios");
|
|
||||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
|
|
||||||
|
|
@ -1977,6 +2169,20 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
const success = summary.failed === 0;
|
const success = summary.failed === 0;
|
||||||
|
|
||||||
|
// 실패한 노드 상세 로깅
|
||||||
|
if (!success) {
|
||||||
|
const failedNodes = nodeSummaries.filter((n) => n.status === "failed");
|
||||||
|
logger.error(
|
||||||
|
`❌ 실패한 노드들:`,
|
||||||
|
failedNodes.map((n) => ({
|
||||||
|
nodeId: n.nodeId,
|
||||||
|
nodeName: n.nodeName,
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
error: n.error,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success,
|
success,
|
||||||
message: success
|
message: success
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ services:
|
||||||
- DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm
|
- DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm
|
||||||
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
|
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
|
||||||
- JWT_EXPIRES_IN=24h
|
- JWT_EXPIRES_IN=24h
|
||||||
|
- ENCRYPTION_KEY=ilshin-plm-encryption-key-2024-secure-32bytes
|
||||||
- CORS_ORIGIN=http://localhost:9771
|
- CORS_ORIGIN=http://localhost:9771
|
||||||
- CORS_CREDENTIALS=true
|
- CORS_CREDENTIALS=true
|
||||||
- LOG_LEVEL=debug
|
- LOG_LEVEL=debug
|
||||||
|
|
@ -26,7 +27,18 @@ services:
|
||||||
- pms-network
|
- pms-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health", "||", "exit", "1"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"wget",
|
||||||
|
"--no-verbose",
|
||||||
|
"--tries=1",
|
||||||
|
"--spider",
|
||||||
|
"http://localhost:8080/health",
|
||||||
|
"||",
|
||||||
|
"exit",
|
||||||
|
"1",
|
||||||
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 15s
|
timeout: 15s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,16 @@ function FlowEditorInner() {
|
||||||
displayName: `새 ${type} 노드`,
|
displayName: `새 ${type} 노드`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// REST API 소스 노드의 경우
|
||||||
|
if (type === "restAPISource") {
|
||||||
|
defaultData.method = "GET";
|
||||||
|
defaultData.url = "";
|
||||||
|
defaultData.headers = {};
|
||||||
|
defaultData.timeout = 30000;
|
||||||
|
defaultData.responseFields = []; // 빈 배열로 초기화
|
||||||
|
defaultData.responseMapping = "";
|
||||||
|
}
|
||||||
|
|
||||||
// 액션 노드의 경우 targetType 기본값 설정
|
// 액션 노드의 경우 targetType 기본값 설정
|
||||||
if (["insertAction", "updateAction", "deleteAction", "upsertAction"].includes(type)) {
|
if (["insertAction", "updateAction", "deleteAction", "upsertAction"].includes(type)) {
|
||||||
defaultData.targetType = "internal"; // 기본값: 내부 DB
|
defaultData.targetType = "internal"; // 기본값: 내부 DB
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,10 @@ export const ConditionNode = memo(({ data, selected }: NodeProps<ConditionNodeDa
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 분기 출력 핸들 */}
|
{/* 분기 출력 핸들 */}
|
||||||
<div className="border-t">
|
<div className="relative border-t">
|
||||||
<div className="grid grid-cols-2">
|
{/* TRUE 출력 - 오른쪽 위 */}
|
||||||
{/* TRUE 출력 */}
|
<div className="relative border-b p-2">
|
||||||
<div className="relative border-r p-2">
|
<div className="flex items-center justify-end gap-1 pr-6 text-xs">
|
||||||
<div className="flex items-center justify-center gap-1 text-xs">
|
|
||||||
<Check className="h-3 w-3 text-green-600" />
|
<Check className="h-3 w-3 text-green-600" />
|
||||||
<span className="font-medium text-green-600">TRUE</span>
|
<span className="font-medium text-green-600">TRUE</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,13 +89,13 @@ export const ConditionNode = memo(({ data, selected }: NodeProps<ConditionNodeDa
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="true"
|
id="true"
|
||||||
className="!-right-1.5 !h-3 !w-3 !border-2 !border-green-500 !bg-white"
|
className="!top-1/2 !-right-1.5 !h-3 !w-3 !-translate-y-1/2 !border-2 !border-green-500 !bg-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FALSE 출력 */}
|
{/* FALSE 출력 - 오른쪽 아래 */}
|
||||||
<div className="relative p-2">
|
<div className="relative p-2">
|
||||||
<div className="flex items-center justify-center gap-1 text-xs">
|
<div className="flex items-center justify-end gap-1 pr-6 text-xs">
|
||||||
<X className="h-3 w-3 text-red-600" />
|
<X className="h-3 w-3 text-red-600" />
|
||||||
<span className="font-medium text-red-600">FALSE</span>
|
<span className="font-medium text-red-600">FALSE</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -104,12 +103,11 @@ export const ConditionNode = memo(({ data, selected }: NodeProps<ConditionNodeDa
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="false"
|
id="false"
|
||||||
className="!-right-1.5 !h-3 !w-3 !border-2 !border-red-500 !bg-white"
|
className="!top-1/2 !-right-1.5 !h-3 !w-3 !-translate-y-1/2 !border-2 !border-red-500 !bg-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
|
|
||||||
// 소스 필드 목록 (연결된 입력 노드에서 가져오기)
|
// 소스 필드 목록 (연결된 입력 노드에서 가져오기)
|
||||||
const [sourceFields, setSourceFields] = useState<Array<{ name: string; label?: string }>>([]);
|
const [sourceFields, setSourceFields] = useState<Array<{ name: string; label?: string }>>([]);
|
||||||
|
// REST API 소스 노드 연결 여부
|
||||||
|
const [hasRestAPISource, setHasRestAPISource] = useState(false);
|
||||||
|
|
||||||
// 🔥 외부 DB 관련 상태
|
// 🔥 외부 DB 관련 상태
|
||||||
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
||||||
|
|
@ -135,9 +137,9 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
const getAllSourceFields = (
|
const getAllSourceFields = (
|
||||||
targetNodeId: string,
|
targetNodeId: string,
|
||||||
visitedNodes = new Set<string>(),
|
visitedNodes = new Set<string>(),
|
||||||
): Array<{ name: string; label?: string }> => {
|
): { fields: Array<{ name: string; label?: string }>; hasRestAPI: boolean } => {
|
||||||
if (visitedNodes.has(targetNodeId)) {
|
if (visitedNodes.has(targetNodeId)) {
|
||||||
return [];
|
return { fields: [], hasRestAPI: false };
|
||||||
}
|
}
|
||||||
visitedNodes.add(targetNodeId);
|
visitedNodes.add(targetNodeId);
|
||||||
|
|
||||||
|
|
@ -146,6 +148,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
||||||
|
|
||||||
const fields: Array<{ name: string; label?: string }> = [];
|
const fields: Array<{ name: string; label?: string }> = [];
|
||||||
|
let foundRestAPI = false;
|
||||||
|
|
||||||
sourceNodes.forEach((node) => {
|
sourceNodes.forEach((node) => {
|
||||||
console.log(`🔍 노드 ${node.id} 타입: ${node.type}`);
|
console.log(`🔍 노드 ${node.id} 타입: ${node.type}`);
|
||||||
|
|
@ -153,18 +156,20 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
|
|
||||||
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
||||||
if (node.type === "dataTransform") {
|
if (node.type === "dataTransform") {
|
||||||
console.log(`✅ 데이터 변환 노드 발견`);
|
console.log("✅ 데이터 변환 노드 발견");
|
||||||
|
|
||||||
// 상위 노드의 원본 필드 먼저 수집
|
// 상위 노드의 원본 필드 먼저 수집
|
||||||
const upperFields = getAllSourceFields(node.id, visitedNodes);
|
const upperResult = getAllSourceFields(node.id, visitedNodes);
|
||||||
|
const upperFields = upperResult.fields;
|
||||||
|
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||||
console.log(` 📤 상위 노드에서 ${upperFields.length}개 필드 가져옴`);
|
console.log(` 📤 상위 노드에서 ${upperFields.length}개 필드 가져옴`);
|
||||||
|
|
||||||
// 변환된 필드 추가 (in-place 변환 고려)
|
// 변환된 필드 추가 (in-place 변환 고려)
|
||||||
if (node.data.transformations && Array.isArray(node.data.transformations)) {
|
if ((node.data as any).transformations && Array.isArray((node.data as any).transformations)) {
|
||||||
console.log(` 📊 ${node.data.transformations.length}개 변환 발견`);
|
console.log(` 📊 ${(node.data as any).transformations.length}개 변환 발견`);
|
||||||
const inPlaceFields = new Set<string>(); // in-place 변환된 필드 추적
|
const inPlaceFields = new Set<string>(); // in-place 변환된 필드 추적
|
||||||
|
|
||||||
node.data.transformations.forEach((transform: any) => {
|
(node.data as any).transformations.forEach((transform: any) => {
|
||||||
const targetField = transform.targetField || transform.sourceField;
|
const targetField = transform.targetField || transform.sourceField;
|
||||||
const isInPlace = !transform.targetField || transform.targetField === transform.sourceField;
|
const isInPlace = !transform.targetField || transform.targetField === transform.sourceField;
|
||||||
|
|
||||||
|
|
@ -196,9 +201,31 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
fields.push(...upperFields);
|
fields.push(...upperFields);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 일반 소스 노드인 경우
|
// REST API 소스 노드인 경우
|
||||||
|
else if (node.type === "restAPISource") {
|
||||||
|
console.log("✅ REST API 소스 노드 발견");
|
||||||
|
foundRestAPI = true;
|
||||||
|
const responseFields = (node.data as any).responseFields;
|
||||||
|
|
||||||
|
if (responseFields && Array.isArray(responseFields)) {
|
||||||
|
console.log(`✅ REST API 노드에서 ${responseFields.length}개 필드 발견`);
|
||||||
|
responseFields.forEach((field: any) => {
|
||||||
|
const fieldName = field.name || field.fieldName;
|
||||||
|
const fieldLabel = field.label || field.displayName;
|
||||||
|
if (fieldName) {
|
||||||
|
fields.push({
|
||||||
|
name: fieldName,
|
||||||
|
label: fieldLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ REST API 노드에 responseFields 없음");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 일반 소스 노드인 경우 (테이블 소스 등)
|
||||||
else {
|
else {
|
||||||
const nodeFields = node.data.fields || node.data.outputFields;
|
const nodeFields = (node.data as any).fields || (node.data as any).outputFields;
|
||||||
|
|
||||||
if (nodeFields && Array.isArray(nodeFields)) {
|
if (nodeFields && Array.isArray(nodeFields)) {
|
||||||
console.log(`✅ 노드 ${node.id}에서 ${nodeFields.length}개 필드 발견`);
|
console.log(`✅ 노드 ${node.id}에서 ${nodeFields.length}개 필드 발견`);
|
||||||
|
|
@ -218,17 +245,19 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return fields;
|
return { fields, hasRestAPI: foundRestAPI };
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔍 INSERT 노드 ID:", nodeId);
|
console.log("🔍 INSERT 노드 ID:", nodeId);
|
||||||
const allFields = getAllSourceFields(nodeId);
|
const result = getAllSourceFields(nodeId);
|
||||||
|
|
||||||
// 중복 제거
|
// 중복 제거
|
||||||
const uniqueFields = Array.from(new Map(allFields.map((field) => [field.name, field])).values());
|
const uniqueFields = Array.from(new Map(result.fields.map((field) => [field.name, field])).values());
|
||||||
|
|
||||||
setSourceFields(uniqueFields);
|
setSourceFields(uniqueFields);
|
||||||
|
setHasRestAPISource(result.hasRestAPI);
|
||||||
console.log("✅ 최종 소스 필드 목록:", uniqueFields);
|
console.log("✅ 최종 소스 필드 목록:", uniqueFields);
|
||||||
|
console.log("✅ REST API 소스 연결:", result.hasRestAPI);
|
||||||
}, [nodeId, nodes, edges]);
|
}, [nodeId, nodes, edges]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -924,10 +953,10 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
<div>
|
<div>
|
||||||
<Label className="mb-1.5 block text-xs font-medium">
|
<Label className="mb-1.5 block text-xs font-medium">
|
||||||
요청 바디 템플릿
|
요청 바디 템플릿
|
||||||
<span className="ml-1 text-gray-500">{`{{fieldName}}`}으로 소스 필드 참조</span>
|
<span className="ml-1 text-gray-500">{"{{fieldName}}"}으로 소스 필드 참조</span>
|
||||||
</Label>
|
</Label>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder={`{\n "name": "{{name}}",\n "email": "{{email}}",\n "age": "{{age}}"\n}`}
|
placeholder={'{\n "name": "{{name}}",\n "email": "{{email}}",\n "age": "{{age}}"\n}'}
|
||||||
value={apiBodyTemplate}
|
value={apiBodyTemplate}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setApiBodyTemplate(e.target.value);
|
setApiBodyTemplate(e.target.value);
|
||||||
|
|
@ -937,7 +966,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
rows={8}
|
rows={8}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
소스 데이터의 필드명을 {`{{필드명}}`} 형태로 참조할 수 있습니다.
|
소스 데이터의 필드명을 {"{{필드명}}"} 형태로 참조할 수 있습니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1011,9 +1040,22 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* 소스 필드 드롭다운 */}
|
{/* 소스 필드 입력/선택 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs text-gray-600">소스 필드</Label>
|
<Label className="text-xs text-gray-600">
|
||||||
|
소스 필드
|
||||||
|
{hasRestAPISource && <span className="ml-1 text-teal-600">(REST API - 직접 입력)</span>}
|
||||||
|
</Label>
|
||||||
|
{hasRestAPISource ? (
|
||||||
|
// REST API 소스인 경우: 직접 입력
|
||||||
|
<Input
|
||||||
|
value={mapping.sourceField || ""}
|
||||||
|
onChange={(e) => handleMappingChange(index, "sourceField", e.target.value || null)}
|
||||||
|
placeholder="필드명 입력 (예: userId, userName)"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// 일반 소스인 경우: 드롭다운 선택
|
||||||
<Select
|
<Select
|
||||||
value={mapping.sourceField || ""}
|
value={mapping.sourceField || ""}
|
||||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||||
|
|
@ -1032,7 +1074,9 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="font-medium">{field.label || field.name}</span>
|
<span className="font-medium">{field.label || field.name}</span>
|
||||||
{field.label && field.label !== field.name && (
|
{field.label && field.label !== field.name && (
|
||||||
<span className="text-muted-foreground font-mono text-xs">{field.name}</span>
|
<span className="text-muted-foreground font-mono text-xs">
|
||||||
|
{field.name}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -1040,6 +1084,10 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
)}
|
||||||
|
{hasRestAPISource && (
|
||||||
|
<p className="mt-1 text-xs text-gray-500">API 응답 JSON의 필드명을 입력하세요</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-1">
|
<div className="flex items-center justify-center py-1">
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||||
const [authToken, setAuthToken] = useState(data.authentication?.token || "");
|
const [authToken, setAuthToken] = useState(data.authentication?.token || "");
|
||||||
const [timeout, setTimeout] = useState(data.timeout?.toString() || "30000");
|
const [timeout, setTimeout] = useState(data.timeout?.toString() || "30000");
|
||||||
const [responseMapping, setResponseMapping] = useState(data.responseMapping || "");
|
const [responseMapping, setResponseMapping] = useState(data.responseMapping || "");
|
||||||
|
const [responseFields, setResponseFields] = useState(data.responseFields || []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayName(data.displayName || "");
|
setDisplayName(data.displayName || "");
|
||||||
|
|
@ -48,6 +49,7 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||||
setAuthToken(data.authentication?.token || "");
|
setAuthToken(data.authentication?.token || "");
|
||||||
setTimeout(data.timeout?.toString() || "30000");
|
setTimeout(data.timeout?.toString() || "30000");
|
||||||
setResponseMapping(data.responseMapping || "");
|
setResponseMapping(data.responseMapping || "");
|
||||||
|
setResponseFields(data.responseFields || []);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
|
|
@ -59,6 +61,10 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("🔧 REST API 노드 업데이트 중...");
|
||||||
|
console.log("📦 responseFields:", responseFields);
|
||||||
|
console.log("📊 responseFields 개수:", responseFields.length);
|
||||||
|
|
||||||
updateNode(nodeId, {
|
updateNode(nodeId, {
|
||||||
displayName,
|
displayName,
|
||||||
url,
|
url,
|
||||||
|
|
@ -71,7 +77,10 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||||
},
|
},
|
||||||
timeout: parseInt(timeout) || 30000,
|
timeout: parseInt(timeout) || 30000,
|
||||||
responseMapping,
|
responseMapping,
|
||||||
|
responseFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("✅ REST API 노드 업데이트 완료");
|
||||||
};
|
};
|
||||||
|
|
||||||
const addHeader = () => {
|
const addHeader = () => {
|
||||||
|
|
@ -88,6 +97,32 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||||
setHeaders(newHeaders);
|
setHeaders(newHeaders);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addResponseField = () => {
|
||||||
|
const newFields = [
|
||||||
|
...responseFields,
|
||||||
|
{
|
||||||
|
name: "",
|
||||||
|
label: "",
|
||||||
|
dataType: "TEXT",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
console.log("➕ 응답 필드 추가:", newFields);
|
||||||
|
setResponseFields(newFields);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateResponseField = (index: number, field: string, value: string) => {
|
||||||
|
const updated = [...responseFields];
|
||||||
|
updated[index] = { ...updated[index], [field]: value };
|
||||||
|
console.log(`✏️ 응답 필드 ${index} 업데이트 (${field}=${value}):`, updated);
|
||||||
|
setResponseFields(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeResponseField = (index: number) => {
|
||||||
|
const newFields = responseFields.filter((_, i) => i !== index);
|
||||||
|
console.log("🗑️ 응답 필드 삭제:", newFields);
|
||||||
|
setResponseFields(newFields);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
<div className="flex items-center gap-2 rounded-md bg-teal-50 p-2">
|
<div className="flex items-center gap-2 rounded-md bg-teal-50 p-2">
|
||||||
|
|
@ -238,6 +273,64 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||||
placeholder="예: data.items"
|
placeholder="예: data.items"
|
||||||
className="mt-1 text-sm"
|
className="mt-1 text-sm"
|
||||||
/>
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">배열 데이터의 경로를 지정하세요 (예: data.items, result.users)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<Label className="text-xs">응답 필드 정의</Label>
|
||||||
|
<Button size="sm" variant="outline" onClick={addResponseField}>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
필드 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[300px] space-y-2 overflow-y-auto">
|
||||||
|
{responseFields.length === 0 ? (
|
||||||
|
<div className="rounded border border-dashed p-3 text-center text-xs text-gray-400">
|
||||||
|
응답 필드를 추가하여 데이터 구조를 정의하세요
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
responseFields.map((field, index) => (
|
||||||
|
<div key={index} className="rounded border bg-gray-50 p-2">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<span className="text-xs font-medium text-gray-700">필드 {index + 1}</span>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => removeResponseField(index)}>
|
||||||
|
<Trash2 className="h-3 w-3 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
value={field.name}
|
||||||
|
onChange={(e) => updateResponseField(index, "name", e.target.value)}
|
||||||
|
placeholder="필드명 (예: userId)"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={field.label || ""}
|
||||||
|
onChange={(e) => updateResponseField(index, "label", e.target.value)}
|
||||||
|
placeholder="표시명 (예: 사용자 ID)"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={field.dataType || "TEXT"}
|
||||||
|
onValueChange={(value) => updateResponseField(index, "dataType", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="text-xs">
|
||||||
|
<SelectValue placeholder="데이터 타입" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="TEXT">텍스트</SelectItem>
|
||||||
|
<SelectItem value="NUMBER">숫자</SelectItem>
|
||||||
|
<SelectItem value="DATE">날짜</SelectItem>
|
||||||
|
<SelectItem value="BOOLEAN">참/거짓</SelectItem>
|
||||||
|
<SelectItem value="JSON">JSON</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleApply} className="w-full">
|
<Button onClick={handleApply} className="w-full">
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
|
|
||||||
// 소스 필드 목록 (연결된 입력 노드에서 가져오기)
|
// 소스 필드 목록 (연결된 입력 노드에서 가져오기)
|
||||||
const [sourceFields, setSourceFields] = useState<Array<{ name: string; label?: string }>>([]);
|
const [sourceFields, setSourceFields] = useState<Array<{ name: string; label?: string }>>([]);
|
||||||
|
// REST API 소스 노드 연결 여부
|
||||||
|
const [hasRestAPISource, setHasRestAPISource] = useState(false);
|
||||||
|
|
||||||
// 🔥 외부 DB 관련 상태
|
// 🔥 외부 DB 관련 상태
|
||||||
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
||||||
|
|
@ -150,9 +152,9 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
const getAllSourceFields = (
|
const getAllSourceFields = (
|
||||||
targetNodeId: string,
|
targetNodeId: string,
|
||||||
visitedNodes = new Set<string>(),
|
visitedNodes = new Set<string>(),
|
||||||
): Array<{ name: string; label?: string }> => {
|
): { fields: Array<{ name: string; label?: string }>; hasRestAPI: boolean } => {
|
||||||
if (visitedNodes.has(targetNodeId)) {
|
if (visitedNodes.has(targetNodeId)) {
|
||||||
return [];
|
return { fields: [], hasRestAPI: false };
|
||||||
}
|
}
|
||||||
visitedNodes.add(targetNodeId);
|
visitedNodes.add(targetNodeId);
|
||||||
|
|
||||||
|
|
@ -161,18 +163,21 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
||||||
|
|
||||||
const fields: Array<{ name: string; label?: string }> = [];
|
const fields: Array<{ name: string; label?: string }> = [];
|
||||||
|
let foundRestAPI = false;
|
||||||
|
|
||||||
sourceNodes.forEach((node) => {
|
sourceNodes.forEach((node) => {
|
||||||
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
||||||
if (node.type === "dataTransform") {
|
if (node.type === "dataTransform") {
|
||||||
// 상위 노드의 원본 필드 먼저 수집
|
// 상위 노드의 원본 필드 먼저 수집
|
||||||
const upperFields = getAllSourceFields(node.id, visitedNodes);
|
const upperResult = getAllSourceFields(node.id, visitedNodes);
|
||||||
|
const upperFields = upperResult.fields;
|
||||||
|
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||||
|
|
||||||
// 변환된 필드 추가 (in-place 변환 고려)
|
// 변환된 필드 추가 (in-place 변환 고려)
|
||||||
if (node.data.transformations && Array.isArray(node.data.transformations)) {
|
if ((node.data as any).transformations && Array.isArray((node.data as any).transformations)) {
|
||||||
const inPlaceFields = new Set<string>();
|
const inPlaceFields = new Set<string>();
|
||||||
|
|
||||||
node.data.transformations.forEach((transform: any) => {
|
(node.data as any).transformations.forEach((transform: any) => {
|
||||||
const targetField = transform.targetField || transform.sourceField;
|
const targetField = transform.targetField || transform.sourceField;
|
||||||
const isInPlace = !transform.targetField || transform.targetField === transform.sourceField;
|
const isInPlace = !transform.targetField || transform.targetField === transform.sourceField;
|
||||||
|
|
||||||
|
|
@ -194,16 +199,33 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
fields.push(...upperFields);
|
fields.push(...upperFields);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// REST API 소스 노드인 경우
|
||||||
|
else if (node.type === "restAPISource") {
|
||||||
|
foundRestAPI = true;
|
||||||
|
const responseFields = (node.data as any).responseFields;
|
||||||
|
if (responseFields && Array.isArray(responseFields)) {
|
||||||
|
responseFields.forEach((field: any) => {
|
||||||
|
const fieldName = field.name || field.fieldName;
|
||||||
|
const fieldLabel = field.label || field.displayName;
|
||||||
|
if (fieldName) {
|
||||||
|
fields.push({
|
||||||
|
name: fieldName,
|
||||||
|
label: fieldLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// 일반 소스 노드인 경우
|
// 일반 소스 노드인 경우
|
||||||
else if (node.type === "tableSource" && node.data.fields) {
|
else if (node.type === "tableSource" && (node.data as any).fields) {
|
||||||
node.data.fields.forEach((field: any) => {
|
(node.data as any).fields.forEach((field: any) => {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
label: field.label || field.displayName,
|
label: field.label || field.displayName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (node.type === "externalDBSource" && node.data.fields) {
|
} else if (node.type === "externalDBSource" && (node.data as any).fields) {
|
||||||
node.data.fields.forEach((field: any) => {
|
(node.data as any).fields.forEach((field: any) => {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
label: field.label || field.displayName,
|
label: field.label || field.displayName,
|
||||||
|
|
@ -212,15 +234,16 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return fields;
|
return { fields, hasRestAPI: foundRestAPI };
|
||||||
};
|
};
|
||||||
|
|
||||||
const allFields = getAllSourceFields(nodeId);
|
const result = getAllSourceFields(nodeId);
|
||||||
|
|
||||||
// 중복 제거
|
// 중복 제거
|
||||||
const uniqueFields = Array.from(new Map(allFields.map((field) => [field.name, field])).values());
|
const uniqueFields = Array.from(new Map(result.fields.map((field) => [field.name, field])).values());
|
||||||
|
|
||||||
setSourceFields(uniqueFields);
|
setSourceFields(uniqueFields);
|
||||||
|
setHasRestAPISource(result.hasRestAPI);
|
||||||
}, [nodeId, nodes, edges]);
|
}, [nodeId, nodes, edges]);
|
||||||
|
|
||||||
const loadTables = async () => {
|
const loadTables = async () => {
|
||||||
|
|
@ -1130,9 +1153,19 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* 소스 필드 드롭다운 */}
|
{/* 소스 필드 - REST API인 경우 입력, 아니면 드롭다운 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs text-gray-600">소스 필드</Label>
|
<Label className="text-xs text-gray-600">
|
||||||
|
소스 필드{hasRestAPISource && " (REST API - 직접 입력)"}
|
||||||
|
</Label>
|
||||||
|
{hasRestAPISource ? (
|
||||||
|
<Input
|
||||||
|
value={mapping.sourceField || ""}
|
||||||
|
onChange={(e) => handleMappingChange(index, "sourceField", e.target.value || null)}
|
||||||
|
placeholder="API 응답 JSON의 필드명을 입력하세요"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Select
|
<Select
|
||||||
value={mapping.sourceField || ""}
|
value={mapping.sourceField || ""}
|
||||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||||
|
|
@ -1151,7 +1184,9 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="font-medium">{field.label || field.name}</span>
|
<span className="font-medium">{field.label || field.name}</span>
|
||||||
{field.label && field.label !== field.name && (
|
{field.label && field.label !== field.name && (
|
||||||
<span className="text-muted-foreground font-mono text-xs">{field.name}</span>
|
<span className="text-muted-foreground font-mono text-xs">
|
||||||
|
{field.name}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -1159,6 +1194,7 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-1">
|
<div className="flex items-center justify-center py-1">
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
|
|
||||||
// 소스 필드 목록 (연결된 입력 노드에서 가져오기)
|
// 소스 필드 목록 (연결된 입력 노드에서 가져오기)
|
||||||
const [sourceFields, setSourceFields] = useState<Array<{ name: string; label?: string }>>([]);
|
const [sourceFields, setSourceFields] = useState<Array<{ name: string; label?: string }>>([]);
|
||||||
|
// REST API 소스 노드 연결 여부
|
||||||
|
const [hasRestAPISource, setHasRestAPISource] = useState(false);
|
||||||
|
|
||||||
// 데이터 변경 시 로컬 상태 업데이트
|
// 데이터 변경 시 로컬 상태 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -137,9 +139,9 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
const getAllSourceFields = (
|
const getAllSourceFields = (
|
||||||
targetNodeId: string,
|
targetNodeId: string,
|
||||||
visitedNodes = new Set<string>(),
|
visitedNodes = new Set<string>(),
|
||||||
): Array<{ name: string; label?: string }> => {
|
): { fields: Array<{ name: string; label?: string }>; hasRestAPI: boolean } => {
|
||||||
if (visitedNodes.has(targetNodeId)) {
|
if (visitedNodes.has(targetNodeId)) {
|
||||||
return [];
|
return { fields: [], hasRestAPI: false };
|
||||||
}
|
}
|
||||||
visitedNodes.add(targetNodeId);
|
visitedNodes.add(targetNodeId);
|
||||||
|
|
||||||
|
|
@ -148,18 +150,21 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
||||||
|
|
||||||
const fields: Array<{ name: string; label?: string }> = [];
|
const fields: Array<{ name: string; label?: string }> = [];
|
||||||
|
let foundRestAPI = false;
|
||||||
|
|
||||||
sourceNodes.forEach((node) => {
|
sourceNodes.forEach((node) => {
|
||||||
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
||||||
if (node.type === "dataTransform") {
|
if (node.type === "dataTransform") {
|
||||||
// 상위 노드의 원본 필드 먼저 수집
|
// 상위 노드의 원본 필드 먼저 수집
|
||||||
const upperFields = getAllSourceFields(node.id, visitedNodes);
|
const upperResult = getAllSourceFields(node.id, visitedNodes);
|
||||||
|
const upperFields = upperResult.fields;
|
||||||
|
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||||
|
|
||||||
// 변환된 필드 추가 (in-place 변환 고려)
|
// 변환된 필드 추가 (in-place 변환 고려)
|
||||||
if (node.data.transformations && Array.isArray(node.data.transformations)) {
|
if ((node.data as any).transformations && Array.isArray((node.data as any).transformations)) {
|
||||||
const inPlaceFields = new Set<string>();
|
const inPlaceFields = new Set<string>();
|
||||||
|
|
||||||
node.data.transformations.forEach((transform: any) => {
|
(node.data as any).transformations.forEach((transform: any) => {
|
||||||
const targetField = transform.targetField || transform.sourceField;
|
const targetField = transform.targetField || transform.sourceField;
|
||||||
const isInPlace = !transform.targetField || transform.targetField === transform.sourceField;
|
const isInPlace = !transform.targetField || transform.targetField === transform.sourceField;
|
||||||
|
|
||||||
|
|
@ -181,16 +186,33 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
fields.push(...upperFields);
|
fields.push(...upperFields);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// REST API 소스 노드인 경우
|
||||||
|
else if (node.type === "restAPISource") {
|
||||||
|
foundRestAPI = true;
|
||||||
|
const responseFields = (node.data as any).responseFields;
|
||||||
|
if (responseFields && Array.isArray(responseFields)) {
|
||||||
|
responseFields.forEach((field: any) => {
|
||||||
|
const fieldName = field.name || field.fieldName;
|
||||||
|
const fieldLabel = field.label || field.displayName;
|
||||||
|
if (fieldName) {
|
||||||
|
fields.push({
|
||||||
|
name: fieldName,
|
||||||
|
label: fieldLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// 일반 소스 노드인 경우
|
// 일반 소스 노드인 경우
|
||||||
else if (node.type === "tableSource" && node.data.fields) {
|
else if (node.type === "tableSource" && (node.data as any).fields) {
|
||||||
node.data.fields.forEach((field: any) => {
|
(node.data as any).fields.forEach((field: any) => {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
label: field.label || field.displayName,
|
label: field.label || field.displayName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (node.type === "externalDBSource" && node.data.fields) {
|
} else if (node.type === "externalDBSource" && (node.data as any).fields) {
|
||||||
node.data.fields.forEach((field: any) => {
|
(node.data as any).fields.forEach((field: any) => {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
label: field.label || field.displayName,
|
label: field.label || field.displayName,
|
||||||
|
|
@ -199,15 +221,16 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return fields;
|
return { fields, hasRestAPI: foundRestAPI };
|
||||||
};
|
};
|
||||||
|
|
||||||
const allFields = getAllSourceFields(nodeId);
|
const result = getAllSourceFields(nodeId);
|
||||||
|
|
||||||
// 중복 제거
|
// 중복 제거
|
||||||
const uniqueFields = Array.from(new Map(allFields.map((field) => [field.name, field])).values());
|
const uniqueFields = Array.from(new Map(result.fields.map((field) => [field.name, field])).values());
|
||||||
|
|
||||||
setSourceFields(uniqueFields);
|
setSourceFields(uniqueFields);
|
||||||
|
setHasRestAPISource(result.hasRestAPI);
|
||||||
}, [nodeId, nodes, edges]);
|
}, [nodeId, nodes, edges]);
|
||||||
|
|
||||||
// 🔥 외부 커넥션 로딩 함수
|
// 🔥 외부 커넥션 로딩 함수
|
||||||
|
|
@ -986,9 +1009,19 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* 소스 필드 드롭다운 */}
|
{/* 소스 필드 - REST API인 경우 입력, 아니면 드롭다운 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs text-gray-600">소스 필드</Label>
|
<Label className="text-xs text-gray-600">
|
||||||
|
소스 필드{hasRestAPISource && " (REST API - 직접 입력)"}
|
||||||
|
</Label>
|
||||||
|
{hasRestAPISource ? (
|
||||||
|
<Input
|
||||||
|
value={mapping.sourceField || ""}
|
||||||
|
onChange={(e) => handleMappingChange(index, "sourceField", e.target.value || null)}
|
||||||
|
placeholder="API 응답 JSON의 필드명을 입력하세요"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Select
|
<Select
|
||||||
value={mapping.sourceField || ""}
|
value={mapping.sourceField || ""}
|
||||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||||
|
|
@ -998,7 +1031,9 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{sourceFields.length === 0 ? (
|
{sourceFields.length === 0 ? (
|
||||||
<div className="p-2 text-center text-xs text-gray-400">연결된 소스 노드가 없습니다</div>
|
<div className="p-2 text-center text-xs text-gray-400">
|
||||||
|
연결된 소스 노드가 없습니다
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
sourceFields.map((field) => (
|
sourceFields.map((field) => (
|
||||||
<SelectItem key={field.name} value={field.name} className="text-xs">
|
<SelectItem key={field.name} value={field.name} className="text-xs">
|
||||||
|
|
@ -1013,6 +1048,7 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-1">
|
<div className="flex items-center justify-center py-1">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue