feat:은행거래+전표 통합 테이블 (입금/출금/잔액/분개 통합, 기간 빠른선택)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -893,156 +893,363 @@ className="px-2.5 py-1 text-xs font-medium bg-amber-100 text-amber-700 rounded-f
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// JournalEntryList (전표 목록 - 수동/은행거래 통합)
|
||||
// JournalEntryList (은행거래 + 수동전표 통합 목록)
|
||||
// ============================================================
|
||||
const JournalEntryList = ({ refreshKey, onEdit }) => {
|
||||
const [entries, setEntries] = useState([]);
|
||||
const JournalEntryList = ({ accountCodes, tradingPartners, onPartnerAdded, refreshKey, onEdit }) => {
|
||||
const [transactions, setTransactions] = useState([]);
|
||||
const [journalEntries, setJournalEntries] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [journalStats, setJournalStats] = useState({});
|
||||
const [dateRange, setDateRange] = useState({
|
||||
start: getKoreanDate(-30),
|
||||
end: getKoreanDate(),
|
||||
start: getKoreanDate(-30).replace(/-/g, ''),
|
||||
end: getKoreanDate().replace(/-/g, ''),
|
||||
});
|
||||
const [sourceFilter, setSourceFilter] = useState('all');
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [viewFilter, setViewFilter] = useState('all'); // all, bank, manual, unjournaled
|
||||
|
||||
const fetchEntries = async () => {
|
||||
// 분개 모달 상태 (은행거래 → 전표 생성)
|
||||
const [showJournalModal, setShowJournalModal] = useState(false);
|
||||
const [modalTransaction, setModalTransaction] = useState(null);
|
||||
|
||||
const setMonthRange = (offset) => {
|
||||
const now = new Date();
|
||||
const target = new Date(now.getFullYear(), now.getMonth() + offset, 1);
|
||||
const start = target.toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' }).replace(/-/g, '');
|
||||
const endDate = offset === 0
|
||||
? getKoreanDate().replace(/-/g, '')
|
||||
: new Date(target.getFullYear(), target.getMonth() + 1, 0).toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' }).replace(/-/g, '');
|
||||
setDateRange({ start, end: endDate });
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ start_date: dateRange.start, end_date: dateRange.end });
|
||||
const res = await fetch(`/finance/journal-entries/list?${params}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setEntries(data.data || []);
|
||||
const bankParams = new URLSearchParams({ startDate: dateRange.start, endDate: dateRange.end });
|
||||
if (selectedAccount) bankParams.set('accountNum', selectedAccount);
|
||||
const journalParams = new URLSearchParams({
|
||||
start_date: formatDate(dateRange.start),
|
||||
end_date: formatDate(dateRange.end),
|
||||
});
|
||||
|
||||
const [bankRes, journalRes] = await Promise.all([
|
||||
fetch(`/finance/journal-entries/bank-transactions?${bankParams}`),
|
||||
fetch(`/finance/journal-entries/list?${journalParams}`),
|
||||
]);
|
||||
const bankData = await bankRes.json();
|
||||
const journalData = await journalRes.json();
|
||||
|
||||
if (bankData.success) {
|
||||
setTransactions(bankData.data?.logs || []);
|
||||
setJournalStats(bankData.data?.journalStats || {});
|
||||
if (bankData.data?.accounts) setAccounts(bankData.data.accounts);
|
||||
}
|
||||
if (journalData.success) {
|
||||
setJournalEntries(journalData.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('전표 목록 조회 실패:', err);
|
||||
console.error('데이터 조회 실패:', err);
|
||||
notify('데이터 조회 중 오류가 발생했습니다.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchEntries(); }, [refreshKey]);
|
||||
useEffect(() => { fetchData(); }, [refreshKey]);
|
||||
|
||||
const manualCount = entries.filter(e => !e.source_type || e.source_type === 'manual').length;
|
||||
const bankCount = entries.filter(e => e.source_type === 'bank_transaction').length;
|
||||
// 전표 맵 생성 (journalId → entry)
|
||||
const journalMap = {};
|
||||
journalEntries.forEach(je => { journalMap[je.id] = je; });
|
||||
|
||||
const filtered = entries.filter(e => {
|
||||
if (sourceFilter === 'manual') return !e.source_type || e.source_type === 'manual';
|
||||
if (sourceFilter === 'bank') return e.source_type === 'bank_transaction';
|
||||
// 통합 행 생성
|
||||
const bankLinkedJournalIds = new Set();
|
||||
const rows = [];
|
||||
|
||||
// 1) 은행거래 행
|
||||
transactions.forEach(tx => {
|
||||
const je = tx.journalId ? journalMap[tx.journalId] : null;
|
||||
if (tx.journalId) bankLinkedJournalIds.add(tx.journalId);
|
||||
rows.push({
|
||||
type: 'bank',
|
||||
key: `bank-${tx.uniqueKey}`,
|
||||
date: tx.transDate,
|
||||
time: tx.transTime,
|
||||
description: tx.summary || '-',
|
||||
deposit: parseFloat(tx.deposit) || 0,
|
||||
withdraw: parseFloat(tx.withdraw) || 0,
|
||||
balance: tx.balance,
|
||||
hasJournal: tx.hasJournal,
|
||||
journalId: tx.journalId,
|
||||
entryNo: tx.journalEntryNo || (je && je.entry_no) || null,
|
||||
lines: je ? je.lines : [],
|
||||
totalDebit: je ? je.total_debit : 0,
|
||||
totalCredit: je ? je.total_credit : 0,
|
||||
bankTx: tx,
|
||||
sortKey: tx.transDate + (tx.transTime || '000000'),
|
||||
});
|
||||
});
|
||||
|
||||
// 2) 수동 전표 행 (은행거래에 연결되지 않은 것만)
|
||||
journalEntries.forEach(je => {
|
||||
if (bankLinkedJournalIds.has(je.id)) return;
|
||||
if (je.source_type === 'bank_transaction') return;
|
||||
rows.push({
|
||||
type: 'manual',
|
||||
key: `manual-${je.id}`,
|
||||
date: je.entry_date.replace(/-/g, ''),
|
||||
time: '',
|
||||
description: je.description || '-',
|
||||
deposit: 0,
|
||||
withdraw: 0,
|
||||
balance: null,
|
||||
hasJournal: true,
|
||||
journalId: je.id,
|
||||
entryNo: je.entry_no,
|
||||
lines: je.lines || [],
|
||||
totalDebit: je.total_debit,
|
||||
totalCredit: je.total_credit,
|
||||
bankTx: null,
|
||||
sortKey: je.entry_date.replace(/-/g, '') + '999999',
|
||||
});
|
||||
});
|
||||
|
||||
// 최신순 정렬
|
||||
rows.sort((a, b) => b.sortKey.localeCompare(a.sortKey));
|
||||
|
||||
// 필터 적용
|
||||
const filtered = rows.filter(r => {
|
||||
if (viewFilter === 'bank') return r.type === 'bank';
|
||||
if (viewFilter === 'manual') return r.type === 'manual';
|
||||
if (viewFilter === 'unjournaled') return r.type === 'bank' && !r.hasJournal;
|
||||
return true;
|
||||
});
|
||||
|
||||
const totalDebit = filtered.reduce((s, e) => s + (e.total_debit || 0), 0);
|
||||
const totalCredit = filtered.reduce((s, e) => s + (e.total_credit || 0), 0);
|
||||
// 통계
|
||||
const bankRows = rows.filter(r => r.type === 'bank');
|
||||
const manualRows = rows.filter(r => r.type === 'manual');
|
||||
const depositSum = bankRows.reduce((s, r) => s + r.deposit, 0);
|
||||
const withdrawSum = bankRows.reduce((s, r) => s + r.withdraw, 0);
|
||||
const totalDebit = filtered.reduce((s, r) => s + (r.totalDebit || 0), 0);
|
||||
const totalCredit = filtered.reduce((s, r) => s + (r.totalCredit || 0), 0);
|
||||
|
||||
const setMonthRange = (offset) => {
|
||||
const now = new Date();
|
||||
const target = new Date(now.getFullYear(), now.getMonth() + offset, 1);
|
||||
const start = target.toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
|
||||
const end = offset === 0
|
||||
? getKoreanDate()
|
||||
: new Date(target.getFullYear(), target.getMonth() + 1, 0).toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
|
||||
setDateRange({ start, end });
|
||||
const handleJournal = (tx) => {
|
||||
setModalTransaction(tx);
|
||||
setShowJournalModal(true);
|
||||
};
|
||||
|
||||
const handleJournalSaved = () => {
|
||||
setShowJournalModal(false);
|
||||
setModalTransaction(null);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleJournalDeleted = () => {
|
||||
setShowJournalModal(false);
|
||||
setModalTransaction(null);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-stone-100">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h2 className="text-lg font-bold text-stone-800 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-emerald-600" /> 전표 목록
|
||||
</h2>
|
||||
<div className="flex items-center gap-1">
|
||||
{[
|
||||
['all', '전체', entries.length, 'bg-emerald-600'],
|
||||
['manual', '수동', manualCount, 'bg-purple-600'],
|
||||
['bank', '은행거래', bankCount, 'bg-blue-600'],
|
||||
].map(([val, label, count, activeBg]) => (
|
||||
<button key={val} onClick={() => setSourceFilter(val)}
|
||||
className={`px-3 py-1 text-xs rounded-full font-medium transition-colors ${sourceFilter === val
|
||||
? `${activeBg} text-white`
|
||||
: 'bg-stone-100 text-stone-500 hover:bg-stone-200'}`}>
|
||||
{label} <span className="font-bold">{count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<input type="date" value={dateRange.start} onChange={(e) => setDateRange(prev => ({ ...prev, start: e.target.value }))}
|
||||
className="rounded-lg border border-stone-200 px-2.5 py-1.5 text-sm focus:ring-2 focus:ring-emerald-500 outline-none" />
|
||||
<span className="text-stone-400">~</span>
|
||||
<input type="date" value={dateRange.end} onChange={(e) => setDateRange(prev => ({ ...prev, end: e.target.value }))}
|
||||
className="rounded-lg border border-stone-200 px-2.5 py-1.5 text-sm focus:ring-2 focus:ring-emerald-500 outline-none" />
|
||||
<button onClick={() => setMonthRange(0)} className="px-2.5 py-1.5 text-xs bg-emerald-50 text-emerald-600 rounded-lg hover:bg-emerald-100 font-medium">이번달</button>
|
||||
<button onClick={() => setMonthRange(-1)} className="px-2.5 py-1.5 text-xs bg-stone-100 text-stone-600 rounded-lg hover:bg-stone-200 font-medium">지난달</button>
|
||||
<button onClick={fetchEntries} disabled={loading}
|
||||
className="px-3 py-1.5 text-xs bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 font-medium flex items-center gap-1.5 disabled:opacity-50">
|
||||
<Search className="w-3.5 h-3.5" /> 조회
|
||||
<div>
|
||||
{/* 필터 바 */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 p-4 mb-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="text-sm text-stone-500 font-medium">기간</label>
|
||||
<input type="date" value={formatDate(dateRange.start)} onChange={(e) => setDateRange(prev => ({ ...prev, start: e.target.value.replace(/-/g, '') }))}
|
||||
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={formatDate(dateRange.end)} onChange={(e) => setDateRange(prev => ({ ...prev, end: e.target.value.replace(/-/g, '') }))}
|
||||
className="rounded-lg border border-stone-200 px-3 py-1.5 text-sm focus:ring-2 focus:ring-emerald-500 outline-none" />
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button onClick={() => setMonthRange(0)} className="px-2.5 py-1.5 text-xs bg-emerald-50 text-emerald-600 rounded-lg hover:bg-emerald-100 transition-colors font-medium">이번달</button>
|
||||
<button onClick={() => setMonthRange(-1)} className="px-2.5 py-1.5 text-xs bg-stone-100 text-stone-600 rounded-lg hover:bg-stone-200 transition-colors font-medium">지난달</button>
|
||||
<button onClick={() => setMonthRange(-2)} className="px-2.5 py-1.5 text-xs bg-stone-100 text-stone-600 rounded-lg hover:bg-stone-200 transition-colors font-medium">D-2월</button>
|
||||
<button onClick={() => setMonthRange(-3)} className="px-2.5 py-1.5 text-xs bg-stone-100 text-stone-600 rounded-lg hover:bg-stone-200 transition-colors font-medium">D-3월</button>
|
||||
<button onClick={() => setMonthRange(-4)} className="px-2.5 py-1.5 text-xs bg-stone-100 text-stone-600 rounded-lg hover:bg-stone-200 transition-colors font-medium">D-4월</button>
|
||||
<button onClick={() => setMonthRange(-5)} className="px-2.5 py-1.5 text-xs bg-stone-100 text-stone-600 rounded-lg hover:bg-stone-200 transition-colors font-medium">D-5월</button>
|
||||
</div>
|
||||
<button onClick={fetchData} disabled={loading}
|
||||
className="px-4 py-1.5 text-sm bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors font-medium flex items-center gap-2 disabled:opacity-50">
|
||||
<Search className="w-4 h-4" /> 조회
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 mt-3">
|
||||
<label className="text-sm text-stone-500 font-medium">계좌</label>
|
||||
<select value={selectedAccount} onChange={(e) => setSelectedAccount(e.target.value)}
|
||||
className="px-3 py-1.5 text-sm border border-stone-200 rounded-lg focus:ring-2 focus:ring-emerald-500 outline-none">
|
||||
<option value="">전체 계좌</option>
|
||||
{accounts.map(a => (
|
||||
<option key={a.bank_account_num} value={a.bank_account_num}>
|
||||
{a.bank_name} {a.bank_account_num}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="text-sm text-stone-500 font-medium ml-2">구분</label>
|
||||
<div className="flex items-center gap-1">
|
||||
{[
|
||||
['all', '전체', rows.length],
|
||||
['bank', '은행거래', bankRows.length],
|
||||
['manual', '수동전표', manualRows.length],
|
||||
['unjournaled', '미분개', bankRows.filter(r => !r.hasJournal).length],
|
||||
].map(([val, label, count]) => (
|
||||
<button key={val} onClick={() => setViewFilter(val)}
|
||||
className={`px-2.5 py-1 text-xs rounded-full font-medium transition-colors ${viewFilter === val
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-stone-100 text-stone-500 hover:bg-stone-200'}`}>
|
||||
{label} <span className="font-bold">{count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 통계 카드 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 p-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-1.5 bg-stone-50 rounded-lg"><FileText className="w-4 h-4 text-stone-600" /></div>
|
||||
<div>
|
||||
<p className="text-[10px] text-stone-400 leading-tight">전체</p>
|
||||
<p className="text-base font-bold text-stone-800">{rows.length}건</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 p-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-1.5 bg-blue-50 rounded-lg"><ArrowDownCircle className="w-4 h-4 text-blue-600" /></div>
|
||||
<div>
|
||||
<p className="text-[10px] text-stone-400 leading-tight">입금</p>
|
||||
<p className="text-base font-bold text-blue-700">{formatCurrency(depositSum)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 p-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-1.5 bg-red-50 rounded-lg"><ArrowUpCircle className="w-4 h-4 text-red-600" /></div>
|
||||
<div>
|
||||
<p className="text-[10px] text-stone-400 leading-tight">출금</p>
|
||||
<p className="text-base font-bold text-red-700">{formatCurrency(withdrawSum)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 p-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-1.5 bg-emerald-50 rounded-lg"><CheckCircle className="w-4 h-4 text-emerald-600" /></div>
|
||||
<div>
|
||||
<p className="text-[10px] text-stone-400 leading-tight">분개완료</p>
|
||||
<p className="text-base font-bold text-emerald-700">{journalStats.journaledCount || 0}건</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 p-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-1.5 bg-amber-50 rounded-lg"><AlertTriangle className="w-4 h-4 text-amber-600" /></div>
|
||||
<div>
|
||||
<p className="text-[10px] text-stone-400 leading-tight">미분개</p>
|
||||
<p className="text-base font-bold text-amber-700">{journalStats.unjournaledCount || 0}건</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 통합 테이블 */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-stone-100 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-stone-400 text-sm">조회 중...</div>
|
||||
<div className="p-12 text-center text-stone-400">
|
||||
<div className="animate-spin w-8 h-8 border-4 border-emerald-200 border-t-emerald-600 rounded-full mx-auto mb-3"></div>
|
||||
조회 중...
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="p-8 text-center text-stone-400 text-sm">전표가 없습니다.</div>
|
||||
<div className="p-12 text-center text-stone-400">
|
||||
<Landmark className="w-12 h-12 mx-auto mb-3 text-stone-300" />
|
||||
<p>조회된 데이터가 없습니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-stone-50 border-b border-stone-200">
|
||||
<th className="px-4 py-2.5 text-left font-medium text-stone-600 w-[150px]">전표번호</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium text-stone-600 w-[100px]">전표일자</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium text-stone-600">적요</th>
|
||||
<th className="px-4 py-2.5 text-left font-medium text-stone-600">분개 내역</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium text-stone-600 w-[110px]">차변</th>
|
||||
<th className="px-4 py-2.5 text-right font-medium text-stone-600 w-[110px]">대변</th>
|
||||
<th className="px-4 py-2.5 text-center font-medium text-stone-600 w-[50px]"></th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-stone-600 w-[80px]">날짜</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-stone-600">적요</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-stone-600 w-[100px]">입금</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-stone-600 w-[100px]">출금</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-stone-600 w-[100px]">잔액</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-stone-600">분개 내역</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-stone-600 w-[90px]">차변</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-stone-600 w-[90px]">대변</th>
|
||||
<th className="px-3 py-2.5 text-center font-medium text-stone-600 w-[60px]">분개</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map(entry => (
|
||||
<tr key={entry.id} className="border-b border-stone-100 hover:bg-stone-50/50 transition-colors group">
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${entry.source_type === 'bank_transaction' ? 'bg-blue-500' : 'bg-purple-500'}`}></span>
|
||||
<span className="font-mono text-emerald-700 font-medium text-xs">{entry.entry_no}</span>
|
||||
{filtered.map(row => (
|
||||
<tr key={row.key} className={`border-b border-stone-100 hover:bg-stone-50/50 transition-colors group ${row.type === 'manual' ? 'bg-purple-50/30' : ''}`}>
|
||||
<td className="px-3 py-2 text-stone-600 text-xs whitespace-nowrap">
|
||||
<div>{formatDate(row.date)}</div>
|
||||
{row.time && <div className="text-stone-400 text-[10px]">{formatTime(row.time)}</div>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-stone-800 text-xs max-w-[200px]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${row.type === 'manual' ? 'bg-purple-500' : 'bg-blue-500'}`}></span>
|
||||
<span className="truncate" title={row.description}>{row.description}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-stone-600 text-xs">{entry.entry_date}</td>
|
||||
<td className="px-4 py-2.5 text-stone-600 text-xs max-w-[180px] truncate">{entry.description || '-'}</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="space-y-0.5">
|
||||
{entry.lines.map(l => (
|
||||
<div key={l.id} className="flex items-center gap-1.5 text-xs">
|
||||
<span className={`px-1 py-0.5 rounded text-[10px] font-bold leading-none ${l.dc_type === 'debit' ? 'bg-blue-50 text-blue-600' : 'bg-red-50 text-red-600'}`}>
|
||||
{l.dc_type === 'debit' ? '차' : '대'}
|
||||
</span>
|
||||
<span className="text-stone-500">{l.account_name}</span>
|
||||
<span className="font-medium text-stone-700">{formatCurrency(l.debit_amount || l.credit_amount)}</span>
|
||||
{l.trading_partner_name && <span className="text-stone-400">({l.trading_partner_name})</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<td className="px-3 py-2 text-right font-medium text-blue-600 text-xs">
|
||||
{row.deposit > 0 ? formatCurrency(row.deposit) : ''}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right font-medium text-blue-700 text-xs">{formatCurrency(entry.total_debit)}</td>
|
||||
<td className="px-4 py-2.5 text-right font-medium text-red-700 text-xs">{formatCurrency(entry.total_credit)}</td>
|
||||
<td className="px-4 py-2.5 text-center">
|
||||
<button onClick={() => onEdit(entry.id)}
|
||||
className="p-1 text-stone-300 group-hover:text-stone-400 hover:!text-emerald-600 hover:bg-emerald-50 rounded transition-colors" title="수정">
|
||||
<Edit3 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<td className="px-3 py-2 text-right font-medium text-red-600 text-xs">
|
||||
{row.withdraw > 0 ? formatCurrency(row.withdraw) : ''}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-stone-500 text-xs">
|
||||
{row.balance !== null ? formatCurrency(row.balance) : ''}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{row.hasJournal && row.lines.length > 0 ? (
|
||||
<div className="space-y-0.5">
|
||||
{row.lines.map((l, i) => (
|
||||
<div key={l.id || i} className="flex items-center gap-1 text-[11px]">
|
||||
<span className={`px-1 py-0.5 rounded text-[9px] font-bold leading-none ${l.dc_type === 'debit' ? 'bg-blue-50 text-blue-600' : 'bg-red-50 text-red-600'}`}>
|
||||
{l.dc_type === 'debit' ? '차' : '대'}
|
||||
</span>
|
||||
<span className="text-stone-500 truncate max-w-[80px]">{l.account_name}</span>
|
||||
<span className="font-medium text-stone-700">{formatCurrency(l.debit_amount || l.credit_amount)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : row.hasJournal ? (
|
||||
<span className="text-emerald-600 text-xs">분개완료</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right font-medium text-blue-700 text-xs">
|
||||
{row.totalDebit > 0 ? formatCurrency(row.totalDebit) : ''}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right font-medium text-red-700 text-xs">
|
||||
{row.totalCredit > 0 ? formatCurrency(row.totalCredit) : ''}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{row.type === 'bank' && !row.hasJournal ? (
|
||||
<button onClick={() => handleJournal(row.bankTx)}
|
||||
className="px-2 py-0.5 text-[10px] font-medium bg-amber-100 text-amber-700 rounded-full hover:bg-amber-200 transition-colors">
|
||||
분개
|
||||
</button>
|
||||
) : row.hasJournal ? (
|
||||
<button onClick={() => row.type === 'bank' ? handleJournal(row.bankTx) : onEdit(row.journalId)}
|
||||
className="p-1 text-stone-300 group-hover:text-emerald-500 hover:bg-emerald-50 rounded transition-colors" title="수정">
|
||||
<Edit3 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="bg-stone-50 border-t-2 border-stone-300">
|
||||
<td colSpan={4} className="px-4 py-2.5 text-xs text-stone-500 font-medium">
|
||||
합계 ({filtered.length}건)
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right font-bold text-blue-700 text-sm">{formatCurrency(totalDebit)}</td>
|
||||
<td className="px-4 py-2.5 text-right font-bold text-red-700 text-sm">{formatCurrency(totalCredit)}</td>
|
||||
<td colSpan={2} className="px-3 py-2.5 text-xs text-stone-500 font-medium">합계 ({filtered.length}건)</td>
|
||||
<td className="px-3 py-2.5 text-right font-bold text-blue-700 text-xs">{formatCurrency(filtered.reduce((s, r) => s + r.deposit, 0))}</td>
|
||||
<td className="px-3 py-2.5 text-right font-bold text-red-700 text-xs">{formatCurrency(filtered.reduce((s, r) => s + r.withdraw, 0))}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td className="px-3 py-2.5 text-right font-bold text-blue-700 text-xs">{formatCurrency(totalDebit)}</td>
|
||||
<td className="px-3 py-2.5 text-right font-bold text-red-700 text-xs">{formatCurrency(totalCredit)}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -1050,6 +1257,19 @@ className="p-1 text-stone-300 group-hover:text-stone-400 hover:!text-emerald-600
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 분개 모달 (은행거래 → 전표 생성/수정) */}
|
||||
{showJournalModal && modalTransaction && (
|
||||
<JournalEntryModal
|
||||
transaction={modalTransaction}
|
||||
accountCodes={accountCodes}
|
||||
tradingPartners={tradingPartners}
|
||||
onClose={() => { setShowJournalModal(false); setModalTransaction(null); }}
|
||||
onSaved={handleJournalSaved}
|
||||
onDeleted={handleJournalDeleted}
|
||||
onPartnerAdded={onPartnerAdded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1912,13 +2132,10 @@ className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BankTransactionTab
|
||||
<JournalEntryList
|
||||
accountCodes={accountCodes}
|
||||
tradingPartners={tradingPartners}
|
||||
onPartnerAdded={handlePartnerAdded}
|
||||
/>
|
||||
|
||||
<JournalEntryList
|
||||
refreshKey={journalListRefreshKey}
|
||||
onEdit={handleEditEntry}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user