diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-17 08:09:08 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-17 08:09:08 +0000 |
| commit | 4a077640becddf65ac2dc98451c5c83aa70108f8 (patch) | |
| tree | ab5ccdb4425ab5788d248a3bf7c4d87d551989af /lib | |
| parent | a5c5c88e3033854b78ffc2fe73b8b6dd0502aa61 (diff) | |
(임수민) 협력업체 정기평가 수정
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/evaluation/service.ts | 81 | ||||
| -rw-r--r-- | lib/evaluation/table/evaluation-details-dialog.tsx | 74 | ||||
| -rw-r--r-- | lib/evaluation/vendor-submission-service.ts | 66 | ||||
| -rw-r--r-- | lib/vendor-evaluation-submit/table/esg-evaluation-form-sheet.tsx | 7 |
4 files changed, 221 insertions, 7 deletions
diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts index b958e371..122d0777 100644 --- a/lib/evaluation/service.ts +++ b/lib/evaluation/service.ts @@ -1283,10 +1283,91 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis questionsWithAttachments: new Set(attachmentsData.map(att => att.criteriaId).filter(Boolean)).size, } + // 🔄 7. 조선/해양 취합 정보 계산 (동시 평가인 경우) + let consolidatedInfo: { + shipbuildingScore: number | null + shipbuildingGrade: string | null + offshoreScore: number | null + offshoreGrade: string | null + consolidatedScore: number | null + consolidatedGrade: string | null + } | undefined = undefined + + // 현재 평가의 evaluationTarget 정보 가져오기 + const currentEvaluationTarget = await db + .select({ + id: evaluationTargets.id, + vendorId: evaluationTargets.vendorId, + evaluationYear: evaluationTargets.evaluationYear, + evaluationRound: evaluationTargets.evaluationRound, + division: evaluationTargets.division, + }) + .from(evaluationTargets) + .innerJoin(periodicEvaluations, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id)) + .where(eq(periodicEvaluations.id, periodicEvaluationId)) + .limit(1) + + if (currentEvaluationTarget.length > 0) { + const target = currentEvaluationTarget[0] + + // 같은 업체, 같은 년도, 같은 라운드의 다른 division 평가가 있는지 확인 + const siblingEvaluations = await db + .select({ + periodicEvaluationId: periodicEvaluations.id, + division: evaluationTargets.division, + finalScore: periodicEvaluations.finalScore, + finalGrade: periodicEvaluations.finalGrade, + }) + .from(evaluationTargets) + .innerJoin(periodicEvaluations, eq(periodicEvaluations.evaluationTargetId, evaluationTargets.id)) + .where( + and( + eq(evaluationTargets.vendorId, target.vendorId), + eq(evaluationTargets.evaluationYear, target.evaluationYear), + eq(evaluationTargets.evaluationRound, target.evaluationRound || "") + ) + ) + + // 조선과 해양 평가가 모두 있는지 확인 + const shipbuilding = siblingEvaluations.find(e => e.division === "SHIPBUILDING") + const offshore = siblingEvaluations.find(e => e.division === "PLANT") + + if (shipbuilding && offshore) { + // 두 평가가 모두 있으면 취합 정보 계산 + const shipScore = shipbuilding.finalScore ? Number(shipbuilding.finalScore) : null + const offScore = offshore.finalScore ? Number(offshore.finalScore) : null + + let consolidatedScore: number | null = null + let consolidatedGrade: string | null = null + + // 둘 다 점수가 있으면 50% 반영하여 취합 + if (shipScore !== null && offScore !== null) { + consolidatedScore = Math.round((shipScore * 0.5 + offScore * 0.5) * 10) / 10 + + // 취합 등급 계산 (점수 기준) + if (consolidatedScore >= 90) consolidatedGrade = "S" + else if (consolidatedScore >= 80) consolidatedGrade = "A" + else if (consolidatedScore >= 70) consolidatedGrade = "B" + else if (consolidatedScore >= 60) consolidatedGrade = "C" + else consolidatedGrade = "D" + } + + consolidatedInfo = { + shipbuildingScore: shipScore, + shipbuildingGrade: shipbuilding.finalGrade, + offshoreScore: offScore, + offshoreGrade: offshore.finalGrade, + consolidatedScore, + consolidatedGrade, + } + } + } + return { evaluationInfo: evaluationInfo[0], reviewerDetails: Array.from(reviewerDetailsMap.values()), attachmentStats, + consolidatedInfo, } } catch (error) { diff --git a/lib/evaluation/table/evaluation-details-dialog.tsx b/lib/evaluation/table/evaluation-details-dialog.tsx index 2f682402..fe7c204a 100644 --- a/lib/evaluation/table/evaluation-details-dialog.tsx +++ b/lib/evaluation/table/evaluation-details-dialog.tsx @@ -228,6 +228,80 @@ export function EvaluationDetailsDialog({ </div> </div> + {/* 🔄 조선/해양 취합 정보 (동시 평가인 경우) */} + {evaluationDetails?.consolidatedInfo && ( + <div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-md"> + <div className="flex items-center gap-2 mb-3"> + <BarChart3 className="h-5 w-5 text-blue-600" /> + <span className="font-semibold text-blue-900">조선/해양 취합 결과</span> + </div> + <div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> + {/* 조선 점수 */} + <div className="space-y-1"> + <div className="text-muted-foreground">조선 확정점수/등급</div> + <div className="flex items-center gap-1"> + {evaluationDetails.consolidatedInfo.shipbuildingScore !== null ? ( + <> + <span className="font-bold text-blue-700"> + {evaluationDetails.consolidatedInfo.shipbuildingScore.toFixed(1)}점 + </span> + {evaluationDetails.consolidatedInfo.shipbuildingGrade && ( + <Badge variant="outline" className="text-xs h-5"> + {evaluationDetails.consolidatedInfo.shipbuildingGrade} + </Badge> + )} + </> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </div> + </div> + + {/* 해양 점수 */} + <div className="space-y-1"> + <div className="text-muted-foreground">해양 확정점수/등급</div> + <div className="flex items-center gap-1"> + {evaluationDetails.consolidatedInfo.offshoreScore !== null ? ( + <> + <span className="font-bold text-blue-700"> + {evaluationDetails.consolidatedInfo.offshoreScore.toFixed(1)}점 + </span> + {evaluationDetails.consolidatedInfo.offshoreGrade && ( + <Badge variant="outline" className="text-xs h-5"> + {evaluationDetails.consolidatedInfo.offshoreGrade} + </Badge> + )} + </> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </div> + </div> + + {/* 취합 점수 (50% 반영) */} + <div className="space-y-1"> + <div className="text-muted-foreground">취합 점수/등급 (50% 반영)</div> + <div className="flex items-center gap-1"> + {evaluationDetails.consolidatedInfo.consolidatedScore !== null ? ( + <> + <span className="font-bold text-purple-700 text-base"> + {evaluationDetails.consolidatedInfo.consolidatedScore.toFixed(1)}점 + </span> + {evaluationDetails.consolidatedInfo.consolidatedGrade && ( + <Badge variant="default" className="bg-purple-600 text-xs h-5"> + {evaluationDetails.consolidatedInfo.consolidatedGrade} + </Badge> + )} + </> + ) : ( + <span className="text-muted-foreground">-</span> + )} + </div> + </div> + </div> + </div> + )} + </CardContent> </Card> </DialogHeader> diff --git a/lib/evaluation/vendor-submission-service.ts b/lib/evaluation/vendor-submission-service.ts index 388f382a..c06a9d2a 100644 --- a/lib/evaluation/vendor-submission-service.ts +++ b/lib/evaluation/vendor-submission-service.ts @@ -129,6 +129,7 @@ export async function getVendorSubmissionDetails(periodicEvaluationId: number): // 협력업체 정보 vendorId: vendors.id, + companyId: evaluationSubmissions.companyId, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName, vendorEmail: vendors.email, @@ -149,7 +150,7 @@ export async function getVendorSubmissionDetails(periodicEvaluationId: number): } const submission = submissionResult[0] - const submissionId = submission.id // evaluationSubmissions.id (integer) + let submissionId = submission.id // evaluationSubmissions.id (integer) const submissionUuid = submission.submissionId // evaluationSubmissions.submissionId (UUID) console.log("=== 협력업체 제출 상세 조회 시작 ===") @@ -157,6 +158,58 @@ export async function getVendorSubmissionDetails(periodicEvaluationId: number): console.log("submissionUuid:", submissionUuid) console.log("submission:", submission) + // 🔍 조선/해양 동시 제출 케이스 처리: 현재 submission에 응답이 없으면 같은 그룹에서 응답이 있는 submission을 찾아서 사용 + const hasResponses = await db + .select({ count: sql<number>`COUNT(*)::int` }) + .from(generalEvaluationResponses) + .where( + and( + eq(generalEvaluationResponses.submissionId, submissionId), + eq(generalEvaluationResponses.isActive, true) + ) + ) + .then(result => (result[0]?.count || 0) > 0) + + if (!hasResponses) { + console.log("현재 submission에 응답 없음. 같은 그룹에서 응답이 있는 submission 찾기...") + + // 같은 companyId, evaluationYear, evaluationRound를 가진 다른 submission 중 응답이 있는 것을 찾음 + const siblingSubmissions = await db + .select({ + id: evaluationSubmissions.id, + submissionId: evaluationSubmissions.submissionId, + }) + .from(evaluationSubmissions) + .where( + and( + eq(evaluationSubmissions.companyId, submission.companyId), + eq(evaluationSubmissions.evaluationYear, submission.evaluationYear), + eq(evaluationSubmissions.evaluationRound, submission.evaluationRound || ""), + eq(evaluationSubmissions.isActive, true) + ) + ) + + // 각 sibling submission에 응답이 있는지 확인 + for (const sibling of siblingSubmissions) { + const siblingHasResponses = await db + .select({ count: sql<number>`COUNT(*)::int` }) + .from(generalEvaluationResponses) + .where( + and( + eq(generalEvaluationResponses.submissionId, sibling.id), + eq(generalEvaluationResponses.isActive, true) + ) + ) + .then(result => (result[0]?.count || 0) > 0) + + if (siblingHasResponses) { + console.log(`응답이 있는 submission 발견: ${sibling.id}`) + submissionId = sibling.id + break + } + } + } + // 2. 일반평가 항목과 응답 조회 const generalEvaluationsResult = await db .select({ @@ -281,7 +334,7 @@ export async function getVendorSubmissionDetails(periodicEvaluationId: number): fileSize: row.fileSize, mimeType: row.mimeType, uploadedBy: row.uploadedBy, - createdAt: new Date(row.attachmentCreatedAt) + createdAt: row.attachmentCreatedAt ? new Date(row.attachmentCreatedAt) : new Date() }) } }) @@ -324,10 +377,10 @@ export async function getVendorSubmissionDetails(periodicEvaluationId: number): // 5. 첨부파일 통계 계산 const allAttachments = generalEvaluationsResult - .filter(row => row.attachmentId) + .filter(row => row.attachmentId && row.fileSize !== null) .map(row => ({ id: row.attachmentId, - fileSize: row.fileSize + fileSize: row.fileSize || 0 })) const attachmentStats = { @@ -348,6 +401,11 @@ export async function getVendorSubmissionDetails(periodicEvaluationId: number): reviewComments: submission.reviewComments, averageEsgScore: submission.averageEsgScore ? Number(submission.averageEsgScore) : null, + // 진행률 통계 + totalGeneralItems: submission.totalGeneralItems, + completedGeneralItems: submission.completedGeneralItems, + totalEsgItems: submission.totalEsgItems, + completedEsgItems: submission.completedEsgItems, vendor: { id: submission.vendorId, diff --git a/lib/vendor-evaluation-submit/table/esg-evaluation-form-sheet.tsx b/lib/vendor-evaluation-submit/table/esg-evaluation-form-sheet.tsx index 0ebe1f8c..f3778f26 100644 --- a/lib/vendor-evaluation-submit/table/esg-evaluation-form-sheet.tsx +++ b/lib/vendor-evaluation-submit/table/esg-evaluation-form-sheet.tsx @@ -115,7 +115,8 @@ export function EsgEvaluationFormSheet({ additionalComments: item.response?.additionalComments || '', }) - if (item.response?.selectedScore) { + // 0점도 유효한 응답이므로 undefined/null만 체크 + if (item.response?.selectedScore !== undefined && item.response?.selectedScore !== null) { scores[item.item.id] = item.response.selectedScore } }) @@ -468,7 +469,7 @@ const handleExportData = async () => { <BarChart3Icon className="h-4 w-4" /> <span className="text-sm"> {evaluation.items.filter(item => - currentScores[item.item.id] >= 0 + currentScores[item.item.id] !== undefined ).length}/{evaluation.items.length} </span> </div> @@ -490,7 +491,7 @@ const handleExportData = async () => { <CardHeader className="pb-3"> <CardTitle className="text-sm flex items-center justify-between"> <span>{item.item.evaluationItem}</span> - {currentScores[item.item.id] > 0 && ( + {currentScores[item.item.id] !== undefined && currentScores[item.item.id] !== null && ( <Badge variant="default" className="bg-green-100 text-green-800"> {currentScores[item.item.id]}점 </Badge> |
