# lastJS.php 개발자 가이드 ## 📋 개요 `lastJS.php`는 방화셔터 견적 시스템의 핵심 JavaScript 컴포넌트로, 견적 데이터 처리, 계산, PDF 생성, 이메일 전송 등의 기능을 담당합니다. 이 파일은 견적 비교 및 최종 처리 페이지에서 사용되며, 복잡한 계산 로직과 사용자 인터페이스 상호작용을 관리합니다. ## 🏗️ 파일 구조 ### 📁 파일 위치 ``` /estimate/common/lastJS.php ``` ### 📊 파일 정보 - **파일 크기**: 29.1KB (753 lines) - **주요 언어**: JavaScript + PHP - **의존성**: jQuery, html2pdf.js, SweetAlert2, Bootstrap - **주요 기능**: 데이터 처리, 계산, PDF 생성, 이메일 전송 ## 🔧 핵심 기능 ### 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 = ; 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) { 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; } ``` ### 📈 **소계 및 총계 계산** ```javascript // 일련번호별 소계 계산 function calculateSubtotalBySerial(serialNumber) { let subtotal = 0; const rows = $(`.calculation-row[data-serial="${serialNumber}"]`); rows.each(function() { const rowTotal = calculateRowTotal($(this)); if (rowTotal > 300) { subtotal += rowTotal; } }); const subtotalCells = $(`.subtotal-cell[data-serial="${serialNumber}"]`); if (subtotalCells.length > 0) { subtotalCells.each(function() { $(this).text(formatNumber(subtotal)); }); } return subtotal; } // 모든 일련번호별 소계 및 총합계 계산 function calculateAllSubtotals() { let grandTotal = 0; const uniqueSerials = new Set(); $('.calculation-row').each(function() { uniqueSerials.add($(this).data('serial')); }); uniqueSerials.forEach(function(serialNumber) { grandTotal += calculateSubtotalBySerial(serialNumber); }); return grandTotal; } // 총합계 계산 function calculateGrandTotal() { const grandTotal = calculateAllSubtotals(); const grandTotalCells = $('.grand-total'); if (grandTotalCells.length > 0) { grandTotalCells.each(function() { $(this).text(formatNumber(grandTotal)); }); $('#totalsum').text(formatNumber(grandTotal)); var EstimateFirstSum = $("#EstimateFirstSum").val(); var EstimateUpdatetSum = $("#EstimateUpdatetSum").val(); $('#koreantotalsum').text(KoreanNumber(Math.round(grandTotal))); // 총금액에서 최초금액 추출하기 if(grandTotal > 0 && EstimateFirstSum < 1) $("#EstimateFirstSum").val(formatNumber(grandTotal)); else $("#EstimateUpdatetSum").val(formatNumber(grandTotal)); // 차액계산 if(cleanNumber($("#EstimateUpdatetSum").val()) > 0) $("#EstimateDiffer").val(formatNumber(cleanNumber($("#EstimateUpdatetSum").val()) - cleanNumber($("#EstimateFirstSum").val()))); else $("#EstimateDiffer").val(0); } } ``` ### 🇰🇷 **한국어 숫자 변환** ```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(''); $("#estimateTotal").val($("#subtotal").text()); 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.php", type: "post", data: datasource, dataType: "json", success: function(data) { setTimeout(function() { $(opener.location).attr("href", "javascript:restorePageNumber();"); setTimeout(function() { hideMsgModal(); hideOverlay(); }, 1500); }, 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: 1 }, html2canvas: { scale: 3 }, 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, dataType: 'json', success: function (response) { if (response.success && callback) { callback(response.filename); } else { Swal.fire('Error', response.error || 'PDF 저장에 실패했습니다.', 'error'); } }, 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_alternative.php', data: { email: recipientEmail, vendorName: vendorName, filename: filename, item: item, formattedDate: formattedDate }, dataType: 'json', success: function(response) { if (response.success) { Swal.fire('Success', response.message || '정상적으로 전송되었습니다.', 'success'); } else { if (response.error && response.error.includes('앱 비밀번호')) { Swal.fire({ icon: 'warning', title: '앱 비밀번호 필요', text: '네이버에서 앱 비밀번호를 요구하고 있습니다. 설정 가이드를 확인해주세요.', confirmButtonText: '가이드 보기', showCancelButton: true, cancelButtonText: '취소' }).then((result) => { if (result.isConfirmed) { window.open('/email/naver_app_password_guide.php', '_blank'); } }); } else { Swal.fire('Error', response.error || '전송에 실패했습니다.', 'error'); } } }, 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); 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.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); }); ``` ## 📊 데이터 변수 ### 🎯 **주요 JavaScript 변수** - `dataList`: 견적 데이터 배열 - `ajaxRequest_write`: 데이터 저장용 AJAX 요청 - `ajaxRequest`: 이메일 전송용 AJAX 요청 - `shutterboxMsg`: 셔터박스 오류 메시지 ### 💰 **금액 관련 변수** - `EstimateFirstSum`: 최초 자동금액 - `EstimateUpdatetSum`: 수정 금액 - `EstimateDiffer`: 차액 - `estimateSurang`: 견적 수량 - `estimateTotal`: 견적 총액 ### 📋 **폼 관련 변수** - `mode`: 작업 모드 (insert/modify/copy) - `num`: 견적 번호 - `detailJson`: 상세 견적 데이터 (JSON) ## 🔧 개발자 사용법 ### 📝 **기본 사용법** ```javascript // 페이지 로드 시 자동 실행 $(document).ready(function() { // 데이터 로드 및 초기화 loadTableData('#detailTable', dataList); // 이벤트 리스너 설정 setupEventListeners(); // 초기 계산 실행 calculateAllSubtotals(); calculateGrandTotal(); }); ``` ### 🧮 **계산 함수 사용** ```javascript // 행별 합계 계산 calculateRowTotal($(row)); // 소계 계산 calculateSubtotalBySerial(serialNumber); // 총계 계산 calculateGrandTotal(); // 첫 번째 테이블 계산 calculateRowTotalFirstTable(); ``` ### 💾 **데이터 저장** ```javascript // 수동 저장 saveData(); // 재계산 후 저장 $('.initialBtn').click(); ``` ### 📄 **PDF 생성** ```javascript // 클라이언트 사이드 PDF 다운로드 generatePDF(); // 서버 사이드 PDF 생성 (이메일용) generatePDF_server(function(filename) { // PDF 생성 완료 후 콜백 실행 }); ``` ### 📧 **이메일 전송** ```javascript // 이메일 전송 시작 sendmail(); // 직접 이메일 전송 sendEmail(recipientEmail, vendorName, item, filename); ``` ## 🚨 주의사항 ### ⚠️ **필수 의존성** - jQuery 3.x - html2pdf.js - SweetAlert2 - Bootstrap 5.x ### 🔒 **보안 고려사항** - AJAX 요청 시 CSRF 토큰 검증 - 입력값 검증 및 이스케이프 처리 - 파일 업로드 보안 검증 ### 📱 **성능 최적화** - AJAX 요청 중복 방지 - 대용량 데이터 처리 최적화 - 메모리 누수 방지 ## 🐛 디버깅 가이드 ### 🔍 **일반적인 문제 해결** #### 1. JSON 데이터 파싱 오류 ```javascript // JSON 파싱 오류 확인 try { dataList = JSON.parse(dataList); } catch (e) { console.error('JSON parsing error: ', e); dataList = []; } ``` #### 2. 계산 오류 ```javascript // 계산 과정 디버깅 console.log('totalPrice', totalPrice); console.log('itemNameInput', itemNameInput.text()); console.log('suInput', suInput.val()); console.log('areaLengthInput', areaLengthInput.val()); console.log('areaPriceInput', areaPriceInput.val()); console.log('unitPriceInput', unitPriceInput.val()); ``` #### 3. AJAX 요청 오류 ```javascript // AJAX 오류 처리 error: function(jqxhr, status, error) { console.log('AJAX Error: ', status, error); console.log('Response: ', jqxhr.responseText); } ``` #### 4. PDF 생성 오류 ```javascript // PDF 생성 옵션 확인 var opt = { margin: [10, 3, 12, 3], filename: result, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 3 }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } }; ``` ## 📚 관련 파일 ### 🔗 **의존성 파일** - `jquery.min.js`: jQuery 라이브러리 - `html2pdf.bundle.min.js`: PDF 생성 라이브러리 - `sweetalert2.min.js`: 알림 라이브러리 - `bootstrap.min.js`: Bootstrap JavaScript ### 🔗 **연관 PHP 파일** - `insert_detail.php`: 데이터 저장 처리 - `get_companyCode.php`: 회사 코드 조회 - `save_pdf.php`: PDF 파일 저장 - `send_email_alternative.php`: 이메일 전송 ### 🔗 **CSS 클래스** - `.calculation-row`: 계산 대상 행 - `.subtotal-cell`: 소계 셀 - `.grand-total`: 총계 셀 - `.total-price`: 행별 합계 셀 ### 🔗 **HTML 요소** - `#detailTable`: 상세 테이블 - `#content-to-print`: PDF 출력 대상 - `#board_form`: 메인 폼 - `#loadingOverlay`: 로딩 오버레이 ## 🎯 향후 개선 방향 ### 🔄 **코드 리팩토링** - 모듈화 및 클래스 기반 구조 - ES6+ 문법 적용 - TypeScript 도입 검토 ### 🎨 **UI/UX 개선** - 실시간 계산 표시 - 진행률 표시 - 에러 처리 개선 ### ⚡ **성능 최적화** - 가상 스크롤링 - 지연 로딩 - 캐싱 시스템 ### 🔧 **기능 확장** - 다국어 지원 - 다크 모드 - 접근성 향상 ## 📊 계산 로직 ### 🧮 **계산 우선순위** 1. **행별 계산**: 수량 × 단가 2. **면적 계산**: 길이 × 면적단가 3. **소계 계산**: 일련번호별 합계 4. **총계 계산**: 전체 합계 ### 📈 **데이터 흐름** 1. **데이터 로드** → JSON 파싱 2. **테이블 업데이트** → 행별 데이터 설정 3. **계산 실행** → 행별/소계/총계 계산 4. **결과 표시** → 포맷팅된 숫자 표시 5. **데이터 저장** → JSON 변환 후 서버 전송 ### 🔄 **업데이트 프로세스** 1. **사용자 입력** → 이벤트 리스너 감지 2. **실시간 계산** → 행별 합계 재계산 3. **연쇄 업데이트** → 소계/총계 재계산 4. **화면 갱신** → 포맷팅된 결과 표시 --- **📅 문서 버전**: 1.0 **👨‍💻 작성자**: 개발팀 **📝 최종 수정일**: 2024-12-24 **🔗 관련 문서**: [견적 시스템 전체 가이드](./README.md), [common_addrowJS 개발자 가이드](./common_addrowJS_developer_guide.md), [common_screen 개발자 가이드](./common_screen_developer_guide.md), [common_slat 개발자 가이드](./common_slat_developer_guide.md), [compare_lastJS 개발자 가이드](./compare_lastJS_developer_guide.md), [compare_price_edit_table 개발자 가이드](./compare_price_edit_table_developer_guide.md), [estimate_compare_head 개발자 가이드](./estimate_compare_head_developer_guide.md)