feat(travel): MiniMap 컴포넌트 — 접기/펼치기 + 전체보기
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
106
src/pages/travel/MiniMap.css
Normal file
106
src/pages/travel/MiniMap.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/* ── MiniMap ── */
|
||||||
|
|
||||||
|
.minimap-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* toolbar */
|
||||||
|
.minimap-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-toggle-btn,
|
||||||
|
.minimap-clear-btn {
|
||||||
|
background: var(--tv-surface);
|
||||||
|
color: var(--tv-muted);
|
||||||
|
border: 1px solid var(--tv-line-bright);
|
||||||
|
border-radius: var(--tv-r-sm);
|
||||||
|
padding: 5px 14px;
|
||||||
|
font: 11px var(--tv-mono);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-toggle-btn:hover,
|
||||||
|
.minimap-clear-btn:hover {
|
||||||
|
color: var(--tv-text);
|
||||||
|
border-color: var(--tv-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* container */
|
||||||
|
.minimap-container {
|
||||||
|
position: relative;
|
||||||
|
height: var(--minimap-h, 200px);
|
||||||
|
border-radius: var(--tv-r-lg);
|
||||||
|
border: 1px solid var(--tv-line-bright);
|
||||||
|
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.35);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height 0.35s ease, opacity 0.35s ease;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-collapsed {
|
||||||
|
height: 0 !important;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* leaflet overrides */
|
||||||
|
.minimap-leaflet {
|
||||||
|
background: var(--tv-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-leaflet .leaflet-tile-pane {
|
||||||
|
filter: brightness(0.7) saturate(0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hint overlay */
|
||||||
|
.minimap-hint {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 800;
|
||||||
|
font: 10px var(--tv-mono);
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: var(--tv-dim);
|
||||||
|
background: rgba(15, 12, 9, 0.65);
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: var(--tv-r-sm);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tooltip */
|
||||||
|
.minimap-tooltip {
|
||||||
|
background: var(--tv-surface) !important;
|
||||||
|
color: var(--tv-text) !important;
|
||||||
|
border: 1px solid var(--tv-line-bright) !important;
|
||||||
|
border-radius: var(--tv-r-sm) !important;
|
||||||
|
font: 11px var(--tv-mono) !important;
|
||||||
|
padding: 3px 10px !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-tooltip::before {
|
||||||
|
border-top-color: var(--tv-surface) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.minimap-container {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reduced motion */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.minimap-container {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/pages/travel/MiniMap.jsx
Normal file
166
src/pages/travel/MiniMap.jsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { MapContainer, TileLayer, GeoJSON, useMap } from 'react-leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||||
|
import './MiniMap.css';
|
||||||
|
|
||||||
|
/* ─────────────────────────────────────────────
|
||||||
|
Region accent palette
|
||||||
|
───────────────────────────────────────────── */
|
||||||
|
export const REGION_PALETTE = {
|
||||||
|
japan: '#e05c4b',
|
||||||
|
korea: '#d64f6e',
|
||||||
|
china: '#c84b3a',
|
||||||
|
europe: '#5b8fc4',
|
||||||
|
france: '#6f8fc4',
|
||||||
|
italy: '#78a46e',
|
||||||
|
spain: '#c4844a',
|
||||||
|
sea: '#4aad8b',
|
||||||
|
thailand: '#4aad8b',
|
||||||
|
vietnam: '#5faa78',
|
||||||
|
bali: '#7aac5a',
|
||||||
|
indonesia: '#8aaa4a',
|
||||||
|
america: '#b4885c',
|
||||||
|
usa: '#b4885c',
|
||||||
|
canada: '#6a9890',
|
||||||
|
africa: '#c47c3c',
|
||||||
|
middle: '#c4a24a',
|
||||||
|
dubai: '#c4a24a',
|
||||||
|
default: '#c8905e',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getRegionAccent(regionId = '') {
|
||||||
|
const id = regionId.toLowerCase();
|
||||||
|
for (const [key, color] of Object.entries(REGION_PALETTE)) {
|
||||||
|
if (key !== 'default' && id.includes(key)) return color;
|
||||||
|
}
|
||||||
|
return REGION_PALETTE.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─────────────────────────────────────────────
|
||||||
|
MapLayer — internal component
|
||||||
|
───────────────────────────────────────────── */
|
||||||
|
function MapLayer({ geojson, selectedRegionId, onSelectRegion }) {
|
||||||
|
const map = useMap();
|
||||||
|
|
||||||
|
const style = useCallback(
|
||||||
|
(feature) => {
|
||||||
|
const rid = feature.properties?.id || feature.properties?.name || '';
|
||||||
|
const isSelected =
|
||||||
|
selectedRegionId && rid.toLowerCase() === selectedRegionId.toLowerCase();
|
||||||
|
const accent = getRegionAccent(rid);
|
||||||
|
return {
|
||||||
|
fillColor: isSelected ? accent : 'rgba(232,221,208,0.12)',
|
||||||
|
fillOpacity: isSelected ? 0.45 : 0.18,
|
||||||
|
color: isSelected ? accent : 'rgba(232,221,208,0.25)',
|
||||||
|
weight: isSelected ? 2.5 : 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[selectedRegionId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onEachFeature = useCallback(
|
||||||
|
(feature, layer) => {
|
||||||
|
const name =
|
||||||
|
feature.properties?.name_ko ||
|
||||||
|
feature.properties?.name ||
|
||||||
|
feature.properties?.id ||
|
||||||
|
'';
|
||||||
|
if (name) {
|
||||||
|
layer.bindTooltip(name, {
|
||||||
|
className: 'minimap-tooltip',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
layer.on('click', () => {
|
||||||
|
const rid = feature.properties?.id || feature.properties?.name || '';
|
||||||
|
onSelectRegion(rid);
|
||||||
|
const bounds = layer.getBounds();
|
||||||
|
if (bounds.isValid()) {
|
||||||
|
map.fitBounds(bounds, { padding: [30, 30], maxZoom: 6 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[map, onSelectRegion],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!geojson) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GeoJSON
|
||||||
|
key={selectedRegionId || '__all__'}
|
||||||
|
data={geojson}
|
||||||
|
style={style}
|
||||||
|
onEachFeature={onEachFeature}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─────────────────────────────────────────────
|
||||||
|
MiniMap
|
||||||
|
───────────────────────────────────────────── */
|
||||||
|
export default function MiniMap({
|
||||||
|
geojson,
|
||||||
|
selectedRegionId,
|
||||||
|
onSelectRegion,
|
||||||
|
onClearRegion,
|
||||||
|
}) {
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const toggleExpanded = () => setExpanded((v) => !v);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="minimap-wrapper">
|
||||||
|
{/* toolbar */}
|
||||||
|
<div className="minimap-toolbar">
|
||||||
|
<button
|
||||||
|
className="minimap-toggle-btn"
|
||||||
|
onClick={toggleExpanded}
|
||||||
|
aria-label={expanded ? '지도 접기' : '지도 펼치기'}
|
||||||
|
>
|
||||||
|
{expanded ? '▲ 지도 접기' : '▼ 지도 펼치기'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{selectedRegionId && (
|
||||||
|
<button className="minimap-clear-btn" onClick={onClearRegion}>
|
||||||
|
전체 보기
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* map container */}
|
||||||
|
<div
|
||||||
|
className={`minimap-container${expanded ? '' : ' minimap-collapsed'}`}
|
||||||
|
style={{
|
||||||
|
'--minimap-h': isMobile ? '150px' : '200px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MapContainer
|
||||||
|
center={[30, 125]}
|
||||||
|
zoom={2}
|
||||||
|
minZoom={2}
|
||||||
|
maxZoom={7}
|
||||||
|
zoomControl={false}
|
||||||
|
attributionControl={false}
|
||||||
|
className="minimap-leaflet"
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
||||||
|
attribution=""
|
||||||
|
/>
|
||||||
|
<MapLayer
|
||||||
|
geojson={geojson}
|
||||||
|
selectedRegionId={selectedRegionId}
|
||||||
|
onSelectRegion={onSelectRegion}
|
||||||
|
/>
|
||||||
|
</MapContainer>
|
||||||
|
|
||||||
|
{!selectedRegionId && expanded && (
|
||||||
|
<div className="minimap-hint">CLICK A REGION</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user