diff --git a/app/Http/Controllers/Barobill/HometaxController.php b/app/Http/Controllers/Barobill/HometaxController.php index 474da07f..c445027d 100644 --- a/app/Http/Controllers/Barobill/HometaxController.php +++ b/app/Http/Controllers/Barobill/HometaxController.php @@ -6,6 +6,7 @@ use App\Models\Barobill\BarobillConfig; use App\Models\Barobill\BarobillMember; use App\Models\Tenants\Tenant; +use App\Services\Barobill\HometaxSyncService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -1174,4 +1175,230 @@ private function xmlToObject(\SimpleXMLElement $xml): object return $result; } + + /** + * 로컬 DB에서 매출 세금계산서 조회 + */ + public function localSales(Request $request, HometaxSyncService $syncService): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + + $startDate = $request->input('startDate'); + $endDate = $request->input('endDate'); + + // YYYYMMDD 형식을 Y-m-d로 변환 + if (strlen($startDate) === 8) { + $startDate = substr($startDate, 0, 4) . '-' . substr($startDate, 4, 2) . '-' . substr($startDate, 6, 2); + } + if (strlen($endDate) === 8) { + $endDate = substr($endDate, 0, 4) . '-' . substr($endDate, 4, 2) . '-' . substr($endDate, 6, 2); + } + + $dateType = $request->input('dateType', 1) == 1 ? 'write' : 'issue'; + $searchCorp = $request->input('searchCorp'); + + $data = $syncService->getLocalInvoices( + $tenantId, + 'sales', + $startDate, + $endDate, + $dateType, + $searchCorp + ); + + return response()->json([ + 'success' => true, + 'data' => $data, + 'lastSyncAt' => $syncService->getLastSyncTime($tenantId, 'sales'), + ]); + } catch (\Throwable $e) { + Log::error('로컬 매출 조회 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '조회 오류: ' . $e->getMessage() + ]); + } + } + + /** + * 로컬 DB에서 매입 세금계산서 조회 + */ + public function localPurchases(Request $request, HometaxSyncService $syncService): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + + $startDate = $request->input('startDate'); + $endDate = $request->input('endDate'); + + // YYYYMMDD 형식을 Y-m-d로 변환 + if (strlen($startDate) === 8) { + $startDate = substr($startDate, 0, 4) . '-' . substr($startDate, 4, 2) . '-' . substr($startDate, 6, 2); + } + if (strlen($endDate) === 8) { + $endDate = substr($endDate, 0, 4) . '-' . substr($endDate, 4, 2) . '-' . substr($endDate, 6, 2); + } + + $dateType = $request->input('dateType', 1) == 1 ? 'write' : 'issue'; + $searchCorp = $request->input('searchCorp'); + + $data = $syncService->getLocalInvoices( + $tenantId, + 'purchase', + $startDate, + $endDate, + $dateType, + $searchCorp + ); + + return response()->json([ + 'success' => true, + 'data' => $data, + 'lastSyncAt' => $syncService->getLastSyncTime($tenantId, 'purchase'), + ]); + } catch (\Throwable $e) { + Log::error('로컬 매입 조회 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '조회 오류: ' . $e->getMessage() + ]); + } + } + + /** + * 바로빌 API에서 데이터를 가져와 로컬 DB에 동기화 + */ + public function sync(Request $request, HometaxSyncService $syncService): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $type = $request->input('type', 'all'); // 'sales', 'purchase', 'all' + $startDate = $request->input('startDate', date('Ymd', strtotime('-1 month'))); + $endDate = $request->input('endDate', date('Ymd')); + $dateType = (int)$request->input('dateType', 1); + + $results = []; + + // 매출 동기화 + if ($type === 'all' || $type === 'sales') { + $salesRequest = new Request([ + 'startDate' => $startDate, + 'endDate' => $endDate, + 'dateType' => $dateType, + 'limit' => 500, + ]); + + $salesResponse = $this->sales($salesRequest); + $salesData = json_decode($salesResponse->getContent(), true); + + if ($salesData['success'] && !empty($salesData['data']['invoices'])) { + $results['sales'] = $syncService->syncInvoices( + $salesData['data']['invoices'], + $tenantId, + 'sales' + ); + } else { + $results['sales'] = [ + 'inserted' => 0, + 'updated' => 0, + 'failed' => 0, + 'total' => 0, + 'error' => $salesData['error'] ?? null, + ]; + } + } + + // 매입 동기화 + if ($type === 'all' || $type === 'purchase') { + $purchaseRequest = new Request([ + 'startDate' => $startDate, + 'endDate' => $endDate, + 'dateType' => $dateType, + 'limit' => 500, + ]); + + $purchaseResponse = $this->purchases($purchaseRequest); + $purchaseData = json_decode($purchaseResponse->getContent(), true); + + if ($purchaseData['success'] && !empty($purchaseData['data']['invoices'])) { + $results['purchase'] = $syncService->syncInvoices( + $purchaseData['data']['invoices'], + $tenantId, + 'purchase' + ); + } else { + $results['purchase'] = [ + 'inserted' => 0, + 'updated' => 0, + 'failed' => 0, + 'total' => 0, + 'error' => $purchaseData['error'] ?? null, + ]; + } + } + + // 총 결과 계산 + $totalInserted = ($results['sales']['inserted'] ?? 0) + ($results['purchase']['inserted'] ?? 0); + $totalUpdated = ($results['sales']['updated'] ?? 0) + ($results['purchase']['updated'] ?? 0); + + return response()->json([ + 'success' => true, + 'message' => "동기화 완료: {$totalInserted}건 추가, {$totalUpdated}건 갱신", + 'data' => $results, + ]); + } catch (\Throwable $e) { + Log::error('홈택스 동기화 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '동기화 오류: ' . $e->getMessage() + ]); + } + } + + /** + * 세금계산서 메모 업데이트 + */ + public function updateMemo(Request $request, HometaxSyncService $syncService): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $id = $request->input('id'); + $memo = $request->input('memo'); + + $success = $syncService->updateMemo($id, $tenantId, $memo); + + return response()->json([ + 'success' => $success, + 'message' => $success ? '메모가 저장되었습니다.' : '저장에 실패했습니다.', + ]); + } catch (\Throwable $e) { + return response()->json([ + 'success' => false, + 'error' => '오류: ' . $e->getMessage() + ]); + } + } + + /** + * 세금계산서 확인 여부 토글 + */ + public function toggleChecked(Request $request, HometaxSyncService $syncService): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $id = $request->input('id'); + + $success = $syncService->toggleChecked($id, $tenantId); + + return response()->json([ + 'success' => $success, + ]); + } catch (\Throwable $e) { + return response()->json([ + 'success' => false, + 'error' => '오류: ' . $e->getMessage() + ]); + } + } } diff --git a/app/Models/Barobill/HometaxInvoice.php b/app/Models/Barobill/HometaxInvoice.php new file mode 100644 index 00000000..73dffaa2 --- /dev/null +++ b/app/Models/Barobill/HometaxInvoice.php @@ -0,0 +1,266 @@ + 'integer', + 'write_date' => 'date', + 'issue_date' => 'date', + 'send_date' => 'date', + 'supply_amount' => 'integer', + 'tax_amount' => 'integer', + 'total_amount' => 'integer', + 'tax_type' => 'integer', + 'purpose_type' => 'integer', + 'issue_type' => 'integer', + 'is_checked' => 'boolean', + 'synced_at' => 'datetime', + ]; + + // 과세유형 상수 + public const TAX_TYPE_TAXABLE = 1; // 과세 + public const TAX_TYPE_ZERO_RATE = 2; // 영세 + public const TAX_TYPE_EXEMPT = 3; // 면세 + + // 영수/청구 상수 + public const PURPOSE_TYPE_RECEIPT = 1; // 영수 + public const PURPOSE_TYPE_CLAIM = 2; // 청구 + + // 발급유형 상수 + public const ISSUE_TYPE_NORMAL = 1; // 정발행 + public const ISSUE_TYPE_REVERSE = 2; // 역발행 + + /** + * 테넌트 관계 + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * 매출 스코프 + */ + public function scopeSales($query) + { + return $query->where('invoice_type', 'sales'); + } + + /** + * 매입 스코프 + */ + public function scopePurchase($query) + { + return $query->where('invoice_type', 'purchase'); + } + + /** + * 기간 스코프 + */ + public function scopePeriod($query, string $startDate, string $endDate, string $dateType = 'write') + { + $column = match($dateType) { + 'issue' => 'issue_date', + 'send' => 'send_date', + default => 'write_date', + }; + + return $query->whereBetween($column, [$startDate, $endDate]); + } + + /** + * 거래처 검색 스코프 + */ + public function scopeSearchCorp($query, string $keyword, string $invoiceType = 'sales') + { + if (empty($keyword)) { + return $query; + } + + // 매출이면 공급받는자, 매입이면 공급자 검색 + if ($invoiceType === 'sales') { + return $query->where(function ($q) use ($keyword) { + $q->where('invoicee_corp_name', 'like', "%{$keyword}%") + ->orWhere('invoicee_corp_num', 'like', "%{$keyword}%"); + }); + } else { + return $query->where(function ($q) use ($keyword) { + $q->where('invoicer_corp_name', 'like', "%{$keyword}%") + ->orWhere('invoicer_corp_num', 'like', "%{$keyword}%"); + }); + } + } + + /** + * 과세유형 라벨 + */ + public function getTaxTypeNameAttribute(): string + { + return match($this->tax_type) { + self::TAX_TYPE_TAXABLE => '과세', + self::TAX_TYPE_ZERO_RATE => '영세', + self::TAX_TYPE_EXEMPT => '면세', + default => '-', + }; + } + + /** + * 영수/청구 라벨 + */ + public function getPurposeTypeNameAttribute(): string + { + return match($this->purpose_type) { + self::PURPOSE_TYPE_RECEIPT => '영수', + self::PURPOSE_TYPE_CLAIM => '청구', + default => '-', + }; + } + + /** + * 발급유형 라벨 + */ + public function getIssueTypeNameAttribute(): string + { + return match($this->issue_type) { + self::ISSUE_TYPE_NORMAL => '정발급', + self::ISSUE_TYPE_REVERSE => '역발급', + default => '-', + }; + } + + /** + * 포맷된 공급가액 + */ + public function getFormattedSupplyAmountAttribute(): string + { + return number_format($this->supply_amount); + } + + /** + * 포맷된 세액 + */ + public function getFormattedTaxAmountAttribute(): string + { + return number_format($this->tax_amount); + } + + /** + * 포맷된 합계 + */ + public function getFormattedTotalAmountAttribute(): string + { + return number_format($this->total_amount); + } + + /** + * API 응답 데이터를 모델 데이터로 변환 + */ + public static function fromApiData(array $apiData, int $tenantId, string $invoiceType): array + { + // 작성일자 파싱 + $writeDate = null; + if (!empty($apiData['writeDate']) && strlen($apiData['writeDate']) >= 8) { + $writeDate = substr($apiData['writeDate'], 0, 4) . '-' . + substr($apiData['writeDate'], 4, 2) . '-' . + substr($apiData['writeDate'], 6, 2); + } + + // 발급일자 파싱 + $issueDate = null; + if (!empty($apiData['issueDT']) && strlen($apiData['issueDT']) >= 8) { + $issueDate = substr($apiData['issueDT'], 0, 4) . '-' . + substr($apiData['issueDT'], 4, 2) . '-' . + substr($apiData['issueDT'], 6, 2); + } + + return [ + 'tenant_id' => $tenantId, + 'nts_confirm_num' => $apiData['ntsConfirmNum'] ?? '', + 'invoice_type' => $invoiceType, + 'write_date' => $writeDate, + 'issue_date' => $issueDate, + 'invoicer_corp_num' => $apiData['invoicerCorpNum'] ?? '', + 'invoicer_corp_name' => $apiData['invoicerCorpName'] ?? '', + 'invoicer_ceo_name' => $apiData['invoicerCEOName'] ?? null, + 'invoicee_corp_num' => $apiData['invoiceeCorpNum'] ?? '', + 'invoicee_corp_name' => $apiData['invoiceeCorpName'] ?? '', + 'invoicee_ceo_name' => $apiData['invoiceeCEOName'] ?? null, + 'supply_amount' => (int)($apiData['supplyAmount'] ?? 0), + 'tax_amount' => (int)($apiData['taxAmount'] ?? 0), + 'total_amount' => (int)($apiData['totalAmount'] ?? 0), + 'tax_type' => (int)($apiData['taxType'] ?? 1), + 'purpose_type' => (int)($apiData['purposeType'] ?? 1), + 'issue_type' => 1, // 기본값: 정발행 + 'item_name' => $apiData['itemName'] ?? null, + 'remark' => $apiData['remark'] ?? null, + 'synced_at' => now(), + ]; + } +} diff --git a/app/Services/Barobill/HometaxSyncService.php b/app/Services/Barobill/HometaxSyncService.php new file mode 100644 index 00000000..890ade03 --- /dev/null +++ b/app/Services/Barobill/HometaxSyncService.php @@ -0,0 +1,229 @@ + int, 'updated' => int, 'failed' => int] + */ + public function syncInvoices(array $invoices, int $tenantId, string $invoiceType): array + { + $result = [ + 'inserted' => 0, + 'updated' => 0, + 'failed' => 0, + 'total' => count($invoices), + ]; + + if (empty($invoices)) { + return $result; + } + + DB::beginTransaction(); + + try { + foreach ($invoices as $apiData) { + // 국세청승인번호가 없으면 스킵 + if (empty($apiData['ntsConfirmNum'])) { + $result['failed']++; + continue; + } + + $modelData = HometaxInvoice::fromApiData($apiData, $tenantId, $invoiceType); + + // upsert (있으면 업데이트, 없으면 삽입) + $existing = HometaxInvoice::where('tenant_id', $tenantId) + ->where('nts_confirm_num', $modelData['nts_confirm_num']) + ->where('invoice_type', $invoiceType) + ->first(); + + if ($existing) { + // 기존 데이터 업데이트 (자체 관리 필드는 유지) + $existing->update([ + 'write_date' => $modelData['write_date'], + 'issue_date' => $modelData['issue_date'], + 'invoicer_corp_num' => $modelData['invoicer_corp_num'], + 'invoicer_corp_name' => $modelData['invoicer_corp_name'], + 'invoicer_ceo_name' => $modelData['invoicer_ceo_name'], + 'invoicee_corp_num' => $modelData['invoicee_corp_num'], + 'invoicee_corp_name' => $modelData['invoicee_corp_name'], + 'invoicee_ceo_name' => $modelData['invoicee_ceo_name'], + 'supply_amount' => $modelData['supply_amount'], + 'tax_amount' => $modelData['tax_amount'], + 'total_amount' => $modelData['total_amount'], + 'tax_type' => $modelData['tax_type'], + 'purpose_type' => $modelData['purpose_type'], + 'item_name' => $modelData['item_name'], + 'remark' => $modelData['remark'], + 'synced_at' => now(), + ]); + $result['updated']++; + } else { + // 새 데이터 삽입 + HometaxInvoice::create($modelData); + $result['inserted']++; + } + } + + DB::commit(); + + Log::info('[HometaxSync] 동기화 완료', [ + 'tenant_id' => $tenantId, + 'invoice_type' => $invoiceType, + 'result' => $result, + ]); + + } catch (\Throwable $e) { + DB::rollBack(); + Log::error('[HometaxSync] 동기화 실패', [ + 'tenant_id' => $tenantId, + 'invoice_type' => $invoiceType, + 'error' => $e->getMessage(), + ]); + throw $e; + } + + return $result; + } + + /** + * 로컬 DB에서 세금계산서 목록 조회 + * + * @param int $tenantId 테넌트 ID + * @param string $invoiceType 'sales' 또는 'purchase' + * @param string $startDate 시작일 (Y-m-d) + * @param string $endDate 종료일 (Y-m-d) + * @param string $dateType 날짜 타입 ('write', 'issue', 'send') + * @param string|null $searchCorp 거래처 검색어 + * @return array + */ + public function getLocalInvoices( + int $tenantId, + string $invoiceType, + string $startDate, + string $endDate, + string $dateType = 'write', + ?string $searchCorp = null + ): array { + $query = HometaxInvoice::where('tenant_id', $tenantId) + ->where('invoice_type', $invoiceType) + ->period($startDate, $endDate, $dateType); + + if (!empty($searchCorp)) { + $query->searchCorp($searchCorp, $invoiceType); + } + + $invoices = $query->orderByDesc('write_date')->get(); + + // API 응답 형식에 맞게 변환 + $formattedInvoices = $invoices->map(function ($inv) { + return [ + 'id' => $inv->id, + 'ntsConfirmNum' => $inv->nts_confirm_num, + 'writeDate' => $inv->write_date?->format('Ymd'), + 'writeDateFormatted' => $inv->write_date?->format('Y-m-d'), + 'issueDT' => $inv->issue_date?->format('Ymd'), + 'issueDateFormatted' => $inv->issue_date?->format('Y-m-d'), + 'invoicerCorpNum' => $inv->invoicer_corp_num, + 'invoicerCorpName' => $inv->invoicer_corp_name, + 'invoicerCEOName' => $inv->invoicer_ceo_name, + 'invoiceeCorpNum' => $inv->invoicee_corp_num, + 'invoiceeCorpName' => $inv->invoicee_corp_name, + 'invoiceeCEOName' => $inv->invoicee_ceo_name, + 'supplyAmount' => $inv->supply_amount, + 'supplyAmountFormatted' => $inv->formatted_supply_amount, + 'taxAmount' => $inv->tax_amount, + 'taxAmountFormatted' => $inv->formatted_tax_amount, + 'totalAmount' => $inv->total_amount, + 'totalAmountFormatted' => $inv->formatted_total_amount, + 'taxType' => $inv->tax_type, + 'taxTypeName' => $inv->tax_type_name, + 'purposeType' => $inv->purpose_type, + 'purposeTypeName' => $inv->purpose_type_name, + 'issueTypeName' => $inv->issue_type_name, + 'itemName' => $inv->item_name, + 'remark' => $inv->remark, + 'memo' => $inv->memo, + 'category' => $inv->category, + 'isChecked' => $inv->is_checked, + 'syncedAt' => $inv->synced_at?->format('Y-m-d H:i:s'), + ]; + })->toArray(); + + // 요약 계산 + $summary = [ + 'totalAmount' => $invoices->sum('supply_amount'), + 'totalTax' => $invoices->sum('tax_amount'), + 'totalSum' => $invoices->sum('total_amount'), + 'count' => $invoices->count(), + ]; + + return [ + 'invoices' => $formattedInvoices, + 'summary' => $summary, + ]; + } + + /** + * 마지막 동기화 시간 조회 + */ + public function getLastSyncTime(int $tenantId, string $invoiceType): ?string + { + $lastSync = HometaxInvoice::where('tenant_id', $tenantId) + ->where('invoice_type', $invoiceType) + ->orderByDesc('synced_at') + ->value('synced_at'); + + return $lastSync?->format('Y-m-d H:i:s'); + } + + /** + * 메모 업데이트 + */ + public function updateMemo(int $id, int $tenantId, ?string $memo): bool + { + return HometaxInvoice::where('id', $id) + ->where('tenant_id', $tenantId) + ->update(['memo' => $memo]) > 0; + } + + /** + * 확인 여부 토글 + */ + public function toggleChecked(int $id, int $tenantId): bool + { + $invoice = HometaxInvoice::where('id', $id) + ->where('tenant_id', $tenantId) + ->first(); + + if (!$invoice) { + return false; + } + + $invoice->is_checked = !$invoice->is_checked; + return $invoice->save(); + } + + /** + * 분류 태그 업데이트 + */ + public function updateCategory(int $id, int $tenantId, ?string $category): bool + { + return HometaxInvoice::where('id', $id) + ->where('tenant_id', $tenantId) + ->update(['category' => $category]) > 0; + } +} diff --git a/resources/views/barobill/hometax/index.blade.php b/resources/views/barobill/hometax/index.blade.php index 915463e2..57477a6f 100644 --- a/resources/views/barobill/hometax/index.blade.php +++ b/resources/views/barobill/hometax/index.blade.php @@ -67,6 +67,11 @@ const API = { sales: '{{ route("barobill.hometax.sales") }}', purchases: '{{ route("barobill.hometax.purchases") }}', + localSales: '{{ route("barobill.hometax.local-sales") }}', + localPurchases: '{{ route("barobill.hometax.local-purchases") }}', + sync: '{{ route("barobill.hometax.sync") }}', + updateMemo: '{{ route("barobill.hometax.update-memo") }}', + toggleChecked: '{{ route("barobill.hometax.toggle-checked") }}', requestCollect: '{{ route("barobill.hometax.request-collect") }}', collectStatus: '{{ route("barobill.hometax.collect-status") }}', export: '{{ route("barobill.hometax.export") }}', @@ -330,6 +335,9 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded const [dateTo, setDateTo] = useState(currentMonth.to); const [dateType, setDateType] = useState('write'); // 'write': 작성일자, 'issue': 발급일자 const [searchCorpName, setSearchCorpName] = useState(''); // 거래처 검색 + const [dataSource, setDataSource] = useState('local'); // 'local': 로컬 DB, 'api': 바로빌 API + const [syncing, setSyncing] = useState(false); // 동기화 중 여부 + const [lastSyncAt, setLastSyncAt] = useState({ sales: null, purchase: null }); // 마지막 동기화 시간 // 진단 관련 상태 const [showDiagnoseModal, setShowDiagnoseModal] = useState(false); @@ -377,11 +385,15 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded startDate: dateFrom.replace(/-/g, ''), endDate: dateTo.replace(/-/g, ''), dateType: dateTypeCode, - limit: 100 + searchCorp: searchCorpName, + limit: 500 }); + // 데이터소스에 따라 API 선택 + const apiUrl = dataSource === 'local' ? API.localSales : API.sales; + try { - const res = await fetch(`${API.sales}?${params}`); + const res = await fetch(`${apiUrl}?${params}`); const json = await res.json(); if (json.success) { @@ -391,6 +403,10 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded pagination: json.data?.pagination || {}, loaded: true }); + // 마지막 동기화 시간 업데이트 + if (json.lastSyncAt) { + setLastSyncAt(prev => ({ ...prev, sales: json.lastSyncAt })); + } // 마지막 수집 시간 갱신 loadCollectStatus(); } else { @@ -414,11 +430,15 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded startDate: dateFrom.replace(/-/g, ''), endDate: dateTo.replace(/-/g, ''), dateType: dateTypeCode, - limit: 100 + searchCorp: searchCorpName, + limit: 500 }); + // 데이터소스에 따라 API 선택 + const apiUrl = dataSource === 'local' ? API.localPurchases : API.purchases; + try { - const res = await fetch(`${API.purchases}?${params}`); + const res = await fetch(`${apiUrl}?${params}`); const json = await res.json(); if (json.success) { @@ -428,6 +448,10 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded pagination: json.data?.pagination || {}, loaded: true }); + // 마지막 동기화 시간 업데이트 + if (json.lastSyncAt) { + setLastSyncAt(prev => ({ ...prev, purchase: json.lastSyncAt })); + } // 마지막 수집 시간 갱신 loadCollectStatus(); } else { @@ -449,6 +473,47 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded } }; + // 바로빌 API에서 로컬 DB로 동기화 + const handleSync = async () => { + if (!confirm('바로빌에서 데이터를 가져와 로컬 DB에 저장합니다.\n계속하시겠습니까?')) return; + + setSyncing(true); + setError(null); + + const dateTypeCode = dateType === 'write' ? 1 : 2; + + try { + const res = await fetch(API.sync, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': CSRF_TOKEN + }, + body: JSON.stringify({ + type: 'all', + startDate: dateFrom.replace(/-/g, ''), + endDate: dateTo.replace(/-/g, ''), + dateType: dateTypeCode + }) + }); + + const data = await res.json(); + if (data.success) { + notify(data.message, 'success'); + // 동기화 후 데이터 다시 로드 + setSalesData(prev => ({ ...prev, loaded: false })); + setPurchaseData(prev => ({ ...prev, loaded: false })); + loadCurrentTabData(); + } else { + notify(data.error || '동기화 실패', 'error'); + } + } catch (err) { + notify('동기화 오류: ' + err.message, 'error'); + } finally { + setSyncing(false); + } + }; + const loadCollectStatus = async () => { try { const res = await fetch(API.collectStatus); @@ -716,7 +781,7 @@ className="ml-auto px-5 py-2 text-sm bg-[#0d6efd] text-white rounded hover:bg-[# {/* 거래처 행 */} -