diff --git a/frontend/src/App.css b/frontend/src/App.css index e6fe0bf..10b64ed 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -966,6 +966,47 @@ html.theme-blue { .tv-time-display { font-size: 12px; color: var(--text2); padding: 0 8px; white-space: nowrap; } .tv-right-toggles { margin-left: 4px; } +/* 하단 바 — 지표 탭 빠른 적용 (전체 탭 제외) */ +.tv-ind-tab-quick { + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; + margin-left: 6px; + padding-left: 8px; + border-left: 1px solid var(--border); + max-width: min(42vw, 420px); + overflow-x: auto; + scrollbar-width: thin; +} +.tv-ind-tab-quick-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 22px; + padding: 0 7px; + background: var(--bg3); + color: var(--text2); + border: 1px solid var(--border); + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + flex-shrink: 0; + transition: background 0.1s, color 0.1s, border-color 0.1s; +} +.tv-ind-tab-quick-btn:hover { + background: color-mix(in srgb, var(--accent) 12%, var(--bg3)); + color: var(--accent); + border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); +} +.tv-ind-tab-quick-label { + max-width: 72px; + overflow: hidden; + text-overflow: ellipsis; +} + /* 시간대 선택 (하단 바) */ .tz-picker { position: relative; flex-shrink: 0; } .tz-picker-trigger { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4b20056..4eb7906 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2510,6 +2510,8 @@ function App() { onAutoScale={() => managerRef.current?.autoScale()} onFitContent={() => managerRef.current?.fitContent()} onToggleGrid={() => setShowGrid(v => !v)} + onAddIndicators={handleAddIndicators} + onAddIndicator={handleAddIndicator} /> {/* chart-screen */} diff --git a/frontend/src/components/BottomBar.tsx b/frontend/src/components/BottomBar.tsx index 7eda005..5e3ae6a 100644 --- a/frontend/src/components/BottomBar.tsx +++ b/frontend/src/components/BottomBar.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type { Timeframe } from '../types'; import TimezonePicker from './TimezonePicker'; +import IndicatorTabQuickButtons from './IndicatorTabQuickButtons'; interface BottomBarProps { timeframe: Timeframe; @@ -16,6 +17,8 @@ interface BottomBarProps { onAutoScale: () => void; onFitContent: () => void; onToggleGrid: () => void; + onAddIndicators?: (types: string[]) => void; + onAddIndicator?: (type: string) => void; } const PERIODS: { label: string; tf: Timeframe }[] = [ @@ -55,6 +58,7 @@ const BottomBar: React.FC = ({ timeframe, logScale, percentScale, showGrid, lastTime, displayTimezone, onTimezoneChange, onTimeframe, onLogScale, onPercentScale, onAutoScale, onFitContent, onToggleGrid, + onAddIndicators, onAddIndicator, }) => { return (
@@ -72,6 +76,8 @@ const BottomBar: React.FC = ({ + +
( + + + + +); + +interface Props { + onAddMany?: (types: string[]) => void; + onAdd?: (type: string) => void; +} + +const IndicatorTabQuickButtons: React.FC = ({ onAddMany, onAdd }) => { + const [customTabs, setCustomTabs] = useState(() => loadCustomTabs()); + + const refreshTabs = useCallback(() => { + setCustomTabs(loadCustomTabs()); + }, []); + + useEffect(() => { + refreshTabs(); + window.addEventListener('gc-indicator-custom-tabs-changed', refreshTabs); + return () => window.removeEventListener('gc-indicator-custom-tabs-changed', refreshTabs); + }, [refreshTabs]); + + if (!onAddMany && !onAdd) return null; + + const applyMain = () => { + applyIndicatorTab({ kind: 'main' }, onAddMany, onAdd); + }; + + const applyCustom = (tab: IndicatorCustomTab) => { + applyIndicatorTab({ kind: 'custom', tab }, onAddMany, onAdd); + }; + + return ( +
+ + {customTabs.map(tab => ( + + ))} +
+ ); +}; + +export default IndicatorTabQuickButtons; diff --git a/frontend/src/components/Toolbar.tsx b/frontend/src/components/Toolbar.tsx index 0a8fdb1..abf66ac 100644 --- a/frontend/src/components/Toolbar.tsx +++ b/frontend/src/components/Toolbar.tsx @@ -10,6 +10,9 @@ import { parseCustomTabKey, type IndicatorTabKey, } from '../utils/indicatorMainTab'; +import { + confirmAndApplyIndicatorTab, +} from '../utils/indicatorTabApply'; import { loadCustomTabs, createCustomTab, @@ -379,19 +382,12 @@ const IndDropdown: React.FC = ({ const handleAddAllInTab = () => { if (!addAllContext || addAllContext.types.length === 0) return; - const validTypes = addAllContext.types.filter( - type => INDICATOR_REGISTRY.some(d => d.type === type), - ); - if (validTypes.length === 0) { - window.alert('추가할 지표가 없습니다.'); + if (category === 'Main') { + confirmAndApplyIndicatorTab({ kind: 'main' }, onAddMany, onAdd); return; } - const msg = `기존 보조지표를 모두 제거하고 ${addAllContext.label} 탭의 지표 ${validTypes.length}개로 교체하시겠습니까?`; - if (!window.confirm(msg)) return; - if (onAddMany) { - onAddMany(validTypes); - } else { - validTypes.forEach(type => onAdd(type)); + if (isCustomTabKey(category) && selectedCustomTab) { + confirmAndApplyIndicatorTab({ kind: 'custom', tab: selectedCustomTab }, onAddMany, onAdd); } }; diff --git a/frontend/src/utils/indicatorCustomTabsStorage.ts b/frontend/src/utils/indicatorCustomTabsStorage.ts index d74b1dd..3edad03 100644 --- a/frontend/src/utils/indicatorCustomTabsStorage.ts +++ b/frontend/src/utils/indicatorCustomTabsStorage.ts @@ -34,6 +34,7 @@ export function loadCustomTabs(): IndicatorCustomTab[] { export function saveCustomTabs(tabs: IndicatorCustomTab[]): void { localStorage.setItem(STORAGE_KEY, JSON.stringify(tabs)); + window.dispatchEvent(new CustomEvent('gc-indicator-custom-tabs-changed')); } export function createCustomTab(name: string, indicatorTypes: string[]): IndicatorCustomTab { diff --git a/frontend/src/utils/indicatorTabApply.ts b/frontend/src/utils/indicatorTabApply.ts new file mode 100644 index 0000000..f549e6f --- /dev/null +++ b/frontend/src/utils/indicatorTabApply.ts @@ -0,0 +1,56 @@ +import { INDICATOR_REGISTRY } from './indicatorRegistry'; +import { MAIN_INDICATOR_TYPES } from './indicatorMainTab'; +import type { IndicatorCustomTab } from './indicatorCustomTabsStorage'; + +export type IndicatorTabApplySource = + | { kind: 'main' } + | { kind: 'custom'; tab: IndicatorCustomTab }; + +export function resolveIndicatorTabApplyTypes(source: IndicatorTabApplySource): string[] { + if (source.kind === 'main') { + return [...MAIN_INDICATOR_TYPES].filter( + type => INDICATOR_REGISTRY.some(d => d.type === type), + ); + } + return source.tab.indicatorTypes.filter( + type => INDICATOR_REGISTRY.some(d => d.type === type), + ); +} + +export function resolveIndicatorTabApplyLabel(source: IndicatorTabApplySource): string { + if (source.kind === 'main') return '주요지표'; + return source.tab.name; +} + +/** 지표 탭 전체 적용 — 기존 보조지표 제거 후 탭 지표만 추가 (확인 없음) */ +export function applyIndicatorTab( + source: IndicatorTabApplySource, + onAddMany: ((types: string[]) => void) | undefined, + onAdd?: (type: string) => void, +): boolean { + const validTypes = resolveIndicatorTabApplyTypes(source); + if (validTypes.length === 0) return false; + if (onAddMany) { + onAddMany(validTypes); + } else { + validTypes.forEach(type => onAdd?.(type)); + } + return true; +} + +/** 지표 탭 전체 적용 — 확인 후 적용 (지표 추가 팝업용) */ +export function confirmAndApplyIndicatorTab( + source: IndicatorTabApplySource, + onAddMany: ((types: string[]) => void) | undefined, + onAdd?: (type: string) => void, +): void { + const validTypes = resolveIndicatorTabApplyTypes(source); + if (validTypes.length === 0) { + window.alert('추가할 지표가 없습니다.'); + return; + } + const label = resolveIndicatorTabApplyLabel(source); + const msg = `기존 보조지표를 모두 제거하고 ${label} 탭의 지표 ${validTypes.length}개로 교체하시겠습니까?`; + if (!window.confirm(msg)) return; + applyIndicatorTab(source, onAddMany, onAdd); +}