import { useState, useEffect, useCallback, type CSSProperties } from 'react'; import type { DerivedLegacyVessel } from '../../features/legacyDashboard/model/types'; import { VESSEL_TYPES } from '../../entities/vessel/model/meta'; import type { SortKey, SortDir } from './VesselSelectModal'; interface VesselSelectGridProps { vessels: DerivedLegacyVessel[]; selectedMmsis: Set; toggleMmsi: (mmsi: number) => void; setMmsis: (mmsis: Set) => void; sortKey: SortKey | null; sortDir: SortDir; onSort: (key: SortKey) => void; } interface DragState { startIdx: number; endIdx: number; direction: 'check' | 'uncheck'; } const STYLE_TABLE: CSSProperties = { width: '100%', borderCollapse: 'collapse', fontSize: 11, }; const STYLE_TH: CSSProperties = { position: 'sticky', top: 0, background: 'rgba(15,23,42,0.98)', color: '#94a3b8', textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid rgba(148,163,184,0.2)', fontWeight: 500, cursor: 'pointer', userSelect: 'none', }; function getSortIndicator(col: SortKey, sortKey: SortKey | null, sortDir: SortDir): string { if (sortKey !== col) return ' –'; return sortDir === 'asc' ? ' ▲' : ' ▼'; } const STYLE_TH_CHECKBOX: CSSProperties = { ...STYLE_TH, width: 28, }; function getTdStyle(isSelected: boolean): CSSProperties { return { padding: '5px 8px', borderBottom: '1px solid rgba(148,163,184,0.08)', cursor: 'pointer', background: isSelected ? 'rgba(59,130,246,0.12)' : undefined, }; } function getStateBadgeStyle(isFishing: boolean, isTransit: boolean): CSSProperties { const color = isFishing ? '#22C55E' : isTransit ? '#3B82F6' : '#64748B'; return { background: `${color}22`, color, borderRadius: 3, padding: '1px 4px', fontSize: 10, }; } function getDotStyle(color: string): CSSProperties { return { display: 'inline-block', width: 8, height: 8, borderRadius: '50%', background: color, marginRight: 4, verticalAlign: 'middle', }; } function isInDragRange(idx: number, drag: DragState): boolean { const min = Math.min(drag.startIdx, drag.endIdx); const max = Math.max(drag.startIdx, drag.endIdx); return idx >= min && idx <= max; } function getDragHighlight(direction: 'check' | 'uncheck'): string { return direction === 'check' ? 'rgba(59,130,246,0.18)' : 'rgba(148,163,184,0.12)'; } export function VesselSelectGrid({ vessels, selectedMmsis, toggleMmsi, setMmsis, sortKey, sortDir, onSort }: VesselSelectGridProps) { const [dragState, setDragState] = useState(null); const handleMouseDown = useCallback( (idx: number, e: React.MouseEvent) => { // 체크박스 직접 클릭은 무시 (기존 onChange 처리) if ((e.target as HTMLElement).tagName === 'INPUT') return; e.preventDefault(); const isSelected = selectedMmsis.has(vessels[idx].mmsi); setDragState({ startIdx: idx, endIdx: idx, direction: isSelected ? 'uncheck' : 'check' }); }, [vessels, selectedMmsis], ); const handleMouseEnter = useCallback( (idx: number) => { if (!dragState) return; setDragState((prev) => (prev ? { ...prev, endIdx: idx } : null)); }, [dragState], ); // document-level mouseup: 드래그 종료 useEffect(() => { if (!dragState) return; const handleMouseUp = () => { const { startIdx, endIdx, direction } = dragState; if (startIdx === endIdx) { // 단일 클릭 toggleMmsi(vessels[startIdx].mmsi); } else { // 범위 선택 const min = Math.min(startIdx, endIdx); const max = Math.max(startIdx, endIdx); const newSet = new Set(selectedMmsis); for (let i = min; i <= max; i++) { const mmsi = vessels[i].mmsi; if (direction === 'check') newSet.add(mmsi); else newSet.delete(mmsi); } setMmsis(newSet); } setDragState(null); }; document.addEventListener('mouseup', handleMouseUp); return () => document.removeEventListener('mouseup', handleMouseUp); }, [dragState, vessels, selectedMmsis, toggleMmsi, setMmsis]); return ( {vessels.map((v, idx) => { const isSelected = selectedMmsis.has(v.mmsi); const meta = VESSEL_TYPES[v.shipCode]; const inRange = dragState ? isInDragRange(idx, dragState) : false; // 드래그 중 범위 내 행 → 예상 상태 미리보기 let rowBg: string | undefined; if (inRange && dragState) { rowBg = getDragHighlight(dragState.direction); } else if (isSelected) { rowBg = 'rgba(59,130,246,0.12)'; } const tdStyle = getTdStyle(false); // 배경은 tr에서 관리 const stateBadgeStyle = getStateBadgeStyle(v.state.isFishing, v.state.isTransit); const mmsiDisplay = String(v.mmsi); const sogDisplay = v.sog !== null ? `${v.sog.toFixed(1)} kt` : '–'; // 드래그 중 범위 내 체크 상태 미리보기 const previewChecked = inRange && dragState ? dragState.direction === 'check' : isSelected; return ( handleMouseDown(idx, e)} onMouseEnter={() => handleMouseEnter(idx)} > ); })}
onSort('shipCode')}>업종{getSortIndicator('shipCode', sortKey, sortDir)} onSort('permitNo')}>등록번호{getSortIndicator('permitNo', sortKey, sortDir)} onSort('name')}>선명{getSortIndicator('name', sortKey, sortDir)} onSort('mmsi')}>MMSI{getSortIndicator('mmsi', sortKey, sortDir)} onSort('sog')}>속력{getSortIndicator('sog', sortKey, sortDir)} onSort('state')}>상태{getSortIndicator('state', sortKey, sortDir)}
toggleMmsi(v.mmsi)} onClick={(e) => e.stopPropagation()} style={{ cursor: 'pointer' }} /> {v.shipCode} {v.permitNo} {v.name} {mmsiDisplay} {sogDisplay} {v.state.label}
); }