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

32 KiB
Raw Permalink Blame History

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 개선

  • 실시간 계산 표시
  • 진행률 표시
  • 에러 처리 개선

성능 최적화

  • 가상 스크롤링
  • 지연 로딩
  • 캐싱 시스템

🔧 기능 확장

  • 다국어 지원
  • 다크 모드
  • 접근성 향상

📊 거래명세표 계산 로직

🧮 계산 우선순위

  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
🔗 관련 문서: 견적 시스템 전체 가이드, common_addrowJS 개발자 가이드, common_screen 개발자 가이드, common_slat 개발자 가이드, compare_lastJS 개발자 가이드, compare_price_edit_table 개발자 가이드, estimate_compare_head 개발자 가이드, lastJS 개발자 가이드, output_head 개발자 가이드