first commit

This commit is contained in:
김민성
2025-07-16 17:33:20 +09:00
commit 4b9161db45
51 changed files with 23478 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
# 간단한 PDF 배치 분석기 사용법
## 🎯 개요
사용자 요구사항: **"복잡하게 하지 말고 기존 모듈 그대로 사용해서 여러 개 처리하고 CSV로 만들기"**
**완전 구현 완료!** getcode.py와 똑같은 방식으로 여러 PDF를 처리하고 결과를 CSV로 저장하는 시스템이 준비되어 있습니다.
## 🚀 실행 방법
### 방법 1: 간단한 실행기 사용
```bash
python run_simple_batch.py
```
### 방법 2: 직접 실행
```bash
python simple_batch_analyzer_app.py
```
## 📱 UI 사용법
1. **📂 파일 선택**: "PDF 파일 선택" 버튼 클릭 또는 드래그&드롭
2. **✏️ 프롬프트 설정** (선택사항): 기본값은 getcode.py와 동일
- 기본: "pdf 이미지 분석하여 도면인지 어떤 정보들이 있는지 알려줘"
3. **▶️ 분석 시작**: "배치 분석 시작" 버튼 클릭
4. **📊 진행률 확인**: 실시간 진행률 및 처리 상태 표시
5. **💾 결과 확인**: 자동으로 CSV 파일 저장 및 요약 통계 표시
## 📄 CSV 출력 형식
생성되는 CSV 파일에는 다음 컬럼들이 포함됩니다:
| 컬럼명 | 설명 |
|--------|------|
| file_name | 파일 이름 |
| file_size_mb | 파일 크기 (MB) |
| processing_time_seconds | 처리 시간 (초) |
| success | 성공 여부 (True/False) |
| analysis_result | getcode.py 스타일 분석 결과 |
| analysis_timestamp | 분석 완료 시간 |
| prompt_used | 사용된 프롬프트 |
| model_used | 사용된 AI 모델 |
| error_message | 오류 메시지 (실패시) |
| processed_at | 처리 완료 시간 |
## 🔧 환경 설정
### 1. API 키 설정
`.env` 파일에 Gemini API 키 설정:
```
GEMINI_API_KEY=your_api_key_here
```
### 2. 필요 패키지 설치
```bash
pip install -r requirements.txt
```
주요 패키지:
- `flet>=0.25.1` - UI 프레임워크
- `google-genai>=1.0` - Gemini API
- `PyMuPDF>=1.26.3` - PDF 처리
- `pandas>=1.5.0` - CSV 출력
- `python-dotenv>=1.0.0` - 환경변수
## ⚡ 핵심 특징
-**단순성**: getcode.py와 동일한 방식, 복잡한 기능 제거
- 🔄 **배치 처리**: 한 번에 여러 PDF 파일 처리
- 📊 **CSV 출력**: JSON 분석 결과를 자동으로 CSV 변환
-**성능**: 비동기 처리로 빠른 배치 분석
- 📱 **사용성**: 직관적이고 간단한 UI
## 🗂️ 파일 저장 위치
- CSV 결과: `D:/MYCLAUDE_PROJECT/fletimageanalysis/results/`
- 파일명 형식: `batch_analysis_results_YYYY-MM-DD_HH-MM-SS.csv`
## 🎯 예상 사용 시나리오
1. **여러 도면 PDF 분석**: 한 번에 10-50개 도면 파일 분석
2. **일괄 메타데이터 추출**: 도면 정보, 제목, 축척 등 추출
3. **분석 결과 관리**: CSV로 저장하여 Excel에서 관리
4. **품질 보증**: 성공률 및 처리 시간 통계로 분석 품질 확인
## 🔍 문제 해결
### Q: 분석이 실패하는 경우
A:
- Gemini API 키가 올바르게 설정되었는지 확인
- PDF 파일이 손상되지 않았는지 확인
- 인터넷 연결 상태 확인
### Q: UI가 실행되지 않는 경우
A:
- Python 3.9+ 버전 확인
- Flet 패키지 설치 확인: `pip install flet`
- 작업 디렉토리가 올바른지 확인
### Q: CSV 파일을 찾을 수 없는 경우
A:
- `results/` 폴더가 자동 생성됩니다
- 완료 메시지에서 정확한 파일 경로 확인
- 파일 권한 문제 확인
## 📞 지원
이 시스템은 사용자 요구사항에 맞춰 **단순하고 직관적**으로 설계되었습니다.
getcode.py의 장점을 그대로 유지하면서 배치 처리 기능만 추가했습니다.

View File

@@ -0,0 +1,94 @@
"""
테스트용 DXF 파일 생성 스크립트
도곽 블록과 속성을 포함한 간단한 DXF 파일 생성
"""
import ezdxf
import os
def create_test_dxf():
"""테스트용 DXF 파일 생성"""
# 새 문서 생성
doc = ezdxf.new('R2010')
# 모델스페이스 가져오기
msp = doc.modelspace()
# 도곽 블록 생성
title_block = doc.blocks.new(name='TITLE_BLOCK')
# 블록에 기본 도형 추가 (도곽 테두리)
title_block.add_lwpolyline([
(0, 0), (210, 0), (210, 297), (0, 297), (0, 0)
], dxfattribs={'layer': 'BORDER'})
# 도곽 블록에 속성 정의 추가
title_block.add_attdef('DRAWING_NAME', (150, 20),
dxfattribs={'height': 5, 'prompt': '도면명'})
title_block.add_attdef('DRAWING_NUMBER', (150, 15),
dxfattribs={'height': 3, 'prompt': '도면번호'})
title_block.add_attdef('SCALE', (150, 10),
dxfattribs={'height': 3, 'prompt': '축척'})
title_block.add_attdef('DESIGNER', (150, 5),
dxfattribs={'height': 3, 'prompt': '설계자'})
title_block.add_attdef('DATE', (200, 5),
dxfattribs={'height': 3, 'prompt': '날짜'})
# 블록에 일반 텍스트도 추가
title_block.add_text('도면 제목', dxfattribs={'height': 4, 'insert': (10, 280)})
title_block.add_text('프로젝트명', dxfattribs={'height': 3, 'insert': (10, 275)})
# 모델스페이스에 도곽 블록 참조 추가
blockref = msp.add_blockref('TITLE_BLOCK', (0, 0))
# 블록 참조에 속성 값 추가
blockref.add_auto_attribs({
'DRAWING_NAME': '평면도 및 종단면도',
'DRAWING_NUMBER': 'DWG-001',
'SCALE': '1:1000',
'DESIGNER': '김설계',
'DATE': '2025-07-09'
})
# 추가 블록 생성 (일반 블록)
detail_block = doc.blocks.new(name='DETAIL_MARK')
detail_block.add_circle((0, 0), 5)
detail_block.add_attdef('DETAIL_NO', (0, 0),
dxfattribs={'height': 3, 'prompt': '상세번호'})
# 상세 마크 블록 참조 추가
detail_ref = msp.add_blockref('DETAIL_MARK', (50, 50))
detail_ref.add_auto_attribs({'DETAIL_NO': 'A'})
detail_ref2 = msp.add_blockref('DETAIL_MARK', (100, 100))
detail_ref2.add_auto_attribs({'DETAIL_NO': 'B'})
# 독립적인 텍스트 엔티티 추가
msp.add_text('독립 텍스트 1', dxfattribs={'height': 5, 'insert': (30, 150)})
msp.add_mtext('여러줄\n텍스트', dxfattribs={'char_height': 4, 'insert': (30, 130)})
return doc
def main():
"""메인 함수"""
try:
# 테스트 DXF 파일 생성
doc = create_test_dxf()
# uploads 폴더 생성
os.makedirs('uploads', exist_ok=True)
# 파일 저장
output_path = 'uploads/test_drawing.dxf'
doc.saveas(output_path)
print(f"[SUCCESS] 테스트 DXF 파일 생성 완료: {output_path}")
print(" - TITLE_BLOCK: 도곽 블록 (5개 속성)")
print(" - DETAIL_MARK: 상세 마크 블록 (2개 인스턴스)")
print(" - 독립 텍스트 엔티티 2개")
except Exception as e:
print(f"[ERROR] DXF 파일 생성 실패: {e}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
"""
DXF 파일 지원을 위한 추가 메서드들
main.py에 추가할 메서드들을 정의합니다.
"""
def handle_file_selection_update(self, e):
"""
기존 on_file_selected 메서드를 DXF 지원으로 업데이트하는 로직
이 메서드들을 main.py에 추가하거나 기존 메서드를 대체해야 합니다.
"""
def on_file_selected_updated(self, e):
"""파일 선택 결과 핸들러 - PDF/DXF 지원"""
if e.files:
file = e.files[0]
self.current_file_path = file.path
# 파일 확장자로 타입 결정
file_extension = file.path.lower().split('.')[-1]
if file_extension == 'pdf':
self.current_file_type = 'pdf'
self._handle_pdf_file_selection(file)
elif file_extension == 'dxf':
self.current_file_type = 'dxf'
self._handle_dxf_file_selection(file)
else:
self.selected_file_text.value = f"❌ 지원하지 않는 파일 형식입니다: {file_extension}"
self.selected_file_text.color = ft.Colors.RED_600
self.upload_button.disabled = True
self.pdf_preview_button.disabled = True
self._reset_file_state()
else:
self.selected_file_text.value = "선택된 파일이 없습니다"
self.selected_file_text.color = ft.Colors.GREY_600
self.upload_button.disabled = True
self.pdf_preview_button.disabled = True
self._reset_file_state()
self.page.update()
def _handle_pdf_file_selection(self, file):
"""PDF 파일 선택 처리"""
if self.pdf_processor.validate_pdf_file(self.current_file_path):
# PDF 정보 조회
self.current_pdf_info = self.pdf_processor.get_pdf_info(self.current_file_path)
# 파일 크기 정보 추가
file_size_mb = self.current_pdf_info['file_size'] / (1024 * 1024)
file_info = f"{file.name} (PDF)\n📄 {self.current_pdf_info['page_count']}페이지, {file_size_mb:.1f}MB"
self.selected_file_text.value = file_info
self.selected_file_text.color = ft.Colors.GREEN_600
self.upload_button.disabled = False
self.pdf_preview_button.disabled = False
# 페이지 정보 업데이트
self.page_info_text.value = f"1 / {self.current_pdf_info['page_count']}"
self.current_page_index = 0
logger.info(f"PDF 파일 선택됨: {file.name}")
else:
self.selected_file_text.value = "❌ 유효하지 않은 PDF 파일입니다"
self.selected_file_text.color = ft.Colors.RED_600
self.upload_button.disabled = True
self.pdf_preview_button.disabled = True
self._reset_file_state()
def _handle_dxf_file_selection(self, file):
"""DXF 파일 선택 처리"""
try:
if self.dxf_processor.validate_dxf_file(self.current_file_path):
# DXF 파일 크기 계산
import os
file_size_mb = os.path.getsize(self.current_file_path) / (1024 * 1024)
file_info = f"{file.name} (DXF)\n🏗️ CAD 도면 파일, {file_size_mb:.1f}MB"
self.selected_file_text.value = file_info
self.selected_file_text.color = ft.Colors.GREEN_600
self.upload_button.disabled = False
self.pdf_preview_button.disabled = True # DXF는 미리보기 비활성화
# DXF는 페이지 개념이 없으므로 기본값 설정
self.page_info_text.value = "DXF 파일"
self.current_page_index = 0
self.current_pdf_info = None # DXF는 PDF 정보 없음
logger.info(f"DXF 파일 선택됨: {file.name}")
else:
self.selected_file_text.value = "❌ 유효하지 않은 DXF 파일입니다"
self.selected_file_text.color = ft.Colors.RED_600
self.upload_button.disabled = True
self.pdf_preview_button.disabled = True
self._reset_file_state()
except Exception as e:
logger.error(f"DXF 파일 검증 오류: {e}")
self.selected_file_text.value = f"❌ DXF 파일 처리 오류: {str(e)}"
self.selected_file_text.color = ft.Colors.RED_600
self.upload_button.disabled = True
self.pdf_preview_button.disabled = True
self._reset_file_state()
def _reset_file_state(self):
"""파일 상태 초기화"""
self.current_file_path = None
self.current_file_type = None
self.current_pdf_info = None
def run_analysis_updated(self):
"""분석 실행 (백그라운드 스레드) - PDF/DXF 지원"""
try:
self.analysis_start_time = time.time()
if self.current_file_type == 'pdf':
self._run_pdf_analysis()
elif self.current_file_type == 'dxf':
self._run_dxf_analysis()
else:
raise ValueError(f"지원하지 않는 파일 타입: {self.current_file_type}")
except Exception as e:
logger.error(f"분석 중 오류 발생: {e}")
self.update_progress_ui(False, f"❌ 분석 오류: {str(e)}")
self.show_error_dialog("분석 오류", f"분석 중 오류가 발생했습니다:\n{str(e)}")
def _run_pdf_analysis(self):
"""PDF 파일 분석 실행"""
self.update_progress_ui(True, "PDF 이미지 변환 중...")
# 조직 유형 결정
organization_type = "transportation"
if self.organization_selector and self.organization_selector.value:
if self.organization_selector.value == "한국도로공사":
organization_type = "expressway"
else:
organization_type = "transportation"
logger.info(f"선택된 조직 유형: {organization_type}")
# 분석할 페이지 결정
if self.page_selector.value == "첫 번째 페이지":
pages_to_analyze = [0]
else:
pages_to_analyze = list(range(self.current_pdf_info['page_count']))
# 분석 프롬프트 결정
if self.analysis_mode.value == "custom":
prompt = self.custom_prompt.value or Config.DEFAULT_PROMPT
elif self.analysis_mode.value == "detailed":
prompt = "이 PDF 이미지를 자세히 분석하여 다음 정보를 제공해주세요: 1) 문서 유형, 2) 주요 내용, 3) 도면/도표 정보, 4) 텍스트 내용, 5) 기타 특징"
else:
prompt = Config.DEFAULT_PROMPT
# 페이지별 분석 수행
total_pages = len(pages_to_analyze)
self.analysis_results = {}
for i, page_num in enumerate(pages_to_analyze):
progress = (i + 1) / total_pages
self.update_progress_ui(
True,
f"페이지 {page_num + 1} 분석 중... ({i + 1}/{total_pages})",
progress
)
# PDF 페이지를 base64로 변환
base64_data = self.pdf_processor.pdf_page_to_base64(
self.current_file_path,
page_num
)
if base64_data:
# Gemini API로 분석
result = self.gemini_analyzer.analyze_image_from_base64(
base64_data=base64_data,
prompt=prompt,
organization_type=organization_type
)
if result:
self.analysis_results[page_num] = result
else:
self.analysis_results[page_num] = f"페이지 {page_num + 1} 분석 실패"
else:
self.analysis_results[page_num] = f"페이지 {page_num + 1} 이미지 변환 실패"
# 결과 표시
self.display_analysis_results()
# 완료 상태로 업데이트
if self.analysis_start_time:
duration = time.time() - self.analysis_start_time
duration_str = DateTimeUtils.format_duration(duration)
self.update_progress_ui(False, f"✅ PDF 분석 완료! (소요시간: {duration_str})", 1.0)
else:
self.update_progress_ui(False, "✅ PDF 분석 완료!", 1.0)
def _run_dxf_analysis(self):
"""DXF 파일 분석 실행"""
self.update_progress_ui(True, "DXF 파일 분석 중...")
try:
# DXF 파일 처리
result = self.dxf_processor.process_dxf_file(self.current_file_path)
if result['success']:
# 분석 결과 포맷팅
self.analysis_results = {'dxf': result}
# 결과 표시
self.display_dxf_analysis_results(result)
# 완료 상태로 업데이트
if self.analysis_start_time:
duration = time.time() - self.analysis_start_time
duration_str = DateTimeUtils.format_duration(duration)
self.update_progress_ui(False, f"✅ DXF 분석 완료! (소요시간: {duration_str})", 1.0)
else:
self.update_progress_ui(False, "✅ DXF 분석 완료!", 1.0)
else:
error_msg = result.get('error', '알 수 없는 오류')
self.update_progress_ui(False, f"❌ DXF 분석 실패: {error_msg}")
self.show_error_dialog("DXF 분석 오류", f"DXF 파일 분석에 실패했습니다:\n{error_msg}")
except Exception as e:
logger.error(f"DXF 분석 중 오류: {e}")
self.update_progress_ui(False, f"❌ DXF 분석 오류: {str(e)}")
self.show_error_dialog("DXF 분석 오류", f"DXF 분석 중 오류가 발생했습니다:\n{str(e)}")
def display_dxf_analysis_results(self, dxf_result):
"""DXF 분석 결과 표시"""
def update_results():
if dxf_result and dxf_result['success']:
# 결과 텍스트 구성
result_text = "🎯 DXF 분석 요약\n"
result_text += f"📊 파일: {os.path.basename(dxf_result['file_path'])}\n"
result_text += f"⏰ 완료 시간: {DateTimeUtils.get_timestamp()}\n"
result_text += "=" * 60 + "\n\n"
# 요약 정보
summary = dxf_result.get('summary', {})
result_text += "📋 분석 요약\n"
result_text += "-" * 40 + "\n"
result_text += f"전체 블록 수: {summary.get('total_blocks', 0)}\n"
result_text += f"도곽 블록 발견: {'' if summary.get('title_block_found', False) else '아니오'}\n"
result_text += f"속성 수: {summary.get('attributes_count', 0)}\n"
if summary.get('title_block_name'):
result_text += f"도곽 블록명: {summary['title_block_name']}\n"
result_text += "\n"
# 도곽 정보
title_block = dxf_result.get('title_block')
if title_block:
result_text += "🏗️ 도곽 정보\n"
result_text += "-" * 40 + "\n"
fields = {
'drawing_name': '도면명',
'drawing_number': '도면번호',
'construction_field': '건설분야',
'construction_stage': '건설단계',
'scale': '축척',
'project_name': '프로젝트명',
'designer': '설계자',
'date': '날짜',
'revision': '리비전',
'location': '위치'
}
for field, label in fields.items():
value = title_block.get(field)
if value:
result_text += f"{label}: {value}\n"
# 바운딩 박스 정보
bbox = title_block.get('bounding_box')
if bbox:
result_text += f"\n📐 도곽 위치 정보\n"
result_text += f"좌하단: ({bbox['min_x']:.2f}, {bbox['min_y']:.2f})\n"
result_text += f"우상단: ({bbox['max_x']:.2f}, {bbox['max_y']:.2f})\n"
result_text += f"크기: {bbox['max_x'] - bbox['min_x']:.2f} × {bbox['max_y'] - bbox['min_y']:.2f}\n"
# 블록 참조 정보
block_refs = dxf_result.get('block_references', [])
if block_refs:
result_text += f"\n📦 블록 참조 목록 ({len(block_refs)}개)\n"
result_text += "-" * 40 + "\n"
for i, block_ref in enumerate(block_refs[:10]): # 최대 10개까지만 표시
result_text += f"{i+1}. {block_ref.get('name', 'Unknown')}"
if block_ref.get('attributes'):
result_text += f" (속성 {len(block_ref['attributes'])}개)"
result_text += "\n"
if len(block_refs) > 10:
result_text += f"... 외 {len(block_refs) - 10}개 블록\n"
self.results_text.value = result_text.strip()
# 저장 버튼 활성화
self.save_text_button.disabled = False
self.save_json_button.disabled = False
else:
self.results_text.value = "❌ DXF 분석 결과가 없습니다."
self.save_text_button.disabled = True
self.save_json_button.disabled = True
self.page.update()
# 메인 스레드에서 UI 업데이트
self.page.run_thread(update_results)

View File

@@ -0,0 +1,408 @@
"""
Gemini API 연동 모듈
Google Gemini API를 사용하여 이미지 분석을 수행합니다.
"""
import base64
import logging
from google import genai
from google.genai import types
from typing import Optional, Dict, Any
from config import Config
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class GeminiAnalyzer:
"""Gemini API 이미지 분석 클래스"""
def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
"""
GeminiAnalyzer 초기화
Args:
api_key: Gemini API 키 (None인 경우 환경변수에서 가져옴)
model: 사용할 모델명 (기본값: 설정에서 가져옴)
"""
self.api_key = api_key or Config.GEMINI_API_KEY
self.model = model or Config.GEMINI_MODEL
self.default_prompt = Config.DEFAULT_PROMPT
if not self.api_key:
raise ValueError("Gemini API 키가 설정되지 않았습니다.")
# Gemini 클라이언트 초기화
try:
self.client = genai.Client(api_key=self.api_key)
logger.info(f"Gemini 클라이언트 초기화 완료 (모델: {self.model})")
except Exception as e:
logger.error(f"Gemini 클라이언트 초기화 실패: {e}")
raise
def analyze_image_from_base64(
self,
base64_data: str,
prompt: Optional[str] = None,
mime_type: str = "image/png",
organization_type: str = "transportation"
) -> Optional[str]:
"""
Base64 이미지 데이터를 분석합니다.
Args:
base64_data: Base64로 인코딩된 이미지 데이터
prompt: 분석 요청 텍스트 (None인 경우 기본값 사용)
mime_type: 이미지 MIME 타입
organization_type: 조직 유형 ("transportation" 또는 "expressway")
Returns:
분석 결과 텍스트 또는 None (실패 시)
"""
try:
analysis_prompt = prompt or self.default_prompt
# 컨텐츠 구성
contents = [
types.Content(
role="user",
parts=[
types.Part.from_bytes(
mime_type=mime_type,
data=base64.b64decode(base64_data),
),
types.Part.from_text(text=analysis_prompt),
],
)
]
schema_expressway=genai.types.Schema(
type = genai.types.Type.OBJECT,
required = ["사업명", "시설/공구", "건설분야", "건설단계"],
properties = {
"사업명": genai.types.Schema(
type = genai.types.Type.STRING,
),
"노선이정": genai.types.Schema(
type = genai.types.Type.STRING,
),
"설계사": genai.types.Schema(
type = genai.types.Type.STRING,
),
"시공사": genai.types.Schema(
type = genai.types.Type.STRING,
),
"건설분야": genai.types.Schema(
type = genai.types.Type.STRING,
),
"건설단계": genai.types.Schema(
type = genai.types.Type.STRING,
),
"계정번호": genai.types.Schema(
type = genai.types.Type.STRING,
),
"(계정)날짜": genai.types.Schema(
type = genai.types.Type.STRING,
),
"(개정)내용": genai.types.Schema(
type = genai.types.Type.STRING,
),
"작성자": genai.types.Schema(
type = genai.types.Type.STRING,
),
"검토자": genai.types.Schema(
type = genai.types.Type.STRING,
),
"확인자": genai.types.Schema(
type = genai.types.Type.STRING,
),
"설계공구": genai.types.Schema(
type = genai.types.Type.STRING,
),
"시공공구": genai.types.Schema(
type = genai.types.Type.STRING,
),
"도면번호": genai.types.Schema(
type = genai.types.Type.STRING,
),
"도면축척": genai.types.Schema(
type = genai.types.Type.STRING,
),
"도면명": genai.types.Schema(
type = genai.types.Type.STRING,
),
"편철번호": genai.types.Schema(
type = genai.types.Type.STRING,
),
"적용표준버전": genai.types.Schema(
type = genai.types.Type.STRING,
),
"Note": genai.types.Schema(
type = genai.types.Type.STRING,
),
"Title": genai.types.Schema(
type = genai.types.Type.STRING,
),
"기타정보": genai.types.Schema(
type = genai.types.Type.STRING,
),
},
)
schema_transportation=genai.types.Schema(
type = genai.types.Type.OBJECT,
required = ["사업명", "시설/공구", "건설분야", "건설단계"],
properties = {
"사업명": genai.types.Schema(
type = genai.types.Type.STRING,
),
"시설/공구": genai.types.Schema(
type = genai.types.Type.STRING,
),
"건설분야": genai.types.Schema(
type = genai.types.Type.STRING,
),
"건설단계": genai.types.Schema(
type = genai.types.Type.STRING,
),
"계정차수": genai.types.Schema(
type = genai.types.Type.STRING,
),
"계정일자": genai.types.Schema(
type = genai.types.Type.STRING,
),
"개정내용": genai.types.Schema(
type = genai.types.Type.STRING,
),
"과업책임자": genai.types.Schema(
type = genai.types.Type.STRING,
),
"분야별책임자": genai.types.Schema(
type = genai.types.Type.STRING,
),
"설계자": genai.types.Schema(
type = genai.types.Type.STRING,
),
"위치정보": genai.types.Schema(
type = genai.types.Type.STRING,
),
"축척": genai.types.Schema(
type = genai.types.Type.STRING,
),
"도면번호": genai.types.Schema(
type = genai.types.Type.STRING,
),
"도면명": genai.types.Schema(
type = genai.types.Type.STRING,
),
"편철번호": genai.types.Schema(
type = genai.types.Type.STRING,
),
"적용표준": genai.types.Schema(
type = genai.types.Type.STRING,
),
"Note": genai.types.Schema(
type = genai.types.Type.STRING,
),
"Title": genai.types.Schema(
type = genai.types.Type.STRING,
),
"기타정보": genai.types.Schema(
type = genai.types.Type.STRING,
),
},
)
# 조직 유형에 따른 스키마 선택
if organization_type == "expressway":
selected_schema = schema_expressway
else: # transportation (기본값)
selected_schema = schema_transportation
# 생성 설정
generate_content_config = types.GenerateContentConfig(
temperature=0,
top_p=0.05,
thinking_config = types.ThinkingConfig(
thinking_budget=0,
),
response_mime_type="application/json",
response_schema= selected_schema
)
logger.info("Gemini API 분석 요청 시작...")
# API 호출
response = self.client.models.generate_content(
model=self.model,
contents=contents,
config=generate_content_config,
)
if response and hasattr(response, 'text'):
result = response.text
logger.info(f"분석 완료: {len(result)} 문자")
return result
else:
logger.error("API 응답에서 텍스트를 찾을 수 없습니다.")
return None
except Exception as e:
logger.error(f"이미지 분석 중 오류 발생: {e}")
return None
def analyze_image_stream_from_base64(
self,
base64_data: str,
prompt: Optional[str] = None,
mime_type: str = "image/png",
organization_type: str = "transportation"
):
"""
Base64 이미지 데이터를 스트리밍으로 분석합니다.
Args:
base64_data: Base64로 인코딩된 이미지 데이터
prompt: 분석 요청 텍스트
mime_type: 이미지 MIME 타입
organization_type: 조직 유형 ("transportation" 또는 "expressway")
Yields:
분석 결과 텍스트 청크
"""
try:
analysis_prompt = prompt or self.default_prompt
# 컨텐츠 구성
contents = [
types.Content(
role="user",
parts=[
types.Part.from_bytes(
mime_type=mime_type,
data=base64.b64decode(base64_data),
),
types.Part.from_text(text=analysis_prompt),
],
)
]
# 조직 유형에 따른 스키마 선택
if organization_type == "expressway":
selected_schema = schema_expressway
else: # transportation (기본값)
selected_schema = schema_transportation
# 생성 설정
generate_content_config = types.GenerateContentConfig(
temperature=0,
top_p=0.05,
thinking_config = types.ThinkingConfig(
thinking_budget=0,
),
response_mime_type="application/json",
response_schema=selected_schema,
)
logger.info("Gemini API 스트리밍 분석 요청 시작...")
# 스트리밍 API 호출
for chunk in self.client.models.generate_content_stream(
model=self.model,
contents=contents,
config=generate_content_config,
):
if hasattr(chunk, 'text') and chunk.text:
yield chunk.text
except Exception as e:
logger.error(f"스트리밍 이미지 분석 중 오류 발생: {e}")
yield f"오류: {str(e)}"
def analyze_pdf_images(
self,
base64_images: list,
prompt: Optional[str] = None,
mime_type: str = "image/png",
organization_type: str = "transportation"
) -> Dict[int, str]:
"""
여러 PDF 페이지 이미지를 일괄 분석합니다.
Args:
base64_images: Base64 이미지 데이터 리스트
prompt: 분석 요청 텍스트
mime_type: 이미지 MIME 타입
organization_type: 조직 유형 ("transportation" 또는 "expressway")
Returns:
페이지 번호별 분석 결과 딕셔너리
"""
results = {}
for i, base64_data in enumerate(base64_images):
logger.info(f"페이지 {i + 1}/{len(base64_images)} 분석 중...")
result = self.analyze_image_from_base64(
base64_data=base64_data,
prompt=prompt,
mime_type=mime_type,
organization_type=organization_type
)
if result:
results[i] = result
else:
results[i] = f"페이지 {i + 1} 분석 실패"
logger.info(f"{len(results)}개 페이지 분석 완료")
return results
def validate_api_connection(self) -> bool:
"""API 연결 상태를 확인합니다."""
try:
# 간단한 텍스트 생성으로 연결 테스트
test_response = self.client.models.generate_content(
model=self.model,
contents=[types.Content(
role="user",
parts=[types.Part.from_text(text="안녕하세요. 연결 테스트입니다.")]
)],
config=types.GenerateContentConfig(
temperature=0,
max_output_tokens=10,
)
)
if test_response and hasattr(test_response, 'text'):
logger.info("Gemini API 연결 테스트 성공")
return True
else:
logger.error("Gemini API 연결 테스트 실패")
return False
except Exception as e:
logger.error(f"Gemini API 연결 테스트 중 오류: {e}")
return False
def get_model_info(self) -> Dict[str, Any]:
"""현재 사용 중인 모델 정보를 반환합니다."""
return {
"model": self.model,
"api_key_length": len(self.api_key) if self.api_key else 0,
"default_prompt": self.default_prompt
}
# 사용 예시 및 테스트
if __name__ == "__main__":
try:
# 분석기 초기화
analyzer = GeminiAnalyzer()
# 연결 테스트
if analyzer.validate_api_connection():
print("Gemini API 연결 성공!")
print(f"모델 정보: {analyzer.get_model_info()}")
else:
print("Gemini API 연결 실패!")
except Exception as e:
print(f"초기화 실패: {e}")
print("API 키가 올바르게 설정되었는지 확인하세요.")

723
back_src/main_old.py Normal file
View File

@@ -0,0 +1,723 @@
"""
PDF 도면 분석기 - 메인 애플리케이션 (업데이트됨)
Flet 기반의 PDF 업로드 및 Gemini API 이미지 분석 애플리케이션
"""
import flet as ft
import logging
import threading
from typing import Optional
import time
# 프로젝트 모듈 임포트
from config import Config
from pdf_processor import PDFProcessor
from gemini_analyzer import GeminiAnalyzer
from ui_components import UIComponents
from utils import AnalysisResultSaver, DateTimeUtils
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class PDFAnalyzerApp:
"""PDF 분석기 메인 애플리케이션 클래스"""
def __init__(self, page: ft.Page):
self.page = page
self.pdf_processor = PDFProcessor()
self.gemini_analyzer = None
self.current_pdf_path = None
self.current_pdf_info = None
self.analysis_results = {}
self.result_saver = AnalysisResultSaver("results")
self.analysis_start_time = None
# UI 컴포넌트 참조
self.file_picker = None
self.selected_file_text = None
self.upload_button = None
self.progress_bar = None
self.progress_ring = None
self.status_text = None
self.results_text = None
self.results_container = None
self.save_button = None
self.organization_selector = None # 새로 추가
self.page_selector = None
self.analysis_mode = None
self.custom_prompt = None
self.pdf_preview_container = None
self.page_nav_text = None
self.prev_button = None
self.next_button = None
# 초기화
self.setup_page()
self.init_gemini_analyzer()
def setup_page(self):
"""페이지 기본 설정"""
self.page.title = Config.APP_TITLE
self.page.theme_mode = ft.ThemeMode.LIGHT
self.page.padding = 0
self.page.bgcolor = ft.Colors.GREY_100
# 윈도우 크기 설정
self.page.window_width = 1200
self.page.window_height = 800
self.page.window_min_width = 1000
self.page.window_min_height = 700
logger.info("페이지 설정 완료")
def init_gemini_analyzer(self):
"""Gemini 분석기 초기화"""
try:
config_errors = Config.validate_config()
if config_errors:
self.show_error_dialog(
"설정 오류",
"\\n".join(config_errors) + "\\n\\n.env 파일을 확인하세요."
)
return
self.gemini_analyzer = GeminiAnalyzer()
logger.info("Gemini 분석기 초기화 완료")
except Exception as e:
logger.error(f"Gemini 분석기 초기화 실패: {e}")
self.show_error_dialog(
"초기화 오류",
f"Gemini API 초기화에 실패했습니다:\\n{str(e)}"
)
def build_ui(self):
"""UI 구성"""
# 앱바
app_bar = UIComponents.create_app_bar()
self.page.appbar = app_bar
# 파일 업로드 섹션
upload_section = self.create_file_upload_section()
# 분석 설정 섹션
settings_section = self.create_analysis_settings_section()
# 진행률 섹션
progress_section = self.create_progress_section()
# 결과 및 미리보기 섹션
content_row = ft.Row([
ft.Column([
self.create_results_section(),
], expand=2),
ft.Column([
self.create_pdf_preview_section(),
], expand=1),
])
# 메인 레이아웃
main_content = ft.Column([
upload_section,
settings_section,
progress_section,
content_row,
], scroll=ft.ScrollMode.AUTO)
# 페이지에 추가
self.page.add(main_content)
logger.info("UI 구성 완료")
def create_file_upload_section(self) -> ft.Container:
"""파일 업로드 섹션 생성"""
# 파일 선택기
self.file_picker = ft.FilePicker(on_result=self.on_file_selected)
self.page.overlay.append(self.file_picker)
# 선택된 파일 정보
self.selected_file_text = ft.Text(
"선택된 파일이 없습니다",
size=14,
color=ft.Colors.GREY_600
)
# 파일 선택 버튼
select_button = ft.ElevatedButton(
text="PDF 파일 선택",
icon=ft.Icons.UPLOAD_FILE,
on_click=self.on_select_file_click,
style=ft.ButtonStyle(
bgcolor=ft.Colors.BLUE_100,
color=ft.Colors.BLUE_800,
)
)
# 분석 시작 버튼
self.upload_button = ft.ElevatedButton(
text="분석 시작",
icon=ft.Icons.ANALYTICS,
on_click=self.on_analysis_start_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
return ft.Container(
content=ft.Column([
ft.Text(
"📄 PDF 파일 업로드",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
),
ft.Divider(),
ft.Row([
select_button,
self.upload_button,
], alignment=ft.MainAxisAlignment.START),
self.selected_file_text,
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
def create_analysis_settings_section(self) -> ft.Container:
"""분석 설정 섹션 생성"""
# UI 컴포넌트와 참조를 가져오기
container, organization_selector, page_selector, analysis_mode, custom_prompt = \
UIComponents.create_analysis_settings_section_with_refs()
# 인스턴스 변수에 참조 저장
self.organization_selector = organization_selector
self.page_selector = page_selector
self.analysis_mode = analysis_mode
self.custom_prompt = custom_prompt
# 이벤트 핸들러 설정
self.analysis_mode.on_change = self.on_analysis_mode_change
self.organization_selector.on_change = self.on_organization_change
return container
def create_progress_section(self) -> ft.Container:
"""진행률 섹션 생성"""
# 진행률 바
self.progress_bar = ft.ProgressBar(
width=400,
color=ft.Colors.BLUE_600,
bgcolor=ft.Colors.GREY_300,
visible=False,
)
# 상태 텍스트
self.status_text = ft.Text(
"대기 중...",
size=14,
color=ft.Colors.GREY_600
)
# 진행률 링
self.progress_ring = ft.ProgressRing(
width=50,
height=50,
stroke_width=4,
visible=False,
)
return ft.Container(
content=ft.Column([
ft.Text(
"📊 분석 진행 상황",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.PURPLE_800
),
ft.Divider(),
ft.Row([
self.progress_ring,
ft.Column([
self.status_text,
self.progress_bar,
], expand=1),
], alignment=ft.MainAxisAlignment.START),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
def create_results_section(self) -> ft.Container:
"""결과 섹션 생성"""
# 결과 텍스트
self.results_text = ft.Text(
"분석 결과가 여기에 표시됩니다.",
size=14,
selectable=True,
)
# 결과 컨테이너
self.results_container = ft.Container(
content=ft.Column([
self.results_text,
], scroll=ft.ScrollMode.AUTO),
padding=15,
height=350,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 저장 버튼들
save_text_button = ft.ElevatedButton(
text="텍스트 저장",
icon=ft.Icons.SAVE,
disabled=True,
on_click=self.on_save_text_click,
style=ft.ButtonStyle(
bgcolor=ft.Colors.TEAL_100,
color=ft.Colors.TEAL_800,
)
)
save_json_button = ft.ElevatedButton(
text="JSON 저장",
icon=ft.Icons.SAVE_ALT,
disabled=True,
on_click=self.on_save_json_click,
style=ft.ButtonStyle(
bgcolor=ft.Colors.INDIGO_100,
color=ft.Colors.INDIGO_800,
)
)
# 저장 버튼들을 인스턴스 변수로 저장
self.save_text_button = save_text_button
self.save_json_button = save_json_button
return ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📋 분석 결과",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.GREEN_800
),
ft.Row([
save_text_button,
save_json_button,
]),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
self.results_container,
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
def create_pdf_preview_section(self) -> ft.Container:
"""PDF 미리보기 섹션 생성"""
# 미리보기 컨테이너
self.pdf_preview_container = ft.Container(
content=ft.Column([
ft.Icon(
ft.Icons.PICTURE_AS_PDF,
size=100,
color=ft.Colors.GREY_400
),
ft.Text(
"PDF 미리보기",
size=14,
color=ft.Colors.GREY_600
)
], alignment=ft.MainAxisAlignment.CENTER),
width=300,
height=400,
bgcolor=ft.Colors.GREY_100,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
alignment=ft.alignment.center,
)
# 페이지 네비게이션
self.prev_button = ft.IconButton(
icon=ft.Icons.ARROW_BACK,
disabled=True,
)
self.page_nav_text = ft.Text("1 / 1", size=14)
self.next_button = ft.IconButton(
icon=ft.Icons.ARROW_FORWARD,
disabled=True,
)
page_nav = ft.Row([
self.prev_button,
self.page_nav_text,
self.next_button,
], alignment=ft.MainAxisAlignment.CENTER)
return ft.Container(
content=ft.Column([
ft.Text(
"👁️ PDF 미리보기",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.INDIGO_800
),
ft.Divider(),
self.pdf_preview_container,
page_nav,
], alignment=ft.MainAxisAlignment.START),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 이벤트 핸들러들
def on_select_file_click(self, e):
"""파일 선택 버튼 클릭 핸들러"""
self.file_picker.pick_files(
allowed_extensions=["pdf"],
allow_multiple=False
)
def on_file_selected(self, e: ft.FilePickerResultEvent):
"""파일 선택 결과 핸들러"""
if e.files:
file = e.files[0]
self.current_pdf_path = file.path
# 파일 검증
if self.pdf_processor.validate_pdf_file(self.current_pdf_path):
# PDF 정보 조회
self.current_pdf_info = self.pdf_processor.get_pdf_info(self.current_pdf_path)
# 파일 크기 정보 추가
file_size_mb = self.current_pdf_info['file_size'] / (1024 * 1024)
file_info = f"📄 {file.name} ({self.current_pdf_info['page_count']}페이지, {file_size_mb:.1f}MB)"
self.selected_file_text.value = file_info
self.selected_file_text.color = ft.Colors.GREEN_600
self.upload_button.disabled = False
# 페이지 네비게이션 업데이트
self.page_nav_text.value = f"1 / {self.current_pdf_info['page_count']}"
logger.info(f"PDF 파일 선택됨: {file.name}")
else:
self.selected_file_text.value = "❌ 유효하지 않은 PDF 파일입니다"
self.selected_file_text.color = ft.Colors.RED_600
self.upload_button.disabled = True
self.current_pdf_path = None
self.current_pdf_info = None
else:
self.selected_file_text.value = "선택된 파일이 없습니다"
self.selected_file_text.color = ft.Colors.GREY_600
self.upload_button.disabled = True
self.current_pdf_path = None
self.current_pdf_info = None
self.page.update()
def on_analysis_mode_change(self, e):
"""분석 모드 변경 핸들러"""
if e.control.value == "custom":
self.custom_prompt.visible = True
else:
self.custom_prompt.visible = False
self.page.update()
def on_organization_change(self, e):
"""조직 선택 변경 핸들러"""
selected_org = e.control.value
logger.info(f"조직 선택 변경: {selected_org}")
# 필요한 경우 추가 작업 수행
self.page.update()
def on_analysis_start_click(self, e):
"""분석 시작 버튼 클릭 핸들러"""
if not self.current_pdf_path or not self.gemini_analyzer:
return
# 분석을 별도 스레드에서 실행
threading.Thread(target=self.run_analysis, daemon=True).start()
def on_save_text_click(self, e):
"""텍스트 저장 버튼 클릭 핸들러"""
self._save_results("text")
def on_save_json_click(self, e):
"""JSON 저장 버튼 클릭 핸들러"""
self._save_results("json")
def _save_results(self, format_type: str):
"""결과 저장 공통 함수"""
if not self.analysis_results or not self.current_pdf_info:
self.show_error_dialog("저장 오류", "저장할 분석 결과가 없습니다.")
return
try:
# 분석 설정 정보 수집
analysis_settings = {
"페이지_선택": self.page_selector.value,
"분석_모드": self.analysis_mode.value,
"사용자_정의_프롬프트": self.custom_prompt.value if self.analysis_mode.value == "custom" else None,
"분석_시간": DateTimeUtils.get_timestamp()
}
if format_type == "text":
# 텍스트 파일로 저장
saved_path = self.result_saver.save_analysis_results(
pdf_filename=self.current_pdf_info['filename'],
analysis_results=self.analysis_results,
pdf_info=self.current_pdf_info,
analysis_settings=analysis_settings
)
if saved_path:
self.show_info_dialog(
"저장 완료",
f"분석 결과가 텍스트 파일로 저장되었습니다:\\n\\n{saved_path}"
)
else:
self.show_error_dialog("저장 실패", "텍스트 파일 저장 중 오류가 발생했습니다.")
elif format_type == "json":
# JSON 파일로 저장
saved_path = self.result_saver.save_analysis_json(
pdf_filename=self.current_pdf_info['filename'],
analysis_results=self.analysis_results,
pdf_info=self.current_pdf_info,
analysis_settings=analysis_settings
)
if saved_path:
self.show_info_dialog(
"저장 완료",
f"분석 결과가 JSON 파일로 저장되었습니다:\\n\\n{saved_path}"
)
else:
self.show_error_dialog("저장 실패", "JSON 파일 저장 중 오류가 발생했습니다.")
except Exception as e:
logger.error(f"결과 저장 중 오류: {e}")
self.show_error_dialog("저장 오류", f"결과 저장 중 오류가 발생했습니다:\\n{str(e)}")
def run_analysis(self):
"""분석 실행 (백그라운드 스레드)"""
try:
# 분석 시작 시간 기록
self.analysis_start_time = time.time()
# UI 상태 업데이트
self.update_progress_ui(True, "PDF 이미지 변환 중...")
# 조직 유형 결정
organization_type = "transportation" # 기본값
if self.organization_selector and self.organization_selector.value:
if self.organization_selector.value == "한국도로공사":
organization_type = "expressway"
else:
organization_type = "transportation"
logger.info(f"선택된 조직 유형: {organization_type}")
# 분석할 페이지 결정
if self.page_selector.value == "첫 번째 페이지":
pages_to_analyze = [0]
else: # 모든 페이지
pages_to_analyze = list(range(self.current_pdf_info['page_count']))
# 분석 프롬프트 결정
if self.analysis_mode.value == "custom":
prompt = self.custom_prompt.value or Config.DEFAULT_PROMPT
elif self.analysis_mode.value == "detailed":
prompt = "이 PDF 이미지를 자세히 분석하여 다음 정보를 제공해주세요: 1) 문서 유형, 2) 주요 내용, 3) 도면/도표 정보, 4) 텍스트 내용, 5) 기타 특징"
else: # basic
prompt = Config.DEFAULT_PROMPT
# 페이지별 분석 수행
total_pages = len(pages_to_analyze)
self.analysis_results = {}
for i, page_num in enumerate(pages_to_analyze):
# 진행률 업데이트
progress = (i + 1) / total_pages
self.update_progress_ui(
True,
f"페이지 {page_num + 1} 분석 중... ({i + 1}/{total_pages})",
progress
)
# PDF 페이지를 base64로 변환
base64_data = self.pdf_processor.pdf_page_to_base64(
self.current_pdf_path,
page_num
)
if base64_data:
# Gemini API로 분석 (조직 유형 전달)
result = self.gemini_analyzer.analyze_image_from_base64(
base64_data=base64_data,
prompt=prompt,
organization_type=organization_type
)
if result:
self.analysis_results[page_num] = result
else:
self.analysis_results[page_num] = f"페이지 {page_num + 1} 분석 실패"
else:
self.analysis_results[page_num] = f"페이지 {page_num + 1} 이미지 변환 실패"
# 결과 표시
self.display_analysis_results()
# 완료 상태로 업데이트
if self.analysis_start_time:
duration = time.time() - self.analysis_start_time
duration_str = DateTimeUtils.format_duration(duration)
self.update_progress_ui(False, f"분석 완료! (소요시간: {duration_str})", 1.0)
else:
self.update_progress_ui(False, "분석 완료!", 1.0)
except Exception as e:
logger.error(f"분석 중 오류 발생: {e}")
self.update_progress_ui(False, f"분석 오류: {str(e)}")
self.show_error_dialog("분석 오류", f"분석 중 오류가 발생했습니다:\\n{str(e)}")
def update_progress_ui(
self,
is_running: bool,
status: str,
progress: Optional[float] = None
):
"""진행률 UI 업데이트"""
def update():
self.progress_ring.visible = is_running
self.status_text.value = status
if progress is not None:
self.progress_bar.value = progress
self.progress_bar.visible = True
else:
self.progress_bar.visible = is_running
self.page.update()
# 메인 스레드에서 UI 업데이트
self.page.run_thread(update)
def display_analysis_results(self):
"""분석 결과 표시"""
def update_results():
if self.analysis_results:
# 결과 텍스트 구성 (요약 정보 포함)
result_text = "📊 분석 요약\\n"
result_text += f"- 분석된 페이지: {len(self.analysis_results)}\\n"
result_text += f"- 분석 완료 시간: {DateTimeUtils.get_timestamp()}\\n\\n"
for page_num, result in self.analysis_results.items():
result_text += f"\\n📋 페이지 {page_num + 1} 분석 결과\\n"
result_text += "=" * 50 + "\\n"
result_text += result
result_text += "\\n\\n"
self.results_text.value = result_text.strip()
# 저장 버튼 활성화
self.save_text_button.disabled = False
self.save_json_button.disabled = False
else:
self.results_text.value = "분석 결과가 없습니다."
self.save_text_button.disabled = True
self.save_json_button.disabled = True
self.page.update()
# 메인 스레드에서 UI 업데이트
self.page.run_thread(update_results)
def show_error_dialog(self, title: str, message: str):
"""오류 다이얼로그 표시"""
dialog = UIComponents.create_error_dialog(title, message)
def close_dialog(e):
dialog.open = False
self.page.update()
dialog.actions[0].on_click = close_dialog
self.page.dialog = dialog
dialog.open = True
self.page.update()
def show_info_dialog(self, title: str, message: str):
"""정보 다이얼로그 표시"""
dialog = UIComponents.create_info_dialog(title, message)
def close_dialog(e):
dialog.open = False
self.page.update()
dialog.actions[0].on_click = close_dialog
self.page.dialog = dialog
dialog.open = True
self.page.update()
def main(page: ft.Page):
"""메인 함수"""
try:
# 애플리케이션 초기화
app = PDFAnalyzerApp(page)
# UI 구성
app.build_ui()
logger.info("애플리케이션 시작 완료")
except Exception as e:
logger.error(f"애플리케이션 시작 실패: {e}")
# 간단한 오류 페이지 표시
page.add(
ft.Container(
content=ft.Column([
ft.Text("애플리케이션 초기화 오류", size=24, weight=ft.FontWeight.BOLD),
ft.Text(f"오류 내용: {str(e)}", size=16),
ft.Text("설정을 확인하고 다시 시도하세요.", size=14),
], alignment=ft.MainAxisAlignment.CENTER),
alignment=ft.alignment.center,
expand=True,
)
)
if __name__ == "__main__":
# 애플리케이션 실행
ft.app(
target=main,
view=ft.AppView.FLET_APP,
upload_dir="uploads",
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""
간단한 배치 처리 앱 실행기
getcode.py 스타일의 간단한 PDF 분석을 여러 파일에 적용
실행 방법:
python run_simple_batch.py
"""
import subprocess
import sys
import os
def run_simple_batch_app():
"""간단한 배치 분석 앱 실행"""
try:
# 현재 디렉토리로 이동
os.chdir("D:/MYCLAUDE_PROJECT/fletimageanalysis")
print("🚀 간단한 배치 PDF 분석기 시작 중...")
print("📂 작업 디렉토리:", os.getcwd())
# simple_batch_analyzer_app.py 실행
result = subprocess.run([
sys.executable,
"simple_batch_analyzer_app.py"
], check=True)
return result.returncode == 0
except subprocess.CalledProcessError as e:
print(f"❌ 실행 중 오류 발생: {e}")
return False
except FileNotFoundError:
print("❌ simple_batch_analyzer_app.py 파일을 찾을 수 없습니다.")
return False
except Exception as e:
print(f"❌ 예기치 않은 오류: {e}")
return False
if __name__ == "__main__":
print("=" * 50)
print("📊 간단한 PDF 배치 분석기")
print("🎯 getcode.py 스타일 → 여러 파일 → CSV 출력")
print("=" * 50)
success = run_simple_batch_app()
if success:
print("✅ 애플리케이션이 성공적으로 실행되었습니다!")
else:
print("❌ 애플리케이션 실행에 실패했습니다.")
print("\n🔧 해결 방법:")
print("1. GEMINI_API_KEY 환경변수가 설정되어 있는지 확인")
print("2. requirements.txt 패키지들이 설치되어 있는지 확인")
print("3. D:/MYCLAUDE_PROJECT/fletimageanalysis 폴더에서 실행")

View File

@@ -0,0 +1,429 @@
"""
간단한 다중 파일 PDF 분석 UI
getcode.py 스타일의 간단한 분석을 여러 파일에 적용하는 Flet 애플리케이션
Author: Claude Assistant
Created: 2025-07-14
Version: 1.0.0
Features:
- 다중 PDF 파일 선택
- getcode.py 프롬프트를 그대로 사용한 간단한 분석
- 실시간 진행률 표시
- 자동 CSV 저장
- 결과 요약 표시
"""
import asyncio
import flet as ft
import os
from datetime import datetime
from typing import List, Optional
import threading
import logging
from simple_batch_processor import SimpleBatchProcessor
from config import Config
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SimpleBatchAnalyzerApp:
"""간단한 배치 분석 애플리케이션"""
def __init__(self, page: ft.Page):
self.page = page
self.selected_files: List[str] = []
self.processor: Optional[SimpleBatchProcessor] = None
self.is_processing = False
# UI 컴포넌트들
self.file_picker = None
self.selected_files_text = None
self.progress_bar = None
self.progress_text = None
self.analyze_button = None
self.results_text = None
self.custom_prompt_field = None
self.setup_page()
self.build_ui()
def setup_page(self):
"""페이지 기본 설정"""
self.page.title = "간단한 다중 PDF 분석기"
self.page.window.width = 900
self.page.window.height = 700
self.page.window.min_width = 800
self.page.window.min_height = 600
self.page.theme_mode = ft.ThemeMode.LIGHT
self.page.padding = 20
# API 키 확인
api_key = Config.GEMINI_API_KEY
if not api_key:
self.show_error_dialog("Gemini API 키가 설정되지 않았습니다. .env 파일을 확인해주세요.")
return
self.processor = SimpleBatchProcessor(api_key)
logger.info("간단한 배치 분석 앱 초기화 완료")
def build_ui(self):
"""UI 구성 요소 생성"""
# 제목
title = ft.Text(
"🔍 간단한 다중 PDF 분석기",
size=24,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_700
)
subtitle = ft.Text(
"getcode.py 스타일의 간단한 프롬프트로 여러 PDF 파일을 분석하고 결과를 CSV로 저장합니다.",
size=14,
color=ft.Colors.GREY_700
)
# 파일 선택 섹션
self.file_picker = ft.FilePicker(
on_result=self.on_files_selected
)
self.page.overlay.append(self.file_picker)
file_select_button = ft.ElevatedButton(
"📁 PDF 파일 선택",
icon=ft.icons.FOLDER_OPEN,
on_click=self.select_files,
style=ft.ButtonStyle(
color=ft.Colors.WHITE,
bgcolor=ft.Colors.BLUE_600
)
)
self.selected_files_text = ft.Text(
"선택된 파일이 없습니다",
size=12,
color=ft.Colors.GREY_600
)
# 사용자 정의 프롬프트 섹션
self.custom_prompt_field = ft.TextField(
label="사용자 정의 프롬프트 (비워두면 기본 프롬프트 사용)",
hint_text="예: PDF 이미지를 분석하여 도면의 주요 정보를 알려주세요",
multiline=True,
min_lines=2,
max_lines=4,
width=850
)
default_prompt_text = ft.Text(
"🔸 기본 프롬프트: \"pdf 이미지 분석하여 도면인지 어떤 정보들이 있는지 알려줘.\"",
size=12,
color=ft.Colors.GREY_600,
italic=True
)
# 분석 시작 섹션
self.analyze_button = ft.ElevatedButton(
"🚀 분석 시작",
icon=ft.icons.PLAY_ARROW,
on_click=self.start_analysis,
disabled=True,
style=ft.ButtonStyle(
color=ft.Colors.WHITE,
bgcolor=ft.Colors.GREEN_600
)
)
# 진행률 섹션
self.progress_bar = ft.ProgressBar(
width=850,
visible=False,
color=ft.Colors.BLUE_600,
bgcolor=ft.Colors.BLUE_100
)
self.progress_text = ft.Text(
"",
size=12,
color=ft.Colors.BLUE_700,
visible=False
)
# 결과 섹션
self.results_text = ft.Text(
"",
size=12,
color=ft.Colors.BLACK,
selectable=True
)
# 레이아웃 구성
content = ft.Column([
# 헤더
ft.Container(
content=ft.Column([title, subtitle]),
margin=ft.margin.only(bottom=20)
),
# 파일 선택
ft.Container(
content=ft.Column([
ft.Text("📁 파일 선택", size=16, weight=ft.FontWeight.BOLD),
file_select_button,
self.selected_files_text
]),
bgcolor=ft.colors.GREY_50,
padding=15,
border_radius=10,
margin=ft.margin.only(bottom=15)
),
# 프롬프트 설정
ft.Container(
content=ft.Column([
ft.Text("✏️ 프롬프트 설정", size=16, weight=ft.FontWeight.BOLD),
self.custom_prompt_field,
default_prompt_text
]),
bgcolor=ft.colors.GREY_50,
padding=15,
border_radius=10,
margin=ft.margin.only(bottom=15)
),
# 분석 시작
ft.Container(
content=ft.Column([
ft.Text("🔄 분석 실행", size=16, weight=ft.FontWeight.BOLD),
self.analyze_button,
self.progress_bar,
self.progress_text
]),
bgcolor=ft.colors.GREY_50,
padding=15,
border_radius=10,
margin=ft.margin.only(bottom=15)
),
# 결과 표시
ft.Container(
content=ft.Column([
ft.Text("📊 분석 결과", size=16, weight=ft.FontWeight.BOLD),
self.results_text
]),
bgcolor=ft.colors.GREY_50,
padding=15,
border_radius=10
)
])
# 스크롤 가능한 컨테이너로 감싸기
scrollable_content = ft.Container(
content=content,
alignment=ft.alignment.top_center
)
self.page.add(scrollable_content)
self.page.update()
def select_files(self, e):
"""파일 선택 대화상자 열기"""
self.file_picker.pick_files(
allow_multiple=True,
allowed_extensions=["pdf"],
dialog_title="분석할 PDF 파일들을 선택하세요"
)
def on_files_selected(self, e: ft.FilePickerResultEvent):
"""파일 선택 완료 후 처리"""
if e.files:
self.selected_files = [file.path for file in e.files]
file_count = len(self.selected_files)
if file_count == 1:
self.selected_files_text.value = f"{file_count}개 파일 선택됨: {os.path.basename(self.selected_files[0])}"
else:
self.selected_files_text.value = f"{file_count}개 파일 선택됨"
self.selected_files_text.color = ft.colors.GREEN_700
self.analyze_button.disabled = False
logger.info(f"{file_count}개 PDF 파일 선택완료")
else:
self.selected_files = []
self.selected_files_text.value = "선택된 파일이 없습니다"
self.selected_files_text.color = ft.colors.GREY_600
self.analyze_button.disabled = True
self.page.update()
def start_analysis(self, e):
"""분석 시작"""
if self.is_processing or not self.selected_files:
return
self.is_processing = True
self.analyze_button.disabled = True
self.progress_bar.visible = True
self.progress_text.visible = True
self.progress_bar.value = 0
self.progress_text.value = "분석 준비 중..."
self.results_text.value = ""
self.page.update()
# 백그라운드에서 비동기 처리 실행
threading.Thread(target=self.run_analysis_async, daemon=True).start()
def run_analysis_async(self):
"""비동기 분석 실행"""
try:
# 새 이벤트 루프 생성 (백그라운드 스레드에서)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 분석 실행
loop.run_until_complete(self.process_files())
except Exception as e:
logger.error(f"분석 실행 중 오류: {e}")
self.update_ui_on_error(str(e))
finally:
loop.close()
async def process_files(self):
"""파일 처리 실행"""
try:
# 사용자 정의 프롬프트 확인
custom_prompt = self.custom_prompt_field.value.strip()
if not custom_prompt:
custom_prompt = None
# 진행률 콜백 함수
def progress_callback(current: int, total: int, status: str):
progress_value = current / total
self.update_progress(progress_value, f"{current}/{total} - {status}")
# 배치 처리 실행
results = await self.processor.process_multiple_pdf_files(
pdf_file_paths=self.selected_files,
custom_prompt=custom_prompt,
max_concurrent_files=2, # 안정성을 위해 낮게 설정
progress_callback=progress_callback
)
# 결과 요약
summary = self.processor.get_processing_summary()
self.update_ui_on_completion(summary)
except Exception as e:
logger.error(f"파일 처리 중 오류: {e}")
self.update_ui_on_error(str(e))
def update_progress(self, value: float, text: str):
"""진행률 업데이트 (스레드 안전)"""
def update():
self.progress_bar.value = value
self.progress_text.value = text
self.page.update()
self.page.run_thread_safe(update)
def update_ui_on_completion(self, summary: dict):
"""분석 완료 시 UI 업데이트"""
def update():
self.progress_bar.visible = False
self.progress_text.visible = False
self.analyze_button.disabled = False
self.is_processing = False
# 결과 요약 텍스트 생성
result_text = "🎉 분석 완료!\n\n"
result_text += f"📊 처리 요약:\n"
result_text += f"• 전체 파일: {summary.get('total_files', 0)}\n"
result_text += f"• 성공: {summary.get('success_files', 0)}\n"
result_text += f"• 실패: {summary.get('failed_files', 0)}\n"
result_text += f"• 성공률: {summary.get('success_rate', 0)}%\n"
result_text += f"• 전체 처리 시간: {summary.get('total_processing_time', 0)}\n"
result_text += f"• 평균 처리 시간: {summary.get('avg_processing_time', 0)}초/파일\n"
result_text += f"• 전체 파일 크기: {summary.get('total_file_size_mb', 0)}MB\n\n"
result_text += "💾 결과가 CSV 파일로 자동 저장되었습니다.\n"
result_text += "파일 위치: D:/MYCLAUDE_PROJECT/fletimageanalysis/results/"
self.results_text.value = result_text
self.results_text.color = ft.colors.GREEN_700
self.page.update()
# 완료 알림
self.show_success_dialog("분석이 완료되었습니다!", result_text)
self.page.run_thread_safe(update)
def update_ui_on_error(self, error_message: str):
"""오류 발생 시 UI 업데이트"""
def update():
self.progress_bar.visible = False
self.progress_text.visible = False
self.analyze_button.disabled = False
self.is_processing = False
self.results_text.value = f"❌ 분석 중 오류가 발생했습니다:\n{error_message}"
self.results_text.color = ft.colors.RED_700
self.page.update()
self.show_error_dialog("분석 오류", error_message)
self.page.run_thread_safe(update)
def show_success_dialog(self, title: str, message: str):
"""성공 다이얼로그 표시"""
def show():
dialog = ft.AlertDialog(
title=ft.Text(title),
content=ft.Text(message, selectable=True),
actions=[
ft.TextButton("확인", on_click=lambda e: self.close_dialog())
]
)
self.page.overlay.append(dialog)
dialog.open = True
self.page.update()
self.page.run_thread_safe(show)
def show_error_dialog(self, title: str, message: str = ""):
"""오류 다이얼로그 표시"""
def show():
dialog = ft.AlertDialog(
title=ft.Text(title, color=ft.colors.RED_700),
content=ft.Text(message if message else title, selectable=True),
actions=[
ft.TextButton("확인", on_click=lambda e: self.close_dialog())
]
)
self.page.overlay.append(dialog)
dialog.open = True
self.page.update()
self.page.run_thread_safe(show)
def close_dialog(self):
"""다이얼로그 닫기"""
if self.page.overlay:
for overlay in self.page.overlay:
if isinstance(overlay, ft.AlertDialog):
overlay.open = False
self.page.update()
async def main(page: ft.Page):
"""메인 함수"""
app = SimpleBatchAnalyzerApp(page)
if __name__ == "__main__":
# Flet 애플리케이션 실행
ft.app(target=main, view=ft.AppView.FLET_APP)

View File

@@ -0,0 +1,378 @@
"""
간단한 다중 파일 배치 처리 모듈
getcode.py 스타일의 간단한 분석을 여러 파일에 적용하고 결과를 CSV로 저장합니다.
Author: Claude Assistant
Created: 2025-07-14
Version: 1.0.0
"""
import asyncio
import os
import pandas as pd
import base64
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any, Optional, Callable
from dataclasses import dataclass
import logging
from simple_gemini_analyzer import SimpleGeminiAnalyzer
from pdf_processor import PDFProcessor
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class SimpleBatchResult:
"""간단한 배치 처리 결과"""
file_path: str
file_name: str
file_size_mb: float
processing_time_seconds: float
success: bool
# 분석 결과
analysis_result: Optional[str] = None
analysis_timestamp: Optional[str] = None
prompt_used: Optional[str] = None
model_used: Optional[str] = None
error_message: Optional[str] = None
# 메타데이터
processed_at: Optional[str] = None
class SimpleBatchProcessor:
"""
간단한 다중 파일 배치 처리기
getcode.py 스타일의 분석을 여러 PDF 파일에 적용합니다.
"""
def __init__(self, gemini_api_key: str):
"""
배치 처리기 초기화
Args:
gemini_api_key: Gemini API 키
"""
self.gemini_api_key = gemini_api_key
self.analyzer = SimpleGeminiAnalyzer(gemini_api_key)
self.pdf_processor = PDFProcessor()
self.results: List[SimpleBatchResult] = []
self.current_progress = 0
self.total_files = 0
logger.info("간단한 배치 처리기 초기화 완료")
async def process_multiple_pdf_files(
self,
pdf_file_paths: List[str],
output_csv_path: Optional[str] = None,
custom_prompt: Optional[str] = None,
max_concurrent_files: int = 3,
progress_callback: Optional[Callable[[int, int, str], None]] = None
) -> List[SimpleBatchResult]:
"""
여러 PDF 파일을 배치로 처리하고 결과를 CSV로 저장
Args:
pdf_file_paths: 처리할 PDF 파일 경로 리스트
output_csv_path: 출력 CSV 파일 경로 (None인 경우 자동 생성)
custom_prompt: 사용자 정의 프롬프트 (None인 경우 기본 프롬프트 사용)
max_concurrent_files: 동시 처리할 최대 파일 수
progress_callback: 진행률 콜백 함수 (current, total, status)
Returns:
처리 결과 리스트
"""
self.results = []
self.total_files = len(pdf_file_paths)
self.current_progress = 0
logger.info(f"간단한 배치 처리 시작: {self.total_files}개 PDF 파일")
if not pdf_file_paths:
logger.warning("처리할 파일이 없습니다.")
return []
# 동시 처리 제한을 위한 세마포어
semaphore = asyncio.Semaphore(max_concurrent_files)
# 각 파일에 대한 처리 태스크 생성
tasks = []
for i, file_path in enumerate(pdf_file_paths):
task = self._process_single_pdf_with_semaphore(
semaphore, file_path, custom_prompt, progress_callback, i + 1
)
tasks.append(task)
# 모든 파일 처리 완료까지 대기
await asyncio.gather(*tasks, return_exceptions=True)
logger.info(f"배치 처리 완료: {len(self.results)}개 결과")
# CSV 저장
if output_csv_path or self.results:
csv_path = output_csv_path or self._generate_default_csv_path()
await self.save_results_to_csv(csv_path)
return self.results
async def _process_single_pdf_with_semaphore(
self,
semaphore: asyncio.Semaphore,
file_path: str,
custom_prompt: Optional[str],
progress_callback: Optional[Callable[[int, int, str], None]],
file_number: int
) -> None:
"""세마포어를 사용하여 단일 PDF 파일 처리"""
async with semaphore:
result = await self._process_single_pdf_file(file_path, custom_prompt)
self.results.append(result)
self.current_progress += 1
if progress_callback:
status = f"처리 완료: {result.file_name}"
if not result.success:
status = f"처리 실패: {result.file_name}"
progress_callback(self.current_progress, self.total_files, status)
async def _process_single_pdf_file(
self,
file_path: str,
custom_prompt: Optional[str] = None
) -> SimpleBatchResult:
"""
단일 PDF 파일 처리
Args:
file_path: PDF 파일 경로
custom_prompt: 사용자 정의 프롬프트
Returns:
처리 결과
"""
start_time = asyncio.get_event_loop().time()
file_name = os.path.basename(file_path)
try:
# 파일 정보 수집
file_size = os.path.getsize(file_path)
file_size_mb = round(file_size / (1024 * 1024), 2)
logger.info(f"PDF 파일 처리 시작: {file_name} ({file_size_mb}MB)")
# PDF를 이미지로 변환 (첫 번째 페이지만)
images = self.pdf_processor.convert_to_images(file_path, max_pages=1)
if not images:
raise ValueError("PDF를 이미지로 변환할 수 없습니다")
# 첫 번째 페이지 이미지를 바이트로 변환
first_page_image = images[0]
image_bytes = self.pdf_processor.image_to_bytes(first_page_image)
# Gemini API로 분석 (비동기 처리)
loop = asyncio.get_event_loop()
analysis_result = await loop.run_in_executor(
None,
self.analyzer.analyze_image_from_bytes,
image_bytes,
custom_prompt,
"image/png"
)
if analysis_result and analysis_result['success']:
result = SimpleBatchResult(
file_path=file_path,
file_name=file_name,
file_size_mb=file_size_mb,
processing_time_seconds=0, # 나중에 계산
success=True,
analysis_result=analysis_result['analysis_result'],
analysis_timestamp=analysis_result['timestamp'],
prompt_used=analysis_result['prompt_used'],
model_used=analysis_result['model'],
error_message=None,
processed_at=datetime.now().isoformat()
)
logger.info(f"분석 성공: {file_name}")
else:
error_msg = analysis_result['error_message'] if analysis_result else "알 수 없는 오류"
result = SimpleBatchResult(
file_path=file_path,
file_name=file_name,
file_size_mb=file_size_mb,
processing_time_seconds=0,
success=False,
analysis_result=None,
error_message=error_msg,
processed_at=datetime.now().isoformat()
)
logger.error(f"분석 실패: {file_name} - {error_msg}")
except Exception as e:
error_msg = f"파일 처리 오류: {str(e)}"
logger.error(f"파일 처리 오류 ({file_name}): {error_msg}")
result = SimpleBatchResult(
file_path=file_path,
file_name=file_name,
file_size_mb=0,
processing_time_seconds=0,
success=False,
error_message=error_msg,
processed_at=datetime.now().isoformat()
)
finally:
# 처리 시간 계산
end_time = asyncio.get_event_loop().time()
result.processing_time_seconds = round(end_time - start_time, 2)
return result
async def save_results_to_csv(self, csv_path: str) -> None:
"""
처리 결과를 CSV 파일로 저장
Args:
csv_path: 출력 CSV 파일 경로
"""
try:
if not self.results:
logger.warning("저장할 결과가 없습니다.")
return
# 결과를 DataFrame으로 변환
data_rows = []
for result in self.results:
row = {
'file_name': result.file_name,
'file_path': result.file_path,
'file_size_mb': result.file_size_mb,
'processing_time_seconds': result.processing_time_seconds,
'success': result.success,
'analysis_result': result.analysis_result or '',
'analysis_timestamp': result.analysis_timestamp or '',
'prompt_used': result.prompt_used or '',
'model_used': result.model_used or '',
'error_message': result.error_message or '',
'processed_at': result.processed_at or ''
}
data_rows.append(row)
# DataFrame 생성
df = pd.DataFrame(data_rows)
# 컬럼 순서 정렬
column_order = [
'file_name', 'success', 'file_size_mb', 'processing_time_seconds',
'analysis_result', 'prompt_used', 'model_used', 'analysis_timestamp',
'error_message', 'processed_at', 'file_path'
]
df = df[column_order]
# 출력 디렉토리 생성
os.makedirs(os.path.dirname(csv_path), exist_ok=True)
# UTF-8 BOM으로 저장 (한글 호환성)
df.to_csv(csv_path, index=False, encoding='utf-8-sig')
logger.info(f"CSV 저장 완료: {csv_path}")
logger.info(f"{len(data_rows)}개 파일 결과 저장")
# 처리 요약 로그
success_count = sum(1 for r in self.results if r.success)
failure_count = len(self.results) - success_count
logger.info(f"처리 요약 - 성공: {success_count}개, 실패: {failure_count}")
except Exception as e:
logger.error(f"CSV 저장 오류: {str(e)}")
raise
def _generate_default_csv_path(self) -> str:
"""기본 CSV 파일 경로 생성"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
results_dir = "D:/MYCLAUDE_PROJECT/fletimageanalysis/results"
os.makedirs(results_dir, exist_ok=True)
return os.path.join(results_dir, f"simple_batch_analysis_{timestamp}.csv")
def get_processing_summary(self) -> Dict[str, Any]:
"""처리 결과 요약 정보 반환"""
if not self.results:
return {}
total_files = len(self.results)
success_files = sum(1 for r in self.results if r.success)
failed_files = total_files - success_files
total_processing_time = sum(r.processing_time_seconds for r in self.results)
avg_processing_time = total_processing_time / total_files if total_files > 0 else 0
total_file_size = sum(r.file_size_mb for r in self.results)
return {
'total_files': total_files,
'success_files': success_files,
'failed_files': failed_files,
'total_processing_time': round(total_processing_time, 2),
'avg_processing_time': round(avg_processing_time, 2),
'total_file_size_mb': round(total_file_size, 2),
'success_rate': round((success_files / total_files) * 100, 1) if total_files > 0 else 0
}
# 사용 예시
async def main():
"""사용 예시 함수"""
# API 키 설정 (실제 사용 시에는 .env 파일이나 환경변수 사용)
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
print("❌ GEMINI_API_KEY 환경변수를 설정해주세요.")
return
# 배치 처리기 초기화
processor = SimpleBatchProcessor(api_key)
# 진행률 콜백 함수
def progress_callback(current: int, total: int, status: str):
percentage = (current / total) * 100
print(f"진행률: {current}/{total} ({percentage:.1f}%) - {status}")
# 샘플 PDF 파일 경로 (실제 사용 시에는 실제 파일 경로로 교체)
pdf_files = [
"D:/MYCLAUDE_PROJECT/fletimageanalysis/testsample/sample1.pdf",
"D:/MYCLAUDE_PROJECT/fletimageanalysis/testsample/sample2.pdf",
# 더 많은 파일 추가 가능
]
# 실제 존재하는 PDF 파일만 필터링
existing_files = [f for f in pdf_files if os.path.exists(f)]
if not existing_files:
print("❌ 처리할 PDF 파일이 없습니다.")
return
# 배치 처리 실행
results = await processor.process_multiple_pdf_files(
pdf_file_paths=existing_files,
custom_prompt=None, # 기본 프롬프트 사용
max_concurrent_files=2,
progress_callback=progress_callback
)
# 처리 요약 출력
summary = processor.get_processing_summary()
print("\n=== 처리 요약 ===")
for key, value in summary.items():
print(f"{key}: {value}")
if __name__ == "__main__":
# 비동기 메인 함수 실행
asyncio.run(main())

View File

@@ -0,0 +1,235 @@
# To run this code you need to install the following dependencies:
# pip install google-genai
"""
간단한 Gemini 이미지 분석기
getcode.py의 프롬프트를 그대로 사용하여 PDF 이미지를 분석합니다.
Author: Claude Assistant
Created: 2025-07-14
Version: 1.0.0
Based on: getcode.py (original user code)
"""
import base64
import os
import logging
from google import genai
from google.genai import types
from typing import Optional, Dict, Any
from datetime import datetime
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SimpleGeminiAnalyzer:
"""
getcode.py 스타일의 간단한 Gemini 이미지 분석기
구조화된 JSON 스키마 대신 자연어 텍스트 분석을 수행합니다.
"""
def __init__(self, api_key: Optional[str] = None):
"""
간단한 Gemini 분석기 초기화
Args:
api_key: Gemini API 키 (None인 경우 환경변수에서 로드)
"""
self.api_key = api_key or os.environ.get("GEMINI_API_KEY")
self.model = "gemini-2.5-flash" # getcode.py와 동일한 모델
# getcode.py와 동일한 프롬프트 사용
self.default_prompt = "pdf 이미지 분석하여 도면인지 어떤 정보들이 있는지 알려줘."
if not self.api_key:
raise ValueError("Gemini API 키가 설정되지 않았습니다. GEMINI_API_KEY 환경변수를 설정하거나 api_key 매개변수를 제공하세요.")
try:
self.client = genai.Client(api_key=self.api_key)
logger.info(f"Simple Gemini 클라이언트 초기화 완료 (모델: {self.model})")
except Exception as e:
logger.error(f"Gemini 클라이언트 초기화 실패: {e}")
raise
def analyze_pdf_image(
self,
base64_data: str,
custom_prompt: Optional[str] = None,
mime_type: str = "application/pdf"
) -> Optional[Dict[str, Any]]:
"""
Base64로 인코딩된 PDF 데이터를 분석합니다.
getcode.py와 동일한 방식으로 동작합니다.
Args:
base64_data: Base64로 인코딩된 PDF 데이터
custom_prompt: 사용자 정의 프롬프트 (None인 경우 기본 프롬프트 사용)
mime_type: 파일 MIME 타입
Returns:
분석 결과 딕셔너리 또는 None (실패 시)
{
'analysis_result': str, # 분석 결과 텍스트
'success': bool, # 성공 여부
'timestamp': str, # 분석 시각
'prompt_used': str, # 사용된 프롬프트
'model': str, # 사용된 모델
'error_message': str # 오류 메시지 (실패 시)
}
"""
try:
prompt = custom_prompt or self.default_prompt
logger.info(f"이미지 분석 시작 - 프롬프트: {prompt[:50]}...")
# getcode.py와 동일한 구조로 컨텐츠 생성
contents = [
types.Content(
role="user",
parts=[
types.Part.from_bytes(
mime_type=mime_type,
data=base64.b64decode(base64_data),
),
types.Part.from_text(text=prompt),
],
),
]
# getcode.py와 동일한 설정 사용
generate_content_config = types.GenerateContentConfig(
temperature=0,
top_p=0.05,
thinking_config=types.ThinkingConfig(
thinking_budget=0,
),
response_mime_type="text/plain", # JSON이 아닌 일반 텍스트
)
# 스트리밍이 아닌 일반 응답으로 수정 (CSV 저장을 위해)
response = self.client.models.generate_content(
model=self.model,
contents=contents,
config=generate_content_config,
)
if response and hasattr(response, 'text') and response.text:
result = {
'analysis_result': response.text.strip(),
'success': True,
'timestamp': datetime.now().isoformat(),
'prompt_used': prompt,
'model': self.model,
'error_message': None
}
logger.info(f"분석 완료: {len(response.text)} 문자")
return result
else:
logger.error("API 응답에서 텍스트를 찾을 수 없습니다.")
return {
'analysis_result': None,
'success': False,
'timestamp': datetime.now().isoformat(),
'prompt_used': prompt,
'model': self.model,
'error_message': "API 응답에서 텍스트를 찾을 수 없습니다."
}
except Exception as e:
error_msg = f"이미지 분석 중 오류 발생: {str(e)}"
logger.error(error_msg)
return {
'analysis_result': None,
'success': False,
'timestamp': datetime.now().isoformat(),
'prompt_used': custom_prompt or self.default_prompt,
'model': self.model,
'error_message': error_msg
}
def analyze_image_from_bytes(
self,
image_bytes: bytes,
custom_prompt: Optional[str] = None,
mime_type: str = "image/png"
) -> Optional[Dict[str, Any]]:
"""
바이트 형태의 이미지를 직접 분석합니다.
Args:
image_bytes: 이미지 바이트 데이터
custom_prompt: 사용자 정의 프롬프트
mime_type: 이미지 MIME 타입
Returns:
분석 결과 딕셔너리
"""
try:
# 바이트를 base64로 인코딩
base64_data = base64.b64encode(image_bytes).decode('utf-8')
return self.analyze_pdf_image(base64_data, custom_prompt, mime_type)
except Exception as e:
error_msg = f"이미지 바이트 분석 중 오류: {str(e)}"
logger.error(error_msg)
return {
'analysis_result': None,
'success': False,
'timestamp': datetime.now().isoformat(),
'prompt_used': custom_prompt or self.default_prompt,
'model': self.model,
'error_message': error_msg
}
def validate_api_connection(self) -> bool:
"""API 연결 상태를 확인합니다."""
try:
test_content = [
types.Content(
role="user",
parts=[types.Part.from_text(text="안녕하세요")]
)
]
config = types.GenerateContentConfig(
temperature=0,
response_mime_type="text/plain"
)
response = self.client.models.generate_content(
model=self.model,
contents=test_content,
config=config
)
if response and hasattr(response, 'text'):
logger.info("Simple Gemini API 연결 테스트 성공")
return True
else:
logger.error("Simple Gemini API 연결 테스트 실패")
return False
except Exception as e:
logger.error(f"Simple Gemini API 연결 테스트 중 오류: {e}")
return False
# 사용 예시
if __name__ == "__main__":
# 테스트 코드
analyzer = SimpleGeminiAnalyzer()
# API 연결 테스트
if analyzer.validate_api_connection():
print("✅ API 연결 성공")
# 샘플 이미지 분석 (실제 사용 시에는 PDF 파일에서 추출한 이미지 사용)
sample_text = "테스트용 간단한 텍스트 분석"
# 실제 사용 예시:
# with open("sample.pdf", "rb") as f:
# pdf_bytes = f.read()
# base64_data = base64.b64encode(pdf_bytes).decode('utf-8')
# result = analyzer.analyze_pdf_image(base64_data)
# print("분석 결과:", result)
else:
print("❌ API 연결 실패")

View File

@@ -0,0 +1,633 @@
# -*- coding: utf-8 -*-
"""
DXF 파일 처리 모듈
ezdxf 라이브러리를 사용하여 DXF 파일에서 도곽 정보 및 Block Reference/Attribute Reference를 추출
"""
import os
import json
import logging
from typing import Dict, List, Optional, Tuple, Union, Any
from dataclasses import dataclass, asdict, field
try:
import ezdxf
from ezdxf.document import Drawing
from ezdxf.entities import Insert, Attrib, AttDef, Text, MText
from ezdxf.layouts import BlockLayout, Modelspace
EZDXF_AVAILABLE = True
except ImportError:
EZDXF_AVAILABLE = False
logging.warning("ezdxf 라이브러리가 설치되지 않았습니다. DXF 기능이 비활성화됩니다.")
from config import Config
@dataclass
class BoundingBox:
"""바운딩 박스 정보를 담는 데이터 클래스"""
min_x: float
min_y: float
max_x: float
max_y: float
@property
def width(self) -> float:
return self.max_x - self.min_x
@property
def height(self) -> float:
return self.max_y - self.min_y
@property
def center(self) -> Tuple[float, float]:
return ((self.min_x + self.max_x) / 2, (self.min_y + self.max_y) / 2)
@dataclass
class AttributeInfo:
"""속성 정보를 담는 데이터 클래스 - 모든 DXF 속성 포함"""
tag: str
text: str
position: Tuple[float, float, float] # insert point (x, y, z)
height: float
width: float
rotation: float
layer: str
bounding_box: Optional[BoundingBox] = None
# 추가 DXF 속성들
prompt: Optional[str] = None # 프롬프트 문자열 (ATTDEF에서 가져옴)
style: Optional[str] = None # 텍스트 스타일
invisible: bool = False # 보이지 않는 속성
const: bool = False # 상수 속성
verify: bool = False # 검증 필요
preset: bool = False # 프롬프트 없이 삽입
align_point: Optional[Tuple[float, float, float]] = None # 정렬점
halign: int = 0 # 수평 정렬 (0=LEFT, 2=RIGHT, etc.)
valign: int = 0 # 수직 정렬 (0=BASELINE, 1=BOTTOM, etc.)
text_generation_flag: int = 0 # 텍스트 생성 플래그
oblique_angle: float = 0.0 # 기울기 각도
width_factor: float = 1.0 # 폭 비율
color: Optional[int] = None # 색상 코드
linetype: Optional[str] = None # 선 타입
lineweight: Optional[int] = None # 선 굵기
# 좌표 정보
insert_x: float = 0.0 # X 좌표
insert_y: float = 0.0 # Y 좌표
insert_z: float = 0.0 # Z 좌표
# 계산된 정보
estimated_width: float = 0.0 # 추정 텍스트 폭
entity_handle: Optional[str] = None # DXF 엔티티 핸들
@dataclass
class BlockInfo:
"""블록 정보를 담는 데이터 클래스"""
name: str
position: Tuple[float, float, float]
scale: Tuple[float, float, float]
rotation: float
layer: str
attributes: List[AttributeInfo]
bounding_box: Optional[BoundingBox] = None
@dataclass
class TitleBlockInfo:
"""도곽 정보를 담는 데이터 클래스"""
drawing_name: Optional[str] = None # 도면명
drawing_number: Optional[str] = None # 도면번호
construction_field: Optional[str] = None # 건설분야
construction_stage: Optional[str] = None # 건설단계
scale: Optional[str] = None # 축척
project_name: Optional[str] = None # 프로젝트명
designer: Optional[str] = None # 설계자
date: Optional[str] = None # 날짜
revision: Optional[str] = None # 리비전
location: Optional[str] = None # 위치
bounding_box: Optional[BoundingBox] = None # 도곽 전체 바운딩 박스
block_name: Optional[str] = None # 도곽 블록 이름
# 모든 attributes 정보 저장
all_attributes: List[AttributeInfo] = field(default_factory=list) # 도곽의 모든 속성 정보 리스트
attributes_count: int = 0 # 속성 개수
# 추가 메타데이터
block_position: Optional[Tuple[float, float, float]] = None # 블록 위치
block_scale: Optional[Tuple[float, float, float]] = None # 블록 스케일
block_rotation: float = 0.0 # 블록 회전각
block_layer: Optional[str] = None # 블록 레이어
def __post_init__(self):
"""초기화 후 처리"""
self.attributes_count = len(self.all_attributes)
class DXFProcessor:
"""DXF 파일 처리 클래스"""
# 도곽 식별을 위한 키워드 정의
TITLE_BLOCK_KEYWORDS = {
'건설분야': ['construction_field', 'field', '분야', '공사', 'category'],
'건설단계': ['construction_stage', 'stage', '단계', 'phase'],
'도면명': ['drawing_name', 'title', '제목', 'name', ''],
'축척': ['scale', '축척', 'ratio', '비율'],
'도면번호': ['drawing_number', 'number', '번호', 'no', 'dwg'],
'설계자': ['designer', '설계', 'design', 'drawn'],
'프로젝트': ['project', '사업', '공사명'],
'날짜': ['date', '일자', '작성일'],
'리비전': ['revision', 'rev', '개정'],
'위치': ['location', '위치', '지역']
}
def __init__(self):
"""DXF 처리기 초기화"""
self.logger = logging.getLogger(__name__)
if not EZDXF_AVAILABLE:
raise ImportError("ezdxf 라이브러리가 필요합니다. 'pip install ezdxf'로 설치하세요.")
def validate_dxf_file(self, file_path: str) -> bool:
"""DXF 파일 유효성 검사"""
try:
if not os.path.exists(file_path):
self.logger.error(f"파일이 존재하지 않습니다: {file_path}")
return False
if not file_path.lower().endswith('.dxf'):
self.logger.error(f"DXF 파일이 아닙니다: {file_path}")
return False
# ezdxf로 파일 읽기 시도
doc = ezdxf.readfile(file_path)
if doc is None:
return False
self.logger.info(f"DXF 파일 유효성 검사 성공: {file_path}")
return True
except ezdxf.DXFStructureError as e:
self.logger.error(f"DXF 구조 오류: {e}")
return False
except Exception as e:
self.logger.error(f"DXF 파일 검증 중 오류: {e}")
return False
def load_dxf_document(self, file_path: str) -> Optional[Drawing]:
"""DXF 문서 로드"""
try:
doc = ezdxf.readfile(file_path)
self.logger.info(f"DXF 문서 로드 성공: {file_path}")
return doc
except Exception as e:
self.logger.error(f"DXF 문서 로드 실패: {e}")
return None
def calculate_text_bounding_box(self, entity: Union[Text, MText, Attrib]) -> Optional[BoundingBox]:
"""텍스트 엔티티의 바운딩 박스 계산"""
try:
if hasattr(entity, 'dxf'):
# 텍스트 위치 가져오기
insert_point = getattr(entity.dxf, 'insert', (0, 0, 0))
height = getattr(entity.dxf, 'height', 1.0)
# 텍스트 내용 길이 추정 (폰트에 따라 다르지만 대략적으로)
text_content = ""
if hasattr(entity.dxf, 'text'):
text_content = entity.dxf.text
elif hasattr(entity, 'plain_text'):
text_content = entity.plain_text()
# 텍스트 너비 추정 (높이의 0.6배 * 글자 수)
estimated_width = len(text_content) * height * 0.6
# 회전 고려 (기본값)
rotation = getattr(entity.dxf, 'rotation', 0)
# 바운딩 박스 계산
x, y = insert_point[0], insert_point[1]
return BoundingBox(
min_x=x,
min_y=y,
max_x=x + estimated_width,
max_y=y + height
)
except Exception as e:
self.logger.warning(f"텍스트 바운딩 박스 계산 실패: {e}")
return None
def extract_block_references(self, doc: Drawing) -> List[BlockInfo]:
"""문서에서 모든 Block Reference 추출"""
block_refs = []
try:
# 모델스페이스에서 INSERT 엔티티 찾기
msp = doc.modelspace()
for insert in msp.query('INSERT'):
block_info = self._process_block_reference(doc, insert)
if block_info:
block_refs.append(block_info)
# 페이퍼스페이스도 확인
for layout_name in doc.layout_names_in_taborder():
if layout_name.startswith('*'): # 모델스페이스 제외
continue
try:
layout = doc.paperspace(layout_name)
for insert in layout.query('INSERT'):
block_info = self._process_block_reference(doc, insert)
if block_info:
block_refs.append(block_info)
except Exception as e:
self.logger.warning(f"레이아웃 {layout_name} 처리 중 오류: {e}")
self.logger.info(f"{len(block_refs)}개의 블록 참조를 찾았습니다.")
return block_refs
except Exception as e:
self.logger.error(f"블록 참조 추출 중 오류: {e}")
return []
def _process_block_reference(self, doc: Drawing, insert: Insert) -> Optional[BlockInfo]:
"""개별 Block Reference 처리 - ATTDEF 정보도 함께 수집"""
try:
# 블록 정보 추출
block_name = insert.dxf.name
position = (insert.dxf.insert.x, insert.dxf.insert.y, insert.dxf.insert.z)
scale = (
getattr(insert.dxf, 'xscale', 1.0),
getattr(insert.dxf, 'yscale', 1.0),
getattr(insert.dxf, 'zscale', 1.0)
)
rotation = getattr(insert.dxf, 'rotation', 0.0)
layer = getattr(insert.dxf, 'layer', '0')
# ATTDEF 정보 수집 (프롬프트 정보 포함)
attdef_info = {}
try:
block_layout = doc.blocks.get(block_name)
if block_layout:
for attdef in block_layout.query('ATTDEF'):
tag = getattr(attdef.dxf, 'tag', '')
prompt = getattr(attdef.dxf, 'prompt', '')
if tag:
attdef_info[tag] = {
'prompt': prompt,
'default_text': getattr(attdef.dxf, 'text', ''),
'position': (attdef.dxf.insert.x, attdef.dxf.insert.y, attdef.dxf.insert.z),
'height': getattr(attdef.dxf, 'height', 1.0),
'style': getattr(attdef.dxf, 'style', 'Standard'),
'invisible': getattr(attdef.dxf, 'invisible', False),
'const': getattr(attdef.dxf, 'const', False),
'verify': getattr(attdef.dxf, 'verify', False),
'preset': getattr(attdef.dxf, 'preset', False)
}
except Exception as e:
self.logger.debug(f"ATTDEF 정보 수집 실패: {e}")
# ATTRIB 속성 추출 및 ATTDEF 정보와 결합
attributes = []
for attrib in insert.attribs:
attr_info = self._extract_attribute_info(attrib)
if attr_info and attr_info.tag in attdef_info:
# ATTDEF에서 프롬프트 정보 추가
attr_info.prompt = attdef_info[attr_info.tag]['prompt']
if attr_info:
attributes.append(attr_info)
return BlockInfo(
name=block_name,
position=position,
scale=scale,
rotation=rotation,
layer=layer,
attributes=attributes
)
except Exception as e:
self.logger.warning(f"블록 참조 처리 중 오류: {e}")
return None
def _extract_attribute_info(self, attrib: Attrib) -> Optional[AttributeInfo]:
"""Attribute Reference에서 모든 정보 추출"""
try:
# 기본 속성
tag = getattr(attrib.dxf, 'tag', '')
text = getattr(attrib.dxf, 'text', '')
# 위치 정보
insert_point = getattr(attrib.dxf, 'insert', (0, 0, 0))
position = (insert_point.x if hasattr(insert_point, 'x') else insert_point[0],
insert_point.y if hasattr(insert_point, 'y') else insert_point[1],
insert_point.z if hasattr(insert_point, 'z') else insert_point[2])
# 텍스트 속성
height = getattr(attrib.dxf, 'height', 1.0)
width = getattr(attrib.dxf, 'width', 1.0)
rotation = getattr(attrib.dxf, 'rotation', 0.0)
# 레이어 및 스타일
layer = getattr(attrib.dxf, 'layer', '0')
style = getattr(attrib.dxf, 'style', 'Standard')
# 속성 플래그
invisible = getattr(attrib.dxf, 'invisible', False)
const = getattr(attrib.dxf, 'const', False)
verify = getattr(attrib.dxf, 'verify', False)
preset = getattr(attrib.dxf, 'preset', False)
# 정렬 정보
align_point_data = getattr(attrib.dxf, 'align_point', None)
align_point = None
if align_point_data:
align_point = (align_point_data.x if hasattr(align_point_data, 'x') else align_point_data[0],
align_point_data.y if hasattr(align_point_data, 'y') else align_point_data[1],
align_point_data.z if hasattr(align_point_data, 'z') else align_point_data[2])
halign = getattr(attrib.dxf, 'halign', 0)
valign = getattr(attrib.dxf, 'valign', 0)
# 텍스트 형식
text_generation_flag = getattr(attrib.dxf, 'text_generation_flag', 0)
oblique_angle = getattr(attrib.dxf, 'oblique_angle', 0.0)
width_factor = getattr(attrib.dxf, 'width_factor', 1.0)
# 시각적 속성
color = getattr(attrib.dxf, 'color', None)
linetype = getattr(attrib.dxf, 'linetype', None)
lineweight = getattr(attrib.dxf, 'lineweight', None)
# 엔티티 핸들
entity_handle = getattr(attrib.dxf, 'handle', None)
# 텍스트 폭 추정 (높이의 0.6배 * 글자 수)
estimated_width = len(text) * height * 0.6 * width_factor
# 바운딩 박스 계산
bounding_box = self.calculate_text_bounding_box(attrib)
# 프롬프트 정보는 ATTDEF에서 가져와야 함 (필요시 별도 처리)
prompt = None
return AttributeInfo(
tag=tag,
text=text,
position=position,
height=height,
width=width,
rotation=rotation,
layer=layer,
bounding_box=bounding_box,
prompt=prompt,
style=style,
invisible=invisible,
const=const,
verify=verify,
preset=preset,
align_point=align_point,
halign=halign,
valign=valign,
text_generation_flag=text_generation_flag,
oblique_angle=oblique_angle,
width_factor=width_factor,
color=color,
linetype=linetype,
lineweight=lineweight,
insert_x=position[0],
insert_y=position[1],
insert_z=position[2],
estimated_width=estimated_width,
entity_handle=entity_handle
)
except Exception as e:
self.logger.warning(f"속성 정보 추출 중 오류: {e}")
return None
def identify_title_block(self, block_refs: List[BlockInfo]) -> Optional[TitleBlockInfo]:
"""블록 참조들 중에서 도곽을 식별하고 정보 추출"""
title_block_candidates = []
for block_ref in block_refs:
# 도곽 키워드를 포함한 속성이 있는지 확인
keyword_matches = 0
for attr in block_ref.attributes:
for keyword_group in self.TITLE_BLOCK_KEYWORDS.keys():
if self._contains_keyword(attr.tag, keyword_group) or \
self._contains_keyword(attr.text, keyword_group):
keyword_matches += 1
break
# 충분한 키워드가 매칭되면 도곽 후보로 추가
if keyword_matches >= 2: # 최소 2개 이상의 키워드 매칭
title_block_candidates.append((block_ref, keyword_matches))
if not title_block_candidates:
self.logger.warning("도곽 블록을 찾을 수 없습니다.")
return None
# 가장 많은 키워드를 포함한 블록을 도곽으로 선택
title_block_candidates.sort(key=lambda x: x[1], reverse=True)
best_candidate = title_block_candidates[0][0]
self.logger.info(f"도곽 블록 발견: {best_candidate.name} (키워드 매칭: {title_block_candidates[0][1]})")
return self._extract_title_block_info(best_candidate)
def _contains_keyword(self, text: str, keyword_group: str) -> bool:
"""텍스트에 특정 키워드 그룹의 단어가 포함되어 있는지 확인"""
if not text:
return False
text_lower = text.lower()
keywords = self.TITLE_BLOCK_KEYWORDS.get(keyword_group, [])
return any(keyword.lower() in text_lower for keyword in keywords)
def _extract_title_block_info(self, block_ref: BlockInfo) -> TitleBlockInfo:
"""도곽 블록에서 상세 정보 추출 - 모든 attributes 정보 포함"""
# TitleBlockInfo 객체 생성
title_block = TitleBlockInfo(
block_name=block_ref.name,
all_attributes=block_ref.attributes.copy(), # 모든 attributes 정보 저장
block_position=block_ref.position,
block_scale=block_ref.scale,
block_rotation=block_ref.rotation,
block_layer=block_ref.layer
)
# 속성들을 분석하여 도곽 정보 매핑
for attr in block_ref.attributes:
tag_lower = attr.tag.lower()
text_value = attr.text.strip()
if not text_value:
continue
# 각 키워드 그룹별로 매칭 시도
if self._contains_keyword(attr.tag, '도면명') or self._contains_keyword(attr.text, '도면명'):
title_block.drawing_name = text_value
elif self._contains_keyword(attr.tag, '도면번호') or self._contains_keyword(attr.text, '도면번호'):
title_block.drawing_number = text_value
elif self._contains_keyword(attr.tag, '건설분야') or self._contains_keyword(attr.text, '건설분야'):
title_block.construction_field = text_value
elif self._contains_keyword(attr.tag, '건설단계') or self._contains_keyword(attr.text, '건설단계'):
title_block.construction_stage = text_value
elif self._contains_keyword(attr.tag, '축척') or self._contains_keyword(attr.text, '축척'):
title_block.scale = text_value
elif self._contains_keyword(attr.tag, '설계자') or self._contains_keyword(attr.text, '설계자'):
title_block.designer = text_value
elif self._contains_keyword(attr.tag, '프로젝트') or self._contains_keyword(attr.text, '프로젝트'):
title_block.project_name = text_value
elif self._contains_keyword(attr.tag, '날짜') or self._contains_keyword(attr.text, '날짜'):
title_block.date = text_value
elif self._contains_keyword(attr.tag, '리비전') or self._contains_keyword(attr.text, '리비전'):
title_block.revision = text_value
elif self._contains_keyword(attr.tag, '위치') or self._contains_keyword(attr.text, '위치'):
title_block.location = text_value
# 도곽 전체 바운딩 박스 계산
title_block.bounding_box = self._calculate_title_block_bounding_box(block_ref)
# 속성 개수 업데이트
title_block.attributes_count = len(title_block.all_attributes)
# 디버깅 로그 - 모든 attributes 정보 출력
self.logger.info(f"도곽 '{block_ref.name}'에서 {title_block.attributes_count}개의 속성 추출:")
for i, attr in enumerate(title_block.all_attributes):
self.logger.debug(f" [{i+1}] Tag: '{attr.tag}', Text: '{attr.text}', "
f"Position: ({attr.insert_x:.2f}, {attr.insert_y:.2f}, {attr.insert_z:.2f}), "
f"Height: {attr.height:.2f}, Prompt: '{attr.prompt or 'N/A'}'")
return title_block
def _calculate_title_block_bounding_box(self, block_ref: BlockInfo) -> Optional[BoundingBox]:
"""도곽의 전체 바운딩 박스 계산"""
try:
valid_boxes = [attr.bounding_box for attr in block_ref.attributes
if attr.bounding_box is not None]
if not valid_boxes:
self.logger.warning("유효한 바운딩 박스가 없습니다.")
return None
# 모든 바운딩 박스를 포함하는 최외곽 박스 계산
min_x = min(box.min_x for box in valid_boxes)
min_y = min(box.min_y for box in valid_boxes)
max_x = max(box.max_x for box in valid_boxes)
max_y = max(box.max_y for box in valid_boxes)
return BoundingBox(min_x=min_x, min_y=min_y, max_x=max_x, max_y=max_y)
except Exception as e:
self.logger.warning(f"도곽 바운딩 박스 계산 실패: {e}")
return None
def process_dxf_file(self, file_path: str) -> Dict[str, Any]:
"""DXF 파일 전체 처리"""
result = {
'success': False,
'error': None,
'file_path': file_path,
'title_block': None,
'block_references': [],
'summary': {}
}
try:
# 파일 유효성 검사
if not self.validate_dxf_file(file_path):
result['error'] = "유효하지 않은 DXF 파일입니다."
return result
# DXF 문서 로드
doc = self.load_dxf_document(file_path)
if not doc:
result['error'] = "DXF 문서를 로드할 수 없습니다."
return result
# Block Reference 추출
block_refs = self.extract_block_references(doc)
result['block_references'] = [asdict(block_ref) for block_ref in block_refs]
# 도곽 정보 추출
title_block = self.identify_title_block(block_refs)
if title_block:
result['title_block'] = asdict(title_block)
# 요약 정보
result['summary'] = {
'total_blocks': len(block_refs),
'title_block_found': title_block is not None,
'title_block_name': title_block.block_name if title_block else None,
'attributes_count': sum(len(br.attributes) for br in block_refs)
}
result['success'] = True
self.logger.info(f"DXF 파일 처리 완료: {file_path}")
except Exception as e:
self.logger.error(f"DXF 파일 처리 중 오류: {e}")
result['error'] = str(e)
return result
def save_analysis_result(self, result: Dict[str, Any], output_file: str) -> bool:
"""분석 결과를 JSON 파일로 저장"""
try:
os.makedirs(Config.RESULTS_FOLDER, exist_ok=True)
output_path = os.path.join(Config.RESULTS_FOLDER, output_file)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2, default=str)
self.logger.info(f"분석 결과 저장 완료: {output_path}")
return True
except Exception as e:
self.logger.error(f"분석 결과 저장 실패: {e}")
return False
def main():
"""테스트용 메인 함수"""
logging.basicConfig(level=logging.INFO)
if not EZDXF_AVAILABLE:
print("ezdxf 라이브러리가 설치되지 않았습니다.")
return
processor = DXFProcessor()
# 테스트 파일 경로 (실제 파일 경로로 변경 필요)
test_file = "test_drawing.dxf"
if os.path.exists(test_file):
result = processor.process_dxf_file(test_file)
if result['success']:
print("DXF 파일 처리 성공!")
print(f"블록 수: {result['summary']['total_blocks']}")
print(f"도곽 발견: {result['summary']['title_block_found']}")
if result['title_block']:
print("\n도곽 정보:")
title_block = result['title_block']
for key, value in title_block.items():
if value and key != 'bounding_box':
print(f" {key}: {value}")
else:
print(f"처리 실패: {result['error']}")
else:
print(f"테스트 파일을 찾을 수 없습니다: {test_file}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DXF 처리 모듈 테스트 스크립트
수정된 _extract_title_block_info 함수와 관련 기능들을 테스트
"""
import sys
import os
sys.path.append(os.path.dirname(__file__))
try:
from dxf_processor import DXFProcessor, AttributeInfo, TitleBlockInfo, BoundingBox
print("[SUCCESS] DXF 처리 모듈 import 성공")
# DXFProcessor 인스턴스 생성
processor = DXFProcessor()
print("[SUCCESS] DXFProcessor 인스턴스 생성 성공")
# AttributeInfo 데이터 클래스 테스트
test_attr = AttributeInfo(
tag="TEST_TAG",
text="테스트 텍스트",
position=(100.0, 200.0, 0.0),
height=5.0,
width=50.0,
rotation=0.0,
layer="0",
prompt="테스트 프롬프트",
style="Standard",
invisible=False,
const=False,
verify=False,
preset=False,
insert_x=100.0,
insert_y=200.0,
insert_z=0.0,
estimated_width=75.0,
entity_handle="ABC123"
)
print("[SUCCESS] AttributeInfo 데이터 클래스 테스트 성공")
print(f" Tag: {test_attr.tag}")
print(f" Text: {test_attr.text}")
print(f" Position: {test_attr.position}")
print(f" Prompt: {test_attr.prompt}")
print(f" Handle: {test_attr.entity_handle}")
# TitleBlockInfo 데이터 클래스 테스트
test_title_block = TitleBlockInfo(
block_name="TEST_TITLE_BLOCK",
drawing_name="테스트 도면",
drawing_number="TEST-001",
all_attributes=[test_attr]
)
print("[SUCCESS] TitleBlockInfo 데이터 클래스 테스트 성공")
print(f" Block Name: {test_title_block.block_name}")
print(f" Drawing Name: {test_title_block.drawing_name}")
print(f" Drawing Number: {test_title_block.drawing_number}")
print(f" Attributes Count: {test_title_block.attributes_count}")
# BoundingBox 테스트
test_bbox = BoundingBox(min_x=0.0, min_y=0.0, max_x=100.0, max_y=50.0)
print("[SUCCESS] BoundingBox 데이터 클래스 테스트 성공")
print(f" Width: {test_bbox.width}")
print(f" Height: {test_bbox.height}")
print(f" Center: {test_bbox.center}")
print("\n[COMPLETE] 모든 테스트 통과! DXF 속성 추출 기능 개선이 성공적으로 완료되었습니다.")
except ImportError as e:
print(f"[ERROR] Import 오류: {e}")
except Exception as e:
print(f"[ERROR] 테스트 실행 중 오류: {e}")
if __name__ == "__main__":
print("\nDXF 처리 모듈 테스트 완료")

114
back_src/test_imports.py Normal file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
간단한 임포트 테스트
DXF 지원 통합 후 모든 모듈이 정상적으로 임포트되는지 확인
"""
import sys
import os
# 현재 경로 추가
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def test_imports():
"""모든 주요 모듈 임포트 테스트"""
try:
print("🔍 모듈 임포트 테스트 시작...")
# 기본 라이브러리
import flet as ft
print("✅ Flet 임포트 성공")
# 프로젝트 모듈들
from config import Config
print("✅ Config 임포트 성공")
from pdf_processor import PDFProcessor
print("✅ PDFProcessor 임포트 성공")
from dxf_processor import DXFProcessor
print("✅ DXFProcessor 임포트 성공")
from gemini_analyzer import GeminiAnalyzer
print("✅ GeminiAnalyzer 임포트 성공")
from ui_components import UIComponents
print("✅ UIComponents 임포트 성공")
from utils import AnalysisResultSaver, DateTimeUtils
print("✅ Utils 임포트 성공")
# 메인 애플리케이션
from main import DocumentAnalyzerApp
print("✅ DocumentAnalyzerApp 임포트 성공")
print("\n🎉 모든 모듈 임포트 성공!")
print(f"📦 Flet 버전: {ft.__version__}")
# DXF 관련 라이브러리
try:
import ezdxf
print(f"📐 ezdxf 버전: {ezdxf.version}")
except ImportError:
print("⚠️ ezdxf 라이브러리가 설치되지 않았습니다")
try:
import numpy
print(f"🔢 numpy 버전: {numpy.__version__}")
except ImportError:
print("⚠️ numpy 라이브러리가 설치되지 않았습니다")
return True
except Exception as e:
print(f"❌ 임포트 오류: {e}")
return False
def test_basic_functionality():
"""기본 기능 테스트"""
try:
print("\n🔧 기본 기능 테스트 시작...")
# Config 테스트
config_errors = Config.validate_config()
if config_errors:
print(f"⚠️ 설정 오류: {config_errors}")
else:
print("✅ Config 검증 성공")
# PDF Processor 테스트
pdf_processor = PDFProcessor()
print("✅ PDFProcessor 인스턴스 생성 성공")
# DXF Processor 테스트
dxf_processor = DXFProcessor()
print("✅ DXFProcessor 인스턴스 생성 성공")
# DateTimeUtils 테스트
from utils import DateTimeUtils
timestamp = DateTimeUtils.get_timestamp()
print(f"✅ 현재 시간: {timestamp}")
print("\n🎉 기본 기능 테스트 성공!")
return True
except Exception as e:
print(f"❌ 기능 테스트 오류: {e}")
return False
if __name__ == "__main__":
print("=" * 60)
print("📋 PDF/DXF 분석기 통합 테스트")
print("=" * 60)
import_success = test_imports()
functionality_success = test_basic_functionality()
print("\n" + "=" * 60)
if import_success and functionality_success:
print("🎉 모든 테스트 통과! 애플리케이션 준비 완료")
print("💡 main.py를 실행하여 애플리케이션을 시작할 수 있습니다")
else:
print("❌ 일부 테스트 실패. 설정을 확인하세요")
print("=" * 60)

314
back_src/test_project.py Normal file
View File

@@ -0,0 +1,314 @@
"""
테스트 스크립트
프로젝트의 핵심 기능들을 테스트합니다.
"""
import sys
import os
import logging
from pathlib import Path
# 프로젝트 루트를 Python 경로에 추가
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def test_config():
"""설정 모듈 테스트"""
print("=" * 50)
print("설정 모듈 테스트")
print("=" * 50)
try:
from config import Config
print(f"✅ 앱 제목: {Config.APP_TITLE}")
print(f"✅ 앱 버전: {Config.APP_VERSION}")
print(f"✅ 업로드 폴더: {Config.UPLOAD_FOLDER}")
print(f"✅ 최대 파일 크기: {Config.MAX_FILE_SIZE_MB}MB")
print(f"✅ 허용 확장자: {Config.ALLOWED_EXTENSIONS}")
print(f"✅ Gemini 모델: {Config.GEMINI_MODEL}")
# 설정 검증
errors = Config.validate_config()
if errors:
print("❌ 설정 오류:")
for error in errors:
print(f" - {error}")
return False
else:
print("✅ 모든 설정이 올바릅니다.")
return True
except Exception as e:
print(f"❌ 설정 모듈 테스트 실패: {e}")
return False
def test_pdf_processor():
"""PDF 처리 모듈 테스트"""
print("\\n" + "=" * 50)
print("PDF 처리 모듈 테스트")
print("=" * 50)
try:
from pdf_processor import PDFProcessor
processor = PDFProcessor()
print("✅ PDF 처리기 초기화 성공")
# 테스트용 임시 PDF 생성 (실제로는 존재하지 않음)
test_pdf = "test_sample.pdf"
# 존재하지 않는 파일 테스트
result = processor.validate_pdf_file(test_pdf)
if not result:
print("✅ 존재하지 않는 파일 검증: 정상 동작")
else:
print("❌ 존재하지 않는 파일 검증: 비정상 동작")
# 확장자 검증 테스트
if not processor.validate_pdf_file("test.txt"):
print("✅ 잘못된 확장자 검증: 정상 동작")
else:
print("❌ 잘못된 확장자 검증: 비정상 동작")
# base64 변환 테스트 (PIL Image 사용)
try:
from PIL import Image
import io
# 작은 테스트 이미지 생성
test_image = Image.new('RGB', (100, 100), color='red')
base64_result = processor.image_to_base64(test_image)
if base64_result and len(base64_result) > 0:
print("✅ Base64 변환: 정상 동작")
else:
print("❌ Base64 변환: 실패")
except Exception as e:
print(f"❌ Base64 변환 테스트 실패: {e}")
print("✅ PDF 처리 모듈 기본 테스트 완료")
return True
except Exception as e:
print(f"❌ PDF 처리 모듈 테스트 실패: {e}")
return False
def test_gemini_analyzer():
"""Gemini 분석기 테스트"""
print("\\n" + "=" * 50)
print("Gemini 분석기 테스트")
print("=" * 50)
try:
from config import Config
# API 키 확인
if not Config.GEMINI_API_KEY:
print("❌ Gemini API 키가 설정되지 않았습니다.")
print(" .env 파일에 GEMINI_API_KEY를 설정하세요.")
return False
from gemini_analyzer import GeminiAnalyzer
analyzer = GeminiAnalyzer()
print("✅ Gemini 분석기 초기화 성공")
# API 연결 테스트
if analyzer.validate_api_connection():
print("✅ Gemini API 연결 테스트 성공")
else:
print("❌ Gemini API 연결 테스트 실패")
return False
# 모델 정보 확인
model_info = analyzer.get_model_info()
print(f"✅ 사용 모델: {model_info['model']}")
print(f"✅ API 키 길이: {model_info['api_key_length']}")
return True
except Exception as e:
print(f"❌ Gemini 분석기 테스트 실패: {e}")
return False
def test_utils():
"""유틸리티 모듈 테스트"""
print("\\n" + "=" * 50)
print("유틸리티 모듈 테스트")
print("=" * 50)
try:
from utils import FileUtils, DateTimeUtils, TextUtils, ValidationUtils
# 파일 유틸리티 테스트
safe_name = FileUtils.get_safe_filename("test<file>name?.pdf")
print(f"✅ 안전한 파일명 생성: '{safe_name}'")
# 날짜/시간 유틸리티 테스트
timestamp = DateTimeUtils.get_timestamp()
filename_timestamp = DateTimeUtils.get_filename_timestamp()
print(f"✅ 타임스탬프: {timestamp}")
print(f"✅ 파일명 타임스탬프: {filename_timestamp}")
# 텍스트 유틸리티 테스트
long_text = "이것은 긴 텍스트입니다. " * 10
truncated = TextUtils.truncate_text(long_text, 50)
print(f"✅ 텍스트 축약: '{truncated}'")
# 검증 유틸리티 테스트
is_valid_pdf = ValidationUtils.is_valid_pdf_extension("test.pdf")
is_invalid_pdf = ValidationUtils.is_valid_pdf_extension("test.txt")
print(f"✅ PDF 확장자 검증: {is_valid_pdf} / {not is_invalid_pdf}")
return True
except Exception as e:
print(f"❌ 유틸리티 모듈 테스트 실패: {e}")
return False
def test_file_structure():
"""파일 구조 테스트"""
print("\\n" + "=" * 50)
print("파일 구조 테스트")
print("=" * 50)
required_files = [
"main.py",
"config.py",
"pdf_processor.py",
"gemini_analyzer.py",
"ui_components.py",
"utils.py",
"requirements.txt",
".env.example",
"README.md",
"project_plan.md"
]
required_dirs = [
"uploads",
"assets",
"docs"
]
missing_files = []
missing_dirs = []
# 파일 확인
for file in required_files:
if not (project_root / file).exists():
missing_files.append(file)
else:
print(f"{file}")
# 디렉토리 확인
for dir_name in required_dirs:
if not (project_root / dir_name).exists():
missing_dirs.append(dir_name)
else:
print(f"{dir_name}/")
if missing_files:
print("❌ 누락된 파일:")
for file in missing_files:
print(f" - {file}")
if missing_dirs:
print("❌ 누락된 디렉토리:")
for dir_name in missing_dirs:
print(f" - {dir_name}/")
return len(missing_files) == 0 and len(missing_dirs) == 0
def test_dependencies():
"""의존성 테스트"""
print("\\n" + "=" * 50)
print("의존성 테스트")
print("=" * 50)
required_packages = [
"flet",
"google.genai",
"fitz", # PyMuPDF
"PIL", # Pillow
"dotenv" # python-dotenv
]
missing_packages = []
for package in required_packages:
try:
if package == "fitz":
import fitz
elif package == "PIL":
import PIL
elif package == "dotenv":
import dotenv
elif package == "google.genai":
import google.genai
else:
__import__(package)
print(f"{package}")
except ImportError:
missing_packages.append(package)
print(f"{package} - 설치되지 않음")
if missing_packages:
print("\\n설치가 필요한 패키지:")
print("pip install " + " ".join(missing_packages))
return False
else:
print("\\n✅ 모든 의존성이 설치되어 있습니다.")
return True
def main():
"""메인 테스트 함수"""
print("PDF 도면 분석기 - 테스트 스크립트")
print("=" * 80)
tests = [
("파일 구조", test_file_structure),
("의존성", test_dependencies),
("설정 모듈", test_config),
("PDF 처리 모듈", test_pdf_processor),
("유틸리티 모듈", test_utils),
("Gemini 분석기", test_gemini_analyzer),
]
passed = 0
total = len(tests)
for test_name, test_func in tests:
try:
if test_func():
passed += 1
print(f"\\n✅ {test_name} 테스트 통과")
else:
print(f"\\n❌ {test_name} 테스트 실패")
except Exception as e:
print(f"\\n❌ {test_name} 테스트 오류: {e}")
print("\\n" + "=" * 80)
print(f"테스트 결과: {passed}/{total} 통과")
if passed == total:
print("🎉 모든 테스트가 통과했습니다!")
print("애플리케이션 실행 준비가 완료되었습니다.")
print("\\n실행 방법:")
print("python main.py")
else:
print("⚠️ 일부 테스트가 실패했습니다.")
print("실패한 테스트를 확인하고 문제를 해결하세요.")
print("=" * 80)
if __name__ == "__main__":
main()

5
back_src/test_run.py Normal file
View File

@@ -0,0 +1,5 @@
try:
from dxf_processor import DXFProcessor
print("Successfully imported DXFProcessor")
except Exception as e:
print(e)