summaryrefslogtreecommitdiff
path: root/lib/bidding/manage/import-bidding-items-from-excel.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-12-04 09:04:09 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-12-04 09:04:09 +0000
commit25749225689c3934bc10ad1e8285e13020b61282 (patch)
tree0c96af8f89b08715589a7ec6d2b19e025215b98f /lib/bidding/manage/import-bidding-items-from-excel.ts
parent0f3954bf57e65caef7b7dd14ea5fccb63fdb2bef (diff)
(최겸)구매 입찰, 계약 수정
Diffstat (limited to 'lib/bidding/manage/import-bidding-items-from-excel.ts')
-rw-r--r--lib/bidding/manage/import-bidding-items-from-excel.ts271
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 파일 파싱 중 오류가 발생했습니다.",
+ ],
+ }
+ }
+}
+