diff options
| author | joonhoekim <26rote@gmail.com> | 2025-12-04 21:02:10 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-12-04 21:02:10 +0900 |
| commit | 240f4f31b3b6ff6a46436978fb988588a1972721 (patch) | |
| tree | dbf81b022d861cb077e84a10b913c26fd064db8b /lib/bidding/manage/import-bidding-items-from-excel.ts | |
| parent | 5699e866201566366981ae8399a835fc7fa9fa47 (diff) | |
| parent | ae211e5b9d9bf8e1566b78a85ec4522360833ea9 (diff) | |
(김준회) Merge branch 'dujinkim' of https://github.com/DTS-Development/SHI_EVCP into dujinkim
Diffstat (limited to 'lib/bidding/manage/import-bidding-items-from-excel.ts')
| -rw-r--r-- | lib/bidding/manage/import-bidding-items-from-excel.ts | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/lib/bidding/manage/import-bidding-items-from-excel.ts b/lib/bidding/manage/import-bidding-items-from-excel.ts new file mode 100644 index 00000000..2e0dfe33 --- /dev/null +++ b/lib/bidding/manage/import-bidding-items-from-excel.ts @@ -0,0 +1,271 @@ +import ExcelJS from "exceljs" +import { PRItemInfo } from "@/components/bidding/manage/bidding-items-editor" +import { getProjectIdByCodeAndName } from "./project-utils" + +export interface ImportBiddingItemsResult { + success: boolean + items: PRItemInfo[] + errors: string[] +} + +/** + * Excel 파일에서 입찰품목 데이터 파싱 + */ +export async function importBiddingItemsFromExcel( + file: File +): Promise<ImportBiddingItemsResult> { + const errors: string[] = [] + const items: PRItemInfo[] = [] + + try { + const workbook = new ExcelJS.Workbook() + const arrayBuffer = await file.arrayBuffer() + await workbook.xlsx.load(arrayBuffer) + + const worksheet = workbook.worksheets[0] + if (!worksheet) { + return { + success: false, + items: [], + errors: ["Excel 파일에 시트가 없습니다."], + } + } + + // 헤더 행 읽기 (첫 번째 행) + const headerRow = worksheet.getRow(1) + const headerValues = headerRow.values as ExcelJS.CellValue[] + + // 헤더 매핑 생성 + const headerMap: Record<string, number> = {} + const expectedHeaders = [ + "프로젝트코드", + "프로젝트명", + "자재그룹코드", + "자재그룹명", + "자재코드", + "자재명", + "수량", + "수량단위", + "중량", + "중량단위", + "납품요청일", + "가격단위", + "구매단위", + "자재순중량", + "내정단가", + "내정금액", + "내정통화", + "예산금액", + "예산통화", + "실적금액", + "실적통화", + "WBS코드", + "WBS명", + "코스트센터코드", + "코스트센터명", + "GL계정코드", + "GL계정명", + "PR번호", + ] + + // 헤더 인덱스 매핑 + for (let i = 1; i < headerValues.length; i++) { + const headerValue = String(headerValues[i] || "").trim() + if (headerValue && expectedHeaders.includes(headerValue)) { + headerMap[headerValue] = i + } + } + + // 필수 헤더 확인 + const requiredHeaders = ["자재그룹코드", "자재그룹명"] + const missingHeaders = requiredHeaders.filter( + (h) => !headerMap[h] + ) + if (missingHeaders.length > 0) { + errors.push( + `필수 컬럼이 없습니다: ${missingHeaders.join(", ")}` + ) + } + + // 데이터 행 읽기 (2번째 행부터) + for (let rowIndex = 2; rowIndex <= worksheet.rowCount; rowIndex++) { + const row = worksheet.getRow(rowIndex) + const rowValues = row.values as ExcelJS.CellValue[] + + // 빈 행 건너뛰기 + if (rowValues.every((val) => !val || String(val).trim() === "")) { + continue + } + + // 셀 값 추출 헬퍼 + const getCellValue = (headerName: string): string => { + const colIndex = headerMap[headerName] + if (!colIndex) return "" + const value = rowValues[colIndex] + if (value == null) return "" + + // ExcelJS 객체 처리 + if (typeof value === "object" && "text" in value) { + return String((value as any).text || "") + } + + // 날짜 처리 + if (value instanceof Date) { + return value.toISOString().split("T")[0] + } + + return String(value).trim() + } + + // 필수값 검증 + const materialGroupNumber = getCellValue("자재그룹코드") + const materialGroupInfo = getCellValue("자재그룹명") + + if (!materialGroupNumber || !materialGroupInfo) { + errors.push( + `${rowIndex}번 행: 자재그룹코드와 자재그룹명은 필수입니다.` + ) + continue + } + + // 수량 또는 중량 검증 + const quantity = getCellValue("수량") + const totalWeight = getCellValue("중량") + const quantityUnit = getCellValue("수량단위") + const weightUnit = getCellValue("중량단위") + + if (!quantity && !totalWeight) { + errors.push( + `${rowIndex}번 행: 수량 또는 중량 중 하나는 필수입니다.` + ) + continue + } + + if (quantity && !quantityUnit) { + errors.push( + `${rowIndex}번 행: 수량이 있으면 수량단위가 필수입니다.` + ) + continue + } + + if (totalWeight && !weightUnit) { + errors.push( + `${rowIndex}번 행: 중량이 있으면 중량단위가 필수입니다.` + ) + continue + } + + // 납품요청일 검증 + const requestedDeliveryDate = getCellValue("납품요청일") + if (!requestedDeliveryDate) { + errors.push( + `${rowIndex}번 행: 납품요청일은 필수입니다.` + ) + continue + } + + // 날짜 형식 검증 + const dateRegex = /^\d{4}-\d{2}-\d{2}$/ + if (requestedDeliveryDate && !dateRegex.test(requestedDeliveryDate)) { + errors.push( + `${rowIndex}번 행: 납품요청일 형식이 올바르지 않습니다. (YYYY-MM-DD 형식)` + ) + continue + } + + // 내정단가 검증 (필수) + const targetUnitPrice = getCellValue("내정단가") + if (!targetUnitPrice || parseFloat(targetUnitPrice.replace(/,/g, "")) <= 0) { + errors.push( + `${rowIndex}번 행: 내정단가는 필수이며 0보다 커야 합니다.` + ) + continue + } + + // 숫자 값 정리 (콤마 제거) + const cleanNumber = (value: string): string => { + return value.replace(/,/g, "").trim() + } + + // 프로젝트 ID 조회 (프로젝트코드와 프로젝트명으로) + const projectCode = getCellValue("프로젝트코드") + const projectName = getCellValue("프로젝트명") + let projectId: number | null = null + + if (projectCode && projectName) { + projectId = await getProjectIdByCodeAndName(projectCode, projectName) + if (!projectId) { + errors.push( + `${rowIndex}번 행: 프로젝트코드 "${projectCode}"와 프로젝트명 "${projectName}"에 해당하는 프로젝트를 찾을 수 없습니다.` + ) + // 프로젝트를 찾지 못해도 계속 진행 (경고만 표시) + } + } + + // PRItemInfo 객체 생성 + const item: PRItemInfo = { + id: -(rowIndex - 1), // 임시 ID (음수) + prNumber: getCellValue("PR번호") || null, + projectId: projectId, + projectInfo: projectName || null, + shi: null, + quantity: quantity ? cleanNumber(quantity) : null, + quantityUnit: quantityUnit || null, + totalWeight: totalWeight ? cleanNumber(totalWeight) : null, + weightUnit: weightUnit || null, + materialDescription: null, + hasSpecDocument: false, + requestedDeliveryDate: requestedDeliveryDate || null, + isRepresentative: false, + annualUnitPrice: null, + currency: "KRW", + materialGroupNumber: materialGroupNumber || null, + materialGroupInfo: materialGroupInfo || null, + materialNumber: getCellValue("자재코드") || null, + materialInfo: getCellValue("자재명") || null, + priceUnit: getCellValue("가격단위") || "1", + purchaseUnit: getCellValue("구매단위") || "EA", + materialWeight: getCellValue("자재순중량") || null, + wbsCode: getCellValue("WBS코드") || null, + wbsName: getCellValue("WBS명") || null, + costCenterCode: getCellValue("코스트센터코드") || null, + costCenterName: getCellValue("코스트센터명") || null, + glAccountCode: getCellValue("GL계정코드") || null, + glAccountName: getCellValue("GL계정명") || null, + targetUnitPrice: cleanNumber(targetUnitPrice) || null, + targetAmount: getCellValue("내정금액") + ? cleanNumber(getCellValue("내정금액")) + : null, + targetCurrency: getCellValue("내정통화") || "KRW", + budgetAmount: getCellValue("예산금액") + ? cleanNumber(getCellValue("예산금액")) + : null, + budgetCurrency: getCellValue("예산통화") || "KRW", + actualAmount: getCellValue("실적금액") + ? cleanNumber(getCellValue("실적금액")) + : null, + actualCurrency: getCellValue("실적통화") || "KRW", + } + + items.push(item) + } + + return { + success: errors.length === 0, + items, + errors, + } + } catch (error) { + console.error("Excel import error:", error) + return { + success: false, + items: [], + errors: [ + error instanceof Error + ? error.message + : "Excel 파일 파싱 중 오류가 발생했습니다.", + ], + } + } +} + |
