diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 10:13:11 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 10:13:11 +0000 |
| commit | dd831478a3ab5ac7182903d41aa4b3e47f28224f (patch) | |
| tree | 2f5ee3f818d6ff6799ebc1f35f3b40b7e6611a2a /lib/bidding/pre-quote/service.ts | |
| parent | d5f26d34c4ac6f3eaac16fbc6069de2c2341a6ff (diff) | |
(최겸) 구매 입찰 테스트 및 수정사항 반영
Diffstat (limited to 'lib/bidding/pre-quote/service.ts')
| -rw-r--r-- | lib/bidding/pre-quote/service.ts | 383 |
1 files changed, 374 insertions, 9 deletions
diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index 7a5db949..680a8ff5 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -2,13 +2,16 @@ import db from '@/db/db' import { biddingCompanies, companyConditionResponses, biddings, prItemsForBidding, biddingDocuments, companyPrItemBids, priceAdjustmentForms } from '@/db/schema/bidding' +import { basicContractTemplates } from '@/db/schema' import { vendors } from '@/db/schema/vendors' import { users } from '@/db/schema' import { sendEmail } from '@/lib/mail/sendEmail' -import { eq, inArray, and } from 'drizzle-orm' -import { saveFile } from '@/lib/file-stroage' -import { downloadFile } from '@/lib/file-download' +import { eq, inArray, and, ilike } from 'drizzle-orm' +import { mkdir, writeFile } from 'fs/promises' +import path from 'path' import { revalidateTag, revalidatePath } from 'next/cache' +import { basicContract } from '@/db/schema/basicContractDocumnet' +import { saveFile } from '@/lib/file-stroage' // userId를 user.name으로 변환하는 유틸리티 함수 async function getUserNameById(userId: string): Promise<string> { @@ -193,18 +196,40 @@ export async function updatePreQuoteSelection(companyIds: number[], isSelected: // 사전견적용 업체 삭제 export async function deleteBiddingCompany(id: number) { try { + // 1. 해당 업체의 초대 상태 확인 + const company = await db + .select({ invitationStatus: biddingCompanies.invitationStatus }) + .from(biddingCompanies) + .where(eq(biddingCompanies.id, id)) + .then(rows => rows[0]) + + if (!company) { + return { + success: false, + error: '해당 업체를 찾을 수 없습니다.' + } + } + + // 이미 초대가 발송된 경우(수락, 거절, 요청됨 등) 삭제 불가 + if (company.invitationStatus !== 'pending') { + return { + success: false, + error: '이미 초대를 보낸 업체는 삭제할 수 없습니다.' + } + } + await db.transaction(async (tx) => { - // 1. 먼저 관련된 조건 응답들 삭제 + // 2. 먼저 관련된 조건 응답들 삭제 await tx.delete(companyConditionResponses) .where(eq(companyConditionResponses.biddingCompanyId, id)) - // 2. biddingCompanies 레코드 삭제 + // 3. biddingCompanies 레코드 삭제 await tx.delete(biddingCompanies) .where(eq(biddingCompanies.id, id)) - }) + }) - return { - success: true, + return { + success: true, message: '업체가 성공적으로 삭제되었습니다.' } } catch (error) { @@ -1157,4 +1182,344 @@ export async function deletePreQuoteDocument( error: '문서 삭제에 실패했습니다.' } } - }
\ No newline at end of file + } + +// 기본계약 발송 (서버 액션) +export async function sendBiddingBasicContracts( + biddingId: number, + vendorData: Array<{ + vendorId: number + vendorName: string + vendorCode?: string + vendorCountry?: string + selectedMainEmail: string + additionalEmails: string[] + customEmails?: Array<{ email: string; name?: string }> + contractRequirements: { + ndaYn: boolean + generalGtcYn: boolean + projectGtcYn: boolean + agreementYn: boolean + } + biddingCompanyId: number + biddingId: number + hasExistingContracts?: boolean + }>, + generatedPdfs: Array<{ + key: string + buffer: number[] + fileName: string + }>, + message?: string +) { + try { + console.log("sendBiddingBasicContracts called with:", { biddingId, vendorData: vendorData.map(v => ({ vendorId: v.vendorId, biddingCompanyId: v.biddingCompanyId, biddingId: v.biddingId })) }); + + // 현재 사용자 정보 조회 (임시로 첫 번째 사용자 사용) + const [currentUser] = await db.select().from(users).limit(1) + + if (!currentUser) { + throw new Error("사용자 정보를 찾을 수 없습니다.") + } + + const results = [] + const savedContracts = [] + + // 트랜잭션 시작 + const contractsDir = path.join(process.cwd(), `${process.env.NAS_PATH}`, "contracts", "generated"); + await mkdir(contractsDir, { recursive: true }); + + const result = await db.transaction(async (tx) => { + // 각 벤더별로 기본계약 생성 및 이메일 발송 + for (const vendor of vendorData) { + // 기존 계약 확인 (biddingCompanyId 기준) + if (vendor.hasExistingContracts) { + console.log(`벤더 ${vendor.vendorName}는 이미 계약이 체결되어 있어 건너뜁니다.`) + continue + } + + // 벤더 정보 조회 + const [vendorInfo] = await tx + .select() + .from(vendors) + .where(eq(vendors.id, vendor.vendorId)) + .limit(1) + + if (!vendorInfo) { + console.error(`벤더 정보를 찾을 수 없습니다: ${vendor.vendorId}`) + continue + } + + // biddingCompany 정보 조회 (biddingCompanyId를 직접 사용) + console.log(`Looking for biddingCompany with id=${vendor.biddingCompanyId}`) + let [biddingCompanyInfo] = await tx + .select() + .from(biddingCompanies) + .where(eq(biddingCompanies.id, vendor.biddingCompanyId)) + .limit(1) + + console.log(`Found biddingCompanyInfo:`, biddingCompanyInfo) + if (!biddingCompanyInfo) { + console.error(`입찰 회사 정보를 찾을 수 없습니다: biddingCompanyId=${vendor.biddingCompanyId}`) + // fallback: biddingId와 vendorId로 찾기 시도 + console.log(`Fallback: Looking for biddingCompany with biddingId=${biddingId}, companyId=${vendor.vendorId}`) + const [fallbackCompanyInfo] = await tx + .select() + .from(biddingCompanies) + .where(and( + eq(biddingCompanies.biddingId, biddingId), + eq(biddingCompanies.companyId, vendor.vendorId) + )) + .limit(1) + console.log(`Fallback found biddingCompanyInfo:`, fallbackCompanyInfo) + if (fallbackCompanyInfo) { + console.log(`Using fallback biddingCompanyInfo`) + biddingCompanyInfo = fallbackCompanyInfo + } else { + console.log(`Available biddingCompanies for biddingId=${biddingId}:`, await tx.select().from(biddingCompanies).where(eq(biddingCompanies.biddingId, biddingId)).limit(10)) + continue + } + } + + // 계약 요구사항에 따라 계약서 생성 + const contractTypes: Array<{ type: string; templateName: string }> = [] + if (vendor.contractRequirements.ndaYn) contractTypes.push({ type: 'NDA', templateName: '비밀' }) + if (vendor.contractRequirements.generalGtcYn) contractTypes.push({ type: 'General_GTC', templateName: 'General GTC' }) + if (vendor.contractRequirements.projectGtcYn) contractTypes.push({ type: 'Project_GTC', templateName: '기술' }) + if (vendor.contractRequirements.agreementYn) contractTypes.push({ type: '기술자료', templateName: '기술자료' }) + console.log("contractTypes", contractTypes) + for (const contractType of contractTypes) { + // PDF 데이터 찾기 (include를 사용하여 유연하게 찾기) + console.log("generatedPdfs", generatedPdfs.map(pdf => pdf.key)) + const pdfData = generatedPdfs.find((pdf: any) => + pdf.key.includes(`${vendor.vendorId}_`) && + pdf.key.includes(`_${contractType.templateName}`) + ) + console.log("pdfData", pdfData, "for contractType", contractType) + if (!pdfData) { + console.error(`PDF 데이터를 찾을 수 없습니다: vendorId=${vendor.vendorId}, templateName=${contractType.templateName}`) + continue + } + + // 파일 저장 (rfq-last 방식) + const fileName = `${contractType.type}_${vendor.vendorCode || vendor.vendorId}_${vendor.biddingCompanyId}_${Date.now()}.pdf` + const filePath = path.join(contractsDir, fileName); + + await writeFile(filePath, Buffer.from(pdfData.buffer)); + + // 템플릿 정보 조회 (rfq-last 방식) + const [template] = await db + .select() + .from(basicContractTemplates) + .where( + and( + ilike(basicContractTemplates.templateName, `%${contractType.templateName}%`), + eq(basicContractTemplates.status, "ACTIVE") + ) + ) + .limit(1); + + console.log("템플릿", contractType.templateName, template); + + // 기존 계약이 있는지 확인 (rfq-last 방식) + const [existingContract] = await tx + .select() + .from(basicContract) + .where( + and( + eq(basicContract.templateId, template?.id), + eq(basicContract.vendorId, vendor.vendorId), + eq(basicContract.biddingCompanyId, biddingCompanyInfo.id) + ) + ) + .limit(1); + + let contractRecord; + + if (existingContract) { + // 기존 계약이 있으면 업데이트 + [contractRecord] = await tx + .update(basicContract) + .set({ + requestedBy: currentUser.id, + status: "PENDING", // 재발송 상태 + fileName: fileName, + filePath: `/contracts/generated/${fileName}`, + deadline: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), + updatedAt: new Date(), + }) + .where(eq(basicContract.id, existingContract.id)) + .returning(); + + console.log("기존 계약 업데이트:", contractRecord.id); + } else { + // 새 계약 생성 + [contractRecord] = await tx + .insert(basicContract) + .values({ + templateId: template?.id || null, + vendorId: vendor.vendorId, + biddingCompanyId: biddingCompanyInfo.id, + rfqCompanyId: null, + generalContractId: null, + requestedBy: currentUser.id, + status: 'PENDING', + fileName: fileName, + filePath: `/contracts/generated/${fileName}`, + deadline: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10일 후 + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + console.log("새 계약 생성:", contractRecord.id); + } + + results.push({ + vendorId: vendor.vendorId, + vendorName: vendor.vendorName, + contractId: contractRecord.id, + contractType: contractType.type, + fileName: fileName, + filePath: `/contracts/generated/${fileName}`, + }) + + // savedContracts에 추가 (rfq-last 방식) + // savedContracts.push({ + // vendorId: vendor.vendorId, + // vendorName: vendor.vendorName, + // templateName: contractType.templateName, + // contractId: contractRecord.id, + // fileName: fileName, + // isUpdated: !!existingContract, // 업데이트 여부 표시 + // }) + } + + // 이메일 발송 (선택사항) + if (vendor.selectedMainEmail) { + try { + await sendEmail({ + to: vendor.selectedMainEmail, + template: 'basic-contract-notification', + context: { + vendorName: vendor.vendorName, + biddingId: biddingId, + contractCount: contractTypes.length, + deadline: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toLocaleDateString('ko-KR'), + loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/partners/bid/${biddingId}`, + message: message || '', + currentYear: new Date().getFullYear(), + language: 'ko' + } + }) + } catch (emailError) { + console.error(`이메일 발송 실패 (${vendor.selectedMainEmail}):`, emailError) + // 이메일 발송 실패해도 계약 생성은 유지 + } + } + } + + return { + success: true, + message: `${results.length}개의 기본계약이 생성되었습니다.`, + results, + savedContracts, + totalContracts: savedContracts.length + } + }) + + return result + + } catch (error) { + console.error('기본계약 발송 실패:', error) + throw new Error( + error instanceof Error + ? error.message + : '기본계약 발송 중 오류가 발생했습니다.' + ) + } +} + +// 기존 기본계약 조회 (서버 액션) +export async function getExistingBasicContractsForBidding(biddingId: number) { + try { + // 해당 biddingId에 속한 biddingCompany들의 기존 기본계약 조회 + const existingContracts = await db + .select({ + id: basicContract.id, + vendorId: basicContract.vendorId, + biddingCompanyId: basicContract.biddingCompanyId, + biddingId: biddingCompanies.biddingId, + templateId: basicContract.templateId, + status: basicContract.status, + createdAt: basicContract.createdAt, + }) + .from(basicContract) + .leftJoin(biddingCompanies, eq(basicContract.biddingCompanyId, biddingCompanies.id)) + .where( + and( + eq(biddingCompanies.biddingId, biddingId), + ) + ) + + return { + success: true, + contracts: existingContracts + } + + } catch (error) { + console.error('기존 계약 조회 실패:', error) + return { + success: false, + error: '기존 계약 조회에 실패했습니다.' + } + } +} + +// 선정된 업체들 조회 (서버 액션) +export async function getSelectedVendorsForBidding(biddingId: number) { + try { + const selectedCompanies = await db + .select({ + id: biddingCompanies.id, + companyId: biddingCompanies.companyId, + companyName: vendors.vendorName, + companyCode: vendors.vendorCode, + companyCountry: vendors.country, + contactPerson: biddingCompanies.contactPerson, + contactEmail: biddingCompanies.contactEmail, + biddingId: biddingCompanies.biddingId, + }) + .from(biddingCompanies) + .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) + .where(and( + eq(biddingCompanies.biddingId, biddingId), + eq(biddingCompanies.isPreQuoteSelected, true) + )) + + return { + success: true, + vendors: selectedCompanies.map(company => ({ + vendorId: company.companyId, // 실제 vendor ID + vendorName: company.companyName || '', + vendorCode: company.companyCode, + vendorCountry: company.companyCountry || '대한민국', + contactPerson: company.contactPerson, + contactEmail: company.contactEmail, + biddingCompanyId: company.id, // biddingCompany ID + biddingId: company.biddingId, + ndaYn: true, // 모든 계약 타입을 활성화 (필요에 따라 조정) + generalGtcYn: true, + projectGtcYn: true, + agreementYn: true + })) + } + } catch (error) { + console.error('선정된 업체 조회 실패:', error) + return { + success: false, + error: '선정된 업체 조회에 실패했습니다.', + vendors: [] + } + } +}
\ No newline at end of file |
