Files
confirms/app/Views/pages/interest/list.php
yangsh 87b8093f92
Some checks failed
Close Pull Request / main (pull_request_target) Has been cancelled
금리비교 추가
2026-01-21 12:03:34 +09:00

998 lines
29 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?= $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() ?>