Compare commits
No commits in common. "fbdf0e9122eda946541e93a00fa9351bb6a1f7ea" and "7921bfef965ee83eb9677894aa4b902b014f089c" have entirely different histories.
fbdf0e9122
...
7921bfef96
@ -54,28 +54,6 @@ router.get('/vworld/:z/:y/:x', async (req, res) => {
|
|||||||
await proxyUpstream(tileUrl, res, 'image/jpeg');
|
await proxyUpstream(tileUrl, res, 'image/jpeg');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── SR 민감자원 벡터타일 ───
|
|
||||||
|
|
||||||
// GET /api/tiles/sr/tilejson — SR TileJSON 프록시 (source-layer 메타데이터)
|
|
||||||
router.get('/sr/tilejson', async (_req, res) => {
|
|
||||||
await proxyUpstream(`${ENC_UPSTREAM}/sr`, res, 'application/json');
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/tiles/sr/style — SR 스타일 JSON 프록시 (레이어별 type/paint/layout 정의)
|
|
||||||
router.get('/sr/style', async (_req, res) => {
|
|
||||||
await proxyUpstream(`${ENC_UPSTREAM}/style/sr`, res, 'application/json');
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/tiles/sr/:z/:x/:y — SR(민감자원) 벡터타일 프록시
|
|
||||||
router.get('/sr/:z/:x/:y', async (req, res) => {
|
|
||||||
const { z, x, y } = req.params;
|
|
||||||
if (!/^\d+$/.test(z) || !/^\d+$/.test(x) || !/^\d+$/.test(y)) {
|
|
||||||
res.status(400).json({ error: '잘못된 타일 좌표' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await proxyUpstream(`${ENC_UPSTREAM}/sr/${z}/${x}/${y}`, res, 'application/x-protobuf');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── S-57 전자해도 (ENC) ───
|
// ─── S-57 전자해도 (ENC) ───
|
||||||
// tiles.gcnautical.com CORS 제한 우회를 위한 프록시 엔드포인트 그룹
|
// tiles.gcnautical.com CORS 제한 우회를 위한 프록시 엔드포인트 그룹
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import { MeasureOverlay } from './MeasureOverlay'
|
|||||||
import { useMeasureTool } from '@common/hooks/useMeasureTool'
|
import { useMeasureTool } from '@common/hooks/useMeasureTool'
|
||||||
import { hexToRgba } from './mapUtils'
|
import { hexToRgba } from './mapUtils'
|
||||||
import { S57EncOverlay } from './S57EncOverlay'
|
import { S57EncOverlay } from './S57EncOverlay'
|
||||||
import { SrOverlay } from './SrOverlay'
|
|
||||||
import { useMapStore } from '@common/store/mapStore'
|
import { useMapStore } from '@common/store/mapStore'
|
||||||
|
import { useThemeStore } from '@common/store/themeStore'
|
||||||
import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle'
|
import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle'
|
||||||
|
|
||||||
const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080'
|
const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080'
|
||||||
@ -113,7 +113,6 @@ interface MapViewProps {
|
|||||||
drawingPoints?: BoomLineCoord[]
|
drawingPoints?: BoomLineCoord[]
|
||||||
layerOpacity?: number
|
layerOpacity?: number
|
||||||
layerBrightness?: number
|
layerBrightness?: number
|
||||||
layerColors?: Record<string, string>
|
|
||||||
backtrackReplay?: {
|
backtrackReplay?: {
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
ships: ReplayShip[]
|
ships: ReplayShip[]
|
||||||
@ -307,7 +306,6 @@ export function MapView({
|
|||||||
drawingPoints = [],
|
drawingPoints = [],
|
||||||
layerOpacity = 50,
|
layerOpacity = 50,
|
||||||
layerBrightness = 50,
|
layerBrightness = 50,
|
||||||
layerColors,
|
|
||||||
backtrackReplay,
|
backtrackReplay,
|
||||||
sensitiveResources = [],
|
sensitiveResources = [],
|
||||||
sensitiveResourceGeojson,
|
sensitiveResourceGeojson,
|
||||||
@ -331,7 +329,7 @@ export function MapView({
|
|||||||
analysisCircleRadiusM = 0,
|
analysisCircleRadiusM = 0,
|
||||||
showOverlays = true,
|
showOverlays = true,
|
||||||
}: MapViewProps) {
|
}: MapViewProps) {
|
||||||
const lightMode = true
|
const lightMode = useThemeStore((s) => s.theme) === 'light'
|
||||||
const { mapToggles, measureMode, measureInProgress, measurements } = useMapStore()
|
const { mapToggles, measureMode, measureInProgress, measurements } = useMapStore()
|
||||||
const { handleMeasureClick } = useMeasureTool()
|
const { handleMeasureClick } = useMeasureTool()
|
||||||
const isControlled = externalCurrentTime !== undefined
|
const isControlled = externalCurrentTime !== undefined
|
||||||
@ -1137,9 +1135,6 @@ export function MapView({
|
|||||||
{/* S-57 전자해도 오버레이 (공식 style.json 기반) */}
|
{/* S-57 전자해도 오버레이 (공식 style.json 기반) */}
|
||||||
<S57EncOverlay visible={mapToggles['s57'] ?? false} />
|
<S57EncOverlay visible={mapToggles['s57'] ?? false} />
|
||||||
|
|
||||||
{/* SR 민감자원 벡터타일 오버레이 */}
|
|
||||||
<SrOverlay enabledLayers={enabledLayers} opacity={layerOpacity} layerColors={layerColors} />
|
|
||||||
|
|
||||||
{/* WMS 레이어 */}
|
{/* WMS 레이어 */}
|
||||||
{wmsLayers.map(layer => (
|
{wmsLayers.map(layer => (
|
||||||
<Source
|
<Source
|
||||||
|
|||||||
@ -1,329 +0,0 @@
|
|||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
||||||
import { useMap } from '@vis.gl/react-maplibre';
|
|
||||||
import { API_BASE_URL } from '../../services/api';
|
|
||||||
import { useLayerTree } from '../../hooks/useLayers';
|
|
||||||
import type { Layer } from '../../services/layerService';
|
|
||||||
import { getOpacityProp, getColorProp } from './srStyles';
|
|
||||||
|
|
||||||
const SR_SOURCE_ID = 'sr';
|
|
||||||
const PROXY_PREFIX = `${API_BASE_URL}/tiles`;
|
|
||||||
|
|
||||||
// MapLibre 내부 요청은 절대 URL이 필요
|
|
||||||
const ABSOLUTE_PREFIX = API_BASE_URL.startsWith('http')
|
|
||||||
? PROXY_PREFIX
|
|
||||||
: `${window.location.origin}${PROXY_PREFIX}`;
|
|
||||||
|
|
||||||
// ─── SR 스타일 JSON (Martin style/sr) ───
|
|
||||||
|
|
||||||
interface SrStyleLayer {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
source: string;
|
|
||||||
'source-layer': string;
|
|
||||||
paint?: Record<string, unknown>;
|
|
||||||
layout?: Record<string, unknown>;
|
|
||||||
filter?: unknown;
|
|
||||||
minzoom?: number;
|
|
||||||
maxzoom?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SrStyle {
|
|
||||||
sources: Record<string, { type: string; tiles?: string[]; url?: string }>;
|
|
||||||
layers: SrStyleLayer[];
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedStyle: SrStyle | null = null;
|
|
||||||
|
|
||||||
async function loadSrStyle(): Promise<SrStyle> {
|
|
||||||
if (cachedStyle) return cachedStyle;
|
|
||||||
const res = await fetch(`${PROXY_PREFIX}/sr/style`);
|
|
||||||
if (!res.ok) throw new Error(`SR style fetch failed: ${res.status}`);
|
|
||||||
cachedStyle = await res.json();
|
|
||||||
return cachedStyle!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 헬퍼: wmsLayer(mpc:XXX)에서 코드 추출 ───
|
|
||||||
|
|
||||||
function extractCode(wmsLayer: string): string | null {
|
|
||||||
// mpc:468 → '468', mpc:386_spr → '386', mpc:kcg → 'kcg', mpc:kcg_ofi → 'kcg_ofi'
|
|
||||||
const match = wmsLayer.match(/^mpc:(.+?)(?:_(spr|sum|fal|win|apr))?$/);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── layerTree → SR 매핑 구축 ───
|
|
||||||
|
|
||||||
interface SrMapping {
|
|
||||||
layerCd: string; // DB LAYER_CD (예: 'LYR001002001004005')
|
|
||||||
code: string; // mpc: 뒤 코드 (예: '468', 'kcg', '3')
|
|
||||||
name: string; // DB 레이어명 (예: '갯벌', '경찰청', '군산')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── source-layer → DB layerCd 매칭 ───
|
|
||||||
|
|
||||||
function matchSourceLayer(sourceLayer: string, mappings: SrMapping[]): string[] {
|
|
||||||
// 1차: 숫자 접두사 매칭 (468_갯벌 → code '468')
|
|
||||||
const numMatch = sourceLayer.match(/^(\d+)/);
|
|
||||||
if (numMatch) {
|
|
||||||
const code = numMatch[1];
|
|
||||||
const matched = mappings.filter(m => m.code === code);
|
|
||||||
if (matched.length > 0) return matched.map(m => m.layerCd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2차: 이름 정확 일치 (경찰청 = 경찰청)
|
|
||||||
const exactMatch = mappings.filter(m => sourceLayer === m.name);
|
|
||||||
if (exactMatch.length > 0) return exactMatch.map(m => m.layerCd);
|
|
||||||
|
|
||||||
// 3차: 접미사 일치 (해경관할구역-군산 → name '군산')
|
|
||||||
const suffixMatch = mappings.filter(m =>
|
|
||||||
sourceLayer.endsWith(`-${m.name}`) || sourceLayer.endsWith(`_${m.name}`)
|
|
||||||
);
|
|
||||||
if (suffixMatch.length > 0) return suffixMatch.map(m => m.layerCd);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSrMappings(layers: Layer[]): SrMapping[] {
|
|
||||||
const result: SrMapping[] = [];
|
|
||||||
function traverse(nodes: Layer[]) {
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (node.wmsLayer) {
|
|
||||||
const code = extractCode(node.wmsLayer);
|
|
||||||
if (code) {
|
|
||||||
result.push({ layerCd: node.id, code, name: node.name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.children) traverse(node.children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
traverse(layers);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 컴포넌트 ───
|
|
||||||
|
|
||||||
interface SrOverlayProps {
|
|
||||||
enabledLayers: Set<string>;
|
|
||||||
opacity?: number;
|
|
||||||
layerColors?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SrOverlay({ enabledLayers, opacity = 100, layerColors }: SrOverlayProps) {
|
|
||||||
const { current: mapRef } = useMap();
|
|
||||||
const { data: layerTree } = useLayerTree();
|
|
||||||
const addedLayersRef = useRef<Set<string>>(new Set());
|
|
||||||
const sourceAddedRef = useRef(false);
|
|
||||||
const [style, setStyle] = useState<SrStyle | null>(cachedStyle);
|
|
||||||
|
|
||||||
// 스타일 JSON 로드 (최초 1회)
|
|
||||||
useEffect(() => {
|
|
||||||
if (style) return;
|
|
||||||
loadSrStyle()
|
|
||||||
.then(setStyle)
|
|
||||||
.catch((err) => console.error('[SrOverlay] SR 스타일 로드 실패:', err));
|
|
||||||
}, [style]);
|
|
||||||
|
|
||||||
const ensureSource = useCallback((map: maplibregl.Map) => {
|
|
||||||
if (sourceAddedRef.current) return;
|
|
||||||
if (map.getSource(SR_SOURCE_ID)) {
|
|
||||||
sourceAddedRef.current = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
map.addSource(SR_SOURCE_ID, {
|
|
||||||
type: 'vector',
|
|
||||||
tiles: [`${ABSOLUTE_PREFIX}/sr/{z}/{x}/{y}`],
|
|
||||||
maxzoom: 14,
|
|
||||||
});
|
|
||||||
sourceAddedRef.current = true;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeAll = useCallback((map: maplibregl.Map) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
if (!(map as any).style) {
|
|
||||||
addedLayersRef.current.clear();
|
|
||||||
sourceAddedRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const id of addedLayersRef.current) {
|
|
||||||
if (map.getLayer(id)) map.removeLayer(id);
|
|
||||||
}
|
|
||||||
addedLayersRef.current.clear();
|
|
||||||
if (map.getSource(SR_SOURCE_ID)) map.removeSource(SR_SOURCE_ID);
|
|
||||||
sourceAddedRef.current = false;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// enabledLayers 변경 시 레이어 동기화
|
|
||||||
useEffect(() => {
|
|
||||||
const map = mapRef?.getMap();
|
|
||||||
if (!map || !layerTree || !style) return;
|
|
||||||
|
|
||||||
const mappings = buildSrMappings(layerTree);
|
|
||||||
|
|
||||||
// source-layer → DB layerCd[] 매핑
|
|
||||||
const sourceLayerToIds = new Map<string, string[]>();
|
|
||||||
for (const sl of style.layers) {
|
|
||||||
const ids = matchSourceLayer(sl['source-layer'], mappings);
|
|
||||||
if (ids.length > 0) sourceLayerToIds.set(sl['source-layer'], ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 커스텀 색상 조회 (source-layer name 기반)
|
|
||||||
const getCustomColor = (sourceLayer: string): string | undefined => {
|
|
||||||
const ids = sourceLayerToIds.get(sourceLayer);
|
|
||||||
if (!ids) return undefined;
|
|
||||||
for (const id of ids) {
|
|
||||||
const c = layerColors?.[id];
|
|
||||||
if (c) return c;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// style JSON 레이어 중 활성화된 DB 레이어에 해당하는 스타일 레이어 필터
|
|
||||||
const enabledStyleLayers = style.layers.filter(sl => {
|
|
||||||
const ids = sourceLayerToIds.get(sl['source-layer']);
|
|
||||||
return ids && ids.some(id => enabledLayers.has(id));
|
|
||||||
});
|
|
||||||
|
|
||||||
const syncLayers = () => {
|
|
||||||
ensureSource(map);
|
|
||||||
|
|
||||||
const activeLayerIds = new Set<string>();
|
|
||||||
|
|
||||||
// 활성화된 레이어 추가 또는 visible 설정
|
|
||||||
for (const sl of enabledStyleLayers) {
|
|
||||||
const layerId = `sr-${sl.id}`;
|
|
||||||
activeLayerIds.add(layerId);
|
|
||||||
|
|
||||||
const customColor = getCustomColor(sl['source-layer']);
|
|
||||||
const layerType = sl.type as 'fill' | 'line' | 'circle';
|
|
||||||
|
|
||||||
if (map.getLayer(layerId)) {
|
|
||||||
map.setLayoutProperty(layerId, 'visibility', 'visible');
|
|
||||||
// 기존 레이어에 커스텀 색상 적용
|
|
||||||
const colorProp = getColorProp(layerType);
|
|
||||||
if (customColor) {
|
|
||||||
map.setPaintProperty(layerId, colorProp, customColor);
|
|
||||||
} else {
|
|
||||||
const orig = sl.paint?.[colorProp];
|
|
||||||
if (orig !== undefined) map.setPaintProperty(layerId, colorProp, orig);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const opacityValue = opacity / 100;
|
|
||||||
const opacityProp = getOpacityProp(layerType);
|
|
||||||
const paint = { ...sl.paint, [opacityProp]: opacityValue };
|
|
||||||
|
|
||||||
// 커스텀 색상 적용
|
|
||||||
if (customColor) {
|
|
||||||
const colorProp = getColorProp(layerType);
|
|
||||||
paint[colorProp] = customColor;
|
|
||||||
if (sl.type === 'fill') {
|
|
||||||
paint['fill-outline-color'] = customColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
map.addLayer({
|
|
||||||
id: layerId,
|
|
||||||
type: sl.type,
|
|
||||||
source: SR_SOURCE_ID,
|
|
||||||
'source-layer': sl['source-layer'],
|
|
||||||
paint,
|
|
||||||
layout: { visibility: 'visible', ...sl.layout },
|
|
||||||
...(sl.filter ? { filter: sl.filter } : {}),
|
|
||||||
...(sl.minzoom !== undefined && { minzoom: sl.minzoom }),
|
|
||||||
...(sl.maxzoom !== undefined && { maxzoom: sl.maxzoom }),
|
|
||||||
} as maplibregl.AddLayerObject);
|
|
||||||
addedLayersRef.current.add(layerId);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`[SrOverlay] 레이어 추가 실패 (${sl.id}):`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 비활성화된 레이어 숨김
|
|
||||||
for (const layerId of addedLayersRef.current) {
|
|
||||||
if (!activeLayerIds.has(layerId)) {
|
|
||||||
if (map.getLayer(layerId)) {
|
|
||||||
map.setLayoutProperty(layerId, 'visibility', 'none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (map.isStyleLoaded()) {
|
|
||||||
syncLayers();
|
|
||||||
} else {
|
|
||||||
map.once('style.load', syncLayers);
|
|
||||||
return () => { map.off('style.load', syncLayers); };
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [enabledLayers, layerTree, style, mapRef, layerColors]);
|
|
||||||
|
|
||||||
// opacity 변경 시 paint 업데이트
|
|
||||||
useEffect(() => {
|
|
||||||
const map = mapRef?.getMap();
|
|
||||||
if (!map || !style) return;
|
|
||||||
|
|
||||||
const opacityValue = opacity / 100;
|
|
||||||
for (const layerId of addedLayersRef.current) {
|
|
||||||
if (!map.getLayer(layerId)) continue;
|
|
||||||
const originalId = layerId.replace(/^sr-/, '');
|
|
||||||
const sl = style.layers.find((l) => l.id === originalId);
|
|
||||||
if (sl) {
|
|
||||||
const prop = getOpacityProp(sl.type as 'fill' | 'line' | 'circle');
|
|
||||||
map.setPaintProperty(layerId, prop, opacityValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [opacity, mapRef]);
|
|
||||||
|
|
||||||
// layerColors 변경 시 paint 업데이트
|
|
||||||
useEffect(() => {
|
|
||||||
const map = mapRef?.getMap();
|
|
||||||
if (!map || !style || !layerTree) return;
|
|
||||||
|
|
||||||
const mappings = buildSrMappings(layerTree);
|
|
||||||
const sourceLayerToIds = new Map<string, string[]>();
|
|
||||||
for (const sl of style.layers) {
|
|
||||||
const ids = matchSourceLayer(sl['source-layer'], mappings);
|
|
||||||
if (ids.length > 0) sourceLayerToIds.set(sl['source-layer'], ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCustomColor = (sourceLayer: string): string | undefined => {
|
|
||||||
const ids = sourceLayerToIds.get(sourceLayer);
|
|
||||||
if (!ids) return undefined;
|
|
||||||
for (const id of ids) {
|
|
||||||
const c = layerColors?.[id];
|
|
||||||
if (c) return c;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const layerId of addedLayersRef.current) {
|
|
||||||
if (!map.getLayer(layerId)) continue;
|
|
||||||
const originalId = layerId.replace(/^sr-/, '');
|
|
||||||
const sl = style.layers.find((l) => l.id === originalId);
|
|
||||||
if (!sl) continue;
|
|
||||||
|
|
||||||
const customColor = getCustomColor(sl['source-layer']);
|
|
||||||
const layerType = sl.type as 'fill' | 'line' | 'circle';
|
|
||||||
const colorProp = getColorProp(layerType);
|
|
||||||
|
|
||||||
if (customColor) {
|
|
||||||
map.setPaintProperty(layerId, colorProp, customColor);
|
|
||||||
} else {
|
|
||||||
const orig = sl.paint?.[colorProp];
|
|
||||||
if (orig !== undefined) map.setPaintProperty(layerId, colorProp, orig as string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [layerColors, mapRef, style, layerTree]);
|
|
||||||
|
|
||||||
// cleanup on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
const map = mapRef?.getMap();
|
|
||||||
if (!map) return;
|
|
||||||
return () => { removeAll(map); };
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [mapRef]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// SR(민감자원) 벡터타일 헬퍼
|
|
||||||
// 스타일은 Martin style/sr JSON에서 동적 로드 (SrOverlay에서 사용)
|
|
||||||
|
|
||||||
/** opacity 속성 키를 레이어 타입에 따라 반환 */
|
|
||||||
export function getOpacityProp(type: 'fill' | 'line' | 'circle'): string {
|
|
||||||
switch (type) {
|
|
||||||
case 'fill': return 'fill-opacity';
|
|
||||||
case 'line': return 'line-opacity';
|
|
||||||
case 'circle': return 'circle-opacity';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** color 속성 키를 레이어 타입에 따라 반환 */
|
|
||||||
export function getColorProp(type: 'fill' | 'line' | 'circle'): string {
|
|
||||||
switch (type) {
|
|
||||||
case 'fill': return 'fill-color';
|
|
||||||
case 'line': return 'line-color';
|
|
||||||
case 'circle': return 'circle-color';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +1,19 @@
|
|||||||
import type { StyleSpecification } from 'maplibre-gl';
|
import type { StyleSpecification } from 'maplibre-gl';
|
||||||
import { useMapStore } from '@common/store/mapStore';
|
import { useMapStore } from '@common/store/mapStore';
|
||||||
|
import { useThemeStore } from '@common/store/themeStore';
|
||||||
import {
|
import {
|
||||||
|
BASE_STYLE,
|
||||||
LIGHT_STYLE,
|
LIGHT_STYLE,
|
||||||
SATELLITE_3D_STYLE,
|
SATELLITE_3D_STYLE,
|
||||||
ENC_EMPTY_STYLE,
|
ENC_EMPTY_STYLE,
|
||||||
} from '@common/components/map/mapStyles';
|
} from '@common/components/map/mapStyles';
|
||||||
|
|
||||||
export function useBaseMapStyle(): StyleSpecification {
|
export function useBaseMapStyle(): StyleSpecification {
|
||||||
|
const theme = useThemeStore((s) => s.theme);
|
||||||
const mapToggles = useMapStore((s) => s.mapToggles);
|
const mapToggles = useMapStore((s) => s.mapToggles);
|
||||||
|
|
||||||
if (mapToggles.s57) return ENC_EMPTY_STYLE;
|
if (mapToggles.s57) return ENC_EMPTY_STYLE;
|
||||||
if (mapToggles.threeD) return SATELLITE_3D_STYLE;
|
if (mapToggles.threeD) return SATELLITE_3D_STYLE;
|
||||||
return LIGHT_STYLE;
|
if (theme === 'light') return LIGHT_STYLE;
|
||||||
|
return BASE_STYLE;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { LayerTree } from '@common/components/layer/LayerTree';
|
import { LayerTree } from '@common/components/layer/LayerTree';
|
||||||
import { useLayerTree } from '@common/hooks/useLayers';
|
import { useLayerTree } from '@common/hooks/useLayers';
|
||||||
import type { Layer } from '@common/services/layerService';
|
import type { Layer } from '@common/services/layerService';
|
||||||
@ -11,8 +12,6 @@ interface InfoLayerSectionProps {
|
|||||||
onLayerOpacityChange: (val: number) => void;
|
onLayerOpacityChange: (val: number) => void;
|
||||||
layerBrightness: number;
|
layerBrightness: number;
|
||||||
onLayerBrightnessChange: (val: number) => void;
|
onLayerBrightnessChange: (val: number) => void;
|
||||||
layerColors: Record<string, string>;
|
|
||||||
onLayerColorChange: (layerId: string, color: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfoLayerSection = ({
|
const InfoLayerSection = ({
|
||||||
@ -24,12 +23,12 @@ const InfoLayerSection = ({
|
|||||||
onLayerOpacityChange,
|
onLayerOpacityChange,
|
||||||
layerBrightness,
|
layerBrightness,
|
||||||
onLayerBrightnessChange,
|
onLayerBrightnessChange,
|
||||||
layerColors,
|
|
||||||
onLayerColorChange,
|
|
||||||
}: InfoLayerSectionProps) => {
|
}: InfoLayerSectionProps) => {
|
||||||
// API에서 레이어 트리 데이터 가져오기 (관리자 설정 USE_YN='Y' 레이어만 반환)
|
// API에서 레이어 트리 데이터 가져오기 (관리자 설정 USE_YN='Y' 레이어만 반환)
|
||||||
const { data: layerTree, isLoading } = useLayerTree();
|
const { data: layerTree, isLoading } = useLayerTree();
|
||||||
|
|
||||||
|
const [layerColors, setLayerColors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 관리자에서 사용여부가 ON인 레이어만 표시 (정적 폴백 없음)
|
// 관리자에서 사용여부가 ON인 레이어만 표시 (정적 폴백 없음)
|
||||||
const effectiveLayers: Layer[] = layerTree ?? [];
|
const effectiveLayers: Layer[] = layerTree ?? [];
|
||||||
|
|
||||||
@ -135,7 +134,7 @@ const InfoLayerSection = ({
|
|||||||
enabledLayers={enabledLayers}
|
enabledLayers={enabledLayers}
|
||||||
onToggleLayer={onToggleLayer}
|
onToggleLayer={onToggleLayer}
|
||||||
layerColors={layerColors}
|
layerColors={layerColors}
|
||||||
onColorChange={onLayerColorChange}
|
onColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -108,8 +108,6 @@ export function LeftPanel({
|
|||||||
onLayerOpacityChange,
|
onLayerOpacityChange,
|
||||||
layerBrightness,
|
layerBrightness,
|
||||||
onLayerBrightnessChange,
|
onLayerBrightnessChange,
|
||||||
layerColors,
|
|
||||||
onLayerColorChange,
|
|
||||||
sensitiveResources = [],
|
sensitiveResources = [],
|
||||||
onImageAnalysisResult,
|
onImageAnalysisResult,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
@ -347,8 +345,6 @@ export function LeftPanel({
|
|||||||
onLayerOpacityChange={onLayerOpacityChange}
|
onLayerOpacityChange={onLayerOpacityChange}
|
||||||
layerBrightness={layerBrightness}
|
layerBrightness={layerBrightness}
|
||||||
onLayerBrightnessChange={onLayerBrightnessChange}
|
onLayerBrightnessChange={onLayerBrightnessChange}
|
||||||
layerColors={layerColors}
|
|
||||||
onLayerColorChange={onLayerColorChange}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Oil Boom Placement Guide Section */}
|
{/* Oil Boom Placement Guide Section */}
|
||||||
|
|||||||
@ -216,10 +216,9 @@ export function OilSpillView() {
|
|||||||
const [drawingPoints, setDrawingPoints] = useState<BoomLineCoord[]>([]);
|
const [drawingPoints, setDrawingPoints] = useState<BoomLineCoord[]>([]);
|
||||||
const [containmentResult, setContainmentResult] = useState<ContainmentResult | null>(null);
|
const [containmentResult, setContainmentResult] = useState<ContainmentResult | null>(null);
|
||||||
|
|
||||||
// 레이어 스타일 (투명도 / 밝기 / 색상)
|
// 레이어 스타일 (투명도 / 밝기)
|
||||||
const [layerOpacity, setLayerOpacity] = useState(50);
|
const [layerOpacity, setLayerOpacity] = useState(50);
|
||||||
const [layerBrightness, setLayerBrightness] = useState(50);
|
const [layerBrightness, setLayerBrightness] = useState(50);
|
||||||
const [layerColors, setLayerColors] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
// 표시 정보 제어
|
// 표시 정보 제어
|
||||||
const [displayControls, setDisplayControls] = useState<DisplayControls>({
|
const [displayControls, setDisplayControls] = useState<DisplayControls>({
|
||||||
@ -1201,8 +1200,6 @@ export function OilSpillView() {
|
|||||||
onLayerOpacityChange={setLayerOpacity}
|
onLayerOpacityChange={setLayerOpacity}
|
||||||
layerBrightness={layerBrightness}
|
layerBrightness={layerBrightness}
|
||||||
onLayerBrightnessChange={setLayerBrightness}
|
onLayerBrightnessChange={setLayerBrightness}
|
||||||
layerColors={layerColors}
|
|
||||||
onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))}
|
|
||||||
sensitiveResources={sensitiveResourceCategories}
|
sensitiveResources={sensitiveResourceCategories}
|
||||||
onImageAnalysisResult={handleImageAnalysisResult}
|
onImageAnalysisResult={handleImageAnalysisResult}
|
||||||
validationErrors={validationErrors}
|
validationErrors={validationErrors}
|
||||||
@ -1239,7 +1236,6 @@ export function OilSpillView() {
|
|||||||
drawingPoints={drawingPoints}
|
drawingPoints={drawingPoints}
|
||||||
layerOpacity={layerOpacity}
|
layerOpacity={layerOpacity}
|
||||||
layerBrightness={layerBrightness}
|
layerBrightness={layerBrightness}
|
||||||
layerColors={layerColors}
|
|
||||||
sensitiveResources={sensitiveResources}
|
sensitiveResources={sensitiveResources}
|
||||||
sensitiveResourceGeojson={
|
sensitiveResourceGeojson={
|
||||||
displayControls.showSensitiveResources ? sensitiveResourceGeojson : null
|
displayControls.showSensitiveResources ? sensitiveResourceGeojson : null
|
||||||
|
|||||||
@ -54,8 +54,6 @@ export interface LeftPanelProps {
|
|||||||
onLayerOpacityChange: (val: number) => void;
|
onLayerOpacityChange: (val: number) => void;
|
||||||
layerBrightness: number;
|
layerBrightness: number;
|
||||||
onLayerBrightnessChange: (val: number) => void;
|
onLayerBrightnessChange: (val: number) => void;
|
||||||
layerColors: Record<string, string>;
|
|
||||||
onLayerColorChange: (layerId: string, color: string) => void;
|
|
||||||
// 영향 민감자원
|
// 영향 민감자원
|
||||||
sensitiveResources?: SensitiveResourceCategory[];
|
sensitiveResources?: SensitiveResourceCategory[];
|
||||||
// 이미지 분석 결과 콜백
|
// 이미지 분석 결과 콜백
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user