diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 14:41:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 14:41:01 +0000 |
| commit | 4ee8b24cfadf47452807fa2af801385ed60ab47c (patch) | |
| tree | e1d1fb029f0cf5519c517494bf9a545505c35700 /lib/rfq-last/table | |
| parent | 265859d691a01cdcaaf9154f93c38765bc34df06 (diff) | |
(대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth
Diffstat (limited to 'lib/rfq-last/table')
| -rw-r--r-- | lib/rfq-last/table/rfq-seal-toggle-cell.tsx | 93 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table-columns.tsx | 73 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-table-toolbar-actions.tsx | 222 |
3 files changed, 314 insertions, 74 deletions
diff --git a/lib/rfq-last/table/rfq-seal-toggle-cell.tsx b/lib/rfq-last/table/rfq-seal-toggle-cell.tsx new file mode 100644 index 00000000..99360978 --- /dev/null +++ b/lib/rfq-last/table/rfq-seal-toggle-cell.tsx @@ -0,0 +1,93 @@ + +"use client"; + +import * as React from "react"; +import { Lock, LockOpen } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { toast } from "sonner"; +import { toggleRfqSealed } from "../service"; + +interface RfqSealToggleCellProps { + rfqId: number; + isSealed: boolean; + onUpdate?: () => void; +} + +export function RfqSealToggleCell({ + rfqId, + isSealed, + onUpdate +}: RfqSealToggleCellProps) { + const [isLoading, setIsLoading] = React.useState(false); + const [currentSealed, setCurrentSealed] = React.useState(isSealed); + + const handleToggle = async (e: React.MouseEvent) => { + e.stopPropagation(); // 행 선택 방지 + + setIsLoading(true); + try { + const result = await toggleRfqSealed(rfqId); + + if (result.success) { + setCurrentSealed(result.data?.rfqSealedYn ?? !currentSealed); + toast.success(result.message); + onUpdate?.(); // 테이블 데이터 새로고침 + } else { + toast.error(result.error); + } + } catch (error) { + toast.error("밀봉 상태 변경 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="sm" + className="h-8 w-8 p-0" + onClick={handleToggle} + disabled={isLoading} + > + {currentSealed ? ( + <Lock className="h-4 w-4 text-red-500" /> + ) : ( + <LockOpen className="h-4 w-4 text-gray-400" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent> + <p>{currentSealed ? "밀봉 해제하기" : "밀봉하기"}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ); +} + +export const sealColumn = { + accessorKey: "rfqSealedYn", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적 밀봉" />, + cell: ({ row, table }) => ( + <RfqSealToggleCell + rfqId={row.original.id} + isSealed={row.original.rfqSealedYn} + onUpdate={() => { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 refreshData 함수를 prop으로 전달받아 사용 + const meta = table.options.meta as any; + meta?.refreshData?.(); + }} + /> + ), + size: 80, + };
\ No newline at end of file diff --git a/lib/rfq-last/table/rfq-table-columns.tsx b/lib/rfq-last/table/rfq-table-columns.tsx index 5f5efcb4..eaf00660 100644 --- a/lib/rfq-last/table/rfq-table-columns.tsx +++ b/lib/rfq-last/table/rfq-table-columns.tsx @@ -18,6 +18,7 @@ import { DataTableRowAction } from "@/types/table"; import { format, differenceInDays } from "date-fns"; import { ko } from "date-fns/locale"; import { useRouter } from "next/navigation"; +import { RfqSealToggleCell } from "./rfq-seal-toggle-cell"; type NextRouter = ReturnType<typeof useRouter>; @@ -120,18 +121,18 @@ export function getRfqColumns({ { accessorKey: "rfqSealedYn", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적 밀봉" />, - cell: ({ row }) => { - const isSealed = row.original.rfqSealedYn; - return ( - <div className="flex justify-center"> - {isSealed ? ( - <Lock className="h-4 w-4 text-red-500" /> - ) : ( - <LockOpen className="h-4 w-4 text-gray-400" /> - )} - </div> - ); - }, + cell: ({ row, table }) => ( + <RfqSealToggleCell + rfqId={row.original.id} + isSealed={row.original.rfqSealedYn} + onUpdate={() => { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 refreshData 함수를 prop으로 전달받아 사용 + const meta = table.options.meta as any; + meta?.refreshData?.(); + }} + /> + ), size: 80, }, @@ -453,18 +454,18 @@ export function getRfqColumns({ { accessorKey: "rfqSealedYn", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적 밀봉" />, - cell: ({ row }) => { - const isSealed = row.original.rfqSealedYn; - return ( - <div className="flex justify-center"> - {isSealed ? ( - <Lock className="h-4 w-4 text-red-500" /> - ) : ( - <LockOpen className="h-4 w-4 text-gray-400" /> - )} - </div> - ); - }, + cell: ({ row, table }) => ( + <RfqSealToggleCell + rfqId={row.original.id} + isSealed={row.original.rfqSealedYn} + onUpdate={() => { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 refreshData 함수를 prop으로 전달받아 사용 + const meta = table.options.meta as any; + meta?.refreshData?.(); + }} + /> + ), size: 80, }, @@ -815,18 +816,18 @@ export function getRfqColumns({ { accessorKey: "rfqSealedYn", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="견적 밀봉" />, - cell: ({ row }) => { - const isSealed = row.original.rfqSealedYn; - return ( - <div className="flex justify-center"> - {isSealed ? ( - <Lock className="h-4 w-4 text-red-500" /> - ) : ( - <LockOpen className="h-4 w-4 text-gray-400" /> - )} - </div> - ); - }, + cell: ({ row, table }) => ( + <RfqSealToggleCell + rfqId={row.original.id} + isSealed={row.original.rfqSealedYn} + onUpdate={() => { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 refreshData 함수를 prop으로 전달받아 사용 + const meta = table.options.meta as any; + meta?.refreshData?.(); + }} + /> + ), size: 80, }, diff --git a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx index 9b696cbd..91b2798f 100644 --- a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx +++ b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { type Table } from "@tanstack/react-table"; -import { Download, RefreshCw, Plus } from "lucide-react"; +import { Download, RefreshCw, Plus, Lock, LockOpen } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -12,8 +12,20 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { toast } from "sonner"; import { RfqsLastView } from "@/db/schema"; import { CreateGeneralRfqDialog } from "./create-general-rfq-dialog"; +import { sealMultipleRfqs, unsealMultipleRfqs } from "../service"; interface RfqTableToolbarActionsProps { table: Table<RfqsLastView>; @@ -27,6 +39,43 @@ export function RfqTableToolbarActions({ rfqCategory = "itb", }: RfqTableToolbarActionsProps) { const [isExporting, setIsExporting] = React.useState(false); + const [isSealing, setIsSealing] = React.useState(false); + const [sealDialogOpen, setSealDialogOpen] = React.useState(false); + const [sealAction, setSealAction] = React.useState<"seal" | "unseal">("seal"); + + const selectedRows = table.getFilteredSelectedRowModel().rows; + const selectedRfqIds = selectedRows.map(row => row.original.id); + + // 선택된 항목들의 밀봉 상태 확인 + const sealedCount = selectedRows.filter(row => row.original.rfqSealedYn).length; + const unsealedCount = selectedRows.filter(row => !row.original.rfqSealedYn).length; + + const handleSealAction = React.useCallback(async (action: "seal" | "unseal") => { + setSealAction(action); + setSealDialogOpen(true); + }, []); + + const confirmSealAction = React.useCallback(async () => { + setIsSealing(true); + try { + const result = sealAction === "seal" + ? await sealMultipleRfqs(selectedRfqIds) + : await unsealMultipleRfqs(selectedRfqIds); + + if (result.success) { + toast.success(result.message); + table.toggleAllRowsSelected(false); // 선택 해제 + onRefresh?.(); // 데이터 새로고침 + } else { + toast.error(result.error); + } + } catch (error) { + toast.error("작업 중 오류가 발생했습니다."); + } finally { + setIsSealing(false); + setSealDialogOpen(false); + } + }, [sealAction, selectedRfqIds, table, onRefresh]); const handleExportCSV = React.useCallback(async () => { setIsExporting(true); @@ -36,6 +85,7 @@ export function RfqTableToolbarActions({ return { "RFQ 코드": original.rfqCode || "", "상태": original.status || "", + "밀봉여부": original.rfqSealedYn ? "밀봉" : "미밀봉", "프로젝트 코드": original.projectCode || "", "프로젝트명": original.projectName || "", "자재코드": original.itemCode || "", @@ -89,6 +139,7 @@ export function RfqTableToolbarActions({ return { "RFQ 코드": original.rfqCode || "", "상태": original.status || "", + "밀봉여부": original.rfqSealedYn ? "밀봉" : "미밀봉", "프로젝트 코드": original.projectCode || "", "프로젝트명": original.projectName || "", "자재코드": original.itemCode || "", @@ -115,48 +166,143 @@ export function RfqTableToolbarActions({ }, [table]); return ( - <div className="flex items-center gap-2"> - {onRefresh && ( - <Button - variant="outline" - size="sm" - onClick={onRefresh} - className="h-8 px-2 lg:px-3" - > - <RefreshCw className="mr-2 h-4 w-4" /> - 새로고침 - </Button> - )} - - <DropdownMenu> - <DropdownMenuTrigger asChild> + <> + <div className="flex items-center gap-2"> + {onRefresh && ( <Button variant="outline" size="sm" + onClick={onRefresh} className="h-8 px-2 lg:px-3" - disabled={isExporting} > - <Download className="mr-2 h-4 w-4" /> - {isExporting ? "내보내는 중..." : "내보내기"} + <RefreshCw className="mr-2 h-4 w-4" /> + 새로고침 </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem onClick={handleExportCSV}> - 전체 데이터 내보내기 - </DropdownMenuItem> - <DropdownMenuItem - onClick={handleExportSelected} - disabled={table.getFilteredSelectedRowModel().rows.length === 0} - > - 선택한 항목 내보내기 ({table.getFilteredSelectedRowModel().rows.length}개) - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - - {/* rfqCategory가 'general'일 때만 일반견적 생성 다이얼로그 표시 */} - {rfqCategory === "general" && ( - <CreateGeneralRfqDialog onSuccess={onRefresh} /> - ) } - </div> + )} + + {/* 견적 밀봉/해제 버튼 */} + {selectedRfqIds.length > 0 && ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2 lg:px-3" + disabled={isSealing} + > + <Lock className="mr-2 h-4 w-4" /> + 견적 밀봉 + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem + onClick={() => handleSealAction("seal")} + disabled={unsealedCount === 0} + > + <Lock className="mr-2 h-4 w-4" /> + 선택 항목 밀봉 ({unsealedCount}개) + </DropdownMenuItem> + <DropdownMenuItem + onClick={() => handleSealAction("unseal")} + disabled={sealedCount === 0} + > + <LockOpen className="mr-2 h-4 w-4" /> + 선택 항목 밀봉 해제 ({sealedCount}개) + </DropdownMenuItem> + <DropdownMenuSeparator /> + <div className="px-2 py-1.5 text-xs text-muted-foreground"> + 전체 {selectedRfqIds.length}개 선택됨 + </div> + </DropdownMenuContent> + </DropdownMenu> + )} + + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2 lg:px-3" + disabled={isExporting} + > + <Download className="mr-2 h-4 w-4" /> + {isExporting ? "내보내는 중..." : "내보내기"} + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={handleExportCSV}> + 전체 데이터 내보내기 + </DropdownMenuItem> + <DropdownMenuItem + onClick={handleExportSelected} + disabled={table.getFilteredSelectedRowModel().rows.length === 0} + > + 선택한 항목 내보내기 ({table.getFilteredSelectedRowModel().rows.length}개) + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + + {/* rfqCategory가 'general'일 때만 일반견적 생성 다이얼로그 표시 */} + {rfqCategory === "general" && ( + <CreateGeneralRfqDialog onSuccess={onRefresh} /> + )} + </div> + + {/* 밀봉 확인 다이얼로그 */} + <AlertDialog open={sealDialogOpen} onOpenChange={setSealDialogOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle> + {sealAction === "seal" ? "견적 밀봉 확인" : "견적 밀봉 해제 확인"} + </AlertDialogTitle> + <AlertDialogDescription> + {sealAction === "seal" + ? `선택한 ${unsealedCount}개의 견적을 밀봉하시겠습니까? 밀봉된 견적은 업체에서 수정할 수 없습니다.` + : `선택한 ${sealedCount}개의 견적 밀봉을 해제하시겠습니까? 밀봉이 해제되면 업체에서 견적을 수정할 수 있습니다.`} + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel disabled={isSealing}>취소</AlertDialogCancel> + <AlertDialogAction + onClick={confirmSealAction} + disabled={isSealing} + className={sealAction === "seal" ? "bg-red-600 hover:bg-red-700" : ""} + > + {isSealing ? "처리 중..." : "확인"} + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </> ); +} + +// CSV 내보내기 유틸리티 함수 +function exportTableToCSV({ data, filename }: { data: any[]; filename: string }) { + if (!data || data.length === 0) { + console.warn("No data to export"); + return; + } + + const headers = Object.keys(data[0]); + const csvContent = [ + headers.join(","), + ...data.map(row => + headers.map(header => { + const value = row[header]; + // 값에 쉼표, 줄바꿈, 따옴표가 있으면 따옴표로 감싸기 + if (typeof value === "string" && (value.includes(",") || value.includes("\n") || value.includes('"'))) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; + }).join(",") + ) + ].join("\n"); + + const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); + URL.revokeObjectURL(link.href); }
\ No newline at end of file |
