{Host ? (
) : (
diff --git a/frontend/src/styles/backtestDashboard.css b/frontend/src/styles/backtestDashboard.css
index 77e9afa..dadb432 100644
--- a/frontend/src/styles/backtestDashboard.css
+++ b/frontend/src/styles/backtestDashboard.css
@@ -1507,6 +1507,12 @@
display: flex;
align-items: center;
gap: 8px;
+ min-width: 0;
+}
+
+.btd-analysis-toolbar-right {
+ flex-shrink: 1;
+ justify-content: flex-end;
}
.btd-analysis-select--static {
@@ -1519,6 +1525,14 @@
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 {
width: 28px;
height: 28px;
diff --git a/frontend/src/styles/widgetDashboard.css b/frontend/src/styles/widgetDashboard.css
index d804c9b..b1582f3 100644
--- a/frontend/src/styles/widgetDashboard.css
+++ b/frontend/src/styles/widgetDashboard.css
@@ -164,16 +164,53 @@
position: absolute;
top: 6px;
right: 6px;
- z-index: 3;
+ z-index: 10;
width: 22px;
height: 22px;
border: 1px solid var(--wd-slot-border);
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);
font-size: 0.65rem;
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 {
@@ -447,6 +484,33 @@
display: flex;
flex-wrap: wrap;
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 {
diff --git a/frontend/src/utils/chartWidgetTimeframes.ts b/frontend/src/utils/chartWidgetTimeframes.ts
new file mode 100644
index 0000000..d0f5fd0
--- /dev/null
+++ b/frontend/src/utils/chartWidgetTimeframes.ts
@@ -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
(
+ WIDGET_CHART_TF_OPTIONS.map((tf, i) => [tf, i]),
+);
+
+/** 전략 DSL 시간봉 + 위젯 기본 옵션 병합 (중복 제거, 표준 순서) */
+export function mergeWidgetTimeframeOptions(strategy?: StrategyDto): Timeframe[] {
+ const merged = new Set([...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);
+ });
+}
diff --git a/frontend/src/widgets/WidgetCandleChartPanel.tsx b/frontend/src/widgets/WidgetCandleChartPanel.tsx
index c910aa5..b0f5a6c 100644
--- a/frontend/src/widgets/WidgetCandleChartPanel.tsx
+++ b/frontend/src/widgets/WidgetCandleChartPanel.tsx
@@ -22,8 +22,9 @@ import type { ChartManager } from '../utils/ChartManager';
import {
buildVirtualTradingChartIndicators,
resolveStrategyPrimaryTimeframe,
- resolveStrategyTimeframes,
} from '../utils/strategyToChartIndicators';
+import { mergeWidgetTimeframeOptions } from '../utils/chartWidgetTimeframes';
+import WidgetTimeframePicker from './panels/WidgetTimeframePicker';
import { timeframeToCandleType } from '../utils/chartCandleType';
import { DISPLAY_COUNT } from '../hooks/useUpbitData';
import { getKoreanName } from '../utils/marketNameCache';
@@ -76,7 +77,7 @@ const WidgetCandleChartPanel: React.FC = ({
chartRealtimeSource = 'BACKEND_STOMP',
}) => {
const { getParams, getVisualConfig } = useIndicatorSettings();
- const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
+ const tfOptions = useMemo(() => mergeWidgetTimeframeOptions(strategy), [strategy]);
const [timeframe, setTimeframe] = useState(
() => resolveStrategyPrimaryTimeframe(strategy),
);
@@ -402,20 +403,11 @@ const WidgetCandleChartPanel: React.FC = ({
{market}
- {tfOptions.length > 1 && (
-
- {tfOptions.map(tf => (
-
- ))}
-
- )}
+
diff --git a/frontend/src/widgets/hosts/MiniChartWidgetHost.tsx b/frontend/src/widgets/hosts/MiniChartWidgetHost.tsx
index 0206c38..ca67753 100644
--- a/frontend/src/widgets/hosts/MiniChartWidgetHost.tsx
+++ b/frontend/src/widgets/hosts/MiniChartWidgetHost.tsx
@@ -1,19 +1,39 @@
-import React from 'react';
+import React, { useState } from 'react';
import PaperMiniChart from '../../components/paper/PaperMiniChart';
import { useWidgetDashboard } from '../WidgetDashboardContext';
import WidgetPanelHeader from '../panels/WidgetPanelHeader';
+import WidgetTimeframePicker from '../panels/WidgetTimeframePicker';
+import { WIDGET_CHART_TF_OPTIONS } from '../../utils/chartWidgetTimeframes';
import { resolveName } from '../widgetMarketUtils';
+import type { Timeframe } from '../../types';
const MiniChartWidgetHost: React.FC = () => {
const { selectedMarket, theme, tickers } = useWidgetDashboard();
const ticker = tickers.get(selectedMarket);
+ const [timeframe, setTimeframe] = useState
('1h');
return (
-
{resolveName(selectedMarket, ticker)} · {selectedMarket}
+
+
+ {resolveName(selectedMarket, ticker)} · {selectedMarket}
+
+
+
);
diff --git a/frontend/src/widgets/hosts/StrategyChartWidgetHost.tsx b/frontend/src/widgets/hosts/StrategyChartWidgetHost.tsx
index c9d8bf4..4fc4dcb 100644
--- a/frontend/src/widgets/hosts/StrategyChartWidgetHost.tsx
+++ b/frontend/src/widgets/hosts/StrategyChartWidgetHost.tsx
@@ -23,6 +23,7 @@ const StrategyChartWidgetHost: React.FC<{ config?: Record }> =
strategyId={strategyId}
theme={theme}
showHoverToolbar={false}
+ enableTimeframePicker
/>
);
diff --git a/frontend/src/widgets/panels/WidgetTimeframePicker.tsx b/frontend/src/widgets/panels/WidgetTimeframePicker.tsx
new file mode 100644
index 0000000..56d61a2
--- /dev/null
+++ b/frontend/src/widgets/panels/WidgetTimeframePicker.tsx
@@ -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
= ({
+ options,
+ value,
+ onChange,
+ className = '',
+ 'aria-label': ariaLabel = '시간봉',
+}) => (
+
+ {options.map(tf => (
+
+ ))}
+
+);
+
+export default WidgetTimeframePicker;