import { format } from "date-fns" /** * ContractSummary 인터페이스 (UI 컴포넌트와 맞춤) */ interface ContractSummary { basicInfo: Record items: Record[] subcontractChecklist: Record | null storageInfo?: Record[] // 임치(물품보관) 계약 정보 } /** * 계약 데이터를 템플릿 변수로 매핑하는 함수 * * @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 = { // ---------------------------------- // 시스템/공통 // ---------------------------------- 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 }