summaryrefslogtreecommitdiff
path: root/lib/basic-contract/template/template-editor-wrapper.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-23 09:08:03 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-23 09:08:03 +0000
commita50bc9baea332f996e6bc3a5d70c69f6d2d0f194 (patch)
tree7493b8a4d9cc7cc3375068f1aa10b0067e85988f /lib/basic-contract/template/template-editor-wrapper.tsx
parent7402e759857d511add0d3eb19f1fa13cb957c1df (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.tsx353
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