diff --git a/CHANGELOG.md b/CHANGELOG.md index 092e4f5..9ac19cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2.11.1] — 2026-06-04 + +### Fixed +- **빌드(main.exe)에서 통계 차트가 표시되지 않던 문제** — frozen 빌드는 PyInstaller가 + matplotlib `QtAgg`(backend_qtagg)만 번들하는데 `chart_widget`이 `backend_qt5agg`를 + import해 실패 → "matplotlib 필요" 폴백만 보였음. **backend_qtagg 우선 import**(+ qt5agg + 폴백) + 실패 원인 로깅, `main.spec`에 `backend_qtagg`/`PyQt5.sip` 명시. +- **통계·도움말·도전과제 화면이 라이트 테마에서도 다크로 고정되던 문제** — `dark_components`와 + 세 화면(+통계 차트 배경/그리드/텍스트)을 현재 테마(`ThemeColors`)에 따르도록 변경. + 다크 기본값은 그대로, 라이트 전환 시 함께 라이트로. 다크 등급 카드/차트 막대 등 강조색은 유지. + ## [2.11.0] — 2026-06-04 ### Changed — UI 전면 다크 리디자인 diff --git a/core/version.py b/core/version.py index 7675549..7db5bba 100644 --- a/core/version.py +++ b/core/version.py @@ -4,4 +4,4 @@ 릴리스 시 이 값을 올린 후 git tag → push. CHANGELOG.md의 최상단 항목과 일치시킬 것. """ -__version__ = '2.11.0' +__version__ = '2.11.1' diff --git a/main.spec b/main.spec index 45fadd9..69fd201 100644 --- a/main.spec +++ b/main.spec @@ -32,8 +32,10 @@ a = Analysis( hiddenimports=[ 'holidays', 'holidays.countries.south_korea', 'win32evtlog', 'win32evtlogutil', + 'matplotlib.backends.backend_qtagg', # frozen 차트 백엔드 (chart_widget 우선 import) 'matplotlib.backends.backend_qt5agg', 'PyQt5.QtSvg', + 'PyQt5.sip', # matplotlib qt_compat가 sip 사용 ], hookspath=[], hooksconfig={}, diff --git a/ui/achievements_view.py b/ui/achievements_view.py index 6ce9b12..3d4e832 100644 --- a/ui/achievements_view.py +++ b/ui/achievements_view.py @@ -17,6 +17,7 @@ from PyQt5.QtGui import QFont from core.achievements import get_all_with_status, get_stats from ui.styles import apply_dark_titlebar +from ui.dark_components import tc, tabs_qss, button_qss, scroll_qss, ACCENT_GOLD, _is_dark # 등급별 색상 팔레트 (배경 그라디언트, 외곽선, 강조) @@ -89,9 +90,9 @@ class AchievementsView(QDialog): self.setMinimumSize(960, 720) self.resize(1100, 800) self._increment_view_count() - self.setStyleSheet("QDialog { background: #1A1B1E; }") + self.setStyleSheet(f"QDialog {{ background: {tc('bg')}; }}") self.init_ui() - apply_dark_titlebar(self, dark=True) + apply_dark_titlebar(self) # 현재 테마에 맞춰 def _increment_view_count(self) -> None: try: @@ -136,14 +137,7 @@ class AchievementsView(QDialog): btn_row.addStretch() close_btn = QPushButton("닫기") close_btn.setMinimumWidth(100) - close_btn.setStyleSheet(""" - QPushButton { - background: #2a2a36; color: #e0e0e8; - border: 1px solid #44446a; border-radius: 6px; - padding: 8px 20px; font-size: 10pt; - } - QPushButton:hover { background: #3a3a4a; border-color: #6b9eff; } - """) + close_btn.setStyleSheet(button_qss('default')) close_btn.clicked.connect(self.accept) btn_row.addWidget(close_btn) layout.addLayout(btn_row) @@ -153,14 +147,13 @@ class AchievementsView(QDialog): # ----- 헤더 ----- def _build_header(self, stats: dict) -> QWidget: container = QFrame() - container.setStyleSheet(""" - QFrame { - background: qlineargradient(x1:0, y1:0, x2:1, y2:0, - stop:0 #1a1a30, stop:1 #2a1a3a); - border: 1px solid #2C2E33; + container.setStyleSheet(f""" + QFrame {{ + background: {tc('panel')}; + border: 1px solid {tc('border')}; border-radius: 12px; - } - QLabel { background: transparent; border: none; color: #e8e8f4; } + }} + QLabel {{ background: transparent; border: none; color: {tc('text')}; }} """) layout = QVBoxLayout() layout.setContentsMargins(20, 16, 20, 16) @@ -173,21 +166,21 @@ class AchievementsView(QDialog): num_row.setSpacing(24) big = QLabel(f"{stats['earned']}" - f" / {stats['total']}") + f" / {stats['total']}") big.setTextFormat(Qt.RichText) num_row.addWidget(big) spacer = QFrame() spacer.setFrameShape(QFrame.VLine) - spacer.setStyleSheet("color: #2C2E33;") + spacer.setStyleSheet(f"color: {tc('border')};") num_row.addWidget(spacer) secret_lbl = QLabel( f"
" - f"🌑 시크릿
" + f"🌑 시크릿
" f"" f"{stats['secret_earned']}" - f" / {stats['secret_total']}" + f" / {stats['secret_total']}" f"
" ) secret_lbl.setTextFormat(Qt.RichText) @@ -197,7 +190,7 @@ class AchievementsView(QDialog): pct_lbl = QLabel( f"
" - f"달성률
" + f"달성률
" f"" f"{pct:.1f}%
" ) @@ -235,17 +228,7 @@ class AchievementsView(QDialog): def _build_grid_tab(self, items: list, secret_mode: bool = False) -> QWidget: scroll = QScrollArea() scroll.setWidgetResizable(True) - scroll.setStyleSheet(""" - QScrollArea { background: transparent; border: none; } - QScrollBar:vertical { - background: #1a1a24; width: 10px; border-radius: 5px; - } - QScrollBar::handle:vertical { - background: #44446a; border-radius: 5px; min-height: 30px; - } - QScrollBar::handle:vertical:hover { background: #6b9eff; } - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; } - """) + scroll.setStyleSheet(scroll_qss()) container = QWidget() container.setStyleSheet("background: transparent;") grid = QGridLayout() @@ -256,7 +239,7 @@ class AchievementsView(QDialog): empty = QLabel("(아직 없음)") empty.setAlignment(Qt.AlignCenter) empty.setStyleSheet( - "color: #6C6E73; padding: 60px; font-size: 12pt; background: transparent;" + f"color: {tc('text_faint')}; padding: 60px; font-size: 12pt; background: transparent;" ) grid.addWidget(empty, 0, 0) else: @@ -279,11 +262,18 @@ class AchievementsView(QDialog): tier = item['tier'] or 'bronze' theme = TIER_THEMES.get(tier, TIER_THEMES['bronze']) - # 시크릿 미발견은 회색 톤으로 + # 라이트 테마: 카드 배경을 패널색으로(등급색은 보더/강조로 유지), 다크: 등급 그라디언트 + light = not _is_dark() if is_locked_secret: - bg_top, bg_bot = '#1a1a26', '#0e0e16' - border = '#3a3a4a' - text_color = '#6C6E73' + if light: + bg_top = bg_bot = tc('panel'); border = tc('border') + else: + bg_top, bg_bot = '#1a1a26', '#0e0e16'; border = '#3a3a4a' + text_color = tc('text_faint') + elif light: + bg_top = bg_bot = tc('panel') + border = theme['border_strong'] if is_earned else theme['border'] + text_color = tc('text') if is_earned else tc('text_dim') else: bg_top = theme['bg_top'] bg_bot = theme['bg_bot'] @@ -342,7 +332,7 @@ class AchievementsView(QDialog): name = QLabel(name_text) name.setStyleSheet( f"font-size: 12pt; font-weight: bold; " - f"color: {'#ffffff' if is_earned else '#d0d0e0'}; " + f"color: {tc('text') if is_earned else tc('text_dim')}; " f"background: transparent; border: none;" ) name.setWordWrap(True) @@ -378,7 +368,7 @@ class AchievementsView(QDialog): desc = QLabel(desc_text) desc.setWordWrap(True) desc.setStyleSheet( - f"color: #a0a0b8; font-size: 9.5pt; " + f"color: {tc('text_dim')}; font-size: 9.5pt; " f"background: transparent; border: none; padding: 0;" ) outer.addWidget(desc) @@ -453,32 +443,5 @@ class AchievementsView(QDialog): # ----- 탭 QSS (다이얼로그 전용) ----- def _tabs_qss(self) -> str: - return """ - QTabWidget::pane { - background: #25262B; - border: 1px solid #2a2a3a; - border-radius: 10px; - top: -1px; - } - QTabBar::tab { - background: #1c1c28; - color: #a0a0b8; - padding: 9px 18px; - border: 1px solid #2a2a3a; - border-bottom: none; - border-top-left-radius: 8px; - border-top-right-radius: 8px; - margin-right: 3px; - font-size: 10pt; - } - QTabBar::tab:selected { - background: #25262B; - color: #ffd24a; - font-weight: bold; - border-bottom: 2px solid #ffd24a; - } - QTabBar::tab:hover:!selected { - background: #2a2a36; - color: #e0e0e8; - } - """ + # 공통 테마 인식형 탭 스타일 (도전과제는 골드 강조 유지) + return tabs_qss(ACCENT_GOLD) diff --git a/ui/chart_widget.py b/ui/chart_widget.py index 484a287..fa21710 100644 --- a/ui/chart_widget.py +++ b/ui/chart_widget.py @@ -10,17 +10,30 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel from PyQt5.QtCore import Qt try: - from matplotlib.figure import Figure - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas import matplotlib - matplotlib.rcParams['font.family'] = ['Malgun Gothic', 'Apple SD Gothic Neo', 'sans-serif'] + from matplotlib.figure import Figure + # frozen(main.exe) 빌드는 PyInstaller matplotlib hook이 'QtAgg'(backend_qtagg)만 + # 번들함 → backend_qt5agg import가 실패해 차트가 안 뜨던 문제. + # 번들된 backend_qtagg를 우선 사용하고, 구버전(dev) 호환으로 qt5agg 폴백. + try: + from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas + except Exception: + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas + matplotlib.rcParams['font.family'] = ['NanumSquare', 'Malgun Gothic', 'Apple SD Gothic Neo', 'sans-serif'] matplotlib.rcParams['axes.unicode_minus'] = False _MPL = True -except ImportError: +except Exception as _mpl_err: + # ImportError 외 backend/sip 로딩 오류도 폴백 처리 + 실제 원인 기록(진단용) _MPL = False + try: + from utils.debug_log import dlog + dlog(f"chart_widget: matplotlib unavailable: {type(_mpl_err).__name__}: {_mpl_err}") + except Exception: + pass -# 다크 테마 색상 (dark_components / styles.py 톤과 일치) +# 차트 색상 — 배경/그리드/텍스트는 현재 테마를 따름(_refresh_chart_colors), +# 막대/선은 데이터 구분용 고정 색. _CHART_BG = '#25262B' _CHART_GRID = '#2C2E33' _CHART_TEXT = '#909296' @@ -30,6 +43,18 @@ _CHART_BAR_WEEKEND = '#fcd34d' # gold (데이터 구분용) _CHART_AVG_LINE = '#51CF66' # green +def _refresh_chart_colors() -> None: + """배경/그리드/텍스트 색을 현재 앱 테마로 갱신 (라이트/다크 추종).""" + global _CHART_BG, _CHART_GRID, _CHART_TEXT + try: + from ui.styles import ThemeColors + _CHART_BG = ThemeColors.get('bg_secondary') + _CHART_GRID = ThemeColors.get('border_subtle') + _CHART_TEXT = ThemeColors.get('text_secondary') + except Exception: + pass + + def _apply_dark_axes(ax) -> None: """차트 ax에 다크 테마 적용 — 텍스트, 그리드, spines, 배경.""" ax.set_facecolor(_CHART_BG) @@ -43,7 +68,8 @@ def _apply_dark_axes(ax) -> None: def _apply_dark_figure(fig) -> None: - """figure 배경을 다크 톤으로.""" + """figure 배경을 현재 테마 톤으로 (모든 draw_* 진입점에서 호출됨).""" + _refresh_chart_colors() fig.patch.set_facecolor(_CHART_BG) @@ -64,6 +90,7 @@ def make_chart_widget(parent=None) -> QWidget: """차트가 그려질 빈 캔버스 위젯. matplotlib 없으면 fallback.""" if not _MPL: return _Fallback("차트 표시에는 matplotlib가 필요합니다.\npip install matplotlib") + _refresh_chart_colors() widget = QWidget(parent) widget.setStyleSheet(f"background: {_CHART_BG}; border-radius: 8px;") layout = QVBoxLayout() diff --git a/ui/dark_components.py b/ui/dark_components.py index 22a36ca..dd02716 100644 --- a/ui/dark_components.py +++ b/ui/dark_components.py @@ -78,26 +78,59 @@ CARD_THEMES = { } +# ── 테마 연동 ────────────────────────────────────────────────── +# 통계/도움말/도전과제 다이얼로그는 열 때마다 새로 생성되므로, 빌드 시점에 현재 +# 앱 테마(ThemeColors)를 읽으면 라이트/다크를 자동으로 따른다. + +def _pal() -> dict: + """현재 앱 테마 팔레트를 dark_components 역할명으로 매핑.""" + from ui.styles import ThemeColors + g = ThemeColors.get + return { + 'bg': g('bg_primary'), 'panel': g('bg_secondary'), 'panel2': g('bg_tertiary'), + 'border': g('border_subtle'), 'border_strong': g('border_default'), + 'text': g('text_primary'), 'text_dim': g('text_secondary'), + 'text_faint': g('text_tertiary'), + 'blue': g('accent_primary'), 'green': g('accent_success'), + 'red': g('accent_danger'), + 'blue_hover': g('accent_primary_hover'), 'blue_pressed': g('accent_primary_pressed'), + 'green_hover': g('accent_success_hover'), 'red_hover': g('accent_danger_hover'), + } + + +def _is_dark() -> bool: + from ui.styles import ThemeColors, DARK_COLORS + return ThemeColors.current is DARK_COLORS + + +def tc(role: str) -> str: + """뷰에서 단일 색을 테마 인식형으로 가져올 때 사용 (예: tc('text')).""" + return _pal().get(role, '#FF00FF') + + # ── QSS 헬퍼 ─────────────────────────────────────────────────── def dialog_qss() -> str: - """다이얼로그 전체 배경.""" - return f"QDialog {{ background: {DARK_BG}; }}" + """다이얼로그 전체 배경 (현재 테마).""" + return f"QDialog {{ background: {_pal()['bg']}; }}" -def tabs_qss(accent: str = ACCENT_BLUE) -> str: +def tabs_qss(accent: str = None) -> str: + p = _pal() + if accent is None: + accent = p['blue'] return f""" QTabWidget::pane {{ - background: {DARK_PANEL}; - border: 1px solid {DARK_BORDER}; + background: {p['panel']}; + border: 1px solid {p['border']}; border-radius: 10px; top: -1px; }} QTabBar::tab {{ - background: {DARK_PANEL_2}; - color: {DARK_TEXT_DIM}; + background: {p['panel2']}; + color: {p['text_dim']}; padding: 9px 18px; - border: 1px solid {DARK_BORDER}; + border: 1px solid {p['border']}; border-bottom: none; border-top-left-radius: 8px; border-top-right-radius: 8px; @@ -105,88 +138,90 @@ def tabs_qss(accent: str = ACCENT_BLUE) -> str: font-size: 10pt; }} QTabBar::tab:selected {{ - background: {DARK_PANEL}; + background: {p['panel']}; color: {accent}; font-weight: bold; border-bottom: 2px solid {accent}; }} QTabBar::tab:hover:!selected {{ - background: #2a2a36; - color: {DARK_TEXT}; + background: {p['border_strong']}; + color: {p['text']}; }} """ def scroll_qss() -> str: + p = _pal() return f""" QScrollArea {{ background: transparent; border: none; }} QScrollBar:vertical {{ - background: {DARK_PANEL_2}; width: 10px; border-radius: 5px; + background: {p['panel2']}; width: 10px; border-radius: 5px; }} QScrollBar::handle:vertical {{ - background: {DARK_BORDER_STRONG}; border-radius: 5px; min-height: 30px; + background: {p['border_strong']}; border-radius: 5px; min-height: 30px; }} - QScrollBar::handle:vertical:hover {{ background: {ACCENT_BLUE}; }} + QScrollBar::handle:vertical:hover {{ background: {p['blue']}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }} QScrollBar:horizontal {{ - background: {DARK_PANEL_2}; height: 10px; border-radius: 5px; + background: {p['panel2']}; height: 10px; border-radius: 5px; }} QScrollBar::handle:horizontal {{ - background: {DARK_BORDER_STRONG}; border-radius: 5px; min-width: 30px; + background: {p['border_strong']}; border-radius: 5px; min-width: 30px; }} - QScrollBar::handle:horizontal:hover {{ background: {ACCENT_BLUE}; }} + QScrollBar::handle:horizontal:hover {{ background: {p['blue']}; }} """ def button_qss(variant: str = 'default') -> str: - """ variant: default | primary | success | danger | ghost """ + """ variant: default | primary | success | danger | ghost (현재 테마) """ + p = _pal() if variant == 'primary': return f""" QPushButton {{ - background: {ACCENT_BLUE}; color: white; + background: {p['blue']}; color: white; border: none; border-radius: 8px; padding: 8px 18px; font-weight: bold; font-size: 10pt; }} - QPushButton:hover {{ background: #69B6F8; }} - QPushButton:pressed {{ background: #3D97E0; }} - QPushButton:disabled {{ background: {DARK_PANEL_2}; color: {DARK_TEXT_FAINT}; }} + QPushButton:hover {{ background: {p['blue_hover']}; }} + QPushButton:pressed {{ background: {p['blue_pressed']}; }} + QPushButton:disabled {{ background: {p['panel2']}; color: {p['text_faint']}; }} """ if variant == 'success': return f""" QPushButton {{ - background: {ACCENT_GREEN}; color: #0e2a1a; + background: {p['green']}; color: white; border: none; border-radius: 8px; padding: 8px 18px; font-weight: bold; font-size: 10pt; }} - QPushButton:hover {{ background: #69DB7C; }} + QPushButton:hover {{ background: {p['green_hover']}; }} """ if variant == 'danger': return f""" QPushButton {{ - background: {ACCENT_RED}; color: white; + background: {p['red']}; color: white; border: none; border-radius: 8px; padding: 8px 18px; font-weight: bold; font-size: 10pt; }} - QPushButton:hover {{ background: #FF6B6B; }} + QPushButton:hover {{ background: {p['red_hover']}; }} """ if variant == 'ghost': return f""" QPushButton {{ - background: transparent; color: {DARK_TEXT_DIM}; - border: 1px solid {DARK_BORDER_STRONG}; border-radius: 8px; + background: transparent; color: {p['text_dim']}; + border: 1px solid {p['border_strong']}; border-radius: 8px; padding: 6px 14px; font-size: 9.5pt; }} - QPushButton:hover {{ background: {DARK_PANEL_2}; color: {DARK_TEXT}; - border-color: {ACCENT_BLUE}; }} + QPushButton:hover {{ background: {p['panel2']}; color: {p['text']}; + border-color: {p['blue']}; }} """ # default return f""" QPushButton {{ - background: {DARK_PANEL_2}; color: {DARK_TEXT}; - border: 1px solid {DARK_BORDER_STRONG}; border-radius: 8px; + background: {p['panel2']}; color: {p['text']}; + border: 1px solid {p['border_strong']}; border-radius: 8px; padding: 8px 18px; font-size: 10pt; }} - QPushButton:hover {{ background: {DARK_BORDER_STRONG}; border-color: {ACCENT_BLUE}; }} + QPushButton:hover {{ background: {p['border_strong']}; border-color: {p['blue']}; }} """ @@ -204,14 +239,15 @@ def build_gradient_header(title: str, big_value: str, subtitle: str = '', big_color: 큰 숫자 색 extra_widgets: 우측에 배치할 위젯 (예: 추가 통계, 토글) """ + p = _pal() container = QFrame() container.setStyleSheet(f""" QFrame {{ - background: {DARK_PANEL}; - border: 1px solid {DARK_BORDER}; + background: {p['panel']}; + border: 1px solid {p['border']}; border-radius: 8px; }} - QLabel {{ background: transparent; border: none; color: {DARK_TEXT}; }} + QLabel {{ background: transparent; border: none; color: {p['text']}; }} """) layout = QHBoxLayout() layout.setContentsMargins(20, 14, 20, 14) @@ -223,13 +259,13 @@ def build_gradient_header(title: str, big_value: str, subtitle: str = '', if title: t = QLabel(title) t.setStyleSheet( - f"font-size: 9pt; color: {DARK_TEXT_DIM}; " + f"font-size: 9pt; color: {p['text_dim']}; " f"background: transparent; border: none;" ) left.addWidget(t) big = QLabel( f"" - f"{big_value}" + (f"" + f"{big_value}" + (f"" f" {subtitle}" if subtitle else '') ) big.setTextFormat(Qt.RichText) @@ -253,16 +289,29 @@ def build_stat_card(title: str, value: str, subtitle: str = '', theme: str = 'blue', icon: str = '') -> QFrame: """단일 통계 카드 — 제목, 큰 숫자, 부제, 좌측 큰 이모지.""" t = CARD_THEMES.get(theme, CARD_THEMES['blue']) + p = _pal() + dark = _is_dark() + # 다크: 등급색 그라디언트 카드 / 라이트: 패널 배경 + 가독성 위해 값은 기본 텍스트색 + if dark: + card_bg = (f"qlineargradient(x1:0, y1:0, x2:0, y2:1, " + f"stop:0 {t['bg_top']}, stop:1 {t['bg_bot']})") + card_border = t['border'] + label_color = t['text'] + value_color = t['border_strong'] + else: + card_bg = p['panel'] + card_border = p['border'] + label_color = p['text'] + value_color = p['text'] card = QFrame() card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) card.setStyleSheet(f""" QFrame {{ - background: qlineargradient(x1:0, y1:0, x2:0, y2:1, - stop:0 {t['bg_top']}, stop:1 {t['bg_bot']}); - border: 1px solid {t['border']}; + background: {card_bg}; + border: 1px solid {card_border}; border-radius: 10px; }} - QLabel {{ background: transparent; border: none; color: {t['text']}; }} + QLabel {{ background: transparent; border: none; color: {label_color}; }} """) outer = QHBoxLayout() outer.setContentsMargins(16, 12, 16, 12) @@ -290,13 +339,13 @@ def build_stat_card(title: str, value: str, subtitle: str = '', title_lbl = QLabel(title) title_lbl.setStyleSheet( - f"font-size: 9.5pt; color: {DARK_TEXT_DIM}; " + f"font-size: 9.5pt; color: {p['text_dim']}; " f"background: transparent; border: none;" ) text_box.addWidget(title_lbl) val_lbl = QLabel( - f"" + f"" f"{value}" ) val_lbl.setTextFormat(Qt.RichText) @@ -306,7 +355,7 @@ def build_stat_card(title: str, value: str, subtitle: str = '', if subtitle: sub_lbl = QLabel(subtitle) sub_lbl.setStyleSheet( - f"font-size: 9pt; color: {DARK_TEXT_DIM}; " + f"font-size: 9pt; color: {p['text_dim']}; " f"background: transparent; border: none;" ) sub_lbl.setWordWrap(True) @@ -321,16 +370,25 @@ def build_section_card(title: str, content: QWidget, theme: str = 'gray', icon: str = '') -> QFrame: """제목 + 내용 큰 카드 (세로 레이아웃).""" t = CARD_THEMES.get(theme, CARD_THEMES['gray']) + p = _pal() + if _is_dark(): + card_bg = (f"qlineargradient(x1:0, y1:0, x2:0, y2:1, " + f"stop:0 {t['bg_top']}, stop:1 {t['bg_bot']})") + card_border = t['border'] + label_color = t['text'] + else: + card_bg = p['panel'] + card_border = p['border'] + label_color = p['text'] card = QFrame() card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) card.setStyleSheet(f""" QFrame {{ - background: qlineargradient(x1:0, y1:0, x2:0, y2:1, - stop:0 {t['bg_top']}, stop:1 {t['bg_bot']}); - border: 1px solid {t['border']}; + background: {card_bg}; + border: 1px solid {card_border}; border-radius: 10px; }} - QLabel {{ background: transparent; border: none; color: {t['text']}; }} + QLabel {{ background: transparent; border: none; color: {label_color}; }} """) layout = QVBoxLayout() layout.setContentsMargins(16, 12, 16, 14) @@ -351,7 +409,7 @@ def build_section_card(title: str, content: QWidget, head.addWidget(i) title_lbl = QLabel(title) title_lbl.setStyleSheet( - f"font-size: 12pt; font-weight: bold; color: {DARK_TEXT}; " + f"font-size: 12pt; font-weight: bold; color: {p['text']}; " f"background: transparent; border: none;" ) head.addWidget(title_lbl) @@ -385,9 +443,11 @@ def style_progressbar(pb: QProgressBar, theme: str = 'blue', def transparent_label(text: str, size: int = 10, weight: str = 'normal', - color: str = DARK_TEXT) -> QLabel: - """글로벌 QSS와 격리된 다크 라벨 (배경 없음, 외곽선 없음).""" + color: str = None) -> QLabel: + """글로벌 QSS와 격리된 라벨 (배경 없음, 외곽선 없음). color 미지정 시 현재 테마 텍스트색.""" lbl = QLabel(text) + if color is None: + color = _pal()['text'] weight_str = 'bold' if weight == 'bold' else 'normal' lbl.setStyleSheet( f"font-size: {size}pt; font-weight: {weight_str}; color: {color}; " diff --git a/ui/help_view.py b/ui/help_view.py index 6986716..6de5e91 100644 --- a/ui/help_view.py +++ b/ui/help_view.py @@ -10,10 +10,7 @@ from PyQt5.QtCore import Qt from core.i18n import tr, tr_html from ui.styles import apply_dark_titlebar -from ui.dark_components import ( - dialog_qss, tabs_qss, button_qss, - DARK_BG, DARK_PANEL, DARK_BORDER, DARK_TEXT, ACCENT_GOLD, -) +from ui.dark_components import dialog_qss, tabs_qss, button_qss, tc class HelpView(QDialog): @@ -37,7 +34,7 @@ class HelpView(QDialog): self.resize(820, 760) self.setStyleSheet(dialog_qss()) self.init_ui() - apply_dark_titlebar(self, dark=True) + apply_dark_titlebar(self) # 현재 테마에 맞춰 def init_ui(self): main_layout = QVBoxLayout() @@ -47,7 +44,7 @@ class HelpView(QDialog): # 다크 타이틀 title = QLabel(tr('window.help')) title.setStyleSheet( - f"font-size: 18pt; font-weight: bold; color: {DARK_TEXT}; " + f"font-size: 18pt; font-weight: bold; color: {tc('text')}; " f"background: transparent; border: none; padding: 4px 0;" ) main_layout.addWidget(title) @@ -88,7 +85,7 @@ class HelpView(QDialog): def _make_tab(self, html: str) -> QWidget: container = QWidget() - container.setStyleSheet(f"background: {DARK_PANEL};") + container.setStyleSheet(f"background: {tc('panel')};") layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) @@ -99,21 +96,21 @@ class HelpView(QDialog): browser.setHtml(styled_html) browser.setStyleSheet(f""" QTextBrowser {{ - background: {DARK_PANEL}; - color: {DARK_TEXT}; + background: {tc('panel')}; + color: {tc('text')}; border: none; padding: 16px 20px; font-size: 10.5pt; - selection-background-color: {ACCENT_GOLD}; - selection-color: #1a1a26; + selection-background-color: {tc('blue')}; + selection-color: #ffffff; }} QScrollBar:vertical {{ - background: {DARK_PANEL}; width: 10px; border-radius: 5px; + background: {tc('panel')}; width: 10px; border-radius: 5px; }} QScrollBar::handle:vertical {{ - background: {DARK_BORDER}; border-radius: 5px; min-height: 30px; + background: {tc('border_strong')}; border-radius: 5px; min-height: 30px; }} - QScrollBar::handle:vertical:hover {{ background: {ACCENT_GOLD}; }} + QScrollBar::handle:vertical:hover {{ background: {tc('blue')}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }} """) layout.addWidget(browser) @@ -122,61 +119,67 @@ class HelpView(QDialog): def _inject_dark_styles(self, html: str) -> str: """HelpHTML 내용에 다크 톤 CSS 주입 (제목/링크/코드/테이블).""" + # 현재 테마 색으로 (라이트/다크 모두 가독성 확보) + text = tc('text') + dim = tc('text_dim') + blue = tc('blue') + green = tc('green') + panel2 = tc('panel2') + border = tc('border') css = f""" """ diff --git a/ui/stats_view.py b/ui/stats_view.py index 310137e..36d67de 100644 --- a/ui/stats_view.py +++ b/ui/stats_view.py @@ -15,7 +15,7 @@ from core.i18n import tr from ui.styles import apply_dark_titlebar from ui.dark_components import ( dialog_qss, tabs_qss, button_qss, build_stat_card, build_section_card, - transparent_label, ACCENT_GOLD, ACCENT_GREEN, DARK_TEXT, DARK_TEXT_DIM, + transparent_label, tc, ) @@ -27,7 +27,7 @@ class StatsView(QDialog): self.db = db if db else Database() self.init_ui() self.load_stats() - apply_dark_titlebar(self, dark=True) + apply_dark_titlebar(self) # 현재 테마에 맞춰 타이틀바 def init_ui(self): """UI 초기화""" @@ -44,7 +44,7 @@ class StatsView(QDialog): # 다크 톤 타이틀 title = QLabel(f"{tr('stats.title')}") title.setStyleSheet( - f"font-size: 18pt; font-weight: bold; color: {DARK_TEXT}; " + f"font-size: 18pt; font-weight: bold; color: {tc('text')}; " f"background: transparent; border: none; padding: 4px 0;" ) layout.addWidget(title) @@ -143,9 +143,9 @@ class StatsView(QDialog): # 추정 급여 (옵션 활성 시) self.salary_label = QLabel("") self.salary_label.setStyleSheet( - f"background: rgba(74, 222, 128, 0.12); " - f"border: 1px solid {ACCENT_GREEN}; border-radius: 8px; " - f"color: {ACCENT_GREEN}; font-weight: bold; " + f"background: rgba(81, 207, 102, 0.12); " + f"border: 1px solid {tc('green')}; border-radius: 8px; " + f"color: {tc('green')}; font-weight: bold; " f"padding: 10px 14px; font-size: 11pt;" ) self.salary_label.setVisible(False) @@ -179,7 +179,7 @@ class StatsView(QDialog): self.pattern_text.setWordWrap(True) self.pattern_text.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.pattern_text.setStyleSheet( - f"font-size: 11pt; color: {DARK_TEXT}; " + f"font-size: 11pt; color: {tc('text')}; " f"background: transparent; border: none; padding: 4px 0;" ) layout.addWidget(build_section_card("패턴 인사이트", self.pattern_text,