""" 연장근무 상세 내역 뷰 """ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QGroupBox, QMessageBox, QMenu, QAction, QDateEdit, QSpinBox, QLineEdit, QComboBox) from PyQt5.QtCore import Qt, QDate from PyQt5.QtGui import QColor 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 get_theme, ThemeColors, apply_dark_titlebar class OvertimeView(QDialog): """연장근무 상세 내역 다이얼로그""" def __init__(self, parent=None, db=None): super().__init__(parent) self.db = db if db else Database() self.init_ui() self.load_data() apply_dark_titlebar(self) def init_ui(self): """UI 초기화""" self.setWindowTitle(tr('window.overtime_view')) self.setModal(True) self.setMinimumSize(800, 500) layout = QVBoxLayout() layout.setSpacing(6) layout.setContentsMargins(12, 10, 12, 10) # 제목 + 잔액 한 줄 header_layout = QHBoxLayout() title = QLabel("연장근무 내역") title.setObjectName("dialog_title") header_layout.addWidget(title) header_layout.addStretch() self.balance_label = QLabel("잔액: 0분") self.balance_label.setObjectName("badge_balance") header_layout.addWidget(self.balance_label) layout.addLayout(header_layout) # 적립 내역 earned_group = QGroupBox("💰 적립 내역") earned_layout = QVBoxLayout() earned_layout.setSpacing(4) earned_layout.setContentsMargins(8, 20, 8, 6) self.earned_table = QTableWidget() self.earned_table.setColumnCount(3) self.earned_table.setHorizontalHeaderLabels(["날짜", "적립", "메모"]) self.earned_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.earned_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.earned_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.earned_table.setAlternatingRowColors(True) self.earned_table.setEditTriggers(QTableWidget.NoEditTriggers) self.earned_table.setSelectionBehavior(QTableWidget.SelectRows) earned_layout.addWidget(self.earned_table) add_earned_button = QPushButton("➕ 수동 적립") add_earned_button.clicked.connect(self.add_earned_record) earned_layout.addWidget(add_earned_button) earned_group.setLayout(earned_layout) layout.addWidget(earned_group) # 사용 내역 used_group = QGroupBox("📤 사용 내역") used_layout = QVBoxLayout() used_layout.setSpacing(4) used_layout.setContentsMargins(8, 20, 8, 6) self.used_table = QTableWidget() self.used_table.setColumnCount(3) self.used_table.setHorizontalHeaderLabels(["날짜", "사용", "사유"]) self.used_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.used_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.used_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.used_table.setAlternatingRowColors(True) self.used_table.setEditTriggers(QTableWidget.NoEditTriggers) self.used_table.setSelectionBehavior(QTableWidget.SelectRows) self.used_table.setContextMenuPolicy(Qt.CustomContextMenu) self.used_table.customContextMenuRequested.connect(self.show_used_context_menu) used_layout.addWidget(self.used_table) add_used_button = QPushButton("➕ 수동 사용") add_used_button.clicked.connect(self.add_used_record) used_layout.addWidget(add_used_button) used_group.setLayout(used_layout) layout.addWidget(used_group) # 닫기 버튼 close_button = QPushButton("닫기") close_button.clicked.connect(self.close) layout.addWidget(close_button) self.setLayout(layout) def load_data(self): """데이터 로드""" # 잔액 업데이트 balance = self.db.get_total_overtime_balance() hours = balance // 60 minutes = balance % 60 self.balance_label.setText(f"현재 잔액: {hours}시간 {minutes}분 ({balance}분)") # 적립 내역 로드 conn = self.db.get_connection() cursor = conn.cursor() cursor.execute(''' SELECT ob.date, ob.earned_minutes, CASE WHEN ob.work_record_id IS NULL THEN '수동 추가' ELSE COALESCE(wr.memo, '') END as memo FROM overtime_bank ob LEFT JOIN work_records wr ON ob.work_record_id = wr.id ORDER BY ob.date DESC ''') earned_records = cursor.fetchall() self.earned_table.setRowCount(len(earned_records)) for i, record in enumerate(earned_records): date_item = QTableWidgetItem(record[0]) date_item.setTextAlignment(Qt.AlignCenter) minutes = record[1] hours = minutes // 60 mins = minutes % 60 time_str = f"{hours}시간 {mins}분" if hours > 0 else f"{mins}분" time_item = QTableWidgetItem(time_str) time_item.setTextAlignment(Qt.AlignCenter) time_item.setForeground(QColor(39, 174, 96)) # 초록색 memo_item = QTableWidgetItem(record[2] or "") self.earned_table.setItem(i, 0, date_item) self.earned_table.setItem(i, 1, time_item) self.earned_table.setItem(i, 2, memo_item) # 사용 내역 로드 (잔액 조정 제외) cursor.execute(''' SELECT id, date, used_minutes, reason FROM overtime_usage WHERE COALESCE(reason, '') != '잔액 조정' ORDER BY date DESC ''') used_records = cursor.fetchall() self.used_table.setRowCount(len(used_records)) for i, record in enumerate(used_records): # ID를 숨겨진 데이터로 저장 date_item = QTableWidgetItem(record[1]) date_item.setTextAlignment(Qt.AlignCenter) date_item.setData(Qt.UserRole, record[0]) # ID 저장 minutes = record[2] hours = minutes // 60 mins = minutes % 60 time_str = f"{hours}시간 {mins}분" if hours > 0 else f"{mins}분" time_item = QTableWidgetItem(time_str) time_item.setTextAlignment(Qt.AlignCenter) time_item.setForeground(QColor(231, 76, 60)) # 빨간색 reason_item = QTableWidgetItem(record[3] or "") self.used_table.setItem(i, 0, date_item) self.used_table.setItem(i, 1, time_item) self.used_table.setItem(i, 2, reason_item) conn.close() def show_used_context_menu(self, position): """사용 내역 우클릭 메뉴""" # 선택된 행이 있는지 확인 selected_rows = self.used_table.selectionModel().selectedRows() if not selected_rows: return # 컨텍스트 메뉴 생성 menu = QMenu(self) delete_action = QAction("❌ 삭제", self) delete_action.triggered.connect(self.delete_used_record) menu.addAction(delete_action) # 메뉴 표시 menu.exec_(self.used_table.viewport().mapToGlobal(position)) def delete_used_record(self): """사용 기록 삭제""" # 선택된 행 가져오기 selected_rows = self.used_table.selectionModel().selectedRows() if not selected_rows: return row = selected_rows[0].row() date_item = self.used_table.item(row, 0) time_item = self.used_table.item(row, 1) reason_item = self.used_table.item(row, 2) # ID 가져오기 usage_id = date_item.data(Qt.UserRole) # 확인 메시지 reply = QMessageBox.question( self, "삭제 확인", f"다음 사용 기록을 삭제하시겠습니까?\n\n" f"날짜: {date_item.text()}\n" f"시간: {time_item.text()}\n" f"사유: {reason_item.text()}", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: # 데이터베이스에서 삭제 conn = self.db.get_connection() cursor = conn.cursor() cursor.execute('DELETE FROM overtime_usage WHERE id = ?', (usage_id,)) conn.commit() conn.close() # 화면 새로고침 self.load_data() # 부모 윈도우의 잔액도 업데이트 if self.parent() and hasattr(self.parent(), 'update_overtime_balance'): self.parent().update_overtime_balance() def add_earned_record(self): """수동 적립 추가""" dialog = AddOvertimeEarnedDialog(self, self.db) if dialog.exec_() == QDialog.Accepted: self.load_data() # 부모 윈도우 업데이트 if self.parent() and hasattr(self.parent(), 'update_overtime_balance'): self.parent().update_overtime_balance() def add_used_record(self): """수동 사용 추가""" dialog = AddOvertimeUsedDialog(self, self.db) if dialog.exec_() == QDialog.Accepted: self.load_data() # 부모 윈도우 업데이트 if self.parent() and hasattr(self.parent(), 'update_overtime_balance'): self.parent().update_overtime_balance() class AddOvertimeEarnedDialog(QDialog): """추가근무 수동 적립 다이얼로그""" def __init__(self, parent=None, db=None): super().__init__(parent) self.db = db self.init_ui() apply_dark_titlebar(self) def init_ui(self): """UI 초기화""" self.setWindowTitle("추가근무 수동 적립") self.setModal(True) self.setMinimumWidth(360) layout = QVBoxLayout() layout.setSpacing(8) layout.setContentsMargins(12, 10, 12, 10) # 제목 title = QLabel("추가근무 수동 적립") title.setObjectName("dialog_subtitle") title.setAlignment(Qt.AlignCenter) layout.addWidget(title) # 날짜 date_layout = QHBoxLayout() date_label = QLabel("날짜:") date_label.setObjectName("field_label") date_label.setFixedWidth(60) self.date_edit = QDateEdit() self.date_edit.setDate(QDate.currentDate()) self.date_edit.setCalendarPopup(True) date_layout.addWidget(date_label) date_layout.addWidget(self.date_edit) layout.addLayout(date_layout) # 시간 (30분 단위) time_layout = QHBoxLayout() time_label = QLabel("시간:") time_label.setObjectName("field_label") time_label.setFixedWidth(60) self.hour_spin = QSpinBox() self.hour_spin.setRange(0, 23) self.hour_spin.setSuffix("시간") self.minute_combo = QComboBox() self.minute_combo.addItems(["0분", "30분"]) time_layout.addWidget(time_label) time_layout.addWidget(self.hour_spin) time_layout.addWidget(self.minute_combo) layout.addLayout(time_layout) # 메모 memo_layout = QHBoxLayout() memo_label = QLabel("메모:") memo_label.setObjectName("field_label") memo_label.setFixedWidth(60) self.memo_edit = QLineEdit() self.memo_edit.setPlaceholderText("선택사항") memo_layout.addWidget(memo_label) memo_layout.addWidget(self.memo_edit) layout.addLayout(memo_layout) # 버튼 button_layout = QHBoxLayout() save_button = QPushButton("저장") save_button.setObjectName("btn_primary") save_button.clicked.connect(self.save) 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 save(self): """저장""" # 시간 계산 (30분 단위) hours = self.hour_spin.value() minutes = 0 if self.minute_combo.currentText() == "0분" else 30 total_minutes = hours * 60 + minutes if total_minutes == 0: QMessageBox.warning(self, "입력 오류", "0분은 추가할 수 없습니다.") return date = self.date_edit.date().toString("yyyy-MM-dd") memo = self.memo_edit.text().strip() # DB에 저장 (work_record_id는 NULL) self.db.add_overtime_earned(None, total_minutes, date) # 메모가 있으면 work_records 업데이트 (기존 메모 보존) if memo: record = self.db.get_work_record(date) if record: existing_memo = record.get('memo', '') or '' # 기존 메모가 있으면 줄바꿈 후 추가 if existing_memo: new_memo = f"{existing_memo}\n[수동 적립] {memo}" else: new_memo = f"[수동 적립] {memo}" conn = self.db.get_connection() cursor = conn.cursor() cursor.execute(''' UPDATE work_records SET memo = ? WHERE date = ? ''', (new_memo, date)) conn.commit() conn.close() QMessageBox.information( self, "저장 완료", f"{hours}시간 {minutes}분이 적립되었습니다." ) self.accept() class AddOvertimeUsedDialog(QDialog): """추가근무 수동 사용 다이얼로그""" def __init__(self, parent=None, db=None): super().__init__(parent) self.db = db self.init_ui() apply_dark_titlebar(self) def init_ui(self): """UI 초기화""" self.setWindowTitle("추가근무 수동 사용") self.setModal(True) self.setMinimumWidth(360) layout = QVBoxLayout() layout.setSpacing(8) layout.setContentsMargins(12, 10, 12, 10) # 제목 + 잔액 한 줄 header_layout = QHBoxLayout() title = QLabel("추가근무 수동 사용") title.setObjectName("dialog_subtitle") header_layout.addWidget(title) header_layout.addStretch() balance = self.db.get_total_overtime_balance() hours = balance // 60 minutes = balance % 60 balance_label = QLabel(f"잔액: {hours}시간 {minutes}분") balance_label.setObjectName("badge_balance") header_layout.addWidget(balance_label) layout.addLayout(header_layout) # 날짜 date_layout = QHBoxLayout() date_label = QLabel("날짜:") date_label.setObjectName("field_label") date_label.setFixedWidth(60) self.date_edit = QDateEdit() self.date_edit.setDate(QDate.currentDate()) self.date_edit.setCalendarPopup(True) date_layout.addWidget(date_label) date_layout.addWidget(self.date_edit) layout.addLayout(date_layout) # 시간 (30분 단위) time_layout = QHBoxLayout() time_label = QLabel("시간:") time_label.setObjectName("field_label") time_label.setFixedWidth(60) self.hour_spin = QSpinBox() self.hour_spin.setRange(0, 23) self.hour_spin.setSuffix("시간") self.minute_combo = QComboBox() self.minute_combo.addItems(["0분", "30분"]) time_layout.addWidget(time_label) time_layout.addWidget(self.hour_spin) time_layout.addWidget(self.minute_combo) layout.addLayout(time_layout) # 사유 reason_layout = QHBoxLayout() reason_label = QLabel("사유:") reason_label.setObjectName("field_label") reason_label.setFixedWidth(60) self.reason_edit = QLineEdit() self.reason_edit.setPlaceholderText("예: 개인 사유") reason_layout.addWidget(reason_label) reason_layout.addWidget(self.reason_edit) layout.addLayout(reason_layout) # 버튼 button_layout = QHBoxLayout() save_button = QPushButton("저장") save_button.setObjectName("btn_primary") save_button.clicked.connect(self.save) 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 save(self): """저장""" # 시간 계산 (30분 단위) hours = self.hour_spin.value() minutes = 0 if self.minute_combo.currentText() == "0분" else 30 total_minutes = hours * 60 + minutes if total_minutes == 0: QMessageBox.warning(self, "입력 오류", "0분은 사용할 수 없습니다.") return # 잔액 확인 balance = self.db.get_total_overtime_balance() if total_minutes > balance: QMessageBox.warning( self, "잔액 부족", f"사용 가능한 시간이 부족합니다.\n\n" f"요청: {hours}시간 {minutes}분\n" f"잔액: {balance // 60}시간 {balance % 60}분" ) return date = self.date_edit.date().toString("yyyy-MM-dd") reason = self.reason_edit.text().strip() or "수동 사용" # DB에 저장 self.db.add_overtime_usage(None, total_minutes, date, reason) QMessageBox.information( self, "저장 완료", f"{hours}시간 {minutes}분이 사용 처리되었습니다." ) self.accept() # 테스트 코드 if __name__ == "__main__": from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) dialog = OvertimeView() dialog.exec_()