From a9575387c3a765a1a65ebc179dae16a21af6eb25 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 12 Sep 2025 08:01:02 +0000 Subject: (임수민) 일반 계약 템플릿 구현 및 basic contract 필터 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/template-editor-wrapper.tsx | 449 +++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 lib/general-contract-template/template/template-editor-wrapper.tsx (limited to 'lib/general-contract-template/template/template-editor-wrapper.tsx') diff --git a/lib/general-contract-template/template/template-editor-wrapper.tsx b/lib/general-contract-template/template/template-editor-wrapper.tsx new file mode 100644 index 00000000..992ed4e0 --- /dev/null +++ b/lib/general-contract-template/template/template-editor-wrapper.tsx @@ -0,0 +1,449 @@ +"use client"; + +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { Save, RefreshCw, Type, FileText, AlertCircle } from "lucide-react"; +import type { WebViewerInstance } from "@pdftron/webviewer"; +import { Badge } from "@/components/ui/badge"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { GeneralContractTemplateViewer } from "@/lib/general-contract-template/template/general-contract-template-viewer"; +import { getExistingTemplateNamesById, saveTemplateFile } from "@/lib/general-contract-template/service"; + +// 변수 패턴 감지를 위한 정규식 +const VARIABLE_PATTERN = /\{\{([^}]+)\}\}/g; + +const getVariablesForTemplate = (templateName: string): string[] => { + // 정확한 매치 먼저 확인 + if (TEMPLATE_VARIABLES_MAP[templateName as keyof typeof TEMPLATE_VARIABLES_MAP]) { + return [...TEMPLATE_VARIABLES_MAP[templateName as keyof typeof TEMPLATE_VARIABLES_MAP]]; + } + + // GTC가 포함된 경우 확인 + if (templateName.includes("GTC")) { + return [...TEMPLATE_VARIABLES_MAP["GTC"]]; + } + + // 다른 키워드들도 포함 관계로 확인 + for (const [key, variables] of Object.entries(TEMPLATE_VARIABLES_MAP)) { + if (templateName.includes(key)) { + return [...variables]; + } + } + + // 기본값 반환 (basic-contract와 동일) + return ["company_name", "company_address", "representative_name", "signature_date"]; +}; + +// 템플릿 이름별 변수 매핑 (basic-contract와 동일) +const TEMPLATE_VARIABLES_MAP = { + "준법서약 (한글)": ["company_name", "company_address", "representative_name", "signature_date"], + "준법서약 (영문)": ["company_name", "company_address", "representative_name", "signature_date"], + "기술자료 요구서": ["company_name", "company_address", "representative_name", "signature_date", 'tax_id', 'phone_number'], + "비밀유지 계약서": ["company_name", "company_address", "representative_name", "signature_date"], + "표준하도급기본 계약서": ["company_name", "company_address", "representative_name", "signature_date"], + "GTC": ["company_name", "company_address", "representative_name", "signature_date"], + "안전보건관리 약정서": ["company_name", "company_address", "representative_name", "signature_date"], + "동반성장": ["company_name", "company_address", "representative_name", "signature_date"], + "윤리규범 준수 서약서": ["company_name", "company_address", "representative_name", "signature_date"], + "기술자료 동의서": ["company_name", "company_address", "representative_name", "signature_date", 'tax_id', 'phone_number'], + "내국신용장 미개설 합의서": ["company_name", "company_address", "representative_name", "signature_date"], + "직납자재 하도급대급등 연동제 의향서": ["company_name", "company_address", "representative_name", "signature_date"] +} as const; + +// 변수별 한글 설명 매핑 (basic-contract와 동일) +const VARIABLE_DESCRIPTION_MAP = { + "company_name": "협력회사명", + "vendor_name": "협력회사명", + "company_address": "회사주소", + "address": "회사주소", + "representative_name": "대표자명", + "signature_date": "서명날짜", + "today_date": "오늘날짜", + "tax_id": "사업자등록번호", + "phone_number": "전화번호", + "phone": "전화번호", + "email": "이메일" +} as const; + +interface TemplateEditorWrapperProps { + templateId: string | number; + filePath: string | null; + fileName: string | null; + refreshAction?: () => Promise | void; +} + +export function TemplateEditorWrapper({ + templateId, + filePath, + fileName, + refreshAction +}: TemplateEditorWrapperProps) { + const [instance, setInstance] = React.useState(null); + const [isSaving, setIsSaving] = React.useState(false); + const [documentVariables, setDocumentVariables] = React.useState([]); + const [templateName, setTemplateName] = React.useState(""); + const [predefinedVariables, setPredefinedVariables] = React.useState([]); + + // 템플릿 이름 로드 및 변수 설정 + React.useEffect(() => { + const loadTemplateInfo = async () => { + try { + const name = await getExistingTemplateNamesById(Number(templateId)); + setTemplateName(name); + + // 템플릿 이름에 따른 변수 설정 + const variables = getVariablesForTemplate(name); + setPredefinedVariables([...variables]); + + console.log("🏷️ 템플릿 이름:", name); + console.log("📝 할당된 변수들:", variables); + } catch (error) { + console.error("템플릿 정보 로드 오류:", error); + // 기본 변수 설정 + setPredefinedVariables(["company_name", "company_address", "representative_name", "signature_date"]); + } + }; + + if (templateId) { + loadTemplateInfo(); + } + }, [templateId]); + + // 문서에서 변수 추출 + const extractVariablesFromDocument = async () => { + if (!instance) return; + + try { + const { documentViewer } = instance.Core; + const doc = documentViewer.getDocument(); + + if (!doc) return; + + // 문서 텍스트 추출 + const textContent = await doc.getDocumentCompletePromise().then(async () => { + const pageCount = doc.getPageCount(); + let fullText = ""; + + for (let i = 1; i <= pageCount; i++) { + try { + const pageText = await doc.loadPageText(i); + fullText += pageText + " "; + } catch (error) { + console.warn(`페이지 ${i} 텍스트 추출 실패:`, error); + } + } + + return fullText; + }); + + // 변수 패턴 매칭 + const matches = textContent.match(VARIABLE_PATTERN); + const variables = matches + ? [...new Set(matches.map(match => match.replace(/[{}]/g, '')))] + : []; + + setDocumentVariables(variables); + + if (variables.length > 0) { + console.log("🔍 발견된 변수들:", variables); + } + + } catch (error) { + console.error("변수 추출 중 오류:", error); + } + }; + + // 인스턴스가 변경될 때마다 변수 추출 + React.useEffect(() => { + if (instance) { + // 문서 로드 완료 이벤트 리스너 추가 + const { documentViewer } = instance.Core; + + const onDocumentLoaded = () => { + setTimeout(() => extractVariablesFromDocument(), 1000); + }; + + documentViewer.addEventListener("documentLoaded", onDocumentLoaded); + + return () => { + documentViewer.removeEventListener("documentLoaded", onDocumentLoaded); + }; + } + }, [instance]); + + const handleSave = async () => { + if (!instance) { + toast.error("뷰어가 준비되지 않았습니다."); + return; + } + + try { + setIsSaving(true); + const { documentViewer } = instance.Core; + const doc = documentViewer.getDocument(); + if (!doc) throw new Error("문서를 찾을 수 없습니다."); + + const data = await doc.getFileData({ + downloadType: "office", + includeAnnotations: true, + }); + + const formData = new FormData(); + formData.append( + "file", + new Blob([data], { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }), + fileName ?? "document.docx" + ); + + const result = await saveTemplateFile(Number(templateId), formData); + if ((result as any)?.error) throw new Error((result as any).error); + toast.success("템플릿이 성공적으로 저장되었습니다."); + + // 변수 재추출 + await extractVariablesFromDocument(); + + if (refreshAction) await refreshAction(); + } catch (err) { + console.error(err); + toast.error(err instanceof Error ? err.message : "저장 중 오류가 발생했습니다."); + } finally { + setIsSaving(false); + } + }; + + // 변수 삽입 함수 (한글 입력 제한 고려) + const insertVariable = async (variableName: string) => { + if (!instance) { + toast.error("뷰어가 준비되지 않았습니다."); + return; + } + + try { + const textToInsert = `{{${variableName}}}`; + + // 1단계: 클립보드 API 시도 + if (navigator.clipboard && navigator.clipboard.writeText) { + try { + await navigator.clipboard.writeText(textToInsert); + toast.success(`변수 "${textToInsert}"가 클립보드에 복사되었습니다.`, { + description: "문서에서 원하는 위치에 Ctrl+V로 붙여넣기 하세요." + }); + return; + } catch (clipboardError) { + console.warn("클립보드 API 사용 실패:", clipboardError); + } + } + + // 2단계: Office 편집기에 직접 삽입 시도 (실험적) + try { + const { documentViewer } = instance.Core; + const doc = documentViewer.getDocument(); + + if (doc && typeof doc.getOfficeEditor === 'function') { + const officeEditor = doc.getOfficeEditor(); + if (officeEditor && typeof (officeEditor as any).insertText === 'function') { + await (officeEditor as any).insertText(textToInsert); + toast.success(`변수 "${textToInsert}"가 문서에 삽입되었습니다.`); + return; + } + } + } catch (insertError) { + console.warn("직접 삽입 실패:", insertError); + } + + // 3단계: 임시 텍스트 영역을 통한 복사 (대안) + try { + const textArea = document.createElement('textarea'); + textArea.value = textToInsert; + textArea.style.position = 'fixed'; + textArea.style.left = '-9999px'; + textArea.style.top = '-9999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + const successful = document.execCommand('copy'); + document.body.removeChild(textArea); + + if (successful) { + toast.success(`변수 "${textToInsert}"가 클립보드에 복사되었습니다.`, { + description: "문서에서 원하는 위치에 Ctrl+V로 붙여넣기 하세요." + }); + } else { + throw new Error("복사 명령 실행 실패"); + } + } catch (fallbackError) { + console.error("모든 복사 방법 실패:", fallbackError); + toast.error("변수 복사에 실패했습니다. 수동으로 입력해주세요."); + } + } catch (error) { + console.error("변수 삽입 실패:", error); + toast.error("변수 삽입에 실패했습니다."); + } + }; + + if (!filePath || !fileName) { + return ( +
+ 첨부파일이 없습니다. +
+ ); + } + + // 문서 새로고침 + const handleRefresh = () => { + window.location.reload(); + }; + + return ( +
+ {/* 상단 도구 모음 */} +
+
+
+ + + +
+ +
+ + + {fileName} + + {templateName && ( + + + {templateName} + + )} + {documentVariables.length > 0 && ( + + + 변수 {documentVariables.length}개 + + )} +
+
+ + {/* 변수 도구 */} + {(documentVariables.length > 0 || predefinedVariables.length > 0) && ( +
+
+

+ + 변수 관리 + {templateName && ( + + ({templateName}) + + )} +

+
+ +
+ {/* 발견된 변수들 */} + {documentVariables.length > 0 && ( +
+

문서에서 발견된 변수:

+
+ {documentVariables.map((variable, index) => ( + + {`{{${variable}}}`} + + ))} +
+
+ )} + + {/* 템플릿별 미리 정의된 변수들 */} + {predefinedVariables.length > 0 && ( +
+

+ {templateName ? `${templateName}에 권장되는 변수` : "자주 사용하는 변수"} (클릭하여 복사): +

+ +
+ {predefinedVariables.map((variable, index) => ( + + + + + +

{VARIABLE_DESCRIPTION_MAP[variable as keyof typeof VARIABLE_DESCRIPTION_MAP] || variable}

+
+
+ ))} +
+
+
+ )} +
+
+ )} +
+ + {/* 뷰어 영역 (확대 문제 해결을 위한 컨테이너 격리) */} +
+
+ +
+
+ + {/* 하단 안내 (한글 입력 팁 포함) */} +
+
+
+ + {'{{변수명}}'} 형식으로 변수를 삽입하면 계약서 생성 시 실제 값으로 치환됩니다. +
+
+ + 한글 입력 제한시 외부 에디터에서 작성 후 복사-붙여넣기를 사용하세요. +
+
+
+
+ ); +} + + -- cgit v1.2.3