diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/form-data-stat/form-data-stat-table.tsx | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/components/form-data-stat/form-data-stat-table.tsx b/components/form-data-stat/form-data-stat-table.tsx new file mode 100644 index 00000000..624e85fb --- /dev/null +++ b/components/form-data-stat/form-data-stat-table.tsx @@ -0,0 +1,276 @@ +"use client"; + +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { RefreshCw } from "lucide-react"; +import { type ColumnDef } from "@tanstack/react-table"; +import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"; +import { ClientDataTable } from "@/components/client-data-table/data-table"; +import { cn } from "@/lib/utils"; +import { toast } from "sonner"; +import type { DataTableAdvancedFilterField } from "@/types/table"; +import { Progress } from "@/components/ui/progress"; +import { getVendorFormStatus } from "@/lib/forms/stat"; + +// 타입 정의 +interface VendorFormStatus { + vendorId: number; + vendorName: string; + formCount: number; // 벤더가 가진 form 개수 + tagCount: number; // 벤더가 가진 tag 개수 + totalFields: number; // 입력해야 하는 총 필드 개수 + completedFields: number; // 입력 완료된 필드 개수 + completionRate: number; // 완료율 (%) +} + +interface VendorFormStatusTableProps { + initialData?: VendorFormStatus[]; +} + +// 완료율에 따른 색상 반환 +const getCompletionColor = (rate: number) => { + if (rate >= 80) return "text-green-600"; + if (rate >= 50) return "text-yellow-600"; + if (rate >= 20) return "text-orange-600"; + return "text-red-600"; +}; + +// 완료율에 따른 Badge variant 반환 +const getCompletionBadgeVariant = (rate: number): "default" | "secondary" | "destructive" | "outline" => { + if (rate >= 80) return "default"; + if (rate >= 50) return "secondary"; + if (rate >= 20) return "outline"; + return "destructive"; +}; + +export function VendorFormStatusTable({ + initialData = [], +}: VendorFormStatusTableProps) { + const [data, setData] = React.useState<VendorFormStatus[]>(initialData); + const [isRefreshing, setIsRefreshing] = React.useState(false); + + // 데이터 새로고침 + const handleRefresh = React.useCallback(async () => { + setIsRefreshing(true); + try { + const result = await getVendorFormStatus(); + setData(result); + toast.success("데이터를 새로고침했습니다."); + } catch (error) { + console.error("Refresh error:", error); + toast.error("새로고침 중 오류가 발생했습니다."); + } finally { + setIsRefreshing(false); + } + }, []); + + // 초기 데이터 로드 + React.useEffect(() => { + if (initialData.length === 0) { + handleRefresh(); + } + }, []); + + // 컬럼 정의 + const columns: ColumnDef<VendorFormStatus>[] = React.useMemo(() => [ + { + accessorKey: "vendorName", + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="벤더명" />, + cell: ({ row }) => ( + <div className="font-medium">{row.original.vendorName}</div> + ), + size: 200, + enablePinning: true, + }, + { + accessorKey: "formCount", + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="Form 개수" />, + cell: ({ row }) => ( + <div className="text-center"> + <Badge variant="outline">{row.original.formCount}</Badge> + </div> + ), + size: 100, + }, + { + accessorKey: "tagCount", + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="Tag 개수" />, + cell: ({ row }) => ( + <div className="text-center"> + <Badge variant="outline">{row.original.tagCount}</Badge> + </div> + ), + size: 100, + }, + { + accessorKey: "totalFields", + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="전체 필드" />, + cell: ({ row }) => ( + <div className="text-center font-mono"> + {row.original.totalFields.toLocaleString()} + </div> + ), + size: 100, + }, + { + accessorKey: "completedFields", + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="완료 필드" />, + cell: ({ row }) => ( + <div className="text-center font-mono"> + {row.original.completedFields.toLocaleString()} + </div> + ), + size: 100, + }, + { + accessorKey: "completionRate", + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="완료율" />, + cell: ({ row }) => { + const rate = row.original.completionRate; + return ( + <div className="flex items-center gap-2"> + <Progress + value={rate} + className="w-[80px]" + /> + <Badge + variant={getCompletionBadgeVariant(rate)} + className={cn("min-w-[60px] justify-center", getCompletionColor(rate))} + > + {rate.toFixed(1)}% + </Badge> + </div> + ); + }, + size: 180, + }, + { + id: "progress", + header: "진행 상태", + cell: ({ row }) => { + const { completedFields, totalFields } = row.original; + return ( + <div className="text-sm text-muted-foreground"> + {completedFields} / {totalFields} + </div> + ); + }, + size: 120, + }, + ], []); + + // 필터 필드 정의 + const advancedFilterFields: DataTableAdvancedFilterField<VendorFormStatus>[] = [ + { id: "vendorName", label: "벤더명", type: "text" }, + { id: "formCount", label: "Form 개수", type: "number" }, + { id: "tagCount", label: "Tag 개수", type: "number" }, + { id: "completionRate", label: "완료율", type: "number" }, + ]; + + // 요약 통계 + const summaryStats = React.useMemo(() => { + const totalVendors = data.length; + const totalForms = data.reduce((sum, v) => sum + v.formCount, 0); + const totalTags = data.reduce((sum, v) => sum + v.tagCount, 0); + const avgCompletionRate = data.length > 0 + ? data.reduce((sum, v) => sum + v.completionRate, 0) / data.length + : 0; + const fullCompletedVendors = data.filter(v => v.completionRate === 100).length; + + return { + totalVendors, + totalForms, + totalTags, + avgCompletionRate, + fullCompletedVendors, + }; + }, [data]); + + return ( + <div className={cn("w-full space-y-4")}> + {/* 요약 카드 */} + <div className="grid gap-4 md:grid-cols-5"> + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">전체 벤더</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{summaryStats.totalVendors}</div> + </CardContent> + </Card> + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">전체 Form</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{summaryStats.totalForms.toLocaleString()}</div> + </CardContent> + </Card> + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">전체 Tag</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{summaryStats.totalTags.toLocaleString()}</div> + </CardContent> + </Card> + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">평균 완료율</CardTitle> + </CardHeader> + <CardContent> + <div className={cn("text-2xl font-bold", getCompletionColor(summaryStats.avgCompletionRate))}> + {summaryStats.avgCompletionRate.toFixed(1)}% + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">완료 벤더</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {summaryStats.fullCompletedVendors} / {summaryStats.totalVendors} + </div> + </CardContent> + </Card> + </div> + + {/* 데이터 테이블 */} + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <CardTitle>벤더별 Form 입력 현황</CardTitle> + <Button + variant="outline" + size="sm" + onClick={handleRefresh} + disabled={isRefreshing} + > + <RefreshCw className={cn("h-4 w-4 mr-2", isRefreshing && "animate-spin")} /> + 새로고침 + </Button> + </div> + </CardHeader> + <CardContent> + <ClientDataTable + columns={columns} + data={data} + advancedFilterFields={advancedFilterFields} + autoSizeColumns={true} + compact={true} + maxHeight="34rem" + initialColumnPinning={{ + left: ["vendorName"], + }} + defaultSorting={[ + { id: "completionRate", desc: false }, // 완료율 낮은 순으로 정렬 + ]} + /> + </CardContent> + </Card> + </div> + ); +}
\ No newline at end of file |
