|
@@ -1,7 +1,7 @@
|
|
|
'use client';
|
|
'use client';
|
|
|
|
|
|
|
|
import './world-map.scss';
|
|
import './world-map.scss';
|
|
|
-import { useMemo, useRef, useState } from 'react';
|
|
|
|
|
|
|
+import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
import { geoNaturalEarth1, geoPath } from 'd3-geo';
|
|
import { geoNaturalEarth1, geoPath } from 'd3-geo';
|
|
|
import { feature } from 'topojson-client';
|
|
import { feature } from 'topojson-client';
|
|
|
import type { Feature, Geometry } from 'geojson';
|
|
import type { Feature, Geometry } from 'geojson';
|
|
@@ -27,6 +27,7 @@ const VH = 500;
|
|
|
const MIN_K = 1;
|
|
const MIN_K = 1;
|
|
|
const MAX_K = 6;
|
|
const MAX_K = 6;
|
|
|
const DEFAULT_K = 1.5; // 기본 살짝 확대
|
|
const DEFAULT_K = 1.5; // 기본 살짝 확대
|
|
|
|
|
+const PIN_H = 15; // 핀 막대 높이 — 머리를 지점 위로 띄워 겹침 완화(map-pin)
|
|
|
|
|
|
|
|
type GeoFeature = Feature<Geometry>;
|
|
type GeoFeature = Feature<Geometry>;
|
|
|
type RegionTab = 'major'|MarketRegion;
|
|
type RegionTab = 'major'|MarketRegion;
|
|
@@ -78,6 +79,32 @@ export default function WorldMarketMap({ rows }: Props)
|
|
|
const dragRef = useRef<{ px: number; py: number; vx: number; vy: number; moved: boolean }|null>(null);
|
|
const dragRef = useRef<{ px: number; py: number; vx: number; vy: number; moved: boolean }|null>(null);
|
|
|
const suppressClickRef = useRef(false);
|
|
const suppressClickRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
+ // 마우스 휠 확대/축소 (커서 위치 기준). React onWheel 은 passive 라 네이티브 리스너로 등록(preventDefault 필요).
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const svg = svgRef.current;
|
|
|
|
|
+ if (!svg) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const onWheel = (e: WheelEvent) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const rect = svg.getBoundingClientRect();
|
|
|
|
|
+ if (rect.width === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const cx = (e.clientX - rect.left) * (VW / rect.width);
|
|
|
|
|
+ const cy = (e.clientY - rect.top) * (VH / rect.height);
|
|
|
|
|
+ const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
|
|
|
|
|
+ setView((v) => {
|
|
|
|
|
+ const k = clamp(v.k * factor, MIN_K, MAX_K);
|
|
|
|
|
+ return { x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k), k };
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+ svg.addEventListener('wheel', onWheel, { passive: false });
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ svg.removeEventListener('wheel', onWheel);
|
|
|
|
|
+ };
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
// 좌표를 아는 국가만 취급 + 핀 좌표 산출
|
|
// 좌표를 아는 국가만 취급 + 핀 좌표 산출
|
|
|
const pins = useMemo<Pin[]>(() => {
|
|
const pins = useMemo<Pin[]>(() => {
|
|
|
return rows
|
|
return rows
|
|
@@ -269,15 +296,15 @@ export default function WorldMarketMap({ rows }: Props)
|
|
|
onClick={() => onPinClick(pin.row)}
|
|
onClick={() => onPinClick(pin.row)}
|
|
|
onKeyDown={(e) => { if ((e.key === 'Enter' || e.key === ' ') && TV_SYMBOL[pin.row.countryCode]) { e.preventDefault(); setChartRow(pin.row); } }}
|
|
onKeyDown={(e) => { if ((e.key === 'Enter' || e.key === ' ') && TV_SYMBOL[pin.row.countryCode]) { e.preventDefault(); setChartRow(pin.row); } }}
|
|
|
>
|
|
>
|
|
|
- {pin.dir !== 'flat' && <circle className='world-map__pulse' r={pin.radius} />}
|
|
|
|
|
- <circle className='world-map__pin-dot' r={pin.radius} />
|
|
|
|
|
- <circle className='world-map__pin-core' r={pin.radius / 2.6} />
|
|
|
|
|
-
|
|
|
|
|
- {/* 상시 라벨 — 국가명 + 등락률 (핀 옆) */}
|
|
|
|
|
- <g className='world-map__label' transform={`translate(${lx} 0)`} textAnchor={anchor}>
|
|
|
|
|
- <text className='world-map__label-name' y='-1.5'>{pin.label}</text>
|
|
|
|
|
- <text className={`world-map__label-rate world-map__label-rate--${pin.dir}`} y='9'>{formatFlucRate(pin.row.flucRateBp)}</text>
|
|
|
|
|
- {isActive && <text className='world-map__label-close' y='19'>{formatIndexClose(pin.row.close)}</text>}
|
|
|
|
|
|
|
+ {/* map-pin: 지점에서 위로 뻗은 막대 + 머리(원) */}
|
|
|
|
|
+ <line className='world-map__pin-stem' x1='0' y1='0' x2='0' y2={-PIN_H} />
|
|
|
|
|
+ <circle className='world-map__pin-head' cx='0' cy={-PIN_H} r={pin.radius} />
|
|
|
|
|
+
|
|
|
|
|
+ {/* 상시 라벨 — 국가명 + 등락률 (머리 옆) */}
|
|
|
|
|
+ <g className='world-map__label' transform={`translate(${lx} ${-PIN_H})`} textAnchor={anchor}>
|
|
|
|
|
+ <text className='world-map__label-name' y='-2'>{pin.label}</text>
|
|
|
|
|
+ <text className={`world-map__label-rate world-map__label-rate--${pin.dir}`} y='8.5'>{formatFlucRate(pin.row.flucRateBp)}</text>
|
|
|
|
|
+ {isActive && <text className='world-map__label-close' y='18'>{formatIndexClose(pin.row.close)}</text>}
|
|
|
</g>
|
|
</g>
|
|
|
</g>
|
|
</g>
|
|
|
);
|
|
);
|