diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-07 05:05:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-07 05:05:40 +0000 |
| commit | 8daa2aeee017c642d2fd171094cf5d442966eb12 (patch) | |
| tree | 289879c50cc8f1f28d44c129846d6b153ecacdac | |
| parent | 7f973de2a814a2040a646161b563c8990b7e2fd2 (diff) | |
(임수민) 기본계약서 서명란 변수 수정
| -rw-r--r-- | lib/basic-contract/template/basic-contract-template-viewer.tsx | 117 | ||||
| -rw-r--r-- | lib/basic-contract/template/template-editor-wrapper.tsx | 101 |
2 files changed, 91 insertions, 127 deletions
diff --git a/lib/basic-contract/template/basic-contract-template-viewer.tsx b/lib/basic-contract/template/basic-contract-template-viewer.tsx index 52ea4153..38438d33 100644 --- a/lib/basic-contract/template/basic-contract-template-viewer.tsx +++ b/lib/basic-contract/template/basic-contract-template-viewer.tsx @@ -8,9 +8,8 @@ import React, { Dispatch, } from "react"; import { WebViewerInstance } from "@pdftron/webviewer"; -import { Loader2, Target } from "lucide-react"; +import { Loader2 } from "lucide-react"; import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; interface BasicContractTemplateViewerProps { templateId?: number; @@ -20,58 +19,23 @@ interface BasicContractTemplateViewerProps { onSignatureFieldFound?: (searchText: string) => void; // 서명란 발견 시 콜백 } -// 서명란 위치 정보 인터페이스 -interface SignatureFieldLocation { - pageNumber: number; - x: number; - y: number; - searchText: string; -} - -// 서명란으로 이동하는 함수 -const navigateToSignatureField = async ( - instance: WebViewerInstance | null, - location: SignatureFieldLocation | null -) => { - if (!instance || !location) { - toast.error("서명란 위치를 찾을 수 없습니다."); - return; - } - - try { - const { documentViewer } = instance.Core; - - // 해당 페이지로 이동 - documentViewer.setCurrentPage(location.pageNumber, true); - - // 페이지 로드 후 알림 - setTimeout(() => { - toast.success(`서명란으로 이동했습니다. (페이지 ${location.pageNumber})`, { - description: `"${location.searchText}" 텍스트를 찾아주세요.` - }); - }, 500); - } catch (error) { - console.error("서명란으로 이동 실패:", error); - toast.error("서명란으로 이동하는데 실패했습니다."); - } -}; - -// 서명란 텍스트 찾기 함수 +// 서명란 텍스트 찾기 함수 (변수 추가를 위해 사용) - 모든 서명란 텍스트를 찾아서 반환 const findSignatureFieldLocation = async ( instance: WebViewerInstance | null -): Promise<SignatureFieldLocation | null> => { +): Promise<string[]> => { if (!instance?.Core?.documentViewer) { - return null; + return []; } try { const { documentViewer } = instance.Core; const doc = documentViewer.getDocument(); - if (!doc) return null; + if (!doc) return []; // 검색할 텍스트 목록 (협력업체와 구매자 모두) const searchTexts = ['협력업체_서명란', '삼성중공업_서명란']; const pageCount = documentViewer.getPageCount(); + const foundTexts: string[] = []; for (const searchText of searchTexts) { for (let pageNumber = 1; pageNumber <= pageCount; pageNumber++) { @@ -88,16 +52,11 @@ const findSignatureFieldLocation = async ( const quads = await doc.getTextPosition(pageNumber, startIndex, endIndex); if (!quads || quads.length === 0) continue; - const q = quads[0] as any; - const x = Math.min(q.x1, q.x2, q.x3, q.x4); - const y = Math.min(q.y1, q.y2, q.y3, q.y4); - - return { - pageNumber, - x, - y, - searchText - }; + // 찾은 서명란 텍스트를 배열에 추가 (중복 제거) + if (!foundTexts.includes(searchText)) { + foundTexts.push(searchText); + } + break; // 해당 텍스트를 찾았으면 다음 텍스트로 이동 } catch (error) { console.warn(`페이지 ${pageNumber}에서 ${searchText} 검색 실패:`, error); continue; @@ -105,10 +64,10 @@ const findSignatureFieldLocation = async ( } } - return null; + return foundTexts; } catch (error) { console.error("서명란 위치 찾기 실패:", error); - return null; + return []; } }; @@ -120,7 +79,6 @@ export function BasicContractTemplateViewer({ onSignatureFieldFound, }: BasicContractTemplateViewerProps) { const [fileLoading, setFileLoading] = useState<boolean>(true); - const [signatureLocation, setSignatureLocation] = useState<SignatureFieldLocation | null>(null); const viewer = useRef<HTMLDivElement>(null); const initialized = useRef(false); const isCancelled = useRef(false); @@ -233,23 +191,22 @@ export function BasicContractTemplateViewer({ loadDocument(instance, filePath); }, [instance, filePath]); - // 문서 로드 후 서명란 위치 찾기 + // 문서 로드 후 서명란 텍스트 찾기 (변수 추가를 위해) useEffect(() => { if (!instance) return; const { documentViewer } = instance.Core; const handleDocumentLoaded = async () => { - // 문서 로드 완료 후 서명란 위치 찾기 + // 문서 로드 완료 후 서명란 텍스트 찾기 setTimeout(async () => { - const location = await findSignatureFieldLocation(instance); - if (location) { - setSignatureLocation(location); - console.log("✅ 서명란 위치 발견:", location); - // 서명란 발견 시 부모 컴포넌트에 알림 - if (onSignatureFieldFound) { - onSignatureFieldFound(location.searchText); - } + const foundTexts = await findSignatureFieldLocation(instance); + if (foundTexts.length > 0 && onSignatureFieldFound) { + console.log("✅ 서명란 텍스트 발견:", foundTexts); + // 모든 찾은 서명란 텍스트에 대해 콜백 호출 + foundTexts.forEach(searchText => { + onSignatureFieldFound(searchText); + }); } }, 2000); }; @@ -259,13 +216,12 @@ export function BasicContractTemplateViewer({ // 이미 문서가 로드되어 있다면 즉시 실행 if (documentViewer.getDocument()) { setTimeout(async () => { - const location = await findSignatureFieldLocation(instance); - if (location) { - setSignatureLocation(location); - // 서명란 발견 시 부모 컴포넌트에 알림 - if (onSignatureFieldFound) { - onSignatureFieldFound(location.searchText); - } + const foundTexts = await findSignatureFieldLocation(instance); + if (foundTexts.length > 0 && onSignatureFieldFound) { + // 모든 찾은 서명란 텍스트에 대해 콜백 호출 + foundTexts.forEach(searchText => { + onSignatureFieldFound(searchText); + }); } }, 2000); } @@ -273,7 +229,7 @@ export function BasicContractTemplateViewer({ return () => { documentViewer.removeEventListener('documentLoaded', handleDocumentLoaded); }; - }, [instance]); + }, [instance, onSignatureFieldFound]); // 한글 지원 Office 문서 로드 const loadDocument = async (instance: WebViewerInstance, documentPath: string) => { @@ -345,21 +301,6 @@ export function BasicContractTemplateViewer({ contain: 'layout style paint', // CSS containment로 격리 }} > - {/* 서명란으로 이동 버튼 - 툴바 아래에 배치 (편집 도구와 겹치지 않도록) */} - {signatureLocation && !fileLoading && ( - <div className="absolute right-4 z-30" style={{ top: '70px' }}> - <Button - variant="outline" - size="sm" - className="h-7 px-2 text-xs bg-blue-50 hover:bg-blue-100 border-blue-200 shadow-sm" - onClick={() => navigateToSignatureField(instance, signatureLocation)} - > - <Target className="h-3 w-3 mr-1" /> - 서명란으로 이동 - </Button> - </div> - )} - {fileLoading && ( <div className="absolute inset-0 flex flex-col items-center justify-center bg-white bg-opacity-90 z-10"> <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> diff --git a/lib/basic-contract/template/template-editor-wrapper.tsx b/lib/basic-contract/template/template-editor-wrapper.tsx index 2c1c7e4d..8d2ce97f 100644 --- a/lib/basic-contract/template/template-editor-wrapper.tsx +++ b/lib/basic-contract/template/template-editor-wrapper.tsx @@ -43,9 +43,9 @@ const getVariablesForTemplate = (templateName: string): string[] => { } } - // 모든 템플릿에 서명란 변수 추가 (중복 제거) - const signatureVars = ["vendor_signature", "buyer_signature"]; - const allVariables = [...variables, ...signatureVars]; + // 모든 템플릿에 서명란 텍스트 추가 (자동 서명 기능을 위해 실제 텍스트 사용) + const signatureTexts = ["협력업체_서명란", "삼성중공업_서명란"]; + const allVariables = [...variables, ...signatureTexts]; return [...new Set(allVariables)]; // 중복 제거 }; @@ -78,21 +78,12 @@ const VARIABLE_DESCRIPTION_MAP = { "phone_number": "전화번호", "phone": "전화번호", "email": "이메일", - "vendor_signature": "협력업체 서명란", - "buyer_signature": "구매자 서명란", "협력업체_서명란": "협력업체 서명란", "삼성중공업_서명란": "구매자 서명란" } as const; -// 서명란 텍스트를 변수명으로 변환 -const getSignatureVariableName = (searchText: string): string => { - if (searchText === '협력업체_서명란') { - return 'vendor_signature'; - } else if (searchText === '삼성중공업_서명란') { - return 'buyer_signature'; - } - return searchText; -}; +// 서명란 텍스트는 그대로 사용 (자동 서명 기능이 정확한 텍스트를 찾아야 함) +// 변환하지 않고 원본 텍스트를 그대로 반환 // 변수 패턴 감지를 위한 정규식 const VARIABLE_PATTERN = /\{\{([^}]+)\}\}/g; @@ -207,7 +198,9 @@ export function TemplateEditorWrapper({ } try { - const textToInsert = `{{${variableName}}}`; + // 서명란 텍스트는 {{}} 없이 그대로 사용 + const isSignatureText = variableName === '협력업체_서명란' || variableName === '삼성중공업_서명란'; + const textToInsert = isSignatureText ? variableName : `{{${variableName}}}`; // 1단계: 클립보드 API 시도 if (navigator.clipboard && navigator.clipboard.writeText) { @@ -417,7 +410,7 @@ export function TemplateEditorWrapper({ {signatureVariables.length > 0 && ( <div> <p className="text-xs text-muted-foreground mb-2"> - 서명란 변수 (클릭하여 복사): + 서명란 텍스트 (클릭하여 복사): </p> <TooltipProvider> <div className="flex flex-wrap gap-1"> @@ -428,9 +421,34 @@ export function TemplateEditorWrapper({ variant="ghost" size="sm" className="h-6 px-2 text-xs hover:bg-green-50 border border-green-200" - onClick={() => insertVariable(variable)} + onClick={async () => { + // 서명란 텍스트는 {{}} 없이 그대로 복사 + const textToInsert = variable; + + // 클립보드에 복사 + if (navigator.clipboard && navigator.clipboard.writeText) { + try { + await navigator.clipboard.writeText(textToInsert); + toast.success(`"${textToInsert}"가 클립보드에 복사되었습니다.`, { + description: "문서에서 원하는 위치에 Ctrl+V로 붙여넣기 하세요." + }); + } catch (clipboardError) { + console.warn("클립보드 API 사용 실패:", clipboardError); + // 대안 방법 + const tempTextArea = document.createElement('textarea'); + tempTextArea.value = textToInsert; + tempTextArea.style.position = 'fixed'; + tempTextArea.style.left = '-9999px'; + document.body.appendChild(tempTextArea); + tempTextArea.select(); + document.execCommand('copy'); + document.body.removeChild(tempTextArea); + toast.success(`"${textToInsert}"가 클립보드에 복사되었습니다.`); + } + } + }} > - {`{{${variable}}}`} + {variable} </Button> </TooltipTrigger> <TooltipContent> @@ -451,23 +469,29 @@ export function TemplateEditorWrapper({ </p> <TooltipProvider> <div className="flex flex-wrap gap-1"> - {predefinedVariables.map((variable, index) => ( - <Tooltip key={index}> - <TooltipTrigger asChild> - <Button - variant="ghost" - size="sm" - className="h-6 px-2 text-xs hover:bg-blue-50" - onClick={() => insertVariable(variable)} - > - {`{{${variable}}}`} - </Button> - </TooltipTrigger> - <TooltipContent> - <p>{VARIABLE_DESCRIPTION_MAP[variable as keyof typeof VARIABLE_DESCRIPTION_MAP] || variable}</p> - </TooltipContent> - </Tooltip> - ))} + {predefinedVariables.map((variable, index) => { + // 서명란 텍스트는 {{}} 없이 표시 + const isSignatureText = variable === '협력업체_서명란' || variable === '삼성중공업_서명란'; + const displayText = isSignatureText ? variable : `{{${variable}}}`; + + return ( + <Tooltip key={index}> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="sm" + className={`h-6 px-2 text-xs ${isSignatureText ? 'hover:bg-green-50 border border-green-200' : 'hover:bg-blue-50'}`} + onClick={() => insertVariable(variable)} + > + {displayText} + </Button> + </TooltipTrigger> + <TooltipContent> + <p>{VARIABLE_DESCRIPTION_MAP[variable as keyof typeof VARIABLE_DESCRIPTION_MAP] || variable}</p> + </TooltipContent> + </Tooltip> + ); + })} </div> </TooltipProvider> </div> @@ -486,11 +510,10 @@ export function TemplateEditorWrapper({ instance={instance} setInstance={setInstance} onSignatureFieldFound={(searchText) => { - // 서명란 발견 시 변수명으로 변환하여 추가 - const variableName = getSignatureVariableName(searchText); + // 서명란 발견 시 원본 텍스트를 그대로 추가 (자동 서명 기능을 위해) setSignatureVariables(prev => { - if (!prev.includes(variableName)) { - return [...prev, variableName]; + if (!prev.includes(searchText)) { + return [...prev, searchText]; } return prev; }); |
