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

26 KiB

compare_lastJS.php 개발자 가이드

📋 개요

compare_lastJS.php는 방화셔터 견적 시스템의 견적 비교 및 계산 기능을 담당하는 핵심 JavaScript 컴포넌트입니다. 이 파일은 견적 데이터의 로딩, 계산, PDF 생성, 이메일 전송 등 견적 비교 화면의 모든 클라이언트 사이드 기능을 제공합니다.

🏗️ 파일 구조

📁 파일 위치

/estimate/common/compare_lastJS.php

📊 파일 정보

  • 파일 크기: 26.7KB (682 lines)
  • 주요 언어: JavaScript + PHP (혼합)
  • 의존성: jQuery, html2pdf, SweetAlert2, Bootstrap
  • 주요 기능: 견적 비교, 계산, PDF 생성, 이메일 전송

🔧 핵심 기능

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);
});

2. 테이블 데이터 로딩

function loadTableData(tableBodySelector, dataList) {
    var tableBody = $(tableBodySelector); // 테이블 본문 선택
    
    // 데이터를 반복하면서 테이블에 행을 업데이트
    var count = 1;
    dataList.forEach(function(rowData, index) {
        var row = tableBody.find('tr').eq(index + count); // index에 맞는 tr을 가져옴
        if (row.length) {
            updateRowData(row, rowData, index);
        }
    });
}

3. 행 데이터 업데이트

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); // 필요 시 계산 함수 호출
}

📊 계산 시스템

🧮 숫자 포맷팅 함수

// 숫자 포맷팅 함수 (콤마 추가 및 소수점 둘째자리에서 반올림)
function formatNumber(value) {
    // 소수점 둘째 자리에서 반올림
    const roundedValue = Math.round(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를 jQuery 객체로 변환
    row = $(row);

    // jQuery 객체에서 값을 가져옴
    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.toFixed(2));

    if (roundedAreaPrice > 0) {
        unitPrice = Math.ceil(areaLength * roundedAreaPrice); // 기본 수량 * 단가
        unitPriceInput.val(formatNumber(unitPrice)); // 단가 업데이트
    }

    let totalPrice;
    if (!areaLength && !areaPrice) {
        totalPrice = su * unitPrice;
    } else if (areaLength && !areaPrice) {
        totalPrice = areaLength * unitPrice;
    } else {
        totalPrice = su * unitPrice;
    }
    
    const totalCell = row.find('.total-price');
    if (totalCell.length) {
        totalCell.text(formatNumber(totalPrice));
    }

    return totalPrice;
}

📊 소계 및 총계 계산

// 일련번호별 소계 계산 (jQuery 방식)
function calculateSubtotalBySerial(serialNumber) {
    let subtotal = 0;
    const rows = $(`.calculation-row[data-serial="${serialNumber}"]`);
    
    rows.each(function() {
        subtotal += calculateRowTotal($(this));
    });

    const subtotalCells = $(`.subtotal-cell[data-serial="${serialNumber}"]`);
    if (subtotalCells.length > 0) {
        subtotalCells.each(function() {
            $(this).text(formatNumber(subtotal));
        });
    } else {
        console.error(`소계 셀을 찾을 수 없습니다. 일련번호: ${serialNumber}`);
    }

    return subtotal;
}

// 모든 일련번호별 소계 및 총합계 계산 (jQuery 방식)
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;
}

// 총합계 계산 (jQuery 방식)
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.ceil(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);
    } else {
        console.error("전체 합계 셀을 찾을 수 없습니다. '.grand-total'이라는 클래스가 올바르게 설정되었는지 확인해주세요.");
    }
}

🎨 한국어 숫자 변환

🇰🇷 한국어 숫자 변환 함수

function KoreanNumber(number) {
    const koreanNumbers = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구'];
    const koreanUnits = ['', '십', '백', '천'];
    const bigUnits = ['', '만', '억', '조'];

    let result = '';
    let unitIndex = 0;
    let numberStr = String(number);

    // 숫자가 0인 경우 '영원'을 반환
    if (number == 0) return '영원';

    // 뒤에서부터 4자리씩 끊어서 처리
    while (numberStr.length > 0) {
        let chunk = numberStr.slice(-4); // 마지막 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 + '';
}

📄 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');

    // 강제로 input 요소의 폰트 크기 변경
    var inputs = element.querySelectorAll('input');
    inputs.forEach(function(input) {
        input.style.fontSize = '6px';  // 모든 input 요소에 6px 폰트 크기 적용
    });

    var opt = {
        margin: [15, 8, 17, 8], // Top, right, bottom, left margins
        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).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], // Top, right, bottom, left margins
        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]; // Base64 인코딩된 PDF 데이터 추출
        var formData = new FormData();
        formData.append('pdf', pdfBase64);
        formData.append('filename', result);

        $.ajax({
            type: 'POST',
            url: '/email/save_pdf.php', // 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) {
            console.log('response : ', 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', // input 창을 텍스트 필드로 설정
                    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', // 이메일 전송을 처리하는 PHP 파일
        data: { email: recipientEmail, vendorName: vendorName, filename: filename, item: item, formattedDate: formattedDate },
        success: function(response) {
            console.log(response);
            Swal.fire('Success', '정상적으로 전송되었습니다.', 'success'); 
        },
        error: function(xhr, status, error) {
            Swal.fire('Error', '전송에 실패했습니다. 확인바랍니다.', 'error'); 
        }
    });
}

💾 데이터 저장 기능

💾 견적 데이터 저장

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 = [];
        // 각 tr의 input 요소 순서대로 처리
        $(this).find('input, select').each(function() {
            let value = $(this).val();
            rowData.push(value); // input 값을 배열에 순서대로 추가
        });
        formData.push(rowData); // 각 행의 input 데이터를 배열에 추가
    });

    // formData는 이제 각 행의 input 값들만 포함하는 배열입니다.
    console.log('formData:', formData);

    // 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);
        }
    });
}

🔄 재계산 기능

🔄 견적 데이터 재계산

$(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: '확인'
                        });
                        console.log("AJAX Error: ", status, error);
                    }
                });
            }
        });
    });
});

🎨 UI/UX 기능

👁️ 체크박스 기반 뷰 제어

$(document).ready(function() {
    // 초기 상태에 따라 Estimateview와 listview를 보여주기/숨기기 설정
    if ($('#showEstimateCheckbox').is(':checked')) {
        $('.Estimateview').show();
    } else {
        $('.Estimateview').hide();
    }

    if ($('#showlistCheckbox').is(':checked')) {
        $('.listview').show();
    } else {
        $('.listview').hide();
    }

    // showEstimateCheckbox 체크박스 변경 시 Estimateview 보여주기/숨기기 설정
    $('#showEstimateCheckbox').on('change', function() {
        if ($(this).is(':checked')) {
            $('.Estimateview').show();
        } else {
            $('.Estimateview').hide();
        }
    });

    // showlistCheckbox 체크박스 변경 시 listview 보여주기/숨기기 설정
    $('#showlistCheckbox').on('change', function() {
        if ($(this).is(':checked')) {
            $('.listview').show();
        } else {
            $('.listview').hide();
        }
    });
});

📊 동적 행 스팬 설정

$(document).ready(function () {
    var rowArray = <?= isset($row_array_json) ? $row_array_json : '[]' ?>;

    if (Array.isArray(rowArray) && rowArray.length > 0) {
        rowArray.forEach(function (rowspanValue, index) {
            var cell = document.getElementById('dynamicRowspan-' + index);
            var cellCompare = document.getElementById('dynamicRowspanCompare-' + index);
            if (cell && rowspanValue > 0) {
                cell.setAttribute('rowspan', rowspanValue);
            }
            if (cellCompare && rowspanValue > 0) {
                cellCompare.setAttribute('rowspan', rowspanValue);
            }
        });
    }
});

🔧 개발자 사용법

📝 기본 사용법

// 페이지 로드 시 자동 실행
$(document).ready(function() {
    // 데이터 로딩 및 초기화
    loadTableData('#detailTable', dataList);
    
    // 이벤트 리스너 설정
    setupEventListeners();
    
    // 초기 계산 실행
    calculateAllSubtotals();
    calculateGrandTotal();
});

🧮 계산 함수 호출

// 행별 합계 계산
calculateRowTotal($(row));

// 소계 계산
calculateSubtotalBySerial(serialNumber);

// 총계 계산
calculateGrandTotal();

📄 PDF 생성

// 클라이언트 사이드 PDF 생성
generatePDF();

// 서버 사이드 PDF 생성 (이메일용)
generatePDF_server(function(filename) {
    // PDF 생성 완료 후 콜백 실행
    console.log('PDF 생성 완료:', filename);
});

📧 이메일 전송

// 이메일 전송 시작
sendmail();

🚨 주의사항

⚠️ 필수 의존성

  • jQuery 3.x
  • html2pdf.js
  • SweetAlert2
  • Bootstrap 5.x

🔒 보안 고려사항

  • AJAX 요청 시 CSRF 토큰 검증
  • 입력값 검증 및 이스케이프 처리
  • 파일 업로드 보안 검증

📱 성능 최적화

  • 대용량 데이터 처리 시 성능 고려
  • AJAX 요청 타임아웃 설정
  • 메모리 누수 방지

🐛 디버깅 가이드

🔍 일반적인 문제 해결

1. 데이터 로딩 오류

// JSON 파싱 오류 확인
console.log('dataList:', dataList);
console.log('dataList type:', typeof dataList);

// 배열 확인
if (!Array.isArray(dataList)) {
    console.error('dataList is not an array');
    dataList = [];
}

2. 계산 오류

// 계산 함수 디버깅
function calculateRowTotal(row) {
    console.log('Row element:', row);
    console.log('Row data:', {
        su: cleanNumber(row.find('.su-input').val()),
        areaLength: cleanNumber(row.find('.area-length-input').val()),
        areaPrice: cleanNumber(row.find('.area-price-input').val()),
        unitPrice: cleanNumber(row.find('.unit-price-input').val())
    });
    
    // ... 계산 로직
}

3. AJAX 오류

// AJAX 요청 디버깅
$.ajax({
    // ... 설정
    success: function(data) {
        console.log('Success response:', data);
    },
    error: function(jqxhr, status, error) {
        console.error('AJAX Error:', {
            status: status,
            error: error,
            responseText: jqxhr.responseText
        });
    }
});

📚 관련 파일

🔗 의존성 파일

  • jQuery: JavaScript 라이브러리
  • html2pdf.js: PDF 생성 라이브러리
  • SweetAlert2: 알림창 라이브러리
  • Bootstrap: UI 프레임워크

🔗 연관 파일

  • insert_detail.php: 견적 데이터 저장 처리
  • get_companyCode.php: 회사 코드 조회
  • save_pdf.php: PDF 파일 저장
  • send_email.php: 이메일 전송 처리

🔗 PHP 변수

  • $detailJson: 견적 상세 데이터
  • $shutterboxMsg: 셔터박스 오류 메시지
  • $row_array_json: 행 스팬 데이터
  • $title_message: 제목 메시지
  • $outworkplace: 작업장 정보
  • $indate: 입력 날짜
  • $secondordnum: 발주처 코드
  • $emailTitle: 이메일 제목

🎯 향후 개선 방향

🔄 코드 리팩토링

  • 함수 분리 및 모듈화
  • 클래스 기반 구조로 변경
  • TypeScript 도입 고려

🎨 UI/UX 개선

  • 실시간 계산 표시
  • 드래그 앤 드롭 기능
  • 반응형 디자인 개선

성능 최적화

  • 가상 스크롤링 도입
  • 지연 로딩 구현
  • 메모리 사용량 최적화

🔧 기능 확장

  • 다중 파일 업로드
  • 실시간 협업 기능
  • 모바일 앱 연동

📅 문서 버전: 1.0
👨‍💻 작성자: 개발팀
📝 최종 수정일: 2024-12-24
🔗 관련 문서: 견적 시스템 전체 가이드, common_addrowJS 개발자 가이드, common_screen 개발자 가이드, common_slat 개발자 가이드