diff options
Diffstat (limited to 'lib/basic-contract/viewer/basic-contract-sign-viewer.tsx')
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 1515 |
1 files changed, 400 insertions, 1115 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index fbf36738..943878da 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -6,16 +6,13 @@ import React, { useRef, SetStateAction, Dispatch, - useMemo, - useCallback, } from "react"; import { WebViewerInstance } from "@pdftron/webviewer"; -import { Loader2, FileText, ClipboardList, AlertTriangle, FileSignature, Target, CheckCircle2 } from "lucide-react"; +import { Loader2, FileText, ClipboardList, AlertTriangle, FileSignature, Target, CheckCircle2, BookOpen } 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, @@ -24,20 +21,15 @@ import { 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"; +import { getActiveSurveyTemplate, getVendorSignatureFile, type SurveyTemplateWithQuestions } from '../service'; +import { useConditionalSurvey } from '../vendor-table/survey-conditional'; +import { SurveyComponent } from './SurveyComponent'; +import { GtcClausesComponent } from './GtcClausesComponent'; interface FileInfo { path: string; name: string; - type: 'main' | 'attachment' | 'survey'; + type: 'main' | 'attachment' | 'survey' | 'clauses'; } interface BasicContractSignViewerProps { @@ -50,21 +42,13 @@ interface BasicContractSignViewerProps { onSign?: (documentData: ArrayBuffer, surveyData?: any) => Promise<void>; instance: WebViewerInstance | null; setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>; - onSurveyComplete?: () => void; // π₯ μλ‘ μΆκ° - onSignatureComplete?: () => void; // π₯ μλ‘ μΆκ° + onSurveyComplete?: () => void; + onSignatureComplete?: () => void; + onGtcCommentStatusChange?: (hasComments: boolean, commentCount: number) => void; + mode?: 'vendor' | 'buyer'; // μΆκ°λ mode prop t?: (key: string) => string; } -// νΌ λ°μ΄ν° νμ
μ μ -interface SurveyFormData { - [key: string]: { - answerValue?: string; - detailText?: string; - otherText?: string; - files?: File[]; - }; -} - // μλ μλͺ
νλ μμ±μ μν νμ
μ μ interface SignaturePattern { regex: RegExp; @@ -80,9 +64,11 @@ interface SignaturePattern { class AutoSignatureFieldDetector { private instance: WebViewerInstance; private signaturePatterns: SignaturePattern[]; + private mode: 'vendor' | 'buyer'; // mode μΆκ° - constructor(instance: WebViewerInstance) { + constructor(instance: WebViewerInstance, mode: 'vendor' | 'buyer' = 'vendor') { this.instance = instance; + this.mode = mode; this.signaturePatterns = this.initializePatterns(); } @@ -128,7 +114,7 @@ class AutoSignatureFieldDetector { } async detectAndCreateSignatureFields(): Promise<string[]> { - console.log("π μμ ν μλͺ
νλ κ°μ§ μμ..."); + console.log(`π μμ ν μλͺ
νλ κ°μ§ μμ... (λͺ¨λ: ${this.mode})`); try { if (!this.instance?.Core?.documentViewer) { @@ -166,77 +152,87 @@ class AutoSignatureFieldDetector { } private async createSimpleSignatureField(): Promise<string> { - try { - const { Core } = this.instance; - const { documentViewer, annotationManager, Annotations } = Core; + 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 page = documentViewer.getPageCount(); + const w = documentViewer.getPageWidth(page) || 612; + const h = documentViewer.getPageHeight(page) || 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 - }); + const fieldName = `simple_signature_${Date.now()}`; + const flags = new Annotations.WidgetFlags(); + flags.set('Required', true); - signatureWidget.setPageNumber(pageCount); - signatureWidget.setX(pageWidth * 0.7); - signatureWidget.setY(pageHeight * 0.85); - signatureWidget.setWidth(150); - signatureWidget.setHeight(50); + const field = new Core.Annotations.Forms.Field(fieldName, { type: 'Sig', flags }); - annotationManager.addAnnotation(signatureWidget); - annotationManager.redrawAnnotation(signatureWidget); + const widget = new Annotations.SignatureWidgetAnnotation(field, { Width: 150, Height: 50 }); + widget.setPageNumber(page); + + // ꡬ맀μ λͺ¨λμΌ λλ μΌμͺ½ νλ¨μΌλ‘ μμΉ μ€μ + if (this.mode === 'buyer') { + widget.setX(w * 0.1); // μΌμͺ½ (10%) + widget.setY(h * 0.85); // νλ¨ (85%) + } else { + // νλ ₯μ
체 λͺ¨λμΌ λλ κΈ°μ‘΄μ²λΌ μ€λ₯Έμͺ½ + widget.setX(w * 0.7); // μ€λ₯Έμͺ½ (70%) + widget.setY(h * 0.85); // νλ¨ (85%) + } + + widget.setWidth(150); + widget.setHeight(50); - console.log(`β
μλͺ
νλ μμ±: ${fieldName}`); - return fieldName; + const fm = annotationManager.getFieldManager(); + fm.addField(field); + annotationManager.addAnnotation(widget); + annotationManager.drawAnnotationsFromList([widget]); - } catch (error) { - console.error("π μλͺ
νλ μμ± μ€ν¨:", error); - return "manual_signature_required"; - } + return fieldName; } } -function useAutoSignatureFields(instance: WebViewerInstance | null) { +function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendor' | 'buyer' = 'vendor') { 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); + const processedDocumentRef = useRef<string | null>(null); // μ²λ¦¬λ λ¬Έμ μΆμ + const handlerRef = useRef<(() => void) | null>(null); // νΈλ€λ¬ μ°Έμ‘° μ μ₯ useEffect(() => { if (!instance) return; const { documentViewer } = instance.Core; + // μλ‘μ΄ νΈλ€λ¬ μμ± (μ°Έμ‘°κ° λ³νμ§ μλλ‘) const handleDocumentLoaded = () => { + // νμ¬ λ¬Έμμ κ³ μ μλ³μ μμ± + const currentDoc = documentViewer.getDocument(); + const documentId = currentDoc ? `${currentDoc.getFilename()}_${Date.now()}` : null; + + // κ°μ λ¬Έμλ₯Ό μ΄λ―Έ μ²λ¦¬νλ€λ©΄ μ€ν΅ + if (documentId && processedDocumentRef.current === documentId) { + console.log("π μ΄λ―Έ μ²λ¦¬λ λ¬Έμμ΄λ―λ‘ μ€ν΅:", documentId); + return; + } + if (processingRef.current) { console.log("π μ΄λ―Έ μ²λ¦¬ μ€μ΄λ―λ‘ μ€ν΅"); return; } + // μ΄μ νμ΄λ¨Έ ν΄λ¦¬μ΄ if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } timeoutRef.current = setTimeout(async () => { + // λ€μ νλ² μ€λ³΅ μ²΄ν¬ if (processingRef.current) return; + if (documentId && processedDocumentRef.current === documentId) return; processingRef.current = true; setIsProcessing(true); @@ -249,32 +245,58 @@ function useAutoSignatureFields(instance: WebViewerInstance | null) { throw new Error("λ¬Έμκ° μ€λΉλμ§ μμμ΅λλ€."); } - const detector = new AutoSignatureFieldDetector(instance); + // κΈ°μ‘΄ μλͺ
νλ νμΈ + const { annotationManager } = instance.Core; + const existingAnnotations = annotationManager.getAnnotationsList(); + const existingSignatureFields = existingAnnotations.filter( + annot => annot instanceof instance.Core.Annotations.SignatureWidgetAnnotation + ); + + // μ΄λ―Έ μλͺ
νλκ° μμΌλ©΄ μμ±νμ§ μμ + if (existingSignatureFields.length > 0) { + console.log("π κΈ°μ‘΄ μλͺ
νλ λ°κ²¬:", existingSignatureFields.length); + const fieldNames = existingSignatureFields.map((field, idx) => + field.getField()?.name || `existing_signature_${idx}` + ); + setSignatureFields(fieldNames); + + // μ²λ¦¬ μλ£ νμ + if (documentId) { + processedDocumentRef.current = documentId; + } + + toast.success(`π ${fieldNames.length}κ°μ κΈ°μ‘΄ μλͺ
νλλ₯Ό νμΈνμ΅λλ€.`); + return; + } + + const detector = new AutoSignatureFieldDetector(instance, mode); // mode μ λ¬ const fields = await detector.detectAndCreateSignatureFields(); setSignatureFields(fields); + // μ²λ¦¬ μλ£ νμ + if (documentId) { + processedDocumentRef.current = documentId; + } + if (fields.length > 0) { const hasSimpleField = fields.some(field => field.startsWith('simple_signature_')); if (hasSimpleField) { + const positionMessage = mode === 'buyer' + ? "λ§μ§λ§ νμ΄μ§ μΌμͺ½ νλ¨μ νλμ μμμμ μλͺ
ν΄μ£ΌμΈμ." + : "λ§μ§λ§ νμ΄μ§ νλ¨μ νλμ μμμμ μλͺ
ν΄μ£ΌμΈμ."; + toast.success("π μλͺ
νλκ° μμ±λμμ΅λλ€.", { - description: "λ§μ§λ§ νμ΄μ§ νλ¨μ νλμ μμμμ μλͺ
ν΄μ£ΌμΈμ.", + description: positionMessage, icon: <FileSignature className="h-4 w-4 text-blue-500" />, duration: 5000 }); - } else { - toast.success(`π ${fields.length}κ°μ μλͺ
νλλ₯Ό νμΈνμ΅λλ€.`, { - description: "κΈ°μ‘΄ μλͺ
νλκ° λ°κ²¬λμμ΅λλ€.", - icon: <CheckCircle2 className="h-4 w-4 text-green-500" />, - duration: 4000 - }); } } } catch (error) { console.error("π μλͺ
νλ μ²λ¦¬ μ€ν¨:", error); - const errorMessage = error instanceof Error ? error.message : "μλͺ
νλ μ²λ¦¬μ μ€ν¨νμ΅λλ€."; setError(errorMessage); @@ -289,27 +311,53 @@ function useAutoSignatureFields(instance: WebViewerInstance | null) { }, 3000); }; - documentViewer.removeEventListener('documentLoaded', handleDocumentLoaded); + // νΈλ€λ¬ μ°Έμ‘° μ μ₯ + handlerRef.current = handleDocumentLoaded; + + // μ΄μ 리μ€λ μ κ±° (μ μ₯λ μ°Έμ‘° μ¬μ©) + if (handlerRef.current) { + documentViewer.removeEventListener('documentLoaded', handlerRef.current); + } + + // μ 리μ€λ λ±λ‘ documentViewer.addEventListener('documentLoaded', handleDocumentLoaded); + // μ΄λ―Έ λ¬Έμκ° λ‘λλμ΄ μλ€λ©΄ μ¦μ μ€ν + if (documentViewer.getDocument()) { + // μ§§μ μ§μ° ν μ€ν (WebViewer μ΄κΈ°ν μλ£ λ³΄μ₯) + setTimeout(() => { + if (!processingRef.current) { + handleDocumentLoaded(); + } + }, 1000); + } + return () => { - documentViewer.removeEventListener('documentLoaded', handleDocumentLoaded); + // ν΄λ¦¬μ΄ μ μ μ₯λ μ°Έμ‘° μ¬μ© + if (handlerRef.current) { + documentViewer.removeEventListener('documentLoaded', handlerRef.current); + } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } + // μν 리μ
processingRef.current = false; + processedDocumentRef.current = null; + handlerRef.current = null; }; - }, [instance]); + }, [instance, mode]); // mode μμ‘΄μ± μΆκ° + // μ»΄ν¬λνΈ μΈλ§μ΄νΈ μ μ 리 useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } processingRef.current = false; + processedDocumentRef.current = null; }; }, []); @@ -320,198 +368,121 @@ function useAutoSignatureFields(instance: WebViewerInstance | null) { error }; } - -// π₯ μλͺ
κ°μ§λ₯Ό μν 컀μ€ν
ν
μμ +// XFDF κΈ°λ° μλͺ
κ°μ§ 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); + const onCompleteRef = useRef(onSignatureComplete); - // μ½λ°± λ νΌλ°μ€ μ
λ°μ΄νΈ useEffect(() => { - onSignatureCompleteRef.current = onSignatureComplete; + onCompleteRef.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; - } + useEffect(() => { + if (!instance?.Core) return; - let hasSignature = false; + const { annotationManager, documentViewer } = instance.Core; + const checkSignedByAppearance = () => { + try { + const annotations = annotationManager.getAnnotationsList(); + const signatureWidgets = annotations.filter( + annot => annot instanceof instance.Core.Annotations.SignatureWidgetAnnotation + ); - // 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; - } + if (signatureWidgets.length === 0) { + return false; } - } - // 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; - } + for (const widget of signatureWidgets) { + const isSignedByAppearance = widget.isSignedByAppearance(); + + if (isSignedByAppearance) { + return true; } } + return false; + + } catch (error) { + console.error('μλͺ
μμ ― νμΈ μ€ μ€λ₯:', error); + return false; } + }; - // 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; - } + const checkSigned = async () => { + try { + const hasSignature = await checkSignedByAppearance(); + + if (hasSignature !== hasValidSignature) { + setHasValidSignature(hasSignature); + + if (hasSignature && onCompleteRef.current) { + onCompleteRef.current(); } } + + } catch (error) { + console.error('μλͺ
νμΈ μ€ μ€λ₯:', error); } + }; - console.log('π μ΅μ’
μλͺ
κ°μ§ κ²°κ³Ό:', { - hasSignature, - fieldsCount: fields.length, - annotationsCount: annotationManager.getAnnotationsList().length - }); - - return hasSignature; - } catch (error) { - console.error('π μλͺ
νμΈ μ€ μλ¬:', error); - return false; - } - }, [instance]); + const onAnnotationChanged = (annotations: any[], action: string) => { + console.log("μλͺ
λ³κ²½") + // if (action === 'delete') return; - // μ€μκ° μλͺ
κ°μ§ (무ν λ λλ§ λ°©μ§) - useEffect(() => { - if (!instance?.Core) return; + setTimeout(checkSigned, 800); + }; - 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 onDocumentLoaded = () => { + setTimeout(checkSigned, 2000); }; - // λ¬Έμ λ‘λ ν λͺ¨λν°λ§ μμ - const { documentViewer } = instance.Core; - - const handleDocumentLoaded = () => { - console.log('π λ¬Έμ λ‘λ μλ£, μλͺ
λͺ¨λν°λ§ μ€λΉ'); - // λ¬Έμ λ‘λ ν 3μ΄ λ€μ λͺ¨λν°λ§ μμ (μμ μ± ν보) - setTimeout(startMonitoring, 3000); + const onPageUpdated = () => { + setTimeout(checkSigned, 1000); }; - if (documentViewer?.getDocument()) { - // μ΄λ―Έ λ¬Έμκ° λ‘λλμ΄ μλ€λ©΄ λ°λ‘ μμ - setTimeout(startMonitoring, 1000); - } else { - // λ¬Έμ λ‘λ λκΈ° - documentViewer?.addEventListener('documentLoaded', handleDocumentLoaded); + const onAnnotationSelected = () => { + setTimeout(checkSigned, 500); + }; + + const onAnnotationUnselected = () => { + setTimeout(checkSigned, 1000); + }; + + try { + annotationManager.addEventListener('annotationChanged', onAnnotationChanged); + annotationManager.addEventListener('annotationSelected', onAnnotationSelected); + annotationManager.addEventListener('annotationUnselected', onAnnotationUnselected); + documentViewer.addEventListener('documentLoaded', onDocumentLoaded); + documentViewer.addEventListener('pageNumberUpdated', onPageUpdated); + + } catch (error) { + console.error('μ΄λ²€νΈ 리μ€λ λ±λ‘ μ€ν¨:', error); } - // ν΄λ¦¬λ ν¨μ + if (documentViewer.getDocument()) { + setTimeout(checkSigned, 1000); + } + + const pollInterval = setInterval(() => { + checkSigned(); + }, 5000); + return () => { - console.log('π§Ή μλͺ
λͺ¨λν°λ§ μ 리'); - if (checkIntervalRef.current) { - clearInterval(checkIntervalRef.current); - checkIntervalRef.current = null; + try { + annotationManager.removeEventListener('annotationChanged', onAnnotationChanged); + annotationManager.removeEventListener('annotationSelected', onAnnotationSelected); + annotationManager.removeEventListener('annotationUnselected', onAnnotationUnselected); + documentViewer.removeEventListener('documentLoaded', onDocumentLoaded); + documentViewer.removeEventListener('pageNumberUpdated', onPageUpdated); + clearInterval(pollInterval); + } catch (error) { + console.error('ν΄λ¦°μ
μ€ μ€λ₯:', error); } - documentViewer?.removeEventListener('documentLoaded', handleDocumentLoaded); }; - }, [instance]); // onSignatureComplete μ κ±°νμ¬ λ¬΄ν λ λλ§ λ°©μ§ + }, [instance, hasValidSignature]); - // μλ μλͺ
νμΈ ν¨μ - const manualCheckSignature = useCallback(async () => { - console.log('π μλ μλͺ
νμΈ μμ²'); - const hasSignature = await checkSignatureFields(); - setHasValidSignature(hasSignature); - lastSignatureStateRef.current = hasSignature; - return hasSignature; - }, [checkSignatureFields]); - - return { - hasValidSignature, - checkSignature: manualCheckSignature - }; + return { hasValidSignature }; } export function BasicContractSignViewer({ @@ -524,8 +495,10 @@ export function BasicContractSignViewer({ onSign, instance, setInstance, - onSurveyComplete, // π₯ μΆκ° - onSignatureComplete, // π₯ μΆκ° + onSurveyComplete, + onSignatureComplete, + onGtcCommentStatusChange, + mode = 'vendor', // κΈ°λ³Έκ° vendor t = (key: string) => key, }: BasicContractSignViewerProps) { @@ -535,7 +508,9 @@ export function BasicContractSignViewer({ 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 [gtcCommentStatus, setGtcCommentStatus] = useState<{ hasComments: boolean; commentCount: number }>({ hasComments: false, commentCount: 0 }); + + console.log(surveyTemplate, "surveyTemplate") const conditionalHandler = useConditionalSurvey(surveyTemplate); @@ -546,13 +521,15 @@ export function BasicContractSignViewer({ const [showDialog, setShowDialog] = useState(isOpen); const webViewerInstance = useRef<WebViewerInstance | null>(null); - const { signatureFields, isProcessing: isAutoSignProcessing, hasSignatureFields, error: autoSignError } = useAutoSignatureFields(webViewerInstance.current || instance); - - // π₯ μλͺ
κ°μ§ ν
μ¬μ© + // mode μ λ¬ + const { signatureFields, isProcessing: isAutoSignProcessing, hasSignatureFields, error: autoSignError } = useAutoSignatureFields(webViewerInstance.current || instance, mode); + const { hasValidSignature } = useSignatureDetection(webViewerInstance.current || instance, onSignatureComplete); - const isComplianceTemplate = templateName.includes('μ€λ²'); - const isNDATemplate = templateName.includes('λΉλ°μ μ§') || templateName.includes('NDA'); + // ꡬ맀μ λͺ¨λμΌ λλ ν
νλ¦Ώ κ΄λ ¨ λ‘μ§ λΉνμ±ν + const isComplianceTemplate = mode === 'buyer' ? false : templateName.includes('μ€λ²'); + const isNDATemplate = mode === 'buyer' ? false : (templateName.includes('λΉλ°μ μ§') || templateName.includes('NDA')); + const isGTCTemplate = mode === 'buyer' ? false : templateName.includes('GTC'); const allFiles: FileInfo[] = React.useMemo(() => { const files: FileInfo[] = []; @@ -565,6 +542,11 @@ export function BasicContractSignViewer({ }); } + // ꡬ맀μ λͺ¨λμΌ λλ μΆκ° νμΌ, μ€λ¬Έμ‘°μ¬, μ‘°ν κ²ν ν μ μΈ + if (mode === 'buyer') { + return files; // λ©μΈ κ³μ½μ νμΌλ§ λ°ν + } + const normalizedAttachments: FileInfo[] = (additionalFiles || []) .map((f: any, idx: number) => ({ path: f.path ?? f.filePath ?? "", @@ -583,8 +565,16 @@ export function BasicContractSignViewer({ }); } + if (isGTCTemplate) { + files.push({ + path: "", + name: "μ‘°ν κ²ν ", + type: "clauses", + }); + } + return files; - }, [filePath, additionalFiles, templateName, isComplianceTemplate]); + }, [filePath, additionalFiles, templateName, isComplianceTemplate, isGTCTemplate, mode]); const cleanupHtmlStyle = () => { const elements = document.querySelectorAll('.Document_container'); @@ -623,7 +613,8 @@ export function BasicContractSignViewer({ useEffect(() => { setShowDialog(isOpen); - if (isOpen && isComplianceTemplate && !surveyTemplate) { + // ꡬ맀μ λͺ¨λκ° μλ λλ§ μ€λ¬Έμ‘°μ¬ ν
νλ¦Ώ λ‘λ + if (isOpen && isComplianceTemplate && !surveyTemplate && mode !== 'buyer') { loadSurveyTemplate(); } @@ -631,7 +622,7 @@ export function BasicContractSignViewer({ setIsInitialLoaded(false); currentDocumentPath.current = ""; } - }, [isOpen, isComplianceTemplate]); + }, [isOpen, isComplianceTemplate, mode]); useEffect(() => { if (!filePath) return; @@ -709,7 +700,9 @@ export function BasicContractSignViewer({ setInstance(newInstance); setFileLoading(false); - const { documentViewer } = newInstance.Core; + const { documentViewer, annotationManager, Annotations } = newInstance.Core; + + const { WidgetFlags } = Annotations; const FitMode = newInstance.UI.FitMode; const handleDocumentLoaded = () => { @@ -730,6 +723,30 @@ export function BasicContractSignViewer({ documentViewer.addEventListener('documentLoaded', handleDocumentLoaded); + // ꡬ맀μ λͺ¨λκ° μλ λλ§ μλ μλͺ
μ μ© + if (mode !== 'buyer') { + annotationManager.addEventListener('annotationChanged', async (annotList, type) => { + for (const annot of annotList) { + const { fieldName, X, Y, Width, Height, PageNumber } = annot; + + if (type === "add" && annot.Subject === "Widget") { + const signatureImage = await getVendorSignatureFile() + + const stamp = new Annotations.StampAnnotation(); + stamp.PageNumber = PageNumber; + stamp.X = X; + stamp.Y = Y; + stamp.Width = Width; + stamp.Height = Height; + + await stamp.setImageData(signatureImage.data.dataUrl); + annot.sign(stamp); + annot.setFieldFlag(WidgetFlags.READ_ONLY, true); + } + } + }); + } + newInstance.UI.setMinZoomLevel('25%'); newInstance.UI.setMaxZoomLevel('400%'); @@ -789,7 +806,7 @@ export function BasicContractSignViewer({ isCancelled.current = true; cleanupWebViewer(); }; - }, [setInstance]); + }, [setInstance, mode]); const getExtFromPath = (p: string) => { const m = p.toLowerCase().match(/\.([a-z0-9]+)(?:\?.*)?$/); @@ -873,17 +890,21 @@ export function BasicContractSignViewer({ const handleTabChange = async (newTab: string) => { setActiveTab(newTab); - if (newTab === "survey") return; + if (newTab === "survey" || newTab === "clauses") return; const currentInstance = webViewerInstance.current || instance; if (!currentInstance || fileLoading) return; + if (newTab === 'survey' && !surveyTemplate && !surveyLoading && mode !== 'buyer') { + loadSurveyTemplate(); + } + 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]; + targetFile = allFiles.filter(f => f.type !== 'survey' && f.type !== 'clauses')[fileIndex]; } if (!targetFile?.path) { @@ -909,7 +930,7 @@ export function BasicContractSignViewer({ documentViewer.updateView(); window.dispatchEvent(new Event("resize")); } catch (e) { - console.warn("ν λ³κ²½ ν λ μ΄μμ μλ‘κ³ μΉ¨ μ€ν΅:", e); + console.warn("ν λ³κ²½ ν λ μ΄μμ μκ³ μΉ¨ μ€ν΅:", e); } }); } catch (e) { @@ -965,22 +986,34 @@ export function BasicContractSignViewer({ downloadType: "pdf", }); - if (isComplianceTemplate && !surveyData.completed) { - toast.error("μ€λ² μ€λ¬Έμ‘°μ¬λ₯Ό λ¨Όμ μλ£ν΄μ£ΌμΈμ."); - setActiveTab('survey'); - return; + // ꡬ맀μ λͺ¨λμΌ λλ μ€λ¬Έμ‘°μ¬μ GTC κ²μ¦ 건λλ°κΈ° + if (mode !== 'buyer') { + if (isComplianceTemplate && !surveyData.completed) { + toast.error("μ€λ² μ€λ¬Έμ‘°μ¬λ₯Ό λ¨Όμ μλ£ν΄μ£ΌμΈμ."); + setActiveTab('survey'); + return; + } + + if (isGTCTemplate && gtcCommentStatus.hasComments) { + toast.error("GTC μ‘°νμ μ½λ©νΈκ° μμ΄ μλͺ
ν μ μμ΅λλ€."); + toast.info("λͺ¨λ μ½λ©νΈλ₯Ό μμ νκ±°λ νμλ₯Ό μλ£ν ν μλͺ
ν΄μ£ΌμΈμ."); + setActiveTab('clauses'); + return; + } } if (onSign) { await onSign(documentData, { formData, surveyData, signatureFields }); } else { - toast.success("κ³μ½μκ° μ±κ³΅μ μΌλ‘ μλͺ
λμμ΅λλ€."); + const actionText = mode === 'buyer' ? "μ΅μ’
μΉμΈ" : "μλͺ
"; + toast.success(`κ³μ½μκ° μ±κ³΅μ μΌλ‘ ${actionText}λμμ΅λλ€.`); } handleClose(); } catch (error) { - console.error("π μλͺ
μ μ₯ μ€ν¨:", error); - toast.error("μλͺ
μ μ μ₯νλλ° μ€ν¨νμ΅λλ€."); + console.error(`π ${mode === 'buyer' ? 'μ΅μ’
μΉμΈ' : 'μλͺ
'} μ μ₯ μ€ν¨:`, error); + const actionText = mode === 'buyer' ? "μ΅μ’
μΉμΈ" : "μλͺ
"; + toast.error(`${actionText}μ μ μ₯νλλ° μ€ν¨νμ΅λλ€.`); } }; @@ -992,842 +1025,7 @@ export function BasicContractSignViewer({ } }; - // κ°μ λ SurveyComponent - const SurveyComponent = () => { - const { - control, - watch, - setValue, - getValues, - formState: { errors }, - trigger, - } = useForm<SurveyFormData>({ - defaultValues: {}, - mode: 'onChange' - }); - - 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: {} - }; - } - - 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 - }); - } - }); - - 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> - ); - } - - 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="κΈ°ν λ΄μ©μ μ
λ ₯ν΄μ£ΌμΈμ" - className="mt-2" - /> - )} - /> - ); - }; - - 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> - <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> - - {/* μΈλΆ μ§ν μν© */} - {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> - </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 className="space-y-4"> - {visibleQuestions.map((question: any) => { - const fieldName = `${question.id}`; - const isComplete = progressStatus.completedQuestionIds.includes(question.id); - const isConditional = !!question.parentQuestionId; - - 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> - - {/* μ§λ¬Έ νμ
λ³ λ λλ§ (κΈ°μ‘΄ μ½λμ λμΌ) */} - {/* 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> - )} - /> - )} - - {/* 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> - )} - - {/* TEXTAREA νμ
*/} - {question.questionType === 'TEXTAREA' && ( - <Controller - name={`${fieldName}.detailText`} - control={control} - rules={{ required: question.isRequired ? 'νμ νλͺ©μ
λλ€.' : false }} - render={({ field }) => ( - <Textarea - {...field} - placeholder="μμΈν λ΄μ©μ μ
λ ₯ν΄μ£ΌμΈμ" - rows={4} - /> - )} - /> - )} - - {/* μμΈ ν
μ€νΈ μ
λ ₯ */} - {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> - ); - })} - </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> - </form> - </CardContent> - </Card> - </div> - ); - }; - - // π₯ μλͺ
μν νμ μ»΄ν¬λνΈ κ°μ + // μλͺ
μν νμ μ»΄ν¬λνΈ const SignatureFieldsStatus = () => { if (!hasSignatureFields && !isAutoSignProcessing && !autoSignError && !hasValidSignature) return null; @@ -1846,11 +1044,10 @@ export function BasicContractSignViewer({ ) : 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}κ° μλͺ
νλ μλ μμ±λ¨ + {signatureFields.length}κ° μλͺ
νλ μλ μμ±λ¨ {mode === 'buyer' ? '(μΌμͺ½ νλ¨)' : ''} </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" /> @@ -1865,7 +1062,8 @@ export function BasicContractSignViewer({ if (!isOpen && !onClose) { return ( <div className="h-full w-full flex flex-col overflow-hidden"> - {allFiles.length > 1 ? ( + {/* ꡬ맀μ λͺ¨λμμλ ν μμ΄ λ¨μΌ λ·°μ΄λ§ νμ */} + {allFiles.length > 1 && mode !== 'buyer' ? ( <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 /> @@ -1876,8 +1074,10 @@ export function BasicContractSignViewer({ tabId = 'main'; } else if (file.type === 'survey') { tabId = 'survey'; + } else if (file.type === 'clauses') { + tabId = 'clauses'; } else { - const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey').length; + const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey' && f.type !== 'clauses').length; tabId = `file-${fileOnlyIndex}`; } @@ -1886,6 +1086,8 @@ export function BasicContractSignViewer({ <div className="flex items-center space-x-1"> {file.type === 'survey' ? ( <ClipboardList className="h-3 w-3" /> + ) : file.type === 'clauses' ? ( + <BookOpen className="h-3 w-3" /> ) : ( <FileText className="h-3 w-3" /> )} @@ -1893,6 +1095,11 @@ export function BasicContractSignViewer({ {file.type === 'survey' && surveyData.completed && ( <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">μλ£</Badge> )} + {file.type === 'clauses' && gtcCommentStatus.hasComments && ( + <Badge variant="destructive" className="ml-1 h-4 px-1 text-xs"> + μ½λ©νΈ {gtcCommentStatus.commentCount} + </Badge> + )} </div> </TabsTrigger> ); @@ -1904,11 +1111,35 @@ export function BasicContractSignViewer({ <div className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`} > - <SurveyComponent /> + {/* λΆλ¦¬λ SurveyComponent μ¬μ© */} + <SurveyComponent + contractId={contractId} + surveyTemplate={surveyTemplate} + surveyLoading={surveyLoading} + conditionalHandler={conditionalHandler} + onSurveyComplete={onSurveyComplete} + onSurveyDataUpdate={setSurveyData} + onLoadSurveyTemplate={loadSurveyTemplate} + setActiveTab={setActiveTab} + /> + </div> + + <div + className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`} + > + {/* GTC μ‘°ν μ»΄ν¬λνΈ */} + <GtcClausesComponent + contractId={contractId} + onCommentStatusChange={(hasComments, commentCount) => { + setGtcCommentStatus({ hasComments, commentCount }); + onGtcCommentStatusChange?.(hasComments, commentCount); + }} + t={t} + /> </div> <div - className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`} + className={`absolute inset-0 ${activeTab !== 'survey' && activeTab !== 'clauses' ? 'block' : 'hidden'}`} > <div className="w-full h-full overflow-auto"> <div @@ -1962,49 +1193,84 @@ export function BasicContractSignViewer({ ); } + const handleGtcCommentStatusChange = React.useCallback((hasComments: boolean, commentCount: number) => { + setGtcCommentStatus({ hasComments, commentCount }); + onGtcCommentStatusChange?.(hasComments, commentCount); + }, [onGtcCommentStatusChange]); + // λ€μ΄μΌλ‘κ·Έ λ·°μ΄ λ λλ§ 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> + <span>{mode === 'buyer' ? 'ꡬ맀μ μ΅μ’
μΉμΈ' : 'κΈ°λ³Έκ³μ½μ μλͺ
'}</span> <SignatureFieldsStatus /> </DialogTitle> <DialogDescription> - κ³μ½μλ₯Ό νμΈνκ³ μλͺ
μ μ§νν΄μ£ΌμΈμ. - {isComplianceTemplate && ( + κ³μ½μλ₯Ό νμΈνκ³ {mode === 'buyer' ? 'μ΅μ’
μΉμΈμ' : 'μλͺ
μ'} μ§νν΄μ£ΌμΈμ. + {mode !== 'buyer' && isComplianceTemplate && ( <span className="block mt-1 text-amber-600">π μ€λ² μ€λ¬Έμ‘°μ¬λ₯Ό λ¨Όμ μλ£ν΄μ£ΌμΈμ.</span> )} + {mode !== 'buyer' && isGTCTemplate && ( + <span className="block mt-1 text-blue-600">π GTC μ‘°νμ κ²ν νκ³ μ½λ©νΈκ° μλμ§ νμΈν΄μ£ΌμΈμ.</span> + )} {hasSignatureFields && ( <span className="block mt-1 text-green-600"> - π― μλͺ
μμΉκ° μλμΌλ‘ κ°μ§λμμ΅λλ€. + π― μλͺ
μμΉκ° μλμΌλ‘ κ°μ§λμμ΅λλ€{mode === 'buyer' ? ' (μΌμͺ½ νλ¨)' : ''}. </span> )} - {/* π₯ μλͺ
μλ£ μν μλ΄ */} {hasValidSignature && ( <span className="block mt-1 text-green-600"> - β
μλͺ
μ΄ μλ£λμμ΅λλ€. + β
{mode === 'buyer' ? 'μΉμΈμ΄' : 'μλͺ
μ΄'} μλ£λμμ΅λλ€. + </span> + )} + {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && ( + <span className="block mt-1 text-red-600"> + β οΈ GTC μ‘°νμ {gtcCommentStatus.commentCount}κ°μ μ½λ©νΈκ° μμ΄ μλͺ
ν μ μμ΅λλ€. </span> )} </DialogDescription> </DialogHeader> <div className="flex-1 min-h-0 overflow-hidden"> - {allFiles.length > 1 ? ( + {/* ꡬ맀μ λͺ¨λμμλ ν μμ΄ λ¨μΌ λ·°μ΄λ§ νμ */} + {allFiles.length > 1 && mode !== 'buyer' ? ( <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}`; + let tabId: string; + if (index === 0) { + tabId = 'main'; + } else if (file.type === 'survey') { + tabId = 'survey'; + } else if (file.type === 'clauses') { + tabId = 'clauses'; + } else { + const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey' && f.type !== 'clauses').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" /> + ) : file.type === 'clauses' ? ( + <BookOpen 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> )} + {file.type === 'clauses' && gtcCommentStatus.hasComments && ( + <Badge variant="destructive" className="ml-1 h-4 px-1 text-xs"> + μ½λ©νΈ {gtcCommentStatus.commentCount} + </Badge> + )} </div> </TabsTrigger> ); @@ -2014,10 +1280,29 @@ export function BasicContractSignViewer({ <div className="flex-1 min-h-0 overflow-hidden relative"> <div className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}> - <SurveyComponent /> + {/* λΆλ¦¬λ SurveyComponent μ¬μ© */} + <SurveyComponent + contractId={contractId} + surveyTemplate={surveyTemplate} + surveyLoading={surveyLoading} + conditionalHandler={conditionalHandler} + onSurveyComplete={onSurveyComplete} + onSurveyDataUpdate={setSurveyData} + onLoadSurveyTemplate={loadSurveyTemplate} + setActiveTab={setActiveTab} + /> + </div> + + <div className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`}> + {/* GTC μ‘°ν μ»΄ν¬λνΈ */} + <GtcClausesComponent + contractId={contractId} + onCommentStatusChange={handleGtcCommentStatusChange} // λ©λͺ¨μ΄μ μ΄μ
λ μ½λ°± μ¬μ© + t={t} + /> </div> - <div className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}> + <div className={`absolute inset-0 ${activeTab !== 'survey' && activeTab !== 'clauses' ? 'block' : 'hidden'}`}> <div className="w-full h-full overflow-auto"> <div ref={viewer} @@ -2069,7 +1354,7 @@ export function BasicContractSignViewer({ <Button variant="outline" onClick={handleClose} disabled={fileLoading}>μ·¨μ</Button> <Button onClick={handleSave} disabled={fileLoading || isAutoSignProcessing}> <FileSignature className="h-4 w-4 mr-2" /> - μλͺ
μλ£ + {mode === 'buyer' ? 'μ΅μ’
μΉμΈ μλ£' : 'μλͺ
μλ£'} </Button> </DialogFooter> </DialogContent> |
