diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-23 09:08:03 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-23 09:08:03 +0000 |
| commit | a50bc9baea332f996e6bc3a5d70c69f6d2d0f194 (patch) | |
| tree | 7493b8a4d9cc7cc3375068f1aa10b0067e85988f /lib/basic-contract/template/template-editor-wrapper.tsx | |
| parent | 7402e759857d511add0d3eb19f1fa13cb957c1df (diff) | |
(대표님, 최겸) 기본계약 템플릿 및 에디터, 기술영업 벤더정보, 파일 보안다운로드, 벤더 document sync 상태 서비스, 메뉴 Config, 기술영업 미사용 제거
Diffstat (limited to 'lib/basic-contract/template/template-editor-wrapper.tsx')
| -rw-r--r-- | lib/basic-contract/template/template-editor-wrapper.tsx | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/lib/basic-contract/template/template-editor-wrapper.tsx b/lib/basic-contract/template/template-editor-wrapper.tsx new file mode 100644 index 00000000..ea353b91 --- /dev/null +++ b/lib/basic-contract/template/template-editor-wrapper.tsx @@ -0,0 +1,353 @@ +"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 { BasicContractTemplateViewer } from "./basic-contract-template-viewer"; +import { saveTemplateFile } from "../service"; + +interface TemplateEditorWrapperProps { + templateId: string | number; + filePath: string; + fileName: string; + refreshAction?: () => Promise<void>; +} + +// 변수 패턴 감지를 위한 정규식 +const VARIABLE_PATTERN = /\{\{([^}]+)\}\}/g; + +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 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 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.insertText === 'function') { + await officeEditor.insertText(textToInsert); + toast.success(`변수 "${textToInsert}"가 문서에 삽입되었습니다.`); + return; + } + } + } catch (insertError) { + console.warn("직접 삽입 실패:", insertError); + } + + // 3단계: 임시 텍스트 영역을 통한 복사 (대안) + try { + 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}"가 클립보드에 복사되었습니다.`, { + description: "Office 편집기에서 한글 입력이 제한될 수 있습니다. 외부에서 작성 후 붙여넣기를 권장합니다." + }); + return; + } catch (fallbackError) { + console.warn("대안 복사 실패:", fallbackError); + } + + // 4단계: 수동 입력 안내 + toast.info(`다음 변수를 수동으로 입력해주세요: ${textToInsert}`, { + description: "한글과 변수는 외부 에디터에서 작성 후 복사-붙여넣기를 권장합니다.", + duration: 8000, + }); + + } catch (error) { + console.error("변수 삽입 오류:", error); + toast.error("변수 삽입 중 오류가 발생했습니다."); + } + }; + + // 문서 저장 + const handleSave = async () => { + if (!instance) { + toast.error("뷰어가 준비되지 않았습니다."); + return; + } + + setIsSaving(true); + try { + const { documentViewer } = instance.Core; + const doc = documentViewer.getDocument(); + + if (!doc) { + throw new Error("문서를 찾을 수 없습니다."); + } + + // Word 문서 저장 + const data = await doc.getFileData({ + downloadType: "office", // Word 파일로 저장 + includeAnnotations: true + }); + + // FormData 생성하여 서버 액션에 전달 + const formData = new FormData(); + formData.append('file', new Blob([data], { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }), fileName); + + // 서버 액션 호출 + const result = await saveTemplateFile(Number(templateId), formData); + + if (result.error) { + throw new Error(result.error); + } + + toast.success("템플릿이 성공적으로 저장되었습니다."); + + // 변수 재추출 + await extractVariablesFromDocument(); + + // 페이지 새로고침 (서버 액션) + if (refreshAction) { + await refreshAction(); + } + + } catch (error) { + console.error("저장 오류:", error); + toast.error(error instanceof Error ? error.message : "저장 중 오류가 발생했습니다."); + } finally { + setIsSaving(false); + } + }; + + // 문서 새로고침 + const handleRefresh = () => { + window.location.reload(); + }; + + // 미리 정의된 변수들 + const predefinedVariables = [ + "회사명", "계약자명", "계약일자", "계약금액", + "납기일자", "담당자명", "담당자연락처", "프로젝트명", + "계약번호", "사업부", "부서명", "승인자명" + ]; + + 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> + {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" /> + 변수 관리 + </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> + )} + + {/* 미리 정의된 변수들 */} + <div> + <p className="text-xs text-muted-foreground mb-2">자주 사용하는 변수 (클릭하여 복사):</p> + <div className="flex flex-wrap gap-1"> + {predefinedVariables.map((variable, index) => ( + <Button + key={index} + variant="ghost" + size="sm" + className="h-6 px-2 text-xs" + onClick={() => insertVariable(variable)} + > + {`{{${variable}}}`} + </Button> + ))} + </div> + </div> + </div> + </div> + )} + </div> + + {/* 뷰어 영역 (확대 문제 해결을 위한 컨테이너 격리) */} + <div className="flex-1 relative overflow-hidden"> + <div className="absolute inset-0"> + <BasicContractTemplateViewer + 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> + ); +}
\ No newline at end of file |
