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(); // 토큰 사용량 저장 AiTokenHelper::saveClaudeUsage($apiResponse, 'claude-3-haiku-20240307', '사업자등록증OCR'); $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(); } }