위젯 종료버튼 겹침문제 수정
This commit is contained in:
@@ -20,7 +20,8 @@
|
|||||||
"minWidth": 960,
|
"minWidth": 960,
|
||||||
"minHeight": 640,
|
"minHeight": 640,
|
||||||
"center": true,
|
"center": true,
|
||||||
"visible": true
|
"visible": true,
|
||||||
|
"acceptFirstMouse": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export async function openWidgetPickerWindow(params: {
|
|||||||
center: true,
|
center: true,
|
||||||
decorations: true,
|
decorations: true,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
|
acceptFirstMouse: true,
|
||||||
backgroundColor: '#1a1b26',
|
backgroundColor: '#1a1b26',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export async function openWidgetWindow(instance: FloatingWidgetInstance): Promis
|
|||||||
minHeight: 240,
|
minHeight: 240,
|
||||||
decorations: true,
|
decorations: true,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
acceptFirstMouse: true,
|
||||||
center: false,
|
center: false,
|
||||||
x: instance.position.x,
|
x: instance.position.x,
|
||||||
y: instance.position.y,
|
y: instance.position.y,
|
||||||
|
|||||||
@@ -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' : ''}`}
|
className={`tmb-nav-btn tmb-nav-group-btn${isActive ? ' tmb-nav-btn--active' : ''}${isOpen ? ' tmb-nav-group-btn--open' : ''}`}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
aria-haspopup="menu"
|
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-icon">{group.icon}</span>
|
||||||
<span className="tmb-nav-label">{group.label}</span>
|
<span className="tmb-nav-label">{group.label}</span>
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
ensurePaperChartOverlays,
|
ensurePaperChartOverlays,
|
||||||
} from '../../utils/strategyToChartIndicators';
|
} from '../../utils/strategyToChartIndicators';
|
||||||
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
||||||
|
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||||
|
import WidgetTimeframePicker from '../../widgets/panels/WidgetTimeframePicker';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
@@ -61,6 +63,9 @@ interface Props {
|
|||||||
focusTimeSec?: number | null;
|
focusTimeSec?: number | null;
|
||||||
/** 하단 확대·축소·좌우 이동 툴바 (위젯 등에서는 false) */
|
/** 하단 확대·축소·좌우 이동 툴바 (위젯 등에서는 false) */
|
||||||
showHoverToolbar?: boolean;
|
showHoverToolbar?: boolean;
|
||||||
|
/** 종목 우측 시간봉 선택 (위젯 전략 차트) */
|
||||||
|
timeframeOptions?: Timeframe[];
|
||||||
|
onTimeframeChange?: (tf: Timeframe) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -117,6 +122,8 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
reportDisabled = false,
|
reportDisabled = false,
|
||||||
focusTimeSec = null,
|
focusTimeSec = null,
|
||||||
showHoverToolbar = true,
|
showHoverToolbar = true,
|
||||||
|
timeframeOptions,
|
||||||
|
onTimeframeChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||||
const { defaults: appDefaults } = useAppSettings();
|
const { defaults: appDefaults } = useAppSettings();
|
||||||
@@ -414,7 +421,16 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
<div className="btd-analysis-toolbar">
|
<div className="btd-analysis-toolbar">
|
||||||
<div className="btd-analysis-toolbar-left">
|
<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">{koName}</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">{tfKo}</span>
|
||||||
|
)}
|
||||||
<span className="btd-analysis-select btd-analysis-select--static">{chartTypeKo}</span>
|
<span className="btd-analysis-select btd-analysis-select--static">{chartTypeKo}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-analysis-toolbar-right">
|
<div className="btd-analysis-toolbar-right">
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
} from '../../utils/backendApi';
|
} from '../../utils/backendApi';
|
||||||
import { loadAnalysisCandles } from '../../utils/analysisChartData';
|
import { loadAnalysisCandles } from '../../utils/analysisChartData';
|
||||||
import { normalizeChartTimeframe } from '../../utils/backtestUiUtils';
|
import { normalizeChartTimeframe } from '../../utils/backtestUiUtils';
|
||||||
|
import { mergeWidgetTimeframeOptions } from '../../utils/chartWidgetTimeframes';
|
||||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||||
import { resolveEvaluationFromLoadedBars } from '../../utils/backtestWarmup';
|
import { resolveEvaluationFromLoadedBars } from '../../utils/backtestWarmup';
|
||||||
import type { OHLCVBar, Theme } from '../../types';
|
import type { OHLCVBar, Theme, Timeframe } from '../../types';
|
||||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||||
import {
|
import {
|
||||||
DEFAULT_PAPER_OVERLAY_VISIBILITY,
|
DEFAULT_PAPER_OVERLAY_VISIBILITY,
|
||||||
@@ -28,6 +29,8 @@ interface Props {
|
|||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
/** 위젯 임베드 시 하단 줌/패닝 툴바 숨김 */
|
/** 위젯 임베드 시 하단 줌/패닝 툴바 숨김 */
|
||||||
showHoverToolbar?: boolean;
|
showHoverToolbar?: boolean;
|
||||||
|
/** 위젯 — 종목 우측 시간봉 선택 */
|
||||||
|
enableTimeframePicker?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,12 +38,16 @@ interface Props {
|
|||||||
* 좌측(과거) 스크롤 시 추가 캔들·보조지표 로드 후 전체 구간 시그널 재계산.
|
* 좌측(과거) 스크롤 시 추가 캔들·보조지표 로드 후 전체 구간 시그널 재계산.
|
||||||
*/
|
*/
|
||||||
const PaperAnalysisChart: React.FC<Props> = ({
|
const PaperAnalysisChart: React.FC<Props> = ({
|
||||||
market, strategyId, theme = 'dark', showHoverToolbar = true,
|
market,
|
||||||
|
strategyId,
|
||||||
|
theme = 'dark',
|
||||||
|
showHoverToolbar = true,
|
||||||
|
enableTimeframePicker = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams } = useIndicatorSettings();
|
const { getParams } = useIndicatorSettings();
|
||||||
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
||||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||||
const [timeframe, setTimeframe] = useState('1h');
|
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [historyLoading, setHistoryLoading] = useState(false);
|
const [historyLoading, setHistoryLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 historyDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const strategyRef = useRef(strategy);
|
const strategyRef = useRef(strategy);
|
||||||
const strategyIdRef = useRef(strategyId);
|
const strategyIdRef = useRef(strategyId);
|
||||||
|
const timeframeRef = useRef(timeframe);
|
||||||
strategyRef.current = strategy;
|
strategyRef.current = strategy;
|
||||||
strategyIdRef.current = strategyId;
|
strategyIdRef.current = strategyId;
|
||||||
|
timeframeRef.current = timeframe;
|
||||||
|
|
||||||
|
const timeframeOptions = useMemo(
|
||||||
|
() => mergeWidgetTimeframeOptions(strategy),
|
||||||
|
[strategy],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!strategyId) {
|
if (!strategyId) {
|
||||||
@@ -71,6 +85,11 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [strategyId]);
|
}, [strategyId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!strategy) return;
|
||||||
|
setTimeframe(normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy)));
|
||||||
|
}, [strategyId, strategy?.id]);
|
||||||
|
|
||||||
const runSignalsForBars = useCallback(async (
|
const runSignalsForBars = useCallback(async (
|
||||||
bars: OHLCVBar[],
|
bars: OHLCVBar[],
|
||||||
tf: string,
|
tf: string,
|
||||||
@@ -139,8 +158,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy));
|
const tf = normalizeChartTimeframe(timeframe);
|
||||||
setTimeframe(tf);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@@ -162,7 +180,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
++backtestGenRef.current;
|
++backtestGenRef.current;
|
||||||
};
|
};
|
||||||
}, [market, strategyId, strategy, toTimeSec, runSignalsForBars]);
|
}, [market, strategyId, strategy, timeframe, toTimeSec, runSignalsForBars]);
|
||||||
|
|
||||||
const handleToggleOverlay = useCallback((key: PaperOverlayToggleKey) => {
|
const handleToggleOverlay = useCallback((key: PaperOverlayToggleKey) => {
|
||||||
setOverlayVisibility(prev => ({ ...prev, [key]: !prev[key] }));
|
setOverlayVisibility(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
@@ -172,7 +190,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
const strat = strategyRef.current;
|
const strat = strategyRef.current;
|
||||||
const sid = strategyIdRef.current;
|
const sid = strategyIdRef.current;
|
||||||
if (!strat || !sid) return;
|
if (!strat || !sid) return;
|
||||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strat));
|
const tf = normalizeChartTimeframe(timeframeRef.current);
|
||||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||||
historyDebounceRef.current = setTimeout(() => {
|
historyDebounceRef.current = setTimeout(() => {
|
||||||
historyDebounceRef.current = null;
|
historyDebounceRef.current = null;
|
||||||
@@ -180,6 +198,10 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
}, HISTORY_BACKTEST_DEBOUNCE_MS);
|
}, HISTORY_BACKTEST_DEBOUNCE_MS);
|
||||||
}, [runSignalsForBars]);
|
}, [runSignalsForBars]);
|
||||||
|
|
||||||
|
const handleTimeframeChange = useCallback((tf: Timeframe) => {
|
||||||
|
setTimeframe(tf);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -195,6 +217,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statusBusy = running || historyLoading;
|
const statusBusy = running || historyLoading;
|
||||||
|
const chartTimeframe = normalizeChartTimeframe(timeframe);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ptd-analysis-chart ptd-analysis-chart--backtest">
|
<div className="ptd-analysis-chart ptd-analysis-chart--backtest">
|
||||||
@@ -208,7 +231,7 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
<BacktestAnalysisChart
|
<BacktestAnalysisChart
|
||||||
symbol={market}
|
symbol={market}
|
||||||
timeframe={timeframe}
|
timeframe={chartTimeframe}
|
||||||
toTimeSec={toTimeSec}
|
toTimeSec={toTimeSec}
|
||||||
barCount={BAR_COUNT}
|
barCount={BAR_COUNT}
|
||||||
signals={signals}
|
signals={signals}
|
||||||
@@ -219,6 +242,8 @@ const PaperAnalysisChart: React.FC<Props> = ({
|
|||||||
overlayVisibility={overlayVisibility}
|
overlayVisibility={overlayVisibility}
|
||||||
onToggleOverlay={handleToggleOverlay}
|
onToggleOverlay={handleToggleOverlay}
|
||||||
showHoverToolbar={showHoverToolbar}
|
showHoverToolbar={showHoverToolbar}
|
||||||
|
timeframeOptions={enableTimeframePicker ? timeframeOptions : undefined}
|
||||||
|
onTimeframeChange={enableTimeframePicker ? handleTimeframeChange : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { createChart, CandlestickSeries, type IChartApi, type ISeriesApi, type Time, ColorType } from 'lightweight-charts';
|
||||||
import type { Theme, Timeframe } from '../../types';
|
import type { Theme, Timeframe } from '../../types';
|
||||||
import { fetchUpbitCandles } from '../../utils/upbitApi';
|
import { fetchUpbitCandles } from '../../utils/upbitApi';
|
||||||
|
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||||
const TF_OPTIONS: Timeframe[] = ['1m', '5m', '10m', '15m', '1h', '4h', '1D'];
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
market: string;
|
market: string;
|
||||||
@@ -11,6 +10,10 @@ interface Props {
|
|||||||
wsLabel?: string;
|
wsLabel?: string;
|
||||||
timeframe?: Timeframe;
|
timeframe?: Timeframe;
|
||||||
onTimeframeChange?: (tf: Timeframe) => void;
|
onTimeframeChange?: (tf: Timeframe) => void;
|
||||||
|
/** 위젯 헤더에 시간봉이 있을 때 내부 행 숨김 */
|
||||||
|
hideTimeframeRow?: boolean;
|
||||||
|
/** 위젯 임베드 시 상단 타이틀·배지 숨김 */
|
||||||
|
hideChartHead?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readChartTheme(container: HTMLElement | null) {
|
function readChartTheme(container: HTMLElement | null) {
|
||||||
@@ -33,6 +36,8 @@ const PaperMiniChart: React.FC<Props> = ({
|
|||||||
wsLabel = 'WebSocket <100ms',
|
wsLabel = 'WebSocket <100ms',
|
||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
onTimeframeChange,
|
onTimeframeChange,
|
||||||
|
hideTimeframeRow = false,
|
||||||
|
hideChartHead = false,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const chartRef = useRef<IChartApi | null>(null);
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
@@ -159,14 +164,17 @@ const PaperMiniChart: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ptd-chart-wrap">
|
<div className="ptd-chart-wrap">
|
||||||
|
{!hideChartHead && (
|
||||||
<div className="ptd-chart-head">
|
<div className="ptd-chart-head">
|
||||||
<span className="ptd-chart-title">실시간 분석 차트</span>
|
<span className="ptd-chart-title">실시간 분석 차트</span>
|
||||||
<span className="ptd-ws-badge">{wsLabel} 실시간 데이터 반영</span>
|
<span className="ptd-ws-badge">{wsLabel} 실시간 데이터 반영</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div ref={containerRef} className="ptd-chart-canvas" />
|
<div ref={containerRef} className="ptd-chart-canvas" />
|
||||||
{loading && <div className="ptd-chart-loading">차트 로딩…</div>}
|
{loading && <div className="ptd-chart-loading">차트 로딩…</div>}
|
||||||
|
{!hideTimeframeRow && (
|
||||||
<div className="ptd-tf-row">
|
<div className="ptd-tf-row">
|
||||||
{TF_OPTIONS.map(tf => (
|
{WIDGET_CHART_TF_OPTIONS.map(tf => (
|
||||||
<button
|
<button
|
||||||
key={tf}
|
key={tf}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -177,6 +185,7 @@ const PaperMiniChart: React.FC<Props> = ({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const WidgetSlotView: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
<div className="wd-slot-body">
|
<div className={['wd-slot-body', Host && onClearWidget ? 'wd-slot-body--has-clear' : ''].filter(Boolean).join(' ')}>
|
||||||
{Host ? (
|
{Host ? (
|
||||||
<Host config={slot.config} />
|
<Host config={slot.config} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1507,6 +1507,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-analysis-toolbar-right {
|
||||||
|
flex-shrink: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-analysis-select--static {
|
.btd-analysis-select--static {
|
||||||
@@ -1519,6 +1525,14 @@
|
|||||||
background: color-mix(in srgb, var(--btd-bg) 60%, transparent);
|
background: color-mix(in srgb, var(--btd-bg) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btd-analysis-tf-row {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
.btd-analysis-tool {
|
.btd-analysis-tool {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
|||||||
@@ -164,16 +164,53 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
right: 6px;
|
right: 6px;
|
||||||
z-index: 3;
|
z-index: 10;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border: 1px solid var(--wd-slot-border);
|
border: 1px solid var(--wd-slot-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: color-mix(in srgb, var(--bg2) 88%, transparent);
|
background: color-mix(in srgb, var(--bg2) 92%, transparent);
|
||||||
color: var(--se-text-muted);
|
color: var(--se-text-muted);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.55;
|
opacity: 0.85;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 위젯 ✕ 버튼 — 차트·패널 헤더 텍스트와 겹침 방지 */
|
||||||
|
.wd-slot-body--has-clear .wd-candle-chart-header,
|
||||||
|
.wd-slot-body--has-clear .btd-analysis-toolbar,
|
||||||
|
.wd-slot-body--has-clear .wd-panel-header,
|
||||||
|
.wd-slot-body--has-clear .wd-chart-symbol-tf-bar {
|
||||||
|
padding-right: 36px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-slot-body--has-clear .btd-analysis-toolbar-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-slot-body--has-clear .btd-analysis-toolbar-right {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 42%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-slot-body--has-clear .btd-analysis-count {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-slot-body--has-clear .wd-candle-chart-header-actions {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
max-width: min(100%, 220px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wd-slot-overlay-clear:hover {
|
.wd-slot-overlay-clear:hover {
|
||||||
@@ -447,6 +484,33 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-chart-symbol-tf-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 4px 10px 6px;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--se-border, var(--border)) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-chart-symbol-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--se-text, var(--text));
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: min(220px, 42%);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-chart-symbol-tf-bar .wd-candle-chart-tf-row {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wd-candle-chart-tf {
|
.wd-candle-chart-tf {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Timeframe } from '../types';
|
||||||
|
import type { StrategyDto } from './backendApi';
|
||||||
|
import { resolveStrategyTimeframes } from './strategyToChartIndicators';
|
||||||
|
|
||||||
|
/** 위젯 차트 — 종목 우측 시간봉 선택 기본 옵션 */
|
||||||
|
export const WIDGET_CHART_TF_OPTIONS: Timeframe[] = [
|
||||||
|
'1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1D',
|
||||||
|
];
|
||||||
|
|
||||||
|
const TF_ORDER = new Map<Timeframe, number>(
|
||||||
|
WIDGET_CHART_TF_OPTIONS.map((tf, i) => [tf, i]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 전략 DSL 시간봉 + 위젯 기본 옵션 병합 (중복 제거, 표준 순서) */
|
||||||
|
export function mergeWidgetTimeframeOptions(strategy?: StrategyDto): Timeframe[] {
|
||||||
|
const merged = new Set<Timeframe>([...WIDGET_CHART_TF_OPTIONS, ...resolveStrategyTimeframes(strategy)]);
|
||||||
|
return [...merged].sort((a, b) => {
|
||||||
|
const ai = TF_ORDER.get(a) ?? 999;
|
||||||
|
const bi = TF_ORDER.get(b) ?? 999;
|
||||||
|
if (ai !== bi) return ai - bi;
|
||||||
|
return a.localeCompare(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -22,8 +22,9 @@ import type { ChartManager } from '../utils/ChartManager';
|
|||||||
import {
|
import {
|
||||||
buildVirtualTradingChartIndicators,
|
buildVirtualTradingChartIndicators,
|
||||||
resolveStrategyPrimaryTimeframe,
|
resolveStrategyPrimaryTimeframe,
|
||||||
resolveStrategyTimeframes,
|
|
||||||
} from '../utils/strategyToChartIndicators';
|
} from '../utils/strategyToChartIndicators';
|
||||||
|
import { mergeWidgetTimeframeOptions } from '../utils/chartWidgetTimeframes';
|
||||||
|
import WidgetTimeframePicker from './panels/WidgetTimeframePicker';
|
||||||
import { timeframeToCandleType } from '../utils/chartCandleType';
|
import { timeframeToCandleType } from '../utils/chartCandleType';
|
||||||
import { DISPLAY_COUNT } from '../hooks/useUpbitData';
|
import { DISPLAY_COUNT } from '../hooks/useUpbitData';
|
||||||
import { getKoreanName } from '../utils/marketNameCache';
|
import { getKoreanName } from '../utils/marketNameCache';
|
||||||
@@ -76,7 +77,7 @@ const WidgetCandleChartPanel: React.FC<Props> = ({
|
|||||||
chartRealtimeSource = 'BACKEND_STOMP',
|
chartRealtimeSource = 'BACKEND_STOMP',
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||||
const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
|
const tfOptions = useMemo(() => mergeWidgetTimeframeOptions(strategy), [strategy]);
|
||||||
const [timeframe, setTimeframe] = useState<Timeframe>(
|
const [timeframe, setTimeframe] = useState<Timeframe>(
|
||||||
() => resolveStrategyPrimaryTimeframe(strategy),
|
() => resolveStrategyPrimaryTimeframe(strategy),
|
||||||
);
|
);
|
||||||
@@ -402,20 +403,11 @@ const WidgetCandleChartPanel: React.FC<Props> = ({
|
|||||||
<span className="wd-candle-chart-market-sym">{market}</span>
|
<span className="wd-candle-chart-market-sym">{market}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{tfOptions.length > 1 && (
|
<WidgetTimeframePicker
|
||||||
<div className="wd-candle-chart-tf-row">
|
options={tfOptions}
|
||||||
{tfOptions.map(tf => (
|
value={timeframe}
|
||||||
<button
|
onChange={setTimeframe}
|
||||||
key={tf}
|
/>
|
||||||
type="button"
|
|
||||||
className={`wd-candle-chart-tf${timeframe === tf ? ' wd-candle-chart-tf--on' : ''}`}
|
|
||||||
onClick={() => setTimeframe(tf)}
|
|
||||||
>
|
|
||||||
{tf}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="wd-candle-chart-header-actions">
|
<div className="wd-candle-chart-header-actions">
|
||||||
<div className="wd-candle-chart-toolbar-toggles" role="group" aria-label="차트 툴바 표시">
|
<div className="wd-candle-chart-toolbar-toggles" role="group" aria-label="차트 툴바 표시">
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import PaperMiniChart from '../../components/paper/PaperMiniChart';
|
import PaperMiniChart from '../../components/paper/PaperMiniChart';
|
||||||
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
||||||
import WidgetPanelHeader from '../panels/WidgetPanelHeader';
|
import WidgetPanelHeader from '../panels/WidgetPanelHeader';
|
||||||
|
import WidgetTimeframePicker from '../panels/WidgetTimeframePicker';
|
||||||
|
import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
|
||||||
import { resolveName } from '../widgetMarketUtils';
|
import { resolveName } from '../widgetMarketUtils';
|
||||||
|
import type { Timeframe } from '../../types';
|
||||||
|
|
||||||
const MiniChartWidgetHost: React.FC = () => {
|
const MiniChartWidgetHost: React.FC = () => {
|
||||||
const { selectedMarket, theme, tickers } = useWidgetDashboard();
|
const { selectedMarket, theme, tickers } = useWidgetDashboard();
|
||||||
const ticker = tickers.get(selectedMarket);
|
const ticker = tickers.get(selectedMarket);
|
||||||
|
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="wd-widget-host wd-widget-host--panel wd-widget-host--mini-chart">
|
<div className="wd-widget-host wd-widget-host--panel wd-widget-host--mini-chart">
|
||||||
<WidgetPanelHeader title="미니 차트" />
|
<WidgetPanelHeader title="미니 차트" />
|
||||||
<div className="wd-panel-subline">{resolveName(selectedMarket, ticker)} · {selectedMarket}</div>
|
<div className="wd-chart-symbol-tf-bar">
|
||||||
|
<span className="wd-chart-symbol-label">
|
||||||
|
{resolveName(selectedMarket, ticker)} · {selectedMarket}
|
||||||
|
</span>
|
||||||
|
<WidgetTimeframePicker
|
||||||
|
options={WIDGET_CHART_TF_OPTIONS}
|
||||||
|
value={timeframe}
|
||||||
|
onChange={setTimeframe}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="wd-mini-chart-wrap">
|
<div className="wd-mini-chart-wrap">
|
||||||
<PaperMiniChart market={selectedMarket} theme={theme} />
|
<PaperMiniChart
|
||||||
|
market={selectedMarket}
|
||||||
|
theme={theme}
|
||||||
|
timeframe={timeframe}
|
||||||
|
onTimeframeChange={setTimeframe}
|
||||||
|
hideChartHead
|
||||||
|
hideTimeframeRow
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const StrategyChartWidgetHost: React.FC<{ config?: Record<string, unknown> }> =
|
|||||||
strategyId={strategyId}
|
strategyId={strategyId}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
showHoverToolbar={false}
|
showHoverToolbar={false}
|
||||||
|
enableTimeframePicker
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Timeframe } from '../../types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: Timeframe[];
|
||||||
|
value: Timeframe;
|
||||||
|
onChange: (tf: Timeframe) => void;
|
||||||
|
className?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 위젯 차트 — 종목 우측 시간봉 버튼 그룹 */
|
||||||
|
const WidgetTimeframePicker: React.FC<Props> = ({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className = '',
|
||||||
|
'aria-label': ariaLabel = '시간봉',
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={`wd-candle-chart-tf-row${className ? ` ${className}` : ''}`}
|
||||||
|
role="group"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{options.map(tf => (
|
||||||
|
<button
|
||||||
|
key={tf}
|
||||||
|
type="button"
|
||||||
|
className={`wd-candle-chart-tf${value === tf ? ' wd-candle-chart-tf--on' : ''}`}
|
||||||
|
aria-pressed={value === tf}
|
||||||
|
onClick={() => onChange(tf)}
|
||||||
|
>
|
||||||
|
{tf}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default WidgetTimeframePicker;
|
||||||
Reference in New Issue
Block a user