From 7779f37c1767b41964e43984f34c68ecc17f7fd3 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 12 May 2026 17:55:17 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EA=B4=80=EB=A6=AC>PART=C2=B7?= =?UTF-8?q?E-BOM=20Excel=20Import=20=EB=A9=94=EB=89=B4=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4=20=E2=80=94=20wace=20partMng=201:1=20=EC=9D=B4?= =?UTF-8?q?=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PART Excel Import (M1·M2 공용): - /partMng/openPartExcelImportPopUp.do + partParsingExcelFile.do + partUploadSave.do 1:1 - 22컬럼 파싱 + NOTE 누적 검증 (품번 필수/중복, PART_TYPE/ACCTFG/UNIT_DC commCode 매핑, ODRFG/LOT_FG/USE_YN/QC_FG/SETITEM_FG/REQ_FG 한글 → 코드값) - 저장은 신규 PART_NO 만 mergePartMng INSERT (기존 IS_LAST='1' 행은 skip) - part-regist + part-search 페이지에 Excel Upload 버튼 + 다이얼로그 연결 BOM Report Excel Import (M3 = openBomReportExcelImportPopUp = "PART 및 구조등록 Excel upload"): - /partMng/parsingExcelFile.do + checkDuplicatePartNo.do + getBomDataForCopy.do + partBomApplySave.do (savePartBomMaster) 1:1 - 10컬럼 파싱 + 자품번/모품번/품명/수량 필수 검증, 모품번이 자품번 목록(Set)에 존재 검증, 수량 숫자+>0 검증, PART_TYPE='0001788'(구매품표준) part_mng 존재 검증 - 1레벨(모품번 없는 첫 행) → 헤더 PART_NO/PART_NAME 자동 채움 - 저장 트랜잭션 (wace 1:1): 헤더 part_bom_report INSERT(신규) / DELETE 자식트리+UPDATE(수정) 자식 PART: part_mng IS_LAST='1' 존재 시 updatePartInfoFromCsv UPDATE, 없으면 insertpartInfo INSERT 부모 PART: 존재 시 lookup, 없으면 "" (절대 INSERT 안 함 — wace 5359-5361) bom_part_qty INSERT (relatePartInfo) — 부모행 CHILD_OBJID 를 PARENT_OBJID 로 체인 - 헤더 PART_NO 중복 검사 (편집 중인 자신 제외) - E-BOM 복사 기능 (기존 BOM → 그리드 행) + Template Download - ebom-regist 페이지에 "E-BOM 등록(Excel)" 버튼 + 다이얼로그 연결 운영 템플릿 정적 자산: - frontend/public/templates/PART_EXCEL_IMPORT_TEMPLATE.xlsx - frontend/public/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx (.gitignore 우회 git add -f) wace structureExcelImportPopup.jsp 는 옛 차종/제품군/사양 도메인 화면으로 운영 메뉴 트리에 서 더이상 호출되지 않아 이식 대상 제외. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/controllers/devBomController.ts | 76 +++ .../src/controllers/devPartController.ts | 39 ++ backend-node/src/routes/devBomRoutes.ts | 13 + backend-node/src/routes/devPartRoutes.ts | 10 + .../src/services/devBomExcelImportService.ts | 476 ++++++++++++++++++ .../src/services/devPartExcelImportService.ts | 372 ++++++++++++++ .../development/ebom-regist/page.tsx | 14 +- .../development/part-regist/page.tsx | 12 +- .../development/part-search/page.tsx | 12 +- .../BomReportExcelImportDialog.tsx | 370 ++++++++++++++ .../development/PartExcelImportDialog.tsx | 280 +++++++++++ frontend/lib/api/devBom.ts | 84 ++++ frontend/lib/api/devPart.ts | 60 +++ .../BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx | Bin 0 -> 92438 bytes .../templates/PART_EXCEL_IMPORT_TEMPLATE.xlsx | Bin 0 -> 127127 bytes 15 files changed, 1815 insertions(+), 3 deletions(-) create mode 100644 backend-node/src/services/devBomExcelImportService.ts create mode 100644 backend-node/src/services/devPartExcelImportService.ts create mode 100644 frontend/components/development/BomReportExcelImportDialog.tsx create mode 100644 frontend/components/development/PartExcelImportDialog.tsx create mode 100644 frontend/public/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx create mode 100644 frontend/public/templates/PART_EXCEL_IMPORT_TEMPLATE.xlsx diff --git a/backend-node/src/controllers/devBomController.ts b/backend-node/src/controllers/devBomController.ts index 50281618..2d88b5b7 100644 --- a/backend-node/src/controllers/devBomController.ts +++ b/backend-node/src/controllers/devBomController.ts @@ -12,6 +12,7 @@ import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import * as svc from "../services/devBomService"; +import * as excelSvc from "../services/devBomExcelImportService"; import { logger } from "../utils/logger"; function parseListFilter(q: Record): svc.BomReportListFilter { @@ -78,6 +79,81 @@ export async function removeMany(req: AuthenticatedRequest, res: Response) { } } +// ─── Excel Import (M3) ──────────────────────────────────── +// POST /api/development/ebom/excel-parse (multipart, field: file) +// GET /api/development/ebom/excel-check-duplicate?partNo=&exclude= +// GET /api/development/ebom/excel-copy-source?productCd= +// GET /api/development/ebom/excel-copy/:objid (기존 BOM → 그리드 행) +// POST /api/development/ebom/excel-save (body: { bomReportObjid?, productCd, partNo, partName, version?, rows }) +// +// wace: parsingExcelFile.do + checkDuplicatePartNo.do + getBomDataForCopy.do + partBomApplySave.do +// + 메인의 code_map.bom_list (select 옵션) + +export async function excelParse(req: AuthenticatedRequest, res: Response) { + try { + const file = (req as any).file as Express.Multer.File | undefined; + if (!file) return res.status(400).json({ success: false, message: "엑셀 파일이 필요합니다." }); + const data = await excelSvc.parseAndValidate(file.buffer); + return res.json({ success: true, data }); + } catch (e: any) { + logger.error("BOM 엑셀 파싱 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + +export async function excelCheckDuplicate(req: AuthenticatedRequest, res: Response) { + try { + const partNo = String(req.query.partNo ?? "").trim(); + const exclude = String(req.query.exclude ?? "").trim() || undefined; + const isDuplicate = await excelSvc.checkDuplicateBomPartNo(partNo, exclude); + return res.json({ success: true, data: { isDuplicate } }); + } catch (e: any) { + logger.error("BOM 헤더 품번 중복 검사 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + +export async function excelCopySource(req: AuthenticatedRequest, res: Response) { + try { + const productCd = String(req.query.productCd ?? "").trim() || undefined; + const rows = await excelSvc.listForCopySelect(productCd); + return res.json({ success: true, data: rows }); + } catch (e: any) { + logger.error("BOM 복사 원본 목록 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + +export async function excelCopy(req: AuthenticatedRequest, res: Response) { + try { + const { objid } = req.params; + const rows = await excelSvc.copyBomForGrid(objid); + return res.json({ success: true, data: { rows } }); + } catch (e: any) { + logger.error("BOM 복사 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + +export async function excelSave(req: AuthenticatedRequest, res: Response) { + try { + const userId = req.user!.userId; + const body = req.body as excelSvc.BomSaveInput; + if (!body || !Array.isArray(body.rows)) { + return res.status(400).json({ success: false, message: "잘못된 요청 본문입니다." }); + } + const result = await excelSvc.saveBomReport(userId, body); + return res.json({ + success: true, + data: result, + message: `${result.mode === "create" ? "등록" : "수정"} 완료 — BOM 행 ${result.bomRows}건 (PART 신규 ${result.insertedParts}건 / 업데이트 ${result.updatedParts}건)`, + }); + } catch (e: any) { + logger.error("BOM Excel 저장 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + // ─── M4 정전개 ───────────────────────────────────────────── export async function ascending(req: AuthenticatedRequest, res: Response) { diff --git a/backend-node/src/controllers/devPartController.ts b/backend-node/src/controllers/devPartController.ts index ff550250..affa0977 100644 --- a/backend-node/src/controllers/devPartController.ts +++ b/backend-node/src/controllers/devPartController.ts @@ -13,6 +13,7 @@ import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import * as svc from "../services/devPartService"; +import * as excelSvc from "../services/devPartExcelImportService"; import { logger } from "../utils/logger"; function parseListFilter(q: Record): svc.PartListFilter { @@ -109,6 +110,44 @@ export async function deploy(req: AuthenticatedRequest, res: Response) { } } +// ─── Excel Import (M1·M2 공용) ────────────────────────────── +// POST /api/development/part/excel-parse (multipart, field: file) +// POST /api/development/part/excel-save (body: { rows: [...] }) +// +// 운영판 wace: openPartExcelImportPopUp.jsp → partParsingExcelFile.do + partUploadSave.do +// 본 RPS 구현: 파일을 메모리 파싱 → 검증 결과(NOTE 포함) 반환 / 저장 시 신규 part_no 만 INSERT. + +export async function excelParse(req: AuthenticatedRequest, res: Response) { + try { + const file = (req as any).file as Express.Multer.File | undefined; + if (!file) return res.status(400).json({ success: false, message: "엑셀 파일이 필요합니다." }); + const data = await excelSvc.parseAndValidate(file.buffer); + return res.json({ success: true, data }); + } catch (e: any) { + logger.error("PART 엑셀 파싱 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + +export async function excelSave(req: AuthenticatedRequest, res: Response) { + try { + const userId = req.user!.userId; + const rows = Array.isArray(req.body?.rows) ? (req.body.rows as excelSvc.SavePartExcelInput[]) : []; + if (rows.length === 0) { + return res.status(400).json({ success: false, message: "저장할 행이 없습니다." }); + } + const result = await excelSvc.saveExcelRows(userId, rows); + return res.json({ + success: true, + data: result, + message: `${result.inserted}건이 저장되었습니다.${result.skipped > 0 ? ` (중복 ${result.skipped}건 건너뜀)` : ""}`, + }); + } catch (e: any) { + logger.error("PART 엑셀 저장 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + // ─── 다중 삭제 ────────────────────────────────────────────── export async function removeMany(req: AuthenticatedRequest, res: Response) { diff --git a/backend-node/src/routes/devBomRoutes.ts b/backend-node/src/routes/devBomRoutes.ts index 9e0676a5..8e3b892b 100644 --- a/backend-node/src/routes/devBomRoutes.ts +++ b/backend-node/src/routes/devBomRoutes.ts @@ -4,16 +4,29 @@ // ============================================================ import { Router } from "express"; +import multer from "multer"; import { authenticateToken } from "../middleware/authMiddleware"; import * as ctrl from "../controllers/devBomController"; const router = Router(); router.use(authenticateToken); +const excelUpload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 10 * 1024 * 1024 }, // 10MB +}); + // M4 — 트리 (정/역전개) — /ebom-tree prefix (라우트 충돌 방지: /:objid 위) router.get("/ebom-tree/ascending", ctrl.ascending); router.get("/ebom-tree/descending", ctrl.descending); +// M3 Excel Import — /:objid 보다 위에 (라우트 충돌 방지) +router.post("/ebom/excel-parse", excelUpload.single("file"), ctrl.excelParse); +router.get("/ebom/excel-check-duplicate", ctrl.excelCheckDuplicate); +router.get("/ebom/excel-copy-source", ctrl.excelCopySource); +router.get("/ebom/excel-copy/:objid", ctrl.excelCopy); +router.post("/ebom/excel-save", ctrl.excelSave); + // M3 — 그리드 + CRUD router.get("/ebom/list", ctrl.getList); router.delete("/ebom", ctrl.removeMany); diff --git a/backend-node/src/routes/devPartRoutes.ts b/backend-node/src/routes/devPartRoutes.ts index ee0db031..d7dbd428 100644 --- a/backend-node/src/routes/devPartRoutes.ts +++ b/backend-node/src/routes/devPartRoutes.ts @@ -4,12 +4,18 @@ // ============================================================ import { Router } from "express"; +import multer from "multer"; import { authenticateToken } from "../middleware/authMiddleware"; import * as ctrl from "../controllers/devPartController"; const router = Router(); router.use(authenticateToken); +const excelUpload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 10 * 1024 * 1024 }, // 10MB +}); + // M1 — 임시(등록) 그리드 router.get("/part-temp/list", ctrl.getTempList); router.post("/part-temp/deploy", ctrl.deploy); @@ -17,6 +23,10 @@ router.post("/part-temp/deploy", ctrl.deploy); // M2 — 릴리즈 그리드 router.get("/part/list", ctrl.getList); +// Excel Import (M1·M2 공용) — /:objid 보다 위에 위치 +router.post("/part/excel-parse", excelUpload.single("file"), ctrl.excelParse); +router.post("/part/excel-save", ctrl.excelSave); + // 다중 삭제 (body: { objids: string[] }) — /:objid 보다 위 router.delete("/part", ctrl.removeMany); diff --git a/backend-node/src/services/devBomExcelImportService.ts b/backend-node/src/services/devBomExcelImportService.ts new file mode 100644 index 00000000..9f6d0a6a --- /dev/null +++ b/backend-node/src/services/devBomExcelImportService.ts @@ -0,0 +1,476 @@ +// ============================================================ +// 개발관리 BOM Report Excel Import 서비스 — wace_plm PartMngService 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 parsingExcelFile 1:1): +// · 자품번 필수 / 엑셀 내 중복 +// · 모품번이 자품번 목록(allPartNumbers Set)에 존재해야 함 (1레벨 = 첫 데이터 행은 제외) +// · 품명 필수 +// · 수량 필수 + 숫자 + > 0 +// · PART_TYPE 코드명 → code_id (못 찾으면 NOTE "부품유형 확인") +// · PART_TYPE='0001788'(구매품표준) → part_mng.part_no 존재 검증 (NOTE "구매품표준 미등록") +// +// 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컬럼) +// ============================================================ + +import * as XLSX from "xlsx"; +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"; + +export interface BomExcelRow { + NOTE: string; + PARENT_PART_NO: string; + PART_NO: string; + PART_NAME: string; + QTY: string; + ITEM_QTY: string; + MATERIAL: string; + SPEC: string; + POST_PROCESSING: string; + MAKER: string; + PART_TYPE: string; + PART_TYPE_NAME?: string; + REMARK: string; +} + +function appendNote(r: BomExcelRow, 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(); +} + +async function fetchPartTypeMap(client: PoolClient): Promise> { + const r = await client.query( + `SELECT CODE_ID AS code_id, CODE_NAME AS code_name + FROM COMM_CODE WHERE PARENT_CODE_ID = $1`, + [CODE_PARENT_PART_TYPE] + ); + const m = new Map(); + for (const row of r.rows) if (row.code_name) m.set(String(row.code_name).trim().toUpperCase(), row); + return m; +} + +// ─── 1) 파싱 + 검증 (parsingExcelFile 1:1) ────────────────── + +export async function parseAndValidate(buffer: Buffer): Promise<{ + rows: BomExcelRow[]; + hasError: boolean; + firstLevel: { part_no: string; part_name: string } | null; +}> { + 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: "" }); + + // wace 1: 모든 자품번을 먼저 Set 으로 수집 (모품번 검증용) + 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 result: BomExcelRow[] = []; + let firstLevel: { part_no: string; part_name: string } | null = null; + + 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; + + 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); + + // wace: PART_NO+PART_NAME 둘 다 빈 행은 스킵 (3줄 연속이면 break) + if (!partNo && !partName) { + emptyRowCnt++; + if (emptyRowCnt > 3) break; + continue; + } + emptyRowCnt = 0; + dataRowIndex++; + + const cur: BomExcelRow = { + NOTE: "", + PARENT_PART_NO: parentPartNo, + PART_NO: partNo, + PART_NAME: partName, + QTY: qty, + ITEM_QTY: qty, // wace: ITEM_QTY 컬럼 없으면 QTY 와 동일 (savePartBomMaster 에서도 QTY 로 채움) + MATERIAL: material, + SPEC: spec, + POST_PROCESSING: postProc, + MAKER: maker, + PART_TYPE: "", + REMARK: remark, + }; + + // (1) 자품번 필수 + if (!partNo) { + appendNote(cur, "필수입력 - 품번"); + } else { + if (partNoSeenInFile.has(partNo)) { + appendNote(cur, `품번 중복: ${partNo}`); + } else { + partNoSeenInFile.set(partNo, dataRowIndex); + } + } + + // (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레벨 (모품번 없는 첫 행)을 헤더 자동 채움 용도로 캡쳐 + if (!firstLevel && !parentPartNo && partNo) { + firstLevel = { part_no: partNo, part_name: partName }; + } + + result.push(cur); + } + } finally { + client.release(); + } + + const hasError = result.some((r) => r.NOTE); + return { rows: result, hasError, firstLevel }; +} + +// ─── 2) 헤더 part_no 중복 검사 (wace checkDuplicatePartNo) ── + +export async function checkDuplicateBomPartNo(partNo: string, excludeObjid?: string): Promise { + if (!partNo) return false; + const sql = excludeObjid + ? `SELECT 1 FROM PART_BOM_REPORT WHERE PART_NO = $1 AND OBJID <> $2 LIMIT 1` + : `SELECT 1 FROM PART_BOM_REPORT WHERE PART_NO = $1 LIMIT 1`; + const params = excludeObjid ? [partNo.trim(), excludeObjid] : [partNo.trim()]; + const r = await getPool().query(sql, params); + return (r.rowCount ?? 0) > 0; +} + +// ─── 3) E-BOM 복사: 기존 BOM_PART_QTY → BomExcelRow[] ────── + +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 + LEFT JOIN COMM_CODE CC ON CC.CODE_ID = PM.PART_TYPE + WHERE Q.BOM_REPORT_OBJID = $1 + ORDER BY Q.SEQ + `; + const r = await getPool().query(sql, [sourceObjid]); + return r.rows.map((row): BomExcelRow => ({ + NOTE: "", + 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 ?? "", + MAKER: row.maker ?? "", + PART_TYPE: row.part_type ?? "", + PART_TYPE_NAME: row.part_type_name ?? "", + REMARK: row.remark ?? "", + })); +} + +// ─── 4) BOM 저장 (savePartBomMaster 1:1) ──────────────────── + +export interface BomSaveInput { + bomReportObjid?: string; // 비어있으면 신규 + productCd: string; + partNo: string; // 헤더 품번 + partName: string; + version?: string; + rows: BomExcelRow[]; +} + +export interface BomSaveResult { + bomReportObjid: string; + insertedParts: number; + updatedParts: number; + bomRows: number; + mode: "create" | "update"; +} + +export async function saveBomReport(userId: string, input: BomSaveInput): Promise { + if (!input.productCd) throw new Error("제품구분은 필수입니다."); + if (!input.partNo) throw new Error("품번은 필수입니다."); + if (!input.partName) throw new Error("품명은 필수입니다."); + + let insertedParts = 0; + let updatedParts = 0; + let bomRows = 0; + let bomReportObjid = (input.bomReportObjid && input.bomReportObjid.trim()) ? input.bomReportObjid.trim() : ""; + const mode: "create" | "update" = bomReportObjid ? "update" : "create"; + + 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 + SET STATUS = 'N', WRITER = $1, edit_date = NOW(), + PRODUCT_CD = $2, PART_NO = $3, PART_NAME = $4, REVISION = $5 + WHERE OBJID = $6`, + [userId, input.productCd, input.partNo, input.partName, input.version ?? null, bomReportObjid] + ); + } else { + bomReportObjid = createObjId(); + await client.query( + `INSERT INTO PART_BOM_REPORT ( + OBJID, CUSTOMER_OBJID, CONTRACT_OBJID, UNIT_CODE, + STATUS, WRITER, REGDATE, + MULTI_YN, MULTI_MASTER_YN, MULTI_BREAK_YN, MULTI_MASTER_OBJID, + PRODUCT_CD, PART_NO, PART_NAME, REVISION + ) VALUES ( + $1, NULL, NULL, NULL, + 'N', $2, NOW(), + 'N', 'N', NULL, NULL, + $3, $4, $5, $6 + )`, + [bomReportObjid, userId, input.productCd, input.partNo, input.partName, input.version ?? null] + ); + } + + if (!input.rows || input.rows.length === 0) return; // wace: 빈 BOM 허용 (헤더만 생성) + + // 자식 PART_NO → part_mng.objid 캐시 (행 처리 후) + const childPartObjIdCache = new Map(); + // bom_part_qty 부모행의 CHILD_OBJID 캐시 (다음 자식들이 이 값을 PARENT_OBJID 로 사용) + const childBomObjIdByPartNo = new Map(); + + // 자식 PART 처리: 있으면 UPDATE / 없으면 INSERT (wace 1:1) + async function upsertChildPart(r: BomExcelRow): Promise { + if (childPartObjIdCache.has(r.PART_NO)) return childPartObjIdCache.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`, + [r.PART_NO] + ); + 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`, + [ + r.PART_NAME ?? "", + r.SPEC ?? "", + r.MATERIAL ?? "", + r.REMARK ?? "", + r.PART_TYPE ?? "", + r.MAKER ?? "", + r.POST_PROCESSING ?? "", + id, + ] + ); + updatedParts++; + childPartObjIdCache.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, + STATUS, REG_DATE, WRITER, IS_LAST, + PART_TYPE, MAKER, POST_PROCESSING, + 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' + )`, + [ + newId, r.PART_NO, + r.PART_NAME ?? "", r.SPEC ?? "", r.MATERIAL ?? "", r.REMARK ?? "", + userId, + r.PART_TYPE ?? "", r.MAKER ?? "", r.POST_PROCESSING ?? "", + ] + ); + insertedParts++; + childPartObjIdCache.set(r.PART_NO, newId); + return newId; + } + + // 부모 PART 처리: 있으면 lookup, 없으면 "" (wace 1:1 — INSERT 절대 안 함) + async function lookupParentPart(partNo: string): Promise { + if (!partNo) return ""; + if (childPartObjIdCache.has(partNo)) return childPartObjIdCache.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); + return id; + } + return ""; // wace 원본 5359-5361: 부모 part_mng 없으면 parent_part_no="" (INSERT 안 함) + } + + for (const r of input.rows) { + if (!r.PART_NO) continue; + + const partObjid = await upsertChildPart(r); + const parentPartObjid = await lookupParentPart(r.PARENT_PART_NO); + + // 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, + PARENT_PART_NO, PART_NO, + QTY, ITEM_QTY, QTY_TEMP, + REGDATE, WRITER, SEQ, STATUS, LAST_PART_OBJID + ) VALUES ( + $1, $2, NULLIF($3, ''), $4, + $5, $6, + COALESCE(NULLIF($7, ''), '0')::numeric, + COALESCE(NULLIF($8, ''), '0')::numeric, + COALESCE(NULLIF($7, ''), '0')::numeric, + NOW(), $9, nextval('seq_bom_qty'), 'deploy', NULL + )`, + [ + bomReportObjid, newBomObjid, parentBomObjid, newChildObjid, + parentPartObjid, partObjid, + r.QTY ?? "", + r.ITEM_QTY ?? r.QTY ?? "", + userId, + ] + ); + + childBomObjIdByPartNo.set(r.PART_NO, newChildObjid); + bomRows++; + } + }); + + logger.info("BOM Excel Import 저장 완료", { userId, bomReportObjid, mode, insertedParts, updatedParts, bomRows }); + return { bomReportObjid, insertedParts, updatedParts, bomRows, mode }; +} + +// ─── 5) BOM 목록 (E-BOM 복사 select 옵션) ───────────────── + +export async function listForCopySelect(productCd?: string) { + const params: any[] = []; + let where = "1=1"; + if (productCd) { params.push(productCd); where = `T.PRODUCT_CD = $1`; } + const sql = ` + SELECT T.OBJID, T.PART_NO, T.PART_NAME, T.REVISION, T.PRODUCT_CD, + TO_CHAR(T.REGDATE, 'YYYY-MM-DD') AS REGDATE + FROM PART_BOM_REPORT T + WHERE ${where} + ORDER BY T.REGDATE DESC NULLS LAST + LIMIT 200 + `; + const r = await getPool().query(sql, params); + return r.rows; +} diff --git a/backend-node/src/services/devPartExcelImportService.ts b/backend-node/src/services/devPartExcelImportService.ts new file mode 100644 index 00000000..f5d41a87 --- /dev/null +++ b/backend-node/src/services/devPartExcelImportService.ts @@ -0,0 +1,372 @@ +// ============================================================ +// 개발관리 PART Excel Import 서비스 — wace_plm PartMngService 1:1 +// +// 원본 흐름 (wace partMng/openPartExcelImportPopUp.jsp): +// 1) /partMng/partParsingExcelFile.do → 업로드 파일 파싱 + 검증 +// 엑셀 row[2..]부터 22컬럼 추출. 검증 결과는 행마다 NOTE 컬럼에 누적. +// 코드명 → 코드값 매핑 (PART_TYPE/ACCTFG/UNIT_DC/UNITMANG_DC, 한글 → 0/1/8/Y/N). +// 품번 중복 (PART_MNG 기존 + 엑셀 내 중복) → "품번중복" NOTE. +// 2) /partMng/partUploadSave.do → 그리드 데이터 INSERT +// partMng.getPartObjid 로 part_no 존재 여부 확인. 없으면 createObjId + mergePartMng INSERT. +// (NOTE에 에러 있는 행은 클라이언트가 차단) +// +// vexplor_rps 차이: +// · 운영판은 파일을 서버에 저장 → /partParsingExcelFile.do 가 그 파일을 읽음. +// · RPS는 multer memoryStorage 로 받은 Buffer 를 그 자리에서 파싱 (WbsTemplate parseExcel 패턴). +// · 결과 그리드는 클라이언트 메모리에만 유지 → 저장 호출 시 다시 검증 한 번 더 수행. +// ============================================================ + +import * as XLSX from "xlsx"; +import { PoolClient } from "pg"; +import { getPool, transaction } from "../database/db"; +import { logger } from "../utils/logger"; +import { createObjId } from "../utils/objidUtil"; + +// ─── wace 운영판 PARENT_CODE_ID (commCode) ────────────────── +const CODE_PARENT_PART_TYPE = "0000062"; // 범주 (PART_TYPE) +const CODE_PARENT_ACCTFG = "0900213"; // 계정구분 +const CODE_PARENT_UNIT_DC = "0001399"; // 단위 (UNIT_DC / UNITMANG_DC) + +// 엑셀 22컬럼 (1행 헤더 — wace JSP colModel 순서) +// 0:품번 1:품명 2:재료 3:열처리경도 4:열처리방법 5:표면처리 6:메이커 7:범주 이름 +// 8:규격 9:계정구분 10:조달구분 11:재고단위 12:관리단위 13:환산수량 +// 14:LOT구분 15:사용여부 16:검사여부 17:SET품여부 18:의뢰여부 19:개당길이 20:개당소요량 21:비고 + +export interface PartExcelRow { + NOTE: string; + PART_NO: string; + PART_NAME: string; + MATERIAL: string; + HEAT_TREATMENT_HARDNESS: string; + HEAT_TREATMENT_METHOD: string; + SURFACE_TREATMENT: string; + MAKER: string; + PART_TYPE: string; // 코드값(`code_id`) — 매핑 실패 시 원본 한글 + PART_TYPE_NAME?: string; // 화면용 라벨 + SPEC: string; + ACCTFG: string; + ACCTFG_NAME?: string; + ODRFG: string; + ODRFG_NAME?: string; + UNIT_DC: string; + UNIT_DC_NAME?: string; + UNITMANG_DC: string; + UNITMANG_DC_NAME?: string; + UNITCHNG_NB: string; + LOT_FG: string; + USE_YN: string; + QC_FG: string; + SETITEM_FG: string; + REQ_FG: string; + UNIT_LENGTH: string; + UNIT_QTY: string; + REMARK: string; +} + +function appendNote(r: PartExcelRow, 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(); +} + +// ─── 코드명 → 코드값 매핑 (commCode 1:1) ──────────────────── + +async function fetchCodeMap( + client: PoolClient, + parentCodeId: string +): Promise> { + const r = await client.query( + `SELECT CODE_ID AS code_id, CODE_NAME AS code_name + FROM COMM_CODE + WHERE PARENT_CODE_ID = $1`, + [parentCodeId] + ); + const m = new Map(); + for (const row of r.rows) { + if (row.code_name) m.set(String(row.code_name).trim().toUpperCase(), row); + } + return m; +} + +function mapKr01(value: string, t: string, f: string, defaultVal: string): string { + const v = value.trim(); + if (!v) return defaultVal; + if (v === t) return "1"; + if (v === f) return "0"; + return v; // 숫자 입력 시 그대로 +} +function mapOdrfg(value: string): string { + const v = value.trim(); + if (!v) return ""; + if (v === "구매") return "0"; + if (v === "생산") return "1"; + if (v.toUpperCase() === "PHANTOM") return "8"; + return v; +} + +// ─── 1) 파싱 + 검증 ───────────────────────────────────────── + +export async function parseAndValidate(buffer: Buffer): Promise<{ + rows: PartExcelRow[]; + hasError: boolean; +}> { + 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 result: PartExcelRow[] = []; + const client = await getPool().connect(); + try { + const [partTypeMap, acctfgMap, unitDcMap] = await Promise.all([ + fetchCodeMap(client, CODE_PARENT_PART_TYPE), + fetchCodeMap(client, CODE_PARENT_ACCTFG), + fetchCodeMap(client, CODE_PARENT_UNIT_DC), + ]); + + const partNoSeenInFile = new Map(); + let emptyRowCnt = 0; + + // wace: rowIndex = 2 부터 (0=안내라벨 "입력", 1=헤더) + for (let i = 2; i < raw.length; i++) { + const row = raw[i]; + if (!row) continue; + + const cur: PartExcelRow = { + NOTE: "", + PART_NO: getCell(row, 0), + PART_NAME: getCell(row, 1), + MATERIAL: getCell(row, 2), + HEAT_TREATMENT_HARDNESS: getCell(row, 3), + HEAT_TREATMENT_METHOD: getCell(row, 4), + SURFACE_TREATMENT: getCell(row, 5), + MAKER: getCell(row, 6), + PART_TYPE: "", + SPEC: getCell(row, 8), + ACCTFG: "", + ODRFG: "", + UNIT_DC: "", + UNITMANG_DC: "", + UNITCHNG_NB: getCell(row, 13), + LOT_FG: "", + USE_YN: "", + QC_FG: "", + SETITEM_FG: "", + REQ_FG: "", + UNIT_LENGTH: getCell(row, 19), + UNIT_QTY: getCell(row, 20), + REMARK: getCell(row, 21), + }; + + // wace getCellValue 의 emptyColCnt(8 미만)로 행 채택. 단순화: 모든 컬럼 빈값이면 빈 행. + const nonEmptyCount = + (cur.PART_NO ? 1 : 0) + (cur.PART_NAME ? 1 : 0) + (cur.MATERIAL ? 1 : 0) + + (cur.HEAT_TREATMENT_HARDNESS ? 1 : 0) + (cur.HEAT_TREATMENT_METHOD ? 1 : 0) + + (cur.SURFACE_TREATMENT ? 1 : 0) + (cur.MAKER ? 1 : 0) + + (getCell(row, 7) ? 1 : 0) + (cur.SPEC ? 1 : 0) + + (getCell(row, 9) ? 1 : 0) + (getCell(row, 10) ? 1 : 0) + + (getCell(row, 11) ? 1 : 0) + (getCell(row, 12) ? 1 : 0) + (cur.UNITCHNG_NB ? 1 : 0) + + (getCell(row, 14) ? 1 : 0) + (getCell(row, 15) ? 1 : 0) + + (getCell(row, 16) ? 1 : 0) + (getCell(row, 17) ? 1 : 0) + + (getCell(row, 18) ? 1 : 0) + (cur.UNIT_LENGTH ? 1 : 0) + + (cur.UNIT_QTY ? 1 : 0) + (cur.REMARK ? 1 : 0); + + if (nonEmptyCount === 0) { + emptyRowCnt++; + if (emptyRowCnt > 3) break; // wace 동일 + continue; + } + + // 품번 필수 + 중복 검사 (DB + 엑셀 내) + if (!cur.PART_NO) { + appendNote(cur, "필수입력 - 품번"); + } else { + const dbRes = await client.query( + `SELECT 1 FROM PART_MNG WHERE PART_NO = $1 LIMIT 1`, + [cur.PART_NO] + ); + if ((dbRes.rowCount ?? 0) > 0) { + appendNote(cur, "품번중복"); + } + if (partNoSeenInFile.has(cur.PART_NO)) { + appendNote(cur, `엑셀 내 품번중복(row ${partNoSeenInFile.get(cur.PART_NO)})`); + } else { + partNoSeenInFile.set(cur.PART_NO, i + 1); + } + } + + if (!cur.PART_NAME) appendNote(cur, "필수입력 - 품명"); + + // PART_TYPE (범주 이름) — 코드명 → code_id + const partTypeIn = getCell(row, 7); + if (partTypeIn) { + const hit = partTypeMap.get(partTypeIn.toUpperCase()); + if (hit) { + cur.PART_TYPE = hit.code_id; + cur.PART_TYPE_NAME = hit.code_name; + } else { + cur.PART_TYPE = partTypeIn; + appendNote(cur, `범주 이름 확인:${partTypeIn}`); + } + } + + // ACCTFG (계정구분) + const acctfgIn = getCell(row, 9); + if (acctfgIn) { + if (/^\d+$/.test(acctfgIn)) { + cur.ACCTFG = acctfgIn; + } else { + const hit = acctfgMap.get(acctfgIn.toUpperCase()); + if (hit) { cur.ACCTFG = hit.code_id; cur.ACCTFG_NAME = hit.code_name; } + else cur.ACCTFG = acctfgIn; + } + } + + // ODRFG (조달구분) + const odrfgIn = getCell(row, 10); + cur.ODRFG = mapOdrfg(odrfgIn); + if (odrfgIn && cur.ODRFG === odrfgIn && !/^[018]$/.test(odrfgIn)) { + appendNote(cur, `조달구분 확인:${odrfgIn}`); + } + cur.ODRFG_NAME = cur.ODRFG === "0" ? "구매" + : cur.ODRFG === "1" ? "생산" + : cur.ODRFG === "8" ? "Phantom" : odrfgIn; + + // UNIT_DC (재고단위) + const unitDcIn = getCell(row, 11); + if (unitDcIn) { + if (/^\d+$/.test(unitDcIn)) cur.UNIT_DC = unitDcIn; + else { + const hit = unitDcMap.get(unitDcIn.toUpperCase()); + if (hit) { cur.UNIT_DC = hit.code_id; cur.UNIT_DC_NAME = hit.code_name; } + else cur.UNIT_DC = unitDcIn; + } + } + + // UNITMANG_DC (관리단위) — 동일 매핑 + const unitmangIn = getCell(row, 12); + if (unitmangIn) { + if (/^\d+$/.test(unitmangIn)) cur.UNITMANG_DC = unitmangIn; + else { + const hit = unitDcMap.get(unitmangIn.toUpperCase()); + if (hit) { cur.UNITMANG_DC = hit.code_id; cur.UNITMANG_DC_NAME = hit.code_name; } + else cur.UNITMANG_DC = unitmangIn; + } + } + + cur.LOT_FG = mapKr01(getCell(row, 14), "사용", "미사용", "0"); + cur.USE_YN = mapKr01(getCell(row, 15), "사용", "미사용", "1"); + cur.QC_FG = mapKr01(getCell(row, 16), "검사", "무검사", "0"); + cur.SETITEM_FG = mapKr01(getCell(row, 17), "여", "부", "0"); + cur.REQ_FG = mapKr01(getCell(row, 18), "여", "부", "0"); + + result.push(cur); + emptyRowCnt = 0; + } + } finally { + client.release(); + } + + const hasError = result.some((r) => r.NOTE); + return { rows: result, hasError }; +} + +// ─── 2) 저장 (mergePartMng 1:1, 신규만 INSERT) ────────────── + +export interface SavePartExcelInput { + PART_NO: string; + PART_NAME: string; + MATERIAL?: string; + HEAT_TREATMENT_HARDNESS?: string; + HEAT_TREATMENT_METHOD?: string; + SURFACE_TREATMENT?: string; + MAKER?: string; + PART_TYPE?: string; + SPEC?: string; + ACCTFG?: string; + ODRFG?: string; + UNIT_DC?: string; + UNITMANG_DC?: string; + UNITCHNG_NB?: string | number; + LOT_FG?: string; + USE_YN?: string; + QC_FG?: string; + SETITEM_FG?: string; + REQ_FG?: string; + UNIT_LENGTH?: string; + UNIT_QTY?: string; + REMARK?: string; +} + +export async function saveExcelRows( + userId: string, + rows: SavePartExcelInput[] +): Promise<{ inserted: number; skipped: number; skippedPartNos: string[] }> { + if (!rows || rows.length === 0) return { inserted: 0, skipped: 0, skippedPartNos: [] }; + + let inserted = 0; + let skipped = 0; + const skippedPartNos: string[] = []; + + await transaction(async (client: PoolClient) => { + for (const r of rows) { + const partNo = (r.PART_NO ?? "").trim(); + if (!partNo) { skipped++; continue; } + + // wace partMng.getPartObjid — IS_LAST='1' 인 동일 part_no 가 있으면 INSERT 스킵 + const existRes = await client.query( + `SELECT OBJID FROM PART_MNG WHERE PART_NO = $1 AND IS_LAST = '1' LIMIT 1`, + [partNo] + ); + if ((existRes.rowCount ?? 0) > 0) { + skipped++; + skippedPartNos.push(partNo); + continue; + } + + const objid = createObjId(); + // wace mergePartMng 1:1 (엑셀 임포트가 채우는 컬럼만) + await client.query( + `INSERT INTO PART_MNG ( + OBJID, PART_NO, PART_NAME, SPEC, MATERIAL, PART_TYPE, REMARK, + STATUS, REG_DATE, WRITER, IS_LAST, + MAKER, + HEAT_TREATMENT_HARDNESS, HEAT_TREATMENT_METHOD, SURFACE_TREATMENT, + ACCTFG, ODRFG, UNIT_DC, UNITMANG_DC, UNITCHNG_NB, + LOT_FG, USE_YN, QC_FG, SETITEM_FG, REQ_FG, + UNIT_LENGTH, UNIT_QTY + ) VALUES ( + $1::numeric, $2, $3, $4, $5, $6, $7, + 'create', NOW(), $8, '1', + $9, + $10, $11, $12, + $13, $14, $15, $16, + CASE WHEN $17 = '' OR $17 IS NULL THEN NULL ELSE $17::numeric END, + COALESCE($18, '0'), COALESCE($19, '1'), COALESCE($20, '0'), + COALESCE($21, '0'), COALESCE($22, '0'), + $23, $24 + )`, + [ + objid, partNo, r.PART_NAME ?? "", r.SPEC ?? null, r.MATERIAL ?? null, + r.PART_TYPE ?? null, r.REMARK ?? null, + userId, + r.MAKER ?? null, + r.HEAT_TREATMENT_HARDNESS ?? null, r.HEAT_TREATMENT_METHOD ?? null, r.SURFACE_TREATMENT ?? null, + r.ACCTFG ?? null, r.ODRFG ?? null, r.UNIT_DC ?? null, r.UNITMANG_DC ?? null, + r.UNITCHNG_NB === undefined || r.UNITCHNG_NB === null ? "" : String(r.UNITCHNG_NB), + r.LOT_FG ?? null, r.USE_YN ?? null, r.QC_FG ?? null, r.SETITEM_FG ?? null, r.REQ_FG ?? null, + r.UNIT_LENGTH ?? null, r.UNIT_QTY ?? null, + ] + ); + inserted++; + } + }); + + logger.info("PART 엑셀 임포트 저장 완료", { userId, inserted, skipped }); + return { inserted, skipped, skippedPartNos }; +} diff --git a/frontend/app/(main)/COMPANY_16/development/ebom-regist/page.tsx b/frontend/app/(main)/COMPANY_16/development/ebom-regist/page.tsx index 52f17273..c6ea0fd1 100644 --- a/frontend/app/(main)/COMPANY_16/development/ebom-regist/page.tsx +++ b/frontend/app/(main)/COMPANY_16/development/ebom-regist/page.tsx @@ -10,13 +10,14 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { - Search, Loader2, RotateCcw, Trash2, Settings, + Search, Loader2, RotateCcw, Trash2, Settings, FileSpreadsheet, } from "lucide-react"; import { toast } from "sonner"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { CommCodeSelect } from "@/components/common/CommCodeSelect"; import { devBomApi, BomReportListFilter, BomReportRow } from "@/lib/api/devBom"; import { BomReportStatusDialog } from "@/components/development/BomReportStatusDialog"; +import { BomReportExcelImportDialog } from "@/components/development/BomReportExcelImportDialog"; const PRODUCT_GROUP = "0000001"; // 제품구분 (vexplor 공용) @@ -53,6 +54,7 @@ export default function EbomRegistPage() { const [statusOpen, setStatusOpen] = useState(false); const [statusObjid, setStatusObjid] = useState(null); + const [excelOpen, setExcelOpen] = useState(false); const fetchList = useCallback(async (override?: Partial) => { setLoading(true); @@ -137,6 +139,10 @@ export default function EbomRegistPage() { {loading ? : } 조회 + + +
@@ -190,6 +195,11 @@ export default function PartSearchPage() { objid={detailObjid} onEdit={handleEditFromDetail} /> +
); } diff --git a/frontend/components/development/BomReportExcelImportDialog.tsx b/frontend/components/development/BomReportExcelImportDialog.tsx new file mode 100644 index 00000000..e0f4d2fa --- /dev/null +++ b/frontend/components/development/BomReportExcelImportDialog.tsx @@ -0,0 +1,370 @@ +"use client"; + +// 개발관리 E-BOM 등록 Excel 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건이라도 있으면 차단. + +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +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"; + +const PRODUCT_GROUP = "0000001"; +const TEMPLATE_DOWNLOAD_URL = "/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + // 수정 모드 시: 기존 BOM_REPORT_OBJID (메인 그리드 행 클릭 → "Excel로 재등록" 등의 진입점) + editObjid?: string | null; + initialProductCd?: string; + onSaved: () => void; +} + +interface Column { + key: keyof BomExcelRow; + label: string; + width: string; + align?: "left" | "center" | "right"; + showNameFor?: keyof BomExcelRow; +} + +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: "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" }, +]; + +function displayValue(r: BomExcelRow, col: Column): string { + if (col.showNameFor) { + const name = r[col.showNameFor]; + if (name) return String(name); + } + return String(r[col.key] ?? ""); +} + +export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, initialProductCd, onSaved }: Props) { + const fileInputRef = useRef(null); + + const [productCd, setProductCd] = useState(""); + const [bomPartNo, setBomPartNo] = useState(""); + const [bomPartName, setBomPartName] = useState(""); + const [version, setVersion] = useState(""); + + const [copyOptions, setCopyOptions] = useState([]); + const [copySelect, setCopySelect] = useState(""); + + const [rows, setRows] = useState([]); + const [hasError, setHasError] = useState(false); + const [fileName, setFileName] = useState(""); + const [parsing, setParsing] = useState(false); + const [saving, setSaving] = useState(false); + const [copying, setCopying] = useState(false); + const [dragOver, setDragOver] = useState(false); + + const reset = useCallback(() => { + setProductCd(initialProductCd ?? ""); + setBomPartNo(""); + setBomPartName(""); + setVersion(""); + setRows([]); + setHasError(false); + setFileName(""); + setCopySelect(""); + }, [initialProductCd]); + + // open 시 초기화 + 복사 옵션 로드 + useEffect(() => { + if (!open) return; + reset(); + devBomApi.excelCopySource().then(setCopyOptions).catch(() => setCopyOptions([])); + }, [open, reset]); + + const handleDialogChange = (v: boolean) => { + if (!v) reset(); + 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); + if (first.part_name) setBomPartName(first.part_name); + }; + + const parseFile = useCallback(async (file: File) => { + if (!/\.xlsx?$/i.test(file.name)) { + toast.error("xlsx 또는 xls 파일만 업로드 가능합니다."); + return; + } + setParsing(true); + setFileName(file.name); + try { + const data = await devBomApi.excelParse(file); + setRows(data.rows ?? []); + setHasError(!!data.hasError); + applyFirstLevelToHeader(data.firstLevel); + if (!data.rows || data.rows.length === 0) { + toast.warning("파싱된 데이터가 없습니다. 템플릿 형식을 확인해 주세요."); + } else if (data.hasError) { + toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); + } else { + toast.success(`${data.rows.length}건 파싱 완료`); + } + } catch (e: any) { + toast.error(e?.response?.data?.message ?? e?.message ?? "엑셀 파싱 실패"); + setRows([]); setHasError(false); setFileName(""); + } finally { + setParsing(false); + } + }, []); + + const handleFileInput = (e: React.ChangeEvent) => { + const f = e.target.files?.[0]; + if (f) parseFile(f); + e.target.value = ""; + }; + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setDragOver(false); + const f = e.dataTransfer.files?.[0]; + if (f) parseFile(f); + }; + + const handleCopy = async () => { + if (!copySelect) { toast.error("복사할 BOM을 선택하세요."); return; } + setCopying(true); + try { + const copied = await devBomApi.excelCopy(copySelect); + setRows(copied); + setHasError(false); + const first = copied.find((r) => !r.PARENT_PART_NO); + if (first) applyFirstLevelToHeader({ part_no: first.PART_NO, part_name: first.PART_NAME }); + toast.success(`BOM 데이터 ${copied.length}건 불러왔습니다.`); + } catch (e: any) { + toast.error(e?.response?.data?.message ?? e?.message ?? "BOM 복사 실패"); + } finally { + setCopying(false); + } + }; + + const handleSave = async () => { + if (!productCd) { toast.error("제품구분을 선택해 주세요."); return; } + if (!bomPartNo) { toast.error("품번을 입력해 주세요."); return; } + if (!bomPartName){ toast.error("품명을 입력해 주세요."); return; } + if (hasError) { + toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); + return; + } + // wace fn_checkDuplicatePartNo 1:1 — 헤더 PART_NO 가 다른 BOM 에 이미 있으면 거부 (편집 중 자신 제외) + try { + const dup = await devBomApi.excelCheckDuplicate(bomPartNo, editObjid ?? undefined); + if (dup) { + toast.error("입력한 품번이 이미 존재합니다. 다른 품번을 입력해주세요."); + return; + } + } catch { /* 중복 확인 실패는 비차단 */ } + + const confirmMsg = rows.length > 0 ? "저장 하시겠습니까?" : "품번, 품명으로 빈 E-BOM을 생성하시겠습니까?"; + if (!confirm(confirmMsg)) return; + + setSaving(true); + try { + const result = await devBomApi.excelSave({ + bomReportObjid: editObjid ?? undefined, + productCd, partNo: bomPartNo, partName: bomPartName, version, + rows, + }); + toast.success(`${result.mode === "create" ? "등록" : "수정"} 완료 — BOM ${result.bomRows}건 (PART 신규 ${result.insertedParts} / 수정 ${result.updatedParts})`); + onSaved(); + handleDialogChange(false); + } catch (e: any) { + toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패"); + } finally { + setSaving(false); + } + }; + + const errorCount = useMemo(() => rows.filter((r) => r.NOTE).length, [rows]); + + return ( + + + + PART 및 구조등록 Excel upload + + + {/* 헤더 */} +
+
+ + +
+
+ + +
+
+ + +
+
+ + setVersion(e.target.value)} placeholder="REV 등" /> +
+
+ + {/* E-BOM 복사 + 액션 버튼 */} +
+
+ + + +
+
+ + + + {rows.length > 0 && ( + + )} +
+
+ +
+ {fileName && {fileName}} + 총 {rows.length}건 + {errorCount > 0 && 에러 {errorCount}건} +
+ + {/* Drop Zone */} + {rows.length === 0 && !parsing && ( +
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + > + +
+ Drag & Drop 또는 클릭하여 BOM 엑셀 템플릿 업로드 (.xlsx, .xls) +
+
+ )} + + {parsing && ( +
+ 파싱 중... +
+ )} + + {/* 결과 그리드 */} + {rows.length > 0 && !parsing && ( +
+ + + + + {COLUMNS.map((c) => ( + + ))} + + + + {rows.map((r, i) => ( + + + {COLUMNS.map((c) => { + const value = displayValue(r, c); + const isNote = c.key === "NOTE"; + return ( + + ); + })} + + ))} + +
# + {c.label} +
{i + 1} + {value} +
+
+ )} + + + + + +
+
+ ); +} diff --git a/frontend/components/development/PartExcelImportDialog.tsx b/frontend/components/development/PartExcelImportDialog.tsx new file mode 100644 index 00000000..df943cee --- /dev/null +++ b/frontend/components/development/PartExcelImportDialog.tsx @@ -0,0 +1,280 @@ +"use client"; + +// 개발관리 PART Excel Import 다이얼로그 +// wace partMng/openPartExcelImportPopUp.jsp 1:1 +// - Template Download / Drag & Drop / 파일선택 → 백엔드 파싱 + 검증 +// - 검증 그리드 22컬럼 + NOTE (에러는 빨강) — wace expenseDetailGrid 1:1 +// - NOTE 있는 행이 하나라도 있으면 저장 차단 (wace fn_save 1:1) + +import React, { useCallback, useMemo, useRef, useState } from "react"; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Download, Upload, Save, Loader2, FileX } from "lucide-react"; +import { toast } from "sonner"; +import { devPartApi, PartExcelRow } from "@/lib/api/devPart"; +import { cn } from "@/lib/utils"; + +const TEMPLATE_DOWNLOAD_URL = "/templates/PART_EXCEL_IMPORT_TEMPLATE.xlsx"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + onSaved: () => void; +} + +interface Column { + key: keyof PartExcelRow; + label: string; + width: string; + align?: "left" | "center" | "right"; + showNameFor?: keyof PartExcelRow; +} + +const COLUMNS: Column[] = [ + { key: "NOTE", label: "결과", width: "min-w-[200px]", align: "left" }, + { key: "PART_NO", label: "품번", width: "min-w-[140px]", align: "center" }, + { key: "PART_NAME", label: "품명", width: "min-w-[200px]", align: "left" }, + { key: "MATERIAL", label: "재료", width: "min-w-[100px]" }, + { key: "HEAT_TREATMENT_HARDNESS", label: "열처리경도", width: "min-w-[110px]" }, + { key: "HEAT_TREATMENT_METHOD", label: "열처리방법", width: "min-w-[110px]" }, + { 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: "SPEC", label: "규격", width: "min-w-[100px]" }, + { key: "ACCTFG", label: "계정구분", width: "min-w-[90px]", align: "center", showNameFor: "ACCTFG_NAME" }, + { key: "ODRFG", label: "조달구분", width: "min-w-[90px]", align: "center", showNameFor: "ODRFG_NAME" }, + { key: "UNIT_DC", label: "재고단위", width: "min-w-[80px]", align: "center", showNameFor: "UNIT_DC_NAME" }, + { key: "UNITMANG_DC", label: "관리단위", width: "min-w-[80px]", align: "center", showNameFor: "UNITMANG_DC_NAME" }, + { key: "UNITCHNG_NB", label: "환산수량", width: "min-w-[80px]", align: "right" }, + { key: "LOT_FG", label: "LOT구분", width: "min-w-[80px]", align: "center" }, + { key: "USE_YN", label: "사용여부", width: "min-w-[80px]", align: "center" }, + { key: "QC_FG", label: "검사여부", width: "min-w-[80px]", align: "center" }, + { key: "SETITEM_FG", label: "SET품여부", width: "min-w-[90px]", align: "center" }, + { key: "REQ_FG", label: "의뢰여부", width: "min-w-[80px]", align: "center" }, + { key: "UNIT_LENGTH", label: "개당길이", width: "min-w-[80px]", align: "right" }, + { key: "UNIT_QTY", label: "개당소요량", width: "min-w-[90px]", align: "right" }, + { key: "REMARK", label: "비고", width: "min-w-[130px]", align: "left" }, +]; + +const LABEL_LOT = { "0": "미사용", "1": "사용" } as Record; +const LABEL_USE = { "0": "미사용", "1": "사용" } as Record; +const LABEL_QC = { "0": "무검사", "1": "검사" } as Record; +const LABEL_YN = { "0": "부", "1": "여" } as Record; + +function displayValue(r: PartExcelRow, col: Column): string { + if (col.showNameFor) { + const name = r[col.showNameFor]; + if (name) return String(name); + return String(r[col.key] ?? ""); + } + const v = String(r[col.key] ?? ""); + if (col.key === "LOT_FG") return LABEL_LOT[v] ?? v; + if (col.key === "USE_YN") return LABEL_USE[v] ?? v; + if (col.key === "QC_FG") return LABEL_QC[v] ?? v; + if (col.key === "SETITEM_FG" || col.key === "REQ_FG") return LABEL_YN[v] ?? v; + return v; +} + +export function PartExcelImportDialog({ open, onOpenChange, onSaved }: Props) { + const fileInputRef = useRef(null); + const [parsedRows, setParsedRows] = useState([]); + const [hasError, setHasError] = useState(false); + const [fileName, setFileName] = useState(""); + const [parsing, setParsing] = useState(false); + const [saving, setSaving] = useState(false); + const [dragOver, setDragOver] = useState(false); + + const reset = useCallback(() => { + setParsedRows([]); + setHasError(false); + setFileName(""); + }, []); + + const handleDialogChange = (v: boolean) => { + if (!v) reset(); + onOpenChange(v); + }; + + const parseFile = useCallback(async (file: File) => { + if (!/\.xlsx?$/i.test(file.name)) { + toast.error("xlsx 또는 xls 파일만 업로드 가능합니다."); + return; + } + setParsing(true); + setFileName(file.name); + try { + const data = await devPartApi.excelParse(file); + setParsedRows(data.rows ?? []); + setHasError(!!data.hasError); + if (!data.rows || data.rows.length === 0) { + toast.warning("파싱된 데이터가 없습니다. 템플릿 형식을 확인해 주세요."); + } else if (data.hasError) { + toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); + } else { + toast.success(`${data.rows.length}건 파싱 완료`); + } + } catch (e: any) { + toast.error(e?.response?.data?.message ?? e?.message ?? "엑셀 파싱 실패"); + reset(); + } finally { + setParsing(false); + } + }, [reset]); + + const handleFileInput = (e: React.ChangeEvent) => { + const f = e.target.files?.[0]; + if (f) parseFile(f); + e.target.value = ""; + }; + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setDragOver(false); + const f = e.dataTransfer.files?.[0]; + if (f) parseFile(f); + }; + + const handleSave = async () => { + if (parsedRows.length === 0) { toast.error("저장할 데이터가 없습니다."); return; } + if (hasError) { + toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); + return; + } + if (!confirm("저장 하시겠습니까?")) return; + setSaving(true); + try { + const res = await devPartApi.excelSave(parsedRows); + toast.success(`${res.inserted}건이 저장되었습니다.${res.skipped > 0 ? ` (중복 ${res.skipped}건 건너뜀)` : ""}`); + onSaved(); + handleDialogChange(false); + } catch (e: any) { + toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패"); + } finally { + setSaving(false); + } + }; + + const errorCount = useMemo(() => parsedRows.filter((r) => r.NOTE).length, [parsedRows]); + + return ( + + + + PART 등록 Excel upload + + +
+ + + + {fileName && ( + + {fileName} + + )} + {parsedRows.length > 0 && ( + + )} +
+ 총 {parsedRows.length}건 + {errorCount > 0 && 에러 {errorCount}건} +
+
+ + {/* Drop Zone — 파싱 전에만 노출 */} + {parsedRows.length === 0 && !parsing && ( +
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + > + +
+ Drag & Drop 또는 클릭하여 엑셀 템플릿 업로드 (.xlsx, .xls) +
+
+ )} + + {parsing && ( +
+ 파싱 중... +
+ )} + + {/* 결과 그리드 */} + {parsedRows.length > 0 && !parsing && ( +
+ + + + + {COLUMNS.map((c) => ( + + ))} + + + + {parsedRows.map((r, i) => ( + + + {COLUMNS.map((c) => { + const value = displayValue(r, c); + const isNote = c.key === "NOTE"; + return ( + + ); + })} + + ))} + +
# + {c.label} +
{i + 1} + {value} +
+
+ )} + + + + + +
+
+ ); +} diff --git a/frontend/lib/api/devBom.ts b/frontend/lib/api/devBom.ts index fff3363b..90fde805 100644 --- a/frontend/lib/api/devBom.ts +++ b/frontend/lib/api/devBom.ts @@ -143,4 +143,88 @@ export const devBomApi = { const res = await apiClient.get("/development/ebom-tree/descending", { params: filter }); return res.data?.data as BomTreeResponse; }, + + // Excel Import + async excelParse(file: File): Promise { + const fd = new FormData(); + fd.append("file", file); + const res = await apiClient.post("/development/ebom/excel-parse", fd, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data?.data as BomExcelParseResponse; + }, + + async excelCheckDuplicate(partNo: string, exclude?: string): Promise { + const res = await apiClient.get("/development/ebom/excel-check-duplicate", { + params: { partNo, exclude }, + }); + return !!res.data?.data?.isDuplicate; + }, + + async excelCopySource(productCd?: string): Promise { + const res = await apiClient.get("/development/ebom/excel-copy-source", { + params: productCd ? { productCd } : undefined, + }); + return (res.data?.data as BomCopySourceRow[]) ?? []; + }, + + async excelCopy(objid: string): Promise { + const res = await apiClient.get(`/development/ebom/excel-copy/${objid}`); + return ((res.data?.data?.rows as BomExcelRow[]) ?? []); + }, + + async excelSave(input: BomExcelSaveInput): Promise { + const res = await apiClient.post("/development/ebom/excel-save", input); + return res.data?.data as BomExcelSaveResult; + }, }; + +// ─── Excel Import 타입 ───────────────────────────────────── + +export interface BomExcelRow { + NOTE: string; + PARENT_PART_NO: string; + PART_NO: string; + PART_NAME: string; + QTY: string; + ITEM_QTY: string; + MATERIAL: string; + SPEC: string; + POST_PROCESSING: string; + MAKER: string; + PART_TYPE: string; + PART_TYPE_NAME?: string; + REMARK: string; +} + +export interface BomExcelParseResponse { + rows: BomExcelRow[]; + hasError: boolean; + firstLevel: { part_no: string; part_name: string } | null; +} + +export interface BomCopySourceRow { + objid: string; + part_no: string; + part_name: string; + revision: string | null; + product_cd: string | null; + regdate: string | null; +} + +export interface BomExcelSaveInput { + bomReportObjid?: string; + productCd: string; + partNo: string; + partName: string; + version?: string; + rows: BomExcelRow[]; +} + +export interface BomExcelSaveResult { + bomReportObjid: string; + insertedParts: number; + updatedParts: number; + bomRows: number; + mode: "create" | "update"; +} diff --git a/frontend/lib/api/devPart.ts b/frontend/lib/api/devPart.ts index 3798a600..caea8991 100644 --- a/frontend/lib/api/devPart.ts +++ b/frontend/lib/api/devPart.ts @@ -203,6 +203,50 @@ export interface DeployResult { eo_nos: Record; } +// ─── Excel Import ──────────────────────────────────────────── + +export interface PartExcelRow { + NOTE: string; + PART_NO: string; + PART_NAME: string; + MATERIAL: string; + HEAT_TREATMENT_HARDNESS: string; + HEAT_TREATMENT_METHOD: string; + SURFACE_TREATMENT: string; + MAKER: string; + PART_TYPE: string; + PART_TYPE_NAME?: string; + SPEC: string; + ACCTFG: string; + ACCTFG_NAME?: string; + ODRFG: string; + ODRFG_NAME?: string; + UNIT_DC: string; + UNIT_DC_NAME?: string; + UNITMANG_DC: string; + UNITMANG_DC_NAME?: string; + UNITCHNG_NB: string; + LOT_FG: string; + USE_YN: string; + QC_FG: string; + SETITEM_FG: string; + REQ_FG: string; + UNIT_LENGTH: string; + UNIT_QTY: string; + REMARK: string; +} + +export interface ExcelParseResponse { + rows: PartExcelRow[]; + hasError: boolean; +} + +export interface ExcelSaveResponse { + inserted: number; + skipped: number; + skippedPartNos: string[]; +} + // ─── API ──────────────────────────────────────────────────── export const devPartApi = { @@ -246,4 +290,20 @@ export const devPartApi = { const res = await apiClient.delete("/development/part", { data: { objids } }); return res.data; }, + + // Excel Import — 파싱 + 검증 + async excelParse(file: File): Promise { + const fd = new FormData(); + fd.append("file", file); + const res = await apiClient.post("/development/part/excel-parse", fd, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data?.data as ExcelParseResponse; + }, + + // Excel Import — 저장 (신규 PART_NO 만 INSERT) + async excelSave(rows: PartExcelRow[]): Promise { + const res = await apiClient.post("/development/part/excel-save", { rows }); + return res.data?.data as ExcelSaveResponse; + }, }; diff --git a/frontend/public/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx b/frontend/public/templates/BOM_REPORT_EXCEL_IMPORT_TEMPLATE.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e763b48c94fa57951bc1515367717a75dd51458b GIT binary patch literal 92438 zcmeEud0dR`+rB+(*|Vf<(SnjSWfCHVh=fX_jY>4yn+BnWEJ>+o6N%D7l-7AjXwe`f z(_(Ad8?B?JX@2K*-Sh~$1SKPOI+-1XGabjA{Q}e2z>wl z{`+5SfrqScQiv_v(?VX=oIbE)cw$O5?!K0DS zWp~;#jTW@1PmBAyVBh+6KW&$E?$eupa7^^&IESXSeC=?pGj|{Ext!$dvm#>JI1Xn- z(NnRl&%Pd7a&)d>uI$tmPew1RdfvRfW_|2G8XIIY71%LDI=T)eJ z#~&#hRG!u_p;d|+H>7%0Q|q2plD{Q+{^D0Yu3mjL}AWcmjENrPtOVFSH zm!AKNbF$agANp<6%T^wHo}=C6{f7Ig%y;~HU1v@0^f|jdPi-lcczXHH{F!Bpg;L{p z&+r<$nV#v{mB%Vmzg+ufPWzAL4}B(1R-2dX9Ctpj^Ms?A!UBgIn@&9Z{&IrTr?yY+ z!JBnl<(x9FD#Sj!e_J=VV&0~rrj3urGnW|77&R$M>ipET!Ke4X+P1I!(4Y9hpVsec zh&!K{vMQqXI`bC2_{P1OaT@=qZms_iJmn4bkX1>%#~a%@Z5=ZVc3G<)Njwzhux75S zsk!aHEgNq!Ch`WKPt@4uVo|J==+~6Z^{pKeYt8>cI=zqxo3Q2?w3Qw z`a4NZQ~U#4z#?goBr^t$9PDDNMC6IHy@SmmdwUyH&4f8K7)lz9^1u7Phx*%eUjn3$ z>GoE3zU4G!Ldi*`x%CEh5+ie;$g>vAS?1YVF;gMUWbbEPrJ?rcT{&-GJ8@Tt{}?){ z?YZLpixY>rO|s>v4Yz;W@^K@#u{2$|l& za@U_}U!*+wHRP1Ut6Ilx`Z_Bj7ZiOywqJGYB->bDg>A>0zwT|jdQk02=812wo=%+j zqi{<1lFrS)Lms|+ls`W33v&;J)4`)T39My@yGp!rh?_X$=?0y37Hg`GhP|tcP4e2H z^YE~O)p|wk>KTjH>V#8jl%DR}`t-o~VRE|ndPiIW=(o?I(1vu2-O zJT9?x>Y(ixBPN~CT&EImz2Tp?ho`x%{5gHB7War*9KAsYP+yBiX>9+wZZ54ihiLK7La;ZqnI(ONfxUkffk!S9`5v#iVV)n(G z^P>6FC65=q*}iw*0p*&ckvhYY8kbjzH=k`+{;2yb$GfDhK4bw4s<98~Ix}bho#Dsy?JLEd6*|h%JNmC$+>@ z!#+7YYkRV3mc2%IQ2q25(rJ8&8;J!r<0D3HyZyGyUh_kiJ$I(KE^o7gjNhlASYxG0 z7Xz&_SDAj!KOB}m_jYJ@_IU2Cr_^sPFI>gK7!MbZZU53X?zvQ(_U^Lj_FZuoek*jI z`1LmWMU75PW>G zL4zg}!AG?=bFeVqb<*L;F)J#9s{#Wf`-z&9Jy$hpY-K;7_?JaVDwULt&|bPtcFxA| zx1-0{8e5*QvVOVn!w#=U5?VfyQU8p(xKe%h(aias-e10Jtag8NbC&iDkNd+`{tjJM zp<1P8CEols+iz6z$(b{!>PJkTFz4OQ&w11DNBpana^0tR_P-l9sJ?yT8~<<2mG3v* zI{Ypz9bCWY#pp#7^|R)TTy6AGcgB^y7PFS>mOs{=VW!b==2U)ieC6?-#anVGM@BwV z@LxVu)Ay;uKXS(_6^2bQSHE99N@nks^#-rbD@<~9*b#jvxy4g%%*KG>lC9$k-+SjK zonLnJ?6@atcZZ4xyf~I}Y`ey@<+HAw|K4=M&ch*3|JztowV&@dU$LxzE;Y{XN$4uQ zd(`8qyr`z%?GpP1li9~Vseb)#&Wd|q{MtC9==QQ|J^Axa&LQRLiS7$h)8rQW_(Z;> z%nf9kT5d1kG~~vvq@jjdB9IbrUv4IkC)p_5(Le}=6`BPZN%;V>14<2;ZzoA@D`*QG!I`{!;|eTuQs0mbQ-pH?k#mC}0q%I(_{t%UTTZ#i?1z8f<&#n^nz z`DrCj#x-B^m>2!8+_1a*EsezXjT$rDC{K-VHRjL7x2|7`k6W-Uf|jd2F>NWJZqm+K zsq$#`%;O8nH15`}P-a)Tnq}&|I}j>eA8N5QaQ*?==Ru}>Wv@k4wp=NrzrW(*(Y<|S zJ;#U-jo{xnsb;65`y4`aJ(SFOJvd3Vwy-!!C95p{%TiH|J$v=}#TUwxwYWpOH@LW8 z|9o!jhFz=XJ~OBqdZ_d32v(pkcQW5SNy_5i%F5%+%9Xcx>JMgGieFpwF>T1XUxy{F z6MwsZUChvYw7Rx*@~Fk?zGg*BvThFDzFp_SYu~+7#ymQ=b3T7sw&%-|M^{EY@TIUz zwp?&k&h7A-`R-xNQM+bSu_sE~pK=!cq8WOvS*yRVyZ#DIM*Y|wMxk~y^XwK?--DD$ zZPhng4b7(xY1TQnhuP4?huyO)$Zu&MHIN0`4sE6Tj~F!Q*G$wX<4SST+QQC43IB<= zUB9o)-e|>S`sN?>#75o?PW*!#+qd0(nCKY$)zqy|LC!k6Z8?QkMK&G3EN%*+JoX zD@sQnihBH1f6IC0OO;w-*CvNuex{vxCSk6^xZ#vxsiPDP@+$-M&m5T%F?r$acDD7# z4QIOleETVRl`{5o^oR7ZyYK4Mp7y=4cJZy%yT&>_Gkrf&_1dfj?*?1O4VpZz!z{dX z*Wg2Y6i!)vF{i(;qUApY4?6v|uic+|8X~K-qxibp<_qesOquvI43_W;s zLUDtWT3O!Ku)*Jy&j#!_x~Ni@`ANrJA!*QK8A%K0yvq({Ig-DMr`jG%W;JYDmXb4i z#cbZAtW&0o6F#nr5idWrDC^Owgp^flLZ^t?ubawve2QaoH2$MW#^9Vw_jqHhXT3Y1 z6iD&9w3^lY!f!OM(w|(M04<%d?r)PNZiC*fJ3nRHwVUSVXE$j76yHAL z%#T49t1Bmt$ou4wVC=={OgTpR^wce5({Zz~S)(go_$Cj_-SKYg(=*PRM|Ygc@Mv$y zx6*r;Z*}!*qsH1aLH2tuvCUjY%Hxm=MyoGhoMfur*~(2Q-^^Is(V3IY=$*XZ1x*I&*q<)(GlQtVnT{f(`h|tvFPpA=|9J=o}%yco*Npp+OknOL_zABzuevM zCfa)SqQ)Jye;jX}Kch?0uNK#`+nTwgrs(u_?UpS@%TGVs>M>&d{aF>Z6`Sk|Jz~q} zi!GTrddRY?O|%zMs}AHGJ~t`+4Q2b@_uq>jU8HSl+_rJ=OUI})+MCaw_&95#JdJv6 z^j`Zp)y2w*!K><7wtmmFZ&zpOu^kIsC#2?b)s}8oNqD>bvdNXa;`h^VpGtx8-rgsyQtr;iPa9> zd9dy9-Ex-=wN8d>w%5)~9Nej6Q#UO@DVslLl*gp2MYD#iNYak9|HB(c7ZbR4AJ~&$ z#r#T*93dXgt+{8TcVlJ4yu2@AH~mc3D#UmmRXR)|=E&e^gFWf3Zg~oyW1@~x1NSVwH(meajg>|!FOQ!%qP)6g zLF?L)X|lhYN0&~wW_@3NcKYLL(`J(-8spqv{xo9^}t8r9%b^p+rO+2du{tC$b7ux1EXyPbDupN9guj0erV~flcAfFYV`6)mxk%%{ zyN5L2h`;m8&6;%9pgA-DqIv4<>pO>2EiB(k`wo54d2^^?Yq@m9H2t!~E!&5Va8?(T za$JXxn_Kp)YnPggQGu16#7XCaMvK%0{+ZgD>pIWHMbwn>w(708<7H@DZ&iA9@!`b} zpM723w&&LK6_ZMrD89Mid(n5o^;d2i8d^-g?HR_Q*t(ya9jo-^)zBknp0ut$^(t5V ztNDjW+iXAQl&bPg&&Roz*A%v{59kiq&b?F^w!$iYP4raLGFsT%l2q?mrjlwVS7UEm zUk$u}?CL$S>650l+>HA(_mV{8`iNEE1G0nUkC-kf_0rW^ZSf^=>W;3J&n#;@!=Xg| zUB;(xil_a60v{?hXwW#Kh*3|v+FD?$S-svEkjPA#>sfUyyTZ9bT1x+U#ZbkaGt-)(6U+2|o4s-A`q6>bwIgzuq2tBtL%> zYkB&n>hIcma%0ysMqPV%cvY-Le)RVbKV2@4V$a;7TfCnB+f8~Wd)ugWCPuTEOzZsT z;vM68mtHOZws2ixgyt#Q6Q9TSt>Yq=&ls`%$0K%;!OL0v$=YMYy>9(et`#3NWbnP% zP1%1ko$4mI9Ikm~UA=34@GJJN&CVvfy8kJ@CK+j763w7cx;zv3EnJy!&u*=))6eXC z(OmO#h|){*>&MpwxJ;Gy(R3X3(uv`Bm@}au+|%Pv+kp(Vvv<{X$GR)eG3szUd;eRG z^+>m0|7a~4`=qtQEos=eO$QxDsiYob)~dFw+`~GSHRs+i=CF$$=VGKKJ-6=i4Zb&N z_08KWZmPPa%zL1C>RjZ_Q?cJxIOT9|XUy02t@3|7{n;_gYb!%!U02MQIQjVM!Pzb6 z{xECwL%bctohUO36IVrF+k7_ELhWPdx8`qUSEM7SA2_#e~9&J$WvqVH?r#|cxceJb}gKK>Advw z7P*ZR&hCA=x;@9>TlV;+HEZhz8EaPt9ygpW^Ln3=nAke&cdU@kNzv6a(c}K z+UfO!qo%g~=~_QnBlp}~hp01U8On@8YI9vHH@)jhK&HvLT`!G;yKbbuq?R1eEKrua zm@1X?e)}z#4z*3H&93^&>s)Nd*T#g5e7WH9!U>PsSHG{iyewvvT+o;o_SQqRzROgL zHRqkuU7s2|dex`)P472ae6JRGb~Suwy5}g|Q{1}xil5^8yC+z))=`u9G_JiCmhF?N z>ZWyw_0+9li{Uogq8|&xmGeGWZ)o-y^3$`bG;Fd;t^Wzi&b-yS^f@MqZ#|v9b9C#S zoXwBCSy{D5Pcc99$bpVoYR6{5qjrw+Z+toT3gDgL?>uY_qZCYY$jjtRSw zq`fRHB{FjM&Ci^A==@uW25SGL zBxxo;vQZrrp;-8PqFwNt5za$*Uri3pUAa9gVWWY*Pt;96*6wX>;zj-iv9}a;l3&iO z+cF|}!^hn=D`oe`teAO3EPemcqiR>5%pdn=$J4uG|Mhx*lX2np<#B?iQ?jpgXIK7t zsF8Y-6&I%3_;gM5n`3PaO?NGvm%O@ufwhXo&JnA;^I;N2K3C85oyn6mUP1qQFT8Qy zZ~1cLN{*Dt^!d*(o&jSaKruR{b~QBcBn3aY0zQM^ zn85Mu;&SPpy!PzPuID%v^o}1)8A{i0)>REp0h?*haOZcg;&{67Xx-rn75qPCbrK!n z8r?NNxf)%6+S?3yS`{>2Q(I-eoa1rvX~`! zYT8cEa^)3sz3JW6*>xJ--y2fwD?D6y2KWB_xFc}mHl}N2xYBsn>D{^C>D_g9QWc&Z zKa%E^G8!2f-K=zm$Ft1ed_R_l;7?;2qvFj?Nm{T*vqS=e&U;_c{d+@}F3+&zccVAm zmDlFo-H_s%OV7&sQ{8q;qN3|Hi|$jOFG=^{e$P(l91{pU&rpWZ_&?JHZajYDAwhQe z*zyBOlPgSrbw?&FS=!PqXs9^H(x{sGyQzXP^fkS^BYkN@)#8fJ3FV&rPNfRZYVZl2 z(NUWn&hTi;@d+;xcyNm=8&=UfTi(8mX5=~_3a_$fbh3W8^KXU31o5{y^Yrb@X}5~s z(mh+tR;4&^7SE{@bTw;T>Tc`4SschnWp`?1(FA6lHSW!9CC*`9bV91&w7`?bEo$rF zgvT>HoOsQGZ+_9?OLFM~kKCeeS;6xat~vax!Gi9Ns~x!sCM`93#lOnBTZ(HuscQ2D z-|BwNOrwW(ahb&)Cz4vavn$%ETnVMDUz?IlX*};A&Gg?>?7hEx?(F^^?bG}z^T$u# zulDOI8Qs4-!HG>}rW$$U*iJVcemJD3b+xw#$ZV&~V;m85EXWFPQ8LY7HVLj)m{NFU zuLai*bJ+`XV@(5F8B3;nny7TAnRhzocGdY+7R%Np=qA(UI#y@@;caZTbnD)D?1z%+ zFaC9Iwcex$BODBW?dURjzc#?H`?=39bFv1LsA{(6uQ)^6P`EfQ}S49>`%;Zft>EP>$T~Ebq?~fUVeGZ`#<#Q zTp4W`L*vh#-5gHk^Vr?=8*Qh571go?|GrbAF3IAjGBpG&`lO01epN+-q0SQ1-9ue} zD|`y4@6eansMR*(`o&ra6^%b$yZyAbv|ZV6uzOyabWlgmwxyR?%*R%O#Ag@s%|2Gt z7)s1%WQ^g;sAl@vIBuTi|1o>V&dgB@r+ktz_xx=TlfrENcYB-TXEDFd%%*Q+_|1Ek zyt;O&x$8vfONPpo^j|lIcy{dD$^3ck-WvbxdE449PosDLEL~sOuw8ZSv+4&oTa8Vh zsHfe(nb%pZw~H~ni+z%DDe6yH6}!2M)3MFCYxR9iQ?J`24v)JxqhQ6}qlupns-1dK z*1$<-dU{p7JeNMBb;Y!!%AXI$JU8}=8euaod8k~e?69>JW!sFO#SZu?jWxc~zV-#A z@3mIn!7A$)ycxT1wxYS3-v?_}#lFT|o^NhDLo;xb&D&F!XSe!Jf4pwb8>jhE@9!~2 z8#e~GTR&N__GaJLcD>2+eLQEa`Ke33qf|CbeKWe`%H8E(CtE3LEKCZgYiFJ5KhLh+ z8C@Z+o=f*%mwr2F`kYDT=NI`-c|2>&ktgzJO=^d=jQ+4;#G8@9vzM8le(2kvt#fBv zZq@qM(|-*!GR^nQ?EdrmdoLL_S6gEG?0M#A3Veq@UY|Tq{?dV4e(&R#_)WO$J=S-E zil*h8A^YaAuK90*Uk8{EtC=&@y!TdpDZqu7#!E?r#pR?VpKf_LE)2xBkGVs`r18(NZ z--fzZuQ4@gWXo{hfgDAdGtj30Uhs?R=xw>yhLQs=Zr6Z|iynA!Tl!lLFK)n`q70ar zw7=2^d9TxdKx@kX&lWs+lZK7XCMcxb1sx&%Qwj#ZG~nRV2ONCafYz`Y&>BWndTmN4 zS~&M^_klYE9n68fIAAzZ21E$jrv72~qPhxsF)(z@{CUg!Lgqd5R@DKwLmhBCRReB^ zIiPb>q~FF2I&Xf~vTuFyzeHI$VDN44zijN^kO5ur*P2J)FpTxy`wT!Ue@Hm#OoX7pFK2aGQQ&yzf-+fF3e^! zbH&A~896P34Z>~>DXNscuw^pyseHG*6o@PtCS8an>8A}l1k-pe_r}HeP|2Rcd&F~|<~gpHI7dvjTw;HQ%tsfE z>L$+}zi8#ZG7U{vxxfr^EuQb#{D?xHW;oYtJxA@v3Z=l<1&`{DKTAXW4UHi zX|Z|s1*^-v8_K>g!;>s3Bw1NnO}64mzl{vk(`%_b<2%utV#auY(HT1o!dUHQ$)`;p4wbC5 zg(nk?4GNQa%DT@DDs96Xv)iQ`&a2+awjIIPJ8nr{NSSwuc)SADNybvs<#r6aG+82P zUe}>5&*adr^q$2Ol+c0vJET>8?zsdQ`-G0 z1ZG|7ZTe7L68*|n!Z1J;CSPZ4!1hR!V5z0dWWzSKx(;0?hQ3Y=ZSmt=ObFE|^iK}u z#7@uahM}R&u^9zsP8Y#WEh^so6yp^b@9LI&H*6~fdwu#LnVy5~bpW;to0w)eg&D1C zt5k^s=uWj7TJ-y8F4hU@~t@4X@{27=T4i;6tRLF5@PrV!ZGv-H0mEY$p zu@|kvb;DL(HEoY+9KmeNUL?a{Sa+xnt$S^dlf;y@n+?NL0@WAKVbsT!A+NXlR+v6% zuBUE^eUquoftSB-idW!N6;=7UpLX`L;+pO8Gp(O5S?R3YY5ZZogF(lNOO(JT!Qd2) z*so!_`?q^O3N`lUB{O9g%YlJ-#viUY7zDS}P;)asr<;M@7AY{k>;|{cmK@oa3dXPU zYY&i&Ia+APl#N@&__B|9WJf$&85o-vq8F~491v(~t0Y+|dHH7cB4xvXiWRk0+4b;) z=_>ePyG?Mu(E%^rqm!qJ1(81Tr z4>$OH6U;*mddmi5NjdF5=2^n}7bRi+Dlk%IYL+rG_UqJ`t_3Dsv(F)tG0oD+l3pl% z_Bnb!sxzL<^rRG|Q4N~vtFmp?m39YI8SOs;Zc1wFI@FLV7Y32%5w8Fr`#cZaS76%l zlzB|0sS!TSGb7>AX>i{Bue6{Q)iKTPk$PM+cdAP^iq>J62*?mv7r#u((m5@r zYlw@6j3vzt(#B(qG7679_f6|}NTSe*+lfQx$(BsWNBiJ=5PStI+k6~rBf&l;_hH{} z&2Gd<#^h01)Gh9xl%~e|jMxdrU1SLFv6XqB#u!wlF81E{$n()*lgSX85BC|a@Bv3y zeuDYbQ!15NpOrqx;otmBi$O+Di$AIewGRx@)S1I}#Hiq5f>D7EA1L+nn$Noj0Tg^u zN>21?_5}Y;NRJ7zGUuKS|Y{>VoKMe<_^F0gkVn2J*7{d7u_sja1CTcqal ziwoW_+P^W@T5*SJRc29FhZW1O-Dhz_BuXu<2Ao<>Ph&6QtjKf)P!2lRAUTr-h@o2W z!3jL0T2S7Od2+THm_JpY`)}&=c8A!c?hEg&Nz$~Q4Irpott>;nP@%e}NFf~9QW-~{Z(;6x<1QjkzJWO*Qhm^KzQ>mX8Ia~o?|riL zN4f|6xk=#?N!t$?lp{ND>R4|O+5kp~YL+yr#76L`#73yUK^kFAJ2t|>?nBub*a*3V z5!SXK%d;p2F&#c8Es>~hIJSUKxMnL05lnH-?4nvEjjFUVt;b)E+_Kzf>U)K+qjwj& za%||`CCR6{4-L&k7P|2cTWD=7VWCeA$U-;Xo7&#N78+B7Eu>aMT4*sp=YSiw&`rWZ zp~ylvv)k8m&#Uq}loe~0oIXN8hwJX&m7qX{5Z%^>7fRidU#k>Rnq6Sl{wX$Z?L)vb z2$p^D9c06|?%5%4&88Nc+Q#E)^Xu@mygG8)bNn1}_nqYstv2=e$u0DON)g>Y2m*W! z&xh6yMv$_M%7hq^vW%sn>Utx4Q8MF;tx_HgeuD*Ofs%qg^r(A{a`-t{-1~-40JWU; zJ)E8#m(KX&j9neT#jdW)BwW4I1Etp|u4Fm~r&p&tPOlMNWO|*UKN@c859NN*3veG` zH~K&XgOX(iaWSh26V03$K3ze4vPpY95EO={o(< zLOk%pJUp;%KBfQ;LFYOoXSNWj?m?tFe6$=9XT`EH15g(na)*IohzKw>Sp2r>{TNli zGn-bW_G-KqgHF5_JNT$dl5(=ED`MpPbSw8Y%@k@@V_XZ9!g6-4OcmEbynTT;Cd12A zw_?)Fbokaf(u5w|g{^Czj zVAf~vuCxWZZ7H)LuY@4>lzg+|OvkL}%T(*TM+cyGp``<+E{-V(W)kadDXGeicdr9a zQe>Ry=klOquyP23e#JojdiF*%9l4MZ%wLH-#og!uIfe_n#f)4?M`B(1nu>2QXqoBl zX0%4B1<;+~&7ckdg4&MOX~=8smw(+J!zfEk`dksbXctK^yjXh!XDkBgJkCWxwZiJ_IYXdgWr-VPxQ zA@1zj;k*U5imVMEkdKm0CA${=jykbz@t&Y2jtqlyP^949#8cJ1Qo%R1HFp(>&)^J|-c@2^f=4X5C z2nd77L}}*se)bHl>()~jM;bht0$KnRbFlu8)WYqk7bK!wSzX0OdpKGQ9(ir?)9OxyE5dzHd2Yy2#jkhgmKkz zQYKmQy@tSQ3M64XSeFc|A4Hz~AfB&siQk0?cCVEXTN-2Fm<+9JzqJxJBs#NypjHw% z6d}y(p7d@gYaJH`-CB6}l*x#=qrk&Z*n~*P=ML0XV&{^p=~>iga9vai1(8=s0tX2y zMvl_#Qa~lBiSB!WOHuf-v`);WL>Z6R0&uB;u)Hu8&#ScUi_f?6GA8<@)4Vb(oT3&hS+_jG#dk0s&^^` z3IqZ-5$Gf_aNi<W+LzeSi9z6E>S7q7Sej zs=lb)3={_lgLjuy0brr<14tW6qEk+bD2>#MHRU-~GkVe+l>st{&L=gXeJSFm8I>&P z%g_fI2yS)Dh<%{Sg33>63i<*B5zp!WBZ$aJL?5Ek>VPNdQ-_GCbg%1b;joBQ5~cwO zXh1rJbyAh(=p;;DvOq&o00xweWDf#v8qIKcp=cUA86=**SfgGn$(G)82k~lDY=v*5 za5mLcW%WRl2|A8t_yy&4J+GE>Qw_A2wl z9xV`YI#{t-WIv<$v=DE?}<26 zXoAa@Yy7+&LKZ6`q=`gZT*6JfqQCfrmPy!$|j)Y zA$Guqfp9c!6`urhdR_@oYGU-z1|U^H_x*vPZzLaIgqGnmShAurz$#2P29Jpfc~4ic zx0tAzxn?m0kwgF^Nj6dB>f zz1|irt4Im}zoGTr>9EWZH941GvAB4|(a7zkqX^W9M;X!gXWE+N|-P`4=jctm&;Ad;<9t|De9DjiTW zJV{HTymB`yhxTS;s(p*G{$%)R^zO10`y)}``301Rck(D4)Y)J@Mif6_7Y#)mNFPq~ zQA}7em8r0w{6DT&)tPu;eQ$&5R@vuoPf&={T-hZ6UpT3r-!Lz{5H9h;O>Y zd+8%Sgicrh{UTf_q=?w6h7KceoiCPrMXFgaH7fD8}-&wvN04PTdPUmTMWPSjn* zzmbnoUdOj=@7X`FS^_|Nzg+wP>s;W*Uc_d=kW#r`51p|F;IMO_mX0VHpfnKihOvZf z=HR0}^_S4&f%Mmjh|z)04SdF;5L9CHSC39(2WY5Qvm6!NK0zS&VE7266pDPh0^EV< zs4x;RJ0u_q<+Q6taf!Sr%rHYC@rj^9p}=^$9;)C9rx9B_hMJ7~wlYzHPUJx4EjRqb}fQ=pygwX3`U^^?7SxxrcL2x_KB)sMAWL7B@{k#abHv?1E)F;a9KDP zIVD~ei9~jA6cEihr^;$OCj{kQa}s^5jeqZE26xGzP5+bioo^f+8Db~uh0xYOm^z9a zkUKA?(JbZ3hi{lS&ccEw>dc7N6-iZ)M#y4F#f8=o7exHwiDYSAMWSR>Kz5gf^h!YcS>@6MJaZb(=Kk%eG5;(v2! zz!0^VdeaLI$%q_efn1F@;y^@2uR=g(dJmEy8IX8;S=E{;!n_v-bz=|A+{B#|_eGZn z$b$$cTR7DnPb*>OlkSz{_a6Uhn3WT69aM^ca$h-`l!AEh_0ZNpUlKe2X}}N zR;bZLwHV5i^X?MHMB$P|J)a2pGH~<@z1+vyvkcl1E|C}~f*H)E5x(KJSeOHiX2{A= z#MeGVogA(+NVo<&3S*^@=b*C}K1HORRCI~F5ayEz6otD)hcVEW_9Ehp9RXAkQ1`}f zqM;n1-1S)n!i1hMLh+N5Qm7%|7_`?^nBMLxSR-0I8SlMpDncvbECKaneXxHlco73r zs&_Cr!~+f!pw!^J@6&_;N=d#V-@N~rNfK)?8dh!qpn;Z(8MGzEECyGhvYxlLfMBDf zszkH|$19LMJ;j8LMxTBI*`aTW@IT0Jz5S0uop`v}p09`oTt_nKN@+{z?0Z@`A zv?ME}PCLi#ky&siOl2s2pumt6pikTrN)jm9y+f$SL|g*-glhmgs)sq$iR6Bo?ytmX zOrNmG*-sVEH5(qIUNNK~iP?S~$_7Ec5hKK!Gx79;ju!xCrdwnl1*EOQm55;j|0PC( zO9YkV_-k0eeMvDI7`Q6hI2r*Thhj=2r0kgbfo`27G0CFPiYXgTGQ(fW22BN&UIf0S zgi`;2prqHccO$xwnt}r89k8J8aU(knzY6NEzk|d)P|#6ZA=Jx7D{DqlEEdP{AEXV4 zbf$xn_R(j=d%2q%ldLDiMaHeGq@}}@xqsDuqNBshQDUxaXctBeP+)&mycIEYWC(&J zKXxG*JRFOdxJeR020qE$I)~Sbb6CIBT%S#CspLXGHFw_n@ zGHe17{X@zW@z51ng=A1agyPapSM)ByBiX@v|m;T}gO=gLjjOzIa7jyHdC@4=Mw zSdhd^l-Cn{{8=N@?pK1JUN4b8k18P=*UCLjqOPe`uemi~iH>2^tq&$Ow_A6+$!Cfk zshm6bK&ZtDE( z11hZSLFHAptn6Pkb$KQ2>33m8s;nZ89}GL{7=NEUmCecaZ>F%aef?6?nki@&jc0kh zQkM7W$9k4{2eA7MI>e8`El<4LTsjxJcIpHttwp=fV0s4n!yY)jwnz-a@j}D{*5ip(5%t$oouK4^YfKHo z$qea5R_Vpr-#%A~C-quwGlgfOAJ+X%A=te_RrdA2wmo3Oau#R4j$!(l| ztf7d=9R31-i9E7jy>qTG+6kk}6wI>3k?tG>K?Bxt-kSjN#Uw_QB_hc@UcwRv62^5t zG#a@@T#;eO$P97kDL(@&!oepD6cmCQ&ve*~b+9Lln7OvWj7X&q zS~W2zccm$=^$*igisH_`Ol>4`&##2zhhj(QGluSV*Uo5&y2Pu_)8u%<$^OnUp2@^- zw8CV(q!2r7T*xu$&wwMpYQQ=|dm=c1mD}bOX+RQrXX1FP5TkmRVDn)cLGsP|TnC0m zXyA}+vlB9Jnm#&vz?md+u{`R2aWcauv6cDH4cMi^H&v_{x_JN;B*IXM+`6t#i$Xdq zUI$P4ZUNpgnOgvB{o_W5UGBWa6{mZy2<(V3<&xIahipM%L?v<-LRRR&*A-UP?YZ@6 zfHc2UN{}#`qlNKCVhRbaFu*p_+=2M3yn~I(*p{i;^U;yNPiFlQ&1F%fGUvxsRtO93Oir;ESm_978`ym2x495t+ zO$`g307FFf?r)k`No9n433LF^#byOF30FePAdO8AUY5E7w?ND~Yr9 z6+I>>=*HXFYAL`)CWF}>nxzgdF0U%DfV|L{Q%Q^0lDA6N5!NP@oMMK#q6ols#?-Kp zh{x0v(N5%{XM@ty3tQCrz3Q5C-#qto{fbZ-MGlz&?u2<@U=cpJ*n53pRQHADpR>sL z7pAum)J2sL4scHk%|;g3hF+kH(n*31rOTHnAjJVmYs-474}pHTH~?h*jTF0JC9-I+uQ?bRe9o{uI!LJ9$DuSMR&!5|oZ=jOWZZ-zs48O%wj>=$u3Meoj zmOxL{VegSJT350V7UvocvjlNJs;CE?D}Wzbam{ze*Lc1-5#f znJywF`Su2jb%ODlU}iKE6`W+C!`>%hMDUX0(f$Rk%2DR9FT4;stgax5{ zDJ@=SRaE+i4K)v1_1z>s{Go7NI(>S=P-Jo7I!c&#p&cqx+39!nRgxrjp)l4ChWcNS zS`3M(1aR8p9)J!EH^gFb8KNn3h@yf6hkiUjmFpIy&esC&)feKDppwXp=W{|ghIL~f zbu4+{58MD_M)KNvBL!+>8=!TswF;9BL!T+)md!>M5*zY(MA*Z`W95U1hfpveVc@U0 z*y7yj{x*Z@bTyhWh^YbhPWxB|d)t$N_9r5#h%F$n#v}w~30eqjO2}Ohp#p020g=&1 z*H1wewoxRZSe4bTd>7IjfEZgw7Xy<_0l7|0Y{4=~Wp!pi;^4>@69}|_a)+~4;wp>G z0`+N{khNmODKI5bm@!+H+OXXX7VP^O-sH0?NW9-L;9(F?Qu3LV&Vj#c(AD@%~ zs6h_y&z?i5V751i74~5~Wugs2je_a~w@Afk^Fmt-(Vhec(G<|HGE zV-6-Dyn~*xU6@Z1pI~=xM?%qZxDm9j3PmNZPr@h}D^E5N1SkL}AcGxmOCtV6A*2Yn zrGFWN3Xehs$1&m>0t$p)tRf~*GRCr0dq70`H3Hy(E(rNCKxQB&M%-lJ5+pYu9~ZXc z9swXGNxvLfRzzMfA%EgcTi41E?8z-M{fivsszYkkdd#CXs0C94uv?(Cqc#IwoY7Z- z18ia&@+8AkdkDOO?2K@MqYt(S7jhmEBk?7pt9LH9b8vOGHxL$iw9Edj#o>iDgtz*! zA6zpC1B4Fqr>PWUuHMV!z<~&iht}Z~mn_jZeSlXG_93$pp#~dK0MsD>))Mt*OlfD5 zzDjW;nODO2#pdbaAQpr@MzjEkT;ZY&A>y9SHJNyfMp~_F51FDw1J=u(LL0?HZ;FN! zHY`?!L>S{z2<=EZLMfPb;OKd!`rlx(jM@c)^Y#KXVQECY>8~2zK^(_K=b*ytfhNDy zJrQKi^-vrkZuylkFnJ12qT{xyF%h~TLwV;f5MB3M7+ezdGMGfSM)UwWY}|RFutWdS zg`j!_8Ak0Q#E*ny)>2HIP5X6=<^+Gn0VE1Yl13-m6J7ei*@fsYt^`EyE#$);bqefIZP6C?(vnQ8-?%;e_eR@w#!7{WPG09{cTcxa3`g zF1_jkCWu|ddu9ZqcZjqmQL86FMx;=BoR9In=NX9wB>nM>MZDaxGUX_yh{!mQ@5m~t zDHxY3%X>=6YxkKiyS0k@G9g^nFg_DqJE0cS<4D{T_pa*%lwlMl_%p6Fw2TxG3Hs6J66i`1m`g@s zfoTA`gYb>}9P!b=AL3mLH(yDR=xDmIPC6uuI(5{Yp%MnIE=h*aBcxtoqY`XcBHqEf ztsTsX7KGX{1Xakpz@|R|EywsUB!y_9HlQOyBvSQhy`bom^s0X=h@8>4M*K848*l;`EABup;OO+!ybvQ^q5)!s1Do(cU3A_ zrGLua^Qinl0Z-%l$v3Q zBmsCE(jOsL$TF9j{RR4%VBBb+?v+1f;FJO%_h>8A2<$;32x@?+@wA0lY(n!06&sYP zmxIB0;J*G*6;P0gG8tRPBXGB{;@V2^Zjc-aZ9mus+T%Xa?FZcjV67n}M*lL8X=%do zIsheH<$DPwGR78#qWOf~0R}3ILXlO3(iP}JWKR8$UcMvDdg81JW zL`$fs5>6wU0~;(n6Qz^PYEY7(?LfjGoy<;%UPAm4F=Qao4M!w+JF4gc#$D`%zrI2! z)5z3RqF@SDQv#y!#Q7yi;_HEREWrcpC*Up+*R~3=7Nbh6JT()aUa?(!+Ol+C#Ay)m z2GmLfni#+d_iMqik(N9j0;Cf}Do|ib#4kmS1jKNfoJqhj9p@8gm9E)dYHH!LdfEC2Zp;Uo*MpjOOSmOu> zlHQY5kbFW_(GV>i!+J3~+JJnk0$gI;{clV|^rF~Bc$>YzCUY_3zM`y{16D)a5Q7^w z2geJ^8>C=KC~M?XB(i4#b;as>9uLQvS8N6|6DhcccS+udWl^EzO1ufvc^wPqrV>3L z-U0z*!2~ftbwd>CXtxzH5V5DK=@s_*MW*;EZcjymrchc$(a|?m3JB$F0Ib1^b1yfF zVp+I-IFSHi)#C=-oeDvkSRYo#5WgtF6khnIkW}NiB>)wRhFJVMaINQ{hdA&73yC5J z+P4wEDWnJ@V$SrndkIz95ZXU73i%I(&D{Fz!NML=2jnJo}D13O9<0sZebPz>5Ljw;jriwZbTV{Ol1|2 z=#iuX{lnK@hj1-K-63=ssB9q)rv;ofs#)V$Hijc2j$p9NB`?1j7#^R+tKc z>>)FrhWgfARwEcP4rh7^h7-?rj9)eZYsYK zolw{J+`=mTH4aG=e}D;4cQ7qQF(}+67FbhJT@s$(!H>|oV`deI_eBYUn^b5m1QduF zsS_@Z_8^`E9O7XGJr?zlW1;y%XKhIN>I->@w2M*!poc8} zL}vy^A6Zd^I8Sf{Jf<-6tFT;xGKJ&}WMDxS3%7BVVE~Khi^@&5(3YY__=r>;q7M?j zBs^e%#=9T{&)5fhh`y9ap}Z3064X~=IdBTW_;Jr(Py&i9Q6GdX;I4Ul`h1~#F*)N_e4@NOMMf85A>UcY$r2U_Oo3l$z#X4}9GZ3GMO4ZkkgeHw1 zAb@v6d_RsM!9vU@7B<~i&W68wEw_X&3+?$Ys*H}R>EV?%_I>(iI2=SZssc;<taMmqqWH55Fz0n*Y<{?kpjLroV9h0sra-=C?!C-!Lcf$ z6UgWy4HSz8y$EOL0P56oQ@% zM-9_TJ6qryNdqRWLdLAhGjSlJ?Eu@@XeU|K*_{W4)S5Vj$DSa_5XnK{pL_&{G`FUp z9y$<)L3D`?*n!o)K8w=DHzuH+Bdk-QfDVoU@5Jk9aH@{&ok^^!nSuqDUUV5kLr9~A zD#ZWLIw<@=R7Yo8MAU%tLIWvJ@JI3$Z5lEf{st}*DnuDrwWHk%@PUjK;t4@t5UkO< z;?5is_O4_+0#PMjMW{xmQ7^d1X?y_Z1jkFLS&8Fv2K1flgvD>PN3{aXd{W^b+o5HV z=a9(JA_k3wQXH3kV+Wn5#DjWGbbwuL--1$K7a@23b&!J(ZZZ6l*G~Xk`k)P>gk($! z#{;WmcZ(f{&h+VU+`9*#0-|Fr_>x0%9b8Eg;kVH7y#qtc&@~)u9jI7gZ4dPN zWI|&uY;WbDqwsP5!n+>e2B2M^kkI9eN)bVnNhBI8&xJCE&yYp3K8l#$as;$}qUh(d zF01o@noeVsy+I)c7Q>;2Q_ssrorN4d(CsJUh^AL2lDAk|ioMWTExs8=;DZp3aApy% zI$lRjwgDxINH-A`#7zOts(Lu+yN-H$5{)ZqedrJoJa}MHW)?}e-!5EL4rg_^2%(zY z>p+t@-AOgjMyrQ%r-FGSG8FN9k+IP+a;?W2Sh^XQ%BQ8KLSD0JmB{>kC z5&xik?0ALI<|@6F=fn)DgL8nscT8|{C@~CBWU|6_k1r{^3)@XkAHg6=Cs7%A zTHEssCWaagM=b9iV zBld}}Mi4)c+_^)Xg~S7rP-D)m3SWSGgwP+vdN&|K7|ikO#k~<|5X5;2BF-Q@&e{ab zng=&J$V($|nSzDIl195M-QIUd5dHOY{S5!_HdOr!JOV!kz#pJ#0=EXq13rNnCzaKj zrQ-^QcqR=s0%%1dx__jU^>j!j7jH!*l4Gg5B#ahViYBIn+5M;1J*2XRgZIP$0b6wX z4*eIGU?gz;gQx*Is1qQlGYV`tI)4>nGklGV;Kgrl7(AdUgJ<#mT7WpXDf=}WVt*6+ ztFvFKkUth+sTwye1gC=#MR#j@7kt#IgMgBWRBi&p;M}Nz)c251f}(;B(351sgMV6w z4+n$myn4ofp1K@0R;Z^YJ74V0W(vA%!UNf!!NiJgAe1-Z6)E8aq=1+|kbsC24xYdo z`dvbRCf1wg5w2NcMcg1HiyXiRV(s|c9)$*Z+$2ONLO?{J*hBW=vKRr|P=b*Lfu9OB z2tw2$pcDBFH&W=hoqQkE3(_+c2+^QK2~5usHw55}r&x>%*Ly*MF$E;Vf~T7BMd3ME zI@ZwfF#zndKYPb#vK5dV{TR`Q)m0LX-CXXzux^|H1UR6BT6}nU90YrgA2ggOhRs2= z1-*E_KIlBabmI%~@ha-Ha1f$04d)Kv%ncxy#hVj|F4l8XKzJ`2u{GkP7g>mqP60W{ z+pYN82nL7O0z-T|8s9v`N)D#K2qihVS;e*mvQ21(2(y4UsXY{~$ie$lq(9WHjc|xa zsMHitU_n9p$;H*%S*W#=8Z{xGz?%A>T3oi`A>v%v-0J)n#YF@FOAi%5?Rp*NLF?0N zC+;){t(vaxe$vYThui`BJ}j5_$)p99=b4#GDO&hlIQQlHA4)UaQhzW z{A-O){s{BF*Nq9jg_5NIHN*G~~ZVKm(E;QLh`rLck_+&7eQ* zU}X^nQ$OWrc@|Tqwu?hE>H|AuD<9?=*o9Ar71xl!f&!4_d&KDs8d9_^j9I-)&(B#n zVu2)*r=$d}7PTEcNGFt*OGIGB70U0JZWFzS=t^CWKgj4ICD&PS7YNSDd)WoLNRmnu zVT}0`anb_aI3{%Hz?rC|1HdxAhlxR#yxE3D0W3cY1N>(Rh}FUy;RJ+{Jv^DcaBm`= zIDsMVhW$U@-aM|!Yik?U+Sb$B8iz+|6`|BBtwV7}1VSyg)R9^XC=PK(MMOmk2qE=Y zMT-JT9T0>%q9CFoPz4he6;L25hCzfVB9SpNB#`-AYw!CG(PMkw_kDi9?;i=dhrRb& z*SglV*50(+ljE91bJ~KX2Wile&?1tJfeu4tFrC-Y$Q(WBO-^q!jciQ1P#PeVyTH$! z%dmoTsPG0_o6)QGPtPim5L+Ef&HW#MrPIP;T@wcuMQ8IBmdhYj=~ zHDe?&4Jkiq(MssiQcRA`(KoVbk_J{ziW8hpB8Q%h=%Hl>sY?+c>;p{44*n7)#(2%R zl|))k6gz~AD1Wil1pdKE*G1#}7&L<)b@XpggwYJw0D#~i-!A~2WTs<7rBvo!U^^+E zyD>%r=lrwYno`tCR%vhME9l2sC|yw|zwO`#?65eA#0G3D!U56$I6jY9DbmA!KHtIlLQs&blxELg zi?^1Eh(HaFmG6hYT7aGbcDf518DacleWq}x9^0iTU{NlUMze{m2JfHVlu@_L0B5L; zYX5a%P0p3VxfE>f(sF?c|MgLK3LtqpJym(VN!GBJj8&2oCV3)ssTv~6%s_12QUymh zSV;}Os?{c3O3RNK{KLT-xA*zm`Yssxj=b<&!^4TS@B0~U|7P|<_T4cvzqzRR<ZQIPkBI7;m>nR#)ui9c!N9 z=xR7}G*M7d@t9m&GGFdPN2tYz*A->yE--`VFN0T@xw8|9&IydySFaPfjqjZ^SE+DV z;k4%`ox8t>aSUb7my$TAMWfsH;GMVI+P`8afA}}^J8kWCNd=Z>=VQF++#cG_kbb56 zvpxEu%?#03x?}AD`#ftuh3V?TWp(qC3Tnp0C%Ss9YK93k~O^!gLd z_MhRu42jEIS5$MMDM($ad885;0tSeSMe+;HVK>}j?Cm9wYePRdd1QPCOo_WVJtNb6 zlD(^;I!OITJ^UA8n1{(og)y3C2|q2YI3eF}1_u8*hAjVlmwAnD;l;e`Qtq)jaL_%HxGR=u`mG65oC3o(IlxEq2akU=NOAz%j{Z%aIShzUC&bqe}v9e z&#vQ~5`|ypiV+)Ny0hcjvc4e5@x#%I-D0j-D!)oE7>{rvD>Pb9`*l8g$Ie$YQ|Nls z&>@R?JA7igMqcL=o%5Nb?FpDPe_2$05Sc7*k&lmfG7QFxViCt!aVz|daYb8uYI%@4 zlmCL)JH;pTPz_qJH7UJN$K1<6phUSI7elaJ7(;Cj z{~;mt?L-;#xJ!9yoNDoYT(Td$`iBF|eFY19#{%%d=QE^V@)O&)KXjcT`cikgJ?x?M zJI_)j4E-ANjG?nWI9{*d`s<}Jzk$~R=6CF*WGd$T*Y~q{dKYxzNGU5x?m;uWM*ZKx8tRVgh zTDFQ9Ju$hq0IekY>v5cyX4&Ms!jeg%nXNSHKl;g0Tuvd%xbH6A15O$++~ z+^Q0bIHAdZSpur%vFDl9J@MrkvGTt$;zS6&j8wS~dK<$HO~-~N>ptQFyEZxXsh6Hl z*bhvFaZy-QENb#8(rRG6;$96*1k*?cf{2%O@kjb>LW`2eKp?eWOW-fkS^Qn2zzJH$ z0*C|_F&_eo#7^y#^0N?kpW$;j%dKHPcI$zXk<=vidrX7=O;!b`^v+JTV}g(Maa>pE z{BzJ!)EF^KiH+duJY%;vP(L~htpZRxb%bGgqp!LiuA=kEz7X$4$)5h?Eousi--I-S z&wEx-&P`zwLWweW$7sL9wbG7{fY0AMTQbSOB~0R-#hV|)gjh_bbeI%CGcI6+dMn1( zL}mm^TZ<0_(QJi`WHbhA)O}s34i4K#X)fRs(adj?j6vPCJl{Wr^VC!8tWWX&2%V>P zu9JVBBEJBH^559HbP}@Q8)F|aNs=rM*fG6KGBDWk*%r+ah+i6|TizP249gh}*wi&^ z{O*T|UdUg@Y!IGY7?gn_MzO}w00XtR&5Wj(Vn`AuQ-fc?bFv-Cz=%=L3f;}SPfnRU z|CSV6A`4<*%;sj+PcdE6pftUZV_(R_rf-iN8Pad@E z)+QIZIZ)j&!msBkF!S4V0Yw^PP~!|u%*M(EK-y=(OhA5ijq}!d2j7875K#h3hBYYB z6?TF}W#ec{1sA_5=ibQ2Y`URfFbV#uWkdcjCS=VHoY~2{PatDbqk*RKm;wHH|7>XT z)#SI}p@KrwCX$$D&v`$ED`#JpAJ_G1N?aqFrWyU%@`3)mXQb2e zOIO{(5>k&3<7VVen;G(J%$nU&{%&ddt)6iyo{^TjR9;`CEcExBp*=q=v~(Q*vmv}wyHdEVtul>e`M6@tsTa=-852jQz}eG( zbW4^^OTZT%h6gWQ3oHbYQo9i?Q~i#wYI&|Xy=Hf$Xad}EV@q<0nc?L~ugaw7^9DNo zHRtjyOZ)Zk?xpq*S{nm+)jj0yDY=|aXD{I&J=ss>9mD;^6>j;%noJbeS%>S~aO9Re z?m*|Cjt|D^nrDV|OQ3V8?LS(qZyxy)j2L2g?knaNwWroOCwJZ-!M*IM#eaY2q@3oY zR14PmsO4?VJ&R>?_5PYUp1W-xKiK&#f7E;P z`MPq;pDcsB*DJr}-*2sz3vZd#udw?SU(N9h?QjZl)JLi3_x&-GdF0#&eRvmK&P2}c zUOiLms3^SqPK3?+UVT!Q@L#!wteMDPliqoZeo!=mK@&V<^_uIt?uv_(|J-2nsORXG zmXF;p;~0D#L#gJbTL^yOH}*MWm{Hz9Ce_%t5AU>1F#kTTZsG7Bl~#(2bDmyt(NuaJ z`?d2DuJWtTgrh>i&I`1fYr+K=zp0#|RR8&+9DoaN{1YvFk@rnV46c4Tu6{-Sb-ze1 zct-hAUOL(#se8*Xyb4FQz&2O(I~+%u_ZeL*pLXyzTz%M`cL0RL=}K#&z#=PKZ;V+~h>e2~NAX`?K%faiCpGx+bV0@n@h6(4-rlG};E%NV%J1}3v&)3(lW zI-Ob7f`F-&GlZ`$UHc|vw=}h|n)tBt5Udd#fioC;X5j~lsrTT*iUZrIZC4Z93SCp2 zG+?+>Q*|D9&6XS-1TQ9dvJC(D)**mS#fI&dIA=Mx-%(_VxAu5I+_DiLRo%R|zX#Y) zu&5%@=_GgHkVk;*`Ti7_K0T}NrGbTSZkLok2iza+=^qS!1W#E8b8cb0TvUVO^kBNC zp8g}jVU}dA^S6CQfZ6Nco&5x^{It)1d)_Oy&$MJ)rw|i^8Et5o=N&F6+5KZ?KZ^6l z@A~i{{4Nff8Uo(W^l|wTLb5FK)squ*EeH35aRZLb41tLsd>3HRsPGBzcbEI5!FkwJ z-K&cur(Q;^l?>Zu6AnJy`vF9jbe{Cwgku|qff@0;+wfic4iyAS{AA(l_B*)-7z_;w|1+}_8>`|`gj;q&xvv&NJMlX7=;YSq?DKT{?4lhezYWk+G{%j z?{^;|_=G-Y&PX?(|K_PZw*o@u&^7L)aR3hZc@Td}<%+_spVqryD(wmJgihOjuny?D z7r?9?Xdh9xhM*3EXibn3z9Xgs2zunyQ_Gc7x)Vj0N}1uehMzso1!SP_c+Wg5WN@cc0$ z+1+O z8eAXiKj%@(Z$SNUsqrYiRF9V+*p$05*c>^y=)qs!I)piN*#A%-viMz6nxF$*{C=Ed zCyZu)xQ2u{bO#V3z;)y)T>D)J@Uof2A(0Ze1Am=!5hFF2Kk5t`7Vr%g_=(=)H~?-z ztS3>1O-|LINP#Kz8cA`Rww$DXj-t;3Eo9>Jbir9!AjMe%u4l!CTXV}LX_VZWI&6J-!0L;>uMql%g+RZ1O*zK#i;R&|~61z2K zP~&XvMeR0mkcVXZ>JaoV z%n0a}JOCYH`old0)1&mwGqvaOrK4`rF}~g-DaGtivW#ShT>zUyHre+{4qJ{a0tEZX z2|}=#&+K+~w$?Y44o9VWj5}%NIZ&T~xjmmdg?!6Dmrvh$`Xdt5_r|&N!kv=uL2k8i zzI=omLUag*DUDS1v+eHThIuI3a_jdX#iHW9CHKSx@r^UZ+kIlUrC_&-gN_p-nLxcZ z%b8{;{vgawAV9$8kLdWGA5*)n-ivmt93*xeq_{y+8V?=<&ySo0?|`&O0Jr)Cl|ulT zbQ&}jJ85paxtAIZlFq>mU`Wgvi|@sPc?$<8r@DgV`0`3#vg?^h#o%i4^~dHk3-pTs z3Ji;TzhX$^vojU<%$7{r1mpv{VHMs0p}UDHmNUXUMCx5StW6jRWQlpQ?_QquVY<%Hw9eodKc1k*HG>m2oa)b%fO)CtMC)`|y*I!|)f?RKp{aj+Bg z{z>6?4J}@eJiNM1M^}U1|H(2PONoR>E~xj5r9bg>2^C@C36BD#D{fn?BZ3619<4q@ z3I=>Zal%}lPIPmnT6=RQ53dXh?O3J8VWj*R{XuoT_-I(Myj_vLfecG87^kqXK3VkM zpgY!z%8r*Xj0*VfA>eU?bmFCm!!s2}_k zWR-f@PtB?GOO*><61W}em-pJI!zK2*!I{j@>JMBJ56dBD?Y2%hf}qn=yUf<5GQ8zf z>~`VrHkIa&*0s#E?U_zKj?fEZPw3Ois}F66&{+831^z@nTAqWzn{Jz1-B4`#Q^FQM z^M{2~M+4}iH}qE17i?9Yie2#l(-+)k1@DEYT(8=_oT-J(Wb??n(mXshx9eHp;gc`} znt6LEnzHO7mf7Yu`5LO7Ex*@AyIi$915){qgTRC2Es)gtwz*>)YyZ+YhFYhP=YUFJ z8q|P9Gr$|j$f9BoND?s3`=}ye==NbC)~c=zFbGQnet^}&<2dt6ovIy&JWq?Sf$t!B z2Ke6oLWqx%)d9 zSE$x-f$;vNVAEWF?IWJQ1%jon_D__{ z(HkOemzo)(z--T>HuPSHL5(ko$5IXgS1OEs{>d+XPxM@Vd4sdd_tl{Ai6+G7!P?47 z^V|!u&y^iuwU>E6Kotz>gBk%{v7$wB+v!OIjN%NY2Qo3Wo(PFHZ0d*CdQR1Yh7?%W=ohQF#s~!?Kzblhtu4f02eBV&B4^iKg;DBZY^AQVpQ8BF95V4}HaFa#@oOC~Su2Try z1In=js7?54284+sCsSTKxoYvi=sGgNzvLQ~(>xM1J^7$eC9 zs?xs9`2k)Dm}vxm!sN&hUC{+OVC(q$LPRj;F~-)W@OfBx!+Sh)iPl@_qQDOKk3u^E zokEur>{E<~2zGh=V`ryp=kOF>d(n8|P(|S7Y!zh!^)pSoqB95aPS*IQ=^%d5xP*l; z$WcGy^H|G`L~?O;nl{12vh*V`jJ&i~QkstH6@m-FEs>MB?SxsR3>0Aq!^ewjeALW! z2K*i(4c-B=2Bu=aumj~ziYiP$fH)GKP*A-Qos64Liws%FT`Sd>N&#j_QuorN z@D*N zH;G_C;ED&2+usP_c!JLG*RhuyIMs4QA>Lt+&?qC(KqKAYft}y|zhu!p9#&6?_vw;^VZ901XDv5&UJeTstLXl*wcg zUATEsDBV4(<%OJlk4g|@tXeL5B@7-poWhxjY2t_WRV{==MhOYK^}K4cE&+K@(vB2( zO@;r`K-RIDttkL*fQ&h3CA6q^+ZZg7=~6F1G8~xV>h#Bi`M`?AN4KB{1k`5c>4+G` z7N0RbNrdz?vIb-^V#EX}9huC*20r^UNa=LG34|tt9fpE2XP^OY8UD&JrG6%C+04|^ z5zLoY{hpufis4K%fO1tTqZfc(Ag?lM`FDJuF=#vzkbXc=gqs+@n$36YPpRP95Smw0 zPLrIf#+a*QyV(1Vore2Eh#7b=rX7(ZUk=!Sdk z_NUmP`vF{Fgi2o7q05ta(AS0*`iLSr%D&})HjgfD^d zKwfYghsnIfm^21-A#ijkW?lYdytGLpbX-Jp#b%(iBE>qt}eN5*dvOBV={# zBBADJZDc4-9J*^wKkzZYACEDjV#9(J}?PXD5^? z5qS-Xb^^1eur*HKX@))aeW2f^43L!6CCI-H7x1*1qFgL&wOOPD*e!;_1f_byqs%pq z`o>=JuTTvq2uh*EAbOK1a_!BmHIAWqYscy4c=8%V`=I~{ClAp=#9A@;UbW$sqH}z` zJ>u##Bjax~KkK5^15kwbZySOQI2@G_3UN*_i&EYJqm2Y0y7 zIxRh$L?k8S0jkFk$N*t#Bj_4Hd}U7((>@-dJ|dgqh$erIsOq9s?6*-_P=w?nZ?Rts z=>c*l!E6hdzq7$oEq&8Q3HJ}f)G|O681=B zBnrcO)OzKF{6rd<+&n-(t66*@sQ+{ir%~QfX)Z&_pXp`E|717n^>I)e5MfTy=oG?N zJJ&mRd`HqqN);_th$w`+K@>qd@)^=Y9Fh~05F9kR$CUl&!?w%DR8FOtF+PcrP*D~_ z>5sy?HwtfolB5#c=ppPz=oPy)Oy0zHouAaaFrlED)i5vdQ09~L>m(Ax$9nmvi8T9ACWT2fD)Nk#6Km;Tc<}%da1 z8h3uMyLPD)+b|^U$GsH(00jf-?=rmxT9M2rxCJ%bAM@>06YH=_8MuUm6H+SH@dE{< z(|&oBo}lXe``|H*tFB6wEo9|bMIqraS&r!d(4+^Tt>z4y4RkW1rK`e3p^t5jDk#ZW zR8+_TcJ&gVlt&bNgw^olDA;U-s3e%Can2MZ1fE-!Y}9su);Kow1WC%cu?7qW*jklp z468_D&l}!pon04+*5heu8Ab92lW`q1OhHmj^c?nZXi(dq!lm7ZgvHtxcNsv+nM{z? zEg*L==8khtgm&P|jZtCjv4GgIzG}mi9dGF{5Gh6Gg<2X2oOIPtHC!Z}sodSo;YVnB z20OLLWu(A`U2)soWvNv zk{PrG4|u`MzI_d{My-P;j|sKDbnB3a(lj(5JQA??YYd1yR^UOa6%j#SG(V)N+!8Vg zQXBT+3IE69zto7u(f~i2$B-2LwK9T~n}Xa&$z?)E-1R4gKfWhhvO~x6JVbh9*0iJZQ&14O(e7Gh~e-i|npR`Did*GubQ( z0Mi*BJ(lZ1PQJ<^=Kt^GlS&giov27)?$QPnRN$7kFB^-s1=^#R*Clq;3Fqtcd(c(A z#cWd`dHp|GhW|J22o6{Of~tJzfKpx01vX=cFf7JE6r@# zf$taA2SaMwU=SE&CPpQBy4N^lGKlq-NE1EAI22Eet(YdHE*$+3q8b1hT@>8*W z4y}2*m+~K1iA)y5YiVUbHeN`HFj{d9AO$tl>$^2LH7H<80S%Wrr}H};)oCzO8<7Ss zs4B5Ml5@}cK2XbHyJrdD{C7uK^f*53=g@ zSN{p5_8lJ&4;&}Qe&$-@DT0YuxBS0O5Bj)0>VvP^lmH=NfBy}uKutXz_75OV4tkhg zVwj6I(1ZCHk^2&8`uG1 z7!y?{tz;2vgD`$gP}0SWh@;({6d+kh1CQXGit|W3Bz0^6)U|NT>;wwuhPo^Bp`84u z*Z&P=w`~0cq&}u0Z@yzARXQc#g5zK_)K_iRU)x< zg({pb!z%9|xa+?|@t>?3NVLt{^+OlfvQ@|465&<9Plx?nmU6LdVh?qK^@wsoZt;iB z334=noLRwE56}U_eT*u43y@)x2=;uzXXkJO7dnk_pw?)6hR!R^?zNMUWKYv}8Bd>B zat0~pu>PN!8N%f4%$#+PFpR=Rn8;ZXr2L!ZgW!9E)Jd?JtR32I$*JCN+ytv9%{F4Z z#bCErcxJBWXkX|%t%03a*p9#ot&71-De8SwhM(W{-vLv0@E~J)vm8I<&;9p@+y6QC-)!YWq9@ z9)o=-?65K=5R~4e5{1nKqU|r0hH>{W&(d~qPkxX>;oa6qi#RZ9*A2{3X43Rx&H#Xj zQCnjTDblcADcD6^ZQLuvjYizbqO;dFcNlj=@fZxVCxERYSOv333POOS)dnMd}0E9v_`_Exe4dTfPX2*>=SfGY30}7*lqG)$V4eaMapA|rE(#Yv2 z5S1c{fE8$471zgZ_$<&u2M>-wf|@jS!)tRSLOPpl*f32!rYA}F<0oQ=sOi;*?aSzi z3D{|;t$)}P#m!)H-I4AvrxtQ}a5j=QDX>p6&=8u6DA`ZS-Ne*v71O|o1@l>Qu|)}I z9RwW!1=t5iUsFdL_vGNXp|r4N5Vnmo&~8GeWkAYb(migG!`f)$mh6?hdQ7c|n;y`J zCS9y>=>Cy8P}^4r9AS2AVKKVL%!ctH(j=m`WgHGRh28XQB3fA^YuLU%Vy|qZVPmSzC7XIVKd<(OSyIhY^=Gj|gVfbM z@c#VbyMT&oek0$D3-`sF{1kFypN}o5ov-D$S}Q9$ZiV)2bp2KL=><_Y zrN?#s`(@IA^$FU@Ws`ENv$R|I`O(^toY=!PvcXlizHpMh8a`arG`K2v475iV3++d{ z!O^FF^AyF3VW;b5OL)tZLaz!xZmSnpz2bh(d0sl`;n9ONi48MnwH{5Z$Db0hZT9Z7 z>ZT>q6^DT=mgr!F;u`#NIy`9{fw}y((_&uh7t$bn>#Yf@qt>d3wXKN_6=M>#o2MrT zS{y&;I7h=h%lFG2@&jf`TuTofuCaG*Emrs(zIVv)NFKa6GB59bT7qw2|7chZ46WK} zJ2qc@P57~&YJ6$OuEt(&xT@2pv%3PXiPx1V9ILJjh=yfJujX3J-ha48lb$48u|MIw z&Di{a_@spMzB%XAIqM^Og74g;Eic*i8>xya+&6!AC|LYljq{}3omWEaj!#us#Xj%L zSWp_|hJHJj#3?u5Evb>G*;&9FZfDvE^Kzm>HuB+l2b|q4Ts8ME)>k2i2np-ka`VVM;r%rEoS70_)dbZI#neeItzVF3tapgjvi57WiXD1!Ke5VP z@^1%srQiC)_bji%A_CvV*CNg*s_?!fxCSN{iE9HtO0yd%jn|o2j|5KsJOv3RieqZn z(siwL6=&7>vK7x&HI`R}dBQWNzySS{51LN8+KH8_A zSSd*zRd{jP*U-%YTE+F_6R+b>s^L${B{VikHI(B_=e*yVu>>M1l?CVvGH^}$5iK(i*BmFw!yWNq0$&sSdC%jUwFm~z@QZzP6 z47&m6t;kuVzfZlFPopC_A0r#D4ZXOT|GM!^c>&O60w2hV5Ee_|VHyH3AKcH+(kx z^wT3+N?ey5nM7SBTI?-hVaE_zSMG3wkCO^sa2694WWBnCTBbCKdH|Cd zV7DxAiS8J>&t>WBUmlmhVVg2;!R0yc=lsl4Ocpz2bbt)2APB5}fG9wn4uwy5mR?h)KBpB)f92F~Q z&cU@zHp4sk@~()cAU$tQ~+o*MCN-p*=EmOumSXtMqnaKunu%(jbyP zXNStB>$@D(-uNGCgk|dv8tAQ_vqLEr(x1klWh077iPox66v@Wfy|H*2LiAw*4R++N zlzl@{Irfl^Y(i%^=xSD>n9u=nT(lEp;%OG&(-aYoWJ_6Z7}_$~#^}6Yds4IMNqlnRmq+oNFH>noiV=)yR;eLyqY7Jo#Z29PWoUFrSwpW` zypW>kkX*E=(Rp4>t}=RuO06b|6KVm`J=exe`EFhBvV?A$@k<5W8&iMPeSASAKYTBt zNik$!Sc`Wh*LA~ms28kNK{NaqVc$9=S_|;>Ixh#ko*&S0il}vv`NOE%#p;CU`2eDo zh(f0|1qjvQsya1Ls#vLx$jkYWlq&g9E-a{G= z7bu26WE>tYvL6SvhUxX`dXUj{a&BxjLSBzW6DeD4Vf;nX?vVW$nN4aUGXJj03F+kO zNdW`XE-J3Uw;wnAqpwnc1NM zo<>w*6*pV|1S?BlhXgGNo41XyUz#O+&32w7<fUw^8xlX%NaF(>9SRmDpd^MN6#_kq#p zDNx|=CrbVz0Lyr_s{&wzvKQ=z2Fu)M9pnb^6xUEZLG>o2j0->88v!b$Mwt9LWSnKG zP^D1Yn_AC4bNNqvqv;Y)agoturnf!-i;c(C{dM+)Ty zG11!AteM8?GZv(J$Q)|a<~%?dZ|PPgRU8vk&p!5L$4}ss8UbYkE!C136KAX8G)-RZ zWPsdQ7Thx}sg`!vSCM{6M$b!j7(latJ>NUcmRYky19K|8;E#HsMt@6QVp=lMP$fW_ z$VxmTNGbIhgI7WdTWYm<(gi?(VU0^{Lxi*!u^IiB#BX{#$QGhyo3Ji{@*QBuR$EMs zF`1NEwEpNyo2+PfBRatCUGd4j`=4~ON$e8HFmfXpr5^T?LQ-krx+J+?lTN~y=6rcX zkm5z$`=_CxB!-m)9AecLwGftfSbu+6YIb;iB(N%@_R1{+vFL>V!g;7+X8?c?Nu8!y zViNk1B-GUsfz16QG^06&Erlqx9#d;*)<=*MEdwP9W;5PW#y^bWB%z)44;=y%>Pm-Q{_Enr45zMd&Dh*7_0l07TQSdq_F zk){%LJ3h-M4Jz`9$%ARp0H}GrDHEjl|NB|e^OzLxCSagOt3sM&(MBXEU@xE(n<^c1pot5&1<6WFwpGpEq@B_s}+^c_50SeW`TyHCH2}?Xe=`2k*Vbf z6PZ{CMtFX7)!4H_`;@KsdXQwGJpvBIc1@}s=F_c84bLyzZ{+aSO1J646-TrY#%`Ym zdJhq%z7w_+0A#!KLa0t;gA)FG9Eh6Nvnk?>s_cKnycbrsBot|}O4Kq?i4)JgJf=q-t^%R0&A#YYl_JfBX1zXJU@6 z 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 E15d)JX#fBK literal 0 HcmV?d00001 diff --git a/frontend/public/templates/PART_EXCEL_IMPORT_TEMPLATE.xlsx b/frontend/public/templates/PART_EXCEL_IMPORT_TEMPLATE.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ebf7d495bc1b4c3b0e6ebda9f29a0ceaecfb084c GIT binary patch literal 127127 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 z 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 literal 0 HcmV?d00001