Compare commits
No commits in common. "6a17876af1144a9636efcaa04cbd185865b8dbc6" and "9ebf4ad9613b32823bb0d90008c20f44d1fdff89" have entirely different histories.
6a17876af1
...
9ebf4ad961
31
CHANGELOG.md
31
CHANGELOG.md
@ -4,37 +4,6 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [2.6.0] — 2026-04-30
|
||||
|
||||
### Added — Phase 4 (3종)
|
||||
- **글꼴 크기 조절** (100% / 125% / 150%) — 설정에서 즉시 반영
|
||||
- **고대비 모드** — 검정 배경 + 노란 텍스트 (시각약자/야간)
|
||||
- **코드 서명 인프라** — `release.ps1`에 Authenticode 서명 단계 추가 (옵션)
|
||||
- `$env:CODE_SIGN_CERT` (`.pfx` 경로) + `$env:CODE_SIGN_PASS` 환경변수 설정 시 자동 서명
|
||||
- signtool.exe 없거나 cert 미설정 시 자동 스킵
|
||||
- 코드 서명 인증서 확보 후 활성화하면 SmartScreen 경고 제거 가능
|
||||
|
||||
### Settings (신규)
|
||||
- `font_scale`, `high_contrast`
|
||||
|
||||
## [2.5.0] — 2026-04-30
|
||||
|
||||
### Added — Phase 3 (4종)
|
||||
- **주간 자동 리포트** — 월요일 첫 출근 시 (또는 첫 5분 tick) 지난주 요약 발송
|
||||
- 시스템 알림 + Discord push (옵션) 동시
|
||||
- `notification_log`로 중복 발송 방지
|
||||
- 항목: 총 근무·일평균·연장근무·가장 긴 날
|
||||
- **matplotlib 차트 호버 디테일** — 막대 위에 마우스 올리면 정확한 수치 툴팁
|
||||
- 일별 근무 시간 차트(주간 탭)에 적용
|
||||
- **출근 시각 분포 차트** — 패턴 분석 탭에 30분 단위 히스토그램
|
||||
- 평균 출근 시각 빨간 점선으로 표시
|
||||
- **휴가 캘린더 시각화** — 연차 관리 → "📅 캘린더 보기"
|
||||
- 사용 일자에 종일/반차/반반차별 색상 표시
|
||||
- 날짜 클릭 → 사용 내역 표시
|
||||
|
||||
### Fixed
|
||||
- `leave_view.py` setLayout 들여쓰기 회귀 수정
|
||||
|
||||
## [2.4.0] — 2026-04-30
|
||||
|
||||
### Added — Phase 2 (5종)
|
||||
|
||||
@ -653,11 +653,8 @@ class Database:
|
||||
'discord_notif_clock_out': 'true',
|
||||
'discord_notif_health': 'true',
|
||||
# v2.4.0
|
||||
'goal_overtime_max_monthly': '0',
|
||||
'goal_avg_hours_daily': '0',
|
||||
# v2.6.0
|
||||
'font_scale': '1.0',
|
||||
'high_contrast': 'false',
|
||||
'goal_overtime_max_monthly': '0', # 0=비활성, >0=분 단위 상한
|
||||
'goal_avg_hours_daily': '0', # 0=비활성
|
||||
}
|
||||
|
||||
conn = self.get_connection()
|
||||
|
||||
@ -37,8 +37,6 @@ THEME = 'theme'
|
||||
TIME_FORMAT = 'time_format'
|
||||
LANGUAGE = 'language'
|
||||
OVERTIME_UNIT = 'overtime_unit'
|
||||
FONT_SCALE = 'font_scale' # '1.0' / '1.25' / '1.5'
|
||||
HIGH_CONTRAST = 'high_contrast'
|
||||
|
||||
# 통합/외부
|
||||
DB_PATH_OVERRIDE = 'db_path_override'
|
||||
|
||||
@ -4,4 +4,4 @@
|
||||
릴리스 시 이 값을 올린 후 git tag → push.
|
||||
CHANGELOG.md의 최상단 항목과 일치시킬 것.
|
||||
"""
|
||||
__version__ = '2.6.0'
|
||||
__version__ = '2.4.0'
|
||||
|
||||
21
release.ps1
21
release.ps1
@ -141,27 +141,6 @@ $mainSize = "{0:N1}MB" -f ((Get-Item dist/main.exe).Length / 1MB)
|
||||
$updaterSize = "{0:N1}MB" -f ((Get-Item dist/updater.exe).Length / 1MB)
|
||||
OkMsg "main.exe ($mainSize) + updater.exe ($updaterSize)"
|
||||
|
||||
# Optional: Authenticode signing if cert available (env vars)
|
||||
# Set CODE_SIGN_CERT (path to .pfx) and CODE_SIGN_PASS to enable
|
||||
if ($env:CODE_SIGN_CERT -and (Test-Path $env:CODE_SIGN_CERT)) {
|
||||
Info "Code signing exes (Authenticode)..."
|
||||
$signtool = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
||||
if (-not $signtool) {
|
||||
Info " signtool.exe not found in PATH — skipping signature"
|
||||
} else {
|
||||
$tsUrl = if ($env:CODE_SIGN_TIMESTAMP) { $env:CODE_SIGN_TIMESTAMP } else { 'http://timestamp.digicert.com' }
|
||||
foreach ($exe in 'dist/main.exe', 'dist/updater.exe') {
|
||||
$args = @('sign', '/f', $env:CODE_SIGN_CERT)
|
||||
if ($env:CODE_SIGN_PASS) { $args += '/p'; $args += $env:CODE_SIGN_PASS }
|
||||
$args += '/tr'; $args += $tsUrl; $args += '/td'; $args += 'sha256'
|
||||
$args += '/fd'; $args += 'sha256'; $args += $exe
|
||||
$rc = Invoke-Native signtool $args
|
||||
if ($rc -ne 0) { Info " WARN: sign failed for $exe (exit $rc)" }
|
||||
else { OkMsg " signed $exe" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ====== 4. ZIP ======
|
||||
Step "4/7 ZIP packaging"
|
||||
$zipPath = "dist/ClockOutCalculator-$Version.zip"
|
||||
|
||||
@ -1,82 +0,0 @@
|
||||
"""
|
||||
접근성 — 글꼴 크기 / 고대비 모드 적용.
|
||||
|
||||
QApplication 글로벌 폰트 + 추가 QSS 오버레이.
|
||||
설정 변경 즉시 반영 (재시작 불필요).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
|
||||
# 고대비 QSS — 검정 배경 + 노란 텍스트 + 굵은 테두리
|
||||
HIGH_CONTRAST_QSS = """
|
||||
* {
|
||||
background-color: #000000;
|
||||
color: #FFEB3B;
|
||||
border-color: #FFEB3B;
|
||||
}
|
||||
QPushButton, QLineEdit, QSpinBox, QComboBox, QTextEdit, QTableWidget, QGroupBox {
|
||||
border: 2px solid #FFEB3B;
|
||||
background-color: #000000;
|
||||
color: #FFEB3B;
|
||||
}
|
||||
QPushButton:hover { background-color: #333333; }
|
||||
QPushButton:pressed { background-color: #FFEB3B; color: #000000; }
|
||||
QPushButton:disabled { color: #888; border-color: #888; }
|
||||
QGroupBox::title { color: #FFEB3B; padding: 0 4px; }
|
||||
QProgressBar { border: 2px solid #FFEB3B; }
|
||||
QProgressBar::chunk { background-color: #FFEB3B; }
|
||||
QToolTip { background-color: #000; color: #FFEB3B; border: 2px solid #FFEB3B; }
|
||||
"""
|
||||
|
||||
|
||||
def apply_font_scale(scale: float) -> None:
|
||||
"""전역 글꼴 크기 배율 적용 (1.0 = 기본, 1.25 = 125%, 1.5 = 150%)."""
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
return
|
||||
base = app.font()
|
||||
if base.pointSize() > 0:
|
||||
# 기존 배율 무시하고 새 배율로 (기본 9pt 가정)
|
||||
base_pt = 9
|
||||
base.setPointSize(int(round(base_pt * scale)))
|
||||
else:
|
||||
base_px = 12
|
||||
base.setPixelSize(int(round(base_px * scale)))
|
||||
app.setFont(base)
|
||||
|
||||
|
||||
def apply_high_contrast(enabled: bool, base_qss: str = "") -> None:
|
||||
"""고대비 모드 ON/OFF. base_qss는 평소 테마 QSS (OFF 시 복원용)."""
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
return
|
||||
if enabled:
|
||||
app.setStyleSheet(base_qss + "\n" + HIGH_CONTRAST_QSS)
|
||||
else:
|
||||
app.setStyleSheet(base_qss)
|
||||
|
||||
|
||||
def apply_from_settings(db) -> None:
|
||||
"""db에서 font_scale + high_contrast 읽어 적용."""
|
||||
try:
|
||||
scale = float(db.get_setting('font_scale', '1.0') or 1.0)
|
||||
except (ValueError, TypeError):
|
||||
scale = 1.0
|
||||
scale = max(0.8, min(2.0, scale))
|
||||
apply_font_scale(scale)
|
||||
|
||||
enabled = db.get_setting('high_contrast', 'false').lower() == 'true'
|
||||
# base_qss는 main_window에서 apply_theme() 호출 직후 적용되므로,
|
||||
# 여기서는 현재 styleSheet 그대로 두고 high_contrast만 추가.
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
return
|
||||
current = app.styleSheet() or ""
|
||||
# 기존에 추가된 HIGH_CONTRAST_QSS 제거
|
||||
base = current.replace(HIGH_CONTRAST_QSS, "").rstrip() + "\n"
|
||||
if enabled:
|
||||
app.setStyleSheet(base + HIGH_CONTRAST_QSS)
|
||||
else:
|
||||
app.setStyleSheet(base.rstrip())
|
||||
@ -50,7 +50,12 @@ def make_chart_widget(parent=None) -> QWidget:
|
||||
|
||||
|
||||
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
|
||||
@ -62,101 +67,17 @@ def draw_daily_hours(widget: QWidget, records: List[dict]) -> None:
|
||||
return
|
||||
|
||||
dates = [r['date'][5:] for r in records] # MM-DD만
|
||||
full_dates = [r['date'] for r in records]
|
||||
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)
|
||||
bars_base = ax.bar(dates, base, label='정상', color='#4a90e2')
|
||||
bars_ot = ax.bar(dates, overtimes, bottom=base, label='연장', color='#ff6b6b')
|
||||
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)
|
||||
|
||||
# 호버 annotation 설정
|
||||
annot = ax.annotate(
|
||||
"", xy=(0, 0), xytext=(15, 15),
|
||||
textcoords="offset points",
|
||||
bbox=dict(boxstyle="round,pad=0.4", fc="#222", ec="#888", alpha=0.95),
|
||||
color="white", fontsize=9,
|
||||
arrowprops=dict(arrowstyle="->", color="#888"),
|
||||
)
|
||||
annot.set_visible(False)
|
||||
|
||||
def on_hover(event):
|
||||
if event.inaxes != ax:
|
||||
if annot.get_visible():
|
||||
annot.set_visible(False)
|
||||
widget._canvas.draw_idle()
|
||||
return
|
||||
for bars, kind in ((bars_base, 'base'), (bars_ot, 'ot')):
|
||||
for i, bar in enumerate(bars):
|
||||
if bar.contains(event)[0]:
|
||||
h = hours[i]; ot = overtimes[i]
|
||||
text = f"▼ {full_dates[i]}\n근무 {h:.1f}h"
|
||||
if ot > 0:
|
||||
text += f"\n연장 +{ot:.1f}h"
|
||||
annot.xy = (bar.get_x() + bar.get_width() / 2, bar.get_height() + bar.get_y())
|
||||
annot.set_text(text)
|
||||
annot.set_visible(True)
|
||||
widget._canvas.draw_idle()
|
||||
return
|
||||
if annot.get_visible():
|
||||
annot.set_visible(False)
|
||||
widget._canvas.draw_idle()
|
||||
|
||||
widget._canvas.mpl_connect("motion_notify_event", on_hover)
|
||||
widget._canvas.draw()
|
||||
|
||||
|
||||
def draw_clock_in_distribution(widget: QWidget, records: List[dict]) -> None:
|
||||
"""출근 시각 분포 히스토그램 (30분 빈)."""
|
||||
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
|
||||
|
||||
# 출근 시각을 분 단위로 (00:00=0)
|
||||
minutes_list = []
|
||||
for r in records:
|
||||
ci = r.get('clock_in')
|
||||
if not ci:
|
||||
continue
|
||||
parts = ci.split(':')
|
||||
if len(parts) >= 2:
|
||||
try:
|
||||
minutes_list.append(int(parts[0]) * 60 + int(parts[1]))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not minutes_list:
|
||||
return
|
||||
|
||||
# 30분 빈
|
||||
bin_size = 30
|
||||
min_m = (min(minutes_list) // bin_size) * bin_size
|
||||
max_m = ((max(minutes_list) // bin_size) + 1) * bin_size
|
||||
bins = list(range(min_m, max_m + bin_size, bin_size))
|
||||
|
||||
ax = fig.add_subplot(111)
|
||||
ax.hist(minutes_list, bins=bins, color='#4a90e2', edgecolor='white')
|
||||
avg = sum(minutes_list) / len(minutes_list)
|
||||
ax.axvline(avg, color='#ff6b6b', linestyle='--',
|
||||
label=f'평균 {int(avg//60):02d}:{int(avg%60):02d}')
|
||||
ax.set_xticks([m for m in bins if m % 60 == 0])
|
||||
ax.set_xticklabels([f"{m//60:02d}:00" for m in bins if m % 60 == 0],
|
||||
rotation=45, fontsize=8)
|
||||
ax.set_ylabel('일수')
|
||||
ax.set_title('출근 시각 분포')
|
||||
ax.legend(loc='upper right', fontsize=8)
|
||||
ax.grid(axis='y', alpha=0.3)
|
||||
widget._canvas.draw()
|
||||
|
||||
|
||||
|
||||
@ -17,58 +17,6 @@ class NotificationOrchestrator:
|
||||
self.notifier = window.notifier
|
||||
self._last_5min_bucket: int | None = None # now.minute (5의 배수일 때만)
|
||||
|
||||
def maybe_send_weekly_report(self, now: datetime) -> None:
|
||||
"""월요일 첫 update_display 호출 시 지난주 요약 발송 (시스템 + Discord).
|
||||
|
||||
notification_log로 중복 가드. 월요일이 아니거나 이미 보냈으면 no-op.
|
||||
"""
|
||||
if now.weekday() != 0: # 0=월요일
|
||||
return
|
||||
if self.db.has_notification_today('system', 'weekly_report'):
|
||||
return
|
||||
# 지난주 데이터 (월~일)
|
||||
from datetime import timedelta as _td
|
||||
last_mon = now.date() - _td(days=7)
|
||||
last_sun = now.date() - _td(days=1)
|
||||
records = self.db.get_work_records_by_range(last_mon.isoformat(), last_sun.isoformat())
|
||||
closed = [r for r in records if r.get('clock_out')]
|
||||
if not closed:
|
||||
return # 지난주 기록 없음
|
||||
total_h = sum((r.get('total_hours') or 0) for r in closed)
|
||||
ot_total = sum((r.get('overtime_minutes') or 0) for r in closed)
|
||||
ot_h, ot_m = ot_total // 60, ot_total % 60
|
||||
avg_h = total_h / len(closed) if closed else 0
|
||||
longest = max(closed, key=lambda r: r.get('total_hours') or 0)
|
||||
longest_str = f"{longest['date']} ({longest.get('total_hours', 0):.1f}h)"
|
||||
|
||||
title = "📊 지난주 요약"
|
||||
body = (f"기간: {last_mon} ~ {last_sun}\n"
|
||||
f"총 근무: {total_h:.1f}시간 ({len(closed)}일)\n"
|
||||
f"일 평균: {avg_h:.1f}시간\n"
|
||||
f"연장근무: {ot_h}시간 {ot_m}분\n"
|
||||
f"가장 긴 날: {longest_str}")
|
||||
self.notifier.notification_signal.emit(title, body)
|
||||
self.db.log_notification('system', 'weekly_report')
|
||||
|
||||
# Discord 도 옵션 활성 시 push
|
||||
if self.db.get_setting('discord_notif_clock_out', 'true').lower() == 'true':
|
||||
url = self.db.get_setting('discord_webhook_url', '') or ''
|
||||
if url:
|
||||
try:
|
||||
from utils.discord_webhook import send, COLOR_BLUE
|
||||
fields = [
|
||||
{"name": "총 근무", "value": f"{total_h:.1f}시간 ({len(closed)}일)", "inline": True},
|
||||
{"name": "일 평균", "value": f"{avg_h:.1f}시간", "inline": True},
|
||||
{"name": "연장근무", "value": f"{ot_h}시간 {ot_m}분", "inline": True},
|
||||
{"name": "가장 긴 날", "value": longest_str, "inline": False},
|
||||
]
|
||||
ok = send(url, "📊 지난주 요약",
|
||||
f"기간: {last_mon} ~ {last_sun}",
|
||||
color=COLOR_BLUE, fields=fields)
|
||||
self.db.log_notification('discord', 'weekly_report', success=ok)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def tick(self, now: datetime, expected_clock_out: datetime, remaining_seconds: float) -> None:
|
||||
n = self.notifier
|
||||
# 1초마다 체크: 30분 전, 점심 미등록, 연장 적립
|
||||
@ -78,13 +26,10 @@ class NotificationOrchestrator:
|
||||
if remaining_seconds < 0:
|
||||
n.check_overtime_earning(abs(int(remaining_seconds / 60)))
|
||||
|
||||
# 5분 간격 throttle: 건강/주간/누적/휴식권고/주간리포트
|
||||
# 5분 간격 throttle: 건강/주간/누적/휴식권고
|
||||
if now.minute % 5 == 0 and self._last_5min_bucket != now.minute:
|
||||
self._last_5min_bucket = now.minute
|
||||
|
||||
# 월요일 첫 출근 시 지난주 리포트
|
||||
self.maybe_send_weekly_report(now)
|
||||
|
||||
# 휴식 권고 (장시간 연속 근무)
|
||||
break_minutes = self.db.get_total_break_minutes_today()
|
||||
n.check_health_break(self.window.clock_in_time, break_minutes, now)
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
"""
|
||||
연차 사용 캘린더 시각화.
|
||||
|
||||
QCalendarWidget에 사용 연차일을 색칠로 표시.
|
||||
- 1.0일: 진한 색
|
||||
- 0.5일(반차): 중간 색
|
||||
- 0.25일(반반차): 옅은 색
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QPushButton, QCalendarWidget)
|
||||
from PyQt5.QtCore import Qt, QDate
|
||||
from PyQt5.QtGui import QTextCharFormat, QColor, QBrush
|
||||
|
||||
from ui.styles import apply_dark_titlebar
|
||||
|
||||
|
||||
class LeaveCalendarView(QDialog):
|
||||
"""연차 캘린더 시각화."""
|
||||
|
||||
def __init__(self, parent=None, db=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
self.setWindowTitle("📅 연차 캘린더")
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(540, 480)
|
||||
self._build_ui()
|
||||
self._mark_dates()
|
||||
apply_dark_titlebar(self)
|
||||
|
||||
def _build_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# 헤더: 잔여 + 범례
|
||||
header = QHBoxLayout()
|
||||
balance = float(self.db.get_setting('leave_balance', '0') or 0)
|
||||
total = float(self.db.get_setting('annual_leave_total', '15') or 15)
|
||||
used = total - balance
|
||||
title = QLabel(f"🌴 잔여 {balance:.2f}일 / 총 {total:.0f}일 (사용 {used:.2f}일)")
|
||||
title.setStyleSheet("font-weight: bold; font-size: 13px;")
|
||||
header.addWidget(title)
|
||||
header.addStretch()
|
||||
layout.addLayout(header)
|
||||
|
||||
# 범례
|
||||
legend = QHBoxLayout()
|
||||
for label, color in [("🟩 종일(1.0)", "#4caf50"),
|
||||
("🟨 반차(0.5)", "#ffc107"),
|
||||
("🟪 반반차(0.25)", "#9c27b0")]:
|
||||
l = QLabel(label)
|
||||
l.setStyleSheet(f"padding: 2px 6px;")
|
||||
legend.addWidget(l)
|
||||
legend.addStretch()
|
||||
layout.addLayout(legend)
|
||||
|
||||
# 캘린더
|
||||
self.calendar = QCalendarWidget()
|
||||
self.calendar.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
|
||||
self.calendar.clicked.connect(self._on_date_click)
|
||||
layout.addWidget(self.calendar, 1)
|
||||
|
||||
# 선택 일자 정보
|
||||
self.detail_label = QLabel("")
|
||||
self.detail_label.setStyleSheet("padding: 6px; color: #888;")
|
||||
layout.addWidget(self.detail_label)
|
||||
|
||||
# 닫기 버튼
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
close_btn = QPushButton("닫기")
|
||||
close_btn.clicked.connect(self.close)
|
||||
btn_row.addWidget(close_btn)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _mark_dates(self):
|
||||
"""연차 사용 일자에 색상 표시."""
|
||||
records = self.db.get_all_leave_records(limit=365)
|
||||
for r in records:
|
||||
try:
|
||||
d = datetime.strptime(r['date'], '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
qd = QDate(d.year, d.month, d.day)
|
||||
days = float(r.get('days') or 0)
|
||||
if days >= 1.0:
|
||||
color = QColor("#4caf50")
|
||||
elif days >= 0.5:
|
||||
color = QColor("#ffc107")
|
||||
else:
|
||||
color = QColor("#9c27b0")
|
||||
fmt = QTextCharFormat()
|
||||
fmt.setBackground(QBrush(color))
|
||||
fmt.setForeground(QBrush(QColor("white")))
|
||||
self.calendar.setDateTextFormat(qd, fmt)
|
||||
|
||||
def _on_date_click(self, qdate):
|
||||
date_str = qdate.toString('yyyy-MM-dd')
|
||||
records = self.db.get_all_leave_records(limit=365)
|
||||
match = [r for r in records if r['date'] == date_str]
|
||||
if not match:
|
||||
self.detail_label.setText(f"{date_str} — 연차 사용 없음")
|
||||
return
|
||||
parts = []
|
||||
for r in match:
|
||||
t = r.get('leave_type', 'annual')
|
||||
d = float(r.get('days') or 0)
|
||||
memo = r.get('memo') or ''
|
||||
parts.append(f"{t} {d}일" + (f" ({memo})" if memo else ""))
|
||||
self.detail_label.setText(f"📅 {date_str}: " + ", ".join(parts))
|
||||
@ -80,11 +80,6 @@ class LeaveView(QDialog):
|
||||
add_leave_button = QPushButton("➕ 연차 사용 추가")
|
||||
add_leave_button.clicked.connect(self.add_leave_record)
|
||||
button_layout.addWidget(add_leave_button)
|
||||
|
||||
cal_button = QPushButton("📅 캘린더 보기")
|
||||
cal_button.clicked.connect(self._show_calendar)
|
||||
button_layout.addWidget(cal_button)
|
||||
|
||||
close_button = QPushButton("닫기")
|
||||
close_button.clicked.connect(self.close)
|
||||
button_layout.addWidget(close_button)
|
||||
@ -92,11 +87,6 @@ class LeaveView(QDialog):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _show_calendar(self):
|
||||
from ui.leave_calendar_view import LeaveCalendarView
|
||||
dlg = LeaveCalendarView(self, self.db)
|
||||
dlg.exec_()
|
||||
|
||||
def load_data(self):
|
||||
"""데이터 로드"""
|
||||
# 잔액 업데이트
|
||||
|
||||
@ -59,13 +59,6 @@ class MainWindow(QMainWindow):
|
||||
from core.i18n import set_language
|
||||
set_language(self.db.get_setting(LANGUAGE, 'ko') or 'ko')
|
||||
|
||||
# 접근성 — 글꼴 크기 + 고대비
|
||||
try:
|
||||
from ui.accessibility import apply_from_settings as _apply_a11y
|
||||
_apply_a11y(self.db)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# TimeCalculator 초기화 (설정값 반영)
|
||||
settings = self.db.get_settings()
|
||||
|
||||
@ -2011,13 +2004,6 @@ class MainWindow(QMainWindow):
|
||||
# auto_lunch 캐시 무효화 (설정에서 토글 가능하므로)
|
||||
self._auto_lunch.invalidate()
|
||||
|
||||
# 접근성 재적용 (글꼴 / 고대비)
|
||||
try:
|
||||
from ui.accessibility import apply_from_settings as _apply_a11y
|
||||
_apply_a11y(self.db)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# UI 업데이트
|
||||
self.update_overtime_balance()
|
||||
self.update_leave_balance()
|
||||
|
||||
@ -24,7 +24,6 @@ from core.settings_keys import (
|
||||
DB_PATH_OVERRIDE, CLOCK_IN_ON_UNLOCK,
|
||||
GOAL_OVERTIME_MAX_MONTHLY, GOAL_AVG_HOURS_DAILY,
|
||||
GITEA_FEEDBACK_TOKEN, GITEA_FEEDBACK_ENABLED,
|
||||
FONT_SCALE, HIGH_CONTRAST,
|
||||
)
|
||||
from utils.csv_exporter import CSVExporter
|
||||
from ui.leave_view import AddLeaveDialog
|
||||
@ -324,22 +323,6 @@ class SettingsView(QDialog):
|
||||
format_row.addStretch()
|
||||
layout.addLayout(format_row)
|
||||
|
||||
# 접근성: 글꼴 크기 + 고대비
|
||||
a11y_row = QHBoxLayout()
|
||||
a11y_row.addWidget(QLabel("글꼴 크기:"))
|
||||
self.font_scale_combo = QComboBox()
|
||||
self.font_scale_combo.addItem("100%", "1.0")
|
||||
self.font_scale_combo.addItem("125%", "1.25")
|
||||
self.font_scale_combo.addItem("150%", "1.5")
|
||||
self.font_scale_combo.setFixedWidth(90)
|
||||
a11y_row.addWidget(self.font_scale_combo)
|
||||
a11y_row.addSpacing(16)
|
||||
self.high_contrast_check = QCheckBox("고대비 모드")
|
||||
self.high_contrast_check.setToolTip("검정 배경 + 노란 텍스트 (시각약자/야간)")
|
||||
a11y_row.addWidget(self.high_contrast_check)
|
||||
a11y_row.addStretch()
|
||||
layout.addLayout(a11y_row)
|
||||
|
||||
# 언어 선택
|
||||
from core.i18n import available_languages, language_label
|
||||
lang_row = QHBoxLayout()
|
||||
@ -921,15 +904,6 @@ class SettingsView(QDialog):
|
||||
settings.get(GITEA_FEEDBACK_ENABLED, False)
|
||||
)
|
||||
|
||||
# 접근성
|
||||
if hasattr(self, 'font_scale_combo'):
|
||||
scale = str(settings.get(FONT_SCALE, '1.0'))
|
||||
idx = self.font_scale_combo.findData(scale)
|
||||
if idx >= 0:
|
||||
self.font_scale_combo.setCurrentIndex(idx)
|
||||
if hasattr(self, 'high_contrast_check'):
|
||||
self.high_contrast_check.setChecked(settings.get(HIGH_CONTRAST, False))
|
||||
|
||||
# 알림
|
||||
self.clock_out_notification_check.setChecked(settings.get(NOTIF_CLOCK_OUT, True))
|
||||
self.lunch_notification_check.setChecked(settings.get(NOTIF_LUNCH, True))
|
||||
@ -1084,10 +1058,6 @@ class SettingsView(QDialog):
|
||||
self.db.set_setting(GITEA_FEEDBACK_TOKEN, self.gitea_token_edit.text().strip())
|
||||
if hasattr(self, 'gitea_feedback_enabled_check'):
|
||||
settings[GITEA_FEEDBACK_ENABLED] = self.gitea_feedback_enabled_check.isChecked()
|
||||
if hasattr(self, 'font_scale_combo'):
|
||||
settings[FONT_SCALE] = self.font_scale_combo.currentData()
|
||||
if hasattr(self, 'high_contrast_check'):
|
||||
settings[HIGH_CONTRAST] = self.high_contrast_check.isChecked()
|
||||
if hasattr(self, 'language_combo'):
|
||||
settings[LANGUAGE] = self.language_combo.currentData()
|
||||
|
||||
|
||||
@ -165,11 +165,7 @@ class StatsView(QDialog):
|
||||
pattern_group.setLayout(pattern_layout)
|
||||
layout.addWidget(pattern_group)
|
||||
|
||||
# 출근 시각 분포 차트
|
||||
from ui.chart_widget import make_chart_widget
|
||||
self.clock_in_chart = make_chart_widget(widget)
|
||||
layout.addWidget(self.clock_in_chart, 1)
|
||||
|
||||
layout.addStretch()
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
@ -257,11 +253,6 @@ class StatsView(QDialog):
|
||||
|
||||
def analyze_patterns(self, records):
|
||||
"""패턴 분석"""
|
||||
# 출근 분포 차트는 데이터 유무와 무관하게 갱신 (빈 차트 표시)
|
||||
if hasattr(self, 'clock_in_chart'):
|
||||
from ui.chart_widget import draw_clock_in_distribution
|
||||
draw_clock_in_distribution(self.clock_in_chart, records or [])
|
||||
|
||||
if not records:
|
||||
self.pattern_text.setText(tr('stats.no_data'))
|
||||
return
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user