/*
* 파일명: 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;
}