ebom, mbom

E-BOM & M-BOM 파트 추가 삭제시 반제품 추가 삭제하면 하위 품목 같이 추가 삭제 되는 기능
E-BOM & M-BOM 파트 추가시 원하는 위치로 집어 넣는 기능(현재는 왼쪽에서 선택한 하위레벨의 제일 밑으로만 들어감)
E-BOM & M-BOM 파트 추가 삭제시 자동저장이 아니라, 저장/닫기 버튼이 있어서 저장버튼 누를때만 저장되도록 변경 필요
신규 프로젝트 생성하고, M-BOM 처음 만들때 E-BOM 을 가져와서 할당할때
최상위 제품을 변경하는 로직이 필요(최상위 제품을 삭제하고 반제품을 최상위 품으로 만드는 로직)
최상위 제품으로 만들려고 하는 반제품의 하위 부품도 딸려와서 구성이 되어야 하며,
M-BOM 의 이름도 변경된 최상위 제품의 품번으로 변경되어 저장되어야 함
This commit is contained in:
2026-02-27 18:50:47 +09:00
parent ac189cd114
commit 3e27278599
10 changed files with 1191 additions and 461 deletions

View File

@@ -19,7 +19,6 @@ function showConfirm(options) {
var message = '';
if(options.title) message += options.title + '\n\n';
if(options.html) {
// HTML 태그 제거
message += options.html.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '');
} else if(options.text) {
message += options.text;
@@ -34,13 +33,91 @@ function showConfirm(options) {
}
}
$(function(){
// 임시 ID 생성
function generateTempId() {
return -Math.floor(Math.random() * 1000000000);
}
// 반제품 E-BOM 하위 구조 조회 (동기)
function fetchEbomSubTree(partNo) {
var result = null;
$.ajax({
url: '/partMng/getEbomSubTreeByPartNo.do',
method: 'POST',
data: { partNo: partNo },
dataType: 'json',
async: false,
success: function(data) { result = data; },
error: function(xhr, status, error) { console.error("E-BOM 하위 구조 조회 오류:", error); }
});
return result;
}
// 반제품 E-BOM 하위 구조를 flat list로 변환
function buildSubTreeFlatList(subTree, parentObjid, baseLevel) {
var flatList = [];
var originalChildToNew = {};
for(var i = 0; i < subTree.length; i++) {
var item = subTree[i];
var itemLevel = parseInt(item.LEVEL || item.level || 1);
var newChildObjid = generateTempId();
var originalChildObjid = item.CHILD_OBJID || item.child_objid || '';
if(originalChildObjid) originalChildToNew[originalChildObjid] = newChildObjid;
flatList.push({
OBJID: generateTempId(),
CHILD_OBJID: newChildObjid,
PARENT_OBJID: parentObjid,
LEVEL: baseLevel + itemLevel,
PART_OBJID: item.PART_OBJID || item.part_objid || '',
PART_NO: item.PART_NO || item.part_no || '',
PART_NAME: item.PART_NAME || item.part_name || '',
QTY: parseInt(item.QTY_TEMP || item.qty_temp || item.QTY || item.qty || 1),
ITEM_QTY: parseInt(item.ITEM_QTY || item.item_qty || item.QTY || item.qty || 1),
QTY_TEMP: parseInt(item.QTY_TEMP || item.qty_temp || item.QTY || item.qty || 1),
SPEC: item.SPEC || item.spec || '',
REVISION: item.REVISION || item.revision || '',
STATUS: 'ACTIVE',
_IS_SUB_PART: true
});
}
// 2레벨 이상 항목의 부모 OBJID 재매핑
for(var i = 0; i < subTree.length; i++) {
var originalParent = subTree[i].PARENT_OBJID || subTree[i].parent_objid || '';
if(originalParent && originalChildToNew[originalParent]) {
flatList[i].PARENT_OBJID = originalChildToNew[originalParent];
}
}
return flatList;
}
// 그리드 데이터 배열의 삽입 위치 계산
function findInsertPosition(allData, parentObjid, parentLevel) {
var insertPosition = -1;
for(var i = 0; i < allData.length; i++) {
if(allData[i].CHILD_OBJID == parentObjid) {
insertPosition = i + 1;
for(var j = i + 1; j < allData.length; j++) {
var nextLevel = parseInt(allData[j].LEVEL || allData[j].level || 1);
if(nextLevel > parentLevel) {
insertPosition = j + 1;
} else {
break;
}
}
break;
}
}
return insertPosition;
}
$(function(){
$('.select2').select2();
//Part 연결
// E-BOM 파트 추가 (클라이언트 편집)
$("#moveLeft").click(function(){
// Tabulator에서 선택된 오른쪽 행 데이터 가져오기
var rightFrame = parent.frames['rightFrame'];
var rightSelectedRows = rightFrame.getSelectedRows ? rightFrame.getSelectedRows() : [];
@@ -49,377 +126,189 @@ $(function(){
return false;
}
// 왼쪽 프레임에서 선택된 행 데이터 가져오기
var leftPartNoObj = $("input[name=checkedPartNo]:checked", parent.frames['leftFrame'].document);
var leftFrame = parent.frames['leftFrame'];
var leftPartNoObj = $("input[name=checkedPartNo]:checked", leftFrame.document);
var leftPartChildObjId = null;
var parentObjid = "";
var parentLevel = 0;
var leftPartNo = null;
var leftPartNoQty = null;
var leftParentObjId = null;
var leftPartLastObjId = null;
var leftQtyParObjId = null;
var leftParentParts = "";
if(leftPartNoObj.length > 0) {
leftPartChildObjId = leftPartNoObj.val();
parentObjid = leftPartNoObj.val();
parentLevel = parseInt(leftPartNoObj.attr("data-LEVEL") || 0);
leftPartNo = leftPartNoObj.attr("data-PART_NO");
leftPartNoQty = leftPartNoObj.attr("data-PART_NO_QTY");
leftParentObjId = leftPartNoObj.attr("data-OBJID");
leftPartLastObjId = leftPartNoObj.attr("data-LAST_PART_OBJID");
leftQtyParObjId = leftPartNoObj.attr("data-PART_OBJID");
leftParentParts = leftPartNoObj.attr("data-PARENT_PARTS") || "";
}
// 같은 Part 연결한건지 체크
var isSamePart = false;
// 같은 Part 연결 방지
for(var i = 0; i < rightSelectedRows.length; i++){
var rowData = rightSelectedRows[i].getData();
var rightPartNo = rowData.PART_NO;
if(rightPartNo == leftPartNo){
if(rowData.PART_NO == leftPartNo){
showConfirm({
title: '연결 불가',
html: '오류 Part No : <strong>['+rightPartNo+']</strong><br>같은 Part No끼리 연결할 수 없습니다.',
html: '오류 Part No : <strong>['+rowData.PART_NO+']</strong><br>같은 Part No끼리 연결할 수 없습니다.',
icon: 'error'
});
isSamePart = true;
return false;
}
}
// 추가할 파트 데이터 준비 (반제품 하위 품목 포함)
var newParts = [];
var subPartCount = 0;
for(var i = 0; i < rightSelectedRows.length; i++){
var rowData = rightSelectedRows[i].getData();
var partChildObjid = generateTempId();
newParts.push({
OBJID: generateTempId(),
CHILD_OBJID: partChildObjid,
PARENT_OBJID: parentObjid,
LEVEL: parentLevel + 1,
PART_OBJID: rowData.OBJID,
PART_NO: rowData.PART_NO,
PART_NAME: rowData.PART_NAME,
QTY: 1,
ITEM_QTY: 1,
QTY_TEMP: 1,
SPEC: rowData.SPEC || '',
REVISION: rowData.REVISION || '',
STATUS: 'ACTIVE'
});
// 반제품 E-BOM 하위 구조 조회
var ebomResult = fetchEbomSubTree(rowData.PART_NO);
if(ebomResult && ebomResult.hasEbom && ebomResult.subTree && ebomResult.subTree.length > 0) {
var subParts = buildSubTreeFlatList(ebomResult.subTree, partChildObjid, parentLevel + 1);
for(var j = 0; j < subParts.length; j++) {
newParts.push(subParts[j]);
}
subPartCount += subParts.length;
}
}
// 왼쪽 그리드에 추가
if(leftFrame && leftFrame._tabulGrid) {
if(parentObjid) {
var allData = leftFrame._tabulGrid.getData();
var insertPosition = findInsertPosition(allData, parentObjid, parentLevel);
if(insertPosition >= 0) {
for(var k = newParts.length - 1; k >= 0; k--) {
allData.splice(insertPosition, 0, newParts[k]);
}
leftFrame._tabulGrid.setData(allData);
} else {
for(var k = 0; k < newParts.length; k++) {
leftFrame._tabulGrid.addData([newParts[k]], false);
}
}
} else {
for(var k = 0; k < newParts.length; k++) {
leftFrame._tabulGrid.addData([newParts[k]], false);
}
}
var message = rightSelectedRows.length + '개 파트가 추가되었습니다.';
if(subPartCount > 0) message += '\n(반제품 하위 ' + subPartCount + '개 품목 포함)';
message += '\n저장 버튼을 눌러 확정하세요.';
showConfirm({ title: '파트 추가', text: message, icon: 'success' });
}
});
// E-BOM 파트 삭제 (클라이언트 편집, 하위 품목 함께 삭제)
$("#moveRight").click(function(){
var leftFrame = parent.frames['leftFrame'];
var leftPartNoObj = $("input[name=checkedPartNo]:checked", leftFrame.document);
if(leftPartNoObj.length === 0) {
showConfirm("삭제할 파트를 선택해주세요.");
return;
}
var childObjid = leftPartNoObj.val();
var selectedLevel = parseInt(leftPartNoObj.attr("data-LEVEL") || 1);
var allData = leftFrame._tabulGrid.getData();
var deleteCount = 0;
var foundIndex = -1;
for(var i = 0; i < allData.length; i++) {
if(allData[i].CHILD_OBJID == childObjid) {
foundIndex = i;
deleteCount = 1;
for(var j = i + 1; j < allData.length; j++) {
var nextLevel = parseInt(allData[j].LEVEL || allData[j].level || 1);
if(nextLevel > selectedLevel) {
deleteCount++;
} else {
break;
}
}
break;
}
}
if(isSamePart) return false;
var confirmText = '선택한 Part를 삭제하시겠습니까?';
if(deleteCount > 1) confirmText += '\n(하위 ' + (deleteCount - 1) + '개 품목도 함께 삭제됩니다)';
confirmText += '\n\n저장 버튼을 눌러야 확정됩니다.';
// 연결하려는 part가 상위에 있는 part인지 확인
var deniedPartArr = [];
if(fnc_checkNull(leftParentParts).indexOf(",") > 0){
deniedPartArr = leftParentParts.split(",");
}
var isDeniedPart = false;
for(var i = 0; i < rightSelectedRows.length; i++){
var rowData = rightSelectedRows[i].getData();
var rightPartNo = rowData.PART_NO;
var rightPartType = rowData.PART_TYPE;
if("unique" == rightPartType){
for(var j = 0 ; j < deniedPartArr.length ; j++){
if(rightPartNo == deniedPartArr[j]){
showConfirm({
title: '연결 불가',
html: '오류 Part No : <strong>['+rightPartNo+']</strong><br>이미 상위에 등록된 Part No 입니다.',
icon: 'error'
});
isDeniedPart = true;
break;
}
}
if(isDeniedPart) break;
}
}
if(isDeniedPart) return;
// 선택된 파트의 OBJID 배열 생성
var rightCheckedArr = [];
for(var i = 0; i < rightSelectedRows.length; i++){
var rowData = rightSelectedRows[i].getData();
rightCheckedArr.push(rowData.OBJID);
}
if(fnc_checkNull(leftPartNo) == ""){
var flag = fn_checkSameTopPartNo(rightCheckedArr);
if(flag == "true"){
showConfirm({
title: '중복 등록 불가',
text: '1레벨에 같은 Part No가 중복 등록될 수 없습니다.',
icon: 'error'
});
return;
}
}
// Part 연결 확인
showConfirm({
title: 'Part 연결',
text: '선택한 Part를 연결하시겠습니까?',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '연결',
cancelButtonText: '취소',
reverseButtons: false
title: 'Part 삭제', text: confirmText, icon: 'warning',
showCancelButton: true, confirmButtonText: '삭제', cancelButtonText: '취소'
}).then(result => {
if (result.isConfirmed) {
fn_relatePartInfo(leftPartChildObjId, rightCheckedArr, leftPartNoQty, leftPartLastObjId, leftPartChildObjId, leftQtyParObjId);
if(result.isConfirmed && foundIndex >= 0) {
allData.splice(foundIndex, deleteCount);
leftFrame._tabulGrid.setData(allData);
var message = '파트가 삭제되었습니다.';
if(deleteCount > 1) message += '\n(하위 ' + (deleteCount - 1) + '개 품목 포함)';
message += '\n저장 버튼을 눌러 확정하세요.';
showConfirm({ title: '파트 삭제', text: message, icon: 'success' });
}
});
});
//end of Part 연결
//연결된 part 삭제
$("#moveRight").click(function(){
var leftPartNoObj = $("input[name=checkedPartNo]:checked", parent.frames['leftFrame'].document);
var leftPartChildObjId = leftPartNoObj.val();
var leftPartNo = $("input[name=checkedPartNo]:checked", parent.frames['leftFrame'].document).attr("data-PART_NO");
var leftParentPartNo = $("input[name=checkedPartNo]:checked", parent.frames['leftFrame'].document).attr("data-PARENT_PART_NO");
var leftParentObjId = $("input[name=checkedPartNo]:checked", parent.frames['leftFrame'].document).attr("data-PARENT_OBJID");
var leftPartLastObjId = $("input[name=checkedPartNo]:checked", parent.frames['leftFrame'].document).attr("data-LAST_PART_OBJID");
fn_deletePartRelateInfo(leftPartNoObj.val(), leftPartLastObjId, leftParentPartNo, leftParentObjId, leftPartChildObjId);
});
//end of 연결된 part 삭제
//연결된 part 변경
// E-BOM 파트 변경 (클라이언트 편집)
$("#moveChange").click(function(){
var leftPartNoList = $("input[name=checkedPartNo]:checked", parent.frames['leftFrame'].document);
var leftFrame = parent.frames['leftFrame'];
var leftPartNoList = $("input[name=checkedPartNo]:checked", leftFrame.document);
if(leftPartNoList.length === 0){
showConfirm("선택된 파트가 없습니다.");
return false;
}
if(leftPartNoList.length > 1){
showConfirm("한번에 1개의 파트만 변경가능합니다.");
return false;
}
if(leftPartNoList.length === 0) { showConfirm("선택된 파트가 없습니다."); return false; }
if(leftPartNoList.length > 1) { showConfirm("한번에 1개의 파트만 변경가능합니다."); return false; }
var leftPartNo = leftPartNoList.attr("data-PART_NO");
var leftPartObjid = leftPartNoList.attr("data-BOM_LAST_PART_OBJID");
var leftParentPartObjid = leftPartNoList.attr("data-PARENT_PART_NO");
var leftPartChildObjId = leftPartNoList.val();
var leftPartBomQtyObjId = leftPartNoList.attr("data-OBJID");
var leftChildObjid = leftPartNoList.val();
// Tabulator에서 선택된 오른쪽 행 데이터 가져오기
var rightFrame = parent.frames['rightFrame'];
var rightSelectedRows = rightFrame.getSelectedRows ? rightFrame.getSelectedRows() : [];
if(rightSelectedRows.length === 0){
showConfirm("선택된 파트가 없습니다.");
return false;
}
if(rightSelectedRows.length > 1){
showConfirm("한번에 1개의 파트만 변경가능합니다.");
return false;
}
if(rightSelectedRows.length === 0) { showConfirm("선택된 파트가 없습니다."); return false; }
if(rightSelectedRows.length > 1) { showConfirm("한번에 1개의 파트만 변경가능합니다."); return false; }
var rightRowData = rightSelectedRows[0].getData();
var rightPartNo = rightRowData.PART_NO;
var rightPartRev = rightRowData.REVISION || "";
var rightObjId = rightRowData.OBJID;
// Part 변경 확인
showConfirm({
title: 'Part 변경',
html: '선택한 Part를 변경하시겠습니까?<br><br>' +
'<strong>기존:</strong> ' + leftPartNo + '<br>' +
'<strong>변경:</strong> ' + rightPartNo,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '변경',
cancelButtonText: '취소',
reverseButtons: false
html: '선택한 Part를 변경하시겠습니까?<br><br><strong>기존:</strong> ' + leftPartNo + '<br><strong>변경:</strong> ' + rightRowData.PART_NO,
icon: 'warning', showCancelButton: true, confirmButtonText: '변경', cancelButtonText: '취소'
}).then(result => {
if (result.isConfirmed) {
fn_changeRelatePartInfo(leftPartBomQtyObjId, rightObjId, leftPartChildObjId, leftParentPartObjid, leftPartChildObjId, leftPartObjid, rightPartNo, rightPartRev);
if(result.isConfirmed && leftFrame && leftFrame._tabulGrid) {
var rows = leftFrame._tabulGrid.getRows();
for(var i = 0; i < rows.length; i++) {
if(rows[i].getData().CHILD_OBJID == leftChildObjid) {
rows[i].update({
PART_OBJID: rightRowData.OBJID,
PART_NO: rightRowData.PART_NO,
PART_NAME: rightRowData.PART_NAME,
REVISION: rightRowData.REVISION || ''
});
showConfirm({ title: '파트 변경', text: '파트가 변경되었습니다.\n저장 버튼을 눌러 확정하세요.', icon: 'success' });
break;
}
}
}
});
});
});
//1레벨에 같은 Part No가 등록되어있는지 확인.
function fn_checkSameTopPartNo(rightCheckedArr){
var result = false;
$.ajax({
url: "/partMng/checkSameTopPartNo.do",
method: 'post',
traditional: true, // 배열 파라미터 처리를 위한 옵션
data: {"OBJID":$("#objId").val(), "rightCheckedArr[]":rightCheckedArr}, // [] 추가
dataType: 'json',
async:false,
success: function(data) {
result = data.result;
}
, error: function(jqxhr, status, error){
/* alert(jqxhr.statusText + ", " + status + ", " + error);
alert(jqxhr.status);
alert(jqxhr.responseText); */
}
});
return result;
}
//end of 1레벨에 같은 Part No가 등록되어있는지 확인.
//구조 연결 해제
function fn_deletePartRelateInfo(leftObjId, leftPartLastObjId, leftParentPartNo, leftParentObjId, leftPartChildObjId){
if(leftObjId == null){
showConfirm("연결 해제할 Part를 선택해 주시기 바랍니다.");
return;
}
showConfirm({
title: 'Part 연결 해제',
text: '선택한 Part의 연결을 해제하시겠습니까?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '연결 해제',
cancelButtonText: '취소',
reverseButtons: false
}).then(result => {
if (!result.isConfirmed) return;
fn_executeDeletePartRelateInfo(leftObjId, leftPartLastObjId, leftParentPartNo, leftParentObjId, leftPartChildObjId);
});
}
// 실제 Part 연결 해제 실행 함수
function fn_executeDeletePartRelateInfo(leftObjId, leftPartLastObjId, leftParentPartNo, leftParentObjId, leftPartChildObjId){
$.ajax({
url: "/partMng/deleteStatusPartRelateInfo.do",
method: 'post',
data: {"OBJID":$("#objId").val(), "leftObjId":leftObjId, "partObjId":leftPartLastObjId, "BOM_REPORT_OBJID":$("#objId").val()
, "leftPartChildObjId":leftPartChildObjId, "leftParentPartNo":leftParentPartNo, "leftParentObjId":leftParentObjId},
dataType: 'json',
success: function(data) {
if(data.result){
$(parent.frames['leftFrame'].document.location.reload());
//$(parent.frames['rightFrame'].fn_searchPart());
// 부모 창(E-BOM 목록) 새로고침
if(window.opener && window.opener.fn_search) {
window.opener.fn_search();
}
}
}
, error: function(jqxhr, status, error){
/* alert(jqxhr.statusText + ", " + status + ", " + error);
alert(jqxhr.status);
alert(jqxhr.responseText); */
}
});
}
//end of 구조 연결 해제
//구조 연결
function fn_relatePartInfo(leftObjId, rightCheckedArr, leftPartNoQty, leftPartLastObjId, leftPartChildObjId, leftQtyParObjId){
if(typeof rightCheckedArr != "undefined" && rightCheckedArr.length == 0){
showConfirm("선택된 Part가 없습니다.");
return;
}
if(leftObjId == null){
showConfirm({
title: '1레벨 등록 확인',
html: '좌측에 선택된 Part정보가 없습니다.<br>이대로 연결하면 1레벨로 등록됩니다.<br><br>진행하시겠습니까?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '진행',
cancelButtonText: '취소',
reverseButtons: false
}).then(result => {
if (result.isConfirmed) {
fn_executeRelatePartInfo(leftObjId, rightCheckedArr, leftPartNoQty, leftPartLastObjId, leftPartChildObjId, leftQtyParObjId);
}
});
return;
}
fn_executeRelatePartInfo(leftObjId, rightCheckedArr, leftPartNoQty, leftPartLastObjId, leftPartChildObjId, leftQtyParObjId);
}
// 실제 Part 연결 실행 함수
function fn_executeRelatePartInfo(leftObjId, rightCheckedArr, leftPartNoQty, leftPartLastObjId, leftPartChildObjId, leftQtyParObjId){
$.ajax({
url: "/partMng/relatePartInfo.do",
method: 'post',
traditional: true, // 배열 파라미터 처리를 위한 옵션
data: {
"leftObjId": leftObjId,
"leftPartNoQty": leftPartNoQty,
"OBJID": $("#objId").val(),
"rightCheckedArr[]": rightCheckedArr, // Spring의 @RequestParam(value = "rightCheckedArr[]") 형식
"partObjId": leftPartLastObjId,
"BOM_REPORT_OBJID": $("#objId").val(),
"leftPartChildObjId": leftPartChildObjId,
"leftQtyParObjId": leftQtyParObjId
},
dataType: 'json',
async:false,
success: function(data) {
if(data.result){
// 왼쪽 프레임 새로고침
$(parent.frames['leftFrame'].document.location.reload());
// 오른쪽 프레임 선택 해제 (Tabulator)
var rightFrame = parent.frames['rightFrame'];
if(rightFrame.clearSelection) {
rightFrame.clearSelection();
}
// 부모 창(E-BOM 목록) 새로고침
if(window.opener && window.opener.fn_search) {
window.opener.fn_search();
}
}
}
, error: function(jqxhr, status, error){
/* alert(jqxhr.statusText + ", " + status + ", " + error);
alert(jqxhr.status);
alert(jqxhr.responseText); */
}
});
}
//end of 구조 연결
//구조 연결 변경
function fn_changeRelatePartInfo(objId,rightObjId,leftObjId,leftPartNoQty,leftPartChildObjId,leftPartObjid,rightPartNo,rightPartRev){
$.ajax({
url: "/partMng/changeRelatePartInfo.do",
method: 'post',
data: {"rightObjId":rightObjId, "OBJID":objId,"BOM_REPORT_OBJID":$("#objId").val(),
"leftObjId":leftObjId,"leftPartNoQty":leftPartNoQty,"leftPartChildObjId":leftPartChildObjId,
"leftPartObjid":leftPartObjid, "rightPartNo":rightPartNo, "rightPartRev":rightPartRev},
dataType: 'json',
async:false,
success: function(data) {
if(data.result){
// 왼쪽 프레임 새로고침
$(parent.frames['leftFrame'].document.location.reload());
// 오른쪽 프레임 검색 다시 수행
var rightFrame = parent.frames['rightFrame'];
if(rightFrame.fn_searchPart) {
rightFrame.fn_searchPart();
}
// 오른쪽 프레임 선택 해제 (Tabulator)
if(rightFrame.clearSelection) {
rightFrame.clearSelection();
}
// 부모 창(E-BOM 목록) 새로고침
if(window.opener && window.opener.fn_search) {
window.opener.fn_search();
}
}
}
, error: function(jqxhr, status, error){
/* alert(jqxhr.statusText + ", " + status + ", " + error);
alert(jqxhr.status);
alert(jqxhr.responseText); */
}
});
}
</script>
</head>
<body class="backcolor" style="border:border:1px solid #ccc;">

View File

@@ -105,7 +105,8 @@ function fn_initGrid() {
'data-LAST_PART_OBJID="' + rowData.LAST_PART_OBJID + '" ' +
'data-PARENT_OBJID="' + rowData.PARENT_OBJID + '" ' +
'data-PART_OBJID="' + rowData.PART_OBJID + '" ' +
'data-BOM_LAST_PART_OBJID="' + rowData.BOM_LAST_PART_OBJID + '">';
'data-BOM_LAST_PART_OBJID="' + rowData.BOM_LAST_PART_OBJID + '" ' +
'data-LEVEL="' + (rowData.LEVEL || rowData.level || 1) + '">';
},
}
];
@@ -297,40 +298,118 @@ function fn_initGrid() {
}
);
// 초기 데이터 로드 (클라이언트 편집 모드 지원)
var initialData = [];
$.ajax({
url: "/partMng/getStructureTreeJson.do",
method: 'GET',
data: { objId: "${info.OBJID}", bomReportObjId: "${info.OBJID}", search_type: "working" },
dataType: 'json',
async: false,
success: function(response) {
if(response && response.length > 0) {
response.forEach(function(item) {
var maxLev = item.MAX_LEVEL || 1;
for(var i = 1; i <= maxLev; i++) {
item['LEVEL_' + i] = (item.LEVEL == i) ? '*' : '';
}
initialData.push(item);
});
}
}
});
_tabulGrid = new Tabulator("#structureGrid", {
layout: "fitColumns",
height: "100%",
pagination: false,
headerSort: false, // 정렬 비활성화
headerSort: false,
movableRows: true,
columns: columns,
data: initialData,
rowFormatter: function(row) {
var data = row.getData();
$(row.getElement()).addClass('level-' + data.LEVEL);
},
ajaxURL: "/partMng/getStructureTreeJson.do",
ajaxParams: {
objId: "${info.OBJID}",
bomReportObjId: "${info.OBJID}",
search_type: "working" // adding 상태 포함해서 조회
},
ajaxResponse: function(url, params, response) {
// 서버 응답 데이터 가공
var processedData = [];
if(response && response.length > 0) {
var maxLevel = response[0].MAX_LEVEL || 1;
response.forEach(function(item) {
// Level 표시를 위한 동적 필드 생성
for(var i = 1; i <= maxLevel; i++) {
item['LEVEL_' + i] = (item.LEVEL == i) ? '*' : '';
}
processedData.push(item);
});
}
return processedData;
}
});
// 드래그 이동 시 같은 부모 내에서만 허용, 하위 품목도 함께 이동
var _lastValidData = initialData.slice();
_tabulGrid.on("rowMoved", function(row) {
var movedData = row.getData();
var movedLevel = parseInt(movedData.LEVEL || 1);
var movedParent = movedData.PARENT_OBJID || '';
var movedChildObjid = movedData.CHILD_OBJID;
var currentData = _tabulGrid.getData();
var movedIndex = -1;
for(var i = 0; i < currentData.length; i++) {
if(currentData[i].CHILD_OBJID == movedChildObjid) { movedIndex = i; break; }
}
if(movedIndex < 0) return;
var isValidMove = false;
if(movedIndex > 0) {
var prevData = currentData[movedIndex - 1];
var prevLevel = parseInt(prevData.LEVEL || 1);
if((prevLevel == movedLevel && prevData.PARENT_OBJID == movedParent) ||
(prevLevel == movedLevel - 1 && prevData.CHILD_OBJID == movedParent)) {
isValidMove = true;
}
} else {
if(!movedParent || movedParent == '') isValidMove = true;
}
if(!isValidMove) {
alert("같은 부모 내에서만 순서를 변경할 수 있습니다.");
_tabulGrid.setData(_lastValidData);
return;
}
// 이전 데이터에서 이동된 행의 하위 품목 추출
var subTree = [];
var oldData = _lastValidData;
var oldIndex = -1;
for(var i = 0; i < oldData.length; i++) {
if(oldData[i].CHILD_OBJID == movedChildObjid) { oldIndex = i; break; }
}
if(oldIndex >= 0) {
for(var j = oldIndex + 1; j < oldData.length; j++) {
if(parseInt(oldData[j].LEVEL || 1) > movedLevel) {
subTree.push(oldData[j]);
} else {
break;
}
}
}
// 하위 품목이 있으면 현재 데이터에서 제거 후 부모 뒤에 삽입
if(subTree.length > 0) {
var subChildObjids = {};
for(var k = 0; k < subTree.length; k++) {
subChildObjids[subTree[k].CHILD_OBJID] = true;
}
var filtered = [];
for(var i = 0; i < currentData.length; i++) {
if(!subChildObjids[currentData[i].CHILD_OBJID]) {
filtered.push(currentData[i]);
}
}
var newParentIndex = -1;
for(var i = 0; i < filtered.length; i++) {
if(filtered[i].CHILD_OBJID == movedChildObjid) { newParentIndex = i; break; }
}
if(newParentIndex >= 0) {
for(var k = subTree.length - 1; k >= 0; k--) {
filtered.splice(newParentIndex + 1, 0, subTree[k]);
}
}
_tabulGrid.setData(filtered);
}
_lastValidData = _tabulGrid.getData();
});
// 행 클릭 시 선택 처리 이벤트
_tabulGrid.on("rowClick", function(e, row) {
// 링크 클릭 시에는 기본 동작 유지 (라디오 버튼은 하이라이트 처리)
@@ -434,6 +513,30 @@ function getSelectedRowData() {
return selectedRowData;
}
// E-BOM 트리 데이터 수집 (저장용) - SEQ는 현재 화면 순서 기준
function getEbomTreeData() {
var allData = _tabulGrid.getData();
return allData.map(function(row, index) {
return {
bomReportObjId: "${info.OBJID}",
objid: row.OBJID || '',
parentObjid: row.PARENT_OBJID || '',
childObjid: row.CHILD_OBJID || '',
parentPartNo: row.PARENT_PART_NO || '',
partNo: row.PART_OBJID || row.PART_NO || '',
lastPartObjid: row.LAST_PART_OBJID || row.BOM_LAST_PART_OBJID || '',
qty: row.QTY || '1',
itemQty: row.ITEM_QTY || '1',
qtyTemp: row.QTY_TEMP || row.QTY || '1',
seq: index + 1,
level: row.LEVEL || 1,
status: row.STATUS || 'ACTIVE',
partNoDisplay: row.PART_NO || '',
partName: row.PART_NAME || ''
};
});
}
// 필터 적용 함수
function fn_applyFilter() {
var partNo = $("#filterPartNo").val().trim();

View File

@@ -83,9 +83,7 @@ function fn_resetFilter() {
$("#filterPartName").val("");
try {
// 프레임 구조: parent(headerFs) -> parent(main) -> frames[1](bottomFs) -> frames[0](leftFrame)
var leftFrame = parent.parent.frames[1].frames['leftFrame'];
if(leftFrame && leftFrame.fn_resetFilter) {
leftFrame.fn_resetFilter();
}
@@ -93,6 +91,71 @@ function fn_resetFilter() {
console.error("Error accessing leftFrame:", e);
}
}
// E-BOM 저장
function fn_saveEbom() {
try {
var leftFrame = parent.parent.frames[1].frames['leftFrame'];
if(!leftFrame || !leftFrame.getEbomTreeData) {
alert("E-BOM 데이터를 가져올 수 없습니다.");
return;
}
var ebomData = leftFrame.getEbomTreeData();
if(!ebomData || ebomData.length === 0) {
alert("저장할 E-BOM 데이터가 없습니다.");
return;
}
if(!confirm("E-BOM을 저장하시겠습니까?")) return;
$.ajax({
url: "/partMng/saveEbom.do",
method: 'POST',
data: JSON.stringify({
bomReportObjId: ebomData[0].bomReportObjId,
ebomData: ebomData
}),
contentType: 'application/json',
dataType: 'json',
success: function(data) {
if(data && data.result === "success") {
alert("E-BOM이 저장되었습니다.");
// 왼쪽 프레임 데이터 리로드
leftFrame.location.reload();
// 부모 창 새로고침
if(window.opener && window.opener.fn_search) {
window.opener.fn_search();
}
} else {
alert("E-BOM 저장에 실패했습니다: " + (data.message || ""));
}
},
error: function(xhr, status, error) {
console.error("E-BOM 저장 오류:", error);
alert("E-BOM 저장 중 오류가 발생했습니다.");
}
});
} catch(e) {
console.error("E-BOM 저장 오류:", e);
alert("E-BOM 저장 중 오류가 발생했습니다.");
}
}
// 창 닫기
function fn_closeEbom() {
if(confirm("저장하지 않은 변경사항은 사라집니다. 닫으시겠습니까?")) {
try {
if(window.top && window.top.opener) {
window.top.close();
} else {
window.top.close();
}
} catch(e) {
window.top.close();
}
}
}
</script>
</head>
<body class="backcolor">
@@ -125,10 +188,12 @@ function fn_resetFilter() {
<input type="text" id="filterPartName" name="filterPartName" value="${partName}">
</td>
<td>
<button type="button" class="plm_btns" onclick="fn_applyFilter()">검색</button>
<button type="button" class="plm_btns" onclick="fn_resetFilter()">초기화</button>
</td>
<td>
<button type="button" class="plm_btns" onclick="fn_applyFilter()">검색</button>
<button type="button" class="plm_btns" onclick="fn_resetFilter()">초기화</button>
<button type="button" class="plm_btns" onclick="fn_saveEbom()" style="background-color:#4CAF50; color:white; margin-left:20px;">저장</button>
<button type="button" class="plm_btns" onclick="fn_closeEbom()">닫기</button>
</td>
</tr>
</table>
</div>

View File

@@ -191,6 +191,11 @@ $(function(){
fn_closeWindow();
});
// 최상위 제품 변경 버튼 클릭
$("#btnChangeTopProduct").click(function(){
fn_changeTopProduct();
});
/* 주석처리: 가공납기/연삭납기 일괄 적용 버튼 이벤트
// 일괄 적용 버튼 클릭
$("#btnApplyBulkDeadline").click(function(){
@@ -394,13 +399,23 @@ function fn_saveMbom() {
}
});
// M-BOM 품번: 1) 최상위 제품 변경 시 수동 설정된 값 2) 기존 M-BOM 품번 3) 빈값(신규 자동 생성)
var manualMbomPartNo = $("#search_mbom_part_no").val().trim();
var mbomPartNo = "";
if(existingMbom) {
mbomPartNo = existingMbom.MBOM_NO;
}
if(manualMbomPartNo && manualMbomPartNo !== existingMbom?.MBOM_NO) {
mbomPartNo = manualMbomPartNo;
}
var saveData = {
projectObjId: projectObjId, // PROJECT_MGMT의 OBJID
projectObjId: projectObjId,
mbomData: mbomData,
partNo: $("#search_part_no").val().trim(),
partName: $("#search_part_name").val().trim(),
mbomPartNo: existingMbom ? existingMbom.MBOM_NO : "", // 기존 M-BOM 품번 사용
isUpdate: existingMbom !== null // 기존 M-BOM이 있으면 업데이트
mbomPartNo: mbomPartNo,
isUpdate: existingMbom !== null
};
console.log("저장할 데이터:", saveData);
@@ -962,6 +977,7 @@ function compareItemFields(before, after) {
</td>
<td style="width: 40px;"></td>
<td >
<input type="button" value="최상위 제품 변경" class="plm_btns" id="btnChangeTopProduct" style="background-color:#FF9800; color:white;">
<input type="button" value="이력보기" class="plm_btns" id="btnHistory">
<input type="button" value="저장" class="plm_btns" id="btnSave">
<input type="button" value="닫기" class="plm_btns" id="btnClose">
@@ -990,7 +1006,96 @@ function compareItemFields(before, after) {
// ==================== M-BOM 전용 파트 추가/삭제/변경 함수 ====================
// M-BOM 파트 추가 (그리드에만 반영)
// 반제품 E-BOM 하위 구조를 flat list로 변환 (재귀)
function buildSubTreeFlatList(subTree, parentObjid, baseLevel) {
var flatList = [];
for(var i = 0; i < subTree.length; i++) {
var item = subTree[i];
var itemLevel = parseInt(item.LEVEL || item.level || 1);
var newChildObjid = generateTempId();
flatList.push({
OBJID: generateTempId(),
CHILD_OBJID: newChildObjid,
PARENT_OBJID: parentObjid,
LEVEL: baseLevel + itemLevel,
PART_OBJID: item.PART_OBJID || item.part_objid || '',
PART_NO: item.PART_NO || item.part_no || '',
PART_NAME: item.PART_NAME || item.part_name || '',
QTY: parseInt(item.QTY_TEMP || item.qty_temp || item.QTY || item.qty || 1),
ITEM_QTY: parseInt(item.ITEM_QTY || item.item_qty || item.QTY || item.qty || 1),
QTY_TEMP: parseInt(item.QTY_TEMP || item.qty_temp || item.QTY || item.qty || 1),
UNIT: item.UNIT_TITLE || item.unit_title || '',
SPEC: item.SPEC || item.spec || '',
REVISION: item.REVISION || item.revision || '',
SUPPLY_TYPE: '사급',
STATUS: 'ACTIVE',
_IS_SUB_PART: true
});
}
// E-BOM 트리의 PARENT_OBJID 재매핑 (CHILD_OBJID 기준)
// 원본 E-BOM의 parent-child 관계를 새 ID로 매핑
var originalChildToNew = {};
for(var i = 0; i < subTree.length; i++) {
var originalChildObjid = subTree[i].CHILD_OBJID || subTree[i].child_objid || '';
if(originalChildObjid) {
originalChildToNew[originalChildObjid] = flatList[i].CHILD_OBJID;
}
}
// 2레벨 이상의 항목은 부모 OBJID를 재매핑
for(var i = 0; i < subTree.length; i++) {
var originalParent = subTree[i].PARENT_OBJID || subTree[i].parent_objid || '';
if(originalParent && originalChildToNew[originalParent]) {
flatList[i].PARENT_OBJID = originalChildToNew[originalParent];
}
// 1레벨 항목은 이미 parentObjid로 설정됨
}
return flatList;
}
// 반제품 E-BOM 하위 구조 조회 (동기)
function fetchEbomSubTree(partNo) {
var result = null;
$.ajax({
url: '/partMng/getEbomSubTreeByPartNo.do',
method: 'POST',
data: { partNo: partNo },
dataType: 'json',
async: false,
success: function(data) {
result = data;
},
error: function(xhr, status, error) {
console.error("E-BOM 하위 구조 조회 오류:", error);
}
});
return result;
}
// 그리드 데이터 배열의 삽입 위치 계산 (부모의 마지막 하위 항목 다음)
function findInsertPosition(allData, parentObjid, parentLevel) {
var insertPosition = -1;
for(var i = 0; i < allData.length; i++) {
if(allData[i].CHILD_OBJID == parentObjid) {
insertPosition = i + 1;
for(var j = i + 1; j < allData.length; j++) {
var rowLevel = allData[j].LEVEL || 1;
if(rowLevel > parentLevel) {
insertPosition = j + 1;
} else {
break;
}
}
break;
}
}
return insertPosition;
}
// M-BOM 파트 추가 (그리드에만 반영, 반제품은 하위 품목 함께 추가)
function fn_mbomAddPart() {
var rightFrame = parent.frames[1].frames['rightFrame'];
var rightSelectedRows = rightFrame.getSelectedRows ? rightFrame.getSelectedRows() : [];
@@ -1009,23 +1114,19 @@ function fn_mbomAddPart() {
if(leftPartNoObj.length > 0) {
parentObjid = leftPartNoObj.attr("data-CHILD_OBJID");
parentLevel = parseInt(leftPartNoObj.attr("data-LEVEL") || 0);
console.log("부모 선택됨:", {
parentObjid: parentObjid,
parentLevel: parentLevel,
partNo: leftPartNoObj.attr("data-PART_NO")
});
} else {
console.log("부모 선택 안됨 - 최상위 레벨에 추가");
}
// 추가할 파트 데이터 준비
// 선택된 각 파트에 대해 반제품 여부 확인 후 추가할 데이터 준비
var newParts = [];
var subPartCount = 0;
for(var i = 0; i < rightSelectedRows.length; i++){
var rowData = rightSelectedRows[i].getData();
var partChildObjid = generateTempId();
var newPart = {
OBJID: generateTempId(),
CHILD_OBJID: generateTempId(),
CHILD_OBJID: partChildObjid,
PARENT_OBJID: parentObjid,
LEVEL: parentLevel + 1,
PART_OBJID: rowData.OBJID,
@@ -1040,80 +1141,59 @@ function fn_mbomAddPart() {
SUPPLY_TYPE: '사급',
STATUS: 'ACTIVE'
};
console.log("추가할 파트:", {
partNo: newPart.PART_NO,
parentObjid: newPart.PARENT_OBJID,
level: newPart.LEVEL
});
newParts.push(newPart);
// 반제품 E-BOM 하위 구조 조회
console.log("파트 추가 - PART_NO:", rowData.PART_NO, "OBJID:", rowData.OBJID, "PART_TYPE:", rowData.PART_TYPE, "PART_TYPE_TITLE:", rowData.PART_TYPE_TITLE);
var ebomResult = fetchEbomSubTree(rowData.PART_NO);
console.log("E-BOM 조회 결과:", JSON.stringify(ebomResult));
if(ebomResult && ebomResult.hasEbom && ebomResult.subTree && ebomResult.subTree.length > 0) {
var subParts = buildSubTreeFlatList(ebomResult.subTree, partChildObjid, parentLevel + 1);
for(var j = 0; j < subParts.length; j++) {
newParts.push(subParts[j]);
}
subPartCount += subParts.length;
console.log("반제품 [" + rowData.PART_NO + "] 하위 " + subParts.length + "개 품목 함께 추가");
}
}
// 왼쪽 프레임의 Tabulator에 추가
if(leftFrame && leftFrame._tabulGrid) {
if(parentObjid) {
// 부모가 선택된 경우: 데이터 배열에 직접 삽입
var allData = leftFrame._tabulGrid.getData();
var insertPosition = -1;
var insertPosition = findInsertPosition(allData, parentObjid, parentLevel);
// 부모 행 찾기
for(var i = 0; i < allData.length; i++) {
if(allData[i].CHILD_OBJID == parentObjid) {
insertPosition = i + 1;
// 부모의 모든 하위 항목(자식, 손자, 증손자 등)을 건너뛰기
for(var j = i + 1; j < allData.length; j++) {
var rowLevel = allData[j].LEVEL || 1;
if(rowLevel > parentLevel) {
// 부모보다 하위 레벨이면 계속 건너뛰기
insertPosition = j + 1;
} else {
// 같은 레벨이거나 상위 레벨이 나오면 중단
break;
}
}
console.log("부모 레벨:", parentLevel, "/ 삽입 위치:", insertPosition);
break;
}
}
// 삽입 위치에 추가
if(insertPosition >= 0) {
console.log("부모 찾음! 삽입 위치:", insertPosition);
// 배열에 직접 삽입
for(var i = newParts.length - 1; i >= 0; i--) {
allData.splice(insertPosition, 0, newParts[i]);
}
// 전체 데이터 다시 설정
leftFrame._tabulGrid.setData(allData);
} else {
console.log("부모를 못 찾음 - 맨 밑에 추가");
// 부모를 못 찾으면 맨 밑에 추가
for(var i = 0; i < newParts.length; i++) {
leftFrame._tabulGrid.addData([newParts[i]], false);
}
}
} else {
// 부모가 선택되지 않은 경우: 최상위 레벨 맨 밑에 추가
for(var i = 0; i < newParts.length; i++) {
leftFrame._tabulGrid.addData([newParts[i]], false);
}
}
var message = rightSelectedRows.length + '개 파트가 추가되었습니다.';
if(subPartCount > 0) {
message += '\n(반제품 하위 ' + subPartCount + '개 품목 포함)';
}
message += '\n저장 버튼을 눌러 확정하세요.';
showConfirm({
title: '파트 추가',
text: newParts.length + '개 파트가 추가되었습니다.\n저장 버튼을 눌러 확정하세요.',
text: message,
icon: 'success'
});
}
}
// M-BOM 파트 삭제 (그리드에서만 제거)
// M-BOM 파트 삭제 (그리드에서만 제거, 하위 품목 함께 삭제)
function fn_mbomDeletePart() {
var leftFrame = parent.frames[1].frames['leftFrame'];
var leftPartNoObj = $("input[name=checkedPartNo]:checked", leftFrame.document);
@@ -1124,10 +1204,41 @@ function fn_mbomDeletePart() {
}
var childObjid = leftPartNoObj.val();
var selectedLevel = parseInt(leftPartNoObj.attr("data-LEVEL") || 1);
// 하위 품목 개수 미리 계산
var allData = leftFrame._tabulGrid.getData();
var deleteCount = 0;
var foundIndex = -1;
console.log("삭제 대상 childObjid:", childObjid, "selectedLevel:", selectedLevel);
for(var i = 0; i < allData.length; i++) {
if(String(allData[i].CHILD_OBJID) == String(childObjid)) {
foundIndex = i;
deleteCount = 1;
// 하위 품목(레벨이 더 높은 연속된 행) 카운트
for(var j = i + 1; j < allData.length; j++) {
var nextLevel = parseInt(allData[j].LEVEL) || 1;
console.log(" 비교 j=" + j + ", PART_NO=" + allData[j].PART_NO + ", LEVEL=" + allData[j].LEVEL + ", parsed=" + nextLevel + " > " + selectedLevel + " = " + (nextLevel > selectedLevel));
if(nextLevel > selectedLevel) {
deleteCount++;
} else {
break;
}
}
break;
}
}
console.log("삭제 예정 개수:", deleteCount, "foundIndex:", foundIndex);
var confirmText = '선택한 Part를 삭제하시겠습니까?';
if(deleteCount > 1) {
confirmText += '\n(하위 ' + (deleteCount - 1) + '개 품목도 함께 삭제됩니다)';
}
confirmText += '\n\n저장 버튼을 눌러야 확정됩니다.';
showConfirm({
title: 'Part 삭제',
text: '선택한 Part를 삭제하시겠습니까?\n(저장 버튼을 눌러야 확정됩니다)',
text: confirmText,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
@@ -1137,21 +1248,22 @@ function fn_mbomDeletePart() {
reverseButtons: false
}).then(result => {
if (result.isConfirmed) {
// Tabulator에서 해당 행 찾아서 삭제
if(leftFrame && leftFrame._tabulGrid) {
var rows = leftFrame._tabulGrid.getRows();
for(var i = 0; i < rows.length; i++) {
var rowData = rows[i].getData();
if(rowData.CHILD_OBJID == childObjid) {
rows[i].delete();
showConfirm({
title: '파트 삭제',
text: '파트가 삭제되었습니다.\n저장 버튼을 눌러 확정하세요.',
icon: 'success'
});
break;
}
if(leftFrame && leftFrame._tabulGrid && foundIndex >= 0) {
// 선택한 행 + 하위 품목 일괄 삭제
allData.splice(foundIndex, deleteCount);
leftFrame._tabulGrid.setData(allData);
var message = '파트가 삭제되었습니다.';
if(deleteCount > 1) {
message += '\n(하위 ' + (deleteCount - 1) + '개 품목 포함)';
}
message += '\n저장 버튼을 눌러 확정하세요.';
showConfirm({
title: '파트 삭제',
text: message,
icon: 'success'
});
}
}
});
@@ -1238,6 +1350,89 @@ function generateTempId() {
return -Math.floor(Math.random() * 1000000000);
}
// M-BOM 최상위 제품 변경 (선택한 반제품을 최상위로 승격)
function fn_changeTopProduct() {
var leftFrame = parent.frames[1].frames['leftFrame'];
var leftPartNoObj = $("input[name=checkedPartNo]:checked", leftFrame.document);
if(leftPartNoObj.length === 0) {
showConfirm("최상위로 설정할 반제품을 왼쪽 트리에서 선택해주세요.");
return;
}
var selectedPartNo = leftPartNoObj.attr("data-PART_NO");
var selectedPartName = leftPartNoObj.attr("data-PART_NAME") || '';
var selectedLevel = parseInt(leftPartNoObj.attr("data-LEVEL") || 1);
if(!selectedPartNo) {
showConfirm("품번 정보를 가져올 수 없습니다.");
return;
}
// 반제품 E-BOM 하위 구조 확인
var ebomResult = fetchEbomSubTree(selectedPartNo);
showConfirm({
title: '최상위 제품 변경',
html: '선택한 품목을 최상위 제품으로 변경하시겠습니까?<br><br>' +
'<strong>품번:</strong> ' + selectedPartNo + '<br>' +
'<strong>품명:</strong> ' + selectedPartName + '<br>' +
(ebomResult && ebomResult.hasEbom ? '<strong>하위 품목:</strong> ' + ebomResult.subTree.length + '개' : '<strong>하위 품목:</strong> 없음') +
'<br><br>기존 M-BOM 트리는 교체되고, M-BOM 품번도 변경됩니다.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '변경',
cancelButtonText: '취소'
}).then(result => {
if(!result.isConfirmed) return;
// 새로운 M-BOM 트리 구성
var newTreeData = [];
if(ebomResult && ebomResult.hasEbom && ebomResult.subTree && ebomResult.subTree.length > 0) {
// E-BOM 하위 구조를 M-BOM 트리로 변환
for(var i = 0; i < ebomResult.subTree.length; i++) {
var item = ebomResult.subTree[i];
var itemLevel = parseInt(item.LEVEL || item.level || 1);
newTreeData.push({
OBJID: generateTempId(),
CHILD_OBJID: item.CHILD_OBJID || item.child_objid || generateTempId(),
PARENT_OBJID: item.PARENT_OBJID || item.parent_objid || '',
LEVEL: itemLevel,
PART_OBJID: item.PART_OBJID || item.part_objid || '',
PART_NO: item.PART_NO || item.part_no || '',
PART_NAME: item.PART_NAME || item.part_name || '',
QTY: parseInt(item.QTY_TEMP || item.qty_temp || item.QTY || item.qty || 1),
ITEM_QTY: parseInt(item.ITEM_QTY || item.item_qty || 1),
QTY_TEMP: parseInt(item.QTY_TEMP || item.qty_temp || item.QTY || item.qty || 1),
UNIT: item.UNIT_TITLE || item.unit_title || '',
SPEC: item.SPEC || item.spec || '',
REVISION: item.REVISION || item.revision || '',
SUPPLY_TYPE: '사급',
STATUS: 'ACTIVE'
});
}
}
// 왼쪽 트리에 새 데이터 설정
if(leftFrame && leftFrame._tabulGrid) {
leftFrame._tabulGrid.setData(newTreeData);
}
// M-BOM 품번을 변경된 최상위 제품의 품번으로 설정
$("#search_mbom_part_no").val(selectedPartNo);
showConfirm({
title: '최상위 제품 변경 완료',
text: '최상위 제품이 [' + selectedPartNo + ']로 변경되었습니다.\n' +
'M-BOM 품번도 [' + selectedPartNo + ']로 설정되었습니다.\n' +
'저장 버튼을 눌러 확정하세요.',
icon: 'success'
});
});
}
// 창 닫기 함수 (프레임 구조 고려)
function fn_closeWindow() {
try {

View File

@@ -1001,6 +1001,7 @@ function fn_initGrid() {
height: "calc(100vh - 70px)",
columns: columns,
data: bomTreeData,
movableRows: true,
rowFormatter: function(row) {
var data = row.getData();
var level = data.LEVEL || 1;
@@ -1008,6 +1009,87 @@ function fn_initGrid() {
}
});
// 초기 유효 데이터 저장
var _lastValidData = bomTreeData.slice();
// 드래그 이동 후 같은 부모 내 이동만 허용, 하위 품목도 함께 이동
_tabulGrid.on("rowMoved", function(row) {
var movedData = row.getData();
var movedLevel = parseInt(movedData.LEVEL || 1);
var movedParent = movedData.PARENT_OBJID || '';
var movedChildObjid = movedData.CHILD_OBJID;
var currentData = _tabulGrid.getData();
var movedIndex = -1;
for(var i = 0; i < currentData.length; i++) {
if(currentData[i].CHILD_OBJID == movedChildObjid) { movedIndex = i; break; }
}
if(movedIndex < 0) return;
// 이동 위치 검증
var isValidMove = false;
if(movedIndex > 0) {
var prevData = currentData[movedIndex - 1];
var prevLevel = parseInt(prevData.LEVEL || 1);
if((prevLevel == movedLevel && prevData.PARENT_OBJID == movedParent) ||
(prevLevel == movedLevel - 1 && prevData.CHILD_OBJID == movedParent)) {
isValidMove = true;
}
} else {
if(!movedParent || movedParent == '') isValidMove = true;
}
if(!isValidMove) {
alert("같은 부모 내에서만 순서를 변경할 수 있습니다.");
_tabulGrid.setData(_lastValidData);
return;
}
// 이전 데이터에서 이동된 행의 하위 품목 추출
var subTree = [];
var oldData = _lastValidData;
var oldIndex = -1;
for(var i = 0; i < oldData.length; i++) {
if(oldData[i].CHILD_OBJID == movedChildObjid) { oldIndex = i; break; }
}
if(oldIndex >= 0) {
for(var j = oldIndex + 1; j < oldData.length; j++) {
if(parseInt(oldData[j].LEVEL || 1) > movedLevel) {
subTree.push(oldData[j]);
} else {
break;
}
}
}
// 하위 품목이 있으면 현재 데이터에서 제거 후 부모 뒤에 삽입
if(subTree.length > 0) {
var subChildObjids = {};
for(var k = 0; k < subTree.length; k++) {
subChildObjids[subTree[k].CHILD_OBJID] = true;
}
var filtered = [];
for(var i = 0; i < currentData.length; i++) {
if(!subChildObjids[currentData[i].CHILD_OBJID]) {
filtered.push(currentData[i]);
}
}
// 부모 위치 다시 찾기
var newParentIndex = -1;
for(var i = 0; i < filtered.length; i++) {
if(filtered[i].CHILD_OBJID == movedChildObjid) { newParentIndex = i; break; }
}
if(newParentIndex >= 0) {
for(var k = subTree.length - 1; k >= 0; k--) {
filtered.splice(newParentIndex + 1, 0, subTree[k]);
}
}
_tabulGrid.setData(filtered);
}
_lastValidData = _tabulGrid.getData();
});
// 행 클릭 시 선택 처리 이벤트
_tabulGrid.on("rowClick", function(e, row) {
// 링크 클릭 시에는 기본 동작 유지
@@ -1174,12 +1256,13 @@ function getMbomTreeData() {
}
// 데이터 구조 변환 (MBOM_DETAIL 테이블에 필요한 모든 필드 포함)
var mbomData = allData.map(function(row) {
// SEQ는 현재 화면 순서(인덱스) 기준으로 재계산
var mbomData = allData.map(function(row, index) {
return {
// BOM 구조 정보
parentObjid: row.PARENT_OBJID,
childObjid: row.CHILD_OBJID,
seq: row.SEQ,
seq: index + 1,
level: row.LEVEL,
// 품목 정보

View File

@@ -1223,6 +1223,58 @@ public class PartMngController {
return bomTreeList != null ? bomTreeList : new ArrayList<Map>();
}
/**
* 품번 기준 E-BOM 하위 구조 조회 (반제품 추가 시 하위 품목 연동용)
* 품번에 해당하는 E-BOM이 있으면 하위 트리를 반환, 없으면 빈 리스트 반환
*/
@RequestMapping("/partMng/getEbomSubTreeByPartNo.do")
@ResponseBody
public Map<String, Object> getEbomSubTreeByPartNo(HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
Map<String, Object> result = new HashMap<>();
try {
String partNo = CommonUtils.checkNull((String)paramMap.get("partNo"));
if(partNo.isEmpty()) {
result.put("hasEbom", false);
result.put("subTree", new ArrayList<>());
return result;
}
// 1차: 품번으로 단독 E-BOM 존재 여부 확인 (PART_BOM_REPORT)
Map bomReport = partMngService.getBomObjIdByPartNo(partNo);
if(bomReport != null && !bomReport.isEmpty()) {
String bomReportObjId = CommonUtils.checkNull(bomReport.get("OBJID"));
if(bomReportObjId.isEmpty()) {
bomReportObjId = CommonUtils.checkNull(bomReport.get("objid"));
}
Map<String, Object> treeParam = new HashMap<>();
treeParam.put("bomReportObjId", bomReportObjId);
treeParam.put("search_type", "working");
List subTree = partMngService.getBOMPartTreeListSimple(treeParam);
result.put("hasEbom", true);
result.put("bomReportObjId", bomReportObjId);
result.put("subTree", subTree != null ? subTree : new ArrayList<>());
return result;
}
// 2차 fallback: 다른 E-BOM 안에서 해당 품번이 하위 구조를 가진 경우 조회
List subTreeFallback = partMngService.findEbomSubTreeForPart(partNo);
if(subTreeFallback != null && !subTreeFallback.isEmpty()) {
result.put("hasEbom", true);
result.put("subTree", subTreeFallback);
return result;
}
result.put("hasEbom", false);
result.put("subTree", new ArrayList<>());
} catch(Exception e) {
e.printStackTrace();
result.put("hasEbom", false);
result.put("subTree", new ArrayList<>());
}
return result;
}
@RequestMapping("/partMng/structurePopupCenter.do")
public String structurePopupCenter(HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
@@ -1250,6 +1302,44 @@ public class PartMngController {
return "/partMng/structurePopupCenter";
}
/**
* E-BOM 일괄 저장 (클라이언트 편집 후 저장)
*/
@RequestMapping("/partMng/saveEbom.do")
@ResponseBody
public Map<String, Object> saveEbom(HttpServletRequest request, @RequestBody Map<String, Object> paramMap) {
Map<String, Object> result = new HashMap<>();
try {
String bomReportObjId = CommonUtils.checkNull((String)paramMap.get("bomReportObjId"));
List<Map<String, Object>> ebomData = (List<Map<String, Object>>) paramMap.get("ebomData");
if(bomReportObjId.isEmpty() || ebomData == null || ebomData.isEmpty()) {
result.put("result", "fail");
result.put("message", "저장할 데이터가 없습니다.");
return result;
}
HttpSession session = request.getSession();
PersonBean person = (PersonBean) session.getAttribute(Constants.PERSON_BEAN);
Map info = person.getLoginInfo();
String userId = CommonUtils.checkNull(info.get("userId"));
boolean saved = partMngService.saveEbomBatch(bomReportObjId, ebomData, userId);
if(saved) {
result.put("result", "success");
} else {
result.put("result", "fail");
result.put("message", "저장에 실패했습니다.");
}
} catch(Exception e) {
e.printStackTrace();
result.put("result", "fail");
result.put("message", e.getMessage());
}
return result;
}
/**
* 구조등록 우측 프레임
* @param request

View File

@@ -19,7 +19,7 @@
A.SEQ,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE
FROM
BOM_PART_QTY A
@@ -53,7 +53,7 @@
B.SEQ,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH)
FROM
BOM_PART_QTY B
@@ -3322,7 +3322,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
(SELECT PART_NO FROM PART_MNG P WHERE 1=1 AND P.OBJID::varchar = A.PARENT_PART_NO) AS PARENT_PART_MNG_NO,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE
FROM
BOM_PART_QTY A
@@ -3361,7 +3361,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
(SELECT PART_NO FROM PART_MNG P WHERE 1=1 AND P.OBJID::varchar = B.PARENT_PART_NO) AS PARENT_PART_MNG_NO,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH)
FROM
BOM_PART_QTY B
@@ -3949,6 +3949,122 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
</insert>
<!-- E-BOM 내에서 특정 품번이 하위 품목을 가진 채 존재하는 BOM 찾기 (fallback용) -->
<select id="findPartInEbomWithChildren" parameterType="map" resultType="map">
SELECT BPQ.BOM_REPORT_OBJID, BPQ.CHILD_OBJID, BPQ.PART_NO AS PART_OBJID
FROM BOM_PART_QTY BPQ
INNER JOIN PART_MNG PM ON PM.OBJID::VARCHAR = BPQ.PART_NO AND PM.IS_LAST = '1'
WHERE PM.PART_NO = #{partNo}
AND (BPQ.STATUS NOT IN ('deleting', 'deleted') OR BPQ.STATUS IS NULL)
AND EXISTS (
SELECT 1 FROM BOM_PART_QTY SUB
WHERE SUB.PARENT_OBJID = BPQ.CHILD_OBJID
AND SUB.BOM_REPORT_OBJID = BPQ.BOM_REPORT_OBJID
AND (SUB.STATUS NOT IN ('deleting', 'deleted') OR SUB.STATUS IS NULL)
)
ORDER BY BPQ.REGDATE DESC
LIMIT 1
</select>
<!-- 특정 CHILD_OBJID 아래의 하위 트리 조회 (반제품 하위 구조 fallback용) -->
<select id="getSubTreeByParentChildObjid" parameterType="map" resultType="map">
WITH RECURSIVE sub_tree(
BOM_REPORT_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
PARENT_PART_NO, PART_NO, LAST_PART_OBJID,
QTY, ITEM_QTY, QTY_TEMP, SEQ, STATUS, LEV
) AS (
SELECT A.BOM_REPORT_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
A.PARENT_PART_NO, A.PART_NO, A.LAST_PART_OBJID,
A.QTY, A.ITEM_QTY, A.QTY_TEMP, A.SEQ, A.STATUS, 1
FROM BOM_PART_QTY A
WHERE A.PARENT_OBJID = #{parentChildObjid}
AND A.BOM_REPORT_OBJID = #{bomReportObjId}
AND (A.STATUS NOT IN ('deleting', 'deleted') OR A.STATUS IS NULL)
UNION ALL
SELECT B.BOM_REPORT_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
B.PARENT_PART_NO, B.PART_NO, B.LAST_PART_OBJID,
B.QTY, B.ITEM_QTY, B.QTY_TEMP, B.SEQ, B.STATUS, S.LEV + 1
FROM BOM_PART_QTY B
JOIN sub_tree S ON B.PARENT_OBJID = S.CHILD_OBJID
AND B.BOM_REPORT_OBJID = S.BOM_REPORT_OBJID
WHERE (B.STATUS NOT IN ('deleting', 'deleted') OR B.STATUS IS NULL)
)
SELECT
S.BOM_REPORT_OBJID
,S.OBJID
,S.PARENT_OBJID
,S.CHILD_OBJID
,S.PARENT_PART_NO
,S.PART_NO AS PART_OBJID
,S.LAST_PART_OBJID AS BOM_LAST_PART_OBJID
,P.OBJID AS LAST_PART_OBJID
,P.PART_NO
,P.PART_NAME
,S.QTY
,S.ITEM_QTY
,(CASE WHEN S.STATUS = 'deploy' THEN S.QTY
WHEN S.STATUS = 'beforeEdit' THEN S.QTY
WHEN S.STATUS != 'editing' AND (S.QTY_TEMP IS NULL OR S.QTY_TEMP = '') THEN S.QTY
ELSE COALESCE(S.QTY_TEMP, S.QTY) END) AS QTY_TEMP
,S.LEV AS LEVEL
,(SELECT COUNT(*) FROM BOM_PART_QTY WHERE PARENT_OBJID = S.CHILD_OBJID) AS SUB_PART_CNT
,S.SEQ
,S.STATUS
,P.SPEC
,P.MATERIAL
,P.REVISION
,(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.PART_TYPE) AS PART_TYPE_TITLE
,(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.UNIT) AS UNIT_TITLE
FROM sub_tree S
INNER JOIN PART_MNG P ON P.OBJID = COALESCE(NULLIF(S.LAST_PART_OBJID, ''), S.PART_NO)
ORDER BY S.LEV, S.SEQ
</select>
<!-- E-BOM 차분 저장: 기존 데이터 조회 -->
<select id="getExistingBomPartQty" parameterType="map" resultType="map">
SELECT CHILD_OBJID, PARENT_OBJID, PART_NO, PARENT_PART_NO, QTY, ITEM_QTY, QTY_TEMP, SEQ, STATUS
FROM BOM_PART_QTY
WHERE BOM_REPORT_OBJID = #{bomReportObjId}
</select>
<!-- E-BOM 차분 저장: CHILD_OBJID 기준 개별 삭제 -->
<delete id="deleteBomPartQtyByChildObjid" parameterType="map">
DELETE FROM BOM_PART_QTY
WHERE BOM_REPORT_OBJID = #{bomReportObjId}
AND CHILD_OBJID = #{childObjid}
</delete>
<!-- E-BOM 차분 저장: CHILD_OBJID 기준 수정 (수량, 순서, 부모 등) -->
<update id="updateBomPartQtyByChildObjid" parameterType="map">
UPDATE BOM_PART_QTY
SET PARENT_OBJID = #{PARENT_OBJID},
QTY = COALESCE(NULLIF(#{QTY}, ''), '0')::NUMERIC,
ITEM_QTY = COALESCE(NULLIF(#{ITEM_QTY}, ''), '0')::NUMERIC,
QTY_TEMP = COALESCE(NULLIF(#{QTY_TEMP}, ''), '0')::NUMERIC,
SEQ = #{SEQ}
WHERE BOM_REPORT_OBJID = #{bomReportObjId}
AND CHILD_OBJID = #{childObjid}
</update>
<!-- E-BOM 차분 저장: 신규 행 INSERT -->
<insert id="insertBomPartQtyBatch" parameterType="map">
INSERT INTO BOM_PART_QTY (
BOM_REPORT_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
PARENT_PART_NO, PART_NO, LAST_PART_OBJID,
QTY, ITEM_QTY, QTY_TEMP,
REGDATE, WRITER, SEQ, STATUS
) VALUES (
#{BOM_REPORT_OBJID}, #{OBJID}, #{PARENT_OBJID}, #{CHILD_OBJID},
#{PARENT_PART_NO}, #{PART_NO}, #{LAST_PART_OBJID},
COALESCE(NULLIF(#{QTY}, ''), '0')::NUMERIC,
COALESCE(NULLIF(#{ITEM_QTY}, ''), '0')::NUMERIC,
COALESCE(NULLIF(#{QTY_TEMP}, ''), '0')::NUMERIC,
NOW(), #{WRITER}, #{SEQ}, #{STATUS}
)
</insert>
<!-- //BOM 구조등록 -->
<insert id="relatePartInfo" parameterType="map">
INSERT INTO BOM_PART_QTY
@@ -5778,7 +5894,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
B.LENGTH,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.SEQ,
B.MAKER,
@@ -5899,7 +6015,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
B.LENGTH,
LEV + 1,
PATH||A.CHILD_OBJID::TEXT,
PATH2||A.SEQ::TEXT,
PATH2||LPAD(A.SEQ::TEXT, 10, '0'),
A.PARENT_OBJID = ANY(PATH),
A.SEQ,
B.MAKER,
@@ -6058,7 +6174,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
COALESCE(BPQ.LAST_PART_OBJID, BPQ.PART_NO) AS LAST_PART_OBJID,
1 AS LEV,
ARRAY[BPQ.CHILD_OBJID::TEXT] AS PATH,
ARRAY[BPQ.SEQ::TEXT] AS PATH2,
ARRAY[LPAD(BPQ.SEQ::TEXT, 10, '0')] AS PATH2,
0 AS LEAF
FROM BOM_PART_QTY BPQ
LEFT JOIN PART_BOM_REPORT PBR ON BPQ.BOM_REPORT_OBJID = PBR.OBJID
@@ -6109,7 +6225,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
COALESCE(BPQ.LAST_PART_OBJID, BPQ.PART_NO) AS LAST_PART_OBJID,
BT.LEV + 1,
BT.PATH || BPQ.CHILD_OBJID::TEXT,
BT.PATH2 || BPQ.SEQ::TEXT,
BT.PATH2 || LPAD(BPQ.SEQ::TEXT, 10, '0'),
0 AS LEAF
FROM BOM_PART_QTY BPQ
INNER JOIN BOM_TREE BT ON BPQ.PARENT_OBJID = BT.CHILD_OBJID

View File

@@ -1238,7 +1238,7 @@
-->
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.SEQ,
A.LAST_PART_OBJID
@@ -1337,7 +1337,7 @@
-->
LEV + 1,
PATH||A.CHILD_OBJID::TEXT,
PATH2||A.SEQ::TEXT,
PATH2||LPAD(A.SEQ::TEXT, 10, '0'),
A.PARENT_OBJID = ANY(PATH),
A.SEQ,
A.LAST_PART_OBJID
@@ -3626,7 +3626,7 @@
A.STATUS,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT,
A.SUPPLY_TYPE,
@@ -3668,7 +3668,7 @@
B.STATUS,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT,
B.SUPPLY_TYPE,
@@ -4155,7 +4155,7 @@
A.STATUS,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT,
A.SUPPLY_TYPE,
@@ -4208,7 +4208,7 @@
B.STATUS,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT,
B.SUPPLY_TYPE,
@@ -4381,7 +4381,7 @@
A.STATUS,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT,
A.WRITER,
@@ -4411,7 +4411,7 @@
B.STATUS,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT,
B.WRITER,

View File

@@ -2235,7 +2235,7 @@ WITH RECURSIVE VIEW_BOM(
A.SEQ,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE
FROM
BOM_PART_QTY A
@@ -2268,8 +2268,8 @@ WITH RECURSIVE VIEW_BOM(
B.SEQ,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
B.PARENT_OBJID = ANY(PATH)
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH)
FROM
BOM_PART_QTY B
JOIN
@@ -3312,7 +3312,7 @@ WITH RECURSIVE VIEW_BOM(
A.STATUS,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT,
A.SUPPLY_TYPE,
@@ -3375,7 +3375,7 @@ WITH RECURSIVE VIEW_BOM(
B.STATUS,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT,
B.SUPPLY_TYPE,
@@ -3691,7 +3691,7 @@ WITH RECURSIVE VIEW_BOM(
A.STATUS,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [A.SEQ::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT,
A.SUPPLY_TYPE,
@@ -3755,7 +3755,7 @@ WITH RECURSIVE VIEW_BOM(
B.STATUS,
LEV + 1,
PATH||B.CHILD_OBJID::TEXT,
PATH2||B.SEQ::TEXT,
PATH2||LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT,
B.SUPPLY_TYPE,

View File

@@ -1475,8 +1475,8 @@ public class PartMngService extends BaseService {
sqlParamMap.put("PARENT_PART_OBJID", CommonUtils.checkNull(paramMap.get("leftObjId")));
sqlParamMap.put("PARENT_PART_NO", CommonUtils.checkNull(paramMap.get("leftPartNoQty")));
sqlParamMap.put("PARENT_QTY_CHILD_OBJID", CommonUtils.checkNull(paramMap.get("leftPartChildObjId")));
sqlParamMap.put("QTY", 1);
sqlParamMap.put("QTY_TEMP", 1);
sqlParamMap.put("QTY", "1");
sqlParamMap.put("QTY_TEMP", "1");
sqlParamMap.put("STATUS", "adding");
sqlParamMap.put("WRITER", userId);
//sqlParamMap.put("bomReportObjId", CommonUtils.checkNull(paramMap.get("OBJID")));
@@ -3840,6 +3840,195 @@ public class PartMngService extends BaseService {
return resultMap;
}
/**
* 품번(String)으로 E-BOM 존재 여부 조회
*/
public Map<String, Object> getBomObjIdByPartNo(String partNo) throws Exception {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("partNo", partNo);
return getBomObjIdByPartNo(paramMap);
}
/**
* E-BOM 하위 트리 조회 (반제품 추가 시 하위 품목 연동용)
* getBOMPartTreeList와 동일하지만 HttpServletRequest 없이 직접 bomReportObjId로 조회
*/
public List getBOMPartTreeListSimple(Map<String, Object> paramMap) {
List<Map<String,Object>> resultList = new ArrayList();
List<Map<String,Object>> finalList = new ArrayList();
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession();
try {
resultList = sqlSession.selectList("partMng.getBOMTreeList", paramMap);
int maxLevel = 0;
if(null != resultList && 0 < resultList.size()) {
for(int i = 0; i < resultList.size(); i++) {
Map resultMap = (HashMap) resultList.get(i);
int resultLevel = Integer.parseInt(CommonUtils.checkNull(resultMap.get("level"), "0"));
if(maxLevel < resultLevel) {
maxLevel = resultLevel;
}
}
for(int i = 0; i < resultList.size(); i++) {
Map resultMap = (HashMap) resultList.get(i);
resultMap.put("MAX_LEVEL", maxLevel);
finalList.add(resultMap);
}
}
} catch(Exception e) {
e.printStackTrace();
} finally {
sqlSession.close();
}
return finalList;
}
/**
* E-BOM 내에서 특정 품번이 하위 구조를 가진 채 존재하는 경우 그 하위 트리를 조회
* 단독 E-BOM(PART_BOM_REPORT)이 없는 반제품도 다른 E-BOM 안에서 하위 구조를 찾아 반환
*/
public List findEbomSubTreeForPart(String partNo) {
List resultList = new ArrayList();
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession();
try {
Map<String, Object> findParam = new HashMap<>();
findParam.put("partNo", partNo);
Map<String, Object> found = (Map<String, Object>) sqlSession.selectOne("partMng.findPartInEbomWithChildren", findParam);
if(found == null || found.isEmpty()) {
return resultList;
}
String bomReportObjId = CommonUtils.checkNull(found.get("bom_report_objid"), CommonUtils.checkNull(found.get("BOM_REPORT_OBJID")));
String parentChildObjid = CommonUtils.checkNull(found.get("child_objid"), CommonUtils.checkNull(found.get("CHILD_OBJID")));
if(bomReportObjId.isEmpty() || parentChildObjid.isEmpty()) {
return resultList;
}
Map<String, Object> treeParam = new HashMap<>();
treeParam.put("bomReportObjId", bomReportObjId);
treeParam.put("parentChildObjid", parentChildObjid);
resultList = sqlSession.selectList("partMng.getSubTreeByParentChildObjid", treeParam);
if(resultList != null && resultList.size() > 0) {
int maxLevel = 0;
for(int i = 0; i < resultList.size(); i++) {
Map row = (HashMap) resultList.get(i);
int lev = Integer.parseInt(CommonUtils.checkNull(row.get("level"), "0"));
if(maxLevel < lev) maxLevel = lev;
}
for(int i = 0; i < resultList.size(); i++) {
Map row = (HashMap) resultList.get(i);
row.put("MAX_LEVEL", maxLevel);
}
}
} catch(Exception e) {
e.printStackTrace();
} finally {
sqlSession.close();
}
return resultList != null ? resultList : new ArrayList();
}
/**
* E-BOM 일괄 저장 (차분 방식: 기존 CHILD_OBJID 보존, 추가/삭제/수정 분리)
* ASSEMBLY_WBS_TASK, SALES_BOM_REPORT_PART 등이 CHILD_OBJID를 참조하므로 전체 삭제 불가
*/
public boolean saveEbomBatch(String bomReportObjId, List<Map<String, Object>> ebomData, String userId) {
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
try {
// 1. 기존 데이터 조회
Map<String, Object> queryParam = new HashMap<>();
queryParam.put("bomReportObjId", bomReportObjId);
List<Map<String, Object>> existingList = sqlSession.selectList("partMng.getExistingBomPartQty", queryParam);
// 기존 CHILD_OBJID 맵 (CHILD_OBJID -> 행 데이터)
Map<String, Map<String, Object>> existingMap = new HashMap<>();
for(Map<String, Object> row : existingList) {
String childObjid = CommonUtils.checkNull(row.get("child_objid"), CommonUtils.checkNull(row.get("CHILD_OBJID")));
if(!childObjid.isEmpty()) {
existingMap.put(childObjid, row);
}
}
// 새 데이터의 CHILD_OBJID 셋
Set<String> newChildObjids = new HashSet<>();
for(Map<String, Object> item : ebomData) {
String childObjid = CommonUtils.checkNull(item.get("childObjid"));
if(!childObjid.isEmpty()) {
newChildObjids.add(childObjid);
}
}
// 새 데이터에서 참조하는 PARENT_OBJID 셋 (삭제 보호용)
Set<String> referencedParentObjids = new HashSet<>();
for(Map<String, Object> item : ebomData) {
String parentObjid = CommonUtils.checkNull(item.get("parentObjid"));
if(!parentObjid.isEmpty()) {
referencedParentObjids.add(parentObjid);
}
}
// 2. 삭제: 기존에 있지만 새 데이터에 없는 항목 (다른 행이 참조하는 부모는 보호)
for(String existingChildObjid : existingMap.keySet()) {
if(!newChildObjids.contains(existingChildObjid)) {
if(referencedParentObjids.contains(existingChildObjid)) {
continue;
}
Map<String, Object> deleteParam = new HashMap<>();
deleteParam.put("bomReportObjId", bomReportObjId);
deleteParam.put("childObjid", existingChildObjid);
sqlSession.delete("partMng.deleteBomPartQtyByChildObjid", deleteParam);
}
}
// 3. 추가/수정
for(int i = 0; i < ebomData.size(); i++) {
Map<String, Object> item = ebomData.get(i);
String childObjid = CommonUtils.checkNull(item.get("childObjid"));
if(existingMap.containsKey(childObjid)) {
// 기존 항목 → 수정 (수량, 순서 등만 업데이트)
Map<String, Object> updateParam = new HashMap<>();
updateParam.put("bomReportObjId", bomReportObjId);
updateParam.put("childObjid", childObjid);
updateParam.put("PARENT_OBJID", CommonUtils.checkNull(item.get("parentObjid")));
updateParam.put("QTY", CommonUtils.checkNull(item.get("qty"), "1"));
updateParam.put("ITEM_QTY", CommonUtils.checkNull(item.get("itemQty"), "1"));
updateParam.put("QTY_TEMP", CommonUtils.checkNull(item.get("qtyTemp"), "1"));
updateParam.put("SEQ", i + 1);
sqlSession.update("partMng.updateBomPartQtyByChildObjid", updateParam);
} else {
// 신규 항목 → INSERT
Map<String, Object> insertParam = new HashMap<>();
insertParam.put("BOM_REPORT_OBJID", bomReportObjId);
insertParam.put("OBJID", CommonUtils.createObjId());
insertParam.put("PARENT_OBJID", CommonUtils.checkNull(item.get("parentObjid")));
insertParam.put("CHILD_OBJID", childObjid.startsWith("-") ? CommonUtils.createObjId() : childObjid);
insertParam.put("PARENT_PART_NO", CommonUtils.checkNull(item.get("parentPartNo")));
insertParam.put("PART_NO", CommonUtils.checkNull(item.get("partNo")));
insertParam.put("LAST_PART_OBJID", CommonUtils.checkNull(item.get("lastPartObjid")));
insertParam.put("QTY", CommonUtils.checkNull(item.get("qty"), "1"));
insertParam.put("ITEM_QTY", CommonUtils.checkNull(item.get("itemQty"), "1"));
insertParam.put("QTY_TEMP", CommonUtils.checkNull(item.get("qtyTemp"), "1"));
insertParam.put("SEQ", i + 1);
insertParam.put("STATUS", "editing");
insertParam.put("WRITER", userId);
sqlSession.insert("partMng.insertBomPartQtyBatch", insertParam);
}
}
sqlSession.commit();
return true;
} catch(Exception e) {
sqlSession.rollback();
e.printStackTrace();
return false;
} finally {
sqlSession.close();
}
}
/**
* BOM 복사를 위한 데이터 조회 (엑셀 파싱 형식과 동일하게 반환)
*/