Files
sam-manage/resources/views/finance/account-transactions.blade.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