diff options
Diffstat (limited to 'lib/rfq-last')
| -rw-r--r-- | lib/rfq-last/service.ts | 1252 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/participation-dialog.tsx | 2 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/send-rfq-dialog.tsx | 2 |
3 files changed, 760 insertions, 496 deletions
diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 7470428f..9943c02d 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -2829,539 +2829,803 @@ export async function sendRfqToVendors({ fileName: string; }>; }) { + const session = await getServerSession(authOptions); + if (!session?.user) { + throw new Error("인증이 필요합니다."); + } + + const currentUser = session.user; + try { - const session = await getServerSession(authOptions) + // 1. RFQ 기본 정보 조회 (트랜잭션 외부) + const rfqData = await getRfqData(rfqId); + if (!rfqData) { + throw new Error("RFQ 정보를 찾을 수 없습니다."); + } - if (!session?.user) { - throw new Error("인증이 필요합니다.") + // 2. PIC 정보 조회 + const picInfo = await getPicInfo(rfqData.picId, rfqData.picName); + + // 3. 프로젝트 정보 조회 + const projectInfo = rfqData.projectId + ? await getProjectInfo(rfqData.projectId) + : null; + + // 4. 첨부파일 준비 + const emailAttachments = await prepareEmailAttachments(rfqId, attachmentIds); + const designAttachments = await getDesignAttachments(rfqId); + + // 5. 벤더별 처리 + const { results, errors, savedContracts, tbeSessionsCreated } = + await processVendors({ + rfqId, + rfqData, + vendors, + currentUser, + picInfo, + emailAttachments, + designAttachments, + generatedPdfs + }); + + // 6. RFQ 상태 업데이트 + if (results.length > 0) { + await updateRfqStatus(rfqId, currentUser.id); } - const currentUser = session.user + return { + success: true, + results, + errors, + savedContracts, + tbeSessionsCreated, + totalSent: results.length, + totalFailed: errors.length, + totalContracts: savedContracts.length, + totalTbeSessions: tbeSessionsCreated.length + }; - // 트랜잭션 시작 - const result = await db.transaction(async (tx) => { - // 1. RFQ 정보 조회 - const [rfqData] = await tx - .select({ - id: rfqsLast.id, - rfqCode: rfqsLast.rfqCode, - rfqType: rfqsLast.rfqType, - rfqTitle: rfqsLast.rfqTitle, - projectId: rfqsLast.projectId, - itemCode: rfqsLast.itemCode, - itemName: rfqsLast.itemName, - dueDate: rfqsLast.dueDate, - packageNo: rfqsLast.packageNo, - packageName: rfqsLast.packageName, - picId: rfqsLast.pic, - picCode: rfqsLast.picCode, - picName: rfqsLast.picName, - projectCompany: rfqsLast.projectCompany, - projectFlag: rfqsLast.projectFlag, - projectSite: rfqsLast.projectSite, - smCode: rfqsLast.smCode, - prNumber: rfqsLast.prNumber, - prIssueDate: rfqsLast.prIssueDate, - series: rfqsLast.series, - EngPicName: rfqsLast.EngPicName, - }) - .from(rfqsLast) - .where(eq(rfqsLast.id, rfqId)); + } catch (error) { + console.error("RFQ 발송 실패:", error); + throw new Error( + error instanceof Error + ? error.message + : "RFQ 발송 중 오류가 발생했습니다." + ); + } +} - if (!rfqData) { - throw new Error("RFQ 정보를 찾을 수 없습니다."); - } +// ============= Helper Functions ============= + +async function getRfqData(rfqId: number) { + const [rfqData] = await db + .select({ + id: rfqsLast.id, + rfqCode: rfqsLast.rfqCode, + rfqType: rfqsLast.rfqType, + rfqTitle: rfqsLast.rfqTitle, + projectId: rfqsLast.projectId, + itemCode: rfqsLast.itemCode, + itemName: rfqsLast.itemName, + dueDate: rfqsLast.dueDate, + packageNo: rfqsLast.packageNo, + packageName: rfqsLast.packageName, + picId: rfqsLast.pic, + picCode: rfqsLast.picCode, + picName: rfqsLast.picName, + projectCompany: rfqsLast.projectCompany, + projectFlag: rfqsLast.projectFlag, + projectSite: rfqsLast.projectSite, + smCode: rfqsLast.smCode, + prNumber: rfqsLast.prNumber, + prIssueDate: rfqsLast.prIssueDate, + series: rfqsLast.series, + EngPicName: rfqsLast.EngPicName, + }) + .from(rfqsLast) + .where(eq(rfqsLast.id, rfqId)); + + return rfqData; +} - // 2. PIC 사용자 정보 조회 - let picEmail = process.env.Email_From_Address; - let picName = rfqData.picName || "구매담당자"; +async function getPicInfo(picId: number | null, picName: string | null) { + let picEmail = process.env.Email_From_Address; + let finalPicName = picName || "구매담당자"; - if (rfqData.picId) { - const [picUser] = await tx - .select() - .from(users) - .where(eq(users.id, rfqData.picId)); + if (picId) { + const [picUser] = await db + .select() + .from(users) + .where(eq(users.id, picId)); - if (picUser?.email) { - picEmail = picUser.email; - picName = picUser.name || picName; - } - } + if (picUser?.email) { + picEmail = picUser.email; + finalPicName = picUser.name || finalPicName; + } + } - // 3. 프로젝트 정보 조회 - let projectInfo = null; - if (rfqData.projectId) { - const [project] = await tx - .select() - .from(projects) - .where(eq(projects.id, rfqData.projectId)); - projectInfo = project; - } + return { picEmail, picName: finalPicName }; +} - // 4. PR 아이템 정보 조회 - const prItems = await tx - .select() - .from(rfqPrItems) - .where(eq(rfqPrItems.rfqsLastId, rfqId)); +async function getProjectInfo(projectId: number) { + const [project] = await db + .select() + .from(projects) + .where(eq(projects.id, projectId)); + return project; +} - // 5. 첨부파일 정보 조회 및 준비 - const attachments = await tx - .select({ - attachment: rfqLastAttachments, - revision: rfqLastAttachmentRevisions - }) - .from(rfqLastAttachments) - .leftJoin( - rfqLastAttachmentRevisions, - and( - eq(rfqLastAttachmentRevisions.attachmentId, rfqLastAttachments.id), - eq(rfqLastAttachmentRevisions.isLatest, true) - ) - ) - .where( - and( - eq(rfqLastAttachments.rfqId, rfqId), - attachmentIds.length > 0 - ? sql`${rfqLastAttachments.id} IN (${sql.join(attachmentIds, sql`, `)})` - : sql`1=1` - ) +async function prepareEmailAttachments(rfqId: number, attachmentIds: number[]) { + const attachments = await db + .select({ + attachment: rfqLastAttachments, + revision: rfqLastAttachmentRevisions + }) + .from(rfqLastAttachments) + .leftJoin( + rfqLastAttachmentRevisions, + and( + eq(rfqLastAttachmentRevisions.attachmentId, rfqLastAttachments.id), + eq(rfqLastAttachmentRevisions.isLatest, true) + ) + ) + .where( + and( + eq(rfqLastAttachments.rfqId, rfqId), + attachmentIds.length > 0 + ? sql`${rfqLastAttachments.id} IN (${sql.join(attachmentIds, sql`, `)})` + : sql`1=1` + ) + ); + + const emailAttachments = []; + + for (const { attachment, revision } of attachments) { + if (revision?.filePath) { + try { + const fullPath = path.join( + process.cwd(), + `${process.env.NAS_PATH}`, + revision.filePath ); + const fileBuffer = await fs.readFile(fullPath); + + emailAttachments.push({ + filename: revision.originalFileName, + content: fileBuffer, + contentType: revision.fileType || 'application/octet-stream' + }); + } catch (error) { + console.error(`첨부파일 읽기 실패: ${revision.filePath}`, error); + } + } + } - // 6. 이메일 첨부파일 준비 - const emailAttachments = []; - for (const { attachment, revision } of attachments) { - if (revision?.filePath) { - try { - const fullPath = path.join(process.cwd(), `${process.env.NAS_PATH}`, revision.filePath); - const fileBuffer = await fs.readFile(fullPath); - emailAttachments.push({ - filename: revision.originalFileName, - content: fileBuffer, - contentType: revision.fileType || 'application/octet-stream' - }); - } catch (error) { - console.error(`첨부파일 읽기 실패: ${revision.filePath}`, error); - } - } + return emailAttachments; +} + +async function getDesignAttachments(rfqId: number) { + return await db + .select({ + attachment: rfqLastAttachments, + revision: rfqLastAttachmentRevisions + }) + .from(rfqLastAttachments) + .leftJoin( + rfqLastAttachmentRevisions, + and( + eq(rfqLastAttachmentRevisions.attachmentId, rfqLastAttachments.id), + eq(rfqLastAttachmentRevisions.isLatest, true) + ) + ) + .where( + and( + eq(rfqLastAttachments.rfqId, rfqId), + eq(rfqLastAttachments.attachmentType, "설계") + ) + ); +} + +async function processVendors({ + rfqId, + rfqData, + vendors, + currentUser, + picInfo, + emailAttachments, + designAttachments, + generatedPdfs +}: { + rfqId: number; + rfqData: any; + vendors: any[]; + currentUser: any; + picInfo: any; + emailAttachments: any[]; + designAttachments: any[]; + generatedPdfs?: any[]; +}) { + const results = []; + const errors = []; + const savedContracts = []; + const tbeSessionsCreated = []; + + // PDF 저장 디렉토리 준비 + const contractsDir = path.join( + process.cwd(), + `${process.env.NAS_PATH}`, + "contracts", + "generated" + ); + await mkdir(contractsDir, { recursive: true }); + + // 각 벤더를 독립적으로 처리 + for (const vendor of vendors) { + try { + const vendorResult = await db.transaction(async (tx) => { + return await processSingleVendor({ + tx, + vendor, + rfqId, + rfqData, + currentUser, + picInfo, + contractsDir, + generatedPdfs, + designAttachments + }); + }); + + results.push(vendorResult.result); + + if (vendorResult.contracts) { + savedContracts.push(...vendorResult.contracts); + } + + if (vendorResult.tbeSession) { + tbeSessionsCreated.push(vendorResult.tbeSession); } - // ========== TBE용 설계 문서 조회 (중요!) ========== - const designAttachments = await tx - .select({ - attachment: rfqLastAttachments, - revision: rfqLastAttachmentRevisions - }) - .from(rfqLastAttachments) - .leftJoin( - rfqLastAttachmentRevisions, - and( - eq(rfqLastAttachmentRevisions.attachmentId, rfqLastAttachments.id), - eq(rfqLastAttachmentRevisions.isLatest, true) - ) + } catch (error) { + console.error(`벤더 ${vendor.vendorName} 처리 실패:`, error); + + errors.push({ + vendorId: vendor.vendorId, + vendorName: vendor.vendorName, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + + // 에러 발생 시 이메일 상태 업데이트 + await updateEmailStatusFailed(rfqId, vendor.vendorId); + } + } + + return { results, errors, savedContracts, tbeSessionsCreated }; +} + +async function processSingleVendor({ + tx, + vendor, + rfqId, + rfqData, + currentUser, + picInfo, + contractsDir, + generatedPdfs, + designAttachments +}: any) { + const isResend = vendor.isResend || false; + const sendVersion = (vendor.sendVersion || 0) + 1; + + // 이메일 수신자 정보 준비 + const emailRecipients = prepareEmailRecipients(vendor, picInfo.picEmail); + + // RFQ Detail 처리 + const newRfqDetail = await handleRfqDetail({ + tx, + rfqId, + vendor, + currentUser, + emailRecipients, + isResend, + sendVersion + }); + + // PDF 계약 처리 + const contracts = await handleContracts({ + tx, + vendor, + generatedPdfs, + contractsDir, + newRfqDetail, + currentUser + }); + + // Vendor Response 처리 + const vendorResponse = await handleVendorResponse({ + tx, + rfqId, + vendor, + newRfqDetail, + currentUser + }); + + // TBE 세션 처리 + const tbeSession = await handleTbeSession({ + tx, + rfqId, + rfqData, + vendor, + newRfqDetail, + currentUser, + designAttachments + }); + + return { + result: { + vendorId: vendor.vendorId, + vendorName: vendor.vendorName, + success: true, + responseId: vendorResponse.id, + isResend, + sendVersion, + tbeSessionCreated: tbeSession + }, + contracts, + tbeSession + }; +} + +function prepareEmailRecipients(vendor: any, picEmail: string) { + const toEmails = [vendor.selectedMainEmail]; + const ccEmails = [...vendor.additionalEmails]; + + vendor.customEmails?.forEach((custom: any) => { + if (custom.email !== vendor.selectedMainEmail && + !vendor.additionalEmails.includes(custom.email)) { + ccEmails.push(custom.email); + } + }); + + return { + to: toEmails, + cc: ccEmails, + sentBy: picEmail + }; +} + +async function handleRfqDetail({ + tx, + rfqId, + vendor, + currentUser, + emailRecipients, + isResend, + sendVersion +}: any) { + // 기존 detail 조회 + const [rfqDetail] = await tx + .select() + .from(rfqLastDetails) + .where( + and( + eq(rfqLastDetails.rfqsLastId, rfqId), + eq(rfqLastDetails.vendorsId, vendor.vendorId), + eq(rfqLastDetails.isLatest, true) + ) + ); + + if (!rfqDetail) { + throw new Error("해당 RFQ에는 벤더가 이미 할당되어있는 상태이어야합니다."); + } + + // 기존 detail을 isLatest=false로 업데이트 + await tx + .update(rfqLastDetails) + .set({ + isLatest: false, + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastDetails.rfqsLastId, rfqId), + eq(rfqLastDetails.vendorsId, vendor.vendorId), + eq(rfqLastDetails.isLatest, true) + ) + ); + + // 새 detail 생성 + const { + id, + updatedBy, + updatedAt, + isLatest, + sendVersion: oldSendVersion, + emailResentCount, + ...restRfqDetail + } = rfqDetail; + + const [newRfqDetail] = await tx + .insert(rfqLastDetails) + .values({ + ...restRfqDetail, + updatedBy: Number(currentUser.id), + updatedAt: new Date(), + isLatest: true, + emailSentAt: new Date(), + emailSentTo: JSON.stringify(emailRecipients), + emailResentCount: isResend ? (emailResentCount || 0) + 1 : 1, + sendVersion: sendVersion, + lastEmailSentAt: new Date(), + emailStatus: "sent", + agreementYn: vendor.contractRequirements?.agreementYn || false, + ndaYn: vendor.contractRequirements?.ndaYn || false, + projectGtcYn: vendor.contractRequirements?.projectGtcYn || false, + generalGtcYn: vendor.contractRequirements?.generalGtcYn || false, + }) + .returning(); + + await tx + .update(basicContract) + .set({ + rfqCompanyId: newRfqDetail.id, + }) + .where( + and( + eq(basicContract.rfqCompanyId, rfqDetail.id), + eq(rfqLastDetails.vendorsId, vendor.vendorId), + ) + ); + + return newRfqDetail; +} + +async function handleContracts({ + tx, + vendor, + generatedPdfs, + contractsDir, + newRfqDetail, + currentUser +}: any) { + if (!generatedPdfs || !vendor.contractRequirements) { + return []; + } + + const savedContracts = []; + const vendorPdfs = generatedPdfs.filter((pdf: any) => + pdf.key.startsWith(`${vendor.vendorId}_`) + ); + + for (const pdfData of vendorPdfs) { + // PDF 파일 저장 + const pdfBuffer = Buffer.from(pdfData.buffer); + const fileName = pdfData.fileName; + const filePath = path.join(contractsDir, fileName); + await writeFile(filePath, pdfBuffer); + + const templateName = pdfData.key.split('_')[2]; + + // 템플릿 조회 + const [template] = await db + .select() + .from(basicContractTemplates) + .where( + and( + ilike(basicContractTemplates.templateName, `%${templateName}%`), + eq(basicContractTemplates.status, "ACTIVE") ) - .where( - and( - eq(rfqLastAttachments.rfqId, rfqId), - eq(rfqLastAttachments.attachmentType, "설계") // 설계 문서만 필터링 - ) - ); + ) + .limit(1); + if (!template) { + console.error(`템플릿을 찾을 수 없음: ${templateName}`); + continue; + } - // 7. 각 벤더별 처리 - const results = []; - const errors = []; - const savedContracts = []; - const tbeSessionsCreated = []; + // 계약 생성 또는 업데이트 + const contractRecord = await createOrUpdateContract({ + tx, + template, + vendor, + newRfqDetail, + fileName, + currentUser + }); - const contractsDir = path.join(process.cwd(), `${process.env.NAS_PATH}`, "contracts", "generated"); - await mkdir(contractsDir, { recursive: true }); + savedContracts.push({ + vendorId: vendor.vendorId, + vendorName: vendor.vendorName, + templateName: templateName, + contractId: contractRecord.id, + fileName: fileName, + isUpdated: contractRecord.isUpdated + }); + } + return savedContracts; +} - for (const vendor of vendors) { - // 재발송 여부 확인 - const isResend = vendor.isResend || false; - const sendVersion = (vendor.sendVersion || 0) + 1; +async function createOrUpdateContract({ + tx, + template, + vendor, + newRfqDetail, + fileName, + currentUser +}: any) { + // 기존 계약 확인 + const [existingContract] = await tx + .select() + .from(basicContract) + .where( + and( + eq(basicContract.templateId, template.id), + eq(basicContract.vendorId, vendor.vendorId), + eq(basicContract.rfqCompanyId, newRfqDetail.id) + ) + ) + .limit(1); - // 7.4 이메일 수신자 정보 준비 - const toEmails = [vendor.selectedMainEmail]; - const ccEmails = [...vendor.additionalEmails]; + if (existingContract) { + // 업데이트 + const [updated] = await tx + .update(basicContract) + .set({ + requestedBy: Number(currentUser.id), + status: "PENDING", + fileName: fileName, + // rfqCompanyId: newRfqDetail.id, + filePath: `/contracts/generated/${fileName}`, + deadline: addDays(new Date(), 10), + updatedAt: new Date() + }) + .where(eq(basicContract.id, existingContract.id)) + .returning(); + + return { ...updated, isUpdated: true }; + } else { + // 새로 생성 + const [created] = await tx + .insert(basicContract) + .values({ + templateId: template.id, + vendorId: vendor.vendorId, + rfqCompanyId: newRfqDetail.id, + requestedBy: Number(currentUser.id), + status: "PENDING", + fileName: fileName, + filePath: `/contracts/generated/${fileName}`, + deadline: addDays(new Date(), 10), + createdAt: new Date(), + updatedAt: new Date() + }) + .returning(); + + return { ...created, isUpdated: false }; + } +} - vendor.customEmails?.forEach(custom => { - if (custom.email !== vendor.selectedMainEmail && - !vendor.additionalEmails.includes(custom.email)) { - ccEmails.push(custom.email); - } - }); +async function handleVendorResponse({ + tx, + rfqId, + vendor, + newRfqDetail, + currentUser +}: any) { + // 기존 응답 확인 + const existingResponses = await tx + .select() + .from(rfqLastVendorResponses) + .where( + and( + eq(rfqLastVendorResponses.rfqsLastId, rfqId), + eq(rfqLastVendorResponses.vendorId, vendor.vendorId) + ) + ); - // 이메일 수신자 정보를 JSON으로 저장 - const emailRecipients = { - to: toEmails, - cc: ccEmails, - sentBy: picEmail - }; + // 기존 응답을 isLatest=false로 업데이트 + if (existingResponses.length > 0) { + await tx + .update(rfqLastVendorResponses) + .set({ isLatest: false }) + .where( + and( + eq(rfqLastVendorResponses.vendorId, vendor.vendorId), + eq(rfqLastVendorResponses.rfqsLastId, rfqId) + ) + ); + } - try { - // 7.1 rfqLastDetails 조회 또는 생성 - let [rfqDetail] = await tx - .select() - .from(rfqLastDetails) - .where( - and( - eq(rfqLastDetails.rfqsLastId, rfqId), - eq(rfqLastDetails.vendorsId, vendor.vendorId), - eq(rfqLastDetails.isLatest, true), - ) - ); - - if (!rfqDetail) { - throw new Error("해당 RFQ에는 벤더가 이미 할당되어있는 상태이어야합니다."); - } + // 새 응답 생성 + const newResponseVersion = existingResponses.length > 0 + ? Math.max(...existingResponses.map(r => r.responseVersion)) + 1 + : 1; + + const [vendorResponse] = await tx + .insert(rfqLastVendorResponses) + .values({ + rfqsLastId: rfqId, + rfqLastDetailsId: newRfqDetail.id, + vendorId: vendor.vendorId, + responseVersion: newResponseVersion, + isLatest: true, + status: "초대됨", + currency: newRfqDetail.currency || "USD", + createdBy: currentUser.id, + updatedBy: currentUser.id, + createdAt: new Date(), + updatedAt: new Date() + }) + .returning(); + + return vendorResponse; +} - // 기존 rfqDetail을 isLatest=false로 업데이트 - const updateResult = await tx - .update(rfqLastDetails) - .set({ - isLatest: false, - updatedAt: new Date() // 업데이트 시간도 기록 - }) - .where( - and( - eq(rfqLastDetails.rfqsLastId, rfqId), - eq(rfqLastDetails.vendorsId, vendor.vendorId), - eq(rfqLastDetails.isLatest, true) - ) - ) - .returning({ id: rfqLastDetails.id }); - - console.log(`Updated ${updateResult.length} records to isLatest=false for vendor ${vendor.vendorId}`); - - const { id, updatedBy, updatedAt, isLatest, sendVersion: oldSendVersion, emailResentCount, ...restRfqDetail } = rfqDetail; - - - let [newRfqDetail] = await tx - .insert(rfqLastDetails) - .values({ - ...restRfqDetail, // 기존 값들 복사 - - // 업데이트할 필드들 - updatedBy: Number(currentUser.id), - updatedAt: new Date(), - isLatest: true, - - // 이메일 관련 필드 업데이트 - emailSentAt: new Date(), - emailSentTo: JSON.stringify(emailRecipients), - emailResentCount: isResend ? (emailResentCount || 0) + 1 : 1, - sendVersion: sendVersion, - lastEmailSentAt: new Date(), - emailStatus: "sent", - - agreementYn: vendor.contractRequirements.agreementYn || false, - ndaYn: vendor.contractRequirements.ndaYn || false, - projectGtcYn: vendor.contractRequirements.projectGtcYn || false, - generalGtcYn: vendor.contractRequirements.generalGtcYn || false, - - - }) - .returning(); - - - // 생성된 PDF 저장 및 DB 기록 - if (generatedPdfs && vendor.contractRequirements) { - const vendorPdfs = generatedPdfs.filter(pdf => - pdf.key.startsWith(`${vendor.vendorId}_`) - ); - - for (const pdfData of vendorPdfs) { - console.log(vendor.vendorId, pdfData.buffer.length) - // PDF 파일 저장 - const pdfBuffer = Buffer.from(pdfData.buffer); - const fileName = pdfData.fileName; - const filePath = path.join(contractsDir, fileName); - - await writeFile(filePath, pdfBuffer); - - const templateName = pdfData.key.split('_')[2]; - - const [template] = await db - .select() - .from(basicContractTemplates) - .where( - and( - ilike(basicContractTemplates.templateName, `%${templateName}%`), - eq(basicContractTemplates.status, "ACTIVE") - ) - ) - .limit(1); - - console.log("템플릿", templateName, template) - - // 기존 계약이 있는지 확인 - const [existingContract] = await tx - .select() - .from(basicContract) - .where( - and( - eq(basicContract.templateId, template.id), - eq(basicContract.vendorId, vendor.vendorId), - eq(basicContract.rfqCompanyId, newRfqDetail.id) - ) - ) - .limit(1); - - let contractRecord; - - if (existingContract) { - // 기존 계약이 있으면 업데이트 - [contractRecord] = await tx - .update(basicContract) - .set({ - requestedBy: Number(currentUser.id), - status: "PENDING", // 재발송 상태 - fileName: fileName, - filePath: `/contracts/generated/${fileName}`, - deadline: addDays(new Date(), 10), - updatedAt: new Date(), - // version을 증가시키거나 이력 관리가 필요하면 추가 - }) - .where(eq(basicContract.id, existingContract.id)) - .returning(); - - console.log("기존 계약 업데이트:", contractRecord.id) - } else { - // 새 계약 생성 - [contractRecord] = await tx - .insert(basicContract) - .values({ - templateId: template.id, - vendorId: vendor.vendorId, - rfqCompanyId: newRfqDetail.id, - requestedBy: Number(currentUser.id), - status: "PENDING", - fileName: fileName, - filePath: `/contracts/generated/${fileName}`, - deadline: addDays(new Date(), 10), - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning(); - - console.log("새 계약 생성:", contractRecord.id) - } - - console.log(contractRecord.vendorId, contractRecord.filePath) - - savedContracts.push({ - vendorId: vendor.vendorId, - vendorName: vendor.vendorName, - templateName: templateName, - contractId: contractRecord.id, - fileName: fileName, - isUpdated: !!existingContract // 업데이트 여부 표시 - }); - } - } +async function handleTbeSession({ + tx, + rfqId, + rfqData, + vendor, + newRfqDetail, + currentUser, + designAttachments +}: any) { + // 기존 활성 TBE 세션 확인 + const [existingActiveTbe] = await tx + .select() + .from(rfqLastTbeSessions) + .where( + and( + eq(rfqLastTbeSessions.rfqsLastId, rfqId), + eq(rfqLastTbeSessions.vendorId, vendor.vendorId), + sql`${rfqLastTbeSessions.status} IN ('준비중', '진행중', '검토중', '보류')` + ) + ); - // 7.3 기존 응답 레코드 확인 - const existingResponses = await tx - .select() - .from(rfqLastVendorResponses) - .where( - and( - eq(rfqLastVendorResponses.rfqsLastId, rfqId), - eq(rfqLastVendorResponses.vendorId, vendor.vendorId) - ) - ); - - // 7.4 기존 응답이 있으면 isLatest=false로 업데이트 - if (existingResponses.length > 0) { - await tx - .update(rfqLastVendorResponses) - .set({ - isLatest: false - }) - .where( - and( - eq(rfqLastVendorResponses.vendorId, vendor.vendorId), - eq(rfqLastVendorResponses.rfqsLastId, rfqId), - ) - ) - } + if (existingActiveTbe) { + console.log(`TBE 세션이 이미 존재함: vendor ${vendor.vendorName}`); + return null; + } - // 7.5 새로운 응답 레코드 생성 - const newResponseVersion = existingResponses.length > 0 - ? Math.max(...existingResponses.map(r => r.responseVersion)) + 1 - : 1; - - const [vendorResponse] = await tx - .insert(rfqLastVendorResponses) - .values({ - rfqsLastId: rfqId, - rfqLastDetailsId: newRfqDetail.id, - vendorId: vendor.vendorId, - responseVersion: newResponseVersion, - isLatest: true, - status: "초대됨", - currency: rfqDetail.currency || "USD", - - // 감사 필드 - createdBy: currentUser.id, - updatedBy: currentUser.id, - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning(); - - - // ========== TBE 세션 및 문서 검토 레코드 생성 시작 ========== - // 7.4 기존 활성 TBE 세션 확인 - const [existingActiveTbe] = await tx - .select() - .from(rfqLastTbeSessions) - .where( - and( - eq(rfqLastTbeSessions.rfqsLastId, rfqId), - eq(rfqLastTbeSessions.vendorId, vendor.vendorId), - sql`${rfqLastTbeSessions.status} IN ('준비중', '진행중', '검토중', '보류')` - ) - ); - - // 7.5 활성 TBE 세션이 없는 경우에만 새로 생성 - if (!existingActiveTbe) { - // TBE 세션 코드 생성 - const year = new Date().getFullYear(); - const [lastTbeSession] = await tx - .select({ sessionCode: rfqLastTbeSessions.sessionCode }) - .from(rfqLastTbeSessions) - .where(sql`${rfqLastTbeSessions.sessionCode} LIKE 'TBE-${year}-%'`) - .orderBy(sql`${rfqLastTbeSessions.sessionCode} DESC`) - .limit(1); - - let sessionNumber = 1; - if (lastTbeSession?.sessionCode) { - const lastNumber = parseInt(lastTbeSession.sessionCode.split('-')[2]); - sessionNumber = isNaN(lastNumber) ? 1 : lastNumber + 1; - } - - const sessionCode = `TBE-${year}-${String(sessionNumber).padStart(3, '0')}`; - - // TBE 세션 생성 - const [tbeSession] = await tx - .insert(rfqLastTbeSessions) - .values({ - rfqsLastId: rfqId, - rfqLastDetailsId: newRfqDetail.id, - vendorId: vendor.vendorId, - sessionCode: sessionCode, - sessionTitle: `${rfqData.rfqCode} - ${vendor.vendorName} 기술검토`, - sessionType: "initial", - status: "준비중", - evaluationResult: null, - plannedStartDate: rfqData.dueDate ? addDays(new Date(rfqData.dueDate), 1) : addDays(new Date(), 14), - plannedEndDate: rfqData.dueDate ? addDays(new Date(rfqData.dueDate), 7) : addDays(new Date(), 21), - leadEvaluatorId: rfqData.picId, - createdBy: Number(currentUser.id), - updatedBy: Number(currentUser.id), - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning(); - - console.log(`TBE 세션 생성됨: ${sessionCode} for vendor ${vendor.vendorName}`); - - // ========== 설계 문서에 대한 검토 레코드 생성 (중요!) ========== - const documentReviewsCreated = []; - - for (const { attachment, revision } of designAttachments) { - const [documentReview] = await tx - .insert(rfqLastTbeDocumentReviews) - .values({ - tbeSessionId: tbeSession.id, - documentSource: "buyer", - buyerAttachmentId: attachment.id, - buyerAttachmentRevisionId: revision?.id || null, - vendorAttachmentId: null, // 구매자 문서이므로 null - documentType: attachment.attachmentType, - documentName: revision?.originalFileName || attachment.serialNo, - reviewStatus: "미검토", - technicalCompliance: null, - qualityAcceptable: null, - requiresRevision: false, - reviewComments: null, - revisionRequirements: null, - hasPdftronComments: false, - pdftronDocumentId: null, - pdftronAnnotationCount: 0, - reviewedBy: null, - reviewedAt: null, - additionalReviewers: null, - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning(); - - documentReviewsCreated.push({ - reviewId: documentReview.id, - attachmentId: attachment.id, - documentName: documentReview.documentName - }); - - console.log(`문서 검토 레코드 생성: ${documentReview.documentName}`); - } - - tbeSessionsCreated.push({ - vendorId: vendor.vendorId, - vendorName: vendor.vendorName, - sessionId: tbeSession.id, - sessionCode: tbeSession.sessionCode, - documentReviewsCount: documentReviewsCreated.length - }); - - console.log(`TBE 세션 ${sessionCode}: 총 ${documentReviewsCreated.length}개 문서 검토 레코드 생성`); - } else { - console.log(`TBE 세션이 이미 존재함: vendor ${vendor.vendorName}`); - } - // ========== TBE 세션 및 문서 검토 레코드 생성 끝 ========== + // TBE 세션 코드 생성 + const sessionCode = await generateTbeSessionCode(tx); + + // TBE 세션 생성 + const [tbeSession] = await tx + .insert(rfqLastTbeSessions) + .values({ + rfqsLastId: rfqId, + rfqLastDetailsId: newRfqDetail.id, + vendorId: vendor.vendorId, + sessionCode: sessionCode, + sessionTitle: `${rfqData.rfqCode} - ${vendor.vendorName} 기술검토`, + sessionType: "initial", + status: "준비중", + evaluationResult: null, + plannedStartDate: rfqData.dueDate + ? addDays(new Date(rfqData.dueDate), 1) + : addDays(new Date(), 14), + plannedEndDate: rfqData.dueDate + ? addDays(new Date(rfqData.dueDate), 7) + : addDays(new Date(), 21), + leadEvaluatorId: rfqData.picId, + createdBy: Number(currentUser.id), + updatedBy: Number(currentUser.id), + createdAt: new Date(), + updatedAt: new Date() + }) + .returning(); + + // 문서 검토 레코드 생성 + const documentReviewsCount = await createDocumentReviews({ + tx, + tbeSession, + designAttachments + }); - } + return { + vendorId: vendor.vendorId, + vendorName: vendor.vendorName, + sessionId: tbeSession.id, + sessionCode: tbeSession.sessionCode, + documentReviewsCount + }; +} - // 8. RFQ 상태 업데이트 - if (results.length > 0) { - await tx - .update(rfqsLast) - .set({ - status: "RFQ 발송", - rfqSendDate: new Date(), - sentBy: Number(currentUser.id), - updatedBy: Number(currentUser.id), - updatedAt: new Date(), - }) - .where(eq(rfqsLast.id, rfqId)); - } +async function generateTbeSessionCode(tx: any) { + const year = new Date().getFullYear(); + const pattern = `TBE-${year}-%`; + + const [lastTbeSession] = await tx + .select({ sessionCode: rfqLastTbeSessions.sessionCode }) + .from(rfqLastTbeSessions) + .where(like(rfqLastTbeSessions.sessionCode,pattern )) + .orderBy(sql`${rfqLastTbeSessions.sessionCode} DESC`) + .limit(1); - return { - success: true, - results, - errors, - savedContracts, - totalSent: results.length, - totalFailed: errors.length, - totalContracts: savedContracts.length - }; + let sessionNumber = 1; + if (lastTbeSession?.sessionCode) { + const lastNumber = parseInt(lastTbeSession.sessionCode.split('-')[2]); + sessionNumber = isNaN(lastNumber) ? 1 : lastNumber + 1; + } + + return `TBE-${year}-${String(sessionNumber).padStart(3, '0')}`; +} + +async function createDocumentReviews({ + tx, + tbeSession, + designAttachments +}: any) { + let documentReviewsCount = 0; + + for (const { attachment, revision } of designAttachments) { + await tx + .insert(rfqLastTbeDocumentReviews) + .values({ + tbeSessionId: tbeSession.id, + documentSource: "buyer", + buyerAttachmentId: attachment.id, + buyerAttachmentRevisionId: revision?.id || null, + vendorAttachmentId: null, + documentType: attachment.attachmentType, + documentName: revision?.originalFileName || attachment.serialNo, + reviewStatus: "미검토", + technicalCompliance: null, + qualityAcceptable: null, + requiresRevision: false, + reviewComments: null, + revisionRequirements: null, + hasPdftronComments: false, + pdftronDocumentId: null, + pdftronAnnotationCount: 0, + reviewedBy: null, + reviewedAt: null, + additionalReviewers: null, + createdAt: new Date(), + updatedAt: new Date() }); - return result; + documentReviewsCount++; + } + + return documentReviewsCount; +} +async function updateEmailStatusFailed(rfqId: number, vendorId: number) { + try { + await db + .update(rfqLastDetails) + .set({ + emailStatus: "failed", + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastDetails.rfqsLastId, rfqId), + eq(rfqLastDetails.vendorsId, vendorId), + eq(rfqLastDetails.isLatest, true) + ) + ); } catch (error) { - console.error("RFQ 발송 실패:", error); - throw new Error( - error instanceof Error - ? error.message - : "RFQ 발송 중 오류가 발생했습니다." - ); + console.error("이메일 상태 업데이트 실패:", error); } } +async function updateRfqStatus(rfqId: number, userId: number) { + await db + .update(rfqsLast) + .set({ + status: "RFQ 발송", + rfqSendDate: new Date(), + sentBy: Number(userId), + updatedBy: Number(userId), + updatedAt: new Date() + }) + .where(eq(rfqsLast.id, rfqId)); + } + export async function updateRfqDueDate( rfqId: number, newDueDate: Date | string, diff --git a/lib/rfq-last/vendor-response/participation-dialog.tsx b/lib/rfq-last/vendor-response/participation-dialog.tsx index a7337ac2..3923872e 100644 --- a/lib/rfq-last/vendor-response/participation-dialog.tsx +++ b/lib/rfq-last/vendor-response/participation-dialog.tsx @@ -73,7 +73,7 @@ export function ParticipationDialog({ title: "참여 확정", description: result.message, }) - // router.push(`/partners/rfq-last/${rfqId}`) + router.push(`/partners/rfq-last/${rfqId}`) router.refresh() } else { toast({ diff --git a/lib/rfq-last/vendor/send-rfq-dialog.tsx b/lib/rfq-last/vendor/send-rfq-dialog.tsx index 619ea749..ce97dcde 100644 --- a/lib/rfq-last/vendor/send-rfq-dialog.tsx +++ b/lib/rfq-last/vendor/send-rfq-dialog.tsx @@ -1422,7 +1422,7 @@ export function SendRfqDialog({ [vendor.vendorId]: !checked })); }} - className="data-[state=checked]:bg-orange-600 data-[state=checked]:border-orange-600" + // className="data-[state=checked]:bg-orange-600 data-[state=checked]:border-orange-600" /> <span className="text-xs text-yellow-800"> {skipContractsForVendor[vendor.vendorId] ? "계약서 유지" : "계약서 재생성"} |
