v2.13.0: 익일 퇴근 처리 수정 + 날짜 지정 일일보고 + 연차 직접입력

Fixed:
- clock_out()이 datetime.now().date() 행에 퇴근을 써서 자정 넘긴 퇴근이
  UPDATE 0건으로 유실되던 버그. 출근일(근무일) 기준으로 수정.
  cancel_clock_out() 및 주말/공휴일 판정·외출 집계도 동일 기준으로 통일.
- 캘린더 시간 수정 / 과거기록 추가가 퇴근<=출근을 반려해 익일 퇴근을
  저장할 수 없던 문제. 익일로 해석하고 동시각만 반려.
  총 근무시간·연장근무 음수 계산도 수정.

Added:
- generate_daily_report(target_date) 날짜 파라미터화 + 캘린더 '보고서 복사'
  버튼. 기본값은 진행 중인 근무일(경계시간 전이면 출근일).
- 연차 직접입력 버튼(메인 창) — 미연결 상태였던 use_custom_leave()를
  기존 AddLeaveDialog 재사용으로 연결.
- resolve_work_datetimes() / is_overnight() 공용 헬퍼 + 유닛 테스트 6개.

Changed:
- 연차 빠른 사용에서 날짜·메모 팝업 제거 (연장근무와 동일하게 확인 1회).
- get_today_overtime_usage() / get_total_break_minutes_today()에 선택적
  date_str 파라미터 추가.

테스트: 유닛 200 / 통합 53 / GUI 8 / i18n 5 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
KINDNICK
2026-08-12 14:06:18 +09:00
co-authored by Claude Opus 5
parent d8c6a9d784
commit f28f0619a4
10 changed files with 324 additions and 136 deletions
+10 -6
View File
@@ -1026,10 +1026,10 @@ class Database:
balance = cursor.fetchone()[0]
return initial_overtime + balance
def get_today_overtime_usage(self) -> int:
"""오늘 사용한 추가근무 시간 조회 (분)"""
def get_today_overtime_usage(self, date_str: str = None) -> int:
"""오늘 사용한 추가근무 시간 조회 (분). date_str 지정 시 그 날짜 기준."""
from datetime import date
today = date.today().isoformat()
today = date_str or date.today().isoformat()
with self._conn() as conn:
cursor = conn.cursor()
cursor.execute('SELECT SUM(used_minutes) FROM overtime_usage WHERE date = ?',
@@ -1590,10 +1590,14 @@ class Database:
cursor.execute('DELETE FROM break_records WHERE id = ?', (break_id,))
conn.commit()
def get_total_break_minutes_today(self) -> int:
"""오늘의 총 외출 시간 (분), 진행 중인 외출 포함"""
def get_total_break_minutes_today(self, date_str: str = None) -> int:
"""오늘의 총 외출 시간 (분), 진행 중인 외출 포함.
date_str 지정 시 그 근무일 기준 (야근으로 자정을 넘긴 뒤 퇴근할 때
호출자가 출근일을 넘겨줘야 그 날의 외출이 집계된다).
"""
from datetime import date, datetime
today = date.today().isoformat()
today = date_str or date.today().isoformat()
with self._conn() as conn:
cursor = conn.cursor()
cursor.execute('''
+12 -4
View File
@@ -466,6 +466,10 @@ _DICT = {
'report.memo': '📝 메모: {memo}',
'report.copied.title': '보고서 복사 완료',
'report.copied.body': '일일 근무 보고서가 클립보드에 복사되었습니다.\n\n{report}',
# 익일(자정 넘김) 퇴근 표기
'label.next_day_time': '익일 {time}',
'edit.overnight_note': '🌙 익일 퇴근으로 저장됩니다 (출근~퇴근 {hours}시간 {minutes}분)',
'cal.copy_report': '보고서 복사',
'label.lunch': '🍱 점심시간',
'label.lunch_short': '점심',
'label.dinner': '🍽️ 저녁시간',
@@ -732,7 +736,7 @@ _DICT = {
'cal.save_memo': '메모 저장',
'cal.save_memo_body': '{date}의 메모가 저장되었습니다.',
'cal.save_memo_title': '메모 저장',
'cal.time_error_body': '퇴근 시간은 출근 시간보다 늦어야 합니다.',
'cal.time_error_body': '퇴근 시간이 출근 시간과 같습니다.\n(퇴근이 출근보다 이르면 익일 퇴근으로 저장됩니다.)',
'cal.time_error_title': '시간 오류',
'clock_in_dialog.cancelled': '취소됨',
'clock_in_dialog.selected': '선택된 시간: {time}',
@@ -814,7 +818,7 @@ _DICT = {
'past_record.check_lunch': '점심시간 포함',
'past_record.dialog_title': '기록 추가 — {date}',
'past_record.info': '{date} 근무 기록을 입력하세요.',
'past_record.input_error_body': '퇴근 시간이 출근 시간보다 빠르거나 같습니다.',
'past_record.input_error_body': '퇴근 시간이 출근 시간과 같습니다.\n(퇴근이 출근보다 이르면 익일 퇴근으로 저장됩니다.)',
'past_record.input_error_title': '입력 오류',
'past_record.label_clock_in': '출근:',
'past_record.label_clock_out': '퇴근:',
@@ -1625,6 +1629,10 @@ _DICT = {
'report.memo': '📝 Memo: {memo}',
'report.copied.title': 'Report Copied',
'report.copied.body': 'Daily work report copied to clipboard.\n\n{report}',
# Next-day (past-midnight) clock-out
'label.next_day_time': 'next day {time}',
'edit.overnight_note': '🌙 Saved as a next-day clock-out ({hours}h {minutes}m in to out)',
'cal.copy_report': 'Copy Report',
'label.lunch': '🍱 Lunch',
'label.lunch_short': 'Lunch',
'label.dinner': '🍽️ Dinner',
@@ -1891,7 +1899,7 @@ _DICT = {
'cal.save_memo': 'Save Memo',
'cal.save_memo_body': 'Memo saved for {date}.',
'cal.save_memo_title': 'Save Memo',
'cal.time_error_body': 'Clock-out must be later than clock-in.',
'cal.time_error_body': 'Clock-out is the same as clock-in.\n(An earlier clock-out is saved as the next day.)',
'cal.time_error_title': 'Time Error',
'clock_in_dialog.cancelled': 'Cancelled',
'clock_in_dialog.selected': 'Selected time: {time}',
@@ -1973,7 +1981,7 @@ _DICT = {
'past_record.check_lunch': 'Include lunch',
'past_record.dialog_title': 'Add Record — {date}',
'past_record.info': 'Enter work record for {date}.',
'past_record.input_error_body': 'Clock-out must be later than clock-in.',
'past_record.input_error_body': 'Clock-out is the same as clock-in.\n(An earlier clock-out is saved as the next day.)',
'past_record.input_error_title': 'Input Error',
'past_record.label_clock_in': 'Clock-in:',
'past_record.label_clock_out': 'Clock-out:',
+26
View File
@@ -6,6 +6,32 @@ from datetime import datetime, time, timedelta
from typing import Tuple, Optional
def resolve_work_datetimes(date_str: str, clock_in: str, clock_out: str) -> Tuple[datetime, datetime]:
"""출근일 기준 (출근, 퇴근) datetime. 퇴근이 출근보다 이르면 익일로 해석.
work_records는 출근일 날짜 한 행에 clock_in/clock_out TIME만 저장하므로
"09:00 출근 → 01:30 퇴근"(야근)은 clock_out < clock_in 으로 나타난다.
break_records의 자정 처리(복귀 < 외출 → 익일)와 같은 규칙.
Args:
date_str: 출근일 'YYYY-MM-DD'
clock_in / clock_out: 'HH:MM:SS' (또는 'HH:MM')
"""
day = datetime.strptime(date_str, '%Y-%m-%d').date()
fmt = '%H:%M:%S' if clock_in.count(':') == 2 else '%H:%M'
ci = datetime.combine(day, datetime.strptime(clock_in, fmt).time())
fmt = '%H:%M:%S' if clock_out.count(':') == 2 else '%H:%M'
co = datetime.combine(day, datetime.strptime(clock_out, fmt).time())
if co < ci:
co += timedelta(days=1)
return ci, co
def is_overnight(clock_in: str, clock_out: str) -> bool:
"""퇴근 시각이 익일인지 (문자열 비교 — 'HH:MM:SS' 는 사전순=시간순)."""
return bool(clock_out) and clock_out < clock_in
class TimeCalculator:
"""근무시간 계산 클래스"""
+1 -1
View File
@@ -4,4 +4,4 @@
릴리스 시 이 값을 올린 후 git tag → push.
CHANGELOG.md의 최상단 항목과 일치시킬 것.
"""
__version__ = '2.12.0'
__version__ = '2.13.0'