위젯 종료버튼 겹침문제 수정

This commit is contained in:
Macbook
2026-06-15 20:59:07 +09:00
parent 8b45362a00
commit 44f6ddfa0f
16 changed files with 263 additions and 53 deletions
@@ -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} />
) : (