From 6a17876af1144a9636efcaa04cbd185865b8dbc6 Mon Sep 17 00:00:00 2001 From: KINDNICK Date: Thu, 30 Apr 2026 18:54:25 +0900 Subject: [PATCH] =?UTF-8?q?v2.6.0:=20Phase=203=20+=20Phase=204=20=E2=80=94?= =?UTF-8?q?=20accessibility=20+=20code=20sign=20infra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 (also v2.5.0 in CHANGELOG): - Weekly auto report on Monday - Matplotlib chart hover annotation - Clock-in time distribution histogram - Leave usage calendar with color coding Phase 4 (v2.6.0): - Font scale 100/125/150% (instant apply) - High-contrast mode (black bg + yellow text) - Authenticode signing infra in release.ps1 (env-gated, optional) Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 13 +++++++ core/database.py | 7 ++-- core/settings_keys.py | 2 ++ core/version.py | 2 +- release.ps1 | 21 +++++++++++ ui/accessibility.py | 82 +++++++++++++++++++++++++++++++++++++++++++ ui/main_window.py | 14 ++++++++ ui/settings_view.py | 30 ++++++++++++++++ 8 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 ui/accessibility.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d5977..293c860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ 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.6.0] — 2026-04-30 + +### Added — Phase 4 (3종) +- **글꼴 크기 조절** (100% / 125% / 150%) — 설정에서 즉시 반영 +- **고대비 모드** — 검정 배경 + 노란 텍스트 (시각약자/야간) +- **코드 서명 인프라** — `release.ps1`에 Authenticode 서명 단계 추가 (옵션) + - `$env:CODE_SIGN_CERT` (`.pfx` 경로) + `$env:CODE_SIGN_PASS` 환경변수 설정 시 자동 서명 + - signtool.exe 없거나 cert 미설정 시 자동 스킵 + - 코드 서명 인증서 확보 후 활성화하면 SmartScreen 경고 제거 가능 + +### Settings (신규) +- `font_scale`, `high_contrast` + ## [2.5.0] — 2026-04-30 ### Added — Phase 3 (4종) diff --git a/core/database.py b/core/database.py index c217cc4..d93f6cd 100644 --- a/core/database.py +++ b/core/database.py @@ -653,8 +653,11 @@ class Database: 'discord_notif_clock_out': 'true', 'discord_notif_health': 'true', # v2.4.0 - 'goal_overtime_max_monthly': '0', # 0=비활성, >0=분 단위 상한 - 'goal_avg_hours_daily': '0', # 0=비활성 + 'goal_overtime_max_monthly': '0', + 'goal_avg_hours_daily': '0', + # v2.6.0 + 'font_scale': '1.0', + 'high_contrast': 'false', } conn = self.get_connection() diff --git a/core/settings_keys.py b/core/settings_keys.py index 4551f3e..396a7a7 100644 --- a/core/settings_keys.py +++ b/core/settings_keys.py @@ -37,6 +37,8 @@ THEME = 'theme' TIME_FORMAT = 'time_format' LANGUAGE = 'language' OVERTIME_UNIT = 'overtime_unit' +FONT_SCALE = 'font_scale' # '1.0' / '1.25' / '1.5' +HIGH_CONTRAST = 'high_contrast' # 통합/외부 DB_PATH_OVERRIDE = 'db_path_override' diff --git a/core/version.py b/core/version.py index 1b6e4f8..18b818e 100644 --- a/core/version.py +++ b/core/version.py @@ -4,4 +4,4 @@ 릴리스 시 이 값을 올린 후 git tag → push. CHANGELOG.md의 최상단 항목과 일치시킬 것. """ -__version__ = '2.5.0' +__version__ = '2.6.0' diff --git a/release.ps1 b/release.ps1 index ad28fef..eef67d6 100644 --- a/release.ps1 +++ b/release.ps1 @@ -141,6 +141,27 @@ $mainSize = "{0:N1}MB" -f ((Get-Item dist/main.exe).Length / 1MB) $updaterSize = "{0:N1}MB" -f ((Get-Item dist/updater.exe).Length / 1MB) OkMsg "main.exe ($mainSize) + updater.exe ($updaterSize)" +# Optional: Authenticode signing if cert available (env vars) +# Set CODE_SIGN_CERT (path to .pfx) and CODE_SIGN_PASS to enable +if ($env:CODE_SIGN_CERT -and (Test-Path $env:CODE_SIGN_CERT)) { + Info "Code signing exes (Authenticode)..." + $signtool = Get-Command signtool.exe -ErrorAction SilentlyContinue + if (-not $signtool) { + Info " signtool.exe not found in PATH — skipping signature" + } else { + $tsUrl = if ($env:CODE_SIGN_TIMESTAMP) { $env:CODE_SIGN_TIMESTAMP } else { 'http://timestamp.digicert.com' } + foreach ($exe in 'dist/main.exe', 'dist/updater.exe') { + $args = @('sign', '/f', $env:CODE_SIGN_CERT) + if ($env:CODE_SIGN_PASS) { $args += '/p'; $args += $env:CODE_SIGN_PASS } + $args += '/tr'; $args += $tsUrl; $args += '/td'; $args += 'sha256' + $args += '/fd'; $args += 'sha256'; $args += $exe + $rc = Invoke-Native signtool $args + if ($rc -ne 0) { Info " WARN: sign failed for $exe (exit $rc)" } + else { OkMsg " signed $exe" } + } + } +} + # ====== 4. ZIP ====== Step "4/7 ZIP packaging" $zipPath = "dist/ClockOutCalculator-$Version.zip" diff --git a/ui/accessibility.py b/ui/accessibility.py new file mode 100644 index 0000000..653c417 --- /dev/null +++ b/ui/accessibility.py @@ -0,0 +1,82 @@ +""" +접근성 — 글꼴 크기 / 고대비 모드 적용. + +QApplication 글로벌 폰트 + 추가 QSS 오버레이. +설정 변경 즉시 반영 (재시작 불필요). +""" +from __future__ import annotations +from PyQt5.QtWidgets import QApplication +from PyQt5.QtGui import QFont + + +# 고대비 QSS — 검정 배경 + 노란 텍스트 + 굵은 테두리 +HIGH_CONTRAST_QSS = """ +* { + background-color: #000000; + color: #FFEB3B; + border-color: #FFEB3B; +} +QPushButton, QLineEdit, QSpinBox, QComboBox, QTextEdit, QTableWidget, QGroupBox { + border: 2px solid #FFEB3B; + background-color: #000000; + color: #FFEB3B; +} +QPushButton:hover { background-color: #333333; } +QPushButton:pressed { background-color: #FFEB3B; color: #000000; } +QPushButton:disabled { color: #888; border-color: #888; } +QGroupBox::title { color: #FFEB3B; padding: 0 4px; } +QProgressBar { border: 2px solid #FFEB3B; } +QProgressBar::chunk { background-color: #FFEB3B; } +QToolTip { background-color: #000; color: #FFEB3B; border: 2px solid #FFEB3B; } +""" + + +def apply_font_scale(scale: float) -> None: + """전역 글꼴 크기 배율 적용 (1.0 = 기본, 1.25 = 125%, 1.5 = 150%).""" + app = QApplication.instance() + if app is None: + return + base = app.font() + if base.pointSize() > 0: + # 기존 배율 무시하고 새 배율로 (기본 9pt 가정) + base_pt = 9 + base.setPointSize(int(round(base_pt * scale))) + else: + base_px = 12 + base.setPixelSize(int(round(base_px * scale))) + app.setFont(base) + + +def apply_high_contrast(enabled: bool, base_qss: str = "") -> None: + """고대비 모드 ON/OFF. base_qss는 평소 테마 QSS (OFF 시 복원용).""" + app = QApplication.instance() + if app is None: + return + if enabled: + app.setStyleSheet(base_qss + "\n" + HIGH_CONTRAST_QSS) + else: + app.setStyleSheet(base_qss) + + +def apply_from_settings(db) -> None: + """db에서 font_scale + high_contrast 읽어 적용.""" + try: + scale = float(db.get_setting('font_scale', '1.0') or 1.0) + except (ValueError, TypeError): + scale = 1.0 + scale = max(0.8, min(2.0, scale)) + apply_font_scale(scale) + + enabled = db.get_setting('high_contrast', 'false').lower() == 'true' + # base_qss는 main_window에서 apply_theme() 호출 직후 적용되므로, + # 여기서는 현재 styleSheet 그대로 두고 high_contrast만 추가. + app = QApplication.instance() + if app is None: + return + current = app.styleSheet() or "" + # 기존에 추가된 HIGH_CONTRAST_QSS 제거 + base = current.replace(HIGH_CONTRAST_QSS, "").rstrip() + "\n" + if enabled: + app.setStyleSheet(base + HIGH_CONTRAST_QSS) + else: + app.setStyleSheet(base.rstrip()) diff --git a/ui/main_window.py b/ui/main_window.py index c441b6f..f7fd7dc 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -59,6 +59,13 @@ class MainWindow(QMainWindow): from core.i18n import set_language set_language(self.db.get_setting(LANGUAGE, 'ko') or 'ko') + # 접근성 — 글꼴 크기 + 고대비 + try: + from ui.accessibility import apply_from_settings as _apply_a11y + _apply_a11y(self.db) + except Exception: + pass + # TimeCalculator 초기화 (설정값 반영) settings = self.db.get_settings() @@ -2004,6 +2011,13 @@ class MainWindow(QMainWindow): # auto_lunch 캐시 무효화 (설정에서 토글 가능하므로) self._auto_lunch.invalidate() + # 접근성 재적용 (글꼴 / 고대비) + try: + from ui.accessibility import apply_from_settings as _apply_a11y + _apply_a11y(self.db) + except Exception: + pass + # UI 업데이트 self.update_overtime_balance() self.update_leave_balance() diff --git a/ui/settings_view.py b/ui/settings_view.py index bc2db00..eb8cfed 100644 --- a/ui/settings_view.py +++ b/ui/settings_view.py @@ -24,6 +24,7 @@ from core.settings_keys import ( DB_PATH_OVERRIDE, CLOCK_IN_ON_UNLOCK, GOAL_OVERTIME_MAX_MONTHLY, GOAL_AVG_HOURS_DAILY, GITEA_FEEDBACK_TOKEN, GITEA_FEEDBACK_ENABLED, + FONT_SCALE, HIGH_CONTRAST, ) from utils.csv_exporter import CSVExporter from ui.leave_view import AddLeaveDialog @@ -323,6 +324,22 @@ class SettingsView(QDialog): format_row.addStretch() layout.addLayout(format_row) + # 접근성: 글꼴 크기 + 고대비 + a11y_row = QHBoxLayout() + a11y_row.addWidget(QLabel("글꼴 크기:")) + self.font_scale_combo = QComboBox() + self.font_scale_combo.addItem("100%", "1.0") + self.font_scale_combo.addItem("125%", "1.25") + self.font_scale_combo.addItem("150%", "1.5") + self.font_scale_combo.setFixedWidth(90) + a11y_row.addWidget(self.font_scale_combo) + a11y_row.addSpacing(16) + self.high_contrast_check = QCheckBox("고대비 모드") + self.high_contrast_check.setToolTip("검정 배경 + 노란 텍스트 (시각약자/야간)") + a11y_row.addWidget(self.high_contrast_check) + a11y_row.addStretch() + layout.addLayout(a11y_row) + # 언어 선택 from core.i18n import available_languages, language_label lang_row = QHBoxLayout() @@ -904,6 +921,15 @@ class SettingsView(QDialog): settings.get(GITEA_FEEDBACK_ENABLED, False) ) + # 접근성 + if hasattr(self, 'font_scale_combo'): + scale = str(settings.get(FONT_SCALE, '1.0')) + idx = self.font_scale_combo.findData(scale) + if idx >= 0: + self.font_scale_combo.setCurrentIndex(idx) + if hasattr(self, 'high_contrast_check'): + self.high_contrast_check.setChecked(settings.get(HIGH_CONTRAST, False)) + # 알림 self.clock_out_notification_check.setChecked(settings.get(NOTIF_CLOCK_OUT, True)) self.lunch_notification_check.setChecked(settings.get(NOTIF_LUNCH, True)) @@ -1058,6 +1084,10 @@ class SettingsView(QDialog): self.db.set_setting(GITEA_FEEDBACK_TOKEN, self.gitea_token_edit.text().strip()) if hasattr(self, 'gitea_feedback_enabled_check'): settings[GITEA_FEEDBACK_ENABLED] = self.gitea_feedback_enabled_check.isChecked() + if hasattr(self, 'font_scale_combo'): + settings[FONT_SCALE] = self.font_scale_combo.currentData() + if hasattr(self, 'high_contrast_check'): + settings[HIGH_CONTRAST] = self.high_contrast_check.isChecked() if hasattr(self, 'language_combo'): settings[LANGUAGE] = self.language_combo.currentData()