KINDNICK 5fb8655a47 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>
2026-06-04 18:21:54 +09:00

208 lines
6.6 KiB
Python

"""
시스템 트레이 아이콘
"""
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QFont, QColor
from PyQt5.QtCore import Qt, QSize
from core.i18n import tr
class SystemTrayIcon(QSystemTrayIcon):
"""시스템 트레이 아이콘 클래스"""
def __init__(self, parent=None):
# 기본 아이콘 생성
icon = self.create_icon("")
super().__init__(icon, parent)
self.parent_window = parent
self.setup_menu()
# 클릭 이벤트
self.activated.connect(self.on_tray_activated)
def create_icon(self, text: str, color: QColor = None) -> QIcon:
"""
텍스트로 아이콘 생성
Args:
text: 아이콘에 표시할 텍스트 (이모지 또는 시간)
color: 배경색
Returns:
QIcon: 생성된 아이콘
"""
pixmap = QPixmap(64, 64)
if color:
pixmap.fill(color)
else:
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
# 텍스트 그리기
if len(text) <= 2:
# 이모지
font = QFont("Segoe UI Emoji", 40)
else:
# 시간 표시
font = QFont("Consolas", 12, QFont.Bold)
painter.fillRect(pixmap.rect(), QColor(255, 255, 255))
painter.setFont(font)
painter.setPen(QColor(0, 0, 0))
painter.drawText(pixmap.rect(), Qt.AlignCenter, text)
painter.end()
return QIcon(pixmap)
def setup_menu(self):
"""트레이 메뉴 설정 — 라인 아이콘 + 앱 다크 톤."""
menu = QMenu()
# (action, 라인 아이콘 이름) — 테마 전환 시 재틴팅용으로 보관
self._icon_actions = []
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()
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()
add(tr('btn.clock_out'), self.quick_clock_out, 'logout')
menu.addSeparator()
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()
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):
getattr(self.parent_window, method_name)()
def _toggle_lunch(self):
if self.parent_window and hasattr(self.parent_window, 'lunch_button'):
self.parent_window.lunch_button.click()
def _break_out(self):
if self.parent_window and hasattr(self.parent_window, 'break_out'):
self.parent_window.break_out()
def _break_in(self):
if self.parent_window and hasattr(self.parent_window, 'break_in'):
self.parent_window.break_in()
def _open_mini_widget(self):
self._call_parent('show_mini_widget')
def on_tray_activated(self, reason):
"""트레이 아이콘 클릭 시"""
if reason == QSystemTrayIcon.DoubleClick:
self.show_window()
def show_window(self):
"""메인 윈도우 표시"""
if self.parent_window:
self.parent_window.show()
self.parent_window.activateWindow()
def quick_clock_out(self):
"""빠른 퇴근"""
if self.parent_window and hasattr(self.parent_window, 'clock_out'):
self.parent_window.clock_out()
def quit_app(self):
"""앱 종료"""
from PyQt5.QtWidgets import QApplication
QApplication.quit()
def update_time_display(self, remaining_str: str):
"""
남은 시간 표시 업데이트
Args:
remaining_str: "HH:MM" 형식의 시간
"""
# 간단한 시간 표시로 아이콘 업데이트
if remaining_str.startswith('+'):
icon = self.create_icon("🔥")
self.setToolTip(tr('tray.tooltip_overtime', time=remaining_str))
elif remaining_str.startswith('-'):
icon = self.create_icon("")
self.setToolTip(tr('tray.tooltip_remaining', time='--'))
else:
parts = remaining_str.split(':')
display_time = f"{parts[0]}:{parts[1]}" if len(parts) >= 2 else remaining_str
icon = self.create_icon("")
self.setToolTip(tr('tray.tooltip_remaining', time=display_time))
self.setIcon(icon)
# 테스트 코드
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
app = QApplication(sys.argv)
window = QMainWindow()
window.setWindowTitle("Main Window")
tray = SystemTrayIcon(window)
tray.show()
window.show()
sys.exit(app.exec_())