fix(holiday): live hot-path now banks overtime from minute 1 on holidays
3 bugs found and fixed: A. update_display() 1Hz 루프에 is_non_working_day 분기 누락 - 휴일/주말 출근 시 '남은 시간 8h'부터 카운트다운 → 실제 휴일 추가근무 시작 안 됨 - 수정: is_non_working_day=True면 calculate_holiday_overtime로 즉시 음수 remaining 표시 - 그룹 타이틀: '주말 근무 (전체 적립)' / '공휴일 근무 (전체 적립)' - 진행바: 휴일은 100% 고정 (의미 없음) - 예상 퇴근: '휴일 근무 (정해진 퇴근시각 없음)' B. check_clock_out_soon 알림이 휴일에도 fire - 휴일엔 정해진 퇴근시각이 없으니 무의미한 알림 - 수정: orchestrator.tick(is_holiday=True)면 스킵 - 점심/저녁/장시간 휴식 알림은 휴일에도 유지 (식사·건강은 휴일에도 챙김) C. 자동복구 퇴근 3곳이 (work_minutes // 30) * 30 하드코딩 - main_window.py:1385, 1512, 1581 — 사용자 overtime_unit (15/30/60) 무시 - 수정: 모두 settings에서 unit_minutes 읽어 calculate_holiday_overtime/calculate_overtime에 전달 리팩터링: 4곳에 중복되던 휴일 연장 계산 로직을 TimeCalculator.calculate_holiday_overtime로 추출. Tests: - tests/test_time_calculator.py: 9개 신규 (TestHolidayOvertime) - _integration_test.py: S52A 휴일 hot-path 회귀 시나리오
This commit is contained in:
parent
c5df37ca57
commit
d41e5cb921
@ -696,6 +696,30 @@ def s51_accessibility_keys():
|
|||||||
assert db.get_setting_bool('high_contrast') is True
|
assert db.get_setting_bool('high_contrast') is True
|
||||||
|
|
||||||
|
|
||||||
|
@case("S52A. 휴일 hot-path: is_non_working_day → 출근 직후부터 즉시 연장 적립")
|
||||||
|
def s52a_holiday_hotpath():
|
||||||
|
"""update_display 분기 회귀 — 휴일에 출근 1분 = 적립 0, 30분 = 적립 30."""
|
||||||
|
from core.time_calculator import TimeCalculator
|
||||||
|
db = fresh_db('s52a')
|
||||||
|
holiday_date = '2026-05-01' # 근로자의 날
|
||||||
|
db.add_holiday(holiday_date, '근로자의 날', is_recurring=True)
|
||||||
|
|
||||||
|
calc = TimeCalculator(work_minutes=480, lunch_duration_minutes=60)
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
# 휴일 인식
|
||||||
|
assert calc.is_non_working_day(ci, db)
|
||||||
|
assert calc.get_day_type(ci, db) == 'holiday'
|
||||||
|
|
||||||
|
# 출근 1분 후: 적립 0 (30분 단위 절삭)
|
||||||
|
now1 = ci + timedelta(minutes=1)
|
||||||
|
actual, earned = calc.calculate_holiday_overtime(ci, now1)
|
||||||
|
assert actual == 1 and earned == 0
|
||||||
|
# 출근 30분 후: 30분 적립 (평일이라면 0, 휴일은 즉시 시작)
|
||||||
|
now30 = ci + timedelta(minutes=30)
|
||||||
|
actual, earned = calc.calculate_holiday_overtime(ci, now30)
|
||||||
|
assert actual == 30 and earned == 30
|
||||||
|
|
||||||
|
|
||||||
@case("S52. CSV import + overtime 적립까지 정상 동작")
|
@case("S52. CSV import + overtime 적립까지 정상 동작")
|
||||||
def s52_csv_overtime():
|
def s52_csv_overtime():
|
||||||
from utils.csv_importer import parse_csv, import_records
|
from utils.csv_importer import parse_csv, import_records
|
||||||
@ -764,6 +788,7 @@ def main():
|
|||||||
s49_discord_empty()
|
s49_discord_empty()
|
||||||
s50_goals()
|
s50_goals()
|
||||||
s51_accessibility_keys()
|
s51_accessibility_keys()
|
||||||
|
s52a_holiday_hotpath()
|
||||||
s52_csv_overtime()
|
s52_csv_overtime()
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|||||||
@ -236,6 +236,36 @@ class TimeCalculator:
|
|||||||
normal_clock_out = self.calculate_clock_out_time(clock_in, include_lunch, include_dinner)
|
normal_clock_out = self.calculate_clock_out_time(clock_in, include_lunch, include_dinner)
|
||||||
return normal_clock_out + timedelta(minutes=target_overtime_minutes)
|
return normal_clock_out + timedelta(minutes=target_overtime_minutes)
|
||||||
|
|
||||||
|
def calculate_holiday_overtime(self, clock_in: datetime, current_time: datetime,
|
||||||
|
include_lunch: bool = False,
|
||||||
|
include_dinner: bool = False,
|
||||||
|
break_minutes: int = 0,
|
||||||
|
unit_minutes: int = 30) -> Tuple[int, int]:
|
||||||
|
"""
|
||||||
|
휴일/주말 근무: 모든 시간을 연장근무로 계산.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
clock_in: 출근 시간
|
||||||
|
current_time: 현재(또는 퇴근) 시간
|
||||||
|
include_lunch/dinner: 식사 시간 차감 여부
|
||||||
|
break_minutes: 외출 시간 (분) — 연장근무에서 제외
|
||||||
|
unit_minutes: 적립 단위 (15/30/60)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(실제 연장 분, 적립 분) — 둘 다 0 이상.
|
||||||
|
"""
|
||||||
|
elapsed_minutes = int((current_time - clock_in).total_seconds() / 60)
|
||||||
|
if include_lunch:
|
||||||
|
elapsed_minutes -= self.lunch_duration_minutes
|
||||||
|
if include_dinner:
|
||||||
|
elapsed_minutes -= self.dinner_duration_minutes
|
||||||
|
elapsed_minutes -= break_minutes
|
||||||
|
elapsed_minutes = max(0, elapsed_minutes)
|
||||||
|
|
||||||
|
unit = unit_minutes if unit_minutes > 0 else 30
|
||||||
|
earned = (elapsed_minutes // unit) * unit
|
||||||
|
return elapsed_minutes, earned
|
||||||
|
|
||||||
def is_weekend(self, date_obj: datetime) -> bool:
|
def is_weekend(self, date_obj: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
주말 여부 확인
|
주말 여부 확인
|
||||||
|
|||||||
@ -98,3 +98,77 @@ class TestDayType:
|
|||||||
mon = datetime(2026, 5, 4)
|
mon = datetime(2026, 5, 4)
|
||||||
assert not calc.is_weekend(mon)
|
assert not calc.is_weekend(mon)
|
||||||
assert calc.get_day_type(mon) == 'normal'
|
assert calc.get_day_type(mon) == 'normal'
|
||||||
|
|
||||||
|
|
||||||
|
class TestHolidayOvertime:
|
||||||
|
"""휴일/주말 근무 적립 — 출근 직후부터 모든 시간이 연장으로."""
|
||||||
|
|
||||||
|
def test_zero_elapsed_returns_zero(self, calc_8h):
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(ci, ci)
|
||||||
|
assert actual == 0 and earned == 0
|
||||||
|
|
||||||
|
def test_one_minute_elapsed_no_lunch(self, calc_8h):
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(minutes=1)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(ci, now)
|
||||||
|
assert actual == 1
|
||||||
|
assert earned == 0 # 30분 단위 절삭
|
||||||
|
|
||||||
|
def test_30min_elapsed_truncates_to_30(self, calc_8h):
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(minutes=30)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(ci, now)
|
||||||
|
assert actual == 30 and earned == 30
|
||||||
|
|
||||||
|
def test_29min_elapsed_truncates_to_zero(self, calc_8h):
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(minutes=29)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(ci, now)
|
||||||
|
assert actual == 29 and earned == 0
|
||||||
|
|
||||||
|
def test_lunch_subtracted(self, calc_8h):
|
||||||
|
# 8h 근무 + 점심 60m → 9h 일했지만 점심 차감 = 8h 적립
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(hours=9)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(
|
||||||
|
ci, now, include_lunch=True
|
||||||
|
)
|
||||||
|
assert actual == 8 * 60
|
||||||
|
assert earned == 8 * 60
|
||||||
|
|
||||||
|
def test_break_minutes_subtracted(self, calc_8h):
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(hours=2)
|
||||||
|
# 외출 30분 → 90분 적립
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(
|
||||||
|
ci, now, break_minutes=30
|
||||||
|
)
|
||||||
|
assert actual == 90 and earned == 90
|
||||||
|
|
||||||
|
def test_unit_minutes_15(self, calc_8h):
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(minutes=44)
|
||||||
|
# 44분 → 30분 적립 (15분 단위)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(
|
||||||
|
ci, now, unit_minutes=15
|
||||||
|
)
|
||||||
|
assert actual == 44 and earned == 30
|
||||||
|
|
||||||
|
def test_unit_minutes_60(self, calc_8h):
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(minutes=119)
|
||||||
|
# 119분 → 60분 적립 (60분 단위)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(
|
||||||
|
ci, now, unit_minutes=60
|
||||||
|
)
|
||||||
|
assert actual == 119 and earned == 60
|
||||||
|
|
||||||
|
def test_negative_clamped_to_zero(self, calc_8h):
|
||||||
|
# 점심 60m + 저녁 60m = 120m 차감되는데 1시간만 일하면 음수
|
||||||
|
ci = datetime(2026, 5, 1, 9, 0)
|
||||||
|
now = ci + timedelta(hours=1)
|
||||||
|
actual, earned = calc_8h.calculate_holiday_overtime(
|
||||||
|
ci, now, include_lunch=True, include_dinner=True
|
||||||
|
)
|
||||||
|
assert actual == 0 and earned == 0
|
||||||
|
|||||||
@ -82,10 +82,13 @@ class NotificationOrchestrator:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
dlog(f"discord weekly_report failed: {e}")
|
dlog(f"discord weekly_report failed: {e}")
|
||||||
|
|
||||||
def tick(self, now: datetime, expected_clock_out: datetime, remaining_seconds: float) -> None:
|
def tick(self, now: datetime, expected_clock_out: datetime, remaining_seconds: float,
|
||||||
|
is_holiday: bool = False) -> None:
|
||||||
n = self.notifier
|
n = self.notifier
|
||||||
# 1초마다 체크: 30분 전, 점심/저녁 미등록, 연장 적립
|
# "퇴근 30분 전" 알림은 휴일/주말엔 무의미 (정해진 퇴근시각 없음)
|
||||||
n.check_clock_out_soon(expected_clock_out, now)
|
if not is_holiday:
|
||||||
|
n.check_clock_out_soon(expected_clock_out, now)
|
||||||
|
# 점심/저녁/장시간 휴식 알림은 휴일에도 그대로 — 식사·건강은 휴일에도 챙김
|
||||||
n.check_lunch_reminder(self.window.clock_in_time,
|
n.check_lunch_reminder(self.window.clock_in_time,
|
||||||
self.window.lunch_break_enabled, now)
|
self.window.lunch_break_enabled, now)
|
||||||
n.check_dinner_reminder(self.window.clock_in_time,
|
n.check_dinner_reminder(self.window.clock_in_time,
|
||||||
|
|||||||
@ -728,21 +728,42 @@ class MainWindow(QMainWindow):
|
|||||||
# 총 차감 시간 (추가근무 + 연차/반차)
|
# 총 차감 시간 (추가근무 + 연차/반차)
|
||||||
total_time_off = overtime_used_today + leave_used_today
|
total_time_off = overtime_used_today + leave_used_today
|
||||||
|
|
||||||
# 남은 시간 계산 (외출 시간 반영, 추가근무/반차 사용 시간 차감)
|
# 휴일/주말이면 출근 직후부터 모든 시간이 연장근무로 흐름.
|
||||||
remaining = self.time_calc.calculate_remaining_time(
|
# 정해진 퇴근 시각이 없으므로 calculate_remaining_time 분기를 건너뛴다.
|
||||||
self.clock_in_time,
|
is_holiday = self.time_calc.is_non_working_day(now, self.db)
|
||||||
include_lunch=self.lunch_break_enabled,
|
|
||||||
include_dinner=self.dinner_break_enabled,
|
if is_holiday:
|
||||||
current_time=now,
|
actual_ot, _ = self.time_calc.calculate_holiday_overtime(
|
||||||
break_minutes=break_minutes
|
self.clock_in_time, now,
|
||||||
)
|
include_lunch=self.lunch_break_enabled,
|
||||||
# 사용한 추가근무 + 반차만큼 남은 시간 감소 (일찍 퇴근 가능)
|
include_dinner=self.dinner_break_enabled,
|
||||||
remaining -= timedelta(minutes=total_time_off)
|
break_minutes=break_minutes,
|
||||||
|
)
|
||||||
|
# 추가근무/반차 사용분만큼 시작점을 늦춤 (즉 그만큼 적게 적립)
|
||||||
|
actual_ot = max(0, actual_ot - total_time_off)
|
||||||
|
remaining = -timedelta(minutes=actual_ot)
|
||||||
|
else:
|
||||||
|
# 평일: 정상 남은 시간 계산
|
||||||
|
remaining = self.time_calc.calculate_remaining_time(
|
||||||
|
self.clock_in_time,
|
||||||
|
include_lunch=self.lunch_break_enabled,
|
||||||
|
include_dinner=self.dinner_break_enabled,
|
||||||
|
current_time=now,
|
||||||
|
break_minutes=break_minutes
|
||||||
|
)
|
||||||
|
# 사용한 추가근무 + 반차만큼 남은 시간 감소 (일찍 퇴근 가능)
|
||||||
|
remaining -= timedelta(minutes=total_time_off)
|
||||||
|
|
||||||
# 남은 시간 표시 및 추가 근무 처리
|
# 남은 시간 표시 및 추가 근무 처리
|
||||||
if remaining.total_seconds() < 0:
|
if remaining.total_seconds() < 0:
|
||||||
# 추가 근무 중
|
# 추가 근무 중 (휴일이면 출근 직후부터 항상 이 분기)
|
||||||
self.remaining_time_group.setTitle("추가 근무 중")
|
day_type = self.time_calc.get_day_type(now, self.db)
|
||||||
|
if day_type == 'weekend':
|
||||||
|
self.remaining_time_group.setTitle("주말 근무 (전체 적립)")
|
||||||
|
elif day_type == 'holiday':
|
||||||
|
self.remaining_time_group.setTitle("공휴일 근무 (전체 적립)")
|
||||||
|
else:
|
||||||
|
self.remaining_time_group.setTitle("추가 근무 중")
|
||||||
# + 기호로 표시
|
# + 기호로 표시
|
||||||
total_seconds = int(abs(remaining.total_seconds()))
|
total_seconds = int(abs(remaining.total_seconds()))
|
||||||
hours = total_seconds // 3600
|
hours = total_seconds // 3600
|
||||||
@ -765,35 +786,46 @@ class MainWindow(QMainWindow):
|
|||||||
self._set_text_if_changed(self.remaining_time_label, remaining_str)
|
self._set_text_if_changed(self.remaining_time_label, remaining_str)
|
||||||
|
|
||||||
# 진행률 업데이트
|
# 진행률 업데이트
|
||||||
# - 외출 시간: 필요 근무시간 증가 (일을 안 한 시간이므로 더 일해야 함)
|
# 휴일은 정해진 근무시간이 없으므로 게이지 의미 없음 → 100%로 채워둠.
|
||||||
# - 추가근무 사용: 필요 근무시간 감소 (미리 일한 것을 사용하므로 덜 일해도 됨)
|
if is_holiday:
|
||||||
progress = self.time_calc.calculate_work_progress(
|
self.progress_bar.setValue(100)
|
||||||
self.clock_in_time,
|
else:
|
||||||
include_lunch=self.lunch_break_enabled,
|
progress = self.time_calc.calculate_work_progress(
|
||||||
include_dinner=self.dinner_break_enabled,
|
self.clock_in_time,
|
||||||
current_time=now,
|
include_lunch=self.lunch_break_enabled,
|
||||||
break_minutes=break_minutes,
|
include_dinner=self.dinner_break_enabled,
|
||||||
overtime_used_minutes=total_time_off
|
current_time=now,
|
||||||
)
|
break_minutes=break_minutes,
|
||||||
self.progress_bar.setValue(int(progress * 100))
|
overtime_used_minutes=total_time_off
|
||||||
|
)
|
||||||
|
self.progress_bar.setValue(int(progress * 100))
|
||||||
|
|
||||||
# 예상 퇴근 시간 (외출 시간 포함)
|
# 예상 퇴근 시간 (외출 시간 포함)
|
||||||
# 추가근무 사용 시간만큼 일찍 퇴근 가능하므로 실제 퇴근 시간에서 차감
|
# 휴일은 정해진 퇴근 시각이 없음 → 출근 시각을 그대로 표시 (= 즉시 적립 시작 의미)
|
||||||
expected_clock_out = self.time_calc.calculate_clock_out_time(
|
if is_holiday:
|
||||||
self.clock_in_time,
|
expected_clock_out = self.clock_in_time
|
||||||
include_lunch=self.lunch_break_enabled,
|
self._set_text_if_changed(
|
||||||
include_dinner=self.dinner_break_enabled,
|
self.expected_time_label,
|
||||||
break_minutes=break_minutes
|
"휴일 근무 (정해진 퇴근시각 없음)"
|
||||||
)
|
)
|
||||||
# 추가근무 + 반차 사용한 만큼 예상 퇴근 시간을 앞당김
|
else:
|
||||||
expected_clock_out -= timedelta(minutes=total_time_off)
|
expected_clock_out = self.time_calc.calculate_clock_out_time(
|
||||||
self._set_text_if_changed(
|
self.clock_in_time,
|
||||||
self.expected_time_label,
|
include_lunch=self.lunch_break_enabled,
|
||||||
f"예상 퇴근: {self.format_time(expected_clock_out)}"
|
include_dinner=self.dinner_break_enabled,
|
||||||
)
|
break_minutes=break_minutes
|
||||||
|
)
|
||||||
|
# 추가근무 + 반차 사용한 만큼 예상 퇴근 시간을 앞당김
|
||||||
|
expected_clock_out -= timedelta(minutes=total_time_off)
|
||||||
|
self._set_text_if_changed(
|
||||||
|
self.expected_time_label,
|
||||||
|
f"예상 퇴근: {self.format_time(expected_clock_out)}"
|
||||||
|
)
|
||||||
|
|
||||||
# 알림은 NotificationOrchestrator로 위임 (5분 throttle 포함)
|
# 알림은 NotificationOrchestrator로 위임 (5분 throttle 포함)
|
||||||
self._notif_orch.tick(now, expected_clock_out, remaining.total_seconds())
|
# 휴일이면 "퇴근 30분 전" 알림은 의미 없으므로 플래그로 게이팅.
|
||||||
|
self._notif_orch.tick(now, expected_clock_out, remaining.total_seconds(),
|
||||||
|
is_holiday=is_holiday)
|
||||||
|
|
||||||
# 트레이 / 미니 위젯 갱신
|
# 트레이 / 미니 위젯 갱신
|
||||||
if remaining.total_seconds() < 0:
|
if remaining.total_seconds() < 0:
|
||||||
@ -1156,16 +1188,14 @@ class MainWindow(QMainWindow):
|
|||||||
unit_minutes = 30
|
unit_minutes = 30
|
||||||
|
|
||||||
if is_non_working_day:
|
if is_non_working_day:
|
||||||
# 주말/공휴일: 모든 시간을 연장근무로 처리 (외출 시간 제외)
|
# 주말/공휴일: 모든 시간을 연장근무로 처리 (식사·외출 시간 제외)
|
||||||
work_minutes = int(total_hours * 60)
|
overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
|
||||||
if self.lunch_break_enabled:
|
self.clock_in_time, now,
|
||||||
work_minutes -= self.time_calc.lunch_duration_minutes
|
include_lunch=self.lunch_break_enabled,
|
||||||
if self.dinner_break_enabled:
|
include_dinner=self.dinner_break_enabled,
|
||||||
work_minutes -= self.time_calc.dinner_duration_minutes
|
break_minutes=break_minutes,
|
||||||
work_minutes -= break_minutes
|
unit_minutes=unit_minutes,
|
||||||
work_minutes = max(0, work_minutes)
|
)
|
||||||
overtime_earned = (work_minutes // unit_minutes) * unit_minutes
|
|
||||||
overtime_actual = work_minutes
|
|
||||||
else:
|
else:
|
||||||
# 평일: 정상 연장근무 계산 (외출 시간 포함)
|
# 평일: 정상 연장근무 계산 (외출 시간 포함)
|
||||||
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
||||||
@ -1342,24 +1372,25 @@ class MainWindow(QMainWindow):
|
|||||||
break_minutes = cursor.fetchone()[0] or 0
|
break_minutes = cursor.fetchone()[0] or 0
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# 추가근무 계산
|
# 추가근무 계산 (사용자 설정 적립 단위 적용)
|
||||||
|
unit_minutes = self.db.get_setting_int('overtime_unit', 30)
|
||||||
|
if unit_minutes not in (15, 30, 60):
|
||||||
|
unit_minutes = 30
|
||||||
if is_non_working_day:
|
if is_non_working_day:
|
||||||
work_minutes = int(total_hours * 60)
|
overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
|
||||||
if self.lunch_break_enabled:
|
self.clock_in_time, workday_end,
|
||||||
work_minutes -= self.time_calc.lunch_duration_minutes
|
include_lunch=self.lunch_break_enabled,
|
||||||
if self.dinner_break_enabled:
|
include_dinner=self.dinner_break_enabled,
|
||||||
work_minutes -= self.time_calc.dinner_duration_minutes
|
break_minutes=break_minutes,
|
||||||
work_minutes -= break_minutes
|
unit_minutes=unit_minutes,
|
||||||
# 음수 방지
|
)
|
||||||
work_minutes = max(0, work_minutes)
|
|
||||||
overtime_earned = (work_minutes // 30) * 30
|
|
||||||
overtime_actual = work_minutes
|
|
||||||
else:
|
else:
|
||||||
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
||||||
self.clock_in_time, workday_end,
|
self.clock_in_time, workday_end,
|
||||||
include_lunch=self.lunch_break_enabled,
|
include_lunch=self.lunch_break_enabled,
|
||||||
include_dinner=self.dinner_break_enabled,
|
include_dinner=self.dinner_break_enabled,
|
||||||
break_minutes=break_minutes
|
break_minutes=break_minutes,
|
||||||
|
unit_minutes=unit_minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
# DB 업데이트 (출근일 날짜에 퇴근 시간 기록)
|
# DB 업데이트 (출근일 날짜에 퇴근 시간 기록)
|
||||||
@ -1498,26 +1529,26 @@ class MainWindow(QMainWindow):
|
|||||||
lunch_enabled = bool(record.get('lunch_break', False))
|
lunch_enabled = bool(record.get('lunch_break', False))
|
||||||
dinner_enabled = bool(record.get('dinner_break', False))
|
dinner_enabled = bool(record.get('dinner_break', False))
|
||||||
|
|
||||||
|
unit_minutes = self.db.get_setting_int('overtime_unit', 30)
|
||||||
|
if unit_minutes not in (15, 30, 60):
|
||||||
|
unit_minutes = 30
|
||||||
if is_non_working_day:
|
if is_non_working_day:
|
||||||
# 주말/공휴일: 모든 시간을 연장근무로 처리 (점심/저녁/외출 제외)
|
# 주말/공휴일: 모든 시간을 연장근무로 처리 (식사·외출 제외)
|
||||||
work_minutes = int(total_hours * 60)
|
overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
|
||||||
if lunch_enabled:
|
clock_in_time, shutdown_time,
|
||||||
work_minutes -= self.time_calc.lunch_duration_minutes
|
include_lunch=lunch_enabled,
|
||||||
if dinner_enabled:
|
include_dinner=dinner_enabled,
|
||||||
work_minutes -= self.time_calc.dinner_duration_minutes
|
break_minutes=break_minutes,
|
||||||
work_minutes -= break_minutes
|
unit_minutes=unit_minutes,
|
||||||
# 음수 방지
|
)
|
||||||
work_minutes = max(0, work_minutes)
|
|
||||||
# 30분 단위로 절삭
|
|
||||||
overtime_earned = (work_minutes // 30) * 30
|
|
||||||
overtime_actual = work_minutes
|
|
||||||
else:
|
else:
|
||||||
# 평일: 정상 연장근무 계산 (외출 시간 포함)
|
# 평일: 정상 연장근무 계산 (외출 시간 포함)
|
||||||
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
||||||
clock_in_time, shutdown_time,
|
clock_in_time, shutdown_time,
|
||||||
include_lunch=lunch_enabled,
|
include_lunch=lunch_enabled,
|
||||||
include_dinner=dinner_enabled,
|
include_dinner=dinner_enabled,
|
||||||
break_minutes=break_minutes
|
break_minutes=break_minutes,
|
||||||
|
unit_minutes=unit_minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
# DB 업데이트 (total_hours는 원본 시간 그대로 저장)
|
# DB 업데이트 (total_hours는 원본 시간 그대로 저장)
|
||||||
@ -1569,23 +1600,24 @@ class MainWindow(QMainWindow):
|
|||||||
lunch_enabled = bool(record.get('lunch_break', False))
|
lunch_enabled = bool(record.get('lunch_break', False))
|
||||||
dinner_enabled = bool(record.get('dinner_break', False))
|
dinner_enabled = bool(record.get('dinner_break', False))
|
||||||
|
|
||||||
|
unit_minutes = self.db.get_setting_int('overtime_unit', 30)
|
||||||
|
if unit_minutes not in (15, 30, 60):
|
||||||
|
unit_minutes = 30
|
||||||
if is_non_working_day:
|
if is_non_working_day:
|
||||||
work_minutes = int(total_hours * 60)
|
overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
|
||||||
if lunch_enabled:
|
clock_in_time, fallback_time,
|
||||||
work_minutes -= self.time_calc.lunch_duration_minutes
|
include_lunch=lunch_enabled,
|
||||||
if dinner_enabled:
|
include_dinner=dinner_enabled,
|
||||||
work_minutes -= self.time_calc.dinner_duration_minutes
|
break_minutes=break_minutes,
|
||||||
work_minutes -= break_minutes
|
unit_minutes=unit_minutes,
|
||||||
# 음수 방지
|
)
|
||||||
work_minutes = max(0, work_minutes)
|
|
||||||
overtime_earned = (work_minutes // 30) * 30
|
|
||||||
overtime_actual = work_minutes
|
|
||||||
else:
|
else:
|
||||||
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
|
||||||
clock_in_time, fallback_time,
|
clock_in_time, fallback_time,
|
||||||
include_lunch=lunch_enabled,
|
include_lunch=lunch_enabled,
|
||||||
include_dinner=dinner_enabled,
|
include_dinner=dinner_enabled,
|
||||||
break_minutes=break_minutes
|
break_minutes=break_minutes,
|
||||||
|
unit_minutes=unit_minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
# DB 업데이트
|
# DB 업데이트
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user