- i18n 사전 100% (break/overtime/leave/clockin) — 50+ 신규 키 - 런타임 재번역 인프라 (ui/i18n_runtime.py) — 재시작 없이 메인 UI 적용 - MealController 분리 — 점심/저녁 토글을 컨트롤러로 추출 - 통합 테스트 +15 (S36-S52: 온보딩/salary/CSV/notification dedupe 등) - pytest 신규 4종 + i18n_runtime 테스트 (총 122 케이스, 90→122) - README/INSTALL/CLAUDE/AGENTS v2.6+ 아키텍처 반영
11 KiB
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 (퇴근시간 계산기) — Windows desktop app: auto-detects clock-in via Windows Event Log or screen-unlock, calculates clock-out time, banks overtime in 30-min units, tracks leave/breaks, with Discord push, onboarding wizard, and self-updating via Gitea Releases.
Tech Stack: Python 3.9+, PyQt5, SQLite, pywin32, matplotlib, optional holidays.
Companion docs: AGENTS.md, INSTALL.md, README.md, CHANGELOG.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 (78MB, embeds updater.exe)
python -m PyInstaller --clean updater.spec # build first — main.spec datas references it
python -m PyInstaller --clean main.spec
# Tests
python _integration_test.py # business-logic scenarios
python _i18n_gui_test.py # ko/en GUI verification
python _gui_smoke_test.py # widget instantiation
python -m pytest tests # unit tests
# Release (one-shot to Gitea)
$env:GITEA_TOKEN = '<PAT>'
.\release.ps1 v2.7.0
Architecture
core/
- database.py — SQLite. 8+ tables:
work_records,overtime_bank,overtime_usage,leave_records,break_records(+break_type),settings,achievements,holidays,notification_log,crash_log. Runtime migrations chained frominit_database(). Helpers:get_setting_int/float/bool(),get_work_minutes(),get_consecutive_overtime_days(),add_korean_holidays_auto(),add_meal_record(),log_notification(),has_notification_today(). WAL mode + 5s busy timeout for cloud-sync friendliness. - time_calculator.py — Internal
work_minutes: int.calculate_overtime(unit_minutes=30)truncates to user-selectable unit (15/30/60).work_hoursis read-only property. - event_monitor.py — Windows Event IDs 6005/4624/6006.
- notifier.py — 7 notifications, each gated by
NOTIF_*setting + db.has_notification_today guard for daily dedupe. Readsnotification_before_minutesfor clock-out alert threshold. - salary.py —
estimate_pay(records, hourly_wage, overtime_rate=1.5)simple month estimator. - i18n.py —
_DICT(ko/en, 30+ categories) +_HELP_HTML(6 tabs). API:tr(key, **kwargs),tr_html(key),set_language(). Runtime retranslate via observer pattern (see B2 in CHANGELOG v2.7.0). - settings_keys.py — All setting keys as constants. Modules import these instead of raw strings. ~35 keys.
- version.py —
__version__single source of truth.
ui/
- main_window.py —
update_display()ticks 1Hz with hot-path caching. Thin delegating shell — heavy work split into controllers below. Single-instance viaQLocalServer "ClockOutCalculatorInstance". Inline edit on clock-in/out labels (click). Auto-extracts updater.exe from PyInstaller_MEIPASSon first run. - onboarding_view.py — 5-step wizard (welcome / work pattern / clock-in detection / leave+salary / discord). Forced on first launch (
ONBOARDING_COMPLETED=false). Re-runnable from Help dialog. - settings_view.py — Work pattern presets, hours+minutes spinboxes, language combo, font scale, high-contrast, DB path override, Discord webhook URL, Gitea feedback token, monthly goals, CSV import.
- stats_view.py — 3 tabs (weekly/monthly/patterns). Salary card on monthly. Goal progress widget. matplotlib charts via
chart_widget.py. - today_summary.py — Post-clockout card (hours/breaks/overtime/salary). Auto-hidden on next clock-in.
- goal_widget.py — Monthly overtime cap + daily avg progress bars. Hidden when both goals=0.
- meal_time_dialog.py — Lunch/dinner real start-end input.
- past_record_dialog.py — Manual past-day entry (calendar right-click).
- leave_calendar_view.py — Color-coded leave usage calendar.
- mini_widget.py — Always-on-top frameless time display.
- help_view.py — 6 tabs from
_HELP_HTML. Bottom-left "Re-run Onboarding" button. - chart_widget.py — matplotlib QtAgg helpers:
draw_daily_hours(with hover annotation),draw_weekday_avg,draw_clock_in_distribution. - accessibility.py — Font scale + high-contrast QSS overlay.
- Dialogs:
calendar_view,break_view,overtime_view,leave_view,clock_in_dialog. Window titles usetr(); deeper labels Korean (incremental i18n).
ui/controllers/
- lock_monitor.py — Windows screen-lock 5s polling. Two modes: AUTO_BREAK_ON_LOCK (lock→break_out, unlock→break_in) and CLOCK_IN_ON_UNLOCK (first unlock = clock-in for users who never reboot).
- auto_lunch.py — 4-hour-since-clock-in auto-toggle lunch. Setting cache + non-working-day cache.
- notification_orchestrator.py — 1Hz tick orchestrates 7 notifications. 5-min throttle for health/weekly/threshold. Monday weekly report + Discord push.
utils/
- backup.py —
backup_db_if_needed(). Daily, 7-file rotation,sqlite3.Connection.backupAPI. - lock_detector.py —
is_screen_locked()via Win32OpenInputDesktop+GetUserObjectInformation. - discord_webhook.py —
send_test/clock_in/clock_out/health_warning. Browser User-Agent (Cloudflare bypass). - updater_client.py — Gitea Releases API.
check_for_update()returns(info, reason)tuple — reasons:UP_TO_DATE/NETWORK_ERROR/NO_RELEASE/NO_ASSET.apply_update()invokes updater.exe. - csv_importer.py —
parse_csv()+import_records(on_conflict='skip'|'overwrite'). Standard format:date,clock_in,clock_out,lunch_minutes,memo. - crash_handler.py —
install_global_handler(db, version)registerssys.excepthook. Logs to crash_log + shows dialog with copy/Gitea-report buttons. - debug_log.py —
dlog()env-gated byCLOCKOUT_DEBUG. - time_format.py —
format_hours_minutes(minutes)shared helper. - system_tray.py — Tray menu, tooltips i18n.
Top-level
- main.py — Entry point. Bootstraps DB, reads
db_path_override, runs auto-backup, registers crash handler, shows onboarding (if needed), instantiates MainWindow. - updater.py — Standalone helper.
--pid <main_pid> --new <new_exe> --target <target_exe>. Waits for main exit, replaces, relaunches. Backup.bakfor rollback. - updater.spec — PyInstaller spec (~6MB, no PyQt deps).
- main.spec — Embeds
build/staging/updater.exeas data (release.ps1 stages it). - release.ps1 — One-shot release: bump version → tests → build both exe → tag push → Gitea Release + asset upload. Optional Authenticode signing via
$env:CODE_SIGN_CERT.
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
Database invariants
work_records.dateUNIQUE.lunch_break,dinner_breakare BOOLEAN flags; durations from settings; ACTUAL meal times viabreak_records.break_type='lunch'/'dinner'.overtime_bank.work_record_idandovertime_usage.work_record_idare NULLable. Don't filterNOT NULL— those are manual additions.leave_records.daysis FLOAT (1.0/0.5/0.25).- Balance:
SUM(bank.earned) - SUM(usage.used). notification_logfor daily dedupe (channel+event_type+date).crash_logfor unhandled exceptions.
Settings system
Stored as string key-value in settings table. Always import keys from settings_keys.py. Auto-sync in save_settings():
WORK_MINUTES ↔ WORK_HOURS(floor)ANNUAL_LEAVE_DAYS ↔ ANNUAL_LEAVE_TOTAL
Migration sentinels prevent re-running.
i18n
tr('key', **kwargs) reads _DICT[current_lang], falls back to ko, then literal key. tr_html('help.html.X') for HelpView. Many deeper dialog labels still Korean — _DICT['ko']/['en']에 키 추가 + tr() 교체로 점진 확장.
Runtime retranslate (v2.7.0+): observer pattern. Widgets register their text via register_translatable(widget, key) from ui/i18n_runtime.py; on set_language() change, all registered widgets are re-fetched.
Conventions
- Database.get_setting() returns string. Use
get_setting_int/float/bool()orget_settings()dict. - 24h
datetimeinternal. 12h conversion only informat_time(). - 1Hz hot path: cache DB calls (
_auto_lunch_enabled_cache,_today_non_working_cache,cached_time_format). Health/weekly throttled to 5-min. - Single-instance dev:
QLocalServerblocks secondpython main.py. UseQT_QPA_PLATFORM=offscreenfor GUI smoke tests. - PyInstaller frozen:
getattr(sys, 'frozen', False)+sys._MEIPASSfor resource paths. - main.exe self-extracts updater.exe to its own folder on first launch (
_ensure_updater_extracted()in main.py).
Tests
- _integration_test.py — Business-logic scenarios (no Qt).
- _gui_smoke_test.py — Widget instantiation via
QT_QPA_PLATFORM=offscreen. - _i18n_gui_test.py — ko/en switching on real widgets.
- tests/ — pytest unit tests:
test_time_calculator,test_database,test_i18n,test_updater,test_csv_importer,test_discord_webhook,test_salary,test_crash_handler(v2.7.0+).
All should be green before any release.
Release flow
# Edit core/version.py + CHANGELOG.md
git add -A && git commit -m "v2.X.Y: ..."
.\release.ps1 v2.X.Y
Auto-handles: version bump check, pytest+integration tests, two-exe build, ZIP, git tag push, Gitea Release create, asset upload (main.exe + updater.exe + ZIP).