From 9c834c4e5ed0bcec00daceea4bd5e4db97dd6192 Mon Sep 17 00:00:00 2001 From: Nan Kyung Lee Date: Mon, 16 Mar 2026 07:57:23 +0900 Subject: [PATCH] =?UTF-8?q?feat(prediction):=20=ED=99=95=EC=82=B0=EC=98=88?= =?UTF-8?q?=EC=B8=A1=20=EC=A7=80=EB=8F=84=20=EB=B0=9D=EC=9D=80=20=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(=EC=9C=A1=EC=A7=80=20=ED=9A=8C=EC=83=89=20+=20=EB=B0=94?= =?UTF-8?q?=EB=8B=A4=20=ED=8C=8C=EB=9E=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 고객요청사항 - 지도를 밝게 하거나, 선명하게 해서 확실히 구분해주세요. - MapView에 lightMode prop 추가 및 해도 스타일(LIGHT_STYLE) 구현 - OpenFreeMap 벡터타일 기반: 육지(회색 #e8e8e8) + 바다(파랑 #a8cce0) 명확 구분 - 한글 지명 라벨 우선 표시 (name:ko → name 폴백) - 도로/건물/경계선 회색 톤 통일, 해양 지명 이탤릭 표시 - 확산예측(OilSpillView)에 lightMode 적용 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/common/components/map/MapView.tsx | 167 +++++++++++++++++- .../prediction/components/OilSpillView.tsx | 1 + 2 files changed, 166 insertions(+), 2 deletions(-) diff --git a/frontend/src/common/components/map/MapView.tsx b/frontend/src/common/components/map/MapView.tsx index 6e6314d..d276b06 100755 --- a/frontend/src/common/components/map/MapView.tsx +++ b/frontend/src/common/components/map/MapView.tsx @@ -47,6 +47,166 @@ const BASE_STYLE: StyleSpecification = { ], } +// MarineTraffic 스타일 — 깔끔한 연한 회색 육지 + 흰색 바다 + 한글 라벨 +const LIGHT_STYLE: StyleSpecification = { + version: 8, + glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf', + sources: { + 'ofm-chart': { + type: 'vector', + url: 'https://tiles.openfreemap.org/planet', + }, + }, + layers: [ + // ── 배경 = 육지 (연한 회색) ── + { + id: 'land-bg', + type: 'background', + paint: { 'background-color': '#e8e8e8' }, + }, + // ── 바다/호수/강 = water 레이어 (파란색) ── + { + id: 'water', + type: 'fill', + source: 'ofm-chart', + 'source-layer': 'water', + paint: { 'fill-color': '#a8cce0' }, + }, + // ── 주요 도로 (zoom 9+) ── + { + id: 'roads-major', + type: 'line', + source: 'ofm-chart', + 'source-layer': 'transportation', + minzoom: 9, + filter: ['in', 'class', 'motorway', 'trunk', 'primary'], + paint: { + 'line-color': '#c0c0c0', + 'line-width': ['interpolate', ['linear'], ['zoom'], 9, 0.4, 14, 1.5], + }, + }, + // ── 보조 도로 (zoom 12+) ── + { + id: 'roads-secondary', + type: 'line', + source: 'ofm-chart', + 'source-layer': 'transportation', + minzoom: 12, + filter: ['in', 'class', 'secondary', 'tertiary'], + paint: { + 'line-color': '#cccccc', + 'line-width': ['interpolate', ['linear'], ['zoom'], 12, 0.3, 14, 1], + }, + }, + // ── 건물 (zoom 14+) ── + { + id: 'buildings', + type: 'fill', + source: 'ofm-chart', + 'source-layer': 'building', + minzoom: 14, + paint: { 'fill-color': '#cbcbcb', 'fill-opacity': 0.5 }, + }, + // ── 국경선 ── + { + id: 'boundaries-country', + type: 'line', + source: 'ofm-chart', + 'source-layer': 'boundary', + filter: ['==', 'admin_level', 2], + paint: { 'line-color': '#999999', 'line-width': 1, 'line-dasharray': [4, 2] }, + }, + // ── 시도 경계 (zoom 5+) ── + { + id: 'boundaries-province', + type: 'line', + source: 'ofm-chart', + 'source-layer': 'boundary', + minzoom: 5, + filter: ['==', 'admin_level', 4], + paint: { 'line-color': '#bbbbbb', 'line-width': 0.5, 'line-dasharray': [3, 2] }, + }, + // ── 국가/시도 라벨 (한글) ── + { + id: 'place-labels-major', + type: 'symbol', + source: 'ofm-chart', + 'source-layer': 'place', + minzoom: 3, + filter: ['in', 'class', 'country', 'state'], + layout: { + 'text-field': ['coalesce', ['get', 'name:ko'], ['get', 'name']], + 'text-font': ['Open Sans Bold'], + 'text-size': ['interpolate', ['linear'], ['zoom'], 3, 11, 8, 16], + 'text-max-width': 8, + }, + paint: { + 'text-color': '#555555', + 'text-halo-color': '#ffffff', + 'text-halo-width': 2, + }, + }, + { + id: 'place-labels-city', + type: 'symbol', + source: 'ofm-chart', + 'source-layer': 'place', + minzoom: 5, + filter: ['in', 'class', 'city', 'town'], + layout: { + 'text-field': ['coalesce', ['get', 'name:ko'], ['get', 'name']], + 'text-font': ['Open Sans Regular'], + 'text-size': ['interpolate', ['linear'], ['zoom'], 5, 10, 12, 14], + 'text-max-width': 7, + }, + paint: { + 'text-color': '#666666', + 'text-halo-color': '#ffffff', + 'text-halo-width': 1.5, + }, + }, + // ── 해양 지명 (water_name) ── + { + id: 'water-labels', + type: 'symbol', + source: 'ofm-chart', + 'source-layer': 'water_name', + layout: { + 'text-field': ['coalesce', ['get', 'name:ko'], ['get', 'name']], + 'text-font': ['Open Sans Italic'], + 'text-size': ['interpolate', ['linear'], ['zoom'], 3, 10, 8, 14], + 'text-max-width': 10, + 'text-letter-spacing': 0.15, + }, + paint: { + 'text-color': '#8899aa', + 'text-halo-color': 'rgba(168,204,224,0.7)', + 'text-halo-width': 1, + }, + }, + // ── 마을/소지명 (zoom 10+) ── + { + id: 'place-labels-village', + type: 'symbol', + source: 'ofm-chart', + 'source-layer': 'place', + minzoom: 10, + filter: ['in', 'class', 'village', 'suburb', 'hamlet'], + layout: { + 'text-field': ['coalesce', ['get', 'name:ko'], ['get', 'name']], + 'text-font': ['Open Sans Regular'], + 'text-size': ['interpolate', ['linear'], ['zoom'], 10, 9, 14, 12], + 'text-max-width': 6, + }, + paint: { + 'text-color': '#777777', + 'text-halo-color': '#ffffff', + 'text-halo-width': 1, + }, + }, + ], +} + // 3D 위성 스타일 (VWorld 위성 배경 + OpenFreeMap 건물 extrusion) // VWorld WMTS: {z}/{y}/{x} (row/col 순서) // OpenFreeMap: CORS 허용, 전 세계 OSM 벡터타일 (building: render_height 포함) @@ -178,6 +338,8 @@ interface MapViewProps { } sensitiveResources?: SensitiveResource[] mapCaptureRef?: React.MutableRefObject<(() => string | null) | null> + /** 밝은 톤 지도 스타일 사용 (CartoDB Positron) */ + lightMode?: boolean } // deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록, interleaved) @@ -262,6 +424,7 @@ export function MapView({ backtrackReplay, sensitiveResources = [], mapCaptureRef, + lightMode = false, }: MapViewProps) { const { mapToggles } = useMapStore() const [currentPosition, setCurrentPosition] = useState<[number, number]>(DEFAULT_CENTER) @@ -732,8 +895,8 @@ export function MapView({ sensitiveResources, ]) - // 3D 모드에 따른 지도 스타일 전환 - const currentMapStyle = mapToggles.threeD ? SATELLITE_3D_STYLE : BASE_STYLE + // 3D 모드 / 밝은 톤에 따른 지도 스타일 전환 + const currentMapStyle = mapToggles.threeD ? SATELLITE_3D_STYLE : lightMode ? LIGHT_STYLE : BASE_STYLE return (
diff --git a/frontend/src/tabs/prediction/components/OilSpillView.tsx b/frontend/src/tabs/prediction/components/OilSpillView.tsx index 0cc91ea..2bac89b 100755 --- a/frontend/src/tabs/prediction/components/OilSpillView.tsx +++ b/frontend/src/tabs/prediction/components/OilSpillView.tsx @@ -461,6 +461,7 @@ export function OilSpillView() { layerOpacity={layerOpacity} layerBrightness={layerBrightness} sensitiveResources={sensitiveResources} + lightMode backtrackReplay={isReplayActive && replayShips.length > 0 ? { isActive: true, ships: replayShips,