desktop download
This commit is contained in:
@@ -199,6 +199,114 @@ const PlatformCard: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const BUILD_POLL_MS = 4000;
|
||||
|
||||
function formatElapsed(startedAtMs?: number): string {
|
||||
if (startedAtMs == null || !Number.isFinite(startedAtMs)) return '—';
|
||||
const sec = Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000));
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = sec % 60;
|
||||
return m > 0 ? `${m}분 ${s.toString().padStart(2, '0')}초` : `${s}초`;
|
||||
}
|
||||
|
||||
function buildStatusLabel(status?: string, building?: boolean): string {
|
||||
if (building) return '빌드 진행 중';
|
||||
switch (status) {
|
||||
case 'building': return '빌드 진행 중';
|
||||
case 'queued': return 'Jenkins 큐 대기';
|
||||
case 'success': return '빌드 성공';
|
||||
case 'failure': return '빌드 실패';
|
||||
case 'aborted': return '빌드 중단';
|
||||
case 'error': return '상태 조회 오류';
|
||||
case 'disabled': return 'Jenkins 비활성';
|
||||
case 'ready': return '빌드 준비됨';
|
||||
case 'idle': return '대기 중';
|
||||
default: return status ?? '—';
|
||||
}
|
||||
}
|
||||
|
||||
function isTerminalBuildStatus(status?: string, building?: boolean): boolean {
|
||||
if (building) return false;
|
||||
return status === 'success' || status === 'failure' || status === 'aborted';
|
||||
}
|
||||
|
||||
const DesktopBuildProgressPanel: React.FC<{
|
||||
status: DesktopAppBuildStatusDto | null;
|
||||
hint: string | null;
|
||||
busy: boolean;
|
||||
tracking: boolean;
|
||||
tick: number;
|
||||
}> = ({ status, hint, busy, tracking, tick }) => {
|
||||
void tick;
|
||||
const building = Boolean(status?.building || status?.status === 'queued');
|
||||
const active = busy || building || tracking;
|
||||
const terminal = isTerminalBuildStatus(status?.status, status?.building);
|
||||
const failed = !building && (status?.status === 'failure' || status?.status === 'error');
|
||||
const succeeded = !building && status?.status === 'success';
|
||||
|
||||
if (!active && !terminal && !hint) return null;
|
||||
|
||||
const steps = [
|
||||
{ id: 'queue', label: 'Jenkins 빌드 시작', done: Boolean(status?.buildNumber) || building || terminal },
|
||||
{
|
||||
id: 'compile',
|
||||
label: 'macOS dmg · Windows exe 빌드',
|
||||
done: succeeded || (building && Boolean(status?.buildNumber)),
|
||||
active: building && Boolean(status?.buildNumber),
|
||||
},
|
||||
{ id: 'deploy', label: '설치 파일 배포', done: succeeded, active: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`app-download-build-progress${active ? ' app-download-build-progress--active' : ''}${failed ? ' app-download-build-progress--failed' : ''}${succeeded ? ' app-download-build-progress--success' : ''}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="app-download-build-progress-head">
|
||||
<div className="app-download-build-progress-title">
|
||||
{(active || building) && <span className="app-download-build-spinner" aria-hidden />}
|
||||
<strong>{buildStatusLabel(status?.status, status?.building)}</strong>
|
||||
{status?.buildNumber != null && (
|
||||
<span className="app-download-build-badge">#{status.buildNumber}</span>
|
||||
)}
|
||||
</div>
|
||||
{(building || status?.startedAtMs) && (
|
||||
<span className="app-download-build-elapsed">
|
||||
경과 {formatElapsed(status?.startedAtMs)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(active || building) && (
|
||||
<div className="app-download-build-bar" aria-hidden>
|
||||
<div className="app-download-build-bar-fill" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ol className="app-download-build-steps">
|
||||
{steps.map(step => (
|
||||
<li
|
||||
key={step.id}
|
||||
className={`app-download-build-step${step.done ? ' done' : ''}${step.active ? ' active' : ''}`}
|
||||
>
|
||||
<span className="app-download-build-step-dot" aria-hidden />
|
||||
{step.label}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{(hint || status?.message) && (
|
||||
<p className="app-download-build-message">{hint ?? status?.message}</p>
|
||||
)}
|
||||
|
||||
{succeeded && (
|
||||
<p className="app-download-build-done-note">아래에서 macOS·Windows 설치 파일을 다운로드할 수 있습니다.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop }) => {
|
||||
const [info, setInfo] = useState<DesktopAppReleaseInfoDto | null>(null);
|
||||
const [buildStatus, setBuildStatus] = useState<DesktopAppBuildStatusDto | null>(null);
|
||||
@@ -206,6 +314,8 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [buildBusy, setBuildBusy] = useState(false);
|
||||
const [buildHint, setBuildHint] = useState<string | null>(null);
|
||||
const [buildTracking, setBuildTracking] = useState(false);
|
||||
const [elapsedTick, setElapsedTick] = useState(0);
|
||||
|
||||
const reloadRelease = useCallback(async () => {
|
||||
const data = await loadDesktopAppReleaseInfo();
|
||||
@@ -228,29 +338,51 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
}, [reloadRelease, reloadBuildStatus]);
|
||||
|
||||
const isBuilding = Boolean(buildStatus?.building || buildStatus?.status === 'queued');
|
||||
const showBuildProgress = Boolean(
|
||||
buildBusy || buildTracking || isBuilding
|
||||
|| (buildStatus?.buildNumber && buildStatus.status !== 'idle' && buildStatus.status !== 'ready'),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBuilding) return;
|
||||
const timer = window.setInterval(() => {
|
||||
if (!showBuildProgress || buildStatus?.startedAtMs == null) return;
|
||||
const timer = window.setInterval(() => setElapsedTick(t => t + 1), 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [showBuildProgress, buildStatus?.startedAtMs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!buildTracking && !isBuilding && !buildBusy) return;
|
||||
const poll = () => {
|
||||
void reloadBuildStatus().then(st => {
|
||||
if (st && !st.building && st.status !== 'queued') {
|
||||
if (!st) return;
|
||||
if (isTerminalBuildStatus(st.status, st.building)) {
|
||||
setBuildTracking(false);
|
||||
void reloadRelease();
|
||||
} else if (!st.building && st.status !== 'queued') {
|
||||
void reloadRelease();
|
||||
}
|
||||
});
|
||||
}, 8000);
|
||||
};
|
||||
poll();
|
||||
const timer = window.setInterval(poll, BUILD_POLL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [isBuilding, reloadBuildStatus, reloadRelease]);
|
||||
}, [buildTracking, isBuilding, buildBusy, reloadBuildStatus, reloadRelease]);
|
||||
|
||||
const handleBuild = useCallback(async () => {
|
||||
if (!canBuildDesktop || buildBusy || isBuilding) return;
|
||||
setBuildBusy(true);
|
||||
setBuildTracking(true);
|
||||
setBuildHint('Jenkins 빌드 요청 중…');
|
||||
try {
|
||||
const res = await triggerDesktopAppBuild();
|
||||
setBuildHint(res.message ?? (res.accepted ? '빌드 시작됨' : '빌드 요청 거부'));
|
||||
if (!res.accepted) setBuildTracking(false);
|
||||
await reloadBuildStatus();
|
||||
if (res.accepted) {
|
||||
window.setTimeout(() => { void reloadBuildStatus(); }, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
setBuildHint(e instanceof Error ? e.message : '빌드 요청 실패');
|
||||
setBuildTracking(false);
|
||||
} finally {
|
||||
setBuildBusy(false);
|
||||
}
|
||||
@@ -261,7 +393,7 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
if (loading) return <p className="app-download-muted">PC 프로그램 정보 불러오는 중…</p>;
|
||||
if (error) return <p className="app-download-error">{error}</p>;
|
||||
|
||||
const statusMessage = buildHint ?? buildStatus?.message;
|
||||
const statusMessage = !showBuildProgress ? (buildHint ?? buildStatus?.message) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -291,6 +423,16 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showBuildProgress && (
|
||||
<DesktopBuildProgressPanel
|
||||
status={buildStatus}
|
||||
hint={buildHint}
|
||||
busy={buildBusy}
|
||||
tracking={buildTracking}
|
||||
tick={elapsedTick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{statusMessage && (
|
||||
<p className={`app-download-build-status${isBuilding ? ' app-download-build-status--active' : ''}`}>
|
||||
{statusMessage}
|
||||
@@ -319,7 +461,7 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
|
||||
{isBuilding && !hasAny && (
|
||||
<p className="app-download-muted app-download-build-wait">
|
||||
Jenkins에서 macOS dmg · Windows setup.exe를 빌드 중입니다. 완료되면 아래에 다운로드가 표시됩니다.
|
||||
Tauri macOS dmg · Windows NSIS 빌드가 진행 중입니다. 완료되면 아래에 다운로드가 표시됩니다.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user