From 3e272785998f95e6159dd0550e4e6ebc55a173df Mon Sep 17 00:00:00 2001 From: hjjeong Date: Fri, 27 Feb 2026 18:50:47 +0900 Subject: [PATCH] =?UTF-8?q?ebom,=20mbom=20E-BOM=20&=20M-BOM=20=ED=8C=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=98=EC=A0=9C=ED=92=88=20=EC=B6=94=EA=B0=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=98=EB=A9=B4=20=ED=95=98=EC=9C=84=20=ED=92=88?= =?UTF-8?q?=EB=AA=A9=20=EA=B0=99=EC=9D=B4=20=EC=B6=94=EA=B0=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=90=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20E-BOM=20&?= =?UTF-8?q?=20M-BOM=20=ED=8C=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=EC=8B=9C=20?= =?UTF-8?q?=EC=9B=90=ED=95=98=EB=8A=94=20=EC=9C=84=EC=B9=98=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=91=EC=96=B4=20=EB=84=A3=EB=8A=94=20=EA=B8=B0=EB=8A=A5(?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EB=8A=94=20=EC=99=BC=EC=AA=BD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=A0=ED=83=9D=ED=95=9C=20=ED=95=98=EC=9C=84?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=EC=9D=98=20=EC=A0=9C=EC=9D=BC=20=EB=B0=91?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=EB=A7=8C=20=EB=93=A4=EC=96=B4=EA=B0=90)=20E-?= =?UTF-8?q?BOM=20&=20M-BOM=20=ED=8C=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EC=8B=9C=20=EC=9E=90=EB=8F=99=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=9D=B4=20=EC=95=84=EB=8B=88=EB=9D=BC,=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5/=EB=8B=AB=EA=B8=B0=20=EB=B2=84=ED=8A=BC=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=88=EC=96=B4=EC=84=9C=20=EC=A0=80=EC=9E=A5=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=88=84=EB=A5=BC=EB=95=8C=EB=A7=8C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EC=8B=A0=EA=B7=9C=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=83=9D=EC=84=B1=ED=95=98=EA=B3=A0,=20M-?= =?UTF-8?q?BOM=20=EC=B2=98=EC=9D=8C=20=EB=A7=8C=EB=93=A4=EB=95=8C=20E-BOM?= =?UTF-8?q?=20=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=99=80=EC=84=9C=20=ED=95=A0?= =?UTF-8?q?=EB=8B=B9=ED=95=A0=EB=95=8C=20=EC=B5=9C=EC=83=81=EC=9C=84=20?= =?UTF-8?q?=EC=A0=9C=ED=92=88=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9D=B4=20=ED=95=84=EC=9A=94(?= =?UTF-8?q?=EC=B5=9C=EC=83=81=EC=9C=84=20=EC=A0=9C=ED=92=88=EC=9D=84=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EA=B3=A0=20=EB=B0=98=EC=A0=9C?= =?UTF-8?q?=ED=92=88=EC=9D=84=20=EC=B5=9C=EC=83=81=EC=9C=84=20=ED=92=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=A7=8C=EB=93=9C=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81)=20=EC=B5=9C=EC=83=81=EC=9C=84=20=EC=A0=9C=ED=92=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=A7=8C=EB=93=A4=EB=A0=A4=EA=B3=A0=20?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=B0=98=EC=A0=9C=ED=92=88=EC=9D=98=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EB=B6=80=ED=92=88=EB=8F=84=20=EB=94=B8?= =?UTF-8?q?=EB=A0=A4=EC=99=80=EC=84=9C=20=EA=B5=AC=EC=84=B1=EC=9D=B4=20?= =?UTF-8?q?=EB=90=98=EC=96=B4=EC=95=BC=20=ED=95=98=EB=A9=B0,=20M-BOM=20?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EB=A6=84=EB=8F=84=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EB=90=9C=20=EC=B5=9C=EC=83=81=EC=9C=84=20=EC=A0=9C=ED=92=88?= =?UTF-8?q?=EC=9D=98=20=ED=92=88=EB=B2=88=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=98=EC=96=B4=20=EC=A0=80=EC=9E=A5=EB=90=98?= =?UTF-8?q?=EC=96=B4=EC=95=BC=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/partMng/structurePopupCenter.jsp | 555 +++++++----------- .../view/partMng/structurePopupLeft.jsp | 151 ++++- .../view/partMng/structurePopupTop.jsp | 77 ++- .../productionplanning/mBomHeaderPopup.jsp | 337 ++++++++--- .../view/productionplanning/mBomPopupLeft.jsp | 87 ++- src/com/pms/controller/PartMngController.java | 90 +++ src/com/pms/mapper/partMng.xml | 132 ++++- src/com/pms/mapper/productionplanning.xml | 16 +- src/com/pms/mapper/salesMng.xml | 14 +- src/com/pms/service/PartMngService.java | 193 +++++- 10 files changed, 1191 insertions(+), 461 deletions(-) diff --git a/WebContent/WEB-INF/view/partMng/structurePopupCenter.jsp b/WebContent/WEB-INF/view/partMng/structurePopupCenter.jsp index 4c5934f..3ec405d 100644 --- a/WebContent/WEB-INF/view/partMng/structurePopupCenter.jsp +++ b/WebContent/WEB-INF/view/partMng/structurePopupCenter.jsp @@ -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(//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 : ['+rightPartNo+']
같은 Part No끼리 연결할 수 없습니다.', + html: '오류 Part No : ['+rowData.PART_NO+']
같은 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 : ['+rightPartNo+']
이미 상위에 등록된 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를 변경하시겠습니까?

' + - '기존: ' + leftPartNo + '
' + - '변경: ' + rightPartNo, - icon: 'warning', - showCancelButton: true, - confirmButtonColor: '#3085d6', - cancelButtonColor: '#d33', - confirmButtonText: '변경', - cancelButtonText: '취소', - reverseButtons: false + html: '선택한 Part를 변경하시겠습니까?

기존: ' + leftPartNo + '
변경: ' + 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정보가 없습니다.
이대로 연결하면 1레벨로 등록됩니다.

진행하시겠습니까?', - 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); */ - } - }); -} diff --git a/WebContent/WEB-INF/view/partMng/structurePopupLeft.jsp b/WebContent/WEB-INF/view/partMng/structurePopupLeft.jsp index b8ceb9e..f9ad771 100644 --- a/WebContent/WEB-INF/view/partMng/structurePopupLeft.jsp +++ b/WebContent/WEB-INF/view/partMng/structurePopupLeft.jsp @@ -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(); diff --git a/WebContent/WEB-INF/view/partMng/structurePopupTop.jsp b/WebContent/WEB-INF/view/partMng/structurePopupTop.jsp index 492993d..b6e316d 100644 --- a/WebContent/WEB-INF/view/partMng/structurePopupTop.jsp +++ b/WebContent/WEB-INF/view/partMng/structurePopupTop.jsp @@ -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(); + } + } +} @@ -125,10 +188,12 @@ function fn_resetFilter() { - - - - + + + + + + diff --git a/WebContent/WEB-INF/view/productionplanning/mBomHeaderPopup.jsp b/WebContent/WEB-INF/view/productionplanning/mBomHeaderPopup.jsp index dbc2dcd..eeab9c0 100644 --- a/WebContent/WEB-INF/view/productionplanning/mBomHeaderPopup.jsp +++ b/WebContent/WEB-INF/view/productionplanning/mBomHeaderPopup.jsp @@ -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) { + @@ -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: '선택한 품목을 최상위 제품으로 변경하시겠습니까?

' + + '품번: ' + selectedPartNo + '
' + + '품명: ' + selectedPartName + '
' + + (ebomResult && ebomResult.hasEbom ? '하위 품목: ' + ebomResult.subTree.length + '개' : '하위 품목: 없음') + + '

기존 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 { diff --git a/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp b/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp index a45b0f7..8158c37 100644 --- a/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp +++ b/WebContent/WEB-INF/view/productionplanning/mBomPopupLeft.jsp @@ -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, // 품목 정보 diff --git a/src/com/pms/controller/PartMngController.java b/src/com/pms/controller/PartMngController.java index f03c754..fa47ebd 100644 --- a/src/com/pms/controller/PartMngController.java +++ b/src/com/pms/controller/PartMngController.java @@ -1223,6 +1223,58 @@ public class PartMngController { return bomTreeList != null ? bomTreeList : new ArrayList(); } + /** + * 품번 기준 E-BOM 하위 구조 조회 (반제품 추가 시 하위 품목 연동용) + * 품번에 해당하는 E-BOM이 있으면 하위 트리를 반환, 없으면 빈 리스트 반환 + */ + @RequestMapping("/partMng/getEbomSubTreeByPartNo.do") + @ResponseBody + public Map getEbomSubTreeByPartNo(HttpServletRequest request, @RequestParam Map paramMap){ + Map 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 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 paramMap){ @@ -1250,6 +1302,44 @@ public class PartMngController { return "/partMng/structurePopupCenter"; } + /** + * E-BOM 일괄 저장 (클라이언트 편집 후 저장) + */ + @RequestMapping("/partMng/saveEbom.do") + @ResponseBody + public Map saveEbom(HttpServletRequest request, @RequestBody Map paramMap) { + Map result = new HashMap<>(); + try { + String bomReportObjId = CommonUtils.checkNull((String)paramMap.get("bomReportObjId")); + List> ebomData = (List>) 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 diff --git a/src/com/pms/mapper/partMng.xml b/src/com/pms/mapper/partMng.xml index 0ed105e..3bf462b 100644 --- a/src/com/pms/mapper/partMng.xml +++ b/src/com/pms/mapper/partMng.xml @@ -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.* + + + + + + + + + + + + DELETE FROM BOM_PART_QTY + WHERE BOM_REPORT_OBJID = #{bomReportObjId} + AND CHILD_OBJID = #{childObjid} + + + + + 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} + + + + + 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 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 diff --git a/src/com/pms/mapper/productionplanning.xml b/src/com/pms/mapper/productionplanning.xml index f0edca5..2fed003 100644 --- a/src/com/pms/mapper/productionplanning.xml +++ b/src/com/pms/mapper/productionplanning.xml @@ -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, diff --git a/src/com/pms/mapper/salesMng.xml b/src/com/pms/mapper/salesMng.xml index b871d17..0145d93 100644 --- a/src/com/pms/mapper/salesMng.xml +++ b/src/com/pms/mapper/salesMng.xml @@ -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, diff --git a/src/com/pms/service/PartMngService.java b/src/com/pms/service/PartMngService.java index def76be..044cbfc 100644 --- a/src/com/pms/service/PartMngService.java +++ b/src/com/pms/service/PartMngService.java @@ -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 getBomObjIdByPartNo(String partNo) throws Exception { + Map paramMap = new HashMap<>(); + paramMap.put("partNo", partNo); + return getBomObjIdByPartNo(paramMap); + } + + /** + * E-BOM 하위 트리 조회 (반제품 추가 시 하위 품목 연동용) + * getBOMPartTreeList와 동일하지만 HttpServletRequest 없이 직접 bomReportObjId로 조회 + */ + public List getBOMPartTreeListSimple(Map paramMap) { + List> resultList = new ArrayList(); + List> 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 findParam = new HashMap<>(); + findParam.put("partNo", partNo); + Map found = (Map) 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 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> ebomData, String userId) { + SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(false); + try { + // 1. 기존 데이터 조회 + Map queryParam = new HashMap<>(); + queryParam.put("bomReportObjId", bomReportObjId); + List> existingList = sqlSession.selectList("partMng.getExistingBomPartQty", queryParam); + + // 기존 CHILD_OBJID 맵 (CHILD_OBJID -> 행 데이터) + Map> existingMap = new HashMap<>(); + for(Map 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 newChildObjids = new HashSet<>(); + for(Map item : ebomData) { + String childObjid = CommonUtils.checkNull(item.get("childObjid")); + if(!childObjid.isEmpty()) { + newChildObjids.add(childObjid); + } + } + + // 새 데이터에서 참조하는 PARENT_OBJID 셋 (삭제 보호용) + Set referencedParentObjids = new HashSet<>(); + for(Map 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 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 item = ebomData.get(i); + String childObjid = CommonUtils.checkNull(item.get("childObjid")); + + if(existingMap.containsKey(childObjid)) { + // 기존 항목 → 수정 (수량, 순서 등만 업데이트) + Map 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 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 복사를 위한 데이터 조회 (엑셀 파싱 형식과 동일하게 반환) */