Some checks failed
CI / test (push) Has been cancelled
핵심 기능: - 단축근무·표준·반일 등 다양한 근무 패턴 (5개 프리셋 + 사용자 정의) - Windows 이벤트 뷰어 자동 출퇴근 감지 - 30분 단위 연장근무 적립/사용 시스템 - 1.0/0.5/0.25일 연차·반차·반반차 - 자동 점심·저녁·외출·자동 백업·화면 잠금 자동 외출 - 한국 공휴일 자동 등록 (음력 포함, holidays 패키지) - matplotlib 차트 기반 주간/월간/패턴 통계 - 미니 위젯 + 시스템 트레이 통합 - 한국어/English i18n - 자가 업데이트 (updater.exe + Gitea Releases) 아키텍처: - core/ (db, time_calculator, notifier, i18n, version, settings_keys) - ui/ (main_window + 9 dialogs + 3 controllers) - utils/ (backup, lock_detector, debug_log, updater_client, time_format) - tests/ (66 pytest 단위) + 통합/i18n GUI 검증 CI/CD: - .gitea/workflows/ci.yml: push 시 pytest + 통합 테스트 - .gitea/workflows/release.yml: v* 태그 push 시 두 .exe 자동 빌드 + Releases 첨부 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
294 lines
10 KiB
Python
294 lines
10 KiB
Python
"""
|
|
외출 관리 화면
|
|
"""
|
|
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
|
QPushButton, QTableWidget, QTableWidgetItem,
|
|
QHeaderView, QMessageBox, QTimeEdit, QLineEdit, QWidget)
|
|
from PyQt5.QtCore import Qt, QTime
|
|
from datetime import datetime
|
|
from core.i18n import tr
|
|
from ui.styles import apply_dark_titlebar
|
|
|
|
|
|
class BreakEditDialog(QDialog):
|
|
"""외출 기록 수정 다이얼로그"""
|
|
|
|
def __init__(self, parent=None, break_record=None):
|
|
super().__init__(parent)
|
|
self.break_record = break_record
|
|
self.init_ui()
|
|
apply_dark_titlebar(self)
|
|
|
|
def init_ui(self):
|
|
"""UI 초기화"""
|
|
self.setWindowTitle("외출 기록 수정")
|
|
self.setFixedSize(380, 180)
|
|
|
|
layout = QVBoxLayout()
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(12, 10, 12, 10)
|
|
|
|
# 외출 시간
|
|
out_layout = QHBoxLayout()
|
|
out_label = QLabel("외출 시간:")
|
|
out_label.setFixedWidth(80)
|
|
self.out_time_edit = QTimeEdit()
|
|
self.out_time_edit.setDisplayFormat("HH:mm:ss")
|
|
out_layout.addWidget(out_label)
|
|
out_layout.addWidget(self.out_time_edit)
|
|
layout.addLayout(out_layout)
|
|
|
|
# 복귀 시간
|
|
in_layout = QHBoxLayout()
|
|
in_label = QLabel("복귀 시간:")
|
|
in_label.setFixedWidth(80)
|
|
self.in_time_edit = QTimeEdit()
|
|
self.in_time_edit.setDisplayFormat("HH:mm:ss")
|
|
in_layout.addWidget(in_label)
|
|
in_layout.addWidget(self.in_time_edit)
|
|
layout.addLayout(in_layout)
|
|
|
|
# 사유
|
|
reason_layout = QHBoxLayout()
|
|
reason_label = QLabel("사유:")
|
|
reason_label.setFixedWidth(80)
|
|
self.reason_edit = QLineEdit()
|
|
reason_layout.addWidget(reason_label)
|
|
reason_layout.addWidget(self.reason_edit)
|
|
layout.addLayout(reason_layout)
|
|
|
|
# 기존 데이터 로드
|
|
if self.break_record:
|
|
break_out = self.break_record.get('break_out', '00:00:00')
|
|
h, m, s = map(int, break_out.split(':'))
|
|
self.out_time_edit.setTime(QTime(h, m, s))
|
|
|
|
break_in = self.break_record.get('break_in')
|
|
if break_in:
|
|
h, m, s = map(int, break_in.split(':'))
|
|
self.in_time_edit.setTime(QTime(h, m, s))
|
|
|
|
reason = self.break_record.get('reason', '')
|
|
if reason:
|
|
self.reason_edit.setText(reason)
|
|
|
|
# 버튼
|
|
button_layout = QHBoxLayout()
|
|
save_button = QPushButton("저장")
|
|
cancel_button = QPushButton("취소")
|
|
|
|
save_button.clicked.connect(self.accept)
|
|
cancel_button.clicked.connect(self.reject)
|
|
|
|
button_layout.addWidget(save_button)
|
|
button_layout.addWidget(cancel_button)
|
|
layout.addLayout(button_layout)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def get_data(self):
|
|
"""입력된 데이터 반환"""
|
|
out_time = self.out_time_edit.time()
|
|
in_time = self.in_time_edit.time()
|
|
reason = self.reason_edit.text()
|
|
|
|
# 복귀 시간 처리: 기존 기록에 복귀 시간이 없으면 None 유지
|
|
# 자정(00:00:00)도 유효한 시간으로 처리
|
|
break_in_str = in_time.toString("HH:mm:ss")
|
|
if self.break_record and not self.break_record.get('break_in'):
|
|
# 기존에 복귀 시간이 없었고, 수정에서도 00:00:00이면 아직 복귀 안 한 것으로 간주
|
|
if break_in_str == "00:00:00":
|
|
break_in_str = None
|
|
|
|
return {
|
|
'break_out': out_time.toString("HH:mm:ss"),
|
|
'break_in': break_in_str,
|
|
'reason': reason
|
|
}
|
|
|
|
|
|
class BreakView(QDialog):
|
|
"""외출 관리 창"""
|
|
|
|
def __init__(self, parent=None, db=None):
|
|
super().__init__(parent)
|
|
self.db = db
|
|
self.parent_window = parent
|
|
self.init_ui()
|
|
self.load_break_records()
|
|
apply_dark_titlebar(self)
|
|
|
|
def init_ui(self):
|
|
"""UI 초기화"""
|
|
self.setWindowTitle(tr('window.break_view'))
|
|
self.setGeometry(200, 200, 550, 350)
|
|
|
|
layout = QVBoxLayout()
|
|
layout.setSpacing(6)
|
|
layout.setContentsMargins(12, 10, 12, 10)
|
|
|
|
# 제목
|
|
title = QLabel("오늘의 외출 기록")
|
|
title.setObjectName("dialog_subtitle")
|
|
title.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(title)
|
|
|
|
# 외출 리스트 테이블
|
|
self.table = QTableWidget()
|
|
self.table.setColumnCount(5)
|
|
self.table.setHorizontalHeaderLabels(["외출 시간", "복귀 시간", "소요 시간", "사유", ""])
|
|
|
|
# 테이블 설정
|
|
header = self.table.horizontalHeader()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
|
header.setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
|
|
|
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
|
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
|
|
|
|
layout.addWidget(self.table)
|
|
|
|
# 총 외출 시간 표시
|
|
self.total_label = QLabel("총 외출 시간: 0분")
|
|
self.total_label.setObjectName("section_title")
|
|
self.total_label.setAlignment(Qt.AlignRight)
|
|
layout.addWidget(self.total_label)
|
|
|
|
# 버튼
|
|
button_layout = QHBoxLayout()
|
|
|
|
self.refresh_button = QPushButton("새로고침")
|
|
close_button = QPushButton("닫기")
|
|
|
|
self.refresh_button.clicked.connect(self.load_break_records)
|
|
close_button.clicked.connect(self.accept)
|
|
|
|
button_layout.addStretch()
|
|
button_layout.addWidget(self.refresh_button)
|
|
button_layout.addWidget(close_button)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def load_break_records(self):
|
|
"""외출 기록 로드"""
|
|
records = self.db.get_today_break_records()
|
|
|
|
self.table.setRowCount(len(records))
|
|
|
|
for i, record in enumerate(records):
|
|
# 외출 시간
|
|
break_out = record.get('break_out', '')
|
|
self.table.setItem(i, 0, QTableWidgetItem(break_out))
|
|
|
|
# 복귀 시간
|
|
break_in = record.get('break_in', '')
|
|
if break_in:
|
|
self.table.setItem(i, 1, QTableWidgetItem(break_in))
|
|
else:
|
|
item = QTableWidgetItem("진행중")
|
|
item.setForeground(Qt.red)
|
|
self.table.setItem(i, 1, item)
|
|
|
|
# 소요 시간
|
|
total_minutes = record.get('total_minutes')
|
|
if total_minutes:
|
|
hours = total_minutes // 60
|
|
minutes = total_minutes % 60
|
|
duration_str = f"{hours}시간 {minutes}분" if hours > 0 else f"{minutes}분"
|
|
self.table.setItem(i, 2, QTableWidgetItem(duration_str))
|
|
else:
|
|
self.table.setItem(i, 2, QTableWidgetItem("-"))
|
|
|
|
# 사유
|
|
reason = record.get('reason', '')
|
|
self.table.setItem(i, 3, QTableWidgetItem(reason))
|
|
|
|
# 액션 버튼
|
|
action_widget = QWidget()
|
|
action_layout = QHBoxLayout()
|
|
action_layout.setContentsMargins(0, 0, 0, 0)
|
|
action_layout.setSpacing(5)
|
|
|
|
edit_button = QPushButton("수정")
|
|
delete_button = QPushButton("삭제")
|
|
|
|
edit_button.setFixedSize(50, 25)
|
|
delete_button.setFixedSize(50, 25)
|
|
|
|
# 클릭 이벤트에 record id 전달
|
|
record_id = record['id']
|
|
edit_button.clicked.connect(lambda checked, rid=record_id: self.edit_record(rid))
|
|
delete_button.clicked.connect(lambda checked, rid=record_id: self.delete_record(rid))
|
|
|
|
action_layout.addWidget(edit_button)
|
|
action_layout.addWidget(delete_button)
|
|
action_widget.setLayout(action_layout)
|
|
|
|
self.table.setCellWidget(i, 4, action_widget)
|
|
|
|
# 총 외출 시간 업데이트
|
|
total_minutes = self.db.get_total_break_minutes_today()
|
|
hours = total_minutes // 60
|
|
minutes = total_minutes % 60
|
|
|
|
if hours > 0:
|
|
self.total_label.setText(f"총 외출 시간: {hours}시간 {minutes}분")
|
|
else:
|
|
self.total_label.setText(f"총 외출 시간: {minutes}분")
|
|
|
|
def edit_record(self, record_id):
|
|
"""외출 기록 수정"""
|
|
# 기존 기록 조회
|
|
records = self.db.get_today_break_records()
|
|
record = next((r for r in records if r['id'] == record_id), None)
|
|
|
|
if not record:
|
|
return
|
|
|
|
# 수정 다이얼로그 표시
|
|
dialog = BreakEditDialog(self, record)
|
|
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
data = dialog.get_data()
|
|
|
|
# DB 업데이트
|
|
self.db.update_break_record(
|
|
record_id,
|
|
data['break_out'],
|
|
data['break_in'],
|
|
data['reason']
|
|
)
|
|
|
|
# 리스트 새로고침
|
|
self.load_break_records()
|
|
|
|
# 부모 창 업데이트
|
|
if self.parent_window and hasattr(self.parent_window, 'update_break_status'):
|
|
self.parent_window.update_break_status()
|
|
if self.parent_window and hasattr(self.parent_window, 'update_times'):
|
|
self.parent_window.update_times()
|
|
|
|
def delete_record(self, record_id):
|
|
"""외출 기록 삭제"""
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"삭제 확인",
|
|
"이 외출 기록을 삭제하시겠습니까?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self.db.delete_break_record(record_id)
|
|
self.load_break_records()
|
|
|
|
# 부모 창 업데이트
|
|
if self.parent_window and hasattr(self.parent_window, 'update_break_status'):
|
|
self.parent_window.update_break_status()
|
|
if self.parent_window and hasattr(self.parent_window, 'update_times'):
|
|
self.parent_window.update_times()
|