데스크톱 앱 리프레시 기능 추가
This commit is contained in:
@@ -12664,6 +12664,32 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
|
|||||||
color: var(--accent, #8b5cf6);
|
color: var(--accent, #8b5cf6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TopMenuBar 새로고침 (데스크톱) */
|
||||||
|
.tmb-refresh-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text2);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tmb-refresh-btn:hover {
|
||||||
|
background: var(--bg4);
|
||||||
|
color: var(--accent, #2196f3);
|
||||||
|
}
|
||||||
|
.tmb-refresh-btn.busy {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
.tmb-refresh-btn.busy svg {
|
||||||
|
animation: tmb-update-spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* TopMenuBar 전체화면 버튼 */
|
/* TopMenuBar 전체화면 버튼 */
|
||||||
.tmb-fullscreen-btn {
|
.tmb-fullscreen-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
|||||||
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
||||||
import { isDesktop } from './utils/platform';
|
import { isDesktop } from './utils/platform';
|
||||||
import { syncDesktopMainWindowTitle } from './utils/desktopBridge';
|
import { syncDesktopMainWindowTitle } from './utils/desktopBridge';
|
||||||
|
import { dispatchPageRefresh } from './utils/pageRefreshEvents';
|
||||||
import { writeDesktopSync } from './utils/desktopSync';
|
import { writeDesktopSync } from './utils/desktopSync';
|
||||||
|
|
||||||
const LAST_MENU_KEY = 'gc_last_menu';
|
const LAST_MENU_KEY = 'gc_last_menu';
|
||||||
@@ -185,6 +186,10 @@ function AppMainContent({
|
|||||||
else document.exitFullscreen().catch(() => {});
|
else document.exitFullscreen().catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePageRefresh = useCallback(() => {
|
||||||
|
dispatchPageRefresh(menuPage);
|
||||||
|
}, [menuPage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerificationIssueNotificationProvider
|
<VerificationIssueNotificationProvider
|
||||||
enabled={canMenu('verification-board')}
|
enabled={canMenu('verification-board')}
|
||||||
@@ -209,6 +214,7 @@ function AppMainContent({
|
|||||||
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
|
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
|
||||||
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
|
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
|
||||||
onFullscreen={handleFullscreen}
|
onFullscreen={handleFullscreen}
|
||||||
|
onRefresh={isDesktop() ? handlePageRefresh : undefined}
|
||||||
onOpenFloatingWidgets={showFloatingWidgets ? () => setFloatingLayoutPickerOpen(true) : undefined}
|
onOpenFloatingWidgets={showFloatingWidgets ? () => setFloatingLayoutPickerOpen(true) : undefined}
|
||||||
floatingWidgetCount={floatingWidgetCount}
|
floatingWidgetCount={floatingWidgetCount}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { normalizeTimezone, setDisplayTimezone, useDisplayTimezone } from '../ut
|
|||||||
import { calcStats, type PriceStats } from '../utils/calculations';
|
import { calcStats, type PriceStats } from '../utils/calculations';
|
||||||
import { saveState, DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
|
import { saveState, DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
|
||||||
import { useWorkspacePersist } from '../hooks/useWorkspacePersist';
|
import { useWorkspacePersist } from '../hooks/useWorkspacePersist';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import type { WatchlistItem } from '../utils/backendApi';
|
import type { WatchlistItem } from '../utils/backendApi';
|
||||||
import {
|
import {
|
||||||
addWatchlistItem, removeWatchlistItem,
|
addWatchlistItem, removeWatchlistItem,
|
||||||
@@ -620,6 +621,7 @@ export function useChartWorkspace({
|
|||||||
const [paperCashBalance, setPaperCashBalance] = useState(0);
|
const [paperCashBalance, setPaperCashBalance] = useState(0);
|
||||||
const [paperCoinQty, setPaperCoinQty] = useState(0);
|
const [paperCoinQty, setPaperCoinQty] = useState(0);
|
||||||
const [paperRefreshKey, setPaperRefreshKey] = useState(0);
|
const [paperRefreshKey, setPaperRefreshKey] = useState(0);
|
||||||
|
const [chartRefreshToken, setChartRefreshToken] = useState(0);
|
||||||
|
|
||||||
const handlePaperOrderFilled = useCallback(() => {
|
const handlePaperOrderFilled = useCallback(() => {
|
||||||
setPaperRefreshKey(k => k + 1);
|
setPaperRefreshKey(k => k + 1);
|
||||||
@@ -679,6 +681,19 @@ export function useChartWorkspace({
|
|||||||
invalidateRuns: btInvalidateRuns,
|
invalidateRuns: btInvalidateRuns,
|
||||||
} = useBacktest();
|
} = useBacktest();
|
||||||
const [btSettings, setBtSettings] = useState<BacktestSettingsDto>(DEFAULT_BACKTEST_SETTINGS);
|
const [btSettings, setBtSettings] = useState<BacktestSettingsDto>(DEFAULT_BACKTEST_SETTINGS);
|
||||||
|
|
||||||
|
usePageRefresh('chart', useCallback(() => {
|
||||||
|
setChartRefreshToken(t => t + 1);
|
||||||
|
void refreshAllTickers();
|
||||||
|
void refreshPaperAccount(symbol);
|
||||||
|
btInvalidateRuns();
|
||||||
|
}, [refreshAllTickers, refreshPaperAccount, symbol, btInvalidateRuns]));
|
||||||
|
|
||||||
|
usePageRefresh('paper', useCallback(() => {
|
||||||
|
setPaperRefreshKey(k => k + 1);
|
||||||
|
void refreshAllTickers();
|
||||||
|
}, [refreshAllTickers]));
|
||||||
|
|
||||||
const [showBtSettings, setShowBtSettings] = useState(false);
|
const [showBtSettings, setShowBtSettings] = useState(false);
|
||||||
const [showBtResult, setShowBtResult] = useState(false);
|
const [showBtResult, setShowBtResult] = useState(false);
|
||||||
const [btStrategyName, setBtStrategyName] = useState('전략');
|
const [btStrategyName, setBtStrategyName] = useState('전략');
|
||||||
@@ -1160,7 +1175,8 @@ export function useChartWorkspace({
|
|||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: chartScreenActive && useUpbit && chartConfigReady,
|
enabled: chartScreenActive && useUpbit && chartConfigReady,
|
||||||
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
|
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
|
||||||
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartConfigReady, chartRealtimeSource]),
|
refreshToken: chartRefreshToken,
|
||||||
|
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartConfigReady, chartRealtimeSource, chartRefreshToken]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
|
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import type { Theme, Timeframe } from '../types';
|
import type { Theme, Timeframe } from '../types';
|
||||||
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import { buildEquityFromSignals } from '../utils/backtestEquity';
|
import { buildEquityFromSignals } from '../utils/backtestEquity';
|
||||||
import {
|
import {
|
||||||
formatChartTypeKo,
|
formatChartTypeKo,
|
||||||
@@ -210,6 +211,8 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
|
|
||||||
useEffect(() => { void fetchAll(); }, [fetchAll]);
|
useEffect(() => { void fetchAll(); }, [fetchAll]);
|
||||||
|
|
||||||
|
usePageRefresh('backtest', () => { void fetchAll(); });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onTradesChanged = () => { void refreshLiveTrades(); };
|
const onTradesChanged = () => { void refreshLiveTrades(); };
|
||||||
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onTradesChanged);
|
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onTradesChanged);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import DashboardSegmentBar from './dashboard/DashboardSegmentBar';
|
|||||||
import DashboardSparkChart from './dashboard/DashboardSparkChart';
|
import DashboardSparkChart from './dashboard/DashboardSparkChart';
|
||||||
import DashboardIndicatorBar from './dashboard/DashboardIndicatorBar';
|
import DashboardIndicatorBar from './dashboard/DashboardIndicatorBar';
|
||||||
import DashboardSystemCard from './dashboard/DashboardSystemCard';
|
import DashboardSystemCard from './dashboard/DashboardSystemCard';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import '../styles/strategyEditorTheme.css';
|
import '../styles/strategyEditorTheme.css';
|
||||||
import '../styles/dashboardCommand.css';
|
import '../styles/dashboardCommand.css';
|
||||||
|
|
||||||
@@ -110,6 +111,8 @@ const DashboardPage: React.FC<Props> = ({ theme, onGoChart }) => {
|
|||||||
return () => window.clearInterval(id);
|
return () => window.clearInterval(id);
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
|
usePageRefresh('dashboard', refresh);
|
||||||
|
|
||||||
const mon = data?.systemMonitor;
|
const mon = data?.systemMonitor;
|
||||||
const app = data?.app as Record<string, unknown> | undefined;
|
const app = data?.app as Record<string, unknown> | undefined;
|
||||||
const health = mon ? healthLevel(mon) : 'yellow';
|
const health = mon ? healthLevel(mon) : 'yellow';
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface NotifyTopMenuBarProps {
|
|||||||
onFullscreen?: () => void;
|
onFullscreen?: () => void;
|
||||||
onOpenFloatingWidgets?: () => void;
|
onOpenFloatingWidgets?: () => void;
|
||||||
floatingWidgetCount?: number;
|
floatingWidgetCount?: number;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotifyTopMenuBar: React.FC<NotifyTopMenuBarProps> = props => {
|
export const NotifyTopMenuBar: React.FC<NotifyTopMenuBarProps> = props => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
|
|||||||
import { loadVirtualSession, loadVirtualTargets } from '../utils/virtualTradingStorage';
|
import { loadVirtualSession, loadVirtualTargets } from '../utils/virtualTradingStorage';
|
||||||
import { resolveVirtualTargetStrategyId } from '../utils/virtualTargetStrategy';
|
import { resolveVirtualTargetStrategyId } from '../utils/virtualTargetStrategy';
|
||||||
import { formatVirtualTargetOptionLabel } from '../utils/virtualTargetNames';
|
import { formatVirtualTargetOptionLabel } from '../utils/virtualTargetNames';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
|
|
||||||
type HistoryTab = 'open' | 'recent' | 'ledger';
|
type HistoryTab = 'open' | 'recent' | 'ledger';
|
||||||
type CenterTab = 'invest' | 'chart';
|
type CenterTab = 'invest' | 'chart';
|
||||||
@@ -152,6 +153,8 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
|
|
||||||
useEffect(() => { if (refreshKey > 0) void loadData(); }, [refreshKey, loadData]);
|
useEffect(() => { if (refreshKey > 0) void loadData(); }, [refreshKey, loadData]);
|
||||||
|
|
||||||
|
usePageRefresh('paper', () => { void loadData(); });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialLoading) return;
|
if (initialLoading) return;
|
||||||
setVirtualTargets(loadVirtualTargets());
|
setVirtualTargets(loadVirtualTargets());
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import StrategyEvaluationStrategyPanel from './strategyEvaluation/StrategyEvalua
|
|||||||
import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel';
|
import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel';
|
||||||
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
||||||
import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab';
|
import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
|
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
|
||||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||||
import { readStoredSize, readStoredBool, storeBool, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
import { readStoredSize, readStoredBool, storeBool, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||||
@@ -375,6 +376,11 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
|||||||
void refreshStrategies();
|
void refreshStrategies();
|
||||||
}, [refreshStrategies]);
|
}, [refreshStrategies]);
|
||||||
|
|
||||||
|
usePageRefresh('strategy-evaluation', () => {
|
||||||
|
void refreshStrategies();
|
||||||
|
setParamsRevision(v => v + 1);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedStrategyId) {
|
if (!selectedStrategyId) {
|
||||||
setSelectedStrategy(null);
|
setSelectedStrategy(null);
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ interface TopMenuBarProps {
|
|||||||
onOpenFloatingWidgets?: () => void;
|
onOpenFloatingWidgets?: () => void;
|
||||||
/** 실행 중인 플로팅 위젯 창 수 */
|
/** 실행 중인 플로팅 위젯 창 수 */
|
||||||
floatingWidgetCount?: number;
|
floatingWidgetCount?: number;
|
||||||
|
/** 데스크톱 — 현재 화면 데이터 새로고침 */
|
||||||
|
onRefresh?: () => void;
|
||||||
/** 메뉴별 접근 허용 (없으면 전체 표시) */
|
/** 메뉴별 접근 허용 (없으면 전체 표시) */
|
||||||
menuPermissions?: Record<string, boolean>;
|
menuPermissions?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
@@ -116,6 +118,15 @@ const IcDesktopUpdate = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const IcRefresh = () => (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M2 8a6 6 0 0 1 10.2-4.2"/>
|
||||||
|
<polyline points="12,2 12,5.5 8.5,5.5"/>
|
||||||
|
<path d="M14 8a6 6 0 0 1-10.2 4.2"/>
|
||||||
|
<polyline points="4,14 4,10.5 7.5,10.5"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const IcNotify = () => (
|
const IcNotify = () => (
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M8 1.5a4.5 4.5 0 0 0-4.5 4.5c0 3.5-1 4.5-1.5 4.5h12c-.5 0-1.5-1-1.5-4.5A4.5 4.5 0 0 0 8 1.5z"/>
|
<path d="M8 1.5a4.5 4.5 0 0 0-4.5 4.5c0 3.5-1 4.5-1.5 4.5h12c-.5 0-1.5-1-1.5-4.5A4.5 4.5 0 0 0 8 1.5z"/>
|
||||||
@@ -293,10 +304,12 @@ export const TopMenuBar = memo(function TopMenuBar({
|
|||||||
onFullscreen,
|
onFullscreen,
|
||||||
onOpenFloatingWidgets,
|
onOpenFloatingWidgets,
|
||||||
floatingWidgetCount = 0,
|
floatingWidgetCount = 0,
|
||||||
|
onRefresh,
|
||||||
menuPermissions,
|
menuPermissions,
|
||||||
}: TopMenuBarProps) {
|
}: TopMenuBarProps) {
|
||||||
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
||||||
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
|
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
|
||||||
|
const [refreshBusy, setRefreshBusy] = useState(false);
|
||||||
const desktopClient = isDesktop();
|
const desktopClient = isDesktop();
|
||||||
const appVersion = useAppTitleBarVersion();
|
const appVersion = useAppTitleBarVersion();
|
||||||
|
|
||||||
@@ -340,6 +353,26 @@ export const TopMenuBar = memo(function TopMenuBar({
|
|||||||
|
|
||||||
{/* 우측 영역 */}
|
{/* 우측 영역 */}
|
||||||
<div className="tmb-right">
|
<div className="tmb-right">
|
||||||
|
{desktopClient && onRefresh && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`tmb-refresh-btn${refreshBusy ? ' busy' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (refreshBusy) return;
|
||||||
|
setRefreshBusy(true);
|
||||||
|
try {
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
window.setTimeout(() => setRefreshBusy(false), 600);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="현재 화면 새로고침"
|
||||||
|
aria-label="현재 화면 새로고침"
|
||||||
|
>
|
||||||
|
<IcRefresh />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{onOpenFloatingWidgets && (
|
{onOpenFloatingWidgets && (
|
||||||
<div className="tmb-floating-widget-wrap">
|
<div className="tmb-floating-widget-wrap">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type StrategyDto,
|
type StrategyDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import { prefetchNotificationStrategies, resolveNotificationStrategy } from '../utils/resolveNotificationStrategy';
|
import { prefetchNotificationStrategies, resolveNotificationStrategy } from '../utils/resolveNotificationStrategy';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import { normalizeMarketCode } from '../utils/strategyHydrate';
|
import { normalizeMarketCode } from '../utils/strategyHydrate';
|
||||||
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
||||||
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
||||||
@@ -182,6 +183,11 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
void refreshPaperData();
|
void refreshPaperData();
|
||||||
}, [refreshPaperData]);
|
}, [refreshPaperData]);
|
||||||
|
|
||||||
|
usePageRefresh('notifications', () => {
|
||||||
|
void refreshHistory();
|
||||||
|
void refreshPaperData();
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type TrendSearchAppSettings,
|
type TrendSearchAppSettings,
|
||||||
} from '../utils/trendSearchAppSettings';
|
} from '../utils/trendSearchAppSettings';
|
||||||
import { autoAddTrendSearchTargets } from '../utils/trendSearchAutoAddTargets';
|
import { autoAddTrendSearchTargets } from '../utils/trendSearchAutoAddTargets';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import '../styles/trendSearchDashboard.css';
|
import '../styles/trendSearchDashboard.css';
|
||||||
import '../styles/virtualTradingDashboard.css';
|
import '../styles/virtualTradingDashboard.css';
|
||||||
|
|
||||||
@@ -136,6 +137,8 @@ const TrendSearchPage: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [applyResults]);
|
}, [applyResults]);
|
||||||
|
|
||||||
|
usePageRefresh('trend-search', () => { void loadFromDb(); });
|
||||||
|
|
||||||
const runScan = useCallback(async () => {
|
const runScan = useCallback(async () => {
|
||||||
setSearching(true);
|
setSearching(true);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type VerificationIssueDto,
|
type VerificationIssueDto,
|
||||||
} from '../utils/verificationBoardApi';
|
} from '../utils/verificationBoardApi';
|
||||||
import { prepareAttachmentsForUpload } from '../utils/verificationAttachmentFiles';
|
import { prepareAttachmentsForUpload } from '../utils/verificationAttachmentFiles';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import '../styles/verificationBoard.css';
|
import '../styles/verificationBoard.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -79,6 +80,8 @@ const VerificationBoardPage: React.FC<Props> = ({
|
|||||||
void reload(stageFilter);
|
void reload(stageFilter);
|
||||||
}, [stageFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [stageFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
usePageRefresh('verification-board', () => { void reload(stageFilter); });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusIssueId == null) return;
|
if (focusIssueId == null) return;
|
||||||
setSelectedId(focusIssueId);
|
setSelectedId(focusIssueId);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import type { VirtualCardDisplayMode } from './virtual/VirtualTargetCard';
|
import type { VirtualCardDisplayMode } from './virtual/VirtualTargetCard';
|
||||||
import { useVirtualIndicatorSnapshots } from '../hooks/useVirtualIndicatorSnapshots';
|
import { useVirtualIndicatorSnapshots } from '../hooks/useVirtualIndicatorSnapshots';
|
||||||
import { PAPER_TRADES_CHANGED_EVENT } from '../utils/paperTradeEvents';
|
import { PAPER_TRADES_CHANGED_EVENT } from '../utils/paperTradeEvents';
|
||||||
|
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||||
import { useVirtualTargetLiveStatus } from '../hooks/useVirtualTargetLiveStatus';
|
import { useVirtualTargetLiveStatus } from '../hooks/useVirtualTargetLiveStatus';
|
||||||
import type { ChartRealtimeSource } from '../hooks/useChartRealtimeData';
|
import type { ChartRealtimeSource } from '../hooks/useChartRealtimeData';
|
||||||
import {
|
import {
|
||||||
@@ -261,6 +262,8 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
if (opts?.focusHistory) setRightTab('history');
|
if (opts?.focusHistory) setRightTab('history');
|
||||||
}, [onPaperOrderFilled]);
|
}, [onPaperOrderFilled]);
|
||||||
|
|
||||||
|
usePageRefresh('virtual', () => { void refreshPaperData(); });
|
||||||
|
|
||||||
const handleGlobalDisplayModeChange = useCallback((mode: VirtualCardDisplayMode) => {
|
const handleGlobalDisplayModeChange = useCallback((mode: VirtualCardDisplayMode) => {
|
||||||
setGlobalDisplayMode(mode);
|
setGlobalDisplayMode(mode);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface Options {
|
|||||||
/** 직접 지정할 페치 봉 수 — UPBIT_DIRECT 전용 (deepObvHistory·WARMUP_COUNT 무시) */
|
/** 직접 지정할 페치 봉 수 — UPBIT_DIRECT 전용 (deepObvHistory·WARMUP_COUNT 무시) */
|
||||||
fetchCount?: number;
|
fetchCount?: number;
|
||||||
source?: ChartRealtimeSource;
|
source?: ChartRealtimeSource;
|
||||||
|
/** 증가 시 초기 캔들 REST 재로드 (화면 새로고침) */
|
||||||
|
refreshToken?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChartRealtimeData(
|
export function useChartRealtimeData(
|
||||||
@@ -25,6 +27,7 @@ export function useChartRealtimeData(
|
|||||||
onNewCandle: opts.onNewCandle,
|
onNewCandle: opts.onNewCandle,
|
||||||
enabled: opts.enabled !== false && useStomp,
|
enabled: opts.enabled !== false && useStomp,
|
||||||
deepObvHistory: opts.deepObvHistory,
|
deepObvHistory: opts.deepObvHistory,
|
||||||
|
refreshToken: opts.refreshToken,
|
||||||
});
|
});
|
||||||
const upbit = useUpbitData(market, timeframe, {
|
const upbit = useUpbitData(market, timeframe, {
|
||||||
onTickUpdate: opts.onTickUpdate,
|
onTickUpdate: opts.onTickUpdate,
|
||||||
@@ -32,6 +35,7 @@ export function useChartRealtimeData(
|
|||||||
enabled: opts.enabled !== false && !useStomp,
|
enabled: opts.enabled !== false && !useStomp,
|
||||||
deepObvHistory: opts.deepObvHistory,
|
deepObvHistory: opts.deepObvHistory,
|
||||||
fetchCount: opts.fetchCount,
|
fetchCount: opts.fetchCount,
|
||||||
|
refreshToken: opts.refreshToken,
|
||||||
});
|
});
|
||||||
return useStomp ? stomp : upbit;
|
return useStomp ? stomp : upbit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ interface Options {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/** OBV 누적 정확도 — 더 많은 초기 봉 로드 */
|
/** OBV 누적 정확도 — 더 많은 초기 봉 로드 */
|
||||||
deepObvHistory?: boolean;
|
deepObvHistory?: boolean;
|
||||||
|
/** 화면 새로고침 — REST 초기 로드 재실행 */
|
||||||
|
refreshToken?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Return {
|
interface Return {
|
||||||
@@ -141,7 +143,7 @@ export function useStompChartData(
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [market, candleType, timeframe, opts.enabled, opts.deepObvHistory]);
|
}, [market, candleType, timeframe, opts.enabled, opts.deepObvHistory, opts.refreshToken]);
|
||||||
|
|
||||||
const syncBarsToReactIfNeeded = useCallback((marketId: string) => {
|
const syncBarsToReactIfNeeded = useCallback((marketId: string) => {
|
||||||
if (!bootstrapFromStompRef.current) return;
|
if (!bootstrapFromStompRef.current) return;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ interface UpbitDataOptions {
|
|||||||
deepObvHistory?: boolean;
|
deepObvHistory?: boolean;
|
||||||
/** 직접 지정할 페치 봉 수 (deepObvHistory·WARMUP_COUNT 무시) */
|
/** 직접 지정할 페치 봉 수 (deepObvHistory·WARMUP_COUNT 무시) */
|
||||||
fetchCount?: number;
|
fetchCount?: number;
|
||||||
|
/** 화면 새로고침 — REST 초기 로드 재실행 */
|
||||||
|
refreshToken?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseUpbitDataReturn {
|
interface UseUpbitDataReturn {
|
||||||
@@ -73,6 +75,7 @@ export function useUpbitData(
|
|||||||
// ── 1. REST API: 초기 캔들 로딩 (표시용 + 워밍업 합산) ─────────────────
|
// ── 1. REST API: 초기 캔들 로딩 (표시용 + 워밍업 합산) ─────────────────
|
||||||
// 이전 market 값을 기억해 종목 변경 시에만 캐시를 무효화한다.
|
// 이전 market 값을 기억해 종목 변경 시에만 캐시를 무효화한다.
|
||||||
const prevMarketRef = useRef<string>('');
|
const prevMarketRef = useRef<string>('');
|
||||||
|
const prevRefreshTokenRef = useRef(opts.refreshToken ?? 0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opts.enabled === false) return;
|
if (opts.enabled === false) return;
|
||||||
@@ -85,15 +88,19 @@ export function useUpbitData(
|
|||||||
setLatestBar(null);
|
setLatestBar(null);
|
||||||
barsRef.current = [];
|
barsRef.current = [];
|
||||||
|
|
||||||
|
const refreshToken = opts.refreshToken ?? 0;
|
||||||
|
const bustCache = refreshToken > 0 && refreshToken !== prevRefreshTokenRef.current;
|
||||||
|
prevRefreshTokenRef.current = refreshToken;
|
||||||
|
|
||||||
// 종목(market)이 변경된 경우에는 캐시를 무효화해 신규 요청을 강제한다.
|
// 종목(market)이 변경된 경우에는 캐시를 무효화해 신규 요청을 강제한다.
|
||||||
// 타임프레임만 바뀐 경우도 캐시 키가 다르므로 자동으로 새 요청이 발생한다.
|
|
||||||
const marketChanged = prevMarketRef.current !== '' && prevMarketRef.current !== market;
|
const marketChanged = prevMarketRef.current !== '' && prevMarketRef.current !== market;
|
||||||
prevMarketRef.current = market;
|
prevMarketRef.current = market;
|
||||||
|
if (bustCache) invalidateMarketCache(market);
|
||||||
|
|
||||||
const fetchCount = opts.fetchCount
|
const fetchCount = opts.fetchCount
|
||||||
?? (opts.deepObvHistory ? OBV_DEEP_HISTORY : DISPLAY_COUNT + WARMUP_COUNT);
|
?? (opts.deepObvHistory ? OBV_DEEP_HISTORY : DISPLAY_COUNT + WARMUP_COUNT);
|
||||||
|
|
||||||
fetchUpbitCandlesCached(market, timeframe, fetchCount, marketChanged)
|
fetchUpbitCandlesCached(market, timeframe, fetchCount, marketChanged || bustCache)
|
||||||
.then(newBars => {
|
.then(newBars => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
barsRef.current = newBars;
|
barsRef.current = newBars;
|
||||||
@@ -109,7 +116,7 @@ export function useUpbitData(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [market, timeframe, opts.enabled, opts.deepObvHistory, opts.fetchCount]);
|
}, [market, timeframe, opts.enabled, opts.deepObvHistory, opts.fetchCount, opts.refreshToken]);
|
||||||
|
|
||||||
// ── 2. WebSocket: 실시간 체결 구독 ────────────────────────────────────
|
// ── 2. WebSocket: 실시간 체결 구독 ────────────────────────────────────
|
||||||
const handleMessage = useCallback((msg: UpbitMessage) => {
|
const handleMessage = useCallback((msg: UpbitMessage) => {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import type { MenuPage } from '../components/TopMenuBar';
|
||||||
|
|
||||||
|
export const PAGE_REFRESH_EVENT = 'gc_page_refresh';
|
||||||
|
|
||||||
|
export type PageRefreshDetail = {
|
||||||
|
page: MenuPage;
|
||||||
|
ts: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function dispatchPageRefresh(page: MenuPage): void {
|
||||||
|
window.dispatchEvent(new CustomEvent<PageRefreshDetail>(PAGE_REFRESH_EVENT, {
|
||||||
|
detail: { page, ts: Date.now() },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 현재 메뉴(또는 지정 목록)에서 새로고침 이벤트 수신 */
|
||||||
|
export function usePageRefresh(
|
||||||
|
page: MenuPage | readonly MenuPage[],
|
||||||
|
onRefresh: () => void | Promise<void>,
|
||||||
|
): void {
|
||||||
|
const handlerRef = useRef(onRefresh);
|
||||||
|
handlerRef.current = onRefresh;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pages = Array.isArray(page) ? page : [page];
|
||||||
|
const listener = (e: Event) => {
|
||||||
|
const detail = (e as CustomEvent<PageRefreshDetail>).detail;
|
||||||
|
if (!detail || !pages.includes(detail.page)) return;
|
||||||
|
void handlerRef.current();
|
||||||
|
};
|
||||||
|
window.addEventListener(PAGE_REFRESH_EVENT, listener);
|
||||||
|
return () => window.removeEventListener(PAGE_REFRESH_EVENT, listener);
|
||||||
|
}, [page]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user