From 2eb717eb2bbfd97a5f149d13049aa336c26c393b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 29 Oct 2025 07:43:44 +0000 Subject: (최겸) 구매 실사 개발(진행중) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pq/service.ts | 310 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 300 insertions(+), 10 deletions(-) (limited to 'lib/pq/service.ts') diff --git a/lib/pq/service.ts b/lib/pq/service.ts index b6640453..8b1986ce 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -1238,7 +1238,7 @@ export async function requestPqChangesAction({ await db .update(vendorPQSubmissions) .set({ - status: "IN_PROGRESS", // 변경 요청 상태로 설정 + status: "SUBMITTED", // 변경 요청 상태로 설정 updatedAt: new Date(), }) .where( @@ -2210,22 +2210,21 @@ export async function approvePQAction({ projectName = projectData?.name || 'Unknown Project'; } - // 5. PQ 상태 업데이트 + // 5. PQ 상태를 QM_REVIEWING으로 업데이트 (TO-BE: QM 검토 단계 추가) await db .update(vendorPQSubmissions) .set({ - status: "APPROVED", - approvedAt: currentDate, + status: "QM_REVIEWING", updatedAt: currentDate, }) .where(eq(vendorPQSubmissions.id, pqSubmissionId)); - // 6. 일반 PQ인 경우 벤더 상태 업데이트 (선택사항) + // 6. 일반 PQ인 경우 벤더 상태를 IN_PQ로 업데이트 (QM 검토 중) if (pqSubmission.type === "GENERAL") { await db .update(vendors) .set({ - status: "PQ_APPROVED", + status: "IN_PQ", updatedAt: currentDate, }) .where(eq(vendors.id, vendorId)); @@ -2235,21 +2234,21 @@ export async function approvePQAction({ if (vendor.email) { try { const emailSubject = pqSubmission.projectId - ? `[eVCP] Project PQ Approved for ${projectName}` - : "[eVCP] General PQ Approved"; + ? `[eVCP] Project PQ Under QM Review for ${projectName}` + : "[eVCP] General PQ Under QM Review"; const portalUrl = `${host}/partners/pq`; await sendEmail({ to: vendor.email, subject: emailSubject, - template: "pq-approved-vendor", + template: "pq-qm-review-vendor", context: { vendorName: vendor.vendorName, projectId: pqSubmission.projectId, projectName: projectName, isProjectPQ: !!pqSubmission.projectId, - approvedDate: currentDate.toLocaleString(), + reviewDate: currentDate.toLocaleString(), portalUrl, } }); @@ -2277,6 +2276,297 @@ export async function approvePQAction({ } } +// QM 검토 승인 액션 +export async function approveQMReviewAction({ + pqSubmissionId, + vendorId, +}: { + pqSubmissionId: number; + vendorId: number; +}) { + unstable_noStore(); + + try { + const headersList = await headers(); + const host = headersList.get('host') || 'localhost:3000'; + const currentDate = new Date(); + + // 1. PQ 제출 정보 조회 + const pqSubmission = await db + .select({ + id: vendorPQSubmissions.id, + vendorId: vendorPQSubmissions.vendorId, + projectId: vendorPQSubmissions.projectId, + type: vendorPQSubmissions.type, + status: vendorPQSubmissions.status, + }) + .from(vendorPQSubmissions) + .where( + and( + eq(vendorPQSubmissions.id, pqSubmissionId), + eq(vendorPQSubmissions.vendorId, vendorId) + ) + ) + .then(rows => rows[0]); + + if (!pqSubmission) { + return { ok: false, error: "PQ submission not found" }; + } + + // 2. 상태 확인 (QM_REVIEWING 상태만 승인 가능) + if (pqSubmission.status !== "QM_REVIEWING") { + return { + ok: false, + error: `Cannot approve QM review in current status: ${pqSubmission.status}` + }; + } + + // 3. 벤더 정보 조회 + const vendor = await db + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + email: vendors.email, + status: vendors.status, + }) + .from(vendors) + .where(eq(vendors.id, vendorId)) + .then(rows => rows[0]); + + if (!vendor) { + return { ok: false, error: "Vendor not found" }; + } + + // 4. 프로젝트 정보 (프로젝트 PQ인 경우) + let projectName = ''; + if (pqSubmission.projectId) { + const projectData = await db + .select({ + id: projects.id, + name: projects.name, + }) + .from(projects) + .where(eq(projects.id, pqSubmission.projectId)) + .then(rows => rows[0]); + + projectName = projectData?.name || 'Unknown Project'; + } + + // 5. PQ 상태를 QM_APPROVED로 업데이트 + await db + .update(vendorPQSubmissions) + .set({ + status: "QM_APPROVED", + approvedAt: currentDate, + updatedAt: currentDate, + }) + .where(eq(vendorPQSubmissions.id, pqSubmissionId)); + + // 6. 일반 PQ인 경우 벤더 상태를 PQ_APPROVED로 업데이트 + if (pqSubmission.type === "GENERAL") { + await db + .update(vendors) + .set({ + status: "PQ_APPROVED", + updatedAt: currentDate, + }) + .where(eq(vendors.id, vendorId)); + } + + // 7. 실사 요청 생성 (QM 승인 후 실사 프로세스 시작) + await db + .insert(vendorInvestigations) + .values({ + vendorId: vendorId, + pqSubmissionId: pqSubmissionId, + investigationStatus: "PLANNED", + investigationMethod: "DOCUMENT_EVAL", // 기본값, 나중에 변경 가능 + }); + + // 8. 벤더에게 이메일 알림 발송 + if (vendor.email) { + try { + const emailSubject = pqSubmission.projectId + ? `[eVCP] Project PQ Approved for ${projectName}` + : "[eVCP] General PQ Approved"; + + const portalUrl = `${host}/partners/pq`; + + await sendEmail({ + to: vendor.email, + subject: emailSubject, + template: "pq-approved-vendor", + context: { + vendorName: vendor.vendorName, + projectId: pqSubmission.projectId, + projectName: projectName, + isProjectPQ: !!pqSubmission.projectId, + approvedDate: currentDate.toLocaleString(), + portalUrl, + } + }); + } catch (emailError) { + console.error("Failed to send vendor notification:", emailError); + } + } + + // 9. 캐시 무효화 + revalidateTag("vendors"); + revalidateTag("vendor-status-counts"); + revalidateTag("pq-submissions"); + revalidateTag("vendor-pq-submissions"); + revalidateTag("vendor-investigations"); + revalidatePath("/evcp/pq_new"); + + return { ok: true }; + } catch (error) { + console.error("QM review approve error:", error); + return { ok: false, error: getErrorMessage(error) }; + } +} + +// QM 검토 거절 액션 +export async function rejectQMReviewAction({ + pqSubmissionId, + vendorId, + rejectReason +}: { + pqSubmissionId: number; + vendorId: number; + rejectReason: string; +}) { + unstable_noStore(); + + try { + const headersList = await headers(); + const host = headersList.get('host') || 'localhost:3000'; + const currentDate = new Date(); + + // 1. PQ 제출 정보 조회 + const pqSubmission = await db + .select({ + id: vendorPQSubmissions.id, + vendorId: vendorPQSubmissions.vendorId, + projectId: vendorPQSubmissions.projectId, + type: vendorPQSubmissions.type, + status: vendorPQSubmissions.status, + }) + .from(vendorPQSubmissions) + .where( + and( + eq(vendorPQSubmissions.id, pqSubmissionId), + eq(vendorPQSubmissions.vendorId, vendorId) + ) + ) + .then(rows => rows[0]); + + if (!pqSubmission) { + return { ok: false, error: "PQ submission not found" }; + } + + // 2. 상태 확인 (QM_REVIEWING 상태만 거절 가능) + if (pqSubmission.status !== "QM_REVIEWING") { + return { + ok: false, + error: `Cannot reject QM review in current status: ${pqSubmission.status}` + }; + } + + // 3. 벤더 정보 조회 + const vendor = await db + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + email: vendors.email, + status: vendors.status, + }) + .from(vendors) + .where(eq(vendors.id, vendorId)) + .then(rows => rows[0]); + + if (!vendor) { + return { ok: false, error: "Vendor not found" }; + } + + // 4. 프로젝트 정보 (프로젝트 PQ인 경우) + let projectName = ''; + if (pqSubmission.projectId) { + const projectData = await db + .select({ + id: projects.id, + name: projects.name, + }) + .from(projects) + .where(eq(projects.id, pqSubmission.projectId)) + .then(rows => rows[0]); + + projectName = projectData?.name || 'Unknown Project'; + } + + // 5. PQ 상태를 QM_REJECTED로 업데이트 + await db + .update(vendorPQSubmissions) + .set({ + status: "QM_REJECTED", + rejectedAt: currentDate, + rejectReason: rejectReason, + updatedAt: currentDate, + }) + .where(eq(vendorPQSubmissions.id, pqSubmissionId)); + + // 6. 일반 PQ인 경우 벤더 상태를 PQ_FAILED로 업데이트 + if (pqSubmission.type === "GENERAL") { + await db + .update(vendors) + .set({ + status: "PQ_FAILED", + updatedAt: currentDate, + }) + .where(eq(vendors.id, vendorId)); + } + + // 7. 벤더에게 이메일 알림 발송 + if (vendor.email) { + try { + const emailSubject = pqSubmission.projectId + ? `[eVCP] Project PQ Rejected for ${projectName}` + : "[eVCP] General PQ Rejected"; + + const portalUrl = `${host}/partners/pq`; + + await sendEmail({ + to: vendor.email, + subject: emailSubject, + template: "pq-rejected-vendor", + context: { + vendorName: vendor.vendorName, + projectId: pqSubmission.projectId, + projectName: projectName, + isProjectPQ: !!pqSubmission.projectId, + rejectedDate: currentDate.toLocaleString(), + rejectReason: rejectReason, + portalUrl, + } + }); + } catch (emailError) { + console.error("Failed to send vendor notification:", emailError); + } + } + + // 8. 캐시 무효화 + revalidateTag("vendors"); + revalidateTag("vendor-status-counts"); + revalidateTag("pq-submissions"); + revalidateTag("vendor-pq-submissions"); + revalidatePath("/evcp/pq_new"); + + return { ok: true }; + } catch (error) { + console.error("QM review reject error:", error); + return { ok: false, error: getErrorMessage(error) }; + } +} + // PQ 거부 액션 export async function rejectPQAction({ pqSubmissionId, -- cgit v1.2.3