생산관리_반제품소요량

This commit is contained in:
2025-12-19 18:06:48 +09:00
parent bbc4474d55
commit 1aa7cb5544
4 changed files with 511 additions and 0 deletions

View File

@@ -0,0 +1,344 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page import="com.pms.common.utils.*"%>
<%@ page import="java.util.*" %>
<%@include file= "/init.jsp" %>
<%
// DB에서 메뉴명 조회 (공통 유틸 사용)
String menuObjId = request.getParameter("menuObjId");
String menuName = CommonUtils.getMenuName(menuObjId, "반제품소요량");
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title><%=Constants.SYSTEM_NAME%></title>
<link href="/css/tabulator/tabulator.min.css" rel="stylesheet">
<script type="text/javascript" src="/js/tabulator/tabulator.min.js"></script>
<style>
body, html {
overflow-x: hidden;
width: 100%;
margin: 0;
padding: 0;
}
.section-title {
font-weight: bold;
font-size: 13px;
margin: 10px 0 8px 0;
color: #333;
border-left: 3px solid #3498db;
padding-left: 8px;
}
#inputGrid {
margin-bottom: 10px;
}
</style>
</head>
<body>
<script type="text/javascript">
// 입력 그리드 (M-BOM 선택 + 수량 입력)
var inputGrid;
// 결과 그리드 (소요량 목록)
var resultGrid;
// 입력 행 번호
var rowSeq = 0;
// M-BOM 옵션 데이터
var mbomOptionsData = {};
$(document).ready(function(){
$('.select2').select2();
// M-BOM 옵션 데이터 초기화
fn_initMbomOptions();
// 입력 그리드 초기화
fn_initInputGrid();
// 결과 그리드 초기화
fn_initResultGrid();
// 조회 버튼
$("#btnSearch").click(function(){
fn_search();
});
// 행 추가 버튼
$("#btnAddRow").click(function(){
fn_addInputRow();
});
// 행 삭제 버튼
$("#btnDeleteRow").click(function(){
fn_deleteInputRow();
});
});
// M-BOM 옵션 데이터 초기화
function fn_initMbomOptions() {
mbomOptionsData = {"": "선택"};
$("#MBOM_SELECT_HIDDEN option").each(function(){
var val = $(this).val();
var text = $(this).text();
if(val !== '') {
mbomOptionsData[val] = text;
}
});
console.log("M-BOM 옵션:", mbomOptionsData);
}
// 입력 그리드 초기화
function fn_initInputGrid() {
// 초기 데이터 1행 포함
var initialData = [{
ROW_ID: 1,
MBOM_OBJID: "",
PART_NAME: "",
QTY: 1
}];
rowSeq = 1;
inputGrid = new Tabulator("#inputGrid", {
data: initialData,
layout: "fitColumns",
height: "180px",
columns: [
{
title: '',
field: 'CHK',
width: 40,
headerSort: false,
hozAlign: 'center',
formatter: function(cell) {
return '<input type="checkbox" class="row-check">';
}
},
{title: 'ROW_ID', field: 'ROW_ID', visible: false},
{
title: 'M-BOM 선택',
field: 'MBOM_OBJID',
headerHozAlign: 'center',
headerSort: false,
editor: "list",
editorParams: function(cell) {
return {
values: mbomOptionsData,
autocomplete: true,
clearable: true,
listOnEmpty: true
};
},
formatter: function(cell) {
var value = cell.getValue();
return mbomOptionsData[value] || "";
},
cellEdited: function(cell) {
// M-BOM 선택 시 품명 자동 입력
var mbomObjid = cell.getValue();
var row = cell.getRow();
if(mbomObjid) {
var mbomName = mbomOptionsData[mbomObjid] || "";
row.update({PART_NAME: mbomName});
} else {
row.update({PART_NAME: ""});
}
}
},
{
title: '품명',
field: 'PART_NAME',
headerHozAlign: 'center',
headerSort: false,
editor: false
},
{
title: '수량',
field: 'QTY',
width: 200,
headerHozAlign: 'center',
headerSort: false,
hozAlign: 'right',
editor: "number",
editorParams: {
min: 1,
step: 1
}
}
],
placeholder: "M-BOM을 선택하고 수량을 입력하세요."
});
}
// 결과 그리드 초기화
function fn_initResultGrid() {
resultGrid = new Tabulator("#resultGrid", {
data: [],
layout: "fitColumns",
height: "calc(100vh - 450px)",
columns: [
{
title: '구분',
field: 'CATEGORY_NAME',
width: 200,
headerHozAlign: 'center',
headerSort: true,
hozAlign: 'center'
},
{
title: '품번',
field: 'PART_NO',
width: 300,
headerHozAlign: 'center',
headerSort: true,
hozAlign: 'left'
},
{
title: '품명',
field: 'PART_NAME',
headerHozAlign: 'center',
headerSort: true,
hozAlign: 'left'
},
{
title: '소요량',
field: 'REQUIRED_QTY',
width: 200,
headerHozAlign: 'center',
headerSort: true,
hozAlign: 'right',
formatter: function(cell) {
var val = cell.getValue();
if(val != null && val !== '') {
return Number(val).toLocaleString();
}
return '';
}
}
],
placeholder: "조회 결과가 없습니다."
});
}
// 입력 행 추가
function fn_addInputRow() {
rowSeq++;
inputGrid.addRow({
ROW_ID: rowSeq,
MBOM_OBJID: "",
PART_NAME: "",
QTY: 1
});
}
// 입력 행 삭제
function fn_deleteInputRow() {
var rows = inputGrid.getRows();
var deleteRows = [];
rows.forEach(function(row) {
var el = row.getElement();
var checkbox = el.querySelector('.row-check');
if(checkbox && checkbox.checked) {
deleteRows.push(row);
}
});
if(deleteRows.length === 0) {
Swal.fire('삭제할 행을 선택해주세요.');
return;
}
deleteRows.forEach(function(row) {
row.delete();
});
}
// 조회
function fn_search() {
var inputData = inputGrid.getData();
// 유효한 입력만 필터링
var mbomItems = [];
inputData.forEach(function(row) {
if(row.MBOM_OBJID && row.QTY > 0) {
mbomItems.push({
mbomObjid: row.MBOM_OBJID,
qty: row.QTY
});
}
});
if(mbomItems.length === 0) {
Swal.fire('M-BOM을 선택하고 수량을 입력해주세요.');
return;
}
// AJAX 조회
$.ajax({
url: "/productionplanning/getSemiProductRequirementList.do",
type: "POST",
contentType: "application/json;charset=UTF-8",
data: JSON.stringify({mbomItems: mbomItems}),
dataType: "json",
success: function(data) {
if(data.result === "success") {
resultGrid.setData(data.list || []);
var cnt = data.list ? data.list.length : 0;
$("#resultCnt").text(cnt);
if(cnt === 0) {
Swal.fire({
title: '조회 결과 없음',
text: '부품 또는 조립품 항목이 없습니다.',
icon: 'info'
});
}
} else {
Swal.fire('조회 실패: ' + (data.msg || ''));
resultGrid.setData([]);
$("#resultCnt").text("0");
}
},
error: function(xhr, status, error) {
console.error('조회 오류:', error);
Swal.fire('조회 중 오류가 발생했습니다.');
resultGrid.setData([]);
}
});
}
</script>
<form name="form1" id="form1" method="post">
<!-- M-BOM 옵션을 위한 숨김 select -->
<select id="MBOM_SELECT_HIDDEN" style="display:none;">
<option value="">선택</option>
${code_map.mbom_list}
</select>
<div class="content-box">
<div class="content-box-s">
<div class="plm_menu_name_gdnsi">
<h2>
<span><%=menuName%></span>
</h2>
<div class="btnArea">
<input type="button" class="plm_btns" value="행 추가" id="btnAddRow">
<input type="button" class="plm_btns" value="행 삭제" id="btnDeleteRow">
<input type="button" class="plm_btns" value="조회" id="btnSearch">
</div>
</div>
<!-- 입력 영역 -->
<div id="plmSearchZon">
<div class="section-title">M-BOM 선택 및 수량 입력</div>
<div id="inputGrid" style="margin-right: 16px;"></div>
</div>
<!-- 결과 그리드 영역 -->
<div class="section-title" style="margin-left: 16px;">반제품 소요량 (부품/조립품) - 조회결과: <span id="resultCnt">0</span>건</div>
<div id="resultGrid" style="margin: 0 16px;"></div>
</div>
</div>
</form>
</body>
</html>

View File

@@ -1844,4 +1844,43 @@ public class ProductionPlanningController extends BaseService {
return resultMap;
}
/**
* 반제품소요량 조회 화면
*/
@RequestMapping("/productionplanning/semiProductRequirementList.do")
public String semiProductRequirementList(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) {
Map code_map = new HashMap();
try {
// M-BOM 목록 (셀렉트박스용)
code_map.put("mbom_list", commonService.bizMakeOptionList("", "", "productionplanning.getMbomListForSelect2"));
request.setAttribute("code_map", code_map);
} catch(Exception e) {
e.printStackTrace();
}
return "/productionplanning/semiProductRequirementList";
}
/**
* M-BOM 기준 반제품 소요량 조회
* @param request
* @param paramMap - mbomItems: [{mbomObjid, qty}, ...]
* @return 품번별 합산된 소요량 목록
*/
@ResponseBody
@RequestMapping(value="/productionplanning/getSemiProductRequirementList.do", produces="application/json;charset=UTF-8")
public Map getSemiProductRequirementList(HttpServletRequest request, @RequestBody Map<String, Object> paramMap) {
Map<String, Object> resultMap = new HashMap<String, Object>();
try {
List list = productionPlanningService.getSemiProductRequirementList(paramMap);
resultMap.put("result", "success");
resultMap.put("list", list);
} catch(Exception e) {
e.printStackTrace();
resultMap.put("result", "fail");
resultMap.put("msg", "조회 실패: " + e.getMessage());
}
return resultMap;
}
}

View File

@@ -4743,5 +4743,26 @@
MODIFIER = #{userId}
WHERE OBJID = #{OBJID}
</update>
<!-- M-BOM 반제품 항목 조회 (PART_TYPE이 부품(0001812), 조립품(0001813)인 항목만, 1레벨 제외) -->
<select id="getMbomSemiProductItems" parameterType="map" resultType="map">
/* productionplanning.getMbomSemiProductItems - M-BOM에서 부품/조립품 조회 (1레벨 제외) */
SELECT
MD.PART_NO,
MD.PART_NAME,
COALESCE(MD.QTY, '1')::INTEGER AS ITEM_QTY,
P.PART_TYPE,
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = P.PART_TYPE LIMIT 1), '') AS CATEGORY_NAME,
COALESCE(P.UNIT, '') AS UNIT,
COALESCE(P.MATERIAL, '') AS MATERIAL,
COALESCE(P.SPEC, '') AS SPEC
FROM MBOM_DETAIL MD
INNER JOIN PART_MNG P ON P.OBJID::VARCHAR = MD.PART_OBJID
WHERE MD.MBOM_HEADER_OBJID = #{mbomHeaderObjid}
AND MD.STATUS = 'ACTIVE'
AND (MD.PARENT_OBJID IS NOT NULL AND MD.PARENT_OBJID != '') -- 1레벨 제외 (PARENT_OBJID가 있는 항목만)
AND P.PART_TYPE IN ('0001812', '0001813') -- 부품, 조립품
ORDER BY P.PART_TYPE, MD.PART_NO
</select>
</mapper>

View File

@@ -2,6 +2,7 @@ package com.pms.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -1827,4 +1828,110 @@ public class ProductionPlanningService {
return result;
}
/**
* M-BOM 기준 반제품 소요량 조회
* - 여러 M-BOM을 입력받아 범주가 '부품', '조립품'인 항목만 조회
* - 동일 품번은 합산하여 반환
* @param paramMap - mbomItems: [{mbomObjid, qty}, ...]
* @return 품번별 합산된 소요량 목록
*/
public List getSemiProductRequirementList(Map paramMap) {
SqlSession sqlSession = null;
List resultList = new ArrayList();
try {
sqlSession = SqlMapConfig.getInstance().getSqlSession();
// 입력받은 M-BOM 목록
List<Map<String, Object>> mbomItems = (List<Map<String, Object>>) paramMap.get("mbomItems");
if(mbomItems == null || mbomItems.isEmpty()) {
return resultList;
}
// 품번별 소요량을 합산하기 위한 Map (key: PART_NO, value: {품번정보, 소요량합계})
Map<String, Map<String, Object>> partNoMap = new LinkedHashMap<>();
// 각 M-BOM별로 조회하여 합산
for(Map<String, Object> mbomItem : mbomItems) {
String mbomObjid = CommonUtils.nullToEmpty((String)mbomItem.get("mbomObjid"));
int inputQty = 0;
Object qtyObj = mbomItem.get("qty");
if(qtyObj != null) {
if(qtyObj instanceof Number) {
inputQty = ((Number)qtyObj).intValue();
} else {
try {
inputQty = Integer.parseInt(qtyObj.toString());
} catch(NumberFormatException e) {
inputQty = 0;
}
}
}
if("".equals(mbomObjid) || inputQty <= 0) {
continue;
}
// M-BOM 항목 조회 (범주가 '부품', '조립품'인 것만)
Map<String, Object> queryParam = new HashMap<>();
queryParam.put("mbomHeaderObjid", mbomObjid);
List<Map<String, Object>> bomItems = sqlSession.selectList("productionplanning.getMbomSemiProductItems", queryParam);
// 소요량 합산 (PostgreSQL은 소문자 키로 반환)
for(Map<String, Object> bomItem : bomItems) {
String partNo = CommonUtils.nullToEmpty((String)bomItem.get("part_no"));
if("".equals(partNo)) continue;
// M-BOM의 항목수량
int itemQty = 0;
Object itemQtyObj = bomItem.get("item_qty");
if(itemQtyObj != null) {
if(itemQtyObj instanceof Number) {
itemQty = ((Number)itemQtyObj).intValue();
} else {
try {
itemQty = Integer.parseInt(itemQtyObj.toString());
} catch(NumberFormatException e) {
itemQty = 0;
}
}
}
// 소요량 = 입력수량 × 항목수량
int requiredQty = inputQty * itemQty;
if(partNoMap.containsKey(partNo)) {
// 기존 품번이면 소요량만 합산
Map<String, Object> existingItem = partNoMap.get(partNo);
int existingQty = (Integer)existingItem.get("REQUIRED_QTY");
existingItem.put("REQUIRED_QTY", existingQty + requiredQty);
} else {
// 새로운 품번이면 추가
Map<String, Object> newItem = new LinkedHashMap<>();
newItem.put("PART_NO", partNo);
newItem.put("PART_NAME", bomItem.get("part_name"));
newItem.put("CATEGORY_NAME", bomItem.get("category_name"));
newItem.put("UNIT", bomItem.get("unit"));
newItem.put("MATERIAL", bomItem.get("material"));
newItem.put("SPEC", bomItem.get("spec"));
newItem.put("REQUIRED_QTY", requiredQty);
partNoMap.put(partNo, newItem);
}
}
}
// Map -> List 변환
resultList = new ArrayList(partNoMap.values());
} catch(Exception e) {
e.printStackTrace();
} finally {
if(sqlSession != null) {
sqlSession.close();
}
}
return resultList;
}
}