대형 파일 집중 변환: - SatelliteRequest: 134→66 (hex 색상 일괄 변환) - IncidentsView: 141→90, MediaModal: 97→38 - HNSScenarioView: 78→38, HNSView: 49→31 - LoginPage, MapView, PredictionInputSection 등 중소 파일 8개 변환 패턴: hex 색상→text-[#hex], CSS 변수→Tailwind 유틸리티, flex/grid/padding/fontSize/fontWeight/overflow 등 정적 속성 className 이동 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 lines
10 KiB
TypeScript
Executable File
225 lines
10 KiB
TypeScript
Executable File
import { useState, useEffect, useCallback } from 'react'
|
|
import type { Dispatch, SetStateAction } from 'react'
|
|
import { fetchHnsAnalyses, type HnsAnalysisItem } from '../services/hnsApi'
|
|
|
|
interface HNSAnalysisListTableProps {
|
|
onTabChange: Dispatch<SetStateAction<'analysis' | 'list'>>
|
|
}
|
|
|
|
const RISK_LABEL: Record<string, string> = {
|
|
CRITICAL: '심각',
|
|
HIGH: '위험',
|
|
MEDIUM: '경고',
|
|
LOW: '관찰',
|
|
}
|
|
|
|
const RISK_STYLE: Record<string, { bg: string; color: string }> = {
|
|
CRITICAL: { bg: 'rgba(239,68,68,0.2)', color: 'var(--red)' },
|
|
HIGH: { bg: 'rgba(239,68,68,0.15)', color: 'var(--red)' },
|
|
MEDIUM: { bg: 'rgba(249,115,22,0.15)', color: 'var(--orange)' },
|
|
LOW: { bg: 'rgba(34,197,94,0.15)', color: 'var(--green)' },
|
|
}
|
|
|
|
function formatDate(dtm: string | null, mode: 'full' | 'date') {
|
|
if (!dtm) return '—'
|
|
const d = new Date(dtm)
|
|
if (mode === 'date') return d.toISOString().slice(0, 10)
|
|
return d.toISOString().slice(0, 16).replace('T', ' ')
|
|
}
|
|
|
|
function substanceTag(sbstNm: string | null): string {
|
|
if (!sbstNm) return '—'
|
|
const match = sbstNm.match(/\(([^)]+)\)/)
|
|
if (match) return match[1]
|
|
return sbstNm.length > 6 ? sbstNm.slice(0, 6) : sbstNm
|
|
}
|
|
|
|
export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps) {
|
|
const [analyses, setAnalyses] = useState<HnsAnalysisItem[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
const loadData = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const items = await fetchHnsAnalyses()
|
|
setAnalyses(items)
|
|
} catch (err) {
|
|
console.error('[hns] 분석 목록 조회 실패:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [loadData])
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-bg-0">
|
|
{/* Header */}
|
|
<div className="px-5 py-4 border-b border-border flex justify-between items-center bg-bg-1">
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-base font-bold flex items-center gap-2">
|
|
<span className="text-[18px]">📋</span>
|
|
HNS 대기확산 분석 목록
|
|
</div>
|
|
<span className="text-[10px] text-text-3 bg-bg-3 font-mono px-[10px] py-1 rounded-xl">
|
|
총 {analyses.length}건
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<input
|
|
type="text"
|
|
placeholder="검색..."
|
|
className="bg-bg-3 border border-border rounded-sm text-[11px] px-3 py-2 w-[200px]"
|
|
/>
|
|
<button
|
|
onClick={() => onTabChange('analysis')}
|
|
className="text-status-orange text-[11px] font-semibold cursor-pointer flex items-center gap-1 px-4 py-2 rounded-sm"
|
|
style={{
|
|
border: '1px solid rgba(249,115,22,0.3)',
|
|
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(239,68,68,0.1))',
|
|
}}
|
|
>
|
|
<span className="text-sm">+</span> 새 분석
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="flex-1 overflow-auto">
|
|
{loading ? (
|
|
<div className="text-center text-text-3 text-[12px] py-20">로딩 중...</div>
|
|
) : (
|
|
<table className="w-full text-[11px] border-collapse">
|
|
<thead className="sticky top-0 z-10">
|
|
<tr className="bg-bg-2 border-b border-border">
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[50px]">번호</th>
|
|
<th className="text-left text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[180px]">분석명</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]">물질</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[130px]">사고일시</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]">분석날짜</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]">사고지점</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]">유출량</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]">알고리즘</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">예측시간</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
|
<div className="flex flex-col items-center gap-0.5">
|
|
<span>AEGL-3</span>
|
|
<span className="text-[8px] text-text-3">생명위협</span>
|
|
</div>
|
|
</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
|
<div className="flex flex-col items-center gap-0.5">
|
|
<span>AEGL-2</span>
|
|
<span className="text-[8px] text-text-3">건강피해</span>
|
|
</div>
|
|
</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
|
<div className="flex flex-col items-center gap-0.5">
|
|
<span>AEGL-1</span>
|
|
<span className="text-[8px] text-text-3">불쾌감</span>
|
|
</div>
|
|
</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]">위험등급</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]">피해반경</th>
|
|
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]">분석자</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{analyses.map((item, index) => {
|
|
const rslt = item.rsltData as Record<string, unknown> | null
|
|
const riskLabel = RISK_LABEL[item.riskCd || ''] || item.riskCd || '—'
|
|
const riskStyle = RISK_STYLE[item.riskCd || ''] || { bg: 'rgba(100,100,100,0.1)', color: 'var(--t3)' }
|
|
const aegl3 = rslt?.aegl3 as boolean | undefined
|
|
const aegl2 = rslt?.aegl2 as boolean | undefined
|
|
const aegl1 = rslt?.aegl1 as boolean | undefined
|
|
const damageRadius = (rslt?.damageRadius as string) || '—'
|
|
const amount = item.spilQty != null ? `${item.spilQty} ${item.spilUnitCd || 'KL'}` : '—'
|
|
|
|
return (
|
|
<tr
|
|
key={item.hnsAnlysSn}
|
|
className="border-b border-border cursor-pointer"
|
|
style={{
|
|
transition: 'background 0.15s',
|
|
background: index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'
|
|
}}
|
|
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg2)'}
|
|
onMouseLeave={(e) => e.currentTarget.style.background = index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'}
|
|
>
|
|
<td className="text-center text-text-3 font-mono px-4 py-3">{item.hnsAnlysSn}</td>
|
|
<td className="font-medium px-4 py-3">{item.anlysNm}</td>
|
|
<td className="text-center px-4 py-3">
|
|
<span
|
|
className="text-[9px] font-semibold text-status-orange px-2 py-1 rounded"
|
|
style={{ background: 'rgba(249,115,22,0.12)' }}
|
|
>
|
|
{substanceTag(item.sbstNm)}
|
|
</span>
|
|
</td>
|
|
<td className="text-center text-text-2 font-mono text-[10px] px-4 py-3">{formatDate(item.acdntDtm, 'full')}</td>
|
|
<td className="text-center text-text-3 font-mono text-[10px] px-4 py-3">{formatDate(item.regDtm, 'date')}</td>
|
|
<td className="text-center text-text-2 px-4 py-3">{item.locNm || '—'}</td>
|
|
<td className="text-center text-text-2 font-mono px-4 py-3">{amount}</td>
|
|
<td className="text-center px-4 py-3">
|
|
<span
|
|
className="text-[9px] font-semibold text-primary-cyan px-2 py-1 rounded"
|
|
style={{ background: 'rgba(6,182,212,0.12)' }}
|
|
>
|
|
{item.algoCd || '—'}
|
|
</span>
|
|
</td>
|
|
<td className="text-center text-text-2 font-mono px-4 py-3">{item.fcstHr ? `${item.fcstHr}H` : '—'}</td>
|
|
<td className="text-center px-4 py-3">
|
|
<div
|
|
className="w-6 h-6 rounded-full mx-auto"
|
|
style={{
|
|
background: aegl3 ? 'rgba(239,68,68,0.8)' : 'rgba(255,255,255,0.1)',
|
|
border: aegl3 ? 'none' : '1px solid var(--bd)'
|
|
}}
|
|
/>
|
|
</td>
|
|
<td className="text-center px-4 py-3">
|
|
<div
|
|
className="w-6 h-6 rounded-full mx-auto"
|
|
style={{
|
|
background: aegl2 ? 'rgba(249,115,22,0.8)' : 'rgba(255,255,255,0.1)',
|
|
border: aegl2 ? 'none' : '1px solid var(--bd)'
|
|
}}
|
|
/>
|
|
</td>
|
|
<td className="text-center px-4 py-3">
|
|
<div
|
|
className="w-6 h-6 rounded-full mx-auto"
|
|
style={{
|
|
background: aegl1 ? 'rgba(234,179,8,0.8)' : 'rgba(255,255,255,0.1)',
|
|
border: aegl1 ? 'none' : '1px solid var(--bd)'
|
|
}}
|
|
/>
|
|
</td>
|
|
<td className="text-center px-4 py-3">
|
|
<span
|
|
className="text-[9px] font-semibold px-[10px] py-1 rounded"
|
|
style={{ background: riskStyle.bg, color: riskStyle.color }}
|
|
>
|
|
{riskLabel}
|
|
</span>
|
|
</td>
|
|
<td className="text-center text-text-2 font-mono px-4 py-3">{damageRadius}</td>
|
|
<td className="text-center text-text-3 text-[10px] px-4 py-3">{item.analystNm || '—'}</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
|
|
{!loading && analyses.length === 0 && (
|
|
<div className="text-center text-text-3 text-[12px] py-20">분석 데이터가 없습니다.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|