diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-27 12:06:26 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-27 12:06:26 +0000 |
| commit | 7548e2ad6948f1c6aa102fcac408bc6c9c0f9796 (patch) | |
| tree | 8e66703ec821888ad51dcc242a508813a027bf71 /lib/basic-contract/vendor-table | |
| parent | 7eac558470ef179dad626a8e82db5784fe86a556 (diff) | |
(대표님, 최겸) 기본계약, 입찰, 파일라우트, 계약서명라우트, 인포메이션, 메뉴설정, PQ(메일템플릿 관련)
Diffstat (limited to 'lib/basic-contract/vendor-table')
3 files changed, 474 insertions, 339 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 319ae4b9..f70bed94 100644 --- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx @@ -22,14 +22,15 @@ import { Loader2, ArrowRight, Trophy, - Target + 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 } from "../service"; +import { getVendorAttachments, processBuyerSignatureAction } from "../service"; // 계약서 상태 타입 정의 interface ContractStatus { @@ -42,16 +43,27 @@ 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, - t + mode = 'vendor', + onBuyerSignComplete, + t, + // 새로 추가된 props + open: externalOpen, + onOpenChange: externalOnOpenChange }: BasicContractSignDialogProps) { - const [open, setOpen] = React.useState(false); + // 내부 상태 (외부 제어가 없을 때 사용) + 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(""); @@ -64,14 +76,21 @@ export function BasicContractSignDialog({ // 계약서 상태 관리 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 }>>({}); const router = useRouter() - console.log(selectedContract,"selectedContract") - console.log(additionalFiles,"additionalFiles") + // 실제 사용할 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; @@ -87,29 +106,28 @@ export function BasicContractSignDialog({ return ""; }; - // 🔥 현재 선택된 계약서의 서명 완료 가능 여부 확인 + // 현재 선택된 계약서의 서명 완료 가능 여부 확인 const canCompleteCurrentContract = React.useMemo(() => { if (!selectedContract) return false; const contractId = selectedContract.id; + + // 구매자 모드에서는 설문조사나 GTC 체크 불필요 + if (isBuyerMode) { + const signatureCompleted = signatureStatus[contractId] === true; + return signatureCompleted; + } + + // 협력업체 모드의 기존 로직 const isComplianceTemplate = selectedContract.templateName?.includes('준법'); + const isGTCTemplate = selectedContract.templateName?.includes('GTC'); - // 1. 준법 템플릿인 경우 설문조사 완료 여부 확인 const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; - - // 2. 서명 완료 여부 확인 + const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.hasComments !== true) : true; const signatureCompleted = signatureStatus[contractId] === true; - console.log('🔍 서명 완료 가능 여부 체크:', { - contractId, - isComplianceTemplate, - surveyCompleted, - signatureCompleted, - canComplete: surveyCompleted && signatureCompleted - }); - - return surveyCompleted && signatureCompleted; - }, [selectedContract, surveyCompletionStatus, signatureStatus]); + return surveyCompleted && gtcCompleted && signatureCompleted; + }, [selectedContract, surveyCompletionStatus, signatureStatus, gtcCommentStatus, isBuyerMode]); // 계약서별 상태 초기화 React.useEffect(() => { @@ -142,7 +160,7 @@ export function BasicContractSignDialog({ return contracts.find(contract => contract.id === nextPendingId) || null; }; - // 다이얼로그 열기/닫기 핸들러 + // 다이얼로그 열기/닫기 핸들러 (외부 제어 지원) const handleOpenChange = (isOpen: boolean) => { if (!isOpen && !allCompleted && completedCount > 0) { // 완료되지 않은 계약서가 있으면 확인 대화상자 @@ -152,7 +170,12 @@ export function BasicContractSignDialog({ if (!confirmClose) return; } - setOpen(isOpen); + // 외부 제어가 있으면 외부 콜백 호출, 없으면 내부 상태 업데이트 + if (isControlledExternally && externalOnOpenChange) { + externalOnOpenChange(isOpen); + } else { + setInternalOpen(isOpen); + } if (!isOpen) { // 다이얼로그 닫을 때 상태 초기화 @@ -160,8 +183,9 @@ export function BasicContractSignDialog({ setSearchTerm(""); setAdditionalFiles([]); setContractStatuses([]); - setSurveyCompletionStatus({}); // 🔥 추가 - setSignatureStatus({}); // 🔥 추가 + setSurveyCompletionStatus({}); + setSignatureStatus({}); + setGtcCommentStatus({}); // WebViewer 인스턴스 정리 if (instance) { try { @@ -202,9 +226,14 @@ export function BasicContractSignDialog({ } } }, [open, contracts, selectedContract, contractStatuses]); - - // 추가 파일 가져오기 useEffect + + // 추가 파일 가져오기 useEffect (구매자 모드에서는 스킵) React.useEffect(() => { + if (isBuyerMode) { + setAdditionalFiles([]); + return; + } + const fetchAdditionalFiles = async () => { if (!selectedContract) { setAdditionalFiles([]); @@ -235,9 +264,9 @@ export function BasicContractSignDialog({ }; fetchAdditionalFiles(); - }, [selectedContract]); + }, [selectedContract, isBuyerMode]); - // 🔥 설문조사 완료 콜백 함수 + // 설문조사 완료 콜백 함수 const handleSurveyComplete = React.useCallback((contractId: number) => { console.log(`📋 설문조사 완료: 계약서 ${contractId}`); setSurveyCompletionStatus(prev => ({ @@ -246,7 +275,7 @@ export function BasicContractSignDialog({ })); }, []); - // 🔥 서명 완료 콜백 함수 + // 서명 완료 콜백 함수 const handleSignatureComplete = React.useCallback((contractId: number) => { console.log(`✍️ 서명 완료: 계약서 ${contractId}`); setSignatureStatus(prev => ({ @@ -255,31 +284,64 @@ export function BasicContractSignDialog({ })); }, []); - // 서명 완료 핸들러 (수정됨) + // GTC 코멘트 상태 변경 콜백 함수 + const handleGtcCommentStatusChange = React.useCallback((contractId: number, hasComments: boolean, commentCount: number) => { + console.log(`📋 GTC 코멘트 상태 변경: 계약서 ${contractId}, 코멘트 ${commentCount}개`); + setGtcCommentStatus(prev => ({ + ...prev, + [contractId]: { hasComments, commentCount } + })); + }, []); + + // 서명 완료 핸들러 const completeSign = async () => { if (!instance || !selectedContract) return; - // 🔥 서명 완료 가능 여부 재확인 + // 서명 완료 가능 여부 재확인 if (!canCompleteCurrentContract) { const contractId = selectedContract.id; - const isComplianceTemplate = selectedContract.templateName?.includes('준법'); - const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; - const signatureCompleted = signatureStatus[contractId] === true; - if (!surveyCompleted) { - toast.error("준법 설문조사를 먼저 완료해주세요.", { - description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.", - icon: <AlertCircle className="h-5 w-5 text-red-500" /> - }); - return; - } - - if (!signatureCompleted) { - toast.error("계약서에 서명을 먼저 완료해주세요.", { - description: "문서의 서명 필드에 서명해주세요.", - icon: <Target className="h-5 w-5 text-blue-500" /> - }); - return; + if (isBuyerMode) { + const signatureCompleted = signatureStatus[contractId] === true; + + if (!signatureCompleted) { + toast.error("계약서에 서명을 먼저 완료해주세요.", { + description: "문서의 서명 필드에 서명해주세요.", + icon: <Target className="h-5 w-5 text-blue-500" /> + }); + return; + } + } else { + // 협력업체 모드의 기존 검증 로직 + const isComplianceTemplate = selectedContract.templateName?.includes('준법'); + const isGTCTemplate = selectedContract.templateName?.includes('GTC'); + const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; + const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.hasComments !== true) : true; + const signatureCompleted = signatureStatus[contractId] === true; + + if (!surveyCompleted) { + toast.error("준법 설문조사를 먼저 완료해주세요.", { + description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.", + icon: <AlertCircle className="h-5 w-5 text-red-500" /> + }); + return; + } + + if (!gtcCompleted) { + toast.error("GTC 조항에 코멘트가 있어 서명할 수 없습니다.", { + description: "조항 검토 탭에서 모든 코멘트를 삭제하거나 협의를 완료해주세요.", + icon: <AlertCircle className="h-5 w-5 text-red-500" /> + }); + return; + } + + if (!signatureCompleted) { + toast.error("계약서에 서명을 먼저 완료해주세요.", { + description: "문서의 서명 필드에 서명해주세요.", + icon: <Target className="h-5 w-5 text-blue-500" /> + }); + return; + } } return; @@ -303,73 +365,135 @@ export function BasicContractSignDialog({ xfdfString, downloadType: "pdf", }); - - // FormData 생성 및 파일 추가 - 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 - ) + + if (isBuyerMode) { + // 구매자 모드: 최종승인 처리 + const result = await processBuyerSignatureAction( + selectedContract.id, + data, + selectedContract.signedFileName || `contract_${selectedContract.id}_buyer_signed.pdf` ); - toast.success("계약서 서명이 완료되었습니다!", { - description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`, - icon: <CheckCircle2 className="h-5 w-5 text-green-500" /> - }); + if (result.success) { + // 성공시 해당 계약서 상태를 완료로 업데이트 + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id + ? { ...status, status: 'completed' as const } + : status + ) + ); - // 다음 미완료 계약서로 자동 이동 - const nextContract = getNextPendingContract(); - if (nextContract) { - setSelectedContract(nextContract); - toast.info(`다음 계약서로 이동합니다`, { - description: nextContract.templateName, - icon: <ArrowRight className="h-4 w-4 text-blue-500" /> + toast.success("최종승인이 완료되었습니다!", { + description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`, + icon: <CheckCircle2 className="h-5 w-5 text-green-500" /> }); + + // 구매자 서명 완료 콜백 호출 + if (onBuyerSignComplete) { + onBuyerSignComplete(selectedContract.id, data); + } + + // 다음 미완료 계약서로 자동 이동 + const nextContract = getNextPendingContract(); + if (nextContract) { + setSelectedContract(nextContract); + toast.info(`다음 계약서로 이동합니다`, { + description: nextContract.templateName, + icon: <ArrowRight className="h-4 w-4 text-blue-500" /> + }); + } else { + // 모든 계약서 완료시 + toast.success("🎉 모든 계약서 최종승인이 완료되었습니다!", { + description: `총 ${totalCount}개 계약서 승인 완료`, + icon: <Trophy className="h-5 w-5 text-yellow-500" /> + }); + } + + router.refresh(); } else { - // 모든 계약서 완료시 - toast.success("🎉 모든 계약서 서명이 완료되었습니다!", { - description: `총 ${totalCount}개 계약서 서명 완료`, - icon: <Trophy className="h-5 w-5 text-yellow-500" /> + // 실패시 에러 상태 업데이트 + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id + ? { ...status, status: 'error' as const, errorMessage: result.message } + : status + ) + ); + + toast.error("최종승인 처리 중 오류가 발생했습니다", { + description: result.message, + icon: <AlertCircle className="h-5 w-5 text-red-500" /> }); } - - router.refresh(); } else { - // 실패시 에러 상태 업데이트 - setContractStatuses(prev => - prev.map(status => - status.id === selectedContract.id - ? { ...status, status: 'error' as const, errorMessage: result.error } - : status - ) - ); - - toast.error("서명 처리 중 오류가 발생했습니다", { - description: result.error, - icon: <AlertCircle className="h-5 w-5 text-red-500" /> + // 협력업체 모드: 기존 로직 + 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.success("계약서 서명이 완료되었습니다!", { + description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`, + icon: <CheckCircle2 className="h-5 w-5 text-green-500" /> + }); + + // 다음 미완료 계약서로 자동 이동 + const nextContract = getNextPendingContract(); + if (nextContract) { + setSelectedContract(nextContract); + toast.info(`다음 계약서로 이동합니다`, { + description: nextContract.templateName, + icon: <ArrowRight className="h-4 w-4 text-blue-500" /> + }); + } else { + // 모든 계약서 완료시 + toast.success("🎉 모든 계약서 서명이 완료되었습니다!", { + description: `총 ${totalCount}개 계약서 서명 완료`, + icon: <Trophy className="h-5 w-5 text-yellow-500" /> + }); + } + + router.refresh(); + } else { + // 실패시 에러 상태 업데이트 + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id + ? { ...status, status: 'error' as const, errorMessage: result.error } + : status + ) + ); + + toast.error("서명 처리 중 오류가 발생했습니다", { + description: result.error, + icon: <AlertCircle className="h-5 w-5 text-red-500" /> + }); + } } } catch (error) { console.error("서명 완료 중 오류:", error); @@ -391,56 +515,99 @@ export function BasicContractSignDialog({ // 모든 서명 완료 핸들러 const completeAllSigns = () => { - setOpen(false); + handleOpenChange(false); if (onSuccess) { onSuccess(); } - toast.success("모든 계약서 서명이 완료되었습니다!", { - description: "계약서 관리 페이지가 새로고침됩니다.", + const successMessage = isBuyerMode + ? "모든 계약서 최종승인이 완료되었습니다!" + : "모든 계약서 서명이 완료되었습니다!"; + + toast.success(successMessage, { + description: "계약서 관리 페이지가 새고침됩니다.", icon: <Trophy className="h-5 w-5 text-yellow-500" /> }); }; return ( <> - {/* 서명 버튼 */} - <Button - variant="outline" - size="sm" - onClick={() => setOpen(true)} - 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 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`} - aria-hidden="true" - /> - <span className="hidden sm:inline flex items-center"> - {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> + {/* 서명 버튼 - 외부 제어가 없을 때만 표시 */} + {!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" )} - {isButtonDisabled && ( - <span className="ml-2 text-xs text-gray-400"> - ({getDisabledReason()}) - </span> + > + {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> - </Button> + <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="px-6 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b flex-shrink-0"> + <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"> - <FileSignature className="mr-2 h-5 w-5 text-blue-500" /> - {t("basicContracts.dialog.title")} + {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="ml-3 bg-blue-50 text-blue-700 border-blue-200"> + <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> {/* 추가 파일 로딩 표시 */} @@ -466,7 +633,12 @@ export function BasicContractSignDialog({ </div> <div className="w-full bg-gray-200 rounded-full h-2"> <div - className="bg-gradient-to-r from-blue-500 to-green-500 h-2 rounded-full transition-all duration-500" + 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> @@ -506,9 +678,11 @@ export function BasicContractSignDialog({ const isCompleted = contractStatus?.status === 'completed'; const hasError = contractStatus?.status === 'error'; - // 🔥 계약서별 완료 상태 확인 + // 계약서별 완료 상태 확인 const isComplianceTemplate = contract.templateName?.includes('준법'); + const isGTCTemplate = contract.templateName?.includes('GTC'); const hasSurveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contract.id] === true : true; + const hasGtcCompleted = isGTCTemplate ? (gtcCommentStatus[contract.id]?.hasComments !== true) : true; const hasSignatureCompleted = signatureStatus[contract.id] === true; return ( @@ -530,7 +704,11 @@ export function BasicContractSignDialog({ {/* 첫 번째 줄: 제목 + 상태 */} <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"> - <FileText className="h-3 w-3 mr-1 text-blue-500 flex-shrink-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 === "비밀유지 계약서" && ( @@ -538,6 +716,12 @@ export function BasicContractSignDialog({ 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> {/* 상태 표시 */} @@ -558,8 +742,8 @@ export function BasicContractSignDialog({ )} </div> - {/* 🔥 완료 상태 표시 */} - {!isCompleted && !hasError && ( + {/* 완료 상태 표시 (구매자 모드에서는 간소화) */} + {!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'}`}> @@ -567,6 +751,12 @@ export function BasicContractSignDialog({ 설문 </span> )} + {isGTCTemplate && ( + <span className={`flex items-center ${hasGtcCompleted ? 'text-green-600' : 'text-red-600'}`}> + <CheckCircle2 className={`h-3 w-3 mr-1 ${hasGtcCompleted ? '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'}`} /> 서명 @@ -574,6 +764,16 @@ export function BasicContractSignDialog({ </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"> @@ -609,14 +809,18 @@ export function BasicContractSignDialog({ {/* 뷰어 헤더 */} <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" /> + {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' ? ( <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"> @@ -625,19 +829,32 @@ export function BasicContractSignDialog({ </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> )} - {/* 준법 템플릿 표시 */} - {selectedContract.templateName?.includes('준법') && ( + {/* 준법/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> + )} - {/* 비밀유지 계약서인 경우 추가 파일 수 표시 */} - {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && ( + {/* 비밀유지 계약서인 경우 추가 파일 수 표시 (구매자 모드가 아닐 때만) */} + {!isBuyerMode && selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && ( <Badge variant="outline" className="ml-2 bg-blue-50 text-blue-700 border-blue-200"> 첨부파일 {additionalFiles.length}개 </Badge> @@ -665,8 +882,12 @@ export function BasicContractSignDialog({ additionalFiles={additionalFiles} instance={instance} setInstance={setInstance} - onSurveyComplete={() => handleSurveyComplete(selectedContract.id)} // 🔥 추가 - onSignatureComplete={() => handleSignatureComplete(selectedContract.id)} // 🔥 추가 + onSurveyComplete={() => handleSurveyComplete(selectedContract.id)} + onSignatureComplete={() => handleSignatureComplete(selectedContract.id)} + onGtcCommentStatusChange={(hasComments, commentCount) => + handleGtcCommentStatusChange(selectedContract.id, hasComments, commentCount) + } + mode={mode} t={t} /> </div> @@ -678,44 +899,58 @@ export function BasicContractSignDialog({ {currentContractStatus?.status === 'completed' ? ( <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" /> - {t("basicContracts.dialog.signWarning")} + {isBuyerMode + ? "계약서에 구매자 서명을 완료해주세요." + : t("basicContracts.dialog.signWarning") + } </p> {/* 완료 상태 체크리스트 */} - <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] ? '완료' : '미완료'} + {!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') && ( + <span className={`flex items-center ${(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? 'text-green-600' : 'text-red-600'}`}> + <CheckCircle2 className={`h-3 w-3 mr-1 ${(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? 'text-green-500' : 'text-red-500'}`} /> + 조항검토 {(gtcCommentStatus[selectedContract.id]?.hasComments !== 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> - )} - <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> + )} + + {/* 구매자 모드의 간소화된 체크리스트 */} + {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> - - {/* 비밀유지 계약서인 경우 추가 안내 */} - {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> @@ -725,11 +960,16 @@ export function BasicContractSignDialog({ {allCompleted ? ( // 모든 계약서 완료시 <Button - className="gap-2 bg-green-600 hover:bg-green-700 transition-colors" + 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' ? ( // 현재 계약서가 완료된 경우 @@ -750,13 +990,16 @@ export function BasicContractSignDialog({ ) : ( // 현재 계약서를 서명해야 하는 경우 <Button - className={`gap-2 transition-colors ${ + className={cn( + "gap-2 transition-colors", canCompleteCurrentContract - ? "bg-blue-600 hover:bg-blue-700" + ? 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} // 🔥 조건 수정 + disabled={!canCompleteCurrentContract || isSubmitting} > {isSubmitting ? ( <> @@ -768,11 +1011,15 @@ export function BasicContractSignDialog({ </> ) : ( <> - <FileSignature className="h-4 w-4" /> - 서명 완료 + {isBuyerMode ? ( + <Shield className="h-4 w-4" /> + ) : ( + <FileSignature className="h-4 w-4" /> + )} + {signButtonText} {totalCount > 1 && ( <span className="ml-1 text-xs"> - ({completedCount + 1}/{totalCount}) + ({completedCount}/{totalCount}) </span> )} </> @@ -784,12 +1031,22 @@ export function BasicContractSignDialog({ </> ) : ( <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 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"> - {t("basicContracts.dialog.selectDocumentDescription")} + {isBuyerMode + ? "승인할 계약서를 선택해주세요." + : t("basicContracts.dialog.selectDocumentDescription") + } </p> </div> )} diff --git a/lib/basic-contract/vendor-table/basic-contract-table.tsx b/lib/basic-contract/vendor-table/basic-contract-table.tsx index 48298f21..cefc0fb2 100644 --- a/lib/basic-contract/vendor-table/basic-contract-table.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-table.tsx @@ -38,6 +38,8 @@ export function BasicContractsVendorTable({ promises }: BasicTemplateTableProps) const [{ data, pageCount }] = React.use(promises) + console.log(data,"data") + // 안전한 번역 함수 (fallback 포함) const safeT = React.useCallback((key: string, fallback: string) => { diff --git a/lib/basic-contract/vendor-table/survey-conditional.ts b/lib/basic-contract/vendor-table/survey-conditional.ts index 71c2c1ff..686cc4ba 100644 --- a/lib/basic-contract/vendor-table/survey-conditional.ts +++ b/lib/basic-contract/vendor-table/survey-conditional.ts @@ -131,6 +131,22 @@ public getIncompleteReason(question: SurveyQuestion, surveyAnswers: Record<numbe return true; } + private isValidFile(file: any): boolean { + if (!file || typeof file !== 'object') return false; + + // 브라우저 File 객체 체크 (새로 업로드한 파일) + if (file.name || file.size || file.type) return true; + + // 서버 파일 메타데이터 체크 (기존 파일) + if (file.filename || file.originalName || file.id || file.mimeType) return true; + + return false; + } + + private hasValidFiles(files: any[]): boolean { + return files && files.length > 0 && files.every(file => this.isValidFile(file)); + } + private isQuestionCompleteEnhanced(question: SurveyQuestion, surveyAnswers: Record<number, any>): boolean { const answer = surveyAnswers[question.id]; @@ -157,60 +173,50 @@ public getIncompleteReason(question: SurveyQuestion, surveyAnswers: Record<numbe const hasOtherText = answer.answerValue === 'OTHER' ? (answer.otherText && answer.otherText.trim() !== '') : true; - // 4️⃣ files 체크 (실제 파일이 있는 경우) - const hasValidFiles = answer.files && answer.files.length > 0 && - answer.files.every((file: any) => file && typeof file === 'object' && - (file.name || file.size || file.type)); // 빈 객체 {} 제외 + // 4️⃣ files 체크 (브라우저 File 객체와 서버 파일 메타데이터 모두 처리) - 수정된 부분 + const hasValidFilesResult = this.hasValidFiles(answer.files || []); console.log(`📊 Q${question.questionNumber} 완료 조건 체크:`, { hasAnswerValue, hasDetailText, hasOtherText, - hasValidFiles, + hasValidFiles: hasValidFilesResult, questionType: question.questionType, hasDetailTextRequired: question.hasDetailText, hasFileUploadRequired: question.hasFileUpload || question.questionType === 'FILE' }); - // 🎯 질문 타입별 완료 조건 + // 질문 타입별 완료 조건은 동일하지만 hasValidFilesResult 변수 사용 switch (question.questionType) { case 'RADIO': case 'DROPDOWN': - // 선택형: answerValue가 있고, OTHER인 경우 otherText도 필요 const isSelectComplete = hasAnswerValue && hasOtherText; - // detailText가 필요한 경우 추가 체크 if (question.hasDetailText && isSelectComplete) { return hasDetailText; } - // 파일 업로드가 필요한 경우 추가 체크 if ((question.hasFileUpload || question.questionType === 'FILE') && isSelectComplete) { - return hasValidFiles; + return hasValidFilesResult; // 수정된 부분 } return isSelectComplete; case 'TEXTAREA': - // 텍스트 영역: detailText 또는 answerValue 중 하나라도 있으면 됨 return hasDetailText || hasAnswerValue; case 'FILE': - // 파일 업로드: 유효한 파일이 있어야 함 - return hasValidFiles; + return hasValidFilesResult; // 수정된 부분 default: - // 기본: answerValue, detailText, 파일 중 하나라도 있으면 완료 - let isComplete = hasAnswerValue || hasDetailText || hasValidFiles; + let isComplete = hasAnswerValue || hasDetailText || hasValidFilesResult; // 수정된 부분 - // detailText가 필수인 경우 if (question.hasDetailText) { isComplete = isComplete && hasDetailText; } - // 파일 업로드가 필수인 경우 if (question.hasFileUpload) { - isComplete = isComplete && hasValidFiles; + isComplete = isComplete && hasValidFilesResult; // 수정된 부분 } return isComplete; @@ -303,17 +309,7 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any // 🎯 현재 답변 상태에 따라 표시되는 질문들 계산 const visibleQuestions = this.getVisibleQuestions(surveyAnswers); - console.log('🔍 표시되는 모든 질문들의 상세 정보:', visibleQuestions.map(q => ({ - id: q.id, - questionNumber: q.questionNumber, - questionText: q.questionText?.substring(0, 30) + '...', - isRequired: q.isRequired, - parentQuestionId: q.parentQuestionId, - conditionalValue: q.conditionalValue, - isConditional: !!q.parentQuestionId, - hasAnswer: !!surveyAnswers[q.id]?.answerValue, - answerValue: surveyAnswers[q.id]?.answerValue - }))); + // 🚨 중요: 트리거된 조건부 질문들을 필수로 처리 const requiredQuestions = visibleQuestions.filter(q => { @@ -334,26 +330,6 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any return false; }); - console.log('📊 필수 질문 필터링 결과:', { - 전체질문수: this.questions.length, - 표시되는질문수: visibleQuestions.length, - 원래필수질문: visibleQuestions.filter(q => q.isRequired).length, - 트리거된조건부질문: visibleQuestions.filter(q => { - if (!q.parentQuestionId || !q.conditionalValue) return false; - const parentAnswer = surveyAnswers[q.parentQuestionId]; - return parentAnswer?.answerValue === q.conditionalValue; - }).length, - 최종필수질문: requiredQuestions.length, - 현재답변수: Object.keys(surveyAnswers).length, - 필수질문들: requiredQuestions.map(q => ({ - id: q.id, - questionNumber: q.questionNumber, - isRequired: q.isRequired, - isConditional: !!q.parentQuestionId, - hasAnswer: !!surveyAnswers[q.id]?.answerValue, - 처리방식: q.isRequired ? '원래필수' : '트리거됨' - })) - }); const completedQuestionIds: number[] = []; const incompleteQuestionIds: number[] = []; @@ -365,10 +341,7 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any // 🎯 개선된 완료 체크: 모든 답변 형태를 고려 const isComplete = this.isQuestionCompleteEnhanced(question, surveyAnswers); - - console.log(`📊 Q${question.questionNumber} 완료 상태: ${isComplete}`); - console.log(`📝 Q${question.questionNumber} 답변 내용:`, surveyAnswers[question.id]); - + // 디버깅 정보 수집 debugInfo[question.id] = { questionText: question.questionText, @@ -414,25 +387,6 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any if (process.env.NODE_ENV === 'development') { (result as any).debugInfo = debugInfo; - // 📋 상세한 진행 상황 로그 - console.log('📋 최종 진행 상황:', { - 총필수질문: requiredQuestions.length, - 완료된질문: completedQuestionIds.length, - 미완료질문: incompleteQuestionIds.length, - 진행률: `${Math.round(progressPercentage)}%`, - 기본질문: visibleQuestions.filter(q => !q.parentQuestionId).length, - 조건부질문: visibleQuestions.filter(q => q.parentQuestionId).length, - 완료된기본질문: completedQuestionIds.filter(id => !visibleQuestions.find(q => q.id === id)?.parentQuestionId).length, - 완료된조건부질문: completedQuestionIds.filter(id => !!visibleQuestions.find(q => q.id === id)?.parentQuestionId).length, - 필수질문상세: requiredQuestions.map(q => ({ - id: q.id, - questionNumber: q.questionNumber, - isRequired: q.isRequired, - isConditional: !!q.parentQuestionId, - isComplete: completedQuestionIds.includes(q.id) - })) - }); - // 🔍 미완료 질문들의 구체적 이유 if (incompleteQuestionIds.length > 0) { console.log('🔍 미완료 질문들:', incompleteQuestionIds.map(id => ({ @@ -446,24 +400,7 @@ private getCompletionDetailsEnhanced(question: SurveyQuestion, answer: any): any // ⚡ 조건부 질문 활성화 및 완료 현황 const conditionalQuestions = visibleQuestions.filter(q => q.parentQuestionId); - if (conditionalQuestions.length > 0) { - console.log('⚡ 조건부 질문 상세 현황:', conditionalQuestions.map(q => ({ - id: q.id, - questionNumber: q.questionNumber, - isRequired: q.isRequired, - parentId: q.parentQuestionId, - condition: q.conditionalValue, - parentAnswer: surveyAnswers[q.parentQuestionId!]?.answerValue, - isTriggered: surveyAnswers[q.parentQuestionId!]?.answerValue === q.conditionalValue, - hasAnswer: !!surveyAnswers[q.id]?.answerValue, - answerValue: surveyAnswers[q.id]?.answerValue, - detailText: surveyAnswers[q.id]?.detailText, - files: surveyAnswers[q.id]?.files, - isComplete: debugInfo[q.id]?.isComplete, - isIncludedInRequired: requiredQuestions.some(rq => rq.id === q.id), - completionDetails: this.getCompletionDetailsEnhanced(q, surveyAnswers[q.id]) - }))); - } + } return result; @@ -606,37 +543,21 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): { const childQuestion = this.questions.find(q => q.id === childId); if (!childQuestion) return; - console.log(`🔍 자식 질문 ${childId} 체크:`, { - childId, - questionNumber: childQuestion.questionNumber, - conditionalValue: childQuestion.conditionalValue, - newParentValue, - shouldKeep: childQuestion.conditionalValue === newParentValue, - currentAnswer: updatedAnswers[childId]?.answerValue - }); - // 새로운 부모 값이 자식의 조건과 맞지 않으면 자식 답변 삭제 if (childQuestion.conditionalValue !== newParentValue) { - console.log(`🗑️ 자식 질문 Q${childQuestion.questionNumber} 답변 초기화 (조건 불일치)`); delete updatedAnswers[childId]; // 재귀적으로 손자 질문들도 정리 const grandChildAnswers = this.clearAffectedChildAnswers(childId, '', updatedAnswers); Object.assign(updatedAnswers, grandChildAnswers); } else { - console.log(`✅ 자식 질문 Q${childQuestion.questionNumber} 유지 (조건 일치)`); } }); const clearedCount = childQuestionIds.filter(childId => !updatedAnswers[childId]).length; const keptCount = childQuestionIds.filter(childId => !!updatedAnswers[childId]).length; - console.log(`📊 자식 질문 정리 완료:`, { - parentQuestionId, - 총자식질문: childQuestionIds.length, - 초기화된질문: clearedCount, - 유지된질문: keptCount - }); + return updatedAnswers; } @@ -680,21 +601,17 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): { conditionalValue: question.conditionalValue }; - console.log(`🔍 질문 완료 체크 [Q${question.questionNumber}]:`, logData); if (!question.isRequired) { - console.log(`✅ Q${question.questionNumber}: 선택 질문이므로 완료`); return true; } if (!answer?.answerValue) { - console.log(`❌ Q${question.questionNumber}: 답변이 없음`); return false; } // 1. '기타' 선택 시 추가 입력이 필요한 경우 if (answer.answerValue === 'OTHER' && !answer.otherText?.trim()) { - console.log(`❌ Q${question.questionNumber}: '기타' 선택했지만 상세 내용 없음`); return false; } @@ -702,16 +619,8 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): { if (question.hasDetailText) { const needsDetailText = ['YES', '네', 'Y', 'true', '1'].includes(answer.answerValue); - console.log(`📝 Q${question.questionNumber} 상세텍스트 체크:`, { - hasDetailText: question.hasDetailText, - answerValue: answer.answerValue, - needsDetailText, - detailText: answer.detailText?.length || 0, - detailTextExists: !!answer.detailText?.trim() - }); if (needsDetailText && !answer.detailText?.trim()) { - console.log(`❌ Q${question.questionNumber}: '${answer.answerValue}' 선택했지만 상세 내용 없음`); return false; } } @@ -720,17 +629,7 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): { if (question.hasFileUpload || question.questionType === 'FILE') { const needsFileUpload = ['YES', '네', 'Y', 'true', '1'].includes(answer.answerValue); - console.log(`📁 Q${question.questionNumber} 파일업로드 체크:`, { - hasFileUpload: question.hasFileUpload, - questionType: question.questionType, - answerValue: answer.answerValue, - needsFileUpload, - filesCount: answer.files?.length || 0, - hasFiles: !!answer.files && answer.files.length > 0 - }); - if (needsFileUpload && (!answer.files || answer.files.length === 0)) { - console.log(`❌ Q${question.questionNumber}: '${answer.answerValue}' 선택했지만 파일 업로드 없음`); return false; } } @@ -739,50 +638,27 @@ getOverallProgressStatus(surveyAnswers: Record<number, any>): { const childQuestions = this.getChildQuestions(question.id); if (childQuestions.length > 0) { - console.log(`🔗 Q${question.questionNumber} 부모 질문 - 자식 질문들:`, - childQuestions.map(c => ({ - id: c.id, - questionNumber: c.questionNumber, - condition: c.conditionalValue, - required: c.isRequired, - text: c.questionText?.substring(0, 20) + '...' - })) - ); // 현재 답변으로 트리거되는 자식 질문들 찾기 const triggeredChildren = childQuestions.filter(child => child.conditionalValue === answer.answerValue ); - console.log(`🎯 Q${question.questionNumber} 답변 '${answer.answerValue}'로 트리거된 자식들:`, - triggeredChildren.map(c => ({ - id: c.id, - questionNumber: c.questionNumber, - required: c.isRequired, - text: c.questionText?.substring(0, 30) + '...' - })) - ); - // 트리거된 필수 자식 질문들이 모두 완료되었는지 확인 for (const childQuestion of triggeredChildren) { if (childQuestion.isRequired) { - console.log(`🔄 자식 질문 Q${childQuestion.questionNumber} 완료 체크 시작...`); const childComplete = this.isQuestionComplete(childQuestion, surveyAnswers); - console.log(`📊 자식 질문 Q${childQuestion.questionNumber} 완료 상태: ${childComplete}`); if (!childComplete) { - console.log(`❌ 부모 Q${question.questionNumber}의 자식 Q${childQuestion.questionNumber} 미완료`); return false; } } } if (triggeredChildren.filter(c => c.isRequired).length > 0) { - console.log(`✅ Q${question.questionNumber}의 모든 필수 조건부 자식 질문들 완료됨`); } } - console.log(`✅ Q${question.questionNumber} 완료 체크 통과`); return true; } |
