v2.6.0: Phase 3 + Phase 4 — accessibility + code sign infra
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) <noreply@anthropic.com>
This commit is contained in:
parent
606da976a0
commit
6a17876af1
13
CHANGELOG.md
13
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/).
|
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
|
## [2.5.0] — 2026-04-30
|
||||||
|
|
||||||
### Added — Phase 3 (4종)
|
### Added — Phase 3 (4종)
|
||||||
|
|||||||
@ -653,8 +653,11 @@ class Database:
|
|||||||
'discord_notif_clock_out': 'true',
|
'discord_notif_clock_out': 'true',
|
||||||
'discord_notif_health': 'true',
|
'discord_notif_health': 'true',
|
||||||
# v2.4.0
|
# v2.4.0
|
||||||
'goal_overtime_max_monthly': '0', # 0=비활성, >0=분 단위 상한
|
'goal_overtime_max_monthly': '0',
|
||||||
'goal_avg_hours_daily': '0', # 0=비활성
|
'goal_avg_hours_daily': '0',
|
||||||
|
# v2.6.0
|
||||||
|
'font_scale': '1.0',
|
||||||
|
'high_contrast': 'false',
|
||||||
}
|
}
|
||||||
|
|
||||||
conn = self.get_connection()
|
conn = self.get_connection()
|
||||||
|
|||||||
@ -37,6 +37,8 @@ THEME = 'theme'
|
|||||||
TIME_FORMAT = 'time_format'
|
TIME_FORMAT = 'time_format'
|
||||||
LANGUAGE = 'language'
|
LANGUAGE = 'language'
|
||||||
OVERTIME_UNIT = 'overtime_unit'
|
OVERTIME_UNIT = 'overtime_unit'
|
||||||
|
FONT_SCALE = 'font_scale' # '1.0' / '1.25' / '1.5'
|
||||||
|
HIGH_CONTRAST = 'high_contrast'
|
||||||
|
|
||||||
# 통합/외부
|
# 통합/외부
|
||||||
DB_PATH_OVERRIDE = 'db_path_override'
|
DB_PATH_OVERRIDE = 'db_path_override'
|
||||||
|
|||||||
@ -4,4 +4,4 @@
|
|||||||
릴리스 시 이 값을 올린 후 git tag → push.
|
릴리스 시 이 값을 올린 후 git tag → push.
|
||||||
CHANGELOG.md의 최상단 항목과 일치시킬 것.
|
CHANGELOG.md의 최상단 항목과 일치시킬 것.
|
||||||
"""
|
"""
|
||||||
__version__ = '2.5.0'
|
__version__ = '2.6.0'
|
||||||
|
|||||||
21
release.ps1
21
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)
|
$updaterSize = "{0:N1}MB" -f ((Get-Item dist/updater.exe).Length / 1MB)
|
||||||
OkMsg "main.exe ($mainSize) + updater.exe ($updaterSize)"
|
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 ======
|
# ====== 4. ZIP ======
|
||||||
Step "4/7 ZIP packaging"
|
Step "4/7 ZIP packaging"
|
||||||
$zipPath = "dist/ClockOutCalculator-$Version.zip"
|
$zipPath = "dist/ClockOutCalculator-$Version.zip"
|
||||||
|
|||||||
82
ui/accessibility.py
Normal file
82
ui/accessibility.py
Normal file
@ -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())
|
||||||
@ -59,6 +59,13 @@ class MainWindow(QMainWindow):
|
|||||||
from core.i18n import set_language
|
from core.i18n import set_language
|
||||||
set_language(self.db.get_setting(LANGUAGE, 'ko') or 'ko')
|
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 초기화 (설정값 반영)
|
# TimeCalculator 초기화 (설정값 반영)
|
||||||
settings = self.db.get_settings()
|
settings = self.db.get_settings()
|
||||||
|
|
||||||
@ -2004,6 +2011,13 @@ class MainWindow(QMainWindow):
|
|||||||
# auto_lunch 캐시 무효화 (설정에서 토글 가능하므로)
|
# auto_lunch 캐시 무효화 (설정에서 토글 가능하므로)
|
||||||
self._auto_lunch.invalidate()
|
self._auto_lunch.invalidate()
|
||||||
|
|
||||||
|
# 접근성 재적용 (글꼴 / 고대비)
|
||||||
|
try:
|
||||||
|
from ui.accessibility import apply_from_settings as _apply_a11y
|
||||||
|
_apply_a11y(self.db)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# UI 업데이트
|
# UI 업데이트
|
||||||
self.update_overtime_balance()
|
self.update_overtime_balance()
|
||||||
self.update_leave_balance()
|
self.update_leave_balance()
|
||||||
|
|||||||
@ -24,6 +24,7 @@ from core.settings_keys import (
|
|||||||
DB_PATH_OVERRIDE, CLOCK_IN_ON_UNLOCK,
|
DB_PATH_OVERRIDE, CLOCK_IN_ON_UNLOCK,
|
||||||
GOAL_OVERTIME_MAX_MONTHLY, GOAL_AVG_HOURS_DAILY,
|
GOAL_OVERTIME_MAX_MONTHLY, GOAL_AVG_HOURS_DAILY,
|
||||||
GITEA_FEEDBACK_TOKEN, GITEA_FEEDBACK_ENABLED,
|
GITEA_FEEDBACK_TOKEN, GITEA_FEEDBACK_ENABLED,
|
||||||
|
FONT_SCALE, HIGH_CONTRAST,
|
||||||
)
|
)
|
||||||
from utils.csv_exporter import CSVExporter
|
from utils.csv_exporter import CSVExporter
|
||||||
from ui.leave_view import AddLeaveDialog
|
from ui.leave_view import AddLeaveDialog
|
||||||
@ -323,6 +324,22 @@ class SettingsView(QDialog):
|
|||||||
format_row.addStretch()
|
format_row.addStretch()
|
||||||
layout.addLayout(format_row)
|
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
|
from core.i18n import available_languages, language_label
|
||||||
lang_row = QHBoxLayout()
|
lang_row = QHBoxLayout()
|
||||||
@ -904,6 +921,15 @@ class SettingsView(QDialog):
|
|||||||
settings.get(GITEA_FEEDBACK_ENABLED, False)
|
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.clock_out_notification_check.setChecked(settings.get(NOTIF_CLOCK_OUT, True))
|
||||||
self.lunch_notification_check.setChecked(settings.get(NOTIF_LUNCH, 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())
|
self.db.set_setting(GITEA_FEEDBACK_TOKEN, self.gitea_token_edit.text().strip())
|
||||||
if hasattr(self, 'gitea_feedback_enabled_check'):
|
if hasattr(self, 'gitea_feedback_enabled_check'):
|
||||||
settings[GITEA_FEEDBACK_ENABLED] = self.gitea_feedback_enabled_check.isChecked()
|
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'):
|
if hasattr(self, 'language_combo'):
|
||||||
settings[LANGUAGE] = self.language_combo.currentData()
|
settings[LANGUAGE] = self.language_combo.currentData()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user