From 3c9a95332298450c7e0f75bfb08944439e1a3739 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 1 Dec 2025 03:09:00 +0000 Subject: (최겸)구매 일반계약 템플릿 자동 연동 및 매핑 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../general-contract-approval-request-dialog.tsx | 281 ++++++------------- .../general-contract-review-request-dialog.tsx | 281 ++++++------------- .../main/create-general-contract-dialog.tsx | 132 +++------ .../main/general-contract-update-sheet.tsx | 15 +- .../main/general-contracts-table-columns.tsx | 19 +- lib/general-contracts/service.ts | 29 ++ lib/general-contracts/utils.ts | 304 +++++++++++++++++++++ 7 files changed, 571 insertions(+), 490 deletions(-) create mode 100644 lib/general-contracts/utils.ts diff --git a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx index 25c1fb9a..46251c71 100644 --- a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx +++ b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx @@ -29,8 +29,12 @@ import { getContractItems, getSubcontractChecklist, uploadContractApprovalFile, - sendContractApprovalRequest + sendContractApprovalRequest, + getContractById, + getContractTemplateByContractType, + getStorageInfo } from '../service' +import { mapContractDataToTemplateVariables } from '../utils' interface ContractApprovalRequestDialogProps { contract: Record @@ -42,6 +46,7 @@ interface ContractSummary { basicInfo: Record items: Record[] subcontractChecklist: Record | null + storageInfo?: Record[] } export function ContractApprovalRequestDialog({ @@ -70,63 +75,6 @@ export function ContractApprovalRequestDialog({ const contractId = contract.id as number const userId = session?.user?.id || '' - // LOI 템플릿용 변수 매핑 함수 - const mapContractSummaryToLOITemplate = (contractSummary: ContractSummary) => { - const { basicInfo, items } = contractSummary - const firstItem = items && items.length > 0 ? items[0] : {} - - // 날짜 포맷팅 헬퍼 함수 - const formatDate = (date: any) => { - if (!date) return '' - try { - const d = new Date(date) - return d.toLocaleDateString('ko-KR') - } catch { - return '' - } - } - - return { - // 날짜 관련 (템플릿에서 {{todayDate}} 형식으로 사용) - todayDate: new Date().toLocaleDateString('ko-KR'), - - // 벤더 정보 - vendorName: basicInfo?.vendorName || '', - representativeName: '', // 벤더 대표자 이름 - 현재 데이터에 없음, 향후 확장 가능 - - // 계약 기본 정보 - contractNumber: basicInfo?.contractNumber || '', - - // 프로젝트 정보 - projectNumber: '', // 프로젝트 코드 - 현재 데이터에 없음, 향후 확장 가능 - projectName: basicInfo?.projectName || '', - project: basicInfo?.projectName || '', - - // 아이템 정보 - item: firstItem?.itemInfo || '', - - // 무역 조건 - incoterms: basicInfo?.deliveryTerm || '', // Incoterms 대신 deliveryTerm 사용 - shippingLocation: basicInfo?.shippingLocation || '', - - // 금액 및 통화 - contractCurrency: basicInfo?.currency || '', - contractAmount: basicInfo?.contractAmount || '', - totalAmount: basicInfo?.contractAmount || '', // totalAmount가 없으면 contractAmount 사용 - - // 수량 - quantity: firstItem?.quantity || '', - - // 납기일 - contractDeliveryDate: formatDate(basicInfo?.contractDeliveryDate), - - // 지급 조건 - paymentTerm: basicInfo?.paymentTerm || '', - - // 유효기간 - validityEndDate: formatDate(basicInfo?.endDate), // validityEndDate 대신 endDate 사용 - } - } // 기본계약 생성 함수 (최종 전송 시점에 호출) const generateBasicContractPdf = async ( @@ -312,6 +260,18 @@ export function ContractApprovalRequestDialog({ } catch { console.log('Subcontract Checklist 데이터 없음') } + + // 임치(물품보관) 계약 정보 확인 (SG) + try { + if (summary.basicInfo?.contractType === 'SG') { + const storageData = await getStorageInfo(contractId) + if (storageData && storageData.length > 0) { + summary.storageInfo = storageData + } + } + } catch { + console.log('임치계약 정보 없음') + } console.log('contractSummary 구조:', summary) console.log('basicInfo 내용:', summary.basicInfo) @@ -324,55 +284,42 @@ export function ContractApprovalRequestDialog({ } }, [contractId]) - // 3단계: 파일 업로드 처리 - const handleFileUpload = async (file: File) => { - // 파일 확장자 검증 - const allowedExtensions = ['.doc', '.docx'] - const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')) - - if (!allowedExtensions.includes(fileExtension)) { - toast.error('Word 문서(.doc, .docx) 파일만 업로드 가능합니다.') - return - } - - if (!userId) { - toast.error('로그인이 필요합니다.') + // 3단계: PDF 생성 및 미리보기 (PDFTron 사용) - 템플릿 자동 로드 + const generatePdf = async () => { + if (!contractSummary) { + toast.error('계약 정보가 필요합니다.') return } setIsLoading(true) try { - // 서버액션을 사용하여 파일 저장 (본 계약문서로 고정) - const result = await uploadContractApprovalFile( - contractId, - file, - userId - ) + // 1. 계약 유형에 맞는 템플릿 조회 + const contractType = contractSummary.basicInfo.contractType as string + const templateResult = await getContractTemplateByContractType(contractType) - if (result.success) { - setUploadedFile(file) - toast.success('파일이 업로드되었습니다.') - } else { - throw new Error(result.error || '파일 업로드 실패') + if (!templateResult.success || !templateResult.template) { + throw new Error(templateResult.error || '템플릿을 찾을 수 없습니다.') } - } catch (error) { - console.error('Error uploading file:', error) - toast.error('파일 업로드 중 오류가 발생했습니다.') - } finally { - setIsLoading(false) - } - } - // 4단계: PDF 생성 및 미리보기 (PDFTron 사용) - const generatePdf = async () => { - if (!uploadedFile || !contractSummary) { - toast.error('업로드된 파일과 계약 정보가 필요합니다.') - return - } + const template = templateResult.template - setIsLoading(true) - try { - // PDFTron을 사용해서 변수 치환 및 PDF 변환 + // 2. 템플릿 파일 다운로드 + const templateResponse = await fetch("/api/contracts/get-template", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ templatePath: template.filePath }), + }) + + if (!templateResponse.ok) { + throw new Error("템플릿 파일을 다운로드할 수 없습니다.") + } + + const templateBlob = await templateResponse.blob() + const templateFile = new File([templateBlob], template.fileName || "template.docx", { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + }) + + // 3. PDFTron을 사용해서 변수 치환 및 PDF 변환 // @ts-ignore const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer) @@ -394,34 +341,34 @@ export function ContractApprovalRequestDialog({ const { Core } = instance const { createDocument } = Core - // 템플릿 문서 생성 및 변수 치환 - const templateDoc = await createDocument(uploadedFile, { - filename: uploadedFile.name, - extension: 'docx', - }) + // 템플릿 문서 생성 및 변수 치환 + const templateDoc = await createDocument(templateFile, { + filename: templateFile.name, + extension: 'docx', + }) - // LOI 템플릿용 변수 매핑 - const mappedTemplateData = mapContractSummaryToLOITemplate(contractSummary) + // 템플릿 변수 매핑 + const mappedTemplateData = mapContractDataToTemplateVariables(contractSummary) - console.log("🔄 변수 치환 시작:", mappedTemplateData) - await templateDoc.applyTemplateValues(mappedTemplateData as any) - console.log("✅ 변수 치환 완료") + console.log("🔄 변수 치환 시작:", mappedTemplateData) + await templateDoc.applyTemplateValues(mappedTemplateData as any) + console.log("✅ 변수 치환 완료") - // PDF 변환 - const fileData = await templateDoc.getFileData() + // PDF 변환 + const fileData = await templateDoc.getFileData() const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' }) - console.log(`✅ PDF 변환 완료: ${uploadedFile.name}`, `크기: ${pdfBuffer.byteLength} bytes`) + console.log(`✅ PDF 변환 완료: ${templateFile.name}`, `크기: ${pdfBuffer.byteLength} bytes`) - // PDF 버퍼를 Blob URL로 변환하여 미리보기 - const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' }) - const pdfUrl = URL.createObjectURL(pdfBlob) - setGeneratedPdfUrl(pdfUrl) - - // PDF 버퍼를 상태에 저장 (최종 전송 시 사용) - setGeneratedPdfBuffer(new Uint8Array(pdfBuffer)) - - toast.success('PDF가 생성되었습니다.') + // PDF 버퍼를 Blob URL로 변환하여 미리보기 + const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' }) + const pdfUrl = URL.createObjectURL(pdfBlob) + setGeneratedPdfUrl(pdfUrl) + + // PDF 버퍼를 상태에 저장 (최종 전송 시 사용) + setGeneratedPdfBuffer(new Uint8Array(pdfBuffer)) + + toast.success('PDF가 생성되었습니다.') } finally { // 임시 WebViewer 정리 @@ -429,9 +376,10 @@ export function ContractApprovalRequestDialog({ document.body.removeChild(tempDiv) } - } catch (error) { + } catch (error: any) { console.error('❌ PDF 생성 실패:', error) - toast.error('PDF 생성 중 오류가 발생했습니다.') + const errorMessage = error instanceof Error ? error.message : (error?.message || '알 수 없는 오류') + toast.error(`PDF 생성 중 오류가 발생했습니다: ${errorMessage}`) } finally { setIsLoading(false) } @@ -498,13 +446,13 @@ export function ContractApprovalRequestDialog({ setPdfViewerInstance(instance) // PDF 버퍼를 Blob으로 변환 - const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' }) + const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' }) const pdfUrl = URL.createObjectURL(pdfBlob) console.log("🔄 PDF Blob URL 생성:", pdfUrl) // 문서 로드 console.log("🔄 문서 로드 시작") - const { documentViewer } = instance.Core + const { documentViewer } = (instance as any).Core // 문서 로드 이벤트 대기 await new Promise((resolve, reject) => { @@ -553,7 +501,7 @@ export function ContractApprovalRequestDialog({ return } - const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' }) + const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' }) const pdfUrl = URL.createObjectURL(pdfBlob) const link = document.createElement('a') @@ -715,7 +663,7 @@ export function ContractApprovalRequestDialog({ - + 1. 계약 현황 정리 @@ -723,10 +671,7 @@ export function ContractApprovalRequestDialog({ 2. 기본계약 체크 - 3. 문서 업로드 - - - 4. PDF 미리보기 + 3. PDF 미리보기 @@ -843,7 +788,7 @@ export function ContractApprovalRequestDialog({
계약성립조건: {contractSummary?.basicInfo?.contractEstablishmentConditions && - Object.entries(contractSummary.basicInfo.contractEstablishmentConditions) + Object.entries(contractSummary.basicInfo.contractEstablishmentConditions as Record) .filter(([, value]) => value === true) .map(([key]) => key) .join(', ') || '없음'} @@ -851,7 +796,7 @@ export function ContractApprovalRequestDialog({
계약해지조건: {contractSummary?.basicInfo?.contractTerminationConditions && - Object.entries(contractSummary.basicInfo.contractTerminationConditions) + Object.entries(contractSummary.basicInfo.contractTerminationConditions as Record) .filter(([, value]) => value === true) .map(([key]) => key) .join(', ') || '없음'} @@ -878,9 +823,9 @@ export function ContractApprovalRequestDialog({
{contractSummary.items.slice(0, 3).map((item: Record, index: number) => (
-
{item.itemInfo || item.description || `품목 ${index + 1}`}
+
{String(item.itemInfo || item.description || `품목 ${index + 1}`)}
- 수량: {item.quantity || 0} | 단가: {item.contractUnitPrice || item.unitPrice || 0} + 수량: {String(item.quantity || 0)} | 단가: {String(item.contractUnitPrice || item.unitPrice || 0)}
))} @@ -1022,72 +967,8 @@ export function ContractApprovalRequestDialog({
- {/* 3단계: 문서 업로드 */} + {/* 3단계: PDF 미리보기 */} - - - - - 계약서 업로드 - - - -
-

일반계약 표준문서 관리 페이지에 접속하여, 원하는 양식의 계약서를 다운받아 수정 후 업로드하세요.

-
- - { - const file = e.target.files?.[0] - if (file) handleFileUpload(file) - }} - /> -

- Word 문서(.doc, .docx) 파일만 업로드 가능합니다. -

-
- - {/* ContractDocuments 컴포넌트 사용 */} - {/*
- - -
*/} - - {uploadedFile && ( -
-
- - 업로드 완료 -
-

{uploadedFile.name}

-
- )} -
-
-
- -
- - -
-
- - {/* 4단계: PDF 미리보기 */} - @@ -1168,7 +1049,7 @@ export function ContractApprovalRequestDialog({
- - -
-
- - {/* 3단계: PDF 미리보기 */} - @@ -870,7 +758,7 @@ export function ContractReviewRequestDialog({
- - - - setForm(prev => ({ ...prev, startDate: date }))} - initialFocus - /> - - + + setForm(prev => ({ ...prev, startDate: e.target.value }))} + min="1900-01-01" + max="2100-12-31" + />
- - - - - - - setForm(prev => ({ ...prev, endDate: date }))} - initialFocus - /> - - + + setForm(prev => ({ ...prev, endDate: e.target.value }))} + min="1900-01-01" + max="2100-12-31" + />
- - - - - - - setForm(prev => ({ ...prev, validityEndDate: date }))} - initialFocus - /> - - + + setForm(prev => ({ ...prev, validityEndDate: e.target.value }))} + min="1900-01-01" + max="2100-12-31" + />
diff --git a/lib/general-contracts/main/general-contract-update-sheet.tsx b/lib/general-contracts/main/general-contract-update-sheet.tsx index 8df74beb..02bde6c9 100644 --- a/lib/general-contracts/main/general-contract-update-sheet.tsx +++ b/lib/general-contracts/main/general-contract-update-sheet.tsx @@ -116,14 +116,23 @@ export function GeneralContractUpdateSheet({ React.useEffect(() => { if (contract) { console.log("Loading contract data:", contract) + + // 날짜 포맷팅 헬퍼 (YYYY-MM-DD) + const formatDateValue = (dateStr: string | null | undefined) => { + if (!dateStr) return "" + // KST 기준 날짜 변환 (입찰 로직과 동일) + const date = new Date(dateStr) + return new Date(date.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10) + } + const formData = { category: contract.category || "", type: contract.type || "", executionMethod: contract.executionMethod || "", name: contract.name || "", - startDate: contract.startDate || "", - endDate: contract.endDate || "", - validityEndDate: contract.validityEndDate || "", + startDate: formatDateValue(contract.startDate), + endDate: formatDateValue(contract.endDate), + validityEndDate: formatDateValue(contract.validityEndDate), contractScope: contract.contractScope || "", notes: contract.notes || "", linkedRfqOrItb: contract.linkedRfqOrItb || "", diff --git a/lib/general-contracts/main/general-contracts-table-columns.tsx b/lib/general-contracts/main/general-contracts-table-columns.tsx index 0b3143fe..c43bb383 100644 --- a/lib/general-contracts/main/general-contracts-table-columns.tsx +++ b/lib/general-contracts/main/general-contracts-table-columns.tsx @@ -368,14 +368,27 @@ export function getGeneralContractsColumns({ setRowAction }: GetColumnsProps): C if (!startDate || !endDate) return - + // UI 표시용 KST 변환 (YYYY-MM-DD) + const formatKstDate = (d: string | Date) => { + const date = new Date(d) + return new Date(date.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10) + } + + const formattedStart = formatKstDate(startDate) + const formattedEnd = formatKstDate(endDate) + const now = new Date() - const isActive = now >= new Date(startDate) && now <= new Date(endDate) - const isExpired = now > new Date(endDate) + const startObj = new Date(startDate) + const endObj = new Date(endDate) + + // 종료일의 경우 23:59:59까지 유효하다고 가정하거나, 단순히 날짜 비교 + const isActive = now >= startObj && now <= new Date(endObj.getTime() + 24 * 60 * 60 * 1000 - 1) + const isExpired = now > new Date(endObj.getTime() + 24 * 60 * 60 * 1000 - 1) return (
- {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")} + {formattedStart} ~ {formattedEnd}
{isActive && ( 진행중 diff --git a/lib/general-contracts/service.ts b/lib/general-contracts/service.ts index 991616d9..3f3dc8de 100644 --- a/lib/general-contracts/service.ts +++ b/lib/general-contracts/service.ts @@ -8,6 +8,7 @@ import { promises as fs } from 'fs' import { generalContracts, generalContractItems, generalContractAttachments } from '@/db/schema/generalContract' import { contracts, contractItems, contractEnvelopes, contractSigners } from '@/db/schema/contract' import { basicContract, basicContractTemplates } from '@/db/schema/basicContractDocumnet' +import { generalContractTemplates } from '@/db/schema' import { vendors } from '@/db/schema/vendors' import { users, roles, userRoles } from '@/db/schema/users' import { projects } from '@/db/schema/projects' @@ -2728,4 +2729,32 @@ export async function confirmContractReview( console.error('당사 검토 확정 오류:', error) throw error } +} + +// 계약 유형에 맞는 최신 템플릿 조회 +export async function getContractTemplateByContractType(contractType: string) { + try { + // 1. 정확한 타입 매칭 시도 + const templates = await db + .select() + .from(generalContractTemplates) + .where( + and( + eq(generalContractTemplates.contractTemplateType, contractType), + eq(generalContractTemplates.status, 'ACTIVE') + ) + ) + .orderBy(desc(generalContractTemplates.revision)) // 최신 리비전 우선 + .limit(1) + + if (templates.length > 0) { + return { success: true, template: templates[0] } + } + + // 2. 매칭되는 템플릿이 없을 경우 (필요 시 로직 추가) + return { success: false, error: '해당 계약 유형에 맞는 템플릿을 찾을 수 없습니다.' } + } catch (error) { + console.error('템플릿 조회 오류:', error) + return { success: false, error: '템플릿 조회 중 오류가 발생했습니다.' } + } } \ No newline at end of file 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 + 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 +} -- cgit v1.2.3