KINDNICK ff71886fd7 v2.7.0: i18n 100% + 런타임 retranslate + 테스트 +47 + 폴리싱
- 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+ 아키텍처 반영
2026-04-30 19:30:47 +09:00

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()