- Meal time dialog (right-click lunch/dinner button to enter actual times) - Calendar right-click context: add/edit/delete past records - Monthly goal settings + progress widget (overtime cap, avg daily) - CSV import (our standard format) with conflict policy - Global crash handler with Gitea Issues auto-report Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
628 lines
24 KiB
Python
628 lines
24 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 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("월간 근무 기록")
|
||
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)
|
||
legend_layout.addWidget(QLabel("🟢 정상"))
|
||
legend_layout.addWidget(QLabel("🔴 연장"))
|
||
legend_layout.addWidget(QLabel("🟡 휴가"))
|
||
legend_layout.addWidget(QLabel("⚪ 없음"))
|
||
legend_layout.addStretch()
|
||
layout.addLayout(legend_layout)
|
||
|
||
# 선택된 날짜 상세 정보
|
||
detail_group = QGroupBox("선택된 날짜 정보")
|
||
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("✏️ 시간 수정")
|
||
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.delete_record_button = QPushButton("🗑️ 기록 삭제")
|
||
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("메모")
|
||
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("추가근무 사유, 특이사항 등...")
|
||
memo_layout.addWidget(self.memo_edit)
|
||
|
||
self.save_memo_button = QPushButton("💾 메모 저장")
|
||
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)
|
||
if existing:
|
||
edit_action = menu.addAction(f"✏️ {date_str} 편집")
|
||
delete_action = menu.addAction(f"🗑️ {date_str} 삭제")
|
||
else:
|
||
add_action = menu.addAction(f"➕ {date_str} 기록 추가")
|
||
|
||
action = menu.exec_(self.calendar.mapToGlobal(pos))
|
||
if action is None:
|
||
return
|
||
|
||
if existing and action.text().startswith("✏️"):
|
||
self._open_edit_dialog(date_str)
|
||
elif existing and action.text().startswith("🗑️"):
|
||
self._delete_record(date_str)
|
||
elif not existing and action.text().startswith("➕"):
|
||
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'):
|
||
# 총 시간/연장근무 계산
|
||
from datetime import datetime as _dt
|
||
ci = _dt.strptime(f"{date_str} {data['clock_in']}", '%Y-%m-%d %H:%M:%S')
|
||
co = _dt.strptime(f"{date_str} {data['clock_out']}", '%Y-%m-%d %H:%M:%S')
|
||
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, "추가 완료", f"{date_str} 기록이 추가되었습니다.")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "오류", f"기록 추가 실패: {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(
|
||
self, "삭제 확인",
|
||
f"{date_str} 기록을 정말 삭제하시겠습니까?\n(연장근무 적립 내역도 함께 삭제됩니다)",
|
||
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, "삭제 완료", f"{date_str} 기록 삭제됨")
|
||
except Exception as e:
|
||
conn.rollback()
|
||
QMessageBox.critical(self, "오류", 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 = f"📅 {selected_date.strftime('%Y년 %m월 %d일')}\n\n"
|
||
detail += f"출근: {record['clock_in']}\n"
|
||
|
||
if record.get('clock_out'):
|
||
detail += f"퇴근: {record['clock_out']}\n"
|
||
detail += f"총 근무시간: {record.get('total_hours', 0):.1f}시간\n"
|
||
|
||
if record.get('lunch_break'):
|
||
detail += f"점심시간: 사용함\n"
|
||
else:
|
||
detail += f"점심시간: 미사용\n"
|
||
|
||
if record.get('dinner_break'):
|
||
detail += f"저녁시간: 사용함\n"
|
||
else:
|
||
detail += f"저녁시간: 미사용\n"
|
||
|
||
if record.get('overtime_earned', 0) > 0:
|
||
earned_min = record['overtime_earned']
|
||
earned_hours = earned_min // 60
|
||
earned_mins = earned_min % 60
|
||
detail += f"\n🔥 연장근무 적립: {earned_hours}시간 {earned_mins}분\n"
|
||
else:
|
||
detail += f"퇴근: 미기록\n"
|
||
|
||
if record.get('memo'):
|
||
detail += f"\n메모: {record['memo']}\n"
|
||
|
||
self.detail_text.setText(detail)
|
||
self.edit_time_button.setEnabled(True)
|
||
self.delete_record_button.setEnabled(True)
|
||
|
||
# 메모 필드 업데이트
|
||
self.memo_edit.setPlainText(record.get('memo', ''))
|
||
self.save_memo_button.setEnabled(True)
|
||
else:
|
||
self.detail_text.setText(f"📅 {selected_date.strftime('%Y년 %m월 %d일')}\n\n기록이 없습니다.")
|
||
self.edit_time_button.setEnabled(False)
|
||
self.delete_record_button.setEnabled(False)
|
||
self.memo_edit.setPlainText('')
|
||
self.save_memo_button.setEnabled(False)
|
||
|
||
def delete_selected_record(self):
|
||
"""선택된 날짜의 출근 기록 삭제"""
|
||
if not self.selected_date_str:
|
||
return
|
||
|
||
reply = QMessageBox.question(
|
||
self,
|
||
"출근 기록 삭제",
|
||
f"{self.selected_date_str}의 출근 기록을 삭제하시겠습니까?\n\n"
|
||
f"※ 연관된 연장근무 적립/사용 기록도 함께 삭제됩니다.\n"
|
||
f"※ 이 작업은 되돌릴 수 없습니다.",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.db.delete_work_record(self.selected_date_str)
|
||
|
||
QMessageBox.information(
|
||
self,
|
||
"삭제 완료",
|
||
f"{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,
|
||
"메모 저장",
|
||
f"{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("출퇴근 시간 수정")
|
||
self.setModal(True)
|
||
self.setMinimumWidth(420)
|
||
|
||
layout = QVBoxLayout()
|
||
layout.setSpacing(8)
|
||
layout.setContentsMargins(12, 10, 12, 10)
|
||
|
||
# 제목
|
||
title = QLabel(f"📅 {self.date_str} 출퇴근 시간 수정")
|
||
title.setObjectName("dialog_subtitle")
|
||
layout.addWidget(title)
|
||
|
||
# 출근 시간
|
||
clock_in_layout = QHBoxLayout()
|
||
clock_in_layout.setSpacing(4)
|
||
clock_in_label = QLabel("출근:")
|
||
clock_in_label.setObjectName("field_label")
|
||
clock_in_label.setFixedWidth(40)
|
||
clock_in_layout.addWidget(clock_in_label)
|
||
|
||
clock_in_minus_btn = QPushButton("-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("+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("퇴근:")
|
||
clock_out_label.setObjectName("field_label")
|
||
clock_out_label.setFixedWidth(40)
|
||
clock_out_layout.addWidget(clock_out_label)
|
||
|
||
clock_out_minus_btn = QPushButton("-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)
|
||
clock_out_layout.addWidget(self.clock_out_edit)
|
||
|
||
clock_out_plus_btn = QPushButton("+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)
|
||
|
||
# 점심/저녁 체크박스 - 한 줄에
|
||
from PyQt5.QtWidgets import QCheckBox
|
||
check_layout = QHBoxLayout()
|
||
self.lunch_check = QCheckBox("점심 (1시간)")
|
||
self.lunch_check.setChecked(bool(self.record.get('lunch_break', False)))
|
||
check_layout.addWidget(self.lunch_check)
|
||
|
||
self.dinner_check = QCheckBox("저녁 (1시간)")
|
||
self.dinner_check.setChecked(bool(self.record.get('dinner_break', False)))
|
||
check_layout.addWidget(self.dinner_check)
|
||
layout.addLayout(check_layout)
|
||
|
||
# 안내 메시지
|
||
note = QLabel("※ 수정 시 연장근무 내역이 재계산됩니다.")
|
||
note.setObjectName("note_text")
|
||
layout.addWidget(note)
|
||
|
||
# 버튼
|
||
button_layout = QHBoxLayout()
|
||
save_button = QPushButton("저장")
|
||
save_button.setObjectName("btn_success")
|
||
save_button.clicked.connect(self.save_changes)
|
||
|
||
cancel_button = QPushButton("취소")
|
||
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 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,
|
||
"시간 오류",
|
||
"퇴근 시간은 출근 시간보다 늦어야 합니다."
|
||
)
|
||
return
|
||
|
||
# 근무 시간 계산
|
||
from datetime import datetime, timedelta
|
||
from core.time_calculator import TimeCalculator
|
||
|
||
# 해당 날짜의 datetime 객체 생성
|
||
date_obj = datetime.strptime(self.date_str, "%Y-%m-%d").date()
|
||
clock_in_dt = datetime.combine(date_obj, datetime.strptime(clock_in, "%H:%M:%S").time())
|
||
clock_out_dt = datetime.combine(date_obj, datetime.strptime(clock_out, "%H:%M:%S").time())
|
||
|
||
# 총 근무시간 계산
|
||
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,
|
||
"수정 완료",
|
||
f"{self.date_str}의 출퇴근 시간이 수정되었습니다.\n\n"
|
||
f"출근: {clock_in}\n"
|
||
f"퇴근: {clock_out}\n"
|
||
f"점심시간: {'사용' if lunch_break else '미사용'}\n"
|
||
f"저녁시간: {'사용' if dinner_break else '미사용'}\n"
|
||
f"외출시간: {break_minutes}분\n"
|
||
f"총 근무시간: {total_hours:.1f}시간\n"
|
||
f"연장근무: {overtime_earned}분 적립"
|
||
)
|
||
|
||
self.accept()
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(
|
||
self,
|
||
"오류",
|
||
f"수정 중 오류가 발생했습니다:\n{str(e)}"
|
||
)
|
||
finally:
|
||
if conn:
|
||
conn.close()
|
||
|
||
|
||
# 테스트 코드
|
||
if __name__ == "__main__":
|
||
from PyQt5.QtWidgets import QApplication
|
||
|
||
app = QApplication(sys.argv)
|
||
dialog = CalendarView()
|
||
dialog.exec_()
|