Added — 도전과제 시스템 (153개 자동 평가) - core/achievements.py: 16개 카테고리, 5단계 등급, 시크릿 9개, 메타 도전과제 - ui/achievements_view.py: 4탭 다이얼로그 (전체/진행중/완료/시크릿) - 5분 throttle 자동 평가 + 시스템 알림 + Discord embed push - achievements 테이블 확장 (code/category/tier/is_secret/progress/target) - hire_date 자동 추적, 뷰 진입 카운터 8개 settings 키 Changed — 다크 테마 디자인 리뉴얼 - ui/dark_components.py: 재사용 가능한 다크 컴포넌트 (header/card/button/progress) - 통계/도움말/도전과제 다이얼로그 일관 다크 톤 - matplotlib 차트 다크 테마 적용 (figure/axes/grid/legend) - 등급별 카드 그라디언트 (브론즈/실버/골드/플래티넘/레전드) Fixed — 안정성·일관성 - 타임존 자정 경계 버그 (has_notification_today UTC vs localtime mismatch) - DB 연결 누수: _conn() 컨텍스트 매니저 도입, 40+ 메서드 변환 - DB 이중 부트스트랩 제거 (MainWindow(db=None) 옵션 인자) - crash_handler 다단계 폴백 (DB → 파일 → stderr) - updater PID race: 지수 backoff 재시도 (총 ~9초) - Discord URL 형식 검증 (snowflake regex) - 일일 보고서 저녁 섹션, MealTimeDialog 출/퇴근 범위 검증 - check_dinner_reminder 신규, 알림 임계값 5개 설정화 - closeEvent timer/notifier 정리 (aboutToQuit hook) - 마이그레이션 12개 모두 _conn() + try/finally - DB 인덱스 5개 추가 (break/overtime/leave date) Tests - pytest 116/116 PASS, 통합 시나리오 48/48 PASS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.7 KiB
Python
75 lines
2.7 KiB
Python
"""
|
|
화면 잠금 감지 컨트롤러.
|
|
|
|
- AUTO_BREAK_ON_LOCK: 출근 후 잠금→외출, 해제→복귀
|
|
- CLOCK_IN_ON_UNLOCK: 미출근 상태에서 잠금 해제 시 출근 자동 기록
|
|
"""
|
|
from __future__ import annotations
|
|
from datetime import datetime
|
|
|
|
from core.settings_keys import AUTO_BREAK_ON_LOCK, CLOCK_IN_ON_UNLOCK
|
|
|
|
|
|
class LockMonitor:
|
|
"""MainWindow에서 5초마다 호출되는 잠금 상태 감시자."""
|
|
|
|
def __init__(self, window):
|
|
self.window = window
|
|
self.db = window.db
|
|
self.last_locked: bool = False
|
|
self._detector_failed_once: bool = False # 첫 실패만 로깅 (5초 폴링 노이즈 방지)
|
|
|
|
def tick(self) -> None:
|
|
try:
|
|
from utils.lock_detector import is_screen_locked
|
|
locked = is_screen_locked()
|
|
except Exception as e:
|
|
if not self._detector_failed_once:
|
|
self._detector_failed_once = True
|
|
from utils.debug_log import dlog
|
|
dlog(f"lock detector failed (silenced after first): {e}")
|
|
return
|
|
|
|
was_locked = self.last_locked
|
|
self.last_locked = locked
|
|
|
|
# 출근 후 자동 외출/복귀
|
|
if (self.db.get_setting(AUTO_BREAK_ON_LOCK, 'false').lower() == 'true'
|
|
and self.window.is_clocked_in):
|
|
if locked and not was_locked and not self.window.is_on_break:
|
|
self.window.break_out(silent=True)
|
|
elif not locked and was_locked and self.window.is_on_break:
|
|
self.window.break_in(silent=True)
|
|
|
|
# 미출근 상태에서 잠금 해제 시 출근
|
|
if (not locked and was_locked
|
|
and not self.window.is_clocked_in
|
|
and self.db.get_setting(CLOCK_IN_ON_UNLOCK, 'false').lower() == 'true'):
|
|
now = datetime.now()
|
|
today_record = self.db.get_today_record()
|
|
if not today_record or not today_record.get('clock_in'):
|
|
self._auto_clock_in_at(now)
|
|
|
|
def _auto_clock_in_at(self, when: datetime) -> None:
|
|
w = self.window
|
|
w.clock_in_time = when
|
|
w.is_clocked_in = True
|
|
w.midnight_rollover_handled = False
|
|
w.auto_lunch_applied_today = False
|
|
|
|
today = when.date().isoformat()
|
|
clock_in_str = when.strftime("%H:%M:%S")
|
|
existing = self.db.get_today_record()
|
|
if existing:
|
|
conn = self.db.get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"UPDATE work_records SET clock_in = ?, is_manual = 0 WHERE date = ?",
|
|
(clock_in_str, today),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
else:
|
|
self.db.add_work_record(today, clock_in_str)
|
|
w.update_display()
|