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

1162 lines
46 KiB
Python
Raw 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 도면 분석기 - 메인 애플리케이션 (업데이트된 좌우 분할 레이아웃)
Flet 기반의 PDF/DXF 업로드 및 분석 애플리케이션
- PDF: Gemini API 이미지 분석
- DXF: ezdxf 라이브러리를 통한 도곽 정보 추출
새로운 UI: 좌측 설정/분석, 우측 결과, PDF 뷰어 모달
"""
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 # NEW - 수정된 DXF 처리기
from comprehensive_text_extractor import ComprehensiveTextExtractor # NEW - 포괄적 텍스트 추출기
from gemini_analyzer import GeminiAnalyzer
from ui_components import UIComponents
from utils import AnalysisResultSaver, DateTimeUtils
from csv_exporter import TitleBlockCSVExporter # NEW - CSV 저장 기능
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DocumentAnalyzerApp:
"""PDF/DXF 분석기 메인 애플리케이션 클래스 - 새로운 좌우 분할 레이아웃"""
def __init__(self, page: ft.Page):
self.page = page
self.pdf_processor = PDFProcessor()
self.dxf_processor = DXFProcessor() # NEW - DXF 처리기
self.text_extractor = ComprehensiveTextExtractor() # NEW - 포괄적 텍스트 추출기
self.csv_exporter = TitleBlockCSVExporter() # NEW - CSV 저장기
self.gemini_analyzer = None
self.current_file_path = None # PDF/DXF 파일 경로
self.current_file_type = None # 파일 타입 (pdf 또는 dxf)
self.current_pdf_info = None # PDF 전용
self.current_title_block_info = None # DXF 타이틀블럭 정보
self.current_text_extraction_result = None # NEW - 포괄적 텍스트 추출 결과
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 # NEW - CSV 저장 버튼
self.title_block_table = None # NEW - 타이틀블럭 속성 테이블
self.comprehensive_text_display = None # NEW - 포괄적 텍스트 표시 컴포넌트
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.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
# 윈도우 크기 설정 - 버튼이 모두 보이게 세로 길게, 가로는 10% 줄임
self.page.window.width = 980 # 1400 * 0.9 = 1260
self.page.window.height = 980 # 1000 -> 1080으로 증가
self.page.window.min_width = 1080 # 1200 * 0.9 = 1080
self.page.window.min_height = 780
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
# 좌측 컨트롤 패널 (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,
),
])
# 메인 컨테이너
main_container = ft.Container(
content=ft.Column([
main_layout,
], expand=True, scroll=ft.ScrollMode.AUTO),
expand=True,
margin=10,
)
# 페이지에 추가
self.page.add(main_container)
# PDF 뷰어 다이얼로그 초기화
self.init_pdf_viewer_dialog()
logger.info("새로운 좌우 분할 UI 구성 완료")
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 파일을 선택하고 분석을 시작하세요.",
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,
)
)
# NEW - CSV 저장 버튼 (DXF 전용)
self.save_csv_button = ft.ElevatedButton(
text="📊 CSV 저장",
icon=ft.Icons.TABLE_CHART,
disabled=True,
visible=False, # 기본적으로 숨김, DXF 분석 시에만 표시
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, # NEW - CSV 저장 버튼 추가
]),
], 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,
)
# 이벤트 핸들러들
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):
"""파일 선택 결과 핸들러 - PDF/DXF 지원"""
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):
# PDF 정보 조회
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):
# DXF 파일 크기 계산
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 # DXF는 미리보기 비활성화
# DXF는 페이지 개념이 없으므로 기본값 설정
self.page_info_text.value = "DXF 파일"
self.current_page_index = 0
self.current_pdf_info = None # DXF는 PDF 정보 없음
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 # NEW - 타이틀블럭 정보 초기화
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:
# PDF 페이지를 이미지로 변환
image_data = self.pdf_processor.pdf_page_to_image_bytes(
self.current_file_path,
self.current_page_index
)
if image_data:
# base64로 인코딩
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
# PDF 분석의 경우 Gemini 분석기가 필요
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 저장 버튼 클릭 핸들러 (DXF 타이틀블럭 속성 전용)"""
if not self.current_title_block_info:
self.show_error_dialog("저장 오류", "저장할 타이틀블럭 속성 정보가 없습니다.")
return
try:
# CSV 파일 저장
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 or not self.current_pdf_info:
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'],
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'],
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):
"""분석 실행 (백그라운드 스레드) - PDF/DXF 지원"""
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)
# 1. 텍스트와 좌표 추출
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}에서 텍스트를 추출하지 못했습니다.")
# 2. 이미지를 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:
# 3. 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:
# DXF 파일 처리
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()
# 메인 스레드에서 UI 업데이트
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:
# 결과가 JSON 문자열이므로 파싱
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):
# JSON 파싱 실패 시 원본 텍스트 표시
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"
# 테이블 데이터 (최대 10개만 표시)
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]): # 최대 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
# CSV 저장 버튼 표시 및 활성화 (타이틀블럭이 있는 경우)
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()
# 메인 스레드에서 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 = DocumentAnalyzerApp(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",
)