캔들차트, 이동평균선, 일목균형표, 볼린저밴드 보이기 숨기기 기능적용
This commit is contained in:
@@ -8,13 +8,10 @@ import { ChartManager } from '../utils/ChartManager';
|
||||
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
||||
import { isIndicatorPaneFocused, resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
|
||||
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
|
||||
import { PAPER_OVERLAY_MENU_TITLE } from '../utils/paperChartOverlayVisibility';
|
||||
|
||||
/** 캔들 오버레이 표시 설정 그룹 메뉴 id */
|
||||
export const CANDLE_OVERLAY_MENU_ID = '__candle_overlay_visibility__';
|
||||
import type { ChartOverlayToggleKey } from '../utils/chartOverlayVisibility';
|
||||
|
||||
export interface CandleOverlayToggleItem {
|
||||
key: string;
|
||||
key: ChartOverlayToggleKey;
|
||||
label: string;
|
||||
visible: boolean;
|
||||
}
|
||||
@@ -26,7 +23,7 @@ export interface ChartRightToolbarProps {
|
||||
paused?: boolean;
|
||||
/** 캔들 pane 상단 — 이동평균·일목·볼린저 표시/숨김 (투자관리 차트 탭) */
|
||||
candleOverlayToggles?: CandleOverlayToggleItem[];
|
||||
onToggleCandleOverlay?: (key: string) => void;
|
||||
onToggleCandleOverlay?: (key: ChartOverlayToggleKey) => void;
|
||||
onSplit?: (hostId: string) => void;
|
||||
onRemove?: (id: string) => void;
|
||||
onDuplicate?: (id: string) => void;
|
||||
@@ -107,6 +104,49 @@ const IconMore = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconMa = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||
stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
|
||||
<path d="M1 10 L5 4 L8 7 L13 2"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconBollinger = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||
stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
|
||||
<path d="M1 4 Q7 1 13 4"/>
|
||||
<path d="M1 7 Q7 4 13 7"/>
|
||||
<path d="M1 10 Q7 13 13 10"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconCandle = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||
stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
|
||||
<line x1="4" y1="2" x2="4" y2="12"/>
|
||||
<rect x="2.5" y="5" width="3" height="4" rx="0.5" fill="currentColor" stroke="none"/>
|
||||
<line x1="10" y1="3" x2="10" y2="11"/>
|
||||
<rect x="8.5" y="4" width="3" height="5" rx="0.5" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconIchimoku = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
|
||||
stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M1 9 L7 5 L13 9" fill="rgba(239,83,80,0.25)"/>
|
||||
<path d="M1 5 L7 9 L13 5" fill="rgba(38,166,154,0.25)"/>
|
||||
<path d="M1 9 L7 5 L13 9"/>
|
||||
<path d="M1 5 L7 9 L13 5"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const OVERLAY_ICONS: Record<ChartOverlayToggleKey, React.ReactNode> = {
|
||||
ma: <IconMa />,
|
||||
bollinger: <IconBollinger />,
|
||||
ichimoku: <IconIchimoku />,
|
||||
candle: <IconCandle />,
|
||||
};
|
||||
|
||||
const IconEye = ({ hidden }: { hidden?: boolean }) => hidden ? (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M1 7 Q4 2.5 7 2.5 Q10 2.5 13 7 Q10 11.5 7 11.5 Q4 11.5 1 7 Z" strokeOpacity="0.35"/>
|
||||
@@ -295,13 +335,12 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
const allowDrag = !!(onDragStart && paneItems.length > 1);
|
||||
const clusterHeight = (allowDrag ? BTN_SIZE + BTN_GAP : 0) + BTN_SIZE;
|
||||
|
||||
const openItem = openMenuId && openMenuId !== CANDLE_OVERLAY_MENU_ID
|
||||
const openItem = openMenuId
|
||||
? paneItems.find(p => p.id === openMenuId)
|
||||
: null;
|
||||
|
||||
const candlePane = paneLayouts.find(l => l.paneIndex === 0);
|
||||
const showOverlayMenu = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
||||
const overlayMenuOpen = openMenuId === CANDLE_OVERLAY_MENU_ID;
|
||||
const showOverlayIcons = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
||||
|
||||
const focusedLayoutId = focusedId
|
||||
? resolveFocusedPaneLayoutId(focusedId, indicators)
|
||||
@@ -314,18 +353,6 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
})()
|
||||
: null;
|
||||
|
||||
const overlayMenuItems: MenuItemDef[] = showOverlayMenu
|
||||
? candleOverlayToggles!.map(item => ({
|
||||
key: item.key,
|
||||
title: item.visible ? `${item.label} 숨기기` : `${item.label} 표시`,
|
||||
icon: <IconEye hidden={!item.visible} />,
|
||||
active: !item.visible,
|
||||
onClick: () => {
|
||||
onToggleCandleOverlay?.(item.key);
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<aside className="chart-right-toolbar" aria-label="차트 우측 도구">
|
||||
<div
|
||||
@@ -348,21 +375,26 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showOverlayMenu && (
|
||||
{showOverlayIcons && (
|
||||
<div
|
||||
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay"
|
||||
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay-icons"
|
||||
style={{ top: candlePane!.topY + 4 }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`tv-side-btn tv-side-group-btn chart-right-toolbar-btn chart-right-toolbar-btn--menu${overlayMenuOpen ? ' active' : ''}`}
|
||||
title="캔들 지표 표시 설정"
|
||||
onMouseEnter={e => openFlyout(CANDLE_OVERLAY_MENU_ID, e.currentTarget)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
<IconMore />
|
||||
<span className="tv-side-expand-arrow" />
|
||||
</button>
|
||||
{candleOverlayToggles!.map((item, idx) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={`tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--overlay${item.visible ? '' : ' chart-right-toolbar-btn--overlay-off'}`}
|
||||
title={item.visible ? `${item.label} 숨기기` : `${item.label} 표시`}
|
||||
style={{ marginTop: idx > 0 ? BTN_GAP : 0 }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleCandleOverlay?.(item.key);
|
||||
}}
|
||||
>
|
||||
{OVERLAY_ICONS[item.key]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -425,28 +457,6 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{overlayMenuOpen && overlayMenuItems.length > 0 && (
|
||||
<div
|
||||
className="tv-side-flyout tv-side-flyout--right-anchor"
|
||||
style={{ top: flyoutPos.top, left: flyoutPos.left }}
|
||||
onMouseEnter={cancelClose}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
<div className="tv-side-flyout-header">{PAPER_OVERLAY_MENU_TITLE}</div>
|
||||
{overlayMenuItems.map(mi => (
|
||||
<button
|
||||
key={mi.key}
|
||||
type="button"
|
||||
className={`tv-side-flyout-item${mi.active ? ' active' : ''}`}
|
||||
onClick={e => { e.stopPropagation(); mi.onClick(); }}
|
||||
>
|
||||
<span className="tv-side-flyout-icon">{mi.icon}</span>
|
||||
<span className="tv-side-flyout-title">{mi.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openItem && openMenuId && (() => {
|
||||
const menuItems = buildMenuItems(openItem, indicators, paneItems, {
|
||||
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
|
||||
|
||||
@@ -44,6 +44,13 @@ import {
|
||||
replaceIndicatorConfigsFromTypes,
|
||||
} from '../utils/indicatorPaneMerge';
|
||||
import { sortIndicatorsByTypeOrder } from '../utils/indicatorListOrder';
|
||||
import {
|
||||
type ChartOverlayToggleKey,
|
||||
type ChartOverlayVisibility,
|
||||
chartOverlayKeyForType,
|
||||
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||
listChartOverlayToggleItems,
|
||||
} from '../utils/chartOverlayVisibility';
|
||||
import { saveSlot } from '../utils/backendApi';
|
||||
import type {
|
||||
OHLCVBar, ChartType, Theme, Timeframe,
|
||||
@@ -192,6 +199,8 @@ export interface ChartSlotProps {
|
||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */
|
||||
chartCrosshairInfoVisible?: boolean;
|
||||
/** 차트 마우스오버 줌·이동 플로팅 툴바 */
|
||||
chartHoverToolbarVisible?: boolean;
|
||||
/** 업비트 실시간 티커 맵 (슬롯 심볼별 현재가·등락) */
|
||||
marketTickers?: Map<string, TickerData>;
|
||||
}
|
||||
@@ -220,6 +229,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
chartVisible = true,
|
||||
chartPaneSeparator,
|
||||
chartCrosshairInfoVisible = true,
|
||||
chartHoverToolbarVisible = true,
|
||||
marketTickers,
|
||||
}, ref) {
|
||||
// ── 전역 지표 파라미터 + 시각 설정 (DB 동기화) ────────────────────────
|
||||
@@ -649,6 +659,37 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
setFocusedIndicatorId(null);
|
||||
}, []);
|
||||
|
||||
const [overlayVisibility, setOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
||||
);
|
||||
const overlayVisibilityRef = useRef(overlayVisibility);
|
||||
useEffect(() => {
|
||||
overlayVisibilityRef.current = overlayVisibility;
|
||||
}, [overlayVisibility]);
|
||||
|
||||
const candleOverlayToggles = useMemo(
|
||||
() => listChartOverlayToggleItems(overlayVisibility),
|
||||
[overlayVisibility],
|
||||
);
|
||||
|
||||
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
||||
setOverlayVisibility(prev => {
|
||||
const next = { ...prev, [key]: !prev[key] };
|
||||
const mgr = managerRef.current;
|
||||
mgr?.setChartOverlayGroupVisible(key, next[key]);
|
||||
if (mgr) {
|
||||
for (const info of mgr.getIndicatorPaneInfo()) {
|
||||
if (chartOverlayKeyForType(info.type) !== key) continue;
|
||||
const reactInd = indicatorsRef.current.find(i => i.id === info.id);
|
||||
mgr.applyIndicatorStyle(reactInd
|
||||
? { ...info.config, ...reactInd, id: info.id, type: info.type }
|
||||
: info.config);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDuplicateIndicator = useCallback((sourceId: string) => {
|
||||
setIndicators(prev => duplicateIndicatorInList(sourceId, prev, newIndId) ?? prev);
|
||||
}, []);
|
||||
@@ -869,7 +910,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
logScale={false}
|
||||
drawingsLocked={compactMode}
|
||||
drawingsVisible={!compactMode}
|
||||
showHoverToolbar={!compactMode}
|
||||
showHoverToolbar={!compactMode && chartHoverToolbarVisible}
|
||||
volumeVisible={chartVolumeVisible}
|
||||
candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels}
|
||||
indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels}
|
||||
@@ -895,6 +936,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels);
|
||||
mgr.setVolumeVisible(chartVolumeVisible);
|
||||
if (chartPaneSeparator) mgr.setPaneSeparatorOptions(chartPaneSeparator);
|
||||
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||
// Time sync: 가시 시간 범위 변화 구독
|
||||
mgr.subscribeVisibleTimeRange(r => {
|
||||
if (!r || isSyncingTimeRef.current) return;
|
||||
@@ -919,6 +961,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
// 첫 마운트 시 pending 상태였던 sync range 를 재적용한다
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr) return;
|
||||
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||
if (skipSyncApplyRef.current) {
|
||||
skipSyncApplyRef.current = false;
|
||||
const fb = mgr.hasIchimoku() ? 28 : 0;
|
||||
@@ -971,6 +1014,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
onToggleIndicatorHidden={handleToggleHidden}
|
||||
onOpenIndicatorSettings={id => setSettingsModalId(id)}
|
||||
onFullView={onFullView ? () => onFullView(symbolRef.current) : undefined}
|
||||
candleOverlayToggles={compactMode ? undefined : candleOverlayToggles}
|
||||
onToggleCandleOverlay={compactMode ? undefined : handleToggleCandleOverlay}
|
||||
/>
|
||||
|
||||
{/* 컨텍스트 툴바 */}
|
||||
|
||||
@@ -125,6 +125,8 @@ interface SettingsPageProps {
|
||||
onChartLegendOptionsChange?: (patch: Partial<ChartLegendVisibility>) => void;
|
||||
chartCrosshairInfoVisible?: boolean;
|
||||
onChartCrosshairInfoVisible?: (v: boolean) => void;
|
||||
chartHoverToolbarVisible?: boolean;
|
||||
onChartHoverToolbarVisible?: (v: boolean) => void;
|
||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||
onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void;
|
||||
paperTradingEnabled?: boolean;
|
||||
@@ -783,6 +785,8 @@ interface ChartPanelProps {
|
||||
onChartLegendOptionsChange?: (patch: Partial<ChartLegendVisibility>) => void;
|
||||
chartCrosshairInfoVisible?: boolean;
|
||||
onChartCrosshairInfoVisible?: (v: boolean) => void;
|
||||
chartHoverToolbarVisible?: boolean;
|
||||
onChartHoverToolbarVisible?: (v: boolean) => void;
|
||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||
onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void;
|
||||
chartTimeFormat?: string;
|
||||
@@ -807,6 +811,8 @@ const ChartPanel: React.FC<ChartPanelProps> = ({
|
||||
onChartLegendOptionsChange,
|
||||
chartCrosshairInfoVisible = true,
|
||||
onChartCrosshairInfoVisible,
|
||||
chartHoverToolbarVisible = true,
|
||||
onChartHoverToolbarVisible,
|
||||
chartPaneSeparator,
|
||||
onChartPaneSeparatorChange,
|
||||
chartTimeFormat = 'MM-dd HH:mm',
|
||||
@@ -1054,6 +1060,22 @@ const ChartPanel: React.FC<ChartPanelProps> = ({
|
||||
</select>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection title="차트 조작 툴바">
|
||||
<SettingRow
|
||||
label="줌·이동 플로팅 툴바"
|
||||
desc="켜면 차트에 마우스를 올렸을 때 하단에 축소(−)·확대(+)·전체 보기·좌우 이동(‹ ›) 버튼이 표시됩니다. 끄면 툴바가 나타나지 않습니다."
|
||||
>
|
||||
<label className="stg-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={chartHoverToolbarVisible}
|
||||
onChange={e => onChartHoverToolbarVisible?.(e.target.checked)}
|
||||
/>
|
||||
<span className="stg-toggle-slider" />
|
||||
</label>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1714,6 +1736,8 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
onChartLegendOptionsChange,
|
||||
chartCrosshairInfoVisible = true,
|
||||
onChartCrosshairInfoVisible,
|
||||
chartHoverToolbarVisible = true,
|
||||
onChartHoverToolbarVisible,
|
||||
chartPaneSeparator,
|
||||
onChartPaneSeparatorChange,
|
||||
tradeAlertSoundEnabled = true,
|
||||
@@ -1828,6 +1852,8 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
onChartLegendOptionsChange={onChartLegendOptionsChange}
|
||||
chartCrosshairInfoVisible={chartCrosshairInfoVisible}
|
||||
onChartCrosshairInfoVisible={onChartCrosshairInfoVisible}
|
||||
chartHoverToolbarVisible={chartHoverToolbarVisible}
|
||||
onChartHoverToolbarVisible={onChartHoverToolbarVisible}
|
||||
chartPaneSeparator={chartPaneSeparator}
|
||||
onChartPaneSeparatorChange={onChartPaneSeparatorChange}
|
||||
chartTimeFormat={chartTimeFormat}
|
||||
|
||||
@@ -164,7 +164,7 @@ interface TradingChartProps {
|
||||
showCandlePaneControls?: boolean;
|
||||
/** 캔들 오버레이(이동평균·일목·볼린저) 표시 토글 — 우측 툴바 상단 */
|
||||
candleOverlayToggles?: CandleOverlayToggleItem[];
|
||||
onToggleCandleOverlay?: (key: string) => void;
|
||||
onToggleCandleOverlay?: (key: import('../utils/chartOverlayVisibility').ChartOverlayToggleKey) => void;
|
||||
}
|
||||
|
||||
const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
Reference in New Issue
Block a user