- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
32 KiB
32 KiB
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. 페이지 초기화 및 데이터 로드
$(document).ready(function() {
$('#loadingOverlay').hide(); // 로딩 오버레이 숨기기
var dataList = <?php echo json_encode($detailJson ?? []); ?>;
// 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. 테이블 데이터 로드 및 업데이트
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. 숫자 포맷팅 및 입력 처리
// 숫자 포맷팅 함수 (콤마 추가 및 소수점 둘째자리에서 반올림)
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);
}
📊 계산 시스템
🧮 행별 합계 계산
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 계산
// 계산식 첫테이블의 일련번호별 소계 계산 (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 포함 계산
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));
});
}
}
🎯 견적 확정액 및 할인 계산
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();
}
🇰🇷 한국어 숫자 변환
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 + '';
}
💾 데이터 저장 시스템
📝 데이터 수집 및 저장
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 생성
function generatePDF() {
var title_message = '<?php echo $title_message; ?>';
var workplace = '<?php echo $outworkplace; ?>';
var deadline = '<?php echo $indate; ?>';
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 생성
function generatePDF_server(callback) {
var workplace = '<?php echo $title_message; ?>';
var item = '<?php echo $emailTitle; ?>';
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');
}
});
});
}
📧 이메일 전송 시스템
📮 이메일 주소 조회 및 전송
function sendmail() {
var secondordnum = '<?php echo $secondordnum; ?>';
var item = '<?php echo $emailTitle; ?>';
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');
}
});
}
📤 실제 이메일 전송
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');
}
});
}
🔄 재계산 시스템
🔢 데이터 재계산
$(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: '확인'
});
}
});
}
});
});
});
👁️ 뷰 토글 시스템
📊 뷰 표시/숨김 제어
$(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();
});
});
💰 할인 시스템
🎯 할인/최종 합계 표시
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 = `
<tfoot>
<tr class="bg-light">
<td colspan="8" class="text-end fw-bold">할인금액</td>
<td colspan="4" class="text-end text-danger"> - ${formatNumber(discount)}</td>
</tr>
<tr class="bg-secondary text-white">
<td colspan="8" class="text-end fw-bold">최종 합계</td>
<td colspan="4" class="text-end fw-bold">${formatNumber(finalRounded)}</td>
</tr>
</tfoot>
`;
$('#tableDetail').append(tfootHTML);
// 화면 하단 두 요소에도 최종합계 반영
$('#totalsum').text(formatNumber(finalRounded));
$('#koreantotalsum').text(KoreanNumber(finalRounded));
}
}
📊 데이터 변수
🎯 주요 JavaScript 변수
dataList: 견적 데이터 배열ajaxRequest_write: 데이터 저장용 AJAX 요청ajaxRequest: 이메일 전송용 AJAX 요청shutterboxMsg: 셔터박스 오류 메시지
💰 금액 관련 변수
EstimateFirstSum: 자동 견적금액EstimateUpdatetSum: 수정 견적금액EstimateDiffer: 견적 차액EstimateFixAmount: 견적확정액EstimateDiscount: 할인금액EstimateFinalSum: 최종 결정금액EstimateDiscountRate: 할인율estimateSurang: 견적 수량estimateTotal: 인정제품 금액ET_unapproved: 비인정제품 금액ET_total: 총 금액
📋 폼 관련 변수
mode: 작업 모드 (insert/modify/copy)num: 견적 번호detailJson: 상세 견적 데이터 (JSON)
🔧 개발자 사용법
📝 기본 사용법
// 페이지 로드 시 자동 실행
$(document).ready(function() {
// 데이터 로드 및 초기화
loadTableData('#detailTable', dataList);
// 이벤트 리스너 설정
setupEventListeners();
// 초기 계산 실행
calculateAllSubtotals();
calculateGrandTotal();
updateDiscountFooterRow();
calculateEstimateAmounts();
});
🧮 계산 함수 사용
// 행별 합계 계산
calculateRowTotal($(row));
// 소계 및 VAT 계산
calculateSubtotalBySerial(serialNumber);
// 총계 및 VAT 포함 계산
calculateGrandTotal();
// 견적 확정액 및 할인 계산
calculateEstimateAmounts();
// 할인/최종 합계 표시
updateDiscountFooterRow();
💾 데이터 저장
// 수동 저장
saveData();
// 재계산 후 저장
$('.initialBtn').click();
📄 PDF 생성
// 클라이언트 사이드 PDF 다운로드
generatePDF();
// 서버 사이드 PDF 생성 (이메일용)
generatePDF_server(function(filename) {
// PDF 생성 완료 후 콜백 실행
});
📧 이메일 전송
// 이메일 전송 시작
sendmail();
// 직접 이메일 전송
sendEmail(recipientEmail, vendorName, item, filename);
🚨 주의사항
⚠️ 필수 의존성
- jQuery 3.x
- html2pdf.js
- SweetAlert2
- Bootstrap 5.x
🔒 보안 고려사항
- AJAX 요청 시 CSRF 토큰 검증
- 입력값 검증 및 이스케이프 처리
- 파일 업로드 보안 검증
📱 성능 최적화
- AJAX 요청 중복 방지
- 대용량 데이터 처리 최적화
- 메모리 누수 방지
🐛 디버깅 가이드
🔍 일반적인 문제 해결
1. JSON 데이터 파싱 오류
// JSON 파싱 오류 확인
try {
dataList = JSON.parse(dataList);
} catch (e) {
console.error('JSON parsing error: ', e);
dataList = [];
}
2. 계산 오류
// 계산 과정 디버깅
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 요청 오류
// AJAX 오류 처리
error: function(jqxhr, status, error) {
console.log('AJAX Error: ', status, error);
console.log('Response: ', jqxhr.responseText);
}
4. PDF 생성 오류
// 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_output.php: 거래명세표 데이터 저장 처리get_companyCode.php: 회사 코드 조회save_pdf.php: PDF 파일 저장send_email.php: 이메일 전송
🔗 CSS 클래스
.calculation-row: 계산 대상 행.subtotal-cell: 소계 셀.vat-cell: VAT 셀.subtotalamount-cell: 소계 합계 셀.grand-total: 총계 셀.grand-vat: 총 VAT 셀.grand-sum: 총 합계 셀
🔗 HTML 요소
#detailTable: 상세 테이블#content-to-print: PDF 출력 대상#board_form: 메인 폼#loadingOverlay: 로딩 오버레이#tableDetail: 거래명세표 테이블
🎯 향후 개선 방향
🔄 코드 리팩토링
- 모듈화 및 클래스 기반 구조
- ES6+ 문법 적용
- TypeScript 도입 검토
🎨 UI/UX 개선
- 실시간 계산 표시
- 진행률 표시
- 에러 처리 개선
⚡ 성능 최적화
- 가상 스크롤링
- 지연 로딩
- 캐싱 시스템
🔧 기능 확장
- 다국어 지원
- 다크 모드
- 접근성 향상
📊 거래명세표 계산 로직
🧮 계산 우선순위
- 행별 계산: 수량 × 단가
- 면적 계산: 길이 × 면적단가
- 소계 계산: 일련번호별 합계
- VAT 계산: 소계 × 10%
- 총계 계산: 소계 + VAT
- 할인 계산: 확정액 × 할인율
- 최종 계산: 확정액 - 할인금액
📈 데이터 흐름
- 데이터 로드 → JSON 파싱
- 테이블 업데이트 → 행별 데이터 설정
- 계산 실행 → 행별/소계/VAT/총계 계산
- 할인 적용 → 할인율 기반 계산
- 결과 표시 → 포맷팅된 숫자 표시
- 데이터 저장 → JSON 변환 후 서버 전송
🔄 업데이트 프로세스
- 사용자 입력 → 이벤트 리스너 감지
- 실시간 계산 → 행별 합계 재계산
- 연쇄 업데이트 → 소계/VAT/총계 재계산
- 할인 적용 → 할인율 변경 시 재계산
- 화면 갱신 → 포맷팅된 결과 표시
📅 문서 버전: 1.0
👨💻 작성자: 개발팀
📝 최종 수정일: 2024-12-24
🔗 관련 문서: 견적 시스템 전체 가이드, common_addrowJS 개발자 가이드, common_screen 개발자 가이드, common_slat 개발자 가이드, compare_lastJS 개발자 가이드, compare_price_edit_table 개발자 가이드, estimate_compare_head 개발자 가이드, lastJS 개발자 가이드, output_head 개발자 가이드