Merge pull request 'V20260210' (#208) from V20260210 into main
Reviewed-on: #208
This commit was merged in pull request #208.
This commit is contained in:
@@ -1647,17 +1647,70 @@ function fn_buildHeaderFieldMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
// Select2 코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑
|
||||
// Select2/코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑
|
||||
// 반환: { value, isCodeColumn, matched }
|
||||
// isCodeColumn=true && matched=false 이면 기준정보에 없는 값 (호출부에서 invalid 처리)
|
||||
function fn_reverseSelectValue(field, value) {
|
||||
if(value === null || value === undefined || value === '') return value;
|
||||
if(value === null || value === undefined || value === '') {
|
||||
return { value: value, isCodeColumn: false, matched: true };
|
||||
}
|
||||
// RAW_MATERIAL: materialList는 [string, ...] 형태(ID=text 동일)
|
||||
if(field === 'RAW_MATERIAL') {
|
||||
var v = String(value);
|
||||
var arr = materialList || [];
|
||||
for(var i = 0; i < arr.length; i++) {
|
||||
if(String(arr[i]) === v) {
|
||||
return { value: arr[i], isCodeColumn: true, matched: true };
|
||||
}
|
||||
}
|
||||
return { value: value, isCodeColumn: true, matched: false };
|
||||
}
|
||||
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;
|
||||
else if(field === 'SUPPLY_TYPE') list = [{id:'자급', text:'자급'}, {id:'사급', text:'사급'}];
|
||||
if(!list || list.length === 0) {
|
||||
return { value: value, isCodeColumn: false, matched: true };
|
||||
}
|
||||
return value;
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
if(list[i].text === value || String(list[i].id) === String(value)) {
|
||||
return { value: list[i].id, isCodeColumn: true, matched: true };
|
||||
}
|
||||
}
|
||||
return { value: value, isCodeColumn: true, matched: false };
|
||||
}
|
||||
|
||||
// 필드명 → 한국어 라벨 매핑 (알람 메시지용)
|
||||
function fn_getFieldLabel(field) {
|
||||
if(field === 'PROCESSING_VENDOR') return '가공업체';
|
||||
if(field === 'CURRENCY') return '환종';
|
||||
if(field === 'SUPPLY_TYPE') return '자급/사급';
|
||||
if(field === 'RAW_MATERIAL') return '소재재질';
|
||||
if(field === 'SIZE') return '규격';
|
||||
if(field === 'RAW_MATERIAL_NO') return '소재품번';
|
||||
if(field === 'PRODUCTION_QTY') return '제작수량';
|
||||
return field;
|
||||
}
|
||||
|
||||
// 숫자만 허용 컬럼 (엑셀 업로드 시 형식 검증)
|
||||
var NUMERIC_FIELDS = { PRODUCTION_QTY: true };
|
||||
|
||||
// 저장 버튼 표시/숨김 — 저장 버튼은 상위 frameset의 헤더 프레임에 위치
|
||||
// 프레임 구조: top = mBomPopupHeaderFs → top.frames[0] = mBomHeaderPopup (#btnSave 보유)
|
||||
function fn_toggleSaveBtn(show) {
|
||||
try {
|
||||
var headerFrame = (top && top.frames && top.frames[0]) ? top.frames[0] : null;
|
||||
if(!headerFrame || !headerFrame.document) return;
|
||||
var btn = headerFrame.document.getElementById('btnSave');
|
||||
if(!btn) return;
|
||||
btn.style.display = show ? '' : 'none';
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// SweetAlert html 모드에서 사용자 입력값이 HTML로 해석되지 않도록 이스케이프
|
||||
function fn_escapeHtml(s) {
|
||||
if(s === null || s === undefined) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// 엑셀 업로드: 파싱 → 역매핑 → OBJID 무결성 검증 → PART_NO 우선 매칭(중복 시 OBJID) → 그리드 주입
|
||||
@@ -1691,6 +1744,7 @@ function fn_excelUpload(file) {
|
||||
return;
|
||||
}
|
||||
var headerFieldMap = fn_buildHeaderFieldMap();
|
||||
var editableFields = fn_buildEditableFields();
|
||||
|
||||
// 기존 행: PART_NO별 그룹 + OBJID 화이트리스트
|
||||
var existing = _tabulGrid.getData() || [];
|
||||
@@ -1704,8 +1758,9 @@ function fn_excelUpload(file) {
|
||||
if(oid) existingObjidSet[oid] = true;
|
||||
}
|
||||
|
||||
// 1) 헤더 → field 변환 + Select2 역매핑
|
||||
// 1) 헤더 → field 변환 + Select2 역매핑 + 기준정보 미매칭/숫자 형식 오류 수집 (편집 가능 컬럼만)
|
||||
var converted = [];
|
||||
var invalidCells = []; // { row, field, label, value, reason }
|
||||
for(var i = 0; i < rows.length; i++) {
|
||||
var src = rows[i];
|
||||
var out = {};
|
||||
@@ -1713,7 +1768,34 @@ function fn_excelUpload(file) {
|
||||
if(!src.hasOwnProperty(header)) continue;
|
||||
var field = headerFieldMap[header];
|
||||
if(!field) continue;
|
||||
out[field] = fn_reverseSelectValue(field, src[header]);
|
||||
var rawVal = src[header];
|
||||
// 숫자 컬럼: 빈 값 통과, 숫자 변환 실패 시 invalid (편집 가능 컬럼만)
|
||||
if(NUMERIC_FIELDS[field] && editableFields[field]) {
|
||||
if(rawVal !== null && rawVal !== undefined && String(rawVal).trim() !== '') {
|
||||
var numVal = Number(String(rawVal).replace(/,/g, ''));
|
||||
if(isNaN(numVal)) {
|
||||
invalidCells.push({
|
||||
row: i + 2, field: field, label: fn_getFieldLabel(field),
|
||||
value: rawVal, reason: '숫자만 입력 가능합니다'
|
||||
});
|
||||
out[field] = rawVal; // 그리드에 그대로 반영(사용자가 보고 수정)
|
||||
} else {
|
||||
out[field] = numVal;
|
||||
}
|
||||
} else {
|
||||
out[field] = rawVal;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 코드 컬럼: 역매핑 + 기준정보 매칭 검증
|
||||
var r = fn_reverseSelectValue(field, rawVal);
|
||||
out[field] = r.value;
|
||||
if(r.isCodeColumn && !r.matched && editableFields[field]) {
|
||||
invalidCells.push({
|
||||
row: i + 2, field: field, label: fn_getFieldLabel(field),
|
||||
value: rawVal, reason: '기준정보에 없습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
converted.push(out);
|
||||
}
|
||||
@@ -1736,9 +1818,6 @@ function fn_excelUpload(file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 편집 가능 컬럼 집합 (이것만 머지 시 덮어쓰기, 그 외는 base 보존)
|
||||
var editableFields = fn_buildEditableFields();
|
||||
|
||||
// 3) PART_NO 우선 매칭 (중복 시 OBJID disambiguate)
|
||||
var usedObjids = {};
|
||||
function pickMatch(uploadRow) {
|
||||
@@ -1821,8 +1900,79 @@ function fn_excelUpload(file) {
|
||||
return;
|
||||
}
|
||||
|
||||
_tabulGrid.replaceData(merged).then(function() {
|
||||
Swal.fire('완료', merged.length + '건이 그리드에 적용되었습니다. 저장 버튼을 눌러 반영하세요.', 'success');
|
||||
// finalize: 그리드 반영 + 알람 + 저장 버튼 토글 (서버 검증 후 호출)
|
||||
function fn_finalizeUpload() {
|
||||
if(invalidCells.length > 0) {
|
||||
_tabulGrid.replaceData(merged).then(function() {
|
||||
fn_toggleSaveBtn(false);
|
||||
// 행 번호 오름차순으로 정렬 후 메시지 구성
|
||||
invalidCells.sort(function(a, b) { return (a.row || 0) - (b.row || 0); });
|
||||
var lines = '';
|
||||
for(var i = 0; i < Math.min(invalidCells.length, 20); i++) {
|
||||
var it = invalidCells[i];
|
||||
lines += ' - ' + it.row + '행: ' + fn_escapeHtml(it.label)
|
||||
+ " '" + fn_escapeHtml(it.value) + "' "
|
||||
+ fn_escapeHtml(it.reason);
|
||||
if(it.hint) {
|
||||
lines += '<div style="color:#888; font-size:11px; padding-left:18px;">↳ '
|
||||
+ fn_escapeHtml(it.hint) + '</div>';
|
||||
} else {
|
||||
lines += '<br>';
|
||||
}
|
||||
}
|
||||
if(invalidCells.length > 20) lines += ' ... (총 ' + invalidCells.length + '건)<br>';
|
||||
var html = '잘못된 값이 포함되어 저장이 차단됩니다.<br>'
|
||||
+ '정상 엑셀로 다시 업로드하세요.<br><br>'
|
||||
+ '<div style="text-align:left; font-family:monospace; font-size:13px;">' + lines + '</div>';
|
||||
Swal.fire({ title: '오류', html: html, icon: 'error', width: 700 });
|
||||
});
|
||||
} else {
|
||||
_tabulGrid.replaceData(merged).then(function() {
|
||||
fn_toggleSaveBtn(true);
|
||||
Swal.fire('완료', merged.length + '건이 그리드에 적용되었습니다. 저장 버튼을 눌러 반영하세요.', 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5) 서버 검증: 소재재질+규격+소재품번 (PART_MNG 마스터 매칭)
|
||||
var serverRows = [];
|
||||
for(var i = 0; i < merged.length; i++) {
|
||||
var r = merged[i];
|
||||
serverRows.push({
|
||||
row: i + 2,
|
||||
SUPPLY_TYPE: r.SUPPLY_TYPE || '',
|
||||
PART_NO: r.PART_NO || '',
|
||||
RAW_MATERIAL: r.RAW_MATERIAL || '',
|
||||
SIZE: r.SIZE || '',
|
||||
RAW_MATERIAL_NO: r.RAW_MATERIAL_NO || ''
|
||||
});
|
||||
}
|
||||
$.ajax({
|
||||
url: '/productionplanning/validateMbomMaterial.do',
|
||||
method: 'POST',
|
||||
contentType: 'application/json; charset=UTF-8',
|
||||
data: JSON.stringify({ rows: serverRows }),
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if(res && res.resultFlag === 'S' && res.invalid && res.invalid.length > 0) {
|
||||
for(var i = 0; i < res.invalid.length; i++) {
|
||||
var inv = res.invalid[i];
|
||||
invalidCells.push({
|
||||
row: inv.row,
|
||||
field: inv.field,
|
||||
label: fn_getFieldLabel(inv.field),
|
||||
value: inv.value,
|
||||
reason: inv.reason,
|
||||
hint: inv.hint || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
fn_finalizeUpload();
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire('경고', '소재 마스터 검증 호출에 실패했습니다. 클라이언트 검증만 적용됩니다.', 'warning');
|
||||
fn_finalizeUpload();
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
|
||||
@@ -109,7 +109,7 @@ body, html {
|
||||
<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" id="btnSave" onclick="fn_save();" style="margin-right: 5px;">
|
||||
<input type="button" value="닫기" class="plm_btns" onclick="window.close();" style="margin-right: 5px;">
|
||||
</div>
|
||||
<div style="clear: both;"></div>
|
||||
@@ -1431,16 +1431,59 @@ function fn_buildHeaderFieldMap() {
|
||||
}
|
||||
|
||||
// Select2 코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑
|
||||
// Select2/정적 코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑
|
||||
// 반환: { value, isCodeColumn, matched }
|
||||
// isCodeColumn=true && matched=false 이면 기준정보에 없는 값 (호출부에서 invalid 처리)
|
||||
// 화이트리스트에 있는 필드는 list가 비어 있어도 isCodeColumn=true로 간주(검증 누락 방지)
|
||||
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;
|
||||
if(value === null || value === undefined || value === '') {
|
||||
return { value: value, isCodeColumn: false, matched: true };
|
||||
}
|
||||
return value;
|
||||
var list = null;
|
||||
var isCode = false;
|
||||
if(field === 'VENDOR_PM' || field === 'PROCESSING_VENDOR') { list = processingVendorList; isCode = true; }
|
||||
else if(field === 'CURRENCY') { list = currencyList; isCode = true; }
|
||||
else if(field === 'USE_YN') { list = [{id:'사용', text:'사용'}, {id:'미사용', text:'미사용'}]; isCode = true; }
|
||||
if(!isCode) {
|
||||
return { value: value, isCodeColumn: false, matched: true };
|
||||
}
|
||||
if(!list) list = [];
|
||||
var v = String(value).trim();
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
if(String(list[i].text).trim() === v || String(list[i].id).trim() === v) {
|
||||
return { value: list[i].id, isCodeColumn: true, matched: true };
|
||||
}
|
||||
}
|
||||
return { value: value, isCodeColumn: true, matched: false };
|
||||
}
|
||||
|
||||
// 필드명 → 한국어 라벨 매핑 (알람 메시지용)
|
||||
function fn_getFieldLabel(field) {
|
||||
if(field === 'VENDOR_PM') return '공급업체';
|
||||
if(field === 'PROCESSING_VENDOR') return '가공업체';
|
||||
if(field === 'CURRENCY') return '환종';
|
||||
if(field === 'USE_YN') return '사용여부';
|
||||
if(field === 'UNIT_PRICE') return '소재단가';
|
||||
if(field === 'PO_QTY') return '발주수량';
|
||||
if(field === 'PROCESSING_UNIT_PRICE') return '가공단가';
|
||||
return field;
|
||||
}
|
||||
|
||||
// 숫자만 허용 컬럼 (엑셀 업로드 시 형식 검증). 천단위 콤마는 허용.
|
||||
var NUMERIC_FIELDS = { UNIT_PRICE: true, PO_QTY: true, PROCESSING_UNIT_PRICE: true };
|
||||
|
||||
// 저장 버튼 표시/숨김 (엑셀 업로드에서 기준정보 미매칭 발생 시 저장 차단)
|
||||
function fn_toggleSaveBtn(show) {
|
||||
var $btn = $('#btnSave');
|
||||
if(!$btn.length) return;
|
||||
if(show) $btn.show();
|
||||
else $btn.hide();
|
||||
}
|
||||
|
||||
// SweetAlert html 모드에서 사용자 입력값이 HTML로 해석되지 않도록 이스케이프
|
||||
function fn_escapeHtml(s) {
|
||||
if(s === null || s === undefined) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// 엑셀 업로드: 파싱 → 역매핑 → OBJID 무결성 검증 → PART_NO 우선 매칭(중복 시 OBJID) → 그리드 주입
|
||||
@@ -1468,6 +1511,7 @@ function fn_excelUpload(file) {
|
||||
return;
|
||||
}
|
||||
var headerFieldMap = fn_buildHeaderFieldMap();
|
||||
var editableFields = fn_buildEditableFields();
|
||||
|
||||
// 기존 행: PART_NO별 그룹 + OBJID 화이트리스트
|
||||
var existing = _tabulGrid.getData() || [];
|
||||
@@ -1481,8 +1525,9 @@ function fn_excelUpload(file) {
|
||||
if(oid) existingObjidSet[oid] = true;
|
||||
}
|
||||
|
||||
// 1) 헤더 → field 변환 + Select2 역매핑
|
||||
// 1) 헤더 → field 변환 + Select2 역매핑 + 기준정보 미매칭/숫자 형식 오류 수집 (편집 가능 컬럼만)
|
||||
var converted = [];
|
||||
var invalidCells = []; // { row, field, label, value, reason }
|
||||
for(var i = 0; i < rows.length; i++) {
|
||||
var src = rows[i];
|
||||
var out = {};
|
||||
@@ -1490,7 +1535,36 @@ function fn_excelUpload(file) {
|
||||
if(!src.hasOwnProperty(header)) continue;
|
||||
var field = headerFieldMap[header];
|
||||
if(!field) continue;
|
||||
out[field] = fn_reverseSelectValue(field, src[header]);
|
||||
var rawVal = src[header];
|
||||
// 숫자 컬럼: 빈 값 통과, 변환 실패 시 invalid
|
||||
if(NUMERIC_FIELDS[field] && editableFields[field]) {
|
||||
if(rawVal !== null && rawVal !== undefined && String(rawVal).trim() !== '') {
|
||||
var numVal = Number(String(rawVal).replace(/,/g, ''));
|
||||
if(isNaN(numVal)) {
|
||||
invalidCells.push({
|
||||
row: i + 2, field: field, label: fn_getFieldLabel(field),
|
||||
value: rawVal, reason: '숫자만 입력 가능합니다'
|
||||
});
|
||||
out[field] = rawVal;
|
||||
} else {
|
||||
out[field] = numVal;
|
||||
}
|
||||
} else {
|
||||
out[field] = rawVal;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 코드 컬럼: 역매핑 + 기준정보 매칭 검증
|
||||
// (editable 체크 없이 검증 — 코드 컬럼은 모두 사용자 입력 대상이므로
|
||||
// editableFields 판정 누락 시에도 안전하게 잡기 위함)
|
||||
var r = fn_reverseSelectValue(field, rawVal);
|
||||
out[field] = r.value;
|
||||
if(r.isCodeColumn && !r.matched) {
|
||||
invalidCells.push({
|
||||
row: i + 2, field: field, label: fn_getFieldLabel(field),
|
||||
value: rawVal, reason: '기준정보에 없습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
converted.push(out);
|
||||
}
|
||||
@@ -1513,9 +1587,6 @@ function fn_excelUpload(file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 편집 가능 컬럼 집합 (이것만 머지 시 덮어쓰기, 그 외는 base 보존)
|
||||
var editableFields = fn_buildEditableFields();
|
||||
|
||||
// 3) PART_NO 우선 매칭 (중복 시 OBJID disambiguate)
|
||||
var usedObjids = {};
|
||||
function pickMatch(uploadRow) {
|
||||
@@ -1596,7 +1667,29 @@ function fn_excelUpload(file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5) 잘못된 셀(기준정보 미매칭/숫자 형식 오류) 분기: 데이터는 그리드에 반영하되 저장 버튼 숨김
|
||||
if(invalidCells.length > 0) {
|
||||
_tabulGrid.replaceData(merged).then(function() {
|
||||
fn_toggleSaveBtn(false);
|
||||
invalidCells.sort(function(a, b) { return (a.row || 0) - (b.row || 0); });
|
||||
var lines = '';
|
||||
for(var i = 0; i < Math.min(invalidCells.length, 20); i++) {
|
||||
var it = invalidCells[i];
|
||||
lines += ' - ' + it.row + '행: ' + fn_escapeHtml(it.label)
|
||||
+ " '" + fn_escapeHtml(it.value) + "' "
|
||||
+ fn_escapeHtml(it.reason) + '<br>';
|
||||
}
|
||||
if(invalidCells.length > 20) lines += ' ... (총 ' + invalidCells.length + '건)<br>';
|
||||
var html = '잘못된 값이 포함되어 저장이 차단됩니다.<br>'
|
||||
+ '정상 엑셀로 다시 업로드하세요.<br><br>'
|
||||
+ '<div style="text-align:left; font-family:monospace; font-size:13px;">' + lines + '</div>';
|
||||
Swal.fire({ title: '오류', html: html, icon: 'error', width: 700 });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_tabulGrid.replaceData(merged).then(function() {
|
||||
fn_toggleSaveBtn(true);
|
||||
Swal.fire('완료', merged.length + '건이 그리드에 적용되었습니다. 저장 버튼을 눌러 반영하세요.', 'success');
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2228,4 +2228,14 @@ public class ProductionPlanningController extends BaseService {
|
||||
return "/productionplanning/mBomHistoryDetailPopup";
|
||||
}
|
||||
|
||||
/**
|
||||
* M-BOM 엑셀 업로드 - 소재 마스터 검증 (소재재질/규격/소재품번)
|
||||
* 클라이언트가 머지된 행 배열을 JSON 본문으로 전달, 서버는 invalid 배열 반환.
|
||||
*/
|
||||
@ResponseBody
|
||||
@RequestMapping("/productionplanning/validateMbomMaterial.do")
|
||||
public Map<String, Object> validateMbomMaterial(@RequestBody Map<String, Object> paramMap) {
|
||||
return productionPlanningService.validateMbomMaterial(paramMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5496,5 +5496,16 @@
|
||||
WHERE ITEM_PARENT_CD IS NOT NULL AND ITEM_PARENT_CD != ''
|
||||
</select>
|
||||
|
||||
<!-- M-BOM 엑셀 업로드 검증용: PART_MNG에서 활성 소재 마스터 (소재재질/규격/소재품번) 전체 조회 -->
|
||||
<!-- resultType은 UpperKeyMap이어야 PostgreSQL의 소문자 컬럼명이 자바에서 대문자 키로 들어옴 -->
|
||||
<select id="selectMaterialMasterAll" resultType="com.pms.common.UpperKeyMap">
|
||||
SELECT
|
||||
PART_NAME AS MATERIAL_CODE,
|
||||
SPEC AS SIZE_SPEC,
|
||||
PART_NO AS MATERIAL_PART_NO
|
||||
FROM PART_MNG
|
||||
WHERE STATUS = 'release'
|
||||
AND ACCTFG = '0'
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -10,6 +10,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.pms.common.JsonUtil;
|
||||
@@ -22,10 +23,10 @@ import com.pms.common.utils.SerialNoSyncUtil;
|
||||
|
||||
@Service
|
||||
public class ProductionPlanningService {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Autowired
|
||||
private BatchService batchService;
|
||||
|
||||
/**
|
||||
* 이슈관리 상세 조회
|
||||
* @param paramMap
|
||||
@@ -1191,12 +1192,11 @@ public class ProductionPlanningService {
|
||||
public boolean saveMbom(HttpServletRequest request, Map<String, Object> paramMap) {
|
||||
SqlSession sqlSession = null;
|
||||
boolean result = false;
|
||||
String mbomHeaderObjidForErp = null;
|
||||
|
||||
try {
|
||||
// 트랜잭션 처리를 위해 autoCommit = false로 설정
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
|
||||
|
||||
// 세션에서 사용자 정보 가져오기
|
||||
HttpSession session = request.getSession();
|
||||
PersonBean person = (PersonBean)session.getAttribute(Constants.PERSON_BEAN);
|
||||
String userId = CommonUtils.checkNull(person.getUserId());
|
||||
@@ -1362,6 +1362,7 @@ public class ProductionPlanningService {
|
||||
}
|
||||
|
||||
// M-BOM 헤더 업데이트
|
||||
mbomHeaderObjidForErp = CommonUtils.checkNull(existingMbom.get("OBJID"));
|
||||
paramMap.put("mbomHeaderObjid", existingMbom.get("OBJID"));
|
||||
sqlSession.update("productionplanning.updateMbomHeader", paramMap);
|
||||
|
||||
@@ -1497,6 +1498,7 @@ public class ProductionPlanningService {
|
||||
}
|
||||
|
||||
// MBOM_HEADER 삽입
|
||||
mbomHeaderObjidForErp = newObjid;
|
||||
sqlSession.insert("productionplanning.insertMbomHeader", paramMap);
|
||||
|
||||
// MBOM_DETAIL 삽입
|
||||
@@ -1586,7 +1588,27 @@ public class ProductionPlanningService {
|
||||
sqlSession.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// DB 커밋 성공 후 ERP BOM 동기화 (ERP 실패해도 DB는 유지)
|
||||
if(result && mbomHeaderObjidForErp != null && !mbomHeaderObjidForErp.isEmpty()) {
|
||||
try {
|
||||
System.out.println("====================================");
|
||||
System.out.println("M-BOM 저장 후 ERP BOM 동기화 시작");
|
||||
System.out.println("MBOM_HEADER_OBJID: " + mbomHeaderObjidForErp);
|
||||
System.out.println("====================================");
|
||||
|
||||
Map<String, Object> erpResult = batchService.syncMbomBomToErp(mbomHeaderObjidForErp);
|
||||
if(erpResult != null && Boolean.TRUE.equals(erpResult.get("success"))) {
|
||||
System.out.println("ERP BOM 동기화 성공: " + erpResult.get("message"));
|
||||
} else {
|
||||
System.err.println("ERP BOM 동기화 실패: " + (erpResult != null ? erpResult.get("message") : "결과 없음"));
|
||||
}
|
||||
} catch(Exception erpEx) {
|
||||
System.err.println("ERP BOM 동기화 오류: " + erpEx.getMessage());
|
||||
erpEx.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2456,4 +2478,150 @@ public class ProductionPlanningService {
|
||||
}
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 키 비교용 정규화: 양끝 공백 제거 + 유니코드 NFC 정규화.
|
||||
* 'Ø', 'Å' 등 결합문자가 NFC/NFD 형태로 저장돼 표면 글자는 같아도 매칭 실패하는 경우 방지.
|
||||
*/
|
||||
private String normalizeKey(Object obj) {
|
||||
String s = CommonUtils.checkNull(obj);
|
||||
if (s.isEmpty()) return s;
|
||||
s = s.trim();
|
||||
try {
|
||||
s = java.text.Normalizer.normalize(s, java.text.Normalizer.Form.NFC);
|
||||
} catch (Exception ignore) {}
|
||||
return s;
|
||||
}
|
||||
|
||||
// 디버깅 힌트용: Set의 일부 샘플을 ", "로 연결 (정렬, 최대 maxCount개)
|
||||
private String joinSample(java.util.Set<String> set, int maxCount) {
|
||||
if (set == null || set.isEmpty()) return "(없음)";
|
||||
java.util.List<String> list = new ArrayList<String>(set);
|
||||
java.util.Collections.sort(list);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < list.size() && i < maxCount; i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
sb.append("'").append(list.get(i)).append("'");
|
||||
}
|
||||
if (list.size() > maxCount) sb.append(" ... (총 ").append(list.size()).append("개)");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* M-BOM 엑셀 업로드 - 소재 마스터(소재재질+규격+소재품번) 검증.
|
||||
* 입력 paramMap: { rows: [ { row, SUPPLY_TYPE, PART_NO, RAW_MATERIAL, SIZE, RAW_MATERIAL_NO }, ... ] }
|
||||
* 출력: { resultFlag: 'S', invalid: [ { row, field, value, reason }, ... ] }
|
||||
*
|
||||
* 검증 규칙 (사급 행만 — 자급은 소재 무관):
|
||||
* - SIZE: (RAW_MATERIAL, SIZE) 조합이 마스터에 존재해야 함
|
||||
* - RAW_MATERIAL_NO:
|
||||
* - (RAW_MATERIAL, SIZE) 모두 있으면 그 조합으로 매핑된 소재품번과 일치해야 함
|
||||
* - 그 외에는 마스터의 소재품번 집합에 존재해야 함
|
||||
*/
|
||||
public Map<String, Object> validateMbomMaterial(Map<String, Object> paramMap) {
|
||||
Map<String, Object> resultMap = new HashMap<String, Object>();
|
||||
List<Map<String, Object>> invalid = new ArrayList<Map<String, Object>>();
|
||||
SqlSession sqlSession = null;
|
||||
try {
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession();
|
||||
|
||||
List<Map> masters = sqlSession.selectList("productionplanning.selectMaterialMasterAll");
|
||||
Map<String, java.util.Set<String>> sizesByMaterial = new HashMap<String, java.util.Set<String>>();
|
||||
Map<String, String> partNoByMaterialSpec = new HashMap<String, String>();
|
||||
java.util.Set<String> validPartNos = new java.util.HashSet<String>();
|
||||
for (int i = 0; i < masters.size(); i++) {
|
||||
Map m = masters.get(i);
|
||||
String material = normalizeKey(m.get("MATERIAL_CODE"));
|
||||
String spec = normalizeKey(m.get("SIZE_SPEC"));
|
||||
String partNo = normalizeKey(m.get("MATERIAL_PART_NO"));
|
||||
java.util.Set<String> sizes = sizesByMaterial.get(material);
|
||||
if (sizes == null) {
|
||||
sizes = new java.util.HashSet<String>();
|
||||
sizesByMaterial.put(material, sizes);
|
||||
}
|
||||
if (!spec.isEmpty()) sizes.add(spec);
|
||||
if (!material.isEmpty() && !spec.isEmpty()) {
|
||||
partNoByMaterialSpec.put(material + "|" + spec, partNo);
|
||||
}
|
||||
if (!partNo.isEmpty()) validPartNos.add(partNo);
|
||||
}
|
||||
|
||||
Object rowsObj = paramMap.get("rows");
|
||||
if (rowsObj instanceof List) {
|
||||
List rows = (List) rowsObj;
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
Object rowObj = rows.get(i);
|
||||
if (!(rowObj instanceof Map)) continue;
|
||||
Map row = (Map) rowObj;
|
||||
|
||||
Object rowNoObj = row.get("row");
|
||||
int rowNo;
|
||||
try {
|
||||
rowNo = (rowNoObj == null) ? (i + 2) : Integer.parseInt(String.valueOf(rowNoObj));
|
||||
} catch (Exception ignore) {
|
||||
rowNo = i + 2;
|
||||
}
|
||||
|
||||
String supplyType = CommonUtils.checkNull(row.get("SUPPLY_TYPE")).trim();
|
||||
if ("자급".equals(supplyType)) continue;
|
||||
|
||||
String material = normalizeKey(row.get("RAW_MATERIAL"));
|
||||
String spec = normalizeKey(row.get("SIZE"));
|
||||
String partNo = normalizeKey(row.get("RAW_MATERIAL_NO"));
|
||||
|
||||
// SIZE 검증
|
||||
if (!material.isEmpty() && !spec.isEmpty()) {
|
||||
java.util.Set<String> sizes = sizesByMaterial.get(material);
|
||||
if (sizes == null || !sizes.contains(spec)) {
|
||||
Map<String, Object> err = new HashMap<String, Object>();
|
||||
err.put("row", rowNo);
|
||||
err.put("field", "SIZE");
|
||||
err.put("value", spec);
|
||||
err.put("reason", "기준정보에 없는 규격입니다");
|
||||
// 디버깅 힌트: 같은 소재재질에 어떤 규격들이 있는지 일부 노출
|
||||
if (sizes == null) {
|
||||
err.put("hint", "PART_MNG에 소재재질 '" + material + "' 자체가 없습니다 (또는 STATUS!='release' / ACCTFG!='0')");
|
||||
} else {
|
||||
err.put("hint", "사용 가능 규격: " + joinSample(sizes, 8));
|
||||
}
|
||||
invalid.add(err);
|
||||
}
|
||||
}
|
||||
|
||||
// RAW_MATERIAL_NO 검증
|
||||
if (!partNo.isEmpty()) {
|
||||
if (!material.isEmpty() && !spec.isEmpty()) {
|
||||
String expected = partNoByMaterialSpec.get(material + "|" + spec);
|
||||
if (expected != null && !expected.equals(partNo)) {
|
||||
Map<String, Object> err = new HashMap<String, Object>();
|
||||
err.put("row", rowNo);
|
||||
err.put("field", "RAW_MATERIAL_NO");
|
||||
err.put("value", partNo);
|
||||
err.put("reason", "(소재재질+규격) 조합과 일치하지 않습니다");
|
||||
err.put("hint", "마스터 매핑값: '" + expected + "'");
|
||||
invalid.add(err);
|
||||
}
|
||||
} else if (!validPartNos.contains(partNo)) {
|
||||
Map<String, Object> err = new HashMap<String, Object>();
|
||||
err.put("row", rowNo);
|
||||
err.put("field", "RAW_MATERIAL_NO");
|
||||
err.put("value", partNo);
|
||||
err.put("reason", "기준정보에 없는 소재품번입니다");
|
||||
invalid.add(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultMap.put("resultFlag", "S");
|
||||
resultMap.put("invalid", invalid);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("resultFlag", "E");
|
||||
resultMap.put("message", e.getMessage());
|
||||
} finally {
|
||||
if (sqlSession != null) sqlSession.close();
|
||||
}
|
||||
return resultMap;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user