Files
goldenChart/desktop/src/StartupUpdateGate.tsx
T
2026-06-15 17:17:06 +09:00

173 lines
5.6 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import DesktopUpdateModal from '@frontend/components/DesktopUpdateModal';
import {
checkForUpdates,
getAppVersion,
type DesktopUpdatePhase,
type DesktopUpdateProgress,
} from './bridge/updater';
import { scheduleMainWindowTitleSync } from './setMainWindowTitle';
import { PlatformBrandIcon, type PlatformBrandIconId } from '@frontend/components/icons/PlatformBrandIcons';
import { isMacDesktop, isWindowsDesktop } from '@frontend/utils/platform';
import '@frontend/App.css';
const PHASE_LABEL: Record<DesktopUpdatePhase, string> = {
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`;
}
function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | null }) {
const phase = progress?.phase ?? 'checking';
const isActive = phase === 'checking';
const showBar = false;
const panelClass = phase === 'error'
? 'desktop-update-progress--failed'
: isActive
? 'desktop-update-progress--active'
: '';
const platformBrand: PlatformBrandIconId | null = isMacDesktop()
? 'mac'
: isWindowsDesktop()
? 'windows'
: null;
return (
<div className="desktop-startup-update">
<div className="desktop-startup-update-card">
{platformBrand && (
<div className="desktop-update-platform-brand desktop-update-platform-brand--startup">
<PlatformBrandIcon platform={platformBrand} />
</div>
)}
<h1 className="desktop-startup-update-title">GoldenChart</h1>
<p className="desktop-startup-update-sub">
{progress?.currentVersion ? `현재 v${progress.currentVersion}` : '시작 중…'}
</p>
<div className={`desktop-update-progress ${panelClass}`.trim()}>
<div className="desktop-update-progress-head">
<span className="desktop-update-progress-title">
{isActive && <span className="desktop-update-spinner" aria-hidden />}
{PHASE_LABEL[phase]}
</span>
</div>
{showBar && (
<div className="desktop-update-bar" role="progressbar" aria-valuemin={0} aria-valuemax={100}>
<div className="desktop-update-bar-fill desktop-update-bar-fill--indeterminate" />
</div>
)}
<ul className="desktop-update-steps">
<li className={`desktop-update-step${phase === 'checking' ? ' active' : phase !== 'error' ? ' done' : ''}`}>
<span className="desktop-update-step-dot" aria-hidden />
서버에서 버전 확인
</li>
<li className="desktop-update-step">
<span className="desktop-update-step-dot" aria-hidden />
업데이트 다운로드
{progress?.downloadedBytes && progress.totalBytes ? (
<span className="desktop-update-bytes">
{formatBytes(progress.downloadedBytes)} / {formatBytes(progress.totalBytes)}
</span>
) : null}
</li>
<li className="desktop-update-step">
<span className="desktop-update-step-dot" aria-hidden />
설치 재시작
</li>
</ul>
{progress?.message && (
<p className={`desktop-update-message${phase === 'error' ? ' desktop-update-message--error' : ''}`}>
{progress.message}
</p>
)}
</div>
</div>
</div>
);
}
/** 시작 시 버전 확인 후 children(메인 앱) 렌더 — 새 버전 있으면 업데이트 모달 표시 */
export function StartupUpdateGate({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false);
const [startupUpdateOpen, setStartupUpdateOpen] = useState(false);
const [progress, setProgress] = useState<DesktopUpdateProgress | null>({
phase: 'checking',
message: '서버에서 새 버전을 확인하는 중…',
});
useEffect(() => {
let cancelled = false;
void (async () => {
let currentVersion = '—';
try {
currentVersion = await getAppVersion();
} catch {
/* ignore */
}
if (cancelled) return;
setProgress({
phase: 'checking',
message: '서버에서 새 버전을 확인하는 중…',
currentVersion,
});
const result = await checkForUpdates();
if (cancelled) return;
if (result.available) {
setProgress({
phase: 'available',
message: result.message ?? `새 버전 v${result.version}을(를) 사용할 수 있습니다.`,
currentVersion,
newVersion: result.version,
});
} else if (result.message?.includes('실패') || result.message?.includes('오류')) {
setProgress({ phase: 'error', message: result.message, currentVersion });
}
scheduleMainWindowTitleSync();
setReady(true);
if (result.available) {
setStartupUpdateOpen(true);
}
})();
return () => {
cancelled = true;
};
}, []);
if (!ready) {
return <StartupUpdateSplash progress={progress} />;
}
return (
<>
{children}
<DesktopUpdateModal
open={startupUpdateOpen}
onClose={() => setStartupUpdateOpen(false)}
autoStart
/>
</>
);
}