수정
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* PC(Tauri) 앱 — 업데이트 확인·다운로드·설치 진행 모달
|
||||
*/
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import DraggableModalFrame from './DraggableModalFrame';
|
||||
import {
|
||||
getDesktopAppVersion,
|
||||
runDesktopUpdate,
|
||||
type DesktopUpdatePhase,
|
||||
type DesktopUpdateProgress,
|
||||
} from '../utils/desktopBridge';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
/** 열릴 때 자동으로 업데이트 확인 시작 */
|
||||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
const DesktopUpdateModal: React.FC<Props> = ({ open, onClose, autoStart = true }) => {
|
||||
const [appVersion, setAppVersion] = useState('—');
|
||||
const [progress, setProgress] = useState<DesktopUpdateProgress | null>(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 () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
setProgress({ phase: 'checking', message: '서버에서 새 버전을 확인하는 중…', currentVersion: appVersion });
|
||||
try {
|
||||
await runDesktopUpdate(p => setProgress(p));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [appVersion, busy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
startedRef.current = false;
|
||||
return;
|
||||
}
|
||||
void getDesktopAppVersion().then(v => {
|
||||
if (v) setAppVersion(v);
|
||||
});
|
||||
if (autoStart && !startedRef.current) {
|
||||
startedRef.current = true;
|
||||
void handleRun();
|
||||
}
|
||||
}, [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;
|
||||
|
||||
return (
|
||||
<DraggableModalFrame
|
||||
title="PC 프로그램 업데이트"
|
||||
onClose={isActive ? () => {} : onClose}
|
||||
closeOnBackdrop={!isActive}
|
||||
width={440}
|
||||
dialogClassName="sp-modal desktop-update-modal"
|
||||
>
|
||||
<div className="desktop-update-modal-body">
|
||||
<p className="desktop-update-version">
|
||||
현재 버전 <strong>v{progress?.currentVersion ?? appVersion}</strong>
|
||||
{progress?.newVersion && progress.newVersion !== progress.currentVersion && (
|
||||
<> → <strong>v{progress.newVersion}</strong></>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className={`desktop-update-progress ${panelClass}`.trim()}>
|
||||
<div className="desktop-update-progress-head">
|
||||
<span className="desktop-update-progress-title">
|
||||
{(phase === 'checking' || phase === 'downloading' || phase === 'installing' || phase === 'relaunching') && (
|
||||
<span className="desktop-update-spinner" aria-hidden />
|
||||
)}
|
||||
{PHASE_LABEL[phase]}
|
||||
</span>
|
||||
{phase === 'downloading' && progress?.downloadPercent != null && (
|
||||
<span className="desktop-update-pct">{progress.downloadPercent}%</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showBar && (
|
||||
<div
|
||||
className="desktop-update-bar"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={barPct ?? undefined}
|
||||
>
|
||||
{barPct != null ? (
|
||||
<div className="desktop-update-bar-fill desktop-update-bar-fill--determinate" style={{ width: `${barPct}%` }} />
|
||||
) : (
|
||||
<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${
|
||||
phase === 'available' || phase === 'downloading' ? ' active'
|
||||
: ['installing', 'relaunching', 'uptodate'].includes(phase) ? ' done' : ''
|
||||
}`}>
|
||||
<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${
|
||||
phase === 'installing' ? ' active' : phase === 'relaunching' || phase === 'uptodate' ? ' done' : ''
|
||||
}`}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{phase === 'error' && (
|
||||
<p className="desktop-update-hint">
|
||||
자동 업데이트가 설정되지 않았을 수 있습니다. 웹 <strong>PC 프로그램</strong> 탭에서 dmg를 받아 다시 설치해 주세요.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{phase === 'uptodate' && (
|
||||
<p className="desktop-update-done-note">현재 설치된 버전이 서버와 같습니다.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="desktop-update-actions">
|
||||
{phase === 'error' && (
|
||||
<button type="button" className="stg-btn-secondary" disabled={busy} onClick={() => { void handleRun(); }}>
|
||||
다시 시도
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="stg-btn-secondary"
|
||||
disabled={isActive}
|
||||
onClick={onClose}
|
||||
>
|
||||
{isActive ? '진행 중…' : '닫기'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DraggableModalFrame>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopUpdateModal;
|
||||
@@ -1,15 +1,12 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { isDesktop } from '../utils/platform';
|
||||
import {
|
||||
checkDesktopUpdates,
|
||||
getDesktopAppVersion,
|
||||
} from '../utils/desktopBridge';
|
||||
import { getDesktopAppVersion } from '../utils/desktopBridge';
|
||||
import DesktopUpdateModal from './DesktopUpdateModal';
|
||||
|
||||
/** PC(Tauri) 앱 — 버전 표시 및 업데이트 확인 */
|
||||
const DesktopUpdatePanel: React.FC = () => {
|
||||
const [version, setVersion] = useState<string>('—');
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop()) return;
|
||||
@@ -18,44 +15,43 @@ const DesktopUpdatePanel: React.FC = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCheck = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setStatus('업데이트 확인 중…');
|
||||
try {
|
||||
const res = await checkDesktopUpdates();
|
||||
setStatus(res.message ?? (res.available ? `새 버전 ${res.version}` : '최신 버전입니다.'));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
const handleOpen = useCallback(() => {
|
||||
setModalOpen(true);
|
||||
}, []);
|
||||
|
||||
if (!isDesktop()) return null;
|
||||
|
||||
return (
|
||||
<div className="stg-section">
|
||||
<h3 className="stg-section-title">PC 앱 (GoldenChart Desktop)</h3>
|
||||
<div className="stg-row">
|
||||
<div className="stg-row-label">
|
||||
<strong>현재 버전</strong>
|
||||
<p className="stg-row-desc">설치된 데스크톱 클라이언트 버전입니다.</p>
|
||||
<>
|
||||
<div className="stg-section">
|
||||
<h3 className="stg-section-title">PC 앱 (GoldenChart Desktop)</h3>
|
||||
<div className="stg-row">
|
||||
<div className="stg-row-label">
|
||||
<strong>현재 버전</strong>
|
||||
<p className="stg-row-desc">설치된 데스크톱 클라이언트 버전입니다.</p>
|
||||
</div>
|
||||
<div className="stg-row-control">
|
||||
<span>{version}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stg-row-control">
|
||||
<span>{version}</span>
|
||||
<div className="stg-row">
|
||||
<div className="stg-row-label">
|
||||
<strong>업데이트</strong>
|
||||
<p className="stg-row-desc">서버에서 새 버전을 확인하고 다운로드합니다.</p>
|
||||
</div>
|
||||
<div className="stg-row-control">
|
||||
<button type="button" className="stg-btn-secondary" onClick={handleOpen}>
|
||||
업데이트 확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stg-row">
|
||||
<div className="stg-row-label">
|
||||
<strong>업데이트</strong>
|
||||
<p className="stg-row-desc">서버에서 새 버전을 확인하고 다운로드합니다.</p>
|
||||
</div>
|
||||
<div className="stg-row-control">
|
||||
<button type="button" className="stg-btn-secondary" disabled={busy} onClick={() => { void handleCheck(); }}>
|
||||
{busy ? '확인 중…' : '업데이트 확인'}
|
||||
</button>
|
||||
{status && <span className="stg-hint" style={{ marginLeft: 8 }}>{status}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopUpdateModal
|
||||
open={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
autoStart
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { AuthSession } from '../utils/auth';
|
||||
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
|
||||
import { canAccessMenu } from '../utils/permissions';
|
||||
import { isDesktop } from '../utils/platform';
|
||||
import { checkDesktopUpdates } from '../utils/desktopBridge';
|
||||
import DesktopUpdateModal from './DesktopUpdateModal';
|
||||
|
||||
export type MenuPage = 'dashboard' | 'chart' | 'paper' | 'virtual' | 'trend-search' | 'verification-board' | 'strategy' | 'strategy-editor' | 'strategy-evaluation' | 'backtest' | 'analysis-report' | 'notifications' | 'settings' | 'widget-dashboard';
|
||||
|
||||
@@ -266,24 +266,9 @@ export const TopMenuBar = memo(function TopMenuBar({
|
||||
menuPermissions,
|
||||
}: TopMenuBarProps) {
|
||||
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
||||
const [desktopUpdateBusy, setDesktopUpdateBusy] = useState(false);
|
||||
const [desktopUpdateHint, setDesktopUpdateHint] = useState('');
|
||||
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
|
||||
const desktopClient = isDesktop();
|
||||
|
||||
const handleDesktopUpdate = useCallback(async () => {
|
||||
if (desktopUpdateBusy) return;
|
||||
setDesktopUpdateBusy(true);
|
||||
setDesktopUpdateHint('업데이트 확인 중…');
|
||||
try {
|
||||
const res = await checkDesktopUpdates();
|
||||
setDesktopUpdateHint(res.message ?? (res.available ? `v${res.version ?? ''} 설치` : '최신 버전'));
|
||||
} catch {
|
||||
setDesktopUpdateHint('업데이트 확인 실패');
|
||||
} finally {
|
||||
setDesktopUpdateBusy(false);
|
||||
}
|
||||
}, [desktopUpdateBusy]);
|
||||
|
||||
useEffect(() => {
|
||||
const sync = () => setIsFullscreen(!!document.fullscreenElement);
|
||||
document.addEventListener('fullscreenchange', sync);
|
||||
@@ -295,6 +280,7 @@ export const TopMenuBar = memo(function TopMenuBar({
|
||||
? MENU_ITEMS.filter(({ page }) => canAccessMenu(menuPermissions, page))
|
||||
: MENU_ITEMS;
|
||||
return (
|
||||
<>
|
||||
<header className="top-menubar">
|
||||
{/* 로고 */}
|
||||
<div className="tmb-logo">
|
||||
@@ -356,10 +342,9 @@ export const TopMenuBar = memo(function TopMenuBar({
|
||||
{desktopClient ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`tmb-app-download-btn tmb-desktop-update-btn${desktopUpdateBusy ? ' busy' : ''}`}
|
||||
onClick={() => { void handleDesktopUpdate(); }}
|
||||
disabled={desktopUpdateBusy}
|
||||
title={desktopUpdateHint || 'PC 프로그램 업데이트'}
|
||||
className="tmb-app-download-btn tmb-desktop-update-btn"
|
||||
onClick={() => setDesktopUpdateOpen(true)}
|
||||
title="PC 프로그램 업데이트"
|
||||
aria-label="PC 프로그램 업데이트"
|
||||
>
|
||||
<IcDesktopUpdate />
|
||||
@@ -430,6 +415,14 @@ export const TopMenuBar = memo(function TopMenuBar({
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
{desktopClient && (
|
||||
<DesktopUpdateModal
|
||||
open={desktopUpdateOpen}
|
||||
onClose={() => setDesktopUpdateOpen(false)}
|
||||
autoStart
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user