diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-17 10:50:28 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-17 10:50:28 +0000 |
| commit | fb276ed3db86fe4fc0c0fcd870fd3d085b034be0 (patch) | |
| tree | 4a8ab1027d7fd14602a0f837d4e18b04e2169e58 /lib/evaluation/service.ts | |
| parent | 4eb7532f822c821fb6b69bf103bd075fefba769b (diff) | |
(대표님) 벤더데이터 S-EDP 변경사항 대응(seperator), 정기평가 점수오류, dim 준비
Diffstat (limited to 'lib/evaluation/service.ts')
| -rw-r--r-- | lib/evaluation/service.ts | 179 |
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 : "평가 상세 정보 조회 중 오류가 발생했습니다" ) } |
