From 534ffcfbc04c0c28b706f02214b5f68ca1d11cd4 Mon Sep 17 00:00:00 2001 From: kent Date: Tue, 16 Dec 2025 01:56:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(mng):=20=EC=82=AC=EC=97=85=EC=9E=90?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EC=A6=9D=20OCR=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 주요 변경사항 - BizCertController: 내부 API (OCR, CRUD) - BizCertOcrService: Claude Vision API 연동, Tesseract.js 지원 - BizCert 모델 및 FormRequest 추가 - config/services.php에 Claude API 설정 추가 ## 프론트엔드 - business-ocr.blade.php: layouts.app 레이아웃 적용 - JS/AI 토글 모드 (Tesseract.js / Claude Vision) - 이미지 전처리 추가 (그레이스케일, 대비 강화, 이진화) - SweetAlert2 연동 (토스트, 삭제 확인) ## API 엔드포인트 - POST /api/biz-cert/ocr - OCR 처리 - GET /api/biz-cert - 목록 조회 - POST /api/biz-cert - 저장 - DELETE /api/biz-cert/{id} - 삭제 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Controllers/Api/BizCertController.php | 89 +++ app/Http/Requests/BizCertStoreRequest.php | 37 + app/Models/BizCert.php | 47 ++ app/Services/BizCertOcrService.php | 242 ++++++ config/services.php | 4 + resources/views/lab/ai/business-ocr.blade.php | 740 ++++++++++++++++-- routes/api.php | 36 + 7 files changed, 1151 insertions(+), 44 deletions(-) create mode 100644 app/Http/Controllers/Api/BizCertController.php create mode 100644 app/Http/Requests/BizCertStoreRequest.php create mode 100644 app/Models/BizCert.php create mode 100644 app/Services/BizCertOcrService.php diff --git a/app/Http/Controllers/Api/BizCertController.php b/app/Http/Controllers/Api/BizCertController.php new file mode 100644 index 00000000..8c31443d --- /dev/null +++ b/app/Http/Controllers/Api/BizCertController.php @@ -0,0 +1,89 @@ +validate([ + 'image' => 'required|string', + 'raw_text' => 'nullable|string', + ]); + + $result = $this->service->processWithClaude( + $request->input('image'), + $request->input('raw_text') + ); + + if (! $result['ok']) { + return response()->json($result, 400); + } + + return response()->json($result); + } + + /** + * 저장된 목록 조회 + * + * GET /api/biz-cert + */ + public function index(): JsonResponse + { + $list = $this->service->list(); + + return response()->json([ + 'ok' => true, + 'data' => $list, + ]); + } + + /** + * 사업자등록증 데이터 저장 + * + * POST /api/biz-cert + */ + public function store(BizCertStoreRequest $request): JsonResponse + { + $bizCert = $this->service->store($request->validated()); + + return response()->json([ + 'ok' => true, + 'message' => '저장되었습니다.', + 'data' => $bizCert, + ]); + } + + /** + * 삭제 + * + * DELETE /api/biz-cert/{id} + */ + public function destroy(int $id): JsonResponse + { + $this->service->delete($id); + + return response()->json([ + 'ok' => true, + 'message' => '삭제되었습니다.', + ]); + } +} diff --git a/app/Http/Requests/BizCertStoreRequest.php b/app/Http/Requests/BizCertStoreRequest.php new file mode 100644 index 00000000..2695bb4c --- /dev/null +++ b/app/Http/Requests/BizCertStoreRequest.php @@ -0,0 +1,37 @@ + 'required|string|max:12', + 'company_name' => 'required|string|max:100', + 'representative' => 'nullable|string|max:50', + 'open_date' => 'nullable|date', + 'address' => 'nullable|string|max:255', + 'biz_type' => 'nullable|string|max:100', + 'biz_item' => 'nullable|string|max:255', + 'issue_date' => 'nullable|date', + 'raw_text' => 'nullable|string', + 'ocr_method' => 'nullable|string|max:20', + ]; + } + + public function messages(): array + { + return [ + 'biz_no.required' => '사업자등록번호는 필수입니다.', + 'company_name.required' => '상호명은 필수입니다.', + ]; + } +} diff --git a/app/Models/BizCert.php b/app/Models/BizCert.php new file mode 100644 index 00000000..60f8df65 --- /dev/null +++ b/app/Models/BizCert.php @@ -0,0 +1,47 @@ + 'date', + 'issue_date' => 'date', + ]; + } + + /** + * 사업자번호 포맷팅 (XXX-XX-XXXXX) + */ + public function getFormattedBizNoAttribute(): string + { + $no = preg_replace('/[^0-9]/', '', $this->biz_no); + if (strlen($no) === 10) { + return substr($no, 0, 3).'-'.substr($no, 3, 2).'-'.substr($no, 5, 5); + } + + return $this->biz_no; + } +} diff --git a/app/Services/BizCertOcrService.php b/app/Services/BizCertOcrService.php new file mode 100644 index 00000000..8140aeb7 --- /dev/null +++ b/app/Services/BizCertOcrService.php @@ -0,0 +1,242 @@ +apiKey = config('services.claude.api_key') ?? ''; + } + + /** + * Claude Vision API를 사용한 OCR 처리 + */ + public function processWithClaude(string $imageBase64, ?string $rawText = null): array + { + if (empty($this->apiKey)) { + return [ + 'ok' => false, + 'error' => 'Claude API 키가 설정되지 않았습니다.', + ]; + } + + // 이미지 데이터 파싱 + $imageData = $imageBase64; + $mediaType = 'image/png'; + + if (preg_match('/^data:image\/(\w+);base64,(.+)$/', $imageBase64, $matches)) { + $mediaType = 'image/'.$matches[1]; + $imageData = $matches[2]; + } + + // 프롬프트 생성 + $promptText = $this->buildPrompt($rawText); + + // API 요청 + $requestBody = [ + 'model' => 'claude-3-haiku-20240307', + 'max_tokens' => 4096, + 'messages' => [ + [ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'image', + 'source' => [ + 'type' => 'base64', + 'media_type' => $mediaType, + 'data' => $imageData, + ], + ], + [ + 'type' => 'text', + 'text' => $promptText, + ], + ], + ], + ], + ]; + + try { + $response = Http::withHeaders([ + 'x-api-key' => $this->apiKey, + 'anthropic-version' => '2023-06-01', + ])->post($this->apiUrl, $requestBody); + + if (! $response->successful()) { + Log::error('Claude API Error', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return [ + 'ok' => false, + 'error' => 'Claude API 호출 실패 (HTTP '.$response->status().')', + ]; + } + + $apiResponse = $response->json(); + $claudeText = $apiResponse['content'][0]['text'] ?? ''; + + if (empty($claudeText)) { + return [ + 'ok' => false, + 'error' => 'Claude API 응답이 비어있습니다.', + ]; + } + + // JSON 추출 + $extractedData = $this->parseClaudeResponse($claudeText); + + if (! $extractedData) { + return [ + 'ok' => false, + 'error' => 'Claude 응답 JSON 파싱 실패', + 'raw_response' => $claudeText, + ]; + } + + return [ + 'ok' => true, + 'data' => $extractedData, + 'raw_response' => $claudeText, + ]; + } catch (\Exception $e) { + Log::error('Claude API Exception', ['message' => $e->getMessage()]); + + return [ + 'ok' => false, + 'error' => 'Claude API 호출 중 오류 발생: '.$e->getMessage(), + ]; + } + } + + /** + * OCR 프롬프트 생성 + */ + private function buildPrompt(?string $rawText = null): string + { + $prompt = "제공된 사업자등록증 이미지를 직접 분석하여 아래 필드를 정확하게 추출해주세요.\n\n"; + + if ($rawText) { + $prompt .= "참고: OCR 텍스트가 제공되었지만 부정확할 수 있으니, 이미지를 직접 읽어서 정확한 정보를 추출해주세요.\n"; + $prompt .= "OCR 텍스트(참고용): {$rawText}\n\n"; + } + + $prompt .= <<<'EOT' +추출할 필드: +1. 사업자등록번호 (10자리 숫자, 형식: 000-00-00000) +2. 상호명 (법인명 또는 단체명) +3. 대표자명 (한글 이름) +4. 개업일자 (YYYY-MM-DD 형식) +5. 본점 소재지 (주소) +6. 업태 +7. 종목 +8. 발급일자 (YYYY-MM-DD 형식) + +**중요 지침:** +- 이미지를 직접 읽어서 정확한 텍스트를 추출하세요. +- 사업자등록번호는 정확히 10자리 숫자여야 하며, 하이픈을 포함하여 000-00-00000 형식으로 반환하세요. +- 날짜는 YYYY-MM-DD 형식으로 변환하세요 (예: 2015년 06월 02일 → 2015-06-02). +- 대표자명은 2-4자의 한글 이름이어야 합니다. +- 이미지가 흐리거나 화질이 좋지 않아도 최대한 정확하게 읽어주세요. +- 특수문자나 공백을 정리해주세요. + +**응답 형식 (JSON만 반환, 설명 없이):** +{ + "biz_no": "123-45-67890", + "company_name": "주식회사 예시", + "representative": "홍길동", + "open_date": "2015-06-02", + "address": "서울특별시 강남구 ...", + "type": "제조업", + "item": "엘리베이터부장품", + "issue_date": "2024-01-15" +} + +데이터를 찾을 수 없으면 빈 문자열("")로 반환하세요. +EOT; + + return $prompt; + } + + /** + * Claude 응답에서 JSON 추출 + */ + private function parseClaudeResponse(string $claudeText): ?array + { + // JSON 부분만 추출 + if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $claudeText, $matches)) { + $jsonText = $matches[0]; + } else { + $jsonText = $claudeText; + } + + $data = json_decode($jsonText, true); + + if (! $data) { + return null; + } + + // 필드명 정규화 (type -> biz_type, item -> biz_item) + return [ + 'biz_no' => $data['biz_no'] ?? '', + 'company_name' => $data['company_name'] ?? '', + 'representative' => $data['representative'] ?? '', + 'open_date' => $data['open_date'] ?? '', + 'address' => $data['address'] ?? '', + 'biz_type' => $data['type'] ?? $data['biz_type'] ?? '', + 'biz_item' => $data['item'] ?? $data['biz_item'] ?? '', + 'issue_date' => $data['issue_date'] ?? '', + ]; + } + + /** + * 사업자등록증 데이터 저장 + */ + public function store(array $data): BizCert + { + return BizCert::create([ + 'biz_no' => preg_replace('/[^0-9]/', '', $data['biz_no'] ?? ''), + 'company_name' => $data['company_name'] ?? '', + 'representative' => $data['representative'] ?? null, + 'open_date' => $data['open_date'] ?: null, + 'address' => $data['address'] ?? null, + 'biz_type' => $data['biz_type'] ?? null, + 'biz_item' => $data['biz_item'] ?? null, + 'issue_date' => $data['issue_date'] ?: null, + 'raw_text' => $data['raw_text'] ?? null, + 'ocr_method' => $data['ocr_method'] ?? 'claude', + ]); + } + + /** + * 목록 조회 + */ + public function list(): \Illuminate\Database\Eloquent\Collection + { + return BizCert::orderBy('created_at', 'desc')->get(); + } + + /** + * 삭제 + */ + public function delete(int $id): bool + { + $bizCert = BizCert::findOrFail($id); + + return $bizCert->delete(); + } +} diff --git a/config/services.php b/config/services.php index 5c2002d5..5c605d71 100644 --- a/config/services.php +++ b/config/services.php @@ -40,4 +40,8 @@ 'project_id' => env('GEMINI_PROJECT_ID', 'codebridge-chatbot'), ], + 'claude' => [ + 'api_key' => env('CLAUDE_API_KEY'), + ], + ]; diff --git a/resources/views/lab/ai/business-ocr.blade.php b/resources/views/lab/ai/business-ocr.blade.php index fc2a824b..f4ec4295 100644 --- a/resources/views/lab/ai/business-ocr.blade.php +++ b/resources/views/lab/ai/business-ocr.blade.php @@ -1,62 +1,714 @@ -@extends('layouts.presentation') +@extends('layouts.app') @section('title', '사업자등록증 OCR') @push('styles') @endpush @section('content') -
-
-
- - - -

사업자등록증 OCR

-

- 사업자등록증 이미지를 업로드하면 AI가 자동으로 텍스트를 추출하고 - 사업자 정보를 구조화된 데이터로 변환합니다. -

-
AI/Automation
+
+ +
+

사업자등록증 OCR

+

이미지나 PDF를 업로드하면 자동으로 정보를 추출합니다

+
+ + +
+ OCR 모드: + + JavaScript OCR (Tesseract.js) +
+ + +
+
+
+

이미지 업로드

+ 대기 중 +
+ +
+ + + + +

클릭하거나 파일을 드래그하세요

+

PNG, JPG, PDF 지원

+
+ + Preview + + +
-
-
-

- - - - - 예정 기능 -

-
-
-

이미지 업로드

-
    -
  • • 사업자등록증 사진 업로드
  • -
  • • 드래그 앤 드롭 지원
  • -
  • • 다중 파일 처리
  • -
+
+

추출된 정보

+ +
+
+
+ +
-
-

OCR 처리

-
    -
  • • Google Vision API 활용
  • -
  • • 사업자번호 자동 추출
  • -
  • • 상호명, 대표자명 인식
  • -
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
-
+ + + + +
+ + +
+
+ + +
+

저장된 사업자등록증

+ + + + + + + + + + + + + +
사업자번호상호대표자개업일OCR등록일
+
@endsection + +@push('scripts') + + + +@endpush diff --git a/routes/api.php b/routes/api.php index 8daaeb6e..82e8a742 100644 --- a/routes/api.php +++ b/routes/api.php @@ -442,6 +442,26 @@ // AI 문의용 오류 보고서 생성 Route::get('/error-report', [ItemFieldController::class, 'generateErrorReport'])->name('errorReport'); + + // 시스템 필드 정의 관리 (마스터 데이터) + Route::prefix('system-definitions')->name('systemDefinitions.')->group(function () { + Route::get('/', [ItemFieldController::class, 'systemFieldDefinitions'])->name('index'); + Route::post('/', [ItemFieldController::class, 'storeSystemFieldDefinition'])->name('store'); + Route::put('/{id}', [ItemFieldController::class, 'updateSystemFieldDefinition'])->name('update'); + Route::delete('/{id}', [ItemFieldController::class, 'destroySystemFieldDefinition'])->name('destroy'); + Route::post('/reorder', [ItemFieldController::class, 'reorderSystemFieldDefinitions'])->name('reorder'); + }); + + // 소스 테이블 관리 + Route::prefix('source-tables')->name('sourceTables.')->group(function () { + Route::post('/', [ItemFieldController::class, 'storeSourceTable'])->name('store'); + Route::delete('/{sourceTable}', [ItemFieldController::class, 'destroySourceTable'])->name('destroy'); + Route::post('/{sourceTable}/sync-field-names', [ItemFieldController::class, 'syncSourceTableFieldNames'])->name('syncFieldNames'); + }); + + // DB 테이블 조회 (등록 가능한 테이블 목록) + Route::get('/database-tables', [ItemFieldController::class, 'databaseTables'])->name('databaseTables'); + Route::get('/database-tables/{table}/columns', [ItemFieldController::class, 'tableColumns'])->name('tableColumns'); }); /* @@ -524,3 +544,19 @@ Route::middleware(['web', 'auth'])->prefix('gemini')->name('api.gemini.')->group(function () { Route::get('/api-key', [GeminiController::class, 'getApiKey'])->name('api-key'); }); + +/* +|-------------------------------------------------------------------------- +| 사업자등록증 OCR API +|-------------------------------------------------------------------------- +| +| Lab > AI > 사업자등록증 OCR 기능 +| Claude Vision API를 사용한 OCR 처리 +| +*/ +Route::middleware(['web', 'auth'])->prefix('biz-cert')->name('api.biz-cert.')->group(function () { + Route::post('/ocr', [\App\Http\Controllers\Api\BizCertController::class, 'ocr'])->name('ocr'); + Route::get('/', [\App\Http\Controllers\Api\BizCertController::class, 'index'])->name('index'); + Route::post('/', [\App\Http\Controllers\Api\BizCertController::class, 'store'])->name('store'); + Route::delete('/{id}', [\App\Http\Controllers\Api\BizCertController::class, 'destroy'])->name('destroy'); +});