/* * 파일명: calculator.js * 위치: /assets/js/calculator.js * 기능: 대출이자 계산기 로직 처리 * 작성일: 2025-03-31 * 수정일: 2025-04-02 */ // =================================== // 초기화 및 이벤트 바인딩 // =================================== document.addEventListener('DOMContentLoaded', function() { const loanForm = document.getElementById('loanForm'); const graceCheck = document.getElementById('graceCheck'); const gracePeriodSection = document.getElementById('gracePeriodSection'); const gracePeriodInput = document.getElementById('gracePeriod'); const scrollToTopBtn = document.getElementById('scrollToTopBtn'); const paymentMethodOptions = document.querySelectorAll('.payment-method-option'); // 폼 제출 이벤트 리스너 loanForm.addEventListener('submit', function(e) { e.preventDefault(); calculateLoan(); // 결과 영역 표시 document.getElementById('resultSection').style.display = 'block'; // 결과 섹션으로 스크롤 document.getElementById('resultSection').scrollIntoView({ behavior: 'smooth' }); }); // 거치기간 체크박스 이벤트 리스너 graceCheck.addEventListener('change', function() { gracePeriodSection.style.display = this.checked ? 'block' : 'none'; gracePeriodInput.disabled = !this.checked; if (!this.checked) { gracePeriodInput.value = ''; } }); // 빠른 금액 선택 버튼 이벤트 리스너 const quickAmountButtons = document.querySelectorAll('.quick-amount-buttons button'); quickAmountButtons.forEach(button => { button.addEventListener('click', function() { const amount = parseInt(this.getAttribute('data-amount')); const loanAmountInput = document.getElementById('loanAmount'); // 기존 값이 있는지 확인 if (loanAmountInput.value) { // 기존 값에 콤마가 있으면 제거하고 숫자로 변환 const currentAmount = parseInt(unformatNumber(loanAmountInput.value)); // 기존 금액과 선택한 금액을 합산 const newAmount = currentAmount + amount; // 합산된 금액을 콤마 포맷으로 표시 loanAmountInput.value = formatNumber(newAmount); } else { // 기존 값이 없으면 선택한 금액만 표시 loanAmountInput.value = formatNumber(amount); } }); }); // 빠른 이자율 선택 버튼 이벤트 리스너 const quickRateButtons = document.querySelectorAll('.quick-rate-buttons button'); quickRateButtons.forEach(button => { button.addEventListener('click', function() { const rate = parseFloat(this.getAttribute('data-rate')); document.getElementById('interestRate').value = rate.toFixed(1); }); }); // 빠른 기간 선택 버튼 이벤트 리스너 const quickTermButtons = document.querySelectorAll('.quick-term-buttons button'); quickTermButtons.forEach(button => { button.addEventListener('click', function() { const term = parseInt(this.getAttribute('data-term')); document.getElementById('loanTerm').value = term; document.getElementById('termType').value = 'year'; }); }); // 빠른 거치기간 선택 버튼 이벤트 리스너 const quickGraceButtons = document.querySelectorAll('.quick-grace-buttons button'); quickGraceButtons.forEach(button => { button.addEventListener('click', function() { const grace = parseInt(this.getAttribute('data-grace')); document.getElementById('gracePeriod').value = grace; document.getElementById('graceType').value = 'year'; }); }); // 초기화 버튼 이벤트 리스너 document.getElementById('resetAmount').addEventListener('click', function() { document.getElementById('loanAmount').value = ''; }); document.getElementById('resetRate').addEventListener('click', function() { document.getElementById('interestRate').value = ''; }); document.getElementById('resetTerm').addEventListener('click', function() { document.getElementById('loanTerm').value = ''; }); document.getElementById('resetGrace').addEventListener('click', function() { document.getElementById('gracePeriod').value = ''; }); // 입력값에 자동 콤마 추가 및 최대값 제한 const loanAmountInput = document.getElementById('loanAmount'); loanAmountInput.addEventListener('input', function() { const value = this.value.replace(/[^\d]/g, ''); // 최대 15자리로 제한 (수백조 단위까지) const truncatedValue = value.slice(0, 15); if (truncatedValue) { this.value = formatNumber(parseInt(truncatedValue)); } else { this.value = ''; } }); // 이자율 입력 처리 (소수점 허용 및 최대값 제한) const interestRateInput = document.getElementById('interestRate'); interestRateInput.addEventListener('input', function() { // 숫자와 소수점만 허용 (소수점은 하나만) this.value = this.value.replace(/[^\d.]/g, ''); const parts = this.value.split('.'); if (parts.length > 2) { this.value = parts[0] + '.' + parts.slice(1).join(''); } // 최대 100%로 제한 const numValue = parseFloat(this.value); if (!isNaN(numValue) && numValue > 100) { this.value = '100'; } }); // 기간 입력 처리 (정수만 허용 및 최대값 제한) const loanTermInput = document.getElementById('loanTerm'); loanTermInput.addEventListener('input', function() { this.value = this.value.replace(/[^\d]/g, ''); // 최대 30년(360개월)으로 제한 const numValue = parseInt(this.value); if (!isNaN(numValue)) { const maxTerm = document.getElementById('termType').value === 'year' ? 30 : 360; if (numValue > maxTerm) { this.value = maxTerm.toString(); } } }); const gracePeriodInputField = document.getElementById('gracePeriod'); gracePeriodInputField.addEventListener('input', function() { this.value = this.value.replace(/[^\d]/g, ''); // 최대 10년(120개월)으로 제한 const numValue = parseInt(this.value); if (!isNaN(numValue)) { const maxGrace = document.getElementById('graceType').value === 'year' ? 10 : 120; if (numValue > maxGrace) { this.value = maxGrace.toString(); } } }); // 기간 타입 변경 이벤트 리스너 document.getElementById('termType').addEventListener('change', function() { const loanTermInput = document.getElementById('loanTerm'); const numValue = parseInt(loanTermInput.value); if (!isNaN(numValue)) { const maxTerm = this.value === 'year' ? 30 : 360; if (numValue > maxTerm) { loanTermInput.value = maxTerm.toString(); } } }); // 거치기간 타입 변경 이벤트 리스너 document.getElementById('graceType').addEventListener('change', function() { const gracePeriodInput = document.getElementById('gracePeriod'); const numValue = parseInt(gracePeriodInput.value); if (!isNaN(numValue)) { const maxGrace = this.value === 'year' ? 10 : 120; if (numValue > maxGrace) { gracePeriodInput.value = maxGrace.toString(); } } }); // 상환방식 선택 처리 paymentMethodOptions.forEach(option => { option.addEventListener('click', function() { // 모든 옵션에서 active 클래스 제거 paymentMethodOptions.forEach(opt => opt.classList.remove('active')); // 선택한 옵션에 active 클래스 추가 this.classList.add('active'); // 해당 라디오 버튼 선택 const methodValue = this.getAttribute('data-method'); document.querySelector(`input[value="${methodValue}"]`).checked = true; }); }); // 기본 상환방식 선택 (원리금균등) document.querySelector('.payment-method-option[data-method="equalPrincipalAndInterest"]').classList.add('active'); // 스크롤 이벤트 리스너 - 맨 위로 스크롤 버튼 표시/숨김 window.addEventListener('scroll', function() { if (window.scrollY > 300) { scrollToTopBtn.classList.add('visible'); } else { scrollToTopBtn.classList.remove('visible'); } }); // 맨 위로 스크롤 버튼 클릭 이벤트 scrollToTopBtn.addEventListener('click', function() { window.scrollTo({ top: 0, behavior: 'smooth' }); }); }); // =================================== // 유틸리티 함수 // =================================== /* 숫자 형식 함수 */ function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } /* 콤마 제거 함수 */ function unformatNumber(str) { return str.replace(/,/g, ''); } /* 통화 포맷 함수 */ function formatCurrency(amount) { return formatNumber(Math.round(amount)) + '원'; } /* 방식명 변환 함수 */ function getMethodName(methodValue) { const methods = { 'equalPrincipalAndInterest': '원리금균등상환', 'equalPrincipal': '원금균등상환', 'bulletPayment': '만기일시상환' }; return methods[methodValue] || methodValue; } // =================================== // 계산 함수 // =================================== /* 대출 계산 함수 */ function calculateLoan() { // 입력값 가져오기 const loanAmountStr = document.getElementById('loanAmount').value; const interestRateStr = document.getElementById('interestRate').value; const loanTermStr = document.getElementById('loanTerm').value; const termType = document.getElementById('termType').value; const paymentMethod = document.querySelector('input[name="paymentMethod"]:checked').value; // 거치기간 확인 const hasGracePeriod = document.getElementById('graceCheck').checked; let gracePeriodMonths = 0; if (hasGracePeriod) { const gracePeriodStr = document.getElementById('gracePeriod').value; const graceType = document.getElementById('graceType').value; if (gracePeriodStr) { const gracePeriod = parseInt(gracePeriodStr); gracePeriodMonths = graceType === 'year' ? gracePeriod * 12 : gracePeriod; } } // 입력값 검증 if (!loanAmountStr || !interestRateStr || !loanTermStr) { alert('모든 필수 항목을 입력해 주세요.'); return; } // 입력값 변환 const loanAmount = parseFloat(unformatNumber(loanAmountStr)); const interestRate = parseFloat(interestRateStr) / 100; // 백분율을 소수로 변환 const loanTerm = parseInt(loanTermStr); if (isNaN(loanAmount) || isNaN(interestRate) || isNaN(loanTerm)) { alert('올바른 숫자 형식으로 입력해 주세요.'); return; } // 입력값 범위 추가 검증 if (loanAmount <= 0) { alert('대출 금액은 0보다 커야 합니다.'); return; } if (interestRate <= 0 || interestRate > 1) { alert('이자율은 0%보다 크고 100% 이하여야 합니다.'); return; } if (loanTerm <= 0) { alert('대출 기간은 0보다 커야 합니다.'); return; } const termInMonths = termType === 'year' ? loanTerm * 12 : loanTerm; // 상환 기간이 너무 길 경우 제한 if (termInMonths > 360) { alert('대출 기간은 최대 30년(360개월)까지 가능합니다.'); return; } const monthlyInterestRate = interestRate / 12; // 각 상환 방식별 계산 결과 let equalPrincipalAndInterestResult = calculateEqualPrincipalAndInterest(loanAmount, monthlyInterestRate, termInMonths, gracePeriodMonths); let equalPrincipalResult = calculateEqualPrincipal(loanAmount, monthlyInterestRate, termInMonths, gracePeriodMonths); let bulletPaymentResult = calculateBulletPayment(loanAmount, monthlyInterestRate, termInMonths, gracePeriodMonths); // 계산 결과를 객체로 구성 const results = { equalPrincipalAndInterest: equalPrincipalAndInterestResult, equalPrincipal: equalPrincipalResult, bulletPayment: bulletPaymentResult }; // 선택된 방식의 결과 표시 displayResults(results[paymentMethod], paymentMethod); // 상환 방식 비교 차트 표시 displayComparisonCharts(results); // 신청 정보 요약 표시 displaySummary(loanAmount, interestRate, loanTerm, termType, gracePeriodMonths, paymentMethod); // 결과 섹션으로 스크롤 document.getElementById('resultSection').scrollIntoView({ behavior: 'smooth' }); } /* 원리금균등상환 계산 함수 */ function calculateEqualPrincipalAndInterest(loanAmount, monthlyInterestRate, termInMonths, gracePeriodMonths) { // 전체 상환 기간 (거치기간 포함) const totalMonths = termInMonths; // 거치기간을 별도로 더하지 않음 // 실제 원금 상환 기간 const repaymentMonths = totalMonths - gracePeriodMonths; if (repaymentMonths <= 0) { alert('거치기간이 대출기간보다 길 수 없습니다.'); return null; } // 상환 일정표 생성 let remainingBalance = loanAmount; let totalInterest = 0; let totalPayment = 0; let paymentSchedule = []; // 월 이자 금액 계산 (정확한 이자 계산) const monthlyInterestAmount = Math.round(loanAmount * monthlyInterestRate); // 거치기간 동안은 이자만 납부 for (let i = 1; i <= gracePeriodMonths; i++) { const interestPayment = monthlyInterestAmount; const payment = interestPayment; totalInterest += interestPayment; totalPayment += payment; paymentSchedule.push({ period: i, payment: payment, principal: 0, interest: interestPayment, balance: remainingBalance }); } // 원리금균등상환 월 납입금 계산 (공식 적용) // PMT = P * r * (1+r)^n / ((1+r)^n - 1) // P: 원금, r: 월 이자율, n: 상환 개월 수 const n = repaymentMonths; const r = monthlyInterestRate; const powFactor = Math.pow(1 + r, n); const monthlyPayment = Math.round(loanAmount * r * powFactor / (powFactor - 1)); // 상환 기간 동안의 원리금균등상환 remainingBalance = loanAmount; // 거치기간 후 원금은 그대로 for (let i = gracePeriodMonths + 1; i <= totalMonths; i++) { const interestPayment = Math.round(remainingBalance * monthlyInterestRate); const principalPayment = monthlyPayment - interestPayment; totalInterest += interestPayment; totalPayment += monthlyPayment; remainingBalance -= principalPayment; // 마지막 달에 남은 원금이 0이 아니면 조정 if (i === totalMonths && Math.abs(remainingBalance) > 0.01) { const adjustedPrincipal = principalPayment + remainingBalance; const finalPayment = adjustedPrincipal + interestPayment; paymentSchedule.push({ period: i, payment: finalPayment, principal: adjustedPrincipal, interest: interestPayment, balance: 0 }); } else { paymentSchedule.push({ period: i, payment: monthlyPayment, principal: principalPayment, interest: interestPayment, balance: Math.max(0, Math.round(remainingBalance)) }); } } return { monthlyPayment: gracePeriodMonths > 0 ? paymentSchedule[0].payment : monthlyPayment, repaymentAfterGrace: monthlyPayment, // 거치기간 후 월납입금 totalPayment: totalPayment, totalInterest: totalInterest, paymentSchedule: paymentSchedule, paymentTrend: paymentSchedule.map(item => ({ period: item.period, payment: item.payment })) }; } /* 원금균등상환 계산 함수 */ function calculateEqualPrincipal(loanAmount, monthlyInterestRate, termInMonths, gracePeriodMonths) { // 전체 상환 기간 (거치기간 포함) const totalMonths = termInMonths; // 거치기간을 별도로 더하지 않음 // 실제 원금 상환 기간 const repaymentMonths = totalMonths - gracePeriodMonths; if (repaymentMonths <= 0) { alert('거치기간이 대출기간보다 길 수 없습니다.'); return null; } // 월 원금상환액 계산 (거치기간 이후) const monthlyPrincipal = Math.round(loanAmount / repaymentMonths); // 상환 일정표 생성 let remainingBalance = loanAmount; let totalInterest = 0; let totalPayment = 0; let paymentSchedule = []; // 거치 기간 동안은 이자만 납부 for (let i = 1; i <= gracePeriodMonths; i++) { const interestPayment = Math.round(remainingBalance * monthlyInterestRate); const payment = interestPayment; totalInterest += interestPayment; totalPayment += payment; paymentSchedule.push({ period: i, payment: payment, principal: 0, interest: interestPayment, balance: remainingBalance }); } // 상환 기간 동안의 원금균등상환 for (let i = gracePeriodMonths + 1; i <= totalMonths; i++) { const interestPayment = Math.round(remainingBalance * monthlyInterestRate); const payment = monthlyPrincipal + interestPayment; totalInterest += interestPayment; totalPayment += payment; remainingBalance -= monthlyPrincipal; // 마지막 달에 남은 원금이 0이 아니면 조정 if (i === totalMonths && Math.abs(remainingBalance) > 0.01) { const adjustedPrincipal = monthlyPrincipal + remainingBalance; const finalPayment = adjustedPrincipal + interestPayment; remainingBalance = 0; paymentSchedule.push({ period: i, payment: finalPayment, principal: adjustedPrincipal, interest: interestPayment, balance: 0 }); } else { paymentSchedule.push({ period: i, payment: payment, principal: monthlyPrincipal, interest: interestPayment, balance: remainingBalance < 0 ? 0 : Math.round(remainingBalance) }); } } return { monthlyPayment: gracePeriodMonths > 0 ? paymentSchedule[0].payment : paymentSchedule[0].payment, totalPayment: totalPayment, totalInterest: totalInterest, paymentSchedule: paymentSchedule, paymentTrend: paymentSchedule.map(item => ({ period: item.period, payment: item.payment })) }; } /* 만기일시상환 계산 함수 */ function calculateBulletPayment(loanAmount, monthlyInterestRate, termInMonths, gracePeriodMonths) { // 전체 상환 기간 (거치기간 포함) const totalMonths = termInMonths; // 거치기간을 별도로 더하지 않음 // 월 이자 계산 const monthlyInterestPayment = Math.round(loanAmount * monthlyInterestRate); // 상환 일정표 생성 let totalInterest = 0; let totalPayment = 0; let paymentSchedule = []; // 만기까지 이자만 납부 for (let i = 1; i < totalMonths; i++) { totalInterest += monthlyInterestPayment; totalPayment += monthlyInterestPayment; paymentSchedule.push({ period: i, payment: monthlyInterestPayment, principal: 0, interest: monthlyInterestPayment, balance: loanAmount }); } // 만기에 원금 + 이자 납부 totalInterest += monthlyInterestPayment; totalPayment += (loanAmount + monthlyInterestPayment); paymentSchedule.push({ period: totalMonths, payment: loanAmount + monthlyInterestPayment, principal: loanAmount, interest: monthlyInterestPayment, balance: 0 }); return { monthlyPayment: monthlyInterestPayment, totalPayment: totalPayment, totalInterest: totalInterest, paymentSchedule: paymentSchedule, paymentTrend: paymentSchedule.map(item => ({ period: item.period, payment: item.payment })) }; } // =================================== // 결과 표시 함수 // =================================== /* 결과 표시 함수 */ function displayResults(result, methodValue) { // 기존 대출 상환 요약 카드 내용 수정 const summaryCardBody = document.querySelector('.card-header.bg-success.text-white').nextElementSibling; // 상환원금과 이자의 평균 계산 let totalPrincipal = 0; let totalInterest = 0; let repaymentMonths = 0; let gracePeriodMonths = 0; let repaymentPeriodMonths = 0; // 모든 회차의 원금과 이자 합산 및 거치기간 확인 result.paymentSchedule.forEach(payment => { totalPrincipal += payment.principal; totalInterest += payment.interest; repaymentMonths++; // 원금이 0인 경우는 거치기간 if (payment.principal === 0) { gracePeriodMonths++; } else { repaymentPeriodMonths++; } }); // 만기일시상환의 경우 마지막 회차만 원금 상환이므로 거치기간 계산 조정 if (methodValue === 'bulletPayment') { gracePeriodMonths = repaymentMonths - 1; repaymentPeriodMonths = 1; } // 거치기간과 상환기간 동안의 월 납입금 계산 let avgGracePeriodPayment = 0; let avgRepaymentPeriodPrincipal = 0; let avgRepaymentPeriodInterest = 0; if (gracePeriodMonths > 0) { // 거치기간 동안의 이자 (대체로 동일) avgGracePeriodPayment = result.paymentSchedule[0].payment; } // 상환기간 동안의 평균 원금과 이자 if (repaymentPeriodMonths > 0) { let repaymentPeriodPrincipal = 0; let repaymentPeriodInterest = 0; for (let i = gracePeriodMonths; i < result.paymentSchedule.length; i++) { repaymentPeriodPrincipal += result.paymentSchedule[i].principal; repaymentPeriodInterest += result.paymentSchedule[i].interest; } avgRepaymentPeriodPrincipal = repaymentPeriodPrincipal / repaymentPeriodMonths; avgRepaymentPeriodInterest = repaymentPeriodInterest / repaymentPeriodMonths; } // 대출 상환 설명 텍스트 생성 const methodName = getMethodName(methodValue); let descriptionText = ''; // 상환 방식에 따른 설명 다르게 구성 if (methodValue === 'equalPrincipalAndInterest') { if (gracePeriodMonths > 0) { descriptionText = `거치기간 ${gracePeriodMonths}개월 동안은 이자만 납부하고 나머지 ${repaymentPeriodMonths}개월 동안 매월 동일한 금액 상환`; } else { descriptionText = `${repaymentMonths}개월 동안 매월 동일한 금액 상환`; } } else if (methodValue === 'equalPrincipal') { if (gracePeriodMonths > 0) { descriptionText = `거치기간 ${gracePeriodMonths}개월 동안은 이자만 납부하고 나머지 ${repaymentPeriodMonths}개월 동안 매월 동일한 원금에 이자 추가 상환`; } else { descriptionText = `${repaymentMonths}개월 동안 매월 동일한 원금에 이자 추가 상환`; } } else if (methodValue === 'bulletPayment') { if (repaymentMonths > 1) { descriptionText = `${repaymentMonths-1}개월 동안 매월 이자만 납부하고 만기 월에 이자와 원금을 한번에 상환`; } else { descriptionText = `만기에 이자와 원금을 한번에 상환`; } } // 대출 상환 요약 내용 구성 let summaryHTML = ''; if (gracePeriodMonths > 0 && methodValue !== 'bulletPayment') { // 거치기간이 있는 경우 summaryHTML = `
${descriptionText}
거치기간 납입 정보
거치기간 월 납입금
${formatCurrency(avgGracePeriodPayment)}
상환기간 납입 정보
월평균 상환원금
${formatCurrency(avgRepaymentPeriodPrincipal)}
월평균 이자액
${formatCurrency(avgRepaymentPeriodInterest)}
월평균 납입금
${formatCurrency(avgRepaymentPeriodPrincipal + avgRepaymentPeriodInterest)}
총 납입 정보
총 대출금액
${formatCurrency(totalPrincipal)}
총 이자
${formatCurrency(totalInterest)}
총 상환금액
${formatCurrency(totalPrincipal + totalInterest)}
`; } else if (methodValue === 'bulletPayment') { // 만기일시상환인 경우 (특별 처리) const monthlyInterest = result.paymentSchedule[0].interest; const finalPayment = result.paymentSchedule[result.paymentSchedule.length-1].payment; summaryHTML = `
${descriptionText}
월납입 정보
월 이자 납입금
${formatCurrency(monthlyInterest)}
만기 납입 정보
만기 상환원금
${formatCurrency(totalPrincipal)}
만기 이자
${formatCurrency(monthlyInterest)}
만기 납입금
${formatCurrency(finalPayment)}
총 납입 정보
총 대출금액
${formatCurrency(totalPrincipal)}
총 이자
${formatCurrency(totalInterest)}
총 상환금액
${formatCurrency(totalPrincipal + totalInterest)}
`; } else { // 거치기간이 없는 경우 const avgPrincipal = totalPrincipal / repaymentMonths; const avgInterest = totalInterest / repaymentMonths; summaryHTML = `
${descriptionText}
월평균 납입 정보
월평균 상환원금
${formatCurrency(avgPrincipal)}
월평균 이자액
${formatCurrency(avgInterest)}
월평균 납입금
${formatCurrency(avgPrincipal + avgInterest)}
총 납입 정보
총 대출금액
${formatCurrency(totalPrincipal)}
총 이자
${formatCurrency(totalInterest)}
총 상환금액
${formatCurrency(totalPrincipal + totalInterest)}
`; } summaryCardBody.innerHTML = summaryHTML; // 대출 상환 요약 헤더 수정 const summaryCardHeader = document.querySelector('.card-header.bg-success.text-white'); summaryCardHeader.style.backgroundColor = '#198754'; // 초록색으로 변경 const summaryCardTitle = summaryCardHeader.querySelector('.card-title'); summaryCardTitle.textContent = '대출 상환 요약'; // 상환 일정표 생성 const scheduleTable = document.getElementById('paymentSchedule'); scheduleTable.innerHTML = ''; // 테이블 초기화 result.paymentSchedule.forEach(payment => { const row = document.createElement('tr'); // 거치기간 행은 배경색 다르게 표시 if (payment.principal === 0 && payment.period <= gracePeriodMonths) { row.classList.add('bg-light'); } // 마지막 회차 (만기일시상환인 경우)는 강조 표시 if (methodValue === 'bulletPayment' && payment.period === result.paymentSchedule.length) { row.classList.add('table-primary'); } // 회차 const periodCell = document.createElement('td'); periodCell.textContent = payment.period; row.appendChild(periodCell); // 납입금 const paymentCell = document.createElement('td'); paymentCell.textContent = formatCurrency(payment.payment); row.appendChild(paymentCell); // 원금 const principalCell = document.createElement('td'); principalCell.textContent = formatCurrency(payment.principal); row.appendChild(principalCell); // 이자 const interestCell = document.createElement('td'); interestCell.textContent = formatCurrency(payment.interest); row.appendChild(interestCell); // 남은 원금 const balanceCell = document.createElement('td'); balanceCell.textContent = formatCurrency(payment.balance); row.appendChild(balanceCell); scheduleTable.appendChild(row); }); // 결과 다운로드 버튼 동적 생성 createDownloadButtons(); // 결과 섹션 표시 document.getElementById('resultSection').style.display = 'block'; } /* 다운로드 버튼 생성 함수 */ function createDownloadButtons() { // 상환 일정표 카드 요소 찾기 const scheduleCard = document.querySelector('.card-header.bg-secondary.text-white').closest('.card'); // 이미 버튼이 있는지 확인 let downloadBtns = document.querySelector('.d-flex.justify-content-end.mt-3.mb-4'); if (downloadBtns) { return; // 이미 버튼이 있으면 생성하지 않음 } // 다운로드 버튼 생성 downloadBtns = document.createElement('div'); downloadBtns.className = 'd-flex justify-content-end mt-3 mb-4'; downloadBtns.innerHTML = ` `; // 상환 일정표 카드 앞에 버튼 삽입 scheduleCard.parentNode.insertBefore(downloadBtns, scheduleCard); // 버튼에 이벤트 리스너 추가 document.getElementById('downloadImage').addEventListener('click', function() { downloadResultAsImage(); }); document.getElementById('downloadPDF').addEventListener('click', function() { downloadResultAsPDF(); }); } /* 이미지로 다운로드 함수 */ function downloadResultAsImage() { // HTML2Canvas 라이브러리가 로드되었는지 확인 if (typeof html2canvas === 'undefined') { alert('이미지 변환 라이브러리가 로드되지 않았습니다. 페이지를 새로고침 후 다시 시도해주세요.'); console.error('html2canvas 라이브러리가 로드되지 않았습니다.'); return; } // 로딩 표시 showLoading(); // 결과 섹션 가져오기 const resultSection = document.getElementById('resultSection'); // 다운로드 버튼 임시 숨기기 const downloadBtns = document.querySelector('.d-flex.justify-content-end.mt-3.mb-4'); if (downloadBtns) { downloadBtns.style.visibility = 'hidden'; } // 광고 영역 임시 숨기기 const adElement = document.querySelector('.card.shadow-sm.mb-4:last-child'); if (adElement) { adElement.style.visibility = 'hidden'; } // html2canvas로 캡처 html2canvas(resultSection, { scale: 1.5, backgroundColor: '#ffffff', useCORS: true, allowTaint: true, onclone: function(clonedDoc) { // 복제된 문서에서 불필요한 요소 완전히 제거 const clonedBtns = clonedDoc.querySelector('.d-flex.justify-content-end.mt-3.mb-4'); if (clonedBtns) clonedBtns.remove(); const clonedAd = clonedDoc.querySelector('.card.shadow-sm.mb-4:last-child'); if (clonedAd) clonedAd.remove(); } }).then(function(canvas) { try { // 이미지 다운로드 const link = document.createElement('a'); link.download = '대출계산결과_' + new Date().toISOString().slice(0, 10) + '.png'; link.href = canvas.toDataURL('image/png'); document.body.appendChild(link); // IE 호환성을 위해 추가 link.click(); document.body.removeChild(link); // 링크 제거 } catch (error) { console.error('이미지 저장 오류:', error); alert('이미지 저장 중 오류가 발생했습니다.'); } finally { // 요소 다시 표시 if (downloadBtns) { downloadBtns.style.visibility = 'visible'; } if (adElement) { adElement.style.visibility = 'visible'; } // 로딩 숨기기 hideLoading(); } }).catch(function(error) { console.error('캡처 오류:', error); alert('이미지 생성 중 오류가 발생했습니다.'); // 요소 다시 표시 if (downloadBtns) { downloadBtns.style.visibility = 'visible'; } if (adElement) { adElement.style.visibility = 'visible'; } // 로딩 숨기기 hideLoading(); }); } /* PDF로 다운로드 함수 */ function downloadResultAsPDF() { // HTML2Canvas와 jsPDF 라이브러리가 로드되었는지 확인 if (typeof html2canvas === 'undefined') { alert('이미지 변환 라이브러리가 로드되지 않았습니다. 페이지를 새로고침 후 다시 시도해주세요.'); console.error('html2canvas 라이브러리가 로드되지 않았습니다.'); return; } if (typeof jsPDF === 'undefined' && typeof jspdf === 'undefined') { alert('PDF 변환 라이브러리가 로드되지 않았습니다. 페이지를 새로고침 후 다시 시도해주세요.'); console.error('jsPDF/jspdf 라이브러리가 로드되지 않았습니다.'); return; } // 로딩 표시 showLoading(); // 결과 섹션 가져오기 const resultSection = document.getElementById('resultSection'); // 다운로드 버튼 임시 숨기기 const downloadBtns = document.querySelector('.d-flex.justify-content-end.mt-3.mb-4'); if (downloadBtns) { downloadBtns.style.visibility = 'hidden'; } // 광고 영역 임시 숨기기 const adElement = document.querySelector('.card.shadow-sm.mb-4:last-child'); if (adElement) { adElement.style.visibility = 'hidden'; } // html2canvas로 캡처 html2canvas(resultSection, { scale: 2, // 해상도 향상을 위해 스케일 증가 backgroundColor: '#ffffff', useCORS: true, allowTaint: true, onclone: function(clonedDoc) { // 복제된 문서에서 불필요한 요소 완전히 제거 const clonedBtns = clonedDoc.querySelector('.d-flex.justify-content-end.mt-3.mb-4'); if (clonedBtns) clonedBtns.remove(); const clonedAd = clonedDoc.querySelector('.card.shadow-sm.mb-4:last-child'); if (clonedAd) clonedAd.remove(); } }).then(function(canvas) { try { // PDF 생성 (jsPDF 객체 생성 방식 다양화) const imgData = canvas.toDataURL('image/png'); let pdf; // jsPDF 라이브러리 버전에 따라 다른 생성 방식 시도 if (typeof jsPDF !== 'undefined') { pdf = new jsPDF('p', 'mm', 'a4'); } else if (typeof jspdf !== 'undefined' && typeof jspdf.jsPDF !== 'undefined') { pdf = new jspdf.jsPDF('p', 'mm', 'a4'); } else { throw new Error('PDF 생성기를 초기화할 수 없습니다.'); } // A4 페이지 크기 (210mm x 297mm) const pdfWidth = 210; const pdfHeight = 297; // 캔버스 비율 계산 const ratio = canvas.width / canvas.height; // PDF에 맞게 이미지 크기 조정 let imgWidth = pdfWidth; let imgHeight = imgWidth / ratio; // 이미지가 PDF 한 페이지보다 큰 경우 여러 페이지로 나누기 let heightLeft = imgHeight; let position = 0; let pageNumber = 1; // 첫 페이지 추가 pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pdfHeight; // 필요한 경우 추가 페이지 생성 while (heightLeft >= 0) { position = -pdfHeight * pageNumber; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pdfHeight; pageNumber++; } pdf.save('대출계산결과_' + new Date().toISOString().slice(0, 10) + '.pdf'); } catch (error) { console.error('PDF 생성 오류:', error); alert('PDF 생성 중 오류가 발생했습니다: ' + error.message); } finally { // 요소 다시 표시 if (downloadBtns) { downloadBtns.style.visibility = 'visible'; } if (adElement) { adElement.style.visibility = 'visible'; } // 로딩 숨기기 hideLoading(); } }).catch(function(error) { console.error('캡처 오류:', error); alert('PDF 생성 중 오류가 발생했습니다.'); // 요소 다시 표시 if (downloadBtns) { downloadBtns.style.visibility = 'visible'; } if (adElement) { adElement.style.visibility = 'visible'; } // 로딩 숨기기 hideLoading(); }); } /* 다운로드 버튼 생성 함수 */ function createDownloadButtons() { // 상환 일정표 카드 요소 찾기 const scheduleCard = document.querySelector('.card-header.bg-secondary.text-white').closest('.card'); // 이미 버튼이 있는지 확인 let downloadBtns = document.querySelector('.d-flex.justify-content-end.mt-3.mb-4'); if (downloadBtns) { return; // 이미 버튼이 있으면 생성하지 않음 } // 다운로드 버튼 생성 downloadBtns = document.createElement('div'); downloadBtns.className = 'd-flex justify-content-end mt-3 mb-4'; downloadBtns.innerHTML = ` `; // 상환 일정표 카드 앞에 버튼 삽입 scheduleCard.parentNode.insertBefore(downloadBtns, scheduleCard); // 버튼에 이벤트 리스너 추가 document.getElementById('downloadImage').addEventListener('click', function() { downloadResultAsImage(); }); document.getElementById('downloadPDF').addEventListener('click', function() { downloadResultAsPDF(); }); } /* 로딩 표시 함수 */ function showLoading() { // 이미 로딩 요소가 있는지 확인 let loadingElement = document.getElementById('loadingOverlay'); if (!loadingElement) { // 로딩 오버레이 생성 loadingElement = document.createElement('div'); loadingElement.id = 'loadingOverlay'; loadingElement.style.position = 'fixed'; loadingElement.style.top = '0'; loadingElement.style.left = '0'; loadingElement.style.width = '100%'; loadingElement.style.height = '100%'; loadingElement.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'; loadingElement.style.display = 'flex'; loadingElement.style.justifyContent = 'center'; loadingElement.style.alignItems = 'center'; loadingElement.style.zIndex = '9999'; // 로딩 스피너 생성 const spinner = document.createElement('div'); spinner.className = 'spinner-border text-primary'; spinner.setAttribute('role', 'status'); spinner.style.width = '3rem'; spinner.style.height = '3rem'; // 로딩 텍스트 생성 const loadingText = document.createElement('div'); loadingText.className = 'ms-3'; loadingText.textContent = '다운로드 준비 중...'; loadingText.style.fontSize = '1.2rem'; // 컨테이너에 요소 추가 const container = document.createElement('div'); container.style.display = 'flex'; container.style.alignItems = 'center'; container.appendChild(spinner); container.appendChild(loadingText); loadingElement.appendChild(container); document.body.appendChild(loadingElement); } else { loadingElement.style.display = 'flex'; } } /* 로딩 숨기기 함수 */ function hideLoading() { const loadingElement = document.getElementById('loadingOverlay'); if (loadingElement) { loadingElement.style.display = 'none'; } } /* 신청 정보 요약 표시 함수 */ function displaySummary(loanAmount, interestRate, loanTerm, termType, gracePeriodMonths, paymentMethod) { document.getElementById('summaryAmount').textContent = formatCurrency(loanAmount); document.getElementById('summaryRate').textContent = (interestRate * 100).toFixed(1) + '%'; // 대출 기간 표시 let termText = ''; if (termType === 'year') { termText = loanTerm + '년 (' + (loanTerm * 12) + '개월)'; } else { termText = loanTerm + '개월'; } document.getElementById('summaryTerm').textContent = termText; // 거치 기간 표시 let graceText = gracePeriodMonths > 0 ? (gracePeriodMonths / 12 >= 1 ? Math.floor(gracePeriodMonths / 12) + '년 ' : '') : '없음'; if (gracePeriodMonths > 0 && gracePeriodMonths % 12 > 0) { graceText += (gracePeriodMonths % 12) + '개월'; } document.getElementById('summaryGrace').textContent = graceText; // 상환 방식 표시 document.getElementById('summaryMethod').textContent = getMethodName(paymentMethod); } /* 상환 방식 비교 차트 표시 함수 */ function displayComparisonCharts(results) { // 월 상환금 비교 차트 displayMonthlyPaymentChart(results); // 총 이자 비교 차트 displayTotalInterestChart(results); // 상환 추이 차트 displayPaymentTrendChart(results); } /* 월 상환금 비교 차트 */ function displayMonthlyPaymentChart(results) { const ctx = document.getElementById('monthlyPaymentChart').getContext('2d'); // 이전 차트 제거 if (window.monthlyPaymentChart instanceof Chart) { window.monthlyPaymentChart.destroy(); } // 거치기간 확인 const hasGracePeriod = results.equalPrincipalAndInterest.paymentSchedule.filter(p => p.principal === 0).length > 0; // 각 납입 정보 계산 const paymentInfo = { equalPrincipalAndInterest: calculatePaymentInfo(results.equalPrincipalAndInterest), equalPrincipal: calculatePaymentInfo(results.equalPrincipal), bulletPayment: calculatePaymentInfo(results.bulletPayment) }; // 차트 데이터 및 옵션 설정 let chartConfig; if (hasGracePeriod) { // 거치기간이 있는 경우: 누적 막대 차트로 표시 const gracePeriodMonths = paymentInfo.equalPrincipalAndInterest.gracePeriodMonths; // 거치기간 납입금 (이자만) const gracePeriodPayments = [ paymentInfo.equalPrincipalAndInterest.gracePeriodPayment, paymentInfo.equalPrincipal.gracePeriodPayment, // 만기일시는 별도 처리 (월 이자) paymentInfo.bulletPayment.monthlyInterest ]; // 상환기간 납입금 (원금 + 이자) const repaymentPeriodPayments = [ paymentInfo.equalPrincipalAndInterest.repaymentPeriodPayment, paymentInfo.equalPrincipal.repaymentPeriodPayment, // 만기일시상환은 0으로 표시 (마지막 달에 원금 상환) 0 ]; chartConfig = { type: 'bar', data: { labels: ['원리금균등', '원금균등', '만기일시'], datasets: [ { label: '거치기간 월 납입금', data: gracePeriodPayments, backgroundColor: 'rgba(255, 206, 86, 0.7)', borderColor: 'rgba(255, 206, 86, 1)', borderWidth: 1, // 커스텀 데이터 저장 gracePeriodMonths: gracePeriodMonths, gracePeriodTotalPayments: [ gracePeriodPayments[0] * gracePeriodMonths, gracePeriodPayments[1] * gracePeriodMonths, // 만기일시는 전체 기간 동안 이자 gracePeriodPayments[2] * results.bulletPayment.paymentSchedule.length ] }, { label: '상환기간 월 납입금', data: repaymentPeriodPayments, backgroundColor: 'rgba(54, 162, 235, 0.7)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1, // 상환기간 정보 저장 repaymentInfo: [ paymentInfo.equalPrincipalAndInterest, paymentInfo.equalPrincipal, paymentInfo.bulletPayment ] } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { tooltip: { callbacks: { label: function(context) { // 데이터셋 인덱스와 데이터 인덱스 const datasetIndex = context.datasetIndex; const dataIndex = context.dataIndex; // 거치기간 월 납입금 (노란색) if (datasetIndex === 0) { // 만기일시상환의 경우 if (dataIndex === 2) { const finalPrincipal = paymentInfo.bulletPayment.finalPrincipal; return [ `거치기간 없는 상품 입니다`, `매월 납입금액: ${formatCurrency(context.raw)}을 납부하다가`, `만기 월에 이자와 원금을 한번에 상환하게 됩니다.` ]; } else { const gracePeriodMonths = context.dataset.gracePeriodMonths; const gracePeriodTotalPayment = context.dataset.gracePeriodTotalPayments[dataIndex]; return [ `거치기간: ${gracePeriodMonths}개월`, `월 납입금액: ${formatCurrency(context.raw)}`, `총 납입금액(거치기간): ${formatCurrency(gracePeriodTotalPayment)}` ]; } } // 상환기간 월 납입금 (파란색) else if (datasetIndex === 1) { const info = context.dataset.repaymentInfo[dataIndex]; if (dataIndex === 2) { // 만기일시상환 return [ `만기 상환원금: ${formatCurrency(info.finalPrincipal)}`, `만기 이자: ${formatCurrency(info.finalInterest)}`, `만기 납입금: ${formatCurrency(info.finalPayment)}` ]; } else { return [ `월평균 상환원금: ${formatCurrency(info.avgPrincipal)}`, `월평균 이자액: ${formatCurrency(info.avgInterest)}`, `월평균 납입금: ${formatCurrency(info.avgPayment)}` ]; } } return formatCurrency(context.raw); } } }, legend: { labels: { generateLabels: function(chart) { // 기본 라벨 생성 const labels = Chart.defaults.plugins.legend.labels.generateLabels(chart); // 만기일시상환의 경우 라벨 수정 if (labels.length > 0) { labels[0].text = labels[0].datasetIndex === 0 ? (hasGracePeriod ? '거치기간 월 납입금' : '월 납입금') : '상환기간 월 납입금'; } return labels; } } } }, scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true, ticks: { callback: function(value) { return formatCurrency(value); } } } } } }; } else { // 거치기간이 없는 경우: 기존 막대 차트 유지 (상환정보 툴팁 업데이트) chartConfig = { type: 'bar', data: { labels: ['원리금균등', '원금균등', '만기일시'], datasets: [{ label: '월 상환금', data: [ results.equalPrincipalAndInterest.monthlyPayment, results.equalPrincipal.monthlyPayment, results.bulletPayment.monthlyPayment ], backgroundColor: [ 'rgba(54, 162, 235, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(255, 206, 86, 0.7)' ], borderColor: [ 'rgba(54, 162, 235, 1)', 'rgba(75, 192, 192, 1)', 'rgba(255, 206, 86, 1)' ], borderWidth: 1, // 상환 정보 저장 paymentInfo: [ paymentInfo.equalPrincipalAndInterest, paymentInfo.equalPrincipal, paymentInfo.bulletPayment ] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { const info = context.dataset.paymentInfo[context.dataIndex]; if (context.dataIndex === 2) { // 만기일시상환 return [ `거치기간 없는 상품 입니다`, `매월 납입금액: ${formatCurrency(context.raw)}을 납부하다가`, `만기 월에 이자와 원금을 한번에 상환하게 됩니다.` ]; } else { return [ `월평균 상환원금: ${formatCurrency(info.avgPrincipal)}`, `월평균 이자액: ${formatCurrency(info.avgInterest)}`, `월평균 납입금: ${formatCurrency(info.avgPayment)}` ]; } } } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return formatCurrency(value); } } } } } }; } // 차트 생성 window.monthlyPaymentChart = new Chart(ctx, chartConfig); // 만기일시상환 설명 추가 (간결하게 조정) const chartContainer = document.querySelector('#monthlyPaymentChart').parentNode; let noteElement = chartContainer.querySelector('.chart-note'); if (!noteElement) { noteElement = document.createElement('div'); noteElement.className = 'chart-note text-center'; chartContainer.appendChild(noteElement); } const lastBulletPayment = results.bulletPayment.paymentSchedule.slice(-1)[0]; noteElement.innerHTML = `※ 만기일시상환: 거치기간이 없는 상품입니다`; } /* 납입 정보 계산 함수 */ function calculatePaymentInfo(result) { const paymentSchedule = result.paymentSchedule; // 원금이 0인 회차는 거치기간 (단, 만기일시상환은 마지막 회차 제외하고 모두 이자만 납부) const lastPeriod = paymentSchedule.length > 0 ? paymentSchedule[paymentSchedule.length-1].period : 0; const gracePeriodItems = paymentSchedule.filter(p => p.principal === 0 && p.period < lastPeriod); const repaymentItems = paymentSchedule.filter(p => p.principal > 0 || p.period === lastPeriod); // 거치기간 정보 const gracePeriodMonths = gracePeriodItems.length; const gracePeriodPayment = gracePeriodItems.length > 0 ? gracePeriodItems[0].payment : 0; const gracePeriodTotalPayment = gracePeriodPayment * gracePeriodMonths; // 상환기간 정보 let repaymentPeriodPayment = 0; let avgPrincipal = 0; let avgInterest = 0; let avgPayment = 0; if (repaymentItems.length > 0) { // 만기일시상환인 경우 마지막 회차만 원금 상환 const isBulletPayment = repaymentItems.length === 1 && repaymentItems[0].principal > 0; if (isBulletPayment) { repaymentPeriodPayment = 0; // 상환기간 납입금은 0 (그래프에 표시 안 함) } else { // 원리금균등/원금균등의 경우 const totalPrincipal = repaymentItems.reduce((sum, item) => sum + item.principal, 0); const totalInterest = repaymentItems.reduce((sum, item) => sum + item.interest, 0); const totalPayment = repaymentItems.reduce((sum, item) => sum + item.payment, 0); avgPrincipal = totalPrincipal / repaymentItems.length; avgInterest = totalInterest / repaymentItems.length; avgPayment = totalPayment / repaymentItems.length; // 첫 상환 회차의 납입금을 대표값으로 사용 repaymentPeriodPayment = repaymentItems[0].payment; } } // 만기일시상환의 경우 마지막 회차 정보 const finalPayment = paymentSchedule.length > 0 ? paymentSchedule[paymentSchedule.length-1].payment : 0; const finalPrincipal = paymentSchedule.length > 0 ? paymentSchedule[paymentSchedule.length-1].principal : 0; const finalInterest = paymentSchedule.length > 0 ? paymentSchedule[paymentSchedule.length-1].interest : 0; // 월 이자 정보 (만기일시상환용) const monthlyInterest = paymentSchedule.length > 0 ? paymentSchedule[0].interest : 0; return { gracePeriodMonths, gracePeriodPayment, gracePeriodTotalPayment, repaymentPeriodPayment, avgPrincipal, avgInterest, avgPayment, finalPayment, finalPrincipal, finalInterest, monthlyInterest }; } /* 총 이자 비교 차트 */ function displayTotalInterestChart(results) { const ctx = document.getElementById('totalInterestChart').getContext('2d'); // 이전 차트 제거 if (window.totalInterestChart instanceof Chart) { window.totalInterestChart.destroy(); } const data = { labels: ['원리금균등', '원금균등', '만기일시'], datasets: [{ label: '총 이자', data: [ results.equalPrincipalAndInterest.totalInterest, results.equalPrincipal.totalInterest, results.bulletPayment.totalInterest ], backgroundColor: [ 'rgba(54, 162, 235, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(255, 206, 86, 0.7)' ], borderColor: [ 'rgba(54, 162, 235, 1)', 'rgba(75, 192, 192, 1)', 'rgba(255, 206, 86, 1)' ], borderWidth: 1 }] }; window.totalInterestChart = new Chart(ctx, { type: 'bar', data: data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { return formatCurrency(context.raw); } } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return formatCurrency(value); } } } } } }); } /* 상환 추이 차트 */ function displayPaymentTrendChart(results) { const ctx = document.getElementById('paymentTrendChart').getContext('2d'); // 이전 차트 제거 if (window.paymentTrendChart instanceof Chart) { window.paymentTrendChart.destroy(); } // 데이터 포인트를 20개로 줄이기 (너무 많으면 차트가 복잡해짐) const equalPrincipalAndInterestTrend = simplifyTrendData(results.equalPrincipalAndInterest.paymentTrend); const equalPrincipalTrend = simplifyTrendData(results.equalPrincipal.paymentTrend); const bulletPaymentTrend = simplifyTrendData(results.bulletPayment.paymentTrend); // 만기일시상환의 마지막 회차 값이 너무 크면 별도 처리 const bulletPaymentData = [...bulletPaymentTrend]; const lastBulletPayment = bulletPaymentData[bulletPaymentData.length - 1]; // 두 개의 데이터셋을 준비 - 하나는 일반 상환금, 하나는 만기일시 마지막 회차용 const regularData = bulletPaymentData.map(item => { // 마지막 회차인 경우 이자 부분만 표시 (원금 제외) if (item.period === lastBulletPayment.period) { // 남은 원금의 이자만 반환 (원리금의 약 1/100 정도의 규모) return item.payment - (results.bulletPayment.paymentSchedule[item.period-1].principal || 0); } return item.payment; }); const data = { labels: equalPrincipalAndInterestTrend.map(item => item.period + '회차'), datasets: [ { label: '원리금균등', data: equalPrincipalAndInterestTrend.map(item => item.payment), borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, fill: false }, { label: '원금균등', data: equalPrincipalTrend.map(item => item.payment), borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.3, fill: false }, { label: '만기일시(이자)', data: regularData, borderColor: 'rgba(255, 206, 86, 1)', backgroundColor: 'rgba(255, 206, 86, 0.1)', tension: 0.3, fill: false } ] }; // 그래프에 참조선 추가 : 만기일시 마지막 회차에 대한 메시지 표시 const lastPayment = results.bulletPayment.paymentSchedule.slice(-1)[0]; window.paymentTrendChart = new Chart(ctx, { type: 'line', data: data, options: { responsive: true, maintainAspectRatio: false, plugins: { tooltip: { callbacks: { label: function(context) { // 만기일시 마지막 회차인 경우 원금 상환 정보 추가 if (context.dataset.label === '만기일시(이자)' && context.dataIndex === regularData.length - 1) { return [ context.dataset.label + ': ' + formatCurrency(context.raw), '만기 원금상환: ' + formatCurrency(lastPayment.principal) ]; } return context.dataset.label + ': ' + formatCurrency(context.raw); } } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return formatCurrency(value); } } } } } }); // 차트 아래에 참고 사항 추가 const chartContainer = document.querySelector('#paymentTrendChart').parentNode; let noteElement = chartContainer.querySelector('.chart-note'); if (!noteElement) { noteElement = document.createElement('div'); noteElement.className = 'chart-note text-center'; chartContainer.appendChild(noteElement); } // noteElement.innerHTML = `※ 만기일시상환의 경우 마지막 회차에 ${formatCurrency(lastPayment.principal)}의 원금을 상환합니다.`; } /* 추이 데이터 단순화 (차트용) */ function simplifyTrendData(data) { if (data.length <= 20) return data; const result = []; const step = Math.ceil(data.length / 20); for (let i = 0; i < data.length; i += step) { if (result.length < 19) { result.push(data[i]); } } // 마지막 항목은 항상 포함 if (data.length > 0 && (result.length === 0 || result[result.length - 1].period !== data[data.length - 1].period)) { result.push(data[data.length - 1]); } return result; }