"""
메인 GUI 윈도우
PyQt5를 사용한 메인 애플리케이션 인터페이스
"""
import sys
from datetime import datetime, timedelta
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QLabel, QPushButton, QProgressBar,
                             QMessageBox, QGroupBox, QGridLayout, QSystemTrayIcon,
                             QShortcut, QDialog)
from PyQt5.QtCore import QTimer, Qt, pyqtSignal, QLockFile, QDir, QSize
from PyQt5.QtGui import QFont, QPalette, QColor, QKeySequence

from core.settings_keys import (
    DB_PATH_OVERRIDE, LANGUAGE, TIME_FORMAT, THEME,
    WORKDAY_BOUNDARY_HOUR, WORK_MINUTES, WORK_HOURS,
    LUNCH_DURATION_MINUTES, DINNER_DURATION_MINUTES,
)
from core.i18n import tr

import os
import sys
# core 모듈을 import하기 위한 경로 추가
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from core.database import Database
from core.event_monitor import EventMonitor
from core.time_calculator import TimeCalculator, resolve_work_datetimes
from ui.clock_in_dialog import ClockInDialog
from ui.calendar_view import CalendarView
from ui.stats_view import StatsView
from ui.leave_view import LeaveView
from ui.settings_view import SettingsView
from ui.break_view import BreakView
from core.notifier import Notifier
from utils.system_tray import SystemTrayIcon
from utils.time_format import format_hours_minutes, format_leave_amount
from ui.styles import get_theme, ThemeColors, apply_dark_titlebar
from utils.debug_log import dlog


class MainWindow(QMainWindow):
    """메인 윈도우 클래스"""

    def __init__(self, db: 'Database' = None):
        """
        Args:
            db: 사전 초기화된 Database 인스턴스. None이면 자체 부트스트랩.
                (main.py가 backup/crash_handler용으로 먼저 만들고 전달)
        """
        super().__init__()

        # 테마 적용
        self.current_theme = 'dark'  # 설정에서 로드 후 덮어씀

        # 데이터베이스 — main.py가 전달하면 재사용, 아니면 자체 부트스트랩
        if db is not None:
            self.db = db
        else:
            bootstrap = Database()
            override_path = bootstrap.get_setting(DB_PATH_OVERRIDE, '') or ''
            if override_path and os.path.exists(os.path.dirname(override_path) or '.'):
                self.db = Database(override_path)
            else:
                self.db = bootstrap
        self.event_monitor = EventMonitor()

        # 언어 초기화 (설정값 반영)
        from core.i18n import set_language
        set_language(self.db.get_setting(LANGUAGE, 'ko') or 'ko')

        # 접근성 — 글꼴 크기 + 고대비
        try:
            from ui.accessibility import apply_from_settings as _apply_a11y
            _apply_a11y(self.db)
        except Exception as e:
            dlog(f"accessibility apply failed: {e}")

        # TimeCalculator 초기화 (설정값 반영)
        settings = self.db.get_settings()

        # 시간 형식 설정 캐시 (매 초 DB 조회 방지)
        self.cached_time_format = str(settings.get(TIME_FORMAT, '24'))

        # 테마 설정
        self.current_theme = str(settings.get(THEME, 'dark'))
        self.apply_theme(self.current_theme)
        self.time_calc = self._build_time_calc(settings)

        # 알림 시스템 (db 전달 — 설정 키로 알림 가드)
        self.notifier = Notifier(self, db=self.db)
        self.notifier.notification_signal.connect(self.show_notification)

        # 도전과제 정의 동기화 (실패는 silent — 핵심 기능 아님)
        try:
            from core.achievements import sync_definitions_to_db
            sync_definitions_to_db(self.db)
        except Exception as e:
            from utils.debug_log import dlog
            dlog(f"achievements sync failed: {e}")

        # 책임 분리된 컨트롤러들 (1Hz hot path + 사용자 액션)
        from ui.controllers.lock_monitor import LockMonitor
        from ui.controllers.auto_lunch import AutoLunchManager
        from ui.controllers.notification_orchestrator import NotificationOrchestrator
        from ui.controllers.meal_controller import MealController
        self._lock_monitor = LockMonitor(self)
        self._auto_lunch = AutoLunchManager(self)
        self._notif_orch = NotificationOrchestrator(self)
        self._meal = MealController(self)

        # 시스템 트레이
        self.tray_icon = SystemTrayIcon(self)
        self.tray_icon.show()

        # 윈도우 아이콘 설정 (시계 아이콘)
        from PyQt5.QtGui import QIcon
        # PyInstaller로 패키징된 경우 _MEIPASS 경로 사용
        if getattr(sys, 'frozen', False):
            # PyInstaller로 실행 중
            base_path = sys._MEIPASS
        else:
            # 일반 Python 실행
            base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

        icon_path = os.path.join(base_path, "3d-alarm.png")
        if os.path.exists(icon_path):
            window_icon = QIcon(icon_path)
        else:
            window_icon = self.tray_icon.create_icon("⏰")
        self.setWindowIcon(window_icon)

        # 상태 변수
        self.clock_in_time = None
        self.lunch_break_enabled = False
        self.dinner_break_enabled = False
        self.is_clocked_in = False
        self.is_on_break = False  # 외출 중 여부
        self.midnight_rollover_handled = False  # 자정 넘김 처리 여부
        self.auto_lunch_applied_today = False  # auto_lunch 중복 적용 방지

        # update_display 1Hz 핫패스 캐시 (설정/날짜 변경 시에만 재계산)
        self._workday_boundary_hour_cache = None
        self._non_working_cache_date = None
        self._non_working_cache_value = None
        self._full_day_leave_cache_date = None
        self._full_day_leave_cache_value = None
        # 컨트롤러는 init_ui() 이후 알림 시스템 생성 시점에 함께 초기화

        # UI 초기화
        self.init_ui()

        # 타이머 시작 (1초마다 업데이트)
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_display)
        self.timer.start(1000)

        # 화면 잠금 감지 (5초 간격, auto_break_on_lock 설정 시 활성)
        self._last_lock_state = False
        self._lock_timer = QTimer()
        self._lock_timer.timeout.connect(self._check_screen_lock)
        self._lock_timer.start(5000)

        # 종료 시 타이머 정리 — aboutToQuit은 QApplication 종료 직전에만 1회 fire.
        # 모든 종료 경로(트레이 메뉴/Discord/언어변경 등)를 한 곳에서 커버.
        try:
            qapp = QApplication.instance()
            if qapp is not None:
                qapp.aboutToQuit.connect(self._on_app_quit)
        except Exception as e:
            dlog(f"aboutToQuit connect failed: {e}")

        # 초기 데이터 로드
        self.load_today_data()

        # 시작 5초 후 백그라운드 업데이트 체크 (실패 시 조용히 무시)
        QTimer.singleShot(5000, lambda: self.check_for_updates(silent=True))

    def _on_app_quit(self) -> None:
        """QApplication.aboutToQuit 핸들러 — 타이머 정지 + 트레이 숨김.

        in-flight 1Hz/5s tick이 부분 파괴된 객체에 대해 fire하는 것 방지.
        """
        try:
            if hasattr(self, 'timer') and self.timer is not None:
                self.timer.stop()
            if hasattr(self, '_lock_timer') and self._lock_timer is not None:
                self._lock_timer.stop()
            if hasattr(self, 'notifier') and self.notifier is not None:
                # Notifier 내부 1분 timer
                if hasattr(self.notifier, 'timer'):
                    self.notifier.timer.stop()
            if hasattr(self, 'tray_icon') and self.tray_icon is not None:
                self.tray_icon.hide()
        except Exception:
            # 정리 실패해도 종료는 진행 — Qt가 결국 다 cleanup
            pass

    def _check_screen_lock(self):
        """LockMonitor 컨트롤러로 위임 (5초 polling)."""
        self._lock_monitor.tick()

    def _set_text_if_changed(self, widget, text: str) -> None:
        """직전 값과 다를 때만 setText (1Hz hot path 무의미한 repaint 방지)."""
        if widget.text() != text:
            widget.setText(text)

    def format_time(self, dt: datetime, include_seconds: bool = False) -> str:
        """
        시간을 설정에 따라 형식화
        Args:
            dt: datetime 객체
            include_seconds: 초 포함 여부
        Returns:
            형식화된 시간 문자열
        """
        # 캐시된 시간 형식 사용 (매 초 DB 조회 방지)
        time_format = getattr(self, 'cached_time_format', '24')

        if time_format == '12':
            # 12시간 형식 (오전/오후)
            hour = dt.hour
            minute = dt.minute
            second = dt.second
            period = tr('label.am') if hour < 12 else tr('label.pm')
            display_hour = hour % 12
            if display_hour == 0:
                display_hour = 12
            minute_str = f"{minute:02d}:{second:02d}" if include_seconds else f"{minute:02d}"
            return tr('time_format.12h', period=period, hour=display_hour, minute=minute_str)
        else:
            # 24시간 형식
            if include_seconds:
                return dt.strftime('%H:%M:%S')
            else:
                return dt.strftime('%H:%M')

    def init_ui(self):
        """UI 초기화"""
        from core.version import __version__
        from ui.i18n_runtime import register
        self._app_version = __version__
        self.setWindowTitle(f"{tr('window.main_title')} v{__version__}")
        register(self, 'window.main_title', setter='setWindowTitle',
                  post=lambda t: f"{t} v{__version__}")
        self.setGeometry(100, 100, 540, 720)
        self.setMinimumSize(500, 600)

        # 외부 컨테이너 (스크롤 + 고정 하단)
        from PyQt5.QtWidgets import QScrollArea

        outer_widget = QWidget()
        outer_layout = QVBoxLayout()
        outer_layout.setSpacing(0)
        outer_layout.setContentsMargins(0, 0, 0, 0)

        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # 중앙 위젯 (스크롤 내부)
        central_widget = QWidget()
        central_widget.setObjectName("central_widget")
        scroll_area.setWidget(central_widget)

        outer_layout.addWidget(scroll_area, 1)
        outer_widget.setLayout(outer_layout)
        self.setCentralWidget(outer_widget)

        # 메인 레이아웃 — 외곽 24px, 위젯 간 12px (통일된 여백 시스템)
        main_layout = QVBoxLayout()
        main_layout.setSpacing(12)
        main_layout.setContentsMargins(24, 20, 24, 16)

        # 1. 헤더 - 앱 타이틀
        title_label = QLabel(tr('app.title'))
        title_label.setObjectName("app_title")
        title_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(title_label)

        # 2. 날짜 표시
        self.date_label = QLabel()
        self.date_label.setObjectName("date_label")
        self.date_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(self.date_label)

        # 1.5 오늘 요약 카드 (퇴근 후 표시, 평소엔 숨김)
        from ui.today_summary import TodaySummaryCard
        self.today_summary_card = TodaySummaryCard()
        main_layout.addWidget(self.today_summary_card)

        # 2. 출근 정보 그룹
        clock_in_group = self.create_clock_in_group()
        main_layout.addWidget(clock_in_group)

        # 3. 남은 시간 표시 그룹 (히어로 — 남은시간 + 진행률 + 예상 퇴근시각 통합)
        remaining_group = self.create_remaining_time_group()
        main_layout.addWidget(remaining_group)

        # 5. 점심/저녁 토글 (가로 배치)
        meal_button_layout = QHBoxLayout()
        meal_button_layout.setSpacing(8)

        self.lunch_button = QPushButton(tr('btn.lunch_add'))
        self.lunch_button.setCheckable(True)
        self.lunch_button.clicked.connect(self.toggle_lunch_break)
        self.lunch_button.setContextMenuPolicy(Qt.CustomContextMenu)
        self.lunch_button.customContextMenuRequested.connect(
            lambda pos: self._show_meal_context('lunch', self.lunch_button, pos)
        )
        self.lunch_button.setToolTip(tr('tooltip.meal_click'))

        self.dinner_button = QPushButton(tr('btn.dinner_add'))
        self.dinner_button.setCheckable(True)
        self.dinner_button.clicked.connect(self.toggle_dinner_break)
        self.dinner_button.setContextMenuPolicy(Qt.CustomContextMenu)
        self.dinner_button.customContextMenuRequested.connect(
            lambda pos: self._show_meal_context('dinner', self.dinner_button, pos)
        )
        self.dinner_button.setToolTip(tr('tooltip.meal_click'))

        meal_button_layout.addWidget(self.lunch_button)
        meal_button_layout.addWidget(self.dinner_button)
        main_layout.addLayout(meal_button_layout)

        # 5-1. 외출 버튼
        break_button_layout = QHBoxLayout()
        break_button_layout.setSpacing(8)

        self.break_out_button = QPushButton(tr('btn.break_out'))
        self.break_out_button.clicked.connect(self.break_out)

        self.break_in_button = QPushButton(tr('btn.break_in'))
        self.break_in_button.clicked.connect(self.break_in)
        self.break_in_button.setEnabled(False)

        self.break_manage_button = QPushButton(tr('btn.break_manage'))
        self.break_manage_button.clicked.connect(self.show_break_management)

        break_button_layout.addWidget(self.break_out_button)
        break_button_layout.addWidget(self.break_in_button)
        break_button_layout.addWidget(self.break_manage_button)

        main_layout.addLayout(break_button_layout)

        # 외출 상태 라벨
        self.break_status_label = QLabel("")
        self.break_status_label.setObjectName("field_label")
        self.break_status_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(self.break_status_label)

        # 6. 연장근무 적립 현황
        overtime_group = self.create_overtime_group()
        main_layout.addWidget(overtime_group)

        central_widget.setLayout(main_layout)

        # 7. 퇴근 버튼 - 강조 스타일 (고정 하단)
        fixed_bottom = QWidget()
        fixed_bottom.setObjectName("fixed_bottom")
        fixed_bottom_layout = QVBoxLayout()
        fixed_bottom_layout.setSpacing(10)
        fixed_bottom_layout.setContentsMargins(24, 12, 24, 16)

        self.clock_out_button = QPushButton(tr('btn.clock_out'))
        self.clock_out_button.setObjectName("clock_out_button")
        self.clock_out_button.setCursor(Qt.PointingHandCursor)
        self.clock_out_button.clicked.connect(self.handle_clock_out_button)
        fixed_bottom_layout.addWidget(self.clock_out_button)

        # 8. 하단 버튼
        bottom_layout = QHBoxLayout()
        bottom_layout.setSpacing(8)

        stats_button = QPushButton(tr('menu.stats'))
        calendar_button = QPushButton(tr('menu.calendar'))
        report_button = QPushButton(tr('menu.daily_report'))
        achievements_button = QPushButton(tr('btn.achievements'))
        help_button = QPushButton(tr('menu.help'))
        settings_button = QPushButton(tr('menu.settings'))

        # 런타임 i18n 등록
        for btn, key in [(stats_button, 'menu.stats'),
                          (calendar_button, 'menu.calendar'),
                          (report_button, 'menu.daily_report'),
                          (help_button, 'menu.help'),
                          (settings_button, 'menu.settings')]:
            register(btn, key)

        # 하단 네비게이션 — 라인 아이콘 + 라벨 (이모지 대체)
        self._nav_icon_specs = [
            (stats_button, 'chart'),
            (calendar_button, 'calendar'),
            (report_button, 'report'),
            (achievements_button, 'award'),
            (help_button, 'help'),
            (settings_button, 'settings'),
        ]
        for btn, _name in self._nav_icon_specs:
            btn.setObjectName("nav_btn")
            bottom_layout.addWidget(btn)

        # 버튼 연결
        stats_button.clicked.connect(self.show_stats)
        calendar_button.clicked.connect(self.show_calendar)
        report_button.clicked.connect(self.generate_daily_report)
        achievements_button.clicked.connect(self.show_achievements)
        help_button.clicked.connect(self.show_help)
        settings_button.clicked.connect(self.show_settings)

        fixed_bottom_layout.addLayout(bottom_layout)
        fixed_bottom.setLayout(fixed_bottom_layout)
        outer_layout.addWidget(fixed_bottom, 0)

        # 초기 날짜 업데이트
        self.update_date_label()

        # 라인 아이콘 적용 (테마 색 틴팅)
        self._apply_button_icons()

        # 앱 내 단축키
        self._setup_shortcuts()

    def _apply_button_icons(self):
        """버튼 아이콘을 현재 테마 색으로 (재)적용. 테마 전환 시에도 호출돼 재틴팅."""
        from ui.icons import get_icon
        sec = ThemeColors.get('text_secondary')
        inv = ThemeColors.get('text_inverse')
        for btn, name in getattr(self, '_nav_icon_specs', []):
            btn.setIcon(get_icon(name, sec, 16))
            btn.setIconSize(QSize(16, 16))
        if getattr(self, 'edit_clock_in_button', None) is not None:
            self.edit_clock_in_button.setIcon(get_icon('edit', sec, 15))
            self.edit_clock_in_button.setIconSize(QSize(15, 15))
        if getattr(self, 'clock_out_button', None) is not None:
            self.clock_out_button.setIcon(get_icon('logout', inv, 18))
            self.clock_out_button.setIconSize(QSize(18, 18))

    def _setup_shortcuts(self):
        """앱 내 단축키 — 메인 창 포커스 시만 동작"""
        bindings = [
            ("Ctrl+O", self.handle_clock_out_button),  # 출/퇴근 토글
            ("Ctrl+L", lambda: self.lunch_button.click()),  # 점심
            ("Ctrl+D", lambda: self.dinner_button.click()),  # 저녁
            ("Ctrl+B", self.show_break_management),  # 외출 관리
            ("Ctrl+,", self.show_settings),  # 설정
            ("F1", self.show_help),  # 도움말
            ("F5", lambda: self.check_for_updates(silent=False)),  # 업데이트 확인
            ("Ctrl+R", self.generate_daily_report),  # 일일보고
        ]
        for keyseq, handler in bindings:
            sc = QShortcut(QKeySequence(keyseq), self)
            sc.activated.connect(handler)

    def _build_time_column(self, label_text: str, value_widget: QLabel) -> QVBoxLayout:
        """라벨(작게) 위 + 시각(크게) 아래 형태의 세로 컬럼. 한 줄 나란히 배치용."""
        col = QVBoxLayout()
        col.setSpacing(2)
        lbl = QLabel(label_text)
        lbl.setObjectName("field_label")
        col.addWidget(lbl)
        col.addWidget(value_widget)
        return col

    def create_clock_in_group(self) -> QGroupBox:
        """출근 정보 그룹 생성 — 출근/현재 시각을 한 줄에 나란히"""
        group = QGroupBox(tr('group.today_work'))

        layout = QVBoxLayout()
        layout.setSpacing(8)
        layout.setContentsMargins(16, 24, 16, 16)

        # 출근 / 현재 시각을 한 줄에 나란히 (2-컬럼)
        row = QHBoxLayout()
        row.setSpacing(12)

        # 출근 컬럼 (라벨 + 편집 버튼 헤더 / 값)
        self.clock_in_value = QLabel("--:--:--")
        self.clock_in_value.setObjectName("time_value")
        # 라벨 자체도 클릭 가능 (인라인 편집 — 출근 시간 빠른 수정)
        self.clock_in_value.setCursor(Qt.PointingHandCursor)
        self.clock_in_value.setToolTip(tr('tooltip.clock_in_edit'))
        self.clock_in_value.mousePressEvent = lambda e: self.manual_clock_in()

        clock_in_col = QVBoxLayout()
        clock_in_col.setSpacing(2)
        clock_in_label = QLabel(tr('label.clock_in_time'))
        clock_in_label.setObjectName("field_label")
        clock_in_col.addWidget(clock_in_label)

        # 시각 + 편집 버튼을 한 줄에 (편집 아이콘이 출근 시각 바로 옆에 붙도록)
        clock_in_value_row = QHBoxLayout()
        clock_in_value_row.setSpacing(6)
        self.edit_clock_in_button = QPushButton("")
        self.edit_clock_in_button.setObjectName("btn_small")
        self.edit_clock_in_button.setFixedWidth(30)
        self.edit_clock_in_button.setToolTip(tr('tooltip.clock_in_edit'))
        self.edit_clock_in_button.clicked.connect(self.manual_clock_in)
        clock_in_value_row.addWidget(self.clock_in_value)
        clock_in_value_row.addWidget(self.edit_clock_in_button)
        clock_in_value_row.addStretch()
        clock_in_col.addLayout(clock_in_value_row)

        # 현재 컬럼
        self.current_time_value = QLabel("--:--:--")
        self.current_time_value.setObjectName("time_value")
        current_col = self._build_time_column(tr('label.current_time'), self.current_time_value)

        row.addLayout(clock_in_col, 1)
        row.addLayout(current_col, 1)
        layout.addLayout(row)

        group.setLayout(layout)
        return group

    def create_remaining_time_group(self) -> QGroupBox:
        """남은 시간 히어로 그룹 — 남은시간(가장 큼) + 진행률 + 예상 퇴근시각"""
        self.remaining_time_group = QGroupBox(tr('group.remaining_time'))

        layout = QVBoxLayout()
        layout.setSpacing(12)
        layout.setContentsMargins(16, 24, 16, 16)

        # 남은 시간 라벨 (히어로 — 화면에서 가장 큰 결과)
        self.remaining_time_label = QLabel("--:--:--")
        self.remaining_time_label.setObjectName("time_display")
        self.remaining_time_label.setAlignment(Qt.AlignCenter)

        # 프로그레스 바 (얇게 6px)
        self.progress_bar = QProgressBar()
        self.progress_bar.setTextVisible(False)
        self.progress_bar.setFixedHeight(6)

        # 예상 퇴근시각 (히어로 카드 내부에 통합)
        self.expected_time_label = QLabel()
        self.expected_time_label.setObjectName("expected_time")
        self.expected_time_label.setAlignment(Qt.AlignCenter)

        layout.addWidget(self.remaining_time_label)
        layout.addWidget(self.progress_bar)
        layout.addWidget(self.expected_time_label)

        self.remaining_time_group.setLayout(layout)
        return self.remaining_time_group

    def create_overtime_group(self) -> QGroupBox:
        """연장근무 및 연차 현황 그룹 생성"""
        group = QGroupBox(tr('group.overtime_leave'))

        layout = QVBoxLayout()
        layout.setSpacing(10)
        layout.setContentsMargins(16, 24, 16, 16)

        # 연장근무 섹션
        overtime_header = QHBoxLayout()
        overtime_title = QLabel(tr('section.overtime_earned'))
        overtime_title.setObjectName("section_title")
        overtime_header.addWidget(overtime_title)
        overtime_header.addStretch()

        self.overtime_balance_label = QLabel(tr('label.overtime_balance_zero'))
        self.overtime_balance_label.setObjectName("badge_overtime")
        overtime_header.addWidget(self.overtime_balance_label)
        layout.addLayout(overtime_header)

        # 연장근무 사용 버튼 (1줄)
        overtime_button_layout = QHBoxLayout()
        overtime_button_layout.setSpacing(4)
        use_30min_button = QPushButton(tr('btn.use_30min'))
        use_1hour_button = QPushButton(tr('btn.use_1hour'))
        use_2hour_button = QPushButton(tr('btn.use_2hour'))
        use_custom_overtime_button = QPushButton(tr('btn.custom_input'))
        overtime_detail_button = QPushButton(tr('btn.detail'))

        for btn in [use_30min_button, use_1hour_button, use_2hour_button, use_custom_overtime_button, overtime_detail_button]:
            btn.setObjectName("btn_small")
            overtime_button_layout.addWidget(btn)

        use_30min_button.clicked.connect(lambda: self.use_overtime(30))
        use_1hour_button.clicked.connect(lambda: self.use_overtime(60))
        use_2hour_button.clicked.connect(lambda: self.use_overtime(120))
        use_custom_overtime_button.clicked.connect(self.use_custom_overtime)
        overtime_detail_button.clicked.connect(self.show_overtime_detail)

        layout.addLayout(overtime_button_layout)

        # 구분선
        separator = QLabel()
        separator.setObjectName("separator")
        layout.addWidget(separator)

        # 연차 섹션
        leave_header = QHBoxLayout()
        leave_title = QLabel(tr('section.leave'))
        leave_title.setObjectName("section_title")
        leave_header.addWidget(leave_title)
        leave_header.addStretch()

        self.leave_balance_label = QLabel(tr('label.leave_balance_zero'))
        self.leave_balance_label.setObjectName("badge_leave")
        leave_header.addWidget(self.leave_balance_label)
        layout.addLayout(leave_header)

        # 연차 사용 버튼 (1줄)
        leave_button_layout = QHBoxLayout()
        leave_button_layout.setSpacing(4)
        use_30min_leave_button = QPushButton(tr('btn.use_30min'))
        use_1hour_leave_button = QPushButton(tr('btn.use_1hour'))
        use_half_leave_button = QPushButton(tr('btn.half_leave'))
        use_full_leave_button = QPushButton(tr('btn.full_leave'))
        use_custom_leave_button = QPushButton(tr('btn.custom_input'))
        leave_detail_button = QPushButton(tr('btn.detail'))

        for btn in [use_30min_leave_button, use_1hour_leave_button, use_half_leave_button,
                    use_full_leave_button, use_custom_leave_button, leave_detail_button]:
            btn.setObjectName("btn_small")
            leave_button_layout.addWidget(btn)

        use_30min_leave_button.clicked.connect(lambda: self.use_leave(0.5/8))  # 0.0625일
        use_1hour_leave_button.clicked.connect(lambda: self.use_leave(1.0/8))  # 0.125일
        use_half_leave_button.clicked.connect(lambda: self.use_leave(0.5))
        use_full_leave_button.clicked.connect(lambda: self.use_leave(1.0))
        use_custom_leave_button.clicked.connect(self.use_custom_leave)  # 연장근무와 동일한 상세입력
        leave_detail_button.clicked.connect(self.show_leave_detail)

        layout.addLayout(leave_button_layout)

        # 구분선
        separator2 = QLabel()
        separator2.setObjectName("separator")
        layout.addWidget(separator2)

        # 총합 시간 표시
        total_header = QHBoxLayout()
        total_title = QLabel(tr('section.total_time'))
        total_title.setObjectName("section_title")
        total_header.addWidget(total_title)
        total_header.addStretch()

        self.total_time_label = QLabel(tr('label.time_hours_minutes', hours=0, minutes=0))
        self.total_time_label.setObjectName("badge_total")
        total_header.addWidget(self.total_time_label)
        layout.addLayout(total_header)

        group.setLayout(layout)
        return group

    def load_today_data(self):
        """오늘 데이터 로드"""
        # 먼저 이전 퇴근 기록들 자동 처리
        self.auto_clock_out_previous_days()

        today_record = self.db.get_today_record()

        if today_record and today_record.get('clock_in'):
            # 이미 출근 기록이 있음
            clock_in_str = today_record['clock_in']
            self.clock_in_time = datetime.strptime(
                f"{datetime.now().date()} {clock_in_str}",
                "%Y-%m-%d %H:%M:%S"
            )
            self.lunch_break_enabled = bool(today_record.get('lunch_break', False))
            self.dinner_break_enabled = bool(today_record.get('dinner_break', False))
            self.is_clocked_in = True
            self.midnight_rollover_handled = False  # 새로운 근무일 시작 시 플래그 리셋
            # 점심이 이미 적용되어 있으면 auto_lunch가 다시 트리거되지 않도록
            self.auto_lunch_applied_today = self.lunch_break_enabled

            # 퇴근했는지 확인
            if today_record.get('clock_out'):
                self.is_clocked_in = False
                self.clock_out_button.setEnabled(True)
                self.clock_out_button.setText(tr('btn.clock_out_cancel'))

                # 퇴근 완료 상태에서도 출퇴근 시간은 표시
                self.clock_in_value.setText(self.format_time(self.clock_in_time, include_seconds=True))
            else:
                # 출근 중이면 퇴근하기 버튼
                self.clock_out_button.setEnabled(True)
                self.clock_out_button.setText(tr('btn.clock_out'))

        else:
            # 출근 기록 없음 — 종일 연차일이면 자동 감지·수동 입력 모두 스킵
            today_str = datetime.now().date().isoformat()
            if self.db.has_full_day_leave(today_str):
                self.is_clocked_in = False
                self.clock_out_button.setEnabled(False)
                # 점심/저녁/외출/잔액 갱신만 수행
                self.lunch_button.setChecked(False)
                self.update_lunch_status()
                self.dinner_button.setChecked(False)
                self.update_dinner_status()
                self.update_break_status()
                self.update_overtime_balance()
                self.update_leave_balance()
                return

            # 출근 기록 없음 - 자동 감지 시도
            auto_clock_in = self.event_monitor.get_work_start_time()

            if auto_clock_in:
                # 자동 감지 성공
                self.clock_in_time = auto_clock_in
                self.is_clocked_in = True
                self.midnight_rollover_handled = False  # 새로운 근무일 시작 시 플래그 리셋

                # DB에 저장
                today = datetime.now().date().isoformat()
                clock_in_str = auto_clock_in.strftime("%H:%M:%S")
                self.db.add_work_record(today, clock_in_str, is_manual=False)

                QMessageBox.information(
                    self,
                    tr('msg.auto_clock_in.title'),
                    tr('msg.auto_clock_in.body', time=clock_in_str)
                )
            else:
                # 자동 감지 실패 - 수동 입력 요청
                reply = QMessageBox.question(
                    self,
                    tr('msg.manual_clock_in.title'),
                    tr('msg.manual_clock_in.body'),
                    QMessageBox.Yes | QMessageBox.No
                )

                if reply == QMessageBox.Yes:
                    self.manual_clock_in()
                else:
                    self.is_clocked_in = False
                    self.clock_out_button.setEnabled(False)

        # 점심 버튼 상태 업데이트
        self.lunch_button.setChecked(self.lunch_break_enabled)
        self.update_lunch_status()

        # 저녁 버튼 상태 업데이트
        self.dinner_button.setChecked(self.dinner_break_enabled)
        self.update_dinner_status()

        # 외출 상태 업데이트
        self.update_break_status()

        # 연장근무 및 연차 잔액 업데이트
        self.update_overtime_balance()
        self.update_leave_balance()

    def update_display(self):
        """디스플레이 업데이트 (1초마다)"""
        now = datetime.now()

        # 현재 시간은 항상 업데이트 (출근 전에도 표시)
        self._set_text_if_changed(self.current_time_value, self.format_time(now, include_seconds=True))

        # 근무일 경계 시간 확인 (캐시, 설정 변경 시 reload_settings에서 무효화)
        if self._workday_boundary_hour_cache is None:
            self._workday_boundary_hour_cache = int(self.db.get_setting(WORKDAY_BOUNDARY_HOUR, '6'))
        workday_boundary_hour = self._workday_boundary_hour_cache

        # 새 근무일 체크: 퇴근 완료 상태에서 날짜가 바뀌고 경계 시간 이후면 새 출근 유도
        if not self.is_clocked_in and self.clock_in_time:
            # 이전 출근 기록이 있고, 날짜가 바뀌었고, 경계 시간 이후면
            if self.clock_in_time.date() != now.date() and now.hour >= workday_boundary_hour:
                self.start_new_workday(now)
                return

        # 종일 연차일 — 출근 안 한 상태에서 전용 카드만 표시 후 종료.
        # (수동 출근 override는 handle_clock_in 경로에서 별도 처리)
        if not self.is_clocked_in:
            today_str = now.date().isoformat()
            if self._full_day_leave_cache_date != now.date():
                self._full_day_leave_cache_value = self.db.has_full_day_leave(today_str)
                self._full_day_leave_cache_date = now.date()
            if self._full_day_leave_cache_value:
                self._render_full_day_leave_state(today_str)
                return

        # 출근하지 않았으면 여기서 종료
        if not self.is_clocked_in or not self.clock_in_time:
            return

        # 근무일 경계 체크: 출근일과 현재 날짜가 다르고, 경계 시간(기본 6시) 이후면 롤오버
        # 예: 야근으로 새벽 2시까지 일해도 6시 전까지는 전날 근무로 인정
        if self.clock_in_time.date() != now.date() and now.hour >= workday_boundary_hour:
            self.handle_workday_rollover(now)

        # 출근 시간 업데이트 (설정 변경 시에도 갱신됨)
        self._set_text_if_changed(self.clock_in_value, self.format_time(self.clock_in_time, include_seconds=True))

        # 자동 점심시간 적용 (설정 + 출근 후 4시간 경과 + 미적용 + 1회만)
        self._auto_lunch.maybe_apply(now)

        # 외출 시간 계산
        break_minutes = self.db.get_total_break_minutes_today()

        # 오늘 사용한 추가근무 시간 계산
        overtime_used_today = self.db.get_today_overtime_usage()

        # 오늘 사용한 연차/반차 시간 계산
        leave_used_today = self.db.get_today_leave_minutes()

        # 총 차감 시간 (추가근무 + 연차/반차)
        total_time_off = overtime_used_today + leave_used_today

        # 휴일/주말 또는 종일연차 override → 출근 직후부터 모든 시간이 연장근무로 흐름.
        if self._non_working_cache_date != now.date():
            self._non_working_cache_value = self.time_calc.is_non_working_day(now, self.db)
            self._non_working_cache_date = now.date()
        is_non_working = self._non_working_cache_value
        if self._full_day_leave_cache_date != now.date():
            self._full_day_leave_cache_value = self.db.has_full_day_leave(now.date().isoformat())
            self._full_day_leave_cache_date = now.date()
        is_full_day_leave = self._full_day_leave_cache_value
        is_holiday = is_non_working or is_full_day_leave

        if is_holiday:
            # 표시는 초 단위로 부드럽게 — 적립(분 절삭)은 퇴근 시 별도 계산.
            # calculate_holiday_overtime와 동일한 차감 항목을 timedelta로 적용.
            deduction_min = break_minutes + overtime_used_today
            if self.lunch_break_enabled:
                deduction_min += self.time_calc.lunch_duration_minutes
            if self.dinner_break_enabled:
                deduction_min += self.time_calc.dinner_duration_minutes
            worked = (now - self.clock_in_time) - timedelta(minutes=deduction_min)
            if worked.total_seconds() < 0:
                worked = timedelta(0)
            remaining = -worked
        else:
            # 평일: 정상 남은 시간 계산. 부분 연차(반차/시간연차)는 leave_used_today에
            # 그대로 반영되어 카운트다운이 단축됨.
            remaining = self.time_calc.calculate_remaining_time(
                self.clock_in_time,
                include_lunch=self.lunch_break_enabled,
                include_dinner=self.dinner_break_enabled,
                current_time=now,
                break_minutes=break_minutes
            )
            # 사용한 추가근무 + 반차만큼 남은 시간 감소 (일찍 퇴근 가능)
            remaining -= timedelta(minutes=total_time_off)

        # 남은 시간 표시 및 추가 근무 처리
        if remaining.total_seconds() < 0:
            # 추가 근무 중 (휴일/연차 override면 출근 직후부터 항상 이 분기)
            day_type = self.time_calc.get_day_type(now, self.db)
            if is_full_day_leave and not is_non_working:
                self.remaining_time_group.setTitle(tr('label.full_day_leave_override'))
            elif day_type == 'weekend':
                self.remaining_time_group.setTitle(tr('label.weekend_work'))
            elif day_type == 'holiday':
                self.remaining_time_group.setTitle(tr('label.holiday_work'))
            else:
                self.remaining_time_group.setTitle(tr('label.overtime_progress'))
            # + 기호로 표시
            total_seconds = int(abs(remaining.total_seconds()))
            hours = total_seconds // 3600
            minutes = (total_seconds % 3600) // 60
            seconds = total_seconds % 60
            remaining_str = f"+{hours:02d}:{minutes:02d}:{seconds:02d}"
            # 퇴근 가능(연장근무 진입) → 그린 피드백
            self.remaining_time_label.setStyleSheet(f"color: {ThemeColors.get('status_normal')};")

        else:
            # 정상 근무 중 — 아직 퇴근 전이므로 기본 텍스트 색
            self.remaining_time_group.setTitle(tr('group.remaining_time'))
            remaining_str = self.time_calc.format_time_delta(remaining)
            self.remaining_time_label.setStyleSheet(f"color: {ThemeColors.get('text_primary')};")


        self._set_text_if_changed(self.remaining_time_label, remaining_str)

        # 진행률 업데이트
        # 휴일은 정해진 근무시간이 없으므로 게이지 의미 없음 → 100%로 채워둠.
        if is_holiday:
            self.progress_bar.setValue(100)
        else:
            progress = self.time_calc.calculate_work_progress(
                self.clock_in_time,
                include_lunch=self.lunch_break_enabled,
                include_dinner=self.dinner_break_enabled,
                current_time=now,
                break_minutes=break_minutes,
                overtime_used_minutes=total_time_off
            )
            self.progress_bar.setValue(int(progress * 100))

        # 예상 퇴근 시간 (외출 시간 포함)
        # 휴일은 정해진 퇴근 시각이 없음 → 출근 시각을 그대로 표시 (= 즉시 적립 시작 의미)
        if is_holiday:
            expected_clock_out = self.clock_in_time
            self._set_text_if_changed(
                self.expected_time_label,
                tr('label.holiday_work_no_clock_out')
            )
        else:
            expected_clock_out = self.time_calc.calculate_clock_out_time(
                self.clock_in_time,
                include_lunch=self.lunch_break_enabled,
                include_dinner=self.dinner_break_enabled,
                break_minutes=break_minutes
            )
            # 추가근무 + 반차 사용한 만큼 예상 퇴근 시간을 앞당김
            expected_clock_out -= timedelta(minutes=total_time_off)
            self._set_text_if_changed(
                self.expected_time_label,
                f"{tr('label.expected_clock_out_prefix')}{self.format_time(expected_clock_out)}"
            )

        # 알림은 NotificationOrchestrator로 위임 (5분 throttle 포함)
        # 휴일이면 "퇴근 30분 전" 알림은 의미 없으므로 플래그로 게이팅.
        self._notif_orch.tick(now, expected_clock_out, remaining.total_seconds(),
                                 is_holiday=is_holiday)

        # 트레이 / 미니 위젯 갱신
        if remaining.total_seconds() < 0:
            display_str = f"+{abs(int(remaining.total_seconds() // 3600)):02d}:{abs(int((remaining.total_seconds() % 3600) // 60)):02d}"
        else:
            display_str = self.time_calc.format_time_delta(remaining)
        self.tray_icon.update_time_display(display_str)
        if getattr(self, '_mini_widget', None) is not None and self._mini_widget.isVisible():
            self._mini_widget.update_remaining(remaining_str)

    def update_date_label(self):
        """날짜 라벨 업데이트"""
        now = datetime.now()
        weekday_keys = ['label.weekday_mon', 'label.weekday_tue', 'label.weekday_wed',
                        'label.weekday_thu', 'label.weekday_fri', 'label.weekday_sat',
                        'label.weekday_sun']
        weekday = tr(weekday_keys[now.weekday()])
        self.date_label.setText(tr('date_format.full', year=now.year, month=now.month, day=now.day, weekday=weekday))

    def _render_full_day_leave_state(self, today_str: str) -> None:
        """오늘이 종일 연차이고 출근 안 한 상태 → 카운트다운 대신 휴가 카드 표시."""
        records = self.db.get_leave_records_by_date(today_str)
        # 가장 큰 일수의 leave_type을 대표로 표시 (보통 1.0짜리 1건)
        if records:
            primary = max(records, key=lambda r: r.get('days') or 0)
            label = primary.get('leave_type') or tr('leave.type.annual')
            memo = primary.get('memo') or ''
        else:
            label = tr('leave.type.annual')
            memo = ''

        self.remaining_time_group.setTitle(tr('label.full_day_leave_today'))
        self.remaining_time_label.setText(tr('label.full_day_leave_in_use'))
        self.remaining_time_label.setStyleSheet(
            f"color: {ThemeColors.get('status_normal')}; font-size: 18px;"
        )
        self.progress_bar.setValue(100)
        if memo:
            self._set_text_if_changed(self.expected_time_label,
                                        tr('label.full_day_leave_format', type=label, memo=memo))
        else:
            self._set_text_if_changed(self.expected_time_label,
                                        label)
        # 트레이/미니 위젯
        self.tray_icon.update_time_display(tr('label.vacation'))
        if getattr(self, '_mini_widget', None) is not None and self._mini_widget.isVisible():
            self._mini_widget.update_remaining(tr('label.vacation'))

    def toggle_lunch_break(self):
        """점심시간 토글 — MealController 위임."""
        self._meal.toggle_lunch()

    def toggle_dinner_break(self):
        """저녁시간 토글 — MealController 위임."""
        self._meal.toggle_dinner()

    def update_lunch_status(self):
        """점심시간 상태 업데이트 — MealController 위임."""
        self._meal.refresh_lunch_label()

    def update_dinner_status(self):
        """저녁시간 상태 업데이트 — MealController 위임."""
        self._meal.refresh_dinner_label()

    def update_overtime_balance(self):
        """연장근무 잔액 업데이트"""
        balance_minutes = self.db.get_total_overtime_balance()
        tokens, time_str = self.time_calc.format_overtime_tokens(balance_minutes)

        hours = balance_minutes // 60
        mins = balance_minutes % 60
        self.overtime_balance_label.setText(tr('label.time_hours_minutes', hours=hours, minutes=mins))
        self.update_total_time()

    def update_leave_balance(self):
        """연차 잔액 업데이트"""
        balance = self.db.get_leave_balance()
        balance_hours = int(balance * 8)
        balance_mins = int((balance * 8 % 1) * 60)
        self.leave_balance_label.setText(tr('label.time_hours_minutes', hours=balance_hours, minutes=balance_mins))
        self.update_total_time()

    def update_total_time(self):
        """연차 + 연장근무 총합 시간 업데이트"""
        # 연장근무 시간 (분)
        overtime_minutes = self.db.get_total_overtime_balance()

        # 연차 시간 (일 -> 분으로 변환, 1일 = 8시간 = 480분)
        leave_balance = self.db.get_leave_balance()
        leave_minutes = int(leave_balance * 480)

        # 총합 (분)
        total_minutes = overtime_minutes + leave_minutes
        total_hours = total_minutes // 60
        total_mins = total_minutes % 60

        self.total_time_label.setText(tr('label.time_hours_minutes', hours=total_hours, minutes=total_mins))

    def use_overtime(self, minutes: int):
        """연장근무 사용 (음수 잔액 허용 - 선사용 후적립 가능)"""
        balance = self.db.get_total_overtime_balance()
        new_balance = balance - minutes

        # 음수가 되는 경우 추가 경고
        if new_balance < 0:
            reply = QMessageBox.warning(
                self,
                tr('msg.overtime_use_minus.title'),
                tr('msg.overtime_use_minus.body', minutes=minutes, balance=balance, new_balance=new_balance),
                QMessageBox.Yes | QMessageBox.No
            )
        else:
            reply = QMessageBox.question(
                self,
                tr('msg.overtime_use.title'),
                tr('msg.overtime_use.body', minutes=minutes, balance=balance, new_balance=new_balance),
                QMessageBox.Yes | QMessageBox.No
            )

        if reply == QMessageBox.Yes:
            try:
                # 오늘 날짜
                from datetime import date
                today = date.today().isoformat()

                # 추가근무 사용 기록 추가 (work_record_id는 NULL로 - 직접 사용)
                self.db.add_overtime_usage(
                    work_record_id=None,
                    used_minutes=minutes,
                    date=today,
                    reason="직접 사용"
                )

                QMessageBox.information(
                    self,
                    tr('msg.overtime_use_done.title'),
                    tr('msg.overtime_use_done.body', minutes=minutes)
                )
                self.update_overtime_balance()
            except Exception as e:
                QMessageBox.warning(
                    self,
                    tr('msg.overtime_use_fail.title'),
                    str(e)
                )
                self.update_overtime_balance()

    def show_overtime_detail(self):
        """연장근무 상세 내역 보기"""
        from ui.overtime_view import OvertimeView
        dialog = OvertimeView(self, self.db)
        dialog.exec_()
        # 다이얼로그 종료 후 잔액 업데이트
        self.update_overtime_balance()

    def use_leave(self, days: float, date_str: str = None, memo: str = None):
        """연차 사용.

        date_str 생략 시 오늘 날짜로 바로 확인만 받고 처리 — 연장근무 사용
        (use_overtime)과 같은 흐름. 날짜·사유를 지정하려면 상세입력
        (use_custom_leave → AddLeaveDialog)을 쓴다.
        """
        balance = self.db.get_leave_balance()

        if balance < days:
            QMessageBox.warning(
                self,
                tr('msg.leave_short.title'),
                tr('msg.leave_short.body', balance=balance, days=days)
            )
            return

        from datetime import date

        if date_str is None:
            date_str = date.today().isoformat()
        else:
            # 날짜 형식 검증
            try:
                datetime.strptime(date_str, "%Y-%m-%d")
            except ValueError:
                QMessageBox.warning(
                    self,
                    tr('msg.input_error.title'),
                    tr('msg.input_error.date_format')
                )
                return

        # 사용 확인
        if days == 1.0:
            leave_type = tr('leave.type.annual')
            days_str = tr('leave.use.full_day')
        elif days == 0.5:
            leave_type = tr('view.leave.type_half')
            days_str = tr('leave.use.half_day')
        elif days == 0.125:
            leave_type = tr('leave.type.hourly_leave')
            days_str = tr('leave.use.hour_1')
        elif days == 0.0625:
            leave_type = tr('leave.type.hourly_leave')
            days_str = tr('leave.use.min_30')
        else:
            leave_type = tr('leave.type.annual')
            hours = days * 8
            days_str = tr('leave.use.custom', days=days, hours=hours)

        reply = QMessageBox.question(
            self,
            tr('msg.leave_use.title'),
            tr('msg.leave_use_confirm.body', date=date_str, type=leave_type, days=days_str, balance_after=balance - days),
            QMessageBox.Yes | QMessageBox.No
        )

        if reply == QMessageBox.Yes:
            try:
                self.db.use_leave(days, date_str, leave_type, memo or None)
                QMessageBox.information(
                    self,
                    tr('msg.leave_use_done.title'),
                    tr('msg.leave_use_done.body', type=leave_type)
                )
                self.update_leave_balance()
            except ValueError as e:
                # 잔액 부족 등 검증 오류
                QMessageBox.warning(
                    self,
                    tr('msg.leave_use_impossible.title'),
                    str(e)
                )
                self.update_leave_balance()  # 최신 잔액으로 새로고침
            except Exception as e:
                # 기타 데이터베이스 오류
                QMessageBox.critical(
                    self,
                    tr('msg.error.title'),
                    tr('msg.error.body', action=tr('msg.leave_use.title'), error=str(e))
                )

    def use_custom_overtime(self):
        """사용자 정의 추가근무 사용"""
        from PyQt5.QtWidgets import QInputDialog

        balance = self.db.get_total_overtime_balance()

        # 사용할 시간 입력 (30분 단위)
        hours, ok = QInputDialog.getDouble(
            self,
            tr('msg.input.title'),
            tr('msg.overtime_input.body'),
            0.5,
            0.5,
            24.0,
            1
        )

        if not ok:
            return

        # 시간을 분으로 변환
        minutes = int(hours * 60)

        # 30분 단위 검증
        if minutes % 30 != 0:
            QMessageBox.warning(
                self,
                tr('msg.input_error.title'),
                tr('msg.input_error.overtime_unit')
            )
            return

        # use_overtime 메서드 호출 (내부에서 잔액 검증 수행)
        self.use_overtime(minutes)

    def use_custom_leave(self):
        """연차 상세 입력 — 날짜(캘린더)/유형/시간/사유를 한 화면에서.

        연차 내역 화면의 AddLeaveDialog를 그대로 재사용한다. 그쪽은 잔액 외에
        주말·공휴일 차단과 같은 날 1일 초과 중복 검증까지 갖추고 있어,
        메인 창에서 바로 띄우는 편이 QInputDialog 연쇄보다 안전하고 간편하다.
        """
        from PyQt5.QtWidgets import QDialog
        from ui.leave_view import AddLeaveDialog

        dialog = AddLeaveDialog(self, self.db)
        if dialog.exec_() == QDialog.Accepted:
            self.update_leave_balance()

    def show_leave_detail(self):
        """연차 상세 내역 보기"""
        from ui.leave_view import LeaveView
        dialog = LeaveView(self, self.db)
        dialog.exec_()
        # 다이얼로그 종료 후 잔액 업데이트
        self.update_leave_balance()

    def handle_clock_out_button(self):
        """퇴근 버튼 클릭 핸들러 - 상태에 따라 퇴근 또는 취소"""
        if self.is_clocked_in:
            # 출근 중 -> 퇴근 처리
            self.clock_out()
        else:
            # 퇴근 완료 -> 퇴근 취소
            self.cancel_clock_out()

    def current_workday_date(self) -> str:
        """진행 중인(또는 방금 끝난) 근무일 날짜 'YYYY-MM-DD'.

        자정~근무일 경계시간(기본 6시) 사이에는 달력상 날짜가 이미 넘어갔어도
        출근일이 근무일이다. handle_workday_rollover()가 경계시간 전까지
        전날 근무를 유지하는 것과 같은 기준.
        """
        if self.is_clocked_in and self.clock_in_time:
            return self.clock_in_time.date().isoformat()
        now = datetime.now()
        boundary = int(self.db.get_setting(WORKDAY_BOUNDARY_HOUR, '6'))
        today_str = now.date().isoformat()
        # 새벽에 퇴근 직후: 오늘 기록이 아직 없으면 전날이 근무일
        if now.hour < boundary and not self.db.get_work_record(today_str):
            return (now.date() - timedelta(days=1)).isoformat()
        return today_str

    def clock_out(self):
        """퇴근 처리"""
        if not self.is_clocked_in:
            return

        now = datetime.now()
        # 근무일 = 출근일. 자정 넘겨 퇴근하면 now.date()는 이미 다음 날이므로
        # 이 값을 써야 출근일 행에 퇴근이 기록된다 (UPDATE 0건 방지).
        workday = self.clock_in_time.date().isoformat()

        # 확인 메시지
        reply = QMessageBox.question(
            self,
            tr('msg.clock_out_confirm.title'),
            tr('msg.clock_out_confirm.body', time=now.strftime('%H:%M:%S')),
            QMessageBox.Yes | QMessageBox.No
        )

        if reply != QMessageBox.Yes:
            # 취소 시 버튼 다시 활성화
            self.clock_out_button.setEnabled(True)
            return

        # 총 근무시간 계산
        total_hours = self.time_calc.calculate_total_work_time(
            self.clock_in_time, now
        )

        # 주말/공휴일 체크 — 근무 성격은 출근일로 판정 (금요일 야근이 토요일
        # 근무로 뒤바뀌지 않게). 롤오버 경로와 동일 기준.
        is_non_working_day = self.time_calc.is_non_working_day(self.clock_in_time, self.db)
        day_type = self.time_calc.get_day_type(self.clock_in_time, self.db)

        # 근무일의 외출 시간 가져오기
        break_minutes = self.db.get_total_break_minutes_today(workday)

        # 적립 단위(분) — 사용자 설정. 기본 30, 옵션 15/60.
        unit_minutes = self.db.get_setting_int('overtime_unit', 30)
        if unit_minutes not in (15, 30, 60):
            unit_minutes = 30

        if is_non_working_day:
            # 주말/공휴일: 모든 시간을 연장근무로 처리 (식사·외출 시간 제외)
            overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
                self.clock_in_time, now,
                include_lunch=self.lunch_break_enabled,
                include_dinner=self.dinner_break_enabled,
                break_minutes=break_minutes,
                unit_minutes=unit_minutes,
            )
        else:
            # 평일: 정상 연장근무 계산 (외출 시간 포함)
            overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
                self.clock_in_time, now,
                include_lunch=self.lunch_break_enabled,
                include_dinner=self.dinner_break_enabled,
                break_minutes=break_minutes,
                unit_minutes=unit_minutes,
            )

        # AUTO_OVERTIME 가드: 자동 적립 OFF + 적립할 게 있으면 사용자에게 확인
        auto_overtime = self.db.get_setting_bool('auto_overtime', True)
        if not auto_overtime and overtime_earned > 0:
            time_str = format_hours_minutes(overtime_earned, omit_zero_minutes=True)
            actual_str = format_hours_minutes(overtime_actual, omit_zero_minutes=True)
            ask = QMessageBox.question(
                self,
                tr('msg.auto_overtime_confirm.title'),
                tr('msg.auto_overtime_confirm.body', actual=actual_str, earned=time_str),
                QMessageBox.Yes | QMessageBox.No,
            )
            if ask != QMessageBox.Yes:
                overtime_earned = 0  # 적립 스킵 (overtime_actual은 기록용으로 유지)

        # DB 업데이트
        today = workday
        clock_out_str = now.strftime("%H:%M:%S")

        self.db.update_clock_out(
            today, clock_out_str, total_hours,
            overtime_actual, overtime_earned
        )

        # 연장근무 적립 기록
        if overtime_earned > 0:
            today_record = self.db.get_work_record(today)
            if today_record:
                self.db.add_overtime_earned(
                    today_record['id'], overtime_earned, today
                )

        # 상태 업데이트
        self.is_clocked_in = False
        self.midnight_rollover_handled = False  # 다음날을 위해 플래그 리셋
        self.clock_out_button.setEnabled(True)
        self.clock_out_button.setText(tr('btn.clock_out_cancel'))

        # 결과 메시지
        if day_type == 'weekend':
            type_info = tr('label.weekend_work_tag') + '\n'
        elif day_type == 'holiday':
            holiday_info = self.db.get_holiday(today)
            holiday_name = holiday_info['name'] if holiday_info else tr('label.holiday_default')
            type_info = tr('label.holiday_work_tag', name=holiday_name) + '\n'
        else:
            type_info = ''

        overtime_info = ''
        if overtime_earned > 0:
            tokens, time_str = self.time_calc.format_overtime_tokens(overtime_earned)
            if is_non_working_day:
                overtime_info = tr('label.full_earned_msg', time=time_str, tokens=tokens)
            else:
                overtime_info = tr('label.overtime_earned_msg', time=time_str, tokens=tokens)

        msg = tr('msg.clock_out_done.body',
                 type_info=type_info,
                 total_work=tr('label.total_work_hours', hours=total_hours),
                 overtime_info=overtime_info)
        QMessageBox.information(self, tr('msg.clock_out_done.title'), msg)

        # 잔액 업데이트
        self.update_overtime_balance()

        # Discord 웹훅 push (옵션)
        self._discord_push_clock_out(now, total_hours, overtime_actual, overtime_earned)

        # 오늘 요약 카드 표시
        self._show_today_summary(total_hours, overtime_actual, overtime_earned, break_minutes)

    def cancel_clock_out(self):
        """퇴근 취소"""
        # 확인 대화상자
        reply = QMessageBox.question(
            self,
            tr('btn.clock_out_cancel'),
            tr('msg.cancel_clock_out_confirm.body'),
            QMessageBox.Yes | QMessageBox.No
        )

        if reply != QMessageBox.Yes:
            return

        try:
            # DB에서 퇴근 취소 — 새벽 퇴근이면 달력 날짜가 아니라 출근일 행
            success = self.db.cancel_clock_out(self.current_workday_date())

            if success:
                # 상태 복원
                self.is_clocked_in = True
                self.clock_out_button.setEnabled(True)
                self.clock_out_button.setText(tr('btn.clock_out'))

                # 잔액 업데이트
                self.update_overtime_balance()

                QMessageBox.information(
                    self,
                    tr('msg.cancel_clock_out_done.title'),
                    tr('msg.cancel_clock_out_done.body')
                )
            else:
                QMessageBox.warning(
                    self,
                    tr('msg.cancel_clock_out_fail.title'),
                    tr('msg.cancel_clock_out_fail.body')
                )

        except Exception as e:
            QMessageBox.critical(
                self,
                tr('msg.error.title'),
                tr('msg.error.body', action=tr('btn.clock_out_cancel'), error=str(e))
            )

    def _apply_auto_overtime_gate(self, overtime_earned: int) -> int:
        """자동 적립(auto_overtime) 설정을 존중해 적립분을 게이팅.

        OFF면 0을 반환해 은행 적립(add_overtime_earned)을 건너뛰게 한다.
        clock_out()은 대화상자로 직접 확인하지만, 자동 퇴근 경로(롤오버 / 이전일
        자동 처리)는 사용자 상호작용 시점이 없으므로 설정만으로 동일하게 게이팅한다.
        실제 연장(work_records.overtime_minutes)은 그대로 기록되고 적립만 스킵된다.
        """
        if overtime_earned > 0 and not self.db.get_setting_bool('auto_overtime', True):
            return 0
        return overtime_earned

    def handle_workday_rollover(self, now: datetime):
        """근무일 경계 처리: 경계시간 직전 퇴근, 경계시간에 출근

        예: 경계시간이 6시인 경우
        - 전날 근무 → 05:59:59 퇴근 처리 (자정~6시 전까지 초과근무로 인정)
        - 당일 근무 → 06:00:00 출근 처리
        """
        if not self.is_clocked_in or not self.clock_in_time:
            return

        # 이미 처리되었으면 중복 실행 방지
        if self.midnight_rollover_handled:
            return

        # 근무일 경계 시간 가져오기
        workday_boundary_hour = int(self.db.get_setting(WORKDAY_BOUNDARY_HOUR, '6'))
        boundary_time_str = f"{workday_boundary_hour:02d}:00:00"
        before_boundary_str = f"{workday_boundary_hour - 1:02d}:59:59" if workday_boundary_hour > 0 else "23:59:59"

        # 전날 기록은 출근일 날짜로 저장
        workday_str = self.clock_in_time.date().isoformat()

        # 퇴근 시간: 오늘 경계시간 직전 (예: 05:59:59)
        workday_end = datetime.combine(
            now.date(),
            datetime.strptime(before_boundary_str, "%H:%M:%S").time()
        )

        # 외출 중이라면 자동으로 복귀 처리 (출근일 날짜로 조회)
        active_break = self.db.get_active_break_record(target_date=workday_str)
        if active_break:
            self.db.update_break_return(active_break['id'], before_boundary_str)

        # 총 근무시간 계산 (출근 ~ 경계시간 직전)
        total_hours = self.time_calc.calculate_total_work_time(
            self.clock_in_time, workday_end
        )

        # 주말/공휴일 체크
        is_non_working_day = self.time_calc.is_non_working_day(self.clock_in_time, self.db)

        # 외출 시간 가져오기
        conn = self.db.get_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT SUM(total_minutes)
            FROM break_records
            WHERE date = ?
        ''', (workday_str,))
        break_minutes = cursor.fetchone()[0] or 0
        conn.close()

        # 추가근무 계산 (사용자 설정 적립 단위 적용)
        unit_minutes = self.db.get_setting_int('overtime_unit', 30)
        if unit_minutes not in (15, 30, 60):
            unit_minutes = 30
        if is_non_working_day:
            overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
                self.clock_in_time, workday_end,
                include_lunch=self.lunch_break_enabled,
                include_dinner=self.dinner_break_enabled,
                break_minutes=break_minutes,
                unit_minutes=unit_minutes,
            )
        else:
            overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
                self.clock_in_time, workday_end,
                include_lunch=self.lunch_break_enabled,
                include_dinner=self.dinner_break_enabled,
                break_minutes=break_minutes,
                unit_minutes=unit_minutes,
            )

        # 자동 적립 OFF면 적립 스킵 (clock_out '아니오'와 동일 의미)
        overtime_earned = self._apply_auto_overtime_gate(overtime_earned)

        # DB 업데이트 (출근일 날짜에 퇴근 시간 기록)
        self.db.update_clock_out(
            workday_str, before_boundary_str, total_hours,
            overtime_actual, overtime_earned
        )

        # 연장근무 적립
        if overtime_earned > 0:
            workday_record = self.db.get_work_record(workday_str)
            if workday_record:
                self.db.add_overtime_earned(
                    workday_record['id'], overtime_earned, workday_str
                )

        # 오늘 경계시간에 출근 처리 (예: 06:00:00)
        today_str = now.date().isoformat()
        self.db.add_work_record(today_str, boundary_time_str, lunch_break=False, is_manual=False)

        # 상태 업데이트
        self.clock_in_time = datetime.combine(
            now.date(),
            datetime.strptime(boundary_time_str, "%H:%M:%S").time()
        )

        # 외출 중이었다면 오늘도 외출 시작
        if self.is_on_break:
            today_record = self.db.get_today_record()
            if today_record:
                self.db.add_break_record(
                    today_record['id'], today_str, boundary_time_str, None
                )

        # 근무일 경계 처리 완료 플래그 설정
        self.midnight_rollover_handled = True

        QMessageBox.information(
            self,
            tr('msg.workday_boundary.title'),
            tr('msg.workday_boundary.body', hour=workday_boundary_hour, before=before_boundary_str, boundary=boundary_time_str)
        )

        # 화면 업데이트
        self.load_today_data()
        self.update_overtime_balance()

    def start_new_workday(self, now: datetime):
        """새 근무일 시작 (퇴근 완료 상태에서 날짜가 바뀌고 경계 시간 이후)"""
        workday_boundary_hour = int(self.db.get_setting(WORKDAY_BOUNDARY_HOUR, '6'))

        # 오늘 이미 출근 기록이 있는지 확인
        today_str = now.date().isoformat()
        today_record = self.db.get_work_record(today_str)

        if today_record:
            # 이미 오늘 기록이 있으면 그것을 로드
            self.load_today_data()
            return

        # 새 근무일 알림 및 출근 처리
        reply = QMessageBox.question(
            self,
            tr('msg.new_workday.title'),
            tr('msg.new_workday.body', date=today_str),
            QMessageBox.Yes | QMessageBox.No
        )

        if reply == QMessageBox.Yes:
            # 상태 초기화
            self.clock_in_time = None
            self.is_clocked_in = False
            self.lunch_break_enabled = False
            self.dinner_break_enabled = False
            self.midnight_rollover_handled = False
            self.auto_lunch_applied_today = False

            # 새 출근 처리 (load_today_data가 자동 감지 또는 수동 입력 처리)
            self.load_today_data()
        else:
            # 거부하면 상태만 초기화하고 대기
            self.clock_in_time = None
            self.is_clocked_in = False
            self.clock_out_button.setEnabled(False)
            self.clock_out_button.setText(tr('btn.clock_out'))

    def _get_break_minutes_for_date(self, date_str: str) -> int:
        """특정 날짜의 총 외출 시간(분)을 안전하게 조회."""
        try:
            with self.db._conn() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT COALESCE(SUM(total_minutes), 0)
                    FROM break_records
                    WHERE date = ?
                ''', (date_str,))
                return int(cursor.fetchone()[0] or 0)
        except Exception as e:
            dlog(f"_get_break_minutes_for_date failed for {date_str}: {e}")
            return 0

    def auto_clock_out_previous_days(self):
        """이전 퇴근 기록들(퇴근 안 한)에 대해 자동으로 종료 시간 등록"""
        from datetime import timedelta

        # 최근 30일간의 기록 중 퇴근하지 않은 모든 기록 처리
        today = datetime.now().date()

        for days_ago in range(1, 31):  # 1일 전부터 30일 전까지 확인
            check_date = (today - timedelta(days=days_ago)).isoformat()
            try:
                record = self.db.get_work_record(check_date)
            except Exception as e:
                dlog(f"auto_clock_out: get_work_record failed for {check_date}: {e}")
                continue

            # 출근은 했지만 퇴근을 안 한 기록 발견
            if not (record and record.get('clock_in') and not record.get('clock_out')):
                continue

            try:
                # 해당 날짜의 종료 시간 감지
                check_date_obj = today - timedelta(days=days_ago)
                shutdown_time = self.event_monitor.get_shutdown_time_by_date(check_date_obj)

                if shutdown_time:
                    # 출근 시간 파싱
                    clock_in_str = record['clock_in']
                    clock_in_time = datetime.strptime(
                        f"{check_date} {clock_in_str}",
                        "%Y-%m-%d %H:%M:%S"
                    )

                    # 주말/공휴일 체크
                    is_non_working_day = self.time_calc.is_non_working_day(clock_in_time, self.db)
                    day_type = self.time_calc.get_day_type(clock_in_time, self.db)

                    # 외출 시간 가져오기
                    break_minutes = self._get_break_minutes_for_date(check_date)

                    # 총 근무시간 계산 (원본 시간)
                    work_duration = shutdown_time - clock_in_time
                    total_hours = work_duration.total_seconds() / 3600

                    # 점심시간/저녁시간 차감 여부
                    lunch_enabled = bool(record.get('lunch_break', False))
                    dinner_enabled = bool(record.get('dinner_break', False))

                    unit_minutes = self.db.get_setting_int('overtime_unit', 30)
                    if unit_minutes not in (15, 30, 60):
                        unit_minutes = 30
                    if is_non_working_day:
                        # 주말/공휴일: 모든 시간을 연장근무로 처리 (식사·외출 제외)
                        overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
                            clock_in_time, shutdown_time,
                            include_lunch=lunch_enabled,
                            include_dinner=dinner_enabled,
                            break_minutes=break_minutes,
                            unit_minutes=unit_minutes,
                        )
                    else:
                        # 평일: 정상 연장근무 계산 (외출 시간 포함)
                        overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
                            clock_in_time, shutdown_time,
                            include_lunch=lunch_enabled,
                            include_dinner=dinner_enabled,
                            break_minutes=break_minutes,
                            unit_minutes=unit_minutes,
                        )

                    # 자동 적립 OFF면 적립 스킵 (clock_out '아니오'와 동일 의미)
                    overtime_earned = self._apply_auto_overtime_gate(overtime_earned)

                    # DB 업데이트 (total_hours는 원본 시간 그대로 저장)
                    clock_out_str = shutdown_time.strftime("%H:%M:%S")
                    self.db.update_clock_out(
                        check_date, clock_out_str, total_hours,
                        overtime_actual, overtime_earned
                    )

                    # 연장근무 적립
                    if overtime_earned > 0:
                        self.db.add_overtime_earned(
                            record['id'], overtime_earned, check_date
                        )

                    day_tag = " (주말)" if day_type == 'weekend' else (" (공휴일)" if day_type == 'holiday' else "")
                    print(f"{check_date}{day_tag} 퇴근 자동 등록: {clock_out_str} (총 {total_hours:.1f}시간, 적립 {overtime_earned}분)")
                else:
                    # 종료 시간을 찾을 수 없는 경우: 해당 날짜 23:59:59로 처리
                    clock_in_str = record['clock_in']
                    clock_in_time = datetime.strptime(
                        f"{check_date} {clock_in_str}",
                        "%Y-%m-%d %H:%M:%S"
                    )
                    fallback_time = datetime.strptime(
                        f"{check_date} 23:59:59",
                        "%Y-%m-%d %H:%M:%S"
                    )

                    # 주말/공휴일 체크
                    is_non_working_day = self.time_calc.is_non_working_day(clock_in_time, self.db)
                    day_type = self.time_calc.get_day_type(clock_in_time, self.db)

                    # 외출 시간 가져오기
                    break_minutes = self._get_break_minutes_for_date(check_date)

                    # 총 근무시간 계산
                    work_duration = fallback_time - clock_in_time
                    total_hours = work_duration.total_seconds() / 3600

                    lunch_enabled = bool(record.get('lunch_break', False))
                    dinner_enabled = bool(record.get('dinner_break', False))

                    unit_minutes = self.db.get_setting_int('overtime_unit', 30)
                    if unit_minutes not in (15, 30, 60):
                        unit_minutes = 30
                    if is_non_working_day:
                        overtime_actual, overtime_earned = self.time_calc.calculate_holiday_overtime(
                            clock_in_time, fallback_time,
                            include_lunch=lunch_enabled,
                            include_dinner=dinner_enabled,
                            break_minutes=break_minutes,
                            unit_minutes=unit_minutes,
                        )
                    else:
                        overtime_actual, overtime_earned = self.time_calc.calculate_overtime(
                            clock_in_time, fallback_time,
                            include_lunch=lunch_enabled,
                            include_dinner=dinner_enabled,
                            break_minutes=break_minutes,
                            unit_minutes=unit_minutes,
                        )

                    # 자동 적립 OFF면 적립 스킵 (clock_out '아니오'와 동일 의미)
                    overtime_earned = self._apply_auto_overtime_gate(overtime_earned)

                    # DB 업데이트
                    self.db.update_clock_out(
                        check_date, "23:59:59", total_hours,
                        overtime_actual, overtime_earned
                    )

                    # 연장근무 적립
                    if overtime_earned > 0:
                        self.db.add_overtime_earned(
                            record['id'], overtime_earned, check_date
                        )

                    day_tag = " (주말)" if day_type == 'weekend' else (" (공휴일)" if day_type == 'holiday' else "")
                    print(f"{check_date}{day_tag} 퇴근 자동 등록 (fallback): 23:59:59 (총 {total_hours:.1f}시간, 적립 {overtime_earned}분)")
            except Exception as e:
                dlog(f"auto_clock_out: processing failed for {check_date}: {e}")
                continue

    def manual_clock_in(self):
        """수동 출근 시간 입력"""
        # 종일 연차 등록일이면 override 의도 확인
        today_str = datetime.now().date().isoformat()
        if self.db.has_full_day_leave(today_str):
            reply = QMessageBox.question(
                self,
                tr('msg.full_day_leave.title'),
                tr('msg.full_day_leave.body'),
                QMessageBox.Yes | QMessageBox.No,
            )
            if reply != QMessageBox.Yes:
                return

        # 기본값: 기존 출근시간이 있으면 그것을, 없으면 None
        default_time = self.clock_in_time if self.clock_in_time else None

        # 다이얼로그 표시
        dialog = ClockInDialog(self, default_time)

        if dialog.exec_() == dialog.Accepted:
            selected_time = dialog.get_time()

            if selected_time:
                # 출근 시간 설정
                self.clock_in_time = selected_time
                self.is_clocked_in = True
                self.midnight_rollover_handled = False  # 새로운 근무일 시작 시 플래그 리셋

                # DB 저장
                today = datetime.now().date().isoformat()
                clock_in_str = selected_time.strftime("%H:%M:%S")

                # 기존 기록이 있는지 확인
                existing_record = self.db.get_today_record()

                if existing_record:
                    # 기존 기록 업데이트 (출근시간만)
                    conn = self.db.get_connection()
                    cursor = conn.cursor()
                    cursor.execute('''
                        UPDATE work_records
                        SET clock_in = ?, is_manual = 1
                        WHERE date = ?
                    ''', (clock_in_str, today))
                    conn.commit()
                    conn.close()
                else:
                    # 새 기록 추가
                    self.db.add_work_record(today, clock_in_str, is_manual=True)

                # UI 업데이트
                self.clock_in_value.setText(clock_in_str)
                self.clock_out_button.setEnabled(True)
                self.clock_out_button.setText(tr('btn.clock_out'))

                QMessageBox.information(
                    self,
                    tr('msg.clock_in_set.title'),
                    tr('msg.clock_in_set.body', time=clock_in_str)
                )

                # Discord 웹훅 (옵션)
                self._discord_push_clock_in(selected_time)

                # 오늘 요약 카드 숨김 (새 출근 시작)
                if hasattr(self, 'today_summary_card'):
                    self.today_summary_card.hide()

    def show_stats(self):
        """통계 창 표시"""
        dialog = StatsView(self, self.db)
        dialog.exec_()

    def show_calendar(self):
        """캘린더 창 표시"""
        # 도전과제 카운터
        try:
            cur = self.db.get_setting_int('calendar_view_count', 0)
            self.db.set_setting('calendar_view_count', str(cur + 1))
        except Exception as e:
            dlog(f"calendar view counter failed: {e}")
        dialog = CalendarView(self, self.db)
        dialog.exec_()

    def show_schedule(self):
        """통합 스케줄(휴일+연차+반복) 창 표시."""
        from ui.schedule_view import ScheduleView
        dlg = ScheduleView(self, self.db)
        dlg.exec_()

    def show_leave_management(self):
        """휴가 관리 창 표시"""
        dialog = LeaveView(self, self.db)
        dialog.exec_()

    def apply_theme(self, theme_name: str):
        """테마 적용"""
        self.current_theme = theme_name
        ThemeColors.set_theme(theme_name)
        self.setStyleSheet(get_theme(theme_name))
        apply_dark_titlebar(self, theme_name == 'dark')
        # 버튼 아이콘을 새 테마 색으로 재틴팅 (init_ui 이후에만)
        if hasattr(self, '_nav_icon_specs'):
            self._apply_button_icons()
        # 트레이 메뉴도 새 테마 QSS/아이콘으로 갱신
        if getattr(self, 'tray_icon', None) is not None:
            self.tray_icon.refresh_theme()
        # 타이틀바 갱신을 위해 크기 미세 조정
        size = self.size()
        self.resize(size.width() + 1, size.height())
        self.resize(size)

    def show_settings(self):
        """설정 창 표시"""
        dialog = SettingsView(self, self.db)
        dialog.exec_()
        # 설정 변경 후 테마 재적용
        new_theme = str(self.db.get_setting(THEME, 'dark'))
        if new_theme != self.current_theme:
            self.apply_theme(new_theme)

    def show_help(self):
        """사용 설명 가이드 창 표시"""
        from ui.help_view import HelpView
        dialog = HelpView(self)
        dialog.exec_()

    def show_achievements(self):
        """도전과제 다이얼로그 표시."""
        from ui.achievements_view import AchievementsView
        # 진입 시 즉시 한 번 평가 — UI에 최신 진행도 반영
        try:
            from core.achievements import evaluate_all
            evaluate_all(self.db)
        except Exception as e:
            dlog(f"achievements evaluate failed: {e}")
        dialog = AchievementsView(self.db, self)
        dialog.exec_()

    def _show_meal_context(self, meal_type: str, button, pos):
        """점심/저녁 버튼 우클릭 → 실제 시간 입력 메뉴."""
        from PyQt5.QtWidgets import QMenu
        from ui.meal_time_dialog import MealTimeDialog
        menu = QMenu(self)
        title = tr('label.lunch_short') if meal_type == 'lunch' else tr('label.dinner_short')
        edit_action = menu.addAction(tr('msg.meal_actual_input', meal=title))
        global_pos = button.mapToGlobal(pos)
        action = menu.exec_(global_pos)
        if action != edit_action:
            return
        if not self.is_clocked_in:
            QMessageBox.warning(self, tr('msg.meal_need_clock_in.title'), tr('msg.meal_need_clock_in.body'))
            return
        default_min = (self.time_calc.lunch_duration_minutes
                        if meal_type == 'lunch'
                        else self.time_calc.dinner_duration_minutes)
        # 식사 시각은 출~퇴근 범위 내여야 함. 호출 시점은 항상 출근 후·미퇴근 상태.
        dialog = MealTimeDialog(
            self, meal_type=meal_type, default_minutes=default_min,
            clock_in_time=self.clock_in_time,
        )
        if dialog.exec_() != QDialog.Accepted:
            return
        start, end, minutes = dialog.get_times()
        today = datetime.now().date().isoformat()
        self.db.add_meal_record(today, start, end, meal_type=meal_type)
        # 자동 토글 ON
        if meal_type == 'lunch':
            self.lunch_break_enabled = True
            self.lunch_button.setChecked(True)
            self.update_lunch_status()
            self.db.update_lunch_break(today, True)
            self.auto_lunch_applied_today = True
        else:
            self.dinner_break_enabled = True
            self.dinner_button.setChecked(True)
            self.update_dinner_status()
            self.db.update_dinner_break(today, True)
        QMessageBox.information(self, tr('msg.meal_recorded.title'),
                                 tr('msg.meal_recorded.body', meal=title, minutes=minutes, start=start, end=end))

    def show_onboarding(self):
        """온보딩 위저드 다시 보기."""
        from ui.onboarding_view import OnboardingWizard
        wizard = OnboardingWizard(self.db, self)
        if wizard.exec_():
            self.reload_settings()
            QMessageBox.information(self, tr('msg.settings_updated.title'), tr('msg.settings_updated.body'))

    # ===== Discord 웹훅 push (옵션, 실패 silent) =====
    def _show_today_summary(self, total_hours, overtime_actual, overtime_earned, break_minutes):
        """퇴근 후 요약 카드 표시. 시급 옵션 활성 시 추정 급여도 포함."""
        if not hasattr(self, 'today_summary_card'):
            return
        # 점심/저녁 시간 (플래그 ON이면 설정값, 아니면 0)
        lunch_min = self.time_calc.lunch_duration_minutes if self.lunch_break_enabled else 0
        dinner_min = (self.time_calc.dinner_duration_minutes
                       if getattr(self, 'dinner_break_enabled', False) else 0)

        # 추정 급여 (옵션)
        salary_text = ""
        if self.db.get_setting('salary_enabled', 'false').lower() == 'true':
            try:
                wage = float(self.db.get_setting('hourly_wage', '0') or 0)
                rate = float(self.db.get_setting('overtime_rate', '1.5') or 1.5)
                if wage > 0:
                    from core.salary import estimate_pay, format_won
                    fake_record = {'total_hours': total_hours, 'overtime_minutes': overtime_actual}
                    result = estimate_pay([fake_record], wage, rate)
                    salary_text = tr('label.today_estimate', amount=format_won(result['total']))
            except (ValueError, TypeError):
                pass

        self.today_summary_card.show_summary(
            total_hours=total_hours,
            lunch_minutes=lunch_min,
            dinner_minutes=dinner_min,
            break_minutes=break_minutes,
            overtime_actual=overtime_actual,
            overtime_earned=overtime_earned,
            salary_text=salary_text,
        )

    def _discord_url(self) -> str:
        return self.db.get_setting('discord_webhook_url', '') or ''

    def _discord_push_clock_in(self, when):
        if self.db.get_setting('discord_notif_clock_in', 'true').lower() != 'true':
            return
        url = self._discord_url()
        if not url:
            return
        try:
            from utils import discord_webhook
            ok = discord_webhook.send_clock_in(url, when.strftime('%H:%M:%S'))
            self.db.log_notification('discord', 'clock_in', success=ok)
        except Exception as e:
            from utils.debug_log import dlog
            dlog(f"discord clock_in push failed: {e}")

    def _discord_push_clock_out(self, when, total_hours, overtime_actual, overtime_earned):
        if self.db.get_setting('discord_notif_clock_out', 'true').lower() != 'true':
            return
        url = self._discord_url()
        if not url:
            return
        try:
            from utils import discord_webhook
            ok = discord_webhook.send_clock_out(
                url, when.strftime('%H:%M:%S'),
                total_hours, overtime_actual, overtime_earned,
            )
            self.db.log_notification('discord', 'clock_out', success=ok)
        except Exception as e:
            from utils.debug_log import dlog
            dlog(f"discord clock_out push failed: {e}")

    def check_for_updates(self, silent: bool = False):
        """업데이트 확인. silent=True면 새 버전 있을 때만 알림 (시작 시 자동 체크용)."""
        from core.version import __version__
        from utils.updater_client import (
            check_for_update, download_update, apply_update,
            UP_TO_DATE, NETWORK_ERROR, NO_RELEASE, NO_ASSET,
        )

        info, reason = check_for_update(__version__)
        if info is None:
            if silent:
                return
            # 사용자가 명시적으로 트리거한 경우만 메시지 표시
            messages = {
                UP_TO_DATE: (tr('update.check_title'), tr('update.up_to_date', version=__version__)),
                NETWORK_ERROR: (tr('msg.error.title'),
                                 tr('update.network_error')),
                NO_RELEASE: (tr('msg.no_data.title'),
                              tr('update.no_release')),
                NO_ASSET: (tr('msg.confirm_delete.title'),
                            tr('update.no_asset')),
            }
            title, body = messages.get(reason, (tr('update.check_title'), tr('update.unknown')))
            QMessageBox.information(self, title, body)
            return

        # 빌드된 환경이 아니면 (개발 .py) 실제 적용 불가 — 알림만
        if not getattr(sys, 'frozen', False):
            QMessageBox.information(
                self,
                tr('update.new_version_title'),
                tr('update.new_found_dev', version=info.version)
            )
            return

        reply = QMessageBox.question(
            self,
            tr('update.new_version_title'),
            tr('update.new_found', current=__version__, new=info.version, notes=info.notes[:500]),
            QMessageBox.Yes | QMessageBox.No,
        )
        if reply != QMessageBox.Yes:
            return

        # 다운로드 (모달 진행 다이얼로그)
        from PyQt5.QtWidgets import QProgressDialog
        progress = QProgressDialog(tr('update.downloading'), tr('btn.cancel'), 0, 100, self)
        progress.setWindowTitle(tr('update.download_title'))
        progress.setWindowModality(Qt.WindowModal)
        progress.setMinimumDuration(0)

        def cb(downloaded, total):
            if total > 0:
                progress.setValue(int(downloaded * 100 / total))
            QApplication.processEvents()

        new_exe = download_update(info.asset_url, progress_cb=cb)
        progress.close()

        if new_exe is None:
            QMessageBox.critical(self, tr('msg.error.title'), tr('update.download_failed'))
            return

        if not apply_update(new_exe):
            QMessageBox.critical(
                self, tr('update.apply_failed_title'),
                tr('update.updater_failed')
            )
            return

        # updater.exe가 메인 종료를 기다리고 있음 → 즉시 종료
        QMessageBox.information(self, tr('msg.error.title'), tr('update.restart'))
        QApplication.quit()

    def show_mini_widget(self):
        """미니 위젯 표시 (Always-on-top)"""
        if not hasattr(self, '_mini_widget') or self._mini_widget is None:
            from ui.mini_widget import MiniWidget
            self._mini_widget = MiniWidget(self)
        self._mini_widget.show()
        self._mini_widget.raise_()

    def show_break_management(self):
        """외출 관리 창 표시"""
        dialog = BreakView(self, self.db)
        dialog.exec_()

    def break_out(self, silent: bool = False):
        """외출 처리. silent=True면 다이얼로그 없이 (잠금 자동 외출용)."""
        if not self.is_clocked_in:
            if not silent:
                QMessageBox.warning(self, tr('break.cannot_no_clock_in.title'), tr('break.cannot_no_clock_in.body'))
            return

        if self.is_on_break:
            if not silent:
                QMessageBox.warning(self, tr('break.cannot_no_clock_in.title'), tr('break.cannot_already_on_break.body'))
            return

        now = datetime.now()
        today = now.date().isoformat()
        break_out_str = now.strftime("%H:%M:%S")

        today_record = self.db.get_today_record()
        if not today_record:
            if not silent:
                QMessageBox.warning(self, tr('break.cannot_no_clock_in.title'), tr('break.cannot_no_record.body'))
            return

        work_record_id = today_record['id']
        reason = tr('break.reason.lock') if silent else None
        self.db.add_break_record(work_record_id, today, break_out_str, reason)

        self.is_on_break = True
        self.break_out_button.setEnabled(False)
        self.break_in_button.setEnabled(True)

        self.update_break_status()

        if not silent:
            QMessageBox.information(self, tr('break.started.title'), tr('break.started.body', time=break_out_str))

    def break_in(self, silent: bool = False):
        """복귀 처리. silent=True면 다이얼로그 없이."""
        if not self.is_on_break:
            return

        now = datetime.now()

        active_break = self.db.get_active_break_record()

        if not active_break:
            if not silent:
                QMessageBox.warning(self, tr('msg.error.title'), tr('break.cannot_return_no_active.body'))
            return

        if not active_break.get('break_out'):
            if not silent:
                QMessageBox.warning(self, tr('msg.error.title'), tr('break.cannot_return_corrupt.body'))
            return

        # 복귀 시간 업데이트
        break_in_str = now.strftime("%H:%M:%S")
        self.db.update_break_return(active_break['id'], break_in_str)

        self.is_on_break = False
        self.break_out_button.setEnabled(True)
        self.break_in_button.setEnabled(False)

        self.update_break_status()

        # 외출 시간 계산 (자정 경계 처리)
        # break_record에 저장된 날짜를 사용하여 자정 경계 문제 해결
        break_date = active_break['date']
        try:
            break_date_obj = datetime.strptime(break_date, "%Y-%m-%d").date()
            break_out_parsed = datetime.strptime(active_break['break_out'], "%H:%M:%S").time()
        except ValueError:
            if not silent:
                QMessageBox.warning(self, tr('msg.error.title'), tr('break.cannot_return_unusable.body'))
            dlog(f"break_in parse failed: date={break_date}, break_out={active_break.get('break_out')}")
            return

        break_out_time = datetime.combine(break_date_obj, break_out_parsed)
        break_in_time = datetime.combine(
            break_date_obj,
            datetime.strptime(break_in_str, "%H:%M:%S").time()
        )

        # 복귀 시간이 외출 시간보다 이전이면 자정을 넘긴 것으로 판단
        if break_in_time < break_out_time:
            from datetime import timedelta
            break_in_time += timedelta(days=1)  # 복귀는 다음 날로 처리

        duration_minutes = int((break_in_time - break_out_time).total_seconds() / 60)

        if not silent:
            QMessageBox.information(
                self,
                tr('break.return.title'),
                tr('break.return.body', time=break_in_str, minutes=duration_minutes)
            )

    def update_break_status(self):
        """외출 상태 업데이트"""
        active_break = self.db.get_active_break_record()

        if active_break:
            break_out = active_break['break_out']
            self.break_status_label.setText(tr('break.status_in_progress', time=break_out))
            self.break_status_label.setStyleSheet(f"color: {ThemeColors.get('status_break_active')}; font-weight: bold;")
            self.is_on_break = True
            self.break_out_button.setEnabled(False)
            self.break_in_button.setEnabled(True)
        else:
            total_minutes = self.db.get_total_break_minutes_today()
            if total_minutes > 0:
                hours = total_minutes // 60
                minutes = total_minutes % 60
                if hours > 0:
                    self.break_status_label.setText(tr('break.status_total_hours_minutes', hours=hours, minutes=minutes))
                else:
                    self.break_status_label.setText(tr('break.status_total_minutes', minutes=minutes))
                self.break_status_label.setStyleSheet(f"color: {ThemeColors.get('status_break_idle')};")
            else:
                self.break_status_label.setText("")

            self.is_on_break = False
            self.break_out_button.setEnabled(True)
            self.break_in_button.setEnabled(False)

    def _build_time_calc(self, settings: dict):
        """settings dict로부터 TimeCalculator 생성.

        Database.get_settings()가 이미 숫자 문자열을 int로 자동 변환하므로
        추가 캐스팅은 불필요. work_minutes 우선, 없으면 work_hours*60 폴백.
        """
        work_minutes = settings.get(WORK_MINUTES)
        if work_minutes is None:
            try:
                work_minutes = int(round(float(settings.get(WORK_HOURS, 8)) * 60))
            except (ValueError, TypeError):
                work_minutes = 480
        return TimeCalculator(
            work_minutes=int(work_minutes),
            lunch_duration_minutes=int(settings.get(LUNCH_DURATION_MINUTES, 60)),
            dinner_duration_minutes=int(settings.get(DINNER_DURATION_MINUTES, 60)),
        )

    def reload_settings(self):
        """설정 다시 불러오기 (설정 변경 후 호출)"""
        settings = self.db.get_settings()
        self.time_calc = self._build_time_calc(settings)

        # 시간 형식 캐시 갱신
        self.cached_time_format = str(settings.get(TIME_FORMAT, '24'))

        # auto_lunch 캐시 무효화 (설정에서 토글 가능하므로)
        self._auto_lunch.invalidate()

        # update_display 1Hz 핫패스 캐시 무효화
        self._workday_boundary_hour_cache = None
        self._non_working_cache_date = None
        self._non_working_cache_value = None
        self._full_day_leave_cache_date = None
        self._full_day_leave_cache_value = None

        # 접근성 재적용 (글꼴 / 고대비)
        try:
            from ui.accessibility import apply_from_settings as _apply_a11y
            _apply_a11y(self.db)
        except Exception as e:
            dlog(f"accessibility reapply failed: {e}")

        # UI 업데이트
        self.update_overtime_balance()
        self.update_leave_balance()

        # 시간 표시 형식이 변경되었을 경우 디스플레이 즉시 업데이트
        if self.is_clocked_in and self.clock_in_time:
            self.update_display()

    def _format_leave_used(self, days: float) -> str:
        """연차 사용량을 '30분' / '3시간 45분' / '1일 4시간' 형태로.

        기존에는 `int(days * 8)`로 시간만 뽑아 30분 연차(0.0625일)가 "0시간"으로
        표시됐다. 환산 기준도 DB 정본(`days * work_minutes`)에 맞춰 단축근무
        사용자의 반차가 4시간으로 뻥튀기되지 않게 한다.
        """
        d, rem = format_leave_amount(days, self.db.get_work_minutes())
        if d and rem:
            return tr('report.leave_used_days_time', days=d,
                       time=format_hours_minutes(rem, omit_zero_minutes=True))
        if d:
            return tr('report.leave_used_days', days=d)
        return format_hours_minutes(rem, omit_zero_minutes=True)

    def _append_meal_section(self, report_lines, today: str, label: str,
                              flag: bool, records: list, default_min: int) -> None:
        """일일 보고서의 점심/저녁 섹션 출력.

        실측 기록(`break_records.break_type='lunch'/'dinner'`)이 있으면
        시작/종료 시각과 실측 분을 표시하고, 플래그만 켜진 경우엔
        설정값(`lunch_duration_minutes`/`dinner_duration_minutes`)을 표시.
        """
        actual_min = sum((r.get('total_minutes') or 0) for r in records)
        if records and actual_min > 0:
            report_lines.append(tr('report.meal_actual', label=label, time=format_hours_minutes(actual_min)))
            for r in records:
                try:
                    start_dt = datetime.fromisoformat(f"{today} {r['break_out']}")
                except (KeyError, ValueError, TypeError):
                    continue
                if r.get('break_in'):
                    try:
                        end_dt = datetime.fromisoformat(f"{today} {r['break_in']}")
                    except (ValueError, TypeError):
                        continue
                    if end_dt < start_dt:
                        end_dt += timedelta(days=1)
                    dur = int((end_dt - start_dt).total_seconds() / 60)
                    report_lines.append(
                        tr('report.break_detail', start=self.format_time(start_dt), end=self.format_time(end_dt), duration=dur, reason='')
                    )
                else:
                    report_lines.append(tr('report.meal_in_progress', start=self.format_time(start_dt)))
        elif flag:
            report_lines.append(tr('report.meal_default', label=label, time=format_hours_minutes(default_min)))
        else:
            return
        report_lines.append("")

    def generate_daily_report(self, target_date: str = None):
        """근무 내역 보고서 생성 및 클립보드 복사.

        Args:
            target_date: 'YYYY-MM-DD'. 생략 시 진행 중인 근무일
                (자정~경계시간 사이면 출근일). 캘린더에서 과거 날짜로도 호출.
        """
        # 도전과제 카운터 (보고서 생성 횟수)
        try:
            cur = self.db.get_setting_int('daily_report_count', 0)
            self.db.set_setting('daily_report_count', str(cur + 1))
        except Exception as e:
            dlog(f"daily report counter failed: {e}")

        date_str = target_date or self.current_workday_date()

        # 해당 근무일의 기록 조회
        work_record = self.db.get_work_record(date_str)

        if not work_record:
            QMessageBox.warning(
                self,
                tr('msg.no_record.title'),
                tr('msg.no_record.body')
            )
            return

        # 보고서 작성
        report_lines = []
        report_lines.append("=" * 40)
        report_lines.append(tr('report.title', date=date_str))
        report_lines.append("=" * 40)
        report_lines.append("")

        # 출근/퇴근 시간 (퇴근이 출근보다 이르면 익일 퇴근)
        clock_in_dt = datetime.fromisoformat(f"{date_str} {work_record['clock_in']}")
        report_lines.append(tr('report.clock_in', time=self.format_time(clock_in_dt, include_seconds=True)))

        if work_record['clock_out']:
            _, clock_out_dt = resolve_work_datetimes(
                date_str, work_record['clock_in'], work_record['clock_out'])
            out_str = self.format_time(clock_out_dt, include_seconds=True)
            if clock_out_dt.date() != clock_in_dt.date():
                out_str = tr('label.next_day_time', time=out_str)
            report_lines.append(tr('report.clock_out', time=out_str))

            # 총 근무 시간
            total_work_hours = work_record.get('total_hours') or work_record.get('work_hours', 0)
            if total_work_hours:
                hours = int(total_work_hours)
                minutes = int((total_work_hours - hours) * 60)
                report_lines.append(tr('report.total_work', time=tr('label.time_hours_minutes', hours=hours, minutes=minutes)))
        else:
            report_lines.append(tr('report.not_clocked_out'))

        report_lines.append("")

        # 외출 / 점심 / 저녁 분리 — break_type 으로 구분 (v2.7.0+)
        # 'break'(또는 NULL) = 일반 외출, 'lunch'/'dinner' = 실측 식사 기록
        all_break_records = self.db.get_break_records_by_date(date_str)
        real_break_records = [b for b in all_break_records
                              if (b.get('break_type') or 'break') == 'break']
        lunch_records = [b for b in all_break_records if b.get('break_type') == 'lunch']
        dinner_records = [b for b in all_break_records if b.get('break_type') == 'dinner']

        # 외출 시간 (식사 제외)
        real_break_minutes = sum((b.get('total_minutes') or 0) for b in real_break_records)
        has_active_break = any(not b.get('break_in') for b in real_break_records)
        if real_break_minutes > 0 or has_active_break:
            report_lines.append(tr('report.break_time', time=format_hours_minutes(real_break_minutes)))
            for br in real_break_records:
                break_out_time = datetime.fromisoformat(f"{date_str} {br['break_out']}")
                if br['break_in']:
                    break_in_time = datetime.fromisoformat(f"{date_str} {br['break_in']}")
                    # 자정 경계 처리: 복귀 시간이 외출 시간보다 이전이면 다음날로 간주
                    if break_in_time < break_out_time:
                        break_in_time += timedelta(days=1)
                    duration = int((break_in_time - break_out_time).total_seconds() / 60)
                    reason = f" ({br['reason']})" if br.get('reason') else ""
                    report_lines.append(tr('report.break_detail', start=self.format_time(break_out_time), end=self.format_time(break_in_time), duration=duration, reason=reason))
                else:
                    reason = f" ({br['reason']})" if br.get('reason') else ""
                    report_lines.append(tr('report.break_in_progress', start=self.format_time(break_out_time), reason=reason))
            report_lines.append("")

        # 점심시간
        lunch_flag = bool(work_record.get('lunch_break', False))
        if lunch_flag or lunch_records:
            self._append_meal_section(
                report_lines, date_str, tr('label.lunch'),
                lunch_flag, lunch_records,
                self.time_calc.lunch_duration_minutes,
            )

        # 저녁시간
        dinner_flag = bool(work_record.get('dinner_break', False))
        if dinner_flag or dinner_records:
            self._append_meal_section(
                report_lines, date_str, tr('label.dinner'),
                dinner_flag, dinner_records,
                self.time_calc.dinner_duration_minutes,
            )

        # 추가 근무 적립
        if work_record['overtime_minutes'] and work_record['overtime_minutes'] > 0:
            ot_hours = work_record['overtime_minutes'] // 60
            ot_mins = work_record['overtime_minutes'] % 60

            # 적립된 추가근무 (30분 단위 절삭)
            overtime_earned = work_record.get('overtime_earned', 0)
            earned_hours = overtime_earned // 60
            earned_mins = overtime_earned % 60

            report_lines.append(tr('report.overtime_occurred', time=tr('label.time_hours_minutes', hours=ot_hours, minutes=ot_mins)))
            report_lines.append(tr('report.overtime_banked', time=tr('label.time_hours_minutes', hours=earned_hours, minutes=earned_mins)))
            report_lines.append("")

        # 해당 날짜에 사용한 추가근무
        overtime_used_today = self.db.get_today_overtime_usage(date_str)
        if overtime_used_today > 0:
            used_hours = overtime_used_today // 60
            used_mins = overtime_used_today % 60
            report_lines.append(tr('report.overtime_used', time=tr('label.time_hours_minutes', hours=used_hours, minutes=used_mins)))

            # 사용 상세 내역
            conn = self.db.get_connection()
            cursor = conn.cursor()
            cursor.execute('''
                SELECT used_minutes, reason, created_at
                FROM overtime_usage
                WHERE date = ?
                ORDER BY created_at ASC
            ''', (date_str,))
            usage_records = cursor.fetchall()
            conn.close()

            for record in usage_records:
                used_min = record[0]
                reason = record[1]
                used_h = used_min // 60
                used_m = used_min % 60
                reason_text = f" - {reason}" if reason else ""
                report_lines.append(tr('report.overtime_used_detail', time=tr('label.time_hours_minutes', hours=used_h, minutes=used_m), reason=reason_text))
            report_lines.append("")

        # 해당 날짜에 사용한 연차 (일괄 추가 및 수동 조정 제외)
        leave_records = self.db.get_leave_records(start_date=date_str, end_date=date_str, exclude_bulk=False)

        # manual 타입이거나 메모에 "일괄 추가"가 포함된 것은 제외
        filtered_leave_records = [
            r for r in leave_records
            if r.get('leave_type') != 'manual'
            and not (r.get('memo') and '일괄 추가' in r['memo'])
        ]

        if filtered_leave_records:
            total_leave_days = sum(r['days'] for r in filtered_leave_records)

            value = self._format_leave_used(total_leave_days)
            report_lines.append(tr('report.leave_used', value=value))

            for lr in filtered_leave_records:
                # leave_type을 현재 언어 이름으로 변환
                leave_type_name = {
                    'annual': tr('leave.type.annual'),
                    'sick': tr('leave.type.sick'),
                    'half_am': tr('leave.type.half_am'),
                    'half_pm': tr('leave.type.half_pm'),
                    'time_off': tr('leave.type.time_off'),
                    '연차': tr('leave.type.annual'),
                    '반차': tr('leave.type.half'),
                    '반반차': tr('leave.type.quarter')
                }.get(lr.get('leave_type', ''), lr.get('leave_type', tr('leave.type.annual')))

                time_str = self._format_leave_used(lr['days'])

                memo = f" - {lr['memo']}" if lr.get('memo') else ""
                report_lines.append(f"  - {leave_type_name}: {time_str}{memo}")
            report_lines.append("")

        # 메모
        if work_record['memo']:
            report_lines.append(tr('report.memo', memo=work_record['memo']))
            report_lines.append("")

        report_lines.append("=" * 40)

        # 클립보드에 복사
        report_text = "\n".join(report_lines)
        clipboard = QApplication.clipboard()
        clipboard.setText(report_text)

        # 미리보기 메시지 박스
        QMessageBox.information(
            self,
            tr('report.copied.title'),
            tr('report.copied.body', report=report_text)
        )

    def show_notification(self, title: str, message: str):
        """알림 표시"""
        # 시스템 트레이 알림
        if self.tray_icon.supportsMessages():
            self.tray_icon.showMessage(
                title,
                message,
                QSystemTrayIcon.Information,
                5000  # 5초간 표시
            )
        else:
            # 대체: 메시지 박스
            QMessageBox.information(self, title, message)

    def closeEvent(self, event):
        """창 닫기 이벤트"""
        # 최소화로 변경 (트레이로)
        event.ignore()
        self.hide()
        if self.tray_icon.supportsMessages():
            self.tray_icon.showMessage(
                tr('app.title'),
                tr('tray.background'),
                QSystemTrayIcon.Information,
                2000
            )
