보조지표 전체보기 후 복귀버튼 추가

This commit is contained in:
Macbook
2026-06-02 23:44:37 +09:00
parent c6976a2e03
commit 08fb442d4a
5 changed files with 206 additions and 2 deletions
+20
View File
@@ -2526,6 +2526,26 @@ html.theme-blue {
opacity: 1; opacity: 1;
} }
/* 보조지표 단독 전체보기 — pane 우측 상단 복원 */
.indicator-pane-controls {
display: flex;
pointer-events: auto;
opacity: 0.88;
transition: opacity 0.15s;
}
.indicator-pane-controls:hover {
opacity: 1;
}
.chart-right-toolbar-cluster--focus-restore {
right: 0;
left: auto;
}
.chart-right-toolbar-btn--restore.active {
color: var(--accent);
border-color: rgba(122, 162, 247, 0.65);
background: rgba(122, 162, 247, 0.22);
}
/* ── PaneLegend 드래그 핸들 ─────────────────────────────────────────── */ /* ── PaneLegend 드래그 핸들 ─────────────────────────────────────────── */
.pane-legend-item { .pane-legend-item {
/* fixed position 은 인라인 style 로 지정되므로 여기선 공통 속성만 */ /* fixed position 은 인라인 style 로 지정되므로 여기선 공통 속성만 */
+30 -2
View File
@@ -6,6 +6,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
import type { IndicatorConfig } from '../types'; import type { IndicatorConfig } from '../types';
import { ChartManager } from '../utils/ChartManager'; import { ChartManager } from '../utils/ChartManager';
import { getPaneHostId } from '../utils/indicatorPaneMerge'; import { getPaneHostId } from '../utils/indicatorPaneMerge';
import { isIndicatorPaneFocused, resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout'; import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
import { PAPER_OVERLAY_MENU_TITLE } from '../utils/paperChartOverlayVisibility'; import { PAPER_OVERLAY_MENU_TITLE } from '../utils/paperChartOverlayVisibility';
@@ -154,8 +155,8 @@ function buildMenuItems(
const itemHostId = getPaneHostId(item.id, indicators); const itemHostId = getPaneHostId(item.id, indicators);
const isMergedPane = samePaneItems.length > 1; const isMergedPane = samePaneItems.length > 1;
const showSplit = isMergedPane && item.id === itemHostId && !!opts.onSplit; const showSplit = isMergedPane && item.id === itemHostId && !!opts.onSplit;
const isFocused = opts.focusedId === item.id; const isFocused = isIndicatorPaneFocused(item.id, opts.focusedId, indicators);
const showExpand = !!(opts.onExpand || (isFocused && opts.onRestore)); const showExpand = !!(opts.onExpand || (opts.focusedId && opts.onRestore));
const done = opts.onDone ?? (() => {}); const done = opts.onDone ?? (() => {});
const cfg = indicators.find(ind => ind.id === item.id); const cfg = indicators.find(ind => ind.id === item.id);
const isHidden = cfg?.hidden === true; const isHidden = cfg?.hidden === true;
@@ -302,6 +303,17 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
const showOverlayMenu = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay); const showOverlayMenu = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
const overlayMenuOpen = openMenuId === CANDLE_OVERLAY_MENU_ID; const overlayMenuOpen = openMenuId === CANDLE_OVERLAY_MENU_ID;
const focusedLayoutId = focusedId
? resolveFocusedPaneLayoutId(focusedId, indicators)
: null;
const focusedPaneLayout = focusedLayoutId
? (() => {
const item = paneItems.find(p => isIndicatorPaneFocused(p.id, focusedId, indicators));
if (item) return resolvePaneItemLayout(manager, item);
return manager.getIndicatorPaneScreenLayout(focusedLayoutId);
})()
: null;
const overlayMenuItems: MenuItemDef[] = showOverlayMenu const overlayMenuItems: MenuItemDef[] = showOverlayMenu
? candleOverlayToggles!.map(item => ({ ? candleOverlayToggles!.map(item => ({
key: item.key, key: item.key,
@@ -320,6 +332,22 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
className="chart-right-toolbar-inner" className="chart-right-toolbar-inner"
style={{ height: Math.max(innerHeight, chartHeight) }} style={{ height: Math.max(innerHeight, chartHeight) }}
> >
{focusedId && onRestore && focusedPaneLayout && (
<div
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--focus-restore"
style={{ top: focusedPaneLayout.topY + 4 }}
>
<button
type="button"
className="tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--restore active"
title="기본 보기로 복원 (모든 보조지표 표시)"
onClick={e => { e.stopPropagation(); onRestore(); }}
>
<IconRestore />
</button>
</div>
)}
{showOverlayMenu && ( {showOverlayMenu && (
<div <div
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay" className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay"
@@ -0,0 +1,87 @@
/**
* 보조지표 단독 전체보기 모드 — 해당 pane 우측 상단 «기본 보기로 복원» 버튼
*/
import React, { useState, useEffect, useCallback } from 'react';
import type { IndicatorConfig } from '../types';
import { ChartManager } from '../utils/ChartManager';
import { resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
const IconRestore = () => (
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="5,13 1,13 1,9"/>
<line x1="1" y1="13" x2="5.5" y2="8.5"/>
<polyline points="9,1 13,1 13,5"/>
<line x1="13" y1="1" x2="8.5" y2="5.5"/>
</svg>
);
export interface IndicatorPaneControlsProps {
manager: ChartManager;
containerEl: HTMLElement;
indicators: IndicatorConfig[];
focusedId: string | null;
onRestore: () => void;
}
const IndicatorPaneControls: React.FC<IndicatorPaneControlsProps> = ({
manager,
containerEl,
indicators,
focusedId,
onRestore,
}) => {
const [pos, setPos] = useState<{ left: number; top: number } | null>(null);
const updatePos = useCallback(() => {
if (!focusedId) {
setPos(null);
return;
}
const layoutId = resolveFocusedPaneLayoutId(focusedId, indicators);
const lay = manager.getIndicatorPaneScreenLayout(layoutId);
if (!lay) {
setPos(null);
return;
}
const rect = containerEl.getBoundingClientRect();
setPos({
left: rect.left + rect.width - 40,
top: rect.top + lay.topY + 4,
});
}, [manager, containerEl, focusedId, indicators]);
useEffect(() => {
updatePos();
const ro = new ResizeObserver(() => updatePos());
ro.observe(containerEl);
const unsub = manager.subscribePaneLayout(() => updatePos());
const id = setInterval(updatePos, 400);
return () => {
ro.disconnect();
unsub();
clearInterval(id);
};
}, [containerEl, manager, updatePos, focusedId]);
if (!focusedId || !pos) return null;
return (
<div
className="indicator-pane-controls"
style={{ position: 'fixed', left: pos.left, top: pos.top, zIndex: 505 }}
onMouseDown={e => e.stopPropagation()}
>
<button
type="button"
className="pane-btn pane-btn-expand active"
onClick={e => { e.stopPropagation(); onRestore(); }}
title="기본 보기로 복원 (모든 보조지표 표시)"
>
<IconRestore />
</button>
</div>
);
};
export default IndicatorPaneControls;
+37
View File
@@ -56,6 +56,7 @@ import ChartHoverToolbar from './ChartHoverToolbar';
import ChartMagnifier from './ChartMagnifier'; import ChartMagnifier from './ChartMagnifier';
import ChartContextMenu from './ChartContextMenu'; import ChartContextMenu from './ChartContextMenu';
import CandlePaneControls from './CandlePaneControls'; import CandlePaneControls from './CandlePaneControls';
import IndicatorPaneControls from './IndicatorPaneControls';
import { getKoreanName } from '../utils/marketNameCache'; import { getKoreanName } from '../utils/marketNameCache';
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone'; import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync'; import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
@@ -1667,6 +1668,15 @@ const TradingChart: React.FC<TradingChartProps> = ({
onRestore={() => setCandleOnlyMode(false)} onRestore={() => setCandleOnlyMode(false)}
/> />
)} )}
{chartMgr && focusedIndicatorId && onRestoreIndicators && chartPaintReady && (
<IndicatorPaneControlsPortal
manager={chartMgr}
indicators={indicators}
getContainer={() => containerRef.current}
focusedId={focusedIndicatorId}
onRestore={onRestoreIndicators}
/>
)}
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && ( {chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
<PaneLegendPortal <PaneLegendPortal
key={indKey(indicators)} key={indKey(indicators)}
@@ -1723,6 +1733,33 @@ const CandlePaneTimeAxisPortal: React.FC<{
return <CandlePaneTimeAxis manager={manager} containerEl={el} />; return <CandlePaneTimeAxis manager={manager} containerEl={el} />;
}; };
const IndicatorPaneControlsPortal: React.FC<{
manager: ChartManager;
indicators: IndicatorConfig[];
getContainer: () => HTMLElement | null;
focusedId: string;
onRestore: () => void;
}> = ({ manager, indicators, getContainer, focusedId, onRestore }) => {
const [el, setEl] = useState<HTMLElement | null>(null);
useEffect(() => {
const c = getContainer();
if (c) { setEl(c); return; }
const tid = setTimeout(() => setEl(getContainer()), 100);
return () => clearTimeout(tid);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!el) return null;
return (
<IndicatorPaneControls
manager={manager}
containerEl={el}
indicators={indicators}
focusedId={focusedId}
onRestore={onRestore}
/>
);
};
const CandlePaneControlsPortal: React.FC<{ const CandlePaneControlsPortal: React.FC<{
manager: ChartManager; manager: ChartManager;
getContainer: () => HTMLElement | null; getContainer: () => HTMLElement | null;
+32
View File
@@ -0,0 +1,32 @@
import type { IndicatorConfig } from '../types';
import { getPaneHostId } from './indicatorPaneMerge';
/** 단독 전체보기(focusedId) 대상 pane 항목인지 — 호스트·병합 멤버 id 모두 매칭 */
export function isIndicatorPaneFocused(
itemId: string,
focusedId: string | null | undefined,
indicators: IndicatorConfig[],
): boolean {
if (!focusedId) return false;
if (itemId === focusedId) return true;
const itemCfg = indicators.find(i => i.id === itemId);
const focusCfg = indicators.find(i => i.id === focusedId);
if (itemCfg?.mergedWith === focusedId) return true;
if (focusCfg?.mergedWith === itemId) return true;
if (focusCfg?.mergedWith && itemCfg?.mergedWith === focusCfg.mergedWith) return true;
const itemHost = getPaneHostId(itemId, indicators);
const focusHost = getPaneHostId(focusedId, indicators);
return itemHost === focusedId || itemHost === focusHost || itemId === focusHost;
}
/** focusedId 에 해당하는 pane 레이아웃 조회용 id (병합 시 호스트 우선) */
export function resolveFocusedPaneLayoutId(
focusedId: string,
indicators: IndicatorConfig[],
): string {
const cfg = indicators.find(i => i.id === focusedId);
if (cfg?.mergedWith) return cfg.mergedWith;
return getPaneHostId(focusedId, indicators);
}