From ffb8e2e99e1d0c105b1c545ff7ab4d3149ec6c48 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Mon, 1 Dec 2025 15:22:38 +0900
Subject: (김준회) 서버측 where, order by 절 지원을 위한 v2 임시작업
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../client-table-v2/client-table-save-view.tsx     | 185 +++++++++++++++++++++
 1 file changed, 185 insertions(+)
 create mode 100644 components/client-table-v2/client-table-save-view.tsx

(limited to 'components/client-table-v2/client-table-save-view.tsx')

diff --git a/components/client-table-v2/client-table-save-view.tsx b/components/client-table-v2/client-table-save-view.tsx
new file mode 100644
index 00000000..73935d00
--- /dev/null
+++ b/components/client-table-v2/client-table-save-view.tsx
@@ -0,0 +1,185 @@
+"use client";
+
+import * as React from "react";
+import { Table } from "@tanstack/react-table";
+import { useSession } from "next-auth/react";
+import { Button } from "@/components/ui/button";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuLabel,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Bookmark, Save, Trash2 } from "lucide-react";
+import {
+  getUserCustomSettings,
+  saveUserCustomSetting,
+  deleteUserCustomSetting,
+} from "@/actions/user-custom-data";
+import { toast } from "sonner"; 
+
+interface ClientTableSaveViewProps<TData> {
+  table: Table<TData>;
+  tableKey: string;
+}
+
+export function ClientTableSaveView<TData>({
+  table,
+  tableKey,
+}: ClientTableSaveViewProps<TData>) {
+  const { data: session } = useSession();
+  const [savedViews, setSavedViews] = React.useState<{ id: string; customSettingName: string; customSetting: Record<string, any> }[]>([]);
+  const [isSaveDialogOpen, setIsSaveDialogOpen] = React.useState(false);
+  const [newViewName, setNewViewName] = React.useState("");
+  const [isLoading, setIsLoading] = React.useState(false);
+
+  const fetchSettings = React.useCallback(async () => {
+    const userIdVal = session?.user?.id;
+    if (!userIdVal) return;
+    
+    const userId = Number(userIdVal); 
+    if (isNaN(userId)) return;
+
+    const res = await getUserCustomSettings(tableKey, userId);
+    if (res.success && res.data) {
+      // @ts-ignore - data from DB might need casting
+      setSavedViews(res.data);
+    }
+  }, [session, tableKey]);
+
+  React.useEffect(() => {
+    if (session) {
+      fetchSettings();
+    }
+  }, [fetchSettings, session]);
+
+  const handleSaveView = async () => {
+    const userIdVal = session?.user?.id;
+    if (!newViewName.trim() || !userIdVal) return;
+    const userId = Number(userIdVal);
+    if (isNaN(userId)) return;
+
+    setIsLoading(true);
+    const state = table.getState();
+    const settingToSave = {
+      sorting: state.sorting,
+      columnFilters: state.columnFilters,
+      globalFilter: state.globalFilter,
+      columnVisibility: state.columnVisibility,
+      columnPinning: state.columnPinning,
+      columnOrder: state.columnOrder,
+      grouping: state.grouping,
+      pagination: { pageSize: state.pagination.pageSize }, 
+    };
+
+    const res = await saveUserCustomSetting(userId, tableKey, newViewName, settingToSave);
+    setIsLoading(false);
+
+    if (res.success) {
+      toast.success("View saved successfully");
+      setIsSaveDialogOpen(false);
+      setNewViewName("");
+      fetchSettings();
+    } else {
+      toast.error("Failed to save view");
+    }
+  };
+
+  const handleLoadView = (setting: { customSetting: Record<string, any> | unknown; customSettingName: string }) => {
+    const s = setting.customSetting as Record<string, any>;
+    if (!s) return;
+
+    if (s.sorting) table.setSorting(s.sorting);
+    if (s.columnFilters) table.setColumnFilters(s.columnFilters);
+    if (s.globalFilter !== undefined) table.setGlobalFilter(s.globalFilter);
+    if (s.columnVisibility) table.setColumnVisibility(s.columnVisibility);
+    if (s.columnPinning) table.setColumnPinning(s.columnPinning);
+    if (s.columnOrder) table.setColumnOrder(s.columnOrder);
+    if (s.grouping) table.setGrouping(s.grouping);
+    if (s.pagination?.pageSize) table.setPageSize(s.pagination.pageSize);
+    
+    toast.success(`View "${setting.customSettingName}" loaded`);
+  };
+
+  const handleDeleteView = async (e: React.MouseEvent, id: string) => {
+    e.stopPropagation();
+    if (!confirm("Are you sure you want to delete this view?")) return;
+
+    const res = await deleteUserCustomSetting(id);
+    if (res.success) {
+      toast.success("View deleted");
+      fetchSettings();
+    } else {
+      toast.error("Failed to delete view");
+    }
+  };
+
+  if (!session) return null;
+
+  return (
+    <>
+        <DropdownMenu>
+          <DropdownMenuTrigger asChild>
+            <Button variant="outline" size="sm" className="ml-2 hidden h-8 lg:flex">
+              <Bookmark className="mr-2 h-4 w-4" />
+              Views
+            </Button>
+          </DropdownMenuTrigger>
+          <DropdownMenuContent align="end" className="w-[200px]">
+            <DropdownMenuLabel>Saved Views</DropdownMenuLabel>
+            <DropdownMenuSeparator />
+            {savedViews.length === 0 ? (
+                <div className="p-2 text-sm text-muted-foreground text-center">No saved views</div>
+            ) : (
+                savedViews.map((view) => (
+                    <DropdownMenuItem key={view.id} onClick={() => handleLoadView(view)} className="flex justify-between cursor-pointer">
+                        <span className="truncate flex-1">{view.customSettingName}</span>
+                        <Button variant="ghost" size="icon" className="h-4 w-4" onClick={(e) => handleDeleteView(e, view.id)}>
+                            <Trash2 className="h-3 w-3 text-destructive" />
+                        </Button>
+                    </DropdownMenuItem>
+                ))
+            )}
+            <DropdownMenuSeparator />
+            <DropdownMenuItem onClick={() => setIsSaveDialogOpen(true)} className="cursor-pointer">
+                <Save className="mr-2 h-4 w-4" />
+                Save Current View
+            </DropdownMenuItem>
+          </DropdownMenuContent>
+        </DropdownMenu>
+
+        <Dialog open={isSaveDialogOpen} onOpenChange={setIsSaveDialogOpen}>
+            <DialogContent>
+                <DialogHeader>
+                    <DialogTitle>Save View</DialogTitle>
+                    <DialogDescription>
+                        Save the current table configuration as a preset.
+                    </DialogDescription>
+                </DialogHeader>
+                <div className="grid gap-4 py-4">
+                    <Input 
+                        placeholder="View Name" 
+                        value={newViewName} 
+                        onChange={(e) => setNewViewName(e.target.value)}
+                    />
+                </div>
+                <DialogFooter>
+                    <Button variant="outline" onClick={() => setIsSaveDialogOpen(false)}>Cancel</Button>
+                    <Button onClick={handleSaveView} disabled={isLoading}>Save</Button>
+                </DialogFooter>
+            </DialogContent>
+        </Dialog>
+    </>
+  );
+}
-- 
cgit v1.2.3

