v2.11.0: UI 전면 다크 리디자인 + 라인 아이콘 + 적립 가드/삭제
- 모던 다크 미니멀 테마(NanumSquare 번들, 단일 accent #4DABF7, 8px radius, flat 버튼, 다크 기본값) - 라인 아이콘 시스템(ui/icons.py, QtSvg) — 앱 전반 이모지 교체 - 다크 깨짐 수정: 테이블 헤더/코너 흰색, 도움말 탭 흰 라인, 트레이/미니위젯 메뉴 - fix: 자동 적립 OFF가 자동 퇴근 경로에서 무시되던 버그(게이팅) - feat: 연장근무 적립 기록 삭제(우클릭) - 테스트 3건 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
da5f91984b
commit
5fb8655a47
@@ -0,0 +1,84 @@
|
||||
"""번들 폰트(NanumSquare) 로딩.
|
||||
|
||||
`font/` 디렉토리의 TTF를 QFontDatabase에 등록해 OS 설치 없이도 사용.
|
||||
PyInstaller frozen(_MEIPASS) / 개발 실행(프로젝트 루트) 양쪽 경로를 지원하며,
|
||||
등록 실패 시 QSS 폰트 체인이 "Malgun Gothic"으로 자연 폴백한다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt5.QtGui import QFontDatabase, QFont
|
||||
|
||||
# 로드할 폰트 파일 — TTF 우선(Windows Qt에서 OTF보다 렌더 안정적).
|
||||
# L/R/B/EB 4단계 굵기 + _ac(라틴·숫자 보정) 변형을 함께 등록.
|
||||
_FONT_FILES = [
|
||||
'NanumSquareL.ttf',
|
||||
'NanumSquareR.ttf',
|
||||
'NanumSquareB.ttf',
|
||||
'NanumSquareEB.ttf',
|
||||
'NanumSquare_acR.ttf',
|
||||
'NanumSquare_acB.ttf',
|
||||
]
|
||||
|
||||
|
||||
def _font_dir() -> str:
|
||||
"""번들 font/ 디렉토리 절대 경로."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
base = getattr(sys, '_MEIPASS', None) or os.path.dirname(sys.executable)
|
||||
else:
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.join(base, 'font')
|
||||
|
||||
|
||||
def load_bundled_fonts() -> list:
|
||||
"""번들 폰트를 등록하고, 등록된 family 이름 목록을 반환."""
|
||||
families: list = []
|
||||
fdir = _font_dir()
|
||||
if not os.path.isdir(fdir):
|
||||
return families
|
||||
for name in _FONT_FILES:
|
||||
path = os.path.join(fdir, name)
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
fid = QFontDatabase.addApplicationFont(path)
|
||||
if fid == -1:
|
||||
continue
|
||||
for fam in QFontDatabase.applicationFontFamilies(fid):
|
||||
if fam not in families:
|
||||
families.append(fam)
|
||||
return families
|
||||
|
||||
|
||||
def _pick_primary(families: list) -> str:
|
||||
"""등록된 family 중 기본 본문용(Regular 굵기) family 선택."""
|
||||
if 'NanumSquare' in families:
|
||||
return 'NanumSquare'
|
||||
for fam in families:
|
||||
low = fam.lower()
|
||||
if 'nanumsquare' in low and 'light' not in low and 'extra' not in low:
|
||||
return fam
|
||||
return 'Malgun Gothic'
|
||||
|
||||
|
||||
def apply_app_font(app, point_size: int = 9) -> str:
|
||||
"""앱 전역 기본 폰트를 NanumSquare로 설정.
|
||||
|
||||
Returns:
|
||||
실제 적용된 primary family 이름 (폴백 시 'Malgun Gothic').
|
||||
"""
|
||||
families = load_bundled_fonts()
|
||||
primary = _pick_primary(families)
|
||||
font = QFont(primary, point_size)
|
||||
font.setStyleStrategy(QFont.PreferAntialias)
|
||||
app.setFont(font)
|
||||
return primary
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
_app = QApplication(sys.argv)
|
||||
fams = load_bundled_fonts()
|
||||
print('font dir:', _font_dir())
|
||||
print('registered families:', fams)
|
||||
print('picked primary:', _pick_primary(fams))
|
||||
+52
-39
@@ -57,62 +57,75 @@ class SystemTrayIcon(QSystemTrayIcon):
|
||||
return QIcon(pixmap)
|
||||
|
||||
def setup_menu(self):
|
||||
"""트레이 메뉴 설정"""
|
||||
"""트레이 메뉴 설정 — 라인 아이콘 + 앱 다크 톤."""
|
||||
menu = QMenu()
|
||||
|
||||
show_action = QAction(tr('tray.open'), self)
|
||||
show_action.triggered.connect(self.show_window)
|
||||
menu.addAction(show_action)
|
||||
# (action, 라인 아이콘 이름) — 테마 전환 시 재틴팅용으로 보관
|
||||
self._icon_actions = []
|
||||
|
||||
mini_action = QAction(tr('tray.mini_widget'), self)
|
||||
mini_action.triggered.connect(self._open_mini_widget)
|
||||
menu.addAction(mini_action)
|
||||
def add(text, slot, icon_name=None):
|
||||
action = QAction(text, self)
|
||||
action.triggered.connect(slot)
|
||||
menu.addAction(action)
|
||||
if icon_name:
|
||||
self._icon_actions.append((action, icon_name))
|
||||
return action
|
||||
|
||||
add(tr('tray.open'), self.show_window, 'home')
|
||||
add(tr('tray.mini_widget'), self._open_mini_widget, 'external-link')
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
lunch_action = QAction(tr('tray.toggle_lunch'), self)
|
||||
lunch_action.triggered.connect(self._toggle_lunch)
|
||||
menu.addAction(lunch_action)
|
||||
|
||||
break_out_action = QAction(tr('btn.break_out'), self)
|
||||
break_out_action.triggered.connect(self._break_out)
|
||||
menu.addAction(break_out_action)
|
||||
|
||||
break_in_action = QAction(tr('btn.break_in'), self)
|
||||
break_in_action.triggered.connect(self._break_in)
|
||||
menu.addAction(break_in_action)
|
||||
add(tr('tray.toggle_lunch'), self._toggle_lunch, 'coffee')
|
||||
add(tr('btn.break_out'), self._break_out)
|
||||
add(tr('btn.break_in'), self._break_in)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
clock_out_action = QAction("✅ " + tr('btn.clock_out'), self)
|
||||
clock_out_action.triggered.connect(self.quick_clock_out)
|
||||
menu.addAction(clock_out_action)
|
||||
add(tr('btn.clock_out'), self.quick_clock_out, 'logout')
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
stats_action = QAction("📊 " + tr('menu.stats'), self)
|
||||
stats_action.triggered.connect(lambda: self._call_parent('show_stats'))
|
||||
menu.addAction(stats_action)
|
||||
|
||||
cal_action = QAction("📅 " + tr('menu.calendar'), self)
|
||||
cal_action.triggered.connect(lambda: self._call_parent('show_calendar'))
|
||||
menu.addAction(cal_action)
|
||||
|
||||
schedule_action = QAction("🗓️ 스케줄", self)
|
||||
schedule_action.triggered.connect(lambda: self._call_parent('show_schedule'))
|
||||
menu.addAction(schedule_action)
|
||||
|
||||
help_action = QAction("📖 " + tr('menu.help'), self)
|
||||
help_action.triggered.connect(lambda: self._call_parent('show_help'))
|
||||
menu.addAction(help_action)
|
||||
add(tr('menu.stats'), lambda: self._call_parent('show_stats'), 'chart')
|
||||
add(tr('menu.calendar'), lambda: self._call_parent('show_calendar'), 'calendar')
|
||||
add('스케줄', lambda: self._call_parent('show_schedule'), 'repeat')
|
||||
add(tr('menu.help'), lambda: self._call_parent('show_help'), 'help')
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
quit_action = QAction(tr('tray.quit'), self)
|
||||
quit_action.triggered.connect(self.quit_app)
|
||||
menu.addAction(quit_action)
|
||||
add(tr('tray.quit'), self.quit_app)
|
||||
|
||||
self.setContextMenu(menu)
|
||||
self.refresh_theme()
|
||||
|
||||
def refresh_theme(self):
|
||||
"""트레이 메뉴에 현재 앱 테마 QSS + 라인 아이콘 색을 (재)적용.
|
||||
|
||||
QMenu()는 부모가 없어 메인 윈도우 스타일시트를 자동 상속하지 않으므로
|
||||
명시적으로 적용한다. 테마 변경 시 main_window.apply_theme에서 호출.
|
||||
"""
|
||||
menu = self.contextMenu()
|
||||
if menu is None:
|
||||
return
|
||||
# 다크 QSS 적용 (메인 윈도우 스타일 우선, 없으면 dark 폴백)
|
||||
qss = self.parent_window.styleSheet() if self.parent_window else ''
|
||||
if not qss:
|
||||
try:
|
||||
from ui.styles import get_theme
|
||||
qss = get_theme('dark')
|
||||
except Exception:
|
||||
qss = ''
|
||||
if qss:
|
||||
menu.setStyleSheet(qss)
|
||||
# 라인 아이콘 틴팅 (메뉴 텍스트 색과 동일하게)
|
||||
try:
|
||||
from ui.icons import get_icon
|
||||
from ui.styles import ThemeColors
|
||||
color = ThemeColors.get('text_primary')
|
||||
for action, name in getattr(self, '_icon_actions', []):
|
||||
action.setIcon(get_icon(name, color, 16))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _call_parent(self, method_name: str):
|
||||
if self.parent_window and hasattr(self.parent_window, method_name):
|
||||
|
||||
Reference in New Issue
Block a user