563 lines
34 KiB
PHP
563 lines
34 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '계정별원장')
|
|
|
|
@push('styles')
|
|
<style>
|
|
@media print { .no-print { display: none !important; } }
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<div id="account-ledger-root"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
@include('partials.react-cdn')
|
|
<script src="https://unpkg.com/lucide@0.469.0?v={{ time() }}"></script>
|
|
@verbatim
|
|
<script type="text/babel">
|
|
const { useState, useRef, useEffect, useCallback } = React;
|
|
|
|
const createIcon = (name) => {
|
|
const _def=((n)=>{const a={'check-circle':'CircleCheck','alert-circle':'CircleAlert','alert-triangle':'TriangleAlert'};if(a[n]&&lucide[a[n]])return lucide[a[n]];const p=n.split('-').map(w=>w.charAt(0).toUpperCase()+w.slice(1)).join('');return lucide[p]||(lucide.icons&&lucide.icons[n])||null;})(name);
|
|
const _c = s => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
return ({ className = "w-5 h-5", ...props }) => {
|
|
if (!_def) return null;
|
|
const [, attrs, children = []] = _def;
|
|
const sp = { className };
|
|
Object.entries(attrs).forEach(([k, v]) => { sp[_c(k)] = v; });
|
|
Object.assign(sp, props);
|
|
return React.createElement("svg", sp, ...children.map(([tag, ca], i) => {
|
|
const cp = { key: i };
|
|
if (ca) Object.entries(ca).forEach(([k, v]) => { cp[_c(k)] = v; });
|
|
return React.createElement(tag, cp);
|
|
}));
|
|
};
|
|
};
|
|
|
|
const Search = createIcon('search');
|
|
const BookOpen = createIcon('book-open');
|
|
const Printer = createIcon('printer');
|
|
const X = createIcon('x');
|
|
const ExternalLink = createIcon('external-link');
|
|
const CreditCard = createIcon('credit-card');
|
|
|
|
// 숫자 포맷
|
|
const fmt = (n) => {
|
|
if (n === 0 || n === null || n === undefined) return '';
|
|
if (n < 0) return '(' + Math.abs(n).toLocaleString() + ')';
|
|
return n.toLocaleString();
|
|
};
|
|
|
|
// ============================================================
|
|
// 전표 상세 모달
|
|
// ============================================================
|
|
function DetailModal({ detail, onClose }) {
|
|
if (!detail) return null;
|
|
|
|
const cellB = 'border border-gray-200';
|
|
const isJournal = detail.type === 'journal';
|
|
|
|
// ESC 키로 닫기
|
|
useEffect(() => {
|
|
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
|
document.addEventListener('keydown', handler);
|
|
return () => document.removeEventListener('keydown', handler);
|
|
}, [onClose]);
|
|
|
|
const goToPage = () => {
|
|
if (isJournal) {
|
|
window.location.href = '/finance/journal-entries?highlight=' + detail.data.id;
|
|
} else {
|
|
window.location.href = '/barobill/hometax?invoice_id=' + detail.sourceId;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={onClose}>
|
|
{/* 오버레이 */}
|
|
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
|
|
{/* 모달 */}
|
|
<div className="relative bg-white rounded-xl shadow-2xl w-full mx-4 overflow-hidden" style={{maxWidth: '900px'}} onClick={e => e.stopPropagation()}>
|
|
{/* 모달 헤더 */}
|
|
<div className="flex items-center justify-between px-5 py-3 bg-indigo-600 text-white">
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-bold text-lg">
|
|
{isJournal ? '전표 상세' : '홈택스 분개 상세'}
|
|
</span>
|
|
{isJournal && detail.data && (
|
|
<span className="text-indigo-200 text-sm">
|
|
{detail.data.entry_no} | {detail.data.entry_date}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button onClick={goToPage} className="flex items-center gap-1 px-2 py-1 text-xs bg-indigo-500 hover:bg-indigo-400 rounded" title="원본 화면으로 이동">
|
|
<ExternalLink className="w-3.5 h-3.5" /> 원본 보기
|
|
</button>
|
|
<button onClick={onClose} className="p-1 hover:bg-indigo-500 rounded">
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 모달 바디 */}
|
|
<div className="p-5 overflow-y-auto" style={{maxHeight: '70vh'}}>
|
|
{isJournal && detail.data ? (
|
|
<>
|
|
{/* 전표 요약 */}
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2 mb-4 text-sm">
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>전표번호</span><span className="font-semibold">{detail.data.entry_no}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>전표일자</span><span>{detail.data.entry_date}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>적요</span><span>{detail.data.description || '-'}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>작성자</span><span>{detail.data.created_by_name || '-'}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>상태</span>
|
|
<span className={detail.data.status === 'confirmed' ? 'text-green-600 font-semibold' : 'text-gray-600'}>
|
|
{detail.data.status === 'confirmed' ? '승인' : detail.data.status === 'draft' ? '임시' : detail.data.status}
|
|
</span>
|
|
</div>
|
|
{detail.data.source_type && (
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>출처</span><span className="text-orange-600">{detail.data.source_type}</span></div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 카드거래 정보 */}
|
|
{detail.cardTx && (
|
|
<div className="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-lg">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<CreditCard className="w-4 h-4 text-orange-500" />
|
|
<span className="text-sm font-semibold text-orange-700">카드거래 정보</span>
|
|
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${detail.cardTx.deduction_type === 'non_deductible' ? 'bg-red-100 text-red-600' : 'bg-emerald-100 text-emerald-600'}`}>
|
|
{detail.cardTx.deduction_type === 'non_deductible' ? '불공제' : '공제'}
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>카드번호</span><span className="font-mono">{'····' + detail.cardTx.card_num.slice(-4)}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>카드사</span><span>{detail.cardTx.card_company_name}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>가맹점</span><span>{detail.cardTx.merchant_name}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>사업자번호</span><span className="font-mono">{detail.cardTx.merchant_biz_num || '-'}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>공급가액</span><span className="text-blue-700">{fmt(detail.cardTx.supply_amount)}</span></div>
|
|
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>세액</span><span className="text-red-600">{fmt(detail.cardTx.tax_amount)}</span></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 분개 라인 테이블 */}
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-gray-100">
|
|
<th className={cellB + ' px-3 py-2 text-center'} style={{width: '40px'}}>No</th>
|
|
<th className={cellB + ' px-3 py-2 text-center'} style={{width: '60px'}}>구분</th>
|
|
<th className={cellB + ' px-3 py-2 text-center'} style={{width: '80px'}}>계정코드</th>
|
|
<th className={cellB + ' px-3 py-2 text-left'}>계정과목</th>
|
|
<th className={cellB + ' px-3 py-2 text-left'}>거래처</th>
|
|
<th className={cellB + ' px-3 py-2 text-right'} style={{width: '120px'}}>차변</th>
|
|
<th className={cellB + ' px-3 py-2 text-right'} style={{width: '120px'}}>대변</th>
|
|
<th className={cellB + ' px-3 py-2 text-left'}>적요</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{detail.data.lines.map((line, i) => (
|
|
<tr key={i} className="hover:bg-gray-50">
|
|
<td className={cellB + ' px-3 py-1.5 text-center text-gray-500'}>{line.line_no || i + 1}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-center'}>
|
|
<span className={line.dc_type === 'debit' ? 'text-blue-600 font-semibold' : 'text-red-600 font-semibold'}>
|
|
{line.dc_type === 'debit' ? '차변' : '대변'}
|
|
</span>
|
|
</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-center font-mono text-indigo-600'}>{line.account_code}</td>
|
|
<td className={cellB + ' px-3 py-1.5'}>{line.account_name}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-gray-600'}>{line.trading_partner_name || ''}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-right text-blue-700'}>{fmt(line.debit_amount)}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-right text-red-600'}>{fmt(line.credit_amount)}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-gray-600 text-xs'}>{line.description || ''}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr className="bg-gray-100 font-semibold">
|
|
<td className={cellB + ' px-3 py-2'} colSpan="5" style={{textAlign: 'right'}}>합 계</td>
|
|
<td className={cellB + ' px-3 py-2 text-right text-blue-700'}>{fmt(detail.data.total_debit)}</td>
|
|
<td className={cellB + ' px-3 py-2 text-right text-red-600'}>{fmt(detail.data.total_credit)}</td>
|
|
<td className={cellB + ' px-3 py-2'}></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</>
|
|
) : !isJournal && detail.lines ? (
|
|
<>
|
|
{/* 홈택스 분개 */}
|
|
<div className="mb-4">
|
|
<span className="text-sm text-orange-600 font-semibold">홈택스 세금계산서 분개</span>
|
|
</div>
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-orange-50">
|
|
<th className={cellB + ' px-3 py-2 text-center'} style={{width: '60px'}}>구분</th>
|
|
<th className={cellB + ' px-3 py-2 text-center'} style={{width: '80px'}}>계정코드</th>
|
|
<th className={cellB + ' px-3 py-2 text-left'}>계정과목</th>
|
|
<th className={cellB + ' px-3 py-2 text-right'} style={{width: '130px'}}>차변</th>
|
|
<th className={cellB + ' px-3 py-2 text-right'} style={{width: '130px'}}>대변</th>
|
|
<th className={cellB + ' px-3 py-2 text-left'}>적요</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{detail.lines.map((line, i) => (
|
|
<tr key={i} className="hover:bg-gray-50">
|
|
<td className={cellB + ' px-3 py-1.5 text-center'}>
|
|
<span className={line.dc_type === 'debit' ? 'text-blue-600 font-semibold' : 'text-red-600 font-semibold'}>
|
|
{line.dc_type === 'debit' ? '차변' : '대변'}
|
|
</span>
|
|
</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-center font-mono text-indigo-600'}>{line.account_code}</td>
|
|
<td className={cellB + ' px-3 py-1.5'}>{line.account_name}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-right text-blue-700'}>{fmt(line.debit_amount)}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-right text-red-600'}>{fmt(line.credit_amount)}</td>
|
|
<td className={cellB + ' px-3 py-1.5 text-gray-600 text-xs'}>{line.description || ''}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</>
|
|
) : (
|
|
<div className="py-8 text-center text-gray-400">상세 정보를 불러올 수 없습니다.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ============================================================
|
|
// 메인 컴포넌트
|
|
// ============================================================
|
|
function AccountLedger() {
|
|
const today = new Date();
|
|
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
|
|
const [startDate, setStartDate] = useState(firstDay.toISOString().slice(0, 10));
|
|
const [endDate, setEndDate] = useState(today.toISOString().slice(0, 10));
|
|
const [accountCode, setAccountCode] = useState('');
|
|
const [accountCodes, setAccountCodes] = useState([]);
|
|
const [data, setData] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [searchText, setSearchText] = useState('');
|
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
const [highlightIndex, setHighlightIndex] = useState(-1);
|
|
const [detail, setDetail] = useState(null);
|
|
const dropdownRef = useRef(null);
|
|
const listRef = useRef(null);
|
|
|
|
// 계정과목 목록 로드
|
|
useEffect(() => {
|
|
fetch('/finance/journal-entries/account-codes')
|
|
.then(r => r.json())
|
|
.then(res => setAccountCodes(res.data || res))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
// 클릭 외부 닫기
|
|
useEffect(() => {
|
|
const handler = (e) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
setShowDropdown(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handler);
|
|
return () => document.removeEventListener('mousedown', handler);
|
|
}, []);
|
|
|
|
// 조회
|
|
const handleSearch = useCallback(() => {
|
|
if (!accountCode) return;
|
|
setLoading(true);
|
|
const params = new URLSearchParams({ start_date: startDate, end_date: endDate, account_code: accountCode });
|
|
fetch('/finance/account-ledger/list?' + params)
|
|
.then(r => r.json())
|
|
.then(d => { setData(d); setLoading(false); })
|
|
.catch(() => setLoading(false));
|
|
}, [startDate, endDate, accountCode]);
|
|
|
|
// 계정과목 선택
|
|
const selectAccount = (ac) => {
|
|
setAccountCode(ac.code);
|
|
setSearchText(ac.code + ' ' + ac.name);
|
|
setShowDropdown(false);
|
|
setHighlightIndex(-1);
|
|
};
|
|
|
|
// 필터된 계정과목
|
|
const filteredCodes = accountCodes.filter(ac => {
|
|
if (!searchText) return true;
|
|
const s = searchText.toLowerCase();
|
|
return ac.code.includes(s) || ac.name.toLowerCase().includes(s);
|
|
});
|
|
|
|
// 키보드 네비게이션
|
|
const handleKeyDown = (e) => {
|
|
if (!showDropdown) {
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
setShowDropdown(true);
|
|
setHighlightIndex(0);
|
|
e.preventDefault();
|
|
}
|
|
return;
|
|
}
|
|
const maxIndex = Math.min(filteredCodes.length, 50) - 1;
|
|
if (maxIndex < 0) return;
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
const ni = highlightIndex < maxIndex ? highlightIndex + 1 : 0;
|
|
setHighlightIndex(ni);
|
|
listRef.current?.children[ni]?.scrollIntoView({ block: 'nearest' });
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
const ni = highlightIndex > 0 ? highlightIndex - 1 : maxIndex;
|
|
setHighlightIndex(ni);
|
|
listRef.current?.children[ni]?.scrollIntoView({ block: 'nearest' });
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
const idx = highlightIndex >= 0 ? highlightIndex : 0;
|
|
if (filteredCodes[idx]) selectAccount(filteredCodes[idx]);
|
|
} else if (e.key === 'Tab') {
|
|
if (highlightIndex >= 0 && filteredCodes[highlightIndex]) {
|
|
selectAccount(filteredCodes[highlightIndex]);
|
|
}
|
|
setShowDropdown(false);
|
|
} else if (e.key === 'Escape') {
|
|
setShowDropdown(false);
|
|
setHighlightIndex(-1);
|
|
}
|
|
};
|
|
|
|
// 전표 드릴다운 (모달)
|
|
const drillDown = (item) => {
|
|
if (item.source_type === 'journal' || item.source_type === 'ecard_transaction' || item.source_type === 'bank_transaction') {
|
|
fetch('/finance/journal-entries/' + item.source_id)
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.success && res.data) {
|
|
setDetail({ type: 'journal', data: res.data, sourceId: item.source_id, cardTx: item.card_tx || null });
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
} else if (item.source_type === 'hometax') {
|
|
fetch('/barobill/hometax/journals?invoice_id=' + item.source_id)
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
setDetail({ type: 'hometax', lines: res.data, sourceId: item.source_id });
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
};
|
|
|
|
const headerBg = 'bg-indigo-600 text-white';
|
|
const cellBorder = 'border border-gray-200';
|
|
const rowHover = 'hover:bg-indigo-50 cursor-pointer';
|
|
const subtotalBg = 'bg-gray-100 font-semibold';
|
|
|
|
return (
|
|
<div className="p-4 space-y-4">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<BookOpen className="w-6 h-6 text-indigo-600" />
|
|
<h1 className="text-xl font-bold text-gray-800">계정별원장</h1>
|
|
</div>
|
|
{data && (
|
|
<button onClick={() => window.print()} className="no-print flex items-center gap-1 px-3 py-1.5 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
|
|
<Printer className="w-4 h-4" />
|
|
인쇄
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 조회 조건 */}
|
|
<div className="no-print bg-white rounded-lg border border-gray-200 p-4">
|
|
<div className="flex flex-wrap items-end gap-4">
|
|
{/* 조회기간 */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-600 mb-1">조회기간</label>
|
|
<div className="flex items-center gap-1">
|
|
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)}
|
|
className="px-2 py-1.5 text-sm border border-gray-300 rounded" />
|
|
<span className="text-gray-400">~</span>
|
|
<input type="date" value={endDate} onChange={e => setEndDate(e.target.value)}
|
|
className="px-2 py-1.5 text-sm border border-gray-300 rounded" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* 계정과목 */}
|
|
<div className="relative" ref={dropdownRef}>
|
|
<label className="block text-xs font-medium text-gray-600 mb-1">계정과목</label>
|
|
<input
|
|
type="text"
|
|
value={searchText}
|
|
onChange={e => { setSearchText(e.target.value); setShowDropdown(true); setHighlightIndex(-1); }}
|
|
onFocus={() => setShowDropdown(true)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="코드 또는 계정명 검색"
|
|
className="px-2 py-1.5 text-sm border border-gray-300 rounded"
|
|
style={{width: '240px'}}
|
|
/>
|
|
{showDropdown && filteredCodes.length > 0 && (
|
|
<div ref={listRef} className="absolute z-50 mt-1 bg-white border border-gray-300 rounded-lg shadow-lg overflow-y-auto" style={{maxHeight: '300px', width: '300px'}}>
|
|
{filteredCodes.slice(0, 50).map((ac, index) => (
|
|
<div key={ac.code} onClick={() => selectAccount(ac)}
|
|
className={`px-3 py-1.5 text-sm cursor-pointer flex justify-between ${index === highlightIndex ? 'bg-indigo-600 text-white' : 'hover:bg-indigo-50'}`}>
|
|
<span className={`font-mono ${index === highlightIndex ? 'text-white' : 'text-indigo-600'}`}>{ac.code}</span>
|
|
<span className={index === highlightIndex ? 'text-white' : 'text-gray-700'}>{ac.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 조회 버튼 */}
|
|
<button onClick={handleSearch} disabled={!accountCode || loading}
|
|
className="flex items-center gap-1 px-4 py-1.5 bg-indigo-600 text-white text-sm rounded hover:bg-indigo-700 disabled:opacity-50">
|
|
<Search className="w-4 h-4" />
|
|
{loading ? '조회중...' : '조회'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 결과 테이블 */}
|
|
{data && data.account && (
|
|
<div className="bg-white rounded-lg border border-gray-200 overflow-x-auto">
|
|
{/* 계정 정보 헤더 */}
|
|
<div className="px-4 py-2 bg-gray-50 border-b border-gray-200 flex items-center gap-4 text-sm">
|
|
<span className="font-semibold text-indigo-600">{data.account.code}</span>
|
|
<span className="font-semibold">{data.account.name}</span>
|
|
<span className="text-gray-500">({data.period.start_date} ~ {data.period.end_date})</span>
|
|
</div>
|
|
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className={headerBg}>
|
|
<th className="px-3 py-2 text-center" style={{width: '100px'}}>날짜</th>
|
|
<th className="px-3 py-2 text-left" style={{width: '200px'}}>적요</th>
|
|
<th className="px-3 py-2 text-left" style={{width: '150px'}}>거래처</th>
|
|
<th className="px-3 py-2 text-center" style={{width: '120px'}}>사업자번호</th>
|
|
<th className="px-3 py-2 text-right" style={{width: '120px'}}>차변</th>
|
|
<th className="px-3 py-2 text-right" style={{width: '120px'}}>대변</th>
|
|
<th className="px-3 py-2 text-right" style={{width: '130px'}}>잔액</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{/* 이월잔액 */}
|
|
{data.carry_forward.balance !== 0 && (
|
|
<tr className="bg-yellow-50 font-semibold">
|
|
<td className={cellBorder + ' px-3 py-1.5 text-center'}>-</td>
|
|
<td className={cellBorder + ' px-3 py-1.5'} colSpan="3">이월잔액</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right'}>{fmt(data.carry_forward.debit)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right'}>{fmt(data.carry_forward.credit)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right'}>{fmt(data.carry_forward.balance)}</td>
|
|
</tr>
|
|
)}
|
|
|
|
{data.monthly_data.map((month, mi) => (
|
|
<React.Fragment key={month.month}>
|
|
{/* 거래 행 */}
|
|
{month.items.map((item, idx) => (
|
|
<tr key={mi + '-' + idx} className={rowHover} onClick={() => drillDown(item)}>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-center text-gray-600'}>{item.date}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5'}>
|
|
<div className="flex items-center gap-1">
|
|
{item.card_tx && <CreditCard className="w-3.5 h-3.5 text-orange-500 flex-shrink-0" />}
|
|
<span>{item.description}</span>
|
|
{item.source_type === 'hometax' && (
|
|
<span className="ml-1 text-xs text-orange-500">[홈택스]</span>
|
|
)}
|
|
{item.card_tx && (
|
|
<span className={`ml-1 px-1.5 py-0.5 rounded text-[10px] font-medium flex-shrink-0 ${item.card_tx.deduction_type === 'non_deductible' ? 'bg-red-100 text-red-600' : 'bg-emerald-100 text-emerald-600'}`}>
|
|
{item.card_tx.deduction_type === 'non_deductible' ? '불공제' : '공제'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{item.card_tx && (
|
|
<div className="text-[11px] text-gray-400 mt-0.5">
|
|
{item.card_tx.card_company_name} {'····' + item.card_tx.card_num.slice(-4)}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-gray-600'}>{item.trading_partner_name}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-center text-gray-500 font-mono text-xs'}>{item.biz_no}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right text-blue-700'}>{fmt(item.debit_amount)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right text-red-600'}>{fmt(item.credit_amount)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right font-semibold'}>{fmt(item.balance)}</td>
|
|
</tr>
|
|
))}
|
|
|
|
{/* 월 소계 */}
|
|
<tr className={subtotalBg}>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-center'}></td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right'} colSpan="3">
|
|
{month.month} 계
|
|
</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right text-blue-700'}>{fmt(month.subtotal.debit)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right text-red-600'}>{fmt(month.subtotal.credit)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5'}></td>
|
|
</tr>
|
|
|
|
{/* 누계 */}
|
|
<tr className={subtotalBg}>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-center'}></td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right'} colSpan="3">
|
|
누 계
|
|
</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right text-blue-700'}>{fmt(month.cumulative.debit)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5 text-right text-red-600'}>{fmt(month.cumulative.credit)}</td>
|
|
<td className={cellBorder + ' px-3 py-1.5'}></td>
|
|
</tr>
|
|
</React.Fragment>
|
|
))}
|
|
|
|
{/* 총합계 */}
|
|
{data.monthly_data.length > 0 && (
|
|
<tr className="bg-indigo-50 font-bold">
|
|
<td className={cellBorder + ' px-3 py-2 text-center'}></td>
|
|
<td className={cellBorder + ' px-3 py-2 text-right'} colSpan="3">총 합 계</td>
|
|
<td className={cellBorder + ' px-3 py-2 text-right text-blue-700'}>{fmt(data.grand_total.debit)}</td>
|
|
<td className={cellBorder + ' px-3 py-2 text-right text-red-600'}>{fmt(data.grand_total.credit)}</td>
|
|
<td className={cellBorder + ' px-3 py-2 text-right'}>{fmt(data.grand_total.balance)}</td>
|
|
</tr>
|
|
)}
|
|
|
|
{/* 데이터 없음 */}
|
|
{data.monthly_data.length === 0 && (
|
|
<tr>
|
|
<td colSpan="7" className="px-3 py-8 text-center text-gray-400">
|
|
조회 기간 내 거래 내역이 없습니다.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{/* 초기 안내 */}
|
|
{!data && !loading && (
|
|
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center text-gray-400">
|
|
<BookOpen className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|
<p>계정과목을 선택하고 조회 버튼을 클릭하세요.</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 전표 상세 모달 */}
|
|
{detail && <DetailModal detail={detail} onClose={() => setDetail(null)} />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ReactDOM.render(<AccountLedger />, document.getElementById('account-ledger-root'));
|
|
</script>
|
|
@endverbatim
|
|
@endpush
|