Clock_out_Time_Calculator/ui/meal_time_dialog.py
KINDNICK 5fb8655a47 v2.11.0: UI 전면 다크 리디자인 + 라인 아이콘 + 적립 가드/삭제
- 모던 다크 미니멀 테마(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>
2026-06-04 18:21:54 +09:00

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