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

320 lines
11 KiB
TypeScript

"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 { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useToast } from "@/hooks/use-toast";
import { Pagination, PaginationInfo } from "@/components/common/Pagination";
import { DeleteConfirmModal } from "@/components/common/DeleteConfirmModal";
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
import { ScrollToTop } from "@/components/common/ScrollToTop";
import { Plus, Search, Edit, Trash2, Copy, MoreVertical, AlertCircle, RefreshCw } from "lucide-react";
/**
* 대시보드 관리 페이지
* - CSR 방식으로 초기 데이터 로드
* - 대시보드 목록 조회
* - 대시보드 생성/수정/삭제/복사
*/
export default function DashboardListPage() {
const router = useRouter();
const { toast } = useToast();
// 상태 관리
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
// 페이지네이션 상태
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [totalCount, setTotalCount] = useState(0);
// 모달 상태
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
// 대시보드 목록 로드
const loadDashboards = async () => {
try {
setLoading(true);
setError(null);
const result = await dashboardApi.getMyDashboards({
search: searchTerm,
page: currentPage,
limit: pageSize,
});
setDashboards(result.dashboards);
setTotalCount(result.pagination.total);
} catch (err) {
console.error("Failed to load dashboards:", err);
setError(
err instanceof Error
? err.message
: "대시보드 목록을 불러오는데 실패했습니다. 네트워크 연결을 확인하거나 잠시 후 다시 시도해주세요.",
);
} finally {
setLoading(false);
}
};
// 검색어/페이지 변경 시 fetch (초기 로딩 포함)
useEffect(() => {
loadDashboards();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchTerm, currentPage, pageSize]);
// 페이지네이션 정보 계산
const paginationInfo: PaginationInfo = {
currentPage,
totalPages: Math.ceil(totalCount / pageSize) || 1,
totalItems: totalCount,
itemsPerPage: pageSize,
startItem: totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1,
endItem: Math.min(currentPage * pageSize, totalCount),
};
const handlePageChange = (page: number) => setCurrentPage(page);
const handlePageSizeChange = (size: number) => {
setPageSize(size);
setCurrentPage(1);
};
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);
toast({ title: "성공", description: "대시보드가 삭제되었습니다." });
loadDashboards();
} catch (err) {
console.error("Failed to delete dashboard:", err);
setDeleteDialogOpen(false);
toast({ title: "오류", description: "대시보드 삭제에 실패했습니다.", variant: "destructive" });
}
};
const handleCopy = async (dashboard: Dashboard) => {
try {
const fullDashboard = await dashboardApi.getDashboard(dashboard.id);
await dashboardApi.createDashboard({
title: `${fullDashboard.title} (복사본)`,
description: fullDashboard.description,
elements: fullDashboard.elements || [],
isPublic: false,
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) =>
new Date(dateString).toLocaleDateString("ko-KR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
// ResponsiveDataView 컬럼 정의
const columns: RDVColumn<Dashboard>[] = [
{
key: "title",
label: "제목",
render: (_v, row) => (
<button
onClick={() => router.push(`/admin/screenMng/dashboardList/${row.id}`)}
className="hover:text-primary cursor-pointer text-left font-medium transition-colors hover:underline"
>
{row.title}
</button>
),
},
{
key: "description",
label: "설명",
render: (_v, row) => (
<span className="text-muted-foreground max-w-md truncate">{row.description || "-"}</span>
),
},
{
key: "createdByName",
label: "생성자",
width: "120px",
render: (_v, row) => row.createdByName || row.createdBy || "-",
},
{
key: "createdAt",
label: "생성일",
width: "120px",
render: (_v, row) => formatDate(row.createdAt),
},
{
key: "updatedAt",
label: "수정일",
width: "120px",
render: (_v, row) => formatDate(row.updatedAt),
},
];
// 모바일 카드 필드 정의
const cardFields: RDVCardField<Dashboard>[] = [
{
label: "설명",
render: (d) => (
<span className="max-w-[200px] truncate">{d.description || "-"}</span>
),
},
{ label: "생성자", render: (d) => d.createdByName || d.createdBy || "-" },
{ label: "생성일", render: (d) => formatDate(d.createdAt) },
{ label: "수정일", render: (d) => formatDate(d.updatedAt) },
];
return (
<div className="bg-background flex min-h-screen flex-col">
<div className="space-y-6 p-4 sm:p-6">
{/* 페이지 헤더 */}
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-muted-foreground text-sm"> </p>
</div>
{/* 검색 및 액션 (반응형) */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex items-center gap-4">
<div className="relative w-full sm:w-[300px]">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
placeholder="대시보드 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="h-10 pl-10 text-sm"
/>
</div>
<div className="text-muted-foreground hidden text-sm sm:block">
<span className="text-foreground font-semibold">{totalCount.toLocaleString()}</span>
</div>
</div>
<Button onClick={() => router.push("/admin/screenMng/dashboardList/new")} className="h-10 gap-2 text-sm font-medium">
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 대시보드 목록 */}
{error ? (
<div className="border-destructive/50 bg-destructive/10 flex flex-col items-center justify-center rounded-lg border p-12">
<div className="flex flex-col items-center gap-4 text-center">
<div className="bg-destructive/20 flex h-16 w-16 items-center justify-center rounded-full">
<AlertCircle className="text-destructive h-8 w-8" />
</div>
<div>
<h3 className="text-destructive mb-2 text-lg font-semibold"> </h3>
<p className="text-destructive/80 max-w-md text-sm">{error}</p>
</div>
<Button onClick={loadDashboards} variant="outline" className="mt-2 gap-2">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</div>
) : (
<ResponsiveDataView<Dashboard>
data={dashboards}
columns={columns}
keyExtractor={(d) => d.id}
isLoading={loading}
emptyMessage="대시보드가 없습니다."
skeletonCount={10}
cardTitle={(d) => d.title}
cardSubtitle={(d) => d.id}
cardFields={cardFields}
onRowClick={(d) => router.push(`/admin/screenMng/dashboardList/${d.id}`)}
renderActions={(d) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e) => e.stopPropagation()}>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => router.push(`/admin/screenMng/dashboardList/${d.id}`)}
className="gap-2 text-sm"
>
<Edit className="h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopy(d)} className="gap-2 text-sm">
<Copy className="h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(d.id, d.title)}
className="text-destructive focus:text-destructive gap-2 text-sm"
>
<Trash2 className="h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
actionsLabel="작업"
actionsWidth="80px"
/>
)}
{/* 페이지네이션 */}
{!loading && dashboards.length > 0 && (
<Pagination
paginationInfo={paginationInfo}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showPageSizeSelector={true}
pageSizeOptions={[10, 20, 50, 100]}
/>
)}
{/* 삭제 확인 모달 */}
<DeleteConfirmModal
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="대시보드 삭제"
description={
<>
&quot;{deleteTarget?.title}&quot; ?
<br /> .
</>
}
onConfirm={handleDeleteConfirm}
/>
</div>
{/* Scroll to Top 버튼 */}
<ScrollToTop />
</div>
);
}