summaryrefslogtreecommitdiff
path: root/lib/dashboard/dashboard-overview-chart.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dashboard/dashboard-overview-chart.tsx')
-rw-r--r--lib/dashboard/dashboard-overview-chart.tsx325
1 files changed, 325 insertions, 0 deletions
diff --git a/lib/dashboard/dashboard-overview-chart.tsx b/lib/dashboard/dashboard-overview-chart.tsx
new file mode 100644
index 00000000..ca5c0006
--- /dev/null
+++ b/lib/dashboard/dashboard-overview-chart.tsx
@@ -0,0 +1,325 @@
+"use client";
+
+import { TrendingUp, BarChart3, PieChart } from "lucide-react";
+import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Pie, PieChart as RechartsPieChart, Cell, ResponsiveContainer, LabelList } from "recharts";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { DashboardStats } from "@/lib/dashboard/service";
+
+interface DashboardOverviewChartProps {
+ data: DashboardStats[];
+ title: string;
+ description?: string;
+}
+
+// 차트 설정
+const chartConfig = {
+ pending: {
+ label: "대기",
+ color: "hsl(var(--chart-1))", // 회색 계열
+ },
+ inProgress: {
+ label: "진행중",
+ color: "hsl(var(--chart-2))", // 파란색 계열
+ },
+ completed: {
+ label: "완료",
+ color: "hsl(var(--chart-3))", // 초록색 계열
+ },
+} satisfies ChartConfig;
+
+// 파이 차트용 색상
+const PIE_COLORS = {
+ pending: "#6b7280",
+ inProgress: "#3b82f6",
+ completed: "#10b981"
+};
+
+export function DashboardOverviewChart({ data, title, description }: DashboardOverviewChartProps) {
+ // 바 차트용 데이터 변환
+ const barChartData = data.map(item => ({
+ name: item.displayName.length > 10 ?
+ item.displayName.substring(0, 10) + "..." :
+ item.displayName,
+ fullName: item.displayName,
+ pending: item.pending,
+ inProgress: item.inProgress,
+ completed: item.completed,
+ total: item.total
+ }));
+
+ // 파이 차트용 데이터 (전체 요약)
+ const totalPending = data.reduce((sum, item) => sum + item.pending, 0);
+ const totalInProgress = data.reduce((sum, item) => sum + item.inProgress, 0);
+ const totalCompleted = data.reduce((sum, item) => sum + item.completed, 0);
+ const totalTasks = totalPending + totalInProgress + totalCompleted;
+
+ const pieChartData = [
+ { name: "대기", value: totalPending, color: PIE_COLORS.pending },
+ { name: "진행중", value: totalInProgress, color: PIE_COLORS.inProgress },
+ { name: "완료", value: totalCompleted, color: PIE_COLORS.completed }
+ ].filter(item => item.value > 0);
+
+ // 완료율 계산
+ const completionRate = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0;
+ const isImproving = completionRate > 50; // 50% 이상이면 개선으로 간주
+
+ return (
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
+ {/* 바 차트 - 2/3 너비 */}
+ <Card className="md:col-span-2">
+ <CardHeader className="pb-4">
+ <CardTitle className="flex items-center gap-2 text-lg">
+ <BarChart3 className="h-5 w-5" />
+ {title} - 업무별 현황
+ </CardTitle>
+ {description && <CardDescription className="text-sm">{description}</CardDescription>}
+ </CardHeader>
+ <CardContent className="pb-2">
+ <ChartContainer
+ config={chartConfig}
+ className="max-h-[200px] w-full" // 고정 높이로 바 차트 크기 제한
+ >
+ <BarChart
+ accessibilityLayer
+ data={barChartData}
+ margin={{ top: 30, right: 15, left: 10, bottom: 5 }} // top margin 증가로 라벨 공간 확보
+ >
+ <CartesianGrid vertical={false} />
+ <XAxis
+ dataKey="name"
+ tickLine={false}
+ tickMargin={10}
+ axisLine={false}
+ fontSize={12}
+ interval={0}
+ // angle={-45}
+ textAnchor="end"
+ height={60}
+ />
+ <YAxis
+ tickLine={false}
+ axisLine={false}
+ fontSize={12}
+ />
+ <ChartTooltip
+ cursor={false}
+ content={<ChartTooltipContent
+ indicator="dashed"
+ labelFormatter={(label, payload) => {
+ const item = barChartData.find(d => d.name === label);
+ return item?.fullName || label;
+ }}
+ />}
+ />
+ <Bar
+ dataKey="pending"
+ fill="var(--color-pending)"
+ radius={[0, 0, 4, 4]}
+ stackId="status"
+ />
+ <Bar
+ dataKey="inProgress"
+ fill="var(--color-inProgress)"
+ radius={[0, 0, 0, 0]}
+ stackId="status"
+ />
+ <Bar
+ dataKey="completed"
+ fill="var(--color-completed)"
+ radius={[4, 4, 0, 0]}
+ stackId="status"
+ >
+ <LabelList
+ dataKey="total"
+ position="top"
+ offset={8}
+ className="fill-foreground"
+ fontSize={12}
+ formatter={(value: number) => value > 0 ? value : ''}
+ />
+ </Bar>
+ </BarChart>
+ </ChartContainer>
+ </CardContent>
+ <CardFooter className="flex-col items-start gap-1 text-sm pt-2">
+ <div className="flex gap-2 items-center leading-none font-medium">
+ {totalTasks > 0 && (
+ <>
+ <TrendingUp className={`h-4 w-4 ${isImproving ? 'text-green-600' : 'text-orange-600'}`} />
+ <span>완료율 {completionRate}% - {isImproving ? '순조롭게 진행중' : '진행 필요'}</span>
+ </>
+ )}
+ </div>
+ <div className="text-muted-foreground leading-none">
+ 총 {totalTasks}건의 업무 현황
+ </div>
+ </CardFooter>
+ </Card>
+
+ {/* 파이 차트 - 1/3 너비 */}
+ <Card className="md:col-span-1">
+ <CardHeader className="pb-4">
+ <CardTitle className="flex items-center gap-2 text-lg">
+ <PieChart className="h-5 w-5" />
+ 업무 분포
+ </CardTitle>
+ <CardDescription className="text-sm">상태별 비율</CardDescription>
+ </CardHeader>
+ <CardContent className="pb-2">
+ <ChartContainer
+ config={chartConfig}
+ className="max-h-[200px] w-full" // 고정 높이로 바 차트 크기 제한
+ >
+ <RechartsPieChart>
+ <ChartTooltip
+ cursor={false}
+ content={<ChartTooltipContent
+ hideLabel
+ formatter={(value, name) => [
+ `${value}건 (${Math.round((Number(value) / totalTasks) * 100)}%)`,
+ name
+ ]}
+ />}
+ />
+ <Pie
+ data={pieChartData}
+ cx="50%"
+ cy="50%"
+ labelLine={false}
+ label={({ name, percent }) =>
+ percent > 0.1 ? `${(percent * 100).toFixed(0)}%` : ''
+ }
+ outerRadius={70} // 작은 공간에 맞게 더 작게 조정
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {pieChartData.map((entry, index) => (
+ <Cell key={`cell-${index}`} fill={entry.color} />
+ ))}
+ </Pie>
+ </RechartsPieChart>
+ </ChartContainer>
+ </CardContent>
+ <CardFooter className="pt-2">
+ <div className="grid grid-cols-1 gap-1 w-full text-center">
+ <div className="text-xs space-y-1">
+ <div className="flex items-center justify-between">
+ <span className="text-muted-foreground">대기</span>
+ <span className="font-medium text-gray-600">{totalPending}</span>
+ </div>
+ <div className="flex items-center justify-between">
+ <span className="text-muted-foreground">진행</span>
+ <span className="font-medium text-blue-600">{totalInProgress}</span>
+ </div>
+ <div className="flex items-center justify-between">
+ <span className="text-muted-foreground">완료</span>
+ <span className="font-medium text-green-600">{totalCompleted}</span>
+ </div>
+ </div>
+ </div>
+ </CardFooter>
+ </Card>
+ </div>
+ );
+}
+
+// 더 컴팩트한 버전 (높이를 더 많이 줄이고 싶은 경우)
+export function CompactDashboardChart({ data, title, description }: DashboardOverviewChartProps) {
+ const totalPending = data.reduce((sum, item) => sum + item.pending, 0);
+ const totalInProgress = data.reduce((sum, item) => sum + item.inProgress, 0);
+ const totalCompleted = data.reduce((sum, item) => sum + item.completed, 0);
+ const totalTasks = totalPending + totalInProgress + totalCompleted;
+
+ const pieChartData = [
+ { name: "대기", value: totalPending, color: PIE_COLORS.pending },
+ { name: "진행중", value: totalInProgress, color: PIE_COLORS.inProgress },
+ { name: "완료", value: totalCompleted, color: PIE_COLORS.completed }
+ ].filter(item => item.value > 0);
+
+ const completionRate = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0;
+
+ return (
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
+ {/* 요약 통계 */}
+ <Card className="flex items-center p-4">
+ <div className="flex-1">
+ <h3 className="text-sm font-medium text-muted-foreground">완료율</h3>
+ <div className="text-2xl font-bold">{completionRate}%</div>
+ <p className="text-xs text-muted-foreground">총 {totalTasks}건</p>
+ </div>
+ <TrendingUp className={`h-8 w-8 ${completionRate > 50 ? 'text-green-600' : 'text-orange-600'}`} />
+ </Card>
+
+ {/* 컴팩트 파이 차트 */}
+ <Card className="col-span-2">
+ <CardHeader className="pb-3">
+ <CardTitle className="text-lg">{title}</CardTitle>
+ </CardHeader>
+ <CardContent className="pb-3">
+ <div className="flex items-center gap-6">
+ {/* 작은 파이 차트 */}
+ <div className="flex-shrink-0">
+ <ChartContainer
+ config={chartConfig}
+ className="h-[120px] w-[120px]"
+ >
+ <RechartsPieChart>
+ <ChartTooltip
+ cursor={false}
+ content={<ChartTooltipContent
+ hideLabel
+ formatter={(value, name) => [`${value}건`, name]}
+ />}
+ />
+ <Pie
+ data={pieChartData}
+ cx="50%"
+ cy="50%"
+ outerRadius={50}
+ innerRadius={20} // 도넛 차트로 만들어 공간 절약
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {pieChartData.map((entry, index) => (
+ <Cell key={`cell-${index}`} fill={entry.color} />
+ ))}
+ </Pie>
+ </RechartsPieChart>
+ </ChartContainer>
+ </div>
+
+ {/* 통계 목록 */}
+ <div className="flex-1 grid grid-cols-1 gap-3">
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-gray-500"></div>
+ <span className="text-sm">대기: {totalPending}건</span>
+ </div>
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-blue-500"></div>
+ <span className="text-sm">진행중: {totalInProgress}건</span>
+ </div>
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-green-500"></div>
+ <span className="text-sm">완료: {totalCompleted}건</span>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+