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+ 아키텍처 반영
This commit is contained in:
+28
-19
@@ -21,7 +21,7 @@ class BreakEditDialog(QDialog):
|
||||
|
||||
def init_ui(self):
|
||||
"""UI 초기화"""
|
||||
self.setWindowTitle("외출 기록 수정")
|
||||
self.setWindowTitle(tr('dlg.break.edit_title'))
|
||||
self.setFixedSize(380, 180)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
@@ -30,7 +30,7 @@ class BreakEditDialog(QDialog):
|
||||
|
||||
# 외출 시간
|
||||
out_layout = QHBoxLayout()
|
||||
out_label = QLabel("외출 시간:")
|
||||
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")
|
||||
@@ -40,7 +40,7 @@ class BreakEditDialog(QDialog):
|
||||
|
||||
# 복귀 시간
|
||||
in_layout = QHBoxLayout()
|
||||
in_label = QLabel("복귀 시간:")
|
||||
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")
|
||||
@@ -50,7 +50,7 @@ class BreakEditDialog(QDialog):
|
||||
|
||||
# 사유
|
||||
reason_layout = QHBoxLayout()
|
||||
reason_label = QLabel("사유:")
|
||||
reason_label = QLabel(tr('dlg.break.reason_label'))
|
||||
reason_label.setFixedWidth(80)
|
||||
self.reason_edit = QLineEdit()
|
||||
reason_layout.addWidget(reason_label)
|
||||
@@ -74,8 +74,8 @@ class BreakEditDialog(QDialog):
|
||||
|
||||
# 버튼
|
||||
button_layout = QHBoxLayout()
|
||||
save_button = QPushButton("저장")
|
||||
cancel_button = QPushButton("취소")
|
||||
save_button = QPushButton(tr('btn.save'))
|
||||
cancel_button = QPushButton(tr('btn.cancel'))
|
||||
|
||||
save_button.clicked.connect(self.accept)
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
@@ -128,7 +128,7 @@ class BreakView(QDialog):
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
|
||||
# 제목
|
||||
title = QLabel("오늘의 외출 기록")
|
||||
title = QLabel(tr('view.break.today_title'))
|
||||
title.setObjectName("dialog_subtitle")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
@@ -136,7 +136,13 @@ class BreakView(QDialog):
|
||||
# 외출 리스트 테이블
|
||||
self.table = QTableWidget()
|
||||
self.table.setColumnCount(5)
|
||||
self.table.setHorizontalHeaderLabels(["외출 시간", "복귀 시간", "소요 시간", "사유", ""])
|
||||
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()
|
||||
@@ -152,7 +158,7 @@ class BreakView(QDialog):
|
||||
layout.addWidget(self.table)
|
||||
|
||||
# 총 외출 시간 표시
|
||||
self.total_label = QLabel("총 외출 시간: 0분")
|
||||
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)
|
||||
@@ -160,8 +166,8 @@ class BreakView(QDialog):
|
||||
# 버튼
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.refresh_button = QPushButton("새로고침")
|
||||
close_button = QPushButton("닫기")
|
||||
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)
|
||||
@@ -190,7 +196,7 @@ class BreakView(QDialog):
|
||||
if break_in:
|
||||
self.table.setItem(i, 1, QTableWidgetItem(break_in))
|
||||
else:
|
||||
item = QTableWidgetItem("진행중")
|
||||
item = QTableWidgetItem(tr('view.break.in_progress'))
|
||||
item.setForeground(Qt.red)
|
||||
self.table.setItem(i, 1, item)
|
||||
|
||||
@@ -199,7 +205,10 @@ class BreakView(QDialog):
|
||||
if total_minutes:
|
||||
hours = total_minutes // 60
|
||||
minutes = total_minutes % 60
|
||||
duration_str = f"{hours}시간 {minutes}분" if hours > 0 else f"{minutes}분"
|
||||
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("-"))
|
||||
@@ -214,8 +223,8 @@ class BreakView(QDialog):
|
||||
action_layout.setContentsMargins(0, 0, 0, 0)
|
||||
action_layout.setSpacing(5)
|
||||
|
||||
edit_button = QPushButton("수정")
|
||||
delete_button = QPushButton("삭제")
|
||||
edit_button = QPushButton(tr('btn.edit_short'))
|
||||
delete_button = QPushButton(tr('btn.delete_short'))
|
||||
|
||||
edit_button.setFixedSize(50, 25)
|
||||
delete_button.setFixedSize(50, 25)
|
||||
@@ -237,9 +246,9 @@ class BreakView(QDialog):
|
||||
minutes = total_minutes % 60
|
||||
|
||||
if hours > 0:
|
||||
self.total_label.setText(f"총 외출 시간: {hours}시간 {minutes}분")
|
||||
self.total_label.setText(tr('view.break.total_fmt', h=hours, m=minutes))
|
||||
else:
|
||||
self.total_label.setText(f"총 외출 시간: {minutes}분")
|
||||
self.total_label.setText(tr('view.break.total_min_only', m=minutes))
|
||||
|
||||
def edit_record(self, record_id):
|
||||
"""외출 기록 수정"""
|
||||
@@ -277,8 +286,8 @@ class BreakView(QDialog):
|
||||
"""외출 기록 삭제"""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"삭제 확인",
|
||||
"이 외출 기록을 삭제하시겠습니까?",
|
||||
tr('msg.confirm_delete.title'),
|
||||
tr('view.break.delete_confirm'),
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
|
||||
@@ -29,14 +29,14 @@ class ClockInDialog(QDialog):
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
|
||||
# 안내 문구
|
||||
info_label = QLabel("오늘의 출근시간을 입력해주세요")
|
||||
info_label = QLabel(tr('dlg.clock_in.prompt'))
|
||||
info_label.setObjectName("field_label")
|
||||
info_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 시간 입력
|
||||
time_layout = QHBoxLayout()
|
||||
time_label = QLabel("출근시간:")
|
||||
time_label = QLabel(tr('dlg.clock_in.label'))
|
||||
time_label.setObjectName("field_label")
|
||||
|
||||
self.time_edit = QTimeEdit()
|
||||
@@ -59,13 +59,13 @@ class ClockInDialog(QDialog):
|
||||
|
||||
# 빠른 선택 버튼
|
||||
quick_layout = QHBoxLayout()
|
||||
quick_label = QLabel("빠른 선택:")
|
||||
quick_label = QLabel(tr('dlg.clock_in.quick'))
|
||||
quick_label.setObjectName("field_label")
|
||||
|
||||
btn_8am = QPushButton("08:00")
|
||||
btn_9am = QPushButton("09:00")
|
||||
btn_10am = QPushButton("10:00")
|
||||
btn_now = QPushButton("현재")
|
||||
btn_now = QPushButton(tr('dlg.clock_in.btn_now'))
|
||||
|
||||
for btn in [btn_8am, btn_9am, btn_10am, btn_now]:
|
||||
btn.setMinimumHeight(30)
|
||||
@@ -87,12 +87,12 @@ class ClockInDialog(QDialog):
|
||||
# 버튼
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
ok_button = QPushButton("확인")
|
||||
ok_button = QPushButton(tr('btn.confirm'))
|
||||
ok_button.setObjectName("btn_primary")
|
||||
ok_button.setMinimumHeight(40)
|
||||
ok_button.clicked.connect(self.accept)
|
||||
|
||||
cancel_button = QPushButton("취소")
|
||||
cancel_button = QPushButton(tr('btn.cancel'))
|
||||
cancel_button.setMinimumHeight(40)
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
점심/저녁 토글 컨트롤러.
|
||||
|
||||
main_window.py에서 toggle_lunch_break / toggle_dinner_break / update_lunch_status /
|
||||
update_dinner_status 가 합쳐져 있던 것을 분리. 1Hz hot path 외 사용자 액션 응답.
|
||||
|
||||
단위 테스트가 가능하도록 window 의존성을 명시적으로 받음.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
|
||||
from core.i18n import tr
|
||||
|
||||
|
||||
class MealController:
|
||||
"""점심/저녁 토글 + 상태 라벨 갱신."""
|
||||
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
self.db = window.db
|
||||
|
||||
# -------- 토글 --------
|
||||
def toggle_lunch(self) -> None:
|
||||
w = self.window
|
||||
w.lunch_break_enabled = w.lunch_button.isChecked()
|
||||
self.refresh_lunch_label()
|
||||
|
||||
# 사용자가 직접 토글하면 자동 적용 플래그를 처리됨으로 간주 (중복 알림 방지)
|
||||
if w.lunch_break_enabled:
|
||||
w.auto_lunch_applied_today = True
|
||||
|
||||
if w.is_clocked_in:
|
||||
today = datetime.now().date().isoformat()
|
||||
self.db.update_lunch_break(today, w.lunch_break_enabled)
|
||||
|
||||
def toggle_dinner(self) -> None:
|
||||
w = self.window
|
||||
w.dinner_break_enabled = w.dinner_button.isChecked()
|
||||
self.refresh_dinner_label()
|
||||
|
||||
if w.is_clocked_in:
|
||||
today = datetime.now().date().isoformat()
|
||||
self.db.update_dinner_break(today, w.dinner_break_enabled)
|
||||
|
||||
# -------- 라벨 --------
|
||||
def refresh_lunch_label(self) -> None:
|
||||
w = self.window
|
||||
w.lunch_button.setText(
|
||||
tr('btn.lunch_applied') if w.lunch_break_enabled else tr('btn.lunch_add')
|
||||
)
|
||||
|
||||
def refresh_dinner_label(self) -> None:
|
||||
w = self.window
|
||||
w.dinner_button.setText(
|
||||
tr('btn.dinner_applied') if w.dinner_break_enabled else tr('btn.dinner_add')
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
런타임 i18n 재번역 — 재시작 없이 언어 전환.
|
||||
|
||||
사용법:
|
||||
from ui.i18n_runtime import register, retranslate_all, set_language_and_retranslate
|
||||
|
||||
label = QLabel(tr('label.foo'))
|
||||
register(label, 'label.foo') # 약한 참조로 등록
|
||||
|
||||
button = QPushButton()
|
||||
register(button, 'btn.bar', kwargs={'name': 'X'})
|
||||
|
||||
# 그룹박스 제목 등 setter가 다른 경우
|
||||
register(group, 'group.work_time', setter='setTitle')
|
||||
|
||||
# 윈도우 제목
|
||||
register(dialog, 'window.foo', setter='setWindowTitle')
|
||||
|
||||
언어 변경:
|
||||
set_language_and_retranslate('en')
|
||||
|
||||
각 widget은 weakref로 보관되므로 삭제되면 자동 정리. format placeholder가 있는
|
||||
키는 kwargs를 함께 등록하면 retranslate 시 같은 인자로 재계산.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import weakref
|
||||
from typing import Any, Callable, List, Tuple, Optional
|
||||
|
||||
from core.i18n import tr, set_language as _set_language
|
||||
|
||||
# (weakref, key, setter_name, kwargs, post_format) 튜플 리스트
|
||||
# weakref가 죽으면 다음 retranslate 시 정리.
|
||||
_registry: List[Tuple[weakref.ReferenceType, str, str, dict, Optional[Callable]]] = []
|
||||
|
||||
|
||||
def register(widget: Any, key: str, *, setter: str = 'setText',
|
||||
kwargs: Optional[dict] = None,
|
||||
post: Optional[Callable[[str], str]] = None) -> None:
|
||||
"""위젯을 retranslate 대상으로 등록.
|
||||
|
||||
Args:
|
||||
widget: PyQt 위젯 (setText/setTitle/setWindowTitle 등 지원)
|
||||
key: i18n 키
|
||||
setter: 호출할 메서드명 (기본 setText)
|
||||
kwargs: tr()에 전달할 format 인자 (정적인 경우만)
|
||||
post: 번역 후 한 번 더 가공할 콜백 — 예: 이모지 prefix
|
||||
"""
|
||||
# 약한 참조 — 위젯 삭제 시 자동 GC
|
||||
try:
|
||||
ref = weakref.ref(widget)
|
||||
except TypeError:
|
||||
# weakref 미지원 객체는 retranslate 불가
|
||||
return
|
||||
_registry.append((ref, key, setter, kwargs or {}, post))
|
||||
# 초기 적용
|
||||
_apply(widget, key, setter, kwargs or {}, post)
|
||||
|
||||
|
||||
def retranslate_all() -> None:
|
||||
"""모든 등록된 위젯에 현재 언어로 텍스트 재적용."""
|
||||
global _registry
|
||||
alive = []
|
||||
for ref, key, setter, kw, post in _registry:
|
||||
widget = ref()
|
||||
if widget is None:
|
||||
continue # 죽은 위젯은 빼버림
|
||||
try:
|
||||
_apply(widget, key, setter, kw, post)
|
||||
alive.append((ref, key, setter, kw, post))
|
||||
except RuntimeError:
|
||||
# Qt C++ 객체 삭제 후 호출 — 정리만 하고 패스
|
||||
continue
|
||||
_registry = alive
|
||||
|
||||
|
||||
def set_language_and_retranslate(lang: str) -> None:
|
||||
"""언어 전환 + 즉시 재번역."""
|
||||
_set_language(lang)
|
||||
retranslate_all()
|
||||
|
||||
|
||||
def clear() -> None:
|
||||
"""레지스트리 비우기 (테스트용)."""
|
||||
_registry.clear()
|
||||
|
||||
|
||||
def _apply(widget: Any, key: str, setter: str, kw: dict,
|
||||
post: Optional[Callable[[str], str]]) -> None:
|
||||
text = tr(key, **kw)
|
||||
if post:
|
||||
text = post(text)
|
||||
fn = getattr(widget, setter, None)
|
||||
if callable(fn):
|
||||
fn(text)
|
||||
+50
-48
@@ -40,27 +40,32 @@ class LeaveView(QDialog):
|
||||
|
||||
# 제목 + 잔액 + 설정 한 줄
|
||||
header_layout = QHBoxLayout()
|
||||
title = QLabel("연차 관리")
|
||||
title = QLabel(tr('view.leave.title'))
|
||||
title.setObjectName("dialog_title")
|
||||
header_layout.addWidget(title)
|
||||
header_layout.addStretch()
|
||||
self.balance_label = QLabel("잔여: 0일")
|
||||
self.balance_label = QLabel(tr('view.leave.balance_zero'))
|
||||
self.balance_label.setObjectName("badge_leave")
|
||||
header_layout.addWidget(self.balance_label)
|
||||
set_balance_button = QPushButton("잔여 설정")
|
||||
set_balance_button = QPushButton(tr('view.leave.btn_set_balance'))
|
||||
set_balance_button.clicked.connect(self.set_balance)
|
||||
header_layout.addWidget(set_balance_button)
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# 사용 내역
|
||||
used_group = QGroupBox("📤 사용 내역")
|
||||
used_group = QGroupBox(tr('view.leave.used_group'))
|
||||
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.setHorizontalHeaderLabels([
|
||||
tr('view.leave.col_date'),
|
||||
tr('view.leave.col_type'),
|
||||
tr('view.leave.col_used'),
|
||||
tr('view.leave.col_reason'),
|
||||
])
|
||||
self.used_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||
self.used_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||
self.used_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
||||
@@ -77,15 +82,15 @@ class LeaveView(QDialog):
|
||||
|
||||
# 버튼들
|
||||
button_layout = QHBoxLayout()
|
||||
add_leave_button = QPushButton("➕ 연차 사용 추가")
|
||||
add_leave_button = QPushButton(tr('view.leave.btn_add'))
|
||||
add_leave_button.clicked.connect(self.add_leave_record)
|
||||
button_layout.addWidget(add_leave_button)
|
||||
|
||||
cal_button = QPushButton("📅 캘린더 보기")
|
||||
cal_button = QPushButton(tr('view.leave.btn_calendar'))
|
||||
cal_button.clicked.connect(self._show_calendar)
|
||||
button_layout.addWidget(cal_button)
|
||||
|
||||
close_button = QPushButton("닫기")
|
||||
close_button = QPushButton(tr('btn.close'))
|
||||
close_button.clicked.connect(self.close)
|
||||
button_layout.addWidget(close_button)
|
||||
layout.addLayout(button_layout)
|
||||
@@ -102,7 +107,8 @@ class LeaveView(QDialog):
|
||||
# 잔액 업데이트
|
||||
balance = self.db.get_leave_balance()
|
||||
hours = balance * 8
|
||||
self.balance_label.setText(f"잔여: {balance}일 (총 {hours}시간)")
|
||||
self.balance_label.setText(tr('view.leave.balance_fmt',
|
||||
days=balance, hours=hours))
|
||||
|
||||
# 사용 내역 로드 (잔액 조정 제외)
|
||||
records = self.db.get_leave_records(exclude_bulk=True)
|
||||
@@ -144,7 +150,7 @@ class LeaveView(QDialog):
|
||||
return
|
||||
|
||||
menu = QMenu(self)
|
||||
delete_action = QAction("삭제", self)
|
||||
delete_action = QAction(tr('btn.delete_short'), self)
|
||||
delete_action.triggered.connect(self.delete_leave_record)
|
||||
menu.addAction(delete_action)
|
||||
menu.exec_(self.used_table.viewport().mapToGlobal(position))
|
||||
@@ -164,11 +170,10 @@ class LeaveView(QDialog):
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"삭제 확인",
|
||||
f"다음 연차 사용 기록을 삭제하시겠습니까?\n\n"
|
||||
f"날짜: {date_item.text()}\n"
|
||||
f"구분: {type_item.text()}\n"
|
||||
f"사용: {days_item.text()}",
|
||||
tr('msg.confirm_delete.title'),
|
||||
tr('view.leave.delete_confirm_body',
|
||||
date=date_item.text(), type=type_item.text(),
|
||||
days=days_item.text()),
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
@@ -183,13 +188,12 @@ class LeaveView(QDialog):
|
||||
|
||||
hours, ok = QInputDialog.getDouble(
|
||||
self,
|
||||
"연차 시간 설정",
|
||||
"연차 잔여 시간을 입력하세요 (0.5시간 단위):\n"
|
||||
"예) 8시간 = 1일, 4시간 = 0.5일(반차), 2시간 = 0.25일, 0.5시간 = 30분",
|
||||
tr('view.leave.set_title'),
|
||||
tr('view.leave.set_prompt'),
|
||||
current_hours,
|
||||
0.0,
|
||||
999.0,
|
||||
1 # 소수점 첫째자리까지 (0.5 단위)
|
||||
1
|
||||
)
|
||||
|
||||
if ok:
|
||||
@@ -200,8 +204,8 @@ class LeaveView(QDialog):
|
||||
self.db.set_leave_balance(days)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"설정 완료",
|
||||
f"연차 잔여 개수가 {days}일 ({hours}시간)로 설정되었습니다."
|
||||
tr('view.leave.set_done_title'),
|
||||
tr('view.leave.set_done_body', days=days, hours=hours)
|
||||
)
|
||||
self.load_data()
|
||||
|
||||
@@ -223,7 +227,7 @@ class AddLeaveDialog(QDialog):
|
||||
|
||||
def init_ui(self):
|
||||
"""UI 초기화"""
|
||||
self.setWindowTitle("연차 사용 기록 추가")
|
||||
self.setWindowTitle(tr('view.leave.add_title'))
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(360)
|
||||
|
||||
@@ -232,14 +236,14 @@ class AddLeaveDialog(QDialog):
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
|
||||
# 제목
|
||||
title = QLabel("연차 사용 기록 추가")
|
||||
title = QLabel(tr('view.leave.add_title'))
|
||||
title.setObjectName("dialog_subtitle")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# 날짜 + 구분 한 줄
|
||||
row1 = QHBoxLayout()
|
||||
date_label = QLabel("날짜:")
|
||||
date_label = QLabel(tr('view.leave.field_date'))
|
||||
date_label.setObjectName("field_label")
|
||||
date_label.setFixedWidth(40)
|
||||
self.date_edit = QDateEdit()
|
||||
@@ -248,14 +252,14 @@ class AddLeaveDialog(QDialog):
|
||||
row1.addWidget(date_label)
|
||||
row1.addWidget(self.date_edit)
|
||||
row1.addSpacing(8)
|
||||
type_label = QLabel("구분:")
|
||||
type_label = QLabel(tr('view.leave.field_type'))
|
||||
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.addItem(tr('view.leave.type_annual'), "annual")
|
||||
self.type_combo.addItem(tr('view.leave.type_half'), "half")
|
||||
self.type_combo.addItem(tr('view.leave.type_quarter'), "quarter")
|
||||
self.type_combo.addItem(tr('view.leave.type_hourly'), "hourly")
|
||||
self.type_combo.currentIndexChanged.connect(self.on_type_changed)
|
||||
row1.addWidget(type_label)
|
||||
row1.addWidget(self.type_combo)
|
||||
@@ -263,14 +267,14 @@ class AddLeaveDialog(QDialog):
|
||||
|
||||
# 사용 시간 (시간 연차용)
|
||||
hours_layout = QHBoxLayout()
|
||||
hours_label = QLabel("시간:")
|
||||
hours_label = QLabel(tr('view.leave.field_hours'))
|
||||
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.setSuffix(' ' + tr('label.unit_hour'))
|
||||
self.hours_spin.setEnabled(False)
|
||||
hours_layout.addWidget(hours_label)
|
||||
hours_layout.addWidget(self.hours_spin)
|
||||
@@ -278,27 +282,27 @@ class AddLeaveDialog(QDialog):
|
||||
|
||||
# 사유
|
||||
memo_layout = QHBoxLayout()
|
||||
memo_label = QLabel("사유:")
|
||||
memo_label = QLabel(tr('view.leave.field_reason'))
|
||||
memo_label.setObjectName("field_label")
|
||||
memo_label.setFixedWidth(40)
|
||||
self.memo_input = QLineEdit()
|
||||
self.memo_input.setPlaceholderText("예) 개인 사유, 병원 방문 등")
|
||||
self.memo_input.setPlaceholderText(tr('view.leave.placeholder_reason'))
|
||||
memo_layout.addWidget(memo_label)
|
||||
memo_layout.addWidget(self.memo_input)
|
||||
layout.addLayout(memo_layout)
|
||||
|
||||
# 안내
|
||||
info_label = QLabel("※ 잔여 연차가 자동 차감됩니다.")
|
||||
info_label = QLabel(tr('view.leave.note_auto_deduct'))
|
||||
info_label.setObjectName("note_text")
|
||||
layout.addWidget(info_label)
|
||||
|
||||
# 버튼
|
||||
button_layout = QHBoxLayout()
|
||||
save_button = QPushButton("저장")
|
||||
save_button = QPushButton(tr('btn.save'))
|
||||
save_button.setObjectName("btn_primary")
|
||||
save_button.clicked.connect(self.save_record)
|
||||
button_layout.addWidget(save_button)
|
||||
cancel_button = QPushButton("취소")
|
||||
cancel_button = QPushButton(tr('btn.cancel'))
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
@@ -336,8 +340,8 @@ class AddLeaveDialog(QDialog):
|
||||
if current_balance < days:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"잔여 연차 부족",
|
||||
f"잔여 연차가 부족합니다.\n현재 잔여: {current_balance}일\n사용 요청: {days}일"
|
||||
tr('view.leave.short_title'),
|
||||
tr('view.leave.short_body', balance=current_balance, req=days)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -345,12 +349,10 @@ class AddLeaveDialog(QDialog):
|
||||
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"이 기록을 추가하시겠습니까?",
|
||||
tr('view.leave.confirm_title'),
|
||||
tr('view.leave.confirm_body',
|
||||
date=date, type=leave_type_name, days=days, hours=hours,
|
||||
reason=(memo if memo else '-')),
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
@@ -360,15 +362,15 @@ class AddLeaveDialog(QDialog):
|
||||
self.db.use_leave(days, date, leave_type_name, memo)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"추가 완료",
|
||||
f"{days}일 ({hours}시간)의 연차 사용이 기록되었습니다."
|
||||
tr('view.leave.added_title'),
|
||||
tr('view.leave.added_body', days=days, hours=hours)
|
||||
)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"오류",
|
||||
f"연차 기록 추가 중 오류가 발생했습니다:\n{str(e)}"
|
||||
tr('view.leave.error_title'),
|
||||
tr('view.leave.error_body', err=str(e))
|
||||
)
|
||||
|
||||
|
||||
|
||||
+23
-29
@@ -81,13 +81,15 @@ class MainWindow(QMainWindow):
|
||||
self.notifier = Notifier(self, db=self.db)
|
||||
self.notifier.notification_signal.connect(self.show_notification)
|
||||
|
||||
# 책임 분리된 컨트롤러들 (1Hz hot path)
|
||||
# 책임 분리된 컨트롤러들 (1Hz hot path + 사용자 액션)
|
||||
from ui.controllers.lock_monitor import LockMonitor
|
||||
from ui.controllers.auto_lunch import AutoLunchManager
|
||||
from ui.controllers.notification_orchestrator import NotificationOrchestrator
|
||||
from ui.controllers.meal_controller import MealController
|
||||
self._lock_monitor = LockMonitor(self)
|
||||
self._auto_lunch = AutoLunchManager(self)
|
||||
self._notif_orch = NotificationOrchestrator(self)
|
||||
self._meal = MealController(self)
|
||||
|
||||
# 시스템 트레이
|
||||
self.tray_icon = SystemTrayIcon(self)
|
||||
@@ -184,7 +186,11 @@ class MainWindow(QMainWindow):
|
||||
def init_ui(self):
|
||||
"""UI 초기화"""
|
||||
from core.version import __version__
|
||||
from ui.i18n_runtime import register
|
||||
self._app_version = __version__
|
||||
self.setWindowTitle(f"⏰ {tr('window.main_title')} v{__version__}")
|
||||
register(self, 'window.main_title', setter='setWindowTitle',
|
||||
post=lambda t: f"⏰ {t} v{__version__}")
|
||||
self.setGeometry(100, 100, 500, 620)
|
||||
self.setMinimumSize(480, 520)
|
||||
|
||||
@@ -326,6 +332,14 @@ class MainWindow(QMainWindow):
|
||||
help_button = QPushButton(tr('menu.help'))
|
||||
settings_button = QPushButton(tr('menu.settings'))
|
||||
|
||||
# 런타임 i18n 등록
|
||||
for btn, key in [(stats_button, 'menu.stats'),
|
||||
(calendar_button, 'menu.calendar'),
|
||||
(report_button, 'menu.daily_report'),
|
||||
(help_button, 'menu.help'),
|
||||
(settings_button, 'menu.settings')]:
|
||||
register(btn, key)
|
||||
|
||||
for btn in [stats_button, calendar_button, report_button, help_button, settings_button]:
|
||||
bottom_layout.addWidget(btn)
|
||||
|
||||
@@ -750,40 +764,20 @@ class MainWindow(QMainWindow):
|
||||
self.date_label.setText(date_str)
|
||||
|
||||
def toggle_lunch_break(self):
|
||||
"""점심시간 토글"""
|
||||
self.lunch_break_enabled = self.lunch_button.isChecked()
|
||||
self.update_lunch_status()
|
||||
|
||||
# 사용자가 직접 토글하면 자동 적용 플래그를 처리됨으로 간주 (중복 알림 방지)
|
||||
if self.lunch_break_enabled:
|
||||
self.auto_lunch_applied_today = True
|
||||
|
||||
# DB 업데이트
|
||||
if self.is_clocked_in:
|
||||
today = datetime.now().date().isoformat()
|
||||
self.db.update_lunch_break(today, self.lunch_break_enabled)
|
||||
"""점심시간 토글 — MealController 위임."""
|
||||
self._meal.toggle_lunch()
|
||||
|
||||
def toggle_dinner_break(self):
|
||||
"""저녁시간 토글"""
|
||||
self.dinner_break_enabled = self.dinner_button.isChecked()
|
||||
self.update_dinner_status()
|
||||
|
||||
# DB 업데이트
|
||||
if self.is_clocked_in:
|
||||
today = datetime.now().date().isoformat()
|
||||
self.db.update_dinner_break(today, self.dinner_break_enabled)
|
||||
"""저녁시간 토글 — MealController 위임."""
|
||||
self._meal.toggle_dinner()
|
||||
|
||||
def update_lunch_status(self):
|
||||
"""점심시간 상태 업데이트"""
|
||||
self.lunch_button.setText(
|
||||
tr('btn.lunch_applied') if self.lunch_break_enabled else tr('btn.lunch_add')
|
||||
)
|
||||
"""점심시간 상태 업데이트 — MealController 위임."""
|
||||
self._meal.refresh_lunch_label()
|
||||
|
||||
def update_dinner_status(self):
|
||||
"""저녁시간 상태 업데이트"""
|
||||
self.dinner_button.setText(
|
||||
tr('btn.dinner_applied') if self.dinner_break_enabled else tr('btn.dinner_add')
|
||||
)
|
||||
"""저녁시간 상태 업데이트 — MealController 위임."""
|
||||
self._meal.refresh_dinner_label()
|
||||
|
||||
def update_overtime_balance(self):
|
||||
"""연장근무 잔액 업데이트"""
|
||||
|
||||
+79
-58
@@ -38,24 +38,28 @@ class OvertimeView(QDialog):
|
||||
|
||||
# 제목 + 잔액 한 줄
|
||||
header_layout = QHBoxLayout()
|
||||
title = QLabel("연장근무 내역")
|
||||
title = QLabel(tr('view.overtime.title'))
|
||||
title.setObjectName("dialog_title")
|
||||
header_layout.addWidget(title)
|
||||
header_layout.addStretch()
|
||||
self.balance_label = QLabel("잔액: 0분")
|
||||
self.balance_label = QLabel(tr('view.overtime.balance_zero'))
|
||||
self.balance_label.setObjectName("badge_balance")
|
||||
header_layout.addWidget(self.balance_label)
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# 적립 내역
|
||||
earned_group = QGroupBox("💰 적립 내역")
|
||||
earned_group = QGroupBox(tr('view.overtime.earned_group'))
|
||||
earned_layout = QVBoxLayout()
|
||||
earned_layout.setSpacing(4)
|
||||
earned_layout.setContentsMargins(8, 20, 8, 6)
|
||||
|
||||
self.earned_table = QTableWidget()
|
||||
self.earned_table.setColumnCount(3)
|
||||
self.earned_table.setHorizontalHeaderLabels(["날짜", "적립", "메모"])
|
||||
self.earned_table.setHorizontalHeaderLabels([
|
||||
tr('view.overtime.col_date'),
|
||||
tr('view.overtime.col_earned'),
|
||||
tr('view.overtime.col_memo'),
|
||||
])
|
||||
self.earned_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||
self.earned_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||
self.earned_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
@@ -64,7 +68,7 @@ class OvertimeView(QDialog):
|
||||
self.earned_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
earned_layout.addWidget(self.earned_table)
|
||||
|
||||
add_earned_button = QPushButton("➕ 수동 적립")
|
||||
add_earned_button = QPushButton(tr('view.overtime.btn_add_earned'))
|
||||
add_earned_button.clicked.connect(self.add_earned_record)
|
||||
earned_layout.addWidget(add_earned_button)
|
||||
|
||||
@@ -72,14 +76,18 @@ class OvertimeView(QDialog):
|
||||
layout.addWidget(earned_group)
|
||||
|
||||
# 사용 내역
|
||||
used_group = QGroupBox("📤 사용 내역")
|
||||
used_group = QGroupBox(tr('view.overtime.used_group'))
|
||||
used_layout = QVBoxLayout()
|
||||
used_layout.setSpacing(4)
|
||||
used_layout.setContentsMargins(8, 20, 8, 6)
|
||||
|
||||
self.used_table = QTableWidget()
|
||||
self.used_table.setColumnCount(3)
|
||||
self.used_table.setHorizontalHeaderLabels(["날짜", "사용", "사유"])
|
||||
self.used_table.setHorizontalHeaderLabels([
|
||||
tr('view.overtime.col_date'),
|
||||
tr('view.overtime.col_used'),
|
||||
tr('view.overtime.col_reason'),
|
||||
])
|
||||
self.used_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||
self.used_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||
self.used_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
@@ -90,7 +98,7 @@ class OvertimeView(QDialog):
|
||||
self.used_table.customContextMenuRequested.connect(self.show_used_context_menu)
|
||||
used_layout.addWidget(self.used_table)
|
||||
|
||||
add_used_button = QPushButton("➕ 수동 사용")
|
||||
add_used_button = QPushButton(tr('view.overtime.btn_add_used'))
|
||||
add_used_button.clicked.connect(self.add_used_record)
|
||||
used_layout.addWidget(add_used_button)
|
||||
|
||||
@@ -98,7 +106,7 @@ class OvertimeView(QDialog):
|
||||
layout.addWidget(used_group)
|
||||
|
||||
# 닫기 버튼
|
||||
close_button = QPushButton("닫기")
|
||||
close_button = QPushButton(tr('btn.close'))
|
||||
close_button.clicked.connect(self.close)
|
||||
layout.addWidget(close_button)
|
||||
|
||||
@@ -110,23 +118,21 @@ class OvertimeView(QDialog):
|
||||
balance = self.db.get_total_overtime_balance()
|
||||
hours = balance // 60
|
||||
minutes = balance % 60
|
||||
self.balance_label.setText(f"현재 잔액: {hours}시간 {minutes}분 ({balance}분)")
|
||||
self.balance_label.setText(tr('view.overtime.balance_fmt',
|
||||
h=hours, m=minutes, total=balance))
|
||||
|
||||
# 적립 내역 로드
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT ob.date, ob.earned_minutes,
|
||||
CASE
|
||||
WHEN ob.work_record_id IS NULL THEN '수동 추가'
|
||||
ELSE COALESCE(wr.memo, '')
|
||||
END as memo
|
||||
SELECT ob.date, ob.earned_minutes, ob.work_record_id, wr.memo
|
||||
FROM overtime_bank ob
|
||||
LEFT JOIN work_records wr ON ob.work_record_id = wr.id
|
||||
ORDER BY ob.date DESC
|
||||
''')
|
||||
earned_records = cursor.fetchall()
|
||||
manual_label = tr('msg.manual_added')
|
||||
|
||||
self.earned_table.setRowCount(len(earned_records))
|
||||
for i, record in enumerate(earned_records):
|
||||
@@ -136,12 +142,17 @@ class OvertimeView(QDialog):
|
||||
minutes = record[1]
|
||||
hours = minutes // 60
|
||||
mins = minutes % 60
|
||||
time_str = f"{hours}시간 {mins}분" if hours > 0 else f"{mins}분"
|
||||
if hours > 0:
|
||||
time_str = tr('view.break.duration_fmt', h=hours, m=mins)
|
||||
else:
|
||||
time_str = tr('view.break.duration_min_only', m=mins)
|
||||
time_item = QTableWidgetItem(time_str)
|
||||
time_item.setTextAlignment(Qt.AlignCenter)
|
||||
time_item.setForeground(QColor(39, 174, 96)) # 초록색
|
||||
|
||||
memo_item = QTableWidgetItem(record[2] or "")
|
||||
# work_record_id NULL이면 "수동 추가", 아니면 wr.memo
|
||||
memo_text = manual_label if record[2] is None else (record[3] or "")
|
||||
memo_item = QTableWidgetItem(memo_text)
|
||||
|
||||
self.earned_table.setItem(i, 0, date_item)
|
||||
self.earned_table.setItem(i, 1, time_item)
|
||||
@@ -166,7 +177,10 @@ class OvertimeView(QDialog):
|
||||
minutes = record[2]
|
||||
hours = minutes // 60
|
||||
mins = minutes % 60
|
||||
time_str = f"{hours}시간 {mins}분" if hours > 0 else f"{mins}분"
|
||||
if hours > 0:
|
||||
time_str = tr('view.break.duration_fmt', h=hours, m=mins)
|
||||
else:
|
||||
time_str = tr('view.break.duration_min_only', m=mins)
|
||||
time_item = QTableWidgetItem(time_str)
|
||||
time_item.setTextAlignment(Qt.AlignCenter)
|
||||
time_item.setForeground(QColor(231, 76, 60)) # 빨간색
|
||||
@@ -188,7 +202,7 @@ class OvertimeView(QDialog):
|
||||
|
||||
# 컨텍스트 메뉴 생성
|
||||
menu = QMenu(self)
|
||||
delete_action = QAction("❌ 삭제", self)
|
||||
delete_action = QAction(tr('view.overtime.menu_delete'), self)
|
||||
delete_action.triggered.connect(self.delete_used_record)
|
||||
menu.addAction(delete_action)
|
||||
|
||||
@@ -213,11 +227,10 @@ class OvertimeView(QDialog):
|
||||
# 확인 메시지
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"삭제 확인",
|
||||
f"다음 사용 기록을 삭제하시겠습니까?\n\n"
|
||||
f"날짜: {date_item.text()}\n"
|
||||
f"시간: {time_item.text()}\n"
|
||||
f"사유: {reason_item.text()}",
|
||||
tr('msg.confirm_delete.title'),
|
||||
tr('view.overtime.delete_confirm_body',
|
||||
date=date_item.text(), time=time_item.text(),
|
||||
reason=reason_item.text()),
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
@@ -266,7 +279,7 @@ class AddOvertimeEarnedDialog(QDialog):
|
||||
|
||||
def init_ui(self):
|
||||
"""UI 초기화"""
|
||||
self.setWindowTitle("추가근무 수동 적립")
|
||||
self.setWindowTitle(tr('view.overtime.manual_earned_title'))
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(360)
|
||||
|
||||
@@ -275,14 +288,14 @@ class AddOvertimeEarnedDialog(QDialog):
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
|
||||
# 제목
|
||||
title = QLabel("추가근무 수동 적립")
|
||||
title = QLabel(tr('view.overtime.manual_earned_title'))
|
||||
title.setObjectName("dialog_subtitle")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# 날짜
|
||||
date_layout = QHBoxLayout()
|
||||
date_label = QLabel("날짜:")
|
||||
date_label = QLabel(tr('view.overtime.field_date'))
|
||||
date_label.setObjectName("field_label")
|
||||
date_label.setFixedWidth(60)
|
||||
self.date_edit = QDateEdit()
|
||||
@@ -294,14 +307,15 @@ class AddOvertimeEarnedDialog(QDialog):
|
||||
|
||||
# 시간 (30분 단위)
|
||||
time_layout = QHBoxLayout()
|
||||
time_label = QLabel("시간:")
|
||||
time_label = QLabel(tr('view.overtime.field_time'))
|
||||
time_label.setObjectName("field_label")
|
||||
time_label.setFixedWidth(60)
|
||||
self.hour_spin = QSpinBox()
|
||||
self.hour_spin.setRange(0, 23)
|
||||
self.hour_spin.setSuffix("시간")
|
||||
self.hour_spin.setSuffix(tr('view.overtime.unit_hour_suffix'))
|
||||
self.minute_combo = QComboBox()
|
||||
self.minute_combo.addItems(["0분", "30분"])
|
||||
self.minute_combo.addItems([tr('view.overtime.minute_0'),
|
||||
tr('view.overtime.minute_30')])
|
||||
time_layout.addWidget(time_label)
|
||||
time_layout.addWidget(self.hour_spin)
|
||||
time_layout.addWidget(self.minute_combo)
|
||||
@@ -309,21 +323,21 @@ class AddOvertimeEarnedDialog(QDialog):
|
||||
|
||||
# 메모
|
||||
memo_layout = QHBoxLayout()
|
||||
memo_label = QLabel("메모:")
|
||||
memo_label = QLabel(tr('view.overtime.field_memo'))
|
||||
memo_label.setObjectName("field_label")
|
||||
memo_label.setFixedWidth(60)
|
||||
self.memo_edit = QLineEdit()
|
||||
self.memo_edit.setPlaceholderText("선택사항")
|
||||
self.memo_edit.setPlaceholderText(tr('view.overtime.placeholder_memo'))
|
||||
memo_layout.addWidget(memo_label)
|
||||
memo_layout.addWidget(self.memo_edit)
|
||||
layout.addLayout(memo_layout)
|
||||
|
||||
# 버튼
|
||||
button_layout = QHBoxLayout()
|
||||
save_button = QPushButton("저장")
|
||||
save_button = QPushButton(tr('btn.save'))
|
||||
save_button.setObjectName("btn_primary")
|
||||
save_button.clicked.connect(self.save)
|
||||
cancel_button = QPushButton("취소")
|
||||
cancel_button = QPushButton(tr('btn.cancel'))
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(save_button)
|
||||
button_layout.addWidget(cancel_button)
|
||||
@@ -335,11 +349,12 @@ class AddOvertimeEarnedDialog(QDialog):
|
||||
"""저장"""
|
||||
# 시간 계산 (30분 단위)
|
||||
hours = self.hour_spin.value()
|
||||
minutes = 0 if self.minute_combo.currentText() == "0분" else 30
|
||||
minutes = 0 if self.minute_combo.currentIndex() == 0 else 30
|
||||
total_minutes = hours * 60 + minutes
|
||||
|
||||
if total_minutes == 0:
|
||||
QMessageBox.warning(self, "입력 오류", "0분은 추가할 수 없습니다.")
|
||||
QMessageBox.warning(self, tr('msg.input_error.title'),
|
||||
tr('view.overtime.zero_add_error'))
|
||||
return
|
||||
|
||||
date = self.date_edit.date().toString("yyyy-MM-dd")
|
||||
@@ -371,8 +386,8 @@ class AddOvertimeEarnedDialog(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"저장 완료",
|
||||
f"{hours}시간 {minutes}분이 적립되었습니다."
|
||||
tr('msg.save_success.title'),
|
||||
tr('view.overtime.saved_earned', h=hours, m=minutes)
|
||||
)
|
||||
self.accept()
|
||||
|
||||
@@ -388,7 +403,7 @@ class AddOvertimeUsedDialog(QDialog):
|
||||
|
||||
def init_ui(self):
|
||||
"""UI 초기화"""
|
||||
self.setWindowTitle("추가근무 수동 사용")
|
||||
self.setWindowTitle(tr('view.overtime.manual_used_title'))
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(360)
|
||||
|
||||
@@ -398,21 +413,25 @@ class AddOvertimeUsedDialog(QDialog):
|
||||
|
||||
# 제목 + 잔액 한 줄
|
||||
header_layout = QHBoxLayout()
|
||||
title = QLabel("추가근무 수동 사용")
|
||||
title = QLabel(tr('view.overtime.manual_used_title'))
|
||||
title.setObjectName("dialog_subtitle")
|
||||
header_layout.addWidget(title)
|
||||
header_layout.addStretch()
|
||||
balance = self.db.get_total_overtime_balance()
|
||||
hours = balance // 60
|
||||
minutes = balance % 60
|
||||
balance_label = QLabel(f"잔액: {hours}시간 {minutes}분")
|
||||
if hours > 0:
|
||||
balance_text = tr('view.break.duration_fmt', h=hours, m=minutes)
|
||||
else:
|
||||
balance_text = tr('view.break.duration_min_only', m=minutes)
|
||||
balance_label = QLabel(f"{tr('view.overtime.balance_zero').split(':')[0]}: {balance_text}")
|
||||
balance_label.setObjectName("badge_balance")
|
||||
header_layout.addWidget(balance_label)
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# 날짜
|
||||
date_layout = QHBoxLayout()
|
||||
date_label = QLabel("날짜:")
|
||||
date_label = QLabel(tr('view.overtime.field_date'))
|
||||
date_label.setObjectName("field_label")
|
||||
date_label.setFixedWidth(60)
|
||||
self.date_edit = QDateEdit()
|
||||
@@ -424,14 +443,15 @@ class AddOvertimeUsedDialog(QDialog):
|
||||
|
||||
# 시간 (30분 단위)
|
||||
time_layout = QHBoxLayout()
|
||||
time_label = QLabel("시간:")
|
||||
time_label = QLabel(tr('view.overtime.field_time'))
|
||||
time_label.setObjectName("field_label")
|
||||
time_label.setFixedWidth(60)
|
||||
self.hour_spin = QSpinBox()
|
||||
self.hour_spin.setRange(0, 23)
|
||||
self.hour_spin.setSuffix("시간")
|
||||
self.hour_spin.setSuffix(tr('view.overtime.unit_hour_suffix'))
|
||||
self.minute_combo = QComboBox()
|
||||
self.minute_combo.addItems(["0분", "30분"])
|
||||
self.minute_combo.addItems([tr('view.overtime.minute_0'),
|
||||
tr('view.overtime.minute_30')])
|
||||
time_layout.addWidget(time_label)
|
||||
time_layout.addWidget(self.hour_spin)
|
||||
time_layout.addWidget(self.minute_combo)
|
||||
@@ -439,21 +459,21 @@ class AddOvertimeUsedDialog(QDialog):
|
||||
|
||||
# 사유
|
||||
reason_layout = QHBoxLayout()
|
||||
reason_label = QLabel("사유:")
|
||||
reason_label = QLabel(tr('view.overtime.field_reason'))
|
||||
reason_label.setObjectName("field_label")
|
||||
reason_label.setFixedWidth(60)
|
||||
self.reason_edit = QLineEdit()
|
||||
self.reason_edit.setPlaceholderText("예: 개인 사유")
|
||||
self.reason_edit.setPlaceholderText(tr('view.overtime.placeholder_reason'))
|
||||
reason_layout.addWidget(reason_label)
|
||||
reason_layout.addWidget(self.reason_edit)
|
||||
layout.addLayout(reason_layout)
|
||||
|
||||
# 버튼
|
||||
button_layout = QHBoxLayout()
|
||||
save_button = QPushButton("저장")
|
||||
save_button = QPushButton(tr('btn.save'))
|
||||
save_button.setObjectName("btn_primary")
|
||||
save_button.clicked.connect(self.save)
|
||||
cancel_button = QPushButton("취소")
|
||||
cancel_button = QPushButton(tr('btn.cancel'))
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(save_button)
|
||||
button_layout.addWidget(cancel_button)
|
||||
@@ -465,11 +485,12 @@ class AddOvertimeUsedDialog(QDialog):
|
||||
"""저장"""
|
||||
# 시간 계산 (30분 단위)
|
||||
hours = self.hour_spin.value()
|
||||
minutes = 0 if self.minute_combo.currentText() == "0분" else 30
|
||||
minutes = 0 if self.minute_combo.currentIndex() == 0 else 30
|
||||
total_minutes = hours * 60 + minutes
|
||||
|
||||
if total_minutes == 0:
|
||||
QMessageBox.warning(self, "입력 오류", "0분은 사용할 수 없습니다.")
|
||||
QMessageBox.warning(self, tr('msg.input_error.title'),
|
||||
tr('view.overtime.zero_use_error'))
|
||||
return
|
||||
|
||||
# 잔액 확인
|
||||
@@ -477,23 +498,23 @@ class AddOvertimeUsedDialog(QDialog):
|
||||
if total_minutes > balance:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"잔액 부족",
|
||||
f"사용 가능한 시간이 부족합니다.\n\n"
|
||||
f"요청: {hours}시간 {minutes}분\n"
|
||||
f"잔액: {balance // 60}시간 {balance % 60}분"
|
||||
tr('view.overtime.balance_short_title'),
|
||||
tr('view.overtime.balance_short_body',
|
||||
req_h=hours, req_m=minutes,
|
||||
bal_h=balance // 60, bal_m=balance % 60)
|
||||
)
|
||||
return
|
||||
|
||||
date = self.date_edit.date().toString("yyyy-MM-dd")
|
||||
reason = self.reason_edit.text().strip() or "수동 사용"
|
||||
reason = self.reason_edit.text().strip() or tr('msg.manual_added')
|
||||
|
||||
# DB에 저장
|
||||
self.db.add_overtime_usage(None, total_minutes, date, reason)
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"저장 완료",
|
||||
f"{hours}시간 {minutes}분이 사용 처리되었습니다."
|
||||
tr('msg.save_success.title'),
|
||||
tr('view.overtime.saved_used', h=hours, m=minutes)
|
||||
)
|
||||
self.accept()
|
||||
|
||||
|
||||
+6
-4
@@ -1114,16 +1114,18 @@ class SettingsView(QDialog):
|
||||
if self.parent_window and hasattr(self.parent_window, 'reload_settings'):
|
||||
self.parent_window.reload_settings()
|
||||
|
||||
# 언어 변경 감지 → 재시작 제안
|
||||
# 언어 변경 감지 → 등록된 위젯 즉시 재번역, 아직 미등록 영역은 재시작 권장
|
||||
if hasattr(self, 'language_combo'):
|
||||
from core.i18n import get_language
|
||||
from ui.i18n_runtime import set_language_and_retranslate
|
||||
new_lang = self.language_combo.currentData()
|
||||
if new_lang and new_lang != get_language():
|
||||
set_language_and_retranslate(new_lang)
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"재시작 필요 / Restart required",
|
||||
"언어 변경을 완전히 적용하려면 재시작이 필요합니다.\n지금 재시작할까요?\n\n"
|
||||
"Restart now to fully apply the language change?",
|
||||
"재시작 / Restart",
|
||||
"주요 화면은 즉시 적용됩니다. 일부 다이얼로그는 재시작 후 완전히 반영됩니다.\n지금 재시작할까요?\n\n"
|
||||
"Main UI updates immediately. Some dialogs need a restart for full effect.\nRestart now?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
|
||||
Reference in New Issue
Block a user