|
|
@@ -1,7 +1,7 @@
|
|
|
'use client';
|
|
|
|
|
|
import './world-map.scss';
|
|
|
-import { useMemo, useState } from 'react';
|
|
|
+import { useEffect, useMemo, useState } from 'react';
|
|
|
import { geoNaturalEarth1, geoPath } from 'd3-geo';
|
|
|
import { feature } from 'topojson-client';
|
|
|
import type { Feature, Geometry } from 'geojson';
|
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
formatFlucRate,
|
|
|
formatIndexClose,
|
|
|
type WorldIndexRow,
|
|
|
- type MarketRegion
|
|
|
+ type MarketRegion,
|
|
|
+ TV_SYMBOL
|
|
|
} from '@/types/worldIndex';
|
|
|
+import TradingViewChart from './TradingViewChart';
|
|
|
|
|
|
// 등거리(plate carrée) 대신 d3-geo geoNaturalEarth1(곡면) 투영.
|
|
|
// 국가별 topojson feature 를 GeoJSON 으로 디코드해 육지 path 와 핀 좌표를 "동일 projection" 으로 산출 → 오정렬 원천 차단.
|
|
|
@@ -60,6 +62,23 @@ export default function WorldMarketMap({ rows }: Props)
|
|
|
{
|
|
|
const [region, setRegion] = useState<RegionTab>('major');
|
|
|
const [active, setActive] = useState<string|null>(null);
|
|
|
+ const [chartRow, setChartRow] = useState<WorldIndexRow|null>(null);
|
|
|
+
|
|
|
+ // 차트 모달 Esc 닫기
|
|
|
+ useEffect(() => {
|
|
|
+ if (!chartRow) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const onKey = (e: KeyboardEvent) => {
|
|
|
+ if (e.key === 'Escape') {
|
|
|
+ setChartRow(null);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ window.addEventListener('keydown', onKey);
|
|
|
+ return () => {
|
|
|
+ window.removeEventListener('keydown', onKey);
|
|
|
+ };
|
|
|
+ }, [chartRow]);
|
|
|
|
|
|
// 좌표를 아는 국가만 취급
|
|
|
const geoRows = useMemo(() => {
|
|
|
@@ -170,6 +189,7 @@ export default function WorldMarketMap({ rows }: Props)
|
|
|
onMouseLeave={() => setActive((cur) => (cur === pin.row.countryCode ? null : cur))}
|
|
|
onFocus={() => setActive(pin.row.countryCode)}
|
|
|
onBlur={() => setActive((cur) => (cur === pin.row.countryCode ? null : cur))}
|
|
|
+ onClick={() => { if (TV_SYMBOL[pin.row.countryCode]) { setChartRow(pin.row); } }}
|
|
|
>
|
|
|
{pin.dir !== 'flat' && <circle className='world-map__pulse' r={pin.radius} />}
|
|
|
<circle className='world-map__pin-dot' r={pin.radius} />
|
|
|
@@ -253,9 +273,9 @@ export default function WorldMarketMap({ rows }: Props)
|
|
|
<button
|
|
|
type='button'
|
|
|
className='world-map__chart-btn'
|
|
|
- title={`${geo.label} 위치 보기`}
|
|
|
- aria-label={`${geo.label} ${row.name} 지도에서 보기`}
|
|
|
- onClick={() => setActive(row.countryCode)}
|
|
|
+ title={TV_SYMBOL[row.countryCode] ? `${row.name} 차트 보기` : `${geo.label} 위치 보기`}
|
|
|
+ aria-label={TV_SYMBOL[row.countryCode] ? `${geo.label} ${row.name} 차트 보기` : `${geo.label} ${row.name} 지도에서 보기`}
|
|
|
+ onClick={() => { if (TV_SYMBOL[row.countryCode]) { setChartRow(row); } else { setActive(row.countryCode); } }}
|
|
|
>
|
|
|
<svg viewBox='0 0 24 24' width='16' height='16' aria-hidden='true'>
|
|
|
<polyline points='3,17 9,11 13,15 21,6' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' />
|
|
|
@@ -271,6 +291,29 @@ export default function WorldMarketMap({ rows }: Props)
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ {chartRow && TV_SYMBOL[chartRow.countryCode] && (
|
|
|
+ <div
|
|
|
+ className='world-map__modal'
|
|
|
+ role='dialog'
|
|
|
+ aria-modal='true'
|
|
|
+ aria-label={`${EXCHANGE_GEO[chartRow.countryCode]?.label ?? ''} ${chartRow.name} 차트`}
|
|
|
+ onClick={() => setChartRow(null)}
|
|
|
+ >
|
|
|
+ <div className='world-map__modal-panel' onClick={(e) => e.stopPropagation()}>
|
|
|
+ <div className='world-map__modal-head'>
|
|
|
+ <span className='world-map__modal-title'>
|
|
|
+ {EXCHANGE_GEO[chartRow.countryCode]?.label} · {chartRow.name}
|
|
|
+ </span>
|
|
|
+ <button type='button' className='world-map__modal-close' aria-label='닫기' onClick={() => setChartRow(null)}>×</button>
|
|
|
+ </div>
|
|
|
+ <div className='world-map__modal-body'>
|
|
|
+ <TradingViewChart symbol={TV_SYMBOL[chartRow.countryCode]} />
|
|
|
+ </div>
|
|
|
+ <p className='world-map__modal-note'>실시간 차트 제공 · TradingView</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</section>
|
|
|
);
|
|
|
}
|