summaryrefslogtreecommitdiff
path: root/lib/rfq-last/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 14:41:01 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 14:41:01 +0000
commit4ee8b24cfadf47452807fa2af801385ed60ab47c (patch)
treee1d1fb029f0cf5519c517494bf9a545505c35700 /lib/rfq-last/table
parent265859d691a01cdcaaf9154f93c38765bc34df06 (diff)
(대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth
Diffstat (limited to 'lib/rfq-last/table')
-rw-r--r--lib/rfq-last/table/rfq-seal-toggle-cell.tsx93
-rw-r--r--lib/rfq-last/table/rfq-table-columns.tsx73
-rw-r--r--lib/rfq-last/table/rfq-table-toolbar-actions.tsx222
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