# -*- coding: utf-8 -*- """ 설정 다이얼로그 모듈 환경설정을 위한 다이얼로그입니다. """ from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QLabel, QFrame, QScrollArea, QSpinBox, QComboBox, QGridLayout, QGroupBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView ) from PySide6.QtCore import Qt from PySide6.QtGui import QFont, QColor from ui.base.base_dialog import BaseDialog from ui.components.custom_input import CustomLineEdit, CustomComboBox, LabeledInput from ui.components.toggle_switch import LabeledToggle from ui.components.custom_button import CustomButton from core.config import ConfigManager from core.signals import GlobalSignals from core.constants import FONT_FAMILY from core.logger import get_logger logger = get_logger(__name__) # 사용 가능한 폰트 목록 AVAILABLE_FONTS = [ "GmarketSans", "Pretendard", "Noto Sans KR", "Spoqa Han Sans Neo", "맑은 고딕", "나눔고딕", "Arial", "Segoe UI", ] # 폰트 굵기 옵션 FONT_WEIGHTS = ["normal", "medium", "bold"] class FontSettingRow(QWidget): """폰트 설정 행 위젯""" def __init__( self, label: str, initial_family: str = FONT_FAMILY, initial_size: int = 13, initial_weight: str = "normal", parent=None ): super().__init__(parent) self.config = ConfigManager() theme = self.config.theme is_dark = theme == 'dark' layout = QHBoxLayout(self) layout.setContentsMargins(0, 4, 0, 4) layout.setSpacing(8) # 레이블 self.label = QLabel(label) self.label.setFont(QFont("GmarketSans", 12)) self.label.setFixedWidth(100) self.label.setStyleSheet(f"color: {'#f8fafc' if is_dark else '#1e293b'};") layout.addWidget(self.label) # 폰트 패밀리 self.family_combo = QComboBox() self.family_combo.addItems(AVAILABLE_FONTS) self.family_combo.setCurrentText(initial_family) self.family_combo.setFixedWidth(150) self.family_combo.setFont(QFont("GmarketSans", 11)) layout.addWidget(self.family_combo) # 폰트 크기 self.size_spin = QSpinBox() self.size_spin.setRange(8, 32) self.size_spin.setValue(initial_size) self.size_spin.setSuffix("px") self.size_spin.setFixedWidth(70) self.size_spin.setFont(QFont("GmarketSans", 11)) layout.addWidget(self.size_spin) # 폰트 굵기 self.weight_combo = QComboBox() self.weight_combo.addItems(FONT_WEIGHTS) self.weight_combo.setCurrentText(initial_weight) self.weight_combo.setFixedWidth(80) self.weight_combo.setFont(QFont("GmarketSans", 11)) layout.addWidget(self.weight_combo) layout.addStretch() self._apply_style() def _apply_style(self): """스타일 적용""" theme = self.config.theme if theme == 'dark': bg = "#1e293b" text = "#f8fafc" border = "#475569" else: bg = "#ffffff" text = "#1e293b" border = "#e2e8f0" style = f""" QComboBox, QSpinBox {{ background-color: {bg}; color: {text}; border: 1px solid {border}; border-radius: 6px; padding: 4px 8px; }} QComboBox:hover, QSpinBox:hover {{ border-color: #3b82f6; }} QComboBox::drop-down {{ border: none; width: 20px; }} QComboBox QAbstractItemView {{ background-color: {bg}; color: {text}; border: 1px solid {border}; selection-background-color: #3b82f6; }} """ self.setStyleSheet(style) def get_values(self): """현재 값 반환""" return { "family": self.family_combo.currentText(), "size": self.size_spin.value(), "weight": self.weight_combo.currentText(), } def set_values(self, family: str, size: int, weight: str): """값 설정""" self.family_combo.setCurrentText(family) self.size_spin.setValue(size) self.weight_combo.setCurrentText(weight) class SettingsDialog(BaseDialog): """ 설정 다이얼로그 애플리케이션 환경설정을 위한 다이얼로그입니다. """ def __init__(self, parent=None): super().__init__(parent, title="환경설정", width=700, height=600, resizable=True) self._font_settings = {} # 영역별 폰트 설정 위젯 저장 self._setup_tabs() self.add_confirm_cancel_buttons("저장", "취소") def _setup_tabs(self): """탭 설정""" self.tabs = QTabWidget() self.tabs.setFont(QFont("GmarketSans", 12)) # 일반 탭 self.tabs.addTab(self._create_general_tab(), "일반") # 레이아웃 탭 self.tabs.addTab(self._create_layout_tab(), "레이아웃") # UI 폰트 탭 self.tabs.addTab(self._create_font_tab(), "폰트 설정") # 편성 설정 탭 self.tabs.addTab(self._create_train_tab(), "편성 설정") # 날씨 탭 self.tabs.addTab(self._create_weather_tab(), "날씨") # 스타일 적용 theme = self.config.theme if theme == 'dark': self.tabs.setStyleSheet(""" QTabWidget::pane { border: 1px solid #334155; border-radius: 8px; background-color: #1e293b; } QTabBar::tab { background-color: #1e293b; color: #94a3b8; padding: 8px 16px; border-top-left-radius: 6px; border-top-right-radius: 6px; } QTabBar::tab:selected { background-color: #334155; color: #f8fafc; } """) else: self.tabs.setStyleSheet(""" QTabWidget::pane { border: 1px solid #e2e8f0; border-radius: 8px; background-color: #ffffff; } QTabBar::tab { background-color: #f1f5f9; color: #64748b; padding: 8px 16px; border-top-left-radius: 6px; border-top-right-radius: 6px; } QTabBar::tab:selected { background-color: #ffffff; color: #1e293b; } """) self.content_layout.addWidget(self.tabs) def _create_general_tab(self) -> QWidget: """일반 탭 생성""" tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(16, 16, 16, 16) layout.setSpacing(16) # 테마 themes = ["다크", "라이트"] self.theme_combo = CustomComboBox(items=themes) current_theme = "다크" if self.config.theme == 'dark' else "라이트" self.theme_combo.set_selected_value(current_theme) layout.addWidget(LabeledInput("테마", self.theme_combo)) # 자동 저장 self.auto_save_toggle = LabeledToggle( "자동 저장", initial_state=self.config.get('app', 'auto_save', True) ) layout.addWidget(self.auto_save_toggle) # 업데이트 확인 self.update_toggle = LabeledToggle( "업데이트 자동 확인", initial_state=self.config.get('app', 'check_updates', True) ) layout.addWidget(self.update_toggle) layout.addStretch() return tab def _create_layout_tab(self) -> QWidget: """레이아웃 탭 생성""" tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(16, 16, 16, 16) layout.setSpacing(16) # 섹션/Todo 비율 info = QLabel("섹션 패널과 Todo 패널의 비율을 설정합니다.\n드래그로도 조절할 수 있습니다.") info.setFont(QFont("GmarketSans", 11)) info.setWordWrap(True) theme = self.config.theme info.setStyleSheet(f"color: {'#94a3b8' if theme == 'dark' else '#64748b'};") layout.addWidget(info) # 섹션 비율 self.section_ratio_input = CustomLineEdit( placeholder="70" ) self.section_ratio_input.setText( str(int(self.config.get('layout', 'section_panel_ratio', 70))) ) layout.addWidget(LabeledInput("섹션 패널 비율 (%)", self.section_ratio_input)) layout.addStretch() return tab def _create_font_tab(self) -> QWidget: """UI 폰트 설정 탭 생성""" tab = QWidget() main_layout = QVBoxLayout(tab) main_layout.setContentsMargins(8, 8, 8, 8) main_layout.setSpacing(8) # 스크롤 영역 scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) content = QWidget() layout = QVBoxLayout(content) layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(16) theme = self.config.theme is_dark = theme == 'dark' # 설명 info = QLabel("각 UI 영역별 폰트를 설정합니다.") info.setFont(QFont("GmarketSans", 11)) info.setStyleSheet(f"color: {'#94a3b8' if is_dark else '#64748b'};") layout.addWidget(info) # 영역별 폰트 설정 font_areas = [ ("인포바", [ ("info_bar", "title", "제목"), ("info_bar", "content", "내용"), ]), ("섹션 (지시/고장/작업/기타)", [ ("section", "title", "제목"), ("section", "header", "테이블 헤더"), ("section", "content", "테이블 내용"), ]), ("할일 목록", [ ("todo", "title", "제목"), ("todo", "content", "내용"), ]), ("메모", [ ("memo", "title", "제목"), ("memo", "content", "내용"), ]), ("일상검수", [ ("daily", "title", "제목"), ("daily", "content", "내용"), ("daily", "train", "편성번호"), ]), ("상태바", [ ("status", "content", "내용"), ]), ("다이얼로그", [ ("dialog", "title", "제목"), ("dialog", "label", "레이블"), ("dialog", "input", "입력"), ("dialog", "button", "버튼"), ]), ] for area_name, settings in font_areas: group = QGroupBox(area_name) group.setFont(QFont("GmarketSans", 12, QFont.Bold)) if is_dark: group.setStyleSheet(""" QGroupBox { color: #f8fafc; border: 1px solid #334155; border-radius: 8px; margin-top: 12px; padding-top: 8px; } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; } """) else: group.setStyleSheet(""" QGroupBox { color: #1e293b; border: 1px solid #e2e8f0; border-radius: 8px; margin-top: 12px; padding-top: 8px; } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; } """) group_layout = QVBoxLayout(group) group_layout.setContentsMargins(12, 16, 12, 12) group_layout.setSpacing(4) for area, style, label in settings: # 현재 설정값 가져오기 current = self.config.get_ui_font(area, style) row = FontSettingRow( label, initial_family=current.get("family", FONT_FAMILY), initial_size=current.get("size", 13), initial_weight=current.get("weight", "normal"), ) group_layout.addWidget(row) # 저장을 위해 참조 보관 key = f"{area}_{style}" self._font_settings[key] = row layout.addWidget(group) layout.addStretch() # 초기화 버튼 reset_btn = CustomButton("폰트 설정 초기화", style_type="secondary") reset_btn.clicked.connect(self._reset_font_settings) layout.addWidget(reset_btn) scroll.setWidget(content) # 스크롤바 스타일 if is_dark: scroll.setStyleSheet(""" QScrollArea { border: none; background-color: #1e293b; } QScrollBar:vertical { background-color: #1e293b; width: 10px; border-radius: 5px; } QScrollBar::handle:vertical { background-color: #475569; border-radius: 5px; min-height: 30px; } QScrollBar::handle:vertical:hover { background-color: #64748b; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } """) else: scroll.setStyleSheet(""" QScrollArea { border: none; background-color: #ffffff; } QScrollBar:vertical { background-color: #f1f5f9; width: 10px; border-radius: 5px; } QScrollBar::handle:vertical { background-color: #cbd5e1; border-radius: 5px; min-height: 30px; } QScrollBar::handle:vertical:hover { background-color: #94a3b8; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } """) main_layout.addWidget(scroll) return tab def _create_train_tab(self) -> QWidget: """편성 설정 탭 생성""" tab = QWidget() main_layout = QVBoxLayout(tab) main_layout.setContentsMargins(16, 16, 16, 16) main_layout.setSpacing(12) theme = self.config.theme is_dark = theme == 'dark' text_color = "#f8fafc" if is_dark else "#1e293b" sub_color = "#94a3b8" if is_dark else "#64748b" # 설명 info = QLabel("각 편성의 차량 유형을 설정합니다.\nA: 구형차량 (파란색), B: 신형차량 (주황색)") info.setFont(QFont("GmarketSans", 11)) info.setStyleSheet(f"color: {sub_color};") info.setWordWrap(True) main_layout.addWidget(info) # 일괄 설정 버튼 btn_container = QWidget() btn_layout = QHBoxLayout(btn_container) btn_layout.setContentsMargins(0, 0, 0, 0) btn_layout.setSpacing(8) all_a_btn = QPushButton("전체 A(구형)") all_a_btn.setFont(QFont("GmarketSans", 11)) all_a_btn.setCursor(Qt.PointingHandCursor) all_a_btn.clicked.connect(lambda: self._set_all_trains('A')) all_a_btn.setStyleSheet(f""" QPushButton {{ background-color: #3b82f6; color: white; border: none; border-radius: 6px; padding: 8px 16px; }} QPushButton:hover {{ background-color: #2563eb; }} """) btn_layout.addWidget(all_a_btn) all_b_btn = QPushButton("전체 B(신형)") all_b_btn.setFont(QFont("GmarketSans", 11)) all_b_btn.setCursor(Qt.PointingHandCursor) all_b_btn.clicked.connect(lambda: self._set_all_trains('B')) all_b_btn.setStyleSheet(f""" QPushButton {{ background-color: #f97316; color: white; border: none; border-radius: 6px; padding: 8px 16px; }} QPushButton:hover {{ background-color: #ea580c; }} """) btn_layout.addWidget(all_b_btn) reset_btn = QPushButton("기본값으로 초기화") reset_btn.setFont(QFont("GmarketSans", 11)) reset_btn.setCursor(Qt.PointingHandCursor) reset_btn.clicked.connect(self._reset_train_settings) reset_btn.setStyleSheet(f""" QPushButton {{ background-color: {'#334155' if is_dark else '#e2e8f0'}; color: {text_color}; border: none; border-radius: 6px; padding: 8px 16px; }} QPushButton:hover {{ background-color: {'#475569' if is_dark else '#cbd5e1'}; }} """) btn_layout.addWidget(reset_btn) btn_layout.addStretch() main_layout.addWidget(btn_container) # 편성 테이블 self.train_table = QTableWidget() self.train_table.setColumnCount(6) self.train_table.setHorizontalHeaderLabels(["편성", "유형", "표시", "편성", "유형", "표시"]) self.train_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.train_table.verticalHeader().setVisible(False) self.train_table.setSelectionMode(QTableWidget.NoSelection) self.train_table.setEditTriggers(QTableWidget.NoEditTriggers) # 테이블 스타일 if is_dark: self.train_table.setStyleSheet(""" QTableWidget { background-color: #1e293b; color: #f8fafc; border: 1px solid #334155; border-radius: 8px; gridline-color: #334155; } QTableWidget::item { padding: 4px; } QHeaderView::section { background-color: #334155; color: #f8fafc; padding: 8px; border: none; font-weight: bold; } """) else: self.train_table.setStyleSheet(""" QTableWidget { background-color: #ffffff; color: #1e293b; border: 1px solid #e2e8f0; border-radius: 8px; gridline-color: #e2e8f0; } QTableWidget::item { padding: 4px; } QHeaderView::section { background-color: #f1f5f9; color: #1e293b; padding: 8px; border: none; font-weight: bold; } """) # 테이블 데이터 채우기 self._load_train_table() main_layout.addWidget(self.train_table, 1) return tab def _load_train_table(self): """편성 테이블 데이터 로드""" train_config = self.config.settings.train # 51개 편성을 2열로 표시 (왼쪽 26개, 오른쪽 25개) rows = 26 self.train_table.setRowCount(rows) for row in range(rows): # 왼쪽 열 (1~26편성) train_num = row + 1 if train_num <= 51: self._set_train_row(row, 0, train_num, train_config) # 오른쪽 열 (27~51편성) train_num = row + 27 if train_num <= 51: self._set_train_row(row, 3, train_num, train_config) def _set_train_row(self, row: int, col_offset: int, train_num: int, train_config): """테이블 한 행 설정""" key = f"train_{train_num}_type" train_type = getattr(train_config, key, 'A') display_num = 100 + train_num display = f"{display_num}{train_type}" # 편성 번호 num_item = QTableWidgetItem(str(train_num)) num_item.setTextAlignment(Qt.AlignCenter) self.train_table.setItem(row, col_offset, num_item) # 유형 토글 버튼 type_btn = QPushButton(train_type) type_btn.setFont(QFont("GmarketSans", 11, QFont.Bold)) type_btn.setCursor(Qt.PointingHandCursor) type_btn.setProperty("train_num", train_num) type_btn.clicked.connect(lambda checked, btn=type_btn: self._toggle_train_type(btn)) self._update_train_btn_style(type_btn, train_type) self.train_table.setCellWidget(row, col_offset + 1, type_btn) # 표시 display_item = QTableWidgetItem(display) display_item.setTextAlignment(Qt.AlignCenter) if train_type == 'A': display_item.setForeground(QColor("#3b82f6")) else: display_item.setForeground(QColor("#f97316")) self.train_table.setItem(row, col_offset + 2, display_item) def _update_train_btn_style(self, btn: QPushButton, train_type: str): """편성 버튼 스타일 업데이트""" if train_type == 'A': btn.setStyleSheet(""" QPushButton { background-color: #3b82f6; color: white; border: none; border-radius: 4px; padding: 4px 8px; min-width: 30px; } QPushButton:hover { background-color: #2563eb; } """) else: btn.setStyleSheet(""" QPushButton { background-color: #f97316; color: white; border: none; border-radius: 4px; padding: 4px 8px; min-width: 30px; } QPushButton:hover { background-color: #ea580c; } """) def _toggle_train_type(self, btn: QPushButton): """편성 유형 토글""" train_num = btn.property("train_num") current = btn.text() new_type = 'B' if current == 'A' else 'A' btn.setText(new_type) self._update_train_btn_style(btn, new_type) # 설정 업데이트 key = f"train_{train_num}_type" setattr(self.config.settings.train, key, new_type) # 표시 업데이트 self._load_train_table() def _set_all_trains(self, train_type: str): """모든 편성 유형 일괄 설정""" train_config = self.config.settings.train for i in range(1, 52): key = f"train_{i}_type" setattr(train_config, key, train_type) self._load_train_table() def _reset_train_settings(self): """편성 설정 초기화""" self.config.reset_to_default('train') self._load_train_table() self.signals.status_message.emit("편성 설정이 초기화되었습니다.", 2000) def _create_weather_tab(self) -> QWidget: """날씨 탭 생성""" tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(16, 16, 16, 16) layout.setSpacing(16) theme = self.config.theme is_dark = theme == 'dark' sub_color = "#94a3b8" if is_dark else "#64748b" # 설명 info = QLabel("날씨 정보 표시 및 지역 설정을 관리합니다.") info.setFont(QFont("GmarketSans", 11)) info.setStyleSheet(f"color: {sub_color};") info.setWordWrap(True) layout.addWidget(info) # 날씨 활성화 self.weather_toggle = LabeledToggle( "날씨 정보 표시", initial_state=self.config.get('weather', 'enabled', True) ) layout.addWidget(self.weather_toggle) # API 키 self.api_key_input = CustomLineEdit( placeholder="OpenWeatherMap API 키" ) self.api_key_input.setText(self.config.get('weather', 'api_key', '')) layout.addWidget(LabeledInput("API 키", self.api_key_input)) # 지역 설정 from ui.dialogs.weather_location_dialog import CITIES self.location_combo = CustomComboBox(items=CITIES) current_location = self.config.get('weather', 'location_name', '부산') if current_location in CITIES: self.location_combo.set_selected_value(current_location) else: self.location_combo.set_selected_value('부산') layout.addWidget(LabeledInput("지역", self.location_combo)) # 예보 단위 설정 forecast_units = ["1시간 단위", "3시간 단위"] self.forecast_unit_combo = CustomComboBox(items=forecast_units) current_unit = self.config.get('weather', 'forecast_unit', '1시간 단위') if current_unit in forecast_units: self.forecast_unit_combo.set_selected_value(current_unit) else: self.forecast_unit_combo.set_selected_value('1시간 단위') layout.addWidget(LabeledInput("예보 단위", self.forecast_unit_combo)) # 업데이트 주기 update_intervals = ["10분", "30분", "1시간", "2시간"] self.update_interval_combo = CustomComboBox(items=update_intervals) current_interval_sec = self.config.get('weather', 'update_interval', 1800) current_interval_min = current_interval_sec // 60 if current_interval_min == 10: interval_str = "10분" elif current_interval_min == 30: interval_str = "30분" elif current_interval_min == 60: interval_str = "1시간" elif current_interval_min == 120: interval_str = "2시간" else: interval_str = "30분" self.update_interval_combo.set_selected_value(interval_str) layout.addWidget(LabeledInput("업데이트 주기", self.update_interval_combo)) layout.addStretch() return tab def _reset_font_settings(self): """폰트 설정 초기화""" self.config.reset_to_default('ui_font') # UI 업데이트 for key, row in self._font_settings.items(): area, style = key.rsplit('_', 1) current = self.config.get_ui_font(area, style) row.set_values( current.get("family", FONT_FAMILY), current.get("size", 13), current.get("weight", "normal") ) self.signals.status_message.emit("폰트 설정이 초기화되었습니다.", 2000) def _on_confirm(self): """저장 버튼 클릭""" # 일반 설정 theme = 'dark' if self.theme_combo.currentText() == "다크" else 'light' self.config.theme = theme self.config.set('app', 'auto_save', self.auto_save_toggle.is_on) self.config.set('app', 'check_updates', self.update_toggle.is_on) # 레이아웃 설정 try: section_ratio = int(self.section_ratio_input.text()) section_ratio = max(30, min(80, section_ratio)) self.config.set('layout', 'section_panel_ratio', section_ratio) self.config.set('layout', 'todo_panel_ratio', 100 - section_ratio) except ValueError: pass # 폰트 설정 for key, row in self._font_settings.items(): area, style = key.rsplit('_', 1) values = row.get_values() self.config.set_ui_font( area, style, family=values["family"], size=values["size"], weight=values["weight"] ) # 날씨 설정 self.config.set('weather', 'enabled', self.weather_toggle.is_on) self.config.set('weather', 'api_key', self.api_key_input.text()) # 지역 설정 selected_city = self.location_combo.currentText() self.config.set('weather', 'location_name', selected_city) # 지역 좌표 업데이트 from ui.dialogs.weather_location_dialog import CITY_COORDINATES if selected_city in CITY_COORDINATES: lat, lon = CITY_COORDINATES[selected_city] self.config.set('weather', 'location_lat', lat) self.config.set('weather', 'location_lon', lon) # 예보 단위 설정 forecast_unit = self.forecast_unit_combo.currentText() self.config.set('weather', 'forecast_unit', forecast_unit) # 업데이트 주기 설정 interval_str = self.update_interval_combo.currentText() if interval_str == "10분": interval_sec = 600 elif interval_str == "30분": interval_sec = 1800 elif interval_str == "1시간": interval_sec = 3600 elif interval_str == "2시간": interval_sec = 7200 else: interval_sec = 1800 self.config.set('weather', 'update_interval', interval_sec) # 저장 self.config.save() # 테마 변경 시그널 self.signals.theme_changed.emit(theme) self.signals.layout_changed.emit() self.accept()