diff options
Diffstat (limited to 'lib/general-contract-template/template/template-editor-wrapper.tsx')
| -rw-r--r-- | lib/general-contract-template/template/template-editor-wrapper.tsx | 449 |
1 files changed, 449 insertions, 0 deletions
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> | void; +} + +export function TemplateEditorWrapper({ + templateId, + filePath, + fileName, + refreshAction +}: TemplateEditorWrapperProps) { + const [instance, setInstance] = React.useState<WebViewerInstance | null>(null); + const [isSaving, setIsSaving] = React.useState(false); + const [documentVariables, setDocumentVariables] = React.useState<string[]>([]); + const [templateName, setTemplateName] = React.useState<string>(""); + const [predefinedVariables, setPredefinedVariables] = React.useState<string[]>([]); + + // 템플릿 이름 로드 및 변수 설정 + 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 ( + <div className="h-full w-full flex items-center justify-center text-muted-foreground"> + 첨부파일이 없습니다. + </div> + ); + } + + // 문서 새로고침 + const handleRefresh = () => { + window.location.reload(); + }; + + return ( + <div className="h-full flex flex-col"> + {/* 상단 도구 모음 */} + <div className="border-b bg-gray-50 p-3"> + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-2"> + <Button + variant="outline" + size="sm" + onClick={handleSave} + disabled={isSaving || !instance} + > + {isSaving ? ( + <> + <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> + 저장 중... + </> + ) : ( + <> + <Save className="mr-2 h-4 w-4" /> + 저장 + </> + )} + </Button> + + <Button + variant="outline" + size="sm" + onClick={handleRefresh} + > + <RefreshCw className="mr-2 h-4 w-4" /> + 새로고침 + </Button> + </div> + + <div className="flex items-center space-x-2"> + <Badge variant="outline"> + <FileText className="mr-1 h-3 w-3" /> + {fileName} + </Badge> + {templateName && ( + <Badge variant="secondary"> + <Type className="mr-1 h-3 w-3" /> + {templateName} + </Badge> + )} + {documentVariables.length > 0 && ( + <Badge variant="secondary"> + <Type className="mr-1 h-3 w-3" /> + 변수 {documentVariables.length}개 + </Badge> + )} + </div> + </div> + + {/* 변수 도구 */} + {(documentVariables.length > 0 || predefinedVariables.length > 0) && ( + <div className="mt-3 p-3 bg-white rounded border"> + <div className="mb-2"> + <h4 className="text-sm font-medium flex items-center"> + <Type className="mr-2 h-4 w-4 text-blue-500" /> + 변수 관리 + {templateName && ( + <span className="ml-2 text-xs text-muted-foreground"> + ({templateName}) + </span> + )} + </h4> + </div> + + <div className="space-y-3"> + {/* 발견된 변수들 */} + {documentVariables.length > 0 && ( + <div> + <p className="text-xs text-muted-foreground mb-2">문서에서 발견된 변수:</p> + <div className="flex flex-wrap gap-1"> + {documentVariables.map((variable, index) => ( + <Badge key={index} variant="outline" className="text-xs"> + {`{{${variable}}}`} + </Badge> + ))} + </div> + </div> + )} + + {/* 템플릿별 미리 정의된 변수들 */} + {predefinedVariables.length > 0 && ( + <div> + <p className="text-xs text-muted-foreground mb-2"> + {templateName ? `${templateName}에 권장되는 변수` : "자주 사용하는 변수"} (클릭하여 복사): + </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> + ))} + </div> + </TooltipProvider> + </div> + )} + </div> + </div> + )} + </div> + + {/* 뷰어 영역 (확대 문제 해결을 위한 컨테이너 격리) */} + <div className="flex-1 relative overflow-hidden"> + <div className="absolute inset-0"> + <GeneralContractTemplateViewer + templateId={typeof templateId === 'string' ? parseInt(templateId) : templateId} + filePath={filePath} + instance={instance} + setInstance={setInstance} + /> + </div> + </div> + + {/* 하단 안내 (한글 입력 팁 포함) */} + <div className="border-t bg-gray-50 p-2"> + <div className="flex items-center justify-between text-xs text-muted-foreground"> + <div className="flex items-center"> + <AlertCircle className="inline h-3 w-3 mr-1" /> + {'{{변수명}}'} 형식으로 변수를 삽입하면 계약서 생성 시 실제 값으로 치환됩니다. + </div> + <div className="flex items-center text-orange-600"> + <AlertCircle className="inline h-3 w-3 mr-1" /> + 한글 입력 제한시 외부 에디터에서 작성 후 복사-붙여넣기를 사용하세요. + </div> + </div> + </div> + </div> + ); +} + + |
