summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-07 05:05:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-07 05:05:40 +0000
commit8daa2aeee017c642d2fd171094cf5d442966eb12 (patch)
tree289879c50cc8f1f28d44c129846d6b153ecacdac
parent7f973de2a814a2040a646161b563c8990b7e2fd2 (diff)
(임수민) 기본계약서 서명란 변수 수정
-rw-r--r--lib/basic-contract/template/basic-contract-template-viewer.tsx117
-rw-r--r--lib/basic-contract/template/template-editor-wrapper.tsx101
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;
});