195 lines
6.1 KiB
TypeScript
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;
|