summaryrefslogtreecommitdiff
path: root/components/form-data-stat/form-data-stat-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/form-data-stat/form-data-stat-table.tsx')
-rw-r--r--components/form-data-stat/form-data-stat-table.tsx276
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