diff --git a/claudedocs/명함추출로직.md b/claudedocs/명함추출로직.md new file mode 100644 index 00000000..8c7c4db1 --- /dev/null +++ b/claudedocs/명함추출로직.md @@ -0,0 +1,367 @@ +# 명함 OCR 추출 로직 기술 문서 + +## 개요 + +명함 이미지를 업로드하면 Google Gemini Vision API를 통해 자동으로 정보를 추출하여 영업권 등록 폼에 자동 입력하는 시스템입니다. + +--- + +## 시스템 아키텍처 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 클라이언트 │ │ MNG 서버 │ │ Gemini API │ +│ (Blade View) │ │ (Laravel) │ │ (Google) │ +└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ + │ │ │ + │ 1. 이미지 업로드 │ │ + │ (Base64) │ │ + ├──────────────────────>│ │ + │ │ 2. Vision API 호출 │ + │ ├──────────────────────>│ + │ │ │ + │ │ 3. JSON 응답 │ + │ │<──────────────────────┤ + │ 4. 추출 데이터 반환 │ │ + │<──────────────────────┤ │ + │ │ │ + │ 5. 폼 필드 자동 입력 │ │ + │ │ │ +``` + +--- + +## 파일 구조 + +``` +/home/aweso/sam/mng/ +├── app/ +│ ├── Http/Controllers/ +│ │ ├── Api/ +│ │ │ └── BusinessCardOcrController.php # OCR API 엔드포인트 +│ │ └── System/ +│ │ └── AiConfigController.php # AI 설정 관리 +│ ├── Models/System/ +│ │ └── AiConfig.php # AI API 설정 모델 +│ └── Services/ +│ └── BusinessCardOcrService.php # Gemini Vision API 호출 서비스 +├── resources/views/ +│ ├── sales/prospects/ +│ │ └── create.blade.php # 영업권 등록 (드래그앤드롭 UI) +│ └── system/ai-config/ +│ └── index.blade.php # AI 설정 관리 페이지 +└── routes/ + └── web.php # 라우트 정의 + +/home/aweso/sam/api/ +└── database/migrations/ + └── 2026_01_27_100000_create_ai_configs_table.php # AI 설정 테이블 +``` + +--- + +## 데이터베이스 스키마 + +### ai_configs 테이블 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | BIGINT | PK | +| name | VARCHAR(50) | 설정 이름 | +| provider | VARCHAR(30) | 제공자 (gemini, claude, openai) | +| api_key | VARCHAR(255) | API 키 (암호화 저장 권장) | +| model | VARCHAR(100) | 모델명 (예: gemini-2.0-flash) | +| base_url | VARCHAR(255) | API Base URL (NULL이면 기본값 사용) | +| description | TEXT | 설명 | +| is_active | BOOLEAN | 활성화 여부 (provider당 1개만 활성) | +| options | JSON | 추가 옵션 | +| created_at | TIMESTAMP | 생성일시 | +| updated_at | TIMESTAMP | 수정일시 | +| deleted_at | TIMESTAMP | 삭제일시 (소프트삭제) | + +--- + +## API 엔드포인트 + +### POST /api/business-card-ocr + +명함 이미지에서 정보를 추출합니다. + +**Request:** +```json +{ + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg..." +} +``` + +**Response (성공):** +```json +{ + "ok": true, + "data": { + "company_name": "주식회사 샘플", + "ceo_name": "홍길동", + "business_number": "123-45-67890", + "contact_phone": "02-1234-5678", + "contact_email": "hong@sample.com", + "address": "서울시 강남구 테헤란로 123", + "position": "대표이사", + "department": "경영지원팀" + }, + "raw_response": "{...}" +} +``` + +**Response (실패):** +```json +{ + "ok": false, + "error": "Gemini API 설정이 없습니다." +} +``` + +--- + +## 핵심 로직 + +### 1. BusinessCardOcrService.php + +```php +class BusinessCardOcrService +{ + public function extractFromImage(string $base64Image): array + { + // 1. 활성화된 Gemini 설정 조회 + $config = AiConfig::getActiveGemini(); + + // 2. Gemini Vision API 호출 + return $this->callGeminiVisionApi($config, $base64Image); + } + + private function callGeminiVisionApi(AiConfig $config, string $base64Image): array + { + // API URL 구성 + $url = "{$config->base_url}/models/{$config->model}:generateContent?key={$config->api_key}"; + + // Base64 이미지 데이터 처리 + // data:image/jpeg;base64, 접두사 제거 + + // API 호출 + $response = Http::timeout(30)->post($url, [ + 'contents' => [[ + 'parts' => [ + ['inline_data' => ['mime_type' => $mimeType, 'data' => $imageData]], + ['text' => $prompt] + ] + ]], + 'generationConfig' => [ + 'temperature' => 0.1, + 'responseMimeType' => 'application/json' + ] + ]); + + // 응답 파싱 및 정규화 + return $this->normalizeData($parsed); + } +} +``` + +### 2. Gemini Vision API 프롬프트 + +``` +이 명함 이미지에서 다음 정보를 추출해주세요. + +## 추출 항목 +1. company_name: 회사명/상호 +2. ceo_name: 대표자명/담당자명 +3. business_number: 사업자등록번호 (000-00-00000 형식) +4. contact_phone: 연락처/전화번호 +5. contact_email: 이메일 +6. address: 주소 +7. position: 직책 +8. department: 부서 + +## 규칙 +1. 정보가 없으면 빈 문자열("")로 응답 +2. 사업자번호는 10자리 숫자를 000-00-00000 형식으로 변환 +3. 전화번호는 하이픈 포함 형식 유지 +4. 한국어로 된 정보를 우선 추출 + +## 출력 형식 (JSON) +{ + "company_name": "", + "ceo_name": "", + "business_number": "", + ... +} +``` + +### 3. 데이터 정규화 + +```php +private function normalizeData(array $data): array +{ + // 사업자번호 정규화 (10자리 → 000-00-00000) + if (!empty($data['business_number'])) { + $digits = preg_replace('/\D/', '', $data['business_number']); + if (strlen($digits) === 10) { + $data['business_number'] = substr($digits, 0, 3) . '-' + . substr($digits, 3, 2) . '-' + . substr($digits, 5); + } + } + + return [ + 'company_name' => trim($data['company_name'] ?? ''), + 'ceo_name' => trim($data['ceo_name'] ?? ''), + // ... 기타 필드 + ]; +} +``` + +--- + +## 프론트엔드 (create.blade.php) + +### 드래그앤드롭 영역 + +```html +
+

명함 이미지를 드래그하거나 클릭하여 업로드

+ +
+ +``` + +### JavaScript 처리 로직 + +```javascript +// 파일 처리 +async function handleFile(file) { + // 1. 이미지 미리보기 + const reader = new FileReader(); + reader.onload = async (e) => { + // 미리보기 표시 + document.getElementById('ocr-preview-image').src = e.target.result; + + // 2. OCR API 호출 + showOcrLoading(true); + const response = await fetch('/api/business-card-ocr', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, + body: JSON.stringify({ image: e.target.result }) + }); + + const result = await response.json(); + + // 3. 폼 필드 자동 입력 + if (result.ok) { + fillFormFields(result.data); + } + }; + reader.readAsDataURL(file); +} + +// 폼 필드 자동 입력 (하이라이트 효과 포함) +function fillFormFields(data) { + const fieldMap = { + 'company_name': 'name', + 'ceo_name': 'ceo_name', + 'business_number': 'business_number', + // ... + }; + + for (const [key, fieldName] of Object.entries(fieldMap)) { + if (data[key]) { + const input = document.querySelector(`[name="${fieldName}"]`); + if (input) { + input.value = data[key]; + // 하이라이트 효과 + input.classList.add('bg-yellow-100'); + setTimeout(() => input.classList.remove('bg-yellow-100'), 2000); + } + } + } +} +``` + +--- + +## AI 설정 관리 + +### 라우트 + +```php +// routes/web.php +Route::prefix('system')->name('system.')->group(function () { + Route::get('ai-config', [AiConfigController::class, 'index'])->name('ai-config.index'); + Route::post('ai-config', [AiConfigController::class, 'store'])->name('ai-config.store'); + Route::put('ai-config/{id}', [AiConfigController::class, 'update'])->name('ai-config.update'); + Route::delete('ai-config/{id}', [AiConfigController::class, 'destroy'])->name('ai-config.destroy'); + Route::post('ai-config/{id}/toggle', [AiConfigController::class, 'toggle'])->name('ai-config.toggle'); + Route::post('ai-config/test', [AiConfigController::class, 'test'])->name('ai-config.test'); +}); + +Route::post('api/business-card-ocr', [BusinessCardOcrController::class, 'process']); +``` + +### Provider별 기본 설정 + +```php +// AiConfig.php +public const DEFAULT_BASE_URLS = [ + 'gemini' => 'https://generativelanguage.googleapis.com/v1beta', + 'claude' => 'https://api.anthropic.com/v1', + 'openai' => 'https://api.openai.com/v1', +]; + +public const DEFAULT_MODELS = [ + 'gemini' => 'gemini-2.0-flash', + 'claude' => 'claude-sonnet-4-20250514', + 'openai' => 'gpt-4o', +]; +``` + +--- + +## 에러 처리 + +| 상황 | 에러 메시지 | 대응 | +|------|------------|------| +| Gemini 설정 없음 | "Gemini API 설정이 없습니다" | AI 설정 페이지에서 설정 추가 | +| API 호출 실패 | "AI API 호출 실패: {status}" | API 키/모델 확인 | +| 연결 실패 | "AI API 연결 실패" | 네트워크/Base URL 확인 | +| 응답 파싱 실패 | "AI 응답 파싱 실패" | 프롬프트 조정 필요 | +| Rate Limit | 429 에러 | 잠시 후 재시도 | + +--- + +## 보안 고려사항 + +1. **API 키 보호**: `api_key` 컬럼 암호화 저장 권장 +2. **마스킹**: UI에서 API 키 앞 8자리만 표시 +3. **CSRF 보호**: 모든 POST 요청에 CSRF 토큰 포함 +4. **파일 검증**: 이미지 파일만 허용 (accept="image/*") + +--- + +## 향후 개선 사항 + +1. **Claude/OpenAI Vision 지원**: 현재 Gemini만 지원, 타 provider 확장 가능 +2. **배치 처리**: 여러 명함 동시 처리 +3. **OCR 결과 캐싱**: 동일 이미지 재처리 방지 +4. **API 키 암호화**: Laravel Crypt 활용 + +--- + +## 참고 자료 + +- [Gemini API 문서](https://ai.google.dev/gemini-api/docs) +- [Gemini Vision API](https://ai.google.dev/gemini-api/docs/vision) +- API 키 파일 위치: `/home/aweso/sam/sales/apikey/gemini_api_key.txt` + +--- + +*문서 작성일: 2026-01-27* diff --git a/resources/views/system/ai-config/index.blade.php b/resources/views/system/ai-config/index.blade.php index 5cb90d05..6f0b2378 100644 --- a/resources/views/system/ai-config/index.blade.php +++ b/resources/views/system/ai-config/index.blade.php @@ -55,6 +55,9 @@ max-height: 90vh; overflow-y: auto; } + .modal-overlay.hidden { + display: none !important; + } @endpush @@ -104,7 +107,7 @@ -