ERP-node/frontend/components/dataflow/external-call/DataMappingSettings.tsx

383 lines
15 KiB
TypeScript

"use client";
import React, { useState, useCallback, 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 { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Plus, Trash2, Database, ArrowRight, Settings } from "lucide-react";
import {
DataMappingConfig,
DataDirection,
TableInfo,
FieldMapping,
InboundMapping,
OutboundMapping,
DATA_DIRECTION_OPTIONS,
INSERT_MODE_OPTIONS,
} from "@/types/external-call/DataMappingTypes";
import { FieldMappingEditor } from "./FieldMappingEditor";
interface DataMappingSettingsProps {
config: DataMappingConfig;
onConfigChange: (config: DataMappingConfig) => void;
httpMethod: string;
availableTables?: TableInfo[];
readonly?: boolean;
}
export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
config,
onConfigChange,
httpMethod,
availableTables = [],
readonly = false,
}) => {
const [localConfig, setLocalConfig] = useState<DataMappingConfig>(config);
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
setLocalConfig(config);
}, [config]);
// HTTP 메서드에 따른 권장 방향 결정
const getRecommendedDirection = useCallback((method: string): DataDirection => {
const upperMethod = method.toUpperCase();
if (upperMethod === "GET") return "inbound";
if (["POST", "PUT", "PATCH"].includes(upperMethod)) return "outbound";
return "none";
}, []);
// 방향 변경 핸들러
const handleDirectionChange = useCallback(
(direction: DataDirection) => {
const newConfig = {
...localConfig,
direction,
// 방향에 따라 불필요한 매핑 제거
inboundMapping:
direction === "inbound" || direction === "bidirectional"
? localConfig.inboundMapping || {
targetTable: "",
fieldMappings: [],
insertMode: "insert" as const,
}
: undefined,
outboundMapping:
direction === "outbound" || direction === "bidirectional"
? localConfig.outboundMapping || {
sourceTable: "",
fieldMappings: [],
}
: undefined,
};
setLocalConfig(newConfig);
onConfigChange(newConfig);
},
[localConfig, onConfigChange],
);
// Inbound 매핑 업데이트
const handleInboundMappingChange = useCallback(
(mapping: Partial<InboundMapping>) => {
const newConfig = {
...localConfig,
inboundMapping: {
...localConfig.inboundMapping!,
...mapping,
},
};
setLocalConfig(newConfig);
onConfigChange(newConfig);
},
[localConfig, onConfigChange],
);
// Outbound 매핑 업데이트
const handleOutboundMappingChange = useCallback(
(mapping: Partial<OutboundMapping>) => {
const newConfig = {
...localConfig,
outboundMapping: {
...localConfig.outboundMapping!,
...mapping,
},
};
setLocalConfig(newConfig);
onConfigChange(newConfig);
},
[localConfig, onConfigChange],
);
// 필드 매핑 업데이트 (Inbound)
const handleInboundFieldMappingsChange = useCallback(
(fieldMappings: FieldMapping[]) => {
handleInboundMappingChange({ fieldMappings });
},
[handleInboundMappingChange],
);
// 필드 매핑 업데이트 (Outbound)
const handleOutboundFieldMappingsChange = useCallback(
(fieldMappings: FieldMapping[]) => {
handleOutboundMappingChange({ fieldMappings });
},
[handleOutboundMappingChange],
);
// 검증 함수
const isConfigValid = useCallback(() => {
if (localConfig.direction === "none") return true;
if (
(localConfig.direction === "inbound" || localConfig.direction === "bidirectional") &&
localConfig.inboundMapping
) {
if (!localConfig.inboundMapping.targetTable) return false;
if (localConfig.inboundMapping.fieldMappings.length === 0) return false;
}
if (
(localConfig.direction === "outbound" || localConfig.direction === "bidirectional") &&
localConfig.outboundMapping
) {
if (!localConfig.outboundMapping.sourceTable) return false;
if (localConfig.outboundMapping.fieldMappings.length === 0) return false;
}
return true;
}, [localConfig]);
const recommendedDirection = getRecommendedDirection(httpMethod);
return (
<Card className="w-full">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
{!isConfigValid() && <Badge variant="destructive"> </Badge>}
{isConfigValid() && localConfig.direction !== "none" && <Badge variant="default"> </Badge>}
</CardTitle>
<p className="text-muted-foreground text-sm"> API와 .</p>
</CardHeader>
<CardContent className="space-y-4">
{/* 매핑 방향 선택 */}
<div className="space-y-2">
<Label> </Label>
<Select value={localConfig.direction} onValueChange={handleDirectionChange} disabled={readonly}>
<SelectTrigger>
<SelectValue placeholder="매핑 방향을 선택하세요" />
</SelectTrigger>
<SelectContent>
{DATA_DIRECTION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex items-center gap-2">
{option.label}
{option.value === recommendedDirection && (
<Badge variant="secondary" className="text-xs">
</Badge>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{localConfig.direction !== recommendedDirection && recommendedDirection !== "none" && (
<p className="text-xs text-amber-600">
💡 {httpMethod} "{DATA_DIRECTION_OPTIONS.find((o) => o.value === recommendedDirection)?.label}"
.
</p>
)}
</div>
{/* 매핑 설정 탭 */}
{localConfig.direction !== "none" && (
<Tabs
defaultValue={localConfig.direction === "bidirectional" ? "inbound" : localConfig.direction}
className="w-full"
>
<TabsList className="grid w-full grid-cols-2">
{(localConfig.direction === "inbound" || localConfig.direction === "bidirectional") && (
<TabsTrigger value="inbound">
<ArrowRight className="mr-1 h-4 w-4" />
</TabsTrigger>
)}
{(localConfig.direction === "outbound" || localConfig.direction === "bidirectional") && (
<TabsTrigger value="outbound">
<ArrowRight className="mr-1 h-4 w-4 rotate-180" />
</TabsTrigger>
)}
</TabsList>
{/* Inbound 매핑 설정 */}
{(localConfig.direction === "inbound" || localConfig.direction === "bidirectional") && (
<TabsContent value="inbound" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> </Label>
<Select
value={localConfig.inboundMapping?.targetTable || ""}
onValueChange={(value) => handleInboundMappingChange({ targetTable: value })}
disabled={readonly}
>
<SelectTrigger>
<SelectValue placeholder="저장할 테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table.name} value={table.name}>
{table.displayName || table.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> </Label>
<Select
value={localConfig.inboundMapping?.insertMode || "insert"}
onValueChange={(value) => handleInboundMappingChange({ insertMode: value as any })}
disabled={readonly}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{INSERT_MODE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 키 필드 설정 (upsert/update 모드일 때) */}
{localConfig.inboundMapping?.insertMode !== "insert" && (
<div className="space-y-2">
<Label> </Label>
<Input
value={localConfig.inboundMapping?.keyFields?.join(", ") || ""}
onChange={(e) =>
handleInboundMappingChange({
keyFields: e.target.value
.split(",")
.map((s) => s.trim())
.filter(Boolean),
})
}
placeholder="id, code"
disabled={readonly}
/>
<p className="text-muted-foreground text-xs">
/ .
</p>
</div>
)}
{/* 필드 매핑 에디터 */}
{localConfig.inboundMapping?.targetTable && (
<FieldMappingEditor
mappings={localConfig.inboundMapping.fieldMappings}
onMappingsChange={handleInboundFieldMappingsChange}
direction="inbound"
targetTable={availableTables.find((t) => t.name === localConfig.inboundMapping?.targetTable)}
readonly={readonly}
/>
)}
</TabsContent>
)}
{/* Outbound 매핑 설정 */}
{(localConfig.direction === "outbound" || localConfig.direction === "bidirectional") && (
<TabsContent value="outbound" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label> </Label>
<Select
value={localConfig.outboundMapping?.sourceTable || ""}
onValueChange={(value) => handleOutboundMappingChange({ sourceTable: value })}
disabled={readonly}
>
<SelectTrigger>
<SelectValue placeholder="데이터를 가져올 테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table.name} value={table.name}>
{table.displayName || table.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 소스 필터 조건 */}
<div className="space-y-2">
<Label> ()</Label>
<Textarea
value={localConfig.outboundMapping?.sourceFilter || ""}
onChange={(e) => handleOutboundMappingChange({ sourceFilter: e.target.value })}
placeholder="status = 'active' AND created_at >= '2024-01-01'"
disabled={readonly}
rows={2}
/>
<p className="text-muted-foreground text-xs">
WHERE . .
</p>
</div>
{/* 필드 매핑 에디터 */}
{localConfig.outboundMapping?.sourceTable && (
<FieldMappingEditor
mappings={localConfig.outboundMapping.fieldMappings}
onMappingsChange={handleOutboundFieldMappingsChange}
direction="outbound"
sourceTable={availableTables.find((t) => t.name === localConfig.outboundMapping?.sourceTable)}
readonly={readonly}
/>
)}
</TabsContent>
)}
</Tabs>
)}
{/* 설정 요약 */}
{localConfig.direction !== "none" && (
<div className="bg-muted mt-4 rounded-lg p-3">
<h4 className="mb-2 text-sm font-medium"> </h4>
<div className="text-muted-foreground space-y-1 text-xs">
<div>: {DATA_DIRECTION_OPTIONS.find((o) => o.value === localConfig.direction)?.label}</div>
{localConfig.inboundMapping && (
<div>
{localConfig.inboundMapping.targetTable}({localConfig.inboundMapping.fieldMappings.length}
)
</div>
)}
{localConfig.outboundMapping && (
<div>
{localConfig.outboundMapping.sourceTable} ({localConfig.outboundMapping.fieldMappings.length}
)
</div>
)}
</div>
</div>
)}
</CardContent>
</Card>
);
};