From a3525f8bdfcf849cc1716fab81cb8facadbe9a8e Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 27 Oct 2025 10:03:06 +0000 Subject: (최겸) 구매 협력업체 관리(PQ/실사관리, 정기평가 협력업체 제출 상세 dialog 개발, MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/evaluation/table/evaluation-columns.tsx | 46 +- lib/evaluation/table/evaluation-table.tsx | 11 + lib/evaluation/table/vendor-submission-dialog.tsx | 623 ++++++++++++++++++++++ lib/evaluation/vendor-submission-service.ts | 369 +++++++++++++ 4 files changed, 1038 insertions(+), 11 deletions(-) create mode 100644 lib/evaluation/table/vendor-submission-dialog.tsx create mode 100644 lib/evaluation/vendor-submission-service.ts (limited to 'lib/evaluation') diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx index e8b51b57..b68aa70d 100644 --- a/lib/evaluation/table/evaluation-columns.tsx +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -6,13 +6,19 @@ 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, Ellipsis, BarChart3 } from "lucide-react"; +import { Pencil, Eye, MessageSquare, Check, X, Clock, FileText, Circle, Ellipsis, BarChart3, ChevronDown } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; import { PeriodicEvaluationView, PeriodicEvaluationAggregatedView } from "@/db/schema"; import { DataTableRowAction } from "@/types/table"; @@ -466,16 +472,34 @@ export function getPeriodicEvaluationsColumns({ cell: ({ row }) => { return (
- + + + + + + setRowAction({ row, type: "view" })} + className="flex items-center gap-2" + > + + 평가 상세 + + setRowAction({ row, type: "vendor-submission" as any })} + className="flex items-center gap-2" + > + + 협력업체 제출 상세 + + +
); }, diff --git a/lib/evaluation/table/evaluation-table.tsx b/lib/evaluation/table/evaluation-table.tsx index 4404967a..1a5b450b 100644 --- a/lib/evaluation/table/evaluation-table.tsx +++ b/lib/evaluation/table/evaluation-table.tsx @@ -37,6 +37,7 @@ import { } from "../service" import { PeriodicEvaluationsTableToolbarActions } from "./periodic-evaluations-toolbar-actions" import { EvaluationDetailsDialog } from "./evaluation-details-dialog" +import { VendorSubmissionDialog } from "./vendor-submission-dialog" import { searchParamsEvaluationsCache } from "../validation" interface PeriodicEvaluationsTableProps { @@ -745,6 +746,16 @@ export function PeriodicEvaluationsTable({ }} evaluation={rowAction?.row.original || null} /> + + { + if (!open) { + setRowAction(null); + } + }} + evaluation={rowAction?.row.original || null} + /> diff --git a/lib/evaluation/table/vendor-submission-dialog.tsx b/lib/evaluation/table/vendor-submission-dialog.tsx new file mode 100644 index 00000000..aff8dc56 --- /dev/null +++ b/lib/evaluation/table/vendor-submission-dialog.tsx @@ -0,0 +1,623 @@ +"use client" + +import * as React from "react" +import { + Eye, + Building2, + User, + Calendar, + CheckCircle2, + Clock, + MessageSquare, + Award, + FileText, + Paperclip, + Download, + File, + BarChart3, + ChevronDown, + ChevronRight +} from "lucide-react" + +import { + Dialog, + DialogContent, + 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" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Separator } from "@/components/ui/separator" +import { Skeleton } from "@/components/ui/skeleton" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { PeriodicEvaluationView } from "@/db/schema" +import { getVendorSubmissionDetails, type VendorSubmissionDetail } from "../vendor-submission-service" +import { formatFileSize, getFileInfo } from "@/lib/file-download" + +interface VendorSubmissionDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluation: PeriodicEvaluationView | null +} + +// 상태별 배지 색상 +const getSubmissionStatusBadge = (status: string) => { + switch (status) { + case "submitted": + return 제출완료 + case "draft": + return 임시저장 + case "reviewed": + return 검토완료 + default: + return {status} + } +} + +// 진행률 계산 +const getProgressPercentage = (completed: number, total: number) => { + if (total === 0) return 0 + return Math.round((completed / total) * 100) +} + +export function VendorSubmissionDialog({ + open, + onOpenChange, + evaluation, +}: VendorSubmissionDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [submissionDetails, setSubmissionDetails] = React.useState(null) + const [expandedGeneralItems, setExpandedGeneralItems] = React.useState>(new Set()) + const [expandedEsgItems, setExpandedEsgItems] = React.useState>(new Set()) + + // 첨부파일 다운로드 핸들러 + const handleDownloadAttachment = async (filePath: string, fileName: string) => { + try { + const { downloadFile } = await import('@/lib/file-download') + await downloadFile(filePath, fileName, { + action: 'download', + showToast: true, + showSuccessToast: true, + onError: (error) => { + console.error("파일 다운로드 실패:", error) + }, + }) + } catch (error) { + console.error("파일 다운로드 실패:", error) + } + } + + // 일반평가 항목 토글 + const toggleGeneralItem = (itemId: number) => { + const newExpanded = new Set(expandedGeneralItems) + if (newExpanded.has(itemId)) { + newExpanded.delete(itemId) + } else { + newExpanded.add(itemId) + } + setExpandedGeneralItems(newExpanded) + } + + // ESG 평가 항목 토글 + const toggleEsgItem = (itemId: number) => { + const newExpanded = new Set(expandedEsgItems) + if (newExpanded.has(itemId)) { + newExpanded.delete(itemId) + } else { + newExpanded.add(itemId) + } + setExpandedEsgItems(newExpanded) + } + + // 제출 상세 정보 로드 + React.useEffect(() => { + if (open && evaluation?.id) { + const loadSubmissionDetails = async () => { + try { + setIsLoading(true) + const details = await getVendorSubmissionDetails(evaluation.id) + setSubmissionDetails(details) + } catch (error) { + console.error("Failed to load vendor submission details:", error) + } finally { + setIsLoading(false) + } + } + + loadSubmissionDetails() + } + }, [open, evaluation?.id]) + + // 다이얼로그 닫을 때 데이터 리셋 + React.useEffect(() => { + if (!open) { + setSubmissionDetails(null) + setExpandedGeneralItems(new Set()) + setExpandedEsgItems(new Set()) + } + }, [open]) + + if (!evaluation) return null + + return ( + + + + {/* 고정 헤더 */} + + + + 협력업체 제출 상세 + + + {/* 평가 기본 정보 */} + + + + + 평가 정보 + + + +
+ {/* 협력업체 */} +
+ 협력업체: + {evaluation.vendorName} + ({evaluation.vendorCode}) +
+ + {/* 평가년도 */} +
+ 년도: + {evaluation.evaluationYear}년 +
+ + {/* 구분 */} +
+ 구분: + + {evaluation.division === "PLANT" ? "해양" : "조선"} + +
+ + {/* 진행상태 */} +
+ 상태: + {evaluation.status} +
+
+
+
+
+ + {/* 스크롤 가능한 컨텐츠 영역 */} +
+ {isLoading ? ( +
+ + + + + + + + +
+ ) : submissionDetails ? ( +
+ {/* 제출 정보 요약 */} + + + + + 제출 정보 + + + +
+
+
제출 상태
+
{getSubmissionStatusBadge(submissionDetails.submissionStatus)}
+
+
+
제출일
+
+ {submissionDetails.submittedAt + ? new Date(submissionDetails.submittedAt).toLocaleDateString('ko-KR') + : "-" + } +
+
+ {/*
+
ESG 평균 점수
+
+ {submissionDetails.averageEsgScore + ? `${submissionDetails.averageEsgScore.toFixed(1)}점` + : "-" + } +
+
*/} + {/*
+
검토자
+
+ {submissionDetails.reviewedBy || "-"} +
+
*/} +
+ + {/* 진행률 표시 */} + {/*
+
+
+ 일반평가 진행률 + + {submissionDetails.completedGeneralItems}/{submissionDetails.totalGeneralItems} + ({getProgressPercentage(submissionDetails.completedGeneralItems, submissionDetails.totalGeneralItems)}%) + +
+
+
+
+
+ +
+
+ ESG 평가 진행률 + + {submissionDetails.completedEsgItems}/{submissionDetails.totalEsgItems} + ({getProgressPercentage(submissionDetails.completedEsgItems, submissionDetails.totalEsgItems)}%) + +
+
+
+
+
+
*/} + + + + {/* 탭으로 일반평가와 ESG 평가 구분 */} + + + + + 일반평가 ({submissionDetails.generalEvaluations.length}개) + + {submissionDetails.vendor.country === "KR" && ( + + + ESG 평가 ({submissionDetails.esgEvaluations.length}개) + + )} + + + {/* 일반평가 탭 */} + + {submissionDetails.generalEvaluations.length > 0 ? ( +
+ {submissionDetails.generalEvaluations.map((item) => ( + + + + toggleGeneralItem(item.id)} + > +
+
+ {expandedGeneralItems.has(item.id) ? ( + + ) : ( + + )} + + {item.serialNumber}. {item.category} + +
+
+ {item.response ? ( + 응답완료 + ) : ( + 미응답 + )} + {item.response?.hasAttachments && ( + + + 첨부파일 + + )} +
+
+
+
+ + +
+
평가 항목
+
{item.inspectionItem}
+ {item.remarks && ( +
+
비고
+
{item.remarks}
+
+ )} +
+ + {item.response ? ( +
+ +
+
협력업체 응답
+
+ {item.response.responseText || "응답 내용이 없습니다."} +
+
+ + {/* 첨부파일 */} + {item.response.attachments.length > 0 && ( +
+
첨부파일
+
+ {item.response.attachments.map((attachment) => { + const fileInfo = getFileInfo(attachment.originalFileName) + return ( +
+ {fileInfo.icon} + + + + {attachment.originalFileName} + + + +
+
{attachment.originalFileName}
+
크기: {formatFileSize(attachment.fileSize)}
+
타입: {fileInfo.type}
+
업로드: {attachment.uploadedBy}
+
+
+
+ +
+ ) + })} +
+
+ )} + + {/* 검토자 의견 */} + {item.response.reviewComments && ( +
+
검토자 의견
+
+ {item.response.reviewComments} +
+
+ )} +
+ ) : ( +
+ +
아직 응답하지 않았습니다
+
+ )} +
+
+
+
+ ))} +
+ ) : ( + + +
+ +
일반평가 항목이 없습니다
+
+
+
+ )} +
+ + {/* ESG 평가 탭 */} + + {submissionDetails.esgEvaluations.length > 0 ? ( +
+ {submissionDetails.esgEvaluations.map((esgEvaluation) => ( + + + + toggleEsgItem(esgEvaluation.id)} + > +
+
+ {expandedEsgItems.has(esgEvaluation.id) ? ( + + ) : ( + + )} + + {esgEvaluation.serialNumber}. {esgEvaluation.category} + +
+
+ + {esgEvaluation.evaluationItems.length}개 항목 + +
+
+
+
+ + +
+
평가 항목
+
{esgEvaluation.inspectionItem}
+
+ + + + {/* ESG 평가 세부 항목들 */} +
+ {esgEvaluation.evaluationItems.map((item) => ( +
+
+
{item.evaluationItem}
+ {item.evaluationItemDescription && ( +
+ {item.evaluationItemDescription} +
+ )} + + {item.response ? ( +
+
+ + 선택된 답변 + + + {item.response.answerOption.answerText} + + + {item.response.selectedScore}점 + +
+ {item.response.additionalComments && ( +
+
추가 의견:
+
{item.response.additionalComments}
+
+ )} +
+ ) : ( +
+ +
아직 응답하지 않았습니다
+
+ )} +
+
+ ))} +
+
+
+
+
+ ))} +
+ ) : ( + + +
+ +
ESG 평가 항목이 없습니다
+
+
+
+ )} +
+
+ + {/* 첨부파일 요약 */} + {submissionDetails.attachmentStats.totalFiles > 0 && ( + + + + + 첨부파일 요약 + + + +
+
+
전체 파일 수
+
{submissionDetails.attachmentStats.totalFiles}개
+
+
+
전체 파일 크기
+
{formatFileSize(submissionDetails.attachmentStats.totalSize)}
+
+
+
일반평가 첨부파일
+
{submissionDetails.attachmentStats.generalEvaluationFiles}개
+
+
+
ESG 평가 첨부파일
+
{submissionDetails.attachmentStats.esgEvaluationFiles}개
+
+
+
+
+ )} + + {/* 검토자 의견 */} + {submissionDetails.reviewComments && ( + + + + + 검토자 의견 + + + +
+ {submissionDetails.reviewComments} +
+
+
+ )} +
+ ) : ( + + +
+ +
제출 내용이 없습니다
+
협력업체가 아직 평가를 제출하지 않았습니다
+
+
+
+ )} +
+ + {/* 고정 푸터 */} +
+ +
+ +
+
+ ) +} diff --git a/lib/evaluation/vendor-submission-service.ts b/lib/evaluation/vendor-submission-service.ts new file mode 100644 index 00000000..388f382a --- /dev/null +++ b/lib/evaluation/vendor-submission-service.ts @@ -0,0 +1,369 @@ +'use server' + +import db from "@/db/db" +import { + evaluationSubmissions, + generalEvaluations, + generalEvaluationResponses, + esgEvaluations, + esgEvaluationItems, + esgAnswerOptions, + esgEvaluationResponses, + vendorEvaluationAttachments, + periodicEvaluations, + evaluationTargets, + vendors, +} from "@/db/schema" +import { eq, and, asc, desc, sql } from "drizzle-orm" + +// 협력업체 제출 상세 정보 타입 정의 +export interface VendorSubmissionDetail { + // 제출 기본 정보 + submissionId: string + evaluationYear: number + evaluationRound: string | null + submissionStatus: string + submittedAt: Date | null + reviewedAt: Date | null + reviewedBy: string | null + reviewComments: string | null + averageEsgScore: number | null + + // 진행률 통계 + totalGeneralItems: number + completedGeneralItems: number + totalEsgItems: number + completedEsgItems: number + + // 협력업체 정보 + vendor: { + id: number + vendorCode: string + vendorName: string + email: string | null + country: string | null + } + + // 일반평가 응답 + generalEvaluations: { + id: number + serialNumber: string + category: string + inspectionItem: string + remarks: string | null + response: { + responseText: string + hasAttachments: boolean + reviewComments: string | null + attachments: { + id: number + fileId: string + originalFileName: string + storedFileName: string + filePath: string + fileSize: number + mimeType: string | null + uploadedBy: string + createdAt: Date + }[] + } | null + }[] + + // ESG 평가 응답 + esgEvaluations: { + id: number + serialNumber: string + category: string + inspectionItem: string + evaluationItems: { + id: number + evaluationItem: string + evaluationItemDescription: string | null + orderIndex: number + response: { + selectedScore: number + additionalComments: string | null + answerOption: { + id: number + answerText: string + score: number + } + } | null + }[] + }[] + + // 첨부파일 통계 + attachmentStats: { + totalFiles: number + totalSize: number + generalEvaluationFiles: number + esgEvaluationFiles: number + } +} + +/** + * 특정 정기평가에 대한 협력업체 제출 상세 정보를 조회합니다 + */ +export async function getVendorSubmissionDetails(periodicEvaluationId: number): Promise { + try { + // 1. 제출 정보 조회 + const submissionResult = await db + .select({ + // 제출 기본 정보 + id: evaluationSubmissions.id, + submissionId: evaluationSubmissions.submissionId, + evaluationYear: evaluationSubmissions.evaluationYear, + evaluationRound: evaluationSubmissions.evaluationRound, + submissionStatus: evaluationSubmissions.submissionStatus, + submittedAt: evaluationSubmissions.submittedAt, + reviewedAt: evaluationSubmissions.reviewedAt, + reviewedBy: evaluationSubmissions.reviewedBy, + reviewComments: evaluationSubmissions.reviewComments, + averageEsgScore: evaluationSubmissions.averageEsgScore, + + // 진행률 통계 + totalGeneralItems: evaluationSubmissions.totalGeneralItems, + completedGeneralItems: evaluationSubmissions.completedGeneralItems, + totalEsgItems: evaluationSubmissions.totalEsgItems, + completedEsgItems: evaluationSubmissions.completedEsgItems, + + // 협력업체 정보 + vendorId: vendors.id, + vendorCode: vendors.vendorCode, + vendorName: vendors.vendorName, + vendorEmail: vendors.email, + vendorCountry: vendors.country, + }) + .from(evaluationSubmissions) + .innerJoin(vendors, eq(evaluationSubmissions.companyId, vendors.id)) + .where( + and( + eq(evaluationSubmissions.periodicEvaluationId, periodicEvaluationId), + eq(evaluationSubmissions.isActive, true) + ) + ) + .limit(1) + + if (submissionResult.length === 0) { + return null // 제출 내용이 없음 + } + + const submission = submissionResult[0] + const submissionId = submission.id // evaluationSubmissions.id (integer) + const submissionUuid = submission.submissionId // evaluationSubmissions.submissionId (UUID) + + console.log("=== 협력업체 제출 상세 조회 시작 ===") + console.log("submissionId (integer):", submissionId) + console.log("submissionUuid:", submissionUuid) + console.log("submission:", submission) + + // 2. 일반평가 항목과 응답 조회 + const generalEvaluationsResult = await db + .select({ + // 일반평가 항목 정보 + generalEvaluationId: generalEvaluations.id, + serialNumber: generalEvaluations.serialNumber, + category: generalEvaluations.category, + inspectionItem: generalEvaluations.inspectionItem, + remarks: generalEvaluations.remarks, + + // 응답 정보 + responseId: generalEvaluationResponses.id, + responseText: generalEvaluationResponses.responseText, + hasAttachments: generalEvaluationResponses.hasAttachments, + reviewComments: generalEvaluationResponses.reviewComments, + + // 첨부파일 정보 + attachmentId: vendorEvaluationAttachments.id, + fileId: vendorEvaluationAttachments.fileId, + originalFileName: vendorEvaluationAttachments.originalFileName, + storedFileName: vendorEvaluationAttachments.storedFileName, + filePath: vendorEvaluationAttachments.filePath, + fileSize: vendorEvaluationAttachments.fileSize, + mimeType: vendorEvaluationAttachments.mimeType, + uploadedBy: vendorEvaluationAttachments.uploadedBy, + attachmentCreatedAt: vendorEvaluationAttachments.createdAt, + }) + .from(generalEvaluations) + .leftJoin( + generalEvaluationResponses, + and( + eq(generalEvaluationResponses.generalEvaluationId, generalEvaluations.id), + eq(generalEvaluationResponses.submissionId, submissionId), + eq(generalEvaluationResponses.isActive, true) + ) + ) + .leftJoin( + vendorEvaluationAttachments, + eq(vendorEvaluationAttachments.generalEvaluationResponseId, generalEvaluationResponses.id) + ) + .where(eq(generalEvaluations.isActive, true)) + .orderBy(asc(generalEvaluations.serialNumber)) + + // 3. ESG 평가 항목과 응답 조회 + const esgEvaluationsResult = await db + .select({ + // ESG 평가표 정보 + esgEvaluationId: esgEvaluations.id, + esgSerialNumber: esgEvaluations.serialNumber, + esgCategory: esgEvaluations.category, + esgInspectionItem: esgEvaluations.inspectionItem, + + // ESG 평가항목 정보 + esgEvaluationItemId: esgEvaluationItems.id, + evaluationItem: esgEvaluationItems.evaluationItem, + evaluationItemDescription: esgEvaluationItems.evaluationItemDescription, + orderIndex: esgEvaluationItems.orderIndex, + + // ESG 응답 정보 + esgResponseId: esgEvaluationResponses.id, + selectedScore: esgEvaluationResponses.selectedScore, + additionalComments: esgEvaluationResponses.additionalComments, + + // ESG 답변 옵션 정보 + answerOptionId: esgAnswerOptions.id, + answerText: esgAnswerOptions.answerText, + answerScore: esgAnswerOptions.score, + }) + .from(esgEvaluations) + .innerJoin(esgEvaluationItems, eq(esgEvaluationItems.esgEvaluationId, esgEvaluations.id)) + .leftJoin( + esgEvaluationResponses, + and( + eq(esgEvaluationResponses.esgEvaluationItemId, esgEvaluationItems.id), + eq(esgEvaluationResponses.submissionId, submissionId), + eq(esgEvaluationResponses.isActive, true) + ) + ) + .leftJoin( + esgAnswerOptions, + eq(esgAnswerOptions.id, esgEvaluationResponses.esgAnswerOptionId) + ) + .where( + and( + eq(esgEvaluations.isActive, true), + eq(esgEvaluationItems.isActive, true) + ) + ) + .orderBy( + asc(esgEvaluations.serialNumber), + asc(esgEvaluationItems.orderIndex) + ) + + // 4. 데이터 가공 + // 일반평가 데이터 그룹화 + const generalEvaluationsMap = new Map() + generalEvaluationsResult.forEach(row => { + if (!generalEvaluationsMap.has(row.generalEvaluationId)) { + generalEvaluationsMap.set(row.generalEvaluationId, { + id: row.generalEvaluationId, + serialNumber: row.serialNumber, + category: row.category, + inspectionItem: row.inspectionItem, + remarks: row.remarks, + response: row.responseId ? { + responseText: row.responseText, + hasAttachments: row.hasAttachments, + reviewComments: row.reviewComments, + attachments: [] + } : null + }) + } + + // 첨부파일 추가 + if (row.attachmentId && generalEvaluationsMap.get(row.generalEvaluationId)?.response) { + generalEvaluationsMap.get(row.generalEvaluationId).response.attachments.push({ + id: row.attachmentId, + fileId: row.fileId, + originalFileName: row.originalFileName, + storedFileName: row.storedFileName, + filePath: row.filePath, + fileSize: row.fileSize, + mimeType: row.mimeType, + uploadedBy: row.uploadedBy, + createdAt: new Date(row.attachmentCreatedAt) + }) + } + }) + + // ESG 평가 데이터 그룹화 + const esgEvaluationsMap = new Map() + esgEvaluationsResult.forEach(row => { + if (!esgEvaluationsMap.has(row.esgEvaluationId)) { + esgEvaluationsMap.set(row.esgEvaluationId, { + id: row.esgEvaluationId, + serialNumber: row.esgSerialNumber, + category: row.esgCategory, + inspectionItem: row.esgInspectionItem, + evaluationItems: [] + }) + } + + const esgEvaluation = esgEvaluationsMap.get(row.esgEvaluationId) + + // 평가항목 추가 (중복 방지) + const existingItem = esgEvaluation.evaluationItems.find((item: any) => item.id === row.esgEvaluationItemId) + if (!existingItem) { + esgEvaluation.evaluationItems.push({ + id: row.esgEvaluationItemId, + evaluationItem: row.evaluationItem, + evaluationItemDescription: row.evaluationItemDescription, + orderIndex: row.orderIndex, + response: row.esgResponseId ? { + selectedScore: Number(row.selectedScore), + additionalComments: row.additionalComments, + answerOption: { + id: row.answerOptionId, + answerText: row.answerText, + score: Number(row.answerScore) + } + } : null + }) + } + }) + + // 5. 첨부파일 통계 계산 + const allAttachments = generalEvaluationsResult + .filter(row => row.attachmentId) + .map(row => ({ + id: row.attachmentId, + fileSize: row.fileSize + })) + + const attachmentStats = { + totalFiles: allAttachments.length, + totalSize: allAttachments.reduce((sum, att) => sum + att.fileSize, 0), + generalEvaluationFiles: allAttachments.length, + esgEvaluationFiles: 0 // ESG는 첨부파일 없음 + } + + return { + submissionId: submission.submissionId, + evaluationYear: submission.evaluationYear, + evaluationRound: submission.evaluationRound, + submissionStatus: submission.submissionStatus, + submittedAt: submission.submittedAt, + reviewedAt: submission.reviewedAt, + reviewedBy: submission.reviewedBy, + reviewComments: submission.reviewComments, + averageEsgScore: submission.averageEsgScore ? Number(submission.averageEsgScore) : null, + + + vendor: { + id: submission.vendorId, + vendorCode: submission.vendorCode || "", + vendorName: submission.vendorName, + email: submission.vendorEmail, + country: submission.vendorCountry, + }, + + generalEvaluations: Array.from(generalEvaluationsMap.values()), + esgEvaluations: Array.from(esgEvaluationsMap.values()), + attachmentStats + } + + } catch (error) { + console.error("Error fetching vendor submission details:", error) + throw new Error("협력업체 제출 상세 정보 조회 중 오류가 발생했습니다") + } +} \ No newline at end of file -- cgit v1.2.3