wing-ops/frontend/src/common/components/map/BacktrackReplayBar.tsx
htlee 3fc8f03238 refactor(css): 인라인 스타일 → Tailwind 유틸리티 클래스 변환 (Phase 2, ~990건)
Phase 2: 정적 인라인 스타일을 Tailwind className으로 변환
- common/: MapView, BacktrackReplayBar, LoginPage, LayerTree, ComboBox, SubMenuBar
- hns/: HNSSubstanceView, HNSScenarioView, HNSView, HNSLeftPanel 등 8파일
- prediction/: BoomDeploymentTheoryView, OilBoomSection, RecalcModal, RightPanel 등 8파일
- incidents/: IncidentsView, IncidentsLeftPanel, IncidentsRightPanel
- rescue/: RescueScenarioView
- aerial/: SatelliteRequest, AerialTheoryView
- assets/: ShipInsurance, AssetTheory, AssetManagement 등 5파일
- board/: BoardView
- reports/: ReportsView, OilSpillReportTemplate, ReportGenerator
- weather/: WeatherMapOverlay, WeatherView, WeatherRightPanel

변환 패턴: color/background/border/borderRadius/display/flex/gap/fontSize/fontWeight → Tailwind
동적 스타일(rgba, gradient, 삼항 조건부, 런타임 변수)은 style prop에 유지
JS 번들: 2,921KB → 2,897KB (-24KB)

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

184 lines
6.0 KiB
TypeScript
Executable File
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ReplayShip, CollisionEvent } from '@common/types/backtrack'
interface BacktrackReplayBarProps {
isPlaying: boolean
replayFrame: number
totalFrames: number
replaySpeed: number
onTogglePlay: () => void
onSeek: (frame: number) => void
onSpeedChange: (speed: number) => void
onClose: () => void
replayShips: ReplayShip[]
collisionEvent: CollisionEvent | null
}
export function BacktrackReplayBar({
isPlaying,
replayFrame,
totalFrames,
replaySpeed,
onTogglePlay,
onSeek,
onSpeedChange,
onClose,
replayShips,
collisionEvent,
}: BacktrackReplayBarProps) {
const progress = (replayFrame / totalFrames) * 100
// Time calculation: 12-hour span from 18:30 to 06:30
const hours = 18.5 + (replayFrame / totalFrames) * 12
const displayHours = hours >= 24 ? hours - 24 : hours
const h = Math.floor(displayHours)
const m = Math.round((displayHours - h) * 60)
const dayLabel = hours >= 24 ? '02-10' : '02-09'
const currentTimeLabel = `${dayLabel} ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')} KST`
const handleSeekClick = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
onSeek(Math.round(pct * totalFrames))
}
return (
<div
className="absolute flex flex-col"
style={{
bottom: '80px', left: '50%', transform: 'translateX(-50%)',
minWidth: '480px', maxWidth: '680px', width: '60%',
background: 'rgba(10,15,25,0.92)', backdropFilter: 'blur(12px)',
border: '1px solid rgba(168,85,247,0.3)', borderRadius: '12px',
padding: '12px 18px', zIndex: 1200,
gap: '10px',
}}
>
{/* Header row */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className="w-2 h-2 rounded-full bg-primary-purple"
style={{ boxShadow: '0 0 8px rgba(168,85,247,0.5)' }}
/>
<span className="text-xs font-bold">
</span>
</div>
<div className="flex items-center gap-1.5">
{/* Speed buttons */}
{[1, 2, 4].map((spd) => (
<button
key={spd}
onClick={() => onSpeedChange(spd)}
className={`bt-spd-btn ${replaySpeed === spd ? 'active' : ''}`}
>
{spd}×
</button>
))}
<div className="w-2" />
{/* Close button */}
<button
onClick={onClose}
className="text-status-red cursor-pointer font-bold"
style={{
padding: '4px 10px', borderRadius: '6px', fontSize: '10px',
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
}}
>
</button>
</div>
</div>
{/* Controls row */}
<div className="flex items-center gap-3">
{/* Play/Pause */}
<button
onClick={onTogglePlay}
className="shrink-0 w-9 h-9 rounded-full flex items-center justify-center text-sm cursor-pointer"
style={{
background: isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.15)',
border: `2px solid ${isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.4)'}`,
color: isPlaying ? '#fff' : 'var(--purple)',
}}
>
{isPlaying ? '⏸' : '▶'}
</button>
{/* Timeline */}
<div className="flex-1 flex flex-col gap-1">
{/* Progress bar */}
<div
className="relative h-5 flex items-center cursor-pointer"
onClick={handleSeekClick}
>
<div
className="w-full h-1 bg-border relative overflow-visible"
style={{ borderRadius: '2px' }}
>
{/* Fill */}
<div
className="absolute top-0 left-0 h-full"
style={{
width: `${progress}%`,
background: 'linear-gradient(90deg, var(--purple), var(--cyan))',
borderRadius: '2px', transition: 'width 0.05s',
}}
/>
{/* Collision marker */}
{collisionEvent && (
<div
className="absolute text-[10px] cursor-pointer"
style={{
top: '-14px',
left: `${collisionEvent.progressPercent}%`,
transform: 'translateX(-50%)',
}}
title={collisionEvent.timeLabel}
>
💥
</div>
)}
</div>
{/* Thumb */}
<div
className="absolute top-1/2 w-3.5 h-3.5 bg-white rounded-full"
style={{
left: `${progress}%`,
transform: 'translate(-50%, -50%)',
border: '3px solid var(--purple)',
boxShadow: '0 0 8px rgba(168,85,247,0.4)',
zIndex: 2, transition: 'left 0.05s',
}}
/>
</div>
{/* Time labels */}
<div className="flex justify-between text-[9px] font-mono">
<span className="text-text-3">18:30</span>
<span className="font-semibold text-primary-purple">{currentTimeLabel}</span>
<span className="text-text-3">06:30</span>
</div>
</div>
</div>
{/* Legend row */}
<div className="flex items-center gap-[14px] pt-1 border-t border-border">
{replayShips.map((ship) => (
<div key={ship.vesselName} className="flex items-center gap-1.5">
<div className="w-4 h-[3px]" style={{ background: ship.color, borderRadius: '1px' }} />
<span className="text-[9px] text-text-2 font-mono">
{ship.vesselName}
</span>
</div>
))}
</div>
</div>
)
}