summaryrefslogtreecommitdiff
path: root/lib/basic-contract
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract')
-rw-r--r--lib/basic-contract/gen-service.ts399
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-columns.tsx52
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