430 lines
15 KiB
Python
430 lines
15 KiB
Python
"""
|
|
간단한 다중 파일 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)
|