ERP-node/frontend/app/(main)/admin/flow-management/page.tsx

361 lines
13 KiB
TypeScript

"use client";
/**
* 플로우 관리 메인 페이지
* - 플로우 정의 목록
* - 플로우 생성/수정/삭제
* - 플로우 편집기로 이동
*/
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Plus, Edit2, Trash2, Play, Workflow, Table, Calendar, User } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { getFlowDefinitions, createFlowDefinition, deleteFlowDefinition } from "@/lib/api/flow";
import { FlowDefinition } from "@/types/flow";
export default function FlowManagementPage() {
const router = useRouter();
const { toast } = useToast();
// 상태
const [flows, setFlows] = useState<FlowDefinition[]>([]);
const [loading, setLoading] = useState(true);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedFlow, setSelectedFlow] = useState<FlowDefinition | null>(null);
// 생성 폼 상태
const [formData, setFormData] = useState({
name: "",
description: "",
tableName: "",
});
// 플로우 목록 조회
const loadFlows = async () => {
setLoading(true);
try {
const response = await getFlowDefinitions({ isActive: true });
if (response.success && response.data) {
setFlows(response.data);
} else {
toast({
title: "조회 실패",
description: response.error || "플로우 목록을 불러올 수 없습니다.",
variant: "destructive",
});
}
} catch (error: any) {
toast({
title: "오류 발생",
description: error.message,
variant: "destructive",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
loadFlows();
}, []);
// 플로우 생성
const handleCreate = async () => {
if (!formData.name || !formData.tableName) {
toast({
title: "입력 오류",
description: "플로우 이름과 테이블 이름은 필수입니다.",
variant: "destructive",
});
return;
}
try {
const response = await createFlowDefinition(formData);
if (response.success && response.data) {
toast({
title: "생성 완료",
description: "플로우가 성공적으로 생성되었습니다.",
});
setIsCreateDialogOpen(false);
setFormData({ name: "", description: "", tableName: "" });
loadFlows();
} else {
toast({
title: "생성 실패",
description: response.error || response.message,
variant: "destructive",
});
}
} catch (error: any) {
toast({
title: "오류 발생",
description: error.message,
variant: "destructive",
});
}
};
// 플로우 삭제
const handleDelete = async () => {
if (!selectedFlow) return;
try {
const response = await deleteFlowDefinition(selectedFlow.id);
if (response.success) {
toast({
title: "삭제 완료",
description: "플로우가 삭제되었습니다.",
});
setIsDeleteDialogOpen(false);
setSelectedFlow(null);
loadFlows();
} else {
toast({
title: "삭제 실패",
description: response.error,
variant: "destructive",
});
}
} catch (error: any) {
toast({
title: "오류 발생",
description: error.message,
variant: "destructive",
});
}
};
// 플로우 편집기로 이동
const handleEdit = (flowId: number) => {
router.push(`/admin/flow-management/${flowId}`);
};
return (
<div className="container mx-auto space-y-4 p-3 sm:space-y-6 sm:p-4 lg:p-6">
{/* 헤더 */}
<div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div className="flex-1">
<h1 className="flex items-center gap-2 text-xl font-bold sm:text-2xl lg:text-3xl">
<Workflow className="h-6 w-6 sm:h-7 sm:w-7 lg:h-8 lg:w-8" />
</h1>
<p className="text-muted-foreground mt-1 text-xs sm:text-sm"> </p>
</div>
<Button onClick={() => setIsCreateDialogOpen(true)} className="w-full sm:w-auto">
<Plus className="mr-2 h-4 w-4" />
<span className="hidden sm:inline"> </span>
<span className="sm:hidden"></span>
</Button>
</div>
{/* 플로우 카드 목록 */}
{loading ? (
<div className="py-8 text-center sm:py-12">
<p className="text-muted-foreground text-sm sm:text-base"> ...</p>
</div>
) : flows.length === 0 ? (
<Card>
<CardContent className="py-8 text-center sm:py-12">
<Workflow className="text-muted-foreground mx-auto mb-3 h-10 w-10 sm:mb-4 sm:h-12 sm:w-12" />
<p className="text-muted-foreground mb-3 text-sm sm:mb-4 sm:text-base"> </p>
<Button onClick={() => setIsCreateDialogOpen(true)} className="w-full sm:w-auto">
<Plus className="mr-2 h-4 w-4" />
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 gap-4 sm:gap-5 md:grid-cols-2 lg:gap-6 xl:grid-cols-3">
{flows.map((flow) => (
<Card
key={flow.id}
className="cursor-pointer transition-shadow hover:shadow-lg"
onClick={() => handleEdit(flow.id)}
>
<CardHeader className="p-4 sm:p-6">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<CardTitle className="flex flex-col gap-1 text-base sm:flex-row sm:items-center sm:gap-2 sm:text-lg">
<span className="truncate">{flow.name}</span>
{flow.isActive && (
<Badge variant="success" className="self-start">
</Badge>
)}
</CardTitle>
<CardDescription className="mt-1 line-clamp-2 text-xs sm:mt-2 sm:text-sm">
{flow.description || "설명 없음"}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-4 pt-0 sm:p-6">
<div className="space-y-1.5 text-xs sm:space-y-2 sm:text-sm">
<div className="text-muted-foreground flex items-center gap-1.5 sm:gap-2">
<Table className="h-3.5 w-3.5 shrink-0 sm:h-4 sm:w-4" />
<span className="truncate">{flow.tableName}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1.5 sm:gap-2">
<User className="h-3.5 w-3.5 shrink-0 sm:h-4 sm:w-4" />
<span className="truncate">: {flow.createdBy}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1.5 sm:gap-2">
<Calendar className="h-3.5 w-3.5 shrink-0 sm:h-4 sm:w-4" />
<span>{new Date(flow.updatedAt).toLocaleDateString("ko-KR")}</span>
</div>
</div>
<div className="mt-3 flex gap-2 sm:mt-4">
<Button
variant="outline"
size="sm"
className="h-8 flex-1 text-xs sm:h-9 sm:text-sm"
onClick={(e) => {
e.stopPropagation();
handleEdit(flow.id);
}}
>
<Edit2 className="mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4" />
</Button>
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs sm:h-9 sm:px-3 sm:text-sm"
onClick={(e) => {
e.stopPropagation();
setSelectedFlow(flow);
setIsDeleteDialogOpen(true);
}}
>
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* 생성 다이얼로그 */}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
<div>
<Label htmlFor="name" className="text-xs sm:text-sm">
*
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="예: 제품 수명주기 관리"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="tableName" className="text-xs sm:text-sm">
*
</Label>
<Input
id="tableName"
value={formData.tableName}
onChange={(e) => setFormData({ ...formData, tableName: e.target.value })}
placeholder="예: products"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
</p>
</div>
<div>
<Label htmlFor="description" className="text-xs sm:text-sm">
</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="플로우에 대한 설명을 입력하세요"
rows={3}
className="text-xs sm:text-sm"
/>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button onClick={handleCreate} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 삭제 확인 다이얼로그 */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
"{selectedFlow?.name}" ?
<br /> .
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => {
setIsDeleteDialogOpen(false);
setSelectedFlow(null);
}}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
variant="destructive"
onClick={handleDelete}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}