216 lines
8.1 KiB
TypeScript
216 lines
8.1 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 { Plus, Search, MoreVertical, Edit, Trash2, Copy, Eye } 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 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 handleDelete = async (id: string, title: string) => {
|
|
if (!confirm(`"${title}" 대시보드를 삭제하시겠습니까?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await dashboardApi.deleteDashboard(id);
|
|
alert("대시보드가 삭제되었습니다.");
|
|
loadDashboards();
|
|
} catch (err) {
|
|
console.error("Failed to delete dashboard:", err);
|
|
alert("대시보드 삭제에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
// 대시보드 복사
|
|
const handleCopy = async (dashboard: Dashboard) => {
|
|
try {
|
|
const newDashboard = await dashboardApi.createDashboard({
|
|
title: `${dashboard.title} (복사본)`,
|
|
description: dashboard.description,
|
|
elements: dashboard.elements || [],
|
|
isPublic: false,
|
|
tags: dashboard.tags,
|
|
category: dashboard.category,
|
|
});
|
|
alert("대시보드가 복사되었습니다.");
|
|
loadDashboards();
|
|
} catch (err) {
|
|
console.error("Failed to copy dashboard:", err);
|
|
alert("대시보드 복사에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
// 포맷팅 헬퍼
|
|
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">
|
|
<div className="text-center">
|
|
<div className="text-lg font-medium text-gray-900">로딩 중...</div>
|
|
<div className="mt-2 text-sm text-gray-500">대시보드 목록을 불러오고 있습니다</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="h-full overflow-auto bg-gray-50 p-6">
|
|
<div className="mx-auto max-w-7xl">
|
|
{/* 헤더 */}
|
|
<div className="mb-6">
|
|
<h1 className="text-3xl font-bold text-gray-900">대시보드 관리</h1>
|
|
<p className="mt-2 text-sm text-gray-600">대시보드를 생성하고 관리할 수 있습니다</p>
|
|
</div>
|
|
|
|
{/* 액션 바 */}
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div className="relative w-64">
|
|
<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="pl-9"
|
|
/>
|
|
</div>
|
|
<Button onClick={() => router.push("/admin/dashboard/new")} className="gap-2">
|
|
<Plus className="h-4 w-4" />새 대시보드 생성
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 에러 메시지 */}
|
|
{error && (
|
|
<Card className="mb-6 border-red-200 bg-red-50 p-4">
|
|
<p className="text-sm text-red-800">{error}</p>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 대시보드 목록 */}
|
|
{dashboards.length === 0 ? (
|
|
<Card className="p-12 text-center">
|
|
<div className="mx-auto mb-4 flex h-24 w-24 items-center justify-center rounded-full bg-gray-100">
|
|
<Plus className="h-12 w-12 text-gray-400" />
|
|
</div>
|
|
<h3 className="mb-2 text-lg font-medium text-gray-900">대시보드가 없습니다</h3>
|
|
<p className="mb-6 text-sm text-gray-500">첫 번째 대시보드를 생성하여 데이터 시각화를 시작하세요</p>
|
|
<Button onClick={() => router.push("/admin/dashboard/new")} className="gap-2">
|
|
<Plus className="h-4 w-4" />새 대시보드 생성
|
|
</Button>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>제목</TableHead>
|
|
<TableHead>설명</TableHead>
|
|
<TableHead>생성일</TableHead>
|
|
<TableHead>수정일</TableHead>
|
|
<TableHead className="w-[80px]">작업</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{dashboards.map((dashboard) => (
|
|
<TableRow key={dashboard.id} className="cursor-pointer hover:bg-gray-50">
|
|
<TableCell className="font-medium">{dashboard.title}</TableCell>
|
|
<TableCell className="max-w-md truncate text-sm text-gray-500">
|
|
{dashboard.description || "-"}
|
|
</TableCell>
|
|
<TableCell className="text-sm text-gray-500">{formatDate(dashboard.createdAt)}</TableCell>
|
|
<TableCell className="text-sm text-gray-500">{formatDate(dashboard.updatedAt)}</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => router.push(`/dashboard/${dashboard.id}`)} className="gap-2">
|
|
<Eye className="h-4 w-4" />
|
|
보기
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => router.push(`/admin/dashboard/edit/${dashboard.id}`)}
|
|
className="gap-2"
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
편집
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleCopy(dashboard)} className="gap-2">
|
|
<Copy className="h-4 w-4" />
|
|
복사
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => handleDelete(dashboard.id, dashboard.title)}
|
|
className="gap-2 text-red-600 focus:text-red-600"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
삭제
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|