V20260210 #208

Merged
hjjeong merged 2 commits from V20260210 into main 2026-04-27 09:20:11 +00:00
5 changed files with 466 additions and 34 deletions

View File

@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// 엑셀 업로드: 파싱 → 역매핑 → 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) {

View File

@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// 엑셀 업로드: 파싱 → 역매핑 → 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');
});
},

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}
}