summaryrefslogtreecommitdiff
path: root/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx')
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx318
1 files changed, 318 insertions, 0 deletions
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