Files
sam-kd/0readme/estimate/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

872 lines
27 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.

# 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)