데이터 저장 구현 #29
|
|
@ -58,9 +58,30 @@ interface SimpleKeySettings {
|
|||
|
||||
// 데이터 저장 설정
|
||||
interface DataSaveSettings {
|
||||
sourceField: string;
|
||||
targetField: string;
|
||||
saveConditions: string;
|
||||
saveMode: "simple" | "conditional" | "split"; // 저장 방식
|
||||
actions: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
actionType: "insert" | "update" | "delete" | "upsert";
|
||||
conditions?: Array<{
|
||||
field: string;
|
||||
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
||||
value: string;
|
||||
}>;
|
||||
fieldMappings: Array<{
|
||||
sourceTable?: string;
|
||||
sourceField: string;
|
||||
targetTable?: string;
|
||||
targetField: string;
|
||||
defaultValue?: string;
|
||||
transformFunction?: string;
|
||||
}>;
|
||||
splitConfig?: {
|
||||
sourceField: string; // 분할할 소스 필드
|
||||
delimiter: string; // 구분자 (예: ",")
|
||||
targetField: string; // 분할된 값이 들어갈 필드
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
// 외부 호출 설정
|
||||
|
|
@ -103,9 +124,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
});
|
||||
|
||||
const [dataSaveSettings, setDataSaveSettings] = useState<DataSaveSettings>({
|
||||
sourceField: "",
|
||||
targetField: "",
|
||||
saveConditions: "",
|
||||
saveMode: "simple",
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const [externalCallSettings, setExternalCallSettings] = useState<ExternalCallSettings>({
|
||||
|
|
@ -124,6 +144,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
const [toTableColumns, setToTableColumns] = useState<ColumnInfo[]>([]);
|
||||
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
|
||||
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
|
||||
// 필요시 로드하는 테이블 컬럼 캐시
|
||||
const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
|
||||
|
||||
// 조건부 연결을 위한 새로운 상태들
|
||||
const [conditions, setConditions] = useState<ConditionNode[]>([]);
|
||||
|
|
@ -179,9 +201,15 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
|
||||
// 데이터 저장 기본값 설정
|
||||
setDataSaveSettings({
|
||||
sourceField: "",
|
||||
targetField: "",
|
||||
saveConditions: "데이터 저장 조건을 입력하세요",
|
||||
saveMode: "simple",
|
||||
actions: [
|
||||
{
|
||||
id: "action_1",
|
||||
name: `${fromDisplayName}에서 ${toDisplayName}로 데이터 저장`,
|
||||
actionType: "insert",
|
||||
fieldMappings: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 외부 호출 기본값 설정
|
||||
|
|
@ -253,6 +281,51 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
}));
|
||||
}, [selectedFromColumns, selectedToColumns]);
|
||||
|
||||
// 테이블 컬럼 로드 함수 (캐시 활용)
|
||||
const loadTableColumns = async (tableName: string): Promise<ColumnInfo[]> => {
|
||||
if (tableColumnsCache[tableName]) {
|
||||
return tableColumnsCache[tableName];
|
||||
}
|
||||
|
||||
try {
|
||||
const columns = await DataFlowAPI.getTableColumns(tableName);
|
||||
setTableColumnsCache((prev) => ({
|
||||
...prev,
|
||||
[tableName]: columns,
|
||||
}));
|
||||
return columns;
|
||||
} catch (error) {
|
||||
console.error(`${tableName} 컬럼 로드 실패:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블 선택 시 컬럼 로드
|
||||
useEffect(() => {
|
||||
const loadColumns = async () => {
|
||||
const tablesToLoad = new Set<string>();
|
||||
|
||||
// 필드 매핑에서 사용되는 모든 테이블 수집
|
||||
dataSaveSettings.actions.forEach((action) => {
|
||||
action.fieldMappings.forEach((mapping) => {
|
||||
if (mapping.sourceTable && !tableColumnsCache[mapping.sourceTable]) {
|
||||
tablesToLoad.add(mapping.sourceTable);
|
||||
}
|
||||
if (mapping.targetTable && !tableColumnsCache[mapping.targetTable]) {
|
||||
tablesToLoad.add(mapping.targetTable);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 필요한 테이블들의 컬럼만 로드
|
||||
for (const tableName of tablesToLoad) {
|
||||
await loadTableColumns(tableName);
|
||||
}
|
||||
};
|
||||
|
||||
loadColumns();
|
||||
}, [dataSaveSettings.actions, tableColumnsCache]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!config.relationshipName || !connection) {
|
||||
toast.error("필수 정보를 모두 입력해주세요.");
|
||||
|
|
@ -303,7 +376,24 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
},
|
||||
plan: {
|
||||
sourceTable: fromTableName,
|
||||
targetActions: [], // 나중에 액션 설정 UI에서 채울 예정
|
||||
targetActions:
|
||||
config.connectionType === "data-save"
|
||||
? dataSaveSettings.actions.map((action) => ({
|
||||
id: action.id,
|
||||
actionType: action.actionType,
|
||||
enabled: true,
|
||||
conditions: action.conditions,
|
||||
fieldMappings: action.fieldMappings.map((mapping) => ({
|
||||
sourceTable: mapping.sourceTable,
|
||||
sourceField: mapping.sourceField,
|
||||
targetTable: mapping.targetTable,
|
||||
targetField: mapping.targetField,
|
||||
defaultValue: mapping.defaultValue,
|
||||
transformFunction: mapping.transformFunction,
|
||||
})),
|
||||
splitConfig: action.splitConfig,
|
||||
}))
|
||||
: [],
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
|
@ -395,7 +485,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
<div className="rounded-lg border border-l-4 border-l-purple-500 bg-purple-50/30 p-4">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-purple-500" />
|
||||
<span className="text-sm font-medium">실행 조건 설정</span>
|
||||
<span className="text-sm font-medium">전체 실행 조건 (언제 이 연결이 동작할지)</span>
|
||||
</div>
|
||||
|
||||
{/* 실행 조건 설정 */}
|
||||
|
|
@ -528,50 +618,478 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
<Save className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm font-medium">데이터 저장 설정</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="sourceField" className="text-sm">
|
||||
소스 필드
|
||||
</Label>
|
||||
<Input
|
||||
id="sourceField"
|
||||
value={dataSaveSettings.sourceField}
|
||||
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, sourceField: e.target.value })}
|
||||
placeholder="소스 필드"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="targetField" className="text-sm">
|
||||
대상 필드
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="targetField"
|
||||
value={dataSaveSettings.targetField}
|
||||
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, targetField: e.target.value })}
|
||||
placeholder="대상 필드"
|
||||
className="text-sm"
|
||||
/>
|
||||
<Button size="sm" variant="outline">
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{/* 저장 방식 선택 */}
|
||||
<div>
|
||||
<Label htmlFor="saveConditions" className="text-sm">
|
||||
저장 조건
|
||||
</Label>
|
||||
<Textarea
|
||||
id="saveConditions"
|
||||
value={dataSaveSettings.saveConditions}
|
||||
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, saveConditions: e.target.value })}
|
||||
placeholder="데이터 저장 조건을 입력하세요"
|
||||
rows={2}
|
||||
className="text-sm"
|
||||
/>
|
||||
<Label className="text-sm font-medium">저장 방식</Label>
|
||||
<Select
|
||||
value={dataSaveSettings.saveMode}
|
||||
onValueChange={(value: "simple" | "conditional" | "split") =>
|
||||
setDataSaveSettings({ ...dataSaveSettings, saveMode: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="simple">단순 저장 - 하나의 테이블에 저장</SelectItem>
|
||||
<SelectItem value="conditional">조건부 저장 - 조건에 따라 다른 테이블에 저장</SelectItem>
|
||||
<SelectItem value="split">분할 저장 - 하나의 데이터를 여러 개로 분할</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 저장 방식별 설명 */}
|
||||
<div className="rounded bg-blue-50 p-3 text-xs">
|
||||
{dataSaveSettings.saveMode === "simple" && (
|
||||
<div>
|
||||
<strong>단순 저장:</strong> From 테이블의 데이터를 To 테이블(
|
||||
{selectedToTable || "선택된 테이블"})에 그대로 저장합니다.
|
||||
</div>
|
||||
)}
|
||||
{dataSaveSettings.saveMode === "conditional" && (
|
||||
<div>
|
||||
<strong>조건부 저장:</strong> 조건에 따라 다른 테이블에 저장합니다.
|
||||
<br />
|
||||
예: 평일 주문 → 당일배송, 주말 주문 → 월요일배송
|
||||
</div>
|
||||
)}
|
||||
{dataSaveSettings.saveMode === "split" && (
|
||||
<div>
|
||||
<strong>분할 저장:</strong> 하나의 필드를 분할하여 여러 레코드로 저장합니다.
|
||||
<br />
|
||||
예: "컴퓨터,마우스,키보드" → 3개의 별도 레코드로 저장
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 액션 목록 */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">저장 액션</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const newAction = {
|
||||
id: `action_${dataSaveSettings.actions.length + 1}`,
|
||||
name: `액션 ${dataSaveSettings.actions.length + 1}`,
|
||||
actionType: "insert" as const,
|
||||
fieldMappings: [],
|
||||
...(dataSaveSettings.saveMode === "conditional" ? { conditions: [] } : {}),
|
||||
...(dataSaveSettings.saveMode === "split"
|
||||
? {
|
||||
splitConfig: { sourceField: "", delimiter: ",", targetField: "" },
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
setDataSaveSettings({
|
||||
...dataSaveSettings,
|
||||
actions: [...dataSaveSettings.actions, newAction],
|
||||
});
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
액션 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{dataSaveSettings.actions.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-3 text-center text-xs text-gray-500">
|
||||
저장 액션을 추가하여 데이터를 어떻게 저장할지 설정하세요.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{dataSaveSettings.actions.map((action, actionIndex) => (
|
||||
<div key={action.id} className="rounded border bg-white p-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<Input
|
||||
value={action.name}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].name = e.target.value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-7 flex-1 text-xs font-medium"
|
||||
placeholder="액션 이름"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
const newActions = dataSaveSettings.actions.filter((_, i) => i !== actionIndex);
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{/* 액션 타입 */}
|
||||
<div>
|
||||
<Label className="text-xs">액션 타입</Label>
|
||||
<Select
|
||||
value={action.actionType}
|
||||
onValueChange={(value: "insert" | "update" | "delete" | "upsert") => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].actionType = value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="insert">INSERT</SelectItem>
|
||||
<SelectItem value="update">UPDATE</SelectItem>
|
||||
<SelectItem value="delete">DELETE</SelectItem>
|
||||
<SelectItem value="upsert">UPSERT</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 액션별 개별 실행 조건 (조건부 저장일 때만) */}
|
||||
{dataSaveSettings.saveMode !== "simple" && (
|
||||
<div className="mt-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">이 액션의 실행 조건 (선택사항)</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
if (!newActions[actionIndex].conditions) {
|
||||
newActions[actionIndex].conditions = [];
|
||||
}
|
||||
newActions[actionIndex].conditions = [
|
||||
...(newActions[actionIndex].conditions || []),
|
||||
{ field: "", operator: "=", value: "" },
|
||||
];
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-2 w-2" />
|
||||
조건 추가
|
||||
</Button>
|
||||
</div>
|
||||
{action.conditions && action.conditions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{action.conditions.map((condition, condIndex) => (
|
||||
<div key={condIndex} className="grid grid-cols-4 items-center gap-2">
|
||||
<Select
|
||||
value={condition.field}
|
||||
onValueChange={(value) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].field = value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-xs">
|
||||
<SelectValue placeholder="필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fromTableColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={condition.operator}
|
||||
onValueChange={(value: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE") => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].operator = value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="=">=</SelectItem>
|
||||
<SelectItem value="!=">!=</SelectItem>
|
||||
<SelectItem value=">">></SelectItem>
|
||||
<SelectItem value="<"><</SelectItem>
|
||||
<SelectItem value=">=">>=</SelectItem>
|
||||
<SelectItem value="<="><=</SelectItem>
|
||||
<SelectItem value="LIKE">LIKE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={condition.value}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
placeholder="값"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
|
||||
(_, i) => i !== condIndex,
|
||||
);
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<Trash2 className="h-2 w-2" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 분할 저장일 때 분할 설정 */}
|
||||
{dataSaveSettings.saveMode === "split" && action.splitConfig && (
|
||||
<div className="mt-3">
|
||||
<Label className="text-xs font-medium">데이터 분할 설정</Label>
|
||||
<div className="mt-1 grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500">분할할 필드</Label>
|
||||
<Select
|
||||
value={action.splitConfig.sourceField}
|
||||
onValueChange={(value) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].splitConfig!.sourceField = value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fromTableColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500">구분자</Label>
|
||||
<Input
|
||||
value={action.splitConfig.delimiter}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].splitConfig!.delimiter = e.target.value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
placeholder=","
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500">저장할 필드</Label>
|
||||
<Input
|
||||
value={action.splitConfig.targetField}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].splitConfig!.targetField = e.target.value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
placeholder="product_name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필드 매핑 */}
|
||||
<div className="mt-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">필드 매핑</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].fieldMappings.push({
|
||||
sourceTable: "",
|
||||
sourceField: "",
|
||||
targetTable: "",
|
||||
targetField: "",
|
||||
defaultValue: "",
|
||||
transformFunction: "",
|
||||
});
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-2 w-2" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{action.fieldMappings.map((mapping, mappingIndex) => (
|
||||
<div key={mappingIndex} className="rounded border bg-gray-50 p-4">
|
||||
{/* 필드 매핑 영역 */}
|
||||
<div className="mb-3">
|
||||
<div className="grid grid-cols-5 items-end gap-3">
|
||||
{/* 소스 테이블 */}
|
||||
<div>
|
||||
<Label className="mb-1 block text-xs text-gray-600">소스 테이블</Label>
|
||||
<Select
|
||||
value={mapping.sourceTable || ""}
|
||||
onValueChange={(value) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].fieldMappings[mappingIndex].sourceTable = value;
|
||||
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = ""; // 컬럼 초기화
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableTables.map((table) => (
|
||||
<SelectItem key={table.tableName} value={table.tableName}>
|
||||
{table.tableName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 소스 컬럼 */}
|
||||
<div>
|
||||
<Label className="mb-1 block text-xs text-gray-600">소스 컬럼</Label>
|
||||
<Select
|
||||
value={mapping.sourceField}
|
||||
onValueChange={(value) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
disabled={!mapping.sourceTable}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mapping.sourceTable &&
|
||||
tableColumnsCache[mapping.sourceTable]?.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
<div className="flex items-end justify-center pb-2">
|
||||
<div className="text-center text-lg font-bold text-gray-400">→</div>
|
||||
</div>
|
||||
|
||||
{/* 타겟 테이블 */}
|
||||
<div>
|
||||
<Label className="mb-1 block text-xs text-gray-600">타겟 테이블</Label>
|
||||
<Select
|
||||
value={mapping.targetTable || ""}
|
||||
onValueChange={(value) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].fieldMappings[mappingIndex].targetTable = value;
|
||||
newActions[actionIndex].fieldMappings[mappingIndex].targetField = ""; // 컬럼 초기화
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableTables.map((table) => (
|
||||
<SelectItem key={table.tableName} value={table.tableName}>
|
||||
{table.tableName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 타겟 컬럼 */}
|
||||
<div>
|
||||
<Label className="mb-1 block text-xs text-gray-600">타겟 컬럼</Label>
|
||||
<Select
|
||||
value={mapping.targetField}
|
||||
onValueChange={(value) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].fieldMappings[mappingIndex].targetField = value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
disabled={!mapping.targetTable}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mapping.targetTable &&
|
||||
tableColumnsCache[mapping.targetTable]?.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기본값 및 삭제 버튼 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<Label className="mb-1 block text-xs text-gray-600">기본값 (선택사항)</Label>
|
||||
<Input
|
||||
value={mapping.defaultValue || ""}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].fieldMappings[mappingIndex].defaultValue =
|
||||
e.target.value;
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-8 text-xs"
|
||||
placeholder="고정값 또는 기본값 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].fieldMappings = newActions[
|
||||
actionIndex
|
||||
].fieldMappings.filter((_, i) => i !== mappingIndex);
|
||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||
}}
|
||||
className="h-8 w-8 p-0 text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue