- 모던 다크 미니멀 테마(NanumSquare 번들, 단일 accent #4DABF7, 8px radius, flat 버튼, 다크 기본값) - 라인 아이콘 시스템(ui/icons.py, QtSvg) — 앱 전반 이모지 교체 - 다크 깨짐 수정: 테이블 헤더/코너 흰색, 도움말 탭 흰 라인, 트레이/미니위젯 메뉴 - fix: 자동 적립 OFF가 자동 퇴근 경로에서 무시되던 버그(게이팅) - feat: 연장근무 적립 기록 삭제(우클릭) - 테스트 3건 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
7.6 KiB
Python
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 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
|