wing-ops/frontend/src/tabs/scat/components/ScatLeftPanel.tsx
htlee c727afd1ba refactor(frontend): 대형 View 서브탭 단위 분할 + FEATURE_ID 체계 도입
6개 대형 View(AerialView, AssetsView, ReportsView, PreScatView, AdminView, LeftPanel)를
서브탭 단위로 분할하여 모듈 경계를 명확히 함.

- AerialView (2,526줄 → 8파일): MediaManagement, OilAreaAnalysis, RealtimeDrone 등
- AssetsView (2,047줄 → 8파일): AssetManagement, AssetMap, ShipInsurance 등
- ReportsView (1,596줄 → 5파일): TemplateFormEditor, ReportGenerator 등
- PreScatView (1,390줄 → 7파일): ScatLeftPanel, ScatMap, ScatPopup 등
- AdminView (1,306줄 → 7파일): UsersPanel, PermissionsPanel, MenusPanel 등
- LeftPanel (1,237줄 → 5파일): PredictionInputSection, InfoLayerSection, OilBoomSection 등

FEATURE_ID 레지스트리(common/constants/featureIds.ts) 및
감사로그 서브탭 추적 훅(useFeatureTracking) 추가.

.gitignore의 scat/ → /scat/ 수정 (scat 탭 파일 추적 누락 수정)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 16:19:22 +09:00

156 lines
6.8 KiB
TypeScript

import type { ScatSegment } from './scatTypes'
import { esiColor, sensColor, statusColor, esiLevel, scatAreas, scatDetailData } from './scatConstants'
interface ScatLeftPanelProps {
segments: ScatSegment[]
selectedSeg: ScatSegment
onSelectSeg: (s: ScatSegment) => void
onOpenPopup: (idx: number) => void
jurisdictionFilter: string
onJurisdictionChange: (v: string) => void
areaFilter: string
onAreaChange: (v: string) => void
phaseFilter: string
onPhaseChange: (v: string) => void
statusFilter: string
onStatusChange: (v: string) => void
searchTerm: string
onSearchChange: (v: string) => void
}
function ScatLeftPanel({
segments,
selectedSeg,
onSelectSeg,
onOpenPopup,
jurisdictionFilter,
onJurisdictionChange,
areaFilter,
onAreaChange,
phaseFilter,
onPhaseChange,
statusFilter,
onStatusChange,
searchTerm,
onSearchChange,
}: ScatLeftPanelProps) {
const filtered = segments.filter(s => {
if (areaFilter !== '전체' && !s.area.includes(areaFilter.replace('서귀포시 ', '').replace('제주시 ', '').replace(' 해안', ''))) return false
if (statusFilter !== '전체' && s.status !== statusFilter) return false
if (searchTerm && !s.code.includes(searchTerm) && !s.name.includes(searchTerm)) return false
return true
})
return (
<div className="w-[340px] min-w-[340px] bg-bg-1 border-r border-border flex flex-col overflow-hidden">
{/* Filters */}
<div className="p-3.5 border-b border-border">
<div className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-text-1 mb-3">
<span className="w-[3px] h-2.5 bg-status-green rounded-sm" />
</div>
<div className="mb-2.5">
<label className="block text-[11px] font-medium text-text-1 mb-1 font-korean"> </label>
<select value={jurisdictionFilter} onChange={e => onJurisdictionChange(e.target.value)} className="prd-i w-full">
<option> ()</option>
<option></option>
<option></option>
</select>
</div>
<div className="mb-2.5">
<label className="block text-[11px] font-medium text-text-1 mb-1 font-korean"> </label>
<select value={areaFilter} onChange={e => onAreaChange(e.target.value)} className="prd-i w-full">
<option></option>
{scatAreas.map(a => (
<option key={a.code}>{a.jurisdiction === '서귀포' ? '서귀포시' : '제주시'} {a.area} </option>
))}
</select>
</div>
<div className="mb-2.5">
<label className="block text-[11px] font-medium text-text-1 mb-1 font-korean"> </label>
<select value={phaseFilter} onChange={e => onPhaseChange(e.target.value)} className="prd-i w-full">
<option>Pre-SCAT ()</option>
<option>SCAT ( )</option>
<option>Post-SCAT ( )</option>
</select>
</div>
<div className="flex gap-1.5 mt-1">
<input
type="text"
placeholder="🔍 구간 검색..."
value={searchTerm}
onChange={e => onSearchChange(e.target.value)}
className="prd-i flex-1"
/>
<select value={statusFilter} onChange={e => onStatusChange(e.target.value)} className="prd-i w-[70px]">
<option></option>
<option></option>
<option></option>
<option></option>
</select>
</div>
</div>
{/* Segment List */}
<div className="flex-1 flex flex-col overflow-hidden p-3.5 pt-2">
<div className="flex items-center justify-between text-[10px] font-bold uppercase tracking-wider text-text-1 mb-2.5">
<span className="flex items-center gap-1.5">
<span className="w-[3px] h-2.5 bg-status-green rounded-sm" />
</span>
<span className="text-primary-cyan font-mono text-[10px]"> {filtered.length} </span>
</div>
<div className="flex-1 overflow-y-auto scrollbar-thin flex flex-col gap-1.5">
{filtered.map(seg => {
const lvl = esiLevel(seg.esiNum)
const borderColor = lvl === 'h' ? 'border-l-status-red' : lvl === 'm' ? 'border-l-status-orange' : 'border-l-status-green'
const isSelected = selectedSeg.id === seg.id
return (
<div
key={seg.id}
onClick={() => { onSelectSeg(seg); onOpenPopup(seg.id % scatDetailData.length) }}
className={`bg-bg-3 border border-border rounded-sm p-2.5 px-3 cursor-pointer transition-all border-l-4 ${borderColor} ${
isSelected ? 'border-status-green bg-[rgba(34,197,94,0.05)]' : 'hover:border-border-light hover:bg-bg-hover'
}`}
>
<div className="flex items-center justify-between mb-1.5">
<span className="text-[10px] font-semibold font-korean flex items-center gap-1.5">
📍 {seg.code} {seg.area}
</span>
<span className="text-[8px] font-bold px-1.5 py-0.5 rounded-lg text-white" style={{ background: esiColor(seg.esiNum) }}>
ESI {seg.esi}
</span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1">
<div className="flex justify-between text-[11px]">
<span className="text-text-2 font-korean"></span>
<span className="text-text-1 font-medium font-mono text-[11px]">{seg.type}</span>
</div>
<div className="flex justify-between text-[11px]">
<span className="text-text-2 font-korean"></span>
<span className="text-text-1 font-medium font-mono text-[11px]">{seg.length}</span>
</div>
<div className="flex justify-between text-[11px]">
<span className="text-text-2 font-korean"></span>
<span className="font-medium font-mono text-[11px]" style={{ color: sensColor[seg.sensitivity] }}>{seg.sensitivity}</span>
</div>
<div className="flex justify-between text-[11px]">
<span className="text-text-2 font-korean"></span>
<span className="font-medium font-mono text-[11px]" style={{ color: statusColor[seg.status] }}>{seg.status}</span>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
export default ScatLeftPanel