제어관리 외부커넥션 설정기능
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus, Trash2, Settings } from "lucide-react";
|
||||
|
||||
// 타입 import
|
||||
import { ColumnInfo } from "@/lib/types/multiConnection";
|
||||
import { getCodesForColumn, CodeItem } from "@/lib/api/codeManagement";
|
||||
|
||||
interface ActionCondition {
|
||||
id: string;
|
||||
field: string;
|
||||
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "IS NULL" | "IS NOT NULL";
|
||||
value: string;
|
||||
valueType?: "static" | "field" | "calculated"; // 값 타입 (고정값, 필드값, 계산값)
|
||||
logicalOperator?: "AND" | "OR";
|
||||
}
|
||||
|
||||
interface FieldValueMapping {
|
||||
id: string;
|
||||
targetField: string;
|
||||
valueType: "static" | "source_field" | "code" | "calculated";
|
||||
value: string;
|
||||
sourceField?: string;
|
||||
codeCategory?: string;
|
||||
}
|
||||
|
||||
interface ActionConditionBuilderProps {
|
||||
actionType: "insert" | "update" | "delete" | "upsert";
|
||||
fromColumns: ColumnInfo[];
|
||||
toColumns: ColumnInfo[];
|
||||
conditions: ActionCondition[];
|
||||
fieldMappings: FieldValueMapping[];
|
||||
onConditionsChange: (conditions: ActionCondition[]) => void;
|
||||
onFieldMappingsChange: (mappings: FieldValueMapping[]) => void;
|
||||
showFieldMappings?: boolean; // 필드 매핑 섹션 표시 여부
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 액션 조건 빌더
|
||||
* - 실행 조건 설정 (WHERE 절)
|
||||
* - 필드 값 매핑 설정 (SET 절)
|
||||
* - 코드 타입 필드 지원
|
||||
*/
|
||||
const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
||||
actionType,
|
||||
fromColumns,
|
||||
toColumns,
|
||||
conditions,
|
||||
fieldMappings,
|
||||
onConditionsChange,
|
||||
onFieldMappingsChange,
|
||||
showFieldMappings = true,
|
||||
}) => {
|
||||
const [availableCodes, setAvailableCodes] = useState<Record<string, CodeItem[]>>({});
|
||||
|
||||
const operators = [
|
||||
{ value: "=", label: "같음 (=)" },
|
||||
{ value: "!=", label: "다름 (!=)" },
|
||||
{ value: ">", label: "큼 (>)" },
|
||||
{ value: "<", label: "작음 (<)" },
|
||||
{ value: ">=", label: "크거나 같음 (>=)" },
|
||||
{ value: "<=", label: "작거나 같음 (<=)" },
|
||||
{ value: "LIKE", label: "포함 (LIKE)" },
|
||||
{ value: "IN", label: "목록 중 하나 (IN)" },
|
||||
{ value: "IS NULL", label: "빈 값 (IS NULL)" },
|
||||
{ value: "IS NOT NULL", label: "값 있음 (IS NOT NULL)" },
|
||||
];
|
||||
|
||||
// 코드 정보 로드
|
||||
useEffect(() => {
|
||||
const loadCodes = async () => {
|
||||
const codeFields = [...fromColumns, ...toColumns].filter(
|
||||
(col) => col.webType === "code" || col.dataType?.toLowerCase().includes("code"),
|
||||
);
|
||||
|
||||
for (const field of codeFields) {
|
||||
try {
|
||||
const codes = await getCodesForColumn(field.columnName, field.webType, field.codeCategory);
|
||||
|
||||
if (codes.length > 0) {
|
||||
setAvailableCodes((prev) => ({
|
||||
...prev,
|
||||
[field.columnName]: codes,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`코드 로드 실패: ${field.columnName}`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fromColumns.length > 0 || toColumns.length > 0) {
|
||||
loadCodes();
|
||||
}
|
||||
}, [fromColumns, toColumns]);
|
||||
|
||||
// 조건 추가
|
||||
const addCondition = () => {
|
||||
const newCondition: ActionCondition = {
|
||||
id: Date.now().toString(),
|
||||
field: "",
|
||||
operator: "=",
|
||||
value: "",
|
||||
...(conditions.length > 0 && { logicalOperator: "AND" }),
|
||||
};
|
||||
|
||||
onConditionsChange([...conditions, newCondition]);
|
||||
};
|
||||
|
||||
// 조건 업데이트
|
||||
const updateCondition = (index: number, updates: Partial<ActionCondition>) => {
|
||||
const updatedConditions = conditions.map((condition, i) =>
|
||||
i === index ? { ...condition, ...updates } : condition,
|
||||
);
|
||||
onConditionsChange(updatedConditions);
|
||||
};
|
||||
|
||||
// 조건 삭제
|
||||
const deleteCondition = (index: number) => {
|
||||
const updatedConditions = conditions.filter((_, i) => i !== index);
|
||||
onConditionsChange(updatedConditions);
|
||||
};
|
||||
|
||||
// 필드 매핑 추가
|
||||
const addFieldMapping = () => {
|
||||
const newMapping: FieldValueMapping = {
|
||||
id: Date.now().toString(),
|
||||
targetField: "",
|
||||
valueType: "static",
|
||||
value: "",
|
||||
};
|
||||
|
||||
onFieldMappingsChange([...fieldMappings, newMapping]);
|
||||
};
|
||||
|
||||
// 필드 매핑 업데이트
|
||||
const updateFieldMapping = (index: number, updates: Partial<FieldValueMapping>) => {
|
||||
const updatedMappings = fieldMappings.map((mapping, i) => (i === index ? { ...mapping, ...updates } : mapping));
|
||||
onFieldMappingsChange(updatedMappings);
|
||||
};
|
||||
|
||||
// 필드 매핑 삭제
|
||||
const deleteFieldMapping = (index: number) => {
|
||||
const updatedMappings = fieldMappings.filter((_, i) => i !== index);
|
||||
onFieldMappingsChange(updatedMappings);
|
||||
};
|
||||
|
||||
// 필드의 값 입력 컴포넌트 렌더링
|
||||
const renderValueInput = (mapping: FieldValueMapping, index: number, targetColumn?: ColumnInfo) => {
|
||||
if (mapping.valueType === "code" && targetColumn?.webType === "code") {
|
||||
const codes = availableCodes[targetColumn.columnName] || [];
|
||||
|
||||
return (
|
||||
<Select value={mapping.value} onValueChange={(value) => updateFieldMapping(index, { value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="코드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codes.map((code) => (
|
||||
<SelectItem key={code.code} value={code.code}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{code.code}
|
||||
</Badge>
|
||||
<span>{code.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
if (mapping.valueType === "source_field") {
|
||||
return (
|
||||
<Select
|
||||
value={mapping.sourceField || ""}
|
||||
onValueChange={(value) => updateFieldMapping(index, { sourceField: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="소스 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* FROM 테이블 필드들 */}
|
||||
{fromColumns.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">FROM 테이블</div>
|
||||
{fromColumns.map((column) => (
|
||||
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-600">📤</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.webType || column.dataType}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TO 테이블 필드들 */}
|
||||
{toColumns.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO 테이블</div>
|
||||
{toColumns.map((column) => (
|
||||
<SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-600">📥</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.webType || column.dataType}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
placeholder="값 입력"
|
||||
value={mapping.value}
|
||||
onChange={(e) => updateFieldMapping(index, { value: e.target.value })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 실행 조건 설정 */}
|
||||
{actionType !== "insert" && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center justify-between text-base">
|
||||
<span>실행 조건 (WHERE)</span>
|
||||
<Button variant="outline" size="sm" onClick={addCondition}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
조건 추가
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{conditions.length === 0 ? (
|
||||
<div className="rounded-lg border-2 border-dashed p-6 text-center">
|
||||
<Settings className="text-muted-foreground mx-auto mb-2 h-6 w-6" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{actionType.toUpperCase()} 액션의 실행 조건을 설정하세요
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
conditions.map((condition, index) => (
|
||||
<div key={condition.id} className="flex items-center gap-3 rounded-lg border p-3">
|
||||
{/* 논리 연산자 */}
|
||||
{index > 0 && (
|
||||
<Select
|
||||
value={condition.logicalOperator || "AND"}
|
||||
onValueChange={(value) => updateCondition(index, { logicalOperator: value as "AND" | "OR" })}
|
||||
>
|
||||
<SelectTrigger className="w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AND">AND</SelectItem>
|
||||
<SelectItem value="OR">OR</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{/* 필드 선택 */}
|
||||
<Select value={condition.field} onValueChange={(value) => updateCondition(index, { field: value })}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* FROM 테이블 컬럼들 */}
|
||||
{fromColumns.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">FROM 테이블</div>
|
||||
{fromColumns.map((column) => (
|
||||
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-600">📤</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TO 테이블 컬럼들 */}
|
||||
{toColumns.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO 테이블</div>
|
||||
{toColumns.map((column) => (
|
||||
<SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-600">📥</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 연산자 선택 */}
|
||||
<Select
|
||||
value={condition.operator}
|
||||
onValueChange={(value) => updateCondition(index, { operator: value as any })}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{operators.map((op) => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{op.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 값 입력 */}
|
||||
{!["IS NULL", "IS NOT NULL"].includes(condition.operator) &&
|
||||
(() => {
|
||||
// FROM/TO 테이블 컬럼 구분
|
||||
let fieldColumn;
|
||||
let actualFieldName;
|
||||
if (condition.field?.startsWith("from.")) {
|
||||
actualFieldName = condition.field.replace("from.", "");
|
||||
fieldColumn = fromColumns.find((col) => col.columnName === actualFieldName);
|
||||
} else if (condition.field?.startsWith("to.")) {
|
||||
actualFieldName = condition.field.replace("to.", "");
|
||||
fieldColumn = toColumns.find((col) => col.columnName === actualFieldName);
|
||||
} else {
|
||||
// 기존 호환성을 위해 TO 테이블에서 먼저 찾기
|
||||
actualFieldName = condition.field;
|
||||
fieldColumn =
|
||||
toColumns.find((col) => col.columnName === condition.field) ||
|
||||
fromColumns.find((col) => col.columnName === condition.field);
|
||||
}
|
||||
|
||||
const fieldCodes = availableCodes[actualFieldName];
|
||||
|
||||
// 코드 타입 필드면 코드 선택
|
||||
if (fieldColumn?.webType === "code" && fieldCodes?.length > 0) {
|
||||
return (
|
||||
<Select value={condition.value} onValueChange={(value) => updateCondition(index, { value })}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="코드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldCodes.map((code) => (
|
||||
<SelectItem key={code.code} value={code.code}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{code.code}
|
||||
</Badge>
|
||||
<span>{code.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// 값 타입 선택 (고정값, 다른 필드 값, 계산식 등)
|
||||
return (
|
||||
<div className="flex flex-1 gap-2">
|
||||
{/* 값 타입 선택 */}
|
||||
<Select
|
||||
value={condition.valueType || "static"}
|
||||
onValueChange={(valueType) => updateCondition(index, { valueType, value: "" })}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="static">고정값</SelectItem>
|
||||
<SelectItem value="field">필드값</SelectItem>
|
||||
<SelectItem value="calculated">계산값</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 값 입력 */}
|
||||
{condition.valueType === "field" ? (
|
||||
<Select
|
||||
value={condition.value}
|
||||
onValueChange={(value) => updateCondition(index, { value })}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* FROM 테이블 필드들 */}
|
||||
{fromColumns.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">
|
||||
FROM 테이블
|
||||
</div>
|
||||
{fromColumns.map((column) => (
|
||||
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-600">📤</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TO 테이블 필드들 */}
|
||||
{toColumns.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO 테이블</div>
|
||||
{toColumns.map((column) => (
|
||||
<SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-600">📥</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={condition.valueType === "calculated" ? "계산식 입력" : "값 입력"}
|
||||
value={condition.value}
|
||||
onChange={(e) => updateCondition(index, { value: e.target.value })}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button variant="ghost" size="sm" onClick={() => deleteCondition(index)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 필드 값 매핑 설정 */}
|
||||
{showFieldMappings && actionType !== "delete" && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center justify-between text-base">
|
||||
<span>필드 값 설정 (SET)</span>
|
||||
<Button variant="outline" size="sm" onClick={addFieldMapping}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
필드 추가
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{fieldMappings.length === 0 ? (
|
||||
<div className="rounded-lg border-2 border-dashed p-6 text-center">
|
||||
<Settings className="text-muted-foreground mx-auto mb-2 h-6 w-6" />
|
||||
<p className="text-muted-foreground text-sm">조건을 만족할 때 설정할 필드 값을 지정하세요</p>
|
||||
</div>
|
||||
) : (
|
||||
fieldMappings.map((mapping, index) => {
|
||||
const targetColumn = toColumns.find((col) => col.columnName === mapping.targetField);
|
||||
|
||||
return (
|
||||
<div key={mapping.id} className="flex items-center gap-3 rounded-lg border p-3">
|
||||
{/* 대상 필드 */}
|
||||
<Select
|
||||
value={mapping.targetField}
|
||||
onValueChange={(value) => updateFieldMapping(index, { targetField: value })}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="대상 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{toColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.webType || column.dataType}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 값 타입 */}
|
||||
<Select
|
||||
value={mapping.valueType}
|
||||
onValueChange={(value) => updateFieldMapping(index, { valueType: value as any })}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="static">고정값</SelectItem>
|
||||
<SelectItem value="source_field">소스필드</SelectItem>
|
||||
{targetColumn?.webType === "code" && <SelectItem value="code">코드선택</SelectItem>}
|
||||
<SelectItem value="calculated">계산식</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 값 입력 */}
|
||||
<div className="flex-1">{renderValueInput(mapping, index, targetColumn)}</div>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button variant="ghost" size="sm" onClick={() => deleteFieldMapping(index)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionConditionBuilder;
|
||||
Reference in New Issue
Block a user