M-BOM, 구매리스트 팝업 엑셀 업로드 기능 추가 및 다운로드 양식 개선 #206
@@ -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 $('<div>').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 = ''; // 동일 파일 재선택 가능하도록 초기화
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<input type="file" id="mbomExcelInput" accept=".xlsx,.xls,.csv" style="display:none;">
|
||||
<input type="button" value="Excel Upload" class="plm_btns" id="btnExcelUpload" style="float:right; margin-right: 5px; margin-bottom: 5px;" onclick="document.getElementById('mbomExcelInput').click();">
|
||||
<input type="button" value="Excel Download" class="plm_btns" id="btnExcel" style="float:right; margin-right: 5px; margin-bottom: 5px;" onclick="fn_excel();">
|
||||
</div>
|
||||
<div id="mBomTableWrap"></div>
|
||||
|
||||
@@ -105,7 +105,9 @@ body, html {
|
||||
<div class="header">
|
||||
<h3 style="margin: 0 0 10px 0; float: left;">구매리스트</h3>
|
||||
<div style="float: right;">
|
||||
<input type="button" class="plm_btns excelIcon excelBtn" value="Excel Download" title="Excel Download">
|
||||
<input type="file" id="purchaseExcelInput" accept=".xlsx,.xls,.csv" style="display:none;">
|
||||
<input type="button" value="Excel Upload" class="plm_btns" onclick="document.getElementById('purchaseExcelInput').click();" style="margin-right: 5px;">
|
||||
<input type="button" class="plm_btns excelIcon" value="Excel Download" title="Excel Download" onclick="fn_excelDownload();">
|
||||
<input type="button" value="견적요청서 생성" class="plm_btns" onclick="fn_createQuotationRequest();" style="margin-right: 5px; background-color: #4CAF50; border-color: #4CAF50;">
|
||||
<input type="button" value="저장" class="plm_btns" onclick="fn_save();" style="margin-right: 5px;">
|
||||
<input type="button" value="닫기" class="plm_btns" onclick="window.close();" style="margin-right: 5px;">
|
||||
@@ -377,7 +379,8 @@ function fn_initGrid() {
|
||||
formatter: function(cell) {
|
||||
return '<input type="checkbox" class="rowCheck">';
|
||||
},
|
||||
headerSort: false
|
||||
headerSort: false,
|
||||
download: false
|
||||
},
|
||||
// 2. No
|
||||
{
|
||||
@@ -387,7 +390,16 @@ function fn_initGrid() {
|
||||
title: 'No',
|
||||
field: 'ROW_NUM',
|
||||
formatter: "rownum",
|
||||
frozen: true
|
||||
frozen: true,
|
||||
download: false
|
||||
},
|
||||
// 행 식별 키 (수정 금지). 그리드에는 숨기고 다운로드에만 포함.
|
||||
{
|
||||
title: 'KEY(수정금지)',
|
||||
field: 'OBJID',
|
||||
visible: false,
|
||||
download: true,
|
||||
headerSort: false
|
||||
},
|
||||
// 2-1. 구분 (M-BOM / 수동)
|
||||
// {
|
||||
@@ -630,6 +642,7 @@ function fn_initGrid() {
|
||||
hozAlign: 'left',
|
||||
width: 150,
|
||||
title: '<span style="background-color: #FFFF00; padding: 2px 5px;">공급업체</span>',
|
||||
titleDownload: '공급업체',
|
||||
field: 'VENDOR_PM',
|
||||
editor: function(cell, onRendered, success, cancel, editorParams) {
|
||||
return createSelect2Editor(processingVendorList)(cell, onRendered, success, cancel, editorParams);
|
||||
@@ -643,6 +656,13 @@ function fn_initGrid() {
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
accessorDownload: function(value) {
|
||||
if(!value) return '';
|
||||
for(var i = 0; i < processingVendorList.length; i++) {
|
||||
if(processingVendorList[i].id == value) return processingVendorList[i].text;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -652,6 +672,7 @@ function fn_initGrid() {
|
||||
hozAlign: 'center',
|
||||
width: 100,
|
||||
title: '<span style="background-color: #FFFF00; padding: 2px 5px;">환종</span>',
|
||||
titleDownload: '환종',
|
||||
field: 'CURRENCY',
|
||||
editor: function(cell, onRendered, success, cancel, editorParams) {
|
||||
return createSelect2Editor(currencyList)(cell, onRendered, success, cancel, editorParams);
|
||||
@@ -669,6 +690,13 @@ function fn_initGrid() {
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
// 31. 단가 (수정가능) -> 소재단가
|
||||
@@ -677,6 +705,7 @@ function fn_initGrid() {
|
||||
hozAlign: 'right',
|
||||
width: 100,
|
||||
title: '<span style="background-color: #FFFF00; padding: 2px 5px;">소재단가</span>',
|
||||
titleDownload: '소재단가',
|
||||
field: 'UNIT_PRICE',
|
||||
editor: 'number',
|
||||
editable: true,
|
||||
@@ -708,6 +737,7 @@ function fn_initGrid() {
|
||||
hozAlign: 'center',
|
||||
width: 90,
|
||||
title: '<span style="background-color: #FFFF00; padding: 2px 5px;">사용여부</span>',
|
||||
titleDownload: '사용여부',
|
||||
field: 'USE_YN',
|
||||
editor: 'list',
|
||||
editorParams: {
|
||||
@@ -750,6 +780,7 @@ function fn_initGrid() {
|
||||
hozAlign: 'right',
|
||||
width: 100,
|
||||
title: '<span style="background-color: #FFFF00; padding: 2px 5px;">발주수량</span>',
|
||||
titleDownload: '발주수량',
|
||||
field: 'PO_QTY',
|
||||
editor: 'number',
|
||||
mutator: function(value, data) {
|
||||
@@ -773,6 +804,7 @@ function fn_initGrid() {
|
||||
hozAlign: 'left',
|
||||
width: 150,
|
||||
title: '<span style="background-color: #FFFF00; padding: 2px 5px;">가공업체</span>',
|
||||
titleDownload: '가공업체',
|
||||
field: 'PROCESSING_VENDOR',
|
||||
editor: function(cell, onRendered, success, cancel, editorParams) {
|
||||
return createSelect2Editor(processingVendorList)(cell, onRendered, success, cancel, editorParams);
|
||||
@@ -786,6 +818,13 @@ function fn_initGrid() {
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
accessorDownload: function(value) {
|
||||
if(!value) return '';
|
||||
for(var i = 0; i < processingVendorList.length; i++) {
|
||||
if(processingVendorList[i].id == value) return processingVendorList[i].text;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
// 가공단가 (수정가능)
|
||||
@@ -794,6 +833,7 @@ function fn_initGrid() {
|
||||
hozAlign: 'right',
|
||||
width: 100,
|
||||
title: '<span style="background-color: #FFFF00; padding: 2px 5px;">가공단가</span>',
|
||||
titleDownload: '가공단가',
|
||||
field: 'PROCESSING_UNIT_PRICE',
|
||||
editor: 'number',
|
||||
editable: true,
|
||||
@@ -1226,6 +1266,318 @@ function fn_mergeSavedData(savedList){
|
||||
});
|
||||
}
|
||||
|
||||
// 컬럼 정의로부터 다운로드/업로드에 사용할 헤더 텍스트 결정 (titleDownload 우선)
|
||||
function fn_getHeaderText(c) {
|
||||
if(c.titleDownload) return c.titleDownload;
|
||||
if(c.title) return $('<div>').html(c.title).text().trim();
|
||||
return c.field || '';
|
||||
}
|
||||
|
||||
// 엑셀 다운로드 (XLSX, ExcelJS). hidden 컬럼인 OBJID도 download:true면 포함.
|
||||
// - formatter HTML 미적용(raw value 또는 accessorDownload 결과)
|
||||
// - 헤더 노란 배경, 테두리, 굵은 글씨
|
||||
// - 날짜 형식 셀은 텍스트 형식으로 강제 (Excel 자동 변환 차단)
|
||||
function fn_excelDownload() {
|
||||
if(!_tabulGrid) return;
|
||||
|
||||
// 1) 내보낼 컬럼 수집 (download:true 우선, 그 외는 visible 기준)
|
||||
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('구매리스트');
|
||||
|
||||
// 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' }
|
||||
};
|
||||
// YYYY-MM-DD 형태 문자열은 텍스트 형식으로 강제 (Excel이 날짜로 자동 변환하지 않도록)
|
||||
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') + '_' +
|
||||
String(today.getDate()).padStart(2, '0') + '_' +
|
||||
String(today.getHours()).padStart(2, '0') + '_' +
|
||||
String(today.getMinutes()).padStart(2, '0');
|
||||
var fileName = '구매리스트_' + 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 === 'VENDOR_PM' || field === 'PROCESSING_VENDOR') list = processingVendorList;
|
||||
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) → 그리드 주입
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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', '#purchaseExcelInput', function() {
|
||||
var file = this.files[0];
|
||||
fn_excelUpload(file);
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
// 저장
|
||||
function fn_save() {
|
||||
var gridData = _tabulGrid.getData();
|
||||
|
||||
181
src/com/pms/common/utils/ExcelParseUtil.java
Normal file
181
src/com/pms/common/utils/ExcelParseUtil.java
Normal file
@@ -0,0 +1,181 @@
|
||||
package com.pms.common.utils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
|
||||
/**
|
||||
* 엑셀/CSV 파일을 헤더행 기반 Map 리스트로 파싱하는 공용 유틸.
|
||||
* .xls / .xlsx / .csv 지원.
|
||||
*/
|
||||
public class ExcelParseUtil {
|
||||
|
||||
private ExcelParseUtil() {}
|
||||
|
||||
/**
|
||||
* 첫 시트(또는 CSV 본체)의 지정한 헤더행을 key로, 이후 모든 행을 value로 하는 Map 리스트를 반환.
|
||||
*
|
||||
* @param file 업로드된 파일
|
||||
* @param headerRowIdx 헤더행 인덱스 (0-base, 일반적으로 0)
|
||||
*/
|
||||
public static List<Map<String, String>> parse(File file, int headerRowIdx) throws Exception {
|
||||
String name = file.getName() == null ? "" : file.getName().toLowerCase();
|
||||
if (name.endsWith(".csv")) {
|
||||
return parseCsv(file, headerRowIdx);
|
||||
}
|
||||
return parseXlsx(file, headerRowIdx);
|
||||
}
|
||||
|
||||
private static List<Map<String, String>> parseXlsx(File file, int headerRowIdx) throws Exception {
|
||||
List<Map<String, String>> resultList = new ArrayList<Map<String, String>>();
|
||||
|
||||
FileInputStream fis = null;
|
||||
Workbook workBook = null;
|
||||
try {
|
||||
fis = new FileInputStream(file);
|
||||
workBook = WorkbookFactory.create(fis);
|
||||
Sheet sheet = workBook.getSheetAt(0);
|
||||
|
||||
Row headerRow = sheet.getRow(headerRowIdx);
|
||||
if (headerRow == null) return resultList;
|
||||
|
||||
int colCount = headerRow.getLastCellNum();
|
||||
String[] headers = new String[colCount];
|
||||
DataFormatter formatter = new DataFormatter();
|
||||
|
||||
for (int c = 0; c < colCount; c++) {
|
||||
Cell cell = headerRow.getCell(c);
|
||||
headers[c] = cell == null ? "" : formatter.formatCellValue(cell).trim();
|
||||
}
|
||||
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int r = headerRowIdx + 1; r <= lastRow; r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) continue;
|
||||
|
||||
Map<String, String> rowMap = new LinkedHashMap<String, String>();
|
||||
boolean hasValue = false;
|
||||
|
||||
for (int c = 0; c < colCount; c++) {
|
||||
String header = headers[c];
|
||||
if (header == null || header.isEmpty()) continue;
|
||||
|
||||
Cell cell = row.getCell(c);
|
||||
String value = cell == null ? "" : formatter.formatCellValue(cell);
|
||||
if (value == null) value = "";
|
||||
value = value.trim();
|
||||
|
||||
rowMap.put(header, value);
|
||||
if (!value.isEmpty()) hasValue = true;
|
||||
}
|
||||
|
||||
if (hasValue) resultList.add(rowMap);
|
||||
}
|
||||
} finally {
|
||||
if (workBook != null) { try { workBook.close(); } catch (Exception ignore) {} }
|
||||
if (fis != null) { try { fis.close(); } catch (Exception ignore) {} }
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
private static List<Map<String, String>> parseCsv(File file, int headerRowIdx) throws Exception {
|
||||
List<Map<String, String>> result = new ArrayList<Map<String, String>>();
|
||||
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
sb.append(line).append('\n');
|
||||
}
|
||||
String csv = sb.toString();
|
||||
if (csv.length() > 0 && csv.charAt(0) == '\uFEFF') {
|
||||
csv = csv.substring(1);
|
||||
}
|
||||
|
||||
List<List<String>> rows = parseCsvText(csv);
|
||||
if (rows.size() <= headerRowIdx) return result;
|
||||
|
||||
List<String> headerRow = rows.get(headerRowIdx);
|
||||
for (int r = headerRowIdx + 1; r < rows.size(); r++) {
|
||||
List<String> dataRow = rows.get(r);
|
||||
Map<String, String> rowMap = new LinkedHashMap<String, String>();
|
||||
boolean hasValue = false;
|
||||
for (int c = 0; c < headerRow.size(); c++) {
|
||||
String header = headerRow.get(c);
|
||||
if (header == null || header.isEmpty()) continue;
|
||||
String value = c < dataRow.size() ? dataRow.get(c) : "";
|
||||
if (value == null) value = "";
|
||||
value = value.trim();
|
||||
rowMap.put(header, value);
|
||||
if (!value.isEmpty()) hasValue = true;
|
||||
}
|
||||
if (hasValue) result.add(rowMap);
|
||||
}
|
||||
} finally {
|
||||
if (br != null) { try { br.close(); } catch (Exception ignore) {} }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 4180 기준 단순 CSV 파서. 따옴표로 감싼 필드 내부의 콤마/개행/이중따옴표를 처리한다.
|
||||
*/
|
||||
private static List<List<String>> parseCsvText(String csv) {
|
||||
List<List<String>> rows = new ArrayList<List<String>>();
|
||||
List<String> currentRow = new ArrayList<String>();
|
||||
StringBuilder field = new StringBuilder();
|
||||
boolean inQuotes = false;
|
||||
|
||||
for (int i = 0; i < csv.length(); i++) {
|
||||
char ch = csv.charAt(i);
|
||||
if (inQuotes) {
|
||||
if (ch == '"') {
|
||||
if (i + 1 < csv.length() && csv.charAt(i + 1) == '"') {
|
||||
field.append('"');
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = false;
|
||||
}
|
||||
} else {
|
||||
field.append(ch);
|
||||
}
|
||||
} else {
|
||||
if (ch == '"') {
|
||||
inQuotes = true;
|
||||
} else if (ch == ',') {
|
||||
currentRow.add(field.toString());
|
||||
field.setLength(0);
|
||||
} else if (ch == '\r') {
|
||||
// 다음 \n에서 행 종료
|
||||
} else if (ch == '\n') {
|
||||
currentRow.add(field.toString());
|
||||
field.setLength(0);
|
||||
rows.add(currentRow);
|
||||
currentRow = new ArrayList<String>();
|
||||
} else {
|
||||
field.append(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (field.length() > 0 || !currentRow.isEmpty()) {
|
||||
currentRow.add(field.toString());
|
||||
rows.add(currentRow);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import com.oreilly.servlet.MultipartRequest;
|
||||
import com.pms.common.FileRenameClass;
|
||||
import com.pms.common.utils.CommonUtils;
|
||||
import com.pms.common.utils.Constants;
|
||||
import com.pms.common.utils.ExcelParseUtil;
|
||||
import com.pms.service.CommonService;
|
||||
|
||||
@Controller
|
||||
@@ -122,6 +123,75 @@ public class CommonController {
|
||||
return "/ajax/ajaxResult";
|
||||
}
|
||||
|
||||
/**
|
||||
* 엑셀 파일을 임시로 업로드 받아 헤더행 기반으로 Map 리스트로 파싱하여 JSON 응답.
|
||||
* 파일은 DB 등록 없이 임시 디렉토리에 저장 후 파싱 완료 시 삭제한다.
|
||||
*
|
||||
* 요청: multipart/form-data, 파일 input name="excelFile", (선택) headerRow
|
||||
* 응답: { resultFlag: "S"/"F", rows: [{헤더: 값, ...}], message }
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@ResponseBody
|
||||
@RequestMapping("/common/parseExcelFile.do")
|
||||
public Map<String, Object> parseExcelFile(HttpServletRequest request) {
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
File uploadedFile = null;
|
||||
FileRenameClass frc = null;
|
||||
try {
|
||||
String tempDir = Constants.FILE_STORAGE + File.separator + "temp_excel";
|
||||
File dir = new File(tempDir);
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
|
||||
int maxSize = 1024 * 1024 * 100; // 100MB
|
||||
frc = new FileRenameClass();
|
||||
MultipartRequest multi = new MultipartRequest(request, tempDir, maxSize, "UTF-8", frc);
|
||||
|
||||
int headerRow = 0;
|
||||
String headerRowParam = multi.getParameter("headerRow");
|
||||
if (headerRowParam != null && !"".equals(headerRowParam)) {
|
||||
try { headerRow = Integer.parseInt(headerRowParam); } catch (Exception ignore) {}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> fileList = frc.getFileList();
|
||||
if (fileList != null && !fileList.isEmpty()) {
|
||||
Map<String, Object> fm = fileList.get(0);
|
||||
String fullPath = CommonUtils.checkNull(fm.get("savedFullFileName"));
|
||||
if (!"".equals(fullPath)) {
|
||||
uploadedFile = new File(fullPath);
|
||||
} else {
|
||||
String path = CommonUtils.checkNull(fm.get("filePath"));
|
||||
String savedName = CommonUtils.checkNull(fm.get("savedFileName"));
|
||||
if (!"".equals(savedName)) {
|
||||
if ("".equals(path)) path = tempDir;
|
||||
uploadedFile = new File(path + File.separator + savedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadedFile == null || !uploadedFile.exists()) {
|
||||
result.put("resultFlag", "F");
|
||||
result.put("message", "업로드된 파일을 찾을 수 없습니다.");
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Map<String, String>> rows = ExcelParseUtil.parse(uploadedFile, headerRow);
|
||||
result.put("resultFlag", "S");
|
||||
result.put("rows", rows);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
result.put("resultFlag", "F");
|
||||
result.put("message", e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (uploadedFile != null && uploadedFile.exists()) {
|
||||
uploadedFile.delete();
|
||||
}
|
||||
} catch (Exception ignore) {}
|
||||
if (frc != null) frc.clear();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 목록 조회(ajax)
|
||||
* @param paramMap
|
||||
|
||||
Reference in New Issue
Block a user