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 거래처 검색어 */ 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(); // 분개 완료 인보이스 ID 조회 $invoiceIds = $invoices->pluck('id')->toArray(); $journaledIds = HometaxInvoiceJournal::getJournaledInvoiceIds($tenantId, $invoiceIds); // API 응답 형식에 맞게 변환 $formattedInvoices = $invoices->map(function ($inv) use ($journaledIds) { 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, 'hasJournal' => in_array($inv->id, $journaledIds), '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; } }