summaryrefslogtreecommitdiff
path: root/lib/general-contract-template/template/template-editor-wrapper.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/general-contract-template/template/template-editor-wrapper.tsx')
-rw-r--r--lib/general-contract-template/template/template-editor-wrapper.tsx449
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>
+ );
+}
+
+