import { useState, useEffect, useRef } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import type { ScatSegment } from './scatTypes' import { esiColor, jejuCoastCoords, scatDetailData } from './scatConstants' interface ScatMapProps { segments: ScatSegment[] selectedSeg: ScatSegment onSelectSeg: (s: ScatSegment) => void onOpenPopup: (idx: number) => void } function ScatMap({ segments, selectedSeg, onSelectSeg, onOpenPopup, }: ScatMapProps) { const mapContainerRef = useRef(null) const mapRef = useRef(null) const markersRef = useRef(null) const [zoom, setZoom] = useState(10) useEffect(() => { if (!mapContainerRef.current || mapRef.current) return const map = L.map(mapContainerRef.current, { center: [33.38, 126.55], zoom: 10, zoomControl: false, attributionControl: false, }) L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, }).addTo(map) L.control.zoom({ position: 'bottomright' }).addTo(map) L.control.attribution({ position: 'bottomleft' }).addAttribution( '© OSM © CARTO' ).addTo(map) map.on('zoomend', () => setZoom(map.getZoom())) mapRef.current = map markersRef.current = L.layerGroup().addTo(map) setTimeout(() => map.invalidateSize(), 100) return () => { map.remove() mapRef.current = null markersRef.current = null } }, []) useEffect(() => { if (!mapRef.current || !markersRef.current) return markersRef.current.clearLayers() // 줌 기반 스케일 계수 (zoom 10=아일랜드뷰 → 14+=클로즈업) const zScale = Math.max(0, (zoom - 9)) / 5 // 0 at z9, 1 at z14 const polyWeight = 1 + zScale * 4 // 1 ~ 5 const selPolyWeight = 2 + zScale * 5 // 2 ~ 7 const glowWeight = 4 + zScale * 14 // 4 ~ 18 const halfLenScale = 0.15 + zScale * 0.85 // 0.15 ~ 1.0 const markerSize = Math.round(6 + zScale * 16) // 6px ~ 22px const markerBorder = zoom >= 13 ? 2 : 1 const markerFontSize = Math.round(4 + zScale * 6) // 4px ~ 10px const showStatusMarker = zoom >= 11 const showStatusText = zoom >= 13 // 제주도 해안선 레퍼런스 라인 const coastline = L.polyline(jejuCoastCoords as [number, number][], { color: 'rgba(6, 182, 212, 0.18)', weight: 1.5, dashArray: '8, 6', }) markersRef.current.addLayer(coastline) segments.forEach(seg => { const isSelected = selectedSeg.id === seg.id const color = esiColor(seg.esiNum) // 해안선 방향 계산 (세그먼트 폴리라인 각도 결정) const coastIdx = seg.id % (jejuCoastCoords.length - 1) const [clat1, clng1] = jejuCoastCoords[coastIdx] const [clat2, clng2] = jejuCoastCoords[(coastIdx + 1) % jejuCoastCoords.length] const dlat = clat2 - clat1 const dlng = clng2 - clng1 const dist = Math.sqrt(dlat * dlat + dlng * dlng) const nDlat = dist > 0 ? dlat / dist : 0 const nDlng = dist > 0 ? dlng / dist : 1 // 구간 길이를 위경도 단위로 변환 (줌 레벨에 따라 스케일링) const halfLen = Math.min(seg.lengthM / 200000, 0.012) * halfLenScale // 해안선 방향을 따라 폴리라인 좌표 생성 const segCoords: [number, number][] = [ [seg.lat - nDlat * halfLen, seg.lng - nDlng * halfLen], [seg.lat, seg.lng], [seg.lat + nDlat * halfLen, seg.lng + nDlng * halfLen], ] // 선택된 구간 글로우 효과 if (isSelected) { const glow = L.polyline(segCoords, { color: '#22c55e', weight: glowWeight, opacity: 0.15, lineCap: 'round', }) markersRef.current!.addLayer(glow) } // ESI 색상 구간 폴리라인 const polyline = L.polyline(segCoords, { color: isSelected ? '#22c55e' : color, weight: isSelected ? selPolyWeight : polyWeight, opacity: isSelected ? 0.95 : 0.7, lineCap: 'round', lineJoin: 'round', }) const statusIcon = seg.status === '완료' ? '✓' : seg.status === '진행중' ? '⏳' : '—' polyline.bindTooltip( `
${seg.code} ${seg.area}
ESI ${seg.esi} · ${seg.length} · ${statusIcon} ${seg.status}
`, { permanent: isSelected, direction: 'top', offset: [0, -10], className: 'scat-map-tooltip', } ) polyline.on('click', () => { onSelectSeg(seg) onOpenPopup(seg.id % scatDetailData.length) }) markersRef.current!.addLayer(polyline) // 조사 상태 마커 (DivIcon) — 줌 레벨에 따라 표시/크기 조절 if (showStatusMarker) { const stColor = seg.status === '완료' ? '#22c55e' : seg.status === '진행중' ? '#eab308' : '#64748b' const stBg = seg.status === '완료' ? 'rgba(34,197,94,0.2)' : seg.status === '진행중' ? 'rgba(234,179,8,0.2)' : 'rgba(100,116,139,0.2)' const stText = seg.status === '완료' ? '✓' : seg.status === '진행중' ? '⏳' : '—' const half = Math.round(markerSize / 2) const statusMarker = L.marker([seg.lat, seg.lng], { icon: L.divIcon({ className: '', html: `
${showStatusText ? stText : ''}
`, iconSize: [0, 0], }), }) statusMarker.on('click', () => { onSelectSeg(seg) onOpenPopup(seg.id % scatDetailData.length) }) markersRef.current!.addLayer(statusMarker) } }) }, [segments, selectedSeg, onSelectSeg, onOpenPopup, zoom]) useEffect(() => { if (!mapRef.current) return mapRef.current.flyTo([selectedSeg.lat, selectedSeg.lng], 12, { duration: 0.6 }) }, [selectedSeg]) const doneCount = segments.filter(s => s.status === '완료').length const progCount = segments.filter(s => s.status === '진행중').length const totalLen = segments.reduce((a, s) => a + s.lengthM, 0) const doneLen = segments.filter(s => s.status === '완료').reduce((a, s) => a + s.lengthM, 0) const highSens = segments.filter(s => s.sensitivity === '최상' || s.sensitivity === '상').reduce((a, s) => a + s.lengthM, 0) const donePct = Math.round(doneCount / segments.length * 100) const progPct = Math.round(progCount / segments.length * 100) const notPct = 100 - donePct - progPct return (
{/* Status chips */}
Pre-SCAT 사전조사
제주도 — 해양경비안전서 관할 해안 · {segments.length}개 구간
{/* Right info cards */}
{/* ESI Legend */}
ESI 민감도 분류 범례
{[ { esi: 'ESI 10', label: '갯벌·습지·맹그로브', color: '#991b1b' }, { esi: 'ESI 9', label: '쉘터 갯벌', color: '#b91c1c' }, { esi: 'ESI 8', label: '쉘터 암반 해안', color: '#dc2626' }, { esi: 'ESI 7', label: '노출 갯벌', color: '#ef4444' }, { esi: 'ESI 6', label: '자갈·혼합 해안', color: '#f97316' }, { esi: 'ESI 5', label: '혼합 모래/자갈', color: '#fb923c' }, { esi: 'ESI 3-4', label: '모래 해안', color: '#facc15' }, { esi: 'ESI 1-2', label: '암반·인공 구조물', color: '#4ade80' }, ].map((item, i) => (
{item.label} {item.esi}
))}
{/* Progress */}
조사 진행률
완료 {donePct}% 진행 {progPct}% 미조사 {notPct}%
{[ ['총 해안선', `${(totalLen / 1000).toFixed(1)} km`, ''], ['조사 완료', `${(doneLen / 1000).toFixed(1)} km`, 'var(--green)'], ['고민감 구간', `${(highSens / 1000).toFixed(1)} km`, 'var(--red)'], ['방제 우선 구간', `${segments.filter(s => s.sensitivity === '최상').length}개`, 'var(--orange)'], ].map(([label, val, color], i) => (
{label} {val}
))}
{/* Coordinates */}
위도 {selectedSeg.lat.toFixed(4)}°N 경도 {selectedSeg.lng.toFixed(4)}°E 축척 1:25,000
) } export default ScatMap