'use client'; import './world-map.scss'; import { useMemo, useState } from 'react'; import { EXCHANGE_GEO, moveDir, formatFlucRate, formatIndexClose, type WorldIndexRow } from '@/types/worldIndex'; import { WORLD_LAND_PATH } from './worldLandPath'; // 등거리(plate carrée) 투영 — viewBox 1000×400, 경도 -180~180 → x, 위도 84~-60 → y (남극 크롭, 등scale). // WORLD_LAND_PATH(실제 Natural Earth 110m 육지) 도 동일 투영으로 생성되어 핀과 정확히 정렬된다. const VW = 1000; const VH = 400; const LAT_MAX = 84; const LAT_SPAN = 144; const project = (lat: number, lng: number) => ({ x: ((lng + 180) / 360) * VW, y: ((LAT_MAX - lat) / LAT_SPAN) * VH }); // 위/경도 격자선 (크롭 위도 범위 내) const LNG_LINES = [-150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150]; const LAT_LINES = [-40, -20, 0, 20, 40, 60]; type Props = { rows: WorldIndexRow[]; }; type Pin = { row: WorldIndexRow; label: string; x: number; y: number; dir: 'up' | 'down' | 'flat'; radius: number; }; export default function WorldMarketMap({ rows }: Props) { const [active, setActive] = useState(null); // 좌표를 아는 국가만 핀으로. 등락 크기에 따라 반경 가변(6~10). const pins = useMemo(() => { return rows .map((row) => { const geo = EXCHANGE_GEO[row.countryCode]; if (!geo) { return null; } const { x, y } = project(geo.lat, geo.lng); const radius = Math.min(10, 6 + Math.abs(row.flucRateBp) / 80); return { row, label: geo.label, x, y, dir: moveDir(row.flucRateBp), radius }; }) .filter((c): c is Pin => c !== null); }, [rows]); // 기준일 = 핀 중 가장 최근 거래일 const asOf = useMemo(() => { return rows.reduce((max, c) => (max === null || c.tradeDate > max ? c.tradeDate : max), null); }, [rows]); const activePin = pins.find((c) => c.row.countryCode === active) ?? null; return (

세계 증시