- i18n 사전 100% (break/overtime/leave/clockin) — 50+ 신규 키 - 런타임 재번역 인프라 (ui/i18n_runtime.py) — 재시작 없이 메인 UI 적용 - MealController 분리 — 점심/저녁 토글을 컨트롤러로 추출 - 통합 테스트 +15 (S36-S52: 온보딩/salary/CSV/notification dedupe 등) - pytest 신규 4종 + i18n_runtime 테스트 (총 122 케이스, 90→122) - README/INSTALL/CLAUDE/AGENTS v2.6+ 아키텍처 반영
303 lines
10 KiB
Python
303 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(tr('dlg.break.edit_title'))
|
|
self.setFixedSize(380, 180)
|
|
|
|
layout = QVBoxLayout()
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(12, 10, 12, 10)
|
|
|
|
# 외출 시간
|
|
out_layout = QHBoxLayout()
|
|
out_label = QLabel(tr('dlg.break.out_label'))
|
|
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(tr('dlg.break.in_label'))
|
|
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(tr('dlg.break.reason_label'))
|
|
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(tr('btn.save'))
|
|
cancel_button = QPushButton(tr('btn.cancel'))
|
|
|
|
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(tr('view.break.today_title'))
|
|
title.setObjectName("dialog_subtitle")
|
|
title.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(title)
|
|
|
|
# 외출 리스트 테이블
|
|
self.table = QTableWidget()
|
|
self.table.setColumnCount(5)
|
|
self.table.setHorizontalHeaderLabels([
|
|
tr('view.break.col_out'),
|
|
tr('view.break.col_in'),
|
|
tr('view.break.col_duration'),
|
|
tr('view.break.col_reason'),
|
|
"",
|
|
])
|
|
|
|
# 테이블 설정
|
|
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(tr('view.break.total_zero'))
|
|
self.total_label.setObjectName("section_title")
|
|
self.total_label.setAlignment(Qt.AlignRight)
|
|
layout.addWidget(self.total_label)
|
|
|
|
# 버튼
|
|
button_layout = QHBoxLayout()
|
|
|
|
self.refresh_button = QPushButton(tr('btn.refresh'))
|
|
close_button = QPushButton(tr('btn.close'))
|
|
|
|
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(tr('view.break.in_progress'))
|
|
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
|
|
if hours > 0:
|
|
duration_str = tr('view.break.duration_fmt', h=hours, m=minutes)
|
|
else:
|
|
duration_str = tr('view.break.duration_min_only', m=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(tr('btn.edit_short'))
|
|
delete_button = QPushButton(tr('btn.delete_short'))
|
|
|
|
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(tr('view.break.total_fmt', h=hours, m=minutes))
|
|
else:
|
|
self.total_label.setText(tr('view.break.total_min_only', m=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,
|
|
tr('msg.confirm_delete.title'),
|
|
tr('view.break.delete_confirm'),
|
|
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()
|