feat:재무 대시보드 계좌별 잔액 바로빌 실시간 연동

- 페이지 로드 시 바로빌 API로 실시간 잔액 조회
- 새로고침 버튼으로 수동 조회 가능
- 총 잔액 카드도 실시간 업데이트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-06 06:18:09 +09:00
parent 1347ba69c0
commit d4bff6d8a7

View File

@@ -35,7 +35,7 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 te
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500"> 잔액</p>
<p class="text-2xl font-bold text-gray-800 mt-1">{{ number_format($accountSummary['total_balance']) }}</p>
<p class="text-2xl font-bold text-gray-800 mt-1" data-total-balance>{{ number_format($accountSummary['total_balance']) }}</p>
<p class="text-xs text-gray-400 mt-1">{{ $accountSummary['total_accounts'] }} 계좌</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
@@ -130,13 +130,19 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 te
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">계좌별 잔액</h2>
<a href="{{ route('finance.accounts.index') }}" class="text-sm text-blue-600 hover:text-blue-700">
전체보기
</a>
<div class="flex items-center gap-3">
<button type="button" onclick="refreshAccountBalances()" id="refreshBalanceBtn" class="text-xs text-gray-500 hover:text-blue-600 flex items-center gap-1" title="잔액 새로고침">
<svg id="refreshBalanceIcon" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
<span id="refreshBalanceText">새로고침</span>
</button>
<a href="{{ route('finance.accounts.index') }}" class="text-sm text-blue-600 hover:text-blue-700">
전체보기
</a>
</div>
</div>
<div class="divide-y divide-gray-100 max-h-80 overflow-y-auto">
<div id="accountBalancesList" class="divide-y divide-gray-100 max-h-80 overflow-y-auto">
@forelse($accountBalances as $account)
<div class="px-6 py-3 flex items-center justify-between hover:bg-gray-50">
<div class="px-6 py-3 flex items-center justify-between hover:bg-gray-50" data-account-number="{{ str_replace('-', '', $account->account_number) }}">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<span class="text-xs font-medium text-gray-600">{{ mb_substr($account->bank_name, 0, 2) }}</span>
@@ -149,7 +155,7 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 te
</div>
</div>
<div class="text-right">
<p class="text-sm font-semibold text-gray-800">{{ number_format($account->balance) }}</p>
<p class="text-sm font-semibold text-gray-800 account-balance" data-original="{{ $account->balance }}">{{ number_format($account->balance) }}</p>
<p class="text-xs text-gray-400">{{ $account->account_type }}</p>
</div>
</div>
@@ -299,4 +305,69 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 te
</div>
@endif
</div>
<script>
async function refreshAccountBalances() {
const btn = document.getElementById('refreshBalanceBtn');
const icon = document.getElementById('refreshBalanceIcon');
const text = document.getElementById('refreshBalanceText');
// 로딩 상태
btn.disabled = true;
icon.classList.add('animate-spin');
text.textContent = '조회중...';
try {
const res = await fetch('{{ route("barobill.eaccount.accounts") }}');
const data = await res.json();
if (data.success && data.accounts) {
// 계좌번호 -> 잔액 맵 생성
const balanceMap = {};
data.accounts.forEach(acc => {
const num = (acc.bankAccountNum || '').replace(/-/g, '');
balanceMap[num] = acc.balance || 0;
});
// 각 계좌 잔액 업데이트
let totalBalance = 0;
document.querySelectorAll('#accountBalancesList [data-account-number]').forEach(row => {
const accNum = row.dataset.accountNumber;
const balanceEl = row.querySelector('.account-balance');
if (balanceEl && balanceMap.hasOwnProperty(accNum)) {
const newBalance = balanceMap[accNum];
balanceEl.textContent = new Intl.NumberFormat('ko-KR').format(newBalance) + '원';
balanceEl.classList.add('text-blue-600');
setTimeout(() => balanceEl.classList.remove('text-blue-600'), 2000);
totalBalance += newBalance;
}
});
// 총 잔액 업데이트
const totalBalanceEl = document.querySelector('[data-total-balance]');
if (totalBalanceEl) {
totalBalanceEl.textContent = new Intl.NumberFormat('ko-KR').format(totalBalance) + '원';
}
text.textContent = '완료';
setTimeout(() => { text.textContent = '새로고침'; }, 2000);
} else {
text.textContent = '실패';
setTimeout(() => { text.textContent = '새로고침'; }, 2000);
}
} catch (e) {
console.error('잔액 조회 오류:', e);
text.textContent = '오류';
setTimeout(() => { text.textContent = '새로고침'; }, 2000);
} finally {
btn.disabled = false;
icon.classList.remove('animate-spin');
}
}
// 페이지 로드 시 자동 조회
document.addEventListener('DOMContentLoaded', () => {
refreshAccountBalances();
});
</script>
@endsection