wing-ops/frontend/src/tabs/scat/components/ScatTimeline.tsx
htlee 628c07f4fb refactor(css): 인라인 style → Tailwind className 일괄 변환 (229건)
안전한 패턴 매칭으로 단독 color/background/fontWeight/fontSize/flex 스타일을
Tailwind 유틸리티 클래스로 변환. 혼합 style에서 개별 속성 추출은 제외하여
시각적 회귀 방지.

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

145 lines
6.5 KiB
TypeScript
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 { useState, useEffect, useRef, useCallback } from 'react'
import type { ScatSegment } from './scatTypes'
interface ScatTimelineProps {
segments: ScatSegment[]
currentIdx: number
onSeek: (idx: number) => void
}
function ScatTimeline({
segments,
currentIdx,
onSeek,
}: ScatTimelineProps) {
const [playing, setPlaying] = useState(false)
const [speed, setSpeed] = useState(1)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const total = Math.min(segments.length, 12)
const displaySegs = segments.slice(0, total)
const pct = ((currentIdx + 1) / total) * 100
const stop = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
setPlaying(false)
}, [])
const play = useCallback(() => {
stop()
setPlaying(true)
intervalRef.current = setInterval(() => {
onSeek(-1) // signal to advance
}, 800 / speed)
}, [speed, stop, onSeek])
const togglePlay = () => {
if (playing) stop()
else play()
}
useEffect(() => {
if (playing && intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = setInterval(() => {
onSeek(-1)
}, 800 / speed)
}
}, [speed, playing, onSeek])
useEffect(() => {
return () => { if (intervalRef.current) clearInterval(intervalRef.current) }
}, [])
const cycleSpeed = () => {
const speeds = [1, 2, 4]
setSpeed(s => speeds[(speeds.indexOf(s) + 1) % speeds.length])
}
const doneCount = segments.filter(s => s.status === '완료').length
const progCount = segments.filter(s => s.status === '진행중').length
const totalLen = segments.reduce((a, s) => a + s.lengthM, 0)
return (
<div className="absolute bottom-0 left-0 right-0 h-[72px] bg-[rgba(15,21,36,0.95)] backdrop-blur-2xl border-t border-border flex items-center px-5 gap-4 z-[40]">
{/* Controls */}
<div className="flex gap-1 flex-shrink-0">
<button onClick={() => onSeek(0)} className="w-[34px] h-[34px] rounded-sm border border-border bg-bg-3 text-text-2 flex items-center justify-center cursor-pointer hover:bg-bg-hover text-sm"></button>
<button onClick={() => onSeek(Math.max(0, currentIdx - 1))} className="w-[34px] h-[34px] rounded-sm border border-border bg-bg-3 text-text-2 flex items-center justify-center cursor-pointer hover:bg-bg-hover text-sm"></button>
<button onClick={togglePlay} className={`w-[34px] h-[34px] rounded-sm border flex items-center justify-center cursor-pointer text-sm ${playing ? 'bg-status-green text-black border-status-green' : 'border-border bg-bg-3 text-text-2 hover:bg-bg-hover'}`}>
{playing ? '⏸' : '▶'}
</button>
<button onClick={() => onSeek(Math.min(total - 1, currentIdx + 1))} className="w-[34px] h-[34px] rounded-sm border border-border bg-bg-3 text-text-2 flex items-center justify-center cursor-pointer hover:bg-bg-hover text-sm"></button>
<button onClick={() => onSeek(total - 1)} className="w-[34px] h-[34px] rounded-sm border border-border bg-bg-3 text-text-2 flex items-center justify-center cursor-pointer hover:bg-bg-hover text-sm"></button>
<div className="w-2" />
<button onClick={cycleSpeed} className="w-[34px] h-[34px] rounded-sm border border-border bg-bg-3 text-text-2 flex items-center justify-center cursor-pointer hover:bg-bg-hover text-xs font-mono font-bold">{speed}×</button>
</div>
{/* Progress */}
<div className="flex-1 flex flex-col gap-1.5">
<div className="flex justify-between px-1">
{displaySegs.map((s, i) => (
<span
key={i}
className={`text-[10px] font-mono cursor-pointer ${i === currentIdx ? 'text-status-green font-semibold' : 'text-text-3'}`}
onClick={() => onSeek(i)}
>
{s.code}
</span>
))}
</div>
<div className="relative h-6 flex items-center">
<div className="w-full h-1 bg-border rounded relative">
<div className="absolute top-0 left-0 h-full rounded transition-all duration-300" style={{ width: `${pct}%`, background: 'linear-gradient(90deg, var(--green), #4ade80)' }} />
{/* Markers */}
{displaySegs.map((s, i) => {
const x = ((i + 0.5) / total) * 100
return (
<div key={i} className="absolute w-0.5 bg-border-light" style={{ left: `${x}%`, top: -3, height: i % 3 === 0 ? 14 : 10 }}>
{s.status === '완료' && (
<span className="absolute -top-[18px] left-1/2 -translate-x-1/2 text-[10px] cursor-pointer" style={{ filter: 'drop-shadow(0 0 4px rgba(34,197,94,0.5))' }}></span>
)}
{s.status === '진행중' && (
<span className="absolute -top-[18px] left-1/2 -translate-x-1/2 text-[10px]"></span>
)}
</div>
)
})}
</div>
{/* Thumb */}
<div
className="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-status-green border-[3px] border-bg-0 rounded-full cursor-grab shadow-[0_0_10px_rgba(34,197,94,0.4)] z-[2] transition-all duration-300"
style={{ left: `${pct}%`, transform: `translate(-50%, -50%)` }}
/>
</div>
</div>
{/* Info */}
<div className="flex flex-col items-end gap-1 flex-shrink-0 min-w-[210px]">
<span className="text-sm font-semibold text-status-green font-mono">
{displaySegs[currentIdx]?.code || 'S-001'} / {total}
</span>
<div className="flex gap-3.5">
<span className="flex items-center gap-1.5 text-[11px]">
<span className="text-text-2 font-korean"></span>
<span className="text-text-1 font-semibold font-mono text-status-green">{doneCount}/{segments.length}</span>
</span>
<span className="flex items-center gap-1.5 text-[11px]">
<span className="text-text-2 font-korean"></span>
<span className="text-text-1 font-semibold font-mono text-status-orange">{progCount}/{segments.length}</span>
</span>
<span className="flex items-center gap-1.5 text-[11px]">
<span className="text-text-2 font-korean"> </span>
<span className="text-text-1 font-semibold font-mono">{(totalLen / 1000).toFixed(1)} km</span>
</span>
</div>
</div>
</div>
)
}
export default ScatTimeline