Clock_out_Time_Calculator/ui/meal_time_dialog.py

177 lines
7.6 KiB
Python

"""
점심/저녁 실제 시간 입력 다이얼로그.
기본 60분 자동 차감 모드와 별개로, 사용자가 정확한 시작/종료 시각을
입력하면 그 값을 break_records.break_type='lunch'/'dinner'로 저장.
식사 시각은 출근~퇴근 범위 내에서만 의미가 있으므로,
clock_in_time이 주어지면 시작이 출근 이전이거나 퇴근 이후로 가는 것을 차단.
"""
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Optional
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTimeEdit, QMessageBox)
from PyQt5.QtCore import QTime
from core.i18n import tr
from ui.styles import apply_dark_titlebar
class MealTimeDialog(QDialog):
"""점심/저녁 실제 시작·종료 시간 입력.
Args:
parent: 부모 위젯
meal_type: 'lunch' 또는 'dinner'
default_minutes: 안내 문구에 표시할 기본 차감 분
clock_in_time: 출근 datetime (옵션). 주어지면 식사가 출근 이후인지 검증.
clock_out_time: 퇴근 datetime (옵션, 보통 미퇴근 상태). 주어지면 퇴근 이전인지 검증.
"""
def __init__(self, parent=None, meal_type: str = 'lunch', default_minutes: int = 60,
clock_in_time: Optional[datetime] = None,
clock_out_time: Optional[datetime] = None):
super().__init__(parent)
self.meal_type = meal_type
self._clock_in = clock_in_time
self._clock_out = clock_out_time
meal_label = tr('label.lunch_short') if meal_type == 'lunch' else tr('label.dinner_short')
self.setWindowTitle(tr('meal.dialog_title', meal=meal_label))
self.setModal(True)
self.setFixedSize(380, 260)
layout = QVBoxLayout()
layout.setSpacing(10)
layout.setContentsMargins(20, 16, 20, 16)
info_text = tr('meal.info_text', meal=meal_label, minutes=default_minutes)
if clock_in_time is not None:
info_text += tr('meal.info_clock_in_limit', time=clock_in_time.strftime('%H:%M'))
info = QLabel(info_text)
info.setWordWrap(True)
info.setStyleSheet("color: #909296; padding-bottom: 6px;")
layout.addWidget(info)
# 합리적 기본값: 출근 이후로 보정
default_start_h = 12 if meal_type == 'lunch' else 18
default_end_h = 13 if meal_type == 'lunch' else 19
if clock_in_time is not None and clock_in_time.hour >= default_start_h:
# 출근이 이미 점심/저녁 기본 시각을 지났으면 출근 직후를 기본값으로
default_start_h = (clock_in_time.hour + 1) % 24
default_end_h = (default_start_h + 1) % 24
# 시작
start_row = QHBoxLayout()
start_row.addWidget(QLabel(tr('meal.label_start')))
self.start_edit = QTimeEdit()
self.start_edit.setDisplayFormat("HH:mm")
self.start_edit.setTime(QTime(default_start_h, 0))
start_row.addWidget(self.start_edit)
start_row.addStretch()
layout.addLayout(start_row)
# 종료
end_row = QHBoxLayout()
end_row.addWidget(QLabel(tr('meal.label_end')))
self.end_edit = QTimeEdit()
self.end_edit.setDisplayFormat("HH:mm")
self.end_edit.setTime(QTime(default_end_h, 0))
end_row.addWidget(self.end_edit)
end_row.addStretch()
layout.addLayout(end_row)
# 미리보기 라벨
self.preview = QLabel("")
self.preview.setStyleSheet("color: #51CF66; font-weight: bold; padding-top: 6px;")
layout.addWidget(self.preview)
self._update_preview()
self.start_edit.timeChanged.connect(self._update_preview)
self.end_edit.timeChanged.connect(self._update_preview)
# 버튼
btn_row = QHBoxLayout()
btn_row.addStretch()
ok_btn = QPushButton(tr('btn.save'))
ok_btn.setObjectName("btn_primary")
ok_btn.clicked.connect(self.accept)
cancel_btn = QPushButton(tr('btn.cancel'))
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(ok_btn)
btn_row.addWidget(cancel_btn)
layout.addLayout(btn_row)
self.setLayout(layout)
apply_dark_titlebar(self)
# -------- 내부 시각 계산 --------
def _resolve_meal_window(self) -> tuple[datetime, datetime, int]:
"""현재 위젯 값에서 (start_dt, end_dt, minutes) 계산.
자정 경계 처리:
1) 야간 출근자(clock_in 18시 이후)가 새벽 식사 시각을 입력하면
시작이 출근 이전으로 보이는데, 이를 다음날 새벽으로 +1day shift.
2) 종료가 시작보다 빠르면 종료에 +1day (점심 12:55→13:30 같은 정상은 영향 X).
주간 출근자(clock_in 09시)가 08시 입력 시 +1day는 적용하지 않아 검증에서 거절.
"""
s = self.start_edit.time().toPyTime()
e = self.end_edit.time().toPyTime()
base_date = (self._clock_in.date() if self._clock_in is not None
else datetime.today().date())
start_dt = datetime.combine(base_date, s)
end_dt = datetime.combine(base_date, e)
# 야간 출근자 자동 보정
if (self._clock_in is not None and start_dt < self._clock_in
and self._clock_in.hour >= 18):
start_dt += timedelta(days=1)
end_dt += timedelta(days=1)
if end_dt < start_dt:
end_dt += timedelta(days=1)
minutes = int((end_dt - start_dt).total_seconds() / 60)
return start_dt, end_dt, minutes
def _update_preview(self):
start_dt, end_dt, minutes = self._resolve_meal_window()
ok, reason = self._validate_window(start_dt, end_dt, minutes)
if not ok:
self.preview.setText(reason)
self.preview.setStyleSheet("color: #FA5252;")
else:
self.preview.setText(tr('meal.preview_total', minutes=minutes))
self.preview.setStyleSheet("color: #51CF66; font-weight: bold;")
def _validate_window(self, start_dt: datetime, end_dt: datetime,
minutes: int) -> tuple[bool, str]:
"""식사 시각이 출/퇴근 범위와 정합인지 검증."""
if minutes <= 0:
return False, tr('meal.error_start_after_end')
if minutes > 8 * 60:
# 자정 경계 처리 후 8시간 초과면 사용자 실수일 가능성 높음
return False, tr('meal.error_too_long')
if self._clock_in is not None and start_dt < self._clock_in:
return False, tr('meal.error_before_clock_in', time=self._clock_in.strftime('%H:%M'))
if self._clock_out is not None and end_dt > self._clock_out:
return False, tr('meal.error_after_clock_out', time=self._clock_out.strftime('%H:%M'))
return True, ""
def accept(self):
"""저장 버튼: 검증 실패 시 다이얼로그 닫지 않고 메시지박스."""
start_dt, end_dt, minutes = self._resolve_meal_window()
ok, reason = self._validate_window(start_dt, end_dt, minutes)
if not ok:
QMessageBox.warning(self, tr('meal.input_error_title'), reason)
return
super().accept()
def get_times(self) -> tuple[str, str, int]:
"""('HH:MM:SS', 'HH:MM:SS', total_minutes) 반환."""
s = self.start_edit.time().toPyTime()
e = self.end_edit.time().toPyTime()
start_str = f"{s.hour:02d}:{s.minute:02d}:00"
end_str = f"{e.hour:02d}:{e.minute:02d}:00"
_, _, minutes = self._resolve_meal_window()
return start_str, end_str, minutes