Clock_out_Time_Calculator/ui/calendar_view.py
KINDNICK f28f0619a4 v2.13.0: 익일 퇴근 처리 수정 + 날짜 지정 일일보고 + 연차 직접입력
Fixed:
- clock_out()이 datetime.now().date() 행에 퇴근을 써서 자정 넘긴 퇴근이
  UPDATE 0건으로 유실되던 버그. 출근일(근무일) 기준으로 수정.
  cancel_clock_out() 및 주말/공휴일 판정·외출 집계도 동일 기준으로 통일.
- 캘린더 시간 수정 / 과거기록 추가가 퇴근<=출근을 반려해 익일 퇴근을
  저장할 수 없던 문제. 익일로 해석하고 동시각만 반려.
  총 근무시간·연장근무 음수 계산도 수정.

Added:
- generate_daily_report(target_date) 날짜 파라미터화 + 캘린더 '보고서 복사'
  버튼. 기본값은 진행 중인 근무일(경계시간 전이면 출근일).
- 연차 직접입력 버튼(메인 창) — 미연결 상태였던 use_custom_leave()를
  기존 AddLeaveDialog 재사용으로 연결.
- resolve_work_datetimes() / is_overnight() 공용 헬퍼 + 유닛 테스트 6개.

Changed:
- 연차 빠른 사용에서 날짜·메모 팝업 제거 (연장근무와 동일하게 확인 1회).
- get_today_overtime_usage() / get_total_break_minutes_today()에 선택적
  date_str 파라미터 추가.

테스트: 유닛 200 / 통합 53 / GUI 8 / i18n 5 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:06:18 +09:00

672 lines
27 KiB
Python

"""
캘린더 뷰 - 월간 근무 기록 조회
"""
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QCalendarWidget, QTextEdit, QGroupBox,
QMessageBox)
from PyQt5.QtCore import QDate, Qt
from PyQt5.QtGui import QTextCharFormat, QColor
from datetime import datetime, date
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.database import Database
from core.i18n import tr
from ui.styles import ThemeColors, apply_dark_titlebar
class CalendarView(QDialog):
"""캘린더 뷰 다이얼로그"""
def __init__(self, parent=None, db=None):
super().__init__(parent)
self.db = db if db else Database()
self.init_ui()
self.load_calendar_data()
apply_dark_titlebar(self)
def init_ui(self):
"""UI 초기화"""
from core.i18n import tr
self.setWindowTitle(tr('window.calendar'))
self.setModal(True)
self.setMinimumSize(520, 820)
layout = QVBoxLayout()
layout.setSpacing(6)
layout.setContentsMargins(12, 10, 12, 10)
# 제목
title = QLabel(tr('cal.dialog_title'))
title.setObjectName("dialog_title")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
# 캘린더
self.calendar = QCalendarWidget()
self.calendar.setMinimumHeight(280)
self.calendar.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
self.calendar.clicked.connect(self.date_selected)
# 우클릭 컨텍스트 메뉴 (과거 일자 수동 추가)
self.calendar.setContextMenuPolicy(Qt.CustomContextMenu)
self.calendar.customContextMenuRequested.connect(self._show_date_context)
layout.addWidget(self.calendar, 1)
# 범례
legend_layout = QHBoxLayout()
legend_layout.setSpacing(12)
for _color, _txt in [('#51CF66', tr('cal.legend_normal')), ('#FA5252', tr('cal.legend_overtime')),
('#FAB005', tr('cal.legend_leave')), ('#6C6E73', tr('cal.legend_none'))]:
_item = QLabel(f"<span style='color:{_color};'>●</span> {_txt}")
_item.setTextFormat(Qt.RichText)
legend_layout.addWidget(_item)
legend_layout.addStretch()
layout.addLayout(legend_layout)
# 선택된 날짜 상세 정보
detail_group = QGroupBox(tr('cal.detail_group_title'))
detail_layout = QVBoxLayout()
detail_layout.setSpacing(6)
detail_layout.setContentsMargins(10, 20, 10, 8)
self.detail_text = QTextEdit()
self.detail_text.setReadOnly(True)
self.detail_text.setMaximumHeight(100)
detail_layout.addWidget(self.detail_text)
# 버튼 레이아웃
button_layout = QHBoxLayout()
button_layout.setSpacing(6)
self.edit_time_button = QPushButton(tr('cal.edit_time'))
self.edit_time_button.setObjectName("btn_primary")
self.edit_time_button.setEnabled(False)
self.edit_time_button.clicked.connect(self.edit_work_time)
button_layout.addWidget(self.edit_time_button)
self.copy_report_button = QPushButton(tr('cal.copy_report'))
self.copy_report_button.setEnabled(False)
self.copy_report_button.clicked.connect(self.copy_daily_report)
button_layout.addWidget(self.copy_report_button)
self.delete_record_button = QPushButton(tr('cal.delete_record'))
self.delete_record_button.setObjectName("btn_danger")
self.delete_record_button.setEnabled(False)
self.delete_record_button.clicked.connect(self.delete_selected_record)
button_layout.addWidget(self.delete_record_button)
detail_layout.addLayout(button_layout)
detail_group.setLayout(detail_layout)
layout.addWidget(detail_group)
# 메모 그룹
memo_group = QGroupBox(tr('cal.memo_group'))
memo_layout = QVBoxLayout()
memo_layout.setSpacing(6)
memo_layout.setContentsMargins(10, 20, 10, 8)
self.memo_edit = QTextEdit()
self.memo_edit.setMaximumHeight(70)
self.memo_edit.setPlaceholderText(tr('cal.memo_placeholder'))
memo_layout.addWidget(self.memo_edit)
self.save_memo_button = QPushButton(tr('cal.save_memo'))
self.save_memo_button.setObjectName("btn_primary")
self.save_memo_button.setEnabled(False)
self.save_memo_button.clicked.connect(self.save_memo)
memo_layout.addWidget(self.save_memo_button)
memo_group.setLayout(memo_layout)
layout.addWidget(memo_group)
# 선택된 날짜 저장용
self.selected_date_str = None
# 닫기 버튼
close_button = QPushButton(tr('btn.close'))
close_button.clicked.connect(self.close)
layout.addWidget(close_button)
self.setLayout(layout)
def load_calendar_data(self):
"""캘린더 데이터 로드"""
# 현재 표시된 월의 데이터 가져오기
current_date = self.calendar.selectedDate()
year = current_date.year()
month = current_date.month()
# 월간 통계 가져오기
stats = self.db.get_monthly_stats(year, month)
records = stats.get('records', [])
# 캘린더에 마킹
for record in records:
record_date = datetime.strptime(record['date'], '%Y-%m-%d').date()
qdate = QDate(record_date.year, record_date.month, record_date.day)
# 포맷 설정
fmt = QTextCharFormat()
if record.get('overtime_earned', 0) > 0:
# 연장근무
fmt.setBackground(QColor(ThemeColors.get('cal_overtime')))
elif record.get('clock_out'):
# 정상 근무
fmt.setBackground(QColor(ThemeColors.get('cal_normal')))
else:
# 출근만 있음
fmt.setBackground(QColor(ThemeColors.get('cal_incomplete')))
fmt.setForeground(QColor(ThemeColors.get('text_primary')))
self.calendar.setDateTextFormat(qdate, fmt)
def _show_date_context(self, pos):
"""캘린더 우클릭 메뉴 — 과거 일자 추가/편집/삭제."""
from PyQt5.QtWidgets import QMenu
qdate = self.calendar.selectedDate()
date_str = qdate.toString('yyyy-MM-dd')
existing = self.db.get_work_record(date_str)
menu = QMenu(self)
edit_action = delete_action = add_action = None
if existing:
edit_action = menu.addAction(tr('cal.context_edit', date=date_str))
delete_action = menu.addAction(tr('cal.context_delete', date=date_str))
else:
add_action = menu.addAction(tr('cal.context_add', date=date_str))
action = menu.exec_(self.calendar.mapToGlobal(pos))
if action is None:
return
# 텍스트 prefix 대신 액션 동일성으로 분기 (이모지 의존 제거)
if action == edit_action:
self._open_edit_dialog(date_str)
elif action == delete_action:
self._delete_record(date_str)
elif action == add_action:
self._add_past_record(date_str)
def _add_past_record(self, date_str: str):
"""과거 일자 수동 추가."""
from ui.past_record_dialog import PastRecordDialog
dialog = PastRecordDialog(self, date_str)
if dialog.exec_() != QDialog.Accepted:
return
data = dialog.get_data()
if not data:
return
try:
wid = self.db.add_work_record(date_str, data['clock_in'], is_manual=True)
if data.get('clock_out'):
# 총 시간/연장근무 계산 (익일 퇴근이면 퇴근에 +1일)
from core.time_calculator import resolve_work_datetimes
ci, co = resolve_work_datetimes(date_str, data['clock_in'], data['clock_out'])
from core.time_calculator import TimeCalculator
wm = self.db.get_work_minutes()
lunch = self.db.get_setting_int('lunch_duration_minutes', 60)
calc = TimeCalculator(work_minutes=wm, lunch_duration_minutes=lunch)
total = (co - ci).total_seconds() / 3600
ot_actual, ot_earned = calc.calculate_overtime(
ci, co,
include_lunch=data.get('lunch', False),
include_dinner=data.get('dinner', False),
)
self.db.update_clock_out(date_str, data['clock_out'], total, ot_actual, ot_earned)
if data.get('lunch'):
self.db.update_lunch_break(date_str, True)
if data.get('dinner'):
self.db.update_dinner_break(date_str, True)
if ot_earned > 0:
self.db.add_overtime_earned(wid, ot_earned, date_str)
self._refresh_calendar()
QMessageBox.information(self, tr('cal.add_done_title'), tr('cal.add_done_body', date=date_str))
except Exception as e:
QMessageBox.critical(self, tr('cal.add_error_title'), tr('cal.add_error_body', error=e))
def _open_edit_dialog(self, date_str: str):
"""기존 일자 편집 — date_selected로 우회 (이미 EditTimeDialog 있음)."""
from PyQt5.QtCore import QDate
y, m, d = date_str.split('-')
self.calendar.setSelectedDate(QDate(int(y), int(m), int(d)))
self.date_selected(self.calendar.selectedDate())
# 사용자가 화면 하단에 표시된 "✏️ 시간 수정" 버튼 클릭하면 편집
def _delete_record(self, date_str: str):
reply = QMessageBox.question(
tr('cal.delete_confirm_title'),
tr('cal.delete_confirm_body', date=date_str),
QMessageBox.Yes | QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
conn = self.db.get_connection()
cursor = conn.cursor()
try:
cursor.execute("DELETE FROM overtime_bank WHERE date = ?", (date_str,))
cursor.execute("DELETE FROM break_records WHERE date = ?", (date_str,))
cursor.execute("DELETE FROM work_records WHERE date = ?", (date_str,))
conn.commit()
self._refresh_calendar()
QMessageBox.information(self, tr('cal.delete_done_title'), tr('cal.delete_done_body', date=date_str))
except Exception as e:
conn.rollback()
QMessageBox.critical(self, tr('cal.edit_error_title'), str(e))
finally:
conn.close()
def _refresh_calendar(self):
"""캘린더 마킹 갱신."""
if hasattr(self, 'load_calendar_data'):
self.load_calendar_data()
elif hasattr(self, 'load_records'):
self.load_records()
def date_selected(self, qdate):
"""날짜 선택 시"""
selected_date = qdate.toPyDate()
date_str = selected_date.isoformat()
self.selected_date_str = date_str
# 해당 날짜 기록 조회
record = self.db.get_work_record(date_str)
if record:
# 상세 정보 표시
detail = tr('cal.detail_date_fmt', year=selected_date.year, month=selected_date.month, day=selected_date.day) + '\n\n'
detail += tr('cal.detail_clock_in', time=record['clock_in']) + '\n'
if record.get('clock_out'):
from core.time_calculator import is_overnight
out_str = record['clock_out']
if is_overnight(record['clock_in'], out_str):
out_str = tr('label.next_day_time', time=out_str)
detail += tr('cal.detail_clock_out', time=out_str) + '\n'
detail += tr('cal.detail_total_hours', hours=record.get('total_hours', 0)) + '\n'
if record.get('lunch_break'):
detail += tr('cal.detail_lunch_used') + '\n'
else:
detail += tr('cal.detail_lunch_unused') + '\n'
if record.get('dinner_break'):
detail += tr('cal.detail_dinner_used') + '\n'
else:
detail += tr('cal.detail_dinner_unused') + '\n'
if record.get('overtime_earned', 0) > 0:
earned_min = record['overtime_earned']
earned_hours = earned_min // 60
earned_mins = earned_min % 60
detail += '\n' + tr('cal.detail_overtime_earned', hours=earned_hours, minutes=earned_mins) + '\n'
else:
detail += tr('cal.detail_clock_out_none') + '\n'
if record.get('memo'):
detail += '\n' + tr('cal.detail_memo', memo=record['memo']) + '\n'
self.detail_text.setText(detail)
self.edit_time_button.setEnabled(True)
self.delete_record_button.setEnabled(True)
self.copy_report_button.setEnabled(True)
# 메모 필드 업데이트
self.memo_edit.setPlainText(record.get('memo', ''))
self.save_memo_button.setEnabled(True)
else:
self.detail_text.setText(tr('cal.detail_date_fmt', year=selected_date.year, month=selected_date.month, day=selected_date.day) + '\n\n' + tr('cal.no_record'))
self.edit_time_button.setEnabled(False)
self.delete_record_button.setEnabled(False)
self.copy_report_button.setEnabled(False)
self.memo_edit.setPlainText('')
self.save_memo_button.setEnabled(False)
def copy_daily_report(self):
"""선택한 날짜의 일일보고서를 클립보드로 복사.
메인 창의 생성기를 그대로 재사용 — 당일 보고를 놓쳐도 나중에 다시 뽑을 수 있다.
"""
if not self.selected_date_str:
return
main_window = self.parent()
if main_window is None or not hasattr(main_window, 'generate_daily_report'):
return # 단독 실행(__main__) 등 부모가 메인 창이 아닌 경우
main_window.generate_daily_report(self.selected_date_str)
def delete_selected_record(self):
"""선택된 날짜의 출근 기록 삭제"""
if not self.selected_date_str:
return
reply = QMessageBox.question(
self,
tr('cal.delete_selected_title'),
tr('cal.delete_selected_body', date=self.selected_date_str),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.db.delete_work_record(self.selected_date_str)
QMessageBox.information(
self,
tr('cal.delete_done_title'),
tr('cal.delete_done_body', date=self.selected_date_str)
)
# 캘린더 새로고침
self.load_calendar_data()
self.detail_text.clear()
self.delete_record_button.setEnabled(False)
def save_memo(self):
"""메모 저장"""
if not self.selected_date_str:
return
memo = self.memo_edit.toPlainText().strip()
# 메모 업데이트
self.db.update_work_memo(self.selected_date_str, memo)
QMessageBox.information(
self,
tr('cal.save_memo_title'),
tr('cal.save_memo_body', date=self.selected_date_str)
)
# 상세 정보 새로고침
qdate = self.calendar.selectedDate()
self.date_selected(qdate)
def edit_work_time(self):
"""출퇴근 시간 수정"""
if not self.selected_date_str:
return
# 기존 기록 조회
record = self.db.get_work_record(self.selected_date_str)
if not record:
return
# 수정 다이얼로그 표시
dialog = EditWorkTimeDialog(self, self.db, self.selected_date_str, record)
if dialog.exec_():
# 수정 성공 시 캘린더 새로고침
self.load_calendar_data()
qdate = self.calendar.selectedDate()
self.date_selected(qdate)
# 부모 윈도우 업데이트
if self.parent() and hasattr(self.parent(), 'update_overtime_balance'):
self.parent().update_overtime_balance()
class EditWorkTimeDialog(QDialog):
"""출퇴근 시간 수정 다이얼로그"""
def __init__(self, parent, db, date_str, record):
super().__init__(parent)
self.db = db
self.date_str = date_str
self.record = record
self.init_ui()
apply_dark_titlebar(self)
def init_ui(self):
"""UI 초기화"""
from PyQt5.QtWidgets import QTimeEdit
from PyQt5.QtCore import QTime
self.setWindowTitle(tr('cal.edit_dialog_title'))
self.setModal(True)
self.setMinimumWidth(420)
layout = QVBoxLayout()
layout.setSpacing(8)
layout.setContentsMargins(12, 10, 12, 10)
# 제목
title = QLabel(tr('cal.edit_dialog_subtitle', date=self.date_str))
title.setObjectName("dialog_subtitle")
layout.addWidget(title)
# 출근 시간
clock_in_layout = QHBoxLayout()
clock_in_layout.setSpacing(4)
clock_in_label = QLabel(tr('cal.label_clock_in'))
clock_in_label.setObjectName("field_label")
clock_in_label.setFixedWidth(40)
clock_in_layout.addWidget(clock_in_label)
clock_in_minus_btn = QPushButton(tr('cal.btn_minus_30'))
clock_in_minus_btn.setFixedWidth(55)
clock_in_minus_btn.clicked.connect(lambda: self.adjust_time(self.clock_in_edit, -30))
clock_in_layout.addWidget(clock_in_minus_btn)
self.clock_in_edit = QTimeEdit()
self.clock_in_edit.setDisplayFormat("HH:mm:ss")
clock_in_time = QTime.fromString(self.record['clock_in'], "HH:mm:ss")
self.clock_in_edit.setTime(clock_in_time)
clock_in_layout.addWidget(self.clock_in_edit)
clock_in_plus_btn = QPushButton(tr('cal.btn_plus_30'))
clock_in_plus_btn.setFixedWidth(55)
clock_in_plus_btn.clicked.connect(lambda: self.adjust_time(self.clock_in_edit, 30))
clock_in_layout.addWidget(clock_in_plus_btn)
layout.addLayout(clock_in_layout)
# 퇴근 시간
clock_out_layout = QHBoxLayout()
clock_out_layout.setSpacing(4)
clock_out_label = QLabel(tr('cal.label_clock_out'))
clock_out_label.setObjectName("field_label")
clock_out_label.setFixedWidth(40)
clock_out_layout.addWidget(clock_out_label)
clock_out_minus_btn = QPushButton(tr('cal.btn_minus_30'))
clock_out_minus_btn.setFixedWidth(55)
clock_out_minus_btn.clicked.connect(lambda: self.adjust_time(self.clock_out_edit, -30))
clock_out_layout.addWidget(clock_out_minus_btn)
self.clock_out_edit = QTimeEdit()
self.clock_out_edit.setDisplayFormat("HH:mm:ss")
if self.record.get('clock_out'):
clock_out_time = QTime.fromString(self.record['clock_out'], "HH:mm:ss")
self.clock_out_edit.setTime(clock_out_time)
else:
# 미퇴근 기록: 기본값 00:00은 "익일 자정 퇴근"으로 오인될 수 있으므로
# 출근 시각과 동일하게 두어 save_changes()의 동시각 반려에 걸리게 한다.
self.clock_out_edit.setTime(clock_in_time)
clock_out_layout.addWidget(self.clock_out_edit)
clock_out_plus_btn = QPushButton(tr('cal.btn_plus_30'))
clock_out_plus_btn.setFixedWidth(55)
clock_out_plus_btn.clicked.connect(lambda: self.adjust_time(self.clock_out_edit, 30))
clock_out_layout.addWidget(clock_out_plus_btn)
layout.addLayout(clock_out_layout)
# 익일 퇴근 안내 (퇴근 < 출근이면 표시)
self.overnight_note = QLabel('')
self.overnight_note.setObjectName("note_text")
self.overnight_note.setVisible(False)
layout.addWidget(self.overnight_note)
self.clock_in_edit.timeChanged.connect(self._refresh_overnight_note)
self.clock_out_edit.timeChanged.connect(self._refresh_overnight_note)
self._refresh_overnight_note()
# 점심/저녁 체크박스 - 한 줄에
from PyQt5.QtWidgets import QCheckBox
check_layout = QHBoxLayout()
self.lunch_check = QCheckBox(tr('cal.check_lunch_1h'))
self.lunch_check.setChecked(bool(self.record.get('lunch_break', False)))
check_layout.addWidget(self.lunch_check)
self.dinner_check = QCheckBox(tr('cal.check_dinner_1h'))
self.dinner_check.setChecked(bool(self.record.get('dinner_break', False)))
check_layout.addWidget(self.dinner_check)
layout.addLayout(check_layout)
# 안내 메시지
note = QLabel(tr('cal.edit_note'))
note.setObjectName("note_text")
layout.addWidget(note)
# 버튼
button_layout = QHBoxLayout()
save_button = QPushButton(tr('btn.save'))
save_button.setObjectName("btn_success")
save_button.clicked.connect(self.save_changes)
cancel_button = QPushButton(tr('btn.cancel'))
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(save_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def adjust_time(self, time_edit, minutes: int):
"""시간 조정 (±분)"""
current_time = time_edit.time()
new_time = current_time.addSecs(minutes * 60)
time_edit.setTime(new_time)
def _refresh_overnight_note(self):
"""퇴근이 출근보다 이르면 익일 퇴근으로 해석됨을 표시."""
ci = self.clock_in_edit.time()
co = self.clock_out_edit.time()
if co >= ci:
self.overnight_note.setVisible(False)
return
total_min = ci.secsTo(co) // 60 + 24 * 60 # 익일까지
self.overnight_note.setText(tr('edit.overnight_note',
hours=total_min // 60, minutes=total_min % 60))
self.overnight_note.setVisible(True)
def save_changes(self):
"""변경사항 저장"""
clock_in = self.clock_in_edit.time().toString("HH:mm:ss")
clock_out = self.clock_out_edit.time().toString("HH:mm:ss")
lunch_break = self.lunch_check.isChecked()
dinner_break = self.dinner_check.isChecked()
# 퇴근 < 출근은 익일 퇴근(야근)으로 허용. 같은 시각만 반려.
if clock_out == clock_in:
QMessageBox.warning(
self,
tr('cal.time_error_title'),
tr('cal.time_error_body')
)
return
# 근무 시간 계산
from datetime import datetime, timedelta
from core.time_calculator import TimeCalculator, resolve_work_datetimes
# 해당 날짜의 datetime 객체 생성 (익일 퇴근이면 퇴근에 +1일)
clock_in_dt, clock_out_dt = resolve_work_datetimes(self.date_str, clock_in, clock_out)
# 총 근무시간 계산
total_hours = (clock_out_dt - clock_in_dt).total_seconds() / 3600
from core.settings_keys import (
WORK_MINUTES, WORK_HOURS, LUNCH_DURATION_MINUTES, DINNER_DURATION_MINUTES,
)
settings = self.db.get_settings()
work_minutes = settings.get(WORK_MINUTES)
if work_minutes is None:
# 레거시 DB: work_hours만 있고 마이그레이션 전인 경우 폴백
try:
work_minutes = int(round(float(settings.get(WORK_HOURS, 8)) * 60))
except (ValueError, TypeError):
work_minutes = 480
time_calc = TimeCalculator(
work_minutes=int(work_minutes),
lunch_duration_minutes=int(settings.get(LUNCH_DURATION_MINUTES, 60)),
dinner_duration_minutes=int(settings.get(DINNER_DURATION_MINUTES, 60)),
)
# 해당 날짜의 외출 시간 조회
break_records = self.db.get_break_records_by_date(self.date_str)
break_minutes = sum(r.get('total_minutes', 0) or 0 for r in break_records)
# calculate_overtime 호출
overtime_actual, overtime_earned = time_calc.calculate_overtime(
clock_in_dt, clock_out_dt,
include_lunch=lunch_break,
include_dinner=dinner_break,
break_minutes=break_minutes
)
# DB 업데이트
conn = None
try:
conn = self.db.get_connection()
cursor = conn.cursor()
# 기존 overtime_earned 값 조회
old_overtime_earned = self.record.get('overtime_earned', 0) or 0
# work_records 업데이트 (dinner_break 포함)
cursor.execute('''
UPDATE work_records
SET clock_in = ?, clock_out = ?, lunch_break = ?, dinner_break = ?,
total_hours = ?, overtime_minutes = ?, overtime_earned = ?
WHERE date = ?
''', (clock_in, clock_out, lunch_break, dinner_break, total_hours, overtime_actual, overtime_earned, self.date_str))
# overtime_bank 테이블도 업데이트 (연장근무 적립 내역)
work_record_id = self.record.get('id')
if work_record_id:
# 기존 적립 내역 삭제
cursor.execute('''
DELETE FROM overtime_bank
WHERE work_record_id = ? AND date = ?
''', (work_record_id, self.date_str))
# 새로운 적립 내역 추가 (0보다 클 때만)
if overtime_earned > 0:
cursor.execute('''
INSERT INTO overtime_bank (work_record_id, earned_minutes, date)
VALUES (?, ?, ?)
''', (work_record_id, overtime_earned, self.date_str))
conn.commit()
QMessageBox.information(
self,
tr('cal.edit_done_title'),
tr('cal.edit_done_body',
date=self.date_str, clock_in=clock_in, clock_out=clock_out,
lunch=tr('cal.detail_lunch_used') if lunch_break else tr('cal.detail_lunch_unused'),
dinner=tr('cal.detail_dinner_used') if dinner_break else tr('cal.detail_dinner_unused'),
break_minutes=break_minutes, total_hours=total_hours, overtime_earned=overtime_earned)
)
self.accept()
except Exception as e:
QMessageBox.critical(
self,
tr('cal.edit_error_title'),
tr('cal.edit_error_body', error=str(e))
)
finally:
if conn:
conn.close()
# 테스트 코드
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
dialog = CalendarView()
dialog.exec_()