차트로딩중 적용
This commit is contained in:
@@ -59,6 +59,7 @@ import CandlePaneControls from './CandlePaneControls';
|
||||
import { getKoreanName } from '../utils/marketNameCache';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
||||
import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
||||
import { sortIndicatorsForPaneLoad } from '../utils/indicatorPaneMerge';
|
||||
import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator';
|
||||
import PaneSeparatorOverlay from './PaneSeparatorOverlay';
|
||||
|
||||
@@ -148,6 +149,8 @@ interface TradingChartProps {
|
||||
showPaneLegend?: boolean;
|
||||
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */
|
||||
crosshairInfoVisible?: boolean;
|
||||
/** 외부 캔들 데이터 fetch 중 (실시간 차트 초기 로딩) */
|
||||
dataLoading?: boolean;
|
||||
}
|
||||
|
||||
const TradingChart: React.FC<TradingChartProps> = ({
|
||||
@@ -184,6 +187,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
paneLayoutClamp = false,
|
||||
showPaneLegend = true,
|
||||
crosshairInfoVisible = true,
|
||||
dataLoading = false,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
||||
@@ -252,6 +256,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
const chartTimeFormat = useChartTimeFormat();
|
||||
const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
|
||||
/** 초기 reloadAll 완료 전 — pane 구분선·부분 레이아웃 노출 방지 */
|
||||
const [chartPaintReady, setChartPaintReady] = useState(false);
|
||||
/** 캔들 pane 전체보기 (오버레이 지표 유지, 하단 보조지표 pane 숨김) */
|
||||
const [candleOnlyMode, setCandleOnlyMode] = useState(false);
|
||||
const paneDragRef = useRef<((id: string, e: React.PointerEvent) => void) | null>(null);
|
||||
@@ -637,6 +643,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
barsRef.current = newBars;
|
||||
reloadInFlightRef.current = true;
|
||||
setChartPaintReady(false);
|
||||
|
||||
try {
|
||||
do {
|
||||
@@ -657,21 +664,12 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
mgr.setTheme(th);
|
||||
mgr.setLogScale(ls);
|
||||
mgr.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
|
||||
commitCandleLayout(mgr);
|
||||
onCandlesReady?.();
|
||||
lastAppliedMarketRef.current = market;
|
||||
|
||||
let stale = false;
|
||||
for (const ind of inds) {
|
||||
if (gen !== reloadGenRef.current) {
|
||||
prevBarsKey.current = '';
|
||||
stale = true;
|
||||
break;
|
||||
}
|
||||
await mgr.addIndicator(ind);
|
||||
const sortedInds = sortIndicatorsForPaneLoad(inds);
|
||||
if (sortedInds.length > 0) {
|
||||
await mgr.addIndicatorsBatch(sortedInds);
|
||||
}
|
||||
if (stale) break;
|
||||
|
||||
if (gen !== reloadGenRef.current) {
|
||||
prevBarsKey.current = '';
|
||||
break;
|
||||
@@ -688,7 +686,6 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
if (gen !== reloadGenRef.current) break;
|
||||
}
|
||||
if (mgr.getLoadedIndicatorCount() < expected) {
|
||||
commitCandleLayout(mgr);
|
||||
if (reloadRetryRef.current < 6) {
|
||||
reloadRetryRef.current += 1;
|
||||
queueIndicatorRecovery();
|
||||
@@ -706,10 +703,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
lastAppliedMarketRef.current = market;
|
||||
|
||||
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
applyPaneLayout(mgr);
|
||||
normalizeChartViewport(mgr);
|
||||
requestAnimationFrame(() => {
|
||||
const futureBars = mgr.hasIchimoku() ? 28 : 0;
|
||||
mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
|
||||
onCandlesReady?.();
|
||||
setChartPaintReady(true);
|
||||
resolve();
|
||||
});
|
||||
})));
|
||||
@@ -740,12 +737,15 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
} while (reloadCoalesceRef.current);
|
||||
} finally {
|
||||
reloadInFlightRef.current = false;
|
||||
if (managerRef.current?.hasMainSeries()) {
|
||||
setChartPaintReady(true);
|
||||
}
|
||||
if (reloadCoalesceRef.current) {
|
||||
reloadCoalesceRef.current = false;
|
||||
queueFullReload();
|
||||
}
|
||||
}
|
||||
}, [applyPaneLayout, commitCandleLayout, normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
||||
}, [normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadAllRef.current = reloadAll;
|
||||
@@ -1022,7 +1022,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
resizeDebounceRef.current = window.setTimeout(() => {
|
||||
resizeDebounceRef.current = null;
|
||||
const m = managerRef.current;
|
||||
if (!m) return;
|
||||
if (!m || reloadInFlightRef.current) return;
|
||||
|
||||
// 컨테이너가 처음으로 유효한 크기를 가질 때:
|
||||
// 데이터가 이미 barsRef에 있지만 차트에 아직 세팅되지 않은 경우 (예: 멀티레이아웃 초기 렌더) 재로드
|
||||
@@ -1123,6 +1123,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
reloadCoalesceRef.current = false;
|
||||
reloadSafetyTimers.current.forEach(clearTimeout);
|
||||
reloadSafetyTimers.current = [];
|
||||
setChartPaintReady(false);
|
||||
managerRef.current?.clearForSymbolChange();
|
||||
managerRef.current?.cancelPendingIndicatorUpdates();
|
||||
}, [market, timeframe]);
|
||||
@@ -1338,8 +1339,9 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
if (!mgr.hasMainSeries()) return;
|
||||
|
||||
const timers = [0, 150, 400, 800, 1500, 2500, 4000].map(delay =>
|
||||
const timers = [600, 1200, 2500, 4000].map(delay =>
|
||||
window.setTimeout(() => {
|
||||
if (reloadInFlightRef.current) return;
|
||||
const m = managerRef.current;
|
||||
if (!m?.hasMainSeries()) return;
|
||||
const w = wrapperRef.current;
|
||||
@@ -1498,20 +1500,28 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
setCtxMenu(null);
|
||||
}, [ctxMenu, market]);
|
||||
|
||||
const showChartLoading = !chartPaintReady || dataLoading;
|
||||
|
||||
return (
|
||||
// wrapperRef: 스크롤 영역 (overflow-y: auto → App.css .tv-chart-wrap)
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={`tv-chart-wrap${candleOnlyMode ? ' tv-chart-wrap--candle-only' : ''}`}
|
||||
className={`tv-chart-wrap${candleOnlyMode ? ' tv-chart-wrap--candle-only' : ''}${chartPaintReady ? '' : ' tv-chart-wrap--bootstrapping'}`}
|
||||
style={{ flex: 1, position: 'relative', minHeight: 0 }}
|
||||
onPointerMove={handleWrapperMouseMove}
|
||||
>
|
||||
{showChartLoading && (
|
||||
<div className="chart-loading chart-loading--chart-area" role="status" aria-live="polite">
|
||||
<div className="loading-spinner" />
|
||||
<span>차트 로딩중...</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="chart-main-row">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="chart-container"
|
||||
/>
|
||||
{chartMgr && !candleOnlyMode && showPaneLegend && (
|
||||
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
|
||||
<ChartRightToolbar
|
||||
manager={chartMgr}
|
||||
indicators={indicators}
|
||||
@@ -1546,7 +1556,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
getContainer={() => containerRef.current}
|
||||
/>
|
||||
)}
|
||||
{chartMgr && paneSeparatorOptions && (
|
||||
{chartMgr && chartPaintReady && paneSeparatorOptions && (
|
||||
<PaneSeparatorOverlay
|
||||
manager={chartMgr}
|
||||
options={paneSeparatorOptions}
|
||||
@@ -1576,7 +1586,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
onRestore={() => setCandleOnlyMode(false)}
|
||||
/>
|
||||
)}
|
||||
{chartMgr && !candleOnlyMode && showPaneLegend && (
|
||||
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
|
||||
<PaneLegendPortal
|
||||
key={indKey(indicators)}
|
||||
manager={chartMgr}
|
||||
|
||||
Reference in New Issue
Block a user