# output_lastJS.php 개발자 가이드 ## 📋 개요 `output_lastJS.php`는 방화셔터 견적 시스템의 거래명세표 출력 페이지 JavaScript 컴포넌트로, 거래명세표 데이터 처리, 계산, PDF 생성, 이메일 전송 등의 기능을 담당합니다. 이 파일은 `lastJS.php`를 기반으로 하여 거래명세표 특화 기능을 추가한 버전으로, VAT 포함 계산, 할인율 적용, 인정제품/비인정제품 구분 등의 고급 기능을 제공합니다. ## 🏗️ 파일 구조 ### 📁 파일 위치 ``` /estimate/common/output_lastJS.php ``` ### 📊 파일 정보 - **파일 크기**: 36.5KB (934 lines) - **주요 언어**: JavaScript + PHP - **의존성**: jQuery, html2pdf.js, SweetAlert2, Bootstrap - **주요 기능**: 거래명세표 데이터 처리, VAT 포함 계산, 할인율 적용 ## 🔧 핵심 기능 ### 1. **페이지 초기화 및 데이터 로드** ```javascript $(document).ready(function() { $('#loadingOverlay').hide(); // 로딩 오버레이 숨기기 var dataList = ; // JSON 데이터를 처리하기 전에 유효성 검사 if (dataList && typeof dataList === 'string') { try { dataList = JSON.parse(dataList); // JSON 문자열을 객체로 변환 } catch (e) { console.error('JSON parsing error: ', e); dataList = []; // 오류 발생 시 빈 배열로 초기화 } } // 배열인지 확인 if (!Array.isArray(dataList)) { dataList = []; } else { $("#updateText").text('견적수정됨'); } // 테이블에 데이터 로드 loadTableData('#detailTable', dataList); // 셔터박스 오류메시지 화면에 표시 var shutterboxMsg = = json_encode($shutterboxMsg) ?>; if (shutterboxMsg) { var shutterboxDiv = document.getElementById("shutterboxMsg"); shutterboxDiv.style.display = "block"; shutterboxDiv.innerHTML = shutterboxMsg; } }); ``` ### 2. **테이블 데이터 로드 및 업데이트** ```javascript function loadTableData(tableBodySelector, dataList) { console.log('loadTableData data: ', dataList); var tableBody = $(tableBodySelector); var count = 1; dataList.forEach(function(rowData, index) { var row = tableBody.find('tr').eq(index + count); if (row.length) { updateRowData(row, rowData, index); } }); } function updateRowData(row, rowData, rowIndex) { // 수정해야 할 td 요소들을 선택하여 해당 값을 업데이트 row.find('.su-input').val(rowData[0]); // 수량 row.find('.area-length-input').val(rowData[2]); // 길이 row.find('.area-price-input').val(rowData[3]); // 면적단가 row.find('.unit-price-input').val(rowData[4]); // 단가 // 수정된 행에 동적 계산 함수 호출 calculateRowTotal(row); } ``` ### 3. **숫자 포맷팅 및 입력 처리** ```javascript // 숫자 포맷팅 함수 (콤마 추가 및 소수점 둘째자리에서 반올림) function formatNumber(value) { // 소수점 둘째 자리에서 반올림 const roundedValue = value; // 콤마 추가 포맷팅 return new Intl.NumberFormat().format(roundedValue); } // 숫자에서 콤마를 제거하는 함수 function cleanNumber(value) { // value가 null 또는 undefined인 경우 0을 반환하도록 처리 if (!value) return 0; return parseFloat(value.replace(/,/g, '')) || 0; } // 입력 필드에서 숫자를 포맷팅하는 함수 function inputNumber(input) { const cursorPosition = input.selectionStart; const value = input.value.replace(/,/g, ''); const formattedValue = Number(value).toLocaleString(); input.value = formattedValue; input.setSelectionRange(cursorPosition, cursorPosition); } ``` ## 📊 계산 시스템 ### 🧮 **행별 합계 계산** ```javascript function calculateRowTotal(row) { row = $(row); const itemNameInput = row.find('.item-name'); const suInput = row.find('.su-input'); const areaLengthInput = row.find('.area-length-input'); const areaPriceInput = row.find('.area-price-input'); const unitPriceInput = row.find('.unit-price-input'); const su = suInput.length ? cleanNumber(suInput.val()) : 1; const areaLength = areaLengthInput.length ? cleanNumber(areaLengthInput.val()) : 1; const areaPrice = areaPriceInput.length ? cleanNumber(areaPriceInput.val()) : 1; let unitPrice = unitPriceInput.length ? cleanNumber(unitPriceInput.val()) : 1; const roundedAreaPrice = parseFloat(areaPrice); if (roundedAreaPrice > 0) { unitPrice = Math.round(Math.round(areaLength * roundedAreaPrice)); unitPriceInput.val(formatNumber(unitPrice)); } let totalPrice = 0; if (!areaLength && !areaPrice) { totalPrice = Math.round(Math.round((su * unitPrice))); } else if (areaLength && !areaPrice) { totalPrice = Math.round(Math.round((areaLength * unitPrice * su))); } else { totalPrice = Math.round(Math.round((su * unitPrice))); } const totalCell = row.find('.total-price'); if (totalCell.length) { if(totalPrice > 200) totalCell.text(formatNumber(totalPrice)); } return totalPrice; } ``` ### 📈 **소계 및 VAT 계산** ```javascript // 계산식 첫테이블의 일련번호별 소계 계산 (jQuery 방식) function calculateSubtotalBySerial(serialNumber) { let subtotal = 0; let vat = 0; let total = 0; const rows = $(`.calculation-row[data-serial="${serialNumber}"]`); rows.each(function() { const rowTotal = calculateRowTotal($(this)); if (rowTotal > 300) { subtotal += rowTotal; } }); vat = Math.round(subtotal * 0.1); total = subtotal + vat; const subtotalCells = $(`.subtotal-cell[data-serial="${serialNumber}"]`); const vatCells = $(`.vat-cell[data-serial="${serialNumber}"]`); const totalCells = $(`.subtotalamount-cell[data-serial="${serialNumber}"]`); if (subtotalCells.length > 0) { subtotalCells.each(function() { $(this).text(formatNumber(subtotal)); }); } if (vatCells.length > 0) { vatCells.each(function() { $(this).text(formatNumber(vat)); }); } if (totalCells.length > 0) { totalCells.each(function() { $(this).text(formatNumber(total)); }); } return subtotal; } ``` ### 💰 **총계 및 VAT 포함 계산** ```javascript function calculateGrandTotal() { // 소수점 이하를 반올림해서 정수 와 원 단위만 남김 const rawTotal = calculateAllSubtotals(); const rawVat = Math.round(rawTotal * 0.1); const grandTotal = rawTotal; const rawSumTotal = grandTotal + rawVat; let finalTotal = 0; if (rawVat > 0) { finalTotal = rawSumTotal; } else { finalTotal = grandTotal; } const grandTotalCells = $('.grand-total'); if (grandTotalCells.length > 0) { grandTotalCells.each(function() { $(this).text(formatNumber(grandTotal)); }); $('#totalsum').text(formatNumber(finalTotal)); $('#koreantotalsum').text(KoreanNumber(finalTotal)); const firstSum = cleanNumber($("#EstimateFirstSum").val()); if (grandTotal > 0 && rawVat > 0 && firstSum < 1) { $("#EstimateFirstSum").val(formatNumber(finalTotal)); } else { $("#EstimateUpdatetSum").val(formatNumber(finalTotal)); } const updatedSum = cleanNumber($("#EstimateUpdatetSum").val()); if (updatedSum > 0) { $("#EstimateDiffer").val(formatNumber(updatedSum - cleanNumber($("#EstimateFirstSum").val()))); } else { $("#EstimateDiffer").val(0); } } let grandVatCells = $('.grand-vat'); if (grandVatCells.length > 0) { grandVatCells.each(function() { $(this).text(formatNumber(rawVat)); }); } let grandSumCells = $('.grand-sum'); if (grandSumCells.length > 0) { grandSumCells.each(function() { $(this).text(formatNumber(rawSumTotal)); }); } } ``` ### 🎯 **견적 확정액 및 할인 계산** ```javascript function calculateEstimateAmounts() { // 1) 자동견적(=EstimateFirstSum) / 수동견적(=EstimateUpdatetSum) 값 가져오기 const autoSum = cleanNumber($("#EstimateFirstSum").val()); let manualSum = cleanNumber($("#EstimateUpdatetSum").val()); // ★ 수동견적과 자동견적이 같으면 수동견적을 지워서 나오지 않게 if (manualSum > 0 && manualSum === autoSum) { manualSum = 0; $("#EstimateUpdatetSum").val(''); } // 2) 견적확정액 결정: 수동견적 있으면 수동, 없으면 자동 const fixAmount = manualSum > 0 ? manualSum : autoSum; $("#EstimateFixAmount").val(formatNumber(fixAmount)); // 3) 할인금액 = 견적확정액 × 할인율 ÷ 100 const discountRate = $("#EstimateDiscountRate").val(); let discountAmt = fixAmount * discountRate / 100; const subtotal = cleanNumber($('#subtotalamount').text()); let finalTotal = 0; let finalRounded = 0; if (discountRate > 0) { finalTotal = subtotal - discountAmt; // 1000원 미만 절사 finalRounded = Math.floor(finalTotal / 1000) * 1000; } else { finalTotal = subtotal; finalRounded = finalTotal; } discountAmt += (finalTotal - finalRounded); $("#EstimateDiscount").val(formatNumber(discountAmt)); // 4) 최종결정금액 = 견적확정액 – 할인금액 const finalSum = fixAmount - discountAmt; $("#EstimateFinalSum").val(formatNumber(finalSum)); // → 할인/최종 합계 row 갱신 updateDiscountFooterRow(); } ``` ### 🇰🇷 **한국어 숫자 변환** ```javascript function KoreanNumber(number) { const koreanNumbers = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구']; const koreanUnits = ['', '십', '백', '천']; const bigUnits = ['', '만', '억', '조']; let result = ''; let unitIndex = 0; let numberStr = String(number); if (number == 0) return '영원'; while (numberStr.length > 0) { let chunk = numberStr.slice(-4); numberStr = numberStr.slice(0, -4); let chunkResult = ''; for (let i = 0; i < chunk.length; i++) { const digit = parseInt(chunk[i]); if (digit > 0) { chunkResult += koreanNumbers[digit] + koreanUnits[chunk.length - i - 1]; } } if (chunkResult) { result = chunkResult + bigUnits[unitIndex] + result; } unitIndex++; } result = result.replace(/일(?=십|백|천)/g, '').trim(); return result + ''; } ``` ## 💾 데이터 저장 시스템 ### 📝 **데이터 수집 및 저장** ```javascript function saveData() { const myform = document.getElementById('board_form'); let allValid = true; if (!allValid) return; var num = $("#num").val(); $("#overlay").show(); $("button").prop("disabled", true); // 모드 설정 (insert 또는 modify) if ($("#mode").val() !== 'copy') { if (Number(num) < 1) { $("#mode").val('insert'); } else { $("#mode").val('modify'); } } else { $("#mode").val('insert'); } // 데이터 수집 (input 요소만 저장) let formData = []; $('#detailTable tbody tr').each(function() { let rowData = []; $(this).find('input, select').each(function() { let value = $(this).val(); rowData.push(value); }); formData.push(rowData); }); // JSON 문자열로 변환하여 form input에 설정 let jsonString = JSON.stringify(formData); $('#detailJson').val(jsonString); $("#estimateSurang").val('= $estimateSurang ?>'); // 수량 저장 $("#estimateTotal").val($("#EstimateFinalSum").val()); // 최종 결정금액 저장 // 총금액 계산 (콤마 제거 후 숫자로 변환) const unapproved = parseFloat(($("#ET_unapproved").val() || "0").replace(/,/g, '')); const approved = parseFloat(($("#estimateTotal").val() || "0").replace(/,/g, '')); const total = unapproved + approved; $("#ET_total").val(total.toLocaleString()); // 부모창 업데이트 if (window.opener && !window.opener.closed) { const $parent = window.opener.$; if ($parent("#estimateSurang").length) { $parent("#estimateSurang").val($("#estimateSurang").val()); } if ($parent("#estimateTotal").length) { $parent("#estimateTotal").val(approved.toLocaleString()); } if ($parent("#ET_unapproved").length) { $parent("#ET_unapproved").val(unapproved.toLocaleString()); } if ($parent("#ET_total").length) { $parent("#ET_total").val(total.toLocaleString()); } } var form = $('#board_form')[0]; var datasource = new FormData(form); if (ajaxRequest_write !== null) { ajaxRequest_write.abort(); } showMsgModal(2); // Ajax 요청으로 서버에 데이터 전송 ajaxRequest_write = $.ajax({ enctype: 'multipart/form-data', processData: false, contentType: false, cache: false, timeout: 600000, url: "/estimate/insert_detail_output.php", // 산출내역 저장 type: "post", data: datasource, dataType: "json", success: function(data) { setTimeout(function() { $(opener.location).attr("href", "javascript:restorePageNumber();"); setTimeout(function() { hideMsgModal(); hideOverlay(); window.close(); }, 1000); }, 1000); }, error: function(jqxhr, status, error) { console.log(jqxhr, status, error); } }); } ``` ## 📄 PDF 생성 시스템 ### 🖨️ **클라이언트 사이드 PDF 생성** ```javascript function generatePDF() { var title_message = ''; var workplace = ''; var deadline = ''; var deadlineDate = new Date(deadline); var formattedDate = "(" + String(deadlineDate.getFullYear()).slice(-2) + "." + ("0" + (deadlineDate.getMonth() + 1)).slice(-2) + "." + ("0" + deadlineDate.getDate()).slice(-2) + ")"; var result = 'KD' + title_message + '(' + workplace +')' + formattedDate + '.pdf'; var element = document.getElementById('content-to-print'); var opt = { margin: [10, 3, 12, 3], filename: result, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 3, useCORS: true, scrollY: 0, scrollX: 0, windowWidth: document.body.scrollWidth, windowHeight: document.body.scrollHeight }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['css', 'legacy'], avoid: ['tr', '.avoid-break'] } }; html2pdf().from(element).set(opt).save(); } ``` ### 🖥️ **서버 사이드 PDF 생성** ```javascript function generatePDF_server(callback) { var workplace = ''; var item = ''; var today = new Date(); var formattedDate = "(" + String(today.getFullYear()).slice(-2) + "." + ("0" + (today.getMonth() + 1)).slice(-2) + "." + ("0" + today.getDate()).slice(-2) + ")"; var result = 'KD' + item +'(' + workplace + ')' + formattedDate + '.pdf'; var element = document.getElementById('content-to-print'); var opt = { margin: [10, 3, 12, 3], filename: result, image: { type: 'jpeg', quality: 0.70 }, html2canvas: { scale: 4 }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: [''] } }; html2pdf().from(element).set(opt).output('datauristring').then(function (pdfDataUri) { var pdfBase64 = pdfDataUri.split(',')[1]; var formData = new FormData(); formData.append('pdf', pdfBase64); formData.append('filename', result); $.ajax({ type: 'POST', url: '/email/save_pdf.php', data: formData, processData: false, contentType: false, success: function (response) { var res = JSON.parse(response); if (callback) { callback(res.filename); } }, error: function (xhr, status, error) { Swal.fire('Error', 'PDF 저장에 실패했습니다.', 'error'); } }); }); } ``` ## 📧 이메일 전송 시스템 ### 📮 **이메일 주소 조회 및 전송** ```javascript function sendmail() { var secondordnum = ''; var item = ''; if (!secondordnum) { Swal.fire({ icon: 'warning', title: '오류 알림', text: '발주처 코드가 없습니다.' }); return; } if (typeof ajaxRequest !== 'undefined' && ajaxRequest !== null) { ajaxRequest.abort(); } ajaxRequest = $.ajax({ type: 'POST', url: '/email/get_companyCode.php', data: { secondordnum: secondordnum }, dataType: 'json', success: function(response) { if (response.error) { Swal.fire('Error', response.error, 'error'); } else { var email = response.email; var vendorName = response.vendor_name; Swal.fire({ title: 'E메일 보내기', text: vendorName + ' Email 주소확인', icon: 'warning', input: 'text', inputLabel: 'Email 주소 수정 가능', inputValue: email, showCancelButton: true, confirmButtonText: '보내기', cancelButtonText: '취소', reverseButtons: true, inputValidator: (value) => { if (!value) { return '이메일 주소를 입력해주세요!'; } const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(value)) { return '올바른 이메일 형식을 입력해주세요!'; } } }).then((result) => { if (result.isConfirmed) { const updatedEmail = result.value; generatePDF_server(function(filename) { sendEmail(updatedEmail, vendorName, item, filename); }); } }); } }, error: function(xhr, status, error) { Swal.fire('Error', '전송중 오류가 발생했습니다.', 'error'); } }); } ``` ### 📤 **실제 이메일 전송** ```javascript function sendEmail(recipientEmail, vendorName, item, filename) { if (typeof ajaxRequest !== 'undefined' && ajaxRequest !== null) { ajaxRequest.abort(); } var today = new Date(); var formattedDate = "(" + String(today.getFullYear()).slice(-2) + "." + ("0" + (today.getMonth() + 1)).slice(-2) + "." + ("0" + today.getDate()).slice(-2) + ")"; ajaxRequest = $.ajax({ type: 'POST', url: '/email/send_email.php', data: { email: recipientEmail, vendorName: vendorName, filename: filename, item: item, formattedDate: formattedDate }, success: function(response) { Swal.fire('Success', '정상적으로 전송되었습니다.', 'success'); }, error: function(xhr, status, error) { Swal.fire('Error', '전송에 실패했습니다. 확인바랍니다.', 'error'); } }); } ``` ## 🔄 재계산 시스템 ### 🔢 **데이터 재계산** ```javascript $(document).ready(function() { $('.initialBtn').on('click', function() { Swal.fire({ title: '견적데이터 재계산', text: "견적 데이터를 재계산 하시겠습니까?", icon: 'warning', showCancelButton: true, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: '예, 재계산합니다', cancelButtonText: '취소' }).then((result) => { if (result.isConfirmed) { $("#estimateTotal").val(0); $("#EstimateFirstSum").val(0); $("#EstimateUpdatetSum").val(0); $("#EstimateDiffer").val(0); $("#EstimateDiscountRate").val(0); $("#EstimateDiscount").val(0); $("#EstimateFinalSum").val(0); var form = $('#board_form')[0]; var datasource = new FormData(form); const initialData = JSON.stringify([]); $('#detailJson').val(initialData); $.ajax({ enctype: 'multipart/form-data', processData: false, contentType: false, cache: false, timeout: 600000, url: "/estimate/insert_detail_output.php", type: "post", data: datasource, dataType: "json", success: function(response) { Swal.fire({ title: '재계산 완료', text: "모든 데이터가 재계산되었습니다.", icon: 'success', confirmButtonText: '확인' }).then(() => { hideMsgModal(); location.reload(); }); }, error: function(jqxhr, status, error) { Swal.fire({ title: '오류', text: "재계산 중 오류가 발생했습니다.", icon: 'error', confirmButtonText: '확인' }); } }); } }); }); }); ``` ## 👁️ 뷰 토글 시스템 ### 📊 **뷰 표시/숨김 제어** ```javascript $(document).ready(function() { // 산출내역서 보이기/숨기기 function toggleEstimateView() { $('#showEstimateCheckbox').is(':checked') ? $('.Estimateview').show() : $('.Estimateview').hide(); } // 소요자재 보이기/숨기기 function toggleListView() { $('#showlistCheckbox').is(':checked') ? $('.listview, .vendor-send, .Novendor-send').show() : $('.listview, .vendor-send, .Novendor-send').hide(); } // 업체발송용 영역 토글 function toggleVendorDiv() { $('#showEstimateCheckbox').prop('checked', false); $('#showlistCheckbox').prop('checked', false); toggleEstimateView(); toggleListView(); if ($('#showVendorCheckbox').is(':checked')) { $('.vendor-send').show(); $('.Novendor-send').hide(); } else { $('.vendor-send').hide(); $('.Novendor-send').show(); } } var option = $('#option').val(); if (option === 'option') { toggleEstimateView(); toggleListView(); toggleVendorDiv(); } // 이벤트 리스너 설정 $('#showEstimateCheckbox').on('change', toggleEstimateView); $('#showlistCheckbox').on('change', function() { toggleListView(); $('#showVendorCheckbox').prop('checked', false); }); $('#showVendorCheckbox').on('change', toggleVendorDiv); // 할인율 변경시 $('#EstimateDiscountRate').on('input change', function() { updateDiscountFooterRow(); calculateEstimateAmounts(); }); }); ``` ## 💰 할인 시스템 ### 🎯 **할인/최종 합계 표시** ```javascript function updateDiscountFooterRow() { const rate = $('#EstimateDiscountRate').val(); const subtotal = cleanNumber($('#subtotalamount').text()); // 기존 tfoot 제거 $('#tableDetail tfoot').remove(); let discount = 0; let finalTotal = 0; let finalRounded = 0; if (rate <= 0) { discount = 0; finalTotal = subtotal; finalRounded = finalTotal; } else { discount = Math.round(subtotal * Number(rate) / 100); finalTotal = subtotal - discount; finalRounded = Math.floor(finalTotal / 1000) * 1000; discount += (finalTotal - finalRounded); } if (rate > 0) { const tfootHTML = `