summaryrefslogtreecommitdiff
path: root/lib/evaluation/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-17 10:50:28 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-17 10:50:28 +0000
commitfb276ed3db86fe4fc0c0fcd870fd3d085b034be0 (patch)
tree4a8ab1027d7fd14602a0f837d4e18b04e2169e58 /lib/evaluation/service.ts
parent4eb7532f822c821fb6b69bf103bd075fefba769b (diff)
(대표님) 벤더데이터 S-EDP 변경사항 대응(seperator), 정기평가 점수오류, dim 준비
Diffstat (limited to 'lib/evaluation/service.ts')
-rw-r--r--lib/evaluation/service.ts179
1 files changed, 140 insertions, 39 deletions
diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts
index 3e85b4a2..c49521da 100644
--- a/lib/evaluation/service.ts
+++ b/lib/evaluation/service.ts
@@ -14,7 +14,10 @@ import {
reviewerEvaluations,
roles,
userRoles,
+ userView,
users,
+ vendorContacts,
+ vendors,
type PeriodicEvaluationView
} from "@/db/schema"
import {
@@ -23,7 +26,7 @@ import {
count,
desc,
ilike,
- or, sql, eq, avg, inArray,like,
+ or, sql, eq, avg, inArray, like,
type SQL
} from "drizzle-orm"
import { filterColumns } from "@/lib/filter-columns"
@@ -34,6 +37,8 @@ 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 { headers } from 'next/headers';
+
export async function getPeriodicEvaluations(input: GetEvaluationTargetsSchema) {
try {
@@ -306,6 +311,8 @@ interface RequestDocumentsData {
evaluationYear: number
evaluationRound: string
message: string
+ dueDate?: string; // 선택적 필드로 추가
+
}
export async function requestDocumentsFromVendors(data: RequestDocumentsData[]) {
@@ -319,7 +326,7 @@ export async function requestDocumentsFromVendors(data: RequestDocumentsData[])
eq(evaluationSubmissions.periodicEvaluationId, item.periodicEvaluationId),
eq(evaluationSubmissions.companyId, item.companyId)
)
- })
+ });
if (existingSubmission) {
// 이미 존재하면 reviewComments만 업데이트
@@ -330,9 +337,9 @@ export async function requestDocumentsFromVendors(data: RequestDocumentsData[])
updatedAt: new Date()
})
.where(eq(evaluationSubmissions.id, existingSubmission.id))
- .returning()
+ .returning();
- return updated
+ return updated;
} else {
// 새로 생성
const [created] = await db
@@ -351,16 +358,16 @@ export async function requestDocumentsFromVendors(data: RequestDocumentsData[])
completedEsgItems: 0,
isActive: true
})
- .returning()
+ .returning();
- return created
+ return created;
}
})
- )
+ );
// periodic_evaluations 테이블의 status를 PENDING_SUBMISSION으로 업데이트
- const periodicEvaluationIds = [...new Set(data.map(item => item.periodicEvaluationId))]
-
+ const periodicEvaluationIds = [...new Set(data.map(item => item.periodicEvaluationId))];
+
await Promise.all(
periodicEvaluationIds.map(async (periodicEvaluationId) => {
await db
@@ -369,23 +376,115 @@ export async function requestDocumentsFromVendors(data: RequestDocumentsData[])
status: 'PENDING_SUBMISSION',
updatedAt: new Date()
})
- .where(eq(periodicEvaluations.id, periodicEvaluationId))
+ .where(eq(periodicEvaluations.id, periodicEvaluationId));
})
- )
+ );
+
+ // 📧 이메일 발송 로직 추가
+ const emailResults = await Promise.allSettled(
+ data.map(async (item) => {
+ // 해당 vendor의 정보와 연락처 정보 조회
+ const vendorInfo = await db.query.vendors.findFirst({
+ where: eq(vendors.id, item.companyId),
+ with: {
+ contacts: {
+ where: eq(vendorContacts.isPrimary, true), // 주 연락처 우선
+ limit: 1
+ }
+ }
+ });
+
+ if (!vendorInfo) {
+ throw new Error(`Vendor not found for companyId: ${item.companyId}`);
+ }
+
+ // 이메일 주소 결정: 주 연락처 > vendor 이메일
+ const emailAddress = vendorInfo.email;
+
+ if (!emailAddress) {
+ throw new Error(`No email address found for vendor: ${vendorInfo.vendorName}`);
+ }
+
+ // CC 이메일 주소들 수집 (주 연락처가 아닌 다른 연락처들)
+ const allContacts = await db.query.vendorContacts.findMany({
+ where: and(
+ eq(vendorContacts.vendorId, item.companyId),
+ eq(vendorContacts.isPrimary, false)
+ )
+ });
+
+ const ccEmails = allContacts
+ .map(contact => contact.contactEmail)
+ .filter(email => email && email !== emailAddress);
+
+ const headersList = await headers();
+ const host = headersList.get('host') || 'localhost:3000';
+
+ const baseUrl = `http://${host}`
+
+ // 정기평가 담당자 이메일 조회
+ const evaluationManagers = await db
+ .select()
+ .from(userView)
+ .where(sql`'정기평가' = ANY(${userView.roles})`)
+ .limit(1);
+
+ const evaluationManager = evaluationManagers[0];
+ const supportEmail = evaluationManager?.user_email ||process.env.Email_From_Address ||'vendor-support@samsung.com';
+
+ // 이메일 템플릿 데이터 준비
+ const templateData = {
+ companyName: vendorInfo.vendorName,
+ evaluationYear: item.evaluationYear,
+ evaluationRound: item.evaluationRound,
+ requestDate: new Date().toLocaleDateString('ko-KR'),
+ dueDate: item.dueDate ? new Date(item.dueDate).toLocaleDateString('ko-KR') : null,
+ reviewComments: item.message,
+ accessUrl:`${baseUrl}/partners/evaluation`,
+ supportEmail:supportEmail,
+ businessHours: '평일 8:00-17:00 (한국시간)'
+ };
+
+ // 이메일 발송
+ return await sendEmail({
+ to: emailAddress,
+ cc: ccEmails.length > 0 ? ccEmails : undefined,
+ subject: `[SHI] ${item.evaluationYear}년 ${item.evaluationRound || ''} 협력업체 평가 자료 요청`,
+ template: 'vendor-evalution-request',
+ context: templateData
+ });
+ })
+ );
+
+ // 이메일 발송 결과 분석
+ const emailSuccessCount = emailResults.filter(result => result.status === 'fulfilled').length;
+ const emailFailures = emailResults
+ .filter(result => result.status === 'rejected')
+ .map(result => (result as PromiseRejectedResult).reason);
+
+ // 실패한 이메일이 있으면 로그에 기록
+ if (emailFailures.length > 0) {
+ console.error('이메일 발송 실패:', emailFailures);
+ }
return {
success: true,
message: `${submissions.length}개 업체에 자료 요청이 완료되었습니다.`,
+ emailResults: {
+ totalSent: emailSuccessCount,
+ totalFailed: emailFailures.length,
+ failures: emailFailures
+ },
submissions
- }
+ };
} catch (error) {
- console.error("Error requesting documents from vendors:", error)
+ console.error("Error requesting documents from vendors:", error);
return {
success: false,
message: "자료 요청 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error"
- }
+ };
}
}
// 기존 요청 상태 확인 함수 추가
@@ -491,7 +590,7 @@ export async function getReviewersForEvaluations(
// 3. role 기반 리뷰어들을 각 evaluationTargetId에 대해 확장
const expandedRoleBasedReviewers: ReviewerInfo[] = []
-
+
for (const evaluationTargetId of evaluationTargetIds) {
for (const reviewer of roleBasedReviewers) {
expandedRoleBasedReviewers.push({
@@ -508,11 +607,11 @@ export async function getReviewersForEvaluations(
// 4. 중복 제거 (같은 사용자가 designated reviewer와 role-based reviewer 모두에 있을 수 있음)
const allReviewers = [...designatedReviewers, ...expandedRoleBasedReviewers]
-
+
// evaluationTargetId + userId 조합으로 중복 제거
const uniqueReviewers = allReviewers.reduce((acc, reviewer) => {
const key = `${reviewer.evaluationTargetId}-${reviewer.id}`
-
+
// 이미 있는 경우 designated reviewer를 우선 (evaluationTargetReviewerId가 양수인 것)
if (acc[key]) {
if (reviewer.evaluationTargetReviewerId > 0) {
@@ -522,7 +621,7 @@ export async function getReviewersForEvaluations(
} else {
acc[key] = reviewer
}
-
+
return acc
}, {} as Record<string, ReviewerInfo>)
@@ -714,8 +813,6 @@ async function sendEvaluationRequestEmails(
}
})
- console.log('평가 그룹:', evaluationGroups)
-
// 4. 각 리뷰어에게 개별 이메일 발송
const emailPromises = []
@@ -730,6 +827,10 @@ async function sendEvaluationRequestEmails(
const otherReviewers = group.reviewers.filter(r => r?.evaluationTargetReviewerId !== reviewer.evaluationTargetReviewerId)
console.log(`${reviewer.userName}(${reviewer.userEmail})에게 이메일 발송 준비`)
+ const headersList = await headers();
+ const host = headersList.get('host') || 'localhost:3000';
+
+ const baseUrl = `http://${host}`
const emailPromise = sendEmail({
to: reviewer.userEmail,
@@ -755,7 +856,7 @@ async function sendEvaluationRequestEmails(
email: r?.userEmail
})).filter(r => r.name),
message: message || "협력업체 정기평가를 진행해 주시기 바랍니다.",
- evaluationUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/evaluations/${group.periodicEvaluationId}/review`
+ evaluationUrl: `${baseUrl}/procurement/evaluation-input/${group.periodicEvaluationId}`
},
}).catch(error => {
console.error(`${reviewer.userEmail}에게 이메일 발송 실패:`, error)
@@ -828,7 +929,7 @@ export async function getReviewerEvaluationStatus(
interface FinalizeEvaluationData {
id: number
finalScore: number
- finalGrade: "S" | "A" | "B" | "C" | "D"
+ finalGrade: "A" | "B" | "C" | "D"
}
/**
@@ -978,7 +1079,7 @@ export interface EvaluationDetailData {
isCompleted: boolean
completedAt: Date | null
reviewerComment: string | null
-
+
// 평가 항목별 상세
evaluationItems: {
// 평가 기준 정보
@@ -990,11 +1091,11 @@ export interface EvaluationDetailData {
range: string | null
remarks: string | null
scoreType: string
-
+
// 선택된 옵션 정보 (fixed 타입인 경우)
selectedDetailId: number | null
selectedDetail: string | null
-
+
// 점수 및 의견
score: number | null
comment: string | null
@@ -1012,7 +1113,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
.select({
id: periodicEvaluations.id,
vendorName: evaluationTargets.vendorName,
- vendorCode: evaluationTargets.vendorCode,
+ vendorCode: evaluationTargets.vendorCode,
evaluationYear: evaluationTargets.evaluationYear,
division: evaluationTargets.division,
status: periodicEvaluations.status,
@@ -1037,7 +1138,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
isCompleted: reviewerEvaluations.isCompleted,
completedAt: reviewerEvaluations.completedAt,
reviewerComment: reviewerEvaluations.reviewerComment,
-
+
// 평가 항목 상세
detailId: reviewerEvaluationDetails.id,
criteriaId: regEvalCriteria.id,
@@ -1048,11 +1149,11 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
range: regEvalCriteria.range,
remarks: regEvalCriteria.remarks,
scoreType: regEvalCriteria.scoreType,
-
+
// 선택된 옵션 정보
selectedDetailId: reviewerEvaluationDetails.regEvalCriteriaDetailsId,
selectedDetail: regEvalCriteriaDetails.detail,
-
+
// 점수 및 의견
score: reviewerEvaluationDetails.score,
comment: reviewerEvaluationDetails.comment,
@@ -1080,14 +1181,14 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
description: reviewerEvaluationAttachments.description,
uploadedBy: reviewerEvaluationAttachments.uploadedBy,
attachmentCreatedAt: reviewerEvaluationAttachments.createdAt,
-
+
// 업로드한 사용자 정보
uploadedByName: users.name,
-
+
// 평가 세부사항 정보
evaluationDetailId: reviewerEvaluationDetails.id,
reviewerEvaluationId: reviewerEvaluationDetails.reviewerEvaluationId,
-
+
// 평가 기준 정보 (질문 식별용)
criteriaId: regEvalCriteriaDetails.criteriaId,
})
@@ -1149,7 +1250,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
reviewerDetailsRaw.forEach(row => {
if (!reviewerDetailsMap.has(row.reviewerEvaluationId)) {
const reviewerAttachments = attachmentsByReviewerId.get(row.reviewerEvaluationId) || []
-
+
reviewerDetailsMap.set(row.reviewerEvaluationId, {
reviewerEvaluationId: row.reviewerEvaluationId,
reviewerName: row.reviewerName || "",
@@ -1160,11 +1261,11 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
completedAt: row.completedAt,
reviewerComment: row.reviewerComment,
evaluationItems: [],
-
+
// 📎 리뷰어별 첨부파일 통계
totalAttachments: reviewerAttachments.length,
totalAttachmentSize: reviewerAttachments.reduce((sum, att) => sum + att.fileSize, 0),
- questionsWithAttachments: new Set(reviewerAttachments.map(att =>
+ questionsWithAttachments: new Set(reviewerAttachments.map(att =>
attachmentsData.find(a => a.attachmentId === att.id)?.criteriaId
).filter(Boolean)).size,
})
@@ -1174,7 +1275,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
if (row.criteriaId && row.detailId) {
const reviewer = reviewerDetailsMap.get(row.reviewerEvaluationId)!
const itemAttachments = attachmentsByDetailId.get(row.detailId) || []
-
+
reviewer.evaluationItems.push({
criteriaId: row.criteriaId,
category: row.category || "",
@@ -1188,7 +1289,7 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
selectedDetail: row.selectedDetail,
score: row.score ? Number(row.score) : null,
comment: row.comment,
-
+
// 📎 항목별 첨부파일 정보
attachments: itemAttachments,
attachmentCount: itemAttachments.length,
@@ -1214,8 +1315,8 @@ export async function getEvaluationDetails(periodicEvaluationId: number): Promis
} catch (error) {
console.error("Error fetching evaluation details:", error)
throw new Error(
- error instanceof Error
- ? error.message
+ error instanceof Error
+ ? error.message
: "평가 상세 정보 조회 중 오류가 발생했습니다"
)
}