diff --git a/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp b/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp
index 5124e55..95389ee 100644
--- a/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp
+++ b/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp
@@ -321,7 +321,16 @@ function fn_initGrid() {
'data-LEVEL="' + (rowData.LEVEL || 1) + '">';
}
});
-
+
+ // 행 식별 키 (수정 금지). 그리드에는 숨기고 다운로드에만 포함.
+ columns.push({
+ title: 'KEY(수정금지)',
+ field: 'OBJID',
+ visible: false,
+ download: true,
+ headerSort: false
+ });
+
// 수준 컬럼 그룹
var levelColumns = [];
for(var i = 1; i <= maxLevel; i++) {
@@ -703,6 +712,7 @@ function fn_initGrid() {
title: '소재품번',
field: 'RAW_MATERIAL_NO',
editor: false,
+ downloadHighlighted: true, // 엑셀 다운로드 시 노란색 강조 (자동 채움 X, 사용자 입력 필요)
formatter: function(cell) {
var data = cell.getRow().getData();
if(data.SUPPLY_TYPE === '자급') return '-';
@@ -794,6 +804,14 @@ function fn_initGrid() {
}
}
return value;
+ },
+ // 다운로드 시 코드값(id) → 코드명(text)
+ accessorDownload: function(value) {
+ if(!value) return '';
+ for(var i = 0; i < supplyVendorList.length; i++) {
+ if(supplyVendorList[i].id == value) return supplyVendorList[i].text;
+ }
+ return value;
}
},
{
@@ -860,6 +878,14 @@ function fn_initGrid() {
}
}
return value;
+ },
+ // 다운로드 시 코드값(id) → 코드명(text)
+ accessorDownload: function(value) {
+ if(!value) return '';
+ for(var i = 0; i < currencyList.length; i++) {
+ if(currencyList[i].id == value) return currencyList[i].text;
+ }
+ return value;
}
},
// 숨김 컬럼: 품의서 작성일 (저장 시 기존 값 유지)
@@ -1455,32 +1481,330 @@ function getMbomTreeData() {
return mbomData;
}
-// 엑셀 다운로드 (CSV 형식)
+// 컬럼 정의로부터 다운로드/업로드에 사용할 헤더 텍스트 결정 (titleDownload 우선)
+function fn_getHeaderText(c) {
+ if(c.titleDownload) return c.titleDownload;
+ if(c.title) return $('
').html(c.title).text().trim();
+ return c.field || '';
+}
+
+// 엑셀 다운로드 (XLSX, ExcelJS). hidden 컬럼인 OBJID도 download:true면 포함.
function fn_excel() {
if(!_tabulGrid) {
Swal.fire('데이터가 없습니다.');
return;
}
-
- // 파일명 생성 (현재 날짜 포함)
+
+ // 1) 내보낼 컬럼 수집
+ var defs = _tabulGrid.getColumnDefinitions();
+ var exportCols = [];
+ function collectCols(arr) {
+ for(var i = 0; i < arr.length; i++) {
+ var c = arr[i];
+ if(c.columns) { collectCols(c.columns); continue; }
+ if(!c.field) continue;
+ if(c.download === false) continue;
+ if(c.download !== true && c.visible === false) continue;
+ exportCols.push({
+ field: c.field,
+ header: fn_getHeaderText(c),
+ accessorDownload: c.accessorDownload || null,
+ highlighted: c.downloadHighlighted === true || (typeof c.editor === 'function' || c.editor === true) || (c.title && /background-color/.test(String(c.title)))
+ });
+ }
+ }
+ collectCols(defs);
+
+ // 2) Workbook
+ var workbook = new ExcelJS.Workbook();
+ var worksheet = workbook.addWorksheet('M-BOM');
+
+ // 3) 헤더
+ var headerRow = worksheet.addRow(exportCols.map(function(c) { return c.header; }));
+ headerRow.height = 22;
+ headerRow.eachCell(function(cell, colNumber) {
+ var col = exportCols[colNumber - 1];
+ cell.font = { bold: true };
+ cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
+ cell.border = {
+ top: { style: 'thin' }, left: { style: 'thin' },
+ bottom: { style: 'thin' }, right: { style: 'thin' }
+ };
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: col.highlighted ? 'FFFFFF00' : 'FFD9E1F2' }
+ };
+ });
+
+ // 4) 데이터
+ var data = _tabulGrid.getData() || [];
+ for(var r = 0; r < data.length; r++) {
+ var row = data[r];
+ var rowVals = exportCols.map(function(c) {
+ var raw = row[c.field];
+ if(c.accessorDownload) {
+ try { raw = c.accessorDownload(raw, row, 'xlsx', {}, null); } catch(e) {}
+ }
+ return raw === null || raw === undefined ? '' : raw;
+ });
+ var dataRow = worksheet.addRow(rowVals);
+ dataRow.eachCell({ includeEmpty: true }, function(cell) {
+ cell.border = {
+ top: { style: 'thin' }, left: { style: 'thin' },
+ bottom: { style: 'thin' }, right: { style: 'thin' }
+ };
+ if(typeof cell.value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(cell.value)) {
+ cell.numFmt = '@';
+ }
+ });
+ }
+
+ // 5) 컬럼 폭
+ exportCols.forEach(function(c, i) {
+ var maxLen = (c.header || '').length;
+ for(var r = 0; r < data.length; r++) {
+ var v = data[r][c.field];
+ if(v != null) {
+ var s = String(v);
+ if(s.length > maxLen) maxLen = s.length;
+ }
+ }
+ worksheet.getColumn(i + 1).width = Math.min(Math.max(maxLen + 2, 10), 40);
+ });
+
+ // 6) 다운로드
var today = new Date();
- var dateStr = today.getFullYear() + '_' +
- String(today.getMonth() + 1).padStart(2, '0') + '_' +
+ var dateStr = today.getFullYear() + '_' +
+ String(today.getMonth() + 1).padStart(2, '0') + '_' +
String(today.getDate()).padStart(2, '0') + '_' +
String(today.getHours()).padStart(2, '0') + '_' +
String(today.getMinutes()).padStart(2, '0');
- var fileName = 'M-BOM_' + dateStr + '.csv';
-
- // Tabulator 내장 다운로드 기능 사용 (CSV)
- _tabulGrid.download("csv", fileName, {
- delimiter: ",",
- bom: true // 한글 깨짐 방지 (UTF-8 BOM)
+ var fileName = 'M-BOM_' + dateStr + '.xlsx';
+
+ workbook.xlsx.writeBuffer().then(function(buffer) {
+ var blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(function() {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 100);
+ }).catch(function(err) {
+ console.error('Excel 다운로드 실패:', err);
+ Swal.fire('오류', 'Excel 다운로드 중 오류가 발생했습니다.', 'error');
});
}
+
+// columns 정의에서 헤더(title) → field 매핑 자동 생성 (titleDownload 우선)
+function fn_buildHeaderFieldMap() {
+ var map = {};
+ var cols = _tabulGrid.getColumnDefinitions();
+ function walk(arr) {
+ for(var i = 0; i < arr.length; i++) {
+ var c = arr[i];
+ if(c.columns) walk(c.columns);
+ if(c.field) {
+ var title = fn_getHeaderText(c);
+ if(title) map[title] = c.field;
+ }
+ }
+ }
+ walk(cols);
+ return map;
+}
+
+// Select2 코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑
+function fn_reverseSelectValue(field, value) {
+ if(value === null || value === undefined || value === '') return value;
+ var list = null;
+ if(field === 'PROCESSING_VENDOR') list = supplyVendorList;
+ else if(field === 'CURRENCY') list = currencyList;
+ if(!list || list.length === 0) return value;
+ for(var i = 0; i < list.length; i++) {
+ if(list[i].text === value || String(list[i].id) === String(value)) return list[i].id;
+ }
+ return value;
+}
+
+// 엑셀 업로드: 파싱 → 역매핑 → OBJID 무결성 검증 → PART_NO 우선 매칭(중복 시 OBJID) → 그리드 주입
+// 매칭 규칙:
+// - PART_NO 그룹에 후보 1개 : 그대로 매칭 (OBJID 무관)
+// - PART_NO 그룹에 후보 N개 : OBJID로 정밀 매칭, 없으면 첫 미사용 후보로 fallback
+// - PART_NO 매칭 실패 : 신규 행
+// 안전장치:
+// - OBJID 값이 있는데 기존 그리드 OBJID 중에 없으면 업로드 중단 (수정금지 위반 방지)
+function fn_excelUpload(file) {
+ if(!file) return;
+ var fd = new FormData();
+ fd.append('excelFile', file);
+ fd.append('headerRow', '0');
+
+ $.ajax({
+ url: '/common/parseExcelFile.do',
+ method: 'POST',
+ data: fd,
+ processData: false,
+ contentType: false,
+ dataType: 'json',
+ success: function(res) {
+ if(!res || res.resultFlag !== 'S') {
+ Swal.fire('오류', (res && res.message) || '엑셀 파싱에 실패했습니다.', 'error');
+ return;
+ }
+ var rows = res.rows || [];
+ if(rows.length === 0) {
+ Swal.fire('알림', '읽어들인 행이 없습니다.', 'warning');
+ return;
+ }
+ var headerFieldMap = fn_buildHeaderFieldMap();
+
+ // 기존 행: PART_NO별 그룹 + OBJID 화이트리스트
+ var existing = _tabulGrid.getData() || [];
+ var existingByPartNo = {};
+ var existingObjidSet = {};
+ for(var i = 0; i < existing.length; i++) {
+ var pno = String(existing[i].PART_NO || '');
+ if(!existingByPartNo[pno]) existingByPartNo[pno] = [];
+ existingByPartNo[pno].push(existing[i]);
+ var oid = String(existing[i].OBJID || '');
+ if(oid) existingObjidSet[oid] = true;
+ }
+
+ // 1) 헤더 → field 변환 + Select2 역매핑
+ var converted = [];
+ for(var i = 0; i < rows.length; i++) {
+ var src = rows[i];
+ var out = {};
+ for(var header in src) {
+ if(!src.hasOwnProperty(header)) continue;
+ var field = headerFieldMap[header];
+ if(!field) continue;
+ out[field] = fn_reverseSelectValue(field, src[header]);
+ }
+ converted.push(out);
+ }
+
+ // 2) OBJID 무결성 검증
+ var invalid = [];
+ for(var i = 0; i < converted.length; i++) {
+ var oid = String(converted[i].OBJID || '');
+ if(oid && !existingObjidSet[oid]) {
+ invalid.push({ row: i + 2, objid: oid });
+ }
+ }
+ if(invalid.length > 0) {
+ var msg = 'KEY(OBJID) 컬럼이 변경되어 업로드를 중단합니다.\n해당 셀은 수정 금지입니다.\n신규 행은 KEY 셀을 비워두세요.\n\n';
+ for(var i = 0; i < Math.min(invalid.length, 5); i++) {
+ msg += ' - ' + invalid[i].row + '행: ' + invalid[i].objid + '\n';
+ }
+ if(invalid.length > 5) msg += ' ... (총 ' + invalid.length + '건)';
+ Swal.fire('오류', msg, 'error');
+ return;
+ }
+
+ // 3) PART_NO 우선 매칭 (중복 시 OBJID disambiguate)
+ var usedObjids = {};
+ function pickMatch(uploadRow) {
+ var pno = String(uploadRow.PART_NO || '');
+ var oid = String(uploadRow.OBJID || '');
+ var candidates = existingByPartNo[pno] || [];
+ if(candidates.length === 0) return null;
+ if(candidates.length === 1) {
+ var only = candidates[0];
+ var k = String(only.OBJID || '');
+ if(usedObjids[k]) return null;
+ usedObjids[k] = true;
+ return only;
+ }
+ // 후보 N개: OBJID 우선
+ if(oid) {
+ for(var j = 0; j < candidates.length; j++) {
+ var c = candidates[j];
+ var ck = String(c.OBJID || '');
+ if(ck === oid && !usedObjids[ck]) {
+ usedObjids[ck] = true;
+ return c;
+ }
+ }
+ }
+ // fallback: 첫 미사용 후보
+ for(var j = 0; j < candidates.length; j++) {
+ var c = candidates[j];
+ var ck = String(c.OBJID || '');
+ if(!usedObjids[ck]) {
+ usedObjids[ck] = true;
+ return c;
+ }
+ }
+ return null;
+ }
+
+ var merged = [];
+ var unmatched = [];
+ for(var i = 0; i < converted.length; i++) {
+ var out = converted[i];
+ var base = pickMatch(out);
+ if(!base) {
+ unmatched.push({ row: i + 2, partNo: String(out.PART_NO || '') });
+ continue;
+ }
+ merged.push($.extend({}, base, out));
+ }
+ if(unmatched.length > 0) {
+ var msg = '기존에 없는 행이 포함되어 업로드를 중단합니다.\n신규 행은 추가할 수 없습니다.\n\n';
+ for(var i = 0; i < Math.min(unmatched.length, 5); i++) {
+ msg += ' - ' + unmatched[i].row + '행: PART_NO=' + (unmatched[i].partNo || '(빈값)') + '\n';
+ }
+ if(unmatched.length > 5) msg += ' ... (총 ' + unmatched.length + '건)';
+ Swal.fire('오류', msg, 'error');
+ return;
+ }
+
+ // 4) 누락 검증: 매칭되지 않은 기존 행이 있으면 차단 (행 삭제 금지)
+ var missing = [];
+ for(var i = 0; i < existing.length; i++) {
+ var oid = String(existing[i].OBJID || '');
+ if(oid && !usedObjids[oid]) {
+ missing.push({ partNo: String(existing[i].PART_NO || ''), objid: oid });
+ }
+ }
+ if(missing.length > 0) {
+ var msg = '엑셀에서 행이 삭제되어 업로드를 중단합니다.\n행 삭제는 허용되지 않습니다.\n\n';
+ for(var i = 0; i < Math.min(missing.length, 5); i++) {
+ msg += ' - PART_NO=' + (missing[i].partNo || '(빈값)') + '\n';
+ }
+ if(missing.length > 5) msg += ' ... (총 ' + missing.length + '건)';
+ Swal.fire('오류', msg, 'error');
+ return;
+ }
+
+ _tabulGrid.replaceData(merged).then(function() {
+ Swal.fire('완료', merged.length + '건이 그리드에 적용되었습니다. 저장 버튼을 눌러 반영하세요.', 'success');
+ });
+ },
+ error: function(xhr, status, error) {
+ Swal.fire('오류', '엑셀 업로드 실패: ' + (error || status), 'error');
+ }
+ });
+}
+
+// 파일 input change 바인딩 (한 번만)
+$(document).on('change', '#mbomExcelInput', function() {
+ var file = this.files[0];
+ fn_excelUpload(file);
+ this.value = ''; // 동일 파일 재선택 가능하도록 초기화
+});
-
+
+
+
diff --git a/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp b/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp
index c81ec23..d213fbc 100644
--- a/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp
+++ b/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp
@@ -105,7 +105,9 @@ body, html {