import db from "@/db/db"; import { basicContract, basicContractTemplates,vendors, rfqLastDetails } from "@/db/schema"; import { eq, and, ilike } from "drizzle-orm"; import { addDays } from "date-fns"; import { writeFile, mkdir } from "fs/promises"; import path from "path"; import { saveBuffer } from '@/lib/file-storage'; // 추가 interface BasicContractParams { templateName: string; vendorId: number; biddingCompanyId?: number | null; rfqCompanyId: number| null; generalContractId?: number | null; requestedBy: number; } /** * 기본 계약서를 생성하는 공용 함수 * @param params 계약서 생성에 필요한 파라미터 * @returns 생성된 계약서 정보 */ export async function generateBasicContract(params: BasicContractParams) { const { templateName, vendorId, biddingCompanyId, rfqCompanyId, generalContractId, requestedBy } = params; try { // 1. 템플릿 조회 (ACTIVE 상태인 최신 리비전) const [template] = await db .select() .from(basicContractTemplates) .where( and( ilike(basicContractTemplates.templateName, templateName), eq(basicContractTemplates.status, "ACTIVE") ) ) .limit(1); if (!template) { throw new Error(`템플릿을 찾을 수 없습니다: ${templateName}`); } // 2. 기본 계약서 생성 const [newContract] = await db .insert(basicContract) .values({ templateId: template.id, vendorId: vendorId, biddingCompanyId: biddingCompanyId, rfqCompanyId: rfqCompanyId, generalContractId: generalContractId, requestedBy: requestedBy, status: "PENDING", // 초기 상태 fileName: template.fileName || `${templateName}_contract.pdf`, filePath: template.filePath || "", deadline: addDays(new Date(), 10), // 마감일은 요청일로부터 10일 후 createdAt: new Date(), updatedAt: new Date(), }) .returning(); return { success: true, contractId: newContract.id, templateName: templateName, status: newContract.status, deadline: newContract.deadline }; } catch (error) { console.error(`기본계약 생성 실패 (${templateName}):`, error); throw new Error( error instanceof Error ? error.message : "기본계약 생성 중 오류가 발생했습니다." ); } } /** * 벤더의 기본계약 요구사항에 따라 계약서들을 일괄 생성 */ export async function generateBasicContractsForVendor({ vendorId, rfqDetailId, contractRequirements, requestedBy, projectCode }: { vendorId: number; rfqDetailId: number; contractRequirements: { ndaYn: boolean; generalGtcYn: boolean; projectGtcYn: boolean; agreementYn: boolean; }; requestedBy: number; projectCode?: string; }) { const results = []; const errors = []; try { // NDA (비밀유지계약서) if (contractRequirements.ndaYn) { try { const result = await generateBasicContract({ templateName: "비밀", vendorId, rfqCompanyId: rfqDetailId, requestedBy }); results.push({ type: "NDA", ...result }); } catch (error) { errors.push({ type: "NDA", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } // General GTC (일반 거래약관) if (contractRequirements.generalGtcYn) { try { const result = await generateBasicContract({ templateName: "General GTC", vendorId, rfqCompanyId: rfqDetailId, requestedBy }); results.push({ type: "General GTC", ...result }); } catch (error) { errors.push({ type: "General GTC", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } // Project GTC (프로젝트별 거래약관) if (contractRequirements.projectGtcYn && projectCode) { try { // 프로젝트별 템플릿명 생성 (예: "프로젝트거래약관_PJ001") const templateName = `${projectCode}`; // 먼저 프로젝트별 템플릿이 있는지 확인 const [projectTemplate] = await db .select() .from(basicContractTemplates) .where( and( eq(basicContractTemplates.templateName, templateName), eq(basicContractTemplates.status, "ACTIVE") ) ); // 프로젝트별 템플릿이 없으면 일반 프로젝트 거래약관 사용 const actualTemplateName = projectTemplate const result = await generateBasicContract({ templateName: actualTemplateName, vendorId, rfqCompanyId: rfqDetailId, requestedBy }); results.push({ type: "Project GTC", projectCode, ...result }); } catch (error) { errors.push({ type: "Project GTC", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } // 기술자료 제공 동의서 if (contractRequirements.agreementYn) { try { const result = await generateBasicContract({ templateName: "기술", vendorId, rfqCompanyId: rfqDetailId, requestedBy }); results.push({ type: "기술자료 제공 동의서", ...result }); } catch (error) { errors.push({ type: "기술자료 제공 동의서", error: error instanceof Error ? error.message : "알 수 없는 오류" }); } } return { success: results.length > 0, results, errors, totalCreated: results.length, totalFailed: errors.length }; } catch (error) { console.error("기본계약 일괄 생성 실패:", error); throw new Error( error instanceof Error ? error.message : "기본계약 일괄 생성 중 오류가 발생했습니다." ); } } /** * 기본계약 상태 업데이트 */ export async function updateBasicContractStatus( contractId: number, status: string, additionalData?: { vendorSignedAt?: Date; buyerSignedAt?: Date; legalReviewRequestedAt?: Date; legalReviewCompletedAt?: Date; completedAt?: Date; } ) { try { const [updated] = await db .update(basicContract) .set({ status, updatedAt: new Date(), ...additionalData }) .where(eq(basicContract.id, contractId)) .returning(); return { success: true, contract: updated }; } catch (error) { console.error("기본계약 상태 업데이트 실패:", error); throw new Error( error instanceof Error ? error.message : "기본계약 상태 업데이트 중 오류가 발생했습니다." ); } } interface BasicContractParams { templateName: string; vendorId: number; biddingCompanyId?: number | null; rfqCompanyId: number; generalContractId?: number | null; requestedBy: number; } /** * 템플릿 데이터 준비 (PDF 변환 전 단계) */ export async function prepareContractTemplate(params: BasicContractParams) { const { templateName, vendorId } = params; try { // 1. 템플릿 조회 const [template] = await db .select() .from(basicContractTemplates) .where( and( ilike(basicContractTemplates.templateName, templateName), eq(basicContractTemplates.status, "ACTIVE") ) ) .limit(1); if (!template) { throw new Error(`템플릿을 찾을 수 없습니다: ${templateName}`); } // 2. 벤더 정보 조회 const [vendor] = await db .select() .from(vendors) .where(eq(vendors.id, vendorId)) .limit(1); if (!vendor) { throw new Error(`벤더를 찾을 수 없습니다: ${vendorId}`); } // 3. 템플릿 데이터 준비 const templateData = { company_name: vendor.vendorName || '협력업체명', company_address: vendor.address || '주소', company_address_detail: vendor.addressDetail || '', company_postal_code: vendor.postalCode || '', company_country: vendor.country || '대한민국', representative_name: vendor.representativeName || '대표자명', representative_email: vendor.representativeEmail || '', representative_phone: vendor.representativePhone || '', tax_id: vendor.taxId || '사업자번호', corporate_registration_number: vendor.corporateRegistrationNumber || '', phone_number: vendor.phone || '전화번호', email: vendor.email || '', website: vendor.website || '', signature_date: new Date().toLocaleDateString('ko-KR'), contract_date: new Date().toISOString().split('T')[0], effective_date: new Date().toISOString().split('T')[0], expiry_date: addDays(new Date(), 365).toISOString().split('T')[0], vendor_code: vendor.vendorCode || '', business_size: vendor.businessSize || '', credit_rating: vendor.creditRating || '', template_type: templateName, contract_number: `BC-${new Date().getFullYear()}-${String(vendorId).padStart(4, '0')}-${Date.now()}`, }; return { success: true, template, vendor, templateData, params }; } catch (error) { console.error(`템플릿 준비 실패 (${templateName}):`, error); throw new Error( error instanceof Error ? error.message : "템플릿 준비 중 오류가 발생했습니다." ); } } /** * PDF 파일 저장 및 DB 레코드 생성 */ export async function saveContractPdf({ pdfBuffer, fileName, params, templateId }: { pdfBuffer: Uint8Array; fileName: string; params: BasicContractParams; templateId: number; }) { try { // 1. PDF 파일 저장 (공용 saveBuffer 사용) const saveResult = await saveBuffer({ buffer: Buffer.from(pdfBuffer), fileName: fileName, // 원본 파일명 (확장자 포함) directory: 'contracts/generated', originalName: fileName, // DB에 저장할 원본명 userId: params.requestedBy // 요청자 ID를 userId로 사용 }); // 저장 실패 시 에러 처리 if (!saveResult.success) { throw new Error(saveResult.error || 'PDF 파일 저장에 실패했습니다.'); } // 2. DB에 계약서 레코드 생성 const [newContract] = await db .insert(basicContract) .values({ templateId: templateId, vendorId: params.vendorId, biddingCompanyId: params.biddingCompanyId, rfqCompanyId: params.rfqCompanyId, generalContractId: params.generalContractId, requestedBy: params.requestedBy, status: "PENDING", fileName: saveResult.originalName || fileName, // 원본 파일명 filePath: saveResult.publicPath, // 웹 접근 가능한 경로 deadline: addDays(new Date(), 10), createdAt: new Date(), updatedAt: new Date(), }) .returning(); return { success: true, contractId: newContract.id, templateName: params.templateName, status: newContract.status, deadline: newContract.deadline, pdfPath: saveResult.publicPath, // 공용 함수가 반환한 경로 pdfFileName: saveResult.originalName || fileName, // 원본 파일명 hashedFileName: saveResult.fileName, // 실제 저장된 해시 파일명 (필요시 사용) securityChecks: saveResult.securityChecks // 보안 검증 결과 (디버깅용) }; } catch (error) { console.error("PDF 저장 실패:", error); throw new Error( error instanceof Error ? error.message : "PDF 저장 중 오류가 발생했습니다." ); } }