wing-ops/frontend/src/tabs/prediction/components/BacktrackModal.tsx
htlee dec066e8bb refactor(css): CSS 인프라 구축 + body default 인라인 스타일 1,055건 제거
Phase 0: CSS 인프라 구축
- Tailwind config 색상 불일치 수정 (t1/t2/t3 → CSS 변수 값으로 통일)
- index.css 1,302줄 → @import 엔트리포인트 7줄로 축소
- common/styles/base.css: @layer base 추출 (CSS 변수, 리셋, body 기본값)
- common/styles/components.css: @layer components + utilities 추출
- common/styles/wing.css: wing-* 디자인 시스템 클래스 신규 정의
- common/utils/cn.ts: className 조합 유틸리티
- App.css 삭제 (내용을 components.css로 통합)

Phase 1: body default 인라인 스타일 일괄 제거
- fontFamily: 'var(--fK)' 781건 제거 (body font-family 상속)
- color: 'var(--t1)' 274건 제거 (body color 상속)
- 빈 style={{}} 78건 정리
- 31개 파일, JS 번들 23KB 감소

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:45:01 +09:00

296 lines
11 KiB
TypeScript
Executable File

import { useRef, useEffect } from 'react'
import type { BacktrackPhase, BacktrackVessel, BacktrackConditions } from '@common/types/backtrack'
interface BacktrackModalProps {
isOpen: boolean
onClose: () => void
phase: BacktrackPhase
conditions: BacktrackConditions
vessels: BacktrackVessel[]
onRunAnalysis: () => void
onStartReplay: () => void
}
export function BacktrackModal({
isOpen,
onClose,
phase,
conditions,
vessels,
onRunAnalysis,
onStartReplay,
}: BacktrackModalProps) {
const backdropRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handler = (e: MouseEvent) => {
if (e.target === backdropRef.current) onClose()
}
if (isOpen) document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [isOpen, onClose])
if (!isOpen) return null
return (
<div
ref={backdropRef}
style={{
position: 'fixed', inset: 0, zIndex: 9999,
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<div style={{
width: '580px', maxHeight: 'calc(100vh - 120px)',
background: 'var(--bg1)', border: '1px solid var(--bd)',
borderRadius: '14px', overflow: 'hidden',
display: 'flex', flexDirection: 'column',
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
}}>
{/* Header */}
<div style={{
padding: '20px 24px', borderBottom: '1px solid var(--bd)',
display: 'flex', alignItems: 'center', gap: '14px',
}}>
<div style={{
width: '40px', height: '40px', borderRadius: '10px',
background: 'linear-gradient(135deg, rgba(168,85,247,0.2), rgba(6,182,212,0.2))',
border: '1px solid rgba(168,85,247,0.3)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '18px',
}}>
🔍
</div>
<div style={{ flex: 1 }}>
<h2 style={{
fontSize: '16px', fontWeight: 700,
margin: 0,
}}>
</h2>
<div style={{
fontSize: '11px', color: 'var(--t3)', marginTop: '2px',
}}>
AIS
</div>
</div>
<button
onClick={onClose}
style={{
width: '32px', height: '32px', borderRadius: '8px',
border: '1px solid var(--bd)', background: 'var(--bg3)',
color: 'var(--t3)', fontSize: '14px', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
</button>
</div>
{/* Scrollable Content */}
<div style={{
flex: 1, overflowY: 'auto', padding: '20px 24px',
display: 'flex', flexDirection: 'column', gap: '16px',
}}>
{/* Analysis Conditions */}
<div>
<h3 style={{
fontSize: '12px', fontWeight: 700, color: 'var(--t2)',
marginBottom: '10px',
}}>
</h3>
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px',
}}>
{[
{ label: '유출 추정 시각', value: conditions.estimatedSpillTime },
{ label: '분석 범위', value: conditions.analysisRange },
{ label: '탐색 반경', value: conditions.searchRadius },
{ label: '유출 위치', value: `${conditions.spillLocation.lat.toFixed(4)}°N, ${conditions.spillLocation.lon.toFixed(4)}°E` },
].map((item, i) => (
<div key={i} style={{
padding: '10px 12px', background: 'var(--bg3)',
border: '1px solid var(--bd)', borderRadius: '8px',
}}>
<div style={{ fontSize: '9px', color: 'var(--t3)', marginBottom: '4px' }}>
{item.label}
</div>
<div style={{ fontSize: '12px', fontWeight: 600, fontFamily: 'var(--fM)' }}>
{item.value}
</div>
</div>
))}
<div style={{
padding: '10px 12px', background: 'var(--bg3)',
border: '1px solid rgba(168,85,247,0.3)', borderRadius: '8px',
gridColumn: '1 / -1',
}}>
<div style={{ fontSize: '9px', color: 'var(--t3)', marginBottom: '4px' }}>
</div>
<div style={{ fontSize: '14px', fontWeight: 700, color: 'var(--purple)', fontFamily: 'var(--fM)' }}>
{conditions.totalVessels} <span style={{ fontSize: '10px', fontWeight: 500, color: 'var(--t3)' }}>(AIS )</span>
</div>
</div>
</div>
</div>
{/* Results */}
{phase === 'results' && vessels.length > 0 && (
<div>
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: '12px',
}}>
<h3 style={{
fontSize: '12px', fontWeight: 700, color: 'var(--t2)',
margin: 0,
}}>
</h3>
<div style={{
padding: '4px 10px', borderRadius: '12px', fontSize: '10px', fontWeight: 700,
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
color: 'var(--red)',
}}>
{conditions.totalVessels} {vessels.length}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{vessels.map((v) => (
<VesselCard key={v.imo} vessel={v} />
))}
</div>
</div>
)}
</div>
{/* Footer */}
<div style={{
padding: '16px 24px', borderTop: '1px solid var(--bd)',
display: 'flex', gap: '8px',
}}>
{phase === 'conditions' && (
<button
onClick={onRunAnalysis}
style={{
flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700,
borderRadius: '8px', cursor: 'pointer',
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
border: 'none', color: '#fff',
}}
>
🔍
</button>
)}
{phase === 'analyzing' && (
<button
disabled
style={{
flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700,
borderRadius: '8px',
background: 'var(--bg3)', border: '1px solid var(--bd)',
color: 'var(--purple)', cursor: 'wait',
}}
>
AIS ...
</button>
)}
{phase === 'results' && (
<button
onClick={onStartReplay}
style={{
flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700,
borderRadius: '8px', cursor: 'pointer',
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
border: 'none', color: '#fff',
}}
>
🗺
</button>
)}
</div>
</div>
</div>
)
}
function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
const probColor = vessel.probability >= 80 ? 'var(--red)' :
vessel.probability >= 20 ? 'var(--orange)' : 'var(--t3)'
return (
<div style={{
padding: '14px', background: 'var(--bg0)',
border: '1px solid var(--bd)', borderLeft: `4px solid ${vessel.color}`,
borderRadius: '10px',
}}>
{/* Header row */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
<div style={{
width: '28px', height: '28px', borderRadius: '50%',
background: `${vessel.color}20`, border: `2px solid ${vessel.color}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '12px', fontWeight: 800, color: vessel.color, fontFamily: 'var(--fM)',
}}>
{vessel.rank}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '13px', fontWeight: 700, fontFamily: 'var(--fM)' }}>
{vessel.name}
</div>
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fM)', marginTop: '2px' }}>
IMO: {vessel.imo} · {vessel.type} · {vessel.flag}
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '22px', fontWeight: 800, color: probColor, fontFamily: 'var(--fM)', lineHeight: 1 }}>
{vessel.probability}%
</div>
<div style={{ fontSize: '8px', color: 'var(--t3)' }}> </div>
</div>
</div>
{/* Stats grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '6px', marginBottom: vessel.description ? '8px' : 0 }}>
{[
{ label: '최근접 시각', value: vessel.closestTime },
{ label: '최근접 거리', value: `${vessel.closestDistance} NM` },
{ label: '속도 변화', value: vessel.speedChange, highlight: vessel.speedChange === '급감속' },
{ label: 'AIS 상태', value: vessel.aisStatus, highlight: vessel.aisStatus === '충돌신호' },
].map((s, i) => (
<div key={i} style={{
padding: '6px', background: 'var(--bg3)', borderRadius: '6px',
border: s.highlight ? '1px solid rgba(239,68,68,0.3)' : '1px solid var(--bd)',
}}>
<div style={{ fontSize: '8px', color: 'var(--t3)', marginBottom: '2px' }}>
{s.label}
</div>
<div style={{
fontSize: '10px', fontWeight: 600, fontFamily: 'var(--fM)',
color: s.highlight ? 'var(--red)' : 'var(--t1)',
}}>
{s.value}
</div>
</div>
))}
</div>
{/* Description */}
{vessel.description && (
<div style={{
padding: '8px 10px', background: 'rgba(239,68,68,0.05)',
border: '1px solid rgba(239,68,68,0.15)', borderRadius: '6px',
fontSize: '9px', color: 'var(--t2)',
lineHeight: '1.5',
}}>
{vessel.description}
</div>
)}
</div>
)
}