"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 } from "lucide-react" import { useRouter } from "next/navigation" import { useToast } from "@/hooks/use-toast" import { updateEvaluationResponse, completeEvaluation } from "./service" import { type EvaluationFormData, type EvaluationQuestionItem, EVALUATION_CATEGORIES } from "./validation" import { DEPARTMENT_CODE_LABELS, divisionMap, vendortypeMap } from "@/types/evaluation" interface EvaluationFormProps { formData: EvaluationFormData onSubmit?: () => void } interface QuestionResponse { detailId: number | null score: number | null comment: string } /** * 평가 폼 메인 컴포넌트 (테이블 레이아웃) */ 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 { evaluationInfo, questions } = formData // 로컬 상태로 모든 응답 관리 const [responses, setResponses] = React.useState>(() => { const initial: Record = {} questions.forEach(question => { const isVariable = question.scoreType === 'variable' initial[question.criteriaId] = { detailId: isVariable ? -1 : question.selectedDetailId, score: isVariable ? question.currentScore || null : (question.selectedDetailId ? question.availableOptions.find(opt => opt.detailId === question.selectedDetailId)?.score || question.currentScore || null : question.currentScore || null), comment: question.currentComment || "", } }) return initial }) // 카테고리별 질문 그룹화 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 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' return updateEvaluationResponse( evaluationInfo.id, isVariable ? -1 : response.detailId!, response.comment, 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}
{/* 평가 테이블 - 카테고리별 */} {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 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}점 )}