diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
| commit | 675b4e3d8ffcb57a041db285417d81e61284d900 (patch) | |
| tree | 254f3d6a6c0ce39ae8fba35618f3810e08945f19 /lib/basic-contract | |
| parent | 39f12cb19f29cbc5568057e154e6adf4789ae736 (diff) | |
(대표님) RFQ-last, tbe-last, 기본계약 템플릿 내 견적,입찰,계약 추가, env.dev NAS_PATH 수정
Diffstat (limited to 'lib/basic-contract')
| -rw-r--r-- | lib/basic-contract/gen-service.ts | 399 | ||||
| -rw-r--r-- | lib/basic-contract/vendor-table/basic-contract-columns.tsx | 52 |
2 files changed, 451 insertions, 0 deletions
diff --git a/lib/basic-contract/gen-service.ts b/lib/basic-contract/gen-service.ts new file mode 100644 index 00000000..5619f98e --- /dev/null +++ b/lib/basic-contract/gen-service.ts @@ -0,0 +1,399 @@ +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"; + +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 파일 저장 + const outputDir = path.join(process.cwd(), process.env.NAS_PATH, "contracts", "generated"); + await mkdir(outputDir, { recursive: true }); + + const timestamp = Date.now(); + const finalFileName = `${fileName.replace('.pdf', '')}_${timestamp}.pdf`; + const outputPath = path.join(outputDir, finalFileName); + + await writeFile(outputPath, Buffer.from(pdfBuffer)); + + const relativePath = `/contracts/generated/${finalFileName}`; + + // 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: finalFileName, + filePath: relativePath, + 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: relativePath, + pdfFileName: finalFileName + }; + } catch (error) { + console.error("PDF 저장 실패:", error); + throw new Error( + error instanceof Error + ? error.message + : "PDF 저장 중 오류가 발생했습니다." + ); + } +}
\ No newline at end of file diff --git a/lib/basic-contract/vendor-table/basic-contract-columns.tsx b/lib/basic-contract/vendor-table/basic-contract-columns.tsx index 1b11285c..c8af2e02 100644 --- a/lib/basic-contract/vendor-table/basic-contract-columns.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-columns.tsx @@ -241,6 +241,57 @@ export function getColumns({ setRowAction, locale = 'ko', t }: GetColumnsProps): groupMap[groupName].push(childCol) }) + const contractTypeColumn: ColumnDef<BasicContractView> = { + id: "contractType", + accessorFn: (row) => { + // 계약 유형 판별 로직 + if (row.generalContractId) return "contract"; + if (row.rfqCompanyId) return "quotation"; + if (row.biddingCompanyId) return "bidding"; + return "general"; + }, + header: ({ column }) => ( + <DataTableColumnHeaderSimple + column={column} + title={locale === 'ko' ? "계약 소스" : "Contract Source"} + /> + ), + cell: ({ getValue }) => { + const type = getValue() as string; + + // 타입별 표시 텍스트와 스타일 정의 + const typeConfig = { + general: { + label: locale === 'ko' ? '일반' : 'General', + variant: 'outline' as const + }, + quotation: { + label: locale === 'ko' ? '견적' : 'Quotation', + variant: 'secondary' as const + }, + contract: { + label: locale === 'ko' ? '계약' : 'Contract', + variant: 'default' as const + }, + bidding: { + label: locale === 'ko' ? '입찰' : 'Bidding', + variant: 'destructive' as const + } + }; + + const config = typeConfig[type as keyof typeof typeConfig] || typeConfig.general; + + return ( + <Badge variant={config.variant}> + {config.label} + </Badge> + ); + }, + enableSorting: true, + enableHiding: true, + minSize: 80, + }; + // ---------------------------------------------------------------- // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 // ---------------------------------------------------------------- @@ -269,6 +320,7 @@ export function getColumns({ setRowAction, locale = 'ko', t }: GetColumnsProps): return [ selectColumn, downloadColumn, // 다운로드 컬럼 추가 + contractTypeColumn, ...nestedColumns, ] }
\ No newline at end of file |
