- 개발팀 전용 폴더 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>
11 KiB
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 에러 | 잠시 후 재시도 |
보안 고려사항
- API 키 보호:
api_key컬럼 암호화 저장 권장 - 마스킹: UI에서 API 키 앞 8자리만 표시
- CSRF 보호: 모든 POST 요청에 CSRF 토큰 포함
- 파일 검증: 이미지 파일만 허용 (accept="image/*")
향후 개선 사항
- Claude/OpenAI Vision 지원: 현재 Gemini만 지원, 타 provider 확장 가능
- 배치 처리: 여러 명함 동시 처리
- OCR 결과 캐싱: 동일 이미지 재처리 방지
- API 키 암호화: Laravel Crypt 활용
참고 자료
- Gemini API 문서
- Gemini Vision API
- API 키 파일 위치:
/home/aweso/sam/sales/apikey/gemini_api_key.txt
문서 작성일: 2026-01-27