From 4ee8b24cfadf47452807fa2af801385ed60ab47c Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 15 Sep 2025 14:41:01 +0000 Subject: (대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfq-last/table/rfq-seal-toggle-cell.tsx | 93 ++++++++++ lib/rfq-last/table/rfq-table-columns.tsx | 73 ++++---- lib/rfq-last/table/rfq-table-toolbar-actions.tsx | 222 +++++++++++++++++++---- 3 files changed, 314 insertions(+), 74 deletions(-) create mode 100644 lib/rfq-last/table/rfq-seal-toggle-cell.tsx (limited to 'lib/rfq-last/table') 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 ( + + + + + + +

{currentSealed ? "밀봉 해제하기" : "밀봉하기"}

+
+
+
+ ); +} + +export const sealColumn = { + accessorKey: "rfqSealedYn", + header: ({ column }) => , + cell: ({ row, table }) => ( + { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 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; @@ -120,18 +121,18 @@ export function getRfqColumns({ { accessorKey: "rfqSealedYn", header: ({ column }) => , - cell: ({ row }) => { - const isSealed = row.original.rfqSealedYn; - return ( -
- {isSealed ? ( - - ) : ( - - )} -
- ); - }, + cell: ({ row, table }) => ( + { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 refreshData 함수를 prop으로 전달받아 사용 + const meta = table.options.meta as any; + meta?.refreshData?.(); + }} + /> + ), size: 80, }, @@ -453,18 +454,18 @@ export function getRfqColumns({ { accessorKey: "rfqSealedYn", header: ({ column }) => , - cell: ({ row }) => { - const isSealed = row.original.rfqSealedYn; - return ( -
- {isSealed ? ( - - ) : ( - - )} -
- ); - }, + cell: ({ row, table }) => ( + { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 refreshData 함수를 prop으로 전달받아 사용 + const meta = table.options.meta as any; + meta?.refreshData?.(); + }} + /> + ), size: 80, }, @@ -815,18 +816,18 @@ export function getRfqColumns({ { accessorKey: "rfqSealedYn", header: ({ column }) => , - cell: ({ row }) => { - const isSealed = row.original.rfqSealedYn; - return ( -
- {isSealed ? ( - - ) : ( - - )} -
- ); - }, + cell: ({ row, table }) => ( + { + // 테이블 데이터를 새로고침하는 로직 + // 이 부분은 상위 컴포넌트에서 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; @@ -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 ( -
- {onRefresh && ( - - )} - - - + <> +
+ {onRefresh && ( - - - - 전체 데이터 내보내기 - - - 선택한 항목 내보내기 ({table.getFilteredSelectedRowModel().rows.length}개) - - - - - {/* rfqCategory가 'general'일 때만 일반견적 생성 다이얼로그 표시 */} - {rfqCategory === "general" && ( - - ) } -
+ )} + + {/* 견적 밀봉/해제 버튼 */} + {selectedRfqIds.length > 0 && ( + + + + + + handleSealAction("seal")} + disabled={unsealedCount === 0} + > + + 선택 항목 밀봉 ({unsealedCount}개) + + handleSealAction("unseal")} + disabled={sealedCount === 0} + > + + 선택 항목 밀봉 해제 ({sealedCount}개) + + +
+ 전체 {selectedRfqIds.length}개 선택됨 +
+
+
+ )} + + + + + + + + 전체 데이터 내보내기 + + + 선택한 항목 내보내기 ({table.getFilteredSelectedRowModel().rows.length}개) + + + + + {/* rfqCategory가 'general'일 때만 일반견적 생성 다이얼로그 표시 */} + {rfqCategory === "general" && ( + + )} +
+ + {/* 밀봉 확인 다이얼로그 */} + + + + + {sealAction === "seal" ? "견적 밀봉 확인" : "견적 밀봉 해제 확인"} + + + {sealAction === "seal" + ? `선택한 ${unsealedCount}개의 견적을 밀봉하시겠습니까? 밀봉된 견적은 업체에서 수정할 수 없습니다.` + : `선택한 ${sealedCount}개의 견적 밀봉을 해제하시겠습니까? 밀봉이 해제되면 업체에서 견적을 수정할 수 있습니다.`} + + + + 취소 + + {isSealing ? "처리 중..." : "확인"} + + + + + ); +} + +// 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 -- cgit v1.2.3