feat(prediction): 확산예측 지도 밝은 해도 스타일 적용 (육지 회색 + 바다 파랑)

고객요청사항 - 지도를 밝게 하거나, 선명하게 해서 확실히 구분해주세요.

- MapView에 lightMode prop 추가 및 해도 스타일(LIGHT_STYLE) 구현
- OpenFreeMap 벡터타일 기반: 육지(회색 #e8e8e8) + 바다(파랑 #a8cce0) 명확 구분
- 한글 지명 라벨 우선 표시 (name:ko → name 폴백)
- 도로/건물/경계선 회색 톤 통일, 해양 지명 이탤릭 표시
- 확산예측(OilSpillView)에 lightMode 적용

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-03-16 07:57:23 +09:00
부모 a470df5518
커밋 9c834c4e5e
2개의 변경된 파일166개의 추가작업 그리고 2개의 파일을 삭제

파일 보기

@ -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) // 3D 위성 스타일 (VWorld 위성 배경 + OpenFreeMap 건물 extrusion)
// VWorld WMTS: {z}/{y}/{x} (row/col 순서) // VWorld WMTS: {z}/{y}/{x} (row/col 순서)
// OpenFreeMap: CORS 허용, 전 세계 OSM 벡터타일 (building: render_height 포함) // OpenFreeMap: CORS 허용, 전 세계 OSM 벡터타일 (building: render_height 포함)
@ -178,6 +338,8 @@ interface MapViewProps {
} }
sensitiveResources?: SensitiveResource[] sensitiveResources?: SensitiveResource[]
mapCaptureRef?: React.MutableRefObject<(() => string | null) | null> mapCaptureRef?: React.MutableRefObject<(() => string | null) | null>
/** 밝은 톤 지도 스타일 사용 (CartoDB Positron) */
lightMode?: boolean
} }
// deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록, interleaved) // deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록, interleaved)
@ -262,6 +424,7 @@ export function MapView({
backtrackReplay, backtrackReplay,
sensitiveResources = [], sensitiveResources = [],
mapCaptureRef, mapCaptureRef,
lightMode = false,
}: MapViewProps) { }: MapViewProps) {
const { mapToggles } = useMapStore() const { mapToggles } = useMapStore()
const [currentPosition, setCurrentPosition] = useState<[number, number]>(DEFAULT_CENTER) const [currentPosition, setCurrentPosition] = useState<[number, number]>(DEFAULT_CENTER)
@ -732,8 +895,8 @@ export function MapView({
sensitiveResources, sensitiveResources,
]) ])
// 3D 모드에 따른 지도 스타일 전환 // 3D 모드 / 밝은 톤에 따른 지도 스타일 전환
const currentMapStyle = mapToggles.threeD ? SATELLITE_3D_STYLE : BASE_STYLE const currentMapStyle = mapToggles.threeD ? SATELLITE_3D_STYLE : lightMode ? LIGHT_STYLE : BASE_STYLE
return ( return (
<div className="w-full h-full relative"> <div className="w-full h-full relative">

파일 보기

@ -461,6 +461,7 @@ export function OilSpillView() {
layerOpacity={layerOpacity} layerOpacity={layerOpacity}
layerBrightness={layerBrightness} layerBrightness={layerBrightness}
sensitiveResources={sensitiveResources} sensitiveResources={sensitiveResources}
lightMode
backtrackReplay={isReplayActive && replayShips.length > 0 ? { backtrackReplay={isReplayActive && replayShips.length > 0 ? {
isActive: true, isActive: true,
ships: replayShips, ships: replayShips,