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

277 lines
11 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { dashboardApi } from "@/lib/api/dashboard";
import { Dashboard } from "@/lib/api/dashboard";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
2025-10-16 18:09:46 +09:00
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useToast } from "@/hooks/use-toast";
2025-10-21 16:28:03 +09:00
import { Plus, Search, Edit, Trash2, Copy, LayoutDashboard, MoreHorizontal } from "lucide-react";
/**
*
* -
* - ///
*/
export default function DashboardListPage() {
const router = useRouter();
const { toast } = useToast();
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
2025-10-16 18:09:46 +09:00
// 모달 상태
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
// 대시보드 목록 로드
const loadDashboards = async () => {
try {
setLoading(true);
const result = await dashboardApi.getMyDashboards({ search: searchTerm });
setDashboards(result.dashboards);
} catch (err) {
console.error("Failed to load dashboards:", err);
toast({
title: "오류",
description: "대시보드 목록을 불러오는데 실패했습니다.",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
loadDashboards();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchTerm]);
2025-10-16 18:09:46 +09:00
// 대시보드 삭제 확인 모달 열기
const handleDeleteClick = (id: string, title: string) => {
setDeleteTarget({ id, title });
setDeleteDialogOpen(true);
};
// 대시보드 삭제 실행
const handleDeleteConfirm = async () => {
if (!deleteTarget) return;
try {
2025-10-16 18:09:46 +09:00
await dashboardApi.deleteDashboard(deleteTarget.id);
setDeleteDialogOpen(false);
setDeleteTarget(null);
toast({
title: "성공",
description: "대시보드가 삭제되었습니다.",
});
loadDashboards();
} catch (err) {
console.error("Failed to delete dashboard:", err);
2025-10-16 18:09:46 +09:00
setDeleteDialogOpen(false);
toast({
title: "오류",
description: "대시보드 삭제에 실패했습니다.",
variant: "destructive",
});
}
};
// 대시보드 복사
const handleCopy = async (dashboard: Dashboard) => {
try {
2025-10-16 17:27:03 +09:00
const fullDashboard = await dashboardApi.getDashboard(dashboard.id);
await dashboardApi.createDashboard({
2025-10-16 17:27:03 +09:00
title: `${fullDashboard.title} (복사본)`,
description: fullDashboard.description,
elements: fullDashboard.elements || [],
isPublic: false,
2025-10-16 17:27:03 +09:00
tags: fullDashboard.tags,
category: fullDashboard.category,
settings: fullDashboard.settings as { resolution?: string; backgroundColor?: string },
});
toast({
title: "성공",
description: "대시보드가 복사되었습니다.",
});
loadDashboards();
} catch (err) {
console.error("Failed to copy dashboard:", err);
toast({
title: "오류",
description: "대시보드 복사에 실패했습니다.",
variant: "destructive",
});
}
};
// 포맷팅 헬퍼
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ko-KR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
};
return (
<div className="min-h-screen bg-gray-50">
<div className="w-full max-w-none space-y-8 px-4 py-8">
{/* 페이지 제목 */}
<div className="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
</div>
{/* 검색 및 필터 */}
<Card className="shadow-sm">
<CardContent className="pt-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="대시보드 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-64 pl-10"
/>
</div>
<Button onClick={() => router.push("/admin/dashboard/new")} className="shrink-0">
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* 대시보드 목록 */}
{loading ? (
<div className="flex h-64 items-center justify-center">
<div className="text-gray-500"> ...</div>
</div>
) : dashboards.length === 0 ? (
<Card className="shadow-sm">
<CardContent className="pt-6">
<div className="py-8 text-center text-gray-500">
<LayoutDashboard className="mx-auto mb-4 h-12 w-12 text-gray-400" />
<p className="mb-2 text-lg font-medium"> </p>
<p className="mb-4 text-sm text-gray-400"> .</p>
<Button onClick={() => router.push("/admin/dashboard/new")}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
) : (
<Card className="shadow-sm">
<CardContent className="p-4">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[250px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{dashboards.map((dashboard) => (
<TableRow key={dashboard.id} className="hover:bg-gray-50">
<TableCell>
<div className="font-medium">{dashboard.title}</div>
</TableCell>
<TableCell className="max-w-md truncate text-sm text-gray-500">
{dashboard.description || "-"}
</TableCell>
<TableCell className="text-sm">{formatDate(dashboard.createdAt)}</TableCell>
<TableCell className="text-right">
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
2025-10-21 16:28:03 +09:00
<MoreHorizontal className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-40 p-1" align="end">
<div className="flex flex-col gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => router.push(`/admin/dashboard/edit/${dashboard.id}`)}
className="h-8 w-full justify-start gap-2 px-2 text-xs"
>
<Edit className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopy(dashboard)}
className="h-8 w-full justify-start gap-2 px-2 text-xs"
>
<Copy className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteClick(dashboard.id, dashboard.title)}
className="h-8 w-full justify-start gap-2 px-2 text-xs text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</PopoverContent>
</Popover>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
</div>
2025-10-16 18:09:46 +09:00
{/* 삭제 확인 모달 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
2025-10-16 18:09:46 +09:00
<AlertDialogDescription>
&quot;{deleteTarget?.title}&quot; ?
<br />
<span className="font-medium text-red-600"> .</span>
2025-10-16 18:09:46 +09:00
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteConfirm}
className="bg-red-600 text-white hover:bg-red-700 focus:ring-red-600"
>
2025-10-16 18:09:46 +09:00
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}