Some checks failed
CI / test (push) Has been cancelled
핵심 기능: - 단축근무·표준·반일 등 다양한 근무 패턴 (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>
112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""
|
|
matplotlib 기반 차트 위젯.
|
|
|
|
stats_view에서 주간/월간 추세를 시각화. matplotlib 미설치 시
|
|
ImportError 안내 라벨로 fallback.
|
|
"""
|
|
from typing import List, Tuple
|
|
|
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
|
|
from PyQt5.QtCore import Qt
|
|
|
|
try:
|
|
from matplotlib.figure import Figure
|
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
import matplotlib
|
|
matplotlib.rcParams['font.family'] = ['Malgun Gothic', 'Apple SD Gothic Neo', 'sans-serif']
|
|
matplotlib.rcParams['axes.unicode_minus'] = False
|
|
_MPL = True
|
|
except ImportError:
|
|
_MPL = False
|
|
|
|
|
|
class _Fallback(QWidget):
|
|
"""matplotlib 미설치 시 안내."""
|
|
def __init__(self, message: str):
|
|
super().__init__()
|
|
layout = QVBoxLayout()
|
|
label = QLabel(message)
|
|
label.setAlignment(Qt.AlignCenter)
|
|
label.setWordWrap(True)
|
|
label.setStyleSheet("color: #888; padding: 20px;")
|
|
layout.addWidget(label)
|
|
self.setLayout(layout)
|
|
|
|
|
|
def make_chart_widget(parent=None) -> QWidget:
|
|
"""차트가 그려질 빈 캔버스 위젯. matplotlib 없으면 fallback."""
|
|
if not _MPL:
|
|
return _Fallback("차트 표시에는 matplotlib가 필요합니다.\npip install matplotlib")
|
|
widget = QWidget(parent)
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
fig = Figure(figsize=(5, 3), dpi=100, tight_layout=True)
|
|
canvas = FigureCanvas(fig)
|
|
layout.addWidget(canvas)
|
|
widget.setLayout(layout)
|
|
widget._figure = fig
|
|
widget._canvas = canvas
|
|
return widget
|
|
|
|
|
|
def draw_daily_hours(widget: QWidget, records: List[dict]) -> None:
|
|
"""일별 근무시간 막대 그래프.
|
|
|
|
Args:
|
|
widget: make_chart_widget()로 만든 위젯
|
|
records: [{date, total_hours, overtime_minutes}, ...]
|
|
"""
|
|
if not getattr(widget, '_figure', None):
|
|
return
|
|
fig = widget._figure
|
|
fig.clear()
|
|
if not records:
|
|
ax = fig.add_subplot(111)
|
|
ax.text(0.5, 0.5, '기록 없음', ha='center', va='center', transform=ax.transAxes)
|
|
widget._canvas.draw()
|
|
return
|
|
|
|
dates = [r['date'][5:] for r in records] # MM-DD만
|
|
hours = [r.get('total_hours', 0) or 0 for r in records]
|
|
overtimes = [(r.get('overtime_minutes', 0) or 0) / 60 for r in records]
|
|
base = [max(h - o, 0) for h, o in zip(hours, overtimes)]
|
|
|
|
ax = fig.add_subplot(111)
|
|
ax.bar(dates, base, label='정상', color='#4a90e2')
|
|
ax.bar(dates, overtimes, bottom=base, label='연장', color='#ff6b6b')
|
|
ax.set_ylabel('시간')
|
|
ax.legend(loc='upper left', fontsize=8)
|
|
ax.tick_params(axis='x', labelrotation=45, labelsize=8)
|
|
ax.grid(axis='y', alpha=0.3)
|
|
widget._canvas.draw()
|
|
|
|
|
|
def draw_weekday_avg(widget: QWidget, records: List[dict]) -> None:
|
|
"""요일별 평균 근무시간 막대 그래프."""
|
|
if not getattr(widget, '_figure', None):
|
|
return
|
|
fig = widget._figure
|
|
fig.clear()
|
|
|
|
from datetime import datetime as _dt
|
|
weekday_totals = [0.0] * 7
|
|
weekday_counts = [0] * 7
|
|
for r in records:
|
|
try:
|
|
d = _dt.strptime(r['date'], '%Y-%m-%d')
|
|
except (ValueError, TypeError):
|
|
continue
|
|
weekday_totals[d.weekday()] += r.get('total_hours', 0) or 0
|
|
weekday_counts[d.weekday()] += 1
|
|
|
|
avg = [(t / c) if c else 0 for t, c in zip(weekday_totals, weekday_counts)]
|
|
labels = ['월', '화', '수', '목', '금', '토', '일']
|
|
|
|
ax = fig.add_subplot(111)
|
|
colors = ['#4a90e2'] * 5 + ['#ff6b6b'] * 2 # 주말 강조
|
|
ax.bar(labels, avg, color=colors)
|
|
ax.set_ylabel('평균 시간')
|
|
ax.set_title('요일별 평균 근무시간')
|
|
ax.grid(axis='y', alpha=0.3)
|
|
widget._canvas.draw()
|