summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-12-08 09:12:44 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-12-08 09:12:44 +0000
commitb6a0f7509f4a58fd792b239a64e3f48269c73749 (patch)
treed3c79a07352c774eed4108e60ec029de897c0137
parent8a19a6fa336768d8b6712752c9d713360067ecb0 (diff)
(임수민) 협력업체 정기 평가 상세보기 수정
-rw-r--r--lib/evaluation/service.ts128
-rw-r--r--lib/evaluation/table/evaluation-details-dialog.tsx23
-rw-r--r--lib/evaluation/vendor-submission-service.ts99
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 // 제출 내용이 없음
}