Phase 1 \u2014 \ubbf8\ub9ac \uc5f0\ucc28 \ub4f1\ub85d
- DB: get_leave_minutes_for(date) / has_full_day_leave(date) /
get_leave_records_by_date(date) / get_leave_records_by_range(start, end)
- TimeCalculator.effective_work_minutes(date_obj, db): \uc5f0\ucc28 \ubd84\ub9cc\ud07c \uc815\uaddc \uadfc\ubb34 \ucc28\uac10
- update_display() 1Hz hot-path:
\u2022 \uc885\uc77c \uc5f0\ucc28 \ub4f1\ub85d\uc77c + \ucd9c\uadfc \uc548 \ud55c \uc0c1\ud0dc \u2192 "\ud83c\udf34 \uc624\ub298\uc740 \ud734\uac00" \uce74\ub4dc \ud45c\uc2dc, \uce74\uc6b4\ud2b8\ub2e4\uc6b4 \uc81c\uac70
\u2022 \uc885\uc77c \uc5f0\ucc28 + \ucd9c\uadfc override \u2192 \ud734\uc77c\ucc98\ub7fc \uc804\uccb4 \uc801\ub9bd
\u2022 \ubd80\ubd84 \uc5f0\ucc28(\ubc18\ucc28/\uc2dc\uac04) \u2192 leave_used_today \uacbd\ub85c\ub85c \uae30\uc874 \ub2e8\ucd95 \uacc4\uc0b0 \uc720\uc9c0
- \uc790\ub3d9 \ucd9c\uadfc\uac10\uc9c0 \uac00\ub4dc: load_today_data\uc5d0\uc11c \uc885\uc77c \uc5f0\ucc28\uc77c\uc774\uba74 event_monitor \ud638\ucd9c \uc790\uccb4 \uc2a4\ud0b5
- \uc218\ub3d9 \ucd9c\uadfc \uac00\ub4dc: manual_clock_in\uc5d0\uc11c \uc885\uc77c \uc5f0\ucc28\uc77c \ud655\uc778 \ud504\ub86c\ud504\ud2b8
- AddLeaveDialog \uac80\uc99d \uac15\ud654:
\u2022 \ubbf8\ub798 1\ub144\uae4c\uc9c0 setMaximumDate
\u2022 \uc8fc\ub9d0/\uacf5\ud734\uc77c \ub4f1\ub85d \ucc28\ub2e8 (\uc774\ubbf8 \ube44\uadfc\ubb34\uc77c)
\u2022 \uac19\uc740 \ub0a0 1\uc77c \ucd08\uacfc \ub204\uc801 \ucc28\ub2e8
- leave_calendar_view: \uc608\uc815(\ud30c\ub791) / \uc0ac\uc6a9\uc644\ub8cc(\ub179/\ub178/\ubcf4) \uc0c9\uc0c1 \ubd84\ub9ac
Phase 2 \u2014 \ud1b5\ud569 \uc2a4\ucf00\uc904 + \ubc18\ubcf5 \uc5f0\ucc28
- recurring_leaves \ud14c\uc774\ube14 (pattern/leave_type/days/start/end/memo)
- core/recurring_leaves.py: weekly / biweekly / monthly \ud328\ud134 \ud30c\uc11c + expand_for_range/date
- get_leave_minutes_for() / has_full_day_leave()\uac00 \ubc18\ubcf5 \ud328\ud134\ub3c4 \ud568\uaed8 \ud569\uc0b0
- ui/recurring_leave_dialog.py: \ub9e4\uc8fc/\uaca9\uc8fc/\ub9e4\uc6d4 \uc785\ub825 + \uc785\ub825 \ub9ac\uc2a4\ud2b8 \uad00\ub9ac
- ui/schedule_view.py: \uc6d4\uac04 \uc2a4\ud50c\ub9ac\ud130 \ub808\uc774\uc544\uc6c3 (\uce98\ub9b0\ub354 + \uc0c1\uc138)
\u2022 \ud734\uc77c(\ube68\uac15) / \uc5f0\ucc28 \uc0ac\uc6a9(\ub179\u30fb\ub178\u30fb\ubcf4) / \uc608\uc815(\ud30c\ub791) / \ubc18\ubcf5(\ud68c\uc0c9) \uc0c9 \ucf54\ub4dc
\u2022 \ub0a0\uc9dc \ud074\ub9ad \u2192 \uc0c1\uc138 \ud328\ub110 (\ub3d9\uc77c\uc77c\uc790 \uad6c\uccb4 \uc5f0\ucc28 + \ubc18\ubcf5 \ub9e4\uce58)
\u2022 \ub9ac\uc2a4\ud2b8 \uc6b0\ud074\ub9ad \uc0ad\uc81c (\uad6c\uccb4 / \ubc18\ubcf5 \uad6c\ubd84)
\u2022 \uc6d4 \ubcc0\uacbd \uc2dc \uc790\ub3d9 reload
- \uc9c4\uc785\uc810: main_window.show_schedule(), tray menu '\ud83d\uddd3\ufe0f \uc2a4\ucf00\uc904', LeaveView '\ud83d\uddd3\ufe0f \uc2a4\ucf00\uc904' \ubc84\ud2bc
Tests
- tests/test_recurring_leaves.py 32\uac1c (\ud328\ud134 \ud30c\uc2f1 / \ub9e4\uce6d / expand / describe)
- tests/test_database.py +12 (TestLeaveQueriesByDate + TestRecurringLeavesDB)
- _integration_test.py +4 (S52B-S52E)
- pytest: 122 \u2192 175 \uc804\ubd80 green
- \ud1b5\ud569: 49 \u2192 53 \uc804\ubd80 green
- UI-5/UI-7 \uae30\uc874 \uace0\uc7a5 (v2.8.0 \ub514\uc790\uc778 \ub9ac\ub274\uc5bc \ub9c8\ub108)
200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
"""
|
||
반복 연차 등록/관리 다이얼로그.
|
||
|
||
지원: 매주/격주 요일, 매월 N일.
|
||
"""
|
||
from __future__ import annotations
|
||
from datetime import date
|
||
|
||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||
QPushButton, QComboBox, QDateEdit, QSpinBox,
|
||
QDoubleSpinBox, QLineEdit, QGroupBox,
|
||
QListWidget, QListWidgetItem, QMessageBox,
|
||
QCheckBox, QButtonGroup, QRadioButton)
|
||
from PyQt5.QtCore import QDate, Qt
|
||
|
||
from core.recurring_leaves import describe_pattern
|
||
from ui.styles import apply_dark_titlebar
|
||
|
||
|
||
_KO_WEEKDAYS = [('월', 'mon'), ('화', 'tue'), ('수', 'wed'),
|
||
('목', 'thu'), ('금', 'fri'), ('토', 'sat'), ('일', 'sun')]
|
||
|
||
|
||
class RecurringLeaveDialog(QDialog):
|
||
"""반복 연차 패턴 추가/삭제."""
|
||
|
||
def __init__(self, parent=None, db=None):
|
||
super().__init__(parent)
|
||
self.db = db
|
||
self.setWindowTitle("🔁 반복 연차 관리")
|
||
self.setMinimumSize(540, 480)
|
||
self._build_ui()
|
||
self._reload_list()
|
||
apply_dark_titlebar(self)
|
||
|
||
def _build_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 기존 패턴 목록
|
||
list_group = QGroupBox("등록된 반복 패턴")
|
||
lg = QVBoxLayout()
|
||
self.list_widget = QListWidget()
|
||
self.list_widget.setMinimumHeight(160)
|
||
lg.addWidget(self.list_widget)
|
||
del_btn = QPushButton("선택 삭제")
|
||
del_btn.clicked.connect(self._delete_selected)
|
||
lg.addWidget(del_btn)
|
||
list_group.setLayout(lg)
|
||
layout.addWidget(list_group)
|
||
|
||
# 신규 등록
|
||
add_group = QGroupBox("신규 패턴 추가")
|
||
ag = QVBoxLayout()
|
||
|
||
# 패턴 종류
|
||
kind_row = QHBoxLayout()
|
||
kind_row.addWidget(QLabel("주기:"))
|
||
self.kind_group = QButtonGroup(self)
|
||
self.rb_weekly = QRadioButton("매주")
|
||
self.rb_weekly.setChecked(True)
|
||
self.rb_biweekly = QRadioButton("격주")
|
||
self.rb_monthly = QRadioButton("매월 N일")
|
||
for rb in (self.rb_weekly, self.rb_biweekly, self.rb_monthly):
|
||
self.kind_group.addButton(rb)
|
||
kind_row.addWidget(rb)
|
||
kind_row.addStretch()
|
||
ag.addLayout(kind_row)
|
||
|
||
# 요일 체크박스 (weekly/biweekly)
|
||
wd_row = QHBoxLayout()
|
||
wd_row.addWidget(QLabel("요일:"))
|
||
self.weekday_checks = []
|
||
for ko, en in _KO_WEEKDAYS:
|
||
cb = QCheckBox(ko)
|
||
self.weekday_checks.append((cb, en))
|
||
wd_row.addWidget(cb)
|
||
wd_row.addStretch()
|
||
ag.addLayout(wd_row)
|
||
|
||
# 매월 N일
|
||
month_row = QHBoxLayout()
|
||
month_row.addWidget(QLabel("매월:"))
|
||
self.day_of_month = QSpinBox()
|
||
self.day_of_month.setRange(1, 31)
|
||
self.day_of_month.setValue(15)
|
||
self.day_of_month.setSuffix("일")
|
||
month_row.addWidget(self.day_of_month)
|
||
month_row.addStretch()
|
||
ag.addLayout(month_row)
|
||
|
||
# 차감 일수
|
||
days_row = QHBoxLayout()
|
||
days_row.addWidget(QLabel("차감:"))
|
||
self.days_combo = QComboBox()
|
||
self.days_combo.addItem("1.0일 (종일)", 1.0)
|
||
self.days_combo.addItem("0.5일 (반차)", 0.5)
|
||
self.days_combo.addItem("0.25일 (반반차)", 0.25)
|
||
days_row.addWidget(self.days_combo)
|
||
days_row.addStretch()
|
||
ag.addLayout(days_row)
|
||
|
||
# 시작/종료 날짜
|
||
date_row = QHBoxLayout()
|
||
date_row.addWidget(QLabel("시작:"))
|
||
self.start_edit = QDateEdit()
|
||
self.start_edit.setDate(QDate.currentDate())
|
||
self.start_edit.setCalendarPopup(True)
|
||
date_row.addWidget(self.start_edit)
|
||
|
||
date_row.addWidget(QLabel("종료:"))
|
||
self.end_edit = QDateEdit()
|
||
self.end_edit.setDate(QDate.currentDate().addMonths(6))
|
||
self.end_edit.setCalendarPopup(True)
|
||
date_row.addWidget(self.end_edit)
|
||
self.no_end_check = QCheckBox("종료 없음 (무기한)")
|
||
self.no_end_check.toggled.connect(
|
||
lambda v: self.end_edit.setEnabled(not v)
|
||
)
|
||
date_row.addWidget(self.no_end_check)
|
||
date_row.addStretch()
|
||
ag.addLayout(date_row)
|
||
|
||
# 메모
|
||
memo_row = QHBoxLayout()
|
||
memo_row.addWidget(QLabel("메모:"))
|
||
self.memo_edit = QLineEdit()
|
||
self.memo_edit.setPlaceholderText("예: 육아 단축근무")
|
||
memo_row.addWidget(self.memo_edit)
|
||
ag.addLayout(memo_row)
|
||
|
||
# 추가 버튼
|
||
add_btn = QPushButton("➕ 추가")
|
||
add_btn.setObjectName("btn_primary")
|
||
add_btn.clicked.connect(self._save)
|
||
ag.addWidget(add_btn)
|
||
|
||
add_group.setLayout(ag)
|
||
layout.addWidget(add_group)
|
||
|
||
# 닫기
|
||
close_btn = QPushButton("닫기")
|
||
close_btn.clicked.connect(self.close)
|
||
layout.addWidget(close_btn)
|
||
|
||
self.setLayout(layout)
|
||
|
||
def _reload_list(self):
|
||
self.list_widget.clear()
|
||
for r in self.db.get_recurring_leaves():
|
||
desc = describe_pattern(r['pattern'])
|
||
end = r.get('end_date') or '무기한'
|
||
text = (f"[{r['id']}] {desc} · {r['days']}일 ({r['leave_type']}) "
|
||
f"· {r['start_date']} ~ {end}")
|
||
if r.get('memo'):
|
||
text += f" — {r['memo']}"
|
||
item = QListWidgetItem(text)
|
||
item.setData(Qt.UserRole, r['id'])
|
||
self.list_widget.addItem(item)
|
||
|
||
def _delete_selected(self):
|
||
item = self.list_widget.currentItem()
|
||
if not item:
|
||
return
|
||
rec_id = item.data(Qt.UserRole)
|
||
reply = QMessageBox.question(
|
||
self, "삭제 확인",
|
||
f"이 반복 패턴을 삭제하시겠습니까?\n\n{item.text()}",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
)
|
||
if reply == QMessageBox.Yes:
|
||
self.db.delete_recurring_leave(rec_id)
|
||
self._reload_list()
|
||
|
||
def _build_pattern(self) -> str | None:
|
||
if self.rb_monthly.isChecked():
|
||
return f"monthly:{self.day_of_month.value()}"
|
||
# weekly/biweekly
|
||
chosen = [en for cb, en in self.weekday_checks if cb.isChecked()]
|
||
if not chosen:
|
||
return None
|
||
prefix = 'weekly' if self.rb_weekly.isChecked() else 'biweekly'
|
||
return f"{prefix}:" + ",".join(chosen)
|
||
|
||
def _save(self):
|
||
pattern = self._build_pattern()
|
||
if not pattern:
|
||
QMessageBox.warning(self, "입력 오류", "최소 한 개 요일을 선택하세요.")
|
||
return
|
||
days = self.days_combo.currentData()
|
||
leave_type = self.days_combo.currentText().split(' ')[1].strip('()')
|
||
start = self.start_edit.date().toString('yyyy-MM-dd')
|
||
end = None if self.no_end_check.isChecked() else self.end_edit.date().toString('yyyy-MM-dd')
|
||
memo = self.memo_edit.text().strip()
|
||
|
||
self.db.add_recurring_leave(pattern, leave_type, days, start, end, memo)
|
||
QMessageBox.information(self, "추가 완료",
|
||
f"반복 패턴이 등록되었습니다.\n{describe_pattern(pattern)}")
|
||
self.memo_edit.clear()
|
||
self._reload_list()
|