first commit
This commit is contained in:
109
back_src/SIMPLE_BATCH_GUIDE.md
Normal file
109
back_src/SIMPLE_BATCH_GUIDE.md
Normal 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의 장점을 그대로 유지하면서 배치 처리 기능만 추가했습니다.
|
||||
94
back_src/create_test_dxf.py
Normal file
94
back_src/create_test_dxf.py
Normal 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()
|
||||
313
back_src/dxf_support_methods.py
Normal file
313
back_src/dxf_support_methods.py
Normal 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)
|
||||
408
back_src/gemini_analyzer_backup.py
Normal file
408
back_src/gemini_analyzer_backup.py
Normal 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
723
back_src/main_old.py
Normal 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",
|
||||
)
|
||||
1161
back_src/main_single_file_backup.py
Normal file
1161
back_src/main_single_file_backup.py
Normal file
File diff suppressed because it is too large
Load Diff
56
back_src/run_simple_batch.py
Normal file
56
back_src/run_simple_batch.py
Normal 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 폴더에서 실행")
|
||||
429
back_src/simple_batch_analyzer_app.py
Normal file
429
back_src/simple_batch_analyzer_app.py
Normal 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)
|
||||
378
back_src/simple_batch_processor.py
Normal file
378
back_src/simple_batch_processor.py
Normal 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())
|
||||
235
back_src/simple_gemini_analyzer.py
Normal file
235
back_src/simple_gemini_analyzer.py
Normal 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 연결 실패")
|
||||
633
back_src/temp_backup/dxf_processor_backup.py
Normal file
633
back_src/temp_backup/dxf_processor_backup.py
Normal 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()
|
||||
76
back_src/test_dxf_processor.py
Normal file
76
back_src/test_dxf_processor.py
Normal 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
114
back_src/test_imports.py
Normal 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
314
back_src/test_project.py
Normal 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
5
back_src/test_run.py
Normal file
@@ -0,0 +1,5 @@
|
||||
try:
|
||||
from dxf_processor import DXFProcessor
|
||||
print("Successfully imported DXFProcessor")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
Reference in New Issue
Block a user