From ffd2e8f4dd3f378c5f51b61bc6fa0a48afaab56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Wed, 11 Feb 2026 14:58:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=9D=80=ED=96=89=EA=B1=B0=EB=9E=98+?= =?UTF-8?q?=EC=A0=84=ED=91=9C=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20(=EC=9E=85=EA=B8=88/=EC=B6=9C=EA=B8=88/=EC=9E=94?= =?UTF-8?q?=EC=95=A1/=EB=B6=84=EA=B0=9C=20=ED=86=B5=ED=95=A9,=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=EB=B9=A0=EB=A5=B8=EC=84=A0=ED=83=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../views/finance/journal-entries.blade.php | 431 +++++++++++++----- 1 file changed, 324 insertions(+), 107 deletions(-) diff --git a/resources/views/finance/journal-entries.blade.php b/resources/views/finance/journal-entries.blade.php index f9ecc6f2..e4e73ec2 100644 --- a/resources/views/finance/journal-entries.blade.php +++ b/resources/views/finance/journal-entries.blade.php @@ -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 ( -
-
-
-
-

- 전표 목록 -

-
- {[ - ['all', '전체', entries.length, 'bg-emerald-600'], - ['manual', '수동', manualCount, 'bg-purple-600'], - ['bank', '은행거래', bankCount, 'bg-blue-600'], - ].map(([val, label, count, activeBg]) => ( - - ))} -
-
- 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" /> - ~ - 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" /> - - - + + + + + +
+ +
+
+ + + +
+ {[ + ['all', '전체', rows.length], + ['bank', '은행거래', bankRows.length], + ['manual', '수동전표', manualRows.length], + ['unjournaled', '미분개', bankRows.filter(r => !r.hasJournal).length], + ].map(([val, label, count]) => ( + + ))} +
+
+
+ + {/* 통계 카드 */} +
+
+
+
+
+

전체

+

{rows.length}건

+
+
+
+
+

입금

+

{formatCurrency(depositSum)}

+
+
+
+
+
+
+
+

출금

+

{formatCurrency(withdrawSum)}

+
+
+
+
+
+
+
+

분개완료

+

{journalStats.journaledCount || 0}건

+
+
+
+
+
+
+
+

미분개

+

{journalStats.unjournaledCount || 0}건

+
+
+
+
+ {/* 통합 테이블 */} +
{loading ? ( -
조회 중...
+
+
+ 조회 중... +
) : filtered.length === 0 ? ( -
전표가 없습니다.
+
+ +

조회된 데이터가 없습니다.

+
) : (
- - - - - - - + + + + + + + + + - {filtered.map(entry => ( - - + + - - - - - - + + + + + ))} - - - + + + + + + + @@ -1050,6 +1257,19 @@ className="p-1 text-stone-300 group-hover:text-stone-400 hover:!text-emerald-600 )} + + {/* 분개 모달 (은행거래 → 전표 생성/수정) */} + {showJournalModal && modalTransaction && ( + { setShowJournalModal(false); setModalTransaction(null); }} + onSaved={handleJournalSaved} + onDeleted={handleJournalDeleted} + onPartnerAdded={onPartnerAdded} + /> + )} ); }; @@ -1912,13 +2132,10 @@ className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-s - - -
전표번호전표일자적요분개 내역차변대변날짜적요입금출금잔액분개 내역차변대변분개
-
- - {entry.entry_no} + {filtered.map(row => ( +
+
{formatDate(row.date)}
+ {row.time &&
{formatTime(row.time)}
} +
+
+ + {row.description}
{entry.entry_date}{entry.description || '-'} -
- {entry.lines.map(l => ( -
- - {l.dc_type === 'debit' ? '차' : '대'} - - {l.account_name} - {formatCurrency(l.debit_amount || l.credit_amount)} - {l.trading_partner_name && ({l.trading_partner_name})} -
- ))} -
+
+ {row.deposit > 0 ? formatCurrency(row.deposit) : ''} {formatCurrency(entry.total_debit)}{formatCurrency(entry.total_credit)} - + + {row.withdraw > 0 ? formatCurrency(row.withdraw) : ''} + + {row.balance !== null ? formatCurrency(row.balance) : ''} + + {row.hasJournal && row.lines.length > 0 ? ( +
+ {row.lines.map((l, i) => ( +
+ + {l.dc_type === 'debit' ? '차' : '대'} + + {l.account_name} + {formatCurrency(l.debit_amount || l.credit_amount)} +
+ ))} +
+ ) : row.hasJournal ? ( + 분개완료 + ) : null} +
+ {row.totalDebit > 0 ? formatCurrency(row.totalDebit) : ''} + + {row.totalCredit > 0 ? formatCurrency(row.totalCredit) : ''} + + {row.type === 'bank' && !row.hasJournal ? ( + + ) : row.hasJournal ? ( + + ) : null}
- 합계 ({filtered.length}건) - {formatCurrency(totalDebit)}{formatCurrency(totalCredit)}합계 ({filtered.length}건){formatCurrency(filtered.reduce((s, r) => s + r.deposit, 0))}{formatCurrency(filtered.reduce((s, r) => s + r.withdraw, 0))}{formatCurrency(totalDebit)}{formatCurrency(totalCredit)}