KINDNICK 606da976a0 v2.5.0: Phase 3 — weekly report, chart hover, clock-in distribution, leave calendar
- Weekly auto report on Monday (system alert + Discord push, dedupe)
- Matplotlib chart hover annotation for daily hours
- Clock-in time distribution histogram (30-min bins) with avg line
- Leave usage calendar with color-coded full/half/quarter days

Fixed: leave_view setLayout indentation regression

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:51:47 +09:00

382 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
연차 상세 내역 뷰
"""
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem,
QHeaderView, QGroupBox, QMessageBox, QInputDialog,
QLineEdit, QDateEdit, QComboBox, QDoubleSpinBox,
QMenu, QAction)
from PyQt5.QtCore import Qt, QDate
from PyQt5.QtGui import QColor
from datetime import datetime
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 apply_dark_titlebar
class LeaveView(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.leave_view'))
self.setModal(True)
self.setMinimumSize(700, 450)
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_leave")
header_layout.addWidget(self.balance_label)
set_balance_button = QPushButton("잔여 설정")
set_balance_button.clicked.connect(self.set_balance)
header_layout.addWidget(set_balance_button)
layout.addLayout(header_layout)
# 사용 내역
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(4)
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.ResizeToContents)
self.used_table.horizontalHeader().setSectionResizeMode(3, 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_context_menu)
used_layout.addWidget(self.used_table)
used_group.setLayout(used_layout)
layout.addWidget(used_group)
# 버튼들
button_layout = QHBoxLayout()
add_leave_button = QPushButton(" 연차 사용 추가")
add_leave_button.clicked.connect(self.add_leave_record)
button_layout.addWidget(add_leave_button)
cal_button = QPushButton("📅 캘린더 보기")
cal_button.clicked.connect(self._show_calendar)
button_layout.addWidget(cal_button)
close_button = QPushButton("닫기")
close_button.clicked.connect(self.close)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def _show_calendar(self):
from ui.leave_calendar_view import LeaveCalendarView
dlg = LeaveCalendarView(self, self.db)
dlg.exec_()
def load_data(self):
"""데이터 로드"""
# 잔액 업데이트
balance = self.db.get_leave_balance()
hours = balance * 8
self.balance_label.setText(f"잔여: {balance}일 (총 {hours}시간)")
# 사용 내역 로드 (잔액 조정 제외)
records = self.db.get_leave_records(exclude_bulk=True)
self.used_table.setRowCount(len(records))
for i, record in enumerate(records):
date_item = QTableWidgetItem(record['date'])
date_item.setTextAlignment(Qt.AlignCenter)
date_item.setData(Qt.UserRole, record['id'])
type_item = QTableWidgetItem(record['leave_type'])
type_item.setTextAlignment(Qt.AlignCenter)
days = record['days']
hours = days * 8
if days == 1.0:
days_str = "1일"
elif days == 0.5:
days_str = "0.5일 (4시간)"
elif hours < 8:
days_str = f"{days}일 ({hours}시간)"
else:
days_str = f"{days}"
days_item = QTableWidgetItem(days_str)
days_item.setTextAlignment(Qt.AlignCenter)
days_item.setForeground(QColor(231, 76, 60)) # 빨간색
memo_item = QTableWidgetItem(record['memo'] or "")
self.used_table.setItem(i, 0, date_item)
self.used_table.setItem(i, 1, type_item)
self.used_table.setItem(i, 2, days_item)
self.used_table.setItem(i, 3, memo_item)
def show_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_leave_record)
menu.addAction(delete_action)
menu.exec_(self.used_table.viewport().mapToGlobal(position))
def delete_leave_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)
type_item = self.used_table.item(row, 1)
days_item = self.used_table.item(row, 2)
leave_id = date_item.data(Qt.UserRole)
reply = QMessageBox.question(
self,
"삭제 확인",
f"다음 연차 사용 기록을 삭제하시겠습니까?\n\n"
f"날짜: {date_item.text()}\n"
f"구분: {type_item.text()}\n"
f"사용: {days_item.text()}",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.db.delete_leave_record(leave_id)
self.load_data()
def set_balance(self):
"""연차 개수 설정 (시간 단위)"""
current_balance = self.db.get_leave_balance()
current_hours = current_balance * 8
hours, ok = QInputDialog.getDouble(
self,
"연차 시간 설정",
"연차 잔여 시간을 입력하세요 (0.5시간 단위):\n"
"예) 8시간 = 1일, 4시간 = 0.5일(반차), 2시간 = 0.25일, 0.5시간 = 30분",
current_hours,
0.0,
999.0,
1 # 소수점 첫째자리까지 (0.5 단위)
)
if ok:
# 0.5시간 단위로 반올림
hours = round(hours * 2) / 2
# 시간을 일수로 변환
days = hours / 8.0
self.db.set_leave_balance(days)
QMessageBox.information(
self,
"설정 완료",
f"연차 잔여 개수가 {days}일 ({hours}시간)로 설정되었습니다."
)
self.load_data()
def add_leave_record(self):
"""연차 사용 기록 추가 다이얼로그"""
dialog = AddLeaveDialog(self, self.db)
if dialog.exec_() == QDialog.Accepted:
self.load_data()
class AddLeaveDialog(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)
# 날짜 + 구분 한 줄
row1 = QHBoxLayout()
date_label = QLabel("날짜:")
date_label.setObjectName("field_label")
date_label.setFixedWidth(40)
self.date_edit = QDateEdit()
self.date_edit.setDate(QDate.currentDate())
self.date_edit.setCalendarPopup(True)
row1.addWidget(date_label)
row1.addWidget(self.date_edit)
row1.addSpacing(8)
type_label = QLabel("구분:")
type_label.setObjectName("field_label")
type_label.setFixedWidth(40)
self.type_combo = QComboBox()
self.type_combo.addItem("연차", "annual")
self.type_combo.addItem("반차", "half")
self.type_combo.addItem("반반차", "quarter")
self.type_combo.addItem("시간", "hourly")
self.type_combo.currentIndexChanged.connect(self.on_type_changed)
row1.addWidget(type_label)
row1.addWidget(self.type_combo)
layout.addLayout(row1)
# 사용 시간 (시간 연차용)
hours_layout = QHBoxLayout()
hours_label = QLabel("시간:")
hours_label.setObjectName("field_label")
hours_label.setFixedWidth(40)
self.hours_spin = QDoubleSpinBox()
self.hours_spin.setRange(0.5, 8.0)
self.hours_spin.setSingleStep(0.5)
self.hours_spin.setValue(1.0)
self.hours_spin.setSuffix(" 시간")
self.hours_spin.setEnabled(False)
hours_layout.addWidget(hours_label)
hours_layout.addWidget(self.hours_spin)
layout.addLayout(hours_layout)
# 사유
memo_layout = QHBoxLayout()
memo_label = QLabel("사유:")
memo_label.setObjectName("field_label")
memo_label.setFixedWidth(40)
self.memo_input = QLineEdit()
self.memo_input.setPlaceholderText("예) 개인 사유, 병원 방문 등")
memo_layout.addWidget(memo_label)
memo_layout.addWidget(self.memo_input)
layout.addLayout(memo_layout)
# 안내
info_label = QLabel("※ 잔여 연차가 자동 차감됩니다.")
info_label.setObjectName("note_text")
layout.addWidget(info_label)
# 버튼
button_layout = QHBoxLayout()
save_button = QPushButton("저장")
save_button.setObjectName("btn_primary")
save_button.clicked.connect(self.save_record)
button_layout.addWidget(save_button)
cancel_button = QPushButton("취소")
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def on_type_changed(self, index):
"""연차 유형 변경 시"""
leave_type = self.type_combo.currentData()
# 시간 연차일 때만 시간 입력 활성화
self.hours_spin.setEnabled(leave_type == "hourly")
def save_record(self):
"""기록 저장"""
date = self.date_edit.date().toString("yyyy-MM-dd")
leave_type = self.type_combo.currentData()
leave_type_name = self.type_combo.currentText()
memo = self.memo_input.text().strip()
# 사용 일수 계산
if leave_type == "annual":
days = 1.0
elif leave_type == "half":
days = 0.5
elif leave_type == "quarter":
days = 0.25
elif leave_type == "hourly":
hours = self.hours_spin.value()
days = hours / 8.0
else:
days = 1.0
# 잔여 연차 확인
current_balance = self.db.get_leave_balance()
if current_balance < days:
QMessageBox.warning(
self,
"잔여 연차 부족",
f"잔여 연차가 부족합니다.\n현재 잔여: {current_balance}\n사용 요청: {days}"
)
return
# 확인 메시지
hours = days * 8
reply = QMessageBox.question(
self,
"연차 사용 기록 추가",
f"날짜: {date}\n"
f"구분: {leave_type_name}\n"
f"사용: {days}일 ({hours}시간)\n"
f"사유: {memo if memo else '(없음)'}\n\n"
f"이 기록을 추가하시겠습니까?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
try:
# 연차 사용 기록 추가 (파라미터 순서: days, date, leave_type, memo)
self.db.use_leave(days, date, leave_type_name, memo)
QMessageBox.information(
self,
"추가 완료",
f"{days}일 ({hours}시간)의 연차 사용이 기록되었습니다."
)
self.accept()
except Exception as e:
QMessageBox.critical(
self,
"오류",
f"연차 기록 추가 중 오류가 발생했습니다:\n{str(e)}"
)
# 테스트 코드
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
dialog = LeaveView()
dialog.exec_()