Unify popup UI to TradeAlertModal design system.

Add AppPopup shell, shared CSS, MUI theme overrides, and center positioning for consistent modal styling across frontend and frontend_golden.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-05-23 16:56:37 +09:00
parent 67db5982d9
commit eaf067db1c
15 changed files with 1383 additions and 494 deletions
@@ -2,22 +2,38 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Paper } from '@mui/material';
/**
* 드래그 가능한 Dialog Paper 컴포넌트
* 모든 Dialog에서 재사용 가능
* 드래그 가능한 Dialog Paper — tam 스타일 팝업, 마운트 시 화면 중앙 배치
*/
const DraggablePaper = (props: any) => {
const paperRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 60 }); // 초기 위치를 더 아래로 (60px)
const [position, setPosition] = useState({ x: 0, y: 0 });
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const centeredOnce = useRef(false);
useEffect(() => {
if (centeredOnce.current) return;
const el = paperRef.current;
if (!el) return;
const placeCenter = () => {
const w = el.offsetWidth || 400;
const h = el.offsetHeight || 320;
setPosition({
x: Math.max(8, (window.innerWidth - w) / 2),
y: Math.max(8, (window.innerHeight - h) / 2),
});
centeredOnce.current = true;
};
placeCenter();
const id = requestAnimationFrame(placeCenter);
return () => cancelAnimationFrame(id);
}, []);
const handleMouseDown = (e: React.MouseEvent) => {
// 타이틀바 영역에서만 드래그 시작
const target = e.target as HTMLElement;
// 버튼, 아이콘버튼, 또는 그 내부 요소를 클릭한 경우 드래그 시작 안 함
if (target.closest('.MuiDialogTitle-root') &&
!target.closest('button') &&
!target.closest('.MuiButton-root') &&
if (target.closest('.MuiDialogTitle-root') &&
!target.closest('button') &&
!target.closest('.MuiButton-root') &&
!target.closest('.MuiIconButton-root')) {
setIsDragging(true);
setDragStart({
@@ -29,17 +45,9 @@ const DraggablePaper = (props: any) => {
const handleMouseMove = useCallback((e: MouseEvent) => {
if (isDragging && paperRef.current) {
// 새 위치 계산
let newX = e.clientX - dragStart.x;
let newY = e.clientY - dragStart.y;
// 상단만 제한 (0보다 작아지지 않도록)
const minY = 0;
newY = Math.max(minY, newY);
setPosition({
x: newX,
y: newY,
x: e.clientX - dragStart.x,
y: Math.max(0, e.clientY - dragStart.y),
});
}
}, [isDragging, dragStart]);
@@ -71,12 +79,29 @@ const DraggablePaper = (props: any) => {
left: position.x,
margin: 0,
cursor: isDragging ? 'grabbing' : 'default',
borderRadius: '12px',
border: '1px solid rgba(63, 126, 245, 0.35)',
backgroundImage: 'none',
backgroundColor: '#1e222d',
boxShadow: '0 0 0 1px rgba(63,126,245,0.08), 0 20px 60px rgba(0,0,0,0.65), 0 0 40px rgba(63,126,245,0.06)',
overflow: 'hidden',
'& .MuiDialogTitle-root': {
cursor: 'grab',
userSelect: 'none',
'&:active': {
cursor: 'grabbing',
},
background: 'linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)',
borderBottom: '1px solid rgba(122,162,247,0.12)',
color: '#c0caf5',
fontSize: '14px',
fontWeight: 700,
'&:active': { cursor: 'grabbing' },
},
'& .MuiDialogContent-root': {
bgcolor: '#1e222d',
color: '#c0caf5',
},
'& .MuiDialogActions-root': {
bgcolor: '#1e222d',
borderTop: '1px solid rgba(122,162,247,0.1)',
},
}}
/>
@@ -84,4 +109,3 @@ const DraggablePaper = (props: any) => {
};
export default DraggablePaper;
@@ -0,0 +1,39 @@
/**
* TradeAlertModal 스타일 MUI Dialog 타이틀 (배지 + EN/KO 제목)
*/
import React from 'react';
import { Box, DialogTitle, IconButton, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { goldenPopupBadgeSx, goldenPopupDialogTitleSx } from '../utils/goldenPopupStyles';
interface Props {
badge?: string;
title: React.ReactNode;
titleKo?: React.ReactNode;
onClose?: () => void;
extra?: React.ReactNode;
}
const GoldenPopupDialogTitle: React.FC<Props> = ({ badge, title, titleKo, onClose, extra }) => (
<DialogTitle sx={goldenPopupDialogTitleSx} component="div">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, minWidth: 0 }}>
{badge ? <Box component="span" sx={goldenPopupBadgeSx}>{badge}</Box> : null}
<Typography component="span" sx={{ fontSize: 'inherit', fontWeight: 'inherit', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{title}
{titleKo ? (
<Typography component="span" sx={{ fontSize: '12px', fontWeight: 400, color: '#9aa5ce', ml: 0.5 }}>
({titleKo})
</Typography>
) : null}
</Typography>
</Box>
{extra}
{onClose ? (
<IconButton size="small" onClick={onClose} sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}>
<CloseIcon fontSize="small" />
</IconButton>
) : null}
</DialogTitle>
);
export default GoldenPopupDialogTitle;
+6 -23
View File
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
@@ -14,9 +13,10 @@ import {
InputAdornment,
Divider,
} from '@mui/material';
import { Visibility, VisibilityOff, Close, AccountCircle } from '@mui/icons-material';
import { Visibility, VisibilityOff, AccountCircle } from '@mui/icons-material';
import { useAuth } from '../contexts/AuthContext';
import DraggablePaper from './DraggablePaper';
import GoldenPopupDialogTitle from './GoldenPopupDialogTitle';
interface LoginDialogProps {
open: boolean;
@@ -87,32 +87,15 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, onSwitchToSign
pointerEvents: 'none',
'& .MuiDialog-container': {
pointerEvents: 'none',
alignItems: 'flex-start',
alignItems: 'center',
justifyContent: 'center',
},
}}
PaperProps={{
sx: {
pointerEvents: 'auto',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
border: '2px solid rgba(80, 100, 160, 0.5)',
}
sx: { pointerEvents: 'auto' },
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
bgcolor: '#1976d2',
color: '#fff',
py: 1.2,
}}>
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px', color: '#fff' }}>
🔐
</Typography>
<IconButton size="small" onClick={handleClose} sx={{ color: '#fff' }}>
<Close />
</IconButton>
</DialogTitle>
<GoldenPopupDialogTitle badge="AUTH" title="LOGIN" titleKo="로그인" onClose={handleClose} />
<form onSubmit={handleSubmit}>
<DialogContent>
@@ -17,10 +17,8 @@ import {
useTheme,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
import ShowChartIcon from '@mui/icons-material/ShowChart';
import {
evaluateAlertStrategy,
getAllAlerts,
@@ -104,9 +102,13 @@ function clamp(x: number, y: number, w: number, h: number) {
return { x: Math.min(Math.max(m, x), maxX), y: Math.min(Math.max(m, y), maxY) };
}
function nearAnchor(anchor: HTMLElement, w: number, h: number) {
const r = anchor.getBoundingClientRect();
return clamp(r.right - w, r.bottom + 8, w, h);
function centerPanel(w: number, h: number) {
if (typeof window === 'undefined') return { x: 24, y: 80 };
const m = 8;
return {
x: Math.max(m, (window.innerWidth - w) / 2),
y: Math.max(m, (window.innerHeight - h) / 2),
};
}
function statusColor(s: ConnectionStatus): 'default' | 'primary' | 'success' | 'error' | 'warning' {
@@ -431,7 +433,7 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
const resizeListenersRef = useRef<{ move: (e: MouseEvent) => void; up: () => void } | null>(null);
useLayoutEffect(() => {
setDragPos(nearAnchor(anchorEl, panelW, panelH));
setDragPos(centerPanel(panelW, panelH));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [anchorEl]);
@@ -516,7 +518,7 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
}}
>
<Paper
elevation={10}
elevation={0}
sx={{
width: '100%',
height: '100%',
@@ -524,21 +526,22 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
display: 'flex',
flexDirection: 'column',
position: 'relative',
border: isDark ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.12)',
bgcolor: 'background.paper',
borderRadius: '12px',
border: '1px solid rgba(63, 126, 245, 0.35)',
bgcolor: '#1e222d',
boxShadow: '0 0 0 1px rgba(63,126,245,0.08), 0 20px 60px rgba(0,0,0,0.65), 0 0 40px rgba(63,126,245,0.06)',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.75,
gap: 1,
px: 1.75,
py: 1.25,
flexShrink: 0,
borderBottom: 1,
borderColor: 'divider',
bgcolor: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
borderBottom: '1px solid rgba(122,162,247,0.12)',
background: 'linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)',
}}
>
<Box
@@ -546,7 +549,7 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
gap: 1,
flex: 1,
minWidth: 0,
cursor: isMaximized ? 'default' : 'grab',
@@ -554,10 +557,27 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
'&:active': { cursor: isMaximized ? 'default' : 'grabbing' },
}}
>
<ShowChartIcon sx={{ fontSize: 20, color: 'primary.main', flexShrink: 0 }} />
<DragIndicatorIcon sx={{ fontSize: 18, color: 'text.secondary', flexShrink: 0, opacity: 0.85 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexShrink: 0 }}>
<Box
component="span"
sx={{
px: 1.25,
py: 0.35,
borderRadius: '20px',
fontSize: '11px',
fontWeight: 800,
color: '#fff',
letterSpacing: '1.2px',
bgcolor: '#3f7ef5',
flexShrink: 0,
}}
>
LIVE
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexShrink: 0, color: '#c0caf5', fontSize: '14px' }}>
STRATEGY DATA
<Typography component="span" sx={{ fontSize: '12px', fontWeight: 400, color: '#9aa5ce', ml: 0.5 }}>
( )
</Typography>
</Typography>
<Typography
variant="caption"
@@ -587,10 +607,17 @@ const AlertRealtimeStrategyPopupShell: React.FC<{
size="small"
onClick={toggleMaximize}
aria-label={isMaximized ? '이전 크기로' : '최대화'}
sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}
>
{isMaximized ? <FullscreenExitIcon fontSize="small" /> : <FullscreenIcon fontSize="small" />}
</IconButton>
<IconButton className="rt-strategy-no-drag" size="small" onClick={onClose} aria-label="닫기">
<IconButton
className="rt-strategy-no-drag"
size="small"
onClick={onClose}
aria-label="닫기"
sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>