release: 2026-03-01 (6건 커밋) #69

병합
htlee develop 에서 main 로 6 commits 를 머지했습니다 2026-03-03 08:51:08 +09:00
5개의 변경된 파일141개의 추가작업 그리고 6개의 파일을 삭제
Showing only changes of commit aba58b2227 - Show all commits

파일 보기

@ -10,9 +10,11 @@
"dependencies": {
"@deck.gl/aggregation-layers": "^9.2.10",
"@deck.gl/core": "^9.2.10",
"@deck.gl/extensions": "^9.2.10",
"@deck.gl/geo-layers": "^9.2.10",
"@deck.gl/layers": "^9.2.10",
"@deck.gl/mapbox": "^9.2.10",
"@deck.gl/mesh-layers": "^9.2.10",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@ -387,6 +389,22 @@
"mjolnir.js": "^3.0.0"
}
},
"node_modules/@deck.gl/extensions": {
"version": "9.2.10",
"resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.10.tgz",
"integrity": "sha512-GMKmps67kX2d4nMbEZYDxGDZWmHDQJkFa9YbL/kSbTyt8OpFe9H5zTuqNog3l75F9Fyop/nq7bQYD2pKOUGPBg==",
"license": "MIT",
"dependencies": {
"@luma.gl/constants": "^9.2.6",
"@luma.gl/shadertools": "^9.2.6",
"@math.gl/core": "^4.1.0"
},
"peerDependencies": {
"@deck.gl/core": "~9.2.0",
"@luma.gl/core": "~9.2.6",
"@luma.gl/engine": "~9.2.6"
}
},
"node_modules/@deck.gl/geo-layers": {
"version": "9.2.10",
"resolved": "https://registry.npmjs.org/@deck.gl/geo-layers/-/geo-layers-9.2.10.tgz",
@ -459,6 +477,25 @@
"@math.gl/web-mercator": "^4.1.0"
}
},
"node_modules/@deck.gl/mesh-layers": {
"version": "9.2.10",
"resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.2.10.tgz",
"integrity": "sha512-YeVIWnODdFkz5yNYzpCtzF6wRhBpe2Gg+oc1hNNrRwdQUV3bsRLJUO13SKNagUKWIbey9K/c4ps2Ns0Dg2AzGg==",
"license": "MIT",
"dependencies": {
"@loaders.gl/gltf": "^4.3.4",
"@loaders.gl/schema": "^4.3.4",
"@luma.gl/gltf": "^9.2.6",
"@luma.gl/shadertools": "^9.2.6"
},
"peerDependencies": {
"@deck.gl/core": "~9.2.0",
"@luma.gl/core": "~9.2.6",
"@luma.gl/engine": "~9.2.6",
"@luma.gl/gltf": "~9.2.6",
"@luma.gl/shadertools": "~9.2.6"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",

파일 보기

@ -12,9 +12,11 @@
"dependencies": {
"@deck.gl/aggregation-layers": "^9.2.10",
"@deck.gl/core": "^9.2.10",
"@deck.gl/extensions": "^9.2.10",
"@deck.gl/geo-layers": "^9.2.10",
"@deck.gl/layers": "^9.2.10",
"@deck.gl/mapbox": "^9.2.10",
"@deck.gl/mesh-layers": "^9.2.10",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",

파일 보기

@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useMemo } from 'react'
import type { MainTab } from '../../types/navigation'
import { useAuthStore } from '../../store/authStore'
import { useMenuStore } from '../../store/menuStore'
import { useMapStore } from '../../store/mapStore'
interface TopBarProps {
activeTab: MainTab
@ -10,10 +11,10 @@ interface TopBarProps {
export function TopBar({ activeTab, onTabChange }: TopBarProps) {
const [showQuickMenu, setShowQuickMenu] = useState(false)
const [mapToggles, setMapToggles] = useState({ s57: true, s101: false, threeD: false, satellite: false })
const quickMenuRef = useRef<HTMLDivElement>(null)
const { hasPermission, user, logout } = useAuthStore()
const { menuConfig, isLoaded } = useMenuStore()
const { mapToggles, toggleMap } = useMapStore()
const tabs = useMemo(() => {
if (!isLoaded || menuConfig.length === 0) return []
@ -31,8 +32,6 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
return () => document.removeEventListener('mousedown', handler)
}, [showQuickMenu])
const toggleMap = (key: keyof typeof mapToggles) => setMapToggles(p => ({ ...p, [key]: !p[key] }))
return (
<div className="h-[52px] bg-bg-1 border-b border-border flex items-center justify-between px-5 relative z-[100]">
{/* Left Section */}

파일 보기

@ -13,8 +13,10 @@ import type { BoomLine, BoomLineCoord } from '@common/types/boomLine'
import type { ReplayShip, CollisionEvent } from '@common/types/backtrack'
import { createBacktrackLayers } from './BacktrackReplayOverlay'
import { hexToRgba } from './mapUtils'
import { useMapStore } from '@common/store/mapStore'
const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080'
const VWORLD_API_KEY = import.meta.env.VITE_VWORLD_API_KEY || ''
// 남해안 중심 좌표 (여수 앞바다)
const DEFAULT_CENTER: [number, number] = [34.5, 127.8]
@ -46,6 +48,59 @@ const BASE_STYLE: StyleSpecification = {
],
}
// 3D 위성 스타일 (VWorld 위성 배경 + OpenFreeMap 건물 extrusion)
// VWorld WMTS: {z}/{y}/{x} (row/col 순서)
// OpenFreeMap: CORS 허용, 전 세계 OSM 벡터타일 (building: render_height 포함)
const SATELLITE_3D_STYLE: StyleSpecification = {
version: 8,
sources: {
'vworld-satellite': {
type: 'raster',
tiles: [`https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Satellite/{z}/{y}/{x}.jpeg`],
tileSize: 256,
attribution: '&copy; <a href="https://www.vworld.kr">국토지리정보원 VWorld</a>',
},
'ofm': {
type: 'vector',
url: 'https://tiles.openfreemap.org/planet',
},
},
layers: [
{
id: 'satellite-base',
type: 'raster',
source: 'vworld-satellite',
minzoom: 0,
maxzoom: 22,
},
{
id: 'roads-3d',
type: 'line',
source: 'ofm',
'source-layer': 'transportation',
filter: ['in', ['get', 'class'], ['literal', ['motorway', 'trunk', 'primary', 'secondary']]],
paint: {
'line-color': 'rgba(255,255,200,0.3)',
'line-width': ['interpolate', ['linear'], ['zoom'], 10, 1, 16, 3],
},
},
{
id: '3d-buildings',
type: 'fill-extrusion',
source: 'ofm',
'source-layer': 'building',
minzoom: 13,
filter: ['!=', ['get', 'hide_3d'], true],
paint: {
'fill-extrusion-color': '#c8b99a',
'fill-extrusion-height': ['max', ['coalesce', ['get', 'render_height'], 0], 3],
'fill-extrusion-base': ['coalesce', ['get', 'render_min_height'], 0],
'fill-extrusion-opacity': 0.85,
},
},
],
}
// 모델별 색상 매핑
const MODEL_COLORS: Record<PredictionModel, string> = {
'KOSPS': '#06b6d4',
@ -124,7 +179,7 @@ interface MapViewProps {
sensitiveResources?: SensitiveResource[]
}
// deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록)
// deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록, interleaved)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function DeckGLOverlay({ layers }: { layers: any[] }) {
const overlay = useControl<MapboxOverlay>(() => new MapboxOverlay({ interleaved: true }))
@ -132,6 +187,20 @@ function DeckGLOverlay({ layers }: { layers: any[] }) {
return null
}
// 3D 모드 pitch/bearing 제어 컴포넌트 (Map 내부에서 useMap() 사용)
function MapPitchController({ threeD }: { threeD: boolean }) {
const { current: map } = useMap()
useEffect(() => {
if (!map) return
map.easeTo(
threeD
? { pitch: 45, bearing: -17, duration: 800 }
: { pitch: 0, bearing: 0, duration: 800 }
)
}, [threeD, map])
return null
}
// 팝업 정보
interface PopupInfo {
longitude: number
@ -157,6 +226,7 @@ export function MapView({
backtrackReplay,
sensitiveResources = [],
}: MapViewProps) {
const { mapToggles } = useMapStore()
const [currentPosition, setCurrentPosition] = useState<[number, number]>(DEFAULT_CENTER)
const [currentTime, setCurrentTime] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
@ -535,6 +605,9 @@ export function MapView({
sensitiveResources,
])
// 3D 모드에 따른 지도 스타일 전환
const currentMapStyle = mapToggles.threeD ? SATELLITE_3D_STYLE : BASE_STYLE
return (
<div className="w-full h-full relative">
<Map
@ -543,12 +616,15 @@ export function MapView({
latitude: center[0],
zoom: zoom,
}}
mapStyle={BASE_STYLE}
mapStyle={currentMapStyle}
className="w-full h-full"
style={{ cursor: isSelectingLocation ? 'crosshair' : 'grab' }}
onClick={handleMapClick}
attributionControl={false}
>
{/* 3D 모드 pitch 제어 */}
<MapPitchController threeD={mapToggles.threeD} />
{/* WMS 레이어 */}
{wmsLayers.map(layer => (
<Source
@ -572,7 +648,7 @@ export function MapView({
</Source>
))}
{/* deck.gl 오버레이 */}
{/* deck.gl 오버레이 (인터리브드: 일반 레이어) */}
<DeckGLOverlay layers={deckLayers} />
{/* 사고 위치 마커 (MapLibre Marker) */}

파일 보기

@ -0,0 +1,21 @@
import { create } from 'zustand'
interface MapToggles {
s57: boolean;
s101: boolean;
threeD: boolean;
satellite: boolean;
}
interface MapState {
mapToggles: MapToggles;
toggleMap: (key: keyof MapToggles) => void;
}
export const useMapStore = create<MapState>((set) => ({
mapToggles: { s57: true, s101: false, threeD: false, satellite: false },
toggleMap: (key) =>
set((s) => ({
mapToggles: { ...s.mapToggles, [key]: !s.mapToggles[key] },
})),
}))