From c657ef972feeafff16ab0e07cb4771f7dd141ba0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 10 Jul 2025 09:55:45 +0000 Subject: (대표님) 20250710 작업사항 - 평가 첨부, 로그인, SEDP 변경 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/evaluation/table/evaluation-columns.tsx | 71 +-- lib/evaluation/table/evaluation-details-dialog.tsx | 591 +++++++++++++-------- .../table/periodic-evaluation-finalize-dialogs.tsx | 16 +- .../table/periodic-evaluations-toolbar-actions.tsx | 2 +- 4 files changed, 403 insertions(+), 277 deletions(-) (limited to 'lib/evaluation/table') 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 }) => , + 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 ( +
+ + +
+ ); + }, + }, // ═══════════════════════════════════════════════════════════════ // 제출 현황 // ═══════════════════════════════════════════════════════════════ @@ -549,32 +583,5 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ] }, - - - - // ░░░ Actions ░░░ - { - id: "actions", - enableHiding: false, - size: 40, - minSize: 40, - cell: ({ row }) => { - return ( -
- - -
- ); - }, - }, ]; } \ 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(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 ( - - - - - - 평가 상세 - - - {/* 평가 기본 정보 */} - - - - - 평가 정보 - - - -
- {/* 협력업체 */} -
- 협력업체: - {evaluation.vendorName} - ({evaluation.vendorCode}) -
+ + + + {/* 고정 헤더 */} + + + + 평가 상세 + + + {/* 평가 기본 정보 */} + + + + + 평가 정보 + + + +
+ {/* 협력업체 */} +
+ 협력업체: + {evaluation.vendorName} + ({evaluation.vendorCode}) +
- {/* 평가년도 */} -
- 년도: - {evaluation.evaluationYear}년 -
+ {/* 평가년도 */} +
+ 년도: + {evaluation.evaluationYear}년 +
- {/* 구분 */} -
- 구분: - - {evaluation.division === "PLANT" ? "해양" : "조선"} - -
+ {/* 구분 */} +
+ 구분: + + {evaluation.division === "PLANT" ? "해양" : "조선"} + +
- {/* 진행상태 */} -
- 상태: - {evaluation.status} -
+ {/* 진행상태 */} +
+ 상태: + {evaluation.status} +
- {/* 평가점수/등급 */} -
- 평가점수/등급: - {evaluation.evaluationScore ? ( -
- - {Number(evaluation.evaluationScore).toFixed(1)}점 - - {evaluation.evaluationGrade && ( - - {evaluation.evaluationGrade} - - )} -
- ) : ( - - - )} -
+ {/* 평가점수/등급 */} +
+ 평가점수/등급: + {evaluation.evaluationScore ? ( +
+ + {Number(evaluation.evaluationScore).toFixed(1)}점 + + {evaluation.evaluationGrade && ( + + {evaluation.evaluationGrade} + + )} +
+ ) : ( + - + )} +
- {/* 확정점수/등급 */} -
- 확정점수/등급: - {evaluation.finalScore ? ( -
- - {Number(evaluation.finalScore).toFixed(1)}점 - - {evaluation.finalGrade && ( - - {evaluation.finalGrade} - - )} -
- ) : ( - 미확정 - )} + {/* 확정점수/등급 */} +
+ 확정점수/등급: + {evaluation.finalScore ? ( +
+ + {Number(evaluation.finalScore).toFixed(1)}점 + + {evaluation.finalGrade && ( + + {evaluation.finalGrade} + + )} +
+ ) : ( + 미확정 + )} +
-
-
-
-
- {isLoading ? ( -
- - - - - - - + -
- ) : evaluationDetails ? ( -
- {/* 통합 평가 테이블 */} - - - - - 평가 상세 내역 - - - - {evaluationDetails.reviewerDetails.some(r => r.evaluationItems.length > 0) ? ( - - - - 담당자 - {/* 상태 */} - 평가부문 - 항목 - 구분 - 범위 - 선택옵션 - 점수 - 의견 - - - - {evaluationDetails.reviewerDetails.map((reviewer) => - reviewer.evaluationItems.map((item, index) => ( - - -
-
{reviewer.departmentName}
-
- {reviewer.reviewerName} -
-
-
- {/* - {reviewer.isCompleted ? ( - - - 완료 - - ) : ( - - - 진행중 - - )} - */} - - - {CATEGORY_LABELS[item.category as keyof typeof CATEGORY_LABELS] || item.category} - - - - {CATEGORY_LABELS2[item.item as keyof typeof CATEGORY_LABELS2] || item.item} - - - {item.classification} - - - {item.range || "-"} - - - {item.scoreType === "variable" ? ( - 직접 입력 - ) : ( - item.selectedDetail || "-" - )} - - - {item.score !== null ? ( - - {item.score.toFixed(1)} + + + {/* 스크롤 가능한 컨텐츠 영역 */} +
+ {isLoading ? ( +
+ + + + + + + + +
+ ) : evaluationDetails ? ( +
+ {/* 통합 평가 테이블 */} + + + + + 평가 상세 내역 + + + + {evaluationDetails.reviewerDetails.some(r => r.evaluationItems.length > 0) ? ( +
+ + + 담당자 + 평가부문 + 항목 + 구분 + 범위 + 선택옵션 + 점수 + 첨부파일 + 의견 + + + + {evaluationDetails.reviewerDetails.map((reviewer) => + reviewer.evaluationItems.map((item, index) => ( + + +
+
{reviewer.departmentName}
+
+ {reviewer.reviewerName} +
+
+
+ + + {CATEGORY_LABELS[item.category as keyof typeof CATEGORY_LABELS] || item.category} + + + + {CATEGORY_LABELS2[item.item as keyof typeof CATEGORY_LABELS2] || item.item} + + + {item.classification} + + + {item.range || "-"} + + + {item.scoreType === "variable" ? ( + 직접 입력 + ) : ( + item.selectedDetail || "-" + )} + + + {item.score !== null ? ( + + {item.score.toFixed(1)} + + ) : ( + - + )} + + + {/* 📎 첨부파일 컬럼 - 개선된 버전 */} + + {item.attachments.length > 0 ? ( +
+ {item.attachments.map((attachment) => { + const fileInfo = getFileInfo(attachment.originalFileName) + return ( +
+ {fileInfo.icon} + + + + {attachment.originalFileName} + + + +
+
{attachment.originalFileName}
+
크기: {formatFileSize(attachment.fileSize)}
+
타입: {fileInfo.type}
+ {attachment.description && ( +
설명: {attachment.description}
+ )} +
업로드: {attachment.uploadedByName}
+
+ {fileInfo.canPreview ? "미리보기 가능" : "다운로드만 가능"} +
+
+
+
+ +
+ ) + })} + {item.attachments.length > 1 && ( +
+ 총 {formatFileSize(item.attachmentTotalSize)} +
+ )} +
+ ) : ( +
+ 첨부파일 없음 +
+ )} +
+ + + {item.comment || ( + 의견 없음 + )} + +
+ )) + )} +
+
+ ) : ( +
+ +
평가 항목이 없습니다
+
+ )} +
+
+ + {/* 리뷰어별 종합 의견 (있는 경우만) */} + {evaluationDetails.reviewerDetails.some(r => r.reviewerComment) && ( + + + + + 종합 의견 + + + + {evaluationDetails.reviewerDetails + .filter(reviewer => reviewer.reviewerComment) + .map((reviewer) => ( +
+
+ {reviewer.departmentName} + {reviewer.reviewerName} + {reviewer.totalAttachments > 0 && ( + + + {reviewer.totalAttachments}개 파일 - ) : ( - - )} - - - {item.comment || ( - 의견 없음 - )} - - - )) - )} - - - ) : ( -
- -
평가 항목이 없습니다
-
+
+
+ {reviewer.reviewerComment} +
+
+ ))} +
+
)} - - - {/* 리뷰어별 종합 의견 (있는 경우만) */} - {evaluationDetails.reviewerDetails.some(r => r.reviewerComment) && ( - - - - - 종합 의견 - - - - {evaluationDetails.reviewerDetails - .filter(reviewer => reviewer.reviewerComment) - .map((reviewer) => ( -
-
- {reviewer.departmentName} - {reviewer.reviewerName} + {/* 📎 첨부파일 요약 (파일이 많은 경우) */} + {evaluationDetails.attachmentStats.totalFiles > 5 && ( + + + + + 첨부파일 요약 + + + +
+
+
전체 파일 수
+
{evaluationDetails.attachmentStats.totalFiles}개
-
- {reviewer.reviewerComment} +
+
전체 파일 크기
+
{formatFileSize(evaluationDetails.attachmentStats.totalSize)}
+
+
+
첨부 질문 수
+
{evaluationDetails.attachmentStats.questionsWithAttachments}개
+
+
+
첨부 담당자 수
+
{evaluationDetails.attachmentStats.reviewersWithAttachments}명
- ))} - - - )} + + + )} - {evaluationDetails.reviewerDetails.length === 0 && ( - + {evaluationDetails.reviewerDetails.length === 0 && ( + + +
+ +
배정된 리뷰어가 없습니다
+
+
+
+ )} +
+ ) : ( +
- -
배정된 리뷰어가 없습니다
+ 평가 상세 정보를 불러올 수 없습니다
)}
- ) : ( - - -
- 평가 상세 정보를 불러올 수 없습니다 -
-
-
- )} -
- -
- -
+ {/* 고정 푸터 */} +
+ +
+ +
+ ) } \ 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 -- cgit v1.2.3