i18n: complete UI/achievement keying, fix help_view tr() and i18n syntax errors
This commit is contained in:
+14
-28
@@ -16,6 +16,7 @@ from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from core.achievements import get_all_with_status, get_stats
|
||||
from core.i18n import tr
|
||||
from ui.styles import apply_dark_titlebar
|
||||
from ui.dark_components import tc, tabs_qss, button_qss, scroll_qss, ACCENT_GOLD, _is_dark
|
||||
|
||||
@@ -29,7 +30,6 @@ TIER_THEMES = {
|
||||
'bg_bot': '#241810',
|
||||
'text': '#ffd9a8',
|
||||
'label': '🥉',
|
||||
'name': '브론즈',
|
||||
},
|
||||
'silver': {
|
||||
'border': '#a8a8a8',
|
||||
@@ -38,7 +38,6 @@ TIER_THEMES = {
|
||||
'bg_bot': '#1c1c22',
|
||||
'text': '#e8e8f0',
|
||||
'label': '🥈',
|
||||
'name': '실버',
|
||||
},
|
||||
'gold': {
|
||||
'border': '#ffb700',
|
||||
@@ -47,7 +46,6 @@ TIER_THEMES = {
|
||||
'bg_bot': '#241c08',
|
||||
'text': '#ffe9a0',
|
||||
'label': '🥇',
|
||||
'name': '골드',
|
||||
},
|
||||
'platinum': {
|
||||
'border': '#7fdbff',
|
||||
@@ -56,7 +54,6 @@ TIER_THEMES = {
|
||||
'bg_bot': '#0e1f28',
|
||||
'text': '#c5ecff',
|
||||
'label': '💎',
|
||||
'name': '플래티넘',
|
||||
},
|
||||
'legend': {
|
||||
'border': '#ff6b9d',
|
||||
@@ -65,20 +62,9 @@ TIER_THEMES = {
|
||||
'bg_bot': '#26101a',
|
||||
'text': '#ffc0d4',
|
||||
'label': '🌟',
|
||||
'name': '레전드',
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY_LABELS = {
|
||||
'streak': '출근 streak', 'punctual': '시간 엄수', 'balance': '워라밸',
|
||||
'ot_bank': '연장 적립', 'ot_use': '연장 사용', 'leave': '연차',
|
||||
'health': '건강', 'special_day': '특별일', 'pattern': '패턴',
|
||||
'milestone': '마일스톤', 'season': '시즌', 'time_slot': '시간대',
|
||||
'meal': '식사', 'break_use': '외출', 'settings': '설정',
|
||||
'stats': '통계', 'secret': '시크릿', 'korea': '한국 문화',
|
||||
'ambition': '야망', 'meta': '메타',
|
||||
}
|
||||
|
||||
|
||||
class AchievementsView(QDialog):
|
||||
"""도전과제 다이얼로그 — 4탭 + 통계 헤더."""
|
||||
@@ -86,7 +72,7 @@ class AchievementsView(QDialog):
|
||||
def __init__(self, db, parent=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
self.setWindowTitle("도전과제")
|
||||
self.setWindowTitle(tr('achieve.title'))
|
||||
self.setMinimumSize(960, 720)
|
||||
self.resize(1100, 800)
|
||||
self._increment_view_count()
|
||||
@@ -121,21 +107,21 @@ class AchievementsView(QDialog):
|
||||
if a['earned_date'] is None and not a['is_secret']]
|
||||
secret_items = [a for a in all_items if a['is_secret']]
|
||||
|
||||
self.tabs.addTab(self._build_grid_tab(all_items), f"🌐 전체 · {len(all_items)}")
|
||||
self.tabs.addTab(self._build_grid_tab(all_items), tr('achieve.tab_all', count=len(all_items)))
|
||||
self.tabs.addTab(self._build_grid_tab(in_progress),
|
||||
f"⚡ 진행 중 · {len(in_progress)}")
|
||||
tr('achieve.tab_in_progress', count=len(in_progress)))
|
||||
self.tabs.addTab(self._build_grid_tab(earned_items),
|
||||
f"✓ 완료 · {len(earned_items)}")
|
||||
tr('achieve.tab_completed', count=len(earned_items)))
|
||||
self.tabs.addTab(
|
||||
self._build_grid_tab(secret_items, secret_mode=True),
|
||||
f"🌑 시크릿 · {stats['secret_earned']}/{stats['secret_total']}"
|
||||
tr('achieve.tab_secret', earned=stats['secret_earned'], total=stats['secret_total'])
|
||||
)
|
||||
layout.addWidget(self.tabs, 1)
|
||||
|
||||
# === 닫기 버튼 ===
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
close_btn = QPushButton("닫기")
|
||||
close_btn = QPushButton(tr('btn.close'))
|
||||
close_btn.setMinimumWidth(100)
|
||||
close_btn.setStyleSheet(button_qss('default'))
|
||||
close_btn.clicked.connect(self.accept)
|
||||
@@ -183,7 +169,7 @@ class AchievementsView(QDialog):
|
||||
|
||||
secret_lbl = QLabel(
|
||||
f"<div style='line-height: 1.3;'>"
|
||||
f"<span style='font-size: 9pt; color: {tc('text_dim')};'>🌑 시크릿</span><br>"
|
||||
f"<span style='font-size: 9pt; color: {tc('text_dim')};'>🌑 {tr('achieve.cat_secret')}</span><br>"
|
||||
f"<span style='font-size: 18pt; font-weight: bold; color: {c_secret};'>"
|
||||
f"{stats['secret_earned']}</span>"
|
||||
f"<span style='font-size: 12pt; color: {tc('text_dim')};'> / {stats['secret_total']}</span>"
|
||||
@@ -196,7 +182,7 @@ class AchievementsView(QDialog):
|
||||
|
||||
pct_lbl = QLabel(
|
||||
f"<div style='text-align: right; line-height: 1.3;'>"
|
||||
f"<span style='font-size: 9pt; color: {tc('text_dim')};'>달성률</span><br>"
|
||||
f"<span style='font-size: 9pt; color: {tc('text_dim')};'>{tr('achieve.completion_rate')}</span><br>"
|
||||
f"<span style='font-size: 24pt; font-weight: bold; color: {c_pct};'>"
|
||||
f"{pct:.1f}%</span></div>"
|
||||
)
|
||||
@@ -242,7 +228,7 @@ class AchievementsView(QDialog):
|
||||
grid.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
if not items:
|
||||
empty = QLabel("(아직 없음)")
|
||||
empty = QLabel(tr('achieve.empty'))
|
||||
empty.setAlignment(Qt.AlignCenter)
|
||||
empty.setStyleSheet(
|
||||
f"color: {tc('text_faint')}; padding: 60px; font-size: 12pt; background: transparent;"
|
||||
@@ -344,9 +330,9 @@ class AchievementsView(QDialog):
|
||||
name.setWordWrap(True)
|
||||
name_box.addWidget(name)
|
||||
|
||||
cat_text = CATEGORY_LABELS.get(item['category'], item['category'] or '')
|
||||
cat_text = tr(f"achieve.cat_{item['category']}")
|
||||
if not is_locked_secret:
|
||||
cat_label = QLabel(f" {theme['label']} {theme['name']} · {cat_text} ")
|
||||
cat_label = QLabel(f" {theme['label']} {tr(f'achieve.tier_{tier}')} · {cat_text} ")
|
||||
cat_label.setStyleSheet(
|
||||
f"font-size: 8.5pt; "
|
||||
f"color: {theme['border_strong'] if _is_dark() else tc('text_dim')}; "
|
||||
@@ -368,7 +354,7 @@ class AchievementsView(QDialog):
|
||||
|
||||
# 2행: 설명
|
||||
if is_locked_secret:
|
||||
desc_text = "🔒 달성하면 공개됩니다"
|
||||
desc_text = tr('achieve.secret_locked')
|
||||
else:
|
||||
desc_text = item['description'] or ''
|
||||
desc = QLabel(desc_text)
|
||||
@@ -381,7 +367,7 @@ class AchievementsView(QDialog):
|
||||
|
||||
# 3행: 진행 게이지 또는 획득 일자
|
||||
if is_earned:
|
||||
earned = QLabel(f" ✓ {item['earned_date']} 달성 ")
|
||||
earned = QLabel(tr('achieve.earned_date', date=item['earned_date']))
|
||||
earned.setStyleSheet(
|
||||
f"color: {theme['border_strong'] if _is_dark() else tc('text')}; "
|
||||
f"font-weight: bold; font-size: 9.5pt; "
|
||||
|
||||
+60
-64
@@ -12,6 +12,7 @@ import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.database import Database
|
||||
from core.i18n import tr
|
||||
from ui.styles import ThemeColors, apply_dark_titlebar
|
||||
|
||||
|
||||
@@ -37,7 +38,7 @@ class CalendarView(QDialog):
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
|
||||
# 제목
|
||||
title = QLabel("월간 근무 기록")
|
||||
title = QLabel(tr('cal.dialog_title'))
|
||||
title.setObjectName("dialog_title")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
@@ -55,8 +56,8 @@ class CalendarView(QDialog):
|
||||
# 범례
|
||||
legend_layout = QHBoxLayout()
|
||||
legend_layout.setSpacing(12)
|
||||
for _color, _txt in [('#51CF66', '정상'), ('#FA5252', '연장'),
|
||||
('#FAB005', '휴가'), ('#6C6E73', '없음')]:
|
||||
for _color, _txt in [('#51CF66', tr('cal.legend_normal')), ('#FA5252', tr('cal.legend_overtime')),
|
||||
('#FAB005', tr('cal.legend_leave')), ('#6C6E73', tr('cal.legend_none'))]:
|
||||
_item = QLabel(f"<span style='color:{_color};'>●</span> {_txt}")
|
||||
_item.setTextFormat(Qt.RichText)
|
||||
legend_layout.addWidget(_item)
|
||||
@@ -64,7 +65,7 @@ class CalendarView(QDialog):
|
||||
layout.addLayout(legend_layout)
|
||||
|
||||
# 선택된 날짜 상세 정보
|
||||
detail_group = QGroupBox("선택된 날짜 정보")
|
||||
detail_group = QGroupBox(tr('cal.detail_group_title'))
|
||||
detail_layout = QVBoxLayout()
|
||||
detail_layout.setSpacing(6)
|
||||
detail_layout.setContentsMargins(10, 20, 10, 8)
|
||||
@@ -78,13 +79,13 @@ class CalendarView(QDialog):
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(6)
|
||||
|
||||
self.edit_time_button = QPushButton("시간 수정")
|
||||
self.edit_time_button = QPushButton(tr('cal.edit_time'))
|
||||
self.edit_time_button.setObjectName("btn_primary")
|
||||
self.edit_time_button.setEnabled(False)
|
||||
self.edit_time_button.clicked.connect(self.edit_work_time)
|
||||
button_layout.addWidget(self.edit_time_button)
|
||||
|
||||
self.delete_record_button = QPushButton("기록 삭제")
|
||||
self.delete_record_button = QPushButton(tr('cal.delete_record'))
|
||||
self.delete_record_button.setObjectName("btn_danger")
|
||||
self.delete_record_button.setEnabled(False)
|
||||
self.delete_record_button.clicked.connect(self.delete_selected_record)
|
||||
@@ -95,17 +96,17 @@ class CalendarView(QDialog):
|
||||
layout.addWidget(detail_group)
|
||||
|
||||
# 메모 그룹
|
||||
memo_group = QGroupBox("메모")
|
||||
memo_group = QGroupBox(tr('cal.memo_group'))
|
||||
memo_layout = QVBoxLayout()
|
||||
memo_layout.setSpacing(6)
|
||||
memo_layout.setContentsMargins(10, 20, 10, 8)
|
||||
|
||||
self.memo_edit = QTextEdit()
|
||||
self.memo_edit.setMaximumHeight(70)
|
||||
self.memo_edit.setPlaceholderText("추가근무 사유, 특이사항 등...")
|
||||
self.memo_edit.setPlaceholderText(tr('cal.memo_placeholder'))
|
||||
memo_layout.addWidget(self.memo_edit)
|
||||
|
||||
self.save_memo_button = QPushButton("메모 저장")
|
||||
self.save_memo_button = QPushButton(tr('cal.save_memo'))
|
||||
self.save_memo_button.setObjectName("btn_primary")
|
||||
self.save_memo_button.setEnabled(False)
|
||||
self.save_memo_button.clicked.connect(self.save_memo)
|
||||
@@ -166,10 +167,10 @@ class CalendarView(QDialog):
|
||||
menu = QMenu(self)
|
||||
edit_action = delete_action = add_action = None
|
||||
if existing:
|
||||
edit_action = menu.addAction(f"{date_str} 편집")
|
||||
delete_action = menu.addAction(f"{date_str} 삭제")
|
||||
edit_action = menu.addAction(tr('cal.context_edit', date=date_str))
|
||||
delete_action = menu.addAction(tr('cal.context_delete', date=date_str))
|
||||
else:
|
||||
add_action = menu.addAction(f"{date_str} 기록 추가")
|
||||
add_action = menu.addAction(tr('cal.context_add', date=date_str))
|
||||
|
||||
action = menu.exec_(self.calendar.mapToGlobal(pos))
|
||||
if action is None:
|
||||
@@ -217,9 +218,9 @@ class CalendarView(QDialog):
|
||||
if ot_earned > 0:
|
||||
self.db.add_overtime_earned(wid, ot_earned, date_str)
|
||||
self._refresh_calendar()
|
||||
QMessageBox.information(self, "추가 완료", f"{date_str} 기록이 추가되었습니다.")
|
||||
QMessageBox.information(self, tr('cal.add_done_title'), tr('cal.add_done_body', date=date_str))
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "오류", f"기록 추가 실패: {e}")
|
||||
QMessageBox.critical(self, tr('cal.add_error_title'), tr('cal.add_error_body', error=e))
|
||||
|
||||
def _open_edit_dialog(self, date_str: str):
|
||||
"""기존 일자 편집 — date_selected로 우회 (이미 EditTimeDialog 있음)."""
|
||||
@@ -231,8 +232,8 @@ class CalendarView(QDialog):
|
||||
|
||||
def _delete_record(self, date_str: str):
|
||||
reply = QMessageBox.question(
|
||||
self, "삭제 확인",
|
||||
f"{date_str} 기록을 정말 삭제하시겠습니까?\n(연장근무 적립 내역도 함께 삭제됩니다)",
|
||||
tr('cal.delete_confirm_title'),
|
||||
tr('cal.delete_confirm_body', date=date_str),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
@@ -245,10 +246,10 @@ class CalendarView(QDialog):
|
||||
cursor.execute("DELETE FROM work_records WHERE date = ?", (date_str,))
|
||||
conn.commit()
|
||||
self._refresh_calendar()
|
||||
QMessageBox.information(self, "삭제 완료", f"{date_str} 기록 삭제됨")
|
||||
QMessageBox.information(self, tr('cal.delete_done_title'), tr('cal.delete_done_body', date=date_str))
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
QMessageBox.critical(self, "오류", str(e))
|
||||
QMessageBox.critical(self, tr('cal.edit_error_title'), str(e))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -270,33 +271,33 @@ class CalendarView(QDialog):
|
||||
|
||||
if record:
|
||||
# 상세 정보 표시
|
||||
detail = f"{selected_date.strftime('%Y년 %m월 %d일')}\n\n"
|
||||
detail += f"출근: {record['clock_in']}\n"
|
||||
detail = tr('cal.detail_date_fmt', year=selected_date.year, month=selected_date.month, day=selected_date.day) + '\n\n'
|
||||
detail += tr('cal.detail_clock_in', time=record['clock_in']) + '\n'
|
||||
|
||||
if record.get('clock_out'):
|
||||
detail += f"퇴근: {record['clock_out']}\n"
|
||||
detail += f"총 근무시간: {record.get('total_hours', 0):.1f}시간\n"
|
||||
detail += tr('cal.detail_clock_out', time=record['clock_out']) + '\n'
|
||||
detail += tr('cal.detail_total_hours', hours=record.get('total_hours', 0)) + '\n'
|
||||
|
||||
if record.get('lunch_break'):
|
||||
detail += f"점심시간: 사용함\n"
|
||||
detail += tr('cal.detail_lunch_used') + '\n'
|
||||
else:
|
||||
detail += f"점심시간: 미사용\n"
|
||||
detail += tr('cal.detail_lunch_unused') + '\n'
|
||||
|
||||
if record.get('dinner_break'):
|
||||
detail += f"저녁시간: 사용함\n"
|
||||
detail += tr('cal.detail_dinner_used') + '\n'
|
||||
else:
|
||||
detail += f"저녁시간: 미사용\n"
|
||||
detail += tr('cal.detail_dinner_unused') + '\n'
|
||||
|
||||
if record.get('overtime_earned', 0) > 0:
|
||||
earned_min = record['overtime_earned']
|
||||
earned_hours = earned_min // 60
|
||||
earned_mins = earned_min % 60
|
||||
detail += f"\n🔥 연장근무 적립: {earned_hours}시간 {earned_mins}분\n"
|
||||
detail += '\n' + tr('cal.detail_overtime_earned', hours=earned_hours, minutes=earned_mins) + '\n'
|
||||
else:
|
||||
detail += f"퇴근: 미기록\n"
|
||||
detail += tr('cal.detail_clock_out_none') + '\n'
|
||||
|
||||
if record.get('memo'):
|
||||
detail += f"\n메모: {record['memo']}\n"
|
||||
detail += '\n' + tr('cal.detail_memo', memo=record['memo']) + '\n'
|
||||
|
||||
self.detail_text.setText(detail)
|
||||
self.edit_time_button.setEnabled(True)
|
||||
@@ -306,7 +307,7 @@ class CalendarView(QDialog):
|
||||
self.memo_edit.setPlainText(record.get('memo', ''))
|
||||
self.save_memo_button.setEnabled(True)
|
||||
else:
|
||||
self.detail_text.setText(f"{selected_date.strftime('%Y년 %m월 %d일')}\n\n기록이 없습니다.")
|
||||
self.detail_text.setText(tr('cal.detail_date_fmt', year=selected_date.year, month=selected_date.month, day=selected_date.day) + '\n\n' + tr('cal.no_record'))
|
||||
self.edit_time_button.setEnabled(False)
|
||||
self.delete_record_button.setEnabled(False)
|
||||
self.memo_edit.setPlainText('')
|
||||
@@ -319,10 +320,8 @@ class CalendarView(QDialog):
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"출근 기록 삭제",
|
||||
f"{self.selected_date_str}의 출근 기록을 삭제하시겠습니까?\n\n"
|
||||
f"※ 연관된 연장근무 적립/사용 기록도 함께 삭제됩니다.\n"
|
||||
f"※ 이 작업은 되돌릴 수 없습니다.",
|
||||
tr('cal.delete_selected_title'),
|
||||
tr('cal.delete_selected_body', date=self.selected_date_str),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
@@ -332,8 +331,8 @@ class CalendarView(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"삭제 완료",
|
||||
f"{self.selected_date_str}의 출근 기록이 삭제되었습니다."
|
||||
tr('cal.delete_done_title'),
|
||||
tr('cal.delete_done_body', date=self.selected_date_str)
|
||||
)
|
||||
|
||||
# 캘린더 새로고침
|
||||
@@ -353,8 +352,8 @@ class CalendarView(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"메모 저장",
|
||||
f"{self.selected_date_str}의 메모가 저장되었습니다."
|
||||
tr('cal.save_memo_title'),
|
||||
tr('cal.save_memo_body', date=self.selected_date_str)
|
||||
)
|
||||
|
||||
# 상세 정보 새로고침
|
||||
@@ -400,7 +399,7 @@ class EditWorkTimeDialog(QDialog):
|
||||
from PyQt5.QtWidgets import QTimeEdit
|
||||
from PyQt5.QtCore import QTime
|
||||
|
||||
self.setWindowTitle("출퇴근 시간 수정")
|
||||
self.setWindowTitle(tr('cal.edit_dialog_title'))
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(420)
|
||||
|
||||
@@ -409,19 +408,19 @@ class EditWorkTimeDialog(QDialog):
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
|
||||
# 제목
|
||||
title = QLabel(f"{self.date_str} 출퇴근 시간 수정")
|
||||
title = QLabel(tr('cal.edit_dialog_subtitle', date=self.date_str))
|
||||
title.setObjectName("dialog_subtitle")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 출근 시간
|
||||
clock_in_layout = QHBoxLayout()
|
||||
clock_in_layout.setSpacing(4)
|
||||
clock_in_label = QLabel("출근:")
|
||||
clock_in_label = QLabel(tr('cal.label_clock_in'))
|
||||
clock_in_label.setObjectName("field_label")
|
||||
clock_in_label.setFixedWidth(40)
|
||||
clock_in_layout.addWidget(clock_in_label)
|
||||
|
||||
clock_in_minus_btn = QPushButton("-30분")
|
||||
clock_in_minus_btn = QPushButton(tr('cal.btn_minus_30'))
|
||||
clock_in_minus_btn.setFixedWidth(55)
|
||||
clock_in_minus_btn.clicked.connect(lambda: self.adjust_time(self.clock_in_edit, -30))
|
||||
clock_in_layout.addWidget(clock_in_minus_btn)
|
||||
@@ -432,7 +431,7 @@ class EditWorkTimeDialog(QDialog):
|
||||
self.clock_in_edit.setTime(clock_in_time)
|
||||
clock_in_layout.addWidget(self.clock_in_edit)
|
||||
|
||||
clock_in_plus_btn = QPushButton("+30분")
|
||||
clock_in_plus_btn = QPushButton(tr('cal.btn_plus_30'))
|
||||
clock_in_plus_btn.setFixedWidth(55)
|
||||
clock_in_plus_btn.clicked.connect(lambda: self.adjust_time(self.clock_in_edit, 30))
|
||||
clock_in_layout.addWidget(clock_in_plus_btn)
|
||||
@@ -441,12 +440,12 @@ class EditWorkTimeDialog(QDialog):
|
||||
# 퇴근 시간
|
||||
clock_out_layout = QHBoxLayout()
|
||||
clock_out_layout.setSpacing(4)
|
||||
clock_out_label = QLabel("퇴근:")
|
||||
clock_out_label = QLabel(tr('cal.label_clock_out'))
|
||||
clock_out_label.setObjectName("field_label")
|
||||
clock_out_label.setFixedWidth(40)
|
||||
clock_out_layout.addWidget(clock_out_label)
|
||||
|
||||
clock_out_minus_btn = QPushButton("-30분")
|
||||
clock_out_minus_btn = QPushButton(tr('cal.btn_minus_30'))
|
||||
clock_out_minus_btn.setFixedWidth(55)
|
||||
clock_out_minus_btn.clicked.connect(lambda: self.adjust_time(self.clock_out_edit, -30))
|
||||
clock_out_layout.addWidget(clock_out_minus_btn)
|
||||
@@ -458,7 +457,7 @@ class EditWorkTimeDialog(QDialog):
|
||||
self.clock_out_edit.setTime(clock_out_time)
|
||||
clock_out_layout.addWidget(self.clock_out_edit)
|
||||
|
||||
clock_out_plus_btn = QPushButton("+30분")
|
||||
clock_out_plus_btn = QPushButton(tr('cal.btn_plus_30'))
|
||||
clock_out_plus_btn.setFixedWidth(55)
|
||||
clock_out_plus_btn.clicked.connect(lambda: self.adjust_time(self.clock_out_edit, 30))
|
||||
clock_out_layout.addWidget(clock_out_plus_btn)
|
||||
@@ -467,27 +466,27 @@ class EditWorkTimeDialog(QDialog):
|
||||
# 점심/저녁 체크박스 - 한 줄에
|
||||
from PyQt5.QtWidgets import QCheckBox
|
||||
check_layout = QHBoxLayout()
|
||||
self.lunch_check = QCheckBox("점심 (1시간)")
|
||||
self.lunch_check = QCheckBox(tr('cal.check_lunch_1h'))
|
||||
self.lunch_check.setChecked(bool(self.record.get('lunch_break', False)))
|
||||
check_layout.addWidget(self.lunch_check)
|
||||
|
||||
self.dinner_check = QCheckBox("저녁 (1시간)")
|
||||
self.dinner_check = QCheckBox(tr('cal.check_dinner_1h'))
|
||||
self.dinner_check.setChecked(bool(self.record.get('dinner_break', False)))
|
||||
check_layout.addWidget(self.dinner_check)
|
||||
layout.addLayout(check_layout)
|
||||
|
||||
# 안내 메시지
|
||||
note = QLabel("※ 수정 시 연장근무 내역이 재계산됩니다.")
|
||||
note = QLabel(tr('cal.edit_note'))
|
||||
note.setObjectName("note_text")
|
||||
layout.addWidget(note)
|
||||
|
||||
# 버튼
|
||||
button_layout = QHBoxLayout()
|
||||
save_button = QPushButton("저장")
|
||||
save_button = QPushButton(tr('btn.save'))
|
||||
save_button.setObjectName("btn_success")
|
||||
save_button.clicked.connect(self.save_changes)
|
||||
|
||||
cancel_button = QPushButton("취소")
|
||||
cancel_button = QPushButton(tr('btn.cancel'))
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addWidget(save_button)
|
||||
@@ -513,8 +512,8 @@ class EditWorkTimeDialog(QDialog):
|
||||
if clock_out <= clock_in:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"시간 오류",
|
||||
"퇴근 시간은 출근 시간보다 늦어야 합니다."
|
||||
tr('cal.time_error_title'),
|
||||
tr('cal.time_error_body')
|
||||
)
|
||||
return
|
||||
|
||||
@@ -597,15 +596,12 @@ class EditWorkTimeDialog(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"수정 완료",
|
||||
f"{self.date_str}의 출퇴근 시간이 수정되었습니다.\n\n"
|
||||
f"출근: {clock_in}\n"
|
||||
f"퇴근: {clock_out}\n"
|
||||
f"점심시간: {'사용' if lunch_break else '미사용'}\n"
|
||||
f"저녁시간: {'사용' if dinner_break else '미사용'}\n"
|
||||
f"외출시간: {break_minutes}분\n"
|
||||
f"총 근무시간: {total_hours:.1f}시간\n"
|
||||
f"연장근무: {overtime_earned}분 적립"
|
||||
tr('cal.edit_done_title'),
|
||||
tr('cal.edit_done_body',
|
||||
date=self.date_str, clock_in=clock_in, clock_out=clock_out,
|
||||
lunch=tr('cal.detail_lunch_used') if lunch_break else tr('cal.detail_lunch_unused'),
|
||||
dinner=tr('cal.detail_dinner_used') if dinner_break else tr('cal.detail_dinner_unused'),
|
||||
break_minutes=break_minutes, total_hours=total_hours, overtime_earned=overtime_earned)
|
||||
)
|
||||
|
||||
self.accept()
|
||||
@@ -613,8 +609,8 @@ class EditWorkTimeDialog(QDialog):
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"오류",
|
||||
f"수정 중 오류가 발생했습니다:\n{str(e)}"
|
||||
tr('cal.edit_error_title'),
|
||||
tr('cal.edit_error_body', error=str(e))
|
||||
)
|
||||
finally:
|
||||
if conn:
|
||||
|
||||
+19
-13
@@ -9,6 +9,8 @@ from typing import List, Tuple
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from core.i18n import tr
|
||||
|
||||
try:
|
||||
import matplotlib
|
||||
from matplotlib.figure import Figure
|
||||
@@ -89,7 +91,7 @@ class _Fallback(QWidget):
|
||||
def make_chart_widget(parent=None) -> QWidget:
|
||||
"""차트가 그려질 빈 캔버스 위젯. matplotlib 없으면 fallback."""
|
||||
if not _MPL:
|
||||
return _Fallback("차트 표시에는 matplotlib가 필요합니다.\npip install matplotlib")
|
||||
return _Fallback(tr('chart.need_matplotlib'))
|
||||
_refresh_chart_colors()
|
||||
widget = QWidget(parent)
|
||||
widget.setStyleSheet(f"background: {_CHART_BG}; border-radius: 8px;")
|
||||
@@ -115,7 +117,7 @@ def draw_daily_hours(widget: QWidget, records: List[dict]) -> None:
|
||||
if not records:
|
||||
ax = fig.add_subplot(111)
|
||||
_apply_dark_axes(ax)
|
||||
ax.text(0.5, 0.5, '기록 없음', ha='center', va='center',
|
||||
ax.text(0.5, 0.5, tr('chart.no_records'), ha='center', va='center',
|
||||
transform=ax.transAxes, color=_CHART_TEXT, fontsize=11)
|
||||
widget._canvas.draw()
|
||||
return
|
||||
@@ -127,10 +129,10 @@ def draw_daily_hours(widget: QWidget, records: List[dict]) -> None:
|
||||
base = [max(h - o, 0) for h, o in zip(hours, overtimes)]
|
||||
|
||||
ax = fig.add_subplot(111)
|
||||
bars_base = ax.bar(dates, base, label='정상', color=_CHART_BAR_NORMAL)
|
||||
bars_ot = ax.bar(dates, overtimes, bottom=base, label='연장',
|
||||
bars_base = ax.bar(dates, base, label=tr('chart.label_normal'), color=_CHART_BAR_NORMAL)
|
||||
bars_ot = ax.bar(dates, overtimes, bottom=base, label=tr('chart.label_overtime'),
|
||||
color=_CHART_BAR_OVERTIME)
|
||||
ax.set_ylabel('시간')
|
||||
ax.set_ylabel(tr('chart.ylabel_hours'))
|
||||
legend = ax.legend(loc='upper left', fontsize=8, facecolor=_CHART_BG,
|
||||
edgecolor=_CHART_GRID, labelcolor=_CHART_TEXT)
|
||||
ax.tick_params(axis='x', labelrotation=45, labelsize=8)
|
||||
@@ -157,9 +159,10 @@ def draw_daily_hours(widget: QWidget, records: List[dict]) -> None:
|
||||
for i, bar in enumerate(bars):
|
||||
if bar.contains(event)[0]:
|
||||
h = hours[i]; ot = overtimes[i]
|
||||
text = f"▼ {full_dates[i]}\n근무 {h:.1f}h"
|
||||
text = tr('chart.hover_text',
|
||||
date=full_dates[i], hours=f"{h:.1f}")
|
||||
if ot > 0:
|
||||
text += f"\n연장 +{ot:.1f}h"
|
||||
text += "\n" + tr('chart.hover_overtime', hours=f"{ot:.1f}")
|
||||
annot.xy = (bar.get_x() + bar.get_width() / 2, bar.get_height() + bar.get_y())
|
||||
annot.set_text(text)
|
||||
annot.set_visible(True)
|
||||
@@ -191,7 +194,7 @@ def draw_clock_in_distribution(widget: QWidget, records: List[dict]) -> None:
|
||||
if not records:
|
||||
ax = fig.add_subplot(111)
|
||||
_apply_dark_axes(ax)
|
||||
ax.text(0.5, 0.5, '기록 없음', ha='center', va='center',
|
||||
ax.text(0.5, 0.5, tr('chart.no_records'), ha='center', va='center',
|
||||
transform=ax.transAxes, color=_CHART_TEXT, fontsize=11)
|
||||
widget._canvas.draw()
|
||||
return
|
||||
@@ -211,7 +214,7 @@ def draw_clock_in_distribution(widget: QWidget, records: List[dict]) -> None:
|
||||
if not minutes_list:
|
||||
ax = fig.add_subplot(111)
|
||||
_apply_dark_axes(ax)
|
||||
ax.text(0.5, 0.5, '기록 없음', ha='center', va='center',
|
||||
ax.text(0.5, 0.5, tr('chart.no_records'), ha='center', va='center',
|
||||
transform=ax.transAxes, color=_CHART_TEXT, fontsize=11)
|
||||
widget._canvas.draw()
|
||||
return
|
||||
@@ -225,12 +228,13 @@ def draw_clock_in_distribution(widget: QWidget, records: List[dict]) -> None:
|
||||
ax.hist(minutes_list, bins=bins, color=_CHART_BAR_NORMAL,
|
||||
edgecolor=_CHART_BG, linewidth=1)
|
||||
avg = sum(minutes_list) / len(minutes_list)
|
||||
avg_time = f"{int(avg//60):02d}:{int(avg%60):02d}"
|
||||
ax.axvline(avg, color=_CHART_AVG_LINE, linestyle='--', linewidth=2,
|
||||
label=f'평균 {int(avg//60):02d}:{int(avg%60):02d}')
|
||||
label=tr('chart.avg_line', time=avg_time))
|
||||
ax.set_xticks([m for m in bins if m % 60 == 0])
|
||||
ax.set_xticklabels([f"{m//60:02d}:00" for m in bins if m % 60 == 0],
|
||||
rotation=45, fontsize=8)
|
||||
ax.set_ylabel('일수')
|
||||
ax.set_ylabel(tr('chart.ylabel_days'))
|
||||
legend = ax.legend(loc='upper right', fontsize=8, facecolor=_CHART_BG,
|
||||
edgecolor=_CHART_GRID, labelcolor=_CHART_TEXT)
|
||||
_apply_dark_axes(ax)
|
||||
@@ -257,11 +261,13 @@ def draw_weekday_avg(widget: QWidget, records: List[dict]) -> None:
|
||||
weekday_counts[d.weekday()] += 1
|
||||
|
||||
avg = [(t / c) if c else 0 for t, c in zip(weekday_totals, weekday_counts)]
|
||||
labels = ['월', '화', '수', '목', '금', '토', '일']
|
||||
labels = [tr('label.weekday_mon'), tr('label.weekday_tue'), tr('label.weekday_wed'),
|
||||
tr('label.weekday_thu'), tr('label.weekday_fri'), tr('label.weekday_sat'),
|
||||
tr('label.weekday_sun')]
|
||||
|
||||
ax = fig.add_subplot(111)
|
||||
colors = [_CHART_BAR_NORMAL] * 5 + [_CHART_BAR_WEEKEND] * 2 # 주말 골드 강조
|
||||
ax.bar(labels, avg, color=colors)
|
||||
ax.set_ylabel('평균 시간')
|
||||
ax.set_ylabel(tr('chart.ylabel_avg_hours'))
|
||||
_apply_dark_axes(ax)
|
||||
widget._canvas.draw()
|
||||
|
||||
@@ -134,8 +134,8 @@ if __name__ == "__main__":
|
||||
dialog = ClockInDialog()
|
||||
if dialog.exec_() == QDialog.Accepted:
|
||||
selected_time = dialog.get_time()
|
||||
print(f"선택된 시간: {selected_time.strftime('%H:%M:%S')}")
|
||||
print(tr('clock_in_dialog.selected', time=selected_time.strftime('%H:%M:%S')))
|
||||
else:
|
||||
print("취소됨")
|
||||
print(tr('clock_in_dialog.cancelled'))
|
||||
|
||||
sys.exit()
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
|
||||
from core.settings_keys import AUTO_BREAK_ON_LOCK, CLOCK_IN_ON_UNLOCK
|
||||
import sqlite3
|
||||
|
||||
|
||||
class LockMonitor:
|
||||
@@ -61,14 +62,23 @@ class LockMonitor:
|
||||
clock_in_str = when.strftime("%H:%M:%S")
|
||||
existing = self.db.get_today_record()
|
||||
if existing:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE work_records SET clock_in = ?, is_manual = 0 WHERE date = ?",
|
||||
(clock_in_str, today),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
with self.db._conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE work_records SET clock_in = ?, is_manual = 0 WHERE date = ?",
|
||||
(clock_in_str, today),
|
||||
)
|
||||
conn.commit()
|
||||
else:
|
||||
self.db.add_work_record(today, clock_in_str)
|
||||
try:
|
||||
self.db.add_work_record(today, clock_in_str)
|
||||
except sqlite3.IntegrityError:
|
||||
# get_today_record()와 add_work_record() 사이 경쟁 조건
|
||||
with self.db._conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE work_records SET clock_in = ?, is_manual = 0 WHERE date = ?",
|
||||
(clock_in_str, today),
|
||||
)
|
||||
conn.commit()
|
||||
w.update_display()
|
||||
|
||||
@@ -10,6 +10,7 @@ from datetime import datetime
|
||||
from core.settings_keys import (
|
||||
HEALTH_CONSECUTIVE_OT_DAYS, WEEKLY_HOURS_THRESHOLD, OVERTIME_THRESHOLD_HOURS,
|
||||
)
|
||||
from core.i18n import tr
|
||||
from utils.debug_log import dlog
|
||||
|
||||
|
||||
@@ -54,12 +55,12 @@ class NotificationOrchestrator:
|
||||
longest = max(closed, key=lambda r: r.get('total_hours') or 0)
|
||||
longest_str = f"{longest['date']} ({longest.get('total_hours', 0):.1f}h)"
|
||||
|
||||
title = "📊 지난주 요약"
|
||||
body = (f"기간: {last_mon} ~ {last_sun}\n"
|
||||
f"총 근무: {total_h:.1f}시간 ({len(closed)}일)\n"
|
||||
f"일 평균: {avg_h:.1f}시간\n"
|
||||
f"연장근무: {ot_h}시간 {ot_m}분\n"
|
||||
f"가장 긴 날: {longest_str}")
|
||||
title = tr('notif.weekly_report.title')
|
||||
body = tr('notif.weekly_report.body',
|
||||
start=last_mon, end=last_sun,
|
||||
total_h=total_h, days=len(closed),
|
||||
avg_h=avg_h, ot_h=ot_h, ot_m=ot_m,
|
||||
longest=longest_str)
|
||||
self.notifier.notification_signal.emit(title, body)
|
||||
self.db.log_notification('system', 'weekly_report')
|
||||
|
||||
@@ -70,13 +71,17 @@ class NotificationOrchestrator:
|
||||
try:
|
||||
from utils.discord_webhook import send, COLOR_BLUE
|
||||
fields = [
|
||||
{"name": "총 근무", "value": f"{total_h:.1f}시간 ({len(closed)}일)", "inline": True},
|
||||
{"name": "일 평균", "value": f"{avg_h:.1f}시간", "inline": True},
|
||||
{"name": "연장근무", "value": f"{ot_h}시간 {ot_m}분", "inline": True},
|
||||
{"name": "가장 긴 날", "value": longest_str, "inline": False},
|
||||
{"name": tr('field.total_work'), "value": tr('field.total_work_value', hours=total_h, days=len(closed)), "inline": True},
|
||||
{"name": tr('field.avg_daily'), "value": tr('field.avg_daily_value', hours=avg_h), "inline": True},
|
||||
{"name": tr('field.overtime'), "value": tr('field.overtime_value', hours=ot_h, minutes=ot_m), "inline": True},
|
||||
{"name": tr('field.longest_day'), "value": longest_str, "inline": False},
|
||||
]
|
||||
ok = send(url, "📊 지난주 요약",
|
||||
f"기간: {last_mon} ~ {last_sun}",
|
||||
ok = send(url, tr('notif.weekly_report.title'),
|
||||
tr('notif.weekly_report.body',
|
||||
start=last_mon, end=last_sun,
|
||||
total_h=total_h, days=len(closed),
|
||||
avg_h=avg_h, ot_h=ot_h, ot_m=ot_m,
|
||||
longest=longest_str),
|
||||
color=COLOR_BLUE, fields=fields)
|
||||
self.db.log_notification('discord', 'weekly_report', success=ok)
|
||||
except Exception as e:
|
||||
@@ -147,8 +152,8 @@ class NotificationOrchestrator:
|
||||
for a in unlocked:
|
||||
self.db.log_notification('system', f'achievement:{a.code}')
|
||||
if notif_on:
|
||||
title = f"{a.badge_icon} 도전과제 달성!"
|
||||
body = f"{a.name}\n{a.description}"
|
||||
title = tr('notif.achievement.title', icon=a.badge_icon)
|
||||
body = tr('notif.achievement.body', name=a.name, description=a.description)
|
||||
self.notifier.notification_signal.emit(title, body)
|
||||
# Discord 통합 push (여러 개면 묶어서)
|
||||
self._discord_achievements(unlocked)
|
||||
@@ -167,8 +172,8 @@ class NotificationOrchestrator:
|
||||
extra = (f"\n... 외 {len(unlocked) - 10}개" if len(unlocked) > 10 else '')
|
||||
ok = discord_webhook.send(
|
||||
url,
|
||||
f"🏆 도전과제 {len(unlocked)}개 달성!",
|
||||
f"새로 잠금 해제된 도전과제 입니다.{extra}",
|
||||
tr('discord.achievement.title', count=len(unlocked)),
|
||||
tr('discord.achievement.body', extra=extra),
|
||||
color=discord_webhook.COLOR_YELLOW,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
+5
-3
@@ -9,6 +9,8 @@ from datetime import datetime, date
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from core.i18n import tr
|
||||
|
||||
|
||||
class GoalWidget(QWidget):
|
||||
"""월간 목표 진행률 표시."""
|
||||
@@ -20,13 +22,13 @@ class GoalWidget(QWidget):
|
||||
layout.setContentsMargins(8, 6, 8, 6)
|
||||
layout.setSpacing(4)
|
||||
|
||||
title = QLabel("이번 달 목표")
|
||||
title = QLabel(tr('goal.title'))
|
||||
title.setStyleSheet("font-weight: bold;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 연장근무 상한
|
||||
ot_row = QHBoxLayout()
|
||||
self.ot_label = QLabel("연장근무:")
|
||||
self.ot_label = QLabel(tr('goal.overtime'))
|
||||
self.ot_label.setFixedWidth(100)
|
||||
self.ot_bar = QProgressBar()
|
||||
self.ot_bar.setTextVisible(True)
|
||||
@@ -37,7 +39,7 @@ class GoalWidget(QWidget):
|
||||
|
||||
# 일평균
|
||||
avg_row = QHBoxLayout()
|
||||
self.avg_label = QLabel("일평균:")
|
||||
self.avg_label = QLabel(tr('goal.avg_daily'))
|
||||
self.avg_label.setFixedWidth(100)
|
||||
self.avg_bar = QProgressBar()
|
||||
self.avg_bar.setTextVisible(True)
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ class HelpView(QDialog):
|
||||
button_layout.setContentsMargins(0, 6, 0, 0)
|
||||
|
||||
# 온보딩 다시 보기 (왼쪽, ghost 스타일)
|
||||
onboarding_button = QPushButton("온보딩 다시 보기")
|
||||
onboarding_button = QPushButton(tr('help.onboarding_button'))
|
||||
onboarding_button.setMinimumHeight(36)
|
||||
onboarding_button.setStyleSheet(button_qss('ghost'))
|
||||
onboarding_button.clicked.connect(self._reopen_onboarding)
|
||||
|
||||
@@ -13,6 +13,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
from PyQt5.QtCore import Qt, QDate
|
||||
from PyQt5.QtGui import QTextCharFormat, QColor, QBrush
|
||||
|
||||
from core.i18n import tr
|
||||
from ui.styles import apply_dark_titlebar
|
||||
|
||||
|
||||
@@ -22,7 +23,7 @@ class LeaveCalendarView(QDialog):
|
||||
def __init__(self, parent=None, db=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
self.setWindowTitle("연차 캘린더")
|
||||
self.setWindowTitle(tr('leave_cal.title'))
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(540, 480)
|
||||
self._build_ui()
|
||||
@@ -37,7 +38,7 @@ class LeaveCalendarView(QDialog):
|
||||
balance = float(self.db.get_setting('leave_balance', '0') or 0)
|
||||
total = float(self.db.get_setting('annual_leave_total', '15') or 15)
|
||||
used = total - balance
|
||||
title = QLabel(f"잔여 {balance:.2f}일 / 총 {total:.0f}일 (사용 {used:.2f}일)")
|
||||
title = QLabel(tr('leave_cal.header', balance=balance, total=total, used=used))
|
||||
title.setStyleSheet("font-weight: bold; font-size: 13px;")
|
||||
header.addWidget(title)
|
||||
header.addStretch()
|
||||
@@ -45,9 +46,9 @@ class LeaveCalendarView(QDialog):
|
||||
|
||||
# 범례 (사용 완료 + 예정 분리)
|
||||
legend = QHBoxLayout()
|
||||
for _color, _txt in [('#51CF66', '종일(1.0)'), ('#FAB005', '반차(0.5)'),
|
||||
('#B197FC', '반반차(0.25)'), ('#4DABF7', '예정'),
|
||||
('#748FFC', '종일+예정')]:
|
||||
for _color, _txt in [('#51CF66', tr('leave_cal.legend_full')), ('#FAB005', tr('leave_cal.legend_half')),
|
||||
('#B197FC', tr('leave_cal.legend_quarter')), ('#4DABF7', tr('leave_cal.legend_planned')),
|
||||
('#748FFC', tr('leave_cal.legend_full_planned'))]:
|
||||
l = QLabel(f"<span style='color:{_color};'>●</span> {_txt}")
|
||||
l.setStyleSheet("padding: 2px 6px;")
|
||||
legend.addWidget(l)
|
||||
@@ -68,7 +69,7 @@ class LeaveCalendarView(QDialog):
|
||||
# 닫기 버튼
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
close_btn = QPushButton("닫기")
|
||||
close_btn = QPushButton(tr('btn.close'))
|
||||
close_btn.clicked.connect(self.close)
|
||||
btn_row.addWidget(close_btn)
|
||||
layout.addLayout(btn_row)
|
||||
@@ -109,12 +110,12 @@ class LeaveCalendarView(QDialog):
|
||||
records = self.db.get_all_leave_records(limit=365)
|
||||
match = [r for r in records if r['date'] == date_str]
|
||||
if not match:
|
||||
self.detail_label.setText(f"{date_str} — 연차 사용 없음")
|
||||
self.detail_label.setText(tr('leave_cal.detail_no_record', date=date_str))
|
||||
return
|
||||
parts = []
|
||||
for r in match:
|
||||
t = r.get('leave_type', 'annual')
|
||||
d = float(r.get('days') or 0)
|
||||
memo = r.get('memo') or ''
|
||||
parts.append(f"{t} {d}일" + (f" ({memo})" if memo else ""))
|
||||
parts.append(tr('leave_cal.detail_memo', type=t, days=d, memo=memo) if memo else tr('leave_cal.detail_label', type=t, days=d))
|
||||
self.detail_label.setText(f"{date_str}: " + ", ".join(parts))
|
||||
|
||||
+13
-14
@@ -90,8 +90,8 @@ class LeaveView(QDialog):
|
||||
cal_button.clicked.connect(self._show_calendar)
|
||||
button_layout.addWidget(cal_button)
|
||||
|
||||
schedule_button = QPushButton("스케줄")
|
||||
schedule_button.setToolTip("휴일 + 연차 + 반복 패턴 통합 보기")
|
||||
schedule_button = QPushButton(tr('view.leave.btn_schedule'))
|
||||
schedule_button.setToolTip(tr('view.leave.schedule_tooltip'))
|
||||
schedule_button.clicked.connect(self._show_schedule)
|
||||
button_layout.addWidget(schedule_button)
|
||||
|
||||
@@ -137,13 +137,13 @@ class LeaveView(QDialog):
|
||||
days = record['days']
|
||||
hours = days * 8
|
||||
if days == 1.0:
|
||||
days_str = "1일"
|
||||
days_str = tr('view.leave.used_1day')
|
||||
elif days == 0.5:
|
||||
days_str = "0.5일 (4시간)"
|
||||
days_str = tr('view.leave.used_half_day')
|
||||
elif hours < 8:
|
||||
days_str = f"{days}일 ({hours}시간)"
|
||||
days_str = tr('view.leave.used_hours_fmt', days=days, hours=hours)
|
||||
else:
|
||||
days_str = f"{days}일"
|
||||
days_str = tr('view.leave.used_days_fmt', days=days)
|
||||
days_item = QTableWidgetItem(days_str)
|
||||
days_item.setTextAlignment(Qt.AlignCenter)
|
||||
days_item.setForeground(QColor(250, 82, 82)) # 사용 = 레드 (#FA5252)
|
||||
@@ -365,17 +365,17 @@ class AddLeaveDialog(QDialog):
|
||||
if date_dt.weekday() in (5, 6): # 토/일
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"주말 등록 불가",
|
||||
"주말에는 연차를 등록할 수 없습니다. (이미 비근무일)"
|
||||
tr('view.leave.weekend_register_forbidden_title'),
|
||||
tr('view.leave.weekend_register_forbidden_body')
|
||||
)
|
||||
return
|
||||
if self.db.is_holiday(date):
|
||||
holiday = self.db.get_holiday(date)
|
||||
name = (holiday or {}).get('name', '공휴일')
|
||||
name = (holiday or {}).get('name', tr('label.holiday_default'))
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"공휴일 등록 불가",
|
||||
f"{date}는 이미 공휴일({name})입니다.\n연차를 차감할 필요가 없습니다."
|
||||
tr('view.leave.holiday_register_forbidden_title'),
|
||||
tr('view.leave.holiday_register_forbidden_body', date=date, name=name)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -385,9 +385,8 @@ class AddLeaveDialog(QDialog):
|
||||
if existing_days + days > 1.0001: # 부동소수점 여유
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"중복 등록 초과",
|
||||
f"{date}에 이미 {existing_days:.2f}일이 등록되어 있어\n"
|
||||
f"추가 {days:.2f}일을 더하면 1일을 초과합니다."
|
||||
tr('view.leave.duplicate_register_title'),
|
||||
tr('view.leave.duplicate_register_body', date=date, existing_days=existing_days, days=days)
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
+299
-285
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -14,6 +14,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QPushButton, QTimeEdit, QMessageBox)
|
||||
from PyQt5.QtCore import QTime
|
||||
|
||||
from core.i18n import tr
|
||||
from ui.styles import apply_dark_titlebar
|
||||
|
||||
|
||||
@@ -35,8 +36,8 @@ class MealTimeDialog(QDialog):
|
||||
self.meal_type = meal_type
|
||||
self._clock_in = clock_in_time
|
||||
self._clock_out = clock_out_time
|
||||
title_kr = '점심' if meal_type == 'lunch' else '저녁'
|
||||
self.setWindowTitle(f"{title_kr} 시간 입력")
|
||||
meal_label = tr('label.lunch_short') if meal_type == 'lunch' else tr('label.dinner_short')
|
||||
self.setWindowTitle(tr('meal.dialog_title', meal=meal_label))
|
||||
self.setModal(True)
|
||||
self.setFixedSize(380, 260)
|
||||
|
||||
@@ -44,10 +45,9 @@ class MealTimeDialog(QDialog):
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(20, 16, 20, 16)
|
||||
|
||||
info_text = (f"{title_kr} 시작·종료 시각을 입력하세요.\n"
|
||||
f"자동 적용된 {default_minutes}분 대신 정확한 시간으로 기록됩니다.")
|
||||
info_text = tr('meal.info_text', meal=meal_label, minutes=default_minutes)
|
||||
if clock_in_time is not None:
|
||||
info_text += f"\n출근 {clock_in_time.strftime('%H:%M')} 이후만 입력 가능."
|
||||
info_text += tr('meal.info_clock_in_limit', time=clock_in_time.strftime('%H:%M'))
|
||||
info = QLabel(info_text)
|
||||
info.setWordWrap(True)
|
||||
info.setStyleSheet("color: #909296; padding-bottom: 6px;")
|
||||
@@ -63,7 +63,7 @@ class MealTimeDialog(QDialog):
|
||||
|
||||
# 시작
|
||||
start_row = QHBoxLayout()
|
||||
start_row.addWidget(QLabel("시작:"))
|
||||
start_row.addWidget(QLabel(tr('meal.label_start')))
|
||||
self.start_edit = QTimeEdit()
|
||||
self.start_edit.setDisplayFormat("HH:mm")
|
||||
self.start_edit.setTime(QTime(default_start_h, 0))
|
||||
@@ -73,7 +73,7 @@ class MealTimeDialog(QDialog):
|
||||
|
||||
# 종료
|
||||
end_row = QHBoxLayout()
|
||||
end_row.addWidget(QLabel("종료:"))
|
||||
end_row.addWidget(QLabel(tr('meal.label_end')))
|
||||
self.end_edit = QTimeEdit()
|
||||
self.end_edit.setDisplayFormat("HH:mm")
|
||||
self.end_edit.setTime(QTime(default_end_h, 0))
|
||||
@@ -92,10 +92,10 @@ class MealTimeDialog(QDialog):
|
||||
# 버튼
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
ok_btn = QPushButton("저장")
|
||||
ok_btn = QPushButton(tr('btn.save'))
|
||||
ok_btn.setObjectName("btn_primary")
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
cancel_btn = QPushButton("취소")
|
||||
cancel_btn = QPushButton(tr('btn.cancel'))
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
btn_row.addWidget(ok_btn)
|
||||
btn_row.addWidget(cancel_btn)
|
||||
@@ -137,24 +137,24 @@ class MealTimeDialog(QDialog):
|
||||
start_dt, end_dt, minutes = self._resolve_meal_window()
|
||||
ok, reason = self._validate_window(start_dt, end_dt, minutes)
|
||||
if not ok:
|
||||
self.preview.setText(f"{reason}")
|
||||
self.preview.setText(reason)
|
||||
self.preview.setStyleSheet("color: #FA5252;")
|
||||
else:
|
||||
self.preview.setText(f"총 {minutes}분")
|
||||
self.preview.setText(tr('meal.preview_total', minutes=minutes))
|
||||
self.preview.setStyleSheet("color: #51CF66; font-weight: bold;")
|
||||
|
||||
def _validate_window(self, start_dt: datetime, end_dt: datetime,
|
||||
minutes: int) -> tuple[bool, str]:
|
||||
"""식사 시각이 출/퇴근 범위와 정합인지 검증."""
|
||||
if minutes <= 0:
|
||||
return False, "시작이 종료보다 늦습니다"
|
||||
return False, tr('meal.error_start_after_end')
|
||||
if minutes > 8 * 60:
|
||||
# 자정 경계 처리 후 8시간 초과면 사용자 실수일 가능성 높음
|
||||
return False, "식사 시간이 8시간을 초과합니다"
|
||||
return False, tr('meal.error_too_long')
|
||||
if self._clock_in is not None and start_dt < self._clock_in:
|
||||
return False, f"출근({self._clock_in.strftime('%H:%M')}) 이전입니다"
|
||||
return False, tr('meal.error_before_clock_in', time=self._clock_in.strftime('%H:%M'))
|
||||
if self._clock_out is not None and end_dt > self._clock_out:
|
||||
return False, f"퇴근({self._clock_out.strftime('%H:%M')}) 이후입니다"
|
||||
return False, tr('meal.error_after_clock_out', time=self._clock_out.strftime('%H:%M'))
|
||||
return True, ""
|
||||
|
||||
def accept(self):
|
||||
@@ -162,7 +162,7 @@ class MealTimeDialog(QDialog):
|
||||
start_dt, end_dt, minutes = self._resolve_meal_window()
|
||||
ok, reason = self._validate_window(start_dt, end_dt, minutes)
|
||||
if not ok:
|
||||
QMessageBox.warning(self, "입력 오류", reason)
|
||||
QMessageBox.warning(self, tr('meal.input_error_title'), reason)
|
||||
return
|
||||
super().accept()
|
||||
|
||||
|
||||
+2
-2
@@ -102,8 +102,8 @@ class MiniWidget(QWidget):
|
||||
qss = ''
|
||||
if qss:
|
||||
menu.setStyleSheet(qss)
|
||||
open_main = menu.addAction("메인 창 열기")
|
||||
close_mini = menu.addAction("미니 위젯 닫기")
|
||||
open_main = menu.addAction(tr('mini.open_main'))
|
||||
close_mini = menu.addAction(tr('mini.close'))
|
||||
action = menu.exec_(event.globalPos())
|
||||
if action == open_main and self.parent_window:
|
||||
self.parent_window.show()
|
||||
|
||||
+40
-63
@@ -32,17 +32,10 @@ WORK_PRESETS = [
|
||||
class WelcomePage(QWizardPage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setTitle("환영합니다!")
|
||||
self.setSubTitle("Clock-out Time Calculator를 처음 사용하시는군요. 5단계로 빠르게 설정하겠습니다.")
|
||||
self.setTitle(tr('onboarding.welcome_title'))
|
||||
self.setSubTitle(tr('onboarding.welcome_subtitle'))
|
||||
layout = QVBoxLayout()
|
||||
intro = QLabel(
|
||||
"이 앱은:\n"
|
||||
"• 컴퓨터 부팅/잠금 해제로 출근 시간 자동 감지\n"
|
||||
"• 30분 단위 연장근무 적립\n"
|
||||
"• 연차·반차·외출 시간 추적\n"
|
||||
"• 매일 퇴근 시간을 1초마다 카운트다운\n\n"
|
||||
"[다음] 버튼을 눌러 시작하세요."
|
||||
)
|
||||
intro = QLabel(tr('onboarding.welcome_intro'))
|
||||
intro.setWordWrap(True)
|
||||
layout.addWidget(intro)
|
||||
self.setLayout(layout)
|
||||
@@ -51,8 +44,8 @@ class WelcomePage(QWizardPage):
|
||||
class WorkPatternPage(QWizardPage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setTitle("근무 패턴")
|
||||
self.setSubTitle("본인의 하루 근무 시간을 선택하세요. 나중에 설정에서 바꿀 수 있습니다.")
|
||||
self.setTitle(tr('onboarding.work_pattern_title'))
|
||||
self.setSubTitle(tr('onboarding.work_pattern_subtitle'))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.button_group = QButtonGroup(self)
|
||||
@@ -127,20 +120,18 @@ class WorkPatternPage(QWizardPage):
|
||||
class ClockInDetectionPage(QWizardPage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setTitle("출근 시간 감지 방식")
|
||||
self.setSubTitle("앱이 출근 시간을 자동으로 어떻게 감지할지 선택하세요.")
|
||||
self.setTitle(tr('onboarding.detection_title'))
|
||||
self.setSubTitle(tr('onboarding.detection_subtitle'))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.option_boot = QRadioButton("PC 부팅 시간 (기본 — 매일 PC를 끄는 경우)")
|
||||
self.option_unlock = QRadioButton("화면 잠금 해제 시간 (PC를 안 끄고 다니는 경우)")
|
||||
self.option_manual = QRadioButton("수동 입력만 (자동 감지 안 함)")
|
||||
self.option_boot = QRadioButton(tr('onboarding.detection_boot'))
|
||||
self.option_unlock = QRadioButton(tr('onboarding.detection_unlock'))
|
||||
self.option_manual = QRadioButton(tr('onboarding.detection_manual'))
|
||||
self.option_boot.setChecked(True)
|
||||
for opt in (self.option_boot, self.option_unlock, self.option_manual):
|
||||
layout.addWidget(opt)
|
||||
|
||||
info = QLabel(
|
||||
"\nPC를 항상 켜둔 채 출근하시는 분은 두 번째 옵션을 권장합니다."
|
||||
)
|
||||
info = QLabel(tr('onboarding.detection_info'))
|
||||
info.setWordWrap(True)
|
||||
info.setStyleSheet("color: #909296; padding: 8px;")
|
||||
layout.addWidget(info)
|
||||
@@ -159,35 +150,35 @@ class ClockInDetectionPage(QWizardPage):
|
||||
class LeaveSalaryPage(QWizardPage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setTitle("연차 + 급여 (옵션)")
|
||||
self.setSubTitle("연차 일수와 급여(선택)를 입력하세요.")
|
||||
self.setTitle(tr('onboarding.leave_salary_title'))
|
||||
self.setSubTitle(tr('onboarding.leave_salary_subtitle'))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
# 연차
|
||||
leave_box = QGroupBox("연간 연차")
|
||||
leave_box = QGroupBox(tr('onboarding.leave_group'))
|
||||
leave_layout = QHBoxLayout()
|
||||
self.leave_spin = QSpinBox()
|
||||
self.leave_spin.setRange(0, 30)
|
||||
self.leave_spin.setValue(15)
|
||||
self.leave_spin.setSuffix(" 일")
|
||||
leave_layout.addWidget(QLabel("내 연차:"))
|
||||
self.leave_spin.setSuffix(tr('label.unit_day'))
|
||||
leave_layout.addWidget(QLabel(tr('onboarding.my_leave')))
|
||||
leave_layout.addWidget(self.leave_spin)
|
||||
leave_layout.addStretch()
|
||||
leave_box.setLayout(leave_layout)
|
||||
layout.addWidget(leave_box)
|
||||
|
||||
# 급여 (옵션)
|
||||
salary_box = QGroupBox("급여 추정 (옵션 — 포괄임금이면 비활성)")
|
||||
salary_box = QGroupBox(tr('onboarding.salary_group'))
|
||||
salary_layout = QVBoxLayout()
|
||||
self.salary_enabled = QCheckBox("급여 추정 활성화")
|
||||
self.salary_enabled = QCheckBox(tr('onboarding.salary_enabled'))
|
||||
salary_layout.addWidget(self.salary_enabled)
|
||||
|
||||
wage_row = QHBoxLayout()
|
||||
wage_row.addWidget(QLabel("시급:"))
|
||||
wage_row.addWidget(QLabel(tr('onboarding.hourly_wage')))
|
||||
self.wage_spin = QSpinBox()
|
||||
self.wage_spin.setRange(0, 1000000)
|
||||
self.wage_spin.setSingleStep(1000)
|
||||
self.wage_spin.setSuffix(" 원/시간")
|
||||
self.wage_spin.setSuffix(tr('onboarding.wage_suffix'))
|
||||
self.wage_spin.setValue(0)
|
||||
self.wage_spin.setEnabled(False)
|
||||
wage_row.addWidget(self.wage_spin)
|
||||
@@ -195,11 +186,11 @@ class LeaveSalaryPage(QWizardPage):
|
||||
salary_layout.addLayout(wage_row)
|
||||
|
||||
rate_row = QHBoxLayout()
|
||||
rate_row.addWidget(QLabel("연장수당 가산률:"))
|
||||
rate_row.addWidget(QLabel(tr('onboarding.overtime_rate')))
|
||||
self.rate_combo = QComboBox()
|
||||
self.rate_combo.addItem("1.0배 (가산 없음)", 1.0)
|
||||
self.rate_combo.addItem("1.5배 (한국 노동법 기본)", 1.5)
|
||||
self.rate_combo.addItem("2.0배 (야근/휴일 가산)", 2.0)
|
||||
self.rate_combo.addItem(tr('onboarding.rate_1x'), 1.0)
|
||||
self.rate_combo.addItem(tr('onboarding.rate_1_5x'), 1.5)
|
||||
self.rate_combo.addItem(tr('onboarding.rate_2x'), 2.0)
|
||||
self.rate_combo.setCurrentIndex(1)
|
||||
self.rate_combo.setEnabled(False)
|
||||
rate_row.addWidget(self.rate_combo)
|
||||
@@ -218,30 +209,25 @@ class LeaveSalaryPage(QWizardPage):
|
||||
class DiscordPage(QWizardPage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setTitle("Discord 알림 (선택)")
|
||||
self.setSubTitle("출퇴근 시각·휴식 권고를 Discord로 받으려면 웹훅 URL을 입력하세요. (모바일에서 푸시 알림)")
|
||||
self.setTitle(tr('onboarding.discord_title'))
|
||||
self.setSubTitle(tr('onboarding.discord_subtitle'))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.enable_check = QCheckBox("Discord 웹훅 알림 사용")
|
||||
self.enable_check = QCheckBox(tr('onboarding.discord_enable'))
|
||||
layout.addWidget(self.enable_check)
|
||||
|
||||
self.url_edit = QLineEdit()
|
||||
self.url_edit.setPlaceholderText("https://discord.com/api/webhooks/...")
|
||||
self.url_edit.setPlaceholderText(tr('onboarding.discord_url_placeholder'))
|
||||
self.url_edit.setEnabled(False)
|
||||
layout.addWidget(self.url_edit)
|
||||
|
||||
guide = QLabel(
|
||||
"셋업 방법:\n"
|
||||
"1. Discord 서버에서 채널 우클릭 → 편집 → 연동 → 웹훅\n"
|
||||
"2. 새 웹훅 만들기 → URL 복사\n"
|
||||
"3. 위 입력란에 붙여넣기"
|
||||
)
|
||||
guide = QLabel(tr('onboarding.discord_guide'))
|
||||
guide.setStyleSheet("color: #909296; padding: 6px;")
|
||||
guide.setWordWrap(True)
|
||||
layout.addWidget(guide)
|
||||
|
||||
test_row = QHBoxLayout()
|
||||
self.test_btn = QPushButton("테스트 메시지 보내기")
|
||||
self.test_btn = QPushButton(tr('onboarding.discord_test'))
|
||||
self.test_btn.setEnabled(False)
|
||||
self.test_btn.clicked.connect(self._test_webhook)
|
||||
test_row.addWidget(self.test_btn)
|
||||
@@ -257,39 +243,30 @@ class DiscordPage(QWizardPage):
|
||||
def _test_webhook(self):
|
||||
url = self.url_edit.text().strip()
|
||||
if not url:
|
||||
QMessageBox.warning(self, "URL 필요", "웹훅 URL을 먼저 입력해주세요.")
|
||||
QMessageBox.warning(self, tr('onboarding.discord_url_required_title'), tr('onboarding.discord_url_required_body'))
|
||||
return
|
||||
from utils import discord_webhook
|
||||
if not discord_webhook.is_valid_webhook_url(url):
|
||||
QMessageBox.warning(
|
||||
self, "URL 형식 오류",
|
||||
"Discord 웹훅 URL 형식이 아닙니다.\n"
|
||||
"예: https://discord.com/api/webhooks/{ID}/{TOKEN}"
|
||||
self, tr('onboarding.discord_url_invalid_title'),
|
||||
tr('onboarding.discord_url_invalid_body')
|
||||
)
|
||||
return
|
||||
ok = discord_webhook.send_test(url)
|
||||
if ok:
|
||||
QMessageBox.information(self, "성공", "Discord 채널에서 테스트 메시지를 확인하세요.")
|
||||
QMessageBox.information(self, tr('onboarding.discord_success'), tr('onboarding.discord_success_body'))
|
||||
else:
|
||||
QMessageBox.warning(self, "실패", "전송 실패. URL을 다시 확인해주세요.")
|
||||
QMessageBox.warning(self, tr('onboarding.discord_failed'), tr('onboarding.discord_failed_body'))
|
||||
|
||||
|
||||
class FinishPage(QWizardPage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setTitle("준비 완료!")
|
||||
self.setSubTitle("이제 출근부터 자동 추적됩니다.")
|
||||
self.setTitle(tr('onboarding.finish_title'))
|
||||
self.setSubTitle(tr('onboarding.finish_subtitle'))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
msg = QLabel(
|
||||
"설정한 내용은 [설정] 메뉴에서 언제든 바꿀 수 있습니다.\n"
|
||||
"온보딩을 다시 보고 싶으면 [도움말 → 온보딩 다시 보기]를 누르세요.\n\n"
|
||||
"단축키:\n"
|
||||
" • Ctrl+O — 출퇴근 토글\n"
|
||||
" • F1 — 도움말\n"
|
||||
" • F5 — 업데이트 확인\n"
|
||||
" • Ctrl+, — 설정"
|
||||
)
|
||||
msg = QLabel(tr('onboarding.finish_msg'))
|
||||
msg.setWordWrap(True)
|
||||
layout.addWidget(msg)
|
||||
self.setLayout(layout)
|
||||
@@ -301,7 +278,7 @@ class OnboardingWizard(QWizard):
|
||||
def __init__(self, db, parent=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
self.setWindowTitle("Clock-out Calculator — 시작 설정")
|
||||
self.setWindowTitle(tr('onboarding.window_title'))
|
||||
self.setMinimumSize(600, 500)
|
||||
self.setWizardStyle(QWizard.ModernStyle)
|
||||
self.setOption(QWizard.NoBackButtonOnStartPage, True)
|
||||
@@ -323,7 +300,7 @@ class OnboardingWizard(QWizard):
|
||||
# 1. 근무 패턴
|
||||
wm, lm, dm = self.work_page.selected_minutes()
|
||||
if wm < 30:
|
||||
QMessageBox.warning(self, "입력 오류", "하루 근무는 최소 30분 이상이어야 합니다.")
|
||||
QMessageBox.warning(self, tr('onboarding.input_error_title'), tr('onboarding.work_min_too_small'))
|
||||
return
|
||||
|
||||
settings = {
|
||||
|
||||
+14
-13
@@ -9,6 +9,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QMessageBox)
|
||||
from PyQt5.QtCore import QTime, Qt
|
||||
|
||||
from core.i18n import tr
|
||||
from ui.styles import apply_dark_titlebar
|
||||
|
||||
|
||||
@@ -18,7 +19,7 @@ class PastRecordDialog(QDialog):
|
||||
def __init__(self, parent=None, date_str: str = ''):
|
||||
super().__init__(parent)
|
||||
self.date_str = date_str
|
||||
self.setWindowTitle(f"기록 추가 — {date_str}")
|
||||
self.setWindowTitle(tr('past_record.dialog_title', date=date_str))
|
||||
self.setModal(True)
|
||||
self.setFixedSize(380, 320)
|
||||
|
||||
@@ -26,13 +27,13 @@ class PastRecordDialog(QDialog):
|
||||
layout.setSpacing(8)
|
||||
layout.setContentsMargins(20, 16, 20, 16)
|
||||
|
||||
info = QLabel(f"{date_str} 근무 기록을 입력하세요.")
|
||||
info = QLabel(tr('past_record.info', date=date_str))
|
||||
info.setStyleSheet("font-weight: bold; padding-bottom: 6px;")
|
||||
layout.addWidget(info)
|
||||
|
||||
# 출근
|
||||
ci_row = QHBoxLayout()
|
||||
ci_row.addWidget(QLabel("출근:"))
|
||||
ci_row.addWidget(QLabel(tr('past_record.label_clock_in')))
|
||||
self.clock_in_edit = QTimeEdit()
|
||||
self.clock_in_edit.setDisplayFormat("HH:mm")
|
||||
self.clock_in_edit.setTime(QTime(9, 0))
|
||||
@@ -42,8 +43,8 @@ class PastRecordDialog(QDialog):
|
||||
|
||||
# 퇴근
|
||||
co_row = QHBoxLayout()
|
||||
co_row.addWidget(QLabel("퇴근:"))
|
||||
self.clock_out_check = QCheckBox("입력")
|
||||
co_row.addWidget(QLabel(tr('past_record.label_clock_out')))
|
||||
self.clock_out_check = QCheckBox(tr('past_record.check_clock_out'))
|
||||
self.clock_out_check.setChecked(True)
|
||||
self.clock_out_edit = QTimeEdit()
|
||||
self.clock_out_edit.setDisplayFormat("HH:mm")
|
||||
@@ -56,18 +57,18 @@ class PastRecordDialog(QDialog):
|
||||
|
||||
# 점심/저녁
|
||||
meal_row = QHBoxLayout()
|
||||
self.lunch_check = QCheckBox("점심시간 포함")
|
||||
self.lunch_check = QCheckBox(tr('past_record.check_lunch'))
|
||||
self.lunch_check.setChecked(True)
|
||||
self.dinner_check = QCheckBox("저녁시간 포함")
|
||||
self.dinner_check = QCheckBox(tr('past_record.check_dinner'))
|
||||
meal_row.addWidget(self.lunch_check)
|
||||
meal_row.addWidget(self.dinner_check)
|
||||
meal_row.addStretch()
|
||||
layout.addLayout(meal_row)
|
||||
|
||||
# 메모
|
||||
layout.addWidget(QLabel("메모 (선택):"))
|
||||
layout.addWidget(QLabel(tr('past_record.label_memo')))
|
||||
self.memo_edit = QLineEdit()
|
||||
self.memo_edit.setPlaceholderText("예: 재택근무 / 외근 / 휴가")
|
||||
self.memo_edit.setPlaceholderText(tr('past_record.memo_placeholder'))
|
||||
layout.addWidget(self.memo_edit)
|
||||
|
||||
layout.addStretch()
|
||||
@@ -75,10 +76,10 @@ class PastRecordDialog(QDialog):
|
||||
# 버튼
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
ok_btn = QPushButton("저장")
|
||||
ok_btn = QPushButton(tr('btn.save'))
|
||||
ok_btn.setObjectName("btn_primary")
|
||||
ok_btn.clicked.connect(self._validate_and_accept)
|
||||
cancel_btn = QPushButton("취소")
|
||||
cancel_btn = QPushButton(tr('btn.cancel'))
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
btn_row.addWidget(ok_btn)
|
||||
btn_row.addWidget(cancel_btn)
|
||||
@@ -92,8 +93,8 @@ class PastRecordDialog(QDialog):
|
||||
ci = self.clock_in_edit.time()
|
||||
co = self.clock_out_edit.time()
|
||||
if co <= ci:
|
||||
QMessageBox.warning(self, "입력 오류",
|
||||
"퇴근 시간이 출근 시간보다 빠르거나 같습니다.")
|
||||
QMessageBox.warning(self, tr('past_record.input_error_title'),
|
||||
tr('past_record.input_error_body'))
|
||||
return
|
||||
self.accept()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
from PyQt5.QtCore import QDate, Qt
|
||||
|
||||
from core.recurring_leaves import describe_pattern
|
||||
from core.i18n import tr
|
||||
from ui.styles import apply_dark_titlebar
|
||||
|
||||
|
||||
@@ -27,7 +28,7 @@ class RecurringLeaveDialog(QDialog):
|
||||
def __init__(self, parent=None, db=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
self.setWindowTitle("반복 연차 관리")
|
||||
self.setWindowTitle(tr('recurring.title'))
|
||||
self.setMinimumSize(540, 480)
|
||||
self._build_ui()
|
||||
self._reload_list()
|
||||
@@ -37,29 +38,29 @@ class RecurringLeaveDialog(QDialog):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# 기존 패턴 목록
|
||||
list_group = QGroupBox("등록된 반복 패턴")
|
||||
list_group = QGroupBox(tr('recurring.list_group'))
|
||||
lg = QVBoxLayout()
|
||||
self.list_widget = QListWidget()
|
||||
self.list_widget.setMinimumHeight(160)
|
||||
lg.addWidget(self.list_widget)
|
||||
del_btn = QPushButton("선택 삭제")
|
||||
del_btn = QPushButton(tr('recurring.btn_delete_selected'))
|
||||
del_btn.clicked.connect(self._delete_selected)
|
||||
lg.addWidget(del_btn)
|
||||
list_group.setLayout(lg)
|
||||
layout.addWidget(list_group)
|
||||
|
||||
# 신규 등록
|
||||
add_group = QGroupBox("신규 패턴 추가")
|
||||
add_group = QGroupBox(tr('recurring.add_group'))
|
||||
ag = QVBoxLayout()
|
||||
|
||||
# 패턴 종류
|
||||
kind_row = QHBoxLayout()
|
||||
kind_row.addWidget(QLabel("주기:"))
|
||||
kind_row.addWidget(QLabel(tr('recurring.label_cycle')))
|
||||
self.kind_group = QButtonGroup(self)
|
||||
self.rb_weekly = QRadioButton("매주")
|
||||
self.rb_weekly = QRadioButton(tr('recurring.weekly'))
|
||||
self.rb_weekly.setChecked(True)
|
||||
self.rb_biweekly = QRadioButton("격주")
|
||||
self.rb_monthly = QRadioButton("매월 N일")
|
||||
self.rb_biweekly = QRadioButton(tr('recurring.biweekly'))
|
||||
self.rb_monthly = QRadioButton(tr('recurring.monthly'))
|
||||
for rb in (self.rb_weekly, self.rb_biweekly, self.rb_monthly):
|
||||
self.kind_group.addButton(rb)
|
||||
kind_row.addWidget(rb)
|
||||
@@ -68,10 +69,10 @@ class RecurringLeaveDialog(QDialog):
|
||||
|
||||
# 요일 체크박스 (weekly/biweekly)
|
||||
wd_row = QHBoxLayout()
|
||||
wd_row.addWidget(QLabel("요일:"))
|
||||
wd_row.addWidget(QLabel(tr('recurring.label_weekday')))
|
||||
self.weekday_checks = []
|
||||
for ko, en in _KO_WEEKDAYS:
|
||||
cb = QCheckBox(ko)
|
||||
cb = QCheckBox(tr(f'label.weekday_{en}'))
|
||||
self.weekday_checks.append((cb, en))
|
||||
wd_row.addWidget(cb)
|
||||
wd_row.addStretch()
|
||||
@@ -79,40 +80,40 @@ class RecurringLeaveDialog(QDialog):
|
||||
|
||||
# 매월 N일
|
||||
month_row = QHBoxLayout()
|
||||
month_row.addWidget(QLabel("매월:"))
|
||||
month_row.addWidget(QLabel(tr('recurring.label_monthly_day')))
|
||||
self.day_of_month = QSpinBox()
|
||||
self.day_of_month.setRange(1, 31)
|
||||
self.day_of_month.setValue(15)
|
||||
self.day_of_month.setSuffix("일")
|
||||
self.day_of_month.setSuffix(tr('recurring.day_suffix'))
|
||||
month_row.addWidget(self.day_of_month)
|
||||
month_row.addStretch()
|
||||
ag.addLayout(month_row)
|
||||
|
||||
# 차감 일수
|
||||
days_row = QHBoxLayout()
|
||||
days_row.addWidget(QLabel("차감:"))
|
||||
days_row.addWidget(QLabel(tr('recurring.label_deduction')))
|
||||
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)
|
||||
self.days_combo.addItem(tr('recurring.deduction_full'), 1.0)
|
||||
self.days_combo.addItem(tr('recurring.deduction_half'), 0.5)
|
||||
self.days_combo.addItem(tr('recurring.deduction_quarter'), 0.25)
|
||||
days_row.addWidget(self.days_combo)
|
||||
days_row.addStretch()
|
||||
ag.addLayout(days_row)
|
||||
|
||||
# 시작/종료 날짜
|
||||
date_row = QHBoxLayout()
|
||||
date_row.addWidget(QLabel("시작:"))
|
||||
date_row.addWidget(QLabel(tr('recurring.label_start')))
|
||||
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("종료:"))
|
||||
date_row.addWidget(QLabel(tr('recurring.label_end')))
|
||||
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 = QCheckBox(tr('recurring.no_end'))
|
||||
self.no_end_check.toggled.connect(
|
||||
lambda v: self.end_edit.setEnabled(not v)
|
||||
)
|
||||
@@ -122,14 +123,14 @@ class RecurringLeaveDialog(QDialog):
|
||||
|
||||
# 메모
|
||||
memo_row = QHBoxLayout()
|
||||
memo_row.addWidget(QLabel("메모:"))
|
||||
memo_row.addWidget(QLabel(tr('recurring.label_memo')))
|
||||
self.memo_edit = QLineEdit()
|
||||
self.memo_edit.setPlaceholderText("예: 육아 단축근무")
|
||||
self.memo_edit.setPlaceholderText(tr('recurring.memo_placeholder'))
|
||||
memo_row.addWidget(self.memo_edit)
|
||||
ag.addLayout(memo_row)
|
||||
|
||||
# 추가 버튼
|
||||
add_btn = QPushButton("추가")
|
||||
add_btn = QPushButton(tr('recurring.btn_add'))
|
||||
add_btn.setObjectName("btn_primary")
|
||||
add_btn.clicked.connect(self._save)
|
||||
ag.addWidget(add_btn)
|
||||
@@ -138,7 +139,7 @@ class RecurringLeaveDialog(QDialog):
|
||||
layout.addWidget(add_group)
|
||||
|
||||
# 닫기
|
||||
close_btn = QPushButton("닫기")
|
||||
close_btn = QPushButton(tr('btn.close'))
|
||||
close_btn.clicked.connect(self.close)
|
||||
layout.addWidget(close_btn)
|
||||
|
||||
@@ -148,7 +149,7 @@ class RecurringLeaveDialog(QDialog):
|
||||
self.list_widget.clear()
|
||||
for r in self.db.get_recurring_leaves():
|
||||
desc = describe_pattern(r['pattern'])
|
||||
end = r.get('end_date') or '무기한'
|
||||
end = r.get('end_date') or tr('recurring.no_end')
|
||||
text = (f"[{r['id']}] {desc} · {r['days']}일 ({r['leave_type']}) "
|
||||
f"· {r['start_date']} ~ {end}")
|
||||
if r.get('memo'):
|
||||
@@ -163,8 +164,8 @@ class RecurringLeaveDialog(QDialog):
|
||||
return
|
||||
rec_id = item.data(Qt.UserRole)
|
||||
reply = QMessageBox.question(
|
||||
self, "삭제 확인",
|
||||
f"이 반복 패턴을 삭제하시겠습니까?\n\n{item.text()}",
|
||||
self, tr('recurring.delete_confirm_title'),
|
||||
tr('recurring.delete_confirm_body', item=item.text()),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
@@ -184,7 +185,7 @@ class RecurringLeaveDialog(QDialog):
|
||||
def _save(self):
|
||||
pattern = self._build_pattern()
|
||||
if not pattern:
|
||||
QMessageBox.warning(self, "입력 오류", "최소 한 개 요일을 선택하세요.")
|
||||
QMessageBox.warning(self, tr('recurring.input_error_title'), tr('recurring.input_error_weekday'))
|
||||
return
|
||||
days = self.days_combo.currentData()
|
||||
leave_type = self.days_combo.currentText().split(' ')[1].strip('()')
|
||||
@@ -193,7 +194,7 @@ class RecurringLeaveDialog(QDialog):
|
||||
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)}")
|
||||
QMessageBox.information(self, tr('recurring.add_done_title'),
|
||||
tr('recurring.add_done_body', pattern=describe_pattern(pattern)))
|
||||
self.memo_edit.clear()
|
||||
self._reload_list()
|
||||
|
||||
+26
-23
@@ -18,6 +18,7 @@ from PyQt5.QtCore import Qt, QDate
|
||||
from PyQt5.QtGui import QTextCharFormat, QColor, QBrush
|
||||
|
||||
from core.recurring_leaves import expand_for_range, describe_pattern
|
||||
from core.i18n import tr
|
||||
from ui.styles import apply_dark_titlebar
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ class ScheduleView(QDialog):
|
||||
def __init__(self, parent=None, db=None):
|
||||
super().__init__(parent)
|
||||
self.db = db
|
||||
self.setWindowTitle("스케줄")
|
||||
self.setWindowTitle(tr('schedule.title'))
|
||||
self.setMinimumSize(820, 560)
|
||||
self._build_ui()
|
||||
self._reload()
|
||||
@@ -49,16 +50,16 @@ class ScheduleView(QDialog):
|
||||
|
||||
# 상단 툴바
|
||||
bar = QHBoxLayout()
|
||||
title = QLabel("월간 스케줄 — 휴일 + 연차 + 반복 패턴")
|
||||
title = QLabel(tr('schedule.header'))
|
||||
title.setStyleSheet("font-weight: bold; font-size: 13px;")
|
||||
bar.addWidget(title)
|
||||
bar.addStretch()
|
||||
|
||||
rec_btn = QPushButton("반복 패턴 관리")
|
||||
rec_btn = QPushButton(tr('schedule.btn_recurring'))
|
||||
rec_btn.clicked.connect(self._open_recurring_dialog)
|
||||
bar.addWidget(rec_btn)
|
||||
|
||||
add_btn = QPushButton("연차 등록")
|
||||
add_btn = QPushButton(tr('schedule.btn_add_leave'))
|
||||
add_btn.clicked.connect(self._open_add_leave_dialog)
|
||||
bar.addWidget(add_btn)
|
||||
|
||||
@@ -66,11 +67,11 @@ class ScheduleView(QDialog):
|
||||
|
||||
# 범례
|
||||
legend = QHBoxLayout()
|
||||
for label, color in [("공휴일", _C_HOLIDAY),
|
||||
("연차 사용", _C_LEAVE_FULL_PAST),
|
||||
("연차 예정", _C_LEAVE_FULL_PLAN),
|
||||
("반차/반반차", _C_LEAVE_HALF_PAST),
|
||||
("반복 패턴", _C_RECURRING)]:
|
||||
for label, color in [(tr('schedule.legend_holiday'), _C_HOLIDAY),
|
||||
(tr('schedule.legend_leave_used'), _C_LEAVE_FULL_PAST),
|
||||
(tr('schedule.legend_leave_planned'), _C_LEAVE_FULL_PLAN),
|
||||
(tr('schedule.legend_half'), _C_LEAVE_HALF_PAST),
|
||||
(tr('schedule.legend_recurring'), _C_RECURRING)]:
|
||||
sw = QLabel(f" {label} ")
|
||||
sw.setStyleSheet(
|
||||
f"background-color: {color.name()}; color: white; "
|
||||
@@ -94,7 +95,7 @@ class ScheduleView(QDialog):
|
||||
right = QWidget()
|
||||
right_layout = QVBoxLayout()
|
||||
|
||||
self.detail_title = QLabel("날짜를 선택하세요")
|
||||
self.detail_title = QLabel(tr('schedule.detail_placeholder'))
|
||||
self.detail_title.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||||
right_layout.addWidget(self.detail_title)
|
||||
|
||||
@@ -109,7 +110,7 @@ class ScheduleView(QDialog):
|
||||
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
close_btn = QPushButton("닫기")
|
||||
close_btn = QPushButton(tr('btn.close'))
|
||||
close_btn.clicked.connect(self.close)
|
||||
layout.addWidget(close_btn)
|
||||
|
||||
@@ -189,18 +190,19 @@ class ScheduleView(QDialog):
|
||||
def _on_date_click(self, qd: QDate):
|
||||
d = date(qd.year(), qd.month(), qd.day())
|
||||
date_str = d.isoformat()
|
||||
weekday_kr = ['월', '화', '수', '목', '금', '토', '일']
|
||||
self.detail_title.setText(f"{date_str} ({weekday_kr[d.weekday()]}요일)")
|
||||
weekday_kr = [tr('label.weekday_mon'), tr('label.weekday_tue'), tr('label.weekday_wed'),
|
||||
tr('label.weekday_thu'), tr('label.weekday_fri'), tr('label.weekday_sat'), tr('label.weekday_sun')]
|
||||
self.detail_title.setText(f"{date_str} ({weekday_kr[d.weekday()]}{tr('schedule.weekday_suffix')})")
|
||||
self.detail_list.clear()
|
||||
|
||||
# 휴일
|
||||
holiday = self.db.get_holiday(date_str) if hasattr(self.db, 'get_holiday') else None
|
||||
if holiday:
|
||||
item = QListWidgetItem(f"공휴일: {holiday.get('name', '공휴일')}")
|
||||
item = QListWidgetItem(tr('schedule.holiday', name=holiday.get('name', tr('label.holiday_default'))))
|
||||
item.setForeground(QBrush(QColor("#e53935")))
|
||||
self.detail_list.addItem(item)
|
||||
elif d.weekday() in (5, 6):
|
||||
item = QListWidgetItem(f"주말 ({weekday_kr[d.weekday()]}요일)")
|
||||
item = QListWidgetItem(tr('schedule.weekend', weekday=weekday_kr[d.weekday()]))
|
||||
self.detail_list.addItem(item)
|
||||
|
||||
# 연차 (구체)
|
||||
@@ -208,7 +210,7 @@ class ScheduleView(QDialog):
|
||||
days = float(r.get('days') or 0)
|
||||
t = r.get('leave_type', '연차')
|
||||
memo = r.get('memo') or ''
|
||||
label = f"{t} {days}일"
|
||||
label = tr('schedule.leave_label', type=t, days=days)
|
||||
if memo:
|
||||
label += f" — {memo}"
|
||||
label += f" [id={r['id']}]"
|
||||
@@ -221,13 +223,14 @@ class ScheduleView(QDialog):
|
||||
from core.recurring_leaves import expand_for_date
|
||||
for occ in expand_for_date(recurring, d):
|
||||
item = QListWidgetItem(
|
||||
f"{describe_pattern(occ.pattern)} · {occ.days}일 ({occ.leave_type})"
|
||||
tr('schedule.recurring_item', pattern=describe_pattern(occ.pattern),
|
||||
days=occ.days, type=occ.leave_type)
|
||||
)
|
||||
item.setData(Qt.UserRole, ('recurring', occ.recurring_id))
|
||||
self.detail_list.addItem(item)
|
||||
|
||||
if self.detail_list.count() == 0:
|
||||
self.detail_list.addItem("일정 없음")
|
||||
self.detail_list.addItem(tr('schedule.no_events'))
|
||||
|
||||
def _on_page_change(self, year: int, month: int):
|
||||
self._reload()
|
||||
@@ -241,7 +244,7 @@ class ScheduleView(QDialog):
|
||||
return
|
||||
kind, _id = data
|
||||
menu = QMenu(self)
|
||||
del_act = menu.addAction("삭제")
|
||||
del_act = menu.addAction(tr('schedule.delete'))
|
||||
chosen = menu.exec_(self.detail_list.viewport().mapToGlobal(pos))
|
||||
if chosen == del_act:
|
||||
self._delete_record(kind, _id)
|
||||
@@ -249,8 +252,8 @@ class ScheduleView(QDialog):
|
||||
def _delete_record(self, kind: str, _id: int):
|
||||
if kind == 'concrete':
|
||||
reply = QMessageBox.question(
|
||||
self, "삭제 확인",
|
||||
"이 연차 기록을 삭제하시겠습니까? (잔액이 자동 복구됩니다.)",
|
||||
self, tr('schedule.delete_leave_confirm_title'),
|
||||
tr('schedule.delete_leave_confirm_body'),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
@@ -261,8 +264,8 @@ class ScheduleView(QDialog):
|
||||
self._on_date_click(d)
|
||||
elif kind == 'recurring':
|
||||
reply = QMessageBox.question(
|
||||
self, "삭제 확인",
|
||||
"이 반복 패턴을 삭제하시겠습니까? (이후 모든 인스턴스 제거)",
|
||||
self, tr('schedule.delete_recurring_confirm_title'),
|
||||
tr('schedule.delete_recurring_confirm_body'),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
|
||||
+182
-201
@@ -58,7 +58,7 @@ class SettingsView(QDialog):
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# 제목
|
||||
title = QLabel("설정")
|
||||
title = QLabel(tr('settings.title'))
|
||||
title.setObjectName("dialog_title")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
main_layout.addWidget(title)
|
||||
@@ -137,16 +137,16 @@ class SettingsView(QDialog):
|
||||
|
||||
# 근무 패턴 프리셋
|
||||
preset_layout = QHBoxLayout()
|
||||
preset_label = QLabel("근무 패턴:")
|
||||
preset_label = QLabel(tr('settings.work_pattern'))
|
||||
preset_label.setFixedWidth(130)
|
||||
self.work_preset_combo = QComboBox()
|
||||
# (label, work_minutes, lunch_minutes)
|
||||
self.work_preset_combo.addItem("표준 8시간 (점심 60분)", (480, 60))
|
||||
self.work_preset_combo.addItem("단축근무 7시간 30분 (점심 30분)", (450, 30))
|
||||
self.work_preset_combo.addItem("단축근무 7시간 (점심 60분)", (420, 60))
|
||||
self.work_preset_combo.addItem("단축근무 6시간 (점심 30분)", (360, 30))
|
||||
self.work_preset_combo.addItem("반일 4시간 (점심 0분)", (240, 0))
|
||||
self.work_preset_combo.addItem("사용자 정의", None)
|
||||
self.work_preset_combo.addItem(tr('settings.preset.standard_8h'), (480, 60))
|
||||
self.work_preset_combo.addItem(tr('settings.preset.short_7h30m'), (450, 30))
|
||||
self.work_preset_combo.addItem(tr('settings.preset.short_7h'), (420, 60))
|
||||
self.work_preset_combo.addItem(tr('settings.preset.short_6h'), (360, 30))
|
||||
self.work_preset_combo.addItem(tr('settings.preset.half_4h'), (240, 0))
|
||||
self.work_preset_combo.addItem(tr('settings.preset.custom'), None)
|
||||
self.work_preset_combo.setFixedWidth(260)
|
||||
self.work_preset_combo.currentIndexChanged.connect(self.on_preset_changed)
|
||||
preset_layout.addWidget(preset_label)
|
||||
@@ -156,18 +156,18 @@ class SettingsView(QDialog):
|
||||
|
||||
# 하루 기본 근무 시간 (시 + 분 분리 입력)
|
||||
work_hours_layout = QHBoxLayout()
|
||||
work_hours_label = QLabel("하루 기본 근무:")
|
||||
work_hours_label = QLabel(tr('settings.daily_work'))
|
||||
work_hours_label.setFixedWidth(130)
|
||||
self.work_hours_spin = QSpinBox()
|
||||
self.work_hours_spin.setRange(0, 12)
|
||||
self.work_hours_spin.setValue(8)
|
||||
self.work_hours_spin.setSuffix(" 시간")
|
||||
self.work_hours_spin.setSuffix(tr('settings.suffix_hour'))
|
||||
self.work_hours_spin.setFixedWidth(100)
|
||||
self.work_minutes_spin = QSpinBox()
|
||||
self.work_minutes_spin.setRange(0, 59)
|
||||
self.work_minutes_spin.setValue(0)
|
||||
self.work_minutes_spin.setSingleStep(15)
|
||||
self.work_minutes_spin.setSuffix(" 분")
|
||||
self.work_minutes_spin.setSuffix(tr('settings.suffix_minute'))
|
||||
self.work_minutes_spin.setFixedWidth(100)
|
||||
# 사용자가 시간/분 직접 변경 시 프리셋을 "사용자 정의"로
|
||||
self.work_hours_spin.valueChanged.connect(self._on_work_time_user_edit)
|
||||
@@ -180,34 +180,34 @@ class SettingsView(QDialog):
|
||||
|
||||
# 점심시간 기본값
|
||||
lunch_layout = QHBoxLayout()
|
||||
lunch_label = QLabel("점심시간 기본:")
|
||||
lunch_label = QLabel(tr('settings.lunch_default'))
|
||||
lunch_label.setFixedWidth(130)
|
||||
self.lunch_spin = QSpinBox()
|
||||
self.lunch_spin.setRange(0, 120)
|
||||
self.lunch_spin.setValue(60)
|
||||
self.lunch_spin.setSingleStep(5)
|
||||
self.lunch_spin.setSuffix(" 분")
|
||||
self.lunch_spin.setSuffix(tr('settings.suffix_minute'))
|
||||
self.lunch_spin.setFixedWidth(110)
|
||||
self.lunch_spin.valueChanged.connect(self._on_work_time_user_edit)
|
||||
lunch_layout.addWidget(lunch_label)
|
||||
lunch_layout.addWidget(self.lunch_spin)
|
||||
|
||||
# 점심시간 자동 적용
|
||||
self.auto_lunch_check = QCheckBox("자동 적용")
|
||||
self.auto_lunch_check.setToolTip("출근 후 4시간 경과 시 자동 적용")
|
||||
self.auto_lunch_check = QCheckBox(tr('settings.auto_apply'))
|
||||
self.auto_lunch_check.setToolTip(tr('settings.auto_apply_tooltip'))
|
||||
lunch_layout.addWidget(self.auto_lunch_check)
|
||||
lunch_layout.addStretch()
|
||||
layout.addLayout(lunch_layout)
|
||||
|
||||
# 저녁시간 기본값
|
||||
dinner_layout = QHBoxLayout()
|
||||
dinner_label = QLabel("저녁시간 기본:")
|
||||
dinner_label = QLabel(tr('settings.dinner_default'))
|
||||
dinner_label.setFixedWidth(130)
|
||||
self.dinner_spin = QSpinBox()
|
||||
self.dinner_spin.setRange(0, 120)
|
||||
self.dinner_spin.setValue(60)
|
||||
self.dinner_spin.setSingleStep(5)
|
||||
self.dinner_spin.setSuffix(" 분")
|
||||
self.dinner_spin.setSuffix(tr('settings.suffix_minute'))
|
||||
self.dinner_spin.setFixedWidth(110)
|
||||
dinner_layout.addWidget(dinner_label)
|
||||
dinner_layout.addWidget(self.dinner_spin)
|
||||
@@ -306,30 +306,30 @@ class SettingsView(QDialog):
|
||||
|
||||
# 알림 체크박스들을 3행으로 배치 (저녁 알림 추가로 5개)
|
||||
check_row1 = QHBoxLayout()
|
||||
self.clock_out_notification_check = QCheckBox("퇴근 30분 전 알림")
|
||||
self.clock_out_notification_check = QCheckBox(tr('settings.notif_clock_out'))
|
||||
self.clock_out_notification_check.setChecked(True)
|
||||
self.lunch_notification_check = QCheckBox("점심시간 등록 알림")
|
||||
self.lunch_notification_check = QCheckBox(tr('settings.notif_lunch'))
|
||||
self.lunch_notification_check.setChecked(True)
|
||||
check_row1.addWidget(self.clock_out_notification_check)
|
||||
check_row1.addWidget(self.lunch_notification_check)
|
||||
layout.addLayout(check_row1)
|
||||
|
||||
check_row2 = QHBoxLayout()
|
||||
self.dinner_notification_check = QCheckBox("저녁시간 등록 알림")
|
||||
self.dinner_notification_check = QCheckBox(tr('settings.notif_dinner'))
|
||||
self.dinner_notification_check.setChecked(True)
|
||||
self.overtime_notification_check = QCheckBox("연장근무 적립 알림")
|
||||
self.overtime_notification_check = QCheckBox(tr('settings.notif_overtime'))
|
||||
self.overtime_notification_check.setChecked(True)
|
||||
check_row2.addWidget(self.dinner_notification_check)
|
||||
check_row2.addWidget(self.overtime_notification_check)
|
||||
layout.addLayout(check_row2)
|
||||
|
||||
check_row3 = QHBoxLayout()
|
||||
self.health_notification_check = QCheckBox("건강 경고 알림")
|
||||
self.health_notification_check = QCheckBox(tr('settings.notif_health'))
|
||||
self.health_notification_check.setChecked(True)
|
||||
self.health_break_notification_check = QCheckBox("휴식 권고 알림")
|
||||
self.health_break_notification_check = QCheckBox(tr('settings.notif_break'))
|
||||
self.health_break_notification_check.setChecked(True)
|
||||
self.health_break_notification_check.setToolTip(
|
||||
"오랜 시간 자리에서 일하면 스트레칭을 권유 (연속 근무 N시간 기준)"
|
||||
tr('settings.notif_break_tooltip')
|
||||
)
|
||||
check_row3.addWidget(self.health_notification_check)
|
||||
check_row3.addWidget(self.health_break_notification_check)
|
||||
@@ -337,75 +337,75 @@ class SettingsView(QDialog):
|
||||
|
||||
# 퇴근 N분 전 알림 시점 설정
|
||||
before_row = QHBoxLayout()
|
||||
before_label = QLabel("퇴근 알림 시점:")
|
||||
before_label = QLabel(tr('settings.notif_before'))
|
||||
before_label.setFixedWidth(110)
|
||||
self.notif_before_spin = QSpinBox()
|
||||
self.notif_before_spin.setRange(1, 120)
|
||||
self.notif_before_spin.setSingleStep(5)
|
||||
self.notif_before_spin.setValue(30)
|
||||
self.notif_before_spin.setSuffix(" 분 전")
|
||||
self.notif_before_spin.setSuffix(' ' + tr('settings.notif_before_spin_suffix'))
|
||||
self.notif_before_spin.setFixedWidth(110)
|
||||
self.notif_before_spin.setToolTip("퇴근 임박 알림이 표시될 시점 (분 단위)")
|
||||
self.notif_before_spin.setToolTip(tr('settings.notif_before_tooltip'))
|
||||
before_row.addWidget(before_label)
|
||||
before_row.addWidget(self.notif_before_spin)
|
||||
before_row.addStretch()
|
||||
layout.addLayout(before_row)
|
||||
|
||||
# === 고급 임계값 (접이식 그룹박스) ===
|
||||
adv_box = QGroupBox("고급 임계값")
|
||||
adv_box = QGroupBox(tr('settings.advanced_thresholds'))
|
||||
adv_box.setCheckable(True)
|
||||
adv_box.setChecked(False) # 기본 접힘
|
||||
adv_box.setToolTip("회사 정책·개인 선호에 맞춰 알림 발생 시점 조정")
|
||||
adv_box.setToolTip(tr('settings.advanced_thresholds_tooltip'))
|
||||
adv_layout = QVBoxLayout()
|
||||
adv_layout.setSpacing(4)
|
||||
|
||||
# 점심 알림 임계 (출근 후 N시간)
|
||||
self.lunch_reminder_spin = self._make_threshold_spin(1, 12, 4, " 시간")
|
||||
self.lunch_reminder_spin.setToolTip("출근 후 N시간 경과 시 점심 미등록 알림")
|
||||
adv_layout.addLayout(self._labeled_row("점심 알림 (출근 +):", self.lunch_reminder_spin))
|
||||
self.lunch_reminder_spin = self._make_threshold_spin(1, 12, 4, tr('settings.suffix_hour'))
|
||||
self.lunch_reminder_spin.setToolTip(tr('settings.lunch_alert_tooltip'))
|
||||
adv_layout.addLayout(self._labeled_row(tr('settings.lunch_alert_after'), self.lunch_reminder_spin))
|
||||
|
||||
# 저녁 알림 임계 (출근 후 N시간)
|
||||
self.dinner_reminder_spin = self._make_threshold_spin(1, 16, 8, " 시간")
|
||||
self.dinner_reminder_spin.setToolTip("출근 후 N시간 경과 시 저녁 미등록 알림")
|
||||
adv_layout.addLayout(self._labeled_row("저녁 알림 (출근 +):", self.dinner_reminder_spin))
|
||||
self.dinner_reminder_spin = self._make_threshold_spin(1, 16, 8, tr('settings.suffix_hour'))
|
||||
self.dinner_reminder_spin.setToolTip(tr('settings.dinner_alert_tooltip'))
|
||||
adv_layout.addLayout(self._labeled_row(tr('settings.dinner_alert_after'), self.dinner_reminder_spin))
|
||||
|
||||
# 연장근무 누적 임계
|
||||
self.overtime_threshold_spin = self._make_threshold_spin(1, 200, 20, " 시간")
|
||||
self.overtime_threshold_spin.setToolTip("연장근무 잔액이 N시간 이상이면 알림")
|
||||
adv_layout.addLayout(self._labeled_row("연장 누적 알림:", self.overtime_threshold_spin))
|
||||
self.overtime_threshold_spin = self._make_threshold_spin(1, 200, 20, tr('settings.suffix_hour'))
|
||||
self.overtime_threshold_spin.setToolTip(tr('settings.overtime_alert_tooltip'))
|
||||
adv_layout.addLayout(self._labeled_row(tr('settings.overtime_alert_at'), self.overtime_threshold_spin))
|
||||
|
||||
# 주 X시간 임계
|
||||
self.weekly_hours_spin = self._make_threshold_spin(20, 168, 52, " 시간")
|
||||
self.weekly_hours_spin.setToolTip("주간 총 근무가 N시간 초과 시 경고 (한국 노동법 기본 52)")
|
||||
adv_layout.addLayout(self._labeled_row("주간 한도 경고:", self.weekly_hours_spin))
|
||||
self.weekly_hours_spin = self._make_threshold_spin(20, 168, 52, tr('settings.suffix_hour'))
|
||||
self.weekly_hours_spin.setToolTip(tr('settings.weekly_limit_tooltip'))
|
||||
adv_layout.addLayout(self._labeled_row(tr('settings.weekly_limit'), self.weekly_hours_spin))
|
||||
|
||||
# 연속 연장근무 일수
|
||||
self.health_consecutive_spin = self._make_threshold_spin(1, 14, 3, " 일")
|
||||
self.health_consecutive_spin.setToolTip("N일 이상 연속 연장근무 시 건강 경고")
|
||||
adv_layout.addLayout(self._labeled_row("연속 연장 경고:", self.health_consecutive_spin))
|
||||
self.health_consecutive_spin = self._make_threshold_spin(1, 14, 3, tr('label.unit_day'))
|
||||
self.health_consecutive_spin.setToolTip(tr('settings.consecutive_ot_tooltip', fallback=''))
|
||||
adv_layout.addLayout(self._labeled_row(tr('settings.consecutive_ot'), self.health_consecutive_spin))
|
||||
|
||||
# 휴식 권고 (연속 근무 시간)
|
||||
self.health_break_hours_spin = self._make_threshold_spin(1, 12, 4, " 시간")
|
||||
self.health_break_hours_spin.setToolTip("연속 근무 N시간 경과 시 스트레칭 권유")
|
||||
adv_layout.addLayout(self._labeled_row("휴식 권고 시점:", self.health_break_hours_spin))
|
||||
self.health_break_hours_spin = self._make_threshold_spin(1, 12, 4, tr('settings.suffix_hour'))
|
||||
self.health_break_hours_spin.setToolTip(tr('settings.break_after_tooltip'))
|
||||
adv_layout.addLayout(self._labeled_row(tr('settings.break_after'), self.health_break_hours_spin))
|
||||
|
||||
adv_box.setLayout(adv_layout)
|
||||
layout.addWidget(adv_box)
|
||||
|
||||
# 시간 형식 + 테마 한 줄에
|
||||
format_row = QHBoxLayout()
|
||||
time_format_label = QLabel("시간 형식:")
|
||||
time_format_label = QLabel(tr('settings.time_format'))
|
||||
time_format_label.setFixedWidth(70)
|
||||
self.time_format_combo = QComboBox()
|
||||
self.time_format_combo.addItem("24시간 (17:30)", "24")
|
||||
self.time_format_combo.addItem("오전/오후 (오후 5:30)", "12")
|
||||
self.time_format_combo.addItem(tr('settings.time_format_24'), "24")
|
||||
self.time_format_combo.addItem(tr('settings.time_format_12'), "12")
|
||||
self.time_format_combo.setFixedWidth(180)
|
||||
|
||||
theme_label = QLabel("테마:")
|
||||
theme_label = QLabel(tr('settings.theme'))
|
||||
theme_label.setFixedWidth(40)
|
||||
self.theme_combo = QComboBox()
|
||||
self.theme_combo.addItem("라이트", "light")
|
||||
self.theme_combo.addItem("다크", "dark")
|
||||
self.theme_combo.addItem(tr('settings.theme_light'), "light")
|
||||
self.theme_combo.addItem(tr('settings.theme_dark'), "dark")
|
||||
self.theme_combo.setFixedWidth(90)
|
||||
self.theme_combo.currentIndexChanged.connect(self.on_theme_changed)
|
||||
|
||||
@@ -419,7 +419,7 @@ class SettingsView(QDialog):
|
||||
|
||||
# 접근성: 글꼴 크기 + 고대비
|
||||
a11y_row = QHBoxLayout()
|
||||
a11y_row.addWidget(QLabel("글꼴 크기:"))
|
||||
a11y_row.addWidget(QLabel(tr('settings.font_scale')))
|
||||
self.font_scale_combo = QComboBox()
|
||||
self.font_scale_combo.addItem("100%", "1.0")
|
||||
self.font_scale_combo.addItem("125%", "1.25")
|
||||
@@ -427,8 +427,8 @@ class SettingsView(QDialog):
|
||||
self.font_scale_combo.setFixedWidth(90)
|
||||
a11y_row.addWidget(self.font_scale_combo)
|
||||
a11y_row.addSpacing(16)
|
||||
self.high_contrast_check = QCheckBox("고대비 모드")
|
||||
self.high_contrast_check.setToolTip("검정 배경 + 노란 텍스트 (시각약자/야간)")
|
||||
self.high_contrast_check = QCheckBox(tr('settings.high_contrast'))
|
||||
self.high_contrast_check.setToolTip(tr('settings.high_contrast_tooltip'))
|
||||
a11y_row.addWidget(self.high_contrast_check)
|
||||
a11y_row.addStretch()
|
||||
layout.addLayout(a11y_row)
|
||||
@@ -436,13 +436,13 @@ class SettingsView(QDialog):
|
||||
# 언어 선택
|
||||
from core.i18n import available_languages, language_label
|
||||
lang_row = QHBoxLayout()
|
||||
lang_label = QLabel("언어 / Language:")
|
||||
lang_label = QLabel(tr('label.language'))
|
||||
lang_label.setFixedWidth(120)
|
||||
self.language_combo = QComboBox()
|
||||
for code in available_languages():
|
||||
self.language_combo.addItem(language_label(code), code)
|
||||
self.language_combo.setFixedWidth(140)
|
||||
self.language_combo.setToolTip("언어 변경은 재시작 후 완전히 적용됩니다.")
|
||||
self.language_combo.setToolTip(tr('group.language_restart_tooltip', fallback=''))
|
||||
lang_row.addWidget(lang_label)
|
||||
lang_row.addWidget(self.language_combo)
|
||||
lang_row.addStretch()
|
||||
@@ -459,16 +459,16 @@ class SettingsView(QDialog):
|
||||
|
||||
# 잔액 + 계산 단위 한 줄
|
||||
top_row = QHBoxLayout()
|
||||
self.current_overtime_label = QLabel("현재 잔액: 계산 중...")
|
||||
self.current_overtime_label = QLabel(tr('settings.current_balance'))
|
||||
self.current_overtime_label.setObjectName("badge_success")
|
||||
top_row.addWidget(self.current_overtime_label)
|
||||
top_row.addStretch()
|
||||
|
||||
unit_label = QLabel("계산 단위:")
|
||||
unit_label = QLabel(tr('settings.calc_unit'))
|
||||
self.overtime_unit_combo = QComboBox()
|
||||
self.overtime_unit_combo.addItem("30분", 30)
|
||||
self.overtime_unit_combo.addItem("1시간", 60)
|
||||
self.overtime_unit_combo.addItem("15분", 15)
|
||||
self.overtime_unit_combo.addItem(tr('view.overtime.minute_30'), 30)
|
||||
self.overtime_unit_combo.addItem(tr('label.time_hours_minutes', hours=1, minutes=0), 60)
|
||||
self.overtime_unit_combo.addItem(tr('view.overtime.minute_0'), 15)
|
||||
self.overtime_unit_combo.setFixedWidth(100)
|
||||
top_row.addWidget(unit_label)
|
||||
top_row.addWidget(self.overtime_unit_combo)
|
||||
@@ -476,28 +476,28 @@ class SettingsView(QDialog):
|
||||
|
||||
# 초기 연장근무 설정
|
||||
initial_overtime_layout = QHBoxLayout()
|
||||
initial_overtime_label = QLabel("기존 연장근무:")
|
||||
initial_overtime_label = QLabel(tr('settings.initial_overtime'))
|
||||
initial_overtime_label.setFixedWidth(100)
|
||||
self.initial_overtime_hours = QSpinBox()
|
||||
self.initial_overtime_hours.setRange(0, 200)
|
||||
self.initial_overtime_hours.setValue(0)
|
||||
self.initial_overtime_hours.setSuffix(" 시간")
|
||||
self.initial_overtime_hours.setSuffix(tr('settings.suffix_hour'))
|
||||
self.initial_overtime_hours.setFixedWidth(110)
|
||||
|
||||
self.initial_overtime_mins = QSpinBox()
|
||||
self.initial_overtime_mins.setRange(0, 59)
|
||||
self.initial_overtime_mins.setValue(0)
|
||||
self.initial_overtime_mins.setSuffix(" 분")
|
||||
self.initial_overtime_mins.setSuffix(tr('settings.suffix_minute'))
|
||||
self.initial_overtime_mins.setFixedWidth(100)
|
||||
|
||||
apply_overtime_btn = QPushButton("적용")
|
||||
apply_overtime_btn = QPushButton(tr('btn.apply'))
|
||||
apply_overtime_btn.setObjectName("btn_small")
|
||||
apply_overtime_btn.setFixedWidth(50)
|
||||
apply_overtime_btn.clicked.connect(self.apply_initial_overtime)
|
||||
|
||||
self.auto_overtime_check = QCheckBox("자동 적립")
|
||||
self.auto_overtime_check = QCheckBox(tr('settings.auto_bank'))
|
||||
self.auto_overtime_check.setChecked(True)
|
||||
self.auto_overtime_check.setToolTip("퇴근 시 연장근무 자동 적립")
|
||||
self.auto_overtime_check.setToolTip(tr('settings.auto_bank_tooltip'))
|
||||
|
||||
initial_overtime_layout.addWidget(initial_overtime_label)
|
||||
initial_overtime_layout.addWidget(self.initial_overtime_hours)
|
||||
@@ -507,7 +507,7 @@ class SettingsView(QDialog):
|
||||
initial_overtime_layout.addWidget(self.auto_overtime_check)
|
||||
layout.addLayout(initial_overtime_layout)
|
||||
|
||||
initial_overtime_note = QLabel("※ 프로그램 사용 전 쌓인 연장근무 시간 (절대값)")
|
||||
initial_overtime_note = QLabel(tr('settings.initial_overtime_note', fallback=''))
|
||||
initial_overtime_note.setObjectName("note_text")
|
||||
layout.addWidget(initial_overtime_note)
|
||||
|
||||
@@ -516,22 +516,22 @@ class SettingsView(QDialog):
|
||||
|
||||
def create_goal_group(self) -> QGroupBox:
|
||||
"""월간 목표 설정 그룹 (0=비활성)."""
|
||||
group = QGroupBox("월간 목표 (0=비활성)")
|
||||
group = QGroupBox(tr('settings.goal_group'))
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(6)
|
||||
|
||||
# 연장근무 상한
|
||||
ot_row = QHBoxLayout()
|
||||
ot_label = QLabel("월 연장근무 상한:")
|
||||
ot_label = QLabel(tr('settings.monthly_ot_cap'))
|
||||
ot_label.setFixedWidth(150)
|
||||
self.goal_ot_h = QSpinBox()
|
||||
self.goal_ot_h.setRange(0, 100)
|
||||
self.goal_ot_h.setSuffix(" 시간")
|
||||
self.goal_ot_h.setSuffix(tr('settings.suffix_hour'))
|
||||
self.goal_ot_h.setFixedWidth(100)
|
||||
self.goal_ot_m = QSpinBox()
|
||||
self.goal_ot_m.setRange(0, 59)
|
||||
self.goal_ot_m.setSingleStep(30)
|
||||
self.goal_ot_m.setSuffix(" 분")
|
||||
self.goal_ot_m.setSuffix(tr('settings.suffix_minute'))
|
||||
self.goal_ot_m.setFixedWidth(90)
|
||||
ot_row.addWidget(ot_label)
|
||||
ot_row.addWidget(self.goal_ot_h)
|
||||
@@ -541,18 +541,18 @@ class SettingsView(QDialog):
|
||||
|
||||
# 일평균 목표
|
||||
avg_row = QHBoxLayout()
|
||||
avg_label = QLabel("일 평균 근무 목표:")
|
||||
avg_label = QLabel(tr('settings.daily_avg_goal'))
|
||||
avg_label.setFixedWidth(150)
|
||||
self.goal_avg = QDoubleSpinBox() if False else QSpinBox() # int*10 방식
|
||||
self.goal_avg.setRange(0, 24)
|
||||
self.goal_avg.setSuffix(" 시간")
|
||||
self.goal_avg.setSuffix(tr('settings.suffix_hour'))
|
||||
self.goal_avg.setFixedWidth(100)
|
||||
avg_row.addWidget(avg_label)
|
||||
avg_row.addWidget(self.goal_avg)
|
||||
avg_row.addStretch()
|
||||
layout.addLayout(avg_row)
|
||||
|
||||
note = QLabel("※ 통계 → 월간 탭에서 진행률 확인")
|
||||
note = QLabel(tr('settings.goal_note', fallback=''))
|
||||
note.setObjectName("note_text")
|
||||
layout.addWidget(note)
|
||||
|
||||
@@ -567,40 +567,40 @@ class SettingsView(QDialog):
|
||||
|
||||
# 연차 개수 + 남은 연차 한 줄
|
||||
top_row = QHBoxLayout()
|
||||
annual_leave_label = QLabel("연간 연차:")
|
||||
annual_leave_label = QLabel(tr('settings.annual_leave'))
|
||||
annual_leave_label.setFixedWidth(70)
|
||||
self.annual_leave_days = QSpinBox()
|
||||
self.annual_leave_days.setRange(0, 30)
|
||||
self.annual_leave_days.setValue(15)
|
||||
self.annual_leave_days.setSuffix(" 일")
|
||||
self.annual_leave_days.setSuffix(tr('label.unit_day'))
|
||||
self.annual_leave_days.setFixedWidth(100)
|
||||
top_row.addWidget(annual_leave_label)
|
||||
top_row.addWidget(self.annual_leave_days)
|
||||
top_row.addStretch()
|
||||
|
||||
self.remaining_leave_label = QLabel("남은 연차: 계산 중...")
|
||||
self.remaining_leave_label = QLabel(tr('settings.remaining_leave'))
|
||||
self.remaining_leave_label.setObjectName("badge_leave")
|
||||
top_row.addWidget(self.remaining_leave_label)
|
||||
layout.addLayout(top_row)
|
||||
|
||||
# 기존 사용 연차 설정
|
||||
used_leave_layout = QHBoxLayout()
|
||||
used_leave_label = QLabel("기존 사용:")
|
||||
used_leave_label = QLabel(tr('settings.used_leave'))
|
||||
used_leave_label.setFixedWidth(70)
|
||||
self.used_leave_hours = QSpinBox()
|
||||
self.used_leave_hours.setRange(0, 200)
|
||||
self.used_leave_hours.setValue(0)
|
||||
self.used_leave_hours.setSuffix(" 시간")
|
||||
self.used_leave_hours.setSuffix(tr('settings.suffix_hour'))
|
||||
self.used_leave_hours.setFixedWidth(110)
|
||||
|
||||
self.used_leave_mins = QSpinBox()
|
||||
self.used_leave_mins.setRange(0, 59)
|
||||
self.used_leave_mins.setValue(0)
|
||||
self.used_leave_mins.setSuffix(" 분")
|
||||
self.used_leave_mins.setSuffix(tr('settings.suffix_minute'))
|
||||
self.used_leave_mins.setSingleStep(30)
|
||||
self.used_leave_mins.setFixedWidth(100)
|
||||
|
||||
apply_used_leave_btn = QPushButton("적용")
|
||||
apply_used_leave_btn = QPushButton(tr('btn.apply'))
|
||||
apply_used_leave_btn.setObjectName("btn_small")
|
||||
apply_used_leave_btn.setFixedWidth(50)
|
||||
apply_used_leave_btn.clicked.connect(self.apply_used_leave)
|
||||
@@ -612,7 +612,7 @@ class SettingsView(QDialog):
|
||||
used_leave_layout.addStretch()
|
||||
layout.addLayout(used_leave_layout)
|
||||
|
||||
used_leave_note = QLabel("※ 프로그램 사용 전 이미 사용한 연차 (1일=8시간)")
|
||||
used_leave_note = QLabel(tr('settings.used_leave_note', fallback=''))
|
||||
used_leave_note.setObjectName("note_text")
|
||||
layout.addWidget(used_leave_note)
|
||||
|
||||
@@ -627,33 +627,33 @@ class SettingsView(QDialog):
|
||||
|
||||
# 공휴일 목록 + 버튼 한 줄
|
||||
button_layout = QHBoxLayout()
|
||||
holiday_list_label = QLabel("등록:")
|
||||
holiday_list_label = QLabel(tr('settings.registered'))
|
||||
button_layout.addWidget(holiday_list_label)
|
||||
|
||||
self.holiday_count_label = QLabel("0개")
|
||||
self.holiday_count_label = QLabel(tr('settings.holiday_count', count=0, year=datetime.now().year))
|
||||
self.holiday_count_label.setObjectName("info_text")
|
||||
button_layout.addWidget(self.holiday_count_label)
|
||||
button_layout.addStretch()
|
||||
|
||||
add_korean_btn = QPushButton("한국 공휴일 (자동)")
|
||||
add_korean_btn = QPushButton(tr('settings.add_korean_holidays'))
|
||||
add_korean_btn.setObjectName("btn_small")
|
||||
add_korean_btn.setToolTip("음력 명절(설/추석) + 임시공휴일 포함 자동 등록")
|
||||
add_korean_btn.setToolTip(tr('settings.add_korean_holidays_tooltip'))
|
||||
add_korean_btn.clicked.connect(self.add_korean_holidays_auto)
|
||||
button_layout.addWidget(add_korean_btn)
|
||||
|
||||
add_custom_btn = QPushButton("추가")
|
||||
add_custom_btn = QPushButton(tr('btn.add'))
|
||||
add_custom_btn.setObjectName("btn_small")
|
||||
add_custom_btn.clicked.connect(self.add_custom_holiday)
|
||||
button_layout.addWidget(add_custom_btn)
|
||||
|
||||
view_holidays_btn = QPushButton("목록")
|
||||
view_holidays_btn = QPushButton(tr('settings.list'))
|
||||
view_holidays_btn.setObjectName("btn_small")
|
||||
view_holidays_btn.clicked.connect(self.view_holidays)
|
||||
button_layout.addWidget(view_holidays_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
holiday_note = QLabel("※ 공휴일 근무 시 모든 시간이 연장근무로 적립됩니다")
|
||||
holiday_note = QLabel(tr('settings.holiday_note', fallback=''))
|
||||
holiday_note.setObjectName("note_text")
|
||||
layout.addWidget(holiday_note)
|
||||
|
||||
@@ -668,7 +668,7 @@ class SettingsView(QDialog):
|
||||
"""공휴일 개수 표시 업데이트"""
|
||||
current_year = datetime.now().year
|
||||
holidays = self.db.get_holidays_by_year(current_year)
|
||||
self.holiday_count_label.setText(f"{len(holidays)}개 ({current_year}년)")
|
||||
self.holiday_count_label.setText(tr('settings.holiday_count', count=len(holidays), year=current_year))
|
||||
|
||||
def add_korean_holidays_auto(self):
|
||||
"""holidays 패키지로 음력/임시 공휴일 포함 자동 추가.
|
||||
@@ -680,19 +680,13 @@ class SettingsView(QDialog):
|
||||
current_year = now.year
|
||||
# 11~12월에 호출 시 다음 해 1월 신정·설 연휴 미리 등록 (연말 자정 경계 대응)
|
||||
include_next = now.month >= 11
|
||||
target_label = (f"{current_year}년 + {current_year + 1}년"
|
||||
if include_next else f"{current_year}년")
|
||||
target_label = (tr('settings.korean_holidays_years_label', start=current_year, end=current_year + 1)
|
||||
if include_next else tr('settings.korean_holidays_years_label_single', year=current_year))
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"한국 공휴일 자동 추가",
|
||||
f"{target_label} 한국 공휴일을 자동으로 등록하시겠습니까?\n\n"
|
||||
"포함:\n"
|
||||
"• 양력 공휴일 (신정/삼일절/어린이날/근로자의 날 등)\n"
|
||||
"• 음력 명절 (설날 연휴/추석 연휴/석가탄신일)\n"
|
||||
"• 정부 지정 대체·임시공휴일\n\n"
|
||||
"※ 1차: 공공데이터포털 특일정보 API (정부 공인, 임시공휴일 포함)\n"
|
||||
"※ 2차 fallback: 'holidays' 패키지 (오프라인)",
|
||||
tr('settings.korean_holidays_title'),
|
||||
tr('settings.korean_holidays_body', years=target_label),
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
@@ -707,18 +701,16 @@ class SettingsView(QDialog):
|
||||
self.update_holiday_count()
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"패키지 미설치",
|
||||
"'holidays' 패키지가 설치되지 않아 고정 공휴일만 추가했습니다.\n\n"
|
||||
"음력/임시공휴일 자동 등록을 원하시면:\n"
|
||||
" pip install holidays"
|
||||
tr('settings.package_not_installed'),
|
||||
tr('settings.package_fallback_body', hint=tr('settings.package_install_hint'))
|
||||
)
|
||||
return
|
||||
|
||||
self.update_holiday_count()
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"추가 완료",
|
||||
f"{current_year}년 한국 공휴일 {added}개가 추가되었습니다."
|
||||
tr('settings.add_done'),
|
||||
tr('settings.korean_holidays_added', year=current_year, count=added)
|
||||
)
|
||||
|
||||
def add_custom_holiday(self):
|
||||
@@ -729,8 +721,8 @@ class SettingsView(QDialog):
|
||||
today = datetime.now().date().isoformat()
|
||||
date_str, ok = QInputDialog.getText(
|
||||
self,
|
||||
"공휴일 추가",
|
||||
"공휴일 날짜를 입력하세요 (YYYY-MM-DD):",
|
||||
tr('settings.holiday_add_title'),
|
||||
tr('settings.holiday_date_prompt'),
|
||||
QLineEdit.Normal,
|
||||
today
|
||||
)
|
||||
@@ -744,16 +736,16 @@ class SettingsView(QDialog):
|
||||
except ValueError:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"입력 오류",
|
||||
"날짜 형식이 잘못되었습니다.\n올바른 형식: YYYY-MM-DD (예: 2024-01-01)"
|
||||
tr('msg.input_error.title'),
|
||||
tr('msg.input_error.date_format')
|
||||
)
|
||||
return
|
||||
|
||||
# 공휴일 이름 입력
|
||||
name, ok = QInputDialog.getText(
|
||||
self,
|
||||
"공휴일 추가",
|
||||
"공휴일 이름을 입력하세요:",
|
||||
tr('settings.holiday_add_title'),
|
||||
tr('settings.holiday_name_prompt'),
|
||||
QLineEdit.Normal,
|
||||
""
|
||||
)
|
||||
@@ -767,8 +759,8 @@ class SettingsView(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"추가 완료",
|
||||
f"공휴일이 추가되었습니다.\n{date_str}: {name}"
|
||||
tr('settings.add_done'),
|
||||
tr('settings.holiday_added', date=date_str, name=name)
|
||||
)
|
||||
|
||||
def view_holidays(self):
|
||||
@@ -779,26 +771,28 @@ class SettingsView(QDialog):
|
||||
if not holidays:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"공휴일 목록",
|
||||
f"{current_year}년에 등록된 공휴일이 없습니다."
|
||||
tr('settings.holiday_list_title'),
|
||||
tr('stats.no_data')
|
||||
)
|
||||
return
|
||||
|
||||
# 목록 생성
|
||||
holiday_list = f"=== {current_year}년 공휴일 목록 ===\n\n"
|
||||
holiday_list = tr('settings.holiday_list_header', year=current_year)
|
||||
for h in holidays:
|
||||
date_obj = datetime.strptime(h['date'], "%Y-%m-%d")
|
||||
weekday = ['월', '화', '수', '목', '금', '토', '일'][date_obj.weekday()]
|
||||
recurring = " (매년)" if h['is_recurring'] else ""
|
||||
holiday_list += f"• {h['date']} ({weekday}): {h['name']}{recurring}\n"
|
||||
weekday = tr(f"label.weekday_{['mon','tue','wed','thu','fri','sat','sun'][date_obj.weekday()]}")
|
||||
recurring = tr('label.recurring_yearly') if h['is_recurring'] else ""
|
||||
holiday_list += tr('settings.holiday_list_item',
|
||||
date=h['date'], weekday=weekday,
|
||||
name=h['name'], recurring=recurring)
|
||||
|
||||
holiday_list += f"\n총 {len(holidays)}개"
|
||||
holiday_list += '\n' + tr('settings.holiday_total', count=len(holidays))
|
||||
|
||||
# 삭제 옵션 제공
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"공휴일 목록",
|
||||
holiday_list + "\n\n공휴일을 삭제하시겠습니까?",
|
||||
tr('settings.holiday_list_title'),
|
||||
holiday_list + tr('settings.holiday_delete_confirm'),
|
||||
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
|
||||
)
|
||||
|
||||
@@ -819,8 +813,8 @@ class SettingsView(QDialog):
|
||||
items = [f"{h['date']}: {h['name']}" for h in holidays]
|
||||
item, ok = QInputDialog.getItem(
|
||||
self,
|
||||
"공휴일 삭제",
|
||||
"삭제할 공휴일을 선택하세요:",
|
||||
tr('settings.holiday_delete_title'),
|
||||
tr('settings.holiday_delete_prompt'),
|
||||
items,
|
||||
0,
|
||||
False
|
||||
@@ -830,7 +824,7 @@ class SettingsView(QDialog):
|
||||
date_str = item.split(":")[0]
|
||||
self.db.delete_holiday_by_date(date_str)
|
||||
self.update_holiday_count()
|
||||
QMessageBox.information(self, "삭제 완료", f"{item}이(가) 삭제되었습니다.")
|
||||
QMessageBox.information(self, tr('settings.delete_done'), tr('settings.holiday_deleted', item=item))
|
||||
|
||||
def create_data_group(self) -> QGroupBox:
|
||||
"""데이터 관리 그룹"""
|
||||
@@ -841,22 +835,22 @@ class SettingsView(QDialog):
|
||||
# CSV 내보내기 버튼들 한 줄
|
||||
export_layout = QHBoxLayout()
|
||||
|
||||
export_work_btn = QPushButton("근무기록")
|
||||
export_work_btn = QPushButton(tr('settings.export_work'))
|
||||
export_work_btn.setObjectName("btn_small")
|
||||
export_work_btn.clicked.connect(self.export_work_records)
|
||||
export_layout.addWidget(export_work_btn)
|
||||
|
||||
export_overtime_btn = QPushButton("연장근무")
|
||||
export_overtime_btn = QPushButton(tr('settings.export_overtime'))
|
||||
export_overtime_btn.setObjectName("btn_small")
|
||||
export_overtime_btn.clicked.connect(self.export_overtime_summary)
|
||||
export_layout.addWidget(export_overtime_btn)
|
||||
|
||||
monthly_btn = QPushButton("월간 요약")
|
||||
monthly_btn = QPushButton(tr('settings.export_monthly'))
|
||||
monthly_btn.setObjectName("btn_small")
|
||||
monthly_btn.clicked.connect(self.export_monthly_summary)
|
||||
export_layout.addWidget(monthly_btn)
|
||||
|
||||
export_label = QLabel("CSV 내보내기")
|
||||
export_label = QLabel(tr('settings.export_csv'))
|
||||
export_label.setObjectName("note_text")
|
||||
export_layout.addWidget(export_label)
|
||||
export_layout.addStretch()
|
||||
@@ -865,12 +859,12 @@ class SettingsView(QDialog):
|
||||
|
||||
# CSV 가져오기
|
||||
import_layout = QHBoxLayout()
|
||||
import_btn = QPushButton("CSV 가져오기")
|
||||
import_btn = QPushButton(tr('settings.import_csv'))
|
||||
import_btn.setObjectName("btn_small")
|
||||
import_btn.setToolTip("date,clock_in,clock_out,lunch_minutes,memo 헤더 포맷")
|
||||
import_btn.setToolTip(tr('settings.import_tooltip'))
|
||||
import_btn.clicked.connect(self._import_csv)
|
||||
import_layout.addWidget(import_btn)
|
||||
import_label = QLabel("우리 표준 포맷 (헤더: date,clock_in,clock_out,lunch_minutes,memo)")
|
||||
import_label = QLabel(tr('settings.import_format'))
|
||||
import_label.setObjectName("note_text")
|
||||
import_layout.addWidget(import_label)
|
||||
import_layout.addStretch()
|
||||
@@ -878,14 +872,14 @@ class SettingsView(QDialog):
|
||||
|
||||
# DB 경로 설정 (클라우드 동기화 가능)
|
||||
db_path_layout = QHBoxLayout()
|
||||
db_path_label = QLabel("DB 경로:")
|
||||
db_path_label = QLabel(tr('settings.db_path_label'))
|
||||
db_path_label.setFixedWidth(60)
|
||||
self.db_path_edit = QLineEdit()
|
||||
self.db_path_edit.setReadOnly(True)
|
||||
self.db_path_edit.setText(self.db.db_path if hasattr(self.db, 'db_path') else 'database.db')
|
||||
db_path_btn = QPushButton("변경...")
|
||||
db_path_btn = QPushButton(tr('settings.change'))
|
||||
db_path_btn.setObjectName("btn_small")
|
||||
db_path_btn.setToolTip("클라우드 폴더(OneDrive/Dropbox 등) 경로로 변경 가능. 재시작 필요.")
|
||||
db_path_btn.setToolTip(tr('settings.db_path_tooltip'))
|
||||
db_path_btn.clicked.connect(self._change_db_path)
|
||||
db_path_layout.addWidget(db_path_label)
|
||||
db_path_layout.addWidget(self.db_path_edit, 1)
|
||||
@@ -893,39 +887,35 @@ class SettingsView(QDialog):
|
||||
layout.addLayout(db_path_layout)
|
||||
|
||||
# 자동 외출 (화면 잠금 시)
|
||||
self.auto_break_check = QCheckBox("화면 잠금 시 자동 외출/복귀")
|
||||
self.auto_break_check.setToolTip("PC가 잠기면 외출 시작, 풀리면 복귀를 자동 처리합니다.")
|
||||
self.auto_break_check = QCheckBox(tr('settings.auto_break_lock'))
|
||||
self.auto_break_check.setToolTip(tr('settings.auto_break_lock_tooltip', fallback=''))
|
||||
layout.addWidget(self.auto_break_check)
|
||||
|
||||
# Gitea 피드백 토큰 (옵션, crash 자동 보고용)
|
||||
feedback_layout = QHBoxLayout()
|
||||
feedback_label = QLabel("Gitea 피드백:")
|
||||
feedback_label = QLabel(tr('settings.gitea_feedback_label', fallback=''))
|
||||
feedback_label.setFixedWidth(80)
|
||||
self.gitea_token_edit = QLineEdit()
|
||||
self.gitea_token_edit.setEchoMode(QLineEdit.Password)
|
||||
self.gitea_token_edit.setPlaceholderText("PAT (issue 쓰기 권한, 옵션)")
|
||||
self.gitea_token_edit.setPlaceholderText(tr('settings.gitea_token_placeholder', fallback=''))
|
||||
feedback_layout.addWidget(feedback_label)
|
||||
feedback_layout.addWidget(self.gitea_token_edit, 1)
|
||||
layout.addLayout(feedback_layout)
|
||||
|
||||
self.gitea_feedback_enabled_check = QCheckBox(
|
||||
"오류 발생 시 'Gitea에 보고' 버튼 활성화"
|
||||
)
|
||||
self.gitea_feedback_enabled_check = QCheckBox(tr('settings.gitea_feedback'))
|
||||
layout.addWidget(self.gitea_feedback_enabled_check)
|
||||
|
||||
# 첫 잠금 해제 = 출근 (PC를 안 끄는 사용자용)
|
||||
self.clock_in_unlock_check = QCheckBox("첫 잠금 해제 시각을 출근시간으로 사용")
|
||||
self.clock_in_unlock_check.setToolTip(
|
||||
"PC를 끄지 않고 출근하는 경우 — 부팅 이벤트가 없어도 화면 잠금 해제 시점을 출근으로 기록합니다."
|
||||
)
|
||||
self.clock_in_unlock_check = QCheckBox(tr('settings.clock_in_unlock'))
|
||||
self.clock_in_unlock_check.setToolTip(tr('settings.clock_in_unlock_tooltip', fallback=''))
|
||||
layout.addWidget(self.clock_in_unlock_check)
|
||||
|
||||
# 업데이트 확인
|
||||
update_layout = QHBoxLayout()
|
||||
from core.version import __version__
|
||||
version_label = QLabel(f"버전: v{__version__}")
|
||||
version_label = QLabel(tr('settings.version', version=__version__))
|
||||
version_label.setObjectName("note_text")
|
||||
update_btn = QPushButton("업데이트 확인 (F5)")
|
||||
update_btn = QPushButton(tr('settings.check_update'))
|
||||
update_btn.setObjectName("btn_small")
|
||||
update_btn.clicked.connect(self._check_updates)
|
||||
update_layout.addWidget(version_label)
|
||||
@@ -941,7 +931,7 @@ class SettingsView(QDialog):
|
||||
current = self.db.db_path if hasattr(self.db, 'db_path') else 'database.db'
|
||||
new_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"데이터베이스 파일 선택",
|
||||
tr('settings.select_db'),
|
||||
current,
|
||||
"SQLite Database (*.db)"
|
||||
)
|
||||
@@ -952,10 +942,8 @@ class SettingsView(QDialog):
|
||||
self.db_path_edit.setText(new_path)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"DB 경로 변경",
|
||||
f"새 경로가 저장되었습니다:\n{new_path}\n\n"
|
||||
"기존 데이터를 사용하려면 현재 database.db 파일을 새 위치로 복사하고\n"
|
||||
"프로그램을 재시작하세요."
|
||||
tr('settings.db_path_label')[:-1],
|
||||
tr('settings.db_path_saved', path=new_path)
|
||||
)
|
||||
|
||||
def load_settings(self):
|
||||
@@ -1105,7 +1093,7 @@ class SettingsView(QDialog):
|
||||
def _import_csv(self):
|
||||
"""CSV 파일에서 근무 기록 일괄 가져오기."""
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self, "CSV 가져오기",
|
||||
self, tr('settings.import_csv'),
|
||||
os.path.expanduser("~"),
|
||||
"CSV files (*.csv);;All files (*.*)",
|
||||
)
|
||||
@@ -1115,19 +1103,17 @@ class SettingsView(QDialog):
|
||||
from utils.csv_importer import parse_csv, import_records
|
||||
rows = parse_csv(path)
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
QMessageBox.critical(self, "파싱 실패", str(e))
|
||||
QMessageBox.critical(self, tr('settings.parse_failed'), str(e))
|
||||
return
|
||||
|
||||
if not rows:
|
||||
QMessageBox.information(self, "빈 파일", "유효한 행이 없습니다.")
|
||||
QMessageBox.information(self, tr('settings.empty_file'), tr('settings.empty_file_body'))
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"충돌 처리",
|
||||
f"{len(rows)}건의 행을 가져오겠습니다.\n\n"
|
||||
"기존 일자와 충돌하면 어떻게 처리할까요?\n"
|
||||
"Yes = 덮어쓰기\nNo = 건너뛰기\nCancel = 취소",
|
||||
tr('settings.conflict_title'),
|
||||
tr('settings.import_rows_intro', count=len(rows)) + tr('settings.conflict_body_detailed'),
|
||||
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
|
||||
)
|
||||
if reply == QMessageBox.Cancel:
|
||||
@@ -1137,12 +1123,12 @@ class SettingsView(QDialog):
|
||||
try:
|
||||
added, updated, skipped = import_records(self.db, rows, on_conflict=policy)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "가져오기 실패", str(e))
|
||||
QMessageBox.critical(self, tr('settings.import_failed'), str(e))
|
||||
return
|
||||
|
||||
QMessageBox.information(
|
||||
self, "완료",
|
||||
f"가져오기 결과:\n• 추가: {added}건\n• 갱신: {updated}건\n• 건너뜀: {skipped}건"
|
||||
self, tr('settings.import_complete'),
|
||||
tr('settings.import_result', added=added, updated=updated, skipped=skipped)
|
||||
)
|
||||
|
||||
def _check_updates(self):
|
||||
@@ -1235,8 +1221,8 @@ class SettingsView(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"저장 완료",
|
||||
"설정이 저장되었습니다."
|
||||
tr('settings.save_done'),
|
||||
tr('settings.save_done_body')
|
||||
)
|
||||
|
||||
# 부모 윈도우에 설정 변경 알림
|
||||
@@ -1252,9 +1238,8 @@ class SettingsView(QDialog):
|
||||
set_language_and_retranslate(new_lang)
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"재시작 / Restart",
|
||||
"주요 화면은 즉시 적용됩니다. 일부 다이얼로그는 재시작 후 완전히 반영됩니다.\n지금 재시작할까요?\n\n"
|
||||
"Main UI updates immediately. Some dialogs need a restart for full effect.\nRestart now?",
|
||||
tr('settings.restart_title'),
|
||||
tr('settings.restart_body'),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
@@ -1286,10 +1271,8 @@ class SettingsView(QDialog):
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"기존 연장근무 설정",
|
||||
f"현재 설정: {old_hours}시간 {old_mins}분\n"
|
||||
f"변경할 값: {hours}시간 {mins}분\n\n"
|
||||
f"기존 연장근무 시간을 변경하시겠습니까?",
|
||||
tr('settings.initial_overtime_title'),
|
||||
tr('settings.initial_overtime_body', old_hours=old_hours, old_mins=old_mins, hours=hours, mins=mins),
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
@@ -1299,8 +1282,8 @@ class SettingsView(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"설정 완료",
|
||||
f"기존 연장근무가 {hours}시간 {mins}분으로 설정되었습니다."
|
||||
tr('settings.initial_overtime_done'),
|
||||
tr('settings.initial_overtime_done_body', hours=hours, mins=mins)
|
||||
)
|
||||
|
||||
# 부모 윈도우 잔액 업데이트
|
||||
@@ -1312,8 +1295,8 @@ class SettingsView(QDialog):
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"오류",
|
||||
f"기존 연장근무 설정 중 오류가 발생했습니다:\n{str(e)}"
|
||||
tr('settings.error'),
|
||||
tr('settings.initial_overtime_error', error=str(e))
|
||||
)
|
||||
|
||||
def update_overtime_balance_display(self):
|
||||
@@ -1321,7 +1304,7 @@ class SettingsView(QDialog):
|
||||
balance_minutes = self.db.get_total_overtime_balance()
|
||||
hours = balance_minutes // 60
|
||||
minutes = balance_minutes % 60
|
||||
self.current_overtime_label.setText(f"현재 잔액: {hours}시간 {minutes}분 ({balance_minutes}분)")
|
||||
self.current_overtime_label.setText(tr('settings.current_overtime_balance', hours=hours, minutes=minutes, balance=balance_minutes))
|
||||
|
||||
def update_remaining_leave(self):
|
||||
"""남은 연차 계산 및 표시"""
|
||||
@@ -1347,7 +1330,7 @@ class SettingsView(QDialog):
|
||||
remaining = total_annual - total_used
|
||||
|
||||
self.remaining_leave_label.setText(
|
||||
f"남은 연차: {remaining:.1f}일 (총 {total_annual}일 중 {total_used:.1f}일 사용)"
|
||||
tr('settings.remaining_leave_fmt', remaining=remaining, total=total_annual, used=total_used)
|
||||
)
|
||||
|
||||
def export_work_records(self):
|
||||
@@ -1358,14 +1341,14 @@ class SettingsView(QDialog):
|
||||
records = stats.get('records', [])
|
||||
|
||||
if not records:
|
||||
QMessageBox.warning(self, "내보내기 실패", "내보낼 기록이 없습니다.")
|
||||
QMessageBox.warning(self, tr('settings.export_failed'), tr('settings.export_no_records'))
|
||||
return
|
||||
|
||||
# 파일 경로 선택
|
||||
default_filename = f"work_records_{now.year}{now.month:02d}.csv"
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"근무 기록 저장",
|
||||
tr('settings.save_work_title'),
|
||||
default_filename,
|
||||
"CSV Files (*.csv)"
|
||||
)
|
||||
@@ -1375,17 +1358,17 @@ class SettingsView(QDialog):
|
||||
saved_path = CSVExporter.export_work_records(records, filename, db=self.db)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"내보내기 완료",
|
||||
f"근무 기록이 저장되었습니다.\n{saved_path}"
|
||||
tr('settings.export_done'),
|
||||
tr('settings.work_exported', path=saved_path)
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "내보내기 실패", f"오류: {str(e)}")
|
||||
QMessageBox.critical(self, tr('settings.export_failed'), tr('settings.export_error', error=str(e)))
|
||||
|
||||
def export_overtime_summary(self):
|
||||
"""연장근무 내역 내보내기"""
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"연장근무 내역 저장",
|
||||
tr('settings.save_ot_title'),
|
||||
f"overtime_summary_{datetime.now().strftime('%Y%m%d')}.csv",
|
||||
"CSV Files (*.csv)"
|
||||
)
|
||||
@@ -1395,18 +1378,18 @@ class SettingsView(QDialog):
|
||||
saved_path = CSVExporter.export_overtime_summary(self.db, filename)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"내보내기 완료",
|
||||
f"연장근무 내역이 저장되었습니다.\n{saved_path}"
|
||||
tr('settings.export_done'),
|
||||
tr('settings.ot_exported', path=saved_path)
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "내보내기 실패", f"오류: {str(e)}")
|
||||
QMessageBox.critical(self, tr('settings.export_failed'), tr('settings.export_error', error=str(e)))
|
||||
|
||||
def export_monthly_summary(self):
|
||||
"""월간 요약 내보내기"""
|
||||
now = datetime.now()
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"월간 요약 저장",
|
||||
tr('settings.save_monthly_title'),
|
||||
f"monthly_summary_{now.year}{now.month:02d}.csv",
|
||||
"CSV Files (*.csv)"
|
||||
)
|
||||
@@ -1416,11 +1399,11 @@ class SettingsView(QDialog):
|
||||
saved_path = CSVExporter.export_monthly_summary(self.db, now.year, now.month, filename)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"내보내기 완료",
|
||||
f"월간 요약이 저장되었습니다.\n{saved_path}"
|
||||
tr('settings.export_done'),
|
||||
tr('settings.monthly_exported', path=saved_path)
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "내보내기 실패", f"오류: {str(e)}")
|
||||
QMessageBox.critical(self, tr('settings.export_failed'), tr('settings.export_error', error=str(e)))
|
||||
|
||||
def apply_used_leave(self):
|
||||
"""기존 사용 연차 설정 (프로그램 사용 전 이미 사용한 연차 - 절대값)"""
|
||||
@@ -1435,10 +1418,8 @@ class SettingsView(QDialog):
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"기존 사용 연차 설정",
|
||||
f"현재 설정: {old_hours}시간 {old_mins}분\n"
|
||||
f"변경할 값: {hours}시간 {mins}분\n\n"
|
||||
f"기존 사용 연차를 변경하시겠습니까?",
|
||||
tr('settings.initial_leave_title'),
|
||||
tr('settings.initial_leave_body', old_hours=old_hours, old_mins=old_mins, hours=hours, mins=mins),
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
@@ -1448,8 +1429,8 @@ class SettingsView(QDialog):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"설정 완료",
|
||||
f"기존 사용 연차가 {hours}시간 {mins}분으로 설정되었습니다."
|
||||
tr('settings.initial_leave_done'),
|
||||
tr('settings.initial_leave_done_body', hours=hours, mins=mins)
|
||||
)
|
||||
|
||||
# 남은 연차 재계산
|
||||
|
||||
+31
-29
@@ -93,13 +93,13 @@ class StatsView(QDialog):
|
||||
# 카드 4개 가로 배치 (총근무 / 출근일 / 평균 / 연장)
|
||||
cards_row = QHBoxLayout()
|
||||
cards_row.setSpacing(10)
|
||||
self.weekly_total_card = build_stat_card("총 근무 시간", "0시간", "이번 주",
|
||||
self.weekly_total_card = build_stat_card(tr('stats.total_work_hours'), tr('stats.value_hours', hours=0), tr('stats.this_week'),
|
||||
theme='blue', icon='clock')
|
||||
self.weekly_days_card = build_stat_card("근무 일수", "0일", "이번 주",
|
||||
self.weekly_days_card = build_stat_card(tr('stats.card_work_days'), tr('stats.value_days', days=0), tr('stats.this_week'),
|
||||
theme='cyan', icon='calendar')
|
||||
self.weekly_avg_card = build_stat_card("일평균", "0시간", "이번 주",
|
||||
self.weekly_avg_card = build_stat_card(tr('stats.card_avg_hours'), tr('stats.value_hours', hours=0), tr('stats.this_week'),
|
||||
theme='green', icon='chart')
|
||||
self.weekly_ot_card = build_stat_card("연장근무", "0시간 0분", "이번 주",
|
||||
self.weekly_ot_card = build_stat_card(tr('stats.card_overtime'), tr('stats.value_hours_minutes', hours=0, minutes=0), tr('stats.this_week'),
|
||||
theme='gold', icon='flame')
|
||||
for c in (self.weekly_total_card, self.weekly_days_card,
|
||||
self.weekly_avg_card, self.weekly_ot_card):
|
||||
@@ -109,7 +109,7 @@ class StatsView(QDialog):
|
||||
# 주간 차트 (일별 근무시간) — 카드 안에
|
||||
from ui.chart_widget import make_chart_widget
|
||||
self.weekly_chart = make_chart_widget(widget)
|
||||
chart_card = build_section_card("일별 근무 시간", self.weekly_chart,
|
||||
chart_card = build_section_card(tr('stats.daily_work_hours'), self.weekly_chart,
|
||||
theme='gray', icon='trending-up')
|
||||
layout.addWidget(chart_card, 1)
|
||||
|
||||
@@ -127,13 +127,13 @@ class StatsView(QDialog):
|
||||
# 카드 4개
|
||||
cards_row = QHBoxLayout()
|
||||
cards_row.setSpacing(10)
|
||||
self.monthly_total_card = build_stat_card("총 근무 시간", "0시간", "이번 달",
|
||||
self.monthly_total_card = build_stat_card(tr('stats.total_work_hours'), tr('stats.value_hours', hours=0), tr('stats.this_month'),
|
||||
theme='blue', icon='clock')
|
||||
self.monthly_days_card = build_stat_card("근무 일수", "0일", "이번 달",
|
||||
self.monthly_days_card = build_stat_card(tr('stats.card_work_days'), tr('stats.value_days', days=0), tr('stats.this_month'),
|
||||
theme='cyan', icon='calendar')
|
||||
self.monthly_avg_card = build_stat_card("일평균", "0시간", "이번 달",
|
||||
self.monthly_avg_card = build_stat_card(tr('stats.card_avg_hours'), tr('stats.value_hours', hours=0), tr('stats.this_month'),
|
||||
theme='green', icon='chart')
|
||||
self.monthly_ot_card = build_stat_card("연장근무", "0시간 0분", "이번 달",
|
||||
self.monthly_ot_card = build_stat_card(tr('stats.card_overtime'), tr('stats.value_hours_minutes', hours=0, minutes=0), tr('stats.this_month'),
|
||||
theme='gold', icon='flame')
|
||||
for c in (self.monthly_total_card, self.monthly_days_card,
|
||||
self.monthly_avg_card, self.monthly_ot_card):
|
||||
@@ -159,7 +159,7 @@ class StatsView(QDialog):
|
||||
# 월간 차트
|
||||
from ui.chart_widget import make_chart_widget
|
||||
self.monthly_chart = make_chart_widget(widget)
|
||||
chart_card = build_section_card("요일별 평균", self.monthly_chart,
|
||||
chart_card = build_section_card(tr('stats.weekday_avg'), self.monthly_chart,
|
||||
theme='gray', icon='chart')
|
||||
layout.addWidget(chart_card, 1)
|
||||
|
||||
@@ -182,13 +182,13 @@ class StatsView(QDialog):
|
||||
f"font-size: 11pt; color: {tc('text')}; "
|
||||
f"background: transparent; border: none; padding: 4px 0;"
|
||||
)
|
||||
layout.addWidget(build_section_card("패턴 인사이트", self.pattern_text,
|
||||
layout.addWidget(build_section_card(tr('stats.pattern_insights'), self.pattern_text,
|
||||
theme='cyan', icon='search'))
|
||||
|
||||
# 출근 시각 분포 차트
|
||||
from ui.chart_widget import make_chart_widget
|
||||
self.clock_in_chart = make_chart_widget(widget)
|
||||
layout.addWidget(build_section_card("출근 시각 분포", self.clock_in_chart,
|
||||
layout.addWidget(build_section_card(tr('stats.clock_in_distribution'), self.clock_in_chart,
|
||||
theme='gray', icon='clock'), 1)
|
||||
|
||||
widget.setLayout(layout)
|
||||
@@ -225,15 +225,15 @@ class StatsView(QDialog):
|
||||
# 주간 통계
|
||||
weekly_stats = self.db.get_weekly_stats()
|
||||
total_hours = weekly_stats.get('total_hours', 0) or 0
|
||||
self._set_card_value(self.weekly_total_card, f"{total_hours:.1f}시간")
|
||||
self._set_card_value(self.weekly_days_card, f"{weekly_stats.get('work_days', 0)}일")
|
||||
self._set_card_value(self.weekly_total_card, tr('stats.value_hours', hours=f"{total_hours:.1f}"))
|
||||
self._set_card_value(self.weekly_days_card, tr('stats.value_days', days=weekly_stats.get('work_days', 0)))
|
||||
avg_hours = weekly_stats.get('avg_hours_per_day', 0) or 0
|
||||
self._set_card_value(self.weekly_avg_card, f"{avg_hours:.1f}시간")
|
||||
self._set_card_value(self.weekly_avg_card, tr('stats.value_hours', hours=f"{avg_hours:.1f}"))
|
||||
|
||||
overtime_minutes = weekly_stats.get('total_overtime_minutes', 0) or 0
|
||||
overtime_hours = overtime_minutes // 60
|
||||
overtime_mins = overtime_minutes % 60
|
||||
self._set_card_value(self.weekly_ot_card, f"{overtime_hours}시간 {overtime_mins}분")
|
||||
self._set_card_value(self.weekly_ot_card, tr('stats.value_hours_minutes', hours=overtime_hours, minutes=overtime_mins))
|
||||
|
||||
# 주간 차트
|
||||
from ui.chart_widget import draw_daily_hours, draw_weekday_avg
|
||||
@@ -251,21 +251,21 @@ class StatsView(QDialog):
|
||||
now = datetime.now()
|
||||
monthly_stats = self.db.get_monthly_stats(now.year, now.month)
|
||||
total_hours = monthly_stats.get('total_hours', 0) or 0
|
||||
self._set_card_value(self.monthly_total_card, f"{total_hours:.1f}시간")
|
||||
self._set_card_value(self.monthly_total_card, tr('stats.value_hours', hours=f"{total_hours:.1f}"))
|
||||
work_days = monthly_stats.get('work_days', 0) or 0
|
||||
self._set_card_value(self.monthly_days_card, f"{work_days}일")
|
||||
self._set_card_value(self.monthly_days_card, tr('stats.value_days', days=work_days))
|
||||
|
||||
if work_days > 0:
|
||||
avg = total_hours / work_days
|
||||
self._set_card_value(self.monthly_avg_card, f"{avg:.1f}시간")
|
||||
self._set_card_value(self.monthly_avg_card, tr('stats.value_hours', hours=f"{avg:.1f}"))
|
||||
else:
|
||||
self._set_card_value(self.monthly_avg_card, "0시간")
|
||||
self._set_card_value(self.monthly_avg_card, tr('stats.value_hours', hours=0))
|
||||
|
||||
overtime_minutes = monthly_stats.get('total_overtime_minutes', 0) or 0
|
||||
overtime_hours = overtime_minutes // 60
|
||||
overtime_mins = overtime_minutes % 60
|
||||
self._set_card_value(self.monthly_ot_card,
|
||||
f"{overtime_hours}시간 {overtime_mins}분")
|
||||
tr('stats.value_hours_minutes', hours=overtime_hours, minutes=overtime_mins))
|
||||
|
||||
# 월간 차트 (요일별 평균)
|
||||
if hasattr(self, 'monthly_chart'):
|
||||
@@ -300,8 +300,10 @@ class StatsView(QDialog):
|
||||
from core.salary import estimate_pay, format_won
|
||||
result = estimate_pay(records, wage, rate)
|
||||
self.salary_label.setText(
|
||||
f"💰 이번 달 추정 급여: {format_won(result['total'])} "
|
||||
f"(기본 {format_won(result['base'])} + 연장 {format_won(result['overtime'])})"
|
||||
tr('stats.salary_estimate',
|
||||
total=format_won(result['total']),
|
||||
base=format_won(result['base']),
|
||||
overtime=format_won(result['overtime']))
|
||||
)
|
||||
self.salary_label.setVisible(True)
|
||||
|
||||
@@ -334,7 +336,7 @@ class StatsView(QDialog):
|
||||
avg_minutes = sum(clock_in_times) / len(clock_in_times)
|
||||
avg_hour = int(avg_minutes // 60)
|
||||
avg_min = int(avg_minutes % 60)
|
||||
insights.append(f"📌 평균 출근시간: {avg_hour:02d}:{avg_min:02d}")
|
||||
insights.append(tr('stats.avg_clock_in', time=f"{avg_hour:02d}:{avg_min:02d}"))
|
||||
|
||||
# 연장근무 빈도
|
||||
overtime_days = len([r for r in records if (r.get('overtime_earned') or 0) > 0])
|
||||
@@ -342,14 +344,14 @@ class StatsView(QDialog):
|
||||
|
||||
if total_days > 0:
|
||||
overtime_rate = (overtime_days / total_days) * 100
|
||||
insights.append(f"📌 연장근무 빈도: {overtime_rate:.0f}% ({overtime_days}/{total_days}일)")
|
||||
insights.append(tr('stats.overtime_frequency', rate=f"{overtime_rate:.0f}", days=overtime_days, total=total_days))
|
||||
|
||||
# 가장 긴 근무일
|
||||
records_with_hours = [r for r in records if (r.get('total_hours') or 0) > 0]
|
||||
if records_with_hours:
|
||||
longest_work = max(records_with_hours, key=lambda x: x.get('total_hours', 0))
|
||||
if longest_work.get('total_hours', 0) > 0:
|
||||
insights.append(f"📌 최장 근무: {longest_work['date']} ({longest_work['total_hours']:.1f}시간)")
|
||||
insights.append(tr('stats.longest_work', date=longest_work['date'], hours=f"{longest_work['total_hours']:.1f}"))
|
||||
|
||||
# 건강 경고
|
||||
recent_records = records[-7:] # 최근 7일
|
||||
@@ -364,15 +366,15 @@ class StatsView(QDialog):
|
||||
consecutive_overtime = 0
|
||||
|
||||
if max_consecutive >= 3:
|
||||
insights.append(f"⚠️ 최근 {max_consecutive}일 연속 연장근무 발생!")
|
||||
insights.append(tr('stats.consecutive_ot_warning', days=max_consecutive))
|
||||
|
||||
# 주 52시간 체크
|
||||
if len(recent_records) >= 7:
|
||||
week_total = sum((r.get('total_hours') or 0) for r in recent_records[-7:])
|
||||
if week_total > 52:
|
||||
insights.append(f"🚨 주 52시간 초과: {week_total:.1f}시간")
|
||||
insights.append(tr('stats.weekly_52_exceeded', hours=f"{week_total:.1f}"))
|
||||
|
||||
self.pattern_text.setText("\n\n".join(insights) if insights else "패턴을 분석할 데이터가 부족합니다.")
|
||||
self.pattern_text.setText("\n\n".join(insights) if insights else tr('stats.no_pattern_data'))
|
||||
|
||||
|
||||
# 테스트 코드
|
||||
|
||||
+8
-6
@@ -7,6 +7,8 @@ from __future__ import annotations
|
||||
from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from core.i18n import tr
|
||||
|
||||
|
||||
class TodaySummaryCard(QFrame):
|
||||
"""퇴근 처리 직후 표시되는 요약 카드."""
|
||||
@@ -30,7 +32,7 @@ class TodaySummaryCard(QFrame):
|
||||
layout.setSpacing(2)
|
||||
|
||||
header = QHBoxLayout()
|
||||
title = QLabel("오늘의 요약")
|
||||
title = QLabel(tr('today.title'))
|
||||
title.setStyleSheet("font-weight: bold; font-size: 13px;")
|
||||
header.addWidget(title)
|
||||
header.addStretch()
|
||||
@@ -70,17 +72,17 @@ class TodaySummaryCard(QFrame):
|
||||
"""
|
||||
h = int(total_hours)
|
||||
m = int((total_hours - h) * 60)
|
||||
self.total_label.setText(f"총 근무: {h}시간 {m}분")
|
||||
self.total_label.setText(tr('today.total_work', hours=h, minutes=m))
|
||||
|
||||
details = []
|
||||
if lunch_minutes > 0:
|
||||
details.append(f"점심 {lunch_minutes}분")
|
||||
details.append(tr('today.detail_lunch', minutes=lunch_minutes))
|
||||
if dinner_minutes > 0:
|
||||
details.append(f"저녁 {dinner_minutes}분")
|
||||
details.append(tr('today.detail_dinner', minutes=dinner_minutes))
|
||||
if break_minutes > 0:
|
||||
details.append(f"외출 {break_minutes}분")
|
||||
details.append(tr('today.detail_break', minutes=break_minutes))
|
||||
if overtime_actual > 0:
|
||||
details.append(f"연장 {overtime_actual}분 → 적립 {overtime_earned}분")
|
||||
details.append(tr('today.detail_overtime', actual=overtime_actual, earned=overtime_earned))
|
||||
self.detail_label.setText(" · ".join(details) if details else "")
|
||||
self.detail_label.setVisible(bool(details))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user