feat:홈택스 세금계산서 로컬 저장 및 동기화 기능 구현

- HometaxInvoice 모델 생성 (로컬 DB 조회/저장)
- HometaxSyncService 서비스 생성 (API 데이터 동기화)
- HometaxController에 로컬 조회/동기화 메서드 추가
- 라우트 추가: local-sales, local-purchases, sync, update-memo, toggle-checked
- UI: 데이터소스 선택 (로컬 DB/바로빌 API), 동기화 버튼 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-03 17:13:18 +09:00
parent 3e12ecf50d
commit ac66e36294
5 changed files with 847 additions and 5 deletions

View File

@@ -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()
]);
}
}
}