import { useEffect, useRef } from 'react' import { useMap } from '@vis.gl/react-maplibre' import type { OceanForecastData } from '../services/khoaApi' interface OceanForecastOverlayProps { forecast: OceanForecastData | null opacity?: number visible?: boolean } // 한국 해역 범위 [lon, lat] const BOUNDS = { nw: [124.5, 38.5] as [number, number], ne: [132.0, 38.5] as [number, number], se: [132.0, 33.0] as [number, number], sw: [124.5, 33.0] as [number, number], } // www.khoa.go.kr 이미지는 CORS 미지원 → Vite 프록시 경유 function toProxyUrl(url: string): string { return url.replace('https://www.khoa.go.kr', '') } /** * OceanForecastOverlay * * MapLibre raster layer는 deck.gl 캔버스보다 항상 아래 렌더링되므로, * WindParticleLayer와 동일하게 canvas를 직접 map 컨테이너에 삽입하는 방식 사용. * z-index 500으로 WindParticleLayer(450) 위에 렌더링. */ export function OceanForecastOverlay({ forecast, opacity = 0.6, visible = true, }: OceanForecastOverlayProps) { const { current: mapRef } = useMap() const canvasRef = useRef(null) const imgRef = useRef(null) useEffect(() => { const map = mapRef?.getMap() if (!map) return const container = map.getContainer() // canvas 생성 (최초 1회) if (!canvasRef.current) { const canvas = document.createElement('canvas') canvas.style.position = 'absolute' canvas.style.top = '0' canvas.style.left = '0' canvas.style.pointerEvents = 'none' canvas.style.zIndex = '500' // WindParticleLayer(450) 위 container.appendChild(canvas) canvasRef.current = canvas } const canvas = canvasRef.current if (!visible || !forecast?.imgFilePath) { canvas.style.display = 'none' return } canvas.style.display = 'block' const proxyUrl = toProxyUrl(forecast.imgFilePath) function draw() { const img = imgRef.current if (!canvas || !img || !img.complete || img.naturalWidth === 0) return const { clientWidth: w, clientHeight: h } = container canvas.width = w canvas.height = h const ctx = canvas.getContext('2d') if (!ctx) return ctx.clearRect(0, 0, w, h) // 4개 꼭짓점을 픽셀 좌표로 변환 const nw = map!.project(BOUNDS.nw) const ne = map!.project(BOUNDS.ne) const sw = map!.project(BOUNDS.sw) const x = Math.min(nw.x, sw.x) const y = nw.y const w2 = ne.x - nw.x const h2 = sw.y - nw.y ctx.globalAlpha = opacity ctx.drawImage(img, x, y, w2, h2) } // 이미지가 바뀌었으면 새로 로드 if (!imgRef.current || imgRef.current.dataset.src !== proxyUrl) { const img = new Image() img.dataset.src = proxyUrl img.onload = draw img.src = proxyUrl imgRef.current = img } else { draw() } map.on('move', draw) map.on('zoom', draw) map.on('resize', draw) return () => { map.off('move', draw) map.off('zoom', draw) map.off('resize', draw) } }, [mapRef, visible, forecast?.imgFilePath, opacity]) // 언마운트 시 canvas 제거 useEffect(() => { return () => { canvasRef.current?.remove() canvasRef.current = null } }, []) return null }