136 lines
3.6 KiB
TypeScript
136 lines
3.6 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|