summaryrefslogtreecommitdiff
path: root/lib/basic-contract/viewer
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/viewer')
-rw-r--r--lib/basic-contract/viewer/basic-contract-sign-viewer.tsx2975
1 files changed, 1674 insertions, 1301 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
index b92df089..fbf36738 100644
--- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
+++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
@@ -1,11 +1,13 @@
"use client";
import React, {
-useState,
-useEffect,
-useRef,
-SetStateAction,
-Dispatch,
+ useState,
+ useEffect,
+ useRef,
+ SetStateAction,
+ Dispatch,
+ useMemo,
+ useCallback,
} from "react";
import { WebViewerInstance } from "@pdftron/webviewer";
import { Loader2, FileText, ClipboardList, AlertTriangle, FileSignature, Target, CheckCircle2 } from "lucide-react";
@@ -15,43 +17,55 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
-Dialog,
-DialogContent,
-DialogHeader,
-DialogTitle,
-DialogDescription,
-DialogFooter,
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
-import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Upload } from "lucide-react";
-
-
+import { CompleteSurveyRequest, SurveyAnswerData, completeSurvey, getActiveSurveyTemplate, type SurveyTemplateWithQuestions } from '../service';
+import { ConditionalSurveyHandler, useConditionalSurvey } from '../vendor-table/survey-conditional';
+import { useForm, useWatch, Controller } from "react-hook-form";
interface FileInfo {
-path: string;
-name: string;
-type: 'main' | 'attachment' | 'survey';
+ path: string;
+ name: string;
+ type: 'main' | 'attachment' | 'survey';
}
interface BasicContractSignViewerProps {
-contractId?: number;
-filePath?: string;
-additionalFiles?: FileInfo[];
-templateName?: string;
-isOpen?: boolean;
-onClose?: () => void;
-onSign?: (documentData: ArrayBuffer, surveyData?: any) => Promise<void>;
-instance: WebViewerInstance | null;
-setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
-t?: (key: string) => string;
+ contractId?: number;
+ filePath?: string;
+ additionalFiles?: FileInfo[];
+ templateName?: string;
+ isOpen?: boolean;
+ onClose?: () => void;
+ onSign?: (documentData: ArrayBuffer, surveyData?: any) => Promise<void>;
+ instance: WebViewerInstance | null;
+ setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
+ onSurveyComplete?: () => void; // πŸ”₯ μƒˆλ‘œ μΆ”κ°€
+ onSignatureComplete?: () => void; // πŸ”₯ μƒˆλ‘œ μΆ”κ°€
+ t?: (key: string) => string;
+}
+
+// 폼 데이터 νƒ€μž… μ •μ˜
+interface SurveyFormData {
+ [key: string]: {
+ answerValue?: string;
+ detailText?: string;
+ otherText?: string;
+ files?: File[];
+ };
}
-// βœ… μžλ™ μ„œλͺ… ν•„λ“œ 생성을 μœ„ν•œ νƒ€μž… μ •μ˜
+// μžλ™ μ„œλͺ… ν•„λ“œ 생성을 μœ„ν•œ νƒ€μž… μ •μ˜
interface SignaturePattern {
regex: RegExp;
name: string;
@@ -62,22 +76,7 @@ interface SignaturePattern {
height?: number;
}
-interface DetectedSignatureLocation {
- pageIndex: number;
- text: string;
- rect: {
- x1: number;
- y1: number;
- x2: number;
- y2: number;
- };
- pattern: SignaturePattern;
- confidence: number;
-}
-
-// βœ… κ°œμ„ λœ μžλ™ μ„œλͺ… ν•„λ“œ 감지 클래슀
-
-// βœ… μ΄ˆκ°„λ‹¨ μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 감지 클래슀 (μƒˆλ‘œκ³ μΉ¨ 제거)
+// μ΄ˆκ°„λ‹¨ μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 감지 클래슀
class AutoSignatureFieldDetector {
private instance: WebViewerInstance;
private signaturePatterns: SignaturePattern[];
@@ -89,7 +88,6 @@ class AutoSignatureFieldDetector {
private initializePatterns(): SignaturePattern[] {
return [
- // ν•œκ΅­μ–΄ νŒ¨ν„΄λ“€
{
regex: /μ„œλͺ…\s*[::]\s*[_\-\s]{3,}/gi,
name: "ν•œκ΅­μ–΄_μ„œλͺ…_콜둠",
@@ -108,7 +106,6 @@ class AutoSignatureFieldDetector {
width: 150,
height: 40
},
- // μ˜μ–΄ νŒ¨ν„΄λ“€
{
regex: /signature\s*[::]\s*[_\-\s]{3,}/gi,
name: "μ˜μ–΄_signature_콜둠",
@@ -132,37 +129,29 @@ class AutoSignatureFieldDetector {
async detectAndCreateSignatureFields(): Promise<string[]> {
console.log("πŸ” μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 감지 μ‹œμž‘...");
-
+
try {
- // βœ… 1단계: κΈ°λ³Έ μœ νš¨μ„± κ²€μ‚¬λ§Œ
if (!this.instance?.Core?.documentViewer) {
throw new Error("WebViewer μΈμŠ€ν„΄μŠ€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
}
const { Core } = this.instance;
const { documentViewer } = Core;
-
- // βœ… 2단계: λ¬Έμ„œ 쑴재 ν™•μΈλ§Œ (getPDFDoc 호좜 μ•ˆν•¨)
+
const document = documentViewer.getDocument();
if (!document) {
throw new Error("PDF λ¬Έμ„œκ°€ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.");
}
- console.log("πŸ“„ λ¬Έμ„œ 확인 μ™„λ£Œ, κΈ°μ‘΄ ν•„λ“œ 검사...");
-
-
- // βœ… 4단계: λ‹¨μˆœ κΈ°λ³Έ μ„œλͺ… ν•„λ“œ 생성 (ν…μŠ€νŠΈ 뢄석 μŠ€ν‚΅)
- console.log("πŸ“ κΈ°λ³Έ μ„œλͺ… ν•„λ“œ 생성...");
+ console.log("πŸ“„ λ¬Έμ„œ 확인 μ™„λ£Œ, κΈ°λ³Έ μ„œλͺ… ν•„λ“œ 생성...");
const defaultField = await this.createSimpleSignatureField();
-
- // βœ… 5단계: μƒˆλ‘œκ³ μΉ¨ 없이 μ™„λ£Œ
- console.log("βœ… μ„œλͺ… ν•„λ“œ 생성 μ™„λ£Œ (μƒˆλ‘œκ³ μΉ¨ μŠ€ν‚΅)");
+
+ console.log("βœ… μ„œλͺ… ν•„λ“œ 생성 μ™„λ£Œ");
return [defaultField];
} catch (error) {
- console.error("πŸ“› μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨:", error);
-
- // μ—λŸ¬ νƒ€μž…λ³„ λ©”μ‹œμ§€
+ console.error("πŸ“› μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨:", error);
+
let errorMessage = "μ„œλͺ… ν•„λ“œ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.";
if (error instanceof Error) {
if (error.message.includes("μΈμŠ€ν„΄μŠ€")) {
@@ -171,35 +160,24 @@ class AutoSignatureFieldDetector {
errorMessage = "λ¬Έμ„œλ₯Ό λΆˆλŸ¬μ˜€λŠ” μ€‘μž…λ‹ˆλ‹€.";
}
}
-
+
throw new Error(errorMessage);
}
}
-
-
- // βœ… μ΄ˆκ°„λ‹¨ μ„œλͺ… ν•„λ“œ 생성 (λ³΅μž‘ν•œ ν…μŠ€νŠΈ 뢄석 없이)
private async createSimpleSignatureField(): Promise<string> {
try {
- const { Core, UI } = this.instance;
+ const { Core } = this.instance;
const { documentViewer, annotationManager, Annotations } = Core;
-
- // νŽ˜μ΄μ§€ 정보 μ•ˆμ „ν•˜κ²Œ κ°€μ Έμ˜€κΈ°
+
const pageCount = documentViewer.getPageCount();
- const lastPageIndex = Math.max(0, pageCount - 1);
-
- // νŽ˜μ΄μ§€ 크기 μ•ˆμ „ν•˜κ²Œ κ°€μ Έμ˜€κΈ°
const pageWidth = documentViewer.getPageWidth(pageCount) || 612;
const pageHeight = documentViewer.getPageHeight(pageCount) || 792;
-
+
console.log(`πŸ“ νŽ˜μ΄μ§€ 정보: ${pageCount}νŽ˜μ΄μ§€, 크기 ${pageWidth}x${pageHeight}`);
-
- // βœ… κ°„λ‹¨ν•œ μ„œλͺ… μ–΄λ…Έν…Œμ΄μ…˜ 생성 (PDFDoc μ ‘κ·Ό 없이)
- const fieldName = `simple_signature_${Date.now()}`;
+ const fieldName = `simple_signature_${Date.now()}`;
const flags = new Annotations.WidgetFlags();
- // flags.set(Annotations.WidgetFlags.REQUIRED, true);
- // flags.set(Annotations.WidgetFlags.READ_ONLY, true);
const formField = new Core.Annotations.Forms.Field(
`SignatureFormField`,
@@ -208,89 +186,36 @@ class AutoSignatureFieldDetector {
flags,
}
);
-
- // μ„œλͺ… μœ„μ ― μ–΄λ…Έν…Œμ΄μ…˜ 생성
- const signatureWidget = new Annotations.SignatureWidgetAnnotation(formField,{
- // appearance: Annotations.SignatureWidgetAnnotation.DefaultAppearance.MATERIAL_OUTLINE,
+
+ const signatureWidget = new Annotations.SignatureWidgetAnnotation(formField, {
Width: 150,
Height: 50
});
-
- // μœ„μΉ˜ μ„€μ • (λ§ˆμ§€λ§‰ νŽ˜μ΄μ§€ ν•˜λ‹¨)
+
signatureWidget.setPageNumber(pageCount);
signatureWidget.setX(pageWidth * 0.7);
signatureWidget.setY(pageHeight * 0.85);
signatureWidget.setWidth(150);
signatureWidget.setHeight(50);
-
- // ν•„λ“œλͺ… μ„€μ •
- // signatureWidget.setFieldName(fieldName);
- // signatureWidget.setCustomData('fieldName', fieldName);
-
- // // μŠ€νƒ€μΌ μ„€μ •
- // signatureWidget.StrokeColor = new Annotations.Color(0, 100, 200); // νŒŒλž€μƒ‰
- // signatureWidget.StrokeThickness = 2;
-
- // μ–΄λ…Έν…Œμ΄μ…˜ μΆ”κ°€
+
annotationManager.addAnnotation(signatureWidget);
annotationManager.redrawAnnotation(signatureWidget);
-
- console.log(`βœ… 간단 μ„œλͺ… ν•„λ“œ 생성: ${fieldName}`);
+
+ console.log(`βœ… μ„œλͺ… ν•„λ“œ 생성: ${fieldName}`);
return fieldName;
-
+
} catch (error) {
- console.error("πŸ“› 간단 μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨:", error);
-
- // βœ… μ΅œν›„μ˜ μˆ˜λ‹¨: ν…μŠ€νŠΈ μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ μ•ˆλ‚΄
- // return await this.createTextGuidance();
+ console.error("πŸ“› μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨:", error);
+ return "manual_signature_required";
}
}
-
- // βœ… μ΅œν›„μ˜ μˆ˜λ‹¨: ν…μŠ€νŠΈ μ•ˆλ‚΄ 생성
- // private async createTextGuidance(): Promise<string> {
- // try {
- // const { Core } = this.instance;
- // const { documentViewer, annotationManager, Annotations } = Core;
-
- // const pageCount = documentViewer.getPageCount();
- // const pageWidth = documentViewer.getPageWidth(pageCount) || 612;
- // const pageHeight = documentViewer.getPageHeight(pageCount) || 792;
-
- // // ν…μŠ€νŠΈ μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ μ„œλͺ… μ•ˆλ‚΄
- // const textAnnot = new Annotations.FreeTextAnnotation();
- // textAnnot.setPageNumber(pageCount);
- // textAnnot.setX(pageWidth * 0.25);
- // textAnnot.setY(pageHeight * 0.1);
- // textAnnot.setWidth(pageWidth * 0.5);
- // textAnnot.setHeight(60);
- // textAnnot.setContents("πŸ‘† μ—¬κΈ°λ₯Ό ν΄λ¦­ν•˜μ—¬ μ„œλͺ…ν•΄μ£Όμ„Έμš”");
- // textAnnot.FontSize = '14pt';
- // textAnnot.TextColor = new Annotations.Color(255, 0, 0); // 빨간색
- // textAnnot.StrokeColor = new Annotations.Color(255, 200, 200);
- // textAnnot.FillColor = new Annotations.Color(255, 240, 240);
-
- // const fieldName = `text_guidance_${Date.now()}`;
- // textAnnot.setCustomData('fieldName', fieldName);
-
- // annotationManager.addAnnotation(textAnnot);
- // annotationManager.redrawAnnotation(textAnnot);
-
- // console.log(`βœ… ν…μŠ€νŠΈ μ•ˆλ‚΄ 생성: ${fieldName}`);
- // return fieldName;
-
- // } catch (error) {
- // console.error("πŸ“› ν…μŠ€νŠΈ μ•ˆλ‚΄ 생성도 μ‹€νŒ¨:", error);
- // return "manual_signature_required";
- // }
- // }
}
function useAutoSignatureFields(instance: WebViewerInstance | null) {
const [signatureFields, setSignatureFields] = useState<string[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
-
- // 쀑볡 μ‹€ν–‰ λ°©μ§€
+
const processingRef = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -298,65 +223,46 @@ function useAutoSignatureFields(instance: WebViewerInstance | null) {
if (!instance) return;
const { documentViewer } = instance.Core;
-
+
const handleDocumentLoaded = () => {
- // βœ… 쀑볡 μ‹€ν–‰ λ°©μ§€
if (processingRef.current) {
console.log("πŸ“› 이미 처리 μ€‘μ΄λ―€λ‘œ μŠ€ν‚΅");
return;
}
- // βœ… κΈ°μ‘΄ 타이머 정리
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
- // βœ… 짧은 μ§€μ—° ν›„ μ‹€ν–‰ (3초)
timeoutRef.current = setTimeout(async () => {
if (processingRef.current) return;
-
+
processingRef.current = true;
setIsProcessing(true);
setError(null);
-
+
try {
- console.log("πŸ“„ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ, μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 처리 μ‹œμž‘...");
-
- // βœ… μ΅œμ’… μœ νš¨μ„± 검사
+ console.log("πŸ“„ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ, μ„œλͺ… ν•„λ“œ 처리 μ‹œμž‘...");
+
if (!instance?.Core?.documentViewer?.getDocument()) {
throw new Error("λ¬Έμ„œκ°€ μ€€λΉ„λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.");
}
const detector = new AutoSignatureFieldDetector(instance);
const fields = await detector.detectAndCreateSignatureFields();
-
+
setSignatureFields(fields);
-
- // βœ… 결과에 λ”°λ₯Έ ν† μŠ€νŠΈ λ©”μ‹œμ§€
+
if (fields.length > 0) {
const hasSimpleField = fields.some(field => field.startsWith('simple_signature_'));
- const hasTextGuidance = fields.some(field => field.startsWith('text_guidance_'));
- const hasManualRequired = fields.includes('manual_signature_required');
-
+
if (hasSimpleField) {
toast.success("πŸ“ μ„œλͺ… ν•„λ“œκ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", {
description: "λ§ˆμ§€λ§‰ νŽ˜μ΄μ§€ ν•˜λ‹¨μ˜ νŒŒλž€μƒ‰ μ˜μ—­μ—μ„œ μ„œλͺ…ν•΄μ£Όμ„Έμš”.",
icon: <FileSignature className="h-4 w-4 text-blue-500" />,
duration: 5000
});
- } else if (hasTextGuidance) {
- toast.success("πŸ“ μ„œλͺ… μ•ˆλ‚΄κ°€ ν‘œμ‹œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.", {
- description: "빨간색 ν…μŠ€νŠΈ μ˜μ—­μ„ ν΄λ¦­ν•˜μ—¬ μ„œλͺ…ν•΄μ£Όμ„Έμš”.",
- icon: <Target className="h-4 w-4 text-red-500" />,
- duration: 6000
- });
- } else if (hasManualRequired) {
- toast.info("μˆ˜λ™ μ„œλͺ…이 ν•„μš”ν•©λ‹ˆλ‹€.", {
- description: "λ¬Έμ„œμ—μ„œ μ„œλͺ…ν•  μœ„μΉ˜λ₯Ό 직접 ν΄λ¦­ν•΄μ£Όμ„Έμš”.",
- icon: <AlertTriangle className="h-4 w-4 text-amber-500" />,
- duration: 5000
- });
} else {
toast.success(`πŸ“‹ ${fields.length}개의 μ„œλͺ… ν•„λ“œλ₯Ό ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.`, {
description: "κΈ°μ‘΄ μ„œλͺ… ν•„λ“œκ°€ λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.",
@@ -364,56 +270,40 @@ function useAutoSignatureFields(instance: WebViewerInstance | null) {
duration: 4000
});
}
- } else {
- toast.info("μ„œλͺ… ν•„λ“œ μ€€λΉ„ 쀑", {
- description: "λ¬Έμ„œμ—μ„œ μ„œλͺ…ν•  μœ„μΉ˜λ₯Ό ν΄λ¦­ν•΄μ£Όμ„Έμš”.",
- icon: <FileSignature className="h-4 w-4 text-blue-500" />,
- duration: 4000
- });
}
-
+
} catch (error) {
- console.error("πŸ“› μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 처리 μ‹€νŒ¨:", error);
-
+ console.error("πŸ“› μ„œλͺ… ν•„λ“œ 처리 μ‹€νŒ¨:", error);
+
const errorMessage = error instanceof Error ? error.message : "μ„œλͺ… ν•„λ“œ μ²˜λ¦¬μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.";
setError(errorMessage);
-
- // βœ… λΆ€λ“œλŸ¬μš΄ μ—λŸ¬ 처리
- if (errorMessage.includes("μ€€λΉ„")) {
- toast.info("λ¬Έμ„œ λ‘œλ”© 쀑", {
- description: "μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•˜κ±°λ‚˜ μˆ˜λ™μœΌλ‘œ μ„œλͺ…ν•΄μ£Όμ„Έμš”.",
- icon: <Loader2 className="h-4 w-4 text-blue-500" />
- });
- } else {
- toast.info("μˆ˜λ™ μ„œλͺ… λͺ¨λ“œ", {
- description: "λ¬Έμ„œμ—μ„œ μ„œλͺ…ν•  μœ„μΉ˜λ₯Ό 직접 ν΄λ¦­ν•΄μ£Όμ„Έμš”.",
- icon: <FileSignature className="h-4 w-4 text-blue-500" />
- });
- }
+
+ toast.info("μˆ˜λ™ μ„œλͺ… λͺ¨λ“œ", {
+ description: "λ¬Έμ„œμ—μ„œ μ„œλͺ…ν•  μœ„μΉ˜λ₯Ό 직접 ν΄λ¦­ν•΄μ£Όμ„Έμš”.",
+ icon: <FileSignature className="h-4 w-4 text-blue-500" />
+ });
} finally {
setIsProcessing(false);
processingRef.current = false;
}
- }, 3000); // 3초 μ§€μ—°
+ }, 3000);
};
- // βœ… 이벀트 λ¦¬μŠ€λ„ˆ 등둝
documentViewer.removeEventListener('documentLoaded', handleDocumentLoaded);
documentViewer.addEventListener('documentLoaded', handleDocumentLoaded);
return () => {
documentViewer.removeEventListener('documentLoaded', handleDocumentLoaded);
-
+
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
-
+
processingRef.current = false;
};
}, [instance]);
- // βœ… μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ 정리
useEffect(() => {
return () => {
if (timeoutRef.current) {
@@ -431,1083 +321,1574 @@ function useAutoSignatureFields(instance: WebViewerInstance | null) {
};
}
-export function BasicContractSignViewer({
-contractId,
-filePath,
-additionalFiles = [],
-templateName = "",
-isOpen = false,
-onClose,
-onSign,
-instance,
-setInstance,
-t = (key: string) => key,
-}: BasicContractSignViewerProps) {
+// πŸ”₯ μ„œλͺ… 감지λ₯Ό μœ„ν•œ μ»€μŠ€ν…€ ν›… μˆ˜μ •
+function useSignatureDetection(instance: WebViewerInstance | null, onSignatureComplete?: () => void) {
+ const [hasValidSignature, setHasValidSignature] = useState(false);
+ const checkIntervalRef = useRef<NodeJS.Timeout | null>(null);
+ const lastSignatureStateRef = useRef(false);
+ const onSignatureCompleteRef = useRef(onSignatureComplete);
- console.log("πŸ” BasicContractSignViewer props:", {
- contractId,
- filePath,
- additionalFiles,
- templateName,
- isNDATemplate: templateName.includes('λΉ„λ°€μœ μ§€') || templateName.includes('NDA')
- });
-
-const [fileLoading, setFileLoading] = useState<boolean>(true);
-const [activeTab, setActiveTab] = useState<string>("main");
-const [surveyData, setSurveyData] = useState<any>({});
-const [surveyAnswers, setSurveyAnswers] = useState<Record<number, any>>({});
-const [surveyTemplate, setSurveyTemplate] = useState<any>(null);
-const [surveyLoading, setSurveyLoading] = useState<boolean>(false);
-const [uploadedFiles, setUploadedFiles] = useState<Record<number, File[]>>({});
-const [isInitialLoaded, setIsInitialLoaded] = useState<boolean>(false);
-
-const viewer = useRef<HTMLDivElement>(null);
-const initialized = useRef(false);
-const isCancelled = useRef(false);
-const currentDocumentPath = useRef<string>("");
-const [showDialog, setShowDialog] = useState(isOpen);
-const webViewerInstance = useRef<WebViewerInstance | null>(null);
-
-// βœ… μžλ™ μ„œλͺ… ν•„λ“œ 생성 ν›… μ‚¬μš©
-const { signatureFields, isProcessing: isAutoSignProcessing, hasSignatureFields, error: autoSignError } = useAutoSignatureFields(webViewerInstance.current || instance);
-
-// ν…œν”Œλ¦Ώ νƒ€μž… νŒλ‹¨
-const isComplianceTemplate = templateName.includes('쀀법');
-const isNDATemplate = templateName.includes('λΉ„λ°€μœ μ§€') || templateName.includes('NDA');
-
-// 파일 λͺ©λ‘ 생성
-const allFiles: FileInfo[] = React.useMemo(() => {
- const files: FileInfo[] = [];
-
- if (filePath) {
- files.push({
- path: filePath,
- name: templateName || "κΈ°λ³Έ κ³„μ•½μ„œ",
- type: "main",
- });
- }
-
- const normalizedAttachments: FileInfo[] = (additionalFiles || [])
- .map((f: any, idx: number) => ({
- path: f.path ?? f.filePath ?? "",
- name: `μ²¨λΆ€νŒŒμΌ ${idx + 1}`,
- type: "attachment" as const,
- }))
- .filter(f => !!f.path);
-
- files.push(...normalizedAttachments);
-
- if (isComplianceTemplate) {
- files.push({
- path: "",
- name: "쀀법 섀문쑰사",
- type: "survey",
- });
- }
+ // 콜백 레퍼런슀 μ—…λ°μ΄νŠΈ
+ useEffect(() => {
+ onSignatureCompleteRef.current = onSignatureComplete;
+ }, [onSignatureComplete]);
- console.log("πŸ“‚ μƒμ„±λœ allFiles:", files, { isNDATemplate, isComplianceTemplate });
- return files;
-}, [filePath, additionalFiles, templateName, isComplianceTemplate, isNDATemplate]);
+ const checkSignatureFields = useCallback(async () => {
+ if (!instance?.Core?.annotationManager) {
+ console.log('πŸ” μ„œλͺ… 체크: annotationManager μ—†μŒ');
+ return false;
+ }
-// WebViewer 정리 ν•¨μˆ˜
-const cleanupWebViewer = () => {
- console.log("🧹 WebViewer 정리 μ‹œμž‘");
-
- if (webViewerInstance.current) {
try {
- const { documentViewer } = webViewerInstance.current.Core;
- if (documentViewer && documentViewer.getDocument()) {
- documentViewer.closeDocument();
+ const { annotationManager, documentViewer } = instance.Core;
+
+ // λ¬Έμ„œκ°€ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μœΌλ©΄ false λ°˜ν™˜
+ if (!documentViewer.getDocument()) {
+ console.log('πŸ” μ„œλͺ… 체크: λ¬Έμ„œ λ―Έλ‘œλ“œ');
+ return false;
}
+
+ let hasSignature = false;
+
+ // 1. Form Fields 확인 (더 μ •ν™•ν•œ 방법)
+ const fieldManager = annotationManager.getFieldManager();
+ const fields = fieldManager.getFields();
- if (webViewerInstance.current.UI && typeof webViewerInstance.current.UI.dispose === 'function') {
- webViewerInstance.current.UI.dispose();
+ console.log('πŸ” 폼 ν•„λ“œ 확인:', fields.map(field => ({
+ name: field.name,
+ type: field.type,
+ value: field.value,
+ hasValue: !!field.value
+ })));
+
+ // μ„œλͺ… ν•„λ“œ 확인
+ for (const field of fields) {
+ // PDFTronμ—μ„œ μ„œλͺ… ν•„λ“œλŠ” 보톡 'Sig' νƒ€μž…μ΄μ§€λ§Œ, 값이 μžˆλŠ”μ§€ μ •ν™•νžˆ 확인
+ if (field.type === 'Sig' || field.name?.toLowerCase().includes('signature')) {
+ if (field.value && (
+ typeof field.value === 'string' && field.value.length > 0 ||
+ typeof field.value === 'object' && field.value !== null
+ )) {
+ hasSignature = true;
+ console.log('πŸ” μ„œλͺ… ν•„λ“œμ—μ„œ μ„œλͺ… 발견:', field.name, field.value);
+ break;
+ }
+ }
}
+
+ // 2. Signature Widget Annotations 확인
+ if (!hasSignature) {
+ const annotations = annotationManager.getAnnotationsList();
+ console.log('πŸ” 주석 확인:', annotations.length, '개');
+
+ for (const annotation of annotations) {
+ // SignatureWidgetAnnotation νƒ€μž… 확인
+ if (annotation.elementName === 'signatureWidget' ||
+ annotation.constructor.name === 'SignatureWidgetAnnotation' ||
+ annotation.Subject === 'Signature') {
+
+ // μ„œλͺ… 데이터가 μžˆλŠ”μ§€ 확인
+ const hasSignatureData = annotation.getImageData && annotation.getImageData() ||
+ annotation.getPath && annotation.getPath() ||
+ annotation.getCustomData && annotation.getCustomData('signature-data');
+
+ if (hasSignatureData) {
+ hasSignature = true;
+ console.log('πŸ” μ„œλͺ… μœ„μ ―μ—μ„œ μ„œλͺ… 발견:', annotation);
+ break;
+ }
+ }
+ }
+ }
+
+ // 3. Ink/FreeHand Annotations 확인 (직접 κ·Έλ¦° μ„œλͺ…)
+ if (!hasSignature) {
+ const annotations = annotationManager.getAnnotationsList();
+
+ for (const annotation of annotations) {
+ if (annotation.elementName === 'freeHand' ||
+ annotation.elementName === 'ink' ||
+ annotation.constructor.name === 'FreeHandAnnotation') {
+
+ // 경둜 데이터가 있으면 μ„œλͺ…μœΌλ‘œ κ°„μ£Ό
+ const hasPath = annotation.getPath && annotation.getPath().length > 0;
+ if (hasPath) {
+ hasSignature = true;
+ console.log('πŸ” 자유 κ·Έλ¦¬κΈ°μ—μ„œ μ„œλͺ… 발견:', annotation);
+ break;
+ }
+ }
+ }
+ }
+
+ console.log('πŸ” μ΅œμ’… μ„œλͺ… 감지 κ²°κ³Ό:', {
+ hasSignature,
+ fieldsCount: fields.length,
+ annotationsCount: annotationManager.getAnnotationsList().length
+ });
+
+ return hasSignature;
} catch (error) {
- console.warn("WebViewer 정리 쀑 μ—λŸ¬ (λ¬΄μ‹œλ¨):", error);
+ console.error('πŸ“› μ„œλͺ… 확인 쀑 μ—λŸ¬:', error);
+ return false;
}
+ }, [instance]);
+
+ // μ‹€μ‹œκ°„ μ„œλͺ… 감지 (λ¬΄ν•œ λ Œλ”λ§ λ°©μ§€)
+ useEffect(() => {
+ if (!instance?.Core) return;
+
+ const startMonitoring = () => {
+ // κΈ°μ‘΄ μΈν„°λ²Œ 정리
+ if (checkIntervalRef.current) {
+ clearInterval(checkIntervalRef.current);
+ checkIntervalRef.current = null;
+ }
+
+ console.log('πŸ” μ„œλͺ… λͺ¨λ‹ˆν„°λ§ μ‹œμž‘');
+
+ // 2μ΄ˆλ§ˆλ‹€ μ„œλͺ… μƒνƒœ 확인 (1μ΄ˆλ³΄λ‹€ 간격을 늘렀 μ„±λŠ₯ κ°œμ„ )
+ checkIntervalRef.current = setInterval(async () => {
+ try {
+ const hasSignature = await checkSignatureFields();
+
+ // μƒνƒœκ°€ μ‹€μ œλ‘œ λ³€κ²½λ˜μ—ˆμ„ λ•Œλ§Œ μ—…λ°μ΄νŠΈ
+ if (hasSignature !== lastSignatureStateRef.current) {
+ console.log('πŸ” μ„œλͺ… μƒνƒœ λ³€κ²½:', lastSignatureStateRef.current, '->', hasSignature);
+
+ lastSignatureStateRef.current = hasSignature;
+ setHasValidSignature(hasSignature);
+
+ // μ„œλͺ…이 μ™„λ£Œλ˜μ—ˆμ„ λ•Œ 콜백 μ‹€ν–‰
+ if (hasSignature && onSignatureCompleteRef.current) {
+ console.log('✍️ μ„œλͺ… μ™„λ£Œ 콜백 μ‹€ν–‰!');
+ onSignatureCompleteRef.current();
+ }
+ }
+ } catch (error) {
+ console.error('πŸ“› μ„œλͺ… λͺ¨λ‹ˆν„°λ§ μ—λŸ¬:', error);
+ }
+ }, 2000);
+ };
+
+ // λ¬Έμ„œ λ‘œλ“œ ν›„ λͺ¨λ‹ˆν„°λ§ μ‹œμž‘
+ const { documentViewer } = instance.Core;
- webViewerInstance.current = null;
- }
-
- if (instance && setInstance) {
- setInstance(null);
- }
-
- setTimeout(() => cleanupHtmlStyle(), 100);
-};
+ const handleDocumentLoaded = () => {
+ console.log('πŸ“„ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ, μ„œλͺ… λͺ¨λ‹ˆν„°λ§ μ€€λΉ„');
+ // λ¬Έμ„œ λ‘œλ“œ ν›„ 3초 뒀에 λͺ¨λ‹ˆν„°λ§ μ‹œμž‘ (μ•ˆμ •μ„± 확보)
+ setTimeout(startMonitoring, 3000);
+ };
-// λ‹€μ΄μ–Όλ‘œκ·Έ 및 파일 μƒνƒœ λ³€κ²½ μ‹œ 리셋
-useEffect(() => {
- setShowDialog(isOpen);
-
- if (isOpen && isComplianceTemplate && !surveyTemplate) {
- loadSurveyTemplate();
- }
+ if (documentViewer?.getDocument()) {
+ // 이미 λ¬Έμ„œκ°€ λ‘œλ“œλ˜μ–΄ μžˆλ‹€λ©΄ λ°”λ‘œ μ‹œμž‘
+ setTimeout(startMonitoring, 1000);
+ } else {
+ // λ¬Έμ„œ λ‘œλ“œ λŒ€κΈ°
+ documentViewer?.addEventListener('documentLoaded', handleDocumentLoaded);
+ }
+
+ // ν΄λ¦¬λ„ˆ ν•¨μˆ˜
+ return () => {
+ console.log('🧹 μ„œλͺ… λͺ¨λ‹ˆν„°λ§ 정리');
+ if (checkIntervalRef.current) {
+ clearInterval(checkIntervalRef.current);
+ checkIntervalRef.current = null;
+ }
+ documentViewer?.removeEventListener('documentLoaded', handleDocumentLoaded);
+ };
+ }, [instance]); // onSignatureComplete μ œκ±°ν•˜μ—¬ λ¬΄ν•œ λ Œλ”λ§ λ°©μ§€
+
+ // μˆ˜λ™ μ„œλͺ… 확인 ν•¨μˆ˜
+ const manualCheckSignature = useCallback(async () => {
+ console.log('πŸ” μˆ˜λ™ μ„œλͺ… 확인 μš”μ²­');
+ const hasSignature = await checkSignatureFields();
+ setHasValidSignature(hasSignature);
+ lastSignatureStateRef.current = hasSignature;
+ return hasSignature;
+ }, [checkSignatureFields]);
+
+ return {
+ hasValidSignature,
+ checkSignature: manualCheckSignature
+ };
+}
+
+export function BasicContractSignViewer({
+ contractId,
+ filePath,
+ additionalFiles = [],
+ templateName = "",
+ isOpen = false,
+ onClose,
+ onSign,
+ instance,
+ setInstance,
+ onSurveyComplete, // πŸ”₯ μΆ”κ°€
+ onSignatureComplete, // πŸ”₯ μΆ”κ°€
+ t = (key: string) => key,
+}: BasicContractSignViewerProps) {
+
+ const [fileLoading, setFileLoading] = useState<boolean>(true);
+ const [activeTab, setActiveTab] = useState<string>("main");
+ const [surveyData, setSurveyData] = useState<any>({});
+ const [isInitialLoaded, setIsInitialLoaded] = useState<boolean>(false);
+ const [surveyTemplate, setSurveyTemplate] = useState<SurveyTemplateWithQuestions | null>(null);
+ const [surveyLoading, setSurveyLoading] = useState<boolean>(false);
+ const [isSubmitting, setIsSubmitting] = useState(false); // 제좜 μƒνƒœ μΆ”κ°€
+
+ const conditionalHandler = useConditionalSurvey(surveyTemplate);
+
+ const viewer = useRef<HTMLDivElement>(null);
+ const initialized = useRef(false);
+ const isCancelled = useRef(false);
+ const currentDocumentPath = useRef<string>("");
+ const [showDialog, setShowDialog] = useState(isOpen);
+ const webViewerInstance = useRef<WebViewerInstance | null>(null);
+
+ const { signatureFields, isProcessing: isAutoSignProcessing, hasSignatureFields, error: autoSignError } = useAutoSignatureFields(webViewerInstance.current || instance);
- if (isOpen) {
+ // πŸ”₯ μ„œλͺ… 감지 ν›… μ‚¬μš©
+ const { hasValidSignature } = useSignatureDetection(webViewerInstance.current || instance, onSignatureComplete);
+
+ const isComplianceTemplate = templateName.includes('쀀법');
+ const isNDATemplate = templateName.includes('λΉ„λ°€μœ μ§€') || templateName.includes('NDA');
+
+ const allFiles: FileInfo[] = React.useMemo(() => {
+ const files: FileInfo[] = [];
+
+ if (filePath) {
+ files.push({
+ path: filePath,
+ name: templateName || "κΈ°λ³Έ κ³„μ•½μ„œ",
+ type: "main",
+ });
+ }
+
+ const normalizedAttachments: FileInfo[] = (additionalFiles || [])
+ .map((f: any, idx: number) => ({
+ path: f.path ?? f.filePath ?? "",
+ name: `μ²¨λΆ€νŒŒμΌ ${idx + 1}`,
+ type: "attachment" as const,
+ }))
+ .filter(f => !!f.path);
+
+ files.push(...normalizedAttachments);
+
+ if (isComplianceTemplate) {
+ files.push({
+ path: "",
+ name: "쀀법 섀문쑰사",
+ type: "survey",
+ });
+ }
+
+ return files;
+ }, [filePath, additionalFiles, templateName, isComplianceTemplate]);
+
+ const cleanupHtmlStyle = () => {
+ const elements = document.querySelectorAll('.Document_container');
+ elements.forEach((elem) => {
+ elem.remove();
+ });
+ };
+
+ const cleanupWebViewer = () => {
+ console.log("🧹 WebViewer 정리 μ‹œμž‘");
+
+ if (webViewerInstance.current) {
+ try {
+ const { documentViewer } = webViewerInstance.current.Core;
+ if (documentViewer && documentViewer.getDocument()) {
+ documentViewer.closeDocument();
+ }
+
+ if (webViewerInstance.current.UI && typeof webViewerInstance.current.UI.dispose === 'function') {
+ webViewerInstance.current.UI.dispose();
+ }
+ } catch (error) {
+ console.warn("WebViewer 정리 쀑 μ—λŸ¬ (λ¬΄μ‹œλ¨):", error);
+ }
+
+ webViewerInstance.current = null;
+ }
+
+ if (instance && setInstance) {
+ setInstance(null);
+ }
+
+ setTimeout(() => cleanupHtmlStyle(), 100);
+ };
+
+ useEffect(() => {
+ setShowDialog(isOpen);
+
+ if (isOpen && isComplianceTemplate && !surveyTemplate) {
+ loadSurveyTemplate();
+ }
+
+ if (isOpen) {
+ setIsInitialLoaded(false);
+ currentDocumentPath.current = "";
+ }
+ }, [isOpen, isComplianceTemplate]);
+
+ useEffect(() => {
+ if (!filePath) return;
+
setIsInitialLoaded(false);
currentDocumentPath.current = "";
- console.log("πŸ”„ μƒˆλ‘œμš΄ κ³„μ•½μ„œ μ—΄λ¦Ό, μƒνƒœ 리셋");
- }
-}, [isOpen, isComplianceTemplate]);
+ setActiveTab("main");
-// filePath λ³€κ²½ μ‹œ μƒνƒœ 리셋 및 μ¦‰μ‹œ λ¬Έμ„œ λ‘œλ“œ
-useEffect(() => {
- if (!filePath) return;
-
- console.log("πŸ”„ filePath λ³€κ²½μœΌλ‘œ μƒνƒœ 리셋 및 λ¬Έμ„œ λ‘œλ“œ:", filePath);
-
- setIsInitialLoaded(false);
- currentDocumentPath.current = "";
- setActiveTab("main");
-
- const currentInstance = webViewerInstance.current || instance;
-
- if (currentInstance) {
- const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
- const encodedPath = normalizedPath.split('/').map(part => encodeURIComponent(part)).join('/');
- const apiFilePath = `/api/files/${encodedPath}`;
-
- console.log("πŸ“„ filePath λ³€κ²½μœΌλ‘œ μ¦‰μ‹œ λ¬Έμ„œ λ‘œλ“œ:", apiFilePath);
-
- loadDocument(currentInstance, apiFilePath, true).then(() => {
- setIsInitialLoaded(true);
- console.log("βœ… filePath λ³€κ²½ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ");
- }).catch((error) => {
- console.error("πŸ“› filePath λ³€κ²½ λ¬Έμ„œ λ‘œλ“œ μ‹€νŒ¨:", error);
- });
- }
-}, [filePath, instance]);
+ const currentInstance = webViewerInstance.current || instance;
-const loadSurveyTemplate = async () => {
- setSurveyLoading(true);
-
- const mockTemplate = {
- id: 1,
- name: 'κΈ°λ³Έ 쀀법 섀문쑰사',
- description: 'λͺ¨λ“  계약업체 λŒ€μƒ κΈ°λ³Έ 쀀법 섀문쑰사',
- questions: [
- {
- id: 4,
- questionNumber: '4',
- questionText: 'κ·€μ‚¬μ˜ 법λ₯ μ  μ‘°μ§ν˜•νƒœλŠ”?',
- questionType: 'DROPDOWN',
- isRequired: true,
- hasDetailText: false,
- hasFileUpload: false,
- options: [
- { id: 1, optionValue: 'COMPANY_CORP', optionText: 'μ£Όμ‹νšŒμ‚¬/μœ ν•œνšŒμ‚¬' },
- { id: 2, optionValue: 'INDIVIDUAL', optionText: 'κ°œμΈνšŒμ‚¬' },
- { id: 3, optionValue: 'PARTNERSHIP', optionText: 'μ‘°ν•©' },
- { id: 4, optionValue: 'JOINT_VENTURE', optionText: '쑰인트벀처' },
- { id: 5, optionValue: 'OTHER', optionText: '기타', allowsOtherInput: true },
- ]
- },
- {
- id: 6,
- questionNumber: '6',
- questionText: 'λΆ€νŒ¨λ°©μ§€μ™€ κ΄€λ ¨ν•œ κ·€μ‚¬μ˜ 쀀법정책이 μžˆμŠ΅λ‹ˆκΉŒ? μžˆλ‹€λ©΄ μ²¨λΆ€νŒŒμΌλ‘œ μ œκ³΅ν•˜μ—¬ μ£Όμ‹œκΈ° λ°”λžλ‹ˆλ‹€.',
- questionType: 'RADIO',
- isRequired: true,
- hasDetailText: false,
- hasFileUpload: true,
- options: [
- { id: 6, optionValue: 'YES', optionText: 'λ„€' },
- { id: 7, optionValue: 'NO', optionText: 'μ•„λ‹ˆμ˜€' },
- ]
- },
- {
- id: 11,
- questionNumber: '11',
- questionText: 'κ·€μ‚¬μ˜ 사주, μž„μ› μ€‘μ—μ„œ μ „(졜근 3λ…„λ‚΄)Β·ν˜„μ§ 곡직자인 μ‚¬λžŒμ΄ μžˆμŠ΅λ‹ˆκΉŒ? λ§Œμ•½ μžˆλ‹€λ©΄ μƒμ„Έν•˜κ²Œ κΈ°μˆ ν•΄ μ£Όμ‹­μ‹œμ˜€.',
- questionType: 'RADIO',
- isRequired: true,
- hasDetailText: true,
- hasFileUpload: false,
- options: [
- { id: 11, optionValue: 'YES', optionText: 'λ„€' },
- { id: 12, optionValue: 'NO', optionText: 'μ•„λ‹ˆμ˜€' },
- ]
- },
- ]
+ if (currentInstance) {
+ const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
+ const encodedPath = normalizedPath.split('/').map(part => encodeURIComponent(part)).join('/');
+ const apiFilePath = `/api/files/${encodedPath}`;
+
+ loadDocument(currentInstance, apiFilePath, true).then(() => {
+ setIsInitialLoaded(true);
+ }).catch((error) => {
+ console.error("πŸ“› λ¬Έμ„œ λ‘œλ“œ μ‹€νŒ¨:", error);
+ });
+ }
+ }, [filePath, instance]);
+
+ const loadSurveyTemplate = async () => {
+ setSurveyLoading(true);
+
+ try {
+ const template = await getActiveSurveyTemplate();
+ setSurveyTemplate(template);
+ } catch (error) {
+ console.error('πŸ“› 섀문쑰사 ν…œν”Œλ¦Ώ λ‘œλ“œ μ‹€νŒ¨:', error);
+ setSurveyTemplate(null);
+ } finally {
+ setSurveyLoading(false);
+ }
};
-
- setSurveyTemplate(mockTemplate);
- setSurveyLoading(false);
-};
-
-// WebViewer μ΄ˆκΈ°ν™” κ°œμ„ 
-useEffect(() => {
- if (!initialized.current && viewer.current) {
- initialized.current = true;
- isCancelled.current = false;
-
- const initializeWebViewer = () => {
- if (!viewer.current || isCancelled.current) {
- console.log("πŸ“› WebViewer μ΄ˆκΈ°ν™” μ·¨μ†Œλ¨ (DOM μ—†μŒ)");
- return;
- }
- const viewerElement = viewer.current;
-
- if (!viewerElement.isConnected) {
- console.log("πŸ“› WebViewer DOM이 μ—°κ²°λ˜μ§€ μ•ŠμŒ, μž¬μ‹œλ„...");
- setTimeout(initializeWebViewer, 100);
- return;
- }
+ useEffect(() => {
+ if (!initialized.current && viewer.current) {
+ initialized.current = true;
+ isCancelled.current = false;
- cleanupWebViewer();
+ const initializeWebViewer = () => {
+ if (!viewer.current || isCancelled.current) {
+ return;
+ }
- console.log("πŸ“„ WebViewer μ΄ˆκΈ°ν™” μ‹œμž‘...");
-
- import("@pdftron/webviewer").then(({ default: WebViewer }) => {
- if (isCancelled.current || !viewer.current) {
- console.log("πŸ“› WebViewer μ΄ˆκΈ°ν™” μ·¨μ†Œλ¨ (import ν›„)");
+ const viewerElement = viewer.current;
+
+ if (!viewerElement.isConnected) {
+ setTimeout(initializeWebViewer, 100);
return;
}
- WebViewer(
- {
- path: "/pdftronWeb",
- licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
- fullAPI: true ,
- disabledElements: [
-
- ]
- },
- viewerElement
- ).then((newInstance) => {
- if (isCancelled.current) {
- console.log("πŸ“› WebViewer μΈμŠ€ν„΄μŠ€ 생성 ν›„ μ·¨μ†Œλ¨");
+ cleanupWebViewer();
+
+ import("@pdftron/webviewer").then(({ default: WebViewer }) => {
+ if (isCancelled.current || !viewer.current) {
return;
}
- console.log("πŸ“„ WebViewer μ΄ˆκΈ°ν™” μ™„λ£Œ");
-
- webViewerInstance.current = newInstance;
- setInstance(newInstance);
- setFileLoading(false);
-
- const { documentViewer } = newInstance.Core;
- const FitMode = newInstance.UI.FitMode;
-
- // λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ μ‹œ 처리
- const handleDocumentLoaded = () => {
+ WebViewer(
+ {
+ path: "/pdftronWeb",
+ licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+ fullAPI: true,
+ },
+ viewerElement
+ ).then((newInstance) => {
+ if (isCancelled.current) {
+ return;
+ }
+
+ webViewerInstance.current = newInstance;
+ setInstance(newInstance);
setFileLoading(false);
- newInstance.UI.setFitMode(FitMode.FitWidth);
-
- requestAnimationFrame(() => {
- try {
- documentViewer.refreshAll();
- documentViewer.updateView();
- window.dispatchEvent(new Event("resize"));
- setTimeout(() => window.dispatchEvent(new Event("resize")), 100);
- } catch (e) {
- console.warn("layout refresh skipped", e);
+
+ const { documentViewer } = newInstance.Core;
+ const FitMode = newInstance.UI.FitMode;
+
+ const handleDocumentLoaded = () => {
+ setFileLoading(false);
+ newInstance.UI.setFitMode(FitMode.FitWidth);
+
+ requestAnimationFrame(() => {
+ try {
+ documentViewer.refreshAll();
+ documentViewer.updateView();
+ window.dispatchEvent(new Event("resize"));
+ setTimeout(() => window.dispatchEvent(new Event("resize")), 100);
+ } catch (e) {
+ console.warn("layout refresh skipped", e);
+ }
+ });
+ };
+
+ documentViewer.addEventListener('documentLoaded', handleDocumentLoaded);
+
+ newInstance.UI.setMinZoomLevel('25%');
+ newInstance.UI.setMaxZoomLevel('400%');
+
+ newInstance.UI.disableElements([
+ "toolbarGroup-Annotate",
+ "toolbarGroup-Shapes",
+ "toolbarGroup-Insert",
+ "toolbarGroup-Edit",
+ "toolbarGroup-FillAndSign",
+ "toolbarGroup-Forms",
+ "saveAsButton",
+ "downloadButton",
+ ]);
+
+ documentViewer.addEventListener('documentLoadingError', (error) => {
+ console.error("πŸ“› λ¬Έμ„œ λ‘œλ”© μ—λŸ¬:", error);
+
+ let showToast = true;
+ let errorMessage = "λ¬Έμ„œλ₯Ό λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.";
+
+ if (error && typeof error === 'object') {
+ const errorStr = JSON.stringify(error).toLowerCase();
+
+ if (errorStr.includes('linearized') || errorStr.includes('getreference')) {
+ showToast = false;
+ } else if (errorStr.includes('network')) {
+ errorMessage = "λ„€νŠΈμ›Œν¬ 연결을 ν™•μΈν•΄μ£Όμ„Έμš”.";
+ } else if (errorStr.includes('permission')) {
+ errorMessage = "λ¬Έμ„œμ— μ ‘κ·Όν•  κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.";
+ }
}
- });
- };
-
- documentViewer.addEventListener('documentLoaded', handleDocumentLoaded);
-
- documentViewer.addEventListener('layoutChanged', () => {
- if (newInstance.UI.getFitMode && newInstance.UI.getFitMode() !== FitMode.Zoom) {
- newInstance.UI.setFitMode(FitMode.Zoom);
- }
- });
-
- newInstance.UI.setMinZoomLevel('25%');
- newInstance.UI.setMaxZoomLevel('400%');
-
- newInstance.UI.disableElements([
- "toolbarGroup-Annotate",
- "toolbarGroup-Shapes",
- "toolbarGroup-Insert",
- "toolbarGroup-Edit",
- "toolbarGroup-FillAndSign",
- "toolbarGroup-Forms",
- "saveAsButton",
- "downloadButton",
-
- ])
-
- documentViewer.addEventListener('documentLoadingError', (error) => {
- console.error("πŸ“› WebViewer λ¬Έμ„œ λ‘œλ”© μ—λŸ¬:", error);
-
- let showToast = true;
- let errorMessage = "λ¬Έμ„œλ₯Ό λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.";
-
- if (error && typeof error === 'object') {
- const errorStr = JSON.stringify(error).toLowerCase();
-
- if (errorStr.includes('linearized') || errorStr.includes('getreference')) {
- console.warn("⚠️ PDF ꡬ쑰 κ²½κ³  (λ¬Έμ„œ λ‘œλ“œλŠ” 진행됨)");
- showToast = false;
- } else if (errorStr.includes('network')) {
- errorMessage = "λ„€νŠΈμ›Œν¬ 연결을 ν™•μΈν•΄μ£Όμ„Έμš”.";
- } else if (errorStr.includes('permission')) {
- errorMessage = "λ¬Έμ„œμ— μ ‘κ·Όν•  κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.";
+ if (showToast) {
+ setFileLoading(false);
+ toast.error(errorMessage);
}
- }
-
- if (showToast) {
- setFileLoading(false);
- toast.error(errorMessage);
- }
- });
+ });
+ }).catch((error) => {
+ console.error("πŸ“› WebViewer μ΄ˆκΈ°ν™” μ‹€νŒ¨:", error);
+ setFileLoading(false);
+ toast.error("λ·°μ–΄ μ΄ˆκΈ°ν™”μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
+ });
}).catch((error) => {
- console.error("πŸ“› WebViewer μ΄ˆκΈ°ν™” μ‹€νŒ¨:", error);
+ console.error("πŸ“› WebViewer λͺ¨λ“ˆ λ‘œλ“œ μ‹€νŒ¨:", error);
setFileLoading(false);
- toast.error("λ·°μ–΄ μ΄ˆκΈ°ν™”μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
+ toast.error("λ·°μ–΄ λͺ¨λ“ˆμ„ λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
});
- }).catch((error) => {
- console.error("πŸ“› WebViewer λͺ¨λ“ˆ λ‘œλ“œ μ‹€νŒ¨:", error);
- setFileLoading(false);
- toast.error("λ·°μ–΄ λͺ¨λ“ˆμ„ λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
+ };
+
+ requestAnimationFrame(() => {
+ setTimeout(initializeWebViewer, 50);
});
- };
+ }
- requestAnimationFrame(() => {
- setTimeout(initializeWebViewer, 50);
- });
- }
+ return () => {
+ isCancelled.current = true;
+ cleanupWebViewer();
+ };
+ }, [setInstance]);
- return () => {
- isCancelled.current = true;
- cleanupWebViewer();
+ const getExtFromPath = (p: string) => {
+ const m = p.toLowerCase().match(/\.([a-z0-9]+)(?:\?.*)?$/);
+ return m ? m[1] : undefined;
};
-}, [setInstance]);
-
-// ν™•μž₯자 μΆ”μΆœ μœ ν‹Έ
-const getExtFromPath = (p: string) => {
- const m = p.toLowerCase().match(/\.([a-z0-9]+)(?:\?.*)?$/);
- return m ? m[1] : undefined;
-};
-
-// λ¬Έμ„œ λ‘œλ“œ ν•¨μˆ˜ κ°œμ„ 
-const loadDocument = async (
- instance: WebViewerInstance,
- documentPath: string,
- forceReload = false
-) => {
- if (!forceReload && currentDocumentPath.current === documentPath) {
- console.log("πŸ“„ λ™μΌν•œ λ¬Έμ„œμ΄λ―€λ‘œ μŠ€ν‚΅:", documentPath);
- return;
- }
-
- setFileLoading(true);
- try {
- console.log("πŸ“„ λ¬Έμ„œ λ‘œλ“œ μ‹œμž‘(UI):", documentPath, forceReload ? "(κ°•μ œ λ¦¬λ‘œλ“œ)" : "");
- if (!instance || !instance.UI || !instance.Core) {
- throw new Error("WebViewer μΈμŠ€ν„΄μŠ€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
+ const loadDocument = async (
+ instance: WebViewerInstance,
+ documentPath: string,
+ forceReload = false
+ ) => {
+ if (!forceReload && currentDocumentPath.current === documentPath) {
+ return;
}
- const ext = getExtFromPath(documentPath);
- await instance.UI.loadDocument(documentPath, {
- ...(ext ? { extension: ext } : {}),
- filename: documentPath.split("/").pop(),
- });
+ setFileLoading(true);
+ try {
+ if (!instance || !instance.UI || !instance.Core) {
+ throw new Error("WebViewer μΈμŠ€ν„΄μŠ€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
+ }
- currentDocumentPath.current = documentPath;
- console.log("πŸ“„ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ(UI):", documentPath);
+ const ext = getExtFromPath(documentPath);
+ await instance.UI.loadDocument(documentPath, {
+ ...(ext ? { extension: ext } : {}),
+ filename: documentPath.split("/").pop(),
+ });
- const { documentViewer } = instance.Core;
- requestAnimationFrame(() => {
- try {
- documentViewer.refreshAll();
- documentViewer.updateView();
- window.dispatchEvent(new Event("resize"));
- setTimeout(() => window.dispatchEvent(new Event("resize")), 100);
- } catch (e) {
- console.warn("λ ˆμ΄μ•„μ›ƒ μƒˆλ‘œκ³ μΉ¨ μŠ€ν‚΅:", e);
- }
- });
- } catch (error) {
- console.error("πŸ“› λ¬Έμ„œ λ‘œλ”© μ‹€νŒ¨(UI):", error);
- currentDocumentPath.current = "";
-
- let msg = "λ¬Έμ„œλ₯Ό λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.";
- if (error instanceof Error) {
- const s = error.message.toLowerCase();
- if (s.includes("network") || s.includes("fetch")) {
- msg = "λ„€νŠΈμ›Œν¬ 연결을 ν™•μΈν•΄μ£Όμ„Έμš”.";
- } else if (s.includes("permission") || s.includes("access")) {
- msg = "λ¬Έμ„œμ— μ ‘κ·Όν•  κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.";
- } else if (s.includes("corrupt") || s.includes("invalid")) {
- msg = "파일이 μ†μƒλ˜μ—ˆκ±°λ‚˜ ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.";
- } else if (s.includes("linearized") || s.includes("getreference")) {
- msg = "";
+ currentDocumentPath.current = documentPath;
+
+ const { documentViewer } = instance.Core;
+ requestAnimationFrame(() => {
+ try {
+ documentViewer.refreshAll();
+ documentViewer.updateView();
+ window.dispatchEvent(new Event("resize"));
+ setTimeout(() => window.dispatchEvent(new Event("resize")), 100);
+ } catch (e) {
+ console.warn("λ ˆμ΄μ•„μ›ƒ μƒˆλ‘œκ³ μΉ¨ μŠ€ν‚΅:", e);
+ }
+ });
+ } catch (error) {
+ console.error("πŸ“› λ¬Έμ„œ λ‘œλ”© μ‹€νŒ¨:", error);
+ currentDocumentPath.current = "";
+
+ let msg = "λ¬Έμ„œλ₯Ό λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.";
+ if (error instanceof Error) {
+ const s = error.message.toLowerCase();
+ if (s.includes("network") || s.includes("fetch")) {
+ msg = "λ„€νŠΈμ›Œν¬ 연결을 ν™•μΈν•΄μ£Όμ„Έμš”.";
+ } else if (s.includes("permission") || s.includes("access")) {
+ msg = "λ¬Έμ„œμ— μ ‘κ·Όν•  κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.";
+ } else if (s.includes("corrupt") || s.includes("invalid")) {
+ msg = "파일이 μ†μƒλ˜μ—ˆκ±°λ‚˜ ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.";
+ } else if (s.includes("linearized") || s.includes("getreference")) {
+ msg = "";
+ }
}
+ if (msg) toast.error(msg);
+ } finally {
+ setFileLoading(false);
}
- if (msg) toast.error(msg);
- } finally {
- setFileLoading(false);
- }
-};
-
-// 폼 데이터 μˆ˜μ§‘ ν•¨μˆ˜
-const collectFormData = async (instance: WebViewerInstance) => {
- try {
- const { documentViewer, annotationManager } = instance.Core;
- const fieldManager = annotationManager.getFieldManager();
- const fields = fieldManager.getFields();
-
- const formData: any = {};
- fields.forEach((field: any) => {
- formData[field.name] = field.value;
- });
-
- console.log('πŸ“ 폼 데이터 μˆ˜μ§‘:', formData);
- return formData;
- } catch (error) {
- console.error('πŸ“› 폼 데이터 μˆ˜μ§‘ μ‹€νŒ¨:', error);
- return {};
- }
-};
+ };
-// νƒ­ λ³€κ²½ ν•Έλ“€λŸ¬
-const handleTabChange = async (newTab: string) => {
- setActiveTab(newTab);
- if (newTab === "survey") return;
-
- const currentInstance = webViewerInstance.current || instance;
- if (!currentInstance || fileLoading) return;
-
- let targetFile: FileInfo | undefined;
- if (newTab === "main") {
- targetFile = allFiles.find(f => f.type === "main");
- } else if (newTab.startsWith("file-")) {
- const fileIndex = parseInt(newTab.replace("file-", ""), 10);
- targetFile = allFiles.filter(f => f.type !== 'survey')[fileIndex];
- }
+ const collectFormData = async (instance: WebViewerInstance) => {
+ try {
+ const { annotationManager } = instance.Core;
+ const fieldManager = annotationManager.getFieldManager();
+ const fields = fieldManager.getFields();
- if (!targetFile?.path) {
- console.warn("πŸ“› λŒ€μƒ νŒŒμΌμ„ 찾을 수 μ—†μŒ:", newTab, allFiles);
- return;
- }
+ const formData: any = {};
+ fields.forEach((field: any) => {
+ formData[field.name] = field.value;
+ });
- const normalizedPath = targetFile.path.startsWith("/")
- ? targetFile.path.substring(1)
- : targetFile.path;
- const encodedPath = normalizedPath.split("/").map(encodeURIComponent).join("/");
- const apiFilePath = `/api/files/${encodedPath}`;
+ return formData;
+ } catch (error) {
+ console.error('πŸ“› 폼 데이터 μˆ˜μ§‘ μ‹€νŒ¨:', error);
+ return {};
+ }
+ };
- console.log("πŸ“„ νƒ­ λ³€κ²½μœΌλ‘œ λ¬Έμ„œ λ‘œλ“œ:", { newTab, targetFile, apiFilePath });
+ const handleTabChange = async (newTab: string) => {
+ setActiveTab(newTab);
+ if (newTab === "survey") return;
- try {
- currentDocumentPath.current = "";
- await loadDocument(currentInstance, apiFilePath, true);
- setIsInitialLoaded(true);
+ const currentInstance = webViewerInstance.current || instance;
+ if (!currentInstance || fileLoading) return;
- const { documentViewer } = currentInstance.Core;
- requestAnimationFrame(() => {
- try {
- documentViewer.refreshAll();
- documentViewer.updateView();
- window.dispatchEvent(new Event("resize"));
- } catch (e) {
- console.warn("νƒ­ λ³€κ²½ ν›„ λ ˆμ΄μ•„μ›ƒ μƒˆλ‘œκ³ μΉ¨ μŠ€ν‚΅:", e);
- }
- });
- } catch (e) {
- console.error("πŸ“› νƒ­ λ³€κ²½ μ‹€νŒ¨:", e);
- }
-};
-
-// 초기 메인 λ¬Έμ„œ λ‘œλ“œ κ°œμ„ 
-useEffect(() => {
- console.log("πŸ” 초기 λ‘œλ“œ 체크:", {
- hasInstance: !!(webViewerInstance.current || instance),
- hasFilePath: !!filePath,
- activeTab,
- isInitialLoaded,
- allFilesLength: allFiles.length,
- isNDATemplate
- });
-
- const currentInstance = webViewerInstance.current || instance;
-
- if (!currentInstance || !filePath || isInitialLoaded) {
- return;
- }
-
- const isMainTab = activeTab === 'main';
- const shouldLoadInitial = allFiles.length === 1 || isMainTab;
-
- if (!shouldLoadInitial || currentDocumentPath.current !== "") {
- return;
- }
-
- const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
- const encodedPath = normalizedPath.split('/').map(part => encodeURIComponent(part)).join('/');
- const apiFilePath = `/api/files/${encodedPath}`;
-
- console.log("πŸ“„ 초기 마운트 λ¬Έμ„œ λ‘œλ“œ:", { apiFilePath, isNDATemplate, activeTab });
-
- currentDocumentPath.current = "";
-
- loadDocument(currentInstance, apiFilePath, true).then(() => {
- setIsInitialLoaded(true);
- console.log("βœ… 초기 마운트 λ‘œλ“œ μ™„λ£Œ");
- }).catch((error) => {
- console.error("πŸ“› 초기 마운트 λ‘œλ“œ μ‹€νŒ¨:", error);
- });
-}, [webViewerInstance.current, instance, filePath, activeTab, isInitialLoaded, allFiles.length, isNDATemplate]);
-
-// 섀문쑰사 λ‹΅λ³€ μ—…λ°μ΄νŠΈ ν•¨μˆ˜
-const updateSurveyAnswer = (questionId: number, field: string, value: any) => {
- setSurveyAnswers(prev => ({
- ...prev,
- [questionId]: {
- ...prev[questionId],
- questionId,
- [field]: value
+ let targetFile: FileInfo | undefined;
+ if (newTab === "main") {
+ targetFile = allFiles.find(f => f.type === "main");
+ } else if (newTab.startsWith("file-")) {
+ const fileIndex = parseInt(newTab.replace("file-", ""), 10);
+ targetFile = allFiles.filter(f => f.type !== 'survey')[fileIndex];
}
- }));
-};
-// 파일 μ—…λ‘œλ“œ ν•Έλ“€λŸ¬
-const handleSurveyFileUpload = (questionId: number, files: FileList | null) => {
- if (!files) return;
-
- const fileArray = Array.from(files);
- setUploadedFiles(prev => ({
- ...prev,
- [questionId]: fileArray
- }));
-
- updateSurveyAnswer(questionId, 'files', fileArray);
-};
+ if (!targetFile?.path) {
+ console.warn("πŸ“› λŒ€μƒ νŒŒμΌμ„ 찾을 수 μ—†μŒ:", newTab, allFiles);
+ return;
+ }
-// 질문 μ™„λ£Œ μ—¬λΆ€ 체크
-const isSurveyQuestionComplete = (question: any): boolean => {
- const answer = surveyAnswers[question.id];
-
- if (!question.isRequired) return true;
- if (!answer?.answerValue) return false;
-
- if (question.hasDetailText && answer.answerValue === 'YES' && !answer.detailText) {
- return false;
- }
-
- if (question.hasFileUpload && answer.answerValue === 'YES' && (!answer.files || answer.files.length === 0)) {
- return false;
- }
-
- return true;
-};
-
-// 전체 섀문쑰사 μ™„λ£Œ μ—¬λΆ€ 체크
-const isSurveyComplete = (): boolean => {
- if (!surveyTemplate?.questions) return false;
- return surveyTemplate.questions.every((question: any) => isSurveyQuestionComplete(question));
-};
-
-// 섀문쑰사 데이터 처리
-const handleSurveyComplete = async () => {
- if (!isSurveyComplete()) {
- toast.error('λͺ¨λ“  ν•„μˆ˜ ν•­λͺ©μ„ μ™„λ£Œν•΄μ£Όμ„Έμš”.', {
- description: 'λ―Έμ™„μ„±λœ 질문이 μžˆμŠ΅λ‹ˆλ‹€.',
- icon: <AlertTriangle className="h-5 w-5 text-red-500" />
- });
- return;
- }
+ const normalizedPath = targetFile.path.startsWith("/")
+ ? targetFile.path.substring(1)
+ : targetFile.path;
+ const encodedPath = normalizedPath.split("/").map(encodeURIComponent).join("/");
+ const apiFilePath = `/api/files/${encodedPath}`;
- try {
- console.log('섀문쑰사 λ‹΅λ³€:', surveyAnswers);
-
- setSurveyData({
- completed: true,
- answers: Object.values(surveyAnswers),
- timestamp: new Date().toISOString()
- });
-
- toast.success("섀문쑰사가 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€!", {
- icon: <CheckCircle2 className="h-5 w-5 text-green-500" />
- });
- } catch (error) {
- console.error('섀문쑰사 μ €μž₯ μ‹€νŒ¨:', error);
- toast.error('섀문쑰사 μ €μž₯에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.');
- }
-};
+ try {
+ currentDocumentPath.current = "";
+ await loadDocument(currentInstance, apiFilePath, true);
+ setIsInitialLoaded(true);
-// μ„œλͺ… μ €μž₯ ν•Έλ“€λŸ¬
-const handleSave = async () => {
- const currentInstance = webViewerInstance.current || instance;
- if (!currentInstance) return;
-
- try {
- const { documentViewer, annotationManager } = currentInstance.Core;
- const doc = documentViewer.getDocument();
-
- if (!doc) {
- toast.error("λ¬Έμ„œκ°€ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.");
+ const { documentViewer } = currentInstance.Core;
+ requestAnimationFrame(() => {
+ try {
+ documentViewer.refreshAll();
+ documentViewer.updateView();
+ window.dispatchEvent(new Event("resize"));
+ } catch (e) {
+ console.warn("νƒ­ λ³€κ²½ ν›„ λ ˆμ΄μ•„μ›ƒ μƒˆλ‘œκ³ μΉ¨ μŠ€ν‚΅:", e);
+ }
+ });
+ } catch (e) {
+ console.error("πŸ“› νƒ­ λ³€κ²½ μ‹€νŒ¨:", e);
+ }
+ };
+
+ useEffect(() => {
+ const currentInstance = webViewerInstance.current || instance;
+
+ if (!currentInstance || !filePath || isInitialLoaded) {
return;
}
-
- const formData = await collectFormData(currentInstance);
-
- const xfdfString = await annotationManager.exportAnnotations();
- const documentData = await doc.getFileData({
- xfdfString,
- downloadType: "pdf",
- });
-
- if (isComplianceTemplate && !surveyData.completed) {
- toast.error("쀀법 섀문쑰사λ₯Ό λ¨Όμ € μ™„λ£Œν•΄μ£Όμ„Έμš”.");
- setActiveTab('survey');
+
+ const isMainTab = activeTab === 'main';
+ const shouldLoadInitial = allFiles.length === 1 || isMainTab;
+
+ if (!shouldLoadInitial || currentDocumentPath.current !== "") {
return;
}
-
- if (onSign) {
- await onSign(documentData, { formData, surveyData, signatureFields });
+
+ const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
+ const encodedPath = normalizedPath.split('/').map(part => encodeURIComponent(part)).join('/');
+ const apiFilePath = `/api/files/${encodedPath}`;
+
+ currentDocumentPath.current = "";
+
+ loadDocument(currentInstance, apiFilePath, true).then(() => {
+ setIsInitialLoaded(true);
+ }).catch((error) => {
+ console.error("πŸ“› 초기 마운트 λ‘œλ“œ μ‹€νŒ¨:", error);
+ });
+ }, [webViewerInstance.current, instance, filePath, activeTab, isInitialLoaded, allFiles.length]);
+
+ const handleSave = async () => {
+ const currentInstance = webViewerInstance.current || instance;
+ if (!currentInstance) return;
+
+ try {
+ const { documentViewer, annotationManager } = currentInstance.Core;
+ const doc = documentViewer.getDocument();
+
+ if (!doc) {
+ toast.error("λ¬Έμ„œκ°€ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.");
+ return;
+ }
+
+ const formData = await collectFormData(currentInstance);
+
+ const xfdfString = await annotationManager.exportAnnotations();
+ const documentData = await doc.getFileData({
+ xfdfString,
+ downloadType: "pdf",
+ });
+
+ if (isComplianceTemplate && !surveyData.completed) {
+ toast.error("쀀법 섀문쑰사λ₯Ό λ¨Όμ € μ™„λ£Œν•΄μ£Όμ„Έμš”.");
+ setActiveTab('survey');
+ return;
+ }
+
+ if (onSign) {
+ await onSign(documentData, { formData, surveyData, signatureFields });
+ } else {
+ toast.success("κ³„μ•½μ„œκ°€ μ„±κ³΅μ μœΌλ‘œ μ„œλͺ…λ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
+ }
+
+ handleClose();
+ } catch (error) {
+ console.error("πŸ“› μ„œλͺ… μ €μž₯ μ‹€νŒ¨:", error);
+ toast.error("μ„œλͺ…을 μ €μž₯ν•˜λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
+ }
+ };
+
+ const handleClose = () => {
+ if (onClose) {
+ onClose();
} else {
- toast.success("κ³„μ•½μ„œκ°€ μ„±κ³΅μ μœΌλ‘œ μ„œλͺ…λ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
+ setShowDialog(false);
}
-
- handleClose();
- } catch (error) {
- console.error("πŸ“› μ„œλͺ… μ €μž₯ μ‹€νŒ¨:", error);
- toast.error("μ„œλͺ…을 μ €μž₯ν•˜λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
- }
-};
-
-// λ‹€μ΄μ–Όλ‘œκ·Έ λ‹«κΈ° ν•Έλ“€λŸ¬
-const handleClose = () => {
- if (onClose) {
- onClose();
- } else {
- setShowDialog(false);
- }
-};
+ };
-// 동적 섀문쑰사 μ»΄ν¬λ„ŒνŠΈ
-const SurveyComponent = () => {
- if (surveyLoading) {
- return (
- <div className="h-full w-full">
- <Card className="h-full">
- <CardContent className="flex flex-col items-center justify-center h-full py-12">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">섀문쑰사λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...</p>
- </CardContent>
- </Card>
- </div>
- );
- }
+ // κ°œμ„ λœ SurveyComponent
+ const SurveyComponent = () => {
+ const {
+ control,
+ watch,
+ setValue,
+ getValues,
+ formState: { errors },
+ trigger,
+ } = useForm<SurveyFormData>({
+ defaultValues: {},
+ mode: 'onChange'
+ });
- if (!surveyTemplate) {
- return (
- <div className="h-full w-full">
- <Card className="h-full">
- <CardContent className="flex flex-col items-center justify-center h-full py-12">
- <AlertTriangle className="h-8 w-8 text-red-500 mb-4" />
- <p className="text-sm text-muted-foreground">섀문쑰사 ν…œν”Œλ¦Ώμ„ 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.</p>
- <Button
- variant="outline"
- onClick={loadSurveyTemplate}
- className="mt-2"
- >
- λ‹€μ‹œ μ‹œλ„
- </Button>
- </CardContent>
- </Card>
- </div>
- );
- }
+ const watchedValues = watch();
+ const [uploadedFiles, setUploadedFiles] = useState<Record<number, File[]>>({});
+
+ // πŸ“Š μ‹€μ‹œκ°„ μ§„ν–‰ μƒνƒœ 계산
+ const progressStatus = useMemo(() => {
+ if (!conditionalHandler || !surveyTemplate) {
+ return {
+ visibleQuestions: [],
+ totalRequired: 0,
+ completedRequired: 0,
+ completedQuestionIds: [],
+ incompleteQuestionIds: [],
+ progressPercentage: 0,
+ debugInfo: {}
+ };
+ }
- const completedCount = surveyTemplate.questions.filter((q: any) => isSurveyQuestionComplete(q)).length;
- const progressPercentage = surveyTemplate.questions.length > 0 ? (completedCount / surveyTemplate.questions.length) * 100 : 0;
+ console.log('πŸ”„ μ‹€μ‹œκ°„ ν”„λ‘œκ·Έλ ˆμŠ€ μž¬κ³„μ‚° 쀑...');
+ console.log('πŸ“ 원본 watchedValues:', watchedValues);
+
+ // ν˜„μž¬ 닡변을 쑰건뢀 ν•Έλ“€λŸ¬κ°€ 인식할 수 μžˆλŠ” ν˜•νƒœλ‘œ λ³€ν™˜
+ const convertedAnswers: Record<number, any> = {};
+ Object.entries(watchedValues).forEach(([questionId, value]) => {
+ const id = parseInt(questionId);
+ const convertedValue = {
+ questionId: id,
+ answerValue: value?.answerValue || '',
+ detailText: value?.detailText || '',
+ otherText: value?.otherText || '',
+ files: value?.files || []
+ };
+
+ convertedAnswers[id] = convertedValue;
+
+ // 각 질문의 λ³€ν™˜ κ³Όμ • 둜그
+ if (value?.answerValue) {
+ console.log(`πŸ“ 질문 ${id} λ³€ν™˜:`, {
+ 원본: value,
+ λ³€ν™˜ν›„: convertedValue
+ });
+ }
+ });
-const renderSurveyQuestion = (question: any) => {
- const answer = surveyAnswers[question.id];
- const isComplete = isSurveyQuestionComplete(question);
-
- return (
- <div key={question.id} className="mb-6 p-4 border rounded-lg bg-gray-50">
- <div className="flex items-start justify-between mb-3">
- <div className="flex-1">
- <Label className="text-sm font-medium text-gray-900 flex items-center">
- <span className="mr-2 px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
- {question.questionNumber}
- </span>
- {question.questionText}
- {question.isRequired && <span className="text-red-500 ml-1">*</span>}
- </Label>
+ console.log('πŸ“ λ³€ν™˜λœ λ‹΅λ³€λ“€ μ΅œμ’…:', convertedAnswers);
+
+ const result = conditionalHandler.getSimpleProgressStatus(convertedAnswers);
+
+ console.log('πŸ“Š μ‹€μ‹œκ°„ μ§„ν–‰ μƒνƒœ μ΅œμ’… κ²°κ³Ό:', {
+ μ „μ²΄ν‘œμ‹œμ§ˆλ¬Έ: result.visibleQuestions.length,
+ ν•„μˆ˜μ§ˆλ¬Έμˆ˜: result.totalRequired,
+ μ™„λ£Œλœν•„μˆ˜μ§ˆλ¬Έ: result.completedRequired,
+ μ§„ν–‰λ₯ : result.progressPercentage,
+ μ™„λ£Œλœμ§ˆλ¬Έλ“€: result.completedQuestionIds,
+ λ―Έμ™„λ£Œμ§ˆλ¬Έλ“€: result.incompleteQuestionIds,
+ 기본질문: result.visibleQuestions.filter(q => !q.parentQuestionId).length,
+ μ‘°κ±΄λΆ€μ§ˆλ¬Έ: result.visibleQuestions.filter(q => q.parentQuestionId).length,
+ μ™„λ£ŒλœκΈ°λ³Έμ§ˆλ¬Έ: result.completedQuestionIds.filter(id => !result.visibleQuestions.find(q => q.id === id)?.parentQuestionId).length,
+ μ™„λ£Œλœμ‘°κ±΄λΆ€μ§ˆλ¬Έ: result.completedQuestionIds.filter(id => !!result.visibleQuestions.find(q => q.id === id)?.parentQuestionId).length
+ });
+
+ // 🚨 쑰건뢀 μ§ˆλ¬Έλ“€μ˜ λ‹΅λ³€ μƒνƒœ νŠΉλ³„ 점검
+ const conditionalQuestions = result.visibleQuestions.filter(q => q.parentQuestionId);
+ if (conditionalQuestions.length > 0) {
+ console.log('🚨 쑰건뢀 μ§ˆλ¬Έλ“€ λ‹΅λ³€ μƒνƒœ 점검:', conditionalQuestions.map(q => ({
+ id: q.id,
+ questionNumber: q.questionNumber,
+ isRequired: q.isRequired,
+ parentId: q.parentQuestionId,
+ watchedValue: watchedValues[q.id],
+ convertedAnswer: convertedAnswers[q.id],
+ hasWatchedAnswer: !!watchedValues[q.id]?.answerValue,
+ hasConvertedAnswer: !!convertedAnswers[q.id]?.answerValue,
+ isInRequiredList: result.totalRequired,
+ isCompleted: result.completedQuestionIds.includes(q.id)
+ })));
+ }
+
+ return result;
+ }, [conditionalHandler, watchedValues, surveyTemplate]);
+
+ // 🎯 동적 μƒνƒœ 정보
+ const visibleQuestions = progressStatus.visibleQuestions;
+ const totalVisibleQuestions = visibleQuestions.length;
+ const baseQuestionCount = surveyTemplate?.questions.length || 0;
+ const conditionalQuestionCount = totalVisibleQuestions - baseQuestionCount;
+ const hasConditionalQuestions = conditionalQuestionCount > 0;
+
+ // βœ… μ™„λ£Œ κ°€λŠ₯ μ—¬λΆ€
+ const canComplete = progressStatus.totalRequired > 0 &&
+ progressStatus.completedRequired === progressStatus.totalRequired;
+
+ if (surveyLoading) {
+ return (
+ <div className="h-full w-full">
+ <Card className="h-full">
+ <CardContent className="flex flex-col items-center justify-center h-full py-12">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">섀문쑰사λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...</p>
+ </CardContent>
+ </Card>
</div>
- {isComplete && (
- <CheckCircle2 className="h-5 w-5 text-green-500 ml-2" />
- )}
- </div>
+ );
+ }
- {question.questionType === 'RADIO' && (
- <RadioGroup
- value={answer?.answerValue || ''}
- onValueChange={(value) => updateSurveyAnswer(question.id, 'answerValue', value)}
- className="space-y-2"
- >
- {question.options?.map((option: any) => (
- <div key={option.id} className="flex items-center space-x-2">
- <RadioGroupItem value={option.optionValue} id={`${question.id}-${option.id}`} />
- <Label htmlFor={`${question.id}-${option.id}`} className="text-sm">
- {option.optionText}
- </Label>
- </div>
- ))}
- </RadioGroup>
- )}
-
- {question.questionType === 'DROPDOWN' && (
- <div className="space-y-2">
- <Select
- value={answer?.answerValue || ''}
- onValueChange={(value) => updateSurveyAnswer(question.id, 'answerValue', value)}
- >
- <SelectTrigger>
- <SelectValue placeholder="μ„ νƒν•΄μ£Όμ„Έμš”" />
- </SelectTrigger>
- <SelectContent>
- {question.options?.map((option: any) => (
- <SelectItem key={option.id} value={option.optionValue}>
- {option.optionText}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
-
- {answer?.answerValue === 'OTHER' && (
+ if (!surveyTemplate) {
+ return (
+ <div className="h-full w-full">
+ <Card className="h-full">
+ <CardContent className="flex flex-col items-center justify-center h-full py-12">
+ <AlertTriangle className="h-8 w-8 text-red-500 mb-4" />
+ <p className="text-sm text-muted-foreground">섀문쑰사 ν…œν”Œλ¦Ώμ„ 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.</p>
+ <Button
+ variant="outline"
+ onClick={loadSurveyTemplate}
+ className="mt-2"
+ >
+ λ‹€μ‹œ μ‹œλ„
+ </Button>
+ </CardContent>
+ </Card>
+ </div>
+ );
+ }
+
+ // 🚨 ν…œν”Œλ¦Ώμ΄ λ‘œλ“œλ˜λ©΄ λͺ¨λ“  μ§ˆλ¬Έλ“€μ˜ isRequired 속성 확인
+ React.useEffect(() => {
+ if (surveyTemplate && surveyTemplate.questions) {
+ console.log('🚨 μ„€λ¬Έ ν…œν”Œλ¦Ώμ˜ λͺ¨λ“  μ§ˆλ¬Έλ“€ isRequired 속성 확인:', surveyTemplate.questions.map(q => ({
+ id: q.id,
+ questionNumber: q.questionNumber,
+ questionText: q.questionText?.substring(0, 30) + '...',
+ isRequired: q.isRequired,
+ parentQuestionId: q.parentQuestionId,
+ conditionalValue: q.conditionalValue,
+ isConditional: !!q.parentQuestionId
+ })));
+
+ const allQuestions = surveyTemplate.questions.length;
+ const requiredQuestions = surveyTemplate.questions.filter(q => q.isRequired).length;
+ const conditionalQuestions = surveyTemplate.questions.filter(q => q.parentQuestionId).length;
+ const requiredConditionalQuestions = surveyTemplate.questions.filter(q => q.parentQuestionId && q.isRequired).length;
+
+ console.log('πŸ“Š ν…œν”Œλ¦Ώ 질문 톡계:', {
+ μ „μ²΄μ§ˆλ¬Έμˆ˜: allQuestions,
+ μ „μ²΄ν•„μˆ˜μ§ˆλ¬Έμˆ˜: requiredQuestions,
+ μ‘°κ±΄λΆ€μ§ˆλ¬Έμˆ˜: conditionalQuestions,
+ ν•„μˆ˜μ‘°κ±΄λΆ€μ§ˆλ¬Έμˆ˜: requiredConditionalQuestions,
+ 기본질문수: allQuestions - conditionalQuestions,
+ ν•„μˆ˜κΈ°λ³Έμ§ˆλ¬Έμˆ˜: requiredQuestions - requiredConditionalQuestions
+ });
+
+ // 🚨 λ§Œμ•½ 쑰건뢀 μ§ˆλ¬Έλ“€μ΄ ν•„μˆ˜κ°€ μ•„λ‹ˆλΌλ©΄ κ²½κ³ 
+ if (conditionalQuestions > 0 && requiredConditionalQuestions === 0) {
+ console.warn('⚠️ κ²½κ³ : 쑰건뢀 μ§ˆλ¬Έλ“€μ΄ λͺ¨λ‘ ν•„μˆ˜κ°€ μ•„λ‹™λ‹ˆλ‹€! λ°μ΄ν„°λ² μ΄μŠ€ 확인 ν•„μš”');
+ console.warn('쑰건뢀 μ§ˆλ¬Έλ“€:', surveyTemplate.questions.filter(q => q.parentQuestionId));
+ }
+ }
+ }, [surveyTemplate]);
+
+ const handleFileUpload = useCallback((questionId: number, files: FileList | null) => {
+ if (!files) return;
+
+ const fileArray = Array.from(files);
+ setUploadedFiles(prev => ({
+ ...prev,
+ [questionId]: fileArray
+ }));
+
+ setValue(`${questionId}.files`, fileArray);
+ }, [setValue]);
+
+ const handleAnswerChange = useCallback((questionId: number, field: string, value: any) => {
+ console.log(`πŸ“ λ‹΅λ³€ λ³€κ²½: 질문 ${questionId}, ν•„λ“œ ${field}, κ°’:`, value);
+
+ // ν•΄λ‹Ή 질문이 쑰건뢀 μ§ˆλ¬ΈμΈμ§€ 확인
+ const question = visibleQuestions.find(q => q.id === questionId);
+ if (question) {
+ console.log(`πŸ“‹ 질문 ${questionId} 상세 정보:`, {
+ id: question.id,
+ questionNumber: question.questionNumber,
+ isRequired: question.isRequired,
+ parentQuestionId: question.parentQuestionId,
+ conditionalValue: question.conditionalValue,
+ isConditional: !!question.parentQuestionId
+ });
+ }
+
+ setValue(`${questionId}.${field}`, value);
+
+ // setValue ν›„ ν˜„μž¬ κ°’ 확인
+ setTimeout(() => {
+ const currentFormValues = getValues();
+ console.log(`βœ… setValue ν›„ 확인 - 질문 ${questionId}:`, {
+ μ„€μ •ν•œκ°’: value,
+ μ €μž₯λœμ „μ²΄κ°’: currentFormValues[questionId],
+ 전체폼값: currentFormValues
+ });
+ }, 0);
+
+ // λΆ€λͺ¨ 질문의 닡변이 λ³€κ²½λ˜λ©΄ 쑰건뢀 μžμ‹ μ§ˆλ¬Έλ“€ 처리
+ if (field === 'answerValue' && conditionalHandler) {
+ const currentValues = getValues();
+ const convertedAnswers: Record<number, any> = {};
+
+ Object.entries(currentValues).forEach(([qId, qValue]) => {
+ const id = parseInt(qId);
+ convertedAnswers[id] = {
+ questionId: id,
+ answerValue: qValue?.answerValue || '',
+ detailText: qValue?.detailText || '',
+ otherText: qValue?.otherText || '',
+ files: qValue?.files || []
+ };
+ });
+
+ // μƒˆλ‘œμš΄ λ‹΅λ³€ 반영
+ convertedAnswers[questionId] = {
+ ...convertedAnswers[questionId],
+ questionId,
+ [field]: value
+ };
+
+ console.log(`πŸ”„ 질문 ${questionId}의 λ‹΅λ³€ λ³€κ²½μœΌλ‘œ μΈν•œ 쑰건뢀 질문 처리...`);
+ console.log(`πŸ”„ λ³€κ²½ ν›„ 전체 λ‹΅λ³€:`, convertedAnswers);
+
+ // 영ν–₯λ°›λŠ” μžμ‹ μ§ˆλ¬Έλ“€μ˜ λ‹΅λ³€ μ΄ˆκΈ°ν™”
+ const clearedAnswers = conditionalHandler.clearAffectedChildAnswers(questionId, value, convertedAnswers);
+
+ console.log(`🧹 μ •λ¦¬λœ λ‹΅λ³€λ“€:`, clearedAnswers);
+
+ // μ‚­μ œλœ 닡변듀을 νΌμ—μ„œλ„ 제거
+ Object.keys(convertedAnswers).forEach(qId => {
+ const id = parseInt(qId);
+ if (id !== questionId && !clearedAnswers[id]) {
+ console.log(`πŸ—‘οΈ 질문 ${id} λ‹΅λ³€ μ΄ˆκΈ°ν™”`);
+ setValue(`${id}`, {
+ answerValue: '',
+ detailText: '',
+ otherText: '',
+ files: []
+ });
+
+ // μ—…λ‘œλ“œλœ νŒŒμΌλ„ μ΄ˆκΈ°ν™”
+ setUploadedFiles(prev => {
+ const updated = { ...prev };
+ delete updated[id];
+ return updated;
+ });
+ }
+ });
+ }
+ }, [setValue, getValues, conditionalHandler, visibleQuestions]);
+
+ // πŸ”₯ 섀문쑰사 μ™„λ£Œ ν•Έλ“€λŸ¬ μˆ˜μ •
+ const handleSurveyComplete = useCallback(async () => {
+ console.log('🎯 섀문쑰사 μ™„λ£Œ μ‹œλ„');
+
+ // 이미 제좜 쀑이면 쀑볡 μ‹€ν–‰ λ°©μ§€
+ if (isSubmitting) {
+ console.log('⚠️ 이미 제좜 쀑...');
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const currentValues = getValues();
+ console.log('πŸ“ ν˜„μž¬ 폼 κ°’λ“€:', currentValues);
+
+ // 폼 검증
+ const isValid = await trigger();
+ console.log('πŸ” 폼 검증 κ²°κ³Ό:', isValid);
+
+ // μ§„ν–‰ μƒνƒœ μ΅œμ’… 확인
+ console.log('πŸ“Š μ΅œμ’… μ§„ν–‰ μƒνƒœ:', {
+ totalRequired: progressStatus.totalRequired,
+ completedRequired: progressStatus.completedRequired,
+ canComplete,
+ μ™„λ£Œλœμ§ˆλ¬Έλ“€: progressStatus.completedQuestionIds,
+ λ―Έμ™„λ£Œμ§ˆλ¬Έλ“€: progressStatus.incompleteQuestionIds
+ });
+
+ if (!canComplete) {
+ let errorMessage = 'λͺ¨λ“  ν•„μˆ˜ ν•­λͺ©μ„ μ™„λ£Œν•΄μ£Όμ„Έμš”.';
+ let errorDescription = `${progressStatus.completedRequired}/${progressStatus.totalRequired} μ™„λ£Œλ¨`;
+
+ // ꡬ체적인 λ―Έμ™„λ£Œ 이유 ν‘œμ‹œ
+ if (progressStatus.incompleteQuestionIds.length > 0) {
+ const incompleteReasons = progressStatus.incompleteQuestionIds.map(id => {
+ const debug = progressStatus.debugInfo?.[id];
+ const question = visibleQuestions.find(q => q.id === id);
+ return `β€’ Q${question?.questionNumber}: ${question?.questionText?.substring(0, 40)}...\n β†’ ${debug?.incompleteReason || 'λ‹΅λ³€ ν•„μš”'}`;
+ }).slice(0, 3);
+
+ errorDescription = incompleteReasons.join('\n\n');
+
+ if (progressStatus.incompleteQuestionIds.length > 3) {
+ errorDescription += `\n\n... μ™Έ ${progressStatus.incompleteQuestionIds.length - 3}개 ν•­λͺ©`;
+ }
+ }
+
+ toast.error(errorMessage, {
+ description: errorDescription,
+ icon: <AlertTriangle className="h-5 w-5 text-red-500" />,
+ duration: 12000
+ });
+
+ // 첫 번째 λ―Έμ™„λ£Œ 질문으둜 슀크둀
+ if (progressStatus.incompleteQuestionIds.length > 0) {
+ const firstIncompleteId = progressStatus.incompleteQuestionIds[0];
+ const element = document.getElementById(`question-${firstIncompleteId}`);
+ if (element) {
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ }
+
+ return;
+ }
+
+ // ν•„μˆ˜ 데이터 확인
+ if (!contractId || !surveyTemplate?.id) {
+ toast.error('κ³„μ•½μ„œ 정보 λ˜λŠ” μ„€λ¬Έ ν…œν”Œλ¦Ώ 정보가 μ—†μŠ΅λ‹ˆλ‹€.');
+ return;
+ }
+
+ // μ„œλ²„ μ•‘μ…˜μ— 전달할 데이터 μ€€λΉ„
+ const surveyAnswers: SurveyAnswerData[] = Object.entries(currentValues)
+ .map(([questionId, value]) => ({
+ questionId: parseInt(questionId),
+ answerValue: value?.answerValue || '',
+ detailText: value?.detailText || '',
+ otherText: value?.otherText || '',
+ files: value?.files || []
+ }))
+ .filter(answer =>
+ // 빈 λ‹΅λ³€ 필터링 (ν•˜μ§€λ§Œ ν•„μˆ˜ 질문의 닡변이 μ™„λ£Œλ˜μ—ˆμŒμ„ 이미 ν™•μΈν–ˆμŒ)
+ answer.answerValue || answer.detailText || (answer.files && answer.files.length > 0)
+ );
+
+ const requestData: CompleteSurveyRequest = {
+ contractId: contractId,
+ templateId: surveyTemplate.id,
+ answers: surveyAnswers,
+ progressStatus: progressStatus // λ””λ²„κΉ…μš© μΆ”κ°€ 정보
+ };
+
+ console.log('πŸ“€ μ„œλ²„λ‘œ 전솑할 데이터:', {
+ contractId: requestData.contractId,
+ templateId: requestData.templateId,
+ answersCount: requestData.answers.length,
+ answers: requestData.answers.map(a => ({
+ questionId: a.questionId,
+ hasAnswer: !!a.answerValue,
+ hasDetail: !!a.detailText,
+ hasOther: !!a.otherText,
+ filesCount: a.files?.length || 0
+ }))
+ });
+
+ // 제좜 쀑 ν† μŠ€νŠΈ ν‘œμ‹œ
+ const submitToast = toast.loading('섀문쑰사λ₯Ό μ €μž₯ν•˜λŠ” 쀑...', {
+ description: 'μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”.',
+ duration: Infinity
+ });
+
+ // μ„œλ²„ μ•‘μ…˜ 호좜
+ const result = await completeSurvey(requestData);
+
+ // λ‘œλ”© ν† μŠ€νŠΈ 제거
+ toast.dismiss(submitToast);
+
+ if (result.success) {
+ // ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœ μ—…λ°μ΄νŠΈ (κΈ°μ‘΄ 둜직 μœ μ§€)
+ setSurveyData({
+ completed: true,
+ answers: surveyAnswers,
+ timestamp: new Date().toISOString(),
+ progressStatus: progressStatus,
+ totalQuestions: totalVisibleQuestions,
+ conditionalQuestions: conditionalQuestionCount,
+ responseId: result.data?.responseId // μ„œλ²„μ—μ„œ λ°˜ν™˜λœ 응닡 ID μ €μž₯
+ });
+
+ // πŸ”₯ λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ— 섀문쑰사 μ™„λ£Œ μ•Œλ¦Ό
+ if (onSurveyComplete) {
+ onSurveyComplete();
+ }
+
+ toast.success("πŸŽ‰ 섀문쑰사가 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€!", {
+ description: `총 ${progressStatus.totalRequired}개 ν•„μˆ˜ 질문 μ™„λ£Œ${hasConditionalQuestions ? ` (쑰건뢀 ${conditionalQuestionCount}개 포함)` : ''}`,
+ icon: <CheckCircle2 className="h-5 w-5 text-green-500" />,
+ duration: 5000
+ });
+
+ console.log('βœ… 섀문쑰사 μ™„λ£Œ:', {
+ totalAnswered: surveyAnswers.length,
+ totalRequired: progressStatus.totalRequired,
+ conditionalQuestions: conditionalQuestionCount,
+ responseId: result.data?.responseId
+ });
+
+ // μžλ™μœΌλ‘œ 메인 νƒ­μœΌλ‘œ 이동 (선택사항)
+ setTimeout(() => {
+ setActiveTab('main');
+ }, 2000);
+
+ } else {
+ // μ„œλ²„ μ—λŸ¬ 처리
+ console.error('❌ μ„œλ²„ 응닡 μ—λŸ¬:', result.message);
+ toast.error('섀문쑰사 μ €μž₯ μ‹€νŒ¨', {
+ description: result.message || 'μ„œλ²„μ—μ„œ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.',
+ icon: <AlertTriangle className="h-5 w-5 text-red-500" />,
+ duration: 8000
+ });
+ }
+
+ } catch (error) {
+ console.error('❌ 섀문쑰사 μ €μž₯ 쀑 μ˜ˆμ™Έ λ°œμƒ:', error);
+
+ let errorMessage = '섀문쑰사 μ €μž₯에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.';
+ let errorDescription = 'λ„€νŠΈμ›Œν¬ 연결을 ν™•μΈν•˜κ³  λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.';
+
+ if (error instanceof Error) {
+ errorDescription = error.message;
+ }
+
+ toast.error(errorMessage, {
+ description: errorDescription,
+ icon: <AlertTriangle className="h-5 w-5 text-red-500" />,
+ duration: 10000
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, [
+ getValues,
+ trigger,
+ progressStatus,
+ visibleQuestions,
+ canComplete,
+ contractId,
+ surveyTemplate?.id,
+ totalVisibleQuestions,
+ conditionalQuestionCount,
+ hasConditionalQuestions,
+ isSubmitting,
+ setActiveTab,
+ onSurveyComplete // πŸ”₯ μΆ”κ°€
+ ]);
+
+ // OTHER ν…μŠ€νŠΈ μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ
+ const OtherTextInput = ({ questionId, fieldName }: { questionId: number; fieldName: string }) => {
+ const answerValue = useWatch({
+ control,
+ name: `${fieldName}.answerValue`
+ });
+
+ if (answerValue !== 'OTHER') return null;
+
+ return (
+ <Controller
+ name={`${fieldName}.otherText`}
+ control={control}
+ render={({ field }) => (
<Input
+ {...field}
placeholder="기타 λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”"
- value={answer?.otherText || ''}
- onChange={(e) => updateSurveyAnswer(question.id, 'otherText', e.target.value)}
className="mt-2"
/>
)}
- </div>
- )}
-
- {question.questionType === 'TEXTAREA' && (
- <Textarea
- placeholder="μƒμ„Έν•œ λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”"
- value={answer?.detailText || ''}
- onChange={(e) => updateSurveyAnswer(question.id, 'detailText', e.target.value)}
- rows={4}
/>
- )}
-
- {question.hasDetailText && answer?.answerValue === 'YES' && (
- <div className="mt-3">
- <Label className="text-sm text-gray-700 mb-2 block">상세 λ‚΄μš©μ„ κΈ°μˆ ν•΄μ£Όμ„Έμš”:</Label>
- <Textarea
- placeholder="μƒμ„Έν•œ λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”"
- value={answer?.detailText || ''}
- onChange={(e) => updateSurveyAnswer(question.id, 'detailText', e.target.value)}
- rows={3}
- className="w-full"
- />
- </div>
- )}
-
- {question.hasFileUpload && answer?.answerValue === 'YES' && (
- <div className="mt-3">
- <Label className="text-sm text-gray-700 mb-2 block">μ²¨λΆ€νŒŒμΌ:</Label>
- <div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
- <input
- type="file"
- multiple
- onChange={(e) => handleSurveyFileUpload(question.id, e.target.files)}
- className="hidden"
- id={`file-${question.id}`}
- />
- <label htmlFor={`file-${question.id}`} className="cursor-pointer">
- <div className="flex flex-col items-center">
- <Upload className="h-8 w-8 text-gray-400 mb-2" />
- <span className="text-sm text-gray-500">νŒŒμΌμ„ μ„ νƒν•˜κ±°λ‚˜ 여기에 λ“œλž˜κ·Έν•˜μ„Έμš”</span>
+ );
+ };
+
+ return (
+ <div className="h-full w-full flex flex-col">
+ <Card className="h-full flex flex-col">
+ <CardHeader className="flex-shrink-0">
+ <CardTitle className="flex items-center justify-between">
+ <div className="flex items-center">
+ <ClipboardList className="h-5 w-5 mr-2 text-amber-500" />
+ {surveyTemplate.name}
+ {conditionalHandler && (
+ <Badge variant="outline" className="ml-2 text-xs">
+ 쑰건뢀 질문 지원
+ </Badge>
+ )}
</div>
- </label>
-
- {uploadedFiles[question.id] && uploadedFiles[question.id].length > 0 && (
- <div className="mt-3 space-y-1">
- {uploadedFiles[question.id].map((file, index) => (
- <div key={index} className="flex items-center space-x-2 text-sm">
- <FileText className="h-4 w-4 text-blue-500" />
- <span>{file.name}</span>
- <span className="text-gray-500">({(file.size / 1024).toFixed(1)} KB)</span>
+ <div className="text-sm text-gray-600">
+ {progressStatus.completedRequired}/{progressStatus.totalRequired} μ™„λ£Œ
+ </div>
+ </CardTitle>
+
+ <CardDescription>
+ {surveyTemplate.description}
+
+ {/* 🎯 동적 질문 수 ν‘œμ‹œ */}
+ <div className="mt-2 space-y-1">
+ <div className="flex items-center text-sm">
+ <span className="text-gray-600">
+ πŸ“‹ 총 {totalVisibleQuestions}개 질문
+ {hasConditionalQuestions && (
+ <span className="text-blue-600 ml-1">
+ (κΈ°λ³Έ {baseQuestionCount}개 + 쑰건뢀 {conditionalQuestionCount}개)
+ </span>
+ )}
+ </span>
+ </div>
+
+ {hasConditionalQuestions && (
+ <div className="text-blue-600 text-sm">
+ ⚑ 닡변에 따라 {conditionalQuestionCount}개 μΆ”κ°€ 질문이 λ‚˜νƒ€λ‚¬μŠ΅λ‹ˆλ‹€
</div>
- ))}
+ )}
+ </div>
+ </CardDescription>
+
+ {/* πŸ“Š 동적 ν”„λ‘œκ·Έλ ˆμŠ€ λ°” */}
+ <div className="space-y-2">
+ <div className="flex justify-between text-xs text-gray-600">
+ <span>ν•„μˆ˜ 질문 μ§„ν–‰λ₯ </span>
+ <span>
+ {Math.round(progressStatus.progressPercentage)}%
+ {hasConditionalQuestions && (
+ <span className="ml-1 text-blue-600">
+ (쑰건뢀 포함)
+ </span>
+ )}
+ </span>
+ </div>
+ <div className="w-full bg-gray-200 rounded-full h-2">
+ <div
+ className="bg-gradient-to-r from-blue-500 to-blue-600 h-2 rounded-full transition-all duration-500 ease-out"
+ style={{ width: `${progressStatus.progressPercentage}%` }}
+ />
</div>
- )}
- </div>
- </div>
- )}
- </div>
- );
-};
- return (
- <div className="h-full w-full flex flex-col">
- <Card className="h-full flex flex-col">
- <CardHeader className="flex-shrink-0">
- <CardTitle className="flex items-center justify-between">
- <div className="flex items-center">
- <ClipboardList className="h-5 w-5 mr-2 text-amber-500" />
- {surveyTemplate.name}
- </div>
- <div className="text-sm text-gray-500">
- {completedCount}/{surveyTemplate.questions.length} μ™„λ£Œ
+ {/* μ„ΈλΆ€ μ§„ν–‰ 상황 */}
+ {progressStatus.totalRequired > 0 && (
+ <div className="text-xs text-gray-500 flex justify-between">
+ <span>μ™„λ£Œ: {progressStatus.completedRequired}개</span>
+ <span>남은 ν•„μˆ˜: {progressStatus.totalRequired - progressStatus.completedRequired}개</span>
+ </div>
+ )}
</div>
- </CardTitle>
- <CardDescription>
- {surveyTemplate.description}
- </CardDescription>
-
- <div className="w-full bg-gray-200 rounded-full h-2">
- <div
- className="bg-blue-600 h-2 rounded-full transition-all duration-300"
- style={{ width: `${progressPercentage}%` }}
- />
- </div>
- </CardHeader>
-
- <CardContent className="flex-1 min-h-0 overflow-y-auto">
- <div className="space-y-6">
- <div className="p-4 border rounded-lg bg-yellow-50">
- <div className="flex items-start">
- <AlertTriangle className="h-5 w-5 text-yellow-600 mt-0.5 mr-2" />
- <div>
- <p className="font-medium text-yellow-800">μ€‘μš” μ•ˆλ‚΄</p>
- <p className="text-sm text-yellow-700 mt-1">
- λ³Έ μ„€λ¬Έμ‘°μ‚¬λŠ” 쀀법 의무 확인을 μœ„ν•œ ν•„μˆ˜ μ ˆμ°¨μž…λ‹ˆλ‹€. λͺ¨λ“  ν•­λͺ©μ„ μ •ν™•νžˆ μž‘μ„±ν•΄μ£Όμ„Έμš”.
- </p>
+ </CardHeader>
+
+ <CardContent className="flex-1 min-h-0 overflow-y-auto">
+ <form onSubmit={(e) => e.preventDefault()} className="space-y-6">
+ <div className="p-4 border rounded-lg bg-yellow-50">
+ <div className="flex items-start">
+ <AlertTriangle className="h-5 w-5 text-yellow-600 mt-0.5 mr-2" />
+ <div>
+ <p className="font-medium text-yellow-800">μ€‘μš” μ•ˆλ‚΄</p>
+ <p className="text-sm text-yellow-700 mt-1">
+ λ³Έ μ„€λ¬Έμ‘°μ‚¬λŠ” 쀀법 의무 확인을 μœ„ν•œ ν•„μˆ˜ μ ˆμ°¨μž…λ‹ˆλ‹€. λͺ¨λ“  ν•­λͺ©μ„ μ •ν™•νžˆ μž‘μ„±ν•΄μ£Όμ„Έμš”.
+ {conditionalHandler && (
+ <span className="block mt-1">
+ ⚑ 닡변에 따라 μΆ”κ°€ 질문이 λ‚˜νƒ€λ‚  수 있으며, 이 경우 λͺ¨λ“  μΆ”κ°€ μ§ˆλ¬Έλ„ μ™„λ£Œν•΄μ•Ό ν•©λ‹ˆλ‹€.
+ </span>
+ )}
+ </p>
+ </div>
</div>
</div>
- </div>
- <div className="space-y-4">
- {surveyTemplate.questions.map((question: any) => renderSurveyQuestion(question))}
- </div>
+ <div className="space-y-4">
+ {visibleQuestions.map((question: any) => {
+ const fieldName = `${question.id}`;
+ const isComplete = progressStatus.completedQuestionIds.includes(question.id);
+ const isConditional = !!question.parentQuestionId;
- <div className="flex justify-end pt-6 border-t">
- <Button
- onClick={handleSurveyComplete}
- disabled={!isSurveyComplete()}
- className="bg-blue-600 hover:bg-blue-700"
- >
- <CheckCircle2 className="h-4 w-4 mr-2" />
- 섀문쑰사 μ™„λ£Œ
- </Button>
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- );
-};
-
-// 디버깅을 μœ„ν•œ useEffect
-useEffect(() => {
- if (isNDATemplate) {
- console.log("πŸ” NDA ν…œν”Œλ¦Ώ 디버깅:", {
- filePath,
- additionalFiles,
- allFiles,
- activeTab,
- isInitialLoaded,
- currentDocumentPath: currentDocumentPath.current,
- hasWebViewerInstance: !!webViewerInstance.current,
- hasParentInstance: !!instance,
- signatureFields,
- hasSignatureFields,
- isAutoSignProcessing,
- autoSignError
- });
- }
-}, [isNDATemplate, filePath, additionalFiles, allFiles, activeTab, isInitialLoaded, signatureFields, hasSignatureFields, isAutoSignProcessing, autoSignError]);
+ return (
+ <div
+ key={question.id}
+ id={`question-${question.id}`}
+ className={`mb-6 p-4 border rounded-lg transition-colors duration-200 ${isConditional
+ ? 'bg-blue-50 border-blue-200'
+ : 'bg-gray-50 border-gray-200'
+ } ${!isComplete && question.isRequired ? 'ring-2 ring-red-100' : ''}`}
+ >
+ <div className="flex items-start justify-between mb-3">
+ <div className="flex-1">
+ <Label className="text-sm font-medium text-gray-900 flex items-center flex-wrap gap-2">
+ <span className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
+ Q{question.questionNumber}
+ </span>
+
+ {isConditional && (
+ <span className="px-2 py-1 bg-amber-100 text-amber-700 rounded text-xs">
+ ⚑ 쑰건뢀 질문
+ </span>
+ )}
+
+ {question.questionType === 'FILE' && (
+ <span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">
+ πŸ“Ž 파일 μ—…λ‘œλ“œ
+ </span>
+ )}
+
+ <div className="w-full mt-1">
+ {question.questionText}
+ {question.isRequired && <span className="text-red-500 ml-1">*</span>}
+ </div>
+ </Label>
+ </div>
+
+ {isComplete && (
+ <CheckCircle2 className="h-5 w-5 text-green-500 ml-2 flex-shrink-0" />
+ )}
+ </div>
-// βœ… μ„œλͺ… ν•„λ“œ μƒνƒœ ν‘œμ‹œ μ»΄ν¬λ„ŒνŠΈ
-const SignatureFieldsStatus = () => {
- if (!hasSignatureFields && !isAutoSignProcessing && !autoSignError) return null;
+ {/* 질문 νƒ€μž…λ³„ λ Œλ”λ§ (κΈ°μ‘΄ μ½”λ“œμ™€ 동일) */}
+ {/* RADIO νƒ€μž… */}
+ {question.questionType === 'RADIO' && (
+ <Controller
+ name={`${fieldName}.answerValue`}
+ control={control}
+ rules={{ required: question.isRequired ? 'ν•„μˆ˜ ν•­λͺ©μž…λ‹ˆλ‹€.' : false }}
+ render={({ field }) => (
+ <RadioGroup
+ value={field.value || ''}
+ onValueChange={(value) => {
+ field.onChange(value);
+ handleAnswerChange(question.id, 'answerValue', value);
+ }}
+ className="space-y-2"
+ >
+ {question.options?.map((option: any) => (
+ <div key={option.id} className="flex items-center space-x-2">
+ <RadioGroupItem value={option.optionValue} id={`${question.id}-${option.id}`} />
+ <Label htmlFor={`${question.id}-${option.id}`} className="text-sm">
+ {option.optionText}
+ </Label>
+ </div>
+ ))}
+ </RadioGroup>
+ )}
+ />
+ )}
- return (
- <div className="mb-2">
- {isAutoSignProcessing ? (
- <Badge variant="secondary" className="text-xs">
- <Loader2 className="h-3 w-3 mr-1 animate-spin" />
- μ„œλͺ… ν•„λ“œ 생성 쀑...
- </Badge>
- ) : autoSignError ? (
- <Badge variant="destructive" className="text-xs bg-red-50 text-red-700 border-red-200">
- <AlertTriangle className="h-3 w-3 mr-1" />
- μžλ™ 생성 μ‹€νŒ¨
- </Badge>
- ) : hasSignatureFields ? (
- <Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
- <Target className="h-3 w-3 mr-1" />
- {signatureFields.length}개 μ„œλͺ… ν•„λ“œ μžλ™ 생성됨
- </Badge>
- ) : null}
- </div>
- );
-};
+ {/* DROPDOWN νƒ€μž… */}
+ {question.questionType === 'DROPDOWN' && (
+ <div className="space-y-2">
+ <Controller
+ name={`${fieldName}.answerValue`}
+ control={control}
+ rules={{ required: question.isRequired ? 'ν•„μˆ˜ ν•­λͺ©μž…λ‹ˆλ‹€.' : false }}
+ render={({ field }) => (
+ <Select
+ value={field.value || ''}
+ onValueChange={(value) => {
+ field.onChange(value);
+ handleAnswerChange(question.id, 'answerValue', value);
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="μ„ νƒν•΄μ£Όμ„Έμš”" />
+ </SelectTrigger>
+ <SelectContent>
+ {question.options?.map((option: any) => (
+ <SelectItem key={option.id} value={option.optionValue}>
+ {option.optionText}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ )}
+ />
+
+ <OtherTextInput questionId={question.id} fieldName={fieldName} />
+ </div>
+ )}
-// 인라인 λ·°μ–΄ λ Œλ”λ§ λΆ€λΆ„ μˆ˜μ •
-if (!isOpen && !onClose) {
- return (
- <div className="h-full w-full flex flex-col overflow-hidden">
- {allFiles.length > 1 ? (
- <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
- <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
- <SignatureFieldsStatus />
- <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
- {allFiles.map((file, index) => {
- let tabId: string;
- if (index === 0) {
- tabId = 'main';
- } else if (file.type === 'survey') {
- tabId = 'survey';
- } else {
- const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey').length;
- tabId = `file-${fileOnlyIndex}`;
- }
-
- return (
- <TabsTrigger key={tabId} value={tabId} className="text-xs">
- <div className="flex items-center space-x-1">
- {file.type === 'survey' ? (
- <ClipboardList className="h-3 w-3" />
- ) : (
- <FileText className="h-3 w-3" />
+ {/* TEXTAREA νƒ€μž… */}
+ {question.questionType === 'TEXTAREA' && (
+ <Controller
+ name={`${fieldName}.detailText`}
+ control={control}
+ rules={{ required: question.isRequired ? 'ν•„μˆ˜ ν•­λͺ©μž…λ‹ˆλ‹€.' : false }}
+ render={({ field }) => (
+ <Textarea
+ {...field}
+ placeholder="μƒμ„Έν•œ λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”"
+ rows={4}
+ />
+ )}
+ />
)}
- <span className="truncate">{file.name}</span>
- {file.type === 'survey' && surveyData.completed && (
- <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">μ™„λ£Œ</Badge>
+
+ {/* 상세 ν…μŠ€νŠΈ μž…λ ₯ */}
+ {question.hasDetailText && (
+ <div className="mt-3">
+ <Label className="text-sm text-gray-700 mb-2 block">상세 λ‚΄μš©μ„ κΈ°μˆ ν•΄μ£Όμ„Έμš”:</Label>
+ <Controller
+ name={`${fieldName}.detailText`}
+ control={control}
+ rules={{ required: question.isRequired ? '상세 λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”.' : false }}
+ render={({ field }) => (
+ <Textarea
+ {...field}
+ placeholder="μƒμ„Έν•œ λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”"
+ rows={3}
+ className="w-full"
+ />
+ )}
+ />
+ </div>
+ )}
+
+ {/* 파일 μ—…λ‘œλ“œ */}
+ {(question.hasFileUpload || question.questionType === 'FILE') && (
+ <div className="mt-3">
+ <Label className="text-sm text-gray-700 mb-2 block">μ²¨λΆ€νŒŒμΌ:</Label>
+ <div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
+ <input
+ type="file"
+ multiple
+ onChange={(e) => handleFileUpload(question.id, e.target.files)}
+ className="hidden"
+ id={`file-${question.id}`}
+ />
+ <label htmlFor={`file-${question.id}`} className="cursor-pointer">
+ <div className="flex flex-col items-center">
+ <Upload className="h-8 w-8 text-gray-400 mb-2" />
+ <span className="text-sm text-gray-500">νŒŒμΌμ„ μ„ νƒν•˜κ±°λ‚˜ 여기에 λ“œλž˜κ·Έν•˜μ„Έμš”</span>
+ </div>
+ </label>
+
+ {uploadedFiles[question.id] && uploadedFiles[question.id].length > 0 && (
+ <div className="mt-3 space-y-1">
+ {uploadedFiles[question.id].map((file, index) => (
+ <div key={index} className="flex items-center space-x-2 text-sm">
+ <FileText className="h-4 w-4 text-blue-500" />
+ <span>{file.name}</span>
+ <span className="text-gray-500">({(file.size / 1024).toFixed(1)} KB)</span>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+
+ {/* μ—λŸ¬ λ©”μ‹œμ§€ */}
+ {errors[fieldName] && (
+ <p className="mt-2 text-sm text-red-600 flex items-center">
+ <AlertTriangle className="h-4 w-4 mr-1" />
+ {errors[fieldName]?.answerValue?.message ||
+ errors[fieldName]?.detailText?.message ||
+ 'ν•„μˆ˜ ν•­λͺ©μ„ μ™„λ£Œν•΄μ£Όμ„Έμš”.'}
+ </p>
)}
</div>
- </TabsTrigger>
- );
- })}
- </TabsList>
- </div>
-
- <div className="flex-1 min-h-0 overflow-hidden relative">
- <div
- className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}
- >
- <SurveyComponent />
- </div>
-
- <div
- className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}
- >
- {/* βœ… μˆ˜μ •: λ™μΌν•œ ꡬ쑰둜 ν†΅μΌν•˜κ³  슀크둀 ν™œμ„±ν™” */}
- <div className="w-full h-full overflow-auto">
- <div
- ref={viewer}
- className="w-full h-full min-h-[400px]"
- style={{
- position: 'relative',
- // βœ… WebViewerκ°€ μŠ€ν¬λ‘€μ„ μ œμ–΄ν•˜λ„λ‘ μ„€μ •
- overflow: 'visible'
- }}
- >
- {fileLoading && (
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">λ¬Έμ„œ λ‘œλ”© 쀑...</p>
- </div>
- )}
- </div>
+ );
+ })}
</div>
- </div>
- </div>
- </Tabs>
- ) : (
- // βœ… μˆ˜μ •: Tabsκ°€ μ—†λŠ” κ²½μš°λ„ λ™μΌν•œ ꡬ쑰둜 λ³€κ²½
- <div className="h-full w-full flex flex-col">
- <div className="flex-shrink-0 p-2">
- <SignatureFieldsStatus />
- </div>
- <div className="flex-1 min-h-0 overflow-hidden relative">
- <div className="absolute inset-0">
- <div className="w-full h-full overflow-auto">
- <div
- ref={viewer}
- className="w-full h-full min-h-[400px]"
- style={{
- position: 'relative',
- // βœ… WebViewerκ°€ μŠ€ν¬λ‘€μ„ μ œμ–΄ν•˜λ„λ‘ μ„€μ •
- overflow: 'visible'
- }}
- >
- {fileLoading && (
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">λ¬Έμ„œ λ‘œλ”© 쀑...</p>
- </div>
- )}
+
+ {/* βœ… ν–₯μƒλœ μ™„λ£Œ λ²„νŠΌ */}
+ <div className="flex justify-end pt-6 border-t">
+ <div className="flex items-center space-x-4">
+ {/* μ§„ν–‰ 상황 μš”μ•½ */}
+ <div className="text-sm">
+ {canComplete ? (
+ <div className="text-green-600 font-medium flex items-center">
+ <CheckCircle2 className="h-4 w-4 mr-1" />
+ λͺ¨λ“  ν•„μˆ˜ ν•­λͺ© μ™„λ£Œλ¨
+ {hasConditionalQuestions && (
+ <span className="ml-2 text-xs text-blue-600">
+ (쑰건뢀 {conditionalQuestionCount}개 포함)
+ </span>
+ )}
+ </div>
+ ) : (
+ <div className="space-y-1">
+ <div className="flex items-center text-gray-600">
+ <AlertTriangle className="h-4 w-4 mr-1 text-red-500" />
+ {progressStatus.completedRequired}/{progressStatus.totalRequired} μ™„λ£Œ
+ </div>
+ {hasConditionalQuestions && (
+ <div className="text-xs text-blue-600">
+ κΈ°λ³Έ + 쑰건뢀 {conditionalQuestionCount}개 포함
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+
+ <Button
+ type="button"
+ onClick={handleSurveyComplete}
+ disabled={!canComplete || isSubmitting}
+ className={`transition-all duration-200 ${canComplete && !isSubmitting
+ ? 'bg-green-600 hover:bg-green-700 shadow-lg'
+ : 'bg-gray-400 cursor-not-allowed'
+ }`}
+ >
+ {isSubmitting ? (
+ <>
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
+ μ €μž₯ 쀑...
+ </>
+ ) : (
+ <>
+ <CheckCircle2 className="h-4 w-4 mr-2" />
+ 섀문쑰사 μ™„λ£Œ
+ </>
+ )}
+ <span className="ml-1 text-xs">
+ ({progressStatus.completedRequired}/{progressStatus.totalRequired})
+ </span>
+ </Button>
</div>
</div>
- </div>
- </div>
- </div>
- )}
- </div>
- );
-}
+ </form>
+ </CardContent>
+ </Card>
+ </div>
+ );
+ };
+
+ // πŸ”₯ μ„œλͺ… μƒνƒœ ν‘œμ‹œ μ»΄ν¬λ„ŒνŠΈ κ°œμ„ 
+ const SignatureFieldsStatus = () => {
+ if (!hasSignatureFields && !isAutoSignProcessing && !autoSignError && !hasValidSignature) return null;
-// λ‹€μ΄μ–Όλ‘œκ·Έ λ·°μ–΄ λ Œλ”λ§ 뢀뢄도 λ™μΌν•˜κ²Œ μˆ˜μ •
-return (
- <Dialog open={showDialog} onOpenChange={handleClose}>
- <DialogContent className="w-[90vw] max-w-6xl h-[90vh] flex flex-col p-0">
- <DialogHeader className="px-6 py-4 border-b flex-shrink-0">
- <DialogTitle className="flex items-center justify-between">
- <span>κΈ°λ³Έκ³„μ•½μ„œ μ„œλͺ…</span>
- <SignatureFieldsStatus />
- </DialogTitle>
- <DialogDescription>
- κ³„μ•½μ„œλ₯Ό ν™•μΈν•˜κ³  μ„œλͺ…을 μ§„ν–‰ν•΄μ£Όμ„Έμš”.
- {isComplianceTemplate && (
- <span className="block mt-1 text-amber-600">πŸ“‹ 쀀법 섀문쑰사λ₯Ό λ¨Όμ € μ™„λ£Œν•΄μ£Όμ„Έμš”.</span>
- )}
- {isNDATemplate && additionalFiles.length > 0 && (
- <span className="block mt-1 text-blue-600">πŸ“Ž μ²¨λΆ€μ„œλ₯˜ {additionalFiles.length}개λ₯Ό 각 νƒ­μ—μ„œ ν™•μΈν•΄μ£Όμ„Έμš”.</span>
- )}
- {hasSignatureFields && (
- <span className="block mt-1 text-green-600">
- 🎯 μ„œλͺ… μœ„μΉ˜κ°€ μžλ™μœΌλ‘œ κ°μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
- {signatureFields.some(f => f.includes('_text')) && (
- <span className="block text-sm text-amber-600">
- πŸ’‘ 빨간색 ν…μŠ€νŠΈλ‘œ ν‘œμ‹œλœ μ˜μ—­μ„ μ°Ύμ•„ μ„œλͺ…ν•΄μ£Όμ„Έμš”.
- </span>
- )}
- {signatureFields.some(f => f.startsWith('default_signature_')) && !signatureFields.some(f => f.includes('_text')) && (
- <span className="block text-sm text-amber-600">
- πŸ’‘ λ§ˆμ§€λ§‰ νŽ˜μ΄μ§€ ν•˜λ‹¨μ˜ 핑크색 μ˜μ—­μ—μ„œ μ„œλͺ…ν•΄μ£Όμ„Έμš”.
- </span>
- )}
- </span>
- )}
- {autoSignError && (
- <span className="block mt-1 text-red-600">⚠️ μžλ™ μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨ - μˆ˜λ™μœΌλ‘œ μ„œλͺ… μœ„μΉ˜λ₯Ό ν΄λ¦­ν•΄μ£Όμ„Έμš”.</span>
- )}
- </DialogDescription>
- </DialogHeader>
+ return (
+ <div className="mb-2 flex items-center space-x-2">
+ {isAutoSignProcessing ? (
+ <Badge variant="secondary" className="text-xs">
+ <Loader2 className="h-3 w-3 mr-1 animate-spin" />
+ μ„œλͺ… ν•„λ“œ 생성 쀑...
+ </Badge>
+ ) : autoSignError ? (
+ <Badge variant="destructive" className="text-xs bg-red-50 text-red-700 border-red-200">
+ <AlertTriangle className="h-3 w-3 mr-1" />
+ μžλ™ 생성 μ‹€νŒ¨
+ </Badge>
+ ) : hasSignatureFields ? (
+ <Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
+ <Target className="h-3 w-3 mr-1" />
+ {signatureFields.length}개 μ„œλͺ… ν•„λ“œ μžλ™ 생성됨
+ </Badge>
+ ) : null}
+
+ {/* πŸ”₯ μ„œλͺ… μ™„λ£Œ μƒνƒœ ν‘œμ‹œ */}
+ {hasValidSignature && (
+ <Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-200">
+ <CheckCircle2 className="h-3 w-3 mr-1" />
+ μ„œλͺ… μ™„λ£Œλ¨
+ </Badge>
+ )}
+ </div>
+ );
+ };
- <div className="flex-1 min-h-0 overflow-hidden">
+ // 인라인 λ·°μ–΄ λ Œλ”λ§
+ if (!isOpen && !onClose) {
+ return (
+ <div className="h-full w-full flex flex-col overflow-hidden">
{allFiles.length > 1 ? (
<Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
<div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
+ <SignatureFieldsStatus />
<TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
{allFiles.map((file, index) => {
- const tabId = index === 0 ? 'main' : file.type === 'survey' ? 'survey' : `file-${index}`;
+ let tabId: string;
+ if (index === 0) {
+ tabId = 'main';
+ } else if (file.type === 'survey') {
+ tabId = 'survey';
+ } else {
+ const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey').length;
+ tabId = `file-${fileOnlyIndex}`;
+ }
+
return (
<TabsTrigger key={tabId} value={tabId} className="text-xs">
<div className="flex items-center space-x-1">
- {file.type === 'survey' ? <ClipboardList className="h-3 w-3" /> : <FileText className="h-3 w-3" />}
+ {file.type === 'survey' ? (
+ <ClipboardList className="h-3 w-3" />
+ ) : (
+ <FileText className="h-3 w-3" />
+ )}
<span className="truncate">{file.name}</span>
{file.type === 'survey' && surveyData.completed && (
<Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">μ™„λ£Œ</Badge>
@@ -1520,17 +1901,20 @@ return (
</div>
<div className="flex-1 min-h-0 overflow-hidden relative">
- <div className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}>
+ <div
+ className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}
+ >
<SurveyComponent />
</div>
- <div className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}>
- {/* βœ… μˆ˜μ •: 슀크둀 ν™œμ„±ν™” */}
+ <div
+ className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}
+ >
<div className="w-full h-full overflow-auto">
- <div
- ref={viewer}
+ <div
+ ref={viewer}
className="w-full h-full min-h-[400px]"
- style={{
+ style={{
position: 'relative',
overflow: 'visible'
}}
@@ -1547,15 +1931,17 @@ return (
</div>
</Tabs>
) : (
- // βœ… μˆ˜μ •: λ‹€μ΄μ–Όλ‘œκ·Έμ—μ„œ λ·°μ–΄λ§Œ μžˆλŠ” κ²½μš°λ„ λ™μΌν•œ ꡬ쑰
- <div className="h-full flex flex-col">
+ <div className="h-full w-full flex flex-col">
+ <div className="flex-shrink-0 p-2">
+ <SignatureFieldsStatus />
+ </div>
<div className="flex-1 min-h-0 overflow-hidden relative">
<div className="absolute inset-0">
<div className="w-full h-full overflow-auto">
- <div
- ref={viewer}
+ <div
+ ref={viewer}
className="w-full h-full min-h-[400px]"
- style={{
+ style={{
position: 'relative',
overflow: 'visible'
}}
@@ -1573,133 +1959,120 @@ return (
</div>
)}
</div>
+ );
+ }
- <DialogFooter className="px-6 py-4 border-t bg-white flex-shrink-0">
- <Button variant="outline" onClick={handleClose} disabled={fileLoading}>μ·¨μ†Œ</Button>
- <Button onClick={handleSave} disabled={fileLoading || isAutoSignProcessing}>
- <FileSignature className="h-4 w-4 mr-2" />
- μ„œλͺ… μ™„λ£Œ
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
-);
-
-// λ‹€μ΄μ–Όλ‘œκ·Έ λ·°μ–΄ λ Œλ”λ§
-return (
- <Dialog open={showDialog} onOpenChange={handleClose}>
- <DialogContent className="w-[90vw] max-w-6xl h-[90vh] flex flex-col p-0">
- <DialogHeader className="px-6 py-4 border-b flex-shrink-0">
- <DialogTitle className="flex items-center justify-between">
- <span>κΈ°λ³Έκ³„μ•½μ„œ μ„œλͺ…</span>
- <SignatureFieldsStatus />
- </DialogTitle>
- <DialogDescription>
- κ³„μ•½μ„œλ₯Ό ν™•μΈν•˜κ³  μ„œλͺ…을 μ§„ν–‰ν•΄μ£Όμ„Έμš”.
- {isComplianceTemplate && (
- <span className="block mt-1 text-amber-600">πŸ“‹ 쀀법 섀문쑰사λ₯Ό λ¨Όμ € μ™„λ£Œν•΄μ£Όμ„Έμš”.</span>
- )}
- {isNDATemplate && additionalFiles.length > 0 && (
- <span className="block mt-1 text-blue-600">πŸ“Ž μ²¨λΆ€μ„œλ₯˜ {additionalFiles.length}개λ₯Ό 각 νƒ­μ—μ„œ ν™•μΈν•΄μ£Όμ„Έμš”.</span>
- )}
- {hasSignatureFields && (
- <span className="block mt-1 text-green-600">
- 🎯 μ„œλͺ… μœ„μΉ˜κ°€ μžλ™μœΌλ‘œ κ°μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
- {signatureFields.some(f => f.includes('_text')) && (
- <span className="block text-sm text-amber-600">
- πŸ’‘ 빨간색 ν…μŠ€νŠΈλ‘œ ν‘œμ‹œλœ μ˜μ—­μ„ μ°Ύμ•„ μ„œλͺ…ν•΄μ£Όμ„Έμš”.
- </span>
- )}
- {signatureFields.some(f => f.startsWith('default_signature_')) && !signatureFields.some(f => f.includes('_text')) && (
- <span className="block text-sm text-amber-600">
- πŸ’‘ λ§ˆμ§€λ§‰ νŽ˜μ΄μ§€ ν•˜λ‹¨μ˜ 핑크색 μ˜μ—­μ—μ„œ μ„œλͺ…ν•΄μ£Όμ„Έμš”.
- </span>
- )}
- </span>
- )}
- {autoSignError && (
- <span className="block mt-1 text-red-600">⚠️ μžλ™ μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨ - μˆ˜λ™μœΌλ‘œ μ„œλͺ… μœ„μΉ˜λ₯Ό ν΄λ¦­ν•΄μ£Όμ„Έμš”.</span>
- )}
- </DialogDescription>
- </DialogHeader>
-
- <div className="flex-1 min-h-0 overflow-hidden">
- {allFiles.length > 1 ? (
- <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
- <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
- <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
- {allFiles.map((file, index) => {
- const tabId = index === 0 ? 'main' : file.type === 'survey' ? 'survey' : `file-${index}`;
- return (
- <TabsTrigger key={tabId} value={tabId} className="text-xs">
- <div className="flex items-center space-x-1">
- {file.type === 'survey' ? <ClipboardList className="h-3 w-3" /> : <FileText className="h-3 w-3" />}
- <span className="truncate">{file.name}</span>
- {file.type === 'survey' && surveyData.completed && (
- <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">μ™„λ£Œ</Badge>
- )}
- </div>
- </TabsTrigger>
- );
- })}
- </TabsList>
- </div>
-
- <div className="flex-1 min-h-0 overflow-hidden relative">
- <div className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}>
- <SurveyComponent />
+ // λ‹€μ΄μ–Όλ‘œκ·Έ λ·°μ–΄ λ Œλ”λ§
+ return (
+ <Dialog open={showDialog} onOpenChange={handleClose}>
+ <DialogContent className="w-[90vw] max-w-6xl h-[90vh] flex flex-col p-0">
+ <DialogHeader className="px-6 py-4 border-b flex-shrink-0">
+ <DialogTitle className="flex items-center justify-between">
+ <span>κΈ°λ³Έκ³„μ•½μ„œ μ„œλͺ…</span>
+ <SignatureFieldsStatus />
+ </DialogTitle>
+ <DialogDescription>
+ κ³„μ•½μ„œλ₯Ό ν™•μΈν•˜κ³  μ„œλͺ…을 μ§„ν–‰ν•΄μ£Όμ„Έμš”.
+ {isComplianceTemplate && (
+ <span className="block mt-1 text-amber-600">πŸ“‹ 쀀법 섀문쑰사λ₯Ό λ¨Όμ € μ™„λ£Œν•΄μ£Όμ„Έμš”.</span>
+ )}
+ {hasSignatureFields && (
+ <span className="block mt-1 text-green-600">
+ 🎯 μ„œλͺ… μœ„μΉ˜κ°€ μžλ™μœΌλ‘œ κ°μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
+ </span>
+ )}
+ {/* πŸ”₯ μ„œλͺ… μ™„λ£Œ μƒνƒœ μ•ˆλ‚΄ */}
+ {hasValidSignature && (
+ <span className="block mt-1 text-green-600">
+ βœ… μ„œλͺ…이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.
+ </span>
+ )}
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex-1 min-h-0 overflow-hidden">
+ {allFiles.length > 1 ? (
+ <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
+ <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
+ <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
+ {allFiles.map((file, index) => {
+ const tabId = index === 0 ? 'main' : file.type === 'survey' ? 'survey' : `file-${index}`;
+ return (
+ <TabsTrigger key={tabId} value={tabId} className="text-xs">
+ <div className="flex items-center space-x-1">
+ {file.type === 'survey' ? <ClipboardList className="h-3 w-3" /> : <FileText className="h-3 w-3" />}
+ <span className="truncate">{file.name}</span>
+ {file.type === 'survey' && surveyData.completed && (
+ <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">μ™„λ£Œ</Badge>
+ )}
+ </div>
+ </TabsTrigger>
+ );
+ })}
+ </TabsList>
</div>
- <div className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}>
- <div
- ref={viewer}
- className="w-full h-full"
- style={{ position: 'relative', minHeight: '400px' }}
- >
- {fileLoading && (
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">λ¬Έμ„œ λ‘œλ”© 쀑...</p>
+ <div className="flex-1 min-h-0 overflow-hidden relative">
+ <div className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}>
+ <SurveyComponent />
+ </div>
+
+ <div className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}>
+ <div className="w-full h-full overflow-auto">
+ <div
+ ref={viewer}
+ className="w-full h-full min-h-[400px]"
+ style={{
+ position: 'relative',
+ overflow: 'visible'
+ }}
+ >
+ {fileLoading && (
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">λ¬Έμ„œ λ‘œλ”© 쀑...</p>
+ </div>
+ )}
</div>
- )}
+ </div>
</div>
</div>
- </div>
- </Tabs>
- ) : (
- <div className="h-full relative">
- <div
- ref={viewer}
- className="absolute inset-0"
- style={{ position: 'relative', minHeight: '400px' }}
- >
- {fileLoading && (
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">λ¬Έμ„œ λ‘œλ”© 쀑...</p>
+ </Tabs>
+ ) : (
+ <div className="h-full flex flex-col">
+ <div className="flex-1 min-h-0 overflow-hidden relative">
+ <div className="absolute inset-0">
+ <div className="w-full h-full overflow-auto">
+ <div
+ ref={viewer}
+ className="w-full h-full min-h-[400px]"
+ style={{
+ position: 'relative',
+ overflow: 'visible'
+ }}
+ >
+ {fileLoading && (
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">λ¬Έμ„œ λ‘œλ”© 쀑...</p>
+ </div>
+ )}
+ </div>
+ </div>
</div>
- )}
+ </div>
</div>
- </div>
- )}
- </div>
-
- <DialogFooter className="px-6 py-4 border-t bg-white flex-shrink-0">
- <Button variant="outline" onClick={handleClose} disabled={fileLoading}>μ·¨μ†Œ</Button>
- <Button onClick={handleSave} disabled={fileLoading || isAutoSignProcessing}>
- <FileSignature className="h-4 w-4 mr-2" />
- μ„œλͺ… μ™„λ£Œ
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
-);
-}
+ )}
+ </div>
-// WebViewer 정리 ν•¨μˆ˜
-const cleanupHtmlStyle = () => {
-const elements = document.querySelectorAll('.Document_container');
-elements.forEach((elem) => {
- elem.remove();
-});
-}; \ No newline at end of file
+ <DialogFooter className="px-6 py-4 border-t bg-white flex-shrink-0">
+ <Button variant="outline" onClick={handleClose} disabled={fileLoading}>μ·¨μ†Œ</Button>
+ <Button onClick={handleSave} disabled={fileLoading || isAutoSignProcessing}>
+ <FileSignature className="h-4 w-4 mr-2" />
+ μ„œλͺ… μ™„λ£Œ
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+} \ No newline at end of file