"use client" import * as React from "react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Building2, CheckCircle, Clock, Save, Send, ArrowLeft, AlertCircle, FileText, Upload, File, X, Download, Paperclip } from "lucide-react" import { useRouter } from "next/navigation" import { useToast } from "@/hooks/use-toast" import { updateEvaluationResponse, updateVariableEvaluationResponse, completeEvaluation } from "./service" import { type EvaluationQuestionItem, EVALUATION_CATEGORIES } from "./validation" import { DEPARTMENT_CODE_LABELS, divisionMap, vendortypeMap } from "@/types/evaluation" import { EvaluationFormData } from "@/types/evaluation-form" // 파일 다운로드 유틸리티 import // import { downloadFile, formatFileSize, getFileInfo } from "@/lib/file-download" interface EvaluationFormProps { formData: EvaluationFormData onSubmit?: () => void } interface QuestionResponse { detailId: number | null score: number | null comment: string } interface AttachmentInfo { id: number originalFileName: string publicPath: string fileSize: number description?: string createdAt: Date } /** * 평가 폼 메인 컴포넌트 (테이블 레이아웃) */ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { const router = useRouter() const { toast } = useToast() const [isLoading, setIsLoading] = React.useState(false) const [isSaving, setIsSaving] = React.useState(false) const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false) const [showCompleteDialog, setShowCompleteDialog] = React.useState(false) const [uploadingFiles, setUploadingFiles] = React.useState>(new Set()) const { evaluationInfo, questions } = formData // 로컬 상태로 모든 응답 관리 const [responses, setResponses] = React.useState>(() => { const initial: Record = {} questions.forEach(question => { const isVariable = question.scoreType === 'variable' // 선택된 답변 옵션 찾기 const selectedOption = question.selectedDetailId ? question.availableOptions.find(opt => opt.detailId === question.selectedDetailId) : null; initial[question.criteriaId] = { detailId: isVariable ? -1 : question.selectedDetailId, score: isVariable ? (question.currentScore ? Number(question.currentScore) : null) : (selectedOption?.score ?? (question.currentScore ? Number(question.currentScore) : null)), comment: question.currentComment || "", } }) return initial }) // 첨부파일 상태 관리 (서버에서 받은 데이터로 초기화) const [attachments, setAttachments] = React.useState>(() => { console.log('Initializing attachments from server data...') const initial: Record = {} questions.forEach(question => { const questionAttachments = Array.isArray(question.attachments) ? question.attachments : [] initial[question.criteriaId] = questionAttachments if (questionAttachments.length > 0) { console.log(`Question ${question.criteriaId} has ${questionAttachments.length} attachments:`, questionAttachments) } }) console.log('Initial attachments state:', initial) return initial }) // 첨부파일 다운로드 핸들러 - downloadFile 사용 const handleDownloadAttachment = async (attachment: AttachmentInfo) => { try { const { downloadFile } = await import('@/lib/file-download') const result = await downloadFile( attachment.publicPath, attachment.originalFileName, { action: 'download', showToast: true, showSuccessToast: true, onError: (error) => { console.error("파일 다운로드 실패:", error) }, onSuccess: (fileName, fileSize) => { // console.log("파일 다운로드 성공:", fileName, fileSize ? formatFileSize(fileSize) : '') } } ) console.log("파일 다운로드 결과:", result) } catch (error) { console.error("다운로드 처리 중 오류:", error) } } // 카테고리별 질문 그룹화 const questionsByCategory = React.useMemo(() => { const grouped = questions.reduce((acc, question) => { const key = question.category if (!acc[key]) { acc[key] = [] } acc[key].push(question) return acc }, {} as Record) return grouped }, [questions]) const categoryNames = EVALUATION_CATEGORIES // 응답 변경 핸들러 const handleResponseChange = (questionId: number, detailId: number, customScore?: number) => { const question = questions.find(q => q.criteriaId === questionId) if (!question) return const selectedOption = question.availableOptions.find(opt => opt.detailId === detailId) setResponses(prev => ({ ...prev, [questionId]: { ...prev[questionId], detailId, score: customScore !== undefined ? customScore : selectedOption?.score || null, } })) setHasUnsavedChanges(true) } // 점수 직접 입력 핸들러 (variable 타입용) const handleScoreChange = (questionId: number, score: number | null) => { console.log('Score changed:', questionId, score) setResponses(prev => ({ ...prev, [questionId]: { ...prev[questionId], score, detailId: prev[questionId].detailId || -1 } })) setHasUnsavedChanges(true) } // 코멘트 변경 핸들러 const handleCommentChange = (questionId: number, comment: string) => { setResponses(prev => ({ ...prev, [questionId]: { ...prev[questionId], comment } })) setHasUnsavedChanges(true) } // 파일 업로드 핸들러 const handleFileUpload = async (questionId: number, file: File, description?: string) => { try { console.log('Starting file upload for question:', questionId, 'file:', file.name) setUploadingFiles(prev => new Set(prev).add(questionId)) // 질문 정보 확인 const question = questions.find(q => q.criteriaId === questionId) if (!question) { throw new Error('질문을 찾을 수 없습니다.') } // 답변이 있는지 확인 const response = responses[questionId] const isVariable = question.scoreType === 'variable' const isAnswered = isVariable ? (response.score !== null) : (response.detailId !== null && response.detailId > 0) if (!isAnswered) { throw new Error('먼저 답변을 선택해주세요.') } // FormData 생성 const formData = new FormData() formData.append('file', file) formData.append('questionId', questionId.toString()) formData.append('evaluationId', evaluationInfo.id.toString()) if (description) { formData.append('description', description) } if (isVariable) { formData.append('isVariable', 'true') } console.log('Sending upload request...') // 파일 업로드 API 호출 const response_api = await fetch('/api/evaluation/attachments', { method: 'POST', body: formData, }) console.log('Upload response status:', response_api.status) if (!response_api.ok) { const errorText = await response_api.text() console.error('Upload failed with status:', response_api.status, 'error:', errorText) throw new Error(`파일 업로드에 실패했습니다. (${response_api.status})`) } const result = await response_api.json() console.log('Upload result:', result) if (result.success && result.attachment) { // 첨부파일 목록 업데이트 setAttachments(prev => ({ ...prev, [questionId]: [...(prev[questionId] || []), { id: result.attachment.id, originalFileName: result.attachment.originalFileName, publicPath: result.attachment.publicPath, fileSize: result.attachment.fileSize, description: result.attachment.description, createdAt: new Date(result.attachment.createdAt), }] })) toast({ title: "파일 업로드 완료", description: `${file.name}이 성공적으로 업로드되었습니다.`, }) } else { throw new Error(result.error || '파일 업로드에 실패했습니다.') } } catch (error) { console.error('File upload failed:', error) toast({ title: "업로드 실패", description: error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다.", variant: "destructive", }) } finally { setUploadingFiles(prev => { const newSet = new Set(prev) newSet.delete(questionId) return newSet }) } } // 첨부파일 삭제 핸들러 const handleDeleteAttachment = async (questionId: number, attachmentId: number) => { try { const response = await fetch(`/api/evaluation/attachments/${attachmentId}`, { method: 'DELETE', }) if (!response.ok) { throw new Error('파일 삭제에 실패했습니다.') } // 첨부파일 목록에서 제거 setAttachments(prev => ({ ...prev, [questionId]: prev[questionId]?.filter(att => att.id !== attachmentId) || [] })) toast({ title: "파일 삭제 완료", description: "첨부파일이 삭제되었습니다.", }) } catch (error) { console.error('File deletion failed:', error) toast({ title: "삭제 실패", description: "파일 삭제 중 오류가 발생했습니다.", variant: "destructive", }) } } // 임시저장 const handleSave = async () => { try { setIsSaving(true) const promises = Object.entries(responses) .filter(([questionId, response]) => { const question = questions.find(q => q.criteriaId === parseInt(questionId)) const isVariable = question?.scoreType === 'variable' if (isVariable) { return response.score !== null } else { return response.detailId !== null && response.detailId > 0 } }) .map(([questionId, response]) => { const question = questions.find(q => q.criteriaId === parseInt(questionId)) const isVariable = question?.scoreType === 'variable' if (isVariable) { // Variable 타입은 별도 함수 사용 return updateVariableEvaluationResponse( evaluationInfo.id, parseInt(questionId), // criteriaId response.score!, response.comment || undefined ) } else { // 일반 타입 return updateEvaluationResponse( evaluationInfo.id, response.detailId!, response.comment || undefined, response.score || undefined ) } }) await Promise.all(promises) setHasUnsavedChanges(false) toast({ title: "임시저장 완료", description: "응답이 성공적으로 저장되었습니다.", }) } catch (error) { console.error('Failed to save responses:', error) toast({ title: "저장 실패", description: "응답 저장 중 오류가 발생했습니다.", variant: "destructive", }) } finally { setIsSaving(false) } } // 평가 완료 처리 (실제 완료 로직) const handleCompleteConfirmed = async () => { try { setIsLoading(true) setShowCompleteDialog(false) // 먼저 모든 응답 저장 await handleSave() // 평가 완료 처리 await completeEvaluation(evaluationInfo.id) toast({ title: "평가 완료", description: "평가가 성공적으로 완료되었습니다.", }) onSubmit?.() router.push('/evcp/evaluation-input') } catch (error) { console.error('Failed to complete evaluation:', error) toast({ title: "완료 실패", description: "평가 완료 처리 중 오류가 발생했습니다.", variant: "destructive", }) } finally { setIsLoading(false) } } // 평가 완료 버튼 클릭 (다이얼로그 표시) const handleCompleteClick = () => { setShowCompleteDialog(true) } const completedCount = Object.values(responses).filter(r => { const question = questions.find(q => q.criteriaId === parseInt(Object.keys(responses).find(key => responses[parseInt(key)] === r) || '0')) const isVariable = question?.scoreType === 'variable' if (isVariable) { return r.score !== null } else { return r.detailId !== null && r.detailId > 0 } }).length const totalCount = questions.length const allCompleted = completedCount === totalCount return (
{/* 헤더 */}

평가 작성

협력업체 평가를 진행해주세요

{evaluationInfo.isCompleted ? ( 완료 ) : ( 진행중 )}
{/* 평가 정보 카드 */} 평가 정보
{evaluationInfo.vendorName} ({evaluationInfo.vendorCode})
{divisionMap[evaluationInfo.division] || evaluationInfo.division}
{vendortypeMap[evaluationInfo.materialType] || evaluationInfo.materialType}
{DEPARTMENT_CODE_LABELS[evaluationInfo.departmentCode] || evaluationInfo.departmentCode}
{/* 📎 첨부파일 통계 정보 */} {formData.attachmentStats && formData.attachmentStats.totalFiles > 0 && (
첨부파일 현황
{formData.attachmentStats.totalFiles}개 파일
{/*
크기: {formatFileSize(formData.attachmentStats.totalSize)}
*/}
첨부 질문: {formData.attachmentStats.questionsWithAttachments}
)}
{/* 평가 테이블 - 카테고리별 */} {Object.entries(questionsByCategory).map(([category, categoryQuestions]) => { const categoryCompletedCount = categoryQuestions.filter(q => { const response = responses[q.criteriaId] const isVariable = q.scoreType === 'variable' if (isVariable) { return response.score !== null } else { return response.detailId !== null } }).length const categoryTotalCount = categoryQuestions.length const categoryProgress = (categoryCompletedCount / categoryTotalCount) * 100 return (
{categoryNames[category] || category} {categoryQuestions.length}개 질문
{categoryCompletedCount} / {categoryTotalCount} 완료
{Math.round(categoryProgress)}%
평가 범위 비고 답변 선택 점수 추가 의견 첨부파일 상태 {categoryQuestions.map((question) => { const response = responses[question.criteriaId] const questionAttachments = attachments[question.criteriaId] || [] const isVariable = question.scoreType === 'variable' const isAnswered = isVariable ? (response.score !== null) : (response.detailId !== null && response.detailId > 0) return ( {question.classification} {question.range} {question.remarks} {!isVariable && ( )} {isVariable && ( { const value = e.target.value if (value === '') { handleScoreChange(question.criteriaId, null) return } const numericValue = parseInt(value) // 0 이상의 정수만 허용 if (!isNaN(numericValue) && numericValue >= 0) { handleScoreChange(question.criteriaId, numericValue) } }} onBlur={(e) => { // 포커스를 잃을 때 추가 검증 const value = e.target.value if (value !== '' && (isNaN(parseInt(value)) || parseInt(value) < 0)) { handleScoreChange(question.criteriaId, null) } }} placeholder="점수 입력 (0 이상)" className="w-48" disabled={isLoading || isSaving} /> )} {isAnswered && ( = 4 ? "default" : response.score! >= 3 ? "secondary" : "destructive"}> {response.score}점 )}