# 명함 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
<div id="ocr-drop-zone" class="border-2 border-dashed border-gray-300 rounded-lg p-8">
    <p>명함 이미지를 드래그하거나 클릭하여 업로드</p>
    <input type="file" id="ocr-file-input" accept="image/*" class="hidden">
</div>
<div id="ocr-preview" class="hidden">
    <img id="ocr-preview-image" class="max-h-48 rounded-lg">
</div>
```

### 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*
