wing-ops/frontend/src/tabs/prediction/components/AnalysisListTable.tsx
htlee ff085252b0 feat(phase4): Board/HNS/Prediction/Aerial/Rescue Mock → API 전환
- Board: 매뉴얼 CRUD + 첨부파일 API (012_board_ext.sql)
- HNS: 분석 CRUD 5개 API (013_hns_analysis.sql)
- Prediction: 분석/역추적/오일펜스 7개 API (014_prediction.sql)
- Aerial: 미디어/CCTV/위성 6개 API + PostGIS (015_aerial.sql)
- Rescue: 구난 작전/시나리오 3개 API + JSONB (016_rescue.sql)
- backtrackMockData.ts 삭제

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

250 lines
10 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 { useState, useEffect, useCallback } from 'react'
import { fetchPredictionAnalyses } from '../services/predictionApi'
import type { PredictionAnalysis } from '../services/predictionApi'
export type Analysis = PredictionAnalysis
interface AnalysisListTableProps {
onTabChange: (tab: string) => void
onSelectAnalysis?: (analysis: Analysis) => void
}
export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisListTableProps) {
const [analyses, setAnalyses] = useState<Analysis[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const itemsPerPage = 10
const loadData = useCallback(async () => {
setLoading(true)
try {
const items = await fetchPredictionAnalyses({ search: searchTerm || undefined })
setAnalyses(items)
} catch (err) {
console.error('[prediction] 분석 목록 조회 실패:', err)
} finally {
setLoading(false)
}
}, [searchTerm])
useEffect(() => {
loadData()
}, [loadData])
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
return (
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(34,197,94,0.15)] text-green-400">
</span>
)
case 'running':
return (
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(249,115,22,0.15)] text-orange-400">
</span>
)
case 'pending':
return (
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(138,150,168,0.15)] text-text-3">
</span>
)
case 'error':
return (
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(239,68,68,0.15)] text-status-red">
</span>
)
default:
return null
}
}
const totalPages = Math.ceil(analyses.length / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const currentAnalyses = analyses.slice(startIndex, endIndex)
const renderPageNumbers = () => {
const pages = []
const maxVisible = 5
if (totalPages <= maxVisible) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i)
}
} else {
if (currentPage <= 3) {
for (let i = 1; i <= 5; i++) pages.push(i)
pages.push('...')
pages.push(totalPages)
} else if (currentPage >= totalPages - 2) {
pages.push(1)
pages.push('...')
for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i)
} else {
pages.push(1)
pages.push('...')
for (let i = currentPage - 1; i <= currentPage + 1; i++) pages.push(i)
pages.push('...')
pages.push(totalPages)
}
}
return pages.map((page, index) => {
if (page === '...') {
return (
<span key={`ellipsis-${index}`} className="px-3 py-1 text-text-3">
...
</span>
)
}
return (
<button
key={page}
onClick={() => setCurrentPage(page as number)}
className={`px-3 py-1 text-sm font-medium rounded transition-colors ${
currentPage === page
? 'bg-primary-cyan text-bg-0'
: 'text-text-2 hover:bg-bg-2'
}`}
>
{page}
</button>
)
})
}
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"> {analyses.length}</p>
</div>
<div className="flex items-center gap-3">
<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 onClick={() => onTabChange('analysis')} 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">
{loading ? (
<div className="text-center py-20 text-text-3 text-sm"> ...</div>
) : (
<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-center text-xs font-bold text-text-3 uppercase tracking-wider">KOSPS</th>
<th className="px-4 py-3 text-center text-xs font-bold text-text-3 uppercase tracking-wider">POSEIDON</th>
<th className="px-4 py-3 text-center text-xs font-bold text-text-3 uppercase tracking-wider">OpenDrift</th>
<th className="px-4 py-3 text-center 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">
{currentAnalyses.map((analysis) => (
<tr
key={analysis.acdntSn}
className="hover:bg-bg-2 transition-colors cursor-pointer group"
>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{analysis.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-primary-cyan hover:underline transition-all cursor-pointer"
onClick={(e) => {
e.stopPropagation()
if (onSelectAnalysis) {
onSelectAnalysis(analysis)
}
}}
>
{analysis.acdntNm}
</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{analysis.occurredAt ? new Date(analysis.occurredAt).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{analysis.duration}</td>
<td className="px-4 py-3 text-sm text-text-2">{analysis.oilType}</td>
<td className="px-4 py-3 text-sm text-text-1 font-mono text-right font-semibold">
{analysis.volume != null ? analysis.volume.toFixed(2) : '—'}
</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.kospsStatus)}</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.poseidonStatus)}</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.opendriftStatus)}</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.backtrackStatus)}</td>
<td className="px-4 py-3 text-sm text-text-2">{analysis.analyst}</td>
<td className="px-4 py-3 text-sm text-text-2">{analysis.officeName}</td>
</tr>
))}
</tbody>
</table>
)}
{!loading && analyses.length === 0 && (
<div className="text-center py-20 text-text-3 text-sm"> .</div>
)}
</div>
{/* 페이지네이션 */}
<div className="flex items-center justify-center gap-2 px-5 py-4 border-t border-border">
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="px-3 py-1 text-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1 text-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
{renderPageNumbers()}
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
)
}