484 lines
16 KiB
Python
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)
|
|
|