diff --git a/.gitignore b/.gitignore index cd16182..431f5cf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,9 @@ Thumbs.db # Cursor files .cursor/ + +# Claude Code +CLAUDE.md +.claude/ +.playwright-mcp/ +.omc/ diff --git a/WebContent/WEB-INF/view/contractMgmt/addEstimatePdfPopup.jsp b/WebContent/WEB-INF/view/contractMgmt/addEstimatePdfPopup.jsp new file mode 100644 index 0000000..8661349 --- /dev/null +++ b/WebContent/WEB-INF/view/contractMgmt/addEstimatePdfPopup.jsp @@ -0,0 +1,150 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ page import="com.pms.common.utils.*"%> +<%@ page import="java.util.*" %> +<%@include file= "/init.jsp" %> + + + + +<%=Constants.SYSTEM_NAME%> + + + + +
+
+
+

추가견적 PDF 첨부

+
+
+ + + + + +
PDF 파일 +
+

* PDF 파일만 첨부 가능합니다. (추후 메일 발송 시 견적서 PDF와 합쳐 발송될 예정입니다)

+
+ Drag & Drop PDF Files Here +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + +
No파일명등록일Size
+
+
+
+
+
+
+ +
+
+
+
+
+ + diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp index 2fdfb32..c927c8d 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp @@ -74,7 +74,7 @@ $(this).val(fnc_addComma($(this).val().replace(/[^0-9.]/g, ""))); } }); - $("input:text[numberOnly]").on("blur", function() { + $(document).on("blur", "input:text[numberOnly]", function() { var val = $(this).val(); if(val && val !== '') { if($(this).hasClass("item-order-quantity") || $(this).attr("id") === "facility_qty") { @@ -133,6 +133,13 @@ function addComma(data) { return formatMoney(data); } + + function addCommaInt(data) { + if(!data && data !== 0) return ''; + var num = Math.round(Number(String(data).replace(/,/g, ''))); + if(isNaN(num)) return ''; + return num.toLocaleString(); + } function removeComma(data) { if(!data) return ''; @@ -331,30 +338,30 @@ // 수주수량 (Machine이고 프로젝트가 있으면 readonly) html += ''; if(isMachine && hasProject) { - html += ''; + html += ''; } else { - html += ''; + html += ''; } html += ''; - + // 수주단가 html += ''; - html += ''; + html += ''; html += ''; - + // 수주공급가액 (자동계산 + 수정가능) html += ''; - html += ''; + html += ''; html += ''; - + // 수주부가세 (자동계산 + 수정가능) html += ''; - html += ''; + html += ''; html += ''; - + // 수주총액 (자동계산 + 수정가능) html += ''; - html += ''; + html += ''; html += ''; // 삭제 버튼 @@ -401,7 +408,8 @@ } else if($(this).hasClass("item-order-vat")) { fn_calculateTotalFromVat(itemId); } - // 총액 직접 수정시에는 재계산 안함 + // 총액 직접 수정시에도 합계는 갱신 + fn_calculateTotal(); }); // 품목 정보 저장 @@ -543,22 +551,22 @@ // 수주 정보 (Machine이고 프로젝트가 있으면 수량 readonly) html += ''; if(isMachine && hasProject) { - html += ''; + html += ''; } else { - html += ''; + html += ''; } html += ''; html += ''; - html += ''; + html += ''; html += ''; html += ''; - html += ''; + html += ''; html += ''; html += ''; - html += ''; + html += ''; html += ''; html += ''; - html += ''; + html += ''; html += ''; html += ''; @@ -624,6 +632,8 @@ } else if($(this).hasClass("item-order-vat")) { fn_calculateTotalFromVat(itemId); } + // 총액 직접 수정시에도 합계는 갱신 + fn_calculateTotal(); }); // 품목 정보 저장 @@ -636,9 +646,11 @@ } } %> + // 기존 품목 로드 완료 후 합계 계산 + fn_calculateTotal(); } - - // 품목별 금액 계산 + + // 품목별 금액 계산 function fn_calculateItemAmount(itemId) { var quantity = parseFloat(removeComma($("#" + itemId + " .item-order-quantity").val())) || 0; var unitPrice = parseFloat(removeComma($("#" + itemId + " .item-order-unit-price").val())) || 0; @@ -654,8 +666,9 @@ // 총액 계산 var totalAmount = supplyPrice + vat; $("#" + itemId + " .item-order-total-amount").val(addComma(totalAmount)); + fn_calculateTotal(); } - + // 부가세 직접 입력 시 총액만 재계산 function fn_calculateTotalFromVat(itemId) { var supplyPrice = parseFloat(removeComma($("#" + itemId + " .item-order-supply-price").val())) || 0; @@ -664,19 +677,36 @@ // 총액 계산 var totalAmount = supplyPrice + vat; $("#" + itemId + " .item-order-total-amount").val(addComma(totalAmount)); + fn_calculateTotal(); } - + // 공급가액 직접 입력 시 부가세와 총액 재계산 function fn_calculateFromSupplyPrice(itemId) { var supplyPrice = parseFloat(removeComma($("#" + itemId + " .item-order-supply-price").val())) || 0; - + // 부가세 자동 계산 (공급가액의 10%) var vat = Math.round(supplyPrice * 0.1); $("#" + itemId + " .item-order-vat").val(addComma(vat)); - + // 총액 계산 var totalAmount = supplyPrice + vat; $("#" + itemId + " .item-order-total-amount").val(addComma(totalAmount)); + fn_calculateTotal(); + } + + // 품목 합계 계산 + function fn_calculateTotal() { + var totalQty = 0, totalSupply = 0, totalVat = 0, totalAmount = 0; + $(".item-row").each(function() { + totalQty += parseFloat(removeComma($(this).find(".item-order-quantity").val())) || 0; + totalSupply += parseFloat(removeComma($(this).find(".item-order-supply-price").val())) || 0; + totalVat += parseFloat(removeComma($(this).find(".item-order-vat").val())) || 0; + totalAmount += parseFloat(removeComma($(this).find(".item-order-total-amount").val())) || 0; + }); + $("#totalOrderQuantity").text(addCommaInt(totalQty)); + $("#totalOrderSupplyPrice").text(addComma(totalSupply)); + $("#totalOrderVat").text(addComma(totalVat)); + $("#totalOrderTotalAmount").text(addComma(totalAmount)); } // 품번/품명 셀렉트박스 옵션 채우기 (Select2 AJAX) @@ -1037,7 +1067,7 @@ if(confirm("해당 품목을 삭제하시겠습니까?")) { $("#" + itemId).remove(); - + // itemList에서 제거 for(var i = 0; i < itemList.length; i++) { if(itemList[i].id == itemId) { @@ -1045,10 +1075,13 @@ break; } } - + // 행 번호 재정렬 fn_reorderItemRows(); - + + // 합계 재계산 + fn_calculateTotal(); + // 모든 행이 삭제되면 안내 메시지 표시 if($(".item-row").length == 0) { $("#noItemRow").show(); @@ -1487,6 +1520,17 @@ + + + Total + 0 + + 0.00 + 0.00 + 0.00 + + + diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp index a7a0369..8ac36df 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp @@ -414,7 +414,22 @@ var columns = [ var objid = fnc_checkNull(cell.getData().OBJID); fn_showEstimateList(objid); } - }, + }, + // 11-1. 추가견적 PDF 첨부 + {headerHozAlign : 'center', hozAlign : 'center', minWidth: 55, widthGrow: 0.7, title : '추가견적', field : 'ADD_EST_CNT', + formatter: function(cell, formatterParams, onRendered){ + var cnt = fnc_checkNull(cell.getValue()); + var icon = '📎'; + if(cnt !== '' && parseInt(cnt) > 0){ + return icon + ' ' + cnt + ''; + } + return icon; + }, + cellClick:function(e, cell){ + var objid = fnc_checkNull(cell.getData().OBJID); + fn_openAddEstimatePdf(objid); + } + }, // 12. 아마란스 결재상태 (hidden) {title:'AMARANTH_STATUS', field:'AMARANTH_STATUS', visible: false}, // 13. 결재상태 (아마란스 전자결재) @@ -556,7 +571,7 @@ function fn_search(){ console.log("품목 검색 조건 설정됨:", partObjId); } - _tabulGrid = fnc_tabul_search(_tabul_layout_fitColumns, _tabulGrid, "/contractMgmt/contractGridList.do", columns, true); + _tabulGrid = fnc_tabul_search(_tabul_layout_fitColumns, _tabulGrid, "/contractMgmt/estimateGridList.do", columns, true); } function _fnc_datepick(){ @@ -635,6 +650,14 @@ function fn_delete(){ } +// 추가견적 PDF 첨부 팝업 +function fn_openAddEstimatePdf(objId){ + var popup_width = 700; + var popup_height = 400; + var url = '/contractMgmt/addEstimatePdfPopup.do?targetObjId=' + objId; + fn_centerPopup(popup_width, popup_height, url); +} + function fn_FileRegist(objId, docType, docTypeName){ var popup_width = 800; var popup_height = 300; diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp index b5474d7..166724f 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp @@ -1662,14 +1662,14 @@ function fn_generatePdf() { pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; - // 페이지가 넘어가면 추가 페이지 생성 - while (heightLeft >= 0) { + // 페이지가 넘어가면 추가 페이지 생성 (1mm 이상 넘칠 때만) + while (heightLeft > 1) { position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; } - + // 파일명 생성 var estimateNo = $("#estimate_no").val() || "견적서"; var fileName = estimateNo + '.pdf'; @@ -1782,13 +1782,13 @@ function fn_generateAndUploadPdf(callback) { pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; - while (heightLeft >= 0) { + while (heightLeft > 1) { position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; } - + console.log('PDF 생성 완료'); // PDF를 Base64로 변환 diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateTemplate2.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateTemplate2.jsp index c3dd8a7..2573c81 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateTemplate2.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateTemplate2.jsp @@ -1158,7 +1158,7 @@ function fn_generateAndUploadPdf(callback) { pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; - while (heightLeft >= 0) { + while (heightLeft > 1) { position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); diff --git a/WebContent/WEB-INF/view/partMng/partMngList.jsp b/WebContent/WEB-INF/view/partMng/partMngList.jsp index 321a6c4..d248cc8 100644 --- a/WebContent/WEB-INF/view/partMng/partMngList.jsp +++ b/WebContent/WEB-INF/view/partMng/partMngList.jsp @@ -125,7 +125,7 @@ String connector = person.getUserId(); $('.select2').select2(); // 품번/품명 Select2 AJAX 초기화 - initPartSelect2Ajax("#SEARCH_PART_NO", "#SEARCH_PART_NAME", "#SEARCH_PART_OBJID"); + // initPartSelect2Ajax("#SEARCH_PART_NO", "#SEARCH_PART_NAME", "#SEARCH_PART_OBJID"); //첨부팝업 $(".File").click(function(){ @@ -760,16 +760,16 @@ function fn_deleteErp(){ - + + + - + + diff --git a/src/com/pms/mapper/partMng.xml b/src/com/pms/mapper/partMng.xml index 23dec61..5b3d2b3 100644 --- a/src/com/pms/mapper/partMng.xml +++ b/src/com/pms/mapper/partMng.xml @@ -2001,9 +2001,11 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.* AND UPPER(T.PART_NO) LIKE UPPER('%${SEARCH_PART_NO}%') + AND UPPER(T.PART_NAME) LIKE UPPER('%${SEARCH_PART_NAME}%') diff --git a/src/com/pms/salesmgmt/controller/ContractMgmtController.java b/src/com/pms/salesmgmt/controller/ContractMgmtController.java index 2854520..bcfc7e5 100644 --- a/src/com/pms/salesmgmt/controller/ContractMgmtController.java +++ b/src/com/pms/salesmgmt/controller/ContractMgmtController.java @@ -2899,6 +2899,14 @@ public class ContractMgmtController { request.setAttribute("docTypeName", CommonUtils.checkNull(paramMap.get("docTypeName"))); return "/contractMgmt/FileRegistPopup"; } + + /** + * 추가견적 PDF 첨부 팝업 + */ + @RequestMapping("/contractMgmt/addEstimatePdfPopup.do") + public String addEstimatePdfPopup(HttpServletRequest request, @RequestParam Map paramMap){ + return "/contractMgmt/addEstimatePdfPopup"; + } /** * 영업정보의 품목 목록 조회 (견적서 작성 시 사용) diff --git a/src/com/pms/salesmgmt/mapper/contractMgmt.xml b/src/com/pms/salesmgmt/mapper/contractMgmt.xml index d680e10..a57a65f 100644 --- a/src/com/pms/salesmgmt/mapper/contractMgmt.xml +++ b/src/com/pms/salesmgmt/mapper/contractMgmt.xml @@ -739,6 +739,8 @@ AND CANCEL_QTY != '' AND CANCEL_QTY != '0' ) AS CANCEL_QTY_SUM + -- 추가견적 PDF 첨부 건수 + ,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE = 'estimate02' AND UPPER(STATUS) = 'ACTIVE') AS ADD_EST_CNT FROM CONTRACT_MGMT AS T LEFT OUTER JOIN @@ -1014,9 +1016,134 @@ AND COALESCE(IS_DIRECT_ORDER, 'N') != 'Y' - ORDER BY REGDATE DESC + ORDER BY REGDATE DESC + + + @@ -3302,13 +3422,13 @@ SELECT , #{qty } , #{warranty } - , #{product_price }::NUMERIC + , #{product_price }::NUMERIC - , #{other_price }::NUMERIC + , #{other_price }::NUMERIC - , #{total_price }::NUMERIC + , #{total_price }::NUMERIC , #{contract_user_id } , #{contract_date } @@ -3317,20 +3437,20 @@ SELECT , #{contract_office_no} , #{contract_fax_no } - ,#{est_release_date} + ,#{est_release_date} , NOW() , #{userId} ,(SELECT TO_CHAR(NOW(),'yy')::VARCHAR ||'E-'||LPAD((SELECT NEXTVAL('estimate_mgmt_seq'))::VARCHAR ,4,'0')) - ,#{contract_product_price}::NUMERIC + ,#{contract_product_price}::NUMERIC ,#{sale} ,#{final_total_price}::NUMERIC ,#{contract_type} ,#{note} - ,#{cus_request_date} + ,#{cus_request_date} ,#{delivery_place} ,#{product_code} diff --git a/src/com/pms/salesmgmt/service/ContractMgmtService.java b/src/com/pms/salesmgmt/service/ContractMgmtService.java index 72288d3..6763b25 100644 --- a/src/com/pms/salesmgmt/service/ContractMgmtService.java +++ b/src/com/pms/salesmgmt/service/ContractMgmtService.java @@ -25,6 +25,8 @@ import org.apache.ibatis.session.SqlSession; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; +import org.apache.pdfbox.multipdf.PDFMergerUtility; +import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -3406,6 +3408,42 @@ private String encodeImageToBase64(String imagePath) { * @param estimateTemplate * @return */ + /** + * 견적서 PDF + 추가견적(estimate02) PDF 파일들을 하나로 병합 + */ + private File mergePdfWithAddEstimate(File estimatePdf, List addEstFiles) throws Exception { + String tempDir = System.getProperty("java.io.tmpdir"); + String mergedFileName = estimatePdf.getName().replace(".pdf", "_merged.pdf"); + File mergedFile = new File(tempDir + File.separator + mergedFileName); + + PDFMergerUtility merger = new PDFMergerUtility(); + merger.setDestinationFileName(mergedFile.getAbsolutePath()); + + // 1. 견적서 PDF 추가 + merger.addSource(estimatePdf); + + // 2. 추가견적 PDF 파일들 추가 + for(Map fileInfo : addEstFiles) { + String filePath = CommonUtils.checkNull(fileInfo.get("FILE_PATH")); + if("".equals(filePath)) filePath = CommonUtils.checkNull(fileInfo.get("file_path")); + String savedName = CommonUtils.checkNull(fileInfo.get("SAVED_FILE_NAME")); + if("".equals(savedName)) savedName = CommonUtils.checkNull(fileInfo.get("saved_file_name")); + if("".equals(savedName)) savedName = CommonUtils.checkNull(fileInfo.get("FILE_SAVED_NAME")); + if("".equals(filePath)) filePath = Constants.FILE_STORAGE; + + File addFile = new File(filePath + File.separator + savedName); + if(addFile.exists() && addFile.getName().toLowerCase().endsWith(".pdf")) { + merger.addSource(addFile); + System.out.println("추가견적 PDF 병합 추가: " + addFile.getName()); + } + } + + merger.mergeDocuments(null); + mergedFile.deleteOnExit(); + + return mergedFile; + } + private File getPdfFromSession(String sessionId, Map estimateTemplate) { try { String tempDir = System.getProperty("java.io.tmpdir"); @@ -3539,18 +3577,37 @@ private String encodeImageToBase64(String imagePath) { } } - // 5. PDF 파일 처리 + // 5. PDF 파일 처리 (견적서 + 추가견적 PDF 병합) ArrayList attachFileList = new ArrayList(); if(!"".equals(pdfSessionId)) { File pdfFile = getPdfFromSession(pdfSessionId, estimateTemplate); if(pdfFile != null && pdfFile.exists()) { + // 추가견적(estimate02) PDF 파일 조회 + Map fileParam = new HashMap(); + fileParam.put("targetObjId", objId); + fileParam.put("docType", "estimate02"); + List addEstFiles = sqlSession.selectList("common.getFileList", fileParam); + + File finalPdf = pdfFile; + + // 추가견적 PDF가 있으면 병합 + if(addEstFiles != null && !addEstFiles.isEmpty()) { + try { + finalPdf = mergePdfWithAddEstimate(pdfFile, addEstFiles); + System.out.println("PDF 병합 완료: 견적서 + 추가견적 " + addEstFiles.size() + "건"); + } catch(Exception mergeEx) { + System.out.println("PDF 병합 실패, 견적서만 첨부: " + mergeEx.getMessage()); + finalPdf = pdfFile; + } + } + HashMap fileMap = new HashMap(); - fileMap.put(Constants.Db.COL_FILE_REAL_NAME, pdfFile.getName()); - fileMap.put(Constants.Db.COL_FILE_SAVED_NAME, pdfFile.getName()); - fileMap.put(Constants.Db.COL_FILE_PATH, pdfFile.getParent()); + fileMap.put(Constants.Db.COL_FILE_REAL_NAME, finalPdf.getName()); + fileMap.put(Constants.Db.COL_FILE_SAVED_NAME, finalPdf.getName()); + fileMap.put(Constants.Db.COL_FILE_PATH, finalPdf.getParent()); attachFileList.add(fileMap); - - System.out.println("PDF 파일 첨부 완료: " + pdfFile.getAbsolutePath()); + + System.out.println("PDF 파일 첨부 완료: " + finalPdf.getAbsolutePath()); } else { System.out.println("PDF 파일을 찾을 수 없습니다: " + pdfSessionId); }