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.tsx313
1 files changed, 229 insertions, 84 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
index 7bffdac9..7d828a7e 100644
--- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
+++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
@@ -7,7 +7,6 @@ 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 {
@@ -19,45 +18,82 @@ import {
FileText,
User,
AlertCircle,
- Calendar
+ Calendar,
+ Loader2
} 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"
+import { BasicContractSignViewer } from "../viewer/basic-contract-sign-viewer";
+import { getVendorAttachments } from "../service";
-// 수정된 props 인터페이스
interface BasicContractSignDialogProps {
contracts: BasicContractView[];
onSuccess?: () => void;
+ hasSelectedRows?: boolean;
+ t: (key: string) => string;
}
-export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractSignDialogProps) {
+export function BasicContractSignDialog({
+ contracts,
+ onSuccess,
+ hasSelectedRows = false,
+ t
+}: 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);
+
+ // 추가된 state들
+ const [additionalFiles, setAdditionalFiles] = React.useState<any[]>([]);
+ const [isLoadingAttachments, setIsLoadingAttachments] = React.useState(false);
+
const router = useRouter()
+ console.log(selectedContract,"selectedContract")
+ console.log(additionalFiles,"additionalFiles")
+
+ // 버튼 비활성화 조건
+ const isButtonDisabled = !hasSelectedRows || contracts.length === 0;
+
+ // 비활성화 이유 텍스트
+ const getDisabledReason = () => {
+ if (!hasSelectedRows) {
+ return t("basicContracts.toolbar.selectRows");
+ }
+ if (contracts.length === 0) {
+ return t("basicContracts.toolbar.noPendingContracts");
+ }
+ return "";
+ };
+
// 다이얼로그 열기/닫기 핸들러
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen);
- // 다이얼로그가 열릴 때 첫 번째 계약서 자동 선택
- if (isOpen && contracts.length > 0 && !selectedContract) {
- setSelectedContract(contracts[0]);
- }
-
if (!isOpen) {
+ // 다이얼로그 닫을 때 상태 초기화
setSelectedContract(null);
setSearchTerm("");
+ setAdditionalFiles([]); // 추가 파일 상태 초기화
+ // WebViewer 인스턴스 정리
+ if (instance) {
+ try {
+ instance.UI.dispose();
+ } catch (error) {
+ console.log("WebViewer dispose error:", error);
+ }
+ setInstance(null);
+ }
}
};
// 계약서 선택 핸들러
const handleSelectContract = (contract: BasicContractView) => {
+ console.log("계약서 선택:", contract.id, contract.templateName);
setSelectedContract(contract);
};
@@ -79,6 +115,40 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
}
}, [open, contracts, selectedContract]);
+ // 추가 파일 가져오기 useEffect
+ React.useEffect(() => {
+ const fetchAdditionalFiles = async () => {
+ if (!selectedContract) {
+ setAdditionalFiles([]);
+ return;
+ }
+
+ // "비밀유지 계약서"인 경우에만 추가 파일 가져오기
+ if (selectedContract.templateName === "비밀유지 계약서") {
+ setIsLoadingAttachments(true);
+ try {
+ const result = await getVendorAttachments(selectedContract.vendorId);
+ if (result.success) {
+ setAdditionalFiles(result.data);
+ console.log("추가 파일 로드됨:", result.data);
+ } else {
+ console.error("Failed to fetch attachments:", result.error);
+ setAdditionalFiles([]);
+ }
+ } catch (error) {
+ console.error("Error fetching attachments:", error);
+ setAdditionalFiles([]);
+ } finally {
+ setIsLoadingAttachments(false);
+ }
+ } else {
+ setAdditionalFiles([]);
+ }
+ };
+
+ fetchAdditionalFiles();
+ }, [selectedContract]);
+
// 서명 완료 핸들러
const completeSign = async () => {
if (!instance || !selectedContract) return;
@@ -89,29 +159,57 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
const doc = documentViewer.getDocument();
const xfdfString = await annotationManager.exportAnnotations();
+ // 폼 필드 데이터 수집
+ const fieldManager = annotationManager.getFieldManager();
+ const fields = fieldManager.getFields();
+ const formData: any = {};
+ fields.forEach((field: any) => {
+ formData[field.name] = field.value;
+ });
+
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.signedFileName || '');
+ const submitFormData = new FormData();
+ submitFormData.append('file', new Blob([data], { type: 'application/pdf' }));
+ submitFormData.append('tableRowId', selectedContract.id.toString());
+ submitFormData.append('templateName', selectedContract.signedFileName || '');
+
+ // 폼 필드 데이터 추가
+ if (Object.keys(formData).length > 0) {
+ submitFormData.append('formData', JSON.stringify(formData));
+ }
+
+ // 준법 템플릿인 경우 필수 필드 검증
+ if (selectedContract.templateName?.includes('준법')) {
+ const requiredFields = ['compliance_agreement', 'legal_review', 'risk_assessment'];
+ const missingFields = requiredFields.filter(field => !formData[field]);
+
+ if (missingFields.length > 0) {
+ toast.error("필수 준법 항목이 누락되었습니다.", {
+ description: `다음 항목을 완료해주세요: ${missingFields.join(', ')}`,
+ icon: <AlertCircle className="h-5 w-5 text-red-500" />
+ });
+ setIsSubmitting(false);
+ return;
+ }
+ }
// API 호출
const response = await fetch('/api/upload/signed-contract', {
method: 'POST',
- body: formData,
+ body: submitFormData,
next: { tags: ["basicContractView-vendor"] },
});
const result = await response.json();
if (result.result) {
- toast.success("서명이 성공적으로 완료되었습니다.", {
- description: "문서가 성공적으로 처리되었습니다.",
+ toast.success(t("basicContracts.messages.signSuccess"), {
+ description: t("basicContracts.messages.documentProcessed"),
icon: <CheckCircle2 className="h-5 w-5 text-green-500" />
});
router.refresh();
@@ -120,22 +218,19 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
onSuccess();
}
} else {
- toast.error("서명 처리 중 오류가 발생했습니다.", {
+ toast.error(t("basicContracts.messages.signError"), {
description: result.error,
icon: <AlertCircle className="h-5 w-5 text-red-500" />
});
}
} catch (error) {
console.error("서명 완료 중 오류:", error);
- toast.error("서명 처리 중 오류가 발생했습니다.");
+ toast.error(t("basicContracts.messages.signError"));
} finally {
setIsSubmitting(false);
}
};
- // 서명 대기중(PENDING) 계약서가 있는지 확인
- const hasPendingContracts = contracts.length > 0;
-
return (
<>
{/* 서명 버튼 */}
@@ -143,62 +238,67 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
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"
+ disabled={isButtonDisabled}
+ className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
- <Upload className="size-4 text-blue-500" aria-hidden="true" />
+ <Upload
+ className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`}
+ aria-hidden="true"
+ />
<span className="hidden sm:inline flex items-center">
- 서명하기
- {contracts.length > 0 && (
+ {t("basicContracts.toolbar.sign")}
+ {contracts.length > 0 && !isButtonDisabled && (
<Badge variant="secondary" className="ml-2 bg-blue-100 text-blue-700 hover:bg-blue-200">
{contracts.length}
</Badge>
)}
+ {isButtonDisabled && (
+ <span className="ml-2 text-xs text-gray-400">
+ ({getDisabledReason()})
+ </span>
+ )}
</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">
+ <DialogContent className="max-w-7xl w-[95vw] h-[90vh] p-0 flex flex-col overflow-hidden" style={{width:'95vw', maxWidth:'95vw'}}>
+ {/* 고정 헤더 */}
+ <DialogHeader className="px-6 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b flex-shrink-0">
<DialogTitle className="text-xl font-bold flex items-center text-gray-800">
<FileSignature className="mr-2 h-5 w-5 text-blue-500" />
- 기본계약서 및 관련문서 서명
+ {t("basicContracts.dialog.title")}
+ {/* 추가 파일 로딩 표시 */}
+ {isLoadingAttachments && (
+ <Loader2 className="ml-2 h-4 w-4 animate-spin 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" />
+ {/* 메인 컨텐츠 영역 - Flexbox 사용 */}
+ <div className="flex flex-1 min-h-0 overflow-hidden">
+ {/* 왼쪽 영역 - 계약서 목록 (고정 너비) */}
+ <div className="w-80 border-r border-gray-200 bg-gray-50 flex flex-col flex-shrink-0">
+ <div className="p-3 border-b flex-shrink-0">
+ <div className="relative">
+ <div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
+ <Search className="h-4 w-4 text-gray-400" />
</div>
<Input
- placeholder="문서명 또는 요청자 검색"
- className="bg-white"
- style={{paddingLeft:25}}
+ placeholder={t("basicContracts.dialog.searchPlaceholder")}
+ className="bg-white pl-8 text-sm"
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">
+ <ScrollArea className="flex-1">
+ <div className="p-2">
{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 className="flex flex-col items-center justify-center h-32 text-center">
+ <FileText className="h-8 w-8 text-gray-300 mb-2" />
+ <p className="text-gray-500 text-sm font-medium">{t("basicContracts.dialog.noDocuments")}</p>
</div>
) : (
<div className="space-y-2">
@@ -207,30 +307,38 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
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",
+ "w-full justify-start text-left h-auto p-2 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 flex-col w-full space-y-1">
+ {/* 첫 번째 줄: 제목 + 상태 */}
<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 className="font-medium text-xs truncate text-gray-800 flex items-center min-w-0">
+ <FileText className="h-3 w-3 mr-1 text-blue-500 flex-shrink-0" />
+ <span className="truncate">{contract.templateName || t("basicContracts.dialog.document")}</span>
+ {/* 비밀유지 계약서인 경우 표시 */}
+ {contract.templateName === "비밀유지 계약서" && (
+ <Badge variant="outline" className="ml-1 bg-green-50 text-green-700 border-green-200 text-xs">
+ NDA
+ </Badge>
+ )}
</span>
- <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">
- 대기중
+ <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200 text-xs ml-2 flex-shrink-0">
+ {t("basicContracts.statusValues.PENDING")}
</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.requestedByName || '알 수 없음'}</span>
+
+ {/* 두 번째 줄: 사용자 + 날짜 */}
+ <div className="flex items-center justify-between text-xs text-gray-500">
+ <div className="flex items-center min-w-0">
+ <User className="h-3 w-3 mr-1 flex-shrink-0" />
+ <span className="truncate">{contract.requestedByName || t("basicContracts.dialog.unknown")}</span>
</div>
- <div className="flex items-center justify-end">
- <Calendar className="h-3 w-3 mr-1" />
+ <div className="flex items-center ml-2 flex-shrink-0">
+ <Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
<span>{formatDate(contract.createdAt)}</span>
</div>
</div>
@@ -243,19 +351,32 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
</ScrollArea>
</div>
- {/* 오른쪽 영역 - 문서 뷰어 */}
- <div className="col-span-1 bg-white flex flex-col h-full">
+ {/* 오른쪽 영역 - 문서 뷰어 (확장 가능) */}
+ <div className="flex-1 bg-white flex flex-col min-w-0">
{selectedContract ? (
<>
- <div className="p-3 border-b bg-gray-50">
+ {/* 뷰어 헤더 */}
+ <div className="p-4 border-b bg-gray-50 flex-shrink-0">
<h3 className="font-semibold text-gray-800 flex items-center">
<FileText className="h-4 w-4 mr-2 text-blue-500" />
- {selectedContract.templateName || '문서'}
+ {selectedContract.templateName || t("basicContracts.dialog.document")}
+ {/* 준법 템플릿 표시 */}
+ {selectedContract.templateName?.includes('준법') && (
+ <Badge variant="outline" className="ml-2 bg-amber-50 text-amber-700 border-amber-200">
+ 준법 서류
+ </Badge>
+ )}
+ {/* 비밀유지 계약서인 경우 추가 파일 수 표시 */}
+ {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && (
+ <Badge variant="outline" className="ml-2 bg-blue-50 text-blue-700 border-blue-200">
+ 첨부파일 {additionalFiles.length}개
+ </Badge>
+ )}
</h3>
- <div className="flex justify-between items-center mt-1 text-xs text-gray-500">
+ <div className="flex justify-between items-center mt-2 text-sm text-gray-500">
<span className="flex items-center">
<User className="h-3 w-3 mr-1" />
- 요청자: {selectedContract.requestedByName || '알 수 없음'}
+ {t("basicContracts.dialog.requester")}: {selectedContract.requestedByName || t("basicContracts.dialog.unknown")}
</span>
<span className="flex items-center">
<Clock className="h-3 w-3 mr-1" />
@@ -263,19 +384,43 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
</span>
</div>
</div>
- <div className="flex-grow overflow-hidden border-b">
+
+ {/* 뷰어 영역 - 남은 공간 모두 사용 */}
+ <div className="flex-1 min-h-0 overflow-hidden">
<BasicContractSignViewer
+ key={selectedContract.id} // key 추가로 컴포넌트 재생성 강제
contractId={selectedContract.id}
filePath={selectedContract.signedFilePath || undefined}
+ templateName={selectedContract.templateName || ""}
+ additionalFiles={additionalFiles} // 추가 파일 전달
instance={instance}
setInstance={setInstance}
+ t={t}
/>
</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>
+
+ {/* 고정 푸터 */}
+ <div className="p-4 flex justify-between items-center bg-gray-50 border-t flex-shrink-0">
+ <div className="flex items-center space-x-4">
+ <p className="text-sm text-gray-600 flex items-center">
+ <AlertCircle className="h-4 w-4 text-yellow-500 mr-1" />
+ {t("basicContracts.dialog.signWarning")}
+ </p>
+ {/* 준법 템플릿인 경우 추가 안내 */}
+ {selectedContract.templateName?.includes('준법') && (
+ <p className="text-xs text-amber-600 flex items-center">
+ <AlertCircle className="h-3 w-3 text-amber-500 mr-1" />
+ 모든 준법 항목을 체크해주세요
+ </p>
+ )}
+ {/* 비밀유지 계약서인 경우 추가 안내 */}
+ {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && (
+ <p className="text-xs text-blue-600 flex items-center">
+ <FileText className="h-3 w-3 text-blue-500 mr-1" />
+ 첨부 서류도 확인해주세요
+ </p>
+ )}
+ </div>
<Button
className="gap-2 bg-blue-600 hover:bg-blue-700 transition-colors"
onClick={completeSign}
@@ -287,12 +432,12 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
<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>
- 처리 중...
+ {t("basicContracts.dialog.processing")}
</>
) : (
<>
<FileSignature className="h-4 w-4" />
- 서명 완료
+ {t("basicContracts.dialog.completeSign")}
</>
)}
</Button>
@@ -303,9 +448,9 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
<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>
+ <h3 className="text-xl font-medium text-gray-800 mb-2">{t("basicContracts.dialog.selectDocument")}</h3>
<p className="text-gray-500 max-w-md">
- 왼쪽 목록에서 서명할 문서를 선택하면 여기에 문서 내용이 표시됩니다.
+ {t("basicContracts.dialog.selectDocumentDescription")}
</p>
</div>
)}