diff options
Diffstat (limited to 'lib/basic-contract/viewer')
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 2975 |
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 |
