""" 점심/저녁 실제 시간 입력 다이얼로그. 기본 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 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 title_kr = '점심' if meal_type == 'lunch' else '저녁' self.setWindowTitle(f"{title_kr} 시간 입력") self.setModal(True) self.setFixedSize(380, 260) layout = QVBoxLayout() layout.setSpacing(10) layout.setContentsMargins(20, 16, 20, 16) info_text = (f"{title_kr} 시작·종료 시각을 입력하세요.\n" f"자동 적용된 {default_minutes}분 대신 정확한 시간으로 기록됩니다.") if clock_in_time is not None: info_text += f"\n출근 {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("시작:")) 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("종료:")) 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("저장") ok_btn.setObjectName("btn_primary") ok_btn.clicked.connect(self.accept) cancel_btn = QPushButton("취소") 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(f"{reason}") self.preview.setStyleSheet("color: #FA5252;") else: self.preview.setText(f"총 {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, "시작이 종료보다 늦습니다" if minutes > 8 * 60: # 자정 경계 처리 후 8시간 초과면 사용자 실수일 가능성 높음 return False, "식사 시간이 8시간을 초과합니다" if self._clock_in is not None and start_dt < self._clock_in: return False, f"출근({self._clock_in.strftime('%H:%M')}) 이전입니다" if self._clock_out is not None and end_dt > self._clock_out: return False, f"퇴근({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, "입력 오류", 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