""" 다중 파일 처리 애플리케이션 클래스 여러 PDF/DXF 파일을 배치로 처리하고 결과를 CSV로 저장하는 기능을 제공합니다. Author: Claude Assistant Created: 2025-07-14 Version: 1.0.0 """ import flet as ft import logging import os import time # 프로젝트 모듈 임포트 from config import Config from multi_file_processor import MultiFileProcessor, BatchProcessingConfig, generate_default_csv_filename from ui_components import MultiFileUIComponents # 로깅 설정 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class MultiFileApp: """다중 파일 처리 애플리케이션 클래스""" def __init__(self, page: ft.Page): self.page = page self.selected_files = [] self.processing_results = [] self.is_processing = False self.is_cancelled = False self.is_paused = False self.processor = None # UI 컴포넌트 참조 self.file_picker = None self.files_container = None self.clear_files_button = None self.batch_analysis_button = None # 배치 설정 컴포넌트 self.organization_selector = None self.concurrent_files_slider = None self.enable_batch_mode = None self.save_intermediate_results = None self.include_error_files = None self.csv_output_path = None self.browse_button = None # 진행률 컴포넌트 self.overall_progress_bar = None self.progress_text = None self.current_status_text = None self.timing_info = None self.log_container = None self.cancel_button = None self.pause_resume_button = None # 결과 컴포넌트 self.summary_stats = None self.results_table = None self.save_csv_button = None self.save_cross_csv_button = None # 새로 추가 self.save_excel_button = None self.clear_results_button = None # 시간 추적 self.start_time = None # Gemini API 키 확인 self.init_processor() def init_processor(self): """다중 파일 처리기 초기화""" try: config_errors = Config.validate_config() if config_errors: logger.error(f"설정 오류: {config_errors}") return # Gemini API 키가 있는지 확인 gemini_api_key = Config.get_gemini_api_key() if not gemini_api_key: logger.error("Gemini API 키가 설정되지 않았습니다") return self.processor = MultiFileProcessor(gemini_api_key) logger.info("다중 파일 처리기 초기화 완료") except Exception as e: logger.error(f"다중 파일 처리기 초기화 실패: {e}") def build_ui(self) -> ft.Column: """다중 파일 처리 UI 구성""" # 파일 업로드 섹션 upload_section = self.create_file_upload_section() # 배치 설정 섹션 settings_section = self.create_batch_settings_section() # 진행률 섹션 progress_section = self.create_progress_section() # 결과 섹션 results_section = self.create_results_section() # 좌측 컨트롤 패널 left_panel = ft.Container( content=ft.Column([ upload_section, ft.Divider(height=10), settings_section, ft.Divider(height=10), progress_section, ], scroll=ft.ScrollMode.AUTO), col={"sm": 12, "md": 5, "lg": 4}, padding=10, ) # 우측 결과 패널 right_panel = ft.Container( content=results_section, col={"sm": 12, "md": 7, "lg": 8}, padding=10, ) # ResponsiveRow를 사용한 좌우 분할 레이아웃 main_layout = ft.ResponsiveRow([left_panel, right_panel]) return ft.Column([ main_layout ], expand=True, scroll=ft.ScrollMode.AUTO) def create_file_upload_section(self) -> ft.Container: """파일 업로드 섹션 생성""" upload_container = MultiFileUIComponents.create_multi_file_upload_section( on_files_selected=self.on_files_selected, on_batch_analysis_click=self.on_batch_analysis_click, on_clear_files_click=self.on_clear_files_click ) # 참조 저장 self.file_picker = upload_container.content.controls[-1] # 마지막 요소가 file_picker self.files_container = upload_container.content.controls[4] # files_container self.clear_files_button = upload_container.content.controls[2].controls[1] self.batch_analysis_button = upload_container.content.controls[2].controls[2] # overlay에 추가 self.page.overlay.append(self.file_picker) return upload_container def create_batch_settings_section(self) -> ft.Container: """배치 설정 섹션 생성""" ( container, self.organization_selector, self.concurrent_files_slider, self.enable_batch_mode, self.save_intermediate_results, self.include_error_files, self.csv_output_path, self.browse_button ) = MultiFileUIComponents.create_batch_settings_section() # 슬라이더 변경 이벤트 처리 self.concurrent_files_slider.on_change = self.on_concurrent_files_change # 경로 선택 버튼 이벤트 처리 self.browse_button.on_click = self.on_browse_csv_path_click return container def create_progress_section(self) -> ft.Container: """진행률 섹션 생성""" ( container, self.overall_progress_bar, self.progress_text, self.current_status_text, self.timing_info, self.log_container, self.cancel_button, self.pause_resume_button ) = MultiFileUIComponents.create_batch_progress_section() # 버튼 이벤트 처리 self.cancel_button.on_click = self.on_cancel_click self.pause_resume_button.on_click = self.on_pause_resume_click return container def create_results_section(self) -> ft.Container: """결과 섹션 생성""" ( container, self.summary_stats, self.results_table, self.save_csv_button, self.save_cross_csv_button, # 새로 추가 self.save_excel_button, self.clear_results_button ) = MultiFileUIComponents.create_batch_results_section() # 버튼 이벤트 처리 self.save_csv_button.on_click = self.on_save_csv_click self.save_cross_csv_button.on_click = self.on_save_cross_csv_click # 새로 추가 self.save_excel_button.on_click = self.on_save_excel_click self.clear_results_button.on_click = self.on_clear_results_click return container # 이벤트 핸들러들 def on_files_selected(self, e: ft.FilePickerResultEvent): """파일 선택 이벤트 핸들러""" if e.files: # 선택된 파일들을 기존 목록에 추가 (중복 제거) existing_paths = {f.path for f in self.selected_files} new_files = [f for f in e.files if f.path not in existing_paths] if new_files: self.selected_files.extend(new_files) MultiFileUIComponents.update_selected_files_list( self.files_container, self.selected_files, self.clear_files_button, self.batch_analysis_button ) MultiFileUIComponents.add_log_message( self.log_container, f"{len(new_files)}개 파일 추가됨 (총 {len(self.selected_files)}개)", "info" ) else: MultiFileUIComponents.add_log_message( self.log_container, "선택된 파일들이 이미 목록에 있습니다", "warning" ) else: MultiFileUIComponents.add_log_message( self.log_container, "파일 선택이 취소되었습니다", "info" ) self.page.update() def on_clear_files_click(self, e): """파일 목록 지우기 이벤트 핸들러""" file_count = len(self.selected_files) self.selected_files.clear() MultiFileUIComponents.update_selected_files_list( self.files_container, self.selected_files, self.clear_files_button, self.batch_analysis_button ) MultiFileUIComponents.add_log_message( self.log_container, f"{file_count}개 파일 목록 초기화", "info" ) self.page.update() def on_batch_analysis_click(self, e): """배치 분석 시작 이벤트 핸들러""" if not self.selected_files: return if self.is_processing: return if not self.processor: self.show_error_dialog("처리기 오류", "다중 파일 처리기가 초기화되지 않았습니다.") return # 비동기 처리 시작 self.page.run_task(self.start_batch_processing) def on_concurrent_files_change(self, e): """동시 처리 수 변경 이벤트 핸들러""" value = int(e.control.value) # 슬라이더 레이블 업데이트 # 상위 Container에서 텍스트 찾아서 업데이트 try: settings_container = e.control.parent.parent # Column -> Container text_control = settings_container.controls[0] text_control.value = f"동시 처리 수: {value}개" self.page.update() except e: # noqa: E722 logger.warning("슬라이더 레이블 업데이트 실패") def on_browse_csv_path_click(self, e): """CSV 저장 경로 선택 이벤트 핸들러""" # 간단한 구현 - 현재 디렉토리 + 자동 생성 파일명 설정 default_filename = generate_default_csv_filename() default_path = os.path.join(os.getcwd(), "results", default_filename) self.csv_output_path.value = default_path self.page.update() MultiFileUIComponents.add_log_message( self.log_container, f"CSV 저장 경로 설정: {default_filename}", "info" ) def on_cancel_click(self, e): """처리 취소 이벤트 핸들러""" if self.is_processing: self.is_cancelled = True MultiFileUIComponents.add_log_message( self.log_container, "사용자가 처리를 취소했습니다", "warning" ) self.page.update() def on_pause_resume_click(self, e): """일시정지/재개 이벤트 핸들러""" # 현재 구현에서는 단순한 토글 기능만 제공 if self.is_processing: self.is_paused = not self.is_paused if self.is_paused: self.pause_resume_button.text = "재개" self.pause_resume_button.icon = ft.Icons.PLAY_ARROW MultiFileUIComponents.add_log_message( self.log_container, "처리가 일시정지되었습니다", "warning" ) else: self.pause_resume_button.text = "일시정지" self.pause_resume_button.icon = ft.Icons.PAUSE MultiFileUIComponents.add_log_message( self.log_container, "처리가 재개되었습니다", "success" ) self.page.update() def on_save_csv_click(self, e): """CSV 저장 이벤트 핸들러""" if not self.processing_results: self.show_error_dialog("저장 오류", "저장할 결과가 없습니다.") return self.page.run_task(self.save_results_to_csv) def on_save_cross_csv_click(self, e): """Cross-Tabulated CSV 저장 이벤트 핸들러 (새로 추가)""" if not self.processing_results: self.show_error_dialog("저장 오류", "저장할 결과가 없습니다.") return self.page.run_task(self.save_cross_tabulated_csv) def on_save_excel_click(self, e): """Excel 저장 이벤트 핸들러""" if not self.processing_results: self.show_error_dialog("저장 오류", "저장할 결과가 없습니다.") return # Excel 저장 기능 (향후 구현) self.show_info_dialog("개발 중", "Excel 저장 기능은 곧 추가될 예정입니다.") def on_clear_results_click(self, e): """결과 초기화 이벤트 핸들러""" self.processing_results.clear() MultiFileUIComponents.update_batch_results( self.summary_stats, self.results_table, self.processing_results, self.save_csv_button, self.save_cross_csv_button, # 새로 추가 self.save_excel_button, self.clear_results_button ) MultiFileUIComponents.add_log_message( self.log_container, "결과가 초기화되었습니다", "info" ) self.page.update() # 비동기 처리 함수들 async def start_batch_processing(self): """배치 처리 시작""" try: self.is_processing = True self.is_cancelled = False self.is_paused = False self.start_time = time.time() # UI 상태 업데이트 self.batch_analysis_button.disabled = True self.cancel_button.disabled = False self.pause_resume_button.disabled = False # 처리 설정 구성 config = BatchProcessingConfig( organization_type=self.organization_selector.value, enable_gemini_batch_mode=self.enable_batch_mode.value, max_concurrent_files=int(self.concurrent_files_slider.value), save_intermediate_results=self.save_intermediate_results.value, output_csv_path=self.csv_output_path.value or None, include_error_files=self.include_error_files.value ) MultiFileUIComponents.add_log_message( self.log_container, f"배치 처리 시작: {len(self.selected_files)}개 파일", "info" ) # 파일 경로 추출 file_paths = [f.path for f in self.selected_files] # 진행률 콜백 함수 def progress_callback(current: int, total: int, status: str): elapsed_time = time.time() - self.start_time if self.start_time else 0 estimated_remaining = (elapsed_time / current * (total - current)) if current > 0 else 0 MultiFileUIComponents.update_batch_progress( self.overall_progress_bar, self.progress_text, self.current_status_text, self.timing_info, current, total, status, elapsed_time, estimated_remaining ) MultiFileUIComponents.add_log_message( self.log_container, status, "success" if "완료" in status else "info" ) self.page.update() # 처리 실행 self.processing_results = await self.processor.process_multiple_files( file_paths, config, progress_callback ) # 결과 표시 MultiFileUIComponents.update_batch_results( self.summary_stats, self.results_table, self.processing_results, self.save_csv_button, self.save_cross_csv_button, # 새로 추가 self.save_excel_button, self.clear_results_button ) # 요약 정보 summary = self.processor.get_processing_summary() MultiFileUIComponents.add_log_message( self.log_container, f"처리 완료! 성공: {summary['success_files']}, 실패: {summary['failed_files']}, 성공률: {summary['success_rate']}%", "success" ) except Exception as e: logger.error(f"배치 처리 오류: {e}") MultiFileUIComponents.add_log_message( self.log_container, f"처리 오류: {str(e)}", "error" ) self.show_error_dialog("처리 오류", f"배치 처리 중 오류가 발생했습니다:\n{str(e)}") finally: # UI 상태 복원 self.is_processing = False self.batch_analysis_button.disabled = False self.cancel_button.disabled = True self.pause_resume_button.disabled = True self.pause_resume_button.text = "일시정지" self.pause_resume_button.icon = ft.Icons.PAUSE self.page.update() async def save_results_to_csv(self): """결과를 CSV로 저장""" try: if not self.processor: self.show_error_dialog("오류", "처리기가 초기화되지 않았습니다.") return # CSV 경로 설정 output_path = self.csv_output_path.value if not output_path: # 자동 생성 results_dir = os.path.join(os.getcwd(), "results") os.makedirs(results_dir, exist_ok=True) output_path = os.path.join(results_dir, generate_default_csv_filename()) # CSV 저장 await self.processor.save_results_to_csv(output_path) self.show_info_dialog( "저장 완료", f"배치 처리 결과가 CSV 파일로 저장되었습니다:\n\n{output_path}" ) MultiFileUIComponents.add_log_message( self.log_container, f"CSV 저장 완료: {os.path.basename(output_path)}", "success" ) except Exception as e: logger.error(f"CSV 저장 오류: {e}") self.show_error_dialog("저장 오류", f"CSV 저장 중 오류가 발생했습니다:\n{str(e)}") MultiFileUIComponents.add_log_message( self.log_container, f"CSV 저장 실패: {str(e)}", "error" ) async def save_cross_tabulated_csv(self): """Cross-tabulated CSV로 저장 (새로 추가)""" try: # Cross-tabulated CSV 내보내기 모듈 임포트 from cross_tabulated_csv_exporter import CrossTabulatedCSVExporter, generate_cross_tabulated_csv_filename exporter = CrossTabulatedCSVExporter() # CSV 경로 설정 results_dir = os.path.join(os.getcwd(), "results") os.makedirs(results_dir, exist_ok=True) # Cross-tabulated CSV 파일명 생성 cross_csv_filename = generate_cross_tabulated_csv_filename("batch_key_value_analysis") output_path = os.path.join(results_dir, cross_csv_filename) # Cross-tabulated CSV 저장 success = exporter.export_cross_tabulated_csv( self.processing_results, output_path, include_coordinates=True, coordinate_source="auto" ) if success: self.show_info_dialog( "Key-Value CSV 저장 완료", f"분석 결과가 Key-Value 형태의 CSV 파일로 저장되었습니다:\n\n{output_path}\n\n" + "이 CSV는 다음과 같은 형태로 구성됩니다:\n" + "- file_name: 파일명\n" + "- file_type: 파일 형식\n" + "- key: 속성 키\n" + "- value: 속성 값\n" + "- x, y: 좌표 정보 (가능한 경우)" ) MultiFileUIComponents.add_log_message( self.log_container, f"Key-Value CSV 저장 완료: {os.path.basename(output_path)}", "success" ) else: self.show_error_dialog( "저장 실패", "Key-Value CSV 저장에 실패했습니다. 로그를 확인해주세요." ) MultiFileUIComponents.add_log_message( self.log_container, "Key-Value CSV 저장 실패", "error" ) except Exception as e: logger.error(f"Cross-tabulated CSV 저장 오류: {e}") self.show_error_dialog( "저장 오류", f"Key-Value CSV 저장 중 오류가 발생했습니다:\n{str(e)}" ) MultiFileUIComponents.add_log_message( self.log_container, f"Key-Value CSV 저장 실패: {str(e)}", "error" ) # 유틸리티 함수들 def show_error_dialog(self, title: str, message: str): """오류 다이얼로그 표시""" from ui_components import UIComponents 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): """정보 다이얼로그 표시""" from ui_components import UIComponents 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() # 사용 예시 if __name__ == "__main__": def main_multi_file(page: ft.Page): """다중 파일 처리 앱 테스트 실행""" page.title = "다중 파일 처리 테스트" page.theme_mode = ft.ThemeMode.LIGHT app = MultiFileApp(page) ui = app.build_ui() page.add(ui) ft.app(target=main_multi_file)