""" 첫 실행 온보딩 위저드. 5단계: 환영 → 근무패턴 → 출근감지 → 연차/시급 → Discord(옵션) → 완료 첫 실행 시 강제 표시. 완료 시 ONBOARDING_COMPLETED=true 저장. 재방문은 메인 메뉴 → "온보딩 다시 보기"로 가능. """ from __future__ import annotations from PyQt5.QtWidgets import (QWizard, QWizardPage, QVBoxLayout, QHBoxLayout, QLabel, QRadioButton, QButtonGroup, QSpinBox, QCheckBox, QLineEdit, QComboBox, QPushButton, QMessageBox, QGroupBox) from PyQt5.QtCore import Qt from ui.styles import apply_dark_titlebar # (label, work_minutes, lunch_minutes) WORK_PRESETS = [ ("표준 8시간 (점심 60분)", 480, 60), ("단축근무 7시간 30분 (점심 30분)", 450, 30), ("단축근무 7시간 (점심 60분)", 420, 60), ("단축근무 6시간 (점심 30분)", 360, 30), ("반일 4시간 (점심 0분)", 240, 0), ] class WelcomePage(QWizardPage): def __init__(self): super().__init__() self.setTitle("👋 환영합니다!") self.setSubTitle("Clock-out Time Calculator를 처음 사용하시는군요. 5단계로 빠르게 설정하겠습니다.") layout = QVBoxLayout() intro = QLabel( "이 앱은:\n" "• 컴퓨터 부팅/잠금 해제로 출근 시간 자동 감지\n" "• 30분 단위 연장근무 적립\n" "• 연차·반차·외출 시간 추적\n" "• 매일 퇴근 시간을 1초마다 카운트다운\n\n" "[다음] 버튼을 눌러 시작하세요." ) intro.setWordWrap(True) layout.addWidget(intro) self.setLayout(layout) class WorkPatternPage(QWizardPage): def __init__(self): super().__init__() self.setTitle("🕘 근무 패턴") self.setSubTitle("본인의 하루 근무 시간을 선택하세요. 나중에 설정에서 바꿀 수 있습니다.") layout = QVBoxLayout() self.button_group = QButtonGroup(self) for i, (label, _, _) in enumerate(WORK_PRESETS): rb = QRadioButton(label) self.button_group.addButton(rb, i) layout.addWidget(rb) if i == 0: rb.setChecked(True) # 사용자 정의 custom_box = QGroupBox("사용자 정의") custom_layout = QHBoxLayout() self.custom_radio = QRadioButton("직접 입력:") self.button_group.addButton(self.custom_radio, len(WORK_PRESETS)) self.hours_spin = QSpinBox() self.hours_spin.setRange(0, 12) self.hours_spin.setValue(8) self.hours_spin.setSuffix(" 시간") self.minutes_spin = QSpinBox() self.minutes_spin.setRange(0, 59) self.minutes_spin.setSingleStep(15) self.minutes_spin.setSuffix(" 분") self.lunch_spin = QSpinBox() self.lunch_spin.setRange(0, 120) self.lunch_spin.setSingleStep(5) self.lunch_spin.setValue(60) self.lunch_spin.setPrefix("점심 ") self.lunch_spin.setSuffix(" 분") custom_layout.addWidget(self.custom_radio) custom_layout.addWidget(self.hours_spin) custom_layout.addWidget(self.minutes_spin) custom_layout.addWidget(self.lunch_spin) custom_layout.addStretch() custom_box.setLayout(custom_layout) layout.addWidget(custom_box) self.setLayout(layout) def selected_minutes(self): idx = self.button_group.checkedId() if 0 <= idx < len(WORK_PRESETS): _, wm, lm = WORK_PRESETS[idx] return wm, lm return self.hours_spin.value() * 60 + self.minutes_spin.value(), self.lunch_spin.value() class ClockInDetectionPage(QWizardPage): def __init__(self): super().__init__() self.setTitle("⏰ 출근 시간 감지 방식") self.setSubTitle("앱이 출근 시간을 자동으로 어떻게 감지할지 선택하세요.") layout = QVBoxLayout() self.option_boot = QRadioButton("PC 부팅 시간 (기본 — 매일 PC를 끄는 경우)") self.option_unlock = QRadioButton("화면 잠금 해제 시간 (PC를 안 끄고 다니는 경우)") self.option_manual = QRadioButton("수동 입력만 (자동 감지 안 함)") self.option_boot.setChecked(True) for opt in (self.option_boot, self.option_unlock, self.option_manual): layout.addWidget(opt) info = QLabel( "\n💡 PC를 항상 켜둔 채 출근하시는 분은 두 번째 옵션을 권장합니다." ) info.setWordWrap(True) info.setStyleSheet("color: #888; padding: 8px;") layout.addWidget(info) layout.addStretch() self.setLayout(layout) def detection_mode(self) -> str: if self.option_unlock.isChecked(): return 'unlock' if self.option_manual.isChecked(): return 'manual' return 'boot' class LeaveSalaryPage(QWizardPage): def __init__(self): super().__init__() self.setTitle("🌴 연차 + 💰 급여 (옵션)") self.setSubTitle("연차 일수와 급여(선택)를 입력하세요.") layout = QVBoxLayout() # 연차 leave_box = QGroupBox("연간 연차") leave_layout = QHBoxLayout() self.leave_spin = QSpinBox() self.leave_spin.setRange(0, 30) self.leave_spin.setValue(15) self.leave_spin.setSuffix(" 일") leave_layout.addWidget(QLabel("내 연차:")) leave_layout.addWidget(self.leave_spin) leave_layout.addStretch() leave_box.setLayout(leave_layout) layout.addWidget(leave_box) # 급여 (옵션) salary_box = QGroupBox("급여 추정 (옵션 — 포괄임금이면 비활성)") salary_layout = QVBoxLayout() self.salary_enabled = QCheckBox("급여 추정 활성화") salary_layout.addWidget(self.salary_enabled) wage_row = QHBoxLayout() wage_row.addWidget(QLabel("시급:")) self.wage_spin = QSpinBox() self.wage_spin.setRange(0, 1000000) self.wage_spin.setSingleStep(1000) self.wage_spin.setSuffix(" 원/시간") self.wage_spin.setValue(0) self.wage_spin.setEnabled(False) wage_row.addWidget(self.wage_spin) wage_row.addStretch() salary_layout.addLayout(wage_row) rate_row = QHBoxLayout() rate_row.addWidget(QLabel("연장수당 가산률:")) self.rate_combo = QComboBox() self.rate_combo.addItem("1.0배 (가산 없음)", 1.0) self.rate_combo.addItem("1.5배 (한국 노동법 기본)", 1.5) self.rate_combo.addItem("2.0배 (야근/휴일 가산)", 2.0) self.rate_combo.setCurrentIndex(1) self.rate_combo.setEnabled(False) rate_row.addWidget(self.rate_combo) rate_row.addStretch() salary_layout.addLayout(rate_row) self.salary_enabled.toggled.connect(self.wage_spin.setEnabled) self.salary_enabled.toggled.connect(self.rate_combo.setEnabled) salary_box.setLayout(salary_layout) layout.addWidget(salary_box) layout.addStretch() self.setLayout(layout) class DiscordPage(QWizardPage): def __init__(self): super().__init__() self.setTitle("💬 Discord 알림 (선택)") self.setSubTitle("출퇴근 시각·휴식 권고를 Discord로 받으려면 웹훅 URL을 입력하세요. (모바일에서 푸시 알림)") layout = QVBoxLayout() self.enable_check = QCheckBox("Discord 웹훅 알림 사용") layout.addWidget(self.enable_check) self.url_edit = QLineEdit() self.url_edit.setPlaceholderText("https://discord.com/api/webhooks/...") self.url_edit.setEnabled(False) layout.addWidget(self.url_edit) guide = QLabel( "셋업 방법:\n" "1. Discord 서버에서 채널 우클릭 → 편집 → 연동 → 웹훅\n" "2. 새 웹훅 만들기 → URL 복사\n" "3. 위 입력란에 붙여넣기" ) guide.setStyleSheet("color: #888; padding: 6px;") guide.setWordWrap(True) layout.addWidget(guide) test_row = QHBoxLayout() self.test_btn = QPushButton("테스트 메시지 보내기") self.test_btn.setEnabled(False) self.test_btn.clicked.connect(self._test_webhook) test_row.addWidget(self.test_btn) test_row.addStretch() layout.addLayout(test_row) self.enable_check.toggled.connect(self.url_edit.setEnabled) self.enable_check.toggled.connect(self.test_btn.setEnabled) layout.addStretch() self.setLayout(layout) def _test_webhook(self): url = self.url_edit.text().strip() if not url: QMessageBox.warning(self, "URL 필요", "웹훅 URL을 먼저 입력해주세요.") return from utils import discord_webhook ok = discord_webhook.send_test(url) if ok: QMessageBox.information(self, "성공", "Discord 채널에서 테스트 메시지를 확인하세요.") else: QMessageBox.warning(self, "실패", "전송 실패. URL을 다시 확인해주세요.") class FinishPage(QWizardPage): def __init__(self): super().__init__() self.setTitle("🎉 준비 완료!") self.setSubTitle("이제 출근부터 자동 추적됩니다.") layout = QVBoxLayout() msg = QLabel( "설정한 내용은 [설정] 메뉴에서 언제든 바꿀 수 있습니다.\n" "온보딩을 다시 보고 싶으면 [도움말 → 온보딩 다시 보기]를 누르세요.\n\n" "🕐 단축키:\n" " • Ctrl+O — 출퇴근 토글\n" " • F1 — 도움말\n" " • F5 — 업데이트 확인\n" " • Ctrl+, — 설정" ) msg.setWordWrap(True) layout.addWidget(msg) self.setLayout(layout) class OnboardingWizard(QWizard): """5단계 첫 실행 위저드. accept() 시 모든 설정 저장 + ONBOARDING_COMPLETED=true.""" def __init__(self, db, parent=None): super().__init__(parent) self.db = db self.setWindowTitle("Clock-out Calculator — 시작 설정") self.setMinimumSize(600, 500) self.setWizardStyle(QWizard.ModernStyle) self.setOption(QWizard.NoBackButtonOnStartPage, True) self.welcome_page = WelcomePage() self.work_page = WorkPatternPage() self.detect_page = ClockInDetectionPage() self.leave_page = LeaveSalaryPage() self.discord_page = DiscordPage() self.finish_page = FinishPage() for page in (self.welcome_page, self.work_page, self.detect_page, self.leave_page, self.discord_page, self.finish_page): self.addPage(page) apply_dark_titlebar(self) def accept(self): # 1. 근무 패턴 wm, lm = self.work_page.selected_minutes() if wm < 30: QMessageBox.warning(self, "입력 오류", "하루 근무는 최소 30분 이상이어야 합니다.") return settings = { 'work_minutes': wm, 'lunch_duration_minutes': lm, 'annual_leave_days': self.leave_page.leave_spin.value(), 'annual_leave_total': self.leave_page.leave_spin.value(), 'salary_enabled': self.leave_page.salary_enabled.isChecked(), 'hourly_wage': self.leave_page.wage_spin.value(), 'overtime_rate': self.leave_page.rate_combo.currentData(), 'onboarding_completed': True, } # 2. 출근 감지 방식 mode = self.detect_page.detection_mode() settings['clock_in_on_unlock'] = (mode == 'unlock') # 3. Discord 웹훅 (옵션) if self.discord_page.enable_check.isChecked(): settings['discord_webhook_url'] = self.discord_page.url_edit.text().strip() self.db.save_settings(settings) super().accept() def maybe_show_onboarding(db, parent=None) -> bool: """ONBOARDING_COMPLETED=false 일 때만 위저드 표시. 사용자가 끝까지 완료하면 True.""" if db.get_setting('onboarding_completed', 'false').lower() == 'true': return False # 이미 완료 wizard = OnboardingWizard(db, parent) return wizard.exec_() == QWizard.Accepted