KINDNICK bedbb1e9ec
Some checks failed
CI / test (push) Has been cancelled
Initial release v2.2.0
핵심 기능:
- 단축근무·표준·반일 등 다양한 근무 패턴 (5개 프리셋 + 사용자 정의)
- Windows 이벤트 뷰어 자동 출퇴근 감지
- 30분 단위 연장근무 적립/사용 시스템
- 1.0/0.5/0.25일 연차·반차·반반차
- 자동 점심·저녁·외출·자동 백업·화면 잠금 자동 외출
- 한국 공휴일 자동 등록 (음력 포함, holidays 패키지)
- matplotlib 차트 기반 주간/월간/패턴 통계
- 미니 위젯 + 시스템 트레이 통합
- 한국어/English i18n
- 자가 업데이트 (updater.exe + Gitea Releases)

아키텍처:
- core/ (db, time_calculator, notifier, i18n, version, settings_keys)
- ui/ (main_window + 9 dialogs + 3 controllers)
- utils/ (backup, lock_detector, debug_log, updater_client, time_format)
- tests/ (66 pytest 단위) + 통합/i18n GUI 검증

CI/CD:
- .gitea/workflows/ci.yml: push 시 pytest + 통합 테스트
- .gitea/workflows/release.yml: v* 태그 push 시 두 .exe 자동 빌드 + Releases 첨부

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:54:40 +09:00

191 lines
5.9 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()
show_action = QAction(tr('tray.open'), self)
show_action.triggered.connect(self.show_window)
menu.addAction(show_action)
mini_action = QAction(tr('tray.mini_widget'), self)
mini_action.triggered.connect(self._open_mini_widget)
menu.addAction(mini_action)
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)
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)
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)
help_action = QAction("📖 " + tr('menu.help'), self)
help_action.triggered.connect(lambda: self._call_parent('show_help'))
menu.addAction(help_action)
menu.addSeparator()
quit_action = QAction(tr('tray.quit'), self)
quit_action.triggered.connect(self.quit_app)
menu.addAction(quit_action)
self.setContextMenu(menu)
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_())