diff options
Diffstat (limited to 'lib/dashboard/dashboard-overview-chart.tsx')
| -rw-r--r-- | lib/dashboard/dashboard-overview-chart.tsx | 325 |
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> + ); +} + |
