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

9.3 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Clock-out Time Calculator (퇴근시간 계산기) is a Windows desktop app that auto-detects clock-in via Windows Event Log, calculates clock-out time, banks overtime in 30-min units, and tracks leave/breaks. Korean UI by default with English (i18n switchable).

Tech Stack: Python 3.9+, PyQt5, SQLite, pywin32, matplotlib, optional holidays, optional anthropic.

Companion docs: AGENTS.md, INSTALL.md, README.md.

Build and Run

pip install -r requirements.txt
python main.py

# Standalone module tests
python core/event_monitor.py
python core/time_calculator.py

# Production build → dist/main.exe
python -m PyInstaller --clean main.spec

# Integration tests (35 + 5 + view scenarios)
python _integration_test.py
python _i18n_gui_test.py
python _gui_smoke_test.py

PyInstaller fails with PermissionError if dist/main.exe is running — kill it first. _integration_test.py and _gui_smoke_test.py are intentionally hidden behind a leading underscore so they don't ship in the build.

Architecture

Core (core/)

  • database.py — SQLite. 8 tables (work_records, overtime_bank, overtime_usage, leave_records, break_records, settings, achievements, holidays). Runtime migrations (migrate_* methods called from init_database()) ALTER existing DBs on startup. Helpers: get_setting_int/float/bool(), get_work_minutes(), get_consecutive_overtime_days(), add_korean_holidays_auto().
  • time_calculator.py — Internal representation is work_minutes: int. work_hours is a read-only property (compatibility shim for legacy callers / float input). 30-min truncation in calculate_overtime().
  • event_monitor.py — Reads Win Event IDs 6005 (boot), 4624 (login), 6006 (shutdown). Admin may be required.
  • notifier.py — 6 notifications, each gated by setting key (NOTIF_CLOCK_OUT/LUNCH/OVERTIME/HEALTH). Texts come from tr() for ko/en.
  • ai_analysis.py — Optional Claude API integration. get_insights(records, api_key): with key → Claude, without → static_summary() fallback.
  • i18n.py_DICT (28 categories × 2 languages) + _HELP_HTML (6 large HTML blocks for HelpView). API: tr('key', **kwargs), tr_html('key'), set_language('ko'|'en').
  • settings_keys.py — All setting keys as constants. Modules import these instead of raw strings.

UI (ui/)

  • main_window.pyupdate_display() ticks 1Hz. State: clock_in_time, is_clocked_in, lunch_break_enabled, dinner_break_enabled, is_on_break, auto_lunch_applied_today. Hot-path caches: _auto_lunch_enabled_cache, _today_non_working_cache. Single-instance via QLocalServer named "ClockOutCalculatorInstance". 7 keyboard shortcuts (Ctrl+O/L/D/B/, F1, Ctrl+R).
  • settings_view.py — Work pattern presets, hour+minute split spinboxes, language combo, DB path override, Claude API key, HTTP API toggle, auto-break toggle. save_settings() sends only WORK_MINUTES — DB auto-syncs WORK_HOURS.
  • stats_view.py — 3 tabs (weekly/monthly/patterns) with matplotlib charts (make_chart_widget, draw_daily_hours, draw_weekday_avg) and AI insight button.
  • mini_widget.py — Always-on-top frameless widget; updated from update_display() when visible.
  • help_view.py — 6 tabs sourced from _HELP_HTML dict (ko/en). _TABS class constant defines (html_key, label_key) pairs.
  • Other dialogs: calendar_view, break_view, overtime_view, leave_view, clock_in_dialog. Window titles use tr(); deeper labels still mostly Korean (point of incremental i18n extension).
  • chart_widget.py — matplotlib QtAgg helpers. Returns _Fallback widget if matplotlib missing.

Utils (utils/)

  • backup.pybackup_db_if_needed(). Once per day, ~/.clockout_backups/database-YYYY-MM-DD.db, 7-file rotation. Uses sqlite3.Connection.backup API for lock-safe copy.
  • lock_detector.py — Windows screen-lock detection via OpenInputDesktop + GetUserObjectInformation (active desktop name != "default" → locked).
  • http_api.py — stdlib http.server on 127.0.0.1, daemon thread. Endpoints: /status, /today, /balance, /weekly. Started from MainWindow.__init__ if HTTP_API_ENABLED=true.
  • debug_log.pydlog(...) env-gated by CLOCKOUT_DEBUG. No-op in production.
  • time_format.pyformat_hours_minutes(minutes) shared helper.
  • system_tray.py — Tray icon menu (lunch/break/clock-out/stats/calendar/help/mini-widget/quit), tooltips i18n.

Time-off accounting in update_display()

Critical invariant — preserve in any change:

break_minutes = self.db.get_total_break_minutes_today()
overtime_used_today = self.db.get_today_overtime_usage()
leave_used_today = self.db.get_today_leave_minutes()
total_time_off = overtime_used_today + leave_used_today

remaining = self.time_calc.calculate_remaining_time(..., break_minutes=break_minutes)
remaining -= timedelta(minutes=total_time_off)   # subtract AFTER, never via break_minutes mutation

Pass actual break_minutes to calculate_remaining_time. Overtime/leave usage subtracted as a timedelta on the result. Progress bar uses overtime_used_minutes=total_time_off keyword arg of calculate_work_progress.

Workday rollover

workday_boundary_hour setting (default 6). start_new_workday() triggers when is_clocked_in=False and clock_in_time.date() != now.date() and now.hour >= boundary. Overnight work past midnight stays attributed to previous workday until that hour. auto_clock_out_previous_days() retroactively closes records using shutdown events (6006).

Database invariants

  • work_records.date UNIQUE (one row/day).
  • lunch_break, dinner_break are BOOLEAN flags; durations live in lunch_duration_minutes/dinner_duration_minutes settings.
  • overtime_bank.work_record_id and overtime_usage.work_record_id are NULLable (manual additions / direct usage). DO NOT filter WHERE work_record_id IS NOT NULL — those rows render with "수동 추가" / "Manual" label.
  • leave_records.days is FLOAT (1.0 / 0.5 / 0.25).
  • Balance: SUM(overtime_bank.earned_minutes) - SUM(overtime_usage.used_minutes) via get_total_overtime_balance().
  • Settings dict from get_settings() already auto-converts numeric strings to int/float — additional int(x) casts in callers are dead code.

Settings system

Stored as string key-value pairs in settings table. Always import keys from core/settings_keys.py — typos become ImportError. Defaults set in init_default_settings(). Auto-sync in save_settings():

  • WORK_MINUTES ↔ WORK_HOURS (floor: 450 min → 7 h, not 8 — settings_view sends WORK_MINUTES only)
  • ANNUAL_LEAVE_DAYS ↔ ANNUAL_LEAVE_TOTAL

Migration sentinels (balance_adjustment_migrated_v2, annual_leave_keys_migrated) prevent re-running migrations every startup.

i18n

tr('key', **kwargs) reads _DICT[current_lang], falls back to ko, then to literal key. tr_html('help.html.X') reads _HELP_HTML dict. Window titles, menus, buttons, group boxes, tray menu, mini widget, all 6 notification messages, and HelpView tabs are translated. Many deeper dialog labels remain Korean — extending is just adding keys + replacing the literal.

Language is read from LANGUAGE setting at MainWindow.__init__. Changing language requires restart for full propagation (existing widget instances keep their original-language text).

Conventions and gotchas

  • Database.get_setting() always returns a string (or default). Use get_setting_int/float/bool() helpers or import a key constant. Already-loaded settings = db.get_settings() dict returns proper types.
  • Time format: Internal calc uses 24h datetime. UI conversion only in format_time() with Korean "오전"/"오후" markers when time_format=12.
  • QSS hover colors: Hex with alpha suffix (#colorDD) renders translucent and can hide text. Use solid colors for hover.
  • Hot-path 1 Hz: update_display() runs every second. Don't add un-cached DB calls — see _auto_lunch_enabled_cache, _today_non_working_cache, cached_time_format patterns. Health/weekly checks are gated by now.minute % 5 == 0 to throttle to 5 min.
  • Bash with spaces: Repo path contains a space. PowerShell more reliable for stderr capture.
  • Single-instance during dev: QLocalServer blocks a second python main.py. Use import-level test or set QT_QPA_PLATFORM=offscreen for GUI smoke tests.
  • PyInstaller frozen? getattr(sys, 'frozen', False) and sys._MEIPASS for resource path resolution (icon).

Tests

All three should be green before any release.