goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,170 @@
/**
* 멀티차트 WebSocket 연결 상태 표시
* - 클릭 시 연결 목록 및 수신 상태 팝업
* - MultiChartMarketDataProvider 내부에서 사용
*/
import React from 'react';
import { Box, Popover, LinearProgress, Typography, Tooltip, Button } from '@mui/material';
import { useMultiChartMarketData } from '../../contexts/MultiChartMarketDataContext';
import type { ConnectionStatus } from '../../hooks/useWebSocketConnectionStatus';
const WS_STATUS_CONFIG: Record<ConnectionStatus, { label: string; color: string; bgColor: string }> = {
connected: { label: '실시간 연결됨', color: 'success.main', bgColor: 'success.main' },
connecting: { label: '연결 중...', color: 'warning.main', bgColor: 'warning.main' },
disconnected: { label: '연결 끊김', color: 'text.secondary', bgColor: 'grey.400' },
error: { label: '연결 오류', color: 'error.main', bgColor: 'error.main' },
};
export const MultiChartWebSocketStatus: React.FC = () => {
const ctx = useMultiChartMarketData();
const status = ctx?.status ?? 'disconnected';
const connectionList = ctx?.connectionList ?? [];
const [popoverAnchor, setPopoverAnchor] = React.useState<HTMLElement | null>(null);
const [, setProgressTick] = React.useState(0);
const [dataReceivedBlink, setDataReceivedBlink] = React.useState(false); // ✅ 데이터 수신 깜빡임
const lastReceivedTimestampRef = React.useRef<number>(0);
React.useEffect(() => {
if (!popoverAnchor) return;
const id = setInterval(() => setProgressTick((t) => t + 1), 1000);
return () => clearInterval(id);
}, [popoverAnchor]);
// ✅ connectionList의 lastReceivedAt 변화 감지 → 깜빡임 트리거
React.useEffect(() => {
if (connectionList.length === 0) return;
const latestReceived = Math.max(...connectionList.map(c => c.lastReceivedAt));
if (latestReceived > 0 && latestReceived !== lastReceivedTimestampRef.current) {
lastReceivedTimestampRef.current = latestReceived;
setDataReceivedBlink(true);
setTimeout(() => setDataReceivedBlink(false), 300); // 300ms 후 자동 꺼짐
}
}, [connectionList]);
const config = WS_STATUS_CONFIG[status as ConnectionStatus] ?? WS_STATUS_CONFIG.disconnected;
const getReceiveProgress = (lastReceivedAt: number) => {
if (!lastReceivedAt) return 0;
const elapsed = (Date.now() - lastReceivedAt) / 1000;
if (elapsed <= 5) return 100;
if (elapsed >= 30) return 0;
return Math.round(100 - ((elapsed - 5) / 25) * 100);
};
return (
<>
<Tooltip title={`WebSocket: ${config.label} (클릭하여 연결 목록 보기)`}>
<Box
onClick={(e) => setPopoverAnchor(e.currentTarget)}
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1,
py: 0.5,
borderRadius: 1,
bgcolor: 'action.hover',
fontSize: '0.75rem',
color: config.color,
cursor: 'pointer',
'&:hover': { bgcolor: 'action.selected' },
// ✅ 데이터 수신 시 연두색 형광 깜빡임
...(dataReceivedBlink && status === 'connected' ? {
boxShadow: '0 0 10px 3px rgba(204, 255, 0, 0.7), inset 0 0 8px 2px rgba(204, 255, 0, 0.3)',
transition: 'box-shadow 0.15s ease-out',
} : {}),
}}
>
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
bgcolor: config.bgColor,
...(status === 'connecting' && {
animation: 'pulse 1.5s ease-in-out infinite',
'@keyframes pulse': { '0%, 100%': { opacity: 1 }, '50%': { opacity: 0.4 } },
}),
}}
/>
{config.label}
</Box>
</Tooltip>
<Popover
open={Boolean(popoverAnchor)}
anchorEl={popoverAnchor}
onClose={() => setPopoverAnchor(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
slotProps={{ paper: { sx: { mt: 0.5, minWidth: 280 } } }}
>
<Box sx={{ p: 1.5 }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
WebSocket
</Typography>
{connectionList.length === 0 ? (
<Typography variant="body2" color="text.secondary">
.
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{connectionList.map((item) => {
const progress = getReceiveProgress(item.lastReceivedAt);
const hasNoData = item.candleCount === 0;
const displayName = item.koreanName || item.market;
return (
<Box key={`${item.market}:${item.timeframe}`} sx={{ minWidth: 240 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.25 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{displayName}
</Typography>
<Typography variant="caption" color="text.secondary">
{item.timeframe} · {item.candleCount}
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 1,
bgcolor: 'action.hover',
'& .MuiLinearProgress-bar': {
bgcolor: progress >= 80 ? 'success.main' : progress >= 40 ? 'warning.main' : 'grey.400',
},
}}
/>
{hasNoData && (
<Typography variant="caption" color="error.main" sx={{ display: 'block', mt: 0.5 }}>
미수신: Redis , ,
</Typography>
)}
</Box>
);
})}
</Box>
)}
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1.5, pt: 1, borderTop: 1, borderColor: 'divider' }}>
0개: market-data , .
</Typography>
{(status === 'disconnected' || status === 'error') && ctx?.reconnect && (
<Button
size="small"
variant="outlined"
fullWidth
sx={{ mt: 1 }}
onClick={() => {
ctx.reconnect();
setPopoverAnchor(null);
}}
>
</Button>
)}
</Box>
</Popover>
</>
);
};