wing-ops/frontend/src/tabs/hns/components/HNSRightPanel.tsx

207 lines
8.0 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-full h-full bg-bg-surface border-l border-stroke p-4 overflow-auto flex items-center justify-center">
<div className="flex flex-col gap-3 items-center text-fg-disabled text-label-1">
<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-full 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-accent)',
animation: 'pulse 1.5s infinite',
}}
></div>
<h3 className="text-title-4 font-bold m-0"> </h3>
</div>
<div className="text-label-2 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-stroke rounded-[var(--radius-sm)]">
<div className="text-label-2 text-fg-disabled mb-1.5"> </div>
<div className="text-title-1 font-bold font-mono text-color-caution">
{maxConc > 0 ? maxConc.toFixed(1) : '—'}{' '}
<span className="text-label-2 font-medium">ppm</span>
</div>
</div>
{/* 확산 면적 */}
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
<div className="text-label-2 text-fg-disabled mb-1.5">AEGL-1 </div>
<div className="text-title-1 font-bold font-mono text-color-accent">
{area > 0 ? area.toFixed(2) : '—'} <span className="text-label-2 font-medium">km²</span>
</div>
</div>
{/* 풍속 */}
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
<div className="text-label-2 text-fg-disabled mb-1.5"></div>
<div className="text-title-1 font-bold font-mono">
{windSpd.toFixed(1)} <span className="text-label-2 font-medium">m/s</span>
</div>
</div>
{/* 풍향 */}
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
<div className="text-label-2 text-fg-disabled mb-1.5"></div>
<div className="text-title-1 font-bold font-mono">
{windDirToCompass(windDir)} <span className="text-label-2 font-medium">{windDir}°</span>
</div>
</div>
</div>
{/* AEGL Zone Details */}
<div>
<h4 className="text-label-2 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)]">
<div className="flex justify-between items-center mb-1">
<span className="text-label-2 font-semibold">AEGL-3 ()</span>
<span className="text-label-2 font-mono text-fg-disabled">
{computedResult?.aeglDistances.aegl3 || 0}m
</span>
</div>
<div className="flex justify-between text-label-2 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)]">
<div className="flex justify-between items-center mb-1">
<span className="text-label-2 font-semibold">AEGL-2 ()</span>
<span className="text-label-2 font-mono text-fg-disabled">
{computedResult?.aeglDistances.aegl2 || 0}m
</span>
</div>
<div className="flex justify-between text-label-2 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)]">
<div className="flex justify-between items-center mb-1">
<span className="text-label-2 font-semibold">AEGL-1 ()</span>
<span className="text-label-2 font-mono text-fg-disabled">
{computedResult?.aeglDistances.aegl1 || 0}m
</span>
</div>
<div className="flex justify-between text-label-2 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-label-2 text-fg-disabled mb-1"> </div>
<div className="text-title-3 font-bold font-mono text-color-accent">
t = {computedResult.timeStep}s
<span className="text-label-2 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-label-2 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-label-2 font-semibold bg-color-navy text-static-white hover:bg-color-navy-hover font-korean"
>
💾
</button>
<button
onClick={onOpenRecalc}
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-bg-elevated border border-stroke text-fg font-korean"
>
🔄
</button>
<button
onClick={onOpenReport}
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-color-navy text-static-white hover:bg-color-navy-hover font-korean"
>
📄
</button>
</div>
</div>
);
}