매매 시그널 디테일 팝업 기능 적용

This commit is contained in:
Macbook
2026-06-10 21:13:53 +09:00
parent bf5554d375
commit 0ff1992b64
19 changed files with 1507 additions and 58 deletions
@@ -0,0 +1,37 @@
import { useLayoutEffect, type RefObject } from 'react';
/** 스크롤 컨테이너 안에서 target 요소가 뷰포트 중앙(세로)에 오도록 scrollTop 조정 */
export function useCenterElementInScrollContainer(
scrollRef: RefObject<HTMLElement | null>,
targetRef: RefObject<HTMLElement | null>,
enabled: boolean,
deps: unknown[] = [],
): void {
useLayoutEffect(() => {
if (!enabled) return;
const scroll = scrollRef.current;
const target = targetRef.current;
if (!scroll || !target) return;
const center = () => {
const scrollRect = scroll.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const targetMidInContent =
scroll.scrollTop + (targetRect.top - scrollRect.top) + targetRect.height / 2;
const targetTop = targetMidInContent - scroll.clientHeight / 2;
scroll.scrollTop = Math.max(0, Math.min(
targetTop,
scroll.scrollHeight - scroll.clientHeight,
));
};
center();
requestAnimationFrame(center);
const ro = new ResizeObserver(() => center());
ro.observe(scroll);
ro.observe(target);
return () => ro.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled, scrollRef, targetRef, ...deps]);
}
@@ -0,0 +1,81 @@
import { useCallback, useEffect, useRef } from 'react';
import type { ChartManager } from '../utils/ChartManager';
type LogicalRange = { from: number; to: number };
/** 분리된 TradingChart 인스턴스 — 가로 시간축(visible logical range) 동기화 */
export function useLinkedChartLogicalRange(chartCount = 2) {
const managersRef = useRef<(ChartManager | null)[]>(Array(chartCount).fill(null));
const unsubsRef = useRef<Array<Array<() => void>>>(
Array.from({ length: chartCount }, () => []),
);
const isSyncingRef = useRef(false);
const clearUnsubs = useCallback((index: number) => {
for (const u of unsubsRef.current[index] ?? []) {
try { u(); } catch { /* ok */ }
}
unsubsRef.current[index] = [];
}, []);
const syncPeers = useCallback((sourceIndex: number, range: LogicalRange) => {
if (isSyncingRef.current) return;
isSyncingRef.current = true;
for (let i = 0; i < managersRef.current.length; i++) {
if (i === sourceIndex) continue;
const peer = managersRef.current[i];
if (!peer?.hasMainSeries() || peer.getRawBarsLength() < 2) continue;
peer.applyVisibleLogicalRange(range.from, range.to);
}
requestAnimationFrame(() => { isSyncingRef.current = false; });
}, []);
const attachRangeSync = useCallback((index: number, mgr: ChartManager) => {
clearUnsubs(index);
const onRange = (r: LogicalRange | null) => {
if (!r || isSyncingRef.current) return;
syncPeers(index, r);
};
unsubsRef.current[index].push(mgr.subscribeVisibleLogicalRange(onRange));
// 커스텀 패닝 등 — LWC logical 이벤트가 누락될 수 있어 viewport 알림에도 연결
unsubsRef.current[index].push(mgr.subscribeViewport(() => {
if (isSyncingRef.current) return;
const r = mgr.getVisibleLogicalRange();
if (r) syncPeers(index, r);
}));
}, [clearUnsubs, syncPeers]);
const registerManager = useCallback((index: number, mgr: ChartManager | null) => {
if (index < 0 || index >= managersRef.current.length) return;
clearUnsubs(index);
managersRef.current[index] = mgr;
if (!mgr) return;
attachRangeSync(index, mgr);
// 후행 차트 마운트 시 — 이미 준비된 peer 시간축에 맞춤
for (let i = 0; i < managersRef.current.length; i++) {
if (i === index) continue;
const peer = managersRef.current[i];
if (!peer?.hasMainSeries() || peer.getRawBarsLength() < 2) continue;
const peerRange = peer.getVisibleLogicalRange();
if (!peerRange) continue;
isSyncingRef.current = true;
mgr.applyVisibleLogicalRange(peerRange.from, peerRange.to);
requestAnimationFrame(() => { isSyncingRef.current = false; });
return;
}
}, [attachRangeSync, clearUnsubs]);
useEffect(() => () => {
for (let i = 0; i < unsubsRef.current.length; i++) {
clearUnsubs(i);
}
}, [clearUnsubs]);
return { registerManager };
}