diff --git a/app/Http/Controllers/Barobill/HometaxController.php b/app/Http/Controllers/Barobill/HometaxController.php
index 301c3025..27226eda 100644
--- a/app/Http/Controllers/Barobill/HometaxController.php
+++ b/app/Http/Controllers/Barobill/HometaxController.php
@@ -5,11 +5,16 @@
use App\Http\Controllers\Controller;
use App\Models\Barobill\BarobillConfig;
use App\Models\Barobill\BarobillMember;
+use App\Models\Barobill\CardTransaction as BarobillCardTransaction;
+use App\Models\Barobill\HometaxInvoice;
+use App\Models\Finance\JournalEntry;
+use App\Models\Finance\JournalEntryLine;
use App\Models\Tenants\Tenant;
use App\Services\Barobill\HometaxSyncService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
@@ -1401,4 +1406,319 @@ public function toggleChecked(Request $request, HometaxSyncService $syncService)
]);
}
}
+
+ /**
+ * 수동 세금계산서 저장
+ */
+ public function manualStore(Request $request): JsonResponse
+ {
+ try {
+ $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
+
+ $validated = $request->validate([
+ 'invoice_type' => 'required|in:sales,purchase',
+ 'write_date' => 'required|date',
+ 'invoicer_corp_name' => 'required|string|max:200',
+ 'invoicer_corp_num' => 'nullable|string|max:20',
+ 'invoicee_corp_name' => 'required|string|max:200',
+ 'invoicee_corp_num' => 'nullable|string|max:20',
+ 'supply_amount' => 'required|numeric|min:0',
+ 'tax_amount' => 'required|numeric|min:0',
+ 'item_name' => 'nullable|string|max:200',
+ 'remark' => 'nullable|string|max:500',
+ 'tax_type' => 'nullable|integer|in:1,2,3',
+ 'purpose_type' => 'nullable|integer|in:1,2',
+ ]);
+
+ // MAN-YYYYMMDD-NNN 형식 자동채번
+ $dateStr = date('Ymd', strtotime($validated['write_date']));
+ $lastNum = HometaxInvoice::where('tenant_id', $tenantId)
+ ->where('nts_confirm_num', 'like', "MAN-{$dateStr}-%")
+ ->orderByRaw('CAST(SUBSTRING_INDEX(nts_confirm_num, "-", -1) AS UNSIGNED) DESC')
+ ->value('nts_confirm_num');
+
+ $seq = 1;
+ if ($lastNum) {
+ $parts = explode('-', $lastNum);
+ $seq = (int)end($parts) + 1;
+ }
+ $ntsConfirmNum = sprintf('MAN-%s-%03d', $dateStr, $seq);
+
+ $totalAmount = (float)$validated['supply_amount'] + (float)$validated['tax_amount'];
+
+ $invoice = HometaxInvoice::create([
+ 'tenant_id' => $tenantId,
+ 'nts_confirm_num' => $ntsConfirmNum,
+ 'invoice_type' => $validated['invoice_type'],
+ 'write_date' => $validated['write_date'],
+ 'issue_date' => $validated['write_date'],
+ 'invoicer_corp_name' => $validated['invoicer_corp_name'],
+ 'invoicer_corp_num' => $validated['invoicer_corp_num'] ?? '',
+ 'invoicee_corp_name' => $validated['invoicee_corp_name'],
+ 'invoicee_corp_num' => $validated['invoicee_corp_num'] ?? '',
+ 'supply_amount' => $validated['supply_amount'],
+ 'tax_amount' => $validated['tax_amount'],
+ 'total_amount' => $totalAmount,
+ 'item_name' => $validated['item_name'] ?? '',
+ 'remark' => $validated['remark'] ?? '',
+ 'tax_type' => $validated['tax_type'] ?? 1,
+ 'purpose_type' => $validated['purpose_type'] ?? 1,
+ 'synced_at' => now(),
+ ]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => '수동 세금계산서가 등록되었습니다.',
+ 'data' => $invoice,
+ ]);
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ return response()->json([
+ 'success' => false,
+ 'error' => '입력값 오류: ' . implode(', ', $e->validator->errors()->all()),
+ ], 422);
+ } catch (\Throwable $e) {
+ Log::error('수동 세금계산서 저장 오류: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'error' => '저장 오류: ' . $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * 수동 세금계산서 수정 (MAN- 건만 가능)
+ */
+ public function manualUpdate(Request $request, int $id): JsonResponse
+ {
+ try {
+ $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
+
+ $invoice = HometaxInvoice::where('id', $id)
+ ->where('tenant_id', $tenantId)
+ ->firstOrFail();
+
+ if (!str_starts_with($invoice->nts_confirm_num, 'MAN-')) {
+ return response()->json([
+ 'success' => false,
+ 'error' => '수동 입력 건만 수정할 수 있습니다.',
+ ], 403);
+ }
+
+ $validated = $request->validate([
+ 'invoice_type' => 'sometimes|in:sales,purchase',
+ 'write_date' => 'sometimes|date',
+ 'invoicer_corp_name' => 'sometimes|string|max:200',
+ 'invoicer_corp_num' => 'nullable|string|max:20',
+ 'invoicee_corp_name' => 'sometimes|string|max:200',
+ 'invoicee_corp_num' => 'nullable|string|max:20',
+ 'supply_amount' => 'sometimes|numeric|min:0',
+ 'tax_amount' => 'sometimes|numeric|min:0',
+ 'item_name' => 'nullable|string|max:200',
+ 'remark' => 'nullable|string|max:500',
+ 'tax_type' => 'nullable|integer|in:1,2,3',
+ 'purpose_type' => 'nullable|integer|in:1,2',
+ ]);
+
+ if (isset($validated['supply_amount']) || isset($validated['tax_amount'])) {
+ $supply = $validated['supply_amount'] ?? $invoice->supply_amount;
+ $tax = $validated['tax_amount'] ?? $invoice->tax_amount;
+ $validated['total_amount'] = (float)$supply + (float)$tax;
+ }
+
+ $invoice->update($validated);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => '수정되었습니다.',
+ 'data' => $invoice->fresh(),
+ ]);
+ } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
+ return response()->json([
+ 'success' => false,
+ 'error' => '해당 세금계산서를 찾을 수 없습니다.',
+ ], 404);
+ } catch (\Throwable $e) {
+ Log::error('수동 세금계산서 수정 오류: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'error' => '수정 오류: ' . $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * 수동 세금계산서 삭제 (MAN- 건만 가능)
+ */
+ public function manualDestroy(int $id): JsonResponse
+ {
+ try {
+ $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
+
+ $invoice = HometaxInvoice::where('id', $id)
+ ->where('tenant_id', $tenantId)
+ ->firstOrFail();
+
+ if (!str_starts_with($invoice->nts_confirm_num, 'MAN-')) {
+ return response()->json([
+ 'success' => false,
+ 'error' => '수동 입력 건만 삭제할 수 있습니다.',
+ ], 403);
+ }
+
+ $invoice->delete();
+
+ return response()->json([
+ 'success' => true,
+ 'message' => '삭제되었습니다.',
+ ]);
+ } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
+ return response()->json([
+ 'success' => false,
+ 'error' => '해당 세금계산서를 찾을 수 없습니다.',
+ ], 404);
+ } catch (\Throwable $e) {
+ Log::error('수동 세금계산서 삭제 오류: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'error' => '삭제 오류: ' . $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * 세금계산서에서 분개(일반전표) 생성
+ */
+ public function createJournalEntry(Request $request): JsonResponse
+ {
+ try {
+ $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
+
+ $validated = $request->validate([
+ 'invoice_id' => 'required|integer',
+ 'lines' => 'required|array|min:1',
+ 'lines.*.dc_type' => 'required|in:debit,credit',
+ 'lines.*.account_code' => 'required|string',
+ 'lines.*.account_name' => 'required|string',
+ 'lines.*.debit_amount' => 'required|numeric|min:0',
+ 'lines.*.credit_amount' => 'required|numeric|min:0',
+ 'lines.*.description' => 'nullable|string',
+ ]);
+
+ $invoice = HometaxInvoice::where('id', $validated['invoice_id'])
+ ->where('tenant_id', $tenantId)
+ ->firstOrFail();
+
+ $result = DB::transaction(function () use ($tenantId, $invoice, $validated) {
+ $entryNo = JournalEntry::generateEntryNo($tenantId, $invoice->write_date);
+
+ $totalDebit = collect($validated['lines'])->sum('debit_amount');
+ $totalCredit = collect($validated['lines'])->sum('credit_amount');
+
+ $tradingPartner = $invoice->invoice_type === 'sales'
+ ? $invoice->invoicee_corp_name
+ : $invoice->invoicer_corp_name;
+
+ $entry = JournalEntry::create([
+ 'tenant_id' => $tenantId,
+ 'entry_no' => $entryNo,
+ 'entry_date' => $invoice->write_date,
+ 'entry_type' => 'general',
+ 'description' => "[홈택스-{$invoice->nts_confirm_num}] {$tradingPartner}",
+ 'total_debit' => $totalDebit,
+ 'total_credit' => $totalCredit,
+ 'status' => 'confirmed',
+ 'created_by_name' => auth()->user()?->name ?? '시스템',
+ ]);
+
+ foreach ($validated['lines'] as $i => $line) {
+ JournalEntryLine::create([
+ 'tenant_id' => $tenantId,
+ 'journal_entry_id' => $entry->id,
+ 'line_no' => $i + 1,
+ 'dc_type' => $line['dc_type'],
+ 'account_code' => $line['account_code'],
+ 'account_name' => $line['account_name'],
+ 'trading_partner_name' => $tradingPartner,
+ 'debit_amount' => $line['debit_amount'],
+ 'credit_amount' => $line['credit_amount'],
+ 'description' => $line['description'] ?? '',
+ ]);
+ }
+
+ return $entry;
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'message' => "전표 {$result->entry_no}가 생성되었습니다.",
+ 'data' => $result->load('lines'),
+ ]);
+ } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
+ return response()->json([
+ 'success' => false,
+ 'error' => '해당 세금계산서를 찾을 수 없습니다.',
+ ], 404);
+ } catch (\Throwable $e) {
+ Log::error('분개 생성 오류: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'error' => '분개 생성 오류: ' . $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * 카드내역 조회 (수동입력 참조용)
+ */
+ public function cardTransactions(Request $request): JsonResponse
+ {
+ try {
+ $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
+
+ $startDate = $request->input('startDate', date('Y-m-d', strtotime('-1 month')));
+ $endDate = $request->input('endDate', date('Y-m-d'));
+ $search = $request->input('search', '');
+
+ $query = BarobillCardTransaction::where('tenant_id', $tenantId)
+ ->whereBetween('use_date', [$startDate, $endDate])
+ ->orderByDesc('use_date')
+ ->orderByDesc('use_time');
+
+ if (!empty($search)) {
+ $query->where(function ($q) use ($search) {
+ $q->where('merchant_name', 'like', "%{$search}%")
+ ->orWhere('merchant_biz_num', 'like', "%{$search}%")
+ ->orWhere('approval_num', 'like', "%{$search}%");
+ });
+ }
+
+ $transactions = $query->limit(100)->get()->map(function ($t) {
+ return [
+ 'id' => $t->id,
+ 'useDate' => $t->use_date,
+ 'useTime' => $t->use_time,
+ 'merchantName' => $t->merchant_name,
+ 'merchantBizNum' => $t->merchant_biz_num,
+ 'approvalNum' => $t->approval_num,
+ 'approvalAmount' => (float)$t->approval_amount,
+ 'approvalAmountFormatted' => number_format($t->approval_amount),
+ 'tax' => (float)($t->tax ?? 0),
+ 'supplyAmount' => (float)($t->modified_supply_amount ?: ($t->approval_amount - ($t->tax ?? 0))),
+ 'cardNum' => $t->card_num ? substr($t->card_num, -4) : '',
+ 'cardCompanyName' => $t->card_company_name ?? '',
+ ];
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'data' => $transactions,
+ ]);
+ } catch (\Throwable $e) {
+ Log::error('카드내역 조회 오류: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'error' => '조회 오류: ' . $e->getMessage(),
+ ]);
+ }
+ }
}
diff --git a/resources/views/barobill/hometax/index.blade.php b/resources/views/barobill/hometax/index.blade.php
index 57ebfc17..89688eef 100644
--- a/resources/views/barobill/hometax/index.blade.php
+++ b/resources/views/barobill/hometax/index.blade.php
@@ -78,6 +78,11 @@
scrapUrl: '{{ route("barobill.hometax.scrap-url") }}',
refreshScrap: '{{ route("barobill.hometax.refresh-scrap") }}',
diagnose: '{{ route("barobill.hometax.diagnose") }}',
+ manualStore: '{{ route("barobill.hometax.manual-store") }}',
+ manualUpdate: '{{ route("barobill.hometax.manual-update", ["id" => "__ID__"]) }}',
+ manualDestroy: '{{ route("barobill.hometax.manual-destroy", ["id" => "__ID__"]) }}',
+ createJournalEntry: '{{ route("barobill.hometax.create-journal-entry") }}',
+ cardTransactions: '{{ route("barobill.hometax.card-transactions") }}',
};
const CSRF_TOKEN = '{{ csrf_token() }}';
@@ -170,7 +175,10 @@ className={`px-6 py-3 text-sm font-medium rounded-lg transition-all ${
type,
onExport,
onRequestCollect,
- summary // 합계 정보
+ summary,
+ onJournalEntry,
+ onEditManual,
+ onDeleteManual
}) => {
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0);
@@ -236,13 +244,14 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded
영수 청구 |
문서 형태 |
발급 형태 |
- 상태 |
+ 상태 |
+ 액션 |
{invoices.length === 0 ? (
- |
+ |
해당 기간에 조회된 세금계산서가 없습니다.
|
@@ -257,7 +266,11 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded
{/* 구분 */}
|
- 홈택
+ {inv.ntsConfirmNum && inv.ntsConfirmNum.startsWith('MAN-') ? (
+ 수동
+ ) : (
+ 홈택
+ )}
|
{/* 작성일자 */}
@@ -312,8 +325,36 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded
정발급
|
{/* 상태 */}
-
- 전송완료
+ |
+ {inv.ntsConfirmNum && inv.ntsConfirmNum.startsWith('MAN-') ? (
+ 수동입력
+ ) : (
+ 전송완료
+ )}
+ |
+ {/* 액션 */}
+
+
+
+ {inv.ntsConfirmNum && inv.ntsConfirmNum.startsWith('MAN-') && (
+ <>
+
+
+ >
+ )}
+
|
);
@@ -334,7 +375,7 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded
{formatCurrency(summary.totalAmount)}
|
- |
+ |
)}
@@ -369,6 +410,14 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded
const [diagnoseResult, setDiagnoseResult] = useState(null);
const [diagnosing, setDiagnosing] = useState(false);
+ // 수동입력 모달
+ const [showManualModal, setShowManualModal] = useState(false);
+ const [manualEditData, setManualEditData] = useState(null);
+
+ // 분개 모달
+ const [showJournalModal, setShowJournalModal] = useState(false);
+ const [journalInvoice, setJournalInvoice] = useState(null);
+
// 초기 로드 (매출만 먼저 조회)
useEffect(() => {
loadSalesData();
@@ -648,6 +697,80 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded
}
};
+ // 수동입력 삭제
+ const handleDeleteManual = async (inv) => {
+ if (!confirm(`"${inv.invoicerCorpName || inv.invoiceeCorpName}" 세금계산서를 삭제하시겠습니까?`)) return;
+ try {
+ const url = API.manualDestroy.replace('__ID__', inv.id);
+ const res = await fetch(url, {
+ method: 'DELETE',
+ headers: { 'X-CSRF-TOKEN': CSRF_TOKEN }
+ });
+ const data = await res.json();
+ if (data.success) {
+ notify('삭제되었습니다.', 'success');
+ loadCurrentTabData();
+ } else {
+ notify(data.error || '삭제 실패', 'error');
+ }
+ } catch (err) {
+ notify('삭제 오류: ' + err.message, 'error');
+ }
+ };
+
+ // 수동입력 저장 (생성/수정)
+ const handleManualSave = async (formData) => {
+ try {
+ const isEdit = !!formData.id;
+ const url = isEdit
+ ? API.manualUpdate.replace('__ID__', formData.id)
+ : API.manualStore;
+ const method = isEdit ? 'PUT' : 'POST';
+
+ const res = await fetch(url, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-TOKEN': CSRF_TOKEN
+ },
+ body: JSON.stringify(formData)
+ });
+ const data = await res.json();
+ if (data.success) {
+ notify(data.message, 'success');
+ setShowManualModal(false);
+ loadCurrentTabData();
+ } else {
+ notify(data.error || '저장 실패', 'error');
+ }
+ } catch (err) {
+ notify('저장 오류: ' + err.message, 'error');
+ }
+ };
+
+ // 분개 생성
+ const handleJournalSave = async (invoiceId, lines) => {
+ try {
+ const res = await fetch(API.createJournalEntry, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-TOKEN': CSRF_TOKEN
+ },
+ body: JSON.stringify({ invoice_id: invoiceId, lines })
+ });
+ const data = await res.json();
+ if (data.success) {
+ notify(data.message, 'success');
+ setShowJournalModal(false);
+ } else {
+ notify(data.error || '분개 생성 실패', 'error');
+ }
+ } catch (err) {
+ notify('분개 생성 오류: ' + err.message, 'error');
+ }
+ };
+
// 이번 달 버튼
const handleThisMonth = () => {
const dates = getMonthDates(0);
@@ -984,6 +1107,18 @@ className="flex items-center gap-2 text-sm font-semibold rounded-md disabled:opa
매입
+
{/* Error Display */}
@@ -1024,6 +1159,15 @@ className="flex items-center gap-2 text-sm font-semibold rounded-md disabled:opa
onExport={handleExport}
onRequestCollect={handleRequestCollect}
summary={currentInvoiceSummary}
+ onJournalEntry={(inv) => {
+ setJournalInvoice(inv);
+ setShowJournalModal(true);
+ }}
+ onEditManual={(inv) => {
+ setManualEditData(inv);
+ setShowManualModal(true);
+ }}
+ onDeleteManual={handleDeleteManual}
/>
)}
@@ -1178,6 +1322,523 @@ className="px-4 py-2 bg-stone-100 text-stone-700 rounded-lg text-sm font-medium
)}
+
+ {/* 수동입력 모달 */}
+ {showManualModal && (
+ setShowManualModal(false)}
+ onSave={handleManualSave}
+ editData={manualEditData}
+ invoiceType={activeTab}
+ />
+ )}
+
+ {/* 분개 모달 */}
+ {showJournalModal && journalInvoice && (
+ { setShowJournalModal(false); setJournalInvoice(null); }}
+ onSave={handleJournalSave}
+ invoice={journalInvoice}
+ />
+ )}
+
+ );
+ };
+
+ // ============================================
+ // ManualInputModal - 수동입력 모달
+ // ============================================
+ const ManualInputModal = ({ isOpen, onClose, onSave, editData, invoiceType }) => {
+ const [form, setForm] = useState({
+ invoice_type: editData?.invoiceType || editData?.invoice_type || invoiceType || 'sales',
+ write_date: editData?.writeDate || editData?.write_date || formatKoreanDate(getKoreanNow()),
+ invoicer_corp_name: editData?.invoicerCorpName || editData?.invoicer_corp_name || '',
+ invoicer_corp_num: editData?.invoicerCorpNum || editData?.invoicer_corp_num || '',
+ invoicee_corp_name: editData?.invoiceeCorpName || editData?.invoicee_corp_name || '',
+ invoicee_corp_num: editData?.invoiceeCorpNum || editData?.invoicee_corp_num || '',
+ supply_amount: editData?.supplyAmount || editData?.supply_amount || '',
+ tax_amount: editData?.taxAmount || editData?.tax_amount || '',
+ item_name: editData?.itemName || editData?.item_name || '',
+ remark: editData?.remark || '',
+ tax_type: editData?.taxType || editData?.tax_type || 1,
+ purpose_type: editData?.purposeType || editData?.purpose_type || 1,
+ });
+ const [showCardPicker, setShowCardPicker] = useState(false);
+ const [saving, setSaving] = useState(false);
+
+ const handleChange = (field, value) => {
+ setForm(prev => ({ ...prev, [field]: value }));
+ };
+
+ const totalAmount = (parseFloat(form.supply_amount) || 0) + (parseFloat(form.tax_amount) || 0);
+
+ const handleSubmit = async () => {
+ if (!form.write_date || !form.supply_amount) {
+ notify('작성일자와 공급가액은 필수입니다.', 'warning');
+ return;
+ }
+ // 매출이면 invoicee(공급받는자), 매입이면 invoicer(공급자) 필수
+ if (form.invoice_type === 'sales' && !form.invoicee_corp_name) {
+ notify('공급받는자(거래처)명을 입력해주세요.', 'warning');
+ return;
+ }
+ if (form.invoice_type === 'purchase' && !form.invoicer_corp_name) {
+ notify('공급자(거래처)명을 입력해주세요.', 'warning');
+ return;
+ }
+ setSaving(true);
+ const submitData = { ...form };
+ if (editData?.id) submitData.id = editData.id;
+ await onSave(submitData);
+ setSaving(false);
+ };
+
+ const handleCardSelect = (card) => {
+ setForm(prev => ({
+ ...prev,
+ supply_amount: card.supplyAmount || (card.approvalAmount - (card.tax || 0)),
+ tax_amount: card.tax || 0,
+ invoicer_corp_name: card.merchantName || prev.invoicer_corp_name,
+ invoicer_corp_num: card.merchantBizNum || prev.invoicer_corp_num,
+ write_date: card.useDate || prev.write_date,
+ remark: `카드승인 ${card.approvalNum || ''}`.trim(),
+ }));
+ setShowCardPicker(false);
+ };
+
+ const isSales = form.invoice_type === 'sales';
+
+ return (
+
+
+
+
+ {editData ? '세금계산서 수정' : '세금계산서 수동 입력'}
+
+
+
+
+ {/* 매출/매입 구분 + 작성일자 */}
+
+
+
+
+
+
+
+ handleChange('write_date', e.target.value)}
+ className="w-full px-3 py-2 border border-stone-300 rounded-lg text-sm focus:ring-2 focus:ring-violet-500 focus:border-violet-500 outline-none"
+ />
+
+
+
+ {/* 거래처 정보 */}
+
+
+
+ {isSales ? '공급받는자 정보' : '공급자 정보'}
+
+ {!editData && form.invoice_type === 'purchase' && (
+
+ )}
+
+
+
+
+ {/* 금액 */}
+
+
+
+ handleChange('supply_amount', e.target.value)}
+ className="w-full px-3 py-2 border border-stone-300 rounded-lg text-sm text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500 outline-none"
+ placeholder="0"
+ />
+
+
+
+ handleChange('tax_amount', e.target.value)}
+ className="w-full px-3 py-2 border border-stone-300 rounded-lg text-sm text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500 outline-none"
+ placeholder="0"
+ />
+
+
+
+
+ {new Intl.NumberFormat('ko-KR').format(totalAmount)}
+
+
+
+
+ {/* 품목 + 과세유형 */}
+
+
+
+ handleChange('item_name', e.target.value)}
+ className="w-full px-3 py-2 border border-stone-300 rounded-lg text-sm focus:ring-2 focus:ring-violet-500 focus:border-violet-500 outline-none"
+ placeholder="품목명"
+ />
+
+
+
+
+
+
+
+ {/* 비고 */}
+
+
+ handleChange('remark', e.target.value)}
+ className="w-full px-3 py-2 border border-stone-300 rounded-lg text-sm focus:ring-2 focus:ring-violet-500 focus:border-violet-500 outline-none"
+ placeholder="메모"
+ />
+
+
+
+
+
+
+
+
+ {/* 카드내역 피커 (중첩 모달) */}
+ {showCardPicker && (
+
setShowCardPicker(false)}
+ />
+ )}
+
+ );
+ };
+
+ // ============================================
+ // JournalEntryModal - 분개 생성 모달
+ // ============================================
+ const JournalEntryModal = ({ isOpen, onClose, onSave, invoice }) => {
+ const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0);
+ const isSales = invoice.invoiceType === 'sales' || invoice.invoice_type === 'sales';
+ const supplyAmount = parseFloat(invoice.supplyAmount || invoice.supply_amount || 0);
+ const taxAmount = parseFloat(invoice.taxAmount || invoice.tax_amount || 0);
+ const totalAmount = parseFloat(invoice.totalAmount || invoice.total_amount || (supplyAmount + taxAmount));
+
+ // 분개 라인 초기값
+ const getDefaultLines = () => {
+ if (isSales) {
+ return [
+ { dc_type: 'debit', account_code: '108', account_name: '외상매출금', debit_amount: totalAmount, credit_amount: 0, description: '' },
+ { dc_type: 'credit', account_code: '401', account_name: '상품매출', debit_amount: 0, credit_amount: supplyAmount, description: '' },
+ { dc_type: 'credit', account_code: '255', account_name: '부가세예수금', debit_amount: 0, credit_amount: taxAmount, description: '' },
+ ];
+ } else {
+ return [
+ { dc_type: 'debit', account_code: '501', account_name: '상품매입', debit_amount: supplyAmount, credit_amount: 0, description: '' },
+ { dc_type: 'debit', account_code: '135', account_name: '부가세대급금', debit_amount: taxAmount, credit_amount: 0, description: '' },
+ { dc_type: 'credit', account_code: '251', account_name: '외상매입금', debit_amount: 0, credit_amount: totalAmount, description: '' },
+ ];
+ }
+ };
+
+ const [lines, setLines] = useState(getDefaultLines());
+ const [saving, setSaving] = useState(false);
+
+ const updateLine = (idx, field, value) => {
+ setLines(prev => prev.map((l, i) => i === idx ? { ...l, [field]: value } : l));
+ };
+
+ const totalDebit = lines.reduce((sum, l) => sum + (parseFloat(l.debit_amount) || 0), 0);
+ const totalCredit = lines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0);
+ const isBalanced = Math.abs(totalDebit - totalCredit) < 1;
+
+ const handleSubmit = async () => {
+ if (!isBalanced) {
+ notify('차변과 대변의 합계가 일치하지 않습니다.', 'warning');
+ return;
+ }
+ setSaving(true);
+ await onSave(invoice.id, lines);
+ setSaving(false);
+ };
+
+ const tradingPartner = isSales
+ ? (invoice.invoiceeCorpName || invoice.invoicee_corp_name || '')
+ : (invoice.invoicerCorpName || invoice.invoicer_corp_name || '');
+
+ return (
+
+
+
+
분개 생성 (일반전표)
+
+
+
+ {/* 세금계산서 정보 */}
+
+
세금계산서 정보
+
+
구분: {isSales ? '매출' : '매입'}
+
거래처: {tradingPartner}
+
공급가액: {formatCurrency(supplyAmount)}
+
세액: {formatCurrency(taxAmount)}
+
+
+
+ {/* 분개 라인 테이블 */}
+
+
분개 내역
+
+ {!isBalanced && (
+
차변과 대변의 합계가 일치하지 않습니다. (차이: {formatCurrency(Math.abs(totalDebit - totalCredit))})
+ )}
+
+
+
+
+
+
+
+
+ );
+ };
+
+ // ============================================
+ // CardTransactionPicker - 카드내역 선택 모달
+ // ============================================
+ const CardTransactionPicker = ({ onSelect, onClose }) => {
+ const [transactions, setTransactions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState('');
+ const [startDate, setStartDate] = useState(formatKoreanDate(new Date(getKoreanNow().setMonth(getKoreanNow().getMonth() - 1))));
+ const [endDate, setEndDate] = useState(formatKoreanDate(getKoreanNow()));
+
+ const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0);
+
+ const loadTransactions = async () => {
+ setLoading(true);
+ try {
+ const params = new URLSearchParams({ startDate, endDate, search });
+ const res = await fetch(`${API.cardTransactions}?${params}`);
+ const data = await res.json();
+ if (data.success) {
+ setTransactions(data.data || []);
+ }
+ } catch (err) {
+ console.error('카드내역 조회 오류:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => { loadTransactions(); }, []);
+
+ return (
+ { if (e.target === e.currentTarget) onClose(); }}>
+
+
+ {/* 검색 */}
+
+ {/* 목록 */}
+
+ {loading ? (
+
+ ) : transactions.length === 0 ? (
+
카드내역이 없습니다.
+ ) : (
+
+
+
+ | 날짜 |
+ 가맹점 |
+ 금액 |
+ 승인번호 |
+ 선택 |
+
+
+
+ {transactions.map((t) => (
+ onSelect(t)}>
+ | {t.useDate} |
+ {t.merchantName} |
+ {t.approvalAmountFormatted} |
+ {t.approvalNum} |
+
+
+ |
+
+ ))}
+
+
+ )}
+
+
);
};
diff --git a/routes/web.php b/routes/web.php
index 425fac18..665ce111 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -525,6 +525,14 @@
Route::post('/sync', [\App\Http\Controllers\Barobill\HometaxController::class, 'sync'])->name('sync');
Route::post('/update-memo', [\App\Http\Controllers\Barobill\HometaxController::class, 'updateMemo'])->name('update-memo');
Route::post('/toggle-checked', [\App\Http\Controllers\Barobill\HometaxController::class, 'toggleChecked'])->name('toggle-checked');
+ // 수동입력 CRUD
+ Route::post('/manual-store', [\App\Http\Controllers\Barobill\HometaxController::class, 'manualStore'])->name('manual-store');
+ Route::put('/manual/{id}', [\App\Http\Controllers\Barobill\HometaxController::class, 'manualUpdate'])->name('manual-update');
+ Route::delete('/manual/{id}', [\App\Http\Controllers\Barobill\HometaxController::class, 'manualDestroy'])->name('manual-destroy');
+ // 분개 생성
+ Route::post('/create-journal-entry', [\App\Http\Controllers\Barobill\HometaxController::class, 'createJournalEntry'])->name('create-journal-entry');
+ // 카드내역 참조
+ Route::get('/card-transactions', [\App\Http\Controllers\Barobill\HometaxController::class, 'cardTransactions'])->name('card-transactions');
});
});