자동빌드 배포 오류 수정
This commit is contained in:
@@ -13309,6 +13309,44 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
|
||||
.app-download-build-btn:hover:not(:disabled) { filter: brightness(1.08); }
|
||||
.app-download-build-btn:disabled, .app-download-build-btn.busy { opacity: 0.65; cursor: wait; }
|
||||
.app-download-build-btn.busy svg { animation: tmb-update-spin 0.9s linear infinite; }
|
||||
.app-download-readiness {
|
||||
margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
|
||||
border: 1px solid var(--border); background: var(--bg2);
|
||||
font-size: 12px; line-height: 1.45;
|
||||
}
|
||||
.app-download-readiness-badge {
|
||||
display: inline-block; font-size: 11px; font-weight: 700;
|
||||
padding: 2px 8px; border-radius: 999px; margin-bottom: 6px;
|
||||
}
|
||||
.app-download-readiness-msg { margin: 0; color: var(--text2); }
|
||||
.app-download-readiness-ver { margin: 6px 0 0; font-size: 11px; color: var(--text3); }
|
||||
.app-download-readiness--up_to_date {
|
||||
border-color: color-mix(in srgb, #22c55e 40%, var(--border));
|
||||
background: color-mix(in srgb, #22c55e 8%, var(--bg2));
|
||||
}
|
||||
.app-download-readiness--up_to_date .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #22c55e 20%, var(--bg)); color: #4ade80;
|
||||
}
|
||||
.app-download-readiness--web_only {
|
||||
border-color: color-mix(in srgb, #94a3b8 40%, var(--border));
|
||||
}
|
||||
.app-download-readiness--web_only .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #94a3b8 20%, var(--bg)); color: var(--text2);
|
||||
}
|
||||
.app-download-readiness--build_required {
|
||||
border-color: color-mix(in srgb, #f59e0b 45%, var(--border));
|
||||
background: color-mix(in srgb, #f59e0b 8%, var(--bg2));
|
||||
}
|
||||
.app-download-readiness--build_required .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #f59e0b 22%, var(--bg)); color: #fbbf24;
|
||||
}
|
||||
.app-download-readiness--building {
|
||||
border-color: color-mix(in srgb, #2196f3 45%, var(--border));
|
||||
background: color-mix(in srgb, #2196f3 8%, var(--bg2));
|
||||
}
|
||||
.app-download-readiness--building .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #2196f3 20%, var(--bg)); color: #5eb5ff;
|
||||
}
|
||||
.app-download-build-status {
|
||||
margin: -8px 0 12px; font-size: 12px; color: var(--text2); line-height: 1.45;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/**
|
||||
* 모바일·PC 프로그램 설치 — 탭 UI (모바일 QR / PC dmg·exe + Jenkins 빌드)
|
||||
*/
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import DraggableModalFrame from './DraggableModalFrame';
|
||||
import {
|
||||
loadDesktopAppBuildReadiness,
|
||||
loadDesktopAppBuildStatus,
|
||||
loadDesktopAppReleaseInfo,
|
||||
loadMobileAppReleaseInfo,
|
||||
triggerDesktopAppBuild,
|
||||
type DesktopAppBuildReadinessDto,
|
||||
type DesktopAppBuildStatusDto,
|
||||
type DesktopAppPlatformReleaseDto,
|
||||
type DesktopAppReleaseInfoDto,
|
||||
@@ -351,12 +353,14 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
const [info, setInfo] = useState<DesktopAppReleaseInfoDto | null>(null);
|
||||
const [mobileInfo, setMobileInfo] = useState<MobileAppReleaseInfoDto | null>(null);
|
||||
const [buildStatus, setBuildStatus] = useState<DesktopAppBuildStatusDto | null>(null);
|
||||
const [readiness, setReadiness] = useState<DesktopAppBuildReadinessDto | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 autoTrackStarted = useRef(false);
|
||||
|
||||
const reloadRelease = useCallback(async () => {
|
||||
const [desktop, mobile] = await Promise.all([
|
||||
@@ -374,19 +378,28 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
return st;
|
||||
}, []);
|
||||
|
||||
const reloadReadiness = useCallback(async () => {
|
||||
const rd = await loadDesktopAppBuildReadiness();
|
||||
setReadiness(rd);
|
||||
return rd;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void Promise.all([reloadRelease(), reloadBuildStatus()])
|
||||
void Promise.all([reloadRelease(), reloadBuildStatus(), reloadReadiness()])
|
||||
.catch(() => { if (!cancelled) setError('PC 프로그램 정보를 불러오지 못했습니다.'); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [reloadRelease, reloadBuildStatus]);
|
||||
}, [reloadRelease, reloadBuildStatus, reloadReadiness]);
|
||||
|
||||
const isBuilding = Boolean(buildStatus?.building || buildStatus?.status === 'queued');
|
||||
const showBuildProgress = Boolean(
|
||||
buildBusy || buildTracking || isBuilding
|
||||
|| (buildStatus?.buildNumber && buildStatus.status !== 'idle' && buildStatus.status !== 'ready'),
|
||||
);
|
||||
const showBuildProgress = Boolean(buildBusy || buildTracking || isBuilding);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || autoTrackStarted.current || !isBuilding) return;
|
||||
autoTrackStarted.current = true;
|
||||
setBuildTracking(true);
|
||||
}, [loading, isBuilding]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showBuildProgress || buildStatus?.startedAtMs == null) return;
|
||||
@@ -401,27 +414,38 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
if (!st) return;
|
||||
if (isTerminalBuildStatus(st.status, st.building)) {
|
||||
setBuildTracking(false);
|
||||
void reloadRelease();
|
||||
} else if (!st.building && st.status !== 'queued') {
|
||||
void reloadRelease();
|
||||
void Promise.all([reloadRelease(), reloadReadiness()]).then(([desktop]) => {
|
||||
if (st.status === 'success' && readiness?.nextBuildVersion && desktop?.version) {
|
||||
if (desktop.version === readiness.nextBuildVersion) {
|
||||
setBuildHint(`빌드 완료 — v${desktop.version} 설치 파일이 반영되었습니다.`);
|
||||
} else {
|
||||
setBuildHint(`빌드 완료 — v${desktop.version} (예상 v${readiness.nextBuildVersion})`);
|
||||
}
|
||||
} else if (st.status === 'failure') {
|
||||
setBuildHint(st.message ?? '빌드 실패 — Jenkins 로그를 확인하세요.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
poll();
|
||||
const timer = window.setInterval(poll, BUILD_POLL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [buildTracking, isBuilding, buildBusy, reloadBuildStatus, reloadRelease]);
|
||||
}, [buildTracking, isBuilding, buildBusy, reloadBuildStatus, reloadRelease, reloadReadiness, readiness?.nextBuildVersion]);
|
||||
|
||||
const handleBuild = useCallback(async () => {
|
||||
if (!canBuildDesktop || buildBusy || isBuilding) return;
|
||||
setBuildBusy(true);
|
||||
setBuildTracking(true);
|
||||
setBuildHint('Jenkins 빌드 요청 중…');
|
||||
const nextVer = readiness?.nextBuildVersion;
|
||||
setBuildHint(nextVer
|
||||
? `Jenkins 빌드 요청 중… (예상 v${nextVer})`
|
||||
: 'Jenkins 빌드 요청 중…');
|
||||
try {
|
||||
const res = await triggerDesktopAppBuild();
|
||||
setBuildHint(res.message ?? (res.accepted ? '빌드 시작됨' : '빌드 요청 거부'));
|
||||
if (!res.accepted) setBuildTracking(false);
|
||||
await reloadBuildStatus();
|
||||
await Promise.all([reloadBuildStatus(), reloadReadiness()]);
|
||||
if (res.accepted) {
|
||||
window.setTimeout(() => { void reloadBuildStatus(); }, 2000);
|
||||
}
|
||||
@@ -431,7 +455,7 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
} finally {
|
||||
setBuildBusy(false);
|
||||
}
|
||||
}, [canBuildDesktop, buildBusy, isBuilding, reloadBuildStatus]);
|
||||
}, [canBuildDesktop, buildBusy, isBuilding, reloadBuildStatus, reloadReadiness, readiness?.nextBuildVersion]);
|
||||
|
||||
const hasAny = info?.mac?.available || info?.windows?.available || mobileInfo?.available;
|
||||
|
||||
@@ -443,10 +467,21 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
),
|
||||
);
|
||||
|
||||
const readinessLabel = useMemo(() => {
|
||||
if (!readiness) return null;
|
||||
switch (readiness.status) {
|
||||
case 'up_to_date': return '최신 버전';
|
||||
case 'web_only': return 'PC 빌드 불필요';
|
||||
case 'build_required': return '빌드 필요';
|
||||
case 'building': return '빌드 진행 중';
|
||||
default: return readiness.buildRequired ? '빌드 필요' : '상태 확인';
|
||||
}
|
||||
}, [readiness]);
|
||||
|
||||
if (loading) return <p className="app-download-muted">PC 프로그램 정보 불러오는 중…</p>;
|
||||
if (error) return <p className="app-download-error">{error}</p>;
|
||||
|
||||
const statusMessage = !showBuildProgress ? (buildHint ?? buildStatus?.message) : null;
|
||||
const statusMessage = !showBuildProgress ? (buildHint ?? (readiness?.status === 'up_to_date' ? readiness.message : buildStatus?.message)) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -467,7 +502,9 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
className={`app-download-build-btn${buildBusy || isBuilding ? ' busy' : ''}`}
|
||||
onClick={() => { void handleBuild(); }}
|
||||
disabled={buildBusy || isBuilding}
|
||||
title="Git main 최신 소스로 macOS·Windows 데스크톱 앱 Jenkins 빌드"
|
||||
title={readiness?.buildRequired
|
||||
? `다음 빌드 예상 버전 v${readiness.nextBuildVersion ?? '?'}`
|
||||
: '현재 최신 소스가 PC 빌드에 반영되어 있습니다'}
|
||||
aria-label="데스크톱 앱 빌드"
|
||||
>
|
||||
<IcDesktopBuild />
|
||||
@@ -476,6 +513,18 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
)}
|
||||
</div>
|
||||
|
||||
{readiness && readinessLabel && (
|
||||
<div className={`app-download-readiness app-download-readiness--${readiness.status}`}>
|
||||
<span className="app-download-readiness-badge">{readinessLabel}</span>
|
||||
<p className="app-download-readiness-msg">{readiness.message}</p>
|
||||
{readiness.publishedVersion && readiness.nextBuildVersion && readiness.buildRequired && (
|
||||
<p className="app-download-readiness-ver">
|
||||
현재 v{readiness.publishedVersion} → 빌드 후 v{readiness.nextBuildVersion}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{desktopVersionMismatch && (
|
||||
<p className="app-download-error" style={{ marginBottom: 8 }}>
|
||||
macOS·Windows 설치 파일 버전이 최신 빌드(v{info?.version})와 다릅니다. 「앱 빌드」를 다시 실행하세요.
|
||||
|
||||
@@ -719,6 +719,23 @@ export async function loadDesktopAppBuildStatus(): Promise<DesktopAppBuildStatus
|
||||
return request<DesktopAppBuildStatusDto>('/desktop-app/build/status');
|
||||
}
|
||||
|
||||
export interface DesktopAppBuildReadinessDto {
|
||||
publishedVersion?: string;
|
||||
nextBuildVersion?: string;
|
||||
buildRequired: boolean;
|
||||
status: string;
|
||||
message?: string;
|
||||
lastBuiltVersion?: string;
|
||||
lastBuiltGitShort?: string;
|
||||
sourceHeadGitShort?: string;
|
||||
pendingCommitCount?: number;
|
||||
desktopChangesPending?: boolean;
|
||||
}
|
||||
|
||||
export async function loadDesktopAppBuildReadiness(): Promise<DesktopAppBuildReadinessDto | null> {
|
||||
return request<DesktopAppBuildReadinessDto>('/desktop-app/build/readiness');
|
||||
}
|
||||
|
||||
export async function triggerDesktopAppBuild(): Promise<DesktopAppBuildTriggerDto> {
|
||||
return requestOrThrow<DesktopAppBuildTriggerDto>('/desktop-app/build', { method: 'POST' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user