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

289 lines
12 KiB
TypeScript

"use client";
import React, { 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 } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Plus, Search, MoreVertical, Edit, Trash2, Copy, CheckCircle2 } from "lucide-react";
/**
* 대시보드 관리 페이지
* - 대시보드 목록 조회
* - 대시보드 생성/수정/삭제/복사
*/
export default function DashboardListPage() {
const router = useRouter();
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [error, setError] = useState<string | null>(null);
// 모달 상태
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
const [successMessage, setSuccessMessage] = useState("");
// 대시보드 목록 로드
const loadDashboards = async () => {
try {
setLoading(true);
setError(null);
const result = await dashboardApi.getMyDashboards({ search: searchTerm });
setDashboards(result.dashboards);
} catch (err) {
console.error("Failed to load dashboards:", err);
setError("대시보드 목록을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
};
useEffect(() => {
loadDashboards();
}, [searchTerm]);
// 대시보드 삭제 확인 모달 열기
const handleDeleteClick = (id: string, title: string) => {
setDeleteTarget({ id, title });
setDeleteDialogOpen(true);
};
// 대시보드 삭제 실행
const handleDeleteConfirm = async () => {
if (!deleteTarget) return;
try {
await dashboardApi.deleteDashboard(deleteTarget.id);
setDeleteDialogOpen(false);
setDeleteTarget(null);
setSuccessMessage("대시보드가 삭제되었습니다.");
setSuccessDialogOpen(true);
loadDashboards();
} catch (err) {
console.error("Failed to delete dashboard:", err);
setDeleteDialogOpen(false);
setError("대시보드 삭제에 실패했습니다.");
}
};
// 대시보드 복사
const handleCopy = async (dashboard: Dashboard) => {
try {
// 전체 대시보드 정보(요소 포함)를 가져오기
const fullDashboard = await dashboardApi.getDashboard(dashboard.id);
const newDashboard = await dashboardApi.createDashboard({
title: `${fullDashboard.title} (복사본)`,
description: fullDashboard.description,
elements: fullDashboard.elements || [],
isPublic: false,
tags: fullDashboard.tags,
category: fullDashboard.category,
settings: (fullDashboard as any).settings, // 해상도와 배경색 설정도 복사
});
setSuccessMessage("대시보드가 복사되었습니다.");
setSuccessDialogOpen(true);
loadDashboards();
} catch (err) {
console.error("Failed to copy dashboard:", err);
setError("대시보드 복사에 실패했습니다.");
}
};
// 포맷팅 헬퍼
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ko-KR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
};
if (loading) {
return (
<div className="flex h-full items-center justify-center rounded-lg border bg-card shadow-sm">
<div className="text-center">
<div className="text-sm font-medium"> ...</div>
<div className="mt-2 text-xs text-muted-foreground"> </div>
</div>
</div>
);
}
return (
<div className="flex min-h-screen flex-col bg-background">
<div className="space-y-6 p-6">
{/* 페이지 헤더 */}
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> </p>
</div>
{/* 검색 및 액션 */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="relative w-full sm:w-[300px]">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="대시보드 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="h-10 pl-10 text-sm"
/>
</div>
<Button onClick={() => router.push("/admin/dashboard/new")} className="h-10 gap-2 text-sm font-medium">
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 에러 메시지 */}
{error && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-destructive"> </p>
<button
onClick={() => setError(null)}
className="text-destructive transition-colors hover:text-destructive/80"
aria-label="에러 메시지 닫기"
>
</button>
</div>
<p className="mt-1.5 text-sm text-destructive/80">{error}</p>
</div>
)}
{/* 대시보드 목록 */}
{dashboards.length === 0 ? (
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
<div className="flex flex-col items-center gap-2 text-center">
<p className="text-sm text-muted-foreground"> </p>
</div>
</div>
) : (
<div className="rounded-lg border bg-card shadow-sm">
<Table>
<TableHeader>
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-right text-sm font-semibold"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{dashboards.map((dashboard) => (
<TableRow key={dashboard.id} className="border-b transition-colors hover:bg-muted/50">
<TableCell className="h-16 text-sm font-medium">{dashboard.title}</TableCell>
<TableCell className="h-16 max-w-md truncate text-sm text-muted-foreground">
{dashboard.description || "-"}
</TableCell>
<TableCell className="h-16 text-sm text-muted-foreground">{formatDate(dashboard.createdAt)}</TableCell>
<TableCell className="h-16 text-sm text-muted-foreground">{formatDate(dashboard.updatedAt)}</TableCell>
<TableCell className="h-16 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => router.push(`/admin/dashboard/edit/${dashboard.id}`)}
className="gap-2 text-sm"
>
<Edit className="h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopy(dashboard)} className="gap-2 text-sm">
<Copy className="h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(dashboard.id, dashboard.title)}
className="gap-2 text-sm text-destructive focus:text-destructive"
>
<Trash2 className="h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
{/* 삭제 확인 모달 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
<AlertDialogHeader>
<AlertDialogTitle className="text-base sm:text-lg"> </AlertDialogTitle>
<AlertDialogDescription className="text-xs sm:text-sm">
&quot;{deleteTarget?.title}&quot; ?
<br />
.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="gap-2 sm:gap-0">
<AlertDialogCancel className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"></AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteConfirm}
className="h-8 flex-1 bg-destructive text-xs hover:bg-destructive/90 sm:h-10 sm:flex-none sm:text-sm"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 성공 모달 */}
<Dialog open={successDialogOpen} onOpenChange={setSuccessDialogOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-md">
<DialogHeader>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<CheckCircle2 className="h-6 w-6 text-primary" />
</div>
<DialogTitle className="text-center text-base sm:text-lg"></DialogTitle>
<DialogDescription className="text-center text-xs sm:text-sm">{successMessage}</DialogDescription>
</DialogHeader>
<div className="flex justify-center pt-4">
<Button onClick={() => setSuccessDialogOpen(false)} className="h-8 text-xs sm:h-10 sm:text-sm">
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}