handOver2/ui/main_window.py

484 lines
16 KiB
Python

# -*- coding: utf-8 -*-
"""
메인 윈도우 모듈
애플리케이션의 메인 윈도우를 정의합니다.
이 모듈은 다음을 포함합니다:
- 메인 윈도우 레이아웃
- 메뉴바
- 인포바, 컨텐츠, 상태바 통합
"""
import sys
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QMenuBar, QMenu,
QSystemTrayIcon, QApplication, QMessageBox
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QIcon, QAction, QFont, QFontDatabase
from ui.panels.info_bar import InfoBar
from ui.panels.content_panel import ContentPanel
from ui.panels.status_bar import StatusBar
from ui.dialogs.login_dialog import LoginDialog
from ui.dialogs.settings_dialog import SettingsDialog
from core.config import ConfigManager
from core.signals import GlobalSignals
from core.constants import APP_NAME, APP_VERSION, FONTS_DIR
from core.logger import get_logger, setup_logger
from database.db_manager import DatabaseManager
from services.auth_service import AuthService
from services.weather_service import WeatherService
from services.update_service import UpdateService
# 로거 설정
logger = get_logger(__name__)
class MainWindow(QMainWindow):
"""
메인 윈도우 클래스
애플리케이션의 메인 윈도우입니다.
인포바, 컨텐츠 패널, 상태바를 포함합니다.
Examples:
>>> app = QApplication(sys.argv)
>>> window = MainWindow()
>>> window.show()
>>> app.exec()
"""
def __init__(self, config_manager: ConfigManager = None):
super().__init__()
# 설정 및 시그널
self.config = config_manager or ConfigManager()
self.signals = GlobalSignals()
# 서비스 초기화
self._init_services()
# 폰트 로드
self._load_fonts()
# UI 설정
self._setup_window()
self._setup_menu()
self._setup_ui()
# 시그널 연결
self._connect_signals()
# 초기 데이터 로드
QTimer.singleShot(100, self._initial_load)
logger.info("메인 윈도우 초기화 완료")
def _init_services(self):
"""서비스 초기화"""
# 데이터베이스
self.db = DatabaseManager()
# 인증 서비스
self.auth = AuthService()
# 날씨 서비스
self.weather = WeatherService()
# 업데이트 서비스
self.update_service = UpdateService()
def _load_fonts(self):
"""폰트 로드"""
# GmarketSans 폰트 로드 시도
font_paths = [
FONTS_DIR / "GmarketSans" / "GmarketSansTTFLight.ttf",
FONTS_DIR / "GmarketSans" / "GmarketSansTTFMedium.ttf",
FONTS_DIR / "GmarketSans" / "GmarketSansTTFBold.ttf",
]
for path in font_paths:
if path.exists():
font_id = QFontDatabase.addApplicationFont(str(path))
if font_id >= 0:
logger.debug(f"폰트 로드: {path.name}")
# 기본 폰트 설정
app = QApplication.instance()
if app:
font = QFont("GmarketSans", 12)
app.setFont(font)
def _setup_window(self):
"""윈도우 설정"""
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 윈도우 크기
width = self.config.get('layout', 'window_width', 1600)
height = self.config.get('layout', 'window_height', 900)
self.resize(width, height)
# 윈도우 위치
x = self.config.get('layout', 'window_x', -1)
y = self.config.get('layout', 'window_y', -1)
if x >= 0 and y >= 0:
self.move(x, y)
else:
# 화면 중앙
screen = QApplication.primaryScreen().geometry()
self.move(
(screen.width() - width) // 2,
(screen.height() - height) // 2
)
# 테마 적용
self._apply_theme()
def _setup_menu(self):
"""메뉴바 설정"""
menubar = self.menuBar()
menubar.setFont(QFont("GmarketSans", 11))
# 파일 메뉴
file_menu = menubar.addMenu("파일(&F)")
refresh_action = QAction("새로고침", self)
refresh_action.setShortcut("F5")
refresh_action.triggered.connect(self._on_refresh)
file_menu.addAction(refresh_action)
file_menu.addSeparator()
exit_action = QAction("종료(&X)", self)
exit_action.setShortcut("Alt+F4")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 편집 메뉴
edit_menu = menubar.addMenu("편집(&E)")
# 전동차 편성관리
train_formation_action = QAction("전동차 편성관리...", self)
train_formation_action.triggered.connect(self._on_train_formation_management)
edit_menu.addAction(train_formation_action)
# 보기 메뉴
view_menu = menubar.addMenu("보기(&V)")
# 설정 메뉴
settings_menu = menubar.addMenu("설정(&S)")
settings_action = QAction("환경설정...", self)
settings_action.triggered.connect(self._on_settings)
settings_menu.addAction(settings_action)
ui_settings_action = QAction("UI 설정...", self)
ui_settings_action.triggered.connect(self._on_ui_settings)
settings_menu.addAction(ui_settings_action)
# 날씨 지역 설정
weather_location_action = QAction("날씨 지역 설정...", self)
weather_location_action.triggered.connect(self._on_weather_location_settings)
settings_menu.addAction(weather_location_action)
settings_menu.addSeparator()
# 테마 서브메뉴
theme_menu = settings_menu.addMenu("테마")
dark_action = QAction("다크 테마", self)
dark_action.setCheckable(True)
dark_action.setChecked(self.config.theme == 'dark')
dark_action.triggered.connect(lambda: self._change_theme('dark'))
theme_menu.addAction(dark_action)
light_action = QAction("라이트 테마", self)
light_action.setCheckable(True)
light_action.setChecked(self.config.theme == 'light')
light_action.triggered.connect(lambda: self._change_theme('light'))
theme_menu.addAction(light_action)
self._theme_actions = [dark_action, light_action]
# 도움말 메뉴
help_menu = menubar.addMenu("도움말(&H)")
about_action = QAction("정보", self)
about_action.triggered.connect(self._on_about)
help_menu.addAction(about_action)
check_update_action = QAction("업데이트 확인", self)
check_update_action.triggered.connect(self._on_check_update)
help_menu.addAction(check_update_action)
def _setup_ui(self):
"""UI 설정"""
# 중앙 위젯
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 메인 레이아웃
layout = QVBoxLayout(central_widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# 인포바 (상단 10%)
self.info_bar = InfoBar()
layout.addWidget(self.info_bar)
# 컨텐츠 패널 (중앙 80%)
self.content_panel = ContentPanel()
layout.addWidget(self.content_panel, 1)
# 상태바 (하단 10%)
self.status_bar = StatusBar()
layout.addWidget(self.status_bar)
def _apply_theme(self):
"""테마 적용"""
theme = self.config.theme
if theme == 'dark':
bg = "#0f172a"
text = "#f8fafc"
border = "#334155"
menu_bg = "#1e293b"
menu_hover = "#334155"
else:
bg = "#f8fafc"
text = "#1e293b"
border = "#e2e8f0"
menu_bg = "#ffffff"
menu_hover = "#f1f5f9"
self.setStyleSheet(f"""
QMainWindow {{
background-color: {bg};
}}
QMenuBar {{
background-color: {menu_bg};
color: {text};
border-bottom: 1px solid {border};
padding: 4px;
}}
QMenuBar::item {{
padding: 6px 12px;
border-radius: 4px;
}}
QMenuBar::item:selected {{
background-color: {menu_hover};
}}
QMenu {{
background-color: {menu_bg};
color: {text};
border: 1px solid {border};
border-radius: 8px;
padding: 4px;
}}
QMenu::item {{
padding: 8px 24px;
border-radius: 4px;
}}
QMenu::item:selected {{
background-color: {menu_hover};
}}
QMenu::separator {{
height: 1px;
background-color: {border};
margin: 4px 8px;
}}
""")
def _connect_signals(self):
"""시그널 연결"""
# 테마 변경
self.signals.theme_changed.connect(self._on_theme_changed)
# 앱 종료 요청
self.signals.app_quit_requested.connect(self.close)
# 날씨 업데이트
self.signals.weather_updated.connect(self._on_weather_updated)
# 날씨 지역 변경
self.signals.weather_location_changed.connect(self._on_weather_location_changed)
# 날씨 새로고침 요청
self.signals.weather_refresh_requested.connect(self._on_weather_refresh_requested)
# 업데이트 가능
self.signals.update_available.connect(self._on_update_available)
def _initial_load(self):
"""초기 데이터 로드"""
# 컨텐츠 로드
self.content_panel.refresh_all()
# 날씨 업데이트 시작
self.weather.start()
# 업데이트 확인
if self.config.get('app', 'check_updates', True):
self.update_service.check_for_updates()
# 상태바 메시지
self.status_bar.show_message("준비 완료", 3000)
# ========================================================================
# 이벤트 핸들러
# ========================================================================
def _on_refresh(self):
"""새로고침"""
self.content_panel.refresh_all()
self.status_bar.show_message("새로고침 완료", 2000)
def _on_settings(self):
"""설정 다이얼로그"""
dialog = SettingsDialog(self)
dialog.exec()
def _on_ui_settings(self):
"""UI 설정 다이얼로그"""
# 환경설정과 동일한 다이얼로그 사용
dialog = SettingsDialog(self)
dialog.exec()
def _on_about(self):
"""정보 다이얼로그"""
from ui.base.base_dialog import BaseDialog
from PySide6.QtWidgets import QLabel
from PySide6.QtCore import Qt
dialog = BaseDialog(self, title="정보", width=350, height=250)
info = QLabel(f"""
<div style="text-align: center;">
<h2>{APP_NAME}</h2>
<p>버전 {APP_VERSION}</p>
<br>
<p>전동차 운용실 업무 인수인계 및<br>고장관리 프로그램</p>
<br>
<p style="color: #64748b;">© 2026 검수팀</p>
</div>
""")
info.setAlignment(Qt.AlignCenter)
dialog.content_layout.addWidget(info)
dialog.add_button("확인", dialog.accept, primary=True)
dialog.exec()
def _on_check_update(self):
"""업데이트 확인"""
self.update_service.check_for_updates(show_no_update=True)
def _on_weather_location_settings(self):
"""날씨 지역 설정"""
from ui.dialogs.weather_location_dialog import WeatherLocationDialog
dialog = WeatherLocationDialog(self)
if dialog.exec():
# 날씨 정보 다시 로드
self._on_weather_location_changed()
self.status_bar.show_message("날씨 지역이 변경되었습니다.", 2000)
def _on_train_formation_management(self):
"""전동차 편성관리 다이얼로그"""
from ui.dialogs.train_formation_dialog import TrainFormationDialog
dialog = TrainFormationDialog(self)
dialog.exec()
def _on_weather_location_changed(self):
"""날씨 지역 변경 시그널 수신"""
# 날씨 정보 강제 새로고침 (지역 변경 시)
self.weather.update_weather(force_refresh=True)
def _on_weather_refresh_requested(self):
"""날씨 새로고침 요청"""
self.weather.refresh()
self.status_bar.show_message("날씨 정보 새로고침 중...", 2000)
def _change_theme(self, theme: str):
"""테마 변경"""
self.config.theme = theme
self.config.save()
self.signals.theme_changed.emit(theme)
# 체크 상태 업데이트
for action in self._theme_actions:
if action.text() == "다크 테마":
action.setChecked(theme == 'dark')
else:
action.setChecked(theme == 'light')
def _on_theme_changed(self, theme: str):
"""테마 변경 시그널 수신"""
self._apply_theme()
# 상태바 메시지
theme_name = "다크" if theme == 'dark' else "라이트"
self.status_bar.show_message(f"{theme_name} 테마로 변경되었습니다.", 2000)
def _on_weather_updated(self, weather_json: str):
"""날씨 업데이트 수신"""
import json
try:
data = json.loads(weather_json)
self.info_bar.update_weather(data)
except json.JSONDecodeError:
pass
def _on_update_available(self, version: str):
"""업데이트 가능 시그널 수신"""
reply = QMessageBox.question(
self,
"업데이트 가능",
f"새로운 버전(v{version})이 있습니다.\n지금 업데이트하시겠습니까?\n\n(업데이트 시 프로그램이 재시작됩니다.)",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self.update_service.trigger_update()
# ========================================================================
# 윈도우 이벤트
# ========================================================================
def closeEvent(self, event):
"""윈도우 닫힘 이벤트"""
# 윈도우 크기/위치 저장
self.config.set('layout', 'window_width', self.width())
self.config.set('layout', 'window_height', self.height())
self.config.set('layout', 'window_x', self.x())
self.config.set('layout', 'window_y', self.y())
self.config.save()
# 서비스 정리
self.weather.stop()
# 데이터베이스 연결 종료
self.db.close()
logger.info("애플리케이션 종료")
event.accept()
def resizeEvent(self, event):
"""윈도우 크기 변경 이벤트"""
super().resizeEvent(event)
def moveEvent(self, event):
"""윈도우 이동 이벤트"""
super().moveEvent(event)