summaryrefslogtreecommitdiff
path: root/lib/basic-contract/vendor-table/basic-contract-sign-dialog-test.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/vendor-table/basic-contract-sign-dialog-test.tsx')
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-sign-dialog-test.tsx1166
1 files changed, 0 insertions, 1166 deletions
diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog-test.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog-test.tsx
deleted file mode 100644
index ace34454..00000000
--- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog-test.tsx
+++ /dev/null
@@ -1,1166 +0,0 @@
-"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 { useToast } from "@/hooks/use-toast";
-import { cn } from "@/lib/utils";
-import type { WebViewerInstance } from "@pdftron/webviewer";
-import type { BasicContractView } from "@/db/schema";
-import {
- Upload,
- FileSignature,
- CheckCircle2,
- Search,
- Clock,
- FileText,
- User,
- AlertCircle,
- Calendar,
- Loader2,
- ArrowRight,
- Trophy,
- Target,
- Shield
-} from "lucide-react";
-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, processBuyerSignatureAction } from "../service";
-
-// 계약서 상태 타입 정의
-interface ContractStatus {
- id: number;
- status: 'pending' | 'completed' | 'error' | 'vendor_signed';
- errorMessage?: string;
-}
-
-interface BasicContractSignDialogProps {
- contracts: BasicContractView[];
- onSuccess?: () => void;
- hasSelectedRows?: boolean;
- mode?: 'vendor' | 'buyer';
- onBuyerSignComplete?: (contractId: number, signedData: ArrayBuffer) => void;
- t: (key: string) => string;
- // 외부 상태 제어를 위한 새로운 props (선택적)
- open?: boolean;
- onOpenChange?: (open: boolean) => void;
-}
-
-export function BasicContractSignDialog({
- contracts,
- onSuccess,
- hasSelectedRows = false,
- mode = 'vendor',
- onBuyerSignComplete,
- t,
- // 새로 추가된 props
- open: externalOpen,
- onOpenChange: externalOnOpenChange
-}: BasicContractSignDialogProps) {
- const { toast } = useToast();
-
- // 내부 상태 (외부 제어가 없을 때 사용)
- const [internalOpen, setInternalOpen] = 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 [contractStatuses, setContractStatuses] = React.useState<ContractStatus[]>([]);
-
- // 서명/설문/GTC 코멘트 완료 상태 관리
- const [surveyCompletionStatus, setSurveyCompletionStatus] = React.useState<Record<number, boolean>>({});
- const [signatureStatus, setSignatureStatus] = React.useState<Record<number, boolean>>({});
- const [gtcCommentStatus, setGtcCommentStatus] = React.useState<Record<number, {
- hasComments: boolean;
- commentCount: number;
- reviewStatus?: string;
- isComplete?: boolean;
- }>>({});
-
- const router = useRouter()
-
- // 실제 사용할 open 상태 (외부 제어가 있으면 외부 상태 사용, 없으면 내부 상태 사용)
- const isControlledExternally = externalOpen !== undefined;
- const open = isControlledExternally ? externalOpen : internalOpen;
-
- // 모드에 따른 텍스트
- const isBuyerMode = mode === 'buyer';
- const dialogTitle = isBuyerMode ? "구매자 최종승인 서명" : t("basicContracts.dialog.title");
- const signButtonText = isBuyerMode ? "최종승인 완료" : "서명 완료 및 저장";
-
- // 버튼 비활성화 조건
- 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 canCompleteCurrentContract = React.useMemo(() => {
- if (!selectedContract) return false;
-
- const contractId = selectedContract.id;
-
- // 구매자 모드: 기존 로직 유지
- if (isBuyerMode) {
- const signatureCompleted = signatureStatus[contractId] === true;
- return signatureCompleted;
- }
-
- // 협력업체 모드 + 내자: TrustNet으로 이동만 하면 되므로 버튼은 항상 활성화
- if (isDomesticVendorForContract(selectedContract)) {
- return true;
- }
-
- const isComplianceTemplate = selectedContract.templateName?.includes('준법');
- const isGTCTemplate = selectedContract.templateName?.includes('GTC');
- const requiresNegotiationComplete = isComplianceTemplate || isGTCTemplate;
-
- const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true;
-
- const negotiationStatus = gtcCommentStatus[contractId];
- const negotiationCleared = requiresNegotiationComplete
- ? (!negotiationStatus?.hasComments || negotiationStatus?.isComplete === true)
- : true;
-
- const signatureCompleted = signatureStatus[contractId] === true;
-
- return surveyCompleted && negotiationCleared && signatureCompleted;
-}, [selectedContract, surveyCompletionStatus, signatureStatus, gtcCommentStatus, isBuyerMode]);
-
-
- // 계약서별 상태 초기화
- // Vendor signed 상태의 계약서도 포함하여 초기화
- React.useEffect(() => {
- if (contracts.length > 0 && contractStatuses.length === 0) {
- setContractStatuses(
- contracts.map(contract => {
- // 이미 서명된 계약서는 vendor_signed 상태로 초기화
- const isSigned = contract.vendorSignedAt ||
- contract.status === "COMPLETED" ||
- contract.status === "VENDOR_SIGNED";
- return {
- id: contract.id,
- status: isSigned ? ('vendor_signed' as const) : ('pending' as const)
- };
- })
- );
- }
- }, [contracts, contractStatuses.length]);
-
- // 완료된 계약서 수 계산
- const completedCount = contractStatuses.filter(status =>
- status.status === 'completed' || status.status === 'vendor_signed'
- ).length;
- const totalCount = contracts.length;
- const allCompleted = completedCount === totalCount && totalCount > 0;
-
- // 현재 선택된 계약서의 상태
- const currentContractStatus = selectedContract
- ? contractStatuses.find(status => status.id === selectedContract.id)
- : null;
-
- // 내자/외자 판별 (vendors.country 기반, KR 앞 2자리)
- const getVendorCountryCode = React.useCallback((contract: BasicContractView | null) => {
- if (!contract || !contract.vendorCountry) return null;
- return contract.vendorCountry.substring(0, 2).toUpperCase();
- }, []);
-
- const isDomesticVendorForContract = React.useCallback((contract: BasicContractView | null) => {
- if (!contract) return false;
- const countryCode = getVendorCountryCode(contract);
- return countryCode === "KR";
- }, [getVendorCountryCode]);
-
- const isDomesticVendor = React.useMemo(() => {
- return isDomesticVendorForContract(selectedContract);
- }, [selectedContract, isDomesticVendorForContract]);
-
- // 다음 미완료 계약서 찾기
- const getNextPendingContract = () => {
- const pendingStatuses = contractStatuses.filter(status =>
- status.status === 'pending' || status.status === 'error'
- );
- if (pendingStatuses.length === 0) return null;
-
- const nextPendingId = pendingStatuses[0].id;
- return contracts.find(contract => contract.id === nextPendingId) || null;
- };
-
- // 다이얼로그 열기/닫기 핸들러 (외부 제어 지원)
- const handleOpenChange = (isOpen: boolean) => {
- if (!isOpen && !allCompleted && completedCount > 0) {
- // 완료되지 않은 계약서가 있으면 확인 대화상자
- // const confirmClose = window.confirm(
- // `${completedCount}/${totalCount}개 계약서가 완료되었습니다. 정말 나가시겠습니까?`
- // );
- const confirmClose = window.confirm(
- `정말 나가시겠습니까?`
- );
- if (!confirmClose) return;
- }
-
- // 외부 제어가 있으면 외부 콜백 호출, 없으면 내부 상태 업데이트
- if (isControlledExternally && externalOnOpenChange) {
- externalOnOpenChange(isOpen);
- } else {
- setInternalOpen(isOpen);
- }
-
- if (!isOpen) {
- // 다이얼로그 닫을 때 상태 초기화
- setSelectedContract(null);
- setSearchTerm("");
- setAdditionalFiles([]);
- setContractStatuses([]);
- setSurveyCompletionStatus({});
- setSignatureStatus({});
- setGtcCommentStatus({});
- // 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);
- };
-
- // 검색된 계약서 필터링
- const filteredContracts = React.useMemo(() => {
- if (!searchTerm.trim()) return contracts;
-
- const term = searchTerm.toLowerCase();
- return contracts.filter(contract =>
- (contract.templateName || '').toLowerCase().includes(term) ||
- (contract.requestedByName || '').toLowerCase().includes(term)
- );
- }, [contracts, searchTerm]);
-
- // 다이얼로그가 열릴 때 첫 번째 미완료 계약서 자동 선택
- React.useEffect(() => {
- if (open && contracts.length > 0 && !selectedContract) {
- const firstPending = getNextPendingContract();
- if (firstPending) {
- setSelectedContract(firstPending);
- } else {
- setSelectedContract(contracts[0]);
- }
- }
- }, [open, contracts, selectedContract, contractStatuses]);
-
- // 추가 파일 가져오기 useEffect (구매자 모드에서는 스킵)
- React.useEffect(() => {
- if (isBuyerMode) {
- setAdditionalFiles([]);
- return;
- }
-
- const fetchAdditionalFiles = async () => {
- if (!selectedContract) {
- setAdditionalFiles([]);
- return;
- }
-
- // "비밀유지 계약서"인 경우에만 추가 파일 가져오기
- if (selectedContract.templateName === "비밀유지 계약서" && selectedContract.vendorId) {
- 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, isBuyerMode]);
-
- // 설문조사 완료 콜백 함수
- const handleSurveyComplete = React.useCallback((contractId: number) => {
- console.log(`📋 설문조사 완료: 계약서 ${contractId}`);
- setSurveyCompletionStatus(prev => ({
- ...prev,
- [contractId]: true
- }));
- }, []);
-
- // 서명 완료 콜백 함수
- const handleSignatureComplete = React.useCallback((contractId: number) => {
- console.log(`✍️ 서명 완료: 계약서 ${contractId}`);
- setSignatureStatus(prev => ({
- ...prev,
- [contractId]: true
- }));
- }, []);
-
- // GTC 코멘트 상태 변경 콜백 함수
- const handleGtcCommentStatusChange = React.useCallback((
- contractId: number,
- hasComments: boolean,
- commentCount: number,
- reviewStatus?: string,
- isComplete?: boolean
- ) => {
- console.log(`📋 GTC 상태 변경: 계약서 ${contractId}, 코멘트 ${commentCount}개, 상태: ${reviewStatus}, 완료: ${isComplete}`);
- setGtcCommentStatus(prev => ({
- ...prev,
- [contractId]: { hasComments, commentCount, reviewStatus, isComplete }
- }));
- }, []);
-
- // 서명 완료 핸들러
- const completeSign = async () => {
- if (!selectedContract) return;
-
- // 협력업체 모드 + 내자일 때는 TrustNet으로 이동 (기존 PDF 서명 로직 사용 안 함)
- if (!isBuyerMode && isDomesticVendor) {
- try {
- const baseUrl = "https://partners.sevcp.com/trustnet/contract-req.html";
- const origin = typeof window !== "undefined" ? window.location.origin : "";
- const url = `${baseUrl}?contractId=${encodeURIComponent(
- String(selectedContract.id)
- )}&origin=${encodeURIComponent(origin)}`;
-
- window.open(url, "_blank", "noopener,noreferrer");
-
- toast({
- title: "공동인증서 서명 페이지가 새 창으로 열렸습니다.",
- description: "TrustNet 화면에서 전자서명을 완료해주세요.",
- });
- } catch (error) {
- console.error("TrustNet 서명 페이지 열기 실패:", error);
- toast({
- title: "TrustNet 서명 페이지를 여는 중 오류가 발생했습니다.",
- variant: "destructive",
- });
- }
-
- return;
- }
-
- if (!instance) return;
-
- // 서명 완료 가능 여부 재확인
- if (!canCompleteCurrentContract) {
- const contractId = selectedContract.id;
-
- if (isBuyerMode) {
- const signatureCompleted = signatureStatus[contractId] === true;
-
- if (!signatureCompleted) {
- toast({
- title: "계약서에 서명을 먼저 완료해주세요.",
- description: "문서의 서명 필드에 서명해주세요.",
- variant: "destructive"
- });
- return;
- }
- } else {
- // 협력업체 모드의 기존 검증 로직
- const isComplianceTemplate = selectedContract.templateName?.includes('준법');
- const isGTCTemplate = selectedContract.templateName?.includes('GTC');
- const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true;
- const requiresNegotiationComplete = isComplianceTemplate || isGTCTemplate;
- const negotiationStatus = gtcCommentStatus[contractId];
- const negotiationCleared = requiresNegotiationComplete
- ? (!negotiationStatus?.hasComments || negotiationStatus?.isComplete === true)
- : true;
- const signatureCompleted = signatureStatus[contractId] === true;
-
- if (!surveyCompleted) {
- toast({
- title: "준법 설문조사를 먼저 완료해주세요.",
- description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.",
- variant: "destructive"
- });
- return;
- }
-
- if (!negotiationCleared) {
- toast({
- title: "코멘트가 있어 서명할 수 없습니다.",
- description: "협의 코멘트 탭에서 모든 코멘트를 삭제하거나 협의를 완료해주세요.",
- variant: "destructive"
- });
- return;
- }
-
- if (!signatureCompleted) {
- toast({
- title: "계약서에 서명을 먼저 완료해주세요.",
- description: "문서의 서명 필드에 서명해주세요.",
- variant: "destructive"
- });
- return;
- }
- }
-
- return;
- }
-
- setIsSubmitting(true);
- try {
- const { documentViewer, annotationManager } = instance.Core;
- 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",
- });
-
- if (isBuyerMode) {
- // 구매자 모드: 최종승인 처리
- const result = await processBuyerSignatureAction(
- selectedContract.id,
- data,
- selectedContract.signedFileName || `contract_${selectedContract.id}_buyer_signed.pdf`
- );
-
- if (result.success) {
- // 성공시 해당 계약서 상태를 완료로 업데이트
- setContractStatuses(prev =>
- prev.map(status =>
- status.id === selectedContract.id
- ? { ...status, status: 'completed' as const }
- : status
- )
- );
-
- toast({
- title: "최종승인이 완료되었습니다!",
- description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`
- });
-
- // 구매자 서명 완료 콜백 호출
- if (onBuyerSignComplete) {
- onBuyerSignComplete(selectedContract.id, data);
- }
-
- // 다음 미완료 계약서로 자동 이동
- const nextContract = getNextPendingContract();
- if (nextContract) {
- setSelectedContract(nextContract);
- } else {
- // 모든 계약서 완료시
- toast({
- title: "🎉 모든 계약서 최종승인이 완료되었습니다!",
- description: `총 ${totalCount}개 계약서 승인 완료`
- });
- }
-
- router.refresh();
- } else {
- // 실패시 에러 상태 업데이트
- setContractStatuses(prev =>
- prev.map(status =>
- status.id === selectedContract.id
- ? { ...status, status: 'error' as const, errorMessage: result.message }
- : status
- )
- );
-
- toast({
- title: "최종승인 처리 중 오류가 발생했습니다",
- description: result.message,
- variant: "destructive"
- });
- }
- } else {
- // 협력업체 모드: 기존 로직
- 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));
- }
-
- // API 호출
- const response = await fetch('/api/upload/signed-contract', {
- method: 'POST',
- body: submitFormData,
- next: { tags: ["basicContractView-vendor"] },
- });
-
- const result = await response.json();
-
- if (result.result) {
- // 성공시 해당 계약서 상태를 완료로 업데이트
- setContractStatuses(prev =>
- prev.map(status =>
- status.id === selectedContract.id
- ? { ...status, status: 'completed' as const }
- : status
- )
- );
-
- toast({
- title: "계약서 서명이 완료되었습니다!",
- description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`
- });
-
- // 다음 미완료 계약서로 자동 이동
- const nextContract = getNextPendingContract();
- if (nextContract) {
- setSelectedContract(nextContract);
- } else {
- // 모든 계약서 완료시
- toast({
- title: "🎉 모든 계약서 서명이 완료되었습니다!",
- description: `총 ${totalCount}개 계약서 서명 완료`
- });
- }
-
- router.refresh();
- } else {
- // 실패시 에러 상태 업데이트
- setContractStatuses(prev =>
- prev.map(status =>
- status.id === selectedContract.id
- ? { ...status, status: 'error' as const, errorMessage: result.error }
- : status
- )
- );
-
- toast({
- title: "서명 처리 중 오류가 발생했습니다",
- description: result.error,
- variant: "destructive"
- });
- }
- }
- } catch (error) {
- console.error("서명 완료 중 오류:", error);
-
- // 에러 상태 업데이트
- setContractStatuses(prev =>
- prev.map(status =>
- status.id === selectedContract.id
- ? { ...status, status: 'error' as const, errorMessage: '서명 처리 중 오류가 발생했습니다' }
- : status
- )
- );
-
- toast({
- title: "서명 처리 중 오류가 발생했습니다",
- variant: "destructive"
- });
- } finally {
- setIsSubmitting(false);
- }
- };
-
- // 모든 서명 완료 핸들러
- const completeAllSigns = () => {
- handleOpenChange(false);
- if (onSuccess) {
- onSuccess();
- }
- const successMessage = isBuyerMode
- ? "모든 계약서 최종승인이 완료되었습니다!"
- : "모든 계약서 서명이 완료되었습니다!";
-
- toast({
- title: successMessage,
- description: "계약서 관리 페이지가 새고침됩니다."
- });
- };
-
- return (
- <>
- {/* 서명 버튼 - 외부 제어가 없을 때만 표시 */}
- {!isControlledExternally && (
- <Button
- variant="outline"
- size="sm"
- onClick={() => handleOpenChange(true)}
- disabled={isButtonDisabled}
- className={cn(
- "gap-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed",
- isBuyerMode
- ? "hover:bg-green-50 hover:text-green-600 hover:border-green-200"
- : "hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200"
- )}
- >
- {isBuyerMode ? (
- <Shield
- className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-green-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">
- {isBuyerMode ? "구매자 서명" : t("basicContracts.toolbar.sign")}
- {contracts.length > 0 && !isButtonDisabled && (
- <Badge
- variant="secondary"
- className={cn(
- "ml-2",
- isBuyerMode
- ? "bg-green-100 text-green-700 hover:bg-green-200"
- : "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-7xl w-[95vw] h-[90vh] p-0 flex flex-col overflow-hidden" style={{ width: '95vw', maxWidth: '95vw' }}>
- {/* 고정 헤더 - 진행 상황 표시 */}
- <DialogHeader className={cn(
- "px-6 py-4 border-b flex-shrink-0",
- isBuyerMode
- ? "bg-gradient-to-r from-green-50 to-emerald-50"
- : "bg-gradient-to-r from-blue-50 to-purple-50"
- )}>
- <DialogTitle className="text-xl font-bold flex items-center justify-between text-gray-800">
- <div className="flex items-center">
- {isBuyerMode ? (
- <Shield className="mr-2 h-5 w-5 text-green-500" />
- ) : (
- <FileSignature className="mr-2 h-5 w-5 text-blue-500" />
- )}
- {dialogTitle}
- {/* 진행 상황 표시 */}
- {/* <Badge
- variant="outline"
- className={cn(
- "ml-3",
- isBuyerMode
- ? "bg-green-50 text-green-700 border-green-200"
- : "bg-blue-50 text-blue-700 border-blue-200"
- )}
- >
- {completedCount}/{totalCount} 완료
- </Badge> */}
- {/* 추가 파일 로딩 표시 */}
- {isLoadingAttachments && (
- <Loader2 className="ml-2 h-4 w-4 animate-spin text-blue-500" />
- )}
- </div>
-
- {allCompleted && (
- <Badge variant="default" className="bg-green-100 text-green-700 border-green-200">
- <Trophy className="h-4 w-4 mr-1" />
- 전체 완료!
- </Badge>
- )}
- </DialogTitle>
-
- {/* 진행률 바 */}
- {totalCount > 1 && (
- <div className="mt-3">
- <div className="flex justify-between text-xs text-gray-600 mb-1">
- <span>전체 진행률</span>
- <span>{Math.round((completedCount / totalCount) * 100)}%</span>
- </div>
- <div className="w-full bg-gray-200 rounded-full h-2">
- <div
- className={cn(
- "h-2 rounded-full transition-all duration-500",
- isBuyerMode
- ? "bg-gradient-to-r from-green-500 to-emerald-500"
- : "bg-gradient-to-r from-blue-500 to-green-500"
- )}
- style={{ width: `${(completedCount / totalCount) * 100}%` }}
- />
- </div>
- </div>
- )}
- </DialogHeader>
-
- {/* 메인 컨텐츠 영역 - 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={t("basicContracts.dialog.searchPlaceholder")}
- className="bg-white pl-8 text-sm"
- value={searchTerm}
- onChange={(e) => setSearchTerm(e.target.value)}
- />
- </div>
- </div>
-
- <ScrollArea className="flex-1">
- <div className="p-2">
- {filteredContracts.length === 0 ? (
- <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">
- {filteredContracts.map((contract) => {
- const contractStatus = contractStatuses.find(status => status.id === contract.id);
- const isCompleted = contractStatus?.status === 'completed' || contractStatus?.status === 'vendor_signed';
- const hasError = contractStatus?.status === 'error';
-
- // 계약서별 완료 상태 확인
- const isComplianceTemplate = contract.templateName?.includes('준법');
- const isGTCTemplate = contract.templateName?.includes('GTC');
- const requiresNegotiation = isComplianceTemplate || isGTCTemplate;
- const hasSurveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contract.id] === true : true;
- const negotiationStatus = gtcCommentStatus[contract.id];
- const hasNegotiationCompleted = requiresNegotiation
- ? (!negotiationStatus?.hasComments || negotiationStatus?.isComplete === true)
- : true;
- const hasSignatureCompleted = signatureStatus[contract.id] === true;
-
- return (
- <Button
- key={contract.id}
- variant="outline"
- className={cn(
- "w-full justify-start text-left h-auto p-2 bg-white transition-colors",
- "border border-gray-200 rounded-md",
- selectedContract?.id === contract.id && !isCompleted && "border-blue-500 bg-blue-50 shadow-sm",
- isCompleted && "border-green-200 bg-green-50",
- hasError && "border-red-200 bg-red-50",
- !isCompleted && !hasError && "hover:bg-blue-50 hover:border-blue-200"
- )}
- onClick={() => handleSelectContract(contract)}
- // Vendor signed 상태에서도 코멘트를 볼 수 있도록 비활성화하지 않음
- disabled={false}
- >
- <div className="flex flex-col w-full space-y-1">
- {/* 첫 번째 줄: 제목 + 상태 */}
- <div className="flex items-center justify-between w-full">
- <span className="font-medium text-xs truncate text-gray-800 flex items-center min-w-0">
- {isBuyerMode ? (
- <Shield className="h-3 w-3 mr-1 text-green-500 flex-shrink-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>
- )}
- {/* GTC 계약서인 경우 표시 */}
- {contract.templateName?.includes('GTC') && (
- <Badge variant="outline" className="ml-1 bg-purple-50 text-purple-700 border-purple-200 text-xs">
- GTC
- </Badge>
- )}
- </span>
-
- {/* 상태 표시 */}
- {isCompleted ? (
- <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 text-xs ml-2 flex-shrink-0">
- <CheckCircle2 className="h-3 w-3 mr-1" />
- 완료
- </Badge>
- ) : hasError ? (
- <Badge variant="outline" className="bg-red-50 text-red-700 border-red-200 text-xs ml-2 flex-shrink-0">
- <AlertCircle className="h-3 w-3 mr-1" />
- 오류
- </Badge>
- ) : (
- <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200 text-xs ml-2 flex-shrink-0">
- 대기
- </Badge>
- )}
- </div>
-
- {/* 완료 상태 표시 (구매자 모드에서는 간소화) */}
- {!isCompleted && !hasError && !isBuyerMode && (
- <div className="flex items-center space-x-2 text-xs">
- {isComplianceTemplate && (
- <span className={`flex items-center ${hasSurveyCompleted ? 'text-green-600' : 'text-gray-400'}`}>
- <CheckCircle2 className={`h-3 w-3 mr-1 ${hasSurveyCompleted ? 'text-green-500' : 'text-gray-300'}`} />
- 설문
- </span>
- )}
- {requiresNegotiation && (
- <span className={`flex items-center ${hasNegotiationCompleted ? 'text-green-600' : 'text-red-600'}`}>
- <CheckCircle2 className={`h-3 w-3 mr-1 ${hasNegotiationCompleted ? 'text-green-500' : 'text-red-500'}`} />
- 협의
- </span>
- )}
- <span className={`flex items-center ${hasSignatureCompleted ? 'text-green-600' : 'text-gray-400'}`}>
- <Target className={`h-3 w-3 mr-1 ${hasSignatureCompleted ? 'text-green-500' : 'text-gray-300'}`} />
- 서명
- </span>
- </div>
- )}
-
- {/* 구매자 모드의 간소화된 상태 표시 */}
- {!isCompleted && !hasError && isBuyerMode && (
- <div className="flex items-center space-x-2 text-xs">
- <span className={`flex items-center ${hasSignatureCompleted ? 'text-green-600' : 'text-gray-400'}`}>
- <Target className={`h-3 w-3 mr-1 ${hasSignatureCompleted ? 'text-green-500' : 'text-gray-300'}`} />
- 구매자 서명
- </span>
- </div>
- )}
-
- {/* 두 번째 줄: 사용자 + 날짜 */}
- <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 ml-2 flex-shrink-0">
- <Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
- <span>{formatDate(contract.createdAt)}</span>
- </div>
- </div>
-
- {/* 에러 메시지 표시 */}
- {hasError && contractStatus?.errorMessage && (
- <div className="text-xs text-red-600 mt-1">
- {contractStatus.errorMessage}
- </div>
- )}
- </div>
- </Button>
- );
- })}
- </div>
- )}
- </div>
- </ScrollArea>
- </div>
-
- {/* 오른쪽 영역 - 문서 뷰어 (확장 가능) */}
- <div className="flex-1 bg-white flex flex-col min-w-0">
- {selectedContract ? (
- <>
- {/* 뷰어 헤더 */}
- <div className="p-4 border-b bg-gray-50 flex-shrink-0">
- <h3 className="font-semibold text-gray-800 flex items-center">
- {isBuyerMode ? (
- <Shield className="h-4 w-4 mr-2 text-green-500" />
- ) : (
- <FileText className="h-4 w-4 mr-2 text-blue-500" />
- )}
- {selectedContract.templateName || t("basicContracts.dialog.document")}
-
- {/* 현재 계약서 상태 표시 */}
- {(currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed') ? (
- <Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200">
- <CheckCircle2 className="h-3 w-3 mr-1" />
- {isBuyerMode ? "승인 완료" : "서명 완료"}
- </Badge>
- ) : currentContractStatus?.status === 'error' ? (
- <Badge variant="outline" className="ml-2 bg-red-50 text-red-700 border-red-200">
- <AlertCircle className="h-3 w-3 mr-1" />
- 처리 실패
- </Badge>
- ) : (
- <Badge variant="outline" className="ml-2 bg-yellow-50 text-yellow-700 border-yellow-200">
- {isBuyerMode ? "승인 대기" : "서명 대기"}
- </Badge>
- )}
-
- {/* 구매자 모드 배지 */}
- {isBuyerMode && (
- <Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200">
- 구매자 모드
- </Badge>
- )}
-
- {/* 준법/GTC 템플릿 표시 (구매자 모드가 아닐 때만) */}
- {!isBuyerMode && selectedContract.templateName?.includes('준법') && (
- <Badge variant="outline" className="ml-2 bg-amber-50 text-amber-700 border-amber-200">
- 준법 서류
- </Badge>
- )}
-
- {!isBuyerMode && selectedContract.templateName?.includes('GTC') && (
- <Badge variant="outline" className="ml-2 bg-purple-50 text-purple-700 border-purple-200">
- GTC 계약서
- </Badge>
- )}
-
- {/* 비밀유지 계약서인 경우 추가 파일 수 표시 (구매자 모드가 아닐 때만) */}
- {!isBuyerMode && 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-2 text-sm text-gray-500">
- <span className="flex items-center">
- <User className="h-3 w-3 mr-1" />
- {t("basicContracts.dialog.requester")}: {selectedContract.requestedByName || t("basicContracts.dialog.unknown")}
- </span>
- <span className="flex items-center">
- <Clock className="h-3 w-3 mr-1" />
- {formatDate(selectedContract.createdAt)}
- </span>
- </div>
- </div>
-
- {/* 뷰어 영역 - 남은 공간 모두 사용 */}
- <div className="flex-1 min-h-0 overflow-hidden">
- <BasicContractSignViewer
- key={selectedContract.id}
- contractId={selectedContract.id}
- filePath={selectedContract.signedFilePath || undefined}
- templateName={selectedContract.templateName || ""}
- additionalFiles={additionalFiles}
- instance={instance}
- setInstance={setInstance}
- onSurveyComplete={() => handleSurveyComplete(selectedContract.id)}
- onSignatureComplete={() => handleSignatureComplete(selectedContract.id)}
- onGtcCommentStatusChange={(hasComments, commentCount, reviewStatus, isComplete) =>
- handleGtcCommentStatusChange(selectedContract.id, hasComments, commentCount, reviewStatus, isComplete)
- }
- mode={mode}
- t={t}
- negotiationCompletedAt={(selectedContract as any).negotiationCompletedAt || null}
- />
- </div>
-
- {/* 고정 푸터 - 동적 버튼 */}
- <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">
- {/* 현재 계약서가 완료된 경우 */}
- {currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed' ? (
- <p className="text-sm text-green-600 flex items-center">
- <CheckCircle2 className="h-4 w-4 text-green-500 mr-1" />
- 이 계약서는 이미 {isBuyerMode ? "승인이" : "서명이"} 완료되었습니다. 코멘트를 확인할 수 있습니다.
- </p>
- ) : currentContractStatus?.status === 'error' ? (
- <p className="text-sm text-red-600 flex items-center">
- <AlertCircle className="h-4 w-4 text-red-500 mr-1" />
- {isBuyerMode ? "승인" : "서명"} 처리 중 오류가 발생했습니다. 다시 시도해주세요.
- </p>
- ) : (
- <>
- {/* 완료 조건 안내 메시지 */}
- <div className="flex flex-col space-y-1">
- <p className="text-sm text-gray-600 flex items-center">
- <AlertCircle className="h-4 w-4 text-yellow-500 mr-1" />
- {isBuyerMode
- ? "계약서에 구매자 서명을 완료해주세요."
- : t("basicContracts.dialog.signWarning")
- }
- </p>
-
- {/* 완료 상태 체크리스트 */}
- {!isBuyerMode && (
- <div className="flex items-center space-x-4 text-xs">
- {selectedContract.templateName?.includes('준법') && (
- <span className={`flex items-center ${surveyCompletionStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
- <CheckCircle2 className={`h-3 w-3 mr-1 ${surveyCompletionStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
- 설문조사 {surveyCompletionStatus[selectedContract.id] ? '완료' : '미완료'}
- </span>
- )}
- {(selectedContract.templateName?.includes('GTC') || selectedContract.templateName?.includes('준법')) && (
- <span className={`flex items-center ${
- (gtcCommentStatus[selectedContract.id]?.isComplete === true) || !gtcCommentStatus[selectedContract.id]?.hasComments
- ? 'text-green-600'
- : 'text-red-600'
- }`}>
- <CheckCircle2 className={`h-3 w-3 mr-1 ${
- (gtcCommentStatus[selectedContract.id]?.isComplete === true) || !gtcCommentStatus[selectedContract.id]?.hasComments
- ? 'text-green-500'
- : 'text-red-500'
- }`} />
- 협의 {(!gtcCommentStatus[selectedContract.id]?.hasComments || gtcCommentStatus[selectedContract.id]?.isComplete === true)
- ? '완료'
- : `미완료 (코멘트 ${gtcCommentStatus[selectedContract.id]?.commentCount || 0}개)`}
- </span>
- )}
- <span className={`flex items-center ${signatureStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
- <Target className={`h-3 w-3 mr-1 ${signatureStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
- 서명 {signatureStatus[selectedContract.id] ? '완료' : '미완료'}
- </span>
- </div>
- )}
-
- {/* 구매자 모드의 간소화된 체크리스트 */}
- {isBuyerMode && (
- <div className="flex items-center space-x-4 text-xs">
- <span className={`flex items-center ${signatureStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}>
- <Target className={`h-3 w-3 mr-1 ${signatureStatus[selectedContract.id] ? 'text-green-500' : 'text-red-500'}`} />
- 구매자 서명 {signatureStatus[selectedContract.id] ? '완료' : '미완료'}
- </span>
- </div>
- )}
- </div>
- </>
- )}
- </div>
-
- {/* 동적 버튼 영역 */}
- <div className="flex items-center space-x-2">
- {allCompleted ? (
- // 모든 계약서 완료시
- <Button
- className={cn(
- "gap-2 transition-colors",
- isBuyerMode
- ? "bg-green-600 hover:bg-green-700"
- : "bg-green-600 hover:bg-green-700"
- )}
- onClick={completeAllSigns}
- >
- <Trophy className="h-4 w-4" />
- 모든 {isBuyerMode ? "승인" : "서명"} 완료
- </Button>
- ) : (currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed') ? (
- // 현재 계약서가 완료된 경우 - 코멘트 확인만 가능
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- className="gap-2"
- onClick={() => {
- const nextContract = getNextPendingContract();
- if (nextContract) {
- setSelectedContract(nextContract);
- }
- }}
- disabled={!getNextPendingContract()}
- >
- <ArrowRight className="h-4 w-4" />
- 다음 계약서
- </Button>
- <p className="text-sm text-gray-500">
- 서명 완료 - 코멘트 확인 가능
- </p>
- </div>
- ) : (
- // 현재 계약서를 서명해야 하는 경우
- <Button
- className={cn(
- "gap-2 transition-colors",
- canCompleteCurrentContract
- ? isBuyerMode
- ? "bg-green-600 hover:bg-green-700"
- : "bg-blue-600 hover:bg-blue-700"
- : "bg-gray-400 cursor-not-allowed"
- )}
- onClick={completeSign}
- disabled={!canCompleteCurrentContract || 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>
- 처리중...
- </>
- ) : (
- <>
- {isBuyerMode ? (
- <Shield className="h-4 w-4" />
- ) : (
- <FileSignature className="h-4 w-4" />
- )}
- {signButtonText}
- {totalCount > 1 && (
- <span className="ml-1 text-xs">
- ({completedCount}/{totalCount})
- </span>
- )}
- </>
- )}
- </Button>
- )}
- </div>
- </div>
- </>
- ) : (
- <div className="flex flex-col items-center justify-center h-full text-center p-6">
- <div className={cn(
- "p-6 rounded-full mb-4",
- isBuyerMode ? "bg-green-50" : "bg-blue-50"
- )}>
- {isBuyerMode ? (
- <Shield className="h-12 w-12 text-green-500" />
- ) : (
- <FileSignature className="h-12 w-12 text-blue-500" />
- )}
- </div>
- <h3 className="text-xl font-medium text-gray-800 mb-2">{t("basicContracts.dialog.selectDocument")}</h3>
- <p className="text-gray-500 max-w-md">
- {isBuyerMode
- ? "승인할 계약서를 선택해주세요."
- : t("basicContracts.dialog.selectDocumentDescription")
- }
- </p>
- </div>
- )}
- </div>
- </div>
- </DialogContent>
- </Dialog>
- </>
- );
-} \ No newline at end of file