대시보드관리 페이지 레이아웃 통일
This commit is contained in:
parent
d57756189f
commit
5ca0a6b6dc
|
|
@ -1,20 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
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 } from "@/components/ui/card";
|
||||
import { Card, CardContent } 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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -25,8 +19,8 @@ import {
|
|||
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";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Plus, Search, Edit, Trash2, Copy, LayoutDashboard, MoreVertical } from "lucide-react";
|
||||
|
||||
/**
|
||||
* 대시보드 관리 페이지
|
||||
|
|
@ -35,27 +29,28 @@ import { Plus, Search, MoreVertical, Edit, Trash2, Copy, CheckCircle2 } from "lu
|
|||
*/
|
||||
export default function DashboardListPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
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("대시보드 목록을 불러오는데 실패했습니다.");
|
||||
toast({
|
||||
title: "오류",
|
||||
description: "대시보드 목록을 불러오는데 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -63,6 +58,7 @@ export default function DashboardListPage() {
|
|||
|
||||
useEffect(() => {
|
||||
loadDashboards();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchTerm]);
|
||||
|
||||
// 대시보드 삭제 확인 모달 열기
|
||||
|
|
@ -79,37 +75,48 @@ export default function DashboardListPage() {
|
|||
await dashboardApi.deleteDashboard(deleteTarget.id);
|
||||
setDeleteDialogOpen(false);
|
||||
setDeleteTarget(null);
|
||||
setSuccessMessage("대시보드가 삭제되었습니다.");
|
||||
setSuccessDialogOpen(true);
|
||||
toast({
|
||||
title: "성공",
|
||||
description: "대시보드가 삭제되었습니다.",
|
||||
});
|
||||
loadDashboards();
|
||||
} catch (err) {
|
||||
console.error("Failed to delete dashboard:", err);
|
||||
setDeleteDialogOpen(false);
|
||||
setError("대시보드 삭제에 실패했습니다.");
|
||||
toast({
|
||||
title: "오류",
|
||||
description: "대시보드 삭제에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 대시보드 복사
|
||||
const handleCopy = async (dashboard: Dashboard) => {
|
||||
try {
|
||||
// 전체 대시보드 정보(요소 포함)를 가져오기
|
||||
const fullDashboard = await dashboardApi.getDashboard(dashboard.id);
|
||||
|
||||
const newDashboard = await dashboardApi.createDashboard({
|
||||
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, // 해상도와 배경색 설정도 복사
|
||||
settings: fullDashboard.settings as { resolution?: string; backgroundColor?: string },
|
||||
});
|
||||
toast({
|
||||
title: "성공",
|
||||
description: "대시보드가 복사되었습니다.",
|
||||
});
|
||||
setSuccessMessage("대시보드가 복사되었습니다.");
|
||||
setSuccessDialogOpen(true);
|
||||
loadDashboards();
|
||||
} catch (err) {
|
||||
console.error("Failed to copy dashboard:", err);
|
||||
setError("대시보드 복사에 실패했습니다.");
|
||||
toast({
|
||||
title: "오류",
|
||||
description: "대시보드 복사에 실패했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -119,120 +126,125 @@ export default function DashboardListPage() {
|
|||
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 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>
|
||||
<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>
|
||||
)}
|
||||
{/* 검색 및 필터 */}
|
||||
<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>
|
||||
|
||||
{/* 대시보드 목록 */}
|
||||
{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>
|
||||
{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>
|
||||
<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(`/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={() => handleDeleteClick(dashboard.id, dashboard.title)}
|
||||
className="gap-2 text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
삭제
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
<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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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">
|
||||
<MoreVertical 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>
|
||||
|
|
@ -241,36 +253,24 @@ export default function DashboardListPage() {
|
|||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>대시보드 삭제</AlertDialogTitle>
|
||||
<AlertDialogTitle>대시보드 삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{deleteTarget?.title}" 대시보드를 삭제하시겠습니까?
|
||||
<br />이 작업은 되돌릴 수 없습니다.
|
||||
<br />
|
||||
<span className="font-medium text-red-600">이 작업은 되돌릴 수 없습니다.</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-red-600 hover:bg-red-700">
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteConfirm}
|
||||
className="bg-red-600 text-white hover:bg-red-700 focus:ring-red-600"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 성공 모달 */}
|
||||
<Dialog open={successDialogOpen} onOpenChange={setSuccessDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<CheckCircle2 className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<DialogTitle className="text-center">완료</DialogTitle>
|
||||
<DialogDescription className="text-center">{successMessage}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button onClick={() => setSuccessDialogOpen(false)}>확인</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue