Files
fletimageanalysis/main.py
2025-07-16 17:33:20 +09:00

1201 lines
46 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
PDF/DXF 도면 분석기 - 통합 애플리케이션 (탭 기반 인터페이스)
단일 파일 처리와 다중 파일 배치 처리를 탭으로 분리
Tab 1: 단일 파일 분석 (기존 기능)
Tab 2: 다중 파일 배치 처리 (새로운 기능)
Author: Claude Assistant
Updated: 2025-07-14
Version: 2.0.0
"""
import flet as ft
import logging
import threading
import base64
from typing import Optional
import time
# 프로젝트 모듈 임포트
from config import Config
from pdf_processor import PDFProcessor
from dxf_processor_fixed import FixedDXFProcessor as DXFProcessor
from comprehensive_text_extractor import ComprehensiveTextExtractor
from gemini_analyzer import GeminiAnalyzer
from ui_components import UIComponents
from utils import AnalysisResultSaver, DateTimeUtils
from csv_exporter import TitleBlockCSVExporter
from multi_file_main import MultiFileApp
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class SingleFileAnalyzerApp:
"""단일 파일 분석기 애플리케이션 클래스 (기존 기능)"""
def __init__(self, page: ft.Page):
self.page = page
self.pdf_processor = PDFProcessor()
self.dxf_processor = DXFProcessor()
self.text_extractor = ComprehensiveTextExtractor()
self.csv_exporter = TitleBlockCSVExporter()
self.gemini_analyzer = None
self.current_file_path = None
self.current_file_type = None
self.current_pdf_info = None
self.current_title_block_info = None
self.current_text_extraction_result = None
self.analysis_results = {}
self.result_saver = AnalysisResultSaver("results")
self.analysis_start_time = None
self.current_page_index = 0
# 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_text_button = None
self.save_json_button = None
self.save_csv_button = None
self.title_block_table = None
self.comprehensive_text_display = None
self.organization_selector = None
self.page_selector = None
self.analysis_mode = None
self.custom_prompt = None
self.pdf_viewer_dialog = None
self.pdf_preview_button = None
# 초기화
self.init_gemini_analyzer()
def init_gemini_analyzer(self):
"""Gemini 분석기 초기화"""
try:
config_errors = Config.validate_config()
if config_errors:
logger.error(f"설정 오류: {config_errors}")
return
self.gemini_analyzer = GeminiAnalyzer()
logger.info("Gemini 분석기 초기화 완료")
except Exception as e:
logger.error(f"Gemini 분석기 초기화 실패: {e}")
def build_ui(self) -> ft.Column:
"""단일 파일 분석 UI 구성 (기존 좌우 분할 레이아웃)"""
# 좌측 컨트롤 패널 (4/12 columns)
left_panel = self.create_left_control_panel()
# 우측 결과 패널 (8/12 columns)
right_panel = self.create_right_results_panel()
# ResponsiveRow를 사용한 좌우 분할 레이아웃
main_layout = ft.ResponsiveRow([
ft.Container(
content=left_panel,
col={"sm": 12, "md": 5, "lg": 4},
padding=10,
),
ft.Container(
content=right_panel,
col={"sm": 12, "md": 7, "lg": 8},
padding=10,
),
])
# PDF 뷰어 다이얼로그 초기화
self.init_pdf_viewer_dialog()
return ft.Column([
main_layout
], expand=True, scroll=ft.ScrollMode.AUTO)
def create_left_control_panel(self) -> ft.Column:
"""좌측 컨트롤 패널 생성"""
# 파일 업로드 섹션
upload_section = self.create_file_upload_section()
# 분석 설정 섹션
settings_section = self.create_analysis_settings_section()
# 진행률 섹션
progress_section = self.create_progress_section()
# 분석 시작 버튼 (크게)
start_analysis_button = ft.Container(
content=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,
),
width=300,
height=50,
),
alignment=ft.alignment.center,
margin=ft.margin.symmetric(vertical=10),
)
self.upload_button = start_analysis_button.content
# PDF 미리보기 버튼
preview_button = ft.Container(
content=ft.ElevatedButton(
text="📄 PDF 미리보기",
icon=ft.Icons.VISIBILITY,
on_click=self.on_pdf_preview_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.BLUE_100,
color=ft.Colors.BLUE_800,
),
width=300,
height=40,
),
alignment=ft.alignment.center,
margin=ft.margin.symmetric(vertical=5),
)
self.pdf_preview_button = preview_button.content
return ft.Column([
upload_section,
ft.Divider(height=20),
settings_section,
ft.Divider(height=20),
progress_section,
ft.Divider(height=20),
start_analysis_button,
preview_button,
], expand=True, scroll=ft.ScrollMode.AUTO)
def create_right_results_panel(self) -> ft.Column:
"""우측 결과 패널 생성"""
# 결과 텍스트
self.results_text = ft.Text(
"분석 결과가 여기에 표시됩니다.\n\n좌측에서 PDF/DXF 파일을 선택하고 분석을 시작하세요.",
size=14,
selectable=True,
)
# 결과 컨테이너
self.results_container = ft.Container(
content=ft.Column([
self.results_text,
], scroll=ft.ScrollMode.AUTO),
padding=20,
bgcolor=ft.Colors.GREY_50,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
expand=True,
)
# 저장 버튼들
self.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,
)
)
self.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,
)
)
# CSV 저장 버튼 (DXF 전용)
self.save_csv_button = ft.ElevatedButton(
text="📊 CSV 저장",
icon=ft.Icons.TABLE_CHART,
disabled=True,
visible=False,
on_click=self.on_save_csv_click,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
# 헤더와 버튼들
header_row = ft.Row([
ft.Text(
"📋 분석 결과",
size=20,
weight=ft.FontWeight.BOLD,
color=ft.Colors.GREEN_800
),
ft.Row([
self.save_text_button,
self.save_json_button,
self.save_csv_button,
]),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN)
return ft.Column([
ft.Container(
content=ft.Column([
header_row,
ft.Divider(),
self.results_container,
]),
padding=20,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
expand=True,
)
], expand=True)
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=12,
color=ft.Colors.GREY_600
)
# 파일 선택 버튼
select_button = ft.ElevatedButton(
text="📁 PDF/DXF 파일 선택",
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,
),
width=280,
)
return ft.Container(
content=ft.Column([
ft.Text(
"📄 PDF/DXF 파일 업로드",
size=16,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
),
ft.Divider(),
select_button,
self.selected_file_text,
]),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
def create_analysis_settings_section(self) -> ft.Container:
"""분석 설정 섹션 생성"""
# 조직 선택
self.organization_selector = ft.Dropdown(
label="분석 스키마",
options=[
ft.dropdown.Option("국토교통부", "국토교통부 - 일반 건설/토목 도면"),
ft.dropdown.Option("한국도로공사", "한국도로공사 - 고속도로 전용 도면"),
],
value="국토교통부",
width=280,
on_change=self.on_organization_change,
)
# 페이지 선택
self.page_selector = ft.Dropdown(
label="분석할 페이지",
options=[
ft.dropdown.Option("첫 번째 페이지"),
ft.dropdown.Option("모든 페이지"),
],
value="첫 번째 페이지",
width=280,
)
# 분석 모드
self.analysis_mode = ft.Dropdown(
label="분석 모드",
options=[
ft.dropdown.Option("basic", "기본 분석"),
ft.dropdown.Option("detailed", "상세 분석"),
ft.dropdown.Option("custom", "사용자 정의"),
],
value="basic",
width=280,
on_change=self.on_analysis_mode_change,
)
# 사용자 정의 프롬프트
self.custom_prompt = ft.TextField(
label="사용자 정의 프롬프트",
multiline=True,
min_lines=3,
max_lines=5,
width=280,
visible=False,
)
return ft.Container(
content=ft.Column([
ft.Text(
"⚙️ 분석 설정",
size=16,
weight=ft.FontWeight.BOLD,
color=ft.Colors.PURPLE_800
),
ft.Divider(),
self.organization_selector,
self.page_selector,
self.analysis_mode,
self.custom_prompt,
]),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
def create_progress_section(self) -> ft.Container:
"""진행률 섹션 생성"""
# 진행률 바
self.progress_bar = ft.ProgressBar(
width=280,
color=ft.Colors.BLUE_600,
bgcolor=ft.Colors.GREY_300,
visible=False,
)
# 상태 텍스트
self.status_text = ft.Text(
"대기 중...",
size=12,
color=ft.Colors.GREY_600
)
# 진행률 링
self.progress_ring = ft.ProgressRing(
width=30,
height=30,
stroke_width=3,
visible=False,
)
return ft.Container(
content=ft.Column([
ft.Text(
"📊 분석 진행 상황",
size=16,
weight=ft.FontWeight.BOLD,
color=ft.Colors.ORANGE_800
),
ft.Divider(),
ft.Row([
self.progress_ring,
ft.Column([
self.status_text,
self.progress_bar,
], expand=1),
], alignment=ft.MainAxisAlignment.START),
]),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
def init_pdf_viewer_dialog(self):
"""PDF 뷰어 다이얼로그 초기화"""
# PDF 이미지 컨테이너
self.pdf_image_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=600,
height=700,
bgcolor=ft.Colors.GREY_100,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
alignment=ft.alignment.center,
)
# 페이지 네비게이션
self.prev_page_button = ft.IconButton(
icon=ft.Icons.ARROW_BACK,
disabled=True,
on_click=self.on_prev_page_click,
)
self.page_info_text = ft.Text("1 / 1", size=14)
self.next_page_button = ft.IconButton(
icon=ft.Icons.ARROW_FORWARD,
disabled=True,
on_click=self.on_next_page_click,
)
page_nav = ft.Row([
self.prev_page_button,
self.page_info_text,
self.next_page_button,
], alignment=ft.MainAxisAlignment.CENTER)
# PDF 뷰어 다이얼로그
self.pdf_viewer_dialog = ft.AlertDialog(
modal=True,
title=ft.Text("PDF 미리보기"),
content=ft.Column([
self.pdf_image_container,
page_nav,
], height=750, width=650),
actions=[
ft.TextButton("닫기", on_click=self.close_pdf_viewer)
],
actions_alignment=ft.MainAxisAlignment.END,
)
# 기존 이벤트 핸들러들 (기존 main.py에서 복사)
# ... (이벤트 핸들러 코드들을 여기에 복사)
def on_select_file_click(self, e):
"""파일 선택 버튼 클릭 핸들러"""
self.file_picker.pick_files(
allowed_extensions=["pdf", "dxf"],
allow_multiple=False
)
def on_file_selected(self, e: ft.FilePickerResultEvent):
"""파일 선택 결과 핸들러"""
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):
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):
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
self.page_info_text.value = "DXF 파일"
self.current_page_index = 0
self.current_pdf_info = None
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
self.current_title_block_info = None
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_pdf_preview_click(self, e):
"""PDF 미리보기 버튼 클릭 핸들러"""
if self.current_file_path and self.current_file_type == 'pdf':
self.load_pdf_preview()
self.page.dialog = self.pdf_viewer_dialog
self.pdf_viewer_dialog.open = True
self.page.update()
def load_pdf_preview(self):
"""PDF 미리보기 로드"""
try:
image_data = self.pdf_processor.pdf_page_to_image_bytes(
self.current_file_path,
self.current_page_index
)
if image_data:
base64_data = base64.b64encode(image_data).decode()
self.pdf_image_container.content = ft.Image(
src_base64=base64_data,
width=600,
height=700,
fit=ft.ImageFit.CONTAIN,
)
self.prev_page_button.disabled = self.current_page_index == 0
self.next_page_button.disabled = self.current_page_index >= self.current_pdf_info['page_count'] - 1
self.page_info_text.value = f"{self.current_page_index + 1} / {self.current_pdf_info['page_count']}"
else:
self.pdf_image_container.content = ft.Text(
"PDF 페이지 로드 실패",
color=ft.Colors.RED_600
)
except Exception as e:
logger.error(f"PDF 미리보기 로드 오류: {e}")
self.pdf_image_container.content = ft.Text(
f"미리보기 오류: {str(e)}",
color=ft.Colors.RED_600
)
def on_prev_page_click(self, e):
"""이전 페이지 버튼 클릭"""
if self.current_page_index > 0:
self.current_page_index -= 1
self.load_pdf_preview()
self.page.update()
def on_next_page_click(self, e):
"""다음 페이지 버튼 클릭"""
if self.current_page_index < self.current_pdf_info['page_count'] - 1:
self.current_page_index += 1
self.load_pdf_preview()
self.page.update()
def close_pdf_viewer(self, e):
"""PDF 뷰어 닫기"""
self.pdf_viewer_dialog.open = False
self.page.update()
def on_analysis_start_click(self, e):
"""분석 시작 버튼 클릭 핸들러"""
if not self.current_file_path or not self.current_file_type:
return
if self.current_file_type == 'pdf' and 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 on_save_csv_click(self, e):
"""CSV 저장 버튼 클릭 핸들러"""
if not self.current_title_block_info:
self.show_error_dialog("저장 오류", "저장할 타이틀블럭 속성 정보가 없습니다.")
return
try:
import os
filename = f"title_block_attributes_{os.path.basename(self.current_file_path).replace('.dxf', '')}"
saved_path = self.csv_exporter.export_title_block_attributes(
self.current_title_block_info,
filename
)
if saved_path:
self.show_info_dialog(
"CSV 저장 완료",
f"타이틀블럭 속성 정보가 CSV 파일로 저장되었습니다:\n\n{saved_path}"
)
else:
self.show_error_dialog("저장 실패", "CSV 파일 저장 중 오류가 발생했습니다.")
except Exception as e:
logger.error(f"CSV 저장 중 오류: {e}")
self.show_error_dialog("저장 오류", f"CSV 저장 중 오류가 발생했습니다:\n{str(e)}")
def _save_results(self, format_type: str):
"""결과 저장 공통 함수"""
if not self.analysis_results:
self.show_error_dialog("저장 오류", "저장할 분석 결과가 없습니다.")
return
try:
analysis_settings = {
"조직_유형": self.organization_selector.value,
"페이지_선택": 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'] if self.current_pdf_info else "dxf_file",
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":
saved_path = self.result_saver.save_analysis_json(
pdf_filename=self.current_pdf_info['filename'] if self.current_pdf_info else "dxf_file",
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()
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 = "expressway" if self.organization_selector.value == "한국도로공사" else "transportation"
logger.info(f"선택된 조직 유형: {organization_type}")
pages_to_analyze = list(range(self.current_pdf_info['page_count'])) if self.page_selector.value == "모든 페이지" else [0]
if self.analysis_mode.value == "custom":
prompt = self.custom_prompt.value or Config.DEFAULT_PROMPT
else:
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}/{total_pages} 처리 중...", progress)
# 텍스트와 좌표 추출
self.update_progress_ui(True, f"페이지 {page_num + 1}: 텍스트 추출 중...", progress)
text_blocks = self.pdf_processor.extract_text_with_coordinates(self.current_file_path, page_num)
if not text_blocks:
logger.warning(f"페이지 {page_num + 1}에서 텍스트를 추출하지 못했습니다.")
# 이미지를 Base64로 변환
self.update_progress_ui(True, f"페이지 {page_num + 1}: 이미지 변환 중...", progress)
base64_data = self.pdf_processor.pdf_page_to_base64(self.current_file_path, page_num)
if base64_data:
# Gemini API로 분석
self.update_progress_ui(True, f"페이지 {page_num + 1}: AI 분석 중...", progress)
result = self.gemini_analyzer.analyze_pdf_page(
base64_data=base64_data,
text_blocks=text_blocks,
prompt=prompt,
organization_type=organization_type
)
self.analysis_results[page_num] = result or f"페이지 {page_num + 1} 분석 실패"
else:
self.analysis_results[page_num] = f"페이지 {page_num + 1} 이미지 변환 실패"
self.display_analysis_results()
duration_str = DateTimeUtils.format_duration(time.time() - self.analysis_start_time)
self.update_progress_ui(False, f"✅ PDF 분석 완료! (소요시간: {duration_str})", 1.0)
def _run_dxf_analysis(self):
"""DXF 파일 분석 실행"""
self.update_progress_ui(True, "DXF 파일 분석 중...")
try:
result = self.dxf_processor.process_dxf_file_comprehensive(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 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()
self.page.run_thread(update)
def display_analysis_results(self):
"""분석 결과 표시"""
def update_results():
if not self.analysis_results:
self.results_text.value = "❌ 분석 결과가 없습니다."
self.save_text_button.disabled = True
self.save_json_button.disabled = True
self.save_csv_button.visible = False
self.page.update()
return
import json
result_text = f"🎯 분석 요약 (총 {len(self.analysis_results)}페이지)\n"
result_text += f"⏰ 완료 시간: {DateTimeUtils.get_timestamp()}\n"
result_text += f"🏢 조직 스키마: {self.organization_selector.value}\n"
result_text += "=" * 60 + "\n\n"
for page_num, result_json in self.analysis_results.items():
result_text += f"📋 페이지 {page_num + 1} 분석 결과\n"
result_text += "-" * 40 + "\n"
try:
data = json.loads(result_json)
for key, item in data.items():
if isinstance(item, dict) and 'value' in item:
val = item.get('value', 'N/A')
x = item.get('x', -1)
y = item.get('y', -1)
result_text += f"- {key}: {val} (좌표: {x:.0f}, {y:.0f})\n"
else:
result_text += f"- {key}: {item}\n"
except (json.JSONDecodeError, TypeError):
result_text += str(result_json)
result_text += "\n" + "=" * 60 + "\n\n"
self.results_text.value = result_text.strip()
self.save_text_button.disabled = False
self.save_json_button.disabled = False
self.save_csv_button.visible = False
self.page.update()
self.page.run_thread(update_results)
def display_dxf_analysis_results(self, dxf_result):
"""DXF 분석 결과 표시"""
def update_results():
if dxf_result and dxf_result['success']:
self.current_title_block_info = dxf_result.get('title_block')
import os
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 += "\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"
if title_block.get('all_attributes'):
result_text += "\n\n📊 타이틀블럭 속성 상세 정보\n"
result_text += "-" * 60 + "\n"
table_data = self.csv_exporter.create_attribute_table_data(title_block)
if table_data:
result_text += f"{'No.':<4} {'Tag':<15} {'Text':<25} {'Prompt':<20} {'X':<8} {'Y':<8} {'Layer':<8}\n"
result_text += "-" * 100 + "\n"
for i, row in enumerate(table_data[:10]):
result_text += f"{row['No.']:<4} {row['Tag'][:14]:<15} {row['Text'][:24]:<25} "
result_text += f"{row['Prompt'][:19]:<20} {row['X']:<8} {row['Y']:<8} {row['Layer'][:7]:<8}\n"
if len(table_data) > 10:
result_text += f"... 외 {len(table_data) - 10}개 속성\n"
result_text += f"\n💡 전체 {len(table_data)}개 속성을 CSV 파일로 저장할 수 있습니다.\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]):
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
if self.current_title_block_info and self.current_title_block_info.get('all_attributes'):
self.save_csv_button.visible = True
self.save_csv_button.disabled = False
else:
self.save_csv_button.visible = False
self.save_csv_button.disabled = True
else:
self.results_text.value = "❌ DXF 분석 결과가 없습니다."
self.save_text_button.disabled = True
self.save_json_button.disabled = True
self.save_csv_button.visible = False
self.save_csv_button.disabled = True
self.current_title_block_info = None
self.page.update()
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()
class TabbedDocumentAnalyzerApp:
"""탭 기반 통합 문서 분석기 애플리케이션"""
def __init__(self, page: ft.Page):
self.page = page
self.setup_page()
# 앱 인스턴스
self.single_file_app = None
self.multi_file_app = None
def setup_page(self):
"""페이지 기본 설정"""
self.page.title = "PDF/DXF 도면 분석기 v2.0"
self.page.theme_mode = ft.ThemeMode.LIGHT
self.page.padding = 0
self.page.bgcolor = ft.Colors.GREY_100
# 윈도우 크기 설정
self.page.window.width = 1400
self.page.window.height = 1000
self.page.window.min_width = 1200
self.page.window.min_height = 800
logger.info("탭 기반 애플리케이션 페이지 설정 완료")
def build_ui(self):
"""탭 기반 UI 구성"""
# 앱바
app_bar = ft.AppBar(
title=ft.Text(
"📄 PDF/DXF 도면 분석기 v2.0",
size=20,
weight=ft.FontWeight.BOLD
),
center_title=True,
bgcolor=ft.Colors.BLUE_600,
color=ft.Colors.WHITE,
automatically_imply_leading=False,
)
self.page.appbar = app_bar
# 탭 생성
tabs = ft.Tabs(
selected_index=0,
animation_duration=300,
divider_color=ft.Colors.BLUE_200,
indicator_color=ft.Colors.BLUE_600,
label_color=ft.Colors.BLUE_800,
unselected_label_color=ft.Colors.GREY_600,
# overlay_color 제거 - Flet 버전 호환성 개선
on_change=self.on_tab_change,
expand=True,
tabs=[
ft.Tab(
icon=ft.Icons.DESCRIPTION,
text="단일 파일 분석",
content=self.create_single_file_tab()
),
ft.Tab(
icon=ft.Icons.BATCH_PREDICTION,
text="다중 파일 배치 처리",
content=self.create_multi_file_tab()
),
],
)
# 메인 컨테이너
main_container = ft.Container(
content=tabs,
expand=True,
padding=5,
)
# 페이지에 추가
self.page.add(main_container)
logger.info("탭 기반 UI 구성 완료")
def create_single_file_tab(self) -> ft.Column:
"""단일 파일 분석 탭 생성"""
# 단일 파일 앱 인스턴스 생성
self.single_file_app = SingleFileAnalyzerApp(self.page)
return self.single_file_app.build_ui()
def create_multi_file_tab(self) -> ft.Column:
"""다중 파일 배치 처리 탭 생성"""
# 다중 파일 앱 인스턴스 생성
self.multi_file_app = MultiFileApp(self.page)
return self.multi_file_app.build_ui()
def on_tab_change(self, e):
"""탭 변경 이벤트 핸들러"""
selected_index = e.control.selected_index
if selected_index == 0:
logger.info("단일 파일 분석 탭 선택")
elif selected_index == 1:
logger.info("다중 파일 배치 처리 탭 선택")
self.page.update()
def main(page: ft.Page):
"""메인 함수"""
try:
# 탭 기반 애플리케이션 초기화
app = TabbedDocumentAnalyzerApp(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",
)