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>
156 lines
6.8 KiB
TypeScript
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
|