Files
sam-kd/0readme/estimate/output_lastJS_developer_guide.md
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

1027 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 = <?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. **테이블 데이터 로드 및 업데이트**
```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 = '<?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 생성**
```javascript
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');
}
});
});
}
```
## 📧 이메일 전송 시스템
### 📮 **이메일 주소 조회 및 전송**
```javascript
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');
}
});
}
```
### 📤 **실제 이메일 전송**
```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 = `
<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)
## 🔧 개발자 사용법
### 📝 **기본 사용법**
```javascript
// 페이지 로드 시 자동 실행
$(document).ready(function() {
// 데이터 로드 및 초기화
loadTableData('#detailTable', dataList);
// 이벤트 리스너 설정
setupEventListeners();
// 초기 계산 실행
calculateAllSubtotals();
calculateGrandTotal();
updateDiscountFooterRow();
calculateEstimateAmounts();
});
```
### 🧮 **계산 함수 사용**
```javascript
// 행별 합계 계산
calculateRowTotal($(row));
// 소계 및 VAT 계산
calculateSubtotalBySerial(serialNumber);
// 총계 및 VAT 포함 계산
calculateGrandTotal();
// 견적 확정액 및 할인 계산
calculateEstimateAmounts();
// 할인/최종 합계 표시
updateDiscountFooterRow();
```
### 💾 **데이터 저장**
```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_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 개선**
- 실시간 계산 표시
- 진행률 표시
- 에러 처리 개선
### ⚡ **성능 최적화**
- 가상 스크롤링
- 지연 로딩
- 캐싱 시스템
### 🔧 **기능 확장**
- 다국어 지원
- 다크 모드
- 접근성 향상
## 📊 거래명세표 계산 로직
### 🧮 **계산 우선순위**
1. **행별 계산**: 수량 × 단가
2. **면적 계산**: 길이 × 면적단가
3. **소계 계산**: 일련번호별 합계
4. **VAT 계산**: 소계 × 10%
5. **총계 계산**: 소계 + VAT
6. **할인 계산**: 확정액 × 할인율
7. **최종 계산**: 확정액 - 할인금액
### 📈 **데이터 흐름**
1. **데이터 로드** → JSON 파싱
2. **테이블 업데이트** → 행별 데이터 설정
3. **계산 실행** → 행별/소계/VAT/총계 계산
4. **할인 적용** → 할인율 기반 계산
5. **결과 표시** → 포맷팅된 숫자 표시
6. **데이터 저장** → JSON 변환 후 서버 전송
### 🔄 **업데이트 프로세스**
1. **사용자 입력** → 이벤트 리스너 감지
2. **실시간 계산** → 행별 합계 재계산
3. **연쇄 업데이트** → 소계/VAT/총계 재계산
4. **할인 적용** → 할인율 변경 시 재계산
5. **화면 갱신** → 포맷팅된 결과 표시
---
**📅 문서 버전**: 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), [lastJS 개발자 가이드](./lastJS_developer_guide.md), [output_head 개발자 가이드](./output_head_developer_guide.md)