ERP-node/frontend/components/screen/panels/DataFlowPanel.tsx

461 lines
16 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import { Plus, ArrowRight, Trash2, Pencil, GitBranch, RefreshCw } from "lucide-react";
import {
getDataFlows,
createDataFlow,
updateDataFlow,
deleteDataFlow,
DataFlow,
} from "@/lib/api/screenGroup";
interface DataFlowPanelProps {
groupId?: number;
screenId?: number;
screens?: Array<{ screen_id: number; screen_name: string }>;
}
export default function DataFlowPanel({ groupId, screenId, screens = [] }: DataFlowPanelProps) {
// 상태 관리
const [dataFlows, setDataFlows] = useState<DataFlow[]>([]);
const [loading, setLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedFlow, setSelectedFlow] = useState<DataFlow | null>(null);
const [formData, setFormData] = useState({
source_screen_id: 0,
source_action: "",
target_screen_id: 0,
target_action: "",
data_mapping: "",
flow_type: "unidirectional",
flow_label: "",
condition_expression: "",
is_active: "Y",
});
// 데이터 로드
const loadDataFlows = useCallback(async () => {
setLoading(true);
try {
const response = await getDataFlows(groupId);
if (response.success && response.data) {
setDataFlows(response.data);
}
} catch (error) {
console.error("데이터 흐름 로드 실패:", error);
} finally {
setLoading(false);
}
}, [groupId]);
useEffect(() => {
loadDataFlows();
}, [loadDataFlows]);
// 모달 열기
const openModal = (flow?: DataFlow) => {
if (flow) {
setSelectedFlow(flow);
setFormData({
source_screen_id: flow.source_screen_id,
source_action: flow.source_action || "",
target_screen_id: flow.target_screen_id,
target_action: flow.target_action || "",
data_mapping: flow.data_mapping ? JSON.stringify(flow.data_mapping, null, 2) : "",
flow_type: flow.flow_type,
flow_label: flow.flow_label || "",
condition_expression: flow.condition_expression || "",
is_active: flow.is_active,
});
} else {
setSelectedFlow(null);
setFormData({
source_screen_id: screenId || 0,
source_action: "",
target_screen_id: 0,
target_action: "",
data_mapping: "",
flow_type: "unidirectional",
flow_label: "",
condition_expression: "",
is_active: "Y",
});
}
setIsModalOpen(true);
};
// 저장
const handleSave = async () => {
if (!formData.source_screen_id || !formData.target_screen_id) {
toast.error("소스 화면과 타겟 화면을 선택해주세요.");
return;
}
try {
let dataMappingJson = null;
if (formData.data_mapping) {
try {
dataMappingJson = JSON.parse(formData.data_mapping);
} catch {
toast.error("데이터 매핑 JSON 형식이 올바르지 않습니다.");
return;
}
}
const payload = {
group_id: groupId,
source_screen_id: formData.source_screen_id,
source_action: formData.source_action || null,
target_screen_id: formData.target_screen_id,
target_action: formData.target_action || null,
data_mapping: dataMappingJson,
flow_type: formData.flow_type,
flow_label: formData.flow_label || null,
condition_expression: formData.condition_expression || null,
is_active: formData.is_active,
};
let response;
if (selectedFlow) {
response = await updateDataFlow(selectedFlow.id, payload);
} else {
response = await createDataFlow(payload);
}
if (response.success) {
toast.success(selectedFlow ? "데이터 흐름이 수정되었습니다." : "데이터 흐름이 추가되었습니다.");
setIsModalOpen(false);
loadDataFlows();
} else {
toast.error(response.message || "저장에 실패했습니다.");
}
} catch (error) {
toast.error("저장 중 오류가 발생했습니다.");
}
};
// 삭제
const handleDelete = async (id: number) => {
if (!confirm("이 데이터 흐름을 삭제하시겠습니까?")) return;
try {
const response = await deleteDataFlow(id);
if (response.success) {
toast.success("데이터 흐름이 삭제되었습니다.");
loadDataFlows();
} else {
toast.error(response.message || "삭제에 실패했습니다.");
}
} catch (error) {
toast.error("삭제 중 오류가 발생했습니다.");
}
};
// 액션 옵션
const sourceActions = [
{ value: "click", label: "클릭" },
{ value: "submit", label: "제출" },
{ value: "select", label: "선택" },
{ value: "change", label: "변경" },
{ value: "doubleClick", label: "더블클릭" },
];
const targetActions = [
{ value: "open", label: "열기" },
{ value: "load", label: "로드" },
{ value: "refresh", label: "새로고침" },
{ value: "save", label: "저장" },
{ value: "filter", label: "필터" },
];
return (
<div className="space-y-4">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold"> </h3>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={loadDataFlows} className="h-8 w-8 p-0">
<RefreshCw className="h-3 w-3" />
</Button>
<Button variant="outline" size="sm" onClick={() => openModal()} className="h-8 gap-1 text-xs">
<Plus className="h-3 w-3" />
</Button>
</div>
</div>
{/* 설명 */}
<p className="text-xs text-muted-foreground">
. (: 목록 )
</p>
{/* 흐름 목록 */}
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div>
) : dataFlows.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-8">
<GitBranch className="h-8 w-8 text-muted-foreground/50" />
<p className="mt-2 text-xs text-muted-foreground"> </p>
</div>
) : (
<div className="space-y-2">
{dataFlows.map((flow) => (
<div
key={flow.id}
className="flex items-center justify-between rounded-lg border bg-card p-3 text-xs"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{/* 소스 화면 */}
<div className="flex flex-col">
<span className="font-medium truncate max-w-[100px]">
{flow.source_screen_name || `화면 ${flow.source_screen_id}`}
</span>
{flow.source_action && (
<span className="text-muted-foreground">{flow.source_action}</span>
)}
</div>
{/* 화살표 */}
<div className="flex items-center gap-1 text-primary">
<ArrowRight className="h-4 w-4" />
{flow.flow_type === "bidirectional" && (
<ArrowRight className="h-4 w-4 rotate-180" />
)}
</div>
{/* 타겟 화면 */}
<div className="flex flex-col">
<span className="font-medium truncate max-w-[100px]">
{flow.target_screen_name || `화면 ${flow.target_screen_id}`}
</span>
{flow.target_action && (
<span className="text-muted-foreground">{flow.target_action}</span>
)}
</div>
{/* 라벨 */}
{flow.flow_label && (
<span className="rounded bg-muted px-2 py-0.5 text-muted-foreground">
{flow.flow_label}
</span>
)}
</div>
{/* 액션 버튼 */}
<div className="flex items-center gap-1 ml-2">
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => openModal(flow)}>
<Pencil className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive hover:text-destructive"
onClick={() => handleDelete(flow.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
))}
</div>
)}
{/* 추가/수정 모달 */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
{selectedFlow ? "데이터 흐름 수정" : "데이터 흐름 추가"}
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 소스 화면 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs sm:text-sm"> *</Label>
<Select
value={formData.source_screen_id.toString()}
onValueChange={(value) => setFormData({ ...formData, source_screen_id: parseInt(value) })}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="화면 선택" />
</SelectTrigger>
<SelectContent>
{screens.map((screen) => (
<SelectItem key={screen.screen_id} value={screen.screen_id.toString()}>
{screen.screen_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={formData.source_action}
onValueChange={(value) => setFormData({ ...formData, source_action: value })}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="액션 선택" />
</SelectTrigger>
<SelectContent>
{sourceActions.map((action) => (
<SelectItem key={action.value} value={action.value}>
{action.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 타겟 화면 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs sm:text-sm"> *</Label>
<Select
value={formData.target_screen_id.toString()}
onValueChange={(value) => setFormData({ ...formData, target_screen_id: parseInt(value) })}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="화면 선택" />
</SelectTrigger>
<SelectContent>
{screens.map((screen) => (
<SelectItem key={screen.screen_id} value={screen.screen_id.toString()}>
{screen.screen_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={formData.target_action}
onValueChange={(value) => setFormData({ ...formData, target_action: value })}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="액션 선택" />
</SelectTrigger>
<SelectContent>
{targetActions.map((action) => (
<SelectItem key={action.value} value={action.value}>
{action.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 흐름 설정 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={formData.flow_type}
onValueChange={(value) => setFormData({ ...formData, flow_type: value })}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="unidirectional"></SelectItem>
<SelectItem value="bidirectional"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Input
value={formData.flow_label}
onChange={(e) => setFormData({ ...formData, flow_label: e.target.value })}
placeholder="예: 상세 보기"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
{/* 데이터 매핑 */}
<div>
<Label className="text-xs sm:text-sm"> (JSON)</Label>
<Textarea
value={formData.data_mapping}
onChange={(e) => setFormData({ ...formData, data_mapping: e.target.value })}
placeholder='{"source_field": "target_field"}'
className="min-h-[80px] font-mono text-xs sm:text-sm"
/>
<p className="mt-1 text-[10px] text-muted-foreground">
</p>
</div>
{/* 조건식 */}
<div>
<Label className="text-xs sm:text-sm"> ()</Label>
<Input
value={formData.condition_expression}
onChange={(e) => setFormData({ ...formData, condition_expression: e.target.value })}
placeholder="예: data.status === 'active'"
className="h-8 font-mono text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => setIsModalOpen(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
onClick={handleSave}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{selectedFlow ? "수정" : "추가"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}