feat:재무관리 4개 페이지 수정 (부가세/매출/미지급금)

- 부가세관리: 신고기간 1P/1C/2P/2C 형식, 세금구분(과세/영세/면세), 카드 공제분 매입 반영, 라벨 변경
- 매출관리: 작성일자/승인번호 라벨, 구분(과세/영세/면세) 추가
- 미지급금: 결제예정일/거래일자 라벨, 청구서번호 숨김, 매입세금계산서 발행여부 체크박스

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-05 18:08:11 +09:00
parent 39f239c938
commit d160dd7fb7
9 changed files with 245 additions and 75 deletions

View File

@@ -17,8 +17,7 @@ public function index(Request $request): JsonResponse
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('vendor_name', 'like', "%{$search}%")
->orWhere('invoice_no', 'like', "%{$search}%");
$q->where('vendor_name', 'like', "%{$search}%");
});
}
@@ -49,6 +48,7 @@ public function index(Request $request): JsonResponse
'status' => $item->status,
'description' => $item->description,
'memo' => $item->memo,
'taxInvoiceIssued' => (bool) $item->tax_invoice_issued,
];
});
@@ -79,7 +79,7 @@ public function store(Request $request): JsonResponse
{
$request->validate([
'vendorName' => 'required|string|max:100',
'invoiceNo' => 'required|string|max:50',
'invoiceNo' => 'nullable|string|max:50',
'amount' => 'required|integer|min:0',
]);
@@ -97,6 +97,7 @@ public function store(Request $request): JsonResponse
'status' => 'unpaid',
'description' => $request->input('description'),
'memo' => $request->input('memo'),
'tax_invoice_issued' => $request->boolean('taxInvoiceIssued', false),
]);
return response()->json([
@@ -112,7 +113,7 @@ public function update(Request $request, int $id): JsonResponse
$request->validate([
'vendorName' => 'required|string|max:100',
'invoiceNo' => 'required|string|max:50',
'invoiceNo' => 'nullable|string|max:50',
'amount' => 'required|integer|min:0',
]);
@@ -126,6 +127,7 @@ public function update(Request $request, int $id): JsonResponse
'status' => $request->input('status', $payable->status),
'description' => $request->input('description'),
'memo' => $request->input('memo'),
'tax_invoice_issued' => $request->boolean('taxInvoiceIssued', $payable->tax_invoice_issued),
]);
return response()->json([

View File

@@ -30,8 +30,8 @@ public function index(Request $request): JsonResponse
$records = $query->orderBy('date', 'desc')->get()->map(fn($item) => [
'id' => $item->id, 'date' => $item->date?->format('Y-m-d'),
'customer' => $item->customer, 'project' => $item->project,
'type' => $item->type, 'amount' => $item->amount,
'vat' => $item->vat, 'status' => $item->status,
'type' => $item->type, 'taxType' => $item->tax_type ?? 'taxable',
'amount' => $item->amount, 'vat' => $item->vat, 'status' => $item->status,
'invoiceNo' => $item->invoice_no, 'memo' => $item->memo,
]);
@@ -48,14 +48,18 @@ public function index(Request $request): JsonResponse
public function store(Request $request): JsonResponse
{
$request->validate(['customer' => 'required|string|max:100', 'amount' => 'required|integer|min:0']);
$request->validate([
'customer' => 'required|string|max:100',
'amount' => 'required|integer|min:0',
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
]);
$tenantId = session('selected_tenant_id', 1);
SalesRecord::create([
'tenant_id' => $tenantId, 'date' => $request->input('date'),
'customer' => $request->input('customer'), 'project' => $request->input('project'),
'type' => $request->input('type'), 'amount' => $request->input('amount', 0),
'vat' => $request->input('vat', 0),
'type' => $request->input('type'), 'tax_type' => $request->input('taxType', 'taxable'),
'amount' => $request->input('amount', 0), 'vat' => $request->input('vat', 0),
'status' => $request->input('status', 'contracted'),
'invoice_no' => $request->input('invoiceNo'), 'memo' => $request->input('memo'),
]);
@@ -67,11 +71,16 @@ public function update(Request $request, int $id): JsonResponse
{
$tenantId = session('selected_tenant_id', 1);
$item = SalesRecord::forTenant($tenantId)->findOrFail($id);
$request->validate(['customer' => 'required|string|max:100', 'amount' => 'required|integer|min:0']);
$request->validate([
'customer' => 'required|string|max:100',
'amount' => 'required|integer|min:0',
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
]);
$item->update([
'date' => $request->input('date'), 'customer' => $request->input('customer'),
'project' => $request->input('project'), 'type' => $request->input('type', $item->type),
'tax_type' => $request->input('taxType', $item->tax_type),
'amount' => $request->input('amount'), 'vat' => $request->input('vat', $item->vat),
'status' => $request->input('status', $item->status),
'invoice_no' => $request->input('invoiceNo'), 'memo' => $request->input('memo'),

View File

@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Models\Finance\VatRecord;
use App\Models\Finance\CardTransaction;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -17,8 +18,7 @@ public function index(Request $request): JsonResponse
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('partner_name', 'like', "%{$search}%")
->orWhere('invoice_no', 'like', "%{$search}%");
$q->where('partner_name', 'like', "%{$search}%");
});
}
@@ -32,6 +32,12 @@ public function index(Request $request): JsonResponse
}
}
if ($taxType = $request->input('tax_type')) {
if ($taxType !== 'all') {
$query->where('tax_type', $taxType);
}
}
if ($status = $request->input('status')) {
if ($status !== 'all') {
$query->where('status', $status);
@@ -45,6 +51,7 @@ public function index(Request $request): JsonResponse
'id' => $record->id,
'period' => $record->period,
'type' => $record->type,
'taxType' => $record->tax_type ?? 'taxable',
'partnerName' => $record->partner_name,
'invoiceNo' => $record->invoice_no,
'invoiceDate' => $record->invoice_date?->format('Y-m-d'),
@@ -53,9 +60,53 @@ public function index(Request $request): JsonResponse
'totalAmount' => $record->total_amount,
'status' => $record->status,
'memo' => $record->memo,
'isCardTransaction' => false,
];
});
// 카드 공제분 매입 반영
$cardRecords = collect();
$cardPurchaseSupply = 0;
$cardPurchaseVat = 0;
if ($period = $request->input('period')) {
[$startDate, $endDate] = $this->periodToDateRange($period);
if ($startDate && $endDate) {
$cardQuery = CardTransaction::forTenant($tenantId)
->where('deduction_type', 'deductible')
->whereBetween('transaction_date', [$startDate, $endDate]);
$cardTransactions = $cardQuery->orderBy('transaction_date', 'desc')->get();
$cardRecords = $cardTransactions->map(function ($card) use ($period) {
$supply = (int) round($card->amount / 1.1);
$vat = $card->amount - $supply;
return [
'id' => 'card_' . $card->id,
'period' => $period,
'type' => 'purchase',
'taxType' => 'taxable',
'partnerName' => $card->merchant,
'invoiceNo' => $card->approval_no ?? '',
'invoiceDate' => $card->transaction_date?->format('Y-m-d'),
'supplyAmount' => $supply,
'vatAmount' => $vat,
'totalAmount' => $card->amount,
'status' => 'filed',
'memo' => $card->memo,
'isCardTransaction' => true,
];
});
$cardPurchaseSupply = $cardRecords->sum('supplyAmount');
$cardPurchaseVat = $cardRecords->sum('vatAmount');
}
}
// 통합 records
$allRecords = $records->concat($cardRecords)->values();
// 해당 기간 통계
$periodQuery = VatRecord::forTenant($tenantId);
if ($period = $request->input('period')) {
@@ -66,8 +117,10 @@ public function index(Request $request): JsonResponse
$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'),
'purchaseSupply' => $periodRecords->where('type', 'purchase')->sum('supply_amount') + $cardPurchaseSupply,
'purchaseVat' => $periodRecords->where('type', 'purchase')->sum('vat_amount') + $cardPurchaseVat,
'cardPurchaseSupply' => $cardPurchaseSupply,
'cardPurchaseVat' => $cardPurchaseVat,
'total' => $periodRecords->count(),
];
@@ -81,7 +134,7 @@ public function index(Request $request): JsonResponse
return response()->json([
'success' => true,
'data' => $records,
'data' => $allRecords,
'stats' => $stats,
'periods' => $periods,
]);
@@ -91,9 +144,9 @@ 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',
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
'supplyAmount' => 'required|integer|min:0',
]);
@@ -103,6 +156,7 @@ public function store(Request $request): JsonResponse
'tenant_id' => $tenantId,
'period' => $request->input('period'),
'type' => $request->input('type', 'sales'),
'tax_type' => $request->input('taxType', 'taxable'),
'partner_name' => $request->input('partnerName'),
'invoice_no' => $request->input('invoiceNo'),
'invoice_date' => $request->input('invoiceDate'),
@@ -127,15 +181,16 @@ public function update(Request $request, int $id): JsonResponse
$request->validate([
'partnerName' => 'required|string|max:100',
'invoiceNo' => 'required|string|max:50',
'period' => 'required|string|max:20',
'type' => 'required|in:sales,purchase',
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
'supplyAmount' => 'required|integer|min:0',
]);
$record->update([
'period' => $request->input('period'),
'type' => $request->input('type'),
'tax_type' => $request->input('taxType', $record->tax_type),
'partner_name' => $request->input('partnerName'),
'invoice_no' => $request->input('invoiceNo'),
'invoice_date' => $request->input('invoiceDate'),
@@ -163,4 +218,31 @@ public function destroy(int $id): JsonResponse
'message' => '세금계산서가 삭제되었습니다.',
]);
}
/**
* 부가세 신고기간을 날짜 범위로 변환
* YYYY-1P: 1기 예정 (0101~0331)
* YYYY-1C: 1기 확정 (0401~0630)
* YYYY-2P: 2기 예정 (0701~0930)
* YYYY-2C: 2기 확정 (1001~1231)
*/
private function periodToDateRange(string $period): array
{
$parts = explode('-', $period);
if (count($parts) !== 2) {
return [null, null];
}
$year = $parts[0];
$code = $parts[1];
$ranges = [
'1P' => ["{$year}-01-01", "{$year}-03-31"],
'1C' => ["{$year}-04-01", "{$year}-06-30"],
'2P' => ["{$year}-07-01", "{$year}-09-30"],
'2C' => ["{$year}-10-01", "{$year}-12-31"],
];
return $ranges[$code] ?? [null, null];
}
}

View File

@@ -23,6 +23,7 @@ class Payable extends Model
'status',
'description',
'memo',
'tax_invoice_issued',
];
protected $casts = [
@@ -30,6 +31,7 @@ class Payable extends Model
'paid_amount' => 'integer',
'issue_date' => 'date',
'due_date' => 'date',
'tax_invoice_issued' => 'boolean',
];
public function scopeForTenant($query, $tenantId)

View File

@@ -11,7 +11,7 @@ class SalesRecord extends Model
protected $table = 'sales_records';
protected $fillable = [
'tenant_id', 'date', 'customer', 'project', 'type',
'tenant_id', 'date', 'customer', 'project', 'type', 'tax_type',
'amount', 'vat', 'status', 'invoice_no', 'memo',
];

View File

@@ -15,6 +15,7 @@ class VatRecord extends Model
'tenant_id',
'period',
'type',
'tax_type',
'partner_name',
'invoice_no',
'invoice_date',

View File

@@ -81,7 +81,8 @@ function PayablesManagement() {
status: 'unpaid',
category: '사무용품',
description: '',
memo: ''
memo: '',
taxInvoiceIssued: false
};
const [formData, setFormData] = useState(initialFormState);
@@ -120,8 +121,7 @@ function PayablesManagement() {
useEffect(() => { fetchPayables(); }, []);
const filteredPayables = payables.filter(item => {
const matchesSearch = (item.vendorName || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
(item.invoiceNo || '').toLowerCase().includes(searchTerm.toLowerCase());
const matchesSearch = (item.vendorName || '').toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = filterStatus === 'all' || item.status === filterStatus;
const matchesCategory = filterCategory === 'all' || item.category === filterCategory;
return matchesSearch && matchesStatus && matchesCategory;
@@ -132,12 +132,12 @@ function PayablesManagement() {
setModalMode('edit');
setEditingItem(item);
const safeItem = {};
Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? ''; });
Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? (key === 'taxInvoiceIssued' ? false : ''); });
setFormData(safeItem);
setShowModal(true);
};
const handleSave = async () => {
if (!formData.vendorName || !formData.invoiceNo || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; }
if (!formData.vendorName || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; }
setSaving(true);
try {
const url = modalMode === 'add' ? '/finance/payables/store' : `/finance/payables/${editingItem.id}`;
@@ -214,8 +214,8 @@ function PayablesManagement() {
};
const handleDownload = () => {
const rows = [['미지급금 관리'], [], ['거래처', '청구서번호', '발행일', '만기일', '분류', '청구금액', '지급액', '잔액', '상태'],
...filteredPayables.map(item => [item.vendorName, item.invoiceNo, item.issueDate, item.dueDate, item.category, item.amount, item.paidAmount, item.amount - item.paidAmount, getStatusLabel(item.status)])];
const rows = [['미지급금 관리'], [], ['거래처', '거래일자', '결제예정일', '분류', '청구금액', '지급액', '잔액', '상태', '세금계산서'],
...filteredPayables.map(item => [item.vendorName, item.issueDate, item.dueDate, item.category, item.amount, item.paidAmount, item.amount - item.paidAmount, getStatusLabel(item.status), item.taxInvoiceIssued ? 'O' : 'X'])];
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 = `미지급금관리_${new Date().toISOString().split('T')[0]}.csv`; link.click();
@@ -275,7 +275,7 @@ function PayablesManagement() {
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="md:col-span-2 relative">
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input type="text" placeholder="거래처, 청구서번호 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-rose-500" />
<input type="text" placeholder="거래처 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-rose-500" />
</div>
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
<option value="all">전체 분류</option>
@@ -299,12 +299,12 @@ function PayablesManagement() {
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">거래처</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">청구서번호</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">만기일</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">결제예정일</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">분류</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">청구금액</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">잔액</th>
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">상태</th>
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">세금계산서</th>
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">연체일</th>
</tr>
</thead>
@@ -316,12 +316,12 @@ function PayablesManagement() {
) : filteredPayables.map(item => (
<tr key={item.id} onClick={() => handleEdit(item)} className={`hover:bg-gray-50 cursor-pointer ${item.status === 'overdue' ? 'bg-red-50/50' : ''}`}>
<td className="px-6 py-4"><p className="text-sm font-medium text-gray-900">{item.vendorName}</p><p className="text-xs text-gray-400">{item.description}</p></td>
<td className="px-6 py-4 text-sm text-gray-600">{item.invoiceNo}</td>
<td className="px-6 py-4 text-sm text-gray-600">{item.dueDate}</td>
<td className="px-6 py-4"><span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">{item.category}</span></td>
<td className="px-6 py-4 text-sm font-medium text-right text-gray-900">{formatCurrency(item.amount)}</td>
<td className="px-6 py-4 text-sm font-bold text-right text-rose-600">{formatCurrency(item.amount - item.paidAmount)}</td>
<td className="px-6 py-4 text-center"><span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusStyle(item.status)}`}>{getStatusLabel(item.status)}</span></td>
<td className="px-6 py-4 text-center">{item.taxInvoiceIssued ? <span className="text-emerald-600 font-bold text-sm">O</span> : <span className="text-gray-300 text-sm">-</span>}</td>
<td className="px-6 py-4 text-center text-sm">{item.status === 'overdue' ? <span className="text-red-600 font-medium">{calculateOverdueDays(item.dueDate)}</span> : '-'}</td>
</tr>
))}
@@ -337,13 +337,13 @@ function PayablesManagement() {
<button onClick={() => setShowModal(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="text" value={formData.vendorName} onChange={(e) => setFormData(prev => ({ ...prev, vendorName: e.target.value }))} placeholder="거래처명" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">청구서번호 *</label><input type="text" value={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: e.target.value }))} placeholder="PO-2026-001" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">거래처 *</label>
<input type="text" value={formData.vendorName} onChange={(e) => setFormData(prev => ({ ...prev, vendorName: e.target.value }))} placeholder="거래처명" className="w-full px-3 py-2 border border-gray-300 rounded-lg" />
</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="date" value={formData.issueDate} onChange={(e) => setFormData(prev => ({ ...prev, issueDate: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">만기</label><input type="date" value={formData.dueDate} onChange={(e) => setFormData(prev => ({ ...prev, dueDate: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">거래일자</label><input type="date" value={formData.issueDate} onChange={(e) => setFormData(prev => ({ ...prev, issueDate: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">결제예정</label><input type="date" value={formData.dueDate} onChange={(e) => setFormData(prev => ({ ...prev, dueDate: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div><label className="block text-sm font-medium text-gray-700 mb-1">분류</label><select value={formData.category} onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{categories.map(cat => <option key={cat} value={cat}>{cat}</option>)}</select></div>
@@ -351,6 +351,10 @@ function PayablesManagement() {
</div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">적요</label><input type="text" value={formData.description} onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} placeholder="적요 입력" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">메모</label><textarea value={formData.memo} onChange={(e) => setFormData(prev => ({ ...prev, memo: e.target.value }))} placeholder="부분지급, 연체 사유 등 메모" rows="2" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<input type="checkbox" id="taxInvoiceIssued" checked={formData.taxInvoiceIssued} onChange={(e) => setFormData(prev => ({ ...prev, taxInvoiceIssued: e.target.checked }))} className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
<label htmlFor="taxInvoiceIssued" className="text-sm font-medium text-gray-700 cursor-pointer">매입세금계산서 발행여부</label>
</div>
{modalMode === 'edit' && (
<div><label className="block text-sm font-medium text-gray-700 mb-1">상태</label><select value={formData.status} onChange={(e) => setFormData(prev => ({ ...prev, status: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="unpaid">미지급</option><option value="partial">부분지급</option><option value="paid">지급완료</option><option value="overdue">연체</option></select></div>
)}

View File

@@ -74,11 +74,26 @@ function SalesManagement() {
{ value: 'completed', label: '매출확정', color: 'bg-gray-100 text-gray-700' }
];
const getTaxTypeLabel = (taxType) => {
const labels = { 'taxable': '과세', 'zero_rated': '영세', 'exempt': '면세' };
return labels[taxType] || taxType;
};
const getTaxTypeStyle = (taxType) => {
const styles = {
'taxable': 'bg-blue-100 text-blue-700',
'zero_rated': 'bg-teal-100 text-teal-700',
'exempt': 'bg-gray-100 text-gray-600'
};
return styles[taxType] || 'bg-gray-100 text-gray-700';
};
const initialFormState = {
date: new Date().toISOString().split('T')[0],
customer: '',
project: '',
type: '프로젝트',
taxType: 'taxable',
amount: '',
vat: '',
status: 'negotiating',
@@ -178,8 +193,8 @@ function SalesManagement() {
};
const handleDownload = () => {
const rows = [['매출관리', `${dateRange.start} ~ ${dateRange.end}`], [], ['날짜', '거래처', '프로젝트', '유형', '공급가액', 'VAT', '합계', '상태'],
...filteredSales.map(item => [item.date, item.customer, item.project, item.type, item.amount, item.vat, item.amount + item.vat, statuses.find(s => s.value === item.status)?.label])];
const rows = [['매출관리', `${dateRange.start} ~ ${dateRange.end}`], [], ['작성일자', '거래처', '프로젝트', '유형', '구분', '공급가액', 'VAT', '합계', '상태'],
...filteredSales.map(item => [item.date, item.customer, item.project, item.type, getTaxTypeLabel(item.taxType), item.amount, item.vat, item.amount + item.vat, statuses.find(s => s.value === item.status)?.label])];
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 = `매출관리_${dateRange.start}_${dateRange.end}.csv`; link.click();
@@ -243,10 +258,11 @@ function SalesManagement() {
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">날짜</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">작성일자</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">거래처</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">프로젝트</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">유형</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">구분</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">공급가액</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">합계(VAT포함)</th>
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">상태</th>
@@ -255,20 +271,21 @@ function SalesManagement() {
</thead>
<tbody className="divide-y divide-gray-100">
{loading ? (
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">
<tr><td colSpan="9" className="px-6 py-12 text-center text-gray-400">
<div className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
데이터를 불러오는 ...
</div>
</td></tr>
) : filteredSales.length === 0 ? (
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
<tr><td colSpan="9" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
) : filteredSales.map(item => (
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => handleEdit(item)}>
<td className="px-6 py-4 text-sm text-gray-600">{item.date}</td>
<td className="px-6 py-4"><p className="text-sm font-medium text-gray-900">{item.customer}</p></td>
<td className="px-6 py-4"><p className="text-sm text-gray-600">{item.project}</p>{item.memo && <p className="text-xs text-gray-400">{item.memo}</p>}</td>
<td className="px-6 py-4"><span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">{item.type}</span></td>
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTaxTypeStyle(item.taxType)}`}>{getTaxTypeLabel(item.taxType)}</span></td>
<td className="px-6 py-4 text-sm font-medium text-right text-gray-900">{formatCurrency(item.amount)}</td>
<td className="px-6 py-4 text-sm font-bold text-right text-blue-600">{formatCurrency(item.amount + item.vat)}</td>
<td className="px-6 py-4 text-center"><span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(item.status)}`}>{getStatusLabel(item.status)}</span></td>
@@ -291,7 +308,7 @@ function SalesManagement() {
</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="date" value={formData.date} onChange={(e) => setFormData(prev => ({ ...prev, date: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">작성일자 *</label><input type="date" value={formData.date} onChange={(e) => setFormData(prev => ({ ...prev, date: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">유형</label><select value={formData.type} onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{types.map(t => <option key={t} value={t}>{t}</option>)}</select></div>
</div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">거래처 *</label><input type="text" value={formData.customer} onChange={(e) => setFormData(prev => ({ ...prev, customer: e.target.value }))} placeholder="거래처명" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
@@ -300,9 +317,10 @@ function SalesManagement() {
<div><label className="block text-sm font-medium text-gray-700 mb-1">공급가액 *</label><input type="text" value={formatInputCurrency(formData.amount)} onChange={(e) => setFormData(prev => ({ ...prev, amount: parseInputCurrency(e.target.value) }))} placeholder="0" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-right" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">VAT</label><input type="text" value={formatInputCurrency(formData.vat)} onChange={(e) => setFormData(prev => ({ ...prev, vat: parseInputCurrency(e.target.value) }))} placeholder="자동계산" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-right" /></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-3 gap-4">
<div><label className="block text-sm font-medium text-gray-700 mb-1">구분</label><select value={formData.taxType} onChange={(e) => setFormData(prev => ({ ...prev, taxType: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="taxable">과세</option><option value="zero_rated">영세</option><option value="exempt">면세</option></select></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">상태</label><select value={formData.status} onChange={(e) => setFormData(prev => ({ ...prev, status: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{statuses.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}</select></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">세금계산서 번호</label><input type="text" value={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: e.target.value }))} placeholder="SAL-2026-001" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">승인번호</label><input type="text" value={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: e.target.value }))} placeholder="승인번호" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
</div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">메모</label><input type="text" value={formData.memo} onChange={(e) => setFormData(prev => ({ ...prev, memo: e.target.value }))} placeholder="메모" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
</div>

View File

@@ -49,16 +49,18 @@
function VatManagement() {
const [vatRecords, setVatRecords] = useState([]);
const [stats, setStats] = useState({ salesSupply: 0, salesVat: 0, purchaseSupply: 0, purchaseVat: 0, total: 0 });
const [stats, setStats] = useState({ salesSupply: 0, salesVat: 0, purchaseSupply: 0, purchaseVat: 0, cardPurchaseSupply: 0, cardPurchaseVat: 0, total: 0 });
const [periods, setPeriods] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const now = new Date();
const currentHalf = now.getMonth() < 6 ? '1H' : '2H';
const [filterPeriod, setFilterPeriod] = useState(`${now.getFullYear()}-${currentHalf}`);
const month = now.getMonth();
const currentPeriodCode = month < 3 ? '1P' : month < 6 ? '1C' : month < 9 ? '2P' : '2C';
const [filterPeriod, setFilterPeriod] = useState(`${now.getFullYear()}-${currentPeriodCode}`);
const [filterType, setFilterType] = useState('all');
const [filterTaxType, setFilterTaxType] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
const [showModal, setShowModal] = useState(false);
@@ -67,12 +69,12 @@ function VatManagement() {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// 기본 기간 옵션 (DB에서 가져온 기간 + 현재/이전 반기)
// 기본 기간 옵션 (1P/1C/2P/2C 형식)
const defaultPeriods = (() => {
const y = now.getFullYear();
const list = [];
for (let i = 0; i <= 2; i++) {
list.push(`${y - i}-1H`, `${y - i}-2H`);
list.push(`${y - i}-1P`, `${y - i}-1C`, `${y - i}-2P`, `${y - i}-2C`);
}
return list;
})();
@@ -81,6 +83,7 @@ function VatManagement() {
const initialFormState = {
period: filterPeriod,
type: 'sales',
taxType: 'taxable',
partnerName: '',
invoiceNo: '',
invoiceDate: now.toISOString().split('T')[0],
@@ -102,7 +105,8 @@ function VatManagement() {
const handleSupplyAmountChange = (value) => {
const supply = parseInt(parseInputCurrency(value)) || 0;
const vat = Math.floor(supply * 0.1);
const taxType = formData.taxType;
const vat = taxType === 'taxable' ? Math.floor(supply * 0.1) : 0;
const total = supply + vat;
setFormData(prev => ({
...prev,
@@ -112,6 +116,18 @@ function VatManagement() {
}));
};
const handleTaxTypeChange = (newTaxType) => {
const supply = parseInt(parseInputCurrency(formData.supplyAmount)) || 0;
const vat = newTaxType === 'taxable' ? Math.floor(supply * 0.1) : 0;
const total = supply + vat;
setFormData(prev => ({
...prev,
taxType: newTaxType,
vatAmount: vat.toString(),
totalAmount: total.toString()
}));
};
const fetchRecords = async (period) => {
setLoading(true);
try {
@@ -135,21 +151,27 @@ function VatManagement() {
useEffect(() => { fetchRecords(filterPeriod); }, [filterPeriod]);
const filteredRecords = vatRecords.filter(item => {
const matchesSearch = item.partnerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.invoiceNo.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType = filterType === 'all' || item.type === filterType;
const matchesSearch = item.partnerName.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType = filterType === 'all' || item.type === filterType || (filterType === 'purchase_card' && item.isCardTransaction);
const matchesTaxType = filterTaxType === 'all' || item.taxType === filterTaxType;
const matchesStatus = filterStatus === 'all' || item.status === filterStatus;
return matchesSearch && matchesType && matchesStatus;
if (filterType === 'purchase_card') return item.isCardTransaction && matchesSearch && matchesTaxType && matchesStatus;
if (filterType === 'purchase' && !item.isCardTransaction) return item.type === 'purchase' && matchesSearch && matchesTaxType && matchesStatus;
if (filterType !== 'all' && filterType !== 'purchase_card') return item.type === filterType && !item.isCardTransaction && matchesSearch && matchesTaxType && matchesStatus;
return matchesSearch && matchesTaxType && matchesStatus;
});
const salesVat = stats.salesVat || 0;
const purchaseVat = stats.purchaseVat || 0;
const salesSupply = stats.salesSupply || 0;
const purchaseSupply = stats.purchaseSupply || 0;
const cardPurchaseSupply = stats.cardPurchaseSupply || 0;
const cardPurchaseVat = stats.cardPurchaseVat || 0;
const netVat = salesVat - purchaseVat;
const handleAdd = () => { setModalMode('add'); setFormData({ ...initialFormState, period: filterPeriod }); setShowModal(true); };
const handleEdit = (item) => {
if (item.isCardTransaction) return;
setModalMode('edit');
setEditingItem(item);
setFormData({
@@ -161,7 +183,7 @@ function VatManagement() {
setShowModal(true);
};
const handleSave = async () => {
if (!formData.partnerName || !formData.invoiceNo || !formData.supplyAmount) { alert('필수 항목을 입력해주세요.'); return; }
if (!formData.partnerName || !formData.supplyAmount) { alert('필수 항목을 입력해주세요.'); return; }
setSaving(true);
try {
const payload = {
@@ -210,18 +232,33 @@ function VatManagement() {
};
const handleDownload = () => {
const rows = [['부가세 관리', getPeriodLabel(filterPeriod)], [], ['구분', '거래처', '세금계산서번호', '발행일', '공급가액', '부가세', '합계', '상태'],
...filteredRecords.map(item => [getTypeLabel(item.type), item.partnerName, item.invoiceNo, item.invoiceDate, item.supplyAmount, item.vatAmount, item.totalAmount, getStatusLabel(item.status)])];
const rows = [['부가세 관리', getPeriodLabel(filterPeriod)], [], ['구분', '세금구분', '거래처', '작성일자', '공급가액', '부가세', '합계', '상태'],
...filteredRecords.map(item => [getTypeLabel(item.type, item.isCardTransaction), getTaxTypeLabel(item.taxType), item.partnerName, item.invoiceDate, item.supplyAmount, item.vatAmount, item.totalAmount, getStatusLabel(item.status)])];
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 = `부가세관리_${filterPeriod}.csv`; link.click();
};
const getTypeLabel = (type) => {
const getTypeLabel = (type, isCard = false) => {
if (isCard) return '매입(카드)';
const labels = { 'sales': '매출', 'purchase': '매입' };
return labels[type] || type;
};
const getTaxTypeLabel = (taxType) => {
const labels = { 'taxable': '과세', 'zero_rated': '영세', 'exempt': '면세' };
return labels[taxType] || taxType;
};
const getTaxTypeStyle = (taxType) => {
const styles = {
'taxable': 'bg-blue-100 text-blue-700',
'zero_rated': 'bg-teal-100 text-teal-700',
'exempt': 'bg-gray-100 text-gray-600'
};
return styles[taxType] || 'bg-gray-100 text-gray-700';
};
const getStatusLabel = (status) => {
const labels = { 'pending': '미신고', 'filed': '신고완료', 'paid': '납부완료' };
return labels[status] || status;
@@ -236,7 +273,8 @@ function VatManagement() {
return styles[status] || 'bg-gray-100 text-gray-700';
};
const getTypeStyle = (type) => {
const getTypeStyle = (type, isCard = false) => {
if (isCard) return 'bg-purple-100 text-purple-700';
const styles = {
'sales': 'bg-emerald-100 text-emerald-700',
'purchase': 'bg-pink-100 text-pink-700'
@@ -245,8 +283,9 @@ function VatManagement() {
};
const getPeriodLabel = (period) => {
const [year, half] = period.split('-');
return `${year}년 ${half === '1H' ? '1기' : '2기'}`;
const [year, code] = period.split('-');
const labels = { '1P': '1기 예정', '1C': '1기 확정', '2P': '2기 예정', '2C': '2기 확정' };
return `${year}년 ${labels[code] || code}`;
};
return (
@@ -318,8 +357,13 @@ function VatManagement() {
</tr>
<tr className="border-b border-gray-100">
<td className="px-6 py-3 text-sm">매입</td>
<td className="px-6 py-3 text-sm text-right">{formatCurrency(purchaseSupply)}</td>
<td className="px-6 py-3 text-sm text-right text-pink-600 font-medium">({formatCurrency(purchaseVat)})</td>
<td className="px-6 py-3 text-sm text-right">{formatCurrency(purchaseSupply - cardPurchaseSupply)}</td>
<td className="px-6 py-3 text-sm text-right text-pink-600 font-medium">({formatCurrency(purchaseVat - cardPurchaseVat)})</td>
</tr>
<tr className="border-b border-gray-100 bg-purple-50/50">
<td className="px-6 py-3 text-sm text-purple-700">매입(카드)</td>
<td className="px-6 py-3 text-sm text-right text-purple-600">{formatCurrency(cardPurchaseSupply)}</td>
<td className="px-6 py-3 text-sm text-right text-purple-600 font-medium">({formatCurrency(cardPurchaseVat)})</td>
</tr>
<tr className="bg-gray-50 font-bold">
<td className="px-6 py-3 text-sm">{netVat >= 0 ? '납부세액' : '환급세액'}</td>
@@ -331,15 +375,22 @@ function VatManagement() {
</div>
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<div className="md:col-span-2 relative">
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input type="text" placeholder="거래처, 세금계산서번호 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500" />
<input type="text" placeholder="거래처 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500" />
</div>
<select value={filterType} onChange={(e) => setFilterType(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
<option value="all">전체 유형</option>
<option value="sales">매출</option>
<option value="purchase">매입</option>
<option value="purchase_card">매입(카드)</option>
</select>
<select value={filterTaxType} onChange={(e) => setFilterTaxType(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
<option value="all">전체 세금구분</option>
<option value="taxable">과세</option>
<option value="zero_rated">영세</option>
<option value="exempt">면세</option>
</select>
<div className="flex gap-1">
{['all', 'pending', 'filed', 'paid'].map(status => (
@@ -348,7 +399,7 @@ function VatManagement() {
</button>
))}
</div>
<button onClick={() => { setSearchTerm(''); setFilterType('all'); setFilterStatus('all'); }} className="flex items-center justify-center gap-2 px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
<button onClick={() => { setSearchTerm(''); setFilterType('all'); setFilterTaxType('all'); setFilterStatus('all'); }} className="flex items-center justify-center gap-2 px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
<RefreshCw className="w-4 h-4" /><span className="text-sm">초기화</span>
</button>
</div>
@@ -359,9 +410,9 @@ function VatManagement() {
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">구분</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">세금구분</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">거래처</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">세금계산서번호</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">발행일</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">작성일자</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">공급가액</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">부가세</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">합계</th>
@@ -374,10 +425,10 @@ function VatManagement() {
) : filteredRecords.length === 0 ? (
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
) : filteredRecords.map(item => (
<tr key={item.id} onClick={() => handleEdit(item)} className="hover:bg-gray-50 cursor-pointer">
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTypeStyle(item.type)}`}>{getTypeLabel(item.type)}</span></td>
<tr key={item.id} onClick={() => handleEdit(item)} className={`hover:bg-gray-50 ${item.isCardTransaction ? 'bg-purple-50/40 cursor-default' : 'cursor-pointer'}`}>
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTypeStyle(item.type, item.isCardTransaction)}`}>{getTypeLabel(item.type, item.isCardTransaction)}</span></td>
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTaxTypeStyle(item.taxType)}`}>{getTaxTypeLabel(item.taxType)}</span></td>
<td className="px-6 py-4 text-sm font-medium text-gray-900">{item.partnerName}</td>
<td className="px-6 py-4 text-sm text-gray-600">{item.invoiceNo}</td>
<td className="px-6 py-4 text-sm text-gray-600">{item.invoiceDate}</td>
<td className="px-6 py-4 text-sm font-medium text-right text-gray-900">{formatCurrency(item.supplyAmount)}</td>
<td className="px-6 py-4 text-sm text-right text-gray-600">{formatCurrency(item.vatAmount)}</td>
@@ -397,16 +448,17 @@ function VatManagement() {
<button onClick={() => setShowModal(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 className="grid grid-cols-3 gap-4">
<div><label className="block text-sm font-medium text-gray-700 mb-1">신고기간 *</label><select value={formData.period} onChange={(e) => setFormData(prev => ({ ...prev, period: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{allPeriods.map(p => <option key={p} value={p}>{getPeriodLabel(p)}</option>)}</select></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">구분 *</label><select value={formData.type} onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="sales">매출</option><option value="purchase">매입</option></select></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">세금구분</label><select value={formData.taxType} onChange={(e) => handleTaxTypeChange(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="taxable">과세</option><option value="zero_rated">영세</option><option value="exempt">면세</option></select></div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">거래처 *</label>
<input type="text" value={formData.partnerName} onChange={(e) => setFormData(prev => ({ ...prev, partnerName: e.target.value }))} placeholder="거래처명" className="w-full px-3 py-2 border border-gray-300 rounded-lg" />
</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={formData.partnerName} onChange={(e) => setFormData(prev => ({ ...prev, partnerName: e.target.value }))} placeholder="거래처명" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">세금계산서번호 *</label><input type="text" value={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: e.target.value }))} placeholder="TAX-2026-0001" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
</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="date" value={formData.invoiceDate} onChange={(e) => setFormData(prev => ({ ...prev, invoiceDate: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">작성일자</label><input type="date" value={formData.invoiceDate} onChange={(e) => setFormData(prev => ({ ...prev, invoiceDate: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">공급가액 *</label><input type="text" value={formatInputCurrency(formData.supplyAmount)} onChange={(e) => handleSupplyAmountChange(e.target.value)} placeholder="0" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-right" /></div>
</div>
<div className="grid grid-cols-2 gap-4">