Compare commits

...

4 커밋

작성자 SHA1 메시지 날짜
fbdf0e9122 refactor(prediction): layerColors 상태를 OilSpillView로 끌어올림
InfoLayerSection 내부 상태였던 layerColors를 OilSpillView에서
관리하도록 변경하여 MapView에 색상 정보를 전달할 수 있도록 함.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 22:31:08 +09:00
646fa38f39 feat(map): SR 민감자원 벡터타일 오버레이 컴포넌트 추가
SrOverlay: Martin SR 스타일 JSON 기반 동적 벡터타일 레이어 렌더링.
srStyles: 레이어 타입별 opacity/color 속성 키 헬퍼.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 22:29:56 +09:00
6620f00ee1 feat(tiles): SR 민감자원 벡터타일 프록시 엔드포인트 추가
Martin 서버의 SR 벡터타일, TileJSON, 스타일 JSON을
백엔드 프록시를 통해 제공하는 라우트 추가.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 22:29:21 +09:00
e4b9c3e5dd refactor(map): 지도 항상 라이트 모드로 고정
useBaseMapStyle에서 테마 구독 제거, 항상 LIGHT_STYLE 반환.
MapView lightMode를 true로 고정하여 앱 다크 모드와 무관하게
지도는 라이트 모드로 표시.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 22:27:39 +09:00
9개의 변경된 파일395개의 추가작업 그리고 12개의 파일을 삭제

파일 보기

@ -54,6 +54,28 @@ 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,6 +113,7 @@ 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[]
@ -306,6 +307,7 @@ export function MapView({
drawingPoints = [], drawingPoints = [],
layerOpacity = 50, layerOpacity = 50,
layerBrightness = 50, layerBrightness = 50,
layerColors,
backtrackReplay, backtrackReplay,
sensitiveResources = [], sensitiveResources = [],
sensitiveResourceGeojson, sensitiveResourceGeojson,
@ -329,7 +331,7 @@ export function MapView({
analysisCircleRadiusM = 0, analysisCircleRadiusM = 0,
showOverlays = true, showOverlays = true,
}: MapViewProps) { }: MapViewProps) {
const lightMode = useThemeStore((s) => s.theme) === 'light' const lightMode = true
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
@ -1135,6 +1137,9 @@ 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

파일 보기

@ -0,0 +1,329 @@
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;
}

파일 보기

@ -0,0 +1,20 @@
// 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,19 +1,15 @@
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;
if (theme === 'light') return LIGHT_STYLE; return LIGHT_STYLE;
return BASE_STYLE;
} }

파일 보기

@ -1,4 +1,3 @@
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';
@ -12,6 +11,8 @@ 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 = ({
@ -23,12 +24,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 ?? [];
@ -134,7 +135,7 @@ const InfoLayerSection = ({
enabledLayers={enabledLayers} enabledLayers={enabledLayers}
onToggleLayer={onToggleLayer} onToggleLayer={onToggleLayer}
layerColors={layerColors} layerColors={layerColors}
onColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))} onColorChange={onLayerColorChange}
/> />
)} )}

파일 보기

@ -108,6 +108,8 @@ export function LeftPanel({
onLayerOpacityChange, onLayerOpacityChange,
layerBrightness, layerBrightness,
onLayerBrightnessChange, onLayerBrightnessChange,
layerColors,
onLayerColorChange,
sensitiveResources = [], sensitiveResources = [],
onImageAnalysisResult, onImageAnalysisResult,
validationErrors, validationErrors,
@ -345,6 +347,8 @@ 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,9 +216,10 @@ 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>({
@ -1200,6 +1201,8 @@ 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}
@ -1236,6 +1239,7 @@ 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,6 +54,8 @@ 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[];
// 이미지 분석 결과 콜백 // 이미지 분석 결과 콜백