diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-08 09:12:44 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-08 09:12:44 +0000 |
| commit | b6a0f7509f4a58fd792b239a64e3f48269c73749 (patch) | |
| tree | d3c79a07352c774eed4108e60ec029de897c0137 /lib | |
| parent | 8a19a6fa336768d8b6712752c9d713360067ecb0 (diff) | |
(임수민) 협력업체 정기 평가 상세보기 수정
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/evaluation/service.ts | 128 | ||||
| -rw-r--r-- | lib/evaluation/table/evaluation-details-dialog.tsx | 23 | ||||
| -rw-r--r-- | lib/evaluation/vendor-submission-service.ts | 99 |
3 files changed, 160 insertions, 90 deletions
diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts index 122d0777..9a6075bb 100644 --- a/lib/evaluation/service.ts +++ b/lib/evaluation/service.ts @@ -37,7 +37,7 @@ import { revalidatePath } from "next/cache" import { DEPARTMENT_CODE_LABELS } from "@/types/evaluation" import { getServerSession } from "next-auth" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { AttachmentDetail, EvaluationDetailResponse } from "@/types/evaluation-form" +import { AttachmentDetail, EvaluationDetailResponse, EvaluationDetailData } from "@/types/evaluation-form" import { headers } from 'next/headers'; export async function getPeriodicEvaluations(input: GetEvaluationsSchema) { @@ -1045,46 +1045,49 @@ export async function unfinalizeEvaluations(evaluationIds: number[]) { } -// 평가 상세 정보 타입 -export interface EvaluationDetailData { - // 리뷰어 정보 - reviewerEvaluationId: number - reviewerName: string - reviewerEmail: string - departmentCode: string - departmentName: string - isCompleted: boolean - completedAt: Date | null - reviewerComment: string | null - - // 평가 항목별 상세 - evaluationItems: { - // 평가 기준 정보 - criteriaId: number - category: string - category2: string - item: string - classification: string - range: string | null - remarks: string | null - scoreType: string - - // 선택된 옵션 정보 (fixed 타입인 경우) - selectedDetailId: number | null - selectedDetail: string | null - - // 점수 및 의견 - score: number | null - comment: string | null - }[] -} +// 평가 상세 정보 타입은 types/evaluation-form.ts에서 import /** * 특정 정기평가의 상세 정보를 조회합니다 */ -export async function getEvaluationDetails(periodicEvaluationId: number): Promise<EvaluationDetailResponse> { +export async function getEvaluationDetails(periodicEvaluationId: number | string): Promise<EvaluationDetailResponse> { try { + // 집계 뷰(id가 "년도_벤더ID" 형태 문자열)에서 호출될 수 있으므로 문자열을 실제 정기평가 id로 해석 + if (typeof periodicEvaluationId === "string") { + const [yearStr, vendorIdStr] = periodicEvaluationId.split("_") + const evaluationYear = Number(yearStr) + const vendorId = Number(vendorIdStr) + + if (Number.isNaN(evaluationYear) || Number.isNaN(vendorId)) { + throw new Error("잘못된 평가 식별자입니다") + } + + const fallback = await db + .select({ + id: periodicEvaluations.id, + finalizedAt: periodicEvaluations.finalizedAt, + updatedAt: periodicEvaluations.updatedAt, + }) + .from(periodicEvaluations) + .innerJoin(evaluationTargets, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id)) + .where( + and( + eq(evaluationTargets.vendorId, vendorId), + eq(evaluationTargets.evaluationYear, evaluationYear) + ) + ) + // 확정된 평가를 우선, 없으면 최신 수정 순 + .orderBy(desc(periodicEvaluations.finalizedAt), desc(periodicEvaluations.updatedAt)) + .limit(1) + + if (fallback.length === 0) { + throw new Error("해당 업체/연도의 평가를 찾을 수 없습니다") + } + + periodicEvaluationId = fallback[0].id + } + // 1. 평가 기본 정보 조회 const evaluationInfo = await db .select({ @@ -1194,12 +1197,17 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis const attachmentsByReviewerId = new Map<number, AttachmentDetail[]>() attachmentsData.forEach(attachment => { + // 필수 필드가 없으면 건너뛰기 + if (!attachment.attachmentId || !attachment.evaluationDetailId || !attachment.reviewerEvaluationId) { + return + } + const attachmentInfo: AttachmentDetail = { id: attachment.attachmentId, - originalFileName: attachment.originalFileName, - storedFileName: attachment.storedFileName, - publicPath: attachment.publicPath, - fileSize: attachment.fileSize, + originalFileName: attachment.originalFileName || "", + storedFileName: attachment.storedFileName || "", + publicPath: attachment.publicPath || "", + fileSize: attachment.fileSize || 0, mimeType: attachment.mimeType || undefined, fileExtension: attachment.fileExtension || undefined, description: attachment.description || undefined, @@ -1225,6 +1233,11 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis const reviewerDetailsMap = new Map<number, EvaluationDetailData>() reviewerDetailsRaw.forEach(row => { + // reviewerEvaluationId가 null이면 건너뛰기 + if (!row.reviewerEvaluationId) { + return + } + if (!reviewerDetailsMap.has(row.reviewerEvaluationId)) { const reviewerAttachments = attachmentsByReviewerId.get(row.reviewerEvaluationId) || [] @@ -1233,7 +1246,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis reviewerName: row.reviewerName || "", reviewerEmail: row.reviewerEmail || "", departmentCode: row.departmentCode || "", - departmentName: DEPARTMENT_CODE_LABELS[row.departmentCode as keyof typeof DEPARTMENT_CODE_LABELS] || row.departmentCode || "", + departmentName: (row.departmentCode && DEPARTMENT_CODE_LABELS[row.departmentCode as keyof typeof DEPARTMENT_CODE_LABELS]) || row.departmentCode || "", isCompleted: row.isCompleted || false, completedAt: row.completedAt, reviewerComment: row.reviewerComment, @@ -1241,7 +1254,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis // 📎 리뷰어별 첨부파일 통계 totalAttachments: reviewerAttachments.length, - totalAttachmentSize: reviewerAttachments.reduce((sum, att) => sum + att.fileSize, 0), + totalAttachmentSize: reviewerAttachments.reduce((sum, att) => sum + (att.fileSize || 0), 0), questionsWithAttachments: new Set(reviewerAttachments.map(att => attachmentsData.find(a => a.attachmentId === att.id)?.criteriaId ).filter(Boolean)).size, @@ -1249,8 +1262,11 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis } // 평가 항목이 있는 경우에만 추가 - if (row.criteriaId && row.detailId) { - const reviewer = reviewerDetailsMap.get(row.reviewerEvaluationId)! + if (row.criteriaId && row.detailId && row.reviewerEvaluationId) { + const reviewer = reviewerDetailsMap.get(row.reviewerEvaluationId) + if (!reviewer) { + return + } const itemAttachments = attachmentsByDetailId.get(row.detailId) || [] reviewer.evaluationItems.push({ @@ -1270,7 +1286,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis // 📎 항목별 첨부파일 정보 attachments: itemAttachments, attachmentCount: itemAttachments.length, - attachmentTotalSize: itemAttachments.reduce((sum, att) => sum + att.fileSize, 0), + attachmentTotalSize: itemAttachments.reduce((sum, att) => sum + (att.fileSize || 0), 0), }) } }) @@ -1278,7 +1294,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis // 📎 6. 전체 첨부파일 통계 계산 const attachmentStats = { totalFiles: attachmentsData.length, - totalSize: attachmentsData.reduce((sum, att) => sum + att.fileSize, 0), + totalSize: attachmentsData.reduce((sum, att) => sum + (att.fileSize || 0), 0), reviewersWithAttachments: attachmentsByReviewerId.size, questionsWithAttachments: new Set(attachmentsData.map(att => att.criteriaId).filter(Boolean)).size, } @@ -1299,7 +1315,6 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis id: evaluationTargets.id, vendorId: evaluationTargets.vendorId, evaluationYear: evaluationTargets.evaluationYear, - evaluationRound: evaluationTargets.evaluationRound, division: evaluationTargets.division, }) .from(evaluationTargets) @@ -1310,7 +1325,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis if (currentEvaluationTarget.length > 0) { const target = currentEvaluationTarget[0] - // 같은 업체, 같은 년도, 같은 라운드의 다른 division 평가가 있는지 확인 + // 같은 업체, 같은 년도의 다른 division 평가가 있는지 확인 const siblingEvaluations = await db .select({ periodicEvaluationId: periodicEvaluations.id, @@ -1323,13 +1338,12 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis .where( and( eq(evaluationTargets.vendorId, target.vendorId), - eq(evaluationTargets.evaluationYear, target.evaluationYear), - eq(evaluationTargets.evaluationRound, target.evaluationRound || "") + eq(evaluationTargets.evaluationYear, target.evaluationYear) ) ) // 조선과 해양 평가가 모두 있는지 확인 - const shipbuilding = siblingEvaluations.find(e => e.division === "SHIPBUILDING") + const shipbuilding = siblingEvaluations.find(e => e.division === "SHIP" || e.division === "SHIPBUILDING") const offshore = siblingEvaluations.find(e => e.division === "PLANT") if (shipbuilding && offshore) { @@ -1363,8 +1377,20 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis } } + if (!evaluationInfo[0]) { + throw new Error("평가 정보를 찾을 수 없습니다") + } + + const info = evaluationInfo[0] return { - evaluationInfo: evaluationInfo[0], + evaluationInfo: { + id: info.id, + vendorName: info.vendorName || "", + vendorCode: info.vendorCode || "", + evaluationYear: info.evaluationYear || 0, + division: info.division || "", + status: info.status, + }, reviewerDetails: Array.from(reviewerDetailsMap.values()), attachmentStats, consolidatedInfo, diff --git a/lib/evaluation/table/evaluation-details-dialog.tsx b/lib/evaluation/table/evaluation-details-dialog.tsx index fe7c204a..e89c9473 100644 --- a/lib/evaluation/table/evaluation-details-dialog.tsx +++ b/lib/evaluation/table/evaluation-details-dialog.tsx @@ -141,6 +141,12 @@ export function EvaluationDetailsDialog({ if (!evaluation) return null + // 일부 뷰 타입에는 점수/등급 필드가 없을 수 있어 안전하게 접근 + const evaluationScore = (evaluation as any)?.evaluationScore as number | null | undefined + const evaluationGrade = (evaluation as any)?.evaluationGrade as string | null | undefined + const finalScore = (evaluation as any)?.finalScore as number | null | undefined + const finalGrade = (evaluation as any)?.finalGrade as string | null | undefined + return ( <TooltipProvider> <Dialog open={open} onOpenChange={onOpenChange}> @@ -192,14 +198,14 @@ export function EvaluationDetailsDialog({ {/* 평가점수/등급 */} <div className="flex items-center gap-2"> <span className="text-muted-foreground">평가점수/등급:</span> - {evaluation.evaluationScore ? ( + {evaluationScore ? ( <div className="flex items-center gap-1"> <span className="font-bold text-blue-600"> - {Number(evaluation.evaluationScore).toFixed(1)}점 + {Number(evaluationScore).toFixed(1)}점 </span> - {evaluation.evaluationGrade && ( + {evaluationGrade && ( <Badge variant="default" className="text-xs h-5"> - {evaluation.evaluationGrade} + {evaluationGrade} </Badge> )} </div> @@ -211,14 +217,14 @@ export function EvaluationDetailsDialog({ {/* 확정점수/등급 */} <div className="flex items-center gap-2"> <span className="text-muted-foreground">확정점수/등급:</span> - {evaluation.finalScore ? ( + {finalScore ? ( <div className="flex items-center gap-1"> <span className="font-bold text-green-600"> - {Number(evaluation.finalScore).toFixed(1)}점 + {Number(finalScore).toFixed(1)}점 </span> - {evaluation.finalGrade && ( + {finalGrade && ( <Badge variant="default" className="bg-green-600 text-xs h-5"> - {evaluation.finalGrade} + {finalGrade} </Badge> )} </div> @@ -301,7 +307,6 @@ export function EvaluationDetailsDialog({ </div> </div> )} - </CardContent> </Card> </DialogHeader> diff --git a/lib/evaluation/vendor-submission-service.ts b/lib/evaluation/vendor-submission-service.ts index c06a9d2a..e7d236ad 100644 --- a/lib/evaluation/vendor-submission-service.ts +++ b/lib/evaluation/vendor-submission-service.ts @@ -107,36 +107,44 @@ export interface VendorSubmissionDetail { export async function getVendorSubmissionDetails(periodicEvaluationId: number): Promise<VendorSubmissionDetail | null> { 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, - companyId: evaluationSubmissions.companyId, - vendorCode: vendors.vendorCode, - vendorName: vendors.vendorName, - vendorEmail: vendors.email, - vendorCountry: vendors.country, - }) - .from(evaluationSubmissions) - .innerJoin(vendors, eq(evaluationSubmissions.companyId, vendors.id)) + // - 우선 현재 periodicEvaluationId 로 검색 + // - 없으면 같은 업체/년도/라운드의 다른 division 제출 건을 검색해 표시(조선/해양 동시 제출 지원) + const submissionSelect = { + // 제출 기본 정보 + 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, + companyId: evaluationSubmissions.companyId, + vendorCode: vendors.vendorCode, + vendorName: vendors.vendorName, + vendorEmail: vendors.email, + vendorCountry: vendors.country, + } + + const buildSubmissionQuery = () => + db + .select(submissionSelect) + .from(evaluationSubmissions) + .innerJoin(vendors, eq(evaluationSubmissions.companyId, vendors.id)) + + // 1-1. 현재 periodicEvaluationId 로 우선 조회 + let submissionResult = await buildSubmissionQuery() .where( and( eq(evaluationSubmissions.periodicEvaluationId, periodicEvaluationId), @@ -145,6 +153,37 @@ export async function getVendorSubmissionDetails(periodicEvaluationId: number): ) .limit(1) + // 1-2. 없으면 같은 업체/년도의 다른 division 제출을 fallback + if (submissionResult.length === 0) { + const evaluationContext = await db + .select({ + vendorId: evaluationTargets.vendorId, + evaluationYear: evaluationTargets.evaluationYear, + }) + .from(periodicEvaluations) + .innerJoin(evaluationTargets, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id)) + .where(eq(periodicEvaluations.id, periodicEvaluationId)) + .limit(1) + + const context = evaluationContext[0] + + if (!context || !context.vendorId || !context.evaluationYear) { + return null + } + + // 같은 업체/년도의 다른 division 제출 찾기 (가장 최근 제출 우선) + submissionResult = await buildSubmissionQuery() + .where( + and( + eq(evaluationSubmissions.companyId, context.vendorId), + eq(evaluationSubmissions.evaluationYear, context.evaluationYear), + eq(evaluationSubmissions.isActive, true) + ) + ) + .orderBy(desc(evaluationSubmissions.submittedAt), desc(evaluationSubmissions.createdAt)) + .limit(1) + } + if (submissionResult.length === 0) { return null // 제출 내용이 없음 } |
