Files
sam-docs/dev/guides/명함추출로직.md
권혁성 db63fcff85 refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:46:03 +09:00

11 KiB

명함 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:

{
  "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
}

Response (성공):

{
  "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 (실패):

{
  "ok": false,
  "error": "Gemini API 설정이 없습니다."
}

핵심 로직

1. BusinessCardOcrService.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. 데이터 정규화

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)

드래그앤드롭 영역

<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 처리 로직

// 파일 처리
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 설정 관리

라우트

// 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별 기본 설정

// 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 활용

참고 자료


문서 작성일: 2026-01-27