매매 시그널 알림 화면 수정

This commit is contained in:
Macbook
2026-05-28 21:58:15 +09:00
parent 9221b8dea2
commit cbad62a5b0
10 changed files with 1814 additions and 75 deletions
@@ -0,0 +1,207 @@
/**
* 그리드형 n×n 레이아웃 선택 (TradingView 스타일 팝업)
*/
import React, { useEffect, useRef, useState } from 'react';
import {
gridPresetsByGroup,
getTradeNotificationGridPreset,
type TradeNotificationGridPreset,
type TradeNotificationGridPresetId,
} from '../../utils/tradeNotificationGridPresets';
interface Props {
value: TradeNotificationGridPresetId;
onChange: (id: TradeNotificationGridPresetId) => void;
disabled?: boolean;
}
/** 20×20 와이어프레임 아이콘 */
const LayoutIcon: React.FC<{ preset: TradeNotificationGridPreset }> = ({ preset }) => {
const s = 20;
const g = 1;
const cells: Array<{ x: number; y: number; w: number; h: number }> = [];
switch (preset.id) {
case '1x1':
cells.push({ x: 0, y: 0, w: s, h: s });
break;
case '2x1':
cells.push({ x: 0, y: 0, w: 9, h: s }, { x: 10, y: 0, w: 9, h: s });
break;
case '1x2':
cells.push({ x: 0, y: 0, w: s, h: 9 }, { x: 0, y: 10, w: s, h: 9 });
break;
case '3x1':
cells.push(
{ x: 0, y: 0, w: 6, h: s },
{ x: 7, y: 0, w: 6, h: s },
{ x: 14, y: 0, w: 6, h: s },
);
break;
case '1x3':
cells.push(
{ x: 0, y: 0, w: s, h: 6 },
{ x: 0, y: 7, w: s, h: 6 },
{ x: 0, y: 14, w: s, h: 6 },
);
break;
case '2x1-31':
cells.push({ x: 0, y: 0, w: 9, h: s }, { x: 10, y: 0, w: 9, h: 9 }, { x: 10, y: 10, w: 9, h: 9 });
break;
case '2x2':
cells.push(
{ x: 0, y: 0, w: 9, h: 9 },
{ x: 10, y: 0, w: 9, h: 9 },
{ x: 0, y: 10, w: 9, h: 9 },
{ x: 10, y: 10, w: 9, h: 9 },
);
break;
case '4x1':
for (let i = 0; i < 4; i++) cells.push({ x: i * 5, y: 0, w: 4, h: s });
break;
case '1x4':
for (let i = 0; i < 4; i++) cells.push({ x: 0, y: i * 5, w: s, h: 4 });
break;
case '2x1-211':
cells.push(
{ x: 0, y: 0, w: 9, h: 9 },
{ x: 10, y: 0, w: 9, h: 9 },
{ x: 0, y: 10, w: 9, h: 9 },
{ x: 10, y: 10, w: 9, h: 9 },
);
break;
case '5x1':
for (let i = 0; i < 5; i++) cells.push({ x: i * 4, y: 0, w: 3, h: s });
break;
case '3x2':
for (let c = 0; c < 3; c++) {
for (let r = 0; r < 2; r++) {
cells.push({ x: c * 7, y: r * 10, w: 6, h: 9 });
}
}
break;
case '2x3':
for (let c = 0; c < 2; c++) {
for (let r = 0; r < 3; r++) {
cells.push({ x: c * 10, y: r * 7, w: 9, h: 6 });
}
}
break;
case '4x2':
for (let c = 0; c < 4; c++) {
for (let r = 0; r < 2; r++) {
cells.push({ x: c * 5, y: r * 10, w: 4, h: 9 });
}
}
break;
case '2x4':
for (let c = 0; c < 2; c++) {
for (let r = 0; r < 4; r++) {
cells.push({ x: c * 10, y: r * 5, w: 9, h: 4 });
}
}
break;
default:
cells.push({ x: 0, y: 0, w: s, h: s });
}
return (
<svg width="22" height="22" viewBox="0 0 20 20" aria-hidden className="tnl-grid-preset-icon-svg">
{cells.map((c, i) => (
<rect
key={i}
x={c.x}
y={c.y}
width={c.w}
height={c.h}
rx={0.5}
fill="none"
stroke="currentColor"
strokeWidth={g}
/>
))}
</svg>
);
};
const TradeNotificationGridLayoutPicker: React.FC<Props> = ({
value,
onChange,
disabled = false,
}) => {
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
const active = getTradeNotificationGridPreset(value);
const groups = [...gridPresetsByGroup().entries()].sort((a, b) => a[0] - b[0]);
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [open]);
return (
<div className="tnl-grid-layout-picker" ref={rootRef}>
<button
type="button"
className={[
'tnl-grid-layout-trigger',
open ? 'tnl-grid-layout-trigger--open' : '',
disabled ? 'tnl-grid-layout-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>
{open && (
<div className="tnl-grid-layout-popover" role="dialog" aria-label="그리드 배치">
<p className="tnl-grid-layout-popover-title"> </p>
{groups.map(([group, presets]) => (
<div key={group} className="tnl-grid-layout-group">
<span className="tnl-grid-layout-group-lbl">{group}</span>
<div className="tnl-grid-layout-group-row">
{presets.map(preset => (
<button
key={preset.id}
type="button"
className={[
'tnl-grid-preset-btn',
value === preset.id ? 'tnl-grid-preset-btn--on' : '',
].filter(Boolean).join(' ')}
title={`${preset.label} (${preset.cols}열)`}
aria-label={`${preset.label} 배치`}
aria-pressed={value === preset.id}
onClick={() => {
onChange(preset.id);
setOpen(false);
}}
>
<LayoutIcon preset={preset} />
</button>
))}
</div>
</div>
))}
</div>
)}
</div>
);
};
export default TradeNotificationGridLayoutPicker;