- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
872 lines
27 KiB
Markdown
872 lines
27 KiB
Markdown
# 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 = <?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) {
|
||
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('<?= $estimateSurang ?>');
|
||
$("#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 = '<?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: 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 = '<?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_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) |