import React, { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { useForm, useWatch, Controller } from "react-hook-form"; import { Loader2, FileText, ClipboardList, AlertTriangle, CheckCircle2, Upload, ChevronDown, ChevronUp, Download } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 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 { CompleteSurveyRequest, ExistingResponse, SurveyAnswerData, completeSurvey, getExistingSurveyResponse, type SurveyTemplateWithQuestions } from '../service'; import { ConditionalSurveyHandler } from '../vendor-table/survey-conditional'; // 폼 데이터 타입 정의 interface SurveyFormData { [key: string]: { answerValue?: string; detailText?: string; otherText?: string; files?: File[]; }; } interface SurveyComponentProps { contractId?: number; surveyTemplate: SurveyTemplateWithQuestions | null; surveyLoading: boolean; surveyLoadAttempted?: boolean; // 로드 시도 여부 추가 conditionalHandler: ConditionalSurveyHandler | null; onSurveyComplete?: () => void; onSurveyDataUpdate: (data: any) => void; onLoadSurveyTemplate: () => void; setActiveTab: (tab: string) => void; contractFilePath?: string; // 계약서 파일 경로 추가 contractFileName?: string; // 계약서 파일 이름 추가 } export const SurveyComponent: React.FC = ({ contractId, surveyTemplate, surveyLoading, surveyLoadAttempted = false, conditionalHandler, onSurveyComplete, onSurveyDataUpdate, onLoadSurveyTemplate, setActiveTab, contractFilePath, contractFileName }) => { const [uploadedFiles, setUploadedFiles] = useState>({}); const [existingResponse, setExistingResponse] = useState(null); const [loadingExistingResponse, setLoadingExistingResponse] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [formInitialized, setFormInitialized] = useState(false); const [isHeaderExpanded, setIsHeaderExpanded] = useState(false); console.log(uploadedFiles,"uploadedFiles") // 무한 렌더링 방지를 위한 ref const loadingRef = useRef(false); const initializedRef = useRef(false); // 기본 폼 설정 - 의존성 최소화 const { control, watch, setValue, getValues, formState: { errors }, trigger, reset } = useForm({ defaultValues: {}, mode: 'onChange' }); const watchedValues = watch(); // 기존 응답 로드 - 한 번만 실행되도록 최적화 useEffect(() => { // 중복 실행 방지 if (loadingRef.current || !contractId || !surveyTemplate?.id || initializedRef.current) { return; } loadingRef.current = true; setLoadingExistingResponse(true); const loadExistingResponse = async () => { try { console.log('📥 기존 설문 응답 조회 시작...', { contractId, templateId: surveyTemplate.id }); const result = await getExistingSurveyResponse(contractId, surveyTemplate.id); if (result.success && result.data) { console.log('✅ 기존 응답 발견:', result.data); setExistingResponse(result.data); // 폼 초기값 설정 const formValues: SurveyFormData = {}; const existingFiles: Record = {}; result.data.answers.forEach(answer => { formValues[answer.questionId] = { answerValue: answer.answerValue || '', detailText: answer.detailText || '', otherText: answer.otherText || '', files: answer.files || [], // 기존 파일 정보 유지 }; // 파일이 있다면 uploadedFiles에도 설정 (표시용) if (answer.files && answer.files.length > 0) { existingFiles[answer.questionId] = answer.files; } }); console.log('📝 폼 초기값 설정:', formValues); // reset을 사용하여 폼 전체를 한 번에 초기화 reset(formValues); setUploadedFiles(existingFiles) // 기존 응답이 완료되었다면 부모에게 알림 if (result.data.status === 'COMPLETED') { const completedData = { completed: true, answers: result.data.answers, timestamp: result.data.completedAt || new Date().toISOString(), responseId: result.data.responseId, }; onSurveyDataUpdate(completedData); // ⭐ 여기가 핵심: 기존 완료된 설문이 있으면 즉시 부모의 설문 완료 상태 업데이트 if (onSurveyComplete) { console.log(`📋 기존 완료된 설문조사 감지 - 부모 상태 업데이트: 계약서 ${contractId}`); onSurveyComplete(); } } } else { console.log('📭 기존 응답 없음'); setExistingResponse(null); } initializedRef.current = true; setFormInitialized(true); } catch (error) { console.error('❌ 기존 응답 로드 중 오류:', error); setExistingResponse(null); initializedRef.current = true; setFormInitialized(true); } finally { setLoadingExistingResponse(false); loadingRef.current = false; } }; loadExistingResponse(); }, [contractId, surveyTemplate?.id,onSurveyComplete]); // 의존성 최소화 // 실시간 진행 상태 계산 - 안정화 const progressStatus = useMemo(() => { if (!formInitialized || !conditionalHandler || !surveyTemplate) { return { visibleQuestions: [], totalRequired: 0, completedRequired: 0, completedQuestionIds: [], incompleteQuestionIds: [], progressPercentage: 0, debugInfo: {} }; } // 현재 답변을 조건부 핸들러가 인식할 수 있는 형태로 변환 const convertedAnswers: Record = {}; Object.entries(watchedValues || {}).forEach(([questionId, value]) => { const id = parseInt(questionId); if (!isNaN(id) && value) { convertedAnswers[id] = { questionId: id, answerValue: value.answerValue || '', detailText: value.detailText || '', otherText: value.otherText || '', files: value.files || [] }; } }); console.log(convertedAnswers,"convertedAnswers") return conditionalHandler.getSimpleProgressStatus(convertedAnswers); }, [conditionalHandler, watchedValues, surveyTemplate, formInitialized]); console.log(progressStatus,"progressStatus") // 동적 상태 정보 - 메모화 const { visibleQuestions, totalVisibleQuestions, baseQuestionCount, conditionalQuestionCount, hasConditionalQuestions, canComplete } = useMemo(() => { 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; return { visibleQuestions, totalVisibleQuestions, baseQuestionCount, conditionalQuestionCount, hasConditionalQuestions, canComplete }; }, [progressStatus, surveyTemplate?.questions]); // 파일 업로드 핸들러 - 안정적인 참조 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); setValue(`${questionId}.${field}`, value); // 부모 질문의 답변이 변경되면 조건부 자식 질문들 처리 if (field === 'answerValue' && conditionalHandler) { // setTimeout으로 다음 tick에서 처리하여 상태 업데이트 충돌 방지 setTimeout(() => { const currentValues = getValues(); const convertedAnswers: Record = {}; Object.entries(currentValues).forEach(([qId, qValue]) => { const id = parseInt(qId); if (!isNaN(id) && qValue) { convertedAnswers[id] = { questionId: id, answerValue: qValue.answerValue || '', detailText: qValue.detailText || '', otherText: qValue.otherText || '', files: qValue.files || [] }; } }); // 새로운 답변 반영 convertedAnswers[questionId] = { ...convertedAnswers[questionId], questionId, [field]: value }; // 영향받는 자식 질문들의 답변 초기화 const clearedAnswers = conditionalHandler.clearAffectedChildAnswers(questionId, value, convertedAnswers); // 삭제된 답변들을 폼에서도 제거 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; }); } }); }, 0); } }, [setValue, getValues, conditionalHandler]); // OTHER 텍스트 입력 컴포넌트 const OtherTextInput = useCallback(({ questionId, fieldName }: { questionId: number; fieldName: string }) => { const answerValue = useWatch({ control, name: `${fieldName}.answerValue` }); const question = visibleQuestions.find(q => q.id === questionId); const selectedOption = question?.options?.find(opt => opt.optionValue === answerValue); if (!selectedOption?.allowsOtherInput) return null; return ( ( )} /> ); }, [control, visibleQuestions]); // 계약서 다운로드 핸들러 const handleDownloadContract = useCallback(async () => { if (!contractFilePath) { toast.error("다운로드할 파일이 없습니다."); return; } try { // 파일 경로를 API 경로로 변환 const normalizedPath = contractFilePath.startsWith('/') ? contractFilePath.substring(1) : contractFilePath; const encodedPath = normalizedPath.split('/').map(part => encodeURIComponent(part)).join('/'); const apiFilePath = `/api/files/${encodedPath}`; // 파일 경로에서 실제 파일명 추출 const actualFileName = contractFileName || contractFilePath.split('/').pop() || '계약서.pdf'; // 다운로드 링크 생성 및 클릭 const link = document.createElement('a'); link.href = apiFilePath; link.download = actualFileName; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success("파일 다운로드가 시작되었습니다."); } catch (error) { console.error("파일 다운로드 실패:", error); toast.error("파일 다운로드에 실패했습니다."); } }, [contractFilePath, contractFileName]); // 설문조사 완료 핸들러 const handleSurveyComplete = useCallback(async () => { console.log('🎯 설문조사 완료 시도'); if (isSubmitting) { console.log('⚠️ 이미 제출 중...'); return; } setIsSubmitting(true); try { const currentValues = getValues(); const isValid = await trigger(); 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 }; const submitToast = toast.loading('설문조사를 저장하는 중...', { description: '잠시만 기다려주세요.', duration: Infinity }); const result = await completeSurvey(requestData); toast.dismiss(submitToast); if (result.success) { const completedSurveyData = { completed: true, answers: surveyAnswers, timestamp: new Date().toISOString(), progressStatus: progressStatus, totalQuestions: totalVisibleQuestions, conditionalQuestions: conditionalQuestionCount, responseId: result.data?.responseId }; onSurveyDataUpdate(completedSurveyData); if (onSurveyComplete) { onSurveyComplete(); } toast.success("🎉 설문조사가 완료되었습니다!", { description: `총 ${progressStatus.totalRequired}개 필수 질문 완료${hasConditionalQuestions ? ` (조건부 ${conditionalQuestionCount}개 포함)` : ''}`, icon: , duration: 5000 }); 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, canComplete, progressStatus, visibleQuestions, contractId, surveyTemplate?.id, totalVisibleQuestions, conditionalQuestionCount, hasConditionalQuestions, isSubmitting, onSurveyComplete, onSurveyDataUpdate, setActiveTab ]); if (surveyLoading || loadingExistingResponse) { return (

{surveyLoading ? '설문조사를 불러오는 중...' : '기존 응답을 확인하는 중...'}

); } if (!surveyTemplate) { if (!surveyLoadAttempted) { return (

설문조사를 준비하는 중...

); } return (

설문조사 템플릿을 불러올 수 없습니다.

); } return (
{/* 항상 보이는 컴팩트 헤더 */}
{surveyTemplate.name} {existingResponse && ( {existingResponse.status === 'COMPLETED' ? '완료됨' : '작성중'} )}
{/* Word 파일 다운로드 버튼 */} {contractFilePath && ( )} {/* 컴팩트 진행률 표시 */}
{progressStatus.completedRequired}/{progressStatus.totalRequired}
{/* 펼치기/접기 버튼 */}
{/* 접을 수 있는 상세 정보 - 조건부 렌더링 */} {isHeaderExpanded && (
{/* 기존 응답 정보 */} {existingResponse && (
{existingResponse.status === 'COMPLETED' ? '이미 완료된 설문조사입니다. 내용을 수정할 수 있습니다.' : '이전에 작성하던 내용이 복원되었습니다.'}
{existingResponse.completedAt && (
완료일시: {new Date(existingResponse.completedAt).toLocaleString('ko-KR')}
)}
)} {/* 질문 정보 - 그리드로 컴팩트하게 */} {/*
📋 총 {totalVisibleQuestions}개 질문
{hasConditionalQuestions && (
⚡ 조건부 {conditionalQuestionCount}개 추가됨
)}
✅ 완료: {progressStatus.completedRequired}개
⏳ 남은 필수: {progressStatus.totalRequired - progressStatus.completedRequired}개
*/} {/* 상세 진행률 바 */} {/*
필수 질문 진행률 {Math.round(progressStatus.progressPercentage)}% {hasConditionalQuestions && ( (조건부 포함) )}
*/} {/* 중요 안내 - 컴팩트하게 */}

준법 의무 확인 필수

모든 필수 항목을 정확히 작성해주세요. 답변에 따라 추가 질문이 나타날 수 있습니다.

)} {/* 헤더가 접혀있을 때 보이는 요약 정보 */} {!isHeaderExpanded && (
📋 {totalVisibleQuestions}개 질문 {hasConditionalQuestions && ( (+{conditionalQuestionCount}개 조건부) )} setIsHeaderExpanded(true)} > 상세 정보 보기 ↑
)}
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 && ( )}
{/* 질문 타입별 렌더링 */} {question.questionType === 'RADIO' && ( ( { field.onChange(value); handleAnswerChange(question.id, 'answerValue', value); }} className="space-y-2" > {question.options?.map((option: any) => (
))}
)} /> )} {question.questionType === 'DROPDOWN' && (
( )} />
)} {question.questionType === 'TEXTAREA' && ( (