diff --git a/app/Http/Controllers/Barobill/HometaxController.php b/app/Http/Controllers/Barobill/HometaxController.php index 5844a4eb..6c2d70ed 100644 --- a/app/Http/Controllers/Barobill/HometaxController.php +++ b/app/Http/Controllers/Barobill/HometaxController.php @@ -1360,6 +1360,84 @@ public function sync(Request $request, HometaxSyncService $syncService): JsonRes } } + /** + * 자동 증분 동기화 + * 마지막 동기화 시점 이후의 데이터만 바로빌 API에서 가져와 로컬 DB에 저장 + */ + public function autoSync(Request $request, HometaxSyncService $syncService): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $type = $request->input('type', 'sales'); // 'sales' 또는 'purchase' + $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); + + if (!$barobillMember) { + return response()->json(['success' => true, 'skipped' => true, 'reason' => 'no_member']); + } + + // 마지막 동기화 시간 확인 + $lastFetchField = $type === 'sales' ? 'last_sales_fetch_at' : 'last_purchases_fetch_at'; + $lastFetch = $barobillMember->$lastFetchField; + + // 10분 이내에 이미 동기화했으면 스킵 + if ($lastFetch && $lastFetch->diffInMinutes(now()) < 10) { + return response()->json([ + 'success' => true, + 'skipped' => true, + 'reason' => 'recent_sync', + 'lastSyncAt' => $syncService->getLastSyncTime($tenantId, $type), + 'lastFetchAt' => $lastFetch->format('Y-m-d H:i:s'), + ]); + } + + // 증분 범위 계산: 마지막 동기화일 -1일 ~ 오늘 + $startDate = $lastFetch + ? $lastFetch->copy()->subDay()->format('Ymd') + : date('Ymd', strtotime('-1 month')); + $endDate = date('Ymd'); + + // 기존 sync 로직 재사용: API 호출 → DB 저장 + $apiMethod = $type === 'sales' ? 'sales' : 'purchases'; + $apiRequest = new Request([ + 'startDate' => $startDate, + 'endDate' => $endDate, + 'dateType' => 1, // 작성일자 기준 + 'limit' => 500, + ]); + + $apiResponse = $this->$apiMethod($apiRequest); + $apiData = json_decode($apiResponse->getContent(), true); + + $syncResult = ['inserted' => 0, 'updated' => 0, 'total' => 0]; + + if ($apiData['success'] && !empty($apiData['data']['invoices'])) { + $syncResult = $syncService->syncInvoices( + $apiData['data']['invoices'], + $tenantId, + $type + ); + } + + return response()->json([ + 'success' => true, + 'skipped' => false, + 'data' => $syncResult, + 'hasNewData' => ($syncResult['inserted'] ?? 0) > 0, + 'lastSyncAt' => $syncService->getLastSyncTime($tenantId, $type), + 'lastFetchAt' => now()->format('Y-m-d H:i:s'), + ]); + } catch (\Throwable $e) { + Log::error('홈택스 자동동기화 오류: ' . $e->getMessage()); + // 자동동기화 실패는 치명적이지 않음 - 로컬 데이터는 정상 표시됨 + return response()->json([ + 'success' => true, + 'skipped' => true, + 'reason' => 'error', + 'error' => $e->getMessage(), + ]); + } + } + /** * 세금계산서 메모 업데이트 */ diff --git a/resources/views/barobill/hometax/index.blade.php b/resources/views/barobill/hometax/index.blade.php index 837ce86a..b4e7e1b6 100644 --- a/resources/views/barobill/hometax/index.blade.php +++ b/resources/views/barobill/hometax/index.blade.php @@ -70,6 +70,7 @@ localSales: '{{ route("barobill.hometax.local-sales") }}', localPurchases: '{{ route("barobill.hometax.local-purchases") }}', sync: '{{ route("barobill.hometax.sync") }}', + autoSync: '{{ route("barobill.hometax.auto-sync") }}', updateMemo: '{{ route("barobill.hometax.update-memo") }}', toggleChecked: '{{ route("barobill.hometax.toggle-checked") }}', requestCollect: '{{ route("barobill.hometax.request-collect") }}', @@ -403,8 +404,7 @@ className="px-2 py-1 bg-red-50 text-red-700 rounded text-xs hover:bg-red-100 tra 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 [autoSyncing, setAutoSyncing] = useState(false); // 자동 동기화 중 여부 const [lastSyncAt, setLastSyncAt] = useState({ sales: null, purchase: null }); // 마지막 동기화 시간 const [selectedPeriod, setSelectedPeriod] = useState(null); // 선택된 기간 버튼 ('q1', 'q2', 'q3', 'q4', 'h1', 'h2', 'year') @@ -434,6 +434,8 @@ className="px-2 py-1 bg-red-50 text-red-700 rounded text-xs hover:bg-red-100 tra loadCollectStatus(); fetchTradingPartners(); loadAccountCodes(); + // 초기 로드 후 자동 동기화 (매출) + triggerAutoSync('sales'); }, []); // 탭 변경 시 해당 탭 데이터 로드 (아직 로드되지 않은 경우) @@ -443,6 +445,8 @@ className="px-2 py-1 bg-red-50 text-red-700 rounded text-xs hover:bg-red-100 tra } else if (activeTab === 'purchase' && !purchaseData.loaded) { loadPurchaseData(); } + // 탭 전환 시 해당 탭 자동 동기화 + triggerAutoSync(activeTab === 'sales' ? 'sales' : 'purchase'); }, [activeTab]); // 매출 데이터 로드 @@ -460,11 +464,8 @@ className="px-2 py-1 bg-red-50 text-red-700 rounded text-xs hover:bg-red-100 tra limit: 500 }); - // 데이터소스에 따라 API 선택 - const apiUrl = dataSource === 'local' ? API.localSales : API.sales; - try { - const res = await fetch(`${apiUrl}?${params}`); + const res = await fetch(`${API.localSales}?${params}`); const json = await res.json(); if (json.success) { @@ -505,11 +506,8 @@ className="px-2 py-1 bg-red-50 text-red-700 rounded text-xs hover:bg-red-100 tra limit: 500 }); - // 데이터소스에 따라 API 선택 - const apiUrl = dataSource === 'local' ? API.localPurchases : API.purchases; - try { - const res = await fetch(`${apiUrl}?${params}`); + const res = await fetch(`${API.localPurchases}?${params}`); const json = await res.json(); if (json.success) { @@ -548,44 +546,39 @@ className="px-2 py-1 bg-red-50 text-red-700 rounded text-xs hover:bg-red-100 tra } }; - // 바로빌 API에서 로컬 DB로 동기화 - const handleSync = async () => { - if (!confirm('바로빌에서 데이터를 가져와 로컬 DB에 저장합니다.\n계속하시겠습니까?')) return; - - setSyncing(true); - setError(null); - - const dateTypeCode = dateType === 'write' ? 1 : 2; - + // 자동 증분 동기화 + const triggerAutoSync = async (type) => { + setAutoSyncing(true); try { - const res = await fetch(API.sync, { + const res = await fetch(API.autoSync, { 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 - }) + body: JSON.stringify({ type }) }); + const json = await res.json(); - 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'); + if (json.success && !json.skipped && json.hasNewData) { + // 신규 데이터가 있으면 화면 갱신 + if (type === 'sales') { + loadSalesData(); + } else { + loadPurchaseData(); + } + notify(`${json.data.inserted}건 추가, ${json.data.updated}건 갱신`, 'success'); + } + + // 마지막 동기화 시간 업데이트 + if (json.lastSyncAt) { + setLastSyncAt(prev => ({ ...prev, [type]: json.lastSyncAt })); } } catch (err) { - notify('동기화 오류: ' + err.message, 'error'); + console.error('자동 동기화 오류:', err); + // 자동동기화 실패는 사용자에게 알리지 않음 } finally { - setSyncing(false); + setAutoSyncing(false); } }; @@ -1013,57 +1006,22 @@ className="flex-1 max-w-md px-3 py-2 text-sm border border-[#ced4da] rounded bg- /> (사업자번호 또는 사업자명) - {/* 데이터소스 행 */} -
- -
- - - {/* 저장 버튼 - 주황색 */} - + {/* 동기화 상태 */} +
+ +
+ {autoSyncing && ( + +
+ 바로빌 동기화 중... +
+ )} + {lastSyncAt[activeTab] && ( + + 마지막 동기화: {lastSyncAt[activeTab]} + + )}
- {lastSyncAt[activeTab] && ( - - 마지막 저장: {lastSyncAt[activeTab]} - - )}
{/* 현재 조회 결과 */}
diff --git a/routes/web.php b/routes/web.php index 065a4e6f..e7f98e53 100644 --- a/routes/web.php +++ b/routes/web.php @@ -583,6 +583,7 @@ Route::get('/local-sales', [\App\Http\Controllers\Barobill\HometaxController::class, 'localSales'])->name('local-sales'); Route::get('/local-purchases', [\App\Http\Controllers\Barobill\HometaxController::class, 'localPurchases'])->name('local-purchases'); Route::post('/sync', [\App\Http\Controllers\Barobill\HometaxController::class, 'sync'])->name('sync'); + Route::post('/auto-sync', [\App\Http\Controllers\Barobill\HometaxController::class, 'autoSync'])->name('auto-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