- DB: ACDNT, SPIL_DATA, PRED_EXEC, ACDNT_WEATHER, ACDNT_MEDIA 5개 테이블 생성 - 시드: 사고 12건, 유출정보 12건, 예측실행 18건, 기상 6건, 미디어 6건 - 백엔드: incidentsService + incidentsRouter (사고 목록/상세/예측/기상/미디어 5개 API) - 프론트: IncidentsView, IncidentTable, IncidentsLeftPanel, MediaModal mock → API 전환 - mockIncidents, WEATHER_DATA, MEDIA_DATA 3개 mock 완전 제거 - SECTION_DATA, MOCK_SENSITIVE, mockVessels는 별도 도메인으로 유지 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
4.9 KiB
TypeScript
Executable File
99 lines
4.9 KiB
TypeScript
Executable File
import { useState, useEffect } from 'react'
|
|
import type { IncidentListItem } from '../services/incidentsApi'
|
|
import { fetchIncidentsRaw } from '../services/incidentsApi'
|
|
|
|
export function IncidentTable() {
|
|
const [incidents, setIncidents] = useState<IncidentListItem[]>([])
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
|
|
useEffect(() => {
|
|
fetchIncidentsRaw()
|
|
.then(setIncidents)
|
|
.catch(() => setIncidents([]))
|
|
}, []);
|
|
|
|
const filteredIncidents = incidents.filter((inc) => {
|
|
if (!searchTerm) return true;
|
|
return inc.acdntNm.toLowerCase().includes(searchTerm.toLowerCase());
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-bg-0">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
<div>
|
|
<h1 className="text-xl font-bold text-text-1">유출유 확산 예측 목록</h1>
|
|
<p className="text-sm text-text-3 mt-1">총 {filteredIncidents.length}건</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button className="px-4 py-2 text-sm font-semibold border border-border rounded-md bg-bg-3 text-text-2 hover:bg-bg-hover hover:text-text-1 transition-all">
|
|
시간별 검색
|
|
</button>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
placeholder="검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-64 px-4 py-2 text-sm bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none"
|
|
/>
|
|
</div>
|
|
<button className="px-4 py-2 text-sm font-semibold rounded-md bg-primary-cyan text-bg-0 hover:shadow-[0_0_16px_rgba(6,182,212,0.3)] transition-all">
|
|
+ 새 분석
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 테이블 */}
|
|
<div className="flex-1 overflow-auto">
|
|
<table className="w-full">
|
|
<thead className="sticky top-0 bg-bg-1 border-b border-border z-10">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">번호</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">사고명</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">사고시각</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">선박유형</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">유종</th>
|
|
<th className="px-4 py-3 text-right text-xs font-bold text-text-3 uppercase tracking-wider">유출량</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">사고유형</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">상태</th>
|
|
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider">분석자</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border">
|
|
{filteredIncidents.map((incident) => (
|
|
<tr
|
|
key={incident.acdntSn}
|
|
className="hover:bg-bg-2 transition-colors cursor-pointer group"
|
|
>
|
|
<td className="px-4 py-3 text-sm text-text-2 font-mono">{incident.acdntSn}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-status-red animate-pulse" />
|
|
<span className="text-sm font-semibold text-text-1 group-hover:text-primary-cyan transition-colors">
|
|
{incident.acdntNm}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-text-2 font-mono">{incident.occrnDtm}</td>
|
|
<td className="px-4 py-3 text-sm text-text-2">{incident.vesselTp ?? '—'}</td>
|
|
<td className="px-4 py-3 text-sm text-text-2">{incident.oilTpCd ?? '—'}</td>
|
|
<td className="px-4 py-3 text-sm text-text-1 font-mono text-right font-semibold">
|
|
{incident.spilQty != null ? incident.spilQty.toFixed(2) : '—'}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-text-2">{incident.acdntTpCd}</td>
|
|
<td className="px-4 py-3">
|
|
<span className="px-2 py-1 text-xs font-semibold rounded-md bg-[rgba(168,85,247,0.15)] text-purple-400">
|
|
{incident.phaseCd}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-text-2">{incident.analystNm ?? '—'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|