68893236+KINDNICK@users.noreply.github.com c5df37ca57 v2.8.0: 도전과제 시스템 + 다크 디자인 리뉴얼 + 안정성 강화
Added — 도전과제 시스템 (153개 자동 평가)
- core/achievements.py: 16개 카테고리, 5단계 등급, 시크릿 9개, 메타 도전과제
- ui/achievements_view.py: 4탭 다이얼로그 (전체/진행중/완료/시크릿)
- 5분 throttle 자동 평가 + 시스템 알림 + Discord embed push
- achievements 테이블 확장 (code/category/tier/is_secret/progress/target)
- hire_date 자동 추적, 뷰 진입 카운터 8개 settings 키

Changed — 다크 테마 디자인 리뉴얼
- ui/dark_components.py: 재사용 가능한 다크 컴포넌트 (header/card/button/progress)
- 통계/도움말/도전과제 다이얼로그 일관 다크 톤
- matplotlib 차트 다크 테마 적용 (figure/axes/grid/legend)
- 등급별 카드 그라디언트 (브론즈/실버/골드/플래티넘/레전드)

Fixed — 안정성·일관성
- 타임존 자정 경계 버그 (has_notification_today UTC vs localtime mismatch)
- DB 연결 누수: _conn() 컨텍스트 매니저 도입, 40+ 메서드 변환
- DB 이중 부트스트랩 제거 (MainWindow(db=None) 옵션 인자)
- crash_handler 다단계 폴백 (DB → 파일 → stderr)
- updater PID race: 지수 backoff 재시도 (총 ~9초)
- Discord URL 형식 검증 (snowflake regex)
- 일일 보고서 저녁 섹션, MealTimeDialog 출/퇴근 범위 검증
- check_dinner_reminder 신규, 알림 임계값 5개 설정화
- closeEvent timer/notifier 정리 (aboutToQuit hook)
- 마이그레이션 12개 모두 _conn() + try/finally
- DB 인덱스 5개 추가 (break/overtime/leave date)

Tests
- pytest 116/116 PASS, 통합 시나리오 48/48 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:11:13 +09:00

188 lines
5.8 KiB
Python

"""
Clock-out Time Calculator
퇴근시간 계산 프로그램 - 메인 실행 파일
"""
import sys
import os
# PyQt5 임포트
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtGui import QFont
from PyQt5.QtCore import QLockFile, QDir
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
# 프로젝트 루트를 경로에 추가
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ui.main_window import MainWindow
from core.database import Database
def _ensure_updater_extracted():
"""main.exe에 내장된 updater.exe를 같은 폴더에 추출.
main.exe만 단독 배포해도 자동 업데이트가 동작하도록, 시작 시 한 번
내장본을 같은 폴더로 복사. 이미 같은 사이즈로 있으면 건너뜀.
개발 환경(.py 실행)에서는 별도 빌드된 updater.exe를 사용해야 하므로 no-op.
"""
if not getattr(sys, 'frozen', False):
return
import shutil
from pathlib import Path
main_dir = Path(sys.executable).parent
target = main_dir / 'updater.exe'
bundled = Path(getattr(sys, '_MEIPASS', '')) / 'updater.exe'
if not bundled.exists():
return # 빌드 시 포함 안 됨
needs_extract = (not target.exists()
or target.stat().st_size != bundled.stat().st_size)
if needs_extract:
try:
shutil.copy2(str(bundled), str(target))
except OSError:
pass # 권한 부족 등은 silent — 자동 업데이트만 안 됨
def check_requirements():
"""필수 요구사항 확인"""
try:
import win32evtlog
return True
except ImportError:
return False
def main():
"""메인 함수"""
# PyInstaller frozen exe에 내장된 updater.exe를 옆 폴더에 자동 추출
_ensure_updater_extracted()
app = QApplication(sys.argv)
# 애플리케이션 정보
app.setApplicationName("Clock-out Time Calculator")
app.setOrganizationName("DevUtil")
app.setApplicationVersion("1.0.0")
# 중복 실행 방지 - 로컬 서버로 체크
server_name = "ClockOutCalculatorInstance"
# 이미 실행 중인지 확인
socket = QLocalSocket()
socket.connectToServer(server_name)
if socket.waitForConnected(500):
# 이미 실행 중 - 기존 인스턴스에 "show" 신호 전송
socket.write(b"show")
socket.flush()
socket.waitForBytesWritten(1000)
socket.disconnectFromServer()
return 0
# 새로운 인스턴스 - 서버 시작
server = QLocalServer()
# 기존 서버가 남아있을 수 있으므로 제거
QLocalServer.removeServer(server_name)
if not server.listen(server_name):
QMessageBox.warning(
None,
"서버 오류",
"프로그램 인스턴스 서버를 시작할 수 없습니다."
)
return 1
# 폰트 설정
app.setFont(QFont("Segoe UI", 9))
# 필수 패키지 확인
if not check_requirements():
QMessageBox.critical(
None,
"요구사항 오류",
"필수 패키지가 설치되지 않았습니다.\n\n"
"다음 명령어를 실행하세요:\n"
"pip install -r requirements.txt"
)
return 1
# 데이터베이스 초기화 — db_path_override 설정 시 그 경로 사용 (클라우드 폴더 등)
# 부트스트랩: 기본 DB로 한 번 열어 override 키 확인
from core.settings_keys import DB_PATH_OVERRIDE
bootstrap = Database()
override_path = bootstrap.get_setting(DB_PATH_OVERRIDE, '') or ''
if override_path and os.path.exists(os.path.dirname(override_path) or '.'):
db = Database(override_path)
else:
db = bootstrap
# 1일 1회 자동 백업 (조용히 실패 — 백업 실패가 앱 실행을 막으면 안 됨)
try:
from utils.backup import backup_db_if_needed
backup_db_if_needed(db)
except Exception as e:
from utils.debug_log import dlog
dlog(f"backup failed: {e}")
# 전역 예외 후킹 (crash report)
try:
from utils.crash_handler import install_global_handler
from core.version import __version__
install_global_handler(db, app_version=__version__)
except Exception as e:
from utils.debug_log import dlog
dlog(f"crash handler install failed: {e}")
# 첫 실행 온보딩 (강제) — ONBOARDING_COMPLETED=true 가 아니면 표시
try:
from ui.onboarding_view import maybe_show_onboarding
maybe_show_onboarding(db)
except Exception as e:
from utils.debug_log import dlog
dlog(f"onboarding skipped: {e}")
# 메인 윈도우 생성 및 표시 (위에서 만든 db 재사용 — 이중 부트스트랩 방지)
try:
window = MainWindow(db=db)
# 서버 연결 처리 - 다른 인스턴스에서 show 신호를 받으면 창을 보여줌
def on_new_connection():
client_socket = server.nextPendingConnection()
if client_socket:
client_socket.waitForReadyRead(1000)
data = client_socket.readAll().data()
if data == b"show":
# 창 표시
window.show()
window.raise_()
window.activateWindow()
client_socket.disconnectFromServer()
server.newConnection.connect(on_new_connection)
window.show()
result = app.exec_()
# 서버 종료
server.close()
return result
except Exception as e:
QMessageBox.critical(
None,
"오류",
f"프로그램 실행 중 오류가 발생했습니다:\n\n{str(e)}"
)
server.close()
return 1
if __name__ == "__main__":
sys.exit(main())