"use client"; import React, { 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"; import { toast } from "sonner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 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, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 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'; } interface BasicContractSignViewerProps { contractId?: number; filePath?: string; additionalFiles?: FileInfo[]; templateName?: string; isOpen?: boolean; onClose?: () => void; onSign?: (documentData: ArrayBuffer, surveyData?: any) => Promise; instance: WebViewerInstance | null; setInstance: Dispatch>; 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; priority: number; offsetX?: number; offsetY?: number; width?: number; height?: number; } // μ΄ˆκ°„λ‹¨ μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 감지 클래슀 class AutoSignatureFieldDetector { private instance: WebViewerInstance; private signaturePatterns: SignaturePattern[]; constructor(instance: WebViewerInstance) { this.instance = instance; this.signaturePatterns = this.initializePatterns(); } private initializePatterns(): SignaturePattern[] { return [ { regex: /μ„œλͺ…\s*[::]\s*[_\-\s]{3,}/gi, name: "ν•œκ΅­μ–΄_μ„œλͺ…_콜둠", priority: 10, offsetX: 80, offsetY: -5, width: 150, height: 40 }, { regex: /μ„œλͺ…λž€\s*[_\-\s]{0,}/gi, name: "ν•œκ΅­μ–΄_μ„œλͺ…λž€", priority: 9, offsetX: 60, offsetY: -5, width: 150, height: 40 }, { regex: /signature\s*[::]\s*[_\-\s]{3,}/gi, name: "μ˜μ–΄_signature_콜둠", priority: 8, offsetX: 120, offsetY: -5, width: 150, height: 40 }, { regex: /sign\s+here\s*[::]?\s*[_\-\s]{0,}/gi, name: "μ˜μ–΄_sign_here", priority: 9, offsetX: 100, offsetY: -5, width: 150, height: 40 } ]; } async detectAndCreateSignatureFields(): Promise { console.log("πŸ” μ•ˆμ „ν•œ μ„œλͺ… ν•„λ“œ 감지 μ‹œμž‘..."); try { if (!this.instance?.Core?.documentViewer) { throw new Error("WebViewer μΈμŠ€ν„΄μŠ€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); } const { Core } = this.instance; const { documentViewer } = Core; const document = documentViewer.getDocument(); if (!document) { throw new Error("PDF λ¬Έμ„œκ°€ λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."); } console.log("πŸ“„ λ¬Έμ„œ 확인 μ™„λ£Œ, κΈ°λ³Έ μ„œλͺ… ν•„λ“œ 생성..."); const defaultField = await this.createSimpleSignatureField(); console.log("βœ… μ„œλͺ… ν•„λ“œ 생성 μ™„λ£Œ"); return [defaultField]; } catch (error) { console.error("πŸ“› μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨:", error); let errorMessage = "μ„œλͺ… ν•„λ“œ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."; if (error instanceof Error) { if (error.message.includes("μΈμŠ€ν„΄μŠ€")) { errorMessage = "λ·°μ–΄κ°€ μ€€λΉ„λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."; } else if (error.message.includes("λ¬Έμ„œ")) { errorMessage = "λ¬Έμ„œλ₯Ό λΆˆλŸ¬μ˜€λŠ” μ€‘μž…λ‹ˆλ‹€."; } } throw new Error(errorMessage); } } private async createSimpleSignatureField(): Promise { 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; console.log(`πŸ“ νŽ˜μ΄μ§€ 정보: ${pageCount}νŽ˜μ΄μ§€, 크기 ${pageWidth}x${pageHeight}`); const fieldName = `simple_signature_${Date.now()}`; const flags = new Annotations.WidgetFlags(); const formField = new Core.Annotations.Forms.Field( `SignatureFormField`, { type: "Sig", flags, } ); 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); annotationManager.addAnnotation(signatureWidget); annotationManager.redrawAnnotation(signatureWidget); console.log(`βœ… μ„œλͺ… ν•„λ“œ 생성: ${fieldName}`); return fieldName; } catch (error) { console.error("πŸ“› μ„œλͺ… ν•„λ“œ 생성 μ‹€νŒ¨:", error); return "manual_signature_required"; } } } function useAutoSignatureFields(instance: WebViewerInstance | null) { const [signatureFields, setSignatureFields] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState(null); const processingRef = useRef(false); const timeoutRef = useRef(null); useEffect(() => { if (!instance) return; const { documentViewer } = instance.Core; const handleDocumentLoaded = () => { if (processingRef.current) { console.log("πŸ“› 이미 처리 μ€‘μ΄λ―€λ‘œ μŠ€ν‚΅"); return; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } timeoutRef.current = setTimeout(async () => { if (processingRef.current) return; processingRef.current = true; setIsProcessing(true); setError(null); try { 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_')); if (hasSimpleField) { toast.success("πŸ“ μ„œλͺ… ν•„λ“œκ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", { description: "λ§ˆμ§€λ§‰ νŽ˜μ΄μ§€ ν•˜λ‹¨μ˜ νŒŒλž€μƒ‰ μ˜μ—­μ—μ„œ μ„œλͺ…ν•΄μ£Όμ„Έμš”.", icon: , duration: 5000 }); } else { toast.success(`πŸ“‹ ${fields.length}개의 μ„œλͺ… ν•„λ“œλ₯Ό ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.`, { description: "κΈ°μ‘΄ μ„œλͺ… ν•„λ“œκ°€ λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", icon: , duration: 4000 }); } } } catch (error) { console.error("πŸ“› μ„œλͺ… ν•„λ“œ 처리 μ‹€νŒ¨:", error); const errorMessage = error instanceof Error ? error.message : "μ„œλͺ… ν•„λ“œ μ²˜λ¦¬μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."; setError(errorMessage); toast.info("μˆ˜λ™ μ„œλͺ… λͺ¨λ“œ", { description: "λ¬Έμ„œμ—μ„œ μ„œλͺ…ν•  μœ„μΉ˜λ₯Ό 직접 ν΄λ¦­ν•΄μ£Όμ„Έμš”.", icon: }); } finally { setIsProcessing(false); processingRef.current = false; } }, 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) { clearTimeout(timeoutRef.current); } processingRef.current = false; }; }, []); return { signatureFields, isProcessing, hasSignatureFields: signatureFields.length > 0, error }; } // πŸ”₯ μ„œλͺ… 감지λ₯Ό μœ„ν•œ μ»€μŠ€ν…€ ν›… μˆ˜μ • function useSignatureDetection(instance: WebViewerInstance | null, onSignatureComplete?: () => void) { const [hasValidSignature, setHasValidSignature] = useState(false); const checkIntervalRef = useRef(null); const lastSignatureStateRef = useRef(false); const onSignatureCompleteRef = useRef(onSignatureComplete); // 콜백 레퍼런슀 μ—…λ°μ΄νŠΈ useEffect(() => { onSignatureCompleteRef.current = onSignatureComplete; }, [onSignatureComplete]); const checkSignatureFields = useCallback(async () => { if (!instance?.Core?.annotationManager) { console.log('πŸ” μ„œλͺ… 체크: annotationManager μ—†μŒ'); return false; } try { 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(); 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.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; const handleDocumentLoaded = () => { console.log('πŸ“„ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ, μ„œλͺ… λͺ¨λ‹ˆν„°λ§ μ€€λΉ„'); // λ¬Έμ„œ λ‘œλ“œ ν›„ 3초 뒀에 λͺ¨λ‹ˆν„°λ§ μ‹œμž‘ (μ•ˆμ •μ„± 확보) setTimeout(startMonitoring, 3000); }; 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(true); const [activeTab, setActiveTab] = useState("main"); const [surveyData, setSurveyData] = useState({}); const [isInitialLoaded, setIsInitialLoaded] = useState(false); const [surveyTemplate, setSurveyTemplate] = useState(null); const [surveyLoading, setSurveyLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); // 제좜 μƒνƒœ μΆ”κ°€ const conditionalHandler = useConditionalSurvey(surveyTemplate); const viewer = useRef(null); const initialized = useRef(false); const isCancelled = useRef(false); const currentDocumentPath = useRef(""); const [showDialog, setShowDialog] = useState(isOpen); const webViewerInstance = useRef(null); const { signatureFields, isProcessing: isAutoSignProcessing, hasSignatureFields, error: autoSignError } = useAutoSignatureFields(webViewerInstance.current || instance); // πŸ”₯ μ„œλͺ… 감지 ν›… μ‚¬μš© 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 = ""; 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}`; 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); } }; useEffect(() => { if (!initialized.current && viewer.current) { initialized.current = true; isCancelled.current = false; const initializeWebViewer = () => { if (!viewer.current || isCancelled.current) { return; } const viewerElement = viewer.current; if (!viewerElement.isConnected) { setTimeout(initializeWebViewer, 100); return; } cleanupWebViewer(); import("@pdftron/webviewer").then(({ default: WebViewer }) => { if (isCancelled.current || !viewer.current) { return; } 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); 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 = "λ¬Έμ„œμ— μ ‘κ·Όν•  κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€."; } } if (showToast) { setFileLoading(false); toast.error(errorMessage); } }); }).catch((error) => { console.error("πŸ“› WebViewer μ΄ˆκΈ°ν™” μ‹€νŒ¨:", error); setFileLoading(false); toast.error("λ·°μ–΄ μ΄ˆκΈ°ν™”μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); }); }).catch((error) => { console.error("πŸ“› WebViewer λͺ¨λ“ˆ λ‘œλ“œ μ‹€νŒ¨:", error); setFileLoading(false); toast.error("λ·°μ–΄ λͺ¨λ“ˆμ„ λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); }); }; requestAnimationFrame(() => { setTimeout(initializeWebViewer, 50); }); } return () => { isCancelled.current = true; cleanupWebViewer(); }; }, [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) { return; } setFileLoading(true); try { if (!instance || !instance.UI || !instance.Core) { throw new Error("WebViewer μΈμŠ€ν„΄μŠ€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); } const ext = getExtFromPath(documentPath); await instance.UI.loadDocument(documentPath, { ...(ext ? { extension: ext } : {}), filename: documentPath.split("/").pop(), }); 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); } }; const collectFormData = async (instance: WebViewerInstance) => { try { const { annotationManager } = instance.Core; const fieldManager = annotationManager.getFieldManager(); const fields = fieldManager.getFields(); const formData: any = {}; fields.forEach((field: any) => { formData[field.name] = field.value; }); 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]; } if (!targetFile?.path) { console.warn("πŸ“› λŒ€μƒ νŒŒμΌμ„ 찾을 수 μ—†μŒ:", newTab, allFiles); 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 { currentDocumentPath.current = ""; await loadDocument(currentInstance, apiFilePath, true); setIsInitialLoaded(true); 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 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}`; 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 { setShowDialog(false); } }; // κ°œμ„ λœ SurveyComponent const SurveyComponent = () => { const { control, watch, setValue, getValues, formState: { errors }, trigger, } = useForm({ defaultValues: {}, mode: 'onChange' }); const watchedValues = watch(); const [uploadedFiles, setUploadedFiles] = useState>({}); // πŸ“Š μ‹€μ‹œκ°„ μ§„ν–‰ μƒνƒœ 계산 const progressStatus = useMemo(() => { if (!conditionalHandler || !surveyTemplate) { return { visibleQuestions: [], totalRequired: 0, completedRequired: 0, completedQuestionIds: [], incompleteQuestionIds: [], progressPercentage: 0, debugInfo: {} }; } console.log('πŸ”„ μ‹€μ‹œκ°„ ν”„λ‘œκ·Έλ ˆμŠ€ μž¬κ³„μ‚° 쀑...'); console.log('πŸ“ 원본 watchedValues:', watchedValues); // ν˜„μž¬ 닡변을 쑰건뢀 ν•Έλ“€λŸ¬κ°€ 인식할 수 μžˆλŠ” ν˜•νƒœλ‘œ λ³€ν™˜ const convertedAnswers: Record = {}; 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 }); } }); 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 (

섀문쑰사λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...

); } if (!surveyTemplate) { return (

섀문쑰사 ν…œν”Œλ¦Ώμ„ 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.

); } // 🚨 ν…œν”Œλ¦Ώμ΄ λ‘œλ“œλ˜λ©΄ λͺ¨λ“  μ§ˆλ¬Έλ“€μ˜ 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 = {}; 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: , 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: , 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: , duration: 8000 }); } } catch (error) { console.error('❌ 섀문쑰사 μ €μž₯ 쀑 μ˜ˆμ™Έ λ°œμƒ:', error); let errorMessage = '섀문쑰사 μ €μž₯에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'; let errorDescription = 'λ„€νŠΈμ›Œν¬ 연결을 ν™•μΈν•˜κ³  λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.'; if (error instanceof Error) { errorDescription = error.message; } toast.error(errorMessage, { description: errorDescription, icon: , 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 ( ( )} /> ); }; return (
{surveyTemplate.name} {conditionalHandler && ( 쑰건뢀 질문 지원 )}
{progressStatus.completedRequired}/{progressStatus.totalRequired} μ™„λ£Œ
{surveyTemplate.description} {/* 🎯 동적 질문 수 ν‘œμ‹œ */}
πŸ“‹ 총 {totalVisibleQuestions}개 질문 {hasConditionalQuestions && ( (κΈ°λ³Έ {baseQuestionCount}개 + 쑰건뢀 {conditionalQuestionCount}개) )}
{hasConditionalQuestions && (
⚑ 닡변에 따라 {conditionalQuestionCount}개 μΆ”κ°€ 질문이 λ‚˜νƒ€λ‚¬μŠ΅λ‹ˆλ‹€
)}
{/* πŸ“Š 동적 ν”„λ‘œκ·Έλ ˆμŠ€ λ°” */}
ν•„μˆ˜ 질문 μ§„ν–‰λ₯  {Math.round(progressStatus.progressPercentage)}% {hasConditionalQuestions && ( (쑰건뢀 포함) )}
{/* μ„ΈλΆ€ μ§„ν–‰ 상황 */} {progressStatus.totalRequired > 0 && (
μ™„λ£Œ: {progressStatus.completedRequired}개 남은 ν•„μˆ˜: {progressStatus.totalRequired - progressStatus.completedRequired}개
)}
e.preventDefault()} className="space-y-6">

μ€‘μš” μ•ˆλ‚΄

λ³Έ μ„€λ¬Έμ‘°μ‚¬λŠ” 쀀법 의무 확인을 μœ„ν•œ ν•„μˆ˜ μ ˆμ°¨μž…λ‹ˆλ‹€. λͺ¨λ“  ν•­λͺ©μ„ μ •ν™•νžˆ μž‘μ„±ν•΄μ£Όμ„Έμš”. {conditionalHandler && ( ⚑ 닡변에 따라 μΆ”κ°€ 질문이 λ‚˜νƒ€λ‚  수 있으며, 이 경우 λͺ¨λ“  μΆ”κ°€ μ§ˆλ¬Έλ„ μ™„λ£Œν•΄μ•Ό ν•©λ‹ˆλ‹€. )}

{visibleQuestions.map((question: any) => { const fieldName = `${question.id}`; const isComplete = progressStatus.completedQuestionIds.includes(question.id); const isConditional = !!question.parentQuestionId; return (
{isComplete && ( )}
{/* 질문 νƒ€μž…λ³„ λ Œλ”λ§ (κΈ°μ‘΄ μ½”λ“œμ™€ 동일) */} {/* RADIO νƒ€μž… */} {question.questionType === 'RADIO' && ( ( { field.onChange(value); handleAnswerChange(question.id, 'answerValue', value); }} className="space-y-2" > {question.options?.map((option: any) => (
))}
)} /> )} {/* DROPDOWN νƒ€μž… */} {question.questionType === 'DROPDOWN' && (
( )} />
)} {/* TEXTAREA νƒ€μž… */} {question.questionType === 'TEXTAREA' && ( (