""" 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", )