summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-17 08:09:08 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-17 08:09:08 +0000
commit4a077640becddf65ac2dc98451c5c83aa70108f8 (patch)
treeab5ccdb4425ab5788d248a3bf7c4d87d551989af /lib
parenta5c5c88e3033854b78ffc2fe73b8b6dd0502aa61 (diff)
(임수민) 협력업체 정기평가 수정
Diffstat (limited to 'lib')
-rw-r--r--lib/evaluation/service.ts81
-rw-r--r--lib/evaluation/table/evaluation-details-dialog.tsx74
-rw-r--r--lib/evaluation/vendor-submission-service.ts66
-rw-r--r--lib/vendor-evaluation-submit/table/esg-evaluation-form-sheet.tsx7
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>