/** * 선박 상세 모달 컴포넌트 (다중 모달 지원, 최대 3개) * 퍼블리시 ShipComponent.jsx의 popupMap shipInfo 구조 활용 * 참조: mda-react-front/src/components/popup/ShipDetailModal.tsx * * - 헤더 드래그로 위치 이동 가능 * - 선박 사진 갤러리 (없으면 기본 이미지) * - 새 모달은 직전 모달의 현재 위치(드래그 반영) 기준 우측 140px 오프셋으로 생성 */ import { useRef, useState, useCallback, useEffect } from 'react'; import useShipStore from '../../stores/shipStore'; import { SHIP_KIND_LABELS, SIGNAL_FLAG_CONFIGS, SPEED_THRESHOLD, SIGNAL_KIND_CODE_FISHING, SIGNAL_KIND_CODE_KCGV, SIGNAL_KIND_CODE_PASSENGER, SIGNAL_KIND_CODE_CARGO, SIGNAL_KIND_CODE_TANKER, SIGNAL_KIND_CODE_GOV, } from '../../types/constants'; import defaultShipImg from '../../assets/img/default-ship.png'; import fishingIcon from '../../assets/img/shipDetail/detailKindIcon/fishing.svg'; import kcgvIcon from '../../assets/img/shipDetail/detailKindIcon/kcgv.svg'; import passengerIcon from '../../assets/img/shipDetail/detailKindIcon/passenger.svg'; import cargoIcon from '../../assets/img/shipDetail/detailKindIcon/cargo.svg'; import tankerIcon from '../../assets/img/shipDetail/detailKindIcon/tanker.svg'; import govIcon from '../../assets/img/shipDetail/detailKindIcon/gov.svg'; import etcIcon from '../../assets/img/shipDetail/detailKindIcon/etc.svg'; import './ShipDetailModal.scss'; /** 선종코드 → 아이콘 매핑 */ const SHIP_KIND_ICONS = { [SIGNAL_KIND_CODE_FISHING]: fishingIcon, [SIGNAL_KIND_CODE_KCGV]: kcgvIcon, [SIGNAL_KIND_CODE_PASSENGER]: passengerIcon, [SIGNAL_KIND_CODE_CARGO]: cargoIcon, [SIGNAL_KIND_CODE_TANKER]: tankerIcon, [SIGNAL_KIND_CODE_GOV]: govIcon, }; /** 선종 아이콘 URL 반환 */ function getShipKindIcon(signalKindCode) { return SHIP_KIND_ICONS[signalKindCode] || etcIcon; } /** * 국기 아이콘 URL 반환 (서버 API) * 참조: mda-react-front/src/services/filterCheck.ts - filterNationFlag() * @param {string} nationalCode - MID 숫자코드 (예: '440', '412') * @returns {string} 국기 이미지 URL */ function getNationalFlagUrl(nationalCode) { if (!nationalCode) return null; const baseUrl = import.meta.env.VITE_API_URL || ''; return `${baseUrl}/ship/image/small/${nationalCode}.svg`; } /** * receivedTime 문자열을 YYYY-MM-DD HH:mm:ss 형식으로 변환 * 입력 예: '20241123112300' 또는 '2024-11-23 11:23:00' 또는 '2024-11-23T11:23:00' * @param {string} raw * @returns {string} */ function formatDateTime(raw) { if (!raw) return '-'; // 이미 YYYY-MM-DD HH:mm:ss 형태면 그대로 반환 if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)) { return raw; } // 숫자만 추출 (구분자 제거) const digits = raw.replace(/\D/g, ''); if (digits.length >= 14) { const y = digits.slice(0, 4); const M = digits.slice(4, 6); const d = digits.slice(6, 8); const h = digits.slice(8, 10); const m = digits.slice(10, 12); const s = digits.slice(12, 14); return `${y}-${M}-${d} ${h}:${m}:${s}`; } // 파싱 불가하면 원본 반환 return raw; } /** * AVETDR 신호 플래그 표시 */ function SignalFlags({ ship }) { const isIntegrate = useShipStore((s) => s.isIntegrate); const isIntegratedShip = ship.targetId && ship.targetId.includes('_'); const useIntegratedMode = isIntegrate && isIntegratedShip; return ( ); } /** * 선박 사진 갤러리 * 이미지가 없으면 기본 이미지(default-ship.png) 표시 */ function ShipGallery({ imageUrlList }) { const [currentIndex, setCurrentIndex] = useState(0); const hasImages = imageUrlList && imageUrlList.length > 0; const images = hasImages ? imageUrlList : [defaultShipImg]; const total = images.length; const canSlide = total > 1; const handlePrev = useCallback(() => { setCurrentIndex((prev) => (prev === 0 ? total - 1 : prev - 1)); }, [total]); const handleNext = useCallback(() => { setCurrentIndex((prev) => (prev === total - 1 ? 0 : prev + 1)); }, [total]); const handleIndicatorClick = useCallback((index) => { setCurrentIndex(index); }, []); return (
{canSlide && ( <> )}
선박 이미지 { e.target.src = defaultShipImg; }} />
{canSlide && (
{images.map((_, i) => (
)}
); } /** * 단일 선박 상세 모달 * @param {Object} props.modal - { ship, id, initialPos } */ export default function ShipDetailModal({ modal }) { const closeDetailModal = useShipStore((s) => s.closeDetailModal); const updateModalPos = useShipStore((s) => s.updateModalPos); // 드래그 상태 - 초기 위치는 스토어에서 계산된 initialPos 사용 const [position, setPosition] = useState(() => ({ ...modal.initialPos })); const posRef = useRef(modal.initialPos); const dragging = useRef(false); const dragStart = useRef({ x: 0, y: 0 }); // 드래그 핸들러 const handleMouseDown = useCallback((e) => { dragging.current = true; dragStart.current = { x: e.clientX - position.x, y: e.clientY - position.y, }; e.preventDefault(); }, [position]); useEffect(() => { const handleMouseMove = (e) => { if (!dragging.current) return; const newPos = { x: e.clientX - dragStart.current.x, y: e.clientY - dragStart.current.y, }; posRef.current = newPos; setPosition(newPos); }; const handleMouseUp = () => { if (dragging.current) { dragging.current = false; // 드래그 종료 시 스토어에 현재 위치 보고 (ref에서 읽어서 render 중 setState 회피) updateModalPos(modal.id, posRef.current); } }; window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [modal.id, updateModalPos]); const { ship, id } = modal; const kindLabel = SHIP_KIND_LABELS[ship.signalKindCode] || '기타'; const sog = Number(ship.sog) || 0; const cog = Number(ship.cog) || 0; const isMoving = sog > SPEED_THRESHOLD; const draught = ship.draught ? `${(Number(ship.draught) / 10).toFixed(1)}m` : '-'; const formattedTime = formatDateTime(ship.receivedTime); return (
{/* header - 드래그 핸들 */}
{kindLabel} {ship.nationalCode && ( 국기 { e.target.style.display = 'none'; }} /> )} {ship.shipName || '-'} {ship.originalTargetId || '-'}
{/* gallery */} {/* body */}
  • 선박상태 {isMoving ? '항해' : '정박'}
    속도/항로 {sog.toFixed(1)} kn / {cog.toFixed(1)}°
    흘수 {draught}
{/* footer */}
데이터 수신시간 : {formattedTime}
); }