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

724 lines
26 KiB
Python

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