rest api 액션노드 기능변경

This commit is contained in:
kjs
2025-10-13 12:00:41 +09:00
parent 68308efd22
commit 1274f58c3c
10 changed files with 617 additions and 174 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">