diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-20 02:52:16 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-20 02:52:16 +0000 |
| commit | 77cbcaf27c9de8b361a6c5a13f0eefb37fd0d0e5 (patch) | |
| tree | 4ae3c46af089c1f23f8e2b1650ba59f5f34e5fc4 /lib/basic-contract | |
| parent | 088a161f8852dd7566619baca93257c0ccd901b7 (diff) | |
(임수민) 기본계약서 서명 위치, 설문조사 수정
Diffstat (limited to 'lib/basic-contract')
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 164 |
1 files changed, 129 insertions, 35 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index 7f5fa027..adc735e9 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -697,6 +697,9 @@ export function BasicContractSignViewer({ }: BasicContractSignViewerProps) { const { toast } = useToast(); + // 🔽 추가 + const signatureFieldsRef = useRef<string[]>([]); + const [fileLoading, setFileLoading] = useState<boolean>(true); const [activeTab, setActiveTab] = useState<string>("main"); const [surveyData, setSurveyData] = useState<any>({}); @@ -822,6 +825,11 @@ export function BasicContractSignViewer({ setTimeout(() => cleanupHtmlStyle(), 100); }; + // 🔽 최신 서명 필드 이름들을 ref에 동기화 + useEffect(() => { + signatureFieldsRef.current = signatureFields; + }, [signatureFields]); + useEffect(() => { setShowDialog(isOpen); @@ -960,33 +968,57 @@ export function BasicContractSignViewer({ documentViewer.addEventListener('documentLoaded', handleDocumentLoaded); - // 구매자 모드가 아닐 때만 자동 서명 적용 - if (mode !== 'buyer') { - annotationManager.addEventListener('annotationChanged', async (annotList, type) => { - for (const annot of annotList) { - const { fieldName, X, Y, Width, Height, PageNumber } = annot; - - if (type === "add" && annot.Subject === "Widget") { - const signatureImage = await getVendorSignatureFile() - - const stamp = new Annotations.StampAnnotation(); - stamp.PageNumber = PageNumber; - stamp.X = X; - stamp.Y = Y; - stamp.Width = Width; - stamp.Height = Height; - - const dataUrl = signatureImage?.data?.dataUrl; - if (dataUrl) { - await stamp.setImageData(dataUrl); - annot.sign(stamp); - annot.setFieldFlag(WidgetFlags.READ_ONLY, true); - } - + // 🔁 서명 관련 annotation 제어 + annotationManager.addEventListener('annotationChanged', async (annotList, type) => { + if (type !== 'add') return; + + for (const annot of annotList) { + const isWidgetSignature = + annot instanceof Annotations.SignatureWidgetAnnotation; + + // 1) 필드 없는(Signature) 서명은 전부 막기 + // - WebViewer 기본 "어디나 한 번 찍는" 서명은 보통 Subject 가 "Signature" 로 들어옵니다. + // - 혹시 다르면 console.log(annot) 찍어서 Subject 를 맞춰주면 됩니다. + if (!isWidgetSignature) { + if (annot.Subject === 'Signature') { + // 지정된 서명란 외 서명 → 즉시 삭제 + annotationManager.deleteAnnotation(annot, false); } + continue; } - }); - } + + // 2) 서명 위젯(필드)은 "우리가 생성한 필드"만 허용 + const field = annot.getField && annot.getField(); + const name = field?.name as string | undefined; + const allowed = signatureFieldsRef.current; + + if (name && allowed.length > 0 && !allowed.includes(name)) { + // 우리가 만든 서명 필드가 아니면 막기 + annotationManager.deleteAnnotation(annot, false); + continue; + } + + // 3) 여기까지 통과한 경우 = 우리가 만든 서명 필드에 대한 서명 + // → 기존 자동 서명(협력사 서명) 로직 유지 + if (mode !== 'buyer') { + const signatureImage = await getVendorSignatureFile(); + if (!signatureImage?.data?.dataUrl) continue; + + const stamp = new Annotations.StampAnnotation(); + stamp.PageNumber = annot.PageNumber; + stamp.X = annot.X; + stamp.Y = annot.Y; + stamp.Width = annot.Width; + stamp.Height = annot.Height; + + await stamp.setImageData(signatureImage.data.dataUrl); + + const { WidgetFlags } = Annotations; + annot.sign(stamp); + annot.setFieldFlag(WidgetFlags.READ_ONLY, true); + } + } + }); newInstance.UI.setMinZoomLevel('25%'); newInstance.UI.setMaxZoomLevel('400%'); @@ -1329,6 +1361,15 @@ export function BasicContractSignViewer({ {/* 구매자 모드에서는 탭 없이 단일 뷰어만 표시 */} {allFiles.length > 1 && mode !== 'buyer' ? ( <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col"> + {/* 준법설문 미완료 알림 배너 */} + {isComplianceTemplate && !surveyData.completed && ( + <div className="bg-amber-50 border-b-2 border-amber-400 px-4 py-2 flex items-center justify-center space-x-2"> + <AlertTriangle className="h-4 w-4 text-amber-600" /> + <span className="text-sm font-semibold text-amber-700"> + ⚠️ 준법 설문조사를 먼저 완료해주세요. 서명을 진행하려면 "준법 설문조사" 탭을 클릭하세요. + </span> + </div> + )} <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0"> <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}> {allFiles.map((file, index) => { @@ -1344,19 +1385,41 @@ export function BasicContractSignViewer({ tabId = `file-${fileOnlyIndex}`; } + const isSurveyTab = file.type === 'survey'; + const isSurveyIncomplete = isSurveyTab && !surveyData.completed; + return ( - <TabsTrigger key={tabId} value={tabId} className="text-xs"> + <TabsTrigger + key={tabId} + value={tabId} + className={`text-xs relative ${ + isSurveyIncomplete + ? 'bg-amber-50 border-2 border-amber-400 shadow-md' + : isSurveyTab + ? 'bg-blue-50 border border-blue-300' + : '' + }`} + > <div className="flex items-center space-x-1"> {file.type === 'survey' ? ( - <ClipboardList className="h-3 w-3" /> + <ClipboardList className={`h-3 w-3 ${isSurveyIncomplete ? 'text-amber-600' : 'text-blue-600'}`} /> ) : file.type === 'clauses' ? ( <BookOpen className="h-3 w-3" /> ) : ( <FileText className="h-3 w-3" /> )} - <span className="truncate">{file.name}</span> - {file.type === 'survey' && surveyData.completed && ( - <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge> + <span className={`truncate font-medium ${isSurveyIncomplete ? 'text-amber-700' : ''}`}> + {file.name} + </span> + {isSurveyTab && !surveyData.completed && ( + <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white animate-bounce"> + 필수 + </Badge> + )} + {isSurveyTab && surveyData.completed && ( + <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs bg-green-500 text-white"> + 완료 + </Badge> )} {file.type === 'clauses' && gtcCommentStatus.hasComments && ( <Badge variant="destructive" className="ml-1 h-4 px-1 text-xs"> @@ -1529,6 +1592,15 @@ export function BasicContractSignViewer({ {/* 구매자 모드에서는 탭 없이 단일 뷰어만 표시 */} {allFiles.length > 1 && mode !== 'buyer' ? ( <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col"> + {/* 준법설문 미완료 알림 배너 */} + {isComplianceTemplate && !surveyData.completed && ( + <div className="bg-amber-50 border-b-2 border-amber-400 px-4 py-2 flex items-center justify-center space-x-2"> + <AlertTriangle className="h-4 w-4 text-amber-600" /> + <span className="text-sm font-semibold text-amber-700"> + ⚠️ 준법 설문조사를 먼저 완료해주세요. 서명을 진행하려면 "준법 설문조사" 탭을 클릭하세요. + </span> + </div> + )} <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0"> <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}> {allFiles.map((file, index) => { @@ -1544,19 +1616,41 @@ export function BasicContractSignViewer({ tabId = `file-${fileOnlyIndex}`; } + const isSurveyTab = file.type === 'survey'; + const isSurveyIncomplete = isSurveyTab && !surveyData.completed; + return ( - <TabsTrigger key={tabId} value={tabId} className="text-xs"> + <TabsTrigger + key={tabId} + value={tabId} + className={`text-xs relative ${ + isSurveyIncomplete + ? 'bg-amber-50 border-2 border-amber-400 shadow-md' + : isSurveyTab + ? 'bg-blue-50 border border-blue-300' + : '' + }`} + > <div className="flex items-center space-x-1"> {file.type === 'survey' ? ( - <ClipboardList className="h-3 w-3" /> + <ClipboardList className={`h-3 w-3 ${isSurveyIncomplete ? 'text-amber-600' : 'text-blue-600'}`} /> ) : file.type === 'clauses' ? ( <BookOpen className="h-3 w-3" /> ) : ( <FileText className="h-3 w-3" /> )} - <span className="truncate">{file.name}</span> - {file.type === 'survey' && surveyData.completed && ( - <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge> + <span className={`truncate font-medium ${isSurveyIncomplete ? 'text-amber-700' : ''}`}> + {file.name} + </span> + {isSurveyTab && !surveyData.completed && ( + <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white animate-bounce"> + 필수 + </Badge> + )} + {isSurveyTab && surveyData.completed && ( + <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs bg-green-500 text-white"> + 완료 + </Badge> )} {file.type === 'clauses' && gtcCommentStatus.hasComments && ( <Badge variant="destructive" className="ml-1 h-4 px-1 text-xs"> |
