M-BOM, 구매리스트 엑셀 업로드 - 기준정보/숫자 형식 검증 및 저장 버튼 차단
- 코드 컬럼(가공업체/공급업체/환종/자급사급/소재재질) 기준정보 매칭 검증 - 숫자 컬럼(제작수량) 형식 검증, 미매칭/형식오류 시 알람 + 저장 버튼 숨김 - M-BOM 규격/소재품번은 PART_MNG 마스터 서버 검증 API 신설 (/productionplanning/validateMbomMaterial.do, NFC 정규화, 디버깅 힌트 포함) - 알람을 html 모드로 변경하여 행번호 정렬 + 줄바꿈 + hint 작은 글자 표시 - 미매칭 데이터는 그리드에 머지 반영하되 저장 버튼만 차단 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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