가상매매 그리드 배치 오류수정

This commit is contained in:
Macbook
2026-06-08 21:09:02 +09:00
parent 041db42741
commit 80c78d75bc
2 changed files with 84 additions and 34 deletions
@@ -1,7 +1,8 @@
/** /**
* 그리드형 n×n 레이아웃 선택 (TradingView 스타일 팝업) — 알림 목록·가상매매 공용 * 그리드형 n×n 레이아웃 선택 (TradingView 스타일 팝업) — 알림 목록·가상매매 공용
*/ */
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { import {
gridPresetsByGroup, gridPresetsByGroup,
getTradeNotificationGridPreset, getTradeNotificationGridPreset,
@@ -134,6 +135,8 @@ const LayoutIcon: React.FC<{ preset: TradeNotificationGridPreset }> = ({ preset
); );
}; };
const POPOVER_WIDTH = 320;
const GridLayoutPicker: React.FC<Props> = ({ const GridLayoutPicker: React.FC<Props> = ({
value, value,
onChange, onChange,
@@ -142,14 +145,46 @@ const GridLayoutPicker: React.FC<Props> = ({
popoverAlign = 'right', popoverAlign = 'right',
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [popoverPos, setPopoverPos] = useState<{ top: number; left: number } | null>(null);
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const active = getTradeNotificationGridPreset(value); const active = getTradeNotificationGridPreset(value);
const groups = [...gridPresetsByGroup().entries()].sort((a, b) => a[0] - b[0]); const groups = [...gridPresetsByGroup().entries()].sort((a, b) => a[0] - b[0]);
const updatePopoverPos = useCallback(() => {
const el = rootRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const width = Math.min(POPOVER_WIDTH, window.innerWidth - 48);
const left = popoverAlign === 'left'
? rect.left
: rect.right - width;
setPopoverPos({
top: rect.bottom + 8,
left: Math.max(8, Math.min(left, window.innerWidth - width - 8)),
});
}, [popoverAlign]);
useEffect(() => {
if (!open) {
setPopoverPos(null);
return;
}
updatePopoverPos();
window.addEventListener('resize', updatePopoverPos);
window.addEventListener('scroll', updatePopoverPos, true);
return () => {
window.removeEventListener('resize', updatePopoverPos);
window.removeEventListener('scroll', updatePopoverPos, true);
};
}, [open, updatePopoverPos]);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
const onDoc = (e: MouseEvent) => { const onDoc = (e: MouseEvent) => {
if (!rootRef.current?.contains(e.target as Node)) setOpen(false); const t = e.target as Node;
if (rootRef.current?.contains(t) || popoverRef.current?.contains(t)) return;
setOpen(false);
}; };
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false); if (e.key === 'Escape') setOpen(false);
@@ -164,30 +199,25 @@ const GridLayoutPicker: React.FC<Props> = ({
const popoverClass = [ const popoverClass = [
cls(variant, 'popover'), cls(variant, 'popover'),
cls(variant, 'popover--portal'),
popoverAlign === 'left' ? cls(variant, 'popover--align-left') : '', popoverAlign === 'left' ? cls(variant, 'popover--align-left') : '',
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
return ( const popover = open && popoverPos ? (
<div className={cls(variant, 'picker')} ref={rootRef}> <div
<button ref={popoverRef}
type="button" className={popoverClass}
className={[ role="dialog"
cls(variant, 'trigger'), aria-label="그리드 배치"
open ? cls(variant, 'trigger--open') : '', style={{
disabled ? cls(variant, 'trigger--disabled') : '', position: 'fixed',
].filter(Boolean).join(' ')} top: popoverPos.top,
title={`그리드 배치: ${active.label}`} left: popoverPos.left,
aria-label="그리드 배치 선택" right: 'auto',
aria-expanded={open} zIndex: 10000,
aria-haspopup="dialog" width: Math.min(POPOVER_WIDTH, window.innerWidth - 48),
disabled={disabled} }}
onClick={() => !disabled && setOpen(o => !o)}
> >
<LayoutIcon preset={active} />
</button>
{open && (
<div className={popoverClass} role="dialog" aria-label="그리드 배치">
<p className={cls(variant, 'popover-title')}> </p> <p className={cls(variant, 'popover-title')}> </p>
{groups.map(([group, presets]) => ( {groups.map(([group, presets]) => (
<div key={group} className={cls(variant, 'group')}> <div key={group} className={cls(variant, 'group')}>
@@ -216,7 +246,28 @@ const GridLayoutPicker: React.FC<Props> = ({
</div> </div>
))} ))}
</div> </div>
)} ) : null;
return (
<div className={cls(variant, 'picker')} ref={rootRef}>
<button
type="button"
className={[
cls(variant, 'trigger'),
open ? cls(variant, 'trigger--open') : '',
disabled ? cls(variant, 'trigger--disabled') : '',
].filter(Boolean).join(' ')}
title={`그리드 배치: ${active.label}`}
aria-label="그리드 배치 선택"
aria-expanded={open}
aria-haspopup="dialog"
disabled={disabled}
onClick={() => !disabled && setOpen(o => !o)}
>
<LayoutIcon preset={active} />
</button>
{popover && createPortal(popover, document.body)}
</div> </div>
); );
}; };
@@ -133,7 +133,6 @@ export const VirtualViewHeaderControls: React.FC<ViewProps> = ({
<VirtualGridLayoutPicker <VirtualGridLayoutPicker
value={gridPreset} value={gridPreset}
onChange={onGridPresetChange} onChange={onGridPresetChange}
disabled={!showSignalChartToggle}
/> />
)} )}
{showSignalChartToggle && onGlobalDisplayModeChange && ( {showSignalChartToggle && onGlobalDisplayModeChange && (