From a50bc9baea332f996e6bc3a5d70c69f6d2d0f194 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 23 Jul 2025 09:08:03 +0000
Subject: (대표님, 최겸) 기본계약 템플릿 및 에디터, 기술영업 벤더정보, 파일
 보안다운로드, 벤더 document sync 상태 서비스, 메뉴 Config, 기술영업 미사용
 제거
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../template/template-editor-wrapper.tsx           | 353 +++++++++++++++++++++
 1 file changed, 353 insertions(+)
 create mode 100644 lib/basic-contract/template/template-editor-wrapper.tsx

(limited to 'lib/basic-contract/template/template-editor-wrapper.tsx')

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
-- 
cgit v1.2.3

