1053 lines
56 KiB
PHP
1053 lines
56 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '일일자금일보')
|
|
|
|
@push('styles')
|
|
<style>
|
|
@media print {
|
|
.no-print { display: none !important; }
|
|
.print-only { display: block !important; }
|
|
body { background: white !important; }
|
|
}
|
|
/* React 앱용 스타일 */
|
|
#daily-fund-root .min-h-screen {
|
|
min-height: auto;
|
|
background: transparent;
|
|
}
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
<div id="daily-fund-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>
|
|
<script type="text/babel" data-version="{{ time() }}">
|
|
const { useState, useRef, useEffect } = React;
|
|
|
|
// Lucide 아이콘을 SVG 컴포넌트로 래핑
|
|
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 Calendar = createIcon('calendar');
|
|
const ChevronLeft = createIcon('chevron-left');
|
|
const ChevronRight = createIcon('chevron-right');
|
|
const Download = createIcon('download');
|
|
const Printer = createIcon('printer');
|
|
const Send = createIcon('send');
|
|
const Plus = createIcon('plus');
|
|
const Trash2 = createIcon('trash-2');
|
|
const Edit = createIcon('edit');
|
|
const Save = createIcon('save');
|
|
const X = createIcon('x');
|
|
const Check = createIcon('check');
|
|
const Wallet = createIcon('wallet');
|
|
const ArrowUpRight = createIcon('arrow-up-right');
|
|
const ArrowDownRight = createIcon('arrow-down-right');
|
|
const Building = createIcon('building');
|
|
const FileSpreadsheet = createIcon('file-spreadsheet');
|
|
const TrendingUp = createIcon('trending-up');
|
|
const TrendingDown = createIcon('trending-down');
|
|
const RefreshCw = createIcon('refresh-cw');
|
|
const AlertCircle = createIcon('alert-circle');
|
|
const CheckCircle = createIcon('check-circle');
|
|
|
|
function DailyFundReport() {
|
|
const today = new Date();
|
|
const [selectedDate, setSelectedDate] = useState(today.toISOString().split('T')[0]);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [showSendModal, setShowSendModal] = useState(false);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [addType, setAddType] = useState('income');
|
|
const [reportSent, setReportSent] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [editingItem, setEditingItem] = useState(null);
|
|
const [editType, setEditType] = useState('income');
|
|
|
|
// 계좌 데이터
|
|
const [accounts] = useState([
|
|
{ id: 1, bank: '국민은행', accountNumber: '123-456-789012', type: '보통예금', color: 'yellow' },
|
|
{ id: 2, bank: '신한은행', accountNumber: '234-567-890123', type: '법인카드 출금', color: 'blue' },
|
|
{ id: 3, bank: '우리은행', accountNumber: '345-678-901234', type: '정기예금', color: 'red' },
|
|
{ id: 4, bank: '하나은행', accountNumber: '456-789-012345', type: '보통예금', color: 'teal' }
|
|
]);
|
|
|
|
// 일별 데이터 (실제로는 API에서 가져옴)
|
|
const [dailyData, setDailyData] = useState({
|
|
'2026-01-21': {
|
|
previousBalance: {
|
|
1: 450000000,
|
|
2: 85000000,
|
|
3: 200000000,
|
|
4: 50000000
|
|
},
|
|
income: [
|
|
{ id: 1, time: '09:30', accountId: 1, description: '(주)제조산업 개발비 1차', amount: 80000000, category: '개발비', note: '계약금 50%' },
|
|
{ id: 2, time: '11:00', accountId: 1, description: '(주)테크솔루션 구독료', amount: 500000, category: '구독료', note: '1월분' },
|
|
{ id: 3, time: '14:30', accountId: 4, description: '(주)스마트팩토리 계약금', amount: 50000000, category: '개발비', note: '계약금 50%' }
|
|
],
|
|
expense: [
|
|
{ id: 1, time: '10:00', accountId: 2, description: 'AWS 호스팅 비용', amount: 2500000, category: '운영비', note: '1월분' },
|
|
{ id: 2, time: '13:00', accountId: 2, description: '사무용품 구입', amount: 150000, category: '소모품비', note: '' },
|
|
{ id: 3, time: '15:00', accountId: 1, description: '외주 개발비 지급', amount: 15000000, category: '외주비', note: '김개발' },
|
|
{ id: 4, time: '16:30', accountId: 4, description: '통신비', amount: 350000, category: '운영비', note: 'KT 인터넷' }
|
|
],
|
|
memo: '오늘 (주)제조산업 개발비 1차 입금 완료. 스마트팩토리 계약 체결.',
|
|
author: '재무팀 김재무',
|
|
createdAt: '2026-01-21 18:00'
|
|
},
|
|
'2026-01-20': {
|
|
previousBalance: {
|
|
1: 385000000,
|
|
2: 87500000,
|
|
3: 200000000,
|
|
4: 50350000
|
|
},
|
|
income: [
|
|
{ id: 1, time: '10:00', accountId: 1, description: '(주)AI산업 구독료', amount: 500000, category: '구독료', note: '1월분' },
|
|
{ id: 2, time: '15:00', accountId: 1, description: '(주)디지털제조 잔금', amount: 65000000, category: '개발비', note: '잔금 50%' }
|
|
],
|
|
expense: [
|
|
{ id: 1, time: '11:00', accountId: 2, description: '급여 지급', amount: 18000000, category: '인건비', note: '정규직 5명' },
|
|
{ id: 2, time: '14:00', accountId: 4, description: '사무실 임대료', amount: 3500000, category: '임대료', note: '1월분' }
|
|
],
|
|
memo: '1월 급여 지급 완료',
|
|
author: '재무팀 김재무',
|
|
createdAt: '2026-01-20 18:30'
|
|
}
|
|
});
|
|
|
|
// 새 거래 폼
|
|
const [newTransaction, setNewTransaction] = useState({
|
|
time: '',
|
|
accountId: 1,
|
|
description: '',
|
|
amount: '',
|
|
category: '',
|
|
note: ''
|
|
});
|
|
|
|
// 현재 선택된 날짜의 데이터
|
|
const currentData = dailyData[selectedDate] || {
|
|
previousBalance: { 1: 0, 2: 0, 3: 0, 4: 0 },
|
|
income: [],
|
|
expense: [],
|
|
memo: '',
|
|
author: '',
|
|
createdAt: ''
|
|
};
|
|
|
|
// 날짜 이동
|
|
const moveDate = (days) => {
|
|
const date = new Date(selectedDate);
|
|
date.setDate(date.getDate() + days);
|
|
setSelectedDate(date.toISOString().split('T')[0]);
|
|
};
|
|
|
|
// 금액 포맷
|
|
const formatCurrency = (num) => {
|
|
if (!num) return '0';
|
|
return num.toLocaleString();
|
|
};
|
|
|
|
const formatCurrencyShort = (num) => {
|
|
if (!num) return '0';
|
|
if (num >= 100000000) return `${(num / 100000000).toFixed(1)}억`;
|
|
if (num >= 10000000) return `${(num / 10000000).toFixed(0)}천만`;
|
|
if (num >= 10000) return `${(num / 10000).toFixed(0)}만`;
|
|
return num.toLocaleString();
|
|
};
|
|
|
|
// 입력용 금액 포맷 (3자리 콤마)
|
|
const formatInputCurrency = (value) => {
|
|
if (!value && value !== 0) return '';
|
|
const num = String(value).replace(/[^\d]/g, '');
|
|
if (!num) return '';
|
|
return Number(num).toLocaleString();
|
|
};
|
|
|
|
// 입력값에서 숫자만 추출
|
|
const parseInputCurrency = (value) => {
|
|
return String(value).replace(/[^\d]/g, '');
|
|
};
|
|
|
|
// 요일 계산
|
|
const getDayOfWeek = (dateStr) => {
|
|
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
|
return days[new Date(dateStr).getDay()];
|
|
};
|
|
|
|
// 날짜 포맷
|
|
const formatDate = (dateStr) => {
|
|
const date = new Date(dateStr);
|
|
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 (${getDayOfWeek(dateStr)})`;
|
|
};
|
|
|
|
// 총 입금액
|
|
const totalIncome = currentData.income.reduce((sum, item) => sum + item.amount, 0);
|
|
|
|
// 총 출금액
|
|
const totalExpense = currentData.expense.reduce((sum, item) => sum + item.amount, 0);
|
|
|
|
// 전일 잔액 합계
|
|
const totalPreviousBalance = Object.values(currentData.previousBalance).reduce((sum, val) => sum + val, 0);
|
|
|
|
// 금일 잔액
|
|
const totalCurrentBalance = totalPreviousBalance + totalIncome - totalExpense;
|
|
|
|
// 계좌별 금일 잔액 계산
|
|
const getAccountCurrentBalance = (accountId) => {
|
|
const prevBalance = currentData.previousBalance[accountId] || 0;
|
|
const accountIncome = currentData.income
|
|
.filter(item => item.accountId === accountId)
|
|
.reduce((sum, item) => sum + item.amount, 0);
|
|
const accountExpense = currentData.expense
|
|
.filter(item => item.accountId === accountId)
|
|
.reduce((sum, item) => sum + item.amount, 0);
|
|
return prevBalance + accountIncome - accountExpense;
|
|
};
|
|
|
|
// 계좌별 입금액
|
|
const getAccountIncome = (accountId) => {
|
|
return currentData.income
|
|
.filter(item => item.accountId === accountId)
|
|
.reduce((sum, item) => sum + item.amount, 0);
|
|
};
|
|
|
|
// 계좌별 출금액
|
|
const getAccountExpense = (accountId) => {
|
|
return currentData.expense
|
|
.filter(item => item.accountId === accountId)
|
|
.reduce((sum, item) => sum + item.amount, 0);
|
|
};
|
|
|
|
// 카테고리별 집계
|
|
const getCategoryTotals = (items) => {
|
|
const totals = {};
|
|
items.forEach(item => {
|
|
if (!totals[item.category]) {
|
|
totals[item.category] = 0;
|
|
}
|
|
totals[item.category] += item.amount;
|
|
});
|
|
return Object.entries(totals).sort((a, b) => b[1] - a[1]);
|
|
};
|
|
|
|
// 거래 추가
|
|
const handleAddTransaction = () => {
|
|
if (!newTransaction.description || !newTransaction.amount) return;
|
|
|
|
const transaction = {
|
|
id: Date.now(),
|
|
time: newTransaction.time || new Date().toTimeString().slice(0, 5),
|
|
accountId: parseInt(newTransaction.accountId),
|
|
description: newTransaction.description,
|
|
amount: parseInt(newTransaction.amount),
|
|
category: newTransaction.category,
|
|
note: newTransaction.note
|
|
};
|
|
|
|
setDailyData(prev => ({
|
|
...prev,
|
|
[selectedDate]: {
|
|
...currentData,
|
|
[addType]: [...currentData[addType], transaction]
|
|
}
|
|
}));
|
|
|
|
setNewTransaction({
|
|
time: '',
|
|
accountId: 1,
|
|
description: '',
|
|
amount: '',
|
|
category: '',
|
|
note: ''
|
|
});
|
|
setShowAddModal(false);
|
|
};
|
|
|
|
// 거래 삭제
|
|
const handleDeleteTransaction = (type, id) => {
|
|
if (!confirm('정말 삭제하시겠습니까?')) return;
|
|
|
|
setDailyData(prev => ({
|
|
...prev,
|
|
[selectedDate]: {
|
|
...currentData,
|
|
[type]: currentData[type].filter(item => item.id !== id)
|
|
}
|
|
}));
|
|
};
|
|
|
|
// 거래 수정 모달 열기
|
|
const handleEditTransaction = (type, item) => {
|
|
setEditType(type);
|
|
setEditingItem({ ...item });
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
// 거래 수정 저장
|
|
const handleSaveEdit = () => {
|
|
if (!editingItem.description || !editingItem.amount) return;
|
|
|
|
setDailyData(prev => ({
|
|
...prev,
|
|
[selectedDate]: {
|
|
...currentData,
|
|
[editType]: currentData[editType].map(item =>
|
|
item.id === editingItem.id
|
|
? { ...editingItem, amount: parseInt(editingItem.amount), accountId: parseInt(editingItem.accountId) }
|
|
: item
|
|
)
|
|
}
|
|
}));
|
|
|
|
setShowEditModal(false);
|
|
setEditingItem(null);
|
|
};
|
|
|
|
// 인쇄
|
|
const handlePrint = () => {
|
|
window.print();
|
|
};
|
|
|
|
// Excel 다운로드 (CSV)
|
|
const handleDownload = () => {
|
|
const rows = [
|
|
['일일자금일보', formatDate(selectedDate)],
|
|
[],
|
|
['구분', '시간', '계좌', '적요', '금액', '카테고리', '비고'],
|
|
...currentData.income.map(item => [
|
|
'입금',
|
|
item.time,
|
|
accounts.find(a => a.id === item.accountId)?.bank || '',
|
|
item.description,
|
|
item.amount,
|
|
item.category,
|
|
item.note
|
|
]),
|
|
...currentData.expense.map(item => [
|
|
'출금',
|
|
item.time,
|
|
accounts.find(a => a.id === item.accountId)?.bank || '',
|
|
item.description,
|
|
item.amount,
|
|
item.category,
|
|
item.note
|
|
]),
|
|
[],
|
|
['전일 잔액', '', '', '', totalPreviousBalance],
|
|
['금일 입금', '', '', '', totalIncome],
|
|
['금일 출금', '', '', '', totalExpense],
|
|
['금일 잔액', '', '', '', totalCurrentBalance]
|
|
];
|
|
|
|
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 = `일일자금일보_${selectedDate}.csv`;
|
|
link.click();
|
|
};
|
|
|
|
// 보고 전송
|
|
const handleSendReport = () => {
|
|
// 실제로는 API 호출
|
|
setReportSent(true);
|
|
setShowSendModal(false);
|
|
setTimeout(() => setReportSent(false), 3000);
|
|
};
|
|
|
|
// 은행 색상
|
|
const getBankColor = (color) => {
|
|
const colors = {
|
|
yellow: 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
|
blue: 'bg-blue-100 text-blue-800 border-blue-300',
|
|
red: 'bg-red-100 text-red-800 border-red-300',
|
|
teal: 'bg-teal-100 text-teal-800 border-teal-300'
|
|
};
|
|
return colors[color] || 'bg-gray-100 text-gray-800 border-gray-300';
|
|
};
|
|
|
|
return (
|
|
<div className="bg-gray-50">
|
|
{/* 헤더 */}
|
|
<header className="bg-white border-b border-gray-200 rounded-t-xl mb-6 no-print">
|
|
<div className="px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-2 bg-emerald-100 rounded-xl">
|
|
<FileSpreadsheet className="w-6 h-6 text-emerald-600" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-xl font-bold text-gray-900">일일자금일보</h1>
|
|
<p className="text-sm text-gray-500">Daily Fund Report</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 transition-colors"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
<span className="text-sm">Excel</span>
|
|
</button>
|
|
<button
|
|
onClick={handlePrint}
|
|
className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
<Printer className="w-4 h-4" />
|
|
<span className="text-sm">인쇄</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setShowSendModal(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors"
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
<span className="text-sm">보고</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* 보고 완료 알림 */}
|
|
{reportSent && (
|
|
<div className="fixed top-20 right-6 bg-emerald-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50 no-print">
|
|
<CheckCircle className="w-5 h-5" />
|
|
<span>보고가 전송되었습니다.</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 날짜 선택 */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6 no-print">
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
onClick={() => moveDate(-1)}
|
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
<ChevronLeft className="w-5 h-5 text-gray-600" />
|
|
</button>
|
|
<div className="flex items-center gap-3">
|
|
<Calendar className="w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="date"
|
|
value={selectedDate}
|
|
onChange={(e) => setSelectedDate(e.target.value)}
|
|
className="text-lg font-semibold text-gray-900 border-0 focus:ring-0 cursor-pointer"
|
|
/>
|
|
<span className="text-lg text-gray-600">({getDayOfWeek(selectedDate)})</span>
|
|
</div>
|
|
<button
|
|
onClick={() => moveDate(1)}
|
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
<ChevronRight className="w-5 h-5 text-gray-600" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 인쇄용 헤더 */}
|
|
<div className="hidden print-only mb-8">
|
|
<h1 className="text-2xl font-bold text-center mb-2">일 일 자 금 일 보</h1>
|
|
<p className="text-center text-gray-600">{formatDate(selectedDate)}</p>
|
|
</div>
|
|
|
|
{/* 요약 카드 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-sm text-gray-500">전일 잔액</span>
|
|
<div className="p-2 bg-gray-100 rounded-lg">
|
|
<Wallet className="w-4 h-4 text-gray-600" />
|
|
</div>
|
|
</div>
|
|
<p className="text-2xl font-bold text-gray-900">{formatCurrencyShort(totalPreviousBalance)}원</p>
|
|
<p className="text-xs text-gray-400 mt-1">{formatCurrency(totalPreviousBalance)}원</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-emerald-200 p-6 bg-emerald-50/30">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-sm text-emerald-700">금일 입금</span>
|
|
<div className="p-2 bg-emerald-100 rounded-lg">
|
|
<ArrowUpRight className="w-4 h-4 text-emerald-600" />
|
|
</div>
|
|
</div>
|
|
<p className="text-2xl font-bold text-emerald-600">+{formatCurrencyShort(totalIncome)}원</p>
|
|
<p className="text-xs text-emerald-500 mt-1">{formatCurrency(totalIncome)}원</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-rose-200 p-6 bg-rose-50/30">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-sm text-rose-700">금일 출금</span>
|
|
<div className="p-2 bg-rose-100 rounded-lg">
|
|
<ArrowDownRight className="w-4 h-4 text-rose-600" />
|
|
</div>
|
|
</div>
|
|
<p className="text-2xl font-bold text-rose-600">-{formatCurrencyShort(totalExpense)}원</p>
|
|
<p className="text-xs text-rose-500 mt-1">{formatCurrency(totalExpense)}원</p>
|
|
</div>
|
|
<div className="bg-gradient-to-br from-blue-600 to-blue-700 rounded-xl p-6 text-white">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-sm text-blue-100">금일 잔액</span>
|
|
<div className="p-2 bg-white/20 rounded-lg">
|
|
<Wallet className="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<p className="text-2xl font-bold">{formatCurrencyShort(totalCurrentBalance)}원</p>
|
|
<p className="text-xs text-blue-200 mt-1">{formatCurrency(totalCurrentBalance)}원</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 계좌별 현황 */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6 mb-6">
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">계좌별 현황</h3>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b border-gray-200">
|
|
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">은행</th>
|
|
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">계좌번호</th>
|
|
<th className="text-right py-3 px-4 text-sm font-semibold text-gray-700">전일잔액</th>
|
|
<th className="text-right py-3 px-4 text-sm font-semibold text-emerald-700">입금</th>
|
|
<th className="text-right py-3 px-4 text-sm font-semibold text-rose-700">출금</th>
|
|
<th className="text-right py-3 px-4 text-sm font-semibold text-blue-700">금일잔액</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{accounts.map(account => (
|
|
<tr key={account.id} className="border-b border-gray-100 hover:bg-gray-50">
|
|
<td className="py-3 px-4">
|
|
<span className={`px-2 py-1 rounded text-xs font-medium border ${getBankColor(account.color)}`}>
|
|
{account.bank}
|
|
</span>
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-gray-600 font-mono">{account.accountNumber}</td>
|
|
<td className="py-3 px-4 text-sm text-right text-gray-900">
|
|
{formatCurrency(currentData.previousBalance[account.id] || 0)}원
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-right text-emerald-600 font-medium">
|
|
{getAccountIncome(account.id) > 0 && '+'}
|
|
{formatCurrency(getAccountIncome(account.id))}원
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-right text-rose-600 font-medium">
|
|
{getAccountExpense(account.id) > 0 && '-'}
|
|
{formatCurrency(getAccountExpense(account.id))}원
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-right text-blue-600 font-bold">
|
|
{formatCurrency(getAccountCurrentBalance(account.id))}원
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr className="bg-gray-50 font-semibold">
|
|
<td className="py-3 px-4 text-sm text-gray-700" colSpan="2">합계</td>
|
|
<td className="py-3 px-4 text-sm text-right text-gray-900">{formatCurrency(totalPreviousBalance)}원</td>
|
|
<td className="py-3 px-4 text-sm text-right text-emerald-600">+{formatCurrency(totalIncome)}원</td>
|
|
<td className="py-3 px-4 text-sm text-right text-rose-600">-{formatCurrency(totalExpense)}원</td>
|
|
<td className="py-3 px-4 text-sm text-right text-blue-600 font-bold">{formatCurrency(totalCurrentBalance)}원</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
{/* 입금 내역 */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-emerald-500 rounded-full"></div>
|
|
입금 내역
|
|
</h3>
|
|
<button
|
|
onClick={() => { setAddType('income'); setShowAddModal(true); }}
|
|
className="flex items-center gap-1 px-3 py-1.5 text-sm text-emerald-600 hover:bg-emerald-50 rounded-lg transition-colors no-print"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
추가
|
|
</button>
|
|
</div>
|
|
{currentData.income.length === 0 ? (
|
|
<p className="text-center text-gray-400 py-8">입금 내역이 없습니다</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{currentData.income.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-between p-3 bg-emerald-50/50 rounded-lg border border-emerald-100 cursor-pointer hover:bg-emerald-100/50 transition-colors"
|
|
onClick={() => handleEditTransaction('income', item)}
|
|
>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-xs text-gray-400">{item.time}</span>
|
|
<span className={`px-1.5 py-0.5 rounded text-xs ${getBankColor(accounts.find(a => a.id === item.accountId)?.color)}`}>
|
|
{accounts.find(a => a.id === item.accountId)?.bank}
|
|
</span>
|
|
<span className="text-xs text-gray-400">{item.category}</span>
|
|
</div>
|
|
<p className="text-sm text-gray-900">{item.description}</p>
|
|
{item.note && <p className="text-xs text-gray-400 mt-0.5">{item.note}</p>}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-semibold text-emerald-600">+{formatCurrency(item.amount)}원</span>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleEditTransaction('income', item); }}
|
|
className="p-1.5 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded transition-colors no-print"
|
|
title="수정"
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleDeleteTransaction('income', item.id); }}
|
|
className="p-1.5 text-gray-400 hover:text-rose-500 hover:bg-rose-50 rounded transition-colors no-print"
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{/* 카테고리별 집계 */}
|
|
{currentData.income.length > 0 && (
|
|
<div className="mt-4 pt-4 border-t border-gray-200">
|
|
<p className="text-xs text-gray-500 mb-2">카테고리별 집계</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{getCategoryTotals(currentData.income).map(([cat, amount]) => (
|
|
<span key={cat} className="px-2 py-1 bg-emerald-100 text-emerald-700 rounded text-xs">
|
|
{cat}: {formatCurrencyShort(amount)}원
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 출금 내역 */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-rose-500 rounded-full"></div>
|
|
출금 내역
|
|
</h3>
|
|
<button
|
|
onClick={() => { setAddType('expense'); setShowAddModal(true); }}
|
|
className="flex items-center gap-1 px-3 py-1.5 text-sm text-rose-600 hover:bg-rose-50 rounded-lg transition-colors no-print"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
추가
|
|
</button>
|
|
</div>
|
|
{currentData.expense.length === 0 ? (
|
|
<p className="text-center text-gray-400 py-8">출금 내역이 없습니다</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{currentData.expense.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-between p-3 bg-rose-50/50 rounded-lg border border-rose-100 cursor-pointer hover:bg-rose-100/50 transition-colors"
|
|
onClick={() => handleEditTransaction('expense', item)}
|
|
>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-xs text-gray-400">{item.time}</span>
|
|
<span className={`px-1.5 py-0.5 rounded text-xs ${getBankColor(accounts.find(a => a.id === item.accountId)?.color)}`}>
|
|
{accounts.find(a => a.id === item.accountId)?.bank}
|
|
</span>
|
|
<span className="text-xs text-gray-400">{item.category}</span>
|
|
</div>
|
|
<p className="text-sm text-gray-900">{item.description}</p>
|
|
{item.note && <p className="text-xs text-gray-400 mt-0.5">{item.note}</p>}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-semibold text-rose-600">-{formatCurrency(item.amount)}원</span>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleEditTransaction('expense', item); }}
|
|
className="p-1.5 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded transition-colors no-print"
|
|
title="수정"
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleDeleteTransaction('expense', item.id); }}
|
|
className="p-1.5 text-gray-400 hover:text-rose-500 hover:bg-rose-50 rounded transition-colors no-print"
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{/* 카테고리별 집계 */}
|
|
{currentData.expense.length > 0 && (
|
|
<div className="mt-4 pt-4 border-t border-gray-200">
|
|
<p className="text-xs text-gray-500 mb-2">카테고리별 집계</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{getCategoryTotals(currentData.expense).map(([cat, amount]) => (
|
|
<span key={cat} className="px-2 py-1 bg-rose-100 text-rose-700 rounded text-xs">
|
|
{cat}: {formatCurrencyShort(amount)}원
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 메모 및 작성 정보 */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">특이사항 / 메모</h3>
|
|
<textarea
|
|
value={currentData.memo}
|
|
onChange={(e) => setDailyData(prev => ({
|
|
...prev,
|
|
[selectedDate]: { ...currentData, memo: e.target.value }
|
|
}))}
|
|
placeholder="특이사항이나 메모를 입력하세요..."
|
|
className="w-full h-24 px-4 py-3 border border-gray-200 rounded-lg resize-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 no-print"
|
|
/>
|
|
{currentData.memo && (
|
|
<div className="hidden print-only mt-2 p-3 bg-gray-50 rounded-lg">
|
|
<p className="text-sm text-gray-700">{currentData.memo}</p>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center justify-between mt-4 pt-4 border-t border-gray-200 text-sm text-gray-500">
|
|
<div>
|
|
{currentData.author && <p>작성자: {currentData.author}</p>}
|
|
{currentData.createdAt && <p>작성일시: {currentData.createdAt}</p>}
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
const now = new Date();
|
|
setDailyData(prev => ({
|
|
...prev,
|
|
[selectedDate]: {
|
|
...currentData,
|
|
author: '재무팀 김재무',
|
|
createdAt: `${now.toISOString().split('T')[0]} ${now.toTimeString().slice(0, 5)}`
|
|
}
|
|
}));
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors no-print"
|
|
>
|
|
<Save className="w-4 h-4" />
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 거래 추가 모달 */}
|
|
{showAddModal && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print">
|
|
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-bold text-gray-900">
|
|
{addType === 'income' ? '입금 추가' : '출금 추가'}
|
|
</h3>
|
|
<button
|
|
onClick={() => setShowAddModal(false)}
|
|
className="p-1 hover:bg-gray-100 rounded-lg"
|
|
>
|
|
<X className="w-5 h-5 text-gray-500" />
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">시간</label>
|
|
<input
|
|
type="time"
|
|
value={newTransaction.time}
|
|
onChange={(e) => setNewTransaction(prev => ({ ...prev, time: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">계좌</label>
|
|
<select
|
|
value={newTransaction.accountId}
|
|
onChange={(e) => setNewTransaction(prev => ({ ...prev, accountId: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500"
|
|
>
|
|
{accounts.map(account => (
|
|
<option key={account.id} value={account.id}>{account.bank}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">적요 *</label>
|
|
<input
|
|
type="text"
|
|
value={newTransaction.description}
|
|
onChange={(e) => setNewTransaction(prev => ({ ...prev, description: e.target.value }))}
|
|
placeholder="거래 내용을 입력하세요"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">금액 *</label>
|
|
<input
|
|
type="text"
|
|
value={formatInputCurrency(newTransaction.amount)}
|
|
onChange={(e) => setNewTransaction(prev => ({ ...prev, amount: parseInputCurrency(e.target.value) }))}
|
|
placeholder="0"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 text-right"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">카테고리</label>
|
|
<select
|
|
value={newTransaction.category}
|
|
onChange={(e) => setNewTransaction(prev => ({ ...prev, category: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500"
|
|
>
|
|
<option value="">선택</option>
|
|
{addType === 'income' ? (
|
|
<>
|
|
<option value="개발비">개발비</option>
|
|
<option value="구독료">구독료</option>
|
|
<option value="용역비">용역비</option>
|
|
<option value="기타수입">기타수입</option>
|
|
</>
|
|
) : (
|
|
<>
|
|
<option value="인건비">인건비</option>
|
|
<option value="운영비">운영비</option>
|
|
<option value="외주비">외주비</option>
|
|
<option value="임대료">임대료</option>
|
|
<option value="소모품비">소모품비</option>
|
|
<option value="기타지출">기타지출</option>
|
|
</>
|
|
)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">비고</label>
|
|
<input
|
|
type="text"
|
|
value={newTransaction.note}
|
|
onChange={(e) => setNewTransaction(prev => ({ ...prev, note: e.target.value }))}
|
|
placeholder="추가 메모"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-3 mt-6">
|
|
<button
|
|
onClick={() => setShowAddModal(false)}
|
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
onClick={handleAddTransaction}
|
|
className={`flex-1 px-4 py-2 text-white rounded-lg ${
|
|
addType === 'income'
|
|
? 'bg-emerald-600 hover:bg-emerald-700'
|
|
: 'bg-rose-600 hover:bg-rose-700'
|
|
}`}
|
|
>
|
|
추가
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 보고 모달 */}
|
|
{showSendModal && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print">
|
|
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-bold text-gray-900">일일자금일보 보고</h3>
|
|
<button
|
|
onClick={() => setShowSendModal(false)}
|
|
className="p-1 hover:bg-gray-100 rounded-lg"
|
|
>
|
|
<X className="w-5 h-5 text-gray-500" />
|
|
</button>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-4 mb-6">
|
|
<p className="text-sm text-gray-600 mb-2">보고 대상: <span className="font-medium text-gray-900">대표이사</span></p>
|
|
<p className="text-sm text-gray-600 mb-2">보고일: <span className="font-medium text-gray-900">{formatDate(selectedDate)}</span></p>
|
|
<div className="border-t border-gray-200 mt-3 pt-3">
|
|
<p className="text-sm text-gray-600">금일 잔액: <span className="font-bold text-blue-600">{formatCurrency(totalCurrentBalance)}원</span></p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={() => setShowSendModal(false)}
|
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
onClick={handleSendReport}
|
|
className="flex-1 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg flex items-center justify-center gap-2"
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
보고 전송
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 수정 모달 */}
|
|
{showEditModal && editingItem && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print">
|
|
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-bold text-gray-900">
|
|
{editType === 'income' ? '입금 수정' : '출금 수정'}
|
|
</h3>
|
|
<button
|
|
onClick={() => { setShowEditModal(false); setEditingItem(null); }}
|
|
className="p-1 hover:bg-gray-100 rounded-lg"
|
|
>
|
|
<X className="w-5 h-5 text-gray-500" />
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">시간</label>
|
|
<input
|
|
type="time"
|
|
value={editingItem.time}
|
|
onChange={(e) => setEditingItem(prev => ({ ...prev, time: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">계좌</label>
|
|
<select
|
|
value={editingItem.accountId}
|
|
onChange={(e) => setEditingItem(prev => ({ ...prev, accountId: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
{accounts.map(account => (
|
|
<option key={account.id} value={account.id}>{account.bank}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">적요 *</label>
|
|
<input
|
|
type="text"
|
|
value={editingItem.description}
|
|
onChange={(e) => setEditingItem(prev => ({ ...prev, description: e.target.value }))}
|
|
placeholder="거래 내용을 입력하세요"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">금액 *</label>
|
|
<input
|
|
type="text"
|
|
value={formatInputCurrency(editingItem.amount)}
|
|
onChange={(e) => setEditingItem(prev => ({ ...prev, amount: parseInputCurrency(e.target.value) }))}
|
|
placeholder="0"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-right"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">카테고리</label>
|
|
<select
|
|
value={editingItem.category}
|
|
onChange={(e) => setEditingItem(prev => ({ ...prev, category: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value="">선택</option>
|
|
{editType === 'income' ? (
|
|
<>
|
|
<option value="개발비">개발비</option>
|
|
<option value="구독료">구독료</option>
|
|
<option value="용역비">용역비</option>
|
|
<option value="기타수입">기타수입</option>
|
|
</>
|
|
) : (
|
|
<>
|
|
<option value="인건비">인건비</option>
|
|
<option value="운영비">운영비</option>
|
|
<option value="외주비">외주비</option>
|
|
<option value="임대료">임대료</option>
|
|
<option value="소모품비">소모품비</option>
|
|
<option value="기타지출">기타지출</option>
|
|
</>
|
|
)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">비고</label>
|
|
<input
|
|
type="text"
|
|
value={editingItem.note || ''}
|
|
onChange={(e) => setEditingItem(prev => ({ ...prev, note: e.target.value }))}
|
|
placeholder="추가 메모"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-3 mt-6">
|
|
<button
|
|
onClick={() => {
|
|
setShowEditModal(false);
|
|
setEditingItem(null);
|
|
handleDeleteTransaction(editType, editingItem.id);
|
|
}}
|
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2"
|
|
>
|
|
<span>🗑️</span> 삭제
|
|
</button>
|
|
<button
|
|
onClick={() => { setShowEditModal(false); setEditingItem(null); }}
|
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 flex items-center justify-center gap-2"
|
|
>
|
|
<span>✕</span> 취소
|
|
</button>
|
|
<button
|
|
onClick={handleSaveEdit}
|
|
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center justify-center gap-2"
|
|
>
|
|
<span>✓</span> 저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// React 앱 마운트
|
|
const rootElement = document.getElementById('daily-fund-root');
|
|
if (rootElement) {
|
|
ReactDOM.createRoot(rootElement).render(<DailyFundReport />);
|
|
}
|
|
</script>
|
|
@endpush
|