1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
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
}
|