매매 시그널 알림 우측패널 동작 수정

This commit is contained in:
Macbook
2026-05-29 01:38:58 +09:00
parent 094a285363
commit 3db7e85e2c
7 changed files with 241 additions and 88 deletions
+12 -4
View File
@@ -11502,15 +11502,23 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
overflow: hidden;
}
.tnl-page--with-right .tnl-right {
flex: 0 0 0;
width: 0;
min-width: 0;
display: flex;
flex-direction: column;
border-left: none;
background: var(--bg2);
overflow: hidden;
transition: width 0.22s cubic-bezier(0.4, 0, 0.2, 1), flex-basis 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
.tnl-page--with-right .tnl-side-wrap--right.tnl-side-wrap--open .tnl-right.tnl-right--open {
flex: 0 0 min(380px, 38vw);
width: min(380px, 38vw);
min-width: 300px;
max-width: 420px;
display: flex;
flex-direction: column;
border-left: 1px solid var(--border);
background: var(--bg2);
overflow: hidden;
}
.tnl-right-tabs {
flex-shrink: 0;
@@ -13,8 +13,10 @@ import PaperSplitPanel from './paper/PaperSplitPanel';
import { useUpbitOrderbook } from '../hooks/useUpbitOrderbook';
import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
import { useAppSettings } from '../hooks/useAppSettings';
import { readStoredBool, storeBool } from './strategyEditor/usePanelResize';
import '../styles/strategyEditorTheme.css';
import '../styles/virtualTradingDashboard.css';
import '../styles/builderPageShell.css';
import '../styles/tradeNotificationList.css';
import TradeNotificationListRow from './tradeNotification/TradeNotificationListRow';
import TradeNotificationGridLayoutPicker from './tradeNotification/TradeNotificationGridLayoutPicker';
@@ -32,6 +34,8 @@ import {
type RightTab = 'trade' | 'orderbook';
const TNL_RIGHT_OPEN_KEY = 'tnl-right-open';
const IcTrash = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<polyline points="3 6 5 6 21 6" />
@@ -106,6 +110,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
const [summary, setSummary] = useState<PaperSummaryDto | null>(null);
const orderAnchorRef = useRef<HTMLDivElement>(null);
const [refreshing, setRefreshing] = useState(false);
const [rightOpen, setRightOpen] = useState(() => readStoredBool(TNL_RIGHT_OPEN_KEY, true));
const { settings: appSettings } = useAppSettings();
const chartLiveReceiveHighlight = appSettings.chartLiveReceiveHighlight ?? true;
@@ -244,11 +249,30 @@ export const TradeNotificationListPage: React.FC<Props> = ({
saveTradeNotificationGridPreset(preset);
}, []);
const toggleRightPanel = useCallback(() => {
setRightOpen(prev => {
const next = !prev;
storeBool(TNL_RIGHT_OPEN_KEY, next);
return next;
});
}, []);
const isListView = listLayout === 'list';
const gridCols = getTradeNotificationGridPreset(gridPreset).cols;
return (
<div className={`tnl-page tnl-page--with-right bps-page--vtd se-page se-page--${theme} app ${theme}`}>
<div
className={[
'tnl-page',
'tnl-page--with-right',
'bps-page--vtd',
'se-page',
`se-page--${theme}`,
'app',
theme,
rightOpen ? '' : 'tnl-page--right-collapsed',
].filter(Boolean).join(' ')}
>
<div className="tnl-body">
<div className="tnl-main" style={{ containerType: 'inline-size', containerName: 'tnl-main' }}>
<div className="tnl-header">
@@ -375,7 +399,38 @@ export const TradeNotificationListPage: React.FC<Props> = ({
</div>
</div>
<aside className="tnl-right" aria-label="매매·호가">
<div className={`tnl-side-wrap tnl-side-wrap--right${rightOpen ? ' tnl-side-wrap--open' : ''}`}>
<button
type="button"
className={`bps-panel-handle bps-panel-handle--right${rightOpen ? ' bps-panel-handle--open' : ''}`}
onClick={toggleRightPanel}
title={rightOpen ? '매매·호가 패널 닫기' : '매매·호가 패널 열기'}
aria-expanded={rightOpen}
aria-label={rightOpen ? '매매·호가 패널 닫기' : '매매·호가 패널 열기'}
>
<svg
width="8"
height="14"
viewBox="0 0 8 14"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
{rightOpen ? (
<polyline points="2,1 6,7 2,13" />
) : (
<polyline points="6,1 2,7 6,13" />
)}
</svg>
</button>
<aside
className={`tnl-right${rightOpen ? ' tnl-right--open' : ''}`}
aria-label="매매·호가"
aria-hidden={!rightOpen}
>
<div className="bps-right-tabs tnl-right-tabs">
<button
type="button"
@@ -462,6 +517,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
</aside>
</div>
</div>
</div>
);
};
+4 -1
View File
@@ -135,6 +135,8 @@ interface TradingChartProps {
paneSeparatorOptions?: ChartPaneSeparatorOptions;
/** true: pane 합산 높이로 chart-container 를 키우지 않고 래퍼 높이에 맞춤 (미니 차트·알림 목록) */
paneLayoutClamp?: boolean;
/** 보조지표 pane 레전드(이름·값 오버레이) 표시 (기본 true, 알림 미니차트는 false) */
showPaneLegend?: boolean;
}
const TradingChart: React.FC<TradingChartProps> = ({
@@ -167,6 +169,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
chartVisible = true,
paneSeparatorOptions,
paneLayoutClamp = false,
showPaneLegend = true,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
@@ -1396,7 +1399,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
onRestore={() => setCandleOnlyMode(false)}
/>
)}
{chartMgr && !candleOnlyMode && (
{chartMgr && !candleOnlyMode && showPaneLegend && (
<PaneLegendPortal
manager={chartMgr}
indicators={indicators}
@@ -22,12 +22,17 @@ export interface TradeNotificationHScrollPaneProps {
/** 스크린리더용 영역 설명 */
label: string;
children: React.ReactNode;
/** 스크롤 시 뷰포트에 완전히 보이지 않는 차트 카드 라벨 숨김 */
clipPeekLabels?: boolean;
}
const PEEK_LABEL_THRESHOLD = 0.99;
const TradeNotificationHScrollPane: React.FC<TradeNotificationHScrollPaneProps> = ({
className = '',
label,
children,
clipPeekLabels = false,
}) => {
const viewportRef = useRef<HTMLDivElement>(null);
const [canScroll, setCanScroll] = useState(false);
@@ -46,6 +51,31 @@ const TradeNotificationHScrollPane: React.FC<TradeNotificationHScrollPaneProps>
setAtEnd(!overflow || el.scrollLeft >= max - 2);
}, []);
const bindPeekLabelClip = useCallback(() => {
const viewport = viewportRef.current;
if (!viewport || !clipPeekLabels) return () => {};
const applyPeek = (card: HTMLElement, ratio: number) => {
card.classList.toggle('tnl-chart-card--peek', ratio < PEEK_LABEL_THRESHOLD);
};
const cards = Array.from(viewport.querySelectorAll<HTMLElement>('.tnl-chart-card'));
if (cards.length === 0) return () => {};
const io = new IntersectionObserver(
entries => {
entries.forEach(entry => applyPeek(entry.target as HTMLElement, entry.intersectionRatio));
},
{ root: viewport, threshold: [0, 0.5, 0.99, 1] },
);
cards.forEach(card => io.observe(card));
return () => {
io.disconnect();
cards.forEach(card => card.classList.remove('tnl-chart-card--peek'));
};
}, [clipPeekLabels]);
useEffect(() => {
const el = viewportRef.current;
if (!el) return;
@@ -60,6 +90,23 @@ const TradeNotificationHScrollPane: React.FC<TradeNotificationHScrollPaneProps>
};
}, [syncScrollState, children]);
useEffect(() => {
if (!clipPeekLabels) return;
let cleanup = bindPeekLabelClip();
const viewport = viewportRef.current;
if (!viewport) return cleanup;
const mo = new MutationObserver(() => {
cleanup();
cleanup = bindPeekLabelClip();
});
mo.observe(viewport, { childList: true, subtree: true });
return () => {
mo.disconnect();
cleanup();
};
}, [clipPeekLabels, bindPeekLabelClip, children]);
const scrollByStep = useCallback((dir: -1 | 1) => {
const el = viewportRef.current;
if (!el) return;
@@ -399,6 +399,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
indicatorScrollNeeded ? 'tnl-row-gallery-indicators--scroll' : '',
].filter(Boolean).join(' ')}
label="보조지표"
clipPeekLabels={indicatorScrollNeeded}
>
<div className="tnl-charts-track tnl-charts-track--indicators-only">
{indicatorCards.map(card => (
@@ -225,6 +225,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
showHoverToolbar={false}
seriesPriceLabelsEnabled={false}
paneLayoutClamp
showPaneLegend={false}
/>
</div>
);
@@ -148,6 +148,7 @@
background: color-mix(in srgb, var(--bg) 40%, var(--bg2));
overflow: hidden;
box-sizing: border-box;
isolation: isolate;
}
.tnl-hscroll-pane__viewport {
@@ -217,6 +218,8 @@
}
.tnl-hscroll-pane__rail {
position: relative;
z-index: 4;
flex: 0 0 32px;
width: 32px;
display: flex;
@@ -789,6 +792,12 @@
font-weight: 700;
}
/* 보조지표 스크롤 — 뷰포트에 완전히 들어오지 않은 카드는 라벨 숨김 */
.tnl-chart-card--peek .tnl-chart-card-head {
visibility: hidden;
pointer-events: none;
}
.tnl-chart-card-meta {
font-size: 10px;
color: var(--text3);
@@ -907,6 +916,34 @@
gap: 12px;
}
/* 우측 매매·호가 패널 접기/펼치기 */
.tnl-side-wrap--right {
display: flex;
flex-direction: row;
align-items: stretch;
flex-shrink: 0;
min-height: 0;
height: 100%;
position: relative;
z-index: 8;
}
.tnl-page--right-collapsed .tnl-main {
flex: 1;
min-width: 0;
}
.tnl-side-wrap--right .tnl-right-tabs,
.tnl-side-wrap--right .tnl-right-body {
min-width: min(380px, 38vw);
}
.tnl-side-wrap--right:not(.tnl-side-wrap--open) .tnl-right-tabs,
.tnl-side-wrap--right:not(.tnl-side-wrap--open) .tnl-right-body {
visibility: hidden;
pointer-events: none;
}
/* 툴바 — 좌: 필터·액션 · 우: 목록/그리드·배치·삭제 */
.tnl-page--with-right .tnl-toolbar {
display: flex;