236 lines
16 KiB
PHP
236 lines
16 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '계좌거래내역')
|
|
|
|
@push('styles')
|
|
<style>
|
|
@media print { .no-print { display: none !important; } }
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
<div id="account-transactions-root"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script src="https://unpkg.com/react@18/umd/react.development.js?v={{ time() }}"></script>
|
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js?v={{ time() }}"></script>
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js?v={{ time() }}"></script>
|
|
<script src="https://unpkg.com/lucide@latest?v={{ time() }}"></script>
|
|
@verbatim
|
|
<script type="text/babel">
|
|
const { useState, useRef, useEffect } = React;
|
|
|
|
const createIcon = (name) => ({ className = "w-5 h-5", ...props }) => {
|
|
const ref = useRef(null);
|
|
useEffect(() => {
|
|
if (ref.current && lucide.icons[name]) {
|
|
ref.current.innerHTML = '';
|
|
const svg = lucide.createElement(lucide.icons[name]);
|
|
svg.setAttribute('class', className);
|
|
ref.current.appendChild(svg);
|
|
}
|
|
}, [className]);
|
|
return <span ref={ref} className="inline-flex items-center" {...props} />;
|
|
};
|
|
|
|
const ArrowUpDown = createIcon('arrow-up-down');
|
|
const ArrowUpCircle = createIcon('arrow-up-circle');
|
|
const ArrowDownCircle = createIcon('arrow-down-circle');
|
|
const Search = createIcon('search');
|
|
const Download = createIcon('download');
|
|
const RefreshCw = createIcon('refresh-cw');
|
|
const Building2 = createIcon('building-2');
|
|
const Filter = createIcon('filter');
|
|
|
|
function AccountTransactions() {
|
|
const [transactions, setTransactions] = useState([
|
|
{ id: 1, date: '2026-01-21', accountName: '기업은행 운영계좌', accountNo: '123-456789-01', type: 'deposit', category: '매출입금', description: '(주)한국테크 서비스대금', amount: 15000000, balance: 125000000, memo: '' },
|
|
{ id: 2, date: '2026-01-21', accountName: '신한은행 급여계좌', accountNo: '110-123-456789', type: 'withdrawal', category: '급여지급', description: '1월 급여 이체', amount: 45000000, balance: 55000000, memo: '정기급여' },
|
|
{ id: 3, date: '2026-01-20', accountName: '기업은행 운영계좌', accountNo: '123-456789-01', type: 'withdrawal', category: '경비지출', description: '사무실 임대료', amount: 5000000, balance: 110000000, memo: '1월분' },
|
|
{ id: 4, date: '2026-01-20', accountName: '국민은행 예비계좌', accountNo: '999-12-345678', type: 'deposit', category: '이자수입', description: '정기예금 이자', amount: 250000, balance: 80250000, memo: '' },
|
|
{ id: 5, date: '2026-01-19', accountName: '기업은행 운영계좌', accountNo: '123-456789-01', type: 'deposit', category: '매출입금', description: '글로벌솔루션 계약금', amount: 8500000, balance: 115000000, memo: '' },
|
|
{ id: 6, date: '2026-01-19', accountName: '신한은행 급여계좌', accountNo: '110-123-456789', type: 'withdrawal', category: '세금납부', description: '원천세 납부', amount: 3200000, balance: 100000000, memo: '12월분' },
|
|
{ id: 7, date: '2026-01-18', accountName: '기업은행 운영계좌', accountNo: '123-456789-01', type: 'withdrawal', category: '거래처지급', description: 'IT솔루션즈 외주비', amount: 12000000, balance: 106500000, memo: '' },
|
|
{ id: 8, date: '2026-01-18', accountName: '국민은행 예비계좌', accountNo: '999-12-345678', type: 'withdrawal', category: '계좌이체', description: '운영자금 이체', amount: 20000000, balance: 80000000, memo: '기업은행으로' },
|
|
{ id: 9, date: '2026-01-17', accountName: '기업은행 운영계좌', accountNo: '123-456789-01', type: 'deposit', category: '매출입금', description: '스마트시스템 유지보수', amount: 3500000, balance: 118500000, memo: '' },
|
|
{ id: 10, date: '2026-01-17', accountName: '신한은행 급여계좌', accountNo: '110-123-456789', type: 'deposit', category: '계좌이체', description: '운영계좌에서 이체', amount: 50000000, balance: 103200000, memo: '' },
|
|
]);
|
|
|
|
const accounts = ['전체', '기업은행 운영계좌', '신한은행 급여계좌', '국민은행 예비계좌'];
|
|
const categories = ['전체', '매출입금', '급여지급', '경비지출', '세금납부', '거래처지급', '이자수입', '계좌이체'];
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [filterAccount, setFilterAccount] = useState('전체');
|
|
const [filterType, setFilterType] = useState('all');
|
|
const [filterCategory, setFilterCategory] = useState('전체');
|
|
const [dateRange, setDateRange] = useState({ start: '', end: '' });
|
|
|
|
const formatCurrency = (num) => num ? num.toLocaleString() : '0';
|
|
|
|
const filteredTransactions = transactions.filter(item => {
|
|
const matchesSearch = item.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.accountName.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesAccount = filterAccount === '전체' || item.accountName === filterAccount;
|
|
const matchesType = filterType === 'all' || item.type === filterType;
|
|
const matchesCategory = filterCategory === '전체' || item.category === filterCategory;
|
|
const matchesDateStart = !dateRange.start || item.date >= dateRange.start;
|
|
const matchesDateEnd = !dateRange.end || item.date <= dateRange.end;
|
|
return matchesSearch && matchesAccount && matchesType && matchesCategory && matchesDateStart && matchesDateEnd;
|
|
});
|
|
|
|
const totalDeposit = filteredTransactions.filter(t => t.type === 'deposit').reduce((sum, t) => sum + t.amount, 0);
|
|
const totalWithdrawal = filteredTransactions.filter(t => t.type === 'withdrawal').reduce((sum, t) => sum + t.amount, 0);
|
|
|
|
const handleDownload = () => {
|
|
const rows = [['계좌거래내역'], [], ['날짜', '계좌', '구분', '분류', '내용', '금액', '잔액', '메모'],
|
|
...filteredTransactions.map(item => [item.date, item.accountName, getTypeLabel(item.type), item.category, item.description, item.amount, item.balance, item.memo])];
|
|
const csvContent = rows.map(row => row.join(',')).join('\n');
|
|
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `계좌거래내역_${new Date().toISOString().split('T')[0]}.csv`; link.click();
|
|
};
|
|
|
|
const getTypeLabel = (type) => {
|
|
const labels = { 'deposit': '입금', 'withdrawal': '출금' };
|
|
return labels[type] || type;
|
|
};
|
|
|
|
const getTypeStyle = (type) => {
|
|
const styles = {
|
|
'deposit': 'text-blue-600',
|
|
'withdrawal': 'text-red-600'
|
|
};
|
|
return styles[type] || 'text-gray-600';
|
|
};
|
|
|
|
const getTypeIcon = (type) => {
|
|
if (type === 'deposit') return <ArrowDownCircle className="w-4 h-4 text-blue-500" />;
|
|
return <ArrowUpCircle className="w-4 h-4 text-red-500" />;
|
|
};
|
|
|
|
const resetFilters = () => {
|
|
setSearchTerm('');
|
|
setFilterAccount('전체');
|
|
setFilterType('all');
|
|
setFilterCategory('전체');
|
|
setDateRange({ start: '', end: '' });
|
|
};
|
|
|
|
return (
|
|
<div className="bg-gray-50 min-h-screen">
|
|
<header className="bg-white border-b border-gray-200 rounded-t-xl mb-6">
|
|
<div className="px-6 py-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-2 bg-blue-100 rounded-xl"><ArrowUpDown className="w-6 h-6 text-blue-600" /></div>
|
|
<div><h1 className="text-xl font-bold text-gray-900">계좌거래내역</h1><p className="text-sm text-gray-500">Account Transactions</p></div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button onClick={handleDownload} className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg"><Download className="w-4 h-4" /><span className="text-sm">Excel</span></button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* 요약 카드 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between mb-2"><span className="text-sm text-gray-500">조회 건수</span><Filter className="w-5 h-5 text-gray-400" /></div>
|
|
<p className="text-2xl font-bold text-gray-900">{filteredTransactions.length}건</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-blue-200 p-6 bg-blue-50/30">
|
|
<div className="flex items-center justify-between mb-2"><span className="text-sm text-blue-700">입금 합계</span><ArrowDownCircle className="w-5 h-5 text-blue-500" /></div>
|
|
<p className="text-2xl font-bold text-blue-600">{formatCurrency(totalDeposit)}원</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-red-200 p-6 bg-red-50/30">
|
|
<div className="flex items-center justify-between mb-2"><span className="text-sm text-red-700">출금 합계</span><ArrowUpCircle className="w-5 h-5 text-red-500" /></div>
|
|
<p className="text-2xl font-bold text-red-600">{formatCurrency(totalWithdrawal)}원</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 필터 영역 */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
|
<div className="md:col-span-2 relative">
|
|
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
|
<input type="text" placeholder="내용, 계좌명 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" />
|
|
</div>
|
|
<select value={filterAccount} onChange={(e) => setFilterAccount(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
|
|
{accounts.map(acc => <option key={acc} value={acc}>{acc}</option>)}
|
|
</select>
|
|
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
|
|
{categories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
|
|
</select>
|
|
<div className="flex gap-1">
|
|
{['all', 'deposit', 'withdrawal'].map(type => (
|
|
<button key={type} onClick={() => setFilterType(type)} className={`flex-1 px-2 py-2 rounded-lg text-xs font-medium ${filterType === type ? (type === 'deposit' ? 'bg-blue-600 text-white' : type === 'withdrawal' ? 'bg-red-600 text-white' : 'bg-gray-800 text-white') : 'bg-gray-100 text-gray-700'}`}>
|
|
{type === 'all' ? '전체' : getTypeLabel(type)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button onClick={resetFilters} className="flex items-center justify-center gap-2 px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
|
|
<RefreshCw className="w-4 h-4" /><span className="text-sm">초기화</span>
|
|
</button>
|
|
</div>
|
|
<div className="flex gap-4 mt-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-500">기간:</span>
|
|
<input type="date" value={dateRange.start} onChange={(e) => setDateRange(prev => ({ ...prev, start: e.target.value }))} className="px-3 py-2 border border-gray-300 rounded-lg text-sm" />
|
|
<span className="text-gray-400">~</span>
|
|
<input type="date" value={dateRange.end} onChange={(e) => setDateRange(prev => ({ ...prev, end: e.target.value }))} className="px-3 py-2 border border-gray-300 rounded-lg text-sm" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 거래내역 테이블 */}
|
|
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">날짜</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">계좌</th>
|
|
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">구분</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">분류</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">내용</th>
|
|
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">금액</th>
|
|
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">잔액</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{filteredTransactions.length === 0 ? (
|
|
<tr><td colSpan="7" className="px-6 py-12 text-center text-gray-400">거래내역이 없습니다.</td></tr>
|
|
) : filteredTransactions.map(item => (
|
|
<tr key={item.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 text-sm text-gray-600">{item.date}</td>
|
|
<td className="px-6 py-4">
|
|
<p className="text-sm font-medium text-gray-900">{item.accountName}</p>
|
|
<p className="text-xs text-gray-400">{item.accountNo}</p>
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<span className="inline-flex items-center gap-1">
|
|
{getTypeIcon(item.type)}
|
|
<span className={`text-sm font-medium ${getTypeStyle(item.type)}`}>{getTypeLabel(item.type)}</span>
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4"><span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">{item.category}</span></td>
|
|
<td className="px-6 py-4">
|
|
<p className="text-sm text-gray-900">{item.description}</p>
|
|
{item.memo && <p className="text-xs text-gray-400">{item.memo}</p>}
|
|
</td>
|
|
<td className={`px-6 py-4 text-sm font-bold text-right ${getTypeStyle(item.type)}`}>
|
|
{item.type === 'deposit' ? '+' : '-'}{formatCurrency(item.amount)}원
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-right text-gray-900">{formatCurrency(item.balance)}원</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const rootElement = document.getElementById('account-transactions-root');
|
|
if (rootElement) { ReactDOM.createRoot(rootElement).render(<AccountTransactions />); }
|
|
</script>
|
|
@endverbatim
|
|
@endpush
|