금리비교 추가
Some checks failed
Close Pull Request / main (pull_request_target) Has been cancelled

This commit is contained in:
yangsh
2026-01-21 12:03:34 +09:00
parent 0feff4ff12
commit 87b8093f92
13 changed files with 2720 additions and 1000 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -640,7 +640,7 @@ $usr_nm = session('usr_nm');
{ data: null, render: fn_prd_render },
{ data: 'rcpt_product_info1' },
<?php if ($usr_level != "45"): ?>
{ data: 'dept_nm' },
{ data: 'dept_nm' },
{ data: 'usr_nm' },
<?php endif; ?>
{ data: 'parcel_out_yn' },
@@ -669,7 +669,7 @@ $usr_nm = session('usr_nm');
if (!rowData) return;
const rcpt_atclno = rowData.rcpt_atclno;
location.href = "<?= site_url('article/dept/detail') ?>/" + rcpt_atclno;
location.href = "<?= site_url('article/receipt/detail') ?>/" + rcpt_atclno;
});

View File

@@ -0,0 +1,998 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<?php
/**
* 문자열/JSON/배열 모두 받아서 "항목 리스트"로 정규화
* - JSON 배열 문자열: '["a","b"]'
* - 개행 문자열: "a\nb"
* - 배열: ["a","b"]
*/
function normalize_lines($v): array
{
if ($v === null)
return [];
// 이미 배열이면 그대로
if (is_array($v)) {
return array_values(array_filter(array_map('trim', $v), fn($x) => $x !== ''));
}
$s = trim((string) $v);
if ($s === '')
return [];
// JSON 배열 문자열이면 decode
if ($s[0] === '[') {
$decoded = json_decode($s, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
return array_values(array_filter(array_map('trim', $decoded), fn($x) => $x !== ''));
}
}
// 개행 기반 분리
$lines = preg_split("/\r\n|\r|\n/", $s);
return array_values(array_filter(array_map('trim', $lines), fn($x) => $x !== ''));
}
/**
* 상세 섹션 렌더
*/
function render_detail_section(string $title, $value): string
{
$lines = normalize_lines($value);
if (empty($lines))
return '';
$html = '<div class="detail-row">';
$html .= '<div class="detail-title">' . esc($title) . '</div>';
$html .= '<ul class="detail-list">';
foreach ($lines as $line) {
$html .= '<li>' . esc($line) . '</li>';
}
$html .= '</ul></div>';
return $html;
}
/**
* 원리금균등상환
* @param float $principal
* @param float $annualRate
* @param int $years
* @return float|int
*/
function calcEqualPaymentMonthly(float $principal, float $annualRate, int $years): float
{
$n = $years * 12;
$r = ($annualRate / 100.0) / 12.0;
if ($n <= 0)
return 0.0;
if (abs($r) < 1e-12)
return $principal / $n;
$pow = pow(1.0 + $r, $n);
return ($principal * $r * $pow) / ($pow - 1.0);
}
/**
* 원금균등상환
* @param float $principal
* @param float $annualRate
* @param int $years
* @return float|int
*/
function calcEqualPrincipalAvgMonthly(float $principal, float $annualRate, int $years): float
{
$n = $years * 12;
$r = ($annualRate / 100.0) / 12.0;
if ($n <= 0)
return 0.0;
// 이자합(원금균등): P*r*(n+1)/2 (매달 원금이 선형 감소)
$totalInterest = $principal * $r * ($n + 1) / 2.0;
$totalPayment = $principal + $totalInterest;
return $totalPayment / $n;
}
function won(float $v): string
{
return number_format((int) round($v));
}
?>
<style>
.product_list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 12px;
}
.product-card {
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 14px;
background: #fff;
}
.title-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.bank_name {
font-weight: 700;
text-decoration: none;
color: #00f;
}
.product_name {
font-weight: 600;
}
.badges {
/*margin-left: auto;*/
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.label_icon {
display: inline-block;
color: #555;
margin: 4px 0 0 5px;
border-radius: 15px;
-webkit-border-radius: 15px;
font-size: 11px;
line-height: 13px;
border: 1px solid #555;
padding: 2px 6px;
vertical-align: top;
font-weight: bold;
}
.rate_detail {
display: flex;
gap: 14px;
margin: 10px 0 0;
}
.rate_detail .rate {
display: flex;
gap: 6px;
align-items: baseline;
}
.rate_detail dt {
color: #6c757d;
font-size: 12px;
}
.rate_detail dd {
margin: 0;
}
.rate_text {
font-size: 16px;
color: #F58027;
}
.text_red {
font-size: 14px;
color: #F58027;
}
.product-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.collapse-box {
margin-top: 12px;
border-top: 1px dashed #e9ecef;
padding-top: 12px;
}
.detail-grid {
display: grid;
gap: 12px;
}
.detail-row {
border: 1px solid #f1f3f5;
border-radius: 10px;
padding: 12px;
background: #fcfcfd;
}
.detail-title {
font-weight: 800;
font-size: 13px;
color: #343a40;
margin-bottom: 6px;
}
.detail-list {
margin: 0;
padding-left: 18px;
color: #495057;
}
.detail-list li {
margin: 3px 0;
}
.detail-foot {
margin-top: 10px;
font-size: 12px;
color: #868e96;
}
table {
text-align: center;
}
.rate-cell {
vertical-align: middle !important;
/* fallback */
padding: 12px 10px;
}
.rate-cell {
display: grid;
flex-direction: column;
justify-content: center;
/* 세로 가운데 */
align-items: flex-start;
/* 왼쪽 정렬 */
height: 100%;
}
.rate-cell span {
line-height: 1.2;
}
.rate-cell p {
margin: 2px 0 0;
line-height: 1.2;
}
/* ===== Owl wrapper ===== */
.owl-wrap {
position: relative;
width: 100%;
max-width: 100%;
min-width: 0;
}
/* Owl 내부 overflow/폭 안정화 */
.owl-wrap .owl-stage-outer {
overflow: hidden;
width: 100%;
max-width: 100%;
}
/* Owl 기본 nav/dots는 우리가 커스텀 컨트롤을 쓰므로 숨김 */
.product-carousel .owl-nav,
.product-carousel .owl-dots {
display: none !important;
}
/* ===== 컨트롤 바 ===== */
.owl-controls-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 14px;
user-select: none;
}
/* 화살표 버튼 */
.owl-controls-bar .owl-btn {
width: 44px;
height: 44px;
border-radius: 999px;
border: 1px solid #dee2e6;
background: #fff;
color: #212529;
font-size: 28px;
/* 아이콘 크기 */
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
cursor: pointer;
}
.owl-controls-bar .owl-btn:hover {
background: #f8f9fa;
}
.owl-controls-bar .owl-btn:active {
transform: translateY(1px);
}
/* 비활성(맨 처음/맨 끝) */
.owl-controls-bar .owl-btn.is-disabled {
opacity: 0.45;
cursor: default;
box-shadow: none;
}
/* 1/N 카운터 */
.owl-controls-bar .owl-counter {
min-width: 74px;
text-align: center;
font-weight: 700;
color: #495057;
}
/* 모바일 터치 영역 살짝 키우기 */
@media (max-width: 576px) {
.owl-controls-bar .owl-btn {
width: 50px;
height: 50px;
font-size: 30px;
}
.owl-controls-bar .owl-counter {
min-width: 84px;
}
}
</style>
<div class="d-grid">
<div class="mb-3 card">
<div class="tabs-lg-alternate card-header">
<ul class="nav nav-justified">
<li class="nav-item">
<a data-bs-toggle="tab" href="#tab-eg9-0" class="active nav-link" data-cd="all">
<div class="widget-number">전체</div>
</a>
</li>
<?php foreach ($place as $p): ?>
<li class="nav-item">
<a data-bs-toggle="tab" href="#tab-eg9-0" class="nav-link" data-cd="<?= $p['cd'] ?>">
<div class="widget-number"><?= $p['cd_nm'] ?></div>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="tab-content">
<div class="tab-pane active" id="tab-eg9-0" role="tabpanel">
<div class="card-body">
<?php
// $data: 상품 리스트 배열이라고 가정
$chunks = array_chunk($data, 5); // 5개씩 슬라이드 1장
?>
<div class="owl-wrap">
<div class="owl-carousel product-carousel">
<?php foreach ($chunks as $pageIdx => $items): ?>
<div class="item">
<ul class="product_list">
<?php foreach ($items as $d): ?>
<li class="product-card">
<div class="product-head">
<div class="title-row">
<a href="<?= esc($d['bank_url'] ?? '#') ?>" class="bank_name" target="_blank" rel="noopener">
<?= esc($d['bank'] ?? '') ?>
</a>
<span class="product_name"><?= esc($d['product'] ?? '') ?></span>
<div class="badges">
<?php if ($d['chg_yn'] == "Y"): ?>
<span class="label_icon">변동금리</span>
<?php endif; ?>
<?php if ($d['split_yn'] == "Y"): ?>
<span class="label_icon">분할상환</span>
<?php endif; ?>
</div>
</div>
<dl class="rate_detail">
<div class="rate">
<dt>최저</dt>
<dd><strong class="rate_text"><?= esc($d['min_rate'] ?? '-') ?>%</strong></dd>
</div>
<div class="rate">
<dt>최고</dt>
<dd><strong class="rate_text"><?= esc($d['max_rate'] ?? '-') ?>%</strong></dd>
</div>
<div class="rate">
<dt>평균</dt>
<dd><strong class="rate_text"><?= esc($d['avg_rate'] ?? '-') ?>%</strong></dd>
</div>
</dl>
</div>
<!-- 액션 버튼 -->
<div class="product-actions">
<button type="button" class="btn btn-sm btn-outline-secondary js-toggle"
data-target="#detail-<?= $d['seq'] ?>">
상세보기
</button>
<button type="button" class="btn btn-sm btn-outline-secondary js-toggle"
data-target="#pay-<?= $d['seq'] ?>">
월평균상환액
</button>
</div>
<!-- 상세 (접기/펼치기) -->
<div class="collapse-box" id="detail-<?= $d['seq'] ?>" hidden>
<div class="detail-grid">
<?= render_detail_section('대출부대비용', $d['extra_fee'] ?? null) ?>
<?= render_detail_section('연체이자율', $d['late_rate'] ?? null) ?>
<?= render_detail_section('가입방식', $d['join_method'] ?? null) ?>
<?= render_detail_section('중도상환수수료', $d['prepay_fee'] ?? null) ?>
<?= render_detail_section('대출한도', $d['loan_limit'] ?? null) ?>
<div class="detail-row">
<div class="detail-title">상품문의</div>
<ul class="detail-list">
<li>
<?= $d['bank'] ?> <?= $d['tel'] ?? '' ?>
</li>
<li><a href="https://m.map.naver.com/search2/search.nhn?query=<?= $d['bank'] ?>"
target="_blank">지점찾기</a>
</li>
</ul>
</div>
</div>
<?php if (!empty($d['prov_date'])): ?>
<div class="detail-foot">금융회사 최종 제공일
<?= esc($d['prov_date']) ?>.
</div>
<?php endif; ?>
</div>
<!-- 월평균상환액 -->
<div class="collapse-box" id="pay-<?= $d['seq'] ?>" hidden>
<?php
$basis = $d['pay_basis'] ?? '1억원 대출, 10년 분할 상환시';
?>
<div class="pay-box">
<div class="detail-row" style="margin-bottom: 15px;">
<ul class="detail-list">
<li>
<?= esc($basis) ?>
</li>
</ul>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th rowspan="2" style="width: 30%;">금리</th>
<th colspan="2">월평균 상환액</th>
</tr>
<tr>
<th style="width: 35%;">원리금 균등상환</th>
<th style="width: 35%;">원금 균등상환</th>
</tr>
</thead>
<tbody>
<tr>
<td class="rate-cell">
<span class="text_black">당월최저</span>
<p><strong class="text_red"><?= $d['min_rate'] ?>%</strong></p>
</td>
<td>
<strong><?= won(calcEqualPaymentMonthly(100000000, $d['min_rate'], 10)) ?></strong>원
</td>
<td><strong>
<?= won(calcEqualPrincipalAvgMonthly(100000000, $d['min_rate'], 10)) ?>
</strong>원
</td>
</tr>
<tr>
<td class="rate-cell">
<span class="text_black">당월최고</span>
<p><strong class="text_red"><?= $d['max_rate'] ?>%</strong></p>
</td>
<td>
<strong><?= won(calcEqualPaymentMonthly(100000000, $d['max_rate'], 10)) ?></strong>원
</td>
<td><strong>
<?= won(calcEqualPrincipalAvgMonthly(100000000, $d['max_rate'], 10)) ?>
</strong>원
</td>
</tr>
<?php if (!empty($d['avg_rate'])): ?>
<tr>
<td class="rate-cell">
<span class="text_black">전월평균</span>
<p><strong class="text_red"><?= $d['avg_rate'] ?>%</strong></p>
</td>
<td>
<strong><?= won(calcEqualPaymentMonthly(100000000, $d['avg_rate'], 10)) ?></strong>원
</td>
<td><strong>
<?= won(calcEqualPrincipalAvgMonthly(100000000, $d['avg_rate'], 10)) ?>
</strong>원
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<div class="detail-foot">
<?php if (!empty($d['prov_date'])): ?>
<span>금융회사 최종 제공일
<?= esc($d['prov_date']) ?>
</span>
<?php endif; ?>
</div>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
</div>
<div class="owl-controls-bar">
<button type="button" class="owl-btn owl-prev" aria-label="이전"></button>
<div class="owl-counter" aria-live="polite">1 / 1</div>
<button type="button" class="owl-btn owl-next" aria-label="다음"></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.theme.default.min.css">
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script>
<script type="text/javascript">
const datas = <?= json_encode($data, JSON_UNESCAPED_UNICODE); ?>;
let owlInited = false;
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'
}[m]));
$(function () {
$('.nav-link').on('click', function () {
const cd = $(this).data('cd');
var arr = new Array();
rebuildByCd(cd);
// switch (cd) {
// // 전체
// case "all":
// arr = datas;
// break;
// // 은행
// case "B":
// $.each(datas, function (idx, v) {
// if (v.bank_type == "B") {
// arr.push(v);
// }
// });
// break;
// // 저축은행
// case "SB":
// $.each(datas, function (idx, v) {
// if (v.bank_type == "SB") {
// arr.push(v);
// }
// });
// break;
// // 보험
// case "I":
// $.each(datas, function (idx, v) {
// if (v.bank_type == "I") {
// arr.push(v);
// }
// });
// break;
// }
// 다시 렌더링
});
const $owl = $('.product-carousel');
initOwlOnce();
// ✅ Owl 드래그가 버튼 클릭을 먹어버리는 문제 방지
$(document).on('mousedown touchstart pointerdown', '.product-carousel .js-toggle, .product-carousel a', function (e) {
e.stopPropagation();
});
// ✅ 클릭도 가끔 막히는 케이스 방지
$(document).on('click', '.product-carousel .js-toggle', function (e) {
e.preventDefault();
e.stopPropagation();
const sel = $(this).data('target');
const $target = $(sel);
if ($target.length) {
$target.prop('hidden', !$target.prop('hidden'));
// 내용 펼치면 높이 변하니까 autoHeight 갱신
setTimeout(() => $('.product-carousel').trigger('refresh.owl.carousel'), 0);
}
});
// Bootstrap 탭 전환 완료 후 폭 재계산
$(document).on('shown.bs.tab', 'a[data-bs-toggle="tab"]', function () {
const $owl = $('.product-carousel');
if ($owl.length) {
// 숨김->보임 전환 직후엔 약간 텀 주는 게 안정적
setTimeout(() => $owl.trigger('refresh.owl.carousel'), 0);
}
});
// ===== 외부 버튼 컨트롤 =====
$('.owl-controls-bar .owl-prev').on('click', function () {
$owl.trigger('prev.owl.carousel');
});
$('.owl-controls-bar .owl-next').on('click', function () {
$owl.trigger('next.owl.carousel');
});
});
function initOwlOnce() {
if (owlInited) return;
owlInited = true;
$('.product-carousel').owlCarousel({
items: 1,
loop: false,
margin: 16,
autoHeight: true,
smartSpeed: 250,
mouseDrag: false,
touchDrag: false,
pullDrag: false,
onInitialized: updateOwlUI,
onTranslated: updateOwlUI,
onResized: updateOwlUI
});
}
// ===== 카운터 + 버튼 활성/비활성 =====
function updateOwlUI(event) {
// event가 없는 경우(수동 호출) 대비
const e = event || $owl.data('owl.carousel');
const carousel = event ? event.relatedTarget : e;
if (!carousel) return;
const total = carousel.items().length; // 총 슬라이드 수(= 5개 묶음 페이지 수)
const current = carousel.relative(carousel.current()) + 1;
$('.owl-counter').text(`${current} / ${total}`);
// 버튼 disable 처리
const $prev = $('.owl-controls-bar .owl-prev');
const $next = $('.owl-controls-bar .owl-next');
$prev.toggleClass('is-disabled', current <= 1);
$next.toggleClass('is-disabled', current >= total);
}
function chunk(arr, size) {
const out = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
// ✅ 카드 HTML 생성 함수 (여기서 너의 카드 마크업을 그대로 문자열로 만들어야 함)
function renderCard(d) {
// 중요: XSS 방지 필요하면 escape 처리(최소한 텍스트는 escape)
const badges = `
${d.chg_yn === 'Y' ? `<span class="label_icon">변동금리</span>` : ``}
${d.split_yn === 'Y' ? `<span class="label_icon">분할상환</span>` : ``}
`;
// detail/pay 영역은 지금 너가 PHP 함수(render_detail_section, won...)를 쓰고 있어서
// 프론트에서 완전 동일하게 만들려면 "서버에서 HTML을 내려받는" 방식이 더 좋음.
// 일단은 카드 상단(요약)만 재렌더하고 상세는 필요 시 Ajax로 가져오는 패턴 추천.
return `
<li class="product-card">
<div class="product-head">
<div class="title-row">
<a href="${esc(d.bank_url || '#')}" class="bank_name" target="_blank" rel="noopener">${esc(d.bank)}</a>
<span class="product_name">${esc(d.product)}</span>
<div class="badges">${badges}</div>
</div>
<dl class="rate_detail">
<div class="rate"><dt>최저</dt><dd><strong class="rate_text">${esc(d.min_rate)}%</strong></dd></div>
<div class="rate"><dt>최고</dt><dd><strong class="rate_text">${esc(d.max_rate)}%</strong></dd></div>
<div class="rate"><dt>평균</dt><dd><strong class="rate_text">${esc(d.avg_rate)}%</strong></dd></div>
</dl>
</div>
<div class="product-actions">
<button type="button" class="btn btn-sm btn-outline-secondary js-toggle" data-target="#detail-${esc(d.seq)}">상세보기</button>
<button type="button" class="btn btn-sm btn-outline-secondary js-toggle" data-target="#pay-${esc(d.seq)}">월평균상환액</button>
</div>
<div class="collapse-box" id="detail-${esc(d.seq)}" hidden>
<div class="detail-grid">
${render_detail_section('대출부대비용', esc(d.extra_fee))}
${render_detail_section('연체이자율', esc(d.late_rate))}
${render_detail_section('가입방식', esc(d.join_method))}
${render_detail_section('중도상환수수료', esc(d.prepay_fee))}
${render_detail_section('대출한도', esc(d.loan_limit))}
<div class="detail-row">
<div class="detail-title">상품문의</div>
<ul class="detail-list">
<li>${esc(d.bank)}</li>
<li>
<a href="https://m.map.naver.com/search2/search.nhn?query=<?= $d['bank'] ?>" target="_blank">${esc(d.tel)}</a>
</li>
</ul>
</div>
</div>
<div class="detail-foot">
금융회사 최종 제공일 ${esc(d.prov_date)}
</div>
</div>
<div class="collapse-box" id="pay-${esc(d.seq)}" hidden>
${render_rates_section(d)}
</div>
</li>
`;
}
function renderSlides(list) {
const pages = chunk(list, 5);
if (!pages.length) {
return `<div class="item"><div class="text-center text-muted py-5">데이터가 없습니다.</div></div>`;
}
return pages.map(page => `
<div class="item">
<ul class="product_list">
${page.map(renderCard).join('')}
</ul>
</div>
`).join('');
}
function destroyOwl($owl) {
if ($owl.hasClass('owl-loaded')) {
$owl.trigger('destroy.owl.carousel');
// owl이 감싼 wrapper 제거
$owl.find('.owl-stage-outer').children().unwrap();
$owl.removeClass('owl-center owl-loaded owl-text-select-on');
$owl.find('.owl-stage').children().unwrap();
}
}
function rebuildByCd(cd) {
const filtered = (cd === 'all')
? datas
: datas.filter(x => String(x.bank_type) === String(cd));
const $owl = $('.product-carousel');
destroyOwl($owl);
$owl.html(renderSlides(filtered));
initOwl($owl);
}
function initOwl($owl) {
$owl.owlCarousel({
items: 1,
loop: false,
margin: 16,
autoHeight: true,
smartSpeed: 250,
mouseDrag: false,
touchDrag: false,
pullDrag: false,
onInitialized: updateOwlUI,
onTranslated: updateOwlUI,
onResized: updateOwlUI
});
}
function render_detail_section(title, value) {
const lines = normalize_lines(value);
if (!lines.length) return '';
str = `
<div class="detail-row">
<div class="detail-title">${title}</div>
<ul class="detail-list">`;
for (const line of lines) {
const text = Array.isArray(line) ? line.join(' ') : String(line ?? '').trim();
if (!text) continue;
str += `<li>${esc(text)}</li>`;
}
str += `
</ul>
</div>
`;
return str;
}
function render_rates_section(d) {
var basis = '1억원 대출, 10년 분할 상환시';
var str = `
<div class="pay-box">
<div class="detail-row" style="margin-bottom: 15px;">
<ul class="detail-list">
<li>
<?= esc($basis) ?>
</li>
</ul>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th rowspan="2" style="width: 30%;">금리</th>
<th colspan="2">월평균 상환액</th>
</tr>
<tr>
<th style="width: 35%;">원리금 균등상환</th>
<th style="width: 35%;">원금 균등상환</th>
</tr>
</thead>
<tbody>
<tr>
<td class="rate-cell">
<span class="text_black">당월최저</span>
<p><strong class="text_red">${esc(d.min_rate)}%</strong></p>
</td>
<td>
<strong>${won(calcEqualPaymentMonthly(100000000, d.min_rate, 10))}</strong>원
</td>
<td>
<strong>${won(calcEqualPaymentMonthly(100000000, d.min_rate, 10))}</strong>원
</td>
</tr>
<tr>
<td class="rate-cell">
<span class="text_black">당월최고</span>
<p><strong class="text_red">${esc(d.max_rate)}%</strong></p>
</td>
<td>
<strong>${won(calcEqualPaymentMonthly(100000000, d.max_rate, 10))}/strong>원
</td>
<td>
<strong>${won(calcEqualPrincipalAvgMonthly(100000000, d.max_rate, 10))}</strong>원
</td>
</tr>`;
if (d.avg_rate != null) {
str += `
<tr>
<td class="rate-cell">
<span class="text_black">전월평균</span>
<p><strong class="text_red">${esc(d.avg_rate)}%</strong></p>
</td>
<td>
<strong>${won(calcEqualPaymentMonthly(100000000, d.avg_rate, 10))} ?></strong>원
</td>
<td>
<strong>${won(calcEqualPrincipalAvgMonthly(100000000, d.avg_rate, 10))}</strong>원
</td>
</tr>
`;
}
str += `
</tbody>
</table>
<div class="detail-foot">
<span>금융회사 최종 제공일 ${esc(d.prov_date)}</span>
</div>
</div>
`;
return str;
}
function normalize_lines(v) {
if (v === null || v === undefined) return [];
// 이미 배열이면
if (Array.isArray(v)) {
return v.map(x => String(x).trim()).filter(x => x !== '');
}
let s = String(v).trim();
if (!s) return [];
// JSON 배열 문자열이면
if (s.startsWith('[')) {
try {
const decoded = JSON.parse(s);
if (Array.isArray(decoded)) {
return decoded.map(x => String(x).trim()).filter(x => x !== '');
}
} catch (e) { }
}
// 개행 기준 분리
return s
.split(/\r\n|\r|\n/)
.map(x => x.trim())
.filter(x => x !== '');
}
// 원리금균등상환
function calcEqualPaymentMonthly(principal, annualRate, years) {
const n = years * 12;
const r = (annualRate / 100.0) / 12.0;
if (n <= 0) return 0.0;
if (Math.abs(r) < 1e-12) return principal / n;
const pow = Math.pow(1.0 + r, n);
return (principal * r * pow) / (pow - 1.0);
}
// 원금균등상환
function calcEqualPrincipalAvgMonthly(principal, annualRate, years) {
const n = years * 12;
const r = (annualRate / 100.0) / 12.0;
if (n <= 0) return 0.0;
// 이자합(원금균등): P * r * (n + 1) / 2
const totalInterest = principal * r * (n + 1) / 2.0;
const totalPayment = principal + totalInterest;
return totalPayment / n;
}
function won(v) {
return Math.round(v).toLocaleString('ko-KR');
}
</script>
<?= $this->endSection() ?>