- 모던 다크 미니멀 테마(NanumSquare 번들, 단일 accent #4DABF7, 8px radius, flat 버튼, 다크 기본값) - 라인 아이콘 시스템(ui/icons.py, QtSvg) — 앱 전반 이모지 교체 - 다크 깨짐 수정: 테이블 헤더/코너 흰색, 도움말 탭 흰 라인, 트레이/미니위젯 메뉴 - fix: 자동 적립 OFF가 자동 퇴근 경로에서 무시되던 버그(게이팅) - feat: 연장근무 적립 기록 삭제(우클릭) - 테스트 3건 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
189 lines
5.9 KiB
Python
189 lines
5.9 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
|
|
|
|
# 폰트 설정 — 번들 NanumSquare 등록 + 전역 적용 (미설치 시 Malgun Gothic 폴백)
|
|
from utils.font_loader import apply_app_font
|
|
apply_app_font(app, 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())
|