castad-pre-v0.3/castad-data/src/components/KoreaMap.tsx

195 lines
6.1 KiB
TypeScript

import React, { useState } from 'react';
import { cn } from '../lib/utils';
interface RegionData {
sido: string;
count: number;
}
interface KoreaMapProps {
data: RegionData[];
onRegionClick?: (sido: string) => void;
className?: string;
}
// 지역별 색상 (축제 수에 따라 그라데이션)
const getRegionColor = (count: number, maxCount: number) => {
if (count === 0) return '#e5e7eb'; // gray-200
const intensity = Math.min(count / Math.max(maxCount, 1), 1);
// Orange gradient from light to dark
if (intensity < 0.2) return '#fed7aa'; // orange-200
if (intensity < 0.4) return '#fdba74'; // orange-300
if (intensity < 0.6) return '#fb923c'; // orange-400
if (intensity < 0.8) return '#f97316'; // orange-500
return '#ea580c'; // orange-600
};
// 한국 지도 SVG 경로 데이터 (간략화된 버전)
const REGION_PATHS: Record<string, { path: string; labelX: number; labelY: number }> = {
'서울': {
path: 'M 145 95 L 155 90 L 165 95 L 165 105 L 155 110 L 145 105 Z',
labelX: 155, labelY: 100
},
'인천': {
path: 'M 120 90 L 140 85 L 145 95 L 145 110 L 135 115 L 120 105 Z',
labelX: 132, labelY: 100
},
'경기': {
path: 'M 120 70 L 180 60 L 195 80 L 190 120 L 165 130 L 135 125 L 110 110 L 115 85 Z',
labelX: 155, labelY: 85
},
'강원': {
path: 'M 180 45 L 250 30 L 280 60 L 270 120 L 220 140 L 190 120 L 195 80 Z',
labelX: 230, labelY: 85
},
'충북': {
path: 'M 165 130 L 220 140 L 230 170 L 200 195 L 165 185 L 155 155 Z',
labelX: 190, labelY: 165
},
'충남': {
path: 'M 90 130 L 155 125 L 165 155 L 155 190 L 120 210 L 80 190 L 70 155 Z',
labelX: 115, labelY: 170
},
'세종': {
path: 'M 145 155 L 165 155 L 165 175 L 145 175 Z',
labelX: 155, labelY: 165
},
'대전': {
path: 'M 155 175 L 175 175 L 175 195 L 155 195 Z',
labelX: 165, labelY: 185
},
'전북': {
path: 'M 80 190 L 155 190 L 165 185 L 175 210 L 155 250 L 100 260 L 70 230 Z',
labelX: 120, labelY: 225
},
'전남': {
path: 'M 70 230 L 100 260 L 155 250 L 170 280 L 150 320 L 90 330 L 50 300 L 45 260 Z',
labelX: 105, labelY: 290
},
'광주': {
path: 'M 95 270 L 115 265 L 120 285 L 100 290 Z',
labelX: 107, labelY: 278
},
'경북': {
path: 'M 200 140 L 270 120 L 290 160 L 280 220 L 240 240 L 200 230 L 175 210 L 175 175 L 200 170 Z',
labelX: 235, labelY: 185
},
'대구': {
path: 'M 225 210 L 250 205 L 255 225 L 230 230 Z',
labelX: 240, labelY: 218
},
'울산': {
path: 'M 275 220 L 295 210 L 300 235 L 280 245 Z',
labelX: 287, labelY: 228
},
'부산': {
path: 'M 260 265 L 290 255 L 300 280 L 275 295 L 255 285 Z',
labelX: 275, labelY: 275
},
'경남': {
path: 'M 155 250 L 200 230 L 240 240 L 260 265 L 255 285 L 220 310 L 170 300 L 150 280 Z',
labelX: 200, labelY: 270
},
'제주': {
path: 'M 80 380 L 150 375 L 160 400 L 140 420 L 90 420 L 70 400 Z',
labelX: 115, labelY: 400
}
};
const KoreaMap: React.FC<KoreaMapProps> = ({ data, onRegionClick, className }) => {
const [hoveredRegion, setHoveredRegion] = useState<string | null>(null);
// 데이터를 지역명으로 매핑
const dataMap = new Map(data.map(d => [d.sido, d.count]));
const maxCount = Math.max(...data.map(d => d.count), 1);
const getCount = (sido: string) => dataMap.get(sido) || 0;
return (
<div className={cn("relative", className)}>
<svg
viewBox="0 0 350 450"
className="w-full h-auto"
style={{ maxHeight: '400px' }}
>
{/* 배경 */}
<rect x="0" y="0" width="350" height="450" fill="transparent" />
{/* 지역별 경로 */}
{Object.entries(REGION_PATHS).map(([sido, { path, labelX, labelY }]) => {
const count = getCount(sido);
const isHovered = hoveredRegion === sido;
return (
<g key={sido}>
{/* 지역 영역 */}
<path
d={path}
fill={getRegionColor(count, maxCount)}
stroke={isHovered ? '#ea580c' : '#9ca3af'}
strokeWidth={isHovered ? 2 : 1}
className="cursor-pointer transition-all duration-200"
style={{
filter: isHovered ? 'brightness(1.1)' : 'none',
transform: isHovered ? 'scale(1.02)' : 'scale(1)',
transformOrigin: `${labelX}px ${labelY}px`
}}
onMouseEnter={() => setHoveredRegion(sido)}
onMouseLeave={() => setHoveredRegion(null)}
onClick={() => onRegionClick?.(sido)}
/>
{/* 지역명 & 축제 수 */}
<text
x={labelX}
y={labelY - 5}
textAnchor="middle"
className="text-[10px] font-bold fill-gray-700 pointer-events-none select-none"
>
{sido}
</text>
{count > 0 && (
<text
x={labelX}
y={labelY + 8}
textAnchor="middle"
className="text-[9px] font-medium fill-orange-700 pointer-events-none select-none"
>
{count}
</text>
)}
</g>
);
})}
</svg>
{/* 호버 툴팁 */}
{hoveredRegion && (
<div className="absolute top-2 right-2 bg-white dark:bg-gray-800 shadow-lg rounded-lg p-3 border z-10">
<p className="font-bold text-sm">{hoveredRegion}</p>
<p className="text-orange-500 font-medium">
{getCount(hoveredRegion)}
</p>
</div>
)}
{/* 범례 */}
<div className="mt-4 flex items-center justify-center gap-2 text-xs text-muted-foreground">
<span></span>
<div className="flex">
{['#e5e7eb', '#fed7aa', '#fdba74', '#fb923c', '#f97316', '#ea580c'].map((color, i) => (
<div
key={i}
className="w-6 h-3"
style={{ backgroundColor: color }}
/>
))}
</div>
<span></span>
</div>
</div>
);
};
export default KoreaMap;