gc-wing/apps/web/src/widgets/map3d/components/VesselContextMenu.tsx

136 lines
3.6 KiB
TypeScript
Raw Normal View 히스토리

import { useEffect, useRef } from 'react';
interface Props {
x: number;
y: number;
mmsi: number;
vesselName: string;
onRequestTrack: (mmsi: number, minutes: number) => void;
onClose: () => void;
}
const TRACK_OPTIONS = [
{ label: '6시간', minutes: 360 },
{ label: '12시간', minutes: 720 },
{ label: '1일', minutes: 1440 },
{ label: '3일', minutes: 4320 },
{ label: '5일', minutes: 7200 },
] as const;
const MENU_WIDTH = 180;
const MENU_PAD = 8;
export function VesselContextMenu({ x, y, mmsi, vesselName, onRequestTrack, onClose }: Props) {
const ref = useRef<HTMLDivElement>(null);
// 화면 밖 보정
const left = Math.min(x, window.innerWidth - MENU_WIDTH - MENU_PAD);
const maxTop = window.innerHeight - (TRACK_OPTIONS.length * 30 + 56) - MENU_PAD;
const top = Math.min(y, maxTop);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
const onClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
};
const onScroll = () => onClose();
window.addEventListener('keydown', onKey);
window.addEventListener('mousedown', onClick, true);
window.addEventListener('scroll', onScroll, true);
return () => {
window.removeEventListener('keydown', onKey);
window.removeEventListener('mousedown', onClick, true);
window.removeEventListener('scroll', onScroll, true);
};
}, [onClose]);
const handleSelect = (minutes: number) => {
onRequestTrack(mmsi, minutes);
onClose();
};
return (
<div
ref={ref}
style={{
position: 'fixed',
left,
top,
zIndex: 9999,
minWidth: MENU_WIDTH,
background: 'rgba(24, 24, 32, 0.96)',
border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 8,
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
padding: '4px 0',
fontFamily: 'system-ui, sans-serif',
fontSize: 12,
color: '#e2e2e2',
backdropFilter: 'blur(8px)',
}}
>
{/* Header */}
<div
style={{
padding: '6px 12px 4px',
fontSize: 10,
fontWeight: 700,
color: 'rgba(255,255,255,0.45)',
letterSpacing: 0.3,
borderBottom: '1px solid rgba(255,255,255,0.06)',
marginBottom: 2,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: MENU_WIDTH - 24,
}}
title={`${vesselName} (${mmsi})`}
>
{vesselName}
</div>
{/* 항적조회 항목 */}
<div
style={{
padding: '4px 12px 2px',
fontSize: 11,
fontWeight: 600,
color: 'rgba(255,255,255,0.6)',
}}
>
</div>
{TRACK_OPTIONS.map((opt) => (
<button
key={opt.minutes}
onClick={() => handleSelect(opt.minutes)}
style={{
display: 'block',
width: '100%',
padding: '5px 12px 5px 24px',
background: 'none',
border: 'none',
color: '#e2e2e2',
fontSize: 12,
textAlign: 'left',
cursor: 'pointer',
lineHeight: 1.4,
}}
onMouseEnter={(e) => {
(e.target as HTMLElement).style.background = 'rgba(59,130,246,0.18)';
}}
onMouseLeave={(e) => {
(e.target as HTMLElement).style.background = 'none';
}}
>
{opt.label}
</button>
))}
</div>
);
}