diff options
Diffstat (limited to 'lib/evaluation')
| -rw-r--r-- | lib/evaluation/table/evaluation-columns.tsx | 54 | ||||
| -rw-r--r-- | lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx | 109 |
2 files changed, 136 insertions, 27 deletions
diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx index b68aa70d..56029e08 100644 --- a/lib/evaluation/table/evaluation-columns.tsx +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -133,7 +133,7 @@ const getDepartmentStatusBadge = (status: string | null) => { }; // 등급별 색상 -const getGradeBadgeVariant = (grade: string | null) => { +const getGradeBadgeVariant = (grade: string | null): "default" | "secondary" | "outline" | "destructive" => { if (!grade) return "outline"; switch (grade) { case "S": return "default"; @@ -264,7 +264,7 @@ export function getPeriodicEvaluationsColumns({ accessorKey: "vendorName", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />, cell: ({ row }) => ( - <div className="truncate max-w-[200px]" title={row.original.vendorName}> + <div className="truncate max-w-[200px]" title={row.original.vendorName || undefined}> {row.original.vendorName} </div> ), @@ -381,7 +381,7 @@ export function getPeriodicEvaluationsColumns({ return finalGrade ? ( <Badge - variant={getGradeBadgeVariant(finalGrade)} + variant={getGradeBadgeVariant(finalGrade) || "outline"} className={isAggregated ? "bg-purple-600" : "bg-green-600"} > {finalGrade} @@ -803,18 +803,56 @@ export function getPeriodicEvaluationsColumns({ }, { - accessorKey: "evaluationGrade", + id: "evaluationGrade", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가등급" />, cell: ({ row }) => { - const grade = row.getValue<string>("evaluationGrade"); + // 확정된 등급이 있으면 우선 표시 + const finalGrade = row.original.finalGrade; const isAggregated = viewMode === "aggregated" && (row.original as PeriodicEvaluationAggregatedView).evaluationCount > 1; - return grade ? ( + if (finalGrade) { + return ( + <Badge + variant={getGradeBadgeVariant(finalGrade) || "outline"} + className={isAggregated ? "bg-purple-600" : ""} + > + {finalGrade}등급 + </Badge> + ); + } + + // 확정된 등급이 없으면 평가점수 기반으로 등급 계산 + const processScore = Number(row.getValue("processScore") || 0); + const priceScore = Number(row.getValue("priceScore") || 0); + const deliveryScore = Number(row.getValue("deliveryScore") || 0); + const selfEvaluationScore = Number(row.getValue("selfEvaluationScore") || 0); + const participationBonus = Number(row.getValue("participationBonus") || 0); + const qualityDeduction = Number(row.getValue("qualityDeduction") || 0); + + const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; + const evaluationScore = totalScore + participationBonus - qualityDeduction; + + // 점수 기반으로 등급 계산 + // A: 95 이상, B: 90-95 미만, C: 60-90 미만, D: 60 미만 + let calculatedGrade: string | null = null; + if (evaluationScore > 0) { + if (evaluationScore >= 95) { + calculatedGrade = "A"; + } else if (evaluationScore >= 90) { + calculatedGrade = "B"; + } else if (evaluationScore >= 60) { + calculatedGrade = "C"; + } else { + calculatedGrade = "D"; + } + } + + return calculatedGrade ? ( <Badge - variant={getGradeBadgeVariant(grade)} + variant={getGradeBadgeVariant(calculatedGrade) || "outline"} className={isAggregated ? "bg-purple-600" : ""} > - {grade} + {calculatedGrade}등급 </Badge> ) : ( <span className="text-muted-foreground">-</span> diff --git a/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx index 84651350..0e9efc8b 100644 --- a/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx +++ b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx @@ -54,6 +54,37 @@ const calculateGrade = (score: number): "A" | "B" | "C" | "D" => { return "D" } +// 등급에 따른 점수 계산 (등급 변경 시 점수 자동 조정) +const calculateScoreFromGrade = (grade: "A" | "B" | "C" | "D"): number => { + switch (grade) { + case "A": + return 95 // A등급 최소 점수 + case "B": + return 90 // B등급 최소 점수 + case "C": + return 60 // C등급 최소 점수 + case "D": + return 0 // D등급은 0점으로 설정 (또는 30점 중간값) + default: + return 0 + } +} + +// 평가점수 계산 (evaluation-columns.tsx와 동일한 로직) +const calculateEvaluationScore = (evaluation: PeriodicEvaluationView): number => { + const processScore = Number(evaluation.processScore || 0); + const priceScore = Number(evaluation.priceScore || 0); + const deliveryScore = Number(evaluation.deliveryScore || 0); + const selfEvaluationScore = Number(evaluation.selfEvaluationScore || 0); + const participationBonus = Number(evaluation.participationBonus || 0); + const qualityDeduction = Number(evaluation.qualityDeduction || 0); + + const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; + const evaluationScore = totalScore + participationBonus - qualityDeduction; + + return evaluationScore; +} + // 개별 평가 스키마 const evaluationItemSchema = z.object({ id: z.number(), @@ -61,8 +92,8 @@ const evaluationItemSchema = z.object({ vendorCode: z.string(), evaluationScore: z.coerce.number().nullable(), finalScore: z.coerce.number() - .min(0, "점수는 0 이상이어야 합니다"), - // .max(100, "점수는 100 이하여야 합니다"), + .min(0, "점수는 0 이상이어야 합니다") + .max(100, "점수는 100 이하여야 합니다"), finalGrade: z.enum(["A", "B", "C", "D"]), }) @@ -103,14 +134,29 @@ export function FinalizeEvaluationDialog({ // evaluations가 변경될 때 폼 초기화 React.useEffect(() => { if (evaluations.length > 0) { - const formData = evaluations.map(evaluation => ({ - id: evaluation.id, - vendorName: evaluation.vendorName || "", - vendorCode: evaluation.vendorCode || "", - evaluationScore: evaluation.evaluationScore ? Number(evaluation.evaluationScore) : null, - finalScore: Number(evaluation.evaluationScore || 0), - finalGrade: calculateGrade(Number(evaluation.evaluationScore || 0)), - })) + const formData = evaluations.map(evaluation => { + // 평가점수 계산 (참고용) + const evaluationScore = calculateEvaluationScore(evaluation); + + // 최종점수가 있으면 우선 사용, 없으면 평가점수 사용 + const finalScoreValue = evaluation.finalScore + ? Number(evaluation.finalScore) + : (evaluationScore > 0 ? evaluationScore : 0); + + // 최종등급이 있으면 우선 사용, 없으면 점수 기반으로 계산 + const finalGradeValue = evaluation.finalGrade + ? evaluation.finalGrade + : calculateGrade(finalScoreValue); + + return { + id: evaluation.id, + vendorName: evaluation.vendorName || "", + vendorCode: evaluation.vendorCode || "", + evaluationScore: evaluationScore > 0 ? evaluationScore : null, + finalScore: finalScoreValue, + finalGrade: finalGradeValue, + }; + }); form.reset({ evaluations: formData }) } @@ -118,13 +164,20 @@ export function FinalizeEvaluationDialog({ // 점수 변경 시 등급 자동 계산 const handleScoreChange = (index: number, score: number) => { - const currentEvaluation = form.getValues(`evaluations.${index}`) const newGrade = calculateGrade(score) + // form.setValue를 사용하여 리렌더링 최소화 + form.setValue(`evaluations.${index}.finalGrade`, newGrade, { shouldValidate: false }) + } + + // 등급 변경 시 점수 자동 조정 + const handleGradeChange = (index: number, grade: "A" | "B" | "C" | "D") => { + const currentEvaluation = form.getValues(`evaluations.${index}`) + const newScore = calculateScoreFromGrade(grade) update(index, { ...currentEvaluation, - finalScore: score, - finalGrade: newGrade, + finalScore: newScore, + finalGrade: grade, }) } @@ -230,14 +283,26 @@ export function FinalizeEvaluationDialog({ min="0" max="100" step="0.1" - {...field} + value={field.value ?? ""} onChange={(e) => { - const value = parseFloat(e.target.value) - field.onChange(value) - if (!isNaN(value)) { - handleScoreChange(index, value) + const inputValue = e.target.value + if (inputValue === "" || inputValue === "-") { + field.onChange(0) + } else { + const numValue = Number(inputValue) + if (!isNaN(numValue)) { + // 입력 중에는 제한하지 않고, blur 시에만 제한 적용 + field.onChange(numValue) + } } }} + onBlur={(e) => { + const value = Number(e.target.value) + const clampedValue = isNaN(value) ? 0 : Math.max(0, Math.min(100, value)) + field.onChange(clampedValue) + handleScoreChange(index, clampedValue) + field.onBlur() + }} className="text-center font-mono" /> </FormControl> @@ -254,7 +319,13 @@ export function FinalizeEvaluationDialog({ render={({ field }) => ( <FormItem> <FormControl> - <Select value={field.value} onValueChange={field.onChange}> + <Select + value={field.value} + onValueChange={(value) => { + field.onChange(value as "A" | "B" | "C" | "D") + handleGradeChange(index, value as "A" | "B" | "C" | "D") + }} + > <SelectTrigger> <SelectValue /> </SelectTrigger> |
