rest api 액션노드 기능변경
This commit is contained in:
@@ -118,6 +118,16 @@ function FlowEditorInner() {
|
||||
displayName: `새 ${type} 노드`,
|
||||
};
|
||||
|
||||
// REST API 소스 노드의 경우
|
||||
if (type === "restAPISource") {
|
||||
defaultData.method = "GET";
|
||||
defaultData.url = "";
|
||||
defaultData.headers = {};
|
||||
defaultData.timeout = 30000;
|
||||
defaultData.responseFields = []; // 빈 배열로 초기화
|
||||
defaultData.responseMapping = "";
|
||||
}
|
||||
|
||||
// 액션 노드의 경우 targetType 기본값 설정
|
||||
if (["insertAction", "updateAction", "deleteAction", "upsertAction"].includes(type)) {
|
||||
defaultData.targetType = "internal"; // 기본값: 내부 DB
|
||||
|
||||
@@ -78,35 +78,33 @@ export const ConditionNode = memo(({ data, selected }: NodeProps<ConditionNodeDa
|
||||
</div>
|
||||
|
||||
{/* 분기 출력 핸들 */}
|
||||
<div className="border-t">
|
||||
<div className="grid grid-cols-2">
|
||||
{/* TRUE 출력 */}
|
||||
<div className="relative border-r p-2">
|
||||
<div className="flex items-center justify-center gap-1 text-xs">
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
<span className="font-medium text-green-600">TRUE</span>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="true"
|
||||
className="!-right-1.5 !h-3 !w-3 !border-2 !border-green-500 !bg-white"
|
||||
/>
|
||||
<div className="relative border-t">
|
||||
{/* TRUE 출력 - 오른쪽 위 */}
|
||||
<div className="relative border-b p-2">
|
||||
<div className="flex items-center justify-end gap-1 pr-6 text-xs">
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
<span className="font-medium text-green-600">TRUE</span>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="true"
|
||||
className="!top-1/2 !-right-1.5 !h-3 !w-3 !-translate-y-1/2 !border-2 !border-green-500 !bg-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* FALSE 출력 */}
|
||||
<div className="relative p-2">
|
||||
<div className="flex items-center justify-center gap-1 text-xs">
|
||||
<X className="h-3 w-3 text-red-600" />
|
||||
<span className="font-medium text-red-600">FALSE</span>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="false"
|
||||
className="!-right-1.5 !h-3 !w-3 !border-2 !border-red-500 !bg-white"
|
||||
/>
|
||||
{/* FALSE 출력 - 오른쪽 아래 */}
|
||||
<div className="relative p-2">
|
||||
<div className="flex items-center justify-end gap-1 pr-6 text-xs">
|
||||
<X className="h-3 w-3 text-red-600" />
|
||||
<span className="font-medium text-red-600">FALSE</span>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="false"
|
||||
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>
|
||||
|
||||
@@ -64,6 +64,8 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
|
||||
// 소스 필드 목록 (연결된 입력 노드에서 가져오기)
|
||||
const [sourceFields, setSourceFields] = useState<Array<{ name: string; label?: string }>>([]);
|
||||
// REST API 소스 노드 연결 여부
|
||||
const [hasRestAPISource, setHasRestAPISource] = useState(false);
|
||||
|
||||
// 🔥 외부 DB 관련 상태
|
||||
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
||||
@@ -135,9 +137,9 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
const getAllSourceFields = (
|
||||
targetNodeId: string,
|
||||
visitedNodes = new Set<string>(),
|
||||
): Array<{ name: string; label?: string }> => {
|
||||
): { fields: Array<{ name: string; label?: string }>; hasRestAPI: boolean } => {
|
||||
if (visitedNodes.has(targetNodeId)) {
|
||||
return [];
|
||||
return { fields: [], hasRestAPI: false };
|
||||
}
|
||||
visitedNodes.add(targetNodeId);
|
||||
|
||||
@@ -146,6 +148,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
||||
|
||||
const fields: Array<{ name: string; label?: string }> = [];
|
||||
let foundRestAPI = false;
|
||||
|
||||
sourceNodes.forEach((node) => {
|
||||
console.log(`🔍 노드 ${node.id} 타입: ${node.type}`);
|
||||
@@ -153,18 +156,20 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
|
||||
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
||||
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}개 필드 가져옴`);
|
||||
|
||||
// 변환된 필드 추가 (in-place 변환 고려)
|
||||
if (node.data.transformations && Array.isArray(node.data.transformations)) {
|
||||
console.log(` 📊 ${node.data.transformations.length}개 변환 발견`);
|
||||
if ((node.data as any).transformations && Array.isArray((node.data as any).transformations)) {
|
||||
console.log(` 📊 ${(node.data as any).transformations.length}개 변환 발견`);
|
||||
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 isInPlace = !transform.targetField || transform.targetField === transform.sourceField;
|
||||
|
||||
@@ -196,9 +201,31 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
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 {
|
||||
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)) {
|
||||
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);
|
||||
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);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
console.log("✅ 최종 소스 필드 목록:", uniqueFields);
|
||||
console.log("✅ REST API 소스 연결:", result.hasRestAPI);
|
||||
}, [nodeId, nodes, edges]);
|
||||
|
||||
/**
|
||||
@@ -924,10 +953,10 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
<div>
|
||||
<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>
|
||||
<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}
|
||||
onChange={(e) => {
|
||||
setApiBodyTemplate(e.target.value);
|
||||
@@ -937,7 +966,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
rows={8}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
소스 데이터의 필드명을 {`{{필드명}}`} 형태로 참조할 수 있습니다.
|
||||
소스 데이터의 필드명을 {"{{필드명}}"} 형태로 참조할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1011,35 +1040,54 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* 소스 필드 드롭다운 */}
|
||||
{/* 소스 필드 입력/선택 */}
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">소스 필드</Label>
|
||||
<Select
|
||||
value={mapping.sourceField || ""}
|
||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||
<SelectValue placeholder="소스 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceFields.length === 0 ? (
|
||||
<div className="p-2 text-center text-xs text-gray-400">
|
||||
연결된 소스 노드가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
sourceFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name} className="text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">{field.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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
|
||||
value={mapping.sourceField || ""}
|
||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||
<SelectValue placeholder="소스 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceFields.length === 0 ? (
|
||||
<div className="p-2 text-center text-xs text-gray-400">
|
||||
연결된 소스 노드가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
sourceFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name} className="text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">
|
||||
{field.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{hasRestAPISource && (
|
||||
<p className="mt-1 text-xs text-gray-500">API 응답 JSON의 필드명을 입력하세요</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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 [timeout, setTimeout] = useState(data.timeout?.toString() || "30000");
|
||||
const [responseMapping, setResponseMapping] = useState(data.responseMapping || "");
|
||||
const [responseFields, setResponseFields] = useState(data.responseFields || []);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(data.displayName || "");
|
||||
@@ -48,6 +49,7 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||
setAuthToken(data.authentication?.token || "");
|
||||
setTimeout(data.timeout?.toString() || "30000");
|
||||
setResponseMapping(data.responseMapping || "");
|
||||
setResponseFields(data.responseFields || []);
|
||||
}, [data]);
|
||||
|
||||
const handleApply = () => {
|
||||
@@ -59,6 +61,10 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔧 REST API 노드 업데이트 중...");
|
||||
console.log("📦 responseFields:", responseFields);
|
||||
console.log("📊 responseFields 개수:", responseFields.length);
|
||||
|
||||
updateNode(nodeId, {
|
||||
displayName,
|
||||
url,
|
||||
@@ -71,7 +77,10 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||
},
|
||||
timeout: parseInt(timeout) || 30000,
|
||||
responseMapping,
|
||||
responseFields,
|
||||
});
|
||||
|
||||
console.log("✅ REST API 노드 업데이트 완료");
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
@@ -88,6 +97,32 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
|
||||
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 (
|
||||
<div className="space-y-4 p-4">
|
||||
<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"
|
||||
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>
|
||||
|
||||
<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 }>>([]);
|
||||
// REST API 소스 노드 연결 여부
|
||||
const [hasRestAPISource, setHasRestAPISource] = useState(false);
|
||||
|
||||
// 🔥 외부 DB 관련 상태
|
||||
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
||||
@@ -150,9 +152,9 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
const getAllSourceFields = (
|
||||
targetNodeId: string,
|
||||
visitedNodes = new Set<string>(),
|
||||
): Array<{ name: string; label?: string }> => {
|
||||
): { fields: Array<{ name: string; label?: string }>; hasRestAPI: boolean } => {
|
||||
if (visitedNodes.has(targetNodeId)) {
|
||||
return [];
|
||||
return { fields: [], hasRestAPI: false };
|
||||
}
|
||||
visitedNodes.add(targetNodeId);
|
||||
|
||||
@@ -161,18 +163,21 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
||||
|
||||
const fields: Array<{ name: string; label?: string }> = [];
|
||||
let foundRestAPI = false;
|
||||
|
||||
sourceNodes.forEach((node) => {
|
||||
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
||||
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 변환 고려)
|
||||
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>();
|
||||
|
||||
node.data.transformations.forEach((transform: any) => {
|
||||
(node.data as any).transformations.forEach((transform: any) => {
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
node.data.fields.forEach((field: any) => {
|
||||
else if (node.type === "tableSource" && (node.data as any).fields) {
|
||||
(node.data as any).fields.forEach((field: any) => {
|
||||
fields.push({
|
||||
name: field.name,
|
||||
label: field.label || field.displayName,
|
||||
});
|
||||
});
|
||||
} else if (node.type === "externalDBSource" && node.data.fields) {
|
||||
node.data.fields.forEach((field: any) => {
|
||||
} else if (node.type === "externalDBSource" && (node.data as any).fields) {
|
||||
(node.data as any).fields.forEach((field: any) => {
|
||||
fields.push({
|
||||
name: field.name,
|
||||
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);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
}, [nodeId, nodes, edges]);
|
||||
|
||||
const loadTables = async () => {
|
||||
@@ -1130,35 +1153,48 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* 소스 필드 드롭다운 */}
|
||||
{/* 소스 필드 - REST API인 경우 입력, 아니면 드롭다운 */}
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">소스 필드</Label>
|
||||
<Select
|
||||
value={mapping.sourceField || ""}
|
||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||
<SelectValue placeholder="소스 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceFields.length === 0 ? (
|
||||
<div className="p-2 text-center text-xs text-gray-400">
|
||||
연결된 소스 노드가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
sourceFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name} className="text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">{field.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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
|
||||
value={mapping.sourceField || ""}
|
||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||
<SelectValue placeholder="소스 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceFields.length === 0 ? (
|
||||
<div className="p-2 text-center text-xs text-gray-400">
|
||||
연결된 소스 노드가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
sourceFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name} className="text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">
|
||||
{field.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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 }>>([]);
|
||||
// REST API 소스 노드 연결 여부
|
||||
const [hasRestAPISource, setHasRestAPISource] = useState(false);
|
||||
|
||||
// 데이터 변경 시 로컬 상태 업데이트
|
||||
useEffect(() => {
|
||||
@@ -137,9 +139,9 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
const getAllSourceFields = (
|
||||
targetNodeId: string,
|
||||
visitedNodes = new Set<string>(),
|
||||
): Array<{ name: string; label?: string }> => {
|
||||
): { fields: Array<{ name: string; label?: string }>; hasRestAPI: boolean } => {
|
||||
if (visitedNodes.has(targetNodeId)) {
|
||||
return [];
|
||||
return { fields: [], hasRestAPI: false };
|
||||
}
|
||||
visitedNodes.add(targetNodeId);
|
||||
|
||||
@@ -148,18 +150,21 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
|
||||
|
||||
const fields: Array<{ name: string; label?: string }> = [];
|
||||
let foundRestAPI = false;
|
||||
|
||||
sourceNodes.forEach((node) => {
|
||||
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
|
||||
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 변환 고려)
|
||||
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>();
|
||||
|
||||
node.data.transformations.forEach((transform: any) => {
|
||||
(node.data as any).transformations.forEach((transform: any) => {
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
node.data.fields.forEach((field: any) => {
|
||||
else if (node.type === "tableSource" && (node.data as any).fields) {
|
||||
(node.data as any).fields.forEach((field: any) => {
|
||||
fields.push({
|
||||
name: field.name,
|
||||
label: field.label || field.displayName,
|
||||
});
|
||||
});
|
||||
} else if (node.type === "externalDBSource" && node.data.fields) {
|
||||
node.data.fields.forEach((field: any) => {
|
||||
} else if (node.type === "externalDBSource" && (node.data as any).fields) {
|
||||
(node.data as any).fields.forEach((field: any) => {
|
||||
fields.push({
|
||||
name: field.name,
|
||||
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);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
}, [nodeId, nodes, edges]);
|
||||
|
||||
// 🔥 외부 커넥션 로딩 함수
|
||||
@@ -986,33 +1009,46 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* 소스 필드 드롭다운 */}
|
||||
{/* 소스 필드 - REST API인 경우 입력, 아니면 드롭다운 */}
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">소스 필드</Label>
|
||||
<Select
|
||||
value={mapping.sourceField || ""}
|
||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||
<SelectValue placeholder="소스 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceFields.length === 0 ? (
|
||||
<div className="p-2 text-center text-xs text-gray-400">연결된 소스 노드가 없습니다</div>
|
||||
) : (
|
||||
sourceFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name} className="text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">{field.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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
|
||||
value={mapping.sourceField || ""}
|
||||
onValueChange={(value) => handleMappingChange(index, "sourceField", value || null)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||
<SelectValue placeholder="소스 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceFields.length === 0 ? (
|
||||
<div className="p-2 text-center text-xs text-gray-400">
|
||||
연결된 소스 노드가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
sourceFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name} className="text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium">{field.label || field.name}</span>
|
||||
{field.label && field.label !== field.name && (
|
||||
<span className="text-muted-foreground font-mono text-xs">{field.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center py-1">
|
||||
|
||||
Reference in New Issue
Block a user