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:
KINDNICK
2026-06-04 18:21:54 +09:00
co-authored by Claude Opus 4.8
parent da5f91984b
commit 5fb8655a47
47 changed files with 948 additions and 425 deletions
+84
View File
@@ -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
View File
@@ -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):