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

2399 lines
83 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
"""
UI 컴포넌트 모듈
Flet 기반 사용자 인터페이스 컴포넌트들을 정의합니다.
"""
import flet as ft
from typing import Callable
import logging
from config import Config
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class UIComponents:
"""UI 컴포넌트 클래스"""
@staticmethod
def create_app_bar() -> ft.AppBar:
"""애플리케이션 상단 바 생성"""
return ft.AppBar(
title=ft.Text(
Config.APP_TITLE,
size=20,
weight=ft.FontWeight.BOLD
),
center_title=True,
bgcolor=ft.Colors.BLUE_600,
color=ft.Colors.WHITE,
automatically_imply_leading=False,
)
@staticmethod
def create_file_upload_section(
on_file_selected: Callable,
on_upload_click: Callable,
organization_selector_ref: Callable = None
) -> ft.Container:
"""파일 업로드 섹션 생성"""
# 파일 선택기
file_picker = ft.FilePicker(
on_result=on_file_selected
)
# 선택된 파일 정보 텍스트
selected_file_text = ft.Text(
"선택된 파일이 없습니다",
size=14,
color=ft.Colors.GREY_600
)
# 파일 선택 버튼
select_button = ft.ElevatedButton(
text="PDF/DXF 파일 선택",
icon=ft.Icons.UPLOAD_FILE,
on_click=lambda _: file_picker.pick_files(
allowed_extensions=Config.ALLOWED_EXTENSIONS,
allow_multiple=False
),
style=ft.ButtonStyle(
bgcolor=ft.Colors.BLUE_100,
color=ft.Colors.BLUE_800,
)
)
# 업로드 버튼
upload_button = ft.ElevatedButton(
text="분석 시작",
icon=ft.Icons.ANALYTICS,
on_click=on_upload_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
# 반환되는 컨테이너에 organization_selector를 포함
container = ft.Container(
content=ft.Column([
ft.Text(
"📄 PDF/DXF 파일 업로드",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
),
ft.Divider(),
ft.Row([
select_button,
upload_button,
], alignment=ft.MainAxisAlignment.START),
selected_file_text,
file_picker, # overlay에 추가될 컴포넌트
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return container
@staticmethod
def create_analysis_settings_section_with_refs() -> tuple:
"""분석 설정 섹션 생성 및 참조 반환"""
# 조직 선택 드롭다운
organization_selector = ft.Dropdown(
label="조직 유형",
value="국토교통부",
options=[
ft.dropdown.Option("국토교통부"),
ft.dropdown.Option("한국도로공사"),
],
width=180,
tooltip="분석할 도면의 조직 유형을 선택하세요",
)
# 페이지 선택 드롭다운
page_selector = ft.Dropdown(
label="분석할 페이지",
value="첫 번째 페이지",
options=[
ft.dropdown.Option("첫 번째 페이지"),
ft.dropdown.Option("모든 페이지"),
ft.dropdown.Option("사용자 지정"),
],
width=200,
)
# 분석 모드 선택
analysis_mode = ft.RadioGroup(
content=ft.Column([
ft.Radio(value="basic", label="기본 분석"),
ft.Radio(value="detailed", label="상세 분석"),
ft.Radio(value="custom", label="사용자 정의"),
]),
value="basic"
)
# 사용자 정의 프롬프트
custom_prompt = ft.TextField(
label="사용자 정의 분석 요청",
multiline=True,
min_lines=3,
max_lines=5,
hint_text="분석하고 싶은 내용을 자세히 입력하세요...",
visible=False,
)
# 조직별 설명 텍스트
org_description = ft.Text(
"💡 국토교통부: 일반 토목/건설 도면 스키마 적용\n" +
"🛣️ 한국도로공사: 고속도로 전용 도면 스키마 적용",
size=12,
color=ft.Colors.BLUE_700,
italic=True,
)
container = ft.Container(
content=ft.Column([
ft.Text(
"⚙️ 분석 설정",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.ORANGE_800
),
ft.Divider(),
org_description,
ft.Row([
ft.Column([
ft.Text("조직 유형:", weight=ft.FontWeight.BOLD),
organization_selector,
], expand=1),
ft.Column([
ft.Text("페이지 선택:", weight=ft.FontWeight.BOLD),
page_selector,
], expand=1),
ft.Column([
ft.Text("분석 모드:", weight=ft.FontWeight.BOLD),
analysis_mode,
], expand=1),
]),
custom_prompt,
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 컴포넌트 참조들과 함께 반환
return container, organization_selector, page_selector, analysis_mode, custom_prompt
@staticmethod
def create_analysis_settings_section() -> ft.Container:
"""기본 분석 설정 섹션 생성 (이전 버전 호환성)"""
container, _, _, _, _ = UIComponents.create_analysis_settings_section_with_refs()
return container
@staticmethod
def create_progress_section() -> ft.Container:
"""진행률 표시 섹션 생성"""
# 진행률 바
progress_bar = ft.ProgressBar(
width=400,
color=ft.Colors.BLUE_600,
bgcolor=ft.Colors.GREY_300,
visible=False,
)
# 상태 텍스트
status_text = ft.Text(
"대기 중...",
size=14,
color=ft.Colors.GREY_600
)
# 스피너
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([
progress_ring,
ft.Column([
status_text,
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),
)
@staticmethod
def create_results_section() -> ft.Container:
"""결과 표시 섹션 생성"""
# 결과 텍스트 영역
results_text = ft.Text(
"분석 결과가 여기에 표시됩니다.",
size=14,
selectable=True,
)
# 결과 컨테이너
results_container = ft.Container(
content=ft.Column([
results_text,
], scroll=ft.ScrollMode.AUTO),
padding=15,
height=300,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 저장 버튼
save_button = ft.ElevatedButton(
text="결과 저장",
icon=ft.Icons.SAVE,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.TEAL_100,
color=ft.Colors.TEAL_800,
)
)
return ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📋 분석 결과",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.GREEN_800
),
save_button,
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
results_container,
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
@staticmethod
def create_pdf_preview_section() -> ft.Container:
"""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=300,
height=400,
bgcolor=ft.Colors.GREY_100,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
alignment=ft.alignment.center,
)
# 페이지 네비게이션
page_nav = ft.Row([
ft.IconButton(
icon=ft.Icons.ARROW_BACK,
disabled=True,
),
ft.Text("1 / 1", size=14),
ft.IconButton(
icon=ft.Icons.ARROW_FORWARD,
disabled=True,
),
], 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(),
image_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),
)
@staticmethod
def create_error_dialog(title: str, message: str) -> ft.AlertDialog:
"""오류 다이얼로그 생성"""
return ft.AlertDialog(
modal=True,
title=ft.Text(title, weight=ft.FontWeight.BOLD),
content=ft.Text(message),
actions=[
ft.TextButton("확인", on_click=lambda e: None),
],
actions_alignment=ft.MainAxisAlignment.END,
)
@staticmethod
def create_info_dialog(title: str, message: str) -> ft.AlertDialog:
"""정보 다이얼로그 생성"""
return ft.AlertDialog(
modal=True,
title=ft.Text(title, weight=ft.FontWeight.BOLD),
content=ft.Text(message),
actions=[
ft.TextButton("확인", on_click=lambda e: None),
],
actions_alignment=ft.MainAxisAlignment.END,
)
@staticmethod
def create_loading_overlay() -> ft.Container:
"""로딩 오버레이 생성"""
return ft.Container(
content=ft.Column([
ft.ProgressRing(width=50, height=50),
ft.Text("처리 중...", size=16, weight=ft.FontWeight.BOLD),
], alignment=ft.MainAxisAlignment.CENTER),
width=200,
height=100,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(2, ft.Colors.BLUE_600),
alignment=ft.alignment.center,
)
@staticmethod
def create_comprehensive_text_display(text_entities: list = None) -> ft.Container:
"""포괄적 텍스트 추출 결과 표시 섹션 생성"""
# 텍스트 엔티티 데이터 테이블
data_table = ft.DataTable(
columns=[
ft.DataColumn(ft.Text("유형", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("텍스트", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("위치 (X, Y)", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("레이어", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("위치 종류", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("블록명", weight=ft.FontWeight.BOLD)),
],
rows=[],
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=5,
divider_thickness=1,
heading_row_color=ft.Colors.BLUE_50,
heading_row_height=50,
data_row_max_height=60,
)
# 통계 정보 표시
stats_container = ft.Container(
content=ft.Column([
ft.Text("📊 추출 통계", size=16, weight=ft.FontWeight.BOLD),
ft.Divider(),
ft.Text("총 텍스트 엔티티: 0개", size=14),
ft.Text("모델스페이스: 0개", size=14),
ft.Text("페이퍼스페이스: 0개", size=14),
ft.Text("블록 내부: 0개", size=14),
ft.Text("비도곽 속성: 0개", size=14),
]),
padding=15,
bgcolor=ft.Colors.BLUE_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.BLUE_200),
width=250,
)
# CSV 저장 버튼
csv_save_button = ft.ElevatedButton(
text="CSV로 저장",
icon=ft.Icons.SAVE_AS,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
# JSON 저장 버튼
json_save_button = ft.ElevatedButton(
text="JSON으로 저장",
icon=ft.Icons.CODE,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
# 필터링 옵션
filter_container = ft.Container(
content=ft.Row([
ft.Dropdown(
label="유형 필터",
value="전체",
options=[
ft.dropdown.Option("전체"),
ft.dropdown.Option("TEXT"),
ft.dropdown.Option("MTEXT"),
ft.dropdown.Option("ATTRIB"),
],
width=150,
),
ft.Dropdown(
label="위치 필터",
value="전체",
options=[
ft.dropdown.Option("전체"),
ft.dropdown.Option("ModelSpace"),
ft.dropdown.Option("PaperSpace"),
ft.dropdown.Option("Block"),
],
width=150,
),
ft.TextField(
label="텍스트 검색",
hint_text="검색할 텍스트 입력...",
width=200,
),
], spacing=10),
padding=10,
)
# 테이블 컨테이너 (스크롤 가능)
table_container = ft.Container(
content=ft.Column([
filter_container,
ft.Container(
content=data_table,
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=5,
),
], scroll=ft.ScrollMode.AUTO),
height=400,
expand=True,
)
return ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📝 DXF 텍스트 엔티티 추출 결과",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.TEAL_800
),
ft.Row([
csv_save_button,
json_save_button,
], spacing=10),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
ft.Row([
stats_container,
table_container,
], spacing=20, expand=True),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
expand=True,
)
@staticmethod
def update_comprehensive_text_display(container: ft.Container, text_entities: list, stats: dict) -> None:
"""포괄적 텍스트 표시 업데이트"""
try:
# 통계 정보 업데이트
stats_column = container.content.controls[2].controls[0].content # stats_container의 Column
stats_column.controls[2] = ft.Text(f"총 텍스트 엔티티: {stats.get('total_count', 0)}", size=14)
stats_column.controls[3] = ft.Text(f"모델스페이스: {len(stats.get('modelspace_texts', []))}", size=14)
stats_column.controls[4] = ft.Text(f"페이퍼스페이스: {len(stats.get('paperspace_texts', []))}", size=14)
stats_column.controls[5] = ft.Text(f"블록 내부: {len(stats.get('block_texts', []))}", size=14)
stats_column.controls[6] = ft.Text(f"비도곽 속성: {len(stats.get('non_title_block_attributes', []))}", size=14)
# 데이터 테이블 업데이트
table_container = container.content.controls[2].controls[1] # table_container
data_table = table_container.content.controls[1].content # data_table
# 테이블 행 생성
rows = []
for i, entity in enumerate(text_entities[:100]): # 처음 100개만 표시
# 텍스트 길이 제한
display_text = entity.get('text', '')[:30] + '...' if len(entity.get('text', '')) > 30 else entity.get('text', '')
position_text = f"({entity.get('position_x', 0):.1f}, {entity.get('position_y', 0):.1f})"
rows.append(
ft.DataRow(
cells=[
ft.DataCell(ft.Text(entity.get('entity_type', 'N/A'), size=12)),
ft.DataCell(ft.Text(display_text, size=12, tooltip=entity.get('text', ''))),
ft.DataCell(ft.Text(position_text, size=12)),
ft.DataCell(ft.Text(entity.get('layer', 'N/A'), size=12)),
ft.DataCell(ft.Text(entity.get('location_type', 'N/A'), size=12)),
ft.DataCell(ft.Text(entity.get('parent_block', 'N/A') or 'N/A', size=12)),
],
color=ft.Colors.BLUE_50 if i % 2 == 0 else ft.Colors.WHITE
)
)
data_table.rows = rows
# 저장 버튼 활성화
header_row = container.content.controls[0] # 헤더 Row
button_row = header_row.controls[1] # 버튼들이 있는 Row
button_row.controls[0].disabled = False # CSV 버튼
button_row.controls[1].disabled = False # JSON 버튼
logger.info(f"포괄적 텍스트 표시 업데이트 완료: {len(text_entities)}개 엔티티")
except Exception as e:
logger.error(f"포괄적 텍스트 표시 업데이트 실패: {e}")
@staticmethod
def create_dxf_analysis_summary(title_block_info: dict = None, block_count: int = 0) -> ft.Container:
"""DXF 분석 요약 정보 표시"""
# 도곽 정보 표시
title_block_content = []
if title_block_info:
title_block_content = [
ft.Text("📋 도곽 정보", size=16, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_800),
ft.Divider(),
ft.Text(f"블록명: {title_block_info.get('block_name', 'N/A')}", size=14),
ft.Text(f"도면명: {title_block_info.get('drawing_name', 'N/A')}", size=14),
ft.Text(f"도면번호: {title_block_info.get('drawing_number', 'N/A')}", size=14),
ft.Text(f"축척: {title_block_info.get('scale', 'N/A')}", size=14),
ft.Text(f"속성 개수: {title_block_info.get('attributes_count', 0)}", size=14),
]
else:
title_block_content = [
ft.Text("📋 도곽 정보", size=16, weight=ft.FontWeight.BOLD, color=ft.Colors.GREY_600),
ft.Divider(),
ft.Text("도곽 블록을 찾을 수 없습니다.", size=14, color=ft.Colors.GREY_600),
]
# 전체 분석 요약
summary_content = [
ft.Text("📊 분석 요약", size=16, weight=ft.FontWeight.BOLD, color=ft.Colors.GREEN_800),
ft.Divider(),
ft.Text(f"총 블록 참조: {block_count}", size=14),
ft.Text(f"도곽 발견: {'' if title_block_info else '아니오'}", size=14),
]
return ft.Container(
content=ft.Row([
ft.Container(
content=ft.Column(title_block_content),
padding=15,
bgcolor=ft.Colors.BLUE_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.BLUE_200),
expand=1,
),
ft.Container(
content=ft.Column(summary_content),
padding=15,
bgcolor=ft.Colors.GREEN_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREEN_200),
expand=1,
),
], spacing=20),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 사용 예시
if __name__ == "__main__":
def dummy_callback(*args, **kwargs):
"""더미 콜백 함수"""
pass
# UI 컴포넌트 테스트
print("UI 컴포넌트 모듈 로드 완료")
print(f"앱 제목: {Config.APP_TITLE}")
class MultiFileUIComponents:
"""다중 파일 처리를 위한 UI 컴포넌트 클래스"""
@staticmethod
def create_multi_file_upload_section(
on_files_selected: Callable,
on_batch_analysis_click: Callable,
on_clear_files_click: Callable
) -> ft.Container:
"""다중 파일 업로드 섹션 생성"""
# 파일 선택기
file_picker = ft.FilePicker(
on_result=on_files_selected
)
# 선택된 파일 목록 표시
selected_files_list = ft.Column(
controls=[
ft.Text(
"선택된 파일이 없습니다",
size=14,
color=ft.Colors.GREY_600
)
],
scroll=ft.ScrollMode.AUTO,
height=150,
)
# 파일 선택 버튰
select_files_button = ft.ElevatedButton(
text="여러 PDF/DXF 파일 선택",
icon=ft.Icons.UPLOAD_FILE,
on_click=lambda _: file_picker.pick_files(
allowed_extensions=Config.ALLOWED_EXTENSIONS,
allow_multiple=True
),
style=ft.ButtonStyle(
bgcolor=ft.Colors.BLUE_100,
color=ft.Colors.BLUE_800,
)
)
# 파일 지우기 버튰
clear_files_button = ft.ElevatedButton(
text="목록 지우기",
icon=ft.Icons.CLEAR,
on_click=on_clear_files_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
# 배치 분석 버튰
batch_analysis_button = ft.ElevatedButton(
text="배치 분석 시작",
icon=ft.Icons.BATCH_PREDICTION,
on_click=on_batch_analysis_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
# 파일 목록 컸테이너
files_container = ft.Container(
content=selected_files_list,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return ft.Container(
content=ft.Column([
ft.Text(
"📄 다중 PDF/DXF 파일 배치 처리",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
),
ft.Divider(),
ft.Row([
select_files_button,
clear_files_button,
batch_analysis_button,
], alignment=ft.MainAxisAlignment.START, spacing=10),
ft.Text(
"선택된 파일 목록:",
size=14,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_700
),
files_container,
file_picker, # overlay에 추가될 컴포넌트
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
@staticmethod
def create_batch_settings_section() -> tuple:
"""배치 처리 설정 섹션 생성 및 참조 반환"""
# 조직 선택 드롭다운
organization_selector = ft.Dropdown(
label="조직 유형",
value="국토교통부",
options=[
ft.dropdown.Option("국토교통부"),
ft.dropdown.Option("한국도로공사"),
],
width=180,
tooltip="분석할 도면의 조직 유형을 선택하세요",
)
# 동시 처리 수 설정
concurrent_files_slider = ft.Slider(
min=1,
max=5,
divisions=4,
value=3,
label="{value}",
width=200,
)
# Gemini 배치 모드 설정
enable_batch_mode = ft.Switch(
label="Gemini 배치 모드 (50% 할인)",
value=False,
tooltip="비실시간 처리로 24시간 내 결과, 50% 비용 절약",
)
# 중간 결과 저장 설정
save_intermediate_results = ft.Switch(
label="중간 결과 저장",
value=True,
tooltip="각 파일 처리 후 즉시 결과 저장",
)
# 오류 파일 포함 설정
include_error_files = ft.Switch(
label="오류 파일 CSV 포함",
value=True,
tooltip="처리 실패한 파일도 CSV에 기록",
)
# CSV 출력 경로 설정
csv_output_path = ft.TextField(
label="CSV 출력 경로 (선택사항)",
hint_text="비워두면 자동 생성...",
width=300,
)
# 경로 선택 버튰
browse_button = ft.IconButton(
icon=ft.Icons.FOLDER_OPEN,
tooltip="저장 위치 선택",
)
container = ft.Container(
content=ft.Column([
ft.Text(
"⚙️ 배치 처리 설정",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.ORANGE_800
),
ft.Divider(),
ft.Row([
ft.Column([
ft.Text("조직 유형:", weight=ft.FontWeight.BOLD),
organization_selector,
], expand=1),
ft.Column([
ft.Text(f"동시 처리 수: {int(concurrent_files_slider.value)}", weight=ft.FontWeight.BOLD),
concurrent_files_slider,
], expand=1),
]),
ft.Divider(),
ft.Column([
enable_batch_mode,
save_intermediate_results,
include_error_files,
], spacing=10),
ft.Divider(),
ft.Row([
csv_output_path,
browse_button,
], alignment=ft.MainAxisAlignment.START),
ft.Text(
"💡 팁: 배치 모드를 사용하면 비용을 50% 절약할 수 있지만, 결과를 받기까지 더 오래 걸립니다.",
size=12,
color=ft.Colors.BLUE_700,
italic=True,
),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return (
container,
organization_selector,
concurrent_files_slider,
enable_batch_mode,
save_intermediate_results,
include_error_files,
csv_output_path,
browse_button
)
@staticmethod
def create_batch_progress_section() -> tuple:
"""배치 처리 진행률 섹션 생성"""
# 전체 진행률 바
overall_progress_bar = ft.ProgressBar(
width=400,
color=ft.Colors.BLUE_600,
bgcolor=ft.Colors.GREY_300,
value=0,
visible=False,
)
# 진행률 텅스트
progress_text = ft.Text(
"0 / 0 파일 처리 완료 (0%)",
size=14,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
)
# 현재 처리 중인 파일 상태
current_status_text = ft.Text(
"대기 중...",
size=14,
color=ft.Colors.GREY_600
)
# 처리 시간 정보
timing_info = ft.Text(
"추정 남은 시간: -",
size=12,
color=ft.Colors.GREY_500
)
# 실시간 로그
log_container = ft.Container(
content=ft.Column(
controls=[],
scroll=ft.ScrollMode.AUTO,
),
height=150,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 취소 버튰
cancel_button = ft.ElevatedButton(
text="처리 취소",
icon=ft.Icons.CANCEL,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.RED_100,
color=ft.Colors.RED_800,
)
)
# 일시정지/재개 버튰
pause_resume_button = ft.ElevatedButton(
text="일시정지",
icon=ft.Icons.PAUSE,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.YELLOW_100,
color=ft.Colors.YELLOW_800,
)
)
container = ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📈 배치 처리 진행상황",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.PURPLE_800
),
ft.Row([
pause_resume_button,
cancel_button,
], spacing=10),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
progress_text,
overall_progress_bar,
current_status_text,
timing_info,
ft.Text(
"📜 실시간 로그:",
size=14,
weight=ft.FontWeight.BOLD,
color=ft.Colors.GREY_700
),
log_container,
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return (
container,
overall_progress_bar,
progress_text,
current_status_text,
timing_info,
log_container,
cancel_button,
pause_resume_button
)
@staticmethod
def create_batch_results_section() -> tuple:
"""배치 처리 결과 섹션 생성"""
# 결과 요약 통계
summary_stats = ft.Container(
content=ft.Column([
ft.Text("📈 처리 요약", size=16, weight=ft.FontWeight.BOLD),
ft.Divider(),
ft.Text("총 파일: 0개", size=14),
ft.Text("성공: 0개", size=14, color=ft.Colors.GREEN_600),
ft.Text("실패: 0개", size=14, color=ft.Colors.RED_600),
ft.Text("성공률: 0%", size=14),
ft.Text("총 처리시간: 0초", size=14),
ft.Text("평균 처리시간: 0초", size=14),
]),
padding=15,
bgcolor=ft.Colors.BLUE_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.BLUE_200),
width=250,
)
# 결과 테이블
results_table = ft.DataTable(
columns=[
ft.DataColumn(ft.Text("파일명", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("유형", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("크기(MB)", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("상태", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("처리시간(s)", weight=ft.FontWeight.BOLD)),
],
rows=[],
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=5,
divider_thickness=1,
heading_row_color=ft.Colors.BLUE_50,
heading_row_height=50,
data_row_max_height=60,
)
# 테이블 컸테이너 (스크롤 가능)
table_container = ft.Container(
content=results_table,
height=300,
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=5,
expand=True,
)
# CSV 저장 버튰
save_csv_button = ft.ElevatedButton(
text="CSV로 저장",
icon=ft.Icons.SAVE_AS,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
# Excel 저장 버튰
save_excel_button = ft.ElevatedButton(
text="Excel로 저장",
icon=ft.Icons.TABLE_CHART,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
# 결과 초기화 버튴
clear_results_button = ft.ElevatedButton(
text="결과 초기화",
icon=ft.Icons.REFRESH,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREY_100,
color=ft.Colors.GREY_800,
)
)
container = ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📄 배치 처리 결과",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.GREEN_800
),
ft.Row([
save_csv_button,
save_excel_button,
clear_results_button,
], spacing=10),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
ft.Row([
summary_stats,
table_container,
], spacing=20, expand=True),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
expand=True,
)
return (
container,
summary_stats,
results_table,
save_csv_button,
save_excel_button,
clear_results_button
)
@staticmethod
def update_selected_files_list(
files_container: ft.Container,
selected_files: list,
clear_button: ft.ElevatedButton,
batch_button: ft.ElevatedButton
) -> None:
"""선택된 파일 목록 업데이트"""
try:
# 파일 목록 컸테이너 찾기
column = files_container.content # Column
if not selected_files:
# 파일이 없을 때
column.controls = [
ft.Text(
"선택된 파일이 없습니다",
size=14,
color=ft.Colors.GREY_600
)
]
clear_button.disabled = True
batch_button.disabled = True
else:
# 파일 목록 표시
file_controls = []
total_size = 0
for i, file_info in enumerate(selected_files, 1):
file_size_mb = file_info.size / (1024 * 1024) if file_info.size else 0
total_size += file_size_mb
file_controls.append(
ft.Container(
content=ft.Row([
ft.Icon(
ft.Icons.PICTURE_AS_PDF if file_info.name.lower().endswith('.pdf') else ft.Icons.ARCHITECTURE,
size=20,
color=ft.Colors.BLUE_600
),
ft.Column([
ft.Text(
f"{i}. {file_info.name}",
size=13,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLACK87
),
ft.Text(
f"{file_size_mb:.1f} MB",
size=11,
color=ft.Colors.GREY_600
),
], spacing=2, expand=True),
], spacing=10),
padding=8,
bgcolor=ft.Colors.BLUE_50 if i % 2 == 0 else ft.Colors.WHITE,
border_radius=4,
)
)
# 요약 정보 추가
file_controls.append(
ft.Container(
content=ft.Text(
f"전체: {len(selected_files)}개 파일, {total_size:.1f} MB",
size=12,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
),
padding=ft.padding.symmetric(vertical=5),
bgcolor=ft.Colors.BLUE_100,
border_radius=4,
alignment=ft.alignment.center,
)
)
column.controls = file_controls
clear_button.disabled = False
batch_button.disabled = False
logger.info(f"파일 목록 업데이트 완료: {len(selected_files)}개 파일")
except Exception as e:
logger.error(f"파일 목록 업데이트 실패: {e}")
@staticmethod
def update_batch_progress(
progress_bar: ft.ProgressBar,
progress_text: ft.Text,
current_status_text: ft.Text,
timing_info: ft.Text,
current: int,
total: int,
status: str,
elapsed_time: float = 0,
estimated_remaining: float = 0
) -> None:
"""배치 처리 진행률 업데이트"""
try:
if total > 0:
progress_value = current / total
progress_bar.value = progress_value
progress_bar.visible = True
progress_percentage = progress_value * 100
progress_text.value = f"{current} / {total} 파일 처리 완료 ({progress_percentage:.1f}%)"
else:
progress_bar.visible = False
progress_text.value = "처리 대기 중..."
current_status_text.value = status
# 시간 정보 업데이트
if elapsed_time > 0 and estimated_remaining > 0:
timing_info.value = f"경과 시간: {elapsed_time:.1f}초, 추정 남은 시간: {estimated_remaining:.1f}"
elif elapsed_time > 0:
timing_info.value = f"경과 시간: {elapsed_time:.1f}"
else:
timing_info.value = "추정 남은 시간: -"
except Exception as e:
logger.error(f"진행률 업데이트 실패: {e}")
@staticmethod
def add_log_message(
log_container: ft.Container,
message: str,
level: str = "info"
) -> None:
"""로그 메시지 추가"""
try:
from datetime import datetime
timestamp = datetime.now().strftime("%H:%M:%S")
# 레벨에 따른 색상 설정
color = ft.Colors.BLACK87
icon = ft.Icons.INFO
if level == "error":
color = ft.Colors.RED_600
icon = ft.Icons.ERROR
elif level == "warning":
color = ft.Colors.ORANGE_600
icon = ft.Icons.WARNING
elif level == "success":
color = ft.Colors.GREEN_600
icon = ft.Icons.CHECK_CIRCLE
log_entry = ft.Container(
content=ft.Row([
ft.Icon(icon, size=16, color=color),
ft.Text(f"[{timestamp}]", size=11, color=ft.Colors.GREY_600),
ft.Text(message, size=12, color=color, expand=True),
], spacing=5),
padding=ft.padding.symmetric(vertical=2, horizontal=5),
)
# 로그 컸테이너에 추가
log_column = log_container.content
log_column.controls.append(log_entry)
# 최대 100개 로그만 유지
if len(log_column.controls) > 100:
log_column.controls = log_column.controls[-100:]
# 자동 스크롤 (마지막 메시지로)
# log_column.scroll_to(offset=-1) # Flet에서 지원 시 사용
except Exception as e:
logger.error(f"로그 메시지 추가 실패: {e}")
@staticmethod
def update_batch_results(
summary_stats: ft.Container,
results_table: ft.DataTable,
processing_results: list,
save_csv_button: ft.ElevatedButton,
save_excel_button: ft.ElevatedButton,
clear_results_button: ft.ElevatedButton
) -> None:
"""배치 처리 결과 업데이트"""
try:
# 요약 통계 계산
total_files = len(processing_results)
success_files = sum(1 for r in processing_results if r.success)
failed_files = total_files - success_files
success_rate = (success_files / total_files * 100) if total_files > 0 else 0
total_processing_time = sum(r.processing_time for r in processing_results)
avg_processing_time = total_processing_time / total_files if total_files > 0 else 0
# 요약 통계 업데이트
stats_column = summary_stats.content
stats_column.controls[2] = ft.Text(f"총 파일: {total_files}", size=14)
stats_column.controls[3] = ft.Text(f"성공: {success_files}", size=14, color=ft.Colors.GREEN_600)
stats_column.controls[4] = ft.Text(f"실패: {failed_files}", size=14, color=ft.Colors.RED_600)
stats_column.controls[5] = ft.Text(f"성공률: {success_rate:.1f}%", size=14)
stats_column.controls[6] = ft.Text(f"총 처리시간: {total_processing_time:.1f}", size=14)
stats_column.controls[7] = ft.Text(f"평균 처리시간: {avg_processing_time:.1f}", size=14)
# 결과 테이블 업데이트
rows = []
for i, result in enumerate(processing_results):
status_icon = "" if result.success else ""
status_text = f"{status_icon} 성공" if result.success else f"{status_icon} 실패"
file_size_mb = result.file_size / (1024 * 1024) if result.file_size else 0
rows.append(
ft.DataRow(
cells=[
ft.DataCell(ft.Text(result.file_name[:30] + '...' if len(result.file_name) > 30 else result.file_name, size=12)),
ft.DataCell(ft.Text(result.file_type, size=12)),
ft.DataCell(ft.Text(f"{file_size_mb:.1f}", size=12)),
ft.DataCell(ft.Text(status_text, size=12)),
ft.DataCell(ft.Text(f"{result.processing_time:.1f}", size=12)),
],
color=ft.Colors.GREEN_50 if result.success else ft.Colors.RED_50
)
)
results_table.rows = rows
# 저장 버튴 활성화
has_results = total_files > 0
save_csv_button.disabled = not has_results
save_excel_button.disabled = not has_results
clear_results_button.disabled = not has_results
logger.info(f"배치 결과 업데이트 완료: {total_files}개 파일")
except Exception as e:
logger.error(f"배치 결과 업데이트 실패: {e}")
class MultiFileUIComponents:
"""다중 파일 처리 UI 컴포넌트 클래스"""
@staticmethod
def create_multi_file_upload_section(
on_files_selected: Callable,
on_batch_analysis_click: Callable,
on_clear_files_click: Callable
) -> ft.Container:
"""다중 파일 업로드 섹션 생성"""
# 파일 선택기 (다중 선택 지원)
file_picker = ft.FilePicker(
on_result=on_files_selected
)
# 파일 선택 버튼
select_files_button = ft.ElevatedButton(
text="파일 선택",
icon=ft.Icons.ADD_CIRCLE,
on_click=lambda _: file_picker.pick_files(
allowed_extensions=Config.ALLOWED_EXTENSIONS,
allow_multiple=True # 다중 선택 허용
),
style=ft.ButtonStyle(
bgcolor=ft.Colors.BLUE_100,
color=ft.Colors.BLUE_800,
)
)
# 파일 목록 지우기 버튼
clear_files_button = ft.ElevatedButton(
text="목록 지우기",
icon=ft.Icons.CLEAR_ALL,
on_click=on_clear_files_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
# 배치 분석 시작 버튼
batch_analysis_button = ft.ElevatedButton(
text="배치 분석 시작",
icon=ft.Icons.PLAY_ARROW,
on_click=on_batch_analysis_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
# 선택된 파일 목록 컨테이너
files_container = ft.Container(
content=ft.Column([
ft.Text(
"선택된 파일이 없습니다",
size=14,
color=ft.Colors.GREY_600
)
], scroll=ft.ScrollMode.AUTO),
height=200,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
container = ft.Container(
content=ft.Column([
ft.Text(
"📁 다중 파일 업로드",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
),
ft.Divider(),
ft.Row([
select_files_button,
clear_files_button,
batch_analysis_button,
], alignment=ft.MainAxisAlignment.START, spacing=10),
ft.Text("선택된 파일 목록:", weight=ft.FontWeight.BOLD),
files_container,
file_picker, # overlay에 추가될 컴포넌트
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return container
@staticmethod
def create_batch_settings_section() -> tuple:
"""배치 설정 섹션 생성"""
# 조직 선택 드롭다운
organization_selector = ft.Dropdown(
label="조직 유형",
value="국토교통부",
options=[
ft.dropdown.Option("국토교통부"),
ft.dropdown.Option("한국도로공사"),
],
width=180,
tooltip="분석할 도면의 조직 유형을 선택하세요",
)
# 동시 처리 수 슬라이더
concurrent_files_slider = ft.Slider(
min=1,
max=5,
value=3,
divisions=4,
label="{value}",
width=200,
)
# 배치 모드 체크박스
enable_batch_mode = ft.Checkbox(
label="Gemini 배치 모드 사용",
value=False,
tooltip="대량 파일 처리 시 성능 향상",
)
# 중간 결과 저장 체크박스
save_intermediate_results = ft.Checkbox(
label="중간 결과 저장",
value=True,
tooltip="처리 중 중간 결과 자동 저장",
)
# 오류 파일 포함 체크박스
include_error_files = ft.Checkbox(
label="실패한 파일도 결과에 포함",
value=True,
tooltip="처리 실패한 파일도 결과 CSV에 포함",
)
# CSV 출력 경로
csv_output_path = ft.TextField(
label="CSV 저장 경로",
hint_text="비어있으면 자동 생성됩니다",
expand=True,
)
# 경로 선택 버튼
browse_button = ft.IconButton(
icon=ft.Icons.FOLDER_OPEN,
tooltip="저장 경로 선택",
)
container = ft.Container(
content=ft.Column([
ft.Text(
"⚙️ 배치 처리 설정",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.ORANGE_800
),
ft.Divider(),
ft.Row([
organization_selector,
]),
ft.Column([
ft.Text("동시 처리 수: 3개", size=14, weight=ft.FontWeight.BOLD),
concurrent_files_slider,
]),
ft.Column([
enable_batch_mode,
save_intermediate_results,
include_error_files,
]),
ft.Row([
csv_output_path,
browse_button,
]),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return (
container,
organization_selector,
concurrent_files_slider,
enable_batch_mode,
save_intermediate_results,
include_error_files,
csv_output_path,
browse_button
)
@staticmethod
def create_batch_progress_section() -> tuple:
"""배치 처리 진행률 섹션 생성"""
# 전체 진행률 바
overall_progress_bar = ft.ProgressBar(
width=400,
color=ft.Colors.GREEN_600,
bgcolor=ft.Colors.GREY_300,
visible=False,
)
# 진행률 텍스트
progress_text = ft.Text(
"대기 중...",
size=14,
color=ft.Colors.GREY_600
)
# 현재 상태 텍스트
current_status_text = ft.Text(
"",
size=12,
color=ft.Colors.BLUE_600,
italic=True,
)
# 시간 정보
timing_info = ft.Text(
"추정 남은 시간: -",
size=12,
color=ft.Colors.GREY_600,
)
# 로그 컨테이너
log_container = ft.Container(
content=ft.Column([], scroll=ft.ScrollMode.AUTO),
height=150,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 취소 버튼
cancel_button = ft.ElevatedButton(
text="취소",
icon=ft.Icons.CANCEL,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.RED_100,
color=ft.Colors.RED_800,
)
)
# 일시정지/재개 버튼
pause_resume_button = ft.ElevatedButton(
text="일시정지",
icon=ft.Icons.PAUSE,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
container = ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📊 배치 처리 진행률",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.PURPLE_800
),
ft.Row([
cancel_button,
pause_resume_button,
], spacing=10),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
progress_text,
overall_progress_bar,
current_status_text,
timing_info,
ft.Text("처리 로그:", weight=ft.FontWeight.BOLD, size=14),
log_container,
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return (
container,
overall_progress_bar,
progress_text,
current_status_text,
timing_info,
log_container,
cancel_button,
pause_resume_button
)
@staticmethod
def create_batch_results_section() -> tuple:
"""배치 처리 결과 섹션 생성"""
# 요약 통계
summary_stats = ft.Container(
content=ft.Column([
ft.Text("📊 처리 요약", size=16, weight=ft.FontWeight.BOLD),
ft.Divider(),
ft.Text("총 파일: 0개", size=14),
ft.Text("성공: 0개", size=14, color=ft.Colors.GREEN_600),
ft.Text("실패: 0개", size=14, color=ft.Colors.RED_600),
ft.Text("성공률: 0%", size=14),
ft.Text("총 처리시간: 0초", size=14),
ft.Text("평균 처리시간: 0초", size=14),
]),
padding=15,
bgcolor=ft.Colors.BLUE_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.BLUE_200),
width=250,
)
# 결과 테이블
results_table = ft.DataTable(
columns=[
ft.DataColumn(ft.Text("파일명", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("형식", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("크기(MB)", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("상태", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("처리시간(초)", weight=ft.FontWeight.BOLD)),
],
rows=[],
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=5,
divider_thickness=1,
heading_row_color=ft.Colors.BLUE_50,
heading_row_height=50,
data_row_max_height=60,
)
# 테이블 컨테이너
table_container = ft.Container(
content=ft.Column([
ft.Text("처리 결과 상세:", weight=ft.FontWeight.BOLD, size=14),
results_table,
], scroll=ft.ScrollMode.AUTO),
expand=True,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# CSV 저장 버튼 (기존)
save_csv_button = ft.ElevatedButton(
text="CSV로 저장",
icon=ft.Icons.SAVE_ALT,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
# Cross-Tabulated CSV 저장 버튼 (새로 추가)
save_cross_csv_button = ft.ElevatedButton(
text="Key-Value CSV",
icon=ft.Icons.VIEW_LIST,
disabled=True,
tooltip="JSON 분석 결과를 key-value 형태의 CSV로 저장",
style=ft.ButtonStyle(
bgcolor=ft.Colors.PURPLE_100,
color=ft.Colors.PURPLE_800,
)
)
# Excel 저장 버튼
save_excel_button = ft.ElevatedButton(
text="Excel로 저장",
icon=ft.Icons.TABLE_CHART,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
# 결과 초기화 버튼
clear_results_button = ft.ElevatedButton(
text="결과 초기화",
icon=ft.Icons.REFRESH,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREY_100,
color=ft.Colors.GREY_800,
)
)
container = ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📄 배치 처리 결과",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.GREEN_800
),
ft.Row([
save_csv_button,
save_cross_csv_button, # 새로운 버튼 추가
save_excel_button,
clear_results_button,
], spacing=10),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
ft.Row([
summary_stats,
table_container,
], spacing=20, expand=True),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
expand=True,
)
return (
container,
summary_stats,
results_table,
save_csv_button,
save_cross_csv_button, # 새로운 버튼 반환
save_excel_button,
clear_results_button
)
class MultiFileUIComponents:
"""다중 파일 처리 UI 컴포넌트 클래스"""
@staticmethod
def create_multi_file_upload_section(
on_files_selected: Callable,
on_batch_analysis_click: Callable,
on_clear_files_click: Callable
) -> ft.Container:
"""다중 파일 업로드 섹션 생성"""
# 파일 선택기
file_picker = ft.FilePicker(
on_result=on_files_selected
)
# 선택된 파일 목록 컨테이너
files_container = ft.Container(
content=ft.Column([
ft.Text(
"선택된 파일이 없습니다",
size=12,
color=ft.Colors.GREY_600
)
]),
height=150,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 파일 선택 버튼
select_button = ft.ElevatedButton(
text="📁 파일 선택",
icon=ft.Icons.UPLOAD_FILE,
on_click=lambda _: file_picker.pick_files(
allowed_extensions=["pdf", "dxf"],
allow_multiple=True
),
style=ft.ButtonStyle(
bgcolor=ft.Colors.BLUE_100,
color=ft.Colors.BLUE_800,
)
)
# 파일 목록 지우기 버튼
clear_files_button = ft.ElevatedButton(
text="🗑️ 목록 지우기",
icon=ft.Icons.CLEAR,
on_click=on_clear_files_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.RED_100,
color=ft.Colors.RED_800,
)
)
# 배치 분석 버튼
batch_analysis_button = ft.ElevatedButton(
text="🚀 배치 분석",
icon=ft.Icons.BATCH_PREDICTION,
on_click=on_batch_analysis_click,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
container = ft.Container(
content=ft.Column([
ft.Text(
"📄 다중 파일 업로드",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_800
),
ft.Divider(),
ft.Row([
select_button,
clear_files_button,
batch_analysis_button,
], alignment=ft.MainAxisAlignment.START),
ft.Text("선택된 파일 목록:", size=14, weight=ft.FontWeight.BOLD),
files_container,
file_picker, # overlay에 추가될 컴포넌트
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return container
@staticmethod
def create_batch_settings_section() -> tuple:
"""배치 설정 섹션 생성"""
# 조직 선택
organization_selector = ft.Dropdown(
label="분석 스키마",
options=[
ft.dropdown.Option("국토교통부", "국토교통부 - 일반 건설/토목 도면"),
ft.dropdown.Option("한국도로공사", "한국도로공사 - 고속도로 전용 도면"),
],
value="국토교통부",
width=280,
)
# 동시 처리 수 슬라이더
concurrent_files_slider = ft.Slider(
min=1,
max=10,
divisions=9,
value=3,
label="동시 처리 수: {value}",
width=280,
)
# 배치 모드 활성화
enable_batch_mode = ft.Checkbox(
label="Gemini 배치 모드 활성화",
value=True,
)
# 중간 결과 저장
save_intermediate_results = ft.Checkbox(
label="중간 결과 저장",
value=False,
)
# 오류 파일 포함
include_error_files = ft.Checkbox(
label="오류 파일도 CSV에 포함",
value=True,
)
# CSV 출력 경로
csv_output_path = ft.TextField(
label="CSV 저장 경로 (선택사항)",
hint_text="비워두면 자동 생성",
width=200,
)
# 경로 선택 버튼
browse_button = ft.ElevatedButton(
text="📁",
tooltip="경로 선택",
width=50,
)
container = ft.Container(
content=ft.Column([
ft.Text(
"⚙️ 배치 설정",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.PURPLE_800
),
ft.Divider(),
organization_selector,
ft.Row([
ft.Text("동시 처리 수:", size=14),
ft.Column([
ft.Text("동시 처리 수: 3개", size=12, color=ft.Colors.GREY_600),
concurrent_files_slider,
], spacing=0),
]),
enable_batch_mode,
save_intermediate_results,
include_error_files,
ft.Row([
csv_output_path,
browse_button,
], alignment=ft.MainAxisAlignment.START),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return (
container,
organization_selector,
concurrent_files_slider,
enable_batch_mode,
save_intermediate_results,
include_error_files,
csv_output_path,
browse_button
)
@staticmethod
def create_batch_progress_section() -> tuple:
"""배치 진행률 섹션 생성"""
# 전체 진행률 바
overall_progress_bar = ft.ProgressBar(
width=280,
color=ft.Colors.BLUE_600,
bgcolor=ft.Colors.GREY_300,
value=0,
)
# 진행률 텍스트
progress_text = ft.Text(
"0 / 0 파일 처리됨",
size=14,
weight=ft.FontWeight.BOLD,
)
# 현재 상태 텍스트
current_status_text = ft.Text(
"대기 중...",
size=12,
color=ft.Colors.GREY_600,
)
# 시간 정보
timing_info = ft.Text(
"소요 시간: 00:00:00",
size=12,
color=ft.Colors.GREY_600,
)
# 로그 컨테이너
log_container = ft.Container(
content=ft.Column([
ft.Text(
"처리 시작 전...",
size=12,
color=ft.Colors.GREY_600
)
], scroll=ft.ScrollMode.AUTO),
height=100,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 취소 버튼
cancel_button = ft.ElevatedButton(
text="취소",
icon=ft.Icons.CANCEL,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.RED_100,
color=ft.Colors.RED_800,
)
)
# 일시정지/재개 버튼
pause_resume_button = ft.ElevatedButton(
text="일시정지",
icon=ft.Icons.PAUSE,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
container = ft.Container(
content=ft.Column([
ft.Text(
"📊 진행 상황",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.ORANGE_800
),
ft.Divider(),
progress_text,
overall_progress_bar,
current_status_text,
timing_info,
ft.Text("처리 로그:", size=14, weight=ft.FontWeight.BOLD),
log_container,
ft.Row([
cancel_button,
pause_resume_button,
], alignment=ft.MainAxisAlignment.START),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
)
return (
container,
overall_progress_bar,
progress_text,
current_status_text,
timing_info,
log_container,
cancel_button,
pause_resume_button
)
@staticmethod
def create_batch_results_section() -> tuple:
"""배치 결과 섹션 생성"""
# 요약 통계
summary_stats = ft.Container(
content=ft.Column([
ft.Text("처리 요약", size=16, weight=ft.FontWeight.BOLD),
ft.Text("처리된 파일: 0개", size=12),
ft.Text("성공: 0개", size=12, color=ft.Colors.GREEN_600),
ft.Text("실패: 0개", size=12, color=ft.Colors.RED_600),
ft.Text("성공률: 0%", size=12, color=ft.Colors.BLUE_600),
]),
width=200,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# 결과 테이블
results_table = ft.DataTable(
columns=[
ft.DataColumn(ft.Text("파일명", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("타입", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("상태", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("처리 시간", weight=ft.FontWeight.BOLD)),
ft.DataColumn(ft.Text("결과", weight=ft.FontWeight.BOLD)),
],
rows=[],
border=ft.border.all(2, ft.Colors.GREY_300),
border_radius=8,
vertical_lines=ft.BorderSide(1, ft.Colors.GREY_300),
horizontal_lines=ft.BorderSide(1, ft.Colors.GREY_300),
)
# 테이블 컨테이너
table_container = ft.Container(
content=ft.Column([
ft.Text("처리 결과 상세:", weight=ft.FontWeight.BOLD, size=14),
results_table,
], scroll=ft.ScrollMode.AUTO),
expand=True,
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_300),
)
# CSV 저장 버튼 (기존)
save_csv_button = ft.ElevatedButton(
text="CSV로 저장",
icon=ft.Icons.SAVE_ALT,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREEN_100,
color=ft.Colors.GREEN_800,
)
)
# Cross-Tabulated CSV 저장 버튼 (새로 추가)
save_cross_csv_button = ft.ElevatedButton(
text="Key-Value CSV",
icon=ft.Icons.VIEW_LIST,
disabled=True,
tooltip="JSON 분석 결과를 key-value 형태의 CSV로 저장",
style=ft.ButtonStyle(
bgcolor=ft.Colors.PURPLE_100,
color=ft.Colors.PURPLE_800,
)
)
# Excel 저장 버튼
save_excel_button = ft.ElevatedButton(
text="Excel로 저장",
icon=ft.Icons.TABLE_CHART,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.ORANGE_100,
color=ft.Colors.ORANGE_800,
)
)
# 결과 초기화 버튼
clear_results_button = ft.ElevatedButton(
text="결과 초기화",
icon=ft.Icons.REFRESH,
disabled=True,
style=ft.ButtonStyle(
bgcolor=ft.Colors.GREY_100,
color=ft.Colors.GREY_800,
)
)
container = ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
"📄 배치 처리 결과",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.GREEN_800
),
ft.Row([
save_csv_button,
save_cross_csv_button, # 새로운 버튼 추가
save_excel_button,
clear_results_button,
], spacing=10),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
ft.Row([
summary_stats,
table_container,
], spacing=20, expand=True),
]),
padding=20,
margin=10,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.border.all(1, ft.Colors.GREY_300),
expand=True,
)
return (
container,
summary_stats,
results_table,
save_csv_button,
save_cross_csv_button, # 새로운 버튼 반환
save_excel_button,
clear_results_button
)
@staticmethod
def update_selected_files_list(files_container, selected_files, clear_button, batch_button):
"""선택된 파일 목록 업데이트"""
if not selected_files:
files_container.content = ft.Column([
ft.Text(
"선택된 파일이 없습니다",
size=12,
color=ft.Colors.GREY_600
)
])
clear_button.disabled = True
batch_button.disabled = True
else:
file_items = []
for i, file in enumerate(selected_files):
file_items.append(
ft.Row([
ft.Text(f"{i+1}.", size=12, width=30),
ft.Text(
file.name,
size=12,
expand=True,
overflow=ft.TextOverflow.ELLIPSIS,
),
ft.Text(
f"({file.path.split('.')[-1].upper()})",
size=10,
color=ft.Colors.GREY_600,
width=50,
),
])
)
files_container.content = ft.Column(
file_items,
scroll=ft.ScrollMode.AUTO,
spacing=2,
)
clear_button.disabled = False
batch_button.disabled = False
@staticmethod
def add_log_message(log_container, message, level="info"):
"""로그 메시지 추가"""
import datetime
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
if level == "error":
color = ft.Colors.RED_600
icon = ""
elif level == "warning":
color = ft.Colors.ORANGE_600
icon = "⚠️"
elif level == "success":
color = ft.Colors.GREEN_600
icon = ""
else:
color = ft.Colors.BLUE_600
icon = ""
log_text = ft.Text(
f"{icon} {timestamp} - {message}",
size=11,
color=color,
)
if log_container.content:
log_container.content.controls.append(log_text)
else:
log_container.content = ft.Column([log_text])
# 로그가 너무 많으면 오래된 것부터 제거
if len(log_container.content.controls) > 20:
log_container.content.controls = log_container.content.controls[-20:]
@staticmethod
def update_batch_progress(progress_bar, progress_text, status_text, timing_info,
current, total, status, elapsed_time, estimated_remaining):
"""배치 처리 진행률 업데이트"""
progress_value = current / total if total > 0 else 0
progress_bar.value = progress_value
progress_text.value = f"{current} / {total} 파일 처리됨"
status_text.value = status
elapsed_str = f"{int(elapsed_time // 3600):02d}:{int((elapsed_time % 3600) // 60):02d}:{int(elapsed_time % 60):02d}"
remaining_str = f"{int(estimated_remaining // 3600):02d}:{int((estimated_remaining % 3600) // 60):02d}:{int(estimated_remaining % 60):02d}"
timing_info.value = f"소요 시간: {elapsed_str} | 예상 남은 시간: {remaining_str}"
@staticmethod
def update_batch_results(summary_stats, results_table, results, save_csv_button,
save_cross_csv_button, save_excel_button, clear_results_button):
"""배치 결과 업데이트"""
if not results:
summary_stats.content.controls[1].value = "처리된 파일: 0개"
summary_stats.content.controls[2].value = "성공: 0개"
summary_stats.content.controls[3].value = "실패: 0개"
summary_stats.content.controls[4].value = "성공률: 0%"
results_table.rows = []
save_csv_button.disabled = True
save_cross_csv_button.disabled = True
save_excel_button.disabled = True
clear_results_button.disabled = True
return
# 요약 통계 업데이트
total_files = len(results)
success_count = sum(1 for r in results if r.success)
failed_count = total_files - success_count
success_rate = (success_count / total_files * 100) if total_files > 0 else 0
summary_stats.content.controls[1].value = f"처리된 파일: {total_files}"
summary_stats.content.controls[2].value = f"성공: {success_count}"
summary_stats.content.controls[3].value = f"실패: {failed_count}"
summary_stats.content.controls[4].value = f"성공률: {success_rate:.1f}%"
# 결과 테이블 업데이트
table_rows = []
for result in results:
status_text = "성공" if result.success else "실패"
status_color = ft.Colors.GREEN_600 if result.success else ft.Colors.RED_600
# 결과 요약 (처음 50자까지)
result_summary = ""
if hasattr(result, 'analysis_result') and result.analysis_result:
result_summary = str(result.analysis_result)[:50] + "..."
elif hasattr(result, 'error_message') and result.error_message:
result_summary = str(result.error_message)[:50] + "..."
table_rows.append(
ft.DataRow(
cells=[
ft.DataCell(ft.Text(result.file_name, size=12)),
ft.DataCell(ft.Text(result.file_type.upper(), size=12)),
ft.DataCell(ft.Text(status_text, size=12, color=status_color)),
ft.DataCell(ft.Text(f"{result.processing_time:.2f}s", size=12)),
ft.DataCell(ft.Text(result_summary, size=10, overflow=ft.TextOverflow.ELLIPSIS)),
]
)
)
results_table.rows = table_rows
# 버튼 활성화
save_csv_button.disabled = False
save_cross_csv_button.disabled = False
save_excel_button.disabled = False
clear_results_button.disabled = False