위젯 종료버튼 겹침문제 수정
This commit is contained in:
@@ -102,7 +102,11 @@ export const TopMenuBarNavGroups = memo(function TopMenuBarNavGroups({
|
||||
className={`tmb-nav-btn tmb-nav-group-btn${isActive ? ' tmb-nav-btn--active' : ''}${isOpen ? ' tmb-nav-group-btn--open' : ''}`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="menu"
|
||||
onClick={() => setOpenGroupId(prev => (prev === group.id ? null : group.id))}
|
||||
onPointerDown={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenGroupId(prev => (prev === group.id ? null : group.id));
|
||||
}}
|
||||
>
|
||||
<span className="tmb-nav-icon">{group.icon}</span>
|
||||
<span className="tmb-nav-label">{group.label}</span>
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
ensurePaperChartOverlays,
|
||||
} from '../../utils/strategyToChartIndicators';
|
||||
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
||||
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||
import WidgetTimeframePicker from '../../widgets/panels/WidgetTimeframePicker';
|
||||
|
||||
interface Props {
|
||||
symbol: string;
|
||||
@@ -61,6 +63,9 @@ interface Props {
|
||||
focusTimeSec?: number | null;
|
||||
/** 하단 확대·축소·좌우 이동 툴바 (위젯 등에서는 false) */
|
||||
showHoverToolbar?: boolean;
|
||||
/** 종목 우측 시간봉 선택 (위젯 전략 차트) */
|
||||
timeframeOptions?: Timeframe[];
|
||||
onTimeframeChange?: (tf: Timeframe) => void;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
@@ -117,6 +122,8 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
reportDisabled = false,
|
||||
focusTimeSec = null,
|
||||
showHoverToolbar = true,
|
||||
timeframeOptions,
|
||||
onTimeframeChange,
|
||||
}) => {
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
const { defaults: appDefaults } = useAppSettings();
|
||||
@@ -414,7 +421,16 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
<div className="btd-analysis-toolbar">
|
||||
<div className="btd-analysis-toolbar-left">
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{koName}</span>
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{tfKo}</span>
|
||||
{onTimeframeChange ? (
|
||||
<WidgetTimeframePicker
|
||||
options={timeframeOptions ?? WIDGET_CHART_TF_OPTIONS}
|
||||
value={chartTimeframe}
|
||||
onChange={onTimeframeChange}
|
||||
className="btd-analysis-tf-row"
|
||||
/>
|
||||
) : (
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{tfKo}</span>
|
||||
)}
|
||||
<span className="btd-analysis-select btd-analysis-select--static">{chartTypeKo}</span>
|
||||
</div>
|
||||
<div className="btd-analysis-toolbar-right">
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
} from '../../utils/backendApi';
|
||||
import { loadAnalysisCandles } from '../../utils/analysisChartData';
|
||||
import { normalizeChartTimeframe } from '../../utils/backtestUiUtils';
|
||||
import { mergeWidgetTimeframeOptions } from '../../utils/chartWidgetTimeframes';
|
||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||
import { resolveEvaluationFromLoadedBars } from '../../utils/backtestWarmup';
|
||||
import type { OHLCVBar, Theme } from '../../types';
|
||||
import type { OHLCVBar, Theme, Timeframe } from '../../types';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
import {
|
||||
DEFAULT_PAPER_OVERLAY_VISIBILITY,
|
||||
@@ -28,6 +29,8 @@ interface Props {
|
||||
theme?: Theme;
|
||||
/** 위젯 임베드 시 하단 줌/패닝 툴바 숨김 */
|
||||
showHoverToolbar?: boolean;
|
||||
/** 위젯 — 종목 우측 시간봉 선택 */
|
||||
enableTimeframePicker?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,12 +38,16 @@ interface Props {
|
||||
* 좌측(과거) 스크롤 시 추가 캔들·보조지표 로드 후 전체 구간 시그널 재계산.
|
||||
*/
|
||||
const PaperAnalysisChart: React.FC<Props> = ({
|
||||
market, strategyId, theme = 'dark', showHoverToolbar = true,
|
||||
market,
|
||||
strategyId,
|
||||
theme = 'dark',
|
||||
showHoverToolbar = true,
|
||||
enableTimeframePicker = false,
|
||||
}) => {
|
||||
const { getParams } = useIndicatorSettings();
|
||||
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||
const [timeframe, setTimeframe] = useState('1h');
|
||||
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
||||
const [running, setRunning] = useState(false);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -53,8 +60,15 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
const historyDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const strategyRef = useRef(strategy);
|
||||
const strategyIdRef = useRef(strategyId);
|
||||
const timeframeRef = useRef(timeframe);
|
||||
strategyRef.current = strategy;
|
||||
strategyIdRef.current = strategyId;
|
||||
timeframeRef.current = timeframe;
|
||||
|
||||
const timeframeOptions = useMemo(
|
||||
() => mergeWidgetTimeframeOptions(strategy),
|
||||
[strategy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategyId) {
|
||||
@@ -71,6 +85,11 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
return () => { cancelled = true; };
|
||||
}, [strategyId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategy) return;
|
||||
setTimeframe(normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy)));
|
||||
}, [strategyId, strategy?.id]);
|
||||
|
||||
const runSignalsForBars = useCallback(async (
|
||||
bars: OHLCVBar[],
|
||||
tf: string,
|
||||
@@ -139,8 +158,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy));
|
||||
setTimeframe(tf);
|
||||
const tf = normalizeChartTimeframe(timeframe);
|
||||
setError(null);
|
||||
|
||||
void (async () => {
|
||||
@@ -162,7 +180,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
cancelled = true;
|
||||
++backtestGenRef.current;
|
||||
};
|
||||
}, [market, strategyId, strategy, toTimeSec, runSignalsForBars]);
|
||||
}, [market, strategyId, strategy, timeframe, toTimeSec, runSignalsForBars]);
|
||||
|
||||
const handleToggleOverlay = useCallback((key: PaperOverlayToggleKey) => {
|
||||
setOverlayVisibility(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
@@ -172,7 +190,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
const strat = strategyRef.current;
|
||||
const sid = strategyIdRef.current;
|
||||
if (!strat || !sid) return;
|
||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strat));
|
||||
const tf = normalizeChartTimeframe(timeframeRef.current);
|
||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||
historyDebounceRef.current = setTimeout(() => {
|
||||
historyDebounceRef.current = null;
|
||||
@@ -180,6 +198,10 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
}, HISTORY_BACKTEST_DEBOUNCE_MS);
|
||||
}, [runSignalsForBars]);
|
||||
|
||||
const handleTimeframeChange = useCallback((tf: Timeframe) => {
|
||||
setTimeframe(tf);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||
}, []);
|
||||
@@ -195,6 +217,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
const statusBusy = running || historyLoading;
|
||||
const chartTimeframe = normalizeChartTimeframe(timeframe);
|
||||
|
||||
return (
|
||||
<div className="ptd-analysis-chart ptd-analysis-chart--backtest">
|
||||
@@ -208,7 +231,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
)}
|
||||
<BacktestAnalysisChart
|
||||
symbol={market}
|
||||
timeframe={timeframe}
|
||||
timeframe={chartTimeframe}
|
||||
toTimeSec={toTimeSec}
|
||||
barCount={BAR_COUNT}
|
||||
signals={signals}
|
||||
@@ -219,6 +242,8 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
||||
overlayVisibility={overlayVisibility}
|
||||
onToggleOverlay={handleToggleOverlay}
|
||||
showHoverToolbar={showHoverToolbar}
|
||||
timeframeOptions={enableTimeframePicker ? timeframeOptions : undefined}
|
||||
onTimeframeChange={enableTimeframePicker ? handleTimeframeChange : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createChart, CandlestickSeries, type IChartApi, type ISeriesApi, type Time, ColorType } from 'lightweight-charts';
|
||||
import type { Theme, Timeframe } from '../../types';
|
||||
import { fetchUpbitCandles } from '../../utils/upbitApi';
|
||||
|
||||
const TF_OPTIONS: Timeframe[] = ['1m', '5m', '10m', '15m', '1h', '4h', '1D'];
|
||||
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
@@ -11,6 +10,10 @@ interface Props {
|
||||
wsLabel?: string;
|
||||
timeframe?: Timeframe;
|
||||
onTimeframeChange?: (tf: Timeframe) => void;
|
||||
/** 위젯 헤더에 시간봉이 있을 때 내부 행 숨김 */
|
||||
hideTimeframeRow?: boolean;
|
||||
/** 위젯 임베드 시 상단 타이틀·배지 숨김 */
|
||||
hideChartHead?: boolean;
|
||||
}
|
||||
|
||||
function readChartTheme(container: HTMLElement | null) {
|
||||
@@ -33,6 +36,8 @@ const PaperMiniChart: React.FC<Props> = ({
|
||||
wsLabel = 'WebSocket <100ms',
|
||||
timeframe: timeframeProp,
|
||||
onTimeframeChange,
|
||||
hideTimeframeRow = false,
|
||||
hideChartHead = false,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const chartRef = useRef<IChartApi | null>(null);
|
||||
@@ -159,24 +164,28 @@ const PaperMiniChart: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="ptd-chart-wrap">
|
||||
<div className="ptd-chart-head">
|
||||
<span className="ptd-chart-title">실시간 분석 차트</span>
|
||||
<span className="ptd-ws-badge">{wsLabel} 실시간 데이터 반영</span>
|
||||
</div>
|
||||
{!hideChartHead && (
|
||||
<div className="ptd-chart-head">
|
||||
<span className="ptd-chart-title">실시간 분석 차트</span>
|
||||
<span className="ptd-ws-badge">{wsLabel} 실시간 데이터 반영</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={containerRef} className="ptd-chart-canvas" />
|
||||
{loading && <div className="ptd-chart-loading">차트 로딩…</div>}
|
||||
<div className="ptd-tf-row">
|
||||
{TF_OPTIONS.map(tf => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
className={`ptd-tf-btn${timeframe === tf ? ' ptd-tf-btn--active' : ''}`}
|
||||
onClick={() => setTimeframe(tf)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!hideTimeframeRow && (
|
||||
<div className="ptd-tf-row">
|
||||
{WIDGET_CHART_TF_OPTIONS.map(tf => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
className={`ptd-tf-btn${timeframe === tf ? ' ptd-tf-btn--active' : ''}`}
|
||||
onClick={() => setTimeframe(tf)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ const WidgetSlotView: React.FC<Props> = ({
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
<div className="wd-slot-body">
|
||||
<div className={['wd-slot-body', Host && onClearWidget ? 'wd-slot-body--has-clear' : ''].filter(Boolean).join(' ')}>
|
||||
{Host ? (
|
||||
<Host config={slot.config} />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user