diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-10 09:55:45 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-10 09:55:45 +0000 |
| commit | c657ef972feeafff16ab0e07cb4771f7dd141ba0 (patch) | |
| tree | befabd884b00d3cc632c628b3e3810f61cc9f38d /lib/evaluation/table | |
| parent | b8a03c9d130435a71c5d6217d06ccb0beb9697e5 (diff) | |
(대표님) 20250710 작업사항 - 평가 첨부, 로그인, SEDP 변경 요구사항 반영
Diffstat (limited to 'lib/evaluation/table')
4 files changed, 403 insertions, 277 deletions
diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx index 4b7d9a80..315ec66b 100644 --- a/lib/evaluation/table/evaluation-columns.tsx +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -8,7 +8,7 @@ import { type ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Pencil, Eye, MessageSquare, Check, X, Clock, FileText, Circle } from "lucide-react"; +import { Pencil, Eye, MessageSquare, Check, X, Clock, FileText, Circle, Ellipsis } from "lucide-react"; import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; import { PeriodicEvaluationView } from "@/db/schema"; import { DataTableRowAction } from "@/types/table"; @@ -40,11 +40,11 @@ const getStatusBadgeVariant = (status: string) => { const getStatusLabel = (status: string) => { const statusMap = { - PENDING_SUBMISSION: "제출대기", + PENDING_SUBMISSION: "자료접수중", SUBMITTED: "제출완료", - IN_REVIEW: "검토중", - REVIEW_COMPLETED: "검토완료", - FINALIZED: "최종확정" + IN_REVIEW: "평가중", + REVIEW_COMPLETED: "평가완료", + FINALIZED: "결과확정" }; return statusMap[status] || status; }; @@ -215,6 +215,14 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): size: 80, }, + { + accessorKey: "status", + header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="Status" />, + cell: ({ row }) => getStatusLabel(row.original.status || ""), + size: 80, + }, + + // ═══════════════════════════════════════════════════════════════ // 협력업체 정보 // ═══════════════════════════════════════════════════════════════ @@ -329,6 +337,32 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): }, ] }, + + { + // id: "평가상세", + // accessorKey: "평가상세", + header: "평가상세", + enableHiding: true, + size: 80, + minSize: 80, + cell: ({ row }) => { + return ( + <div className="flex items-center gap-1"> + <Button + variant="ghost" + size="icon" + className="size-8" + onClick={() => setRowAction({ row, type: "view" })} + aria-label="상세보기" + title="상세보기" + > + <Ellipsis className="size-4" /> + </Button> + + </div> + ); + }, + }, // ═══════════════════════════════════════════════════════════════ // 제출 현황 // ═══════════════════════════════════════════════════════════════ @@ -549,32 +583,5 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ] }, - - - - // ░░░ Actions ░░░ - { - id: "actions", - enableHiding: false, - size: 40, - minSize: 40, - cell: ({ row }) => { - return ( - <div className="flex items-center gap-1"> - <Button - variant="ghost" - size="icon" - className="size-8" - onClick={() => setRowAction({ row, type: "view" })} - aria-label="상세보기" - title="상세보기" - > - <Eye className="size-4" /> - </Button> - - </div> - ); - }, - }, ]; }
\ No newline at end of file diff --git a/lib/evaluation/table/evaluation-details-dialog.tsx b/lib/evaluation/table/evaluation-details-dialog.tsx index df4ef016..2f682402 100644 --- a/lib/evaluation/table/evaluation-details-dialog.tsx +++ b/lib/evaluation/table/evaluation-details-dialog.tsx @@ -10,7 +10,11 @@ import { Clock, MessageSquare, Award, - FileText + FileText, + Paperclip, + Download, + File, + BarChart3 } from "lucide-react" import { @@ -19,6 +23,12 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" @@ -26,7 +36,10 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Separator } from "@/components/ui/separator" import { Skeleton } from "@/components/ui/skeleton" import { PeriodicEvaluationView } from "@/db/schema" -import { getEvaluationDetails, type EvaluationDetailData } from "../service" +import { getEvaluationDetails} from "../service" +import { AttachmentDetail, EvaluationDetailResponse } from "@/types/evaluation-form" +// 파일 다운로드 유틸리티 import +import { downloadFile, formatFileSize, getFileInfo } from "@/lib/file-download" interface EvaluationDetailsDialogProps { open: boolean @@ -75,10 +88,30 @@ export function EvaluationDetailsDialog({ evaluation, }: EvaluationDetailsDialogProps) { const [isLoading, setIsLoading] = React.useState(false) - const [evaluationDetails, setEvaluationDetails] = React.useState<{ - evaluationInfo: any - reviewerDetails: EvaluationDetailData[] - } | null>(null) + const [evaluationDetails, setEvaluationDetails] = React.useState<EvaluationDetailResponse | null>(null) + + // 첨부파일 다운로드 핸들러 - downloadFile 사용 + const handleDownloadAttachment = async (attachment: AttachmentDetail) => { + try { + 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) : '') + } + } + ) + } catch (error) { + console.error("다운로드 처리 중 오류:", error) + } + } // 평가 상세 정보 로드 React.useEffect(() => { @@ -109,258 +142,346 @@ export function EvaluationDetailsDialog({ if (!evaluation) return null return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-7xl max-h-[90vh] overflow-y-auto"> - <DialogHeader className="space-y-4"> - <DialogTitle className="flex items-center gap-2"> - <Eye className="h-5 w-5 text-blue-600" /> - 평가 상세 - </DialogTitle> - - {/* 평가 기본 정보 */} - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2 text-lg"> - <Building2 className="h-5 w-5" /> - 평가 정보 - </CardTitle> - </CardHeader> - <CardContent> - <div className="flex flex-wrap items-center gap-6 text-sm"> - {/* 협력업체 */} - <div className="flex items-center gap-2"> - <span className="text-muted-foreground">협력업체:</span> - <span className="font-medium">{evaluation.vendorName}</span> - <span className="text-muted-foreground">({evaluation.vendorCode})</span> - </div> + <TooltipProvider> + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-7xl h-[90vh] flex flex-col"> + {/* 고정 헤더 */} + <DialogHeader className="space-y-4 flex-shrink-0"> + <DialogTitle className="flex items-center gap-2"> + <Eye className="h-5 w-5 text-blue-600" /> + 평가 상세 + </DialogTitle> + + {/* 평가 기본 정보 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2 text-lg"> + <Building2 className="h-5 w-5" /> + 평가 정보 + </CardTitle> + </CardHeader> + <CardContent> + <div className="flex flex-wrap items-center gap-6 text-sm mb-4"> + {/* 협력업체 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">협력업체:</span> + <span className="font-medium">{evaluation.vendorName}</span> + <span className="text-muted-foreground">({evaluation.vendorCode})</span> + </div> - {/* 평가년도 */} - <div className="flex items-center gap-2"> - <span className="text-muted-foreground">년도:</span> - <span className="font-medium">{evaluation.evaluationYear}년</span> - </div> + {/* 평가년도 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">년도:</span> + <span className="font-medium">{evaluation.evaluationYear}년</span> + </div> - {/* 구분 */} - <div className="flex items-center gap-2"> - <span className="text-muted-foreground">구분:</span> - <Badge variant="outline" className="text-xs"> - {evaluation.division === "PLANT" ? "해양" : "조선"} - </Badge> - </div> + {/* 구분 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">구분:</span> + <Badge variant="outline" className="text-xs"> + {evaluation.division === "PLANT" ? "해양" : "조선"} + </Badge> + </div> - {/* 진행상태 */} - <div className="flex items-center gap-2"> - <span className="text-muted-foreground">상태:</span> - <Badge variant="secondary" className="text-xs">{evaluation.status}</Badge> - </div> + {/* 진행상태 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">상태:</span> + <Badge variant="secondary" className="text-xs">{evaluation.status}</Badge> + </div> - {/* 평가점수/등급 */} - <div className="flex items-center gap-2"> - <span className="text-muted-foreground">평가점수/등급:</span> - {evaluation.evaluationScore ? ( - <div className="flex items-center gap-1"> - <span className="font-bold text-blue-600"> - {Number(evaluation.evaluationScore).toFixed(1)}점 - </span> - {evaluation.evaluationGrade && ( - <Badge variant="default" className="text-xs h-5"> - {evaluation.evaluationGrade} - </Badge> - )} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - )} - </div> + {/* 평가점수/등급 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">평가점수/등급:</span> + {evaluation.evaluationScore ? ( + <div className="flex items-center gap-1"> + <span className="font-bold text-blue-600"> + {Number(evaluation.evaluationScore).toFixed(1)}점 + </span> + {evaluation.evaluationGrade && ( + <Badge variant="default" className="text-xs h-5"> + {evaluation.evaluationGrade} + </Badge> + )} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </div> - {/* 확정점수/등급 */} - <div className="flex items-center gap-2"> - <span className="text-muted-foreground">확정점수/등급:</span> - {evaluation.finalScore ? ( - <div className="flex items-center gap-1"> - <span className="font-bold text-green-600"> - {Number(evaluation.finalScore).toFixed(1)}점 - </span> - {evaluation.finalGrade && ( - <Badge variant="default" className="bg-green-600 text-xs h-5"> - {evaluation.finalGrade} - </Badge> - )} - </div> - ) : ( - <span className="text-muted-foreground">미확정</span> - )} + {/* 확정점수/등급 */} + <div className="flex items-center gap-2"> + <span className="text-muted-foreground">확정점수/등급:</span> + {evaluation.finalScore ? ( + <div className="flex items-center gap-1"> + <span className="font-bold text-green-600"> + {Number(evaluation.finalScore).toFixed(1)}점 + </span> + {evaluation.finalGrade && ( + <Badge variant="default" className="bg-green-600 text-xs h-5"> + {evaluation.finalGrade} + </Badge> + )} + </div> + ) : ( + <span className="text-muted-foreground">미확정</span> + )} + </div> </div> - </div> - </CardContent> - </Card> - </DialogHeader> - {isLoading ? ( - <div className="space-y-4"> - <Card> - <CardHeader> - <Skeleton className="h-6 w-48" /> - </CardHeader> - <CardContent> - <Skeleton className="h-64 w-full" /> - </CardContent> + </CardContent> </Card> - </div> - ) : evaluationDetails ? ( - <div className="space-y-6"> - {/* 통합 평가 테이블 */} - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <FileText className="h-5 w-5" /> - 평가 상세 내역 - </CardTitle> - </CardHeader> - <CardContent> - {evaluationDetails.reviewerDetails.some(r => r.evaluationItems.length > 0) ? ( - <Table> - <TableHeader> - <TableRow> - <TableHead className="w-[120px]">담당자</TableHead> - {/* <TableHead className="w-[80px]">상태</TableHead> */} - <TableHead className="w-[100px]">평가부문</TableHead> - <TableHead className="w-[100px]">항목</TableHead> - <TableHead className="w-[150px]">구분</TableHead> - <TableHead className="w-[200px]">범위</TableHead> - <TableHead className="w-[200px]">선택옵션</TableHead> - <TableHead className="w-[80px]">점수</TableHead> - <TableHead className="min-w-[200px]">의견</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {evaluationDetails.reviewerDetails.map((reviewer) => - reviewer.evaluationItems.map((item, index) => ( - <TableRow key={`${reviewer.reviewerEvaluationId}-${item.criteriaId}-${index}`}> - <TableCell> - <div className="space-y-1"> - <div className="font-medium text-sm">{reviewer.departmentName}</div> - <div className="text-xs text-muted-foreground"> - {reviewer.reviewerName} - </div> - </div> - </TableCell> - {/* <TableCell> - {reviewer.isCompleted ? ( - <Badge variant="default" className="flex items-center gap-1"> - <CheckCircle2 className="h-3 w-3" /> - 완료 - </Badge> - ) : ( - <Badge variant="secondary" className="flex items-center gap-1"> - <Clock className="h-3 w-3" /> - 진행중 - </Badge> - )} - </TableCell> */} - <TableCell> - <Badge variant={getCategoryBadgeVariant(item.category)}> - {CATEGORY_LABELS[item.category as keyof typeof CATEGORY_LABELS] || item.category} - </Badge> - </TableCell> - <TableCell> - {CATEGORY_LABELS2[item.item as keyof typeof CATEGORY_LABELS2] || item.item} - </TableCell> - <TableCell className="font-medium"> - {item.classification} - </TableCell> - <TableCell className="text-sm"> - {item.range || "-"} - </TableCell> - <TableCell className="text-sm"> - {item.scoreType === "variable" ? ( - <Badge variant="outline">직접 입력</Badge> - ) : ( - item.selectedDetail || "-" - )} - </TableCell> - <TableCell> - {item.score !== null ? ( - <Badge variant="default" className="font-mono"> - {item.score.toFixed(1)} + </DialogHeader> + + {/* 스크롤 가능한 컨텐츠 영역 */} + <div className="flex-1 overflow-y-auto min-h-0"> + {isLoading ? ( + <div className="space-y-4 p-1"> + <Card> + <CardHeader> + <Skeleton className="h-6 w-48" /> + </CardHeader> + <CardContent> + <Skeleton className="h-64 w-full" /> + </CardContent> + </Card> + </div> + ) : evaluationDetails ? ( + <div className="space-y-6 p-1"> + {/* 통합 평가 테이블 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + 평가 상세 내역 + </CardTitle> + </CardHeader> + <CardContent> + {evaluationDetails.reviewerDetails.some(r => r.evaluationItems.length > 0) ? ( + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[120px]">담당자</TableHead> + <TableHead className="w-[100px]">평가부문</TableHead> + <TableHead className="w-[100px]">항목</TableHead> + <TableHead className="w-[150px]">구분</TableHead> + <TableHead className="w-[200px]">범위</TableHead> + <TableHead className="w-[200px]">선택옵션</TableHead> + <TableHead className="w-[80px]">점수</TableHead> + <TableHead className="w-[150px]">첨부파일</TableHead> + <TableHead className="min-w-[200px]">의견</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {evaluationDetails.reviewerDetails.map((reviewer) => + reviewer.evaluationItems.map((item, index) => ( + <TableRow key={`${reviewer.reviewerEvaluationId}-${item.criteriaId}-${index}`}> + <TableCell> + <div className="space-y-1"> + <div className="font-medium text-sm">{reviewer.departmentName}</div> + <div className="text-xs text-muted-foreground"> + {reviewer.reviewerName} + </div> + </div> + </TableCell> + <TableCell> + <Badge variant={getCategoryBadgeVariant(item.category)}> + {CATEGORY_LABELS[item.category as keyof typeof CATEGORY_LABELS] || item.category} + </Badge> + </TableCell> + <TableCell> + {CATEGORY_LABELS2[item.item as keyof typeof CATEGORY_LABELS2] || item.item} + </TableCell> + <TableCell className="font-medium"> + {item.classification} + </TableCell> + <TableCell className="text-sm"> + {item.range || "-"} + </TableCell> + <TableCell className="text-sm"> + {item.scoreType === "variable" ? ( + <Badge variant="outline">직접 입력</Badge> + ) : ( + item.selectedDetail || "-" + )} + </TableCell> + <TableCell> + {item.score !== null ? ( + <Badge variant="default" className="font-mono"> + {item.score.toFixed(1)} + </Badge> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </TableCell> + + {/* 📎 첨부파일 컬럼 - 개선된 버전 */} + <TableCell> + {item.attachments.length > 0 ? ( + <div className="space-y-1"> + {item.attachments.map((attachment) => { + const fileInfo = getFileInfo(attachment.originalFileName) + return ( + <div key={attachment.id} className="flex items-center gap-1 p-1 bg-muted rounded"> + <span className="text-sm">{fileInfo.icon}</span> + <Tooltip> + <TooltipTrigger asChild> + <span className="text-xs truncate max-w-[80px] cursor-help"> + {attachment.originalFileName} + </span> + </TooltipTrigger> + <TooltipContent> + <div className="text-xs space-y-1"> + <div className="font-medium">{attachment.originalFileName}</div> + <div>크기: {formatFileSize(attachment.fileSize)}</div> + <div>타입: {fileInfo.type}</div> + {attachment.description && ( + <div>설명: {attachment.description}</div> + )} + <div>업로드: {attachment.uploadedByName}</div> + <div className="text-muted-foreground"> + {fileInfo.canPreview ? "미리보기 가능" : "다운로드만 가능"} + </div> + </div> + </TooltipContent> + </Tooltip> + <Button + variant="ghost" + size="sm" + className="h-4 w-4 p-0" + onClick={() => handleDownloadAttachment(attachment)} + > + <Download className="h-3 w-3" /> + </Button> + </div> + ) + })} + {item.attachments.length > 1 && ( + <div className="text-xs text-muted-foreground"> + 총 {formatFileSize(item.attachmentTotalSize)} + </div> + )} + </div> + ) : ( + <div className="text-xs text-muted-foreground"> + 첨부파일 없음 + </div> + )} + </TableCell> + + <TableCell className="text-sm"> + {item.comment || ( + <span className="text-muted-foreground">의견 없음</span> + )} + </TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + ) : ( + <div className="text-center text-muted-foreground py-8"> + <FileText className="h-8 w-8 mx-auto mb-2" /> + <div>평가 항목이 없습니다</div> + </div> + )} + </CardContent> + </Card> + + {/* 리뷰어별 종합 의견 (있는 경우만) */} + {evaluationDetails.reviewerDetails.some(r => r.reviewerComment) && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <MessageSquare className="h-5 w-5" /> + 종합 의견 + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {evaluationDetails.reviewerDetails + .filter(reviewer => reviewer.reviewerComment) + .map((reviewer) => ( + <div key={reviewer.reviewerEvaluationId} className="space-y-2"> + <div className="flex items-center gap-2"> + <Badge variant="outline">{reviewer.departmentName}</Badge> + <span className="text-sm font-medium">{reviewer.reviewerName}</span> + {reviewer.totalAttachments > 0 && ( + <Badge variant="secondary" className="text-xs"> + <Paperclip className="h-3 w-3 mr-1" /> + {reviewer.totalAttachments}개 파일 </Badge> - ) : ( - <span className="text-muted-foreground">-</span> )} - </TableCell> - <TableCell className="text-sm"> - {item.comment || ( - <span className="text-muted-foreground">의견 없음</span> - )} - </TableCell> - </TableRow> - )) - )} - </TableBody> - </Table> - ) : ( - <div className="text-center text-muted-foreground py-8"> - <FileText className="h-8 w-8 mx-auto mb-2" /> - <div>평가 항목이 없습니다</div> - </div> + </div> + <div className="bg-muted p-3 rounded-md text-sm"> + {reviewer.reviewerComment} + </div> + </div> + ))} + </CardContent> + </Card> )} - </CardContent> - </Card> - {/* 리뷰어별 종합 의견 (있는 경우만) */} - {evaluationDetails.reviewerDetails.some(r => r.reviewerComment) && ( - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <MessageSquare className="h-5 w-5" /> - 종합 의견 - </CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - {evaluationDetails.reviewerDetails - .filter(reviewer => reviewer.reviewerComment) - .map((reviewer) => ( - <div key={reviewer.reviewerEvaluationId} className="space-y-2"> - <div className="flex items-center gap-2"> - <Badge variant="outline">{reviewer.departmentName}</Badge> - <span className="text-sm font-medium">{reviewer.reviewerName}</span> + {/* 📎 첨부파일 요약 (파일이 많은 경우) */} + {evaluationDetails.attachmentStats.totalFiles > 5 && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <BarChart3 className="h-5 w-5" /> + 첨부파일 요약 + </CardTitle> + </CardHeader> + <CardContent> + <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> + <div className="space-y-1"> + <div className="text-muted-foreground">전체 파일 수</div> + <div className="font-bold text-lg">{evaluationDetails.attachmentStats.totalFiles}개</div> </div> - <div className="bg-muted p-3 rounded-md text-sm"> - {reviewer.reviewerComment} + <div className="space-y-1"> + <div className="text-muted-foreground">전체 파일 크기</div> + <div className="font-bold text-lg">{formatFileSize(evaluationDetails.attachmentStats.totalSize)}</div> + </div> + <div className="space-y-1"> + <div className="text-muted-foreground">첨부 질문 수</div> + <div className="font-bold text-lg">{evaluationDetails.attachmentStats.questionsWithAttachments}개</div> + </div> + <div className="space-y-1"> + <div className="text-muted-foreground">첨부 담당자 수</div> + <div className="font-bold text-lg">{evaluationDetails.attachmentStats.reviewersWithAttachments}명</div> </div> </div> - ))} - </CardContent> - </Card> - )} + </CardContent> + </Card> + )} - {evaluationDetails.reviewerDetails.length === 0 && ( - <Card> + {evaluationDetails.reviewerDetails.length === 0 && ( + <Card> + <CardContent className="py-8"> + <div className="text-center text-muted-foreground"> + <User className="h-8 w-8 mx-auto mb-2" /> + <div>배정된 리뷰어가 없습니다</div> + </div> + </CardContent> + </Card> + )} + </div> + ) : ( + <Card className="m-1"> <CardContent className="py-8"> <div className="text-center text-muted-foreground"> - <User className="h-8 w-8 mx-auto mb-2" /> - <div>배정된 리뷰어가 없습니다</div> + 평가 상세 정보를 불러올 수 없습니다 </div> </CardContent> </Card> )} </div> - ) : ( - <Card> - <CardContent className="py-8"> - <div className="text-center text-muted-foreground"> - 평가 상세 정보를 불러올 수 없습니다 - </div> - </CardContent> - </Card> - )} - <div className="flex justify-end pt-4"> - <Button variant="outline" onClick={() => onOpenChange(false)}> - 닫기 - </Button> - </div> - </DialogContent> - </Dialog> + {/* 고정 푸터 */} + <div className="flex justify-end pt-4 border-t flex-shrink-0"> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 닫기 + </Button> + </div> + </DialogContent> + </Dialog> + </TooltipProvider> ) }
\ No newline at end of file diff --git a/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx index d6784754..84651350 100644 --- a/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx +++ b/lib/evaluation/table/periodic-evaluation-finalize-dialogs.tsx @@ -40,18 +40,16 @@ import { finalizeEvaluations } from "../service" // 등급 옵션 const GRADE_OPTIONS = [ - { value: "S", label: "S등급 (90점 이상)" }, - { value: "A", label: "A등급 (80-89점)" }, - { value: "B", label: "B등급 (70-79점)" }, - { value: "C", label: "C등급 (60-69점)" }, + { value: "A", label: "A등급 (95점 이상)" }, + { value: "B", label: "B등급 (90-95점 미만)" }, + { value: "C", label: "C등급 (60-90점 미만)" }, { value: "D", label: "D등급 (60점 미만)" }, ] as const // 점수에 따른 등급 계산 -const calculateGrade = (score: number): "S" | "A" | "B" | "C" | "D" => { - if (score >= 90) return "S" - if (score >= 80) return "A" - if (score >= 70) return "B" +const calculateGrade = (score: number): "A" | "B" | "C" | "D" => { + if (score >= 95) return "A" + if (score >= 90) return "B" if (score >= 60) return "C" return "D" } @@ -65,7 +63,7 @@ const evaluationItemSchema = z.object({ finalScore: z.coerce.number() .min(0, "점수는 0 이상이어야 합니다"), // .max(100, "점수는 100 이하여야 합니다"), - finalGrade: z.enum(["S", "A", "B", "C", "D"]), + finalGrade: z.enum(["A", "B", "C", "D"]), }) // 전체 폼 스키마 diff --git a/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx b/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx index 39a95cc7..d910f916 100644 --- a/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx +++ b/lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx @@ -49,7 +49,7 @@ export function PeriodicEvaluationsTableToolbarActions({ // 권한 체크 (방법 1 또는 방법 2 중 선택) const { hasRole, isLoading: roleLoading } = useAuthRole() - const canManageEvaluations = hasRole('정기평가') + const canManageEvaluations = hasRole('정기평가') || hasRole('admin') // 선택된 행들 const selectedRows = table.getFilteredSelectedRowModel().rows |
