desktop download

This commit is contained in:
Macbook
2026-06-14 11:22:44 +09:00
parent e204aa5dc6
commit 3f29e37139
2 changed files with 226 additions and 7 deletions
+77
View File
@@ -13211,6 +13211,83 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
margin: -8px 0 12px; font-size: 12px; color: var(--text2); line-height: 1.45;
}
.app-download-build-status--active { color: #5eb5ff; }
.app-download-build-progress {
margin: 0 0 14px; padding: 12px 14px; border-radius: 12px;
border: 1px solid var(--border); background: var(--bg2);
}
.app-download-build-progress--active {
border-color: color-mix(in srgb, #2196f3 45%, var(--border));
background: color-mix(in srgb, #2196f3 8%, var(--bg2));
}
.app-download-build-progress--success {
border-color: color-mix(in srgb, #22c55e 45%, var(--border));
background: color-mix(in srgb, #22c55e 8%, var(--bg2));
}
.app-download-build-progress--failed {
border-color: color-mix(in srgb, #ef4444 45%, var(--border));
background: color-mix(in srgb, #ef4444 8%, var(--bg2));
}
.app-download-build-progress-head {
display: flex; align-items: center; justify-content: space-between;
gap: 10px; flex-wrap: wrap; margin-bottom: 10px;
}
.app-download-build-progress-title {
display: inline-flex; align-items: center; gap: 8px; font-size: 13px;
}
.app-download-build-spinner {
width: 14px; height: 14px; border-radius: 50%;
border: 2px solid color-mix(in srgb, #2196f3 25%, transparent);
border-top-color: #2196f3;
animation: tmb-update-spin 0.8s linear infinite;
}
.app-download-build-badge {
font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
background: color-mix(in srgb, #2196f3 18%, var(--bg));
color: #5eb5ff;
}
.app-download-build-elapsed { font-size: 11px; color: var(--text3); font-variant-numeric: tabular-nums; }
.app-download-build-bar {
height: 4px; border-radius: 999px; overflow: hidden;
background: color-mix(in srgb, var(--text3) 15%, transparent);
margin-bottom: 12px;
}
.app-download-build-bar-fill {
height: 100%; width: 40%; border-radius: inherit;
background: linear-gradient(90deg, #2196f3, #5eb5ff);
animation: app-download-build-indeterminate 1.4s ease-in-out infinite;
}
@keyframes app-download-build-indeterminate {
0% { transform: translateX(-120%); }
100% { transform: translateX(320%); }
}
.app-download-build-steps {
list-style: none; margin: 0; padding: 0;
display: flex; flex-direction: column; gap: 8px;
}
.app-download-build-step {
display: flex; align-items: center; gap: 8px;
font-size: 12px; color: var(--text3); line-height: 1.4;
}
.app-download-build-step.active { color: #5eb5ff; font-weight: 600; }
.app-download-build-step.done { color: var(--text2); }
.app-download-build-step-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
border: 2px solid color-mix(in srgb, var(--text3) 50%, transparent);
background: transparent;
}
.app-download-build-step.active .app-download-build-step-dot {
border-color: #2196f3; background: #2196f3;
box-shadow: 0 0 0 3px color-mix(in srgb, #2196f3 25%, transparent);
}
.app-download-build-step.done .app-download-build-step-dot {
border-color: #22c55e; background: #22c55e;
}
.app-download-build-message {
margin: 10px 0 0; font-size: 12px; color: var(--text2); line-height: 1.45;
}
.app-download-build-done-note {
margin: 8px 0 0; font-size: 12px; color: #4ade80; line-height: 1.45;
}
.app-download-build-note, .app-download-build-wait {
margin: 0 0 12px; font-size: 12px; line-height: 1.5;
}
+149 -7
View File
@@ -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>
)}