This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
|
||||
998
app/Views/pages/interest/list.php
Normal file
998
app/Views/pages/interest/list.php
Normal 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 => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
}[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() ?>
|
||||
Reference in New Issue
Block a user