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