summaryrefslogtreecommitdiff
path: root/lib/basic-contract/vendor-table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/vendor-table')
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-columns.tsx214
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx318
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-table.tsx94
-rw-r--r--lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx56
4 files changed, 682 insertions, 0 deletions
diff --git a/lib/basic-contract/vendor-table/basic-contract-columns.tsx b/lib/basic-contract/vendor-table/basic-contract-columns.tsx
new file mode 100644
index 00000000..b79487d7
--- /dev/null
+++ b/lib/basic-contract/vendor-table/basic-contract-columns.tsx
@@ -0,0 +1,214 @@
+"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Paperclip } from "lucide-react"
+import { toast } from "sonner"
+
+import { getErrorMessage } from "@/lib/handle-error"
+import { formatDate, formatDateTime } from "@/lib/utils"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { basicContractColumnsConfig, basicContractVendorColumnsConfig } from "@/config/basicContractColumnsConfig"
+import { BasicContractView } from "@/db/schema"
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BasicContractView> | null>>
+}
+
+/**
+ * 파일 다운로드 함수
+ */
+/**
+ * 파일 다운로드 함수
+ */
+const handleFileDownload = (filePath: string | null, fileName: string | null) => {
+ if (!filePath || !fileName) {
+ toast.error("파일 정보가 없습니다.");
+ return;
+ }
+
+ try {
+ // 전체 URL 생성
+ const fullUrl = `${window.location.origin}${filePath}`;
+
+ // a 태그를 생성하여 다운로드 실행
+ const link = document.createElement('a');
+ link.href = fullUrl;
+ link.download = fileName; // 다운로드될 파일명 설정
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ toast.success("파일 다운로드를 시작합니다.");
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error);
+ toast.error("파일 다운로드 중 오류가 발생했습니다.");
+ }
+};
+
+/**
+ * tanstack table 컬럼 정의 (중첩 헤더 버전)
+ */
+export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicContractView>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<BasicContractView> = {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ maxSize: 30,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) 파일 다운로드 컬럼 (아이콘)
+ // ----------------------------------------------------------------
+ const downloadColumn: ColumnDef<BasicContractView> = {
+ id: "download",
+ header: "",
+ cell: ({ row }) => {
+ const template = row.original;
+ const filePath = template.status === "PENDING" ? template.filePath : template.signedFilePath
+
+ return (
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={() => handleFileDownload(filePath, template.fileName)}
+ title={`${template.fileName} 다운로드`}
+ className="hover:bg-muted"
+ >
+ <Paperclip className="h-4 w-4" />
+ <span className="sr-only">다운로드</span>
+ </Button>
+ );
+ },
+ maxSize: 30,
+ enableSorting: false,
+ }
+
+
+ // ----------------------------------------------------------------
+ // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
+ // ----------------------------------------------------------------
+ // 4-1) groupMap: { [groupName]: ColumnDef<BasicContractView>[] }
+ const groupMap: Record<string, ColumnDef<BasicContractView>[]> = {}
+
+ basicContractVendorColumnsConfig.forEach((cfg) => {
+ // 만약 group가 없으면 "_noGroup" 처리
+ const groupName = cfg.group || "_noGroup"
+
+ if (!groupMap[groupName]) {
+ groupMap[groupName] = []
+ }
+
+ // child column 정의
+ const childCol: ColumnDef<BasicContractView> = {
+ accessorKey: cfg.id,
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={cfg.label} />
+ ),
+ meta: {
+ excelHeader: cfg.excelHeader,
+ group: cfg.group,
+ type: cfg.type,
+ },
+ cell: ({ row, cell }) => {
+ // 날짜 형식 처리
+ if (cfg.id === "createdAt" || cfg.id === "updatedAt" || cfg.id === "completedAt") {
+ const dateVal = cell.getValue() as Date
+ return formatDateTime(dateVal)
+ }
+
+ // Status 컬럼에 Badge 적용
+ if (cfg.id === "status") {
+ const status = row.getValue(cfg.id) as string
+ const isPending = status === "PENDING"
+
+ return (
+ <Badge
+ variant={!isPending ? "default" : "secondary"}
+ >
+ {status}
+ </Badge>
+ )
+ }
+
+ // 나머지 컬럼은 그대로 값 표시
+ return row.getValue(cfg.id) ?? ""
+ },
+ minSize: 80,
+
+ }
+
+ groupMap[groupName].push(childCol)
+ })
+
+ // ----------------------------------------------------------------
+ // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기
+ // ----------------------------------------------------------------
+ const nestedColumns: ColumnDef<BasicContractView>[] = []
+
+ // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함
+ // 여기서는 그냥 Object.entries 순서
+ Object.entries(groupMap).forEach(([groupName, colDefs]) => {
+ if (groupName === "_noGroup") {
+ // 그룹 없음 → 그냥 최상위 레벨 컬럼
+ nestedColumns.push(...colDefs)
+ } else {
+ // 상위 컬럼
+ nestedColumns.push({
+ id: groupName,
+ header: groupName, // "Basic Info", "Metadata" 등
+ columns: colDefs,
+ })
+ }
+ })
+
+ // ----------------------------------------------------------------
+ // 5) 최종 컬럼 배열: select, download, nestedColumns, actions
+ // ----------------------------------------------------------------
+ return [
+ selectColumn,
+ downloadColumn, // 다운로드 컬럼 추가
+ ...nestedColumns,
+ ]
+} \ No newline at end of file
diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
new file mode 100644
index 00000000..28a4fd71
--- /dev/null
+++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
@@ -0,0 +1,318 @@
+"use client";
+
+import * as React from "react";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { formatDate } from "@/lib/utils";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+import { BasicContractSignViewer } from "@/lib/basic-contract/viewer/basic-contract-sign-viewer";
+import type { WebViewerInstance } from "@pdftron/webviewer";
+import type { BasicContractView } from "@/db/schema";
+import {
+ Upload,
+ FileSignature,
+ CheckCircle2,
+ Search,
+ Clock,
+ FileText,
+ User,
+ AlertCircle,
+ Calendar
+} from "lucide-react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
+import { useRouter } from "next/navigation"
+
+// 수정된 props 인터페이스
+interface BasicContractSignDialogProps {
+ contracts: BasicContractView[];
+ onSuccess?: () => void;
+}
+
+export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractSignDialogProps) {
+ const [open, setOpen] = React.useState(false);
+ const [selectedContract, setSelectedContract] = React.useState<BasicContractView | null>(null);
+ const [instance, setInstance] = React.useState<null | WebViewerInstance>(null);
+ const [searchTerm, setSearchTerm] = React.useState("");
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const router = useRouter()
+
+ // 다이얼로그 열기/닫기 핸들러
+ const handleOpenChange = (isOpen: boolean) => {
+ setOpen(isOpen);
+
+ // 다이얼로그가 열릴 때 첫 번째 계약서 자동 선택
+ if (isOpen && contracts.length > 0 && !selectedContract) {
+ setSelectedContract(contracts[0]);
+ }
+
+ if (!isOpen) {
+ setSelectedContract(null);
+ setSearchTerm("");
+ }
+ };
+
+ // 계약서 선택 핸들러
+ const handleSelectContract = (contract: BasicContractView) => {
+ setSelectedContract(contract);
+ };
+
+ // 검색된 계약서 필터링
+ const filteredContracts = React.useMemo(() => {
+ if (!searchTerm.trim()) return contracts;
+
+ const term = searchTerm.toLowerCase();
+ return contracts.filter(contract =>
+ (contract.templateName || '').toLowerCase().includes(term) ||
+ (contract.userName || '').toLowerCase().includes(term)
+ );
+ }, [contracts, searchTerm]);
+
+ // 다이얼로그가 열릴 때 첫 번째 계약서 자동 선택
+ React.useEffect(() => {
+ if (open && contracts.length > 0 && !selectedContract) {
+ setSelectedContract(contracts[0]);
+ }
+ }, [open, contracts, selectedContract]);
+
+ // 서명 완료 핸들러
+ const completeSign = async () => {
+ if (!instance || !selectedContract) return;
+
+ setIsSubmitting(true);
+ try {
+ const { documentViewer, annotationManager } = instance.Core;
+ const doc = documentViewer.getDocument();
+ const xfdfString = await annotationManager.exportAnnotations();
+
+ const data = await doc.getFileData({
+ xfdfString,
+ downloadType: "pdf",
+ });
+
+ // FormData 생성 및 파일 추가
+ const formData = new FormData();
+ formData.append('file', new Blob([data], { type: 'application/pdf' }));
+ formData.append('tableRowId', selectedContract.id.toString());
+ formData.append('templateName', selectedContract.fileName || '');
+
+ // API 호출
+ const response = await fetch('/api/upload/signed-contract', {
+ method: 'POST',
+ body: formData,
+ next: { tags: ["basicContractView-vendor"] },
+ });
+
+ const result = await response.json();
+
+ if (result.result) {
+ toast.success("서명이 성공적으로 완료되었습니다.", {
+ description: "문서가 성공적으로 처리되었습니다.",
+ icon: <CheckCircle2 className="h-5 w-5 text-green-500" />
+ });
+ router.refresh();
+ setOpen(false);
+ if (onSuccess) {
+ onSuccess();
+ }
+ } else {
+ toast.error("서명 처리 중 오류가 발생했습니다.", {
+ description: result.error,
+ icon: <AlertCircle className="h-5 w-5 text-red-500" />
+ });
+ }
+ } catch (error) {
+ console.error("서명 완료 중 오류:", error);
+ toast.error("서명 처리 중 오류가 발생했습니다.");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // 서명 대기중(PENDING) 계약서가 있는지 확인
+ const hasPendingContracts = contracts.length > 0;
+
+ return (
+ <>
+ {/* 서명 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setOpen(true)}
+ disabled={!hasPendingContracts}
+ className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200"
+ >
+ <Upload className="size-4 text-blue-500" aria-hidden="true" />
+ <span className="hidden sm:inline flex items-center">
+ 서명하기
+ {contracts.length > 0 && (
+ <Badge variant="secondary" className="ml-2 bg-blue-100 text-blue-700 hover:bg-blue-200">
+ {contracts.length}
+ </Badge>
+ )}
+ </span>
+ </Button>
+
+ {/* 서명 다이얼로그 - 고정 높이 유지 */}
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogContent className="max-w-5xl h-[650px] w-[90vw] p-0 overflow-hidden rounded-lg shadow-lg border border-gray-200">
+ <DialogHeader className="p-6 bg-gradient-to-r from-blue-50 to-purple-50 border-b">
+ <DialogTitle className="text-xl font-bold flex items-center text-gray-800">
+ <FileSignature className="mr-2 h-5 w-5 text-blue-500" />
+ 기본계약서 및 관련문서 서명
+ </DialogTitle>
+ </DialogHeader>
+
+ <div className="grid grid-cols-2 h-[calc(100%-4rem)] overflow-hidden">
+ {/* 왼쪽 영역 - 계약서 목록 */}
+ <div className="col-span-1 border-r border-gray-200 bg-gray-50">
+ <div className="p-4 border-b">
+ <div className="relative mb-10">
+ <div className="absolute inset-y-0 left-3.5 flex items-center pointer-events-none">
+ <Search className="h-4 w-8 text-gray-400" />
+ </div>
+ <Input
+ placeholder="문서명 또는 요청자 검색"
+ className="bg-white"
+ style={{paddingLeft:25}}
+ value={searchTerm}
+ onChange={(e) => setSearchTerm(e.target.value)}
+ />
+ </div>
+ <Tabs defaultValue="all" className="w-full">
+ <TabsList className="w-full">
+ <TabsTrigger value="all" className="flex-1">전체 ({contracts.length})</TabsTrigger>
+ <TabsTrigger value="contracts" className="flex-1">계약서</TabsTrigger>
+ <TabsTrigger value="docs" className="flex-1">관련문서</TabsTrigger>
+ </TabsList>
+ </Tabs>
+ </div>
+
+ <ScrollArea className="h-[calc(100%-6rem)]">
+ <div className="p-3">
+ {filteredContracts.length === 0 ? (
+ <div className="flex flex-col items-center justify-center h-40 text-center">
+ <FileText className="h-12 w-12 text-gray-300 mb-2" />
+ <p className="text-gray-500 font-medium">서명 요청된 문서가 없습니다.</p>
+ <p className="text-gray-400 text-sm mt-1">나중에 다시 확인해주세요.</p>
+ </div>
+ ) : (
+ <div className="space-y-2">
+ {filteredContracts.map((contract) => (
+ <Button
+ key={contract.id}
+ variant="outline"
+ className={cn(
+ "w-full justify-start text-left h-auto p-3 bg-white hover:bg-blue-50 transition-colors",
+ "border border-gray-200 hover:border-blue-200 rounded-md",
+ selectedContract?.id === contract.id && "border-blue-500 bg-blue-50 shadow-sm"
+ )}
+ onClick={() => handleSelectContract(contract)}
+ >
+ <div className="flex flex-col w-full">
+ <div className="flex items-center justify-between w-full">
+ <span className="font-semibold truncate text-gray-800 flex items-center">
+ <FileText className="h-4 w-4 mr-2 text-blue-500" />
+ {contract.templateName || '문서'}
+ </span>
+ <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">
+ 대기중
+ </Badge>
+ </div>
+ <Separator className="my-2 bg-gray-100" />
+ <div className="grid grid-cols-2 gap-1 mt-1 text-xs text-gray-500">
+ <div className="flex items-center">
+ <User className="h-3 w-3 mr-1" />
+ <span className="truncate">{contract.userName || '알 수 없음'}</span>
+ </div>
+ <div className="flex items-center justify-end">
+ <Calendar className="h-3 w-3 mr-1" />
+ <span>{formatDate(contract.createdAt)}</span>
+ </div>
+ </div>
+ </div>
+ </Button>
+ ))}
+ </div>
+ )}
+ </div>
+ </ScrollArea>
+ </div>
+
+ {/* 오른쪽 영역 - 문서 뷰어 */}
+ <div className="col-span-1 bg-white flex flex-col h-full">
+ {selectedContract ? (
+ <>
+ <div className="p-3 border-b bg-gray-50">
+ <h3 className="font-semibold text-gray-800 flex items-center">
+ <FileText className="h-4 w-4 mr-2 text-blue-500" />
+ {selectedContract.templateName || '문서'}
+ </h3>
+ <div className="flex justify-between items-center mt-1 text-xs text-gray-500">
+ <span className="flex items-center">
+ <User className="h-3 w-3 mr-1" />
+ 요청자: {selectedContract.userName || '알 수 없음'}
+ </span>
+ <span className="flex items-center">
+ <Clock className="h-3 w-3 mr-1" />
+ {formatDate(selectedContract.createdAt)}
+ </span>
+ </div>
+ </div>
+ <div className="flex-grow overflow-hidden border-b">
+ <BasicContractSignViewer
+ contractId={selectedContract.id}
+ filePath={selectedContract.filePath || undefined}
+ instance={instance}
+ setInstance={setInstance}
+ />
+ </div>
+ <div className="p-3 flex justify-between items-center bg-gray-50">
+ <p className="text-sm text-gray-600">
+ <AlertCircle className="h-4 w-4 text-yellow-500 inline mr-1" />
+ 서명 후에는 변경할 수 없습니다.
+ </p>
+ <Button
+ className="gap-2 bg-blue-600 hover:bg-blue-700 transition-colors"
+ onClick={completeSign}
+ disabled={isSubmitting}
+ >
+ {isSubmitting ? (
+ <>
+ <svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+ </svg>
+ 처리 중...
+ </>
+ ) : (
+ <>
+ <FileSignature className="h-4 w-4" />
+ 서명 완료
+ </>
+ )}
+ </Button>
+ </div>
+ </>
+ ) : (
+ <div className="flex flex-col items-center justify-center h-full text-center p-6">
+ <div className="bg-blue-50 p-6 rounded-full mb-4">
+ <FileSignature className="h-12 w-12 text-blue-500" />
+ </div>
+ <h3 className="text-xl font-medium text-gray-800 mb-2">문서를 선택해주세요</h3>
+ <p className="text-gray-500 max-w-md">
+ 왼쪽 목록에서 서명할 문서를 선택하면 여기에 문서 내용이 표시됩니다.
+ </p>
+ </div>
+ )}
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ </>
+ );
+} \ No newline at end of file
diff --git a/lib/basic-contract/vendor-table/basic-contract-table.tsx b/lib/basic-contract/vendor-table/basic-contract-table.tsx
new file mode 100644
index 00000000..34e15ae3
--- /dev/null
+++ b/lib/basic-contract/vendor-table/basic-contract-table.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import * as React from "react";
+import { DataTable } from "@/components/data-table/data-table";
+import { Button } from "@/components/ui/button";
+import { Plus, Loader2 } from "lucide-react";
+import { useDataTable } from "@/hooks/use-data-table";
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+import { toast } from "sonner";
+import { getColumns } from "./basic-contract-columns";
+import { getBasicContracts, getBasicContractsByVendorId } from "../service";
+import { BasicContractView } from "@/db/schema";
+import { BasicContractTableToolbarActions } from "./basicContract-table-toolbar-actions";
+
+
+interface BasicTemplateTableProps {
+ promises: Promise<
+ [
+ Awaited<ReturnType<typeof getBasicContractsByVendorId>>,
+ ]
+ >
+}
+
+
+export function BasicContractsVendorTable({ promises }: BasicTemplateTableProps) {
+
+
+ const [rowAction, setRowAction] =
+ React.useState<DataTableRowAction<BasicContractView> | null>(null)
+
+
+ const [{ data, pageCount }] =
+ React.use(promises)
+
+ // console.log(data)
+
+ // 컬럼 설정 - 외부 파일에서 가져옴
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction }),
+ [setRowAction]
+ )
+
+ // config 기반으로 필터 필드 설정
+ const advancedFilterFields: DataTableAdvancedFilterField<BasicContractView>[] = [
+ { id: "templateName", label: "템플릿명", type: "text" },
+ {
+ id: "status", label: "상태", type: "select", options: [
+ { label: "서명대기", value: "PENDING" },
+ { label: "서명완료", value: "COMPLETED" },
+ ]
+ },
+ { id: "userName", label: "요청자", type: "text" },
+ { id: "createdAt", label: "생성일", type: "date" },
+ { id: "updatedAt", label: "수정일", type: "date" },
+ ];
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ // filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ return (
+ <>
+
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ >
+ <BasicContractTableToolbarActions table={table} />
+
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ </>
+
+ );
+} \ No newline at end of file
diff --git a/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx
new file mode 100644
index 00000000..2e5e4471
--- /dev/null
+++ b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+import * as React from "react"
+import { type Task } from "@/db/schema/tasks"
+import { type Table } from "@tanstack/react-table"
+import { Download, Upload } from "lucide-react"
+
+import { exportTableToExcel } from "@/lib/export"
+import { Button } from "@/components/ui/button"
+import { BasicContractView } from "@/db/schema"
+import { BasicContractSignDialog } from "./basic-contract-sign-dialog"
+
+interface TemplateTableToolbarActionsProps {
+ table: Table<BasicContractView>
+}
+
+export function BasicContractTableToolbarActions({ table }: TemplateTableToolbarActionsProps) {
+ // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식
+
+ const inPendingContracts = React.useMemo(() => {
+ return table
+ .getFilteredSelectedRowModel()
+ .rows
+ .map(row => row.original)
+ .filter(contract => contract.status === "PENDING");
+ }, [table.getFilteredSelectedRowModel().rows]);
+
+
+ return (
+ <div className="flex items-center gap-2">
+
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <BasicContractSignDialog
+ contracts={inPendingContracts}
+ onSuccess={() => table.toggleAllRowsSelected(false)}
+ />
+ ) : null}
+
+ {/** 4) Export 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() =>
+ exportTableToExcel(table, {
+ filename: "basci-contract-requested-list",
+ excludeColumns: ["select", "actions"],
+ })
+ }
+ className="gap-2"
+ >
+ <Download className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Export</span>
+ </Button>
+ </div>
+ )
+} \ No newline at end of file