- BankTransaction 모델: 입출금 내역 저장 (계정과목 포함) - 바로빌 데이터와 DB 저장 데이터 매칭하여 계정과목 유지 - 계정과목 드롭다운 선택 및 저장 기능 - 엑셀(CSV) 다운로드 기능 - 저장된 행은 녹색 배경으로 표시 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
646 lines
31 KiB
PHP
646 lines
31 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '계좌 입출금내역')
|
|
|
|
@section('content')
|
|
<!-- 현재 테넌트 정보 카드 (React 외부) -->
|
|
@if($currentTenant)
|
|
<div class="rounded-xl shadow-lg p-5 mb-6" style="background: linear-gradient(to right, #059669, #0d9488); color: white;">
|
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="p-3 rounded-xl" style="background: rgba(255,255,255,0.2);">
|
|
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold" style="background: rgba(255,255,255,0.2);">T-ID: {{ $currentTenant->id }}</span>
|
|
@if($currentTenant->id == 1)
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold" style="background: #facc15; color: #713f12;">파트너사</span>
|
|
@endif
|
|
</div>
|
|
<h2 class="text-xl font-bold">{{ $currentTenant->company_name }}</h2>
|
|
</div>
|
|
</div>
|
|
@if($barobillMember)
|
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">사업자번호</p>
|
|
<p class="font-medium">{{ $barobillMember->biz_no }}</p>
|
|
</div>
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">대표자</p>
|
|
<p class="font-medium">{{ $barobillMember->ceo_name ?? '-' }}</p>
|
|
</div>
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">담당자</p>
|
|
<p class="font-medium">{{ $barobillMember->manager_name ?? '-' }}</p>
|
|
</div>
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">바로빌 ID</p>
|
|
<p class="font-medium">{{ $barobillMember->barobill_id }}</p>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="flex items-center gap-2" style="color: #fef08a;">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<span class="text-sm">바로빌 회원사 미연동</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div id="eaccount-root"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
|
|
<script type="text/babel">
|
|
const { useState, useEffect, useRef, useCallback } = React;
|
|
|
|
// API Routes
|
|
const API = {
|
|
accounts: '{{ route("barobill.eaccount.accounts") }}',
|
|
transactions: '{{ route("barobill.eaccount.transactions") }}',
|
|
accountCodes: '{{ route("barobill.eaccount.account-codes") }}',
|
|
save: '{{ route("barobill.eaccount.save") }}',
|
|
export: '{{ route("barobill.eaccount.export") }}',
|
|
};
|
|
|
|
const CSRF_TOKEN = '{{ csrf_token() }}';
|
|
|
|
// 날짜 유틸리티 함수
|
|
const getMonthDates = (offset = 0) => {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = now.getMonth() + offset;
|
|
const firstDay = new Date(year, month, 1);
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
return {
|
|
from: firstDay.toISOString().split('T')[0],
|
|
to: lastDay.toISOString().split('T')[0]
|
|
};
|
|
};
|
|
|
|
// Toast 알림
|
|
const showToast = (message, type = 'info') => {
|
|
if (window.showToast) {
|
|
window.showToast(message, type);
|
|
} else {
|
|
alert(message);
|
|
}
|
|
};
|
|
|
|
// StatCard Component
|
|
const StatCard = ({ title, value, subtext, icon, color = 'blue' }) => {
|
|
const colorClasses = {
|
|
blue: 'bg-blue-50 text-blue-600',
|
|
green: 'bg-green-50 text-green-600',
|
|
red: 'bg-red-50 text-red-600',
|
|
stone: 'bg-stone-50 text-stone-600'
|
|
};
|
|
return (
|
|
<div className="bg-white rounded-xl p-6 shadow-sm border border-stone-100 hover:shadow-md transition-shadow">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<h3 className="text-sm font-medium text-stone-500">{title}</h3>
|
|
<div className={`p-2 rounded-lg ${colorClasses[color] || colorClasses.blue}`}>
|
|
{icon}
|
|
</div>
|
|
</div>
|
|
<div className="text-2xl font-bold text-stone-900 mb-1">{value}</div>
|
|
{subtext && <div className="text-xs text-stone-400">{subtext}</div>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// AccountSelector Component
|
|
const AccountSelector = ({ accounts, selectedAccount, onSelect }) => (
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
onClick={() => onSelect('')}
|
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
selectedAccount === ''
|
|
? 'bg-emerald-600 text-white'
|
|
: 'bg-white border border-stone-200 text-stone-700 hover:bg-stone-50'
|
|
}`}
|
|
>
|
|
전체 계좌
|
|
</button>
|
|
{accounts.map(acc => (
|
|
<button
|
|
key={acc.bankAccountNum}
|
|
onClick={() => onSelect(acc.bankAccountNum)}
|
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
selectedAccount === acc.bankAccountNum
|
|
? 'bg-emerald-600 text-white'
|
|
: 'bg-white border border-stone-200 text-stone-700 hover:bg-stone-50'
|
|
}`}
|
|
>
|
|
{acc.bankName} {acc.bankAccountNum ? '****' + acc.bankAccountNum.slice(-4) : ''}
|
|
{acc.accountName && ` (${acc.accountName})`}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
// AccountCodeSelect Component
|
|
const AccountCodeSelect = ({ value, onChange, accountCodes }) => (
|
|
<select
|
|
value={value || ''}
|
|
onChange={(e) => {
|
|
const selected = accountCodes.find(c => c.code === e.target.value);
|
|
onChange(e.target.value, selected?.name || '');
|
|
}}
|
|
className="w-full px-2 py-1 text-xs border border-stone-200 rounded focus:ring-2 focus:ring-emerald-500 outline-none bg-white"
|
|
>
|
|
<option value="">선택</option>
|
|
{accountCodes.map(code => (
|
|
<option key={code.code} value={code.code}>{code.code} {code.name}</option>
|
|
))}
|
|
</select>
|
|
);
|
|
|
|
// TransactionTable Component
|
|
const TransactionTable = ({
|
|
logs,
|
|
loading,
|
|
dateFrom,
|
|
dateTo,
|
|
onDateFromChange,
|
|
onDateToChange,
|
|
onThisMonth,
|
|
onLastMonth,
|
|
totalCount,
|
|
accountCodes,
|
|
onAccountCodeChange,
|
|
onSave,
|
|
onExport,
|
|
saving,
|
|
hasChanges
|
|
}) => {
|
|
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0) + '원';
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-20">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl shadow-sm border border-stone-100 overflow-hidden">
|
|
<div className="p-6 border-b border-stone-100">
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
<h2 className="text-lg font-bold text-stone-900">입출금 내역</h2>
|
|
{/* 기간 조회 필터 */}
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-sm text-stone-500">기간</label>
|
|
<input
|
|
type="date"
|
|
value={dateFrom}
|
|
onChange={(e) => onDateFromChange(e.target.value)}
|
|
className="rounded-lg border border-stone-200 px-3 py-1.5 text-sm focus:ring-2 focus:ring-emerald-500 outline-none"
|
|
/>
|
|
<span className="text-stone-400">~</span>
|
|
<input
|
|
type="date"
|
|
value={dateTo}
|
|
onChange={(e) => onDateToChange(e.target.value)}
|
|
className="rounded-lg border border-stone-200 px-3 py-1.5 text-sm focus:ring-2 focus:ring-emerald-500 outline-none"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onThisMonth}
|
|
className="px-3 py-1.5 text-sm bg-emerald-50 text-emerald-600 rounded-lg hover:bg-emerald-100 transition-colors font-medium"
|
|
>
|
|
이번 달
|
|
</button>
|
|
<button
|
|
onClick={onLastMonth}
|
|
className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-stone-200 transition-colors font-medium"
|
|
>
|
|
지난달
|
|
</button>
|
|
</div>
|
|
<span className="text-sm text-stone-500 ml-2">
|
|
조회: <span className="font-semibold text-stone-700">{logs.length}</span>건
|
|
{totalCount !== logs.length && (
|
|
<span className="text-stone-400"> / 전체 {totalCount}건</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{/* 저장/엑셀 버튼 */}
|
|
<div className="flex items-center gap-2 mt-4">
|
|
<button
|
|
onClick={onSave}
|
|
disabled={saving || logs.length === 0}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
hasChanges
|
|
? 'bg-emerald-600 text-white hover:bg-emerald-700'
|
|
: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
|
|
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
>
|
|
{saving ? (
|
|
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
|
</svg>
|
|
)}
|
|
{hasChanges ? '변경사항 저장' : '저장'}
|
|
</button>
|
|
<button
|
|
onClick={onExport}
|
|
disabled={logs.length === 0}
|
|
className="flex items-center gap-2 px-4 py-2 bg-blue-100 text-blue-700 rounded-lg text-sm font-medium hover:bg-blue-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
엑셀 다운로드
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-x-auto" style={ {maxHeight: '500px', overflowY: 'auto'} }>
|
|
<table className="w-full text-left text-sm text-stone-600">
|
|
<thead className="bg-stone-50 text-xs uppercase font-medium text-stone-500 sticky top-0">
|
|
<tr>
|
|
<th className="px-4 py-4 bg-stone-50">거래일시</th>
|
|
<th className="px-4 py-4 bg-stone-50">계좌정보</th>
|
|
<th className="px-4 py-4 bg-stone-50">적요/내용</th>
|
|
<th className="px-4 py-4 text-right bg-stone-50 text-blue-600">입금</th>
|
|
<th className="px-4 py-4 text-right bg-stone-50 text-red-600">출금</th>
|
|
<th className="px-4 py-4 text-right bg-stone-50">잔액</th>
|
|
<th className="px-4 py-4 bg-stone-50">상대방</th>
|
|
<th className="px-4 py-4 bg-stone-50 min-w-[150px]">계정과목</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-stone-100">
|
|
{logs.length === 0 ? (
|
|
<tr>
|
|
<td colSpan="8" className="px-6 py-8 text-center text-stone-400">
|
|
해당 기간에 조회된 입출금 내역이 없습니다.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
logs.map((log, index) => (
|
|
<tr key={index} className={`hover:bg-stone-50 transition-colors ${log.isSaved ? 'bg-green-50/30' : ''}`}>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
<div className="font-medium text-stone-900">{log.transDateTime || '-'}</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="font-medium text-stone-900">{log.bankName}</div>
|
|
<div className="text-xs text-stone-400 font-mono">
|
|
{log.bankAccountNum ? '****' + log.bankAccountNum.slice(-4) : '-'}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="font-medium text-stone-900">{log.summary || '-'}</div>
|
|
{log.memo && <div className="text-xs text-stone-400">{log.memo}</div>}
|
|
</td>
|
|
<td className="px-4 py-3 text-right font-medium text-blue-600">
|
|
{log.deposit > 0 ? log.depositFormatted + '원' : '-'}
|
|
</td>
|
|
<td className="px-4 py-3 text-right font-medium text-red-600">
|
|
{log.withdraw > 0 ? log.withdrawFormatted + '원' : '-'}
|
|
</td>
|
|
<td className="px-4 py-3 text-right text-stone-700">
|
|
{log.balanceFormatted}원
|
|
</td>
|
|
<td className="px-4 py-3 text-stone-500">
|
|
{log.cast || '-'}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<AccountCodeSelect
|
|
value={log.accountCode}
|
|
onChange={(code, name) => onAccountCodeChange(index, code, name)}
|
|
accountCodes={accountCodes}
|
|
/>
|
|
{log.accountName && (
|
|
<div className="text-xs text-emerald-600 mt-1">{log.accountName}</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Main App Component
|
|
const App = () => {
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [accounts, setAccounts] = useState([]);
|
|
const [selectedAccount, setSelectedAccount] = useState('');
|
|
const [logs, setLogs] = useState([]);
|
|
const [summary, setSummary] = useState({});
|
|
const [pagination, setPagination] = useState({});
|
|
const [error, setError] = useState(null);
|
|
const [accountCodes, setAccountCodes] = useState([]);
|
|
const [hasChanges, setHasChanges] = useState(false);
|
|
|
|
// 날짜 필터 상태 (기본: 현재 월)
|
|
const currentMonth = getMonthDates(0);
|
|
const [dateFrom, setDateFrom] = useState(currentMonth.from);
|
|
const [dateTo, setDateTo] = useState(currentMonth.to);
|
|
|
|
// 초기 로드
|
|
useEffect(() => {
|
|
loadAccounts();
|
|
loadAccountCodes();
|
|
}, []);
|
|
|
|
// 날짜 또는 계좌 변경 시 거래내역 로드
|
|
useEffect(() => {
|
|
if (dateFrom && dateTo) {
|
|
loadTransactions();
|
|
}
|
|
}, [dateFrom, dateTo, selectedAccount]);
|
|
|
|
const loadAccounts = async () => {
|
|
try {
|
|
const response = await fetch(API.accounts);
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
setAccounts(data.accounts || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('계좌 목록 로드 오류:', err);
|
|
}
|
|
};
|
|
|
|
const loadAccountCodes = async () => {
|
|
try {
|
|
const response = await fetch(API.accountCodes);
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
setAccountCodes(data.data || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('계정과목 목록 로드 오류:', err);
|
|
}
|
|
};
|
|
|
|
const loadTransactions = async (page = 1) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
setHasChanges(false);
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
startDate: dateFrom.replace(/-/g, ''),
|
|
endDate: dateTo.replace(/-/g, ''),
|
|
accountNum: selectedAccount,
|
|
page: page,
|
|
limit: 50
|
|
});
|
|
|
|
const response = await fetch(`${API.transactions}?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
setLogs(data.data?.logs || []);
|
|
setPagination(data.data?.pagination || {});
|
|
setSummary(data.data?.summary || {});
|
|
} else {
|
|
setError(data.error || '조회 실패');
|
|
setLogs([]);
|
|
}
|
|
} catch (err) {
|
|
setError('서버 통신 오류: ' + err.message);
|
|
setLogs([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// 계정과목 변경 핸들러
|
|
const handleAccountCodeChange = useCallback((index, code, name) => {
|
|
setLogs(prevLogs => {
|
|
const newLogs = [...prevLogs];
|
|
newLogs[index] = {
|
|
...newLogs[index],
|
|
accountCode: code,
|
|
accountName: name
|
|
};
|
|
return newLogs;
|
|
});
|
|
setHasChanges(true);
|
|
}, []);
|
|
|
|
// 저장 핸들러
|
|
const handleSave = async () => {
|
|
if (logs.length === 0) return;
|
|
|
|
setSaving(true);
|
|
try {
|
|
const response = await fetch(API.save, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': CSRF_TOKEN,
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ transactions: logs })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
showToast(data.message, 'success');
|
|
setHasChanges(false);
|
|
// 저장 후 다시 로드하여 isSaved 상태 갱신
|
|
loadTransactions();
|
|
} else {
|
|
showToast(data.error || '저장 실패', 'error');
|
|
}
|
|
} catch (err) {
|
|
showToast('저장 오류: ' + err.message, 'error');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// 엑셀 다운로드 핸들러
|
|
const handleExport = () => {
|
|
const params = new URLSearchParams({
|
|
startDate: dateFrom.replace(/-/g, ''),
|
|
endDate: dateTo.replace(/-/g, ''),
|
|
accountNum: selectedAccount
|
|
});
|
|
window.location.href = `${API.export}?${params}`;
|
|
};
|
|
|
|
// 이번 달 버튼
|
|
const handleThisMonth = () => {
|
|
const dates = getMonthDates(0);
|
|
setDateFrom(dates.from);
|
|
setDateTo(dates.to);
|
|
};
|
|
|
|
// 지난달 버튼
|
|
const handleLastMonth = () => {
|
|
const dates = getMonthDates(-1);
|
|
setDateFrom(dates.from);
|
|
setDateTo(dates.to);
|
|
};
|
|
|
|
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0) + '원';
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Page Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-stone-900">계좌 입출금내역</h1>
|
|
<p className="text-stone-500 mt-1">바로빌 API를 통한 계좌 입출금내역 조회 및 계정과목 관리</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
@if($isTestMode)
|
|
<span className="px-3 py-1 bg-amber-100 text-amber-700 rounded-full text-xs font-medium">테스트 모드</span>
|
|
@endif
|
|
@if($hasSoapClient)
|
|
<span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-xs font-medium">SOAP 연결됨</span>
|
|
@else
|
|
<span className="px-3 py-1 bg-red-100 text-red-700 rounded-full text-xs font-medium">SOAP 미연결</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dashboard */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<StatCard
|
|
title="총 입금액"
|
|
value={formatCurrency(summary.totalDeposit)}
|
|
subtext="조회기간 합계"
|
|
icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4"/></svg>}
|
|
color="blue"
|
|
/>
|
|
<StatCard
|
|
title="총 출금액"
|
|
value={formatCurrency(summary.totalWithdraw)}
|
|
subtext="조회기간 합계"
|
|
icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 12H4"/></svg>}
|
|
color="red"
|
|
/>
|
|
<StatCard
|
|
title="등록된 계좌"
|
|
value={`${accounts.length}개`}
|
|
subtext="사용 가능한 계좌"
|
|
icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>}
|
|
color="green"
|
|
/>
|
|
<StatCard
|
|
title="거래건수"
|
|
value={`${(summary.count || 0).toLocaleString()}건`}
|
|
subtext="전체 입출금 건수"
|
|
icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>}
|
|
color="stone"
|
|
/>
|
|
</div>
|
|
|
|
{/* Account Filter */}
|
|
{accounts.length > 0 && (
|
|
<div className="bg-white rounded-xl shadow-sm border border-stone-100 p-6">
|
|
<h2 className="text-sm font-medium text-stone-700 mb-3">계좌 선택</h2>
|
|
<AccountSelector
|
|
accounts={accounts}
|
|
selectedAccount={selectedAccount}
|
|
onSelect={setSelectedAccount}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-600 p-4 rounded-xl">
|
|
<div className="flex items-start gap-3">
|
|
<div className="text-xl">⚠️</div>
|
|
<div className="flex-1">
|
|
<p className="font-semibold mb-2">{error}</p>
|
|
{error.includes('-50214') && (
|
|
<div className="mt-3 p-3 bg-white rounded border border-red-200 text-sm">
|
|
<p className="font-medium mb-2">해결 방법:</p>
|
|
<ol className="list-decimal list-inside space-y-1 text-stone-700">
|
|
<li>바로빌 사이트(<a href="https://www.barobill.co.kr" target="_blank" className="text-blue-600 hover:underline">https://www.barobill.co.kr</a>)에 로그인</li>
|
|
<li>계좌 관리 메뉴에서 해당 계좌 확인</li>
|
|
<li>계좌 비밀번호가 변경되었는지 확인</li>
|
|
<li>인증서가 만료되지 않았는지 확인</li>
|
|
<li>필요시 계좌 재등록 또는 비밀번호 재설정</li>
|
|
</ol>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Transaction Table */}
|
|
{!error && (
|
|
<TransactionTable
|
|
logs={logs}
|
|
loading={loading}
|
|
dateFrom={dateFrom}
|
|
dateTo={dateTo}
|
|
onDateFromChange={setDateFrom}
|
|
onDateToChange={setDateTo}
|
|
onThisMonth={handleThisMonth}
|
|
onLastMonth={handleLastMonth}
|
|
totalCount={summary.count || logs.length}
|
|
accountCodes={accountCodes}
|
|
onAccountCodeChange={handleAccountCodeChange}
|
|
onSave={handleSave}
|
|
onExport={handleExport}
|
|
saving={saving}
|
|
hasChanges={hasChanges}
|
|
/>
|
|
)}
|
|
|
|
{/* Pagination */}
|
|
{!error && pagination.maxPageNum > 1 && (
|
|
<div className="flex justify-center gap-2">
|
|
<button
|
|
onClick={() => loadTransactions(Math.max(1, pagination.currentPage - 1))}
|
|
disabled={pagination.currentPage === 1}
|
|
className="px-3 py-1 rounded bg-white border disabled:opacity-50"
|
|
>
|
|
이전
|
|
</button>
|
|
<span className="px-3 py-1">
|
|
{pagination.currentPage} / {pagination.maxPageNum}
|
|
</span>
|
|
<button
|
|
onClick={() => loadTransactions(Math.min(pagination.maxPageNum, pagination.currentPage + 1))}
|
|
disabled={pagination.currentPage === pagination.maxPageNum}
|
|
className="px-3 py-1 rounded bg-white border disabled:opacity-50"
|
|
>
|
|
다음
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('eaccount-root'));
|
|
root.render(<App />);
|
|
</script>
|
|
@endpush
|