알림목록 화면오류 수정

This commit is contained in:
Macbook
2026-05-30 01:28:58 +09:00
parent ba6695eb9c
commit 0e46104106
5 changed files with 116 additions and 57 deletions
+11 -1
View File
@@ -358,6 +358,13 @@ const TradingChart: React.FC<TradingChartProps> = ({
required = mgr.resetPaneHeights(wrapperH); required = mgr.resetPaneHeights(wrapperH);
} }
if (paneLayoutClamp) {
try {
const w = wrapper.clientWidth;
if (w > 0 && wrapperH > 0) mgr.resize(w, wrapperH);
} catch { /* ok */ }
}
if (!paneLayoutClamp && required > wrapperH + 4) { if (!paneLayoutClamp && required > wrapperH + 4) {
container.style.height = `${required}px`; container.style.height = `${required}px`;
} else { } else {
@@ -730,7 +737,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
const mgr = new ChartManager(containerRef.current, theme); const mgr = new ChartManager(containerRef.current, theme, {
compactPaneLayout: paneLayoutClamp,
autoSize: !paneLayoutClamp,
});
managerRef.current = mgr; managerRef.current = mgr;
setChartMgr(mgr); setChartMgr(mgr);
if (!volumeVisibleRef.current) { if (!volumeVisibleRef.current) {
@@ -141,11 +141,11 @@ const TradeNotificationListRow: React.FC<Props> = ({
const chartExpanded = isListLayout && expandedChartKey != null; const chartExpanded = isListLayout && expandedChartKey != null;
const chartActive = isListLayout && chartsEnabled && panelActive; const chartActive = isListLayout && chartsEnabled && panelActive;
const signalActive = isListLayout && panelActive && !chartExpanded; const signalCardEnabled = isListLayout && panelActive && !chartExpanded;
const ticker = tickers?.get(item.market); const ticker = tickers?.get(item.market);
const expandedIndicator = indicatorCards.find(c => c.id === expandedChartKey); const expandedIndicator = indicatorCards.find(c => c.id === expandedChartKey);
const signalSnapshotEnabled = isListLayout && panelActive && item.strategyId != null; const signalSnapshotEnabled = signalCardEnabled && item.strategyId != null;
const { snapshot, loading: signalLoading } = useTradeNotificationSignalSnapshot( const { snapshot, loading: signalLoading } = useTradeNotificationSignalSnapshot(
item.market, item.market,
item.strategyId, item.strategyId,
@@ -356,28 +356,26 @@ const TradeNotificationListRow: React.FC<Props> = ({
</TradeNotificationHScrollPane> </TradeNotificationHScrollPane>
) : ( ) : (
<> <>
{signalActive && ( <TradeNotificationHScrollPane
<TradeNotificationHScrollPane className="tnl-row-gallery-signal"
className="tnl-row-gallery-signal" label="신호 일치율"
label="신호 일치율" >
> <TradeNotificationSignalCard
<TradeNotificationSignalCard market={item.market}
market={item.market} strategyId={item.strategyId}
strategyId={item.strategyId} strategy={strategy}
strategy={strategy} strategies={strategies}
strategies={strategies} strategyLabel={strategyLabel}
strategyLabel={strategyLabel} meta={strategyLabel}
meta={strategyLabel} candleType={item.candleType}
candleType={item.candleType} theme={theme}
theme={theme} ticker={ticker}
ticker={ticker} enabled={signalCardEnabled}
enabled={signalActive} snapshot={snapshot}
snapshot={snapshot} signalLoading={signalLoading}
signalLoading={signalLoading} onExpandCharts={() => setExpandedChartKey('candle')}
onExpandCharts={() => setExpandedChartKey('candle')} />
/> </TradeNotificationHScrollPane>
</TradeNotificationHScrollPane>
)}
<div className="tnl-row-gallery-candle" aria-label="캔들 차트"> <div className="tnl-row-gallery-candle" aria-label="캔들 차트">
<TradeSignalChartCard <TradeSignalChartCard
@@ -50,6 +50,17 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const marketRef = useRef(market); const marketRef = useRef(market);
marketRef.current = market; marketRef.current = market;
const syncChartLayout = useCallback(() => {
const wrap = canvasWrapRef.current;
const mgr = managerRef.current;
if (!wrap || !mgr?.hasMainSeries()) return;
const chartWrap = wrap.querySelector<HTMLElement>('.tv-chart-wrap');
const w = chartWrap?.clientWidth ?? wrap.clientWidth;
const h = chartWrap?.clientHeight ?? wrap.clientHeight;
if (w <= 0 || h <= 0) return;
mgr.syncLayout(w, h);
}, []);
const handleCandlesReady = useCallback(() => { const handleCandlesReady = useCallback(() => {
chartLiveReadyRef.current = true; chartLiveReadyRef.current = true;
const pending = pendingBarRef.current; const pending = pendingBarRef.current;
@@ -57,12 +68,8 @@ const TradeSignalMiniChart: React.FC<Props> = ({
if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return; if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return;
pendingBarRef.current = null; pendingBarRef.current = null;
mgr.updateBar(pending); mgr.updateBar(pending);
const wrap = canvasWrapRef.current; syncChartLayout();
if (wrap && wrap.clientWidth > 0 && wrap.clientHeight > 0) { }, [syncChartLayout]);
mgr.resetPaneHeights(wrap.clientHeight);
mgr.resize(wrap.clientWidth, wrap.clientHeight);
}
}, []);
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => { const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
if (barsMarketRef.current !== marketRef.current) return; if (barsMarketRef.current !== marketRef.current) return;
@@ -107,17 +114,6 @@ const TradeSignalMiniChart: React.FC<Props> = ({
barsMarketRef.current = barsMarket; barsMarketRef.current = barsMarket;
const barsReady = bars.length >= 2 && barsMarket === market; const barsReady = bars.length >= 2 && barsMarket === market;
const syncChartLayout = useCallback(() => {
const wrap = canvasWrapRef.current;
const mgr = managerRef.current;
if (!wrap || !mgr?.hasMainSeries()) return;
const w = wrap.clientWidth;
const h = wrap.clientHeight;
if (w <= 0 || h <= 0) return;
mgr.resetPaneHeights(h);
mgr.resize(w, h);
}, []);
useLayoutEffect(() => { useLayoutEffect(() => {
chartLiveReadyRef.current = false; chartLiveReadyRef.current = false;
pendingBarRef.current = null; pendingBarRef.current = null;
@@ -132,14 +128,8 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const timers = [200, 600, 1200].map(delay => const timers = [200, 600, 1200].map(delay =>
setTimeout(() => { setTimeout(() => {
if (!canvasWrapRef.current) return; if (!canvasWrapRef.current) return;
const h = canvasWrapRef.current.clientHeight;
if (h <= 0) return;
const w = canvasWrapRef.current.clientWidth;
if (managerRef.current?.hasMainSeries()) { if (managerRef.current?.hasMainSeries()) {
if (w > 0) { syncChartLayout();
managerRef.current.resetPaneHeights(h);
managerRef.current.resize(w, h);
}
} else if (!chartReloadTriggeredRef.current && delay >= 600 && barsReady) { } else if (!chartReloadTriggeredRef.current && delay >= 600 && barsReady) {
chartReloadTriggeredRef.current = true; chartReloadTriggeredRef.current = true;
setChartReloadTick(t => t + 1); setChartReloadTick(t => t + 1);
@@ -1160,6 +1160,16 @@ ul.tnl-list.tnl-list--grid {
min-height: 300px; min-height: 300px;
} }
/* 그리드형 — 상세 메타와 투자전략 구분선 사이 여백 축소 */
.tnl-summary-card--grid .tnl-summary-meta {
flex: 0 1 auto;
}
.tnl-summary-card--grid .tnl-summary-strategy-field {
margin-top: -6px;
padding-top: 2px;
}
.tnl-row--grid.tnl-row--unread .tnl-summary-card { .tnl-row--grid.tnl-row--unread .tnl-summary-card {
border-color: rgba(122, 162, 247, 0.45); border-color: rgba(122, 162, 247, 0.45);
} }
+59 -8
View File
@@ -258,13 +258,23 @@ export class ChartManager {
/** applyPaneLayout / resetPaneHeights 에서 측정한 마지막 가용 높이(px) */ /** applyPaneLayout / resetPaneHeights 에서 측정한 마지막 가용 높이(px) */
private _lastLayoutAvailableHeight?: number; private _lastLayoutAvailableHeight?: number;
constructor(container: HTMLElement, theme: Theme) { /** 알림 목록 미니 차트 등 고정 높이 — pane 최소 px·비율 완화 */
private _compactPaneLayout = false;
constructor(
container: HTMLElement,
theme: Theme,
options?: { autoSize?: boolean; compactPaneLayout?: boolean },
) {
this.container = container; this.container = container;
this._compactPaneLayout = options?.compactPaneLayout ?? false;
const autoSize = options?.autoSize ?? !this._compactPaneLayout;
const t = getTheme(theme); const t = getTheme(theme);
this.chart = createChart(container, { this.chart = createChart(container, {
/* autoSize: true → LWC가 내부적으로 ResizeObserver를 사용해 컨테이너 크기 변경을 자동으로 처리. /* autoSize: true → LWC가 내부적으로 ResizeObserver를 사용해 컨테이너 크기 변경을 자동으로 처리.
멀티 레이아웃에서 초기 렌더 시 컨테이너가 0 크기일 때도 차트가 정상 표시됨. */ 멀티 레이아웃에서 초기 렌더 시 컨테이너가 0 크기일 때도 차트가 정상 표시됨.
autoSize: true, compact(미니) 차트는 autoSize:false + syncLayout() 으로 피드백 루프 방지. */
autoSize,
layout: { layout: {
background: { type: ColorType.Solid, color: t.bgColor }, background: { type: ColorType.Solid, color: t.bgColor },
textColor: t.textColor, textColor: t.textColor,
@@ -764,10 +774,39 @@ export class ChartManager {
/** 지표 추가·제거 후 pane index 동기화 + stretch 재배분 */ /** 지표 추가·제거 후 pane index 동기화 + stretch 재배분 */
private _finalizeIndicatorPaneLayout(): void { private _finalizeIndicatorPaneLayout(): void {
if (this._activeIndicatorPaneIndices().size === 0) return;
this._removeOrphanSubPanes(); this._removeOrphanSubPanes();
this._resyncIndicatorPaneIndices(); if (this._activeIndicatorPaneIndices().size > 0) {
this.resetPaneHeights(this._lastLayoutAvailableHeight); this._resyncIndicatorPaneIndices();
} else {
this._trimTrailingEmptySubPanes();
}
const H = this._lastLayoutAvailableHeight ?? this.container.clientHeight;
this.resetPaneHeights(H > 0 ? H : undefined);
if (this._compactPaneLayout && H > 0) {
try {
const w = this.container.clientWidth;
if (w > 0) this.chart.resize(w, H);
} catch { /* ok */ }
}
this._notifyPaneLayout();
}
/**
* 고정 높이 미니 차트 — pane 비율 재계산 + LWC resize 를 한 번에 수행.
* (autoSize:false 일 때 외부 ResizeObserver 가 호출)
*/
syncLayout(width: number, height: number): void {
if (height > 0) this._lastLayoutAvailableHeight = height;
this._removeOrphanSubPanes();
if (this._activeIndicatorPaneIndices().size > 0) {
this._resyncIndicatorPaneIndices();
} else {
this._trimTrailingEmptySubPanes();
}
this.resetPaneHeights(height);
try {
if (width > 0 && height > 0) this.chart.resize(width, height);
} catch { /* ok */ }
this._notifyPaneLayout(); this._notifyPaneLayout();
} }
@@ -3530,7 +3569,7 @@ export class ChartManager {
const volumeShown = this._volumeVisible && !this._candleOnlyLayout; const volumeShown = this._volumeVisible && !this._candleOnlyLayout;
const VOL_FRAC = this._volumeFrac(H); const VOL_FRAC = this._volumeFrac(H);
const MIN_IND_PX = 80; const MIN_IND_PX = this._minIndPx(H);
const ORPHAN_STRETCH = 0.0001; const ORPHAN_STRETCH = 0.0001;
/** 보조지표 pane 은 동일 stretch weight → 항상 같은 높이 */ /** 보조지표 pane 은 동일 stretch weight → 항상 같은 높이 */
const IND_EQUAL_WEIGHT = 1; const IND_EQUAL_WEIGHT = 1;
@@ -3569,14 +3608,26 @@ export class ChartManager {
return totalRequired; return totalRequired;
} }
private _minIndPx(H: number): number {
if (this._compactPaneLayout) {
return Math.min(56, Math.max(28, Math.round(H * 0.2)));
}
return 80;
}
private _paneHeightFractions( private _paneHeightFractions(
indicatorPaneCount: number, indicatorPaneCount: number,
volFrac: number, volFrac: number,
availableHeight?: number,
): { mainFrac: number; eachIndFrac: number } { ): { mainFrac: number; eachIndFrac: number } {
const N = indicatorPaneCount; const N = indicatorPaneCount;
if (N === 0) { if (N === 0) {
return { mainFrac: 1 - volFrac, eachIndFrac: 0 }; return { mainFrac: 1 - volFrac, eachIndFrac: 0 };
} }
if (this._compactPaneLayout && N === 1 && availableHeight != null && availableHeight < 400) {
const mainFrac = 0.52;
return { mainFrac, eachIndFrac: Math.max(0.28, 1 - mainFrac - volFrac) };
}
if (N <= 3) { if (N <= 3) {
return { mainFrac: 0.5, eachIndFrac: (0.5 - volFrac) / N }; return { mainFrac: 0.5, eachIndFrac: (0.5 - volFrac) / N };
} }
@@ -3621,7 +3672,7 @@ export class ChartManager {
? this._lastLayoutAvailableHeight ? this._lastLayoutAvailableHeight
: this.container.clientHeight); : this.container.clientHeight);
const N = this._activeIndicatorPaneIndices().size; const N = this._activeIndicatorPaneIndices().size;
const { mainFrac } = this._paneHeightFractions(N, this._volumeFrac(H)); const { mainFrac } = this._paneHeightFractions(N, this._volumeFrac(H), H);
return this._applyPaneStretchFactors(mainFrac, availableHeight); return this._applyPaneStretchFactors(mainFrac, availableHeight);
} }