ERP-node/frontend/components/flow/FlowConditionBuilder.tsx

256 lines
9.2 KiB
TypeScript

/**
* 플로우 조건 빌더
* 동적 조건 생성 UI
*/
import { useState, useEffect } from "react";
import { Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { FlowConditionGroup, FlowCondition, ConditionOperator } from "@/types/flow";
import { getTableColumns } from "@/lib/api/tableManagement";
interface FlowConditionBuilderProps {
flowId: number;
tableName?: string; // 조회할 테이블명
condition?: FlowConditionGroup;
onChange: (condition: FlowConditionGroup | undefined) => void;
}
const OPERATORS: { value: ConditionOperator; label: string }[] = [
{ value: "equals", label: "같음 (=)" },
{ value: "not_equals", label: "같지 않음 (!=)" },
{ value: "greater_than", label: "보다 큼 (>)" },
{ value: "less_than", label: "보다 작음 (<)" },
{ value: "greater_than_or_equal", label: "이상 (>=)" },
{ value: "less_than_or_equal", label: "이하 (<=)" },
{ value: "in", label: "포함 (IN)" },
{ value: "not_in", label: "제외 (NOT IN)" },
{ value: "like", label: "유사 (LIKE)" },
{ value: "not_like", label: "유사하지 않음 (NOT LIKE)" },
{ value: "is_null", label: "NULL" },
{ value: "is_not_null", label: "NOT NULL" },
];
export function FlowConditionBuilder({ flowId, tableName, condition, onChange }: FlowConditionBuilderProps) {
const [columns, setColumns] = useState<any[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
const [conditionType, setConditionType] = useState<"AND" | "OR">(condition?.type || "AND");
const [conditions, setConditions] = useState<FlowCondition[]>(condition?.conditions || []);
// 테이블 컬럼 로드
useEffect(() => {
if (!tableName) {
setColumns([]);
return;
}
const loadColumns = async () => {
try {
setLoadingColumns(true);
console.log("🔍 Loading columns for table:", tableName);
const response = await getTableColumns(tableName);
console.log("📦 Column API response:", response);
if (response.success && response.data?.columns) {
const columnArray = Array.isArray(response.data.columns) ? response.data.columns : [];
console.log("✅ Setting columns:", columnArray.length, "items");
setColumns(columnArray);
} else {
console.error("❌ Failed to load columns:", response.message);
setColumns([]);
}
} catch (error) {
console.error("❌ Exception loading columns:", error);
setColumns([]);
} finally {
setLoadingColumns(false);
}
};
loadColumns();
}, [tableName]);
// 조건 변경 시 부모에 전달
useEffect(() => {
if (conditions.length === 0) {
onChange(undefined);
} else {
onChange({
type: conditionType,
conditions,
});
}
}, [conditionType, conditions]);
// 조건 추가
const addCondition = () => {
setConditions([
...conditions,
{
column: "",
operator: "equals",
value: "",
},
]);
};
// 조건 수정
const updateCondition = (index: number, field: keyof FlowCondition, value: any) => {
const newConditions = [...conditions];
newConditions[index] = {
...newConditions[index],
[field]: value,
};
setConditions(newConditions);
};
// 조건 삭제
const removeCondition = (index: number) => {
setConditions(conditions.filter((_, i) => i !== index));
};
// value가 필요 없는 연산자 체크
const needsValue = (operator: ConditionOperator) => {
return operator !== "is_null" && operator !== "is_not_null";
};
return (
<div className="space-y-4">
{/* 조건 타입 선택 */}
<div>
<Label> </Label>
<Select value={conditionType} onValueChange={(value) => setConditionType(value as "AND" | "OR")}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND ( )</SelectItem>
<SelectItem value="OR">OR ( )</SelectItem>
</SelectContent>
</Select>
</div>
{/* 조건 목록 */}
<div className="space-y-3">
{conditions.length === 0 ? (
<div className="text-muted-foreground rounded border-2 border-dashed py-4 text-center text-sm">
<br />
</div>
) : (
conditions.map((cond, index) => (
<div key={index} className="space-y-2 rounded border bg-gray-50 p-3">
{/* 조건 번호 및 삭제 버튼 */}
<div className="flex items-center justify-between">
<Badge variant="outline"> {index + 1}</Badge>
<Button variant="ghost" size="sm" onClick={() => removeCondition(index)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
{/* 컬럼 선택 */}
<div>
<Label className="text-xs"></Label>
{loadingColumns ? (
<Input value="컬럼 로딩 중..." disabled className="h-8" />
) : !Array.isArray(columns) || columns.length === 0 ? (
<Input
value={cond.column}
onChange={(e) => updateCondition(index, "column", e.target.value)}
placeholder="테이블을 먼저 선택하세요"
className="h-8"
/>
) : (
<Select value={cond.column} onValueChange={(value) => updateCondition(index, "column", value)}>
<SelectTrigger className="h-8">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span className="font-medium">{col.displayName || col.columnName}</span>
<span className="text-xs text-gray-500">({col.dataType})</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
{/* 연산자 선택 */}
<div>
<Label className="text-xs"></Label>
<Select
value={cond.operator}
onValueChange={(value) => updateCondition(index, "operator", value as ConditionOperator)}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{OPERATORS.map((op) => (
<SelectItem key={op.value} value={op.value}>
{op.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 값 입력 */}
{needsValue(cond.operator) && (
<div>
<Label className="text-xs"></Label>
<Input
value={cond.value || ""}
onChange={(e) => updateCondition(index, "value", e.target.value)}
placeholder="값 입력"
className="h-8"
/>
{(cond.operator === "in" || cond.operator === "not_in") && (
<p className="text-muted-foreground mt-1 text-xs">(,) </p>
)}
</div>
)}
</div>
))
)}
</div>
{/* 조건 추가 버튼 */}
<Button variant="outline" size="sm" onClick={addCondition} className="w-full">
<Plus className="mr-2 h-4 w-4" />
</Button>
{/* 조건 요약 */}
{conditions.length > 0 && (
<div className="rounded bg-blue-50 p-3 text-sm">
<strong> :</strong>
<div className="mt-2 space-y-1">
{conditions.map((cond, index) => (
<div key={index} className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
{cond.column}
</Badge>
<span className="text-muted-foreground">
{OPERATORS.find((op) => op.value === cond.operator)?.label}
</span>
{needsValue(cond.operator) && <code className="rounded bg-white px-2 py-1 text-xs">{cond.value}</code>}
{index < conditions.length - 1 && <Badge>{conditionType}</Badge>}
</div>
))}
</div>
</div>
)}
</div>
);
}