diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-01 03:09:00 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-01 03:09:00 +0000 |
| commit | 3c9a95332298450c7e0f75bfb08944439e1a3739 (patch) | |
| tree | 1ecc04bf97dfd572736ee56119b02bd72678720d /lib/general-contracts/utils.ts | |
| parent | c92ddd6bae8e187cccfddb37373460ebea0ade27 (diff) | |
(최겸)구매 일반계약 템플릿 자동 연동 및 매핑 기능 추가
Diffstat (limited to 'lib/general-contracts/utils.ts')
| -rw-r--r-- | lib/general-contracts/utils.ts | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/lib/general-contracts/utils.ts b/lib/general-contracts/utils.ts new file mode 100644 index 00000000..ec15a3a1 --- /dev/null +++ b/lib/general-contracts/utils.ts @@ -0,0 +1,304 @@ +import { format } from "date-fns" + +/** + * ContractSummary 인터페이스 (UI 컴포넌트와 맞춤) + */ +interface ContractSummary { + basicInfo: Record<string, any> + items: Record<string, any>[] + subcontractChecklist: Record<string, any> | null + storageInfo?: Record<string, any>[] // 임치(물품보관) 계약 정보 +} + +/** + * 계약 데이터를 템플릿 변수로 매핑하는 함수 + * + * @param contractSummary 계약 요약 정보 + * @returns PDFTron 템플릿에 적용할 변수 맵 (Key-Value) + */ +export function mapContractDataToTemplateVariables(contractSummary: ContractSummary) { + const { basicInfo, items, storageInfo } = contractSummary + const firstItem = items && items.length > 0 ? items[0] : {} + + // 날짜 포맷팅 헬퍼 (YYYY-MM-DD) + const formatDate = (date: any) => { + if (!date) return '' + try { + const d = new Date(date) + if (isNaN(d.getTime())) return String(date) + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + } catch { + return String(date) + } + } + + // 금액 포맷팅 헬퍼 (천단위 콤마) + const formatCurrency = (amount: any) => { + if (amount === undefined || amount === null || amount === '') return '' + const num = Number(amount) + if (isNaN(num)) return String(amount) + return num.toLocaleString('ko-KR') + } + + // 비율 포맷팅 (소수점 제거 등 필요 시) + const formatRate = (rate: any) => { + if (!rate) return '' + return String(rate) + } + + // 1. 프로젝트 정보 (Items에서 프로젝트명 추출 시도) + const projectName = basicInfo.projectName || (items.length > 0 ? items[0].projectName : '') || "Following potential projects" + const projectCode = basicInfo.projectCode || (items.length > 0 ? items[0].projectCode : '') || '' + + // 2. 계약금액 표시 로직 (단가/물량 계약은 '별첨 참조') + const contractScope = basicInfo.contractScope || '' + let displayContractAmount = formatCurrency(basicInfo.contractAmount) + let displayContractAmountText = '' + + if (contractScope === '단가' || contractScope === '물량(실적)') { + displayContractAmount = '별첨 참조' + displayContractAmountText = '별첨 참조' + } else { + displayContractAmountText = displayContractAmount + } + + // 공급가액 & 부가세 (임시 계산 로직 제거) + // 실제로는 taxType에 따라 다를 수 있음 (영세율 등) - 데이터가 있으면 매핑 + const supplyPrice = basicInfo.supplyPrice ? formatCurrency(basicInfo.supplyPrice) : '' + const vat = basicInfo.vat ? formatCurrency(basicInfo.vat) : '' + + // 3. 지급조건 상세 텍스트 생성 + // 납품 전 + const prePaymentData = basicInfo.paymentBeforeDelivery || {} + let prePaymentText = '' + const prePaymentParts: string[] = [] + if (prePaymentData.apBond) prePaymentParts.push(`AP Bond(${prePaymentData.apBondPercent}%)`) + if (prePaymentData.drawingSubmission) prePaymentParts.push(`도면제출(${prePaymentData.drawingSubmissionPercent}%)`) + if (prePaymentData.materialPurchase) prePaymentParts.push(`소재구매(${prePaymentData.materialPurchasePercent}%)`) + if (prePaymentData.additionalCondition) prePaymentParts.push(`추가조건(${prePaymentData.additionalConditionPercent}%)`) + if (prePaymentParts.length > 0) { + prePaymentText = `(선급금) ${prePaymentParts.join(', ')}` + } + + // 납품 + const deliveryPaymentText = basicInfo.paymentDelivery ? `(본품 납품) ${basicInfo.paymentDelivery}` : '' + + // 납품 후 + const postPaymentData = basicInfo.paymentAfterDelivery || {} + let postPaymentText = '' + const postPaymentParts: string[] = [] + if (postPaymentData.commissioning) postPaymentParts.push(`Commissioning(${postPaymentData.commissioningPercent}%)`) + if (postPaymentData.finalDocument) postPaymentParts.push(`최종문서(${postPaymentData.finalDocumentPercent}%)`) + if (postPaymentData.other) postPaymentParts.push(`기타(${postPaymentData.otherText})`) + if (postPaymentParts.length > 0) { + postPaymentText = `(납품 외) ${postPaymentParts.join(', ')}` + } + + // 4. 보증금 및 위약금 (DB 필드값 사용, 임시 계산 제거) + // DB에 해당 필드가 없으면 빈 값으로 매핑됨. + const contractDepositAmount = basicInfo.contractDepositAmount || '' + const defectDepositAmount = basicInfo.defectDepositAmount || '' + const paymentDepositAmount = basicInfo.paymentDepositAmount || '' + const unfairJointActPenaltyAmount = basicInfo.unfairJointActPenaltyAmount || '' + + // 지체상금 + const liquidatedDamagesRate = basicInfo.liquidatedDamagesPercent || '0' + + // 5. 조건 텍스트 변환 (JSON -> String) + // 계약해지조건 + let terminationConditionsText = '' + if (basicInfo.contractTerminationConditions) { + try { + const cond = typeof basicInfo.contractTerminationConditions === 'string' + ? JSON.parse(basicInfo.contractTerminationConditions) + : basicInfo.contractTerminationConditions + + const active: string[] = [] + if (cond.standardTermination) active.push('표준 계약해지조건') + if (cond.projectNotAwarded) active.push('프로젝트 미수주 시') + if (cond.other) active.push('기타') + terminationConditionsText = active.join(', ') + } catch (e) {} + } + + // 계약성립조건 + let establishmentConditionsText = '' + if (basicInfo.contractEstablishmentConditions) { + try { + const cond = typeof basicInfo.contractEstablishmentConditions === 'string' + ? JSON.parse(basicInfo.contractEstablishmentConditions) + : basicInfo.contractEstablishmentConditions + + const active: string[] = [] + if (cond.regularVendorRegistration) active.push('정규업체 등록(실사 포함) 시') + if (cond.projectAward) active.push('프로젝트 수주 시') + if (cond.ownerApproval) active.push('선주 승인 시') + if (cond.other) active.push('기타') + establishmentConditionsText = active.join(', ') + } catch (e) {} + } + + // 품질/하자보증기간 텍스트 + let warrantyPeriodText = '' + if (basicInfo.warrantyPeriod) { + try { + const wp = typeof basicInfo.warrantyPeriod === 'string' ? JSON.parse(basicInfo.warrantyPeriod) : basicInfo.warrantyPeriod + const parts: string[] = [] + if (wp.납품후?.enabled) parts.push(`납품 후 ${wp.납품후.period}개월`) + if (wp.인도후?.enabled) parts.push(`인도 후 ${wp.인도후.period}개월`) + if (wp.작업후?.enabled) parts.push(`작업 후 ${wp.작업후.period}개월`) + if (wp.기타?.enabled) parts.push(`기타`) + warrantyPeriodText = parts.join(', ') + } catch(e) {} + } + + // 6. 임치(물품보관) 계약 관련 (SG) + const storageItems = storageInfo || [] + // 템플릿에서 루프를 지원하지 않을 경우를 대비한 텍스트 포맷 (Fallback) + const storageTableText = storageItems.length > 0 + ? storageItems.map((item, idx) => + `${idx + 1}. PO No.: ${item.poNumber || '-'}, 호선: ${item.hullNumber || '-'}, 미입고 잔여금액: ${formatCurrency(item.remainingAmount)}` + ).join('\n') + : '' + + + // ═══════════════════════════════════════════════════════════════ + // 변수 매핑 시작 + // ═══════════════════════════════════════════════════════════════ + const variables: Record<string, any> = { + // ---------------------------------- + // 시스템/공통 + // ---------------------------------- + todayDate: formatDate(new Date()), // {{Today}} : 현재 날짜 + + // ---------------------------------- + // 계약 기본 정보 + // ---------------------------------- + contractName: basicInfo.contractName || basicInfo.name || '', // {{계약명}} + contractNumber: basicInfo.contractNumber || '', // {{계약번호}} + contractDate: formatDate(basicInfo.registeredAt || basicInfo.createdAt), // {{계약일자}} + + // ---------------------------------- + // 프로젝트 정보 + // ---------------------------------- + projectName: projectName, // {{프로젝트}}, {{대상호선}} : 없으면 'Following potential projects' + projectCode: projectCode, // {{프로젝트코드}} + + // ---------------------------------- + // 금액 정보 + // ---------------------------------- + contractAmount: displayContractAmount, // {{계약금액}} : '별첨 참조' 또는 금액 + supplyPrice: supplyPrice, // (공급가액) + vat: vat, // (부가가치세) + contractCurrency: basicInfo.currency || 'KRW', // 통화 + + // ---------------------------------- + // 협력업체(Vendor) 정보 + // ---------------------------------- + vendorName: basicInfo.vendorName || '', // {{VendorName}}, {{계약업체}}, {{수탁자}} + vendorAddress: basicInfo.vendorAddress || basicInfo.address || '', // {{VendorAddress}}, {{수탁자 주소}}, {{보관장소}} + vendorCeoName: basicInfo.vendorCeoName || basicInfo.representativeName || '', // {{Vendor_CEO_Name}}, {{대표이사}} + // vendorPhone, vendorEmail 등 필요시 추가 + + // ---------------------------------- + // 당사(SHI) 정보 (고정값/설정값) + // ---------------------------------- + shiAddress: "경기도 성남시 분당구 판교로 227번길 23", // {{SHI_Address}}, {{위탁자 주소}} + shiCeoName: "최성안", // {{SHI_CEO_Name}}, {{대표이사}} + + // ---------------------------------- + // 품목 정보 + // ---------------------------------- + // Frame Agreement 등의 {{자재그룹}}, {{자재그룹명}} + itemGroup: firstItem.itemCode || '', // {{자재그룹}} : 일단 ItemCode 매핑 (자재그룹 코드가 별도로 있다면 수정 필요) + itemGroupName: firstItem.itemInfo || '', // {{자재그룹명}} : ItemInfo 매핑 + pkgNo: firstItem.itemCode || '', // {{PKG No.}} + pkgName: firstItem.itemInfo || '', // {{PKG명}} + + // 일반 계약품목 / 임치 대상품목 + itemDescription: firstItem.itemInfo || firstItem.description || basicInfo.contractName || '', // {{계약품목}}, {{계약내용}} + itemInfo: firstItem.itemInfo || '', // {{Item 정보}} + itemName: firstItem.itemInfo || '', // {{ItemName}} + + // OF 배상품목 + reimbursementItem: firstItem.itemInfo || '', // {{배상품목}} + + // ---------------------------------- + // 사양 및 공급범위 + // ---------------------------------- + // {{사양 및 공급범위}} : 사양서 파일 유무에 따라 텍스트 변경 + // 실제 파일 존재 여부를 여기서 알기 어려우므로 specificationType으로 판단 + scopeOfSupply: basicInfo.specificationType === '첨부서류 참조' + ? '사양서 파일 참조(As per Technical agreement)' + : (basicInfo.specificationManualText || basicInfo.contractName || ''), + + // ---------------------------------- + // 계약 기간 및 유효기간 + // ---------------------------------- + contractPeriod: `${formatDate(basicInfo.startDate)} ~ ${formatDate(basicInfo.endDate)}`, // {{계약기간}}, {{FA 유효기간}}, {{보관날짜}} + contractStartDate: formatDate(basicInfo.startDate), + contractEndDate: formatDate(basicInfo.endDate), + validityEndDate: formatDate(basicInfo.validityEndDate || basicInfo.endDate), // {{LOI 유효기간}}, {{계약체결유효기간}} + + // ---------------------------------- + // 인도/지급 조건 + // ---------------------------------- + incoterms: basicInfo.deliveryTerm || '', // {{Incoterms}}, {{물품인도조건}} + paymentTerms: basicInfo.paymentTerm || '', // {{지급조건}}, {{대금지불조건}} - 코드값(L003 등)일 수 있음 + + // 상세 지급조건 (선급금, 납품, 납품 외) + // 템플릿에 (선급금) ... (본품 납품) ... 항목이 미리 적혀있는지, 변수로 넣어야 하는지에 따라 다름 + // 예시에서는 줄글로 보임. 각각 매핑. + prePaymentCondition: prePaymentText, // (선급금) 조건 텍스트 + deliveryPaymentCondition: deliveryPaymentText, // (본품 납품) 조건 텍스트 + postPaymentCondition: postPaymentText, // (납품 외) 조건 텍스트 + + // ---------------------------------- + // 보증기간 및 보증금 + // ---------------------------------- + warrantyPeriod: warrantyPeriodText, // {{품질/하자보증기간}} + + // 금액 계산 필드들 (DB 필드값이 없으면 빈 값) + contractDeposit: formatCurrency(contractDepositAmount), // {{계약보증금}} + defectDeposit: formatCurrency(defectDepositAmount), // {{하자보증금}} + paymentDeposit: formatCurrency(paymentDepositAmount), // {{지급보증금}} + + unfairJointActPenalty: formatCurrency(unfairJointActPenaltyAmount), // {{부정담합위약금}}, {{부당한공동행위}} + + // 지체상금 + liquidatedDamagesRate: formatRate(liquidatedDamagesRate), // {{지체상금비율}} + // liquidatedDamages: formatCurrency(liquidatedDamagesAmount), // 금액이 필요한 경우 사용 + + // ---------------------------------- + // 기타 조건 + // ---------------------------------- + terminationConditions: terminationConditionsText, // {{계약해지조건}} + establishmentConditions: establishmentConditionsText, // {{계약성립조건}} + subcontractInterlocking: basicInfo.interlockingSystem || 'N', // {{하도급연동}} + + // ---------------------------------- + // 참조/연결 정보 + // ---------------------------------- + // OF의 {{관련계약번호}} + linkedContractNumber: basicInfo.linkedPoNumber || basicInfo.linkedBidNumber || basicInfo.linkedRfqOrItb || '', + + // ---------------------------------- + // 임치(물품보관) 계약 (SG) + // ---------------------------------- + storageTableText: storageTableText, // {{storageTableText}} (fallback) + // PDFTron에서 배열을 받아 테이블 루프를 돌릴 수 있다면 아래 키를 사용 + storageList: storageItems, + } + + // 3. 모든 키를 순회하며 undefined나 null을 빈 문자열로 변환 (안전장치) + Object.keys(variables).forEach(key => { + if (variables[key] === undefined || variables[key] === null) { + variables[key] = '' + } + }) + + return variables +} |
