# -*- 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"""

{APP_NAME}

버전 {APP_VERSION}


전동차 운용실 업무 인수인계 및
고장관리 프로그램


© 2026 검수팀

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