From 0c791d21d63eaf7c6be9feccd55885b663db29bc Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 12 May 2026 18:02:15 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EA=B4=80=EB=A6=AC>E-BOM=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=E2=80=94=20XLSX=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=ED=8F=90=EA=B8=B0=20=ED=9B=84=20CSV(11=EC=BB=AC=EB=9F=BC=C2=B7?= =?UTF-8?q?=EC=88=98=EC=A4=80=20=EA=B8=B0=EB=B0=98)=EB=A1=9C=20=EC=9E=AC?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 운영판 wace 재확인 결과 BOM 등록은 XLSX가 아니라 CSV 가 진짜 입력 포맷이었음. 근거: · openBomReportExcelImportPopUp.jsp : 제목 "PART 및 구조등록 CSV upload", Drop Zone "Drag & Drop CSV 템플릿", fnc_setFileDropZone(..., "csv") 로 CSV 만 허용 · /partMng/parsingExcelFile.do 가 .csv 분기에서 별도 함수 parsingCsvFile() 호출 · CSV 11컬럼 : 수준 / 품번 / 품명 / 수량 / 항목수량 / 재료 / 열처리경도 / 열처리방법 / 표면처리 / 공급업체(MAKER) / 범주이름(PART_TYPE) · CSV 는 PARENT_PART_NO 컬럼이 없고 "수준" 으로 부모-자식 자동 결정 · 숫자("1","2","3"): 깊이 D 의 부모 = D-1 의 최신 품번 · 점 표기법("1","1.1","1.4.1"): 마지막 점 이전 수준이 부모 backend (devBomExcelImportService.ts): · XLSX(xlsx 라이브러리) 파싱 → CSV(iconv-lite) 파싱으로 완전 재작성 · 인코딩 자동 감지 1:1 — CP949 → UTF-8 → EUC-KR → MS949 순서, � 카운트로 베스트 선택, UTF-8 BOM() 자동 제거 · CSV 영문 범주명 자동 변환 — Assy/ASSY→조립품, Buy/BUY→구매품, Make/MAKE→부품 · 범주별 ACCTFG/ODRFG 자동 설정 — 조립품(0001813)·부품(0001812) → ACCTFG=4·ODRFG=1, 구매품(0000063) → ACCTFG=7·ODRFG=0 · 기본값 일괄 — UNIT_DC=0001400(EA) · UNITMANG_DC=0001400 · UNITCHNG_NB=1 · LOT_FG=1 · USE_YN=1 · QC_FG=0 · SETITEM_FG=0 · REQ_FG=0 · 검증 1:1 — rowIndex > 2 에서만 모품번 자품번 목록 존재 검사, NOTE 누적 · 저장 로직(savePartBomMaster)은 동일 유지 — 자식 PART updatePartInfoFromCsv UPDATE / insertpartInfo INSERT, 부모 PART 존재 시 lookup·없으면 "" (INSERT 안 함), bom_part_qty INSERT 시 ACCTFG/ODRFG/UNIT_DC/UNITCHNG_NB 등 모든 컬럼 동기 frontend: · BomExcelRow → BomCsvRow (LEVEL 컬럼 + ACCTFG/ODRFG/UNIT_DC/UNITCHNG_NB/LOT_FG/USE_YN/ QC_FG/SETITEM_FG/REQ_FG 자동 채움 필드 추가). BomExcelRow 는 호환 type alias 로 보존 · BomReportExcelImportDialog : 제목 "PART 및 구조등록 CSV upload", accept=".csv", Drop Zone 안내 텍스트 변경, 그리드 컬럼 — 결과/수준/모품번(자동)/품번/품명/수량/항목수량/재료/ 열처리경도/열처리방법/표면처리/공급업체/범주/계정구분(자동)/조달구분(자동) · 파싱 결과의 encoding 정보 표시 (CP949/UTF-8 등) 정적 자산: · 운영 BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx 제거 (운영판도 실제 사용 안 함) · 신규 BOM_REPORT_CSV_IMPORT_TEMPLATE.csv 추가 (11컬럼 헤더 + 4행 샘플) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/services/devBomExcelImportService.ts | 571 +++++++++++------- .../BomReportExcelImportDialog.tsx | 106 ++-- frontend/lib/api/devBom.ts | 35 +- .../BOM_REPORT_CSV_IMPORT_TEMPLATE.csv | 6 + .../BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx | Bin 92438 -> 0 bytes 5 files changed, 453 insertions(+), 265 deletions(-) create mode 100644 frontend/public/templates/BOM_REPORT_CSV_IMPORT_TEMPLATE.csv delete mode 100644 frontend/public/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx diff --git a/backend-node/src/services/devBomExcelImportService.ts b/backend-node/src/services/devBomExcelImportService.ts index 9f6d0a6a..b118aced 100644 --- a/backend-node/src/services/devBomExcelImportService.ts +++ b/backend-node/src/services/devBomExcelImportService.ts @@ -1,68 +1,170 @@ // ============================================================ -// 개발관리 BOM Report Excel Import 서비스 — wace_plm PartMngService 1:1 +// 개발관리 BOM Report CSV Import 서비스 — wace_plm PartMngService.parsingCsvFile 1:1 // -// 원본 흐름 (wace partMng/openBomReportExcelImportPopUp.jsp): -// 1) /partMng/parsingExcelFile.do → 업로드 파일 파싱 + 검증 (parsingExcelFile) -// 2) /partMng/checkDuplicatePartNo.do → PART_BOM_REPORT 헤더 PART_NO 중복 (수정 중인 자신 제외) -// 3) /partMng/partBomApplySave.do → savePartBomMaster() -// 3-1) 헤더 INSERT(신규) / UPDATE+자식트리 DELETE+STATUS reset(수정) -// 3-2) 행마다 (wace partBomMaster 1:1): -// ㄱ. partMng.getPartObjid(PART_NO, IS_LAST=1) -// → 있음: part_objid 재사용 + partMng.updatePartInfoFromCsv 로 기존 row UPDATE -// → 없음: createObjId + partMng.insertpartInfo (part_mng 신규 INSERT) -// ㄴ. partMng.getPartObjid(PARENT_PART_NO, IS_LAST=1) -// → 있음: parent_part_objid 사용 -// → 없음: parent_part_objid = "" (INSERT 절대 안 함 — wace 원본 5359-5361) -// ㄷ. partMng.getBomPartQtyObjid(PARENT_PART_NO, BOM_REPORT_OBJID) -// → bom_part_qty 부모행의 CHILD_OBJID 를 parent_objid 로 사용 -// ㄹ. partMng.relatePartInfo → bom_part_qty INSERT +// 운영판 흐름 (wace partMng/openBomReportExcelImportPopUp.jsp): +// · Drop Zone: "Drag & Drop CSV 템플릿" + fnc_setFileDropZone(..., "csv") → CSV 전용 +// · /partMng/parsingExcelFile.do 의 .csv 분기에서 parsingCsvFile() 호출 +// · /partMng/partBomApplySave.do 의 savePartBomMaster() 로 저장 // -// 검증 (wace parsingExcelFile 1:1): -// · 자품번 필수 / 엑셀 내 중복 -// · 모품번이 자품번 목록(allPartNumbers Set)에 존재해야 함 (1레벨 = 첫 데이터 행은 제외) -// · 품명 필수 -// · 수량 필수 + 숫자 + > 0 -// · PART_TYPE 코드명 → code_id (못 찾으면 NOTE "부품유형 확인") -// · PART_TYPE='0001788'(구매품표준) → part_mng.part_no 존재 검증 (NOTE "구매품표준 미등록") +// CSV 컬럼 (11개, 헤더 1줄 후 데이터): +// 0:수준 1:품번 2:품명 3:수량 4:항목수량 5:재료 6:열처리경도 7:열처리방법 +// 8:표면처리 9:공급업체(MAKER) 10:범주이름(PART_TYPE) +// (11번 이후 컬럼은 파싱하지 않음 — 범주이름에 따라 자동값 설정) // -// vexplor_rps 적응: -// · CUSTOMER_OBJID/CONTRACT_OBJID/UNIT_CODE 본 메뉴에서 입력받지 않음 → NULL -// · 운영판 BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx 10컬럼 그대로: -// 0:모품번 1:자품번 2:품명 3:수량 4:재질 5:사양(규격) 6:후처리 7:MAKER 8:부품유형 9:REMARK -// (자바 코드는 25컬럼까지 매핑하지만 실제 운영 템플릿은 10컬럼) +// 핵심: PARENT_PART_NO 컬럼이 없음. "수준(level)" 으로 부모-자식 자동 결정. +// · 숫자("1","2","3","4"): 깊이 D 행의 부모 = depthToPartNoMap[D-1] (마지막에 등록된 D-1 깊이 품번) +// · 점 표기법("1","1.1","1.4.1"): 마지막 점 이전이 부모 수준 → levelToPartNoMap[parentLevel] +// +// CSV 자동 변환: +// · Assy/ASSY → 조립품, Buy/BUY → 구매품, Make/MAKE → 부품 (영문 PART_TYPE → 한글) +// · 범주 → 계정구분/조달구분 자동 (조립품 0001813 / 부품 0001812 → ACCTFG=4, ODRFG=1; +// 구매품 0000063 → ACCTFG=7, ODRFG=0) +// · 기본값 일괄: UNIT_DC=0001400(EA), UNITMANG_DC=0001400, UNITCHNG_NB=1, +// LOT_FG=1(사용), USE_YN=1(사용), QC_FG=0(무검사), SETITEM_FG=0(부), REQ_FG=0(부) +// +// 검증 (wace 1:1): +// · 모품번 자품번 목록 존재 검증은 rowIndex > 2 일 때만 (1·2 레벨 면제) +// · PART_TYPE 매핑 실패: NOTE "부품유형 확인:..." +// · 결과 누적: emptyColCnt < 9 또는 NOTE 있으면 row 채택 +// +// 인코딩 자동 감지 (wace detectFileEncoding 1:1): +// CP949 → UTF-8 → EUC-KR → MS949 순서. 각각 디코딩 → 깨진 문자(�) 개수 세서 0이면 즉시 선택, +// 아니면 가장 적은 것. UTF-8 BOM(EF BB BF) 자동 제거. +// +// 저장 (savePartBomMaster 1:1): +// · 헤더 part_bom_report INSERT(신규) / DELETE 자식트리+UPDATE(수정) +// · 자식 PART: IS_LAST='1' 존재 시 updatePartInfoFromCsv UPDATE, 없으면 insertpartInfo INSERT +// · 부모 PART: 존재 시 lookup, 없으면 "" (절대 INSERT 안 함) +// · bom_part_qty INSERT (relatePartInfo) — 부모행 CHILD_OBJID 체인 // ============================================================ -import * as XLSX from "xlsx"; +import * as iconv from "iconv-lite"; import { PoolClient } from "pg"; import { getPool, transaction } from "../database/db"; import { logger } from "../utils/logger"; import { createObjId } from "../utils/objidUtil"; const CODE_PARENT_PART_TYPE = "0000062"; +const PART_TYPE_ASSEMBLY = "0001813"; // 조립품 +const PART_TYPE_PART = "0001812"; // 부품 +const PART_TYPE_BUY = "0000063"; // 구매품 -export interface BomExcelRow { +const DEFAULT_UNIT_DC = "0001400"; // EA +const DEFAULT_UNITMANG_DC = "0001400"; // EA +const DEFAULT_UNITCHNG_NB = "1"; +const DEFAULT_LOT_FG = "1"; // 사용 +const DEFAULT_USE_YN = "1"; // 사용 +const DEFAULT_QC_FG = "0"; // 무검사 +const DEFAULT_SETITEM_FG = "0"; // 부 +const DEFAULT_REQ_FG = "0"; // 부 + +export interface BomCsvRow { NOTE: string; - PARENT_PART_NO: string; + LEVEL: string; // 표시용 (CSV 수준) + PARENT_PART_NO: string; // 수준에서 계산된 부모 품번 PART_NO: string; PART_NAME: string; QTY: string; ITEM_QTY: string; MATERIAL: string; - SPEC: string; - POST_PROCESSING: string; + HEAT_TREATMENT_HARDNESS: string; + HEAT_TREATMENT_METHOD: string; + SURFACE_TREATMENT: string; MAKER: string; - PART_TYPE: string; - PART_TYPE_NAME?: string; - REMARK: string; + PART_TYPE: string; // code_id (자동 변환된 한글 → DB 조회 결과) + PART_TYPE_NAME: string; // 한글 (Assy→조립품 변환 적용) + ACCTFG: string; + ODRFG: string; + UNIT_DC: string; + UNITMANG_DC: string; + UNITCHNG_NB: string; + LOT_FG: string; + USE_YN: string; + QC_FG: string; + SETITEM_FG: string; + REQ_FG: string; } -function appendNote(r: BomExcelRow, msg: string) { +function appendNote(r: BomCsvRow, msg: string) { r.NOTE = r.NOTE ? `${r.NOTE} / ${msg}` : msg; } -function getCell(row: any[], idx: number): string { - const v = row?.[idx]; - if (v === undefined || v === null) return ""; - return String(v).trim(); + +// ─── 헬퍼 ─────────────────────────────────────────────────── + +// wace getCsvValue 1:1 — 따옴표 제거 + 빈 카운트 +function getCsvValue(values: string[], idx: number, emptyCounter: { count: number }): string { + if (idx >= values.length) { + emptyCounter.count++; + return ""; + } + let v = (values[idx] ?? "").trim(); + if (v.length > 1 && v.startsWith('"') && v.endsWith('"')) { + v = v.substring(1, v.length - 1); + } + if (!v) emptyCounter.count++; + return v; +} + +// wace getParentLevel 1:1: "1.4.1" → "1.4" / "1.8" → "1" / "1" → "" +function getParentLevel(level: string): string { + if (!level) return ""; + const lastDot = level.lastIndexOf("."); + return lastDot > 0 ? level.substring(0, lastDot) : ""; +} + +// CSV 영문 범주명 → 한글 (wace 1:1) +function normalizePartTypeName(raw: string): string { + const upper = raw.toUpperCase(); + if (upper === "ASSY") return "조립품"; + if (upper === "BUY") return "구매품"; + if (upper === "MAKE") return "부품"; + return raw; +} + +// 범주 code_id → ACCTFG/ODRFG 자동 매핑 (wace 1:1) +function autoAcctfgOdrfg(partTypeCode: string): { acctfg: string; odrfg: string } { + if (partTypeCode === PART_TYPE_ASSEMBLY || partTypeCode === PART_TYPE_PART) { + return { acctfg: "4", odrfg: "1" }; // 반제품/생산 + } + if (partTypeCode === PART_TYPE_BUY) { + return { acctfg: "7", odrfg: "0" }; // 비용/구매 + } + return { acctfg: "", odrfg: "" }; +} + +// 인코딩 자동 감지 (wace detectFileEncoding 1:1) +function detectAndDecode(buffer: Buffer): { text: string; encoding: string } { + const encodings = ["cp949", "utf-8", "euc-kr", "ms949"]; + let bestEncoding = "utf-8"; + let bestText = ""; + let minReplacement = Number.POSITIVE_INFINITY; + + for (const enc of encodings) { + try { + const decoded = iconv.decode(buffer, enc); + let replacementCount = 0; + const sample = decoded.substring(0, 4096); + for (let i = 0; i < sample.length; i++) { + if (sample.charCodeAt(i) === 0xFFFD) replacementCount++; + } + if (replacementCount === 0) { + return { text: stripBom(decoded), encoding: enc }; + } + if (replacementCount < minReplacement) { + minReplacement = replacementCount; + bestEncoding = enc; + bestText = decoded; + } + } catch { + continue; + } + } + return { text: stripBom(bestText), encoding: bestEncoding }; +} + +function stripBom(s: string): string { + if (s.length > 0 && s.charCodeAt(0) === 0xFEFF) return s.substring(1); + return s; } async function fetchPartTypeMap(client: PoolClient): Promise> { @@ -76,142 +178,151 @@ async function fetchPartTypeMap(client: PoolClient): Promise { - const wb = XLSX.read(buffer, { type: "buffer" }); - const sheet = wb.Sheets[wb.SheetNames[0]]; - const raw: any[][] = XLSX.utils.sheet_to_json(sheet, { header: 1, raw: false, defval: "" }); + const { text, encoding } = detectAndDecode(buffer); + const lines = text.split(/\r\n|\r|\n/); - // wace 1: 모든 자품번을 먼저 Set 으로 수집 (모품번 검증용) + // 1차 스캔: 모든 자품번 + 수준→품번 매핑 (wace 1:1) const allPartNumbers = new Set(); - for (let i = 2; i < raw.length; i++) { - const partNo = getCell(raw[i], 1); // column 1 = 자품번 (운영 템플릿) - if (partNo) allPartNumbers.add(partNo); + const allRows: string[][] = []; + const levelToPartNoMap = new Map(); + + for (let i = 0; i < lines.length; i++) { + if (lines[i] === undefined) continue; + const values = lines[i].split(","); + allRows.push(values); + if (i > 0 && values.length > 1) { + let level = (values[0] ?? "").trim(); + let partNo = (values[1] ?? "").trim(); + if (level.length > 1 && level.startsWith('"') && level.endsWith('"')) level = level.substring(1, level.length - 1); + if (partNo.length > 1 && partNo.startsWith('"') && partNo.endsWith('"')) partNo = partNo.substring(1, partNo.length - 1); + if (partNo) { + allPartNumbers.add(partNo); + if (level) levelToPartNoMap.set(level, partNo); + } + } } - const result: BomExcelRow[] = []; + const result: BomCsvRow[] = []; let firstLevel: { part_no: string; part_name: string } | null = null; + // 2차 스캔: 행 파싱 + 부모 결정 + 검증 + const currentDepthPartNoMap = new Map(); // 숫자 수준용 (wace 1:1) + const client = await getPool().connect(); try { const partTypeMap = await fetchPartTypeMap(client); - const partNoSeenInFile = new Map(); - let emptyRowCnt = 0; - let dataRowIndex = 0; // 데이터 행 카운터 (1부터 — wace rowIndex 와 다름, 의미는 동일) - for (let i = 2; i < raw.length; i++) { - const row = raw[i]; - if (!row) continue; + let rowIndex = 0; + for (const values of allRows) { + if (rowIndex === 0) { rowIndex++; continue; } // 헤더 skip + if (values.length < 11) { rowIndex++; continue; } // wace: 최소 11컬럼 (수준 포함) - const parentPartNo = getCell(row, 0); - const partNo = getCell(row, 1); - const partName = getCell(row, 2); - const qty = getCell(row, 3); - const material = getCell(row, 4); - const spec = getCell(row, 5); - const postProc = getCell(row, 6); - const maker = getCell(row, 7); - const partTypeName = getCell(row, 8); - const remark = getCell(row, 9); + const emptyCounter = { count: 0 }; + let colIndex = 0; + const level = getCsvValue(values, colIndex++, emptyCounter); + const partNo = getCsvValue(values, colIndex++, emptyCounter); + const partName = getCsvValue(values, colIndex++, emptyCounter); + const qty = getCsvValue(values, colIndex++, emptyCounter); + const itemQty = getCsvValue(values, colIndex++, emptyCounter); + const material = getCsvValue(values, colIndex++, emptyCounter); + const heatHardness = getCsvValue(values, colIndex++, emptyCounter); + const heatMethod = getCsvValue(values, colIndex++, emptyCounter); + const surfaceTreatment = getCsvValue(values, colIndex++, emptyCounter); + const supplier = getCsvValue(values, colIndex++, emptyCounter); + const partTypeRaw = getCsvValue(values, colIndex++, emptyCounter); - // wace: PART_NO+PART_NAME 둘 다 빈 행은 스킵 (3줄 연속이면 break) - if (!partNo && !partName) { - emptyRowCnt++; - if (emptyRowCnt > 3) break; - continue; + // 수준 → 부모 결정 (wace 1:1) + let parentPartNo = ""; + if (level) { + const asInt = Number(level); + if (Number.isInteger(asInt) && /^\d+$/.test(level)) { + // 숫자 수준: 깊이 D 행 → D-1 깊이의 최신 품번이 부모 + const currentDepth = asInt; + if (partNo) currentDepthPartNoMap.set(currentDepth, partNo); + if (currentDepth > 1) { + parentPartNo = currentDepthPartNoMap.get(currentDepth - 1) ?? ""; + } + } else { + // 점 표기법: 마지막 점 이전 = 부모 수준 + const parentLevel = getParentLevel(level); + if (parentLevel) parentPartNo = levelToPartNoMap.get(parentLevel) ?? ""; + } } - emptyRowCnt = 0; - dataRowIndex++; - const cur: BomExcelRow = { - NOTE: "", + let noteMsg = ""; + + // 모품번 자품번 목록 존재 검증 (wace: rowIndex > 2 일 때만) + if (parentPartNo && rowIndex > 2 && !allPartNumbers.has(parentPartNo)) { + noteMsg += `모품번 미존재: ${parentPartNo} ; `; + } + + // PART_TYPE 처리 (wace 1:1) + const partTypeNormalized = partTypeRaw ? normalizePartTypeName(partTypeRaw) : ""; + let partTypeCode = ""; + if (partTypeNormalized) { + const hit = partTypeMap.get(partTypeNormalized.toUpperCase()); + if (hit) { + partTypeCode = hit.code_id; + } else if (rowIndex > 2) { + noteMsg += `부품유형 확인: ${partTypeRaw} ; `; + } + } + + const { acctfg, odrfg } = autoAcctfgOdrfg(partTypeCode); + + const row: BomCsvRow = { + NOTE: noteMsg.trim().replace(/\s*;\s*$/, ""), + LEVEL: level, PARENT_PART_NO: parentPartNo, PART_NO: partNo, PART_NAME: partName, QTY: qty, - ITEM_QTY: qty, // wace: ITEM_QTY 컬럼 없으면 QTY 와 동일 (savePartBomMaster 에서도 QTY 로 채움) + ITEM_QTY: itemQty || qty, MATERIAL: material, - SPEC: spec, - POST_PROCESSING: postProc, - MAKER: maker, - PART_TYPE: "", - REMARK: remark, + HEAT_TREATMENT_HARDNESS: heatHardness, + HEAT_TREATMENT_METHOD: heatMethod, + SURFACE_TREATMENT: surfaceTreatment, + MAKER: supplier, + PART_TYPE: partTypeCode, + PART_TYPE_NAME: partTypeNormalized, + ACCTFG: acctfg, + ODRFG: odrfg, + UNIT_DC: DEFAULT_UNIT_DC, + UNITMANG_DC: DEFAULT_UNITMANG_DC, + UNITCHNG_NB: DEFAULT_UNITCHNG_NB, + LOT_FG: DEFAULT_LOT_FG, + USE_YN: DEFAULT_USE_YN, + QC_FG: DEFAULT_QC_FG, + SETITEM_FG: DEFAULT_SETITEM_FG, + REQ_FG: DEFAULT_REQ_FG, }; - // (1) 자품번 필수 - if (!partNo) { - appendNote(cur, "필수입력 - 품번"); - } else { - if (partNoSeenInFile.has(partNo)) { - appendNote(cur, `품번 중복: ${partNo}`); - } else { - partNoSeenInFile.set(partNo, dataRowIndex); - } + // wace: NOTE 있거나 emptyColCnt < 9 면 결과 채택 + if (row.NOTE || emptyCounter.count < 9) { + result.push(row); } - // (2) 모품번 검증 (wace 1:1): 첫 데이터 행은 제외, 그 외는 자품번 목록에 있어야 함 - if (!parentPartNo && dataRowIndex > 1) { - appendNote(cur, "필수입력 - 모품번"); - } else if (parentPartNo && !allPartNumbers.has(parentPartNo)) { - appendNote(cur, `모품번이 자품번 목록에 없습니다: ${parentPartNo}`); - } - - // (3) 품명 필수 - if (!partName) appendNote(cur, "필수입력 - 품명"); - - // (4) 수량 필수 + 숫자 + > 0 (wace 1:1) - if (!qty) { - appendNote(cur, "필수입력 - 수량"); - } else { - const qtyValue = Number(qty); - if (!Number.isFinite(qtyValue)) { - appendNote(cur, `수량은 숫자여야 합니다: ${qty}`); - } else if (qtyValue <= 0) { - appendNote(cur, `수량은 0보다 커야 합니다: ${qty}`); - } - } - - // (5) PART_TYPE 코드 변환 + 구매품표준 검증 - if (partTypeName) { - const hit = partTypeMap.get(partTypeName.toUpperCase()); - if (hit) { - cur.PART_TYPE = hit.code_id; - cur.PART_TYPE_NAME = hit.code_name; - - // wace: rowIndex > 2 인 경우만 구매품표준 검증 (1~2 레벨은 면제) - if (hit.code_name === "구매품표준" && dataRowIndex > 2 && partNo) { - const exist = await client.query( - `SELECT 1 FROM PART_MNG WHERE PART_NO = $1 LIMIT 1`, - [partNo] - ); - if ((exist.rowCount ?? 0) === 0) { - appendNote(cur, `품번에 해당하는 구매품표준이 없습니다: ${partNo}`); - } - } - } else { - cur.PART_TYPE = partTypeName; - appendNote(cur, `부품유형 확인: ${partTypeName}`); - } - } - - // (6) 1레벨 (모품번 없는 첫 행)을 헤더 자동 채움 용도로 캡쳐 + // 첫 1레벨(parent 없는 첫 행) → 헤더 자동 if (!firstLevel && !parentPartNo && partNo) { firstLevel = { part_no: partNo, part_name: partName }; } - result.push(cur); + rowIndex++; } } finally { client.release(); } const hasError = result.some((r) => r.NOTE); - return { rows: result, hasError, firstLevel }; + return { rows: result, hasError, firstLevel, encoding }; } // ─── 2) 헤더 part_no 중복 검사 (wace checkDuplicatePartNo) ── @@ -226,52 +337,79 @@ export async function checkDuplicateBomPartNo(partNo: string, excludeObjid?: str return (r.rowCount ?? 0) > 0; } -// ─── 3) E-BOM 복사: 기존 BOM_PART_QTY → BomExcelRow[] ────── +// ─── 3) E-BOM 복사: 기존 BOM_PART_QTY → BomCsvRow[] ──────── -export async function copyBomForGrid(sourceObjid: string): Promise { +export async function copyBomForGrid(sourceObjid: string): Promise { const sql = ` - SELECT - Q.QTY, Q.ITEM_QTY, Q.SEQ, - PM.PART_NO AS PART_NO_REAL, - PM.PART_NAME AS PART_NAME, - PM.MATERIAL, PM.SPEC, PM.POST_PROCESSING, PM.MAKER, PM.REMARK, - PM.PART_TYPE, - CC.CODE_NAME AS PART_TYPE_NAME, - PM_PARENT.PART_NO AS PARENT_PART_NO_REAL - FROM BOM_PART_QTY Q - LEFT JOIN PART_MNG PM ON PM.OBJID::varchar = Q.PART_NO - LEFT JOIN PART_MNG PM_PARENT ON PM_PARENT.OBJID::varchar = Q.PARENT_PART_NO + WITH RECURSIVE TREE AS ( + SELECT BP.OBJID, BP.PARENT_OBJID, BP.CHILD_OBJID, BP.PART_NO, BP.PARENT_PART_NO, BP.QTY, BP.ITEM_QTY, + 1 AS LEV, ARRAY[BP.SEQ] AS PATH + FROM BOM_PART_QTY BP + WHERE BP.BOM_REPORT_OBJID = $1 + AND (BP.PARENT_OBJID IS NULL OR BP.PARENT_OBJID = '') + UNION ALL + SELECT B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID, B.PART_NO, B.PARENT_PART_NO, B.QTY, B.ITEM_QTY, + T.LEV + 1, T.PATH || B.SEQ + FROM BOM_PART_QTY B + JOIN TREE T ON B.PARENT_OBJID = T.CHILD_OBJID + WHERE B.BOM_REPORT_OBJID = $1 + ) + SELECT T.LEV, + PM.PART_NO AS PART_NO_REAL, + PM.PART_NAME AS PART_NAME, + T.QTY, T.ITEM_QTY, + PM.MATERIAL, PM.HEAT_TREATMENT_HARDNESS, PM.HEAT_TREATMENT_METHOD, PM.SURFACE_TREATMENT, + PM.MAKER, PM.PART_TYPE, + CC.CODE_NAME AS PART_TYPE_NAME, + PM_PARENT.PART_NO AS PARENT_PART_NO_REAL, + PM.ACCTFG, PM.ODRFG, + PM.UNIT_DC, PM.UNITMANG_DC, PM.UNITCHNG_NB, + PM.LOT_FG, PM.USE_YN, PM.QC_FG, PM.SETITEM_FG, PM.REQ_FG, + T.PATH + FROM TREE T + LEFT JOIN PART_MNG PM ON PM.OBJID::varchar = T.PART_NO + LEFT JOIN PART_MNG PM_PARENT ON PM_PARENT.OBJID::varchar = T.PARENT_PART_NO LEFT JOIN COMM_CODE CC ON CC.CODE_ID = PM.PART_TYPE - WHERE Q.BOM_REPORT_OBJID = $1 - ORDER BY Q.SEQ + ORDER BY T.PATH `; const r = await getPool().query(sql, [sourceObjid]); - return r.rows.map((row): BomExcelRow => ({ + return r.rows.map((row): BomCsvRow => ({ NOTE: "", + LEVEL: String(row.lev ?? ""), PARENT_PART_NO: row.parent_part_no_real ?? "", PART_NO: row.part_no_real ?? "", PART_NAME: row.part_name ?? "", QTY: row.qty != null ? String(row.qty) : "", ITEM_QTY: row.item_qty != null ? String(row.item_qty) : (row.qty != null ? String(row.qty) : ""), MATERIAL: row.material ?? "", - SPEC: row.spec ?? "", - POST_PROCESSING: row.post_processing ?? "", + HEAT_TREATMENT_HARDNESS: row.heat_treatment_hardness ?? "", + HEAT_TREATMENT_METHOD: row.heat_treatment_method ?? "", + SURFACE_TREATMENT: row.surface_treatment ?? "", MAKER: row.maker ?? "", PART_TYPE: row.part_type ?? "", PART_TYPE_NAME: row.part_type_name ?? "", - REMARK: row.remark ?? "", + ACCTFG: row.acctfg ?? "", + ODRFG: row.odrfg ?? "", + UNIT_DC: row.unit_dc ?? DEFAULT_UNIT_DC, + UNITMANG_DC: row.unitmang_dc ?? DEFAULT_UNITMANG_DC, + UNITCHNG_NB: row.unitchng_nb != null ? String(row.unitchng_nb) : DEFAULT_UNITCHNG_NB, + LOT_FG: row.lot_fg ?? DEFAULT_LOT_FG, + USE_YN: row.use_yn ?? DEFAULT_USE_YN, + QC_FG: row.qc_fg ?? DEFAULT_QC_FG, + SETITEM_FG: row.setitem_fg ?? DEFAULT_SETITEM_FG, + REQ_FG: row.req_fg ?? DEFAULT_REQ_FG, })); } // ─── 4) BOM 저장 (savePartBomMaster 1:1) ──────────────────── export interface BomSaveInput { - bomReportObjid?: string; // 비어있으면 신규 + bomReportObjid?: string; productCd: string; - partNo: string; // 헤더 품번 + partNo: string; partName: string; version?: string; - rows: BomExcelRow[]; + rows: BomCsvRow[]; } export interface BomSaveResult { @@ -295,7 +433,6 @@ export async function saveBomReport(userId: string, input: BomSaveInput): Promis await transaction(async (client: PoolClient) => { if (mode === "update") { - // wace: 기존 BOM 수정 시 자식 트리 DELETE + STATUS='N' reset (deleteBomPartQtyByBomObjid + resetBomReportStatus) await client.query(`DELETE FROM BOM_PART_QTY WHERE BOM_REPORT_OBJID = $1`, [bomReportObjid]); await client.query( `UPDATE PART_BOM_REPORT @@ -322,16 +459,14 @@ export async function saveBomReport(userId: string, input: BomSaveInput): Promis ); } - if (!input.rows || input.rows.length === 0) return; // wace: 빈 BOM 허용 (헤더만 생성) + if (!input.rows || input.rows.length === 0) return; - // 자식 PART_NO → part_mng.objid 캐시 (행 처리 후) - const childPartObjIdCache = new Map(); - // bom_part_qty 부모행의 CHILD_OBJID 캐시 (다음 자식들이 이 값을 PARENT_OBJID 로 사용) - const childBomObjIdByPartNo = new Map(); + const partObjIdCache = new Map(); // PART_NO → part_mng.objid (자식·부모 공용 캐시) + const childBomObjIdByPartNo = new Map(); // PART_NO → bom_part_qty.child_objid - // 자식 PART 처리: 있으면 UPDATE / 없으면 INSERT (wace 1:1) - async function upsertChildPart(r: BomExcelRow): Promise { - if (childPartObjIdCache.has(r.PART_NO)) return childPartObjIdCache.get(r.PART_NO)!; + // 자식 PART: 있으면 updatePartInfoFromCsv UPDATE, 없으면 insertpartInfo INSERT (wace 1:1) + async function upsertChildPart(r: BomCsvRow): Promise { + if (partObjIdCache.has(r.PART_NO)) return partObjIdCache.get(r.PART_NO)!; const exist = await client.query( `SELECT OBJID::varchar AS part_objid FROM PART_MNG WHERE PART_NO = $1 AND IS_LAST = '1' LIMIT 1`, @@ -339,76 +474,89 @@ export async function saveBomReport(userId: string, input: BomSaveInput): Promis ); if ((exist.rowCount ?? 0) > 0) { const id = exist.rows[0].part_objid; - // wace updatePartInfoFromCsv 1:1 (BOM Excel 에 있는 컬럼만 — 나머지는 그대로) - // 단, 원본 매퍼는 빈값이어도 UPDATE 함. RPS 에서는 BOM 엑셀에 없는 컬럼은 NULL로 덮어쓰지 않도록 COALESCE 처리. await client.query( `UPDATE PART_MNG SET - PART_NAME = $1, - SPEC = $2, - MATERIAL = $3, - REMARK = $4, - PART_TYPE = COALESCE(NULLIF($5, ''), PART_TYPE), - MAKER = $6, - POST_PROCESSING = $7, - EDIT_DATE = NOW() - WHERE OBJID = $8::numeric`, + PART_NAME = $1, + MATERIAL = $2, + HEAT_TREATMENT_HARDNESS = $3, + HEAT_TREATMENT_METHOD = $4, + SURFACE_TREATMENT = $5, + PART_TYPE = COALESCE(NULLIF($6, ''), PART_TYPE), + MAKER = $7, + ACCTFG = $8, + ODRFG = $9, + UNIT_DC = $10, + UNITMANG_DC = $11, + UNITCHNG_NB = COALESCE(NULLIF($12, ''), '0')::numeric, + LOT_FG = COALESCE(NULLIF($13, ''), LOT_FG), + USE_YN = COALESCE(NULLIF($14, ''), USE_YN), + QC_FG = COALESCE(NULLIF($15, ''), QC_FG), + SETITEM_FG = COALESCE(NULLIF($16, ''), SETITEM_FG), + REQ_FG = COALESCE(NULLIF($17, ''), REQ_FG), + EDIT_DATE = NOW() + WHERE OBJID = $18::numeric`, [ - r.PART_NAME ?? "", - r.SPEC ?? "", - r.MATERIAL ?? "", - r.REMARK ?? "", - r.PART_TYPE ?? "", - r.MAKER ?? "", - r.POST_PROCESSING ?? "", + r.PART_NAME, r.MATERIAL, + r.HEAT_TREATMENT_HARDNESS, r.HEAT_TREATMENT_METHOD, r.SURFACE_TREATMENT, + r.PART_TYPE, r.MAKER, + r.ACCTFG, r.ODRFG, + r.UNIT_DC, r.UNITMANG_DC, r.UNITCHNG_NB, + r.LOT_FG, r.USE_YN, r.QC_FG, r.SETITEM_FG, r.REQ_FG, id, ] ); updatedParts++; - childPartObjIdCache.set(r.PART_NO, id); + partObjIdCache.set(r.PART_NO, id); return id; } - // 신규 INSERT (insertpartInfo 1:1, BOM Excel 컬럼만 채움) const newId = createObjId(); await client.query( `INSERT INTO PART_MNG ( - OBJID, PART_NO, PART_NAME, SPEC, MATERIAL, REMARK, + OBJID, PART_NO, PART_NAME, MATERIAL, REMARK, STATUS, REG_DATE, WRITER, IS_LAST, - PART_TYPE, MAKER, POST_PROCESSING, + HEAT_TREATMENT_HARDNESS, HEAT_TREATMENT_METHOD, SURFACE_TREATMENT, + PART_TYPE, MAKER, + ACCTFG, ODRFG, UNIT_DC, UNITMANG_DC, UNITCHNG_NB, LOT_FG, USE_YN, QC_FG, SETITEM_FG, REQ_FG ) VALUES ( - $1::numeric, $2, $3, $4, $5, $6, - 'create', NOW(), $7, '1', - NULLIF($8, ''), $9, $10, - '0', '1', '0', '0', '0' + $1::numeric, $2, $3, $4, '', + 'create', NOW(), $5, '1', + $6, $7, $8, + NULLIF($9, ''), $10, + $11, $12, $13, $14, + COALESCE(NULLIF($15, ''), '0')::numeric, + COALESCE($16, '0'), COALESCE($17, '1'), COALESCE($18, '0'), + COALESCE($19, '0'), COALESCE($20, '0') )`, [ - newId, r.PART_NO, - r.PART_NAME ?? "", r.SPEC ?? "", r.MATERIAL ?? "", r.REMARK ?? "", + newId, r.PART_NO, r.PART_NAME, r.MATERIAL, userId, - r.PART_TYPE ?? "", r.MAKER ?? "", r.POST_PROCESSING ?? "", + r.HEAT_TREATMENT_HARDNESS, r.HEAT_TREATMENT_METHOD, r.SURFACE_TREATMENT, + r.PART_TYPE, r.MAKER, + r.ACCTFG, r.ODRFG, r.UNIT_DC, r.UNITMANG_DC, r.UNITCHNG_NB, + r.LOT_FG, r.USE_YN, r.QC_FG, r.SETITEM_FG, r.REQ_FG, ] ); insertedParts++; - childPartObjIdCache.set(r.PART_NO, newId); + partObjIdCache.set(r.PART_NO, newId); return newId; } - // 부모 PART 처리: 있으면 lookup, 없으면 "" (wace 1:1 — INSERT 절대 안 함) + // 부모 PART: 있으면 lookup, 없으면 "" (wace 1:1 — INSERT 절대 안 함) async function lookupParentPart(partNo: string): Promise { if (!partNo) return ""; - if (childPartObjIdCache.has(partNo)) return childPartObjIdCache.get(partNo)!; - + if (partObjIdCache.has(partNo)) return partObjIdCache.get(partNo)!; const exist = await client.query( `SELECT OBJID::varchar AS part_objid FROM PART_MNG WHERE PART_NO = $1 AND IS_LAST = '1' LIMIT 1`, [partNo] ); if ((exist.rowCount ?? 0) > 0) { const id = exist.rows[0].part_objid; - childPartObjIdCache.set(partNo, id); + partObjIdCache.set(partNo, id); return id; } - return ""; // wace 원본 5359-5361: 부모 part_mng 없으면 parent_part_no="" (INSERT 안 함) + return ""; } for (const r of input.rows) { @@ -416,15 +564,10 @@ export async function saveBomReport(userId: string, input: BomSaveInput): Promis const partObjid = await upsertChildPart(r); const parentPartObjid = await lookupParentPart(r.PARENT_PART_NO); + const parentBomObjid = r.PARENT_PART_NO ? (childBomObjIdByPartNo.get(r.PARENT_PART_NO) ?? "") : ""; + const newBomObjid = createObjId(); + const newChildObjid = createObjId(); - // wace: bom_part_qty 에서 부모 행의 CHILD_OBJID 조회 (PARENT_PART_NO + BOM_REPORT_OBJID). - // 행이 엑셀 순서대로 들어오므로 메모리 캐시면 동등 (자식이 부모보다 항상 뒤에 등장). - const parentBomObjid = r.PARENT_PART_NO ? (childBomObjIdByPartNo.get(r.PARENT_PART_NO) ?? "") : ""; - - const newBomObjid = createObjId(); - const newChildObjid = createObjId(); - - // wace relatePartInfo 1:1 — QTY/ITEM_QTY/QTY_TEMP 모두 COALESCE(NULLIF, '0')::numeric await client.query( `INSERT INTO BOM_PART_QTY ( BOM_REPORT_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID, @@ -453,7 +596,7 @@ export async function saveBomReport(userId: string, input: BomSaveInput): Promis } }); - logger.info("BOM Excel Import 저장 완료", { userId, bomReportObjid, mode, insertedParts, updatedParts, bomRows }); + logger.info("BOM CSV Import 저장 완료", { userId, bomReportObjid, mode, insertedParts, updatedParts, bomRows }); return { bomReportObjid, insertedParts, updatedParts, bomRows, mode }; } diff --git a/frontend/components/development/BomReportExcelImportDialog.tsx b/frontend/components/development/BomReportExcelImportDialog.tsx index e0f4d2fa..fae60795 100644 --- a/frontend/components/development/BomReportExcelImportDialog.tsx +++ b/frontend/components/development/BomReportExcelImportDialog.tsx @@ -1,11 +1,16 @@ "use client"; -// 개발관리 E-BOM 등록 Excel Import 다이얼로그 +// 개발관리 E-BOM 등록 CSV Import 다이얼로그 // wace partMng/openBomReportExcelImportPopUp.jsp 1:1 -// - 헤더: 제품구분 / 품번(readonly, 1레벨 자동) / 품명(readonly, 1레벨 자동) / Version -// - E-BOM 복사 (기존 BOM 선택 → 그리드 채움) -// - Drag&Drop 엑셀 → 백엔드 파싱 + 검증 (NOTE 누적) -// - 저장: part_bom_report 헤더 + bom_part_qty 트리. NOTE 있는 행이 1건이라도 있으면 차단. +// +// 운영판 흐름: +// · Drop Zone: Drag & Drop CSV 템플릿 (fnc_setFileDropZone(..., "csv")) +// · 파싱: parsingExcelFile.do 의 .csv 분기 → parsingCsvFile (수준 기반 부모 자동 매핑) +// · 저장: partBomApplySave.do → savePartBomMaster +// +// CSV 컬럼 (11개, 헤더 1줄 후 데이터): +// 0:수준 1:품번 2:품명 3:수량 4:항목수량 5:재료 6:열처리경도 7:열처리방법 +// 8:표면처리 9:공급업체(MAKER) 10:범주이름(PART_TYPE) import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { @@ -18,44 +23,58 @@ import { CommCodeSelect } from "@/components/common/CommCodeSelect"; import { Download, Upload, Save, Loader2, FileX, Copy } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; -import { devBomApi, BomExcelRow, BomCopySourceRow } from "@/lib/api/devBom"; +import { devBomApi, BomCsvRow, BomCopySourceRow } from "@/lib/api/devBom"; const PRODUCT_GROUP = "0000001"; -const TEMPLATE_DOWNLOAD_URL = "/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx"; +const TEMPLATE_DOWNLOAD_URL = "/templates/BOM_REPORT_CSV_IMPORT_TEMPLATE.csv"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; - // 수정 모드 시: 기존 BOM_REPORT_OBJID (메인 그리드 행 클릭 → "Excel로 재등록" 등의 진입점) editObjid?: string | null; initialProductCd?: string; onSaved: () => void; } interface Column { - key: keyof BomExcelRow; + key: keyof BomCsvRow; label: string; width: string; align?: "left" | "center" | "right"; - showNameFor?: keyof BomExcelRow; + showNameFor?: keyof BomCsvRow; } +// 그리드 컬럼: 화면 표시는 핵심 + 자동 채움 컬럼 (운영 그리드 25컬럼 중 CSV 11컬럼 + 자동 ACCTFG/ODRFG) const COLUMNS: Column[] = [ - { key: "NOTE", label: "결과", width: "min-w-[220px]", align: "left" }, - { key: "PARENT_PART_NO", label: "모품번", width: "min-w-[140px]", align: "center" }, - { key: "PART_NO", label: "자품번", width: "min-w-[140px]", align: "center" }, + { key: "NOTE", label: "결과", width: "min-w-[200px]", align: "left" }, + { key: "LEVEL", label: "수준", width: "min-w-[60px]", align: "center" }, + { key: "PARENT_PART_NO", label: "모품번 (자동)", width: "min-w-[140px]", align: "center" }, + { key: "PART_NO", label: "품번", width: "min-w-[140px]", align: "center" }, { key: "PART_NAME", label: "품명", width: "min-w-[200px]", align: "left" }, { key: "QTY", label: "수량", width: "min-w-[70px]", align: "right" }, { key: "ITEM_QTY", label: "항목수량", width: "min-w-[80px]", align: "right" }, - { key: "MATERIAL", label: "재질", width: "min-w-[100px]" }, - { key: "SPEC", label: "사양(규격)", width: "min-w-[110px]" }, - { key: "POST_PROCESSING", label: "후처리", width: "min-w-[100px]" }, - { key: "MAKER", label: "MAKER", width: "min-w-[110px]" }, - { key: "PART_TYPE", label: "부품유형", width: "min-w-[100px]", align: "center", showNameFor: "PART_TYPE_NAME" }, - { key: "REMARK", label: "REMARK", width: "min-w-[130px]", align: "left" }, + { key: "MATERIAL", label: "재료", width: "min-w-[100px]" }, + { key: "HEAT_TREATMENT_HARDNESS", label: "열처리경도", width: "min-w-[100px]" }, + { key: "HEAT_TREATMENT_METHOD", label: "열처리방법", width: "min-w-[100px]" }, + { key: "SURFACE_TREATMENT", label: "표면처리", width: "min-w-[100px]" }, + { key: "MAKER", label: "공급업체", width: "min-w-[110px]" }, + { key: "PART_TYPE", label: "범주", width: "min-w-[90px]", align: "center", showNameFor: "PART_TYPE_NAME" }, + { key: "ACCTFG", label: "계정구분(자동)", width: "min-w-[90px]", align: "center" }, + { key: "ODRFG", label: "조달구분(자동)", width: "min-w-[90px]", align: "center" }, ]; -function displayValue(r: BomExcelRow, col: Column): string { +const ACCTFG_LABEL: Record = { "4": "반제품", "7": "비용" }; +const ODRFG_LABEL: Record = { "0": "구매", "1": "생산", "8": "Phantom" }; + +function displayValue(r: BomCsvRow, col: Column): string { + if (col.key === "ACCTFG") { + const v = String(r.ACCTFG ?? ""); + return ACCTFG_LABEL[v] ?? v; + } + if (col.key === "ODRFG") { + const v = String(r.ODRFG ?? ""); + return ODRFG_LABEL[v] ?? v; + } if (col.showNameFor) { const name = r[col.showNameFor]; if (name) return String(name); @@ -74,9 +93,10 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init const [copyOptions, setCopyOptions] = useState([]); const [copySelect, setCopySelect] = useState(""); - const [rows, setRows] = useState([]); + const [rows, setRows] = useState([]); const [hasError, setHasError] = useState(false); const [fileName, setFileName] = useState(""); + const [encoding, setEncoding] = useState(""); const [parsing, setParsing] = useState(false); const [saving, setSaving] = useState(false); const [copying, setCopying] = useState(false); @@ -90,10 +110,10 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init setRows([]); setHasError(false); setFileName(""); + setEncoding(""); setCopySelect(""); }, [initialProductCd]); - // open 시 초기화 + 복사 옵션 로드 useEffect(() => { if (!open) return; reset(); @@ -105,7 +125,6 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init onOpenChange(v); }; - // wace gridFn.search().gridComplete — 1레벨(PARENT_PART_NO 없는 첫 행) → 헤더 자동 채움 const applyFirstLevelToHeader = (first: { part_no: string; part_name: string } | null) => { if (!first) return; if (first.part_no) setBomPartNo(first.part_no); @@ -113,8 +132,8 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init }; const parseFile = useCallback(async (file: File) => { - if (!/\.xlsx?$/i.test(file.name)) { - toast.error("xlsx 또는 xls 파일만 업로드 가능합니다."); + if (!/\.csv$/i.test(file.name)) { + toast.error("CSV(.csv) 파일만 업로드 가능합니다."); return; } setParsing(true); @@ -123,17 +142,18 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init const data = await devBomApi.excelParse(file); setRows(data.rows ?? []); setHasError(!!data.hasError); + setEncoding(data.encoding ?? ""); applyFirstLevelToHeader(data.firstLevel); if (!data.rows || data.rows.length === 0) { - toast.warning("파싱된 데이터가 없습니다. 템플릿 형식을 확인해 주세요."); + toast.warning("파싱된 데이터가 없습니다. CSV 형식을 확인해 주세요."); } else if (data.hasError) { - toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); + toast.error("CSV 파일 로딩결과가 유효하지 않습니다. 결과 메시지를 확인해 주세요."); } else { - toast.success(`${data.rows.length}건 파싱 완료`); + toast.success(`${data.rows.length}건 파싱 완료 (인코딩: ${data.encoding})`); } } catch (e: any) { - toast.error(e?.response?.data?.message ?? e?.message ?? "엑셀 파싱 실패"); - setRows([]); setHasError(false); setFileName(""); + toast.error(e?.response?.data?.message ?? e?.message ?? "CSV 파싱 실패"); + setRows([]); setHasError(false); setFileName(""); setEncoding(""); } finally { setParsing(false); } @@ -173,17 +193,16 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init if (!bomPartNo) { toast.error("품번을 입력해 주세요."); return; } if (!bomPartName){ toast.error("품명을 입력해 주세요."); return; } if (hasError) { - toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); + toast.error("CSV 파일 로딩결과가 유효하지 않습니다. 결과 메시지를 확인해 주세요."); return; } - // wace fn_checkDuplicatePartNo 1:1 — 헤더 PART_NO 가 다른 BOM 에 이미 있으면 거부 (편집 중 자신 제외) try { const dup = await devBomApi.excelCheckDuplicate(bomPartNo, editObjid ?? undefined); if (dup) { toast.error("입력한 품번이 이미 존재합니다. 다른 품번을 입력해주세요."); return; } - } catch { /* 중복 확인 실패는 비차단 */ } + } catch { /* 비차단 */ } const confirmMsg = rows.length > 0 ? "저장 하시겠습니까?" : "품번, 품명으로 빈 E-BOM을 생성하시겠습니까?"; if (!confirm(confirmMsg)) return; @@ -209,9 +228,9 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init return ( - + - PART 및 구조등록 Excel upload + PART 및 구조등록 CSV upload {/* 헤더 */} @@ -226,11 +245,11 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init
- +
- +
@@ -266,17 +285,18 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init {rows.length > 0 && ( - )} @@ -285,6 +305,7 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init
{fileName && {fileName}} + {encoding && 인코딩: {encoding}} 총 {rows.length}건 {errorCount > 0 && 에러 {errorCount}건}
@@ -303,7 +324,10 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init >
- Drag & Drop 또는 클릭하여 BOM 엑셀 템플릿 업로드 (.xlsx, .xls) + Drag & Drop 또는 클릭하여 CSV 템플릿 업로드 (.csv) +
+
+ 컬럼: 수준 / 품번 / 품명 / 수량 / 항목수량 / 재료 / 열처리경도 / 열처리방법 / 표면처리 / 공급업체 / 범주이름
)} diff --git a/frontend/lib/api/devBom.ts b/frontend/lib/api/devBom.ts index 90fde805..fc4ba30c 100644 --- a/frontend/lib/api/devBom.ts +++ b/frontend/lib/api/devBom.ts @@ -168,9 +168,9 @@ export const devBomApi = { return (res.data?.data as BomCopySourceRow[]) ?? []; }, - async excelCopy(objid: string): Promise { + async excelCopy(objid: string): Promise { const res = await apiClient.get(`/development/ebom/excel-copy/${objid}`); - return ((res.data?.data?.rows as BomExcelRow[]) ?? []); + return ((res.data?.data?.rows as BomCsvRow[]) ?? []); }, async excelSave(input: BomExcelSaveInput): Promise { @@ -179,28 +179,43 @@ export const devBomApi = { }, }; -// ─── Excel Import 타입 ───────────────────────────────────── +// ─── CSV Import 타입 (wace parsingCsvFile 1:1) ───────────── -export interface BomExcelRow { +export interface BomCsvRow { NOTE: string; + LEVEL: string; PARENT_PART_NO: string; PART_NO: string; PART_NAME: string; QTY: string; ITEM_QTY: string; MATERIAL: string; - SPEC: string; - POST_PROCESSING: string; + HEAT_TREATMENT_HARDNESS: string; + HEAT_TREATMENT_METHOD: string; + SURFACE_TREATMENT: string; MAKER: string; PART_TYPE: string; - PART_TYPE_NAME?: string; - REMARK: string; + PART_TYPE_NAME: string; + ACCTFG: string; + ODRFG: string; + UNIT_DC: string; + UNITMANG_DC: string; + UNITCHNG_NB: string; + LOT_FG: string; + USE_YN: string; + QC_FG: string; + SETITEM_FG: string; + REQ_FG: string; } +// 기존 코드 호환용 별칭 (필요 시 마이그레이션) +export type BomExcelRow = BomCsvRow; + export interface BomExcelParseResponse { - rows: BomExcelRow[]; + rows: BomCsvRow[]; hasError: boolean; firstLevel: { part_no: string; part_name: string } | null; + encoding: string; } export interface BomCopySourceRow { @@ -218,7 +233,7 @@ export interface BomExcelSaveInput { partNo: string; partName: string; version?: string; - rows: BomExcelRow[]; + rows: BomCsvRow[]; } export interface BomExcelSaveResult { diff --git a/frontend/public/templates/BOM_REPORT_CSV_IMPORT_TEMPLATE.csv b/frontend/public/templates/BOM_REPORT_CSV_IMPORT_TEMPLATE.csv new file mode 100644 index 00000000..3d9d21c1 --- /dev/null +++ b/frontend/public/templates/BOM_REPORT_CSV_IMPORT_TEMPLATE.csv @@ -0,0 +1,6 @@ +수준,품번,품명,수량,항목수량,재료,열처리경도,열처리방법,표면처리,공급업체,범주이름 +1,RFX-140,Peak 소성로,1,1,재질1,,,,(주)배관랜드,구매품 +2,RFX-140-010,치환실 앗세이,1,1,재질2,,,,(주)네온테크,조립품 +3,RFX-140-010-001,치환실 앗세이_001,1,1,재질3,,,,(주)우리전열,구매품 +3,RFX-140-010-002,AC전원,1,1,,,,,ABB,구매품 +2,RFX-140-020,전원부 앗세이,1,1,,,,,,부품 diff --git a/frontend/public/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx b/frontend/public/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx deleted file mode 100644 index e763b48c94fa57951bc1515367717a75dd51458b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92438 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 z 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 E15d)JX#fBK