kcg-monitoring/frontend/src/components/SubmarineCableLayer.tsx
htlee 2534faa488 feat: 프론트엔드 모노레포 이관 + signal-batch 연동 + Tailwind/i18n/테마 전환
- frontend/ 폴더로 프론트엔드 전체 이관
- signal-batch API 연동 (한국 선박 위치 데이터)
- Tailwind CSS 4 + CSS 변수 테마 토큰 (dark/light)
- i18next 다국어 (ko/en) 인프라 + 28개 컴포넌트 적용
- 레이어 패널 트리 구조 재설계 (카테고리별 온/오프, 범례)
- Google OAuth 로그인 화면 + DEV LOGIN 우회
- 외부 API CORS 프록시 전환 (Airplanes.live, OpenSky, CelesTrak)
- ShipLayer 이미지 탭 전환 (signal-batch / MarineTraffic)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:54:41 +09:00

155 lines
5.9 KiB
TypeScript

import { useState } from 'react';
import { Marker, Popup, Source, Layer } from 'react-map-gl/maplibre';
import { KOREA_SUBMARINE_CABLES, KOREA_LANDING_POINTS } from '../services/submarineCable';
import type { SubmarineCable } from '../services/submarineCable';
export function SubmarineCableLayer() {
const [selectedCable, setSelectedCable] = useState<SubmarineCable | null>(null);
const [selectedPoint, setSelectedPoint] = useState<{ name: string; lat: number; lng: number; cables: string[] } | null>(null);
// Build GeoJSON for all cables
const geojson: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: KOREA_SUBMARINE_CABLES.map(cable => ({
type: 'Feature' as const,
properties: {
id: cable.id,
name: cable.name,
color: cable.color,
},
geometry: {
type: 'LineString' as const,
coordinates: cable.route,
},
})),
};
return (
<>
{/* Cable lines */}
<Source id="submarine-cables" type="geojson" data={geojson}>
<Layer
id="submarine-cables-outline"
type="line"
paint={{
'line-color': ['get', 'color'],
'line-width': 1.5,
'line-opacity': 0.25,
}}
/>
<Layer
id="submarine-cables-line"
type="line"
paint={{
'line-color': ['get', 'color'],
'line-width': 1,
'line-opacity': 0.6,
'line-dasharray': [4, 3],
}}
/>
</Source>
{/* Landing points */}
{KOREA_LANDING_POINTS.map(pt => (
<Marker key={pt.name} longitude={pt.lng} latitude={pt.lat} anchor="center"
onClick={(e) => { e.originalEvent.stopPropagation(); setSelectedPoint(pt); setSelectedCable(null); }}>
<div style={{
width: 8, height: 8, borderRadius: '50%',
background: '#00e5ff', border: '1.5px solid #fff',
boxShadow: '0 0 6px #00e5ff88',
cursor: 'pointer',
}} />
</Marker>
))}
{/* Cable name labels along route (midpoint) */}
{KOREA_SUBMARINE_CABLES.map(cable => {
const mid = cable.route[Math.floor(cable.route.length / 3)];
if (!mid) return null;
return (
<Marker key={`label-${cable.id}`} longitude={mid[0]} latitude={mid[1]} anchor="center"
onClick={(e) => { e.originalEvent.stopPropagation(); setSelectedCable(cable); setSelectedPoint(null); }}>
<div style={{
fontSize: 7, fontFamily: 'monospace', fontWeight: 600,
color: cable.color, cursor: 'pointer',
textShadow: '0 0 3px #000, 0 0 3px #000, 0 0 6px #000',
whiteSpace: 'nowrap', opacity: 0.8,
}}>
{cable.name}
</div>
</Marker>
);
})}
{/* Landing point popup */}
{selectedPoint && (
<Popup longitude={selectedPoint.lng} latitude={selectedPoint.lat}
onClose={() => setSelectedPoint(null)} closeOnClick={false}
anchor="bottom" maxWidth="260px" className="gl-popup">
<div style={{ fontFamily: 'monospace', fontSize: 12, minWidth: 180 }}>
<div style={{
background: '#00e5ff', color: '#000',
padding: '4px 8px', borderRadius: '4px 4px 0 0',
margin: '-10px -10px 8px -10px',
fontWeight: 700, fontSize: 13,
display: 'flex', alignItems: 'center', gap: 6,
}}>
<span>📡</span> {selectedPoint.name}
</div>
<div style={{ fontSize: 10, color: '#aaa', marginBottom: 4 }}>
: {selectedPoint.cables.length}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{selectedPoint.cables.map(cid => {
const c = KOREA_SUBMARINE_CABLES.find(cc => cc.id === cid);
if (!c) return null;
return (
<div key={cid} style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 10 }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: c.color, flexShrink: 0 }} />
<span style={{ color: '#ddd' }}>{c.name}</span>
</div>
);
})}
</div>
</div>
</Popup>
)}
{/* Cable info popup */}
{selectedCable && (
<Popup
longitude={selectedCable.route[0][0]}
latitude={selectedCable.route[0][1]}
onClose={() => setSelectedCable(null)} closeOnClick={false}
anchor="bottom" maxWidth="280px" className="gl-popup">
<div style={{ fontFamily: 'monospace', fontSize: 12, minWidth: 200 }}>
<div style={{
background: selectedCable.color, color: '#000',
padding: '4px 8px', borderRadius: '4px 4px 0 0',
margin: '-10px -10px 8px -10px',
fontWeight: 700, fontSize: 13,
}}>
🔌 {selectedCable.name}
</div>
<div style={{ fontSize: 11, display: 'flex', flexDirection: 'column', gap: 3 }}>
<div>
<span style={{ color: '#888' }}>: </span>
<span style={{ color: '#ddd' }}>{selectedCable.landingPoints.join(' → ')}</span>
</div>
{selectedCable.rfsYear && (
<div><span style={{ color: '#888' }}>: </span>{selectedCable.rfsYear}</div>
)}
{selectedCable.length && (
<div><span style={{ color: '#888' }}> : </span>{selectedCable.length}</div>
)}
{selectedCable.owners && (
<div><span style={{ color: '#888' }}>: </span>{selectedCable.owners}</div>
)}
</div>
</div>
</Popup>
)}
</>
);
}