216 lines
8.3 KiB
TypeScript
Executable File
216 lines
8.3 KiB
TypeScript
Executable File
import type { DispersionGridResult, WeatherFetchResult } from '../utils/dispersionTypes';
|
|
import { windDirToCompass } from '../hooks/useWeatherFetch';
|
|
|
|
interface HNSRightPanelProps {
|
|
dispersionResult: {
|
|
zones: Array<{
|
|
level: string;
|
|
color: string;
|
|
radius: number;
|
|
angle: number;
|
|
}>;
|
|
timestamp: string;
|
|
windDirection: number;
|
|
substance: string;
|
|
concentration: {
|
|
'AEGL-3': string;
|
|
'AEGL-2': string;
|
|
'AEGL-1': string;
|
|
};
|
|
} | null;
|
|
computedResult?: DispersionGridResult | null;
|
|
weatherData?: WeatherFetchResult | null;
|
|
onOpenRecalc?: () => void;
|
|
onOpenReport?: () => void;
|
|
onSave?: () => void;
|
|
}
|
|
|
|
export function HNSRightPanel({
|
|
dispersionResult,
|
|
computedResult,
|
|
weatherData,
|
|
onOpenRecalc,
|
|
onOpenReport,
|
|
onSave,
|
|
}: HNSRightPanelProps) {
|
|
if (!dispersionResult) {
|
|
return (
|
|
<div className="w-[300px] bg-bg-surface border-l border-stroke p-4 overflow-auto">
|
|
<div className="flex flex-col gap-3 items-center justify-center h-full text-fg-disabled text-xs">
|
|
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
|
|
<div>예측 실행 후 결과가 표시됩니다</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const area = computedResult?.aeglAreas.aegl1 ?? 0;
|
|
const maxConc = computedResult?.maxConcentration ?? 0;
|
|
const windSpd = weatherData?.windSpeed ?? 5.0;
|
|
const windDir = weatherData?.windDirection ?? dispersionResult.windDirection;
|
|
const modelLabel =
|
|
computedResult?.modelType === 'plume'
|
|
? 'Gaussian Plume'
|
|
: computedResult?.modelType === 'puff'
|
|
? 'Gaussian Puff'
|
|
: computedResult?.modelType === 'dense_gas'
|
|
? 'Dense Gas (B-M)'
|
|
: 'ALOHA';
|
|
|
|
return (
|
|
<div className="w-[300px] bg-bg-surface border-l border-stroke p-4 overflow-auto flex flex-col gap-4">
|
|
{/* Header */}
|
|
<div>
|
|
<div className="flex items-center gap-1.5 mb-2">
|
|
<div
|
|
style={{
|
|
width: '6px',
|
|
height: '6px',
|
|
borderRadius: '50%',
|
|
background: 'var(--color-warning)',
|
|
animation: 'pulse 1.5s infinite',
|
|
}}
|
|
></div>
|
|
<h3 className="text-[13px] font-bold m-0">예측 결과</h3>
|
|
</div>
|
|
<div className="text-[10px] text-fg-disabled font-mono">
|
|
{dispersionResult.substance} · {modelLabel}
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<div className="flex flex-col gap-2">
|
|
{/* 최대 농도 */}
|
|
<div className="p-3 bg-bg-card border border-[rgba(239,68,68,0.2)] rounded-[var(--radius-sm)]">
|
|
<div className="text-[10px] text-fg-disabled mb-1.5">최대 농도</div>
|
|
<div className="text-[20px] font-bold font-mono text-color-danger">
|
|
{maxConc > 0 ? maxConc.toFixed(1) : '—'}{' '}
|
|
<span className="text-[10px] font-medium">ppm</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 확산 면적 */}
|
|
<div className="p-3 bg-bg-card border border-[rgba(6,182,212,0.2)] rounded-[var(--radius-sm)]">
|
|
<div className="text-[10px] text-fg-disabled mb-1.5">AEGL-1 확산 면적</div>
|
|
<div className="text-[20px] font-bold font-mono text-color-accent">
|
|
{area > 0 ? area.toFixed(2) : '—'} <span className="text-[10px] font-medium">km²</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 풍속 */}
|
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-[10px] text-fg-disabled mb-1.5">풍속</div>
|
|
<div className="text-[20px] font-bold font-mono">
|
|
{windSpd.toFixed(1)} <span className="text-[10px] font-medium">m/s</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 풍향 */}
|
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-[10px] text-fg-disabled mb-1.5">풍향</div>
|
|
<div className="text-[20px] font-bold font-mono">
|
|
{windDirToCompass(windDir)} <span className="text-[10px] font-medium">{windDir}°</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AEGL Zone Details */}
|
|
<div>
|
|
<h4 className="text-[11px] font-semibold text-fg-sub mt-0 mb-2.5">AEGL 구역 상세</h4>
|
|
<div className="flex flex-col gap-2">
|
|
{/* AEGL-3 */}
|
|
<div
|
|
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
|
|
style={{ borderLeft: '3px solid rgba(239,68,68,1)' }}
|
|
>
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-[11px] font-semibold">AEGL-3 (생명위협)</span>
|
|
<span className="text-[10px] font-mono text-fg-disabled">
|
|
{computedResult?.aeglDistances.aegl3 || 0}m
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-[10px] text-fg-disabled">
|
|
<span>{dispersionResult.concentration['AEGL-3']}</span>
|
|
<span className="font-mono">{computedResult?.aeglAreas.aegl3 ?? 0} km²</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AEGL-2 */}
|
|
<div
|
|
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
|
|
style={{ borderLeft: '3px solid rgba(249,115,22,1)' }}
|
|
>
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-[11px] font-semibold">AEGL-2 (건강피해)</span>
|
|
<span className="text-[10px] font-mono text-fg-disabled">
|
|
{computedResult?.aeglDistances.aegl2 || 0}m
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-[10px] text-fg-disabled">
|
|
<span>{dispersionResult.concentration['AEGL-2']}</span>
|
|
<span className="font-mono">{computedResult?.aeglAreas.aegl2 ?? 0} km²</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AEGL-1 */}
|
|
<div
|
|
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
|
|
style={{ borderLeft: '3px solid rgba(234,179,8,1)' }}
|
|
>
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-[11px] font-semibold">AEGL-1 (불쾌감)</span>
|
|
<span className="text-[10px] font-mono text-fg-disabled">
|
|
{computedResult?.aeglDistances.aegl1 || 0}m
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-[10px] text-fg-disabled">
|
|
<span>{dispersionResult.concentration['AEGL-1']}</span>
|
|
<span className="font-mono">{computedResult?.aeglAreas.aegl1 ?? 0} km²</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 시간 정보 (puff/dense_gas) */}
|
|
{computedResult && computedResult.modelType !== 'plume' && (
|
|
<div className="p-2.5 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-[10px] text-fg-disabled mb-1">현재 시뮬레이션 시간</div>
|
|
<div className="text-[14px] font-bold font-mono text-color-accent">
|
|
t = {computedResult.timeStep}s
|
|
<span className="text-[10px] font-normal text-fg-disabled ml-1.5">
|
|
({(computedResult.timeStep / 60).toFixed(1)}분)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Timestamp */}
|
|
<div className="mt-auto pt-3 border-t border-stroke text-[10px] text-fg-disabled font-mono">
|
|
예측 시각: {new Date(dispersionResult.timestamp).toLocaleString('ko-KR')}
|
|
</div>
|
|
|
|
{/* Bottom Action Buttons */}
|
|
<div className="flex gap-1.5 pt-3 border-t border-stroke">
|
|
<button
|
|
onClick={onSave}
|
|
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean"
|
|
>
|
|
💾 저장
|
|
</button>
|
|
<button
|
|
onClick={onOpenRecalc}
|
|
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(249,115,22,0.1)] border border-[rgba(249,115,22,0.3)] text-color-warning font-korean"
|
|
>
|
|
🔄 재계산
|
|
</button>
|
|
<button
|
|
onClick={onOpenReport}
|
|
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-primary-cyan to-primary-blue text-white font-korean"
|
|
>
|
|
📄 보고서
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|