/** * PC(Tauri) 앱 — 업데이트 확인·다운로드·설치 진행 모달 */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import DraggableModalFrame from './DraggableModalFrame'; import { getDesktopAppVersion, runDesktopUpdate, type DesktopUpdatePhase, type DesktopUpdateProgress, } from '../utils/desktopBridge'; /** 다른 오버레이(플로팅 위젯 13000 등)보다 위에 표시 */ const DESKTOP_UPDATE_MODAL_Z = 15000; import { isMacDesktop, isWindowsDesktop } from '../utils/platform'; import { PlatformBrandIcon, type PlatformBrandIconId } from './icons/PlatformBrandIcons'; interface Props { open: boolean; onClose: () => void; /** 열릴 때 자동으로 업데이트 확인 시작 */ autoStart?: boolean; } const PHASE_LABEL: Record = { checking: '업데이트 확인 중', uptodate: '최신 버전', available: '새 버전 발견', downloading: '다운로드 중', installing: '설치 중', relaunching: '재시작 중', error: '업데이트 실패', }; function formatBytes(n?: number): string { if (n == null || !Number.isFinite(n) || n <= 0) return ''; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / (1024 * 1024)).toFixed(1)} MB`; } const DesktopUpdateModal: React.FC = ({ open, onClose, autoStart = true }) => { const [appVersion, setAppVersion] = useState('—'); const [progress, setProgress] = useState(null); const [busy, setBusy] = useState(false); const startedRef = useRef(false); const isActive = busy || progress?.phase === 'checking' || progress?.phase === 'downloading' || progress?.phase === 'installing' || progress?.phase === 'relaunching'; const panelClass = progress?.phase === 'error' ? 'desktop-update-progress--failed' : progress?.phase === 'uptodate' ? 'desktop-update-progress--success' : isActive ? 'desktop-update-progress--active' : ''; const handleRun = useCallback(async (opts?: { bypassLoopGuard?: boolean }) => { if (busy) return; setBusy(true); setProgress({ phase: 'checking', message: '서버에서 새 버전을 확인하는 중…', currentVersion: appVersion }); try { await runDesktopUpdate(p => setProgress(p), { bypassLoopGuard: opts?.bypassLoopGuard ?? true, }); } finally { setBusy(false); } }, [appVersion, busy]); useEffect(() => { if (!open) { startedRef.current = false; setProgress(null); setBusy(false); return; } void getDesktopAppVersion().then(v => { if (v) setAppVersion(v); }); if (autoStart && !startedRef.current) { startedRef.current = true; void handleRun({ bypassLoopGuard: true }); } }, [open, autoStart, handleRun]); if (!open) return null; const phase = progress?.phase ?? 'checking'; const showBar = phase === 'downloading' || phase === 'installing' || phase === 'available'; const barPct = phase === 'installing' ? 100 : progress?.downloadPercent; const platformBrand: PlatformBrandIconId | null = isMacDesktop() ? 'mac' : isWindowsDesktop() ? 'windows' : null; return ( {} : onClose} closeOnBackdrop={!isActive} width={440} zIndex={DESKTOP_UPDATE_MODAL_Z} dialogClassName="sp-modal desktop-update-modal" >
{platformBrand && (
)}

현재 버전 v{progress?.currentVersion ?? appVersion} {progress?.newVersion && progress.newVersion !== progress.currentVersion && ( <> → v{progress.newVersion} )}

{(phase === 'checking' || phase === 'downloading' || phase === 'installing' || phase === 'relaunching') && ( )} {PHASE_LABEL[phase]} {phase === 'downloading' && progress?.downloadPercent != null && ( {progress.downloadPercent}% )}
{showBar && (
{barPct != null ? (
) : (
)}
)}
  • 서버에서 버전 확인
  • 업데이트 다운로드 {progress?.downloadedBytes && progress.totalBytes ? ( {formatBytes(progress.downloadedBytes)} / {formatBytes(progress.totalBytes)} ) : null}
  • 설치 및 재시작
{progress?.message && (

{progress.message}

)} {phase === 'error' && (

{isWindowsDesktop() ? ( <> 자동 업데이트가 설정되지 않았을 수 있습니다. 웹 PC 프로그램 탭에서 exe 설치 파일을 받아 다시 설치해 주세요. ) : isMacDesktop() ? ( <> 자동 업데이트가 설정되지 않았을 수 있습니다. 웹 PC 프로그램 탭에서 dmg를 받아 Applications에 다시 설치해 주세요. ) : ( <> 자동 업데이트가 설정되지 않았을 수 있습니다. 웹 PC 프로그램 탭에서 macOS는 dmg, Windows는 exe 설치 파일을 받아 다시 설치해 주세요. )}

)} {phase === 'uptodate' && (

현재 설치된 버전이 서버와 같습니다.

)}
{phase === 'error' && ( )}
); }; export default DesktopUpdateModal;