diff --git a/app/Http/Controllers/Finance/VatRecordController.php b/app/Http/Controllers/Finance/VatRecordController.php new file mode 100644 index 00000000..7754516e --- /dev/null +++ b/app/Http/Controllers/Finance/VatRecordController.php @@ -0,0 +1,166 @@ +input('search')) { + $query->where(function ($q) use ($search) { + $q->where('partner_name', 'like', "%{$search}%") + ->orWhere('invoice_no', 'like', "%{$search}%"); + }); + } + + if ($period = $request->input('period')) { + $query->where('period', $period); + } + + if ($type = $request->input('type')) { + if ($type !== 'all') { + $query->where('type', $type); + } + } + + if ($status = $request->input('status')) { + if ($status !== 'all') { + $query->where('status', $status); + } + } + + $records = $query->orderBy('invoice_date', 'desc') + ->get() + ->map(function ($record) { + return [ + 'id' => $record->id, + 'period' => $record->period, + 'type' => $record->type, + 'partnerName' => $record->partner_name, + 'invoiceNo' => $record->invoice_no, + 'invoiceDate' => $record->invoice_date?->format('Y-m-d'), + 'supplyAmount' => $record->supply_amount, + 'vatAmount' => $record->vat_amount, + 'totalAmount' => $record->total_amount, + 'status' => $record->status, + 'memo' => $record->memo, + ]; + }); + + // 해당 기간 통계 + $periodQuery = VatRecord::forTenant($tenantId); + if ($period = $request->input('period')) { + $periodQuery->where('period', $period); + } + $periodRecords = $periodQuery->get(); + + $stats = [ + 'salesSupply' => $periodRecords->where('type', 'sales')->sum('supply_amount'), + 'salesVat' => $periodRecords->where('type', 'sales')->sum('vat_amount'), + 'purchaseSupply' => $periodRecords->where('type', 'purchase')->sum('supply_amount'), + 'purchaseVat' => $periodRecords->where('type', 'purchase')->sum('vat_amount'), + 'total' => $periodRecords->count(), + ]; + + // 사용 중인 기간 목록 + $periods = VatRecord::forTenant($tenantId) + ->select('period') + ->distinct() + ->orderBy('period', 'desc') + ->pluck('period') + ->toArray(); + + return response()->json([ + 'success' => true, + 'data' => $records, + 'stats' => $stats, + 'periods' => $periods, + ]); + } + + public function store(Request $request): JsonResponse + { + $request->validate([ + 'partnerName' => 'required|string|max:100', + 'invoiceNo' => 'required|string|max:50', + 'period' => 'required|string|max:20', + 'type' => 'required|in:sales,purchase', + 'supplyAmount' => 'required|integer|min:0', + ]); + + $tenantId = session('selected_tenant_id', 1); + + $record = VatRecord::create([ + 'tenant_id' => $tenantId, + 'period' => $request->input('period'), + 'type' => $request->input('type', 'sales'), + 'partner_name' => $request->input('partnerName'), + 'invoice_no' => $request->input('invoiceNo'), + 'invoice_date' => $request->input('invoiceDate'), + 'supply_amount' => $request->input('supplyAmount', 0), + 'vat_amount' => $request->input('vatAmount', 0), + 'total_amount' => $request->input('totalAmount', 0), + 'status' => $request->input('status', 'pending'), + 'memo' => $request->input('memo'), + ]); + + return response()->json([ + 'success' => true, + 'message' => '세금계산서가 등록되었습니다.', + 'data' => ['id' => $record->id], + ]); + } + + public function update(Request $request, int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $record = VatRecord::forTenant($tenantId)->findOrFail($id); + + $request->validate([ + 'partnerName' => 'required|string|max:100', + 'invoiceNo' => 'required|string|max:50', + 'period' => 'required|string|max:20', + 'type' => 'required|in:sales,purchase', + 'supplyAmount' => 'required|integer|min:0', + ]); + + $record->update([ + 'period' => $request->input('period'), + 'type' => $request->input('type'), + 'partner_name' => $request->input('partnerName'), + 'invoice_no' => $request->input('invoiceNo'), + 'invoice_date' => $request->input('invoiceDate'), + 'supply_amount' => $request->input('supplyAmount', 0), + 'vat_amount' => $request->input('vatAmount', 0), + 'total_amount' => $request->input('totalAmount', 0), + 'status' => $request->input('status'), + 'memo' => $request->input('memo'), + ]); + + return response()->json([ + 'success' => true, + 'message' => '세금계산서가 수정되었습니다.', + ]); + } + + public function destroy(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $record = VatRecord::forTenant($tenantId)->findOrFail($id); + $record->delete(); + + return response()->json([ + 'success' => true, + 'message' => '세금계산서가 삭제되었습니다.', + ]); + } +} diff --git a/app/Models/Finance/VatRecord.php b/app/Models/Finance/VatRecord.php new file mode 100644 index 00000000..b648ff58 --- /dev/null +++ b/app/Models/Finance/VatRecord.php @@ -0,0 +1,39 @@ + 'date', + 'supply_amount' => 'integer', + 'vat_amount' => 'integer', + 'total_amount' => 'integer', + ]; + + public function scopeForTenant($query, $tenantId) + { + return $query->where('tenant_id', $tenantId); + } +} diff --git a/resources/views/finance/vat.blade.php b/resources/views/finance/vat.blade.php index 8c562356..985995d3 100644 --- a/resources/views/finance/vat.blade.php +++ b/resources/views/finance/vat.blade.php @@ -9,6 +9,7 @@ @endpush @section('content') +
@endsection @@ -47,17 +48,16 @@ const RefreshCw = createIcon('refresh-cw'); function VatManagement() { - const [vatRecords, setVatRecords] = useState([ - { id: 1, period: '2025-2H', type: 'sales', partnerName: '(주)한국테크', invoiceNo: 'TAX-2025-1234', invoiceDate: '2025-12-15', supplyAmount: 10000000, vatAmount: 1000000, totalAmount: 11000000, status: 'filed' }, - { id: 2, period: '2025-2H', type: 'sales', partnerName: '글로벌솔루션', invoiceNo: 'TAX-2025-1235', invoiceDate: '2025-12-20', supplyAmount: 8500000, vatAmount: 850000, totalAmount: 9350000, status: 'filed' }, - { id: 3, period: '2025-2H', type: 'purchase', partnerName: 'IT솔루션즈', invoiceNo: 'TAX-2025-5001', invoiceDate: '2025-12-10', supplyAmount: 5000000, vatAmount: 500000, totalAmount: 5500000, status: 'filed' }, - { id: 4, period: '2026-1H', type: 'sales', partnerName: '스마트시스템', invoiceNo: 'TAX-2026-0001', invoiceDate: '2026-01-05', supplyAmount: 15000000, vatAmount: 1500000, totalAmount: 16500000, status: 'pending' }, - { id: 5, period: '2026-1H', type: 'purchase', partnerName: '클라우드서비스', invoiceNo: 'TAX-2026-0501', invoiceDate: '2026-01-10', supplyAmount: 3000000, vatAmount: 300000, totalAmount: 3300000, status: 'pending' }, - { id: 6, period: '2026-1H', type: 'sales', partnerName: '디지털웍스', invoiceNo: 'TAX-2026-0002', invoiceDate: '2026-01-15', supplyAmount: 7800000, vatAmount: 780000, totalAmount: 8580000, status: 'pending' }, - ]); + const [vatRecords, setVatRecords] = useState([]); + const [stats, setStats] = useState({ salesSupply: 0, salesVat: 0, purchaseSupply: 0, purchaseVat: 0, total: 0 }); + const [periods, setPeriods] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); const [searchTerm, setSearchTerm] = useState(''); - const [filterPeriod, setFilterPeriod] = useState('2026-1H'); + const now = new Date(); + const currentHalf = now.getMonth() < 6 ? '1H' : '2H'; + const [filterPeriod, setFilterPeriod] = useState(`${now.getFullYear()}-${currentHalf}`); const [filterType, setFilterType] = useState('all'); const [filterStatus, setFilterStatus] = useState('all'); @@ -65,18 +65,30 @@ function VatManagement() { const [modalMode, setModalMode] = useState('add'); const [editingItem, setEditingItem] = useState(null); - const periods = ['2026-1H', '2025-2H', '2025-1H', '2024-2H']; + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + + // 기본 기간 옵션 (DB에서 가져온 기간 + 현재/이전 반기) + const defaultPeriods = (() => { + const y = now.getFullYear(); + const list = []; + for (let i = 0; i <= 2; i++) { + list.push(`${y - i}-1H`, `${y - i}-2H`); + } + return list; + })(); + const allPeriods = [...new Set([...periods, ...defaultPeriods])].sort().reverse(); const initialFormState = { - period: '2026-1H', + period: filterPeriod, type: 'sales', partnerName: '', invoiceNo: '', - invoiceDate: new Date().toISOString().split('T')[0], + invoiceDate: now.toISOString().split('T')[0], supplyAmount: '', vatAmount: '', totalAmount: '', - status: 'pending' + status: 'pending', + memo: '' }; const [formData, setFormData] = useState(initialFormState); @@ -88,7 +100,6 @@ function VatManagement() { }; const parseInputCurrency = (value) => String(value).replace(/[^\d]/g, ''); - // 공급가액 변경시 부가세 자동 계산 const handleSupplyAmountChange = (value) => { const supply = parseInt(parseInputCurrency(value)) || 0; const vat = Math.floor(supply * 0.1); @@ -101,41 +112,102 @@ function VatManagement() { })); }; + const fetchRecords = async (period) => { + setLoading(true); + try { + const params = new URLSearchParams({ period: period || filterPeriod }); + const res = await fetch(`/finance/vat/list?${params}`); + const data = await res.json(); + if (data.success) { + setVatRecords(data.data); + setStats(data.stats); + if (data.periods && data.periods.length > 0) { + setPeriods(data.periods); + } + } + } catch (err) { + console.error('부가세 조회 실패:', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchRecords(filterPeriod); }, [filterPeriod]); + const filteredRecords = vatRecords.filter(item => { const matchesSearch = item.partnerName.toLowerCase().includes(searchTerm.toLowerCase()) || item.invoiceNo.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesPeriod = item.period === filterPeriod; const matchesType = filterType === 'all' || item.type === filterType; const matchesStatus = filterStatus === 'all' || item.status === filterStatus; - return matchesSearch && matchesPeriod && matchesType && matchesStatus; + return matchesSearch && matchesType && matchesStatus; }); - // 기간별 요약 계산 - const periodRecords = vatRecords.filter(r => r.period === filterPeriod); - const salesVat = periodRecords.filter(r => r.type === 'sales').reduce((sum, r) => sum + r.vatAmount, 0); - const purchaseVat = periodRecords.filter(r => r.type === 'purchase').reduce((sum, r) => sum + r.vatAmount, 0); - const salesSupply = periodRecords.filter(r => r.type === 'sales').reduce((sum, r) => sum + r.supplyAmount, 0); - const purchaseSupply = periodRecords.filter(r => r.type === 'purchase').reduce((sum, r) => sum + r.supplyAmount, 0); + const salesVat = stats.salesVat || 0; + const purchaseVat = stats.purchaseVat || 0; + const salesSupply = stats.salesSupply || 0; + const purchaseSupply = stats.purchaseSupply || 0; const netVat = salesVat - purchaseVat; - const handleAdd = () => { setModalMode('add'); setFormData(initialFormState); setShowModal(true); }; - const handleEdit = (item) => { setModalMode('edit'); setEditingItem(item); setFormData({ ...item, supplyAmount: item.supplyAmount.toString(), vatAmount: item.vatAmount.toString(), totalAmount: item.totalAmount.toString() }); setShowModal(true); }; - const handleSave = () => { - if (!formData.partnerName || !formData.invoiceNo || !formData.supplyAmount) { alert('필수 항목을 입력해주세요.'); return; } - const newItem = { - ...formData, - supplyAmount: parseInt(parseInputCurrency(formData.supplyAmount)) || 0, - vatAmount: parseInt(parseInputCurrency(formData.vatAmount)) || 0, - totalAmount: parseInt(parseInputCurrency(formData.totalAmount)) || 0 - }; - if (modalMode === 'add') { - setVatRecords(prev => [{ id: Date.now(), ...newItem }, ...prev]); - } else { - setVatRecords(prev => prev.map(item => item.id === editingItem.id ? { ...item, ...newItem } : item)); - } - setShowModal(false); setEditingItem(null); + const handleAdd = () => { setModalMode('add'); setFormData({ ...initialFormState, period: filterPeriod }); setShowModal(true); }; + const handleEdit = (item) => { + setModalMode('edit'); + setEditingItem(item); + setFormData({ + ...item, + supplyAmount: item.supplyAmount.toString(), + vatAmount: item.vatAmount.toString(), + totalAmount: item.totalAmount.toString() + }); + setShowModal(true); + }; + const handleSave = async () => { + if (!formData.partnerName || !formData.invoiceNo || !formData.supplyAmount) { alert('필수 항목을 입력해주세요.'); return; } + setSaving(true); + try { + const payload = { + ...formData, + supplyAmount: parseInt(parseInputCurrency(formData.supplyAmount)) || 0, + vatAmount: parseInt(parseInputCurrency(formData.vatAmount)) || 0, + totalAmount: parseInt(parseInputCurrency(formData.totalAmount)) || 0, + }; + const url = modalMode === 'add' ? '/finance/vat/store' : `/finance/vat/${editingItem.id}`; + const res = await fetch(url, { + method: modalMode === 'add' ? 'POST' : 'PUT', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (!res.ok) { + const errors = data.errors ? Object.values(data.errors).flat().join('\n') : data.message; + alert(errors || '저장에 실패했습니다.'); + return; + } + setShowModal(false); + setEditingItem(null); + fetchRecords(filterPeriod); + } catch (err) { + console.error('저장 실패:', err); + alert('저장에 실패했습니다.'); + } finally { + setSaving(false); + } + }; + const handleDelete = async (id) => { + if (!confirm('정말 삭제하시겠습니까?')) return; + try { + const res = await fetch(`/finance/vat/${id}`, { + method: 'DELETE', + headers: { 'X-CSRF-TOKEN': csrfToken }, + }); + if (res.ok) { + setShowModal(false); + fetchRecords(filterPeriod); + } + } catch (err) { + console.error('삭제 실패:', err); + alert('삭제에 실패했습니다.'); + } }; - const handleDelete = (id) => { if (confirm('정말 삭제하시겠습니까?')) { setVatRecords(prev => prev.filter(item => item.id !== id)); setShowModal(false); } }; const handleDownload = () => { const rows = [['부가세 관리', getPeriodLabel(filterPeriod)], [], ['구분', '거래처', '세금계산서번호', '발행일', '공급가액', '부가세', '합계', '상태'], @@ -198,7 +270,7 @@ function VatManagement() {
@@ -297,7 +369,9 @@ function VatManagement() { - {filteredRecords.length === 0 ? ( + {loading ? ( +
불러오는 중...
+ ) : filteredRecords.length === 0 ? ( 데이터가 없습니다. ) : filteredRecords.map(item => ( handleEdit(item)} className="hover:bg-gray-50 cursor-pointer"> @@ -324,7 +398,7 @@ function VatManagement() {
-
+
@@ -339,14 +413,15 @@ function VatManagement() {
- {modalMode === 'edit' && ( +
- )} +
setFormData(prev => ({ ...prev, memo: e.target.value }))} placeholder="메모" className="w-full px-3 py-2 border border-gray-300 rounded-lg" />
+
{modalMode === 'edit' && } - +
diff --git a/routes/web.php b/routes/web.php index b5c569c5..0002e58b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -998,6 +998,14 @@ return view('finance.vat'); })->name('vat'); + + // 부가세 관리 API + Route::prefix('vat')->name('vat.')->group(function () { + Route::get('/list', [\App\Http\Controllers\Finance\VatRecordController::class, 'index'])->name('list'); + Route::post('/store', [\App\Http\Controllers\Finance\VatRecordController::class, 'store'])->name('store'); + Route::put('/{id}', [\App\Http\Controllers\Finance\VatRecordController::class, 'update'])->name('update'); + Route::delete('/{id}', [\App\Http\Controllers\Finance\VatRecordController::class, 'destroy'])->name('destroy'); + }); }); /*