import ExcelJS from "exceljs" import { PRItemInfo } from "@/components/bidding/manage/bidding-items-editor" import { getProjectIdByCodeAndName } from "./project-utils" import { decryptWithServerAction } from "@/components/drm/drmUtils" export interface ImportBiddingItemsResult { success: boolean items: PRItemInfo[] errors: string[] } /** * Excel 파일에서 입찰품목 데이터 파싱 */ export async function importBiddingItemsFromExcel( file: File ): Promise { const errors: string[] = [] const items: PRItemInfo[] = [] try { const workbook = new ExcelJS.Workbook() // DRM 해제 후 ArrayBuffer 획득 (DRM 서버 미연결 시 원본 반환) const arrayBuffer = await decryptWithServerAction(file) 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 = {} 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 파일 파싱 중 오류가 발생했습니다.", ], } } }